feat(libmeanfield): variadic refactor

also added normaliztion operator
This commit is contained in:
2026-09-06 10:15:00 -04:00
parent 71423d543f
commit 76818f2f82
63 changed files with 28794 additions and 1119 deletions

View File

@@ -1,8 +1,12 @@
#include <algorithm>
#include <catch2/catch_test_macros.hpp>
#include <cmath>
#include <cstdint>
#include <limits>
#include <mfem.hpp>
#include <numbers>
#include <stdexcept>
#include <type_traits>
#include <vector>
import mean_field;
@@ -30,12 +34,41 @@ namespace {
return vector;
}
void require_all_ranks(
const bool localCondition,
const MPI_Comm communicator,
const char *description
) {
int rank = 0;
int size = 0;
MPI_Comm_rank(communicator, &rank);
MPI_Comm_size(communicator, &size);
const int localFailure = localCondition ? size : rank;
int firstFailure = size;
const int result = MPI_Allreduce(
&localFailure,
&firstFailure,
1,
MPI_INT,
MPI_MIN,
communicator
);
REQUIRE(result == MPI_SUCCESS);
CAPTURE(description, localCondition, firstFailure);
REQUIRE(firstFailure == size);
}
double global_dot(
const mfem::Vector &left,
const mfem::Vector &right,
const MPI_Comm communicator
) {
REQUIRE(left.Size() == right.Size());
require_all_ranks(
left.Size() == right.Size(),
communicator,
"global dot-product vector sizes"
);
const double local = left * right;
double global = 0.0;
REQUIRE(MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_SUM, communicator) == MPI_SUCCESS);
@@ -48,6 +81,39 @@ namespace {
) {
return std::sqrt(global_dot(vector, vector, communicator));
}
[[nodiscard]] mean_field::operators::StellarEquilibriumDependencies
make_stellar_dependencies(const std::uint64_t revision = 1) {
return {
.discretization = {.identity = 16101, .revision = 1},
.density = {.identity = 16103, .revision = revision},
.surfaceDeformation = {.identity = 16111, .revision = revision},
.gravityGradient = {.identity = 16127, .revision = revision},
.gravityPotential = {.identity = 16139, .revision = revision},
.enthalpy = {.identity = 16141, .revision = revision},
.bernoulliConstant = {.identity = 16183, .revision = revision},
.rotation = {.identity = 16187, .revision = revision},
.targetMass = {.identity = 16189, .revision = 1}
};
}
void check_rank_consistent_scalar(
const double value,
const MPI_Comm communicator,
const double relativeTolerance = 2.0e-13
) {
double minimum = 0.0;
double maximum = 0.0;
REQUIRE(MPI_Allreduce(&value, &minimum, 1, MPI_DOUBLE, MPI_MIN, communicator) == MPI_SUCCESS);
REQUIRE(MPI_Allreduce(&value, &maximum, 1, MPI_DOUBLE, MPI_MAX, communicator) == MPI_SUCCESS);
CAPTURE(value, minimum, maximum);
CHECK(std::isfinite(minimum));
CHECK(std::isfinite(maximum));
CHECK(
std::abs(maximum - minimum) <=
relativeTolerance * std::max({1.0, std::abs(minimum), std::abs(maximum)})
);
}
} // namespace
TEST_CASE(
@@ -97,6 +163,750 @@ TEST_CASE(
CHECK(f.logicalReferenceMesh->GetNE() == f.mesh->GetNE());
}
TEST_CASE(
"MPI Fixed Angular Momentum Produces One Consistent Global Invariant Row",
"[mpi][distributed][fixed-angular-momentum][physics][jacobian]"
) {
using namespace mean_field;
const auto args = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(args.mesh_file, args, 0);
require_all_ranks(
finiteElements.okay(),
MPI_COMM_WORLD,
"fixed-angular-momentum FEM setup"
);
const MPI_Comm communicator = finiteElements.mesh->GetComm();
mfem::ParGridFunction densityField(finiteElements.densityFes.get());
mfem::ConstantCoefficient densityCoefficient(1.23);
densityField.ProjectCoefficient(densityCoefficient);
mfem::Vector densityTrue;
densityField.GetTrueDofs(densityTrue);
mfem::Vector displacementTrue(finiteElements.displacementFes->GetTrueVSize());
displacementTrue = 0.0;
finiteElements.displacement->SetFromTrueDofs(displacementTrue);
mfem::Vector gravityGradientTrue(finiteElements.gravityFluxFes->GetTrueVSize());
mfem::Vector gravityPotentialTrue(finiteElements.gravityPotentialFes->GetTrueVSize());
gravityGradientTrue = 0.0;
gravityPotentialTrue = 0.0;
operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
finiteElements,
*finiteElements.domainMapperStateless
);
gravityContext.Prepare(
{.density = gravityContext.GetDensityMap().gather(densityTrue),
.displacement = gravityContext.GetDisplacementMap().gather(displacementTrue),
.gravity_gradient = gravityContext.GetGravityGradientMap().gather(gravityGradientTrue),
.gravity_potential = gravityContext.GetGravityPotentialMap().gather(gravityPotentialTrue)},
{.discretization = {.value = 2},
.displacement = {.value = 3},
.density = {.value = 5},
.gravity_gradient = {.value = 7},
.gravity_potential = {.value = 11}}
);
constexpr double targetAngularMomentum = 0.37;
constexpr double angularVelocity = 0.61;
operators::PreparedAngularMomentumOperator invariant(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext,
models::compileConstraint(
integral::FixedAngularMomentum({.Jtotal = dimensions::AngularMomentumValue{targetAngularMomentum}})
)
);
const operators::AngularMomentumDependencies dependencies{
.discretization = {.identity = 101, .revision = 2},
.density = {.identity = 103, .revision = 5},
.displacement = {.identity = 107, .revision = 3},
.rotation = {.identity = 109, .revision = 13}
};
const auto preparation = invariant.Prepare(angularVelocity, dependencies);
CHECK(preparation.rebuiltStaticPlan);
CHECK(preparation.refreshedGeometry);
CHECK(preparation.refreshedDensity);
CHECK(preparation.updatedAngularVelocity);
CHECK(preparation.assembledResidual);
const double independentMoment = analysis::get_moment_of_inertia(finiteElements, densityField);
const double preparedMoment = invariant.GetMomentOfInertia();
const double comparisonScale = std::max({std::abs(independentMoment), std::abs(preparedMoment), 1.0e-300});
CHECK(std::abs(preparedMoment - independentMoment) / comparisonScale <= 3.0e-13);
double minimumMoment = 0.0;
double maximumMoment = 0.0;
MPI_Allreduce(&preparedMoment, &minimumMoment, 1, MPI_DOUBLE, MPI_MIN, finiteElements.mesh->GetComm());
MPI_Allreduce(&preparedMoment, &maximumMoment, 1, MPI_DOUBLE, MPI_MAX, finiteElements.mesh->GetComm());
CHECK(std::abs(maximumMoment - minimumMoment) / comparisonScale <= 2.0e-15);
mfem::Vector residual;
invariant.BuildResidual(residual);
require_all_ranks(
residual.Size() == 1,
communicator,
"fixed-angular-momentum residual size"
);
CHECK(
std::abs(residual(0) - (angularVelocity * independentMoment - targetAngularMomentum)) /
std::max({std::abs(residual(0)), std::abs(angularVelocity * independentMoment), 1.0}) <=
3.0e-13
);
mfem::Vector densityAction;
invariant.ApplyDensityJacobianAction(gravityContext.GetDensityMap().gather(densityTrue), densityAction);
require_all_ranks(
densityAction.Size() == 1,
communicator,
"fixed-angular-momentum density action size"
);
CHECK(std::abs(densityAction(0) - angularVelocity * preparedMoment) / comparisonScale <= 3.0e-13);
constexpr double angularVelocityVariation = -0.29;
mfem::Vector angularVelocityAction;
invariant.ApplyAngularVelocityJacobianAction(angularVelocityVariation, angularVelocityAction);
require_all_ranks(
angularVelocityAction.Size() == 1,
communicator,
"fixed-angular-momentum rotation action size"
);
CHECK(
std::abs(angularVelocityAction(0) - angularVelocityVariation * preparedMoment) / comparisonScale <=
2.0e-15
);
}
TEST_CASE(
"MPI Density Volume Context Forwards The Prepared Global Mass Integral",
"[mpi][distributed][integral-context][physics-extension]"
) {
using namespace mean_field;
const utils::Args arguments = test_utils::setup_args();
fem::FEM finiteElements = fem::setup_fem(arguments.mesh_file, arguments, 0);
require_all_ranks(
finiteElements.okay(),
MPI_COMM_WORLD,
"density-volume context FEM setup"
);
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double densityValue = 1.23;
mfem::ParGridFunction densityField(finiteElements.densityFes.get());
mfem::ConstantCoefficient densityCoefficient(densityValue);
densityField.ProjectCoefficient(densityCoefficient);
mfem::Vector densityTrue;
densityField.GetTrueDofs(densityTrue);
mfem::Vector displacementTrue(finiteElements.displacementFes->GetTrueVSize());
displacementTrue = 0.0;
finiteElements.displacement->SetFromTrueDofs(displacementTrue);
mfem::Vector gravityGradientTrue(finiteElements.gravityFluxFes->GetTrueVSize());
mfem::Vector gravityPotentialTrue(finiteElements.gravityPotentialFes->GetTrueVSize());
gravityGradientTrue = 0.0;
gravityPotentialTrue = 0.0;
operators::context::gravity_field::GravityFieldLinearizationContext gravityContext(
finiteElements,
*finiteElements.domainMapperStateless
);
const mfem::Vector reducedDensity = gravityContext.GetDensityMap().gather(
densityTrue
);
gravityContext.Prepare(
{.density = reducedDensity,
.displacement = gravityContext.GetDisplacementMap().gather(displacementTrue),
.gravity_gradient = gravityContext.GetGravityGradientMap().gather(gravityGradientTrue),
.gravity_potential = gravityContext.GetGravityPotentialMap().gather(gravityPotentialTrue)},
{.discretization = {.value = 2},
.displacement = {.value = 3},
.density = {.value = 5},
.gravity_gradient = {.value = 7},
.gravity_potential = {.value = 11}}
);
operators::PreparedMassNormalizationOperator massIntegral(
finiteElements,
*finiteElements.domainMapperStateless,
gravityContext
);
massIntegral.Prepare(
models::compileConstraint(
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{1.0}})
),
{.discretization = {.identity = 101, .revision = 2},
.density = {.identity = 103, .revision = 5},
.displacement = {.identity = 107, .revision = 3},
.targetMass = {.identity = 109, .revision = 1}}
);
class DistributedMassIntegralCore final {
public:
explicit DistributedMassIntegralCore(
const operators::PreparedMassNormalizationOperator &mass
) noexcept
: m_mass(&mass) {
}
[[nodiscard]] double ApplyDensityVolumeIntegralDensityAction(
const mfem::Vector &direction
) const {
mfem::Vector action;
m_mass->ApplyDensityJacobianAction(direction, action);
return action(0);
}
[[nodiscard]] double ApplyDensityVolumeIntegralSurfaceShapeAction(
const mfem::Vector &
) const noexcept {
return 0.0;
}
private:
const operators::PreparedMassNormalizationOperator *m_mass;
};
const DistributedMassIntegralCore testCore{massIntegral};
const stellar::DensityVolumeIntegralContext<integral::FixedTotalMass>
densityIntegral{testCore};
const double integratedMass =
densityIntegral.integrateDensity(reducedDensity).value();
const double linearizedDensityMass =
densityIntegral.linearizeDensityIntegral(reducedDensity).value();
const double preparedMass = massIntegral.GetCurrentMass();
const double independentMass =
densityValue * analysis::get_mesh_volume(finiteElements);
check_rank_consistent_scalar(integratedMass, communicator);
check_rank_consistent_scalar(linearizedDensityMass, communicator);
check_rank_consistent_scalar(preparedMass, communicator);
check_rank_consistent_scalar(independentMass, communicator);
const double massScale = std::max(
{1.0, std::abs(integratedMass), std::abs(independentMass)}
);
CHECK(std::abs(integratedMass - independentMass) <= 3.0e-12 * massScale);
CHECK(std::abs(integratedMass - preparedMass) <= 3.0e-13 * massScale);
CHECK(std::abs(linearizedDensityMass - integratedMass) <=
3.0e-13 * massScale);
}
TEST_CASE(
"MPI Assembled Variadic Stellar Root Normalizes And Applies Its Inferred Preconditioner",
"[mpi][distributed][stellar-equilibrium][normalization][preconditioning][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_all_ranks(
finiteElements.okay(),
MPI_COMM_WORLD,
"variadic stellar-root FEM setup"
);
const MPI_Comm communicator = finiteElements.mesh->GetComm();
constexpr double radius = utils::RADIUS;
constexpr double mass = utils::MASS;
const double polytropicConstant =
2.0 * utils::G * radius * radius / std::numbers::pi_v<double>;
const double centralDensity =
std::numbers::pi_v<double> * mass / (4.0 * radius * radius * radius);
auto model = model::StellarModel(
eos::Polytrope({.n = 1.0, .K = polytropicConstant}),
surface::Isobaric({.Psurf = dimensions::PressureValue{0.0}}),
integral::FixedTotalMass({.Mtotal = dimensions::MassValue{mass}}),
integral::FixedAngularMomentum({
.Jtotal = dimensions::AngularMomentumValue{0.05},
.axis = {0.0, 0.0, 1.0}
}),
constraint::FixedCentralDensity({.RhoC = dimensions::DensityValue{centralDensity}})
);
auto problem = equilibrium::discretize(
model,
equilibrium::makeStellarDiscretization(
finiteElements,
normalization::PhysicalRieszDiagonal{
dimensions::LengthValue{radius},
utils::G
}
)
);
auto projected = seed::makeProjectedEquilibriumState(
problem,
seed::LaneEmden({
.centralDensity = dimensions::DensityValue{centralDensity},
.radialSampleCount = 512
})
);
auto normalized = normalization::makeNormalizedStellarEquilibriumOperator(problem);
using Problem = std::remove_cvref_t<decltype(problem)>;
using Form = typename Problem::FormType;
constexpr auto massResidualBlock = utils::blocks::get_residual_block<Form>(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term
);
constexpr auto angularMomentumResidualBlock = utils::blocks::get_residual_block<Form>(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
);
constexpr auto centralDensityResidualBlock = utils::blocks::get_residual_block<Form>(
utils::blocks::fixed_central_density_phase.central_value_term
);
require_all_ranks(
problem.StateSize() == problem.EquationSize(),
communicator,
"variadic stellar-root square layout"
);
mfem::Vector normalizedState;
normalized.NormalizeState(projected.values, normalizedState);
const auto preparation = normalized.Prepare(
normalizedState,
make_stellar_dependencies(1)
);
require_all_ranks(
preparation.DidAnyWork() &&
preparation.generatedPhysicalControl &&
preparation.template specification<models::FixedAngularMomentum>().generatedRotation &&
problem.IsPrepared() &&
normalized.IsPrepared(),
communicator,
"initial variadic stellar-root preparation"
);
// The astronomy-facing integral handle must delegate to the same mapped
// physical-volume quadrature and collective reduction as the prepared
// mass equation, without exposing the FEM/core objects to extension
// physics. Linearity in density gives an independent check of both
// public operations on every rank.
const auto physicalState = problem.GetManifest().stateView(projected.values);
const auto physicalDensity = physicalState.block(
utils::blocks::density_field.mass_term
);
const stellar::DensityVolumeIntegralContext<integral::FixedTotalMass>
densityIntegral{problem.GetPhysicalOperator()};
const double integratedMass = densityIntegral.integrateDensity(
physicalDensity
).value();
const double linearizedMass = densityIntegral.linearizeDensityIntegral(
physicalDensity
).value();
const double preparedMass = problem.GetPhysicalOperator()
.GetFixedMassReport()
.achieved;
check_rank_consistent_scalar(integratedMass, communicator);
check_rank_consistent_scalar(linearizedMass, communicator);
const double massComparisonScale = std::max(
{std::abs(integratedMass), std::abs(preparedMass), 1.0e-300}
);
CHECK(std::abs(integratedMass - preparedMass) / massComparisonScale <=
3.0e-13);
CHECK(std::abs(linearizedMass - preparedMass) / massComparisonScale <=
3.0e-13);
mfem::Vector normalizedResidual;
normalized.BuildResidual(normalizedResidual);
require_all_ranks(
normalizedResidual.Size() == problem.EquationSize(),
communicator,
"normalized variadic residual size"
);
auto residualView = problem.GetManifest().residualView(normalizedResidual);
const auto &layout = problem.GetManifest().layout();
const auto &residualFactors = normalized.GetNormalization().ResidualFactors();
const auto massReport = problem.GetPreparedOperator().GetFixedMassReport();
const auto angularMomentumReport =
problem.GetPreparedOperator().GetAngularMomentumReport();
const auto centralDensityReport =
problem.GetPreparedOperator().GetCentralDensityReport();
const auto checkReportedScalarResidual = [&](const mfem::Vector &block,
const double expected) {
require_all_ranks(
block.Size() == 1,
communicator,
"reported scalar residual block size"
);
const double actual = block(0);
CAPTURE(actual, expected);
CHECK(std::isfinite(expected));
CHECK(
std::abs(actual - expected) <=
5.0e-13 * std::max({1.0, std::abs(actual), std::abs(expected)})
);
check_rank_consistent_scalar(actual, finiteElements.mesh->GetComm());
};
checkReportedScalarResidual(
residualView.block(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term
),
massReport.dimensionalResidual *
residualFactors(layout.offset(massResidualBlock))
);
checkReportedScalarResidual(
residualView.block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
),
angularMomentumReport.dimensionalResidual *
residualFactors(layout.offset(angularMomentumResidualBlock))
);
checkReportedScalarResidual(
residualView.block(
utils::blocks::fixed_central_density_phase.central_value_term
),
centralDensityReport.enthalpyResidual *
residualFactors(layout.offset(centralDensityResidualBlock))
);
mfem::Vector normalizedDirection = make_deterministic_vector(problem.StateSize(), 0.37);
const auto directionState = problem.GetManifest().stateView(normalizedDirection);
mfem::Vector massDirection = directionState.block(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term
);
mfem::Vector angularVelocityDirection = directionState.block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
);
mfem::Vector phaseDirection = directionState.block(
utils::blocks::fixed_central_density_phase.central_value_term
);
require_all_ranks(
massDirection.Size() == 1 &&
angularVelocityDirection.Size() == 1 &&
phaseDirection.Size() == 1,
communicator,
"generated scalar direction block sizes"
);
massDirection(0) = 0.17;
angularVelocityDirection(0) = -0.23;
phaseDirection(0) = 0.31;
massDirection.SyncAliasMemory(normalizedDirection);
angularVelocityDirection.SyncAliasMemory(normalizedDirection);
phaseDirection.SyncAliasMemory(normalizedDirection);
normalizedDirection /= global_norm(normalizedDirection, finiteElements.mesh->GetComm());
mfem::Vector normalizedAction;
normalized.Mult(normalizedDirection, normalizedAction);
require_all_ranks(
normalizedAction.Size() == problem.EquationSize(),
communicator,
"normalized variadic Jacobian-action size"
);
/* Different dependency revisions are intentional: the state changes in
* each difference evaluation, so the distributed physical contexts must
* be rebuilt even though all persistent identities remain the same. */
constexpr double differenceStep = 1.0e-5;
mfem::Vector plusState(normalizedState);
plusState.Add(differenceStep, normalizedDirection);
const auto plusPreparation = normalized.Prepare(
plusState,
make_stellar_dependencies(2)
);
require_all_ranks(
plusPreparation.DidAnyWork(),
communicator,
"positive finite-difference preparation"
);
mfem::Vector plusResidual;
normalized.BuildResidual(plusResidual);
mfem::Vector minusState(normalizedState);
minusState.Add(-differenceStep, normalizedDirection);
const auto minusPreparation = normalized.Prepare(
minusState,
make_stellar_dependencies(3)
);
require_all_ranks(
minusPreparation.DidAnyWork(),
communicator,
"negative finite-difference preparation"
);
mfem::Vector minusResidual;
normalized.BuildResidual(minusResidual);
mfem::Vector finiteDifference(plusResidual);
finiteDifference -= minusResidual;
finiteDifference /= 2.0 * differenceStep;
const auto restoredPreparation = normalized.Prepare(
normalizedState,
make_stellar_dependencies(4)
);
require_all_ranks(
restoredPreparation.DidAnyWork() && normalized.IsPrepared(),
communicator,
"restored finite-difference preparation"
);
mfem::Vector restoredResidual;
normalized.BuildResidual(restoredResidual);
mfem::Vector restoredResidualDifference(restoredResidual);
restoredResidualDifference -= normalizedResidual;
const double restoredResidualError = global_norm(
restoredResidualDifference,
finiteElements.mesh->GetComm()
) / std::max({
global_norm(restoredResidual, finiteElements.mesh->GetComm()),
global_norm(normalizedResidual, finiteElements.mesh->GetComm()),
std::numeric_limits<double>::epsilon()
});
CAPTURE(restoredResidualError);
CHECK(restoredResidualError <= 2.0e-12);
mfem::Vector finiteDifferenceError(normalizedAction);
finiteDifferenceError -= finiteDifference;
const double actionNorm = global_norm(
normalizedAction,
finiteElements.mesh->GetComm()
);
const double finiteDifferenceNorm = global_norm(
finiteDifference,
finiteElements.mesh->GetComm()
);
const double completeDifferenceError = global_norm(
finiteDifferenceError,
finiteElements.mesh->GetComm()
) / std::max({
actionNorm,
finiteDifferenceNorm,
std::numeric_limits<double>::epsilon()
});
CAPTURE(actionNorm, finiteDifferenceNorm, completeDifferenceError);
CHECK(actionNorm > std::numeric_limits<double>::min());
CHECK(finiteDifferenceNorm > std::numeric_limits<double>::min());
CHECK(completeDifferenceError <= 8.0e-5);
const int locallyFinite =
vector_is_finite(normalizedResidual) &&
vector_is_finite(normalizedAction) &&
vector_is_finite(finiteDifference) ? 1 : 0;
int globallyFinite = 0;
REQUIRE(MPI_Allreduce(
&locallyFinite,
&globallyFinite,
1,
MPI_INT,
MPI_MIN,
finiteElements.mesh->GetComm()
) == MPI_SUCCESS);
CHECK(globallyFinite == 1);
CHECK(global_norm(normalizedResidual, finiteElements.mesh->GetComm()) > 0.0);
CHECK(global_norm(normalizedAction, finiteElements.mesh->GetComm()) > 0.0);
auto actionView = problem.GetManifest().residualView(normalizedAction);
auto finiteDifferenceView = problem.GetManifest().residualView(finiteDifference);
const auto checkGlobalRow = [&](const auto &term, const char *rowName) {
const mfem::Vector residualBlock = residualView.block(term);
const mfem::Vector actionBlock = actionView.block(term);
const mfem::Vector differenceBlock = finiteDifferenceView.block(term);
require_all_ranks(
residualBlock.Size() == 1 &&
actionBlock.Size() == 1 &&
differenceBlock.Size() == 1,
communicator,
"global scalar residual/Jacobian block sizes"
);
check_rank_consistent_scalar(residualBlock(0), finiteElements.mesh->GetComm());
check_rank_consistent_scalar(actionBlock(0), finiteElements.mesh->GetComm());
check_rank_consistent_scalar(differenceBlock(0), finiteElements.mesh->GetComm());
const double rowMagnitude = std::max(
std::abs(actionBlock(0)),
std::abs(differenceBlock(0))
);
const double rowDifferenceError =
std::abs(actionBlock(0) - differenceBlock(0)) /
std::max(rowMagnitude, std::numeric_limits<double>::epsilon());
CAPTURE(rowName, actionBlock(0), differenceBlock(0), rowMagnitude,
rowDifferenceError);
CHECK(rowMagnitude > 1.0e-10);
CHECK(rowDifferenceError <= 8.0e-5);
};
checkGlobalRow(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term,
"fixed-total-mass"
);
checkGlobalRow(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term,
"fixed-angular-momentum"
);
checkGlobalRow(
utils::blocks::fixed_central_density_phase.central_value_term,
"fixed-central-density phase"
);
auto component = preconditioning::makePreconditioner(problem);
STATIC_CHECK(decltype(component)::borderValueArity == 3);
STATIC_CHECK(decltype(component)::borderResidualArity == 3);
auto physicalInverse = preconditioning::prepare(problem, component);
require_all_ranks(
physicalInverse.IsCurrent(),
communicator,
"prepared physical preconditioner currentness"
);
auto scaledInverse = normalized.MakeScaledPreconditioner(physicalInverse);
require_all_ranks(
scaledInverse.IsCurrent(),
communicator,
"prepared normalized preconditioner currentness"
);
mfem::Vector normalizedRightHandSide =
make_deterministic_vector(problem.EquationSize(), 0.73);
auto rightHandSideView = problem.GetManifest().residualView(normalizedRightHandSide);
mfem::Vector massRightHandSide = rightHandSideView.block(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term
);
mfem::Vector angularMomentumRightHandSide = rightHandSideView.block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
);
mfem::Vector phaseRightHandSide = rightHandSideView.block(
utils::blocks::fixed_central_density_phase.central_value_term
);
massRightHandSide(0) = 0.11;
angularMomentumRightHandSide(0) = -0.19;
phaseRightHandSide(0) = 0.29;
massRightHandSide.SyncAliasMemory(normalizedRightHandSide);
angularMomentumRightHandSide.SyncAliasMemory(normalizedRightHandSide);
phaseRightHandSide.SyncAliasMemory(normalizedRightHandSide);
normalizedRightHandSide /= global_norm(
normalizedRightHandSide,
finiteElements.mesh->GetComm()
);
mfem::Vector firstCorrection(scaledInverse.Height());
mfem::Vector repeatedCorrection(scaledInverse.Height());
firstCorrection = 0.0;
repeatedCorrection = 0.0;
scaledInverse.Mult(normalizedRightHandSide, firstCorrection);
scaledInverse.Mult(normalizedRightHandSide, repeatedCorrection);
mfem::Vector repeatDifference(repeatedCorrection);
repeatDifference -= firstCorrection;
const double correctionNorm = global_norm(firstCorrection, finiteElements.mesh->GetComm());
const double repeatError = global_norm(repeatDifference, finiteElements.mesh->GetComm()) /
std::max(correctionNorm, std::numeric_limits<double>::epsilon());
CAPTURE(correctionNorm, repeatError);
const int locallyFiniteCorrections =
vector_is_finite(firstCorrection) && vector_is_finite(repeatedCorrection) ? 1 : 0;
int globallyFiniteCorrections = 0;
REQUIRE(MPI_Allreduce(
&locallyFiniteCorrections,
&globallyFiniteCorrections,
1,
MPI_INT,
MPI_MIN,
finiteElements.mesh->GetComm()
) == MPI_SUCCESS);
CHECK(globallyFiniteCorrections == 1);
CHECK(std::isfinite(correctionNorm));
CHECK(correctionNorm > 0.0);
CHECK(repeatError <= 2.0e-12);
require_all_ranks(
physicalInverse.IsCurrent() && scaledInverse.IsCurrent(),
communicator,
"preconditioner currentness after repeated application"
);
const mfem::Vector &readOnlyCorrection = firstCorrection;
const auto correctionView = problem.GetManifest().stateView(readOnlyCorrection);
const double massCorrection = correctionView.block(
utils::blocks::fixed_total_mass_constraint.mass_normalization_term
)(0);
const double angularVelocityCorrection = correctionView.block(
utils::blocks::fixed_angular_momentum_constraint.angular_velocity_term
)(0);
const double phaseCorrection = correctionView.block(
utils::blocks::fixed_central_density_phase.central_value_term
)(0);
CAPTURE(massCorrection, angularVelocityCorrection, phaseCorrection);
check_rank_consistent_scalar(
massCorrection,
finiteElements.mesh->GetComm()
);
check_rank_consistent_scalar(
angularVelocityCorrection,
finiteElements.mesh->GetComm()
);
check_rank_consistent_scalar(
phaseCorrection,
finiteElements.mesh->GetComm()
);
/* A Newton iteration reparses the same normalized state under fresh
* dependency revisions. Every rank must observe the stale inverse, and
* refresh must reconstruct the inferred border actions and Schur data. */
const auto secondPreparation = normalized.Prepare(
normalizedState,
make_stellar_dependencies(5)
);
require_all_ranks(
secondPreparation.DidAnyWork() && normalized.IsPrepared(),
communicator,
"second variadic stellar-root preparation"
);
mfem::Vector secondPreparedResidual;
normalized.BuildResidual(secondPreparedResidual);
mfem::Vector secondPreparedResidualDifference(secondPreparedResidual);
secondPreparedResidualDifference -= normalizedResidual;
const double secondPreparedResidualError = global_norm(
secondPreparedResidualDifference,
finiteElements.mesh->GetComm()
) / std::max({
global_norm(secondPreparedResidual, finiteElements.mesh->GetComm()),
global_norm(normalizedResidual, finiteElements.mesh->GetComm()),
std::numeric_limits<double>::epsilon()
});
CAPTURE(secondPreparedResidualError);
CHECK(secondPreparedResidualError <= 2.0e-12);
require_all_ranks(
!physicalInverse.IsCurrent() && !scaledInverse.IsCurrent(),
communicator,
"preconditioners become stale together"
);
CHECK_THROWS_AS(
scaledInverse.Mult(normalizedRightHandSide, repeatedCorrection),
std::logic_error
);
const auto refresh = physicalInverse.Refresh();
CHECK(refresh.specificationActionsRefreshed);
CHECK(refresh.rebuiltSchurComplement);
CHECK(refresh.DidAnyWork());
require_all_ranks(
physicalInverse.IsCurrent() && scaledInverse.IsCurrent(),
communicator,
"refreshed preconditioner currentness"
);
const auto noOpRefresh = physicalInverse.Refresh();
CHECK_FALSE(noOpRefresh.DidAnyWork());
mfem::Vector refreshedCorrection(scaledInverse.Height());
refreshedCorrection = 0.0;
scaledInverse.Mult(normalizedRightHandSide, refreshedCorrection);
mfem::Vector refreshDifference(refreshedCorrection);
refreshDifference -= firstCorrection;
const double refreshError = global_norm(
refreshDifference,
finiteElements.mesh->GetComm()
) / std::max(
correctionNorm,
std::numeric_limits<double>::epsilon()
);
const int locallyFiniteRefresh = vector_is_finite(refreshedCorrection) ? 1 : 0;
int globallyFiniteRefresh = 0;
REQUIRE(MPI_Allreduce(
&locallyFiniteRefresh,
&globallyFiniteRefresh,
1,
MPI_INT,
MPI_MIN,
finiteElements.mesh->GetComm()
) == MPI_SUCCESS);
CAPTURE(refreshError);
CHECK(globallyFiniteRefresh == 1);
CHECK(refreshError <= 2.0e-10);
}
TEST_CASE(
"MPI Prepared Gravity Operators Preserve Global Algebraic Identities",
"[mpi][distributed][gravity][operators][unit]"