Files
MeanField/tests/normalization/stellar_equilibrium.cpp
2026-09-06 10:15:00 -04:00

1188 lines
54 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(
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);
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(
finiteElements,
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(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);
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(finiteElements, 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(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(
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());
}