Files
Emily Boudreaux cb20faeb3d 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.
2026-11-09 13:53:17 -05:00

168 lines
9.3 KiB
C++

#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"));
}