#pragma once #include #include #include namespace plot::data { /// Aggregate of one bucket: the extremes of the samples it covers. struct MinMax { f32 min = 0.0f; f32 max = 0.0f; }; /// Samples per level-0 bucket. Fixed so the LOD geometry is identical for every /// series and the level maths is a shift. inline constexpr u32 kBucket = 64; /// Incremental min/max LOD pyramid, indexed in absolute bucket space. /// /// Level `l` bucket `k` covers the absolute samples /// `[k * (kBucket << l), (k + 1) * (kBucket << l))`. Bucket boundaries are /// therefore fixed for the pyramid's whole life: appending never re-buckets an /// existing aggregate, and the window a caller reads can slide over the same /// buckets instead of being rebuilt from the raw samples. /// /// Each level holds its complete buckets in a deque (O(1) push_back *and* /// pop_front) plus a `base`, the absolute level-`l` bucket index of its first /// entry. `append()` folds a sample into the in-progress level-0 bucket and, when /// that bucket completes, pushes it and walks up -- stopping at the first level /// that gains no new entry. Level `l` gains one entry per `kBucket << l` samples, /// so an append costs amortised O(1) and never walks every live level. /// /// `drop_before(u)` slides the window's leading edge to absolute sample `u`: each /// level discards the buckets whose span starts before `u`, so the memory is /// bounded by the retained window (about n/4 bytes, ~6% of 4-byte raw samples) /// and not by the number of samples ever appended. Every stored entry is the /// complete aggregate of its own span and every stored span lies inside the /// retained window, so no aggregate is ever recomputed or invalidated. /// /// Queries are in *window* coordinates: index 0 is the oldest retained sample, /// `size()` is the retained sample count, and `window_base()` is the absolute /// index window index 0 refers to. That is the same window `Series` publishes, /// which is why a `StreamSeries` can hand the pyramid a moving base and keep /// answering in its own coordinates. /// /// A pyramid stores *aggregates*, so it can only answer exactly for whole /// buckets; a range with partial edge buckets would silently pull in samples /// outside the range. Every query here is therefore bucket-grained and named /// *_buckets, and exact range queries live on Series, which owns the raw samples. class MinMaxPyramid { public: MinMaxPyramid() = default; /// Sizes the level table for a window of `n` samples so a following run of /// appends never grows the level vector. The bucket deques themselves grow in /// chunks: a sliding window cannot know their final length up front. void reserve(u64 n); void append(f32 v); /// Slides the retained window's leading edge to absolute sample index /// `absolute_sample`, dropping every aggregate that covered a sample before it. /// Monotonic for arguments ahead of the base; a no-op for arguments at or /// behind it, and for an empty pyramid. /// /// An argument past `window_end()` (a producer that outran the consumer by more /// than the ring held) drops everything and restarts the window there: the /// samples in between were never appended, so they are not representable. void drop_before(u64 absolute_sample); void clear(); /// Samples in the retained window. u64 size() const noexcept { return count_ - base_; } /// Absolute index of the sample window index 0 refers to. u64 window_base() const noexcept { return base_; } /// Absolute index one past the last appended sample: the first sample index a /// following append() will occupy. u64 window_end() const noexcept { return count_; } /// At least 1, even when empty, so level_count() - 1 is always valid. u32 level_count() const noexcept { return live_levels_; } /// Absolute level-`l` bucket index of level `l`'s first entry, and how many /// entries it holds. They cover `[level_base(l), level_base(l) + level_size(l))`: /// entry `i` is the aggregate of the absolute samples /// `[i * (kBucket << l), (i + 1) * (kBucket << l))`. A level that exists but /// holds nothing, or a level past level_count(), reports size 0 (its base is /// then meaningless). u64 level_base(u32 l) const noexcept; u64 level_size(u32 l) const noexcept; /// Aggregate of the absolute level-`l` bucket `i`. Precondition: /// level_base(l) <= i < level_base(l) + level_size(l). MinMax bucket(u32 l, u64 i) const; /// Min/max over the whole level-0 buckets fully contained in the window range /// [begin, end). Partial edge buckets are excluded, never approximated; an /// empty result is a degenerate Range with span() == 0. Range reduce_buckets(u64 begin, u64 end) const; /// Sum of the samples in those same whole buckets, accumulated in double. f64 sum_buckets(u64 begin, u64 end) const; /// Sum of squares of the samples in those same whole buckets. f64 sumsq_buckets(u64 begin, u64 end) const; /// Pyramid level whose buckets hold about `samples_per_pixel` samples, for a /// window of `n` samples: 0 when a pixel covers at most one bucket. Clamped to /// the levels that exist, to levels with more than one bucket, and to levels /// that give the window at least two buckets. u32 level_for(u64 n, f64 samples_per_pixel) const; /// Index of the sample nearest x on the uniform grid (x0 + i*dx), clamped to /// [0, size() - 1]. Returns 0 for an empty pyramid or a non-positive dx. u64 snap(f64 x, f64 x0, f64 dx) const; private: /// One bucket: its extremes plus the raw-sum aggregates Series folds over. struct Entry { MinMax mm; f64 sum = 0.0; f64 sumsq = 0.0; }; /// One level's complete buckets, in absolute bucket-index order. struct Level { std::deque entries; u64 base = 0; ///< absolute level-l bucket index of entries.front() }; void add_level(); /// Appends `e` at absolute level-`l` bucket `k`, which must be one past the /// level's last entry. void push(u32 l, u64 k, const Entry& e); /// Entry for absolute level-`l` bucket `i`; the caller has checked the range. const Entry& at(u32 l, u64 i) const { return levels_[l].entries[static_cast(i - levels_[l].base)]; } u64 count_ = 0; ///< absolute index one past the last appended sample u64 base_ = 0; ///< absolute index of the window's oldest sample u32 live_levels_ = 1; std::vector levels_{1}; Entry pending_; ///< the in-progress level-0 bucket, not yet an entry }; } // namespace plot::data