Tensor0 Usage Guide
This guide documents the current Tensor0 public API. It focuses on small, deterministic examples that can be copied into tests or interactive sessions.
Local Checkout Setup
For a non-editable user install and build prerequisites, see Installation. The commands below are for contributors; Development explains compiler memory budgeting and verification.
From the repository root:
uv sync --group dev
uv run maturin develop
Run the current verification set with:
cargo test
uv run pytest tests -q
Supported Sector Families
Tensor0 currently supports these public sector families and aliases:
Trivialfor ordinary tensors without symmetryU1IrrepZ2Irrep,Z3Irrep,Z4IrrepFermionParitySU2IrrepFermionNumberFermionParityU1IrrepU1SU2IrrepFermionParitySU2IrrepFermionParityU1SU2Irrep
Arbitrary dynamic product sector families are not currently supported.
Ordinary Tensors with Trivial Symmetry
Trivial is the no-symmetry sector family. Its only sector label is the empty
tuple (). It is a SectorType constant, not a callable sector-value class.
These four spellings return the same ordinary ElementarySpace:
from tensor0 import ComplexSpace, Trivial, Vect, space
ordinary = Vect(4)
assert ordinary == Vect[Trivial](4)
assert ordinary == space(Trivial, {(): 4})
assert ordinary == ComplexSpace(4)
Vect(dim=0, *, dual=False) is the canonical concise constructor.
ComplexSpace(...) remains a compatibility spelling and returns exactly the
same native space without a separate class. Dimensions are non-boolean,
non-negative integers. Dual, zero, and unit spaces therefore use the same APIs:
from tensor0 import ComplexSpace, Trivial, Vect, unit_space, zero_space
dual = Vect(4, dual=True)
assert dual == Vect(4).dual()
assert Vect() == zero_space(Trivial)
assert Vect(1) == unit_space(Trivial)
assert ComplexSpace(4) == Vect(4)
A rank-zero HomSpace has no visible factor from which to infer its sector
family, so pass sector_type=Trivial explicitly:
import jax.numpy as jnp
from tensor0 import Trivial, from_dense, hom, scalar
rank_zero = hom((), (), sector_type=Trivial)
value = from_dense(rank_zero, jnp.asarray(2.0))
assert float(scalar(value)) == 2.0
Every correctly shaped dense array is valid for a Trivial HomSpace.
from_dense() accepts either the full visible shape or its codomain-by-domain
matrix shape. The sole coupled block uses the sector key () and has the
matrix shape:
import jax.numpy as jnp
from tensor0 import Vect, from_dense, hom, to_dense
target = hom((Vect(2), Vect(3)), (Vect(4),))
dense = jnp.arange(24, dtype=jnp.float32).reshape(2, 3, 4)
tensor = from_dense(target, dense)
assert tensor.block(()).shape == (6, 4)
assert bool(jnp.array_equal(tensor.block(()), dense.reshape(6, 4)))
assert bool(jnp.array_equal(to_dense(tensor), dense))
Trivial tensors use the same composition, contraction, trace, transform, and factorization APIs as symmetric tensors. For example:
import jax.numpy as jnp
from tensor0 import (
Vect,
from_dense,
hom,
scalar,
svd_compact,
tensorcontract,
tensortrace,
to_dense,
)
left_dense = jnp.arange(6, dtype=jnp.float32).reshape(2, 3)
right_dense = jnp.arange(12, dtype=jnp.float32).reshape(3, 4)
left = from_dense(hom((Vect(2),), (Vect(3),)), left_dense)
right = from_dense(hom((Vect(3),), (Vect(4),)), right_dense)
contracted = tensorcontract(
left,
right,
axes=((1,), (0,)),
output=(((0, 0),), ((1, 1),)),
)
assert bool(jnp.allclose(to_dense(contracted), left_dense @ right_dense))
square_dense = jnp.arange(9, dtype=jnp.float32).reshape(3, 3)
square = from_dense(
hom((Vect(3),), (Vect(3),)),
square_dense,
)
traced = tensortrace(square, axes=((0,), (1,)), output=((), ()))
assert bool(jnp.allclose(scalar(traced), jnp.trace(square_dense)))
u, s, vh = svd_compact(left)
assert bool(jnp.allclose(to_dense(u @ s @ vh), left_dense, atol=1e-5))
For Trivial tensors, to_dense() is a direct immutable JAX reshape of packed
storage. from_dense() likewise uses a direct reshape when the input already
has the target storage dtype; required dtype promotion may materialize a new
array. Neither API promises physical buffer aliasing or a mutable/write-through
view. Nontrivial sector families continue to use symmetry-aware projection and
reconstruction, including tol validation in from_dense(); they do not use
Trivial's dense numerical fast paths.
Tensor0 does not currently support product-sector families such as
Trivial @ U1Irrep, PlanarTrivial, real or Cartesian space families,
mutable dense/block views, or generic dense execution for nontrivial
symmetries. Use tensor.block(()) for the sole Trivial block; tensor[()] is
not a dense-array access spelling.
Spaces, HomSpaces, and TensorMap
space(...) builds an elementary graded space. The dictionary maps sector labels
to degeneracy dimensions. hom(codomain, domain) builds the typed linear-map
space used by TensorMap; its two sides are public ProductSpace values.
import jax.numpy as jnp
from tensor0 import (
TensorMap,
U1Irrep,
dim,
hom,
reduced_dim,
space,
storage_dim,
)
v = space(U1Irrep, {0: 2, 1: 3})
h = hom((v,), (v,))
assert dim(v) == 5
assert reduced_dim(v) == 5
assert dim(h.codomain) == 5
total_dim = storage_dim(h)
tensor = TensorMap(h, jnp.arange(total_dim, dtype=jnp.float32))
sector_0_block = tensor.block(0)
sector_1_block = tensor.block((1,))
ordered_blocks = tensor.blocks()
subblocks = tensor.subblocks()
(row_tree, col_tree), reduced = next(iter(subblocks))
assert tensor[row_tree, col_tree].shape == reduced.shape
# U1 has UniqueFusion. Domain indices use visible (dual) sector labels.
charge_one = tensor[1, -1]
Physical dim, elementary reduced_dim, and packed storage_dim are distinct:
the first includes quantum dimensions, the second sums degeneracy dimensions,
and the third is the required 1D storage length for a HomSpace. TensorMap
stores that HomSpace and a VectorStorage-compatible JAX array. Advanced
layout records and accessors, including get_degeneracystructure, remain public
from tensor0.structure rather than the package root.
A rank-zero map has no factor from which to infer its sector family, so it must be typed explicitly:
scalar_space = hom((), (), sector_type=U1Irrep)
scalar_tensor = TensorMap(scalar_space, jnp.asarray([2.0]))
The public space algebra is fuse, unit_space, zero_space, infimum,
supremum, direct_sum, is_isomorphic, is_monomorphic, and
is_epimorphic. Binary algebra requires matching sector families and dual
flags. Empty ProductSpace values retain their sector family in equality,
hashing, caching, and JAX static metadata; use .spaces when a tuple of factors
is needed.
Use scalar(tensor) only for scalar TensorMaps with no visible indices.
tensor.subblocks() is a reusable lazy view in canonical fusion-tree order;
each iteration creates and reads one subblock at a time. The view supports
len(view), integer and negative indexing, and slices; a slice returns an
ordinary tuple. Use either
tensor[row_tree, col_tree] / tensor.subblock(row_tree, col_tree) or a tuple
of visible sectors for UniqueFusion families, such as tensor[1, -1] /
tensor.subblock((1, -1)). A tuple-valued product sector on a rank-one tensor
needs an outer tuple, for example tensor[((0, 0),)]. Returned JAX arrays are
immutable values rather than write-through views.
Composition
TensorMap @ TensorMap composes matching coupled blocks. Tensor0 follows the
hom(codomain, domain) convention, so the left domain must equal the right
codomain and the result has the left codomain and right domain.
import jax.numpy as jnp
from tensor0 import TensorMap, U1Irrep, hom, space
from tensor0.structure import get_degeneracystructure
def data_for(h):
total_dim = get_degeneracystructure(h).total_dim
return jnp.arange(1, total_dim + 1, dtype=jnp.float32) / 10.0
v = space(U1Irrep, {0: 2, 1: 3})
w = space(U1Irrep, {0: 5, 1: 7})
x = space(U1Irrep, {0: 11, 1: 13})
a_space = hom((v,), (w,))
b_space = hom((w,), (x,))
a = TensorMap(a_space, data_for(a_space))
b = TensorMap(b_space, data_for(b_space))
c = a @ b
assert c.space == hom((v,), (x,))
For partial contractions, traces, named tensor networks, and integer-label networks, see the contraction guide.
Inverse, Pseudoinverse, and Direct Solves
inverse(tensor) and tensor.inverse() invert every square reduced block and
return the reversed HomSpace. pseudoinverse(tensor, *, atol=0.0, rtol=None)
and its method form also support rectangular maps and apply one global singular
value cutoff. Both functional forms accept ordinary and diagonal tensor maps.
Use direct solves when the goal is an equation rather than an inverse:
from tensor0 import inverse, left_solve, right_solve
operator_inverse = inverse(operator)
x_left = left_solve(operator, rhs) # operator @ x_left == rhs
x_right = right_solve(lhs, operator) # x_right @ operator == lhs
The operator in either solve must be a blockwise square isomorphism. Both solves call JAX's linear solver directly on each reduced block; they do not form an explicit inverse. Singularity and nonsingularity are numerical preconditions, so traced execution follows JAX numerical behavior rather than raising data-dependent Python exceptions.
Pseudoinverse rejects singular values at or below
max(atol, rtol * max(singular_values)). If rtol is omitted, it is ten times
the largest reduced block dimension times machine epsilon for the promoted
real dtype. This policy is shared by ordinary and diagonal tensor maps.
Morphism Constructors
identity(space, *, dtype=None) constructs the identity on an elementary or
product space. isomorphism(codomain, domain, *, dtype=None) and
unitary(codomain, domain, *, dtype=None) require isomorphic spaces, while
isometry(codomain, domain, *, dtype=None) requires the domain to embed into
the codomain sector by sector.
import jax.numpy as jnp
from tensor0 import (
U1Irrep,
allclose,
identity,
isometry,
isomorphism,
space,
unitary,
)
small = space(U1Irrep, {0: 2, 1: 1})
large = space(U1Irrep, {0: 3, 1: 2})
unit = identity(small, dtype=jnp.float32)
isomorphic = isomorphism(small, small, dtype=jnp.float32)
basis_change = unitary(small, small, dtype=jnp.float32)
embedding = isometry(large, small, dtype=jnp.float32)
assert bool(allclose(isomorphic, unit, rtol=1e-5, atol=1e-6))
assert bool(allclose(basis_change, unit, rtol=1e-5, atol=1e-6))
assert bool(
allclose(
embedding.adjoint() @ embedding,
unit,
rtol=1e-5,
atol=1e-6,
)
)
Product-to-fused isomorphisms and unitaries use Tensor0's existing fusion-tree
gauge. Invalid non-isomorphic or non-monomorphic inputs fail before numerical
allocation. With dtype=None, allocation follows the configured JAX default;
explicit real and complex dtypes are preserved. identity is the only public
identity name—there is no id alias or constructor classmethod.
For the spaces currently supported by Tensor0, isomorphism and unitary use
the same deterministic gauge-fixed construction. The unitary name records
the stronger contract that reversing the map agrees with both its inverse and
its adjoint; it does not select a different numerical construction.
Tensor Product
tensor_product(left, right) is the explicit disconnected tensor-network
product. Its codomain factors are ordered as left then right, followed by the
left-then-right domain factors in the domain partition:
from tensor0 import U1Irrep, identity, space, tensor_product
left_space = space(U1Irrep, {0: 2})
right_space = space(U1Irrep, {0: 3})
left = identity(left_space)
right = identity(right_space)
product = tensor_product(left, right)
assert product.codomain.spaces == (left_space, right_space)
assert product.domain.spaces == (left_space, right_space)
Both inputs must use the same sector family. The implementation uses the tested disconnected-contraction semantics, including fusion coefficients, fermionic input order, scalar operands, JIT, and differentiation. Tensor0 does not attach this operation to an overloaded tensor-product operator.
Explicit-Key Random Construction
random_normal(key, space, *, dtype=None) samples the packed reduced storage
of a HomSpace directly. random_isometry(key, codomain, domain, *,
dtype=None) samples and blockwise orthogonalizes an isometric embedding while
preserving the exact requested product-space partitions. Tensor0 passes the supplied key to exactly one
jax.random.normal call without pre-splitting or mutating it, returning a
replacement key, or reading global RNG state. JAX may internally split the key
when implementing complex sampling:
import jax
import jax.numpy as jnp
from tensor0 import U1Irrep, equal, hom, is_isometric, random_isometry, random_normal, space
factor = space(U1Irrep, {0: 2, 1: 1})
target = hom((factor,), (factor,))
key = jax.random.key(0)
sample = random_normal(key, target, dtype=jnp.complex64)
repeated = random_normal(key, target, dtype=jnp.complex64)
assert bool(equal(sample, repeated))
large = space(U1Irrep, {0: 3, 1: 2})
embedding = random_isometry(key, large, factor, dtype=jnp.float32)
assert embedding.codomain.spaces == (large,)
assert embedding.domain.spaces == (factor,)
assert bool(is_isometric(embedding, rtol=1e-5, atol=1e-6))
dtype=None follows jax.random.normal's configured real default. An explicit
complex dtype follows JAX's complex-normal semantics, and an empty HomSpace
produces empty storage of the requested dtype. Capture the space and dtype in a
jitted wrapper, for example jax.jit(lambda key: random_normal(key, target)),
so the spaces remain static metadata. There are no randn or randisometry
aliases, classmethods, implicit keys, or global generators.
Compact QR/LQ and Orthogonalization
qr_compact(tensor) returns (q, r) with q @ r == tensor, while
lq_compact(tensor) returns (l, q) with l @ q == tensor. Both use the
deterministic non-dual connecting space
infimum(fuse(tensor.codomain), fuse(tensor.domain)). For QR this space is
q.domain == r.codomain; for LQ it is l.domain == q.codomain.
import jax.numpy as jnp
from tensor0 import U1Irrep, allclose, from_blocks, hom, lq_compact, qr_compact, space
factor = space(U1Irrep, {0: 2})
target = hom((factor,), (factor,))
tensor = from_blocks(
target,
{0: jnp.asarray([[2.0, 1.0], [1.0, 3.0]], dtype=jnp.float32)},
)
left_q, r = qr_compact(tensor)
l, right_q = lq_compact(tensor)
assert left_q.domain == r.codomain
assert l.domain == right_q.codomain
assert bool(allclose(left_q @ r, tensor, rtol=1e-5, atol=1e-6))
assert bool(allclose(l @ right_q, tensor, rtol=1e-5, atol=1e-6))
Each nonzero diagonal entry of R or L is normalized to a positive real
value by moving its sign or complex phase into the corresponding column or row
of Q. A zero diagonal entry uses the neutral phase 1, preserving
reconstruction and static compact shapes.
left_orth(tensor, *, alg=None, trunc=None) and
right_orth(tensor, *, alg=None, trunc=None) are the tensor-network-oriented
interfaces. Without optional arguments they use qr_compact and lq_compact,
respectively. With alg="svd", the compact SVD is grouped as (u, s @ vh) for
left orthogonalization or (u @ s, vh) for right orthogonalization.
Supplying a truncation strategy selects the SVD path automatically and reduces
the connecting space. An explicit alg="qr" or alg="lq" cannot be combined
with trunc. These high-level functions return only the two grouped factors;
call svd_trunc directly when the discarded-weight error is required. Polar
orthogonalization and backend algorithm objects are not supported.
Factor dtypes follow JAX linear-algebra promotion. In particular, integer or boolean storage produces inexact QR/LQ factors rather than being cast back to the input dtype; typed empty outputs use the same promotion rule.
Rank-deficient and zero blocks support eager and JIT value computation, but their output shapes do not shrink with numerical rank. Differentiation is promised only for appropriate full-rank blocks with nonzero normalized diagonals; Tensor0 does not promise QR/LQ derivatives at rank deficiency.
Hermitian Eigenvalues and Eigenvectors
is_hermitian(tensor, *, atol=0.0, rtol=0.0) returns a scalar JAX boolean
array. Its default check is exact; nonzero tolerances are always explicit.
eigh_vals(tensor) returns a real SectorVector, and eigh_full(tensor)
returns (d, v) with eigenvalues ordered within each sector and reconstruction
tensor == v @ d @ v.adjoint(). eigh_trunc(tensor, *, trunc=None) returns the
same (d, v) shape after selecting by absolute eigenvalue magnitude while
preserving eigenvalue signs and JAX's ascending within-sector order.
from tensor0 import allclose, eigh_full, eigh_trunc, eigh_vals, is_hermitian, truncrank
assert bool(is_hermitian(tensor, rtol=1e-5, atol=1e-6))
values = eigh_vals(tensor)
d, v = eigh_full(tensor)
reconstructed = v @ d @ v.adjoint()
assert bool(allclose(reconstructed, tensor, rtol=1e-5, atol=1e-6))
truncated_d, truncated_v = eigh_trunc(tensor, trunc=truncrank(1))
projected = truncated_v @ truncated_d @ truncated_v.adjoint()
All three functions first require the static TensorMap space to be an
endomorphism with equal codomain and domain. Hermiticity itself is a numerical
precondition: eigh_vals and eigh_full do not convert a predicate to a Python
boolean, raise on data inside JIT, project, or symmetrize a non-Hermitian input.
Eager callers that need validation should call
bool(is_hermitian(tensor, ...)) before decomposition; jitted callers can keep
the predicate as a JAX scalar.
eigh_trunc accepts the same rank, tolerance, error, space, and combined
truncation strategies as svd_trunc, but intentionally does not return a
discarded-weight error because signed Hermitian spectra need a separate error
contract. Nontrivial strategies select value-dependent output shapes and must
therefore run eagerly. The no-truncation path has fixed shapes and supports
whole-function JIT; results selected eagerly remain ordinary JAX-compatible
tensor maps for downstream jitted computation.
Eigenvalue and eigenvector dtypes also follow JAX promotion: integer or boolean storage produces inexact eigenvectors and real inexact eigenvalues, including for typed empty outputs.
At degenerate spectra, eigenvalues and reconstruction remain supported in eager and JIT execution, but the eigenvector basis is only as deterministic as the JAX backend. Eigenvector derivatives and a unique basis are not promised at degeneracy; gradient guarantees are restricted to nondegenerate spectra.
SVD
svd_vals(...) returns singular values as a SectorVector over the infimum
bond space. svd_compact(...) returns (u, s, vh), where s is a
DiagonalTensorMap and composes directly with ordinary tensor maps.
svd_full(...) returns (u, s, vh) over the fused codomain/domain spaces, with
s as a regular TensorMap.
import jax.numpy as jnp
from tensor0 import TensorMap, U1Irrep, hom, space, svd_compact
from tensor0.structure import get_degeneracystructure
def data_for(h):
total_dim = get_degeneracystructure(h).total_dim
return jnp.arange(1, total_dim + 1, dtype=jnp.float32) / 10.0
left = space(U1Irrep, {0: 2, 1: 4})
right = space(U1Irrep, {0: 3, 1: 2})
h = hom((left,), (right,))
tensor = TensorMap(h, data_for(h))
u, s, vh = svd_compact(tensor)
reconstructed = u @ s @ vh
assert s.domain.spaces == (s.index_space,)
assert s.codomain.spaces == (s.index_space,)
DiagonalTensorMap exposes the shared tensor metadata and block interface,
diagonal-preserving arithmetic, adjoint, norm, inverse, and cutoff
pseudoinverse. to_tensor_map() remains available for interoperability, but is
not required for ordinary composition or SVD reconstruction. Pseudoinverse uses
the shared ordinary/diagonal reduced-block cutoff described above and maps
values at or below the cutoff to zero; both tolerances are non-negative.
Use svd_trunc(...) with notrunc(), truncrank(...), trunctol(...),
truncspace(...), or truncerror(...) when a truncation strategy is needed.
rank(...) and cond(...) are SVD-derived helpers; rank counts sector ranks
with quantum-dimension weighting, and cond currently supports the 2-norm.
Predicates and Comparison
TensorMap.dtype and DiagonalTensorMap.dtype report the storage dtype.
is_diagonal(...), is_isometric(...), is_unitary(...),
is_positive_definite(...), equal(...), and allclose(...) return scalar
JAX boolean arrays, so eager code may call bool(...) while jitted code keeps
the result as an array. Exact comparison requires equal spaces, dtypes, and values.
Approximate comparison requires equal spaces, uses JAX numerical promotion, and
requires both tolerances as keyword arguments:
from tensor0 import allclose, equal, is_diagonal, is_positive_definite, is_unitary
diagonal_predicate = is_diagonal(s)
same_values = allclose(reconstructed, tensor, rtol=1e-5, atol=1e-6)
exact_copy = equal(tensor, tensor.copy())
unitary_predicate = is_unitary(identity(s.index_space))
positive_predicate = is_positive_definite(identity(s.index_space))
Mixed ordinary/diagonal comparison uses mathematical tensor values. Neither
comparison contract is attached to TensorMap.__eq__.
is_isometric(tensor, side="left") checks tensor.adjoint() @ tensor against
the domain identity. With side="right", it checks
tensor @ tensor.adjoint() against the codomain identity. The side is static
configuration in jitted code, so capture it in a wrapper such as
jax.jit(lambda value: is_isometric(value, side="right")). A unitary checks
both identities and first requires statically isomorphic spaces. A statically
impossible relation returns scalar false without reading numerical storage.
Positive definiteness requires an equal-space endomorphism and checks both
Hermiticity and that every eigenvalue exceeds
max(atol, rtol * max(abs(eigenvalues))). Empty isometries, unitaries, and
positive-definite endomorphisms use the explicit vacuous truth value True.
Transforms
The public transform helpers are permute(...), braid(...), transpose(...),
repartition(...), flip(...), twist(...), insertleftunit(...),
insertrightunit(...), and removeunit(...). The corresponding TensorMap
methods have the same behavior and error contracts.
import jax.numpy as jnp
from tensor0 import TensorMap, U1Irrep, hom, space, storage_dim
def data_for(h):
total_dim = storage_dim(h)
return jnp.arange(total_dim, dtype=jnp.float32)
v = space(U1Irrep, {0: 2, 1: 1})
w = space(U1Irrep, {0: 1, 1: 2})
x = space(U1Irrep, {1: 1})
h = hom((v, w), (x,))
tensor = TensorMap(h, data_for(h))
result = tensor.permute(((1,), (0, 2)))
assert result.space == hom((w,), (v.dual(), x))
flip(tensor, indices, inv=False) changes the duality presentation of the
selected 0-based visible indices without moving them across the
codomain/domain partition. It preserves visible sector multiplicities and
applies the fusion-tree Z-isomorphism coefficient:
from tensor0 import flip
flipped = flip(tensor, (0, 2))
restored = flip(flipped, (0, 2), inv=True)
assert restored.space == tensor.space
flip is distinct from elementary-space dual() and is not generally
involutory: two forward flips can introduce a phase. Use one forward and one
inverse flip to restore the original tensor. Flipping both sides of a matching
contraction leg preserves the contraction result.
Unit-Space Insertion and Removal
The unit-space operations have these signatures:
insertleftunit(tensor, position=None, *, dual=False)
insertrightunit(tensor, position=None, *, dual=False)
removeunit(tensor, index)
position is a 0-based boundary in 0..tensor.numind: it is the number of
original visible indices before the new unit, and the new unit appears at
visible index position. The default is tensor.numind, the final boundary.
For M = tensor.numout, the two insertion operations differ only at the
codomain/domain boundary:
| Boundary | insertleftunit attaches to |
insertrightunit attaches to |
|---|---|---|
0 <= position < M |
codomain at position |
codomain at position |
position == M |
first domain factor | final codomain factor |
M < position <= tensor.numind |
domain at position - M |
domain at position - M |
Consequently, on a rank-zero TensorMap, left insertion creates a domain factor and right insertion creates a codomain factor. Removing the newly inserted visible index is an exact structural inverse:
from tensor0 import insertleftunit, insertrightunit, removeunit
boundary = tensor.numout
left = insertleftunit(tensor, boundary)
right = insertrightunit(tensor, boundary)
assert left.numout == tensor.numout
assert left.numin == tensor.numin + 1
assert right.numout == tensor.numout + 1
assert right.numin == tensor.numin
assert removeunit(left, boundary).space == tensor.space
assert removeunit(right, boundary).space == tensor.space
dual selects the canonical unit or its dual as the factor attached to the
destination codomain or domain ProductSpace. A visible domain leg is, by the
HomSpace convention, the dual of its stored domain factor. Its visible
duality presentation is therefore opposite to the value of dual; codomain
legs present the value directly.
For all currently supported sector families, unit insertion preserves the
canonical flattened layout. The returned TensorMap therefore shares the
source's VectorStorage without writing to its payload, preserves dtype, and
does not perform a dense conversion or data remap. This sharing is safe for the
normal immutable JAX Array payload:
assert left.storage is tensor.storage
assert right.storage is tensor.storage
assert removeunit(left, boundary).storage is tensor.storage
position, index, and dual are structural metadata and must be static
under jax.jit. Capture them in a closure, as below, or mark the corresponding
arguments static with static_argnums or static_argnames:
import jax
insert_at_boundary = jax.jit(
lambda value: insertleftunit(value, boundary, dual=True)
)
compiled_left = insert_at_boundary(tensor)
removeunit accepts a canonical unit factor and its dual, but rejects a
one-dimensional factor carrying a non-unit sector, a higher-multiplicity unit,
or a factor with any additional nonzero sector. Insertion requires a
TensorMap, an integer boundary or None, and a boolean dual; removal
requires a TensorMap and an integer index. Booleans are not accepted as
indices. Invalid types raise TypeError, while an out-of-range boundary or
index and removal of a non-unit factor raise ValueError.
Tensor0 supports simple monoidal units for its current built-in sector
families. These operations do not expose TensorKit's mutable copy option or
its elementary-space conj option, and they are not a general singleton-axis,
reshape, or squeeze API.
For small correctness checks, compare transform results through public dense conversion.
Dense Conversion
to_dense(...) and from_dense(...) are public APIs. Trivial tensors use the
direct immutable array path described above. Other sector families use these
APIs as correctness-first projection and reconstruction utilities for small
examples and tests.
import jax.numpy as jnp
from tensor0 import TensorMap, U1Irrep, from_dense, hom, space, storage_dim, to_dense
v = space(U1Irrep, {0: 2, 1: 3})
h = hom((v,), (v,))
total_dim = storage_dim(h)
tensor = TensorMap(h, jnp.arange(total_dim, dtype=jnp.float32))
dense = to_dense(tensor)
rebuilt = from_dense(tensor.space, dense)
Dense conversion does not enable dense numerical execution for nontrivial sector families.
JAX jit and grad
TensorMap and DiagonalTensorMap are JAX pytrees. Their storage arrays are the
dynamic leaves, while space metadata is static auxiliary data.
import jax
import jax.numpy as jnp
from tensor0 import TensorMap, U1Irrep, hom, space, storage_dim
def data_for(h):
total_dim = storage_dim(h)
return jnp.arange(1, total_dim + 1, dtype=jnp.float32) / 10.0
v = space(U1Irrep, {0: 2, 1: 3})
w = space(U1Irrep, {0: 5, 1: 7})
x = space(U1Irrep, {0: 11, 1: 13})
a_space = hom((v,), (w,))
b_space = hom((w,), (x,))
a = TensorMap(a_space, data_for(a_space))
b = TensorMap(b_space, data_for(b_space))
@jax.jit
def compose(left, right):
return left @ right
jitted = compose(a, b)
def loss(data):
candidate = TensorMap(a.space, data)
composed = candidate @ b
return jnp.sum(composed.storage.data ** 2)
value, gradient = jax.value_and_grad(loss)(a.storage.data)
Changing only storage values keeps the same pytree structure. Changing the
HomSpace metadata creates a different static JAX specialization.
For optimization over a TensorMap argument, use tensor0.grad or
tensor0.value_and_grad. These wrappers return the Riesz gradient for the
quantum-dimension-weighted TensorMap inner product. Ordinary JAX array
arguments retain their coordinate gradients.
import tensor0
def tensor_loss(candidate):
return jnp.real(tensor0.inner(candidate, candidate))
value, gradient = tensor0.value_and_grad(tensor_loss)(a)
updated = a - 0.01 * gradient
jax.grad(tensor_loss)(a) remains available and returns the packed-storage
coordinate cotangent. If the differentiated argument is an array and the
function constructs a TensorMap internally, tensor0.grad likewise returns
an array coordinate gradient; the wrapper does not infer a metric from
intermediate values. TensorMap Riesz gradients currently require a real-valued
objective and do not support holomorphic=True or DiagonalTensorMap
arguments.
Current Boundaries
Tensor0 currently does not include:
- GenericFusion
- anyonic braiding
- arbitrary dynamic product sector families
- full NumPy einsum syntax or automatic contraction-order optimization
- mutable block views or in-place public APIs
- generic dense numerical execution for nontrivial sector families
- native JAX kernels
- performance guarantees
This guide only documents the public API behavior available in the current checkout.