Tensor fields

This page documents the tensor metadata and coefficient-function wrappers in src/tensor_fields.hpp and src/tensor_fields.cpp. The corresponding TensorField and VectorField adapters in src/wrappers.py are included where they define the Python-facing behavior.

K-forms and double forms derive from the tensor wrapper, but their degrees, ambient dimensions, alternation, and wedge operations belong to the separate k-form component documentation.

Role and representation

TensorFieldCoefficientFunction wraps an NGSolve CoefficientFunction without changing its component values. The wrapped coefficient remains the full-shaped value graph. The tensor wrapper adds ordered variance metadata so geometric operations can distinguish covariant and contravariant axes.

TensorMeta is the authoritative metadata representation. Character i of the public covariance string and bit i of its internal mask describe component axis i: "1" means covariant and "0" contravariant. Tensor labels use the same axis order. The available einsum alphabet limits the rank to MAX_SIGNATURE_LABELS (currently 52).

The metadata helpers preserve that ordering when they append, prepend, erase, or concatenate axes. Operations needing an additional einsum label must obtain it through FreshLabel so the rank limit is checked consistently.

Wrapper invariants

Every TensorFieldCoefficientFunction satisfies these invariants after construction:

  • the wrapped coefficient is non-null,

  • the metadata rank equals coef->Dimensions().Size(), and

  • all component axes have the same dimension.

Rank-zero tensor fields are valid. VectorFieldCoefficientFunction is the rank-one specialization with fixed covariance string "0".

The C++ object owns the authoritative metadata. Python exposes covariant_indices and the full-shaped value graph as read-only properties. Python code should not duplicate this state in instance attributes.

Canonical wrapping

Construct C++ wrappers through TensorFieldCF and VectorFieldCF. These factories centralize validation and prevent repeated retyping from growing the expression graph.

TensorFieldCF reuses an existing tensor wrapper when its TensorMeta matches. Otherwise it removes tensor-wrapper layers until reaching the underlying value graph and constructs a wrapper with the requested metadata. This also means that explicit generic retyping can intentionally discard a derived semantic type, such as a form, while preserving its full component values.

VectorFieldCF similarly reuses an existing vector wrapper, removes other tensor-wrapper layers, and then verifies that the value graph has rank one.

On the Python side, arithmetic implemented by TensorField and VectorField operates on .coef and calls the corresponding as_* adapter to restore metadata. as_tensorfield also dispatches the signatures "", "0", and "1" to the scalar-field, vector-field, and one-form specializations. An explicitly requested covariance string is validated and applied even when the input is already a Python tensor wrapper. Only an omitted or matching string permits immediate reuse. Numeric scaling accepts complex scalars in either operand order. Derived objects define their additional behavior in their own components.

Value-graph behavior

The tensor wrapper forwards evaluation, domain checks, derivative evaluation, zero detection, and nonzero patterns to the wrapped coefficient. Generated code declares an output with the same shape and real or complex scalar type, then copies the generated input values into it.

GetFullCoefficient is the C++ accessor for the full-shaped semantic value graph. Python .coef exposes the same graph. Canonical conversions use this accessor when removing metadata-only wrapper layers. Operations whose operands must remain targets for Diff or Replace instead retain those semantic operand wrappers in their expression graphs.

SymbolicExpressionCoefficientFunction in src/symbolic_expression.* shares evaluation, transformation, nonzero-pattern caching, and Jacobian construction between tensor algebra and proxy derivatives. It retains semantic operands independently of the native evaluation graph. Evaluation, child discovery, nonzero patterns, and code generation must use a consistent graph layout that keeps native proxy leaves visible to NGSolve. SymbolicEinsumCF retains its original operands for symbolic operations while removing metadata-only wrapper layers from the native evaluation inputs. Projection uses this same helper: its archive retains the original tensor operand, and Replace reconstructs the contraction from that operand.

Transform transforms the value graph and reconstructs the semantic wrapper through the virtual Rewrap hook. It also participates in NGSolve’s replacement and transformation caches. Derived tensor types override only Rewrap when the base transformation behavior is sufficient.

Directional Diff also uses Rewrap, except when the wrapper itself is the differentiation variable, in which case NGSolve’s direction is returned directly.

For Jacobian differentiation:

  • a scalar-valued variable appends no component axis, so Rewrap preserves the original semantic wrapper,

  • a tensor-valued variable appends axes whose variance is not defined by the original wrapper, so the Jacobian is an untyped CoefficientFunction, and

  • differentiating a wrapper with respect to itself returns NGSolve’s identity coefficient function directly.

Compile preserves shape, scalar type, and values, but returns NGSolve’s compiled coefficient-function type. Code that subsequently needs tensor metadata must wrap the compiled coefficient explicitly.

Tensor operations

The operations implemented in tensor_fields.cpp preserve axis order and metadata as follows:

TensorProduct

Concatenates the left axes with the right axes, concatenates their TensorMeta objects in the same order, and uses disjoint einsum labels. The combined rank may not exceed MAX_SIGNATURE_LABELS.

PermuteTensorCF

Validates that the supplied order is a permutation of all axes, then applies the same permutation to component values and the covariance string.

SymbolicEinsumCF

Delegates evaluation and code generation to native einsum, and reconstructs transformations and derivatives from the original signature and operands. Tensor products, permutations, and the contractions used by form Hodge operations use this helper. Its constructor arguments reconstruct the native evaluator on archive input. See K-forms and double forms for the operand-identity contract and native auxiliary-node limitations.

SymbolicSumCF and ScaleCoefficientCF

Preserve operands when constructing sums and scalar products, including operands whose current value is zero. Form operations use these helpers where native zero simplification would otherwise erase an operand or a scalar factor.

ApplyProjectorToIndex

Contracts a projector with one selected component axis. It uses a fresh einsum label while retaining the original output label, slot position, and covariance metadata.

IsVectorField and IsOneForm

Inspect TensorMeta only: they recognize rank-one covariance masks zero and one, respectively. They do not require a particular dynamic subclass.

Equivalence and serialization

The equivalence key contains the dynamic description, covariance string, and wrapped value-graph key. Derived semantic types with additional metadata must override the key calculation and include that state.

TensorFieldCoefficientFunction::GetCArgs archives the full coefficient and covariance string. VectorFieldCoefficientFunction::GetCArgs archives the full coefficient because its variance is fixed. Both dynamic types are registered with their immediate archive-aware base, and both Python bindings provide NGSPickle.

When constructor state changes, update GetCArgs and the pickle round-trip tests in the same change. Python instance attributes are not a substitute for restoring C++ metadata.

Extending tensor fields

For a new class derived from TensorFieldCoefficientFunction:

  1. Validate its shape and semantic metadata during construction.

  2. Override Rewrap and delegate reconstruction to a canonical factory.

  3. Include all additional metadata in its equivalence key and GetCArgs.

  4. Register the type for polymorphic archiving and add NGSPickle when the concrete type is exposed to Python.

  5. Test construction, replacement, directional differentiation, scalar and tensor-valued Jacobians, compilation, pickling, and wrapper flattening.

Component verification

After building and installing the extension as described in Developer guide, run the tensor metadata and forwarding tests:

ctest --test-dir build \
    -R '^(tensor_meta|tensor_field_forwarding)$' \
    --output-on-failure

The current Python wrapper regression file also contains downstream form and manifold cases because those types derive from or consume tensor fields:

python -m pytest -q tests/test_wrappers_tensor_fields.py

Changes to the base wrapper, generated code, or serialization can affect every derived form type. For those changes, follow the full-suite guidance in Developer guide.

Zero-valued Jacobians

Jacobian axes are the result axes followed by the variable axes. Construction retains every directional column in a symbolic Jacobian node, even when native stacking simplifies its evaluation to zero. This is needed for mixed derivatives and subsequent replacement: for zero scalar wrappers a and b, Wedge(a, b).Diff(a).Diff(b, CF(1)) must still equal one. Column dependencies survive archive round trips. A childless native ZeroCF remains a constant. Its differentiation behavior is not changed.