2183 lines
94 KiB
C++
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));
|
|
}
|