1188 lines
54 KiB
C++
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());
|
|
}
|