#include #include #include #include #include #include #include #include #include #include #include "serif/utils/serialize/numerics.hpp" namespace { namespace serialization = serif::utils::serialize; namespace blocks = serif::discretization::blocks; namespace dependencies = serif::utils::dependencies; namespace model = serif::model; namespace equations = serif::equations; namespace dimensions = serif::dimensions; namespace normalization = serif::solver::normalization; using EOS = serif::eos::models::Polytrope; using Coordinates = dependencies::KeyList; using Rows = dependencies::KeyList; using Layout = blocks::SystemLayout; /** @brief Exercise the complete concrete barotropic equation description without a mesh. */ [[nodiscard]] auto physical_description(const EOS& eos) { return model::EquilibriumEquations{ .interior_equations = equations::EquationRegistry{equations::BarotropicDensityBalance{eos, dimensions::PressureValue{0.25}}, equations::PrimalPoissonBalance{12.5}, equations::MassNormalization{4.0}}, .shape_force = equations::SurfacePotentialBalance{}, .surface_condition = serif::geometry::surface::Isobaric{dimensions::PressureValue{0.25}} }; } } TEST_CASE("Serialized stellar physics reconstructs every concrete barotropic component", "[unit][serialize][serialize_components]") { auto original = model::make_stellar_model(EOS{1.5, 2.75}, physical_description(EOS{1.5, 2.75}), model::FixedTotalMass{dimensions::MassValue{4.0}}, model::FixedAngularMomentum{{.angular_momentum = dimensions::AngularMomentumValue{0.3}, .axis = {0, 1, 0}, .center = {0.2, -0.4, 0.1}}}); const auto node = serialization::encode(original); const auto restored = serialization::decode>(node); REQUIRE(restored); CHECK(serialization::encode(*restored) == node); CHECK(restored->equation_of_state().polytropic_constant() == 2.75); CHECK(restored->template specification().center()[1] == -0.4); auto invalid = node; invalid.children.at("eos").children.at("index") = serialization::encode(-2.0); const auto rejected = serialization::decode>(invalid); REQUIRE_FALSE(rejected); CHECK(rejected.error().path == "eos"); } TEST_CASE("Scalar equilibrium archives retain the density balance, primal gravity and the isobar level", "[unit][serialize][serialize_components]") { const EOS eos{1.5, 2.75}; const std::array axis{0.0, 1.0, 0.0}; const std::array center{0.2, -0.4, 0.1}; const auto original = model::make_stellar_model(eos, model::EquilibriumEquations{ .interior_equations = equations::EquationRegistry{equations::BarotropicDensityBalance{eos, dimensions::PressureValue{0.25}, axis, center}, equations::PrimalPoissonBalance{12.5}, equations::MassNormalization{4.0}, equations::AngularMomentumConstraint{0.3, axis, center}}, .shape_force = equations::SurfacePotentialBalance{axis, center}, .surface_condition = serif::geometry::surface::Isobaric{dimensions::PressureValue{0.25}}}, model::FixedTotalMass{dimensions::MassValue{4.0}}, model::FixedAngularMomentum{{.angular_momentum = dimensions::AngularMomentumValue{0.3}, .axis = axis, .center = center}}); const auto node = serialization::encode(original); const auto restored = serialization::decode>(node); REQUIRE(restored); CHECK(serialization::encode(*restored) == node); CHECK(restored->equations().interior_equations.template get().four_pi_G() == 12.5); CHECK(restored->equations().shape_force.rotation().axis_origin() == center); CHECK(restored->template specification().target().value() == 4.0); } TEST_CASE("Barotropic density archives preserve surface pressure and a tilted displaced rotation axis", "[unit][serialize][serialize_components][barotropic_density]") { using Balance = equations::BarotropicDensityBalance; const std::array axis{0.0, 3.0, 4.0}, center{0.2, -0.4, 0.1}; const Balance original{EOS{2.0, 0.4}, dimensions::PressureValue{0.05}, axis, center}; const auto node = serialization::encode(original); CHECK(node.type == "equation.barotropic_density"); const auto restored = serialization::decode(node); REQUIRE(restored); CHECK(restored->surface_pressure().value() == 0.05); CHECK(restored->equation_of_state().polytropic_index() == 2.0); CHECK(restored->equation_of_state().polytropic_constant() == 0.4); CHECK(restored->rotation().axis_origin() == center); equations::EquationPoint point; point.fields.get() = 1.44; point.fields.get() = -0.7; point.fields.get() = -0.9; point.fields.get() = 0.3; point.position = {1.2, 0.7, -0.2}; const auto expected = original.evaluate(point), actual = restored->evaluate(point); // For n=2, K=0.4, P_surface=0.05: rho_surface=0.25 and A(rho)-A_surface=0.84. CHECK(actual.residual == Catch::Approx(1.04 - 0.09 * original.rotation().centrifugal_potential(point.position)).margin(2.0e-15)); CHECK(actual.residual == Catch::Approx(expected.residual).margin(2.0e-15)); for (std::size_t index = 0; index < actual.field_derivatives.values.size(); ++index) { CHECK(actual.field_derivatives.values[index] == Catch::Approx(expected.field_derivatives.values[index]).margin(2.0e-15)); } for (int component = 0; component < 3; ++component) { CHECK(restored->rotation().angular_velocity_vector()[component] == Catch::Approx(original.rotation().angular_velocity_vector()[component]).margin(2.0e-15)); CHECK(actual.position_derivative[component] == Catch::Approx(expected.position_derivative[component]).margin(2.0e-15)); } // Deserialization must own formerly borrowed physics and reconstruct its finite-pressure reference. const auto borrowed_record = [&]() { const EOS temporary{2.0, 0.4}; return serialization::encode(equations::BarotropicDensityBalance{model::borrow_eos(temporary), dimensions::PressureValue{0.05}, axis, center}); }(); using Borrowed = equations::BarotropicDensityBalance>; const auto reowned = serialization::decode(borrowed_record); REQUIRE(reowned); CHECK(reowned->equation_of_state().get().polytropic_constant() == 0.4); CHECK(reowned->evaluate(point).residual == Catch::Approx(expected.residual).margin(2.0e-15)); CHECK_FALSE(serialization::decode(node)); auto invalid = node; invalid.children.at("surface_pressure") = serialization::encode(dimensions::PressureValue{-0.1}); CHECK_FALSE(serialization::decode(invalid)); invalid = node; invalid.children.at("axis") = serialization::encode(std::array{0.0, 0.0, 0.0}); CHECK_FALSE(serialization::decode(invalid)); } TEST_CASE("Deferred model archives restore current physics without pretending to recreate a callback", "[unit][serialize][serialize_components]") { const double coefficient = 2.0; auto original = model::make_stellar_model(EOS{1.0, 1.0}, [coefficient](const auto& star) { return model::EquilibriumEquations{equations::EquationRegistry{equations::PrimalPoissonBalance{coefficient * star.equation_of_state().polytropic_index()}}}; }); const auto node = serialization::encode(original); CHECK(node.children.at("authored_definition").type == "unsupported"); CHECK_FALSE(serialization::decode>(node)); const auto restored = serialization::restore_stellar_model(node); REQUIRE(restored); CHECK(restored->equations().interior_equations.template get().four_pi_G() == coefficient); CHECK(serialization::encode(restored->equations()) == node.children.at("resolved_equations")); } TEST_CASE("Physical state serialization preserves coefficients and rejects mismatched coordinate schemas", "[unit][serialize][serialize_components]") { const Layout layout{{2, 3, 2, 1}, {2, 3, 2, 1}}; std::vector coefficients{0.0, -0.0, 1.0e-250, -1.0e200, 2.0, 0.2, -0.3, 1.5}; const model::StellarState original{layout, coefficients}; auto node = serialization::encode(original); const auto restored = serialization::decode>(node); REQUIRE(restored); REQUIRE(restored->values().size() == coefficients.size()); for (std::size_t index = 0; index < coefficients.size(); ++index) { CHECK(std::bit_cast(restored->values()[index]) == std::bit_cast(coefficients[index])); } node.children.at("layout").children.at("values").children.at("0").children.at("id") = serialization::encode(std::string{"gravity.potential"}); CHECK_FALSE(serialization::decode>(node)); node = serialization::encode(original); node.children.at("coefficients") = serialization::encode(std::vector{1.0}); CHECK_FALSE(serialization::decode>(node)); } TEST_CASE("Default numerical recipes retain resolved parameters rather than selecting fresh defaults", "[unit][serialize][serialize_components]") { using PrimalValues = dependencies::KeyList; using PrimalRows = dependencies::KeyList; const blocks::SystemLayout primal_layout{{2, 3, 1, 1, 2}, {2, 3, 1, 1, 2}}; const auto primal_inverse = serif::solver::preconditioner::make_primal_preconditioner(primal_layout); const auto restored_primal = serialization::decode>(serialization::encode(primal_inverse)); REQUIRE(restored_primal); CHECK(serialization::encode(*restored_primal) == serialization::encode(primal_inverse)); auto options = serif::solver::SolverOptions<>{}; options.method.maximum_iterations = 123; options.linear_solver.restart_length = 31; options.line_search.contraction = 0.4; const auto restored_options = serialization::decode(serialization::encode(options)); REQUIRE(restored_options); CHECK(serialization::encode(*restored_options) == serialization::encode(options)); const auto numerical = normalization::make_normalization(normalization::NormalizationOptions{ .scheme = normalization::LaneEmdenApproximation{.n = 2.5, .mass = dimensions::MassValue{4.}, .central_density = dimensions::DensityValue{2.}}, .coordinates = normalization::Dimensionless{}, .gravitational_constant = dimensions::QuantityValue{0.75}}); const auto restored_numerical = serialization::decode>(serialization::encode(numerical)); REQUIRE(restored_numerical); CHECK(serialization::encode(*restored_numerical) == serialization::encode(numerical)); const auto factors = normalization::make_diagonal_normalization({2., 4.}, {0.1, 0.3}); REQUIRE(factors); auto saved_factors = serialization::encode(*factors); REQUIRE(serialization::decode(saved_factors)); saved_factors.children.at("state") = serialization::encode(std::vector{-1., 4.}); CHECK_FALSE(serialization::decode(saved_factors)); } TEST_CASE("Surface representations retain physical centers and harmonic selection", "[unit][serialize][serialize_components]") { using namespace serif::geometry::deformation; const DomainDeformation original{SphericalHarmonicSurface{.reference_center = {0.1, -0.2, 0.3}, .maximum_degree = 8, .maximum_azimuthal_order = 3}, PowerLawRadialExtension{.radial_power = 1.75}, FixedInfinityRadialExtension{}}; const auto restored = serialization::decode>(serialization::encode(original)); REQUIRE(restored); CHECK(*restored == original); const auto nodal = serialization::decode(serialization::encode(NodalRadialSurface{{1., 2., 3.}})); REQUIRE(nodal); CHECK(nodal->reference_center[2] == 3.0); } TEST_CASE("Quantity serializers reject incorrect dimensions and unknown schema versions", "[unit][serialize][serialize_components]") { auto node = serialization::encode(dimensions::MassValue{7.0}); CHECK_FALSE(serialization::decode(node)); node.version = 17; CHECK_FALSE(serialization::decode(node)); } TEST_CASE("Quadrature role controls survive serialization without compiled caches", "[unit][serialize][serialize_components]") { namespace quadrature = serif::discretization::quadrature; quadrature::RuleSet rules; rules.set_role_control(quadrature::QuadratureRole::projection, {.fixed_order = 12, .boost = 2}); rules.set_fallback_control({.boost = 3}); const quadrature::Policy original{rules}; const auto restored = serialization::decode(serialization::encode(original)); REQUIRE(restored); CHECK(restored->rules().role_control(quadrature::QuadratureRole::projection).fixed_order == 12); CHECK(restored->rules().fallback_control().boost == 3); } TEST_CASE("Prescribed input serialization distinguishes owned values from borrowed runtime objects", "[unit][serialize][serialize_components]") { const auto inputs = dependencies::make_dependency_context(dependencies::own(0.3)); const auto restored = serialization::decode>(serialization::encode(inputs)); REQUIRE(restored); CHECK(restored->template get() == 0.3); double prescribed = 0.4; const auto borrowed = dependencies::make_dependency_context(dependencies::borrow(prescribed)); const auto node = serialization::encode(borrowed); CHECK(node.children.at("resources").children.at("0").children.at("reconstruction").type == "unsupported"); CHECK_FALSE(serialization::decode>(node)); } TEST_CASE("Decoded numerical options reject invalid budgets and approximation controls", "[unit][serialize][serialize_components]") { auto options = serialization::encode(serif::solver::SolverOptions<>{}); options.children.at("method").children.at("maximum_iterations") = serialization::encode(-3); const auto invalid_solver = serialization::decode>(options); REQUIRE_FALSE(invalid_solver); CHECK(invalid_solver.error().path == "method"); using AMG = serif::solver::preconditioner::strategies::inverse::FixedAMG; CHECK_FALSE(serialization::decode(serialization::encode(AMG{.cycles = 0}))); using Surface = serif::geometry::deformation::SphericalHarmonicSurface; CHECK_FALSE(serialization::decode(serialization::encode(Surface{.maximum_degree = 2, .maximum_azimuthal_order = 3}))); using Integration = serif::utils::laneemden::IntegrationOptions; CHECK_FALSE(serialization::decode(serialization::encode(Integration{.maximum_steps = 0}))); } TEST_CASE("Custom quadrature form controls remain inspectable when their descriptor cannot be reowned", "[unit][serialize][serialize_components]") { namespace quadrature = serif::discretization::quadrature; quadrature::RuleSet rules; rules.set_form_control({.name = "custom_mass", .trial = {.field = {"density"}, .name = "mass"}, .test = {.field = {"density"}, .name = "mass"}}, {.boost = 7}); const auto node = serialization::encode(rules); CHECK(node.children.at("forms").children.at("reconstruction").type == "unsupported"); const auto boost = serialization::decode(node.children.at("forms").children.at("0").children.at("control").children.at("boost")); REQUIRE(boost); CHECK(*boost == 7); CHECK_FALSE(serialization::decode(node)); } TEST_CASE("Configured discretizations preserve owned normalization inputs and resolved choices", "[unit][serialize][serialize_components]") { auto star = model::make_stellar_model(EOS{1.5, 2.75}, physical_description(EOS{1.5, 2.75}), model::FixedTotalMass{dimensions::MassValue{4.0}}); auto numerical = normalization::NormalizationOptions{ .scheme = normalization::LaneEmdenApproximation{.central_density = dimensions::DensityValue{2.0}}, .sources = dependencies::make_dependency_context(dependencies::own(0.3))}; auto original = model::make_discretization(model::DiscretizationOptions{ .model = std::move(star), .finite_elements = std::vector{0.0, 0.5, 1.0}, .normalization = std::move(numerical)}); const auto node = serialization::encode(original); CHECK(node.type == "model.configured_discretization"); const auto restored = serialization::decode(node); REQUIRE(restored); CHECK(serialization::encode(*restored) == node); CHECK(restored->normalization.scheme.mass == dimensions::MassValue{4.0}); CHECK(restored->normalization.sources.template get() == 0.3); }