Files
MeanField_SERiF/docs/static/operators.md
Emily Boudreaux 6b57738330 docs(docs): built docs
Note for now much of the docs is LLM written. This was done to save time and focus on code development. The docs have been LOOSLEY reviewed. However, the code itself has been either fully written by hand or fully reviewed. Therefore, the docs should at the moment be seen as a loose resource rather than authoratiative (and this is on top of the general ability for docs to drift)
2026-11-09 14:09:44 -05:00

36 KiB

Applying equations as discrete operators

An equation describes local physics and its chosen weak formulation. An operator evaluates that description on a discretization to produce a residual R(u) and a Jacobian action J(u) du. New physics using supported operands needs an equation, not another bespoke operator.

The MFEM volume backend supports global scalar VALUE, scalar H1/L2 VALUE, scalar H1 GRADIENT and vector H1 VALUE/DIVERGENCE/GRADIENT operands and tests on three-dimensional conforming serial meshes. Scalar equations, mass and angular-momentum constraints, and primal gravity use the same volume graph and numerical application functions. Surface integrals, H(div), H(curl), and parallel assembly remain outside the current scope.

Where things belong

Responsibility Implementation
Physical laws, parameters, admissibility, local partial derivatives equations
Existing field and term identities, spaces, supports, operands discretization
Bind a spatial term to an MFEM space or a global scalar to its mesh TermBinding
Supported coefficient indices, orientation, extraction, assembly DofMap
Traverse an equation's additive contributions in declaration order operators/contributions.hpp
Interpret operand order, row support, and quadrature forms EquationPlan
Bind physical terms to coefficient vectors in an ordinary dependency context CoefficientView, make_term_coefficients
Verify and reuse canonical tensor factors TensorBasisCache
Contract a dense basis or tensor factors with pointwise maps BasisEvaluation
Evaluate a supported operation and apply its basis or transpose OperandEvaluator
Name volume stages and describe their fixed configuration and results volume_data.hpp
Sample bases and operands, map geometry, and evaluate one element's equation element_preparation.hpp
Apply these stages over one contribution's supported elements volume_stages.hpp
Validate and describe current volume sources volume_inputs.hpp
Declare volume graph nodes and return an ordinary dependency cache volume_computations.hpp
Consume accepted volume data and apply its Jacobian or transpose volume_application.hpp
Integrate and differentiate accepted element data element_evaluation.hpp
Compare exact MFEM coefficient values and persistent binding identities CoefficientTracking
Track changes, retain accepted results, and publish successful preparations utils::dependencies
Expose the frozen linearization to MFEM solvers JacobianOperator

The volume path separates input construction, graph declarations, accepted data, and numerical application. The dependency cache is their only preparation owner. Operators consume the equation interface; concrete physics headers do not depend on operators or MFEM. Geometry and quadrature continue to use the existing mapping and discretization modules.

A binding identifies a term, not a new field catalog and not an operand operation. Gravity's potential and gradient are different terms. Multiple operations on a term share one coefficient vector and one packed Jacobian column. Plan::Operands lists unique sampled operations; Plan::Columns lists unique coefficient terms.

Reading the calculation

make_contribution_computations in volume_computations.hpp reads in numerical order: reference bases, geometry, operand samples, then equation values. Its short lambdas call the ordinary numerical functions in volume_stages.hpp and element_preparation.hpp. The declarations themselves do no numerical work; try_prepare runs the required nodes later.

The contribution index distinguishes repeated equations, including repeated instances of the same type. Each node returns data for the contribution's supported elements; there is no separate C++ graph node for every mesh point.

Result key (Basis, Geometry, and sample keys are in backend::volume_keys) Declared numerical dependencies
Basis<I> MeshAndCoefficientBindings: fixed-mesh coordinate sequence and binding identities; equation and integration options are fixed configuration.
Geometry<I> Basis<I>, displacement coefficients, compactification coefficients.
ReferenceValues<I, Operand> Basis<I> and the coefficient source identified by Operand::term_type.
PhysicalValues<I, Operand> That operand's reference values and Geometry<I>; present only when the operand's physical representation depends on geometry.
QuadratureLinearization<I> Basis, geometry, and each declared operand's reference or physical values. Residuals, derivatives, weights, and contribution scaling are computed together.
Linearization Mesh and binding metadata and every contribution's QuadratureLinearization; retain ordered batches, assemble the residual, and validate complete integrals.

There are no manual change flags inside these numerical stages. The dependency engine follows the declared inputs to decide which stages need execution. For scalar VALUE,

u(q) = sum_j E(q,j) c(j)
z(q) = W(q) f(u(q), x(q))
R_element = E_test^T z

element_residual performs the last contraction. element_jacobian_action differentiates the same expression. Coefficients, explicit physical position, integration weight, and (for gradients and H1 divergence) both operand and test bases vary. Scalar and vector H1 value bases stay fixed. The evaluator's evaluate method and its transpose contraction work for all supported operations by flattening a vector sample's components into three rows per point.

OperandValue<Operand> describes the sampled shape using both term rank and operation: Value(u) of a vector is std::array<double, 3>, while Divergence(u) is double. PointLinearization<Operands, Result> supplies scalar/vector/matrix partials of the appropriate shape; the default result remains scalar for existing equations.

There are two maps. MFEM first maps from its canonical element to the fixed computational mesh: RT CalcVShape(transformation, ...) and CalcPhysDivShape(...) already include this map. OperandEvaluator then applies SERiF's additional map A = dx/dX, j = det(A): g_x = A g_X / j, div_x(g_x) = div_X(g_X) / j. The same transformation is applied to the test basis. Geometry variations include delta(A g_X / j) and delta(div_X(g_X) / j) at fixed coefficients, on both sides of the contraction. Cached fixed-mesh samples make these variations independent of subsequent changes to the caller's coefficient vectors.

H1 divergence uses a different transformation: div_x(u) = trace(G A^-1), where G[c,k] = partial_X_k(u_c). Its evaluator retains all nine entries of G internally, then produces one scalar per physical quadrature point. A scalar reference divergence alone would lose the information needed under shear or anisotropic dilation. The geometry derivative is trace(G delta(A^-1)), using the existing mapping variation.

Consequently an evaluator's reference sample and physical value can have different shapes. It declares both sizes, reads its own reference samples, and advertises geometry_dependent. Element assembly uses those declarations without branching on a particular finite-element family. Coefficient evaluation and test integration still share the same matrix-vector and transpose contractions.

prepare_volume validates the current coefficients, constructs their dependency context, and asks the ordinary dependency cache to prepare the graph. The final volume_keys::Linearization result owns the assembled residual and each contribution's immutable element data. Ordinary application functions consume this result; there is no second prepared-operator lifecycle.

Residual and Jacobian calls do not evaluate the EOS or silently prepare new values. Packed columns remain unique terms in first-appearance order (Plan::Columns), followed by displacement.

Model ownership and startup

The model owns caches directly. Fixed discretization, current coefficient inputs, and integration choices have separate names. With bindings, coefficient vectors, and the mapping already constructed, one volume row can be set up as follows:

namespace dependencies = serif::utils::dependencies;
namespace backend = serif::operators::backend::mfem;
namespace equations = serif::equations;

const auto closure_equation = equations::BarotropicDensityBalance{equation_of_state};
using Closure = std::remove_cvref_t<decltype(closure_equation)>;

const backend::VolumeDiscretization closure_discretization{
    .residual_binding = density_binding,
    .displacement_binding = displacement_binding,
    .compactification = compactification,
    .mapper = mapper
};
const auto term_coefficients = backend::make_term_coefficients(
    backend::bind(density_binding, density),
    backend::bind(potential_binding, potential),
    backend::bind(surface_level_binding, surface_level),
    backend::bind(angular_velocity_binding, angular_velocity)
);
const auto closure_inputs = backend::make_volume_inputs<Closure>(
    closure_discretization, term_coefficients, displacement
);
auto closure_cache = backend::make_volume_equation_cache(
    closure_equation, closure_discretization, closure_inputs
);
auto& closure = closure_cache;

// Repeat preparation with the current coefficients during nonlinear iterations.
backend::prepare_volume(closure, closure_discretization, term_coefficients, displacement);
const auto& linearization = closure.get<backend::volume_keys::Linearization>();
backend::volume_residual(linearization, density_residual);
backend::apply_jacobian(linearization, direction, displacement_direction, workspace, action);
backend::JacobianOperator jacobian(closure);

The snippets use <type_traits> for std::remove_cvref_t and <utility> for std::move. The equation sandbox supplies the complete manufactured equilibrium. VolumeOptions optionally supplies named quadrature_policy and contraction_mode members as the factory's fourth argument.

Cache construction declares work without evaluating physics. The graph owns the equation, integration options, and reusable preparation scratch. Meshes, spaces, term bindings, compactification, and mapper are borrowed and must outlive its uses. Coefficient contexts own views, not the coefficient vectors. Keep those vectors alive while preparing; accepted numerical results do not borrow their contents. The workspace used for Jacobian application remains caller-owned and is not a tracked dependency.

make_volume_inputs captures current mesh/binding metadata and borrows current coefficients. Rebuild this context when the discretization changes. Calling prepare_volume or try_prepare_volume does that explicitly on every call and validates before requesting the complete linearization. Calling cache.try_prepare directly is also available with a current, validated context; it is the generic engine's interface and permits partial result requests.

Moving a cache after an adapter borrows it invalidates that reference. The cache must outlive its adapter. The adapter reads the cache's latest accepted linearization on every call, so rejection or a request that omits that linearization makes application unavailable. References returned by cache.get must be reacquired after preparation or invalidation.

Two adapters can share one cache, while separate caches retain independent linearizations. Separate caches are not connected or published as one transaction; a controller prepares them explicitly. Shared intermediates can be declared in a common graph when that becomes useful.

Equation contributions

An equation may describe itself as an additive tuple returned by contributions(); the primal Poisson balance does so for its flux and source. Contributions share the same residual term, while leaves retain independent test operations, supports, and quadrature rules. ContributionsOf collects leaf types for concept checks and operand planning. operators::for_each_contribution visits the corresponding leaf objects in declaration order, borrowing the owned objects, including equations that cannot be copied.

Global scalars: mass, angular momentum, C, and Ω

barotropic_constant::mass_normalization stores the unknown Bernoulli constant C and receives the mass constraint. rotation::angular_velocity stores the signed angular speed Ω about a prescribed axis and center. They are explicitly declared GlobalScalarTerms, with one coefficient each and no finite-element space. Other nonspatial descriptors, such as surface-deformation coefficients, are not implicitly admitted as scalars.

using C = serif::discretization::blocks::barotropic_constant::mass_normalization;
using Omega = serif::discretization::blocks::rotation::angular_velocity;
using serif::discretization::backend::mfem::TermBinding;
namespace backend = serif::operators::backend::mfem;

TermBinding<C, Schema> c_binding(mesh);
TermBinding<Omega, Schema> omega_binding(mesh);
mfem::Vector c(1), omega(1);
c = surface_level;
omega = angular_velocity;
const auto term_coefficients = backend::make_term_coefficients(
    backend::bind(density_binding, density),
    backend::bind(potential_binding, potential),
    backend::bind(c_binding, c),
    backend::bind(omega_binding, omega)
);
const backend::VolumeDiscretization mass_discretization{
    .residual_binding = c_binding,
    .displacement_binding = displacement_binding,
    .compactification = compactification,
    .mapper = mapper
};
const auto mass_inputs = backend::make_volume_inputs<serif::equations::MassNormalization>(
    mass_discretization, term_coefficients, displacement
);
auto mass = backend::make_volume_equation_cache(
    serif::equations::MassNormalization{target_mass}, mass_discretization, mass_inputs
);
backend::prepare_volume(mass, mass_discretization, term_coefficients, displacement);

const backend::VolumeDiscretization angular_momentum_discretization{
    .residual_binding = omega_binding,
    .displacement_binding = displacement_binding,
    .compactification = compactification,
    .mapper = mapper
};
const auto angular_momentum_inputs = backend::make_volume_inputs<serif::equations::AngularMomentum>(
    angular_momentum_discretization, term_coefficients, displacement
);
auto angular_momentum = backend::make_volume_equation_cache(
    serif::equations::AngularMomentum{target_momentum, axis, center},
    angular_momentum_discretization, angular_momentum_inputs
);
backend::prepare_volume(angular_momentum, angular_momentum_discretization, term_coefficients, displacement);

The scalar basis is one at every quadrature point. The same basis contractions, geometry derivatives, coefficient validation, frozen snapshots, and packed adapter serve spatial and global terms. A scalar row sums every supported element into its single entry; there is no artificial order-zero FE space or per-element scalar DOF. Global scalar reductions compensate cancellation at quadrature points and across elements, preserving the old column-sum accuracy. Its mesh, support, and lifetime checks still apply. Preparation also rejects overflow in the integrated scalar residual, including its offset, before publishing the candidate. Calling .space() on a global binding is an error; common assembly uses .mesh() and .full_size().

MassNormalization evaluates rho over the stellar domains. Its residual_offset() returns -target_mass, which final volume assembly adds once after integration. This optional hook is restricted to global scalar rows and describes a constant, state-independent offset. Arithmetic scales the offset along with its contribution; the offset has no density or geometry derivative. The mass row depends on density and displacement, not on C directly. C influences the density-balance row.

AngularMomentum receives the Ω row and evaluates rho Omega s^2, where s is distance from the prescribed physical axis through center. Its offset is -target_momentum. The target must be finite and nonnegative; trial angular speeds may be signed or zero. It reuses RigidRotation for s^2 = 2 Psi_unit and its position derivative. The equation supplies dF/drho = Omega s^2, dF/dOmega = rho s^2, and dF/dx = 2 rho Omega grad(Psi_unit); the existing backend differentiates volume.

An equation may also define validation_integrand(point, linearization) and validate_integral(value). During the existing quadrature loop the backend sums that scalar measure with the physical integration weight, then validates its total over the contribution's support before publishing the preparation. Throwing std::domain_error rejects the trial state. Each leaf is validated separately. Angular momentum uses its already computed dF/dOmega to reject a nonfinite or negative total moment of inertia, including at Ω = 0; it does not require every local trial density to be positive.

The density-balance constructor accepts an axis (default z) and center (default zero), not C or Ω. The axis is normalized once. The balance reads Ω and C from the state. It implements d(A(rho) + Phi - Psi - C)/dC = -1 and dPsi/dOmega = 2 Omega Psi_unit. These derivatives are defined at Ω = 0 without dividing by Ω. RigidRotation remains a fixed-vector utility for independent calculations.

Packed density-balance columns are rho, Phi, C, Omega, displacement. Mass columns are density, displacement; angular-momentum columns are density, Omega, displacement. Each global scalar column occupies one entry. Directions must bind the same scalar descriptors, including an explicit zero when a control is held fixed. Reprepare after changing its value, as with spatial coefficients.

The mass port retains the old 1e-12 integral comparison, 5e-7 affine scaling, 3e-8 density finite difference at step 1e-3, 2e-7 geometry finite difference at step 1e-6, 2e-11 independent complete action, and 2e-15 column sum. Serial transpose actions and preparation work reports are available. MPI reductions and coupled root-layout routing remain deferred.

The angular-momentum port retains the old 2e-13 moment comparison, 7e-7 fifth-power dilation law, 3e-13 parallel-axis identity, 2e-11 independent action, 3e-15 column sum, and 2e-15 angular-speed action comparison. Density, geometry, and angular-speed differences retain thresholds 4e-8, 4e-7, and 2e-10 at steps 1e-3, 1e-6, and 1e-6. Tests exercise tilted and displaced axes, both element types and vector orderings, a curved star, frozen snapshots, rejection and recovery, and default quadrature. Serial cache behavior is checked through the shared preparation reports. Distributed/root-layout tests await their corresponding infrastructure. The supported physical part of the old rotating-system test also checks one shared Ω across density-balance and invariant rows (3e-7 and 3e-10 at step 1e-5), its zero columns in mass, the test closure, and gravity, and the different zero-speed limits. Seed generation, surface projection, and root manifests are not part of this volume-row port.

Prescribed surface pressure

Isobaric prescribes a typed PressureValue. It describes the physical condition without selecting enthalpy or any other field. The model's surface compiler selects a formulation from the available thermodynamic fields, solved equation rows, and EOS relations:

  1. Use a pressure unknown directly when one is available.
  2. Otherwise use a declared inverse EOS relation to resolve a boundary field.
  3. Otherwise evaluate a declared forward pressure relation and subtract the target.

An unavailable or ambiguous choice is rejected at compile time. The compiler uses the EOS's declared relations; it does not numerically invert an EOS. A preferred surface field can narrow the choice when the physical model admits several rows. See AutomaticPressureSurface.

For a polytrope using density, the selected condition reduces to

R_i = rho_i - rho_EOS(P_surface)
delta R_i = delta rho_i

Other EOS relations may depend on temperature, composition, or other bound fields. Their values and derivatives are evaluated from the current boundary state. Zero pressure also goes through the selected relation; its carrier value need not be zero. A pressure unknown needs no EOS evaluation at all.

The finite-element backend compiles traces of the required fields at the selected carrier nodes on StellarSurfaceBoundary. These fields may have different spaces and polynomial orders. The scalar carrier must have a supported nodal H1 row; Bernstein/positive coefficients are rejected because they are not point values.

For standalone operator work, the shared boundary graph can be used directly. Here Formulation is the selected formulation, eos_source explicitly tracks the EOS parameters, and coefficients binds the fields required by that formulation:

namespace physics = serif::geometry::surface;
namespace surface = physics::backend::mfem;
namespace dependencies = serif::utils::dependencies;

const auto boundary = surface::make_pressure_boundary<Formulation>(coefficients);
const auto inputs = surface::make_pressure_boundary_inputs(
    boundary, eos_source,
    physics::Isobaric{serif::dimensions::PressureValue{0.03125}}, coefficients
);
auto cache = dependencies::make_computation_cache<physics::SurfaceFailure>(
    surface::make_pressure_boundary_graph<Formulation>(), inputs
);
const auto prepared = cache.try_prepare(inputs);
if (prepared) {
    const auto& linearization = cache.get<physics::keys::Linearization>();
    // First assemble the volume residual and Jacobian action into these vectors.
    surface::apply_residual_rows(*boundary, linearization, residual);
    surface::apply_jacobian_rows(*boundary, linearization, direction, action);
}
// Handle prepared.error() before using a result from a rejected trial.

The graph tracks pressure, EOS parameters, and field coefficients. Preparation owns the evaluated residuals and derivatives until the next preparation, so later input changes do not silently change an already prepared action. Invalid numerical states return a SurfaceFailure; invalid or stale bindings remain programming errors. Both coefficients and direction must use the bindings that compiled the traces, and those bindings and borrowed values must remain alive.

Apply boundary replacement after volume assembly. It removes every volume coupling at a selected row and inserts every derivative required by the chosen surface relation. For the transpose, mask selected residual-dual entries before applying the volume transpose, then add the boundary transpose to each affected field. Interior rows remain untouched. There is no surface measure or quadrature weight in this strong condition. The model's assembled operator performs this composition; a standalone volume operator alone does not.

The pressure regressions retain the MeanField residual/action comparisons at 1e-13, the analytic closure/density-balance thresholds of 2e-12, and the mass threshold of 2e-11 * target_mass. The constrained density-balance finite difference retains step 1e-5 and threshold 2e-6. Analytic boundary values and independent MFEM boundary masks check the shared graph on tetrahedra, hexahedra, both vector orderings, and a curved star. General surface tests also cover non-enthalpy EOS relations, state-dependent couplings, transpose actions, cache invalidation, rejection, and recovery.

Selective serial preparation

Preparation defaults to PreparationMode::reuse_unchanged, matching the generic dependency engine. Request a full refresh explicitly with PreparationMode::rebuild:

backend::prepare_volume(closure, closure_discretization, term_coefficients, displacement);
const auto report = backend::volume_work_report(closure);
// report.geometry_points, sampled_operands, physics_points and reused_elements

backend::prepare_volume(
    closure, closure_discretization, term_coefficients, displacement,
    backend::PreparationMode::rebuild
);

The dependency engine owns exact coefficient snapshots and accepted results. make_volume_inputs supplies MeshAndCoefficientBindings, displacement, compactification, and the equation plan's coefficient terms as source roles. CoefficientTracking identifies each source by its persistent binding and compares coefficient contents. Temporary context wrappers and copied equal vectors therefore retain valid work. A changed operand refreshes its samples and the physics of contributions using that operand. Other contributions retain their accepted data. Displacement or compactification changes refresh mapping and mapped bases while retaining unaffected fixed-mesh coefficient samples. A fixed-mesh coordinate change rebuilds those bases too. No coefficient hashes or caller-maintained coefficient revision counters are required.

Reuse assumes fixed equation configuration and any external data it references. Request PreparationMode::rebuild, or invalidate the cache, after modifying external EOS data; changes hidden behind a runtime EOS view cannot be discovered automatically. Changing owned equation parameters requires a new graph. preparation_count() counts successful generic cache preparations, including reuse and partial requests. Failed attempts do not increment it; invalidation preserves the count. Numerical work counts are derived from the most recent graph report, rather than separately stored state.

Inspect any cache directly through the dependency API:

namespace stages = backend::volume_keys;
const auto& graph_report = closure.report();
const auto basis_status = graph_report.status<stages::Basis<0>>();
const auto geometry_status = graph_report.status<stages::Geometry<0>>();
const auto physics_status = graph_report.status<stages::QuadratureLinearization<0>>();

Statuses are computed, reused, or not_required. The numerical work report summarizes these statuses in element and quadrature counts. Volume helpers request the whole graph; volume_keys::Linearization retains accepted contributions and the assembled residual together. Direct dependency requests can prepare selected intermediates, but application requires the final linearization to be available.

Rejection makes current cache results unavailable. The generic engine keeps its last accepted storage for possible later reuse; backend preparation helpers additionally invalidate that storage on validation or numerical failure, ensuring a fresh retry. Neither path publishes a partly prepared result. Reacquire accepted data after every successful preparation rather than retaining a reference across cache updates.

Reuse depends only on unchanged tracked inputs. A recomputed node advances its version even when it happens to produce the same value, so required downstream computations also refresh. There is no output-equivalence test. Coefficient lookup uses the ordinary dependency context; operand-list deduplication uses CombinedKeyList.

Tensor contractions and complete transposes

On eligible H1/L2 and RT hexahedra, the backend verifies Cartesian quadrature ordering, native signed DOF maps, and the factorization against MFEM basis values and derivatives. It then performs successive one-dimensional contractions. H1 gradients and RT divergence use the same factors and transpose routine. Owned tables are shared within one volume graph and keyed by finite-element identity, operation and complete quadrature contents. Temporary or modified rules cannot leave borrowed table data behind.

Small pointwise maps apply MFEM's canonical-to-fixed-mesh transformation and SERiF's additional physical transformation. Tetrahedra, non-Cartesian rules and unverified factorizations keep the dense path. Neither path changes the quadrature rule. Supply VolumeOptions{.contraction_mode = ContractionMode::dense} to the cache factory to use the dense reference path explicitly. tensor_element_count(linearization) reports how many accepted contribution-elements use tensor contractions, including geometry.

apply_jacobian_transpose(linearization, residual_dual, workspace, packed_result) returns all coefficient duals in Plan::Columns order followed by displacement. JacobianOperator also implements MFEM's MultTranspose. The transpose includes weight, explicit position, operand mapping, and test mapping derivatives. Geometry is differentiated through twelve local inputs: three displacement values and nine canonical derivatives. The mapping module supplies their exact directional variations; no finite differences or global Jacobian-column probing are used in production.

A strong nodal replacement requires the following transpose order:

backend::mask_residual_dual(boundary, residual_dual, volume_dual);
backend::apply_jacobian_transpose(volume, volume_dual, workspace, packed_dual);
// Obtain the constrained term's block from packed_dual using the volume plan's column order.
backend::add_jacobian_transpose_rows(boundary, residual_dual, constrained_term_dual);

Mask residual rows before the volume transpose, then add the constraint transpose to the appropriate coefficient block. Zeroing coefficient entries after the volume transpose would incorrectly remove contributions from unconstrained residual rows. For centering, the constrained coefficient block is the displacement geometry block. These remain serial coordinate transposes; no mass-matrix identification of duals or MPI true-DOF ownership is implicit.

Tensor tests retain the old scalar 2e-12 and gradient/RT 2e-11 comparisons. Full equation transposes are checked against dense contractions, individual Jacobian columns, the old mass-adapter 2e-12 pairing threshold, both element types/orderings, and curved compactified geometry. Existing tests and numerical tolerances are unchanged.

Restrictions and what depends on them

Boundary Enforcement Why the numerical code needs it
Authoring interface equations::Equation Physics supplies the declared pointwise input/output contract. This does not promise execution support.
Current volume algorithm operators::VolumeEquation Every contribution needs a volume-domain support and implemented test/input operations. ScalarVolumeEquation remains available for existing callers.
Requested operand evaluation OperandEvaluator<Operand> specialization; EvaluableOperand queries its interface There is no generic fallback that ignores the operation tag. An unsupported operation cannot silently become VALUE sampling.
Actual MFEM element used for sampling Always-active range/map checks in sample_basis; gradients and H1 divergence additionally require a transformation Only scalar VALUE bases (one or three copies) are sampled.
Actual space bound to a term TermBinding: family/rank compile-time checks; dimension, continuity, order, range, mapping runtime checks Coefficient layout and basis representation must match the declared term. A valid binding still needs a supported operation; vector H1 VALUE and DIVERGENCE are implemented; GRADIENT and CURL remain unsupported.
Supported DOF mapping DofMap checks serial conforming 3D spaces, sizes, support, orientation, and revisions Extraction and assembly assume this representation.
Selected boundary rows select_boundary_dofs and DofSelection::validate Boundary tags must exist in the schema, have supported DOFs, and remain unchanged during use.
Cross-binding consistency make_volume_inputs and the basis stage check schema, mesh, binding identities, and input support Every operand must cover its contribution's integration support, which may be narrower than the row. No operand is silently extended into vacuum.
Inadmissible geometry or physics Element preparation returns PreparationRejection with element and point; optional integral validation rejects at contribution level A nonlinear trial state can be rejected without publishing incomplete data. Integral rejections have no single element/point.
Missing or stale preparation Dependency result availability and validate_volume_data Cached matrices and geometry must match the borrowed discretization.

Malformed bindings and coefficient sizes are programming errors. Numerical trial-state rejections are values returned from try_prepare_volume. The corresponding prepare_* convenience functions translate a rejection for the caller. Physical admissibility remains the equation/EOS author's responsibility.

Extending the implementation later

For new physics expressible using supported operands, change only the equation and model selection. Keep local physical partial derivatives in the equation; mesh and mapping derivatives belong to numerical execution.

For a new space representation, start with TermBinding and the existing space descriptors. For a new operation, start with OperandEvaluator: its result shape, basis evaluation, coefficient action, test contraction, and geometry dependence must agree. Then examine the element residual and its Jacobian together. In particular, the existing gradient and H1 divergence implementations show why both operand and test variations are needed. A new operation must provide these mathematics before its capability check can admit it.

The volume admission contract should expand only after those calculations, point/cache shapes, and quadrature handling exist. Unsupported operations remain compile-time failures. There is no separate hand-maintained capability registry.

Compile-time checks in operators/plan.cpp cover supported scalar, gradient and H1 divergence operations, sampled result shapes, packed columns, and rejection of unsupported operations. Authoring an equation remains distinct from executing it with this backend. The existing equation tests retain independent quadrature comparisons, analytic dilation checks, centered differences, ownership checks, and their original tolerances.

Gravity without a separate assembly path

primal_gravity.hpp returns its weak Poisson residual as a tuple of contributions: the potential-gradient flux on all domains and the density source on StellarDomains. Contribution<TestOperand, Support> is an optional authoring helper; each contribution supplies Operands, evaluate, and additional_order. All contributions share the row term, but may select different test operations and supports. Existing single-contribution equations need no wrapper. The source's StellarDomains support prevents reading density on vacuum elements. This is volume-operator support, not a new boundary-condition API.

API migration

Previous name Current name
equations::backend::mfem::EquationField discretization::backend::mfem::TermBinding
equations::backend::mfem::FieldDofs discretization::backend::mfem::DofMap
equations::backend::mfem::FieldState operators::backend::mfem::CoefficientView
EquationState, State make_term_coefficients(bind(...), ...) returns a dependency context
PreparedEquation, PreparedOperator, VolumeComputations make_volume_equation_cache, prepare_volume, and free application functions
equations::EquationPlan operators::EquationPlan
EQWorkspace, EQElementData operators::backend::mfem::Workspace, ElementData

bind(binding, coefficients) now produces a view with .binding and .values. Pointwise equation inputs and physical derivatives retain their existing interface.