Typed Expressions (TExpr)
@indices M a b ccreates typed index variables bound to manifold:Mtensor(:T)returns aTensorHead; apply indices to get an expression:T[-a,-b]- Typed expressions validate at construction time, but still serialize into the current string-based engine internally
- Catches slot-count and manifold errors at construction, not inside the engine
- Both APIs coexist:
ToCanonical(T[-a,-b])andToCanonical("T[-a,-b]")are equivalent
The TExpr layer gives you a Julia- and Python-native way to write tensor expressions using operator overloading and indexed syntax, rather than strings.
Both APIs coexist — the string API is never removed. Pick whichever fits your workflow.
# String API (always works)
ToCanonical("RiemannCD[-a,-b,-c,-d] + RiemannCD[-a,-c,-d,-b] + RiemannCD[-a,-d,-b,-c]")
# Typed API (same result, errors caught earlier)
@indices M a b c d
Riem = tensor(:RiemannCD)
ToCanonical(Riem[-a,-b,-c,-d] + Riem[-a,-c,-d,-b] + Riem[-a,-d,-b,-c])Why use the typed API?
The string API works, but creates friction:
| Problem | String API | Typed API |
|---|---|---|
| Wrong slot count | "T[-a,-b,-c]" silently malformed | T[-a,-b,-c] → error at construction |
| Wrong manifold | Mixing :a from M and :i from N undetected | Manifold checked on every Idx |
| Index appearing 3× | Caught deep in canonicalization | Will fail validation at tensor application |
| Discoverability | Must memorize RiemannCD, RicciCD, ... | Tab-complete on tensor(: |
| Composability | String concatenation for multi-step expressions | T[-a,-b] + S[-a,-b] is valid Julia |
| IDE support | No completions, no hover docs | Full LSP support on tensor heads and indices |
The typed layer is a thin wrapper — it serializes to strings, calls the same battle-tested engine, and reconstructs typed results where supported. Its main benefit is correctness and ergonomics at the API boundary, not a new execution engine or guaranteed performance improvement.
Quick Start (Julia)
using XAct
reset_state!()
def_manifold!(:M, 4, [:a, :b, :c, :d, :e, :f])
def_metric!(-1, "g[-a,-b]", :CD) # creates Riemann, Ricci, Weyl, ...
# Step 1: declare index variables bound to a manifold
@indices M a b c d e f
# Step 2: get tensor handles
Riem = tensor(:RiemannCD)
Ric = tensor(:RicciCD)
g_h = tensor(:g)
# Step 3: write expressions
ToCanonical(Riem[-a,-b,-c,-d] + Riem[-a,-c,-d,-b] + Riem[-a,-d,-b,-c]) # "0"
ToCanonical(Riem[-a,-b,-c,-d] - Riem[-c,-d,-a,-b]) # "0"
# Contraction
def_tensor!(:V, ["a"], :M)
V = tensor(:V)
Contract(V[a] * g_h[-a,-b]) # "V[-b]"
# Rank-0 scalar: RS[] with empty index list
RS = tensor(:RicciScalarCD)
Simplify(RS[] * g_h[-a,-b])Core Concepts
The typed API is built on four building blocks: index variables (@indices), tensor handles (tensor()), covariant derivative heads (covd()), and arithmetic operators.
Index variables — @indices
@indices M a b c d e fCreates Idx objects bound to manifold :M. The macro validates that each label is registered for that manifold at runtime.
a is contravariant (up); -a (unary minus) is covariant (down):
a # Idx(:a, :M) — contravariant
-a # DnIdx — covariant
-(-a) # Idx(:a, :M) — back to contravariantTensor handles — tensor()
T = tensor(:T) # registered tensor
Riem = tensor(:RiemannCD) # auto-created by def_metric!tensor() returns a TensorHead — a lightweight named handle. It is not a TExpr; you must apply indices to get an expression:
T # TensorHead(:T) — not usable in arithmetic
T[-a,-b] # TTensor — valid TExpr, ready for operatorsCovariant derivative heads — covd()
def_tensor!(:phi, String[], :M) # scalar field (rank-0)
phi = tensor(:phi)
CD = covd(:CD)
expr = CD[-a](CD[-b](phi[])) # nabla_a nabla_b phi
CommuteCovDs(expr, "CD", "-a", "-b") # typed overload, no manual string conversionArithmetic
All TExpr subtypes support +, -, *, and unary -:
# assumes reset_state!() + def_manifold!(:M, 4, ...) + def_metric!(-1,"g[-a,-b]",:CD) already run
def_tensor!(:T, ["-a", "-b"], :M; symmetry_str="Symmetric[{-a,-b}]")
def_tensor!(:S, ["-a", "-b"], :M)
@indices M a b c
T = tensor(:T)
S = tensor(:S)
T[-a,-b] + S[-a,-b] # TSum
T[-a,-b] * S[-c,-d] # TProd
2 * T[-a,-b] # TProd with coeff=2
(1//3) * T[-a,-b] # Rational coefficient
-(T[-a,-b]) # TProd with coeff=-1
T[-a,-b] - S[-a,-b] # TSum with negated termUse Rational{Int} for exact results: (1//3) * T[-a,-b] not 0.333 * T[-a,-b].
Validation at construction time
Errors are caught when you build expressions, not when you call the engine:
# Slot count
Riem[-a,-b,-c] # ERROR: RiemannCD has 4 slots, got 3
# Manifold membership
def_manifold!(:N, 3, [:i, :j, :k])
@indices N i j k
Riem[-a,-b,-i,-j] # ERROR: index i is from manifold N, slot 3 expects M
# Tensor not defined
tensor(:Undefined) # ERROR: Tensor Undefined is not defined (was reset_state!() called?)
# Index not registered for manifold
@indices M x y # ERROR: Index x is not registered for manifold MPython quick start
import xact
xact.reset()
M = xact.Manifold("M", 4, ["a", "b", "c", "d", "e", "f"])
g = xact.Metric(M, "g", signature=-1, covd="CD")
# Typed index objects
a, b, c, d, e, f = xact.indices(M)
# Tensor handles
Riem = xact.tensor("RiemannCD")
V = xact.Tensor("V", ["a"], M)
g_h = xact.tensor("g")
# Build expressions with operators
expr = Riem[-a,-b,-c,-d] + Riem[-a,-c,-d,-b] + Riem[-a,-d,-b,-c]
xact.canonicalize(expr) # "0"
xact.contract(V[a] * g_h[-a,-b]) # "V[-b]"
# Error at construction
xact.tensor("RiemannCD")[-a,-b,-c] # IndexError: RiemannCD has 4 slots, got 3Interoperability with the string API
All engine functions accept both String and TExpr. If you pass a TExpr, it is first serialized into the current string-based engine. Some typed entry points reconstruct typed results on the way out; string inputs continue to return strings.
r1 = ToCanonical(Riem[-a,-b,-c,-d] + Riem[-a,-c,-d,-b])
r2 = Contract(r1)
r3 = Simplify(r2)You can freely mix the two styles:
# Start typed, continue with strings
step1 = ToCanonical(Riem[-a,-b,-c,-d] + Riem[-a,-c,-d,-b] + Riem[-a,-d,-b,-c])
step2 = RiemannSimplify(step1, :CD)Architecture
The typed layer is a thin serialization wrapper — the engine is untouched:
User code TExpr layer Engine
--------- ----------- ------
T[-a,-b] -> TTensor(:T, [DnIdx..])
|
_to_string()
|
"T[-a,-b]" -> _parse_expression()
_canonicalize_term()
canonicalize_slots() ← XPerm
_apply_identities!()
|
result string <- "T[-a,-b]"XPerm and the core canonicalization engine operate entirely on strings and slot positions. The typed layer never reaches into them.
Roadmap
| Stage | Status | Description |
|---|---|---|
| Stage 1 | ✅ Shipped | Typed construction, validation, serialization |
| Stage 2 | ✅ Shipped | Typed construction + typed integration over the existing string engine |
| Stage 3 | Planned | Rich display — Unicode REPL, LaTeX for Jupyter |
| Stage 4 | Planned | Introspection — free_indices(), rank(), terms(). |
For the full design rationale, see the TExpr design spec.