feat(backend): Added backend FEM code (specifically MFEM backend)

Much of the code to do actual finite element computations lives in this backend. The idea here is that the physics is all seperate and more descriptive than the backend. Further, we could, in principal swap out to another finite element backend (e.g. deal.II) without messing up stuff outside of this backend. Note LLM's made signifigant contributions to this module

LLMs (ChatGPT6 astra max and Claude Opus 5.5 medium) made signifigant contributions to this module. All LLM written code has been read over and verified.
This commit is contained in:
2026-11-09 13:48:09 -05:00
parent e9a9fc7653
commit 811bbd9694
184 changed files with 21058 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
# MFEM backend
MFEM implementations live here and in the matching `src/lib/serif/backend/mfem` tree. Physics descriptions, equations, EOS relations, normalization rules, Newton acceptance, and continuation scheduling stay in their ordinary SERiF modules. The directory migration preserves physics-oriented namespaces; include paths identify the implementation backend. Helpers whose responsibility changed now follow their owner: `model::make_discretization`, `geometry::surface::AutomaticPressureSurface`, generic preconditioner diagonal regularization, and discretization element coordinates.
| Directory | Responsibility |
| --- | --- |
| `discretization` | Bind physical terms to MFEM spaces; own coefficient ordering, element orientation, basis evaluation, quadrature, traces, and MFEM mesh extraction. |
| `mapping` | Evaluate physical mappings and their variations from supplied reference geometry and current coefficients. |
| `geometry` | Adapt surface and deformation descriptions to MFEM resources, including pressure traces and boundary metrics. |
| `operators` | Integrate equation contributions and expose their linear actions, transposes, diagonals, and assembly. No EOS-specific pressure construction belongs here. |
| `model` | Connect the preceding adapters into the model's computational graph and publish owning evaluated systems. The deformation-to-equation bridge belongs here. |
| `solver` | Supply MFEM linear solvers, inverses, geometry admissibility, and stellar adapters to the common Newton and continuation algorithms. |
| `projection` | Project reusable radial profiles into MFEM finite-element spaces. |
| `shared` | Small backend facilities with multiple independent consumers: live mesh/space snapshots, boundary traversal, vector spans, and matrix/vector checks. |
## Ownership and reuse
The computational graph owns immutable prepared data, including fixed geometry at equation quadrature points. A mapper does not own a second mutable reference-geometry cache. Standalone point and face mapping evaluate their own reference geometry directly. Accepted systems retain their preparation owners and remain independent of subsequent candidate preparation.
MFEM resources are externally mutable. Snapshot policies distinguish topology, material/boundary labels, finite-element layout, and physical/logical coordinate revisions. Input factories capture current revisions; reusing stale input metadata is rejected. Notify MFEM of direct coordinate changes (for example with `NodesUpdated`) and rebuild the input context after changing its resources. Revision tracking cannot detect unreported writes to external coordinate storage.
A `DofMap` owns the meaning of reduced coefficients. Public coefficient access validates that meaning. Internal element traversals obtain a validated `Elements` view once, then retain element bounds, support, and vector-size checks without rescanning all mesh attributes for every element. Do not mutate the mesh or space during a traversal. Element orientation is implemented once in `discretization/element_coordinates`.
Surface normalization and surface preconditioning share validated boundary traversal, but keep their different integrands: vector radial/harmonic metric diagonals versus scalar H1 mass and stiffness matrices. Likewise, model block selection preserves native child assembly and repeated ranges; restriction of an arbitrary preconditioner operator retains its separate unique-index contract.
## Core boundary
`serif_core_lib` and `serif_core_dep` have no MFEM dependency. The core header check and the standard-vector Newton tests compile without MFEM include paths. The public `serif` library includes the core and adds this backend. Convenience entry headers may select MFEM adapters, while physical configuration and numerical algorithms remain available separately.
Continuation families, physical parameter laws, coordinate stepping, and the driver share `serif::solver::continuation` in `solver/continuation`. Its entry header is backend independent; only stellar correction wiring and finite-element state transfer live in `backend/mfem/solver/continuation`.
The common Newton loop asks its vector adapter only for owned vectors and coefficient spans. The MFEM wrapper supplies linear-solve wiring; it does not duplicate acceptance, backtracking, or checkpoint ownership. A future backend should implement its discretization and execution adapters and reuse these common rules, rather than copy the nonlinear or continuation algorithms.
Current term bindings require serial, conforming three-dimensional MFEM spaces. Supported value, gradient, divergence, trace, and tensor/dense basis paths retain their explicit compile-time and runtime checks. This layout does not imply that every MFEM space or another finite-element library is already supported.
## Optional surface density reconstruction
Pass the numerical choice directly to the stellar factory, after any deformation:
```cpp
namespace reconstruction = serif::discretization::reconstruction;
using Density = serif::discretization::blocks::density::mass;
auto finite_elements = serif::model::make_stellar_discretization(
star, mesh_options, moving_boundary,
reconstruction::SurfacePower<Density>{
.density_exponent = n,
.region = reconstruction::SurfaceAdjacent<
serif::discretization::domain::StellarSurfaceBoundary>{}});
```
Omitting the argument preserves ordinary interpolation. Exponent 1 takes that same path without a reconstruction plan. Stored coefficients remain physical nodal densities. Only material elements touching the selected boundary change: with inward reference coordinate `t=0` at the surface and `t=1` at their inner face, their zero-trace radial space becomes `t^n P_(p-1)`. Tangential interpolation and deeper elements remain ordinary. The full basis retains surface degrees of freedom and reproduces constants; the vacuum solve enforces their zero values explicitly.
The plan belongs to the field binding and is immutable throughout a solve. Its exponent is an explicit numerical choice; changing an EOS during continuation does not silently change the representation. A different profile, mesh or exponent requires a new binding and deliberate field transfer. Construction validates matching radial coordinates and the actual hanging-node constraints. Tangential/equatorial refinement is supported when those constraints preserve the reconstructed traces. Incompatible radial hanging layers fail with a SERiF diagnostic.
`discretization::backend::mfem::FieldValues<Term>` reads the same physical representation as the equation operators. Use it for arbitrary-point values, scalar H1 gradients or batches of samples. Its binding, coefficients and mesh resources must outlive the reader. Extra deformation/compactification maps must be supplied for physical gradients and H(div) values. Ordinary MFEM GridFunction interpolation of reconstructed nodal values is not the represented field.
Reference preparation replaces only the radial tensor factors; forward and transpose contractions retain the existing kernels. Automatic volume rules include a fractional-power allowance before quadrature-policy resolution, so an explicit fixed order remains authoritative. Gaussian quadrature is still approximate and should be checked by increasing its order for a new regime. Nonnegativity certifies the polynomial carrier with its positive weight and requires the selected zero-density trace. This implementation does not provide a certificate for finite-pressure surface lifting or a general EOS surface profile.
To add a numerical form, provide an ADL-visible `make_reconstruction(specification, binding)` overload, a specification with `using Term`, and an immutable `RadialProfile` that evaluates complete nodal functions and inward derivatives, including endpoints. The profile must preserve nodal values, constants and compatible face traces. Add its admissibility certificate and archive codec explicitly; missing capabilities report errors. No MFEM element subclass or physics-equation rewrite is required. The current surface-power form supports scalar H1 mass density only.
Weighted archive bindings use schema version 2; older readers reject them rather than reinterpret their coefficients. The live Rayfem adapter sends a higher-order sampled display field labeled `density (sampled)`. This display approximation is never used by the solver or TOP exporter.

View File

@@ -0,0 +1,260 @@
#pragma once
/**
* @file basis_evaluation.hpp
* @author Emily M. Boudreaux
* @brief Basis evaluation, pointwise mapping, and transpose application using dense or tensor-product representations.
*/
#include <memory>
#include "mfem/fem/fe/fe_base.hpp"
#include "mfem/fem/intrules.hpp"
#include "serif/backend/mfem/discretization/tensor_basis.hpp"
namespace serif::discretization::backend::mfem {
/**
* @brief Reusable scratch storage for basis contractions, reducing repeated allocations.
*/
struct BasisWorkspace {
TensorWorkspace tensor;
::mfem::Vector reference_values;
::mfem::Vector mapping_input;
::mfem::Vector mapping_output;
};
/**
* @class BasisEvaluation
* @brief Represents an element-local basis evaluation operator, supporting dense or tensor contractions, pointwise mappings, and transpose application.
*
* Consider a scalar field represented on one finite element:
*
* \f[
* \rho_h(\xi) = \sum_{j=1}^{n} c_j \phi_j(\xi).
* \f]
*
* Here, \f$c_{j}\f$ is a scalar coefficient, \f$\phi_{j}\f$ is its associated basis function,
* and \f$\xi\f$ is the canonical reference coordinate. The field is expressed in
* these coordinates through the element mapping.
*
* For a quadrature rule with \f$N_{q}\f$ points, define the evaluation matrix \f$\mathbf{E}\f$ by
*
* \f[
* E_{qj} = \phi_j(\xi_q).
* \f]
*
* \f$\mathbf{E}\f$ therefore has \f$N_{q}\f$ rows and \f$n\f$ columns. The field value at one point is
*
* \f[
* \rho_h(\xi_q) = \sum_{j=1}^{n} E_{qj} c_j.
* \f]
*
* Collecting the samples into a vector gives
*
* \f[
* \boldsymbol{\rho}_h = \mathbf{E}\mathbf{c}.
* \f]
*
* This class represents that linear operation without necessarily storing
* the full matrix \f$\mathbf{E}\f$: it can instead apply equivalent tensor contractions.
*
* @par Example
* Evaluate a constant scalar field on the reference cube, then compute
* its integral against each basis function.
*
* @code{.cpp}
* namespace backend = serif::discretization::backend::mfem;
*
* // First-order nodal H1 basis on the reference cube.
* ::mfem::H1_HexahedronElement element(1);
* const ::mfem::IntegrationRule& rule =
* ::mfem::IntRules.Get(::mfem::Geometry::CUBE, 2);
*
* backend::TensorBasisCache cache;
* backend::BasisWorkspace workspace;
* backend::BasisEvaluation basis;
*
* // Prepare E: one scalar basis value per quadrature point and DOF.
* basis.sample(
* element,
* rule,
* backend::ReferenceOperation::value,
* 1, // One copy of the scalar basis.
* cache,
* backend::ContractionMode::automatic
* );
*
* // For this nodal basis, equal coefficients represent a constant field.
* ::mfem::Vector coefficients(element.GetDof());
* coefficients = 2.0;
*
* // Compute values = E * coefficients.
* // Every quadrature-point value is approximately 2.0.
* ::mfem::Vector values;
* basis.evaluate(coefficients, values, workspace);
*
* // Supply quadrature weights explicitly: integrate does not add them.
* ::mfem::Vector weighted_values(rule.GetNPoints());
* for (int q = 0; q < rule.GetNPoints(); ++q) {
* weighted_values[q] = rule.IntPoint(q).weight * values[q];
* }
*
* // Compute element_integrals = E^T * weighted_values.
* ::mfem::Vector element_integrals;
* basis.integrate(weighted_values, element_integrals, workspace);
* @endcode
*
* Each output entry approximates
* \f[
* \int_{\hat K} \phi_j(\xi)\rho_h(\xi)\,d\xi.
* \f]
* These are element-DOF integral contributions, not reconstructed field
* coefficients. Integration over a physical element additionally requires
* the appropriate volume Jacobian in the quadrature weights.
*/
class BasisEvaluation final {
public:
/**
* @brief Prepare basis values or derivatives at quadrature points using a dense matrix or tensor-product representation.
* @param element Finite element to sample.
* @param rule Integration rule to use for sampling.
* @param operation Reference operation to perform.
* @param copies Number of independent copies of the finite-element basis; for example, three scalar basis copies for a vector H1 field.
* @param cache Tensor basis cache to use.
* @param mode Contraction mode to use.
*/
void sample(
const ::mfem::FiniteElement& element,
const ::mfem::IntegrationRule& rule,
ReferenceOperation operation,
int copies,
TensorBasisCache& cache,
ContractionMode mode,
const ElementReconstruction* reconstruction = nullptr
);
/**
* @brief Set the basis evaluation to a constant basis with the given number of points.
* @param points Number of quadrature points to use for the constant basis.
*/
void set_constant(
int points
);
/**
* @brief Allocate storage for a mapped basis using the reference's coefficient ordering and quadrature points.
* @param reference Basis evaluation supplying the dimensions and any shared tensor representation.
* @param components Number of output components per quadrature point.
*
* @note Populate the destination at every quadrature point using map_point before evaluation or transpose application.
*/
void reset_from(
const BasisEvaluation& reference,
int components
);
/**
* @brief Apply a pointwise linear map to the reference basis or its geometry variation.
* @tparam MapT Type of the mapping function to apply.
* @param point Quadrature point index to apply the mapping at.
* @param reference Reference basis evaluation to map from.
* @param map Mapping function to apply.
* @param workspace Reusable pointwise input and output buffers.
*
* @pre The mapping callback must be linear in its input and fill every output component.
*/
template <typename MapT>
void map_point(
int point,
const BasisEvaluation& reference,
MapT&& map,
BasisWorkspace& workspace
);
/**
* @brief Apply the basis to native coefficients; no integration weight is introduced.
* @param coefficients Input coefficients to apply the basis to.
* @param values Output values after applying the basis.
* @param workspace Workspace to use for intermediate computations.
*/
void evaluate(
const ::mfem::Vector& coefficients,
::mfem::Vector& values,
BasisWorkspace& workspace
) const;
/**
* @brief evaluate() for several coefficient vectors stored lane-interleaved (entry i of lane j at i * lanes + j).
* @note Each lane's values equal evaluate() on that lane alone; the output is interleaved the same way.
*/
void evaluate_many(
const ::mfem::Vector& coefficients,
::mfem::Vector& values,
int lanes,
BasisWorkspace& workspace
) const;
/**
* @brief Apply the coordinate transpose to already weighted quadrature duals.
* @param[in] values Quadrature data with any required integration weights already applied.
* @param[out] coefficients Element-DOF contributions computed as E^T times values.
* @param workspace Workspace to use for intermediate computations.
*/
void integrate(
const ::mfem::Vector& values,
::mfem::Vector& coefficients,
BasisWorkspace& workspace
) const;
/// Number of flattened quadrature samples.
[[nodiscard]] int height() const noexcept;
/// Number of native element coefficients.
[[nodiscard]] int width() const noexcept;
/// Report whether contractions use verified Cartesian factors.
[[nodiscard]] bool uses_tensor() const noexcept;
private:
int m_points{0}; ///< Number of quadrature points in the rule
int m_components{0}; ///< Number of output components per point
int m_width{0}; ///< Number of native element coefficients
bool m_identity_mapping{false}; ///< Canonical tensor samples need no pointwise mapping
std::shared_ptr<const TensorBasis> m_tensor; ///< Canonical tensor representation, when the tensor path is selected
::mfem::DenseMatrix m_dense; ///< Full evaluation matrix, including applied mappings, when the dense path is selected
::mfem::DenseMatrix m_mapping; ///< Stacked pointwise maps; canonical identity maps require no storage
};
} // namespace serif::discretization::backend::mfem
template <typename MapT>
void serif::discretization::backend::mfem::BasisEvaluation::map_point(const int point, const BasisEvaluation& reference, MapT&& map, BasisWorkspace& workspace) {
const bool canonical_reference = reference.m_identity_mapping;
if (m_identity_mapping) {
// Directly mapping a canonical destination must preserve the untouched points.
m_mapping.SetSize(m_points * m_components, m_components);
m_mapping = 0.0;
for (int q = 0; q < m_points; ++q) {
for (int component = 0; component < m_components; ++component) {
m_mapping(q * m_components + component, component) = 1.0;
}
}
}
m_identity_mapping = false;
const ::mfem::DenseMatrix& source = reference.m_tensor ? reference.m_mapping : reference.m_dense;
::mfem::DenseMatrix& destination = m_tensor ? m_mapping : m_dense;
const int columns = canonical_reference ? reference.m_components : source.Width();
::mfem::Vector& input = workspace.mapping_input;
::mfem::Vector& output = workspace.mapping_output;
input.SetSize(reference.m_components);
output.SetSize(m_components);
for (int column = 0; column < columns; ++column) {
for (int axis = 0; axis < reference.m_components; ++axis) {
input[axis] = canonical_reference ? static_cast<double>(axis == column) : source(point * reference.m_components + axis, column);
}
map(input, output);
for (int axis = 0; axis < m_components; ++axis) {
destination(point * m_components + axis, column) = output[axis];
}
}
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include <vector>
#include "mfem/mesh/mesh.hpp"
#include "mfem/mesh/ncmesh.hpp"
#include "serif/utils/error/errors.hpp"
namespace serif::discretization::backend::mfem {
/** @brief Expand a selected coarse interface label onto its physical leaf faces, integrating each leaf once. */
[[nodiscard]] inline std::vector<int> boundary_leaf_faces(::mfem::Mesh& mesh, const int attribute) {
using namespace utils::errors;
std::vector<int> labels(mesh.GetNumFaces(), 0);
for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) {
const int face = mesh.GetBdrElementFaceIndex(boundary);
const int label = mesh.GetBdrAttribute(boundary);
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(labels[face] == 0 || labels[face] == label,
"A weak boundary face has conflicting explicit boundary labels.");
labels[face] = label;
}
std::vector<int> child_count(mesh.GetNumFaces(), 0);
if (mesh.ncmesh != nullptr) {
const auto& faces = mesh.ncmesh->GetFaceList();
for (const auto& slave : faces.slaves) {
if (labels[slave.master] != attribute) { continue; }
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(labels[slave.index] == 0 || labels[slave.index] == attribute,
"A selected coarse boundary label conflicts with an explicit leaf label.");
labels[slave.index] = attribute;
++child_count[slave.master];
}
}
std::vector<int> result;
for (int face = 0; face < mesh.GetNumFaces(); ++face) {
if (labels[face] != attribute) { continue; }
if (mesh.GetFaceInformation(face).tag == ::mfem::Mesh::FaceInfoTag::MasterNonconforming) {
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(child_count[face] > 0,
"A selected nonconforming master face has no physical leaf faces.");
continue;
}
result.push_back(face);
}
return result;
}
}

View File

@@ -0,0 +1,95 @@
#pragma once
/**
* @file boundary_trace.hpp
* @author Emily M. Boudreaux
*
* @brief File containing the definition of the restricted dof space for a scalar boundary trace. Note in FEM a trace is a restriction on a function to the boundary of a domain. Therefore,
* This file contains the definitions for getting the DOFs at the boundary of a scalar field.
*/
#include <memory>
#include <utility>
#include "mfem/linalg/sparsemat.hpp"
#include "serif/backend/mfem/discretization/dof_selection.hpp"
namespace serif::discretization::backend::mfem
{
/**
* @brief Concept Limiting the input term to either a global scalar term or a spatial term of rank 0 in the H1 family. Outside of H1, the trace is a vector (e.g. the normal trace
* for a H(div) field or the tangential trace for a H(curl) field). Further, for higher
* rank H1 fields the trace is a tensor.
**/
template <typename TermT>
concept BoundaryTraceTerm = blocks::ScalarTraceTerm<TermT>;
/**
* @todo We have this forward definition here to avoid a circular dependency; however, a better refactor should be made here to avoid needing this forward declaration.
*/
class ScalarBoundaryTrace;
namespace detail
{
/** @brief Construct one interpolation operator in the selected residual-node ordering. */
[[nodiscard]] ScalarBoundaryTrace build_boundary_trace(std::shared_ptr<const DofSelection> rows, const DofMap& input, const ::mfem::FiniteElementSpace* input_space);
}
/**
* @brief An immutable scalar interpolation operator from supported coefficients to selected residual nodes.
* @note Both term bindings must outlive the trace. The sparse matrix and selection are shared immutable data;
* the same matrix supplies the forward evaluation and its exact coordinate transpose without quadrature weights.
*/
class ScalarBoundaryTrace final
{
public:
/** @brief Return the borrowed input coefficient binding. */
[[nodiscard]] const DofMap& binding() const noexcept;
/** @brief Return the owned selection defining the interpolation row ordering. */
[[nodiscard]] const DofSelection& rows() const noexcept;
/** @brief Return the interpolation matrix after checking its borrowed discretization. */
[[nodiscard]] const ::mfem::SparseMatrix& interpolation() const;
/** @brief Reject stale mesh topology, finite-element bindings, or domain and boundary labels. */
void validate() const;
private:
/** @brief Publish the completed matrix together with the bindings defining its coordinates. */
ScalarBoundaryTrace(std::shared_ptr<const DofSelection> rows, const DofMap& input, std::shared_ptr<const ::mfem::SparseMatrix> interpolation);
friend ScalarBoundaryTrace detail::build_boundary_trace(std::shared_ptr<const DofSelection>, const DofMap&, const ::mfem::FiniteElementSpace*);
std::shared_ptr<const DofSelection> m_rows;
const DofMap& m_binding;
std::shared_ptr<const ::mfem::SparseMatrix> m_interpolation;
};
/**
* @brief Sample a continuous scalar field at the selected nodal H1 residual points.
* @note Input and residual spaces may have different orders and numbering but must share a mesh.
* Discontinuous fields require an explicit physical trace convention and are intentionally excluded.
*/
template <BoundaryTraceTerm TermT, domain::schema::IsSchema SchemaT>
[[nodiscard]] ScalarBoundaryTrace make_boundary_trace(std::shared_ptr<const DofSelection> rows, const TermBinding<TermT, SchemaT>& input)
{
if constexpr (blocks::GlobalScalarTerm<TermT>)
{
return detail::build_boundary_trace(std::move(rows), input, nullptr);
}
else
{
return detail::build_boundary_trace(std::move(rows), input, &input.space());
}
}
/** @brief A trace borrows its input binding, so temporaries cannot supply its coefficient ordering. */
template <BoundaryTraceTerm TermT, domain::schema::IsSchema SchemaT>
ScalarBoundaryTrace make_boundary_trace(std::shared_ptr<const DofSelection>, TermBinding<TermT, SchemaT>&&) = delete;
/** @brief Const temporary bindings have the same lifetime restriction. */
template <BoundaryTraceTerm TermT, domain::schema::IsSchema SchemaT>
ScalarBoundaryTrace make_boundary_trace(std::shared_ptr<const DofSelection>, const TermBinding<TermT, SchemaT>&&) = delete;
}

View File

@@ -0,0 +1,117 @@
#pragma once
/**
* @file coefficient_tracking.hpp
* @brief Snapshot and comparison policy for MFEM coefficient views.
*/
#include <algorithm>
#include "mfem/linalg/vector.hpp"
namespace serif::discretization::backend::mfem {
/**
* @struct CoefficientTracking
* @brief Track coefficient values using an owned snapshot and the identity of their binding.
*
* A coefficient vector describes numerical values, while its binding
* determines how those values correspond to discretization degrees of freedom.
* Both pieces of information matter when deciding whether cached work can
* be reused.
*
* This policy provides three operations to the dependency system:
*
* - capture copies the current coefficient values into an owned snapshot.
* - matches compares current values against a previously captured snapshot.
* - identity identifies the persistent binding associated with the values.
*
* The vector's allocation is not part of its identity. A different vector
* containing equal coefficients can represent the same input when it uses
* the same binding. Conversely, equal coefficients associated with a
* different binding must not automatically reuse the previous calculation.
*
* This policy supplies comparisons; the dependency system decides which
* computations must run.
*
* @note Value comparison uses floating-point equality without a tolerance.
* It does not check whether the coefficients are finite or admissible;
* those checks belong to input validation.
*/
struct CoefficientTracking {
/**
* @brief Copy the current coefficient values into an independent snapshot.
*
* Subsequent changes to the source vector do not modify the returned
* snapshot. The entire supplied vector is copied; if the view contains
* only selected nodal coefficients, only those selected values are captured.
*
* @tparam ViewT View type exposing its coefficients through a values member.
* @param[in] view View of the current coefficients.
* @return An owned copy of the coefficient vector.
*/
template <typename ViewT>
[[nodiscard]] static ::mfem::Vector capture(const ViewT& view);
/**
* @brief Check whether the current coefficients equal a captured snapshot.
*
* The vectors must have the same size and equal corresponding entries.
* Two empty vectors match. Their storage addresses do not affect the
* comparison.
*
* This method compares values only. Binding identity is supplied
* separately by identity and checked by the dependency system.
*
* @tparam ViewT View type exposing its coefficients through a values member.
* @param[in] captured Previously captured coefficient values.
* @param[in] view View of the current coefficients.
* @return True when the sizes and all corresponding values are equal.
*/
template <typename ViewT>
[[nodiscard]] bool matches(
const ::mfem::Vector& captured,
const ViewT& view
) const;
/**
* @brief Identify the persistent binding associated with the coefficients.
*
* The returned address identifies the binding object, rather than the
* temporary view or the coefficient vector's storage. Creating another
* view of the same binding therefore preserves this identity.
*
* @tparam ViewT View type exposing its persistent binding through a binding member.
* @param[in] view View referring to the coefficient binding.
* @return The address of the binding object.
*
* @pre The binding member refers to an object whose lifetime and address
* remain valid while the dependency system relies on this identity.
*/
template <typename ViewT>
[[nodiscard]] static const void* identity(const ViewT& view);
};
} // namespace serif::discretization::backend::mfem
template <typename ViewT>
::mfem::Vector serif::discretization::backend::mfem::CoefficientTracking::capture(const ViewT& view) {
return ::mfem::Vector(view.values);
}
template <typename ViewT>
bool serif::discretization::backend::mfem::CoefficientTracking::matches(
const ::mfem::Vector& captured,
const ViewT& view
) const {
return captured.Size() == view.values.Size() &&
(captured.Size() == 0 ||
std::equal(
captured.GetData(),
captured.GetData() + captured.Size(),
view.values.GetData()
));
}
template <typename ViewT>
[[nodiscard]] const void* serif::discretization::backend::mfem::CoefficientTracking::identity(const ViewT& view) {
return &view.binding;
}

View File

@@ -0,0 +1,498 @@
#pragma once
/**
* @file dof_map.hpp
* @author Emily M. Boudreaux
* @brief This file contains the @ref DofMap which allows for the full
* dof vector to be projected into a reduced space of dofs on supported elements for some field Generally,
* while much of the code here may be exercised, you will rarely see a DofMap be directly invoked. Rather,
* these are generally implicitly constructed through their child class term_binding.
*/
#include <cmath>
#include <optional>
#include <memory>
#include "serif/backend/mfem/discretization/reconstruction.hpp"
#include <stdexcept>
#include "mfem/fem/fespace.hpp"
#include "mfem/fem/pfespace.hpp"
#include "serif/utils/error/errors.hpp"
#include "serif/utils/error/nonfinite.hpp"
namespace serif::discretization::backend::mfem {
class ElementCoordinates;
/**
* @brief A mapping from a full MFEM finite-element space to a reduced set of DOFs that
* are supporter by the given element support. That is to say that if one has some set of
* dofs D (perhaps it looks like [0, 1, 2, 3, 4] as a hypothetical) and dofs 0, 2, and 4 are
* associated to domain A while 1 and 3 are associated to domain B. Then if one were to
* construct a DofMap with element support [1, 0, 1, 0, 1] (where 1 indicates that the element
* is supported and 0 indicates that it is not) then the reduced dof map would be [0, 2, 4] and
* the full to reduced map would be [-1, -1, 0, -1, 1].
*
* @note Solver coordinates are supported independent (true) DOFs. Element coefficients
* include hanging-node interpolation. Coefficient restriction and dual accumulation
* are separate operations. Distributed spaces are not supported.
*/
class DofMap {
protected:
DofMap(
::mfem::FiniteElementSpace& space,
const ::mfem::Array<int>& element_support,
std::shared_ptr<const ReconstructionPlan> reconstruction = {}
) :
m_space(&space),
m_mesh(*space.GetMesh()),
m_space_sequence(space.GetSequence()),
m_mesh_sequence(space.GetMesh()->GetSequence()),
m_attributes(space.GetNE()),
m_element_support(element_support),
m_full_to_reduced(space.GetVSize()),
m_reconstruction(std::move(reconstruction)) {
using namespace utils::errors;
#ifdef MFEM_USE_MPI
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(dynamic_cast<::mfem::ParFiniteElementSpace*>(&space) == nullptr,"DofMap does not yet assemble parallel true DOFs.");
#endif
m_prolongation = space.GetConformingProlongation();
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(!constrained() || !space.IsVariableOrder(), "Constrained term bindings require fixed-order serial spaces; varying element order is not supported.");
serif_ensure<SERiFErrorCode::INVALID_DIMENSION, std::invalid_argument>(space.GetMesh()->Dimension() == 3 && space.GetMesh()->SpaceDimension() == 3,"Term bindings currently require a three-dimensional mesh.");
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(element_support.Size() == space.GetNE(), "Element support does not match the space.");
m_full_to_reduced = -1;
::mfem::Array<int> dofs;
for (int element = 0; element < space.GetNE(); ++element) {
m_attributes[element] = space.GetMesh()->GetAttribute(element);
if (element_support[element] == 0) {
continue;
}
space.GetElementVDofs(element, dofs);
for (const int dof : dofs) {
m_full_to_reduced[dof < 0 ? -1 - dof : dof] = 0;
}
}
if (m_reconstruction) {
m_boundary_attributes.emplace(m_mesh.GetNBE());
for (int boundary = 0; boundary < m_mesh.GetNBE(); ++boundary) {
(*m_boundary_attributes)[boundary] = m_mesh.GetBdrAttribute(boundary);
}
}
if (m_reconstruction) { m_reconstruction->validate_binding(*this); }
if (constrained()) {
build_constrained_map();
return;
}
for (int dof = 0; dof < space.GetVSize(); ++dof) {
if (m_full_to_reduced[dof] == 0) {
m_full_to_reduced[dof] = m_reduced_to_full.Size();
m_reduced_to_full.Append(dof);
}
}
}
DofMap(
::mfem::Mesh& mesh,
const ::mfem::Array<int>& element_support
) :
m_space(nullptr),
m_mesh(mesh),
m_space_sequence(-1),
m_mesh_sequence(mesh.GetSequence()),
m_attributes(mesh.GetNE()),
m_element_support(element_support),
m_reduced_to_full(1),
m_full_to_reduced(1) {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_DIMENSION, std::invalid_argument>(mesh.Dimension() == 3 && mesh.SpaceDimension() == 3, "Term bindings currently require a three-dimensional mesh.");
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(element_support.Size() == mesh.GetNE(), "Element support does not match the mesh.");
m_reduced_to_full[0] = m_full_to_reduced[0] = 0;
for (int element = 0; element < mesh.GetNE(); ++element) {
m_attributes[element] = mesh.GetAttribute(element);
}
}
~DofMap() = default;
public:
/**
* @brief Borrow element access for one traversal after checking the complete binding.
* @note Do not retain this view across mesh or space changes. Each element still checks
* its index, vector sizes, local attribute and space revision, without rescanning the mesh.
*/
class Elements final {
public:
/** @brief Extract supported coefficients in the native element's primal orientation. */
void element_values(int element, const ::mfem::Vector& reduced, ::mfem::Vector& values) const;
/** @brief Add native element dual coefficients to a full-space vector. */
void add_element(int element, ::mfem::Vector& values, ::mfem::Vector& full) const;
/** @brief Describe local coordinates for direct assembly or diagonal extraction. */
[[nodiscard]] ElementCoordinates coordinates(int element) const;
private:
friend class DofMap;
friend class ElementCoordinates;
/** @brief Only DofMap can create a view after validating all domain attributes. */
explicit Elements(const DofMap& binding) : m_binding(binding) {}
const DofMap& m_binding;
};
/** @brief Validate once before traversing elements without repeated mesh-wide scans. */
[[nodiscard]] Elements validated_elements() const {
validate();
return Elements(*this);
}
/** @brief Read the immutable physical-field representation retained by this binding. */
[[nodiscard]] const std::shared_ptr<const ReconstructionPlan>& reconstruction() const noexcept { return m_reconstruction; }
/** @brief Select the reference adapter without adding work to ordinary elements. */
[[nodiscard]] const ElementReconstruction* reconstruction_element(int element) const {
if (m_reconstruction == nullptr) { return nullptr; }
return m_reconstruction->element(element);
}
DofMap(const DofMap&) = delete;
DofMap& operator=(const DofMap&) = delete;
DofMap(DofMap&&) = delete;
DofMap& operator=(DofMap&&) = delete;
/**
* @brief Return the borrowed finite-element space associated with this DOF map.
* @return A reference to the finite-element space in m_space
*/
[[nodiscard]] ::mfem::FiniteElementSpace& space() const {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR, std::logic_error>(m_space != nullptr, "Global scalars do not have a finite-element space.");
return *m_space;
}
/**
* @brief Return the borrowed mesh associated with this DOF map.
* @return A reference to the mesh in m_mesh
*/
[[nodiscard]] ::mfem::Mesh& mesh() const noexcept { return m_mesh; }
/**
* @brief Return the number of full-space DOFs, including those not supported by the element support.
* @return The number of full-space DOFs (not reduced by element support)
*/
[[nodiscard]] int full_size() const noexcept { return m_full_to_reduced.Size(); }
/**
* @brief Return the number of supported DOFs, which is the size of the reduced coefficient vector.
* @return The number of supported DOFs (reduced by element support)
*/
[[nodiscard]] int size() const noexcept {
return m_reduced_to_full.Size();
}
/**
* @brief Locate a local coefficient represented by exactly one supported independent coordinate.
* @param full_dof index of the dof in the full space
* @return An optional index of the dof in the reduced space, or std::nullopt for an unsupported or weighted hanging coefficient.
*/
[[nodiscard]] std::optional<int> reduced_dof(const int full_dof) const {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::out_of_range>(full_dof >= 0 && full_dof < full_size(), "Full DOF index is out of range.");
const int reduced = m_full_to_reduced[full_dof];
return reduced < 0 ? std::nullopt : std::optional<int>{reduced};
}
/** @brief Return the representative local coefficient selected for one supported independent coordinate. */
[[nodiscard]] int full_dof(const int reduced) const {
using namespace utils::errors;
validate_structure();
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::out_of_range>(reduced >= 0 && reduced < size(),
"Supported DOF index {} is outside the binding of size {}.", reduced, size());
return m_reduced_to_full[reduced];
}
/** @brief Report whether local coefficients require hanging-node interpolation. */
[[nodiscard]] bool constrained() const noexcept { return m_prolongation != nullptr; }
/** @brief Return the full independent coordinate count before selecting material support. */
[[nodiscard]] int true_size() const noexcept { return m_space == nullptr ? 1 : m_space->GetTrueVSize(); }
/** @brief Visit the supported independent coefficients contributing to one unsigned local DOF. */
template <typename Visitor>
void for_each_coefficient(const int local_dof, Visitor&& visit) const {
using namespace utils::errors;
validate_structure();
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::out_of_range>(local_dof >= 0 && local_dof < full_size(), "Local DOF index is outside its finite-element space.");
if (!constrained()) {
const int index = m_full_to_reduced[local_dof];
if (index >= 0) {
visit(index, 1.0);
}
return;
}
const int* offsets = m_prolongation->GetI();
const int* columns = m_prolongation->GetJ();
const double* weights = m_prolongation->GetData();
for (int entry = offsets[local_dof]; entry < offsets[local_dof + 1]; ++entry) {
const int index = m_true_to_reduced[columns[entry]];
if (index >= 0 && weights[entry] != 0.0) {
visit(index, weights[entry]);
}
}
}
/** @brief Select supported coefficients from a full independent vector, without interpolation. */
[[nodiscard]] ::mfem::Vector gather_true(const ::mfem::Vector& values) const {
using namespace utils::errors;
validate();
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::invalid_argument>(values.Size() == true_size(),
"Independent coefficient vector has size {}, expected {}.", values.Size(), true_size());
if (!constrained()) {
return gather(values);
}
::mfem::Vector result(size());
for (int index = 0; index < size(); ++index) {
result[index] = values[m_reduced_to_true[index]];
}
return result;
}
/** @brief Expand supported coefficients into a full independent vector, without interpolation. */
[[nodiscard]] ::mfem::Vector scatter_true(const ::mfem::Vector& values) const {
validate_values(values);
if (!constrained()) {
return scatter(values);
}
::mfem::Vector result(true_size());
result = 0.0;
for (int index = 0; index < size(); ++index) {
result[m_reduced_to_true[index]] = values[index];
}
return result;
}
/** @brief Accumulate local dual contributions with the transpose of coefficient expansion. */
[[nodiscard]] ::mfem::Vector accumulate_dual(const ::mfem::Vector& full) const {
::mfem::Vector result;
accumulate_dual(full, result);
return result;
}
/** @brief Accumulate local dual contributions while reusing caller storage. */
void accumulate_dual(const ::mfem::Vector& full, ::mfem::Vector& result) const {
using namespace utils::errors;
validate();
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::invalid_argument>(full.Size() == full_size(),
"Local dual vector has size {}, expected {}.", full.Size(), full_size());
if (!constrained()) {
gather(full, result);
return;
}
::mfem::Vector independent(true_size());
m_prolongation->MultTranspose(full, independent);
result = gather_true(independent);
}
/**
* @brief Check whether an element is supported by this dof map
* @param element element index
* @return true if element is supported by this dof map, false otherwise
*/
[[nodiscard]] bool supports(const int element) const {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_ELEMENT_ID_ERROR, std::out_of_range>(element >= 0 && element < m_element_support.Size(), "Element index is out of range.");
return m_element_support[element] != 0;
}
/**
* @brief Validate the dof map
*/
void validate() const {
using namespace utils::errors;
validate_structure();
if (m_boundary_attributes) {
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR>(m_mesh.GetNBE() == m_boundary_attributes->Size(), "Rebuild reconstructed bindings after boundary changes.");
for (int boundary = 0; boundary < m_mesh.GetNBE(); ++boundary) {
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR>(m_mesh.GetBdrAttribute(boundary) == (*m_boundary_attributes)[boundary],
"Rebuild reconstructed bindings after boundary labels change.");
}
}
for (int element = 0; element < m_mesh.GetNE(); ++element) {
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR>(m_mesh.GetAttribute(element) == m_attributes[element],"Term binding domain attributes changed unexpectedly.");
}
}
/**
* @brief Check supported coefficient count and finiteness.
* @param values vector of coefficients to validate
*/
void validate_values(const ::mfem::Vector& values) const {
using namespace utils::errors;
validate();
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::invalid_argument>(values.Size() == size(), "Term binding size is {}, expected {} supported DOFs.", values.Size(), size());
for (int i = 0; i < values.Size(); ++i) {
serif_ensure<SERiFErrorCode::NON_FINITE_VALUE_ERROR, NonFiniteValue>(std::isfinite(values[i]), "Term binding contains a non-finite coefficient.");
}
}
/**
* @brief Restrict a full coefficient vector to supported DOFs.
* @param full full coefficient vector
* @return vector of supported coefficients
*/
[[nodiscard]] ::mfem::Vector gather(const ::mfem::Vector& full) const {
::mfem::Vector result;
gather(full, result);
return result;
}
/** @brief Restrict supported coefficients while reusing the destination allocation. */
void gather(const ::mfem::Vector& full, ::mfem::Vector& result) const {
using namespace utils::errors;
validate();
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::invalid_argument>(full.Size() == full_size(), "Full coefficient vector does not match its space.");
if (full.GetData() == result.GetData()) {
const ::mfem::Vector copy(full);
gather(copy, result);
return;
}
if (constrained()) {
::mfem::Vector independent(true_size());
m_space->GetConformingRestriction()->Mult(full, independent);
result = gather_true(independent);
return;
}
result.SetSize(size());
for (int index = 0; index < size(); ++index) {
result[index] = full[m_reduced_to_full[index]];
}
}
/**
* @brief Expand supported independent coefficients into the constrained local field.
* Unsupported independent coordinates are zero. Interpolation can populate local coordinates
* outside the supported elements; element support still controls physical integration.
* @param reduced vector of supported coefficients
* @return full local coefficient vector satisfying hanging-node interpolation
*/
[[nodiscard]] ::mfem::Vector scatter(const ::mfem::Vector& reduced) const {
using namespace utils::errors;
validate_values(reduced);
::mfem::Vector result(full_size());
if (constrained()) {
m_prolongation->Mult(scatter_true(reduced), result);
return result;
}
result = 0.0;
for (int i = 0; i < size(); ++i) {
result[m_reduced_to_full[i]] = reduced[i];
}
return result;
}
/**
* @brief Extract element coefficients, including primal orientation transforms.
* @param element element index
* @param reduced vector of supported coefficients
* @param values vector to store extracted element coefficients
*/
void element_values(int element, const ::mfem::Vector& reduced, ::mfem::Vector& values) const;
/**
* @brief Apply dual orientation transforms and add an element residual.
* @param element element index
* @param values vector of element coefficients
* @param full full coefficient vector to which the element residual will be added
*/
void add_element(int element, ::mfem::Vector& values, ::mfem::Vector& full) const;
private:
friend class ElementCoordinates;
/** @brief Close material support over hanging-node dependencies and retain independent coordinates. */
void build_constrained_map() {
using namespace utils::errors;
m_true_to_reduced.SetSize(true_size());
m_true_to_reduced = -1;
const int* offsets = m_prolongation->GetI();
const int* columns = m_prolongation->GetJ();
const double* weights = m_prolongation->GetData();
for (int local = 0; local < full_size(); ++local) {
if (m_full_to_reduced[local] < 0) {
continue;
}
for (int entry = offsets[local]; entry < offsets[local + 1]; ++entry) {
if (weights[entry] != 0.0) {
m_true_to_reduced[columns[entry]] = 0;
}
}
}
const ::mfem::SparseMatrix* restriction = m_space->GetConformingRestriction();
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(restriction != nullptr,
"A constrained serial space must provide its coefficient restriction.");
for (int independent = 0; independent < true_size(); ++independent) {
if (m_true_to_reduced[independent] < 0) {
continue;
}
const int begin = restriction->GetI()[independent];
const int end = restriction->GetI()[independent + 1];
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(end == begin + 1 && restriction->GetData()[begin] == 1.0,
"Fixed-order coefficient restriction must select one independent local DOF; coordinate {} has an unsupported restriction.", independent);
m_true_to_reduced[independent] = m_reduced_to_true.Size();
m_reduced_to_true.Append(independent);
m_reduced_to_full.Append(restriction->GetJ()[begin]);
}
m_full_to_reduced = -1;
for (int local = 0; local < full_size(); ++local) {
const int begin = offsets[local];
if (offsets[local + 1] == begin + 1 && weights[begin] == 1.0) {
m_full_to_reduced[local] = m_true_to_reduced[columns[begin]];
}
}
}
/** @brief Check borrowed resource revisions in constant time before accessing their arrays. */
void validate_structure() const {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR, std::logic_error>(m_mesh.GetSequence() == m_mesh_sequence && m_mesh.GetNE() == m_attributes.Size() && (m_space == nullptr || (m_space->GetMesh() == &m_mesh && m_space->GetSequence() == m_space_sequence && m_space->GetVSize() == full_size())), "Rebuild term bindings after discretization updates.");
}
::mfem::FiniteElementSpace* m_space; ///< Borrowed pointer to the finite-element space associated with this dof map, or nullptr for a global scalar. This must outlive this class
::mfem::Mesh& m_mesh; ///< Borrowed reference to the mesh associated with this dof map. This must outlive this class
long m_space_sequence; ///< Sequence number of the finite-element space at construction, or -1 for a global scalar
long m_mesh_sequence; ///< Sequence number of the mesh at construction
::mfem::Array<int> m_attributes; ///< Array of attribute indices associated with this dof map
::mfem::Array<int> m_element_support; ///< Array of 0/1 values indicating whether each element is supported by this dof map
const ::mfem::SparseMatrix* m_prolongation{nullptr}; ///< Borrowed constraint matrix, guarded by the space revision.
::mfem::Array<int> m_true_to_reduced; ///< Independent coordinate to supported coordinate.
::mfem::Array<int> m_reduced_to_true; ///< Supported coordinate to independent coordinate.
::mfem::Array<int> m_reduced_to_full; ///< Mapping from reduced to full degrees of freedom
::mfem::Array<int> m_full_to_reduced; ///< Mapping from full to reduced degrees of freedom
std::optional<::mfem::Array<int>> m_boundary_attributes; ///< Reconstructed bindings depend on boundary labels.
std::shared_ptr<const ReconstructionPlan> m_reconstruction; ///< Immutable; a different representation requires a new binding.
};
} // namespace serif::discretization::backend::mfem

View File

@@ -0,0 +1,158 @@
#pragma once
/**
* @file dof_selection.hpp
* @author Emily M. Boudreaux
* @brief This file contains the @ref DofSelection which allows for the selection of a subset of DOFs from a term binding, with boundary attribute information.
*/
#include "serif/backend/mfem/discretization/term_binding.hpp"
#include "serif/backend/mfem/discretization/dof_map.hpp"
namespace serif::discretization::backend::mfem {
/**
* @brief A selection of supported DOFs from a term binding, with boundary attribute information.
* The purpose here is to allow for the selection of a subset of DOFs from a term binding,
* such as those associated with a particular boundary, while still retaining the ability to
* validate the selection against the underlying mesh and term binding.
*/
class DofSelection final {
public:
/**
* @brief Construct a DOF selection from a term binding and a list of reduced DOFs.
* @param binding A reference to the term binding from which the DOFs are selected. Further, this binding must outlive the DOF selection.
* @param reduced_dofs A list of reduced DOFs to select. This list should be in the reduced space of the term binding. Further, these must be sorted in
* a strictly increasing order and must be unique. This is enforced at construction time.
*/
DofSelection(
const DofMap& binding,
const ::mfem::Array<int>& reduced_dofs,
const bool track_coordinates = false
) :
m_binding(binding),
m_dofs(reduced_dofs),
m_boundary_attributes(binding.mesh().GetNBE()),
m_nodes_sequence(track_coordinates ? binding.mesh().GetNodesSequence() : -1) {
using namespace utils::errors;
binding.validate();
int previous = -1;
for (const int dof : m_dofs) {
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::invalid_argument>(dof > previous && dof < binding.size(), "Selected DOFs must be sorted, unique, and inside the term binding.");
previous = dof;
}
for (int boundary = 0; boundary < m_boundary_attributes.Size(); ++boundary) {
m_boundary_attributes[boundary] = binding.mesh().GetBdrAttribute(boundary);
}
}
/**
* @brief Return the underlying term binding associated with this DOF selection.
* @return A reference to the underlying term binding.
*/
[[nodiscard]] const DofMap& binding() const noexcept { return m_binding; }
/**
* @brief Return the list of reduced DOFs in this selection.
* @return A reference to the list of reduced DOFs. Note these are in the reduced space of the binding provided at construction time.
*/
[[nodiscard]] const ::mfem::Array<int>& reduced_dofs() const noexcept { return m_dofs; }
/**
* @brief Return the number of selected DOFs in this selection.
* @return The number of selected DOFs.
*/
[[nodiscard]] int size() const noexcept { return m_dofs.Size(); }
/**
* @brief Return the list of boundary attributes associated with this selection.
* @return A reference to the list of boundary attributes. These are in the same order as the boundaries in the underlying mesh.
*/
[[nodiscard]] const ::mfem::Array<int>& boundary_attributes() const noexcept { return m_boundary_attributes; }
/**
* @brief Report whether this selection becomes stale when the fixed mesh coordinates change.
* Point constraints require this tracking even when a caller supplies the selected DOFs explicitly.
* @return True if coordinate updates invalidate the selection.
*/
[[nodiscard]] bool tracks_coordinates() const noexcept { return m_nodes_sequence >= 0; }
/**
* @brief Validate the DOF selection against the underlying term binding and mesh.
* This checks that the underlying term binding and mesh have not changed since the DOF selection was constructed.
* If they have changed, an exception is thrown. This is important to ensure that the DOF selection remains valid
* and consistent with the underlying data structures.
*/
void validate() const {
using namespace utils::errors;
m_binding.validate();
const auto& mesh = m_binding.mesh();
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR, std::logic_error>(m_nodes_sequence < 0 || mesh.GetNodesSequence() == m_nodes_sequence, "Rebuild point selections after fixed-mesh coordinate changes.");
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR, std::logic_error>(mesh.GetNBE() == m_boundary_attributes.Size(), "Rebuild DOF selections after boundary changes.");
for (int boundary = 0; boundary < m_boundary_attributes.Size(); ++boundary) {
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR, std::logic_error>(mesh.GetBdrAttribute(boundary) == m_boundary_attributes[boundary], "Rebuild DOF selections after boundary attributes change.");
}
}
private:
const DofMap& m_binding; ///< Reference to the underlying term binding. This must outlive the DOF selection.
::mfem::Array<int> m_dofs; ///< List of reduced DOFs in this selection. These are in the reduced space of the binding provided at construction time.
::mfem::Array<int> m_boundary_attributes; ///< List of boundary attributes associated with this selection. These are in the same order as the boundaries in the underlying mesh.
long m_nodes_sequence; ///< Coordinate revision, or -1 for selections independent of geometry.
};
/**
* @brief Select DOFs associated with a particular boundary from a term binding.
* @tparam BoundaryT The boundary type to select. This must be a valid boundary in the domain schema of the term binding.
* @tparam TermT The term type of the term binding. This must be a valid term type in the domain schema of the term binding. (e.g. something like Density)
* @tparam SchemaT The domain schema type of the term binding. This must be a valid domain schema type that contains the specified boundary.
* @param binding The term binding from which to select DOFs. This must outlive the returned DOF selection.
* @return A DofSelection object representing the selected DOFs associated with the specified boundary.
*/
template <domain::IsBoundary BoundaryT, blocks::SpatialTerm TermT, domain::schema::IsSchema SchemaT>
[[nodiscard]] DofSelection select_boundary_dofs(
const TermBinding<TermT, SchemaT>& binding
) {
using namespace utils::errors;
static_assert(SchemaT::template contains_boundary<BoundaryT>(), "The boundary must be registered in the domain schema.");
binding.validate();
::mfem::Array<int> marker(binding.size());
marker = 0;
::mfem::Array<int> dofs;
::mfem::Array<int> selected;
auto& space = binding.space();
const auto& mesh = binding.mesh();
for (int boundary = 0; boundary < mesh.GetNBE(); ++boundary) {
if (!SchemaT::template boundary_id_matches<BoundaryT>(mesh.GetBdrAttribute(boundary))) {
continue;
}
space.GetBdrElementVDofs(boundary, dofs);
for (const int encoded : dofs) {
bool supported = false;
binding.for_each_coefficient(::mfem::FiniteElementSpace::DecodeDof(encoded), [&](const int index, const double) {
marker[index] = 1;
supported = true;
});
serif_ensure<SERiFErrorCode::INVALID_DOMAIN_ERROR, std::invalid_argument>(supported, "A selected boundary DOF lies outside the term's support.");
}
}
::mfem::FiniteElementSpace::MarkerToList(marker, selected);
serif_ensure<SERiFErrorCode::INVALID_DOMAIN_ERROR, std::invalid_argument>(selected.Size() > 0, "The requested boundary has no DOFs on this serial mesh.");
return DofSelection(binding, selected);
}
} // namespace serif::discretization::backend::mfem

View File

@@ -0,0 +1,59 @@
#pragma once
#include <utility>
#include "mfem/fem/geom.hpp"
#include "mfem/fem/doftrans.hpp"
#include "serif/backend/mfem/discretization/dof_map.hpp"
namespace serif::discretization::backend::mfem {
/** @brief One element's native orientation and its supported full/reduced coefficient coordinates. */
class ElementCoordinates final {
public:
/** @brief Read local coordinates from a binding validated for the current traversal. */
ElementCoordinates(const DofMap::Elements& elements, int element);
/** @brief Return the number of native element coefficients. */
[[nodiscard]] int size() const noexcept { return m_dofs.Size(); }
/** @brief Return a supported coefficient index, or -1 outside the binding support. */
[[nodiscard]] int reduced_dof(int local) const;
/** @brief Visit independent weights for one native coefficient after dual orientation. */
template <typename Visitor>
void for_each_coefficient(const int local, Visitor&& visit) const {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::out_of_range>(local >= 0 && local < size(),
"Local element coefficient {} is outside the element of size {}.", local, size());
if (m_supported) {
m_binding.for_each_coefficient(::mfem::FiniteElementSpace::DecodeDof(m_dofs[local]), std::forward<Visitor>(visit));
}
}
/** @brief Locate a supported coefficient among this element's native coordinates. */
[[nodiscard]] int local_dof(int reduced) const;
/** @brief Express one native local basis direction, including its primal orientation.
* On constrained spaces, callers distribute its contribution through the hanging-node weights. */
void unit_direction(int column, ::mfem::Vector& direction) const;
/** @brief Restrict coefficients and apply their primal orientation. */
void element_values(const ::mfem::Vector& reduced, ::mfem::Vector& values) const;
/** @brief Convert integrated native coefficients to global dual orientation. */
void orient_dual(::mfem::Vector& values) const;
/** @brief Orient and add integrated coefficients into the full-space vector. */
void add_element(::mfem::Vector& values, ::mfem::Vector& full) const;
private:
/** @brief Apply MFEM's nontrivial primal transformation, when present. */
void orient_primal(::mfem::Vector& values) const;
const DofMap& m_binding;
bool m_supported;
::mfem::Array<int> m_dofs;
::mfem::DofTransformation m_transformation;
};
} // namespace serif::discretization::backend::mfem

View File

@@ -0,0 +1,120 @@
#pragma once
#include <concepts>
#include "serif/backend/mfem/discretization/term_coefficients.hpp"
#include "serif/backend/mfem/operators/operand_evaluation.hpp"
namespace serif::discretization::backend::mfem {
/**
* @brief Read a field using the same representation and mappings as its equation operators.
* @note This reader borrows its binding and coefficients. They must outlive it, and the mesh
* must not change during a sampling traversal. Each reader reuses scratch storage and is not
* shared between threads. Scalar VALUE fields need no extra map on a moving mesh; gradients
* require the overload accepting the additional SERiF mapping context.
*/
template <blocks::SpatialTerm Term>
class FieldValues final {
public:
using ValueOperand = forms::Operand<Term>;
using Value = forms::OperandValue<ValueOperand>;
/** @brief Borrow supported physical coefficients, including their hanging-node constraints. */
FieldValues(const DofMap& binding, const ::mfem::Vector& coefficients) :
m_binding(binding), m_coefficients(coefficients), m_elements(binding.validated_elements()), m_rule(1) {
using namespace utils::errors;
using Traits = blocks::SpatialTermTraits<Term>;
binding.validate_values(coefficients);
constexpr int copies = Traits::rank == 0 ? 1 : 3;
constexpr int continuity = Traits::family == functional_space::Family::h1 ? ::mfem::FiniteElementCollection::CONTINUOUS : ::mfem::FiniteElementCollection::DISCONTINUOUS;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(Traits::rank <= 1 &&
(Traits::family == functional_space::Family::h1 || Traits::family == functional_space::Family::l2),
"FieldValues supports scalar/vector H1 and L2 fields.");
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(binding.space().GetVDim() == copies &&
binding.space().FEColl()->GetContType() == continuity,
"FieldValues term family or rank disagrees with the supplied binding; pass the matching typed coefficient view.");
for (int element = 0; element < binding.space().GetNE(); ++element) {
const auto& finite_element = *binding.space().GetFE(element);
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(finite_element.GetOrder() == Traits::order &&
finite_element.GetRangeType() == ::mfem::FiniteElement::SCALAR &&
finite_element.GetMapType() == ::mfem::FiniteElement::VALUE,
"FieldValues term order or mapping disagrees with the supplied binding; pass the matching typed coefficient view.");
}
}
/** @brief Retain the field identity carried by a typed coefficient view. */
template <typename Binding> requires std::same_as<typename Binding::Term, Term>
explicit FieldValues(const operators::backend::mfem::CoefficientView<Binding>& field) :
FieldValues(field.binding, field.values) {}
FieldValues(const DofMap&, ::mfem::Vector&&) = delete;
FieldValues(const DofMap&, const ::mfem::Vector&&) = delete;
/** @brief Evaluate at a canonical element point, including the fixed MFEM mesh map. */
[[nodiscard]] Value value(int element, const ::mfem::IntegrationPoint& point) {
return sample<forms::Operation::Value>(element, point);
}
/**
* @brief Sample an element's points together, reusing tensor factors for repeated sampling rules.
* @note Vector components are consecutive within each point. As for value(), these samples
* include the fixed MFEM mesh map.
*/
void values(int element, const ::mfem::IntegrationRule& points, ::mfem::Vector& result) {
using Evaluator = operators::backend::mfem::OperandEvaluator<ValueOperand>;
m_elements.element_values(element, m_coefficients, m_local);
auto& transformation = *m_binding.mesh().GetElementTransformation(element);
Evaluator::sample_basis(m_binding, element, points, m_basis, m_cache,
ContractionMode::automatic, transformation, m_workspace);
Evaluator::evaluate(m_basis, m_local, result, m_workspace);
}
/** @brief Evaluate with the additional deformation or compactification map used by the solve. */
[[nodiscard]] Value value(int element, const ::mfem::IntegrationPoint& point, const mapping::MappingPointContext& context) {
using Evaluator = operators::backend::mfem::OperandEvaluator<ValueOperand>;
return Evaluator::physical_value(sample<forms::Operation::Value>(element, point), context);
}
/** @brief Return a scalar H1 field's gradient in the fixed computational mesh coordinates. */
[[nodiscard]] std::array<double, 3> gradient(int element, const ::mfem::IntegrationPoint& point)
requires (blocks::SpatialTermTraits<Term>::rank == 0 && blocks::SpatialTermTraits<Term>::family == functional_space::Family::h1) {
return sample<forms::Operation::Gradient>(element, point);
}
/** @brief Return a scalar H1 field's physical gradient after the additional SERiF map. */
[[nodiscard]] std::array<double, 3> gradient(int element, const ::mfem::IntegrationPoint& point, const mapping::MappingPointContext& context)
requires (blocks::SpatialTermTraits<Term>::rank == 0 && blocks::SpatialTermTraits<Term>::family == functional_space::Family::h1) {
using Operand = forms::Operand<Term, forms::Operation::Gradient>;
using Evaluator = operators::backend::mfem::OperandEvaluator<Operand>;
return Evaluator::physical_value(sample<forms::Operation::Gradient>(element, point), context);
}
private:
/** @brief Reuse the equation evaluator so diagnostic samples cannot bypass reconstruction. */
template <typename Operation>
[[nodiscard]] auto sample(int element, const ::mfem::IntegrationPoint& point) {
using Operand = forms::Operand<Term, Operation>;
using Evaluator = operators::backend::mfem::OperandEvaluator<Operand>;
m_elements.element_values(element, m_coefficients, m_local);
m_rule.IntPoint(0) = point;
auto& transformation = *m_binding.mesh().GetElementTransformation(element);
// Arbitrary one-point queries must not retain a separate tensor plan for every query.
Evaluator::sample_basis(m_binding, element, m_rule, m_basis, m_cache,
ContractionMode::dense, transformation, m_workspace);
Evaluator::evaluate(m_basis, m_local, m_values, m_workspace);
return Evaluator::reference_value(m_values, 0);
}
const DofMap& m_binding;
const ::mfem::Vector& m_coefficients;
DofMap::Elements m_elements;
::mfem::IntegrationRule m_rule;
::mfem::Vector m_local, m_values;
BasisEvaluation m_basis;
TensorBasisCache m_cache;
BasisWorkspace m_workspace;
};
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include <expected>
#include <string>
#include <vector>
#include "serif/backend/mfem/discretization/term_binding.hpp"
namespace serif::discretization::backend::mfem
{
namespace detail
{
/** @brief Assemble a supported reference mass diagonal for scalar or vector H1 copies. */
[[nodiscard]] std::expected<std::vector<double>, std::string> reference_mass_diagonal(const DofMap& binding);
}
/**
* @brief Measure the supported coefficients in their reference finite-element geometry.
* @return The exact mass-matrix diagonal in the binding's reduced ordering, or an invalid-metric diagnostic.
* @note The binding must remain current. Coordinates belong to the reference mesh, not a trial deformation.
*/
template <blocks::SpatialTerm Term, domain::schema::IsSchema Schema>
[[nodiscard]] std::expected<std::vector<double>, std::string> reference_mass_diagonal(const TermBinding<Term, Schema>& binding)
{
return detail::reference_mass_diagonal(binding);
}
}

View File

@@ -0,0 +1,44 @@
#pragma once
/**
* @file concepts.hpp
* @author Emily M. Boudreaux
*
* @brief File containing concepts for the quadrature backend module. These concepts are used to constrain template parameters in the quadrature backend module.
*/
#include "mfem/fem/intrules.hpp"
namespace serif::discretization::quadrature::backend::mfem {
/**
* @brief A HasReferenceSetIntegrationRule is an integrator that can be configured with a reference to an integration rule.
*/
template <typename IntegratorT>
concept HasReferenceSetIntegrationRule = requires (IntegratorT &integrator, const ::mfem::IntegrationRule &integration_rule) {
integrator.SetIntegrationRule(integration_rule);
};
/**
* @brief A HasPointerSetIntegrationRule is an integrator that can be configured with a pointer to an integration rule.
*/
template <typename IntegratorT>
concept HasPointerSetIntegrationRule = requires (IntegratorT &integrator, const ::mfem::IntegrationRule &integration_rule) {
integrator.SetIntRule(&integration_rule);
};
/**
* @brief A HasSetIntRule is an integrator that can be configured with an integration rule by value.
*/
template <typename IntegratorT>
concept HasSetIntRule = requires (IntegratorT &integrator, const ::mfem::IntegrationRule &integration_rule) {
integrator.SetIntRule(integration_rule);
};
/**
* @brief A ConfigurableIntegrator is an integrator that can be configured with an integration rule in some way (either by reference, by pointer, or by value).
*/
template <typename IntegratorT>
concept ConfigurableIntegrator = HasReferenceSetIntegrationRule<IntegratorT> ||
HasPointerSetIntegrationRule<IntegratorT> ||
HasSetIntRule<IntegratorT>;
}

View File

@@ -0,0 +1,32 @@
#pragma once
/**
* @file configure.hpp
* @author Emily M. Boudreaux
*
* @brief File containing utility functions for configuring MFEM integrators with integration rules.
*/
#include "mfem/fem/intrules.hpp"
#include "serif/backend/mfem/discretization/quadrature/concepts.hpp"
namespace serif::discretization::quadrature::backend::mfem {
/**
* @brief Configure an MFEM integrator with an integration rule. This function will call the appropriate SetIntegrationRule method on the integrator based on its capabilities.
* @tparam IntegratorT The type of the integrator to configure.
* @param integrator The integrator to configure.
* @param integration_rule The integration rule to use for configuration.
*/
template <ConfigurableIntegrator IntegratorT>
void set_integration_rule(IntegratorT &integrator, const ::mfem::IntegrationRule &integration_rule) {
if constexpr (HasReferenceSetIntegrationRule<IntegratorT>) {
integrator.SetIntegrationRule(integration_rule);
} else if constexpr (HasPointerSetIntegrationRule<IntegratorT>) {
integrator.SetIntRule(&integration_rule);
} else {
integrator.SetIntRule(integration_rule);
}
}
}

View File

@@ -0,0 +1,134 @@
#pragma once
/**
* @file mfem_resolver.hpp
* @author Emily M. Boudreaux
*
* @brief File containing the MFEMResolver class. This class is used to resolve the appropriate MFEM integration rule for a given quadrature context.
*
* @note We may want to consider renaming this file to runtime to stick to convention. The reason I did not initially is that this is more specific than pure runtime
* projection of compile time invariants (which is what most of the other files named runtime.hpp do). Something to think about.
*
* @note You will see ::mfem throughout. This is an important syntax to use here since MFEM's code lives in the mfem namespace; however, we also have code living in an
* mfem namespace. Outside of translation units that include using namespace serif::discretization::quadrature::backend and outside of the
* serif::discretization::quadrature::backend::mfem namespace itself these two things do not conflict. However, in those two cases there is a degeneracy the compiler
* cannot resolve between mfem:: and mfem::. Using ::mfem explicitly tells the compiler that we are looking from the root of the namespace tree not the current namespace.
*/
#include <span>
#include <format>
#include <unordered_map>
#include "mfem/fem/geom.hpp"
#include "mfem/fem/intrules.hpp"
#include "serif/discretization/quadrature/rules.hpp"
#include "serif/backend/mfem/discretization/quadrature/order.hpp"
#include "serif/discretization/domain/concepts.hpp"
#include "serif/discretization/quadrature/policy.hpp"
#include "serif/discretization/quadrature/query.hpp"
#include "serif/backend/mfem/discretization/quadrature/concepts.hpp"
#include "serif/backend/mfem/discretization/quadrature/configure.hpp"
#include "serif/backend/mfem/discretization/quadrature/mfem_static_order.hpp"
#include "serif/utils/format/strings.hpp"
namespace serif::discretization::quadrature::backend::mfem {
/**
* @brief The MFEMQuadratureContext struct is used to describe the context in which we are trying to resolve an MFEM integration rule. This includes the geometry type, the order of the geometry weight, any dynamic orders, the domain or set of domains, and the mapping kind.
*
* The idea here is that there is some information we can know at compile time (e.g. the role the particular integration is playing) and some we can only know
* at runtime (e.g. a manual quadrature boost requested by the user) so QuadratureContext contains all of that information which can then be turned into
* a single quadrature order. This is born out of the recognition that there is really not one good choice for quadrature order; rather,
* different systems have different accuracy requirements and should generally use the lowest order that is sufficient for their needs.
*/
struct MFEMQuadratureContext final {
QuadratureRole role = QuadratureRole::discretization; ///< The role of the quadrature rule.
::mfem::Geometry::Type geometry = ::mfem::Geometry::INVALID; ///< The geometry type of the element for which we are trying to resolve an MFEM integration rule.
int geometry_weight_order = 0; ///< The order of the geometry weight.
std::span<const int> dynamic_orders{}; ///< The dynamic orders of the quadrature rule. Dynamics orders come from runtime information.
domain::DomainOrSet domain = domain::AllDomains{}; ///< The domain or set of domains for which we are trying to resolve an MFEM integration rule.
MappingKind mapping = MappingKind::none; ///< The mapping kind of the quadrature rule.
};
/**
* @brief The MFEMRule struct is used to describe the result of resolving an MFEM integration rule. This includes the resolution (which contains the order and any other relevant information) and a pointer to the integration rule itself.
*/
struct MFEMRule final {
Resolution resolution{}; ///< The resolution of the MFEM integration rule, which contains the order and any other relevant information.
const ::mfem::IntegrationRule *integration_rule = nullptr; ///< A pointer to the MFEM integration rule itself.
};
/**
* @brief The MFEMResolver class is used to resolve the appropriate MFEM integration rule for a given quadrature context. It uses a policy to determine the appropriate order and other relevant information for the integration rule.
*/
class MFEMResolver final {
public:
/**
* @brief Constructs an MFEMResolver with the given policy.
*
* @param policy The policy used to determine the appropriate order and other relevant information for the integration rule.
*/
explicit MFEMResolver (Policy policy) noexcept : m_policy(std::move(policy)) {} // Do we actually want to use move rvalue semantics here?
/**
* @brief Resolves the appropriate MFEM integration rule for a given quadrature context and form type.
*
* @tparam FormT The form type for which we are trying to resolve an MFEM integration rule.
*
* @param context The quadrature context describing the situation in which we are trying to resolve an MFEM integration rule.
*
* @return An MFEMRule struct containing the resolution and a pointer to the MFEM integration rule itself.
*/
template <forms::Form FormT>
[[nodiscard]] MFEMRule resolve(const MFEMQuadratureContext& context) const {
if (context.geometry_weight_order < 0) {
utils::errors::serif_error<std::invalid_argument, utils::errors::SERiFErrorCode::INVALID_QUADRATURE_ORDER>("MFEM geometry weight order must be non-negative. Received an order of {}", context.geometry_weight_order);
}
const int dynamic_order = sum_dynamic_orders(context.dynamic_orders);
const int base_order = mfem_form_static_order<FormT> + dynamic_order + context.geometry_weight_order;
const Query query {
.form = forms::form_descriptor<FormT>,
.role = context.role,
.domain = context.domain,
.mapping = context.mapping,
.base_order = base_order
};
const Resolution resolution = m_policy.resolve(query);
const ::mfem::IntegrationRule &integration_rule = ::mfem::IntRules.Get(context.geometry, resolution.order);
return MFEMRule{.resolution = resolution, .integration_rule = &integration_rule};
}
/**
* @brief Configures an MFEM integrator with the appropriate integration rule for a given quadrature context and form type. This function will call the appropriate SetIntegrationRule method on the integrator based on its capabilities.
*
* @tparam FormT The form type for which we are trying to configure the integrator.
* @tparam IntegratorT The type of the integrator to configure.
*
* @param integrator The integrator to configure.
* @param context The quadrature context describing the situation in which we are trying to configure the integrator.
*
* @return The resolution of the MFEM integration rule used to configure the integrator.
*/
template <forms::Form FormT, ConfigurableIntegrator IntegratorT>
Resolution configure(IntegratorT &integrator, const MFEMQuadratureContext& context) const {
const MFEMRule rule = resolve<FormT>(context);
set_integration_rule(integrator, *rule.integration_rule);
return rule.resolution;
}
private:
Policy m_policy; ///< The policy used to determine the appropriate order and other relevant information for the integration rule.
};
}

View File

@@ -0,0 +1,39 @@
#pragma once
/**
* @file mfem_static_order.hpp
* @author Emily M. Boudreaux
*
* @brief File containing the mfem_form_static_order trait. This trait is used to determine the static order of an MFEM integrator for a given form type.
*/
#include "serif/discretization/blocks/concepts.hpp"
#include "serif/discretization/forms/operands.hpp"
#include "serif/discretization/forms/concept.hpp"
#include "serif/utils/types/type_list.hpp"
#include "serif/backend/mfem/discretization/quadrature/order.hpp"
namespace serif::discretization::quadrature::backend::mfem {
/**
* @brief A helper struct to compute the static order of an MFEM integrator for a given list of operands.
* @tparam OperandListT The list of operand types.
*/
template <typename OperandListT>
struct MFEMOperandListOrder;
/**
* @brief A specialization of the MFEMOperandListOrder struct for a list of operand types. This struct is used to compute the static order of an MFEM integrator for a given list of operands.
* @tparam OperandTs The operand types that make up the list.
*/
template <forms::IsOperand... OperandTs>
struct MFEMOperandListOrder<utils::types::TypeList<OperandTs...>> final {
static constexpr int value = (resolve_operand_order<OperandTs>() + ... + 0); ///< The static order of the MFEM integrator for the given list of operands. This is computed by summing the static orders of each operand in the list.
};
/**
* @brief A trait to determine the static order of an MFEM integrator for a given form type.
* @tparam FormT The form type to check.
*/
template <forms::Form FormT>
inline constexpr int mfem_form_static_order = MFEMOperandListOrder<typename FormT::operand_types>::value;
}

View File

@@ -0,0 +1,86 @@
#pragma once
/**
* @file order.hpp
* @author Emily M. Boudreaux
*
* @brief File containing the resolve_operand_order function and related utilities. These are used to determine the static order of an integrator for a given operand or form type.
*/
#include <type_traits>
#include "serif/discretization/functional_space/space.hpp"
#include "serif/discretization/blocks/traits.hpp"
#include "serif/discretization/forms/operands.hpp"
#include "serif/utils/error/errors.hpp"
namespace serif::discretization::quadrature::backend::mfem {
/**
* @brief A function to resolve the static order of an MFEM integrator for a given operand type.
*
* @tparam OperandT The operand type to resolve the static order for.
*
* @return The static order of the MFEM integrator for the given operand type.
*
* @todo We already have some of this information in the form_traits file, we should probably extract the order boost from there.
*/
template <forms::IsOperand OperandT>
[[nodiscard]] consteval int resolve_operand_order() noexcept {
using Operand = std::remove_cvref_t<OperandT>;
using Term = Operand::term_type;
using Operation = Operand::operation_type;
if constexpr (!blocks::HasFunctionalSpace<Term>) {
static_assert(std::same_as<Operation, forms::Operation::Value>, "Non-Spatial terms only support the value operation at the moment.");
return 0;
} else {
using Space = blocks::SpatialTermTraits<Term>;
constexpr auto family = Space::family;
constexpr int order = Space::order;
if constexpr (std::same_as<Operation, forms::Operation::Value>) {
return order;
} else if constexpr (std::same_as<Operation, forms::Operation::Gradient>) {
static_assert(family == functional_space::Family::h1, "Only H1 spaces support the gradient operation at the moment.");
return order > 0 ? order - 1 : 0;
} else if constexpr (std::same_as<Operation, forms::Operation::Divergence>) {
static_assert(Space::rank == 1 && family == functional_space::Family::h1, "Divergence requires a vector H1 term.");
return order > 0 ? order - 1 : 0;
} else if constexpr (std::same_as<Operation, forms::Operation::Curl>) {
static_assert(family == functional_space::Family::hcurl, "Only H(curl) spaces support the curl operation at the moment.");
return order > 0 ? order - 1 : 0;
} else if constexpr (std::same_as<Operation, forms::Operation::NormalTrace>) {
static_assert(family == functional_space::Family::hdiv, "Only H(div) spaces support the normal trace operation at the moment.");
return order;
} else {
static_assert(false, "Unsupported operation type. This indicates that an operation was used which is not currently registered in the operand configuration system. If you are a developer and you are seeing this error, please register your operation in the resolve_operand_order function in discretization/quadrature/detail.hpp. If you are a user and you are seeing this error, please report it to the SERiF developers.");
return 0;
}
}
}
/**
* @brief Sums the dynamic orders in a span of integers.
* @param dynamic_orders The span of dynamic orders to sum.
* @return The sum of the dynamic orders.
*/
[[nodiscard]] inline int sum_dynamic_orders(const std::span<const int> dynamic_orders) {
int total = 0;
for (const int order : dynamic_orders) {
if (order < 0) {
utils::errors::serif_error<std::invalid_argument, utils::errors::SERiFErrorCode::INVALID_QUADRATURE_ORDER>("Dynamic quadrature order cannot be negative. Received {}.", order);
}
total += order;
}
return total;
}
}

View File

@@ -0,0 +1,83 @@
#pragma once
#include <memory>
#include <vector>
#include "mfem/fem/fe/fe_base.hpp"
#include "serif/discretization/blocks/fields.hpp"
#include "serif/discretization/reconstruction/surface_power.hpp"
namespace mfem { class FiniteElementSpace; }
namespace serif::discretization::backend::mfem {
class DofMap;
/** @brief An element's radial reconstruction; the enclosing plan owns its profile. */
struct ElementReconstruction {
const reconstruction::RadialProfile* profile{nullptr};
int inward_axis{0};
bool surface_at_high_coordinate{false};
/** @brief Compare the mathematical reference operators for tensor-cache reuse. */
bool operator==(const ElementReconstruction&) const = default;
/** @brief Evaluate radial factors in MFEM's canonical coordinate and ordering. */
void radial(const ::mfem::FiniteElement& element, double coordinate, ::mfem::Vector& values, ::mfem::Vector& derivatives) const;
/** @brief Evaluate a dense reference oracle, retaining MFEM's native nodal ordering. */
void sample(const ::mfem::FiniteElement& element, const ::mfem::IntegrationPoint& point,
::mfem::Vector& values, ::mfem::DenseMatrix& derivatives) const;
};
/** @brief Immutable reconstruction selected on material-side surface elements and validated against their interfaces. */
class ReconstructionPlan final {
public:
/** @brief Bind a radial profile to one named boundary, rejecting incompatible element and constraint layouts. */
ReconstructionPlan(const DofMap& binding, int boundary_attribute, std::shared_ptr<const reconstruction::RadialProfile> profile);
/** @brief Prevent attaching a resolved element selection to a different finite-element space. */
void validate_binding(const DofMap& binding) const;
/** @brief Return no adapter on ordinary interior elements. */
[[nodiscard]] const ElementReconstruction* element(int index) const;
/** @brief Return the profile retained by accepted operators and state readers. */
[[nodiscard]] const reconstruction::RadialProfile& profile() const noexcept { return *m_profile; }
/** @brief Return the surface label needed to reproduce this plan on archive restoration. */
[[nodiscard]] int boundary_attribute() const noexcept { return m_boundary_attribute; }
/** @brief Overintegrate fractional radial powers; verify convergence when choosing a new profile. */
[[nodiscard]] int integration_order_increment() const noexcept { return 8; }
private:
/** @brief Require existing constrained coefficients to represent continuous reconstructed traces. */
void validate_interfaces(const DofMap& binding) const;
const ::mfem::FiniteElementSpace* m_space;
long m_space_sequence;
long m_mesh_sequence;
std::vector<int> m_domain_attributes, m_boundary_attributes, m_element_support;
int m_boundary_attribute;
std::shared_ptr<const reconstruction::RadialProfile> m_profile;
std::vector<ElementReconstruction> m_elements;
};
}
namespace serif::discretization::backend::mfem {
/**
* @brief Resolve a surface-power request against a typed binding before publishing the discretization.
* @note Other forms add an ADL-visible make_reconstruction(specification, binding) overload returning
* an immutable ReconstructionPlan. Unsupported fields must report a SERiF error in that overload.
*/
template <typename Term, typename Binding>
[[nodiscard]] std::shared_ptr<const ReconstructionPlan> make_reconstruction(
const reconstruction::SurfacePower<Term>& specification, const Binding& binding) {
using namespace utils::errors;
if constexpr (!std::same_as<Term, blocks::density::mass>) {
serif_error<std::invalid_argument, SERiFErrorCode::INVALID_ARGUMENT>("SurfacePower supports only scalar H1 mass density; the requested field has no validated reconstruction.");
return {};
} else {
int boundary_attribute = 0;
for (const auto& boundary : Binding::Schema::boundary_descriptors()) {
if (boundary.name == specification.region.boundary_name) { boundary_attribute = boundary.ID; }
}
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(boundary_attribute != 0,
"SurfacePower boundary '{}' is not registered in this discretization's domain schema.", specification.region.boundary_name);
auto profile = std::make_shared<const reconstruction::SurfacePowerProfile>(specification.density_exponent);
if (specification.density_exponent == 1.0) { return {}; }
return std::make_shared<const ReconstructionPlan>(binding, boundary_attribute, std::move(profile));
}
}
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include <memory>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
#include "mfem/fem/gridfunc.hpp"
#include "serif/backend/mfem/mapping/domain_mapper.hpp"
#include "serif/utils/dependencies/dependency_context.hpp"
#include "serif/utils/dependencies/key_list_operations.hpp"
namespace serif::discretization::backend::mfem {
/** @brief Own meshes and finite-element resources in reverse dependency order, for creation and archive restoration. */
struct Resources {
std::vector<std::unique_ptr<::mfem::Mesh>> meshes;
std::vector<std::unique_ptr<::mfem::FiniteElementCollection>> collections;
std::vector<std::unique_ptr<::mfem::FiniteElementSpace>> spaces;
std::unique_ptr<::mfem::GridFunction> compactification;
std::unique_ptr<mapping::DomainMapper> mapper;
};
template <typename Context, typename Keys = typename Context::Keys>
struct OwnedBindings;
/** @brief Keep typed bindings alive beside the resources they borrow; neither copying a configuration nor restoring it copies these resources. */
template <typename Context, typename... Keys>
struct OwnedBindings<Context, utils::dependencies::KeyList<Keys...>> {
template <typename Key>
using Binding = std::remove_cvref_t<decltype(std::declval<Context>().template get<Key>())>;
Resources resources;
std::tuple<std::unique_ptr<Binding<Keys>>...> bindings;
/** @brief Borrow each stable binding through the existing computational-graph input vocabulary. */
[[nodiscard]] auto context() const {
using namespace utils::dependencies;
return make_dependency_context(borrow<Keys>(*std::get<key_index<Keys, KeyList<Keys...>>()>(bindings))...);
}
};
}

View File

@@ -0,0 +1,26 @@
#pragma once
/**
* @file schema.hpp
* @author Emily M. Boudreaux
* @brief Adapt MFEM meshes to domain schema validation.
*/
#include "mfem/mesh/mesh.hpp"
#include "serif/backend/mfem/discretization/topology.hpp"
#include "serif/discretization/domain/concepts.hpp"
#include "serif/discretization/domain/schema/validation/all.hpp"
namespace serif::backend::mfem::discretization {
/**
* @brief Validate an MFEM mesh against a physical domain schema.
* @tparam SchemaT The schema type to validate against.
* @param mesh The MFEM mesh to validate.
* @return The validation result for every relation in the schema.
* @note Reuse make_mesh_topology when several schemas inspect the same mesh.
*/
template <::serif::discretization::domain::schema::IsSchema SchemaT>
[[nodiscard]] ::serif::discretization::domain::schema::validation::SchemaValidationResult validate_schema(const ::mfem::Mesh& mesh) {
return ::serif::discretization::domain::schema::validation::validate_schema<SchemaT>(make_mesh_topology(mesh));
}
}

View File

@@ -0,0 +1,539 @@
#pragma once
/**
* @file tensor_basis.hpp
* @author Emily M. Boudreaux
* @brief Implementation of fast tensor evaluation functions. These do not contribute physics but are used to accelerate evaluations.
*/
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <memory>
#include <vector>
#include <utility>
#include "mfem/fem/fe/fe_h1.hpp"
#include "mfem/fem/fe/fe_l2.hpp"
#include "serif/utils/error/errors.hpp"
#include "serif/backend/mfem/discretization/reconstruction.hpp"
namespace serif::discretization::backend::mfem {
/**
* @enum ReferenceOperation
* @brief Options enum to control what is actually evaluated at the reference points.
*/
enum class ReferenceOperation {
value, ///< Evaluate the basis functions at the reference points
gradient, ///< Evaluate the gradients of the basis functions at the reference points
value_and_gradient ///< Evaluate both the basis functions and their gradients at the reference points
};
/**
* @enum ContractionMode
* @brief Options enum to control how the tensor contractions are performed.
*/
enum class ContractionMode {
automatic, ///< Automatically choose the contraction mode based on the number of points and dofs
dense ///< Use a dense contraction mode
};
/**
* @struct TensorWorkspace
* @brief A workspace for tensor contractions to avoid repeated allocations and pack memory (TODO: does this actual pack memory in a useful manner when using mfem vectors? Probably something to look into).
*/
struct TensorWorkspace {
::mfem::Vector coefficients; ///< The coefficients of the basis functions at the reference points
::mfem::Vector first; ///< The first contraction of the coefficients with the basis functions at the reference points
::mfem::Vector second; ///< The second contraction of the coefficients with the basis functions at the reference points
};
/**
* @class TensorBasis
* @brief A class that represents a tensor basis for a finite element. This class is used to evaluate the basis functions and their derivatives at a set of reference
* points.
*/
class TensorBasis final {
/**
* @struct Product
* @brief A struct that represents a product of one-dimensional basis functions.
*/
struct Product {
std::array<::mfem::DenseMatrix, 3> factors; ///< The one-dimensional basis functions for each axis
::mfem::Array<int> dofs; ///< The dofs associated with the product
int output; ///< The output index for the product (used for mapping to the correct component of the output vector)
};
public:
/// Return no tensor plan unless its factorization agrees with MFEM's canonical basis.
/**
* @brief Make a tensor basis for a given finite element, integration rule, reference operation, and number of copies. This function returns a shared pointer to a TensorBasis object that can be used to do efficient evaluation of the basis functions and their derivatives at reference points.
* @param element The finite element for which to construct the tensor basis.
* @param rule The integration rule to use for evaluating the basis functions.
* @param operation The reference operation to apply (e.g., value, gradient).
* @param copies The number of copies of the basis functions.
* @returns A shared pointer to a TensorBasis object if the tensor basis can be constructed, or an empty shared pointer if it cannot.
*/
[[nodiscard]] static std::shared_ptr<const TensorBasis> make(
const ::mfem::FiniteElement& element,
const ::mfem::IntegrationRule& rule,
const ReferenceOperation operation,
const int copies,
const ElementReconstruction* reconstruction = nullptr
) {
const auto* scalar = dynamic_cast<const ::mfem::NodalTensorFiniteElement*>(&element);
const bool scalar_hex = dynamic_cast<const ::mfem::H1_HexahedronElement*>(&element) || dynamic_cast<const ::mfem::L2_HexahedronElement*>(&element);
// TODO: This is an awful lot of checks, compress these into some sort of "is this a valid tensor basis" function that can be used in the tests as well.
if (copies <= 0 || !scalar_hex || rule.GetNPoints() == 0 || scalar == nullptr || element.GetMapType() != ::mfem::FiniteElement::VALUE) {
return {};
}
const int q = static_cast<int>(std::lround(std::cbrt(rule.GetNPoints())));
if (q * q * q != rule.GetNPoints()) {
return {};
}
for (int z = 0; z < q; ++z) {
for (int y = 0; y < q; ++y) {
for (int x = 0; x < q; ++x) {
const auto& point = rule.IntPoint(x + q * (y + q * z));
if (point.x != rule.IntPoint(x).x || point.y != rule.IntPoint(q * y).y || point.z != rule.IntPoint(q * q * z).z) {
return {};
}
}
}
}
auto result = std::make_shared<TensorBasis>();
result->m_points = rule.GetNPoints();
result->m_dofs = element.GetDof() * copies;
const int samples = operation == ReferenceOperation::gradient ? 3 : operation == ReferenceOperation::value_and_gradient ? 4 : 1;
result->m_components = copies * samples;
const int closed = element.GetOrder() + 1;
const int block = element.GetDof();
const auto& native_map = scalar->GetDofMap();
if (native_map.Size() != 0 && native_map.Size() != element.GetDof()) {
return {};
}
// TODO: I once again make use of a vector<bool> since it is the most obvious conceptual datatype. However, the standard backed implementation of
// vector<bool> is not actually a vector of bools; rather, it is a bitset. I dont think this matters for what I am doing but is is probably
// worth being aware of this
std::vector<bool> seen(element.GetDof(), false);
for (int dof = 0; dof < element.GetDof(); ++dof) {
const int encoded = native_map.Size() ? native_map[dof] : dof;
const int native = encoded < 0 ? -1 - encoded : encoded;
if (encoded < 0 || native < 0 || native >= element.GetDof() || seen[native]) {
return {};
}
seen[native] = true;
}
for (int component = 0; component < copies; ++component) {
for (int sample = 0; sample < samples; ++sample) {
auto&[factors, dofs, output] = result->m_products.emplace_back();
output = component * samples + sample;
const int derivative = operation == ReferenceOperation::gradient ? sample : operation == ReferenceOperation::value_and_gradient ? sample - 1 : -1;
int tensor_dofs = 1;
for (int axis = 0; axis < 3; ++axis) {
const int count = closed;
tensor_dofs *= count;
::mfem::DenseMatrix& factor = factors[axis];
factor.SetSize(q, count);
::mfem::Vector values(count);
::mfem::Vector gradients(count);
const auto& basis = scalar->GetBasis1D();
for (int point = 0; point < q; ++point) {
const double coordinate = axis == 0 ? rule.IntPoint(point).x : axis == 1 ? rule.IntPoint(q * point).y : rule.IntPoint(q * q * point).z;
// Note that here this is just two overloads of Eval that mfem::Poly_1D provides, the first one evaluates the gradient and the latter evaluates the value.
if (reconstruction != nullptr && axis == reconstruction->inward_axis) {
reconstruction->radial(element, coordinate, values, gradients);
}
else if (axis == derivative) {
basis.Eval(coordinate, values, gradients);
}
else {
basis.Eval(coordinate, values);
}
for (int dof = 0; dof < count; ++dof) {
factor(point, dof) = axis == derivative ? gradients[dof] : values[dof];
}
}
}
if (tensor_dofs != block) {
return {};
}
dofs.SetSize(block);
for (int lex = 0; lex < block; ++lex) {
const int encoded = native_map.Size() ? native_map[lex] : lex;
dofs[lex] = encoded + component * element.GetDof();
}
}
}
return result->agrees_with(element, rule, operation, reconstruction) ? result : nullptr;
}
/**
* @brief Get the number of quadrature points in the Cartesian rule.
* @returns The number of quadrature points in the Cartesian rule.
*/
[[nodiscard]] int points() const noexcept {
return m_points;
}
/**
* @brief Get the number of degrees of freedom in the tensor basis.
* @returns The number of degrees of freedom in the tensor basis.
*/
[[nodiscard]] int dofs() const noexcept {
return m_dofs;
}
/**
* @brief Get the number of canonical output components per point in the tensor basis.
* @returns The number of canonical output components per point in the tensor basis.
*/
[[nodiscard]] int components() const noexcept {
return m_components;
}
/// Evaluate canonical samples, or their coordinate transpose, with caller-owned scratch.
/**
* @brief Apply the tensor basis to an input vector and store the results in an output vector. This function performs the tensor contraction of the input
* vector with the tensor basis.
* @param input The input vector to be contracted with the tensor basis.
* @param output The output vector to store the result of the contraction. (Owned by the caller)
* @param workspace The workspace to use for the contraction. (Owned by the caller)
* @param transpose Whether to transpose the contraction (default is false).
*
* @pre The input vector must have a size equal to the number of degrees of freedom in the tensor basis if transpose is false, or equal to the number of points times the number of components if transpose is true.
* @pre The output vector must be constructed
* @post The output vector will have a size equal to the number of points times the number of components if transpose is false, or equal to the number of degrees of freedom in the tensor basis if transpose is true.
* @post The output vector will contain the result of the tensor contraction of the input vector with the tensor basis.
*/
void apply(const ::mfem::Vector& input, ::mfem::Vector& output, TensorWorkspace& workspace, const bool transpose = false) const {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::invalid_argument>(input.Size() == (transpose ? m_points * m_components : m_dofs), "Tensor contraction input has the wrong coefficient or quadrature size.");
output.SetSize(transpose ? m_dofs : m_points * m_components);
output = 0.0;
for (const auto& product : m_products) {
auto& coefficients = workspace.coefficients;
coefficients.SetSize(transpose ? m_points : product.dofs.Size());
for (int i = 0; i < coefficients.Size(); ++i) {
const int encoded = transpose ? 0 : product.dofs[i];
coefficients[i] = transpose ? input[i * m_components + product.output] : encoded < 0 ? -input[-1 - encoded] : input[encoded];
}
std::array<int, 3> shape;
for (int axis = 0; axis < 3; ++axis) {
shape[axis] = transpose ? product.factors[axis].Height() : product.factors[axis].Width();
}
const ::mfem::Vector* source = &coefficients;
for (int step = 0; step < 3; ++step) {
const int axis = transpose ? 2 - step : step;
auto& destination = step % 2 == 0 ? workspace.first : workspace.second;
contract_axis(product.factors[axis], axis, shape, *source, destination, transpose);
shape[axis] = transpose ? product.factors[axis].Width() : product.factors[axis].Height();
source = &destination;
}
for (int i = 0; i < source->Size(); ++i) {
if (transpose) {
const int encoded = product.dofs[i];
output[encoded < 0 ? -1 - encoded : encoded] += (encoded < 0 ? -1.0 : 1.0) * (*source)[i];
} else {
output[i * m_components + product.output] += (*source)[i];
}
}
}
}
/**
* @brief apply() for several coefficient vectors at once, stored lane-interleaved (entry i of lane j at i * lanes + j).
* @note The lanes form the fastest tensor index, so every lane is contracted with exactly the operations and
* summation order apply() uses for it alone; only the loops are shared.
*/
void apply_many(const ::mfem::Vector& input, ::mfem::Vector& output, const int lanes, TensorWorkspace& workspace, const bool transpose = false) const {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::invalid_argument>(lanes > 0 && input.Size() == lanes * (transpose ? m_points * m_components : m_dofs), "Batched tensor contraction input has the wrong coefficient or quadrature size.");
output.SetSize(lanes * (transpose ? m_dofs : m_points * m_components));
output = 0.0;
for (const auto& product : m_products) {
auto& coefficients = workspace.coefficients;
const int entries = transpose ? m_points : product.dofs.Size();
coefficients.SetSize(entries * lanes);
for (int i = 0; i < entries; ++i) {
const int encoded = transpose ? 0 : product.dofs[i];
for (int lane = 0; lane < lanes; ++lane) {
coefficients[i * lanes + lane] = transpose ? input[(i * m_components + product.output) * lanes + lane] : encoded < 0 ? -input[(-1 - encoded) * lanes + lane] : input[encoded * lanes + lane];
}
}
std::array<int, 3> shape;
for (int axis = 0; axis < 3; ++axis) {
shape[axis] = transpose ? product.factors[axis].Height() : product.factors[axis].Width();
}
const ::mfem::Vector* source = &coefficients;
for (int step = 0; step < 3; ++step) {
const int axis = transpose ? 2 - step : step;
auto& destination = step % 2 == 0 ? workspace.first : workspace.second;
contract_axis(product.factors[axis], axis, shape, *source, destination, transpose, lanes);
shape[axis] = transpose ? product.factors[axis].Width() : product.factors[axis].Height();
source = &destination;
}
const int results = source->Size() / lanes;
for (int i = 0; i < results; ++i) {
for (int lane = 0; lane < lanes; ++lane) {
if (transpose) {
const int encoded = product.dofs[i];
output[(encoded < 0 ? -1 - encoded : encoded) * lanes + lane] += (encoded < 0 ? -1.0 : 1.0) * (*source)[i * lanes + lane];
} else {
output[(i * m_components + product.output) * lanes + lane] += (*source)[i * lanes + lane];
}
}
}
}
}
private:
/**
* @breif apply one factor along one tensor axis, teh other two indices will remain unchanged
* @param factor The one-dimensional basis function to apply along the specified axis.
* @param axis The axis along which to apply the factor (0, 1, or 2).
* @param shape The shape of the input tensor (number of points along each axis).
* @param input The input vector to be contracted with the factor.
* @param output The output vector to store the result of the contraction.
* @param transpose Whether to transpose the factor matrix before applying it.
* @param lanes Number of interleaved independent tensors (the fastest index); 1 for a single tensor.
*
* @pre The input vector must have a size equal to the product of the dimensions specified in the shape array.
* @post The output vector will have a size equal to the product of the dimensions specified in the next array.
* @post The output vector will contain the result of the contraction of the input vector with the factor along the specified axis.
*/
static void contract_axis(
const ::mfem::DenseMatrix& factor,
const int axis,
const std::array<int, 3>& shape,
const ::mfem::Vector& input,
::mfem::Vector& output,
const bool transpose,
const int lanes = 1
) {
const int input_extent = shape[axis];
const int output_extent = transpose ? factor.Width() : factor.Height();
int inner = lanes, outer = 1;
for (int dimension = 0; dimension < axis; ++dimension) {
inner *= shape[dimension];
}
for (int dimension = axis + 1; dimension < 3; ++dimension) {
outer *= shape[dimension];
}
output.SetSize(outer * output_extent * inner);
output = 0.0;
const int factor_height = factor.Height();
const double* factors = factor.GetData();
const double* source = input.GetData();
double* destination = output.GetData();
// Each tensor slice is contiguous. Every output still sums columns in ascending order.
for (int block = 0; block < outer; ++block) {
for (int row = 0; row < output_extent; ++row) {
double* output_slice = destination + (block * output_extent + row) * inner;
for (int column = 0; column < input_extent; ++column) {
const double coefficient = transpose ? factors[row * factor_height + column] : factors[column * factor_height + row];
const double* input_slice = source + (block * input_extent + column) * inner;
for (int entry = 0; entry < inner; ++entry) {
output_slice[entry] += coefficient * input_slice[entry];
}
}
}
}
}
/// Verify signed ordering and every requested basis entry before enabling the fast path.
/**
* @brief Verify that the signed ordering and every requested basis entry agrees with MFEM's canonical basis.
* @param element The finite element to verify against.
* @param rule The integration rule to use for evaluating the basis functions.
* @param operation The reference operation to apply (e.g., value, gradient).
*
* @returns True if the tensor basis agrees with MFEM's canonical basis, false otherwise.
*/
[[nodiscard]] bool agrees_with(
const ::mfem::FiniteElement& element,
const ::mfem::IntegrationRule& rule,
const ReferenceOperation operation,
const ElementReconstruction* reconstruction
) const {
const int samples = operation == ReferenceOperation::gradient ? 3 : operation == ReferenceOperation::value_and_gradient ? 4 : 1;
::mfem::Vector values(element.GetDof());
::mfem::DenseMatrix derivatives(element.GetDof(), 3);
for (int point = 0; point < m_points; ++point) {
if (reconstruction != nullptr) {
reconstruction->sample(element, rule.IntPoint(point), values, derivatives);
} else {
element.CalcShape(rule.IntPoint(point), values);
if (samples > 1) {
element.CalcDShape(rule.IntPoint(point), derivatives);
}
}
for (const auto&[factors, dofs, output] : m_products) {
const int q = factors[0].Height(), nx = factors[0].Width(), ny = factors[1].Width();
for (int dofID = 0; dofID < dofs.Size(); ++dofID) {
const int encoded = dofs[dofID], native = (encoded < 0 ? -1 - encoded : encoded) % element.GetDof();
const int derivative = operation == ReferenceOperation::gradient ? output % samples : output % samples - 1;
double expected = samples == 1 || derivative < 0 ? values[native] : derivatives(native, derivative);
expected *= encoded < 0 ? -1.0 : 1.0;
const double actual = factors[0](point % q, dofID % nx) * factors[1]((point / q) % q, (dofID / nx) % ny) * factors[2](point / (q * q), dofID / (nx * ny));
// Similar to above in this file, this is a long series of checks, maybe refactor into a function?
if (!std::isfinite(actual) || !std::isfinite(expected) || std::abs(actual - expected) > 64.0 * std::numeric_limits<double>::epsilon() * std::max(1.0, std::abs(expected))) {
return false;
}
}
}
}
return true;
}
int m_points{0}; ///< The number of quadrature points in the Cartesian rule.
int m_dofs{0}; ///< The number of degrees of freedom in the tensor basis.
int m_components{0}; ///< The number of canonical output components per point in the tensor basis.
std::vector<Product> m_products; ///< The products of one-dimensional basis functions for each axis. (TOOD: consider if this should be a heap allocated vector of it this could be put on the stack somehow)
};
/// Share immutable tensor plans within one operator; copied rules are keyed by their contents.
/**
* @class TensorBasisCache
* @brief A cache for TensorBasis objects to avoid repeated construction of the same tensor basis.
* This class stores a vector of "Entry" structs which each contain a non-owning pointer to some element,
* options, a shared pointer to a TensorBasis, and the points used to construct the TensorBasis.
*/
class TensorBasisCache final {
/**
* @struct Entry
* @brief A struct that represents an entry in the TensorBasisCache. Each entry contains a non-owning pointer to a finite element, the reference operation, the number of copies, the points used to construct the TensorBasis, and a shared pointer to the TensorBasis itself.
*
* @todo Consider if we can remove the shared pointer from here, shared pointers introduce overhead with reference counting and that may slow down the cache.
*/
struct Entry {
const ::mfem::FiniteElement* element;
ReferenceOperation operation;
int copies;
ElementReconstruction reconstruction;
std::vector<std::array<double, 4>> points;
std::shared_ptr<const TensorBasis> basis;
};
public:
/// Retrieve a verified plan, caching unsupported combinations as well.
/**
* @brief Retrieve a verified tensor basis, caching unsupported combinations as well.
* @param element The finite element for which to retrieve the tensor basis.
* @param rule The integration rule to use for evaluating the basis functions.
* @param operation The reference operation to apply (e.g., value, gradient).
* @param copies The number of copies of the basis functions.
* @returns A shared pointer to a TensorBasis object if the tensor basis can be constructed, or an empty shared pointer if it cannot.
*
* @todo I think it would make sense, at some point, to switch this to an std::optional return
*/
[[nodiscard]] std::shared_ptr<const TensorBasis> get(
const ::mfem::FiniteElement& element,
const ::mfem::IntegrationRule& rule,
ReferenceOperation operation,
const int copies,
const ElementReconstruction* reconstruction = nullptr
) {
std::vector<std::array<double, 4>> points;
for (int q = 0; q < rule.GetNPoints(); ++q) {
const auto& point = rule.IntPoint(q);
points.push_back({point.x, point.y, point.z, point.weight});
}
for (const auto& [el, op, cps, representation, pts, bs] : m_entries) {
if (el == &element && op == operation && cps == copies && representation == (reconstruction == nullptr ? ElementReconstruction{} : *reconstruction) && pts == points) {
return bs;
}
}
const auto basis = TensorBasis::make(element, rule, operation, copies, reconstruction);
m_entries.push_back({
.element = &element,
.operation = operation,
.copies = copies,
.reconstruction = reconstruction == nullptr ? ElementReconstruction{} : *reconstruction,
.points = std::move(points),
.basis = basis
});
return basis;
}
/**
* @brief Get the number of entries in the TensorBasisCache.
* @return The number of entries in the cache.
*/
[[nodiscard]] std::size_t size() const noexcept {
return m_entries.size();
}
/**
* @brief Clear the TensorBasisCache, removing all entries.
*/
void clear() {
m_entries.clear();
}
private:
std::vector<Entry> m_entries; ///< The entries in the TensorBasisCache, each containing a non-owning pointer to a finite element, the reference operation, the number of copies, the points used to construct the TensorBasis, and a shared pointer to the TensorBasis itself.
};
} // namespace serif::discretization::backend::mfem

View File

@@ -0,0 +1,116 @@
#pragma once
/**
* @file term_binding.hpp
* @author Emily M. Boudreaux
* @brief A runtime binding between some term descriptor and its MFEM finite-element space. This is used to project
* down the compile time term descriptors into runtime reinterpretations which can then be used to extract out things like
* the list of dofs, the element support, and the mesh.
*/
#include <algorithm>
#include "serif/discretization/blocks/traits.hpp"
#include "serif/discretization/domain/schema/concepts.hpp"
#include "serif/backend/mfem/discretization/dof_map.hpp"
#include "serif/utils/error/errors.hpp"
namespace serif::discretization::backend::mfem {
/**
* @brief A runtime binding between a term descriptor and its MFEM finite-element space. This is used to project
* down the compile time term descriptors into runtime reinterpretations which can then be used to extract
* things like the list of dofs, the element support, and the mesh. Note that this is a child of
* DofMap, therefor all term bindings are also dof maps
* @tparam TermT The term descriptor type to bind to. Note that if the FiniteElementSpace is not a global scalar then this must be a spatial term descriptor (@ref SpatialTerm)
* @tparam SchemaT The domain schema type to bind to. This must be a valid schema (@ref IsSchema)
*/
template <typename TermT, domain::schema::IsSchema SchemaT>
requires (blocks::SpatialTerm<TermT> || blocks::GlobalScalarTerm<TermT>)
class TermBinding final : public DofMap {
public:
using Term = TermT;
using Schema = SchemaT;
/**
* @brief Construct a term binding from an MFEM finite-element space. This requires that the term be a valid spatial term (@ref SpatialTerm)
* @param space The finite-element space to bind to.
*/
explicit TermBinding(
::mfem::FiniteElementSpace& space, std::shared_ptr<const ReconstructionPlan> reconstruction = {}
) requires blocks::SpatialTerm<Term> :
DofMap(space, element_support(checked_mesh(space)), std::move(reconstruction)) {
using Traits = blocks::SpatialTermTraits<Term>;
using namespace utils::errors;
static_assert(Traits::family == functional_space::Family::h1 || Traits::family == functional_space::Family::l2, "The MFEM backend currently only supports H1 and L2 spaces.");
static_assert(Traits::rank <= 1, "Tensor terms are not supported by this backend.");
if (this->reconstruction()) {
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(Traits::family == functional_space::Family::h1 && Traits::rank == 0,
"Radial reconstruction currently supports only scalar H1 fields.");
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(std::same_as<Term, blocks::density::mass> ||
dynamic_cast<const reconstruction::SurfacePowerProfile*>(&this->reconstruction()->profile()) == nullptr,
"SurfacePower has been validated only for mass density; select that field or implement a compatible reconstruction form.");
}
const int vector_dimension = Traits::rank == 0 ? 1 : 3;
const int continuity = Traits::family == functional_space::Family::h1 ? ::mfem::FiniteElementCollection::CONTINUOUS: ::mfem::FiniteElementCollection::DISCONTINUOUS;
serif_ensure<SERiFErrorCode::INVALID_DIMENSION, std::invalid_argument>(space.GetVDim() == vector_dimension,"Term vector dimension disagrees with its scalar-copy representation.");
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(space.FEColl()->GetContType() == continuity,"Term continuity disagrees with its descriptor.");
for (int element = 0; element < space.GetNE(); ++element) {
const auto* finite_element = space.GetFE(element);
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(
finite_element != nullptr && finite_element->GetOrder() == Traits::order &&
finite_element->GetRangeType() == ::mfem::FiniteElement::SCALAR &&
finite_element->GetMapType() == ::mfem::FiniteElement::VALUE,
"Term elements must match the registered family order, range, and mapping.");
}
}
/**
* @brief Construct a term binding from an MFEM mesh. This requires that the term be a valid global scalar term (@ref GlobalScalarTerm). E.g. the only way you
* can construct such a term binding is for things like the total mass, angular momentum, etc...
* @param mesh The mesh to bind to, note that the mesh must be three-dimensional and have a space dimension of three.
*/
explicit TermBinding(
::mfem::Mesh& mesh
) requires discretization::blocks::GlobalScalarTerm<Term> :
DofMap(mesh, element_support(mesh)) {}
private:
/**
* @brief Check that the mesh associated with a finite-element space is valid for term binding construction.
* @param space The finite-element space to check.
* @return The mesh associated with the finite-element space.
*/
static ::mfem::Mesh& checked_mesh(const ::mfem::FiniteElementSpace& space) {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR>(space.GetMesh() != nullptr && space.FEColl() != nullptr, "Term bindings require an initialized finite-element space.");
return *space.GetMesh();
}
/**
* @brief Translate the mesh into an element support array for the particular term binding
* @param mesh the mesh to translate into an element support array
* @return An array where each entry is 1 if the corresponding element is supported by the term binding and 0 otherwise. This array is parallel to the mesh elements ordering array.
*/
static ::mfem::Array<int> element_support(const ::mfem::Mesh& mesh) {
using namespace utils::errors;
constexpr auto domains = Schema::domain_descriptors();
::mfem::Array<int> result(mesh.GetNE());
auto ID_matches = [](const auto& domain, const int attribute) -> bool {
return domain.ID == attribute;
};
for (int element = 0; element < mesh.GetNE(); ++element) {
const int attribute = mesh.GetAttribute(element);
serif_ensure<SERiFErrorCode::INVALID_DOMAIN_ERROR>(std::ranges::any_of(domains, [&](const auto& domain) { return ID_matches(domain, attribute); }), "Element attribute does not match the registered domain.");
result[element] = Schema::template domain_id_belongs_to<typename Term::Support>(attribute);
}
return result;
}
};
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include "serif/utils/dependencies/dependency_context.hpp"
#include "serif/backend/mfem/discretization/term_binding.hpp"
namespace serif::operators::backend::mfem {
struct MappingCoefficients {
const ::mfem::FiniteElementSpace& binding;
const ::mfem::Vector& values;
};
template <typename BindingT>
struct CoefficientView {
using Term = BindingT::Term;
const BindingT& binding;
const ::mfem::Vector& values;
};
/// Borrow a term binding and its coefficients without extending their lifetimes.
template <typename BindingT>
[[nodiscard]] CoefficientView<BindingT> bind(const BindingT& binding, const ::mfem::Vector& values) {
return {binding, values};
}
template <typename BindingT>
void bind(const BindingT&, ::mfem::Vector&&) = delete;
template <typename BindingT>
void bind(const BindingT&, const ::mfem::Vector&&) = delete;
template <typename BindingT>
void bind(const BindingT&&, const ::mfem::Vector&) = delete;
/// Associate each physical term with its bound coefficients in an ordinary dependency context.
template <typename... BindingTs>
[[nodiscard]] auto make_term_coefficients(BindingTs... bindings) {
using namespace utils::dependencies;
static_assert(sizeof...(BindingTs) > 0, "Coefficient inputs must bind at least one term.");
return make_dependency_context(own<typename BindingTs::Term>(bindings)...);
}
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,12 @@
#pragma once
#include "serif/discretization/domain/mesh/topology.hpp"
namespace mfem {
class Mesh;
}
namespace serif::backend::mfem::discretization {
/** @brief Copy MFEM material labels and connectivity into backend-independent topology. */
[[nodiscard]] ::serif::discretization::domain::mesh::MeshTopology make_mesh_topology(const ::mfem::Mesh& mesh);
}

View File

@@ -0,0 +1,168 @@
#pragma once
/**
* @file computations.hpp
* @author Emily M. Boudreaux
*
* @brief File containing the factories to make the computation graph for the mfem backend of the geometry/deformation module.
*/
#include <algorithm>
#include <utility>
#include "serif/backend/mfem/geometry/deformation/constrained_radial_extension.hpp"
#include "serif/backend/mfem/geometry/deformation/inputs.hpp"
#include "serif/backend/mfem/geometry/deformation/radial_interpolation.hpp"
#include "serif/backend/mfem/geometry/deformation/surface_metric.hpp"
#include "serif/geometry/deformation/surface_embedding.hpp"
namespace serif::geometry::deformation::backend::mfem
{
//TODO: Some parts of this this detail namespace can be factored into implementation files as
// they are not templated. Keeping them as inline functions in a header may increase compile times
// for an already slow library.
namespace detail
{
/**
* @brief Share a successfully compiled immutable table without copying it into each consumer.
*
* @todo A shared pointer was the easiest way to implement this; however, this introduces some non-zero
* overhead related to reference counting. Generally, shared pointers should only be used when absolutely
* necessary and I have not given deep thought to if that is the case here (and more generally across the
* graph system).
*
* @param result The result of a computation that may be successful or may have failed.
*
* @returns If the inputted result is successful, returns a shared pointer to the result. If the inputted result is a failure, returns the same failure.
*/
template <typename Value>
[[nodiscard]] std::expected<std::shared_ptr<const Value>, DeformationFailure> share_reference_result(std::expected<Value, DeformationFailure> result)
{
if (!result) {
return std::unexpected(result.error());
}
return std::make_shared<const Value>(std::move(*result));
}
/**
* @brief Compile a radial interpolation from a reference discretization and set of material attributes.
*
* @param input The reference discretization containing the scalar and displacement finite element spaces, logical mesh, and material attributes.
* @param selected A vector of integers representing the selected material attributes for the radial interpolation.
*
* @returns If successful, returns a RadialInterpolation object representing the compiled radial interpolation. If there is an error, returns a DeformationFailure object describing the failure.
*
* @todo This is a holdover from the original MeanField code and its bespoke reference tracking system. It is likely that this can be removed in favor of SERiF's more robust dependencies graph system.
*
* Broadly the idea here (I am noticing in this doc writing process I say broadly a lot...) we loop over all elements in the mesh, determine if they have
* stellar or vacuum support
*/
[[nodiscard]] inline std::expected<RadialInterpolation, DeformationFailure> compile_radial_reference(
const ReferenceDiscretization& input,
const std::vector<int>& selected)
{
const ::mfem::Mesh* mesh = input.scalar_space.GetMesh();
if (!mesh) {
return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Radial deformation requires an initialized scalar reference mesh."});
}
std::vector<int> stellar_support(mesh->GetNE());
std::vector<int> vacuum_support(mesh->GetNE());
for (int element = 0; element < mesh->GetNE(); ++element) {
const int attribute = mesh->GetAttribute(element);
stellar_support[element] = std::ranges::find(input.stellar_attributes, attribute) != input.stellar_attributes.end();
vacuum_support[element] = std::ranges::find(input.vacuum_attributes, attribute) != input.vacuum_attributes.end();
if (stellar_support[element] == 0 && vacuum_support[element] == 0) {
return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "A volume element belongs to neither the stellar nor vacuum deformation support.", .element = element});
}
}
return build_radial_interpolation(
input.scalar_space,
input.displacement_space,
input.logical_mesh,
stellar_support,
vacuum_support,
input.surface_attribute,
input.infinity_attribute,
selected
);
}
} // namespace detail
/**
* @brief Optionally supply the reference surface mass diagonal in native shape coordinates.
* @note This reference metric is independent of shape coefficients and the volume extension; requesting it must not compile either.
*
*/
[[nodiscard]] inline auto make_surface_metric_computation()
{
using namespace utils::dependencies;
return make_computation<keys::SurfaceMetric, InputList<keys::SurfaceDiscretization, keys::ReferenceSurface, keys::SurfaceMap>>(
[](const SurfaceDiscretization& input, const std::shared_ptr<const ReferenceSurface>& reference, const std::shared_ptr<const SurfaceEmbedding>& surface) -> std::expected<std::vector<double>, DeformationFailure>
{
if (!reference || !surface)
{
return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_configuration, .message = "The shape metric requires compiled surface reference geometry and its embedding."});
}
return detail::surface_parameter_mass_diagonal(input.scalar_space, input.surface_attribute, *reference, *surface);
});
}
/**
* @brief Compile fixed reference factors and evaluate shape motion with one dependency engine.
* @note Configuration values are named context inputs. Factories declare work; try_prepare performs it explicitly.
*/
template <typename Surface>
requires requires(const Surface& model, std::span<const std::array<double, 3>> positions)
{
{ build_surface_embedding(model, positions) } -> std::same_as<std::expected<SurfaceEmbedding, DeformationFailure>>;
}
[[nodiscard]] auto make_deformation_computations()
{
using namespace utils::dependencies;
const auto surface_nodes = make_computation<keys::SurfaceNodes, InputList<keys::SurfaceDiscretization>>(
[](const SurfaceDiscretization& input)
{
return select_surface_scalar_dofs(input.scalar_space, input.surface_attribute);
});
const auto reference_surface = make_computation<keys::ReferenceSurface, InputList<keys::SurfaceDiscretization, keys::ReferenceCoordinates, keys::SurfaceNodes>>(
[](const SurfaceDiscretization& input, const ::mfem::FiniteElementSpace&, const std::vector<int>& selected)
{
return detail::share_reference_result(detail::sample_reference_surface(input.scalar_space, selected));
});
const auto radial_interpolation = make_computation<keys::RadialInterpolation, InputList<keys::ReferenceDiscretization, keys::SurfaceNodes>>(
[](const ReferenceDiscretization& input, const std::vector<int>& selected)
{
return detail::share_reference_result(detail::compile_radial_reference(input, selected));
});
const auto surface_map = make_computation<keys::SurfaceMap, InputList<keys::ReferenceSurface, keys::SurfaceModel>>(
[](const std::shared_ptr<const ReferenceSurface>& reference, const Surface& model)
{
return detail::share_reference_result(build_surface_embedding(model, reference->positions));
});
const auto extension = make_computation<keys::VolumeExtension, InputList<keys::RadialInterpolation, keys::ReferenceDiscretization, keys::InteriorModel, keys::VacuumModel>>(
[](const std::shared_ptr<const RadialInterpolation>& interpolation, const ReferenceDiscretization& reference, const PowerLawRadialExtension& interior, const FixedInfinityRadialExtension& vacuum)
{
return detail::share_reference_result(make_constrained_radial_extension(interpolation, reference.displacement_space, interior, vacuum));
});
return std::tuple_cat(std::tuple{surface_nodes, reference_surface, radial_interpolation, surface_map, extension}, deformation::make_deformation_computations<SurfaceEmbedding, ConstrainedRadialExtension>());
}
/** @brief Provide a standalone graph while leaving the same computations available for equation composition. */
template <typename Surface>
[[nodiscard]] auto make_deformation_graph()
{
return std::apply([](auto... computations)
{
return utils::dependencies::make_computation_graph(std::move(computations)...);
},
make_deformation_computations<Surface>());
}
} // namespace serif::geometry::deformation::backend::mfem

View File

@@ -0,0 +1,193 @@
#pragma once
#include <memory>
#include <span>
#include <utility>
#include "mfem/fem/fespace.hpp"
#ifdef MFEM_USE_MPI
#include "mfem/fem/pfespace.hpp"
#endif
#include "mfem/linalg/sparsemat.hpp"
#include "serif/geometry/deformation/radial_extension.hpp"
#include "serif/geometry/deformation/detail/action_storage.hpp"
namespace serif::geometry::deformation::backend::mfem {
/**
* @brief Keep radial displacement continuous across hanging faces in local component-block coordinates.
* @note The fixed map is P R E: radial sampling E, coefficient restriction R, and prolongation P.
* Its transpose is E^T R^T P^T. Owned matrices keep published maps independent of later mesh changes.
*/
class ConstrainedRadialExtension {
public:
using InputSpace = RadialExtension::InputSpace;
using OutputSpace = RadialExtension::OutputSpace;
static constexpr bool is_linear = true;
/** @brief Reserve separate local and independent vectors for each caller's actions. */
struct Workspace {
::mfem::Vector local;
::mfem::Vector independent;
};
/** @brief Expose the same immutable radial table as the unmodified physical extension. */
[[nodiscard]] const std::shared_ptr<const RadialInterpolation>& interpolation() const noexcept {
return m_radial.interpolation();
}
/** @brief Expose the regional radial response before imposing finite-element continuity. */
[[nodiscard]] std::span<const double> radial_weights() const noexcept {
return m_radial.radial_weights();
}
/** @brief Return the interleaved Cartesian surface displacement size. */
[[nodiscard]] std::size_t input_size() const noexcept {
return m_radial.input_size();
}
/** @brief Retain local component-block output for the existing deformation consumers. */
[[nodiscard]] std::size_t output_size() const noexcept {
return m_radial.output_size();
}
/** @brief Allocate constraint scratch once, leaving conforming actions allocation-free. */
[[nodiscard]] Workspace make_workspace() const;
/** @brief Apply radial motion with temporary constraint storage, preserving the two-argument calling form. */
[[nodiscard]] DeformationStatus apply(std::span<const double> surface, std::span<double> volume) const;
/** @brief Pull back volume work with temporary constraint storage, preserving the two-argument calling form. */
[[nodiscard]] DeformationStatus apply_transpose(std::span<const double> volume_force, std::span<double> surface_force) const;
/** @brief Sample radial motion and reconstruct its hanging coefficients from independent values. */
[[nodiscard]] DeformationStatus apply(std::span<const double> surface, std::span<double> volume, Workspace& workspace) const;
/** @brief Pull back volume work through the exact transpose of the constrained extension. */
[[nodiscard]] DeformationStatus apply_transpose(std::span<const double> volume_force, std::span<double> surface_force, Workspace& workspace) const;
private:
/** @brief Retain validated radial physics and optional owned finite-element constraints. */
ConstrainedRadialExtension(RadialExtension radial, std::unique_ptr<const ::mfem::SparseMatrix> prolongation, std::unique_ptr<const ::mfem::SparseMatrix> restriction);
/** @brief Reject scratch from another map or buffers that alias caller-owned action data. */
[[nodiscard]] DeformationStatus validate_workspace(const Workspace& workspace, std::span<const double> input, std::span<double> output) const;
friend std::expected<ConstrainedRadialExtension, DeformationFailure> make_constrained_radial_extension(
std::shared_ptr<const RadialInterpolation>, const ::mfem::FiniteElementSpace&, PowerLawRadialExtension, FixedInfinityRadialExtension);
RadialExtension m_radial;
std::unique_ptr<const ::mfem::SparseMatrix> m_prolongation;
std::unique_ptr<const ::mfem::SparseMatrix> m_restriction;
};
/** @brief Compile the original radial extension and the matching serial component-block constraints. */
[[nodiscard]] std::expected<ConstrainedRadialExtension, DeformationFailure> make_constrained_radial_extension(
std::shared_ptr<const RadialInterpolation> interpolation,
const ::mfem::FiniteElementSpace& displacement_space,
PowerLawRadialExtension interior = {},
FixedInfinityRadialExtension vacuum = {});
} // namespace serif::geometry::deformation::backend::mfem
namespace serif::geometry::deformation::backend::mfem {
inline ConstrainedRadialExtension::ConstrainedRadialExtension(RadialExtension radial,
std::unique_ptr<const ::mfem::SparseMatrix> prolongation, std::unique_ptr<const ::mfem::SparseMatrix> restriction) :
m_radial(std::move(radial)), m_prolongation(std::move(prolongation)), m_restriction(std::move(restriction)) {}
inline ConstrainedRadialExtension::Workspace ConstrainedRadialExtension::make_workspace() const {
if (!m_prolongation) {
return {};
}
return {.local = ::mfem::Vector(static_cast<int>(output_size())), .independent = ::mfem::Vector(m_restriction->Height())};
}
inline DeformationStatus ConstrainedRadialExtension::apply(std::span<const double> surface, std::span<double> volume) const {
Workspace workspace = make_workspace();
return apply(surface, volume, workspace);
}
inline DeformationStatus ConstrainedRadialExtension::apply_transpose(std::span<const double> volume_force, std::span<double> surface_force) const {
Workspace workspace = make_workspace();
return apply_transpose(volume_force, surface_force, workspace);
}
inline DeformationStatus ConstrainedRadialExtension::validate_workspace(const Workspace& workspace,
std::span<const double> input, std::span<double> output) const {
if (workspace.local.Size() != static_cast<int>(output_size()) || workspace.independent.Size() != m_restriction->Height()) {
return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::invalid_size, .message = "Constrained radial action requires scratch allocated for this extension."});
}
const std::span<double> local(workspace.local.GetData(), workspace.local.Size());
const std::span<double> independent(workspace.independent.GetData(), workspace.independent.Size());
if (const DeformationStatus status = deformation::detail::validate_disjoint_output(local, {input, output, independent}); !status) {
return status;
}
return deformation::detail::validate_disjoint_output(independent, {input, output});
}
inline DeformationStatus ConstrainedRadialExtension::apply(std::span<const double> surface,
std::span<double> volume, Workspace& workspace) const {
if (!m_prolongation) {
return m_radial.apply(surface, volume);
}
if (const DeformationStatus status = deformation::detail::validate_action_arguments(volume, output_size(), {{surface, input_size()}}); !status) {
return status;
}
if (const DeformationStatus status = validate_workspace(workspace, surface, volume); !status) {
return status;
}
if (const DeformationStatus status = m_radial.apply(surface, {workspace.local.GetData(), static_cast<std::size_t>(workspace.local.Size())}); !status) {
return status;
}
m_restriction->Mult(workspace.local, workspace.independent);
::mfem::Vector output(volume.data(), static_cast<int>(volume.size()));
m_prolongation->Mult(workspace.independent, output);
return validate_deformation_values(volume, output_size());
}
inline DeformationStatus ConstrainedRadialExtension::apply_transpose(std::span<const double> volume_force,
std::span<double> surface_force, Workspace& workspace) const {
if (!m_prolongation) {
return m_radial.apply_transpose(volume_force, surface_force);
}
if (const DeformationStatus status = deformation::detail::validate_action_arguments(surface_force, input_size(), {{volume_force, output_size()}}); !status) {
return status;
}
if (const DeformationStatus status = validate_workspace(workspace, volume_force, surface_force); !status) {
return status;
}
// MFEM's vector view requires mutable storage; matrix application only reads this force.
const ::mfem::Vector input(const_cast<double*>(volume_force.data()), static_cast<int>(volume_force.size()));
m_prolongation->MultTranspose(input, workspace.independent);
m_restriction->MultTranspose(workspace.independent, workspace.local);
return m_radial.apply_transpose({workspace.local.GetData(), static_cast<std::size_t>(workspace.local.Size())}, surface_force);
}
inline std::expected<ConstrainedRadialExtension, DeformationFailure> make_constrained_radial_extension(
std::shared_ptr<const RadialInterpolation> interpolation, const ::mfem::FiniteElementSpace& displacement_space,
PowerLawRadialExtension interior, FixedInfinityRadialExtension vacuum) {
#ifdef MFEM_USE_MPI
if (dynamic_cast<const ::mfem::ParFiniteElementSpace*>(&displacement_space) != nullptr) {
return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Constrained radial extension requires a serial displacement space."});
}
#endif
auto radial = make_radial_extension(std::move(interpolation), interior, vacuum);
if (!radial) {
return std::unexpected(radial.error());
}
if (displacement_space.GetVDim() != 3 || displacement_space.GetOrdering() != ::mfem::Ordering::byNODES ||
displacement_space.GetVSize() != static_cast<int>(radial->output_size())) {
return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Constrained radial extension requires its three-component reference space in component-block ordering."});
}
const ::mfem::SparseMatrix* prolongation = displacement_space.GetConformingProlongation();
if (prolongation == nullptr) {
return ConstrainedRadialExtension(std::move(*radial), nullptr, nullptr);
}
const ::mfem::SparseMatrix* restriction = displacement_space.GetConformingRestriction();
if (displacement_space.IsVariableOrder() || restriction == nullptr || prolongation->Height() != displacement_space.GetVSize() ||
prolongation->Width() != displacement_space.GetTrueVSize() || restriction->Height() != prolongation->Width() ||
restriction->Width() != prolongation->Height()) {
return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "Constrained radial extension requires fixed-order serial prolongation and matching coefficient restriction matrices."});
}
return ConstrainedRadialExtension(std::move(*radial), std::make_unique<const ::mfem::SparseMatrix>(*prolongation),
std::make_unique<const ::mfem::SparseMatrix>(*restriction));
}
} // namespace serif::geometry::deformation::backend::mfem

View File

@@ -0,0 +1,79 @@
#pragma once
/**
* @file radial_locator.hpp
* @author Emily M. Boudreaux
*
* @brief File containing some utility functions for locating points on a radial shell.
*/
#include "mfem/fem/intrules.hpp"
#include "mfem/linalg/vector.hpp"
#include "serif/geometry/deformation/failure.hpp"
namespace mfem {
/**
* @brief Forward definition of the mfem::Mesh class to avoid having to parse the entire mfem mesh header file.
*/
class Mesh;
}
namespace serif::geometry::deformation::backend::mfem::detail {
/** @struct LocatedBoundaryPoint
*
* @brief A structure representing a located boundary point, including the boundary element index and the integration point on that element.
*/
struct LocatedBoundaryPoint {
int boundary_element; ///< The index of the boundary element where the point is located.
::mfem::IntegrationPoint point; ///< The integration point on the boundary element where the point is located.
};
/**
* @brief compute the cubical local radius of a given position vector. Alternative put:
* compute the \f$L\infty\f$ norm of the position vector (i.e. the maximum absolute value of its components)
*
* @param position The position vector for which to compute the logical radius.
* @returns The logical radius of the position vector, defined as the maximum absolute value of its components (i.e., the L\f$\infty\f$ norm).
*/
[[nodiscard]] double logical_radius(const ::mfem::Vector& position);
/**
* @brief Compute the logical radius of a boundary shell in a mesh, given the mesh and the boundary attribute.
*
* @param mesh The mesh containing the boundary shell.
* @param attribute The boundary attribute identifying the shell to compute the logical radius for.
*
* @returns The logical radius of the boundary shell if successful, or a DeformationFailure object if an error occurs (e.g., incompatible discretization, non-finite vertex, or invalid geometry).
*
* @par Explanation
* The algorithm here is as follows
* 1. Loop over all boundary elements. validate they they have the correct attribute id (if not skip them)
* 2. Get the geometry type of the element and validate that it has a valid transformation and is a square (which for mfem really means a quadrilateral). If not return an error
* 3. Get the vertices of the element and loop over them. For each vertex
* a. Transform the vertex into a position vector
* b. validate that each component of the position vector is finite. If not return an error
* c. Compute the logical radius of the position vector (L\f$\infty\f$ norm) and update the minimum and maximum logical radius values seen so far
* d. Increment the number of samples seen so far
* 4. validate that at least one sample was seen, that the minimum values is positive, and that are at least separated by 1e-12 * max(1, maximum). If not return an error
* 5. Return the average of the minimum and maximum logical radius values seen.
*
* Effectively then what this does is find an estimate for the radius of some enclosing surface over the boundary elements.
*/
[[nodiscard]] std::expected<double, DeformationFailure> boundary_logical_radius(::mfem::Mesh& mesh, int attribute);
/**
* @brief Locate a projected logical ray on a quadrilateral shell.
*
* @param mesh The mesh containing the boundary shell.
* @param attribute The boundary attribute identifying the shell to locate the point on.
* @param target The target position vector to locate on the boundary shell.
*
* @returns A LocatedBoundaryPoint object representing the located boundary point if successful, or a DeformationFailure object if an error occurs
*/
[[nodiscard]] std::expected<LocatedBoundaryPoint, DeformationFailure> locate_boundary_point(
::mfem::Mesh& mesh,
int attribute,
const ::mfem::Vector& target
);
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include <optional>
#include "serif/backend/mfem/discretization/dof_map.hpp"
namespace serif::geometry::deformation::backend::mfem {
/**
* @brief Visit the same displacement coordinate mapping for values and their derivatives.
*
* Deformation maps use full component blocks, whereas equation bindings use supported
* DOFs in their MFEM space's ordering. Each supported coefficient has one source in
* the full deformation vector. Visiting these pairs also defines the exact transpose
* and the sparse projection without reproducing ordering or support logic.
* @param binding A current, validated displacement binding.
* @param visit Receives the supported row and full component-block column.
*/
template <typename Function>
void for_each_displacement_coefficient(const discretization::backend::mfem::DofMap& binding, Function&& visit) {
const ::mfem::FiniteElementSpace& space = binding.space();
const int nodes = space.GetNDofs();
if (binding.constrained()) {
// Restriction selects one local representative per independent coefficient.
// Visiting every identity row of prolongation could count the same coefficient twice.
for (int row = 0; row < binding.size(); ++row) {
const int local = binding.full_dof(row);
const int component = space.GetOrdering() == ::mfem::Ordering::byNODES ? local / nodes : local % space.GetVDim();
const int node = space.GetOrdering() == ::mfem::Ordering::byNODES ? local % nodes : local / space.GetVDim();
visit(row, node + component * nodes);
}
return;
}
for (int component = 0; component < space.GetVDim(); ++component) {
for (int node = 0; node < nodes; ++node) {
const std::optional<int> row = binding.reduced_dof(space.DofToVDof(node, component));
if (row) {
visit(*row, node + component * nodes);
}
}
}
}
} // namespace serif::geometry::deformation::backend::mfem

View File

@@ -0,0 +1,55 @@
#pragma once
#include <expected>
#include <memory>
#include <span>
#include <vector>
#include "serif/geometry/deformation/failure.hpp"
#include "serif/geometry/deformation/geometry_step.hpp"
namespace mfem {
class FiniteElementSpace;
class GridFunction;
class IntegrationRule;
class Vector;
}
namespace serif::mapping {
class DomainMapper;
struct DMFixedMeshGeometry;
}
namespace serif::geometry::deformation::backend::mfem {
/**
* @brief Borrow one sampling rule; repeat an element to retain the actual union of downstream rules.
* @note reference_geometry optionally holds sample_fixed_mesh_geometry for every point of this rule on
* the unchanged reference mesh. The reference mesh never moves during a solve, so a caller that already
* owns these samples (the volume equations' Basis nodes) can share them instead of having each step
* limit recompute the high-order mesh Jacobian. Without it, or if its size does not match, samples
* are computed on demand exactly as before.
*/
struct GeometrySampleRule {
int element;
const ::mfem::IntegrationRule* integration_rule;
std::shared_ptr<const std::vector<mapping::DMFixedMeshGeometry>> reference_geometry{};
};
/**
* @brief Find the first sampled mapping boundary along accepted + step direction.
*
* Inputs use the displacement space's true-DOF ordering. The compactification coordinate is fixed
* throughout this straight volume-displacement path. An exterior map must be affine in displacement.
* The result covers exactly the supplied rules, including repeats: quadrature rules need not be nested.
* Only serial spaces are supported. Parallel spaces are rejected before any mapping is evaluated.
*/
[[nodiscard]] std::expected<GeometryStepLimit, DeformationFailure> estimate_geometry_step_limit(
const mapping::DomainMapper& mapper,
const ::mfem::FiniteElementSpace& displacement_space,
const ::mfem::GridFunction& compactification_coordinate,
const ::mfem::Vector& accepted_displacement,
const ::mfem::Vector& displacement_direction,
std::span<const GeometrySampleRule> sample_rules,
GeometryStepOptions options = {}
);
}

View File

@@ -0,0 +1,157 @@
#pragma once
#include <algorithm>
#include <array>
#include <ranges>
#include <span>
#include <utility>
#include <vector>
#include "mfem/fem/fespace.hpp"
#include "serif/discretization/domain/schema/schemas.hpp"
#include "serif/discretization/domain/schema/concepts.hpp"
#include "serif/geometry/deformation/models.hpp"
#include "serif/backend/mfem/shared/reference_tracking.hpp"
#include "serif/geometry/deformation/computations.hpp"
#include "serif/utils/dependencies/dependency_context.hpp"
namespace serif::geometry::deformation::backend::mfem {
/** @brief Borrow the paired reference meshes; attributes retain their physical domain meaning. */
struct ReferenceDiscretization {
const ::mfem::FiniteElementSpace& scalar_space;
const ::mfem::FiniteElementSpace& displacement_space;
::mfem::Mesh& logical_mesh;
std::vector<int> stellar_attributes;
std::vector<int> vacuum_attributes;
int surface_attribute;
int infinity_attribute;
};
/** @brief The surface selection needs only its scalar space and boundary label. */
struct SurfaceDiscretization {
const ::mfem::FiniteElementSpace& scalar_space;
int surface_attribute;
};
/** @brief Equal physical values remain reusable when a context is reconstructed. */
struct DeformationValueTracking : utils::dependencies::ValueSnapshot {
/** @brief Wrapper addresses do not identify the underlying physical input. */
template <typename Value>
[[nodiscard]] const void* identity(const Value&) const noexcept {
return nullptr;
}
};
/** @brief Capture the values behind a shape view, independently of its temporary wrapper. */
struct ShapeCoefficientTracking : DeformationValueTracking {
/** @brief The cache must retain values rather than another view of mutable state. */
[[nodiscard]] std::vector<double> capture(std::span<const double> coefficients) const {
return {coefficients.begin(), coefficients.end()};
}
/** @brief Rebinding equal values reuses work; any changed coefficient invalidates it. */
[[nodiscard]] bool matches(const std::vector<double>& captured, std::span<const double> coefficients) const {
return std::ranges::equal(captured, coefficients);
}
};
/** @brief Track topology and labels once, independently of changes to physical coordinates. */
struct SurfaceDiscretizationTracking : DeformationValueTracking {
struct Snapshot {
::serif::backend::mfem::shared::ReferenceMeshSnapshot mesh;
::serif::backend::mfem::shared::ReferenceSpaceSnapshot space;
int surface_attribute;
};
/** @brief Capture exactly the metadata that determines the canonical scalar surface nodes. */
[[nodiscard]] Snapshot capture(const SurfaceDiscretization& input) const;
/** @brief Compare mutable boundary labels without allocating a second snapshot. */
[[nodiscard]] bool matches(const Snapshot& snapshot, const SurfaceDiscretization& input) const;
};
/** @brief Logical interpolation depends on paired topology and logical coordinates, not physical coordinates. */
struct ReferenceDiscretizationTracking : DeformationValueTracking {
struct Snapshot {
std::array<::serif::backend::mfem::shared::ReferenceMeshSnapshot, 2> meshes;
std::array<::serif::backend::mfem::shared::ReferenceSpaceSnapshot, 2> spaces;
std::vector<int> stellar_attributes;
std::vector<int> vacuum_attributes;
int surface_attribute;
int infinity_attribute;
};
/** @brief Capture each paired mesh once; space metadata retains both companion identities. */
[[nodiscard]] Snapshot capture(const ReferenceDiscretization& input) const;
/** @brief Compare exact topology, numbering, logical coordinates and material selections. */
[[nodiscard]] bool matches(const Snapshot& snapshot, const ReferenceDiscretization& input) const;
};
/** @brief Resolve physical domain names through the schema rather than hard-coded MFEM attributes. */
template <discretization::domain::schema::IsSchema Schema = discretization::domain::schema::CoreEnvelopeVacuumDomainSchema>
requires(Schema::template contains_domain<discretization::domain::StellarDomains>() && Schema::template contains_domain<discretization::domain::VacuumDomain>() && Schema::template contains_boundary<discretization::domain::StellarSurfaceBoundary>() && Schema::template contains_boundary<discretization::domain::InfinitySurfaceBoundary>())
[[nodiscard]] ReferenceDiscretization deformation_discretization(const ::mfem::FiniteElementSpace& scalar_space, const ::mfem::FiniteElementSpace& displacement_space, ::mfem::Mesh& logical_mesh) {
ReferenceDiscretization result{
.scalar_space = scalar_space,
.displacement_space = displacement_space,
.logical_mesh = logical_mesh,
.stellar_attributes = {},
.vacuum_attributes = {},
.surface_attribute = Schema::template boundary_id<discretization::domain::StellarSurfaceBoundary>(),
.infinity_attribute = Schema::template boundary_id<discretization::domain::InfinitySurfaceBoundary>()};
for (const auto& domain : Schema::domain_descriptors()) {
if (Schema::template domain_id_belongs_to<discretization::domain::StellarDomains>(domain.ID)) {
result.stellar_attributes.push_back(domain.ID);
}
if (Schema::template domain_id_belongs_to<discretization::domain::VacuumDomain>(domain.ID)) {
result.vacuum_attributes.push_back(domain.ID);
}
}
return result;
}
namespace detail {
/** @brief Share physical input construction between borrowed containers and state views. */
template <typename Surface, typename Interior, typename Vacuum, utils::dependencies::IsResource ShapeResource>
[[nodiscard]] auto deformation_inputs(const DomainDeformation<Surface, Interior, Vacuum>& model, const ReferenceDiscretization& discretization, ShapeResource coefficients) {
using namespace utils::dependencies;
return make_dependency_context(
own<keys::ReferenceDiscretization>(discretization, ReferenceDiscretizationTracking{}),
own<keys::SurfaceDiscretization>(SurfaceDiscretization{.scalar_space = discretization.scalar_space, .surface_attribute = discretization.surface_attribute}, SurfaceDiscretizationTracking{}),
borrow<keys::ReferenceCoordinates>(discretization.scalar_space, track_by([](const ::mfem::FiniteElementSpace& space) {
const ::mfem::Mesh* mesh = space.GetMesh();
return std::pair{mesh, mesh ? mesh->GetNodesSequence() : -1L};
})),
own<keys::SurfaceModel>(model.surface, DeformationValueTracking{}),
own<keys::InteriorModel>(model.stellar_interior, DeformationValueTracking{}),
own<keys::VacuumModel>(model.vacuum, DeformationValueTracking{}),
std::move(coefficients));
}
} // namespace detail
/**
* @brief Bind physical choices and a borrowed shape vector without evaluating deformation.
* @note Meshes, spaces, and the vector must outlive the context. Borrowing the container
* retains its current storage and size even if the caller subsequently resizes it.
* Coordinate edits must notify MFEM through its normal node-update API.
*/
template <typename Surface, typename Interior, typename Vacuum>
[[nodiscard]] auto make_deformation_inputs(const DomainDeformation<Surface, Interior, Vacuum>& model, const ReferenceDiscretization& discretization, const std::vector<double>& coefficients) {
return detail::deformation_inputs(model, discretization, utils::dependencies::borrow<keys::ShapeCoefficients>(coefficients, DeformationValueTracking{}));
}
/**
* @brief Bind shape coordinates directly from a packed numerical state.
* @note The context owns the view, not its storage. The underlying coefficients must
* remain alive at a stable address while the context is used. Published linearizations
* own their coefficient values and remain valid after subsequent state changes.
*/
template <typename Surface, typename Interior, typename Vacuum>
[[nodiscard]] auto make_deformation_inputs(const DomainDeformation<Surface, Interior, Vacuum>& model, const ReferenceDiscretization& discretization, std::span<const double> coefficients) {
return detail::deformation_inputs(model, discretization, utils::dependencies::own<keys::ShapeCoefficients>(coefficients, ShapeCoefficientTracking{}));
}
/** @brief Reject temporary owning containers whose coefficient storage would expire. */
template <typename Surface, typename Interior, typename Vacuum, std::ranges::range Coefficients>
requires (!std::ranges::borrowed_range<Coefficients>)
void make_deformation_inputs(const DomainDeformation<Surface, Interior, Vacuum>&, const ReferenceDiscretization&, Coefficients&&) = delete;
} // namespace serif::geometry::deformation::backend::mfem

View File

@@ -0,0 +1,30 @@
#pragma once
#include <span>
#include "serif/geometry/deformation/radial_extension.hpp"
#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp"
namespace serif::geometry::deformation::backend::mfem
{
/**
* @brief Compile logical-ray interpolation on paired physical and logical meshes.
* Supports are zero/one element masks. The logical radius is the maximum absolute
* Cartesian coordinate, preserving the STROID reference foliation after physical distortion.
* Serial nodal spaces may have hanging nodes away from the conforming stellar surface.
* The table samples local coefficients; the backend extension enforces their constraints.
*/
[[nodiscard]] std::expected<RadialInterpolation, DeformationFailure> build_radial_interpolation(
const ::mfem::FiniteElementSpace& scalar_space,
const ::mfem::FiniteElementSpace& vector_space,
::mfem::Mesh& logical_mesh,
std::span<const int> stellar_element_support,
std::span<const int> vacuum_element_support,
int surface_attribute,
int infinity_attribute);
namespace detail
{
/** @brief Reuse the validated scalar space and surface selection supplied by the reference graph. */
[[nodiscard]] std::expected<RadialInterpolation, DeformationFailure> build_radial_interpolation(const ::mfem::FiniteElementSpace& scalar_space, const ::mfem::FiniteElementSpace& vector_space, ::mfem::Mesh& logical_mesh, std::span<const int> stellar_element_support, std::span<const int> vacuum_element_support, int surface_attribute, int infinity_attribute, const std::vector<int>& selected);
} // namespace detail
} // namespace serif::geometry::deformation::backend::mfem

View File

@@ -0,0 +1,53 @@
#pragma once
#include <array>
#include <vector>
#include "serif/geometry/deformation/failure.hpp"
#include "serif/geometry/deformation/reference_surface.hpp"
#include "serif/geometry/deformation/surface_embedding.hpp"
namespace mfem
{
class FiniteElementSpace;
class Mesh;
} // namespace mfem
namespace serif::geometry::deformation::backend::mfem
{
/** @brief Reject spaces whose coefficients cannot be interpreted as serial nodal positions. */
[[nodiscard]] DeformationStatus validate_scalar_reference_space(const ::mfem::FiniteElementSpace& scalar_space);
/**
* @brief Require identical scalar coefficient meanings in two companion spaces.
* Vector component ordering may differ; element bases and their scalar-node indices must agree.
*/
[[nodiscard]] DeformationStatus validate_companion_numbering(
const ::mfem::FiniteElementSpace& reference_space,
const ::mfem::FiniteElementSpace& companion_space);
/** @brief Sample a paired mesh at the scalar companion nodes, checking shared-node agreement. */
[[nodiscard]] std::expected<std::vector<std::array<double, 3>>, DeformationFailure> sample_reference_positions(
const ::mfem::FiniteElementSpace& scalar_space,
::mfem::Mesh& reference_mesh);
/** @brief Select canonical surface nodes without retaining a mesh or finite-element-space pointer. */
[[nodiscard]] std::expected<std::vector<int>, DeformationFailure> select_surface_scalar_dofs(
const ::mfem::FiniteElementSpace& scalar_space,
int surface_attribute);
/** @brief Sample physical reference positions in ascending scalar-DOF order. */
[[nodiscard]] std::expected<ReferenceSurface, DeformationFailure> sample_reference_surface(
const ::mfem::FiniteElementSpace& scalar_space,
int surface_attribute);
/** @brief Require the same ordered reference nodes and physical positions as a compiled surface embedding. */
[[nodiscard]] DeformationStatus validate_surface_trace(const ::mfem::FiniteElementSpace& scalar_space, int surface_attribute, const ReferenceSurface& reference_surface, const SurfaceEmbedding& embedding);
namespace detail
{
/** @brief Sample paired coordinates after the scalar node-selection stage has validated the space. */
[[nodiscard]] std::expected<std::vector<std::array<double, 3>>, DeformationFailure> sample_reference_positions(const ::mfem::FiniteElementSpace& scalar_space, ::mfem::Mesh& reference_mesh);
/** @brief Reuse the graph's canonical surface nodes when only physical coordinates changed. */
[[nodiscard]] std::expected<ReferenceSurface, DeformationFailure> sample_reference_surface(const ::mfem::FiniteElementSpace& scalar_space, const std::vector<int>& selected);
} // namespace detail
} // namespace serif::geometry::deformation::backend::mfem

View File

@@ -0,0 +1,28 @@
#pragma once
#include <expected>
#include <vector>
#include "serif/geometry/deformation/failure.hpp"
#include "serif/geometry/deformation/reference_surface.hpp"
#include "serif/geometry/deformation/surface_embedding.hpp"
namespace mfem
{
class FiniteElementSpace;
}
namespace serif::geometry::deformation::backend::mfem
{
/**
* @brief Measure each shape parameter with the reference boundary mass norm of its Cartesian finite-element trace.
* @note This is diag(S^T (M_boundary tensor I) S), independent of the interior/vacuum extension and of the current shape coefficients. It uses the mass integrator's standard quadrature and does not orthogonalize harmonic coordinates.
*/
[[nodiscard]] std::expected<std::vector<double>, DeformationFailure> surface_parameter_mass_diagonal(const ::mfem::FiniteElementSpace& scalar_space, int surface_attribute, const ReferenceSurface& reference_surface, const SurfaceEmbedding& embedding);
namespace detail
{
/** @brief Assemble from the current reference trace and its embedding, already paired by the dependency graph. */
[[nodiscard]] std::expected<std::vector<double>, DeformationFailure> surface_parameter_mass_diagonal(const ::mfem::FiniteElementSpace& scalar_space, int surface_attribute, const ReferenceSurface& reference_surface, const SurfaceEmbedding& embedding);
} // namespace detail
} // namespace serif::geometry::deformation::backend::mfem

View File

@@ -0,0 +1,69 @@
#pragma once
#include "serif/backend/mfem/geometry/surface/boundary_inputs.hpp"
#include "serif/geometry/surface/computations.hpp"
#include "serif/backend/mfem/shared/finite.hpp"
namespace serif::geometry::surface::backend::mfem
{
namespace detail
{
/** @brief Sample the same fixed linear trace used by the boundary tangent and its transpose. */
template <typename Term, typename Boundary, typename Input>
[[nodiscard]] std::expected<BoundaryValues<Term>, SurfaceFailure> sample_boundary_values(const Boundary& boundary, const Input& input)
{
using namespace utils::errors;
const auto& trace = boundary.traces.template get<Term>();
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR, std::invalid_argument>(&trace.binding() == &input.binding, "Pressure-boundary coefficients must use the binding that compiled their trace.");
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::invalid_argument>(input.values.Size() == trace.binding().size(), "Pressure-boundary coefficients must match the supported field size.");
if (!utils::checks::is_finite(input.values))
{
return std::unexpected(SurfaceFailure{.reason = SurfaceFailureReason::non_finite_state, .message = "A pressure-boundary field contains a non-finite coefficient."});
}
BoundaryValues<Term> result{.values = std::vector<double>(boundary.selected_rows->size())};
::mfem::Vector values(result.values.data(), static_cast<int>(result.values.size()));
trace.interpolation().Mult(input.values, values);
if (!utils::checks::is_finite(values))
{
return std::unexpected(SurfaceFailure{.reason = SurfaceFailureReason::non_finite_state, .message = "A finite-element boundary trace produced a non-finite value."});
}
return result;
}
/** @brief Declare one sampled value column per unique physical term, without a separate preparation owner. */
template <PressureBoundaryFormulation Formulation, typename... Terms>
[[nodiscard]] auto pressure_boundary_computations(utils::dependencies::KeyList<Terms...>)
{
using namespace utils::dependencies;
const auto sample = []<typename Term>()
{
return make_computation<keys::BoundarySamples<Term>, InputList<keys::BoundaryDiscretization, Term>>(
[](const auto& boundary, const auto& input)
{
static_assert(std::same_as<typename std::remove_cvref_t<decltype(*boundary)>::Formulation, Formulation>, "The boundary graph must use the formulation that compiled its traces.");
return sample_boundary_values<Term>(*boundary, input);
});
};
return std::tuple_cat(std::tuple{sample.template operator()<Terms>()...}, make_pressure_surface_computations<Formulation, keys::BoundarySamples>());
}
}
/** @brief Combine finite-element sampling with the unchanged thermodynamic graph; coefficient sources can also feed volume equations. */
template <PressureBoundaryFormulation Formulation>
[[nodiscard]] auto make_pressure_boundary_computations()
{
return detail::pressure_boundary_computations<Formulation>(typename Formulation::StateTerms{});
}
/** @brief Construct a standalone isobaric boundary graph with the ordinary dependency-cache lifecycle. */
template <PressureBoundaryFormulation Formulation>
[[nodiscard]] auto make_pressure_boundary_graph()
{
return std::apply([](auto... computations)
{
return utils::dependencies::make_computation_graph(std::move(computations)...);
}, make_pressure_boundary_computations<Formulation>());
}
}

View File

@@ -0,0 +1,81 @@
#pragma once
#include <memory>
#include <utility>
#include "serif/backend/mfem/discretization/boundary_trace.hpp"
#include "serif/geometry/surface/compilation.hpp"
#include "serif/backend/mfem/discretization/term_coefficients.hpp"
namespace serif::geometry::surface::backend::mfem
{
namespace detail
{
/** @brief Strong nodal rows need a unique trace for every thermodynamic input. */
template <typename... Terms>
[[nodiscard]] consteval bool supported_boundary_terms(utils::dependencies::KeyList<Terms...>)
{
return (discretization::backend::mfem::BoundaryTraceTerm<Terms> && ...);
}
}
/** @brief Isobaric rows are scalar nodal H1 equations with continuous scalar or global EOS inputs. */
template <typename Formulation>
concept PressureBoundaryFormulation = PressureSurfaceFormulationType<Formulation> &&
discretization::blocks::SpatialTerm<typename Formulation::Test> &&
discretization::blocks::SpatialTermTraits<typename Formulation::Test>::family == discretization::functional_space::Family::h1 &&
detail::supported_boundary_terms(typename Formulation::StateTerms{});
/**
* @brief The selected equation rows and the finite-element traces supplying their physical state.
* @note Bindings and their meshes must outlive this object. EOS values and readiness belong to the dependency cache.
*/
template <PressureBoundaryFormulation FormulationT, typename Traces>
struct PressureBoundary
{
using Formulation = FormulationT;
std::shared_ptr<const discretization::backend::mfem::DofSelection> selected_rows;
Traces traces;
/** @brief Reject stale bindings and selections even when all coefficient values are unchanged. */
void validate() const
{
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(selected_rows != nullptr, "An isobaric boundary requires a row selection.");
const auto validate_traces = [&]<typename... Terms>(utils::dependencies::KeyList<Terms...>)
{
(traces.template get<Terms>().validate(), ...);
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR, std::invalid_argument>(((&traces.template get<Terms>().rows() == selected_rows.get()) && ...), "Every pressure-boundary trace must sample the same ordered equation rows.");
};
validate_traces(typename Formulation::StateTerms{});
}
};
namespace detail
{
/** @brief Compile each unique field once, including quantities that share one coefficient term. */
template <PressureBoundaryFormulation Formulation, typename Coefficients, typename... Terms>
[[nodiscard]] auto make_pressure_boundary(const Coefficients& coefficients, utils::dependencies::KeyList<Terms...>)
{
using namespace utils::dependencies;
using namespace discretization::backend::mfem;
using RowBinding = std::remove_cvref_t<decltype(coefficients.template get<typename Formulation::Test>().binding)>;
static_assert((std::same_as<typename RowBinding::Schema, typename std::remove_cvref_t<decltype(coefficients.template get<Terms>().binding)>::Schema> && ...), "Pressure-boundary fields must share a domain schema.");
const auto& row_binding = coefficients.template get<typename Formulation::Test>().binding;
const auto rows = std::make_shared<const DofSelection>(select_boundary_dofs<typename Formulation::Support>(row_binding));
auto traces = make_dependency_context(own<Terms>(make_boundary_trace(rows, coefficients.template get<Terms>().binding))...);
using Boundary = PressureBoundary<Formulation, decltype(traces)>;
return std::make_shared<const Boundary>(Boundary{.selected_rows = rows, .traces = std::move(traces)});
}
}
/** @brief Compile nodal boundary traces explicitly; this does not sample coefficients or evaluate the EOS. */
template <PressureBoundaryFormulation Formulation, utils::dependencies::IsDependencyContext Coefficients>
requires utils::dependencies::SatisfiesKeyList<typename Coefficients::Bindings, typename Formulation::StateTerms>
[[nodiscard]] auto make_pressure_boundary(const Coefficients& coefficients)
{
return detail::make_pressure_boundary<Formulation>(coefficients, typename Formulation::StateTerms{});
}
}

View File

@@ -0,0 +1,90 @@
#pragma once
#include "serif/backend/mfem/discretization/coefficient_tracking.hpp"
#include "serif/backend/mfem/geometry/surface/boundary_data.hpp"
#include "serif/geometry/surface/samples.hpp"
namespace serif::geometry::surface::backend::mfem
{
namespace keys
{
struct BoundaryDiscretization
{
};
/** @brief Keep sampled boundary state distinct from volume coefficient sources for the same term. */
template <typename Term>
struct BoundarySamples
{
};
}
namespace detail
{
/** @brief Shared traces have stable identity; validation catches changes outside the coefficient vectors. */
struct BoundaryTracking
{
/** @brief Validate the borrowed discretization and retain its trace owner so an old address cannot identify a new boundary. */
template <typename Boundary>
[[nodiscard]] std::shared_ptr<const Boundary> capture(const std::shared_ptr<const Boundary>& boundary) const
{
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(boundary != nullptr, "Pressure-boundary inputs require compiled boundary traces.");
boundary->validate();
return boundary;
}
/** @brief Validate even unchanged inputs, so stale selections cannot remain publicly ready. */
template <typename Boundary>
[[nodiscard]] bool matches(const std::shared_ptr<const Boundary>& captured, const std::shared_ptr<const Boundary>& boundary) const
{
return captured == capture(boundary);
}
/** @brief Moving a context does not move the compiled boundary it owns. */
template <typename Boundary>
[[nodiscard]] const void* identity(const std::shared_ptr<const Boundary>& boundary) const
{
return boundary.get();
}
};
}
/** @brief Share boundary metadata with a model's common coefficient context. */
template <typename Boundary, typename EOSResource>
[[nodiscard]] auto make_pressure_boundary_metadata(std::shared_ptr<const Boundary> boundary, EOSResource equation_of_state, IsobaricSurface condition)
{
using namespace utils::dependencies;
return make_dependency_context(own<keys::BoundaryDiscretization>(std::move(boundary), detail::BoundaryTracking{}),
std::move(equation_of_state), pressure_condition(std::move(condition)));
}
namespace detail
{
/** @brief Preserve standard coefficient keys and policies in a standalone boundary context. */
template <typename Boundary, typename EOSResource, typename Coefficients, typename... Terms>
[[nodiscard]] auto pressure_boundary_inputs(std::shared_ptr<const Boundary> boundary, EOSResource equation_of_state, IsobaricSurface condition, const Coefficients& coefficients, utils::dependencies::KeyList<Terms...>)
{
using namespace utils::dependencies;
return combine_dependency_contexts(make_pressure_boundary_metadata(std::move(boundary), std::move(equation_of_state), std::move(condition)),
make_dependency_context(own<Terms>(coefficients.template get<Terms>(), discretization::backend::mfem::CoefficientTracking{})...));
}
}
/**
* @brief Bind the physical condition, explicitly tracked EOS, and borrowed field coefficients without evaluating them.
* @note Inputs may be reused while coefficient vectors change. The caller's bindings, vectors and borrowed EOS must remain alive.
*/
template <typename Boundary, utils::dependencies::IsResource EOSResource, utils::dependencies::IsDependencyContext Coefficients>
requires std::same_as<typename EOSResource::Key, surface::keys::EquationOfState> &&
std::same_as<std::remove_cvref_t<typename EOSResource::Target>, typename Boundary::Formulation::EquationOfState> &&
utils::dependencies::SatisfiesKeyList<typename Coefficients::Bindings, typename Boundary::Formulation::StateTerms>
[[nodiscard]] auto make_pressure_boundary_inputs(std::shared_ptr<const Boundary> boundary, EOSResource equation_of_state, IsobaricSurface condition, const Coefficients& coefficients)
{
return detail::pressure_boundary_inputs(std::move(boundary), std::move(equation_of_state), std::move(condition), coefficients, typename Boundary::Formulation::StateTerms{});
}
}

View File

@@ -0,0 +1,141 @@
#pragma once
#include "serif/utils/error/nonfinite.hpp"
#include <cmath>
#include <vector>
#include "serif/backend/mfem/geometry/surface/boundary_data.hpp"
#include "serif/geometry/surface/linearization.hpp"
#include "serif/backend/mfem/shared/finite.hpp"
namespace serif::geometry::surface::backend::mfem
{
namespace detail
{
/** @brief Check that an action uses the current discretization and the complete residual space. */
template <typename Boundary>
void validate_boundary_output(const Boundary& boundary, const ::mfem::Vector& result)
{
using namespace utils::errors;
boundary.validate();
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::invalid_argument>(result.Size() == boundary.selected_rows->binding().size(), "Pressure-boundary output must match the residual binding size.");
}
/** @brief The accepted graph result contains one local equation for each ordered boundary node. */
template <typename Boundary, typename Linearizations>
void validate_boundary_samples(const Boundary& boundary, const Linearizations& linearizations)
{
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::invalid_argument>(linearizations.size() == static_cast<std::size_t>(boundary.selected_rows->size()), "Pressure-boundary linearizations must match the compiled row selection.");
}
/** @brief Publish a complete finite action only after all input directions have been read. */
template <typename Boundary>
void replace_boundary_rows(const Boundary& boundary, const ::mfem::Vector& values, ::mfem::Vector& result)
{
using namespace utils::errors;
serif_ensure<SERiFErrorCode::NON_FINITE_VALUE_ERROR, NonFiniteValue>(utils::checks::is_finite(values), "The pressure-boundary action produced a non-finite value; reject this candidate.");
const auto& rows = boundary.selected_rows->reduced_dofs();
for (int index = 0; index < rows.Size(); ++index)
{
result[rows[index]] = values[index];
}
}
/** @brief Accumulate all physical couplings using the same traces that supplied the EOS state. */
template <typename Boundary, typename Linearizations, typename Directions, typename... Terms>
[[nodiscard]] ::mfem::Vector boundary_jacobian_action(const Boundary& boundary, const Linearizations& linearizations, const Directions& directions, utils::dependencies::KeyList<Terms...>)
{
using namespace utils::errors;
::mfem::Vector result(boundary.selected_rows->size());
::mfem::Vector samples(result.Size());
result = 0.0;
const auto add_direction = [&]<typename Term>()
{
const auto& trace = boundary.traces.template get<Term>();
const auto& direction = directions.template get<Term>();
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR, std::invalid_argument>(&direction.binding == &trace.binding(), "A pressure-boundary direction must use the binding that compiled its trace.");
trace.binding().validate_values(direction.values);
trace.interpolation().Mult(direction.values, samples);
for (int index = 0; index < result.Size(); ++index)
{
result[index] += linearizations[index].field_derivatives.template get<Term>() * samples[index];
}
};
(add_direction.template operator()<Terms>(), ...);
return result;
}
}
/** @brief Replace selected residual rows with a prepared isobaric equation, preserving all interior rows. */
template <PressureBoundaryFormulation Formulation, typename Traces>
void apply_residual_rows(const PressureBoundary<Formulation, Traces>& boundary, const std::vector<SurfaceLinearization<Formulation>>& linearizations, ::mfem::Vector& result)
{
using namespace utils::errors;
detail::validate_boundary_output(boundary, result);
detail::validate_boundary_samples(boundary, linearizations);
for (const auto& point : linearizations)
{
serif_ensure<SERiFErrorCode::NON_FINITE_VALUE_ERROR, NonFiniteValue>(std::isfinite(point.residual), "The pressure-boundary residual contains a non-finite value; reject this candidate.");
}
for (int index = 0; index < boundary.selected_rows->size(); ++index)
{
result[boundary.selected_rows->reduced_dofs()[index]] = linearizations[index].residual;
}
}
/** @brief Replace full Jacobian rows, including every EOS input; directions may alias the output. */
template <PressureBoundaryFormulation Formulation, typename Traces, utils::dependencies::IsDependencyContext Directions>
requires utils::dependencies::SatisfiesKeyList<typename Directions::Bindings, typename Formulation::StateTerms>
void apply_jacobian_rows(const PressureBoundary<Formulation, Traces>& boundary, const std::vector<SurfaceLinearization<Formulation>>& linearizations, const Directions& directions, ::mfem::Vector& result)
{
detail::validate_boundary_output(boundary, result);
detail::validate_boundary_samples(boundary, linearizations);
const ::mfem::Vector values = detail::boundary_jacobian_action(boundary, linearizations, directions, typename Formulation::StateTerms{});
detail::replace_boundary_rows(boundary, values, result);
}
/** @brief Remove the replaced rows before applying the volume Jacobian transpose. */
template <PressureBoundaryFormulation Formulation, typename Traces>
void mask_residual_dual(const PressureBoundary<Formulation, Traces>& boundary, const ::mfem::Vector& dual, ::mfem::Vector& volume_dual)
{
detail::validate_boundary_output(boundary, dual);
boundary.selected_rows->binding().validate_values(dual);
volume_dual = dual;
for (const int row : boundary.selected_rows->reduced_dofs())
{
volume_dual[row] = 0.0;
}
}
/**
* @brief Add one physical term's constrained-row transpose to its existing volume contribution.
* @note The caller supplies the same accepted linearizations as the forward action. Discard the output if a non-finite action throws.
*/
template <typename Term, PressureBoundaryFormulation Formulation, typename Traces>
requires utils::dependencies::BindingsWithBinding<typename Traces::Bindings, Term>
void add_jacobian_transpose_rows(const PressureBoundary<Formulation, Traces>& boundary, const std::vector<SurfaceLinearization<Formulation>>& linearizations, const ::mfem::Vector& dual, ::mfem::Vector& result)
{
using namespace utils::errors;
detail::validate_boundary_output(boundary, dual);
detail::validate_boundary_samples(boundary, linearizations);
boundary.selected_rows->binding().validate_values(dual);
const auto& trace = boundary.traces.template get<Term>();
trace.binding().validate_values(result);
::mfem::Vector weights(boundary.selected_rows->size());
for (int index = 0; index < weights.Size(); ++index)
{
weights[index] = linearizations[index].field_derivatives.template get<Term>() * dual[boundary.selected_rows->reduced_dofs()[index]];
}
serif_ensure<SERiFErrorCode::NON_FINITE_VALUE_ERROR, NonFiniteValue>(utils::checks::is_finite(weights), "The pressure-boundary transpose produced non-finite weights; reject this candidate.");
trace.interpolation().AddMultTranspose(weights, result);
serif_ensure<SERiFErrorCode::NON_FINITE_VALUE_ERROR, NonFiniteValue>(utils::checks::is_finite(result), "The pressure-boundary transpose produced a non-finite result; discard this output.");
}
}

View File

@@ -0,0 +1,4 @@
#pragma once
#include "serif/backend/mfem/geometry/surface/boundary_computations.hpp"
#include "serif/backend/mfem/geometry/surface/boundary_rows.hpp"

View File

@@ -0,0 +1,54 @@
#pragma once
#include <cmath>
#include <memory>
#include <utility>
#include <vector>
#include "serif/backend/mfem/geometry/surface/boundary_rows.hpp"
#include "serif/backend/mfem/operators/replaced_rows.hpp"
#include "serif/utils/dependencies/key_list_operations.hpp"
namespace serif::geometry::surface::backend::mfem {
/** @brief Freeze one EOS partial times its finite-element trace in selected boundary-row coordinates. */
template <typename Field, PressureBoundaryFormulation Formulation, typename Traces>
requires utils::dependencies::ContainsKey<Field, typename Formulation::StateTerms>
[[nodiscard]] std::shared_ptr<const ::mfem::SparseMatrix> pressure_field_trace(const PressureBoundary<Formulation, Traces>& boundary, const std::vector<SurfaceLinearization<Formulation>>& linearizations) {
using namespace utils::errors;
boundary.validate();
detail::validate_boundary_samples(boundary, linearizations);
auto result = std::make_shared<::mfem::SparseMatrix>(boundary.traces.template get<Field>().interpolation());
::mfem::Vector derivatives(boundary.selected_rows->size());
for (int row = 0; row < derivatives.Size(); ++row) {
derivatives[row] = linearizations[row].field_derivatives.template get<Field>();
serif_ensure<SERiFErrorCode::NON_FINITE_VALUE_ERROR>(std::isfinite(derivatives[row]), "Pressure-boundary derivative {} is nonfinite. Supply an accepted surface linearization.", row);
}
result->ScaleRows(derivatives);
return result;
}
/**
* @brief Install one field's accepted isobaric derivative rows on its physical volume block.
* @note Every EOS input uses its own interpolation. Absent fields receive zero constrained rows. Copied derivative-weighted traces keep this accepted block independent of later EOS candidates and of the boundary object's lifetime.
*/
template <typename Field, PressureBoundaryFormulation Formulation, typename Traces>
[[nodiscard]] operators::backend::mfem::LinearizedBlock pressure_field_block(operators::backend::mfem::LinearizedBlock volume, const PressureBoundary<Formulation, Traces>& boundary, const std::vector<SurfaceLinearization<Formulation>>& linearizations) {
using namespace utils::errors;
boundary.validate();
detail::validate_boundary_samples(boundary, linearizations);
serif_ensure<SERiFErrorCode::MATRIX_SHAPE_ERROR>(volume.action()->Height() == boundary.selected_rows->binding().size(), "The selected physical block and pressure boundary must use the same full residual coordinate count.");
std::shared_ptr<const ::mfem::SparseMatrix> replacement;
if constexpr (utils::dependencies::ContainsKey<Field, typename Formulation::StateTerms>) {
replacement = pressure_field_trace<Field>(boundary, linearizations);
serif_ensure<SERiFErrorCode::MATRIX_SHAPE_ERROR>(replacement->Width() == volume.action()->Width(), "The selected physical field and its pressure trace have different coefficient counts.");
}
else {
auto zero = std::make_shared<::mfem::SparseMatrix>(boundary.selected_rows->size(), volume.action()->Width());
zero->Finalize();
replacement = std::move(zero);
}
const ::mfem::Array<int>& selected = boundary.selected_rows->reduced_dofs();
return operators::backend::mfem::replace_block_rows(std::move(volume), std::vector<int>(selected.begin(), selected.end()), std::move(replacement));
}
} // namespace serif::geometry::surface::backend::mfem

View File

@@ -0,0 +1,75 @@
#pragma once
/**
* @file compactification.hpp
* @author Emily M. Boudreaux
*
* @brief File containing the definition of the ExteriorDomainMap interface, which is used to define the mapping from reference coordinates to physical coordinates in the exterior domain.
*/
#include <string_view>
#include <memory>
#include "serif/backend/mfem/mapping/status.hpp"
#include "serif/backend/mfem/mapping/compactification/input_result.hpp"
#include "serif/backend/mfem/mapping/compactification/variation.hpp"
namespace serif::mapping::compactification {
/**
* @class ExteriorDomainMap
*
* @brief Abstract interface for some mapping from reference coordinates to physical coordinates in the exterior domain.
* Currently the only concrete implementation of this is the kelvin compactification.
*/
class ExteriorDomainMap {
public:
virtual ~ExteriorDomainMap() = default;
/**
* @brief Check if the mapping is affine in the displacement, that is if the mapping is linear in the displacement and does not depend on the displacement.
*
* @return True if the mapping is affine in the displacement, false otherwise.
*/
[[nodiscard]] virtual bool is_affine_in_displacement() const noexcept = 0;
/**
* @brief Evaluate the mapping at a given point in the reference domain.
*
* @param input The input to the mapping, which contains the reference coordinates and the displacement.
* @param result The result of the mapping, which contains the physical coordinates and the Jacobian of the mapping.
*
* @return The status of the mapping evaluation, which indicates if the evaluation was successful or if there was an error.
*
*/
[[nodiscard]] virtual MappingStatus evaluate(
const ExteriorMapInput &input,
ExteriorMapResult &result
) const = 0;
/**
* @brief Evaluate the variation of the mapping at a given point in the reference domain.
*
* @param input The input to the mapping, which contains the reference coordinates and the displacement.
* @param result The result of the mapping, which contains the physical coordinates and the Jacobian of the mapping.
* @param direction The direction of the variation, which is a vector in the reference domain.
* @param variation The variation of the mapping, which contains the variation of the physical coordinates and the variation of the Jacobian.
*
* @return The status of the mapping evaluation, which indicates if the evaluation was successful or if there was an error.
*/
[[nodiscard]] virtual MappingStatus evaluate_variation(
const ExteriorMapInput &input,
const ExteriorMapResult &result,
const ExteriorMapDirection &direction,
ExteriorMapVariation &variation
) const = 0;
/**
* @brief Return the name of the mapping, which is used for debugging and logging.
*
* @return The name of the mapping.
*/
[[nodiscard]] virtual std::string_view get_name() const noexcept = 0;
};
}

View File

@@ -0,0 +1,36 @@
#pragma once
/**
* @file input_result.hpp
* @author Emily M. Boudreaux
*
* @brief File containing the input and result structures for the compactification mapping.
*/
#include "mfem/linalg/vector.hpp"
#include "mfem/linalg/densemat.hpp"
namespace serif::mapping::compactification {
/**
* @struct ExteriorMapInput
*
* @brief Structure representing the input to the exterior domain mapping. This includes the reference position, displaced position, displacement Jacobian, compactification coordinate gradient, and compactification coordinate.
*/
struct ExteriorMapInput {
const mfem::Vector &reference_position; ///< Position in reference space to evaluate the mapping at.
const mfem::Vector &displaced_position; ///< Position in displaced space to evaluate the mapping at (note this is displaced but not yet compactified).
const mfem::DenseMatrix &displacement_jacobian; ///< Jacobian of the displacement mapping at the reference position.
const mfem::Vector &compactification_coordinate_gradient; ///< Gradient of the compactification coordinate at the reference position.
double compactification_coordinate; ///< The compactification coordinate at the reference position. This ranges from 0 to 1, where 0 is the inner surface and 1 is the outer surface of the reference domain.
};
/**
* @struct ExteriorMapResult
*
* @brief Structure representing the result of the exterior domain mapping. This includes the physical position and the Jacobian of the mapping.
*/
struct ExteriorMapResult {
mfem::Vector physical_position; ///< Position in physical space after applying the mapping.
mfem::DenseMatrix mapping_jacobian; ///< Jacobian of the mapping at the reference position, which maps from reference coordinates to physical coordinates.
};
}

View File

@@ -0,0 +1,191 @@
#pragma once
/**
* @file kelvin.hpp
* @author Emily M. Boudreaux
*
* @brief File containing the definition of the KelvinCompactification class, which is a concrete implementation of the ExteriorDomainMap interface for the Kelvin compactification.
*/
#include <string_view>
#include "mfem/linalg/vector.hpp"
#include "serif/backend/mfem/mapping/status.hpp"
#include "serif/backend/mfem/mapping/compactification/compactification.hpp"
#include "serif/utils/user_options/mapping/compactification/kelvin.hpp"
namespace serif::mapping::compactification {
/**
* @class KelvinCompactification
*
* @brief Concrete implementation of the ExteriorDomainMap interface for the Kelvin compactification.
*
* Broadly, for some reference position \f$\mathbf{x_{r}}\f$ on some reference domain \f$\Omega_{r}\f$ the Kelvin compactification maps to a physical position \f$\mathbf{x_{p}}\f$ on some physical domain \f$\Omega_{p}\f$ through the mapping
* \f[
* \mathbf{x_{p}} = \frac{\mathbf{x_{r}}}{1 - \frac{|\mathbf{x_{r}}|}{R_{\infty}}}
* \f]
*
* This results in the inner surface of the domain being mapped to the exterior surface of the physical domain while the outer
* surface of the exterior domain is mapped to infinity. There are some implications of this, namely that we
* loose accuracy in all things as we approach the outer surface of the domain. However, it is nice in so far
* as we can apply true boundary conditions at infinity.
*/
class KelvinCompactification final : public ExteriorDomainMap {
public:
/**
* @brief Construct a KelvinCompactification with the given options.
*
* @param options The options for the Kelvin compactification, which must be valid.
*
* @throws std::invalid_argument If the reference stellar radius is not finite or positive
* @throws std::invalid_argument If the reference infinity radius is not finite or positive
* @throws std::invalid_argument If the reference infinity radius is not greater than the reference stellar radius
* @throws std::invalid_argument If the coordinate tolerance is not finite or positive or is greater than or equal to one
*/
explicit KelvinCompactification(
const utils::user_options::KelvinCompactificationOptions& options
);
/**
* @brief Check if the mapping is affine in the displacement. For the Kelvin compactification, this is always true
*
* @return false
*/
[[nodiscard]] bool is_affine_in_displacement() const noexcept override;
/**
* @brief Evaluate the mapping at a given point in the reference domain.
*
* @param[in] input The input point in the reference domain to evaluate the mapping at.
* @param[out] result The result of the mapping evaluation, which will be filled in by the method.
*
* @return A status indicating the success or failure of the mapping evaluation. Mapping may fail if
* - the size of the input reference position is less than or equal to zero
* - the input displaced position size does not match the input reference position size
* - the input compactification coordinate gradient size does not match the input reference position size
* - the displacement_jacobian height does not match the input reference position size
* - the displacement_jacobian width does not match the input reference position size
* - the input reference, displaced, compactification coordinate gradient or jacobian contains non-finite values
* - There is some issue computing the radial factors (this error will be passed through)
*
* The mapping jacobian comes from trivial application of the product rule
* recall: \f$r_i = scale * x_i\f$ where \f$r\f$ is the physical position and \f$x\f$ is the displaced position.
* then we can differentiate wrt. \f$X_j\f$ holding nothing fixed. Note the capital \f$X\f$ here, this is the mesh coordinate not the displaced position.
* Lets call this jacobian \f$F\f$
* \f[
* F = \frac{\partial r_i}{\partial X_{j}}
* \f]
*
* \f$F\f$ then tells us how the physical position changes as we move along mesh coordinates
*
* Lets then apply this to the function we have for the kelvin compactification
* \f[
* F = scale * \frac{\partial x_i}{\partial X_j} + x_i * \frac{\partial scale}{\partial X_j}
* \f]
*
* In the implementation you can see the displacement jacobian (\f$\frac{\partial x_i}{\partial X_j}\f$) and the scale derivative (\f$\frac{\partial scale}{\partial X_j}\f$) being applied to compute the mapping jacobian.
*/
[[nodiscard]] MappingStatus evaluate(
const ExteriorMapInput& input,
ExteriorMapResult& result
) const override;
/**
* @brief Evaluate the variation of the mapping at a given point in the reference domain.
*
* @param[in] input The input point in the reference domain to evaluate the mapping at.
* @param[in] result The result of the mapping evaluation, which will be used in the variation computation.
* @param[in] direction The direction in which to evaluate the variation.
* @param[out] variation The result of the variation evaluation, which will be filled in by the method.
*
* @return A status indicating the success or failure of the variation evaluation. The reasons for a failure are effectively the same
* as the reasons for a failure in the evaluate method just extended to cover the additional inputs.
*/
[[nodiscard]] MappingStatus evaluate_variation(
const ExteriorMapInput& input,
const ExteriorMapResult& result,
const ExteriorMapDirection& direction,
ExteriorMapVariation& variation
) const override;
/**
* @brief Get the name of the mapping, which is "KelvinCompactification" for this class.
* @return The name of the mapping.
*/
[[nodiscard]] std::string_view get_name() const noexcept override;
/**
* @brief Get the reference stellar radius for the mapping, which is the radius of the inner surface of the reference domain.
* @return The reference stellar radius for the mapping.
*/
[[nodiscard]] double get_reference_stellar_radius() const noexcept;
/**
* @brief Get the reference infinity radius for the mapping, which is the radius of the outer surface of the reference domain.
* @return The reference infinity radius for the mapping.
*/
[[nodiscard]] double get_reference_infinity_radius() const noexcept;
/**
* @brief Get the coordinate tolerance for the mapping, which is the tolerance for the compactification coordinate.
* @return The coordinate tolerance for the mapping.
*/
[[nodiscard]] double get_coordinate_tolerance() const noexcept;
/**
* @brief Evaluate the mapping jacobian variation factors at some given compactification coordinate.
*
* @param[in] compactification_coordinate The compactification coordinate at which to evaluate the mapping jacobian variation factors. (Note this runs from 0 to 1, where 0 is the inner surface and 1 is the outer surface of the reference domain)
* @param[in] coordinateGradient The gradient of the compactification coordinate with respect to the mesh coordinates, which must be finite and have size 3.
*
* @param[out] scale The scale factor for the mapping jacobian, which will be filled in by the method.
* @param[out] scaleGradient The gradient of the scale factor with respect to the mesh coordinates, which will be filled in by the method.
*
* @return The status of the mapping evaluation.
*/
[[nodiscard]] MappingStatus evaluate_jacobian_variation_factors(
double compactification_coordinate,
const mfem::Vector &coordinateGradient,
double &scale,
mfem::Vector &scaleGradient
) const;
private:
/**
* @struct RadialFactors
*
* @brief A struct to hold the radial factors for the Kelvin compactification mapping.
*/
struct RadialFactors {
double compactification_coordinate; ///< The compactification coordinate, which runs from 0 to 1, where 0 is the inner surface and 1 is the outer surface of the reference domain.
double computational_radius; ///< The computational radius, which is the radius of the reference domain at the given compactification coordinate.
double scale; ///< The scale factor for the mapping jacobian.
double scale_derivative; ///< The derivative of the scale factor with respect to the compactification coordinate.
};
private:
/**
* @brief Compute the radial factors for the Kelvin compactification mapping at a given compactification coordinate.
*
* @param[in] compactification_coordinate The compactification coordinate at which to compute the radial factors. (Note this runs from 0 to 1, where 0 is the inner surface and 1 is the outer surface of the reference domain)
* @param[out] radial_factors The radial factors, which will be filled in by the method.
*
* @return The status of the mapping evaluation.
*
* @note See the implementation file for more details on the actual algorithm used here.
*/
[[nodiscard]] MappingStatus compute_radial_factors(
double compactification_coordinate,
RadialFactors &radial_factors
) const;
private:
utils::user_options::KelvinCompactificationOptions m_options; ///< The options for the Kelvin compactification, which must be valid.
const std::string_view m_name = "KelvinCompactification"; ///< The name of the mapping, which is "KelvinCompactification" for this class.
};
}

View File

@@ -0,0 +1,35 @@
#pragma once
/**
* @file variation.hpp
* @author Emily M. Boudreaux
*
* @brief File containing the structures for the variation of the compactification mapping.
*/
#include "mfem/linalg/vector.hpp"
#include "mfem/linalg/densemat.hpp"
namespace serif::mapping::compactification {
/**
* @struct ExteriorMapDirection
*
* @brief Structure representing the direction of the variation in the exterior domain mapping.
* This includes the variation of the displaced position and the variation of the displacement Jacobian.
*/
struct ExteriorMapDirection {
const mfem::Vector &displaced_position_variation; ///< Variation of the position in displaced space.
const mfem::DenseMatrix &displacement_jacobian_variation; ///< Variation of the Jacobian of the displacement mapping.
};
/**
* @struct ExteriorMapVariation
*
* @brief Structure representing the variation of the exterior domain mapping.
* This includes the variation of the physical position and the variation of the mapping Jacobian.
*/
struct ExteriorMapVariation {
mfem::Vector physical_position_variation; ///< Variation of the position in physical space.
mfem::DenseMatrix mapping_jacobian_variation; ///< Variation of the Jacobian of the mapping.
};
}

View File

@@ -0,0 +1,92 @@
#pragma once
/**
* @file context.hpp
* @author Emily M. Boudreaux
*
* @brief File containing the mapping context structures for volume and face mappings.
*
* @todo Like a lot of the parts of the mapping module written before I started factoring mfem stuff into
* backend modules, a good amount of this might be movable to some mfem backend
*
* All of these different "context" structures are built to contain sufficient information that the
* caller which gets one of these back can evaluate the mapping and its derivatives properly
* at some point.
*/
#include "mfem/linalg/densemat.hpp"
#include "mfem/linalg/vector.hpp"
namespace serif::mapping {
/**
* @struct VolumeQuadratureContext
*
* @brief A structure representing the context of a volume quadrature point, including the inverse Jacobian of the mapping and the determinant of the mapping.
*
* @todo Add a worked example here
*/
struct VolumeQuadratureContext {
mfem::DenseMatrix J_element_to_physical_inverse; ///< The inverse Jacobian of the mapping from the element to the physical space.
double mapping_determinate; ///< The determinant of the mapping from the element to the physical space,
double physical_integration_weight; ///< The physical integration weight for the quadrature point.
};
/**
* @struct FaceQuadratureContext
*
* @brief A structure representing the context of a face quadrature point, including the normal vector, surface weight, and scale factor for the dot product of the velocity and normal vector.
*
* @todo Add a worked example here
*/
struct FaceQuadratureContext {
mfem::Vector normal; ///< The normal vector at the face quadrature point.
double ds; ///< The surface weight for the face quadrature point.
double v_dot_n_scale; ///< The scale factor for the dot product of the velocity and normal vector at the face quadrature point.
};
/**
* @struct MappingPointContext
*
* @brief A structure representing the context of a mapping point, including the reference position, displaced position, physical position, displacement Jacobian, mapping Jacobian, inverse mapping Jacobian, mapping determinant, and compactification status.
*
* @todo Add a worked example here
*/
struct MappingPointContext {
mfem::Vector reference_position; ///< The reference position of the mapping point.
mfem::Vector displaced_position; ///< The displaced position of the mapping point.
mfem::Vector physical_position; ///< The physical position of the mapping point.
mfem::DenseMatrix displacement_jacobian; ///< The displacement Jacobian of the mapping point.
mfem::DenseMatrix mapping_jacobian; ///< The mapping Jacobian of the mapping point.
mfem::DenseMatrix inverse_mapping_jacobian; ///< The inverse mapping Jacobian of the mapping point.
double mapping_determinate{0.0}; ///< The determinant of the mapping Jacobian at the mapping point.
bool compactified{false}; ///< A flag indicating whether the mapping point is compactified (true) or not (false).
};
/**
* @struct VolumeMappingContext
*
* @brief A structure representing the context of a volume mapping, including the mapping point context and volume quadrature context.
*
* @todo Add a worked example here
*/
struct VolumeMappingContext {
MappingPointContext mapping; ///< The mapping point context for the volume mapping.
VolumeQuadratureContext quadrature; ///< The volume quadrature context for the volume mapping.
};
/**
* @struct FaceMappingContext
*
* @brief A structure representing the context of a face mapping, including the mapping point context and face quadrature context.
*
* @todo Add a worked example here
*/
struct FaceMappingContext {
MappingPointContext mapping; ///< The mapping point context for the face mapping.
FaceQuadratureContext quadrature; ///< The face quadrature context for the face mapping.
mfem::Vector reference_normal; ///< The reference normal vector for the face mapping.
double reference_surface_weight{0.0}; ///< The reference surface weight for the face mapping.
double physical_surface_weight{0.0}; ///< The physical surface weight for the face mapping.
};
}

View File

@@ -0,0 +1,254 @@
#pragma once
/**
* @file data.hpp
* @author Emily M. Boudreaux
*
* @brief File containing some misc data structures for the mapping module. This was the very first file
* I ported over from MeanField and I think because of that its organization is actually quite wonky.
* For one its int this poorly named "data" file, which could mean any number of things. Regardless,
* these are basically low leval containers for useful stuff and also some scratch workspaces
* for mapping operations.
*
* @todo As said above there is some refactoring to make this cleaner which could happen.
*/
#include "mfem/fem/fe/fe_base.hpp"
#include "mfem/fem/fespace.hpp"
#include "mfem/linalg/vector.hpp"
#include "mfem/linalg/densemat.hpp"
#include "mfem/mesh/mesh.hpp"
#include <cstdint>
#include "serif/backend/mfem/mapping/compactification/kelvin.hpp"
#include "serif/backend/mfem/mapping/context.hpp"
#include "serif/backend/mfem/mapping/data.hpp"
namespace serif::mapping {
/**
* @enum FaceElementSide
*
* @brief An enumeration representing the side of a face element, either ELEMENT_1 or ELEMENT_2. There is no inherent ordering
* to these; however, broadly I have been thinking about ELEMENT_1 as the "inner" side and ELEMENT_2 as the "outer" side.
*/
enum class FaceElementSide : uint8_t {
ELEMENT_1, ///< Represents the first side of a face element.
ELEMENT_2 ///< Represents the second side of a face element.
};
/**
* @class ElementDisplacementData
*
* @brief A class representing the displacement data for a finite element, including the finite element, degree of freedom matrix, and dimension.
*/
class ElementDisplacementData {
public:
/**
* @brief Constructs an ElementDisplacementData object with the given finite element and displacement degrees of freedom.
*
* @param element The finite element associated with the displacement data.
* @param displacement_dofs The element vector of displacement degrees of freedom, ordered by nodes within each component as MFEM element vdofs are.
*
* @throws std::invalid_argument if the finite element has zero or negative degrees of freedom,
* or if the size of the displacement_dofs vector is not a positive multiple of the element's degree of freedom count.
*
* The dof matrix is populated such that for row i and column j, with n total dofs, dof_matrix(i, j) = displacement_dofs(i + j * n).
*/
ElementDisplacementData(
const mfem::FiniteElement& element,
mfem::Vector& displacement_dofs
);
/**
* @brief Returns the finite element associated with the displacement data.
* @return A constant reference to the finite element.
*/
[[nodiscard]] const mfem::FiniteElement& get_element() const noexcept;
/**
* @brief Returns the degree of freedom matrix for the displacement data.
* @return A constant reference to the degree of freedom matrix.
*/
[[nodiscard]] const mfem::DenseMatrix &get_dof_matrix() const noexcept;
/**
* @brief Returns the dimension of the displacement data.
* @return The dimension of the displacement data.
*/
[[nodiscard]] int get_dimension() const noexcept;
/**
* @brief Returns the degree of freedom count for the displacement data.
* @return The degree of freedom count for the displacement data.
*/
[[nodiscard]] int get_dof_count() const noexcept;
private:
const mfem::FiniteElement *m_element; ///< Pointer to the finite element associated with the displacement data.
mfem::DenseMatrix m_dof_matrix; ///< Matrix representing the degrees of freedom for the displacement data.
int m_dimension; ///< The dimension of the displacement data (number of components).
};
/**
* @struct CompactificationPointData
*
* @brief A structure representing the compactification point data, including the compactification coordinate and coordinate gradient.
*/
struct CompactificationPointData {
double compactification_coordinate{0.0}; ///< The compactification coordinate value at the point.
mfem::Vector coordinate_gradient; ///< The gradient of the compactification coordinate at the point.
};
/**
* @brief Creates an ElementDisplacementData object from a finite element and a vector of displacement degrees of freedom.
*
* @param element The finite element associated with the displacement data.
* @param displacement_dofs The vector containing the displacement degrees of freedom.
* @return An ElementDisplacementData object constructed from the provided finite element and displacement degrees of freedom.
*
* @note This is just a convenience function which calls the constructor for ElementDisplacementData.
*/
[[nodiscard]] ElementDisplacementData element_displacement_data_from_element_vdofs(
const mfem::FiniteElement& element,
mfem::Vector &displacement_dofs
);
/**
* @class ElementCompactificationData
*
* @brief A class representing the compactification data for a finite element, including the finite element and degrees of freedom.
*/
class ElementCompactificationData {
public:
/**
* @brief Constructs an ElementCompactificationData object with the given finite element and degrees of freedom.
*
* @param element The finite element associated with the compactification data.
* @param dofs The vector containing the degrees of freedom for the compactification data.
*
* @throws std::invalid_argument if the finite element is not a scalar finite element, does not provide a gradient, or has zero or negative degrees of freedom,
* or if the size of the dofs vector does not match the finite element's degree of freedom count.
*/
ElementCompactificationData(
const mfem::FiniteElement& element,
const mfem::Vector& dofs
);
/**
* @brief Returns the finite element associated with the compactification data.
* @return A constant reference to the finite element.
*/
[[nodiscard]] const mfem::FiniteElement& get_element() const noexcept;
/**
* @brief Returns the degrees of freedom for the compactification data.
* @return A constant reference to the vector of degrees of freedom.
*/
[[nodiscard]] const mfem::Vector &get_dofs() const noexcept;
/**
* @brief Returns the degree of freedom count for the compactification data.
* @return The degree of freedom count for the compactification data.
*/
[[nodiscard]] int get_dof_count() const noexcept;
private:
const mfem::FiniteElement *m_element; ///< Pointer to the finite element associated with the compactification data.
mfem::Vector m_dofs; ///< Vector representing the degrees of freedom for the compactification data.
};
struct ElementMappingData {
const ElementDisplacementData &displacement;
const ElementCompactificationData &compactification;
};
/**
* @class DMWorkspace
*
* @brief A class representing a workspace for displacement mapping operations, including various vectors and matrices used in the mapping process.
* primarily this is just a separate class so we can more easily make this code thread-safe in the future. At construction of
* an instance of this class loads of Heap allocation is done (or really whatever mfem does internally for allocation for vector which dependents on
* the selected device) and then that allocation can be reused.
*/
class DMWorkspace {
public:
/**
* @brief Constructs a DMWorkspace object with the specified dimension.
*
* @param dimension The dimension of the workspace (default is 3).
*
* @throws std::invalid_argument if the specified dimension is not positive and finite.
*/
explicit DMWorkspace (int dimension = 3);
/**
* @brief Sets the dimension of the workspace.
*
* @param dimension The new dimension of the workspace.
*
* @throws std::invalid_argument if the specified dimension is not positive and finite.
*
* @post All internal vectors are sized to dimension
* @post All internal matrices are sized to dimension x dimension
* @post The compactification coordinate is set to 0.0
*/
void set_dimension(int dimension);
/**
* @brief Returns the dimension of the workspace.
* @return The dimension of the workspace.
*/
[[nodiscard]] int get_dimension() const noexcept;
public: // This is a workspace class, these members are kept public so we do not need indirection through getters to access them
mfem::Vector m_shape; ///< Pre-allocated vector for shape function values at a quadrature point.
mfem::DenseMatrix m_reference_dshape; ///< Pre-allocated matrix for reference shape function derivatives at a quadrature point.
mfem::DenseMatrix m_mesh_dshape; ///< Pre-allocated matrix for mesh shape function derivatives at a quadrature point.
mfem::DenseMatrix m_reference_field_jacobian; ///< Pre-allocated matrix for the reference field Jacobian at a quadrature point.
mfem::Vector m_field_value; ///< Pre-allocated vector for field values at a quadrature point.
mfem::DenseMatrix m_field_jacobian; ///< Pre-allocated matrix for field Jacobian at a quadrature point.
mfem::Vector m_compactification_shape; ///< Pre-allocated vector for compactification shape function values at a quadrature point.
mfem::DenseMatrix m_compactification_dshape; ///< Pre-allocated matrix for compactification shape function derivatives at a quadrature point.
CompactificationPointData m_compactification_point; ///< Pre-allocated data for compactification point.
mfem::Vector m_reference_normal; ///< Pre-allocated vector for reference normal at a quadrature point.
mfem::Vector m_mapped_normal; ///< Pre-allocated vector for mapped normal at a quadrature point.
mfem::DenseMatrix m_full_element_jacobian; ///< Pre-allocated matrix for full element Jacobian at a quadrature point.
mfem::Vector m_vector_temp; ///< Pre-allocated temporary vector for intermediate calculations.
mfem::DenseMatrix m_matrix_temp_1; ///< Pre-allocated temporary matrix for intermediate calculations.
mfem::DenseMatrix m_matrix_temp_2; ///< Pre-allocated temporary matrix for intermediate calculations.
compactification::ExteriorMapResult m_exterior_result; ///< Pre-allocated result for exterior mapping at a quadrature point.
compactification::ExteriorMapVariation m_exterior_variation; ///< Pre-allocated variation for exterior mapping at a quadrature point.
private:
int m_dimension; ///< The number of spatial dimensions for the workspace (e.g., 2D or 3D).
};
/**
* @struct DMFixedMeshGeometry
*
* @brief A structure representing the fixed mesh geometry at a specific point, including position, Jacobian, inverse Jacobian, and weight.
*/
struct DMFixedMeshGeometry {
mfem::Vector position; ///< The physical position of the point in the mesh.
mfem::DenseMatrix jacobian; ///< The Jacobian matrix of the mapping from the reference element to the physical element at the point.
mfem::DenseMatrix inverse_jacobian; ///< The inverse of the Jacobian matrix of the mapping from the reference element to the physical element at the point.
double weight{0.0}; ///< The mapping weight associated with the point, typically used for integration purposes.
};
/**
* @struct DMReferenceDisplacementSample
*
* @brief A structure representing a sample of the reference displacement, including the value and gradient of the displacement at a specific point.
*/
struct DMReferenceDisplacementSample {
const mfem::Vector& value; ///< The value of the reference displacement at the point.
const mfem::DenseMatrix& gradient; ///< The gradient of the reference displacement at the point.
};
}

View File

@@ -0,0 +1,219 @@
#pragma once
#include <expected>
#include <memory>
#include "mfem/linalg/vector.hpp"
#include "mfem/linalg/densemat.hpp"
#include "mfem/fem/eltrans.hpp"
#include "mfem/fem/intrules.hpp"
#include "serif/backend/mfem/mapping/compactification/kelvin.hpp"
#include "serif/backend/mfem/mapping/context.hpp"
#include "serif/backend/mfem/mapping/data.hpp"
#include "serif/backend/mfem/mapping/variation.hpp"
#include "serif/utils/user_options/mapping/domain_mapper.hpp"
namespace serif::mapping {
/** @brief Sample immutable mesh geometry; callers decide how long its mesh revision remains valid. */
[[nodiscard]] std::expected<DMFixedMeshGeometry, MappingStatus> sample_fixed_mesh_geometry(
mfem::ElementTransformation& transformation, const mfem::IntegrationPoint& point
);
class DomainMapper {
public:
DomainMapper(const DomainMapper&) = delete;
DomainMapper& operator=(const DomainMapper&) = delete;
~DomainMapper() = default;
DomainMapper(DomainMapper&&) = default;
DomainMapper& operator=(DomainMapper&&) = default;
DomainMapper(
utils::user_options::DomainMapperOptions options,
std::unique_ptr<const compactification::ExteriorDomainMap> exterior_map
);
[[nodiscard]] MappingStatus evaluate_point(
const ElementMappingData& element_data,
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint& ip,
DMWorkspace& workspace,
MappingPointContext& context
) const;
/**
* @brief Map a point using checked fixed geometry sampled by the caller.
* @note Geometry must come from sample_fixed_mesh_geometry for this exact element, point,
* and mesh revision. Dimensions, finite values, positive weight and invertibility are checked; identity and
* freshness remain the caller's responsibility. No sample is retained by the mapper.
*/
[[nodiscard]] MappingStatus evaluate_point(
const ElementMappingData& element_data,
mfem::ElementTransformation& transformation,
const mfem::IntegrationPoint& ip,
DMWorkspace& workspace,
MappingPointContext& context,
const DMFixedMeshGeometry& fixed_geometry
) const;
/** @brief Map a volume point, optionally reusing fixed geometry owned by the reference graph node.
* @note Supplied geometry must belong to this element and point at the current mesh revision.
*/
[[nodiscard]] MappingStatus evaluate_volume(
const ElementMappingData& element_data,
mfem::ElementTransformation& transformation,
const mfem::IntegrationPoint& ip,
DMWorkspace& workspace,
VolumeMappingContext& context,
const DMReferenceDisplacementSample *displacement_sample = nullptr,
const DMFixedMeshGeometry* fixed_geometry = nullptr
) const;
[[nodiscard]] MappingStatus evaluate_face(
const ElementMappingData& element_data,
mfem::FaceElementTransformations& transformation,
FaceElementSide side,
const mfem::IntegrationPoint& ip,
DMWorkspace& workspace,
FaceMappingContext& context
) const;
[[nodiscard]] MappingStatus evaluate_point_variation(
const ElementMappingData& element_data,
const ElementDisplacementData& direction,
mfem::ElementTransformation& transformation,
const mfem::IntegrationPoint& ip,
const MappingPointContext& context,
DMWorkspace& workspace,
MappingPointVariation& variation
) const;
/**
* @brief Differentiate a point map using its already sampled fixed-mesh inverse Jacobian.
* @note The inverse must come from the checked fixed geometry for this exact element,
* point and mesh revision; context must describe the same accepted displacement. Size and
* finite values are checked. Non-VALUE finite elements retain their original physical-gradient
* evaluation path. Neither the inverse nor any field sample is retained.
*/
[[nodiscard]] MappingStatus evaluate_point_variation(
const ElementMappingData& element_data,
const ElementDisplacementData& direction,
mfem::ElementTransformation& transformation,
const mfem::IntegrationPoint& ip,
const MappingPointContext& context,
DMWorkspace& workspace,
MappingPointVariation& variation,
const mfem::DenseMatrix& inverse_mesh_jacobian
) const;
/**
* @brief Sample the fixed compactification coordinate and its gradient in mesh coordinates.
* @param inverse_mesh_jacobian Optional inverse Jacobian from the same element and point.
* @note Reuse this sample for several displacement directions at this point. Refresh it when
* the compactification coefficients, element, point or reference mesh change.
*/
[[nodiscard]] MappingStatus evaluate_compactification_coordinate(
const ElementCompactificationData& compactification,
mfem::ElementTransformation& transformation,
const mfem::IntegrationPoint& ip,
DMWorkspace& workspace,
CompactificationPointData& point_data,
const mfem::DenseMatrix *inverse_mesh_jacobian = nullptr
) const;
/**
* @brief Differentiate the physical volume map along one of several directions sharing this point.
* @param direction_sample The displacement direction's value and canonical-element gradient at this point.
* @param compactification_sample Must match this point and the frozen coefficients; it is ignored on
* interior elements. Its dimension and finite values are checked before use.
* @param inverse_mesh_jacobian context.quadrature.J_element_to_physical_inverse times
* context.mapping.mapping_jacobian, formed once per point by the caller.
*/
[[nodiscard]] MappingStatus evaluate_volume_direction_variation(
const ElementMappingData& element_data,
const ElementDisplacementData& direction,
mfem::ElementTransformation& transformation,
const mfem::IntegrationPoint& ip,
const VolumeMappingContext& context,
DMWorkspace& workspace,
VolumeMappingVariation& variation,
const DMReferenceDisplacementSample& direction_sample,
const CompactificationPointData* compactification_sample,
const mfem::DenseMatrix& inverse_mesh_jacobian
) const;
[[nodiscard]] MappingStatus evaluate_face_variation(
const ElementMappingData& element_data,
const ElementDisplacementData& direction,
mfem::FaceElementTransformations &transformation,
FaceElementSide side,
const mfem::IntegrationPoint& ip,
const FaceMappingContext& context,
DMWorkspace& workspace,
FaceMappingVariation& variation
) const;
[[nodiscard]] bool is_compactified_element(const mfem::ElementTransformation& transformation) const noexcept;
[[nodiscard]] int get_dimension() const noexcept;
/** @brief Expose the immutable mapping prescription for reproducible reconstruction. */
[[nodiscard]] const utils::user_options::DomainMapperOptions& options() const noexcept { return m_options; }
[[nodiscard]] const compactification::ExteriorDomainMap& get_exterior_map() const noexcept;
private:
[[nodiscard]] MappingStatus evaluate_point_impl(
const ElementMappingData& element_data,
mfem::ElementTransformation& transformation,
const mfem::IntegrationPoint& ip,
DMWorkspace& workspace,
MappingPointContext& context,
const DMFixedMeshGeometry& geometry,
const DMReferenceDisplacementSample* displacement_sample = nullptr
) const;
void validate_element_data(
const ElementMappingData& element_data
) const;
void evaluate_displacement_field(
const ElementDisplacementData& field,
mfem::ElementTransformation& transformation,
const mfem::IntegrationPoint& ip,
DMWorkspace& workspace,
mfem::Vector& value,
mfem::DenseMatrix& jacobian,
const mfem::DenseMatrix *inverse_mesh_jacobian
) const;
[[nodiscard]] MappingStatus evaluate_point_variation_impl(
const ElementMappingData& element_data,
const ElementDisplacementData& direction,
mfem::ElementTransformation& transformation,
const mfem::IntegrationPoint& ip,
const MappingPointContext& context,
DMWorkspace& workspace,
MappingPointVariation& variation,
const mfem::DenseMatrix *inverse_mesh_jacobian,
const DMReferenceDisplacementSample* direction_sample = nullptr,
const CompactificationPointData* compactification_sample = nullptr
) const;
// These are static since they do not use any class members or workspace. Really these could and
// maybe should be free functions...
[[nodiscard]] static mfem::ElementTransformation& select_face_element_transformation(
const mfem::FaceElementTransformations& transformations,
FaceElementSide side
);
[[nodiscard]] static const mfem::IntegrationPoint& select_face_element_integration_point(
const mfem::FaceElementTransformations& transformations,
FaceElementSide side
);
private:
utils::user_options::DomainMapperOptions m_options;
std::unique_ptr<const compactification::ExteriorDomainMap> m_exterior_map;
};
}

View File

@@ -0,0 +1,82 @@
#pragma once
#include <memory>
#include "mfem/fem/gridfunc.hpp"
#include "mfem/fem/eltrans.hpp"
#include "mfem/fem/intrules.hpp"
#include "mfem/linalg/vector.hpp"
#include "mfem/general/array.hpp"
#include "serif/backend/mfem/mapping/domain_mapper.hpp"
#include "serif/backend/mfem/mapping/context.hpp"
#include "serif/backend/mfem/mapping/status.hpp"
namespace serif::mapping {
// Note this is NOT thread safe.
class GridFunctionMappingEvaluator {
public:
/*
* The evaluator references the supplied grid functions and caches copies of
* their element-local DOFs. You need to make sure you call InvalidateCache() or Refresh() after either
* grid function's values are modified. Changes to the finite-element-space sequence
* should be detected automatically.
*
* This object owns mutable workspace and cache state and *is not* thread-safe.
*/
GridFunctionMappingEvaluator(
const DomainMapper& mapper,
const mfem::GridFunction& displacement,
const mfem::GridFunction& compactification_coordinate
);
/** @brief Identify the mesh supplying the mapping fields so callers can validate shared geometry. */
[[nodiscard]] const mfem::Mesh* mesh() const noexcept {
const mfem::FiniteElementSpace* space = m_displacement.FESpace();
return space == nullptr ? nullptr : space->GetMesh();
}
void invalidate_cache() noexcept;
void refresh();
[[nodiscard]] MappingStatus evaluate_point(
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
MappingPointContext &context
);
[[nodiscard]] MappingStatus evaluate_volume(
mfem::ElementTransformation &transformation,
const mfem::IntegrationPoint &integration_point,
VolumeMappingContext &context
);
[[nodiscard]] MappingStatus evaluate_face(
mfem::FaceElementTransformations &transformation,
FaceElementSide side,
const mfem::IntegrationPoint &integration_point,
FaceMappingContext &context
);
private:
void validate_field_bindings() const;
[[nodiscard]] bool invalidate_for_changed_spaces();
void load_element(int element_id);
private:
const DomainMapper &m_mapper;
const mfem::GridFunction &m_displacement;
const mfem::GridFunction &m_compactification_coordinate;
const mfem::FiniteElementSpace *m_displacement_space;
const mfem::FiniteElementSpace *m_compactification_space;
long m_displacement_space_sequence;
long m_compactification_space_sequence;
DMWorkspace m_workspace;
mfem::Array<int> m_displacement_dofs;
mfem::Array<int> m_compactification_dofs;
mfem::Vector m_element_displacement;
mfem::Vector m_element_compactification;
std::unique_ptr<ElementDisplacementData> m_displacement_data;
std::unique_ptr<ElementCompactificationData> m_compactification_data;
int m_cached_element_id{-1};
};
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include <unordered_map>
#include <cstdint>
#include <utility>
#include "serif/utils/error/codes.hpp"
#include "serif/meta/enums.hpp"
namespace serif::mapping
{
enum class MappingStatus : uint16_t {
VALID,
// Start this at 400 so this is parallel to the mapping error codes. There must be a better way to
// do this but it is not worth the time to sort out now
INVALID_DIMENSION = 400,
NON_FINITE_MAPPING_INPUT,
INVALID_REFERENCE_RADIUS,
AT_COMPACTIFIED_INFINITY,
OUTSIDE_REFERENCE_DOMAIN,
NON_FINITE_MAPPING_RESULT,
NON_POSITIVE_DETERMINATE
};
static auto MappingStatus_enum_string_map = meta::enum_to_enum_string_map<MappingStatus>();
static auto MappingStatus_string_enum_map = meta::enum_to_string_enum_map<MappingStatus>();
inline std::unordered_map<MappingStatus, utils::errors::SERiFErrorCode> make_MappingStatus_SERiFErrorCode_map() {
std::unordered_map<MappingStatus, utils::errors::SERiFErrorCode> ret;
for (const auto& [MS, MS_name] : MappingStatus_enum_string_map) {
if (std::to_underlying(MS) < 400) {
continue; // Skip VALID since it is not in the error code set
}
ret.insert({MS, utils::errors::SERiFErrorCodeNameToTypeMap.at(MS_name)});
}
return ret;
}
inline std::unordered_map<utils::errors::SERiFErrorCode, MappingStatus> make_SERIFErrorCode_MappingStatus_map() {
std::unordered_map<utils::errors::SERiFErrorCode, MappingStatus> ret;
for (const auto& [MS, MS_name] : MappingStatus_enum_string_map) {
if (std::to_underlying(MS) < 400) {
continue; // Skip VALID since it is not in the error code set
}
ret.insert({utils::errors::SERiFErrorCodeNameToTypeMap.at(MS_name), MS});
}
return ret;
}
// We make these parallel lists so to ensure that we can lookup between SERiF error codes
// and equivalent MappingStatus. Note again this is ugly and bad and I am ashamed of this code....
// anyhoodles there is likely a better way but pick your battles amiright?
static auto MappingStatus_SERiFErrorCode_map = make_MappingStatus_SERiFErrorCode_map();
static auto SERiFErrorCode_MappingStatus_map = make_SERIFErrorCode_MappingStatus_map();
}

View File

@@ -0,0 +1,60 @@
#pragma once
#include "serif/backend/mfem/mapping/variation.hpp"
#include "serif/backend/mfem/mapping/context.hpp"
namespace serif::mapping {
void map_reference_gradient_to_physical(
const MappingPointContext& context,
const mfem::Vector& reference_gradient,
mfem::Vector& physical_gradient
);
void map_physical_gradient_to_reference(
const MappingPointContext& context,
const mfem::Vector& physical_gradient,
mfem::Vector& reference_gradient
);
void map_reference_vector_gradient_to_physical(
const MappingPointContext& context,
const mfem::DenseMatrix& reference_vector_gradient,
mfem::DenseMatrix& physical_vector_gradient
);
void map_physical_vector_gradient_to_reference(
const MappingPointContext& context,
const mfem::DenseMatrix& physical_vector_gradient,
mfem::DenseMatrix& reference_vector_gradient
);
void compute_scalar_diffusion_tensor(
const MappingPointContext& context,
mfem::DenseMatrix& diffusion_tensor
);
void map_hcurl_field_to_physical(
const MappingPointContext& context,
const mfem::Vector& reference_field,
mfem::Vector& physical_field
);
void map_physical_field_to_hcurl_reference(
const MappingPointContext& context,
const mfem::Vector& physical_field,
mfem::Vector& reference_field
);
void map_hcurl_curl_to_physical(
const MappingPointContext& context,
const mfem::Vector &reference_curl,
mfem::Vector &physical_curl
);
void map_physical_curl_to_hcurl_reference(
const MappingPointContext& context,
const mfem::Vector& physical_curl,
mfem::Vector& reference_curl
);
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include "mfem/linalg/densemat.hpp"
#include "mfem/linalg/vector.hpp"
namespace serif::mapping {
struct MappingPointVariation {
mfem::Vector displacement_variation;
mfem::Vector physical_position_variation;
mfem::DenseMatrix displacement_jacobian_variation;
mfem::DenseMatrix mapping_jacobian_variation;
mfem::DenseMatrix inverse_mapping_jacobian_variation;
double mapping_determinate_variation{0.0};
};
struct VolumeMappingVariation {
MappingPointVariation mapping;
double weight_variation{0.0};
};
struct FaceMappingVariation {
MappingPointVariation mapping;
mfem::Vector physical_normal_variation;
double physical_surface_weight_variation{0.0};
double normal_flux_scale_variation{0.0};
};
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include "mfem/linalg/densemat.hpp"
#include "serif/backend/mfem/mapping/context.hpp"
#include <vector>
namespace serif::mapping {
class VolumeMappingCache {
public:
void set_size(int point_count, int dimension);
void store(int point, VolumeMappingContext& context);
void load(int point, VolumeMappingContext& context) const;
void load_inverse_jacobian(int point, mfem::DenseMatrix& inverse) const;
[[nodiscard]] int get_point_count() const noexcept;
[[nodiscard]] int get_dimension() const noexcept;
private:
void validate_point(int point) const;
[[nodiscard]] const double* get_point_data(int point) const;
std::vector<double> m_data;
int m_point_count{0};
int m_dimension{0};
int m_point_stride{0};
};
}

View File

@@ -0,0 +1,144 @@
#pragma once
#include <cstddef>
#include <memory>
#include <tuple>
#include <type_traits>
#include <utility>
#include "serif/model/equilibrium_plan.hpp"
#include "serif/backend/mfem/model/inputs.hpp"
#include "serif/backend/mfem/geometry/surface/pressure_boundary.hpp"
#include "serif/backend/mfem/geometry/deformation/inputs.hpp"
namespace serif::model::backend::mfem
{
/** @brief Retain physical descriptions and all numerical bindings behind one stable owner. */
template <typename Model, typename DescriptionT, typename Discretization, typename Boundary, typename PlanT>
struct BoundModel
{
using Description = DescriptionT;
using Plan = PlanT;
std::shared_ptr<const Model> model;
Description definition;
std::shared_ptr<const Discretization> discretization;
Boundary boundary;
/** @brief Forward an equation binding without copying finite-element resources. */
template <typename Row>
[[nodiscard]] auto equation_discretization() const
{
return discretization->template equation_discretization<Row>();
}
};
/** @brief Compile selections explicitly while leaving thermodynamic and equation evaluation to the graph. */
template <utils::dependencies::IsKeyList Prescribed = utils::dependencies::KeyList<>, typename Model, typename Discretization, typename Coefficients>
[[nodiscard]] auto bind_model_definition(std::shared_ptr<const Model> model, std::shared_ptr<const Discretization> discretization,
const Coefficients& coefficients)
{
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(model != nullptr && discretization != nullptr, "Binding a stellar model requires both an owning physical description and an owning discretization.");
auto definition = resolve_equilibrium_equations(*model);
using Description = decltype(definition);
using AvailableFields = typename Coefficients::Keys;
using Plan = EquilibriumPlan<typename Model::EquationOfState, Description, Prescribed, AvailableFields>;
static_assert(Plan::Surface::supported,
"The isobaric condition has no unique supported equation. Check the EOS relations, available boundary fields, derivatives and solved equation rows. For genuinely ambiguous rows an equation may name its IsobaricSurfaceField.");
detail::validate_coefficient_owners(*discretization, coefficients, typename Plan::Coefficients{});
const auto boundary = [&]()
{
if constexpr (Plan::has_surface && Plan::Surface::supported)
{
using Formulation = typename Plan::Surface::Formulation;
return geometry::surface::backend::mfem::make_pressure_boundary<Formulation>(coefficients);
}
else
{
return NoSurfaceCondition{};
}
}();
using Binding = BoundModel<Model, Description, Discretization, decltype(boundary), Plan>;
return std::make_shared<const Binding>(Binding{.model = std::move(model), .definition = std::move(definition),
.discretization = std::move(discretization), .boundary = boundary});
}
namespace detail
{
/** @brief Track immutable physics with a policy type independent of the caller's field ordering. */
template <typename EOS>
[[nodiscard]] auto equation_of_state_input(const EOS& eos) {
using namespace utils::dependencies;
return borrow<geometry::surface::keys::EquationOfState>(eos, track_by([](const EOS& value) {
return std::addressof(value);
}));
}
/** @brief Count either backend displacement coefficients or independent surface coefficients. */
template <typename Geometry>
[[nodiscard]] std::size_t geometry_size(const Geometry& coefficients)
{
if constexpr (requires { coefficients.Size(); })
{
return static_cast<std::size_t>(coefficients.Size());
}
else
{
return coefficients.size();
}
}
}
/** @brief Build one input schema from an ordinary physical state and the selected geometry coordinates. */
template <typename Bound, typename Coefficients, typename Geometry>
[[nodiscard]] auto equilibrium_inputs(const Bound& bound, const Coefficients& coefficients, const Geometry& geometry_coefficients)
{
namespace execution = operators::backend::mfem;
using Description = typename Bound::Description;
using Plan = typename Bound::Plan;
const auto& discretization = *bound.discretization;
detail::validate_coefficient_owners(discretization, coefficients, typename Plan::Coefficients{});
const auto common = detail::model_inputs(discretization, coefficients, typename Description::InteriorEquations::Equations{}, typename Plan::Coefficients{});
const auto deformation = [&]()
{
if constexpr (Plan::has_shape)
{
return geometry::deformation::backend::mfem::make_deformation_inputs(discretization.deformation.model, discretization.deformation.reference, geometry_coefficients);
}
else
{
return utils::dependencies::make_dependency_context(utils::dependencies::own<execution::DisplacementCoefficients>(
execution::bind(discretization.template binding<discretization::blocks::displacement::vector>(), geometry_coefficients), discretization::backend::mfem::CoefficientTracking{}));
}
}();
const auto boundary = [&]()
{
using namespace utils::dependencies;
if constexpr (Plan::has_surface)
{
const auto eos = detail::equation_of_state_input(bound.model->equation_of_state());
return geometry::surface::backend::mfem::make_pressure_boundary_metadata(bound.boundary, eos, bound.definition.surface_condition);
}
else
{
return make_dependency_context();
}
}();
return utils::dependencies::combine_dependency_contexts(common, deformation, boundary);
}
/** @brief Resolve physical block sizes, including the explicitly supplied independent shape count. */
template <typename Plan, typename Discretization, typename Geometry>
[[nodiscard]] typename Plan::Layout equilibrium_layout(const Discretization& discretization, const Geometry& geometry_coefficients) {
return discretization::blocks::make_system_layout<typename Plan::Layout>([&]<typename Term>() -> std::size_t {
if constexpr (std::same_as<Term, discretization::blocks::surface_deformation::coefficients> || std::same_as<Term, discretization::blocks::surface_deformation::shape_equilibrium>) {
return detail::geometry_size(geometry_coefficients);
} else {
return static_cast<std::size_t>(discretization.template binding<Term>().size());
}
});
}
}

View File

@@ -0,0 +1,94 @@
#pragma once
#include <memory>
#include <optional>
#include <tuple>
#include <utility>
#include "serif/backend/mfem/model/system.hpp"
#include "serif/backend/mfem/model/volume_actions.hpp"
#include "serif/backend/mfem/model/volume.hpp"
#include "serif/backend/mfem/model/computations/shape_derivatives.hpp"
#include "serif/backend/mfem/model/computations/scalar_row.hpp"
#include "serif/backend/mfem/operators/volume_computations.hpp"
#include "serif/backend/mfem/operators/volume_field_block.hpp"
namespace serif::model::computations {
/** @brief One equation's accepted contribution to the complete physical system. */
template <typename Row>
struct InteriorContribution {
};
/** @brief Ordinary equations, point constraints and row replacements share one small assembly result. */
struct Contributions final {
std::vector<backend::mfem::ResidualContribution> residual;
std::vector<backend::mfem::JacobianContribution> jacobian;
std::vector<backend::mfem::RowReplacement> replacements;
/// Optional complete actions accelerate whole-system products while Jacobian blocks remain independently selectable.
std::optional<std::vector<backend::mfem::JacobianContribution>> actions;
};
/** @brief Assemble field and optional geometry columns from the same retained volume result. */
template <typename Equation, typename Schema, typename Layout>
[[nodiscard]] Contributions volume_contribution(std::shared_ptr<const operators::backend::mfem::VolumeData<Equation, Schema>> accepted,
const Layout& layout, std::optional<backend::mfem::ColumnContribution> geometry = {}) {
using namespace utils::dependencies;
using namespace discretization::blocks;
namespace execution = operators::backend::mfem;
using Row = typename Equation::Test;
const int row_offset = static_cast<int>(layout.template offset<RepresentedValueKind::residual, Row>());
Contributions result;
result.residual.push_back({row_offset, std::vector<double>(accepted->residual.GetData(), accepted->residual.GetData() + accepted->residual.Size())});
operators::EquationPlan<Equation>::for_each_column([&]<typename Column>(Column, std::size_t) {
using Field = typename Column::term_type;
if constexpr (ContainsKey<Field, typename Layout::Values>) {
result.jacobian.push_back({row_offset, static_cast<int>(layout.template offset<RepresentedValueKind::value, Field>()), execution::volume_field_block<Field>(accepted)});
}
});
if (geometry) {
result.jacobian.push_back({row_offset, geometry->column_offset, serif::operators::backend::mfem::compose_blocks(backend::mfem::volume_geometry_block(accepted), geometry->block)});
}
result.actions = std::vector<backend::mfem::JacobianContribution>{{row_offset, 0, backend::mfem::volume_row_action(accepted, layout, geometry)}};
return result;
}
/** @brief Declare one owning contribution per equation; shape coordinates add one explicit shared prerequisite. */
template <typename Equation, typename Layout>
[[nodiscard]] auto interior_contribution(Layout layout) {
using namespace utils::dependencies;
using namespace discretization::blocks;
namespace execution = operators::backend::mfem;
using Row = typename Equation::Test;
using Volume = typename execution::volume_keys::Scoped<Row>::Linearization;
if constexpr (GlobalScalarTerm<Row>) {
return make_owning_computation<InteriorContribution<Row>, InputList<Volume, ScalarResidualRow<Row>>>([layout](const auto& volume, const auto& row) {
const int offset = static_cast<int>(layout.template offset<RepresentedValueKind::residual, Row>());
return Contributions{
.residual = {{offset, std::vector<double>(volume->residual.GetData(), volume->residual.GetData() + volume->residual.Size())}},
.jacobian = {{offset, 0, *row}}
};
});
} else if constexpr (ContainsKey<surface_deformation::coefficients, typename Layout::Values>) {
return make_owning_computation<InteriorContribution<Row>, InputList<Volume, ShapeDerivatives>>([layout](const auto& volume, const auto& shape) {
return volume_contribution(volume, layout, backend::mfem::ColumnContribution{
static_cast<int>(layout.template offset<RepresentedValueKind::value, surface_deformation::coefficients>()), shape->tangent});
});
}
else {
return make_owning_computation<InteriorContribution<Row>, InputList<Volume>>([layout](const auto& volume) {
return volume_contribution(volume, layout);
});
}
}
/** @brief Scalar equations publish a reusable functional before joining ordinary system assembly. */
template <typename Equation, typename Layout>
[[nodiscard]] auto interior_computations(const Layout& layout) {
if constexpr (discretization::blocks::GlobalScalarTerm<typename Equation::Test>) {
return std::tuple{scalar_residual_row<Equation>(layout), interior_contribution<Equation>(layout)};
} else {
return std::tuple{interior_contribution<Equation>(layout)};
}
}
} // namespace serif::model::computations

View File

@@ -0,0 +1,49 @@
#pragma once
#include <array>
#include <limits>
#include <tuple>
#include "serif/backend/mfem/model/system.hpp"
#include "serif/model/keys.hpp"
#include "serif/operators/linearized_system_view.hpp"
#include "serif/utils/dependencies/computation.hpp"
namespace serif::model::computations {
namespace detail {
/** @brief Extract selected physical rows and columns in the caller's explicit order. */
template <typename Layout, typename... Rows, typename... Fields>
[[nodiscard]] auto physical_block(const backend::mfem::PhysicalSystem& system, const Layout& layout,
utils::dependencies::KeyList<Rows...>, utils::dependencies::KeyList<Fields...>) {
using Kind = discretization::blocks::RepresentedValueKind;
static_assert(sizeof...(Rows) > 0 && sizeof...(Fields) > 0, "Select at least one equation and one physical coordinate for a solver block.");
const std::array rows{backend::mfem::CoordinateRange{static_cast<int>(layout.template offset<Kind::residual, Rows>()), static_cast<int>(layout.template size<Kind::residual, Rows>())}...};
const std::array columns{backend::mfem::CoordinateRange{static_cast<int>(layout.template offset<Kind::value, Fields>()), static_cast<int>(layout.template size<Kind::value, Fields>())}...};
return system.block(rows, columns);
}
}
/** @brief Publish a requested physical block from the same physical root used for residual and Jacobian actions. */
template <typename Block, discretization::blocks::SystemLayoutType Layout>
[[nodiscard]] auto physical_block(const Layout& layout) {
using namespace utils::dependencies;
using namespace utils::errors;
using Kind = discretization::blocks::RepresentedValueKind;
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(layout.template total_size<Kind::residual>() <= std::numeric_limits<int>::max() &&
layout.template total_size<Kind::value>() <= std::numeric_limits<int>::max(), "Physical solver block coordinates exceed MFEM's integer index range. Supply a representable system layout.");
return make_computation<Block, InputList<keys::PhysicalSystem>>([layout](const std::shared_ptr<const backend::mfem::PhysicalSystem>& system) {
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(system != nullptr, "Physical solver blocks require an evaluated stellar system.");
serif_ensure<SERiFErrorCode::MATRIX_SHAPE_ERROR>(layout.template total_size<Kind::residual>() == static_cast<std::size_t>(system->jacobian().action()->Height()) &&
layout.template total_size<Kind::value>() == static_cast<std::size_t>(system->jacobian().action()->Width()), "The requested solver block layout does not match its frozen physical system. Use that system's coordinate description.");
return detail::physical_block(*system, layout, typename Block::Rows{}, typename Block::Values{});
});
}
/** @brief Supply precisely the requested physical couplings to existing preconditioner computations. */
template <discretization::blocks::SystemLayoutType Layout, typename... Blocks>
[[nodiscard]] auto make_physical_block_computations(const Layout& layout, utils::dependencies::KeyList<Blocks...>) {
return std::tuple{physical_block<Blocks>(layout)...};
}
}

View File

@@ -0,0 +1,40 @@
#pragma once
#include "serif/backend/mfem/discretization/dof_selection.hpp"
#include "serif/backend/mfem/model/computations/interior.hpp"
namespace serif::model::computations {
/** @brief The zero potential trace at compactified infinity, independent of the stellar isobar. */
struct PotentialInfinityContribution {};
/** @brief Replace only the compactified-infinity potential rows by the zero physical trace. */
template <typename Layout, typename Binding>
[[nodiscard]] auto potential_infinity_contribution(Layout layout, const Binding& binding) {
using namespace utils::dependencies;
using namespace discretization::blocks;
using namespace utils::errors;
// Normalization selects this same fixed boundary on the same potential binding.
const auto selected = discretization::backend::mfem::select_boundary_dofs<discretization::domain::InfinitySurfaceBoundary>(binding);
return make_computation<PotentialInfinityContribution, InputList<gravity::potential>>(
[layout, selected](const auto& potential) {
selected.validate();
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR>(&potential.binding == &selected.binding() && potential.values.Size() == potential.binding.size(),
"Infinity trace coefficients must use the selected potential binding.");
const auto& reduced = selected.reduced_dofs();
const int row_offset = static_cast<int>(layout.template offset<RepresentedValueKind::residual, gravity::potential>());
auto trace = std::make_shared<::mfem::SparseMatrix>(reduced.Size(), potential.binding.size());
backend::mfem::RowReplacement replacement;
for (int index = 0; index < reduced.Size(); ++index) {
replacement.selected_rows.push_back(row_offset + reduced[index]);
replacement.residual.push_back(potential.values[reduced[index]]);
trace->Add(index, reduced[index], 1.0);
}
trace->Finalize();
replacement.jacobian.push_back({.column_offset = static_cast<int>(layout.template offset<RepresentedValueKind::value, gravity::potential>()),
.block = operators::backend::mfem::linearized_block(std::move(trace))});
Contributions result;
result.replacements.push_back(std::move(replacement));
return result;
});
}
}

View File

@@ -0,0 +1,69 @@
#pragma once
#include <cmath>
#include <limits>
#include "serif/discretization/blocks/system_layout.hpp"
#include "serif/backend/mfem/geometry/surface/boundary_inputs.hpp"
#include "serif/backend/mfem/model/computations/interior.hpp"
#include "serif/backend/mfem/geometry/surface/pressure_field_block.hpp"
namespace serif::model::computations
{
/** @brief The complete isobaric replacement applied after ordinary and generated equation contributions. */
struct PressureBoundaryContribution
{
};
/**
* @brief Publish accepted pressure rows and their solved-field derivatives in system coordinates.
* @note Prescribed EOS inputs still participate in surface evaluation, but have no Newton column.
* The compiled surface formulation selects its physical carrier; no enthalpy assumption enters assembly.
*/
template <geometry::surface::backend::mfem::PressureBoundaryFormulation Formulation, discretization::blocks::SystemLayoutType Layout>
requires utils::dependencies::ContainsKey<typename Formulation::CarrierTerm, typename Layout::Residuals>
[[nodiscard]] auto pressure_boundary_contribution(Layout layout)
{
using namespace utils::dependencies;
using namespace utils::errors;
using namespace discretization::blocks;
namespace surface = geometry::surface;
namespace execution = operators::backend::mfem;
using Carrier = typename Formulation::CarrierTerm;
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(layout.template total_size<RepresentedValueKind::value>() <= static_cast<std::size_t>(std::numeric_limits<int>::max()) && layout.template total_size<RepresentedValueKind::residual>() <= static_cast<std::size_t>(std::numeric_limits<int>::max()), "Pressure-boundary system coordinates must fit MFEM's integer index range.");
return make_computation<PressureBoundaryContribution, InputList<surface::keys::Linearization, surface::backend::mfem::keys::BoundaryDiscretization>>(
[layout](const std::vector<surface::SurfaceLinearization<Formulation>>& linearizations, const auto& boundary)
{
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(boundary != nullptr, "Pressure-boundary assembly requires its compiled trace owner.");
boundary->validate();
surface::backend::mfem::detail::validate_boundary_samples(*boundary, linearizations);
serif_ensure<SERiFErrorCode::MATRIX_SHAPE_ERROR>(layout.template size<RepresentedValueKind::residual, Carrier>() == static_cast<std::size_t>(boundary->selected_rows->binding().size()), "The pressure carrier's residual layout must match its finite-element row binding.");
const int row_offset = static_cast<int>(layout.template offset<RepresentedValueKind::residual, Carrier>());
backend::mfem::RowReplacement replacement;
replacement.selected_rows.reserve(linearizations.size());
replacement.residual.reserve(linearizations.size());
for (int index = 0; index < boundary->selected_rows->size(); ++index)
{
serif_ensure<SERiFErrorCode::NON_FINITE_VALUE_ERROR>(std::isfinite(linearizations[index].residual), "A pressure replacement must contain finite accepted residuals.");
replacement.selected_rows.push_back(row_offset + boundary->selected_rows->reduced_dofs()[index]);
replacement.residual.push_back(linearizations[index].residual);
}
[&]<typename... Fields>(KeyList<Fields...>)
{
const auto add_column = [&]<typename Field>()
{
if constexpr (ContainsKey<Field, typename Layout::Values>)
{
auto matrix = surface::backend::mfem::pressure_field_trace<Field>(*boundary, linearizations);
serif_ensure<SERiFErrorCode::MATRIX_SHAPE_ERROR>(layout.template size<RepresentedValueKind::value, Field>() == static_cast<std::size_t>(matrix->Width()), "A solved pressure input's coordinate count must match its finite-element trace.");
replacement.jacobian.push_back({.column_offset = static_cast<int>(layout.template offset<RepresentedValueKind::value, Field>()), .block = execution::linearized_block(std::move(matrix))});
}
};
(add_column.template operator()<Fields>(), ...);
}(typename Formulation::StateTerms{});
Contributions result;
result.replacements.push_back(std::move(replacement));
return result;
});
}
} // namespace serif::model::computations

View File

@@ -0,0 +1,34 @@
#pragma once
#include "serif/backend/mfem/model/scalar_row.hpp"
#include "serif/backend/mfem/model/volume_actions.hpp"
#include "serif/backend/mfem/model/computations/shape_derivatives.hpp"
#include "serif/backend/mfem/operators/volume_computations.hpp"
namespace serif::model::computations {
/** @brief One global scalar equation's accepted derivative in complete physical state coordinates. */
template <discretization::blocks::GlobalScalarTerm Row>
struct ScalarResidualRow {
};
/** @brief Rebuild a scalar functional only when its physical equation or shape derivative changes. */
template <typename Equation, typename Layout>
requires discretization::blocks::GlobalScalarTerm<typename Equation::Test>
[[nodiscard]] auto scalar_residual_row(Layout layout) {
using namespace utils::dependencies;
using namespace discretization::blocks;
using Row = typename Equation::Test;
using Volume = typename operators::backend::mfem::volume_keys::Scoped<Row>::Linearization;
if constexpr (ContainsKey<surface_deformation::coefficients, typename Layout::Values>) {
return make_owning_computation<ScalarResidualRow<Row>, InputList<Volume, ShapeDerivatives>>([layout](const auto& volume, const auto& shape) {
return backend::mfem::cache_scalar_row(backend::mfem::volume_row_action(volume, layout, backend::mfem::ColumnContribution{
static_cast<int>(layout.template offset<RepresentedValueKind::value, surface_deformation::coefficients>()), shape->tangent}));
});
} else {
return make_owning_computation<ScalarResidualRow<Row>, InputList<Volume>>([layout](const auto& volume) {
return backend::mfem::cache_scalar_row(backend::mfem::volume_row_action(volume, layout));
});
}
}
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include "serif/geometry/deformation/computations.hpp"
#include "serif/backend/mfem/model/deformation.hpp"
namespace serif::model::computations {
struct ShapeDerivatives {
};
/** @brief Prepare the projected shape derivative once per evaluated geometry, retaining its computed owner. */
[[nodiscard]] inline auto shape_derivatives(const discretization::backend::mfem::DofMap& binding) {
using namespace utils::dependencies;
const auto projection = backend::mfem::displacement_projection(binding);
return make_owning_computation<ShapeDerivatives, InputList<geometry::deformation::keys::Linearization>>([projection](const auto& deformation) {
return backend::mfem::ShapeDerivatives{.deformation = deformation, .projection = projection,
.tangent = serif::operators::backend::mfem::compose_blocks(projection, backend::mfem::deformation_tangent(deformation))};
});
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <tuple>
#include "serif/discretization/blocks/keys.hpp"
#include "serif/discretization/blocks/system_layout.hpp"
#include "serif/utils/dependencies/computation.hpp"
namespace serif::model::computations {
/** @brief Give normalization and preconditioning one producer for their shared coordinate layout. */
template <discretization::blocks::SystemLayoutType Layout>
[[nodiscard]] auto make_solver_layout_computations(const Layout& layout) {
using namespace utils::dependencies;
return std::tuple{make_computation<discretization::blocks::keys::Layout, InputList<>>([layout]() {
return layout;
})};
}
}

View File

@@ -0,0 +1,45 @@
#pragma once
#include "serif/backend/mfem/model/surface_potential.hpp"
#include "serif/geometry/deformation/computations.hpp"
#include "serif/backend/mfem/model/computations/interior.hpp"
namespace serif::model::computations {
struct ShapeContribution {
};
/** @brief Replace the volume force pullback by scalar effective-potential moments on the same surface. */
template <typename Layout>
[[nodiscard]] auto surface_potential_contribution(equations::SurfacePotentialBalance equation, Layout layout) {
using namespace utils::dependencies;
using namespace discretization::blocks;
namespace deformation = geometry::deformation;
using Equation = equations::SurfacePotentialBalance;
return make_computation<ShapeContribution, InputList<deformation::keys::SurfaceDiscretization,
deformation::keys::ReferenceSurface, deformation::keys::SurfaceMap, deformation::keys::SurfaceDisplacement,
Equation::Potential, Equation::SurfaceLevel, Equation::AngularVelocity>>(
[equation, layout](const auto& discretization, const auto& reference, const auto& embedding,
const auto& displacement, const auto& potential, const auto& level, const auto& rotation) {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(level.values.Size() == 1 && rotation.values.Size() == 1,
"The surface level and rigid angular velocity each require one coefficient.");
auto accepted = backend::mfem::assemble_surface_potential(equation, discretization, *reference,
*embedding, displacement, potential.binding, potential.values, level.values[0], rotation.values[0]);
const int row = static_cast<int>(layout.template offset<RepresentedValueKind::residual, surface_deformation::shape_equilibrium>());
Contributions result;
result.residual.push_back({.row_offset = row, .values = std::move(accepted.residual)});
const auto add_column = [&]<typename Field, typename Matrix>(std::shared_ptr<Matrix> matrix) {
if constexpr (ContainsKey<Field, typename Layout::Values>) {
result.jacobian.push_back({.row_offset = row,
.column_offset = static_cast<int>(layout.template offset<RepresentedValueKind::value, Field>()),
.block = operators::backend::mfem::linearized_block(std::shared_ptr<const Matrix>(std::move(matrix)))});
}
};
add_column.template operator()<Equation::Potential>(std::move(accepted.potential));
add_column.template operator()<Equation::SurfaceLevel>(std::move(accepted.level));
add_column.template operator()<Equation::AngularVelocity>(std::move(accepted.angular_velocity));
add_column.template operator()<surface_deformation::coefficients>(std::move(accepted.shape));
return result;
});
}
}

View File

@@ -0,0 +1,69 @@
#pragma once
#include <limits>
#include <memory>
#include <span>
#include <utility>
#include "serif/geometry/deformation/linearization.hpp"
#include <vector>
#include "serif/backend/mfem/operators/actions.hpp"
#include "serif/backend/mfem/shared/parallel.hpp"
#include "serif/utils/error/errors.hpp"
namespace serif::discretization::backend::mfem {
class DofMap;
}
namespace serif::model::backend::mfem {
using operators::backend::mfem::LinearizedBlock;
/** @brief Map the deformation's component ordering into the supported displacement ordering once. */
[[nodiscard]] LinearizedBlock displacement_projection(const discretization::backend::mfem::DofMap& binding);
/** @brief Share one accepted deformation and its projected derivative across all physical equations. */
template <typename Deformation>
struct ShapeDerivatives {
std::shared_ptr<const Deformation> deformation;
LinearizedBlock projection;
LinearizedBlock tangent;
};
namespace detail {
/** @brief Reject null accepted maps and dimensions that MFEM cannot represent. */
template <geometry::deformation::DeformationMap Map>
void validate_deformation(const std::shared_ptr<const geometry::deformation::DeformationLinearization<Map>>& state) {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(state && state->map, "A model shape action requires an accepted deformation map.");
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(state->map->input_size() <= static_cast<std::size_t>(std::numeric_limits<int>::max()) && state->map->output_size() <= static_cast<std::size_t>(std::numeric_limits<int>::max()), "Deformation coordinate counts exceed the MFEM integer range.");
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(state->parameters.size() == state->map->input_size(), "The accepted deformation must contain every shape coordinate.");
}
/** @brief Preserve the originating deformation diagnostic at the MFEM action boundary. */
void require_deformation_action(const geometry::deformation::DeformationStatus& status);
} // namespace detail
/** @brief Retain the accepted shape and expose its displacement tangent E and exact transpose E^T. */
template <geometry::deformation::DeformationMap Map>
[[nodiscard]] LinearizedBlock deformation_tangent(std::shared_ptr<const geometry::deformation::DeformationLinearization<Map>> state) {
detail::validate_deformation(state);
const int rows = static_cast<int>(state->map->output_size());
const int columns = static_cast<int>(state->map->input_size());
return operators::backend::mfem::linearized_action(rows, columns, [state, workspace = std::make_shared<geometry::deformation::DeformationWorkspace<Map>>(geometry::deformation::make_deformation_workspace(*state->map))](const ::mfem::Vector& input, ::mfem::Vector& output) mutable {
detail::require_deformation_action(geometry::deformation::apply_jacobian(*state, std::span<const double>(input.GetData(), input.Size()), std::span<double>(output.GetData(), output.Size()), *workspace));
}, [state, workspace = std::make_shared<geometry::deformation::DeformationWorkspace<Map>>(geometry::deformation::make_deformation_workspace(*state->map))](const ::mfem::Vector& input, ::mfem::Vector& output) mutable {
detail::require_deformation_action(geometry::deformation::apply_jacobian_transpose(*state, std::span<const double>(input.GetData(), input.Size()), std::span<double>(output.GetData(), output.Size()), *workspace));
}, [state, workspaces = std::vector<std::shared_ptr<geometry::deformation::DeformationWorkspace<Map>>>{}](std::span<const ::mfem::Vector* const> inputs, std::span<::mfem::Vector* const> outputs) mutable {
// Directions are independent; each worker owns its workspace, so every column is computed as by Mult.
const auto workers = static_cast<std::size_t>(utils::parallel::worker_count());
while (workspaces.size() < workers) {
workspaces.push_back(std::make_shared<geometry::deformation::DeformationWorkspace<Map>>(geometry::deformation::make_deformation_workspace(*state->map)));
}
utils::parallel::for_each_index(static_cast<int>(inputs.size()), [&](const int index, const int worker) {
const auto& input = *inputs[static_cast<std::size_t>(index)];
auto& output = *outputs[static_cast<std::size_t>(index)];
detail::require_deformation_action(geometry::deformation::apply_jacobian(*state, std::span<const double>(input.GetData(), input.Size()), std::span<double>(output.GetData(), output.Size()), *workspaces[static_cast<std::size_t>(worker)]));
});
});
}
} // namespace serif::model::backend::mfem

View File

@@ -0,0 +1,87 @@
#pragma once
#include <memory>
#include <utility>
#include "serif/backend/mfem/operators/volume_data.hpp"
#include "serif/backend/mfem/geometry/deformation/inputs.hpp"
#include "serif/utils/error/errors.hpp"
namespace serif::model::backend::mfem
{
/** @brief Prescribe displacement externally when no shape unknown is selected. */
struct NoDeformation
{
};
/** @brief Numerical surface representation and its reference geometry, separate from force equations. */
template <typename DomainModel>
struct DeformationDiscretization
{
DomainModel model;
geometry::deformation::backend::mfem::ReferenceDiscretization reference;
};
/**
* @brief Bind physical terms to existing finite-element resources with one explicit lifetime owner.
* @note This is a numerical binding, not a stellar-model description. The owner must own every
* referenced mesh, space, binding, mapper and compactification field. No equation is evaluated here.
*/
template <typename Owner, utils::dependencies::IsDependencyContext Bindings, typename Deformation = NoDeformation>
struct StellarDiscretization final
{
std::shared_ptr<const Owner> owner;
Bindings term_bindings;
const ::mfem::GridFunction& compactification;
const mapping::DomainMapper& mapper;
Deformation deformation{};
/** @brief Retrieve a physical term's existing typed finite-element binding. */
template <typename Term>
[[nodiscard]] const auto& binding() const
{
return term_bindings.template get<Term>();
}
/** @brief Describe one equation's discretization using the common physical resources. */
template <typename Row>
[[nodiscard]] auto equation_discretization() const
{
using Binding = std::remove_cvref_t<decltype(binding<Row>())>;
using Schema = typename Binding::Schema;
return operators::backend::mfem::VolumeDiscretization<Row, Schema>{
.residual_binding = binding<Row>(),
.displacement_binding = binding<discretization::blocks::displacement::vector>(),
.compactification = compactification,
.mapper = mapper
};
}
};
/** @brief Retain the supplied resource owner without copying finite-element spaces or changing their addresses. */
template <typename Owner, utils::dependencies::IsDependencyContext Bindings>
[[nodiscard]] auto make_stellar_discretization(std::shared_ptr<Owner> owner, Bindings bindings,
const ::mfem::GridFunction& compactification, const mapping::DomainMapper& mapper)
{
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(owner != nullptr, "A stellar discretization requires an owner for its borrowed finite-element resources.");
return StellarDiscretization<std::remove_const_t<Owner>, Bindings>{
.owner = std::move(owner), .term_bindings = std::move(bindings),
.compactification = compactification, .mapper = mapper
};
}
/** @brief Add an explicit deformation prescription without changing the physical equation definition. */
template <typename Owner, typename Bindings, typename DomainModel>
[[nodiscard]] auto with_deformation(StellarDiscretization<Owner, Bindings> discretization, DomainModel model,
geometry::deformation::backend::mfem::ReferenceDiscretization reference)
{
return StellarDiscretization<Owner, Bindings, DeformationDiscretization<DomainModel>>{
.owner = std::move(discretization.owner), .term_bindings = std::move(discretization.term_bindings),
.compactification = discretization.compactification, .mapper = discretization.mapper,
.deformation = {.model = std::move(model), .reference = std::move(reference)}
};
}
}

View File

@@ -0,0 +1,200 @@
#pragma once
#include <functional>
#include <memory>
#include <tuple>
#include <type_traits>
#include <utility>
#include "serif/backend/mfem/model/equilibrium_evaluator.hpp"
#include "serif/backend/mfem/model/discretization.hpp"
#include "serif/model/configured_discretization.hpp"
#include "serif/backend/mfem/model/normalization/computations.hpp"
#include "serif/backend/mfem/model/graph.hpp"
#include "serif/model/execution_validation.hpp"
namespace serif::model::backend::mfem {
namespace detail {
/** @brief Select physical coordinates without adding normalization graph services. */
struct PhysicalEvaluation final {};
/** @brief Ordinary model evaluation adds no caller-selected preparation computations. */
struct NoAdditionalComputations {
/** @brief Separate computations needed for every evaluation from those needed only for linear preparation. */
auto operator()(const auto&, const auto&, auto, const auto&) const { return std::pair{std::tuple{}, std::tuple{}}; }
};
/** @brief Derive requested result identities directly from the supplied computation declarations. */
template <typename... Computations>
[[nodiscard]] auto computation_keys(const std::tuple<Computations...>&) {
return utils::dependencies::KeyList<typename Computations::Key...>{};
}
/** @brief Bind physics once and add only the explicitly selected numerical services. */
template <typename Prescribed, typename SpecificationMap, typename ExplicitZeroCouplings, typename Model, typename Discretization,
typename Coefficients, typename Geometry, typename NumericalOptions = PhysicalEvaluation, typename Preconditioner = preconditioning::None,
typename Services = NoAdditionalComputations, typename ObserverFactory = utils::dependencies::NoGraphObservers>
[[nodiscard]] auto bind_evaluator(std::shared_ptr<const Model> model, std::shared_ptr<const Discretization> discretization,
const Coefficients& coefficients, const Geometry& geometry, operators::backend::mfem::VolumeOptions options, NumericalOptions numerical_options = {}, Preconditioner preconditioner = {}, Services services = {}, ObserverFactory observer_factory = {}) {
const auto bound = bind_model_definition<Prescribed>(std::move(model), std::move(discretization), coefficients);
using Bound = typename decltype(bound)::element_type;
using Plan = typename Bound::Plan;
static_assert(executable_model_topology<Model, typename Bound::Description, Plan, SpecificationMap, ExplicitZeroCouplings>(),
"The selected model needs complete equation and prescription bindings, including explicit generated-coordinate effects. This check does not prove mathematical closure.");
const auto layout = equilibrium_layout<Plan>(*bound->discretization, geometry);
const auto construct = [&]<typename ResultKey>(ResultKey, auto numerical, auto additional) {
auto inverse = [&]() {
if constexpr (std::same_as<Preconditioner, preconditioning::None>) {
return std::tuple{};
} else {
return preconditioning::make_computations<ResultKey>(preconditioner, layout, numerical);
}
}();
auto numerical_services = std::tuple_cat(std::move(numerical), std::move(inverse));
auto extensions = services(bound, layout, ResultKey{}, numerical_services);
using EvaluationServices = decltype(computation_keys(extensions.first));
using LinearServices = decltype(computation_keys(extensions.second));
auto graph = make_equilibrium_graph(bound, layout,
std::tuple_cat(std::move(numerical_services), std::move(extensions.first), std::move(extensions.second)), options);
const auto inputs = utils::dependencies::combine_dependency_contexts(candidate_inputs(*bound, coefficients, geometry), additional);
// Observation sees the final graph, but does not become a physical input or invalidate cached results.
auto observers = std::invoke(observer_factory, std::type_identity<decltype(graph)>{});
return EquilibriumEvaluator<std::remove_const_t<Bound>, decltype(graph), std::remove_cvref_t<decltype(inputs)>, decltype(additional), ResultKey, EvaluationServices, LinearServices, decltype(observers)>(
bound, std::move(graph), layout, std::move(additional), std::move(observers));
};
if constexpr (std::same_as<NumericalOptions, PhysicalEvaluation>) {
return construct(keys::PhysicalSystem{}, std::tuple{}, utils::dependencies::make_dependency_context());
} else {
auto resolved = normalization::resolve_configuration(std::move(numerical_options), *bound->model);
using NormalizationPlan = normalization::PlanFor<decltype(resolved.plan), decltype(resolved.coordinates), Bound>;
static_assert(solver::normalization::NormalizationPreparable<NormalizationPlan, decltype(layout)>,
"The normalization plan must provide one compatible rule for every unknown and residual row.");
const auto configuration = std::make_shared<const decltype(resolved)>(std::move(resolved));
const auto metrics = normalization::MetricsFor<NormalizationPlan>{};
const auto additional = normalization::make_inputs(configuration, *bound->discretization, metrics);
const auto numerical = normalization::make_computations<NormalizationPlan>(*bound, *configuration, layout, metrics);
return construct(normalization::PreparedSystem{}, numerical, additional);
}
}
/** @brief Retain finite-element bindings before constructing temporary views into an owning state. */
template <typename Prescribed, typename SpecificationMap, typename ExplicitZeroCouplings, typename Model, typename Discretization,
typename Layout, typename Geometry, typename Inputs, typename NumericalOptions = PhysicalEvaluation, typename Preconditioner = preconditioning::None,
typename Services = NoAdditionalComputations, typename ObserverFactory = utils::dependencies::NoGraphObservers>
[[nodiscard]] auto bind_state_evaluator(Model model, Discretization discretization, const StellarState<Layout>& state,
const Geometry& geometry, operators::backend::mfem::VolumeOptions options, const Inputs& prescribed, NumericalOptions numerical = {}, Preconditioner preconditioner = {}, Services services = {}, ObserverFactory observer_factory = {}) {
using namespace utils::errors;
using PrescribedKeys = utils::dependencies::CombinedKeyList<Prescribed, typename Inputs::Keys>;
const auto owner = std::make_shared<const Discretization>(std::move(discretization));
const auto fields = state_coefficients(*owner, state, prescribed);
const auto coefficients = fields.context();
auto evaluator = bind_evaluator<PrescribedKeys, SpecificationMap, ExplicitZeroCouplings>(std::make_shared<const Model>(std::move(model)),
owner, coefficients, geometry, options, std::move(numerical), std::move(preconditioner), std::move(services), std::move(observer_factory));
static_assert(std::same_as<typename StellarState<Layout>::Layout, typename decltype(evaluator)::Layout>,
"The state's physical coordinate identities must match the selected equations and prescribed fields.");
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(state.layout() == evaluator.layout(),
"The stellar state's field or equation sizes do not match the bound equations. Construct it with the matching physical layout.");
return evaluator;
}
}
/**
* @brief Bind explicitly selected physics and external field views; initial values establish input types only.
* @note The optional final factory receives the completed graph type and supplies cache observers.
* The default installs no observers; timing and other observation are not physical inputs.
*/
template <utils::dependencies::IsKeyList Prescribed = utils::dependencies::KeyList<>,
utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings,
utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model,
typename Discretization, typename Coefficients, typename Geometry, typename ObserverFactory = utils::dependencies::NoGraphObservers>
requires requires { typename Coefficients::Keys; }
[[nodiscard]] auto bind_stellar_model(Model model, Discretization discretization, const Coefficients& coefficients,
const Geometry& geometry, operators::backend::mfem::VolumeOptions options = {}, ObserverFactory observer_factory = {}) {
return detail::bind_evaluator<Prescribed, SpecificationMap, ExplicitZeroCouplings>(std::make_shared<const Model>(std::move(model)),
std::make_shared<const Discretization>(std::move(discretization)), coefficients, geometry, options, detail::PhysicalEvaluation{}, preconditioning::None{}, detail::NoAdditionalComputations{}, std::move(observer_factory));
}
/**
* @brief Bind configured numerical coordinates with external physical field views and geometry.
* @note The optional final factory receives the completed graph type and supplies cache observers.
* The default installs no observers; timing and other observation are not physical inputs.
*/
template <utils::dependencies::IsKeyList Prescribed = utils::dependencies::KeyList<>,
utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings,
utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model,
typename Discretization, typename Options, typename Preconditioner, typename Coefficients, typename Geometry, typename ObserverFactory = utils::dependencies::NoGraphObservers>
requires requires { typename Coefficients::Keys; }
[[nodiscard]] auto bind_stellar_model(DiscretizationOptions<Model, Discretization, Options, Preconditioner> configuration,
const Coefficients& coefficients, const Geometry& geometry, operators::backend::mfem::VolumeOptions options = {}, ObserverFactory observer_factory = {}) {
return detail::bind_evaluator<Prescribed, SpecificationMap, ExplicitZeroCouplings>(std::make_shared<const Model>(std::move(configuration.model)),
std::make_shared<const Discretization>(std::move(configuration.finite_elements)), coefficients, geometry, options, std::move(configuration.normalization), std::move(configuration.preconditioner), detail::NoAdditionalComputations{}, std::move(observer_factory));
}
/**
* @brief Bind a moving star with all solved physical coefficients and shape coordinates in one state.
* @note The optional final factory receives the completed graph type and supplies cache observers.
* The default installs no observers; timing and other observation are not physical inputs.
*/
template <utils::dependencies::IsKeyList Prescribed = utils::dependencies::KeyList<>,
utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings,
utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model,
typename Discretization, typename Layout, utils::dependencies::IsDependencyContext Inputs = utils::dependencies::DependencyContext<>,
typename ObserverFactory = utils::dependencies::NoGraphObservers>
requires utils::dependencies::ContainsKey<discretization::blocks::surface_deformation::coefficients, typename Layout::Values>
[[nodiscard]] auto bind_stellar_model(Model model, Discretization discretization, const StellarState<Layout>& state,
operators::backend::mfem::VolumeOptions options = {}, const Inputs& prescribed = Inputs{}, ObserverFactory observer_factory = {}) {
return detail::bind_state_evaluator<Prescribed, SpecificationMap, ExplicitZeroCouplings>(std::move(model), std::move(discretization), state,
state.template get<discretization::blocks::surface_deformation::coefficients>(), options, prescribed, detail::PhysicalEvaluation{}, preconditioning::None{}, detail::NoAdditionalComputations{}, std::move(observer_factory));
}
/**
* @brief Bind a fixed-geometry star while keeping prescribed displacement outside the solved coordinates.
* @note The optional final factory receives the completed graph type and supplies cache observers.
* The default installs no observers; timing and other observation are not physical inputs.
*/
template <utils::dependencies::IsKeyList Prescribed = utils::dependencies::KeyList<>,
utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings,
utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model,
typename Discretization, typename Layout, utils::dependencies::IsDependencyContext Inputs = utils::dependencies::DependencyContext<>,
typename ObserverFactory = utils::dependencies::NoGraphObservers>
requires (!utils::dependencies::ContainsKey<discretization::blocks::surface_deformation::coefficients, typename Layout::Values>)
[[nodiscard]] auto bind_stellar_model(Model model, Discretization discretization, const StellarState<Layout>& state, const ::mfem::Vector& displacement,
operators::backend::mfem::VolumeOptions options = {}, const Inputs& prescribed = Inputs{}, ObserverFactory observer_factory = {}) {
return detail::bind_state_evaluator<Prescribed, SpecificationMap, ExplicitZeroCouplings>(std::move(model), std::move(discretization), state,
displacement, options, prescribed, detail::PhysicalEvaluation{}, preconditioning::None{}, detail::NoAdditionalComputations{}, std::move(observer_factory));
}
/**
* @brief Bind a moving state with normalization defaults resolved from its authoritative physical model.
* @note The optional final factory receives the completed graph type and supplies cache observers.
* The default installs no observers; timing and other observation are not physical inputs.
*/
template <utils::dependencies::IsKeyList Prescribed = utils::dependencies::KeyList<>,
utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings,
utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model,
typename Discretization, typename Options, typename Preconditioner, typename Layout, utils::dependencies::IsDependencyContext Inputs = utils::dependencies::DependencyContext<>,
typename Services = detail::NoAdditionalComputations, typename ObserverFactory = utils::dependencies::NoGraphObservers>
requires utils::dependencies::ContainsKey<discretization::blocks::surface_deformation::coefficients, typename Layout::Values>
[[nodiscard]] auto bind_stellar_model(DiscretizationOptions<Model, Discretization, Options, Preconditioner> configuration, const StellarState<Layout>& state,
operators::backend::mfem::VolumeOptions options = {}, const Inputs& prescribed = Inputs{}, Services services = {}, ObserverFactory observer_factory = {}) {
return detail::bind_state_evaluator<Prescribed, SpecificationMap, ExplicitZeroCouplings>(std::move(configuration.model), std::move(configuration.finite_elements), state,
state.template get<discretization::blocks::surface_deformation::coefficients>(), options, prescribed, std::move(configuration.normalization), std::move(configuration.preconditioner), std::move(services), std::move(observer_factory));
}
/**
* @brief Bind normalized fixed-geometry evaluation without introducing displacement unknowns.
* @note The optional final factory receives the completed graph type and supplies cache observers.
* The default installs no observers; timing and other observation are not physical inputs.
*/
template <utils::dependencies::IsKeyList Prescribed = utils::dependencies::KeyList<>,
utils::dependencies::IsBindings SpecificationMap = DefaultSpecificationBindings,
utils::dependencies::IsKeyList ExplicitZeroCouplings = utils::dependencies::KeyList<>, StellarModelType Model,
typename Discretization, typename Options, typename Preconditioner, typename Layout, utils::dependencies::IsDependencyContext Inputs = utils::dependencies::DependencyContext<>,
typename Services = detail::NoAdditionalComputations, typename ObserverFactory = utils::dependencies::NoGraphObservers>
requires (!utils::dependencies::ContainsKey<discretization::blocks::surface_deformation::coefficients, typename Layout::Values>)
[[nodiscard]] auto bind_stellar_model(DiscretizationOptions<Model, Discretization, Options, Preconditioner> configuration, const StellarState<Layout>& state,
const ::mfem::Vector& displacement, operators::backend::mfem::VolumeOptions options = {}, const Inputs& prescribed = Inputs{}, Services services = {}, ObserverFactory observer_factory = {}) {
return detail::bind_state_evaluator<Prescribed, SpecificationMap, ExplicitZeroCouplings>(std::move(configuration.model), std::move(configuration.finite_elements), state,
displacement, options, prescribed, std::move(configuration.normalization), std::move(configuration.preconditioner), std::move(services), std::move(observer_factory));
}
}

View File

@@ -0,0 +1,171 @@
#pragma once
#include <expected>
#include <span>
#include "serif/backend/mfem/model/binding.hpp"
#include "serif/backend/mfem/model/failure.hpp"
#include "serif/backend/mfem/model/state.hpp"
#include "serif/backend/mfem/model/preconditioning/computations.hpp"
#include "serif/backend/mfem/shared/finite.hpp"
#include "serif/utils/dependencies/computation_cache.hpp"
namespace serif::model::backend::mfem {
namespace detail {
/** @brief Use one shape-view input schema for both packed states and external coefficient containers. */
template <typename Bound, typename Coefficients, typename Geometry>
[[nodiscard]] auto candidate_inputs(const Bound& bound, const Coefficients& coefficients, const Geometry& geometry) {
if constexpr (Bound::Plan::has_shape) {
return equilibrium_inputs(bound, coefficients, std::span<const double>{geometry});
} else {
return equilibrium_inputs(bound, coefficients, geometry);
}
}
}
/**
* @brief Evaluate explicit stellar candidates using one reusable dependency cache.
* @note Successful evaluation returns an owning frozen result; it does not accept a solver step.
* Retain that handle while evaluating other candidates. Numerical rejection makes the current cache
* unavailable without changing previously returned results. This object never owns the caller's state.
*/
template <typename Bound, typename Graph, typename Inputs, typename AdditionalInputs = utils::dependencies::DependencyContext<>,
typename ResultKey = keys::PhysicalSystem,
utils::dependencies::IsKeyList EvaluationServices = utils::dependencies::KeyList<>,
utils::dependencies::IsKeyList LinearServices = utils::dependencies::KeyList<>,
typename Observers = utils::dependencies::Observers<>>
class EquilibriumEvaluator final {
public:
using Plan = typename Bound::Plan;
using Layout = typename Plan::Layout;
using SurfaceFormulation = typename Plan::Surface::Formulation;
using Cache = utils::dependencies::ComputationCache<Graph, Inputs, EquilibriumFailure, Observers>;
using Result = std::remove_cvref_t<decltype(std::declval<const Cache&>().template get<ResultKey>())>;
using Evaluation = std::expected<Result, typename Cache::Failure>;
template <typename RequestedKey>
using EvaluationFor = std::expected<std::remove_cvref_t<decltype(std::declval<const Cache&>().template get<RequestedKey>())>, typename Cache::Failure>;
/** @brief Retain the physical and finite-element owners before constructing the reusable cache. */
EquilibriumEvaluator(std::shared_ptr<const Bound> bound, Graph graph, Layout layout, AdditionalInputs additional, Observers observers = Observers{}) :
m_bound(std::move(bound)), m_cache(std::move(graph), std::move(observers)), m_layout(std::move(layout)), m_additional(std::move(additional)) {
}
/** @brief Inspect the independently ordered physical unknown and residual coordinates. */
[[nodiscard]] const Layout& layout() const noexcept {
return m_layout;
}
/** @brief Inspect the latest evaluation's work report and requested graph services. */
[[nodiscard]] const Cache& cache() const noexcept {
return m_cache;
}
/** @brief Inspect the immutable physical definition used by every evaluation. */
[[nodiscard]] const auto& model() const noexcept {
return *m_bound->model;
}
/** @brief Inspect the owned physical and FE bindings for solver geometry and numerical policies. */
[[nodiscard]] const Bound& binding() const noexcept {
return *m_bound;
}
private:
/**
* @brief Evaluate explicitly bound fields and geometry, allowing arbitrary caller-owned storage.
* @note Nonfinite coefficients are numerical rejections. Incorrect bindings and sizes are caller
* errors. Either failure invalidates current cache access before returning or reporting the error.
*/
template <typename RequestedKey, typename Coefficients, typename Geometry>
requires (detail::coefficient_inputs<Coefficients>(typename Plan::Coefficients{}))
[[nodiscard]] auto evaluate_result(const Coefficients& coefficients, const Geometry& geometry,
utils::dependencies::PreparationMode mode = utils::dependencies::PreparationMode::reuse_unchanged) -> EvaluationFor<RequestedKey> {
m_cache.clear_preparation();
const bool finite = [&]<typename... Terms>(utils::dependencies::KeyList<Terms...>) {
return (utils::checks::is_finite(coefficients.template get<Terms>().values) && ...);
}(typename Plan::Coefficients{});
if (!finite) {
return std::unexpected(typename Cache::Failure{.computation_index = Graph::size,
.error = EquilibriumFailure{operators::backend::mfem::PreparationRejection{
.reason = operators::backend::mfem::PreparationRejectionReason::NON_FINITE,
.message = "A stellar candidate contains a non-finite physical coefficient. Supply finite field and scalar values before evaluating it."
}}});
}
const auto inputs = utils::dependencies::combine_dependency_contexts(detail::candidate_inputs(*m_bound, coefficients, geometry), m_additional);
using Requested = utils::dependencies::CombinedKeyList<utils::dependencies::KeyList<RequestedKey>, EvaluationServices,
std::conditional_t<std::same_as<RequestedKey, preconditioning::PreparedSystem>, LinearServices, utils::dependencies::KeyList<>>>;
const auto preparation = m_cache.try_prepare(inputs, Requested{}, mode);
if (!preparation) {
return std::unexpected(preparation.error());
}
return m_cache.template get<RequestedKey>();
}
/** @brief Evaluate a moving star directly from its physical fields and shape in one owning state. */
template <typename RequestedKey, utils::dependencies::IsDependencyContext Prescribed = utils::dependencies::DependencyContext<>>
requires Plan::has_shape && detail::PrescribedInputsFor<Plan, Prescribed>
[[nodiscard]] auto evaluate_result(const StellarState<Layout>& state, const Prescribed& prescribed = Prescribed{},
utils::dependencies::PreparationMode mode = utils::dependencies::PreparationMode::reuse_unchanged) -> EvaluationFor<RequestedKey> {
validate_state(state);
const auto fields = detail::state_coefficients(*m_bound->discretization, state, prescribed);
const auto coefficients = fields.context();
return evaluate_result<RequestedKey>(coefficients, state.template get<discretization::blocks::surface_deformation::coefficients>(), mode);
}
/** @brief Evaluate a fixed-geometry star; prescribed displacement is independent of its solved fields. */
template <typename RequestedKey, utils::dependencies::IsDependencyContext Prescribed = utils::dependencies::DependencyContext<>>
requires (!Plan::has_shape) && detail::PrescribedInputsFor<Plan, Prescribed>
[[nodiscard]] auto evaluate_result(const StellarState<Layout>& state, const ::mfem::Vector& displacement, const Prescribed& prescribed = Prescribed{},
utils::dependencies::PreparationMode mode = utils::dependencies::PreparationMode::reuse_unchanged) -> EvaluationFor<RequestedKey> {
validate_state(state);
const auto fields = detail::state_coefficients(*m_bound->discretization, state, prescribed);
const auto coefficients = fields.context();
return evaluate_result<RequestedKey>(coefficients, displacement, mode);
}
/** @brief Reject a same-type state with incompatible coordinate sizes before constructing backend views. */
void validate_state(const StellarState<Layout>& state) {
using namespace utils::errors;
m_cache.clear_preparation();
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(state.layout() == m_layout,
"The stellar state's field or equation sizes differ from this evaluator's layout. Construct the state with the matching discretization layout.");
}
public:
/**
* @brief Evaluate a candidate without preparing its inverse.
* @param inputs An owning state (and prescribed inputs), or explicit coefficient and geometry views.
* Fixed-geometry states also take the prescribed displacement. A final PreparationMode is optional.
* Selected evaluation services are prepared in the same transaction; linear-only services remain unrequested.
*/
template <typename... InputsT>
requires requires(EquilibriumEvaluator& evaluator, const InputsT&... inputs) {
evaluator.template evaluate_result<ResultKey>(inputs...);
}
[[nodiscard]] Evaluation evaluate(const InputsT&... inputs) {
return evaluate_result<ResultKey>(inputs...);
}
/**
* @brief Prepare an owning Jacobian and matching inverse only when a linear correction is needed.
* @param inputs The same candidate and optional preparation arguments accepted by evaluate().
* @note A numerical inverse failure preserves every previously returned preparation.
*/
template <typename... InputsT>
requires utils::dependencies::ContainsKey<preconditioning::PreparedSystem, typename Graph::Results> &&
requires(EquilibriumEvaluator& evaluator, const InputsT&... inputs) {
evaluator.template evaluate_result<preconditioning::PreparedSystem>(inputs...);
}
[[nodiscard]] auto prepare_linear_system(const InputsT&... inputs) -> decltype(evaluate_result<preconditioning::PreparedSystem>(inputs...)) {
return evaluate_result<preconditioning::PreparedSystem>(inputs...);
}
private:
std::shared_ptr<const Bound> m_bound;
Cache m_cache;
Layout m_layout;
AdditionalInputs m_additional;
};
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <variant>
#include "serif/geometry/deformation/failure.hpp"
#include "serif/geometry/surface/linearization.hpp"
#include "serif/backend/mfem/operators/rejection.hpp"
#include "serif/solver/preconditioner/failure.hpp"
namespace serif::model::backend::mfem {
/** @brief Retain the original physical or numerical preparation failure and its diagnostic location. */
using EquilibriumFailure = std::variant<operators::backend::mfem::PreparationRejection,
geometry::deformation::DeformationFailure, geometry::surface::SurfaceFailure, solver::preconditioner::Failure, std::string>;
}

View File

@@ -0,0 +1,136 @@
#pragma once
#include "serif/backend/mfem/discretization/resources.hpp"
#include "serif/backend/mfem/discretization/term_binding.hpp"
#include "serif/backend/mfem/model/discretization.hpp"
#include "serif/model/equilibrium_plan.hpp"
#include "serif/model/mesh_options.hpp"
#include "serif/model/stellar_model.hpp"
namespace serif::model::backend::mfem {
/** @brief Generate physical and logical meshes with the standard core/envelope/vacuum labels. */
[[nodiscard]] discretization::backend::mfem::Resources make_stellar_resources(const StellarMeshOptions& options);
/** @brief Share identical collections and spaces, retaining native coefficient numbering and storage. */
[[nodiscard]] ::mfem::FiniteElementSpace& make_space(discretization::backend::mfem::Resources& resources,
std::unique_ptr<::mfem::FiniteElementCollection> collection, int components, int ordering = ::mfem::Ordering::byNODES);
namespace detail {
/** @brief Recognize a field-specific reconstruction request without prescribing its numerical form. */
template <typename Specification>
concept ReconstructionSpecification = requires { typename Specification::Term; };
/** @brief Attach a requested numerical interpretation while the bindings still have a single mutable owner. */
template <typename Owner, typename... Terms, ReconstructionSpecification Specification>
void apply_reconstruction(Owner& owner, utils::dependencies::KeyList<Terms...>, const Specification& specification) {
using namespace utils::errors;
using Term = typename Specification::Term;
using Keys = utils::dependencies::KeyList<Terms...>;
if constexpr (!(std::same_as<Term, Terms> || ...)) {
serif_error<std::invalid_argument, SERiFErrorCode::INVALID_ARGUMENT>("The reconstruction's field is not present in this stellar model.");
} else {
auto& binding = std::get<utils::dependencies::key_index<Term, Keys>()>(owner.bindings);
using discretization::backend::mfem::make_reconstruction;
if constexpr (requires { make_reconstruction(specification, *binding); }) {
auto plan = make_reconstruction(specification, *binding);
if constexpr (discretization::blocks::SpatialTerm<Term>) {
using Binding = typename Owner::template Binding<Term>;
binding = std::make_unique<Binding>(binding->space(), std::move(plan));
} else {
serif_error<std::invalid_argument, SERiFErrorCode::INVALID_ARGUMENT>("Spatial reconstruction cannot be attached to a global scalar field.");
}
} else {
serif_error<std::invalid_argument, SERiFErrorCode::INVALID_ARGUMENT>("This reconstruction form has no preparation function for the selected backend and field.");
}
}
}
/** @brief Reject duplicate field requests before constructing any finite elements. */
template <ReconstructionSpecification... Specifications>
void validate_reconstruction_requests() {
using namespace utils::errors;
constexpr bool unique = (([]<typename Specification>() {
return (0 + ... + int(std::same_as<typename Specification::Term, typename Specifications::Term>)) == 1;
}.template operator()<Specifications>()) && ...);
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(unique, "Specify at most one reconstruction for each field.");
}
/** @brief Construct only representations supported by TermBinding, directly from the term's compile-time descriptor. */
template <discretization::blocks::SpatialTerm Term>
[[nodiscard]] ::mfem::FiniteElementSpace& term_space(discretization::backend::mfem::Resources& resources) {
using Traits = discretization::blocks::SpatialTermTraits<Term>;
using enum discretization::functional_space::Family;
static_assert(Traits::rank <= 1 && (Traits::family == h1 || Traits::family == l2),
"The selected backend supports scalar/vector H1 and L2 terms.");
constexpr int components = Traits::rank == 0 ? 1 : 3;
if constexpr (Traits::family == h1) {
return make_space(resources, std::make_unique<::mfem::H1_FECollection>(Traits::order, 3), components, components == 1 ? ::mfem::Ordering::byNODES : ::mfem::Ordering::byVDIM);
} else {
return make_space(resources, std::make_unique<::mfem::L2_FECollection>(Traits::order, 3), components, components == 1 ? ::mfem::Ordering::byNODES : ::mfem::Ordering::byVDIM);
}
}
/** @brief Own precisely the model's coefficients plus displacement geometry; no particular thermodynamic field is required. */
template <typename... Terms, ReconstructionSpecification... Specifications>
[[nodiscard]] auto make_bindings(const StellarMeshOptions& options, utils::dependencies::KeyList<Terms...> keys, const Specifications&... specifications) {
using namespace utils::dependencies;
using Schema = discretization::domain::schema::CoreEnvelopeVacuumDomainSchema;
using Context = decltype(make_dependency_context(borrow<Terms>(std::declval<discretization::backend::mfem::TermBinding<Terms, Schema>&>())...));
using Owner = discretization::backend::mfem::OwnedBindings<Context>;
validate_reconstruction_requests<Specifications...>();
auto owner = std::make_shared<Owner>();
owner->resources = make_stellar_resources(options);
([&] {
using Binding = typename Owner::template Binding<Terms>;
auto& binding = std::get<key_index<Terms, KeyList<Terms...>>()>(owner->bindings);
if constexpr (discretization::blocks::SpatialTerm<Terms>) {
binding = std::make_unique<Binding>(term_space<Terms>(owner->resources));
} else {
binding = std::make_unique<Binding>(*owner->resources.meshes.front());
}
}(), ...);
(apply_reconstruction(*owner, keys, specifications), ...);
return owner;
}
}
/**
* @brief Create an owning fixed-geometry discretization using the physical model's term descriptors.
* @note This convenience factory uses the standard spherical mesh and domain schema. The existing
* owner-and-bindings overload remains available for custom meshes, schemas, and native spaces.
*/
template <StellarModelType Model, detail::ReconstructionSpecification... Specifications>
[[nodiscard]] auto make_stellar_discretization(const Model&, const StellarMeshOptions& options = {}, const Specifications&... specifications) {
using Description = decltype(resolve_equilibrium_equations(std::declval<const Model&>()));
using Plan = EquilibriumPlan<typename Model::EquationOfState, Description>;
using Terms = utils::dependencies::CombinedKeyList<typename Plan::Coefficients,
utils::dependencies::KeyList<discretization::blocks::displacement::vector>>;
auto owner = detail::make_bindings(options, Terms{}, specifications...);
return make_stellar_discretization(owner, owner->context(), *owner->resources.compactification, *owner->resources.mapper);
}
/** @brief Add a moving surface with reference companions chosen from displacement geometry, independently of EOS variables. */
template <StellarModelType Model, typename Surface, typename Interior, typename Vacuum, detail::ReconstructionSpecification... Specifications>
[[nodiscard]] auto make_stellar_discretization(const Model& model, const StellarMeshOptions& options,
geometry::deformation::DomainDeformation<Surface, Interior, Vacuum> deformation, const Specifications&... specifications) {
using Description = decltype(resolve_equilibrium_equations(std::declval<const Model&>()));
using Plan = EquilibriumPlan<typename Model::EquationOfState, Description>;
using Terms = utils::dependencies::CombinedKeyList<typename Plan::Coefficients,
utils::dependencies::KeyList<discretization::blocks::displacement::vector>>;
auto owner = detail::make_bindings(options, Terms{}, specifications...);
using Displacement = discretization::blocks::displacement::vector;
const auto context = owner->context();
const auto* collection = context.template get<Displacement>().space().FEColl();
auto& scalar = make_space(owner->resources, std::unique_ptr<::mfem::FiniteElementCollection>{::mfem::FiniteElementCollection::New(collection->Name())}, 1);
auto& reference_displacement = make_space(owner->resources,
std::unique_ptr<::mfem::FiniteElementCollection>{::mfem::FiniteElementCollection::New(collection->Name())}, 3, ::mfem::Ordering::byNODES);
const auto reference = geometry::deformation::backend::mfem::deformation_discretization(
scalar, reference_displacement, *owner->resources.meshes[1]);
return with_deformation(make_stellar_discretization(owner, context, *owner->resources.compactification, *owner->resources.mapper),
std::move(deformation), reference);
}
}

View File

@@ -0,0 +1,174 @@
#pragma once
#include <limits>
#include <memory>
#include <tuple>
#include <utility>
#include "serif/backend/mfem/model/computations/interior.hpp"
#include "serif/backend/mfem/model/computations/surface_potential.hpp"
#include "serif/backend/mfem/model/computations/potential_infinity.hpp"
#include "serif/backend/mfem/model/computations/pressure_boundary.hpp"
#include "serif/model/equilibrium_plan.hpp"
#include "serif/backend/mfem/geometry/deformation/computations.hpp"
#include "serif/backend/mfem/model/volume_displacement.hpp"
#include "serif/backend/mfem/geometry/surface/boundary_computations.hpp"
#include "serif/model/keys.hpp"
#include "serif/model/plan.hpp"
namespace serif::model {
namespace detail {
/** @brief Collect all additions before applying any boundary replacement. */
template <typename Layout, typename Owner, typename... ContributionKeys>
[[nodiscard]] auto physical_system_computation(Layout layout, std::shared_ptr<const Owner> owner,
utils::dependencies::KeyList<ContributionKeys...>) {
using namespace utils::dependencies;
using namespace discretization::blocks;
using namespace utils::errors;
const std::size_t rows = layout.template total_size<RepresentedValueKind::residual>();
const std::size_t columns = layout.template total_size<RepresentedValueKind::value>();
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(rows <= std::numeric_limits<int>::max() && columns <= std::numeric_limits<int>::max(), "Stellar system dimensions exceed the backend's integer range.");
return make_computation<keys::PhysicalSystem, InputList<ContributionKeys...>>([layout, owner = std::move(owner)](const auto&... contributions) {
computations::Contributions combined;
combined.actions.emplace();
const auto append = [&](const computations::Contributions& part) {
combined.residual.insert(combined.residual.end(), part.residual.begin(), part.residual.end());
combined.jacobian.insert(combined.jacobian.end(), part.jacobian.begin(), part.jacobian.end());
combined.replacements.insert(combined.replacements.end(), part.replacements.begin(), part.replacements.end());
const auto& actions = part.actions ? *part.actions : part.jacobian;
combined.actions->insert(combined.actions->end(), actions.begin(), actions.end());
};
(append(contributions), ...);
return backend::mfem::make_physical_system(
static_cast<int>(layout.template total_size<RepresentedValueKind::residual>()),
static_cast<int>(layout.template total_size<RepresentedValueKind::value>()),
std::move(combined.residual), std::move(combined.jacobian), std::move(combined.replacements), owner, std::move(combined.actions));
});
}
/** @brief Every interior equation contributes once; extension keys identify explicit additions or replacements. */
template <typename... Equations, typename... Additional>
[[nodiscard]] consteval auto contribution_keys(utils::types::TypeList<Equations...>, utils::dependencies::KeyList<Additional...>) {
return utils::dependencies::KeyList<computations::InteriorContribution<typename Equations::Test>..., Additional...>{};
}
/** @brief Share each immutable equation with its volume graph, then publish one complete row contribution. */
template <typename Registry, typename Discretization, typename Layout, typename... Equations>
[[nodiscard]] auto interior_computations(const std::shared_ptr<const Registry>& registry, const Discretization& discretization,
const Layout& layout, operators::backend::mfem::VolumeOptions options, utils::types::TypeList<Equations...>) {
return std::tuple_cat(operators::backend::mfem::make_volume_equation_computations<typename Equations::Test>(
std::shared_ptr<const Equations>(registry, &registry->template get<Equations>()),
discretization.template equation_discretization<typename Equations::Test>(), options)...,
computations::interior_computations<Equations>(layout)...);
}
}
/**
* @brief Construct the complete stellar graph at one explicit composition point.
* @note The registry is the authoritative equation list. Additional declarations supply deformation,
* boundary treatment, constraints, normalization or inverses; only named contribution results modify
* the physical system. Dependency preparation schedules the work and publishes complete results.
*/
template <ExecutableInteriorEquations Registry, typename Discretization, discretization::blocks::SystemLayoutType Layout,
typename... AdditionalComputations, typename... AdditionalContributions>
[[nodiscard]] auto make_model_graph(std::shared_ptr<const Registry> registry, std::shared_ptr<const Discretization> discretization,
const Layout& layout, std::tuple<AdditionalComputations...> additional = {},
utils::dependencies::KeyList<AdditionalContributions...> contributions = {}, operators::backend::mfem::VolumeOptions options = {}) {
using namespace utils::errors;
using Equations = typename Registry::Equations;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(registry != nullptr && discretization != nullptr, "Model graph construction requires an owning equation registry and discretization binding.");
const auto interior = detail::interior_computations(registry, *discretization, layout, options, Equations{});
const auto physical = detail::physical_system_computation(layout, discretization, detail::contribution_keys(Equations{}, contributions));
return std::apply([](auto... computations) {
return utils::dependencies::make_computation_graph(std::move(computations)...);
}, std::tuple_cat(interior, std::move(additional), std::tuple{physical}));
}
/** @brief Retain a standalone registry, including move-only equations supplied as an rvalue. */
template <ExecutableInteriorEquations Registry, typename Discretization, discretization::blocks::SystemLayoutType Layout,
typename... AdditionalComputations, typename... AdditionalContributions>
[[nodiscard]] auto make_model_graph(Registry registry, std::shared_ptr<const Discretization> discretization,
const Layout& layout, std::tuple<AdditionalComputations...> additional = {},
utils::dependencies::KeyList<AdditionalContributions...> contributions = {}, operators::backend::mfem::VolumeOptions options = {}) {
return make_model_graph(std::make_shared<const Registry>(std::move(registry)), std::move(discretization), layout, std::move(additional), contributions, std::move(options));
}
namespace detail {
/** @brief One shared deformation feeds all geometry derivatives and the surface-potential shape row. */
template <typename Bound, typename Layout>
[[nodiscard]] auto shape_computations(const std::shared_ptr<const Bound>& bound, const Layout& layout) {
namespace deformation = geometry::deformation::backend::mfem;
namespace execution = operators::backend::mfem;
if constexpr (Bound::Plan::has_shape) {
using Surface = std::remove_cvref_t<decltype(bound->discretization->deformation.model.surface)>;
using Metadata = typename execution::volume_keys::Scoped<discretization::blocks::gravity::potential>::Bindings;
const auto& displacement = bound->discretization->template binding<discretization::blocks::displacement::vector>();
return std::tuple_cat(deformation::make_deformation_computations<Surface>(),
deformation::make_volume_displacement_computations<Metadata>(displacement),
std::tuple{computations::shape_derivatives(displacement),
computations::surface_potential_contribution(bound->definition.shape_force, layout)});
}
else {
return std::tuple{};
}
}
/** @brief Sample the internally selected pressure formulation through its compiled traces. */
template <typename Bound, typename Layout>
[[nodiscard]] auto boundary_computations(const Bound&, const Layout& layout) {
using Plan = typename Bound::Plan;
if constexpr (Plan::has_surface) {
using Formulation = typename Plan::Surface::Formulation;
return std::tuple_cat(geometry::surface::backend::mfem::make_pressure_boundary_computations<Formulation>(),
std::tuple{computations::pressure_boundary_contribution<Formulation>(layout)});
}
else {
return std::tuple{};
}
}
/** @brief Name exactly the explicitly selected non-interior contributions to final assembly. */
template <typename Plan>
[[nodiscard]] consteval auto equilibrium_contribution_keys() {
using namespace utils::dependencies;
using Shape = std::conditional_t<Plan::has_shape,
KeyList<computations::ShapeContribution>, KeyList<>>;
using Surface = std::conditional_t<Plan::has_surface, KeyList<computations::PressureBoundaryContribution>, KeyList<>>;
using Infinity = std::conditional_t<Plan::has_primal_gravity, KeyList<computations::PotentialInfinityContribution>, KeyList<>>;
return CombinedKeyList<Shape, Surface, Infinity>{};
}
}
/**
* @brief Connect the explicit equilibrium definition in one readable place.
* @note Interior physics, shape force and surface replacement remain separate
* declarations. The final physical-system computation collects additions before replacing rows.
*/
template <typename Bound, typename Layout, typename... NumericalComputations>
[[nodiscard]] auto make_equilibrium_graph(std::shared_ptr<const Bound> bound, const Layout& layout,
std::tuple<NumericalComputations...> numerical = {}, operators::backend::mfem::VolumeOptions options = {}) {
using namespace utils::errors;
using Description = typename Bound::Description;
using Registry = typename Description::InteriorEquations;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(bound != nullptr, "An equilibrium graph requires a bound model owner. Bind the physical description and discretization before constructing the graph.");
const auto shape = detail::shape_computations(bound, layout);
const auto surface = detail::boundary_computations(*bound, layout);
const auto infinity = [&]() {
if constexpr (Bound::Plan::has_primal_gravity) {
return std::tuple{computations::potential_infinity_contribution(layout,
bound->discretization->template binding<discretization::blocks::gravity::potential>())};
} else {
return std::tuple{};
}
}();
const auto contributions = detail::equilibrium_contribution_keys<typename Bound::Plan>();
return make_model_graph(std::shared_ptr<const Registry>(bound, &bound->definition.interior_equations), bound, layout,
std::tuple_cat(shape, surface, infinity, std::move(numerical)), contributions, options);
}
}

View File

@@ -0,0 +1,66 @@
#pragma once
#include "serif/model/plan.hpp"
#include "serif/backend/mfem/operators/volume_inputs.hpp"
namespace serif::model::backend::mfem
{
namespace detail
{
/** @brief Accepted actions may borrow only bindings retained by the declared discretization owner. */
template <typename Discretization, typename Coefficients, typename... Terms>
void validate_coefficient_owners(const Discretization& discretization, const Coefficients& coefficients, utils::dependencies::KeyList<Terms...>)
{
using namespace utils::errors;
const auto validate = [&]<typename Term>()
{
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR>(std::addressof(coefficients.template get<Term>().binding) == std::addressof(discretization.template binding<Term>()),
"Stellar coefficients must use the exact term bindings retained by their discretization owner. Rebind the coefficient vector through that discretization before preparing the model.");
};
(validate.template operator()<Terms>(), ...);
}
/** @brief Bind common physical coefficients once and keep equation-specific metadata distinct. */
template <typename Discretization, typename Coefficients, typename... Equations, typename... Fields>
[[nodiscard]] auto model_inputs(const Discretization& discretization, const Coefficients& coefficients,
utils::types::TypeList<Equations...>, utils::dependencies::KeyList<Fields...>)
{
using namespace utils::dependencies;
using namespace utils::errors;
namespace execution = operators::backend::mfem;
const auto* compactification_space = discretization.compactification.FESpace();
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR>(compactification_space != nullptr,
"Stellar compactification requires a finite-element space. Bind compactification to the model mesh before preparing its equations.");
return make_dependency_context(
execution::make_volume_binding_input<Equations, typename Equations::Test>(discretization.template equation_discretization<typename Equations::Test>(), coefficients)...,
own<execution::CompactificationCoefficients>(execution::MappingCoefficients{.binding = *compactification_space, .values = discretization.compactification}, discretization::backend::mfem::CoefficientTracking{}),
own<Fields>(coefficients.template get<Fields>(), discretization::backend::mfem::CoefficientTracking{})...);
}
}
/** @brief Supply the same physical coefficient resources to all explicitly listed interior equations. */
template <ExecutableInteriorEquations Registry, typename Discretization, typename Coefficients>
[[nodiscard]] auto make_model_inputs(const Discretization& discretization, const Coefficients& coefficients)
{
using Plan = ModelPlan<Registry>;
detail::validate_coefficient_owners(discretization, coefficients, typename Plan::Coefficients{});
return detail::model_inputs(discretization, coefficients, typename Registry::Equations{}, typename Plan::Coefficients{});
}
/** @brief Prepare an interior system with a prescribed displacement field, without adding displacement unknowns. */
template <ExecutableInteriorEquations Registry, typename Discretization, typename Coefficients>
[[nodiscard]] auto make_model_inputs(const Discretization& discretization, const Coefficients& coefficients, const ::mfem::Vector& displacement)
{
const auto inputs = make_model_inputs<Registry>(discretization, coefficients);
const auto view = operators::backend::mfem::bind(discretization.template binding<discretization::blocks::displacement::vector>(), displacement);
return utils::dependencies::combine_dependency_contexts(inputs, utils::dependencies::make_dependency_context(
utils::dependencies::own<operators::backend::mfem::DisplacementCoefficients>(view, discretization::backend::mfem::CoefficientTracking{})));
}
/** @brief Reject a displacement temporary whose coefficients would expire before graph preparation. */
template <typename Registry, typename Discretization, typename Coefficients>
void make_model_inputs(const Discretization&, const Coefficients&, const ::mfem::Vector&&) = delete;
}

View File

@@ -0,0 +1,117 @@
#pragma once
#include <memory>
#include <tuple>
#include <vector>
#include "serif/backend/mfem/model/normalized_system.hpp"
#include "serif/backend/mfem/model/computations/solver_layout.hpp"
#include "serif/utils/dependencies/snapshot.hpp"
#include "serif/model/keys.hpp"
#include "serif/model/normalization/plan.hpp"
#include "serif/backend/mfem/solver/normalization/metric_computations.hpp"
#include "serif/solver/normalization/computations.hpp"
#include "serif/backend/mfem/geometry/deformation/computations.hpp"
#include "serif/backend/mfem/geometry/surface/boundary_inputs.hpp"
#include "serif/backend/mfem/discretization/dof_selection.hpp"
namespace serif::model::normalization {
struct Configuration final {};
struct PreparedSystem final {};
namespace detail {
/** @brief Rebuilding the input context does not change the identity of an immutable configuration. */
struct ConfigurationTracking final : utils::dependencies::ValueSnapshot {
/** @brief Track the owned configuration rather than a copied shared pointer wrapper. */
template <typename Options>
[[nodiscard]] const void* identity(const std::shared_ptr<const Options>& options) const {
return options.get();
}
};
template <typename Metric>
struct VolumeMetric {
using Term = void;
};
template <typename TermT>
struct VolumeMetric<solver::normalization::ReferenceVolumeMetric<TermT>> {
using Term = TermT;
};
/** @brief Reference volume metrics borrow the same term binding and track its mesh and space revisions. */
template <typename Metric, typename Discretization>
[[nodiscard]] auto metric_inputs(const Discretization& discretization) {
using namespace utils::dependencies;
using Term = typename VolumeMetric<Metric>::Term;
if constexpr (!std::same_as<Term, void>) {
return make_dependency_context(solver::normalization::backend::mfem::reference_metric_binding(discretization.template binding<Term>()));
} else {
return make_dependency_context();
}
}
/** @brief Supply built-in metrics once; custom metric keys are provided by the user's ordinary graph sources. */
template <typename Metric, typename Discretization>
[[nodiscard]] auto metric_computations(const Discretization&) {
using Term = typename VolumeMetric<Metric>::Term;
if constexpr (!std::same_as<Term, void>) {
using Binding = std::remove_cvref_t<decltype(std::declval<const Discretization&>().template binding<Term>())>;
return std::tuple{solver::normalization::backend::mfem::make_reference_volume_metric_computation<Term, typename Binding::Schema>()};
} else if constexpr (std::same_as<Metric, geometry::deformation::keys::SurfaceMetric>) {
return std::tuple{geometry::deformation::backend::mfem::make_surface_metric_computation()};
} else {
return std::tuple{};
}
}
}
/** @brief Keep the immutable configuration alive and reference metrics separate from candidate coefficients. */
template <typename Options, typename Discretization, typename... Metrics>
[[nodiscard]] auto make_inputs(std::shared_ptr<const Options> options, const Discretization& discretization, utils::dependencies::KeyList<Metrics...>) {
using namespace utils::dependencies;
const auto configuration = make_dependency_context(own<Configuration>(options, detail::ConfigurationTracking{}));
return combine_dependency_contexts(configuration, options->sources, detail::metric_inputs<Metrics>(discretization)...);
}
/** @brief Compose normalization into the physical graph, including the selected surface row replacement. */
template <typename NormalizationPlan, typename Bound, typename Options, typename Layout, typename... Metrics>
[[nodiscard]] auto make_computations(const Bound& bound, const Options& options, const Layout& layout, utils::dependencies::KeyList<Metrics...>) {
using namespace utils::dependencies;
namespace numerical = solver::normalization;
const auto scales = make_computation<numerical::keys::Scales, InputList<Configuration>>(
[](const std::shared_ptr<const Options>& configuration) {
using numerical::prepare_reference_scales;
return prepare_reference_scales(configuration->scheme, configuration->gravitational_constant);
});
const auto surface = [&]() {
if constexpr (Bound::Plan::has_surface) {
return std::tuple{make_computation<SurfaceRows, InputList<geometry::surface::backend::mfem::keys::BoundaryDiscretization>>([](const auto& boundary) {
const auto rows = boundary->selected_rows->reduced_dofs();
return std::vector<int>(rows.begin(), rows.end());
})};
} else {
return std::tuple{};
}
}();
const auto prepared = make_computation<PreparedSystem, InputList<keys::PhysicalSystem, numerical::keys::Normalization, numerical::keys::Scales>>(
backend::mfem::make_normalized_system);
const auto infinity = [&]() {
if constexpr (Bound::Plan::has_primal_gravity) {
const auto& binding = bound.discretization->template binding<discretization::blocks::gravity::potential>();
// Match potential_infinity_contribution: its zero trace uses potential units, not Poisson-row units.
const auto selected = discretization::backend::mfem::select_boundary_dofs<discretization::domain::InfinitySurfaceBoundary>(binding);
return std::tuple{make_computation<InfinityRows, InputList<>>([selected] {
selected.validate();
const auto& rows = selected.reduced_dofs();
return std::vector<int>(rows.begin(), rows.end());
})};
} else {
return std::tuple{};
}
}();
return std::tuple_cat(std::tuple{scales, prepared}, computations::make_solver_layout_computations(layout), surface, infinity,
detail::metric_computations<Metrics>(*bound.discretization)...,
numerical::make_normalization_computations<NormalizationPlan>(layout), options.computations);
}
}

View File

@@ -0,0 +1,76 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <expected>
#include <memory>
#include <string>
#include <vector>
#include "serif/backend/mfem/model/system.hpp"
#include "serif/backend/mfem/solver/normalization/operators.hpp"
#include "serif/solver/normalization/scales.hpp"
#include "serif/utils/error/errors.hpp"
namespace serif::model::backend::mfem {
/** @brief A frozen physical preparation and its owning solver coordinates, safe after later graph preparation. */
class NormalizedSystem final {
public:
/** @brief Retain the physical operator before creating its normalized action and residual. */
NormalizedSystem(std::shared_ptr<const PhysicalSystem> physical,
solver::normalization::DiagonalNormalization coordinates, solver::normalization::StellarReferenceScales scales) :
m_physical(checked_physical(std::move(physical))), m_scales(std::move(scales)),
m_jacobian(*m_physical->jacobian().action(), std::move(coordinates)), m_residual(m_physical->residual().size()) {
m_jacobian.normalization().normalize_residual(m_physical->residual(), m_residual);
}
/** @brief Inspect the matching dimensional residual, Jacobian and selected physical blocks. */
[[nodiscard]] const std::shared_ptr<const PhysicalSystem>& physical() const noexcept {
return m_physical;
}
/** @brief Convert states, residuals and solver corrections with this preparation's frozen factors. */
[[nodiscard]] const solver::normalization::DiagonalNormalization& coordinates() const noexcept {
return m_jacobian.normalization();
}
/** @brief Inspect the characteristic scales actually used, including the estimated radius. */
[[nodiscard]] const solver::normalization::StellarReferenceScales& reference_scales() const noexcept {
return m_scales;
}
/** @brief Read the normalized residual in the model's independent equation ordering. */
[[nodiscard]] const std::vector<double>& residual() const noexcept {
return m_residual;
}
/** @brief Apply L J S^-1 or its transpose while retaining the physical operator owner. */
[[nodiscard]] const solver::normalization::backend::mfem::ScaledJacobianOperator& jacobian() const noexcept {
return m_jacobian;
}
private:
/** @brief Diagnose a missing preparation before a normalized adapter borrows its Jacobian. */
[[nodiscard]] static std::shared_ptr<const PhysicalSystem> checked_physical(std::shared_ptr<const PhysicalSystem> physical) {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(physical != nullptr, "Normalized coordinates require an frozen physical system. Prepare the stellar model first.");
return physical;
}
std::shared_ptr<const PhysicalSystem> m_physical;
solver::normalization::StellarReferenceScales m_scales;
solver::normalization::backend::mfem::ScaledJacobianOperator m_jacobian;
std::vector<double> m_residual;
};
/** @brief Reject a nonrepresentable normalized residual before publishing the matching physical preparation. */
[[nodiscard]] inline std::expected<std::shared_ptr<const NormalizedSystem>, std::string> make_normalized_system(
std::shared_ptr<const PhysicalSystem> physical, solver::normalization::DiagonalNormalization coordinates,
solver::normalization::StellarReferenceScales reference) {
auto result = std::make_shared<const NormalizedSystem>(std::move(physical), std::move(coordinates), std::move(reference));
if (!std::ranges::all_of(result->residual(), [](double value) { return std::isfinite(value); })) {
return std::unexpected("The normalized stellar residual is not finite. Check the reference scales and the physical candidate.");
}
return result;
}
}

View File

@@ -0,0 +1,78 @@
#pragma once
#include <concepts>
#include <memory>
#include <utility>
#include "serif/backend/mfem/model/normalized_system.hpp"
#include "serif/backend/mfem/solver/preconditioner/solver.hpp"
namespace serif::model::backend::mfem {
/**
* @brief Retain one evaluated star and its matching inverse for a linear solve.
* @note Later evaluations cannot change these operators. MFEM adapters reuse scratch storage;
* applications of the same preparation must not run concurrently.
*/
template <typename System>
requires (std::same_as<System, PhysicalSystem> || std::same_as<System, NormalizedSystem>)
class PreconditionedSystem final {
public:
/** @brief Establish physical ownership before constructing an adapter in the selected solver coordinates. */
PreconditionedSystem(std::shared_ptr<const System> system, solver::preconditioner::backend::mfem::Inverse inverse) :
m_system(checked_system(std::move(system))),
m_physical_inverse(std::make_shared<solver::preconditioner::backend::mfem::Solver>(std::move(inverse), physical_jacobian())),
m_inverse(make_solver()) {
}
/** @brief Inspect the residual and Jacobian belonging to this inverse. */
[[nodiscard]] const std::shared_ptr<const System>& system() const noexcept {
return m_system;
}
/** @brief Supply MFEM with the matching physical or normalized inverse; unrelated rebinding is rejected. */
[[nodiscard]] ::mfem::Solver& preconditioner() const noexcept {
return *m_inverse;
}
/** @brief Apply or inspect the dimensional inverse before the coordinate transformation. */
[[nodiscard]] const solver::preconditioner::backend::mfem::Solver& physical_preconditioner() const noexcept {
return *m_physical_inverse;
}
/** @brief Tell the outer solver whether the selected inverse has a fixed linear application. */
[[nodiscard]] solver::preconditioner::ApplicationContract contract() const noexcept {
return m_physical_inverse->contract();
}
private:
/** @brief Report an invalid owner before any adapter borrows an operator from it. */
[[nodiscard]] static std::shared_ptr<const System> checked_system(std::shared_ptr<const System> system) {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(system != nullptr,
"A preconditioned stellar system requires an evaluated star. Prepare the matching residual and Jacobian before binding its inverse.");
return system;
}
/** @brief Bind the physical inverse to the dimensional root even when the outer solve is normalized. */
[[nodiscard]] auto physical_jacobian() const {
if constexpr (std::same_as<System, NormalizedSystem>) {
return m_system->physical()->jacobian().action();
} else {
return m_system->jacobian().action();
}
}
/** @brief Reuse the existing normalization adapter while retaining both of its borrowed owners. */
[[nodiscard]] std::shared_ptr<::mfem::Solver> make_solver() const {
if constexpr (std::same_as<System, NormalizedSystem>) {
return std::make_shared<solver::normalization::backend::mfem::ScaledPreconditioner>(*m_physical_inverse, m_system->jacobian());
} else {
return m_physical_inverse;
}
}
std::shared_ptr<const System> m_system;
std::shared_ptr<solver::preconditioner::backend::mfem::Solver> m_physical_inverse;
std::shared_ptr<::mfem::Solver> m_inverse;
};
}

View File

@@ -0,0 +1,56 @@
#pragma once
#include <concepts>
#include <memory>
#include <tuple>
#include <type_traits>
#include <utility>
#include "serif/backend/mfem/model/preconditioned_system.hpp"
#include "serif/backend/mfem/model/computations/physical_blocks.hpp"
#include "serif/backend/mfem/model/computations/solver_layout.hpp"
#include "serif/backend/mfem/geometry/deformation/computations.hpp"
#include "serif/backend/mfem/solver/preconditioner/graph.hpp"
namespace serif::model::preconditioning {
/** @brief An owning evaluated system and its matching preconditioner. */
struct PreparedSystem final {};
namespace detail {
/** @brief Add a common reference computation only when normalization has not already supplied it. */
template <typename Computation, typename... Existing>
[[nodiscard]] auto missing_computation(Computation computation, const std::tuple<Existing...>&) {
if constexpr ((std::same_as<typename Computation::Key, typename Existing::Key> || ...)) {
return std::tuple{};
} else {
return std::tuple{std::move(computation)};
}
}
}
/**
* @brief Connect a recipe to the same physical root, layout and reference geometry as model evaluation.
* @note Merely adding these computations does not prepare an inverse. Only requesting PreparedSystem
* reaches this branch, so line-search evaluations can reuse the physical graph without inverse setup.
*/
template <typename ResultKey, typename Recipe, typename Layout, typename... Existing>
[[nodiscard]] auto make_computations(const Recipe& recipe, const Layout& layout, const std::tuple<Existing...>& existing) {
using namespace utils::dependencies;
namespace numerical = solver::preconditioner;
const auto inverse = numerical::make_preconditioner_computations(recipe, layout);
using Graph = decltype(std::apply([](auto... computations) {
return make_computation_graph(std::move(computations)...);
}, inverse));
using PhysicalKeys = typename numerical::computations::detail::PhysicalSources<typename Graph::Inputs>::Type;
const auto shared_layout = std::apply([&](auto computation) {
return detail::missing_computation(std::move(computation), existing);
}, computations::make_solver_layout_computations(layout));
const auto prepared = make_computation<PreparedSystem, InputList<ResultKey, numerical::keys::Preconditioner>>(
[](const auto& system, const numerical::backend::mfem::Inverse& inverse) {
using System = std::remove_const_t<typename std::remove_cvref_t<decltype(system)>::element_type>;
return std::make_shared<const backend::mfem::PreconditionedSystem<System>>(system, inverse);
});
return std::tuple_cat(inverse, shared_layout,
computations::make_physical_block_computations(layout, PhysicalKeys{}), std::tuple{prepared});
}
}

View File

@@ -0,0 +1,15 @@
#pragma once
#include <expected>
#include "serif/backend/mfem/operators/linearized_block.hpp"
#include "serif/backend/mfem/operators/rejection.hpp"
namespace serif::model::backend::mfem {
/**
* @brief Retain one scalar equation's coefficient derivatives instead of repeating its integration.
* @note Applying the exact transpose to one recovers the complete row without probing state columns.
* The returned ordinary matrix owns its coefficients independently of the source action.
* Numerical overflow rejects this preparation; malformed operator dimensions remain caller errors.
*/
[[nodiscard]] std::expected<operators::backend::mfem::LinearizedBlock, operators::backend::mfem::PreparationRejection> cache_scalar_row(const operators::backend::mfem::LinearizedBlock& source);
}

View File

@@ -0,0 +1,86 @@
#pragma once
#include <array>
#include <limits>
#include "serif/model/plan.hpp"
#include "serif/model/state.hpp"
#include "serif/backend/mfem/discretization/term_coefficients.hpp"
namespace serif::model::backend::mfem::detail {
/** @brief Recognize a field view by its physical identity and backend coefficient representation. */
template <typename Inputs, typename Term>
concept CoefficientInput = requires(const Inputs& inputs) {
typename Inputs::Keys;
{ inputs.template get<Term>().values } -> std::convertible_to<const ::mfem::Vector&>;
requires std::same_as<typename std::remove_cvref_t<decltype(inputs.template get<Term>().binding)>::Term, Term>;
};
/** @brief Check required fields before entering evaluation, including during compile-time API detection. */
template <typename Inputs, typename... Terms>
[[nodiscard]] consteval bool coefficient_inputs(utils::dependencies::KeyList<Terms...>) {
return (CoefficientInput<Inputs, Terms> && ...);
}
/** @brief Prescribed views must supply every unsolved input and cannot duplicate a solved coordinate. */
template <typename Plan, typename Inputs>
concept PrescribedInputsFor = utils::dependencies::IsDependencyContext<Inputs> &&
(utils::dependencies::key_count_v<utils::dependencies::CombinedKeyList<typename Plan::Values, typename Inputs::Keys>> ==
utils::dependencies::key_count_v<typename Plan::Values> + utils::dependencies::key_count_v<typename Inputs::Keys>) &&
coefficient_inputs<Inputs>(typename model::detail::RemoveKeys<typename Plan::Coefficients, typename Plan::Values>::Type{});
template <typename Discretization, typename Layout, typename Prescribed, typename Fields>
class StateCoefficients;
/**
* @brief Borrow an owning state's fields for one synchronous equation evaluation.
* @note MFEM requires mutable pointers to construct its vector views, but these wrappers are only
* exposed through const coefficient views. Keep this adapter alive until graph evaluation returns.
* Recreate it for each call so copies and moves of StellarState never leave persistent self-references.
*/
template <typename Discretization, typename Layout, typename Prescribed, typename... Fields>
class StateCoefficients<Discretization, Layout, Prescribed, utils::dependencies::KeyList<Fields...>> final {
public:
using Keys = utils::dependencies::CombinedKeyList<utils::dependencies::KeyList<Fields...>, typename Prescribed::Keys>;
static_assert(utils::dependencies::key_count_v<Keys> == sizeof...(Fields) + utils::dependencies::key_count_v<typename Prescribed::Keys>,
"A physical field cannot be both solved and prescribed. Remove its prescribed input or its solved coordinate.");
/** @brief Create backend views without copying the state's physical coefficients. */
StateCoefficients(const Discretization& discretization, const StellarState<Layout>& state, const Prescribed& prescribed) :
m_discretization(discretization), m_prescribed(prescribed), m_fields{field_view(state.template get<Fields>())...} {
}
/** @brief Reuse ordinary dependency contexts while the temporary MFEM wrappers remain alive. */
[[nodiscard]] auto context() const & {
using namespace utils::dependencies;
const auto fields = make_dependency_context(own<Fields>(operators::backend::mfem::bind(
m_discretization.template binding<Fields>(), m_fields[key_index<Fields, KeyList<Fields...>>()]))...);
return combine_dependency_contexts(fields, m_prescribed);
}
/** @brief A coefficient context cannot borrow a temporary set of backend wrappers. */
auto context() const && = delete;
private:
/** @brief Check MFEM's index range before constructing a read-only use of its mutable-pointer view. */
[[nodiscard]] static ::mfem::Vector field_view(std::span<const double> values) {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(values.size() <= static_cast<std::size_t>(std::numeric_limits<int>::max()),
"A stellar field exceeds MFEM's coefficient index range. Supply a representable discretization.");
return ::mfem::Vector(const_cast<double*>(values.data()), static_cast<int>(values.size()));
}
const Discretization& m_discretization;
const Prescribed& m_prescribed;
std::array<::mfem::Vector, sizeof...(Fields)> m_fields;
};
/** @brief Keep surface coordinates in the state while exposing only ordinary fields to volume equations. */
template <typename Discretization, typename Layout, typename Prescribed>
[[nodiscard]] auto state_coefficients(const Discretization& discretization, const StellarState<Layout>& state, const Prescribed& prescribed) {
using Fields = typename model::detail::RemoveKeys<typename Layout::Values,
utils::dependencies::KeyList<discretization::blocks::surface_deformation::coefficients>>::Type;
return StateCoefficients<Discretization, Layout, Prescribed, Fields>(discretization, state, prescribed);
}
}

View File

@@ -0,0 +1,40 @@
#pragma once
#include <memory>
#include <span>
#include <vector>
#include "mfem/linalg/densemat.hpp"
#include "mfem/linalg/sparsemat.hpp"
#include "serif/backend/mfem/discretization/dof_map.hpp"
#include "serif/backend/mfem/geometry/deformation/inputs.hpp"
#include "serif/geometry/deformation/reference_surface.hpp"
#include "serif/geometry/deformation/surface_embedding.hpp"
#include "serif/equations/surface_potential.hpp"
namespace serif::model::backend::mfem {
/** @brief Accepted surface moments and derivatives in existing shape and field coordinates. */
struct SurfacePotentialLinearization {
std::vector<double> residual;
std::shared_ptr<::mfem::SparseMatrix> potential;
std::shared_ptr<::mfem::SparseMatrix> shape;
std::shared_ptr<::mfem::DenseMatrix> level;
std::shared_ptr<::mfem::DenseMatrix> angular_velocity;
};
/**
* @brief Integrate the effective-potential balance using dS_reference / area_reference.
* @note All matrices own accepted values. The reference measure and radial directions are
* fixed; only the centrifugal potential depends explicitly on material surface motion.
* Displacement contains three Cartesian components per selected reference node; shape rows
* and columns use the embedding's independent radial lengths or harmonic coefficients.
*/
[[nodiscard]] SurfacePotentialLinearization assemble_surface_potential(
const equations::SurfacePotentialBalance& equation,
const geometry::deformation::backend::mfem::SurfaceDiscretization& discretization,
const geometry::deformation::ReferenceSurface& reference,
const geometry::deformation::SurfaceEmbedding& embedding,
std::span<const double> displacement,
const discretization::backend::mfem::DofMap& potential_binding,
const ::mfem::Vector& potential, double level, double angular_velocity);
}

View File

@@ -0,0 +1,77 @@
#pragma once
#include <memory>
#include <optional>
#include <span>
#include <vector>
#include "serif/backend/mfem/operators/actions.hpp"
namespace serif::model::backend::mfem {
using operators::backend::mfem::LinearizedBlock;
/** @brief One contiguous range of equation or state coordinates in an evaluated system. */
struct CoordinateRange {
int offset;
int size;
};
/** @brief One evaluated equation contribution in the independently ordered residual coordinates. */
struct ResidualContribution {
int row_offset;
std::vector<double> values;
};
/** @brief One frozen physical Jacobian coupling; overlapping contributions add. */
struct JacobianContribution {
int row_offset;
int column_offset;
LinearizedBlock block;
};
/** @brief A coupling from state coordinates into an ordered selection of replacement rows. */
struct ColumnContribution {
int column_offset;
LinearizedBlock block;
};
/** @brief Replace complete equations after all ordinary and generated contributions have been added. */
struct RowReplacement {
std::vector<int> selected_rows;
std::vector<double> residual;
std::vector<ColumnContribution> jacobian;
};
namespace detail {
struct SystemData;
}
/** @brief Own one evaluated residual and Jacobian, retaining the discretization owner used by every child action. */
class PhysicalSystem final {
public:
/**
* @brief Validate coordinate bounds and row ownership without assuming a square system.
* @note Optional complete actions must represent the same ordinary Jacobian as the blocks.
* They accelerate whole-system products; block selection and assembly keep using the physical blocks.
*/
PhysicalSystem(int residual_size, int state_size, std::vector<ResidualContribution> residuals, std::vector<JacobianContribution> jacobian, std::vector<RowReplacement> replacements = {}, std::shared_ptr<const void> owner = {}, std::optional<std::vector<JacobianContribution>> complete_actions = {});
/** @brief Read the frozen physical residual in the declared residual order. */
[[nodiscard]] const std::vector<double>& residual() const noexcept;
/** @brief Access both directions of this frozen physical Jacobian. */
[[nodiscard]] const LinearizedBlock& jacobian() const noexcept;
/** @brief Select a physical coupling without applying unrelated equations; the result retains this snapshot and its owner. */
[[nodiscard]] LinearizedBlock block(int row_offset, int row_size, int column_offset, int column_size) const;
/** @brief Concatenate selected equation and state ranges in their supplied order, retaining this evaluated system. */
[[nodiscard]] LinearizedBlock block(std::span<const CoordinateRange> rows, std::span<const CoordinateRange> columns) const;
private:
std::shared_ptr<const detail::SystemData> m_data;
std::vector<double> m_residual;
LinearizedBlock m_jacobian;
};
/** @brief Publish an immutable evaluated system that remains independent of later graph preparation. */
[[nodiscard]] std::shared_ptr<const PhysicalSystem> make_physical_system(int residual_size, int state_size, std::vector<ResidualContribution> residuals, std::vector<JacobianContribution> jacobian, std::vector<RowReplacement> replacements = {}, std::shared_ptr<const void> owner = {}, std::optional<std::vector<JacobianContribution>> complete_actions = {});
} // namespace serif::model::backend::mfem

View File

@@ -0,0 +1,39 @@
#pragma once
#include <algorithm>
#include <array>
#include "serif/backend/mfem/operators/actions.hpp"
#include "serif/backend/mfem/operators/volume_application.hpp"
namespace serif::model::backend::mfem {
using operators::backend::mfem::LinearizedBlock;
/** @brief Copy the accepted volume residual for additive physical system assembly. */
template <operators::VolumeEquation Equation, typename Schema>
[[nodiscard]] std::vector<double> volume_residual_values(const std::shared_ptr<const operators::backend::mfem::VolumeData<Equation, Schema>>& data) {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(data != nullptr, "Model assembly requires an accepted volume equation.");
operators::backend::mfem::validate_volume_data(*data);
return std::vector<double>(data->residual.GetData(), data->residual.GetData() + data->residual.Size());
}
/** @brief Select only the displacement derivative while reusing the equation's complete geometry and transpose kernels. */
template <operators::VolumeEquation Equation, typename Schema>
[[nodiscard]] LinearizedBlock volume_geometry_block(std::shared_ptr<const operators::backend::mfem::VolumeData<Equation, Schema>> data) {
using namespace utils::errors;
namespace volume = operators::backend::mfem;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(data != nullptr, "A model geometry block requires an accepted volume equation.");
const int rows = volume::volume_height(*data);
const int columns = data->configuration->discretization.displacement_binding.size();
return operators::backend::mfem::linearized_action(rows, columns, [data, workspace = volume::Workspace{}](const ::mfem::Vector& input, ::mfem::Vector& output) mutable {
const std::array<const ::mfem::Vector*, operators::EquationPlan<Equation>::column_count> directions{};
volume::apply_volume_direction<false>(*data, directions, input, workspace, output);
}, [data, columns, workspace = volume::Workspace{}, packed = ::mfem::Vector{}](const ::mfem::Vector& input, ::mfem::Vector& output) mutable {
volume::apply_jacobian_transpose(*data, input, workspace, packed);
std::copy_n(packed.GetData() + packed.Size() - columns, columns, output.GetData());
}, [data](std::span<const ::mfem::Vector* const> inputs, std::span<::mfem::Vector* const> outputs) {
volume::apply_volume_displacements(*data, inputs, outputs);
});
}
} // namespace serif::model::backend::mfem

View File

@@ -0,0 +1,89 @@
#pragma once
#include <algorithm>
#include <array>
#include <limits>
#include <optional>
#include "serif/discretization/blocks/system_layout.hpp"
#include "serif/backend/mfem/model/system.hpp"
#include "serif/backend/mfem/operators/volume_application.hpp"
namespace serif::model::backend::mfem {
/**
* @brief Apply every solved field and geometry direction in one traversal of an accepted volume equation.
* @note The optional geometry column maps independent state coordinates into the equation's displacement
* coefficients. Prescribed fields have zero direction but remain part of the frozen physical state.
*/
template <operators::VolumeEquation Equation, typename Schema, discretization::blocks::SystemLayoutType Layout>
[[nodiscard]] LinearizedBlock volume_row_action(std::shared_ptr<const operators::backend::mfem::VolumeData<Equation, Schema>> data, const Layout& layout, std::optional<ColumnContribution> geometry = {}) {
using namespace utils::errors;
using namespace utils::dependencies;
using Kind = discretization::blocks::RepresentedValueKind;
using Plan = operators::EquationPlan<Equation>;
namespace volume = operators::backend::mfem;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT>(data != nullptr, "A complete volume action requires an accepted equation.");
const std::size_t columns = layout.template total_size<Kind::value>();
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(columns <= static_cast<std::size_t>(std::numeric_limits<int>::max()), "Complete volume action state coordinates exceed MFEM's integer range.");
std::array<int, Plan::column_count> offsets;
offsets.fill(-1);
std::array<::mfem::Vector, Plan::column_count> fields;
Plan::for_each_column([&]<typename Column>(Column, std::size_t column) {
using Field = typename Column::term_type;
fields[column].SetSize(data->bindings.columns[column]->size());
fields[column] = 0.0;
if constexpr (ContainsKey<Field, typename Layout::Values>) {
serif_ensure<SERiFErrorCode::MATRIX_SHAPE_ERROR>(layout.template size<Kind::value, Field>() == static_cast<std::size_t>(fields[column].Size()), "Solved field coordinates must match the accepted volume binding.");
offsets[column] = static_cast<int>(layout.template offset<Kind::value, Field>());
}
});
const int displacement_size = data->configuration->discretization.displacement_binding.size();
if (geometry) {
const auto& action = geometry->block.action();
serif_ensure<SERiFErrorCode::MATRIX_SHAPE_ERROR>(action->Height() == displacement_size && geometry->column_offset >= 0 && static_cast<std::size_t>(geometry->column_offset) <= columns && static_cast<std::size_t>(action->Width()) <= columns - geometry->column_offset, "The geometry column must map a valid state interval into the accepted displacement coefficients.");
}
return operators::backend::mfem::linearized_action(volume::volume_height(*data), static_cast<int>(columns),
[data, offsets, fields = std::move(fields), geometry, displacement = ::mfem::Vector(displacement_size), geometry_direction = ::mfem::Vector{}, workspace = volume::Workspace{}](const ::mfem::Vector& input, ::mfem::Vector& output) mutable {
std::array<const ::mfem::Vector*, Plan::column_count> directions;
for (std::size_t column = 0; column < fields.size(); ++column) {
if (offsets[column] >= 0) {
std::copy_n(input.GetData() + offsets[column], fields[column].Size(), fields[column].GetData());
}
directions[column] = &fields[column];
}
displacement = 0.0;
if (geometry) {
geometry_direction.SetSize(geometry->block.action()->Width());
std::copy_n(input.GetData() + geometry->column_offset, geometry_direction.Size(), geometry_direction.GetData());
geometry->block.action()->Mult(geometry_direction, displacement);
}
if (geometry) {
volume::apply_volume_direction(*data, directions, displacement, workspace, output);
} else {
volume::apply_volume_direction<true, false>(*data, directions, displacement, workspace, output);
}
},
[data, offsets, geometry, displacement_size, packed = ::mfem::Vector{}, displacement_dual = ::mfem::Vector(displacement_size), geometry_dual = ::mfem::Vector{}, workspace = volume::Workspace{}](const ::mfem::Vector& input, ::mfem::Vector& output) mutable {
volume::apply_jacobian_transpose(*data, input, workspace, packed);
int source = 0;
for (std::size_t column = 0; column < offsets.size(); ++column) {
const int size = data->bindings.columns[column]->size();
if (offsets[column] >= 0) {
for (int index = 0; index < size; ++index) {
output[offsets[column] + index] += packed[source + index];
}
}
source += size;
}
if (geometry) {
std::copy_n(packed.GetData() + source, displacement_size, displacement_dual.GetData());
geometry_dual.SetSize(geometry->block.action()->Width());
geometry_dual = 0.0;
geometry->block.action()->MultTranspose(displacement_dual, geometry_dual);
for (int index = 0; index < geometry_dual.Size(); ++index) {
output[geometry->column_offset + index] += geometry_dual[index];
}
}
});
}
} // namespace serif::model::backend::mfem

View File

@@ -0,0 +1,81 @@
#pragma once
#include <algorithm>
#include <tuple>
#include "serif/geometry/deformation/computations.hpp"
#include "serif/backend/mfem/geometry/deformation/inputs.hpp"
#include "serif/backend/mfem/geometry/deformation/displacement_mapping.hpp"
#include "serif/backend/mfem/geometry/deformation/reference_geometry.hpp"
#include "serif/backend/mfem/operators/volume_inputs.hpp"
namespace serif::geometry::deformation::backend::mfem {
namespace volume_displacement_keys {
struct CheckedBinding {
};
struct Coefficients {
};
} // namespace volume_displacement_keys
/**
* @brief Connect a full component-block deformation vector to an equation's displacement binding.
*
* Reference-dependent validation proves the two spaces assign the same meaning to each scalar coefficient.
* Subsequent computations own coefficients in the binding's global ordering and supported-DOF numbering,
* then return the existing CoefficientView into that immutable prerequisite. Cache result ownership
* keeps the vector's address stable throughout downstream evaluation and accepted-result reuse.
* MetadataKey selects an equation-scoped binding source when several equations share the graph.
* The binding itself is borrowed and must outlive this graph and its results, as for volume equations.
*/
template <typename MetadataKey = operators::backend::mfem::MeshAndCoefficientBindings, discretization::domain::schema::IsSchema Schema>
[[nodiscard]] auto make_volume_displacement_computations(const discretization::backend::mfem::TermBinding<discretization::blocks::displacement::vector, Schema>& binding) {
using namespace utils::dependencies;
using Binding = discretization::backend::mfem::TermBinding<discretization::blocks::displacement::vector, Schema>;
namespace equations_backend = operators::backend::mfem;
const auto checked_binding = make_computation<volume_displacement_keys::CheckedBinding, InputList<deformation::keys::ReferenceDiscretization, MetadataKey>>(
[&binding](const ReferenceDiscretization& reference, const auto& metadata) -> std::expected<const Binding*, DeformationFailure> {
binding.validate();
if (metadata.displacement_binding != &binding) {
return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "The deformation-to-equation connection requires the displacement binding used when constructing it."});
}
const ::mfem::FiniteElementSpace& space = binding.space();
if (space.GetVDim() != 3 || reference.displacement_space.GetVDim() != 3 || reference.scalar_space.GetVDim() != 1) {
return std::unexpected(DeformationFailure{.reason = DeformationFailureReason::incompatible_discretization, .message = "The deformation connection requires three-component displacements and a scalar reference companion."});
}
if (const DeformationStatus numbering = validate_companion_numbering(reference.scalar_space, space); !numbering) {
return std::unexpected(numbering.error());
}
if (const DeformationStatus numbering = validate_companion_numbering(reference.displacement_space, space); !numbering) {
return std::unexpected(numbering.error());
}
return &binding;
});
const auto coefficients = make_computation<volume_displacement_keys::Coefficients, InputList<deformation::keys::VolumeDisplacement, volume_displacement_keys::CheckedBinding>>(
[](const std::vector<double>& displacement, const Binding* binding) -> std::expected<::mfem::Vector, DeformationFailure> {
const ::mfem::FiniteElementSpace& space = binding->space();
if (const auto validation = validate_deformation_values(displacement, static_cast<std::size_t>(binding->full_size())); !validation) {
return std::unexpected(validation.error());
}
::mfem::Vector values(binding->size());
if (space.GetOrdering() == ::mfem::Ordering::byNODES && binding->size() == binding->full_size()) {
std::ranges::copy(displacement, values.GetData());
return values;
}
for_each_displacement_coefficient(*binding, [&](const int row, const int column) {
values[row] = displacement[static_cast<std::size_t>(column)];
});
return values;
});
const auto view = make_computation<equations_backend::DisplacementCoefficients, InputList<volume_displacement_keys::Coefficients, volume_displacement_keys::CheckedBinding>>(
[](const ::mfem::Vector& values, const Binding* binding) -> equations_backend::CoefficientView<Binding> {
return equations_backend::bind(*binding, values);
});
return std::tuple{checked_binding, coefficients, view};
}
/** @brief A graph cannot retain a displacement binding supplied as a temporary. */
template <typename MetadataKey = operators::backend::mfem::MeshAndCoefficientBindings, discretization::domain::schema::IsSchema Schema>
void make_volume_displacement_computations(const discretization::backend::mfem::TermBinding<discretization::blocks::displacement::vector, Schema>&&) = delete;
} // namespace serif::geometry::deformation::backend::mfem

View File

@@ -0,0 +1,34 @@
#pragma once
#include <functional>
#include <span>
#include <vector>
#include "serif/backend/mfem/operators/linearized_block.hpp"
namespace serif::operators::backend::mfem {
using MatrixAction = std::function<void(const ::mfem::Vector&, ::mfem::Vector&)>;
/**
* @brief Apply the forward action to several directions at once (zeroed outputs, as MatrixAction receives).
* @note Each output must equal what MatrixAction produces for its input; batching only shares traversals.
*/
using MatrixArrayAction = std::function<void(std::span<const ::mfem::Vector* const>, std::span<::mfem::Vector* const>)>;
/** @brief Wrap owned callbacks with checked dimensions, finite results, and overlap-safe application. */
[[nodiscard]] LinearizedBlock linearized_action(int rows, int columns, MatrixAction action, MatrixAction transpose);
/** @brief As above, with a batched forward action used by the operator's ArrayMult. */
[[nodiscard]] LinearizedBlock linearized_action(int rows, int columns, MatrixAction action, MatrixAction transpose, MatrixArrayAction batch);
/** @brief Apply an operator to several directions; ArrayMult for SERiF actions, otherwise one Mult per direction. */
void apply_many(const ::mfem::Operator& action, std::span<const ::mfem::Vector* const> inputs, std::span<::mfem::Vector* const> outputs);
/** @brief Transpose a linearized block without discarding efficient assembly and diagonal operations. */
[[nodiscard]] LinearizedBlock transpose_block(LinearizedBlock block);
/** @brief Compose linearized blocks in their written mathematical order. */
[[nodiscard]] LinearizedBlock compose_blocks(LinearizedBlock left, LinearizedBlock right);
/** @brief Sum compatible linearized blocks while retaining their native diagonal and sparse assembly operations. */
[[nodiscard]] LinearizedBlock sum_blocks(int rows, int columns, std::vector<LinearizedBlock> blocks);
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,47 @@
#pragma once
#include <array>
#include <memory>
#include <vector>
#include "mfem/fem/intrules.hpp"
#include "serif/backend/mfem/mapping/volume_cache.hpp"
#include "serif/backend/mfem/mapping/data.hpp"
#include "serif/backend/mfem/discretization/basis_evaluation.hpp"
namespace serif::operators::backend::mfem {
using discretization::backend::mfem::BasisEvaluation;
/// Fixed-mesh basis samples; independent of the current coefficient values and displacement.
template <std::size_t OperandCount>
struct ElementBasis final {
int element{-1};
const ::mfem::IntegrationRule* rule{nullptr};
BasisEvaluation reference_test_basis;
std::array<BasisEvaluation, OperandCount> reference_operand_basis;
BasisEvaluation displacement_basis;
std::vector<mapping::DMFixedMeshGeometry> reference_geometry;
};
/// Physical mapping and mapped bases for one frozen displacement.
template <std::size_t OperandCount>
struct ElementGeometry final {
BasisEvaluation test_basis;
std::array<BasisEvaluation, OperandCount> operand_basis;
::mfem::Vector displacement;
::mfem::Vector compactification;
mapping::VolumeMappingCache mapping;
};
/// Weighted equation data retains the exact basis, geometry, and samples used to compute it.
template <std::size_t OperandCount>
struct ElementData final {
int element{-1};
std::shared_ptr<const ElementBasis<OperandCount>> basis;
std::shared_ptr<const ElementGeometry<OperandCount>> geometry;
std::array<std::shared_ptr<const ::mfem::Vector>, OperandCount> reference_operand_values;
::mfem::Vector weighted_residual;
::mfem::DenseMatrix weighted_field_derivatives;
::mfem::DenseMatrix weighted_position_derivative;
double validation_integral{0.0};
};
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,163 @@
#pragma once
#include <array>
#include <cmath>
#include <span>
#include "serif/utils/error/nonfinite.hpp"
#include "serif/backend/mfem/operators/operand_evaluation.hpp"
#include "serif/backend/mfem/operators/element_data.hpp"
#include "serif/backend/mfem/operators/workspace.hpp"
#include "serif/operators/plan.hpp"
namespace serif::operators::backend::mfem {
/// Integrate the cached scalar/vector physics against its actual test operation.
template <typename ContributionT, std::size_t Count>
void element_residual(const ElementData<Count>& data, ::mfem::Vector& result, BasisWorkspace& workspace) {
OperandEvaluator<equations::TestOperandOf<ContributionT>>::integrate(data.geometry->test_basis, data.weighted_residual, result, workspace);
}
/**
* @brief Finish an element action from its field part (workspace.quadrature_values) and, with Geometry, the
* displacement samples (workspace.displacement_samples): contract the geometry derivatives and integrate.
*/
template <VolumeEquation EquationT, typename ContributionT, bool Fields, bool Geometry>
void finish_element_action(
const ElementData<EquationPlan<EquationT>::operand_count>& data,
const ::mfem::DenseMatrix& geometry_derivatives,
Workspace& workspace,
::mfem::Vector& result
) {
using namespace utils::errors;
using Test = OperandEvaluator<equations::TestOperandOf<ContributionT>>;
const int points = data.basis->rule->GetNPoints();
if constexpr (Geometry) {
workspace.reference_test_values.SetSize(points * Test::reference_components);
// MFEM stores each derivative column contiguously; keep the same twelve-term sum.
const int derivative_stride = geometry_derivatives.Height();
const double* derivatives = geometry_derivatives.GetData();
const double* displacement = workspace.displacement_samples.GetData();
double* values = workspace.reference_test_values.GetData();
for (int point = 0; point < points; ++point) {
for (int output = 0; output < Test::reference_components; ++output) {
const int row = point * Test::reference_components + output;
double value = 0.0;
for (int input = 0; input < 12; ++input) {
value += derivatives[input * derivative_stride + row] * displacement[12 * point + input];
}
values[row] = value;
}
}
}
if constexpr (Geometry && !Test::geometry_dependent) {
// Both terms have the same test representation, so one integration is sufficient.
workspace.quadrature_values += workspace.reference_test_values;
}
for (int entry = 0; entry < workspace.quadrature_values.Size(); ++entry) {
serif_ensure<SERiFErrorCode::NON_FINITE_VALUE_ERROR, NonFiniteValue>(
std::isfinite(workspace.quadrature_values[entry]), "Non-finite equation Jacobian action."
);
}
if constexpr (Fields || !Test::geometry_dependent) {
Test::integrate(data.geometry->test_basis, workspace.quadrature_values, result, workspace.basis);
} else {
result.SetSize(data.basis->reference_test_basis.width());
result = 0.0;
}
if constexpr (Geometry && Test::geometry_dependent) {
for (int entry = 0; entry < workspace.reference_test_values.Size(); ++entry) {
serif_ensure<SERiFErrorCode::NON_FINITE_VALUE_ERROR, NonFiniteValue>(
std::isfinite(workspace.reference_test_values[entry]), "Non-finite equation geometry action."
);
}
data.basis->reference_test_basis.integrate(workspace.reference_test_values, workspace.test_variation_result, workspace.basis);
result += workspace.test_variation_result;
}
}
/**
* @brief Apply the accepted field derivatives and local displacement derivatives.
* @note Geometry derivatives are graph results, expressed against the fixed-mesh test basis.
* The two compile-time switches exclude structurally absent directions without sampling zero fields.
*/
template <VolumeEquation EquationT, typename ContributionT, bool Fields = true, bool Geometry = true>
void element_jacobian_action(
const ElementData<EquationPlan<EquationT>::operand_count>& data,
const ::mfem::DenseMatrix& geometry_derivatives,
const std::array<discretization::backend::mfem::DofMap::Elements, EquationPlan<EquationT>::column_count>& columns,
const std::array<const ::mfem::Vector*, EquationPlan<EquationT>::column_count>& directions,
const discretization::backend::mfem::DofMap::Elements& displacement_binding,
const ::mfem::Vector& displacement_direction,
Workspace& workspace,
::mfem::Vector& result
) {
using Plan = EquationPlan<EquationT>;
using Test = OperandEvaluator<equations::TestOperandOf<ContributionT>>;
const int points = data.basis->rule->GetNPoints();
workspace.quadrature_values.SetSize(points * Test::components);
workspace.quadrature_values = 0.0;
if constexpr (Fields) {
Plan::template for_each_input<ContributionT>([&]<typename OperandT>(OperandT, std::size_t index) {
using Evaluator = OperandEvaluator<OperandT>;
constexpr auto column = Plan::template column_index<OperandT>();
columns[column].element_values(data.element, *directions[column], workspace.element_values);
Evaluator::evaluate(data.geometry->operand_basis[index], workspace.element_values, workspace.operand_values[index], workspace.basis);
for (int point = 0; point < points; ++point) {
for (int output = 0; output < Test::components; ++output) {
const int row = point * Test::components + output;
for (int input = 0; input < Evaluator::components; ++input) {
workspace.quadrature_values[row] += data.weighted_field_derivatives(row, Plan::template component_offset<OperandT>() + input)
* workspace.operand_values[index][point * Evaluator::components + input];
}
}
}
});
}
if constexpr (Geometry) {
displacement_binding.element_values(data.element, displacement_direction, workspace.displacement_direction);
data.basis->displacement_basis.evaluate(workspace.displacement_direction, workspace.displacement_samples, workspace.basis);
}
finish_element_action<EquationT, ContributionT, Fields, Geometry>(data, geometry_derivatives, workspace, result);
}
/**
* @brief element_jacobian_action<false, true> for several displacement directions of one element.
* @note The displacement samples of all directions come from one lane-batched basis evaluation (each lane equals
* the single evaluation); every direction is then finished exactly as element_jacobian_action finishes it.
*/
template <VolumeEquation EquationT, typename ContributionT>
void element_geometry_actions(
const ElementData<EquationPlan<EquationT>::operand_count>& data,
const ::mfem::DenseMatrix& geometry_derivatives,
const discretization::backend::mfem::DofMap::Elements& displacement_binding,
std::span<const ::mfem::Vector* const> displacement_directions,
Workspace& workspace,
std::span<::mfem::Vector* const> results
) {
using Test = OperandEvaluator<equations::TestOperandOf<ContributionT>>;
const int lanes = static_cast<int>(displacement_directions.size());
const int points = data.basis->rule->GetNPoints();
for (int lane = 0; lane < lanes; ++lane) {
displacement_binding.element_values(data.element, *displacement_directions[static_cast<std::size_t>(lane)], workspace.displacement_direction);
const int width = workspace.displacement_direction.Size();
workspace.lane_coefficients.SetSize(width * lanes);
for (int i = 0; i < width; ++i) {
workspace.lane_coefficients[i * lanes + lane] = workspace.displacement_direction[i];
}
}
data.basis->displacement_basis.evaluate_many(workspace.lane_coefficients, workspace.lane_samples, lanes, workspace.basis);
const int samples = workspace.lane_samples.Size() / lanes;
for (int lane = 0; lane < lanes; ++lane) {
workspace.displacement_samples.SetSize(samples);
for (int i = 0; i < samples; ++i) {
workspace.displacement_samples[i] = workspace.lane_samples[i * lanes + lane];
}
workspace.quadrature_values.SetSize(points * Test::components);
workspace.quadrature_values = 0.0;
finish_element_action<EquationT, ContributionT, false, true>(data, geometry_derivatives, workspace, *results[static_cast<std::size_t>(lane)]);
}
}
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,327 @@
#pragma once
#include <array>
#include <cmath>
#include <expected>
#include <memory>
#include <utility>
#include "serif/backend/mfem/mapping/domain_mapper.hpp"
#include "serif/backend/mfem/operators/element_data.hpp"
#include "serif/backend/mfem/operators/operand_evaluation.hpp"
#include "serif/backend/mfem/operators/quadrature.hpp"
#include "serif/backend/mfem/operators/rejection.hpp"
#include "serif/backend/mfem/discretization/term_coefficients.hpp"
#include "serif/backend/mfem/operators/workspace.hpp"
#include "serif/backend/mfem/shared/parallel.hpp"
#include "serif/utils/misc/compensated_sum.hpp"
namespace serif::operators::backend::mfem {
/// Sample the contribution's quadrature rule and bases on the fixed computational mesh.
template <VolumeEquation EquationT, typename ContributionT>
[[nodiscard]] std::expected<ElementBasis<EquationPlan<EquationT>::operand_count>, PreparationRejection> sample_element_basis(
const ContributionT& equation,
int element,
const discretization::backend::mfem::DofMap& row,
const discretization::backend::mfem::DofMap& displacement_binding,
const std::array<const discretization::backend::mfem::DofMap*, EquationPlan<EquationT>::column_count>& columns,
const mapping::DomainMapper& mapper,
const discretization::quadrature::Policy& policy,
TensorBasisCache& basis_cache,
ContractionMode contraction_mode
) {
using Plan = EquationPlan<EquationT>;
using Test = OperandEvaluator<equations::TestOperandOf<ContributionT>>;
auto& transformation = *displacement_binding.space().GetElementTransformation(element);
const auto& displacement_element = *displacement_binding.space().GetFE(element);
const auto mapping_kind = mapper.is_compactified_element(transformation) ? discretization::quadrature::MappingKind::kelvin : discretization::quadrature::MappingKind::general;
ElementBasis<Plan::operand_count> basis{
.element = element
};
int reconstruction_order = 0;
const auto include_reconstruction = [&](const auto& binding) {
if (binding.reconstruction_element(element) != nullptr) {
reconstruction_order = std::max(reconstruction_order, binding.reconstruction()->integration_order_increment());
}
};
include_reconstruction(row);
Plan::template for_each_input<ContributionT>([&]<typename OperandT>(OperandT, std::size_t) {
include_reconstruction(*columns[Plan::template column_index<OperandT>()]);
});
basis.rule = &equation_rule<EquationT>(equation, policy, transformation, displacement_element, mapping_kind, reconstruction_order);
basis.reference_geometry.reserve(basis.rule->GetNPoints());
for (int point = 0; point < basis.rule->GetNPoints(); ++point) {
auto geometry = mapping::sample_fixed_mesh_geometry(transformation, basis.rule->IntPoint(point));
if (!geometry) {
return std::unexpected(PreparationRejection{
PreparationRejectionReason::MAPPING, geometry.error(), element, point, "Invalid reference mesh geometry."
});
}
basis.reference_geometry.push_back(std::move(*geometry));
}
BasisWorkspace workspace;
Test::sample_basis(row, element, *basis.rule, basis.reference_test_basis, basis_cache, contraction_mode, transformation, workspace);
basis.displacement_basis.sample(
displacement_element, *basis.rule, ReferenceOperation::value_and_gradient, 3, basis_cache, contraction_mode
);
Plan::template for_each_input<ContributionT>([&]<typename OperandT>(OperandT, std::size_t index) {
static_assert(EvaluableOperand<OperandT>, "The MFEM operator has no evaluator for this operand.");
const auto& binding = *columns[Plan::template column_index<OperandT>()];
OperandEvaluator<OperandT>::sample_basis(
binding, element, *basis.rule, basis.reference_operand_basis[index], basis_cache, contraction_mode, transformation, workspace
);
});
return basis;
}
/// Contract one operand's fixed-mesh basis with its current coefficients.
template <VolumeEquation EquationT, EvaluableOperand OperandT, typename BindingT>
requires std::same_as<typename BindingT::Term, typename OperandT::term_type>
[[nodiscard]] ::mfem::Vector sample_element_operand(
const ElementBasis<EquationPlan<EquationT>::operand_count>& basis,
const CoefficientView<BindingT>& input,
const discretization::backend::mfem::DofMap::Elements& elements,
Workspace& workspace
) {
using Plan = EquationPlan<EquationT>;
constexpr std::size_t index = equations::OperandValues<typename Plan::Operands>::template index<OperandT>();
::mfem::Vector values;
elements.element_values(basis.element, input.values, workspace.element_values);
OperandEvaluator<OperandT>::evaluate(basis.reference_operand_basis[index], workspace.element_values, values, workspace.basis);
return values;
}
/// Freeze displacement and compactification, then map quadrature points and bases to physical space.
template <VolumeEquation EquationT, typename ContributionT>
[[nodiscard]] std::expected<ElementGeometry<EquationPlan<EquationT>::operand_count>, PreparationRejection> map_element_geometry(
const ElementBasis<EquationPlan<EquationT>::operand_count>& basis,
const discretization::backend::mfem::DofMap& displacement_binding,
const discretization::backend::mfem::DofMap::Elements& displacement_elements,
const ::mfem::Vector& displacement,
const MappingCoefficients& compactification,
const mapping::DomainMapper& mapper,
Workspace& workspace
) {
using Plan = EquationPlan<EquationT>;
using Test = OperandEvaluator<equations::TestOperandOf<ContributionT>>;
ElementGeometry<Plan::operand_count> geometry;
if constexpr (Test::geometry_dependent) {
geometry.test_basis.reset_from(basis.reference_test_basis, Test::components);
} else {
geometry.test_basis = basis.reference_test_basis;
}
Plan::template for_each_input<ContributionT>([&]<typename OperandT>(OperandT, std::size_t index) {
using Evaluator = OperandEvaluator<OperandT>;
if constexpr (Evaluator::geometry_dependent) {
geometry.operand_basis[index].reset_from(basis.reference_operand_basis[index], Evaluator::components);
} else {
geometry.operand_basis[index] = basis.reference_operand_basis[index];
}
});
displacement_elements.element_values(basis.element, displacement, geometry.displacement);
::mfem::Array<int> compactification_dofs;
// A local DofTransformation keeps concurrent element loops off the space's shared one.
::mfem::DofTransformation compactification_transform;
compactification.binding.GetElementVDofs(basis.element, compactification_dofs, compactification_transform);
compactification.values.GetSubVector(compactification_dofs, geometry.compactification);
if (compactification_transform.GetDofTransformation() != nullptr) {
compactification_transform.InvTransformPrimal(geometry.compactification);
}
basis.displacement_basis.evaluate(geometry.displacement, workspace.displacement_samples, workspace.basis);
const auto& displacement_element = *displacement_binding.space().GetFE(basis.element);
const auto displacement_data = mapping::element_displacement_data_from_element_vdofs(displacement_element, geometry.displacement);
const mapping::ElementCompactificationData compactification_data(
*compactification.binding.GetFE(basis.element), geometry.compactification
);
const mapping::ElementMappingData mapping_data{displacement_data, compactification_data};
::mfem::IsoparametricTransformation transformation;
::serif::utils::parallel::element_transformation(*displacement_binding.space().GetMesh(), basis.element, transformation);
const int point_count = basis.rule->GetNPoints();
geometry.mapping.set_size(point_count, 3);
for (int point = 0; point < point_count; ++point) {
for (int axis = 0; axis < 3; ++axis) {
workspace.displacement_value[axis] = workspace.displacement_samples[12 * point + 4 * axis];
for (int derivative = 0; derivative < 3; ++derivative) {
workspace.displacement_gradient(axis, derivative) = workspace.displacement_samples[12 * point + 4 * axis + derivative + 1];
}
}
const mapping::DMReferenceDisplacementSample sample{workspace.displacement_value, workspace.displacement_gradient};
const auto status = mapper.evaluate_volume(
mapping_data, transformation, basis.rule->IntPoint(point),
workspace.mapping_workspace, workspace.mapping_context, &sample, &basis.reference_geometry[point]
);
if (status != mapping::MappingStatus::VALID) {
return std::unexpected(PreparationRejection{
PreparationRejectionReason::MAPPING, status, basis.element, point, "Invalid equation geometry."
});
}
geometry.mapping.store(point, workspace.mapping_context);
const auto& context = workspace.mapping_context.mapping;
if constexpr (Test::geometry_dependent) {
Test::map_basis_point(point, context, basis.reference_test_basis, geometry.test_basis, workspace.basis);
}
Plan::template for_each_input<ContributionT>([&]<typename OperandT>(OperandT, std::size_t index) {
if constexpr (OperandEvaluator<OperandT>::geometry_dependent) {
OperandEvaluator<OperandT>::map_basis_point(point, context, basis.reference_operand_basis[index], geometry.operand_basis[index], workspace.basis);
}
});
}
return geometry;
}
/// Apply an operand's physical transformation to its cached fixed-mesh samples.
template <EvaluableOperand OperandT, std::size_t Count>
[[nodiscard]] ::mfem::Vector map_element_operand(
const ::mfem::Vector& reference,
const ElementGeometry<Count>& geometry,
Workspace& workspace
) {
using Evaluator = OperandEvaluator<OperandT>;
const int point_count = geometry.mapping.get_point_count();
::mfem::Vector values(point_count * Evaluator::components);
for (int point = 0; point < point_count; ++point) {
geometry.mapping.load(point, workspace.mapping_context);
const auto value = Evaluator::physical_value(
Evaluator::reference_value(reference, point), workspace.mapping_context.mapping
);
for (int component = 0; component < Evaluator::components; ++component) {
values[point * Evaluator::components + component] = discretization::forms::component(value, component);
}
}
return values;
}
/// Pack the physical samples requested by this contribution at one quadrature point.
template <VolumeEquation EquationT, typename ContributionT>
[[nodiscard]] equations::EquationPoint<typename ContributionT::Operands> element_equation_point(
const std::array<std::shared_ptr<const ::mfem::Vector>, EquationPlan<EquationT>::operand_count>& physical_values,
const mapping::MappingPointContext& context,
int point
) {
using Plan = EquationPlan<EquationT>;
equations::EquationPoint<typename ContributionT::Operands> values;
Plan::template for_each_input<ContributionT>([&]<typename OperandT>(OperandT, std::size_t index) {
using Evaluator = OperandEvaluator<OperandT>;
auto& value = values.fields.template get<OperandT>();
for (int component = 0; component < Evaluator::components; ++component) {
discretization::forms::component(value, component) = (*physical_values[index])[point * Evaluator::components + component];
}
});
for (int axis = 0; axis < 3; ++axis) {
values.position[axis] = context.physical_position[axis];
}
return values;
}
/// Weight a point's residual and derivatives; reject any non-finite stored component.
template <VolumeEquation EquationT, typename ContributionT>
[[nodiscard]] bool store_element_linearization(
const equations::PointLinearization<typename ContributionT::Operands, typename OperandEvaluator<equations::TestOperandOf<ContributionT>>::Value>& physics,
int point,
double weight,
ElementData<EquationPlan<EquationT>::operand_count>& data
) {
using Plan = EquationPlan<EquationT>;
using Test = OperandEvaluator<equations::TestOperandOf<ContributionT>>;
using Result = typename Test::Value;
bool finite = true;
for (int output = 0; output < Test::components; ++output) {
const int row_index = point * Test::components + output;
data.weighted_residual[row_index] = weight * discretization::forms::component(physics.residual, output);
finite = finite && std::isfinite(data.weighted_residual[row_index]);
Plan::template for_each_input<ContributionT>([&]<typename OperandT>(OperandT, std::size_t) {
using Input = typename OperandEvaluator<OperandT>::Value;
for (int input = 0; input < OperandEvaluator<OperandT>::components; ++input) {
auto& derivative = data.weighted_field_derivatives(row_index, Plan::template component_offset<OperandT>() + input);
derivative = weight * discretization::forms::derivative_component<Result, Input>(
physics.field_derivatives.template get<OperandT>(), output, input
);
finite = finite && std::isfinite(derivative);
}
});
for (int axis = 0; axis < 3; ++axis) {
auto& derivative = data.weighted_position_derivative(row_index, axis);
derivative = weight * discretization::forms::derivative_component<Result, discretization::forms::VectorValue>(
physics.position_derivative, output, axis
);
finite = finite && std::isfinite(derivative);
}
}
return finite;
}
/// Evaluate and weight the equation while retaining its immutable preparation inputs.
template <VolumeEquation EquationT, typename ContributionT>
[[nodiscard]] std::expected<ElementData<EquationPlan<EquationT>::operand_count>, PreparationRejection> evaluate_element_equation(
const ContributionT& equation,
double coefficient,
std::shared_ptr<const ElementBasis<EquationPlan<EquationT>::operand_count>> basis,
std::shared_ptr<const ElementGeometry<EquationPlan<EquationT>::operand_count>> geometry,
std::array<std::shared_ptr<const ::mfem::Vector>, EquationPlan<EquationT>::operand_count> reference_values,
const std::array<std::shared_ptr<const ::mfem::Vector>, EquationPlan<EquationT>::operand_count>& physical_values,
Workspace& workspace
) {
using Plan = EquationPlan<EquationT>;
using Test = OperandEvaluator<equations::TestOperandOf<ContributionT>>;
using Result = typename Test::Value;
ElementData<Plan::operand_count> data{
.element = basis->element,
.basis = std::move(basis),
.geometry = std::move(geometry),
.reference_operand_values = std::move(reference_values)
};
const int point_count = data.basis->rule->GetNPoints();
data.weighted_residual.SetSize(point_count * Test::components);
data.weighted_field_derivatives.SetSize(point_count * Test::components, Plan::component_count);
data.weighted_field_derivatives = 0.0;
data.weighted_position_derivative.SetSize(point_count * Test::components, 3);
utils::CompensatedSum element_validation;
for (int point = 0; point < point_count; ++point) {
data.geometry->mapping.load(point, workspace.mapping_context);
const auto values = element_equation_point<EquationT, ContributionT>(
physical_values, workspace.mapping_context.mapping, point
);
equations::PointLinearization<typename ContributionT::Operands, Result> physics;
try {
physics = equation.evaluate(values);
if constexpr (requires { equation.validate_integral(0.0); }) {
// Physical admissibility is independent of equation arithmetic coefficients.
element_validation.add(
workspace.mapping_context.quadrature.physical_integration_weight * equation.validation_integrand(values, physics)
);
}
} catch (const std::domain_error& error) {
return std::unexpected(PreparationRejection{
PreparationRejectionReason::EQUATION, mapping::MappingStatus::VALID, data.element, point, error.what()
});
}
// Geometry variations still need the unscaled physical integration weight.
const double weight = coefficient * workspace.mapping_context.quadrature.physical_integration_weight;
if (!store_element_linearization<EquationT, ContributionT>(physics, point, weight, data)) {
return std::unexpected(PreparationRejection{
PreparationRejectionReason::NON_FINITE, mapping::MappingStatus::VALID,
data.element, point, "Non-finite equation integrand or derivative."
});
}
}
data.validation_integral = element_validation.value();
return data;
}
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,70 @@
#pragma once
#include "serif/backend/mfem/operators/element_evaluation.hpp"
namespace serif::operators::backend::mfem {
/** @brief Transpose the same accepted field and geometry contractions used by the forward action. */
template <VolumeEquation EquationT, typename ContributionT>
void element_jacobian_transpose(
const ElementData<EquationPlan<EquationT>::operand_count>& data,
const ::mfem::DenseMatrix& geometry_derivatives,
const discretization::backend::mfem::DofMap::Elements& row,
const ::mfem::Vector& residual_dual,
Workspace& workspace,
std::array<::mfem::Vector, EquationPlan<EquationT>::column_count>& column_duals,
::mfem::Vector& displacement_dual
) {
using namespace utils::errors;
using Plan = EquationPlan<EquationT>;
using Test = OperandEvaluator<equations::TestOperandOf<ContributionT>>;
for (::mfem::Vector& column : column_duals) {
column.SetSize(0);
}
row.element_values(data.element, residual_dual, workspace.element_values);
Test::evaluate(data.geometry->test_basis, workspace.element_values, workspace.test_values, workspace.basis);
data.basis->reference_test_basis.evaluate(workspace.element_values, workspace.reference_test_values, workspace.basis);
const int points = data.basis->rule->GetNPoints();
Plan::template for_each_input<ContributionT>([&]<typename OperandT>(OperandT, std::size_t index) {
using Evaluator = OperandEvaluator<OperandT>;
workspace.operand_dual.SetSize(points * Evaluator::components);
for (int point = 0; point < points; ++point) {
for (int input = 0; input < Evaluator::components; ++input) {
double value = 0.0;
for (int output = 0; output < Test::components; ++output) {
const int row_index = point * Test::components + output;
value += data.weighted_field_derivatives(row_index, Plan::template component_offset<OperandT>() + input) * workspace.test_values[row_index];
}
workspace.operand_dual[point * Evaluator::components + input] = value;
}
}
Evaluator::integrate(data.geometry->operand_basis[index], workspace.operand_dual, workspace.element_result, workspace.basis);
auto& column = column_duals[Plan::template column_index<OperandT>()];
if (column.Size() == 0) {
column = workspace.element_result;
} else {
column += workspace.element_result;
}
});
workspace.displacement_dual.SetSize(12 * points);
// Use the same stored columns and component order as the forward geometry action.
const int derivative_stride = geometry_derivatives.Height();
const double* derivatives = geometry_derivatives.GetData();
const double* test_values = workspace.reference_test_values.GetData();
double* displacement = workspace.displacement_dual.GetData();
for (int point = 0; point < points; ++point) {
for (int input = 0; input < 12; ++input) {
double value = 0.0;
for (int output = 0; output < Test::reference_components; ++output) {
const int row_index = point * Test::reference_components + output;
value += derivatives[input * derivative_stride + row_index] * test_values[row_index];
}
serif_ensure<SERiFErrorCode::NON_FINITE_VALUE_ERROR, NonFiniteValue>(
std::isfinite(value), "Non-finite geometry Jacobian transpose action."
);
displacement[12 * point + input] = value;
}
}
data.basis->displacement_basis.integrate(workspace.displacement_dual, displacement_dual, workspace.basis);
}
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,159 @@
#pragma once
#include <array>
#include <cmath>
#include <expected>
#include "serif/backend/mfem/operators/element_data.hpp"
#include "serif/backend/mfem/operators/operand_evaluation.hpp"
#include "serif/backend/mfem/operators/rejection.hpp"
#include "serif/backend/mfem/operators/workspace.hpp"
#include "serif/operators/plan.hpp"
#include "serif/backend/mfem/mapping/domain_mapper.hpp"
#include "serif/backend/mfem/shared/parallel.hpp"
namespace serif::operators::backend::mfem {
/** @brief Differentiate the weighted physics through position, operand mappings and the volume measure. */
template <VolumeEquation Equation, typename Contribution>
[[nodiscard]] auto geometry_integrand_variation(
const ElementData<EquationPlan<Equation>::operand_count>& data,
int point,
const mapping::VolumeMappingContext& context,
const mapping::VolumeMappingVariation& variation
) {
using Plan = EquationPlan<Equation>;
using Test = OperandEvaluator<equations::TestOperandOf<Contribution>>;
typename Test::Value result{};
for (int output = 0; output < Test::components; ++output) {
const int row = point * Test::components + output;
double value = data.weighted_residual[row] * variation.weight_variation / context.quadrature.physical_integration_weight;
for (int axis = 0; axis < 3; ++axis) {
value += data.weighted_position_derivative(row, axis) * variation.mapping.physical_position_variation[axis];
}
discretization::forms::component(result, output) = value;
}
Plan::template for_each_input<Contribution>([&]<typename Operand>(Operand, std::size_t index) {
using Evaluator = OperandEvaluator<Operand>;
if constexpr (Evaluator::geometry_dependent) {
const auto delta = Evaluator::geometry_variation(
Evaluator::reference_value(*data.reference_operand_values[index], point), context.mapping, variation.mapping
);
for (int output = 0; output < Test::components; ++output) {
for (int input = 0; input < Evaluator::components; ++input) {
discretization::forms::component(result, output) += data.weighted_field_derivatives(
point * Test::components + output, Plan::template component_offset<Operand>() + input
) * discretization::forms::component(delta, input);
}
}
}
});
return result;
}
/**
* @brief Differentiate one weighted integrand with respect to local displacement values and gradients.
* @note The graph owns this matrix. Its twelve columns are u_c and partial_{xi_k} u_c, ordered
* as the displacement basis samples. Rows are quadrature points times fixed-mesh test components.
* This includes the variation of the test map: subsequent forward and transpose actions therefore
* use the same fixed reference basis and never reevaluate the physical mapping.
* The twelve directions are exact local derivatives, independent of finite-element order; they
* are neither finite differences nor probes of the global coefficient vector.
*/
template <VolumeEquation Equation, typename Contribution>
[[nodiscard]] std::expected<::mfem::DenseMatrix, PreparationRejection> element_geometry_derivatives(
const ElementData<EquationPlan<Equation>::operand_count>& data,
const discretization::backend::mfem::DofMap& displacement_binding,
const ::mfem::FiniteElementSpace& compactification_space,
const mapping::DomainMapper& mapper,
Workspace& workspace
) {
using Test = OperandEvaluator<equations::TestOperandOf<Contribution>>;
const auto& element = *displacement_binding.space().GetFE(data.element);
workspace.element_values = data.geometry->displacement;
const auto base = mapping::element_displacement_data_from_element_vdofs(element, workspace.element_values);
const mapping::ElementCompactificationData compactification(
*compactification_space.GetFE(data.element), data.geometry->compactification
);
const mapping::ElementMappingData mapping_data{base, compactification};
::mfem::IsoparametricTransformation transformation;
::serif::utils::parallel::element_transformation(*displacement_binding.space().GetMesh(), data.element, transformation);
const int points = data.basis->rule->GetNPoints();
::mfem::DenseMatrix result(points * Test::reference_components, 12);
const mapping::DMReferenceDisplacementSample sample{workspace.displacement_value, workspace.displacement_gradient};
mapping::CompactificationPointData compactification_sample;
::mfem::DenseMatrix inverse_mesh_jacobian(3);
for (int point = 0; point < points; ++point) {
data.geometry->mapping.load(point, workspace.mapping_context);
// All twelve directions share the inverse fixed-mesh Jacobian, the compactification sample
// and the physical test values at this point.
::mfem::Mult(workspace.mapping_context.quadrature.J_element_to_physical_inverse,
workspace.mapping_context.mapping.mapping_jacobian, inverse_mesh_jacobian);
const mapping::CompactificationPointData* fixed_sample = nullptr;
if (workspace.mapping_context.mapping.compactified) {
const auto status = mapper.evaluate_compactification_coordinate(
compactification, transformation, data.basis->rule->IntPoint(point),
workspace.mapping_workspace, compactification_sample, &inverse_mesh_jacobian
);
if (status != mapping::MappingStatus::VALID) {
return std::unexpected(PreparationRejection{
.reason = PreparationRejectionReason::MAPPING, .mapping_status = status,
.element = data.element, .quadrature_point = point,
.message = "Invalid fixed compactification sample for an equation derivative."
});
}
fixed_sample = &compactification_sample;
}
std::array<typename Test::Value, Test::reference_components> tests;
for (int test_component = 0; test_component < Test::reference_components; ++test_component) {
typename Test::ReferenceValue reference{};
discretization::forms::component(reference, test_component) = 1.0;
tests[test_component] = Test::physical_value(reference, workspace.mapping_context.mapping);
}
for (int component = 0; component < 12; ++component) {
workspace.displacement_value = 0.0;
workspace.displacement_gradient = 0.0;
if (component % 4 == 0) {
workspace.displacement_value[component / 4] = 1.0;
} else {
workspace.displacement_gradient(component / 4, component % 4 - 1) = 1.0;
}
const auto status = mapper.evaluate_volume_direction_variation(
mapping_data, base, transformation, data.basis->rule->IntPoint(point),
workspace.mapping_context, workspace.mapping_workspace, workspace.mapping_variation, sample, fixed_sample,
inverse_mesh_jacobian
);
if (status != mapping::MappingStatus::VALID) {
return std::unexpected(PreparationRejection{
.reason = PreparationRejectionReason::MAPPING, .mapping_status = status,
.element = data.element, .quadrature_point = point,
.message = "Invalid local displacement derivative of an equation."
});
}
const auto physics = geometry_integrand_variation<Equation, Contribution>(
data, point, workspace.mapping_context, workspace.mapping_variation
);
for (int test_component = 0; test_component < Test::reference_components; ++test_component) {
typename Test::ReferenceValue reference{};
discretization::forms::component(reference, test_component) = 1.0;
const auto& test = tests[test_component];
const auto test_variation = Test::geometry_variation(reference, workspace.mapping_context.mapping, workspace.mapping_variation.mapping);
double value = 0.0;
for (int output = 0; output < Test::components; ++output) {
value += discretization::forms::component(test, output) * discretization::forms::component(physics, output)
+ data.weighted_residual[point * Test::components + output] * discretization::forms::component(test_variation, output);
}
if (!std::isfinite(value)) {
return std::unexpected(PreparationRejection{
.reason = PreparationRejectionReason::NON_FINITE, .element = data.element,
.quadrature_point = point, .message = "Non-finite local displacement derivative of an equation."
});
}
result(point * Test::reference_components + test_component, component) = value;
}
}
}
return result;
}
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,47 @@
#pragma once
#include "mfem/linalg/operator.hpp"
#include "serif/backend/mfem/operators/volume_application.hpp"
namespace serif::operators::backend::mfem {
/// Adapt an accepted dependency cache directly to MFEM's linear operator interface.
/// The cache must outlive this adapter. Calls sharing spaces or a mapper must be serialized.
template <typename CacheT>
class JacobianOperator final : public ::mfem::Operator {
public:
/// Fix dimensions from an already accepted volume linearization.
explicit JacobianOperator(const CacheT& cache) :
Operator(volume_height(cache.template get<volume_keys::Linearization>()), volume_width(cache.template get<volume_keys::Linearization>())),
m_cache(cache) {}
JacobianOperator(CacheT&&) = delete;
JacobianOperator(const CacheT&&) = delete;
/// Apply the latest accepted Jacobian; a failed preparation makes this unavailable.
void Mult(const ::mfem::Vector& direction, ::mfem::Vector& result) const override {
const auto& data = checked_data();
apply_jacobian(data, direction, m_workspace, result);
}
/// Apply the coordinate transpose, including operand, test and geometry variations.
void MultTranspose(const ::mfem::Vector& dual, ::mfem::Vector& result) const override {
const auto& data = checked_data();
apply_jacobian_transpose(data, dual, m_workspace, result);
}
private:
/// MFEM's fixed dimensions must still match the cache's current accepted result.
[[nodiscard]] const auto& checked_data() const {
using namespace utils::errors;
const auto& data = m_cache.template get<volume_keys::Linearization>();
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR>(
volume_width(data) == Width() && volume_height(data) == Height(),
"Equation operator dimensions changed unexpectedly.");
return data;
}
const CacheT& m_cache;
mutable Workspace m_workspace;
};
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,56 @@
#pragma once
#include <functional>
#include <memory>
#include <vector>
#include "mfem/linalg/operator.hpp"
#include "mfem/linalg/sparsemat.hpp"
namespace serif::operators::backend::mfem
{
/** @brief An accepted physical block with optional efficient diagonal and sparse assembly operations. */
class LinearizedBlock final
{
public:
using Action = std::shared_ptr<const ::mfem::Operator>;
using Diagonal = std::function<std::vector<double>()>;
using Assembly = std::function<std::shared_ptr<const ::mfem::SparseMatrix>()>;
/** @brief Retain an immutable action and its matching assembly routines; captures must retain their accepted data. */
explicit LinearizedBlock(Action action, Diagonal diagonal = {}, Assembly assembly = {});
/** @brief Read the physical action belonging to this accepted block. */
[[nodiscard]] const Action& action() const noexcept;
/** @brief Whether diagonal extraction is available without global column probing. */
[[nodiscard]] bool has_diagonal() const noexcept;
/** @brief Whether this block supports explicit sparse assembly. */
[[nodiscard]] bool has_assembly() const noexcept;
/** @brief Extract the physical operator diagonal, including any installed boundary rows. */
[[nodiscard]] std::vector<double> diagonal() const;
/** @brief Assemble only this selected physical block. */
[[nodiscard]] std::shared_ptr<const ::mfem::SparseMatrix> assemble() const;
/** @brief Accepted block identity is stable across copies, but changes when a new linearization is supplied. */
[[nodiscard]] bool operator==(const LinearizedBlock&) const = default;
private:
struct Data;
std::shared_ptr<const Data> m_data;
};
/** @brief Extract a diagonal and reject a callback that violates its declared physical block dimensions. */
[[nodiscard]] std::vector<double> checked_diagonal(const LinearizedBlock& block);
/** @brief Assemble a non-null finalized sparse matrix whose dimensions match its declared physical action. */
[[nodiscard]] std::shared_ptr<const ::mfem::SparseMatrix> checked_assembly(const LinearizedBlock& block);
/** @brief Retain an already assembled physical block without copying its matrix. */
[[nodiscard]] LinearizedBlock linearized_block(std::shared_ptr<const ::mfem::SparseMatrix> matrix);
/** @brief Retain a small assembled physical block, such as a generated scalar constraint. */
[[nodiscard]] LinearizedBlock linearized_block(std::shared_ptr<const ::mfem::DenseMatrix> matrix);
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,339 @@
#pragma once
#include <array>
#include <concepts>
#include <type_traits>
#include <stdexcept>
#include <utility>
#include "mfem/fem/fe/fe_base.hpp"
#include "mfem/fem/eltrans.hpp"
#include "serif/operators/concepts.hpp"
#include "serif/backend/mfem/discretization/basis_evaluation.hpp"
#include "serif/backend/mfem/discretization/dof_map.hpp"
#include "serif/backend/mfem/mapping/transformations.hpp"
#include "serif/utils/error/errors.hpp"
#include "serif/utils/misc/compensated_sum.hpp"
namespace serif::operators::backend::mfem {
using discretization::backend::mfem::BasisEvaluation;
using discretization::backend::mfem::BasisWorkspace;
using discretization::backend::mfem::ContractionMode;
using discretization::backend::mfem::ReferenceOperation;
using discretization::backend::mfem::TensorBasisCache;
// Intentionally undefined: an operation tag alone never enables evaluation.
// New operations must supply their own implementation and linearization rules.
template <typename OperandT>
struct OperandEvaluator;
/// Scalar and vector H1 gradients share the existing value/divergence coefficient contractions.
template <MappedOperand OperandT>
struct OperandEvaluator<OperandT> final {
using Term = typename OperandT::term_type;
using Value = discretization::forms::OperandValue<OperandT>;
static constexpr int components = discretization::forms::component_count<Value>;
static constexpr bool global_scalar = discretization::blocks::GlobalScalarTerm<Term>;
static constexpr bool divergence = std::same_as<typename OperandT::operation_type, discretization::forms::Operation::Divergence>;
static constexpr bool vector_copies = [] {
if constexpr (global_scalar) {
return false;
} else {
return discretization::blocks::SpatialTermTraits<Term>::rank == 1;
}
}();
static constexpr bool h1_divergence = vector_copies && divergence;
static constexpr bool scalar_gradient = ScalarGradientOperand<OperandT>;
static constexpr bool vector_gradient = VectorGradientOperand<OperandT>;
static constexpr bool needs_vector_gradient = h1_divergence || vector_gradient;
static constexpr bool geometry_dependent = needs_vector_gradient || scalar_gradient;
// H1 divergence and gradient retain G[c,k] = partial_X_k(u_c) in component-major order.
// Only divergence contracts the mapped gradient to a scalar.
using ReferenceValue = std::conditional_t<needs_vector_gradient, std::array<double, 9>, Value>;
static constexpr int reference_components = needs_vector_gradient ? 9 : components;
/// A global coefficient has basis one; spatial operands use their native element basis.
static void sample_basis(
const discretization::backend::mfem::DofMap& binding,
int element,
const ::mfem::IntegrationRule& rule,
BasisEvaluation& basis,
TensorBasisCache& cache,
ContractionMode mode,
::mfem::ElementTransformation& transformation,
BasisWorkspace& workspace
) {
if constexpr (global_scalar) {
basis.set_constant(rule.GetNPoints());
} else {
sample_basis(*binding.space().GetFE(element), rule, basis, cache, mode, transformation, workspace, binding.reconstruction_element(element));
}
}
/// Sample a canonical tensor/dense basis, then apply MFEM's fixed-mesh mapping.
static void sample_basis(
const ::mfem::FiniteElement& element,
const ::mfem::IntegrationRule& rule,
BasisEvaluation& basis,
TensorBasisCache& cache,
ContractionMode mode,
::mfem::ElementTransformation& transformation,
BasisWorkspace& workspace,
const discretization::backend::mfem::ElementReconstruction* reconstruction = nullptr
) requires (!global_scalar) {
using namespace utils::errors;
serif_ensure<SERiFErrorCode::INVALID_ARGUMENT, std::invalid_argument>(
element.GetRangeType() == ::mfem::FiniteElement::SCALAR && element.GetMapType() == ::mfem::FiniteElement::VALUE,
"Operand sampling requires scalar VALUE bases (one or three copies)."
);
const auto operation = (needs_vector_gradient || scalar_gradient) ? ReferenceOperation::gradient : ReferenceOperation::value;
BasisEvaluation canonical;
canonical.sample(element, rule, operation, vector_copies ? 3 : 1, cache, mode, reconstruction);
if constexpr (!geometry_dependent) {
basis = std::move(canonical);
} else {
basis.reset_from(canonical, reference_components);
for (int point = 0; point < rule.GetNPoints(); ++point) {
transformation.SetIntPoint(&rule.IntPoint(point));
basis.map_point(point, canonical, [&](const ::mfem::Vector& input, ::mfem::Vector& output) {
if constexpr (needs_vector_gradient) {
const ::mfem::DenseMatrix& inverse = transformation.InverseJacobian();
output = 0.0;
for (int component = 0; component < 3; ++component) {
for (int physical = 0; physical < 3; ++physical) {
for (int reference = 0; reference < 3; ++reference) {
output[3 * component + physical] += input[3 * component + reference] * inverse(reference, physical);
}
}
}
} else {
transformation.InverseJacobian().MultTranspose(input, output);
}
}, workspace);
}
}
}
/// Apply SERiF's map to small tensor point maps, or to dense basis columns on other elements.
static void map_basis_point(
int point,
const mapping::MappingPointContext& context,
const BasisEvaluation& reference,
BasisEvaluation& result,
BasisWorkspace& workspace,
const mapping::MappingPointVariation* variation = nullptr
) {
result.map_point(point, reference, [&](const ::mfem::Vector& input, ::mfem::Vector& output) {
ReferenceValue value{};
for (int axis = 0; axis < reference_components; ++axis) {
discretization::forms::component(value, axis) = input[axis];
}
const Value mapped = variation ? geometry_variation(value, context, *variation) : physical_value(value, context);
for (int axis = 0; axis < components; ++axis) {
output[axis] = discretization::forms::component(mapped, axis);
}
}, workspace);
}
/*
* For a scalar VALUE operand, the following contraction evaluates a scalar
* finite-element field at quadrature points. Vector H1 and H(div) use the
* same matrix-vector product with its component copies or gradient bases.
*
* On one element, the field is represented by
*
* ρ_h(ξ) = ∑_{j=1}^{n} c_j N_j(ξ),
*
* where n is the number of local scalar DOFs, c_j is the coefficient
* associated with basis function N_j, and ξ is a coordinate in the
* canonical reference element.
*
* There are two coordinate maps to distinguish:
*
* ξ -> X: MFEM's canonical element to the fixed computational mesh.
* X -> x: SERiF's additional mapping to physical space.
*
* Here ρ_h(ξ) denotes the scalar field pulled back to the canonical
* element. Its basis values use the ordinary scalar VALUE mapping.
*
* sample_basis evaluates each N_j at every quadrature point ξ_q,
* producing the basis evaluation matrix
*
* E_qj = N_j(ξ_q).
*
* The physical E has one row per point for scalar values and divergences,
* and three rows per point for vector values. Before the additional map,
* H1 divergence retains nine gradient entries per point. Vector H1 has three independent component copies of each scalar basis.
*
* element_values extracts this element's coefficients from the supported
* field vector, including the required DOF sign and orientation handling.
* We then size the output vector to the number of quadrature points.
*
* Finally, evaluate performs the contraction represented by the matrix-vector product
*
* operand_values = E c,
*
* using either the dense matrix or equivalent tensor contractions,
*
* so that
*
* operand_values[q] = ∑_j N_j(ξ_q) c_j = ρ_h(ξ_q).
*
* The expansion above writes c_j N_j, while this matrix product writes
* N_j c_j. These are products of individual scalars, whose order can be
* exchanged. Despite appearances (to me at least) we are not exchanging a matrix and a vector.
*/
/// Apply a cached tensor or dense basis using reusable contraction scratch.
static void evaluate(
const BasisEvaluation& basis,
const ::mfem::Vector& coefficients,
::mfem::Vector& values,
BasisWorkspace& workspace
) {
basis.evaluate(coefficients, values, workspace);
}
/// Transpose the same contraction; global scalar reductions retain compensated summation.
static void integrate(
const BasisEvaluation& basis,
const ::mfem::Vector& values,
::mfem::Vector& result,
BasisWorkspace& workspace
) {
if constexpr (global_scalar) {
utils::CompensatedSum sum;
for (int point = 0; point < values.Size(); ++point) {
sum.add(values[point]);
}
result.SetSize(1);
result[0] = sum.value();
} else {
basis.integrate(values, result, workspace);
}
}
/// Read the reference data needed for one physical operand sample.
static ReferenceValue reference_value(
const ::mfem::Vector& values,
int point
) {
ReferenceValue result{};
for (int axis = 0; axis < reference_components; ++axis) {
discretization::forms::component(result, axis) = values[point * reference_components + axis];
}
return result;
}
/// Apply only SERiF's additional fixed-mesh to physical map (MFEM's map is already included).
static Value physical_value(
const ReferenceValue& reference,
const mapping::MappingPointContext& context
) {
if constexpr (h1_divergence) {
return gradient_trace(reference, context.inverse_mapping_jacobian);
} else if constexpr (vector_gradient) {
return transform_vector_gradient(reference, context.inverse_mapping_jacobian);
} else if constexpr (scalar_gradient) {
return transform_gradient(reference, context.inverse_mapping_jacobian);
} else {
return reference;
}
}
/// At fixed coefficients: scalar gradients and H1 divergence vary through A^-1; values do not vary.
static Value geometry_variation(
const ReferenceValue& reference,
const mapping::MappingPointContext& context,
const mapping::MappingPointVariation& variation
) {
if constexpr (h1_divergence) {
return gradient_trace(reference, variation.inverse_mapping_jacobian_variation);
} else if constexpr (vector_gradient) {
return transform_vector_gradient(reference, variation.inverse_mapping_jacobian_variation);
} else if constexpr (scalar_gradient) {
return transform_gradient(reference, variation.inverse_mapping_jacobian_variation);
} else {
return {};
}
}
private:
/** @brief Apply G_x = G_X A^-1; the inverse variation gives the same contraction for delta(G_x). */
static Value transform_vector_gradient(const ReferenceValue& gradient, const ::mfem::DenseMatrix& inverse) requires vector_gradient {
Value result{};
for (int component = 0; component < 3; ++component) {
for (int physical = 0; physical < 3; ++physical) {
for (int reference = 0; reference < 3; ++reference) {
result[component][physical] += gradient[3 * component + reference] * inverse(reference, physical);
}
}
}
return result;
}
/// grad_x(z) = A^-T grad_X(z); the inverse variation gives its geometry derivative.
static Value transform_gradient(
const ReferenceValue& gradient,
const ::mfem::DenseMatrix& inverse
) requires scalar_gradient {
Value result{};
for (int physical = 0; physical < 3; ++physical) {
for (int reference = 0; reference < 3; ++reference) {
result[physical] += inverse(reference, physical) * gradient[reference];
}
}
return result;
}
/// div_x(u) = trace(G A^-1); replacing A^-1 by its variation gives delta(div_x(u)).
static double gradient_trace(
const ReferenceValue& gradient,
const ::mfem::DenseMatrix& inverse
) requires h1_divergence {
double result = 0.0;
for (int component = 0; component < 3; ++component) {
for (int derivative = 0; derivative < 3; ++derivative) {
result += gradient[3 * component + derivative] * inverse(derivative, component);
}
}
return result;
}
};
/// Check the complete interface used by preparation, Jacobian application, and its transpose.
template <typename OperandT>
concept EvaluableOperand = requires(
const discretization::backend::mfem::DofMap& binding,
const ::mfem::IntegrationRule& rule,
::mfem::ElementTransformation& transformation,
const BasisEvaluation& reference_basis,
BasisEvaluation& mapped_basis,
TensorBasisCache& cache,
BasisWorkspace& workspace,
const ::mfem::Vector& coefficients,
::mfem::Vector& values,
const typename OperandEvaluator<OperandT>::ReferenceValue& reference_value,
const mapping::MappingPointContext& context,
const mapping::MappingPointVariation& variation
) {
typename OperandEvaluator<OperandT>::Value;
typename OperandEvaluator<OperandT>::ReferenceValue;
{ OperandEvaluator<OperandT>::components } -> std::convertible_to<int>;
{ OperandEvaluator<OperandT>::reference_components } -> std::convertible_to<int>;
{ OperandEvaluator<OperandT>::geometry_dependent } -> std::convertible_to<bool>;
{ OperandEvaluator<OperandT>::sample_basis(binding, 0, rule, mapped_basis, cache, ContractionMode::automatic, transformation, workspace) } -> std::same_as<void>;
{ OperandEvaluator<OperandT>::evaluate(reference_basis, coefficients, values, workspace) } -> std::same_as<void>;
{ OperandEvaluator<OperandT>::integrate(reference_basis, coefficients, values, workspace) } -> std::same_as<void>;
{ OperandEvaluator<OperandT>::reference_value(coefficients, 0) } -> std::same_as<typename OperandEvaluator<OperandT>::ReferenceValue>;
{ OperandEvaluator<OperandT>::physical_value(reference_value, context) } -> std::same_as<typename OperandEvaluator<OperandT>::Value>;
{ OperandEvaluator<OperandT>::geometry_variation(reference_value, context, variation) } -> std::same_as<typename OperandEvaluator<OperandT>::Value>;
{ OperandEvaluator<OperandT>::map_basis_point(0, context, reference_basis, mapped_basis, workspace) } -> std::same_as<void>;
{ OperandEvaluator<OperandT>::map_basis_point(0, context, reference_basis, mapped_basis, workspace, &variation) } -> std::same_as<void>;
};
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,55 @@
#pragma once
#include <cstddef>
#include <expected>
#include "serif/utils/dependencies/preparation.hpp"
#include "serif/backend/mfem/operators/rejection.hpp"
namespace serif::operators::backend::mfem {
using PreparationMode = utils::dependencies::PreparationMode;
/// Numerical work derived from the dependency engine's most recent report.
struct NumericalWork {
std::size_t reused_elements{0};
std::size_t geometry_points{0};
std::size_t sampled_operands{0};
std::size_t physics_points{0};
std::size_t refreshed_scalars{0};
/// Report whether numerical preparation refreshed any samples or physics.
[[nodiscard]] bool did_work() const noexcept {
return geometry_points != 0 || sampled_operands != 0 || physics_points != 0 || refreshed_scalars != 0;
}
};
/// A failed validation or calculation must make the previous accepted result inaccessible.
template <typename CacheT, typename PrepareT>
[[nodiscard]] std::expected<void, PreparationRejection> try_prepare_validated(CacheT& cache, PrepareT&& prepare) {
// Stack unwinding must invalidate the cache without intercepting unrelated caller errors.
struct InvalidateOnFailure final {
CacheT& cache;
bool prepared{false};
/** @brief Make previous results unavailable whenever preparation exits unsuccessfully. */
~InvalidateOnFailure() {
if (!prepared) {
cache.invalidate();
}
}
};
InvalidateOnFailure guard{.cache = cache};
try {
const auto result = prepare();
if (!result) {
return std::unexpected(result.error().error);
}
guard.prepared = true;
return {};
} catch (const std::domain_error& error) {
return std::unexpected(PreparationRejection{
.reason = PreparationRejectionReason::NON_FINITE,
.message = error.what()
});
}
}
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,61 @@
#pragma once
#include <algorithm>
#include <array>
#include <limits>
#include "mfem/fem/eltrans.hpp"
#include "mfem/fem/fe/fe_base.hpp"
#include "serif/backend/mfem/discretization/quadrature/mfem_resolver.hpp"
#include "serif/operators/plan.hpp"
namespace serif::operators::backend::mfem {
/// Resolve one rule covering all operand forms and the geometry weight.
template <VolumeEquation RowEquationT, typename ContributionT>
[[nodiscard]] const ::mfem::IntegrationRule& equation_rule(
const ContributionT& equation,
const discretization::quadrature::Policy& policy,
::mfem::ElementTransformation& transformation,
const ::mfem::FiniteElement& displacement_element,
discretization::quadrature::MappingKind mapping_kind = discretization::quadrature::MappingKind::general,
int reconstruction_order = 0
) {
using namespace discretization::quadrature;
using Plan = EquationPlan<RowEquationT>;
const discretization::quadrature::backend::mfem::MFEMResolver resolver(policy);
const int position_order = std::max(transformation.Order(), displacement_element.GetOrder());
const int weight_order = transformation.OrderW() + 3 * std::max(0, displacement_element.GetOrder() - 1);
int order = 0;
Plan::template for_each_input<ContributionT>([&]<typename OperandT>([[maybe_unused]] OperandT operand, std::size_t) {
using namespace utils::errors;
using Form = Plan::template Form<OperandT, ContributionT>;
std::array extra{equation.additional_order(operand, position_order)};
serif_ensure<SERiFErrorCode::INVALID_QUADRATURE_ORDER, std::invalid_argument>(
extra[0] >= 0 && reconstruction_order >= 0 &&
extra[0] <= std::numeric_limits<int>::max() - weight_order - discretization::quadrature::backend::mfem::mfem_form_static_order<Form> &&
reconstruction_order <= std::numeric_limits<int>::max() - weight_order - discretization::quadrature::backend::mfem::mfem_form_static_order<Form> - extra[0],
"Equation quadrature order exceeds the supported range."
);
// Automatic rules account for the nonpolynomial profile; explicit policy orders remain authoritative.
extra[0] += reconstruction_order;
const auto resolved = resolver.template resolve<Form>({
.geometry = transformation.GetGeometryType(),
.geometry_weight_order = weight_order,
.dynamic_orders = extra,
.domain = equations::SupportOf<ContributionT>{},
.mapping = mapping_kind
});
order = std::max(order, resolved.resolution.order);
});
return ::mfem::IntRules.Get(transformation.GetGeometryType(), order);
}
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,21 @@
#pragma once
#include <string>
#include "serif/backend/mfem/mapping/status.hpp"
namespace serif::operators::backend::mfem {
enum class PreparationRejectionReason {
MAPPING,
EQUATION,
NON_FINITE
};
struct PreparationRejection final {
PreparationRejectionReason reason;
mapping::MappingStatus mapping_status{mapping::MappingStatus::VALID};
int element{-1};
int quadrature_point{-1};
std::string message;
};
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,14 @@
#pragma once
#include <memory>
#include <vector>
#include "serif/backend/mfem/operators/linearized_block.hpp"
namespace serif::operators::backend::mfem
{
/**
* @brief Replace selected physical equation rows with an immutable sparse row block in the same unknown coordinates.
* @note Replacement row i belongs to selected_rows[i]. Efficient diagonal and sparse assembly remain available when the original block supplies them; no columns are probed. Both matrices must remain immutable after publication.
*/
[[nodiscard]] LinearizedBlock replace_block_rows(LinearizedBlock original, std::vector<int> selected_rows, std::shared_ptr<const ::mfem::SparseMatrix> replacement);
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,367 @@
#pragma once
#include <algorithm>
#include <array>
#include <functional>
#include <span>
#include <tuple>
#include <vector>
#include "serif/backend/mfem/operators/volume_inputs.hpp"
#include "serif/backend/mfem/operators/element_evaluation.hpp"
#include "serif/backend/mfem/operators/element_transpose.hpp"
#include "serif/backend/mfem/shared/parallel.hpp"
namespace serif::operators::backend::mfem {
/// Reject changes to discretization objects borrowed by an accepted linearization.
template <VolumeEquation EquationT, typename SchemaT>
void validate_volume_data(const VolumeData<EquationT, SchemaT>& data) {
using namespace utils::errors;
const auto& discretization = data.configuration->discretization;
validate_volume_discretization(discretization);
for (const auto* column : data.bindings.columns) {
column->validate();
}
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR>(
discretization.residual_binding.mesh().GetNodesSequence() == data.bindings.nodes_sequence,
"The prepared equation mesh changed; prepare again or rebuild its bindings.");
}
/// Visit each contribution's own batch in equation order, without scanning other contributions.
template <VolumeEquation EquationT, typename SchemaT, typename VisitT>
void for_each_volume_element(const VolumeData<EquationT, SchemaT>& data, VisitT&& visit) {
[&]<std::size_t... Indices>(std::index_sequence<Indices...>) {
const auto visit_contribution = [&]<std::size_t Index>() {
using Contribution = std::tuple_element_t<Index, equations::ContributionsOf<EquationT>>;
for (const auto& element : data.contributions[Index]) {
visit.template operator()<Contribution>(*element);
}
};
(visit_contribution.template operator()<Indices>(), ...);
}(std::make_index_sequence<std::tuple_size_v<equations::ContributionsOf<EquationT>>>{});
}
/** @brief Visit equation data together with its graph-owned local geometry derivative matrix. */
template <VolumeEquation EquationT, typename SchemaT, typename VisitT>
void for_each_volume_linearization(const VolumeData<EquationT, SchemaT>& data, VisitT&& visit) {
[&]<std::size_t... Indices>(std::index_sequence<Indices...>) {
const auto visit_contribution = [&]<std::size_t Index>() {
using Contribution = std::tuple_element_t<Index, equations::ContributionsOf<EquationT>>;
const auto& elements = data.contributions[Index];
for (std::size_t element = 0; element < elements.size(); ++element) {
visit.template operator()<Contribution>(*elements[element], *data.geometry_derivatives[Index][element]);
}
};
(visit_contribution.template operator()<Indices>(), ...);
}(std::make_index_sequence<std::tuple_size_v<equations::ContributionsOf<EquationT>>>{});
}
/// Return the supported residual size.
template <VolumeEquation EquationT, typename SchemaT>
[[nodiscard]] int volume_height(const VolumeData<EquationT, SchemaT>& data) {
validate_volume_data(data);
return data.configuration->discretization.residual_binding.size();
}
/// Packed directions contain unique coefficient terms followed by displacement.
template <VolumeEquation EquationT, typename SchemaT>
[[nodiscard]] int volume_width(const VolumeData<EquationT, SchemaT>& data) {
validate_volume_data(data);
int size = data.configuration->discretization.displacement_binding.size();
for (const auto* column : data.bindings.columns) {
size += column->size();
}
return size;
}
/// Count contribution-elements using tensor contractions for any sampled basis.
template <VolumeEquation EquationT, typename SchemaT>
[[nodiscard]] std::size_t tensor_element_count(const VolumeData<EquationT, SchemaT>& data) {
validate_volume_data(data);
std::size_t count = 0;
for_each_volume_element(data, [&]<typename ContributionT>(const auto& element) {
bool tensor = element.basis->displacement_basis.uses_tensor() || element.geometry->test_basis.uses_tensor();
for (const auto& basis : element.geometry->operand_basis) {
tensor = tensor || basis.uses_tensor();
}
count += tensor;
});
return count;
}
/// Return the largest contribution quadrature order on a supported mesh element.
template <VolumeEquation EquationT, typename SchemaT>
[[nodiscard]] int integration_order(const VolumeData<EquationT, SchemaT>& data, int element) {
using namespace utils::errors;
validate_volume_data(data);
int order = -1;
for_each_volume_element(data, [&]<typename ContributionT>(const auto& entry) {
if (entry.element == element) {
order = std::max(order, entry.basis->rule->GetOrder());
}
});
serif_ensure<SERiFErrorCode::OUT_OF_RANGE_ERROR, std::out_of_range>(
order >= 0, "Element {} is not in the prepared equation support.", element);
return order;
}
/// Copy the residual already assembled and checked when this result was accepted.
template <VolumeEquation EquationT, typename SchemaT>
void volume_residual(const VolumeData<EquationT, SchemaT>& data, ::mfem::Vector& result) {
validate_volume_data(data);
result = data.residual;
}
/** @brief Validate coefficient bindings once before a complete element traversal. */
template <std::size_t Count>
[[nodiscard]] std::array<discretization::backend::mfem::DofMap::Elements, Count> column_elements(
const std::array<const discretization::backend::mfem::DofMap*, Count>& columns
) {
return std::apply([](const auto*... column) {
return std::array<discretization::backend::mfem::DofMap::Elements, Count>{column->validated_elements()...};
}, columns);
}
/// Apply only the requested directions and assemble their supported residual contribution.
template <bool Fields = true, bool Geometry = true, VolumeEquation EquationT, typename SchemaT>
void apply_volume_direction(
const VolumeData<EquationT, SchemaT>& data,
const std::array<const ::mfem::Vector*, EquationPlan<EquationT>::column_count>& directions,
const ::mfem::Vector& displacement_direction,
Workspace& workspace,
::mfem::Vector& result
) {
using Plan = EquationPlan<EquationT>;
validate_volume_data(data);
const auto& discretization = data.configuration->discretization;
if constexpr (Geometry) {
discretization.displacement_binding.validate_values(displacement_direction);
}
if constexpr (Fields) {
for (std::size_t column = 0; column < Plan::column_count; ++column) {
data.bindings.columns[column]->validate_values(*directions[column]);
}
}
workspace.full_result.SetSize(discretization.residual_binding.full_size());
workspace.full_result = 0.0;
workspace.operand_values.resize(Plan::operand_count);
utils::CompensatedSum global_sum;
const auto rows = discretization.residual_binding.validated_elements();
const auto columns = column_elements(data.bindings.columns);
const auto displacement = discretization.displacement_binding.validated_elements();
// Element actions are independent and run concurrently, each into its own result. They are then added
// into the residual one by one in the original element order, so every sum is unchanged.
std::vector<std::function<void(Workspace&, ::mfem::Vector&)>> actions;
std::vector<int> action_elements;
for_each_volume_linearization(data, [&]<typename ContributionT>(const auto& element, const auto& geometry_derivatives) {
action_elements.push_back(element.element);
actions.emplace_back([&](Workspace& local, ::mfem::Vector& element_result) {
element_jacobian_action<EquationT, ContributionT, Fields, Geometry>(
element, geometry_derivatives, columns, directions, displacement,
displacement_direction, local, element_result);
});
});
std::vector<::mfem::Vector> element_results(actions.size());
const int workers = utils::parallel::worker_count();
std::vector<Workspace> worker_workspaces(static_cast<std::size_t>(workers > 1 ? workers - 1 : 0));
for (auto& local : worker_workspaces) {
local.operand_values.resize(Plan::operand_count);
}
utils::parallel::for_each_index(static_cast<int>(actions.size()), [&](const int index, const int worker) {
Workspace& local = worker == 0 ? workspace : worker_workspaces[static_cast<std::size_t>(worker - 1)];
actions[static_cast<std::size_t>(index)](local, element_results[static_cast<std::size_t>(index)]);
});
for (std::size_t index = 0; index < actions.size(); ++index) {
if constexpr (discretization::blocks::GlobalScalarTerm<typename Plan::Test>) {
global_sum.add(element_results[index][0]);
} else {
rows.add_element(action_elements[index], element_results[index], workspace.full_result);
}
}
if constexpr (discretization::blocks::GlobalScalarTerm<typename Plan::Test>) {
workspace.full_result[0] = global_sum.value();
}
result = discretization.residual_binding.accumulate_dual(workspace.full_result);
discretization.residual_binding.validate_values(result);
}
/**
* @brief apply_volume_direction<false, true> for several displacement directions in one traversal.
* @note Every (direction, element) action runs concurrently with worker-owned scratch, then each direction
* adds its element results in the original element order, so each result equals the single-direction call.
*/
template <VolumeEquation EquationT, typename SchemaT>
void apply_volume_displacements(
const VolumeData<EquationT, SchemaT>& data,
std::span<const ::mfem::Vector* const> displacement_directions,
std::span<::mfem::Vector* const> results
) {
using Plan = EquationPlan<EquationT>;
validate_volume_data(data);
const auto& discretization = data.configuration->discretization;
for (const auto* direction : displacement_directions) {
discretization.displacement_binding.validate_values(*direction);
}
const auto rows = discretization.residual_binding.validated_elements();
const auto columns = column_elements(data.bindings.columns);
const auto displacement = discretization.displacement_binding.validated_elements();
std::vector<std::function<void(Workspace&, std::span<::mfem::Vector* const>)>> actions;
std::vector<int> action_elements;
for_each_volume_linearization(data, [&]<typename ContributionT>(const auto& element, const auto& geometry_derivatives) {
action_elements.push_back(element.element);
actions.emplace_back([&](Workspace& local, std::span<::mfem::Vector* const> element_results) {
element_geometry_actions<EquationT, ContributionT>(
element, geometry_derivatives, displacement, displacement_directions, local, element_results);
});
});
const std::size_t elements = actions.size();
const std::size_t count = displacement_directions.size();
// element_results[direction * elements + element]
std::vector<::mfem::Vector> element_results(elements * count);
const int workers = utils::parallel::worker_count();
std::vector<Workspace> worker_workspaces(static_cast<std::size_t>(workers));
for (auto& local : worker_workspaces) {
local.operand_values.resize(Plan::operand_count);
}
utils::parallel::for_each_index(static_cast<int>(elements), [&](const int element, const int worker) {
std::vector<::mfem::Vector*> lanes(count);
for (std::size_t direction = 0; direction < count; ++direction) {
lanes[direction] = &element_results[direction * elements + static_cast<std::size_t>(element)];
}
actions[static_cast<std::size_t>(element)](worker_workspaces[static_cast<std::size_t>(worker)], lanes);
});
utils::parallel::for_each_index(static_cast<int>(count), [&](const int direction, int) {
const auto first = static_cast<std::size_t>(direction) * elements;
::mfem::Vector full(discretization.residual_binding.full_size());
full = 0.0;
utils::CompensatedSum global_sum;
for (std::size_t index = 0; index < elements; ++index) {
if constexpr (discretization::blocks::GlobalScalarTerm<typename Plan::Test>) {
global_sum.add(element_results[first + index][0]);
} else {
rows.add_element(action_elements[index], element_results[first + index], full);
}
}
if constexpr (discretization::blocks::GlobalScalarTerm<typename Plan::Test>) {
full[0] = global_sum.value();
}
auto& result = *results[static_cast<std::size_t>(direction)];
result = discretization.residual_binding.accumulate_dual(full);
discretization.residual_binding.validate_values(result);
});
}
/// Apply a direction whose terms explicitly name the accepted coefficient bindings.
template <VolumeEquation EquationT, typename SchemaT, typename CoefficientsT>
void apply_jacobian(
const VolumeData<EquationT, SchemaT>& data,
const CoefficientsT& direction,
const ::mfem::Vector& displacement_direction,
Workspace& workspace,
::mfem::Vector& result
) {
using namespace utils::errors;
using Plan = EquationPlan<EquationT>;
std::array<const ::mfem::Vector*, Plan::column_count> directions;
Plan::for_each_column([&]<typename OperandT>(OperandT, std::size_t column) {
const auto& input = direction.template get<typename OperandT::term_type>();
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR, std::invalid_argument>(
&input.binding == data.bindings.columns[column],
"Jacobian directions must use the prepared coefficient bindings.");
directions[column] = &input.values;
});
apply_volume_direction(data, directions, displacement_direction, workspace, result);
}
/// Unpack unique coefficient terms and displacement, then use the same numerical action.
template <VolumeEquation EquationT, typename SchemaT>
void apply_jacobian(
const VolumeData<EquationT, SchemaT>& data,
const ::mfem::Vector& direction,
Workspace& workspace,
::mfem::Vector& result
) {
using namespace utils::errors;
using Plan = EquationPlan<EquationT>;
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT, std::invalid_argument>(
direction.Size() == volume_width(data), "Packed equation direction has the wrong size.");
std::array<::mfem::Vector, Plan::column_count> values;
std::array<const ::mfem::Vector*, Plan::column_count> directions;
int offset = 0;
for (std::size_t column = 0; column < Plan::column_count; ++column) {
values[column].SetSize(data.bindings.columns[column]->size());
std::copy_n(direction.GetData() + offset, values[column].Size(), values[column].GetData());
directions[column] = &values[column];
offset += values[column].Size();
}
::mfem::Vector displacement_direction(data.configuration->discretization.displacement_binding.size());
std::copy_n(direction.GetData() + offset, displacement_direction.Size(), displacement_direction.GetData());
apply_volume_direction(data, directions, displacement_direction, workspace, result);
}
/// Assemble the full coordinate transpose in the same packed order as the forward action.
template <VolumeEquation EquationT, typename SchemaT>
void apply_jacobian_transpose(
const VolumeData<EquationT, SchemaT>& data,
const ::mfem::Vector& residual_dual,
Workspace& workspace,
::mfem::Vector& result
) {
using Plan = EquationPlan<EquationT>;
validate_volume_data(data);
const auto& discretization = data.configuration->discretization;
discretization.residual_binding.validate_values(residual_dual);
std::array<::mfem::Vector, Plan::column_count> full_columns;
std::array<::mfem::Vector, Plan::column_count> element_columns;
std::array<utils::CompensatedSum, Plan::column_count> scalar_sums;
for (std::size_t column = 0; column < Plan::column_count; ++column) {
full_columns[column].SetSize(data.bindings.columns[column]->full_size());
full_columns[column] = 0.0;
}
::mfem::Vector full_displacement(discretization.displacement_binding.full_size());
::mfem::Vector element_displacement;
full_displacement = 0.0;
const auto row_elements = discretization.residual_binding.validated_elements();
const auto columns = column_elements(data.bindings.columns);
const auto displacement_elements = discretization.displacement_binding.validated_elements();
for_each_volume_linearization(data, [&]<typename ContributionT>(const auto& element, const auto& geometry_derivatives) {
element_jacobian_transpose<EquationT, ContributionT>(
element, geometry_derivatives, row_elements, residual_dual, workspace,
element_columns, element_displacement);
Plan::for_each_column([&]<typename OperandT>(OperandT, std::size_t column) {
if (element_columns[column].Size() == 0) {
return;
}
if constexpr (discretization::blocks::GlobalScalarTerm<typename OperandT::term_type>) {
scalar_sums[column].add(element_columns[column][0]);
} else {
columns[column].add_element(element.element, element_columns[column], full_columns[column]);
}
});
displacement_elements.add_element(element.element, element_displacement, full_displacement);
});
::mfem::Vector packed(volume_width(data));
int offset = 0;
Plan::for_each_column([&]<typename OperandT>(OperandT, std::size_t column) {
if constexpr (discretization::blocks::GlobalScalarTerm<typename OperandT::term_type>) {
full_columns[column][0] = scalar_sums[column].value();
}
const auto values = data.bindings.columns[column]->accumulate_dual(full_columns[column]);
data.bindings.columns[column]->validate_values(values);
std::copy_n(values.GetData(), values.Size(), packed.GetData() + offset);
offset += values.Size();
});
const auto displacement = discretization.displacement_binding.accumulate_dual(full_displacement);
discretization.displacement_binding.validate_values(displacement);
std::copy_n(displacement.GetData(), displacement.Size(), packed.GetData() + offset);
result = packed;
}
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,248 @@
#pragma once
#include <expected>
#include <memory>
#include <tuple>
#include <utility>
#include "serif/backend/mfem/operators/volume_stages.hpp"
#include "serif/backend/mfem/operators/volume_inputs.hpp"
#include "serif/utils/dependencies/dependencies.hpp"
namespace serif::operators::backend::mfem
{
/// Values need a mapping node only when the operand's physical representation depends on geometry.
template <std::size_t Contribution, typename OperandT, typename Scope = void>
using VolumeOperandResult =
std::conditional_t<OperandEvaluator<OperandT>::geometry_dependent, typename volume_keys::Scoped<Scope>::template PhysicalValues<Contribution, OperandT>,
typename volume_keys::Scoped<Scope>::template ReferenceValues<Contribution, OperandT>>;
/// Declare one coefficient contraction, followed by a physical mapping when the space requires it.
template <std::size_t Contribution, typename OperandT, VolumeEquation EquationT, typename SchemaT, typename Scope = void>
[[nodiscard]] auto make_operand_computations(const std::shared_ptr<VolumeScratch>& scratch)
{
using namespace utils::dependencies;
using Basis = ElementBatch<ElementBasis<EquationPlan<EquationT>::operand_count>>;
using Geometry = ElementBatch<ElementGeometry<EquationPlan<EquationT>::operand_count>>;
using Binding = discretization::backend::mfem::TermBinding<typename OperandT::term_type, SchemaT>;
using ReferenceValues = typename volume_keys::Scoped<Scope>::template ReferenceValues<Contribution, OperandT>;
const auto sample_values = make_computation<ReferenceValues, InputList<typename volume_keys::Scoped<Scope>::template Basis<Contribution>, typename OperandT::term_type>>(
[scratch](const Basis& basis, const CoefficientView<Binding>& input)
{
return sample_contribution_operand<EquationT, OperandT>(basis, input, scratch->workspace);
});
if constexpr (OperandEvaluator<OperandT>::geometry_dependent)
{
const auto map_values = make_computation<typename volume_keys::Scoped<Scope>::template PhysicalValues<Contribution, OperandT>,
InputList<ReferenceValues, typename volume_keys::Scoped<Scope>::template Geometry<Contribution>>>(
[scratch](const OperandSamples& samples, const Geometry& geometry)
{
return map_contribution_operand<OperandT>(samples, geometry, scratch->workspace);
});
return std::tuple{sample_values, map_values};
}
else
{
return std::tuple{sample_values};
}
}
/// Build one contribution's branch in the same order as its numerical description.
template <std::size_t Index, typename Scope, VolumeEquation EquationT, typename SchemaT, typename ContributionT, typename... Operands>
[[nodiscard]] auto make_contribution_computations(const std::shared_ptr<const VolumeConfiguration<EquationT, SchemaT>>& configuration,
const std::shared_ptr<VolumeScratch>& scratch, ContributionBinding<ContributionT> contribution,
utils::types::TypeList<Operands...>)
{
using namespace utils::dependencies;
using Plan = EquationPlan<EquationT>;
using Basis = ElementBatch<ElementBasis<Plan::operand_count>>;
using Geometry = ElementBatch<ElementGeometry<Plan::operand_count>>;
using DisplacementBinding = typename VolumeConfiguration<EquationT, SchemaT>::DisplacementBinding;
const auto reference_basis = make_computation<typename volume_keys::Scoped<Scope>::template Basis<Index>, InputList<typename volume_keys::Scoped<Scope>::Bindings>>(
[configuration, scratch, contribution](const BasisBindings<Plan::column_count>& bindings)
{
return sample_contribution_basis(*configuration, contribution, bindings, scratch->basis);
});
const auto geometry = make_computation<typename volume_keys::Scoped<Scope>::template Geometry<Index>,
InputList<typename volume_keys::Scoped<Scope>::template Basis<Index>, DisplacementCoefficients, CompactificationCoefficients>>(
[configuration, scratch](const Basis& basis, const CoefficientView<DisplacementBinding>& displacement, const MappingCoefficients& compactification)
{
return map_contribution_geometry<EquationT, SchemaT, ContributionT>(*configuration, basis, displacement, compactification, scratch->workspace);
});
const auto operand_values = std::tuple_cat(make_operand_computations<Index, Operands, EquationT, SchemaT, Scope>(scratch)...);
const auto equation_values = make_computation<typename volume_keys::Scoped<Scope>::template QuadratureLinearization<Index>,
InputList<typename volume_keys::Scoped<Scope>::template Basis<Index>,
typename volume_keys::Scoped<Scope>::template Geometry<Index>, VolumeOperandResult<Index, Operands, Scope>...>>(
[scratch, contribution](const Basis& basis, const Geometry& geometry, const SamplesFor<Operands>&... samples)
{
return evaluate_contribution_equation<EquationT, ContributionT, Operands...>(contribution, basis, geometry, scratch->workspace, samples...);
});
const auto geometry_derivatives = make_computation<typename volume_keys::Scoped<Scope>::template GeometryDerivatives<Index>,
InputList<typename volume_keys::Scoped<Scope>::template QuadratureLinearization<Index>>>(
[configuration, scratch](const ContributionData<Plan::operand_count>& data) {
return differentiate_contribution_geometry<EquationT, SchemaT, ContributionT>(*configuration, data, scratch->workspace);
});
return std::tuple_cat(std::tuple{reference_basis, geometry}, operand_values, std::tuple{equation_values, geometry_derivatives});
}
/// Expose contribution computations so deformation and equations can share one dependency graph.
template <typename Scope = void, VolumeEquation EquationT, typename SchemaT, std::size_t... Indices>
[[nodiscard]] auto connect_volume_contributions(const std::shared_ptr<const VolumeConfiguration<EquationT, SchemaT>>& configuration, std::index_sequence<Indices...>)
{
using namespace utils::dependencies;
using Plan = EquationPlan<EquationT>;
const auto scratch = std::make_shared<VolumeScratch>();
const auto contributions = bind_contributions(configuration->equation);
const auto calculations = std::tuple_cat(make_contribution_computations<Indices, Scope>(
configuration, scratch, std::get<Indices>(contributions), typename std::tuple_element_t<Indices, equations::ContributionsOf<EquationT>>::Operands{})...);
const auto volume =
make_computation<typename volume_keys::Scoped<Scope>::Linearization,
InputList<typename volume_keys::Scoped<Scope>::Bindings, typename volume_keys::Scoped<Scope>::template QuadratureLinearization<Indices>...,
typename volume_keys::Scoped<Scope>::template GeometryDerivatives<Indices>...>>(
[configuration, scratch](const BasisBindings<Plan::column_count>& bindings, const auto&... results)
{
// The first group contains the physical equations; the second their local geometry derivatives.
const auto values = std::tie(results...);
auto volume = collect_volume_results(configuration, bindings, scratch->workspace, std::get<Indices>(values)...);
if (volume) {
volume->geometry_derivatives = {std::get<sizeof...(Indices) + Indices>(values)...};
}
return volume;
});
return std::tuple_cat(calculations, std::tuple{volume});
}
/// Convert the dependency engine's node statuses to numerical work counts for one contribution.
template <VolumeEquation EquationT, std::size_t Index, typename Scope = void, typename CacheT>
void report_contribution_preparation(const CacheT& cache, NumericalWork& report)
{
using Plan = EquationPlan<EquationT>;
using Contribution = std::tuple_element_t<Index, equations::ContributionsOf<EquationT>>;
const auto& basis = cache.template get<typename volume_keys::Scoped<Scope>::template Basis<Index>>();
const auto& status = cache.report();
std::size_t points = 0;
for (const auto& element : basis)
{
points += element->rule->GetNPoints();
}
if (status.template status<typename volume_keys::Scoped<Scope>::template Geometry<Index>>() == utils::dependencies::PreparationStatus::computed)
{
report.geometry_points += points;
}
if (status.template status<typename volume_keys::Scoped<Scope>::template QuadratureLinearization<Index>>() == utils::dependencies::PreparationStatus::computed)
{
report.physics_points += points;
}
else
{
report.reused_elements += basis.size();
}
Plan::template for_each_input<Contribution>(
[&]<typename OperandT>(OperandT, std::size_t)
{
if (status.template status<typename volume_keys::Scoped<Scope>::template ReferenceValues<Index, OperandT>>() == utils::dependencies::PreparationStatus::computed)
{
report.sampled_operands += basis.size();
}
});
}
/**
* @brief Declare reusable volume computations without selecting their source of displacement.
* @note This allocates scratch and owns the equation configuration; numerical work begins at try_prepare.
* Borrowed discretization objects must outlive the computations and their accepted results.
*/
template <typename Scope = void, VolumeEquation EquationT, typename SchemaT>
[[nodiscard]] auto make_volume_equation_computations(std::shared_ptr<const EquationT> equation, const VolumeDiscretization<typename EquationT::Test, SchemaT>& discretization,
VolumeOptions options = {})
{
utils::errors::serif_ensure<utils::errors::SERiFErrorCode::INVALID_ARGUMENT>(equation != nullptr, "Volume computations require an owning equation; supply a non-null equation or retained model member.");
validate_volume_discretization(discretization);
const auto configuration = std::make_shared<const VolumeConfiguration<EquationT, SchemaT>>(
VolumeConfiguration<EquationT, SchemaT>{.equation = std::move(equation), .discretization = discretization, .options = std::move(options)});
return connect_volume_contributions<Scope>(configuration, std::make_index_sequence<std::tuple_size_v<equations::ContributionsOf<EquationT>>>{});
}
/** @brief Own standalone equations while allowing complete models to share their existing immutable members. */
template <typename Scope = void, VolumeEquation EquationT, typename SchemaT>
[[nodiscard]] auto make_volume_equation_computations(EquationT equation, const VolumeDiscretization<typename EquationT::Test, SchemaT>& discretization, VolumeOptions options = {})
{
return make_volume_equation_computations<Scope>(std::make_shared<const EquationT>(std::move(equation)), discretization, std::move(options));
}
/// Construct a standalone cache from the same computations used by a coupled deformation graph.
template <typename Scope = void, VolumeEquation EquationT, typename SchemaT, typename InputsT>
[[nodiscard]] auto make_volume_equation_cache(EquationT equation, const VolumeDiscretization<typename EquationT::Test, SchemaT>& discretization, const InputsT& inputs,
VolumeOptions options = {})
{
return std::apply(
[&](auto... calculations)
{
return utils::dependencies::make_computation_cache<PreparationRejection>(inputs, std::move(calculations)...);
},
make_volume_equation_computations<Scope>(std::move(equation), discretization, std::move(options)));
}
/// Recover the equation type from a cache's accepted result type, without accessing a result.
template <typename CacheT, typename Scope = void>
using VolumeEquationOf = typename std::remove_cvref_t<decltype(std::declval<const CacheT&>().template get<typename volume_keys::Scoped<Scope>::Linearization>())>::Equation;
/// Validate current inputs and atomically publish a complete volume linearization.
template <typename Scope = void, typename CacheT, typename DiscretizationT, typename CoefficientsT>
[[nodiscard]] std::expected<void, PreparationRejection> try_prepare_volume(CacheT& cache, const DiscretizationT& discretization, const CoefficientsT& term_coefficients,
const ::mfem::Vector& displacement_coefficients,
PreparationMode mode = PreparationMode::reuse_unchanged)
{
return try_prepare_validated(cache,
[&]()
{
const auto inputs =
make_volume_inputs<VolumeEquationOf<CacheT, Scope>, Scope>(discretization, term_coefficients, displacement_coefficients);
return cache.try_prepare(inputs, {}, mode);
});
}
/// Throw a caller-visible numerical error when a complete preparation cannot be accepted.
template <typename Scope = void, typename CacheT, typename DiscretizationT, typename CoefficientsT>
void prepare_volume(CacheT& cache, const DiscretizationT& discretization, const CoefficientsT& term_coefficients, const ::mfem::Vector& displacement_coefficients,
PreparationMode mode = PreparationMode::reuse_unchanged)
{
using namespace utils::errors;
const auto result = try_prepare_volume<Scope>(cache, discretization, term_coefficients, displacement_coefficients, mode);
if (!result)
{
serif_error<std::domain_error, SERiFErrorCode::INVALID_ARGUMENT>("{} preparation rejected at element {}, point {}: {}", VolumeEquationOf<CacheT, Scope>::name,
result.error().element, result.error().quadrature_point, result.error().message);
}
}
/// Derive work counts from accepted node statuses; no second report lifecycle is stored.
template <typename Scope = void, typename CacheT>
[[nodiscard]] NumericalWork volume_work_report(const CacheT& cache)
{
using Equation = VolumeEquationOf<CacheT, Scope>;
NumericalWork report;
if (cache.template contains<typename volume_keys::Scoped<Scope>::Linearization>())
{
[&]<std::size_t... Indices>(std::index_sequence<Indices...>)
{
(report_contribution_preparation<Equation, Indices, Scope>(cache, report), ...);
}(std::make_index_sequence<std::tuple_size_v<equations::ContributionsOf<Equation>>>{});
}
return report;
}
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,168 @@
#pragma once
#include <array>
#include <memory>
#include <vector>
#include "mfem/fem/gridfunc.hpp"
#include "serif/backend/mfem/mapping/domain_mapper.hpp"
#include "serif/backend/mfem/discretization/term_coefficients.hpp"
#include "serif/backend/mfem/operators/element_data.hpp"
#include "serif/backend/mfem/operators/quadrature.hpp"
#include "serif/backend/mfem/operators/workspace.hpp"
namespace serif::operators::backend::mfem
{
using discretization::backend::mfem::ContractionMode;
using discretization::backend::mfem::TensorBasisCache;
namespace volume_keys
{
/** @brief Keep equation-local stages distinct while sharing physical coefficient sources. */
template <typename Scope>
struct Scoped
{
template <std::size_t Contribution>
struct Basis
{
};
template <std::size_t Contribution>
struct Geometry
{
};
template <std::size_t Contribution, typename OperandT>
struct ReferenceValues
{
};
template <std::size_t Contribution, typename OperandT>
struct PhysicalValues
{
};
template <std::size_t Contribution>
struct QuadratureLinearization
{
};
/** @brief Weighted local displacement derivatives, owned and invalidated by the graph. */
template <std::size_t Contribution>
struct GeometryDerivatives {};
struct Linearization
{
};
struct Bindings
{
};
};
template <std::size_t Contribution>
using Basis = Scoped<void>::Basis<Contribution>;
template <std::size_t Contribution>
using Geometry = Scoped<void>::Geometry<Contribution>;
template <std::size_t Contribution, typename OperandT>
using ReferenceValues = Scoped<void>::ReferenceValues<Contribution, OperandT>;
template <std::size_t Contribution, typename OperandT>
using PhysicalValues = Scoped<void>::PhysicalValues<Contribution, OperandT>;
template <std::size_t Contribution>
using QuadratureLinearization = Scoped<void>::QuadratureLinearization<Contribution>;
template <std::size_t Contribution>
using GeometryDerivatives = Scoped<void>::GeometryDerivatives<Contribution>;
using Linearization = Scoped<void>::Linearization;
} // namespace volume_keys
template <std::size_t ColumnCount>
struct BasisBindings
{
const discretization::backend::mfem::DofMap* residual_binding;
const discretization::backend::mfem::DofMap* displacement_binding;
const ::mfem::FiniteElementSpace* compactification_space;
const mapping::DomainMapper* mapper;
long nodes_sequence;
std::array<const discretization::backend::mfem::DofMap*, ColumnCount> columns;
/// Basis data changes when either the reference mesh or any coefficient binding changes.
bool operator==(const BasisBindings&) const = default;
};
/// Borrowed discretization objects must outlive their graph and its accepted results.
template <typename TestT, typename SchemaT>
struct VolumeDiscretization
{
using RowBinding = discretization::backend::mfem::TermBinding<TestT, SchemaT>;
using DisplacementBinding = discretization::backend::mfem::TermBinding<discretization::blocks::displacement::vector, SchemaT>;
const RowBinding& residual_binding;
const DisplacementBinding& displacement_binding;
const ::mfem::GridFunction& compactification;
const mapping::DomainMapper& mapper;
const ::mfem::FiniteElementSpace* compactification_space{compactification.FESpace()};
long compactification_sequence{compactification_space == nullptr ? -1 : compactification_space->GetSequence()};
};
template <typename TestT, typename SchemaT>
VolumeDiscretization(const discretization::backend::mfem::TermBinding<TestT, SchemaT>&,
const discretization::backend::mfem::TermBinding<discretization::blocks::displacement::vector, SchemaT>&, const ::mfem::GridFunction&,
const mapping::DomainMapper&) -> VolumeDiscretization<TestT, SchemaT>;
/// Numerical choices fixed when the graph is constructed.
struct VolumeOptions
{
/// @todo Change this default. The automatic rule (operators/backend/mfem/quadrature.hpp: equation_rule) adds
/// OrderW() of the geometry (17 for geometry order 6) plus 3 (p_disp - 1) to every form, giving ~order 35
/// (5,832 points per hexahedron) on the stellar meshes. A fixed discretization order of 11 (216 points) gave
/// the same ESTER/TOP agreement for the n=2, J=0.05 benchmark at 0.96 GiB instead of 13.6 GB
/// (profiling/results/FINDINGS.md, sections 14-15); complete_sandbox already passes it explicitly. Changing
/// the library default changes every solve, so test expectations (tolerances, iteration counts, stored
/// reference values) may need updating, and a fixed order should be checked against higher field orders.
discretization::quadrature::Policy quadrature_policy{{}};
ContractionMode contraction_mode{ContractionMode::automatic};
};
/// A stage owns immutable element results, shared with downstream stages and accepted linearizations.
template <typename ElementT>
using ElementBatch = std::vector<std::shared_ptr<const ElementT>>;
/// Reference samples remain available for derivatives through the physical mapping.
struct OperandSamples
{
ElementBatch<::mfem::Vector> reference;
ElementBatch<::mfem::Vector> physical;
};
template <std::size_t OperandCount>
struct ContributionData
{
ElementBatch<ElementData<OperandCount>> elements;
double residual_offset{0.0};
};
/// Stable equation ownership also retains temporary and move-only contribution trees.
template <VolumeEquation EquationT, typename SchemaT>
struct VolumeConfiguration
{
using DisplacementBinding = typename VolumeDiscretization<typename EquationT::Test, SchemaT>::DisplacementBinding;
std::shared_ptr<const EquationT> equation;
VolumeDiscretization<typename EquationT::Test, SchemaT> discretization;
VolumeOptions options;
};
/// One accepted result contains ordered contribution batches and the assembled residual.
template <VolumeEquation EquationT, typename SchemaT>
struct VolumeData
{
using Equation = EquationT;
using Schema = SchemaT;
using Plan = EquationPlan<EquationT>;
std::shared_ptr<const VolumeConfiguration<EquationT, SchemaT>> configuration;
BasisBindings<Plan::column_count> bindings;
std::array<ElementBatch<ElementData<Plan::operand_count>>, std::tuple_size_v<equations::ContributionsOf<EquationT>>> contributions;
std::array<ElementBatch<::mfem::DenseMatrix>, std::tuple_size_v<equations::ContributionsOf<EquationT>>> geometry_derivatives;
::mfem::Vector residual;
};
/// Scratch belongs to graph callables and is never a dependency source.
struct VolumeScratch
{
Workspace workspace;
TensorBasisCache basis;
};
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,570 @@
#pragma once
#include <algorithm>
#include <concepts>
#include <functional>
#include <memory>
#include <vector>
#include "serif/backend/mfem/discretization/element_coordinates.hpp"
#include "serif/backend/mfem/operators/linearized_block.hpp"
#include "serif/backend/mfem/operators/volume_application.hpp"
#include "serif/backend/mfem/shared/parallel.hpp"
namespace serif::operators::backend::mfem {
namespace detail {
/** @brief Ignore a structurally absent field before gathering coefficients or touching contribution bases. */
template <typename Field, typename Equation, typename Schema, typename Visit>
void for_each_field_element(const VolumeData<Equation, Schema>& data, Visit&& visit) {
for_each_volume_element(data, [&]<typename Contribution>(const auto& element) {
constexpr bool reads_field = []<typename... Operands>(utils::types::TypeList<Operands...>) {
return (std::same_as<Field, typename Operands::term_type> || ...);
}(typename Contribution::Operands{});
if constexpr (reads_field) {
visit.template operator()<Contribution>(element);
}
});
}
/** @brief Contract only the requested physical field; geometry is frozen in this accepted linearization. */
template <typename Field, typename Equation, typename Contribution>
void field_integrand(const ElementData<EquationPlan<Equation>::operand_count>& element, const ::mfem::Vector& direction, ::mfem::Vector& integrand, ::mfem::Vector& samples, BasisWorkspace& workspace) {
using Plan = EquationPlan<Equation>;
using Test = OperandEvaluator<equations::TestOperandOf<Contribution>>;
integrand.SetSize(element.basis->rule->GetNPoints() * Test::components);
integrand = 0.0;
Plan::template for_each_input<Contribution>([&]<typename Operand>(Operand, const std::size_t index) {
if constexpr (std::same_as<typename Operand::term_type, Field>) {
using Evaluator = OperandEvaluator<Operand>;
Evaluator::evaluate(element.geometry->operand_basis[index], direction, samples, workspace);
for (int point = 0; point < element.basis->rule->GetNPoints(); ++point) {
for (int output = 0; output < Test::components; ++output) {
const int row = point * Test::components + output;
for (int input = 0; input < Evaluator::components; ++input) {
integrand[row] += element.weighted_field_derivatives(row, Plan::template component_offset<Operand>() + input) * samples[point * Evaluator::components + input];
}
}
}
}
});
}
/** @brief Locate a physical field in the equation's unique coefficient columns. */
template <typename Field, typename Equation>
[[nodiscard]] consteval std::size_t field_column() {
return equations::OperandValues<typename EquationPlan<Equation>::Columns>::template index<Field>();
}
/** @brief Retain accepted data while applying a single field coupling with reusable contraction scratch. */
template <typename Field, VolumeEquation Equation, typename Schema>
class VolumeFieldOperator final : public ::mfem::Operator {
public:
/** @brief Borrowed discretization resources still follow the accepted volume result's validity contract. */
explicit VolumeFieldOperator(std::shared_ptr<const VolumeData<Equation, Schema>> data) : ::mfem::Operator(volume_height(*data), data->bindings.columns[field_column<Field, Equation>()]->size()), m_data(std::move(data)) {
m_input.SetSize(width);
m_full.SetSize(m_data->configuration->discretization.residual_binding.full_size());
}
/**
* @brief Apply selected field derivatives without evaluating zero geometry variations.
* @note Element results are computed concurrently, each into its own vector, then added into the
* residual one by one in the original element order, so every sum is unchanged.
*/
void Mult(const ::mfem::Vector& input, ::mfem::Vector& output) const override {
using namespace utils::errors;
validate_volume_data(*m_data);
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(input.Size() == width, "A physical field block received {} coefficients; expected {}.", input.Size(), width);
m_input = input;
m_full = 0.0;
const auto& row = m_data->configuration->discretization.residual_binding;
const auto& column = *m_data->bindings.columns[field_column<Field, Equation>()];
utils::CompensatedSum global_sum;
const auto row_elements = row.validated_elements();
const auto column_elements = column.validated_elements();
m_actions.clear();
m_elements.clear();
detail::for_each_field_element<Field>(*m_data, [&]<typename Contribution>(const auto& element) {
using Test = OperandEvaluator<equations::TestOperandOf<Contribution>>;
m_elements.push_back(element.element);
m_actions.emplace_back([&element, &column_elements, this](Scratch& local, ::mfem::Vector& result) {
column_elements.element_values(element.element, m_input, local.direction);
field_integrand<Field, Equation, Contribution>(element, local.direction, local.integrand, local.samples, local.workspace);
Test::integrate(element.geometry->test_basis, local.integrand, result, local.workspace);
});
});
run_actions();
for (std::size_t index = 0; index < m_actions.size(); ++index) {
if constexpr (discretization::blocks::GlobalScalarTerm<typename Equation::Test>) {
global_sum.add(m_results[index][0]);
} else {
row_elements.add_element(m_elements[index], m_results[index], m_full);
}
}
if constexpr (discretization::blocks::GlobalScalarTerm<typename Equation::Test>) {
m_full[0] = global_sum.value();
}
row.accumulate_dual(m_full, output);
row.validate_values(output);
}
/** @brief Apply the exact coefficient transpose of the selected physical field coupling (concurrent elements, ordered sums, as Mult). */
void MultTranspose(const ::mfem::Vector& input, ::mfem::Vector& output) const override {
using namespace utils::errors;
using Plan = EquationPlan<Equation>;
validate_volume_data(*m_data);
serif_ensure<SERiFErrorCode::INVALID_DOF_COUNT>(input.Size() == height, "A physical field transpose received {} coefficients; expected {}.", input.Size(), height);
m_dual = input;
const auto& row = m_data->configuration->discretization.residual_binding;
const auto& column = *m_data->bindings.columns[field_column<Field, Equation>()];
m_full_transpose.SetSize(column.full_size());
m_full_transpose = 0.0;
utils::CompensatedSum global_sum;
const auto row_elements = row.validated_elements();
const auto column_elements = column.validated_elements();
m_actions.clear();
m_elements.clear();
detail::for_each_field_element<Field>(*m_data, [&]<typename Contribution>(const auto& element) {
using Test = OperandEvaluator<equations::TestOperandOf<Contribution>>;
// One action per field operand, in operand order, each recomputing the element's test samples.
Plan::template for_each_input<Contribution>([&]<typename Operand>(Operand, const std::size_t index) {
if constexpr (std::same_as<typename Operand::term_type, Field>) {
m_elements.push_back(element.element);
m_actions.emplace_back([&element, &row_elements, index, this](Scratch& local, ::mfem::Vector& result) {
using Evaluator = OperandEvaluator<Operand>;
row_elements.element_values(element.element, m_dual, local.direction);
element.geometry->test_basis.evaluate(local.direction, local.test_samples, local.workspace);
local.integrand.SetSize(element.basis->rule->GetNPoints() * Evaluator::components);
local.integrand = 0.0;
for (int point = 0; point < element.basis->rule->GetNPoints(); ++point) {
for (int field_component = 0; field_component < Evaluator::components; ++field_component) {
for (int test_component = 0; test_component < Test::components; ++test_component) {
const int test_index = point * Test::components + test_component;
local.integrand[point * Evaluator::components + field_component] += element.weighted_field_derivatives(test_index,
Plan::template component_offset<Operand>() + field_component) *
local.test_samples[test_index];
}
}
}
Evaluator::integrate(element.geometry->operand_basis[index], local.integrand, result, local.workspace);
});
}
});
});
run_actions();
for (std::size_t index = 0; index < m_actions.size(); ++index) {
if constexpr (discretization::blocks::GlobalScalarTerm<Field>) {
global_sum.add(m_results[index][0]);
} else {
column_elements.add_element(m_elements[index], m_results[index], m_full_transpose);
}
}
if constexpr (discretization::blocks::GlobalScalarTerm<Field>) {
m_full_transpose[0] = global_sum.value();
}
column.accumulate_dual(m_full_transpose, output);
column.validate_values(output);
}
private:
/** @brief One worker's element scratch. */
struct Scratch {
::mfem::Vector direction, integrand, samples, test_samples;
BasisWorkspace workspace;
};
/** @brief Run the collected element actions concurrently, each into its own result. */
void run_actions() const {
m_results.resize(m_actions.size());
m_scratch.resize(static_cast<std::size_t>(std::max(1, utils::parallel::worker_count())));
utils::parallel::for_each_index(static_cast<int>(m_actions.size()), [&](const int index, const int worker) {
m_actions[static_cast<std::size_t>(index)](m_scratch[static_cast<std::size_t>(worker)], m_results[static_cast<std::size_t>(index)]);
});
}
std::shared_ptr<const VolumeData<Equation, Schema>> m_data;
mutable ::mfem::Vector m_input, m_full;
mutable ::mfem::Vector m_dual, m_full_transpose;
mutable std::vector<std::function<void(Scratch&, ::mfem::Vector&)>> m_actions;
mutable std::vector<int> m_elements;
mutable std::vector<::mfem::Vector> m_results;
mutable std::vector<Scratch> m_scratch;
};
} // namespace detail
namespace detail {
/** @brief One element's probed block: column c's integrated values for every local row, column-major. */
struct ProbedElementBlock {
std::vector<int> rows; ///< Reduced row coordinate of each local row, or -1 when unsupported.
std::vector<int> columns; ///< Reduced column coordinate of each local column, or -1 when skipped.
std::vector<double> values;
};
/** @brief Per-worker scratch for probing element blocks. */
struct ProbeScratch {
::mfem::Vector direction, integrand, samples, values;
BasisWorkspace workspace;
};
/** @brief Weighted reduced coordinates of each local coordinate, as CSR: local i owns [offsets[i], offsets[i+1]). */
struct CoefficientExpansion {
std::vector<int> offsets{0};
std::vector<int> indices;
std::vector<double> weights;
};
/** @brief One element's probed block on constrained bindings: column-major values and both constraint expansions. */
struct ConstrainedElementBlock {
int row_count{0};
std::vector<double> values;
CoefficientExpansion rows;
CoefficientExpansion columns;
};
/** @brief Write MFEM-style CSR rows (already in Finalize() order) into an owning SparseMatrix. */
[[nodiscard]] inline std::shared_ptr<const ::mfem::SparseMatrix> make_csr(
const std::vector<std::vector<int>>& row_columns, const std::vector<std::vector<double>>& row_values, const int height, const int width
) {
int* I = new int[static_cast<std::size_t>(height) + 1];
I[0] = 0;
for (int row = 0; row < height; ++row) { I[row + 1] = I[row] + static_cast<int>(row_columns[static_cast<std::size_t>(row)].size()); }
int* J = new int[static_cast<std::size_t>(I[height])];
double* A = new double[static_cast<std::size_t>(I[height])];
bool sorted = true;
for (int row = 0; row < height; ++row) {
int last_column = -1;
const auto& columns = row_columns[static_cast<std::size_t>(row)];
const auto& values = row_values[static_cast<std::size_t>(row)];
for (std::size_t k = 0; k < columns.size(); ++k) {
J[I[row] + static_cast<int>(k)] = columns[k];
A[I[row] + static_cast<int>(k)] = values[k];
if (last_column > columns[k]) { sorted = false; }
last_column = columns[k];
}
}
return std::make_shared<::mfem::SparseMatrix>(I, J, A, height, width, true, true, sorted);
}
/**
* @brief SparseMatrix::Add for every probed entry followed by Finalize(), built without MFEM's linked rows.
* @note MFEM keeps each row as a list with the newest column first, adds every value in insertion order
* starting from 0.0, and Finalize() writes the list head first while dropping exact zeros (a zero diagonal
* finds itself and is dropped too). Here each row replays its own insertions in the original global order
* (element, then column, then local row), so every sum, the column order and the dropped entries match.
* Rows are independent, so they are built concurrently.
*/
[[nodiscard]] inline std::shared_ptr<const ::mfem::SparseMatrix> finalize_probed_blocks(
const std::vector<ProbedElementBlock>& blocks, const int height, const int width
) {
// Row -> (block, local row) in insertion order: blocks ascending, then local rows ascending.
std::vector<int> offsets(static_cast<std::size_t>(height) + 1, 0);
for (const auto& block : blocks) {
for (const int row : block.rows) {
if (row >= 0) { ++offsets[static_cast<std::size_t>(row) + 1]; }
}
}
for (int row = 0; row < height; ++row) { offsets[row + 1] += offsets[row]; }
std::vector<int> incidence_block(static_cast<std::size_t>(offsets.back()));
std::vector<int> incidence_row(static_cast<std::size_t>(offsets.back()));
{
std::vector<int> cursor(offsets.begin(), offsets.end() - 1);
for (int block = 0; block < static_cast<int>(blocks.size()); ++block) {
const auto& rows = blocks[static_cast<std::size_t>(block)].rows;
for (int local = 0; local < static_cast<int>(rows.size()); ++local) {
if (rows[local] >= 0) {
const int slot = cursor[static_cast<std::size_t>(rows[local])]++;
incidence_block[static_cast<std::size_t>(slot)] = block;
incidence_row[static_cast<std::size_t>(slot)] = local;
}
}
}
}
struct RowScratch {
std::vector<int> slot_of_column;
std::vector<int> columns;
std::vector<double> values;
};
std::vector<std::vector<int>> row_columns(static_cast<std::size_t>(height));
std::vector<std::vector<double>> row_values(static_cast<std::size_t>(height));
std::vector<RowScratch> scratch(static_cast<std::size_t>(utils::parallel::worker_count()));
utils::parallel::for_each_index(height, [&](const int row, const int worker) {
RowScratch& local = scratch[static_cast<std::size_t>(worker)];
local.slot_of_column.resize(static_cast<std::size_t>(width), -1);
local.columns.clear();
local.values.clear();
for (int entry = offsets[row]; entry < offsets[row + 1];) {
// Insertions for one block come column by column, each column visiting its local rows in order.
const int block_index = incidence_block[static_cast<std::size_t>(entry)];
int end = entry;
while (end < offsets[row + 1] && incidence_block[static_cast<std::size_t>(end)] == block_index) { ++end; }
const auto& block = blocks[static_cast<std::size_t>(block_index)];
const int block_rows = static_cast<int>(block.rows.size());
for (int column = 0; column < static_cast<int>(block.columns.size()); ++column) {
const int column_index = block.columns[static_cast<std::size_t>(column)];
if (column_index < 0) { continue; }
int& slot = local.slot_of_column[static_cast<std::size_t>(column_index)];
for (int k = entry; k < end; ++k) {
if (slot < 0) {
slot = static_cast<int>(local.columns.size());
local.columns.push_back(column_index);
local.values.push_back(0.0);
}
local.values[static_cast<std::size_t>(slot)] += block.values[static_cast<std::size_t>(column * block_rows + incidence_row[static_cast<std::size_t>(k)])];
}
}
entry = end;
}
auto& columns = row_columns[static_cast<std::size_t>(row)];
auto& values = row_values[static_cast<std::size_t>(row)];
for (std::size_t slot = local.columns.size(); slot-- > 0;) {
local.slot_of_column[static_cast<std::size_t>(local.columns[slot])] = -1;
if (local.values[slot] == 0.0) { continue; }
columns.push_back(local.columns[slot]);
values.push_back(local.values[slot]);
}
});
return make_csr(row_columns, row_values, height, width);
}
/**
* @brief The constrained form of finalize_probed_blocks: SparseMatrix::Add(row_index, column_index,
* row_weight * value * column_weight) for every element, column, local row, row coefficient and column
* coefficient in that order, followed by Finalize().
* @note Each global row replays its own insertions in the original order (see finalize_probed_blocks), so
* every sum, the column order and the dropped exact zeros match MFEM's linked-row assembly.
*/
[[nodiscard]] inline std::shared_ptr<const ::mfem::SparseMatrix> finalize_constrained_blocks(
const std::vector<ConstrainedElementBlock>& blocks, const int height, const int width
) {
// Row -> (block, local row, weight) in insertion order: blocks, then local rows, then their coefficients.
std::vector<int> offsets(static_cast<std::size_t>(height) + 1, 0);
for (const auto& block : blocks) {
for (const int row : block.rows.indices) { ++offsets[static_cast<std::size_t>(row) + 1]; }
}
for (int row = 0; row < height; ++row) { offsets[row + 1] += offsets[row]; }
std::vector<int> incidence_block(static_cast<std::size_t>(offsets.back()));
std::vector<int> incidence_row(static_cast<std::size_t>(offsets.back()));
std::vector<double> incidence_weight(static_cast<std::size_t>(offsets.back()));
{
std::vector<int> cursor(offsets.begin(), offsets.end() - 1);
for (int block = 0; block < static_cast<int>(blocks.size()); ++block) {
const auto& rows = blocks[static_cast<std::size_t>(block)].rows;
for (int local = 0; local + 1 < static_cast<int>(rows.offsets.size()); ++local) {
for (int k = rows.offsets[static_cast<std::size_t>(local)]; k < rows.offsets[static_cast<std::size_t>(local) + 1]; ++k) {
const int slot = cursor[static_cast<std::size_t>(rows.indices[static_cast<std::size_t>(k)])]++;
incidence_block[static_cast<std::size_t>(slot)] = block;
incidence_row[static_cast<std::size_t>(slot)] = local;
incidence_weight[static_cast<std::size_t>(slot)] = rows.weights[static_cast<std::size_t>(k)];
}
}
}
}
struct RowScratch {
std::vector<int> slot_of_column;
std::vector<int> columns;
std::vector<double> values;
};
std::vector<std::vector<int>> row_columns(static_cast<std::size_t>(height));
std::vector<std::vector<double>> row_values(static_cast<std::size_t>(height));
std::vector<RowScratch> scratch(static_cast<std::size_t>(utils::parallel::worker_count()));
utils::parallel::for_each_index(height, [&](const int row, const int worker) {
RowScratch& local = scratch[static_cast<std::size_t>(worker)];
local.slot_of_column.resize(static_cast<std::size_t>(width), -1);
local.columns.clear();
local.values.clear();
for (int entry = offsets[row]; entry < offsets[row + 1];) {
const int block_index = incidence_block[static_cast<std::size_t>(entry)];
int end = entry;
while (end < offsets[row + 1] && incidence_block[static_cast<std::size_t>(end)] == block_index) { ++end; }
const auto& block = blocks[static_cast<std::size_t>(block_index)];
const auto& columns = block.columns;
for (int column = 0; column + 1 < static_cast<int>(columns.offsets.size()); ++column) {
for (int k = entry; k < end; ++k) {
const double value = block.values[static_cast<std::size_t>(column) * static_cast<std::size_t>(block.row_count) +
static_cast<std::size_t>(incidence_row[static_cast<std::size_t>(k)])];
const double row_weight = incidence_weight[static_cast<std::size_t>(k)];
for (int c = columns.offsets[static_cast<std::size_t>(column)]; c < columns.offsets[static_cast<std::size_t>(column) + 1]; ++c) {
int& slot = local.slot_of_column[static_cast<std::size_t>(columns.indices[static_cast<std::size_t>(c)])];
if (slot < 0) {
slot = static_cast<int>(local.columns.size());
local.columns.push_back(columns.indices[static_cast<std::size_t>(c)]);
local.values.push_back(0.0);
}
// A separate statement keeps the product unfused, as when it was passed to SparseMatrix::Add.
const double contribution = row_weight * value * columns.weights[static_cast<std::size_t>(c)];
local.values[static_cast<std::size_t>(slot)] += contribution;
}
}
}
entry = end;
}
auto& columns = row_columns[static_cast<std::size_t>(row)];
auto& values = row_values[static_cast<std::size_t>(row)];
for (std::size_t slot = local.columns.size(); slot-- > 0;) {
local.slot_of_column[static_cast<std::size_t>(local.columns[slot])] = -1;
if (local.values[slot] == 0.0) { continue; }
columns.push_back(local.columns[slot]);
values.push_back(local.values[slot]);
}
});
return make_csr(row_columns, row_values, height, width);
}
} // namespace detail
/** @brief Assemble a selected field block from accepted weighted derivatives, never by probing the whole system. */
template <typename Field, VolumeEquation Equation, typename Schema>
[[nodiscard]] std::shared_ptr<const ::mfem::SparseMatrix> assemble_volume_field(const VolumeData<Equation, Schema>& data) {
validate_volume_data(data);
const auto& row_binding = data.configuration->discretization.residual_binding;
const auto& column_binding = *data.bindings.columns[detail::field_column<Field, Equation>()];
if (!row_binding.constrained() && !column_binding.constrained()) {
// Probe elements concurrently (the same operations per column as below), then build the rows.
const auto row_elements = row_binding.validated_elements();
const auto column_elements = column_binding.validated_elements();
std::vector<std::function<void(detail::ProbedElementBlock&, detail::ProbeScratch&)>> probes;
detail::for_each_field_element<Field>(data, [&]<typename Contribution>(const auto& element) {
probes.emplace_back([&row_elements, &column_elements, &element](detail::ProbedElementBlock& block, detail::ProbeScratch& scratch) {
using Test = OperandEvaluator<equations::TestOperandOf<Contribution>>;
const auto rows = row_elements.coordinates(element.element);
const auto columns = column_elements.coordinates(element.element);
block.rows.resize(static_cast<std::size_t>(rows.size()));
block.columns.resize(static_cast<std::size_t>(columns.size()));
block.values.assign(static_cast<std::size_t>(rows.size()) * static_cast<std::size_t>(columns.size()), 0.0);
for (int row = 0; row < rows.size(); ++row) {
block.rows[static_cast<std::size_t>(row)] = rows.reduced_dof(row) >= 0 ? rows.reduced_dof(row) : -1;
}
for (int column = 0; column < columns.size(); ++column) {
block.columns[static_cast<std::size_t>(column)] = columns.reduced_dof(column) >= 0 ? columns.reduced_dof(column) : -1;
if (columns.reduced_dof(column) < 0) {
continue;
}
columns.unit_direction(column, scratch.direction);
detail::field_integrand<Field, Equation, Contribution>(element, scratch.direction, scratch.integrand, scratch.samples, scratch.workspace);
Test::integrate(element.geometry->test_basis, scratch.integrand, scratch.values, scratch.workspace);
rows.orient_dual(scratch.values);
for (int row = 0; row < rows.size(); ++row) {
block.values[static_cast<std::size_t>(column) * static_cast<std::size_t>(rows.size()) + static_cast<std::size_t>(row)] = scratch.values[row];
}
}
});
});
std::vector<detail::ProbedElementBlock> blocks(probes.size());
std::vector<detail::ProbeScratch> scratch(static_cast<std::size_t>(utils::parallel::worker_count()));
utils::parallel::for_each_index(static_cast<int>(probes.size()), [&](const int index, const int worker) {
probes[static_cast<std::size_t>(index)](blocks[static_cast<std::size_t>(index)], scratch[static_cast<std::size_t>(worker)]);
});
return detail::finalize_probed_blocks(blocks, row_binding.size(), column_binding.size());
}
// Hanging-node constraints distribute each local entry over weighted reduced coordinates. Probe elements
// concurrently, then replay the constrained insertions row by row in their original order.
const auto row_elements = row_binding.validated_elements();
const auto column_elements = column_binding.validated_elements();
const bool skip_unsupported_columns = !column_binding.constrained();
std::vector<std::function<void(detail::ConstrainedElementBlock&, detail::ProbeScratch&)>> probes;
detail::for_each_field_element<Field>(data, [&]<typename Contribution>(const auto& element) {
probes.emplace_back([&row_elements, &column_elements, &element, skip_unsupported_columns](detail::ConstrainedElementBlock& block, detail::ProbeScratch& scratch) {
using Test = OperandEvaluator<equations::TestOperandOf<Contribution>>;
const auto rows = row_elements.coordinates(element.element);
const auto columns = column_elements.coordinates(element.element);
const auto expand = [](const auto& coordinates, const int local, detail::CoefficientExpansion& expansion) {
coordinates.for_each_coefficient(local, [&](const int index, const double weight) {
expansion.indices.push_back(index);
expansion.weights.push_back(weight);
});
expansion.offsets.push_back(static_cast<int>(expansion.indices.size()));
};
block.row_count = rows.size();
block.values.assign(static_cast<std::size_t>(rows.size()) * static_cast<std::size_t>(columns.size()), 0.0);
for (int row = 0; row < rows.size(); ++row) {
expand(rows, row, block.rows);
}
for (int column = 0; column < columns.size(); ++column) {
if (skip_unsupported_columns && columns.reduced_dof(column) < 0) {
block.columns.offsets.push_back(static_cast<int>(block.columns.indices.size()));
continue;
}
expand(columns, column, block.columns);
columns.unit_direction(column, scratch.direction);
detail::field_integrand<Field, Equation, Contribution>(element, scratch.direction, scratch.integrand, scratch.samples, scratch.workspace);
Test::integrate(element.geometry->test_basis, scratch.integrand, scratch.values, scratch.workspace);
rows.orient_dual(scratch.values);
for (int row = 0; row < rows.size(); ++row) {
block.values[static_cast<std::size_t>(column) * static_cast<std::size_t>(rows.size()) + static_cast<std::size_t>(row)] = scratch.values[row];
}
}
});
});
std::vector<detail::ConstrainedElementBlock> blocks(probes.size());
std::vector<detail::ProbeScratch> scratch(static_cast<std::size_t>(utils::parallel::worker_count()));
utils::parallel::for_each_index(static_cast<int>(probes.size()), [&](const int index, const int worker) {
probes[static_cast<std::size_t>(index)](blocks[static_cast<std::size_t>(index)], scratch[static_cast<std::size_t>(worker)]);
});
return detail::finalize_constrained_blocks(blocks, row_binding.size(), column_binding.size());
}
/** @brief Extract exact physical diagonals, retaining constraint cross terms when hanging nodes are present. */
template <typename Field, VolumeEquation Equation, typename Schema>
[[nodiscard]] std::vector<double> volume_field_diagonal(const VolumeData<Equation, Schema>& data) {
using namespace utils::errors;
validate_volume_data(data);
const auto& row_binding = data.configuration->discretization.residual_binding;
const auto& column_binding = *data.bindings.columns[detail::field_column<Field, Equation>()];
serif_ensure<SERiFErrorCode::MATRIX_SHAPE_ERROR>(row_binding.size() == column_binding.size(), "Physical diagonal extraction requires a square selected block.");
if (row_binding.constrained() || column_binding.constrained()) {
// Off-diagonal local entries also contribute to diag(P^T A P).
const auto matrix = assemble_volume_field<Field>(data);
::mfem::Vector diagonal;
matrix->GetDiag(diagonal);
return std::vector<double>(diagonal.GetData(), diagonal.GetData() + diagonal.Size());
}
std::vector<double> diagonal(row_binding.size(), 0.0);
::mfem::Vector direction, row_direction, integrand, samples, test_samples;
BasisWorkspace workspace;
const auto row_elements = row_binding.validated_elements();
const auto column_elements = column_binding.validated_elements();
detail::for_each_field_element<Field>(data, [&]<typename Contribution>(const auto& element) {
const auto rows = row_elements.coordinates(element.element);
const auto columns = column_elements.coordinates(element.element);
for (int column = 0; column < columns.size(); ++column) {
const int index = columns.reduced_dof(column);
if (index < 0) {
continue;
}
const int row = rows.local_dof(index);
if (row < 0) {
continue;
}
columns.unit_direction(column, direction);
rows.unit_direction(row, row_direction);
detail::field_integrand<Field, Equation, Contribution>(element, direction, integrand, samples, workspace);
element.geometry->test_basis.evaluate(row_direction, test_samples, workspace);
diagonal[index] += test_samples * integrand;
}
});
return diagonal;
}
/** @brief Expose efficient physical block operations from an owning accepted dependency result. */
template <typename Field, VolumeEquation Equation, typename Schema>
[[nodiscard]] LinearizedBlock volume_field_block(std::shared_ptr<const VolumeData<Equation, Schema>> data) {
utils::errors::serif_ensure<utils::errors::SERiFErrorCode::INVALID_ARGUMENT>(data != nullptr, "A physical block needs an accepted volume linearization.");
auto action = std::make_shared<detail::VolumeFieldOperator<Field, Equation, Schema>>(data);
return LinearizedBlock(std::move(action), [data]() {
return volume_field_diagonal<Field>(*data);
}, [data]() {
return assemble_volume_field<Field>(*data);
});
}
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,151 @@
#pragma once
#include "serif/backend/mfem/operators/volume_data.hpp"
#include "serif/backend/mfem/discretization/coefficient_tracking.hpp"
namespace serif::operators::backend::mfem
{
using discretization::backend::mfem::CoefficientTracking;
/** @brief Validate external mesh resources before the graph compares their metadata. */
struct MetadataTracking {
/** @brief Reject stale contexts even when the caller bypasses the ordinary input factory. */
template <std::size_t ColumnCount>
[[nodiscard]] BasisBindings<ColumnCount> capture(const BasisBindings<ColumnCount>& input) const {
using namespace utils::errors;
input.residual_binding->validate();
input.displacement_binding->validate();
for (const auto* column : input.columns) {
column->validate();
}
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR>(
input.residual_binding->mesh().GetNodesSequence() == input.nodes_sequence,
"The volume input context describes an earlier mesh revision; rebuild the inputs before preparing the graph.");
return input;
}
/** @brief Equal metadata can be reused only while its borrowed mesh and spaces remain valid. */
template <std::size_t ColumnCount>
[[nodiscard]] bool matches(const BasisBindings<ColumnCount>& captured, const BasisBindings<ColumnCount>& input) const {
return captured == capture(input);
}
/** @brief Metadata already contains persistent identities; temporary wrapper addresses are irrelevant. */
template <typename T>
[[nodiscard]] const void* identity(const T&) const {
return nullptr;
}
};
using MeshAndCoefficientBindings = volume_keys::Scoped<void>::Bindings;
struct DisplacementCoefficients
{
};
struct CompactificationCoefficients
{
};
/// Check borrowed mesh objects before sampling or applying accepted results.
template <typename TestT, typename SchemaT>
void validate_volume_discretization(const VolumeDiscretization<TestT, SchemaT>& discretization)
{
using namespace utils::errors;
discretization.residual_binding.validate();
discretization.displacement_binding.validate();
const auto* space = discretization.compactification_space;
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR>(space != nullptr && discretization.compactification.FESpace() == space &&
space->GetSequence() == discretization.compactification_sequence &&
discretization.compactification.Size() == space->GetVSize(),
"Compactification binding changed unexpectedly.");
serif_ensure<SERiFErrorCode::MISMATCHED_MESHING_MAPS, std::invalid_argument>(
space->GetMesh() == &discretization.residual_binding.mesh() && discretization.displacement_binding.space().GetMesh() == &discretization.residual_binding.mesh(),
"Term bindings must share a mesh.");
serif_ensure<SERiFErrorCode::INVALID_DIMENSION, std::invalid_argument>(discretization.mapper.get_dimension() == 3 && discretization.compactification.VectorDim() == 1,
"Equation mapping requires a three-dimensional mapper and scalar compactification.");
}
/** @brief Validate one equation's bindings without constructing duplicate coefficient resources. */
template <VolumeEquation EquationT, typename Scope = void, typename SchemaT, typename CoefficientsT, typename... Columns>
[[nodiscard]] auto make_volume_binding_input(const VolumeDiscretization<typename EquationT::Test, SchemaT>& discretization, const CoefficientsT& term_coefficients,
utils::types::TypeList<Columns...>)
{
using namespace utils::dependencies;
using namespace utils::errors;
validate_volume_discretization(discretization);
const auto validate_term = [&]<typename OperandT>(OperandT)
{
const auto& input = term_coefficients.template get<typename OperandT::term_type>();
static_assert(std::same_as<typename std::remove_cvref_t<decltype(input.binding)>::Schema, SchemaT>, "Term bindings must use the same domain schema.");
input.binding.validate_values(input.values);
serif_ensure<SERiFErrorCode::MISMATCHED_MESHING_MAPS, std::invalid_argument>(&input.binding.mesh() == &discretization.residual_binding.mesh(),
"Operator inputs must share the residual mesh.");
};
(validate_term(Columns{}), ...);
const BasisBindings<sizeof...(Columns)> bindings{.residual_binding = &discretization.residual_binding,
.displacement_binding = &discretization.displacement_binding,
.compactification_space = discretization.compactification_space,
.mapper = &discretization.mapper,
.nodes_sequence = discretization.residual_binding.mesh().GetNodesSequence(),
.columns = {&term_coefficients.template get<typename Columns::term_type>().binding...}};
return own<typename volume_keys::Scoped<Scope>::Bindings>(bindings, MetadataTracking{});
}
/** @brief Share equation metadata with a model's common physical coefficient context. */
template <VolumeEquation EquationT, typename Scope = void, typename SchemaT, typename CoefficientsT>
[[nodiscard]] auto make_volume_binding_input(const VolumeDiscretization<typename EquationT::Test, SchemaT>& discretization, const CoefficientsT& term_coefficients)
{
return make_volume_binding_input<EquationT, Scope>(discretization, term_coefficients, typename EquationPlan<EquationT>::Columns{});
}
/** @brief Supply precisely the consumed terms for a standalone equation. */
template <VolumeEquation EquationT, typename Scope = void, typename SchemaT, typename CoefficientsT, typename... Columns>
[[nodiscard]] auto make_volume_inputs(const VolumeDiscretization<typename EquationT::Test, SchemaT>& discretization, const CoefficientsT& term_coefficients, utils::types::TypeList<Columns...> columns)
{
using namespace utils::dependencies;
const auto bindings = make_volume_binding_input<EquationT, Scope>(discretization, term_coefficients, columns);
return make_dependency_context(
bindings,
own<CompactificationCoefficients>(MappingCoefficients{.binding = *discretization.compactification_space, .values = discretization.compactification},
CoefficientTracking{}),
own<typename Columns::term_type>(term_coefficients.template get<typename Columns::term_type>(), CoefficientTracking{})...);
}
/** @brief Bind equation and reference inputs when displacement will be produced inside the dependency graph. */
template <VolumeEquation EquationT, typename Scope = void, typename SchemaT, typename CoefficientsT>
[[nodiscard]] auto make_volume_inputs(const VolumeDiscretization<typename EquationT::Test, SchemaT>& discretization, const CoefficientsT& term_coefficients)
{
return make_volume_inputs<EquationT, Scope>(discretization, term_coefficients, typename EquationPlan<EquationT>::Columns{});
}
/** @brief Append a checked external displacement while preserving each original resource's tracking policy. */
template <VolumeEquation EquationT, typename Scope = void, typename SchemaT, typename CoefficientsT, typename... Columns>
[[nodiscard]] auto make_volume_inputs(const VolumeDiscretization<typename EquationT::Test, SchemaT>& discretization, const CoefficientsT& term_coefficients,
const ::mfem::Vector& displacement_coefficients, utils::types::TypeList<Columns...> columns)
{
using namespace utils::dependencies;
const auto inputs = make_volume_inputs<EquationT, Scope>(discretization, term_coefficients, columns);
discretization.displacement_binding.validate_values(displacement_coefficients);
return make_dependency_context(inputs.template resource<typename volume_keys::Scoped<Scope>::Bindings>(),
own<DisplacementCoefficients>(bind(discretization.displacement_binding, displacement_coefficients), CoefficientTracking{}),
inputs.template resource<CompactificationCoefficients>(), inputs.template resource<typename Columns::term_type>()...);
}
/// Build a checked source context without retaining the coefficient vectors.
template <VolumeEquation EquationT, typename Scope = void, typename SchemaT, typename CoefficientsT>
[[nodiscard]] auto make_volume_inputs(const VolumeDiscretization<typename EquationT::Test, SchemaT>& discretization, const CoefficientsT& term_coefficients,
const ::mfem::Vector& displacement_coefficients)
{
return make_volume_inputs<EquationT, Scope>(discretization, term_coefficients, displacement_coefficients, typename EquationPlan<EquationT>::Columns{});
}
template <VolumeEquation EquationT, typename Scope = void, typename SchemaT, typename CoefficientsT>
void make_volume_inputs(const VolumeDiscretization<typename EquationT::Test, SchemaT>&, const CoefficientsT&, ::mfem::Vector&&) = delete;
template <VolumeEquation EquationT, typename Scope = void, typename SchemaT, typename CoefficientsT>
void make_volume_inputs(const VolumeDiscretization<typename EquationT::Test, SchemaT>&, const CoefficientsT&, const ::mfem::Vector&&) = delete;
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,421 @@
#pragma once
#include <atomic>
#include <cmath>
#include <exception>
#include <expected>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include "serif/operators/contributions.hpp"
#include "serif/backend/mfem/operators/element_preparation.hpp"
#include "serif/backend/mfem/operators/element_evaluation.hpp"
#include "serif/backend/mfem/operators/geometry_derivatives.hpp"
#include "serif/backend/mfem/operators/preparation.hpp"
#include "serif/backend/mfem/operators/volume_data.hpp"
#include "serif/backend/mfem/shared/parallel.hpp"
#include "serif/utils/error/errors.hpp"
#include "serif/utils/types/strings.hpp"
namespace serif::operators::backend::mfem {
namespace detail {
/**
* @brief Build one immutable batch entry per index, concurrently, with one Workspace per worker.
* @note make(index, workspace) must depend only on its index and write only that workspace, so the
* batch is identical for every thread count. Like the serial loop, the result is the batch or the
* first failure in index order (a rejection, or a rethrown exception). Indices after a known
* failure are skipped; every index before the first failure always runs.
*/
template <typename ElementT, typename MakeT>
[[nodiscard]] std::expected<ElementBatch<ElementT>, PreparationRejection> build_element_batch(
const int count, Workspace& workspace, MakeT&& make
) {
struct Slot {
std::shared_ptr<const ElementT> value;
std::optional<PreparationRejection> rejection;
std::exception_ptr error;
};
std::vector<Slot> slots(static_cast<std::size_t>(count));
const int workers = ::serif::utils::parallel::worker_count();
std::vector<Workspace> worker_workspaces(static_cast<std::size_t>(workers > 1 ? workers - 1 : 0));
std::atomic<int> first_failure{count};
const auto fail = [&first_failure](const int index) {
int current = first_failure.load(std::memory_order_relaxed);
while (index < current && !first_failure.compare_exchange_weak(current, index, std::memory_order_relaxed)) {}
};
::serif::utils::parallel::for_each_index(count, [&](const int index, const int worker) {
if (index > first_failure.load(std::memory_order_relaxed)) {
return;
}
Workspace& local = worker == 0 ? workspace : worker_workspaces[static_cast<std::size_t>(worker - 1)];
Slot& slot = slots[static_cast<std::size_t>(index)];
try {
auto value = make(index, local);
if (value) {
slot.value = std::make_shared<const ElementT>(std::move(*value));
return;
}
slot.rejection = std::move(value.error());
} catch (...) {
slot.error = std::current_exception();
}
fail(index);
});
ElementBatch<ElementT> result;
result.reserve(slots.size());
for (auto& slot : slots) {
if (slot.error) {
std::rethrow_exception(slot.error);
}
if (slot.rejection) {
return std::unexpected(std::move(*slot.rejection));
}
result.push_back(std::move(slot.value));
}
return result;
}
}
/// Sample the reference discretization once for each supported element of this contribution.
template <VolumeEquation EquationT, typename SchemaT, typename ContributionT>
[[nodiscard]] std::expected<ElementBatch<ElementBasis<EquationPlan<EquationT>::operand_count>>, PreparationRejection> sample_contribution_basis(
const VolumeConfiguration<EquationT, SchemaT>& configuration,
const ContributionBinding<ContributionT>& contribution,
const BasisBindings<EquationPlan<EquationT>::column_count>& bindings,
TensorBasisCache& cache
) {
using namespace utils::errors;
using Plan = EquationPlan<EquationT>;
using Support = equations::SupportOf<ContributionT>;
const auto& discretization = configuration.discretization;
serif_ensure<SERiFErrorCode::CACHE_FE_BINDING_ERROR, std::invalid_argument>(
bindings.residual_binding == &discretization.residual_binding &&
bindings.displacement_binding == &discretization.displacement_binding &&
bindings.compactification_space == discretization.compactification_space &&
bindings.mapper == &discretization.mapper,
"Prepare this volume graph with the discretization used to construct it.");
ElementBatch<ElementBasis<Plan::operand_count>> result;
for (int element = 0; element < configuration.discretization.residual_binding.mesh().GetNE(); ++element) {
if (!SchemaT::template domain_id_belongs_to<Support>(configuration.discretization.residual_binding.mesh().GetAttribute(element))) {
continue;
}
serif_ensure<SERiFErrorCode::INVALID_DOMAIN_ERROR, std::invalid_argument>(configuration.discretization.residual_binding.supports(element), "Contribution support extends outside its residual row.");
Plan::template for_each_input<ContributionT>([&]<typename OperandT>(OperandT, std::size_t) {
serif_ensure<SERiFErrorCode::INVALID_DOMAIN_ERROR, std::invalid_argument>(bindings.columns[Plan::template column_index<OperandT>()]->supports(element), "An operand does not cover this contribution's integration support.");
});
auto basis = sample_element_basis<EquationT>(
*contribution.equation, element, configuration.discretization.residual_binding, configuration.discretization.displacement_binding, bindings.columns,
configuration.discretization.mapper, configuration.options.quadrature_policy, cache, configuration.options.contraction_mode);
if (!basis) {
return std::unexpected(basis.error());
}
result.push_back(std::make_shared<const ElementBasis<Plan::operand_count>>(std::move(*basis)));
}
return result;
}
/// Only this operand's coefficients and reference basis determine its reference samples.
template <VolumeEquation EquationT, typename OperandT, typename BindingT>
[[nodiscard]] OperandSamples sample_contribution_operand(
const ElementBatch<ElementBasis<EquationPlan<EquationT>::operand_count>>& basis,
const CoefficientView<BindingT>& input,
Workspace& workspace
) {
OperandSamples result;
result.reference.reserve(basis.size());
const auto elements = input.binding.validated_elements();
for (const auto& element : basis) {
result.reference.push_back(std::make_shared<const ::mfem::Vector>(sample_element_operand<EquationT, OperandT>(*element, input, elements, workspace)));
}
// Scalar values need no second numerical representation; mapped operands replace these pointers later.
result.physical = result.reference;
return result;
}
/// Freeze mapping data and mapped bases independently of the equation's coefficient inputs.
template <VolumeEquation EquationT, typename SchemaT, typename ContributionT>
[[nodiscard]] std::expected<ElementBatch<ElementGeometry<EquationPlan<EquationT>::operand_count>>, PreparationRejection>
map_contribution_geometry(
const VolumeConfiguration<EquationT, SchemaT>& configuration,
const ElementBatch<ElementBasis<EquationPlan<EquationT>::operand_count>>& basis,
const CoefficientView<typename VolumeConfiguration<EquationT, SchemaT>::DisplacementBinding>& displacement,
const MappingCoefficients& compactification,
Workspace& workspace
) {
const auto displacement_elements = displacement.binding.validated_elements();
return detail::build_element_batch<ElementGeometry<EquationPlan<EquationT>::operand_count>>(
static_cast<int>(basis.size()), workspace, [&](const int element, Workspace& local) {
return map_element_geometry<EquationT, ContributionT>(*basis[static_cast<std::size_t>(element)], displacement.binding,
displacement_elements, displacement.values, compactification, configuration.discretization.mapper, local);
});
}
/// Map only operands whose physical representation depends on deformation.
template <typename OperandT, std::size_t OperandCount>
[[nodiscard]] OperandSamples map_contribution_operand(
const OperandSamples& samples,
const ElementBatch<ElementGeometry<OperandCount>>& geometry,
Workspace& workspace
) {
OperandSamples result{.reference = samples.reference};
result.physical.reserve(geometry.size());
for (std::size_t element = 0; element < geometry.size(); ++element) {
result.physical.push_back(std::make_shared<const ::mfem::Vector>(map_element_operand<OperandT>(*samples.reference[element], *geometry[element], workspace)));
}
return result;
}
/** @brief Compute local displacement derivatives as an immutable graph result for this contribution. */
template <VolumeEquation Equation, typename Schema, typename Contribution>
[[nodiscard]] std::expected<ElementBatch<::mfem::DenseMatrix>, PreparationRejection> differentiate_contribution_geometry(
const VolumeConfiguration<Equation, Schema>& configuration,
const ContributionData<EquationPlan<Equation>::operand_count>& contribution,
Workspace& workspace
) {
const auto& discretization = configuration.discretization;
return detail::build_element_batch<::mfem::DenseMatrix>(
static_cast<int>(contribution.elements.size()), workspace, [&](const int element, Workspace& local) {
return element_geometry_derivatives<Equation, Contribution>(
*contribution.elements[static_cast<std::size_t>(element)], discretization.displacement_binding,
*discretization.compactification_space, discretization.mapper, local
);
});
}
/// Associate an operand type pack with equally ordered sample arguments.
template <typename OperandT>
using SamplesFor = OperandSamples;
/// Evaluate one equation contribution; its basis, mapping and individual operand samples are already prepared.
template <VolumeEquation EquationT, typename ContributionT, typename... Operands>
[[nodiscard]] std::expected<ContributionData<EquationPlan<EquationT>::operand_count>, PreparationRejection>
evaluate_contribution_equation(
const ContributionBinding<ContributionT>& contribution,
const ElementBatch<ElementBasis<EquationPlan<EquationT>::operand_count>>& basis,
const ElementBatch<ElementGeometry<EquationPlan<EquationT>::operand_count>>& geometry,
Workspace& workspace,
const SamplesFor<Operands>&... samples
) {
using Plan = EquationPlan<EquationT>;
if (!std::isfinite(contribution.coefficient)) {
return std::unexpected(PreparationRejection{.reason = PreparationRejectionReason::NON_FINITE, .message = "The product of equation coefficients is non-finite."});
}
ContributionData<Plan::operand_count> result;
if constexpr (requires { contribution.equation->residual_offset(); }) {
static_assert(discretization::blocks::GlobalScalarTerm<typename Plan::Test>, "Residual offsets require a global scalar row.");
result.residual_offset = contribution.coefficient * contribution.equation->residual_offset();
}
utils::CompensatedSum validation_integral;
for (std::size_t element = 0; element < basis.size(); ++element) {
std::array<std::shared_ptr<const ::mfem::Vector>, Plan::operand_count> reference_values{}, physical_values{};
((reference_values[equations::OperandValues<typename Plan::Operands>::template index<Operands>()] = samples.reference[element], physical_values[equations::OperandValues<typename Plan::Operands>::template index<Operands>()] = samples.physical[element]), ...);
auto data = evaluate_element_equation<EquationT>(*contribution.equation, contribution.coefficient, basis[element], geometry[element], std::move(reference_values), physical_values, workspace);
if (!data) {
return std::unexpected(data.error());
}
validation_integral.add(data->validation_integral);
result.elements.push_back(std::make_shared<const ElementData<Plan::operand_count>>(std::move(*data)));
}
if constexpr (requires { contribution.equation->validate_integral(0.0); }) {
try {
contribution.equation->validate_integral(validation_integral.value());
} catch (const std::domain_error& error) {
return std::unexpected(PreparationRejection{.reason = PreparationRejectionReason::EQUATION, .message = error.what()});
}
}
return result;
}
/**
* @brief Collect the results of all contributions into a single VolumeData. This contains the residuals due to
* each contribution for all degrees of freedom as well as data needed for subsequent jacobian calculations
*
* @tparam EquationT The equation type which is being solved, this is used to determine the test and operand types.
* @tparam SchemaT The schema type which is being solved, this is used to constrain the support of the contributions and define which material and boundary attributes belong to what dofs
* @tparam Contributions The contributions which are being evaluated, these are the terms in the equation which are being integrated over the volume
*
* @param configuration The configuration of the volume problem, including the equation and discretization.
* @param bindings The basis bindings for the volume problem, including the residual and displacement bindings
* @param workspace The workspace for the volume problem, including the full result vector and element
* @param contributions The contributions to the volume problem, including their basis, geometry, and operand samples.
*
* @returns A VolumeData object containing the residuals due to each contribution for all degrees of freedom as well as data needed for subsequent jacobian calculations.
*
* @start_compile_errors
* @compile_error If the a contribution defines a residual_offset but that residual offset is not addable to a double producing a double.
*
* @par Detailed Explanation
* This is a really important function on the backend to lets take some time to walk through it. We take in
* some configuration data (including things like what equation we are solving and what discretization we are using, the basis
* binding which contains mappings from full dof space to supported dof space, a runtime workspace, and
* a variable number of contributions.
*
* After some setup we initialize the VolumeData object which will be what we eventually return. The first part is then
* to loop over all contributions and for each contribution accumulate its residual_offset. See @ref AngularMomentumConstraint for more
* detail; however, the gist is that this is some constant term which should be removed from the final integrated quantity to
* evaluate the residual (think the integrated angular momentum - the target angular momentum for the residual).
*
* We first fold over all contributions and accumulate their residuals and following that there is then some basic validation that the total residual offset is finite.
*
* Next we extract the "residual bindings" from the discretization provided by the configuration. The residual bindings is a, after a few inheritances, DofMap.
* This means it fundamentally represents some term and how that term is supported on the mesh. So when we ask for say the size of the residual binding
* what that is saying is "how many dofs is this residual defined for". We make the full result a vector the size of the number of dofs the
* residual is defined for.
*
* We use the CompensatedSum class to help avoid round off and cancellation errors.
*
* We then go through and for each contribution to each element we evaluate the residual from that contribution. The evaluate_residual function
* is the primary function where we actually integrate an equations contributions over a volume and we include this in the global residual sum.
* We fold over and call this function for every contribution and for every element. Thereby getting the total residual for the entire volume due
* to all contributions.
*
* What we end up with is this global_sum. We then need to apply one more modification to it, if we are working with some global scalar (e.g. fixed angular
* momentum) then we add the residual offset (note that this will often be a negative value, AngularMomentumConstraint for example returns -angular momentum target).
*
* Then, regardless of if we are working with a global scalar or not we gather the full result into the residual vector. Gathering here means that we select
* all the residual values on supported dofs and distribute them into the correct indices of the full space residual vector. Dofs which are not
* supported by the residual binding will be set to zero.
*
* @par TL:DR
* Use this function to evaluate the residual of a volume problem, likely you never need to think about the implementation details.
*
*
*/
template <VolumeEquation EquationT, typename SchemaT, typename... Contributions>
[[nodiscard]] std::expected<VolumeData<EquationT, SchemaT>, PreparationRejection> collect_volume_results(
const std::shared_ptr<const VolumeConfiguration<EquationT, SchemaT>>& configuration,
const BasisBindings<EquationPlan<EquationT>::column_count>& bindings,
Workspace& workspace,
const Contributions&... contributions
) {
using namespace utils::errors;
using Plan = EquationPlan<EquationT>;
VolumeData<EquationT, SchemaT> result{
.configuration = configuration,
.bindings = bindings,
.contributions = {contributions.elements...}
};
double residual_offset = 0.0;
auto accumulate_residual_offset = [](const auto& contribution, double& total_offset) {
using namespace utils::errors;
if constexpr (requires { contribution.residual_offset; }) {
static_assert(requires { total_offset += contribution.residual_offset; }, "Residual offsets must be addable to a double. Currently it is not. This is likely an issue with how an equation is defined rather than some issue with the backend.");
total_offset += contribution.residual_offset;
}
};
(accumulate_residual_offset(contributions, residual_offset), ...);
if (!std::isfinite(residual_offset)) {
return std::unexpected(PreparationRejection{
.reason = PreparationRejectionReason::NON_FINITE,
.message = "The global residual offset is non-finite."
});
}
const auto& residual_binding = configuration->discretization.residual_binding;
workspace.full_result.SetSize(residual_binding.full_size());
workspace.full_result = 0.0;
utils::CompensatedSum global_sum;
std::optional<PreparationRejection> rejection;
const auto assemble_element_residuals = []<std::size_t Index>(
const auto& l_result,
Workspace& l_workspace,
utils::CompensatedSum& l_sum,
std::optional<PreparationRejection>& l_rejection,
const auto& l_residual_binding
) {
using Contribution = std::tuple_element_t<Index, equations::ContributionsOf<EquationT>>;
const auto elements = l_residual_binding.validated_elements();
for (const auto& element : l_result.contributions[Index]) {
if (l_rejection) {
return;
}
// Trace this function if you want to see how the residual is actually being integrated
// This gets the integrated weak residual for some element associated to some test function
// Note that this means that the shape of this result will depend on the shape of the test space.
// The primary distinction here will then be between point wise and global physis. Global constraints
// are integrated against a test function of a constant scalar 1, therefore they have a shape of 1 and only
// one residual is associated per element; whereas point-wise physics end up with a residual for every
// coefficient of the test function space on that element.
// TLDR: Get the residual
element_residual<Contribution>(*element, l_workspace.element_result, l_workspace.basis);
if constexpr (discretization::blocks::GlobalScalarTerm<typename Plan::Test>) {
l_sum.add(l_workspace.element_result[0]);
if (!std::isfinite(l_sum.value())) {
l_rejection = PreparationRejection{
.reason = PreparationRejectionReason::NON_FINITE,
.element = element->element,
.message = "The scalar integral overflowed while accumulating this element."
};
}
} else {
elements.add_element(element->element, l_workspace.element_result, l_workspace.full_result);
}
}
};
const auto assemble_contributions = [&assemble_element_residuals]<std::size_t... Indices>(
std::index_sequence<Indices...>,
const auto &l_result,
Workspace& l_workspace,
utils::CompensatedSum& l_sum,
std::optional<PreparationRejection>& l_rejection,
const auto& l_residual_binding
) {
(assemble_element_residuals.template operator()<Indices>(l_result, l_workspace, l_sum, l_rejection, l_residual_binding), ...);
};
// Note the side effects on global_sum and rejection (workspace also has side effects but this is less interesting as that's only there to avoid reallocations)
auto index_sequence = std::make_index_sequence<sizeof...(Contributions)>{};
assemble_contributions(index_sequence, result, workspace, global_sum, rejection, residual_binding);
if (rejection) {
return std::unexpected(*rejection);
}
if constexpr (discretization::blocks::GlobalScalarTerm<typename Plan::Test>) {
workspace.full_result[0] = global_sum.value() + residual_offset;
if (!std::isfinite(workspace.full_result[0])) {
return std::unexpected(PreparationRejection{
.reason = PreparationRejectionReason::NON_FINITE,
.message = "Non-finite integrated scalar residual."
});
}
}
result.residual = residual_binding.accumulate_dual(workspace.full_result);
residual_binding.validate_values(result.residual);
return result;
}
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,34 @@
#pragma once
#include <vector>
#include "serif/backend/mfem/mapping/data.hpp"
#include "serif/backend/mfem/mapping/variation.hpp"
#include "serif/backend/mfem/discretization/basis_evaluation.hpp"
namespace serif::operators::backend::mfem {
/// Reusable scratch for applying the accepted element linearization.
struct Workspace final {
mapping::DMWorkspace mapping_workspace{3};
mapping::VolumeMappingContext mapping_context;
mapping::VolumeMappingVariation mapping_variation;
::mfem::Vector element_values;
::mfem::Vector element_result;
::mfem::Vector quadrature_values;
::mfem::Vector displacement_direction;
::mfem::Vector full_result;
discretization::backend::mfem::BasisEvaluation test_basis_variation;
discretization::backend::mfem::BasisWorkspace basis;
::mfem::Vector displacement_samples;
::mfem::Vector displacement_value = ::mfem::Vector(3);
::mfem::DenseMatrix displacement_gradient{3};
::mfem::Vector test_values;
::mfem::Vector reference_test_values;
::mfem::Vector operand_dual;
::mfem::Vector displacement_dual;
::mfem::Vector test_variation_result;
std::vector<::mfem::Vector> operand_values;
::mfem::Vector lane_coefficients; ///< Lane-interleaved element displacement directions (batched geometry actions)
::mfem::Vector lane_samples; ///< Their lane-interleaved displacement samples
};
} // namespace serif::operators::backend::mfem

View File

@@ -0,0 +1,84 @@
#pragma once
#include <array>
#include <expected>
#include <functional>
#include <string>
#include "mfem/fem/gridfunc.hpp"
#include "serif/utils/projection/radial.hpp"
namespace serif::mapping {
class GridFunctionMappingEvaluator;
}
namespace serif::utils::projection::backend::mfem {
/**
* @brief Sample physical radii about a center, optionally through SERiF's existing geometry mapping.
* The mapping must use the projection space's mesh and must be refreshed after its input fields change.
*/
struct RadialProjectionOptions {
std::array<double, 3> center{};
mapping::GridFunctionMappingEvaluator* mapping{nullptr};
// Infinity callbacks return limiting values already pulled back to mesh coordinates.
std::function<RadialValue(const ::mfem::Vector&)> scalar_at_infinity{};
std::function<std::expected<::mfem::Vector, std::string>(const ::mfem::Vector&)> vector_at_infinity{};
};
using ProjectionResult = std::expected<::mfem::GridFunction, std::string>;
namespace detail {
/** @brief Implement scalar projection once for all checked radial profile types. */
[[nodiscard]] ProjectionResult project_scalar(
::mfem::FiniteElementSpace& space,
const std::function<RadialValue(double)>& profile,
const RadialProjectionOptions& options
);
/** @brief Implement vector projection once for all checked radial profile types. */
[[nodiscard]] ProjectionResult project_vector(
::mfem::FiniteElementSpace& space,
const std::function<RadialValue(double)>& radial_amplitude,
const RadialProjectionOptions& options
);
}
/**
* @brief Project a radial scalar with the finite element's native coefficient projection.
*
* This uses native interpolation and conforming restriction, not a global L2 fit.
* Hanging-node constraints are enforced before publishing the field.
* The returned field owns its coefficients
* and borrows the supplied space. The profile controls exterior values; no radius clamping or
* mass adjustment is applied. A rejected sample discards the entire unpublished field.
* At compactified infinity an explicit scalar_at_infinity callback may supply the limiting
* value in mesh coordinates, before the finite element's native mapping. Without that
* prescription infinity is rejected. Meshes and spaces must remain alive for the returned field.
*/
template <RadialFunction Function>
[[nodiscard]] ProjectionResult project_scalar(
::mfem::FiniteElementSpace& space,
const Function& profile,
const RadialProjectionOptions& options = {}
) {
return detail::project_scalar(space, projection::detail::checked_profile(profile), options);
}
/**
* @brief Project amplitude(r) times the outward radial unit vector into a vector H1/L2 space.
*
* Smooth radial vectors must have zero amplitude at the center. Conforming restriction enforces
* hanging-node constraints before publishing the field; this is not a global L2 fit. The profile
* remains in physical coordinates.
* At compactified infinity, vector_at_infinity must supply the limit in mesh coordinates. The callback
* receives the original mesh position, without subtraction of the configured physical center.
*/
template <RadialFunction Function>
[[nodiscard]] ProjectionResult project_vector(
::mfem::FiniteElementSpace& space,
const Function& radial_amplitude,
const RadialProjectionOptions& options = {}
) {
return detail::project_vector(space, projection::detail::checked_profile(radial_amplitude), options);
}
}

View File

@@ -0,0 +1,10 @@
#pragma once
namespace serif::backend::mfem {
/**
* @brief Initialize the selected numerical runtime once, before application work begins.
* @note Call on the main thread. An existing MPI session remains externally owned; a session
* started here is finalized by MFEM at process exit. Serial builds need no initialization.
*/
void initialize();
}

View File

@@ -0,0 +1,50 @@
# MFEM numerical archives
`save_discretization(discretization)` records the physical and logical reference
meshes, finite-element collections and spaces, term support and reduced numbering,
compactification coefficients, mapping prescription, and deformation prescription.
The application's resource owner and computational caches are not persistent data.
`restore_discretization<Original>(node)` returns an expected owning discretization.
`Original` supplies its compile-time term, domain-schema and deformation types.
The returned object has a new resource-owner type; this is why ordinary
`decode<Original>` cannot reconstruct an arbitrary application-owned discretization.
Its bindings and reference spaces borrow only resources retained by that new owner.
For a complete `make_discretization(...)` configuration, include `configuration.hpp`
and use `restore_configuration<OriginalConfiguration>(node)`. This restores the
resolved physical equations, independent FE resources, normalization choices and
preconditioner recipe together. The original model's authored callback is replaced
by the saved resolved equations; executable callbacks cannot be recreated from data.
For an external mesh, call
`save_discretization(discretization, {.embed_mesh = false})`. Supply a `MeshResolver`
to restoration: it receives each requested content fingerprint and returns native
MFEM mesh text. `mesh_text(mesh)` produces the canonical text expected by this
resolver. The same content and reconstruction checks apply to embedded and
external meshes. Paths are deliberately left to the application.
Mesh text uses the classic locale and `max_digits10`. Restoration disables MFEM's
refinement marking and optional element-orientation repair, then requires the
reconstructed native mesh text to match exactly. Space reconstruction also checks
the native signed element numbering; typed bindings check domain support and
reduced numbering. Existing geometry validation checks paired reference meshes
and shared scalar/displacement companion collections.
The current native format supports finite conforming serial 3D meshes and
fixed-order standard H1, L2 and RT spaces. Unsupported resource kinds remain
explicit `unsupported` nodes in the archive. Exact restoration currently requires
the same MFEM version, MFEM exception support and its `MFEM_ERROR_THROW` error
policy, so malformed native input can be reported instead of terminating the
application. The loader does not change the application's global MFEM error policy. Additional dependency
versions and MFEM feature configuration are provenance, not reconstructed objects.
MPI library information is queried only if MPI is already running.
Store physical `StellarState` coefficients alongside the matching numerical
description. The caller supplies that pairing; a bare coefficient vector cannot
identify its originating mesh. State loading validates the compiled coordinate
schema, backend loading validates the mesh and FE representation, and Newton
resumption additionally checks the rebuilt accepted residual metric. Element
samples, residuals, Jacobians and preconditioners are recomputed from the loaded
inputs. A loaded candidate still requires numerical qualification before it can
be reported as a converged equilibrium.

Some files were not shown because too many files have changed in this diff Show More