module; #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 gravityValues, std::vector 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(m_gravityValues.size()); } [[nodiscard]] int GravityResidualSize() const noexcept { return static_cast(m_gravityResiduals.size()); } [[nodiscard]] int RetainedValueSize() const noexcept { return static_cast(m_retainedValues.size()); } [[nodiscard]] int RetainedResidualSize() const noexcept { return static_cast(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(index)); } } private: [[nodiscard]] static std::vector Complement( const int size, const std::vector &selected ) { if (size < 0) { throw std::invalid_argument("A gravity partition requires non-negative root dimensions."); } std::vector used(static_cast(size), false); for (const int index : selected) { if (index < 0 || index >= size || used[static_cast(index)]) { throw std::invalid_argument("Gravity partition indices must be unique and in range."); } used[static_cast(index)] = true; } std::vector complement; complement.reserve(static_cast(size) - selected.size()); for (int index = 0; index < size; ++index) { if (!used[static_cast(index)]) { complement.push_back(index); } } return complement; } static void VerifySizes( const std::vector &indices, const int fullSize, const mfem::Vector &full, const mfem::Vector &compact ) { if (full.Size() != fullSize || compact.Size() != static_cast(indices.size())) { throw std::invalid_argument("Gravity partition vectors must have their preallocated sizes."); } const std::less 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 &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(index)) = full(indices[index]); } } static void Inject( const std::vector &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(index)); } } int m_rootValueSize; int m_rootResidualSize; std::vector m_gravityValues; std::vector m_gravityResiduals; std::vector m_retainedValues; std::vector m_retainedResiduals; }; template concept GravityPartitionForm = requires { typename Form::value_blocks; typename Form::residual_blocks; requires utils::blocks::block_form_is_valid_v
; requires utils::blocks::contains_type_v; requires utils::blocks::contains_type_v; 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 [[nodiscard]] GravityBlockPartition MakeGravityBlockPartition(const utils::blocks::form_layout &layout) { using namespace utils::blocks; std::vector values; std::vector residuals; const auto append = [](std::vector &indices, const int offset, const int size) { for (int index = 0; index < size; ++index) { indices.push_back(offset + index); } }; const auto value = [&]() { constexpr auto block = value_block>{}; append(values, layout.offset(block), layout.size(block)); }; const auto residual = [&]() { constexpr auto block = residual_block>{}; append(residuals, layout.offset(block), layout.size(block)); }; value.template operator()(); value.template operator()(); residual.template operator()(); residual.template operator()(); return { layout.value_offsets().Last(), layout.residual_offsets().Last(), std::move(values), std::move(residuals) }; } template concept GravitySubsystemOperation = std::derived_from && requires(const Operation &operation) { { operation.IsPrepared() } -> std::same_as; { operation.GetProblem().GetPreparationGeneration() } -> std::convertible_to; }; template concept GravitySubsystemPreconditioner = std::derived_from && LinearPreconditionerApplicationContractAvailable && requires(const Preconditioner &inverse) { { inverse.IsCurrent() } -> std::same_as; }; } // namespace mean_field::solver namespace mean_field::solver::detail { template 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 class GravityRestrictedInverse final : public mfem::Solver { public: static constexpr auto applicationContract = linearPreconditionerApplicationContract; GravityRestrictedInverse( const Preconditioner &inverse, const GravityRestrictedOperator &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 *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, detail::GravityRestrictedInverse> class PreparedGravitySubsystem final { public: using OperatorType = Operation; using PreconditionerType = Preconditioner; using RestrictedOperator = detail::GravityRestrictedOperator; using RestrictedInverse = detail::GravityRestrictedInverse; using BackendType = PreparedLinearBackendType; 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 &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(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 m_lastSolve; }; template concept GravitySubsystemRuntime = requires( Subsystem &subsystem, const Subsystem &constant, const mfem::Vector &rhs, mfem::Vector &correction, const LinearSolveControl &control ) { { constant.IsCurrent() } -> std::same_as; { constant.GetPartition() } -> std::same_as; { constant.GetCommunicator() } -> std::same_as; { subsystem.Solve(rhs, correction, control) } -> std::same_as; { constant.GetFullOperator().Mult(rhs, correction) } -> std::same_as; { constant.GetFullOperator().GetProblem().GetPreparationGeneration() } -> std::convertible_to; }; template 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