#include #include #include #include #include #include #include #include #include #include #include #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 normalized_state; std::array normalized_residual; std::array recovered_state; std::array 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::quiet_NaN(), std::numeric_limits::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(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::denorm_min(), 1.0); REQUIRE(small_metric); CHECK(*small_metric == std::sqrt(std::numeric_limits::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{overlap}.first(2), std::span{overlap}.last(2)), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_BUFFER_OVERLAP"))); CHECK_THROWS_MATCHES(map->normalize_state(std::span{overlap}.last(2), std::span{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{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{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{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{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 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{storage}.first(2), std::span{storage}.last(2)); CHECK(storage == std::array{3.0, 5.0, 6.0, 20.0}); map->denormalize_state(std::span{storage}.last(2), std::span{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(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")); }