feat(data): SPSC ring buffer, incremental min/max LOD pyramid, series types
This commit is contained in:
1 parent
c93c98002b
commit
138fa478c3
15 files changed
+1182
-8
No files matched your search
@@ -26,6 +26,9 @@ add_executable(plot_tests
|
||||
main.cpp
|
||||
test_transform.cpp
|
||||
test_gl_context.cpp
|
||||
test_ring_buffer.cpp
|
||||
test_minmax_pyramid.cpp
|
||||
test_series.cpp
|
||||
egl_headless.cpp
|
||||
)
|
||||
target_link_libraries(plot_tests PRIVATE plot::core ${plot_egl_library})
|
||||
|
||||
+19
-8
@@ -35,6 +35,23 @@ struct Failure {
|
||||
std::string msg;
|
||||
};
|
||||
|
||||
/// Compares two doubles within `eps`. `PLOT_CHECK_NEAR(a, b)` leaves it to the
|
||||
/// default below; pass an explicit tolerance for computed values.
|
||||
inline constexpr double kNearEps = 1e-9;
|
||||
|
||||
inline void check_near(double a, double b, double eps, const char* file, int line,
|
||||
const char* lhs, const char* rhs) {
|
||||
if (!(std::fabs(a - b) <= eps)) {
|
||||
throw Failure{std::string(file) + ":" + std::to_string(line) + " CHECK_NEAR(" + lhs + ", " +
|
||||
rhs + ") got " + std::to_string(a) + " vs " + std::to_string(b)};
|
||||
}
|
||||
}
|
||||
|
||||
inline void check_near(double a, double b, const char* file, int line, const char* lhs,
|
||||
const char* rhs) {
|
||||
check_near(a, b, kNearEps, file, line, lhs, rhs);
|
||||
}
|
||||
|
||||
inline int run_all() {
|
||||
int failed = 0;
|
||||
for (auto& c : cases()) {
|
||||
@@ -64,11 +81,5 @@ inline int run_all() {
|
||||
" CHECK(" #cond ")"}; \
|
||||
} while (0)
|
||||
|
||||
#define PLOT_CHECK_NEAR(a, b, eps) \
|
||||
do { \
|
||||
const double _a = (a), _b = (b); \
|
||||
if (!(std::fabs(_a - _b) <= (eps))) \
|
||||
throw ::plottest::Failure{std::string(__FILE__) + ":" + std::to_string(__LINE__) + \
|
||||
" CHECK_NEAR(" #a ", " #b ") got " + std::to_string(_a) + \
|
||||
" vs " + std::to_string(_b)}; \
|
||||
} while (0)
|
||||
#define PLOT_CHECK_NEAR(a, b, ...) \
|
||||
::plottest::check_near((a), (b) __VA_OPT__(,) __VA_ARGS__, __FILE__, __LINE__, #a, #b)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "harness.hpp"
|
||||
#include <plot/data/ring_buffer.hpp>
|
||||
|
||||
PLOT_TEST(ring_wraps_and_keeps_latest) {
|
||||
plot::data::RingBuffer<f32> r(8); // capacity rounded up to a power of two
|
||||
for (int i = 0; i < 20; ++i) r.push(f32(i));
|
||||
PLOT_CHECK(r.size() == 8);
|
||||
PLOT_CHECK_NEAR(r[0], 12.0); // oldest retained
|
||||
PLOT_CHECK_NEAR(r[7], 19.0); // newest
|
||||
}
|
||||
PLOT_TEST(ring_clear_resets) {
|
||||
plot::data::RingBuffer<f32> r(8);
|
||||
for (int i = 0; i < 5; ++i) r.push(f32(i));
|
||||
r.clear();
|
||||
PLOT_CHECK(r.size() == 0);
|
||||
r.push(42.0f);
|
||||
PLOT_CHECK(r.size() == 1);
|
||||
PLOT_CHECK_NEAR(r[0], 42.0);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "harness.hpp"
|
||||
#include <plot/data/array_series.hpp>
|
||||
#include <plot/data/stream_series.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#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.pyramid().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);
|
||||
for (int i = 0; i < 3000; ++i) s.push(f32(i % 97)); // wraps the ring twice
|
||||
s.acquire();
|
||||
const u64 n = s.size();
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user