module; #include #include #include #include #include #include #include #include #include #include #include export module mean_field:normalization.operators; export import :normalization.physical_riesz; export namespace mean_field::normalization { class DiagonalNormalization final { public: DiagonalNormalization( mfem::Vector stateToNormalized, mfem::Vector residualToNormalized ) : m_stateToNormalized(std::move(stateToNormalized)), m_residualToNormalized(std::move(residualToNormalized)) { ValidateFactors(m_stateToNormalized, "state"); ValidateFactors(m_residualToNormalized, "residual"); } [[nodiscard]] static DiagonalNormalization Identity( const int stateSize, const int residualSize ) { if (stateSize < 0 || residualSize < 0) { throw std::invalid_argument("Normalization dimensions cannot be negative."); } mfem::Vector state(stateSize); mfem::Vector residual(residualSize); state = 1.0; residual = 1.0; return {std::move(state), std::move(residual)}; } [[nodiscard]] int StateSize() const noexcept { return m_stateToNormalized.Size(); } [[nodiscard]] int ResidualSize() const noexcept { return m_residualToNormalized.Size(); } [[nodiscard]] const mfem::Vector &StateFactors() const noexcept { return m_stateToNormalized; } [[nodiscard]] const mfem::Vector &ResidualFactors() const noexcept { return m_residualToNormalized; } void NormalizeState( const mfem::Vector &physical, mfem::Vector &normalized ) const { Apply(m_stateToNormalized, physical, normalized, false, "state"); } void DenormalizeState( const mfem::Vector &normalized, mfem::Vector &physical ) const { Apply(m_stateToNormalized, normalized, physical, true, "state"); } void NormalizeResidual( const mfem::Vector &physical, mfem::Vector &normalized ) const { Apply(m_residualToNormalized, physical, normalized, false, "residual"); } void DenormalizeResidual( const mfem::Vector &normalized, mfem::Vector &physical ) const { Apply(m_residualToNormalized, normalized, physical, true, "residual"); } [[nodiscard]] double LocalStateNormSquared(const mfem::Vector &physical) const { return LocalNormSquared(m_stateToNormalized, physical, "state"); } [[nodiscard]] double LocalResidualNormSquared(const mfem::Vector &physical) const { return LocalNormSquared(m_residualToNormalized, physical, "residual"); } private: static void ValidateFactors( const mfem::Vector &factors, const char *role ) { for (int index = 0; index < factors.Size(); ++index) { if (!std::isfinite(factors(index)) || factors(index) <= 0.0) { throw std::invalid_argument( std::string("The ") + role + " normalization factors must be finite and positive." ); } } } static void Apply( const mfem::Vector &factors, const mfem::Vector &input, mfem::Vector &output, const bool inverse, const char *role ) { if (input.Size() != factors.Size()) { throw std::invalid_argument(std::string("The ") + role + " vector has the wrong size."); } const bool exactAlias = input.GetData() == output.GetData() && input.Size() == output.Size(); if (!exactAlias) { output.SetSize(input.Size()); } for (int index = 0; index < input.Size(); ++index) { const double value = input(index); output(index) = inverse ? value / factors(index) : factors(index) * value; } } [[nodiscard]] static double LocalNormSquared( const mfem::Vector &factors, const mfem::Vector &physical, const char *role ) { if (physical.Size() != factors.Size()) { throw std::invalid_argument(std::string("The ") + role + " vector has the wrong size."); } double normSquared = 0.0; for (int index = 0; index < physical.Size(); ++index) { const double normalized = factors(index) * physical(index); normSquared += normalized * normalized; } return normSquared; } mfem::Vector m_stateToNormalized; mfem::Vector m_residualToNormalized; }; /* Detection-safe public operation for an ordinary third-party runtime * policy. The exact policy is recovered from the problem type and must own * every method in its compiled plan. Its implementation remains beside * the policy and is found by ADL, so adding a normalization family does * not edit a library registry or switch. */ template concept RuntimePreparedNormalizationOperation = requires(const std::remove_cvref_t &problem) { typename std::remove_cvref_t::NormalizationPrescriptionType; typename std::remove_cvref_t::FormType; requires RuntimePreparedNormalizationFor< typename std::remove_cvref_t::NormalizationPrescriptionType, typename std::remove_cvref_t::FormType>; { problem.GetNormalizationPrescription() } -> std::same_as::NormalizationPrescriptionType &>; { prepareStellarNormalization( problem.GetNormalizationPrescription(), problem) } -> std::same_as; }; template requires utils::blocks::block_form_is_valid_v
class DiagonalNormalizationBuilder final { public: explicit DiagonalNormalizationBuilder(const utils::blocks::form_layout &layout) : m_layout(&layout), m_stateFactors(layout.value_offsets().Last()), m_residualFactors(layout.residual_offsets().Last()) { } explicit DiagonalNormalizationBuilder( utils::blocks::form_layout && ) = delete; explicit DiagonalNormalizationBuilder( const utils::blocks::form_layout && ) = delete; template requires utils::blocks::contains_type_v void SetValueBlock( const double physicalScale, const mfem::Vector &primalGramDiagonal ) { constexpr int block = utils::blocks::type_index_v; RequireUnassigned(m_valueAssigned[block], "value"); AssignBlock( m_stateFactors, m_layout->value_offsets()[block], m_layout->value_offsets()[block + 1] - m_layout->value_offsets()[block], physicalScale, primalGramDiagonal, false ); m_valueAssigned[block] = true; } template requires utils::blocks::contains_type_v void SetResidualBlock( const double physicalScale, const mfem::Vector &primalGramDiagonal ) { constexpr int block = utils::blocks::type_index_v; RequireUnassigned(m_residualAssigned[block], "residual"); AssignBlock( m_residualFactors, m_layout->residual_offsets()[block], m_layout->residual_offsets()[block + 1] - m_layout->residual_offsets()[block], physicalScale, primalGramDiagonal, true ); m_residualAssigned[block] = true; } template requires utils::blocks::contains_type_v void SetValueGlobal(const double physicalScale) { constexpr int block = utils::blocks::type_index_v; SetConstantMetricValueBlock(physicalScale, BlockSize(m_layout->value_offsets(), block)); } template requires utils::blocks::contains_type_v void SetResidualGlobal(const double physicalScale) { constexpr int block = utils::blocks::type_index_v; mfem::Vector metric(BlockSize(m_layout->residual_offsets(), block)); metric = 1.0; SetResidualBlock(physicalScale, metric); } template requires utils::blocks::contains_type_v void SetHybridResidualBlock( const double physicalScale, const mfem::Vector &bulkPrimalGramDiagonal, const std::span pointRows, const double pointMetric = 1.0 ) { constexpr int block = utils::blocks::type_index_v; const int size = BlockSize(m_layout->residual_offsets(), block); if (bulkPrimalGramDiagonal.Size() != size) { throw std::invalid_argument("The hybrid residual Gram diagonal has the wrong size."); } ValidateMetric(pointMetric); std::vector isPointRow(static_cast(size), false); for (const int row : pointRows) { if (row < 0 || row >= size) { throw std::out_of_range("A hybrid point row lies outside its residual block."); } if (isPointRow[static_cast(row)]) { throw std::invalid_argument("A hybrid point row was supplied more than once."); } isPointRow[static_cast(row)] = true; } mfem::Vector metric(size); for (int row = 0; row < size; ++row) { metric(row) = isPointRow[static_cast(row)] ? pointMetric : bulkPrimalGramDiagonal(row); } SetResidualBlock(physicalScale, metric); } [[nodiscard]] DiagonalNormalization Build() && { for (const bool assigned : m_valueAssigned) { if (!assigned) { throw std::logic_error("The normalization is missing a value block."); } } for (const bool assigned : m_residualAssigned) { if (!assigned) { throw std::logic_error("The normalization is missing a residual block."); } } return {std::move(m_stateFactors), std::move(m_residualFactors)}; } private: template void SetConstantMetricValueBlock( const double physicalScale, const int size ) { mfem::Vector metric(size); metric = 1.0; SetValueBlock(physicalScale, metric); } [[nodiscard]] static int BlockSize( const mfem::Array &offsets, const int block ) noexcept { return offsets[block + 1] - offsets[block]; } static void RequireUnassigned( const bool assigned, const char *role ) { if (assigned) { throw std::logic_error(std::string("The ") + role + " block normalization was assigned twice."); } } static void ValidateMetric(const double metric) { if (!std::isfinite(metric) || metric <= 0.0) { throw std::invalid_argument("Every Riesz Gram diagonal entry must be finite and positive."); } } static void AssignBlock( mfem::Vector &factors, const int offset, const int size, const double physicalScale, const mfem::Vector &primalGramDiagonal, const bool dual ) { if (!std::isfinite(physicalScale) || physicalScale <= 0.0) { throw std::invalid_argument("A physical normalization scale must be finite and positive."); } if (primalGramDiagonal.Size() != size) { throw std::invalid_argument("A Riesz Gram diagonal has the wrong block size."); } for (int index = 0; index < size; ++index) { const double metric = primalGramDiagonal(index); ValidateMetric(metric); const double rieszFactor = std::sqrt(metric); const double factor = dual ? 1.0 / (physicalScale * rieszFactor) : rieszFactor / physicalScale; if (!std::isfinite(factor) || factor <= 0.0) { throw std::overflow_error("A normalization factor is not finite and positive."); } factors(offset + index) = factor; } } const utils::blocks::form_layout *m_layout; mfem::Vector m_stateFactors; mfem::Vector m_residualFactors; std::array m_valueAssigned{}; std::array m_residualAssigned{}; }; class ScaledJacobianOperator final : public mfem::Operator { public: ScaledJacobianOperator( const mfem::Operator &physicalJacobian, const DiagonalNormalization &normalization ) : mfem::Operator(normalization.ResidualSize(), normalization.StateSize()), m_physicalJacobian(&physicalJacobian), m_normalization(&normalization), m_physicalDirection(normalization.StateSize()), m_physicalAction(normalization.ResidualSize()) { if (physicalJacobian.Width() != normalization.StateSize() || physicalJacobian.Height() != normalization.ResidualSize()) { throw std::invalid_argument("The physical Jacobian and normalization dimensions do not agree."); } } ScaledJacobianOperator( mfem::Operator &&, const DiagonalNormalization & ) = delete; ScaledJacobianOperator( const mfem::Operator &&, const DiagonalNormalization & ) = delete; ScaledJacobianOperator( const mfem::Operator &, DiagonalNormalization && ) = delete; ScaledJacobianOperator( const mfem::Operator &, const DiagonalNormalization && ) = delete; void Mult( const mfem::Vector &normalizedDirection, mfem::Vector &normalizedAction ) const override { m_normalization->DenormalizeState(normalizedDirection, m_physicalDirection); m_physicalJacobian->Mult(m_physicalDirection, m_physicalAction); m_normalization->NormalizeResidual(m_physicalAction, normalizedAction); } private: const mfem::Operator *m_physicalJacobian; const DiagonalNormalization *m_normalization; mutable mfem::Vector m_physicalDirection; mutable mfem::Vector m_physicalAction; }; class ScaledInverseOperator final : public mfem::Operator { public: ScaledInverseOperator( const mfem::Operator &physicalInverse, const DiagonalNormalization &normalization ) : mfem::Operator(normalization.StateSize(), normalization.ResidualSize()), m_physicalInverse(&physicalInverse), m_normalization(&normalization), m_physicalResidual(normalization.ResidualSize()), m_physicalCorrection(normalization.StateSize()) { if (physicalInverse.Width() != normalization.ResidualSize() || physicalInverse.Height() != normalization.StateSize()) { throw std::invalid_argument("The physical inverse and normalization dimensions do not agree."); } } ScaledInverseOperator( mfem::Operator &&, const DiagonalNormalization & ) = delete; ScaledInverseOperator( const mfem::Operator &&, const DiagonalNormalization & ) = delete; ScaledInverseOperator( const mfem::Operator &, DiagonalNormalization && ) = delete; ScaledInverseOperator( const mfem::Operator &, const DiagonalNormalization && ) = delete; void Mult( const mfem::Vector &normalizedResidual, mfem::Vector &normalizedCorrection ) const override { m_normalization->DenormalizeResidual(normalizedResidual, m_physicalResidual); m_physicalInverse->Mult(m_physicalResidual, m_physicalCorrection); m_normalization->NormalizeState(m_physicalCorrection, normalizedCorrection); } private: const mfem::Operator *m_physicalInverse; const DiagonalNormalization *m_normalization; mutable mfem::Vector m_physicalResidual; mutable mfem::Vector m_physicalCorrection; }; struct ScaledPreconditionerStatistics final { std::uint64_t operatorBindings{0}; std::uint64_t applications{0}; }; /* * Solver-compatible realization of R^{-1} M^{-1} L^{-1}. The wrapped * inverse always sees the dimensional Jacobian, even when an MFEM Krylov * solver binds this object to the normalized Jacobian L J R. */ class ScaledPreconditioner final : public mfem::Solver { public: ScaledPreconditioner( mfem::Solver &physicalInverse, const mfem::Operator &physicalJacobian, const mfem::Operator &normalizedJacobian, const DiagonalNormalization &normalization ) : mfem::Solver( normalization.StateSize(), normalization.ResidualSize(), physicalInverse.iterative_mode ), m_physicalInverse(&physicalInverse), m_physicalJacobian(&physicalJacobian), m_expectedNormalizedJacobian(&normalizedJacobian), m_normalization(&normalization), m_physicalResidual(normalization.ResidualSize()), m_physicalCorrection(normalization.StateSize()) { if (physicalInverse.Width() != normalization.ResidualSize() || physicalInverse.Height() != normalization.StateSize() || physicalJacobian.Width() != normalization.StateSize() || physicalJacobian.Height() != normalization.ResidualSize()) { throw std::invalid_argument( "The physical preconditioner, Jacobian, and normalization dimensions do not agree." ); } SetOperator(normalizedJacobian); } ScaledPreconditioner( mfem::Solver &, mfem::Operator &&, const mfem::Operator &, const DiagonalNormalization & ) = delete; ScaledPreconditioner( mfem::Solver &, const mfem::Operator &&, const mfem::Operator &, const DiagonalNormalization & ) = delete; ScaledPreconditioner( mfem::Solver &, const mfem::Operator &, mfem::Operator &&, const DiagonalNormalization & ) = delete; ScaledPreconditioner( mfem::Solver &, const mfem::Operator &, const mfem::Operator &&, const DiagonalNormalization & ) = delete; ScaledPreconditioner( mfem::Solver &, const mfem::Operator &, const mfem::Operator &, DiagonalNormalization && ) = delete; ScaledPreconditioner( mfem::Solver &, const mfem::Operator &, const mfem::Operator &, const DiagonalNormalization && ) = delete; ScaledPreconditioner(const ScaledPreconditioner &) = delete; ScaledPreconditioner &operator=(const ScaledPreconditioner &) = delete; ScaledPreconditioner(ScaledPreconditioner &&) = delete; ScaledPreconditioner &operator=(ScaledPreconditioner &&) = delete; void SetOperator(const mfem::Operator &normalizedJacobian) override { if (normalizedJacobian.Width() != Width() || normalizedJacobian.Height() != Height()) { throw std::invalid_argument( "The scaled preconditioner received an incompatible normalized Jacobian." ); } if (&normalizedJacobian != m_expectedNormalizedJacobian) { throw std::invalid_argument( "The scaled preconditioner cannot be rebound to a different normalized Jacobian." ); } m_physicalInverse->SetOperator(*m_physicalJacobian); m_normalizedJacobian = &normalizedJacobian; ++m_statistics.operatorBindings; } void Mult( const mfem::Vector &normalizedResidual, mfem::Vector &normalizedCorrection ) const override { if (m_normalizedJacobian == nullptr) { throw std::logic_error("The scaled preconditioner has not been bound to a normalized Jacobian."); } if (normalizedResidual.Size() != Width() || normalizedCorrection.Size() != Height()) { throw std::invalid_argument( "The scaled preconditioner requires compatible, preallocated normalized vectors." ); } m_normalization->DenormalizeResidual(normalizedResidual, m_physicalResidual); m_physicalInverse->Mult(m_physicalResidual, m_physicalCorrection); m_normalization->NormalizeState(m_physicalCorrection, normalizedCorrection); ++m_statistics.applications; } [[nodiscard]] const mfem::Solver &GetPhysicalInverse() const noexcept { return *m_physicalInverse; } [[nodiscard]] const mfem::Operator &GetPhysicalJacobian() const noexcept { return *m_physicalJacobian; } [[nodiscard]] const mfem::Operator &GetNormalizedJacobian() const { if (m_normalizedJacobian == nullptr) { throw std::logic_error("The scaled preconditioner has not been bound to a normalized Jacobian."); } return *m_normalizedJacobian; } [[nodiscard]] const ScaledPreconditionerStatistics &GetStatistics() const noexcept { return m_statistics; } private: mfem::Solver *m_physicalInverse; const mfem::Operator *m_physicalJacobian; const mfem::Operator *m_expectedNormalizedJacobian; const mfem::Operator *m_normalizedJacobian{nullptr}; const DiagonalNormalization *m_normalization; mutable mfem::Vector m_physicalResidual; mutable mfem::Vector m_physicalCorrection; mutable ScaledPreconditionerStatistics m_statistics; }; } // namespace mean_field::normalization