Files
MeanField/tests/normalization/stellar_equilibrium.cpp

1000 lines
51 KiB
C++

#include <algorithm>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <cstring>
#include <limits>
#include <numbers>
#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;
namespace normalization_policy_extension_test {
/* A deliberately simple third normalization family. It is neither the
* library identity policy nor Physical Riesz: its two constant factors are
* runtime data owned by the compile-time discretization policy type. */
class UniformDiagonal final : public mean_field::normalization::NormalizationPrescriptionTag {
public:
UniformDiagonal(
const double stateFactor,
const double residualFactor
)
: m_stateFactor(stateFactor),
m_residualFactor(residualFactor) {
if (!std::isfinite(stateFactor) || stateFactor <= 0.0 || !std::isfinite(residualFactor) ||
residualFactor <= 0.0) {
throw std::invalid_argument("Uniform diagonal normalization requires finite, positive factors.");
}
}
[[nodiscard]] double stateFactor() const noexcept {
return m_stateFactor;
}
[[nodiscard]] double residualFactor() const noexcept {
return m_residualFactor;
}
private:
double m_stateFactor;
double m_residualFactor;
};
struct CompilationOnly final : mean_field::normalization::NormalizationPrescriptionTag { };
/* This deliberately claims identity coordinates while also requesting
* the third-party runtime adapter. The type-level runtime audit must reject
* that semantic mismatch even though both declarations exist. */
struct MisdeclaredRuntime final : mean_field::normalization::NormalizationPrescriptionTag { };
/* Adversarial policies isolate two other extension-boundary failures:
* borrowing another policy's coordinate ownership, and declaring a sound
* plan without implementing the corresponding runtime operation. */
struct BorrowedRuntime final : mean_field::normalization::NormalizationPrescriptionTag { };
struct MissingPreparation final : mean_field::normalization::NormalizationPrescriptionTag { };
struct WrongPreparationResult final : mean_field::normalization::NormalizationPrescriptionTag { };
class MoveOnlyDiagonal final : public mean_field::normalization::NormalizationPrescriptionTag {
public:
MoveOnlyDiagonal(
const double stateFactor,
const double residualFactor
)
: m_stateFactor(stateFactor),
m_residualFactor(residualFactor) {
}
MoveOnlyDiagonal(const MoveOnlyDiagonal &) = delete;
MoveOnlyDiagonal &operator=(const MoveOnlyDiagonal &) = delete;
MoveOnlyDiagonal(MoveOnlyDiagonal &&) noexcept = default;
MoveOnlyDiagonal &operator=(MoveOnlyDiagonal &&) noexcept = default;
[[nodiscard]] double stateFactor() const noexcept {
return m_stateFactor;
}
[[nodiscard]] double residualFactor() const noexcept {
return m_residualFactor;
}
private:
double m_stateFactor;
double m_residualFactor;
};
} // namespace normalization_policy_extension_test
namespace mean_field::normalization {
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<normalization_policy_extension_test::UniformDiagonal, Form>
: RuntimePreparedNormalizationCompilation<normalization_policy_extension_test::UniformDiagonal, Form> { };
/* Deliberately omits runtimeAvailableFor. A symbolic plan alone must not
* make this prescription usable by discretize(). */
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<normalization_policy_extension_test::CompilationOnly, Form> {
using Plan = RuntimePreparedNormalizationPlanFor<normalization_policy_extension_test::CompilationOnly, Form>;
static constexpr bool registered = CompleteNormalizationFor<Plan, Form>;
};
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<normalization_policy_extension_test::MisdeclaredRuntime, Form> {
using Plan = IdentityNormalizationPlanFor<Form>;
static constexpr bool registered = CompleteNormalizationFor<Plan, Form>;
template <typename PhysicalCore, typename SpecificationTypes>
static constexpr bool runtimeAvailableFor = registered;
};
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<normalization_policy_extension_test::BorrowedRuntime, Form> {
using Plan = RuntimePreparedNormalizationPlanFor<normalization_policy_extension_test::UniformDiagonal, Form>;
static constexpr bool registered = CompleteNormalizationFor<Plan, Form>;
template <typename PhysicalCore, typename SpecificationTypes>
static constexpr bool runtimeAvailableFor = registered;
};
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<normalization_policy_extension_test::MissingPreparation, Form>
: RuntimePreparedNormalizationCompilation<normalization_policy_extension_test::MissingPreparation, Form> { };
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<normalization_policy_extension_test::WrongPreparationResult, Form>
: RuntimePreparedNormalizationCompilation<normalization_policy_extension_test::WrongPreparationResult, Form> {
};
template <typename Form>
requires utils::blocks::block_form_is_valid_v<Form>
struct NormalizationCompilation<normalization_policy_extension_test::MoveOnlyDiagonal, Form>
: RuntimePreparedNormalizationCompilation<normalization_policy_extension_test::MoveOnlyDiagonal, Form> { };
} // namespace mean_field::normalization
namespace normalization_policy_extension_test {
/* Found by argument-dependent lookup through the policy carried by the
* problem's discretization type. No library switch or internal trait is
* modified to prepare this third-party normalization. */
template <mean_field::equilibrium::DiscretizedStellarEquilibriumProblem Problem>
requires std::same_as<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
UniformDiagonal>
[[nodiscard]] mean_field::normalization::DiagonalNormalization prepareStellarNormalization(
const UniformDiagonal &policy,
const Problem &problem
) {
mfem::Vector stateFactors(problem.StateSize());
mfem::Vector residualFactors(problem.EquationSize());
stateFactors = policy.stateFactor();
residualFactors = policy.residualFactor();
return {std::move(stateFactors), std::move(residualFactors)};
}
template <mean_field::equilibrium::DiscretizedStellarEquilibriumProblem Problem>
requires std::same_as<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
MisdeclaredRuntime>
[[nodiscard]] mean_field::normalization::DiagonalNormalization prepareStellarNormalization(
const MisdeclaredRuntime &,
const Problem &problem
) {
return mean_field::normalization::DiagonalNormalization::Identity(problem.StateSize(), problem.EquationSize());
}
/* A preparation operation with the wrong result type must not satisfy the
* solver-facing normalization contract. MissingPreparation intentionally
* has no operation at all. */
template <mean_field::equilibrium::DiscretizedStellarEquilibriumProblem Problem>
requires std::same_as<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
WrongPreparationResult>
[[nodiscard]] int prepareStellarNormalization(
const WrongPreparationResult &,
const Problem &
) {
return 0;
}
template <mean_field::equilibrium::DiscretizedStellarEquilibriumProblem Problem>
requires std::same_as<
typename std::remove_cvref_t<Problem>::NormalizationPrescriptionType,
MoveOnlyDiagonal>
[[nodiscard]] mean_field::normalization::DiagonalNormalization prepareStellarNormalization(
const MoveOnlyDiagonal &policy,
const Problem &problem
) {
mfem::Vector stateFactors(problem.StateSize());
mfem::Vector residualFactors(problem.EquationSize());
stateFactors = policy.stateFactor();
residualFactors = policy.residualFactor();
return {std::move(stateFactors), std::move(residualFactors)};
}
} // namespace normalization_policy_extension_test
namespace {
namespace normalization = mean_field::normalization;
using ThirdPolicyModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass>>;
using ThirdPolicyForm = mean_field::operators::CompiledStellarEquilibriumForm<ThirdPolicyModel>;
using ThirdPolicyDiscretization =
mean_field::equilibrium::StellarDiscretizationFor<normalization_policy_extension_test::UniformDiagonal>;
using CompilationOnlyDiscretization =
mean_field::equilibrium::StellarDiscretizationFor<normalization_policy_extension_test::CompilationOnly>;
using MisdeclaredRuntimeDiscretization =
mean_field::equilibrium::StellarDiscretizationFor<normalization_policy_extension_test::MisdeclaredRuntime>;
using BorrowedRuntimeDiscretization =
mean_field::equilibrium::StellarDiscretizationFor<normalization_policy_extension_test::BorrowedRuntime>;
using MissingPreparationDiscretization =
mean_field::equilibrium::StellarDiscretizationFor<normalization_policy_extension_test::MissingPreparation>;
using WrongPreparationResultDiscretization =
mean_field::equilibrium::StellarDiscretizationFor<normalization_policy_extension_test::WrongPreparationResult>;
using MoveOnlyDiscretization =
mean_field::equilibrium::StellarDiscretizationFor<normalization_policy_extension_test::MoveOnlyDiagonal>;
template <typename Model, typename Discretization>
concept CanDiscretizeWithNormalization = requires(Model model, Discretization discretization) {
mean_field::equilibrium::discretize(std::move(model), std::move(discretization));
};
template <typename Problem>
concept CanMakeNormalizedStellarEquilibriumOperator =
requires(Problem &problem) { mean_field::normalization::makeNormalizedStellarEquilibriumOperator(problem); };
template <typename Problem, typename Model, typename Discretization>
concept CanDirectlyConstructStellarEquilibriumProblem =
requires(Model model, Discretization discretization) { Problem{std::move(model), std::move(discretization)}; };
template <typename NormalizedOperator, typename PhysicalInverse>
concept CanMakeScaledStellarPreconditioner =
requires(const NormalizedOperator &normalized, PhysicalInverse &inverse) {
normalized.MakeScaledPreconditioner(inverse);
};
template <typename Problem, typename PhysicalInverse>
concept HasNormalizedStellarPreconditioner =
requires { typename normalization::NormalizedStellarPreconditioner<Problem, PhysicalInverse>; };
template <typename Problem> class ProblemBoundInverseWithoutFreshness : public mfem::Solver {
public:
[[nodiscard]] const Problem &GetProblem() const noexcept;
};
template <typename Problem> class FreshInverseWithoutProblem : public mfem::Solver {
public:
[[nodiscard]] bool IsCurrent() const noexcept;
};
template <typename Problem> class MutableProblemIdentityInverse : public mfem::Solver {
public:
[[nodiscard]] Problem &GetProblem() const noexcept;
[[nodiscard]] bool IsCurrent() const noexcept;
};
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies makeDependencies() {
return {
.discretization = {.identity = 12101, .revision = 1},
.density = {.identity = 12109, .revision = 1},
.surfaceDeformation = {.identity = 12113, .revision = 1},
.gravityGradient = {.identity = 12119, .revision = 1},
.gravityPotential = {.identity = 12143, .revision = 1},
.enthalpy = {.identity = 12149, .revision = 1},
.bernoulliConstant = {.identity = 12157, .revision = 1},
.rotation = {.identity = 12161, .revision = 1},
.targetMass = {.identity = 12163, .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]] double blockRms(
const mfem::Vector &vector,
const int begin,
const int end
) {
double squaredNorm = 0.0;
for (int index = begin; index < end; ++index) {
squaredNorm += vector(index) * vector(index);
}
return std::sqrt(squaredNorm / static_cast<double>(end - begin));
}
struct BlockResponseSpread final {
double physical{0.0};
double normalized{0.0};
double physicalActivityFloor{0.0};
double normalizedActivityFloor{0.0};
int activeResponses{0};
};
template <typename Form>
[[nodiscard]] BlockResponseSpread measureBlockResponseSpread(
const mfem::Operator &physicalJacobian,
const mfem::Operator &normalizedJacobian,
const mean_field::utils::blocks::form_layout<Form> &layout
) {
struct Response final {
double physical;
double normalized;
};
std::vector<Response> responses;
responses.reserve(Form::value_block_count * Form::residual_block_count);
const auto &valueOffsets = layout.value_offsets();
const auto &residualOffsets = layout.residual_offsets();
for (int valueBlock = 0; valueBlock < Form::value_block_count; ++valueBlock) {
mfem::Vector direction(physicalJacobian.Width());
direction = 0.0;
const int begin = valueOffsets[valueBlock];
const int end = valueOffsets[valueBlock + 1];
double squaredNorm = 0.0;
for (int index = begin; index < end; ++index) {
const double localIndex = static_cast<double>(index - begin + 1);
const double value = std::sin(0.37 * localIndex + 0.41 * static_cast<double>(valueBlock + 1)) +
0.29 * std::cos(0.17 * localIndex - 0.23 * static_cast<double>(valueBlock + 1));
direction(index) = value;
squaredNorm += value * value;
}
REQUIRE(squaredNorm > 0.0);
direction *= 1.0 / std::sqrt(squaredNorm);
mfem::Vector physicalAction;
mfem::Vector normalizedAction;
physicalJacobian.Mult(direction, physicalAction);
normalizedJacobian.Mult(direction, normalizedAction);
REQUIRE(physicalAction.Size() == physicalJacobian.Height());
REQUIRE(normalizedAction.Size() == normalizedJacobian.Height());
for (int residualBlock = 0; residualBlock < Form::residual_block_count; ++residualBlock) {
responses.push_back(
{.physical =
blockRms(physicalAction, residualOffsets[residualBlock], residualOffsets[residualBlock + 1]),
.normalized =
blockRms(normalizedAction, residualOffsets[residualBlock], residualOffsets[residualBlock + 1])}
);
}
}
double globalLargestPhysical = 0.0;
double globalLargestNormalized = 0.0;
for (const Response &response : responses) {
// A non-finite response must never disappear merely because the
// other coordinate system decides that entry is inactive.
REQUIRE(std::isfinite(response.physical));
REQUIRE(std::isfinite(response.normalized));
globalLargestPhysical = std::max(globalLargestPhysical, response.physical);
globalLargestNormalized = std::max(globalLargestNormalized, response.normalized);
}
REQUIRE(globalLargestPhysical > 0.0);
REQUIRE(globalLargestNormalized > 0.0);
/* Activity is the union of entries resolved in either coordinate
* system. Separate relative floors make the selection symmetric:
* normalization cannot improve its reported spread merely by pushing
* a physically active response below a normalized-only gate (and the
* converse is equally prohibited). The absolute floor excludes only
* denormal-scale arithmetic, not modeled stellar coefficients. */
constexpr double relativeActivityFloor = 1.0e-10;
constexpr double absoluteActivityFloor = 64.0 * std::numeric_limits<double>::min();
const double physicalActivityFloor =
std::max(absoluteActivityFloor, relativeActivityFloor * globalLargestPhysical);
const double normalizedActivityFloor =
std::max(absoluteActivityFloor, relativeActivityFloor * globalLargestNormalized);
double smallestPhysical = std::numeric_limits<double>::infinity();
double largestPhysical = 0.0;
double smallestNormalized = std::numeric_limits<double>::infinity();
double largestNormalized = 0.0;
int activeResponses = 0;
for (const Response &response : responses) {
const bool physicallyActive = response.physical > physicalActivityFloor;
const bool normalizedActive = response.normalized > normalizedActivityFloor;
if (!physicallyActive && !normalizedActive) {
continue;
}
// Diagonal two-sided scaling preserves structural support. Once
// either representation resolves an interaction, both responses
// must therefore be usable in the spread comparison.
REQUIRE(std::isfinite(response.physical));
REQUIRE(std::isfinite(response.normalized));
REQUIRE(response.physical > 0.0);
REQUIRE(response.normalized > 0.0);
smallestPhysical = std::min(smallestPhysical, response.physical);
largestPhysical = std::max(largestPhysical, response.physical);
smallestNormalized = std::min(smallestNormalized, response.normalized);
largestNormalized = std::max(largestNormalized, response.normalized);
++activeResponses;
}
REQUIRE(activeResponses > 0);
return {
.physical = largestPhysical / smallestPhysical,
.normalized = largestNormalized / smallestNormalized,
.physicalActivityFloor = physicalActivityFloor,
.normalizedActivityFloor = normalizedActivityFloor,
.activeResponses = activeResponses
};
}
} // namespace
TEST_CASE(
"A Third-Party Normalization Policy Reaches Discretization And The Normalized Operator",
"[normalization][extension][type_contract][integration]"
) {
using namespace mean_field;
using Policy = normalization_policy_extension_test::UniformDiagonal;
using CompilationOnly = normalization_policy_extension_test::CompilationOnly;
using MisdeclaredRuntime = normalization_policy_extension_test::MisdeclaredRuntime;
using BorrowedRuntime = normalization_policy_extension_test::BorrowedRuntime;
using MissingPreparation = normalization_policy_extension_test::MissingPreparation;
using WrongPreparationResult = normalization_policy_extension_test::WrongPreparationResult;
using MoveOnlyDiagonal = normalization_policy_extension_test::MoveOnlyDiagonal;
using PhysicalCore = operators::StellarEquilibriumPhysicalCoreType<ThirdPolicyModel>;
using ExpectedPlan = normalization::RuntimePreparedNormalizationPlanFor<Policy, ThirdPolicyForm>;
STATIC_CHECK(normalization::NormalizationPrescription<Policy>);
STATIC_CHECK(normalization::CompilableNormalizationFor<Policy, ThirdPolicyForm>);
STATIC_CHECK(normalization::RuntimePreparedNormalizationFor<Policy, ThirdPolicyForm>);
STATIC_CHECK(std::same_as<normalization::NormalizationPlanFor<Policy, ThirdPolicyForm>, ExpectedPlan>);
STATIC_CHECK(
normalization::StellarNormalizationRuntimeAvailableFor<
Policy, ThirdPolicyForm, PhysicalCore, ThirdPolicyModel::SpecificationTypes>
);
STATIC_CHECK(
equilibrium::StellarEquilibriumModelDiscretizationCompatible<ThirdPolicyModel, ThirdPolicyDiscretization>
);
STATIC_CHECK(CanDiscretizeWithNormalization<ThirdPolicyModel, ThirdPolicyDiscretization>);
STATIC_CHECK(normalization::CompilableNormalizationFor<CompilationOnly, ThirdPolicyForm>);
STATIC_CHECK_FALSE(
normalization::StellarNormalizationRuntimeAvailableFor<
CompilationOnly, ThirdPolicyForm, PhysicalCore, ThirdPolicyModel::SpecificationTypes>
);
STATIC_CHECK_FALSE(
equilibrium::StellarEquilibriumModelDiscretizationCompatible<ThirdPolicyModel, CompilationOnlyDiscretization>
);
STATIC_CHECK_FALSE(CanDiscretizeWithNormalization<ThirdPolicyModel, CompilationOnlyDiscretization>);
STATIC_CHECK(normalization::CompilableNormalizationFor<MisdeclaredRuntime, ThirdPolicyForm>);
STATIC_CHECK_FALSE(normalization::RuntimePreparedNormalizationFor<MisdeclaredRuntime, ThirdPolicyForm>);
STATIC_CHECK_FALSE(
normalization::StellarNormalizationRuntimeAvailableFor<
MisdeclaredRuntime, ThirdPolicyForm, PhysicalCore, ThirdPolicyModel::SpecificationTypes>
);
STATIC_CHECK_FALSE(
equilibrium::StellarEquilibriumModelDiscretizationCompatible<ThirdPolicyModel, MisdeclaredRuntimeDiscretization>
);
STATIC_CHECK_FALSE(CanDiscretizeWithNormalization<ThirdPolicyModel, MisdeclaredRuntimeDiscretization>);
STATIC_CHECK(normalization::CompilableNormalizationFor<BorrowedRuntime, ThirdPolicyForm>);
STATIC_CHECK_FALSE(normalization::RuntimePreparedNormalizationFor<BorrowedRuntime, ThirdPolicyForm>);
STATIC_CHECK_FALSE(
normalization::StellarNormalizationRuntimeAvailableFor<
BorrowedRuntime, ThirdPolicyForm, PhysicalCore, ThirdPolicyModel::SpecificationTypes>
);
STATIC_CHECK_FALSE(CanDiscretizeWithNormalization<ThirdPolicyModel, BorrowedRuntimeDiscretization>);
using MissingPreparationProblem =
equilibrium::StellarEquilibriumProblem<ThirdPolicyModel, MissingPreparationDiscretization>;
STATIC_CHECK(normalization::RuntimePreparedNormalizationFor<MissingPreparation, ThirdPolicyForm>);
STATIC_CHECK_FALSE(
equilibrium::StellarEquilibriumModelDiscretizationCompatible<ThirdPolicyModel, MissingPreparationDiscretization>
);
STATIC_CHECK_FALSE(CanDiscretizeWithNormalization<ThirdPolicyModel, MissingPreparationDiscretization>);
STATIC_CHECK_FALSE(normalization::NormalizableStellarEquilibriumProblem<MissingPreparationProblem>);
STATIC_CHECK_FALSE(CanMakeNormalizedStellarEquilibriumOperator<MissingPreparationProblem>);
STATIC_CHECK_FALSE(
CanDirectlyConstructStellarEquilibriumProblem<
MissingPreparationProblem, ThirdPolicyModel, MissingPreparationDiscretization>
);
using WrongPreparationResultProblem =
equilibrium::StellarEquilibriumProblem<ThirdPolicyModel, WrongPreparationResultDiscretization>;
STATIC_CHECK(normalization::RuntimePreparedNormalizationFor<WrongPreparationResult, ThirdPolicyForm>);
STATIC_CHECK_FALSE(
equilibrium::StellarEquilibriumModelDiscretizationCompatible<
ThirdPolicyModel, WrongPreparationResultDiscretization>
);
STATIC_CHECK_FALSE(CanDiscretizeWithNormalization<ThirdPolicyModel, WrongPreparationResultDiscretization>);
STATIC_CHECK_FALSE(normalization::NormalizableStellarEquilibriumProblem<WrongPreparationResultProblem>);
STATIC_CHECK_FALSE(CanMakeNormalizedStellarEquilibriumOperator<WrongPreparationResultProblem>);
STATIC_CHECK_FALSE(std::copy_constructible<MoveOnlyDiagonal>);
STATIC_CHECK(std::move_constructible<MoveOnlyDiagonal>);
STATIC_CHECK_FALSE(std::copy_constructible<MoveOnlyDiscretization>);
STATIC_CHECK(std::move_constructible<MoveOnlyDiscretization>);
STATIC_CHECK(normalization::RuntimePreparedNormalizationFor<MoveOnlyDiagonal, ThirdPolicyForm>);
STATIC_CHECK(
equilibrium::StellarEquilibriumModelDiscretizationCompatible<ThirdPolicyModel, MoveOnlyDiscretization>
);
STATIC_CHECK(CanDiscretizeWithNormalization<ThirdPolicyModel, MoveOnlyDiscretization>);
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
constexpr double stateFactor = 0.125;
constexpr double residualFactor = 32.0;
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})
);
auto problem = equilibrium::discretize(
model, equilibrium::makeStellarDiscretization(std::move(finiteElements), Policy{stateFactor, residualFactor})
);
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem);
using Problem = std::remove_cvref_t<decltype(problem)>;
STATIC_CHECK_FALSE(
CanDirectlyConstructStellarEquilibriumProblem<Problem, ThirdPolicyModel, ThirdPolicyDiscretization>
);
STATIC_CHECK(std::same_as<typename Problem::NormalizationPrescriptionType, Policy>);
STATIC_CHECK(normalization::NormalizableStellarEquilibriumProblem<Problem>);
REQUIRE(normalized.GetNormalization().StateSize() == problem.StateSize());
REQUIRE(normalized.GetNormalization().ResidualSize() == problem.EquationSize());
for (int index = 0; index < problem.StateSize(); ++index) {
CHECK(normalized.GetNormalization().StateFactors()(index) == Catch::Approx(stateFactor).epsilon(2.0e-15));
}
for (int index = 0; index < problem.EquationSize(); ++index) {
CHECK(normalized.GetNormalization().ResidualFactors()(index) == Catch::Approx(residualFactor).epsilon(2.0e-15));
}
mfem::Vector physicalState(problem.StateSize());
for (int index = 0; index < physicalState.Size(); ++index) {
physicalState(index) = 0.25 + 0.01 * static_cast<double>(index);
}
mfem::Vector normalizedState;
mfem::Vector recoveredState;
normalized.NormalizeState(physicalState, normalizedState);
REQUIRE(normalizedState.Size() == physicalState.Size());
for (int index = 0; index < normalizedState.Size(); ++index) {
CHECK(normalizedState(index) == Catch::Approx(stateFactor * physicalState(index)).epsilon(2.0e-15));
}
CHECK(normalizedState(0) != Catch::Approx(physicalState(0)).epsilon(2.0e-15));
normalized.DenormalizeState(normalizedState, recoveredState);
CHECK(relativeError(recoveredState, physicalState) <= 2.0e-15);
mfem::Vector physicalResidual(problem.EquationSize());
for (int index = 0; index < physicalResidual.Size(); ++index) {
physicalResidual(index) = -0.5 - 0.02 * static_cast<double>(index);
}
mfem::Vector normalizedResidual;
mfem::Vector recoveredResidual;
normalized.NormalizeResidual(physicalResidual, normalizedResidual);
REQUIRE(normalizedResidual.Size() == physicalResidual.Size());
for (int index = 0; index < normalizedResidual.Size(); ++index) {
CHECK(normalizedResidual(index) == Catch::Approx(residualFactor * physicalResidual(index)).epsilon(2.0e-15));
}
CHECK(normalizedResidual(0) != Catch::Approx(physicalResidual(0)).epsilon(2.0e-15));
normalized.DenormalizeResidual(normalizedResidual, recoveredResidual);
CHECK(relativeError(recoveredResidual, physicalResidual) <= 2.0e-15);
fem::FEM moveOnlyFiniteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(moveOnlyFiniteElements.okay());
auto moveOnlyProblem = equilibrium::discretize(
model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})
),
equilibrium::makeStellarDiscretization(std::move(moveOnlyFiniteElements), MoveOnlyDiagonal{0.5, 4.0})
);
auto moveOnlyNormalized = normalization::makeNormalizedStellarEquilibriumOperator(moveOnlyProblem);
CHECK(moveOnlyNormalized.GetNormalization().StateFactors()(0) == Catch::Approx(0.5).epsilon(2.0e-15));
CHECK(moveOnlyNormalized.GetNormalization().ResidualFactors()(0) == Catch::Approx(4.0).epsilon(2.0e-15));
}
TEST_CASE(
"Normalized Stellar Equilibrium Preserves Physical Residuals Jacobians And Frozen Solve Coordinates",
"[normalization][stellar-equilibrium][integration]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
constexpr double radius = utils::RADIUS;
constexpr double mass = 1.0e8;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
const normalization::PhysicalRieszDiagonal policy{dimensions::LengthValue{radius}, utils::G};
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem =
equilibrium::discretize(model, equilibrium::makeStellarDiscretization(std::move(finiteElements), policy));
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem);
using Problem = std::remove_cvref_t<decltype(problem)>;
using NormalizedOperator = std::remove_cvref_t<decltype(normalized)>;
STATIC_CHECK(equilibrium::DiscretizedStellarEquilibriumProblem<Problem>);
STATIC_CHECK(std::same_as<NormalizedOperator, normalization::NormalizedStellarEquilibriumOperator<Problem>>);
CHECK_FALSE(normalized.IsPrepared());
mfem::Vector unavailableResidual;
CHECK_THROWS_AS(normalized.BuildResidual(unavailableResidual), std::logic_error);
mfem::Vector normalizedState;
normalized.NormalizeState(projected.values, normalizedState);
const mfem::Vector frozenStateFactors(normalized.GetNormalization().StateFactors());
const mfem::Vector frozenResidualFactors(normalized.GetNormalization().ResidualFactors());
const auto dependencies = makeDependencies();
const auto rotation = zeroRotation();
const auto preparation = normalized.Prepare(normalizedState, dependencies, rotation);
CHECK(preparation.DidAnyWork());
CHECK(normalized.IsPrepared());
CHECK(problem.GetPreparationGeneration() == 1);
CHECK(relativeError(normalized.GetPhysicalState(), projected.values) <= 4.0e-15);
mfem::Vector physicalResidual;
mfem::Vector expectedNormalizedResidual;
mfem::Vector actualNormalizedResidual;
problem.BuildResidual(physicalResidual);
normalized.NormalizeResidual(physicalResidual, expectedNormalizedResidual);
normalized.BuildResidual(actualNormalizedResidual);
CHECK(relativeError(normalized.GetPhysicalResidual(), physicalResidual) <= 2.0e-15);
CHECK(relativeError(actualNormalizedResidual, expectedNormalizedResidual) <= 2.0e-15);
mfem::Vector normalizedDirection(problem.StateSize());
for (int index = 0; index < normalizedDirection.Size(); ++index) {
normalizedDirection(index) =
std::sin(0.013 * static_cast<double>(index + 1)) + 0.17 * std::cos(0.031 * static_cast<double>(index + 1));
}
normalizedDirection *= 1.0 / normalizedDirection.Norml2();
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) <= 3.0e-14);
const BlockResponseSpread spread = measureBlockResponseSpread<Problem::FormType>(
problem.GetLinearizationOperator(), normalized, problem.GetManifest().layout()
);
CAPTURE(
spread.physical, spread.normalized, spread.physicalActivityFloor, spread.normalizedActivityFloor,
spread.activeResponses
);
CHECK(spread.activeResponses >= 12);
CHECK(std::isfinite(spread.physical));
CHECK(std::isfinite(spread.normalized));
CHECK(spread.normalized < spread.physical);
CHECK(spread.physical / spread.normalized > 1.0e6);
CHECK(
std::memcmp(
frozenStateFactors.GetData(), normalized.GetNormalization().StateFactors().GetData(),
sizeof(mfem::real_t) * frozenStateFactors.Size()
) == 0
);
CHECK(
std::memcmp(
frozenResidualFactors.GetData(), normalized.GetNormalization().ResidualFactors().GetData(),
sizeof(mfem::real_t) * frozenResidualFactors.Size()
) == 0
);
CHECK(normalized.GetStatistics().normalizationPreparations == 1);
auto physicalInverse = preconditioning::prepare(problem, preconditioning::makeIdentityPlan(problem));
using PhysicalInverse = std::remove_cvref_t<decltype(physicalInverse)>;
STATIC_CHECK(normalization::ProblemBoundStellarInverseFor<PhysicalInverse, Problem>);
STATIC_CHECK(CanMakeScaledStellarPreconditioner<NormalizedOperator, PhysicalInverse>);
STATIC_CHECK(HasNormalizedStellarPreconditioner<Problem, PhysicalInverse>);
STATIC_CHECK_FALSE(normalization::ProblemBoundStellarInverseFor<mfem::Solver, Problem>);
STATIC_CHECK_FALSE(normalization::ProblemBoundStellarInverseFor<int, int>);
STATIC_CHECK_FALSE(
normalization::ProblemBoundStellarInverseFor<ProblemBoundInverseWithoutFreshness<Problem>, Problem>
);
STATIC_CHECK_FALSE(normalization::ProblemBoundStellarInverseFor<FreshInverseWithoutProblem<Problem>, Problem>);
STATIC_CHECK_FALSE(normalization::ProblemBoundStellarInverseFor<MutableProblemIdentityInverse<Problem>, Problem>);
STATIC_CHECK_FALSE(CanMakeScaledStellarPreconditioner<NormalizedOperator, mfem::Solver>);
STATIC_CHECK_FALSE(HasNormalizedStellarPreconditioner<Problem, mfem::Solver>);
STATIC_CHECK(
std::constructible_from<
normalization::ScaledPreconditioner, mfem::Solver &, const mfem::Operator &, const mfem::Operator &,
const normalization::DiagonalNormalization &>
);
CHECK(&physicalInverse.GetProblem() == &problem);
auto scaledInverse = normalized.MakeScaledPreconditioner(physicalInverse);
fem::FEM otherFiniteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(otherFiniteElements.okay());
auto otherModel = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto otherProblem = equilibrium::discretize(
otherModel, equilibrium::makeStellarDiscretization(std::move(otherFiniteElements), policy)
);
auto otherNormalized = normalization::makeNormalizedStellarEquilibriumOperator(otherProblem);
mfem::Vector otherNormalizedState;
otherNormalized.NormalizeState(projected.values, otherNormalizedState);
otherNormalized.Prepare(otherNormalizedState, dependencies, rotation);
REQUIRE(otherNormalized.IsPrepared());
const std::uint64_t bindingsBeforeWrongInstance = physicalInverse.GetStatistics().operatorBindings;
CHECK_THROWS_AS(otherNormalized.MakeScaledPreconditioner(physicalInverse), std::invalid_argument);
CHECK(physicalInverse.GetStatistics().operatorBindings == bindingsBeforeWrongInstance);
CHECK_THROWS_AS(scaledInverse.SetOperator(otherNormalized), std::invalid_argument);
mfem::Vector normalizedRightHandSide(problem.EquationSize());
for (int index = 0; index < normalizedRightHandSide.Size(); ++index) {
normalizedRightHandSide(index) = 0.2 * std::sin(0.023 * static_cast<double>(index + 1));
}
mfem::Vector physicalRightHandSide;
mfem::Vector physicalCorrection(problem.StateSize());
mfem::Vector expectedNormalizedCorrection;
mfem::Vector actualNormalizedCorrection(problem.StateSize());
normalized.DenormalizeResidual(normalizedRightHandSide, physicalRightHandSide);
physicalInverse.Mult(physicalRightHandSide, physicalCorrection);
normalized.NormalizeState(physicalCorrection, expectedNormalizedCorrection);
scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection);
CHECK(relativeError(actualNormalizedCorrection, expectedNormalizedCorrection) <= 2.0e-15);
CHECK(&scaledInverse.GetPhysicalJacobian() == &problem.GetLinearizationOperator());
CHECK(&scaledInverse.GetNormalizedJacobian() == &normalized);
CHECK(&scaledInverse.GetPhysicalInverse() == &physicalInverse);
mfem::IdentityOperator differentNormalizedJacobian(problem.StateSize());
CHECK_THROWS_AS(scaledInverse.SetOperator(differentNormalizedJacobian), std::invalid_argument);
scaledInverse.SetOperator(normalized);
problem.Prepare(normalized.GetPhysicalState(), dependencies, rotation);
CHECK(problem.GetPreparationGeneration() == 2);
CHECK_FALSE(normalized.IsPrepared());
CHECK_FALSE(scaledInverse.IsCurrent());
CHECK_THROWS_AS(normalized.Mult(normalizedDirection, actualNormalizedAction), std::logic_error);
CHECK_THROWS_AS(scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection), std::logic_error);
normalized.Prepare(normalizedState, dependencies, rotation);
CHECK(normalized.IsPrepared());
CHECK(problem.GetPreparationGeneration() == 3);
CHECK_FALSE(physicalInverse.IsCurrent());
CHECK_FALSE(scaledInverse.IsCurrent());
const auto reprepareRefresh = physicalInverse.Refresh();
CHECK(reprepareRefresh.changes.linearization);
CHECK(physicalInverse.IsCurrent());
CHECK(scaledInverse.IsCurrent());
normalized.RefreshNormalization();
CHECK_FALSE(normalized.IsPrepared());
CHECK_FALSE(scaledInverse.IsCurrent());
CHECK_THROWS_AS(scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection), std::logic_error);
CHECK(normalized.GetStatistics().normalizationPreparations == 2);
CHECK(relativeError(normalized.GetNormalization().StateFactors(), frozenStateFactors) <= 2.0e-15);
CHECK(relativeError(normalized.GetNormalization().ResidualFactors(), frozenResidualFactors) <= 2.0e-15);
normalized.NormalizeState(projected.values, normalizedState);
normalized.Prepare(normalizedState, dependencies, rotation);
CHECK(normalized.IsPrepared());
CHECK(problem.GetPreparationGeneration() == 4);
CHECK(normalized.GetStatistics().physicalPreparations == 3);
CHECK(normalized.GetStatistics().residualRetrievals == 1);
CHECK(normalized.GetStatistics().jacobianApplications >= Problem::FormType::value_block_count + 1);
auto changedDependencies = dependencies;
++changedDependencies.density.revision;
normalized.Prepare(normalizedState, changedDependencies, rotation);
CHECK(normalized.IsPrepared());
CHECK_FALSE(physicalInverse.IsCurrent());
CHECK_FALSE(scaledInverse.IsCurrent());
CHECK_THROWS_AS(scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection), std::logic_error);
const auto refreshReport = physicalInverse.Refresh();
CHECK(refreshReport.changes.Any());
CHECK_FALSE(refreshReport.DidAnyWork());
CHECK(physicalInverse.IsCurrent());
CHECK(scaledInverse.IsCurrent());
scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection);
CHECK(relativeError(actualNormalizedCorrection, expectedNormalizedCorrection) <= 2.0e-15);
CHECK(problem.GetPreparationGeneration() == 5);
CHECK(normalized.GetStatistics().physicalPreparations == 4);
}
TEST_CASE(
"Physical Riesz Normalization Includes Generated Angular Velocity And Angular Momentum Coordinates",
"[normalization][fixed-angular-momentum][integration]"
) {
using namespace mean_field;
using Catch::Approx;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
constexpr double mass = 7.0;
constexpr double radius = 3.0;
constexpr double gravitationalConstant = 5.0;
const normalization::PhysicalRieszDiagonal policy{dimensions::LengthValue{radius}, gravitationalConstant};
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{2.0}})
);
auto problem =
equilibrium::discretize(model, equilibrium::makeStellarDiscretization(std::move(finiteElements), policy));
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem);
using Problem = std::remove_cvref_t<decltype(problem)>;
using Form = typename Problem::FormType;
const auto scales = normalization::deriveStellarCharacteristicScales(
dimensions::MassValue{mass}, dimensions::LengthValue{radius}, gravitationalConstant
);
constexpr auto angularVelocityBlock =
utils::blocks::get_value_block<Form>(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term);
constexpr auto angularMomentumBlock =
utils::blocks::get_residual_block<Form>(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term);
const auto &layout = problem.GetManifest().layout();
CHECK(layout.size(angularVelocityBlock) == 1);
CHECK(layout.size(angularMomentumBlock) == 1);
CHECK(
normalized.GetNormalization().StateFactors()(layout.offset(angularVelocityBlock)) ==
Approx(1.0 / scales.angularVelocity).epsilon(2.0e-15)
);
CHECK(
normalized.GetNormalization().ResidualFactors()(layout.offset(angularMomentumBlock)) ==
Approx(1.0 / scales.angularMomentum).epsilon(2.0e-15)
);
mfem::Vector physicalState(problem.StateSize());
physicalState = 0.0;
const auto stateView = problem.GetManifest().stateView(physicalState);
stateView.block(utils::blocks::density_field.mass_term) = 1.0;
stateView.block(utils::blocks::enthalpy_field.specific_term) = 0.7;
stateView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term) = 1.0;
stateView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term) = 0.4;
mfem::Vector normalizedState;
normalized.NormalizeState(physicalState, normalizedState);
const auto preparation = normalized.Prepare(normalizedState, makeDependencies());
CHECK(preparation.generatedPhysicalControl);
CHECK(preparation.template specification<models::FixedAngularMomentum>().generatedRotation);
CHECK(normalized.IsPrepared());
CHECK(relativeError(normalized.GetPhysicalState(), physicalState) < 3.0e-15);
CHECK(problem.GetPreparedOperator().GetAngularMomentumReport().angularVelocity == Approx(0.4));
mfem::Vector normalizedDirection(problem.StateSize());
for (int index = 0; index < normalizedDirection.Size(); ++index) {
normalizedDirection(index) = 0.03 * std::sin(0.019 * 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) < 4.0e-14);
}
TEST_CASE(
"Two Sided Normalization Composes With The Full Coupled Stellar Preconditioner",
"[normalization][preconditioning][fixed-angular-momentum][central-density][integration]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
REQUIRE(finiteElements.okay());
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant = 2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity = std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{0.05}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(
model,
equilibrium::makeStellarDiscretization(
std::move(finiteElements), normalization::PhysicalRieszDiagonal{dimensions::LengthValue{radius}, utils::G}
)
);
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 1024}));
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem);
mfem::Vector normalizedState;
normalized.NormalizeState(projected.values, normalizedState);
const auto dependencies = makeDependencies();
const auto preparation = normalized.Prepare(normalizedState, dependencies);
REQUIRE(preparation.generatedPhysicalControl);
REQUIRE(normalized.IsPrepared());
using Problem = std::remove_cvref_t<decltype(problem)>;
const BlockResponseSpread coupledSpread = measureBlockResponseSpread<Problem::FormType>(
problem.GetLinearizationOperator(), normalized, problem.GetManifest().layout()
);
const double coupledImprovement = coupledSpread.physical / coupledSpread.normalized;
CAPTURE(
coupledSpread.physical, coupledSpread.normalized, coupledSpread.physicalActivityFloor,
coupledSpread.normalizedActivityFloor, coupledSpread.activeResponses, coupledImprovement
);
CHECK(coupledSpread.activeResponses >= static_cast<int>(Problem::FormType::value_block_count));
CHECK(std::isfinite(coupledSpread.physical));
CHECK(std::isfinite(coupledSpread.normalized));
// This is intentionally a conservative first empirical contract: the
// real coupled operator must improve, while the coordinated test run will
// determine whether a stronger stable factor is justified.
CHECK(coupledSpread.normalized < coupledSpread.physical);
CHECK(coupledImprovement > 1.0);
auto physicalInverse = preconditioning::prepare(problem, preconditioning::makePreconditioner(problem));
REQUIRE(physicalInverse.IsCurrent());
auto scaledInverse = normalized.MakeScaledPreconditioner(physicalInverse);
REQUIRE(scaledInverse.IsCurrent());
mfem::Vector normalizedRightHandSide(problem.EquationSize());
for (int index = 0; index < normalizedRightHandSide.Size(); ++index) {
normalizedRightHandSide(index) = 0.19 * std::sin(0.031 * static_cast<double>(index + 1)) +
0.07 * std::cos(0.017 * static_cast<double>(index + 1));
}
mfem::Vector physicalRightHandSide(problem.EquationSize());
mfem::Vector physicalCorrection(problem.StateSize());
mfem::Vector expectedNormalizedCorrection(problem.StateSize());
mfem::Vector actualNormalizedCorrection(problem.StateSize());
normalized.DenormalizeResidual(normalizedRightHandSide, physicalRightHandSide);
physicalInverse.Mult(physicalRightHandSide, physicalCorrection);
normalized.NormalizeState(physicalCorrection, expectedNormalizedCorrection);
scaledInverse.Mult(normalizedRightHandSide, actualNormalizedCorrection);
CHECK(relativeError(actualNormalizedCorrection, expectedNormalizedCorrection) <= 3.0e-13);
const auto correctionView = problem.GetManifest().stateView(actualNormalizedCorrection);
const double massCorrection =
correctionView.block(utils::blocks::fixed_total_mass_constraint.mass_normalization_term)(0);
const double angularVelocityCorrection =
correctionView.block(utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term)(0);
const double phaseCorrection =
correctionView.block(utils::blocks::fixed_central_density_phase.central_value_term)(0);
CAPTURE(massCorrection, angularVelocityCorrection, phaseCorrection);
CHECK(std::isfinite(massCorrection));
CHECK(std::isfinite(angularVelocityCorrection));
CHECK(std::isfinite(phaseCorrection));
CHECK(std::abs(massCorrection) > 1.0e-16);
CHECK(std::abs(angularVelocityCorrection) > 1.0e-16);
CHECK(std::abs(phaseCorrection) > 1.0e-16);
mfem::Vector expectedNormalizedPreconditionedAction(problem.EquationSize());
mfem::Vector actualNormalizedPreconditionedAction(problem.EquationSize());
mfem::Vector physicalPreconditionedAction(problem.EquationSize());
problem.ApplyLinearization(physicalCorrection, physicalPreconditionedAction);
normalized.NormalizeResidual(physicalPreconditionedAction, expectedNormalizedPreconditionedAction);
normalized.Mult(actualNormalizedCorrection, actualNormalizedPreconditionedAction);
CHECK(relativeError(actualNormalizedPreconditionedAction, expectedNormalizedPreconditionedAction) <= 4.0e-13);
normalized.Prepare(normalizedState, dependencies);
CHECK(normalized.IsPrepared());
CHECK_FALSE(physicalInverse.IsCurrent());
CHECK_FALSE(scaledInverse.IsCurrent());
const auto refresh = physicalInverse.Refresh();
CHECK(refresh.rebuiltSchurComplement);
CHECK(refresh.DidAnyWork());
CHECK(physicalInverse.IsCurrent());
CHECK(scaledInverse.IsCurrent());
}