#include #include #include #include #include #include #include #include #include #include #include import mean_field; import test_helpers; /* * This file deliberately implements complete third-party scalar constraints * rather than adding library builtins. It is an executable example of the * intended physics extension boundary: each declaration and exact * residual/Jacobian provider is written once and is then folded into every * model-level operator. The generic preconditioner projects those same * providers into its generated border blocks; extension authors do not write * a second, potentially inconsistent border action. Prepared extension physics * receives only its exact specification, the state and residual blocks it * declared, and any narrowly typed astronomy service admitted by that * declaration. Backend FEM, model, dependency, and core objects remain absent * from the author-facing protocol. */ namespace magnetic_specific_energy_extension_test { namespace blocks = mean_field::utils::blocks; namespace preconditioning = mean_field::preconditioning; class FixedMagneticSpecificEnergy; class FixedCoreThermalBalance; struct MagneticAmplitudeTerm final { using value = blocks::generated_value_block>; using residual = blocks::generated_residual_block>; }; inline constexpr MagneticAmplitudeTerm magneticAmplitudeTerm{}; struct CoreThermalMultiplierTerm final { using value = blocks::generated_value_block>; using residual = blocks::generated_residual_block>; }; inline constexpr CoreThermalMultiplierTerm coreThermalMultiplierTerm{}; class PreparedMagneticSpecificEnergy; class PreparedCoreThermalBalance; inline constexpr double thermalMagneticCoupling = 0.125; inline constexpr double magneticThermalFeedback = 0.2; inline constexpr double thermalMassCoupling = 0.35; /* * Toy magnetic closure used by this integration test: * * e_B(h_0, a_B) = 1/2 a_B^2 h_0, * R_B = e_B - e_B,target, * R_h += e_B,target a_B. * * a_B is a dimensionless physical amplitude. This makes the invariant * genuinely nonlinear and exercises all three generated-border blocks: * structure-to-border, border-to-structure, and border-to-border. */ class FixedMagneticSpecificEnergy final { public: struct Parameters final { mean_field::dimensions::SpecificEnergyValue target; }; using TargetValue = mean_field::dimensions::SpecificEnergyValue; using ModelDefinition = mean_field::integral::FixedScalarWithPhysicalCoordinate< FixedMagneticSpecificEnergy, "FixedMagneticSpecificEnergy", mean_field::stellar:: Reads, mean_field::stellar::Changes, mean_field::stellar::ScalarConstraint< mean_field::dimensions::quantity::SpecificEnergy, mean_field::dimensions::quantity::Dimensionless, mean_field::dimensions::quantity::SpecificEnergy, "fixed_magnetic_specific_energy.amplitude", "a_B", "fixed_magnetic_specific_energy.residual", "R_EB">>; using EquilibriumPhysics = mean_field::operators::LocalSpecificationEquilibriumPhysics; explicit FixedMagneticSpecificEnergy(const Parameters parameters) : m_target(parameters.target) { if (!std::isfinite(m_target.value()) || m_target.value() <= 0.0) { throw std::invalid_argument("The target magnetic specific energy must be finite and positive."); } } [[nodiscard]] TargetValue target() const noexcept { return m_target; } private: TargetValue m_target; }; /* * A second scalar closure used to audit a pack containing more than one * third-party nested runtime and direct coupling between their generated * coordinates and equations: * * x_T = lambda_T / h_ref, * R_T = P_* [h_0/h_ref + 1/4 x_T^2 + gamma a_B x_T - 1 * + delta (integral rho dV / M_ref - 1)], * R_B += eta a_B lambda_T, * R_h += lambda_T. * * lambda_T has specific-energy units, while R_T has pressure units. The * explicit h_ref and M_ref keep every ratio dimensionless, and the * quadratic term makes the multiplier's own diagonal border action state * dependent. The density term is evaluated as a global finite-element * physical-volume integral. The two cross terms exercise * GeneratedCoordinateOf and ConstraintOf numerically: the thermal package * contributes to the magnetic row without either package knowing a root * offset or a constraint-pack ordering. */ class FixedCoreThermalBalance final { public: struct Parameters final { mean_field::dimensions::PressureValue target; mean_field::dimensions::SpecificEnthalpyValue referenceSpecificEnthalpy; mean_field::dimensions::MassValue referenceMass; }; using TargetValue = mean_field::dimensions::PressureValue; using ModelDefinition = mean_field::integral::FixedScalarWithMultiplier< FixedCoreThermalBalance, "FixedCoreThermalBalance", mean_field::stellar::Reads< mean_field::stellar::state::Density, mean_field::stellar::state::SurfaceShape, mean_field::stellar::state::SpecificEnthalpy, mean_field::stellar::state::OwnGeneratedCoordinate, mean_field::stellar::state::GeneratedCoordinateOf>, mean_field::stellar::Changes< mean_field::stellar::equation::HydrostaticBalance, mean_field::stellar::equation::ConstraintOf>, mean_field::stellar::ScalarConstraint< mean_field::dimensions::quantity::Pressure, mean_field::dimensions::quantity::SpecificEnergy, mean_field::dimensions::quantity::Pressure, "fixed_core_thermal_balance.multiplier", "lambda_T", "fixed_core_thermal_balance.residual", "R_T">>; using EquilibriumPhysics = mean_field::operators::LocalSpecificationEquilibriumPhysics; explicit FixedCoreThermalBalance(const Parameters parameters) : m_target(parameters.target), m_referenceSpecificEnthalpy(parameters.referenceSpecificEnthalpy), m_referenceMass(parameters.referenceMass) { if (!std::isfinite(m_target.value()) || m_target.value() <= 0.0) { throw std::invalid_argument("The target core thermal balance must be finite and positive."); } if (!std::isfinite(m_referenceSpecificEnthalpy.value()) || m_referenceSpecificEnthalpy.value() <= 0.0) { throw std::invalid_argument("The core thermal balance reference enthalpy must be finite and positive."); } if (!std::isfinite(m_referenceMass.value()) || m_referenceMass.value() <= 0.0) { throw std::invalid_argument("The core thermal balance reference mass must be finite and positive."); } } [[nodiscard]] TargetValue target() const noexcept { return m_target; } [[nodiscard]] mean_field::dimensions::SpecificEnthalpyValue referenceSpecificEnthalpy() const noexcept { return m_referenceSpecificEnthalpy; } [[nodiscard]] mean_field::dimensions::MassValue referenceMass() const noexcept { return m_referenceMass; } private: TargetValue m_target; mean_field::dimensions::SpecificEnthalpyValue m_referenceSpecificEnthalpy; mean_field::dimensions::MassValue m_referenceMass; }; struct MagneticSpecificEnergyPreparationReport final { bool stateChanged{true}; }; struct CoreThermalBalancePreparationReport final { bool stateChanged{true}; }; class PreparedMagneticSpecificEnergy final { public: using Report = MagneticSpecificEnergyPreparationReport; explicit PreparedMagneticSpecificEnergy(const FixedMagneticSpecificEnergy &specification) noexcept : m_target(specification.target().value()) { } template [[nodiscard]] Report PrepareAfterPhysical(const StateView &state) { const auto enthalpy = state.specificEnthalpy(); const auto amplitude = state.generatedCoordinate(); if (enthalpy.Size() == 0 || amplitude.Size() != 1 || !std::isfinite(enthalpy(0)) || !std::isfinite(amplitude(0))) { throw std::invalid_argument( "Magnetic-specific-energy preparation requires finite enthalpy and amplitude." ); } const bool changed = !m_isPrepared || enthalpy(0) != m_referenceEnthalpy || amplitude(0) != m_amplitude; m_referenceEnthalpy = enthalpy(0); m_amplitude = amplitude(0); m_invariantResidual = 0.5 * m_amplitude * m_amplitude * m_referenceEnthalpy - m_target; m_hydrostaticContribution = m_target * m_amplitude; m_isPrepared = true; return {.stateChanged = changed}; } template [[nodiscard]] auto AddResidual( mean_field::stellar::equation::OwnConstraint, Row &row ) const { return row.add(m_invariantResidual); } template [[nodiscard]] auto AddResidual( mean_field::stellar::equation::HydrostaticBalance, Row &row ) const { return row.add(m_hydrostaticContribution); } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::OwnConstraint, mean_field::stellar::state::SpecificEnthalpy>, const Direction &direction, Row &row ) const { const auto enthalpyDirection = direction.specificEnthalpy(); return row.add(0.5 * m_amplitude * m_amplitude * enthalpyDirection(0)); } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::OwnConstraint, mean_field::stellar::state::OwnGeneratedCoordinate>, const Direction &direction, Row &row ) const { const auto amplitudeDirection = direction.generatedCoordinate(); return row.add(m_amplitude * m_referenceEnthalpy * amplitudeDirection(0)); } template < typename Direction, typename Row> [[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::HydrostaticBalance, mean_field::stellar::state::SpecificEnthalpy>, const Direction &, Row & ) const noexcept { return mean_field::stellar::zeroDerivative; } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::HydrostaticBalance, mean_field::stellar::state::OwnGeneratedCoordinate>, const Direction &direction, Row &row ) const { const auto amplitudeDirection = direction.generatedCoordinate(); return row.add(m_target * amplitudeDirection(0)); } [[nodiscard]] bool IsPrepared() const noexcept { return m_isPrepared; } [[nodiscard]] double target() const noexcept { return m_target; } [[nodiscard]] double referenceEnthalpy() const noexcept { return m_referenceEnthalpy; } [[nodiscard]] double amplitude() const noexcept { return m_amplitude; } private: double m_target{0.0}; double m_referenceEnthalpy{0.0}; double m_amplitude{0.0}; double m_invariantResidual{0.0}; double m_hydrostaticContribution{0.0}; bool m_isPrepared{false}; }; class PreparedCoreThermalBalance final { public: using Report = CoreThermalBalancePreparationReport; using IntegralContext = mean_field::stellar::DensityVolumeIntegralContext; PreparedCoreThermalBalance( const FixedCoreThermalBalance &specification, IntegralContext integralContext ) noexcept : m_targetPressure(specification.target().value()), m_referenceEnthalpy(specification.referenceSpecificEnthalpy().value()), m_referenceMass(specification.referenceMass().value()), m_integralContext(integralContext) { } template [[nodiscard]] Report PrepareAfterPhysical(const StateView &state) { const auto density = state.density(); const auto enthalpy = state.specificEnthalpy(); const auto multiplier = state.generatedCoordinate(); const auto magneticAmplitude = state.template generatedCoordinate(); if (enthalpy.Size() == 0 || multiplier.Size() != 1 || magneticAmplitude.Size() != 1 || !std::isfinite(enthalpy(0)) || !std::isfinite(multiplier(0)) || !std::isfinite(magneticAmplitude(0))) { throw std::invalid_argument( "Core-thermal-balance preparation requires finite enthalpy, multiplier, and magnetic amplitude." ); } const double integratedMass = m_integralContext.integrateDensity(density).value(); const bool changed = !m_isPrepared || enthalpy(0) != m_stateEnthalpy || multiplier(0) != m_multiplier || magneticAmplitude(0) != m_magneticAmplitude || integratedMass != m_integratedMass; m_stateEnthalpy = enthalpy(0); m_multiplier = multiplier(0); m_magneticAmplitude = magneticAmplitude(0); m_integratedMass = integratedMass; const double normalizedMultiplier = m_multiplier / m_referenceEnthalpy; m_constraintResidual = m_targetPressure * (m_stateEnthalpy / m_referenceEnthalpy + 0.25 * normalizedMultiplier * normalizedMultiplier + thermalMagneticCoupling * m_magneticAmplitude * normalizedMultiplier - 1.0 + thermalMassCoupling * (m_integratedMass / m_referenceMass - 1.0)); m_magneticConstraintContribution = magneticThermalFeedback * m_magneticAmplitude * m_multiplier; m_hydrostaticContribution = m_multiplier; m_isPrepared = true; return {.stateChanged = changed}; } template [[nodiscard]] auto AddResidual( mean_field::stellar::equation::OwnConstraint, Row &row ) const { return row.add(m_constraintResidual); } template [[nodiscard]] auto AddResidual( mean_field::stellar::equation::HydrostaticBalance, Row &row ) const { return row.add(m_hydrostaticContribution); } template [[nodiscard]] auto AddResidual( mean_field::stellar::equation::ConstraintOf, Row &row ) const { return row.add(m_magneticConstraintContribution); } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::OwnConstraint, mean_field::stellar::state::Density>, const Direction &direction, Row &row ) const { const double massDirection = m_integralContext.linearizeDensityIntegral(direction.density()).value(); return row.add(thermalMassCoupling * m_targetPressure / m_referenceMass * massDirection); } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::OwnConstraint, mean_field::stellar::state::SurfaceShape>, const Direction &direction, Row &row ) const { const double massDirection = m_integralContext.linearizeSurfaceShapeIntegral(direction.surfaceShape()).value(); return row.add(thermalMassCoupling * m_targetPressure / m_referenceMass * massDirection); } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::OwnConstraint, mean_field::stellar::state::SpecificEnthalpy>, const Direction &direction, Row &row ) const { return row.add(m_targetPressure / m_referenceEnthalpy * direction.specificEnthalpy()(0)); } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::OwnConstraint, mean_field::stellar::state::OwnGeneratedCoordinate>, const Direction &direction, Row &row ) const { return row.add( m_targetPressure / m_referenceEnthalpy * (0.5 * m_multiplier / m_referenceEnthalpy + thermalMagneticCoupling * m_magneticAmplitude) * direction.generatedCoordinate()(0) ); } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::OwnConstraint, mean_field::stellar::state::GeneratedCoordinateOf>, const Direction &direction, Row &row ) const { return row.add( thermalMagneticCoupling * m_targetPressure * m_multiplier / m_referenceEnthalpy * direction.template generatedCoordinate()(0) ); } template < typename Direction, typename Row> [[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::HydrostaticBalance, mean_field::stellar::state::Density>, const Direction &, Row & ) const noexcept { return mean_field::stellar::zeroDerivative; } template < typename Direction, typename Row> [[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::HydrostaticBalance, mean_field::stellar::state::SurfaceShape>, const Direction &, Row & ) const noexcept { return mean_field::stellar::zeroDerivative; } template < typename Direction, typename Row> [[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::HydrostaticBalance, mean_field::stellar::state::SpecificEnthalpy>, const Direction &, Row & ) const noexcept { return mean_field::stellar::zeroDerivative; } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::HydrostaticBalance, mean_field::stellar::state::OwnGeneratedCoordinate>, const Direction &direction, Row &row ) const { return row.add(direction.generatedCoordinate()(0)); } template < typename Direction, typename Row> [[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::HydrostaticBalance, mean_field::stellar::state::GeneratedCoordinateOf>, const Direction &, Row & ) const noexcept { return mean_field::stellar::zeroDerivative; } template < typename Direction, typename Row> [[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::ConstraintOf, mean_field::stellar::state::Density>, const Direction &, Row & ) const noexcept { return mean_field::stellar::zeroDerivative; } template < typename Direction, typename Row> [[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::ConstraintOf, mean_field::stellar::state::SurfaceShape>, const Direction &, Row & ) const noexcept { return mean_field::stellar::zeroDerivative; } template < typename Direction, typename Row> [[nodiscard]] mean_field::stellar::StructuralZero AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::ConstraintOf, mean_field::stellar::state::SpecificEnthalpy>, const Direction &, Row & ) const noexcept { return mean_field::stellar::zeroDerivative; } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::ConstraintOf, mean_field::stellar::state::OwnGeneratedCoordinate>, const Direction &direction, Row &row ) const { return row.add(magneticThermalFeedback * m_magneticAmplitude * direction.generatedCoordinate()(0)); } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::ConstraintOf, mean_field::stellar::state::GeneratedCoordinateOf>, const Direction &direction, Row &row ) const { return row.add( magneticThermalFeedback * m_multiplier * direction.template generatedCoordinate()(0) ); } [[nodiscard]] bool IsPrepared() const noexcept { return m_isPrepared; } [[nodiscard]] double target() const noexcept { return m_targetPressure; } [[nodiscard]] double referenceEnthalpy() const noexcept { return m_referenceEnthalpy; } [[nodiscard]] double stateEnthalpy() const noexcept { return m_stateEnthalpy; } [[nodiscard]] double referenceMass() const noexcept { return m_referenceMass; } [[nodiscard]] double integratedMass() const noexcept { return m_integratedMass; } [[nodiscard]] IntegralContext integralContext() const noexcept { return m_integralContext; } [[nodiscard]] double multiplier() const noexcept { return m_multiplier; } [[nodiscard]] double magneticAmplitude() const noexcept { return m_magneticAmplitude; } private: double m_targetPressure{0.0}; double m_referenceEnthalpy{0.0}; double m_referenceMass{0.0}; double m_stateEnthalpy{0.0}; double m_multiplier{0.0}; double m_magneticAmplitude{0.0}; double m_integratedMass{0.0}; double m_constraintResidual{0.0}; double m_magneticConstraintContribution{0.0}; double m_hydrostaticContribution{0.0}; IntegralContext m_integralContext; bool m_isPrepared{false}; }; /* No separate border-action class is needed. The generic * preconditioner adapter projects the exact AddJacobianAction providers * above onto the compiler-inferred border incidence set. */ } // namespace magnetic_specific_energy_extension_test namespace { namespace extension = magnetic_specific_energy_extension_test; namespace blocks = mean_field::utils::blocks; using MagneticIntegral = extension::FixedMagneticSpecificEnergy; using MagneticModel = mean_field::model::StellarModel>; using ReorderedMagneticModel = decltype(mean_field::model::StellarModel( std::declval(), std::declval(), std::declval(), std::declval() )); using RieszDiscretization = mean_field::equilibrium::StellarDiscretizationFor>; using MagneticProblem = mean_field::equilibrium::StellarEquilibriumProblem; using MagneticForm = typename MagneticProblem::FormType; using MagneticJacobian = typename MagneticProblem::JacobianFormType; using MagneticBorder = mean_field::preconditioning::CompiledSpecificationBorderFor; using ThermalConstraint = extension::FixedCoreThermalBalance; using DualExtensionModel = mean_field::model::StellarModel>; using ReorderedDualExtensionModel = decltype(mean_field::model::StellarModel( std::declval(), std::declval(), std::declval(), std::declval(), std::declval(), std::declval() )); using DualExtensionProblem = mean_field::equilibrium::StellarEquilibriumProblem; using DualExtensionForm = typename DualExtensionProblem::FormType; using DualExtensionJacobian = typename DualExtensionProblem::JacobianFormType; using DualExtensionBorder = mean_field::preconditioning::CompiledSpecificationBorderFor; template concept HasDensity = requires(const View &view) { view.density(); }; template concept HasSpecificEnthalpy = requires(const View &view) { view.specificEnthalpy(); }; template concept HasGeneratedCoordinate = requires(const View &view) { view.generatedCoordinate(); }; template concept HasConstraintResidual = requires(const View &view) { view.constraintResidual(); }; struct ReadSpecificEnthalpyAndAdd final { template < typename Direction, typename Row> requires requires( const Direction &direction, Row &row ) { direction.specificEnthalpy(); row.add(direction(0)); } [[nodiscard]] auto operator()( const Direction &direction, Row &row ) const { return row.add(direction(0)); } }; struct ReadDensityAndAdd final { template < typename Direction, typename Row> requires requires( const Direction &direction, Row &row ) { direction.density(); row.add(direction(0)); } [[nodiscard]] auto operator()( const Direction &direction, Row &row ) const { return row.add(direction(0)); } }; struct ReadGeneratedCoordinateAndAdd final { template < typename Direction, typename Row> requires requires( const Direction &direction, Row &row ) { direction.generatedCoordinate(); row.add(direction(0)); } [[nodiscard]] auto operator()( const Direction &direction, Row &row ) const { return row.add(direction(0)); } }; template concept CanAddSpecificEnthalpyFromGenerated = requires(const View &view) { view.addSpecificEnthalpyFrom(extension::magneticAmplitudeTerm, ReadGeneratedCoordinateAndAdd{}); }; template concept CanAddConstraintResidualFromEnthalpy = requires(const View &view) { view.addConstraintResidualFrom(blocks::enthalpy_field.specific_term, ReadSpecificEnthalpyAndAdd{}); }; template concept CanAddConstraintResidualFromGenerated = requires(const View &view) { view.addConstraintResidualFrom(extension::magneticAmplitudeTerm, ReadGeneratedCoordinateAndAdd{}); }; template concept CanClaimEnthalpyButReadDensity = requires(const View &view) { view.addConstraintResidualFrom(blocks::enthalpy_field.specific_term, ReadDensityAndAdd{}); }; template concept HasDidAnyWork = requires(const Report &report) { report.DidAnyWork(); }; /* Deliberately dishonest providers used to exercise the runtime side of * the exact-provider contract. These compile because their signatures are * valid; the single-use row must still reject what they actually do. */ struct AddsToOneResidualRowTwice final { template [[nodiscard]] auto AddResidual( mean_field::stellar::equation::OwnConstraint, Row &row ) const { const auto firstContribution = row.add(1.0); static_cast(firstContribution); return row.add(2.0); } }; struct ReportsContributionWithoutAdding final { template [[nodiscard]] mean_field::stellar::ContributionAdded AddResidual( mean_field::stellar::equation::OwnConstraint, Row & ) const noexcept { return {}; } }; struct ReportsStructuralZeroAfterAdding final { template [[nodiscard]] mean_field::stellar::StructuralZero AddResidual( mean_field::stellar::equation::OwnConstraint, Row &row ) const { const auto contribution = row.add(1.0); static_cast(contribution); return mean_field::stellar::structuralZero; } }; [[nodiscard]] mean_field::operators::StellarEquilibriumDependencies makeDependencies() { return { .discretization = {.identity = 83003, .revision = 1}, .density = {.identity = 83009, .revision = 1}, .surfaceDeformation = {.identity = 83023, .revision = 1}, .gravityGradient = {.identity = 83047, .revision = 1}, .gravityPotential = {.identity = 83059, .revision = 1}, .enthalpy = {.identity = 83063, .revision = 1}, .bernoulliConstant = {.identity = 83071, .revision = 1}, .rotation = {.identity = 83077, .revision = 1}, .targetMass = {.identity = 83089, .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]] bool allFinite(const mfem::Vector &vector) { for (int index = 0; index < vector.Size(); ++index) { if (!std::isfinite(vector(index))) { return false; } } return true; } } // namespace TEST_CASE( "A Third-Party Magnetic Integral Compiles Through Every Coupled System Layer", "[extensions][integral][type-contract][preconditioning][normalization]" ) { namespace mf = mean_field; using StructureToBorderAction = mf::preconditioning::SpecificationStructureToBorderActionView; using BorderToStructureAction = mf::preconditioning::SpecificationBorderToStructureActionView; using BorderToBorderAction = mf::preconditioning::SpecificationBorderToBorderActionView; using PreparedMagneticPhysics = mf::preconditioning::PreparedSpecificationEquilibriumPhysicsT; using PreparedMagneticBorder = mf::preconditioning::PreparedSpecificationBorderPhysicsT; STATIC_CHECK(std::same_as); STATIC_CHECK(mf::models::ModelSpecification); STATIC_CHECK_FALSE(mf::operators::StellarEquilibriumRuntimeContribution::registered); STATIC_CHECK_FALSE(HasDidAnyWork); STATIC_CHECK(mf::operators::StellarEquilibriumPhysicsAvailableFor); STATIC_CHECK(mf::equilibrium::StellarEquilibriumModel); STATIC_CHECK(mf::equilibrium::StellarEquilibriumModelDiscretizationCompatible); STATIC_CHECK(MagneticModel::symbolicallySquare); STATIC_CHECK(MagneticForm::value_block_count == 7); STATIC_CHECK(MagneticForm::residual_block_count == 7); STATIC_CHECK(MagneticBorder::valueArity == 2); STATIC_CHECK(MagneticBorder::residualArity == 2); STATIC_CHECK(MagneticBorder::specificationCount == 2); STATIC_CHECK(mf::preconditioning::SpecificationBorderContribution::RequiredCouplings::size == 3); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< typename extension::MagneticAmplitudeTerm::residual, typename extension::MagneticAmplitudeTerm::value, MagneticJacobian> ); STATIC_CHECK(std::same_as); STATIC_CHECK(std::constructible_from); STATIC_CHECK(mf::preconditioning::PreparedSpecificationBorderActionFor); STATIC_CHECK(mf::preconditioning::SpecificationBorderPhysicsAvailableFor); STATIC_CHECK(mf::preconditioning::DefaultStellarPreconditionerAvailableFor); // The operation object exposes neither raw endpoint. Selecting a compiled // pair binds the callback to exactly one source and exactly one row. STATIC_CHECK_FALSE(HasConstraintResidual); STATIC_CHECK(CanAddConstraintResidualFromEnthalpy); STATIC_CHECK_FALSE(CanAddConstraintResidualFromGenerated); STATIC_CHECK_FALSE(CanClaimEnthalpyButReadDensity); STATIC_CHECK_FALSE(HasSpecificEnthalpy); STATIC_CHECK_FALSE(HasDensity); STATIC_CHECK_FALSE(HasSpecificEnthalpy); STATIC_CHECK(CanAddSpecificEnthalpyFromGenerated); STATIC_CHECK_FALSE(HasGeneratedCoordinate); STATIC_CHECK_FALSE(HasConstraintResidual); STATIC_CHECK(CanAddConstraintResidualFromGenerated); STATIC_CHECK_FALSE(CanAddConstraintResidualFromEnthalpy); STATIC_CHECK( mf::preconditioning::specificationBorderValueOffset != mf::preconditioning::specificationBorderValueOffset ); STATIC_CHECK( mf::preconditioning::specificationBorderResidualOffset != mf::preconditioning::specificationBorderResidualOffset ); } TEST_CASE( "Exact Physics Provider Rows Reject Inconsistent Contribution Accounting", "[extensions][integral][runtime-contract]" ) { namespace mf = mean_field; const mf::utils::Args arguments = test_utils::setup_args(); mf::fem::FEM finiteElements = mf::fem::setup_fem(arguments.mesh_file, arguments, 0); REQUIRE(finiteElements.okay()); auto model = mf::model::StellarModel( mf::eos::Polytrope({.n = 1.0, .K = 0.25}), mf::surface::Isobaric({.Psurf = mf::dimensions::PressureValue{0.0}}), mf::integral::FixedTotalMass({.Mtotal = mf::dimensions::MassValue{1.0}}), MagneticIntegral({.target = mf::dimensions::SpecificEnergyValue{1.0}}) ); auto problem = mf::equilibrium::discretize(model, std::move(finiteElements)); using Access = mf::operators::detail::SpecificationRuntimeAccess; using Equations = mf::utils::blocks::type_list; using DoubleAdd = mf::operators::detail::ExactResidualProviderSet; using MissingAdd = mf::operators::detail::ExactResidualProviderSet; using ZeroAfterAdd = mf::operators::detail::ExactResidualProviderSet; STATIC_CHECK(DoubleAdd::complete); STATIC_CHECK(MissingAdd::complete); STATIC_CHECK(ZeroAfterAdd::complete); mfem::Vector residual(problem.EquationSize()); residual = 0.0; typename Access::ResidualView restrictedResidual{ problem.GetManifest().residualView(residual), problem.GetPhysicalOperator().GetSurfaceConstraintOperator() }; CHECK_THROWS_AS(DoubleAdd::Apply(AddsToOneResidualRowTwice{}, restrictedResidual), std::logic_error); CHECK_THROWS_AS(MissingAdd::Apply(ReportsContributionWithoutAdding{}, restrictedResidual), std::logic_error); CHECK_THROWS_AS(ZeroAfterAdd::Apply(ReportsStructuralZeroAfterAdding{}, restrictedResidual), std::logic_error); } TEST_CASE( "A Third-Party Magnetic Integral Retains Its Physics Under Normalization And Preconditioning", "[extensions][integral][physics][jacobian][preconditioning][normalization][integration]" ) { namespace mf = mean_field; const mf::utils::Args arguments = test_utils::setup_args(); mf::fem::FEM finiteElements = mf::fem::setup_fem(arguments.mesh_file, arguments, 0); REQUIRE(finiteElements.okay()); constexpr double referenceRadius = 2.0; constexpr double gravitationalConstant = 3.0; constexpr double targetMass = 1.0; // Deliberately distinct from GM/R (= 1.5). This prevents a target-based // normalization from accidentally passing a characteristic-scale test. constexpr double targetMagneticSpecificEnergy = 1.7; constexpr double referenceEnthalpy = 2.0; constexpr double magneticAmplitude = 0.8; auto model = mf::model::StellarModel( mf::eos::Polytrope({.n = 1.0, .K = 0.25}), mf::surface::Isobaric({.Psurf = mf::dimensions::PressureValue{0.0}}), mf::integral::FixedTotalMass({.Mtotal = mf::dimensions::MassValue{targetMass}}), MagneticIntegral({.target = mf::dimensions::SpecificEnergyValue{targetMagneticSpecificEnergy}}) ); auto problem = mf::equilibrium::discretize( model, mf::equilibrium::makeStellarDiscretization( std::move(finiteElements), mf::normalization::PhysicalRieszDiagonal{ mf::dimensions::LengthValue{referenceRadius}, gravitationalConstant } ) ); using Problem = std::remove_cvref_t; using Form = typename Problem::FormType; constexpr auto magneticValueBlock = blocks::get_value_block
(extension::magneticAmplitudeTerm); constexpr auto magneticResidualBlock = blocks::get_residual_block(extension::magneticAmplitudeTerm); const auto &manifest = problem.GetManifest(); const auto &layout = manifest.layout(); const auto &descriptor = manifest.specification(); CHECK(descriptor.stableId == "FixedMagneticSpecificEnergy"); CHECK(descriptor.role == mf::models::SpecificationRole::invariant); CHECK(descriptor.columnPolicy == mf::operators::RootColumnPolicy::generated_physical_coordinate); CHECK(descriptor.valueBlock == magneticValueBlock.index); CHECK(descriptor.residualBlock == magneticResidualBlock.index); CHECK(descriptor.target == Catch::Approx(targetMagneticSpecificEnergy)); CHECK(descriptor.residualScale == Catch::Approx(targetMagneticSpecificEnergy)); CHECK(descriptor.targetUnits == "specific_energy"); CHECK(descriptor.residualUnits == "specific_energy"); CHECK(layout.size(magneticValueBlock) == 1); CHECK(layout.size(magneticResidualBlock) == 1); auto normalized = mf::normalization::makeNormalizedStellarEquilibriumOperator(problem); const auto scales = mf::normalization::deriveStellarCharacteristicScales( mf::dimensions::MassValue{targetMass}, mf::dimensions::LengthValue{referenceRadius}, gravitationalConstant ); CHECK(scales.specificEnergy == Catch::Approx(1.5)); CHECK(scales.specificEnergy != Catch::Approx(targetMagneticSpecificEnergy)); CHECK( normalized.GetNormalization().StateFactors()(layout.offset(magneticValueBlock)) == Catch::Approx(1.0).epsilon(2.0e-15) ); CHECK( normalized.GetNormalization().ResidualFactors()(layout.offset(magneticResidualBlock)) == Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15) ); mfem::Vector state(problem.StateSize()); state = 0.0; const auto stateView = manifest.stateView(state); stateView.block(blocks::density_field.mass_term) = 1.0; stateView.block(blocks::enthalpy_field.specific_term) = referenceEnthalpy; stateView.block(blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.25; stateView.block(extension::magneticAmplitudeTerm) = magneticAmplitude; auto dependencies = makeDependencies(); problem.Prepare(state, dependencies, zeroRotation()); mfem::Vector residual; problem.BuildResidual(residual); REQUIRE(allFinite(residual)); auto residualView = manifest.residualView(residual); const double expectedInvariant = 0.5 * magneticAmplitude * magneticAmplitude * referenceEnthalpy - targetMagneticSpecificEnergy; CHECK(residualView.block(extension::magneticAmplitudeTerm)(0) == Catch::Approx(expectedInvariant).epsilon(4.0e-14)); // A centered difference in the generated amplitude probes both its // magnetic-pressure column and its own nonlinear invariant row. mfem::Vector amplitudeDirection(problem.StateSize()); amplitudeDirection = 0.0; constexpr double amplitudeVariation = 0.37; manifest.stateView(amplitudeDirection).block(extension::magneticAmplitudeTerm)(0) = amplitudeVariation; mfem::Vector analyticAmplitudeAction; problem.ApplyLinearization(amplitudeDirection, analyticAmplitudeAction); const auto analyticAmplitudeView = manifest.residualView(analyticAmplitudeAction); const auto hydrostaticAmplitudeAction = analyticAmplitudeView.block(blocks::enthalpy_field.specific_term); std::vector isSurfaceRow(static_cast(hydrostaticAmplitudeAction.Size()), false); for (const int row : problem.GetPressureSurfaceRows().reduced_dofs()) { isSurfaceRow[static_cast(row)] = true; CHECK(hydrostaticAmplitudeAction(row) == Catch::Approx(0.0).margin(2.0e-14)); } int interiorRowsChecked = 0; for (int row = 0; row < hydrostaticAmplitudeAction.Size(); ++row) { if (!isSurfaceRow[static_cast(row)]) { CHECK( hydrostaticAmplitudeAction(row) == Catch::Approx(targetMagneticSpecificEnergy * amplitudeVariation).epsilon(3.0e-14) ); ++interiorRowsChecked; } } REQUIRE(interiorRowsChecked > 0); CHECK( analyticAmplitudeView.block(extension::magneticAmplitudeTerm)(0) == Catch::Approx(magneticAmplitude * referenceEnthalpy * amplitudeVariation).epsilon(3.0e-14) ); constexpr double epsilon = 2.0e-7; mfem::Vector plusState(state); mfem::Vector minusState(state); manifest.stateView(plusState).block(extension::magneticAmplitudeTerm)(0) += epsilon * amplitudeVariation; manifest.stateView(minusState).block(extension::magneticAmplitudeTerm)(0) -= epsilon * amplitudeVariation; mfem::Vector plusResidual; mfem::Vector minusResidual; problem.Prepare(plusState, dependencies, zeroRotation()); problem.BuildResidual(plusResidual); problem.Prepare(minusState, dependencies, zeroRotation()); problem.BuildResidual(minusResidual); mfem::Vector finiteDifference(plusResidual); finiteDifference -= minusResidual; finiteDifference /= 2.0 * epsilon; CHECK(relativeError(analyticAmplitudeAction, finiteDifference) <= 3.0e-9); // A structure-only perturbation independently exercises de_B/dh_0. problem.Prepare(state, dependencies, zeroRotation()); mfem::Vector enthalpyDirection(problem.StateSize()); enthalpyDirection = 0.0; constexpr double enthalpyVariation = 0.29; manifest.stateView(enthalpyDirection).block(blocks::enthalpy_field.specific_term)(0) = enthalpyVariation; mfem::Vector enthalpyAction; problem.ApplyLinearization(enthalpyDirection, enthalpyAction); auto enthalpyActionView = manifest.residualView(enthalpyAction); CHECK( enthalpyActionView.block(extension::magneticAmplitudeTerm)(0) == Catch::Approx(0.5 * magneticAmplitude * magneticAmplitude * enthalpyVariation).epsilon(3.0e-14) ); mfem::Vector normalizedState; normalized.NormalizeState(state, normalizedState); normalized.Prepare(normalizedState, dependencies, zeroRotation()); REQUIRE(normalized.IsPrepared()); mfem::Vector normalizedDirection(problem.StateSize()); for (int index = 0; index < normalizedDirection.Size(); ++index) { normalizedDirection(index) = 0.021 * std::sin(0.19 * 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) <= 5.0e-13); auto component = mf::preconditioning::makePreconditioner(problem); using Component = decltype(component); STATIC_CHECK( mf::preconditioning::CompletePreconditionerFor< mf::preconditioning::PreconditionerPlan, typename Problem::FormType> ); STATIC_CHECK( mf::preconditioning::CompatiblePreconditionerFor< mf::preconditioning::PreconditionerPlan, typename Problem::FormType, typename Problem::JacobianFormType> ); auto physicalInverse = mf::preconditioning::prepare(problem, component); REQUIRE(physicalInverse.IsCurrent()); const auto &coordinateMap = physicalInverse.GetCoordinateMap(); mfem::Vector groupedAmplitudeDirection(coordinateMap.PreconditionerCorrectionSize()); mfem::Vector groupedCouplingAction(coordinateMap.PreconditionerResidualSize()); mfem::Vector inferredCouplingAction(problem.EquationSize()); coordinateMap.PackCorrection(amplitudeDirection, groupedAmplitudeDirection); physicalInverse.GetGroupedPreconditioner().GetCouplings().Mult(groupedAmplitudeDirection, groupedCouplingAction); coordinateMap.UnpackResidual(groupedCouplingAction, inferredCouplingAction); CHECK(relativeError(inferredCouplingAction, analyticAmplitudeAction) <= 4.0e-13); auto scaledInverse = normalized.MakeScaledPreconditioner(physicalInverse); REQUIRE(scaledInverse.IsCurrent()); mfem::Vector normalizedRightHandSide(problem.EquationSize()); for (int index = 0; index < normalizedRightHandSide.Size(); ++index) { normalizedRightHandSide(index) = 0.17 * std::sin(0.037 * static_cast(index + 1)) + 0.05 * std::cos(0.023 * static_cast(index + 1)); } mfem::Vector actualCorrection(problem.StateSize()); mfem::Vector repeatedCorrection(problem.StateSize()); scaledInverse.Mult(normalizedRightHandSide, actualCorrection); scaledInverse.Mult(normalizedRightHandSide, repeatedCorrection); REQUIRE(allFinite(actualCorrection)); CHECK(relativeError(actualCorrection, repeatedCorrection) <= 2.0e-14); mfem::Vector physicalRightHandSide; mfem::Vector physicalCorrection(problem.StateSize()); mfem::Vector expectedCorrection; normalized.DenormalizeResidual(normalizedRightHandSide, physicalRightHandSide); physicalInverse.Mult(physicalRightHandSide, physicalCorrection); normalized.NormalizeState(physicalCorrection, expectedCorrection); CHECK(relativeError(actualCorrection, expectedCorrection) <= 5.0e-13); const auto correctionView = manifest.stateView(actualCorrection); const double magneticCorrection = correctionView.block(extension::magneticAmplitudeTerm)(0); CAPTURE(magneticCorrection); CHECK(std::isfinite(magneticCorrection)); CHECK(std::abs(magneticCorrection) > 1.0e-16); // The extension's border action snapshots state-dependent coefficients. // Reusing the same dependency stamps with a different Newton state must // still invalidate and reconstruct those coefficients. mfem::Vector changedState(state); constexpr double changedReferenceEnthalpy = 2.4; constexpr double changedMagneticAmplitude = 1.1; auto changedStateView = manifest.stateView(changedState); auto changedEnthalpy = changedStateView.block(blocks::enthalpy_field.specific_term); changedEnthalpy = changedReferenceEnthalpy; changedEnthalpy.SyncAliasMemory(changedState); auto changedAmplitude = changedStateView.block(extension::magneticAmplitudeTerm); changedAmplitude = changedMagneticAmplitude; changedAmplitude.SyncAliasMemory(changedState); mfem::Vector changedNormalizedState; normalized.NormalizeState(changedState, changedNormalizedState); const auto changedPreparation = normalized.Prepare(changedNormalizedState, dependencies, zeroRotation()); CHECK(changedPreparation.template specification().stateChanged); CHECK(changedPreparation.assembledResidual); mfem::Vector changedResidual; problem.BuildResidual(changedResidual); const double changedInvariant = 0.5 * changedMagneticAmplitude * changedMagneticAmplitude * changedReferenceEnthalpy - targetMagneticSpecificEnergy; CHECK( manifest.residualView(changedResidual).block(extension::magneticAmplitudeTerm)(0) == Catch::Approx(changedInvariant).epsilon(4.0e-14) ); CHECK_FALSE(physicalInverse.IsCurrent()); CHECK_FALSE(scaledInverse.IsCurrent()); mfem::Vector changedAnalyticAmplitudeAction; problem.ApplyLinearization(amplitudeDirection, changedAnalyticAmplitudeAction); mfem::Vector stateDependentDifference(changedAnalyticAmplitudeAction); stateDependentDifference -= analyticAmplitudeAction; CHECK(stateDependentDifference.Norml2() > 1.0e-8); const auto changedRefresh = physicalInverse.Refresh(); CHECK(changedRefresh.specificationActionsRefreshed); CHECK(changedRefresh.rebuiltSchurComplement); CHECK(physicalInverse.IsCurrent()); CHECK(scaledInverse.IsCurrent()); physicalInverse.GetGroupedPreconditioner().GetCouplings().Mult(groupedAmplitudeDirection, groupedCouplingAction); coordinateMap.UnpackResidual(groupedCouplingAction, inferredCouplingAction); CHECK(relativeError(inferredCouplingAction, changedAnalyticAmplitudeAction) <= 4.0e-13); const auto noOpRefresh = physicalInverse.Refresh(); CHECK_FALSE(noOpRefresh.DidAnyWork()); } TEST_CASE( "Two Directly Coupled Third-Party Scalar Constraints Survive The Complete Inferred Stellar Stack", "[extensions][variadic][constraint][multiplier][fixed-angular-momentum][jacobian][normalization][preconditioning][" "lifecycle][integration]" ) { namespace mf = mean_field; using Catch::Approx; using PreparedThermalPhysics = mf::preconditioning::PreparedSpecificationEquilibriumPhysicsT; using PreparedMagneticBorder = mf::preconditioning::PreparedSpecificationBorderPhysicsT; using PreparedThermalBorder = mf::preconditioning::PreparedSpecificationBorderPhysicsT; using ThermalIntegralContext = mf::stellar::DensityVolumeIntegralContext; using ThermalTopology = mf::operators::StellarEquilibriumContributionTopology; STATIC_CHECK(std::same_as); STATIC_CHECK(mf::models::ModelSpecification); STATIC_CHECK(mf::models::ModelSpecification); STATIC_CHECK_FALSE(mf::operators::StellarEquilibriumRuntimeContribution::registered); STATIC_CHECK_FALSE(mf::operators::StellarEquilibriumRuntimeContribution::registered); STATIC_CHECK(mf::operators::StellarEquilibriumPhysicsAvailableFor); STATIC_CHECK(mf::operators::StellarEquilibriumPhysicsAvailableFor); STATIC_CHECK(mf::equilibrium::StellarEquilibriumModel); STATIC_CHECK(mf::operators::StellarEquilibriumSystemCompilable); STATIC_CHECK( mf::equilibrium::StellarEquilibriumModelDiscretizationCompatible ); STATIC_CHECK(DualExtensionForm::value_block_count == 9); STATIC_CHECK(DualExtensionForm::residual_block_count == 9); STATIC_CHECK(DualExtensionBorder::valueArity == 4); STATIC_CHECK(DualExtensionBorder::residualArity == 4); STATIC_CHECK(DualExtensionBorder::specificationCount == 4); STATIC_CHECK(DualExtensionBorder::RequiredCouplings::size == 20); STATIC_CHECK(ThermalTopology::ResidualEquations::size == 3); STATIC_CHECK(ThermalTopology::Derivatives::size == 15); STATIC_CHECK(mf::preconditioning::SpecificationBorderContribution::RequiredCouplings::size == 3); STATIC_CHECK( mf::preconditioning::SpecificationBorderContribution::RequiredCouplings::size == 12 ); STATIC_CHECK_FALSE(mf::operators::DensityVolumeIntegralSpecification); STATIC_CHECK(mf::operators::DensityVolumeIntegralSpecification); STATIC_CHECK(mf::normalization::CompleteStellarNormalizationFor); STATIC_CHECK(mf::preconditioning::DefaultStellarPreconditionerAvailableFor); STATIC_CHECK(mf::preconditioning::SpecificationBorderPhysicsAvailableFor); STATIC_CHECK(mf::preconditioning::SpecificationBorderPhysicsAvailableFor); STATIC_CHECK(std::same_as); STATIC_CHECK(std::constructible_from); STATIC_CHECK_FALSE(std::constructible_from); STATIC_CHECK(std::constructible_from); STATIC_CHECK(std::constructible_from); STATIC_CHECK( mf::preconditioning::PreparedSpecificationBorderActionFor ); STATIC_CHECK( mf::preconditioning::PreparedSpecificationBorderActionFor ); STATIC_CHECK_FALSE(std::same_as); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< typename extension::MagneticAmplitudeTerm::residual, typename extension::MagneticAmplitudeTerm::value, DualExtensionJacobian> ); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< typename extension::MagneticAmplitudeTerm::residual, mf::utils::blocks::enthalpy::specific::value, DualExtensionJacobian> ); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< mf::utils::blocks::enthalpy::specific::residual, typename extension::MagneticAmplitudeTerm::value, DualExtensionJacobian> ); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< typename extension::CoreThermalMultiplierTerm::residual, typename extension::CoreThermalMultiplierTerm::value, DualExtensionJacobian> ); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< typename extension::CoreThermalMultiplierTerm::residual, mf::utils::blocks::enthalpy::specific::value, DualExtensionJacobian> ); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< mf::utils::blocks::enthalpy::specific::residual, typename extension::CoreThermalMultiplierTerm::value, DualExtensionJacobian> ); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< typename extension::MagneticAmplitudeTerm::residual, typename extension::CoreThermalMultiplierTerm::value, DualExtensionJacobian> ); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< typename extension::CoreThermalMultiplierTerm::residual, typename extension::MagneticAmplitudeTerm::value, DualExtensionJacobian> ); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< typename extension::CoreThermalMultiplierTerm::residual, mf::utils::blocks::density::mass::value, DualExtensionJacobian> ); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< typename extension::CoreThermalMultiplierTerm::residual, mf::utils::blocks::surface_deformation::parameters::value, DualExtensionJacobian> ); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< typename extension::MagneticAmplitudeTerm::residual, mf::utils::blocks::density::mass::value, DualExtensionJacobian> ); STATIC_CHECK( mf::utils::blocks::has_jacobian_coupling_v< typename extension::MagneticAmplitudeTerm::residual, mf::utils::blocks::surface_deformation::parameters::value, DualExtensionJacobian> ); const mf::utils::Args arguments = test_utils::setup_args(); mf::fem::FEM finiteElements = mf::fem::setup_fem(arguments.mesh_file, arguments, 0); REQUIRE(finiteElements.okay()); constexpr double referenceRadius = 2.0; constexpr double gravitationalConstant = 3.0; constexpr double targetMass = 1.0; constexpr double targetAngularMomentum = 0.2; constexpr double targetMagneticSpecificEnergy = 1.7; constexpr double targetThermalPressure = 1.4; constexpr double thermalReferenceEnthalpy = 2.6; constexpr double thermalReferenceMass = 2.3; constexpr double referenceEnthalpy = 2.0; constexpr double magneticAmplitude = 0.8; constexpr double thermalMultiplier = -0.6; constexpr double angularVelocity = 0.35; // Deliberately spell the specifications in a noncanonical order. The // resulting object must still be the exact type audited above. auto model = mf::model::StellarModel( ThermalConstraint( {.target = mf::dimensions::PressureValue{targetThermalPressure}, .referenceSpecificEnthalpy = mf::dimensions::SpecificEnthalpyValue{thermalReferenceEnthalpy}, .referenceMass = mf::dimensions::MassValue{thermalReferenceMass}} ), mf::integral::FixedAngularMomentum({.Jtotal = mf::dimensions::AngularMomentumValue{targetAngularMomentum}}), mf::surface::Isobaric({.Psurf = mf::dimensions::PressureValue{0.0}}), MagneticIntegral({.target = mf::dimensions::SpecificEnergyValue{targetMagneticSpecificEnergy}}), mf::eos::Polytrope({.n = 1.0, .K = 0.25}), mf::integral::FixedTotalMass({.Mtotal = mf::dimensions::MassValue{targetMass}}) ); STATIC_CHECK(std::same_as, DualExtensionModel>); auto problem = mf::equilibrium::discretize( model, mf::equilibrium::makeStellarDiscretization( std::move(finiteElements), mf::normalization::PhysicalRieszDiagonal{ mf::dimensions::LengthValue{referenceRadius}, gravitationalConstant } ) ); using Problem = std::remove_cvref_t; using Form = typename Problem::FormType; STATIC_CHECK(std::same_as); constexpr auto magneticValueBlock = blocks::get_value_block(extension::magneticAmplitudeTerm); constexpr auto magneticResidualBlock = blocks::get_residual_block(extension::magneticAmplitudeTerm); constexpr auto thermalValueBlock = blocks::get_value_block(extension::coreThermalMultiplierTerm); constexpr auto thermalResidualBlock = blocks::get_residual_block(extension::coreThermalMultiplierTerm); const auto &manifest = problem.GetManifest(); const auto &layout = manifest.layout(); const auto &massDescriptor = manifest.template specification(); const auto &angularDescriptor = manifest.template specification(); const auto &magneticDescriptor = manifest.template specification(); const auto &thermalDescriptor = manifest.template specification(); REQUIRE(manifest.constraints().size() == 5); CHECK(magneticDescriptor.stableId == "FixedMagneticSpecificEnergy"); CHECK(thermalDescriptor.stableId == "FixedCoreThermalBalance"); CHECK(magneticDescriptor.columnPolicy == mf::operators::RootColumnPolicy::generated_physical_coordinate); CHECK(thermalDescriptor.columnPolicy == mf::operators::RootColumnPolicy::existing_physical_multiplier); CHECK(magneticDescriptor.target == Approx(targetMagneticSpecificEnergy)); CHECK(thermalDescriptor.target == Approx(targetThermalPressure)); CHECK(thermalDescriptor.targetUnits == "pressure"); CHECK(thermalDescriptor.residualUnits == "pressure"); CHECK(magneticDescriptor.valueBlock == magneticValueBlock.index); CHECK(magneticDescriptor.residualBlock == magneticResidualBlock.index); CHECK(thermalDescriptor.valueBlock == thermalValueBlock.index); CHECK(thermalDescriptor.residualBlock == thermalResidualBlock.index); CHECK(layout.size(magneticValueBlock) == 1); CHECK(layout.size(magneticResidualBlock) == 1); CHECK(layout.size(thermalValueBlock) == 1); CHECK(layout.size(thermalResidualBlock) == 1); CHECK(massDescriptor.valueBlock != angularDescriptor.valueBlock); CHECK(massDescriptor.valueBlock != magneticDescriptor.valueBlock); CHECK(massDescriptor.valueBlock != thermalDescriptor.valueBlock); CHECK(angularDescriptor.valueBlock != magneticDescriptor.valueBlock); CHECK(angularDescriptor.valueBlock != thermalDescriptor.valueBlock); CHECK(magneticDescriptor.valueBlock != thermalDescriptor.valueBlock); CHECK(massDescriptor.residualBlock != angularDescriptor.residualBlock); CHECK(massDescriptor.residualBlock != magneticDescriptor.residualBlock); CHECK(massDescriptor.residualBlock != thermalDescriptor.residualBlock); CHECK(angularDescriptor.residualBlock != magneticDescriptor.residualBlock); CHECK(angularDescriptor.residualBlock != thermalDescriptor.residualBlock); CHECK(magneticDescriptor.residualBlock != thermalDescriptor.residualBlock); auto normalized = mf::normalization::makeNormalizedStellarEquilibriumOperator(problem); const auto scales = mf::normalization::deriveStellarCharacteristicScales( mf::dimensions::MassValue{targetMass}, mf::dimensions::LengthValue{referenceRadius}, gravitationalConstant ); CHECK( normalized.GetNormalization().StateFactors()(layout.offset(magneticValueBlock)) == Approx(1.0).epsilon(2.0e-15) ); CHECK( normalized.GetNormalization().StateFactors()(layout.offset(thermalValueBlock)) == Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15) ); CHECK( normalized.GetNormalization().ResidualFactors()(layout.offset(magneticResidualBlock)) == Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15) ); CHECK( normalized.GetNormalization().ResidualFactors()(layout.offset(thermalResidualBlock)) == Approx(1.0 / scales.pressure).epsilon(2.0e-15) ); mfem::Vector state(problem.StateSize()); state = 0.0; const auto stateView = manifest.stateView(state); stateView.block(blocks::density_field.mass_term) = 1.0; stateView.block(blocks::enthalpy_field.specific_term) = referenceEnthalpy; stateView.block(blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.25; stateView.block(blocks::fixed_angular_momentum_constraint.angular_velocity_term) = angularVelocity; stateView.generatedCoordinate() = magneticAmplitude; stateView.generatedCoordinate() = thermalMultiplier; auto dependencies = makeDependencies(); const auto preparation = problem.Prepare(state, dependencies); REQUIRE(preparation.generatedPhysicalControl); REQUIRE(preparation.template specification().generatedRotation); REQUIRE(preparation.template specification().stateChanged); REQUIRE(preparation.template specification().stateChanged); REQUIRE(problem.IsPrepared()); const auto &preparedThermal = problem.GetPreparedOperator().template GetPreparedContribution(); const double integratedMass = preparedThermal.integratedMass(); const double independentlyIntegratedMass = problem.GetPhysicalOperator().GetMassNormalizationOperator().GetCurrentMass(); CHECK(integratedMass == Approx(independentlyIntegratedMass).epsilon(3.0e-14)); mfem::Vector residual; problem.BuildResidual(residual); REQUIRE(allFinite(residual)); const auto residualView = manifest.residualView(residual); const double expectedMagneticResidual = 0.5 * magneticAmplitude * magneticAmplitude * referenceEnthalpy - targetMagneticSpecificEnergy + extension::magneticThermalFeedback * magneticAmplitude * thermalMultiplier; const double normalizedThermalMultiplier = thermalMultiplier / thermalReferenceEnthalpy; const double expectedThermalResidual = targetThermalPressure * (referenceEnthalpy / thermalReferenceEnthalpy + 0.25 * normalizedThermalMultiplier * normalizedThermalMultiplier + extension::thermalMagneticCoupling * magneticAmplitude * normalizedThermalMultiplier - 1.0 + extension::thermalMassCoupling * (integratedMass / thermalReferenceMass - 1.0)); CHECK(residualView.constraintResidual()(0) == Approx(expectedMagneticResidual).epsilon(4.0e-14)); CHECK(residualView.constraintResidual()(0) == Approx(expectedThermalResidual).epsilon(4.0e-14)); CHECK(expectedMagneticResidual != Approx(expectedThermalResidual)); const auto makeBorderDirection = [&](const double magneticVariation, const double thermalVariation) { mfem::Vector direction(problem.StateSize()); direction = 0.0; const auto view = manifest.stateView(direction); view.generatedCoordinate() = magneticVariation; view.generatedCoordinate() = thermalVariation; return direction; }; constexpr double magneticVariation = 0.31; constexpr double thermalVariation = -0.27; const mfem::Vector magneticDirection = makeBorderDirection(magneticVariation, 0.0); const mfem::Vector thermalDirection = makeBorderDirection(0.0, thermalVariation); mfem::Vector combinedDirection(magneticDirection); combinedDirection += thermalDirection; mfem::Vector magneticAction; mfem::Vector thermalAction; mfem::Vector combinedAction; problem.ApplyLinearization(magneticDirection, magneticAction); problem.ApplyLinearization(thermalDirection, thermalAction); problem.ApplyLinearization(combinedDirection, combinedAction); const auto magneticActionView = manifest.residualView(magneticAction); const auto thermalActionView = manifest.residualView(thermalAction); CHECK( magneticActionView.constraintResidual()(0) == Approx( (magneticAmplitude * referenceEnthalpy + extension::magneticThermalFeedback * thermalMultiplier) * magneticVariation ) .epsilon(4.0e-14) ); CHECK( magneticActionView.constraintResidual()(0) == Approx( extension::thermalMagneticCoupling * targetThermalPressure * normalizedThermalMultiplier * magneticVariation ) .epsilon(4.0e-14) ); CHECK( thermalActionView.constraintResidual()(0) == Approx(extension::magneticThermalFeedback * magneticAmplitude * thermalVariation).epsilon(4.0e-14) ); CHECK( thermalActionView.constraintResidual()(0) == Approx( targetThermalPressure / thermalReferenceEnthalpy * (0.5 * normalizedThermalMultiplier + extension::thermalMagneticCoupling * magneticAmplitude) * thermalVariation ) .epsilon(4.0e-14) ); const auto checkInteriorHydrostaticAction = [&](mfem::Vector &action, const double expected) { const mfem::Vector hydrostatic = manifest.residualView(action).block(blocks::enthalpy_field.specific_term); std::vector surfaceRows(static_cast(hydrostatic.Size()), false); for (const int row : problem.GetPressureSurfaceRows().reduced_dofs()) { surfaceRows[static_cast(row)] = true; CHECK(hydrostatic(row) == Approx(0.0).margin(3.0e-14)); } int interiorRows = 0; for (int row = 0; row < hydrostatic.Size(); ++row) { if (!surfaceRows[static_cast(row)]) { CHECK(hydrostatic(row) == Approx(expected).epsilon(4.0e-14)); ++interiorRows; } } REQUIRE(interiorRows > 0); }; checkInteriorHydrostaticAction(magneticAction, targetMagneticSpecificEnergy * magneticVariation); checkInteriorHydrostaticAction(thermalAction, thermalVariation); // The complete public residual provides an independent centered difference // for both generated columns at once. Because only the two nested scalar // states change, the physical core is intentionally reusable here. constexpr double differenceStep = 2.0e-7; mfem::Vector plusState(state); mfem::Vector minusState(state); plusState.Add(differenceStep, combinedDirection); minusState.Add(-differenceStep, combinedDirection); problem.Prepare(plusState, dependencies); mfem::Vector plusResidual; problem.BuildResidual(plusResidual); problem.Prepare(minusState, dependencies); mfem::Vector minusResidual; problem.BuildResidual(minusResidual); mfem::Vector difference(plusResidual); difference -= minusResidual; difference /= 2.0 * differenceStep; CHECK(relativeError(combinedAction, difference) <= 5.0e-9); problem.Prepare(state, dependencies); mfem::Vector enthalpyDirection(problem.StateSize()); enthalpyDirection = 0.0; constexpr double enthalpyVariation = 0.23; manifest.stateView(enthalpyDirection).block(blocks::enthalpy_field.specific_term)(0) = enthalpyVariation; mfem::Vector enthalpyAction; problem.ApplyLinearization(enthalpyDirection, enthalpyAction); const auto enthalpyActionView = manifest.residualView(enthalpyAction); CHECK( enthalpyActionView.block(extension::magneticAmplitudeTerm)(0) == Approx(0.5 * magneticAmplitude * magneticAmplitude * enthalpyVariation).epsilon(4.0e-14) ); CHECK( enthalpyActionView.block(extension::coreThermalMultiplierTerm)(0) == Approx(targetThermalPressure / thermalReferenceEnthalpy * enthalpyVariation).epsilon(4.0e-14) ); // The new astronomy-facing integration context evaluates a true global // finite-element functional. Its density derivative must agree with the // independently assembled fixed-mass row and with a nonlinear centered // difference, while the exact border action preserves the same coupling. mfem::Vector densityDirection(problem.StateSize()); densityDirection = 0.0; auto densityDirectionBlock = manifest.stateView(densityDirection).block(blocks::density_field.mass_term); for (int index = 0; index < densityDirectionBlock.Size(); ++index) { densityDirectionBlock(index) = 0.17 + 0.09 * std::sin(0.37 * static_cast(index + 1)); } mfem::Vector densityAction; problem.ApplyLinearization(densityDirection, densityAction); const auto densityActionView = manifest.residualView(densityAction); const double densityMassAction = densityActionView.block(blocks::fixed_total_mass_constraint.mass_normalization_term)(0); const double expectedThermalDensityAction = extension::thermalMassCoupling * targetThermalPressure / thermalReferenceMass * densityMassAction; CHECK( densityActionView.constraintResidual()(0) == Approx(expectedThermalDensityAction).epsilon(8.0e-13) ); CHECK(densityActionView.constraintResidual()(0) == Approx(0.0).margin(2.0e-14)); // This row is exactly linear in density. Use a deliberately macroscopic // perturbation so that subtracting two O(1) residuals does not bury the // O(step) signal in roundoff; there is no truncation-error tradeoff here. constexpr double densityDifferenceStep = 1.0e-4; mfem::Vector plusDensityState(state); mfem::Vector minusDensityState(state); plusDensityState.Add(densityDifferenceStep, densityDirection); minusDensityState.Add(-densityDifferenceStep, densityDirection); auto plusDensityDependencies = dependencies; plusDensityDependencies.density.revision = 2; problem.Prepare(plusDensityState, plusDensityDependencies); mfem::Vector plusDensityResidual; problem.BuildResidual(plusDensityResidual); auto minusDensityDependencies = plusDensityDependencies; minusDensityDependencies.density.revision = 3; problem.Prepare(minusDensityState, minusDensityDependencies); mfem::Vector minusDensityResidual; problem.BuildResidual(minusDensityResidual); const double finiteDifferenceThermalDensity = (manifest.residualView(plusDensityResidual).constraintResidual()(0) - manifest.residualView(minusDensityResidual).constraintResidual()(0)) / (2.0 * densityDifferenceStep); CHECK(finiteDifferenceThermalDensity == Approx(expectedThermalDensityAction).epsilon(2.0e-8)); dependencies = minusDensityDependencies; dependencies.density.revision = 4; problem.Prepare(state, dependencies); // Physical-volume integration also depends on the deformed stellar // domain. The restricted context maps a surface-shape direction through // the current deformation and differentiates the same global integral. mfem::Vector surfaceDirection(problem.StateSize()); surfaceDirection = 0.0; auto surfaceDirectionBlock = manifest.stateView(surfaceDirection).block(blocks::surface_deformation_field.parameters_term); for (int index = 0; index < surfaceDirectionBlock.Size(); ++index) { surfaceDirectionBlock(index) = 0.021 * std::cos(0.29 * static_cast(index + 1)); } mfem::Vector surfaceAction; problem.ApplyLinearization(surfaceDirection, surfaceAction); const auto surfaceActionView = manifest.residualView(surfaceAction); const double surfaceMassAction = surfaceActionView.block(blocks::fixed_total_mass_constraint.mass_normalization_term)(0); CHECK( surfaceActionView.constraintResidual()(0) == Approx(extension::thermalMassCoupling * targetThermalPressure / thermalReferenceMass * surfaceMassAction) .epsilon(8.0e-13) ); CHECK(surfaceActionView.constraintResidual()(0) == Approx(0.0).margin(2.0e-14)); // Unlike the density column, this column differentiates the nonlinear // domain deformation. Check the complete public residual independently // so the test exercises the surface-to-displacement map, physical-volume // integration, and typed constraint row as one composed operation. constexpr double surfaceDifferenceStep = 1.0e-3; mfem::Vector plusSurfaceState(state); mfem::Vector minusSurfaceState(state); plusSurfaceState.Add(surfaceDifferenceStep, surfaceDirection); minusSurfaceState.Add(-surfaceDifferenceStep, surfaceDirection); auto plusSurfaceDependencies = dependencies; ++plusSurfaceDependencies.surfaceDeformation.revision; problem.Prepare(plusSurfaceState, plusSurfaceDependencies); mfem::Vector plusSurfaceResidual; problem.BuildResidual(plusSurfaceResidual); auto minusSurfaceDependencies = plusSurfaceDependencies; ++minusSurfaceDependencies.surfaceDeformation.revision; problem.Prepare(minusSurfaceState, minusSurfaceDependencies); mfem::Vector minusSurfaceResidual; problem.BuildResidual(minusSurfaceResidual); const double finiteDifferenceThermalSurface = (manifest.residualView(plusSurfaceResidual).constraintResidual()(0) - manifest.residualView(minusSurfaceResidual).constraintResidual()(0)) / (2.0 * surfaceDifferenceStep); CHECK( finiteDifferenceThermalSurface == Approx(surfaceActionView.constraintResidual()(0)).epsilon(3.0e-7) ); dependencies = minusSurfaceDependencies; ++dependencies.surfaceDeformation.revision; problem.Prepare(state, dependencies); mfem::Vector normalizedState; normalized.NormalizeState(state, normalizedState); const auto normalizedPreparation = normalized.Prepare(normalizedState, dependencies); REQUIRE(normalizedPreparation.generatedPhysicalControl); REQUIRE(normalizedPreparation.template specification().stateChanged == false); REQUIRE(normalizedPreparation.template specification().stateChanged == false); REQUIRE(normalized.IsPrepared()); mfem::Vector expectedNormalizedResidual; mfem::Vector actualNormalizedResidual; normalized.NormalizeResidual(residual, expectedNormalizedResidual); normalized.BuildResidual(actualNormalizedResidual); CHECK(relativeError(actualNormalizedResidual, expectedNormalizedResidual) <= 5.0e-13); const auto normalizedResidualView = manifest.residualView(actualNormalizedResidual); CHECK( normalizedResidualView.block(extension::magneticAmplitudeTerm)(0) == Approx(expectedMagneticResidual / scales.specificEnergy).epsilon(4.0e-14) ); CHECK( normalizedResidualView.block(extension::coreThermalMultiplierTerm)(0) == Approx(expectedThermalResidual / scales.pressure).epsilon(4.0e-14) ); mfem::Vector normalizedDirection; normalized.NormalizeState(combinedDirection, normalizedDirection); mfem::Vector physicalCombinedAction; mfem::Vector expectedNormalizedAction; mfem::Vector actualNormalizedAction; problem.ApplyLinearization(combinedDirection, physicalCombinedAction); normalized.NormalizeResidual(physicalCombinedAction, expectedNormalizedAction); normalized.Mult(normalizedDirection, actualNormalizedAction); CHECK(relativeError(actualNormalizedAction, expectedNormalizedAction) <= 5.0e-13); auto component = mf::preconditioning::makePreconditioner(problem); using Component = std::remove_cvref_t; STATIC_CHECK(Component::borderValueArity == 4); STATIC_CHECK(Component::borderResidualArity == 4); STATIC_CHECK( mf::preconditioning::CompletePreconditionerFor< mf::preconditioning::PreconditionerPlan, typename Problem::FormType> ); STATIC_CHECK( mf::preconditioning::CompatiblePreconditionerFor< mf::preconditioning::PreconditionerPlan, typename Problem::FormType, typename Problem::JacobianFormType> ); auto physicalInverse = mf::preconditioning::prepare(problem, component); REQUIRE(physicalInverse.IsCurrent()); const auto &coordinateMap = physicalInverse.GetCoordinateMap(); const auto inferredBorderAction = [&](const mfem::Vector &rootDirection) { mfem::Vector groupedDirection(coordinateMap.PreconditionerCorrectionSize()); mfem::Vector groupedAction(coordinateMap.PreconditionerResidualSize()); mfem::Vector rootAction(problem.EquationSize()); coordinateMap.PackCorrection(rootDirection, groupedDirection); physicalInverse.GetGroupedPreconditioner().GetCouplings().Mult(groupedDirection, groupedAction); coordinateMap.UnpackResidual(groupedAction, rootAction); return rootAction; }; const mfem::Vector inferredMagneticAction = inferredBorderAction(magneticDirection); const mfem::Vector inferredThermalAction = inferredBorderAction(thermalDirection); CHECK(relativeError(inferredMagneticAction, magneticAction) <= 5.0e-13); CHECK(relativeError(inferredThermalAction, thermalAction) <= 5.0e-13); mfem::Vector inferredEnthalpyBorderAction = inferredBorderAction(enthalpyDirection); const auto inferredEnthalpyView = manifest.residualView(inferredEnthalpyBorderAction); CHECK( inferredEnthalpyView.block(extension::magneticAmplitudeTerm)(0) == Approx(enthalpyActionView.block(extension::magneticAmplitudeTerm)(0)).epsilon(4.0e-13) ); CHECK( inferredEnthalpyView.block(extension::coreThermalMultiplierTerm)(0) == Approx(enthalpyActionView.block(extension::coreThermalMultiplierTerm)(0)).epsilon(4.0e-13) ); mfem::Vector inferredDensityBorderAction = inferredBorderAction(densityDirection); const auto inferredDensityView = manifest.residualView(inferredDensityBorderAction); CHECK( inferredDensityView.constraintResidual()(0) == Approx(densityActionView.constraintResidual()(0)).epsilon(8.0e-13) ); CHECK(inferredDensityView.constraintResidual()(0) == Approx(0.0).margin(2.0e-14)); mfem::Vector inferredSurfaceBorderAction = inferredBorderAction(surfaceDirection); const auto inferredSurfaceView = manifest.residualView(inferredSurfaceBorderAction); CHECK( inferredSurfaceView.constraintResidual()(0) == Approx(surfaceActionView.constraintResidual()(0)).epsilon(8.0e-13) ); CHECK(inferredSurfaceView.constraintResidual()(0) == Approx(0.0).margin(2.0e-14)); auto scaledInverse = normalized.MakeScaledPreconditioner(physicalInverse); REQUIRE(scaledInverse.IsCurrent()); mfem::Vector normalizedRightHandSide(problem.EquationSize()); for (int index = 0; index < normalizedRightHandSide.Size(); ++index) { normalizedRightHandSide(index) = 0.13 * std::sin(0.031 * static_cast(index + 1)) + 0.04 * std::cos(0.019 * static_cast(index + 1)); } auto rightHandSideView = manifest.residualView(normalizedRightHandSide); rightHandSideView.block(extension::magneticAmplitudeTerm) = 0.37; rightHandSideView.block(extension::coreThermalMultiplierTerm) = -0.29; mfem::Vector firstCorrection(problem.StateSize()); mfem::Vector repeatedCorrection(problem.StateSize()); scaledInverse.Mult(normalizedRightHandSide, firstCorrection); scaledInverse.Mult(normalizedRightHandSide, repeatedCorrection); REQUIRE(allFinite(firstCorrection)); CHECK(relativeError(firstCorrection, repeatedCorrection) <= 3.0e-14); const auto correctionView = manifest.stateView(static_cast(firstCorrection)); const double magneticCorrection = correctionView.block(extension::magneticAmplitudeTerm)(0); const double thermalCorrection = correctionView.block(extension::coreThermalMultiplierTerm)(0); CAPTURE(magneticCorrection, thermalCorrection); CHECK(std::abs(magneticCorrection) > 1.0e-16); CHECK(std::abs(thermalCorrection) > 1.0e-16); // Change coefficients in both nested slots while deliberately reusing the // same dependency stamps. Both cached actions must become stale and both // must be reconstructed by one variadic refresh. constexpr double changedEnthalpy = 2.4; constexpr double changedMagneticAmplitude = 1.1; constexpr double changedThermalMultiplier = 0.5; mfem::Vector changedState(state); const auto changedStateView = manifest.stateView(changedState); changedStateView.block(blocks::enthalpy_field.specific_term) = changedEnthalpy; changedStateView.block(extension::magneticAmplitudeTerm) = changedMagneticAmplitude; changedStateView.block(extension::coreThermalMultiplierTerm) = changedThermalMultiplier; mfem::Vector changedNormalizedState; normalized.NormalizeState(changedState, changedNormalizedState); const auto changedPreparation = normalized.Prepare(changedNormalizedState, dependencies); CHECK(changedPreparation.template specification().stateChanged); CHECK(changedPreparation.template specification().stateChanged); CHECK_FALSE(physicalInverse.IsCurrent()); CHECK_FALSE(scaledInverse.IsCurrent()); mfem::Vector changedResidual; problem.BuildResidual(changedResidual); const auto changedResidualView = manifest.residualView(changedResidual); const double changedExpectedMagneticResidual = 0.5 * changedMagneticAmplitude * changedMagneticAmplitude * changedEnthalpy - targetMagneticSpecificEnergy + extension::magneticThermalFeedback * changedMagneticAmplitude * changedThermalMultiplier; const double changedNormalizedThermalMultiplier = changedThermalMultiplier / thermalReferenceEnthalpy; const double changedExpectedThermalResidual = targetThermalPressure * (changedEnthalpy / thermalReferenceEnthalpy + 0.25 * changedNormalizedThermalMultiplier * changedNormalizedThermalMultiplier + extension::thermalMagneticCoupling * changedMagneticAmplitude * changedNormalizedThermalMultiplier - 1.0 + extension::thermalMassCoupling * (integratedMass / thermalReferenceMass - 1.0)); CHECK( changedResidualView.block(extension::magneticAmplitudeTerm)(0) == Approx(changedExpectedMagneticResidual).epsilon(4.0e-14) ); CHECK( changedResidualView.block(extension::coreThermalMultiplierTerm)(0) == Approx(changedExpectedThermalResidual).epsilon(4.0e-14) ); mfem::Vector changedMagneticAction; mfem::Vector changedThermalAction; problem.ApplyLinearization(magneticDirection, changedMagneticAction); problem.ApplyLinearization(thermalDirection, changedThermalAction); mfem::Vector magneticActionChange(changedMagneticAction); mfem::Vector thermalActionChange(changedThermalAction); magneticActionChange -= magneticAction; thermalActionChange -= thermalAction; CHECK(magneticActionChange.Norml2() > 1.0e-8); CHECK(thermalActionChange.Norml2() > 1.0e-8); const auto refresh = physicalInverse.Refresh(); CHECK(refresh.specificationActionsRefreshed); CHECK(refresh.rebuiltSchurComplement); CHECK(refresh.DidAnyWork()); REQUIRE(physicalInverse.IsCurrent()); REQUIRE(scaledInverse.IsCurrent()); const mfem::Vector refreshedMagneticAction = inferredBorderAction(magneticDirection); const mfem::Vector refreshedThermalAction = inferredBorderAction(thermalDirection); CHECK(relativeError(refreshedMagneticAction, changedMagneticAction) <= 5.0e-13); CHECK(relativeError(refreshedThermalAction, changedThermalAction) <= 5.0e-13); const auto noOpRefresh = physicalInverse.Refresh(); CHECK_FALSE(noOpRefresh.DidAnyWork()); mfem::Vector refreshedCorrection(problem.StateSize()); scaledInverse.Mult(normalizedRightHandSide, refreshedCorrection); REQUIRE(allFinite(refreshedCorrection)); }