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.
168 lines
9.3 KiB
C++
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"));
|
|
}
|