Files
plotter/tests/test_series.cpp
T
Martino Ferrari 5f1a4991e2 fix(data): remove MinMaxPyramid::reduce, repoint its two test callers
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
2026-09-27 21:45:03 +02:00

131 lines
4.5 KiB
C++

#include "harness.hpp"
#include <plot/data/array_series.hpp>
#include <plot/data/stream_series.hpp>
#include <algorithm>
#include <cmath>
#include <random>
#include <thread>
#include <vector>
PLOT_TEST(array_series_bounds_and_gather) {
std::vector<f32> y{1, 5, -2, 3};
plot::data::ArraySeries s(std::move(y), /*x0=*/10.0, /*dx=*/0.5);
PLOT_CHECK(s.size() == 4);
PLOT_CHECK_NEAR(s.x_bounds().min, 10.0, 1e-9);
PLOT_CHECK_NEAR(s.x_bounds().max, 11.5, 1e-9);
PLOT_CHECK_NEAR(s.y_bounds().min, -2.0, 1e-6);
std::vector<f32> out;
s.gather(0, 0, 4, out);
PLOT_CHECK(out.size() == 8); // interleaved x,y
PLOT_CHECK_NEAR(out[0], 10.0, 1e-6);
PLOT_CHECK_NEAR(out[1], 1.0, 1e-6);
PLOT_CHECK_NEAR(out[6], 11.5, 1e-6);
}
PLOT_TEST(stream_series_acquire_publishes_snapshot) {
plot::data::StreamSeries s(1 << 12, /*x0=*/0.0, /*dx=*/1e-6);
for (int i = 0; i < 100; ++i) s.push(f32(i));
PLOT_CHECK(s.produced() == 100);
s.acquire();
PLOT_CHECK(s.size() == 100);
// Pyramid must track the published samples.
PLOT_CHECK_NEAR(s.reduce(0, 100).max, 99.0, 1e-4);
for (int i = 100; i < 250; ++i) s.push(f32(i));
s.acquire();
PLOT_CHECK(s.size() == 250);
}
PLOT_TEST(stream_series_drops_oldest_when_full) {
plot::data::StreamSeries s(256, 0.0, 1.0);
for (int i = 0; i < 1000; ++i) s.push(f32(i));
s.acquire();
PLOT_CHECK(s.size() <= 256);
PLOT_CHECK_NEAR(s.y_bounds().max, 999.0, 1e-4);
}
PLOT_TEST(array_series_exact_range_queries_match_bruteforce) {
// The strong test for the exactness split: arbitrary, deliberately
// unaligned ranges must match a brute-force scan of the raw samples.
std::mt19937 rng(7);
std::uniform_real_distribution<float> d(-5.0f, 5.0f);
std::vector<f32> y(4000);
for (auto& v : y) v = d(rng);
plot::data::ArraySeries s(y, 0.0, 1.0);
for (u64 a = 0; a < 4000; a += 173) {
for (u64 b = a + 1; b <= 4000; b += 331) {
float lo = y[a], hi = y[a];
double sum = 0.0, sq = 0.0;
for (u64 i = a; i < b; ++i) {
lo = std::min(lo, y[i]); hi = std::max(hi, y[i]);
sum += y[i]; sq += double(y[i]) * double(y[i]);
}
const plot::Range r = s.reduce(a, b);
PLOT_CHECK_NEAR(r.min, lo, 1e-4);
PLOT_CHECK_NEAR(r.max, hi, 1e-4);
PLOT_CHECK_NEAR(s.sum(a, b), sum, 1e-2);
PLOT_CHECK_NEAR(s.sumsq(a, b), sq, 1e-1);
}
}
}
PLOT_TEST(stream_series_exact_queries_survive_ring_wrap) {
plot::data::StreamSeries s(1u << 10, 0.0, 1.0);
constexpr u64 kPushes = 3000;
constexpr u64 kFirstRetained = kPushes - (1u << 10); // 1976
for (u64 i = 0; i < kPushes; ++i) s.push(f32(i % 97)); // wraps the ring twice
s.acquire();
const u64 n = s.size();
PLOT_CHECK(n == 1u << 10);
PLOT_CHECK_NEAR(s.x0(), 1976.0, 1e-9);
PLOT_CHECK(s.x_bounds().min == s.x0());
PLOT_CHECK(s.pyramid().size() == n);
const plot::Range r = s.reduce(0, n); // retained window, wrapped
PLOT_CHECK_NEAR(r.max, 96.0, 1e-4);
PLOT_CHECK_NEAR(r.min, 0.0, 1e-4);
double sum = 0.0, sumsq = 0.0;
for (u64 i = kFirstRetained; i < kPushes; ++i) {
const double v = static_cast<double>(i % 97);
sum += v;
sumsq += v * v;
}
PLOT_CHECK_NEAR(s.sum(0, n), sum, 1e-2);
PLOT_CHECK_NEAR(s.sumsq(0, n), sumsq, 1e-1);
std::vector<f32> out;
s.gather(0, 0, n, out); // level 0: raw samples
PLOT_CHECK(out.size() == 2 * n);
PLOT_CHECK_NEAR(out[0], s.x0(), 1e-6);
PLOT_CHECK_NEAR(out[1], f32(kFirstRetained % 97), 1e-6);
PLOT_CHECK_NEAR(out[2 * n - 2], s.x0() + f64(n - 1), 1e-6);
}
PLOT_TEST(stream_series_push_and_read_are_race_free) {
// The ring overwrites, so the producer reuses the slot of the snapshot's oldest
// retained sample while the consumer may still be reading it. Relaxed atomic
// slots make that defined; nothing else in the suite has two threads, so this
// is the case ThreadSanitizer actually flags when that is not the case.
plot::data::StreamSeries s(1u << 8, 0.0, 1.0);
constexpr u64 kPushes = 50'000;
std::thread producer([&s] {
for (u64 i = 0; i < kPushes; ++i) s.push(f32(i % 971));
});
u64 reads = 0;
while (s.produced() < kPushes) {
s.acquire();
const u64 n = s.size();
if (n == 0) continue;
(void)s.reduce(0, n);
(void)s.sum(0, n);
(void)s.sumsq(0, n);
std::vector<f32> out;
s.gather(0, 0, n, out); // reads the oldest edge
PLOT_CHECK(out.size() == 2 * n);
++reads;
}
producer.join();
PLOT_CHECK(s.produced() == kPushes);
PLOT_CHECK(reads > 0);
}