Files
plotter/include/plot/data/minmax_pyramid.hpp
T

83 lines
3.4 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.
/// reduce() is the one non-*_buckets query: it is the *covering* aggregate over
/// the level-0 buckets that [begin, end) touches. Use it to ask which buckets a
/// range spans, never to measure a range.
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;
/// Min/max over the covering level-0 buckets: every bucket that holds a sample
/// of [begin, end). Exact only when the range is bucket-aligned.
Range reduce(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