module; #include #include #include #include #include #include #include #include #include #include #include #include #include export module mean_field:preconditioning.material_surface; export import :operators.prepared_stellar_equilibrium; export import :operators.stellar_equilibrium_problem; export import :preconditioning.backend_implementations; export import :preconditioning.plan; export namespace mean_field::preconditioning { struct MaterialSurfaceBlockDiagonal final { }; struct CoupledMaterialIndependentSurface final { }; struct MaterialThenSurfaceTriangular final { }; struct SurfaceThenMaterialTriangular final { }; struct ApproximateMaterialSurfaceLDU final { }; template struct IsMaterialSurfaceFactorizationPolicy : std::false_type { }; template <> struct IsMaterialSurfaceFactorizationPolicy : std::true_type { }; template <> struct IsMaterialSurfaceFactorizationPolicy : std::true_type { }; template <> struct IsMaterialSurfaceFactorizationPolicy : std::true_type { }; template <> struct IsMaterialSurfaceFactorizationPolicy : std::true_type { }; template <> struct IsMaterialSurfaceFactorizationPolicy : std::true_type { }; template concept MaterialSurfaceFactorizationPolicy = IsMaterialSurfaceFactorizationPolicy>::value; namespace detail { template struct MaterialSurfaceConcatenate; template <> struct MaterialSurfaceConcatenate<> { using Type = utils::blocks::type_list<>; }; template struct MaterialSurfaceConcatenate> { using Type = utils::blocks::type_list; }; template struct MaterialSurfaceConcatenate< utils::blocks::type_list, utils::blocks::type_list, Remaining...> { using Type = typename MaterialSurfaceConcatenate, Remaining...>::Type; }; template using MaterialSurfaceConcatenateT = typename MaterialSurfaceConcatenate::Type; template struct NonCarrierEquations; template struct NonCarrierEquations, CarrierField> { using Type = material::ThermodynamicEquationCatalog<>; }; template struct NonCarrierEquations, CarrierField> { private: using Tail = typename NonCarrierEquations, CarrierField>::Type; template struct Prepend; template struct Prepend> { using Type = material::ThermodynamicEquationCatalog; }; public: using Type = std::conditional_t< std::same_as, Tail, typename Prepend::Type>; }; template struct EquationCorrectionBlocks; template struct EquationCorrectionBlocks> { using Type = utils::blocks::type_list; }; template struct EquationResidualBlocks; template struct EquationResidualBlocks> { using Type = utils::blocks::type_list; }; template struct CouplingsForResidual; template struct CouplingsForResidual, JacobianForm> { using Type = utils::blocks::type_list<>; }; template struct CouplingsForResidual, JacobianForm> { private: using Tail = typename CouplingsForResidual, JacobianForm>::Type; public: using Type = std::conditional_t< utils::blocks::has_jacobian_coupling_v, MaterialSurfaceConcatenateT>, Tail>, Tail>; }; template struct InducedCouplings; template struct InducedCouplings, Corrections, JacobianForm> { using Type = utils::blocks::type_list<>; }; template struct InducedCouplings, Corrections, JacobianForm> { using Type = MaterialSurfaceConcatenateT< typename CouplingsForResidual::Type, typename InducedCouplings, Corrections, JacobianForm>::Type>; }; template struct IsMaterialSurfaceDescriptor : std::false_type { }; template struct MaterialSurfaceListIsSubset; template struct MaterialSurfaceListIsSubset, Universe> : std::bool_constant<(utils::blocks::contains_type_v && ...)> { }; } // namespace detail template < material::CompiledThermodynamicEquations ThermodynamicEquationsT, typename SurfaceConstraintT, typename FormT, typename JacobianFormT> requires requires { typename SurfaceConstraintT::CarrierField; typename SurfaceConstraintT::SurfaceDependencies; typename SurfaceConstraintT::SurfaceDependencies::StateFieldTypes; } && utils::blocks::valid_jacobian_form struct CompiledMaterialSurfaceDescriptor final { using ThermodynamicEquations = ThermodynamicEquationsT; using SurfaceConstraint = SurfaceConstraintT; using Form = FormT; using JacobianForm = JacobianFormT; using CarrierField = typename SurfaceConstraint::CarrierField; using CarrierEquation = material::ThermodynamicEquationForFieldT; using NonCarrierMaterialEquations = typename detail::NonCarrierEquations::Type; using NonCarrierCorrectionBlocks = typename detail::EquationCorrectionBlocks::Type; using NonCarrierResidualBlocks = typename detail::EquationResidualBlocks::Type; using SurfaceStateFields = typename SurfaceConstraint::SurfaceDependencies::StateFieldTypes; using CorrectionBlocks = detail::MaterialSurfaceConcatenateT< NonCarrierCorrectionBlocks, utils::blocks::type_list, utils::blocks::type_list>; using ResidualBlocks = detail::MaterialSurfaceConcatenateT< NonCarrierResidualBlocks, utils::blocks::type_list, utils::blocks::type_list>; using RequiredCouplings = typename detail::InducedCouplings::Type; static constexpr bool symbolicallySquare = CorrectionBlocks::size == ResidualBlocks::size; static constexpr bool surfaceDependenciesBelongToMaterial = material:: fieldsBelongToThermodynamicEquations; static constexpr bool correctionBlocksBelongToForm = detail::MaterialSurfaceListIsSubset::value; static constexpr bool residualBlocksBelongToForm = detail::MaterialSurfaceListIsSubset::value; }; template requires equilibrium::DiscretizedStellarEquilibriumProblem> using MaterialSurfaceDescriptorFor = CompiledMaterialSurfaceDescriptor< typename std::remove_cvref_t::ThermodynamicEquationsType, typename std::remove_cvref_t::CompiledSurfaceConstraintType, typename std::remove_cvref_t::FormType, typename std::remove_cvref_t::JacobianFormType>; namespace detail { template struct IsMaterialSurfaceDescriptor< CompiledMaterialSurfaceDescriptor> : std::bool_constant< material::CompiledThermodynamicEquations::ThermodynamicEquations> && CompiledMaterialSurfaceDescriptor:: symbolicallySquare && CompiledMaterialSurfaceDescriptor:: surfaceDependenciesBelongToMaterial && CompiledMaterialSurfaceDescriptor:: correctionBlocksBelongToForm && CompiledMaterialSurfaceDescriptor:: residualBlocksBelongToForm && utils::blocks::types_are_unique_v::CorrectionBlocks> && utils::blocks::types_are_unique_v::ResidualBlocks>> { }; } // namespace detail template concept MaterialSurfaceDescriptor = detail::IsMaterialSurfaceDescriptor>::value; /* * Capability boundary for EOS-specific material/surface surrogate * assembly. The current kernels remain polytropic, but selection no * longer embeds that closed-world type test in the descriptor concept. */ template struct MaterialSurfaceEquationOfStateBackend { static constexpr bool registered = false; }; template <> struct MaterialSurfaceEquationOfStateBackend { static constexpr bool registered = true; using CoreType = operators::PreparedStellarEquilibriumOperator; }; template concept ImplementedMaterialSurfaceEquationOfState = requires { { MaterialSurfaceEquationOfStateBackend>::registered } -> std::convertible_to; requires MaterialSurfaceEquationOfStateBackend< std::remove_cvref_t>::registered; typename MaterialSurfaceEquationOfStateBackend>::CoreType; }; /* * Registering an EOS-to-core association is intentionally not enough to * claim that the material/surface preconditioner can execute it. Every * implementation listed here must have matching prepared operators and * prepare(...) overloads below. A future backend should add its pair only * after those executable pieces exist; this keeps capability queries * truthful while the current kernels still consume the legacy physical * core directly. */ template struct MaterialSurfaceExecutableRuntime { static constexpr bool available = false; }; template <> struct MaterialSurfaceExecutableRuntime { static constexpr bool available = true; }; template concept ExecutableMaterialSurfaceRuntimeFor = requires { { MaterialSurfaceExecutableRuntime< std::remove_cvref_t, std::remove_cvref_t>::available } -> std::convertible_to; requires MaterialSurfaceExecutableRuntime< std::remove_cvref_t, std::remove_cvref_t>::available; }; template concept ImplementedMaterialSurfaceDescriptor = MaterialSurfaceDescriptor && ImplementedMaterialSurfaceEquationOfState< typename Descriptor::ThermodynamicEquations::EquationOfStateType>; template concept MaterialSurfaceRuntimeFor = ImplementedMaterialSurfaceDescriptor && requires { typename MaterialSurfaceEquationOfStateBackend< typename std::remove_cvref_t::ThermodynamicEquations::EquationOfStateType>::CoreType; requires std::same_as< std::remove_cvref_t, typename MaterialSurfaceEquationOfStateBackend< typename std::remove_cvref_t::ThermodynamicEquations::EquationOfStateType>::CoreType>; requires ExecutableMaterialSurfaceRuntimeFor< typename std::remove_cvref_t::ThermodynamicEquations::EquationOfStateType, PhysicalCore>; }; template concept MaterialSurfacePreconditionerProblem = equilibrium::DiscretizedStellarEquilibriumProblem && requires { requires MaterialSurfaceRuntimeFor< MaterialSurfaceDescriptorFor>, typename std::remove_cvref_t::PhysicalCoreType>; }; using DensityMassDiagonalCharacteristics = OperatorCharacteristics< OperatorCategory::mass_like, OperatorValueStructure::scalar, OperatorSymmetry::symmetric, OperatorDefiniteness::positive_definite, OperatorRepresentation::diagonal, OperatorDistribution::distributed_true_dof, OperatorFESpace::l2>; using EnthalpyMassDiagonalCharacteristics = OperatorCharacteristics< OperatorCategory::mass_like, OperatorValueStructure::scalar, OperatorSymmetry::symmetric, OperatorDefiniteness::positive_definite, OperatorRepresentation::diagonal, OperatorDistribution::distributed_true_dof, OperatorFESpace::h1>; // This describes the assembled diagonal surrogate, not the generally // nonsymmetric pulled-back q-to-R_q operator that it approximates. A // calibrated scalar multiple may carry either sign, so definiteness is not // claimed by this legacy path. using SurfaceDiagonalCharacteristics = OperatorCharacteristics< OperatorCategory::surface_like, OperatorValueStructure::scalar, OperatorSymmetry::symmetric, OperatorDefiniteness::unspecified, OperatorRepresentation::diagonal, OperatorDistribution::distributed_true_dof, OperatorFESpace::h1>; // The frequency-aware surface surrogate is an elliptic scalar operator on // the ambient H1 space whose trace supplies the deformation parameters. // A positive mass coefficient removes the constant-mode nullspace of the // tangential stiffness contribution. using SurfaceH1MassStiffnessCharacteristics = OperatorCharacteristics< OperatorCategory::elliptic_like, OperatorValueStructure::scalar, OperatorSymmetry::symmetric, OperatorDefiniteness::positive_definite, OperatorRepresentation::assembled_sparse, OperatorDistribution::distributed_true_dof, OperatorFESpace::h1>; enum class SurfaceRieszCalibrationTarget { none, surface_jacobian, approximate_material_schur }; // operator_action fits the surrogate s M directly to the requested target // T. right_preconditioned_action instead fits T (alpha M^{-1}) to the // identity and stores s = 1 / alpha. Both produce one fixed linear // diagonal operator after setup; the distinction is solely the calibration // objective. enum class SurfaceRieszCalibrationObjective { operator_action, right_preconditioned_action }; struct SurfaceRieszCalibrationOptions final { SurfaceRieszCalibrationTarget target{SurfaceRieszCalibrationTarget::none}; int probeCount{0}; SurfaceRieszCalibrationObjective objective{SurfaceRieszCalibrationObjective::operator_action}; }; struct MaterialSurfaceDiagonalOptions final { double relativeFloor{1.0e-12}; double absoluteFloor{1.0e-14}; SurfaceRieszCalibrationOptions surfaceCalibration{}; }; struct SurfaceMassDiagonal final { }; struct SurfaceH1MassStiffness final { SurfaceRieszCalibrationOptions calibration{ .target = SurfaceRieszCalibrationTarget::approximate_material_schur, .probeCount = 6 }; double relativeMassCoefficientFloor{1.0e-10}; double gramRelativeTolerance{1.0e-12}; }; template struct SurfaceSurrogateTraits { static constexpr bool registered = false; }; template <> struct SurfaceSurrogateTraits { static constexpr bool registered = true; using OperatorDescription = SurfaceDiagonalCharacteristics; }; template <> struct SurfaceSurrogateTraits { static constexpr bool registered = true; using OperatorDescription = SurfaceH1MassStiffnessCharacteristics; }; template concept MaterialSurfaceSurrogate = SurfaceSurrogateTraits>::registered; struct SurfaceH1NormalEquations final { double massMass{0.0}; double massStiffness{0.0}; double stiffnessStiffness{0.0}; double massTarget{0.0}; double stiffnessTarget{0.0}; double targetTarget{0.0}; }; struct SurfaceH1FitReport final { SurfaceRieszCalibrationTarget target{SurfaceRieszCalibrationTarget::none}; int probeCount{0}; double sign{1.0}; double massCoefficient{1.0}; double stiffnessCoefficient{0.0}; double relativeResidual{0.0}; double relativeGramDeterminant{0.0}; SurfaceH1NormalEquations normalEquations{}; [[nodiscard]] bool WasCalibrated() const noexcept { return target != SurfaceRieszCalibrationTarget::none; } }; namespace detail { [[nodiscard]] inline SurfaceH1FitReport fitSurfaceH1Coefficients( const SurfaceH1NormalEquations &equations, const SurfaceH1MassStiffness &configuration ) { const std::array values{ equations.massMass, equations.massStiffness, equations.stiffnessStiffness, equations.massTarget, equations.stiffnessTarget, equations.targetTarget, configuration.relativeMassCoefficientFloor, configuration.gramRelativeTolerance }; if (!std::all_of(values.begin(), values.end(), [](const double value) { return std::isfinite(value); }) || equations.massMass <= 0.0 || equations.stiffnessStiffness <= 0.0 || equations.targetTarget <= 0.0 || configuration.relativeMassCoefficientFloor <= 0.0 || configuration.gramRelativeTolerance <= 0.0 || configuration.gramRelativeTolerance >= 1.0 || configuration.calibration.target == SurfaceRieszCalibrationTarget::none || configuration.calibration.probeCount <= 0) { throw std::invalid_argument( "The surface H1 fit requires a calibration target, probes, and finite positive Gram data." ); } const double massNorm = std::sqrt(equations.massMass); const double stiffnessNorm = std::sqrt(equations.stiffnessStiffness); const double correlation = equations.massStiffness / massNorm / stiffnessNorm; const double relativeDeterminant = 1.0 - correlation * correlation; const double normalizedMassTarget = equations.massTarget / massNorm; const double normalizedStiffnessTarget = equations.stiffnessTarget / stiffnessNorm; if (!std::isfinite(massNorm) || !std::isfinite(stiffnessNorm) || !std::isfinite(correlation) || !std::isfinite(relativeDeterminant) || !std::isfinite(normalizedMassTarget) || !std::isfinite(normalizedStiffnessTarget) || relativeDeterminant <= configuration.gramRelativeTolerance) { throw std::runtime_error("The surface H1 calibration probes do not distinguish mass and stiffness."); } const double amplitude = std::sqrt(equations.targetTarget) / massNorm; const double minimumMassCoefficient = configuration.relativeMassCoefficientFloor * amplitude; if (!std::isfinite(amplitude) || !std::isfinite(minimumMassCoefficient) || minimumMassCoefficient <= 0.0) { throw std::runtime_error("The surface H1 calibration produced an invalid positive mass floor."); } struct Candidate final { double sign{1.0}; double mass{0.0}; double stiffness{0.0}; double residual{std::numeric_limits::infinity()}; }; const auto evaluate = [&](const double sign, const double mass, const double stiffness) { Candidate candidate{.sign = sign, .mass = mass, .stiffness = stiffness}; if (!std::isfinite(mass) || !std::isfinite(stiffness) || mass < minimumMassCoefficient || stiffness < 0.0) { return candidate; } const double normalizedMass = mass * massNorm; const double normalizedStiffness = stiffness * stiffnessNorm; candidate.residual = equations.targetTarget + normalizedMass * normalizedMass + 2.0 * correlation * normalizedMass * normalizedStiffness + normalizedStiffness * normalizedStiffness - 2.0 * sign * (normalizedMass * normalizedMassTarget + normalizedStiffness * normalizedStiffnessTarget); candidate.residual = std::max(0.0, candidate.residual); return candidate; }; Candidate best; for (const double sign : {-1.0, 1.0}) { const double unconstrainedMass = sign * (normalizedMassTarget - correlation * normalizedStiffnessTarget) / relativeDeterminant / massNorm; const double unconstrainedStiffness = sign * (normalizedStiffnessTarget - correlation * normalizedMassTarget) / relativeDeterminant / stiffnessNorm; if (unconstrainedMass >= minimumMassCoefficient && unconstrainedStiffness >= 0.0) { const Candidate candidate = evaluate(sign, unconstrainedMass, unconstrainedStiffness); if (candidate.residual < best.residual) { best = candidate; } } const Candidate massOnly = evaluate(sign, std::max(minimumMassCoefficient, sign * normalizedMassTarget / massNorm), 0.0); if (massOnly.residual < best.residual) { best = massOnly; } const double boundaryStiffness = std::max( 0.0, (sign * normalizedStiffnessTarget - minimumMassCoefficient * massNorm * correlation) / stiffnessNorm ); const Candidate massFloor = evaluate(sign, minimumMassCoefficient, boundaryStiffness); if (massFloor.residual < best.residual) { best = massFloor; } } if (!std::isfinite(best.residual) || !std::isfinite(best.mass) || !std::isfinite(best.stiffness)) { throw std::runtime_error("The surface H1 calibration failed to produce a finite constrained fit."); } return { .target = configuration.calibration.target, .probeCount = configuration.calibration.probeCount, .sign = best.sign, .massCoefficient = best.mass, .stiffnessCoefficient = best.stiffness, .relativeResidual = std::sqrt(best.residual / equations.targetTarget), .relativeGramDeterminant = relativeDeterminant, .normalEquations = equations }; } } // namespace detail using CoupledMaterialSurfaceCharacteristics = OperatorCharacteristics< OperatorCategory::mixed, OperatorValueStructure::block, OperatorSymmetry::nonsymmetric, OperatorDefiniteness::unspecified, OperatorRepresentation::matrix_free, OperatorDistribution::distributed_true_dof, OperatorFESpace::product>; namespace backend { template < MaterialSurfaceDescriptor Descriptor, Registered MaterialBackend, Registered SurfaceBackend, MaterialSurfaceFactorizationPolicy Policy, MaterialSurfaceSurrogate SurfaceSurrogate = SurfaceMassDiagonal> requires Compatible && Compatible && Compatible::OperatorDescription> struct MaterialSurface final { using DescriptorType = Descriptor; using MaterialBackendType = MaterialBackend; using SurfaceBackendType = SurfaceBackend; using FactorizationPolicyType = Policy; using SurfaceSurrogateType = SurfaceSurrogate; }; template < MaterialSurfaceDescriptor Descriptor, Registered MaterialBackend, Registered SurfaceBackend, MaterialSurfaceFactorizationPolicy Policy, MaterialSurfaceSurrogate SurfaceSurrogate> requires Compatible && Compatible && Compatible::OperatorDescription> struct Traits> { static constexpr bool registered = true; static constexpr ApplicationContract applicationContract = ::mean_field::preconditioning::backend::applicationContract == ApplicationContract::stationary_linear && ::mean_field::preconditioning::backend::applicationContract == ApplicationContract::stationary_linear ? ApplicationContract::stationary_linear : ApplicationContract::flexible; static constexpr bool supportsSerialExecution = Traits::supportsSerialExecution && Traits::supportsSerialExecution; static constexpr bool supportsDistributedExecution = Traits::supportsDistributedExecution && Traits::supportsDistributedExecution; static constexpr SymmetryRequirement symmetryRequirement = SymmetryRequirement::none; static constexpr NullspaceRequirement nullspaceRequirement = NullspaceRequirement::none; static constexpr SurrogateRequirement surrogateRequirement = Traits::surrogateRequirement; static constexpr bool requiresAssembledSparseSurrogate = Traits::requiresAssembledSparseSurrogate; using PreparationDependencies = preconditioning::PreparationDependencies< PreparationDependency::discretization, PreparationDependency::geometry, PreparationDependency::equation_of_state, PreparationDependency::linearization>; template static constexpr bool supports = Characteristics::category == OperatorCategory::mixed && Characteristics::valueStructure == OperatorValueStructure::block && Characteristics::symmetry == OperatorSymmetry::nonsymmetric && Characteristics::representation == OperatorRepresentation::matrix_free && Characteristics::distribution == OperatorDistribution::distributed_true_dof && Characteristics::finiteElementSpace == OperatorFESpace::product; }; } // namespace backend template < ImplementedMaterialSurfaceDescriptor DescriptorT, backend::Registered MaterialBackendT, backend::Registered SurfaceBackendT, MaterialSurfaceFactorizationPolicy FactorizationPolicyT, MaterialSurfaceSurrogate SurfaceSurrogateT = SurfaceMassDiagonal> requires backend::Compatible && backend::Compatible && backend::Compatible< SurfaceBackendT, typename SurfaceSurrogateTraits::OperatorDescription> class MaterialSurfaceBlock final { public: using Descriptor = DescriptorT; using CorrectionBlocks = typename Descriptor::CorrectionBlocks; using ResidualBlocks = typename Descriptor::ResidualBlocks; using RequiredCouplings = typename Descriptor::RequiredCouplings; using OperatorDescription = CoupledMaterialSurfaceCharacteristics; using BackendType = backend:: MaterialSurface; using PreparationDependencies = typename backend::Traits::PreparationDependencies; using MaterialBackend = MaterialBackendT; using SurfaceBackend = SurfaceBackendT; using Factorization = FactorizationPolicyT; using SurfaceSurrogate = SurfaceSurrogateT; constexpr MaterialSurfaceBlock( MaterialBackendT materialBackend = {}, SurfaceBackendT surfaceBackend = {}, FactorizationPolicyT factorizationPolicy = {}, MaterialSurfaceDiagonalOptions diagonalOptions = {}, SurfaceSurrogateT surfaceSurrogate = {} ) : m_materialBackend(std::move(materialBackend)), m_surfaceBackend(std::move(surfaceBackend)), m_factorizationPolicy(std::move(factorizationPolicy)), m_diagonalOptions(diagonalOptions), m_surfaceSurrogate(std::move(surfaceSurrogate)) { } [[nodiscard]] constexpr const MaterialBackendT &materialBackend() const noexcept { return m_materialBackend; } [[nodiscard]] constexpr const SurfaceBackendT &surfaceBackend() const noexcept { return m_surfaceBackend; } [[nodiscard]] constexpr const FactorizationPolicyT &factorizationPolicy() const noexcept { return m_factorizationPolicy; } [[nodiscard]] constexpr const MaterialSurfaceDiagonalOptions &diagonalOptions() const noexcept { return m_diagonalOptions; } [[nodiscard]] constexpr const SurfaceSurrogateT &surfaceSurrogate() const noexcept { return m_surfaceSurrogate; } private: MaterialBackendT m_materialBackend; SurfaceBackendT m_surfaceBackend; FactorizationPolicyT m_factorizationPolicy; MaterialSurfaceDiagonalOptions m_diagonalOptions; SurfaceSurrogateT m_surfaceSurrogate; }; template < MaterialSurfacePreconditionerProblem Problem, backend::Registered MaterialBackend = backend::Diagonal, backend::Registered SurfaceBackend = backend::Diagonal, MaterialSurfaceFactorizationPolicy Policy = SurfaceThenMaterialTriangular> [[nodiscard]] constexpr auto materialSurfaceBlock( const Problem &, MaterialBackend materialBackend = {}, SurfaceBackend surfaceBackend = {}, Policy policy = {}, const MaterialSurfaceDiagonalOptions diagonalOptions = {} ) { using Descriptor = MaterialSurfaceDescriptorFor>; return MaterialSurfaceBlock{ std::move(materialBackend), std::move(surfaceBackend), std::move(policy), diagonalOptions }; } template < MaterialSurfacePreconditionerProblem Problem, backend::Registered MaterialBackend, backend::Registered SurfaceBackend, MaterialSurfaceFactorizationPolicy Policy, MaterialSurfaceSurrogate SurfaceSurrogate> [[nodiscard]] constexpr auto materialSurfaceBlock( const Problem &, MaterialBackend materialBackend, SurfaceBackend surfaceBackend, Policy policy, SurfaceSurrogate surfaceSurrogate, const MaterialSurfaceDiagonalOptions diagonalOptions = {} ) { using Descriptor = MaterialSurfaceDescriptorFor>; return MaterialSurfaceBlock{ std::move(materialBackend), std::move(surfaceBackend), std::move(policy), diagonalOptions, std::move(surfaceSurrogate) }; } class MaterialSurfaceJacobianOperator final : public mfem::Operator { public: explicit MaterialSurfaceJacobianOperator(const operators::PreparedStellarEquilibriumOperator &operation) : mfem::Operator(TotalSize(operation)), m_operation(std::addressof(operation)), m_offsets(4), m_zeroDensity(operation.GetBarotropicClosureOperator().GetDensitySize()), m_zeroGravity( operation.GetDisplacementOperator().GetGravityContext().GetGravityGradientMap().reduced_size() ), m_zeroPotential(operation.GetHydrostaticOperator().GetGravityPotentialMap().reduced_size()), m_zeroEnthalpy(operation.GetBarotropicClosureOperator().GetEnthalpySize()), m_volumeDisplacement(operation.GetDomainDeformation().volumeDisplacementSize()), m_mechanicalAction(operation.GetDomainDeformation().volumeDisplacementSize()), m_pullbackAction(operation.GetDomainDeformation().parameterCount()), m_fullDirection(operation.Width()), m_fullAction(operation.Height()) { m_offsets[0] = 0; m_offsets[1] = m_zeroDensity.Size(); m_offsets[2] = m_offsets[1] + operation.GetDomainDeformation().parameterCount(); m_offsets[3] = Height(); m_zeroDensity = 0.0; m_zeroGravity = 0.0; m_zeroPotential = 0.0; m_zeroEnthalpy = 0.0; } void Mult( const mfem::Vector &direction, mfem::Vector &action ) const override { VerifyVectors(direction, action); const auto densityDirection = ConstBlock(direction, 0); const auto surfaceDirection = ConstBlock(direction, 1); const auto enthalpyDirection = ConstBlock(direction, 2); auto densityAction = MutableBlock(action, 0); auto surfaceAction = MutableBlock(action, 1); auto enthalpyAction = MutableBlock(action, 2); // This operator is the diagnostic restriction R_material J // P_material, so construct it through the authoritative stellar // Jacobian. The block factorization below continues to use the // direct coupling actions and does not pay for a full Jacobian // application. m_fullDirection = 0.0; const auto fullDirectionView = m_operation->GetRootManifest().stateView(m_fullDirection); mfem::Vector fullDensityDirection = fullDirectionView.block(utils::blocks::density_field.mass_term); mfem::Vector fullSurfaceDirection = fullDirectionView.block(utils::blocks::surface_deformation_field.parameters_term); mfem::Vector fullEnthalpyDirection = fullDirectionView.block(utils::blocks::enthalpy_field.specific_term); fullDensityDirection = densityDirection; fullSurfaceDirection = surfaceDirection; fullEnthalpyDirection = enthalpyDirection; m_operation->Mult(m_fullDirection, m_fullAction); const auto fullActionView = m_operation->GetRootManifest().residualView(m_fullAction); const auto fullDensityAction = fullActionView.block(utils::blocks::density_field.mass_term); const auto fullSurfaceAction = fullActionView.block(utils::blocks::surface_deformation_field.shape_equilibrium_term); const auto fullEnthalpyAction = fullActionView.block(utils::blocks::enthalpy_field.specific_term); densityAction = fullDensityAction; surfaceAction = fullSurfaceAction; enthalpyAction = fullEnthalpyAction; } void ApplyEnthalpyToDensity( const mfem::Vector &enthalpy, mfem::Vector &densityAction ) const { VerifyBlock(enthalpy, 2, "enthalpy direction"); VerifyBlock(densityAction, 0, "density action"); m_operation->GetBarotropicClosureOperator().Mult( m_zeroDensity, enthalpy, ZeroVolumeDisplacement(), densityAction ); } void ApplySurfaceToMaterial( const mfem::Vector &surface, mfem::Vector &densityAction, mfem::Vector &enthalpyAction ) const { VerifyBlock(surface, 1, "surface direction"); VerifyBlock(densityAction, 0, "density action"); VerifyBlock(enthalpyAction, 2, "enthalpy action"); GenerateDisplacement(surface); m_operation->GetBarotropicClosureOperator().Mult( m_zeroDensity, m_zeroEnthalpy, m_volumeDisplacement, densityAction ); m_operation->GetHydrostaticOperator().ApplyDisplacementJacobianAction(m_volumeDisplacement, enthalpyAction); m_operation->GetSurfaceConstraintOperator().ApplyJacobianRows(m_zeroEnthalpy, enthalpyAction); } void ApplyMaterialToSurface( const mfem::Vector &density, const mfem::Vector &enthalpy, mfem::Vector &surfaceAction ) const { VerifyBlock(density, 0, "density direction"); VerifyBlock(enthalpy, 2, "enthalpy direction"); VerifyBlock(surfaceAction, 1, "surface action"); m_operation->GetDisplacementOperator().ApplyCompleteJacobianAction( density, ZeroVolumeDisplacement(), m_zeroGravity, enthalpy, m_mechanicalAction ); m_operation->GetDomainDeformation().applyJacobianTranspose( m_operation->GetSurfaceDeformationParameters(), m_mechanicalAction, surfaceAction ); } void ApplySurfaceToSurface( const mfem::Vector &surface, mfem::Vector &surfaceAction ) const { VerifyBlock(surface, 1, "surface direction"); VerifyBlock(surfaceAction, 1, "surface action"); GenerateDisplacement(surface); m_operation->GetDisplacementOperator().ApplyDisplacementJacobianAction( m_volumeDisplacement, m_mechanicalAction ); m_operation->GetDomainDeformation().applyJacobianTranspose( m_operation->GetSurfaceDeformationParameters(), m_mechanicalAction, surfaceAction ); m_operation->GetDomainDeformation().applyPullbackDerivative( m_operation->GetSurfaceDeformationParameters(), surface, m_operation->GetFullMechanicalResidual(), m_pullbackAction ); surfaceAction += m_pullbackAction; } [[nodiscard]] const mfem::Array &GetOffsets() const noexcept { return m_offsets; } private: [[nodiscard]] static int TotalSize(const operators::PreparedStellarEquilibriumOperator &operation) { if (!operation.IsPrepared()) { throw std::logic_error("The material-surface Jacobian requires a prepared stellar operator."); } return operation.GetBarotropicClosureOperator().GetDensitySize() + operation.GetDomainDeformation().parameterCount() + operation.GetBarotropicClosureOperator().GetEnthalpySize(); } [[nodiscard]] mfem::Vector ConstBlock( const mfem::Vector &vector, int block ) const { return mfem::Vector( const_cast(vector.GetData()) + m_offsets[block], m_offsets[block + 1] - m_offsets[block] ); } [[nodiscard]] mfem::Vector MutableBlock( mfem::Vector &vector, int block ) const { return mfem::Vector(vector.GetData() + m_offsets[block], m_offsets[block + 1] - m_offsets[block]); } void VerifyBlock( const mfem::Vector &vector, int block, const char *name ) const { if (vector.Size() != m_offsets[block + 1] - m_offsets[block]) { throw std::invalid_argument(std::string("The material-surface ") + name + " has the wrong size."); } } void VerifyVectors( const mfem::Vector &direction, const mfem::Vector &action ) const { if (direction.Size() != Width() || action.Size() != Height()) { throw std::invalid_argument("The material-surface Jacobian requires compatible, preallocated vectors."); } } void GenerateDisplacement(const mfem::Vector &surface) const { m_operation->GetDomainDeformation().applyJacobian( m_operation->GetSurfaceDeformationParameters(), surface, m_volumeDisplacement ); } [[nodiscard]] const mfem::Vector &ZeroVolumeDisplacement() const { m_volumeDisplacement = 0.0; return m_volumeDisplacement; } const operators::PreparedStellarEquilibriumOperator *m_operation; mfem::Array m_offsets; mfem::Vector m_zeroDensity; mfem::Vector m_zeroGravity; mfem::Vector m_zeroPotential; mfem::Vector m_zeroEnthalpy; mutable mfem::Vector m_volumeDisplacement; mutable mfem::Vector m_mechanicalAction; mutable mfem::Vector m_pullbackAction; mutable mfem::Vector m_fullDirection; mutable mfem::Vector m_fullAction; }; struct MaterialSurfaceFactorizationStatistics final { std::uint64_t applications{0}; std::uint64_t densityInverseApplications{0}; std::uint64_t surfaceInverseApplications{0}; std::uint64_t enthalpyInverseApplications{0}; std::uint64_t enthalpyToDensityApplications{0}; std::uint64_t surfaceToMaterialApplications{0}; std::uint64_t materialToSurfaceApplications{0}; }; template concept MaterialSurfaceCouplingOperator = requires( const Candidate &couplings, const mfem::Vector &density, const mfem::Vector &surface, const mfem::Vector &enthalpy, mfem::Vector &densityAction, mfem::Vector &surfaceAction, mfem::Vector &enthalpyAction ) { { couplings.Height() } -> std::same_as; { couplings.GetOffsets() } -> std::same_as &>; couplings.ApplyEnthalpyToDensity(enthalpy, densityAction); couplings.ApplySurfaceToMaterial(surface, densityAction, enthalpyAction); couplings.ApplyMaterialToSurface(density, enthalpy, surfaceAction); }; template < MaterialSurfaceFactorizationPolicy Policy, MaterialSurfaceCouplingOperator CouplingOperator = MaterialSurfaceJacobianOperator> class MaterialSurfaceFactorizationOperator final : public mfem::Solver { public: MaterialSurfaceFactorizationOperator( Policy policy, const mfem::Solver &densityInverse, const mfem::Solver &surfaceInverse, const mfem::Solver &enthalpyInverse, const CouplingOperator &couplings ) : mfem::Solver(couplings.Height()), m_policy(std::move(policy)), m_densityInverse(std::addressof(densityInverse)), m_surfaceInverse(std::addressof(surfaceInverse)), m_enthalpyInverse(std::addressof(enthalpyInverse)), m_couplings(std::addressof(couplings)), m_offsets(couplings.GetOffsets()), m_densityWorkspace(densityInverse.Height()), m_densityCoupling(densityInverse.Height()), m_surfaceWorkspace(surfaceInverse.Height()), m_enthalpyWorkspace(enthalpyInverse.Height()) { if (densityInverse.Height() != densityInverse.Width() || surfaceInverse.Height() != surfaceInverse.Width() || enthalpyInverse.Height() != enthalpyInverse.Width() || densityInverse.Height() != m_offsets[1] || surfaceInverse.Height() != m_offsets[2] - m_offsets[1] || enthalpyInverse.Height() != m_offsets[3] - m_offsets[2]) { throw std::invalid_argument("Material-surface inverse blocks do not match the coupled operator."); } } void SetOperator(const mfem::Operator &operation) override { if (operation.Height() != Height() || operation.Width() != Width()) { throw std::invalid_argument("The material-surface factorization received an incompatible operator."); } } void Mult( const mfem::Vector &rightHandSide, mfem::Vector &action ) const override { if (rightHandSide.Size() != Width() || action.Size() != Height()) { throw std::invalid_argument( "The material-surface factorization requires compatible, preallocated vectors." ); } const auto densityRightHandSide = ConstBlock(rightHandSide, 0); const auto surfaceRightHandSide = ConstBlock(rightHandSide, 1); const auto enthalpyRightHandSide = ConstBlock(rightHandSide, 2); auto densityAction = MutableBlock(action, 0); auto surfaceAction = MutableBlock(action, 1); auto enthalpyAction = MutableBlock(action, 2); if constexpr (std::same_as) { m_densityInverse->Mult(densityRightHandSide, densityAction); m_surfaceInverse->Mult(surfaceRightHandSide, surfaceAction); m_enthalpyInverse->Mult(enthalpyRightHandSide, enthalpyAction); } else if constexpr (std::same_as) { m_surfaceInverse->Mult(surfaceRightHandSide, surfaceAction); m_couplings->ApplySurfaceToMaterial(surfaceAction, m_densityWorkspace, m_enthalpyWorkspace); m_enthalpyWorkspace *= -1.0; m_enthalpyWorkspace += enthalpyRightHandSide; m_enthalpyInverse->Mult(m_enthalpyWorkspace, enthalpyAction); m_couplings->ApplyEnthalpyToDensity(enthalpyAction, m_densityCoupling); m_densityWorkspace += m_densityCoupling; m_densityWorkspace *= -1.0; m_densityWorkspace += densityRightHandSide; m_densityInverse->Mult(m_densityWorkspace, densityAction); ++m_statistics.surfaceToMaterialApplications; ++m_statistics.enthalpyToDensityApplications; } else if constexpr (std::same_as) { // Form m_0 = M^{-1} b_m with the upper-triangular material // inverse, where m = (rho, h). m_enthalpyInverse->Mult(enthalpyRightHandSide, enthalpyAction); m_couplings->ApplyEnthalpyToDensity(enthalpyAction, m_densityWorkspace); m_densityWorkspace *= -1.0; m_densityWorkspace += densityRightHandSide; m_densityInverse->Mult(m_densityWorkspace, densityAction); // Apply the surface inverse to b_q - A_qm m_0. The surface // surrogate is an approximation to the resulting Schur // complement rather than merely to A_qq. m_couplings->ApplyMaterialToSurface(densityAction, enthalpyAction, m_surfaceWorkspace); m_surfaceWorkspace *= -1.0; m_surfaceWorkspace += surfaceRightHandSide; m_surfaceInverse->Mult(m_surfaceWorkspace, surfaceAction); // Recover m = M^{-1}(b_m - A_mq q). Recomputing the material // solve is algebraically equivalent to the conventional LDU // correction m_0 - M^{-1} A_mq q. m_couplings->ApplySurfaceToMaterial(surfaceAction, m_densityWorkspace, m_enthalpyWorkspace); m_enthalpyWorkspace *= -1.0; m_enthalpyWorkspace += enthalpyRightHandSide; m_enthalpyInverse->Mult(m_enthalpyWorkspace, enthalpyAction); m_couplings->ApplyEnthalpyToDensity(enthalpyAction, m_densityCoupling); m_densityWorkspace += m_densityCoupling; m_densityWorkspace *= -1.0; m_densityWorkspace += densityRightHandSide; m_densityInverse->Mult(m_densityWorkspace, densityAction); ++m_statistics.materialToSurfaceApplications; ++m_statistics.surfaceToMaterialApplications; m_statistics.enthalpyToDensityApplications += 2; ++m_statistics.densityInverseApplications; ++m_statistics.enthalpyInverseApplications; } else { m_enthalpyInverse->Mult(enthalpyRightHandSide, enthalpyAction); m_couplings->ApplyEnthalpyToDensity(enthalpyAction, m_densityWorkspace); m_densityWorkspace *= -1.0; m_densityWorkspace += densityRightHandSide; m_densityInverse->Mult(m_densityWorkspace, densityAction); ++m_statistics.enthalpyToDensityApplications; if constexpr (std::same_as) { m_couplings->ApplyMaterialToSurface(densityAction, enthalpyAction, m_surfaceWorkspace); m_surfaceWorkspace *= -1.0; m_surfaceWorkspace += surfaceRightHandSide; m_surfaceInverse->Mult(m_surfaceWorkspace, surfaceAction); ++m_statistics.materialToSurfaceApplications; } else { static_assert(std::same_as); m_surfaceInverse->Mult(surfaceRightHandSide, surfaceAction); } } ++m_statistics.densityInverseApplications; ++m_statistics.surfaceInverseApplications; ++m_statistics.enthalpyInverseApplications; ++m_statistics.applications; } [[nodiscard]] const mfem::Array &GetOffsets() const noexcept { return m_offsets; } [[nodiscard]] const MaterialSurfaceFactorizationStatistics &GetStatistics() const noexcept { return m_statistics; } private: [[nodiscard]] mfem::Vector ConstBlock( const mfem::Vector &vector, int block ) const { return mfem::Vector( const_cast(vector.GetData()) + m_offsets[block], m_offsets[block + 1] - m_offsets[block] ); } [[nodiscard]] mfem::Vector MutableBlock( mfem::Vector &vector, int block ) const { return mfem::Vector(vector.GetData() + m_offsets[block], m_offsets[block + 1] - m_offsets[block]); } Policy m_policy; const mfem::Solver *m_densityInverse; const mfem::Solver *m_surfaceInverse; const mfem::Solver *m_enthalpyInverse; const CouplingOperator *m_couplings; mfem::Array m_offsets; mutable mfem::Vector m_densityWorkspace; mutable mfem::Vector m_densityCoupling; mutable mfem::Vector m_surfaceWorkspace; mutable mfem::Vector m_enthalpyWorkspace; mutable MaterialSurfaceFactorizationStatistics m_statistics; }; struct DiagonalPreparationQuality final { double minimumAbsoluteEntryBeforeRegularization{0.0}; double maximumAbsoluteEntryBeforeRegularization{0.0}; double appliedFloor{0.0}; std::uint64_t regularizedEntries{0}; }; struct SurfaceRieszCalibrationReport final { SurfaceRieszCalibrationTarget target{SurfaceRieszCalibrationTarget::none}; int probeCount{0}; SurfaceRieszCalibrationObjective objective{SurfaceRieszCalibrationObjective::operator_action}; double leastSquaresNumerator{0.0}; double leastSquaresDenominator{0.0}; double scale{1.0}; double inverseMultiplier{1.0}; [[nodiscard]] bool WasCalibrated() const noexcept { return target != SurfaceRieszCalibrationTarget::none; } }; namespace detail { struct SurfaceRieszScalarFit final { double surrogateScale{1.0}; double inverseMultiplier{1.0}; }; [[nodiscard]] inline SurfaceRieszScalarFit fitSurfaceRieszScalar( const double leastSquaresNumerator, const double leastSquaresDenominator, const SurfaceRieszCalibrationObjective objective ) { if (!std::isfinite(leastSquaresNumerator) || !std::isfinite(leastSquaresDenominator) || leastSquaresDenominator <= 0.0) { throw std::invalid_argument("Surface Riesz scalar calibration requires finite, nondegenerate data."); } const double fittedMultiplier = leastSquaresNumerator / leastSquaresDenominator; if (!std::isfinite(fittedMultiplier) || fittedMultiplier == 0.0) { throw std::runtime_error("Surface Riesz scalar calibration produced a zero or non-finite multiplier."); } const double surrogateScale = objective == SurfaceRieszCalibrationObjective::operator_action ? fittedMultiplier : 1.0 / fittedMultiplier; if (!std::isfinite(surrogateScale) || surrogateScale == 0.0) { throw std::runtime_error("Surface Riesz scalar calibration produced a zero or non-finite scale."); } return {.surrogateScale = surrogateScale, .inverseMultiplier = 1.0 / surrogateScale}; } } // namespace detail struct MaterialSurfaceBlockPreparationReport final { bool linearizationChanged{false}; bool rebuiltDensityInverse{false}; bool rebuiltSurfaceInverse{false}; bool rebuiltEnthalpyInverse{false}; DiagonalPreparationQuality densityDiagonal; DiagonalPreparationQuality surfaceDiagonal; DiagonalPreparationQuality enthalpyDiagonal; [[nodiscard]] bool DidAnyWork() const noexcept { return rebuiltDensityInverse || rebuiltSurfaceInverse || rebuiltEnthalpyInverse; } }; struct PreparedMaterialSurfaceBlockStatistics final { std::uint64_t setups{0}; std::uint64_t refreshChecks{0}; std::uint64_t refreshes{0}; std::uint64_t noOpRefreshes{0}; std::uint64_t surfaceJacobianProbes{0}; std::uint64_t surfaceRieszAssemblies{0}; std::uint64_t surfaceH1Assemblies{0}; }; template requires MaterialSurfaceRuntimeFor class PreparedMaterialSurfaceBlock final : public mfem::Solver { public: using Block = MaterialSurfaceBlock; PreparedMaterialSurfaceBlock( const operators::PreparedStellarEquilibriumOperator &operation, Block block ) : mfem::Solver(MaterialSurfaceJacobianOperator(operation).Height()), m_block(std::move(block)), m_operation(std::addressof(operation)), m_couplings(operation), m_densityDiagonal(AssembleDensityDiagonal(operation)), m_surfaceDiagonal(AssembleSurfaceRieszDiagonal(operation)), m_enthalpyDiagonal(AssembleEnthalpyDiagonal(operation)), m_densityQuality(Regularize( m_densityDiagonal, m_block.diagonalOptions(), Communicator(operation) )), m_surfaceQuality(Regularize( m_surfaceDiagonal, m_block.diagonalOptions(), Communicator(operation) )), m_enthalpyQuality(Regularize( m_enthalpyDiagonal, m_block.diagonalOptions(), Communicator(operation) )), m_densityInverse( m_block.materialBackend(), m_densityDiagonal ), m_surfaceInverse( m_block.surfaceBackend(), m_surfaceDiagonal ), m_enthalpyInverse( m_block.materialBackend(), m_enthalpyDiagonal ), m_factorization( m_block.factorizationPolicy(), m_densityInverse, m_surfaceInverse, m_enthalpyInverse, m_couplings ), m_dependencies(operation.GetDependencies()) { m_statistics.setups = 1; m_statistics.surfaceRieszAssemblies = 1; m_surfaceCalibration = CalibrateSurfaceRiesz(); } PreparedMaterialSurfaceBlock(const PreparedMaterialSurfaceBlock &) = delete; PreparedMaterialSurfaceBlock &operator=(const PreparedMaterialSurfaceBlock &) = delete; PreparedMaterialSurfaceBlock(PreparedMaterialSurfaceBlock &&) = delete; PreparedMaterialSurfaceBlock &operator=(PreparedMaterialSurfaceBlock &&) = delete; void SetOperator(const mfem::Operator &operation) override { m_factorization.SetOperator(operation); } void Mult( const mfem::Vector &rightHandSide, mfem::Vector &action ) const override { if (!IsCurrent()) { throw std::logic_error("The material-surface block is stale; refresh it before application."); } m_factorization.Mult(rightHandSide, action); } [[nodiscard]] bool IsCurrent() const noexcept { return m_operation->IsPrepared() && m_operation->GetDependencies() == m_dependencies; } [[nodiscard]] MaterialSurfaceBlockPreparationReport Refresh(const operators::PreparedStellarEquilibriumOperator &operation) { if (std::addressof(operation) != m_operation) { throw std::invalid_argument("A material-surface block cannot change stellar-operator identity."); } if (!operation.IsPrepared()) { throw std::logic_error("A material-surface block cannot refresh from an unprepared operator."); } ++m_statistics.refreshChecks; MaterialSurfaceBlockPreparationReport report{ .linearizationChanged = operation.GetDependencies() != m_dependencies }; if (!report.linearizationChanged) { ++m_statistics.noOpRefreshes; return report; } const operators::StellarEquilibriumDependencies currentDependencies = operation.GetDependencies(); const bool geometryChanged = currentDependencies.discretization != m_dependencies.discretization || currentDependencies.surfaceDeformation != m_dependencies.surfaceDeformation; const bool calibratedSurface = m_block.diagonalOptions().surfaceCalibration.target != SurfaceRieszCalibrationTarget::none; if (geometryChanged || calibratedSurface) { m_densityDiagonal = AssembleDensityDiagonal(operation); m_surfaceDiagonal = AssembleSurfaceRieszDiagonal(operation); ++m_statistics.surfaceRieszAssemblies; m_enthalpyDiagonal = AssembleEnthalpyDiagonal(operation); m_densityQuality = Regularize(m_densityDiagonal, m_block.diagonalOptions(), Communicator(operation)); m_surfaceQuality = Regularize(m_surfaceDiagonal, m_block.diagonalOptions(), Communicator(operation)); m_enthalpyQuality = Regularize(m_enthalpyDiagonal, m_block.diagonalOptions(), Communicator(operation)); m_densityInverse.Refresh(m_densityDiagonal); // Calibration always starts from the newly assembled, // unscaled surface mass inverse. This is required by the // right-preconditioned objective and also prevents repeated // refreshes from compounding the previous calibration scale. m_surfaceInverse.Refresh(m_surfaceDiagonal); m_enthalpyInverse.Refresh(m_enthalpyDiagonal); if (calibratedSurface) { m_surfaceCalibration = CalibrateSurfaceRiesz(); } report.rebuiltDensityInverse = true; report.rebuiltSurfaceInverse = true; report.rebuiltEnthalpyInverse = true; report.densityDiagonal = m_densityQuality; report.surfaceDiagonal = m_surfaceQuality; report.enthalpyDiagonal = m_enthalpyQuality; ++m_statistics.refreshes; } else { ++m_statistics.noOpRefreshes; } m_dependencies = currentDependencies; return report; } [[nodiscard]] const MaterialSurfaceJacobianOperator &GetCoupledOperator() const noexcept { return m_couplings; } [[nodiscard]] const MaterialSurfaceFactorizationOperator &GetFactorization() const noexcept { return m_factorization; } [[nodiscard]] const mfem::Vector &GetDensityDiagonal() const noexcept { return m_densityDiagonal; } [[nodiscard]] const mfem::Vector &GetSurfaceDiagonal() const noexcept { return m_surfaceDiagonal; } [[nodiscard]] const mfem::Vector &GetEnthalpyDiagonal() const noexcept { return m_enthalpyDiagonal; } [[nodiscard]] const DiagonalPreparationQuality &GetDensityDiagonalQuality() const noexcept { return m_densityQuality; } [[nodiscard]] const DiagonalPreparationQuality &GetSurfaceDiagonalQuality() const noexcept { return m_surfaceQuality; } [[nodiscard]] const SurfaceRieszCalibrationReport &GetSurfaceCalibration() const noexcept { return m_surfaceCalibration; } [[nodiscard]] const DiagonalPreparationQuality &GetEnthalpyDiagonalQuality() const noexcept { return m_enthalpyQuality; } [[nodiscard]] const PreparedMaterialSurfaceBlockStatistics &GetStatistics() const noexcept { return m_statistics; } private: [[nodiscard]] static MPI_Comm Communicator(const operators::PreparedStellarEquilibriumOperator &operation) { return operation.GetHydrostaticOperator().GetFEM().mesh->GetComm(); } [[nodiscard]] static mfem::Vector AssembleDensityDiagonal(const operators::PreparedStellarEquilibriumOperator &operation) { mfem::Vector diagonal; operation.GetBarotropicClosureOperator().AssembleDensityJacobianDiagonal(diagonal); return diagonal; } [[nodiscard]] static mfem::Vector AssembleEnthalpyDiagonal(const operators::PreparedStellarEquilibriumOperator &operation) { mfem::Vector diagonal; operation.GetHydrostaticOperator().AssembleEnthalpyJacobianDiagonal(diagonal); mfem::Vector ones(diagonal.Size()); ones = 1.0; operation.GetSurfaceConstraintOperator().ApplyJacobianRows(ones, diagonal); return diagonal; } [[nodiscard]] static mfem::Vector AssembleSurfaceRieszDiagonal(const operators::PreparedStellarEquilibriumOperator &operation) { using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; const fem::FEM &finiteElements = operation.GetHydrostaticOperator().GetFEM(); MFEM_VERIFY( finiteElements.mesh != nullptr && finiteElements.surfaceDeformationFes != nullptr, "The material/free-surface Riesz surrogate requires the surface finite-element space." ); mfem::Array stellarSurfaceMarker(finiteElements.mesh->bdr_attributes.Max()); stellarSurfaceMarker = 0; constexpr int stellarSurfaceAttribute = DomainSchema::template boundary_attribute(); MFEM_VERIFY( stellarSurfaceAttribute > 0 && stellarSurfaceAttribute <= stellarSurfaceMarker.Size(), "The stellar-surface boundary attribute is absent from the finite-element mesh." ); stellarSurfaceMarker[stellarSurfaceAttribute - 1] = 1; mfem::ParBilinearForm surfaceRiesz(finiteElements.surfaceDeformationFes.get()); surfaceRiesz.AddBoundaryIntegrator(new mfem::MassIntegrator(), stellarSurfaceMarker); surfaceRiesz.Assemble(); surfaceRiesz.Finalize(); std::unique_ptr surfaceRieszMatrix(surfaceRiesz.ParallelAssemble()); MFEM_VERIFY(surfaceRieszMatrix != nullptr, "The stellar-surface Riesz surrogate failed to assemble."); mfem::Vector ambientDiagonal; surfaceRieszMatrix->GetDiag(ambientDiagonal); const field::ScalarBoundaryDofMap surfaceMap = field::make_stellar_surface_scalar_dof_map(*finiteElements.surfaceDeformationFes); mfem::Vector diagonal = surfaceMap.gather(ambientDiagonal); MFEM_VERIFY( diagonal.Size() == operation.GetDomainDeformation().parameterCount(), "The stellar-surface Riesz diagonal does not match the deformation parameter space." ); return diagonal; } [[nodiscard]] SurfaceRieszCalibrationReport CalibrateSurfaceRiesz() { const SurfaceRieszCalibrationOptions calibration = m_block.diagonalOptions().surfaceCalibration; if (calibration.target == SurfaceRieszCalibrationTarget::none) { if (calibration.probeCount != 0 || calibration.objective != SurfaceRieszCalibrationObjective::operator_action) { throw std::invalid_argument( "An uncalibrated surface Riesz surrogate must request zero probes and the default objective." ); } return {}; } if (calibration.probeCount <= 0) { throw std::invalid_argument("Surface Riesz calibration requires at least one deterministic probe."); } using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; const fem::FEM &finiteElements = m_operation->GetHydrostaticOperator().GetFEM(); const field::ScalarBoundaryDofMap surfaceMap = field::make_stellar_surface_scalar_dof_map(*finiteElements.surfaceDeformationFes); if (surfaceMap.local_size() != m_surfaceDiagonal.Size()) { throw std::logic_error("Surface Riesz calibration received an incompatible boundary coordinate map."); } mfem::Vector probe(m_surfaceDiagonal.Size()); mfem::Vector rieszAction(m_surfaceDiagonal.Size()); mfem::Vector targetAction(m_surfaceDiagonal.Size()); mfem::Vector baseInverseAction(m_surfaceDiagonal.Size()); mfem::Vector materialFeedback(m_surfaceDiagonal.Size()); mfem::Vector densityRightHandSide(m_densityDiagonal.Size()); mfem::Vector densityCorrection(m_densityDiagonal.Size()); mfem::Vector densityCoupling(m_densityDiagonal.Size()); mfem::Vector enthalpyRightHandSide(m_enthalpyDiagonal.Size()); mfem::Vector enthalpyCorrection(m_enthalpyDiagonal.Size()); double localNumerator = 0.0; double localDenominator = 0.0; for (int sample = 0; sample < calibration.probeCount; ++sample) { for (int index = 0; index < probe.Size(); ++index) { std::uint64_t value = static_cast(surfaceMap.global_boundary_dof(index)); value += 0x9e3779b97f4a7c15ULL * static_cast(sample + 1); value = (value ^ (value >> 30U)) * 0xbf58476d1ce4e5b9ULL; value = (value ^ (value >> 27U)) * 0x94d049bb133111ebULL; value ^= value >> 31U; probe(index) = (value & 1ULL) == 0ULL ? -1.0 : 1.0; rieszAction(index) = m_surfaceDiagonal(index) * probe(index); } if (calibration.objective == SurfaceRieszCalibrationObjective::right_preconditioned_action) { m_surfaceInverse.Mult(probe, baseInverseAction); } const mfem::Vector &targetDirection = calibration.objective == SurfaceRieszCalibrationObjective::operator_action ? probe : baseInverseAction; m_couplings.ApplySurfaceToSurface(targetDirection, targetAction); if (calibration.target == SurfaceRieszCalibrationTarget::approximate_material_schur) { m_couplings.ApplySurfaceToMaterial(targetDirection, densityRightHandSide, enthalpyRightHandSide); m_enthalpyInverse.Mult(enthalpyRightHandSide, enthalpyCorrection); m_couplings.ApplyEnthalpyToDensity(enthalpyCorrection, densityCoupling); densityRightHandSide -= densityCoupling; m_densityInverse.Mult(densityRightHandSide, densityCorrection); m_couplings.ApplyMaterialToSurface(densityCorrection, enthalpyCorrection, materialFeedback); targetAction -= materialFeedback; } if (calibration.objective == SurfaceRieszCalibrationObjective::operator_action) { // Fit s M q ~= T q. The stored surface surrogate is s M. localNumerator += rieszAction * targetAction; localDenominator += rieszAction * rieszAction; } else { // Fit alpha T M^{-1} q ~= q. Since the stored surrogate is // s M, its inverse multiplier is alpha = 1 / s. This is the // surface factor in the right-preconditioned product. localNumerator += targetAction * probe; localDenominator += targetAction * targetAction; } } const MPI_Comm communicator = Communicator(*m_operation); double globalNumerator = 0.0; double globalDenominator = 0.0; MPI_Allreduce(&localNumerator, &globalNumerator, 1, MPI_DOUBLE, MPI_SUM, communicator); MPI_Allreduce(&localDenominator, &globalDenominator, 1, MPI_DOUBLE, MPI_SUM, communicator); if (!std::isfinite(globalNumerator) || !std::isfinite(globalDenominator) || globalDenominator <= 0.0) { throw std::runtime_error("Surface Riesz calibration produced an invalid least-squares problem."); } const detail::SurfaceRieszScalarFit fit = detail::fitSurfaceRieszScalar(globalNumerator, globalDenominator, calibration.objective); m_surfaceDiagonal *= fit.surrogateScale; m_surfaceQuality = Regularize(m_surfaceDiagonal, m_block.diagonalOptions(), communicator); m_surfaceInverse.Refresh(m_surfaceDiagonal); m_statistics.surfaceJacobianProbes += static_cast(calibration.probeCount); return { .target = calibration.target, .probeCount = calibration.probeCount, .objective = calibration.objective, .leastSquaresNumerator = globalNumerator, .leastSquaresDenominator = globalDenominator, .scale = fit.surrogateScale, .inverseMultiplier = fit.inverseMultiplier }; } [[nodiscard]] static DiagonalPreparationQuality Regularize( mfem::Vector &diagonal, const MaterialSurfaceDiagonalOptions options, const MPI_Comm communicator ) { if (!std::isfinite(options.relativeFloor) || options.relativeFloor < 0.0 || !std::isfinite(options.absoluteFloor) || options.absoluteFloor <= 0.0) { throw std::invalid_argument("Material-surface diagonal floors must be finite and nonnegative."); } double localMaximum = 0.0; double localMinimum = std::numeric_limits::infinity(); for (int index = 0; index < diagonal.Size(); ++index) { if (!std::isfinite(diagonal(index))) { throw std::invalid_argument("A material-surface diagonal contains a non-finite entry."); } const double magnitude = std::abs(diagonal(index)); localMaximum = std::max(localMaximum, magnitude); localMinimum = std::min(localMinimum, magnitude); } double globalMaximum = 0.0; double globalMinimum = 0.0; MPI_Allreduce(&localMaximum, &globalMaximum, 1, MPI_DOUBLE, MPI_MAX, communicator); MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, communicator); const double floor = std::max(options.absoluteFloor, options.relativeFloor * globalMaximum); std::uint64_t localRegularized = 0; for (int index = 0; index < diagonal.Size(); ++index) { if (std::abs(diagonal(index)) < floor) { diagonal(index) = std::copysign(floor, diagonal(index) == 0.0 ? 1.0 : diagonal(index)); ++localRegularized; } } std::uint64_t globalRegularized = 0; MPI_Allreduce(&localRegularized, &globalRegularized, 1, MPI_UINT64_T, MPI_SUM, communicator); return { .minimumAbsoluteEntryBeforeRegularization = globalMinimum, .maximumAbsoluteEntryBeforeRegularization = globalMaximum, .appliedFloor = floor, .regularizedEntries = globalRegularized }; } Block m_block; const operators::PreparedStellarEquilibriumOperator *m_operation; MaterialSurfaceJacobianOperator m_couplings; mfem::Vector m_densityDiagonal; mfem::Vector m_surfaceDiagonal; mfem::Vector m_enthalpyDiagonal; DiagonalPreparationQuality m_densityQuality; DiagonalPreparationQuality m_surfaceQuality; DiagonalPreparationQuality m_enthalpyQuality; SurfaceRieszCalibrationReport m_surfaceCalibration; backend::PreparedDiagonal m_densityInverse; backend::PreparedDiagonal m_surfaceInverse; backend::PreparedDiagonal m_enthalpyInverse; MaterialSurfaceFactorizationOperator m_factorization; operators::StellarEquilibriumDependencies m_dependencies; PreparedMaterialSurfaceBlockStatistics m_statistics; }; // The AMG backend acts on the ambient scalar H1 true-DOF space. Surface // deformation parameters, however, contain only the locally owned trace // DOFs. This adapter is the sole conversion point between those two // coordinate systems. The sign is deliberately applied outside AMG so the // sparse surrogate supplied to BoomerAMG remains positive definite. class SignedScalarBoundarySolverAdapter final : public mfem::Solver { public: SignedScalarBoundarySolverAdapter( const mfem::Solver &ambientSolver, field::ScalarBoundaryDofMap surfaceMap, const double sign ) : mfem::Solver(surfaceMap.local_size()), m_ambientSolver(std::addressof(ambientSolver)), m_surfaceMap(std::move(surfaceMap)), m_ambientRightHandSide(m_surfaceMap.volume_true_dof_size()), m_ambientAction(m_surfaceMap.volume_true_dof_size()) { if (ambientSolver.Height() != m_surfaceMap.volume_true_dof_size() || ambientSolver.Width() != m_surfaceMap.volume_true_dof_size()) { throw std::invalid_argument("The surface AMG solver is incompatible with its ambient H1 space."); } SetSign(sign); } void SetOperator(const mfem::Operator &operation) override { if (operation.Height() != Height() || operation.Width() != Width()) { throw std::invalid_argument("The surface trace solver received an operator of incompatible size."); } } void SetSign(const double sign) { if (sign != -1.0 && sign != 1.0) { throw std::invalid_argument("The fitted surface-operator sign must be exactly -1 or +1."); } m_sign = sign; } void Mult( const mfem::Vector &rightHandSide, mfem::Vector &action ) const override { if (rightHandSide.Size() != Width() || action.Size() != Height()) { throw std::invalid_argument("The surface trace solver received incompatible vectors."); } m_surfaceMap.scatter(rightHandSide, m_ambientRightHandSide); m_ambientSolver->Mult(m_ambientRightHandSide, m_ambientAction); m_surfaceMap.gather(m_ambientAction, action); action *= m_sign; } [[nodiscard]] double GetSign() const noexcept { return m_sign; } [[nodiscard]] const field::ScalarBoundaryDofMap &GetSurfaceMap() const noexcept { return m_surfaceMap; } private: const mfem::Solver *m_ambientSolver; field::ScalarBoundaryDofMap m_surfaceMap; double m_sign{1.0}; mutable mfem::Vector m_ambientRightHandSide; mutable mfem::Vector m_ambientAction; }; template < MaterialSurfaceDescriptor Descriptor, MaterialSurfaceFactorizationPolicy Policy, backend::ApplicationMode Mode> requires MaterialSurfaceRuntimeFor class PreparedH1MaterialSurfaceBlock final : public mfem::Solver { public: using SurfaceBackend = backend::HypreBoomerAMG; using Block = MaterialSurfaceBlock; PreparedH1MaterialSurfaceBlock( const operators::PreparedStellarEquilibriumOperator &operation, Block block ) : mfem::Solver(MaterialSurfaceJacobianOperator(operation).Height()), m_block(std::move(block)), m_operation(std::addressof(operation)), m_couplings(operation), m_surfaceMap(SurfaceMap(operation)), m_densityDiagonal(AssembleDensityDiagonal(operation)), m_enthalpyDiagonal(AssembleEnthalpyDiagonal(operation)), m_dependencies(operation.GetDependencies()) { ValidateConfiguration(m_block); m_densityQuality = Regularize(m_densityDiagonal, m_block.diagonalOptions(), Communicator(operation)); m_enthalpyQuality = Regularize(m_enthalpyDiagonal, m_block.diagonalOptions(), Communicator(operation)); m_densityInverse = std::make_unique(m_block.materialBackend(), m_densityDiagonal); m_enthalpyInverse = std::make_unique(m_block.materialBackend(), m_enthalpyDiagonal); m_surfaceMass = AssembleSurfaceOperator(operation, 1.0, 0.0, false); m_surfaceStiffness = AssembleSurfaceOperator(operation, 0.0, 1.0, false); m_surfaceFit = FitSurfaceOperator(); m_surfaceSurrogate = AssembleSurfaceOperator( operation, m_surfaceFit.massCoefficient, m_surfaceFit.stiffnessCoefficient, true ); m_surfaceInverse = std::make_unique>(m_block.surfaceBackend(), *m_surfaceSurrogate); RebindFactorization(); m_statistics.setups = 1; m_statistics.surfaceJacobianProbes = static_cast(m_block.surfaceSurrogate().calibration.probeCount); m_statistics.surfaceH1Assemblies = 3; } PreparedH1MaterialSurfaceBlock(const PreparedH1MaterialSurfaceBlock &) = delete; PreparedH1MaterialSurfaceBlock &operator=(const PreparedH1MaterialSurfaceBlock &) = delete; PreparedH1MaterialSurfaceBlock(PreparedH1MaterialSurfaceBlock &&) = delete; PreparedH1MaterialSurfaceBlock &operator=(PreparedH1MaterialSurfaceBlock &&) = delete; void SetOperator(const mfem::Operator &operation) override { m_factorization->SetOperator(operation); } void Mult( const mfem::Vector &rightHandSide, mfem::Vector &action ) const override { if (!IsCurrent()) { throw std::logic_error("The H1 material-surface block is stale; refresh it before application."); } m_factorization->Mult(rightHandSide, action); } [[nodiscard]] bool IsCurrent() const noexcept { return m_operation->IsPrepared() && m_operation->GetDependencies() == m_dependencies; } [[nodiscard]] MaterialSurfaceBlockPreparationReport Refresh(const operators::PreparedStellarEquilibriumOperator &operation) { if (std::addressof(operation) != m_operation) { throw std::invalid_argument("An H1 material-surface block cannot change stellar-operator identity."); } if (!operation.IsPrepared()) { throw std::logic_error("An H1 material-surface block cannot refresh from an unprepared operator."); } ++m_statistics.refreshChecks; MaterialSurfaceBlockPreparationReport report{ .linearizationChanged = operation.GetDependencies() != m_dependencies }; if (!report.linearizationChanged) { ++m_statistics.noOpRefreshes; return report; } const operators::StellarEquilibriumDependencies currentDependencies = operation.GetDependencies(); const bool geometryChanged = currentDependencies.discretization != m_dependencies.discretization || currentDependencies.surfaceDeformation != m_dependencies.surfaceDeformation; m_densityDiagonal = AssembleDensityDiagonal(operation); m_enthalpyDiagonal = AssembleEnthalpyDiagonal(operation); m_densityQuality = Regularize(m_densityDiagonal, m_block.diagonalOptions(), Communicator(operation)); m_enthalpyQuality = Regularize(m_enthalpyDiagonal, m_block.diagonalOptions(), Communicator(operation)); m_densityInverse->Refresh(m_densityDiagonal); m_enthalpyInverse->Refresh(m_enthalpyDiagonal); if (geometryChanged) { field::ScalarBoundaryDofMap currentSurfaceMap = SurfaceMap(operation); if (currentSurfaceMap.local_size() != m_surfaceMap.local_size() || currentSurfaceMap.volume_true_dof_size() != m_surfaceMap.volume_true_dof_size()) { throw std::invalid_argument( "An H1 material-surface block cannot change discretization size during refresh." ); } m_surfaceMap = std::move(currentSurfaceMap); m_surfaceMass = AssembleSurfaceOperator(operation, 1.0, 0.0, false); m_surfaceStiffness = AssembleSurfaceOperator(operation, 0.0, 1.0, false); m_statistics.surfaceH1Assemblies += 2; } m_surfaceFit = FitSurfaceOperator(); auto refreshedSurrogate = AssembleSurfaceOperator( operation, m_surfaceFit.massCoefficient, m_surfaceFit.stiffnessCoefficient, true ); m_surfaceInverse->Refresh(*refreshedSurrogate); m_surfaceSurrogate = std::move(refreshedSurrogate); RebindFactorization(); m_statistics.surfaceJacobianProbes += static_cast(m_block.surfaceSurrogate().calibration.probeCount); ++m_statistics.surfaceH1Assemblies; ++m_statistics.refreshes; report.rebuiltDensityInverse = true; report.rebuiltSurfaceInverse = true; report.rebuiltEnthalpyInverse = true; report.densityDiagonal = m_densityQuality; report.enthalpyDiagonal = m_enthalpyQuality; m_dependencies = currentDependencies; return report; } [[nodiscard]] const MaterialSurfaceJacobianOperator &GetCoupledOperator() const noexcept { return m_couplings; } [[nodiscard]] const MaterialSurfaceFactorizationOperator &GetFactorization() const noexcept { return *m_factorization; } [[nodiscard]] const SurfaceH1FitReport &GetSurfaceFit() const noexcept { return m_surfaceFit; } [[nodiscard]] const mfem::HypreParMatrix &GetSurfaceMassMatrix() const noexcept { return *m_surfaceMass; } [[nodiscard]] const mfem::HypreParMatrix &GetSurfaceStiffnessMatrix() const noexcept { return *m_surfaceStiffness; } [[nodiscard]] const mfem::HypreParMatrix &GetSurfaceSurrogateMatrix() const noexcept { return *m_surfaceSurrogate; } [[nodiscard]] const SignedScalarBoundarySolverAdapter &GetSurfaceInverse() const noexcept { return *m_surfaceBoundaryInverse; } [[nodiscard]] const backend::PreparedHypreBoomerAMG &GetSurfaceBackend() const noexcept { return *m_surfaceInverse; } [[nodiscard]] const mfem::Vector &GetDensityDiagonal() const noexcept { return m_densityDiagonal; } [[nodiscard]] const mfem::Vector &GetEnthalpyDiagonal() const noexcept { return m_enthalpyDiagonal; } [[nodiscard]] const DiagonalPreparationQuality &GetDensityDiagonalQuality() const noexcept { return m_densityQuality; } [[nodiscard]] const DiagonalPreparationQuality &GetEnthalpyDiagonalQuality() const noexcept { return m_enthalpyQuality; } [[nodiscard]] const PreparedMaterialSurfaceBlockStatistics &GetStatistics() const noexcept { return m_statistics; } private: static void ValidateConfiguration(const Block &block) { const SurfaceRieszCalibrationOptions legacyCalibration = block.diagonalOptions().surfaceCalibration; if (legacyCalibration.target != SurfaceRieszCalibrationTarget::none || legacyCalibration.probeCount != 0) { throw std::invalid_argument( "Surface H1 calibration must be configured on SurfaceH1MassStiffness, not diagonal options." ); } const SurfaceH1MassStiffness &surface = block.surfaceSurrogate(); if (surface.calibration.target == SurfaceRieszCalibrationTarget::none || surface.calibration.probeCount < 3 || surface.calibration.probeCount > 64 || surface.calibration.objective != SurfaceRieszCalibrationObjective::operator_action || !std::isfinite(surface.relativeMassCoefficientFloor) || surface.relativeMassCoefficientFloor <= 0.0 || !std::isfinite(surface.gramRelativeTolerance) || surface.gramRelativeTolerance <= 0.0 || surface.gramRelativeTolerance >= 1.0) { throw std::invalid_argument( "SurfaceH1MassStiffness requires an operator-action target, 3--64 probes, and finite positive fit " "tolerances." ); } } [[nodiscard]] static MPI_Comm Communicator(const operators::PreparedStellarEquilibriumOperator &operation) { return operation.GetHydrostaticOperator().GetFEM().mesh->GetComm(); } [[nodiscard]] static field::ScalarBoundaryDofMap SurfaceMap(const operators::PreparedStellarEquilibriumOperator &operation) { using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; const fem::FEM &finiteElements = operation.GetHydrostaticOperator().GetFEM(); return field::make_stellar_surface_scalar_dof_map(*finiteElements.surfaceDeformationFes); } [[nodiscard]] static mfem::Vector AssembleDensityDiagonal(const operators::PreparedStellarEquilibriumOperator &operation) { mfem::Vector diagonal; operation.GetBarotropicClosureOperator().AssembleDensityJacobianDiagonal(diagonal); return diagonal; } [[nodiscard]] static mfem::Vector AssembleEnthalpyDiagonal(const operators::PreparedStellarEquilibriumOperator &operation) { mfem::Vector diagonal; operation.GetHydrostaticOperator().AssembleEnthalpyJacobianDiagonal(diagonal); mfem::Vector ones(diagonal.Size()); ones = 1.0; operation.GetSurfaceConstraintOperator().ApplyJacobianRows(ones, diagonal); return diagonal; } [[nodiscard]] static std::unique_ptr AssembleSurfaceOperator( const operators::PreparedStellarEquilibriumOperator &operation, const double massCoefficient, const double stiffnessCoefficient, const bool eliminateZeroRows ) { if (!std::isfinite(massCoefficient) || !std::isfinite(stiffnessCoefficient) || massCoefficient < 0.0 || stiffnessCoefficient < 0.0 || massCoefficient + stiffnessCoefficient <= 0.0) { throw std::invalid_argument("Surface H1 coefficients must be finite, nonnegative, and nonzero."); } using DomainSchema = utils::domain::CoreEnvelopeVacuumDomainSchema; const fem::FEM &finiteElements = operation.GetHydrostaticOperator().GetFEM(); MFEM_VERIFY( finiteElements.mesh != nullptr && finiteElements.surfaceDeformationFes != nullptr, "The surface H1 surrogate requires the ambient scalar finite-element space." ); mfem::Array stellarSurfaceMarker(finiteElements.mesh->bdr_attributes.Max()); stellarSurfaceMarker = 0; constexpr int stellarSurfaceAttribute = DomainSchema::template boundary_attribute(); MFEM_VERIFY( stellarSurfaceAttribute > 0 && stellarSurfaceAttribute <= stellarSurfaceMarker.Size(), "The stellar-surface boundary attribute is absent from the finite-element mesh." ); stellarSurfaceMarker[stellarSurfaceAttribute - 1] = 1; mfem::ConstantCoefficient massWeight(massCoefficient); mfem::ConstantCoefficient stiffnessWeight(stiffnessCoefficient); mfem::ParBilinearForm surfaceOperator(finiteElements.surfaceDeformationFes.get()); if (massCoefficient > 0.0) { surfaceOperator.AddBoundaryIntegrator(new mfem::MassIntegrator(massWeight), stellarSurfaceMarker); } if (stiffnessCoefficient > 0.0) { surfaceOperator.AddBoundaryIntegrator( new mfem::DiffusionIntegrator(stiffnessWeight), stellarSurfaceMarker ); } surfaceOperator.Assemble(); surfaceOperator.Finalize(); std::unique_ptr matrix(surfaceOperator.ParallelAssemble()); MFEM_VERIFY(matrix != nullptr, "The surface H1 surrogate failed to assemble."); if (eliminateZeroRows) { matrix->EliminateZeroRows(); } return matrix; } [[nodiscard]] static double Legendre12(const double coordinate) { double previous = 1.0; double current = coordinate; for (int degree = 2; degree <= 12; ++degree) { const double next = ((2.0 * static_cast(degree) - 1.0) * coordinate * current - (static_cast(degree) - 1.0) * previous) / static_cast(degree); previous = current; current = next; } return current; } void FillProbe( const int sample, mfem::Vector &probe ) const { const bool useOrderedMode = sample < 3; for (int index = 0; index < probe.Size(); ++index) { const long long globalDof = m_surfaceMap.global_boundary_dof(index); if (useOrderedMode) { const double coordinate = -1.0 + 2.0 * (static_cast(globalDof) + 0.5) / static_cast(m_surfaceMap.global_size()); const double mode = sample == 0 ? 1.0 : (sample == 1 ? 0.5 * (3.0 * coordinate * coordinate - 1.0) : Legendre12(coordinate)); probe(index) = mode; } else { std::uint64_t value = static_cast(globalDof); value += 0x9e3779b97f4a7c15ULL * static_cast(sample + 1); value = (value ^ (value >> 30U)) * 0xbf58476d1ce4e5b9ULL; value = (value ^ (value >> 27U)) * 0x94d049bb133111ebULL; value ^= value >> 31U; probe(index) = (value & 1ULL) == 0ULL ? -1.0 : 1.0; } } const double localNormSquared = probe * probe; double globalNormSquared = 0.0; MPI_Allreduce(&localNormSquared, &globalNormSquared, 1, MPI_DOUBLE, MPI_SUM, Communicator(*m_operation)); if (!std::isfinite(globalNormSquared) || globalNormSquared <= 0.0) { throw std::runtime_error("A deterministic surface H1 calibration probe has zero norm."); } probe /= std::sqrt(globalNormSquared); } void ApplyAmbientSurfaceOperator( const mfem::HypreParMatrix &surfaceOperator, const mfem::Vector &probe, mfem::Vector &action, mfem::Vector &ambientProbe, mfem::Vector &ambientAction ) const { m_surfaceMap.scatter(probe, ambientProbe); surfaceOperator.Mult(ambientProbe, ambientAction); m_surfaceMap.gather(ambientAction, action); } [[nodiscard]] SurfaceH1FitReport FitSurfaceOperator() { const SurfaceH1MassStiffness &configuration = m_block.surfaceSurrogate(); if (configuration.calibration.target == SurfaceRieszCalibrationTarget::none || configuration.calibration.probeCount < 3) { throw std::invalid_argument( "The surface H1 mass-plus-stiffness surrogate requires at least three calibration probes." ); } mfem::Vector probe(m_surfaceMap.local_size()); mfem::Vector massAction(m_surfaceMap.local_size()); mfem::Vector stiffnessAction(m_surfaceMap.local_size()); mfem::Vector targetAction(m_surfaceMap.local_size()); mfem::Vector materialFeedback(m_surfaceMap.local_size()); mfem::Vector ambientProbe(m_surfaceMap.volume_true_dof_size()); mfem::Vector ambientAction(m_surfaceMap.volume_true_dof_size()); mfem::Vector densityRightHandSide(m_densityDiagonal.Size()); mfem::Vector densityCorrection(m_densityDiagonal.Size()); mfem::Vector densityCoupling(m_densityDiagonal.Size()); mfem::Vector enthalpyRightHandSide(m_enthalpyDiagonal.Size()); mfem::Vector enthalpyCorrection(m_enthalpyDiagonal.Size()); SurfaceH1NormalEquations local; for (int sample = 0; sample < configuration.calibration.probeCount; ++sample) { FillProbe(sample, probe); ApplyAmbientSurfaceOperator(*m_surfaceMass, probe, massAction, ambientProbe, ambientAction); ApplyAmbientSurfaceOperator(*m_surfaceStiffness, probe, stiffnessAction, ambientProbe, ambientAction); m_couplings.ApplySurfaceToSurface(probe, targetAction); if (configuration.calibration.target == SurfaceRieszCalibrationTarget::approximate_material_schur) { m_couplings.ApplySurfaceToMaterial(probe, densityRightHandSide, enthalpyRightHandSide); m_enthalpyInverse->Mult(enthalpyRightHandSide, enthalpyCorrection); m_couplings.ApplyEnthalpyToDensity(enthalpyCorrection, densityCoupling); densityRightHandSide -= densityCoupling; m_densityInverse->Mult(densityRightHandSide, densityCorrection); m_couplings.ApplyMaterialToSurface(densityCorrection, enthalpyCorrection, materialFeedback); targetAction -= materialFeedback; } local.massMass += massAction * massAction; local.massStiffness += massAction * stiffnessAction; local.stiffnessStiffness += stiffnessAction * stiffnessAction; local.massTarget += massAction * targetAction; local.stiffnessTarget += stiffnessAction * targetAction; local.targetTarget += targetAction * targetAction; } std::array localValues{local.massMass, local.massStiffness, local.stiffnessStiffness, local.massTarget, local.stiffnessTarget, local.targetTarget}; std::array globalValues{}; MPI_Allreduce( localValues.data(), globalValues.data(), static_cast(globalValues.size()), MPI_DOUBLE, MPI_SUM, Communicator(*m_operation) ); const SurfaceH1NormalEquations global{ .massMass = globalValues[0], .massStiffness = globalValues[1], .stiffnessStiffness = globalValues[2], .massTarget = globalValues[3], .stiffnessTarget = globalValues[4], .targetTarget = globalValues[5] }; return detail::fitSurfaceH1Coefficients(global, configuration); } void RebindFactorization() { auto surfaceBoundaryInverse = std::make_unique(*m_surfaceInverse, m_surfaceMap, m_surfaceFit.sign); auto factorization = std::make_unique>( m_block.factorizationPolicy(), *m_densityInverse, *surfaceBoundaryInverse, *m_enthalpyInverse, m_couplings ); m_factorization = std::move(factorization); m_surfaceBoundaryInverse = std::move(surfaceBoundaryInverse); } [[nodiscard]] static DiagonalPreparationQuality Regularize( mfem::Vector &diagonal, const MaterialSurfaceDiagonalOptions options, const MPI_Comm communicator ) { if (!std::isfinite(options.relativeFloor) || options.relativeFloor < 0.0 || !std::isfinite(options.absoluteFloor) || options.absoluteFloor <= 0.0) { throw std::invalid_argument("Material-surface diagonal floors must be finite and nonnegative."); } double localMaximum = 0.0; double localMinimum = std::numeric_limits::infinity(); for (int index = 0; index < diagonal.Size(); ++index) { if (!std::isfinite(diagonal(index))) { throw std::invalid_argument("A material-surface diagonal contains a non-finite entry."); } const double magnitude = std::abs(diagonal(index)); localMaximum = std::max(localMaximum, magnitude); localMinimum = std::min(localMinimum, magnitude); } double globalMaximum = 0.0; double globalMinimum = 0.0; MPI_Allreduce(&localMaximum, &globalMaximum, 1, MPI_DOUBLE, MPI_MAX, communicator); MPI_Allreduce(&localMinimum, &globalMinimum, 1, MPI_DOUBLE, MPI_MIN, communicator); const double floor = std::max(options.absoluteFloor, options.relativeFloor * globalMaximum); std::uint64_t localRegularized = 0; for (int index = 0; index < diagonal.Size(); ++index) { if (std::abs(diagonal(index)) < floor) { diagonal(index) = std::copysign(floor, diagonal(index) == 0.0 ? 1.0 : diagonal(index)); ++localRegularized; } } std::uint64_t globalRegularized = 0; MPI_Allreduce(&localRegularized, &globalRegularized, 1, MPI_UINT64_T, MPI_SUM, communicator); return { .minimumAbsoluteEntryBeforeRegularization = globalMinimum, .maximumAbsoluteEntryBeforeRegularization = globalMaximum, .appliedFloor = floor, .regularizedEntries = globalRegularized }; } Block m_block; const operators::PreparedStellarEquilibriumOperator *m_operation; MaterialSurfaceJacobianOperator m_couplings; field::ScalarBoundaryDofMap m_surfaceMap; mfem::Vector m_densityDiagonal; mfem::Vector m_enthalpyDiagonal; DiagonalPreparationQuality m_densityQuality; DiagonalPreparationQuality m_enthalpyQuality; std::unique_ptr m_densityInverse; std::unique_ptr m_enthalpyInverse; std::unique_ptr m_surfaceMass; std::unique_ptr m_surfaceStiffness; std::unique_ptr m_surfaceSurrogate; SurfaceH1FitReport m_surfaceFit; std::unique_ptr> m_surfaceInverse; std::unique_ptr m_surfaceBoundaryInverse; std::unique_ptr> m_factorization; operators::StellarEquilibriumDependencies m_dependencies; PreparedMaterialSurfaceBlockStatistics m_statistics; }; template < MaterialSurfaceDescriptor Descriptor, MaterialSurfaceFactorizationPolicy Policy> requires MaterialSurfaceRuntimeFor [[nodiscard]] auto prepare( const operators::PreparedStellarEquilibriumOperator &operation, MaterialSurfaceBlock< Descriptor, backend::Diagonal, backend::Diagonal, Policy> block ) { return PreparedMaterialSurfaceBlock{operation, std::move(block)}; } template < equilibrium::StellarEquilibriumModel Model, equilibrium::StellarDiscretizationType Discretization, MaterialSurfaceFactorizationPolicy Policy> requires MaterialSurfacePreconditionerProblem< equilibrium::StellarEquilibriumProblem> [[nodiscard]] auto prepare( const equilibrium::StellarEquilibriumProblem &problem, MaterialSurfaceBlock< MaterialSurfaceDescriptorFor>, backend::Diagonal, backend::Diagonal, Policy> block ) { return prepare(problem.GetPhysicalOperator(), std::move(block)); } template < MaterialSurfaceDescriptor Descriptor, MaterialSurfaceFactorizationPolicy Policy, backend::ApplicationMode Mode> requires MaterialSurfaceRuntimeFor [[nodiscard]] auto prepare( const operators::PreparedStellarEquilibriumOperator &operation, MaterialSurfaceBlock< Descriptor, backend::Diagonal, backend::HypreBoomerAMG, Policy, SurfaceH1MassStiffness> block ) { return PreparedH1MaterialSurfaceBlock{operation, std::move(block)}; } template < equilibrium::StellarEquilibriumModel Model, equilibrium::StellarDiscretizationType Discretization, MaterialSurfaceFactorizationPolicy Policy, backend::ApplicationMode Mode> requires MaterialSurfacePreconditionerProblem< equilibrium::StellarEquilibriumProblem> [[nodiscard]] auto prepare( const equilibrium::StellarEquilibriumProblem &problem, MaterialSurfaceBlock< MaterialSurfaceDescriptorFor>, backend::Diagonal, backend::HypreBoomerAMG, Policy, SurfaceH1MassStiffness> block ) { return prepare(problem.GetPhysicalOperator(), std::move(block)); } } // namespace mean_field::preconditioning