The pyramid's public query surface is the bucket-grained trio (reduce_buckets/sum_buckets/sumsq_buckets); reduce() was the one non-*_buckets query and silently over-covered non-aligned ranges, which is exactly the misuse the exactness split exists to prevent. Exact range queries stay on Series. - delete the declaration, its doc paragraph and the definition; no helper became dead (fold_range/cover_buckets/ceil_to_bucket are all still used by the *_buckets queries) - pyramid_clear_and_rebuild: complete bucket 0 and assert its extrema via reduce_buckets(0, kBucket) -- a pyramid has no exact sub-bucket query - stream_series_acquire_publishes_snapshot: s.pyramid().reduce(0, 100) -> s.reduce(0, 100), keeping the == 99.0 assertion
75 lines
3.0 KiB
C++
75 lines
3.0 KiB
C++
#pragma once
|
|
#include <span>
|
|
#include <vector>
|
|
|
|
#include <plot/core/types.hpp>
|
|
|
|
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.
|
|
///
|
|
/// Level 0 buckets kBucket consecutive samples and keeps one (min, max) per
|
|
/// bucket; level l buckets 2^l level-0 buckets. Memory is n/4 bytes -- about 6%
|
|
/// of 4-byte raw samples. append() merges into the last bucket at each level and
|
|
/// walks up the O(log n) chain above it, so building is amortised O(1) per sample
|
|
/// and nothing is ever recomputed from raw samples.
|
|
///
|
|
/// 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() { add_level(); }
|
|
|
|
/// Sizes every level (buckets and prefix sums) for `n` samples so a following
|
|
/// run of appends performs no reallocation. The pyramid itself stays empty.
|
|
void reserve(u64 n);
|
|
void append(f32 v);
|
|
void clear();
|
|
|
|
u64 size() const noexcept { return size_; }
|
|
/// At least 1, even when empty, so level_count() - 1 is always valid.
|
|
u32 level_count() const noexcept { return live_levels_; }
|
|
/// Level `l`'s buckets; empty span when `l` is beyond level_count().
|
|
std::span<const MinMax> level(u32 l) const;
|
|
|
|
/// Min/max over the whole level-0 buckets fully contained in [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:
|
|
void add_level();
|
|
|
|
u64 size_ = 0;
|
|
u64 reserved_n_ = 0;
|
|
u32 live_levels_ = 1;
|
|
std::vector<std::vector<MinMax>> levels_; // levels_[0] = kBucket granularity
|
|
std::vector<std::vector<f64>> sums_, sumsqs_; // inclusive prefix sums per level
|
|
};
|
|
|
|
} // namespace plot::data
|