Files
MeanField/tests/extensions/fixed_magnetic_specific_energy.cpp
2026-09-06 10:15:00 -04:00

2183 lines
94 KiB
C++

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