834 lines
36 KiB
C++
834 lines
36 KiB
C++
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <concepts>
|
|
#include <limits>
|
|
#include <span>
|
|
#include <stdexcept>
|
|
#include <utility>
|
|
|
|
#include <catch2/catch_approx.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <mfem.hpp>
|
|
|
|
import mean_field;
|
|
import test_helpers;
|
|
|
|
namespace {
|
|
namespace blocks = mean_field::utils::blocks;
|
|
namespace normalization = mean_field::normalization;
|
|
namespace models = mean_field::models;
|
|
|
|
struct ModelWithoutFixedTotalMass final { };
|
|
|
|
template <typename Model>
|
|
concept SupportsModelDerivedStellarScales =
|
|
requires(const normalization::PhysicalRieszDiagonal<> &policy, const Model &model) {
|
|
{
|
|
normalization::deriveStellarCharacteristicScales(policy, model)
|
|
} -> std::same_as<normalization::StellarCharacteristicScales>;
|
|
};
|
|
|
|
struct TestValue final : blocks::value_block_base { };
|
|
struct TestResidual final : blocks::residual_block_base { };
|
|
using TestForm = blocks::block_form<blocks::type_list<TestValue>, blocks::type_list<TestResidual>>;
|
|
|
|
using GlobalSpecificEnergyNormalization = models::
|
|
CoordinateNormalization<models::RieszTopology::global_scalar, models::PhysicalScaleLaw::specific_energy>;
|
|
using VolumeSpecificEnergyNormalization = models::
|
|
CoordinateNormalization<models::RieszTopology::scalar_volume_l2, models::PhysicalScaleLaw::specific_energy>;
|
|
|
|
class SelfDescribingMagneticSpecificEnergy final {
|
|
public:
|
|
struct Parameters final {
|
|
double target;
|
|
};
|
|
|
|
using ModelDefinition = mean_field::integral::FixedWithPhysicalCoordinate<
|
|
SelfDescribingMagneticSpecificEnergy,
|
|
"NormalizationMockMagneticSpecificEnergy",
|
|
models::DependsOn<blocks::density::mass::value>,
|
|
models::Affects<blocks::enthalpy::specific::residual>,
|
|
models::GlobalScalarNormalization<
|
|
models::PhysicalScaleLaw::dimensionless,
|
|
models::PhysicalScaleLaw::specific_energy>>;
|
|
|
|
explicit constexpr SelfDescribingMagneticSpecificEnergy(const Parameters parameters) noexcept
|
|
: m_target(parameters.target) {
|
|
}
|
|
|
|
private:
|
|
double m_target;
|
|
};
|
|
|
|
class MissingGeneratedNormalization final {
|
|
public:
|
|
struct Parameters final {
|
|
double target;
|
|
};
|
|
|
|
using ModelDefinition = mean_field::integral::FixedWithMultiplier<
|
|
MissingGeneratedNormalization,
|
|
"NormalizationMockMissing",
|
|
models::DependsOn<blocks::density::mass::value>,
|
|
models::Affects<blocks::enthalpy::specific::residual>>;
|
|
|
|
explicit constexpr MissingGeneratedNormalization(const Parameters parameters) noexcept
|
|
: m_target(parameters.target) {
|
|
}
|
|
|
|
private:
|
|
double m_target;
|
|
};
|
|
|
|
class GeneratedVolumeCoordinateWithoutMetricSource final {
|
|
public:
|
|
struct Parameters final {
|
|
double target;
|
|
};
|
|
|
|
using ModelDefinition = mean_field::integral::FixedWithPhysicalCoordinate<
|
|
GeneratedVolumeCoordinateWithoutMetricSource,
|
|
"NormalizationMockVolumeCoordinate",
|
|
models::DependsOn<blocks::density::mass::value>,
|
|
models::Affects<blocks::enthalpy::specific::residual>,
|
|
models::GeneratedNormalization<VolumeSpecificEnergyNormalization, GlobalSpecificEnergyNormalization>>;
|
|
|
|
explicit constexpr GeneratedVolumeCoordinateWithoutMetricSource(const Parameters parameters) noexcept
|
|
: m_target(parameters.target) {
|
|
}
|
|
|
|
private:
|
|
double m_target;
|
|
};
|
|
|
|
struct MalformedGeneratedNormalization final { };
|
|
|
|
class MalformedGeneratedNormalizationConstraint final {
|
|
public:
|
|
struct Parameters final {
|
|
double target;
|
|
};
|
|
|
|
using ModelDefinition = mean_field::integral::FixedWithMultiplier<
|
|
MalformedGeneratedNormalizationConstraint,
|
|
"NormalizationMockMalformed",
|
|
models::DependsOn<blocks::density::mass::value>,
|
|
models::Affects<blocks::enthalpy::specific::residual>,
|
|
MalformedGeneratedNormalization>;
|
|
|
|
explicit constexpr MalformedGeneratedNormalizationConstraint(const Parameters parameters) noexcept
|
|
: m_target(parameters.target) {
|
|
}
|
|
|
|
private:
|
|
double m_target;
|
|
};
|
|
|
|
template <typename Specification>
|
|
using GeneratedValueBlock = blocks::generated_value_block<models::PhysicalCoordinateFor<Specification>>;
|
|
|
|
template <typename Specification>
|
|
using GeneratedMultiplierBlock = blocks::generated_value_block<models::MultiplierFor<Specification>>;
|
|
|
|
template <typename Specification>
|
|
using GeneratedResidualBlock = blocks::generated_residual_block<models::ResidualFor<Specification>>;
|
|
|
|
using SelfDescribingValue = GeneratedValueBlock<SelfDescribingMagneticSpecificEnergy>;
|
|
using SelfDescribingResidual = GeneratedResidualBlock<SelfDescribingMagneticSpecificEnergy>;
|
|
using SelfDescribingForm =
|
|
blocks::block_form<blocks::type_list<SelfDescribingValue>, blocks::type_list<SelfDescribingResidual>>;
|
|
|
|
using MissingValue = GeneratedMultiplierBlock<MissingGeneratedNormalization>;
|
|
using MissingResidual = GeneratedResidualBlock<MissingGeneratedNormalization>;
|
|
using MissingNormalizationForm =
|
|
blocks::block_form<blocks::type_list<MissingValue>, blocks::type_list<MissingResidual>>;
|
|
|
|
using UnpreparedVolumeValue = GeneratedValueBlock<GeneratedVolumeCoordinateWithoutMetricSource>;
|
|
using UnpreparedVolumeResidual = GeneratedResidualBlock<GeneratedVolumeCoordinateWithoutMetricSource>;
|
|
using UnpreparedVolumeForm =
|
|
blocks::block_form<blocks::type_list<UnpreparedVolumeValue>, blocks::type_list<UnpreparedVolumeResidual>>;
|
|
|
|
class DenseOperator final : public mfem::Operator {
|
|
public:
|
|
explicit DenseOperator(const mfem::DenseMatrix &matrix)
|
|
: mfem::Operator(
|
|
matrix.Height(),
|
|
matrix.Width()
|
|
),
|
|
m_matrix(matrix) {
|
|
}
|
|
|
|
void Mult(
|
|
const mfem::Vector &input,
|
|
mfem::Vector &output
|
|
) const override {
|
|
m_matrix.Mult(input, output);
|
|
}
|
|
|
|
private:
|
|
mfem::DenseMatrix m_matrix;
|
|
};
|
|
|
|
class DenseInverseSolver final : public mfem::Solver {
|
|
public:
|
|
explicit DenseInverseSolver(const mfem::DenseMatrix &inverse)
|
|
: mfem::Solver(
|
|
inverse.Height(),
|
|
inverse.Width()
|
|
),
|
|
m_inverse(inverse) {
|
|
}
|
|
|
|
void SetOperator(const mfem::Operator &operation) override {
|
|
if (operation.Height() != Height() || operation.Width() != Width()) {
|
|
throw std::invalid_argument("The dense inverse received an incompatible operator.");
|
|
}
|
|
m_boundOperator = &operation;
|
|
++m_bindings;
|
|
}
|
|
|
|
void Mult(
|
|
const mfem::Vector &input,
|
|
mfem::Vector &output
|
|
) const override {
|
|
if (m_boundOperator == nullptr) {
|
|
throw std::logic_error("The dense inverse must be bound before application.");
|
|
}
|
|
m_inverse.Mult(input, output);
|
|
}
|
|
|
|
[[nodiscard]] const mfem::Operator *BoundOperator() const noexcept {
|
|
return m_boundOperator;
|
|
}
|
|
|
|
[[nodiscard]] int Bindings() const noexcept {
|
|
return m_bindings;
|
|
}
|
|
|
|
private:
|
|
mfem::DenseMatrix m_inverse;
|
|
const mfem::Operator *m_boundOperator{nullptr};
|
|
int m_bindings{0};
|
|
};
|
|
|
|
[[nodiscard]] mfem::Vector vector(std::initializer_list<double> values) {
|
|
mfem::Vector result(static_cast<int>(values.size()));
|
|
int index = 0;
|
|
for (const double value : values) {
|
|
result(index++) = value;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void checkVector(
|
|
const mfem::Vector &actual,
|
|
const mfem::Vector &expected,
|
|
const double epsilon = 2.0e-13
|
|
) {
|
|
REQUIRE(actual.Size() == expected.Size());
|
|
for (int index = 0; index < actual.Size(); ++index) {
|
|
CHECK(actual(index) == Catch::Approx(expected(index)).epsilon(epsilon).margin(1.0e-300));
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE(
|
|
"Pointer-Retaining Normalization Operators Reject Temporary Dependencies",
|
|
"[normalization][type][lifetime]"
|
|
) {
|
|
using Map = normalization::DiagonalNormalization;
|
|
|
|
STATIC_CHECK(std::constructible_from<normalization::ScaledJacobianOperator, const DenseOperator &, const Map &>);
|
|
STATIC_CHECK_FALSE(std::constructible_from<normalization::ScaledJacobianOperator, DenseOperator &&, const Map &>);
|
|
STATIC_CHECK_FALSE(
|
|
std::constructible_from<normalization::ScaledJacobianOperator, const DenseOperator &&, const Map &>
|
|
);
|
|
STATIC_CHECK_FALSE(std::constructible_from<normalization::ScaledJacobianOperator, const DenseOperator &, Map &&>);
|
|
|
|
STATIC_CHECK(std::constructible_from<normalization::ScaledInverseOperator, const DenseOperator &, const Map &>);
|
|
STATIC_CHECK_FALSE(std::constructible_from<normalization::ScaledInverseOperator, DenseOperator &&, const Map &>);
|
|
STATIC_CHECK_FALSE(std::constructible_from<normalization::ScaledInverseOperator, const DenseOperator &, Map &&>);
|
|
|
|
STATIC_CHECK(
|
|
std::constructible_from<
|
|
normalization::ScaledPreconditioner, DenseInverseSolver &, const DenseOperator &, const DenseOperator &,
|
|
const Map &>
|
|
);
|
|
STATIC_CHECK_FALSE(
|
|
std::constructible_from<
|
|
normalization::ScaledPreconditioner, DenseInverseSolver &&, const DenseOperator &, const DenseOperator &,
|
|
const Map &>
|
|
);
|
|
STATIC_CHECK_FALSE(
|
|
std::constructible_from<
|
|
normalization::ScaledPreconditioner, DenseInverseSolver &, DenseOperator &&, const DenseOperator &,
|
|
const Map &>
|
|
);
|
|
STATIC_CHECK_FALSE(
|
|
std::constructible_from<
|
|
normalization::ScaledPreconditioner, DenseInverseSolver &, const DenseOperator &, DenseOperator &&,
|
|
const Map &>
|
|
);
|
|
STATIC_CHECK_FALSE(
|
|
std::constructible_from<
|
|
normalization::ScaledPreconditioner, DenseInverseSolver &, const DenseOperator &, const DenseOperator &,
|
|
Map &&>
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Characteristic Stellar Scales Satisfy Gravity Virial And Rotation Identities",
|
|
"[normalization][physics]"
|
|
) {
|
|
using namespace mean_field;
|
|
|
|
constexpr double mass = 7.0;
|
|
constexpr double radius = 3.0;
|
|
constexpr double gravity = 5.0;
|
|
const auto scales = normalization::deriveStellarCharacteristicScales(
|
|
dimensions::MassValue{mass}, dimensions::LengthValue{radius}, gravity
|
|
);
|
|
|
|
CHECK(scales.density == Catch::Approx(mass / std::pow(radius, 3)));
|
|
CHECK(scales.acceleration == Catch::Approx(gravity * mass / std::pow(radius, 2)));
|
|
CHECK(scales.specificEnergy == Catch::Approx(gravity * mass / radius));
|
|
CHECK(scales.pressure == Catch::Approx(gravity * mass * mass / std::pow(radius, 4)));
|
|
CHECK(scales.angularVelocity == Catch::Approx(std::sqrt(gravity * mass / std::pow(radius, 3))));
|
|
CHECK(scales.angularMomentum == Catch::Approx(mass * std::sqrt(gravity * mass * radius)));
|
|
|
|
// Hydrostatic/virial energy scales agree: P R^3 = M Phi = F R.
|
|
const double virial = scales.pressure * std::pow(radius, 3);
|
|
CHECK(virial == Catch::Approx(mass * scales.specificEnergy).epsilon(2.0e-15));
|
|
CHECK(virial == Catch::Approx(scales.force * radius).epsilon(2.0e-15));
|
|
|
|
// Omega_0 is the Kepler/break-up scale and J_0 = M R^2 Omega_0.
|
|
CHECK(
|
|
scales.angularVelocity * scales.angularVelocity * radius == Catch::Approx(scales.acceleration).epsilon(2.0e-15)
|
|
);
|
|
CHECK(scales.angularMomentum == Catch::Approx(mass * radius * radius * scales.angularVelocity).epsilon(2.0e-15));
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Characteristic Scales Obey The Expected Stellar Homology Exponents",
|
|
"[normalization][physics]"
|
|
) {
|
|
using namespace mean_field;
|
|
|
|
const auto reference =
|
|
normalization::deriveStellarCharacteristicScales(dimensions::MassValue{2.5}, dimensions::LengthValue{4.0}, 3.0);
|
|
constexpr double massFactor = 11.0;
|
|
constexpr double radiusFactor = 0.2;
|
|
constexpr double gravityFactor = 7.0;
|
|
const auto transformed = normalization::deriveStellarCharacteristicScales(
|
|
dimensions::MassValue{2.5 * massFactor}, dimensions::LengthValue{4.0 * radiusFactor}, 3.0 * gravityFactor
|
|
);
|
|
|
|
CHECK(
|
|
transformed.density / reference.density ==
|
|
Catch::Approx(massFactor / std::pow(radiusFactor, 3)).epsilon(4.0e-15)
|
|
);
|
|
CHECK(
|
|
transformed.acceleration / reference.acceleration ==
|
|
Catch::Approx(gravityFactor * massFactor / std::pow(radiusFactor, 2)).epsilon(4.0e-15)
|
|
);
|
|
CHECK(
|
|
transformed.inverseTimeSquared / reference.inverseTimeSquared ==
|
|
Catch::Approx(gravityFactor * massFactor / std::pow(radiusFactor, 3)).epsilon(4.0e-15)
|
|
);
|
|
CHECK(
|
|
transformed.specificEnergy / reference.specificEnergy ==
|
|
Catch::Approx(gravityFactor * massFactor / radiusFactor).epsilon(4.0e-15)
|
|
);
|
|
CHECK(
|
|
transformed.pressure / reference.pressure ==
|
|
Catch::Approx(gravityFactor * massFactor * massFactor / std::pow(radiusFactor, 4)).epsilon(4.0e-15)
|
|
);
|
|
CHECK(
|
|
transformed.angularVelocity / reference.angularVelocity ==
|
|
Catch::Approx(std::sqrt(gravityFactor * massFactor / std::pow(radiusFactor, 3))).epsilon(4.0e-15)
|
|
);
|
|
CHECK(
|
|
transformed.angularMomentum / reference.angularMomentum ==
|
|
Catch::Approx(massFactor * std::sqrt(gravityFactor * massFactor * radiusFactor)).epsilon(4.0e-15)
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Physical Block Scales Distinguish Invariants From Numerical Phase Conditions",
|
|
"[normalization][physics]"
|
|
) {
|
|
using namespace mean_field;
|
|
|
|
const auto scales =
|
|
normalization::deriveStellarCharacteristicScales(dimensions::MassValue{9.0}, dimensions::LengthValue{2.0}, 4.0);
|
|
CHECK(normalization::physicalScale<blocks::density::mass::value>(scales) == scales.density);
|
|
CHECK(normalization::physicalScale<blocks::gravity::gradient::value>(scales) == scales.acceleration);
|
|
CHECK(normalization::physicalScale<blocks::gravity::poisson::residual>(scales) == scales.inverseTimeSquared);
|
|
CHECK(normalization::physicalScale<blocks::fixed_total_mass::mass_normalization::residual>(scales) == 9.0);
|
|
CHECK(
|
|
normalization::physicalScale<blocks::fixed_angular_momentum::angular_velocity::value>(scales) ==
|
|
scales.angularVelocity
|
|
);
|
|
CHECK(
|
|
normalization::physicalScale<blocks::fixed_angular_momentum::angular_velocity::residual>(scales) ==
|
|
scales.angularMomentum
|
|
);
|
|
|
|
// The central-density condition is implemented as h(0)-h_target, so its residual scale is energy/mass,
|
|
// despite the physical target being expressed as a density.
|
|
CHECK(
|
|
normalization::physicalScale<blocks::fixed_central_density::central_value::residual>(scales) ==
|
|
scales.specificEnergy
|
|
);
|
|
CHECK(
|
|
normalization::physicalScale<blocks::fixed_central_density::central_value::residual>(scales) != scales.density
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Generated Physical Riesz Laws Come From A Self-Describing Physics Specification",
|
|
"[normalization][type][extension]"
|
|
) {
|
|
using namespace mean_field;
|
|
using ValueTraits = normalization::PhysicalRieszBlockTraits<SelfDescribingValue>;
|
|
using ResidualTraits = normalization::PhysicalRieszBlockTraits<SelfDescribingResidual>;
|
|
|
|
STATIC_CHECK(models::SelfDescribingModelSpecification<SelfDescribingMagneticSpecificEnergy>);
|
|
STATIC_CHECK(models::CompleteGeneratedNormalizationFor<SelfDescribingMagneticSpecificEnergy>);
|
|
STATIC_CHECK(operators::StellarEquilibriumSpecificationCompilable<SelfDescribingMagneticSpecificEnergy>);
|
|
STATIC_CHECK(
|
|
normalization::GeneratedValuePhysicalRieszNormalizable<
|
|
models::PhysicalCoordinateFor<SelfDescribingMagneticSpecificEnergy>>
|
|
);
|
|
STATIC_CHECK(
|
|
normalization::GeneratedResidualPhysicalRieszNormalizable<
|
|
models::ResidualFor<SelfDescribingMagneticSpecificEnergy>>
|
|
);
|
|
STATIC_CHECK(normalization::CompleteGeneratedPhysicalRieszNormalizationFor<SelfDescribingMagneticSpecificEnergy>);
|
|
STATIC_CHECK(normalization::CompilableNormalizationFor<normalization::PhysicalRieszDiagonal<>, SelfDescribingForm>);
|
|
STATIC_CHECK(normalization::RegisteredStellarSpecificationNormalization<SelfDescribingMagneticSpecificEnergy>);
|
|
STATIC_CHECK(
|
|
normalization::CompleteStellarSpecificationNormalizationFor<
|
|
SelfDescribingMagneticSpecificEnergy, SelfDescribingForm>
|
|
);
|
|
|
|
STATIC_CHECK(ValueTraits::Method::topology == normalization::RieszTopology::global_scalar);
|
|
STATIC_CHECK(ValueTraits::Method::scale == normalization::PhysicalScaleKind::dimensionless);
|
|
STATIC_CHECK(ResidualTraits::Method::topology == normalization::RieszTopology::global_scalar);
|
|
STATIC_CHECK(ResidualTraits::Method::scale == normalization::PhysicalScaleKind::specific_energy);
|
|
|
|
const auto scales =
|
|
normalization::deriveStellarCharacteristicScales(dimensions::MassValue{9.0}, dimensions::LengthValue{2.0}, 4.0);
|
|
const blocks::form_layout<SelfDescribingForm> layout({1}, {1});
|
|
normalization::DiagonalNormalizationBuilder<SelfDescribingForm> builder(layout);
|
|
normalization::StellarSpecificationNormalizationContribution<SelfDescribingMagneticSpecificEnergy>::Apply(
|
|
builder, scales
|
|
);
|
|
const normalization::DiagonalNormalization map = std::move(builder).Build();
|
|
|
|
REQUIRE(map.StateFactors().Size() == 1);
|
|
REQUIRE(map.ResidualFactors().Size() == 1);
|
|
CHECK(map.StateFactors()(0) == Catch::Approx(1.0).epsilon(2.0e-15));
|
|
CHECK(map.ResidualFactors()(0) == Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15));
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Generated Normalization Completeness Is SFINAE Safe And Rejects Missing Runtime Metrics",
|
|
"[normalization][type][validation]"
|
|
) {
|
|
using namespace mean_field;
|
|
|
|
STATIC_CHECK_FALSE(normalization::CompleteGeneratedPhysicalRieszNormalizationFor<int>);
|
|
STATIC_CHECK_FALSE(normalization::RegisteredStellarSpecificationNormalization<int>);
|
|
STATIC_CHECK_FALSE(normalization::CompleteStellarNormalizationFor<int, SelfDescribingForm>);
|
|
|
|
STATIC_CHECK(models::ModelSpecification<MissingGeneratedNormalization>);
|
|
STATIC_CHECK_FALSE(models::CompleteGeneratedNormalizationFor<MissingGeneratedNormalization>);
|
|
STATIC_CHECK_FALSE(normalization::CompleteGeneratedPhysicalRieszNormalizationFor<MissingGeneratedNormalization>);
|
|
STATIC_CHECK_FALSE(
|
|
normalization::CompilableNormalizationFor<normalization::PhysicalRieszDiagonal<>, MissingNormalizationForm>
|
|
);
|
|
STATIC_CHECK_FALSE(normalization::RegisteredStellarSpecificationNormalization<MissingGeneratedNormalization>);
|
|
STATIC_CHECK_FALSE(
|
|
normalization::CompleteStellarSpecificationNormalizationFor<
|
|
MissingGeneratedNormalization, MissingNormalizationForm>
|
|
);
|
|
|
|
STATIC_CHECK(models::ModelSpecification<MalformedGeneratedNormalizationConstraint>);
|
|
STATIC_CHECK_FALSE(models::CompleteGeneratedNormalizationFor<MalformedGeneratedNormalizationConstraint>);
|
|
STATIC_CHECK_FALSE(
|
|
normalization::CompleteGeneratedPhysicalRieszNormalizationFor<MalformedGeneratedNormalizationConstraint>
|
|
);
|
|
STATIC_CHECK_FALSE(
|
|
normalization::RegisteredStellarSpecificationNormalization<MalformedGeneratedNormalizationConstraint>
|
|
);
|
|
|
|
// The declaration itself is a valid Riesz law, but runtime stellar
|
|
// preparation has no finite-element Gram source for a generated volume
|
|
// field. The stronger runtime concept must therefore reject it.
|
|
STATIC_CHECK(
|
|
normalization::CompleteGeneratedPhysicalRieszNormalizationFor<GeneratedVolumeCoordinateWithoutMetricSource>
|
|
);
|
|
STATIC_CHECK(
|
|
normalization::CompilableNormalizationFor<normalization::PhysicalRieszDiagonal<>, UnpreparedVolumeForm>
|
|
);
|
|
STATIC_CHECK_FALSE(
|
|
normalization::RegisteredStellarSpecificationNormalization<GeneratedVolumeCoordinateWithoutMetricSource>
|
|
);
|
|
STATIC_CHECK_FALSE(
|
|
normalization::CompleteStellarSpecificationNormalizationFor<
|
|
GeneratedVolumeCoordinateWithoutMetricSource, UnpreparedVolumeForm>
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Characteristic Scale Construction Rejects Invalid Or Overflowing References",
|
|
"[normalization][validation]"
|
|
) {
|
|
using namespace mean_field;
|
|
|
|
CHECK_THROWS_AS(
|
|
normalization::deriveStellarCharacteristicScales(dimensions::MassValue{0.0}, dimensions::LengthValue{1.0}, 1.0),
|
|
std::invalid_argument
|
|
);
|
|
CHECK_THROWS_AS(
|
|
normalization::deriveStellarCharacteristicScales(
|
|
dimensions::MassValue{1.0}, dimensions::LengthValue{-1.0}, 1.0
|
|
),
|
|
std::invalid_argument
|
|
);
|
|
CHECK_THROWS_AS(
|
|
(normalization::PhysicalRieszDiagonal{dimensions::LengthValue{1.0}, std::numeric_limits<double>::quiet_NaN()}),
|
|
std::invalid_argument
|
|
);
|
|
CHECK_THROWS_AS(
|
|
normalization::deriveStellarCharacteristicScales(
|
|
dimensions::MassValue{1.0e300}, dimensions::LengthValue{1.0e-200}, 1.0e100
|
|
),
|
|
std::overflow_error
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Diagonal Riesz Maps Reproduce Primal And Dual Norms Across Extreme Metrics",
|
|
"[normalization][math]"
|
|
) {
|
|
const blocks::form_layout<TestForm> layout({3}, {3});
|
|
normalization::DiagonalNormalizationBuilder<TestForm> builder(layout);
|
|
const mfem::Vector gram = vector({1.0e-20, 4.0, 9.0e20});
|
|
constexpr double stateScale = 10.0;
|
|
constexpr double residualScale = 0.25;
|
|
builder.SetValueBlock<TestValue>(stateScale, gram);
|
|
builder.SetResidualBlock<TestResidual>(residualScale, gram);
|
|
const normalization::DiagonalNormalization map = std::move(builder).Build();
|
|
|
|
const mfem::Vector state = vector({3.0e10, -2.0, 4.0e-10});
|
|
const mfem::Vector residual = vector({2.0e-10, -3.0, 5.0e10});
|
|
double expectedPrimalNormSquared = 0.0;
|
|
double expectedDualNormSquared = 0.0;
|
|
for (int index = 0; index < gram.Size(); ++index) {
|
|
expectedPrimalNormSquared += gram(index) * state(index) * state(index) / (stateScale * stateScale);
|
|
expectedDualNormSquared += residual(index) * residual(index) / (gram(index) * residualScale * residualScale);
|
|
}
|
|
CHECK(map.LocalStateNormSquared(state) == Catch::Approx(expectedPrimalNormSquared).epsilon(3.0e-15));
|
|
CHECK(map.LocalResidualNormSquared(residual) == Catch::Approx(expectedDualNormSquared).epsilon(3.0e-15));
|
|
|
|
mfem::Vector normalizedState;
|
|
mfem::Vector recoveredState;
|
|
mfem::Vector normalizedResidual;
|
|
mfem::Vector recoveredResidual;
|
|
map.NormalizeState(state, normalizedState);
|
|
map.DenormalizeState(normalizedState, recoveredState);
|
|
map.NormalizeResidual(residual, normalizedResidual);
|
|
map.DenormalizeResidual(normalizedResidual, recoveredResidual);
|
|
checkVector(recoveredState, state, 3.0e-15);
|
|
checkVector(recoveredResidual, residual, 3.0e-15);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Hybrid Riesz Rows Replace Missing Volume Metrics With Point Metrics",
|
|
"[normalization][math]"
|
|
) {
|
|
using HybridForm = blocks::block_form<
|
|
blocks::type_list<blocks::enthalpy::specific::value>, blocks::type_list<blocks::enthalpy::specific::residual>>;
|
|
const blocks::form_layout<HybridForm> layout({4}, {4});
|
|
normalization::DiagonalNormalizationBuilder<HybridForm> builder(layout);
|
|
builder.SetValueBlock<blocks::enthalpy::specific::value>(2.0, vector({2.0, 3.0, 5.0, 7.0}));
|
|
|
|
// Replaced isobaric rows may have zero bulk mass because they are no longer volume weak rows.
|
|
const mfem::Vector bulkMetric = vector({4.0, 0.0, 16.0, 0.0});
|
|
const std::array<int, 2> pointRows{1, 3};
|
|
builder.SetHybridResidualBlock<blocks::enthalpy::specific::residual>(
|
|
5.0, bulkMetric, std::span<const int>{pointRows}, 1.0
|
|
);
|
|
const auto map = std::move(builder).Build();
|
|
CHECK(map.ResidualFactors()(0) == Catch::Approx(1.0 / 10.0));
|
|
CHECK(map.ResidualFactors()(1) == Catch::Approx(1.0 / 5.0));
|
|
CHECK(map.ResidualFactors()(2) == Catch::Approx(1.0 / 20.0));
|
|
CHECK(map.ResidualFactors()(3) == Catch::Approx(1.0 / 5.0));
|
|
|
|
normalization::DiagonalNormalizationBuilder<HybridForm> duplicateRows(layout);
|
|
duplicateRows.SetValueGlobal<blocks::enthalpy::specific::value>(1.0);
|
|
const std::array<int, 2> duplicates{1, 1};
|
|
CHECK_THROWS_AS(
|
|
duplicateRows.SetHybridResidualBlock<blocks::enthalpy::specific::residual>(
|
|
1.0, vector({1.0, 1.0, 1.0, 1.0}), std::span<const int>{duplicates}
|
|
),
|
|
std::invalid_argument
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Runtime Normalization Assembly Rejects Missing Duplicate And Invalid Data",
|
|
"[normalization][validation]"
|
|
) {
|
|
const blocks::form_layout<TestForm> layout({2}, {2});
|
|
|
|
normalization::DiagonalNormalizationBuilder<TestForm> missing(layout);
|
|
missing.SetValueBlock<TestValue>(1.0, vector({1.0, 1.0}));
|
|
CHECK_THROWS_AS(std::move(missing).Build(), std::logic_error);
|
|
|
|
normalization::DiagonalNormalizationBuilder<TestForm> duplicate(layout);
|
|
duplicate.SetValueBlock<TestValue>(1.0, vector({1.0, 1.0}));
|
|
CHECK_THROWS_AS(duplicate.SetValueBlock<TestValue>(1.0, vector({1.0, 1.0})), std::logic_error);
|
|
|
|
normalization::DiagonalNormalizationBuilder<TestForm> zeroMetric(layout);
|
|
CHECK_THROWS_AS(zeroMetric.SetValueBlock<TestValue>(1.0, vector({1.0, 0.0})), std::invalid_argument);
|
|
|
|
normalization::DiagonalNormalizationBuilder<TestForm> wrongSize(layout);
|
|
CHECK_THROWS_AS(wrongSize.SetResidualBlock<TestResidual>(1.0, vector({1.0})), std::invalid_argument);
|
|
|
|
CHECK_THROWS_AS(
|
|
normalization::DiagonalNormalization(vector({1.0, std::numeric_limits<double>::infinity()}), vector({1.0})),
|
|
std::invalid_argument
|
|
);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Scaled Jacobian And Inverse Implement The Exact Coordinate Change",
|
|
"[normalization][linear-algebra]"
|
|
) {
|
|
const mfem::Vector stateFactors = vector({1.0e-9, 2.0e7});
|
|
const mfem::Vector residualFactors = vector({5.0e8, 3.0e-6});
|
|
const normalization::DiagonalNormalization map(stateFactors, residualFactors);
|
|
|
|
// Start from a well-conditioned normalized Jacobian A_hat and form the dimensional
|
|
// J = L^{-1} A_hat R^{-1}. Its entries span the physical unit ranges, while L J R
|
|
// must recover A_hat rather than an artificially ill-conditioned dense matrix.
|
|
constexpr double normalizedMatrix[2][2]{{4.0, 1.0}, {2.0, 3.0}};
|
|
constexpr double normalizedInverse[2][2]{{0.3, -0.1}, {-0.2, 0.4}};
|
|
mfem::DenseMatrix matrix(2);
|
|
mfem::DenseMatrix inverse(2);
|
|
for (int row = 0; row < 2; ++row) {
|
|
for (int column = 0; column < 2; ++column) {
|
|
matrix(row, column) = normalizedMatrix[row][column] * stateFactors(column) / residualFactors(row);
|
|
inverse(row, column) = normalizedInverse[row][column] * residualFactors(column) / stateFactors(row);
|
|
}
|
|
}
|
|
const DenseOperator physicalJacobian(matrix);
|
|
const DenseOperator physicalInverse(inverse);
|
|
const normalization::ScaledJacobianOperator scaledJacobian(physicalJacobian, map);
|
|
const normalization::ScaledInverseOperator scaledInverse(physicalInverse, map);
|
|
|
|
const mfem::Vector direction = vector({0.75, -1.25});
|
|
mfem::Vector action;
|
|
scaledJacobian.Mult(direction, action);
|
|
mfem::Vector expected(2);
|
|
expected(0) = 4.0 * direction(0) + direction(1);
|
|
expected(1) = 2.0 * direction(0) + 3.0 * direction(1);
|
|
checkVector(action, expected, 4.0e-15);
|
|
|
|
mfem::Vector recovered;
|
|
scaledInverse.Mult(action, recovered);
|
|
checkVector(recovered, direction, 2.0e-13);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Scaled Preconditioning Routes An Exact Physical Inverse Through FGMRES",
|
|
"[normalization][solver]"
|
|
) {
|
|
const mfem::Vector stateFactors = vector({1.0e-9, 2.0e7});
|
|
const mfem::Vector residualFactors = vector({5.0e8, 3.0e-6});
|
|
const normalization::DiagonalNormalization map(stateFactors, residualFactors);
|
|
|
|
constexpr double normalizedMatrix[2][2]{{4.0, 1.0}, {2.0, 3.0}};
|
|
constexpr double normalizedInverse[2][2]{{0.3, -0.1}, {-0.2, 0.4}};
|
|
mfem::DenseMatrix physicalMatrix(2);
|
|
mfem::DenseMatrix physicalInverseMatrix(2);
|
|
for (int row = 0; row < 2; ++row) {
|
|
for (int column = 0; column < 2; ++column) {
|
|
physicalMatrix(row, column) = normalizedMatrix[row][column] * stateFactors(column) / residualFactors(row);
|
|
physicalInverseMatrix(row, column) =
|
|
normalizedInverse[row][column] * residualFactors(column) / stateFactors(row);
|
|
}
|
|
}
|
|
|
|
const DenseOperator physicalJacobian(physicalMatrix);
|
|
const normalization::ScaledJacobianOperator scaledJacobian(physicalJacobian, map);
|
|
DenseInverseSolver physicalInverse(physicalInverseMatrix);
|
|
normalization::ScaledPreconditioner scaledPreconditioner(physicalInverse, physicalJacobian, scaledJacobian, map);
|
|
|
|
CHECK(physicalInverse.BoundOperator() == &physicalJacobian);
|
|
CHECK(&scaledPreconditioner.GetPhysicalJacobian() == &physicalJacobian);
|
|
CHECK(&scaledPreconditioner.GetNormalizedJacobian() == &scaledJacobian);
|
|
|
|
const mfem::Vector rightHandSide = vector({1.5, -0.75});
|
|
mfem::Vector directCorrection(2);
|
|
scaledPreconditioner.Mult(rightHandSide, directCorrection);
|
|
const mfem::Vector expected = vector({0.525, -0.6});
|
|
checkVector(directCorrection, expected, 3.0e-13);
|
|
|
|
mfem::FGMRESSolver krylov(MPI_COMM_WORLD);
|
|
krylov.SetRelTol(1.0e-13);
|
|
krylov.SetAbsTol(1.0e-15);
|
|
krylov.SetMaxIter(4);
|
|
krylov.SetKDim(2);
|
|
krylov.SetPrintLevel(0);
|
|
krylov.SetPreconditioner(scaledPreconditioner);
|
|
krylov.SetOperator(scaledJacobian);
|
|
|
|
mfem::Vector solution(2);
|
|
solution = 0.0;
|
|
krylov.Mult(rightHandSide, solution);
|
|
CHECK(krylov.GetConverged());
|
|
CHECK(krylov.GetNumIterations() <= 1);
|
|
checkVector(solution, expected, 3.0e-13);
|
|
CHECK(physicalInverse.BoundOperator() == &physicalJacobian);
|
|
CHECK(physicalInverse.Bindings() >= 2);
|
|
CHECK(scaledPreconditioner.GetStatistics().operatorBindings >= 2);
|
|
CHECK(scaledPreconditioner.GetStatistics().applications >= 2);
|
|
|
|
mfem::IdentityOperator differentNormalizedJacobian(2);
|
|
CHECK_THROWS_AS(scaledPreconditioner.SetOperator(differentNormalizedJacobian), std::invalid_argument);
|
|
mfem::IdentityOperator wrongSize(3);
|
|
CHECK_THROWS_AS(scaledPreconditioner.SetOperator(wrongSize), std::invalid_argument);
|
|
mfem::Vector wrongCorrection(1);
|
|
CHECK_THROWS_AS(scaledPreconditioner.Mult(rightHandSide, wrongCorrection), std::invalid_argument);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"Physical Riesz Scaling Collapses A Forty-Eight-Decade Diagonal Imbalance",
|
|
"[normalization][numerics]"
|
|
) {
|
|
const mfem::Vector stateFactors = vector({1.0e-12, 1.0, 1.0e12});
|
|
const mfem::Vector residualFactors = vector({1.0e12, 1.0, 1.0e-12});
|
|
const normalization::DiagonalNormalization map(stateFactors, residualFactors);
|
|
|
|
mfem::DenseMatrix physicalMatrix(3);
|
|
physicalMatrix = 0.0;
|
|
for (int index = 0; index < 3; ++index) {
|
|
physicalMatrix(index, index) = stateFactors(index) / residualFactors(index);
|
|
}
|
|
CHECK(physicalMatrix(2, 2) / physicalMatrix(0, 0) == Catch::Approx(1.0e48));
|
|
|
|
const DenseOperator physicalJacobian(physicalMatrix);
|
|
const normalization::ScaledJacobianOperator scaledJacobian(physicalJacobian, map);
|
|
const mfem::Vector direction = vector({-2.0, 3.5, 0.125});
|
|
mfem::Vector action;
|
|
scaledJacobian.Mult(direction, action);
|
|
checkVector(action, direction, 4.0e-15);
|
|
}
|
|
|
|
TEST_CASE(
|
|
"A Compiled Stellar Problem Prepares Reference Physical Riesz Coordinates",
|
|
"[normalization][integration]"
|
|
) {
|
|
using namespace mean_field;
|
|
|
|
utils::Args args = test_utils::setup_args();
|
|
fem::FEM finiteElements = fem::setup_fem(args.mesh_file, args, 0);
|
|
REQUIRE(finiteElements.okay());
|
|
|
|
constexpr double targetMass = 2.0;
|
|
constexpr double referenceRadius = 1.25;
|
|
constexpr double gravitationalConstant = 3.0;
|
|
const normalization::PhysicalRieszDiagonal policy{dimensions::LengthValue{referenceRadius}, gravitationalConstant};
|
|
auto discretization = equilibrium::makeStellarDiscretization(std::move(finiteElements), policy);
|
|
auto problem = equilibrium::discretize(
|
|
model::StellarModel(
|
|
eos::Polytrope({.n = 3.0, .K = 0.25}), surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
|
|
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{targetMass}}),
|
|
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{1.0}})
|
|
),
|
|
std::move(discretization)
|
|
);
|
|
|
|
using ProblemType = std::remove_cvref_t<decltype(problem)>;
|
|
using Form = typename ProblemType::FormType;
|
|
using ModelType = std::remove_cvref_t<decltype(problem.GetStellarModel())>;
|
|
STATIC_CHECK(SupportsModelDerivedStellarScales<ModelType>);
|
|
STATIC_CHECK_FALSE(SupportsModelDerivedStellarScales<ModelWithoutFixedTotalMass>);
|
|
STATIC_CHECK(
|
|
std::same_as<typename ProblemType::NormalizationPrescriptionType, std::remove_cvref_t<decltype(policy)>>
|
|
);
|
|
CHECK(problem.GetNormalizationPrescription().referenceRadius() == dimensions::LengthValue{referenceRadius});
|
|
|
|
const normalization::DiagonalNormalization map = normalization::prepareNormalization(problem);
|
|
REQUIRE(map.StateSize() == problem.StateSize());
|
|
REQUIRE(map.ResidualSize() == problem.EquationSize());
|
|
for (int index = 0; index < map.StateSize(); ++index) {
|
|
CHECK(std::isfinite(map.StateFactors()(index)));
|
|
CHECK(map.StateFactors()(index) > 0.0);
|
|
}
|
|
for (int index = 0; index < map.ResidualSize(); ++index) {
|
|
CHECK(std::isfinite(map.ResidualFactors()(index)));
|
|
CHECK(map.ResidualFactors()(index) > 0.0);
|
|
}
|
|
|
|
const auto scales = normalization::deriveStellarCharacteristicScales(policy, problem.GetStellarModel());
|
|
const auto &layout = problem.GetManifest().layout();
|
|
constexpr int massValueBlock =
|
|
blocks::type_index_v<blocks::fixed_total_mass::mass_normalization::value, typename Form::value_blocks>;
|
|
constexpr int massResidualBlock =
|
|
blocks::type_index_v<blocks::fixed_total_mass::mass_normalization::residual, typename Form::residual_blocks>;
|
|
constexpr int phaseValueBlock =
|
|
blocks::type_index_v<blocks::fixed_central_density::central_value::value, typename Form::value_blocks>;
|
|
constexpr int phaseResidualBlock =
|
|
blocks::type_index_v<blocks::fixed_central_density::central_value::residual, typename Form::residual_blocks>;
|
|
constexpr int enthalpyResidualBlock =
|
|
blocks::type_index_v<blocks::enthalpy::specific::residual, typename Form::residual_blocks>;
|
|
|
|
CHECK(
|
|
map.StateFactors()(layout.value_offsets()[massValueBlock]) ==
|
|
Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15)
|
|
);
|
|
CHECK(
|
|
map.ResidualFactors()(layout.residual_offsets()[massResidualBlock]) ==
|
|
Catch::Approx(1.0 / targetMass).epsilon(2.0e-15)
|
|
);
|
|
CHECK(
|
|
map.StateFactors()(layout.value_offsets()[phaseValueBlock]) ==
|
|
Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15)
|
|
);
|
|
CHECK(
|
|
map.ResidualFactors()(layout.residual_offsets()[phaseResidualBlock]) ==
|
|
Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15)
|
|
);
|
|
|
|
const auto &surfaceRows = problem.GetPressureSurfaceRows().reduced_dofs();
|
|
REQUIRE(surfaceRows.Size() > 0);
|
|
for (const int row : surfaceRows) {
|
|
const int rootRow = layout.residual_offsets()[enthalpyResidualBlock] + row;
|
|
CHECK(map.ResidualFactors()(rootRow) == Catch::Approx(1.0 / scales.specificEnergy).epsilon(2.0e-15));
|
|
}
|
|
|
|
mfem::Vector physicalState(problem.StateSize());
|
|
for (int index = 0; index < physicalState.Size(); ++index) {
|
|
physicalState(index) = std::sin(0.37 * static_cast<double>(index + 1));
|
|
}
|
|
mfem::Vector normalizedState;
|
|
mfem::Vector recoveredState;
|
|
map.NormalizeState(physicalState, normalizedState);
|
|
map.DenormalizeState(normalizedState, recoveredState);
|
|
checkVector(recoveredState, physicalState, 4.0e-15);
|
|
|
|
const normalization::ScaledJacobianOperator scaledJacobian(problem.GetLinearizationOperator(), map);
|
|
CHECK(scaledJacobian.Width() == problem.StateSize());
|
|
CHECK(scaledJacobian.Height() == problem.EquationSize());
|
|
|
|
// The existing provisional structure preconditioner remains available for this distinct problem type.
|
|
const auto structureBlock = preconditioning::stellarStructureBlock(problem);
|
|
STATIC_CHECK(preconditioning::PreconditionerComponent<std::remove_cvref_t<decltype(structureBlock)>>);
|
|
}
|