Add prepared gravity subsystem and second-order trial correction
This commit is contained in:
85
experiments/GRAVITY_SUBSYSTEM_IMPLEMENTATION_2026-09-17.md
Normal file
85
experiments/GRAVITY_SUBSYSTEM_IMPLEMENTATION_2026-09-17.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Prepared gravity subsystem and curvature correction
|
||||
|
||||
This branch introduces solver components; the complete equilibrium equations,
|
||||
normalization, and model specification remain the source of truth.
|
||||
|
||||
## Block and lifetime contracts
|
||||
|
||||
`MakeGravityBlockPartition<Form>(layout)` selects the gravity-gradient and
|
||||
gravity-potential blocks by their compiled value and residual types. Its
|
||||
constraints reject forms lacking those blocks. The complement is retained in
|
||||
root order, including any generated angular-momentum or central-density border.
|
||||
There are no string identifiers or fixed root offsets in this selection.
|
||||
|
||||
`GravityBlockPartition` also has a checked explicit-index constructor for
|
||||
algebraic tests and other compiled adapters. Local partitions may be empty;
|
||||
gravity and retained local systems must be square. A globally empty gravity
|
||||
system is rejected by the prepared linear backend. Gather/injection require
|
||||
preallocated, nonoverlapping storage.
|
||||
|
||||
`PreparedGravitySubsystem` owns restricted operator/inverse workspaces and the
|
||||
existing prepared linear backend. It borrows the full normalized operator,
|
||||
inverse, and communicator at stable addresses. It is neither movable nor
|
||||
copyable. Preparation records the root preparation generation. After candidate
|
||||
preparation the caller must refresh its physical inverse and explicitly refresh
|
||||
the subsystem; stale applications and solves fail. The subsystem does not
|
||||
prepare a candidate, change an accepted state, or commit a step.
|
||||
|
||||
The exact gravity action is the principal restriction of the root Jacobian.
|
||||
The preconditioner is the corresponding restriction of the supplied root
|
||||
inverse. This reuses existing normalization, gravity boundary conditions, and
|
||||
preconditioning recipes, but does not assert that the restricted inverse is an
|
||||
exact gravity inverse. Every solve retains the backend's independently checked
|
||||
true-defect gate. Both wrappers preallocate full-vector workspaces; applications
|
||||
perform gather/scatter and the existing root actions without allocating their
|
||||
own vectors. Full root actions may do unnecessary non-gravity work; a specialized
|
||||
gravity action/inverse adapter remains a future optimization requiring equality
|
||||
tests against this reference restriction.
|
||||
|
||||
Attempted solves, completed inner iterations, failed calls, completed solve time,
|
||||
and the last completed solve report are available without history allocation.
|
||||
Contract exceptions count as failed calls but do not trigger new collectives to
|
||||
measure elapsed time during unwinding.
|
||||
|
||||
The existing FGMRES backend now admits ranks with zero local equations while
|
||||
requiring a nonempty global system. `GetConfiguration()` returns its immutable
|
||||
prescription so another prepared operator can reuse the same backend options.
|
||||
|
||||
## Curvature correction
|
||||
|
||||
`PreparedGravityCurvatureCorrection` captures `G(x)` and `J_G(x) p` before any
|
||||
candidate preparation. For a prepared candidate `x + alpha p` it solves
|
||||
|
||||
```
|
||||
G_z(x + alpha p) delta_z = G(x) + alpha J_G(x) p - G(x + alpha p).
|
||||
```
|
||||
|
||||
Only the compact gravity correction is returned. The caller may inject it only
|
||||
after a converged inner report, then must prepare and evaluate the complete
|
||||
candidate again before applying its usual acceptance rules. In particular,
|
||||
improving gravity does not certify the hydrostatic, material, or shape residual.
|
||||
|
||||
For smooth equations with uniformly bounded inverse gravity block, the right
|
||||
side and correction are second order in `alpha`. This preserves the base and
|
||||
Newton tangent even when the base does not lie on the gravity solution manifold.
|
||||
It differs from projecting the candidate to `G=0`, which can introduce a finite
|
||||
correction at zero step when the base gravity residual is nonzero. Inexact inner
|
||||
tolerances control cancellation of the remainder; they cannot replace the outer
|
||||
equilibrium and virial checks.
|
||||
|
||||
The owner captures a new base for each new Newton direction and invalidates the
|
||||
cache when that direction is discarded. The stored base generation is exposed
|
||||
for diagnostics; preparing trials intentionally does not invalidate the captured
|
||||
base arrays. Reusing those arrays after replacing the base or direction violates
|
||||
the caller's explicit lifecycle contract.
|
||||
|
||||
## Test coverage
|
||||
|
||||
`tests/solver/gravity_subsystem.cpp` covers typed forms with/without a central
|
||||
border, noncontiguous and differently ordered row/value maps, invalid maps and
|
||||
overlapping storage, exact restricted solves, true-defect failure, nonfinite
|
||||
inputs, stale operator/inverse preparation, locally empty MPI partitions,
|
||||
globally empty rejection, preserved backend options, zero affine curvature at
|
||||
an off-manifold base, and the second-order correction at curved trial geometry.
|
||||
The root integration campaign compiles these components and tests real stellar
|
||||
states; this branch does not claim a new validated stellar equilibrium.
|
||||
632
libmeanfield/interface/solver/gravity_subsystem.cppm
Normal file
632
libmeanfield/interface/solver/gravity_subsystem.cppm
Normal file
@@ -0,0 +1,632 @@
|
||||
module;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <mfem.hpp>
|
||||
#include <mpi.h>
|
||||
|
||||
export module mean_field:solver.gravity_subsystem;
|
||||
|
||||
export import :solver.linear_backend;
|
||||
export import :utils.blocks;
|
||||
|
||||
export namespace mean_field::solver {
|
||||
/* The maps are local true-dof maps. Gravity and retained coordinates need
|
||||
* not be contiguous or share the residual ordering. Only the discretized
|
||||
* sizes are runtime data; the production factory selects block identities
|
||||
* from the compiled equation form. */
|
||||
class GravityBlockPartition final {
|
||||
public:
|
||||
GravityBlockPartition(
|
||||
const int rootValueSize,
|
||||
const int rootResidualSize,
|
||||
std::vector<int> gravityValues,
|
||||
std::vector<int> gravityResiduals
|
||||
)
|
||||
: m_rootValueSize(rootValueSize),
|
||||
m_rootResidualSize(rootResidualSize),
|
||||
m_gravityValues(std::move(gravityValues)),
|
||||
m_gravityResiduals(std::move(gravityResiduals)),
|
||||
m_retainedValues(Complement(
|
||||
rootValueSize,
|
||||
m_gravityValues
|
||||
)),
|
||||
m_retainedResiduals(Complement(
|
||||
rootResidualSize,
|
||||
m_gravityResiduals
|
||||
)) {
|
||||
if (m_gravityValues.size() != m_gravityResiduals.size() ||
|
||||
m_retainedValues.size() != m_retainedResiduals.size()) {
|
||||
throw std::invalid_argument("Gravity elimination requires square gravity and retained blocks.");
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] int RootValueSize() const noexcept {
|
||||
return m_rootValueSize;
|
||||
}
|
||||
[[nodiscard]] int RootResidualSize() const noexcept {
|
||||
return m_rootResidualSize;
|
||||
}
|
||||
[[nodiscard]] int GravityValueSize() const noexcept {
|
||||
return static_cast<int>(m_gravityValues.size());
|
||||
}
|
||||
[[nodiscard]] int GravityResidualSize() const noexcept {
|
||||
return static_cast<int>(m_gravityResiduals.size());
|
||||
}
|
||||
[[nodiscard]] int RetainedValueSize() const noexcept {
|
||||
return static_cast<int>(m_retainedValues.size());
|
||||
}
|
||||
[[nodiscard]] int RetainedResidualSize() const noexcept {
|
||||
return static_cast<int>(m_retainedResiduals.size());
|
||||
}
|
||||
|
||||
void GatherGravityValues(
|
||||
const mfem::Vector &full,
|
||||
mfem::Vector &compact
|
||||
) const {
|
||||
Gather(m_gravityValues, m_rootValueSize, full, compact);
|
||||
}
|
||||
void GatherGravityResiduals(
|
||||
const mfem::Vector &full,
|
||||
mfem::Vector &compact
|
||||
) const {
|
||||
Gather(m_gravityResiduals, m_rootResidualSize, full, compact);
|
||||
}
|
||||
void GatherRetainedValues(
|
||||
const mfem::Vector &full,
|
||||
mfem::Vector &compact
|
||||
) const {
|
||||
Gather(m_retainedValues, m_rootValueSize, full, compact);
|
||||
}
|
||||
void GatherRetainedResiduals(
|
||||
const mfem::Vector &full,
|
||||
mfem::Vector &compact
|
||||
) const {
|
||||
Gather(m_retainedResiduals, m_rootResidualSize, full, compact);
|
||||
}
|
||||
void InjectGravityValues(
|
||||
const mfem::Vector &compact,
|
||||
mfem::Vector &full
|
||||
) const {
|
||||
Inject(m_gravityValues, m_rootValueSize, compact, full);
|
||||
}
|
||||
void InjectGravityResiduals(
|
||||
const mfem::Vector &compact,
|
||||
mfem::Vector &full
|
||||
) const {
|
||||
Inject(m_gravityResiduals, m_rootResidualSize, compact, full);
|
||||
}
|
||||
void InjectRetainedValues(
|
||||
const mfem::Vector &compact,
|
||||
mfem::Vector &full
|
||||
) const {
|
||||
Inject(m_retainedValues, m_rootValueSize, compact, full);
|
||||
}
|
||||
void InjectRetainedResiduals(
|
||||
const mfem::Vector &compact,
|
||||
mfem::Vector &full
|
||||
) const {
|
||||
Inject(m_retainedResiduals, m_rootResidualSize, compact, full);
|
||||
}
|
||||
void AddGravityValues(
|
||||
const mfem::Vector &compact,
|
||||
mfem::Vector &full
|
||||
) const {
|
||||
VerifySizes(m_gravityValues, m_rootValueSize, full, compact);
|
||||
for (std::size_t index = 0; index < m_gravityValues.size(); ++index) {
|
||||
full(m_gravityValues[index]) += compact(static_cast<int>(index));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] static std::vector<int> Complement(
|
||||
const int size,
|
||||
const std::vector<int> &selected
|
||||
) {
|
||||
if (size < 0) {
|
||||
throw std::invalid_argument("A gravity partition requires non-negative root dimensions.");
|
||||
}
|
||||
std::vector<bool> used(static_cast<std::size_t>(size), false);
|
||||
for (const int index : selected) {
|
||||
if (index < 0 || index >= size || used[static_cast<std::size_t>(index)]) {
|
||||
throw std::invalid_argument("Gravity partition indices must be unique and in range.");
|
||||
}
|
||||
used[static_cast<std::size_t>(index)] = true;
|
||||
}
|
||||
std::vector<int> complement;
|
||||
complement.reserve(static_cast<std::size_t>(size) - selected.size());
|
||||
for (int index = 0; index < size; ++index) {
|
||||
if (!used[static_cast<std::size_t>(index)]) {
|
||||
complement.push_back(index);
|
||||
}
|
||||
}
|
||||
return complement;
|
||||
}
|
||||
|
||||
static void VerifySizes(
|
||||
const std::vector<int> &indices,
|
||||
const int fullSize,
|
||||
const mfem::Vector &full,
|
||||
const mfem::Vector &compact
|
||||
) {
|
||||
if (full.Size() != fullSize || compact.Size() != static_cast<int>(indices.size())) {
|
||||
throw std::invalid_argument("Gravity partition vectors must have their preallocated sizes.");
|
||||
}
|
||||
const std::less<const mfem::real_t *> before;
|
||||
if (compact.Size() != 0 && full.Size() != 0 && before(full.GetData(), compact.GetData() + compact.Size()) &&
|
||||
before(compact.GetData(), full.GetData() + full.Size())) {
|
||||
throw std::invalid_argument("Gravity gather and injection require distinct vector storage.");
|
||||
}
|
||||
}
|
||||
|
||||
static void Gather(
|
||||
const std::vector<int> &indices,
|
||||
const int fullSize,
|
||||
const mfem::Vector &full,
|
||||
mfem::Vector &compact
|
||||
) {
|
||||
VerifySizes(indices, fullSize, full, compact);
|
||||
for (std::size_t index = 0; index < indices.size(); ++index) {
|
||||
compact(static_cast<int>(index)) = full(indices[index]);
|
||||
}
|
||||
}
|
||||
|
||||
static void Inject(
|
||||
const std::vector<int> &indices,
|
||||
const int fullSize,
|
||||
const mfem::Vector &compact,
|
||||
mfem::Vector &full
|
||||
) {
|
||||
VerifySizes(indices, fullSize, full, compact);
|
||||
full = 0.0;
|
||||
for (std::size_t index = 0; index < indices.size(); ++index) {
|
||||
full(indices[index]) = compact(static_cast<int>(index));
|
||||
}
|
||||
}
|
||||
|
||||
int m_rootValueSize;
|
||||
int m_rootResidualSize;
|
||||
std::vector<int> m_gravityValues;
|
||||
std::vector<int> m_gravityResiduals;
|
||||
std::vector<int> m_retainedValues;
|
||||
std::vector<int> m_retainedResiduals;
|
||||
};
|
||||
|
||||
template <typename Form>
|
||||
concept GravityPartitionForm = requires {
|
||||
typename Form::value_blocks;
|
||||
typename Form::residual_blocks;
|
||||
requires utils::blocks::block_form_is_valid_v<Form>;
|
||||
requires utils::blocks::contains_type_v<utils::blocks::gravity::gradient::value, typename Form::value_blocks>;
|
||||
requires utils::blocks::contains_type_v<utils::blocks::gravity::poisson::value, typename Form::value_blocks>;
|
||||
requires utils::blocks::contains_type_v<
|
||||
utils::blocks::gravity::gradient::residual, typename Form::residual_blocks>;
|
||||
requires utils::blocks::contains_type_v<
|
||||
utils::blocks::gravity::poisson::residual, typename Form::residual_blocks>;
|
||||
};
|
||||
|
||||
template <GravityPartitionForm Form>
|
||||
[[nodiscard]] GravityBlockPartition MakeGravityBlockPartition(const utils::blocks::form_layout<Form> &layout) {
|
||||
using namespace utils::blocks;
|
||||
std::vector<int> values;
|
||||
std::vector<int> residuals;
|
||||
const auto append = [](std::vector<int> &indices, const int offset, const int size) {
|
||||
for (int index = 0; index < size; ++index) {
|
||||
indices.push_back(offset + index);
|
||||
}
|
||||
};
|
||||
const auto value = [&]<typename Block>() {
|
||||
constexpr auto block = value_block<type_index_v<Block, typename Form::value_blocks>>{};
|
||||
append(values, layout.offset(block), layout.size(block));
|
||||
};
|
||||
const auto residual = [&]<typename Block>() {
|
||||
constexpr auto block = residual_block<type_index_v<Block, typename Form::residual_blocks>>{};
|
||||
append(residuals, layout.offset(block), layout.size(block));
|
||||
};
|
||||
value.template operator()<gravity::gradient::value>();
|
||||
value.template operator()<gravity::poisson::value>();
|
||||
residual.template operator()<gravity::gradient::residual>();
|
||||
residual.template operator()<gravity::poisson::residual>();
|
||||
return {
|
||||
layout.value_offsets().Last(), layout.residual_offsets().Last(), std::move(values), std::move(residuals)
|
||||
};
|
||||
}
|
||||
|
||||
template <typename Operation>
|
||||
concept GravitySubsystemOperation =
|
||||
std::derived_from<Operation, mfem::Operator> && requires(const Operation &operation) {
|
||||
{ operation.IsPrepared() } -> std::same_as<bool>;
|
||||
{ operation.GetProblem().GetPreparationGeneration() } -> std::convertible_to<std::uint64_t>;
|
||||
};
|
||||
|
||||
template <typename Preconditioner>
|
||||
concept GravitySubsystemPreconditioner =
|
||||
std::derived_from<Preconditioner, mfem::Solver> &&
|
||||
LinearPreconditionerApplicationContractAvailable<Preconditioner> && requires(const Preconditioner &inverse) {
|
||||
{ inverse.IsCurrent() } -> std::same_as<bool>;
|
||||
};
|
||||
} // namespace mean_field::solver
|
||||
|
||||
namespace mean_field::solver::detail {
|
||||
template <GravitySubsystemOperation Operation> class GravityRestrictedOperator final : public mfem::Operator {
|
||||
public:
|
||||
GravityRestrictedOperator(
|
||||
const Operation &operation,
|
||||
const GravityBlockPartition &partition
|
||||
)
|
||||
: mfem::Operator(
|
||||
partition.GravityResidualSize(),
|
||||
partition.GravityValueSize()
|
||||
),
|
||||
m_operation(std::addressof(operation)),
|
||||
m_partition(std::addressof(partition)),
|
||||
m_fullDirection(partition.RootValueSize()),
|
||||
m_fullAction(partition.RootResidualSize()) {
|
||||
if (operation.Width() != partition.RootValueSize() || operation.Height() != partition.RootResidualSize()) {
|
||||
throw std::invalid_argument("The gravity partition does not match its root operator.");
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void Refresh() {
|
||||
if (!m_operation->IsPrepared()) {
|
||||
throw std::logic_error("Gravity requires a prepared root linearization before refresh.");
|
||||
}
|
||||
m_generation = m_operation->GetProblem().GetPreparationGeneration();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsCurrent() const {
|
||||
return m_operation->IsPrepared() && m_generation == m_operation->GetProblem().GetPreparationGeneration();
|
||||
}
|
||||
|
||||
void Mult(
|
||||
const mfem::Vector &direction,
|
||||
mfem::Vector &action
|
||||
) const override {
|
||||
if (!IsCurrent()) {
|
||||
throw std::logic_error("The gravity linearization must be refreshed after preparation.");
|
||||
}
|
||||
m_partition->InjectGravityValues(direction, m_fullDirection);
|
||||
m_operation->Mult(m_fullDirection, m_fullAction);
|
||||
m_partition->GatherGravityResiduals(m_fullAction, action);
|
||||
}
|
||||
|
||||
private:
|
||||
const Operation *m_operation;
|
||||
const GravityBlockPartition *m_partition;
|
||||
std::uint64_t m_generation{0};
|
||||
mutable mfem::Vector m_fullDirection;
|
||||
mutable mfem::Vector m_fullAction;
|
||||
};
|
||||
|
||||
template <GravitySubsystemOperation Operation, GravitySubsystemPreconditioner Preconditioner>
|
||||
class GravityRestrictedInverse final : public mfem::Solver {
|
||||
public:
|
||||
static constexpr auto applicationContract = linearPreconditionerApplicationContract<Preconditioner>;
|
||||
|
||||
GravityRestrictedInverse(
|
||||
const Preconditioner &inverse,
|
||||
const GravityRestrictedOperator<Operation> &operation,
|
||||
const GravityBlockPartition &partition
|
||||
)
|
||||
: 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()) {
|
||||
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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void Mult(
|
||||
const mfem::Vector &residual,
|
||||
mfem::Vector &correction
|
||||
) const override {
|
||||
if (!IsCurrent()) {
|
||||
throw std::logic_error("The gravity inverse requires a current root inverse.");
|
||||
}
|
||||
m_partition->InjectGravityResiduals(residual, m_fullResidual);
|
||||
m_inverse->Mult(m_fullResidual, m_fullCorrection);
|
||||
m_partition->GatherGravityValues(m_fullCorrection, correction);
|
||||
}
|
||||
|
||||
private:
|
||||
const Preconditioner *m_inverse;
|
||||
const GravityRestrictedOperator<Operation> *m_operation;
|
||||
const GravityBlockPartition *m_partition;
|
||||
mutable mfem::Vector m_fullResidual;
|
||||
mutable mfem::Vector m_fullCorrection;
|
||||
};
|
||||
} // 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. */
|
||||
template <
|
||||
GravitySubsystemOperation Operation,
|
||||
GravitySubsystemPreconditioner Preconditioner,
|
||||
LinearBackendConfiguration Backend = linear::FGMRES>
|
||||
requires LinearBackendRuntimeAvailableFor<
|
||||
Backend,
|
||||
detail::GravityRestrictedOperator<Operation>,
|
||||
detail::GravityRestrictedInverse<Operation, Preconditioner>>
|
||||
class PreparedGravitySubsystem final {
|
||||
public:
|
||||
using OperatorType = Operation;
|
||||
using PreconditionerType = Preconditioner;
|
||||
using RestrictedOperator = detail::GravityRestrictedOperator<Operation>;
|
||||
using RestrictedInverse = detail::GravityRestrictedInverse<Operation, Preconditioner>;
|
||||
using BackendType = PreparedLinearBackendType<Backend, RestrictedOperator, RestrictedInverse>;
|
||||
|
||||
PreparedGravitySubsystem(
|
||||
const Operation &operation,
|
||||
const Preconditioner &preconditioner,
|
||||
GravityBlockPartition partition,
|
||||
const MPI_Comm communicator,
|
||||
Backend backend = {}
|
||||
)
|
||||
: m_fullOperator(std::addressof(operation)),
|
||||
m_fullPreconditioner(std::addressof(preconditioner)),
|
||||
m_partition(std::move(partition)),
|
||||
m_operator(
|
||||
operation,
|
||||
m_partition
|
||||
),
|
||||
m_inverse(
|
||||
preconditioner,
|
||||
m_operator,
|
||||
m_partition
|
||||
),
|
||||
m_backend(prepareLinearBackend(
|
||||
std::move(backend),
|
||||
m_operator,
|
||||
m_inverse,
|
||||
communicator
|
||||
)) {
|
||||
if (!IsCurrent()) {
|
||||
throw std::logic_error("Gravity subsystem preparation requires a current inverse.");
|
||||
}
|
||||
}
|
||||
|
||||
PreparedGravitySubsystem(const PreparedGravitySubsystem &) = delete;
|
||||
PreparedGravitySubsystem &operator=(const PreparedGravitySubsystem &) = delete;
|
||||
PreparedGravitySubsystem(PreparedGravitySubsystem &&) = delete;
|
||||
PreparedGravitySubsystem &operator=(PreparedGravitySubsystem &&) = delete;
|
||||
|
||||
void Refresh() {
|
||||
if (!m_fullPreconditioner->IsCurrent()) {
|
||||
throw std::logic_error("Refresh the root inverse before refreshing its gravity subsystem.");
|
||||
}
|
||||
m_operator.Refresh();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsCurrent() const {
|
||||
return m_operator.IsCurrent() && m_inverse.IsCurrent() && m_backend.IsReady();
|
||||
}
|
||||
[[nodiscard]] const GravityBlockPartition &GetPartition() const noexcept {
|
||||
return m_partition;
|
||||
}
|
||||
[[nodiscard]] const RestrictedOperator &GetOperator() const noexcept {
|
||||
return m_operator;
|
||||
}
|
||||
[[nodiscard]] const Operation &GetFullOperator() const noexcept {
|
||||
return *m_fullOperator;
|
||||
}
|
||||
[[nodiscard]] const Preconditioner &GetFullPreconditioner() const noexcept {
|
||||
return *m_fullPreconditioner;
|
||||
}
|
||||
[[nodiscard]] MPI_Comm GetCommunicator() const noexcept {
|
||||
return m_backend.GetCommunicator();
|
||||
}
|
||||
[[nodiscard]] std::uint64_t GetSolveCount() const noexcept {
|
||||
return m_solveCount;
|
||||
}
|
||||
[[nodiscard]] std::uint64_t GetIterationCount() const noexcept {
|
||||
return m_iterationCount;
|
||||
}
|
||||
[[nodiscard]] std::uint64_t GetFailureCount() const noexcept {
|
||||
return m_failureCount;
|
||||
}
|
||||
[[nodiscard]] double GetSolveSeconds() const noexcept {
|
||||
return m_solveSeconds;
|
||||
}
|
||||
[[nodiscard]] const std::optional<LinearSolveReport> &GetLastSolveReport() const noexcept {
|
||||
return m_lastSolve;
|
||||
}
|
||||
|
||||
[[nodiscard]] LinearSolveReport Solve(
|
||||
const mfem::Vector &rightHandSide,
|
||||
mfem::Vector &correction,
|
||||
const LinearSolveControl &control
|
||||
) {
|
||||
++m_solveCount;
|
||||
try {
|
||||
const auto report = m_backend.Solve(rightHandSide, correction, control);
|
||||
m_iterationCount += static_cast<std::uint64_t>(report.iterations);
|
||||
m_solveSeconds += report.solveSeconds;
|
||||
if (!report.Converged()) {
|
||||
++m_failureCount;
|
||||
}
|
||||
m_lastSolve = report;
|
||||
return report;
|
||||
} catch (...) {
|
||||
++m_failureCount;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const Operation *m_fullOperator;
|
||||
const Preconditioner *m_fullPreconditioner;
|
||||
GravityBlockPartition m_partition;
|
||||
RestrictedOperator m_operator;
|
||||
RestrictedInverse m_inverse;
|
||||
BackendType m_backend;
|
||||
// Times and iteration counts cover completed calls; failed contracts
|
||||
// are counted without attempting another collective during unwinding.
|
||||
std::uint64_t m_solveCount{0};
|
||||
std::uint64_t m_iterationCount{0};
|
||||
std::uint64_t m_failureCount{0};
|
||||
double m_solveSeconds{0.0};
|
||||
std::optional<LinearSolveReport> m_lastSolve;
|
||||
};
|
||||
|
||||
template <typename Subsystem>
|
||||
concept GravitySubsystemRuntime = requires(
|
||||
Subsystem &subsystem,
|
||||
const Subsystem &constant,
|
||||
const mfem::Vector &rhs,
|
||||
mfem::Vector &correction,
|
||||
const LinearSolveControl &control
|
||||
) {
|
||||
{ constant.IsCurrent() } -> std::same_as<bool>;
|
||||
{ constant.GetPartition() } -> std::same_as<const GravityBlockPartition &>;
|
||||
{ constant.GetCommunicator() } -> std::same_as<MPI_Comm>;
|
||||
{ subsystem.Solve(rhs, correction, control) } -> std::same_as<LinearSolveReport>;
|
||||
{ constant.GetFullOperator().Mult(rhs, correction) } -> std::same_as<void>;
|
||||
{ constant.GetFullOperator().GetProblem().GetPreparationGeneration() } -> std::convertible_to<std::uint64_t>;
|
||||
};
|
||||
|
||||
template <GravitySubsystemRuntime Subsystem> class PreparedGravityCurvatureCorrection final {
|
||||
public:
|
||||
explicit PreparedGravityCurvatureCorrection(Subsystem &subsystem)
|
||||
: m_subsystem(std::addressof(subsystem)),
|
||||
m_baseResidual(subsystem.GetPartition().GravityResidualSize()),
|
||||
m_baseAction(subsystem.GetPartition().GravityResidualSize()),
|
||||
m_fullAction(subsystem.GetPartition().RootResidualSize()),
|
||||
m_rightHandSide(subsystem.GetPartition().GravityResidualSize()) {
|
||||
}
|
||||
|
||||
PreparedGravityCurvatureCorrection(const PreparedGravityCurvatureCorrection &) = delete;
|
||||
PreparedGravityCurvatureCorrection &operator=(const PreparedGravityCurvatureCorrection &) = delete;
|
||||
PreparedGravityCurvatureCorrection(PreparedGravityCurvatureCorrection &&) = delete;
|
||||
PreparedGravityCurvatureCorrection &operator=(PreparedGravityCurvatureCorrection &&) = delete;
|
||||
|
||||
// Capture must precede trial preparation: using a candidate Jacobian
|
||||
// here would introduce a first-order change into the Newton tangent.
|
||||
void CaptureBase(
|
||||
const mfem::Vector &baseResidual,
|
||||
const mfem::Vector &baseDirection
|
||||
) {
|
||||
m_hasBase = false;
|
||||
const auto &partition = m_subsystem->GetPartition();
|
||||
RequireCollectively(
|
||||
m_subsystem->IsCurrent() && baseResidual.Size() == partition.RootResidualSize() &&
|
||||
baseDirection.Size() == partition.RootValueSize(),
|
||||
"Gravity curvature capture requires a current base and "
|
||||
"complete vectors."
|
||||
);
|
||||
partition.GatherGravityResiduals(baseResidual, m_baseResidual);
|
||||
m_subsystem->GetFullOperator().Mult(baseDirection, m_fullAction);
|
||||
partition.GatherGravityResiduals(m_fullAction, m_baseAction);
|
||||
m_baseGeneration = m_subsystem->GetFullOperator().GetProblem().GetPreparationGeneration();
|
||||
m_hasBase = true;
|
||||
}
|
||||
|
||||
void Invalidate() noexcept {
|
||||
m_hasBase = false;
|
||||
}
|
||||
[[nodiscard]] bool HasBase() const noexcept {
|
||||
return m_hasBase;
|
||||
}
|
||||
[[nodiscard]] std::uint64_t GetBaseGeneration() const noexcept {
|
||||
return m_baseGeneration;
|
||||
}
|
||||
|
||||
// The caller owns candidate preparation and commits only after a new
|
||||
// complete residual evaluation. A failed inner solve never modifies
|
||||
// an accepted state; its output remains a diagnostic correction only.
|
||||
[[nodiscard]] LinearSolveReport SolveCorrection(
|
||||
const double alpha,
|
||||
const mfem::Vector &trialResidual,
|
||||
mfem::Vector &gravityCorrection,
|
||||
const LinearSolveControl &control
|
||||
) {
|
||||
const auto &partition = m_subsystem->GetPartition();
|
||||
RequireCollectively(
|
||||
m_hasBase && m_subsystem->IsCurrent() && std::isfinite(alpha) && alpha >= 0.0 &&
|
||||
trialResidual.Size() == partition.RootResidualSize() &&
|
||||
gravityCorrection.Size() == partition.GravityValueSize(),
|
||||
"Gravity curvature correction requires a captured base, a current "
|
||||
"candidate, and valid vectors."
|
||||
);
|
||||
double minimumAlpha = 0.0;
|
||||
double maximumAlpha = 0.0;
|
||||
if (MPI_Allreduce(&alpha, &minimumAlpha, 1, MPI_DOUBLE, MPI_MIN, m_subsystem->GetCommunicator()) !=
|
||||
MPI_SUCCESS ||
|
||||
MPI_Allreduce(&alpha, &maximumAlpha, 1, MPI_DOUBLE, MPI_MAX, m_subsystem->GetCommunicator()) !=
|
||||
MPI_SUCCESS) {
|
||||
throw std::runtime_error(
|
||||
"Gravity curvature correction could not verify "
|
||||
"its distributed step length."
|
||||
);
|
||||
}
|
||||
if (minimumAlpha != maximumAlpha) {
|
||||
throw std::invalid_argument(
|
||||
"Gravity curvature correction requires the "
|
||||
"same step length on every rank."
|
||||
);
|
||||
}
|
||||
partition.GatherGravityResiduals(trialResidual, m_rightHandSide);
|
||||
for (int index = 0; index < m_rightHandSide.Size(); ++index) {
|
||||
m_rightHandSide(index) = m_baseResidual(index) + alpha * m_baseAction(index) - m_rightHandSide(index);
|
||||
}
|
||||
gravityCorrection = 0.0;
|
||||
return m_subsystem->Solve(m_rightHandSide, gravityCorrection, control);
|
||||
}
|
||||
|
||||
private:
|
||||
void RequireCollectively(
|
||||
const bool valid,
|
||||
const char *message
|
||||
) const {
|
||||
int local = valid ? 1 : 0;
|
||||
int global = 0;
|
||||
if (MPI_Allreduce(&local, &global, 1, MPI_INT, MPI_MIN, m_subsystem->GetCommunicator()) != MPI_SUCCESS) {
|
||||
throw std::runtime_error(
|
||||
"Gravity curvature correction could not verify "
|
||||
"distributed validity."
|
||||
);
|
||||
}
|
||||
if (global == 0) {
|
||||
throw std::logic_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
Subsystem *m_subsystem;
|
||||
mfem::Vector m_baseResidual;
|
||||
mfem::Vector m_baseAction;
|
||||
mfem::Vector m_fullAction;
|
||||
mfem::Vector m_rightHandSide;
|
||||
std::uint64_t m_baseGeneration{0};
|
||||
bool m_hasBase{false};
|
||||
};
|
||||
} // namespace mean_field::solver
|
||||
@@ -664,12 +664,25 @@ export namespace mean_field::solver::linear {
|
||||
if (m_communicator == MPI_COMM_NULL) {
|
||||
throw std::invalid_argument("MFEM FGMRES requires a non-null MPI communicator.");
|
||||
}
|
||||
if (operation.Height() <= 0 || operation.Width() <= 0 || operation.Height() != operation.Width() ||
|
||||
preconditioner.Height() != operation.Width() || preconditioner.Width() != operation.Height()) {
|
||||
const bool localDimensionsAreValid =
|
||||
operation.Height() >= 0 && operation.Width() >= 0 && operation.Height() == operation.Width() &&
|
||||
preconditioner.Height() == operation.Width() && preconditioner.Width() == operation.Height();
|
||||
if (!AllRanksAgree(localDimensionsAreValid, m_communicator)) {
|
||||
throw std::invalid_argument(
|
||||
"MFEM FGMRES requires compatible square operator and preconditioner dimensions."
|
||||
"MFEM FGMRES requires compatible square "
|
||||
"operator and preconditioner dimensions."
|
||||
);
|
||||
}
|
||||
// A partition may own no local unknowns while still taking
|
||||
// part in distributed operator applications and reductions.
|
||||
const long long localSize = operation.Height();
|
||||
long long globalSize = 0;
|
||||
if (MPI_Allreduce(&localSize, &globalSize, 1, MPI_LONG_LONG, MPI_SUM, m_communicator) != MPI_SUCCESS) {
|
||||
throw std::runtime_error("MFEM FGMRES could not determine its global system size.");
|
||||
}
|
||||
if (globalSize == 0) {
|
||||
throw std::invalid_argument("MFEM FGMRES requires a globally nonempty system.");
|
||||
}
|
||||
|
||||
m_rightHandSide = 0.0;
|
||||
m_operationAction = 0.0;
|
||||
@@ -699,6 +712,10 @@ export namespace mean_field::solver::linear {
|
||||
return m_communicator;
|
||||
}
|
||||
|
||||
[[nodiscard]] FGMRES GetConfiguration() const {
|
||||
return FGMRES{m_options};
|
||||
}
|
||||
|
||||
[[nodiscard]] bool IsReady() const {
|
||||
return m_operation != nullptr && m_preconditioner != nullptr && m_communicator != MPI_COMM_NULL &&
|
||||
m_operation->Height() == m_operation->Width() &&
|
||||
|
||||
411
tests/solver/gravity_subsystem.cpp
Normal file
411
tests/solver/gravity_subsystem.cpp
Normal file
@@ -0,0 +1,411 @@
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <mfem.hpp>
|
||||
#include <mpi.h>
|
||||
|
||||
import mean_field;
|
||||
|
||||
namespace {
|
||||
using namespace mean_field;
|
||||
|
||||
class PreparedDenseOperator final : public mfem::Operator {
|
||||
public:
|
||||
explicit PreparedDenseOperator(mfem::DenseMatrix matrix)
|
||||
: mfem::Operator(
|
||||
matrix.Height(),
|
||||
matrix.Width()
|
||||
),
|
||||
m_matrix(std::move(matrix)) {
|
||||
}
|
||||
|
||||
void Mult(
|
||||
const mfem::Vector &direction,
|
||||
mfem::Vector &action
|
||||
) const override {
|
||||
if (!m_prepared) {
|
||||
throw std::logic_error("Test operator is unprepared.");
|
||||
}
|
||||
m_matrix.Mult(direction, action);
|
||||
}
|
||||
void Prepare(mfem::DenseMatrix matrix) {
|
||||
m_matrix = std::move(matrix);
|
||||
m_prepared = true;
|
||||
++m_generation;
|
||||
}
|
||||
void Invalidate() {
|
||||
m_prepared = false;
|
||||
}
|
||||
[[nodiscard]] bool IsPrepared() const {
|
||||
return m_prepared;
|
||||
}
|
||||
[[nodiscard]] const PreparedDenseOperator &GetProblem() const {
|
||||
return *this;
|
||||
}
|
||||
[[nodiscard]] std::uint64_t GetPreparationGeneration() const {
|
||||
return m_generation;
|
||||
}
|
||||
|
||||
private:
|
||||
mfem::DenseMatrix m_matrix;
|
||||
std::uint64_t m_generation{1};
|
||||
bool m_prepared{true};
|
||||
};
|
||||
|
||||
class PreparedPermutationInverse final : public mfem::Solver {
|
||||
public:
|
||||
static constexpr auto applicationContract = preconditioning::ApplicationContract::stationary_linear;
|
||||
|
||||
explicit PreparedPermutationInverse(
|
||||
const PreparedDenseOperator &operation,
|
||||
std::vector<int> residualForValue = {}
|
||||
)
|
||||
: mfem::Solver(operation.Width()),
|
||||
m_operation(operation),
|
||||
m_residualForValue(std::move(residualForValue)) {
|
||||
if (m_residualForValue.empty()) {
|
||||
for (int index = 0; index < Width(); ++index) {
|
||||
m_residualForValue.push_back(index);
|
||||
}
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
void SetOperator(const mfem::Operator &) override {
|
||||
}
|
||||
void Mult(
|
||||
const mfem::Vector &rhs,
|
||||
mfem::Vector &action
|
||||
) const override {
|
||||
if (!IsCurrent()) {
|
||||
throw std::logic_error("Test inverse is stale.");
|
||||
}
|
||||
for (int index = 0; index < Width(); ++index) {
|
||||
action(index) = rhs(m_residualForValue[static_cast<std::size_t>(index)]);
|
||||
}
|
||||
}
|
||||
void Refresh() {
|
||||
m_generation = m_operation.GetPreparationGeneration();
|
||||
}
|
||||
[[nodiscard]] bool IsCurrent() const {
|
||||
return m_operation.IsPrepared() && m_generation == m_operation.GetPreparationGeneration();
|
||||
}
|
||||
|
||||
private:
|
||||
const PreparedDenseOperator &m_operation;
|
||||
std::vector<int> m_residualForValue;
|
||||
std::uint64_t m_generation{0};
|
||||
};
|
||||
|
||||
[[nodiscard]] mfem::DenseMatrix Diagonal(const std::initializer_list<double> entries) {
|
||||
mfem::DenseMatrix result(static_cast<int>(entries.size()));
|
||||
result = 0.0;
|
||||
int index = 0;
|
||||
for (const double entry : entries) {
|
||||
result(index, index) = entry;
|
||||
++index;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] mfem::DenseMatrix CurvedJacobian(
|
||||
const double q,
|
||||
const double z
|
||||
) {
|
||||
mfem::DenseMatrix result(2);
|
||||
result(0, 0) = 2.0 * q * z - 3.0 * q * q;
|
||||
result(0, 1) = 1.0 + q * q;
|
||||
result(1, 0) = 1.0;
|
||||
result(1, 1) = 2.0;
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] mfem::Vector CurvedResidual(
|
||||
const double q,
|
||||
const double z
|
||||
) {
|
||||
mfem::Vector result(2);
|
||||
result(0) = (1.0 + q * q) * z - q * q * q;
|
||||
result(1) = q + 2.0 * z;
|
||||
return result;
|
||||
}
|
||||
|
||||
using PlainForm = utils::blocks::surface_deformed_stellar_equilibrium_form;
|
||||
using CentralForm = utils::blocks::central_density_bordered_stellar_equilibrium_form;
|
||||
static_assert(solver::GravityPartitionForm<PlainForm>);
|
||||
static_assert(solver::GravityPartitionForm<CentralForm>);
|
||||
static_assert(!solver::GravityPartitionForm<int>);
|
||||
static_assert(solver::GravitySubsystemOperation<PreparedDenseOperator>);
|
||||
static_assert(solver::GravitySubsystemPreconditioner<PreparedPermutationInverse>);
|
||||
static_assert(!solver::GravitySubsystemOperation<mfem::Operator>);
|
||||
static_assert(!solver::GravitySubsystemPreconditioner<mfem::Solver>);
|
||||
} // namespace
|
||||
|
||||
TEST_CASE(
|
||||
"Gravity partition preserves noncontiguous coordinate and equation order",
|
||||
"[solver][gravity_subsystem]"
|
||||
) {
|
||||
using Catch::Approx;
|
||||
using namespace mean_field;
|
||||
solver::GravityBlockPartition partition(5, 5, {3, 1}, {0, 4});
|
||||
mfem::Vector full(5), compact(2), retained(3), assembled(5);
|
||||
for (int index = 0; index < 5; ++index) {
|
||||
full(index) = 10.0 + index;
|
||||
}
|
||||
partition.GatherGravityValues(full, compact);
|
||||
CHECK(compact(0) == 13.0);
|
||||
CHECK(compact(1) == 11.0);
|
||||
partition.InjectGravityResiduals(compact, assembled);
|
||||
CHECK(assembled(0) == 13.0);
|
||||
CHECK(assembled(4) == 11.0);
|
||||
CHECK(assembled(1) == 0.0);
|
||||
partition.GatherRetainedValues(full, retained);
|
||||
CHECK(retained(0) == 10.0);
|
||||
CHECK(retained(1) == 12.0);
|
||||
CHECK(retained(2) == 14.0);
|
||||
partition.InjectRetainedValues(retained, assembled);
|
||||
partition.AddGravityValues(compact, assembled);
|
||||
assembled -= full;
|
||||
CHECK(assembled.Norml2() == 0.0);
|
||||
CHECK_THROWS_AS(solver::GravityBlockPartition(3, 3, {0, 0}, {0, 1}), std::invalid_argument);
|
||||
CHECK_THROWS_AS(solver::GravityBlockPartition(3, 3, {3}, {0}), std::invalid_argument);
|
||||
CHECK_THROWS_AS(solver::GravityBlockPartition(3, 3, {0}, {0, 1}), std::invalid_argument);
|
||||
mfem::Vector aliased(full.GetData() + 1, 2);
|
||||
CHECK_THROWS_AS(partition.InjectGravityValues(aliased, full), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Gravity partition compiles from forms with and without a central border",
|
||||
"[solver][gravity_subsystem]"
|
||||
) {
|
||||
using namespace mean_field;
|
||||
const auto check = []<typename Form>() {
|
||||
std::array<int, Form::value_block_count> valueSizes{};
|
||||
std::array<int, Form::residual_block_count> residualSizes{};
|
||||
valueSizes.fill(1);
|
||||
residualSizes.fill(1);
|
||||
utils::blocks::form_layout<Form> layout(valueSizes, residualSizes);
|
||||
const auto partition = solver::MakeGravityBlockPartition<Form>(layout);
|
||||
CHECK(partition.GravityValueSize() == 2);
|
||||
CHECK(partition.GravityResidualSize() == 2);
|
||||
CHECK(partition.RetainedValueSize() == Form::value_block_count - 2);
|
||||
CHECK(partition.RetainedResidualSize() == Form::residual_block_count - 2);
|
||||
};
|
||||
check.template operator()<PlainForm>();
|
||||
check.template operator()<CentralForm>();
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Gravity subsystem solves the actual restricted operator and rejects "
|
||||
"stale preparation",
|
||||
"[solver][gravity_subsystem]"
|
||||
) {
|
||||
using Catch::Approx;
|
||||
using namespace mean_field;
|
||||
PreparedDenseOperator operation(Diagonal({2.0, 3.0, 7.0}));
|
||||
PreparedPermutationInverse inverse(operation);
|
||||
solver::PreparedGravitySubsystem subsystem(
|
||||
operation, inverse, solver::GravityBlockPartition(3, 3, {1, 0}, {1, 0}), MPI_COMM_WORLD
|
||||
);
|
||||
static_assert(solver::GravitySubsystemRuntime<decltype(subsystem)>);
|
||||
mfem::Vector rhs(2), correction(2), action(2);
|
||||
rhs(0) = 9.0;
|
||||
rhs(1) = -8.0;
|
||||
correction = 0.0;
|
||||
const solver::LinearSolveControl control{.relativeTolerance = 1.0e-12, .maximumIterations = 8};
|
||||
const auto report = subsystem.Solve(rhs, correction, control);
|
||||
REQUIRE(report.Converged());
|
||||
CHECK(correction(0) == Approx(3.0).margin(1.0e-12));
|
||||
CHECK(correction(1) == Approx(-4.0).margin(1.0e-12));
|
||||
subsystem.GetOperator().Mult(correction, action);
|
||||
action -= rhs;
|
||||
double localSquared = action * action;
|
||||
double globalSquared = 0.0;
|
||||
MPI_Allreduce(&localSquared, &globalSquared, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
|
||||
CHECK(report.trueResidualNorm == Approx(std::sqrt(globalSquared)).margin(1.0e-14));
|
||||
|
||||
operation.Prepare(Diagonal({4.0, 6.0, 7.0}));
|
||||
CHECK_FALSE(subsystem.IsCurrent());
|
||||
CHECK_THROWS_AS(subsystem.Solve(rhs, correction, control), std::logic_error);
|
||||
CHECK_THROWS_AS(subsystem.Refresh(), std::logic_error);
|
||||
inverse.Refresh();
|
||||
CHECK_FALSE(subsystem.IsCurrent());
|
||||
subsystem.Refresh();
|
||||
REQUIRE(subsystem.Solve(rhs, correction, control).Converged());
|
||||
CHECK(correction(0) == Approx(1.5).margin(1.0e-12));
|
||||
CHECK(correction(1) == Approx(-2.0).margin(1.0e-12));
|
||||
operation.Invalidate();
|
||||
CHECK_FALSE(subsystem.IsCurrent());
|
||||
CHECK_THROWS_AS(subsystem.Refresh(), std::logic_error);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Gravity inner solve reports an unresolved true defect instead of "
|
||||
"certifying a correction",
|
||||
"[solver][gravity_subsystem]"
|
||||
) {
|
||||
using namespace mean_field;
|
||||
PreparedDenseOperator operation(Diagonal({2.0, 3.0, 7.0}));
|
||||
PreparedPermutationInverse inverse(operation);
|
||||
solver::PreparedGravitySubsystem subsystem(
|
||||
operation, inverse, solver::GravityBlockPartition(3, 3, {0, 1}, {0, 1}), MPI_COMM_WORLD
|
||||
);
|
||||
mfem::Vector rhs(2), correction(2);
|
||||
rhs = 1.0;
|
||||
correction = 0.0;
|
||||
const solver::LinearSolveControl control{.relativeTolerance = 1.0e-12, .maximumIterations = 1};
|
||||
const auto report = subsystem.Solve(rhs, correction, control);
|
||||
CHECK_FALSE(report.Converged());
|
||||
CHECK(report.status == solver::LinearSolveStatus::maximum_iterations);
|
||||
CHECK(report.trueResidualNorm > control.ConvergenceThreshold(report.rightHandSideNorm));
|
||||
CHECK(report.relativeTrueResidualNorm > 0.1);
|
||||
CHECK(subsystem.GetSolveCount() == 1);
|
||||
CHECK(subsystem.GetIterationCount() == static_cast<std::uint64_t>(report.iterations));
|
||||
CHECK(subsystem.GetFailureCount() == 1);
|
||||
CHECK(subsystem.GetSolveSeconds() == report.solveSeconds);
|
||||
REQUIRE(subsystem.GetLastSolveReport().has_value());
|
||||
CHECK(subsystem.GetLastSolveReport()->trueResidualNorm == report.trueResidualNorm);
|
||||
rhs(0) = std::numeric_limits<double>::quiet_NaN();
|
||||
CHECK_THROWS_AS(subsystem.Solve(rhs, correction, control), std::invalid_argument);
|
||||
CHECK(subsystem.GetSolveCount() == 2);
|
||||
CHECK(subsystem.GetFailureCount() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Distributed gravity solves admit empty local partitions but reject "
|
||||
"a globally empty system",
|
||||
"[solver][gravity_subsystem]"
|
||||
) {
|
||||
using Catch::Approx;
|
||||
using namespace mean_field;
|
||||
int rank = 0;
|
||||
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
|
||||
const int localSize = rank == 0 ? 1 : 0;
|
||||
mfem::DenseMatrix matrix(localSize);
|
||||
if (localSize != 0) {
|
||||
matrix(0, 0) = 4.0;
|
||||
}
|
||||
PreparedDenseOperator operation(matrix);
|
||||
PreparedPermutationInverse inverse(operation);
|
||||
std::vector<int> indices = localSize != 0 ? std::vector<int>{0} : std::vector<int>{};
|
||||
solver::PreparedGravitySubsystem subsystem(
|
||||
operation, inverse, solver::GravityBlockPartition(localSize, localSize, indices, indices), MPI_COMM_WORLD
|
||||
);
|
||||
mfem::Vector rhs(localSize), correction(localSize);
|
||||
rhs = 12.0;
|
||||
correction = 0.0;
|
||||
const auto report = subsystem.Solve(rhs, correction, {});
|
||||
REQUIRE(report.Converged());
|
||||
CHECK(report.rightHandSideNorm == 12.0);
|
||||
if (localSize != 0) {
|
||||
CHECK(correction(0) == Approx(3.0));
|
||||
}
|
||||
|
||||
auto backend = solver::linear::prepareLinearBackend(
|
||||
solver::linear::FGMRES{{.restartLength = 13, .printLevel = -1}}, operation, inverse, MPI_COMM_WORLD
|
||||
);
|
||||
CHECK(backend.GetConfiguration().GetOptions().restartLength == 13);
|
||||
CHECK(backend.GetConfiguration().GetOptions().printLevel == -1);
|
||||
|
||||
PreparedDenseOperator emptyOperation(mfem::DenseMatrix(0));
|
||||
PreparedPermutationInverse emptyInverse(emptyOperation);
|
||||
CHECK_THROWS_AS(
|
||||
(solver::PreparedGravitySubsystem(
|
||||
emptyOperation, emptyInverse, solver::GravityBlockPartition(0, 0, {}, {}), MPI_COMM_WORLD
|
||||
)),
|
||||
std::invalid_argument
|
||||
);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Affine gravity receives no curvature correction even away from "
|
||||
"gravity equilibrium",
|
||||
"[solver][gravity_subsystem]"
|
||||
) {
|
||||
using namespace mean_field;
|
||||
mfem::DenseMatrix jacobian(2);
|
||||
jacobian(0, 0) = 2.0;
|
||||
jacobian(0, 1) = 3.0;
|
||||
jacobian(1, 0) = 1.0;
|
||||
jacobian(1, 1) = 1.0;
|
||||
PreparedDenseOperator operation(jacobian);
|
||||
PreparedPermutationInverse inverse(operation, {1, 0});
|
||||
solver::PreparedGravitySubsystem subsystem(
|
||||
operation, inverse, solver::GravityBlockPartition(2, 2, {1}, {0}), MPI_COMM_WORLD
|
||||
);
|
||||
solver::PreparedGravityCurvatureCorrection correction(subsystem);
|
||||
mfem::Vector baseResidual(2), direction(2), trialResidual(2), gravityStep(1);
|
||||
baseResidual(0) = 0.75;
|
||||
baseResidual(1) = 0.75;
|
||||
direction(0) = 0.125;
|
||||
direction(1) = 0.5;
|
||||
correction.CaptureBase(baseResidual, direction);
|
||||
const auto baseGeneration = correction.GetBaseGeneration();
|
||||
for (const double alpha : {1.0, 0.5, 0.25, 0.125, 0.0}) {
|
||||
operation.Prepare(jacobian);
|
||||
inverse.Refresh();
|
||||
subsystem.Refresh();
|
||||
trialResidual(0) = 0.75 + alpha * 1.75;
|
||||
trialResidual(1) = 0.75 + alpha * 0.625;
|
||||
const auto report = correction.SolveCorrection(alpha, trialResidual, gravityStep, {});
|
||||
REQUIRE(report.Converged());
|
||||
CHECK(report.rightHandSideNorm == 0.0);
|
||||
CHECK(gravityStep.Norml2() == 0.0);
|
||||
CHECK(correction.GetBaseGeneration() == baseGeneration);
|
||||
}
|
||||
correction.Invalidate();
|
||||
CHECK_FALSE(correction.HasBase());
|
||||
CHECK_THROWS_AS(correction.SolveCorrection(1.0, trialResidual, gravityStep, {}), std::logic_error);
|
||||
}
|
||||
|
||||
TEST_CASE(
|
||||
"Gravity curvature correction preserves the frozen tangent and is "
|
||||
"second order",
|
||||
"[solver][gravity_subsystem]"
|
||||
) {
|
||||
using Catch::Approx;
|
||||
using namespace mean_field;
|
||||
constexpr double baseQ = 0.4, baseZ = 1.2, deltaQ = 0.3, deltaZ = -0.2;
|
||||
PreparedDenseOperator operation(CurvedJacobian(baseQ, baseZ));
|
||||
PreparedPermutationInverse inverse(operation, {1, 0});
|
||||
solver::PreparedGravitySubsystem subsystem(
|
||||
operation, inverse, solver::GravityBlockPartition(2, 2, {1}, {0}), MPI_COMM_WORLD
|
||||
);
|
||||
solver::PreparedGravityCurvatureCorrection correction(subsystem);
|
||||
const auto baseResidual = CurvedResidual(baseQ, baseZ);
|
||||
mfem::Vector direction(2), action(2), gravityStep(1), candidate(2);
|
||||
direction(0) = deltaQ;
|
||||
direction(1) = deltaZ;
|
||||
operation.Mult(direction, action);
|
||||
correction.CaptureBase(baseResidual, direction);
|
||||
double previousStep = 0.0;
|
||||
for (const double alpha : {0.02, 0.01, 0.005, 0.0025}) {
|
||||
candidate(0) = baseQ + alpha * deltaQ;
|
||||
candidate(1) = baseZ + alpha * deltaZ;
|
||||
operation.Prepare(CurvedJacobian(candidate(0), candidate(1)));
|
||||
inverse.Refresh();
|
||||
subsystem.Refresh();
|
||||
const auto trialResidual = CurvedResidual(candidate(0), candidate(1));
|
||||
const auto report = correction.SolveCorrection(
|
||||
alpha, trialResidual, gravityStep, {.relativeTolerance = 1.0e-12, .maximumIterations = 4}
|
||||
);
|
||||
REQUIRE(report.Converged());
|
||||
subsystem.GetPartition().AddGravityValues(gravityStep, candidate);
|
||||
const auto correctedResidual = CurvedResidual(candidate(0), candidate(1));
|
||||
CHECK(correctedResidual(0) == Approx(baseResidual(0) + alpha * action(0)).margin(2.0e-15));
|
||||
CHECK(candidate(0) == baseQ + alpha * deltaQ);
|
||||
if (previousStep != 0.0) {
|
||||
CHECK(std::abs(gravityStep(0) / previousStep - 0.25) < 0.004);
|
||||
}
|
||||
previousStep = gravityStep(0);
|
||||
}
|
||||
CHECK(baseResidual(0) == CurvedResidual(baseQ, baseZ)(0));
|
||||
CHECK_THROWS_AS(correction.SolveCorrection(-1.0, baseResidual, gravityStep, {}), std::logic_error);
|
||||
}
|
||||
Reference in New Issue
Block a user