Riemannian manifolds
This page documents src/riemannian_manifold.hpp and
src/riemannian_manifold.cpp. The component owns metric-dependent tensor
algebra, covariant differential operators, boundary projections, and curvature
dispatch. It builds on Tensor fields for tensor metadata and
semantic expression nodes, on Numerical-gradient developer guide for physical
derivatives, and on K-forms and double forms for form representation.
Construction and metric sources
A manifold is constructed from a non-null square metric of dimension two or
three. normal_sign must be exactly +1 or -1. Other magnitudes,
zero, and non-finite values are rejected instead of silently scaling or
invalidating the normal. Test-function metrics are rejected. The constructor distinguishes three
metric sources:
an ordinary coefficient function uses the analytic derivative and curvature construction in this component,
an
HCurlCurlFESpacetrial proxy uses the Regge additional proxies, anda grid function in
HCurlCurlFESpaceuses the corresponding additional evaluators.
A direct proxy or grid function from another finite-element space is rejected.
The metric, its inverse, determinant, normalized normals, tangents, conormals,
and projectors are initialized eagerly. Metric derivatives, Christoffel
symbols, curvature tensors, and the second fundamental form are initialized
on first access through EnsureCurvature. Initialization is protected by
std::call_once so concurrent first access constructs one shared cache and
publishes it atomically. Keep this dispatch centralized:
public curvature accessors must not independently reconstruct one of the three
source paths.
The constructor retains whether the metric graph contains a trial proxy in
has_trial for future trial-dependent dispatch. It also retains det_g as
the unique determinant node used by the volume density and by any future
determinant consumers. Do not reconstruct DeterminantCF(g) independently.
The cached G, G_inv, G_F, G_F_inv, G_E, and G_E_inv
objects are typed views of the corresponding value graphs. normal_sign
orients the Euclidean normal used during construction. change_riemann_sign
selects the supported Riemann-tensor sign convention and is carried into the
lazy curvature construction.
Intrinsic strata
The VorB argument selects metric data on an intrinsic stratum:
VOLThe full metric
g, its inverse, andsqrt(det(g)).BNDThe face-tangential metric and inverse. Covariant derivatives are projected in every output slot after differentiation.
BBNDThe edge-tangential metric and inverse. It is available to algebraic operations such as raising, lowering, traces, and inner products, but not to
CovDerivative.BBBNDOnly the stored zero-dimensional volume factor is exposed.
Every public operation validates its supported VorB values before an
identity, scalar, empty-slot, or structural-zero shortcut. GetVolumeForm
validates all four array indices explicitly. Do not index the internal volume
array with an unchecked enum value.
Tensor and form contracts
Inputs must be non-null and belong to the manifold dimension. Generic tensor
fields encode the dimension in each component axis. K-forms and double forms
also carry an explicit ambient dimension, which is authoritative for scalar
rank-zero values. A scalar K-form may use ambient dimension zero to mean
unknown. Supported scalar operations infer the current manifold dimension,
matching the form-component contract. Axis variance is read from TensorMeta.
Operations must change values and metadata together.
Raise and Lower act on one selected axis using the inverse metric or
metric for the selected stratum. Their vector and one-form specializations are
preserved. Rank-one and rank-two operations use reconstructible symbolic
matrix-product nodes. Higher ranks use reconstructible einsum nodes.
Multi-index overloads apply the same checked single-index operation in the
supplied order.
All metric-backed algebra selects the volume, boundary, or edge metric and its
inverse through the same internal selector. New overloads should use that
selector so validation and cached metric identity cannot drift between
Raise, Lower, InnerProduct, and Trace.
Python addition and subtraction require identical axis variance for typed
tensor fields. In particular, a vector field cannot be added to a one-form.
Combining a k-form with a generic tensor of the same covariant shape returns a
generic tensor, because the operation cannot prove that alternation is retained.
Multiplication and division between typed tensor wrappers are scalar scaling,
except for rank-two matrix products whose contracted axes have opposite
variance. That contraction returns a typed rank-two tensor with the two free
axis variances. VectorField * VectorField is deliberately not an inner
product: the vector objects do not own a metric, so geometric contraction must
be written as M.InnerProduct(v, w). Multiplication by an explicitly untyped,
non-scalar NGSolve coefficient retains NGSolve’s native matrix/vector-product
behavior and returns an untyped coefficient, since no output variance can be
inferred.
Trace contracts two selected tensor axes. Opposite-variance axes contract
directly. Equal-variance axes insert the appropriate metric. A double-form
trace repeatedly contracts the first left and right axes and returns a scalar
only after both degrees reach zero. TraceSigma raises both axes of its
(1,1) sigma operand and contracts one left/right pair. SlotInnerProduct
is the complete double-form trace, with factorial normalization when
forms=True.
Contraction removes one tensor axis by pairing it with a vector. A
contravariant tensor axis inserts the metric before contraction.
ContractSlot maps the double-form left/right slot convention onto that
primitive. Transpose is an axis permutation and therefore permutes both
component values and covariance metadata.
Tensor inner products pair every axis. Axes with opposite variance contract
directly. Equal-variance pairs insert one metric or inverse metric. With
forms=True, k-form and double-form overloads apply the appropriate
factorial normalization.
Semantic expression graphs
Public manifold algebra retains tensor and form wrappers as semantic operands.
This is essential even when an operand currently evaluates to zero. Native
NGSolve einsum, addition, or multiplication may simplify such an expression
and erase the wrapper. A later Replace or Diff targeting that wrapper
would then incorrectly remain zero.
Use SymbolicEinsumCF, SymbolicSumCF, ScaleCoefficientCF, and
PermuteTensorCF for public-operand algebra. These helpers keep semantic
operands for transformation and differentiation while delegating numerical
evaluation to efficient native coefficient functions. Do not pass
GetFullCoefficient() to a new native algebra node merely as an
optimization: that accessor intentionally removes the wrapper identity.
InputCoefficientFunctions and TraverseTree continue to expose the
native evaluation layout required by NGSolve evaluators, zero patterns, and
generated code. Policy checks that need the retained meaning of an expression,
such as rejecting a test proxy hidden by native zero simplification, use
TraverseSemanticDAG instead. Do not change evaluation traversal to expose
semantic operands: the evaluator input order is a separate contract.
Low-rank metric products, inner products, traces, and matrix transposes use
specialized semantic nodes with native evaluators. This preserves the same
Diff and Replace contract without routing common rank-one and rank-two
operations through a generalized einsum. Numerical evaluators may unwrap one
semantic layer, but recursive unwrapping during construction is forbidden: it
would repeatedly traverse shared descendants and turn linear DAG rebuilding
into quadratic work.
The contract covers musical maps, traces, tensor and slot inner products,
cross products, contractions, transposition, S, J, s, and the
algebra used to assemble differential operators. Structural results that are
mathematically independent of the input, such as a degree-overflow form, may
still return a fresh zero.
Covariant differential operators
CovDerivative prepends a covariant derivative axis. It differentiates the
semantic input before adding one Christoffel term per component axis: a minus
term for a covariant axis and a plus term for a contravariant axis. A
zero-valued wrapper must take this same path. IsZeroCF is not sufficient to
prove independence from future differentiation or replacement targets.
CovDerivative and CovHessian are the canonical public Python names.
CovDeriv and CovHesse remain compatibility aliases and issue
DeprecationWarning. Existing scripts continue to run, while new code and
maintained examples should use the canonical spellings.
Only VOL and BND derivatives are supported. Boundary output is
projected after all derivative and connection terms are assembled. Composite
operators such as covariant Hessian, divergence, curl, inc, ein,
Laplacian, symmetric derivative, rotation, and the Lichnerowicz Laplacian reuse
these checked primitives and retain their intermediate semantic graphs.
compile_inner=False preserves the original semantic input throughout the
derivative graph. compile_inner=True compiles one shared native value graph
before constructing the gradient and connection terms. This mode is a
performance option for evaluation, not a promise that later wrapper-targeted
Replace or Diff can cross the compiled boundary. Trial and test proxies
are rejected in this mode because compiling their inner graph would violate
NGSolve proxy assembly semantics.
Exterior covariant derivatives and codifferentials of double forms first call
CovDerivative. They then permute, alternate, trace, scale, and optionally
project the result. Keep their scaling symbolic so a presently zero derivative
does not lose its source operand.
CovDerivative and CovHessian are the canonical Python and C++ binding
names. CovDeriv and CovHesse remain compatibility aliases. The Python
wrapper emits DeprecationWarning when they are used. Existing programs
therefore continue to run, while new code and gradual migrations should use
the canonical spellings.
Performance evidence
benchmarks/benchmark_tensor_algebra.py checks values and records
construction time, assembly time, and coefficient-tree statistics for
Raise, Lower, InnerProduct, and Trace. It also compares the
rank-one and rank-two Raise matrix-product implementations with the exact
reconstructible einsum alternatives in alternating order. Reports belong in
benchmarks/results. Correctness is smoke-tested in CI, but timing thresholds
are intentionally excluded because short assembly timings are host- and
load-sensitive.
Projection modes
Projection modes are validated before rank-zero or empty-slot shortcuts.
ProjectTensor supports mode zero (identity), one (face tangent), two
(normal contraction of the first axis, with remaining axes face tangent), and
three (edge tangent). ProjectDoubleForm applies modes independently to the
left and right slots. Its mode two is a pure normal contraction. It additionally
supports mode four, contraction with a supplied conormal. By default, remaining
indices in that slot are edge-projected before this contraction, and
project_remaining=False suppresses that projection. Edge modes require the
edge projector, conormal mode requires an explicit conormal, and contracting an
empty slot is an error.
Projectors are applied according to variance: covariant and contravariant axes use transposed representations of the same geometric map. Projection nodes retain their input tensor as a symbolic operand, as documented in Tensor fields.
Curvature conventions
All curvature accessors trigger the same lazy initialization. For ordinary
coefficient-function metrics, curvature is derived from the physical metric
gradient and Christoffel symbols. Regge proxies and grid functions provide
native additional operators. The native Regge curvature output is divided
by det(g) so it agrees with the geometric curvature operator used here:
Gauss curvature in two dimensions and the contravariant curvature operator in
three dimensions.
change_riemann_sign changes the Riemann convention consistently across the
ordinary and Regge paths. Tests compare normalized Regge, proxy, and analytic
results. Do not validate one curvature path only against another expression
that reuses the same convention code.
Validation and maintenance
Validate pointers, manifold dimensions, indices, modes, slots, and VorB
values at public boundaries. Validation must precede shortcuts so invalid input
does not accidentally become an identity or zero. New public tensor operations
should have regression coverage for a zero-valued semantic wrapper under both
directional Diff and Replace.
Private state should represent persistent manifold state only. Temporary proxy handles and constructor-only orientation arguments remain local. Remove disabled experimental coefficient-function classes and branches rather than keeping unreachable alternatives beside the active graph implementation.
Verification
After configuring a developer build and making that extension available to Python, run the focused C++ and Python contracts first:
cmake --build build --target test_riemannian_manifold
ctest --test-dir build -R '^riemannian_manifold$' --output-on-failure
python -m pytest -q tests/test_riemannian_manifold_contracts.py
Then run the manifold core, covariant-operator, double-form, projection, compiled-inner-graph, and cross-component regression files. Changes to the shared symbolic algebra or derivative construction require the full C++ and Python suites, following Developer guide.