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:
167
tests/normalization/diagonal.cpp
Normal file
167
tests/normalization/diagonal.cpp
Normal 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"));
|
||||
}
|
||||
Reference in New Issue
Block a user