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