Files
MeanField/libmeanfield/interface/preconditioning/material_surface.cppm
2026-09-06 10:15:00 -04:00

2179 lines
110 KiB
C++

module;
#include <algorithm>
#include <array>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <mfem.hpp>
#include <mpi.h>
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 <typename Candidate> struct IsMaterialSurfaceFactorizationPolicy : std::false_type { };
template <> struct IsMaterialSurfaceFactorizationPolicy<MaterialSurfaceBlockDiagonal> : std::true_type { };
template <> struct IsMaterialSurfaceFactorizationPolicy<CoupledMaterialIndependentSurface> : std::true_type { };
template <> struct IsMaterialSurfaceFactorizationPolicy<MaterialThenSurfaceTriangular> : std::true_type { };
template <> struct IsMaterialSurfaceFactorizationPolicy<SurfaceThenMaterialTriangular> : std::true_type { };
template <> struct IsMaterialSurfaceFactorizationPolicy<ApproximateMaterialSurfaceLDU> : std::true_type { };
template <typename Candidate>
concept MaterialSurfaceFactorizationPolicy =
IsMaterialSurfaceFactorizationPolicy<std::remove_cvref_t<Candidate>>::value;
namespace detail {
template <typename... Lists> struct MaterialSurfaceConcatenate;
template <> struct MaterialSurfaceConcatenate<> {
using Type = utils::blocks::type_list<>;
};
template <typename... Types> struct MaterialSurfaceConcatenate<utils::blocks::type_list<Types...>> {
using Type = utils::blocks::type_list<Types...>;
};
template <typename... Left, typename... Right, typename... Remaining>
struct MaterialSurfaceConcatenate<
utils::blocks::type_list<Left...>,
utils::blocks::type_list<Right...>,
Remaining...> {
using Type =
typename MaterialSurfaceConcatenate<utils::blocks::type_list<Left..., Right...>, Remaining...>::Type;
};
template <typename... Lists>
using MaterialSurfaceConcatenateT = typename MaterialSurfaceConcatenate<Lists...>::Type;
template <typename Equations, typename CarrierField> struct NonCarrierEquations;
template <typename CarrierField>
struct NonCarrierEquations<material::ThermodynamicEquationCatalog<>, CarrierField> {
using Type = material::ThermodynamicEquationCatalog<>;
};
template <typename First, typename... Remaining, typename CarrierField>
struct NonCarrierEquations<material::ThermodynamicEquationCatalog<First, Remaining...>, CarrierField> {
private:
using Tail =
typename NonCarrierEquations<material::ThermodynamicEquationCatalog<Remaining...>, CarrierField>::Type;
template <typename Head, typename List> struct Prepend;
template <typename Head, typename... TailEquations>
struct Prepend<Head, material::ThermodynamicEquationCatalog<TailEquations...>> {
using Type = material::ThermodynamicEquationCatalog<Head, TailEquations...>;
};
public:
using Type = std::conditional_t<
std::same_as<typename First::FieldType, CarrierField>,
Tail,
typename Prepend<First, Tail>::Type>;
};
template <typename Equations> struct EquationCorrectionBlocks;
template <typename... Equations>
struct EquationCorrectionBlocks<material::ThermodynamicEquationCatalog<Equations...>> {
using Type = utils::blocks::type_list<typename Equations::Correction...>;
};
template <typename Equations> struct EquationResidualBlocks;
template <typename... Equations>
struct EquationResidualBlocks<material::ThermodynamicEquationCatalog<Equations...>> {
using Type = utils::blocks::type_list<typename Equations::Residual...>;
};
template <typename Residual, typename Corrections, typename JacobianForm> struct CouplingsForResidual;
template <typename Residual, typename JacobianForm>
struct CouplingsForResidual<Residual, utils::blocks::type_list<>, JacobianForm> {
using Type = utils::blocks::type_list<>;
};
template <typename Residual, typename First, typename... Remaining, typename JacobianForm>
struct CouplingsForResidual<Residual, utils::blocks::type_list<First, Remaining...>, JacobianForm> {
private:
using Tail =
typename CouplingsForResidual<Residual, utils::blocks::type_list<Remaining...>, JacobianForm>::Type;
public:
using Type = std::conditional_t<
utils::blocks::has_jacobian_coupling_v<Residual, First, JacobianForm>,
MaterialSurfaceConcatenateT<utils::blocks::type_list<Coupling<Residual, First>>, Tail>,
Tail>;
};
template <typename Residuals, typename Corrections, typename JacobianForm> struct InducedCouplings;
template <typename Corrections, typename JacobianForm>
struct InducedCouplings<utils::blocks::type_list<>, Corrections, JacobianForm> {
using Type = utils::blocks::type_list<>;
};
template <typename First, typename... Remaining, typename Corrections, typename JacobianForm>
struct InducedCouplings<utils::blocks::type_list<First, Remaining...>, Corrections, JacobianForm> {
using Type = MaterialSurfaceConcatenateT<
typename CouplingsForResidual<First, Corrections, JacobianForm>::Type,
typename InducedCouplings<utils::blocks::type_list<Remaining...>, Corrections, JacobianForm>::Type>;
};
template <typename Candidate, typename = void> struct IsMaterialSurfaceDescriptor : std::false_type { };
template <typename Candidate, typename Universe> struct MaterialSurfaceListIsSubset;
template <typename... Candidates, typename Universe>
struct MaterialSurfaceListIsSubset<utils::blocks::type_list<Candidates...>, Universe>
: std::bool_constant<(utils::blocks::contains_type_v<Candidates, Universe> && ...)> { };
} // 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<FormT, JacobianFormT>
struct CompiledMaterialSurfaceDescriptor final {
using ThermodynamicEquations = ThermodynamicEquationsT;
using SurfaceConstraint = SurfaceConstraintT;
using Form = FormT;
using JacobianForm = JacobianFormT;
using CarrierField = typename SurfaceConstraint::CarrierField;
using CarrierEquation =
material::ThermodynamicEquationForFieldT<typename ThermodynamicEquations::Equations, CarrierField>;
using NonCarrierMaterialEquations =
typename detail::NonCarrierEquations<typename ThermodynamicEquations::Equations, CarrierField>::Type;
using NonCarrierCorrectionBlocks = typename detail::EquationCorrectionBlocks<NonCarrierMaterialEquations>::Type;
using NonCarrierResidualBlocks = typename detail::EquationResidualBlocks<NonCarrierMaterialEquations>::Type;
using SurfaceStateFields = typename SurfaceConstraint::SurfaceDependencies::StateFieldTypes;
using CorrectionBlocks = detail::MaterialSurfaceConcatenateT<
NonCarrierCorrectionBlocks,
utils::blocks::type_list<utils::blocks::surface_deformation::parameters::value>,
utils::blocks::type_list<typename CarrierEquation::Correction>>;
using ResidualBlocks = detail::MaterialSurfaceConcatenateT<
NonCarrierResidualBlocks,
utils::blocks::type_list<utils::blocks::surface_deformation::shape_equilibrium::residual>,
utils::blocks::type_list<typename CarrierEquation::Residual>>;
using RequiredCouplings =
typename detail::InducedCouplings<ResidualBlocks, CorrectionBlocks, JacobianForm>::Type;
static constexpr bool symbolicallySquare = CorrectionBlocks::size == ResidualBlocks::size;
static constexpr bool surfaceDependenciesBelongToMaterial = material::
fieldsBelongToThermodynamicEquations<SurfaceStateFields, typename ThermodynamicEquations::Equations>;
static constexpr bool correctionBlocksBelongToForm =
detail::MaterialSurfaceListIsSubset<CorrectionBlocks, typename Form::value_blocks>::value;
static constexpr bool residualBlocksBelongToForm =
detail::MaterialSurfaceListIsSubset<ResidualBlocks, typename Form::residual_blocks>::value;
};
template <typename Problem>
requires equilibrium::DiscretizedStellarEquilibriumProblem<std::remove_cvref_t<Problem>>
using MaterialSurfaceDescriptorFor = CompiledMaterialSurfaceDescriptor<
typename std::remove_cvref_t<Problem>::ThermodynamicEquationsType,
typename std::remove_cvref_t<Problem>::CompiledSurfaceConstraintType,
typename std::remove_cvref_t<Problem>::FormType,
typename std::remove_cvref_t<Problem>::JacobianFormType>;
namespace detail {
template <typename ThermodynamicEquations, typename SurfaceConstraint, typename Form, typename JacobianForm>
struct IsMaterialSurfaceDescriptor<
CompiledMaterialSurfaceDescriptor<ThermodynamicEquations, SurfaceConstraint, Form, JacobianForm>>
: std::bool_constant<
material::CompiledThermodynamicEquations<typename CompiledMaterialSurfaceDescriptor<
ThermodynamicEquations,
SurfaceConstraint,
Form,
JacobianForm>::ThermodynamicEquations> &&
CompiledMaterialSurfaceDescriptor<ThermodynamicEquations, SurfaceConstraint, Form, JacobianForm>::
symbolicallySquare &&
CompiledMaterialSurfaceDescriptor<ThermodynamicEquations, SurfaceConstraint, Form, JacobianForm>::
surfaceDependenciesBelongToMaterial &&
CompiledMaterialSurfaceDescriptor<ThermodynamicEquations, SurfaceConstraint, Form, JacobianForm>::
correctionBlocksBelongToForm &&
CompiledMaterialSurfaceDescriptor<ThermodynamicEquations, SurfaceConstraint, Form, JacobianForm>::
residualBlocksBelongToForm &&
utils::blocks::types_are_unique_v<typename CompiledMaterialSurfaceDescriptor<
ThermodynamicEquations,
SurfaceConstraint,
Form,
JacobianForm>::CorrectionBlocks> &&
utils::blocks::types_are_unique_v<typename CompiledMaterialSurfaceDescriptor<
ThermodynamicEquations,
SurfaceConstraint,
Form,
JacobianForm>::ResidualBlocks>> { };
} // namespace detail
template <typename Candidate>
concept MaterialSurfaceDescriptor = detail::IsMaterialSurfaceDescriptor<std::remove_cvref_t<Candidate>>::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 <typename EquationOfState>
struct MaterialSurfaceEquationOfStateBackend {
static constexpr bool registered = false;
};
template <>
struct MaterialSurfaceEquationOfStateBackend<eos::Polytrope> {
static constexpr bool registered = true;
using CoreType = operators::PreparedStellarEquilibriumOperator;
};
template <typename EquationOfState>
concept ImplementedMaterialSurfaceEquationOfState = requires {
{
MaterialSurfaceEquationOfStateBackend<std::remove_cvref_t<EquationOfState>>::registered
} -> std::convertible_to<bool>;
requires MaterialSurfaceEquationOfStateBackend<
std::remove_cvref_t<EquationOfState>>::registered;
typename MaterialSurfaceEquationOfStateBackend<std::remove_cvref_t<EquationOfState>>::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 <typename EquationOfState, typename PhysicalCore>
struct MaterialSurfaceExecutableRuntime {
static constexpr bool available = false;
};
template <>
struct MaterialSurfaceExecutableRuntime<eos::Polytrope, operators::PreparedStellarEquilibriumOperator> {
static constexpr bool available = true;
};
template <typename EquationOfState, typename PhysicalCore>
concept ExecutableMaterialSurfaceRuntimeFor = requires {
{
MaterialSurfaceExecutableRuntime<
std::remove_cvref_t<EquationOfState>,
std::remove_cvref_t<PhysicalCore>>::available
} -> std::convertible_to<bool>;
requires MaterialSurfaceExecutableRuntime<
std::remove_cvref_t<EquationOfState>,
std::remove_cvref_t<PhysicalCore>>::available;
};
template <typename Descriptor>
concept ImplementedMaterialSurfaceDescriptor =
MaterialSurfaceDescriptor<Descriptor> &&
ImplementedMaterialSurfaceEquationOfState<
typename Descriptor::ThermodynamicEquations::EquationOfStateType>;
template <typename Descriptor, typename PhysicalCore>
concept MaterialSurfaceRuntimeFor =
ImplementedMaterialSurfaceDescriptor<Descriptor> && requires {
typename MaterialSurfaceEquationOfStateBackend<
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType>::CoreType;
requires std::same_as<
std::remove_cvref_t<PhysicalCore>,
typename MaterialSurfaceEquationOfStateBackend<
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType>::CoreType>;
requires ExecutableMaterialSurfaceRuntimeFor<
typename std::remove_cvref_t<Descriptor>::ThermodynamicEquations::EquationOfStateType,
PhysicalCore>;
};
template <typename Candidate>
concept MaterialSurfacePreconditionerProblem =
equilibrium::DiscretizedStellarEquilibriumProblem<Candidate> && requires {
requires MaterialSurfaceRuntimeFor<
MaterialSurfaceDescriptorFor<std::remove_cvref_t<Candidate>>,
typename std::remove_cvref_t<Candidate>::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 <typename Candidate> struct SurfaceSurrogateTraits {
static constexpr bool registered = false;
};
template <> struct SurfaceSurrogateTraits<SurfaceMassDiagonal> {
static constexpr bool registered = true;
using OperatorDescription = SurfaceDiagonalCharacteristics;
};
template <> struct SurfaceSurrogateTraits<SurfaceH1MassStiffness> {
static constexpr bool registered = true;
using OperatorDescription = SurfaceH1MassStiffnessCharacteristics;
};
template <typename Candidate>
concept MaterialSurfaceSurrogate = SurfaceSurrogateTraits<std::remove_cvref_t<Candidate>>::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<double>::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<MaterialBackend, DensityMassDiagonalCharacteristics> &&
Compatible<MaterialBackend, EnthalpyMassDiagonalCharacteristics> &&
Compatible<SurfaceBackend, typename SurfaceSurrogateTraits<SurfaceSurrogate>::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<MaterialBackend, DensityMassDiagonalCharacteristics> &&
Compatible<MaterialBackend, EnthalpyMassDiagonalCharacteristics> &&
Compatible<SurfaceBackend, typename SurfaceSurrogateTraits<SurfaceSurrogate>::OperatorDescription>
struct Traits<MaterialSurface<Descriptor, MaterialBackend, SurfaceBackend, Policy, SurfaceSurrogate>> {
static constexpr bool registered = true;
static constexpr ApplicationContract applicationContract =
::mean_field::preconditioning::backend::applicationContract<MaterialBackend> ==
ApplicationContract::stationary_linear &&
::mean_field::preconditioning::backend::applicationContract<SurfaceBackend> ==
ApplicationContract::stationary_linear
? ApplicationContract::stationary_linear
: ApplicationContract::flexible;
static constexpr bool supportsSerialExecution =
Traits<MaterialBackend>::supportsSerialExecution && Traits<SurfaceBackend>::supportsSerialExecution;
static constexpr bool supportsDistributedExecution =
Traits<MaterialBackend>::supportsDistributedExecution &&
Traits<SurfaceBackend>::supportsDistributedExecution;
static constexpr SymmetryRequirement symmetryRequirement = SymmetryRequirement::none;
static constexpr NullspaceRequirement nullspaceRequirement = NullspaceRequirement::none;
static constexpr SurrogateRequirement surrogateRequirement = Traits<SurfaceBackend>::surrogateRequirement;
static constexpr bool requiresAssembledSparseSurrogate =
Traits<SurfaceBackend>::requiresAssembledSparseSurrogate;
using PreparationDependencies = preconditioning::PreparationDependencies<
PreparationDependency::discretization,
PreparationDependency::geometry,
PreparationDependency::equation_of_state,
PreparationDependency::linearization>;
template <OperatorCharacteristicsType Characteristics>
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<MaterialBackendT, DensityMassDiagonalCharacteristics> &&
backend::Compatible<MaterialBackendT, EnthalpyMassDiagonalCharacteristics> &&
backend::Compatible<
SurfaceBackendT,
typename SurfaceSurrogateTraits<SurfaceSurrogateT>::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<Descriptor, MaterialBackendT, SurfaceBackendT, FactorizationPolicyT, SurfaceSurrogateT>;
using PreparationDependencies = typename backend::Traits<BackendType>::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<std::remove_cvref_t<Problem>>;
return MaterialSurfaceBlock<Descriptor, MaterialBackend, SurfaceBackend, Policy>{
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<std::remove_cvref_t<Problem>>;
return MaterialSurfaceBlock<Descriptor, MaterialBackend, SurfaceBackend, Policy, SurfaceSurrogate>{
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<int> &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<mfem::real_t *>(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<int> 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 <typename Candidate>
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<int>;
{ couplings.GetOffsets() } -> std::same_as<const mfem::Array<int> &>;
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<Policy, MaterialSurfaceBlockDiagonal>) {
m_densityInverse->Mult(densityRightHandSide, densityAction);
m_surfaceInverse->Mult(surfaceRightHandSide, surfaceAction);
m_enthalpyInverse->Mult(enthalpyRightHandSide, enthalpyAction);
} else if constexpr (std::same_as<Policy, SurfaceThenMaterialTriangular>) {
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<Policy, ApproximateMaterialSurfaceLDU>) {
// 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<Policy, MaterialThenSurfaceTriangular>) {
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<Policy, CoupledMaterialIndependentSurface>);
m_surfaceInverse->Mult(surfaceRightHandSide, surfaceAction);
}
}
++m_statistics.densityInverseApplications;
++m_statistics.surfaceInverseApplications;
++m_statistics.enthalpyInverseApplications;
++m_statistics.applications;
}
[[nodiscard]] const mfem::Array<int> &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<mfem::real_t *>(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<int> 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 <MaterialSurfaceDescriptor Descriptor, MaterialSurfaceFactorizationPolicy Policy>
requires MaterialSurfaceRuntimeFor<Descriptor, operators::PreparedStellarEquilibriumOperator>
class PreparedMaterialSurfaceBlock final : public mfem::Solver {
public:
using Block = MaterialSurfaceBlock<Descriptor, backend::Diagonal, backend::Diagonal, Policy>;
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<Policy> &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<int> stellarSurfaceMarker(finiteElements.mesh->bdr_attributes.Max());
stellarSurfaceMarker = 0;
constexpr int stellarSurfaceAttribute =
DomainSchema::template boundary_attribute<utils::domain::StellarSurface>();
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<mfem::HypreParMatrix> 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<DomainSchema>(*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<DomainSchema>(*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<std::uint64_t>(surfaceMap.global_boundary_dof(index));
value += 0x9e3779b97f4a7c15ULL * static_cast<std::uint64_t>(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<std::uint64_t>(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<double>::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<Policy> 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<Descriptor, operators::PreparedStellarEquilibriumOperator>
class PreparedH1MaterialSurfaceBlock final : public mfem::Solver {
public:
using SurfaceBackend = backend::HypreBoomerAMG<Mode>;
using Block =
MaterialSurfaceBlock<Descriptor, backend::Diagonal, SurfaceBackend, Policy, SurfaceH1MassStiffness>;
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<backend::PreparedDiagonal>(m_block.materialBackend(), m_densityDiagonal);
m_enthalpyInverse =
std::make_unique<backend::PreparedDiagonal>(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<backend::PreparedHypreBoomerAMG<Mode>>(m_block.surfaceBackend(), *m_surfaceSurrogate);
RebindFactorization();
m_statistics.setups = 1;
m_statistics.surfaceJacobianProbes =
static_cast<std::uint64_t>(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<std::uint64_t>(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<Policy> &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<Mode> &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<DomainSchema>(*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<mfem::HypreParMatrix> 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<int> stellarSurfaceMarker(finiteElements.mesh->bdr_attributes.Max());
stellarSurfaceMarker = 0;
constexpr int stellarSurfaceAttribute =
DomainSchema::template boundary_attribute<utils::domain::StellarSurface>();
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<mfem::HypreParMatrix> 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<double>(degree) - 1.0) * coordinate * current -
(static_cast<double>(degree) - 1.0) * previous) /
static_cast<double>(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<double>(globalDof) + 0.5) /
static_cast<double>(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<std::uint64_t>(globalDof);
value += 0x9e3779b97f4a7c15ULL * static_cast<std::uint64_t>(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<double, 6> localValues{local.massMass, local.massStiffness, local.stiffnessStiffness,
local.massTarget, local.stiffnessTarget, local.targetTarget};
std::array<double, 6> globalValues{};
MPI_Allreduce(
localValues.data(), globalValues.data(), static_cast<int>(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<SignedScalarBoundarySolverAdapter>(*m_surfaceInverse, m_surfaceMap, m_surfaceFit.sign);
auto factorization = std::make_unique<MaterialSurfaceFactorizationOperator<Policy>>(
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<double>::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<backend::PreparedDiagonal> m_densityInverse;
std::unique_ptr<backend::PreparedDiagonal> m_enthalpyInverse;
std::unique_ptr<mfem::HypreParMatrix> m_surfaceMass;
std::unique_ptr<mfem::HypreParMatrix> m_surfaceStiffness;
std::unique_ptr<mfem::HypreParMatrix> m_surfaceSurrogate;
SurfaceH1FitReport m_surfaceFit;
std::unique_ptr<backend::PreparedHypreBoomerAMG<Mode>> m_surfaceInverse;
std::unique_ptr<SignedScalarBoundarySolverAdapter> m_surfaceBoundaryInverse;
std::unique_ptr<MaterialSurfaceFactorizationOperator<Policy>> m_factorization;
operators::StellarEquilibriumDependencies m_dependencies;
PreparedMaterialSurfaceBlockStatistics m_statistics;
};
template <
MaterialSurfaceDescriptor Descriptor,
MaterialSurfaceFactorizationPolicy Policy>
requires MaterialSurfaceRuntimeFor<Descriptor, operators::PreparedStellarEquilibriumOperator>
[[nodiscard]] auto prepare(
const operators::PreparedStellarEquilibriumOperator &operation,
MaterialSurfaceBlock<
Descriptor,
backend::Diagonal,
backend::Diagonal,
Policy> block
) {
return PreparedMaterialSurfaceBlock<Descriptor, Policy>{operation, std::move(block)};
}
template <
equilibrium::StellarEquilibriumModel Model,
equilibrium::StellarDiscretizationType Discretization,
MaterialSurfaceFactorizationPolicy Policy>
requires MaterialSurfacePreconditionerProblem<
equilibrium::StellarEquilibriumProblem<Model, Discretization>>
[[nodiscard]] auto prepare(
const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
MaterialSurfaceBlock<
MaterialSurfaceDescriptorFor<equilibrium::StellarEquilibriumProblem<Model, Discretization>>,
backend::Diagonal,
backend::Diagonal,
Policy> block
) {
return prepare(problem.GetPhysicalOperator(), std::move(block));
}
template <
MaterialSurfaceDescriptor Descriptor,
MaterialSurfaceFactorizationPolicy Policy,
backend::ApplicationMode Mode>
requires MaterialSurfaceRuntimeFor<Descriptor, operators::PreparedStellarEquilibriumOperator>
[[nodiscard]] auto prepare(
const operators::PreparedStellarEquilibriumOperator &operation,
MaterialSurfaceBlock<
Descriptor,
backend::Diagonal,
backend::HypreBoomerAMG<Mode>,
Policy,
SurfaceH1MassStiffness> block
) {
return PreparedH1MaterialSurfaceBlock<Descriptor, Policy, Mode>{operation, std::move(block)};
}
template <
equilibrium::StellarEquilibriumModel Model,
equilibrium::StellarDiscretizationType Discretization,
MaterialSurfaceFactorizationPolicy Policy,
backend::ApplicationMode Mode>
requires MaterialSurfacePreconditionerProblem<
equilibrium::StellarEquilibriumProblem<Model, Discretization>>
[[nodiscard]] auto prepare(
const equilibrium::StellarEquilibriumProblem<Model, Discretization> &problem,
MaterialSurfaceBlock<
MaterialSurfaceDescriptorFor<equilibrium::StellarEquilibriumProblem<Model, Discretization>>,
backend::Diagonal,
backend::HypreBoomerAMG<Mode>,
Policy,
SurfaceH1MassStiffness> block
) {
return prepare(problem.GetPhysicalOperator(), std::move(block));
}
} // namespace mean_field::preconditioning