#include #include #include #include #include #include #include #include #include #include #include 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 concept SupportsModelDerivedStellarScales = requires(const normalization::PhysicalRieszDiagonal<> &policy, const Model &model) { { normalization::deriveStellarCharacteristicScales(policy, model) } -> std::same_as; }; struct TestValue final : blocks::value_block_base { }; struct TestResidual final : blocks::residual_block_base { }; using TestForm = blocks::block_form, blocks::type_list>; using GlobalSpecificEnergyNormalization = models:: CoordinateNormalization; using VolumeSpecificEnergyNormalization = models:: CoordinateNormalization; class SelfDescribingMagneticSpecificEnergy final { public: struct Parameters final { double target; }; using ModelDefinition = mean_field::integral::FixedWithPhysicalCoordinate< SelfDescribingMagneticSpecificEnergy, "NormalizationMockMagneticSpecificEnergy", models::DependsOn, models::Affects, 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, models::Affects>; 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, models::Affects, models::GeneratedNormalization>; 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, models::Affects, MalformedGeneratedNormalization>; explicit constexpr MalformedGeneratedNormalizationConstraint(const Parameters parameters) noexcept : m_target(parameters.target) { } private: double m_target; }; template using GeneratedValueBlock = blocks::generated_value_block>; template using GeneratedMultiplierBlock = blocks::generated_value_block>; template using GeneratedResidualBlock = blocks::generated_residual_block>; using SelfDescribingValue = GeneratedValueBlock; using SelfDescribingResidual = GeneratedResidualBlock; using SelfDescribingForm = blocks::block_form, blocks::type_list>; using MissingValue = GeneratedMultiplierBlock; using MissingResidual = GeneratedResidualBlock; using MissingNormalizationForm = blocks::block_form, blocks::type_list>; using UnpreparedVolumeValue = GeneratedValueBlock; using UnpreparedVolumeResidual = GeneratedResidualBlock; using UnpreparedVolumeForm = blocks::block_form, blocks::type_list>; 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 values) { mfem::Vector result(static_cast(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); STATIC_CHECK_FALSE(std::constructible_from); STATIC_CHECK_FALSE( std::constructible_from ); STATIC_CHECK_FALSE(std::constructible_from); STATIC_CHECK(std::constructible_from); STATIC_CHECK_FALSE(std::constructible_from); STATIC_CHECK_FALSE(std::constructible_from); 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(scales) == scales.density); CHECK(normalization::physicalScale(scales) == scales.acceleration); CHECK(normalization::physicalScale(scales) == scales.inverseTimeSquared); CHECK(normalization::physicalScale(scales) == 9.0); CHECK( normalization::physicalScale(scales) == scales.angularVelocity ); CHECK( normalization::physicalScale(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(scales) == scales.specificEnergy ); CHECK( normalization::physicalScale(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; using ResidualTraits = normalization::PhysicalRieszBlockTraits; STATIC_CHECK(models::SelfDescribingModelSpecification); STATIC_CHECK(models::CompleteGeneratedNormalizationFor); STATIC_CHECK(operators::StellarEquilibriumSpecificationCompilable); STATIC_CHECK( normalization::GeneratedValuePhysicalRieszNormalizable< models::PhysicalCoordinateFor> ); STATIC_CHECK( normalization::GeneratedResidualPhysicalRieszNormalizable< models::ResidualFor> ); STATIC_CHECK(normalization::CompleteGeneratedPhysicalRieszNormalizationFor); STATIC_CHECK(normalization::CompilableNormalizationFor, SelfDescribingForm>); STATIC_CHECK(normalization::RegisteredStellarSpecificationNormalization); 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 layout({1}, {1}); normalization::DiagonalNormalizationBuilder builder(layout); normalization::StellarSpecificationNormalizationContribution::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); STATIC_CHECK_FALSE(normalization::RegisteredStellarSpecificationNormalization); STATIC_CHECK_FALSE(normalization::CompleteStellarNormalizationFor); STATIC_CHECK(models::ModelSpecification); STATIC_CHECK_FALSE(models::CompleteGeneratedNormalizationFor); STATIC_CHECK_FALSE(normalization::CompleteGeneratedPhysicalRieszNormalizationFor); STATIC_CHECK_FALSE( normalization::CompilableNormalizationFor, MissingNormalizationForm> ); STATIC_CHECK_FALSE(normalization::RegisteredStellarSpecificationNormalization); STATIC_CHECK_FALSE( normalization::CompleteStellarSpecificationNormalizationFor< MissingGeneratedNormalization, MissingNormalizationForm> ); STATIC_CHECK(models::ModelSpecification); STATIC_CHECK_FALSE(models::CompleteGeneratedNormalizationFor); STATIC_CHECK_FALSE( normalization::CompleteGeneratedPhysicalRieszNormalizationFor ); STATIC_CHECK_FALSE( normalization::RegisteredStellarSpecificationNormalization ); // 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 ); STATIC_CHECK( normalization::CompilableNormalizationFor, UnpreparedVolumeForm> ); STATIC_CHECK_FALSE( normalization::RegisteredStellarSpecificationNormalization ); 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::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 layout({3}, {3}); normalization::DiagonalNormalizationBuilder 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(stateScale, gram); builder.SetResidualBlock(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::type_list>; const blocks::form_layout layout({4}, {4}); normalization::DiagonalNormalizationBuilder builder(layout); builder.SetValueBlock(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 pointRows{1, 3}; builder.SetHybridResidualBlock( 5.0, bulkMetric, std::span{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 duplicateRows(layout); duplicateRows.SetValueGlobal(1.0); const std::array duplicates{1, 1}; CHECK_THROWS_AS( duplicateRows.SetHybridResidualBlock( 1.0, vector({1.0, 1.0, 1.0, 1.0}), std::span{duplicates} ), std::invalid_argument ); } TEST_CASE( "Runtime Normalization Assembly Rejects Missing Duplicate And Invalid Data", "[normalization][validation]" ) { const blocks::form_layout layout({2}, {2}); normalization::DiagonalNormalizationBuilder missing(layout); missing.SetValueBlock(1.0, vector({1.0, 1.0})); CHECK_THROWS_AS(std::move(missing).Build(), std::logic_error); normalization::DiagonalNormalizationBuilder duplicate(layout); duplicate.SetValueBlock(1.0, vector({1.0, 1.0})); CHECK_THROWS_AS(duplicate.SetValueBlock(1.0, vector({1.0, 1.0})), std::logic_error); normalization::DiagonalNormalizationBuilder zeroMetric(layout); CHECK_THROWS_AS(zeroMetric.SetValueBlock(1.0, vector({1.0, 0.0})), std::invalid_argument); normalization::DiagonalNormalizationBuilder wrongSize(layout); CHECK_THROWS_AS(wrongSize.SetResidualBlock(1.0, vector({1.0})), std::invalid_argument); CHECK_THROWS_AS( normalization::DiagonalNormalization(vector({1.0, std::numeric_limits::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; using Form = typename ProblemType::FormType; using ModelType = std::remove_cvref_t; STATIC_CHECK(SupportsModelDerivedStellarScales); STATIC_CHECK_FALSE(SupportsModelDerivedStellarScales); STATIC_CHECK( std::same_as> ); 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; constexpr int massResidualBlock = blocks::type_index_v; constexpr int phaseValueBlock = blocks::type_index_v; constexpr int phaseResidualBlock = blocks::type_index_v; constexpr int enthalpyResidualBlock = blocks::type_index_v; 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(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>); }