mirror of
https://github.com/slsdetectorgroup/aare.git
synced 2026-10-21 00:26:18 +02:00
- MSVC has no ssize_t, which made every translation unit fail on Windows. - Define aare::ssize_t in defs.hpp: the platform ssize_t on POSIX (so nothing changes there) and std::ptrdiff_t on MSVC. - Code inside the namespace picks it up unchanged; the few files that use ssize_t outside the namespace get a using-declaration. FilePtr.hpp now includes defs.hpp since it had no aare include. Closing #393 Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
324 lines
11 KiB
C++
324 lines
11 KiB
C++
// SPDX-License-Identifier: MPL-2.0
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/generators/catch_generators.hpp>
|
|
|
|
#include "aare/logger.hpp"
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <random>
|
|
#include <thread>
|
|
#include <type_traits>
|
|
#include <vector>
|
|
|
|
#include "test_config.hpp"
|
|
#include "test_macros.hpp"
|
|
|
|
#include "aare/hist/PixelHistogram.hpp"
|
|
|
|
using aare::NDArray;
|
|
using aare::NDView;
|
|
using aare::PixelHistogram;
|
|
using aare::ssize_t;
|
|
|
|
namespace {
|
|
// The synchronous fill() has been removed; fill_async() is the only entry
|
|
// point. This helper submits one frame and blocks until it has been merged
|
|
// so the tests can keep their straightforward, ordered expectations.
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Fill one pixel of a 5x10 histogram") {
|
|
PixelHistogram hist(5, 10, 20, 0.0f, 10.0f);
|
|
NDArray<float, 2> image(
|
|
{5, 10}, -1.0f); // Need to fill with -1 to not generate counts
|
|
|
|
image(2, 3) = 5.7f; // This should go into bin 11 (since bins are [0-0.5),
|
|
// [0.5-1.0), ..., [9.5-10.0))
|
|
|
|
// fill_blocking(hist, image.view());
|
|
|
|
hist.fill_async(NDArray<float, 2>(image));
|
|
hist.flush(); // Wait for the async fill to complete before we check the
|
|
// results
|
|
|
|
auto values = hist.values();
|
|
REQUIRE(values.shape(0) == 5);
|
|
REQUIRE(values.shape(1) == 10);
|
|
REQUIRE(values.shape(2) == 20);
|
|
|
|
// Check that the correct bin for pixel (2,3) has count 1
|
|
CHECK(values(2, 3, 11) == 1);
|
|
|
|
// Check that all other bins are zero
|
|
for (ssize_t row = 0; row < values.shape(0); ++row) {
|
|
for (ssize_t col = 0; col < values.shape(1); ++col) {
|
|
for (ssize_t bin = 0; bin < values.shape(2); ++bin) {
|
|
if (!(row == 2 && col == 3 && bin == 11)) {
|
|
CHECK(values(row, col, bin) == 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Fill pixels with uneven partial histogram row slices") {
|
|
PixelHistogram hist(5, 4, 10, 0.0f, 10.0f, 3);
|
|
NDArray<float, 2> image({5, 4}, -1.0f);
|
|
|
|
image(0, 0) = 0.2f;
|
|
image(1, 1) = 1.2f;
|
|
image(2, 2) = 2.2f;
|
|
image(3, 3) = 3.2f;
|
|
image(4, 0) = 4.2f;
|
|
|
|
hist.fill_async(NDArray<float, 2>(image));
|
|
hist.flush();
|
|
|
|
auto values = hist.values();
|
|
REQUIRE(values.shape(0) == 5);
|
|
REQUIRE(values.shape(1) == 4);
|
|
REQUIRE(values.shape(2) == 10);
|
|
|
|
CHECK(values(0, 0, 0) == 1);
|
|
CHECK(values(1, 1, 1) == 1);
|
|
CHECK(values(2, 2, 2) == 1);
|
|
CHECK(values(3, 3, 3) == 1);
|
|
CHECK(values(4, 0, 4) == 1);
|
|
|
|
for (ssize_t row = 0; row < values.shape(0); ++row) {
|
|
for (ssize_t col = 0; col < values.shape(1); ++col) {
|
|
for (ssize_t bin = 0; bin < values.shape(2); ++bin) {
|
|
const bool expected = (row == 0 && col == 0 && bin == 0) ||
|
|
(row == 1 && col == 1 && bin == 1) ||
|
|
(row == 2 && col == 2 && bin == 2) ||
|
|
(row == 3 && col == 3 && bin == 3) ||
|
|
(row == 4 && col == 0 && bin == 4);
|
|
if (!expected) {
|
|
CHECK(values(row, col, bin) == 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Row partitioning handles rows < n_threads * ceil(rows/n_threads)") {
|
|
// Regression test for the pre-existing row partitioning bug: with the
|
|
// old ceil(rows / n_threads) scheme, rows=17 and n_threads=8 left
|
|
// trailing threads with negative row counts and threw
|
|
// "stop >= start required" from boost::histogram during construction.
|
|
// The current scheme distributes the remainder so every thread gets
|
|
// at least floor(rows / n_threads) rows. We also exercise the case
|
|
// where n_threads > rows, which is clamped down to rows.
|
|
const auto p = GENERATE(table<int, int>({
|
|
{17, 8}, // previously broken: 8 threads * ceil(17/8) = 24 > 17
|
|
{17, 5}, // previously broken: 5 threads * ceil(17/5) = 20 > 17
|
|
{13, 4}, // previously broken: 4 * ceil(13/4) = 16 > 13
|
|
{17, 32}, // n_threads > rows -> clamped to rows
|
|
{1, 8}, // n_threads > rows -> single thread, single row
|
|
{6, 6}, // balanced
|
|
}));
|
|
const int rows = std::get<0>(p);
|
|
const int n_threads = std::get<1>(p);
|
|
const int cols = 3;
|
|
const int n_bins = 4;
|
|
constexpr float xmin = 0.0f;
|
|
constexpr float xmax = 4.0f;
|
|
|
|
PixelHistogram hist(rows, cols, n_bins, xmin, xmax, n_threads);
|
|
|
|
// Put one in-range value per row so we can verify every row is covered.
|
|
NDArray<float, 2> img({rows, cols}, -1.0f); // -1 is out of range
|
|
for (ssize_t r = 0; r < rows; ++r) {
|
|
img(r, 0) = static_cast<float>(r % n_bins) + 0.5f;
|
|
}
|
|
hist.fill_async(NDArray<float, 2>(img));
|
|
hist.flush();
|
|
|
|
auto h = hist.values();
|
|
REQUIRE(h.shape(0) == rows);
|
|
REQUIRE(h.shape(1) == cols);
|
|
REQUIRE(h.shape(2) == n_bins);
|
|
|
|
for (ssize_t r = 0; r < rows; ++r) {
|
|
const int expected_bin = static_cast<int>(r % n_bins);
|
|
for (ssize_t c = 0; c < cols; ++c) {
|
|
for (ssize_t b = 0; b < n_bins; ++b) {
|
|
const uint16_t want =
|
|
(c == 0 && b == expected_bin) ? uint16_t{1} : uint16_t{0};
|
|
if (h(r, c, b) != want) {
|
|
INFO("rows=" << rows << " n_threads=" << n_threads
|
|
<< " r=" << r << " c=" << c << " b=" << b
|
|
<< " got=" << h(r, c, b) << " want=" << want);
|
|
CHECK(h(r, c, b) == want);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Random fills match a reference implementation") {
|
|
// End-to-end correctness check: compare the implementation against a
|
|
// simple per-pixel reference for several thread counts. Values are
|
|
// sampled slightly wider than [xmin, xmax) so the out-of-range filter
|
|
// is also exercised.
|
|
constexpr int rows = 17;
|
|
constexpr int cols = 23;
|
|
constexpr int n_bins = 32;
|
|
constexpr float xmin = -1.5f;
|
|
constexpr float xmax = 4.5f;
|
|
const int n_threads = GENERATE(1, 2, 4, 8);
|
|
|
|
PixelHistogram hist(rows, cols, n_bins, xmin, xmax, n_threads);
|
|
|
|
std::mt19937 rng(0xC0FFEE);
|
|
std::uniform_real_distribution<float> dist(xmin - 0.5f, xmax + 0.5f);
|
|
|
|
constexpr int n_frames = 4;
|
|
std::vector<NDArray<float, 2>> frames;
|
|
frames.reserve(n_frames);
|
|
for (int f = 0; f < n_frames; ++f) {
|
|
NDArray<float, 2> img({rows, cols}, 0.0f);
|
|
for (ssize_t r = 0; r < rows; ++r) {
|
|
for (ssize_t c = 0; c < cols; ++c) {
|
|
img(r, c) = dist(rng);
|
|
}
|
|
}
|
|
frames.push_back(std::move(img));
|
|
}
|
|
|
|
NDArray<uint32_t, 3> expected({rows, cols, n_bins}, 0u);
|
|
const float inv_range = static_cast<float>(n_bins) / (xmax - xmin);
|
|
for (const auto &img : frames) {
|
|
for (ssize_t r = 0; r < rows; ++r) {
|
|
for (ssize_t c = 0; c < cols; ++c) {
|
|
const float v = img(r, c);
|
|
if (!(v >= xmin) || !(v < xmax))
|
|
continue;
|
|
int bin = static_cast<int>((v - xmin) * inv_range);
|
|
if (bin >= n_bins)
|
|
bin = n_bins - 1;
|
|
if (bin < 0)
|
|
bin = 0;
|
|
expected(r, c, bin) += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto &img : frames) {
|
|
hist.fill_async(NDArray<float, 2>(img));
|
|
}
|
|
hist.flush();
|
|
auto h = hist.values();
|
|
|
|
REQUIRE(h.shape(0) == rows);
|
|
REQUIRE(h.shape(1) == cols);
|
|
REQUIRE(h.shape(2) == n_bins);
|
|
|
|
bool all_match = true;
|
|
for (ssize_t r = 0; r < rows && all_match; ++r) {
|
|
for (ssize_t c = 0; c < cols && all_match; ++c) {
|
|
for (ssize_t b = 0; b < n_bins && all_match; ++b) {
|
|
if (h(r, c, b) != expected(r, c, b)) {
|
|
all_match = false;
|
|
INFO("n_threads=" << n_threads << " r=" << r << " c=" << c
|
|
<< " b=" << b << " got=" << h(r, c, b)
|
|
<< " expected=" << expected(r, c, b));
|
|
CHECK(h(r, c, b) == expected(r, c, b));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CHECK(all_match);
|
|
}
|
|
|
|
TEST_CASE("fill_async with mismatched shape throws") {
|
|
PixelHistogram hist(8, 8, 16, 0.0f, 1.0f, 2);
|
|
NDArray<float, 2> bad({4, 4}, 0.0f);
|
|
CHECK_THROWS_AS(hist.fill_async(std::move(bad)), std::invalid_argument);
|
|
}
|
|
|
|
TEST_CASE("Destructor drains pending async fills") {
|
|
// Submit more frames than the queue can hold so backpressure kicks in,
|
|
// then immediately let the histogram go out of scope and verify that
|
|
// the merged values() matches the reference computed sequentially.
|
|
constexpr int rows = 11;
|
|
constexpr int cols = 7;
|
|
constexpr int n_bins = 8;
|
|
constexpr float xmin = 0.0f;
|
|
constexpr float xmax = 1.0f;
|
|
constexpr int n_frames = 12;
|
|
constexpr std::size_t max_pending = 2;
|
|
|
|
std::vector<NDArray<float, 2>> frames;
|
|
frames.reserve(n_frames);
|
|
std::mt19937 rng(0xDEADBEEF);
|
|
std::uniform_real_distribution<float> dist(xmin, xmax);
|
|
for (int f = 0; f < n_frames; ++f) {
|
|
NDArray<float, 2> img({rows, cols}, 0.0f);
|
|
for (ssize_t r = 0; r < rows; ++r)
|
|
for (ssize_t c = 0; c < cols; ++c)
|
|
img(r, c) = dist(rng);
|
|
frames.push_back(std::move(img));
|
|
}
|
|
|
|
NDArray<uint16_t, 3> snapshot({rows, cols, n_bins}, uint16_t{0});
|
|
{
|
|
PixelHistogram hist(rows, cols, n_bins, xmin, xmax, 2, max_pending);
|
|
for (auto &img : frames) {
|
|
// Move a copy so we can also build the reference below.
|
|
NDArray<float, 2> copy({rows, cols}, 0.0f);
|
|
std::memcpy(copy.data(), img.data(), copy.total_bytes());
|
|
hist.fill_async(std::move(copy));
|
|
}
|
|
// No explicit flush(); destructor must drain.
|
|
// Capture values() *after* the loop but inside the scope so it
|
|
// observes everything that was submitted (values flushes too).
|
|
snapshot = hist.values();
|
|
}
|
|
|
|
PixelHistogram reference(rows, cols, n_bins, xmin, xmax, 2);
|
|
for (const auto &img : frames)
|
|
reference.fill_async(NDArray<float, 2>(img.view()));
|
|
auto expected = reference.values();
|
|
|
|
bool all_match = true;
|
|
for (ssize_t r = 0; r < rows && all_match; ++r) {
|
|
for (ssize_t c = 0; c < cols && all_match; ++c) {
|
|
for (ssize_t b = 0; b < n_bins && all_match; ++b) {
|
|
if (snapshot(r, c, b) != expected(r, c, b)) {
|
|
all_match = false;
|
|
INFO("r=" << r << " c=" << c << " b=" << b
|
|
<< " got=" << snapshot(r, c, b)
|
|
<< " expected=" << expected(r, c, b));
|
|
CHECK(snapshot(r, c, b) == expected(r, c, b));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CHECK(all_match);
|
|
}
|
|
|
|
TEST_CASE("PixelHistogram supports custom storage and axis types") {
|
|
PixelHistogram<uint32_t, double> hist(2, 2, 4, 0.0, 4.0, 1);
|
|
NDArray<double, 2> image({2, 2}, -1.0);
|
|
|
|
image(0, 0) = 0.25;
|
|
image(0, 1) = 1.25;
|
|
image(1, 0) = 2.25;
|
|
image(1, 1) = 3.25;
|
|
|
|
hist.fill_async(NDArray<double, 2>(image));
|
|
hist.flush();
|
|
|
|
auto values = hist.values();
|
|
STATIC_REQUIRE(std::is_same_v<decltype(values(0, 0, 0)), uint32_t &>);
|
|
CHECK(values(0, 0, 0) == uint32_t{1});
|
|
CHECK(values(0, 1, 1) == uint32_t{1});
|
|
CHECK(values(1, 0, 2) == uint32_t{1});
|
|
CHECK(values(1, 1, 3) == uint32_t{1});
|
|
|
|
auto centers = hist.bin_centers();
|
|
STATIC_REQUIRE(std::is_same_v<decltype(centers(0)), double &>);
|
|
CHECK(centers.shape(0) == 4);
|
|
}
|