#include #include #include #include #include #include #include #include #include #include #include #include #include #include import mean_field; import test_helpers; namespace normalization_policy_extension_test { /* A deliberately simple third normalization family. It is neither the * library identity policy nor Physical Riesz: its two constant factors are * runtime data owned by the compile-time discretization policy type. */ class UniformDiagonal final : public mean_field::normalization::NormalizationPrescriptionTag { public: UniformDiagonal( const double stateFactor, const double residualFactor ) : m_stateFactor(stateFactor), m_residualFactor(residualFactor) { if (!std::isfinite(stateFactor) || stateFactor <= 0.0 || !std::isfinite(residualFactor) || residualFactor <= 0.0) { throw std::invalid_argument( "Uniform diagonal normalization requires finite, positive factors." ); } } [[nodiscard]] double stateFactor() const noexcept { return m_stateFactor; } [[nodiscard]] double residualFactor() const noexcept { return m_residualFactor; } private: double m_stateFactor; double m_residualFactor; }; struct CompilationOnly final : mean_field::normalization::NormalizationPrescriptionTag { }; /* This deliberately claims identity coordinates while also requesting * the third-party runtime adapter. The type-level runtime audit must reject * that semantic mismatch even though both declarations exist. */ struct MisdeclaredRuntime final : mean_field::normalization::NormalizationPrescriptionTag { }; /* Adversarial policies isolate two other extension-boundary failures: * borrowing another policy's coordinate ownership, and declaring a sound * plan without implementing the corresponding runtime operation. */ struct BorrowedRuntime final : mean_field::normalization::NormalizationPrescriptionTag { }; struct MissingPreparation final : mean_field::normalization::NormalizationPrescriptionTag { }; struct WrongPreparationResult final : mean_field::normalization::NormalizationPrescriptionTag { }; class MoveOnlyDiagonal final : public mean_field::normalization::NormalizationPrescriptionTag { public: MoveOnlyDiagonal( const double stateFactor, const double residualFactor ) : m_stateFactor(stateFactor), m_residualFactor(residualFactor) { } MoveOnlyDiagonal(const MoveOnlyDiagonal &) = delete; MoveOnlyDiagonal &operator=(const MoveOnlyDiagonal &) = delete; MoveOnlyDiagonal(MoveOnlyDiagonal &&) noexcept = default; MoveOnlyDiagonal &operator=(MoveOnlyDiagonal &&) noexcept = default; [[nodiscard]] double stateFactor() const noexcept { return m_stateFactor; } [[nodiscard]] double residualFactor() const noexcept { return m_residualFactor; } private: double m_stateFactor; double m_residualFactor; }; } // namespace normalization_policy_extension_test namespace mean_field::normalization { template requires utils::blocks::block_form_is_valid_v
struct NormalizationCompilation< normalization_policy_extension_test::UniformDiagonal, Form> : RuntimePreparedNormalizationCompilation< normalization_policy_extension_test::UniformDiagonal, Form> { }; /* Deliberately omits runtimeAvailableFor. A symbolic plan alone must not * make this prescription usable by discretize(). */ template requires utils::blocks::block_form_is_valid_v struct NormalizationCompilation< normalization_policy_extension_test::CompilationOnly, Form> { using Plan = RuntimePreparedNormalizationPlanFor< normalization_policy_extension_test::CompilationOnly, Form>; static constexpr bool registered = CompleteNormalizationFor; }; template requires utils::blocks::block_form_is_valid_v struct NormalizationCompilation< normalization_policy_extension_test::MisdeclaredRuntime, Form> { using Plan = IdentityNormalizationPlanFor; static constexpr bool registered = CompleteNormalizationFor; template static constexpr bool runtimeAvailableFor = registered; }; template requires utils::blocks::block_form_is_valid_v struct NormalizationCompilation< normalization_policy_extension_test::BorrowedRuntime, Form> { using Plan = RuntimePreparedNormalizationPlanFor< normalization_policy_extension_test::UniformDiagonal, Form>; static constexpr bool registered = CompleteNormalizationFor; template static constexpr bool runtimeAvailableFor = registered; }; template requires utils::blocks::block_form_is_valid_v struct NormalizationCompilation< normalization_policy_extension_test::MissingPreparation, Form> : RuntimePreparedNormalizationCompilation< normalization_policy_extension_test::MissingPreparation, Form> { }; template requires utils::blocks::block_form_is_valid_v struct NormalizationCompilation< normalization_policy_extension_test::WrongPreparationResult, Form> : RuntimePreparedNormalizationCompilation< normalization_policy_extension_test::WrongPreparationResult, Form> { }; template requires utils::blocks::block_form_is_valid_v struct NormalizationCompilation< normalization_policy_extension_test::MoveOnlyDiagonal, Form> : RuntimePreparedNormalizationCompilation< normalization_policy_extension_test::MoveOnlyDiagonal, Form> { }; } // namespace mean_field::normalization namespace normalization_policy_extension_test { /* Found by argument-dependent lookup through the policy carried by the * problem's discretization type. No library switch or internal trait is * modified to prepare this third-party normalization. */ template requires std::same_as< typename std::remove_cvref_t::NormalizationPrescriptionType, UniformDiagonal> [[nodiscard]] mean_field::normalization::DiagonalNormalization prepareStellarNormalization( const UniformDiagonal &policy, const Problem &problem ) { mfem::Vector stateFactors(problem.StateSize()); mfem::Vector residualFactors(problem.EquationSize()); stateFactors = policy.stateFactor(); residualFactors = policy.residualFactor(); return {std::move(stateFactors), std::move(residualFactors)}; } template requires std::same_as< typename std::remove_cvref_t::NormalizationPrescriptionType, MisdeclaredRuntime> [[nodiscard]] mean_field::normalization::DiagonalNormalization prepareStellarNormalization( const MisdeclaredRuntime &, const Problem &problem ) { return mean_field::normalization::DiagonalNormalization::Identity( problem.StateSize(), problem.EquationSize() ); } /* A preparation operation with the wrong result type must not satisfy the * solver-facing normalization contract. MissingPreparation intentionally * has no operation at all. */ template requires std::same_as< typename std::remove_cvref_t::NormalizationPrescriptionType, WrongPreparationResult> [[nodiscard]] int prepareStellarNormalization( const WrongPreparationResult &, const Problem & ) { return 0; } template requires std::same_as< typename std::remove_cvref_t::NormalizationPrescriptionType, MoveOnlyDiagonal> [[nodiscard]] mean_field::normalization::DiagonalNormalization prepareStellarNormalization( const MoveOnlyDiagonal &policy, const Problem &problem ) { mfem::Vector stateFactors(problem.StateSize()); mfem::Vector residualFactors(problem.EquationSize()); stateFactors = policy.stateFactor(); residualFactors = policy.residualFactor(); return {std::move(stateFactors), std::move(residualFactors)}; } } // namespace normalization_policy_extension_test namespace { namespace normalization = mean_field::normalization; using ThirdPolicyModel = mean_field::model::StellarModel< mean_field::models::SpecificationSet< mean_field::eos::Polytrope, mean_field::surface::Isobaric, mean_field::models::FixedTotalMass>>; using ThirdPolicyForm = mean_field::operators::CompiledStellarEquilibriumForm; using ThirdPolicyDiscretization = mean_field::equilibrium::StellarDiscretizationFor< normalization_policy_extension_test::UniformDiagonal>; using CompilationOnlyDiscretization = mean_field::equilibrium::StellarDiscretizationFor< normalization_policy_extension_test::CompilationOnly>; using MisdeclaredRuntimeDiscretization = mean_field::equilibrium::StellarDiscretizationFor< normalization_policy_extension_test::MisdeclaredRuntime>; using BorrowedRuntimeDiscretization = mean_field::equilibrium::StellarDiscretizationFor< normalization_policy_extension_test::BorrowedRuntime>; using MissingPreparationDiscretization = mean_field::equilibrium::StellarDiscretizationFor< normalization_policy_extension_test::MissingPreparation>; using WrongPreparationResultDiscretization = mean_field::equilibrium::StellarDiscretizationFor< normalization_policy_extension_test::WrongPreparationResult>; using MoveOnlyDiscretization = mean_field::equilibrium::StellarDiscretizationFor< normalization_policy_extension_test::MoveOnlyDiagonal>; template concept CanDiscretizeWithNormalization = requires( Model model, Discretization discretization ) { mean_field::equilibrium::discretize( std::move(model), std::move(discretization) ); }; template concept CanMakeNormalizedStellarEquilibriumOperator = requires(Problem &problem) { mean_field::normalization::makeNormalizedStellarEquilibriumOperator(problem); }; template concept CanDirectlyConstructStellarEquilibriumProblem = requires( Model model, Discretization discretization ) { Problem{std::move(model), std::move(discretization)}; }; template concept CanMakeScaledStellarPreconditioner = requires(const NormalizedOperator &normalized, PhysicalInverse &inverse) { normalized.MakeScaledPreconditioner(inverse); }; template concept HasNormalizedStellarPreconditioner = requires { typename normalization::NormalizedStellarPreconditioner; }; template class ProblemBoundInverseWithoutFreshness : public mfem::Solver { public: [[nodiscard]] const Problem &GetProblem() const noexcept; }; template class FreshInverseWithoutProblem : public mfem::Solver { public: [[nodiscard]] bool IsCurrent() const noexcept; }; template class MutableProblemIdentityInverse : public mfem::Solver { public: [[nodiscard]] Problem &GetProblem() const noexcept; [[nodiscard]] bool IsCurrent() const noexcept; }; [[nodiscard]] mean_field::operators::StellarEquilibriumDependencies makeDependencies() { return { .discretization = {.identity = 12101, .revision = 1}, .density = {.identity = 12109, .revision = 1}, .surfaceDeformation = {.identity = 12113, .revision = 1}, .gravityGradient = {.identity = 12119, .revision = 1}, .gravityPotential = {.identity = 12143, .revision = 1}, .enthalpy = {.identity = 12149, .revision = 1}, .bernoulliConstant = {.identity = 12157, .revision = 1}, .rotation = {.identity = 12161, .revision = 1}, .targetMass = {.identity = 12163, .revision = 1} }; } [[nodiscard]] mean_field::physics::RigidRotation zeroRotation() { mfem::Vector angularVelocity(3); mfem::Vector center(3); angularVelocity = 0.0; center = 0.0; return {angularVelocity, center}; } [[nodiscard]] double relativeError( const mfem::Vector &actual, const mfem::Vector &expected ) { if (actual.Size() != expected.Size()) { return std::numeric_limits::infinity(); } mfem::Vector difference(actual); difference -= expected; return difference.Norml2() / std::max({1.0, actual.Norml2(), expected.Norml2()}); } [[nodiscard]] double blockRms( const mfem::Vector &vector, const int begin, const int end ) { double squaredNorm = 0.0; for (int index = begin; index < end; ++index) { squaredNorm += vector(index) * vector(index); } return std::sqrt(squaredNorm / static_cast(end - begin)); } struct BlockResponseSpread final { double physical{0.0}; double normalized{0.0}; double physicalActivityFloor{0.0}; double normalizedActivityFloor{0.0}; int activeResponses{0}; }; template [[nodiscard]] BlockResponseSpread measureBlockResponseSpread( const mfem::Operator &physicalJacobian, const mfem::Operator &normalizedJacobian, const mean_field::utils::blocks::form_layout &layout ) { struct Response final { double physical; double normalized; }; std::vector responses; responses.reserve(Form::value_block_count * Form::residual_block_count); const auto &valueOffsets = layout.value_offsets(); const auto &residualOffsets = layout.residual_offsets(); for (int valueBlock = 0; valueBlock < Form::value_block_count; ++valueBlock) { mfem::Vector direction(physicalJacobian.Width()); direction = 0.0; const int begin = valueOffsets[valueBlock]; const int end = valueOffsets[valueBlock + 1]; double squaredNorm = 0.0; for (int index = begin; index < end; ++index) { const double localIndex = static_cast(index - begin + 1); const double value = std::sin(0.37 * localIndex + 0.41 * static_cast(valueBlock + 1)) + 0.29 * std::cos(0.17 * localIndex - 0.23 * static_cast(valueBlock + 1)); direction(index) = value; squaredNorm += value * value; } REQUIRE(squaredNorm > 0.0); direction *= 1.0 / std::sqrt(squaredNorm); mfem::Vector physicalAction; mfem::Vector normalizedAction; physicalJacobian.Mult(direction, physicalAction); normalizedJacobian.Mult(direction, normalizedAction); REQUIRE(physicalAction.Size() == physicalJacobian.Height()); REQUIRE(normalizedAction.Size() == normalizedJacobian.Height()); for (int residualBlock = 0; residualBlock < Form::residual_block_count; ++residualBlock) { responses.push_back({ .physical = blockRms( physicalAction, residualOffsets[residualBlock], residualOffsets[residualBlock + 1] ), .normalized = blockRms( normalizedAction, residualOffsets[residualBlock], residualOffsets[residualBlock + 1] ) }); } } double globalLargestPhysical = 0.0; double globalLargestNormalized = 0.0; for (const Response &response : responses) { // A non-finite response must never disappear merely because the // other coordinate system decides that entry is inactive. REQUIRE(std::isfinite(response.physical)); REQUIRE(std::isfinite(response.normalized)); globalLargestPhysical = std::max(globalLargestPhysical, response.physical); globalLargestNormalized = std::max(globalLargestNormalized, response.normalized); } REQUIRE(globalLargestPhysical > 0.0); REQUIRE(globalLargestNormalized > 0.0); /* Activity is the union of entries resolved in either coordinate * system. Separate relative floors make the selection symmetric: * normalization cannot improve its reported spread merely by pushing * a physically active response below a normalized-only gate (and the * converse is equally prohibited). The absolute floor excludes only * denormal-scale arithmetic, not modeled stellar coefficients. */ constexpr double relativeActivityFloor = 1.0e-10; constexpr double absoluteActivityFloor = 64.0 * std::numeric_limits::min(); const double physicalActivityFloor = std::max( absoluteActivityFloor, relativeActivityFloor * globalLargestPhysical ); const double normalizedActivityFloor = std::max( absoluteActivityFloor, relativeActivityFloor * globalLargestNormalized ); double smallestPhysical = std::numeric_limits::infinity(); double largestPhysical = 0.0; double smallestNormalized = std::numeric_limits::infinity(); double largestNormalized = 0.0; int activeResponses = 0; for (const Response &response : responses) { const bool physicallyActive = response.physical > physicalActivityFloor; const bool normalizedActive = response.normalized > normalizedActivityFloor; if (!physicallyActive && !normalizedActive) { continue; } // Diagonal two-sided scaling preserves structural support. Once // either representation resolves an interaction, both responses // must therefore be usable in the spread comparison. REQUIRE(std::isfinite(response.physical)); REQUIRE(std::isfinite(response.normalized)); REQUIRE(response.physical > 0.0); REQUIRE(response.normalized > 0.0); smallestPhysical = std::min(smallestPhysical, response.physical); largestPhysical = std::max(largestPhysical, response.physical); smallestNormalized = std::min(smallestNormalized, response.normalized); largestNormalized = std::max(largestNormalized, response.normalized); ++activeResponses; } REQUIRE(activeResponses > 0); return { .physical = largestPhysical / smallestPhysical, .normalized = largestNormalized / smallestNormalized, .physicalActivityFloor = physicalActivityFloor, .normalizedActivityFloor = normalizedActivityFloor, .activeResponses = activeResponses }; } } // namespace TEST_CASE( "A Third-Party Normalization Policy Reaches Discretization And The Normalized Operator", "[normalization][extension][type_contract][integration]" ) { using namespace mean_field; using Policy = normalization_policy_extension_test::UniformDiagonal; using CompilationOnly = normalization_policy_extension_test::CompilationOnly; using MisdeclaredRuntime = normalization_policy_extension_test::MisdeclaredRuntime; using BorrowedRuntime = normalization_policy_extension_test::BorrowedRuntime; using MissingPreparation = normalization_policy_extension_test::MissingPreparation; using WrongPreparationResult = normalization_policy_extension_test::WrongPreparationResult; using MoveOnlyDiagonal = normalization_policy_extension_test::MoveOnlyDiagonal; using PhysicalCore = operators::StellarEquilibriumPhysicalCoreType; using ExpectedPlan = normalization::RuntimePreparedNormalizationPlanFor< Policy, ThirdPolicyForm>; STATIC_CHECK(normalization::NormalizationPrescription); STATIC_CHECK(normalization::CompilableNormalizationFor); STATIC_CHECK(normalization::RuntimePreparedNormalizationFor< Policy, ThirdPolicyForm>); STATIC_CHECK(std::same_as< normalization::NormalizationPlanFor, ExpectedPlan>); STATIC_CHECK(normalization::StellarNormalizationRuntimeAvailableFor< Policy, ThirdPolicyForm, PhysicalCore, ThirdPolicyModel::SpecificationTypes>); STATIC_CHECK(equilibrium::StellarEquilibriumModelDiscretizationCompatible< ThirdPolicyModel, ThirdPolicyDiscretization>); STATIC_CHECK(CanDiscretizeWithNormalization< ThirdPolicyModel, ThirdPolicyDiscretization>); STATIC_CHECK(normalization::CompilableNormalizationFor< CompilationOnly, ThirdPolicyForm>); STATIC_CHECK_FALSE(normalization::StellarNormalizationRuntimeAvailableFor< CompilationOnly, ThirdPolicyForm, PhysicalCore, ThirdPolicyModel::SpecificationTypes>); STATIC_CHECK_FALSE(equilibrium::StellarEquilibriumModelDiscretizationCompatible< ThirdPolicyModel, CompilationOnlyDiscretization>); STATIC_CHECK_FALSE(CanDiscretizeWithNormalization< ThirdPolicyModel, CompilationOnlyDiscretization>); STATIC_CHECK(normalization::CompilableNormalizationFor< MisdeclaredRuntime, ThirdPolicyForm>); STATIC_CHECK_FALSE(normalization::RuntimePreparedNormalizationFor< MisdeclaredRuntime, ThirdPolicyForm>); STATIC_CHECK_FALSE(normalization::StellarNormalizationRuntimeAvailableFor< MisdeclaredRuntime, ThirdPolicyForm, PhysicalCore, ThirdPolicyModel::SpecificationTypes>); STATIC_CHECK_FALSE(equilibrium::StellarEquilibriumModelDiscretizationCompatible< ThirdPolicyModel, MisdeclaredRuntimeDiscretization>); STATIC_CHECK_FALSE(CanDiscretizeWithNormalization< ThirdPolicyModel, MisdeclaredRuntimeDiscretization>); STATIC_CHECK(normalization::CompilableNormalizationFor< BorrowedRuntime, ThirdPolicyForm>); STATIC_CHECK_FALSE(normalization::RuntimePreparedNormalizationFor< BorrowedRuntime, ThirdPolicyForm>); STATIC_CHECK_FALSE(normalization::StellarNormalizationRuntimeAvailableFor< BorrowedRuntime, ThirdPolicyForm, PhysicalCore, ThirdPolicyModel::SpecificationTypes>); STATIC_CHECK_FALSE(CanDiscretizeWithNormalization< ThirdPolicyModel, BorrowedRuntimeDiscretization>); using MissingPreparationProblem = equilibrium::StellarEquilibriumProblem< ThirdPolicyModel, MissingPreparationDiscretization>; STATIC_CHECK(normalization::RuntimePreparedNormalizationFor< MissingPreparation, ThirdPolicyForm>); STATIC_CHECK_FALSE(equilibrium::StellarEquilibriumModelDiscretizationCompatible< ThirdPolicyModel, MissingPreparationDiscretization>); STATIC_CHECK_FALSE(CanDiscretizeWithNormalization< ThirdPolicyModel, MissingPreparationDiscretization>); STATIC_CHECK_FALSE(normalization::NormalizableStellarEquilibriumProblem< MissingPreparationProblem>); STATIC_CHECK_FALSE(CanMakeNormalizedStellarEquilibriumOperator< MissingPreparationProblem>); STATIC_CHECK_FALSE(CanDirectlyConstructStellarEquilibriumProblem< MissingPreparationProblem, ThirdPolicyModel, MissingPreparationDiscretization>); using WrongPreparationResultProblem = equilibrium::StellarEquilibriumProblem< ThirdPolicyModel, WrongPreparationResultDiscretization>; STATIC_CHECK(normalization::RuntimePreparedNormalizationFor< WrongPreparationResult, ThirdPolicyForm>); STATIC_CHECK_FALSE(equilibrium::StellarEquilibriumModelDiscretizationCompatible< ThirdPolicyModel, WrongPreparationResultDiscretization>); STATIC_CHECK_FALSE(CanDiscretizeWithNormalization< ThirdPolicyModel, WrongPreparationResultDiscretization>); STATIC_CHECK_FALSE(normalization::NormalizableStellarEquilibriumProblem< WrongPreparationResultProblem>); STATIC_CHECK_FALSE(CanMakeNormalizedStellarEquilibriumOperator< WrongPreparationResultProblem>); STATIC_CHECK_FALSE(std::copy_constructible); STATIC_CHECK(std::move_constructible); STATIC_CHECK_FALSE(std::copy_constructible); STATIC_CHECK(std::move_constructible); STATIC_CHECK(normalization::RuntimePreparedNormalizationFor< MoveOnlyDiagonal, ThirdPolicyForm>); STATIC_CHECK(equilibrium::StellarEquilibriumModelDiscretizationCompatible< ThirdPolicyModel, MoveOnlyDiscretization>); STATIC_CHECK(CanDiscretizeWithNormalization< ThirdPolicyModel, MoveOnlyDiscretization>); const utils::Args arguments = test_utils::setup_args(); fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0); REQUIRE(finiteElements.okay()); constexpr double stateFactor = 0.125; constexpr double residualFactor = 32.0; auto model = model::StellarModel( eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}) ); auto problem = equilibrium::discretize( model, equilibrium::makeStellarDiscretization( finiteElements, Policy{stateFactor, residualFactor} ) ); auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem); using Problem = std::remove_cvref_t; STATIC_CHECK_FALSE(CanDirectlyConstructStellarEquilibriumProblem< Problem, ThirdPolicyModel, ThirdPolicyDiscretization>); STATIC_CHECK(std::same_as); STATIC_CHECK(normalization::NormalizableStellarEquilibriumProblem); REQUIRE(normalized.GetNormalization().StateSize() == problem.StateSize()); REQUIRE(normalized.GetNormalization().ResidualSize() == problem.EquationSize()); for (int index = 0; index < problem.StateSize(); ++index) { CHECK(normalized.GetNormalization().StateFactors()(index) == Catch::Approx(stateFactor).epsilon(2.0e-15)); } for (int index = 0; index < problem.EquationSize(); ++index) { CHECK(normalized.GetNormalization().ResidualFactors()(index) == Catch::Approx(residualFactor).epsilon(2.0e-15)); } mfem::Vector physicalState(problem.StateSize()); for (int index = 0; index < physicalState.Size(); ++index) { physicalState(index) = 0.25 + 0.01 * static_cast(index); } mfem::Vector normalizedState; mfem::Vector recoveredState; normalized.NormalizeState(physicalState, normalizedState); REQUIRE(normalizedState.Size() == physicalState.Size()); for (int index = 0; index < normalizedState.Size(); ++index) { CHECK(normalizedState(index) == Catch::Approx(stateFactor * physicalState(index)).epsilon(2.0e-15)); } CHECK(normalizedState(0) != Catch::Approx(physicalState(0)).epsilon(2.0e-15)); normalized.DenormalizeState(normalizedState, recoveredState); CHECK(relativeError(recoveredState, physicalState) <= 2.0e-15); mfem::Vector physicalResidual(problem.EquationSize()); for (int index = 0; index < physicalResidual.Size(); ++index) { physicalResidual(index) = -0.5 - 0.02 * static_cast(index); } mfem::Vector normalizedResidual; mfem::Vector recoveredResidual; normalized.NormalizeResidual(physicalResidual, normalizedResidual); REQUIRE(normalizedResidual.Size() == physicalResidual.Size()); for (int index = 0; index < normalizedResidual.Size(); ++index) { CHECK(normalizedResidual(index) == Catch::Approx(residualFactor * physicalResidual(index)).epsilon(2.0e-15)); } CHECK(normalizedResidual(0) != Catch::Approx(physicalResidual(0)).epsilon(2.0e-15)); normalized.DenormalizeResidual(normalizedResidual, recoveredResidual); CHECK(relativeError(recoveredResidual, physicalResidual) <= 2.0e-15); auto moveOnlyProblem = equilibrium::discretize( model::StellarModel( eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}}) ), equilibrium::makeStellarDiscretization( finiteElements, MoveOnlyDiagonal{0.5, 4.0} ) ); auto moveOnlyNormalized = normalization::makeNormalizedStellarEquilibriumOperator(moveOnlyProblem); CHECK(moveOnlyNormalized.GetNormalization().StateFactors()(0) == Catch::Approx(0.5).epsilon(2.0e-15)); CHECK(moveOnlyNormalized.GetNormalization().ResidualFactors()(0) == Catch::Approx(4.0).epsilon(2.0e-15)); } TEST_CASE( "Normalized Stellar Equilibrium Preserves Physical Residuals Jacobians And Frozen Solve Coordinates", "[normalization][stellar-equilibrium][integration]" ) { using namespace mean_field; const utils::Args arguments = test_utils::setup_args(); fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0); REQUIRE(finiteElements.okay()); constexpr double radius = utils::RADIUS; constexpr double mass = 1.0e8; const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v; const double centralDensity = std::numbers::pi_v * mass / (4.0 * radius * radius * radius); const normalization::PhysicalRieszDiagonal policy{ dimensions::LengthValue{radius}, utils::G }; auto model = model::StellarModel( eos::Polytrope({.n = 1.0, .K = polytropicConstant}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}), constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}}) ); auto problem = equilibrium::discretize( model, equilibrium::makeStellarDiscretization(finiteElements, policy) ); auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024})); auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem); using Problem = std::remove_cvref_t; using NormalizedOperator = std::remove_cvref_t; STATIC_CHECK(equilibrium::DiscretizedStellarEquilibriumProblem); STATIC_CHECK(std::same_as< NormalizedOperator, normalization::NormalizedStellarEquilibriumOperator>); CHECK_FALSE(normalized.IsPrepared()); mfem::Vector unavailableResidual; CHECK_THROWS_AS(normalized.BuildResidual(unavailableResidual), std::logic_error); mfem::Vector normalizedState; normalized.NormalizeState(projected.values, normalizedState); const mfem::Vector frozenStateFactors(normalized.GetNormalization().StateFactors()); const mfem::Vector frozenResidualFactors(normalized.GetNormalization().ResidualFactors()); const auto dependencies = makeDependencies(); const auto rotation = zeroRotation(); const auto preparation = normalized.Prepare(normalizedState, dependencies, rotation); CHECK(preparation.DidAnyWork()); CHECK(normalized.IsPrepared()); CHECK(problem.GetPreparationGeneration() == 1); CHECK(relativeError(normalized.GetPhysicalState(), projected.values) <= 4.0e-15); mfem::Vector physicalResidual; mfem::Vector expectedNormalizedResidual; mfem::Vector actualNormalizedResidual; problem.BuildResidual(physicalResidual); normalized.NormalizeResidual(physicalResidual, expectedNormalizedResidual); normalized.BuildResidual(actualNormalizedResidual); CHECK(relativeError(normalized.GetPhysicalResidual(), physicalResidual) <= 2.0e-15); CHECK(relativeError(actualNormalizedResidual, expectedNormalizedResidual) <= 2.0e-15); mfem::Vector normalizedDirection(problem.StateSize()); for (int index = 0; index < normalizedDirection.Size(); ++index) { normalizedDirection(index) = std::sin(0.013 * static_cast(index + 1)) + 0.17 * std::cos(0.031 * static_cast(index + 1)); } normalizedDirection *= 1.0 / normalizedDirection.Norml2(); mfem::Vector physicalDirection; mfem::Vector physicalAction; mfem::Vector expectedNormalizedAction; mfem::Vector actualNormalizedAction; normalized.DenormalizeState(normalizedDirection, physicalDirection); problem.ApplyLinearization(physicalDirection, physicalAction); normalized.NormalizeResidual(physicalAction, expectedNormalizedAction); normalized.Mult(normalizedDirection, actualNormalizedAction); CHECK(relativeError(actualNormalizedAction, expectedNormalizedAction) <= 3.0e-14); const BlockResponseSpread spread = measureBlockResponseSpread( problem.GetLinearizationOperator(), normalized, problem.GetManifest().layout() ); CAPTURE( spread.physical, spread.normalized, spread.physicalActivityFloor, spread.normalizedActivityFloor, spread.activeResponses ); CHECK(spread.activeResponses >= 12); CHECK(std::isfinite(spread.physical)); CHECK(std::isfinite(spread.normalized)); CHECK(spread.normalized < spread.physical); CHECK(spread.physical / spread.normalized > 1.0e6); CHECK(std::memcmp( frozenStateFactors.GetData(), normalized.GetNormalization().StateFactors().GetData(), sizeof(mfem::real_t) * frozenStateFactors.Size() ) == 0); CHECK(std::memcmp( frozenResidualFactors.GetData(), normalized.GetNormalization().ResidualFactors().GetData(), sizeof(mfem::real_t) * frozenResidualFactors.Size() ) == 0); CHECK(normalized.GetStatistics().normalizationPreparations == 1); auto physicalInverse = preconditioning::prepare(problem, preconditioning::makeIdentityPlan(problem)); using PhysicalInverse = std::remove_cvref_t; STATIC_CHECK(normalization::ProblemBoundStellarInverseFor); STATIC_CHECK(CanMakeScaledStellarPreconditioner); STATIC_CHECK(HasNormalizedStellarPreconditioner); STATIC_CHECK_FALSE(normalization::ProblemBoundStellarInverseFor); STATIC_CHECK_FALSE(normalization::ProblemBoundStellarInverseFor); STATIC_CHECK_FALSE(normalization::ProblemBoundStellarInverseFor< ProblemBoundInverseWithoutFreshness, Problem>); STATIC_CHECK_FALSE(normalization::ProblemBoundStellarInverseFor< FreshInverseWithoutProblem, Problem>); STATIC_CHECK_FALSE(normalization::ProblemBoundStellarInverseFor< MutableProblemIdentityInverse, Problem>); STATIC_CHECK_FALSE(CanMakeScaledStellarPreconditioner); STATIC_CHECK_FALSE(HasNormalizedStellarPreconditioner); STATIC_CHECK(std::constructible_from< normalization::ScaledPreconditioner, mfem::Solver &, const mfem::Operator &, const mfem::Operator &, const normalization::DiagonalNormalization &>); CHECK(&physicalInverse.GetProblem() == &problem); auto scaledInverse = normalized.MakeScaledPreconditioner(physicalInverse); auto otherModel = model::StellarModel( eos::Polytrope({.n = 1.0, .K = polytropicConstant}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}), constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}}) ); auto otherProblem = equilibrium::discretize( otherModel, equilibrium::makeStellarDiscretization(finiteElements, policy) ); auto otherNormalized = normalization::makeNormalizedStellarEquilibriumOperator(otherProblem); mfem::Vector otherNormalizedState; otherNormalized.NormalizeState(projected.values, otherNormalizedState); otherNormalized.Prepare(otherNormalizedState, dependencies, rotation); REQUIRE(otherNormalized.IsPrepared()); const std::uint64_t bindingsBeforeWrongInstance = physicalInverse.GetStatistics().operatorBindings; CHECK_THROWS_AS( otherNormalized.MakeScaledPreconditioner(physicalInverse), std::invalid_argument ); CHECK(physicalInverse.GetStatistics().operatorBindings == bindingsBeforeWrongInstance); CHECK_THROWS_AS(scaledInverse.SetOperator(otherNormalized), std::invalid_argument); mfem::Vector normalizedRightHandSide(problem.EquationSize()); for (int index = 0; index < normalizedRightHandSide.Size(); ++index) { normalizedRightHandSide(index) = 0.2 * std::sin(0.023 * static_cast(index + 1)); } mfem::Vector physicalRightHandSide; mfem::Vector physicalCorrection(problem.StateSize()); mfem::Vector expectedNormalizedCorrection; mfem::Vector actualNormalizedCorrection(problem.StateSize()); normalized.DenormalizeResidual(normalizedRightHandSide, physicalRightHandSide); physicalInverse.Mult(physicalRightHandSide, physicalCorrection); normalized.NormalizeState(physicalCorrection, expectedNormalizedCorrection); scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection); CHECK(relativeError(actualNormalizedCorrection, expectedNormalizedCorrection) <= 2.0e-15); CHECK(&scaledInverse.GetPhysicalJacobian() == &problem.GetLinearizationOperator()); CHECK(&scaledInverse.GetNormalizedJacobian() == &normalized); CHECK(&scaledInverse.GetPhysicalInverse() == &physicalInverse); mfem::IdentityOperator differentNormalizedJacobian(problem.StateSize()); CHECK_THROWS_AS(scaledInverse.SetOperator(differentNormalizedJacobian), std::invalid_argument); scaledInverse.SetOperator(normalized); problem.Prepare(normalized.GetPhysicalState(), dependencies, rotation); CHECK(problem.GetPreparationGeneration() == 2); CHECK_FALSE(normalized.IsPrepared()); CHECK_FALSE(scaledInverse.IsCurrent()); CHECK_THROWS_AS(normalized.Mult(normalizedDirection, actualNormalizedAction), std::logic_error); CHECK_THROWS_AS(scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection), std::logic_error); normalized.Prepare(normalizedState, dependencies, rotation); CHECK(normalized.IsPrepared()); CHECK(problem.GetPreparationGeneration() == 3); CHECK_FALSE(physicalInverse.IsCurrent()); CHECK_FALSE(scaledInverse.IsCurrent()); const auto reprepareRefresh = physicalInverse.Refresh(); CHECK(reprepareRefresh.changes.linearization); CHECK(physicalInverse.IsCurrent()); CHECK(scaledInverse.IsCurrent()); normalized.RefreshNormalization(); CHECK_FALSE(normalized.IsPrepared()); CHECK_FALSE(scaledInverse.IsCurrent()); CHECK_THROWS_AS(scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection), std::logic_error); CHECK(normalized.GetStatistics().normalizationPreparations == 2); CHECK(relativeError(normalized.GetNormalization().StateFactors(), frozenStateFactors) <= 2.0e-15); CHECK(relativeError(normalized.GetNormalization().ResidualFactors(), frozenResidualFactors) <= 2.0e-15); normalized.NormalizeState(projected.values, normalizedState); normalized.Prepare(normalizedState, dependencies, rotation); CHECK(normalized.IsPrepared()); CHECK(problem.GetPreparationGeneration() == 4); CHECK(normalized.GetStatistics().physicalPreparations == 3); CHECK(normalized.GetStatistics().residualRetrievals == 1); CHECK(normalized.GetStatistics().jacobianApplications >= Problem::FormType::value_block_count + 1); auto changedDependencies = dependencies; ++changedDependencies.density.revision; normalized.Prepare(normalizedState, changedDependencies, rotation); CHECK(normalized.IsPrepared()); CHECK_FALSE(physicalInverse.IsCurrent()); CHECK_FALSE(scaledInverse.IsCurrent()); CHECK_THROWS_AS(scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection), std::logic_error); const auto refreshReport = physicalInverse.Refresh(); CHECK(refreshReport.changes.Any()); CHECK_FALSE(refreshReport.DidAnyWork()); CHECK(physicalInverse.IsCurrent()); CHECK(scaledInverse.IsCurrent()); scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection); CHECK(relativeError(actualNormalizedCorrection, expectedNormalizedCorrection) <= 2.0e-15); CHECK(problem.GetPreparationGeneration() == 5); CHECK(normalized.GetStatistics().physicalPreparations == 4); } TEST_CASE( "Physical Riesz Normalization Includes Generated Angular Velocity And Angular Momentum Coordinates", "[normalization][fixed-angular-momentum][integration]" ) { using namespace mean_field; using Catch::Approx; const utils::Args arguments = test_utils::setup_args(); fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0); REQUIRE(finiteElements.okay()); constexpr double mass = 7.0; constexpr double radius = 3.0; constexpr double gravitationalConstant = 5.0; const normalization::PhysicalRieszDiagonal policy{ dimensions::LengthValue{radius}, gravitationalConstant }; auto model = model::StellarModel( eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}), integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{2.0}}) ); auto problem = equilibrium::discretize( model, equilibrium::makeStellarDiscretization(finiteElements, policy) ); auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem); using Problem = std::remove_cvref_t; using Form = typename Problem::FormType; const auto scales = normalization::deriveStellarCharacteristicScales( dimensions::MassValue{mass}, dimensions::LengthValue{radius}, gravitationalConstant ); constexpr auto angularVelocityBlock = utils::blocks::get_value_block( utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term ); constexpr auto angularMomentumBlock = utils::blocks::get_residual_block( utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term ); const auto &layout = problem.GetManifest().layout(); CHECK(layout.size(angularVelocityBlock) == 1); CHECK(layout.size(angularMomentumBlock) == 1); CHECK(normalized.GetNormalization().StateFactors()(layout.offset(angularVelocityBlock)) == Approx(1.0 / scales.angularVelocity).epsilon(2.0e-15)); CHECK(normalized.GetNormalization().ResidualFactors()(layout.offset(angularMomentumBlock)) == Approx(1.0 / scales.angularMomentum).epsilon(2.0e-15)); mfem::Vector physicalState(problem.StateSize()); physicalState = 0.0; const auto stateView = problem.GetManifest().stateView(physicalState); stateView.block(utils::blocks::density_field.mass_term) = 1.0; stateView.block(utils::blocks::enthalpy_field.specific_term) = 0.7; stateView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 1.0; stateView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term) = 0.4; mfem::Vector normalizedState; normalized.NormalizeState(physicalState, normalizedState); const auto preparation = normalized.Prepare(normalizedState, makeDependencies()); CHECK(preparation.generatedPhysicalControl); CHECK(preparation.template specification().generatedRotation); CHECK(normalized.IsPrepared()); CHECK(relativeError(normalized.GetPhysicalState(), physicalState) < 3.0e-15); CHECK(problem.GetPreparedOperator().GetAngularMomentumReport().angularVelocity == Approx(0.4)); mfem::Vector normalizedDirection(problem.StateSize()); for (int index = 0; index < normalizedDirection.Size(); ++index) { normalizedDirection(index) = 0.03 * std::sin(0.019 * static_cast(index + 1)); } mfem::Vector physicalDirection; mfem::Vector physicalAction; mfem::Vector expectedNormalizedAction; mfem::Vector actualNormalizedAction; normalized.DenormalizeState(normalizedDirection, physicalDirection); problem.ApplyLinearization(physicalDirection, physicalAction); normalized.NormalizeResidual(physicalAction, expectedNormalizedAction); normalized.Mult(normalizedDirection, actualNormalizedAction); CHECK(relativeError(actualNormalizedAction, expectedNormalizedAction) < 4.0e-14); } TEST_CASE( "Two Sided Normalization Composes With The Full Coupled Stellar Preconditioner", "[normalization][preconditioning][fixed-angular-momentum][central-density][integration]" ) { using namespace mean_field; const utils::Args arguments = test_utils::setup_args(); fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0); REQUIRE(finiteElements.okay()); constexpr double radius = utils::RADIUS; constexpr double mass = utils::MASS; const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v; const double centralDensity = std::numbers::pi_v * mass / (4.0 * radius * radius * radius); auto model = model::StellarModel( eos::Polytrope({.n = 1.0, .K = polytropicConstant}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}), integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.05}}), constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}}) ); auto problem = equilibrium::discretize( model, equilibrium::makeStellarDiscretization( finiteElements, normalization::PhysicalRieszDiagonal{dimensions::LengthValue{radius}, utils::G} ) ); auto projected = seed::makeProjectedEquilibriumState( problem, seed::LaneEmden({.radialSampleCount = 1024}) ); auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem); mfem::Vector normalizedState; normalized.NormalizeState(projected.values, normalizedState); const auto dependencies = makeDependencies(); const auto preparation = normalized.Prepare(normalizedState, dependencies); REQUIRE(preparation.generatedPhysicalControl); REQUIRE(normalized.IsPrepared()); using Problem = std::remove_cvref_t; const BlockResponseSpread coupledSpread = measureBlockResponseSpread( problem.GetLinearizationOperator(), normalized, problem.GetManifest().layout() ); const double coupledImprovement = coupledSpread.physical / coupledSpread.normalized; CAPTURE( coupledSpread.physical, coupledSpread.normalized, coupledSpread.physicalActivityFloor, coupledSpread.normalizedActivityFloor, coupledSpread.activeResponses, coupledImprovement ); CHECK(coupledSpread.activeResponses >= static_cast(Problem::FormType::value_block_count)); CHECK(std::isfinite(coupledSpread.physical)); CHECK(std::isfinite(coupledSpread.normalized)); // This is intentionally a conservative first empirical contract: the // real coupled operator must improve, while the coordinated test run will // determine whether a stronger stable factor is justified. CHECK(coupledSpread.normalized < coupledSpread.physical); CHECK(coupledImprovement > 1.0); auto physicalInverse = preconditioning::prepare( problem, preconditioning::makePreconditioner(problem) ); REQUIRE(physicalInverse.IsCurrent()); auto scaledInverse = normalized.MakeScaledPreconditioner(physicalInverse); REQUIRE(scaledInverse.IsCurrent()); mfem::Vector normalizedRightHandSide(problem.EquationSize()); for (int index = 0; index < normalizedRightHandSide.Size(); ++index) { normalizedRightHandSide(index) = 0.19 * std::sin(0.031 * static_cast(index + 1)) + 0.07 * std::cos(0.017 * static_cast(index + 1)); } mfem::Vector physicalRightHandSide(problem.EquationSize()); mfem::Vector physicalCorrection(problem.StateSize()); mfem::Vector expectedNormalizedCorrection(problem.StateSize()); mfem::Vector actualNormalizedCorrection(problem.StateSize()); normalized.DenormalizeResidual(normalizedRightHandSide, physicalRightHandSide); physicalInverse.Mult(physicalRightHandSide, physicalCorrection); normalized.NormalizeState(physicalCorrection, expectedNormalizedCorrection); scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection); CHECK(relativeError(actualNormalizedCorrection, expectedNormalizedCorrection) <= 3.0e-13); const auto correctionView = problem.GetManifest().stateView(actualNormalizedCorrection); const double massCorrection = correctionView.block( utils::blocks::fixed_total_mass_constraint.mass_normalization_term )(0); const double angularVelocityCorrection = correctionView.block( utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term )(0); const double phaseCorrection = correctionView.block( utils::blocks::fixed_central_density_phase.central_value_term )(0); CAPTURE(massCorrection, angularVelocityCorrection, phaseCorrection); CHECK(std::isfinite(massCorrection)); CHECK(std::isfinite(angularVelocityCorrection)); CHECK(std::isfinite(phaseCorrection)); CHECK(std::abs(massCorrection) > 1.0e-16); CHECK(std::abs(angularVelocityCorrection) > 1.0e-16); CHECK(std::abs(phaseCorrection) > 1.0e-16); mfem::Vector expectedNormalizedPreconditionedAction(problem.EquationSize()); mfem::Vector actualNormalizedPreconditionedAction(problem.EquationSize()); mfem::Vector physicalPreconditionedAction(problem.EquationSize()); problem.ApplyLinearization(physicalCorrection, physicalPreconditionedAction); normalized.NormalizeResidual( physicalPreconditionedAction, expectedNormalizedPreconditionedAction ); normalized.Mult( actualNormalizedCorrection, actualNormalizedPreconditionedAction ); CHECK(relativeError( actualNormalizedPreconditionedAction, expectedNormalizedPreconditionedAction ) <= 4.0e-13); normalized.Prepare(normalizedState, dependencies); CHECK(normalized.IsPrepared()); CHECK_FALSE(physicalInverse.IsCurrent()); CHECK_FALSE(scaledInverse.IsCurrent()); const auto refresh = physicalInverse.Refresh(); CHECK(refresh.rebuiltSchurComplement); CHECK(refresh.DidAnyWork()); CHECK(physicalInverse.IsCurrent()); CHECK(scaledInverse.IsCurrent()); }