test(tests): added test suite

The test suite is extensive. Note that LLMs were used to aid in the development of the test suite. All LLM generated code was manually reviewed.
This commit is contained in:
2026-11-09 13:53:17 -05:00
parent 74e55a6859
commit cb20faeb3d
202 changed files with 35505 additions and 953 deletions

View File

@@ -0,0 +1,167 @@
#include <array>
#include <cmath>
#include <limits>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <catch2/matchers/catch_matchers_exception.hpp>
#include "serif/solver/normalization/diagonal.hpp"
namespace
{
namespace normalization = serif::solver::normalization;
using Kind = serif::discretization::blocks::RepresentedValueKind;
}
// Ported from MeanField tests/normalization/physical_riesz.cpp.
TEST_CASE("Diagonal Riesz maps reproduce primal and dual norms across extreme metrics", "[normalization][unit][math]")
{
const std::array metric{1.0e-20, 4.0, 9.0e20};
constexpr double state_scale = 10.0;
constexpr double residual_scale = 0.25;
const auto state_factors = normalization::make_riesz_factors(Kind::value, metric, state_scale);
const auto residual_factors = normalization::make_riesz_factors(Kind::residual, metric, residual_scale);
REQUIRE(state_factors);
REQUIRE(residual_factors);
const auto map = normalization::make_diagonal_normalization(*state_factors, *residual_factors);
REQUIRE(map);
const std::array state{3.0e10, -2.0, 4.0e-10};
const std::array residual{2.0e-10, -3.0, 5.0e10};
double primal_norm = 0.0;
double dual_norm = 0.0;
for (std::size_t index = 0; index < metric.size(); ++index)
{
primal_norm += metric[index] * state[index] * state[index] / (state_scale * state_scale);
dual_norm += residual[index] * residual[index] / (metric[index] * residual_scale * residual_scale);
}
CHECK(map->state_norm_squared(state) == Catch::Approx(primal_norm).epsilon(3.0e-15));
CHECK(map->residual_norm_squared(residual) == Catch::Approx(dual_norm).epsilon(3.0e-15));
std::array<double, 3> normalized_state;
std::array<double, 3> normalized_residual;
std::array<double, 3> recovered_state;
std::array<double, 3> recovered_residual;
map->normalize_state(state, normalized_state);
map->denormalize_state(normalized_state, recovered_state);
map->normalize_residual(residual, normalized_residual);
map->denormalize_residual(normalized_residual, recovered_residual);
for (std::size_t index = 0; index < state.size(); ++index)
{
CHECK(recovered_state[index] == Catch::Approx(state[index]).epsilon(3.0e-15));
CHECK(recovered_residual[index] == Catch::Approx(residual[index]).epsilon(3.0e-15));
}
}
TEST_CASE("Riesz factors reject nonpositive nonfinite and unrepresentable data", "[normalization][unit][validation]")
{
for (const double invalid : {0.0, -1.0, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::infinity()})
{
CHECK_FALSE(normalization::make_riesz_factor(Kind::value, invalid, 1.0));
CHECK_FALSE(normalization::make_riesz_factor(Kind::residual, 1.0, invalid));
CHECK_FALSE(normalization::make_riesz_factors(Kind::value, std::array{1.0, invalid}, 1.0));
CHECK_FALSE(normalization::make_riesz_factors(Kind::residual, {}, invalid));
CHECK_FALSE(normalization::make_diagonal_normalization({invalid}, {1.0}));
CHECK_FALSE(normalization::make_diagonal_normalization({1.0}, {invalid}));
}
CHECK_FALSE(normalization::make_riesz_factor(static_cast<Kind>(25), 1.0, 1.0));
CHECK_FALSE(normalization::make_riesz_factor(Kind::value, 1.0e300, 1.0e-300));
CHECK_FALSE(normalization::make_riesz_factor(Kind::value, 1.0e-300, 1.0e300));
CHECK_FALSE(normalization::make_riesz_factor(Kind::residual, 1.0e-300, 1.0e-300));
CHECK_FALSE(normalization::make_riesz_factor(Kind::residual, 1.0e300, 1.0e300));
}
TEST_CASE("Dual Riesz factors retain representable subnormals without overflowing an intermediate product", "[normalization][unit][math]")
{
const auto factor = normalization::make_riesz_factor(Kind::residual, 1.0e300, 1.0e160);
REQUIRE(factor);
CHECK(*factor > 0.0);
CHECK(*factor / 1.0e-310 == Catch::Approx(1.0).epsilon(1.0e-12));
const auto small_metric = normalization::make_riesz_factor(Kind::value, std::numeric_limits<double>::denorm_min(), 1.0);
REQUIRE(small_metric);
CHECK(*small_metric == std::sqrt(std::numeric_limits<double>::denorm_min()));
}
TEST_CASE("Diagonal coordinate changes permit exact in-place conversion and reject partial overlap", "[normalization][unit][validation]")
{
const auto map = normalization::make_diagonal_normalization({2.0, 4.0}, {0.5, 0.25});
REQUIRE(map);
std::array values{3.0, -7.0};
map->normalize_state(values, values);
CHECK(values == std::array{6.0, -28.0});
map->denormalize_state(values, values);
CHECK(values == std::array{3.0, -7.0});
map->normalize_residual(values, values);
CHECK(values == std::array{1.5, -1.75});
map->denormalize_residual(values, values);
CHECK(values == std::array{3.0, -7.0});
std::array overlap{1.0, 2.0, 3.0};
CHECK_THROWS_MATCHES(map->normalize_state(std::span<const double>{overlap}.first(2), std::span<double>{overlap}.last(2)), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_BUFFER_OVERLAP")));
CHECK_THROWS_MATCHES(map->normalize_state(std::span<const double>{overlap}.last(2), std::span<double>{overlap}.first(2)), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_BUFFER_OVERLAP")));
CHECK(overlap == std::array{1.0, 2.0, 3.0});
}
TEST_CASE("Diagonal normalization checks both vector sizes before changing any output", "[normalization][unit][validation]")
{
const auto map = normalization::make_diagonal_normalization({2.0, 4.0}, {3.0});
REQUIRE(map);
const std::array input{1.0, 2.0};
std::array output{9.0, 8.0};
CHECK_THROWS_MATCHES(map->normalize_state(std::span<const double>{input}.first(1), output), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_VECTOR_SIZE_MISMATCH")));
CHECK_THROWS_MATCHES(map->normalize_state(input, std::span<double>{output}.first(1)), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_VECTOR_SIZE_MISMATCH")));
CHECK_THROWS_MATCHES(map->denormalize_state(input, std::span<double>{output}.first(1)), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_VECTOR_SIZE_MISMATCH")));
CHECK_THROWS_MATCHES(map->normalize_residual(input, output), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_VECTOR_SIZE_MISMATCH")));
CHECK_THROWS_MATCHES(map->denormalize_residual(input, output), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_VECTOR_SIZE_MISMATCH")));
CHECK_THROWS_MATCHES(map->state_norm_squared(std::span<const double>{input}.first(1)), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_VECTOR_SIZE_MISMATCH")));
CHECK_THROWS_MATCHES(map->residual_norm_squared(input), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_VECTOR_SIZE_MISMATCH")));
CHECK(output == std::array{9.0, 8.0});
CHECK(map->state_size() == 2);
CHECK(map->residual_size() == 1);
}
TEST_CASE("Frozen normalization owns its factors and supports empty coordinate sets", "[normalization][unit]")
{
std::vector<double> factors{2.0};
const auto map = normalization::make_diagonal_normalization(factors, {3.0, 4.0});
REQUIRE(map);
factors[0] = 17.0;
CHECK(map->state_factors()[0] == 2.0);
CHECK(map->residual_factors()[1] == 4.0);
const auto empty = normalization::make_diagonal_normalization({}, {});
REQUIRE(empty);
empty->normalize_state({}, {});
empty->denormalize_residual({}, {});
CHECK(empty->state_norm_squared({}) == 0.0);
CHECK(empty->residual_norm_squared({}) == 0.0);
}
TEST_CASE("Normalization accepts adjacent buffers and identifies expected failures once", "[normalization][unit][validation]")
{
const auto map = normalization::make_diagonal_normalization({2.0, 4.0}, {});
REQUIRE(map);
std::array storage{3.0, 5.0, 0.0, 0.0};
map->normalize_state(std::span<const double>{storage}.first(2), std::span<double>{storage}.last(2));
CHECK(storage == std::array{3.0, 5.0, 6.0, 20.0});
map->denormalize_state(std::span<const double>{storage}.last(2), std::span<double>{storage}.first(2));
CHECK(storage == std::array{3.0, 5.0, 6.0, 20.0});
const auto invalid_metric = normalization::make_riesz_factors(Kind::value, std::array{1.0, 0.0}, 2.0);
REQUIRE_FALSE(invalid_metric);
CHECK_THAT(invalid_metric.error(), Catch::Matchers::ContainsSubstring("NORMALIZATION_ERROR"));
REQUIRE(invalid_metric.error().find("code No.:") != std::string::npos);
CHECK(invalid_metric.error().find("code No.:") == invalid_metric.error().rfind("code No.:"));
const auto invalid_role = normalization::make_riesz_factors(static_cast<Kind>(25), {}, 1.0);
REQUIRE_FALSE(invalid_role);
CHECK_THAT(invalid_role.error(), Catch::Matchers::ContainsSubstring("INVALID_ARGUMENT"));
const auto invalid_factors = normalization::make_diagonal_normalization({1.0}, {0.0});
REQUIRE_FALSE(invalid_factors);
CHECK_THAT(invalid_factors.error(), Catch::Matchers::ContainsSubstring("NORMALIZATION_ERROR"));
}