Apply canonical gravity blocks without full root work

This commit is contained in:
2026-09-17 10:15:35 -04:00
parent b7ea1bdcf8
commit 6e812aeca2
4 changed files with 725 additions and 37 deletions

View File

@@ -0,0 +1,27 @@
# Compact gravity applications
Branch: `research/gravity-compact-application-20260917`.
This change removes work from applications inside a gravity-only Krylov solve. It does not change equations, coordinates, tolerances, gravity completion, line search, or the verified true-defect gate. Full root preparation and inverse refresh remain mandatory.
## Selection and coordinates
`PreparedGravitySubsystem` accepts a final compile-time application policy. Its default, `AutomaticGravityApplications`, uses a compact action only when a registered capability is present and the supplied local partition exactly equals the typed canonical partition. A collective minimum selects the same path on every rank. `RootRestrictedGravityApplications` keeps the original full-root injection/application/restriction as a test reference and fallback. Existing construction and user-facing stellar specifications are unchanged.
The built-in operator capability requires the known physical stellar core and checks the compiled specification contributions: none may modify gravity rows. It reuses `GravityFieldOperator::ApplyGravityUnknowns`, the existing affine action on gradient and potential at fixed material state and geometry. This applies `[M B^T; B 0]`, including existing reduced/full boundary maps, without invoking zero density or displacement variations. Its compact packing is always gradient followed by potential; the typed root layout chooses the row and value locations independently.
Normalization is unchanged: the action is `D_r A_G D_x^-1`, and the inverse action is `D_x P_G D_r^-1`. Factors are gathered from the frozen normalization on refresh, after the new root preparation is ready. Root generations and child inverse currentness invalidate stale applications. No pointers into normalization buffers survive a refresh.
The direct inverse capability recognizes the standard normalized physical preconditioner and independent material/gravity factorization with the exact gravity block identities. It invokes the already-prepared physical gravity block. Unknown custom inverse recipes and other structure factorizations retain the full-root restriction. The capability's application contract is included in the wrapper's compile-time contract; a potentially flexible branch cannot be advertised as stationary. Selecting a compact inverse is a preconditioning choice, not an assertion that it is the exact gravity inverse. Full-root restriction equivalence must be measured for actual fixtures; the same exact gravity true-defect gate is always used.
`GravityCompactOperatorAccess` and `GravityCompactInverseAccess` are explicit extension contracts. Custom code opts in by specializing them; similarly named getters alone never enable a shortcut. A registered operator promises the exact gravity principal block, canonical partition, and full diagonal factors. A registered inverse promises a current compact physical action with a declared application contract. These are lower-level numerical extension points, not required model inputs.
## Verification
`tests/solver/gravity_compact_applications.cpp` compares compact and explicit root-reference actions for nontrivial directions, nonuniform row/state factors, and both standard and optional-central-border typed forms. It checks factor refresh, stale root and child inverse rejection, unknown inverse fallback, and collective fallback when only rank zero supplies a noncanonical map. Existing generic tests retain empty-local-rank and true-defect failure coverage.
`plateau_gravity_application_diagnostics.hpp` supplies `plateau_diagnostics::ProbeGravityApplications(subsystem, outputDirectory)`. Call after preparing the full normalized operator, refreshing its inverse, and preparing/refreshing the subsystem. It writes `gravity_applications.csv` with three deterministic nontrivial global directions, selected paths, relative/absolute compact-versus-full errors, and separate application times. It neither prepares a candidate nor changes accepted state. The reference policy also allows direct solve comparisons outside the probe.
Recommended bounded real checks: run the helper once on the frozen n=1.5 fixture and once on a fixture with the central-density border; repeat the smaller viable fixture on two ranks. Require operator relative error near floating-point roundoff, then inspect inverse equivalence and timings separately. Run both compact and reference inner solves at the same true-defect tolerance before interpreting speed or iteration changes. The frozen strategy probe's existing inner report already carries operator/inverse application times; expose those alongside `UsesCompactOperator()` and `UsesCompactPreconditioner()` when recording campaign results.
At the time of this implementation commit, the new C++ tests and real-FEM comparison helper await the root-coordinated build/run. No performance or stellar-physics acceptance is claimed from the algebraic tests. End-to-end models continue to require the existing strict convergence and virial checks.

View File

@@ -0,0 +1,121 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <limits>
#include <stdexcept>
#include <mfem.hpp>
#include <mpi.h>
// Include after importing mean_field, as with the other frozen-state probes.
namespace plateau_diagnostics {
struct GravityApplicationProbeOptions final {
int directions{3};
};
/* Exercise complete, nontrivial compact vectors: isolated basis entries
* can miss permutation or normalization mistakes shared by large blocks.
* The reference is deliberately the full normalized root application.
* No preparation, accepted-state update, or convergence claim occurs here. */
template <mean_field::solver::GravitySubsystemRuntime Subsystem>
void ProbeGravityApplications(
const Subsystem &subsystem,
const std::filesystem::path &outputDirectory,
const GravityApplicationProbeOptions options = {}
) {
using namespace mean_field::solver;
const auto communicator = subsystem.GetCommunicator();
int rank = 0;
MPI_Comm_rank(communicator, &rank);
const int localValid = subsystem.IsCurrent() && options.directions > 0;
int allValid = 0;
MPI_Allreduce(&localValid, &allValid, 1, MPI_INT, MPI_MIN, communicator);
int minimumDirections = 0, maximumDirections = 0;
MPI_Allreduce(&options.directions, &minimumDirections, 1, MPI_INT, MPI_MIN, communicator);
MPI_Allreduce(&options.directions, &maximumDirections, 1, MPI_INT, MPI_MAX, communicator);
if (!allValid || minimumDirections != maximumDirections) {
throw std::invalid_argument(
"Gravity application probes require a current subsystem and common positive direction count."
);
}
PreparedGravitySubsystem reference(
subsystem.GetFullOperator(), subsystem.GetFullPreconditioner(), subsystem.GetPartition(), communicator,
linear::FGMRES{}, RootRestrictedGravityApplications{}
);
const int size = subsystem.GetPartition().GravityValueSize();
const long long localSize = size;
long long globalOffset = 0;
MPI_Exscan(&localSize, &globalOffset, 1, MPI_LONG_LONG, MPI_SUM, communicator);
if (rank == 0)
globalOffset = 0;
const auto norm = [communicator](const mfem::Vector &vector) {
const double local = vector * vector;
double global = 0.0;
MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_SUM, communicator);
return std::sqrt(global);
};
const auto elapsed = [communicator](const double start) {
const double local = MPI_Wtime() - start;
double global = 0.0;
MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_MAX, communicator);
return global;
};
std::ofstream output;
if (rank == 0) {
std::filesystem::create_directories(outputDirectory);
output.open(outputDirectory / "gravity_applications.csv");
output
<< std::setprecision(17)
<< "direction,compact_operator,compact_inverse,operator_norm,operator_error,operator_relative_error,"
"compact_operator_seconds,root_operator_seconds,inverse_norm,inverse_error,inverse_relative_error,"
"compact_inverse_seconds,root_inverse_seconds\n";
}
int outputReady = rank != 0 || output.good();
MPI_Allreduce(MPI_IN_PLACE, &outputReady, 1, MPI_INT, MPI_MIN, communicator);
if (!outputReady)
throw std::runtime_error("Could not create gravity application diagnostics.");
mfem::Vector direction(size), compact(size), restricted(size), difference(size);
for (int trial = 0; trial < options.directions; ++trial) {
for (int i = 0; i < size; ++i) {
const double coordinate = static_cast<double>(globalOffset + i + 1);
direction(i) =
std::sin(0.37 * coordinate * (trial + 1)) + 0.4 * std::cos(0.11 * coordinate * (trial + 2));
}
double start = MPI_Wtime();
subsystem.GetOperator().Mult(direction, compact);
const double compactOperatorSeconds = elapsed(start);
start = MPI_Wtime();
reference.GetOperator().Mult(direction, restricted);
const double rootOperatorSeconds = elapsed(start);
difference = compact;
difference -= restricted;
const double operatorNorm = norm(restricted);
const double operatorError = norm(difference);
start = MPI_Wtime();
subsystem.GetPreconditioner().Mult(direction, compact);
const double compactInverseSeconds = elapsed(start);
start = MPI_Wtime();
reference.GetPreconditioner().Mult(direction, restricted);
const double rootInverseSeconds = elapsed(start);
difference = compact;
difference -= restricted;
const double inverseNorm = norm(restricted);
const double inverseError = norm(difference);
const auto relative = [](double error, double scale) {
return scale == 0.0 ? (error == 0.0 ? 0.0 : std::numeric_limits<double>::infinity()) : error / scale;
};
if (rank == 0) {
output << trial << ',' << subsystem.UsesCompactOperator() << ','
<< subsystem.UsesCompactPreconditioner() << ',' << operatorNorm << ',' << operatorError << ','
<< relative(operatorError, operatorNorm) << ',' << compactOperatorSeconds << ','
<< rootOperatorSeconds << ',' << inverseNorm << ',' << inverseError << ','
<< relative(inverseError, inverseNorm) << ',' << compactInverseSeconds << ','
<< rootInverseSeconds << '\n';
}
}
}
} // namespace plateau_diagnostics

View File

@@ -19,6 +19,8 @@ module;
export module mean_field:solver.gravity_subsystem;
export import :solver.linear_backend;
import :normalization.stellar_equilibrium;
import :preconditioning.equilibrium_coordinates;
export import :utils.blocks;
export namespace mean_field::solver {
@@ -71,6 +73,8 @@ export namespace mean_field::solver {
return static_cast<int>(m_retainedResiduals.size());
}
[[nodiscard]] bool operator==(const GravityBlockPartition &) const = default;
void GatherGravityValues(
const mfem::Vector &full,
mfem::Vector &compact
@@ -258,12 +262,158 @@ export namespace mean_field::solver {
};
} // namespace mean_field::solver
export namespace mean_field::solver {
struct AutomaticGravityApplications final { };
struct RootRestrictedGravityApplications final { };
template <typename Candidate>
concept GravityApplicationPolicy = std::same_as<Candidate, AutomaticGravityApplications> ||
std::same_as<Candidate, RootRestrictedGravityApplications>;
/* An opt-in capability promises the exact physical gravity principal block
* in canonical gradient/potential order. Merely exposing similarly named
* getters does not satisfy this contract. Custom operators keep the root
* restriction unless their own specialization makes this promise. */
template <typename Operation> struct GravityCompactOperatorAccess {
static constexpr bool available = false;
};
template <typename Operation, typename Preconditioner> struct GravityCompactInverseAccess {
static constexpr bool available = false;
};
} // namespace mean_field::solver
namespace mean_field::solver::detail {
template <GravitySubsystemOperation Operation> class GravityRestrictedOperator final : public mfem::Operator {
template <typename Couplings> struct SpecificationsLeaveGravityRows;
template <typename... Couplings>
struct SpecificationsLeaveGravityRows<utils::blocks::type_list<Couplings...>>
: std::bool_constant<
((!std::same_as<typename Couplings::Residual, utils::blocks::gravity::gradient::residual> &&
!std::same_as<typename Couplings::Residual, utils::blocks::gravity::poisson::residual>) &&
...)> { };
[[nodiscard]] inline bool CollectiveGravityApplicationChoice(
const bool local,
const MPI_Comm communicator
) {
const int localChoice = local ? 1 : 0;
int globalChoice = 0;
if (MPI_Allreduce(&localChoice, &globalChoice, 1, MPI_INT, MPI_MIN, communicator) != MPI_SUCCESS) {
throw std::runtime_error("Could not select a collective gravity application path.");
}
return globalChoice != 0;
}
} // namespace mean_field::solver::detail
export namespace mean_field::solver {
template <typename Problem>
requires std::same_as<typename Problem::PhysicalCoreType, operators::PreparedStellarEquilibriumOperator> &&
detail::SpecificationsLeaveGravityRows<typename operators::CompiledStellarEquilibriumSystem<
typename Problem::ModelType>::ContributionJacobianCouplings>::value
struct GravityCompactOperatorAccess<normalization::NormalizedStellarEquilibriumOperator<Problem>> {
using Operation = normalization::NormalizedStellarEquilibriumOperator<Problem>;
static constexpr bool available = true;
[[nodiscard]] static GravityBlockPartition CanonicalPartition(const Operation &operation) {
return MakeGravityBlockPartition(operation.GetProblem().GetManifest().layout());
}
[[nodiscard]] static const mfem::Vector &StateFactors(const Operation &operation) {
return operation.GetNormalization().StateFactors();
}
[[nodiscard]] static const mfem::Vector &ResidualFactors(const Operation &operation) {
return operation.GetNormalization().ResidualFactors();
}
static void ApplyPhysical(
const Operation &operation,
const mfem::Vector &direction,
mfem::Vector &action
) {
const auto &physical = operation.GetProblem().GetPhysicalOperator();
const auto &geometry = physical.GetGravityContext().GetGeometryContext();
const int gradientSize = physical.GetGravityContext().GetGravityGradientMap().reduced_size();
const int potentialSize = physical.GetGravityContext().GetGravityPotentialMap().reduced_size();
if (direction.Size() != gradientSize + potentialSize || action.Size() != direction.Size()) {
throw std::invalid_argument("Compact gravity does not match the physical field maps.");
}
const mfem::Vector gradient(const_cast<mfem::real_t *>(direction.GetData()), gradientSize);
auto *potentialData = const_cast<mfem::real_t *>(direction.GetData());
if (gradientSize != 0)
potentialData += gradientSize;
const mfem::Vector potential(potentialData, potentialSize);
// Gravity is affine in these unknowns at fixed geometry and density.
// Reusing its residual action retains boundary maps and signs while
// avoiding the zero material/geometry variations in the full Jacobian.
physical.GetGravityOperator().ApplyGravityUnknowns(gradient, potential, geometry, action);
}
};
template <typename Problem, typename Block>
requires GravityCompactOperatorAccess<
normalization::NormalizedStellarEquilibriumOperator<Problem>>::available &&
std::same_as<
typename Block::StructureComponent::Factorization,
preconditioning::IndependentStellarSubsystems> &&
std::same_as<
typename Block::StructureComponent::GravityComponent::CorrectionBlocks,
utils::blocks::
type_list<utils::blocks::gravity::gradient::value, utils::blocks::gravity::poisson::value>> &&
std::same_as<
typename Block::StructureComponent::GravityComponent::ResidualBlocks,
utils::blocks::type_list<
utils::blocks::gravity::gradient::residual,
utils::blocks::gravity::poisson::residual>>
struct GravityCompactInverseAccess<
normalization::NormalizedStellarEquilibriumOperator<Problem>,
normalization::
NormalizedStellarPreconditioner<Problem, preconditioning::PreparedStellarPreconditioner<Problem, Block>>> {
using Operation = normalization::NormalizedStellarEquilibriumOperator<Problem>;
using Preconditioner = normalization::
NormalizedStellarPreconditioner<Problem, preconditioning::PreparedStellarPreconditioner<Problem, Block>>;
using GravityBlock = typename Block::StructureComponent::GravityComponent;
static constexpr bool available = true;
static constexpr auto applicationContract =
preconditioning::backend::Traits<typename GravityBlock::BackendType>::applicationContract;
[[nodiscard]] static const auto &PhysicalInverse(const Preconditioner &inverse) {
return inverse.GetPhysicalInverse()
.GetGroupedPreconditioner()
.GetStructurePreconditioner()
.GetGravityPreconditioner();
}
[[nodiscard]] static bool Matches(
const Operation &operation,
const Preconditioner &inverse
) {
const auto &gravity = PhysicalInverse(inverse);
const auto canonical = GravityCompactOperatorAccess<Operation>::CanonicalPartition(operation);
return std::addressof(inverse.GetNormalizedJacobian()) == std::addressof(operation) &&
gravity.Height() == canonical.GravityValueSize() &&
gravity.Width() == canonical.GravityResidualSize();
}
[[nodiscard]] static bool IsCurrent(const Preconditioner &inverse) {
return PhysicalInverse(inverse).IsCurrent();
}
static void ApplyPhysical(
const Preconditioner &inverse,
const mfem::Vector &residual,
mfem::Vector &correction
) {
PhysicalInverse(inverse).Mult(residual, correction);
}
};
} // namespace mean_field::solver
namespace mean_field::solver::detail {
template <GravitySubsystemOperation Operation, GravityApplicationPolicy Applications>
class GravityRestrictedOperator final : public mfem::Operator {
public:
using Compact = GravityCompactOperatorAccess<Operation>;
static constexpr bool canCompact =
std::same_as<Applications, AutomaticGravityApplications> && Compact::available;
GravityRestrictedOperator(
const Operation &operation,
const GravityBlockPartition &partition
const GravityBlockPartition &partition,
MPI_Comm communicator
)
: mfem::Operator(
partition.GravityResidualSize(),
@@ -272,10 +422,21 @@ namespace mean_field::solver::detail {
m_operation(std::addressof(operation)),
m_partition(std::addressof(partition)),
m_fullDirection(partition.RootValueSize()),
m_fullAction(partition.RootResidualSize()) {
m_fullAction(partition.RootResidualSize()),
m_stateFactors(Width()),
m_residualFactors(Height()),
m_physicalDirection(Width()),
m_physicalAction(Height()) {
if (operation.Width() != partition.RootValueSize() || operation.Height() != partition.RootResidualSize()) {
throw std::invalid_argument("The gravity partition does not match its root operator.");
}
if constexpr (canCompact) {
// All ranks must enter the same child collectives, even if only
// one rank was given a noncanonical public partition.
m_compact = CollectiveGravityApplicationChoice(
Compact::CanonicalPartition(operation) == partition, communicator
);
}
Refresh();
}
@@ -283,12 +444,29 @@ namespace mean_field::solver::detail {
if (!m_operation->IsPrepared()) {
throw std::logic_error("Gravity requires a prepared root linearization before refresh.");
}
if constexpr (canCompact) {
if (m_compact) {
m_partition->GatherGravityValues(Compact::StateFactors(*m_operation), m_stateFactors);
m_partition->GatherGravityResiduals(Compact::ResidualFactors(*m_operation), m_residualFactors);
}
}
m_generation = m_operation->GetProblem().GetPreparationGeneration();
}
[[nodiscard]] bool IsCurrent() const {
return m_operation->IsPrepared() && m_generation == m_operation->GetProblem().GetPreparationGeneration();
}
[[nodiscard]] bool UsesCompactApplication() const noexcept {
return m_compact;
}
[[nodiscard]] const Operation &GetFullOperator() const noexcept {
return *m_operation;
}
[[nodiscard]] const mfem::Vector &StateFactors() const noexcept {
return m_stateFactors;
}
[[nodiscard]] const mfem::Vector &ResidualFactors() const noexcept {
return m_residualFactors;
}
void Mult(
const mfem::Vector &direction,
@@ -297,6 +475,19 @@ namespace mean_field::solver::detail {
if (!IsCurrent()) {
throw std::logic_error("The gravity linearization must be refreshed after preparation.");
}
if (direction.Size() != Width() || action.Size() != Height()) {
throw std::invalid_argument("Gravity actions require compatible preallocated vectors.");
}
if constexpr (canCompact) {
if (m_compact) {
for (int i = 0; i < Width(); ++i)
m_physicalDirection(i) = direction(i) / m_stateFactors(i);
Compact::ApplyPhysical(*m_operation, m_physicalDirection, m_physicalAction);
for (int i = 0; i < Height(); ++i)
action(i) = m_residualFactors(i) * m_physicalAction(i);
return;
}
}
m_partition->InjectGravityValues(direction, m_fullDirection);
m_operation->Mult(m_fullDirection, m_fullAction);
m_partition->GatherGravityResiduals(m_fullAction, action);
@@ -306,50 +497,89 @@ namespace mean_field::solver::detail {
const Operation *m_operation;
const GravityBlockPartition *m_partition;
std::uint64_t m_generation{0};
mutable mfem::Vector m_fullDirection;
mutable mfem::Vector m_fullAction;
bool m_compact{false};
mutable mfem::Vector m_fullDirection, m_fullAction;
mfem::Vector m_stateFactors, m_residualFactors;
mutable mfem::Vector m_physicalDirection, m_physicalAction;
};
template <GravitySubsystemOperation Operation, GravitySubsystemPreconditioner Preconditioner>
template <
GravitySubsystemOperation Operation,
GravitySubsystemPreconditioner Preconditioner,
GravityApplicationPolicy Applications>
class GravityRestrictedInverse final : public mfem::Solver {
public:
static constexpr auto applicationContract = linearPreconditionerApplicationContract<Preconditioner>;
using Compact = GravityCompactInverseAccess<Operation, Preconditioner>;
using RestrictedOperator = GravityRestrictedOperator<Operation, Applications>;
static constexpr bool canCompact =
std::same_as<Applications, AutomaticGravityApplications> && Compact::available;
static constexpr auto applicationContract = [] {
if constexpr (canCompact) {
if constexpr (Compact::applicationContract == preconditioning::ApplicationContract::flexible)
return preconditioning::ApplicationContract::flexible;
}
return linearPreconditionerApplicationContract<Preconditioner>;
}();
GravityRestrictedInverse(
const Preconditioner &inverse,
const GravityRestrictedOperator<Operation> &operation,
const GravityBlockPartition &partition
const RestrictedOperator &operation,
const GravityBlockPartition &partition,
MPI_Comm communicator
)
: mfem::Solver(partition.GravityValueSize()),
m_inverse(std::addressof(inverse)),
m_operation(std::addressof(operation)),
m_partition(std::addressof(partition)),
m_fullResidual(partition.RootResidualSize()),
m_fullCorrection(partition.RootValueSize()) {
m_fullCorrection(partition.RootValueSize()),
m_physicalResidual(Width()),
m_physicalCorrection(Height()) {
if (inverse.Width() != partition.RootResidualSize() || inverse.Height() != partition.RootValueSize()) {
throw std::invalid_argument("The gravity partition does not match its root inverse.");
}
}
[[nodiscard]] bool IsCurrent() const {
return m_operation->IsCurrent() && m_inverse->IsCurrent();
}
void SetOperator(const mfem::Operator &operation) override {
if (std::addressof(operation) != m_operation) {
throw std::invalid_argument(
"A prepared gravity inverse cannot be "
"rebound to a different operator."
if constexpr (canCompact) {
m_compact = CollectiveGravityApplicationChoice(
operation.UsesCompactApplication() && Compact::Matches(operation.GetFullOperator(), inverse),
communicator
);
}
}
[[nodiscard]] bool IsCurrent() const {
if constexpr (canCompact) {
if (m_compact && !Compact::IsCurrent(*m_inverse))
return false;
}
return m_operation->IsCurrent() && m_inverse->IsCurrent();
}
[[nodiscard]] bool UsesCompactApplication() const noexcept {
return m_compact;
}
void SetOperator(const mfem::Operator &operation) override {
if (std::addressof(operation) != m_operation) {
throw std::invalid_argument("A prepared gravity inverse cannot be rebound to a different operator.");
}
}
void Mult(
const mfem::Vector &residual,
mfem::Vector &correction
) const override {
if (!IsCurrent()) {
if (!IsCurrent())
throw std::logic_error("The gravity inverse requires a current root inverse.");
if (residual.Size() != Width() || correction.Size() != Height()) {
throw std::invalid_argument("Gravity inverse actions require compatible preallocated vectors.");
}
if constexpr (canCompact) {
if (m_compact) {
const auto &state = m_operation->StateFactors();
const auto &rows = m_operation->ResidualFactors();
for (int i = 0; i < Width(); ++i)
m_physicalResidual(i) = residual(i) / rows(i);
Compact::ApplyPhysical(*m_inverse, m_physicalResidual, m_physicalCorrection);
for (int i = 0; i < Height(); ++i)
correction(i) = state(i) * m_physicalCorrection(i);
return;
}
}
m_partition->InjectGravityResiduals(residual, m_fullResidual);
m_inverse->Mult(m_fullResidual, m_fullCorrection);
@@ -358,32 +588,34 @@ namespace mean_field::solver::detail {
private:
const Preconditioner *m_inverse;
const GravityRestrictedOperator<Operation> *m_operation;
const RestrictedOperator *m_operation;
const GravityBlockPartition *m_partition;
mutable mfem::Vector m_fullResidual;
mutable mfem::Vector m_fullCorrection;
bool m_compact{false};
mutable mfem::Vector m_fullResidual, m_fullCorrection, m_physicalResidual, m_physicalCorrection;
};
} // namespace mean_field::solver::detail
export namespace mean_field::solver {
/* Restricting the supplied inverse is a preconditioner, not an assertion
* that its principal block equals G_z^{-1}. The backend still verifies the
* true gravity defect. Keeping the exact root operator as the source of
* every action also retains its boundary rows and normalization. */
* true gravity defect. Registered compact applications retain the same
* boundary rows and diagonal coordinate maps; unknown recipes retain
* the complete root restriction. */
template <
GravitySubsystemOperation Operation,
GravitySubsystemPreconditioner Preconditioner,
LinearBackendConfiguration Backend = linear::FGMRES>
LinearBackendConfiguration Backend = linear::FGMRES,
GravityApplicationPolicy Applications = AutomaticGravityApplications>
requires LinearBackendRuntimeAvailableFor<
Backend,
detail::GravityRestrictedOperator<Operation>,
detail::GravityRestrictedInverse<Operation, Preconditioner>>
detail::GravityRestrictedOperator<Operation, Applications>,
detail::GravityRestrictedInverse<Operation, Preconditioner, Applications>>
class PreparedGravitySubsystem final {
public:
using OperatorType = Operation;
using PreconditionerType = Preconditioner;
using RestrictedOperator = detail::GravityRestrictedOperator<Operation>;
using RestrictedInverse = detail::GravityRestrictedInverse<Operation, Preconditioner>;
using RestrictedOperator = detail::GravityRestrictedOperator<Operation, Applications>;
using RestrictedInverse = detail::GravityRestrictedInverse<Operation, Preconditioner, Applications>;
using BackendType = PreparedLinearBackendType<Backend, RestrictedOperator, RestrictedInverse>;
PreparedGravitySubsystem(
@@ -391,19 +623,22 @@ export namespace mean_field::solver {
const Preconditioner &preconditioner,
GravityBlockPartition partition,
const MPI_Comm communicator,
Backend backend = {}
Backend backend = {},
Applications = {}
)
: m_fullOperator(std::addressof(operation)),
m_fullPreconditioner(std::addressof(preconditioner)),
m_partition(std::move(partition)),
m_operator(
operation,
m_partition
m_partition,
communicator
),
m_inverse(
preconditioner,
m_operator,
m_partition
m_partition,
communicator
),
m_backend(prepareLinearBackend(
std::move(backend),
@@ -437,6 +672,15 @@ export namespace mean_field::solver {
[[nodiscard]] const RestrictedOperator &GetOperator() const noexcept {
return m_operator;
}
[[nodiscard]] const RestrictedInverse &GetPreconditioner() const noexcept {
return m_inverse;
}
[[nodiscard]] bool UsesCompactOperator() const noexcept {
return m_operator.UsesCompactApplication();
}
[[nodiscard]] bool UsesCompactPreconditioner() const noexcept {
return m_inverse.UsesCompactApplication();
}
[[nodiscard]] const Operation &GetFullOperator() const noexcept {
return *m_fullOperator;
}

View File

@@ -0,0 +1,296 @@
#include <array>
#include <cmath>
#include <cstdint>
#include <stdexcept>
#include <vector>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <mfem.hpp>
#include <mpi.h>
import mean_field;
namespace gravity_compact_test {
using namespace mean_field;
template <typename Form> class Operation final : public mfem::Operator {
template <std::size_t N> static auto Sizes() {
std::array<int, N> sizes;
sizes.fill(1);
return sizes;
}
public:
Operation()
: mfem::Operator(
Form::residual_block_count,
Form::value_block_count
),
layout(
Sizes<Form::value_block_count>(),
Sizes<Form::residual_block_count>()
),
partition(solver::MakeGravityBlockPartition(layout)),
state(Width()),
rows(Height()),
physicalDirection(Width()),
physicalAction(Height()),
gravityDirection(2),
gravityAction(2) {
for (int i = 0; i < Width(); ++i)
state(i) = 0.3 + 0.7 * (i + 1);
for (int i = 0; i < Height(); ++i)
rows(i) = 1.2 + 0.4 * (i + 1);
}
void Mult(
const mfem::Vector &direction,
mfem::Vector &action
) const override {
++rootApplications;
for (int i = 0; i < Width(); ++i)
physicalDirection(i) = direction(i) / state(i);
partition.GatherGravityValues(physicalDirection, gravityDirection);
ApplyGravity(gravityDirection, gravityAction);
partition.InjectGravityResiduals(gravityAction, physicalAction);
for (int i = 0; i < Height(); ++i)
action(i) = rows(i) * physicalAction(i);
}
void ApplyGravity(
const mfem::Vector &direction,
mfem::Vector &action
) const {
action(0) = 3.0 * direction(0) + 2.0 * direction(1);
action(1) = 2.0 * direction(0);
}
bool IsPrepared() const {
return prepared;
}
const Operation &GetProblem() const {
return *this;
}
std::uint64_t GetPreparationGeneration() const {
return generation;
}
void Prepare() {
prepared = true;
++generation;
}
utils::blocks::form_layout<Form> layout;
solver::GravityBlockPartition partition;
mfem::Vector state, rows;
bool prepared{true};
std::uint64_t generation{1};
mutable int rootApplications{0}, compactApplications{0};
private:
mutable mfem::Vector physicalDirection, physicalAction, gravityDirection, gravityAction;
};
template <typename Form, bool Registered = true> class Inverse final : public mfem::Solver {
public:
static constexpr auto applicationContract = preconditioning::ApplicationContract::stationary_linear;
explicit Inverse(const Operation<Form> &operation)
: mfem::Solver(operation.Width()),
operation(operation),
physicalRhs(Width()),
physicalCorrection(Height()),
compactRhs(2),
compactCorrection(2) {
Refresh();
}
void SetOperator(const mfem::Operator &) override {
}
void Mult(
const mfem::Vector &rhs,
mfem::Vector &action
) const override {
++rootApplications;
for (int i = 0; i < Width(); ++i)
physicalRhs(i) = rhs(i) / operation.rows(i);
operation.partition.GatherGravityResiduals(physicalRhs, compactRhs);
ApplyGravity(compactRhs, compactCorrection);
operation.partition.InjectGravityValues(compactCorrection, physicalCorrection);
for (int i = 0; i < Height(); ++i)
action(i) = operation.state(i) * physicalCorrection(i);
}
void ApplyGravity(
const mfem::Vector &rhs,
mfem::Vector &action
) const {
action = rhs;
}
bool IsCurrent() const {
return operation.IsPrepared() && generation == operation.generation;
}
void Refresh() {
generation = operation.generation;
compactCurrent = true;
}
const Operation<Form> &operation;
bool compactCurrent{true};
mutable int rootApplications{0}, compactApplications{0};
private:
std::uint64_t generation{0};
mutable mfem::Vector physicalRhs, physicalCorrection, compactRhs, compactCorrection;
};
} // namespace gravity_compact_test
namespace mean_field::solver {
template <typename Form> struct GravityCompactOperatorAccess<gravity_compact_test::Operation<Form>> {
using Operation = gravity_compact_test::Operation<Form>;
static constexpr bool available = true;
static GravityBlockPartition CanonicalPartition(const Operation &operation) {
return operation.partition;
}
static const mfem::Vector &StateFactors(const Operation &operation) {
return operation.state;
}
static const mfem::Vector &ResidualFactors(const Operation &operation) {
return operation.rows;
}
static void ApplyPhysical(
const Operation &operation,
const mfem::Vector &direction,
mfem::Vector &action
) {
++operation.compactApplications;
operation.ApplyGravity(direction, action);
}
};
template <typename Form>
struct GravityCompactInverseAccess<gravity_compact_test::Operation<Form>, gravity_compact_test::Inverse<Form>> {
using Operation = gravity_compact_test::Operation<Form>;
using Inverse = gravity_compact_test::Inverse<Form>;
static constexpr bool available = true;
static constexpr auto applicationContract = preconditioning::ApplicationContract::stationary_linear;
static bool Matches(
const Operation &operation,
const Inverse &inverse
) {
return &operation == &inverse.operation;
}
static bool IsCurrent(const Inverse &inverse) {
return inverse.compactCurrent;
}
static void ApplyPhysical(
const Inverse &inverse,
const mfem::Vector &rhs,
mfem::Vector &action
) {
++inverse.compactApplications;
inverse.ApplyGravity(rhs, action);
}
};
} // namespace mean_field::solver
namespace {
template <typename Form> void CheckCanonicalApplications() {
using namespace mean_field;
gravity_compact_test::Operation<Form> operation;
gravity_compact_test::Inverse<Form> inverse(operation);
solver::PreparedGravitySubsystem compact(operation, inverse, operation.partition, MPI_COMM_WORLD);
solver::PreparedGravitySubsystem reference(
operation, inverse, operation.partition, MPI_COMM_WORLD, solver::linear::FGMRES{},
solver::RootRestrictedGravityApplications{}
);
REQUIRE(compact.UsesCompactOperator());
REQUIRE(compact.UsesCompactPreconditioner());
REQUIRE_FALSE(reference.UsesCompactOperator());
REQUIRE_FALSE(reference.UsesCompactPreconditioner());
mfem::Vector direction(2), direct(2), restricted(2), correction(2);
for (int trial = 0; trial < 4; ++trial) {
direction(0) = std::sin(0.3 + trial);
direction(1) = std::cos(0.7 + 2 * trial);
compact.GetOperator().Mult(direction, direct);
reference.GetOperator().Mult(direction, restricted);
direct -= restricted;
CHECK(direct.Norml2() < 1.0e-13);
compact.GetPreconditioner().Mult(direction, direct);
reference.GetPreconditioner().Mult(direction, restricted);
direct -= restricted;
CHECK(direct.Norml2() < 1.0e-13);
}
CHECK(operation.compactApplications == 4);
CHECK(operation.rootApplications == 4);
CHECK(inverse.compactApplications == 4);
CHECK(inverse.rootApplications == 4);
correction = 0.0;
auto report = compact.Solve(direction, correction, {.relativeTolerance = 1.0e-12, .maximumIterations = 4});
REQUIRE(report.Converged());
reference.GetOperator().Mult(correction, restricted);
restricted -= direction;
CHECK(restricted.Norml2() < 1.0e-11);
CHECK(report.operatorApplications > 0);
CHECK(report.inversePreconditionerApplications > 0);
CHECK(report.operatorSeconds >= 0.0);
CHECK(report.inversePreconditionerSeconds >= 0.0);
// A normalization refresh invalidates the accepted linearization; the
// compact factors must be reacquired when its new generation is ready.
operation.state *= 2.0;
operation.rows *= 0.25;
operation.Prepare();
CHECK_FALSE(compact.IsCurrent());
CHECK_THROWS_AS(compact.GetOperator().Mult(direction, direct), std::logic_error);
CHECK_THROWS_AS(compact.Refresh(), std::logic_error);
inverse.Refresh();
compact.Refresh();
reference.Refresh();
compact.GetOperator().Mult(direction, direct);
reference.GetOperator().Mult(direction, restricted);
direct -= restricted;
CHECK(direct.Norml2() < 1.0e-13);
inverse.compactCurrent = false;
CHECK_FALSE(compact.IsCurrent());
CHECK_THROWS_AS(compact.GetPreconditioner().Mult(direction, direct), std::logic_error);
}
} // namespace
TEST_CASE(
"Compact gravity preserves normalized block actions with optional borders",
"[solver][gravity_subsystem][gravity_compact]"
) {
CheckCanonicalApplications<mean_field::utils::blocks::surface_deformed_stellar_equilibrium_form>();
CheckCanonicalApplications<mean_field::utils::blocks::central_density_bordered_stellar_equilibrium_form>();
}
TEST_CASE(
"Compact gravity selection preserves unknown inverses and collectively rejects noncanonical maps",
"[solver][gravity_subsystem][gravity_compact]"
) {
using namespace mean_field;
using Form = utils::blocks::central_density_bordered_stellar_equilibrium_form;
gravity_compact_test::Operation<Form> operation;
gravity_compact_test::Inverse<Form, false> customInverse(operation);
static_assert(!solver::GravityCompactInverseAccess<decltype(operation), decltype(customInverse)>::available);
solver::PreparedGravitySubsystem unknown(operation, customInverse, operation.partition, MPI_COMM_WORLD);
CHECK(unknown.UsesCompactOperator());
CHECK_FALSE(unknown.UsesCompactPreconditioner());
int rank = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
auto selected = operation.partition;
if (rank == 0)
selected = solver::GravityBlockPartition(operation.Width(), operation.Height(), {1, 0}, {0, 1});
gravity_compact_test::Inverse<Form> inverse(operation);
solver::PreparedGravitySubsystem fallback(operation, inverse, selected, MPI_COMM_WORLD);
solver::PreparedGravitySubsystem reference(
operation, inverse, selected, MPI_COMM_WORLD, solver::linear::FGMRES{},
solver::RootRestrictedGravityApplications{}
);
CHECK_FALSE(fallback.UsesCompactOperator());
CHECK_FALSE(fallback.UsesCompactPreconditioner());
mfem::Vector direction(2), actual(2), expected(2);
direction(0) = 0.43;
direction(1) = -0.91;
fallback.GetOperator().Mult(direction, actual);
reference.GetOperator().Mult(direction, expected);
actual -= expected;
CHECK(actual.Norml2() == 0.0);
fallback.GetPreconditioner().Mult(direction, actual);
reference.GetPreconditioner().Mult(direction, expected);
actual -= expected;
CHECK(actual.Norml2() == 0.0);
}