#include #include #include #include #include #include #include #include #include #include #include import mean_field; import test_helpers; namespace specification_border_test { namespace blocks = mean_field::utils::blocks; namespace preconditioning = mean_field::preconditioning; class PhysicsFacingBorderConstraint; class PreparedPhysicsFacingBorderConstraint; struct PhysicsFacingBorderTerm final { using value = blocks::generated_value_block>; using residual = blocks::generated_residual_block>; }; inline constexpr PhysicsFacingBorderTerm physicsFacingBorderTerm{}; class MockPhysicsBorderAction final { public: explicit MockPhysicsBorderAction(const PreparedPhysicsFacingBorderConstraint &prepared) noexcept; template < typename Direction, typename Row> [[nodiscard]] auto ApplyJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::OwnConstraint, mean_field::stellar::state::SpecificEnthalpy>, const Direction &direction, Row &row ) const { return row.add(m_phaseDerivative * direction.specificEnthalpy()(0)); } template < typename Direction, typename Row> [[nodiscard]] auto ApplyJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::HydrostaticBalance, mean_field::stellar::state::OwnGeneratedCoordinate>, const Direction &direction, Row &row ) const { return row.add(m_coefficient * direction.generatedCoordinate()(0)); } private: double m_coefficient; double m_phaseDerivative; }; class PhysicsFacingBorderConstraint final { public: struct Parameters final { mean_field::dimensions::SpecificEnthalpyValue target; }; using TargetValue = mean_field::dimensions::SpecificEnthalpyValue; using ScalarDescription = mean_field::stellar::ScalarConstraint< mean_field::dimensions::quantity::SpecificEnthalpy, mean_field::dimensions::quantity::SpecificEnthalpy, mean_field::dimensions::quantity::SpecificEnthalpy, "test.phase.border", "lambda_test", "test.phase.residual", "R_test">; using ModelDefinition = mean_field::constraint::ScalarPhaseCondition< PhysicsFacingBorderConstraint, "PhysicsFacingBorderConstraint", mean_field::stellar::Reads, mean_field::stellar::Changes, ScalarDescription>; using SpecificationBorderPhysics = preconditioning::LocalSpecificationBorderPhysics; using EquilibriumPhysics = mean_field::operators::LocalSpecificationEquilibriumPhysics; explicit constexpr PhysicsFacingBorderConstraint(const Parameters parameters) noexcept : m_target(parameters.target) { } [[nodiscard]] constexpr TargetValue target() const noexcept { return m_target; } private: TargetValue m_target; }; struct PhysicsFacingPreparationReport final { }; class PreparedPhysicsFacingBorderConstraint final { public: using Report = PhysicsFacingPreparationReport; explicit PreparedPhysicsFacingBorderConstraint(const PhysicsFacingBorderConstraint &specification) noexcept : m_target(specification.target().value()) { } template [[nodiscard]] Report PrepareAfterPhysical(const StateView &state) { const auto enthalpy = state.specificEnthalpy(); const auto border = state.generatedCoordinate(); m_referenceEnthalpy = enthalpy(0); m_border = border(0); m_coefficient = coefficientFor(m_referenceEnthalpy); m_phaseDerivative = m_coefficient + coefficientDerivative * (m_referenceEnthalpy - m_target); m_phaseResidual = m_coefficient * (enthalpy(0) - m_target); m_borderValue = m_coefficient * border(0); m_isPrepared = true; return {}; } template [[nodiscard]] auto AddResidual( mean_field::stellar::equation::OwnConstraint, Row &row ) const { return row.add(m_phaseResidual); } template [[nodiscard]] auto AddResidual( mean_field::stellar::equation::HydrostaticBalance, Row &row ) const { return row.add(m_borderValue); } 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_phaseDerivative * direction.specificEnthalpy()(0)); } template < typename Direction, typename Row> [[nodiscard]] auto AddJacobianAction( mean_field::stellar::Derivative< mean_field::stellar::equation::HydrostaticBalance, mean_field::stellar::state::SpecificEnthalpy>, const Direction &direction, Row &row ) const { return row.add(coefficientDerivative * m_border * direction.specificEnthalpy()(0)); } 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(m_coefficient * direction.generatedCoordinate()(0)); } [[nodiscard]] bool IsPrepared() const noexcept { return m_isPrepared; } [[nodiscard]] double coefficient() const noexcept { return m_coefficient; } [[nodiscard]] double phaseDerivative() const noexcept { return m_phaseDerivative; } private: [[nodiscard]] static constexpr double coefficientFor(const double referenceEnthalpy) noexcept { return 1.0 + coefficientDerivative * referenceEnthalpy; } static constexpr double coefficientDerivative = 0.125; double m_target{0.0}; double m_referenceEnthalpy{0.0}; double m_border{0.0}; double m_coefficient{1.0}; double m_phaseDerivative{1.0}; double m_phaseResidual{0.0}; double m_borderValue{0.0}; bool m_isPrepared{false}; }; inline MockPhysicsBorderAction::MockPhysicsBorderAction( const PreparedPhysicsFacingBorderConstraint &prepared ) noexcept : m_coefficient(prepared.coefficient()), m_phaseDerivative(prepared.phaseDerivative()) { } /* This distinct problem type lets the test provide a trusted backend * adapter in addition to the constraint's nested physics package without * changing the ordinary PhysicsFacingProblem exercised below. */ using DualProviderModel = mean_field::model::StellarModel>; using DualProviderProblem = mean_field::equilibrium::StellarEquilibriumProblem; } // namespace specification_border_test /* Simulate a trusted library backend being added for a self-describing * constraint that already supplies its astronomer-facing nested package. */ namespace mean_field::preconditioning::detail { template <> class PreparedSpecificationBorderAction< specification_border_test::PhysicsFacingBorderConstraint, specification_border_test::DualProviderProblem> final { public: static constexpr bool registered = true; explicit PreparedSpecificationBorderAction(const specification_border_test::DualProviderProblem &) noexcept { } void ApplyStructureToBorder( const StellarStructureDirectionView &, mfem::Vector & ) const noexcept { } void ApplyBorderToStructure( const mfem::Vector &, StellarStructureActionView ) const noexcept { } void ApplyBorderToBorder( const mfem::Vector &, mfem::Vector & ) const noexcept { } }; /* Adversarially claim that the built-in stellar-structure backend handles * this extension's nonzero core-to-core edge. The Polytrope core does not * authorize the specification, so the public topology audit must ignore * this specialization and continue to reject the default structure PC. */ template <> struct StellarStructureBackendHandledCouplings< operators::PreparedStellarEquilibriumOperator, specification_border_test::PhysicsFacingBorderConstraint> { using Type = utils::blocks::type_list>; }; } // namespace mean_field::preconditioning::detail namespace { namespace backend = mean_field::preconditioning::backend; namespace blocks = mean_field::utils::blocks; namespace preconditioning = mean_field::preconditioning; template using ModelWith = mean_field::model::StellarModel>; using PhysicsFacingBorderConstraint = specification_border_test::PhysicsFacingBorderConstraint; using MockPhysicsBorderAction = specification_border_test::MockPhysicsBorderAction; class IncompletePhysicsFacingBorderConstraint final { public: struct Parameters final { }; using ModelDefinition = mean_field::constraint::PhaseCondition< IncompletePhysicsFacingBorderConstraint, "IncompletePhysicsFacingBorderConstraint", mean_field::models::DependsOn, mean_field::models::Affects>; struct SpecificationBorderPhysics final { static constexpr bool registered = true; }; explicit constexpr IncompletePhysicsFacingBorderConstraint(Parameters) noexcept { } }; using BaseModel = ModelWith; using CentralModel = ModelWith< mean_field::eos::Polytrope, mean_field::surface::Isobaric, mean_field::models::FixedTotalMass, mean_field::models::FixedCentralDensity>; using AngularModel = ModelWith< mean_field::eos::Polytrope, mean_field::surface::Isobaric, mean_field::models::FixedTotalMass, mean_field::models::FixedAngularMomentum>; using AngularCentralModel = mean_field::model::StellarModel>; using PhysicsFacingModel = ModelWith< mean_field::eos::Polytrope, mean_field::surface::Isobaric, mean_field::models::FixedTotalMass, PhysicsFacingBorderConstraint>; using ReorderedCentralModel = mean_field::model::StellarModel>; using BaseProblem = mean_field::equilibrium::StellarEquilibriumProblem; using CentralProblem = mean_field::equilibrium::StellarEquilibriumProblem; using AngularProblem = mean_field::equilibrium::StellarEquilibriumProblem; using AngularCentralProblem = mean_field::equilibrium::StellarEquilibriumProblem; using PhysicsFacingProblem = mean_field::equilibrium::StellarEquilibriumProblem; using DualProviderProblem = specification_border_test::DualProviderProblem; using BaseBorder = preconditioning::CompiledSpecificationBorderFor; using CentralBorder = preconditioning::CompiledSpecificationBorderFor; using AngularBorder = preconditioning::CompiledSpecificationBorderFor; using AngularCentralBorder = preconditioning::CompiledSpecificationBorderFor; using BaseComponent = decltype(preconditioning::specificationBorderBlock(std::declval())); using BaseCouplingOperator = preconditioning::SpecificationBorderJacobianOperator; using PreparedBaseBorder = preconditioning::PreparedSpecificationBorderBlock; using CentralComponent = decltype(preconditioning::specificationBorderBlock(std::declval())); using AngularComponent = decltype(preconditioning::specificationBorderBlock(std::declval())); using AngularCentralComponent = decltype(preconditioning::specificationBorderBlock(std::declval())); using UnsupportedStructureComponent = preconditioning::IdentityBlock; using UnsupportedBorderComponent = preconditioning::SpecificationBorderBlock< UnsupportedStructureComponent, BaseModel, typename BaseProblem::FormType, typename BaseProblem::JacobianFormType>; using BasePlan = preconditioning::PreconditionerPlan; using CentralPlan = preconditioning::PreconditionerPlan; using AngularPlan = preconditioning::PreconditionerPlan; using AngularCentralPlan = preconditioning::PreconditionerPlan; using PhysicsFacingRieszDiscretization = mean_field::equilibrium::StellarDiscretizationFor>; using PhysicsStructureToBorderAction = preconditioning::SpecificationStructureToBorderActionView; using PhysicsBorderToStructureAction = preconditioning::SpecificationBorderToStructureActionView; using PhysicsBorderToBorderAction = preconditioning::SpecificationBorderToBorderActionView; using MassStructureToBorderAction = preconditioning::SpecificationStructureToBorderActionView; template concept HasDensityBlock = requires(const View &view) { view.density(); }; template concept HasSpecificEnthalpyBlock = requires(const View &view) { view.specificEnthalpy(); }; template concept HasGeneratedCoordinateBlock = requires(const View &view) { view.generatedCoordinate(); }; template concept HasConstraintResidualBlock = requires(const View &view) { view.constraintResidual(); }; struct AddBoundContribution final { template < typename Direction, typename Row> requires requires( const Direction &direction, Row &row ) { direction.size(); row.add(1.0); } [[nodiscard]] auto operator()( const Direction &, Row &row ) const { return row.add(1.0); } }; struct AddBoundVectorContribution final { template < typename Direction, typename Row> requires requires( const Direction &direction, Row &row, const mfem::Vector &contribution ) { direction.values(); row.add(contribution); } [[nodiscard]] auto operator()( const Direction &direction, Row &row ) const { mfem::Vector contribution(direction.Size()); contribution = 0.0; return row.add(contribution); } }; 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 ReadSurfaceShapeAndAdd final { template < typename Direction, typename Row> requires requires( const Direction &direction, Row &row ) { direction.surfaceShape(); 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)); } }; struct MutateSpecificEnthalpyAndAdd final { template < typename Direction, typename Row> requires requires( Direction &direction, Row &row ) { direction.specificEnthalpy()(0) = 1.0; row.add(1.0); } [[nodiscard]] auto operator()( Direction &, Row &row ) const { return row.add(1.0); } }; struct AccessNamedHydrostaticRow final { template < typename Direction, typename Row> requires requires( const Direction &, const Row &row ) { row.specificEnthalpy(); } void operator()( const Direction &, const Row & ) const noexcept { } }; template concept CanAddSpecificEnthalpyFromGenerated = requires(const View &view) { view.addSpecificEnthalpyFrom( specification_border_test::physicsFacingBorderTerm, 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( specification_border_test::physicsFacingBorderTerm, ReadGeneratedCoordinateAndAdd{} ); }; template concept CanClaimEnthalpyButReadDensity = requires(const View &view) { view.addConstraintResidualFrom(blocks::enthalpy_field.specific_term, ReadDensityAndAdd{}); }; template concept CanMutateBoundEnthalpySource = requires(const View &view) { view.addConstraintResidualFrom(blocks::enthalpy_field.specific_term, MutateSpecificEnthalpyAndAdd{}); }; template concept CanAccessNamedRowInsideCallback = requires(const View &view) { view.addConstraintResidualFrom(blocks::enthalpy_field.specific_term, AccessNamedHydrostaticRow{}); }; template concept CanAddMassResidualFromSurface = requires(const View &view) { view.addConstraintResidualFrom(blocks::surface_deformation_field.parameters_term, ReadSurfaceShapeAndAdd{}); }; template concept CanAddVectorMassResidualFromSurface = requires(const View &view) { view.addConstraintResidualFrom(blocks::surface_deformation_field.parameters_term, AddBoundVectorContribution{}); }; template concept CanClaimDensityButReadSurface = requires(const View &view) { view.addConstraintResidualFrom(blocks::density_field.mass_term, ReadSurfaceShapeAndAdd{}); }; template concept ExposesUnrestrictedVector = requires(const View &view) { view.vector(); }; template concept CanPrepareSpecificationBorder = requires(const Problem &problem, Block block) { preconditioning::prepare(problem, std::move(block)); }; template concept CanPrepareSpecificationBorderFromTemporary = requires(Block block) { preconditioning::prepare(std::declval(), std::move(block)); }; template concept CanMakeDefaultStellarPreconditioner = requires(const Problem &problem) { preconditioning::makePreconditioner(problem); }; /* This deliberately implements the old unrestricted provider protocol. * It would be structurally usable, but must not be accepted as a * third-party escape hatch around the declared-coupling views. */ struct UnsafeRawBorderProvider final { static constexpr bool registered = true; template class Prepared final { public: static constexpr bool registered = true; explicit Prepared(const Problem &) noexcept { } void ApplyStructureToBorder( const preconditioning::StellarStructureDirectionView &, mfem::Vector & ) const noexcept { } void ApplyBorderToStructure( const mfem::Vector &, preconditioning::StellarStructureActionView ) const noexcept { } void ApplyBorderToBorder( const mfem::Vector &, mfem::Vector & ) const noexcept { } }; template < mean_field::models::ModelSpecification Specification, typename Problem> [[nodiscard]] static Prepared< Specification, std::remove_cvref_t> prepare(const Problem &problem) { return Prepared>{problem}; } }; template class NonMovablePhysicsBorderAction final { public: explicit NonMovablePhysicsBorderAction(const Problem &) noexcept { } NonMovablePhysicsBorderAction(const NonMovablePhysicsBorderAction &) = delete; NonMovablePhysicsBorderAction(NonMovablePhysicsBorderAction &&) = delete; void ApplyStructureToBorder( preconditioning::SpecificationStructureToBorderActionView< PhysicsFacingBorderConstraint, Problem> ) const noexcept { } void ApplyBorderToStructure( preconditioning::SpecificationBorderToStructureActionView< PhysicsFacingBorderConstraint, Problem> ) const noexcept { } void ApplyBorderToBorder( preconditioning::SpecificationBorderToBorderActionView< PhysicsFacingBorderConstraint, Problem> ) const noexcept { } }; /* Complete old-style action whose only defect is accepting the enclosing * Problem. The physics-facing wrapper must reject it even though every * numerical hook is otherwise valid. */ template class ProblemOnlyPhysicsBorderAction final { public: explicit ProblemOnlyPhysicsBorderAction(const Problem &) noexcept { } void ApplyStructureToBorder( preconditioning::SpecificationStructureToBorderActionView< PhysicsFacingBorderConstraint, Problem> ) const noexcept { } void ApplyBorderToStructure( preconditioning::SpecificationBorderToStructureActionView< PhysicsFacingBorderConstraint, Problem> ) const noexcept { } void ApplyBorderToBorder( preconditioning::SpecificationBorderToBorderActionView< PhysicsFacingBorderConstraint, Problem> ) const noexcept { } }; class CompleteMockBorderAction final { public: static constexpr bool registered = true; explicit CompleteMockBorderAction(const BaseProblem &) noexcept { } void ApplyStructureToBorder( const preconditioning::StellarStructureDirectionView &, mfem::Vector & ) const noexcept { } void ApplyBorderToStructure( const mfem::Vector &, preconditioning::StellarStructureActionView ) const noexcept { } void ApplyBorderToBorder( const mfem::Vector &, mfem::Vector & ) const noexcept { } }; class RegisteredButIncompleteBorderAction final { public: static constexpr bool registered = true; explicit RegisteredButIncompleteBorderAction(const BaseProblem &) noexcept { } }; template class IncompleteMockPhysicsBorderAction final { public: explicit IncompleteMockPhysicsBorderAction(const Problem &) noexcept { } }; class KnownBorderCouplings final { public: explicit KnownBorderCouplings(const int borderSize) : m_borderSize(borderSize), m_structureToBorder( borderSize, StructureSize() ), m_borderToStructure( StructureSize(), borderSize ), m_borderDiagonal(borderSize) { if (borderSize <= 0) { throw std::invalid_argument("The known border must have positive size."); } for (int row = 0; row < borderSize; ++row) { for (int column = 0; column < StructureSize(); ++column) { m_structureToBorder(row, column) = 0.04 * static_cast((row + 1) * (column + 2)); m_borderToStructure(column, row) = -0.03 * static_cast((column + 1) * (row + 2)); } for (int column = 0; column < borderSize; ++column) { m_borderDiagonal(row, column) = row == column ? 2.0 + static_cast(row) : 0.01 * static_cast(row + column + 1); } } } [[nodiscard]] static constexpr int StructureSize() noexcept { return 3; } [[nodiscard]] int BorderSize() const noexcept { return m_borderSize; } void ApplyStructureToBorder( const mfem::Vector &direction, mfem::Vector &action ) const { m_structureToBorder.Mult(direction, action); } void ApplyBorderToStructure( const mfem::Vector &direction, mfem::Vector &action ) const { m_borderToStructure.Mult(direction, action); } void ApplyBorderToBorder( const mfem::Vector &direction, mfem::Vector &action ) const { m_borderDiagonal.Mult(direction, action); } void IncreaseBorderDiagonal(const double increment) { for (int index = 0; index < m_borderSize; ++index) { m_borderDiagonal(index, index) += increment; } } [[nodiscard]] const mfem::DenseMatrix &StructureToBorder() const noexcept { return m_structureToBorder; } [[nodiscard]] const mfem::DenseMatrix &BorderToStructure() const noexcept { return m_borderToStructure; } [[nodiscard]] const mfem::DenseMatrix &BorderDiagonal() const noexcept { return m_borderDiagonal; } private: int m_borderSize; mfem::DenseMatrix m_structureToBorder; mfem::DenseMatrix m_borderToStructure; mfem::DenseMatrix m_borderDiagonal; }; [[nodiscard]] double relativeError( const mfem::Vector &left, const mfem::Vector &right ) { mfem::Vector difference(left); difference -= right; return difference.Norml2() / std::max({1.0, left.Norml2(), right.Norml2()}); } template < preconditioning::ApplicationContract StructureInverseContract = preconditioning::ApplicationContract::stationary_linear> void verifyKnownBorderFactorization(const int borderSize) { mfem::Vector structureDiagonal(KnownBorderCouplings::StructureSize()); structureDiagonal(0) = 2.0; structureDiagonal(1) = 3.0; structureDiagonal(2) = 5.0; auto structureInverse = backend::prepare(backend::Diagonal{}, structureDiagonal); KnownBorderCouplings couplings(borderSize); using Factorization = preconditioning::SpecificationBorderFactorizationOperator; Factorization factorization(structureInverse, couplings); constexpr bool cachesStructureResponse = Factorization::cachesStructureInverseBorderCoupling; const auto expectedSchurEntry = [&](const int row, const int column) { double correction = 0.0; for (int inner = 0; inner < KnownBorderCouplings::StructureSize(); ++inner) { correction += couplings.StructureToBorder()(row, inner) * couplings.BorderToStructure()(inner, column) / structureDiagonal(inner); } return couplings.BorderDiagonal()(row, column) - correction; }; for (int row = 0; row < borderSize; ++row) { for (int column = 0; column < borderSize; ++column) { CHECK( factorization.GetSchurComplement()(row, column) == Catch::Approx(expectedSchurEntry(row, column)).margin(2.0e-14) ); } } const int completeSize = KnownBorderCouplings::StructureSize() + borderSize; mfem::DenseMatrix completeMatrix(completeSize); completeMatrix = 0.0; for (int index = 0; index < KnownBorderCouplings::StructureSize(); ++index) { completeMatrix(index, index) = structureDiagonal(index); } for (int row = 0; row < KnownBorderCouplings::StructureSize(); ++row) { for (int column = 0; column < borderSize; ++column) { completeMatrix(row, KnownBorderCouplings::StructureSize() + column) = couplings.BorderToStructure()(row, column); completeMatrix(KnownBorderCouplings::StructureSize() + column, row) = couplings.StructureToBorder()(column, row); } } for (int row = 0; row < borderSize; ++row) { for (int column = 0; column < borderSize; ++column) { completeMatrix( KnownBorderCouplings::StructureSize() + row, KnownBorderCouplings::StructureSize() + column ) = couplings.BorderDiagonal()(row, column); } } mfem::Vector rightHandSide(completeSize); for (int index = 0; index < completeSize; ++index) { rightHandSide(index) = 0.25 + 0.17 * static_cast(index + 1); } mfem::Vector actual(completeSize); mfem::Vector expected(completeSize); factorization.Mult(rightHandSide, actual); mfem::DenseMatrixInverse exactInverse(completeMatrix); exactInverse.Mult(rightHandSide, expected); CHECK(relativeError(actual, expected) <= 2.0e-13); const auto statisticsBeforeRefresh = factorization.GetStatistics(); CHECK(statisticsBeforeRefresh.setups == 1); CHECK(statisticsBeforeRefresh.schurProbes == static_cast(borderSize)); CHECK(statisticsBeforeRefresh.applications == 1); CHECK( statisticsBeforeRefresh.structureInverseApplications == static_cast(borderSize + (cachesStructureResponse ? 1 : 2)) ); CHECK( statisticsBeforeRefresh.cachedStructureInverseBorderApplications == static_cast(cachesStructureResponse ? 1 : 0) ); CHECK(statisticsBeforeRefresh.structureToBorderApplications == static_cast(borderSize + 1)); CHECK( statisticsBeforeRefresh.borderToStructureApplications == static_cast(borderSize + (cachesStructureResponse ? 0 : 1)) ); CHECK(statisticsBeforeRefresh.borderToBorderApplications == static_cast(borderSize)); CHECK( structureInverse.GetStatistics().applications == static_cast(borderSize + (cachesStructureResponse ? 1 : 2)) ); for (int index = 0; index < KnownBorderCouplings::StructureSize(); ++index) { structureDiagonal(index) += 0.25 * static_cast(index + 1); completeMatrix(index, index) = structureDiagonal(index); } structureInverse.Refresh(structureDiagonal); couplings.IncreaseBorderDiagonal(0.5); for (int index = 0; index < borderSize; ++index) { completeMatrix( KnownBorderCouplings::StructureSize() + index, KnownBorderCouplings::StructureSize() + index ) += 0.5; } factorization.RefreshSchurComplement(); CHECK(factorization.GetStatistics().setups == 2); CHECK(factorization.GetStatistics().schurProbes == static_cast(2 * borderSize)); CHECK(factorization.GetStatistics().borderToBorderApplications == static_cast(2 * borderSize)); CHECK( factorization.GetStatistics().structureInverseApplications == static_cast(2 * borderSize + (cachesStructureResponse ? 1 : 2)) ); CHECK( factorization.GetStatistics().cachedStructureInverseBorderApplications == static_cast(cachesStructureResponse ? 1 : 0) ); CHECK( factorization.GetStatistics().structureToBorderApplications == static_cast(2 * borderSize + 1) ); CHECK( factorization.GetStatistics().borderToStructureApplications == static_cast(2 * borderSize + (cachesStructureResponse ? 0 : 1)) ); for (int row = 0; row < borderSize; ++row) { for (int column = 0; column < borderSize; ++column) { CHECK( factorization.GetSchurComplement()(row, column) == Catch::Approx(expectedSchurEntry(row, column)).margin(2.0e-14) ); } } mfem::Vector refreshedActual(completeSize); mfem::Vector refreshedExpected(completeSize); factorization.Mult(rightHandSide, refreshedActual); mfem::DenseMatrixInverse refreshedExactInverse(completeMatrix); refreshedExactInverse.Mult(rightHandSide, refreshedExpected); CHECK(relativeError(refreshedActual, refreshedExpected) <= 2.0e-13); const auto statisticsAfterRefreshApplication = factorization.GetStatistics(); CHECK(statisticsAfterRefreshApplication.applications == 2); CHECK( statisticsAfterRefreshApplication.structureInverseApplications == static_cast(2 * borderSize + (cachesStructureResponse ? 2 : 4)) ); CHECK( statisticsAfterRefreshApplication.cachedStructureInverseBorderApplications == static_cast(cachesStructureResponse ? 2 : 0) ); CHECK( statisticsAfterRefreshApplication.structureToBorderApplications == static_cast(2 * borderSize + 2) ); CHECK( statisticsAfterRefreshApplication.borderToStructureApplications == static_cast(2 * borderSize + (cachesStructureResponse ? 0 : 2)) ); } [[nodiscard]] mean_field::operators::StellarEquilibriumDependencies makeDependencies(const std::uint64_t revision = 1) { return { .discretization = {.identity = 9201, .revision = 1}, .density = {.identity = 9203, .revision = revision}, .surfaceDeformation = {.identity = 9207, .revision = revision}, .gravityGradient = {.identity = 9211, .revision = revision}, .gravityPotential = {.identity = 9217, .revision = revision}, .enthalpy = {.identity = 9223, .revision = revision}, .bernoulliConstant = {.identity = 9229, .revision = revision}, .rotation = {.identity = 9231, .revision = revision}, .targetMass = {.identity = 9237, .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}; } template < typename View, typename Term> void assignStateBlock( const View &view, const Term &term, const mfem::Vector &source, mfem::Vector &state ) { mfem::Vector destination = view.block(term); REQUIRE(destination.Size() == source.Size()); destination = source; destination.SyncAliasMemory(state); } } // namespace TEST_CASE( "Model Specifications Compile Complete Canonical Preconditioning Borders", "[preconditioning][specification_border][unit][type_contract]" ) { using ExpectedBaseCorrections = blocks::type_list; using ExpectedBaseResiduals = blocks::type_list; using ExpectedCentralCorrections = blocks::type_list< blocks::fixed_total_mass::mass_normalization::value, blocks::fixed_central_density::central_value::value>; using ExpectedCentralResiduals = blocks::type_list< blocks::fixed_total_mass::mass_normalization::residual, blocks::fixed_central_density::central_value::residual>; using ExpectedAngularCorrections = blocks::type_list< blocks::fixed_total_mass::mass_normalization::value, blocks::fixed_angular_momentum::angular_velocity::value>; using ExpectedAngularResiduals = blocks::type_list< blocks::fixed_total_mass::mass_normalization::residual, blocks::fixed_angular_momentum::angular_velocity::residual>; using ExpectedAngularCentralCorrections = blocks::type_list< blocks::fixed_total_mass::mass_normalization::value, blocks::fixed_angular_momentum::angular_velocity::value, blocks::fixed_central_density::central_value::value>; STATIC_CHECK(std::same_as); STATIC_CHECK(BaseBorder::valueArity == 1); STATIC_CHECK(BaseBorder::residualArity == 1); STATIC_CHECK(BaseBorder::specificationCount == 1); STATIC_CHECK(std::same_as); STATIC_CHECK(std::same_as); STATIC_CHECK(BaseBorder::RequiredCouplings::size == 3); STATIC_CHECK(CentralBorder::valueArity == 2); STATIC_CHECK(CentralBorder::residualArity == 2); STATIC_CHECK(CentralBorder::specificationCount == 2); STATIC_CHECK(std::same_as); STATIC_CHECK(std::same_as); STATIC_CHECK(CentralBorder::RequiredCouplings::size == 5); STATIC_CHECK(AngularBorder::valueArity == 2); STATIC_CHECK(AngularBorder::residualArity == 2); STATIC_CHECK(AngularBorder::specificationCount == 2); STATIC_CHECK(std::same_as); STATIC_CHECK(std::same_as); STATIC_CHECK(AngularBorder::RequiredCouplings::size == 8); STATIC_CHECK(AngularCentralBorder::valueArity == 3); STATIC_CHECK(AngularCentralBorder::residualArity == 3); STATIC_CHECK(AngularCentralBorder::specificationCount == 3); STATIC_CHECK(std::same_as); STATIC_CHECK(AngularCentralBorder::RequiredCouplings::size == 10); STATIC_CHECK( preconditioning::specificationBorderValueOffset == 0 ); STATIC_CHECK( preconditioning::specificationBorderValueOffset == 1 ); STATIC_CHECK( preconditioning::specificationBorderResidualOffset == 0 ); STATIC_CHECK( preconditioning::specificationBorderResidualOffset == 1 ); STATIC_CHECK( preconditioning::specificationBorderValueOffset == 0 ); STATIC_CHECK( preconditioning::specificationBorderValueOffset< mean_field::models::FixedAngularMomentum, AngularCentralModel> == 1 ); STATIC_CHECK( preconditioning::specificationBorderValueOffset == 2 ); STATIC_CHECK(preconditioning::PreconditionerComponent); STATIC_CHECK(preconditioning::PreconditionerComponent); STATIC_CHECK(preconditioning::PreconditionerComponent); STATIC_CHECK(preconditioning::PreconditionerComponent); STATIC_CHECK(preconditioning::SpecificationBorderPreparableFor); STATIC_CHECK(preconditioning::SpecificationBorderPreparableFor); STATIC_CHECK(CanPrepareSpecificationBorder); STATIC_CHECK_FALSE(CanPrepareSpecificationBorderFromTemporary); STATIC_CHECK(std::constructible_from); STATIC_CHECK_FALSE(std::constructible_from); STATIC_CHECK_FALSE(std::constructible_from); STATIC_CHECK(std::constructible_from); STATIC_CHECK_FALSE(std::constructible_from); STATIC_CHECK_FALSE(std::constructible_from); // A refreshed structure inverse invalidates the cached A^-1 B columns and // the dense Schur complement even when the problem snapshot itself did not // change. Keep the complete invalidation truth table executable at compile // time so this lifecycle branch cannot silently regress. STATIC_CHECK_FALSE(preconditioning::detail::specificationBorderCachesRequireRefresh(false, false)); STATIC_CHECK(preconditioning::detail::specificationBorderCachesRequireRefresh(true, false)); STATIC_CHECK(preconditioning::detail::specificationBorderCachesRequireRefresh(false, true)); STATIC_CHECK(preconditioning::detail::specificationBorderCachesRequireRefresh(true, true)); STATIC_CHECK(preconditioning::SpecificationBorderBlockType); STATIC_CHECK_FALSE(preconditioning::SpecificationBorderPreparableFor); STATIC_CHECK_FALSE(CanPrepareSpecificationBorder); STATIC_CHECK(preconditioning::DefaultStellarPreconditionerAvailableFor); STATIC_CHECK(preconditioning::DefaultStellarPreconditionerAvailableFor); STATIC_CHECK(preconditioning::DefaultStellarPreconditionerAvailableFor); STATIC_CHECK(preconditioning::DefaultStellarPreconditionerAvailableFor); STATIC_CHECK(CanMakeDefaultStellarPreconditioner); STATIC_CHECK(CanMakeDefaultStellarPreconditioner); STATIC_CHECK(CanMakeDefaultStellarPreconditioner); STATIC_CHECK(CanMakeDefaultStellarPreconditioner); using PhysicsFacingStructureSupport = preconditioning::DefaultStellarStructurePhysicalTopologySupport; using UnhandledPhysicsFacingStructureEdge = mean_field::operators::StellarEquilibriumJacobianCoupling< blocks::enthalpy::specific::residual, blocks::enthalpy::specific::value>; STATIC_CHECK(PhysicsFacingStructureSupport::UnsupportedCouplings::size == 1); STATIC_CHECK( mean_field::utils::blocks::contains_type_v< UnhandledPhysicsFacingStructureEdge, typename PhysicsFacingStructureSupport::UnsupportedCouplings> ); STATIC_CHECK_FALSE(preconditioning::DefaultStellarPreconditionerAvailableFor); STATIC_CHECK_FALSE(CanMakeDefaultStellarPreconditioner); STATIC_CHECK_FALSE( mean_field::operators::StellarEquilibriumRuntimeContribution::registered ); STATIC_CHECK_FALSE( mean_field::operators::stellarEquilibriumBackendRuntimeAuthorized< PhysicsFacingBorderConstraint, PhysicsFacingModel> ); STATIC_CHECK( mean_field::operators::StellarEquilibriumPhysicsAvailableFor ); STATIC_CHECK( mean_field::equilibrium::StellarEquilibriumModelDiscretizationCompatible< PhysicsFacingModel, PhysicsFacingRieszDiscretization> ); STATIC_CHECK(BaseComponent::RequiredCouplings::size == 20); STATIC_CHECK(CentralComponent::RequiredCouplings::size == 22); STATIC_CHECK(AngularComponent::RequiredCouplings::size == 25); STATIC_CHECK(AngularCentralComponent::RequiredCouplings::size == 27); STATIC_CHECK(preconditioning::CompletePreconditionerFor); STATIC_CHECK( preconditioning::CompatiblePreconditionerFor< BasePlan, typename BaseProblem::FormType, typename BaseProblem::JacobianFormType> ); STATIC_CHECK(preconditioning::CompletePreconditionerFor); STATIC_CHECK( preconditioning::CompatiblePreconditionerFor< CentralPlan, typename CentralProblem::FormType, typename CentralProblem::JacobianFormType> ); STATIC_CHECK(preconditioning::CompletePreconditionerFor); STATIC_CHECK( preconditioning::CompatiblePreconditionerFor< AngularPlan, typename AngularProblem::FormType, typename AngularProblem::JacobianFormType> ); STATIC_CHECK( preconditioning::CompletePreconditionerFor ); STATIC_CHECK( preconditioning::CompatiblePreconditionerFor< AngularCentralPlan, typename AngularCentralProblem::FormType, typename AngularCentralProblem::JacobianFormType> ); STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible); STATIC_CHECK(preconditioning::PreparedSpecificationBorderActionFor); STATIC_CHECK_FALSE( preconditioning::PreparedSpecificationBorderActionFor ); using MockPhysicsProvider = preconditioning::LocalSpecificationBorderPhysics; using IncompleteMockPhysicsProvider = preconditioning::SpecificationBorderPhysics; using NonMovableMockPhysicsProvider = preconditioning::SpecificationBorderPhysics; using ProblemOnlyMockPhysicsProvider = preconditioning::SpecificationBorderPhysics; STATIC_CHECK( preconditioning::SpecificationBorderPhysicsFor< MockPhysicsProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem> ); STATIC_CHECK( std::constructible_from< MockPhysicsBorderAction, const preconditioning::PreparedSpecificationEquilibriumPhysicsT< PhysicsFacingBorderConstraint, PhysicsFacingProblem> &> ); STATIC_CHECK_FALSE(std::constructible_from); STATIC_CHECK_FALSE( preconditioning::SpecificationBorderPhysicsFor< IncompleteMockPhysicsProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem> ); STATIC_CHECK_FALSE(preconditioning::DeclaredCouplingSafeSpecificationBorderPhysics); STATIC_CHECK_FALSE( preconditioning::SpecificationBorderPhysicsFor< UnsafeRawBorderProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem> ); STATIC_CHECK_FALSE( preconditioning::SpecificationBorderPhysicsFor< NonMovableMockPhysicsProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem> ); using PreparedPhysicsFacingEquilibrium = preconditioning::PreparedSpecificationEquilibriumPhysicsT; using ProblemOnlyAction = ProblemOnlyPhysicsBorderAction; STATIC_CHECK( std::same_as ); STATIC_CHECK(std::constructible_from); STATIC_CHECK_FALSE(std::constructible_from); STATIC_CHECK_FALSE( preconditioning::SpecificationBorderPhysicsFor< ProblemOnlyMockPhysicsProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem> ); STATIC_CHECK( preconditioning::SpecificationBorderPhysicsAvailableFor ); STATIC_CHECK_FALSE( preconditioning::SpecificationBorderPhysicsAvailableFor ); STATIC_CHECK_FALSE( preconditioning::SpecificationBorderPhysicsAvailableFor ); STATIC_CHECK( std::same_as< preconditioning::PreparedSpecificationBorderPhysicsT, MockPhysicsProvider::Prepared> ); STATIC_CHECK( std::same_as< typename preconditioning::PreparedSpecificationBorderPhysicsT< PhysicsFacingBorderConstraint, PhysicsFacingProblem>::Physics, MockPhysicsBorderAction> ); // An operation view exposes no direction at all until a declared exact // Jacobian pair is selected. The callback then receives only that pair's // source and an additive handle to only that pair's row. STATIC_CHECK_FALSE(HasConstraintResidualBlock); STATIC_CHECK(CanAddConstraintResidualFromEnthalpy); STATIC_CHECK_FALSE(CanAddConstraintResidualFromGenerated); STATIC_CHECK_FALSE(HasSpecificEnthalpyBlock); STATIC_CHECK_FALSE(HasDensityBlock); STATIC_CHECK_FALSE(CanClaimEnthalpyButReadDensity); STATIC_CHECK_FALSE(CanMutateBoundEnthalpySource); STATIC_CHECK_FALSE(CanAccessNamedRowInsideCallback); // FixedTotalMass has both density and shape as legal structure sources. // Even in that multi-source operation, selecting density cannot deliver // the independently legal shape direction to the callback. STATIC_CHECK(CanAddMassResidualFromSurface); STATIC_CHECK(CanAddVectorMassResidualFromSurface); STATIC_CHECK_FALSE(CanClaimDensityButReadSurface); STATIC_CHECK_FALSE(HasSpecificEnthalpyBlock); STATIC_CHECK(CanAddSpecificEnthalpyFromGenerated); STATIC_CHECK_FALSE(HasDensityBlock); STATIC_CHECK_FALSE(HasConstraintResidualBlock); STATIC_CHECK_FALSE(CanAddConstraintResidualFromGenerated); STATIC_CHECK_FALSE(CanAddConstraintResidualFromEnthalpy); STATIC_CHECK_FALSE(ExposesUnrestrictedVector); } TEST_CASE( "A Nested And Backend Border Physics Provider Is Rejected As Ambiguous", "[preconditioning][specification_border][physics-extension][type_contract]" ) { using Specification = PhysicsFacingBorderConstraint; using Problem = DualProviderProblem; using NestedProvider = typename Specification::SpecificationBorderPhysics; using BackendAction = preconditioning::detail::PreparedSpecificationBorderAction; using BackendProvider = preconditioning::detail::BuiltinSpecificationBorderPhysics; using Selection = preconditioning::detail::SpecificationBorderPhysicsSelectionAudit; using NestedOnlySelection = preconditioning::detail::SpecificationBorderPhysicsSelectionAudit< PhysicsFacingBorderConstraint, PhysicsFacingProblem>; using BackendOnlySelection = preconditioning::detail::SpecificationBorderPhysicsSelectionAudit< mean_field::models::FixedTotalMass, BaseProblem>; using MalformedProvider = preconditioning::SpecificationBorderPhysics; // Provider selection has four intentionally distinct outcomes. In // particular, the ambiguity fixture uses a different model type, so its // backend registration cannot contaminate the ordinary nested-only path. STATIC_CHECK_FALSE(NestedOnlySelection::ambiguous); STATIC_CHECK(NestedOnlySelection::available); STATIC_CHECK( preconditioning::SpecificationBorderPhysicsAvailableFor ); STATIC_CHECK_FALSE(BackendOnlySelection::ambiguous); STATIC_CHECK(BackendOnlySelection::available); STATIC_CHECK( preconditioning::SpecificationBorderPhysicsAvailableFor ); STATIC_CHECK_FALSE( preconditioning::SpecificationBorderPhysicsFor< MalformedProvider, PhysicsFacingBorderConstraint, PhysicsFacingProblem> ); // Both implementations are independently complete. The aggregate path // must still reject the model instead of silently preferring the nested one. STATIC_CHECK(preconditioning::PreparedSpecificationBorderActionFor); STATIC_CHECK(preconditioning::SpecificationBorderPhysicsFor); STATIC_CHECK(preconditioning::SpecificationBorderPhysicsFor); STATIC_CHECK(Selection::ambiguous); STATIC_CHECK_FALSE(Selection::available); STATIC_CHECK_FALSE(preconditioning::SpecificationBorderPhysicsAvailableFor); STATIC_CHECK_FALSE(preconditioning::CompleteSpecificationBorderActionsFor); STATIC_CHECK_FALSE(preconditioning::DefaultStellarPreconditionerAvailableFor); // The public query remains safe for unrelated types as well as ambiguous // valid problem types. STATIC_CHECK_FALSE(preconditioning::SpecificationBorderPhysicsAvailableFor); } TEST_CASE( "Physics-Facing Constraint Hooks Reproduce Their Declared Jacobian Edges", "[preconditioning][specification_border][physics-extension][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 referenceRadius = 2.0; constexpr double gravitationalConstant = 3.0; constexpr double targetMass = 1.0; const auto stellarModel = model::StellarModel( eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}), integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}), PhysicsFacingBorderConstraint({.target = dimensions::SpecificEnthalpyValue{1.0}}) ); auto problem = equilibrium::discretize( stellarModel, equilibrium::makeStellarDiscretization( std::move(finiteElements), normalization::PhysicalRieszDiagonal{dimensions::LengthValue{referenceRadius}, gravitationalConstant} ) ); auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem); using Problem = std::remove_cvref_t; using Form = typename Problem::FormType; constexpr auto customValueBlock = utils::blocks::get_value_block
(specification_border_test::physicsFacingBorderTerm); constexpr auto customResidualBlock = utils::blocks::get_residual_block(specification_border_test::physicsFacingBorderTerm); const auto scales = normalization::deriveStellarCharacteristicScales( dimensions::MassValue{targetMass}, dimensions::LengthValue{referenceRadius}, gravitationalConstant ); const auto &layout = problem.GetManifest().layout(); CHECK( normalized.GetNormalization().StateFactors()(layout.offset(customValueBlock)) == Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15) ); CHECK( normalized.GetNormalization().ResidualFactors()(layout.offset(customResidualBlock)) == Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15) ); mfem::Vector state(problem.StateSize()); state = 0.0; const auto stateView = problem.GetManifest().stateView(state); stateView.block(blocks::density_field.mass_term) = 1.0; stateView.block(blocks::enthalpy_field.specific_term) = 1.0; stateView.block(blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.25; stateView.block(specification_border_test::physicsFacingBorderTerm) = 0.3; const auto initialDependencies = makeDependencies(1); problem.Prepare(state, initialDependencies, zeroRotation()); /* Independently differentiate the public residual. These two directions * isolate the two custom edges, so agreement cannot be manufactured by * comparing two copies of the mock's analytic formula. */ const auto centeredDifference = [&](const mfem::Vector &direction) { constexpr double step = 1.0e-6; mfem::Vector plusState(state); plusState.Add(step, direction); problem.Prepare(plusState, initialDependencies, zeroRotation()); mfem::Vector plusResidual; problem.BuildResidual(plusResidual); mfem::Vector minusState(state); minusState.Add(-step, direction); problem.Prepare(minusState, initialDependencies, zeroRotation()); mfem::Vector minusResidual; problem.BuildResidual(minusResidual); plusResidual -= minusResidual; plusResidual /= 2.0 * step; problem.Prepare(state, initialDependencies, zeroRotation()); return plusResidual; }; mfem::Vector enthalpyOnlyDirection(problem.StateSize()); enthalpyOnlyDirection = 0.0; auto enthalpyOnlyView = problem.GetManifest().stateView(enthalpyOnlyDirection); mfem::Vector enthalpyOnlyBlock = enthalpyOnlyView.block(blocks::enthalpy_field.specific_term); enthalpyOnlyBlock(0) = 0.7; enthalpyOnlyBlock.SyncAliasMemory(enthalpyOnlyDirection); mfem::Vector enthalpyOnlyAction; problem.ApplyLinearization(enthalpyOnlyDirection, enthalpyOnlyAction); mfem::Vector enthalpyOnlyDifference = centeredDifference(enthalpyOnlyDirection); const auto enthalpyOnlyAnalyticView = problem.GetManifest().residualView(enthalpyOnlyAction); const auto enthalpyOnlyDifferenceView = problem.GetManifest().residualView(enthalpyOnlyDifference); CHECK( enthalpyOnlyAnalyticView.block(specification_border_test::physicsFacingBorderTerm)(0) == Catch::Approx(enthalpyOnlyDifferenceView.block(specification_border_test::physicsFacingBorderTerm)(0)) .margin(2.0e-10) ); mfem::Vector borderOnlyDirection(problem.StateSize()); borderOnlyDirection = 0.0; auto borderOnlyView = problem.GetManifest().stateView(borderOnlyDirection); mfem::Vector borderOnlyBlock = borderOnlyView.block(specification_border_test::physicsFacingBorderTerm); borderOnlyBlock(0) = 0.4; borderOnlyBlock.SyncAliasMemory(borderOnlyDirection); mfem::Vector borderOnlyAction; problem.ApplyLinearization(borderOnlyDirection, borderOnlyAction); mfem::Vector borderOnlyDifference = centeredDifference(borderOnlyDirection); const mfem::Vector analyticHydrostatic = problem.GetManifest().residualView(borderOnlyAction).block(blocks::enthalpy_field.specific_term); const mfem::Vector differenceHydrostatic = problem.GetManifest().residualView(borderOnlyDifference).block(blocks::enthalpy_field.specific_term); CHECK(relativeError(analyticHydrostatic, differenceHydrostatic) <= 2.0e-10); preconditioning::SpecificationBorderJacobianOperator coupling(problem); REQUIRE(coupling.BorderSize() == 2); const auto &offsets = coupling.GetStructureOffsets(); const int enthalpySize = offsets[3] - offsets[2]; REQUIRE(enthalpySize > 0); const mfem::Array &surfaceRows = problem.GetPressureSurfaceRows().reduced_dofs(); REQUIRE(surfaceRows.Size() > 0); const auto setExpectedHydrostaticBorderAction = [&offsets, &surfaceRows, enthalpySize](mfem::Vector &action, const double contribution) { for (int index = offsets[2]; index < offsets[3]; ++index) { action(index) = contribution; } for (const int row : surfaceRows) { REQUIRE(row >= 0); REQUIRE(row < enthalpySize); action(offsets[2] + row) = 0.0; } }; constexpr double enthalpyVariation = 0.7; constexpr double borderVariation = 0.4; mfem::Vector groupedDirection(coupling.Width()); groupedDirection = 0.0; groupedDirection(offsets[2]) = enthalpyVariation; groupedDirection(coupling.StructureSize() + 1) = borderVariation; mfem::Vector actual(coupling.Height()); coupling.Mult(groupedDirection, actual); mfem::Vector expected(coupling.Height()); expected = 0.0; constexpr double initialCoefficient = 1.0 + 0.125 * 1.0; setExpectedHydrostaticBorderAction(expected, initialCoefficient * borderVariation); expected(coupling.StructureSize() + 1) = initialCoefficient * enthalpyVariation; CHECK(relativeError(actual, expected) <= 2.0e-14); // Compare each inferred cross block with the corresponding slice of the // authoritative root Jacobian. Structure-to-structure physics is omitted // deliberately; this operator owns only the specification border. mfem::Vector structureRootDirection(problem.StateSize()); structureRootDirection = 0.0; auto structureRootView = problem.GetManifest().stateView(structureRootDirection); mfem::Vector enthalpyRootDirection = structureRootView.block(blocks::enthalpy_field.specific_term); enthalpyRootDirection(0) = enthalpyVariation; enthalpyRootDirection.SyncAliasMemory(structureRootDirection); mfem::Vector borderRootDirection(problem.StateSize()); borderRootDirection = 0.0; auto borderRootView = problem.GetManifest().stateView(borderRootDirection); mfem::Vector customBorderDirection = borderRootView.block(specification_border_test::physicsFacingBorderTerm); customBorderDirection(0) = borderVariation; customBorderDirection.SyncAliasMemory(borderRootDirection); mfem::Vector structureRootAction; mfem::Vector borderRootAction; problem.ApplyLinearization(structureRootDirection, structureRootAction); problem.ApplyLinearization(borderRootDirection, borderRootAction); const auto structureResidual = problem.GetManifest().residualView(structureRootAction); const auto borderResidual = problem.GetManifest().residualView(borderRootAction); CHECK( structureResidual.block(specification_border_test::physicsFacingBorderTerm)(0) == Catch::Approx(actual(coupling.StructureSize() + 1)).margin(2.0e-14) ); const mfem::Vector enthalpyBorderAction = borderResidual.block(blocks::enthalpy_field.specific_term); const mfem::Vector expectedEnthalpyAction(actual.GetData() + offsets[2], enthalpySize); CHECK(relativeError(enthalpyBorderAction, expectedEnthalpyAction) <= 2.0e-14); /* A Newton-state relinearization invalidates the standalone inferred * border operator even when dependency stamps are intentionally reused. * This model is deliberately unavailable to the default full * preconditioner because its additional nonzero h <- h term has no * structure-backend implementation. */ mfem::Vector refreshedState(state); auto refreshedStateView = problem.GetManifest().stateView(refreshedState); mfem::Vector refreshedEnthalpy = refreshedStateView.block(blocks::enthalpy_field.specific_term); refreshedEnthalpy = 3.0; refreshedEnthalpy.SyncAliasMemory(refreshedState); problem.Prepare(refreshedState, initialDependencies, zeroRotation()); CHECK_FALSE(coupling.IsCurrent()); CHECK_THROWS_AS(coupling.Mult(groupedDirection, actual), std::logic_error); CHECK(coupling.Refresh()); CHECK(coupling.IsCurrent()); coupling.Mult(groupedDirection, actual); constexpr double refreshedCoefficient = 1.0 + 0.125 * 3.0; constexpr double refreshedPhaseDerivative = refreshedCoefficient + 0.125 * (3.0 - 1.0); expected = 0.0; setExpectedHydrostaticBorderAction(expected, refreshedCoefficient * borderVariation); expected(coupling.StructureSize() + 1) = refreshedPhaseDerivative * enthalpyVariation; CHECK(relativeError(actual, expected) <= 2.0e-14); CHECK_FALSE(coupling.Refresh()); } TEST_CASE( "Dense Specification Borders Cache Stationary Structure Responses And Reproduce Exact Block Factorizations", "[preconditioning][specification_border][unit][factorization]" ) { SECTION("one generated scalar") { verifyKnownBorderFactorization(1); } SECTION("two generated scalars") { verifyKnownBorderFactorization(2); } SECTION("four generated scalars") { verifyKnownBorderFactorization(4); } } TEST_CASE( "Flexible Specification Borders Preserve Per-Application Structure Solves", "[preconditioning][specification_border][unit][factorization]" ) { verifyKnownBorderFactorization(2); } TEST_CASE( "Generated Specification Border Actions Match The Authoritative Stellar Jacobian", "[preconditioning][specification_border][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); const auto stellarModel = 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(stellarModel, std::move(finiteElements)); auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 512})); problem.Prepare(projected.values, makeDependencies(), zeroRotation()); preconditioning::SpecificationBorderJacobianOperator coupling(problem); REQUIRE(coupling.BorderSize() == 2); REQUIRE(coupling.StructureSize() + coupling.BorderSize() == problem.StateSize()); const auto &offsets = coupling.GetStructureOffsets(); using Form = typename std::remove_cvref_t::FormType; const auto &layout = problem.GetManifest().layout(); CHECK(offsets[1] - offsets[0] == layout.size(blocks::get_value_block(blocks::density_field.mass_term))); CHECK( offsets[2] - offsets[1] == layout.size(blocks::get_value_block(blocks::surface_deformation_field.parameters_term)) ); CHECK(offsets[3] - offsets[2] == layout.size(blocks::get_value_block(blocks::enthalpy_field.specific_term))); CHECK(offsets[4] - offsets[3] == layout.size(blocks::get_value_block(blocks::gravity_field.gradient_term))); CHECK(offsets[5] - offsets[4] == layout.size(blocks::get_value_block(blocks::gravity_field.poisson_term))); mfem::Vector groupedDirection(coupling.Width()); for (int index = 0; index < groupedDirection.Size(); ++index) { groupedDirection(index) = 0.015 * std::sin(0.23 * static_cast(index + 1)); } const auto groupedBlock = [&](const int block) { return mfem::Vector(groupedDirection.GetData() + offsets[block], offsets[block + 1] - offsets[block]); }; mfem::Vector structureOnlyRoot(problem.StateSize()); structureOnlyRoot = 0.0; const auto structureView = problem.GetManifest().stateView(structureOnlyRoot); assignStateBlock(structureView, blocks::density_field.mass_term, groupedBlock(0), structureOnlyRoot); assignStateBlock( structureView, blocks::surface_deformation_field.parameters_term, groupedBlock(1), structureOnlyRoot ); assignStateBlock(structureView, blocks::enthalpy_field.specific_term, groupedBlock(2), structureOnlyRoot); assignStateBlock(structureView, blocks::gravity_field.gradient_term, groupedBlock(3), structureOnlyRoot); assignStateBlock(structureView, blocks::gravity_field.poisson_term, groupedBlock(4), structureOnlyRoot); mfem::Vector borderOnlyRoot(problem.StateSize()); borderOnlyRoot = 0.0; const auto borderView = problem.GetManifest().stateView(borderOnlyRoot); mfem::Vector massDirection(groupedDirection.GetData() + coupling.StructureSize(), 1); mfem::Vector centralDirection(groupedDirection.GetData() + coupling.StructureSize() + 1, 1); assignStateBlock( borderView, blocks::fixed_total_mass_constraint.mass_normalization_term, massDirection, borderOnlyRoot ); assignStateBlock( borderView, blocks::fixed_central_density_phase.central_value_term, centralDirection, borderOnlyRoot ); mfem::Vector structureOnlyAction; mfem::Vector borderOnlyAction; problem.ApplyLinearization(structureOnlyRoot, structureOnlyAction); problem.ApplyLinearization(borderOnlyRoot, borderOnlyAction); auto structureOnlyResidual = problem.GetManifest().residualView(structureOnlyAction); auto borderOnlyResidual = problem.GetManifest().residualView(borderOnlyAction); mfem::Vector expected(coupling.Height()); expected = 0.0; expected.SetVector(borderOnlyResidual.block(blocks::density_field.mass_term), offsets[0]); expected.SetVector(borderOnlyResidual.block(blocks::surface_deformation_field.shape_equilibrium_term), offsets[1]); expected.SetVector(borderOnlyResidual.block(blocks::enthalpy_field.specific_term), offsets[2]); expected.SetVector(borderOnlyResidual.block(blocks::gravity_field.gradient_term), offsets[3]); expected.SetVector(borderOnlyResidual.block(blocks::gravity_field.poisson_term), offsets[4]); expected.SetVector( structureOnlyResidual.block(blocks::fixed_total_mass_constraint.mass_normalization_term), coupling.StructureSize() ); expected.SetVector( structureOnlyResidual.block(blocks::fixed_central_density_phase.central_value_term), coupling.StructureSize() + 1 ); mfem::Vector borderDiagonal(2); borderDiagonal(0) = borderOnlyResidual.block(blocks::fixed_total_mass_constraint.mass_normalization_term)(0); borderDiagonal(1) = borderOnlyResidual.block(blocks::fixed_central_density_phase.central_value_term)(0); mfem::Vector expectedBorder(expected, coupling.StructureSize(), coupling.BorderSize()); expectedBorder += borderDiagonal; expectedBorder.SyncAliasMemory(expected); mfem::Vector actual(coupling.Height()); coupling.Mult(groupedDirection, actual); CHECK(relativeError(actual, expected) <= 2.0e-12); auto component = preconditioning::makePreconditioner(problem); using Component = decltype(component); STATIC_CHECK(std::same_as); auto prepared = preconditioning::prepare(problem, component); using GroupedPreconditioner = typename decltype(prepared)::GroupedPreconditioner; using PreparedFactorization = typename GroupedPreconditioner::Factorization; STATIC_CHECK(PreparedFactorization::cachesStructureInverseBorderCoupling); mfem::Vector rightHandSide(prepared.Width()); for (int index = 0; index < rightHandSide.Size(); ++index) { rightHandSide(index) = std::cos(0.11 * static_cast(index + 1)); } mfem::Vector correction(prepared.Height()); prepared.Mult(rightHandSide, correction); for (int index = 0; index < correction.Size(); ++index) { REQUIRE(std::isfinite(correction(index))); } const auto &factorizationStatistics = prepared.GetGroupedPreconditioner().GetFactorization().GetStatistics(); CHECK(factorizationStatistics.setups == 1); CHECK(factorizationStatistics.schurProbes == 2); CHECK(factorizationStatistics.applications == 1); CHECK(factorizationStatistics.structureInverseApplications == 3); CHECK(factorizationStatistics.cachedStructureInverseBorderApplications == 1); CHECK(factorizationStatistics.borderToStructureApplications == 2); const std::uint64_t setupsBeforeNoOpRefresh = factorizationStatistics.setups; const auto unchanged = prepared.Refresh(); CHECK_FALSE(unchanged.DidAnyWork()); CHECK(prepared.GetGroupedPreconditioner().GetFactorization().GetStatistics().setups == setupsBeforeNoOpRefresh); CHECK(prepared.IsCurrent()); /* * A contribution may change with the prepared state even when callers * intentionally reuse the same dependency stamps. The problem generation * must therefore invalidate and rebuild the dense border Schur complement. */ problem.Prepare(projected.values, makeDependencies(), zeroRotation()); CHECK_FALSE(prepared.IsCurrent()); const std::uint64_t setupsBeforeRelinearization = prepared.GetGroupedPreconditioner().GetFactorization().GetStatistics().setups; const auto relinearized = prepared.Refresh(); CHECK(relinearized.rebuiltSchurComplement); CHECK(relinearized.DidAnyWork()); CHECK( prepared.GetGroupedPreconditioner().GetFactorization().GetStatistics().setups == setupsBeforeRelinearization + 1 ); CHECK(prepared.IsCurrent()); } TEST_CASE( "Fixed Angular Momentum Border Actions Match The Authoritative Generated Rotation Jacobian", "[preconditioning][specification_border][fixed-angular-momentum][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()); 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}}), integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.2}}) ); auto problem = equilibrium::discretize(model, std::move(finiteElements)); mfem::Vector state(problem.StateSize()); state = 0.0; const auto stateView = problem.GetManifest().stateView(state); stateView.block(blocks::density_field.mass_term) = 1.0; stateView.block(blocks::enthalpy_field.specific_term) = 1.0; stateView.block(blocks::fixed_total_mass_constraint.mass_normalization_term) = 0.25; stateView.block(blocks::fixed_angular_momentum_constraint.angular_velocity_term) = 0.4; problem.Prepare(state, makeDependencies()); preconditioning::SpecificationBorderJacobianOperator coupling(problem); REQUIRE(coupling.BorderSize() == 2); REQUIRE(coupling.StructureSize() + coupling.BorderSize() == problem.StateSize()); const auto &offsets = coupling.GetStructureOffsets(); mfem::Vector groupedDirection(coupling.Width()); for (int index = 0; index < groupedDirection.Size(); ++index) { groupedDirection(index) = 0.017 * std::sin(0.29 * static_cast(index + 1)); } const auto groupedBlock = [&](const int block) { return mfem::Vector(groupedDirection.GetData() + offsets[block], offsets[block + 1] - offsets[block]); }; mfem::Vector structureOnlyRoot(problem.StateSize()); structureOnlyRoot = 0.0; const auto structureView = problem.GetManifest().stateView(structureOnlyRoot); assignStateBlock(structureView, blocks::density_field.mass_term, groupedBlock(0), structureOnlyRoot); assignStateBlock( structureView, blocks::surface_deformation_field.parameters_term, groupedBlock(1), structureOnlyRoot ); assignStateBlock(structureView, blocks::enthalpy_field.specific_term, groupedBlock(2), structureOnlyRoot); assignStateBlock(structureView, blocks::gravity_field.gradient_term, groupedBlock(3), structureOnlyRoot); assignStateBlock(structureView, blocks::gravity_field.poisson_term, groupedBlock(4), structureOnlyRoot); mfem::Vector borderOnlyRoot(problem.StateSize()); borderOnlyRoot = 0.0; const auto borderView = problem.GetManifest().stateView(borderOnlyRoot); mfem::Vector massDirection(groupedDirection.GetData() + coupling.StructureSize(), 1); mfem::Vector angularVelocityDirection(groupedDirection.GetData() + coupling.StructureSize() + 1, 1); assignStateBlock( borderView, blocks::fixed_total_mass_constraint.mass_normalization_term, massDirection, borderOnlyRoot ); assignStateBlock( borderView, blocks::fixed_angular_momentum_constraint.angular_velocity_term, angularVelocityDirection, borderOnlyRoot ); mfem::Vector structureOnlyAction; mfem::Vector borderOnlyAction; problem.ApplyLinearization(structureOnlyRoot, structureOnlyAction); problem.ApplyLinearization(borderOnlyRoot, borderOnlyAction); auto structureOnlyResidual = problem.GetManifest().residualView(structureOnlyAction); auto borderOnlyResidual = problem.GetManifest().residualView(borderOnlyAction); CHECK(borderOnlyResidual.block(blocks::surface_deformation_field.shape_equilibrium_term).Norml2() > 0.0); CHECK(borderOnlyResidual.block(blocks::enthalpy_field.specific_term).Norml2() > 0.0); CHECK(borderOnlyResidual.block(blocks::fixed_angular_momentum_constraint.angular_velocity_term)(0) != 0.0); mfem::Vector expected(coupling.Height()); expected = 0.0; expected.SetVector(borderOnlyResidual.block(blocks::density_field.mass_term), offsets[0]); expected.SetVector(borderOnlyResidual.block(blocks::surface_deformation_field.shape_equilibrium_term), offsets[1]); expected.SetVector(borderOnlyResidual.block(blocks::enthalpy_field.specific_term), offsets[2]); expected.SetVector(borderOnlyResidual.block(blocks::gravity_field.gradient_term), offsets[3]); expected.SetVector(borderOnlyResidual.block(blocks::gravity_field.poisson_term), offsets[4]); expected.SetVector( structureOnlyResidual.block(blocks::fixed_total_mass_constraint.mass_normalization_term), coupling.StructureSize() ); expected.SetVector( structureOnlyResidual.block(blocks::fixed_angular_momentum_constraint.angular_velocity_term), coupling.StructureSize() + 1 ); mfem::Vector expectedBorder(expected, coupling.StructureSize(), coupling.BorderSize()); expectedBorder(0) += borderOnlyResidual.block(blocks::fixed_total_mass_constraint.mass_normalization_term)(0); expectedBorder(1) += borderOnlyResidual.block(blocks::fixed_angular_momentum_constraint.angular_velocity_term)(0); expectedBorder.SyncAliasMemory(expected); mfem::Vector actual(coupling.Height()); coupling.Mult(groupedDirection, actual); CHECK(relativeError(actual, expected) <= 2.0e-12); auto component = preconditioning::makePreconditioner(problem); using Component = decltype(component); STATIC_CHECK(std::same_as); auto prepared = preconditioning::prepare(problem, component); CHECK(prepared.IsCurrent()); mfem::Vector rightHandSide(prepared.Width()); for (int index = 0; index < rightHandSide.Size(); ++index) { rightHandSide(index) = std::cos(0.13 * static_cast(index + 1)); } mfem::Vector correction(prepared.Height()); prepared.Mult(rightHandSide, correction); REQUIRE(correction.Size() == prepared.Height()); for (int index = 0; index < correction.Size(); ++index) { CHECK(std::isfinite(correction(index))); } const auto &statistics = prepared.GetGroupedPreconditioner().GetFactorization().GetStatistics(); CHECK(statistics.setups == 1); CHECK(statistics.schurProbes == 2); }