Files
MeanField/tests/extensions/fixed_magnetic_specific_energy.cpp

1913 lines
93 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, std::move(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(
std::move(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(
std::move(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));
}