feat(data): SPSC ring buffer, incremental min/max LOD pyramid, series types

This commit is contained in:
Martino Ferrari committed 2026-09-27 21:29:59 +02:00
1 parent c93c98002b
commit 138fa478c3
15 files changed
+1182 -8

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
#include "harness.hpp"
#include <plot/data/minmax_pyramid.hpp>
#include <random>
using plot::data::kBucket;
using plot::data::MinMaxPyramid;
using plot::Range;
PLOT_TEST(pyramid_bucket_extremes_preserved) {
MinMaxPyramid p;
p.reserve(1000);
for (int i = 0; i < 1000; ++i) p.append(i == 617 ? -50.0f : (i == 42 ? 900.0f : 1.0f));
PLOT_CHECK(p.size() == 1000);
const Range r = p.reduce_buckets(0, 960); // exactly 15 whole buckets
PLOT_CHECK_NEAR(r.min, -50.0, 1e-6);
PLOT_CHECK_NEAR(r.max, 900.0, 1e-6);
}
PLOT_TEST(pyramid_reduce_buckets_matches_bruteforce_on_aligned_ranges) {
std::mt19937 rng(1234);
std::uniform_real_distribution<float> d(-100.0f, 100.0f);
std::vector<float> v(5000);
MinMaxPyramid p;
for (auto& x : v) { x = d(rng); p.append(x); }
for (u64 a = 0; a < 5000; a += kBucket * 3) {
for (u64 b = a + kBucket; b <= 5000; b += kBucket * 7) {
const u64 ahi = (b / kBucket) * kBucket; // whole buckets only
if (ahi <= a) continue;
float lo = v[a], hi = v[a];
for (u64 i = a; i < ahi; ++i) { lo = std::min(lo, v[i]); hi = std::max(hi, v[i]); }
const Range r = p.reduce_buckets(a, ahi);
PLOT_CHECK_NEAR(r.min, lo, 1e-4);
PLOT_CHECK_NEAR(r.max, hi, 1e-4);
}
}
}
PLOT_TEST(pyramid_reduce_buckets_excludes_partial_edges) {
// Bucket 1 covers samples [64,128). reduce_buckets answers in whole buckets
// only, so the spike must show up in its own bucket and nowhere else.
MinMaxPyramid p;
for (int i = 0; i < 128; ++i) p.append(i == 70 ? 999.0f : 1.0f);
PLOT_CHECK_NEAR(p.reduce_buckets(64, 128).max, 999.0, 1e-6);
PLOT_CHECK_NEAR(p.reduce_buckets(0, 64).max, 1.0, 1e-6);
PLOT_CHECK(p.reduce_buckets(64, 64).span() == 0.0); // empty -> degenerate
}
PLOT_TEST(pyramid_aligned_queries_never_include_a_partial_bucket) {
// Corpus of 100 samples: bucket 0 is whole, bucket 1 (samples 64..127) is
// partial. A whole-bucket query must not reach into the partial one, in
// either direction.
MinMaxPyramid p;
for (int i = 0; i < 100; ++i) p.append(i == 99 ? 777.0f : 1.0f);
PLOT_CHECK(p.reduce_buckets(64, 100).span() == 0.0); // no whole bucket inside
PLOT_CHECK_NEAR(p.reduce_buckets(0, 100).max, 1.0, 1e-6); // bucket 0 only
PLOT_CHECK(p.sum_buckets(64, 100) == 0.0);
PLOT_CHECK(p.sumsq_buckets(64, 100) == 0.0);
}
PLOT_TEST(pyramid_level_selection) {
MinMaxPyramid p;
p.reserve(1u << 20);
for (u64 i = 0; i < (1u << 20); ++i) p.append(0.0f);
// <= kBucket samples per pixel must use level 0 (raw path handles it anyway).
PLOT_CHECK(p.level_for(1u << 20, 1.0) == 0);
// 512 samples per pixel with kBucket == 64 → bucket size 512 → level 3.
PLOT_CHECK(p.level_for(1u << 20, 512.0) == 3);
// Level selection must never exceed the deepest level that has data.
PLOT_CHECK(p.level_for(1000, 1e9) <= p.level_count() - 1);
}
PLOT_TEST(pyramid_snap_returns_nearest_sample) {
MinMaxPyramid p;
for (int i = 0; i < 100; ++i) p.append(float(i));
PLOT_CHECK(p.snap(10.4, 0.0, 1.0) == 10);
PLOT_CHECK(p.snap(10.6, 0.0, 1.0) == 11);
PLOT_CHECK(p.snap(-5.0, 0.0, 1.0) == 0); // clamped
PLOT_CHECK(p.snap(1e9, 0.0, 1.0) == 99); // clamped
}
PLOT_TEST(pyramid_sums_match_bruteforce_on_aligned_ranges) {
std::mt19937 rng(99);
std::uniform_real_distribution<float> d(-3.0f, 7.0f);
std::vector<float> v(3000);
MinMaxPyramid p;
for (auto& x : v) { x = d(rng); p.append(x); }
for (u64 a = 0; a < 3000; a += kBucket * 5) {
for (u64 b = a + kBucket; b <= 3000; b += kBucket * 9) {
double s = 0.0, sq = 0.0;
for (u64 i = a; i < b; ++i) { s += v[i]; sq += double(v[i]) * double(v[i]); }
PLOT_CHECK_NEAR(p.sum_buckets(a, b), s, 1e-3);
PLOT_CHECK_NEAR(p.sumsq_buckets(a, b), sq, 1e-2);
}
}
}
PLOT_TEST(pyramid_clear_and_rebuild) {
MinMaxPyramid p;
for (int i = 0; i < 500; ++i) p.append(1.0f);
p.clear();
PLOT_CHECK(p.size() == 0);
PLOT_CHECK(p.level_count() <= 1);
p.append(7.0f);
PLOT_CHECK(p.size() == 1);
PLOT_CHECK_NEAR(p.reduce(0, 1).max, 7.0, 1e-6);
}