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:
266
tests/normalization/computations.cpp
Normal file
266
tests/normalization/computations.cpp
Normal file
@@ -0,0 +1,266 @@
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||
|
||||
#include "serif/tests/equations/test_equations.hpp"
|
||||
#include "serif/equations/mass_normalization.hpp"
|
||||
#include "serif/equations/angular_momentum.hpp"
|
||||
#include "serif/equations/primal_gravity.hpp"
|
||||
#include "serif/solver/normalization/normalization.hpp"
|
||||
#include "serif/utils/dependencies/dependencies.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace normalization = serif::solver::normalization;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace dimensions = serif::dimensions;
|
||||
namespace blocks = serif::discretization::blocks;
|
||||
using Kind = blocks::RepresentedValueKind;
|
||||
using Density = blocks::density::mass;
|
||||
using Constant = blocks::barotropic_constant::mass_normalization;
|
||||
using Rotation = blocks::rotation::angular_velocity;
|
||||
using Mass = serif::equations::MassNormalization;
|
||||
using Momentum = serif::equations::AngularMomentumConstraint;
|
||||
|
||||
/** @brief Make a test reference with deliberately different mass, enthalpy, pressure and rotation scales. */
|
||||
normalization::StellarReferenceScales reference_scales()
|
||||
{
|
||||
return normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{9.0}, dimensions::QuantityValue<dimensions::Length>{2.0}, dimensions::QuantityValue<dimensions::GravitationalConstant>{4.0}).value();
|
||||
}
|
||||
|
||||
/** @brief Construct a cache from the ordinary computation tuple, as a coupled graph would. */
|
||||
template <typename Plan, typename Scales = normalization::StellarReferenceScales, typename Context, typename Layout>
|
||||
auto make_cache(const Context& context, const Layout& layout)
|
||||
{
|
||||
return std::apply([&](auto... computations)
|
||||
{
|
||||
return dependencies::make_computation_cache(context, std::move(computations)...);
|
||||
}, normalization::make_normalization_computations<Plan, Scales>(layout));
|
||||
}
|
||||
|
||||
struct SurfaceRows
|
||||
{
|
||||
};
|
||||
|
||||
struct TemperatureCoordinate
|
||||
{
|
||||
using Quantity = dimensions::Temperature;
|
||||
};
|
||||
|
||||
struct TemperatureMetric
|
||||
{
|
||||
using Coordinates = dependencies::KeyList<TemperatureCoordinate>;
|
||||
};
|
||||
|
||||
struct ThermalScales
|
||||
{
|
||||
dimensions::QuantityValue<dimensions::Temperature> temperature;
|
||||
|
||||
/** @brief Track an external scale provider without adding normalization-specific revision machinery. */
|
||||
bool operator==(const ThermalScales&) const = default;
|
||||
};
|
||||
|
||||
/** @brief Supply a quantity that cannot be inferred from stellar mass, radius, and gravity. */
|
||||
dimensions::QuantityValue<dimensions::Temperature> characteristic_scale(const ThermalScales& scales, dimensions::Temperature)
|
||||
{
|
||||
return scales.temperature;
|
||||
}
|
||||
|
||||
/** @brief A third-party method whose configuration is an explicit tracked input, including move-only ownership. */
|
||||
struct UniformDiagonal : normalization::NormalizationPrescriptionTag
|
||||
{
|
||||
std::unique_ptr<double> multiplier;
|
||||
double residual_multiplier;
|
||||
std::size_t extra_count = 0;
|
||||
|
||||
/** @brief Return the actual policy's factors for the exact requested role and coordinate. */
|
||||
template <Kind Role, typename Coordinate>
|
||||
std::expected<std::vector<double>, std::string> prepare_factors(normalization::keys::Factors<Role, Coordinate>, std::size_t count) const
|
||||
{
|
||||
return std::vector<double>(count + extra_count, Role == Kind::value ? *multiplier : residual_multiplier);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("Normalization plans preserve distinct global quantities and obey layout rather than declaration order", "[unit][normalization][dependencies]")
|
||||
{
|
||||
using Plan = normalization::NormalizationPlan<
|
||||
dependencies::Bindings<normalization::PhysicalValueRule<Rotation>, normalization::PhysicalValueRule<Constant>>,
|
||||
dependencies::Bindings<normalization::PhysicalResidualRule<Mass>, normalization::PhysicalResidualRule<Momentum>>>;
|
||||
const normalization::CoordinateLayout<dependencies::KeyList<Constant, Rotation>, dependencies::KeyList<Momentum, Mass>> layout{.state_sizes = {1, 1}, .residual_sizes = {1, 1}};
|
||||
auto scales = reference_scales();
|
||||
double irrelevant_candidate = 0.0;
|
||||
const auto context = dependencies::make_dependency_context(dependencies::borrow<normalization::keys::Layout>(layout), dependencies::borrow<normalization::keys::Scales>(scales), dependencies::borrow<Density>(irrelevant_candidate));
|
||||
auto cache = make_cache<Plan>(context, layout);
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
const normalization::DiagonalNormalization frozen = cache.get<normalization::keys::Normalization>();
|
||||
CHECK(frozen.state_factors()[0] == Catch::Approx(1.0 / scales.specific_energy.value()).epsilon(2.0e-15));
|
||||
CHECK(frozen.state_factors()[1] == Catch::Approx(1.0 / scales.angular_velocity.value()).epsilon(2.0e-15));
|
||||
CHECK(frozen.residual_factors()[0] == Catch::Approx(1.0 / scales.angular_momentum.value()).epsilon(2.0e-15));
|
||||
CHECK(frozen.residual_factors()[1] == Catch::Approx(1.0 / scales.mass.value()).epsilon(2.0e-15));
|
||||
|
||||
irrelevant_candidate = std::numeric_limits<double>::quiet_NaN();
|
||||
auto report = cache.try_prepare(context);
|
||||
REQUIRE(report);
|
||||
CHECK(report->computed_count() == 0);
|
||||
scales.mass = dimensions::QuantityValue<dimensions::Mass>{18.0};
|
||||
report = cache.try_prepare(context);
|
||||
REQUIRE(report);
|
||||
CHECK(cache.get<normalization::keys::Normalization>().residual_factors()[1] == Catch::Approx(1.0 / 18.0));
|
||||
CHECK(frozen.residual_factors()[1] == Catch::Approx(1.0 / 9.0));
|
||||
}
|
||||
|
||||
TEST_CASE("One reference volume metric supplies independent primal and dual coordinates", "[unit][normalization][dependencies]")
|
||||
{
|
||||
using Metric = normalization::ReferenceVolumeMetric<Density>;
|
||||
using Method = normalization::PhysicalRieszCoordinate<dimensions::MassDensity, Metric>;
|
||||
using Plan = normalization::NormalizationPlan<dependencies::Bindings<dependencies::Binding<Density, Method>>, dependencies::Bindings<dependencies::Binding<Density, Method>>>;
|
||||
const normalization::CoordinateLayout<dependencies::KeyList<Density>, dependencies::KeyList<Density>> layout{.state_sizes = {3}, .residual_sizes = {3}};
|
||||
const auto scales = reference_scales();
|
||||
std::vector<double> metric{1.0e-20, 4.0, 9.0e20};
|
||||
const auto context = dependencies::make_dependency_context(dependencies::borrow<normalization::keys::Layout>(layout), dependencies::borrow<normalization::keys::Scales>(scales), dependencies::borrow<Metric>(metric));
|
||||
auto cache = make_cache<Plan>(context, layout);
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
const auto& map = cache.get<normalization::keys::Normalization>();
|
||||
for (std::size_t index = 0; index < metric.size(); ++index)
|
||||
{
|
||||
CHECK(map.state_factors()[index] == Catch::Approx(std::sqrt(metric[index]) / scales.density.value()).epsilon(3.0e-15));
|
||||
CHECK(map.residual_factors()[index] == Catch::Approx(1.0 / (std::sqrt(metric[index]) * scales.density.value())).epsilon(3.0e-15));
|
||||
}
|
||||
metric[1] = 0.0;
|
||||
const auto failure = cache.try_prepare(context);
|
||||
REQUIRE_FALSE(failure);
|
||||
const auto originating_failure = normalization::make_riesz_factors(Kind::value, metric, scales.density.value());
|
||||
REQUIRE_FALSE(originating_failure);
|
||||
CHECK(failure.error().error == originating_failure.error());
|
||||
CHECK_THAT(failure.error().error, Catch::Matchers::ContainsSubstring("NORMALIZATION_ERROR"));
|
||||
const std::string& diagnostic = failure.error().error;
|
||||
REQUIRE(diagnostic.find("code No.:") != std::string::npos);
|
||||
CHECK(diagnostic.find("code No.:") == diagnostic.rfind("code No.:"));
|
||||
CHECK_FALSE(cache.ready());
|
||||
CHECK_THROWS_AS(cache.get<normalization::keys::Normalization>(), std::logic_error);
|
||||
metric[1] = 16.0;
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(cache.get<normalization::keys::Normalization>().state_factors()[1] == Catch::Approx(4.0 / scales.density.value()));
|
||||
metric.push_back(1.0);
|
||||
const auto wrong_size = cache.try_prepare(context);
|
||||
REQUIRE_FALSE(wrong_size);
|
||||
CHECK_THAT(wrong_size.error().error, Catch::Matchers::ContainsSubstring("NORMALIZATION_VECTOR_SIZE_MISMATCH"));
|
||||
metric.pop_back();
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(cache.get<normalization::keys::Normalization>().state_factors()[1] == Catch::Approx(4.0 / scales.density.value()));
|
||||
}
|
||||
|
||||
TEST_CASE("Pressure point rows replace both the volume metric and its physical scale", "[unit][normalization][dependencies]")
|
||||
{
|
||||
using Enthalpy = ::serif::tests::equations::Enthalpy;
|
||||
using Metric = normalization::ReferenceVolumeMetric<Enthalpy>;
|
||||
using Volume = normalization::PhysicalRieszCoordinate<dimensions::SpecificEnthalpy, Metric>;
|
||||
using Hybrid = normalization::WithPointRows<Volume, dimensions::Pressure, SurfaceRows>;
|
||||
using Plan = normalization::NormalizationPlan<dependencies::Bindings<>, dependencies::Bindings<dependencies::Binding<Enthalpy, Hybrid>>>;
|
||||
const normalization::CoordinateLayout<dependencies::KeyList<>, dependencies::KeyList<Enthalpy>> layout{.state_sizes = {}, .residual_sizes = {4}};
|
||||
const auto scales = reference_scales();
|
||||
const std::vector<double> metric{4.0, 0.0, 16.0, 0.0};
|
||||
std::vector<int> rows{1, 3};
|
||||
const auto context = dependencies::make_dependency_context(dependencies::borrow<normalization::keys::Layout>(layout), dependencies::borrow<normalization::keys::Scales>(scales), dependencies::borrow<Metric>(metric), dependencies::borrow<SurfaceRows>(rows));
|
||||
auto cache = make_cache<Plan>(context, layout);
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
const auto& map = cache.get<normalization::keys::Normalization>();
|
||||
CHECK(map.state_size() == 0);
|
||||
CHECK(map.residual_factors()[0] == Catch::Approx(1.0 / (2.0 * scales.specific_energy.value())));
|
||||
CHECK(map.residual_factors()[1] == Catch::Approx(1.0 / scales.pressure.value()));
|
||||
CHECK(map.residual_factors()[2] == Catch::Approx(1.0 / (4.0 * scales.specific_energy.value())));
|
||||
CHECK(map.residual_factors()[3] == Catch::Approx(1.0 / scales.pressure.value()));
|
||||
for (const std::vector<int>& invalid : {std::vector<int>{1, 1}, {1, 4}, {-1, 3}, {1}})
|
||||
{
|
||||
rows = invalid;
|
||||
CHECK_FALSE(cache.try_prepare(context));
|
||||
}
|
||||
rows = {1, 3};
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
}
|
||||
|
||||
TEST_CASE("Custom normalization uses its exact move-only prescription and tracks configuration changes", "[unit][normalization][dependencies]")
|
||||
{
|
||||
using Method = normalization::RuntimePreparedCoordinate<UniformDiagonal>;
|
||||
using Plan = normalization::NormalizationPlan<dependencies::Bindings<dependencies::Binding<Density, Method>>, dependencies::Bindings<dependencies::Binding<Density, Method>>>;
|
||||
normalization::CoordinateLayout<dependencies::KeyList<Density>, dependencies::KeyList<Density>> layout{.state_sizes = {2}, .residual_sizes = {3}};
|
||||
UniformDiagonal policy{.multiplier = std::make_unique<double>(2.0), .residual_multiplier = 5.0};
|
||||
const auto context = dependencies::make_dependency_context(dependencies::borrow<normalization::keys::Layout>(layout), dependencies::borrow<normalization::keys::Prescription<UniformDiagonal>>(policy, dependencies::track_by([](const UniformDiagonal& method)
|
||||
{
|
||||
return std::tuple{*method.multiplier, method.residual_multiplier, method.extra_count};
|
||||
})));
|
||||
auto cache = make_cache<Plan>(context, layout);
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(cache.get<normalization::keys::Normalization>().state_size() == 2);
|
||||
CHECK(cache.get<normalization::keys::Normalization>().residual_size() == 3);
|
||||
CHECK(cache.get<normalization::keys::Normalization>().state_factors()[1] == 2.0);
|
||||
CHECK(cache.get<normalization::keys::Normalization>().residual_factors()[2] == 5.0);
|
||||
*policy.multiplier = 3.0;
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(cache.get<normalization::keys::Normalization>().state_factors()[1] == 3.0);
|
||||
policy.residual_multiplier = -1.0;
|
||||
CHECK_FALSE(cache.try_prepare(context));
|
||||
policy.residual_multiplier = 5.0;
|
||||
policy.extra_count = 1;
|
||||
CHECK_FALSE(cache.try_prepare(context));
|
||||
policy.extra_count = 0;
|
||||
layout.state_sizes[0] = std::numeric_limits<std::size_t>::max();
|
||||
CHECK_FALSE(cache.try_prepare(context));
|
||||
}
|
||||
|
||||
TEST_CASE("External physical scales share ordinary metric preparation and invalidation", "[unit][normalization][dependencies]")
|
||||
{
|
||||
using Method = normalization::PhysicalRieszCoordinate<dimensions::Temperature, TemperatureMetric>;
|
||||
using Rule = dependencies::Binding<TemperatureCoordinate, Method>;
|
||||
using Plan = normalization::NormalizationPlan<dependencies::Bindings<Rule>, dependencies::Bindings<Rule>>;
|
||||
const normalization::CoordinateLayout<dependencies::KeyList<TemperatureCoordinate>, dependencies::KeyList<TemperatureCoordinate>> layout{.state_sizes = {2}, .residual_sizes = {2}};
|
||||
ThermalScales scales{.temperature = dimensions::QuantityValue<dimensions::Temperature>{6000.0}};
|
||||
const std::vector<double> metric{4.0, 9.0};
|
||||
const auto context = dependencies::make_dependency_context(dependencies::borrow<normalization::keys::Layout>(layout), dependencies::borrow<normalization::keys::Scales>(scales), dependencies::borrow<TemperatureMetric>(metric));
|
||||
auto cache = make_cache<Plan, ThermalScales>(context, layout);
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(cache.get<normalization::keys::Normalization>().state_factors()[0] == Catch::Approx(2.0 / 6000.0));
|
||||
CHECK(cache.get<normalization::keys::Normalization>().residual_factors()[1] == Catch::Approx(1.0 / 18000.0));
|
||||
REQUIRE(cache.try_prepare(context)->computed_count() == 0);
|
||||
scales.temperature = dimensions::QuantityValue<dimensions::Temperature>{3000.0};
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(cache.get<normalization::keys::Normalization>().state_factors()[0] == Catch::Approx(2.0 / 3000.0));
|
||||
scales.temperature = dimensions::QuantityValue<dimensions::Temperature>{-1.0};
|
||||
CHECK_FALSE(cache.try_prepare(context));
|
||||
}
|
||||
|
||||
TEST_CASE("Oversized normalization layouts fail before allocating identity factors", "[unit][normalization][dependencies]")
|
||||
{
|
||||
using Plan = normalization::NormalizationPlan<dependencies::Bindings<dependencies::Binding<Density, normalization::IdentityCoordinate>>, dependencies::Bindings<>>;
|
||||
const normalization::CoordinateLayout<dependencies::KeyList<Density>, dependencies::KeyList<>> layout{.state_sizes = {std::numeric_limits<std::size_t>::max()}, .residual_sizes = {}};
|
||||
const auto context = dependencies::make_dependency_context(dependencies::borrow<normalization::keys::Layout>(layout));
|
||||
auto cache = make_cache<Plan>(context, layout);
|
||||
CHECK_FALSE(cache.try_prepare(context));
|
||||
}
|
||||
|
||||
TEST_CASE("Identity preparation needs no physical sources and validates global coordinate arity", "[unit][normalization][dependencies]")
|
||||
{
|
||||
using Plan = normalization::NormalizationPlan<dependencies::Bindings<dependencies::Binding<Constant, normalization::IdentityCoordinate>>, dependencies::Bindings<>>;
|
||||
normalization::CoordinateLayout<dependencies::KeyList<Constant>, dependencies::KeyList<>> layout{.state_sizes = {1}, .residual_sizes = {}};
|
||||
const auto context = dependencies::make_dependency_context(dependencies::borrow<normalization::keys::Layout>(layout));
|
||||
auto cache = make_cache<Plan>(context, layout);
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(cache.get<normalization::keys::Normalization>().state_factors()[0] == 1.0);
|
||||
layout.state_sizes[0] = 2;
|
||||
CHECK_FALSE(cache.try_prepare(context));
|
||||
}
|
||||
|
||||
TEST_CASE("Empty normalization plans preserve empty independently packed coordinate spaces", "[unit][normalization][dependencies]")
|
||||
{
|
||||
using Plan = normalization::NormalizationPlan<dependencies::Bindings<>, dependencies::Bindings<>>;
|
||||
const normalization::CoordinateLayout<dependencies::KeyList<>, dependencies::KeyList<>> layout{.state_sizes = {}, .residual_sizes = {}};
|
||||
const auto context = dependencies::make_dependency_context();
|
||||
auto cache = make_cache<Plan>(context, layout);
|
||||
REQUIRE(cache.try_prepare(context));
|
||||
CHECK(cache.get<normalization::keys::Normalization>().state_size() == 0);
|
||||
CHECK(cache.get<normalization::keys::Normalization>().residual_size() == 0);
|
||||
}
|
||||
119
tests/normalization/configuration.cpp
Normal file
119
tests/normalization/configuration.cpp
Normal file
@@ -0,0 +1,119 @@
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <numbers>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include "serif/solver/normalization/configuration.hpp"
|
||||
|
||||
namespace {
|
||||
namespace normalization = serif::solver::normalization;
|
||||
namespace dimensions = serif::dimensions;
|
||||
using Gravity = dimensions::QuantityValue<dimensions::GravitationalConstant>;
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
/** @brief Keep dimensional reference inputs visible in each numerical test. */
|
||||
[[nodiscard]] auto reference_scheme(const double n, const double mass, const double density) {
|
||||
return normalization::LaneEmdenApproximation{
|
||||
.n = n,
|
||||
.mass = dimensions::QuantityValue<dimensions::Mass>{mass},
|
||||
.central_density = dimensions::QuantityValue<dimensions::MassDensity>{density}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Normalization configuration preserves explicit choices without numerical preparation", "[unit][normalization][configuration]") {
|
||||
const auto defaults = normalization::make_normalization();
|
||||
CHECK(defaults.scheme.n == 3.0);
|
||||
CHECK(defaults.gravitational_constant.value() == 1.0);
|
||||
|
||||
// An invalid numerical input can still be described; preparation supplies the recoverable failure.
|
||||
const auto options = normalization::make_normalization(normalization::NormalizationOptions{
|
||||
.scheme = reference_scheme(-1.0, 7.0, 2.0),
|
||||
.coordinates = normalization::Dimensionless{},
|
||||
.gravitational_constant = Gravity{0.25}
|
||||
});
|
||||
CHECK(options.scheme.n == -1.0);
|
||||
CHECK(options.scheme.mass.value() == 7.0);
|
||||
CHECK(options.scheme.central_density.value() == 2.0);
|
||||
CHECK(options.gravitational_constant.value() == 0.25);
|
||||
CHECK_FALSE(normalization::prepare_reference_scales(options.scheme, options.gravitational_constant));
|
||||
}
|
||||
|
||||
TEST_CASE("Lane Emden normalization reference radii match analytic polytropes", "[unit][normalization][configuration]") {
|
||||
constexpr double mass = 7.0;
|
||||
constexpr double central_density = 2.0;
|
||||
constexpr double gravity = 0.25;
|
||||
for (const double n : {0.0, 1.0}) {
|
||||
const auto reference = normalization::prepare_reference_scales(reference_scheme(n, mass, central_density), Gravity{gravity});
|
||||
REQUIRE(reference);
|
||||
double radius_cubed = 3.0 * mass / (4.0 * std::numbers::pi * central_density);
|
||||
if (n == 1.0) {
|
||||
radius_cubed = std::numbers::pi * mass / (4.0 * central_density);
|
||||
}
|
||||
const double radius = std::cbrt(radius_cubed);
|
||||
CHECK_THAT(reference->radius.value(), WithinRel(radius, 2.0e-8));
|
||||
CHECK_THAT(reference->mass.value(), WithinRel(mass, 2.0e-14));
|
||||
CHECK(reference->gravitational_constant.value() == gravity);
|
||||
CHECK_THAT(reference->density.value(), WithinRel(mass / radius_cubed, 6.0e-8));
|
||||
CHECK_THAT(reference->specific_energy.value(), WithinRel(gravity * mass / radius, 2.0e-8));
|
||||
CHECK_THAT(reference->pressure.value(), WithinRel(gravity * mass * mass / std::pow(radius, 4), 8.0e-8));
|
||||
CHECK_THAT(reference->angular_velocity.value(), WithinRel(std::sqrt(gravity * mass / radius_cubed), 4.0e-8));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Lane Emden reference preparation respects consistent physical-unit conversions", "[unit][normalization][configuration]") {
|
||||
constexpr double mass_conversion = 1000.0;
|
||||
constexpr double length_conversion = 100.0;
|
||||
constexpr double time_conversion = 10000.0;
|
||||
constexpr double original_mass = 2.0;
|
||||
constexpr double original_density = 3.0;
|
||||
constexpr double original_gravity = 0.8;
|
||||
const auto original = normalization::prepare_reference_scales(reference_scheme(3.0, original_mass, original_density), Gravity{original_gravity});
|
||||
const auto converted = normalization::prepare_reference_scales(reference_scheme(
|
||||
3.0,
|
||||
original_mass * mass_conversion,
|
||||
original_density * mass_conversion / std::pow(length_conversion, 3)
|
||||
), Gravity{original_gravity * std::pow(length_conversion, 3) / (mass_conversion * time_conversion * time_conversion)});
|
||||
REQUIRE(original);
|
||||
REQUIRE(converted);
|
||||
CHECK_THAT(converted->radius.value(), WithinRel(original->radius.value() * length_conversion, 2.0e-13));
|
||||
CHECK_THAT(converted->density.value(), WithinRel(original->density.value() * mass_conversion / std::pow(length_conversion, 3), 2.0e-13));
|
||||
CHECK_THAT(converted->acceleration.value(), WithinRel(original->acceleration.value() * length_conversion / std::pow(time_conversion, 2), 2.0e-13));
|
||||
CHECK_THAT(converted->specific_energy.value(), WithinRel(original->specific_energy.value() * std::pow(length_conversion / time_conversion, 2), 2.0e-13));
|
||||
CHECK_THAT(converted->pressure.value(), WithinRel(original->pressure.value() * mass_conversion / (length_conversion * std::pow(time_conversion, 2)), 2.0e-13));
|
||||
CHECK_THAT(converted->angular_velocity.value(), WithinRel(original->angular_velocity.value() / time_conversion, 2.0e-13));
|
||||
CHECK_THAT(converted->angular_momentum.value(), WithinRel(original->angular_momentum.value() * mass_conversion * length_conversion * length_conversion / time_conversion, 2.0e-13));
|
||||
CHECK_THAT(converted->force.value(), WithinRel(original->force.value() * mass_conversion * length_conversion / std::pow(time_conversion, 2), 2.0e-13));
|
||||
}
|
||||
|
||||
TEST_CASE("Normalization reference failures stay explicit for unusable inputs and missing surfaces", "[unit][normalization][configuration]") {
|
||||
for (const double invalid : {0.0, -1.0, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::infinity()}) {
|
||||
CHECK_FALSE(normalization::prepare_reference_scales(reference_scheme(1.0, invalid, 1.0), Gravity{1.0}));
|
||||
CHECK_FALSE(normalization::prepare_reference_scales(reference_scheme(1.0, 1.0, invalid), Gravity{1.0}));
|
||||
CHECK_FALSE(normalization::prepare_reference_scales(reference_scheme(1.0, 1.0, 1.0), Gravity{invalid}));
|
||||
}
|
||||
const auto infinite_polytrope = normalization::prepare_reference_scales(reference_scheme(5.0, 1.0, 1.0), Gravity{1.0});
|
||||
REQUIRE_FALSE(infinite_polytrope);
|
||||
CHECK_FALSE(infinite_polytrope.error().empty());
|
||||
auto unresolved = reference_scheme(3.0, 1.0, 1.0);
|
||||
unresolved.integration.maximum_radius = 1.0;
|
||||
CHECK_FALSE(normalization::prepare_reference_scales(unresolved, Gravity{1.0}));
|
||||
}
|
||||
|
||||
TEST_CASE("Explicit normalization scales share validation and require a consistent G", "[unit][normalization][configuration]") {
|
||||
const auto reference = normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{2.0}, dimensions::QuantityValue<dimensions::Length>{3.0}, Gravity{0.5});
|
||||
REQUIRE(reference);
|
||||
const auto options = normalization::make_normalization(normalization::NormalizationOptions{
|
||||
.scheme = *reference,
|
||||
.gravitational_constant = Gravity{0.5}
|
||||
});
|
||||
const auto prepared = normalization::prepare_reference_scales(options.scheme, options.gravitational_constant);
|
||||
REQUIRE(prepared);
|
||||
CHECK(*prepared == *reference);
|
||||
CHECK_FALSE(normalization::prepare_reference_scales(options.scheme, Gravity{1.0}));
|
||||
auto invalid = *reference;
|
||||
invalid.radius = dimensions::QuantityValue<dimensions::Length>{0.0};
|
||||
CHECK_FALSE(normalization::prepare_reference_scales(invalid, Gravity{0.5}));
|
||||
}
|
||||
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"));
|
||||
}
|
||||
248
tests/normalization/dimensionless.cpp
Normal file
248
tests/normalization/dimensionless.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "mfem/fem/gridfunc.hpp"
|
||||
#include "mfem/fem/linearform.hpp"
|
||||
#include "serif/tests/equations/test_equations.hpp"
|
||||
#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp"
|
||||
#include "serif/backend/mfem/geometry/deformation/surface_metric.hpp"
|
||||
#include "serif/backend/mfem/solver/normalization/metric_computations.hpp"
|
||||
#include "serif/solver/normalization/normalization.hpp"
|
||||
#include "serif/tests/deformation/mesh.hpp"
|
||||
#include "serif/utils/dependencies/computation_cache.hpp"
|
||||
|
||||
namespace {
|
||||
namespace normalization = serif::solver::normalization;
|
||||
namespace dimensions = serif::dimensions;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace discretization = serif::discretization;
|
||||
namespace blocks = discretization::blocks;
|
||||
namespace deformation = serif::geometry::deformation;
|
||||
using Schema = discretization::domain::schema::CoreEnvelopeVacuumDomainSchema;
|
||||
using Kind = blocks::RepresentedValueKind;
|
||||
|
||||
/** @brief Represent the same reference star after changing numerical mass, length, and time units. */
|
||||
[[nodiscard]] normalization::StellarReferenceScales reference_scales(double length, double mass, double time) {
|
||||
const auto result = normalization::prepare_stellar_reference_scales(
|
||||
dimensions::QuantityValue<dimensions::Mass>{2.0 * mass},
|
||||
dimensions::QuantityValue<dimensions::Length>{3.0 * length},
|
||||
dimensions::QuantityValue<dimensions::GravitationalConstant>{0.7 * length * length * length / (mass * time * time)});
|
||||
REQUIRE(result);
|
||||
return *result;
|
||||
}
|
||||
|
||||
/** @brief Exercise the public dependency computations with independent physical state and residual quantities. */
|
||||
template <typename Value, typename Residual, typename ValueQuantity, typename ResidualQuantity, typename Metric>
|
||||
[[nodiscard]] normalization::DiagonalNormalization prepare_factors(const std::vector<double>& metric, const normalization::StellarReferenceScales& scales) {
|
||||
using Layout = normalization::CoordinateLayout<dependencies::KeyList<Value>, dependencies::KeyList<Residual>>;
|
||||
using Plan = normalization::NormalizationPlan<
|
||||
dependencies::Bindings<dependencies::Binding<Value, normalization::DimensionlessRieszCoordinate<ValueQuantity, Metric>>>,
|
||||
dependencies::Bindings<dependencies::Binding<Residual, normalization::DimensionlessRieszCoordinate<ResidualQuantity, Metric>>>>;
|
||||
const Layout layout{.state_sizes = {metric.size()}, .residual_sizes = {metric.size()}};
|
||||
const auto inputs = dependencies::make_dependency_context(
|
||||
dependencies::borrow<normalization::keys::Layout>(layout),
|
||||
dependencies::borrow<normalization::keys::Scales>(scales), dependencies::borrow<Metric>(metric));
|
||||
auto graph = std::apply([](auto... nodes) {
|
||||
return dependencies::make_computation_graph(std::move(nodes)...);
|
||||
}, normalization::make_normalization_computations<Plan>(layout));
|
||||
auto cache = dependencies::make_computation_cache<std::string>(std::move(graph), inputs);
|
||||
REQUIRE(cache.try_prepare(inputs));
|
||||
return cache.template get<normalization::keys::Normalization>();
|
||||
}
|
||||
|
||||
/** @brief Keep dimensionless vectors and their measured norms together for cross-unit comparisons. */
|
||||
struct MeasuredCoordinates {
|
||||
std::vector<double> state;
|
||||
std::vector<double> residual;
|
||||
double state_norm_squared;
|
||||
double residual_norm_squared;
|
||||
};
|
||||
|
||||
/** @brief Compare actual coordinates, because equal norms alone could hide different block weights or permutations. */
|
||||
void check_same_coordinates(const MeasuredCoordinates& first, const MeasuredCoordinates& second) {
|
||||
REQUIRE(first.state.size() == second.state.size());
|
||||
REQUIRE(first.residual.size() == second.residual.size());
|
||||
for (std::size_t index = 0; index < first.state.size(); ++index) {
|
||||
CHECK(second.state[index] == Catch::Approx(first.state[index]).epsilon(2.0e-11).margin(1.0e-13));
|
||||
CHECK(second.residual[index] == Catch::Approx(first.residual[index]).epsilon(2.0e-11).margin(1.0e-13));
|
||||
}
|
||||
CHECK(second.state_norm_squared == Catch::Approx(first.state_norm_squared).epsilon(2.0e-11));
|
||||
CHECK(second.residual_norm_squared == Catch::Approx(first.residual_norm_squared).epsilon(2.0e-11));
|
||||
}
|
||||
|
||||
/** @brief Use real MFEM projection and weak integration of scalar fields. */
|
||||
template <typename Term>
|
||||
[[nodiscard]] MeasuredCoordinates volume_coordinates(mfem::FiniteElementCollection& collection, double length, double mass, double time) {
|
||||
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON, 2.0 * length, 1.5 * length, 0.75 * length);
|
||||
for (int element = 0; element < mesh.GetNE(); ++element) {
|
||||
mesh.GetElement(element)->SetAttribute(2);
|
||||
}
|
||||
mesh.SetAttributes();
|
||||
mfem::FiniteElementSpace space(&mesh, &collection);
|
||||
const discretization::backend::mfem::TermBinding<Term, Schema> binding{space};
|
||||
const auto metric = discretization::backend::mfem::reference_mass_diagonal(binding);
|
||||
REQUIRE(metric);
|
||||
const normalization::StellarReferenceScales scales = reference_scales(length, mass, time);
|
||||
const double physical_scale = normalization::characteristic_scale(scales, typename Term::Quantity{}).value();
|
||||
mfem::GridFunction field(&space);
|
||||
mfem::LinearForm residual(&space);
|
||||
mfem::ConstantCoefficient scalar(0.4 * physical_scale);
|
||||
field.ProjectCoefficient(scalar);
|
||||
residual.AddDomainIntegrator(new mfem::DomainLFIntegrator(scalar));
|
||||
residual.Assemble();
|
||||
const mfem::Vector state_coefficients = binding.gather(field);
|
||||
const mfem::Vector residual_coefficients = binding.gather(residual);
|
||||
const std::span<const double> state{state_coefficients.GetData(), static_cast<std::size_t>(state_coefficients.Size())};
|
||||
const std::span<const double> weak{residual_coefficients.GetData(), static_cast<std::size_t>(residual_coefficients.Size())};
|
||||
const auto factors = prepare_factors<Term, Term, typename Term::Quantity, typename Term::Quantity, normalization::ReferenceVolumeMetric<Term>>(*metric, scales);
|
||||
MeasuredCoordinates result{.state = std::vector<double>(state.size()), .residual = std::vector<double>(weak.size()),
|
||||
.state_norm_squared = factors.state_norm_squared(state), .residual_norm_squared = factors.residual_norm_squared(weak)};
|
||||
factors.normalize_state(state, result.state);
|
||||
factors.normalize_residual(weak, result.residual);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @brief Supply an exact cellwise norm independent of a diagonal approximation to a higher-order Gram matrix. */
|
||||
struct ConstantDensityField final : blocks::field {
|
||||
struct Values final : blocks::term<ConstantDensityField> {
|
||||
using FunctionalSpace = discretization::functional_space::DiscreteFunctionalSpace<discretization::functional_space::L2, 0, 0>;
|
||||
using Support = discretization::domain::StellarDomains;
|
||||
using Quantity = dimensions::MassDensity;
|
||||
};
|
||||
};
|
||||
|
||||
/** @brief Build each native surface metric on the same physical sphere expressed in different length units. */
|
||||
template <typename Surface>
|
||||
[[nodiscard]] MeasuredCoordinates surface_coordinates(const Surface& surface, double length, double mass, double time) {
|
||||
serif::tests::deformation::PairedMesh mesh(0, 0.0, 2);
|
||||
mfem::VectorFunctionCoefficient dilation(3, [=](const mfem::Vector& point, mfem::Vector& image) {
|
||||
image = point;
|
||||
image *= length;
|
||||
});
|
||||
mesh.generated.mesh->Transform(dilation);
|
||||
const auto reference = deformation::backend::mfem::sample_reference_surface(mesh.scalar_space, mesh.surface_attribute);
|
||||
REQUIRE(reference);
|
||||
const auto embedding = deformation::build_surface_embedding(surface, reference->positions);
|
||||
REQUIRE(embedding);
|
||||
const auto metric = deformation::backend::mfem::surface_parameter_mass_diagonal(mesh.scalar_space, mesh.surface_attribute, *reference, *embedding);
|
||||
REQUIRE(metric);
|
||||
const normalization::StellarReferenceScales scales = reference_scales(length, mass, time);
|
||||
const auto factors = prepare_factors<blocks::surface_deformation::coefficients, blocks::surface_deformation::shape_equilibrium,
|
||||
dimensions::Length, dimensions::Force, deformation::keys::SurfaceMetric>(*metric, scales);
|
||||
std::vector<double> state(metric->size()), residual(metric->size());
|
||||
double expected_state_norm = 0.0;
|
||||
double expected_residual_norm = 0.0;
|
||||
double expected_work = 0.0;
|
||||
for (std::size_t index = 0; index < metric->size(); ++index) {
|
||||
const double shape = std::sin(0.7 * (index + 1));
|
||||
const double force = std::cos(0.3 * (index + 1));
|
||||
state[index] = shape * scales.radius.value();
|
||||
residual[index] = force * scales.force.value();
|
||||
const double area_fraction = (*metric)[index] / std::pow(scales.radius.value(), 2);
|
||||
expected_state_norm += area_fraction * shape * shape;
|
||||
expected_residual_norm += force * force / area_fraction;
|
||||
expected_work += shape * force;
|
||||
}
|
||||
MeasuredCoordinates result{.state = std::vector<double>(metric->size()), .residual = std::vector<double>(metric->size()),
|
||||
.state_norm_squared = factors.state_norm_squared(state), .residual_norm_squared = factors.residual_norm_squared(residual)};
|
||||
factors.normalize_state(state, result.state);
|
||||
factors.normalize_residual(residual, result.residual);
|
||||
CHECK(result.state_norm_squared == Catch::Approx(expected_state_norm).epsilon(2.0e-13));
|
||||
CHECK(result.residual_norm_squared == Catch::Approx(expected_residual_norm).epsilon(2.0e-13));
|
||||
double work = 0.0;
|
||||
for (std::size_t index = 0; index < metric->size(); ++index) {
|
||||
work += result.state[index] * result.residual[index];
|
||||
}
|
||||
CHECK(work == Catch::Approx(expected_work).epsilon(2.0e-12));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Dimensionless volume coordinates are independent of mass length and time units", "[normalization][dimensionless][integration]") {
|
||||
mfem::H1_FECollection enthalpy(3, 3);
|
||||
mfem::H1_FECollection density(blocks::density::mass::FunctionalSpace::order, 3);
|
||||
const auto check = []<typename Term>(mfem::FiniteElementCollection& collection) {
|
||||
const MeasuredCoordinates baseline = volume_coordinates<Term>(collection, 1.0, 1.0, 1.0);
|
||||
for (const std::array<double, 3> conversion : {std::array{100.0, 1000.0, 1.0}, std::array{0.03, 7.0, 0.2}}) {
|
||||
check_same_coordinates(baseline, volume_coordinates<Term>(collection, conversion[0], conversion[1], conversion[2]));
|
||||
}
|
||||
};
|
||||
check.template operator()<::serif::tests::equations::Enthalpy>(enthalpy);
|
||||
check.template operator()<blocks::density::mass>(density);
|
||||
}
|
||||
|
||||
TEST_CASE("Dimensionless cellwise state and weak residual norms recover the volume integral", "[normalization][dimensionless][math]") {
|
||||
mfem::L2_FECollection collection(0, 3);
|
||||
for (const double length : {1.0, 100.0}) {
|
||||
const MeasuredCoordinates result = volume_coordinates<ConstantDensityField::Values>(collection, length, 1000.0, 0.2);
|
||||
const double integral = 0.4 * 0.4 * (2.0 * 1.5 * 0.75) / std::pow(3.0, 3);
|
||||
CHECK(result.state_norm_squared == Catch::Approx(integral).epsilon(2.0e-13));
|
||||
CHECK(result.residual_norm_squared == Catch::Approx(integral).epsilon(2.0e-13));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Dimensionless harmonic and nodal shape norms retain force work across unit changes", "[normalization][dimensionless][integration]") {
|
||||
const auto check = []<typename Surface>(const Surface& surface) {
|
||||
const MeasuredCoordinates baseline = surface_coordinates(surface, 1.0, 1.0, 1.0);
|
||||
check_same_coordinates(baseline, surface_coordinates(surface, 100.0, 1000.0, 0.2));
|
||||
};
|
||||
check(deformation::SphericalHarmonicSurface{.maximum_degree = 2});
|
||||
check(deformation::NodalRadialSurface{});
|
||||
}
|
||||
|
||||
TEST_CASE("Dimensionless point replacements retain their own pressure scale", "[normalization][dimensionless][math]") {
|
||||
using Enthalpy = ::serif::tests::equations::Enthalpy;
|
||||
using Metric = normalization::ReferenceVolumeMetric<Enthalpy>;
|
||||
struct Selection {};
|
||||
using Layout = normalization::CoordinateLayout<dependencies::KeyList<>, dependencies::KeyList<Enthalpy>>;
|
||||
using Method = normalization::WithPointRows<normalization::DimensionlessRieszCoordinate<dimensions::SpecificEnthalpy, Metric>, dimensions::Pressure, Selection>;
|
||||
using Plan = normalization::NormalizationPlan<dependencies::Bindings<>, dependencies::Bindings<dependencies::Binding<Enthalpy, Method>>>;
|
||||
const Layout layout{.state_sizes = {}, .residual_sizes = {2}};
|
||||
const std::vector<int> rows{1};
|
||||
for (const double length : {1.0, 100.0}) {
|
||||
const auto scales = reference_scales(length, 1000.0, 0.2);
|
||||
const std::vector<double> metric{4.0 * length * length * length, std::numeric_limits<double>::quiet_NaN()};
|
||||
const auto inputs = dependencies::make_dependency_context(dependencies::borrow<normalization::keys::Layout>(layout),
|
||||
dependencies::borrow<normalization::keys::Scales>(scales), dependencies::borrow<Metric>(metric), dependencies::borrow<Selection>(rows));
|
||||
auto graph = std::apply([](auto... nodes) {
|
||||
return dependencies::make_computation_graph(std::move(nodes)...);
|
||||
}, normalization::make_normalization_computations<Plan>(layout));
|
||||
auto cache = dependencies::make_computation_cache<std::string>(std::move(graph), inputs);
|
||||
REQUIRE(cache.try_prepare(inputs));
|
||||
const auto& factors = cache.get<normalization::keys::Normalization>();
|
||||
std::array<double, 2> residual{scales.specific_energy.value() * 8.0 * std::pow(length, 3), scales.pressure.value() * 0.7};
|
||||
std::array<double, 2> normalized;
|
||||
factors.normalize_residual(residual, normalized);
|
||||
CHECK(normalized[0] == Catch::Approx(4.0 / std::sqrt(27.0)).epsilon(2.0e-13));
|
||||
CHECK(normalized[1] == Catch::Approx(0.7).epsilon(2.0e-13));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Dimensionless geometry preparation reports invalid or unrepresentable scales", "[normalization][dimensionless][validation]") {
|
||||
for (const double invalid : {0.0, -1.0, std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN()}) {
|
||||
CHECK_FALSE(normalization::detail::dimensionless_riesz_scale(Kind::value, normalization::ReferenceGeometry::volume, 1.0, invalid));
|
||||
CHECK_FALSE(normalization::detail::dimensionless_riesz_scale(Kind::residual, normalization::ReferenceGeometry::volume, invalid, 1.0));
|
||||
}
|
||||
CHECK_FALSE(normalization::detail::dimensionless_riesz_scale(Kind::value, normalization::ReferenceGeometry::volume, 1.0e300, 1.0e100));
|
||||
CHECK_FALSE(normalization::detail::dimensionless_riesz_scale(Kind::value, static_cast<normalization::ReferenceGeometry>(99), 1.0, 1.0));
|
||||
CHECK_FALSE(normalization::detail::dimensionless_riesz_scale(static_cast<Kind>(99), normalization::ReferenceGeometry::volume, 1.0, 1.0));
|
||||
}
|
||||
|
||||
TEST_CASE("Dimensionless global coordinates retain independent unknown and constraint scales", "[normalization][dimensionless][math]") {
|
||||
using MassConstant = blocks::barotropic_constant::mass_normalization;
|
||||
for (const std::array<double, 3> conversion : {std::array{1.0, 1.0, 1.0}, std::array{100.0, 1000.0, 0.2}}) {
|
||||
const auto scales = reference_scales(conversion[0], conversion[1], conversion[2]);
|
||||
const auto factors = prepare_factors<MassConstant, MassConstant, dimensions::GravitationalPotential, dimensions::Mass, normalization::UnitMetric>({1.0}, scales);
|
||||
const std::array<double, 1> state{0.3 * scales.specific_energy.value()}, residual{0.7 * scales.mass.value()};
|
||||
CHECK(factors.state_norm_squared(state) == Catch::Approx(0.09).epsilon(2.0e-13));
|
||||
CHECK(factors.residual_norm_squared(residual) == Catch::Approx(0.49).epsilon(2.0e-13));
|
||||
}
|
||||
}
|
||||
12
tests/normalization/meson.build
Normal file
12
tests/normalization/meson.build
Normal file
@@ -0,0 +1,12 @@
|
||||
normalization_test_sources = files(
|
||||
'computations.cpp',
|
||||
'configuration.cpp',
|
||||
'diagonal.cpp',
|
||||
'dimensionless.cpp',
|
||||
'operators.cpp',
|
||||
'point_rows.cpp',
|
||||
'scales.cpp',
|
||||
'surface_metric.cpp',
|
||||
'volume_computations.cpp',
|
||||
'volume_metrics.cpp',
|
||||
)
|
||||
316
tests/normalization/operators.cpp
Normal file
316
tests/normalization/operators.cpp
Normal file
@@ -0,0 +1,316 @@
|
||||
#include "serif/utils/error/errors.hpp"
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
#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 <mfem/linalg/densemat.hpp>
|
||||
#include <mfem/linalg/solvers.hpp>
|
||||
|
||||
#include "serif/backend/mfem/solver/normalization/operators.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace normalization = serif::solver::normalization;
|
||||
namespace backend = normalization::backend::mfem;
|
||||
|
||||
/** @brief Form a vector from physically readable test coordinates. */
|
||||
mfem::Vector vector(std::initializer_list<double> values)
|
||||
{
|
||||
mfem::Vector result(static_cast<int>(values.size()));
|
||||
int index = 0;
|
||||
for (const double value : values)
|
||||
{
|
||||
result(index++) = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @brief Compare coordinate actions at the original MeanField relative tolerances. */
|
||||
void check_vector(const mfem::Vector &actual, const mfem::Vector &expected, double epsilon = 2.0e-13)
|
||||
{
|
||||
REQUIRE(actual.Size() == expected.Size());
|
||||
for (int index = 0; index < actual.Size(); ++index)
|
||||
{
|
||||
CHECK(actual(index) == Catch::Approx(expected(index)).epsilon(epsilon).margin(1.0e-300));
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Exact physical inverse which records what dimensional Jacobian MFEM binds. */
|
||||
class DenseInverseSolver final : public mfem::Solver
|
||||
{
|
||||
public:
|
||||
/** @brief Copy a small reference inverse for independent solver assertions. */
|
||||
explicit DenseInverseSolver(const mfem::DenseMatrix &inverse) : mfem::Solver(inverse.Height(), inverse.Width()), m_inverse(inverse)
|
||||
{
|
||||
}
|
||||
|
||||
/** @brief Bind the dimensional Jacobian, permitting a rectangular inverse map. */
|
||||
void SetOperator(const mfem::Operator &operation) override
|
||||
{
|
||||
if (operation.Height() != Width() || operation.Width() != Height())
|
||||
{
|
||||
serif::utils::errors::serif_error<std::invalid_argument, serif::utils::errors::SERiFErrorCode::INVALID_ARGUMENT>("Dense test inverse received an incompatible Jacobian.");
|
||||
}
|
||||
bound_operator = &operation;
|
||||
++bindings;
|
||||
}
|
||||
|
||||
/** @brief Apply the exact inverse after checking the iterative initial guess. */
|
||||
void Mult(const mfem::Vector &input, mfem::Vector &output) const override
|
||||
{
|
||||
if (iterative_mode)
|
||||
{
|
||||
for (int index = 0; index < output.Size(); ++index)
|
||||
{
|
||||
CHECK(output(index) == 0.0);
|
||||
}
|
||||
}
|
||||
m_inverse.Mult(input, output);
|
||||
}
|
||||
|
||||
/** @brief Apply the transpose of the exact inverse. */
|
||||
void MultTranspose(const mfem::Vector &input, mfem::Vector &output) const override
|
||||
{
|
||||
m_inverse.MultTranspose(input, output);
|
||||
}
|
||||
|
||||
const mfem::Operator *bound_operator{nullptr};
|
||||
int bindings{0};
|
||||
|
||||
private:
|
||||
mfem::DenseMatrix m_inverse;
|
||||
};
|
||||
|
||||
/** @brief Recover dimensional matrices from a well-conditioned normalized test problem. */
|
||||
struct PhysicalSystem final
|
||||
{
|
||||
normalization::DiagonalNormalization map = *normalization::make_diagonal_normalization({1.0e-9, 2.0e7}, {5.0e8, 3.0e-6});
|
||||
mfem::DenseMatrix jacobian{2};
|
||||
mfem::DenseMatrix inverse{2};
|
||||
|
||||
/** @brief Use independently known normalized matrix and inverse entries. */
|
||||
PhysicalSystem()
|
||||
{
|
||||
constexpr double normalized_matrix[2][2]{{4.0, 1.0}, {2.0, 3.0}};
|
||||
constexpr double normalized_inverse[2][2]{{0.3, -0.1}, {-0.2, 0.4}};
|
||||
for (int row = 0; row < 2; ++row)
|
||||
{
|
||||
for (int column = 0; column < 2; ++column)
|
||||
{
|
||||
jacobian(row, column) = normalized_matrix[row][column] * map.state_factors()[column] / map.residual_factors()[row];
|
||||
inverse(row, column) = normalized_inverse[row][column] * map.residual_factors()[column] / map.state_factors()[row];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Ported from MeanField tests/normalization/physical_riesz.cpp, with transpose coverage added.
|
||||
TEST_CASE("Scaled Jacobian and inverse implement the exact coordinate change", "[normalization][unit][linear-algebra]")
|
||||
{
|
||||
const PhysicalSystem physical;
|
||||
const backend::ScaledJacobianOperator jacobian(physical.jacobian, physical.map);
|
||||
const backend::ScaledInverseOperator inverse(physical.inverse, physical.map);
|
||||
const mfem::Vector direction = vector({0.75, -1.25});
|
||||
mfem::Vector action;
|
||||
jacobian.Mult(direction, action);
|
||||
check_vector(action, vector({1.75, -2.25}), 4.0e-15);
|
||||
mfem::Vector recovered;
|
||||
inverse.Mult(action, recovered);
|
||||
check_vector(recovered, direction);
|
||||
|
||||
jacobian.MultTranspose(direction, action);
|
||||
check_vector(action, vector({0.5, -3.0}), 4.0e-15);
|
||||
inverse.MultTranspose(action, recovered);
|
||||
check_vector(recovered, direction);
|
||||
}
|
||||
|
||||
TEST_CASE("Scaled physical preconditioning preserves dimensional binding through FGMRES", "[normalization][unit][solver]")
|
||||
{
|
||||
const PhysicalSystem physical;
|
||||
const backend::ScaledJacobianOperator jacobian(physical.jacobian, physical.map);
|
||||
DenseInverseSolver inverse(physical.inverse);
|
||||
backend::ScaledPreconditioner preconditioner(inverse, jacobian);
|
||||
CHECK(inverse.bound_operator == &physical.jacobian);
|
||||
const mfem::Vector right_hand_side = vector({1.5, -0.75});
|
||||
const mfem::Vector expected = vector({0.525, -0.6});
|
||||
mfem::Vector correction(2);
|
||||
preconditioner.Mult(right_hand_side, correction);
|
||||
check_vector(correction, expected, 3.0e-13);
|
||||
|
||||
mfem::FGMRESSolver krylov;
|
||||
krylov.SetRelTol(1.0e-13);
|
||||
krylov.SetAbsTol(1.0e-15);
|
||||
krylov.SetMaxIter(4);
|
||||
krylov.SetKDim(2);
|
||||
krylov.SetPrintLevel(0);
|
||||
krylov.SetPreconditioner(preconditioner);
|
||||
krylov.SetOperator(jacobian);
|
||||
mfem::Vector solution(2);
|
||||
solution = 0.0;
|
||||
krylov.Mult(right_hand_side, solution);
|
||||
CHECK(krylov.GetConverged());
|
||||
CHECK(krylov.GetNumIterations() <= 1);
|
||||
check_vector(solution, expected, 3.0e-13);
|
||||
CHECK(inverse.bound_operator == &physical.jacobian);
|
||||
CHECK(inverse.bindings >= 2);
|
||||
CHECK(preconditioner.statistics().operator_bindings >= 2);
|
||||
CHECK(preconditioner.statistics().applications >= 2);
|
||||
|
||||
preconditioner.MultTranspose(right_hand_side, correction);
|
||||
check_vector(correction, vector({0.6, -0.45}));
|
||||
}
|
||||
|
||||
TEST_CASE("Scaled preconditioners reject unrelated preparations and malformed vectors", "[normalization][unit][validation]")
|
||||
{
|
||||
const PhysicalSystem physical;
|
||||
const backend::ScaledJacobianOperator jacobian(physical.jacobian, physical.map);
|
||||
DenseInverseSolver inverse(physical.inverse);
|
||||
backend::ScaledPreconditioner preconditioner(inverse, jacobian);
|
||||
const backend::ScaledJacobianOperator unrelated(physical.jacobian, physical.map);
|
||||
const mfem::IdentityOperator wrong_size(3);
|
||||
CHECK_THROWS_MATCHES(preconditioner.SetOperator(unrelated), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_ERROR")));
|
||||
CHECK_THROWS_MATCHES(preconditioner.SetOperator(wrong_size), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_ERROR")));
|
||||
CHECK(inverse.bindings == 1);
|
||||
|
||||
const mfem::Vector right_hand_side = vector({1.0, 2.0});
|
||||
mfem::Vector wrong_output(1);
|
||||
CHECK_THROWS_MATCHES(preconditioner.Mult(right_hand_side, wrong_output), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_VECTOR_SIZE_MISMATCH")));
|
||||
CHECK_THROWS_MATCHES(preconditioner.MultTranspose(right_hand_side, wrong_output), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_VECTOR_SIZE_MISMATCH")));
|
||||
mfem::Vector output(2);
|
||||
output = 7.0;
|
||||
CHECK_THROWS_MATCHES(preconditioner.Mult(wrong_output, output), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("NORMALIZATION_VECTOR_SIZE_MISMATCH")));
|
||||
check_vector(output, vector({7.0, 7.0}));
|
||||
CHECK(preconditioner.statistics().applications == 0);
|
||||
CHECK_THROWS_MATCHES((backend::ScaledJacobianOperator{wrong_size, physical.map}), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("MATRIX_SHAPE_ERROR")));
|
||||
CHECK_THROWS_MATCHES((backend::ScaledInverseOperator{wrong_size, physical.map}), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("MATRIX_SHAPE_ERROR")));
|
||||
}
|
||||
|
||||
TEST_CASE("Physical Riesz scaling collapses a forty-eight-decade diagonal imbalance", "[normalization][unit][numerics]")
|
||||
{
|
||||
const auto map = normalization::make_diagonal_normalization({1.0e-12, 1.0, 1.0e12}, {1.0e12, 1.0, 1.0e-12});
|
||||
REQUIRE(map);
|
||||
mfem::DenseMatrix physical(3);
|
||||
physical = 0.0;
|
||||
for (int index = 0; index < 3; ++index)
|
||||
{
|
||||
physical(index, index) = map->state_factors()[index] / map->residual_factors()[index];
|
||||
}
|
||||
CHECK(physical(2, 2) / physical(0, 0) == Catch::Approx(1.0e48));
|
||||
const backend::ScaledJacobianOperator jacobian(physical, *map);
|
||||
const mfem::Vector direction = vector({-2.0, 3.5, 0.125});
|
||||
mfem::Vector action;
|
||||
jacobian.Mult(direction, action);
|
||||
check_vector(action, direction, 4.0e-15);
|
||||
}
|
||||
|
||||
TEST_CASE("Rectangular normalized operators and their transposes preserve independent coordinate sizes", "[normalization][unit][linear-algebra]")
|
||||
{
|
||||
const auto map = normalization::make_diagonal_normalization({2.0, 3.0}, {5.0, 7.0, 11.0});
|
||||
REQUIRE(map);
|
||||
mfem::DenseMatrix physical(3, 2);
|
||||
mfem::DenseMatrix physical_inverse(2, 3);
|
||||
for (int row = 0; row < 3; ++row)
|
||||
{
|
||||
for (int column = 0; column < 2; ++column)
|
||||
{
|
||||
const double entry = 1.0 + 2.0 * row + column;
|
||||
physical(row, column) = entry * map->state_factors()[column] / map->residual_factors()[row];
|
||||
physical_inverse(column, row) = entry * map->residual_factors()[row] / map->state_factors()[column];
|
||||
}
|
||||
}
|
||||
const backend::ScaledJacobianOperator jacobian(physical, *map);
|
||||
const backend::ScaledInverseOperator inverse(physical_inverse, *map);
|
||||
mfem::Vector action;
|
||||
jacobian.Mult(vector({2.0, -0.5}), action);
|
||||
check_vector(action, vector({1.0, 4.0, 7.0}));
|
||||
jacobian.MultTranspose(vector({1.0, 2.0, 3.0}), action);
|
||||
check_vector(action, vector({22.0, 28.0}));
|
||||
inverse.Mult(vector({1.0, 2.0, 3.0}), action);
|
||||
check_vector(action, vector({22.0, 28.0}));
|
||||
inverse.MultTranspose(vector({2.0, -0.5}), action);
|
||||
check_vector(action, vector({1.0, 4.0, 7.0}));
|
||||
|
||||
DenseInverseSolver physical_solver(physical_inverse);
|
||||
backend::ScaledPreconditioner preconditioner(physical_solver, jacobian);
|
||||
CHECK(physical_solver.bound_operator == &physical);
|
||||
action.SetSize(2);
|
||||
preconditioner.Mult(vector({1.0, 2.0, 3.0}), action);
|
||||
check_vector(action, vector({22.0, 28.0}));
|
||||
action.SetSize(3);
|
||||
preconditioner.MultTranspose(vector({2.0, -0.5}), action);
|
||||
check_vector(action, vector({1.0, 4.0, 7.0}));
|
||||
}
|
||||
|
||||
TEST_CASE("Normalized adapters own frozen factors and support in-place applications", "[normalization][unit][lifetime]")
|
||||
{
|
||||
PhysicalSystem physical;
|
||||
const backend::ScaledJacobianOperator jacobian(physical.jacobian, physical.map);
|
||||
physical.map = *normalization::make_diagonal_normalization({1.0, 1.0}, {1.0, 1.0});
|
||||
CHECK(jacobian.normalization().state_factors()[0] == 1.0e-9);
|
||||
CHECK(&jacobian.physical_operator() == &physical.jacobian);
|
||||
mfem::Vector values = vector({0.75, -1.25});
|
||||
jacobian.Mult(values, values);
|
||||
check_vector(values, vector({1.75, -2.25}));
|
||||
values = vector({0.75, -1.25});
|
||||
jacobian.MultTranspose(values, values);
|
||||
check_vector(values, vector({0.5, -3.0}));
|
||||
}
|
||||
|
||||
TEST_CASE("Physical iterative inverses receive fresh zero correction guesses", "[normalization][unit][solver]")
|
||||
{
|
||||
const PhysicalSystem physical;
|
||||
const backend::ScaledJacobianOperator jacobian(physical.jacobian, physical.map);
|
||||
DenseInverseSolver inverse(physical.inverse);
|
||||
inverse.iterative_mode = true;
|
||||
backend::ScaledPreconditioner preconditioner(inverse, jacobian);
|
||||
mfem::Vector correction(2);
|
||||
correction = 41.0;
|
||||
preconditioner.Mult(vector({1.5, -0.75}), correction);
|
||||
check_vector(correction, vector({0.525, -0.6}));
|
||||
preconditioner.Mult(vector({1.5, -0.75}), correction);
|
||||
check_vector(correction, vector({0.525, -0.6}));
|
||||
}
|
||||
|
||||
TEST_CASE("Scaled preconditioners bind an initially unconfigured physical solver before checking its dimensions", "[normalization][unit][solver]")
|
||||
{
|
||||
mfem::DenseMatrix physical(2);
|
||||
physical(0, 0) = 4.0;
|
||||
physical(0, 1) = 1.0;
|
||||
physical(1, 0) = 1.0;
|
||||
physical(1, 1) = 3.0;
|
||||
const auto map = normalization::make_diagonal_normalization({2.0, 5.0}, {7.0, 11.0});
|
||||
REQUIRE(map);
|
||||
const backend::ScaledJacobianOperator jacobian(physical, *map);
|
||||
mfem::CGSolver inverse;
|
||||
inverse.SetRelTol(1.0e-14);
|
||||
inverse.SetAbsTol(1.0e-15);
|
||||
inverse.SetMaxIter(4);
|
||||
inverse.SetPrintLevel(0);
|
||||
CHECK(inverse.Width() == 0);
|
||||
CHECK(inverse.Height() == 0);
|
||||
|
||||
backend::ScaledPreconditioner preconditioner(inverse, jacobian);
|
||||
CHECK(inverse.Width() == 2);
|
||||
CHECK(inverse.Height() == 2);
|
||||
CHECK(preconditioner.statistics().operator_bindings == 1);
|
||||
mfem::Vector correction(2);
|
||||
preconditioner.Mult(vector({2.45, -3.575}), correction);
|
||||
CHECK(inverse.GetConverged());
|
||||
check_vector(correction, vector({0.25, -0.75}));
|
||||
|
||||
preconditioner.SetOperator(jacobian);
|
||||
CHECK(preconditioner.statistics().operator_bindings == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("Normalization reports MFEM index overflow before narrowing", "[normalization][unit][validation]")
|
||||
{
|
||||
const std::size_t largest_index = static_cast<std::size_t>(std::numeric_limits<int>::max());
|
||||
CHECK(backend::detail::mfem_size(0) == 0);
|
||||
CHECK(backend::detail::mfem_size(largest_index) == std::numeric_limits<int>::max());
|
||||
CHECK_THROWS_MATCHES(backend::detail::mfem_size(largest_index + 1), std::invalid_argument, Catch::Matchers::MessageMatches(Catch::Matchers::ContainsSubstring("INVALID_DOF_COUNT")));
|
||||
}
|
||||
87
tests/normalization/point_rows.cpp
Normal file
87
tests/normalization/point_rows.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#include <array>
|
||||
#include <limits>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||
|
||||
#include "serif/solver/normalization/diagonal.hpp"
|
||||
#include "serif/solver/normalization/point_rows.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace normalization = serif::solver::normalization;
|
||||
}
|
||||
|
||||
// Ported from MeanField tests/normalization/physical_riesz.cpp, without assuming matching pressure and enthalpy scales.
|
||||
TEST_CASE("Replaced residual rows use point metrics and independent physical units", "[normalization][unit][math]")
|
||||
{
|
||||
const std::array metric{4.0, 0.0, 16.0, 0.0};
|
||||
const std::array point_rows{1, 3};
|
||||
const auto enthalpy = normalization::make_point_replaced_residual_factors(metric, 5.0, point_rows, 5.0);
|
||||
REQUIRE(enthalpy);
|
||||
CHECK((*enthalpy)[0] == Catch::Approx(1.0 / 10.0));
|
||||
CHECK((*enthalpy)[1] == Catch::Approx(1.0 / 5.0));
|
||||
CHECK((*enthalpy)[2] == Catch::Approx(1.0 / 20.0));
|
||||
CHECK((*enthalpy)[3] == Catch::Approx(1.0 / 5.0));
|
||||
|
||||
const auto pressure = normalization::make_point_replaced_residual_factors(metric, 5.0, point_rows, 7.0);
|
||||
REQUIRE(pressure);
|
||||
CHECK((*pressure)[0] == (*enthalpy)[0]);
|
||||
CHECK((*pressure)[2] == (*enthalpy)[2]);
|
||||
CHECK((*pressure)[1] == Catch::Approx(1.0 / 7.0));
|
||||
CHECK((*pressure)[3] == Catch::Approx(1.0 / 7.0));
|
||||
}
|
||||
|
||||
TEST_CASE("Point normalization rejects duplicate and out-of-range physical row selections", "[normalization][unit][validation]")
|
||||
{
|
||||
const std::array metric{4.0, 9.0};
|
||||
const auto duplicate = normalization::make_point_replaced_residual_factors(metric, 1.0, std::array{1, 1}, 1.0);
|
||||
REQUIRE_FALSE(duplicate);
|
||||
CHECK_THAT(duplicate.error(), Catch::Matchers::ContainsSubstring("INVALID_ARGUMENT"));
|
||||
for (const int row : {-1, 2})
|
||||
{
|
||||
const auto outside = normalization::make_point_replaced_residual_factors(metric, 1.0, std::array{row}, 1.0);
|
||||
REQUIRE_FALSE(outside);
|
||||
CHECK_THAT(outside.error(), Catch::Matchers::ContainsSubstring("OUT_OF_RANGE_ERROR"));
|
||||
}
|
||||
CHECK_FALSE(normalization::make_point_replaced_residual_factors({}, 1.0, std::array{0}, 1.0));
|
||||
}
|
||||
|
||||
TEST_CASE("Only replaced residual rows may have unusable volume metrics", "[normalization][unit][validation]")
|
||||
{
|
||||
for (const double invalid : {0.0, -1.0, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::infinity()})
|
||||
{
|
||||
const std::array metric{4.0, invalid};
|
||||
const auto replaced = normalization::make_point_replaced_residual_factors(metric, 2.0, std::array{1}, 5.0);
|
||||
REQUIRE(replaced);
|
||||
CHECK((*replaced)[0] == Catch::Approx(0.25));
|
||||
CHECK((*replaced)[1] == Catch::Approx(0.2));
|
||||
CHECK_FALSE(normalization::make_point_replaced_residual_factors(metric, 2.0, {}, 5.0));
|
||||
CHECK_FALSE(normalization::make_point_replaced_residual_factors(metric, invalid, std::array{1}, 5.0));
|
||||
CHECK_FALSE(normalization::make_point_replaced_residual_factors(metric, 2.0, std::array{1}, invalid));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Point row composition validates factors only in the representations actually used", "[normalization][unit][math]")
|
||||
{
|
||||
const auto extreme_bulk = normalization::make_point_replaced_residual_factors(std::array{1.0e300, 0.0}, 1.0e-310, std::array{1}, 2.0);
|
||||
REQUIRE(extreme_bulk);
|
||||
CHECK((*extreme_bulk)[0] == Catch::Approx(1.0e160).epsilon(1.0e-12));
|
||||
CHECK((*extreme_bulk)[1] == 0.5);
|
||||
|
||||
const auto no_points = normalization::make_point_replaced_residual_factors(std::array{4.0}, 2.0, {}, 1.0e-310);
|
||||
REQUIRE(no_points);
|
||||
CHECK(no_points->front() == 0.25);
|
||||
CHECK_FALSE(normalization::make_point_replaced_residual_factors(std::array{4.0}, 2.0, std::array{0}, 1.0e-310));
|
||||
|
||||
const auto no_bulk = normalization::make_point_replaced_residual_factors(std::array{0.0}, 1.0e-310, std::array{0}, 2.0);
|
||||
REQUIRE(no_bulk);
|
||||
CHECK(no_bulk->front() == 0.5);
|
||||
const auto empty = normalization::make_point_replaced_residual_factors({}, 1.0e-310, {}, 1.0e-310);
|
||||
REQUIRE(empty);
|
||||
CHECK(empty->empty());
|
||||
const auto empty_factors = normalization::make_riesz_factors(serif::discretization::blocks::RepresentedValueKind::residual, {}, 1.0e-310);
|
||||
REQUIRE(empty_factors);
|
||||
CHECK(empty_factors->empty());
|
||||
}
|
||||
101
tests/normalization/scales.cpp
Normal file
101
tests/normalization/scales.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "serif/solver/normalization/scales.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace dimensions = serif::dimensions;
|
||||
namespace normalization = serif::solver::normalization;
|
||||
|
||||
/** @brief Supply typed reference values while retaining concise physical test cases. */
|
||||
std::expected<normalization::StellarReferenceScales, std::string> scales(double mass, double radius, double gravity = 1.0)
|
||||
{
|
||||
return normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{mass}, dimensions::QuantityValue<dimensions::Length>{radius}, dimensions::QuantityValue<dimensions::GravitationalConstant>{gravity});
|
||||
}
|
||||
}
|
||||
|
||||
// Ported from MeanField tests/normalization/physical_riesz.cpp.
|
||||
TEST_CASE("Characteristic stellar scales satisfy gravity virial and rotation identities", "[normalization][unit][physics]")
|
||||
{
|
||||
constexpr double mass = 7.0;
|
||||
constexpr double radius = 3.0;
|
||||
constexpr double gravity = 5.0;
|
||||
const auto prepared = scales(mass, radius, gravity);
|
||||
REQUIRE(prepared);
|
||||
const normalization::StellarReferenceScales &reference = *prepared;
|
||||
|
||||
CHECK(reference.density.value() == Catch::Approx(mass / std::pow(radius, 3)));
|
||||
CHECK(reference.acceleration.value() == Catch::Approx(gravity * mass / std::pow(radius, 2)));
|
||||
CHECK(reference.specific_energy.value() == Catch::Approx(gravity * mass / radius));
|
||||
CHECK(reference.pressure.value() == Catch::Approx(gravity * mass * mass / std::pow(radius, 4)));
|
||||
CHECK(reference.angular_velocity.value() == Catch::Approx(std::sqrt(gravity * mass / std::pow(radius, 3))));
|
||||
CHECK(reference.angular_momentum.value() == Catch::Approx(mass * std::sqrt(gravity * mass * radius)));
|
||||
|
||||
const double virial = reference.pressure.value() * std::pow(radius, 3);
|
||||
CHECK(virial == Catch::Approx(mass * reference.specific_energy.value()).epsilon(2.0e-15));
|
||||
CHECK(virial == Catch::Approx(reference.force.value() * radius).epsilon(2.0e-15));
|
||||
CHECK(reference.angular_velocity.value() * reference.angular_velocity.value() * radius == Catch::Approx(reference.acceleration.value()).epsilon(2.0e-15));
|
||||
CHECK(reference.angular_momentum.value() == Catch::Approx(mass * radius * radius * reference.angular_velocity.value()).epsilon(2.0e-15));
|
||||
}
|
||||
|
||||
TEST_CASE("Characteristic scales obey stellar homology exponents", "[normalization][unit][physics]")
|
||||
{
|
||||
const auto reference = scales(2.5, 4.0, 3.0);
|
||||
constexpr double mass_factor = 11.0;
|
||||
constexpr double radius_factor = 0.2;
|
||||
constexpr double gravity_factor = 7.0;
|
||||
const auto transformed = scales(2.5 * mass_factor, 4.0 * radius_factor, 3.0 * gravity_factor);
|
||||
REQUIRE(reference);
|
||||
REQUIRE(transformed);
|
||||
|
||||
CHECK(transformed->density.value() / reference->density.value() == Catch::Approx(mass_factor / std::pow(radius_factor, 3)).epsilon(4.0e-15));
|
||||
CHECK(transformed->acceleration.value() / reference->acceleration.value() == Catch::Approx(gravity_factor * mass_factor / std::pow(radius_factor, 2)).epsilon(4.0e-15));
|
||||
CHECK(transformed->inverse_time_squared.value() / reference->inverse_time_squared.value() == Catch::Approx(gravity_factor * mass_factor / std::pow(radius_factor, 3)).epsilon(4.0e-15));
|
||||
CHECK(transformed->specific_energy.value() / reference->specific_energy.value() == Catch::Approx(gravity_factor * mass_factor / radius_factor).epsilon(4.0e-15));
|
||||
CHECK(transformed->pressure.value() / reference->pressure.value() == Catch::Approx(gravity_factor * mass_factor * mass_factor / std::pow(radius_factor, 4)).epsilon(4.0e-15));
|
||||
CHECK(transformed->angular_velocity.value() / reference->angular_velocity.value() == Catch::Approx(std::sqrt(gravity_factor * mass_factor / std::pow(radius_factor, 3))).epsilon(4.0e-15));
|
||||
CHECK(transformed->angular_momentum.value() / reference->angular_momentum.value() == Catch::Approx(mass_factor * std::sqrt(gravity_factor * mass_factor * radius_factor)).epsilon(4.0e-15));
|
||||
CHECK(transformed->force.value() / reference->force.value() == Catch::Approx(gravity_factor * mass_factor * mass_factor / std::pow(radius_factor, 2)).epsilon(4.0e-15));
|
||||
}
|
||||
|
||||
TEST_CASE("Typed scales distinguish residual quantities and share the gravitational energy scale", "[normalization][unit][physics]")
|
||||
{
|
||||
const auto reference = scales(9.0, 2.0, 4.0);
|
||||
REQUIRE(reference);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::Dimensionless{}).value() == 1.0);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::Mass{}).value() == 9.0);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::Length{}).value() == 2.0);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::MassDensity{}) == reference->density);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::Acceleration{}) == reference->acceleration);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::InverseTimeSquared{}) == reference->inverse_time_squared);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::Pressure{}) == reference->pressure);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::AngularVelocity{}) == reference->angular_velocity);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::AngularMomentum{}) == reference->angular_momentum);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::Force{}) == reference->force);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::SpecificEnergy{}) == reference->specific_energy);
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::SpecificEnthalpy{}).value() == reference->specific_energy.value());
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::GravitationalPotential{}).value() == reference->specific_energy.value());
|
||||
CHECK(normalization::characteristic_scale(*reference, dimensions::SpecificEnthalpy{}).value() != reference->density.value());
|
||||
CHECK(*reference == *scales(9.0, 2.0, 4.0));
|
||||
CHECK_FALSE(*reference == *scales(9.0, 2.1, 4.0));
|
||||
}
|
||||
|
||||
TEST_CASE("Characteristic scale preparation rejects invalid and unrepresentable references", "[normalization][unit][validation]")
|
||||
{
|
||||
for (const double invalid : {0.0, -1.0, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::infinity()})
|
||||
{
|
||||
CHECK_FALSE(scales(invalid, 1.0));
|
||||
CHECK_FALSE(scales(1.0, invalid));
|
||||
CHECK_FALSE(scales(1.0, 1.0, invalid));
|
||||
}
|
||||
CHECK_FALSE(scales(1.0e300, 1.0e-200, 1.0e100));
|
||||
CHECK_FALSE(scales(1.0, 1.0e300));
|
||||
CHECK_FALSE(scales(1.0e-300, 1.0));
|
||||
REQUIRE(scales(1.0, 1.0));
|
||||
}
|
||||
232
tests/normalization/surface_metric.cpp
Normal file
232
tests/normalization/surface_metric.cpp
Normal file
@@ -0,0 +1,232 @@
|
||||
// MeanField normalization/spherical_harmonic_surface.cpp, composed with the existing reference geometry graph.
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "mfem/fem/coefficient.hpp"
|
||||
#include "serif/backend/mfem/geometry/deformation/computations.hpp"
|
||||
#include "serif/solver/normalization/computations.hpp"
|
||||
#include "serif/tests/deformation/mesh.hpp"
|
||||
#include "serif/utils/dependencies/computation_cache.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace deformation = serif::geometry::deformation;
|
||||
namespace backend = deformation::backend::mfem;
|
||||
namespace normalization = serif::solver::normalization;
|
||||
namespace dimensions = serif::dimensions;
|
||||
namespace blocks = serif::discretization::blocks;
|
||||
namespace keys = deformation::keys;
|
||||
namespace normalization_keys = normalization::keys;
|
||||
using Status = dependencies::PreparationStatus;
|
||||
using Failure = std::variant<deformation::DeformationFailure, std::string>;
|
||||
using Shape = blocks::surface_deformation::coefficients;
|
||||
using Force = blocks::surface_deformation::shape_equilibrium;
|
||||
using Layout = normalization::CoordinateLayout<dependencies::KeyList<Shape>, dependencies::KeyList<Force>>;
|
||||
using Plan = normalization::NormalizationPlan<
|
||||
dependencies::Bindings<dependencies::Binding<Shape, normalization::PhysicalRieszCoordinate<dimensions::Length, keys::SurfaceMetric>>>,
|
||||
dependencies::Bindings<dependencies::Binding<Force, normalization::PhysicalRieszCoordinate<dimensions::Force, keys::SurfaceMetric>>>>;
|
||||
|
||||
/** @brief Retain all borrowed reference and trial resources while selecting only reference normalization work. */
|
||||
struct SurfaceFixture
|
||||
{
|
||||
serif::tests::deformation::PairedMesh mesh{1, 0.0, 3};
|
||||
backend::ReferenceDiscretization discretization = backend::deformation_discretization(mesh.scalar_space, mesh.vector_space, *mesh.generated.reference_mesh);
|
||||
deformation::DomainDeformation<deformation::SphericalHarmonicSurface> model{.surface = {.maximum_degree = 4}, .stellar_interior = {.radial_power = 0.5}, .vacuum = {}};
|
||||
std::vector<double> coefficients{std::numeric_limits<double>::quiet_NaN()};
|
||||
Layout layout{.state_sizes = {25}, .residual_sizes = {25}};
|
||||
normalization::StellarReferenceScales scales = *normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{2.0}, dimensions::QuantityValue<dimensions::Length>{3.0});
|
||||
|
||||
/** @brief Preserve each existing deformation resource's identity and tracking when adding normalization inputs. */
|
||||
[[nodiscard]] auto inputs() const
|
||||
{
|
||||
const auto reference = backend::make_deformation_inputs(model, discretization, coefficients);
|
||||
return dependencies::make_dependency_context(
|
||||
reference.resource<keys::ReferenceDiscretization>(),
|
||||
reference.resource<keys::SurfaceDiscretization>(),
|
||||
reference.resource<keys::ReferenceCoordinates>(),
|
||||
reference.resource<keys::SurfaceModel>(),
|
||||
reference.resource<keys::InteriorModel>(),
|
||||
reference.resource<keys::VacuumModel>(),
|
||||
reference.resource<keys::ShapeCoefficients>(),
|
||||
dependencies::borrow<normalization_keys::Layout>(layout),
|
||||
dependencies::borrow<normalization_keys::Scales>(scales));
|
||||
}
|
||||
};
|
||||
|
||||
/** @brief Share one metric producer between the primal and dual coordinate factors. */
|
||||
[[nodiscard]] auto make_cache(const SurfaceFixture& fixture)
|
||||
{
|
||||
auto computations = std::tuple_cat(backend::make_deformation_computations<deformation::SphericalHarmonicSurface>(), std::tuple{backend::make_surface_metric_computation()}, normalization::make_normalization_computations<Plan>(fixture.layout));
|
||||
auto graph = std::apply([](auto... nodes)
|
||||
{
|
||||
return dependencies::make_computation_graph(std::move(nodes)...);
|
||||
}, std::move(computations));
|
||||
return dependencies::make_computation_cache<Failure>(std::move(graph), fixture.inputs());
|
||||
}
|
||||
|
||||
/** @brief Recover MeanField's primal/dual factors and cancellation of reference Gram weights in physical work. */
|
||||
void check_harmonic_factors(const normalization::DiagonalNormalization& coordinates, const std::vector<double>& metric, const normalization::StellarReferenceScales& scales)
|
||||
{
|
||||
REQUIRE(coordinates.state_size() == metric.size());
|
||||
REQUIRE(coordinates.residual_size() == metric.size());
|
||||
double physical_work = 0.0;
|
||||
double normalized_work = 0.0;
|
||||
std::vector<double> direction(metric.size()), force(metric.size()), normalized_direction(metric.size()), normalized_force(metric.size());
|
||||
for (std::size_t mode = 0; mode < metric.size(); ++mode)
|
||||
{
|
||||
CAPTURE(mode);
|
||||
REQUIRE(std::isfinite(metric[mode]));
|
||||
REQUIRE(metric[mode] > 0.0);
|
||||
CHECK(coordinates.state_factors()[mode] == Catch::Approx(std::sqrt(metric[mode]) / scales.radius.value()).epsilon(3.0e-14));
|
||||
CHECK(coordinates.residual_factors()[mode] == Catch::Approx(1.0 / (scales.force.value() * std::sqrt(metric[mode]))).epsilon(3.0e-14));
|
||||
CHECK(coordinates.state_factors()[mode] * coordinates.residual_factors()[mode] == Catch::Approx(1.0 / (scales.radius.value() * scales.force.value())).epsilon(3.0e-14));
|
||||
direction[mode] = std::sin(0.7 * (mode + 1));
|
||||
force[mode] = std::cos(0.3 * (mode + 1));
|
||||
physical_work += direction[mode] * force[mode];
|
||||
}
|
||||
coordinates.normalize_state(direction, normalized_direction);
|
||||
coordinates.normalize_residual(force, normalized_force);
|
||||
for (std::size_t mode = 0; mode < metric.size(); ++mode)
|
||||
{
|
||||
normalized_work += normalized_direction[mode] * normalized_force[mode];
|
||||
}
|
||||
CHECK(normalized_work == Catch::Approx(physical_work / (scales.radius.value() * scales.force.value())).epsilon(5.0e-14));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Harmonic normalization uses native shape metrics before candidate preparation", "[integration][normalization][meanfield]")
|
||||
{
|
||||
SurfaceFixture fixture;
|
||||
SECTION("All real azimuthal modes")
|
||||
{
|
||||
}
|
||||
SECTION("Axisymmetric modes")
|
||||
{
|
||||
fixture.model.surface.maximum_azimuthal_order = 0;
|
||||
fixture.layout = {.state_sizes = {5}, .residual_sizes = {5}};
|
||||
}
|
||||
auto cache = make_cache(fixture);
|
||||
const auto initial = cache.try_prepare(fixture.inputs(), dependencies::RequestedResults<normalization_keys::Normalization>{});
|
||||
REQUIRE(initial);
|
||||
CHECK(initial->computed_count() == 7);
|
||||
CHECK(initial->status<keys::SurfaceMetric>() == Status::computed);
|
||||
CHECK(initial->status<keys::RadialInterpolation>() == Status::not_required);
|
||||
CHECK(initial->status<keys::VolumeExtension>() == Status::not_required);
|
||||
CHECK(initial->status<keys::SurfaceDisplacement>() == Status::not_required);
|
||||
CHECK(initial->status<keys::VolumeDisplacement>() == Status::not_required);
|
||||
CHECK_FALSE(cache.contains<keys::DomainMap>());
|
||||
CHECK_FALSE(cache.contains<keys::Linearization>());
|
||||
CHECK(cache.get<keys::SurfaceMetric>().size() == fixture.layout.state_sizes[0]);
|
||||
check_harmonic_factors(cache.get<normalization_keys::Normalization>(), cache.get<keys::SurfaceMetric>(), fixture.scales);
|
||||
}
|
||||
|
||||
TEST_CASE("Reference changes refresh harmonic normalization while frozen solve coordinates remain unchanged", "[integration][normalization][meanfield]")
|
||||
{
|
||||
SurfaceFixture fixture;
|
||||
auto cache = make_cache(fixture);
|
||||
const auto requested = dependencies::RequestedResults<normalization_keys::Normalization>{};
|
||||
REQUIRE(cache.try_prepare(fixture.inputs(), requested));
|
||||
const normalization::DiagonalNormalization frozen = cache.get<normalization_keys::Normalization>();
|
||||
const std::vector<double> original_metric = cache.get<keys::SurfaceMetric>();
|
||||
const std::vector<double> original_factors(frozen.state_factors().begin(), frozen.state_factors().end());
|
||||
|
||||
fixture.coefficients.assign(25, 0.03);
|
||||
fixture.model.stellar_interior.radial_power = 3.0;
|
||||
const auto trial_changed = cache.try_prepare(fixture.inputs(), requested);
|
||||
REQUIRE(trial_changed);
|
||||
CHECK(trial_changed->computed_count() == 0);
|
||||
CHECK(trial_changed->status<keys::SurfaceMetric>() == Status::reused);
|
||||
CHECK(trial_changed->status<normalization_keys::Normalization>() == Status::reused);
|
||||
CHECK_FALSE(cache.contains<keys::RadialInterpolation>());
|
||||
|
||||
fixture.scales = *normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{4.0}, dimensions::QuantityValue<dimensions::Length>{5.0});
|
||||
const auto scales_changed = cache.try_prepare(fixture.inputs(), requested);
|
||||
REQUIRE(scales_changed);
|
||||
CHECK(scales_changed->computed_count() == 3);
|
||||
CHECK(scales_changed->status<keys::SurfaceMetric>() == Status::reused);
|
||||
CHECK(cache.get<keys::SurfaceMetric>() == original_metric);
|
||||
check_harmonic_factors(cache.get<normalization_keys::Normalization>(), original_metric, fixture.scales);
|
||||
CHECK_FALSE(std::ranges::equal(cache.get<normalization_keys::Normalization>().state_factors(), original_factors));
|
||||
CHECK(std::ranges::equal(frozen.state_factors(), original_factors));
|
||||
|
||||
mfem::VectorFunctionCoefficient stretch(3, [](const mfem::Vector& position, mfem::Vector& image)
|
||||
{
|
||||
image = position;
|
||||
image[2] *= 0.85;
|
||||
});
|
||||
fixture.mesh.generated.mesh->Transform(stretch);
|
||||
const auto reference_changed = cache.try_prepare(fixture.inputs(), requested);
|
||||
REQUIRE(reference_changed);
|
||||
CHECK(reference_changed->status<keys::SurfaceNodes>() == Status::reused);
|
||||
CHECK(reference_changed->status<keys::ReferenceSurface>() == Status::computed);
|
||||
CHECK(reference_changed->status<keys::SurfaceMetric>() == Status::computed);
|
||||
CHECK(reference_changed->status<normalization_keys::Normalization>() == Status::computed);
|
||||
CHECK(cache.get<keys::SurfaceMetric>() != original_metric);
|
||||
check_harmonic_factors(cache.get<normalization_keys::Normalization>(), cache.get<keys::SurfaceMetric>(), fixture.scales);
|
||||
CHECK(std::ranges::equal(frozen.state_factors(), original_factors));
|
||||
CHECK_FALSE(cache.contains<keys::VolumeDisplacement>());
|
||||
}
|
||||
|
||||
TEST_CASE("Prepared reference traces normalize shape without trial or extension sources", "[integration][normalization]")
|
||||
{
|
||||
SurfaceFixture fixture;
|
||||
const auto reference = backend::sample_reference_surface(fixture.mesh.scalar_space, fixture.mesh.surface_attribute);
|
||||
REQUIRE(reference);
|
||||
const auto surface = deformation::build_surface_embedding(fixture.model.surface, reference->positions);
|
||||
REQUIRE(surface);
|
||||
const auto inputs = dependencies::make_dependency_context(
|
||||
dependencies::own<keys::SurfaceDiscretization>(backend::SurfaceDiscretization{.scalar_space = fixture.mesh.scalar_space, .surface_attribute = fixture.mesh.surface_attribute}, backend::SurfaceDiscretizationTracking{}),
|
||||
dependencies::own<keys::ReferenceSurface>(std::make_shared<const deformation::ReferenceSurface>(*reference)),
|
||||
dependencies::own<keys::SurfaceMap>(std::make_shared<const deformation::SurfaceEmbedding>(*surface)),
|
||||
dependencies::borrow<normalization_keys::Layout>(fixture.layout),
|
||||
dependencies::borrow<normalization_keys::Scales>(fixture.scales));
|
||||
auto computations = std::tuple_cat(std::tuple{backend::make_surface_metric_computation()}, normalization::make_normalization_computations<Plan>(fixture.layout));
|
||||
auto graph = std::apply([](auto... nodes)
|
||||
{
|
||||
return dependencies::make_computation_graph(std::move(nodes)...);
|
||||
}, std::move(computations));
|
||||
auto cache = dependencies::make_computation_cache<Failure>(std::move(graph), inputs);
|
||||
const auto prepared = cache.try_prepare(inputs);
|
||||
REQUIRE(prepared);
|
||||
CHECK(prepared->computed_count() == 4);
|
||||
check_harmonic_factors(cache.get<normalization_keys::Normalization>(), cache.get<keys::SurfaceMetric>(), fixture.scales);
|
||||
}
|
||||
|
||||
TEST_CASE("Harmonic normalization preserves geometry and factor failure types", "[integration][normalization]")
|
||||
{
|
||||
SurfaceFixture fixture;
|
||||
auto cache = make_cache(fixture);
|
||||
const auto requested = dependencies::RequestedResults<normalization_keys::Normalization>{};
|
||||
REQUIRE(cache.try_prepare(fixture.inputs(), requested));
|
||||
SECTION("Invalid harmonic geometry retains its domain failure")
|
||||
{
|
||||
fixture.model.surface.maximum_degree = -1;
|
||||
const auto failed = cache.try_prepare(fixture.inputs(), requested);
|
||||
REQUIRE_FALSE(failed);
|
||||
REQUIRE(std::holds_alternative<deformation::DeformationFailure>(failed.error().error));
|
||||
CHECK_FALSE(std::get<deformation::DeformationFailure>(failed.error().error).message.empty());
|
||||
fixture.model.surface.maximum_degree = 4;
|
||||
}
|
||||
SECTION("Incompatible normalization layout retains its factor diagnostic")
|
||||
{
|
||||
fixture.layout.state_sizes[0] = 24;
|
||||
const auto failed = cache.try_prepare(fixture.inputs(), requested);
|
||||
REQUIRE_FALSE(failed);
|
||||
REQUIRE(std::holds_alternative<std::string>(failed.error().error));
|
||||
CHECK(std::get<std::string>(failed.error().error).find("24") != std::string::npos);
|
||||
fixture.layout.state_sizes[0] = 25;
|
||||
}
|
||||
CHECK_FALSE(cache.contains<normalization_keys::Normalization>());
|
||||
REQUIRE(cache.try_prepare(fixture.inputs(), requested));
|
||||
check_harmonic_factors(cache.get<normalization_keys::Normalization>(), cache.get<keys::SurfaceMetric>(), fixture.scales);
|
||||
}
|
||||
163
tests/normalization/volume_computations.cpp
Normal file
163
tests/normalization/volume_computations.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "mfem/fem/coefficient.hpp"
|
||||
#include "serif/discretization/domain/schema/schemas.hpp"
|
||||
#include "serif/backend/mfem/solver/normalization/metric_computations.hpp"
|
||||
#include "serif/solver/normalization/normalization.hpp"
|
||||
#include "serif/utils/dependencies/computation_cache.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace normalization = serif::solver::normalization;
|
||||
namespace backend = normalization::backend::mfem;
|
||||
namespace dependencies = serif::utils::dependencies;
|
||||
namespace dimensions = serif::dimensions;
|
||||
namespace discretization = serif::discretization;
|
||||
using Density = discretization::blocks::density::mass;
|
||||
using Schema = discretization::domain::schema::CoreEnvelopeVacuumDomainSchema;
|
||||
using Binding = discretization::backend::mfem::TermBinding<Density, Schema>;
|
||||
using Metric = normalization::ReferenceVolumeMetric<Density>;
|
||||
using ValueRule = normalization::PhysicalValueRule<Density>;
|
||||
using Plan = normalization::NormalizationPlan<dependencies::Bindings<ValueRule>, dependencies::Bindings<dependencies::Binding<Density, ValueRule::Target>>>;
|
||||
using Layout = normalization::CoordinateLayout<dependencies::KeyList<Density>, dependencies::KeyList<Density>>;
|
||||
using Status = dependencies::PreparationStatus;
|
||||
|
||||
/** @brief Exercise linear geometry that gains MFEM nodes during partial assembly as well as existing nodal geometry. */
|
||||
[[nodiscard]] mfem::Mesh make_reference_mesh(const bool nodal, const double length)
|
||||
{
|
||||
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON, 2.0 * length, length, length);
|
||||
if (nodal)
|
||||
{
|
||||
mesh.SetCurvature(2);
|
||||
}
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/** @brief Keep reference geometry, binding identity, and candidate coefficients independently observable. */
|
||||
struct VolumeFixture
|
||||
{
|
||||
mfem::Mesh mesh;
|
||||
mfem::H1_FECollection collection{Density::FunctionalSpace::order, 3};
|
||||
mfem::FiniteElementSpace space{&mesh, &collection};
|
||||
Binding binding{space};
|
||||
Layout layout{.state_sizes = {static_cast<std::size_t>(binding.size())}, .residual_sizes = {static_cast<std::size_t>(binding.size())}};
|
||||
normalization::StellarReferenceScales scales = *normalization::prepare_stellar_reference_scales(dimensions::QuantityValue<dimensions::Mass>{4.0}, dimensions::QuantityValue<dimensions::Length>{3.0});
|
||||
std::vector<double> candidate = std::vector<double>(binding.size(), 1.0);
|
||||
|
||||
/** @brief Construct equal layouts on optionally different reference geometries. */
|
||||
explicit VolumeFixture(const bool nodal, const double length = 1.0) : mesh(make_reference_mesh(nodal, length))
|
||||
{
|
||||
}
|
||||
|
||||
/** @brief Allow another live binding to replace the reference without changing context or layout types. */
|
||||
[[nodiscard]] auto inputs(Binding& reference)
|
||||
{
|
||||
return dependencies::make_dependency_context(
|
||||
backend::reference_metric_binding(reference),
|
||||
dependencies::borrow<normalization::keys::Layout>(layout),
|
||||
dependencies::borrow<normalization::keys::Scales>(scales),
|
||||
dependencies::borrow<Density>(candidate));
|
||||
}
|
||||
};
|
||||
|
||||
/** @brief Compose the shared backend metric with both normalization coordinate roles. */
|
||||
[[nodiscard]] auto make_cache(VolumeFixture& fixture)
|
||||
{
|
||||
auto computations = std::tuple_cat(std::tuple{backend::make_reference_volume_metric_computation<Density, Schema>()}, normalization::make_normalization_computations<Plan>(fixture.layout));
|
||||
auto graph = std::apply([](auto... nodes)
|
||||
{
|
||||
return dependencies::make_computation_graph(std::move(nodes)...);
|
||||
}, std::move(computations));
|
||||
return dependencies::make_computation_cache<std::string>(std::move(graph), fixture.inputs(fixture.binding));
|
||||
}
|
||||
|
||||
/** @brief Check exact scale cancellation independently of the graph's factor construction. */
|
||||
void check_factors(const normalization::DiagonalNormalization& factors, const std::vector<double>& metric, const double scale)
|
||||
{
|
||||
REQUIRE(factors.state_size() == metric.size());
|
||||
REQUIRE(factors.residual_size() == metric.size());
|
||||
for (std::size_t index = 0; index < metric.size(); ++index)
|
||||
{
|
||||
CHECK(factors.state_factors()[index] == Catch::Approx(std::sqrt(metric[index]) / scale).epsilon(3.0e-14));
|
||||
CHECK(factors.residual_factors()[index] == Catch::Approx(1.0 / (scale * std::sqrt(metric[index]))).epsilon(3.0e-14));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Reference volume normalization reuses one metric independently of trial coefficients", "[integration][normalization]")
|
||||
{
|
||||
for (const bool nodal : {false, true})
|
||||
{
|
||||
CAPTURE(nodal);
|
||||
VolumeFixture fixture(nodal);
|
||||
CHECK((fixture.mesh.GetNodes() != nullptr) == nodal);
|
||||
auto cache = make_cache(fixture);
|
||||
const auto prepared = cache.try_prepare(fixture.inputs(fixture.binding));
|
||||
REQUIRE(prepared);
|
||||
CHECK(prepared->computed_count() == 4);
|
||||
const std::vector<double> original_metric = cache.get<Metric>();
|
||||
check_factors(cache.get<normalization::keys::Normalization>(), original_metric, fixture.scales.density.value());
|
||||
|
||||
const auto unchanged = cache.try_prepare(fixture.inputs(fixture.binding));
|
||||
REQUIRE(unchanged);
|
||||
CHECK(unchanged->computed_count() == 0);
|
||||
CHECK(unchanged->status<Metric>() == Status::reused);
|
||||
std::ranges::fill(fixture.candidate, std::numeric_limits<double>::quiet_NaN());
|
||||
const auto changed_candidate = cache.try_prepare(fixture.inputs(fixture.binding));
|
||||
REQUIRE(changed_candidate);
|
||||
CHECK(changed_candidate->computed_count() == 0);
|
||||
CHECK(cache.get<Metric>() == original_metric);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Reference volume normalization follows geometry revisions and exact binding identity", "[integration][normalization]")
|
||||
{
|
||||
for (const bool nodal : {false, true})
|
||||
{
|
||||
CAPTURE(nodal);
|
||||
VolumeFixture fixture(nodal);
|
||||
auto cache = make_cache(fixture);
|
||||
REQUIRE(cache.try_prepare(fixture.inputs(fixture.binding)));
|
||||
const normalization::DiagonalNormalization frozen = cache.get<normalization::keys::Normalization>();
|
||||
const std::vector<double> original_metric = cache.get<Metric>();
|
||||
const std::vector<double> original_factors(frozen.state_factors().begin(), frozen.state_factors().end());
|
||||
|
||||
mfem::VectorFunctionCoefficient dilation(3, [](const mfem::Vector& point, mfem::Vector& image)
|
||||
{
|
||||
image = point;
|
||||
image *= 2.0;
|
||||
});
|
||||
fixture.mesh.Transform(dilation);
|
||||
const auto changed_geometry = cache.try_prepare(fixture.inputs(fixture.binding));
|
||||
REQUIRE(changed_geometry);
|
||||
CHECK(changed_geometry->status<Metric>() == Status::computed);
|
||||
const std::vector<double> dilated_metric = cache.get<Metric>();
|
||||
for (std::size_t index = 0; index < original_metric.size(); ++index)
|
||||
{
|
||||
CHECK(dilated_metric[index] == Catch::Approx(8.0 * original_metric[index]).epsilon(2.0e-12));
|
||||
}
|
||||
check_factors(cache.get<normalization::keys::Normalization>(), dilated_metric, fixture.scales.density.value());
|
||||
CHECK(std::ranges::equal(frozen.state_factors(), original_factors));
|
||||
|
||||
VolumeFixture replacement(nodal, 3.0);
|
||||
REQUIRE(replacement.binding.size() == fixture.binding.size());
|
||||
const auto changed_binding = cache.try_prepare(fixture.inputs(replacement.binding));
|
||||
REQUIRE(changed_binding);
|
||||
CHECK(changed_binding->status<Metric>() == Status::computed);
|
||||
const std::vector<double> replaced_metric = cache.get<Metric>();
|
||||
for (std::size_t index = 0; index < original_metric.size(); ++index)
|
||||
{
|
||||
CHECK(replaced_metric[index] == Catch::Approx(27.0 * original_metric[index]).epsilon(2.0e-12));
|
||||
}
|
||||
const auto replacement_unchanged = cache.try_prepare(fixture.inputs(replacement.binding));
|
||||
REQUIRE(replacement_unchanged);
|
||||
CHECK(replacement_unchanged->computed_count() == 0);
|
||||
}
|
||||
}
|
||||
300
tests/normalization/volume_metrics.cpp
Normal file
300
tests/normalization/volume_metrics.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
// Port the exact reference mass-diagonal comparisons from MeanField normalization/stellar_equilibrium.cpp.
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include "mfem/mesh/ncmesh.hpp"
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "mfem/fem/bilinearform.hpp"
|
||||
#include "mfem/fem/bilininteg.hpp"
|
||||
#include "mfem/fem/gridfunc.hpp"
|
||||
#include "mfem/general/forall.hpp"
|
||||
#include "serif/tests/equations/test_equations.hpp"
|
||||
#include "serif/backend/mfem/discretization/mass_diagonal.hpp"
|
||||
#include "serif/discretization/domain/schema/schemas.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace discretization = serif::discretization;
|
||||
namespace backend = discretization::backend::mfem;
|
||||
namespace blocks = discretization::blocks;
|
||||
namespace domain = discretization::domain;
|
||||
namespace functional_space = discretization::functional_space;
|
||||
using Schema = domain::schema::CoreEnvelopeVacuumDomainSchema;
|
||||
|
||||
/** @brief Exercise the same mass construction for stellar support and independent vector components. */
|
||||
struct TestField final : blocks::field
|
||||
{
|
||||
template <functional_space::SpaceDescriptor Space, int Order, std::size_t Rank, typename Domain = domain::StellarDomains>
|
||||
struct values final : blocks::term<TestField>
|
||||
{
|
||||
using FunctionalSpace = functional_space::DiscreteFunctionalSpace<Space, Order, Rank>;
|
||||
using Support = Domain;
|
||||
};
|
||||
};
|
||||
|
||||
/** @brief Keep shared interface coefficients while excluding the second material's volume. */
|
||||
[[nodiscard]] mfem::Mesh reference_mesh(const mfem::Element::Type geometry)
|
||||
{
|
||||
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(2, 1, 1, geometry, 2.0, 1.5, 0.75);
|
||||
for (int element = 0; element < mesh.GetNE(); ++element)
|
||||
{
|
||||
mesh.GetElement(element)->SetAttribute(element < mesh.GetNE() / 2 ? 2 : 3);
|
||||
}
|
||||
mesh.SetAttributes();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/** @brief Compare the production tensor shortcut or fallback against an independently assembled sparse matrix. */
|
||||
template <typename Term>
|
||||
void check_reference_diagonal(mfem::FiniteElementSpace& space)
|
||||
{
|
||||
const backend::TermBinding<Term, Schema> binding{space};
|
||||
const auto actual = backend::reference_mass_diagonal(binding);
|
||||
REQUIRE(actual);
|
||||
REQUIRE(actual->size() == static_cast<std::size_t>(binding.size()));
|
||||
|
||||
mfem::Array<int> marker(space.GetMesh()->attributes.Max());
|
||||
for (int attribute = 1; attribute <= marker.Size(); ++attribute)
|
||||
{
|
||||
marker[attribute - 1] = Schema::template domain_id_belongs_to<typename Term::Support>(attribute);
|
||||
}
|
||||
mfem::BilinearForm reference(&space);
|
||||
if constexpr (blocks::SpatialTermTraits<Term>::rank == 1)
|
||||
{
|
||||
reference.AddDomainIntegrator(new mfem::VectorMassIntegrator(), marker);
|
||||
}
|
||||
else
|
||||
{
|
||||
reference.AddDomainIntegrator(new mfem::MassIntegrator(), marker);
|
||||
}
|
||||
reference.Assemble();
|
||||
reference.Finalize();
|
||||
mfem::Vector full_diagonal;
|
||||
reference.SpMat().GetDiag(full_diagonal);
|
||||
const mfem::Vector expected = binding.gather(full_diagonal);
|
||||
double error_squared = 0.0;
|
||||
for (int index = 0; index < expected.Size(); ++index)
|
||||
{
|
||||
CAPTURE(index, (*actual)[index], expected[index]);
|
||||
CHECK(std::isfinite((*actual)[index]));
|
||||
CHECK((*actual)[index] > 0.0);
|
||||
error_squared += std::pow((*actual)[index] - expected[index], 2);
|
||||
}
|
||||
CHECK(std::sqrt(error_squared) <= 2.0e-11 * expected.Norml2());
|
||||
}
|
||||
|
||||
/** @brief Similar reference bodies change scalar volume metrics by length cubed and flux metrics by inverse length. */
|
||||
template <typename Term>
|
||||
void check_geometry_scaling(mfem::FiniteElementSpace& space, const double exponent)
|
||||
{
|
||||
const backend::TermBinding<Term, Schema> binding{space};
|
||||
const auto original = backend::reference_mass_diagonal(binding);
|
||||
REQUIRE(original);
|
||||
constexpr double length_scale = 2.5;
|
||||
mfem::VectorFunctionCoefficient dilation(3, [](const mfem::Vector& point, mfem::Vector& image)
|
||||
{
|
||||
image = point;
|
||||
image *= length_scale;
|
||||
});
|
||||
space.GetMesh()->Transform(dilation);
|
||||
const auto scaled = backend::reference_mass_diagonal(binding);
|
||||
REQUIRE(scaled);
|
||||
REQUIRE(scaled->size() == original->size());
|
||||
for (std::size_t index = 0; index < original->size(); ++index)
|
||||
{
|
||||
const double expected = (*original)[index] * std::pow(length_scale, exponent);
|
||||
CHECK(std::abs((*scaled)[index] - expected) <= 2.0e-12 * expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Reference mass diagonals match exact scalar assembly", "[unit][normalization][meanfield]")
|
||||
{
|
||||
for (const mfem::Element::Type geometry : {mfem::Element::HEXAHEDRON, mfem::Element::TETRAHEDRON})
|
||||
{
|
||||
CAPTURE(geometry);
|
||||
mfem::Mesh mesh = reference_mesh(geometry);
|
||||
mfem::H1_FECollection h1(3, 3);
|
||||
mfem::FiniteElementSpace enthalpy(&mesh, &h1), density(&mesh, &h1);
|
||||
check_reference_diagonal<::serif::tests::equations::Enthalpy>(enthalpy);
|
||||
check_reference_diagonal<blocks::density::mass>(density);
|
||||
mfem::H1_FECollection potential_collection(blocks::gravity::potential::FunctionalSpace::order, 3);
|
||||
mfem::FiniteElementSpace potential(&mesh, &potential_collection);
|
||||
check_reference_diagonal<blocks::gravity::potential>(potential);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Reference mass diagonals preserve vector component ordering", "[unit][normalization]")
|
||||
{
|
||||
for (const mfem::Element::Type geometry : {mfem::Element::HEXAHEDRON, mfem::Element::TETRAHEDRON})
|
||||
{
|
||||
mfem::Mesh mesh = reference_mesh(geometry);
|
||||
mfem::H1_FECollection h1(3, 3);
|
||||
mfem::L2_FECollection l2(2, 3);
|
||||
for (const mfem::Ordering::Type ordering : {mfem::Ordering::byNODES, mfem::Ordering::byVDIM})
|
||||
{
|
||||
CAPTURE(geometry, ordering);
|
||||
mfem::FiniteElementSpace displacement(&mesh, &h1, 3, ordering), velocity(&mesh, &l2, 3, ordering);
|
||||
check_reference_diagonal<blocks::displacement::vector>(displacement);
|
||||
check_reference_diagonal<TestField::values<functional_space::L2, 2, 1>>(velocity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Reference mass diagonals use curved geometry in both assembly paths", "[unit][normalization]")
|
||||
{
|
||||
for (const mfem::Element::Type geometry : {mfem::Element::HEXAHEDRON, mfem::Element::TETRAHEDRON})
|
||||
{
|
||||
CAPTURE(geometry);
|
||||
mfem::Mesh mesh = reference_mesh(geometry);
|
||||
mesh.SetCurvature(2);
|
||||
mfem::VectorFunctionCoefficient curved(3, [](const mfem::Vector& point, mfem::Vector& image)
|
||||
{
|
||||
image = point;
|
||||
image[1] += 0.06 * point[0] * point[0];
|
||||
image[2] *= 1.0 + 0.04 * point[0];
|
||||
});
|
||||
mesh.Transform(curved);
|
||||
mfem::H1_FECollection h1(3, 3);
|
||||
mfem::FiniteElementSpace enthalpy(&mesh, &h1), density(&mesh, &h1), displacement(&mesh, &h1, 3);
|
||||
check_reference_diagonal<::serif::tests::equations::Enthalpy>(enthalpy);
|
||||
check_reference_diagonal<blocks::density::mass>(density);
|
||||
check_reference_diagonal<blocks::displacement::vector>(displacement);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Reference metrics retain scalar length scaling", "[unit][normalization]")
|
||||
{
|
||||
SECTION("Scalar volume coefficients")
|
||||
{
|
||||
mfem::Mesh mesh = reference_mesh(mfem::Element::HEXAHEDRON);
|
||||
mfem::H1_FECollection h1(3, 3);
|
||||
mfem::FiniteElementSpace enthalpy(&mesh, &h1);
|
||||
check_geometry_scaling<::serif::tests::equations::Enthalpy>(enthalpy, 3.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Reference metrics retain supported ordering and reject stale material bindings", "[unit][normalization]")
|
||||
{
|
||||
mfem::Mesh mesh = reference_mesh(mfem::Element::HEXAHEDRON);
|
||||
mfem::H1_FECollection h1(3, 3);
|
||||
mfem::FiniteElementSpace space(&mesh, &h1);
|
||||
const backend::TermBinding<::serif::tests::equations::Enthalpy, Schema> binding{space};
|
||||
const auto diagonal = backend::reference_mass_diagonal(binding);
|
||||
REQUIRE(diagonal);
|
||||
CHECK(diagonal->size() < static_cast<std::size_t>(space.GetVSize()));
|
||||
|
||||
mesh.GetElement(0)->SetAttribute(3);
|
||||
CHECK_THROWS(backend::reference_mass_diagonal(binding));
|
||||
mesh.SetAttributes();
|
||||
const backend::TermBinding<::serif::tests::equations::Enthalpy, Schema> empty_binding{space};
|
||||
const auto empty = backend::reference_mass_diagonal(empty_binding);
|
||||
REQUIRE(empty);
|
||||
CHECK(empty->empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Reference metrics reject zero volume rather than constructing singular normalization", "[unit][normalization]")
|
||||
{
|
||||
mfem::Mesh mesh = reference_mesh(mfem::Element::HEXAHEDRON);
|
||||
mfem::VectorFunctionCoefficient collapsed(3, [](const mfem::Vector& point, mfem::Vector& image)
|
||||
{
|
||||
image = point;
|
||||
image[2] = 0.0;
|
||||
});
|
||||
mesh.Transform(collapsed);
|
||||
mfem::H1_FECollection h1(3, 3);
|
||||
mfem::FiniteElementSpace space(&mesh, &h1);
|
||||
const backend::TermBinding<::serif::tests::equations::Enthalpy, Schema> binding{space};
|
||||
const auto diagonal = backend::reference_mass_diagonal(binding);
|
||||
REQUIRE_FALSE(diagonal);
|
||||
CHECK(diagonal.error().find("positive finite") != std::string::npos);
|
||||
}
|
||||
|
||||
namespace {
|
||||
/** @brief Assemble an independent dense element oracle before MFEM applies hanging-node constraints. */
|
||||
class ReconstructedMass final : public mfem::BilinearFormIntegrator {
|
||||
public:
|
||||
/** @brief Borrow the weighted binding and select a verification quadrature increment. */
|
||||
ReconstructedMass(const backend::DofMap& binding, int increment) : m_binding(binding), m_increment(increment) {}
|
||||
/** @brief Preserve every local cross term, so the oracle tests constrained diagonal reduction. */
|
||||
void AssembleElementMatrix(const mfem::FiniteElement& element, mfem::ElementTransformation& transformation,
|
||||
mfem::DenseMatrix& matrix) override {
|
||||
const auto* reconstruction = m_binding.reconstruction_element(transformation.ElementNo);
|
||||
const int order = mfem::MassIntegrator::GetRule(element, element, transformation).GetOrder() + m_increment;
|
||||
const auto& rule = mfem::IntRules.Get(element.GetGeomType(), order);
|
||||
matrix.SetSize(element.GetDof());
|
||||
matrix = 0.0;
|
||||
mfem::Vector shapes(element.GetDof());
|
||||
mfem::DenseMatrix derivatives;
|
||||
for (int index = 0; index < rule.GetNPoints(); ++index) {
|
||||
const auto& point = rule.IntPoint(index);
|
||||
if (reconstruction != nullptr) { reconstruction->sample(element, point, shapes, derivatives); }
|
||||
else { element.CalcShape(point, shapes); }
|
||||
transformation.SetIntPoint(&point);
|
||||
const double weight = point.weight * transformation.Weight();
|
||||
for (int row = 0; row < element.GetDof(); ++row) {
|
||||
for (int column = 0; column < element.GetDof(); ++column) { matrix(row, column) += weight * shapes[row] * shapes[column]; }
|
||||
}
|
||||
}
|
||||
}
|
||||
private:
|
||||
const backend::DofMap& m_binding;
|
||||
int m_increment;
|
||||
};
|
||||
|
||||
/** @brief Form the actual sparse mass matrix, then reduce all couplings before reading its diagonal. */
|
||||
[[nodiscard]] mfem::Vector reconstructed_diagonal_oracle(const backend::DofMap& binding, int increment) {
|
||||
mfem::BilinearForm mass(&binding.space());
|
||||
mass.AddDomainIntegrator(new ReconstructedMass(binding, increment));
|
||||
mass.Assemble();
|
||||
mass.Finalize();
|
||||
mfem::Vector diagonal;
|
||||
if (binding.constrained()) {
|
||||
const auto& prolongation = *binding.space().GetConformingProlongation();
|
||||
const std::unique_ptr<mfem::SparseMatrix> reduced(mfem::RAP(prolongation, mass.SpMat(), prolongation));
|
||||
reduced->GetDiag(diagonal);
|
||||
return binding.gather_true(diagonal);
|
||||
}
|
||||
mass.SpMat().GetDiag(diagonal);
|
||||
return binding.gather(diagonal);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Surface-power reference metrics retain constrained cross terms and converge under quadrature", "[unit][normalization][reconstruction]") {
|
||||
for (bool constrained : {false, true}) {
|
||||
mfem::Mesh mesh = mfem::Mesh::MakeCartesian3D(2, 1, 1, mfem::Element::HEXAHEDRON);
|
||||
mfem::Array<int> vertices;
|
||||
for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) {
|
||||
mesh.GetBdrElementVertices(boundary, vertices);
|
||||
bool surface = true;
|
||||
for (int vertex : vertices) { surface = surface && mesh.GetVertex(vertex)[2] == 0.0; }
|
||||
mesh.GetBdrElement(boundary)->SetAttribute(surface ? 1 : 2);
|
||||
}
|
||||
mesh.SetAttributes();
|
||||
if (constrained) {
|
||||
mfem::Array<mfem::Refinement> refinement;
|
||||
refinement.Append(mfem::Refinement(0, mfem::Refinement::Y));
|
||||
mesh.GeneralRefinement(refinement, 1);
|
||||
}
|
||||
mfem::H1_FECollection collection(3, 3);
|
||||
mfem::FiniteElementSpace space(&mesh, &collection);
|
||||
const backend::TermBinding<blocks::density::mass, Schema> ordinary(space);
|
||||
const auto plan = std::make_shared<backend::ReconstructionPlan>(ordinary, 1,
|
||||
std::make_shared<discretization::reconstruction::SurfacePowerProfile>(5.0 / 3.0));
|
||||
const backend::TermBinding<blocks::density::mass, Schema> binding(space, plan);
|
||||
REQUIRE(binding.constrained() == constrained);
|
||||
const auto actual = backend::reference_mass_diagonal(binding);
|
||||
REQUIRE(actual);
|
||||
const mfem::Vector oracle = reconstructed_diagonal_oracle(binding, plan->integration_order_increment());
|
||||
const mfem::Vector refined = reconstructed_diagonal_oracle(binding, plan->integration_order_increment() + 16);
|
||||
for (int index = 0; index < binding.size(); ++index) {
|
||||
CAPTURE(constrained, index);
|
||||
CHECK((*actual)[index] > 0.0);
|
||||
CHECK(std::abs((*actual)[index] - oracle[index]) <= 2e-11 * oracle[index]);
|
||||
// Fractional powers are not integrated exactly by a finite Gaussian rule.
|
||||
CHECK(std::abs((*actual)[index] - refined[index]) <= 3e-4 * refined[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user