1913 lines
93 KiB
C++
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));
|
|
}
|