feat(preconditioner): major work on preconditioner system

first preconditioner MVP
This commit is contained in:
2026-09-04 07:54:10 -04:00
parent 25510008dd
commit 71423d543f
61 changed files with 15920 additions and 422 deletions

View File

@@ -0,0 +1,392 @@
#include <algorithm>
#include <cmath>
#include <memory>
#include <stdexcept>
#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 preconditioning = mean_field::preconditioning;
namespace backend = mean_field::preconditioning::backend;
using DiagonalMass = preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::mass_like,
preconditioning::OperatorValueStructure::scalar,
preconditioning::OperatorSymmetry::symmetric,
preconditioning::OperatorDefiniteness::positive_definite,
preconditioning::OperatorRepresentation::diagonal,
preconditioning::OperatorDistribution::local,
preconditioning::OperatorFESpace::h1>;
using MatrixFreeHdivMass = preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::mass_like,
preconditioning::OperatorValueStructure::vector,
preconditioning::OperatorSymmetry::symmetric,
preconditioning::OperatorDefiniteness::positive_definite,
preconditioning::OperatorRepresentation::matrix_free,
preconditioning::OperatorDistribution::distributed_true_dof,
preconditioning::OperatorFESpace::h_div>;
using DenseBorder = preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::dense_border,
preconditioning::OperatorValueStructure::block,
preconditioning::OperatorSymmetry::nonsymmetric,
preconditioning::OperatorDefiniteness::indefinite,
preconditioning::OperatorRepresentation::assembled_dense,
preconditioning::OperatorDistribution::local>;
using ScalarH1Elliptic = preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::elliptic_like,
preconditioning::OperatorValueStructure::scalar,
preconditioning::OperatorSymmetry::symmetric,
preconditioning::OperatorDefiniteness::positive_definite,
preconditioning::OperatorRepresentation::assembled_sparse,
preconditioning::OperatorDistribution::distributed_true_dof,
preconditioning::OperatorFESpace::h1>;
using ConstantNullspaceH1Elliptic = preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::elliptic_like,
preconditioning::OperatorValueStructure::scalar,
preconditioning::OperatorSymmetry::symmetric,
preconditioning::OperatorDefiniteness::positive_semidefinite,
preconditioning::OperatorRepresentation::assembled_sparse,
preconditioning::OperatorDistribution::distributed_true_dof,
preconditioning::OperatorFESpace::h1,
preconditioning::OperatorNullspace::constant_mode>;
using HdivElliptic = preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::elliptic_like,
preconditioning::OperatorValueStructure::vector,
preconditioning::OperatorSymmetry::symmetric,
preconditioning::OperatorDefiniteness::positive_definite,
preconditioning::OperatorRepresentation::assembled_sparse,
preconditioning::OperatorDistribution::distributed_true_dof,
preconditioning::OperatorFESpace::h_div>;
using NonsymmetricH1Elliptic = preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::elliptic_like,
preconditioning::OperatorValueStructure::scalar,
preconditioning::OperatorSymmetry::nonsymmetric,
preconditioning::OperatorDefiniteness::indefinite,
preconditioning::OperatorRepresentation::assembled_sparse,
preconditioning::OperatorDistribution::distributed_true_dof,
preconditioning::OperatorFESpace::h1>;
using SuppliedNullspaceH1Elliptic = preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::elliptic_like,
preconditioning::OperatorValueStructure::scalar,
preconditioning::OperatorSymmetry::symmetric,
preconditioning::OperatorDefiniteness::positive_semidefinite,
preconditioning::OperatorRepresentation::assembled_sparse,
preconditioning::OperatorDistribution::distributed_true_dof,
preconditioning::OperatorFESpace::h1,
preconditioning::OperatorNullspace::supplied_basis>;
using FixedAMG = backend::HypreBoomerAMG<backend::FixedCycles>;
using AdaptiveAMG = backend::HypreBoomerAMG<backend::SolveToTolerance>;
using DiagonalComponent = preconditioning::ComponentDeclaration<
blocks::type_list<blocks::density::mass::value>,
blocks::type_list<blocks::density::mass::residual>,
blocks::type_list<>,
DiagonalMass,
backend::Diagonal>;
using UnderdeclaredDiagonalComponent = preconditioning::ComponentDeclaration<
blocks::type_list<blocks::density::mass::value>,
blocks::type_list<blocks::density::mass::residual>,
blocks::type_list<>,
DiagonalMass,
backend::Diagonal,
preconditioning::NoPreparationDependencies>;
class TinyParallelH1Operator final {
public:
TinyParallelH1Operator()
: m_serialMesh(mfem::Mesh::MakeCartesian1D(4)),
m_parallelMesh(
MPI_COMM_WORLD,
m_serialMesh
),
m_collection(
1,
1
),
m_space(
&m_parallelMesh,
&m_collection
),
m_form(&m_space) {
m_form.AddDomainIntegrator(new mfem::DiffusionIntegrator());
m_form.AddDomainIntegrator(new mfem::MassIntegrator());
m_form.Assemble();
m_form.Finalize();
m_matrix.reset(m_form.ParallelAssemble());
}
[[nodiscard]] const mfem::HypreParMatrix &matrix() const {
return *m_matrix;
}
private:
mfem::Mesh m_serialMesh;
mfem::ParMesh m_parallelMesh;
mfem::H1_FECollection m_collection;
mfem::ParFiniteElementSpace m_space;
mfem::ParBilinearForm m_form;
std::unique_ptr<mfem::HypreParMatrix> m_matrix;
};
class KnownMatrixFreeSPDOperator final : public mfem::Operator {
public:
KnownMatrixFreeSPDOperator()
: mfem::Operator(3),
m_matrix(3) {
m_matrix = 0.0;
m_matrix(0, 0) = 4.0;
m_matrix(0, 1) = 1.0;
m_matrix(1, 0) = 1.0;
m_matrix(1, 1) = 3.0;
m_matrix(1, 2) = 0.5;
m_matrix(2, 1) = 0.5;
m_matrix(2, 2) = 2.0;
}
void Mult(
const mfem::Vector &input,
mfem::Vector &output
) const override {
m_matrix.Mult(input, output);
}
void AssembleDiagonal(mfem::Vector &diagonal) const override {
diagonal.SetSize(Height());
for (int index = 0; index < Height(); ++index) {
diagonal(index) = m_matrix(index, index);
}
}
private:
mfem::DenseMatrix m_matrix;
};
[[nodiscard]] double relativeResidual(
const mfem::HypreParMatrix &matrix,
const mfem::Vector &rightHandSide,
const mfem::Vector &action
) {
mfem::Vector residual(rightHandSide.Size());
matrix.Mult(action, residual);
residual -= rightHandSide;
return gravity_prepared_test_utils::global_norm(residual, matrix.GetComm()) /
gravity_prepared_test_utils::global_norm(rightHandSide, matrix.GetComm());
}
} // namespace
TEST_CASE(
"Preconditioning Backends Expose Complete Compile-Time Capabilities",
tags::preconditioning_backend_unit
) {
STATIC_CHECK(backend::Compatible<backend::Diagonal, DiagonalMass>);
STATIC_CHECK(backend::Compatible<backend::Diagonal, MatrixFreeHdivMass>);
STATIC_CHECK(backend::Compatible<backend::MatrixFreeChebyshev, MatrixFreeHdivMass>);
STATIC_CHECK(backend::Compatible<backend::DenseDirect, DenseBorder>);
STATIC_CHECK(backend::Compatible<FixedAMG, ScalarH1Elliptic>);
STATIC_CHECK(backend::Compatible<FixedAMG, ConstantNullspaceH1Elliptic>);
STATIC_CHECK_FALSE(backend::Compatible<FixedAMG, HdivElliptic>);
STATIC_CHECK_FALSE(backend::Compatible<FixedAMG, NonsymmetricH1Elliptic>);
STATIC_CHECK_FALSE(backend::Compatible<FixedAMG, SuppliedNullspaceH1Elliptic>);
STATIC_CHECK(backend::ArnoldiAdmissible<FixedAMG>);
STATIC_CHECK(backend::ArnoldiAdmissible<backend::MatrixFreeChebyshev>);
STATIC_CHECK_FALSE(backend::ArnoldiAdmissible<AdaptiveAMG>);
STATIC_CHECK(backend::requiresAssembledSparseSurrogate<FixedAMG>);
STATIC_CHECK_FALSE(backend::requiresAssembledSparseSurrogate<backend::Diagonal>);
STATIC_CHECK(backend::Traits<backend::Diagonal>::supportsSerialExecution);
STATIC_CHECK(backend::Traits<backend::Diagonal>::supportsDistributedExecution);
STATIC_CHECK(backend::Traits<backend::DenseDirect>::supportsSerialExecution);
STATIC_CHECK_FALSE(backend::Traits<backend::DenseDirect>::supportsDistributedExecution);
STATIC_CHECK_FALSE(backend::Traits<FixedAMG>::supportsSerialExecution);
STATIC_CHECK(backend::Traits<FixedAMG>::supportsDistributedExecution);
STATIC_CHECK(preconditioning::PreconditionerComponent<DiagonalComponent>);
STATIC_CHECK(
DiagonalComponent::PreparationDependencies::contains(preconditioning::PreparationDependency::linearization)
);
STATIC_CHECK_FALSE(preconditioning::PreconditionerComponent<UnderdeclaredDiagonalComponent>);
}
TEST_CASE(
"Diagonal Backend Exactly Inverts A Known Diagonal Operator",
tags::preconditioning_backend_unit
) {
mfem::Vector diagonal(3);
diagonal(0) = 2.0;
diagonal(1) = -4.0;
diagonal(2) = 0.5;
auto prepared = backend::prepare(backend::Diagonal{}, diagonal);
mfem::Vector rightHandSide(3);
rightHandSide(0) = 4.0;
rightHandSide(1) = 8.0;
rightHandSide(2) = -1.0;
mfem::Vector action(3);
prepared.Mult(rightHandSide, action);
CHECK(action(0) == Catch::Approx(2.0));
CHECK(action(1) == Catch::Approx(-2.0));
CHECK(action(2) == Catch::Approx(-2.0));
CHECK(prepared.GetStatistics().setups == 1);
CHECK(prepared.GetStatistics().applications == 1);
CHECK(prepared.GetStatistics().innerIterations == 0);
diagonal(1) = 0.0;
CHECK_THROWS_AS(prepared.Refresh(diagonal), std::invalid_argument);
}
TEST_CASE(
"Dense Direct Backend Exactly Solves And Refreshes A Known Border",
tags::preconditioning_backend_unit
) {
mfem::DenseMatrix matrix(2);
matrix(0, 0) = 4.0;
matrix(0, 1) = 1.0;
matrix(1, 0) = 2.0;
matrix(1, 1) = 3.0;
auto prepared = backend::prepare(backend::DenseDirect{}, matrix);
mfem::Vector rightHandSide(2);
rightHandSide(0) = 7.0;
rightHandSide(1) = 1.0;
mfem::Vector action(2);
prepared.Mult(rightHandSide, action);
CHECK(action(0) == Catch::Approx(2.0).margin(1.0e-14));
CHECK(action(1) == Catch::Approx(-1.0).margin(1.0e-14));
matrix = 0.0;
matrix(0, 0) = 2.0;
matrix(1, 1) = 4.0;
prepared.Refresh(matrix);
prepared.Mult(rightHandSide, action);
CHECK(action(0) == Catch::Approx(3.5).margin(1.0e-14));
CHECK(action(1) == Catch::Approx(0.25).margin(1.0e-14));
CHECK(prepared.GetStatistics().setups == 2);
CHECK(prepared.GetStatistics().applications == 2);
}
TEST_CASE(
"Matrix-Free Chebyshev Backend Is A Fixed Linear Positive Approximate Inverse",
tags::preconditioning_backend_unit
) {
const KnownMatrixFreeSPDOperator operation;
auto prepared =
backend::prepare(backend::MatrixFreeChebyshev{.order = 3, .powerIterations = 20}, operation, MPI_COMM_WORLD);
mfem::Vector first(3);
first(0) = 1.0;
first(1) = -2.0;
first(2) = 0.25;
mfem::Vector second(3);
second(0) = -0.5;
second(1) = 0.75;
second(2) = 3.0;
mfem::Vector combination(first);
combination *= 1.7;
combination.Add(-0.4, second);
mfem::Vector firstAction(3);
mfem::Vector secondAction(3);
mfem::Vector combinationAction(3);
prepared.Mult(first, firstAction);
prepared.Mult(second, secondAction);
prepared.Mult(combination, combinationAction);
mfem::Vector expected(firstAction);
expected *= 1.7;
expected.Add(-0.4, secondAction);
mfem::Vector linearityError(combinationAction);
linearityError -= expected;
CHECK(linearityError.Norml2() <= 2.0e-12 * std::max(1.0, expected.Norml2()));
CHECK((first * firstAction) > 0.0);
CHECK(prepared.GetStatistics().setups == 1);
CHECK(prepared.GetStatistics().applications == 3);
CHECK(prepared.GetStatistics().innerIterations == 9);
CHECK(prepared.GetStatistics().lastInnerIterations == 3);
CHECK_THROWS_AS(
backend::prepare(backend::MatrixFreeChebyshev{.order = 0}, operation, MPI_COMM_WORLD), std::invalid_argument
);
CHECK_THROWS_AS(
backend::prepare(backend::MatrixFreeChebyshev{.order = 6}, operation, MPI_COMM_WORLD), std::invalid_argument
);
CHECK_THROWS_AS(
backend::prepare(backend::MatrixFreeChebyshev{.powerTolerance = 1.0}, operation, MPI_COMM_WORLD),
std::invalid_argument
);
CHECK_THROWS_AS(
backend::prepare(backend::MatrixFreeChebyshev{.powerSeed = 0}, operation, MPI_COMM_WORLD), std::invalid_argument
);
}
TEST_CASE(
"BoomerAMG Fixed Cycles Are Linear While Adaptive Application Meets Its Tolerance",
tags::preconditioning_backend_unit
) {
TinyParallelH1Operator problem;
const mfem::HypreParMatrix &matrix = problem.matrix();
mfem::Vector exact(matrix.Width());
exact = 1.0;
mfem::Vector rightHandSide(matrix.Height());
matrix.Mult(exact, rightHandSide);
auto fixed = backend::prepare(FixedAMG{backend::FixedCycles{.cycles = 2}}, matrix);
mfem::Vector fixedAction(matrix.Width());
fixedAction = 0.0;
fixed.Mult(rightHandSide, fixedAction);
CHECK(relativeResidual(matrix, rightHandSide, fixedAction) < 1.0);
mfem::Vector secondExact(matrix.Width());
for (int index = 0; index < secondExact.Size(); ++index) {
secondExact(index) = 0.25 + static_cast<double>(index);
}
mfem::Vector secondRightHandSide(matrix.Height());
matrix.Mult(secondExact, secondRightHandSide);
mfem::Vector secondAction(matrix.Width());
secondAction = 0.0;
fixed.Mult(secondRightHandSide, secondAction);
mfem::Vector combinedRightHandSide(rightHandSide);
combinedRightHandSide *= 0.7;
combinedRightHandSide.Add(-0.2, secondRightHandSide);
mfem::Vector combinedAction(matrix.Width());
combinedAction = 0.0;
fixed.Mult(combinedRightHandSide, combinedAction);
mfem::Vector expectedCombinedAction(fixedAction);
expectedCombinedAction *= 0.7;
expectedCombinedAction.Add(-0.2, secondAction);
combinedAction -= expectedCombinedAction;
CHECK(
gravity_prepared_test_utils::global_norm(combinedAction, matrix.GetComm()) <
1.0e-11 * gravity_prepared_test_utils::global_norm(expectedCombinedAction, matrix.GetComm())
);
CHECK(fixed.GetStatistics().setups == 1);
CHECK(fixed.GetStatistics().applications == 3);
CHECK(fixed.GetStatistics().lastInnerIterations >= 1);
CHECK(fixed.GetStatistics().lastInnerIterations <= 2);
auto adaptive = backend::prepare(
AdaptiveAMG{backend::SolveToTolerance{.relativeTolerance = 1.0e-10, .maximumCycles = 50}}, matrix
);
mfem::Vector adaptiveAction(matrix.Width());
adaptiveAction = 0.0;
adaptive.Mult(rightHandSide, adaptiveAction);
CHECK(relativeResidual(matrix, rightHandSide, adaptiveAction) < 1.0e-8);
CHECK(adaptive.GetStatistics().setups == 1);
CHECK(adaptive.GetStatistics().applications == 1);
CHECK(adaptive.GetStatistics().lastInnerIterations >= 1);
CHECK(adaptive.GetStatistics().lastInnerIterations <= 50);
}

View File

@@ -0,0 +1,226 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <numbers>
#include <stdexcept>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace {
namespace blocks = mean_field::utils::blocks;
namespace preconditioning = mean_field::preconditioning;
using Form = blocks::central_density_bordered_stellar_equilibrium_form;
using GroupedComponent = preconditioning::ComponentDeclaration<
blocks::type_list<
blocks::density::mass::value,
blocks::surface_deformation::parameters::value,
blocks::enthalpy::specific::value,
blocks::gravity::gradient::value,
blocks::gravity::poisson::value,
blocks::fixed_total_mass::mass_normalization::value,
blocks::fixed_central_density::central_value::value>,
blocks::type_list<
blocks::density::mass::residual,
blocks::surface_deformation::shape_equilibrium::residual,
blocks::enthalpy::specific::residual,
blocks::gravity::gradient::residual,
blocks::gravity::poisson::residual,
blocks::fixed_total_mass::mass_normalization::residual,
blocks::fixed_central_density::central_value::residual>,
blocks::type_list<>,
preconditioning::IdentityOperatorCharacteristics,
preconditioning::backend::Identity>;
using IncompleteComponent = preconditioning::ComponentDeclaration<
blocks::type_list<
blocks::density::mass::value,
blocks::surface_deformation::parameters::value,
blocks::enthalpy::specific::value,
blocks::gravity::gradient::value,
blocks::gravity::poisson::value,
blocks::fixed_total_mass::mass_normalization::value>,
blocks::type_list<
blocks::density::mass::residual,
blocks::surface_deformation::shape_equilibrium::residual,
blocks::enthalpy::specific::residual,
blocks::gravity::gradient::residual,
blocks::gravity::poisson::residual,
blocks::fixed_total_mass::mass_normalization::residual>,
blocks::type_list<>,
preconditioning::IdentityOperatorCharacteristics,
preconditioning::backend::Identity>;
[[nodiscard]] blocks::form_layout<Form> makeUnevenLayout() {
return {
std::array<int, Form::value_block_count>{2, 3, 4, 5, 6, 1, 1},
std::array<int, Form::residual_block_count>{4, 5, 2, 3, 6, 1, 1}
};
}
[[nodiscard]] double relativeError(
const mfem::Vector &left,
const mfem::Vector &right
) {
mfem::Vector difference(left);
difference -= right;
return difference.Norml2() / std::max({1.0, left.Norml2(), right.Norml2()});
}
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies makeDependencies() {
return {
.discretization = {.identity = 9301, .revision = 1},
.density = {.identity = 9303, .revision = 1},
.surfaceDeformation = {.identity = 9307, .revision = 1},
.gravityGradient = {.identity = 9311, .revision = 1},
.gravityPotential = {.identity = 9317, .revision = 1},
.enthalpy = {.identity = 9323, .revision = 1},
.bernoulliConstant = {.identity = 9329, .revision = 1},
.rotation = {.identity = 9331, .revision = 1},
.targetMass = {.identity = 9337, .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};
}
} // namespace
TEST_CASE(
"Typed Equilibrium Coordinate Maps Preserve Every Uneven Block Without Scaling",
"[preconditioning][equilibrium_coordinates][unit]"
) {
STATIC_CHECK(preconditioning::EquilibriumCoordinateComponentFor<GroupedComponent, Form>);
STATIC_CHECK_FALSE(preconditioning::EquilibriumCoordinateComponentFor<IncompleteComponent, Form>);
const auto layout = makeUnevenLayout();
preconditioning::EquilibriumPreconditionerCoordinateMap<Form, GroupedComponent> coordinates(layout);
REQUIRE(coordinates.EquilibriumStateSize() == 22);
REQUIRE(coordinates.EquilibriumResidualSize() == 22);
REQUIRE(coordinates.PreconditionerCorrectionSize() == 22);
REQUIRE(coordinates.PreconditionerResidualSize() == 22);
const auto &correctionRanges = coordinates.GetCorrectionRanges();
CHECK(correctionRanges[0] == (preconditioning::EquilibriumCoordinateRange{0, 0, 2}));
CHECK(correctionRanges[1] == (preconditioning::EquilibriumCoordinateRange{2, 2, 3}));
CHECK(correctionRanges[2] == (preconditioning::EquilibriumCoordinateRange{14, 5, 6}));
CHECK(correctionRanges[3] == (preconditioning::EquilibriumCoordinateRange{5, 11, 4}));
CHECK(correctionRanges[4] == (preconditioning::EquilibriumCoordinateRange{9, 15, 5}));
CHECK(correctionRanges[5] == (preconditioning::EquilibriumCoordinateRange{20, 20, 1}));
CHECK(correctionRanges[6] == (preconditioning::EquilibriumCoordinateRange{21, 21, 1}));
const auto &residualRanges = coordinates.GetResidualRanges();
CHECK(residualRanges[0] == (preconditioning::EquilibriumCoordinateRange{9, 0, 2}));
CHECK(residualRanges[1] == (preconditioning::EquilibriumCoordinateRange{11, 2, 3}));
CHECK(residualRanges[2] == (preconditioning::EquilibriumCoordinateRange{14, 5, 6}));
CHECK(residualRanges[3] == (preconditioning::EquilibriumCoordinateRange{0, 11, 4}));
CHECK(residualRanges[4] == (preconditioning::EquilibriumCoordinateRange{4, 15, 5}));
CHECK(residualRanges[5] == (preconditioning::EquilibriumCoordinateRange{20, 20, 1}));
CHECK(residualRanges[6] == (preconditioning::EquilibriumCoordinateRange{21, 21, 1}));
mfem::Vector equilibriumCorrection(22);
mfem::Vector equilibriumResidual(22);
for (int index = 0; index < 22; ++index) {
equilibriumCorrection(index) = 100.0 + static_cast<double>(index);
equilibriumResidual(index) = -200.0 - static_cast<double>(index);
}
mfem::Vector groupedCorrection(22);
mfem::Vector groupedResidual(22);
const double *const groupedCorrectionStorage = groupedCorrection.GetData();
const double *const groupedResidualStorage = groupedResidual.GetData();
coordinates.PackCorrection(equilibriumCorrection, groupedCorrection);
coordinates.PackResidual(equilibriumResidual, groupedResidual);
CHECK(groupedCorrection.GetData() == groupedCorrectionStorage);
CHECK(groupedResidual.GetData() == groupedResidualStorage);
CHECK(groupedCorrection(5) == equilibriumCorrection(14));
CHECK(groupedCorrection(11) == equilibriumCorrection(5));
CHECK(groupedResidual(0) == equilibriumResidual(9));
CHECK(groupedResidual(11) == equilibriumResidual(0));
mfem::Vector recoveredCorrection(22);
mfem::Vector recoveredResidual(22);
coordinates.UnpackCorrection(groupedCorrection, recoveredCorrection);
coordinates.UnpackResidual(groupedResidual, recoveredResidual);
CHECK(relativeError(recoveredCorrection, equilibriumCorrection) == 0.0);
CHECK(relativeError(recoveredResidual, equilibriumResidual) == 0.0);
const auto &statistics = coordinates.GetStatistics();
CHECK(statistics.correctionPacks == 1);
CHECK(statistics.correctionUnpacks == 1);
CHECK(statistics.residualPacks == 1);
CHECK(statistics.residualUnpacks == 1);
mfem::Vector wrongSize(21);
CHECK_THROWS_AS(coordinates.PackResidual(wrongSize, groupedResidual), std::invalid_argument);
CHECK_THROWS_AS(coordinates.UnpackCorrection(wrongSize, recoveredCorrection), std::invalid_argument);
}
TEST_CASE(
"Prepared Stellar Preconditioning Matches An Explicit Canonical Coordinate Transformation",
"[preconditioning][equilibrium_coordinates][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}}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(model, finiteElements);
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 512}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
auto component = preconditioning::makePreconditioner(problem);
auto prepared = preconditioning::prepare(problem, component);
prepared.SetOperator(problem.GetLinearizationOperator());
mfem::Vector equilibriumResidual(problem.EquationSize());
for (int index = 0; index < equilibriumResidual.Size(); ++index) {
equilibriumResidual(index) = 0.25 * std::cos(0.19 * static_cast<double>(index + 1));
}
mfem::Vector groupedResidual(problem.EquationSize());
mfem::Vector groupedCorrection(problem.StateSize());
mfem::Vector expected(problem.StateSize());
prepared.GetCoordinateMap().PackResidual(equilibriumResidual, groupedResidual);
prepared.GetGroupedPreconditioner().Mult(groupedResidual, groupedCorrection);
prepared.GetCoordinateMap().UnpackCorrection(groupedCorrection, expected);
mfem::Vector actual(problem.StateSize());
const double *const actionStorage = actual.GetData();
prepared.Mult(equilibriumResidual, actual);
CHECK(actual.GetData() == actionStorage);
CHECK(relativeError(actual, expected) <= 2.0e-12);
const auto &statistics = prepared.GetStatistics();
CHECK(statistics.applications == 1);
CHECK(statistics.residualCoordinateMappings == 1);
CHECK(statistics.correctionCoordinateMappings == 1);
CHECK(prepared.GetCoordinateMap().GetStatistics().residualPacks == 2);
CHECK(prepared.GetCoordinateMap().GetStatistics().correctionUnpacks == 2);
const auto unchanged = prepared.Refresh();
CHECK_FALSE(unchanged.DidAnyWork());
CHECK(prepared.IsCurrent());
}

View File

@@ -0,0 +1,393 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <type_traits>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace {
namespace backend = mean_field::preconditioning::backend;
namespace blocks = mean_field::utils::blocks;
namespace gravity_context = mean_field::operators::context::gravity_field;
namespace preconditioning = mean_field::preconditioning;
using FixedAMG = backend::HypreBoomerAMG<backend::FixedCycles>;
using AdaptiveAMG = backend::HypreBoomerAMG<backend::SolveToTolerance>;
using FixedGravityLDU =
preconditioning::GravityFieldBlock<backend::Diagonal, FixedAMG, preconditioning::GravityApproximateLDU>;
using ChebyshevGravityLDU = preconditioning::
GravityFieldBlock<backend::MatrixFreeChebyshev, FixedAMG, preconditioning::GravityApproximateLDU>;
using AdaptiveGravityLDU =
preconditioning::GravityFieldBlock<backend::Diagonal, AdaptiveAMG, preconditioning::GravityApproximateLDU>;
using DensityIdentity =
preconditioning::IdentityBlock<blocks::density::mass::value, blocks::density::mass::residual>;
using SurfaceIdentity = preconditioning::IdentityBlock<
blocks::surface_deformation::parameters::value,
blocks::surface_deformation::shape_equilibrium::residual>;
using EnthalpyIdentity =
preconditioning::IdentityBlock<blocks::enthalpy::specific::value, blocks::enthalpy::specific::residual>;
using MassIdentity = preconditioning::IdentityBlock<
blocks::fixed_total_mass::mass_normalization::value,
blocks::fixed_total_mass::mass_normalization::residual>;
using FixedGravityPlan = preconditioning::
PreconditionerPlan<DensityIdentity, SurfaceIdentity, FixedGravityLDU, EnthalpyIdentity, MassIdentity>;
using AdaptiveGravityPlan = preconditioning::
PreconditionerPlan<DensityIdentity, SurfaceIdentity, AdaptiveGravityLDU, EnthalpyIdentity, MassIdentity>;
template <typename Policy>
mfem::Vector applyKnownFactorization(
Policy policy,
const mfem::Vector &rightHandSide,
preconditioning::GravityFactorizationStatistics *statistics = nullptr
) {
mfem::Vector massDiagonal(2);
massDiagonal = 1.0;
auto massInverse = backend::prepare(backend::Diagonal{}, massDiagonal);
mfem::DenseMatrix schurMatrix(1);
schurMatrix(0, 0) = 5.0;
auto schurInverse = backend::prepare(backend::DenseDirect{}, schurMatrix);
mfem::DenseMatrix divergence(1, 2);
divergence(0, 0) = 2.0;
divergence(0, 1) = -1.0;
preconditioning::GravityFactorizationOperator<Policy> factorization(
policy, massInverse, schurInverse, divergence
);
mfem::Vector action(factorization.Height());
action = std::numeric_limits<double>::quiet_NaN();
factorization.Mult(rightHandSide, action);
if (statistics != nullptr) {
*statistics = factorization.GetStatistics();
}
return action;
}
void checkVector(
const mfem::Vector &computed,
const std::array<
double,
3> &expected
) {
REQUIRE(computed.Size() == static_cast<int>(expected.size()));
for (int index = 0; index < computed.Size(); ++index) {
CHECK(computed(index) == Catch::Approx(expected[static_cast<std::size_t>(index)]).margin(2.0e-14));
}
}
template <typename Policy> void checkExactDenseRecovery(Policy policy) {
mfem::DenseMatrix mass(2);
mass(0, 0) = 2.0;
mass(0, 1) = 0.5;
mass(1, 0) = 0.5;
mass(1, 1) = 1.5;
auto massInverse = backend::prepare(backend::DenseDirect{}, mass);
mfem::DenseMatrix divergence(1, 2);
divergence(0, 0) = 1.0;
divergence(0, 1) = -2.0;
mfem::Vector divergenceTranspose(2);
divergenceTranspose(0) = 1.0;
divergenceTranspose(1) = -2.0;
mfem::Vector massInverseDivergenceTranspose(2);
massInverse.Mult(divergenceTranspose, massInverseDivergenceTranspose);
mfem::DenseMatrix schur(1);
schur(0, 0) = divergenceTranspose * massInverseDivergenceTranspose;
auto schurInverse = backend::prepare(backend::DenseDirect{}, schur);
preconditioning::GravityFactorizationOperator<Policy> factorization(
policy, massInverse, schurInverse, divergence
);
mfem::Vector exact(3);
exact(0) = 0.7;
exact(1) = -1.2;
exact(2) = 0.4;
mfem::Vector rightHandSide(3);
mfem::Vector exactGradient(exact.GetData(), 2);
mfem::Vector gradientRightHandSide(rightHandSide.GetData(), 2);
mass.Mult(exactGradient, gradientRightHandSide);
gradientRightHandSide(0) += divergence(0, 0) * exact(2);
gradientRightHandSide(1) += divergence(0, 1) * exact(2);
rightHandSide(2) = divergence(0, 0) * exact(0) + divergence(0, 1) * exact(1);
mfem::Vector action(3);
action = 0.0;
const double *const actionStorage = action.GetData();
factorization.Mult(rightHandSide, action);
CHECK(action.GetData() == actionStorage);
for (int index = 0; index < action.Size(); ++index) {
CHECK(action(index) == Catch::Approx(exact(index)).margin(2.0e-13));
}
mfem::Vector repeated(3);
repeated = 0.0;
factorization.Mult(rightHandSide, repeated);
for (int index = 0; index < repeated.Size(); ++index) {
CHECK(repeated(index) == action(index));
}
}
struct PreparedGeometry final {
mean_field::fem::FEM finiteElements;
gravity_context::GravityFieldGeometryContext context;
explicit PreparedGeometry(const mean_field::utils::Args &arguments)
: finiteElements(
mean_field::fem::setup_fem(
arguments.mesh_file,
arguments,
0
)
),
context(
finiteElements,
*finiteElements.domainMapperStateless
) {
mfem::Vector displacementTrue(finiteElements.displacementFes->GetTrueVSize());
displacementTrue = 0.0;
const mfem::Vector displacement = context.GetDisplacementMap().gather(displacementTrue);
context.PreparePrimal(displacement, {.value = 1}, {.value = 1});
}
};
} // namespace
TEST_CASE(
"Gravity Field Blocks Expose Complete Compile-Time Ownership And Backend Contracts",
tags::preconditioning_gravity_unit
) {
using Form = blocks::surface_deformed_stellar_equilibrium_form;
using JacobianForm = blocks::surface_deformed_stellar_equilibrium_jacobian_form;
STATIC_CHECK(preconditioning::PreconditionerComponent<FixedGravityLDU>);
STATIC_CHECK(preconditioning::PreconditionerComponent<ChebyshevGravityLDU>);
STATIC_CHECK(preconditioning::PreconditionerComponent<AdaptiveGravityLDU>);
STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename FixedGravityLDU::BackendType>);
STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename ChebyshevGravityLDU::BackendType>);
STATIC_CHECK_FALSE(preconditioning::backend::ArnoldiAdmissible<typename AdaptiveGravityLDU::BackendType>);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<FixedGravityPlan, Form>);
STATIC_CHECK(preconditioning::CompatiblePreconditionerFor<FixedGravityPlan, Form, JacobianForm>);
STATIC_CHECK(preconditioning::StationaryLinearPreconditionerPlan<FixedGravityPlan>);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<AdaptiveGravityPlan, Form>);
STATIC_CHECK(preconditioning::CompatiblePreconditionerFor<AdaptiveGravityPlan, Form, JacobianForm>);
STATIC_CHECK_FALSE(preconditioning::StationaryLinearPreconditionerPlan<AdaptiveGravityPlan>);
STATIC_CHECK(FixedGravityLDU::RequiredCouplings::size == 3);
}
TEST_CASE(
"Gravity Factorization Policies Preserve Their Signed Block Algebra",
tags::preconditioning_gravity_unit
) {
mfem::Vector rightHandSide(3);
rightHandSide(0) = 3.0;
rightHandSide(1) = 4.0;
rightHandSide(2) = 7.0;
checkVector(applyKnownFactorization(preconditioning::GravityBlockDiagonal{}, rightHandSide), {3.0, 4.0, 1.4});
checkVector(applyKnownFactorization(preconditioning::GravityLowerTriangular{}, rightHandSide), {3.0, 4.0, -1.0});
checkVector(applyKnownFactorization(preconditioning::GravityUpperTriangular{}, rightHandSide), {5.8, 2.6, -1.4});
preconditioning::GravityFactorizationStatistics statistics;
checkVector(
applyKnownFactorization(preconditioning::GravityApproximateLDU{}, rightHandSide, &statistics), {5.0, 3.0, -1.0}
);
CHECK(statistics.applications == 1);
CHECK(statistics.massInverseApplications == 2);
CHECK(statistics.potentialSchurApplications == 1);
CHECK(statistics.divergenceApplications == 1);
CHECK(statistics.transposeDivergenceApplications == 1);
}
TEST_CASE(
"Exact Gravity LDU Recovers A Dense Coupled Saddle-Point System",
tags::preconditioning_gravity_unit
) {
checkExactDenseRecovery(preconditioning::GravityApproximateLDU{});
}
TEST_CASE(
"Assembled Gravity Divergence Matches The Prepared Matrix-Free Couplings",
tags::preconditioning_gravity_integration
) {
const auto arguments = test_utils::setup_args();
PreparedGeometry geometry(arguments);
const auto assembledDivergence = preconditioning::assembleGravityDivergenceSurrogate(geometry.finiteElements);
const mfem::Operator &preparedDivergence = geometry.context.GetDivergenceOperator();
const mfem::Vector flux = gravity_prepared_test_utils::make_deterministic_vector(
geometry.finiteElements.gravityFluxFes->GetTrueVSize(), 0.31
);
mfem::Vector assembledForward(assembledDivergence->Height());
mfem::Vector preparedForward(preparedDivergence.Height());
assembledDivergence->Mult(flux, assembledForward);
preparedDivergence.Mult(flux, preparedForward);
const mfem::Vector potential = gravity_prepared_test_utils::make_deterministic_vector(
geometry.finiteElements.gravityPotentialFes->GetTrueVSize(), 0.73
);
mfem::Vector assembledTranspose(assembledDivergence->Width());
mfem::Vector preparedTranspose(preparedDivergence.Width());
assembledDivergence->MultTranspose(potential, assembledTranspose);
preparedDivergence.MultTranspose(potential, preparedTranspose);
const MPI_Comm communicator = geometry.finiteElements.mesh->GetComm();
CHECK(gravity_prepared_test_utils::relative_error(assembledForward, preparedForward, communicator) <= 2.0e-12);
CHECK(gravity_prepared_test_utils::relative_error(assembledTranspose, preparedTranspose, communicator) <= 2.0e-12);
const auto &gradientMap = geometry.context.GetMassOperator().GetFluxMap();
const auto &potentialMap = geometry.context.GetSourceOperator().GetPotentialMap();
preconditioning::ReducedGravityDivergenceOperator reducedDivergence(preparedDivergence, gradientMap, potentialMap);
const mfem::Vector reducedFlux =
gravity_prepared_test_utils::make_deterministic_vector(gradientMap.reduced_size(), 0.47);
mfem::Vector reducedAction(reducedDivergence.Height());
reducedDivergence.Mult(reducedFlux, reducedAction);
const mfem::Vector trueFlux = gradientMap.scatter(reducedFlux);
mfem::Vector trueAction(potentialMap.full_size());
preparedDivergence.Mult(trueFlux, trueAction);
const mfem::Vector expectedReducedAction = potentialMap.gather(trueAction);
CHECK(gravity_prepared_test_utils::relative_error(reducedAction, expectedReducedAction, communicator) <= 2.0e-14);
}
TEST_CASE(
"Prepared Gravity Block Diagonal Is Legacy Equivalent And Allocation Stable",
tags::preconditioning_gravity_integration
) {
const auto arguments = test_utils::setup_args();
PreparedGeometry geometry(arguments);
mean_field::operators::ReducedGravityFieldPreconditioner legacy(geometry.finiteElements, geometry.context);
const auto block = preconditioning::GravityFieldBlock(
backend::Diagonal{}, FixedAMG{backend::FixedCycles{.cycles = 1}}, preconditioning::GravityBlockDiagonal{}
);
auto prepared = preconditioning::prepare(geometry.finiteElements, geometry.context, block);
const mfem::Vector rightHandSide = gravity_prepared_test_utils::make_deterministic_vector(prepared.Width(), 0.59);
mfem::Vector legacyAction(prepared.Height());
mfem::Vector preparedAction(prepared.Height());
legacyAction = 0.0;
preparedAction = 0.0;
double *const preparedStorage = preparedAction.GetData();
const std::uint64_t massPreparations = geometry.context.GetMassOperator().GetPreparationCount();
const std::uint64_t sourcePreparations = geometry.context.GetSourceOperator().GetPreparationCount();
legacy.Mult(rightHandSide, legacyAction);
prepared.Mult(rightHandSide, preparedAction);
CHECK(preparedAction.GetData() == preparedStorage);
CHECK(geometry.context.GetMassOperator().GetPreparationCount() == massPreparations);
CHECK(geometry.context.GetSourceOperator().GetPreparationCount() == sourcePreparations);
CHECK(
gravity_prepared_test_utils::relative_error(
preparedAction, legacyAction, geometry.finiteElements.mesh->GetComm()
) <= 2.0e-12
);
}
TEST_CASE(
"Prepared Gravity Blocks Refresh Explicitly Without Repreparing Geometry",
tags::preconditioning_gravity_integration
) {
const auto arguments = test_utils::setup_args();
PreparedGeometry geometry(arguments);
const auto block = preconditioning::GravityFieldBlock(
backend::Diagonal{}, FixedAMG{backend::FixedCycles{.cycles = 1}}, preconditioning::GravityApproximateLDU{}
);
auto prepared = preconditioning::prepare(geometry.finiteElements, geometry.context, block);
const auto chebyshevBlock = preconditioning::GravityFieldBlock(
backend::MatrixFreeChebyshev{.order = 2, .powerIterations = 10}, FixedAMG{backend::FixedCycles{.cycles = 1}},
preconditioning::GravityApproximateLDU{}
);
auto chebyshevPrepared = preconditioning::prepare(geometry.finiteElements, geometry.context, chebyshevBlock);
const auto unchanged = prepared.Refresh(geometry.finiteElements, geometry.context);
const auto unchangedChebyshev = chebyshevPrepared.Refresh(geometry.finiteElements, geometry.context);
CHECK_FALSE(unchanged.DidAnyWork());
CHECK_FALSE(unchangedChebyshev.DidAnyWork());
CHECK(prepared.GetStatistics().refreshChecks == 1);
CHECK(prepared.GetStatistics().noOpRefreshes == 1);
const mfem::Vector displacementTrue = gravity_prepared_test_utils::make_displacement(geometry.finiteElements, 0.4);
const mfem::Vector displacement = geometry.context.GetDisplacementMap().gather(displacementTrue);
geometry.context.PreparePrimal(displacement, {.value = 1}, {.value = 2});
CHECK_FALSE(prepared.IsCurrent());
CHECK_FALSE(chebyshevPrepared.IsCurrent());
mfem::Vector rightHandSide(prepared.Width());
mfem::Vector action(prepared.Height());
rightHandSide = 1.0;
action = 0.0;
CHECK_THROWS_AS(prepared.Mult(rightHandSide, action), std::logic_error);
CHECK_THROWS_AS(chebyshevPrepared.Mult(rightHandSide, action), std::logic_error);
const std::uint64_t massPreparations = geometry.context.GetMassOperator().GetPreparationCount();
const std::uint64_t sourcePreparations = geometry.context.GetSourceOperator().GetPreparationCount();
const auto changed = prepared.Refresh(geometry.finiteElements, geometry.context);
const auto changedChebyshev = chebyshevPrepared.Refresh(geometry.finiteElements, geometry.context);
CHECK(changed.geometryChanged);
CHECK_FALSE(changed.discretizationChanged);
CHECK(changed.rebuiltMassInverse);
CHECK_FALSE(changed.rebuiltDivergenceBinding);
CHECK(changed.rebuiltPotentialSchur);
CHECK(prepared.IsCurrent());
CHECK(changedChebyshev.geometryChanged);
CHECK(changedChebyshev.rebuiltMassInverse);
CHECK(changedChebyshev.rebuiltPotentialSchur);
CHECK(chebyshevPrepared.IsCurrent());
CHECK(chebyshevPrepared.GetMassInverse().GetStatistics().setups == 2);
CHECK(geometry.context.GetMassOperator().GetPreparationCount() == massPreparations);
CHECK(geometry.context.GetSourceOperator().GetPreparationCount() == sourcePreparations);
CHECK(prepared.GetStatistics().refreshes == 1);
mfem::Vector refreshedAction(chebyshevPrepared.Height());
refreshedAction = 0.0;
chebyshevPrepared.Mult(rightHandSide, refreshedAction);
for (int index = 0; index < refreshedAction.Size(); ++index) {
CHECK(std::isfinite(refreshedAction(index)));
}
// A discretization revision reconstructs the matrix-free mass operator. The
// owning gravity block must reject every route to its now-stale inverse until
// refresh has rebound and rebuilt the Chebyshev smoother.
geometry.context.PreparePrimal(displacement, {.value = 2}, {.value = 2});
CHECK_FALSE(chebyshevPrepared.IsCurrent());
CHECK_THROWS_AS(chebyshevPrepared.Mult(rightHandSide, action), std::logic_error);
CHECK_THROWS_AS(chebyshevPrepared.GetMassInverse(), std::logic_error);
const auto reconstructed = chebyshevPrepared.Refresh(geometry.finiteElements, geometry.context);
CHECK(reconstructed.discretizationChanged);
CHECK_FALSE(reconstructed.geometryChanged);
CHECK(reconstructed.rebuiltMassInverse);
CHECK(reconstructed.rebuiltDivergenceBinding);
CHECK(reconstructed.rebuiltPotentialSchur);
CHECK(chebyshevPrepared.IsCurrent());
CHECK(chebyshevPrepared.GetMassInverse().GetStatistics().setups == 3);
mfem::Vector firstReconstructedAction(chebyshevPrepared.Height());
mfem::Vector secondReconstructedAction(chebyshevPrepared.Height());
firstReconstructedAction = 0.0;
secondReconstructedAction = 0.0;
chebyshevPrepared.Mult(rightHandSide, firstReconstructedAction);
chebyshevPrepared.Mult(rightHandSide, secondReconstructedAction);
mfem::Vector repeatabilityError(firstReconstructedAction);
repeatabilityError -= secondReconstructedAction;
CHECK(repeatabilityError.Norml2() <= 2.0e-14 * std::max(1.0, firstReconstructedAction.Norml2()));
for (int index = 0; index < firstReconstructedAction.Size(); ++index) {
CHECK(std::isfinite(firstReconstructedAction(index)));
}
}

View File

@@ -0,0 +1,614 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <concepts>
#include <numbers>
#include <stdexcept>
#include <type_traits>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace {
namespace backend = mean_field::preconditioning::backend;
namespace blocks = mean_field::utils::blocks;
namespace preconditioning = mean_field::preconditioning;
using PolytropicModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass,
mean_field::constraint::FixedCentralDensity>>;
using PolytropicProblem = mean_field::equilibrium::StellarEquilibriumProblem<PolytropicModel>;
using PolytropicMaterialSurfaceDescriptor = preconditioning::MaterialSurfaceDescriptorFor<PolytropicProblem>;
using MaterialSurfaceDiagonal = preconditioning::MaterialSurfaceBlock<
PolytropicMaterialSurfaceDescriptor,
backend::Diagonal,
backend::Diagonal,
preconditioning::SurfaceThenMaterialTriangular>;
using FixedCycleAMG = backend::HypreBoomerAMG<backend::FixedCycles>;
using MaterialSurfaceH1 = preconditioning::MaterialSurfaceBlock<
PolytropicMaterialSurfaceDescriptor,
backend::Diagonal,
FixedCycleAMG,
preconditioning::ApproximateMaterialSurfaceLDU,
preconditioning::SurfaceH1MassStiffness>;
using PreparedMaterialSurfaceDiagonal = preconditioning::PreparedMaterialSurfaceBlock<
PolytropicMaterialSurfaceDescriptor,
preconditioning::SurfaceThenMaterialTriangular>;
using PreparedMaterialSurfaceH1 = preconditioning::PreparedH1MaterialSurfaceBlock<
PolytropicMaterialSurfaceDescriptor,
preconditioning::ApproximateMaterialSurfaceLDU,
backend::FixedCycles>;
class KnownCouplings final {
public:
KnownCouplings() : m_offsets(4) {
m_offsets[0] = 0;
m_offsets[1] = 1;
m_offsets[2] = 2;
m_offsets[3] = 3;
}
[[nodiscard]] int Height() const noexcept {
return 3;
}
[[nodiscard]] const mfem::Array<int> &GetOffsets() const noexcept {
return m_offsets;
}
void ApplyEnthalpyToDensity(
const mfem::Vector &enthalpy,
mfem::Vector &density
) const {
density(0) = 4.0 * enthalpy(0);
}
void ApplySurfaceToMaterial(
const mfem::Vector &surface,
mfem::Vector &density,
mfem::Vector &enthalpy
) const {
density(0) = 3.0 * surface(0);
enthalpy(0) = 8.0 * surface(0);
}
void ApplyMaterialToSurface(
const mfem::Vector &density,
const mfem::Vector &enthalpy,
mfem::Vector &surface
) const {
surface(0) = 5.0 * density(0) + 7.0 * enthalpy(0);
}
private:
mfem::Array<int> m_offsets;
};
template <typename Policy>
[[nodiscard]] mfem::Vector applyKnownFactorization(
Policy policy,
const mfem::Vector &rightHandSide,
const double surfaceEntry = 6.0
) {
mfem::Vector densityDiagonal(1);
mfem::Vector surfaceDiagonal(1);
mfem::Vector enthalpyDiagonal(1);
densityDiagonal(0) = 2.0;
surfaceDiagonal(0) = surfaceEntry;
enthalpyDiagonal(0) = 9.0;
const auto densityInverse = backend::prepare(backend::Diagonal{}, densityDiagonal);
const auto surfaceInverse = backend::prepare(backend::Diagonal{}, surfaceDiagonal);
const auto enthalpyInverse = backend::prepare(backend::Diagonal{}, enthalpyDiagonal);
const KnownCouplings couplings;
preconditioning::MaterialSurfaceFactorizationOperator<Policy, KnownCouplings> factorization(
policy, densityInverse, surfaceInverse, enthalpyInverse, couplings
);
mfem::Vector action(3);
factorization.Mult(rightHandSide, action);
return action;
}
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies makeDependencies(std::uint64_t revision = 1) {
return {
.discretization = {.identity = 8101, .revision = 1},
.density = {.identity = 8103, .revision = revision},
.surfaceDeformation = {.identity = 8107, .revision = revision},
.gravityGradient = {.identity = 8111, .revision = revision},
.gravityPotential = {.identity = 8117, .revision = revision},
.enthalpy = {.identity = 8123, .revision = revision},
.bernoulliConstant = {.identity = 8129, .revision = revision},
.rotation = {.identity = 8131, .revision = revision},
.targetMass = {.identity = 8137, .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 &left,
const mfem::Vector &right
) {
mfem::Vector difference(left);
difference -= right;
return difference.Norml2() / std::max({1.0, left.Norml2(), right.Norml2()});
}
} // namespace
TEST_CASE(
"Compiled Material Surface Blocks Derive Their Physical Ownership And Backend Requirements",
"[preconditioning][material_surface][unit][type_contract]"
) {
using Form = blocks::surface_deformed_stellar_equilibrium_form;
using JacobianForm = blocks::surface_deformed_stellar_equilibrium_jacobian_form;
using GravityIdentity =
preconditioning::IdentityBlock<blocks::gravity::gradient::value, blocks::gravity::gradient::residual>;
using PotentialIdentity =
preconditioning::IdentityBlock<blocks::gravity::poisson::value, blocks::gravity::poisson::residual>;
using MassIdentity = preconditioning::IdentityBlock<
blocks::fixed_total_mass::mass_normalization::value, blocks::fixed_total_mass::mass_normalization::residual>;
using Plan =
preconditioning::PreconditionerPlan<MaterialSurfaceDiagonal, GravityIdentity, PotentialIdentity, MassIdentity>;
STATIC_CHECK(preconditioning::PreconditionerComponent<MaterialSurfaceDiagonal>);
STATIC_CHECK(preconditioning::PreconditionerComponent<MaterialSurfaceH1>);
STATIC_CHECK(preconditioning::MaterialSurfaceDescriptor<PolytropicMaterialSurfaceDescriptor>);
STATIC_CHECK(
mean_field::material::CompiledThermodynamicEquations<typename PolytropicProblem::ThermodynamicEquationsType>
);
STATIC_CHECK(
std::same_as<
typename PolytropicMaterialSurfaceDescriptor::SurfaceStateFields,
mean_field::field::TypeList<mean_field::field::Enthalpy>>
);
STATIC_CHECK(MaterialSurfaceDiagonal::CorrectionBlocks::size == 3);
STATIC_CHECK(MaterialSurfaceDiagonal::ResidualBlocks::size == 3);
STATIC_CHECK(MaterialSurfaceDiagonal::RequiredCouplings::size == 8);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<Plan, Form>);
STATIC_CHECK(preconditioning::CompatiblePreconditionerFor<Plan, Form, JacobianForm>);
STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename MaterialSurfaceDiagonal::BackendType>);
STATIC_CHECK_FALSE(std::is_copy_constructible_v<PreparedMaterialSurfaceDiagonal>);
STATIC_CHECK_FALSE(std::is_copy_assignable_v<PreparedMaterialSurfaceDiagonal>);
STATIC_CHECK_FALSE(std::is_move_constructible_v<PreparedMaterialSurfaceDiagonal>);
STATIC_CHECK_FALSE(std::is_move_assignable_v<PreparedMaterialSurfaceDiagonal>);
STATIC_CHECK_FALSE(std::is_copy_constructible_v<PreparedMaterialSurfaceH1>);
STATIC_CHECK_FALSE(std::is_copy_assignable_v<PreparedMaterialSurfaceH1>);
STATIC_CHECK_FALSE(std::is_move_constructible_v<PreparedMaterialSurfaceH1>);
STATIC_CHECK_FALSE(std::is_move_assignable_v<PreparedMaterialSurfaceH1>);
STATIC_CHECK(
preconditioning::backend::Compatible<backend::Diagonal, preconditioning::SurfaceDiagonalCharacteristics>
);
STATIC_CHECK_FALSE(
preconditioning::backend::Compatible<backend::DenseDirect, preconditioning::SurfaceDiagonalCharacteristics>
);
STATIC_CHECK(
preconditioning::backend::Compatible<FixedCycleAMG, preconditioning::SurfaceH1MassStiffnessCharacteristics>
);
STATIC_CHECK_FALSE(std::same_as<MaterialSurfaceDiagonal, MaterialSurfaceH1>);
STATIC_CHECK(std::same_as<typename MaterialSurfaceH1::SurfaceSurrogate, preconditioning::SurfaceH1MassStiffness>);
STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename MaterialSurfaceH1::BackendType>);
}
TEST_CASE(
"Surface H1 Calibration Recovers Signed Nonnegative Mass And Stiffness Fits",
"[preconditioning][material_surface][surface_h1][unit]"
) {
const preconditioning::SurfaceH1MassStiffness configuration{
.calibration =
{.target = preconditioning::SurfaceRieszCalibrationTarget::approximate_material_schur, .probeCount = 4},
.relativeMassCoefficientFloor = 1.0e-12,
.gramRelativeTolerance = 1.0e-12
};
const preconditioning::SurfaceH1NormalEquations exactPositive{
.massMass = 2.0,
.massStiffness = 2.0,
.stiffnessStiffness = 5.0,
.massTarget = 10.0,
.stiffnessTarget = 19.0,
.targetTarget = 77.0
};
const auto positive = preconditioning::detail::fitSurfaceH1Coefficients(exactPositive, configuration);
CHECK(positive.WasCalibrated());
CHECK(positive.sign == 1.0);
CHECK(positive.massCoefficient == Catch::Approx(2.0).margin(2.0e-13));
CHECK(positive.stiffnessCoefficient == Catch::Approx(3.0).margin(2.0e-13));
CHECK(positive.relativeResidual == Catch::Approx(0.0).margin(2.0e-13));
CHECK(positive.relativeGramDeterminant > configuration.gramRelativeTolerance);
CHECK(positive.normalEquations.targetTarget == Catch::Approx(77.0));
auto exactNegative = exactPositive;
exactNegative.massTarget = -exactNegative.massTarget;
exactNegative.stiffnessTarget = -exactNegative.stiffnessTarget;
const auto negative = preconditioning::detail::fitSurfaceH1Coefficients(exactNegative, configuration);
CHECK(negative.sign == -1.0);
CHECK(negative.massCoefficient == Catch::Approx(2.0).margin(2.0e-13));
CHECK(negative.stiffnessCoefficient == Catch::Approx(3.0).margin(2.0e-13));
CHECK(negative.relativeResidual == Catch::Approx(0.0).margin(2.0e-13));
const preconditioning::SurfaceH1NormalEquations massDominated{
.massMass = 1.0,
.massStiffness = 0.0,
.stiffnessStiffness = 1.0,
.massTarget = 4.0,
.stiffnessTarget = -2.0,
.targetTarget = 20.0
};
const auto constrained = preconditioning::detail::fitSurfaceH1Coefficients(massDominated, configuration);
CHECK(constrained.sign == 1.0);
CHECK(constrained.massCoefficient == Catch::Approx(4.0).margin(2.0e-13));
CHECK(constrained.stiffnessCoefficient == Catch::Approx(0.0).margin(2.0e-13));
CHECK(constrained.relativeResidual == Catch::Approx(std::sqrt(0.2)).margin(2.0e-13));
auto rankDeficient = exactPositive;
rankDeficient.massMass = 1.0;
rankDeficient.massStiffness = 2.0;
rankDeficient.stiffnessStiffness = 4.0;
CHECK_THROWS_AS(
preconditioning::detail::fitSurfaceH1Coefficients(rankDeficient, configuration), std::runtime_error
);
}
TEST_CASE(
"Surface Riesz Scalar Calibration Distinguishes Operator And Right-Preconditioned Objectives",
"[preconditioning][material_surface][surface_riesz][unit]"
) {
using Objective = preconditioning::SurfaceRieszCalibrationObjective;
const auto operatorFit = preconditioning::detail::fitSurfaceRieszScalar(6.0, 2.0, Objective::operator_action);
CHECK(operatorFit.surrogateScale == Catch::Approx(3.0));
CHECK(operatorFit.inverseMultiplier == Catch::Approx(1.0 / 3.0));
const auto inverseFit =
preconditioning::detail::fitSurfaceRieszScalar(6.0, 2.0, Objective::right_preconditioned_action);
CHECK(inverseFit.surrogateScale == Catch::Approx(1.0 / 3.0));
CHECK(inverseFit.inverseMultiplier == Catch::Approx(3.0));
CHECK_THROWS_AS(
preconditioning::detail::fitSurfaceRieszScalar(1.0, 0.0, Objective::operator_action), std::invalid_argument
);
CHECK_THROWS_AS(
preconditioning::detail::fitSurfaceRieszScalar(0.0, 1.0, Objective::right_preconditioned_action),
std::runtime_error
);
}
TEST_CASE(
"Signed Surface Solver Adapts Boundary Coordinates Without Exposing An Indefinite Backend",
"[preconditioning][material_surface][surface_h1][unit]"
) {
mfem::DenseMatrix ambientMatrix(3);
ambientMatrix = 0.0;
ambientMatrix(0, 0) = 2.0;
ambientMatrix(1, 1) = 7.0;
ambientMatrix(2, 2) = 4.0;
const auto ambientInverse = backend::prepare(backend::DenseDirect{}, ambientMatrix);
mfem::Array<int> boundaryTrueDofs(2);
boundaryTrueDofs[0] = 0;
boundaryTrueDofs[1] = 2;
mean_field::field::ScalarBoundaryDofMap surfaceMap(3, boundaryTrueDofs, 0, 2);
preconditioning::SignedScalarBoundarySolverAdapter surfaceInverse(ambientInverse, surfaceMap, -1.0);
mfem::Vector rightHandSide(2);
mfem::Vector action(2);
rightHandSide(0) = 2.0;
rightHandSide(1) = 4.0;
surfaceInverse.Mult(rightHandSide, action);
CHECK(action(0) == Catch::Approx(-1.0));
CHECK(action(1) == Catch::Approx(-1.0));
CHECK(surfaceInverse.GetSign() == -1.0);
CHECK_THROWS_AS(surfaceInverse.SetSign(0.0), std::invalid_argument);
}
TEST_CASE(
"Material Surface Factorization Policies Preserve Their Signed Triangular Algebra",
"[preconditioning][material_surface][unit][factorization]"
) {
mfem::Vector rightHandSide(3);
rightHandSide(0) = 29.0;
rightHandSide(1) = 44.0;
rightHandSide(2) = 43.0;
const auto check = [](const mfem::Vector &value, std::array<double, 3> expected) {
for (int index = 0; index < value.Size(); ++index) {
CHECK(value(index) == Catch::Approx(expected[static_cast<std::size_t>(index)]).margin(2.0e-13));
}
};
check(
applyKnownFactorization(preconditioning::MaterialSurfaceBlockDiagonal{}, rightHandSide),
{14.5, 44.0 / 6.0, 43.0 / 9.0}
);
check(
applyKnownFactorization(preconditioning::CoupledMaterialIndependentSurface{}, rightHandSide),
{(29.0 - 4.0 * (43.0 / 9.0)) / 2.0, 44.0 / 6.0, 43.0 / 9.0}
);
const double materialEnthalpy = 43.0 / 9.0;
const double materialDensity = (29.0 - 4.0 * materialEnthalpy) / 2.0;
check(
applyKnownFactorization(preconditioning::MaterialThenSurfaceTriangular{}, rightHandSide),
{materialDensity, (44.0 - 5.0 * materialDensity - 7.0 * materialEnthalpy) / 6.0, materialEnthalpy}
);
const double surfaceFirst = 44.0 / 6.0;
const double surfaceCorrectedEnthalpy = (43.0 - 8.0 * surfaceFirst) / 9.0;
check(
applyKnownFactorization(preconditioning::SurfaceThenMaterialTriangular{}, rightHandSide),
{(29.0 - 3.0 * surfaceFirst - 4.0 * surfaceCorrectedEnthalpy) / 2.0, surfaceFirst, surfaceCorrectedEnthalpy}
);
const double firstMaterialEnthalpy = 43.0 / 9.0;
const double firstMaterialDensity = (29.0 - 4.0 * firstMaterialEnthalpy) / 2.0;
const double lduSurface = (44.0 - 5.0 * firstMaterialDensity - 7.0 * firstMaterialEnthalpy) / 6.0;
const double lduEnthalpy = (43.0 - 8.0 * lduSurface) / 9.0;
check(
applyKnownFactorization(preconditioning::ApproximateMaterialSurfaceLDU{}, rightHandSide),
{(29.0 - 3.0 * lduSurface - 4.0 * lduEnthalpy) / 2.0, lduSurface, lduEnthalpy}
);
// M = [[2,4],[0,9]], B = [3,8]^T, C = [5,7], so the exact
// scalar surface Schur complement is 6 - C M^{-1} B = 7/6.
const mfem::Vector exact =
applyKnownFactorization(preconditioning::ApproximateMaterialSurfaceLDU{}, rightHandSide, 7.0 / 6.0);
CHECK(2.0 * exact(0) + 3.0 * exact(1) + 4.0 * exact(2) == Catch::Approx(29.0).margin(2.0e-12));
CHECK(5.0 * exact(0) + 6.0 * exact(1) + 7.0 * exact(2) == Catch::Approx(44.0).margin(2.0e-12));
CHECK(8.0 * exact(1) + 9.0 * exact(2) == Catch::Approx(43.0).margin(2.0e-12));
}
TEST_CASE(
"Generated Material Surface Action Is The Exact Restricted Stellar Jacobian And Uses A Bounded Surrogate",
"[preconditioning][material_surface][surface_h1][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);
const auto stellarModel = 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(stellarModel, finiteElements);
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 512}));
const auto rotation = zeroRotation();
problem.Prepare(projected.values, makeDependencies(), rotation);
const auto &physical = problem.GetPreparedOperator().GetPhysicalOperator();
const auto block = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, backend::Diagonal{}, preconditioning::SurfaceThenMaterialTriangular{}
);
const auto defaultBlock = preconditioning::materialSurfaceBlock(problem);
STATIC_CHECK(
std::same_as<
typename std::remove_cvref_t<decltype(block)>::Descriptor,
preconditioning::MaterialSurfaceDescriptorFor<decltype(problem)>>
);
STATIC_CHECK(std::same_as<std::remove_cvref_t<decltype(defaultBlock)>, std::remove_cvref_t<decltype(block)>>);
auto prepared = preconditioning::prepare(problem, block);
const auto &restricted = prepared.GetCoupledOperator();
mfem::Vector restrictedDirection(restricted.Width());
for (int index = 0; index < restrictedDirection.Size(); ++index) {
restrictedDirection(index) = 0.01 * std::sin(0.37 * static_cast<double>(index + 1));
}
mfem::Vector restrictedAction(restricted.Height());
restricted.Mult(restrictedDirection, restrictedAction);
mfem::Vector fullDirection(physical.Width());
fullDirection = 0.0;
const auto fullDirectionView = physical.GetRootManifest().directionView(fullDirection);
const auto &offsets = restricted.GetOffsets();
const mfem::Vector densityDirection(restrictedDirection.GetData(), offsets[1]);
const mfem::Vector surfaceDirection(restrictedDirection.GetData() + offsets[1], offsets[2] - offsets[1]);
const mfem::Vector enthalpyDirection(restrictedDirection.GetData() + offsets[2], offsets[3] - offsets[2]);
mfem::Vector fullDensityDirection = fullDirectionView.block(blocks::density_field.mass_term);
mfem::Vector fullSurfaceDirection = fullDirectionView.block(blocks::surface_deformation_field.parameters_term);
mfem::Vector fullEnthalpyDirection = fullDirectionView.block(blocks::enthalpy_field.specific_term);
fullDensityDirection = densityDirection;
fullSurfaceDirection = surfaceDirection;
fullEnthalpyDirection = enthalpyDirection;
mfem::Vector fullAction;
physical.Mult(fullDirection, fullAction);
const auto fullActionView = physical.GetRootManifest().residualView(fullAction);
mfem::Vector expected(restricted.Height());
mfem::Vector expectedDensity(expected.GetData(), offsets[1]);
mfem::Vector expectedSurface(expected.GetData() + offsets[1], offsets[2] - offsets[1]);
mfem::Vector expectedEnthalpy(expected.GetData() + offsets[2], offsets[3] - offsets[2]);
const mfem::Vector fullDensityAction = fullActionView.block(blocks::density_field.mass_term);
const mfem::Vector fullSurfaceAction =
fullActionView.block(blocks::surface_deformation_field.shape_equilibrium_term);
const mfem::Vector fullEnthalpyAction = fullActionView.block(blocks::enthalpy_field.specific_term);
expectedDensity = fullDensityAction;
expectedSurface = fullSurfaceAction;
expectedEnthalpy = fullEnthalpyAction;
const mfem::Vector restrictedDensity(restrictedAction.GetData(), offsets[1]);
const mfem::Vector restrictedSurface(restrictedAction.GetData() + offsets[1], offsets[2] - offsets[1]);
const mfem::Vector restrictedEnthalpy(restrictedAction.GetData() + offsets[2], offsets[3] - offsets[2]);
INFO("Restricted density-row error = " << relativeError(restrictedDensity, expectedDensity));
INFO("Restricted surface-row error = " << relativeError(restrictedSurface, expectedSurface));
INFO("Restricted enthalpy-row error = " << relativeError(restrictedEnthalpy, expectedEnthalpy));
CHECK(relativeError(restrictedDensity, expectedDensity) <= 2.0e-12);
CHECK(relativeError(restrictedSurface, expectedSurface) <= 2.0e-12);
CHECK(relativeError(restrictedEnthalpy, expectedEnthalpy) <= 2.0e-12);
CHECK(relativeError(restrictedAction, expected) <= 2.0e-12);
CHECK(prepared.GetDensityDiagonalQuality().maximumAbsoluteEntryBeforeRegularization > 0.0);
CHECK(prepared.GetSurfaceDiagonalQuality().maximumAbsoluteEntryBeforeRegularization > 0.0);
CHECK(prepared.GetSurfaceDiagonalQuality().minimumAbsoluteEntryBeforeRegularization > 0.0);
CHECK(prepared.GetSurfaceDiagonalQuality().regularizedEntries == 0);
CHECK(prepared.GetEnthalpyDiagonalQuality().maximumAbsoluteEntryBeforeRegularization > 0.0);
CHECK(prepared.GetStatistics().surfaceJacobianProbes == 0);
CHECK(prepared.GetStatistics().surfaceRieszAssemblies == 1);
const auto calibratedBlock = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, backend::Diagonal{}, preconditioning::ApproximateMaterialSurfaceLDU{},
{.surfaceCalibration = {
.target = preconditioning::SurfaceRieszCalibrationTarget::approximate_material_schur,
.probeCount = 3,
.objective = preconditioning::SurfaceRieszCalibrationObjective::right_preconditioned_action
}}
);
auto calibrated = preconditioning::prepare(problem, calibratedBlock);
CHECK(calibrated.GetSurfaceCalibration().WasCalibrated());
CHECK(
calibrated.GetSurfaceCalibration().target ==
preconditioning::SurfaceRieszCalibrationTarget::approximate_material_schur
);
CHECK(calibrated.GetSurfaceCalibration().probeCount == 3);
CHECK(
calibrated.GetSurfaceCalibration().objective ==
preconditioning::SurfaceRieszCalibrationObjective::right_preconditioned_action
);
CHECK(std::isfinite(calibrated.GetSurfaceCalibration().scale));
CHECK(calibrated.GetSurfaceCalibration().scale != 0.0);
CHECK(calibrated.GetStatistics().surfaceJacobianProbes == 3);
CHECK(calibrated.GetSurfaceDiagonalQuality().maximumAbsoluteEntryBeforeRegularization > 0.0);
const auto frequencyAwareBlock = preconditioning::materialSurfaceBlock(
problem, backend::Diagonal{}, FixedCycleAMG{backend::FixedCycles{.cycles = 1}},
preconditioning::ApproximateMaterialSurfaceLDU{},
preconditioning::SurfaceH1MassStiffness{
.calibration =
{.target = preconditioning::SurfaceRieszCalibrationTarget::approximate_material_schur, .probeCount = 4},
.relativeMassCoefficientFloor = 1.0e-10,
.gramRelativeTolerance = 1.0e-12
}
);
STATIC_CHECK(
std::same_as<
typename std::remove_cvref_t<decltype(frequencyAwareBlock)>::SurfaceSurrogate,
preconditioning::SurfaceH1MassStiffness>
);
auto frequencyAware = preconditioning::prepare(problem, frequencyAwareBlock);
using PreparedFrequencyAware = std::remove_cvref_t<decltype(frequencyAware)>;
STATIC_CHECK_FALSE(std::copy_constructible<PreparedFrequencyAware>);
STATIC_CHECK_FALSE(std::move_constructible<PreparedFrequencyAware>);
const auto &surfaceFit = frequencyAware.GetSurfaceFit();
CHECK(surfaceFit.WasCalibrated());
CHECK(surfaceFit.target == preconditioning::SurfaceRieszCalibrationTarget::approximate_material_schur);
CHECK(surfaceFit.probeCount == 4);
CHECK((surfaceFit.sign == -1.0 || surfaceFit.sign == 1.0));
CHECK(std::isfinite(surfaceFit.massCoefficient));
CHECK(surfaceFit.massCoefficient > 0.0);
CHECK(std::isfinite(surfaceFit.stiffnessCoefficient));
CHECK(surfaceFit.stiffnessCoefficient >= 0.0);
CHECK(std::isfinite(surfaceFit.relativeResidual));
CHECK(surfaceFit.relativeGramDeterminant > 1.0e-12);
CHECK(surfaceFit.normalEquations.targetTarget > 0.0);
CHECK(frequencyAware.GetSurfaceInverse().Height() == physical.GetDomainDeformation().parameterCount());
CHECK(frequencyAware.GetSurfaceSurrogateMatrix().Height() == finiteElements.surfaceDeformationFes->GetTrueVSize());
CHECK(frequencyAware.GetSurfaceBackend().GetStatistics().setups == 1);
CHECK(frequencyAware.GetStatistics().surfaceJacobianProbes == 4);
CHECK(frequencyAware.GetStatistics().surfaceH1Assemblies == 3);
const auto frequencyAwareNoChange = frequencyAware.Refresh(physical);
CHECK_FALSE(frequencyAwareNoChange.DidAnyWork());
CHECK(frequencyAware.GetStatistics().noOpRefreshes == 1);
mfem::Vector rightHandSide(prepared.Width());
mfem::Vector correction(prepared.Height());
mfem::Vector repeatedCorrection(prepared.Height());
for (int index = 0; index < rightHandSide.Size(); ++index) {
rightHandSide(index) = std::cos(0.19 * static_cast<double>(index + 1));
}
correction = 0.0;
repeatedCorrection = 0.0;
double *const correctionStorage = correction.GetData();
prepared.Mult(rightHandSide, correction);
prepared.Mult(rightHandSide, repeatedCorrection);
CHECK(correction.GetData() == correctionStorage);
CHECK(relativeError(correction, repeatedCorrection) <= 2.0e-15);
for (int index = 0; index < correction.Size(); ++index) {
REQUIRE(std::isfinite(correction(index)));
}
mfem::Vector frequencyAwareCorrection(frequencyAware.Height());
mfem::Vector repeatedFrequencyAwareCorrection(frequencyAware.Height());
frequencyAwareCorrection = 0.0;
repeatedFrequencyAwareCorrection = 0.0;
frequencyAware.Mult(rightHandSide, frequencyAwareCorrection);
frequencyAware.Mult(rightHandSide, repeatedFrequencyAwareCorrection);
CHECK(relativeError(frequencyAwareCorrection, repeatedFrequencyAwareCorrection) <= 2.0e-13);
for (int index = 0; index < frequencyAwareCorrection.Size(); ++index) {
REQUIRE(std::isfinite(frequencyAwareCorrection(index)));
}
const auto noChange = prepared.Refresh(physical);
CHECK_FALSE(noChange.DidAnyWork());
// Full stellar-Jacobian finite-difference accuracy is covered by the
// prepared-stellar-equilibrium tests. Here we change the state only to
// exercise the material-surface refresh contract without repeating two
// expensive nonlinear residual assemblies.
mfem::Vector changedState(projected.values);
mfem::Vector borderedDirection(problem.StateSize());
borderedDirection = 0.0;
mfem::Vector physicalDirection(borderedDirection.GetData(), physical.Width());
physicalDirection = fullDirection;
changedState.Add(1.0e-5, borderedDirection);
problem.Prepare(changedState, makeDependencies(2), rotation);
CHECK_FALSE(prepared.IsCurrent());
rightHandSide = 1.0;
correction = 0.0;
CHECK_THROWS_AS(prepared.Mult(rightHandSide, correction), std::logic_error);
const auto refreshed = prepared.Refresh(problem.GetPreparedOperator().GetPhysicalOperator());
CHECK(refreshed.linearizationChanged);
CHECK(refreshed.rebuiltDensityInverse);
CHECK(refreshed.rebuiltSurfaceInverse);
CHECK(refreshed.rebuiltEnthalpyInverse);
CHECK(prepared.IsCurrent());
CHECK(prepared.GetStatistics().surfaceJacobianProbes == 0);
CHECK(prepared.GetStatistics().surfaceRieszAssemblies == 2);
auto densityOnlyDependencies = makeDependencies(2);
densityOnlyDependencies.density.revision = 3;
problem.Prepare(changedState, densityOnlyDependencies, rotation);
CHECK_FALSE(prepared.IsCurrent());
const auto stateOnlyRefresh = prepared.Refresh(problem.GetPreparedOperator().GetPhysicalOperator());
CHECK(stateOnlyRefresh.linearizationChanged);
CHECK_FALSE(stateOnlyRefresh.DidAnyWork());
CHECK_FALSE(stateOnlyRefresh.rebuiltDensityInverse);
CHECK_FALSE(stateOnlyRefresh.rebuiltSurfaceInverse);
CHECK_FALSE(stateOnlyRefresh.rebuiltEnthalpyInverse);
CHECK(prepared.IsCurrent());
CHECK(prepared.GetStatistics().surfaceRieszAssemblies == 2);
CHECK_FALSE(calibrated.IsCurrent());
CHECK_FALSE(frequencyAware.IsCurrent());
const auto calibratedRefresh = calibrated.Refresh(problem.GetPreparedOperator().GetPhysicalOperator());
CHECK(calibratedRefresh.DidAnyWork());
CHECK(calibratedRefresh.rebuiltDensityInverse);
CHECK(calibratedRefresh.rebuiltSurfaceInverse);
CHECK(calibratedRefresh.rebuiltEnthalpyInverse);
CHECK(calibrated.IsCurrent());
CHECK(calibrated.GetStatistics().surfaceJacobianProbes == 6);
CHECK(calibrated.GetStatistics().surfaceRieszAssemblies == 2);
const auto frequencyAwareRefresh = frequencyAware.Refresh(problem.GetPreparedOperator().GetPhysicalOperator());
CHECK(frequencyAwareRefresh.DidAnyWork());
CHECK(frequencyAwareRefresh.rebuiltDensityInverse);
CHECK(frequencyAwareRefresh.rebuiltSurfaceInverse);
CHECK(frequencyAwareRefresh.rebuiltEnthalpyInverse);
CHECK(frequencyAware.IsCurrent());
CHECK(frequencyAware.GetSurfaceBackend().GetStatistics().setups == 2);
CHECK(frequencyAware.GetStatistics().surfaceJacobianProbes == 8);
CHECK(frequencyAware.GetStatistics().surfaceH1Assemblies == 6);
frequencyAware.Mult(rightHandSide, frequencyAwareCorrection);
for (int index = 0; index < frequencyAwareCorrection.Size(); ++index) {
REQUIRE(std::isfinite(frequencyAwareCorrection(index)));
}
}

View File

@@ -0,0 +1,236 @@
#include <concepts>
#include <type_traits>
#include <catch2/catch_test_macros.hpp>
import mean_field;
import test_helpers;
namespace {
namespace blocks = mean_field::utils::blocks;
namespace preconditioning = mean_field::preconditioning;
using Form = blocks::surface_deformed_stellar_equilibrium_form;
using JacobianForm = blocks::surface_deformed_stellar_equilibrium_jacobian_form;
using CentralForm = blocks::central_density_bordered_stellar_equilibrium_form;
using CentralJacobianForm = blocks::central_density_bordered_stellar_equilibrium_jacobian_form;
using DensityIdentity =
preconditioning::IdentityBlock<blocks::density::mass::value, blocks::density::mass::residual>;
using SurfaceIdentity = preconditioning::IdentityBlock<
blocks::surface_deformation::parameters::value,
blocks::surface_deformation::shape_equilibrium::residual>;
using GravityGradientIdentity =
preconditioning::IdentityBlock<blocks::gravity::gradient::value, blocks::gravity::gradient::residual>;
using GravityPotentialIdentity =
preconditioning::IdentityBlock<blocks::gravity::poisson::value, blocks::gravity::poisson::residual>;
using EnthalpyIdentity =
preconditioning::IdentityBlock<blocks::enthalpy::specific::value, blocks::enthalpy::specific::residual>;
using MassIdentity = preconditioning::IdentityBlock<
blocks::fixed_total_mass::mass_normalization::value,
blocks::fixed_total_mass::mass_normalization::residual>;
using CentralDensityIdentity = preconditioning::IdentityBlock<
blocks::fixed_central_density::central_value::value,
blocks::fixed_central_density::central_value::residual>;
using IdentityPlan = preconditioning::PreconditionerPlan<
DensityIdentity,
SurfaceIdentity,
GravityGradientIdentity,
GravityPotentialIdentity,
EnthalpyIdentity,
MassIdentity>;
using CentralIdentityPlan = preconditioning::PreconditionerPlan<
DensityIdentity,
SurfaceIdentity,
GravityGradientIdentity,
GravityPotentialIdentity,
EnthalpyIdentity,
MassIdentity,
CentralDensityIdentity>;
using IncompleteCentralPlan = IdentityPlan;
struct AlternateDensityIdentity final : preconditioning::ComponentDeclaration<
blocks::type_list<blocks::density::mass::value>,
blocks::type_list<blocks::density::mass::residual>,
blocks::type_list<>,
preconditioning::IdentityOperatorCharacteristics,
preconditioning::backend::Identity> { };
using DuplicateOwnershipPlan = preconditioning::PreconditionerPlan<
DensityIdentity,
SurfaceIdentity,
GravityGradientIdentity,
GravityPotentialIdentity,
EnthalpyIdentity,
MassIdentity,
CentralDensityIdentity,
AlternateDensityIdentity>;
using ExplicitOverlapPlan = preconditioning::OverlappingPreconditionerPlan<
DensityIdentity,
SurfaceIdentity,
GravityGradientIdentity,
GravityPotentialIdentity,
EnthalpyIdentity,
MassIdentity,
CentralDensityIdentity,
AlternateDensityIdentity>;
struct ExtraCorrection final : blocks::value_block_base { };
struct ExtraResidual final : blocks::residual_block_base { };
using ExtraIdentity = preconditioning::IdentityBlock<ExtraCorrection, ExtraResidual>;
using UnexpectedOwnershipPlan = preconditioning::PreconditionerPlan<
DensityIdentity,
SurfaceIdentity,
GravityGradientIdentity,
GravityPotentialIdentity,
EnthalpyIdentity,
MassIdentity,
ExtraIdentity>;
using CoupledGravity = preconditioning::ComponentDeclaration<
blocks::type_list<blocks::gravity::gradient::value, blocks::gravity::poisson::value>,
blocks::type_list<blocks::gravity::gradient::residual, blocks::gravity::poisson::residual>,
blocks::type_list<
preconditioning::Coupling<blocks::gravity::gradient::residual, blocks::gravity::gradient::value>,
preconditioning::Coupling<blocks::gravity::gradient::residual, blocks::gravity::poisson::value>,
preconditioning::Coupling<blocks::gravity::poisson::residual, blocks::gravity::gradient::value>>,
preconditioning::IdentityOperatorCharacteristics,
preconditioning::backend::Identity>;
using ValidCoupledPlan = preconditioning::
PreconditionerPlan<DensityIdentity, SurfaceIdentity, CoupledGravity, EnthalpyIdentity, MassIdentity>;
using InvalidGravityCoupling = preconditioning::ComponentDeclaration<
blocks::type_list<blocks::gravity::gradient::value, blocks::gravity::poisson::value>,
blocks::type_list<blocks::gravity::gradient::residual, blocks::gravity::poisson::residual>,
blocks::type_list<preconditioning::Coupling<blocks::gravity::gradient::residual, blocks::density::mass::value>>,
preconditioning::IdentityOperatorCharacteristics,
preconditioning::backend::Identity>;
using InvalidCoupledPlan = preconditioning::
PreconditionerPlan<DensityIdentity, SurfaceIdentity, InvalidGravityCoupling, EnthalpyIdentity, MassIdentity>;
using IncompatibleBackendComponent = preconditioning::ComponentDeclaration<
blocks::type_list<blocks::gravity::gradient::value>,
blocks::type_list<blocks::gravity::gradient::residual>,
blocks::type_list<>,
preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::elliptic_like,
preconditioning::OperatorValueStructure::vector,
preconditioning::OperatorSymmetry::symmetric,
preconditioning::OperatorDefiniteness::positive_definite,
preconditioning::OperatorRepresentation::assembled_sparse,
preconditioning::OperatorDistribution::distributed_true_dof,
preconditioning::OperatorFESpace::h_div>,
preconditioning::backend::HypreBoomerAMG<>>;
struct IncoherentPlanDeclaration final {
using ComponentTypes = blocks::type_list<DensityIdentity>;
using CorrectionBlocks = blocks::type_list<blocks::gravity::gradient::value>;
using ResidualBlocks = blocks::type_list<blocks::density::mass::residual>;
using RequiredCouplings = blocks::type_list<>;
};
} // namespace
TEST_CASE(
"Preconditioning Backends Advertise Compile-Time Operator Compatibility",
tags::preconditioning_type_contract
) {
using ScalarElliptic = preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::elliptic_like, preconditioning::OperatorValueStructure::scalar,
preconditioning::OperatorSymmetry::symmetric, preconditioning::OperatorDefiniteness::positive_definite,
preconditioning::OperatorRepresentation::assembled_sparse,
preconditioning::OperatorDistribution::distributed_true_dof, preconditioning::OperatorFESpace::h1>;
using VectorElliptic = preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::elliptic_like, preconditioning::OperatorValueStructure::vector,
preconditioning::OperatorSymmetry::symmetric, preconditioning::OperatorDefiniteness::positive_definite,
preconditioning::OperatorRepresentation::assembled_sparse,
preconditioning::OperatorDistribution::distributed_true_dof, preconditioning::OperatorFESpace::h_div>;
using LocalDenseBorder = preconditioning::OperatorCharacteristics<
preconditioning::OperatorCategory::dense_border, preconditioning::OperatorValueStructure::block,
preconditioning::OperatorSymmetry::nonsymmetric, preconditioning::OperatorDefiniteness::indefinite,
preconditioning::OperatorRepresentation::assembled_dense, preconditioning::OperatorDistribution::local>;
using FixedAMG = preconditioning::backend::HypreBoomerAMG<preconditioning::backend::FixedCycles>;
STATIC_CHECK(preconditioning::backend::Registered<FixedAMG>);
STATIC_CHECK(preconditioning::backend::Compatible<FixedAMG, ScalarElliptic>);
STATIC_CHECK_FALSE(preconditioning::backend::Compatible<FixedAMG, VectorElliptic>);
STATIC_CHECK(preconditioning::backend::Compatible<preconditioning::backend::DenseDirect, LocalDenseBorder>);
STATIC_CHECK_FALSE(preconditioning::PreconditionerComponent<IncompatibleBackendComponent>);
STATIC_CHECK(
preconditioning::backend::applicationContract<FixedAMG> ==
preconditioning::ApplicationContract::stationary_linear
);
}
TEST_CASE(
"Preconditioner Plans Prove Complete Unique Ownership Of Every Equilibrium Block",
tags::preconditioning_type_contract
) {
STATIC_CHECK(preconditioning::PreconditionerComponent<DensityIdentity>);
STATIC_CHECK(preconditioning::PreconditionerPlanType<IdentityPlan>);
STATIC_CHECK_FALSE(preconditioning::PreconditionerPlanType<IncoherentPlanDeclaration>);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<IdentityPlan, Form>);
STATIC_CHECK(preconditioning::CompatiblePreconditionerFor<IdentityPlan, Form, JacobianForm>);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<CentralIdentityPlan, CentralForm>);
STATIC_CHECK(preconditioning::CompatiblePreconditionerFor<CentralIdentityPlan, CentralForm, CentralJacobianForm>);
STATIC_CHECK(preconditioning::StationaryLinearPreconditionerPlan<CentralIdentityPlan>);
}
TEST_CASE(
"Preconditioner Coverage Reports Missing Generated Borders And Rejects Accidental Overlap",
tags::preconditioning_type_contract
) {
using IncompleteCoverage = preconditioning::PreconditionerCoverage<CentralForm, IncompleteCentralPlan>;
using DuplicateCoverage = preconditioning::PreconditionerCoverage<CentralForm, DuplicateOwnershipPlan>;
STATIC_CHECK_FALSE(preconditioning::CompletePreconditionerFor<IncompleteCentralPlan, CentralForm>);
STATIC_CHECK(IncompleteCoverage::MissingCorrectionBlocks::size == 1);
STATIC_CHECK(IncompleteCoverage::MissingResidualBlocks::size == 1);
STATIC_CHECK(
blocks::contains_type_v<
blocks::fixed_central_density::central_value::value, IncompleteCoverage::MissingCorrectionBlocks>
);
STATIC_CHECK(
blocks::contains_type_v<
blocks::fixed_central_density::central_value::residual, IncompleteCoverage::MissingResidualBlocks>
);
STATIC_CHECK_FALSE(preconditioning::CompletePreconditionerFor<DuplicateOwnershipPlan, CentralForm>);
STATIC_CHECK(DuplicateCoverage::RepeatedCorrectionBlocks::size == 1);
STATIC_CHECK(DuplicateCoverage::RepeatedResidualBlocks::size == 1);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<ExplicitOverlapPlan, CentralForm>);
}
TEST_CASE(
"Preconditioner Coverage Rejects Blocks Outside The Compiled Stellar Form",
tags::preconditioning_type_contract
) {
using Coverage = preconditioning::PreconditionerCoverage<Form, UnexpectedOwnershipPlan>;
STATIC_CHECK_FALSE(preconditioning::CompletePreconditionerFor<UnexpectedOwnershipPlan, Form>);
STATIC_CHECK(Coverage::UnexpectedCorrectionBlocks::size == 1);
STATIC_CHECK(Coverage::UnexpectedResidualBlocks::size == 1);
STATIC_CHECK(blocks::contains_type_v<ExtraCorrection, Coverage::UnexpectedCorrectionBlocks>);
STATIC_CHECK(blocks::contains_type_v<ExtraResidual, Coverage::UnexpectedResidualBlocks>);
}
TEST_CASE(
"Preconditioner Component Dependencies Must Exist In The Compiled Jacobian Graph",
tags::preconditioning_type_contract
) {
STATIC_CHECK(preconditioning::PreconditionerComponent<CoupledGravity>);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<ValidCoupledPlan, Form>);
STATIC_CHECK(preconditioning::CompatiblePreconditionerFor<ValidCoupledPlan, Form, JacobianForm>);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<InvalidCoupledPlan, Form>);
STATIC_CHECK_FALSE(preconditioning::requiredCouplingsExist<InvalidCoupledPlan, JacobianForm>);
STATIC_CHECK_FALSE(preconditioning::CompatiblePreconditionerFor<InvalidCoupledPlan, Form, JacobianForm>);
}

View File

@@ -0,0 +1,519 @@
#include <algorithm>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <numbers>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace {
namespace backend = mean_field::preconditioning::backend;
namespace blocks = mean_field::utils::blocks;
namespace preconditioning = mean_field::preconditioning;
using BaseModel = mean_field::operators::StellarEquilibriumSpecificationModel;
using CentralModel = mean_field::operators::CentralDensityStellarEquilibriumSpecificationModel;
using ReorderedCentralModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::models::FixedCentralDensity,
mean_field::surface::Isobaric,
mean_field::models::FixedTotalMass,
mean_field::eos::Polytrope>>;
using BaseProblem = mean_field::equilibrium::StellarEquilibriumProblem<BaseModel>;
using CentralProblem = mean_field::equilibrium::StellarEquilibriumProblem<CentralModel>;
using BaseBorder = preconditioning::CompiledSpecificationBorderFor<BaseModel>;
using CentralBorder = preconditioning::CompiledSpecificationBorderFor<CentralModel>;
using BaseComponent = decltype(preconditioning::specificationBorderBlock(std::declval<const BaseProblem &>()));
using CentralComponent =
decltype(preconditioning::specificationBorderBlock(std::declval<const CentralProblem &>()));
using BasePlan = preconditioning::PreconditionerPlan<BaseComponent>;
using CentralPlan = preconditioning::PreconditionerPlan<CentralComponent>;
class KnownBorderCouplings final {
public:
explicit KnownBorderCouplings(const int borderSize)
: m_borderSize(borderSize),
m_structureToBorder(
borderSize,
StructureSize()
),
m_borderToStructure(
StructureSize(),
borderSize
),
m_borderDiagonal(borderSize) {
if (borderSize <= 0) {
throw std::invalid_argument("The known border must have positive size.");
}
for (int row = 0; row < borderSize; ++row) {
for (int column = 0; column < StructureSize(); ++column) {
m_structureToBorder(row, column) = 0.04 * static_cast<double>((row + 1) * (column + 2));
m_borderToStructure(column, row) = -0.03 * static_cast<double>((column + 1) * (row + 2));
}
for (int column = 0; column < borderSize; ++column) {
m_borderDiagonal(row, column) =
row == column ? 2.0 + static_cast<double>(row) : 0.01 * static_cast<double>(row + column + 1);
}
}
}
[[nodiscard]] static constexpr int StructureSize() noexcept {
return 3;
}
[[nodiscard]] int BorderSize() const noexcept {
return m_borderSize;
}
void ApplyStructureToBorder(
const mfem::Vector &direction,
mfem::Vector &action
) const {
m_structureToBorder.Mult(direction, action);
}
void ApplyBorderToStructure(
const mfem::Vector &direction,
mfem::Vector &action
) const {
m_borderToStructure.Mult(direction, action);
}
void ApplyBorderToBorder(
const mfem::Vector &direction,
mfem::Vector &action
) const {
m_borderDiagonal.Mult(direction, action);
}
void IncreaseBorderDiagonal(const double increment) {
for (int index = 0; index < m_borderSize; ++index) {
m_borderDiagonal(index, index) += increment;
}
}
[[nodiscard]] const mfem::DenseMatrix &StructureToBorder() const noexcept {
return m_structureToBorder;
}
[[nodiscard]] const mfem::DenseMatrix &BorderToStructure() const noexcept {
return m_borderToStructure;
}
[[nodiscard]] const mfem::DenseMatrix &BorderDiagonal() const noexcept {
return m_borderDiagonal;
}
private:
int m_borderSize;
mfem::DenseMatrix m_structureToBorder;
mfem::DenseMatrix m_borderToStructure;
mfem::DenseMatrix m_borderDiagonal;
};
[[nodiscard]] double relativeError(
const mfem::Vector &left,
const mfem::Vector &right
) {
mfem::Vector difference(left);
difference -= right;
return difference.Norml2() / std::max({1.0, left.Norml2(), right.Norml2()});
}
template <
preconditioning::ApplicationContract StructureInverseContract =
preconditioning::ApplicationContract::stationary_linear>
void verifyKnownBorderFactorization(const int borderSize) {
mfem::Vector structureDiagonal(KnownBorderCouplings::StructureSize());
structureDiagonal(0) = 2.0;
structureDiagonal(1) = 3.0;
structureDiagonal(2) = 5.0;
auto structureInverse = backend::prepare(backend::Diagonal{}, structureDiagonal);
KnownBorderCouplings couplings(borderSize);
using Factorization =
preconditioning::SpecificationBorderFactorizationOperator<KnownBorderCouplings, StructureInverseContract>;
Factorization factorization(structureInverse, couplings);
constexpr bool cachesStructureResponse = Factorization::cachesStructureInverseBorderCoupling;
const auto expectedSchurEntry = [&](const int row, const int column) {
double correction = 0.0;
for (int inner = 0; inner < KnownBorderCouplings::StructureSize(); ++inner) {
correction += couplings.StructureToBorder()(row, inner) * couplings.BorderToStructure()(inner, column) /
structureDiagonal(inner);
}
return couplings.BorderDiagonal()(row, column) - correction;
};
for (int row = 0; row < borderSize; ++row) {
for (int column = 0; column < borderSize; ++column) {
CHECK(
factorization.GetSchurComplement()(row, column) ==
Catch::Approx(expectedSchurEntry(row, column)).margin(2.0e-14)
);
}
}
const int completeSize = KnownBorderCouplings::StructureSize() + borderSize;
mfem::DenseMatrix completeMatrix(completeSize);
completeMatrix = 0.0;
for (int index = 0; index < KnownBorderCouplings::StructureSize(); ++index) {
completeMatrix(index, index) = structureDiagonal(index);
}
for (int row = 0; row < KnownBorderCouplings::StructureSize(); ++row) {
for (int column = 0; column < borderSize; ++column) {
completeMatrix(row, KnownBorderCouplings::StructureSize() + column) =
couplings.BorderToStructure()(row, column);
completeMatrix(KnownBorderCouplings::StructureSize() + column, row) =
couplings.StructureToBorder()(column, row);
}
}
for (int row = 0; row < borderSize; ++row) {
for (int column = 0; column < borderSize; ++column) {
completeMatrix(
KnownBorderCouplings::StructureSize() + row, KnownBorderCouplings::StructureSize() + column
) = couplings.BorderDiagonal()(row, column);
}
}
mfem::Vector rightHandSide(completeSize);
for (int index = 0; index < completeSize; ++index) {
rightHandSide(index) = 0.25 + 0.17 * static_cast<double>(index + 1);
}
mfem::Vector actual(completeSize);
mfem::Vector expected(completeSize);
factorization.Mult(rightHandSide, actual);
mfem::DenseMatrixInverse exactInverse(completeMatrix);
exactInverse.Mult(rightHandSide, expected);
CHECK(relativeError(actual, expected) <= 2.0e-13);
const auto statisticsBeforeRefresh = factorization.GetStatistics();
CHECK(statisticsBeforeRefresh.setups == 1);
CHECK(statisticsBeforeRefresh.schurProbes == static_cast<std::uint64_t>(borderSize));
CHECK(statisticsBeforeRefresh.applications == 1);
CHECK(
statisticsBeforeRefresh.structureInverseApplications ==
static_cast<std::uint64_t>(borderSize + (cachesStructureResponse ? 1 : 2))
);
CHECK(
statisticsBeforeRefresh.cachedStructureInverseBorderApplications ==
static_cast<std::uint64_t>(cachesStructureResponse ? 1 : 0)
);
CHECK(statisticsBeforeRefresh.structureToBorderApplications == static_cast<std::uint64_t>(borderSize + 1));
CHECK(
statisticsBeforeRefresh.borderToStructureApplications ==
static_cast<std::uint64_t>(borderSize + (cachesStructureResponse ? 0 : 1))
);
CHECK(statisticsBeforeRefresh.borderToBorderApplications == static_cast<std::uint64_t>(borderSize));
CHECK(
structureInverse.GetStatistics().applications ==
static_cast<std::uint64_t>(borderSize + (cachesStructureResponse ? 1 : 2))
);
for (int index = 0; index < KnownBorderCouplings::StructureSize(); ++index) {
structureDiagonal(index) += 0.25 * static_cast<double>(index + 1);
completeMatrix(index, index) = structureDiagonal(index);
}
structureInverse.Refresh(structureDiagonal);
couplings.IncreaseBorderDiagonal(0.5);
for (int index = 0; index < borderSize; ++index) {
completeMatrix(
KnownBorderCouplings::StructureSize() + index, KnownBorderCouplings::StructureSize() + index
) += 0.5;
}
factorization.RefreshSchurComplement();
CHECK(factorization.GetStatistics().setups == 2);
CHECK(factorization.GetStatistics().schurProbes == static_cast<std::uint64_t>(2 * borderSize));
CHECK(factorization.GetStatistics().borderToBorderApplications == static_cast<std::uint64_t>(2 * borderSize));
CHECK(
factorization.GetStatistics().structureInverseApplications ==
static_cast<std::uint64_t>(2 * borderSize + (cachesStructureResponse ? 1 : 2))
);
CHECK(
factorization.GetStatistics().cachedStructureInverseBorderApplications ==
static_cast<std::uint64_t>(cachesStructureResponse ? 1 : 0)
);
CHECK(
factorization.GetStatistics().structureToBorderApplications ==
static_cast<std::uint64_t>(2 * borderSize + 1)
);
CHECK(
factorization.GetStatistics().borderToStructureApplications ==
static_cast<std::uint64_t>(2 * borderSize + (cachesStructureResponse ? 0 : 1))
);
for (int row = 0; row < borderSize; ++row) {
for (int column = 0; column < borderSize; ++column) {
CHECK(
factorization.GetSchurComplement()(row, column) ==
Catch::Approx(expectedSchurEntry(row, column)).margin(2.0e-14)
);
}
}
mfem::Vector refreshedActual(completeSize);
mfem::Vector refreshedExpected(completeSize);
factorization.Mult(rightHandSide, refreshedActual);
mfem::DenseMatrixInverse refreshedExactInverse(completeMatrix);
refreshedExactInverse.Mult(rightHandSide, refreshedExpected);
CHECK(relativeError(refreshedActual, refreshedExpected) <= 2.0e-13);
const auto statisticsAfterRefreshApplication = factorization.GetStatistics();
CHECK(statisticsAfterRefreshApplication.applications == 2);
CHECK(
statisticsAfterRefreshApplication.structureInverseApplications ==
static_cast<std::uint64_t>(2 * borderSize + (cachesStructureResponse ? 2 : 4))
);
CHECK(
statisticsAfterRefreshApplication.cachedStructureInverseBorderApplications ==
static_cast<std::uint64_t>(cachesStructureResponse ? 2 : 0)
);
CHECK(
statisticsAfterRefreshApplication.structureToBorderApplications ==
static_cast<std::uint64_t>(2 * borderSize + 2)
);
CHECK(
statisticsAfterRefreshApplication.borderToStructureApplications ==
static_cast<std::uint64_t>(2 * borderSize + (cachesStructureResponse ? 0 : 2))
);
}
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies
makeDependencies(const std::uint64_t revision = 1) {
return {
.discretization = {.identity = 9201, .revision = 1},
.density = {.identity = 9203, .revision = revision},
.surfaceDeformation = {.identity = 9207, .revision = revision},
.gravityGradient = {.identity = 9211, .revision = revision},
.gravityPotential = {.identity = 9217, .revision = revision},
.enthalpy = {.identity = 9223, .revision = revision},
.bernoulliConstant = {.identity = 9229, .revision = revision},
.rotation = {.identity = 9231, .revision = revision},
.targetMass = {.identity = 9237, .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};
}
template <
typename View,
typename Term>
void assignStateBlock(
const View &view,
const Term &term,
const mfem::Vector &source,
mfem::Vector &state
) {
mfem::Vector destination = view.block(term);
REQUIRE(destination.Size() == source.Size());
destination = source;
destination.SyncAliasMemory(state);
}
} // namespace
TEST_CASE(
"Model Specifications Compile Complete Canonical Preconditioning Borders",
"[preconditioning][specification_border][unit][type_contract]"
) {
using ExpectedBaseCorrections = blocks::type_list<blocks::fixed_total_mass::mass_normalization::value>;
using ExpectedBaseResiduals = blocks::type_list<blocks::fixed_total_mass::mass_normalization::residual>;
using ExpectedCentralCorrections = blocks::type_list<
blocks::fixed_total_mass::mass_normalization::value, blocks::fixed_central_density::central_value::value>;
using ExpectedCentralResiduals = blocks::type_list<
blocks::fixed_total_mass::mass_normalization::residual, blocks::fixed_central_density::central_value::residual>;
STATIC_CHECK(std::same_as<CentralModel, ReorderedCentralModel>);
STATIC_CHECK(BaseBorder::valueArity == 1);
STATIC_CHECK(BaseBorder::residualArity == 1);
STATIC_CHECK(BaseBorder::specificationCount == 1);
STATIC_CHECK(std::same_as<typename BaseBorder::CorrectionBlocks, ExpectedBaseCorrections>);
STATIC_CHECK(std::same_as<typename BaseBorder::ResidualBlocks, ExpectedBaseResiduals>);
STATIC_CHECK(BaseBorder::RequiredCouplings::size == 3);
STATIC_CHECK(CentralBorder::valueArity == 2);
STATIC_CHECK(CentralBorder::residualArity == 2);
STATIC_CHECK(CentralBorder::specificationCount == 2);
STATIC_CHECK(std::same_as<typename CentralBorder::CorrectionBlocks, ExpectedCentralCorrections>);
STATIC_CHECK(std::same_as<typename CentralBorder::ResidualBlocks, ExpectedCentralResiduals>);
STATIC_CHECK(CentralBorder::RequiredCouplings::size == 5);
STATIC_CHECK(
preconditioning::specificationBorderValueOffset<mean_field::models::FixedTotalMass, CentralModel> == 0
);
STATIC_CHECK(
preconditioning::specificationBorderValueOffset<mean_field::models::FixedCentralDensity, CentralModel> == 1
);
STATIC_CHECK(
preconditioning::specificationBorderResidualOffset<mean_field::models::FixedTotalMass, CentralModel> == 0
);
STATIC_CHECK(
preconditioning::specificationBorderResidualOffset<mean_field::models::FixedCentralDensity, CentralModel> == 1
);
STATIC_CHECK(preconditioning::PreconditionerComponent<BaseComponent>);
STATIC_CHECK(preconditioning::PreconditionerComponent<CentralComponent>);
STATIC_CHECK(BaseComponent::RequiredCouplings::size == 19);
STATIC_CHECK(CentralComponent::RequiredCouplings::size == 21);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<BasePlan, typename BaseProblem::FormType>);
STATIC_CHECK(
preconditioning::CompatiblePreconditionerFor<
BasePlan, typename BaseProblem::FormType, typename BaseProblem::JacobianFormType>
);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<CentralPlan, typename CentralProblem::FormType>);
STATIC_CHECK(
preconditioning::CompatiblePreconditionerFor<
CentralPlan, typename CentralProblem::FormType, typename CentralProblem::JacobianFormType>
);
STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename CentralComponent::BackendType>);
}
TEST_CASE(
"Dense Specification Borders Cache Stationary Structure Responses And Reproduce Exact Block Factorizations",
"[preconditioning][specification_border][unit][factorization]"
) {
SECTION("one generated scalar") {
verifyKnownBorderFactorization(1);
}
SECTION("two generated scalars") {
verifyKnownBorderFactorization(2);
}
SECTION("four generated scalars") {
verifyKnownBorderFactorization(4);
}
}
TEST_CASE(
"Flexible Specification Borders Preserve Per-Application Structure Solves",
"[preconditioning][specification_border][unit][factorization]"
) {
verifyKnownBorderFactorization<preconditioning::ApplicationContract::flexible>(2);
}
TEST_CASE(
"Generated Specification Border Actions Match The Authoritative Stellar Jacobian",
"[preconditioning][specification_border][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);
const auto stellarModel = 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(stellarModel, finiteElements);
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 512}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
preconditioning::SpecificationBorderJacobianOperator coupling(problem);
REQUIRE(coupling.BorderSize() == 2);
REQUIRE(coupling.StructureSize() + coupling.BorderSize() == problem.StateSize());
const auto &offsets = coupling.GetStructureOffsets();
mfem::Vector groupedDirection(coupling.Width());
for (int index = 0; index < groupedDirection.Size(); ++index) {
groupedDirection(index) = 0.015 * std::sin(0.23 * static_cast<double>(index + 1));
}
const auto groupedBlock = [&](const int block) {
return mfem::Vector(groupedDirection.GetData() + offsets[block], offsets[block + 1] - offsets[block]);
};
mfem::Vector structureOnlyRoot(problem.StateSize());
structureOnlyRoot = 0.0;
const auto structureView = problem.GetManifest().directionView(structureOnlyRoot);
assignStateBlock(structureView, blocks::density_field.mass_term, groupedBlock(0), structureOnlyRoot);
assignStateBlock(
structureView, blocks::surface_deformation_field.parameters_term, groupedBlock(1), structureOnlyRoot
);
assignStateBlock(structureView, blocks::enthalpy_field.specific_term, groupedBlock(2), structureOnlyRoot);
assignStateBlock(structureView, blocks::gravity_field.gradient_term, groupedBlock(3), structureOnlyRoot);
assignStateBlock(structureView, blocks::gravity_field.poisson_term, groupedBlock(4), structureOnlyRoot);
mfem::Vector borderOnlyRoot(problem.StateSize());
borderOnlyRoot = 0.0;
const auto borderView = problem.GetManifest().directionView(borderOnlyRoot);
mfem::Vector massDirection(groupedDirection.GetData() + coupling.StructureSize(), 1);
mfem::Vector centralDirection(groupedDirection.GetData() + coupling.StructureSize() + 1, 1);
assignStateBlock(
borderView, blocks::fixed_total_mass_constraint.mass_normalization_term, massDirection, borderOnlyRoot
);
assignStateBlock(
borderView, blocks::fixed_central_density_phase.central_value_term, centralDirection, borderOnlyRoot
);
mfem::Vector structureOnlyAction;
mfem::Vector borderOnlyAction;
problem.ApplyLinearization(structureOnlyRoot, structureOnlyAction);
problem.ApplyLinearization(borderOnlyRoot, borderOnlyAction);
auto structureOnlyResidual = problem.GetManifest().residualView(structureOnlyAction);
auto borderOnlyResidual = problem.GetManifest().residualView(borderOnlyAction);
mfem::Vector expected(coupling.Height());
expected = 0.0;
expected.SetVector(borderOnlyResidual.block(blocks::density_field.mass_term), offsets[0]);
expected.SetVector(borderOnlyResidual.block(blocks::surface_deformation_field.shape_equilibrium_term), offsets[1]);
expected.SetVector(borderOnlyResidual.block(blocks::enthalpy_field.specific_term), offsets[2]);
expected.SetVector(borderOnlyResidual.block(blocks::gravity_field.gradient_term), offsets[3]);
expected.SetVector(borderOnlyResidual.block(blocks::gravity_field.poisson_term), offsets[4]);
expected.SetVector(
structureOnlyResidual.block(blocks::fixed_total_mass_constraint.mass_normalization_term),
coupling.StructureSize()
);
expected.SetVector(
structureOnlyResidual.block(blocks::fixed_central_density_phase.central_value_term),
coupling.StructureSize() + 1
);
mfem::Vector borderDiagonal(2);
borderDiagonal(0) = borderOnlyResidual.block(blocks::fixed_total_mass_constraint.mass_normalization_term)(0);
borderDiagonal(1) = borderOnlyResidual.block(blocks::fixed_central_density_phase.central_value_term)(0);
mfem::Vector expectedBorder(expected, coupling.StructureSize(), coupling.BorderSize());
expectedBorder += borderDiagonal;
expectedBorder.SyncAliasMemory(expected);
mfem::Vector actual(coupling.Height());
coupling.Mult(groupedDirection, actual);
CHECK(relativeError(actual, expected) <= 2.0e-12);
auto component = preconditioning::makePreconditioner(problem);
using Component = decltype(component);
STATIC_CHECK(std::same_as<Component, CentralComponent>);
auto prepared = preconditioning::prepare(problem, component);
using GroupedPreconditioner = typename decltype(prepared)::GroupedPreconditioner;
using PreparedFactorization = typename GroupedPreconditioner::Factorization;
STATIC_CHECK(PreparedFactorization::cachesStructureInverseBorderCoupling);
mfem::Vector rightHandSide(prepared.Width());
for (int index = 0; index < rightHandSide.Size(); ++index) {
rightHandSide(index) = std::cos(0.11 * static_cast<double>(index + 1));
}
mfem::Vector correction(prepared.Height());
prepared.Mult(rightHandSide, correction);
for (int index = 0; index < correction.Size(); ++index) {
REQUIRE(std::isfinite(correction(index)));
}
const auto &factorizationStatistics = prepared.GetGroupedPreconditioner().GetFactorization().GetStatistics();
CHECK(factorizationStatistics.setups == 1);
CHECK(factorizationStatistics.schurProbes == 2);
CHECK(factorizationStatistics.applications == 1);
CHECK(factorizationStatistics.structureInverseApplications == 3);
CHECK(factorizationStatistics.cachedStructureInverseBorderApplications == 1);
CHECK(factorizationStatistics.borderToStructureApplications == 2);
const auto unchanged = prepared.Refresh();
CHECK_FALSE(unchanged.DidAnyWork());
CHECK(prepared.IsCurrent());
}

View File

@@ -0,0 +1,336 @@
#include <array>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace preconditioning_runtime_test {
namespace blocks = mean_field::utils::blocks;
using Form = blocks::surface_deformed_stellar_equilibrium_form;
using JacobianForm = blocks::surface_deformed_stellar_equilibrium_jacobian_form;
using Layout = blocks::form_layout<Form>;
class Manifest final {
public:
Manifest()
: m_layout(
std::array<
int,
Form::value_block_count>{
2,
3,
4,
5,
6,
1
},
std::array<
int,
Form::residual_block_count>{
4,
5,
2,
3,
6,
1
}
) {
}
[[nodiscard]] const Layout &layout() const noexcept {
return m_layout;
}
private:
Layout m_layout;
};
class Problem final {
public:
Problem() : m_linearization(m_manifest.layout().value_offsets().Last()) {
m_snapshot.discretization = {.identity = 11, .revision = 1};
m_snapshot.geometry = {.identity = 12, .revision = 1};
m_snapshot.equationOfStateIdentity = &m_equationOfStateToken;
m_snapshot.linearization.discretization = m_snapshot.discretization;
m_snapshot.linearization.density = {.identity = 21, .revision = 1};
}
void AdvanceDensity() noexcept {
++m_snapshot.linearization.density.revision;
}
void AdvanceGeometry() noexcept {
++m_snapshot.geometry.revision;
}
void SetPrepared(const bool prepared) noexcept {
m_prepared = prepared;
}
[[nodiscard]] bool IsPrepared() const noexcept {
return m_prepared;
}
[[nodiscard]] int StateSize() const noexcept {
return m_manifest.layout().value_offsets().Last();
}
[[nodiscard]] int EquationSize() const noexcept {
return m_manifest.layout().residual_offsets().Last();
}
[[nodiscard]] const Manifest &GetManifest() const noexcept {
return m_manifest;
}
[[nodiscard]] const mfem::Operator &GetLinearizationOperator() const noexcept {
return m_linearization;
}
[[nodiscard]] mean_field::preconditioning::StellarPreconditionerLifecycleSnapshot Snapshot() const {
return m_snapshot;
}
private:
Manifest m_manifest;
mfem::IdentityOperator m_linearization;
std::uint8_t m_equationOfStateToken{0};
mean_field::preconditioning::StellarPreconditionerLifecycleSnapshot m_snapshot;
bool m_prepared{true};
};
} // namespace preconditioning_runtime_test
template <> struct mean_field::preconditioning::StellarEquilibriumProblemTraits<preconditioning_runtime_test::Problem> {
using Problem = preconditioning_runtime_test::Problem;
using Form = preconditioning_runtime_test::Form;
using JacobianForm = preconditioning_runtime_test::JacobianForm;
using Manifest = preconditioning_runtime_test::Manifest;
static constexpr bool registered = true;
[[nodiscard]] static bool IsPrepared(const Problem &problem) noexcept {
return problem.IsPrepared();
}
[[nodiscard]] static int StateSize(const Problem &problem) noexcept {
return problem.StateSize();
}
[[nodiscard]] static int EquationSize(const Problem &problem) noexcept {
return problem.EquationSize();
}
[[nodiscard]] static const Manifest &ManifestOf(const Problem &problem) noexcept {
return problem.GetManifest();
}
[[nodiscard]] static const mfem::Operator &LinearizationOperator(const Problem &problem) noexcept {
return problem.GetLinearizationOperator();
}
[[nodiscard]] static mean_field::preconditioning::StellarPreconditionerLifecycleSnapshot
Snapshot(const Problem &problem) {
return problem.Snapshot();
}
};
namespace {
namespace blocks = mean_field::utils::blocks;
namespace preconditioning = mean_field::preconditioning;
using ModelWithoutPhase = mean_field::operators::StellarEquilibriumSpecificationModel;
using CentralDensityModel = mean_field::operators::CentralDensityStellarEquilibriumSpecificationModel;
using ProblemWithoutPhase = mean_field::equilibrium::StellarEquilibriumProblem<ModelWithoutPhase>;
using CentralDensityProblem = mean_field::equilibrium::StellarEquilibriumProblem<CentralDensityModel>;
using PlanWithoutPhase = preconditioning::IdentityPreconditionerPlanFor<ProblemWithoutPhase>;
using CentralDensityPlan = preconditioning::IdentityPreconditionerPlanFor<CentralDensityProblem>;
using RefreshingDensityIdentity = preconditioning::ComponentDeclaration<
blocks::type_list<blocks::density::mass::value>,
blocks::type_list<blocks::density::mass::residual>,
blocks::type_list<>,
preconditioning::IdentityOperatorCharacteristics,
preconditioning::backend::Identity,
preconditioning::PreparationDependencies<preconditioning::PreparationDependency::linearization>>;
using SurfaceIdentity = preconditioning::IdentityBlock<
blocks::surface_deformation::parameters::value,
blocks::surface_deformation::shape_equilibrium::residual>;
using GravityGradientIdentity =
preconditioning::IdentityBlock<blocks::gravity::gradient::value, blocks::gravity::gradient::residual>;
using GravityPotentialIdentity =
preconditioning::IdentityBlock<blocks::gravity::poisson::value, blocks::gravity::poisson::residual>;
using EnthalpyIdentity =
preconditioning::IdentityBlock<blocks::enthalpy::specific::value, blocks::enthalpy::specific::residual>;
using FixedMassIdentity = preconditioning::IdentityBlock<
blocks::fixed_total_mass::mass_normalization::value,
blocks::fixed_total_mass::mass_normalization::residual>;
using SelectiveRefreshPlan = preconditioning::PreconditionerPlan<
RefreshingDensityIdentity,
SurfaceIdentity,
GravityGradientIdentity,
GravityPotentialIdentity,
EnthalpyIdentity,
FixedMassIdentity>;
[[nodiscard]] constexpr SelectiveRefreshPlan makeSelectiveRefreshPlan() {
return SelectiveRefreshPlan{RefreshingDensityIdentity{}, SurfaceIdentity{}, GravityGradientIdentity{},
GravityPotentialIdentity{}, EnthalpyIdentity{}, FixedMassIdentity{}};
}
} // namespace
TEST_CASE(
"Stellar Identity Plans Follow The Compiled Equilibrium Problem Type",
tags::preconditioning_runtime_unit
) {
STATIC_CHECK(mean_field::equilibrium::DiscretizedStellarEquilibriumProblem<ProblemWithoutPhase>);
STATIC_CHECK(mean_field::equilibrium::DiscretizedStellarEquilibriumProblem<CentralDensityProblem>);
STATIC_CHECK(preconditioning::StellarPreconditionerProblem<ProblemWithoutPhase>);
STATIC_CHECK(preconditioning::StellarPreconditionerProblem<CentralDensityProblem>);
STATIC_CHECK(preconditioning::CompletePreconditionerFor<PlanWithoutPhase, typename ProblemWithoutPhase::FormType>);
STATIC_CHECK(
preconditioning::CompletePreconditionerFor<CentralDensityPlan, typename CentralDensityProblem::FormType>
);
STATIC_CHECK(PlanWithoutPhase::ComponentTypes::size == 6);
STATIC_CHECK(CentralDensityPlan::ComponentTypes::size == 7);
}
TEST_CASE(
"Prepared Stellar Identity Preconditioning Is Bitwise Equivalent To The P0 Baseline",
tags::preconditioning_runtime_unit
) {
preconditioning_runtime_test::Problem problem;
auto plan = preconditioning::makeIdentityPlan(problem);
using Plan = decltype(plan);
using Problem = preconditioning_runtime_test::Problem;
auto preconditioner = preconditioning::prepare(problem, std::move(plan));
STATIC_CHECK(preconditioning::PreparedPreconditionerPlanFor<Plan, Problem>);
CHECK(preconditioner.Height() == problem.StateSize());
CHECK(preconditioner.Width() == problem.EquationSize());
CHECK(preconditioner.IsCurrent());
CHECK(&preconditioner.GetLinearizationOperator() == &problem.GetLinearizationOperator());
preconditioner.SetOperator(problem.GetLinearizationOperator());
mfem::Vector residual(problem.EquationSize());
mfem::Vector correction(problem.StateSize());
for (int index = 0; index < residual.Size(); ++index) {
residual(index) = static_cast<double>(index) - 10.25;
}
correction = -1.0;
const mfem::real_t *const correctionStorage = correction.GetData();
const auto statisticsBefore = preconditioner.GetStatistics();
preconditioner.Mult(residual, correction);
const auto statisticsAfter = preconditioner.GetStatistics();
CHECK(correction.GetData() == correctionStorage);
CHECK(std::memcmp(correction.GetData(), residual.GetData(), sizeof(mfem::real_t) * residual.Size()) == 0);
const mfem::Vector densityCorrection = preconditioner.GetCorrectionBlock<blocks::density::mass::value>(correction);
const mfem::Vector densityResidual = preconditioner.GetResidualBlock<blocks::density::mass::residual>(residual);
CHECK(densityCorrection.Size() == 2);
CHECK(densityCorrection.GetData() == correction.GetData());
CHECK(densityResidual.Size() == 2);
CHECK(densityResidual.GetData() == residual.GetData() + 9);
CHECK(statisticsAfter.setups == statisticsBefore.setups);
CHECK(statisticsAfter.refreshes == statisticsBefore.refreshes);
CHECK(statisticsAfter.componentSetups == 6);
CHECK(statisticsAfter.applications == statisticsBefore.applications + 1);
CHECK(statisticsAfter.backendApplications == statisticsBefore.backendApplications + 1);
CHECK(statisticsAfter.innerIterations == 0);
CHECK(statisticsAfter.operatorBindings == 1);
}
TEST_CASE(
"Stellar Preconditioner Refresh Is Explicit And Dependency Aware",
tags::preconditioning_runtime_unit
) {
preconditioning_runtime_test::Problem problem;
auto preconditioner = preconditioning::prepare(problem, preconditioning::makeIdentityPlan(problem));
mfem::Vector residual(problem.EquationSize());
mfem::Vector correction(problem.StateSize());
residual = 1.0;
correction = 0.0;
const auto noChange = preconditioner.Refresh();
CHECK_FALSE(noChange.changes.Any());
CHECK_FALSE(noChange.DidAnyWork());
CHECK(preconditioner.GetStatistics().noOpRefreshes == 1);
problem.AdvanceDensity();
CHECK_FALSE(preconditioner.IsCurrent());
CHECK_THROWS_AS(preconditioner.Mult(residual, correction), std::logic_error);
const auto linearizationRefresh = preconditioner.Refresh();
CHECK(linearizationRefresh.changes.linearization);
CHECK_FALSE(linearizationRefresh.changes.discretization);
CHECK_FALSE(linearizationRefresh.changes.geometry);
CHECK_FALSE(linearizationRefresh.DidAnyWork());
CHECK(preconditioner.IsCurrent());
CHECK(preconditioner.GetStatistics().refreshes == 1);
CHECK(preconditioner.GetStatistics().componentRefreshes == 0);
problem.AdvanceGeometry();
const auto geometryRefresh = preconditioner.Refresh();
CHECK(geometryRefresh.changes.geometry);
CHECK_FALSE(geometryRefresh.changes.linearization);
CHECK(preconditioner.GetStatistics().refreshes == 2);
problem.SetPrepared(false);
CHECK_FALSE(preconditioner.IsCurrent());
CHECK_THROWS_AS(preconditioner.Refresh(), std::logic_error);
}
TEST_CASE(
"Stellar Preconditioner Refresh Touches Only Components With Changed Dependencies",
tags::preconditioning_runtime_unit
) {
preconditioning_runtime_test::Problem problem;
auto preconditioner = preconditioning::prepare(problem, makeSelectiveRefreshPlan());
problem.AdvanceGeometry();
const auto geometryRefresh = preconditioner.Refresh();
CHECK(geometryRefresh.changes.geometry);
CHECK_FALSE(geometryRefresh.changes.linearization);
CHECK_FALSE(geometryRefresh.DidAnyWork());
CHECK(geometryRefresh.refreshedComponents == 0);
problem.AdvanceDensity();
const auto linearizationRefresh = preconditioner.Refresh();
CHECK_FALSE(linearizationRefresh.changes.geometry);
CHECK(linearizationRefresh.changes.linearization);
CHECK(linearizationRefresh.DidAnyWork());
CHECK(linearizationRefresh.refreshedComponents == 1);
CHECK(preconditioner.GetStatistics().componentRefreshes == 1);
}
TEST_CASE(
"Stellar Preconditioner Application Requires Preallocated Compatible Vectors",
tags::preconditioning_runtime_unit
) {
preconditioning_runtime_test::Problem problem;
auto preconditioner = preconditioning::prepare(problem, preconditioning::makeIdentityPlan(problem));
mfem::Vector residual(problem.EquationSize());
mfem::Vector missingCorrection;
mfem::IdentityOperator wrongOperator(problem.StateSize() - 1);
CHECK_THROWS_AS(preconditioner.Mult(residual, missingCorrection), std::invalid_argument);
CHECK_THROWS_AS(preconditioner.SetOperator(wrongOperator), std::invalid_argument);
preconditioning_runtime_test::Problem unpreparedProblem;
unpreparedProblem.SetPrepared(false);
CHECK_THROWS_AS(
preconditioning::prepare(unpreparedProblem, preconditioning::makeIdentityPlan(unpreparedProblem)),
std::logic_error
);
}

View File

@@ -0,0 +1,419 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <concepts>
#include <cstdint>
#include <numbers>
#include <type_traits>
#include <utility>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
import mean_field;
import test_helpers;
namespace {
namespace backend = mean_field::preconditioning::backend;
namespace blocks = mean_field::utils::blocks;
namespace preconditioning = mean_field::preconditioning;
struct MaterialValue final : blocks::value_block_base { };
struct GravityValue final : blocks::value_block_base { };
struct MaterialResidual final : blocks::residual_block_base { };
struct GravityResidual final : blocks::residual_block_base { };
using MockForm = blocks::block_form<
blocks::type_list<MaterialValue, GravityValue>,
blocks::type_list<MaterialResidual, GravityResidual>>;
using MockJacobian = blocks::type_list<
blocks::block_row<MaterialResidual, MaterialValue, GravityValue>,
blocks::block_row<GravityResidual, MaterialValue, GravityValue>>;
using MockMaterialComponent = preconditioning::ComponentDeclaration<
blocks::type_list<MaterialValue>,
blocks::type_list<MaterialResidual>,
blocks::type_list<preconditioning::Coupling<MaterialResidual, MaterialValue>>,
preconditioning::IdentityOperatorCharacteristics,
backend::Identity,
preconditioning::NoPreparationDependencies>;
using MockGravityComponent = preconditioning::ComponentDeclaration<
blocks::type_list<GravityValue>,
blocks::type_list<GravityResidual>,
blocks::type_list<preconditioning::Coupling<GravityResidual, GravityValue>>,
preconditioning::IdentityOperatorCharacteristics,
backend::Identity,
preconditioning::NoPreparationDependencies>;
using MockStructure = preconditioning::StellarStructureBlock<
MockMaterialComponent,
MockGravityComponent,
MockForm,
MockJacobian,
preconditioning::ApproximateStellarBlockLDU>;
template <typename MaterialComponent, typename GravityComponent>
concept MockComponentsCanCompose = requires {
typename preconditioning::StellarStructureBlock<
MaterialComponent, GravityComponent, MockForm, MockJacobian, preconditioning::IndependentStellarSubsystems>;
};
class KnownCrossCouplings final {
public:
[[nodiscard]] constexpr int MaterialSize() const noexcept {
return 1;
}
[[nodiscard]] constexpr int GravitySize() const noexcept {
return 1;
}
void ApplyMaterialToGravity(
const mfem::Vector &materialDirection,
mfem::Vector &gravityAction
) const {
gravityAction(0) = 3.0 * materialDirection(0);
}
void ApplyGravityToMaterial(
const mfem::Vector &gravityDirection,
mfem::Vector &materialAction
) const {
materialAction(0) = 7.0 * gravityDirection(0);
}
};
template <typename Policy>
[[nodiscard]] mfem::Vector applyKnownFactorization(
Policy policy,
preconditioning::StellarStructureFactorizationStatistics *statistics = nullptr
) {
mfem::Vector materialDiagonal(1);
mfem::Vector gravityDiagonal(1);
materialDiagonal(0) = 2.0;
gravityDiagonal(0) = 5.0;
auto materialInverse = backend::prepare(backend::Diagonal{}, materialDiagonal);
auto gravityInverse = backend::prepare(backend::Diagonal{}, gravityDiagonal);
const KnownCrossCouplings couplings;
preconditioning::StellarStructureFactorizationOperator<Policy, KnownCrossCouplings> factorization(
policy, materialInverse, gravityInverse, couplings
);
mfem::Vector rightHandSide(2);
mfem::Vector action(2);
rightHandSide(0) = 11.0;
rightHandSide(1) = 13.0;
factorization.Mult(rightHandSide, action);
if (statistics != nullptr) {
*statistics = factorization.GetStatistics();
}
return action;
}
using PolytropicModel = mean_field::model::StellarModel<mean_field::models::SpecificationSet<
mean_field::eos::Polytrope,
mean_field::surface::Isobaric,
mean_field::integral::FixedTotalMass,
mean_field::constraint::FixedCentralDensity>>;
using PolytropicProblem = mean_field::equilibrium::StellarEquilibriumProblem<PolytropicModel>;
using MaterialComponent =
decltype(preconditioning::materialSurfaceBlock(std::declval<const PolytropicProblem &>()));
using FixedAMG = backend::HypreBoomerAMG<backend::FixedCycles>;
using GravityComponent =
preconditioning::GravityFieldBlock<backend::Diagonal, FixedAMG, preconditioning::GravityApproximateLDU>;
using PolytropicStructure = decltype(preconditioning::stellarStructureBlock(
std::declval<const PolytropicProblem &>(),
std::declval<MaterialComponent>(),
std::declval<GravityComponent>(),
preconditioning::IndependentStellarSubsystems{}
));
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies
makeDependencies(const std::uint64_t revision = 1) {
return {
.discretization = {.identity = 9101, .revision = 1},
.density = {.identity = 9103, .revision = revision},
.surfaceDeformation = {.identity = 9107, .revision = revision},
.gravityGradient = {.identity = 9111, .revision = revision},
.gravityPotential = {.identity = 9117, .revision = revision},
.enthalpy = {.identity = 9123, .revision = revision},
.bernoulliConstant = {.identity = 9129, .revision = revision},
.rotation = {.identity = 9131, .revision = revision},
.targetMass = {.identity = 9137, .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 &left,
const mfem::Vector &right
) {
mfem::Vector difference(left);
difference -= right;
return difference.Norml2() / std::max({1.0, left.Norml2(), right.Norml2()});
}
} // namespace
TEST_CASE(
"Stellar Structure Composition Derives Both Cross-Subsystem Graphs",
"[preconditioning][stellar_structure][unit][type_contract]"
) {
using ExpectedMaterialToGravity = blocks::type_list<
preconditioning::Coupling<blocks::gravity::gradient::residual, blocks::surface_deformation::parameters::value>,
preconditioning::Coupling<blocks::gravity::poisson::residual, blocks::density::mass::value>,
preconditioning::Coupling<blocks::gravity::poisson::residual, blocks::surface_deformation::parameters::value>>;
using ExpectedGravityToMaterial = blocks::type_list<
preconditioning::Coupling<
blocks::surface_deformation::shape_equilibrium::residual, blocks::gravity::gradient::value>,
preconditioning::Coupling<blocks::enthalpy::specific::residual, blocks::gravity::poisson::value>>;
STATIC_CHECK(preconditioning::PreconditionerComponent<PolytropicStructure>);
STATIC_CHECK(std::same_as<typename PolytropicStructure::MaterialToGravityCouplings, ExpectedMaterialToGravity>);
STATIC_CHECK(std::same_as<typename PolytropicStructure::GravityToMaterialCouplings, ExpectedGravityToMaterial>);
STATIC_CHECK(PolytropicStructure::MaterialToGravityCouplings::size == 3);
STATIC_CHECK(PolytropicStructure::GravityToMaterialCouplings::size == 2);
STATIC_CHECK(PolytropicStructure::RequiredCouplings::size == 16);
STATIC_CHECK(preconditioning::backend::ArnoldiAdmissible<typename PolytropicStructure::BackendType>);
using FixedMassIdentity = preconditioning::IdentityBlock<
blocks::fixed_total_mass::mass_normalization::value, blocks::fixed_total_mass::mass_normalization::residual>;
using FixedCentralDensityIdentity = preconditioning::IdentityBlock<
blocks::fixed_central_density::central_value::value, blocks::fixed_central_density::central_value::residual>;
using CompletePlan =
preconditioning::PreconditionerPlan<PolytropicStructure, FixedMassIdentity, FixedCentralDensityIdentity>;
STATIC_CHECK(preconditioning::CompletePreconditionerFor<CompletePlan, typename PolytropicProblem::FormType>);
STATIC_CHECK(
preconditioning::CompatiblePreconditionerFor<
CompletePlan, typename PolytropicProblem::FormType, typename PolytropicProblem::JacobianFormType>
);
STATIC_CHECK(MockComponentsCanCompose<MockMaterialComponent, MockGravityComponent>);
STATIC_CHECK_FALSE(MockComponentsCanCompose<MockMaterialComponent, MockMaterialComponent>);
STATIC_CHECK(preconditioning::PreconditionerComponent<MockStructure>);
STATIC_CHECK(MockStructure::MaterialToGravityCouplings::size == 1);
STATIC_CHECK(MockStructure::GravityToMaterialCouplings::size == 1);
}
TEST_CASE(
"Stellar Structure Factorizations Preserve Independent Triangular And Approximate LDU Algebra",
"[preconditioning][stellar_structure][unit][factorization]"
) {
const auto check = [](const mfem::Vector &value, const std::array<double, 2> expected) {
REQUIRE(value.Size() == 2);
CHECK(value(0) == Catch::Approx(expected[0]).margin(2.0e-14));
CHECK(value(1) == Catch::Approx(expected[1]).margin(2.0e-14));
mfem::Vector expectedVector(2);
expectedVector(0) = expected[0];
expectedVector(1) = expected[1];
CHECK(relativeError(value, expectedVector) <= 2.0e-14);
};
check(applyKnownFactorization(preconditioning::IndependentStellarSubsystems{}), {5.5, 2.6});
check(applyKnownFactorization(preconditioning::MaterialThenGravityTriangular{}), {5.5, -0.7});
check(applyKnownFactorization(preconditioning::GravityThenMaterialTriangular{}), {-3.6, 2.6});
preconditioning::StellarStructureFactorizationStatistics statistics;
check(applyKnownFactorization(preconditioning::ApproximateStellarBlockLDU{}, &statistics), {7.95, -0.7});
CHECK(statistics.applications == 1);
CHECK(statistics.materialSurfaceInverseApplications == 2);
CHECK(statistics.gravityInverseApplications == 1);
CHECK(statistics.materialToGravityApplications == 1);
CHECK(statistics.gravityToMaterialApplications == 1);
}
TEST_CASE(
"Stellar Structure Cross Actions Are Exact Restricted Jacobian Actions And Compose Prepared Blocks",
"[preconditioning][stellar_structure][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);
const auto stellarModel = 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(stellarModel, finiteElements);
auto projected = seed::makeProjectedEquilibriumState(problem, seed::LaneEmden({.radialSampleCount = 512}));
problem.Prepare(projected.values, makeDependencies(), zeroRotation());
const auto &physical = problem.GetPreparedOperator().GetPhysicalOperator();
preconditioning::StellarStructureCrossJacobianOperator cross(physical);
mfem::Vector direction(cross.Width());
for (int index = 0; index < direction.Size(); ++index) {
direction(index) = 0.01 * std::sin(0.29 * static_cast<double>(index + 1));
}
mfem::Vector crossAction(cross.Height());
cross.Mult(direction, crossAction);
const mfem::Vector materialDirection(direction.GetData(), cross.MaterialSize());
const mfem::Vector gravityDirection(direction.GetData() + cross.MaterialSize(), cross.GravitySize());
const auto &materialOffsets = cross.GetMaterialOffsets();
const auto &gravityOffsets = cross.GetGravityOffsets();
mfem::Vector materialOnlyDirection(physical.Width());
materialOnlyDirection = 0.0;
auto materialOnlyView = physical.GetRootManifest().directionView(materialOnlyDirection);
mfem::Vector materialDensity = materialOnlyView.block(blocks::density_field.mass_term);
mfem::Vector materialSurface = materialOnlyView.block(blocks::surface_deformation_field.parameters_term);
mfem::Vector materialEnthalpy = materialOnlyView.block(blocks::enthalpy_field.specific_term);
const mfem::Vector sourceDensity(
const_cast<mfem::real_t *>(materialDirection.GetData()) + materialOffsets[0],
materialOffsets[1] - materialOffsets[0]
);
const mfem::Vector sourceSurface(
const_cast<mfem::real_t *>(materialDirection.GetData()) + materialOffsets[1],
materialOffsets[2] - materialOffsets[1]
);
const mfem::Vector sourceEnthalpy(
const_cast<mfem::real_t *>(materialDirection.GetData()) + materialOffsets[2],
materialOffsets[3] - materialOffsets[2]
);
materialDensity = sourceDensity;
materialSurface = sourceSurface;
materialEnthalpy = sourceEnthalpy;
mfem::Vector materialOnlyAction;
physical.Mult(materialOnlyDirection, materialOnlyAction);
const auto materialOnlyActionView = physical.GetRootManifest().residualView(materialOnlyAction);
mfem::Vector gravityOnlyDirection(physical.Width());
gravityOnlyDirection = 0.0;
auto gravityOnlyView = physical.GetRootManifest().directionView(gravityOnlyDirection);
mfem::Vector gravityGradient = gravityOnlyView.block(blocks::gravity_field.gradient_term);
mfem::Vector gravityPotential = gravityOnlyView.block(blocks::gravity_field.poisson_term);
const mfem::Vector sourceGravityGradient(
const_cast<mfem::real_t *>(gravityDirection.GetData()) + gravityOffsets[0],
gravityOffsets[1] - gravityOffsets[0]
);
const mfem::Vector sourceGravityPotential(
const_cast<mfem::real_t *>(gravityDirection.GetData()) + gravityOffsets[1],
gravityOffsets[2] - gravityOffsets[1]
);
gravityGradient = sourceGravityGradient;
gravityPotential = sourceGravityPotential;
mfem::Vector gravityOnlyAction;
physical.Mult(gravityOnlyDirection, gravityOnlyAction);
const auto gravityOnlyActionView = physical.GetRootManifest().residualView(gravityOnlyAction);
mfem::Vector expected(cross.Height());
expected = 0.0;
expected.SetVector(gravityOnlyActionView.block(blocks::density_field.mass_term), materialOffsets[0]);
expected.SetVector(
gravityOnlyActionView.block(blocks::surface_deformation_field.shape_equilibrium_term), materialOffsets[1]
);
expected.SetVector(gravityOnlyActionView.block(blocks::enthalpy_field.specific_term), materialOffsets[2]);
expected.SetVector(
materialOnlyActionView.block(blocks::gravity_field.gradient_term), cross.MaterialSize() + gravityOffsets[0]
);
expected.SetVector(
materialOnlyActionView.block(blocks::gravity_field.poisson_term), cross.MaterialSize() + gravityOffsets[1]
);
const mfem::Vector expectedMaterial(expected.GetData(), cross.MaterialSize());
const mfem::Vector expectedGravity(expected.GetData() + cross.MaterialSize(), cross.GravitySize());
const mfem::Vector expectedDensity(
expectedMaterial.GetData() + materialOffsets[0], materialOffsets[1] - materialOffsets[0]
);
const mfem::Vector expectedSurface(
expectedMaterial.GetData() + materialOffsets[1], materialOffsets[2] - materialOffsets[1]
);
const mfem::Vector expectedEnthalpy(
expectedMaterial.GetData() + materialOffsets[2], materialOffsets[3] - materialOffsets[2]
);
const mfem::Vector expectedGravityGradient(
expectedGravity.GetData() + gravityOffsets[0], gravityOffsets[1] - gravityOffsets[0]
);
const mfem::Vector expectedGravityPotential(
expectedGravity.GetData() + gravityOffsets[1], gravityOffsets[2] - gravityOffsets[1]
);
const mfem::Vector crossMaterial(crossAction.GetData(), cross.MaterialSize());
const mfem::Vector crossGravity(crossAction.GetData() + cross.MaterialSize(), cross.GravitySize());
const mfem::Vector crossDensity(
crossMaterial.GetData() + materialOffsets[0], materialOffsets[1] - materialOffsets[0]
);
const mfem::Vector crossSurface(
crossMaterial.GetData() + materialOffsets[1], materialOffsets[2] - materialOffsets[1]
);
const mfem::Vector crossEnthalpy(
crossMaterial.GetData() + materialOffsets[2], materialOffsets[3] - materialOffsets[2]
);
const mfem::Vector crossGravityGradient(
crossGravity.GetData() + gravityOffsets[0], gravityOffsets[1] - gravityOffsets[0]
);
const mfem::Vector crossGravityPotential(
crossGravity.GetData() + gravityOffsets[1], gravityOffsets[2] - gravityOffsets[1]
);
INFO(
"gravity-to-material density-row error = " << relativeError(crossDensity, expectedDensity)
<< ", actual norm = " << crossDensity.Norml2()
<< ", expected norm = " << expectedDensity.Norml2()
);
INFO(
"gravity-to-material surface-row error = " << relativeError(crossSurface, expectedSurface)
<< ", actual norm = " << crossSurface.Norml2()
<< ", expected norm = " << expectedSurface.Norml2()
);
INFO(
"gravity-to-material enthalpy-row error = " << relativeError(crossEnthalpy, expectedEnthalpy)
<< ", actual norm = " << crossEnthalpy.Norml2()
<< ", expected norm = " << expectedEnthalpy.Norml2()
);
INFO(
"material-to-gravity gradient-row error = " << relativeError(crossGravityGradient, expectedGravityGradient)
<< ", actual norm = " << crossGravityGradient.Norml2()
<< ", expected norm = " << expectedGravityGradient.Norml2()
);
INFO(
"material-to-gravity Poisson-row error = " << relativeError(crossGravityPotential, expectedGravityPotential)
<< ", actual norm = " << crossGravityPotential.Norml2()
<< ", expected norm = " << expectedGravityPotential.Norml2()
);
CHECK(relativeError(crossDensity, expectedDensity) <= 2.0e-12);
CHECK(relativeError(crossSurface, expectedSurface) <= 2.0e-12);
CHECK(relativeError(crossEnthalpy, expectedEnthalpy) <= 2.0e-12);
CHECK(relativeError(crossGravityGradient, expectedGravityGradient) <= 2.0e-12);
CHECK(relativeError(crossGravityPotential, expectedGravityPotential) <= 2.0e-12);
CHECK(relativeError(crossAction, expected) <= 2.0e-12);
auto materialBlock = preconditioning::materialSurfaceBlock(problem);
auto gravityBlock = preconditioning::GravityFieldBlock(
backend::Diagonal{}, FixedAMG{backend::FixedCycles{.cycles = 1}}, preconditioning::GravityApproximateLDU{}
);
auto structure = preconditioning::stellarStructureBlock(
problem, materialBlock, gravityBlock, preconditioning::ApproximateStellarBlockLDU{}
);
auto prepared = preconditioning::prepare(problem, structure);
mfem::Vector rightHandSide(prepared.Width());
mfem::Vector correction(prepared.Height());
for (int index = 0; index < rightHandSide.Size(); ++index) {
rightHandSide(index) = std::cos(0.17 * static_cast<double>(index + 1));
}
prepared.Mult(rightHandSide, correction);
for (int index = 0; index < correction.Size(); ++index) {
REQUIRE(std::isfinite(correction(index)));
}
const auto &statistics = prepared.GetFactorization().GetStatistics();
CHECK(statistics.applications == 1);
CHECK(statistics.materialSurfaceInverseApplications == 2);
CHECK(statistics.gravityInverseApplications == 1);
CHECK(statistics.materialToGravityApplications == 1);
CHECK(statistics.gravityToMaterialApplications == 1);
const auto unchanged = prepared.Refresh();
CHECK_FALSE(unchanged.DidAnyWork());
CHECK(prepared.IsCurrent());
}