#include #include #include #include #include #include #include #include #include #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{2.0 * mass}, dimensions::QuantityValue{3.0 * length}, dimensions::QuantityValue{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 [[nodiscard]] normalization::DiagonalNormalization prepare_factors(const std::vector& metric, const normalization::StellarReferenceScales& scales) { using Layout = normalization::CoordinateLayout, dependencies::KeyList>; using Plan = normalization::NormalizationPlan< dependencies::Bindings>>, dependencies::Bindings>>>; const Layout layout{.state_sizes = {metric.size()}, .residual_sizes = {metric.size()}}; const auto inputs = dependencies::make_dependency_context( dependencies::borrow(layout), dependencies::borrow(scales), dependencies::borrow(metric)); auto graph = std::apply([](auto... nodes) { return dependencies::make_computation_graph(std::move(nodes)...); }, normalization::make_normalization_computations(layout)); auto cache = dependencies::make_computation_cache(std::move(graph), inputs); REQUIRE(cache.try_prepare(inputs)); return cache.template get(); } /** @brief Keep dimensionless vectors and their measured norms together for cross-unit comparisons. */ struct MeasuredCoordinates { std::vector state; std::vector 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 [[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 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 state{state_coefficients.GetData(), static_cast(state_coefficients.Size())}; const std::span weak{residual_coefficients.GetData(), static_cast(residual_coefficients.Size())}; const auto factors = prepare_factors>(*metric, scales); MeasuredCoordinates result{.state = std::vector(state.size()), .residual = std::vector(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 { using FunctionalSpace = discretization::functional_space::DiscreteFunctionalSpace; 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 [[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(*metric, scales); std::vector 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(metric->size()), .residual = std::vector(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 = [](mfem::FiniteElementCollection& collection) { const MeasuredCoordinates baseline = volume_coordinates(collection, 1.0, 1.0, 1.0); for (const std::array conversion : {std::array{100.0, 1000.0, 1.0}, std::array{0.03, 7.0, 0.2}}) { check_same_coordinates(baseline, volume_coordinates(collection, conversion[0], conversion[1], conversion[2])); } }; check.template operator()<::serif::tests::equations::Enthalpy>(enthalpy); check.template operator()(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(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 = [](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; struct Selection {}; using Layout = normalization::CoordinateLayout, dependencies::KeyList>; using Method = normalization::WithPointRows, dimensions::Pressure, Selection>; using Plan = normalization::NormalizationPlan, dependencies::Bindings>>; const Layout layout{.state_sizes = {}, .residual_sizes = {2}}; const std::vector rows{1}; for (const double length : {1.0, 100.0}) { const auto scales = reference_scales(length, 1000.0, 0.2); const std::vector metric{4.0 * length * length * length, std::numeric_limits::quiet_NaN()}; const auto inputs = dependencies::make_dependency_context(dependencies::borrow(layout), dependencies::borrow(scales), dependencies::borrow(metric), dependencies::borrow(rows)); auto graph = std::apply([](auto... nodes) { return dependencies::make_computation_graph(std::move(nodes)...); }, normalization::make_normalization_computations(layout)); auto cache = dependencies::make_computation_cache(std::move(graph), inputs); REQUIRE(cache.try_prepare(inputs)); const auto& factors = cache.get(); std::array residual{scales.specific_energy.value() * 8.0 * std::pow(length, 3), scales.pressure.value() * 0.7}; std::array 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::infinity(), std::numeric_limits::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(99), 1.0, 1.0)); CHECK_FALSE(normalization::detail::dimensionless_riesz_scale(static_cast(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 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({1.0}, scales); const std::array 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)); } }