Tensors
oCAS provides basic tensor algebra with explicit index management. A
Tensor carries named index slots (upper/lower), optional symmetry
metadata, and can be converted to an Atom expression for symbolic
processing.
Index Slots
Each index on a tensor has:
| Property | Type | Meaning |
|---|---|---|
label | Atom | Index name (typically a Greek letter or i, j, …) |
position | IndexPosition | Upper (contravariant) or Lower (covariant) |
#![allow(unused)] fn main() { use ocas_core::arena::Arena; use ocas_atom::AtomArena; use ocas_atom::tensor::{IndexSlot, IndexPosition}; let arena = Arena::new(); let ctx = AtomArena::new(&arena); let mu = IndexSlot::new(ctx.var("mu"), IndexPosition::Upper); let nu = IndexSlot::new(ctx.var("nu"), IndexPosition::Lower); }
Creating Tensors
A Tensor is defined by a name (Symbol) and a vector of index slots:
#![allow(unused)] fn main() { use ocas_core::arena::Arena; use ocas_atom::{AtomArena, Symbol}; use ocas_atom::tensor::{Tensor, IndexSlot, IndexPosition, Symmetry}; let arena = Arena::new(); let ctx = AtomArena::new(&arena); let slots = vec![ IndexSlot::new(ctx.var("mu"), IndexPosition::Upper), IndexSlot::new(ctx.var("nu"), IndexPosition::Lower), ]; let t = Tensor::new(Symbol::new("g"), slots); println!("rank = {}", t.rank()); // 2 println!("name = {}", t.name()); // g }
Optionally attach symmetry metadata:
#![allow(unused)] fn main() { let symmetric = t.clone().with_symmetry(Symmetry::Symmetric); let antisymmetric = t.clone().with_symmetry(Symmetry::Antisymmetric); }
Symmetry metadata is advisory for downstream consumers; the tensor algebra operations do not automatically symmetrize.
Contraction
contract takes two tensors and returns either a scalar (if all indices
contract) or a TensorProduct with the remaining free indices:
#![allow(unused)] fn main() { use ocas_core::arena::Arena; use ocas_atom::{AtomArena, Symbol}; use ocas_atom::tensor::{contract, Contracted, IndexSlot, IndexPosition, Tensor}; let arena = Arena::new(); let ctx = AtomArena::new(&arena); // Contract two rank-1 tensors: A^μ B_μ → scalar let a = Tensor::new(Symbol::new("A"), vec![ IndexSlot::new(ctx.var("mu"), IndexPosition::Upper), ]); let b = Tensor::new(Symbol::new("B"), vec![ IndexSlot::new(ctx.var("mu"), IndexPosition::Lower), ]); match contract(&ctx, &a, &b) { Contracted::Scalar(expr) => println!("scalar: {}", expr), Contracted::Product(tp) => { for f in &tp.factors { println!("free factor: {}", f.name()); } } } }
Contraction matches indices by label and opposite position. Multiple pairs of matching indices are contracted in a single call.
Symmetrization
symmetrise_sign returns the sign of the antisymmetrization of a
tensor: +1 for even permutations, -1 for odd permutations of the
dummy (contracted) labels. This is useful for computing determinant-like
expressions.
#![allow(unused)] fn main() { use ocas_atom::tensor::symmetrise_sign; let sign = symmetrise_sign(&tensor); }
Converting to Atoms
A tensor can be lowered to a standard Atom expression via to_atom:
#![allow(unused)] fn main() { let atom = tensor.to_atom(&ctx); println!("{}", atom); // e.g. g(mu, nu) }
This allows tensors to participate in the general symbolic pipeline (rewriting, simplification, differentiation).
Python & C Usage
Python
from ocas import Tensor, contract_tensors, tensor_symmetrise_sign
# Create a rank-2 tensor
g = Tensor("g", [("mu", "upper"), ("nu", "lower")])
print(g.rank()) # 2
print(g.name()) # g
# Contract two tensors
A = Tensor("A", [("mu", "upper")])
B = Tensor("B", [("mu", "lower")])
result = contract_tensors(A, B)
print(result) # scalar expression
# Antisymmetrization sign
sign = tensor_symmetrise_sign(g)
C
#include <ocas.h>
int err = 0;
/* Create tensor A^μ (slots is a "label,position;..." string; symmetry may be NULL) */
struct ocas_OcasTensor *A = ocas_tensor_create("A", "mu,upper", NULL, &err);
/* Query rank */
size_t rank = ocas_tensor_rank(A); /* 1 */
ocas_tensor_free(A);
See the Python API and C/C++ API chapters for full documentation.
Limitations
- Tensor algebra is explicit: indices are matched by label, not by
Einstein summation convention. You must call
contractmanually. - The
TensorSymmetrymetadata supports onlySymmetricandAntisymmetric; finer-grained symmetries (subset symmetriessymmetric_subsets, cycliccyclic) are expressed through the 0.22.0SymmetrySpec/TensorRegistryin graph canonicalization. - Graph canonicalization requires registering each tensor's symmetry
spec in a
TensorRegistryfirst; unregistered tensors are treated as having no symmetry. - Graph-based canonicalization does not perform physics-level semantics such as automatic symmetry enforcement in Wick contractions.
Tensor Canonicalisation (0.22.0)
Graph-isomorphism-based canonicalisation provides label-independent unique canonical forms for tensor expressions. Two tensor products that differ only by index relabelling produce the same canonical form.
How it works
- Encode the tensor expression as a graph: tensor heads become head vertices, each index slot becomes a slot vertex, contractions become undirected edges, and symmetries are encoded as hidden positional data.
- Run McKay refinement-individualisation (1-WL colour refinement + individualisation DFS with automorphism-orbit pruning) on the graph.
- Reconstruct the canonical expression from the canonical labelling,
renaming dummy indices from a per-dimension-group pool (
d0,d1, …).
#![allow(unused)] fn main() { use ocas_atom::tensor::canon::canonicalize_tensors; use ocas_atom::tensor::spec::{SymmetrySpec, TensorRegistry}; use ocas_atom::AtomArena; use ocas_atom::Symbol; use ocas_core::arena::Arena; let arena = Arena::new(); let ctx = AtomArena::new(&arena); let mut reg = TensorRegistry::new(); reg.register(Symbol::new("T"), SymmetrySpec::none()); reg.register(Symbol::new("U"), SymmetrySpec::none()); let i = ctx.var("i"); let j = ctx.var("j"); let k = ctx.var("k"); let t = ctx.fun("T", &[i, j]); let u = ctx.fun("U", &[j, k]); let prod = ctx.mul(&[t, u]); let ct = canonicalize_tensors(&ctx, prod, ®).unwrap(); println!("canonical: {}", ct.canonical_form); // T(i,d0) * U(d0,k) }
Symmetry Specification
| Spec | Meaning |
|---|---|
SymmetrySpec::none() | All slots are independent |
SymmetrySpec::fully_symmetric(n) | Slots interchangeable (hidden positions) |
SymmetrySpec::fully_antisymmetric(n) | Slots antisymmetric (Ricci identity verification) |
| Custom subsets | symmetric_subsets / antisymmetric_subsets / cyclic fields |
Dummy Index Management
dummy::refresh_dummies renames all labels appearing exactly twice to
canonical names (d0, d1, …) per dimension group:
#![allow(unused)] fn main() { use ocas_atom::tensor::dummy::refresh_dummies; let result = refresh_dummies(&ctx, expr, ®)?; // "T(i,j)*U(j,i)" → "T(d0,d1)*U(d1,d0)" }
Young Projector (explicit)
young::young_project expands a tensor with a Young tableau symmetry into
a sum over permutations, with signs for antisymmetric columns:
#![allow(unused)] fn main() { use ocas_atom::tensor::young::{YoungTableau, young_project}; // Fully antisymmetric [1,1]: f(a,b) → f(a,b) - f(b,a) let result = young_project(&ctx, f_ab, &YoungTableau::new(vec![1, 1])); // Fully symmetric [2]: g(a,b) → g(a,b) + g(b,a) let result = young_project(&ctx, g_ab, &YoungTableau::new(vec![2])); }
Python API
import ocas
# Canonicalise
result = ocas.canonicalize_tensors("T(i,j)*U(j,k)", {"T": "none", "U": "none"})
# Symmetric canonicalisation (g(b,a) == g(a,b))
result = ocas.canonicalize_tensors("g(b,a)", {"g": "symmetric"})
# Young projector
result = ocas.young_project("f(a,b)", [1, 1])
# Refresh dummy indices
result = ocas.refresh_dummies("T(i,j)*U(j,i)", {"T": "none", "U": "none"})