// SPDX-License-Identifier: GPL-3.0-or-later // Copyright 2026 YAMS Contributors #include #include #include #include #include #include using Catch::Approx; using yams::search::computePersistenceH0; using yams::search::deterministicSubsample; namespace { std::vector> makeTwoClusters(std::size_t nPerCluster, std::size_t dim, float separation, std::uint32_t seed) { std::mt19937 gen{seed}; std::normal_distribution noise{0.0F, 0.05F}; std::vector> out; out.reserve(2 * nPerCluster); for (std::size_t i = 0; i < nPerCluster; ++i) { std::vector v(dim, 0.0F); for (auto& x : v) { x = noise(gen); } out.push_back(v); } for (std::size_t i = 0; i < nPerCluster; ++i) { std::vector v(dim, 0.0F); v[0] = separation; for (std::size_t k = 1; k < dim; ++k) { v[k] = noise(gen); } v[0] += noise(gen); out.push_back(v); } return out; } std::vector> makeCollapsed(std::size_t n, std::size_t dim, std::uint32_t seed) { std::mt19937 gen{seed}; std::normal_distribution tiny{0.0F, 1e-5F}; std::vector> out(n, std::vector(dim, 0.0F)); for (auto& row : out) { for (auto& x : row) { x = tiny(gen); } } return out; } } // namespace TEST_CASE("computePersistenceH0: empty / degenerate inputs", "[unit][search][tda][topological_quality][catch2]") { CHECK(computePersistenceH0(std::span{}, 0, 0) == Approx(0.0)); CHECK(computePersistenceH0(std::vector>{}) == Approx(0.0)); std::vector> single{{0.1F, 0.2F, 0.3F}}; CHECK(computePersistenceH0(single) == Approx(0.0)); } TEST_CASE("computePersistenceH0: collapsed cluster yields low persistence", "[unit][search][tda][topological_quality][catch2]") { auto pts = makeCollapsed(64, 8, 42); const double r = computePersistenceH0(pts); CHECK(r < 100.0); } TEST_CASE("computePersistenceH0: well-separated clusters produce measurable persistence", "[unit][search][tda][topological_quality][catch2]") { auto pts = makeTwoClusters(32, 16, 5.0F, 7); const double r = computePersistenceH0(pts); CHECK(r > 0.1); } TEST_CASE("computePersistenceH0: dispersed points > collapsed points", "[unit][search][tda][topological_quality][catch2]") { auto collapsed = makeCollapsed(64, 8, 123); std::mt19937 gen{99}; std::uniform_real_distribution uni{-1.0F, 1.0F}; std::vector> dispersed(64, std::vector(8, 0.0F)); for (auto& row : dispersed) { for (auto& x : row) { x = uni(gen); } } const double rCollapsed = computePersistenceH0(collapsed); const double rDispersed = computePersistenceH0(dispersed); CHECK(rDispersed > rCollapsed); } TEST_CASE("computePersistenceH0: overload matches span form", "[unit][search][tda][topological_quality][catch2]") { auto pts = makeTwoClusters(16, 4, 2.0F, 1); const double rA = computePersistenceH0(pts); std::vector flat; for (const auto& row : pts) { flat.insert(flat.end(), row.begin(), row.end()); } const double rB = computePersistenceH0(std::span(flat), pts.size(), 4); CHECK(rA == Approx(rB)); } TEST_CASE("deterministicSubsample: reproducible + bounded", "[unit][search][tda][topological_quality][catch2]") { auto a = deterministicSubsample(1000, 100, 42); auto b = deterministicSubsample(1000, 100, 42); CHECK(a == b); CHECK(a.size() == 100); for (auto i : a) { CHECK(i < 1000); } auto c = deterministicSubsample(1000, 100, 43); CHECK(a != c); auto d = deterministicSubsample(50, 100, 42); CHECK(d.size() == 50); for (std::size_t k = 0; k < 50; ++k) { CHECK(d[k] == k); } }