The repository had no licence at all, so every source was all-rights-reserved by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino Ferrari) and a two-line SPDX header to each of the 113 first-party sources -- 48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the grant travels with each file rather than relying on the root document alone. Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely vendored code is untouched: third_party/stb_truetype.h carries no header of ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py emit() now writes the two lines into its header comment and the file was regenerated -- gen_atlas.py --check passes and the regeneration adds only those comment lines to the previously committed file. README's provenance section now states MIT and links LICENSE. No code, test or public API change.
153 lines
7.3 KiB
C++
153 lines
7.3 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#pragma once
|
|
#include <deque>
|
|
#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, 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 and not by the number of samples ever appended.
|
|
/// A bucket entry is 24 bytes -- `MinMax` plus the exact `sum`/`sumsq` the exact
|
|
/// range queries fold over -- so level 0 costs 0.375n bytes and the geometric tail
|
|
/// brings the whole pyramid to about 0.75n bytes, ~19% of the 4n bytes of raw
|
|
/// samples. (A min/max-only index would be 0.25n, ~6%: the two doubles are the
|
|
/// price of answering mean/RMS/integral from whole buckets instead of raw
|
|
/// samples.) 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, and a no-op only when `absolute_sample <= window_base()`: an empty
|
|
/// pyramid is not exempt, because a caller whose window already starts past
|
|
/// everything appended so far has to be able to say so (that is exactly what a
|
|
/// ring that saturated before the first acquire() passes here).
|
|
///
|
|
/// An argument past `window_end()` -- a producer that outran the consumer by
|
|
/// more than the ring held -- drops every aggregate and restarts the window at
|
|
/// `absolute_sample`: the samples in between were never appended, so they are
|
|
/// not representable, and the window the caller reads is the new one.
|
|
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 its own exact sum and sum of squares. The two
|
|
/// doubles take a bucket from 8 bytes to 24, which is what lets mean/RMS/
|
|
/// integral come from whole buckets instead of raw samples.
|
|
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<Entry> 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<usize>(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<Level> levels_{1};
|
|
Entry pending_; ///< the in-progress level-0 bucket, not yet an entry
|
|
};
|
|
|
|
} // namespace plot::data
|