tenet¶
The top-level namespace: tensors, legs, spaces, structures and the tensor operations.
tenet ¶
symtenet: non-Abelian symmetric tensors with ndarray-style APIs.
SymmetricTensor is the tensor type. Its categorical structure is
Leg (IN/OUT, Side),
GradedSpace / ProductSpace,
FusionTree and TensorStructure, and
as_map views it as a map from domain to codomain.
The operations group by what a caller comes for: arithmetic and reductions
(add, multiply, norm, trace,
inner, allclose); contraction (einsum,
tensordot, compose); leg and axis moves
(transpose, repartition, bend,
fuse / unfuse, conj,
adjoint); construction (identity,
isometry, from_matrices,
direct_sum, embed,
to_symmetry); and block access (to_matrices,
apply_blocks).
save / load persist one tensor, and anything that would change
block structure raises StructureChangingError.
Every function that validates against an atol takes PROJECT there to
mean "project, don't check" instead.
Submodules with their own pages: tenet.linalg (decompositions), tenet.network
(DMRG and CTMRG), tenet.symmetry (providers and sector labels), and the opt-in JAX
seams tenet.pytree and tenet.ad, which enable_jax turns on.
PROJECT
module-attribute
¶
The atol that means "project onto the symmetric subspace, do not check".
Pass it wherever an atol is accepted — from_dense,
restrict, to_symmetry — to skip the concrete-value
check those functions otherwise run, which is what makes them traceable:
import math, tenet tenet.PROJECT is math.inf True
It is math.inf, not a distinct object: infinity is the limit of a tolerance, "any
residual acceptable", so the mode and the tolerance value coincide and every existing
atol=math.inf call site keeps working unchanged. The name exists because a call site
reading atol=tenet.PROJECT says which of the two operations is happening, while one
reading atol=math.inf requires the reader to know the idiom.
FusionTree
dataclass
¶
FusionTree(
uncoupled: tuple[Sector, ...],
inner: tuple[Sector, ...],
multiplicities: tuple[int, ...],
coupled: Sector,
)
Bases: _HashMemo
A left-associated fusion tree. Frozen, hashable, totally ordered.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uncoupled
|
tuple of Sector
|
The |
required |
inner
|
tuple of Sector
|
The |
required |
multiplicities
|
tuple of int
|
One label per vertex, length |
required |
coupled
|
Sector
|
The total sector |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from tenet import fusion_trees
>>> from tenet.symmetry import SU2, SU2Sector
>>> half = SU2Sector(1) # sectors are labelled by 2j
>>> trees = fusion_trees(SU2, (half, half, half), half)
>>> len(trees) # two independent trees, told apart by the internal line
2
>>> trees[0].lines()
(SU2Sector(two_j=1), SU2Sector(two_j=0), SU2Sector(two_j=1))
>>> trees[0].rank
3
rank
property
¶
N, the number of uncoupled sectors.
Returns:
| Type | Description |
|---|---|
int
|
|
lines ¶
lines() -> tuple[Sector, ...]
The left spine (e_0, ..., e_{N-1}); () for N == 0.
Returns:
| Type | Description |
|---|---|
tuple of Sector
|
The spine sectors, ending in |
vertices ¶
((e_k, u_{k+1}, e_{k+1}, mu_k), ...) for k = 0 .. N-2.
Returns:
| Type | Description |
|---|---|
tuple of (Sector, Sector, Sector, int)
|
One |
validate ¶
validate(provider: FusionRules) -> None
Raise if the tree is not a valid basis label for provider.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider
|
FusionRules
|
The provider whose fusion rules the tree must satisfy. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a rank-0 tree does not couple to the unit, a rank-1 tree does not couple to its own uncoupled sector, a vertex violates the fusion rules, or a multiplicity label is out of range. |
Leg
dataclass
¶
Leg(
space: GradedSpace,
side: Side,
dual: bool = False,
name: Hashable | None = None,
)
Bases: _HashMemo
One tensor axis: space, side, dual and an optional name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
space
|
GradedSpace
|
The graded representation space this axis carries. |
required |
side
|
Side
|
|
required |
dual
|
bool
|
Whether the axis carries |
False
|
name
|
Hashable or None
|
User bookkeeping label. Default |
None
|
Examples:
>>> from tenet import OUT, GradedSpace, Leg
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> leg = Leg(V, OUT, name="p")
>>> leg.degeneracy(U1Sector(0))
2
>>> leg.dualized().fused_sector(U1Sector(1))
U1Sector(charge=-1)
Notes
Frozen and hashable; name participates in equality like any other field.
provider
property
¶
sectors
property
¶
sectors: tuple[Sector, ...]
Space sectors in the space's canonical order.
Returns:
| Type | Description |
|---|---|
tuple of Sector
|
|
degeneracy ¶
degeneracy(a: Sector) -> int
m_a for a space label a (not a fused label).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
Sector
|
A sector labelling the space, in the space's own convention. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The degeneracy |
Notes
For a dual U(1) leg, fused_sector negates the charge, so feeding a
fused label here would silently read the wrong degeneracy — use
space_sector first.
fused_sector ¶
space_sector ¶
Inverse of fused_sector.
dual is an involution, so lossless.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
u
|
Sector
|
A fused (tree-side) sector label. |
required |
Returns:
| Type | Description |
|---|---|
Sector
|
The space label: |
dualized ¶
dualized() -> Leg
New leg with dual flipped — a relabelling of V ↔ V* only.
Returns:
| Type | Description |
|---|---|
Leg
|
A copy of this leg with |
Notes
tenet.flip_dual is the sanctioned numerical route for changing a leg's
dual flag: it also relabels the space through provider.dual and
pays the Z-isomorphism's scalar per fusion tree. repartition is the
route that changes side together with dual, paying the bending
coefficient. dualized() itself is metadata-only, and it is
not the metadata half of flip_dual: toggling the flag alone changes
which sector the leg contributes to a fusion tree (fused_sector goes
from a to dual(a)), so the block set genuinely changes and no
scalar can express the difference — never use dualized() to build a
flip.
Side ¶
Bases: Enum
Which half of Hom(domain, codomain) a leg belongs to.
MapLayout
dataclass
¶
MapLayout(
structure: TensorStructure,
axes_order: tuple[int, ...],
sectors: tuple[Sector, ...],
rows: tuple[Band, ...],
cols: tuple[Band, ...],
grid: tuple[
tuple[Sector, tuple[tuple[int, ...], ...]], ...
],
shapes: tuple[tuple[int, int], ...],
)
Where every block sits inside its coupled-sector matrix. Array-free, hashable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure
|
TensorStructure
|
The structure this layout was computed from. |
required |
axes_order
|
tuple of int
|
|
required |
sectors
|
tuple of Sector
|
The coupled sectors, sorted; one matrix |
required |
rows
|
tuple of Band
|
|
required |
cols
|
tuple of Band
|
|
required |
grid
|
tuple
|
Per coupled sector, block indices in row-major (row band × column band) order. |
required |
shapes
|
tuple of (int, int)
|
The |
required |
Notes
Built only by map_layout, never by hand. The block for
(ot, it) occupies rows [row offset, + Π m_out) and
columns [col offset, + Π m_in) of B_c — an outer reshape, no data
motion beyond one transpose and one reshape per block.
grid
instance-attribute
¶
grid: tuple[tuple[Sector, tuple[tuple[int, ...], ...]], ...]
Per coupled sector, block indices in row-major (row band × column band) order.
shapes
instance-attribute
¶
The shape of each B_c, aligned with sectors.
row_bands ¶
row_bands(
c: Sector,
) -> tuple[tuple[FusionTree, int, int], ...]
(output tree, offset, extent) for c, in row order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
c
|
Sector
|
A coupled sector of the layout. |
required |
Returns:
| Type | Description |
|---|---|
tuple of (FusionTree, int, int)
|
The row bands of |
col_bands ¶
col_bands(
c: Sector,
) -> tuple[tuple[FusionTree, int, int], ...]
(input tree, offset, extent) for c, in column order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
c
|
Sector
|
A coupled sector of the layout. |
required |
Returns:
| Type | Description |
|---|---|
tuple of (FusionTree, int, int)
|
The column bands of |
TensorMapView
dataclass
¶
TensorMapView(tensor: SymmetricTensor)
A semantic view of a tensor as a morphism. Nothing is moved or materialized.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tensor
|
SymmetricTensor
|
The tensor being viewed. Held, not copied. |
required |
Examples:
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> m = t.as_map()
>>> m.codomain.reduced_dim, m.domain.reduced_dim
(3, 3)
>>> u, s, vh = m.svd()
>>> s.ndim
2
Notes
as_map allocates nothing: the view holds the tensor and every property is
derived from the side metadata the legs already carry. Materializing an
(out..., in...) reordering here would violate
invariant 3 and fork T.as_map().svd() from svd(T, axes=...).
codomain
property
¶
codomain: ProductSpace
domain
property
¶
domain: ProductSpace
matrices ¶
matrices() -> dict[Sector, Any]
{c: B_c} — to_matrices of the underlying tensor.
Returns:
| Type | Description |
|---|---|
dict of Sector to array
|
One matrix per coupled sector. |
compose ¶
compose(
other: TensorMapView | SymmetricTensor,
) -> SymmetricTensor
self ∘ other. See tenet.compose.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
TensorMapView or SymmetricTensor
|
The morphism applied first; its codomain must match this map's domain. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The composition, as a tensor. |
adjoint ¶
adjoint() -> SymmetricTensor
T† in Hom(codomain, domain). See tenet.adjoint.
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The adjoint tensor. |
svd ¶
svd(
*, bond: GradedSpace | None = None
) -> tuple[
SymmetricTensor, SymmetricTensor, SymmetricTensor
]
U, S, Vh for the current partition. See svd.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bond
|
GradedSpace or None
|
|
None
|
Returns:
| Type | Description |
|---|---|
U, S, Vh : SymmetricTensor
|
The factors, as for svd. |
svd_truncated ¶
svd_truncated(
**kwargs: Any,
) -> tuple[
SymmetricTensor, SymmetricTensor, SymmetricTensor
]
Truncated U, S, Vh for the current partition. Not jittable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**kwargs
|
Any
|
Every keyword of svd_truncated, forwarded unchanged. |
{}
|
Returns:
| Type | Description |
|---|---|
U, S, Vh : SymmetricTensor
|
The truncated factors. |
qr ¶
qr() -> tuple[SymmetricTensor, SymmetricTensor]
Q, R for the current partition. See qr.
Returns:
| Type | Description |
|---|---|
Q, R : SymmetricTensor
|
The factors. |
eigh ¶
eigh() -> tuple[SymmetricTensor, SymmetricTensor]
W, V for the current partition. See eigh.
Returns:
| Type | Description |
|---|---|
W, V : SymmetricTensor
|
Eigenvalues and eigenvectors. |
eig ¶
eig() -> tuple[SymmetricTensor, SymmetricTensor]
W, V for the current partition. See eig.
Returns:
| Type | Description |
|---|---|
W, V : SymmetricTensor
|
Eigenvalues and eigenvectors. |
eigvals ¶
eigvals() -> SymmetricTensor
W for the current partition. See eigvals.
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The eigenvalues. |
expm ¶
expm(*, alpha: Any = 1.0) -> SymmetricTensor
exp(alpha * T) for the current partition. See expm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
scalar
|
The prefactor inside the exponential. Default |
1.0
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The matrix exponential. |
polar ¶
polar(
side: str = "left",
) -> tuple[SymmetricTensor, SymmetricTensor]
W, P for the current partition. See polar.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
side
|
str
|
|
'left'
|
Returns:
| Type | Description |
|---|---|
W, P : SymmetricTensor
|
The isometric and positive factors. |
left_null ¶
left_null() -> SymmetricTensor
N with N† T = 0 for the current partition. See
left_null.
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The left null space. |
right_null ¶
right_null() -> SymmetricTensor
N with T N† = 0 for the current partition. See
right_null.
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The right null space. |
lq ¶
lq() -> tuple[SymmetricTensor, SymmetricTensor]
L, Q for the current partition. See lq.
Returns:
| Type | Description |
|---|---|
L, Q : SymmetricTensor
|
The factors. |
GradedSpace
dataclass
¶
GradedSpace(
provider: _DualFusionRules,
sectors: tuple[tuple[Sector, int], ...],
)
Bases: _HashMemo
Immutable graded space: sectors is sorted by sector, all m >= 1.
Use new to build one from a mapping; the raw constructor takes an already-canonical tuple and does not validate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider
|
provider
|
The symmetry provider whose sectors label the grading. |
required |
sectors
|
tuple of (Sector, int)
|
Already-canonical |
required |
Examples:
>>> from tenet import GradedSpace
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(1): 1, U1Sector(0): 2})
>>> tuple(V)
(U1Sector(charge=0), U1Sector(charge=1))
>>> V.reduced_dim, V.dim
(3, 3)
>>> V.degeneracy(U1Sector(0))
2
reduced_dim
property
¶
Σ m_a: what reduced ndarray blocks are made of. Any provider.
Returns:
| Type | Description |
|---|---|
int
|
The total degeneracy dimension. |
dim
property
¶
Σ m_a d_a: the full dense dimension.
Returns:
| Type | Description |
|---|---|
int
|
The dense carrier-space dimension. |
Raises:
| Type | Description |
|---|---|
CapabilityError
|
If the provider lacks |
new
classmethod
¶
new(
provider: _DualFusionRules,
sectors: Mapping[Sector, int]
| Iterable[tuple[Sector, int]],
) -> GradedSpace
Normalizing constructor: sorts, rejects duplicates and m <= 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider
|
provider
|
The symmetry provider the sectors must belong to. |
required |
sectors
|
Mapping[Sector, int] or iterable of (Sector, int)
|
Sector → degeneracy, in any order. |
required |
Returns:
| Type | Description |
|---|---|
GradedSpace
|
The canonical space: pairs sorted by sector. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If a sector is not of the provider's own sector type. |
ValueError
|
If a degeneracy is |
Notes
All sectors must share one type, and that type must be the provider's
own sector type (taken as type(provider.unit)) — the provider
protocol exposes no sector-type attribute, so unit is the proxy.
direct_sum ¶
direct_sum(other: GradedSpace) -> GradedSpace
self ⊕ other at the label level: union of sectors, degeneracies added.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
GradedSpace
|
The other summand; must be over the same provider. |
required |
Returns:
| Type | Description |
|---|---|
GradedSpace
|
The label-level direct sum, canonically sorted. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Notes
This says nothing about dual — the flag lives on the Leg
(invariant 2), not on the space, so whether two legs' conventions are
summable is checked where the legs are, by the tensor-level
tenet.direct_sum; tenet.flip_dual is the numerical route for
normalising a dual convention beforehand. TensorKit's ⊕ must compare
isdual here precisely because its flag lives on the space.
degeneracy ¶
degeneracy(a: Sector) -> int
m_a, or 0 if a is absent (so filtering reads as a predicate).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
Sector
|
The sector to look up. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The degeneracy of |
sector_offset ¶
sector_offset(a: Sector) -> int
Start of a's slab in the dense layout.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
Sector
|
A sector of this space. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The offset of |
Raises:
| Type | Description |
|---|---|
CapabilityError
|
If the provider lacks |
KeyError
|
If |
Notes
Sectors are laid out in canonical order, each contributing a contiguous
slab of m_a * d_a; the within-slab index is alpha * d_a + m.
ProductSpace
dataclass
¶
ProductSpace(legs: tuple[Leg, ...])
An ordered tuple of legs, viewed as one factor of Hom(domain, codomain).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
legs
|
tuple of Leg
|
The ordered legs making up this factor. |
required |
Examples:
>>> from tenet import IN, OUT, GradedSpace, Leg, ProductSpace
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> out = ProductSpace((Leg(V, OUT),))
>>> out.reduced_dim
3
>>> out.matches(ProductSpace((Leg(V, IN),))) is None # side is not compared
True
>>> out.matches(ProductSpace((Leg(V, IN, dual=True),))) # dual is
0
Notes
A TensorMap-level value type: nobody constructs one
to make an ordinary five-axis tensor, and SymmetricTensor.codomain keeps
returning plain legs. It appears on TensorMapView.
Frozen, hashable, array-free; equal iff its ordered legs are equal (name
included, since Leg compares it). Composition, however, ignores name —
see matches.
provider
property
¶
The legs' shared provider.
Returns:
| Type | Description |
|---|---|
provider
|
The first leg's provider. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the |
dim
property
¶
Π leg.space.dim — the full dense dimension.
Returns:
| Type | Description |
|---|---|
int
|
The product of the legs' dense dimensions. |
Raises:
| Type | Description |
|---|---|
CapabilityError
|
If the provider lacks |
reduced_dim
property
¶
Π leg.space.reduced_dim. Any provider.
Returns:
| Type | Description |
|---|---|
int
|
The product of the legs' degeneracy dimensions. |
matches ¶
matches(other: ProductSpace) -> int | None
None if composable against other, else the first offending position.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
ProductSpace
|
The factor to compare against, leg by leg. |
required |
Returns:
| Type | Description |
|---|---|
int or None
|
|
Notes
Composability is (space, dual, order), exactly (invariant 2): the
effective categorical objects V or V* must agree, in order. side
is not compared (OUT meets IN by construction) and neither is name,
which is user bookkeeping. Dimensions are never compared on their own — a
charge-reversed U(1) partner has the same dimension and the wrong space.
Differing lengths report the first position past the shorter tuple; callers that can say something better about leg counts should check them first.
FusionBlockKey
dataclass
¶
FusionBlockKey(
output_tree: FusionTree, input_tree: FusionTree
)
Bases: _HashMemo
An output/input fusion-tree pair with a shared coupled sector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
output_tree
|
FusionTree
|
The tree over the OUT legs. |
required |
input_tree
|
FusionTree
|
The tree over the IN legs; must couple to the same sector. |
required |
Notes
Both trees list their uncoupled sectors in public axis order restricted
to their side, already dual-resolved (Leg.fused_sector). Field order is
load-bearing: it defines the canonical sort used by block_order.
TensorStructure
dataclass
¶
TensorStructure(legs: tuple[Leg, ...])
Bases: _HashMemo
Ordered legs plus everything derivable from them. Immutable and hashable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
legs
|
tuple of Leg
|
The tensor's legs, in public axis order. At least one. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from tenet import IN, OUT, GradedSpace, Leg, TensorStructure
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> s = TensorStructure((Leg(V, OUT), Leg(V, IN)))
>>> s.ndim, s.num_blocks
(2, 2)
>>> s.out_axes, s.in_axes
((0,), (1,))
>>> s.block_shape(s.block_order[0])
(2, 2)
provider
property
¶
The shared provider. validate() is what checks the legs agree.
Returns:
| Type | Description |
|---|---|
provider
|
The first leg's provider. |
out_axes
property
¶
Public axis indices with side is OUT, ascending.
Returns:
| Type | Description |
|---|---|
tuple of int
|
The OUT axes. |
in_axes
property
¶
Public axis indices with side is IN, ascending.
Returns:
| Type | Description |
|---|---|
tuple of int
|
The IN axes. |
block_order
property
¶
block_order: tuple[FusionBlockKey, ...]
Every structurally allowed key, sorted. The storage contract.
Returns:
| Type | Description |
|---|---|
tuple of FusionBlockKey
|
The canonical block order: |
num_blocks
property
¶
Number of structurally allowed blocks.
Returns:
| Type | Description |
|---|---|
int
|
|
Notes
Counted off the cross product, so asking how many blocks there are does not build them: the count is the reason the layout never has to.
block_shapes
property
¶
Every block's shape, aligned with block_order.
The whole-table companion to
block_shape, for a caller walking
block_order rather than asking about one key. Same information; the
difference is cost, and it is not small.
block_shape(key) enters two structure-keyed caches -- one to turn the key
into an index, one for the table -- and each entry pays a hash of this
TensorStructure, which reaches through its legs to their spaces and sectors.
Called once per block in a loop, a tensor with a thousand blocks pays two
thousand deep hashes to read a tuple that was already built. This property pays
one. Hoisting it out of SymmetricTensor.__post_init__ alone took 17% off a
cold SU(2) plan workload (#307).
Returns:
| Type | Description |
|---|---|
tuple of tuple of int
|
One shape per key of |
Examples:
index_of ¶
index_of(key: FusionBlockKey) -> int
Position of key in block_order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
FusionBlockKey
|
A key of this structure. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The index of |
Raises:
| Type | Description |
|---|---|
KeyError
|
If |
axis_sectors ¶
axis_sectors(key: FusionBlockKey) -> tuple[Sector, ...]
One space sector per public axis, de-dualized via Leg.space_sector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
FusionBlockKey
|
A key of this structure. |
required |
Returns:
| Type | Description |
|---|---|
tuple of Sector
|
One space sector per public axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If |
Notes
A lookup into the per-structure table built once by
_axis_sectors_table; foreign keys raise KeyError as before.
block_shape ¶
block_shape(key: FusionBlockKey) -> tuple[int, ...]
Degeneracies in public axis order (invariant 7).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
FusionBlockKey
|
A key of this structure. |
required |
Returns:
| Type | Description |
|---|---|
tuple of int
|
The block's shape: one degeneracy per public axis. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If |
validate ¶
validate(key: FusionBlockKey | None = None) -> None
Check the legs, and either every key in block_order or just key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
FusionBlockKey or None
|
The single key to check; |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the legs disagree on the provider, a tree's rank does not match
its side's leg count, a tree fails |
Notes
Explicit and total (invariant 11): one provider for all legs; each tree's
rank matches its side's leg count and passes FusionTree.validate; every
uncoupled label maps back into the corresponding leg's space; the two
coupled sectors agree.
StructureChangingError ¶
Bases: TypeError
Raised when an operation whose output structure depends on block values is asked to run inside a traced (jit/grad/vmap) region.
Invariants 9 and 10: the library
never hides the distinction between a shape-static operation and one that
decides its own output structure from the numbers. Lives here next to
CapabilityError, subclasses TypeError
for the same reason it does, and is exported from tenet.
SymmetricTensor
dataclass
¶
SymmetricTensor(
structure: TensorStructure, blocks: Sequence[Array]
)
A symmetric tensor: categorical structure plus one dense matrix per coupled sector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure
|
TensorStructure
|
The static, hashable half: legs and everything derived from them. |
required |
blocks
|
tuple of array
|
One reduced block per key, in |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the number of blocks does not match |
Examples:
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> t.ndim, len(t.blocks)
(2, 2)
>>> t.shape, t.reduced_shape
((3, 3), (3, 3))
>>> t.backend
'numpy'
data
property
¶
One dense matrix per coupled sector, in map_layout(structure).sectors order.
Returns:
| Type | Description |
|---|---|
tuple of array
|
The tensor's coupled-sector matrices -- its storage, and its pytree leaves. |
Notes
Gathered on demand where gathering is not free. On a mutable backend the constructor gathers immediately: it is one strided copy per block and it hands back every block of the result as a view into the matrix it just wrote, so the two forms cost one pass between them and a write through either reaches the other. On an immutable backend there is no memory to alias and no destination to write through, so a gather is a fresh array built by concatenation -- and under a JAX trace, one graph node per block with a backward pass of its own. There it is deferred until something actually asks for a matrix, which the pytree does at every traced boundary and a contraction does at every lowering, and which a transpose or an elementwise map never does at all.
Either way the two forms hold the same values and the tensor is the same tensor; what the backend decides is only which of them is built first.
blocks
property
¶
One reduced block per key, in structure.block_order. Views into data.
Returns:
| Type | Description |
|---|---|
tuple of array
|
|
Notes
The storage contract, unchanged in meaning and changed in mechanism: each block
is a two-dimensional slice of its coupled-sector matrix, reshaped into the
block's axes and permuted back into public axis order. Nothing is copied, and
writing into a block writes into the tensor. The cut is memoized, so a caller
that reads blocks in a loop pays for it once.
A block is a live view and is usually not C-contiguous. Two consequences for
a caller that wants a block of its own. numpy.array(block, copy=True) and
numpy.ascontiguousarray(block) are both the wrong tool: the first defaults to
order "K" and keeps the block's layout, so .reshape(-1) on the result
hands back another copy and a write into it is lost; the second does not copy at
all when the block already happens to be contiguous, so a write into it reaches
this tensor. numpy.array(block, order="C", copy=True) is the one that is both
a copy and flat.
legs
property
¶
legs: tuple[Leg, ...]
The structure's legs, in public axis order.
Returns:
| Type | Description |
|---|---|
tuple of Leg
|
|
provider
property
¶
codomain
property
¶
codomain: tuple[Leg, ...]
The OUT legs, in public axis order.
Returns:
| Type | Description |
|---|---|
tuple of Leg
|
The OUT legs. ProductSpace is the fused view of the same legs, when one is wanted. |
domain
property
¶
domain: tuple[Leg, ...]
IN legs in public axis order.
Returns:
| Type | Description |
|---|---|
tuple of Leg
|
The IN legs. |
shape
property
¶
Full physical dimension per public axis: Σ_a m_a d_a.
Equal to self.to_dense().shape.
Returns:
| Type | Description |
|---|---|
tuple of int
|
One dense dimension per public axis. |
Raises:
| Type | Description |
|---|---|
CapabilityError
|
If the provider lacks |
reduced_shape
property
¶
Degeneracy dimension per public axis: Σ_a m_a. Any provider.
The storage-facing shape: what the reduced blocks are made of.
Returns:
| Type | Description |
|---|---|
tuple of int
|
One degeneracy dimension per public axis. |
dtype
property
¶
The single dtype shared by all blocks (__post_init__ validates it).
Returns:
| Type | Description |
|---|---|
dtype
|
The first block's dtype. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the tensor has no blocks — the dtype is then undefined. |
backend
property
¶
"numpy" / "jax" / "torch", inferred from the first block.
Returns:
| Type | Description |
|---|---|
str
|
The autoray backend name. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the tensor has no blocks — the backend is then undefined. |
Notes
One tensor uses one backend; construction does not re-check every block,
since to_backend is the only sanctioned way to move them.
device
property
¶
The first block's own .device (None if it has none).
Returns:
| Type | Description |
|---|---|
device or None
|
Whatever the backend exposes. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the tensor has no blocks — the device is then undefined. |
Notes
A plain getattr: autoray exposes no portable device accessor, and
NumPy >= 2 arrays already carry .device == 'cpu'.
from_data
classmethod
¶
from_data(
structure: TensorStructure, data: Sequence[Array]
) -> SymmetricTensor
Wrap coupled-sector matrices as a tensor, unchecked and zero-copy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure
|
TensorStructure
|
The structure the matrices belong to. |
required |
data
|
sequence of array
|
One matrix per coupled sector, in |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The tensor holding exactly those arrays. |
Notes
The storage constructor, for a caller that already has the matrices -- from_matrices, which checks them, and the plan appliers, which built them from the structure they are being handed. The public constructor stays the trust boundary and keeps validating; this one takes the arrays as given, since re-deriving what the layout just produced would be checking our own arithmetic.
from_blocks
classmethod
¶
from_blocks(
legs: Sequence[Leg],
blocks: Mapping[FusionBlockKey, Array],
) -> SymmetricTensor
Build from public legs by naming fusion-block keys; absent keys are zero.
Only the blocks the caller has an opinion about are named; the keys come from
TensorStructure(legs).block_order — no throwaway tensor is needed to
discover the layout.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
legs
|
sequence of Leg
|
The legs, in public axis order. |
required |
blocks
|
mapping of FusionBlockKey to array
|
The blocks to set. Every key must belong to
|
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The assembled tensor; the constructor validates every block's shape. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If a key is foreign to the structure. The message names the legal keys (the first few, plus the count) and where to read the rest. |
ValueError
|
If |
Notes
An absent key is zero, not an error. The convenience is not the argument — strictness would be worth the inconvenience if it caught mistakes, and here it does not: a mistyped key is an unknown key, not a missing one, so it raises either way. Demanding every key would only penalise the case the constructor exists for, which is naming one block of many.
docs/tutorials/symmetric-tensors.md is this constructor at length:
reading a key, the U(1) and SU(3) operators built with it, and what the
foreign-key refusal catches.
Examples:
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor, TensorStructure
>>> from tenet.symmetry import SU2, SU2Sector
>>> V = GradedSpace.new(SU2, {SU2Sector(1): 1}) # one spin-1/2 multiplet
>>> legs = (Leg(V, OUT), Leg(V, OUT, dual=True)) # the evaluation cup
>>> structure = TensorStructure(legs)
>>> key, = structure.block_order # exactly one fusion channel
>>> t = SymmetricTensor.from_blocks(legs, {key: np.ones(structure.block_shape(key))})
>>> t.blocks
(array([[1.]]),)
zeros
classmethod
¶
zeros(
legs: Sequence[Leg], dtype: Any = float64
) -> SymmetricTensor
All-zero blocks over legs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
legs
|
sequence of Leg
|
The legs, in public axis order. |
required |
dtype
|
dtype
|
The blocks' dtype. Default |
float64
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The zero tensor, NumPy blocks. |
random
classmethod
¶
random(
legs: Sequence[Leg],
*,
seed: int | None = None,
dtype: Any = float64,
) -> SymmetricTensor
Standard-normal blocks from np.random.default_rng(seed), reproducible.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
legs
|
sequence of Leg
|
The legs, in public axis order. |
required |
seed
|
int or None
|
The RNG seed; |
None
|
dtype
|
dtype
|
The blocks' dtype. Default |
float64
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The random tensor, NumPy blocks. |
block ¶
block(key: FusionBlockKey) -> Array
The stored block for key — the array itself, not a copy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
FusionBlockKey
|
A key of this tensor's structure. |
required |
Returns:
| Type | Description |
|---|---|
array
|
The block, in public axis order. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If |
items ¶
items() -> Iterator[tuple[FusionBlockKey, Array]]
Iterate (key, block) pairs in block_order.
Yields:
| Type | Description |
|---|---|
tuple of (FusionBlockKey, array)
|
Each key with its stored block. |
with_blocks ¶
with_blocks(
blocks: Mapping[FusionBlockKey, Array],
) -> SymmetricTensor
Same structure, the named blocks replaced and the rest carried over.
The immutable spelling of assigning to one block: self is untouched
and a new tensor is returned. The keys are this tensor's own, from
self.structure.block_order or items.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
blocks
|
mapping of FusionBlockKey to array
|
The blocks to replace. Keys absent from the mapping keep the block they already have; an empty mapping is a no-op copy. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
A new tensor over the same structure. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If a key is foreign to this tensor's structure — the same message from_blocks raises. |
ValueError
|
From the ordinary constructor, if a replacement has the wrong shape (naming the expected shape and the key) or a dtype the other blocks do not share. |
Examples:
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> t = SymmetricTensor.zeros((Leg(V, OUT), Leg(V, IN)))
>>> key = t.structure.block_order[0]
>>> u = t.with_blocks({key: np.ones(t.structure.block_shape(key))})
>>> u.block(key)
array([[1., 1.],
[1., 1.]])
>>> t.block(key).any() # the original is untouched
np.False_
astype ¶
astype(dtype: Any) -> SymmetricTensor
Same structure and backend, every block cast to dtype.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dtype
|
dtype
|
The target dtype. Any spelling NumPy recognizes — |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
A new tensor whose blocks all carry |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the tensor has no blocks — there is nothing to cast, and the result's dtype would be undefined, as for dtype. |
Notes
Blockwise ar.do("astype", b, dtype), so the backend's own casting
rules apply: JAX truncates a request for a dtype its jax_enable_x64
setting does not admit, exactly as it does on any other array.
Examples:
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> c = t.astype(np.complex128)
>>> c.dtype
dtype('complex128')
>>> c.structure == t.structure and c.legs == t.legs
True
>>> bool(np.allclose(np.asarray(c.blocks[0]).real, t.blocks[0]))
True
to_backend ¶
to_backend(
backend: str, dtype: Any = None
) -> SymmetricTensor
Same structure, blocks converted with ar.do("array", b, like=backend).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
backend
|
str
|
The target autoray backend, e.g. |
required |
dtype
|
dtype or None
|
Cast the blocks to this dtype after the move, via
astype. |
None
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
A new tensor on |
Notes
A move to the backend the tensor already lives on is the identity on the
blocks — no copy, and, on torch, no torch.tensor(x), which would
detach them from the autograd graph. An explicit dtype still runs.
The target backend's own dtype policy applies to the move (JAX demotes
float64 to float32 unless jax_enable_x64 is set). dtype runs
after that move rather than as part of it, so it overrides a backend's
choice — e.g. a real tensor moved to JAX and asked for
np.complex128 arrives complex — but it cannot override a backend's
refusal: JAX truncates np.float64 to float32 in astype too
when jax_enable_x64 is unset, because in that mode the dtype does
not exist for it to produce. Enabling jax_enable_x64 is the only fix
for that one, and it is process-global.
Examples:
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> t.to_backend("numpy", dtype=np.complex128).dtype
dtype('complex128')
get_params ¶
The coupled-sector matrices — a pytree of backend arrays. See data.
Returns:
| Type | Description |
|---|---|
tuple of array
|
|
Notes
The identity, deliberately: data is an ordered tuple, so no dict ordering
and no key hashing ever enters the dynamic data.
These are the same leaves tenet.pytree hands JAX, and for the same reason:
they are the arrays the tensor is made of. A block is a view cut out of one of
them, so a torch leaf handed back as a block would be a non-leaf with no .grad
of its own, and a JAX leaf handed back as a block would put the cut in the graph.
The optimizer differentiates the storage; blocks is how the result is read.
set_params ¶
set_params(params: Sequence[Array]) -> SymmetricTensor
Same structure, new numerical data. A new tensor; self is untouched.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
params
|
sequence of array
|
The new coupled-sector matrices, in the order get_params returns them. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
A new tensor over the same structure. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the count, a shape or the dtypes do not match the structure's layout -- from_matrices's refusals, which are the trust boundary for arrays given as matrices. |
copy ¶
copy() -> SymmetricTensor
A new instance sharing the same structure and stored arrays.
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The shallow copy. |
as_map ¶
as_map() -> TensorMapView
View this tensor as a morphism. Zero-copy.
Returns:
| Type | Description |
|---|---|
TensorMapView
|
The semantic view; see TensorMapView. |
conj ¶
conj() -> SymmetricTensor
Conjugate the blocks; legs unchanged. See tenet.conj.
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The blockwise complex conjugate. |
adjoint ¶
adjoint() -> SymmetricTensor
T†: every leg's side flips, blocks are conjugated and key-swapped.
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The adjoint. Not |
norm ¶
qdim-weighted Frobenius norm (a backend scalar). See tenet.norm.
Returns:
| Type | Description |
|---|---|
scalar
|
The norm, on this tensor's backend. |
apply_blocks ¶
apply_blocks(fn: Any) -> SymmetricTensor
fn on each reduced block. See tenet.apply_blocks for the caveat.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fn
|
callable
|
Applied to each block; must preserve shape and be backend-generic. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The mapped tensor, same structure. |
block_sqrt ¶
block_sqrt() -> SymmetricTensor
Blockwise sqrt — not sqrt(self.to_dense()). See tenet.block_sqrt.
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The blockwise square root. |
block_power ¶
block_power(p: Any) -> SymmetricTensor
Blockwise self ** p for a scalar p. See tenet.block_power.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
p
|
scalar
|
The exponent. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The blockwise power. |
save ¶
Write to path as a single .npz. See tenet.save.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
path - like
|
Where to write. |
required |
compress
|
bool
|
Compress the archive. Default |
False
|
load
classmethod
¶
load(path: Any) -> SymmetricTensor
Read a file written by save; NumPy blocks.
See tenet.load.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
path - like
|
The |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The loaded tensor, NumPy blocks. |
transpose ¶
transpose(*axes: Any) -> SymmetricTensor
T.transpose(2, 0, 1), T.transpose((2, 0, 1)) or T.transpose().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*axes
|
int or sequence of int
|
The new axis order; empty (or |
()
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The permuted tensor; see tenet.transpose — no leg changes
|
repartition ¶
repartition(
outputs: Sequence[int], inputs: Sequence[int]
) -> SymmetricTensor
T.repartition(outputs=(0, 1), inputs=(2,)). See tenet.repartition.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
outputs
|
sequence of int
|
The public axes to place on the OUT side. |
required |
inputs
|
sequence of int
|
The public axes to place on the IN side. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The repartitioned tensor. |
Notes
Every leg that crosses sides is bent: its side and its dual both
flip. Requires BendingCoefficients unless no leg crosses.
fuse ¶
fuse(*axes: Any) -> SymmetricTensor
T.fuse(0, 1) or T.fuse((0, 1)). See tenet.fuse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*axes
|
int or sequence of int
|
The adjacent axes to fuse into one. |
()
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The fused tensor. |
unfuse ¶
unfuse(axis: int, legs: Sequence[Leg]) -> SymmetricTensor
Split axis into legs. See tenet.unfuse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
axis
|
int
|
The fused axis to split. |
required |
legs
|
sequence of Leg
|
The constituent legs the axis splits into. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The unfused tensor. |
embed ¶
embed(legs: Sequence[Leg]) -> SymmetricTensor
Zero-pad into larger, containing legs. See tenet.embed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
legs
|
sequence of Leg
|
The target legs, one per axis, each containing the current leg. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The embedded tensor. |
to_symmetry ¶
to_symmetry(
target: _DualFusionRules, *, atol: float | None = None
) -> SymmetricTensor
Restrict to a smaller symmetry, e.g. SU(2) -> U(1). See tenet.to_symmetry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
provider
|
The smaller symmetry's provider. |
required |
atol
|
float or None
|
Symmetry-check tolerance; |
None
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The tensor over |
restrict ¶
restrict(
legs: Sequence[Leg], *, atol: float | None = None
) -> SymmetricTensor
Slice down to smaller, contained legs. See tenet.restrict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
legs
|
sequence of Leg
|
The target legs, one per axis, each contained in the current leg. |
required |
atol
|
float or None
|
Tolerance for the discarded weight check; |
None
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The restricted tensor. |
direct_sum ¶
direct_sum(
other: SymmetricTensor, axes: int | Sequence[int]
) -> SymmetricTensor
self ⊕ other along axes. See tenet.direct_sum.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
SymmetricTensor
|
The other summand. |
required |
axes
|
int or sequence of int
|
The axes along which the spaces are summed. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The direct sum. |
to_dense ¶
T = Σ_τ A^(τ) ⊗ C^(τ) expanded into a dense array of self's backend.
Returns:
| Type | Description |
|---|---|
array
|
The dense carrier-basis array, shape shape. |
Raises:
| Type | Description |
|---|---|
CapabilityError
|
If the provider lacks |
Notes
Explicit by design (invariant 9). See
tenet.ops.dense.to_dense — traceable and differentiable.
from_dense
classmethod
¶
from_dense(
dense: Array,
legs: Sequence[Leg],
*,
atol: float | None = None,
) -> SymmetricTensor
Project a dense carrier-basis array onto the symmetric subspace of legs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dense
|
array
|
The dense array, in the layout convention of the module docstring. |
required |
legs
|
sequence of Leg
|
The legs describing each dense axis. |
required |
atol
|
float or None
|
Tolerance for the symmetry check; |
None
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The projected tensor. |
Notes
The inverse of to_dense; non-symmetric
input is refused rather than silently projected. See
tenet.ops.dense.from_dense.
coupled_sectors ¶
coupled_sectors(
provider: FusionRules, uncoupled: Sequence[Sector]
) -> tuple[Sector, ...]
Every sector reachable from uncoupled, canonically sorted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider
|
FusionRules
|
The provider supplying the fusion rules. |
required |
uncoupled
|
sequence of Sector
|
The external sectors, already dual-resolved. |
required |
Returns:
| Type | Description |
|---|---|
tuple of Sector
|
The reachable coupled sectors, sorted. |
Examples:
fusion_trees ¶
fusion_trees(
provider: FusionRules,
uncoupled: Sequence[Sector],
coupled: Sector,
) -> tuple[FusionTree, ...]
All valid left-associated trees for uncoupled -> coupled, sorted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider
|
FusionRules
|
The provider supplying fusion rules and |
required |
uncoupled
|
sequence of Sector
|
The external sectors, already dual-resolved. |
required |
coupled
|
Sector
|
The total sector the trees must couple to. |
required |
Returns:
| Type | Description |
|---|---|
tuple of FusionTree
|
Every valid tree, in canonical sorted order; empty if |
Examples:
as_map ¶
as_map(t: SymmetricTensor) -> TensorMapView
View t as a morphism. Zero-copy: no block is read, moved or allocated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor to view. |
required |
Returns:
| Type | Description |
|---|---|
TensorMapView
|
The semantic view. |
from_matrices ¶
from_matrices(
structure: TensorStructure, mats: Mapping[Sector, Any]
) -> SymmetricTensor
Inverse of to_matrices against the same structure. Exact round-trip.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure
|
TensorStructure
|
The structure the matrices belong to; the matrices carry no categorical information, so the structure has to be given. |
required |
mats
|
Mapping[Sector, array]
|
Exactly the coupled sectors of |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The tensor whose lowering is |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a sector is missing, unknown, its matrix has the wrong shape (invariant 11), or the matrices do not share one dtype. |
Examples:
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet import from_matrices, to_matrices
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> from_matrices(t.structure, to_matrices(t)) == t
True
Notes
Zero-copy, and now trivially so: the matrices are the storage, so this checks them against the layout and hands them to the tensor. Nothing is allocated but the tuple, and the blocks are cut out of them only if someone asks for blocks.
The refusals above are the whole check, and they are deliberately spelled over the
matrices rather than over the blocks that come out of them. The matrices are the
untrusted input; the blocks are views cut to the shapes structure dictates, so
re-reading those shapes back off them would be one pass per block -- 613,468 of them
on a rank-8 SU(2) intermediate, a fifth of the contraction that builds it -- to
confirm the reshape that has not even happened yet. One touch per coupled sector says
the same thing (#328).
map_layout ¶
map_layout(structure: TensorStructure) -> MapLayout
The lowering plan for structure. Cached: repeat calls return one object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
structure
|
TensorStructure
|
The structure to lay out. |
required |
Returns:
| Type | Description |
|---|---|
MapLayout
|
The per-sector band and grid tables. |
Examples:
>>> from tenet import IN, OUT, GradedSpace, Leg, TensorStructure, map_layout
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> layout = map_layout(TensorStructure((Leg(V, OUT), Leg(V, IN))))
>>> layout.sectors
(U1Sector(charge=0), U1Sector(charge=1))
>>> layout.shape(U1Sector(0))
(2, 2)
to_matrices ¶
to_matrices(t: SymmetricTensor) -> dict[Sector, Any]
{c: B_c}, one dense backend matrix per coupled sector. t is untouched.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor to lower. |
required |
Returns:
| Type | Description |
|---|---|
dict of Sector to array
|
One matrix per coupled sector, laid out per map_layout. |
Examples:
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor, to_matrices
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> mats = to_matrices(t)
>>> sorted(mats)
[U1Sector(charge=0), U1Sector(charge=1)]
>>> mats[U1Sector(0)].shape
(2, 2)
Notes
The tensor's own storage, keyed by sector rather than positionally: the matrices are
the arrays t holds, not copies of them, so writing into one writes into t.
add ¶
add(
a: SymmetricTensor, b: SymmetricTensor
) -> SymmetricTensor
a + b. Requires identical structures; near-misses are errors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
SymmetricTensor
|
The left operand. |
required |
b
|
SymmetricTensor
|
The right operand. Its structure must equal |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The blockwise sum, on the operands' shared structure. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the structures differ — different providers, different |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> b = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=1)
>>> bool(tenet.allclose(tenet.add(a, b), a + b))
True
adjoint ¶
adjoint(t: SymmetricTensor) -> SymmetricTensor
T†: the Euclidean-adjoint morphism in Hom(codomain, domain).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The morphism to dagger. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> d = tenet.adjoint(a)
>>> d.legs[0].side, bool(tenet.allclose(tenet.adjoint(d), a))
(<Side.IN: 'in'>, True)
Notes
Every leg keeps its space, dual and name and flips its side;
the public axis order is unchanged; the block for key (ot, it) becomes the
conjugate of the block for key (it, ot) — no axis permutation is needed,
because reduced axes travel with their own legs (invariant 7).
Deliberately no requires(...): this needs conj on the backend and the
identity of the trees, nothing a provider could fail to supply. The dagger
structure this leans on is named by
DaggerData, today a marker every provider
satisfies.
allclose ¶
allclose(
a: SymmetricTensor,
b: SymmetricTensor,
*,
rtol: float = 1e-05,
atol: float = 1e-08,
) -> bool
Tolerant comparison. Different structures give False, never an error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
SymmetricTensor
|
The left operand. |
required |
b
|
SymmetricTensor
|
The right operand; a structure mismatch is |
required |
rtol
|
float
|
Relative tolerance, forwarded to the backend's |
1e-05
|
atol
|
float
|
Absolute tolerance, forwarded likewise. Default |
1e-08
|
Returns:
| Type | Description |
|---|---|
bool
|
|
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> b = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=1)
>>> tenet.allclose(a, a), tenet.allclose(a, b)
(True, False)
apply_blocks ¶
apply_blocks(
t: SymmetricTensor, fn: Callable[[Array], Array]
) -> SymmetricTensor
fn applied to each reduced block. Coefficient space, not dense space.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor whose reduced blocks |
required |
fn
|
callable
|
An elementwise and shape-preserving function of one backend array.
It is not checked (a shape change is caught by
|
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> tenet.allclose(tenet.apply_blocks(a, lambda blk: 2 * blk), a + a)
True
Notes
Structure is untouched, so this is linear-algebra-free, backend-generic,
traceable and differentiable: it is exactly t.set_params(map(fn,
t.get_params())), which is quimb's Tensor.apply_to_arrays over the same
parameter protocol.
fn not being required to satisfy fn(0) == 0 is a structural fact: the
blocks hold only allowed fusion channels, so any fn returns a valid
symmetric tensor and every symmetry-forbidden dense entry stays exactly zero.
What it does not do is commute with dense expansion. T = Σ_τ A^(τ) ⊗
C^(τ), so for a non-Abelian provider apply_blocks(t, f).to_dense() !=
f(t.to_dense()) — off by 1.673 on a dense scale of 3.82 for a rank-3
SU(2) tensor with f = sqrt. For every shipped Abelian provider
(all-ones CG, d_a == 1) they agree exactly. If you want dense-elementwise
semantics, densify explicitly.
bend ¶
bend(t: SymmetricTensor, axis: int) -> SymmetricTensor
Bend axis to the other side: side flipped, dual flipped, moved last.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor whose leg is bent. |
required |
axis
|
int
|
The public axis to bend. It must currently be the last leg of its own side (it need not be the last public axis). |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The bent tensor: the moved leg takes the largest public position on
its new side, with |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
CapabilityError
|
If the provider does not implement |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> b = tenet.bend(a, 0) # the OUT leg becomes a dual IN leg, moved last
>>> b.legs[-1].side, b.legs[-1].dual
(<Side.IN: 'in'>, True)
block_power ¶
block_power(t: SymmetricTensor, p: Any) -> SymmetricTensor
Blockwise t ** p. p is a scalar exponent, never a tensor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor whose blocks are raised to |
required |
p
|
scalar
|
The exponent — a Python number or 0-d backend array, never a tensor. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
Every block raised to |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> tenet.allclose(tenet.block_power(a, 2), tenet.apply_blocks(a, lambda b: b * b))
True
Notes
Same coefficient-space semantics and the same backend-owned branch cuts and
nans as block_sqrt; p = -0.5 is the inverse-√S of
canonical-form and gauge-fixing loops. It runs over data for the same reason.
block_sqrt ¶
block_sqrt(t: SymmetricTensor) -> SymmetricTensor
Blockwise sqrt. The svd splitter: u @ sqrt(s), sqrt(s) @ vh.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor whose blocks are square-rooted, entry by entry. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The elementwise square root of every block, on |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> q = tenet.apply_blocks(a, abs) # non-negative blocks
>>> tenet.allclose(tenet.block_sqrt(tenet.block_power(q, 2)), q)
True
Notes
Blockwise, i.e. on the coefficients — see apply_blocks for the blockwise/dense caveat, which for SU(2) is 44% of the array's own scale and completely silent.
For the S returned by tenet.linalg.svd this is the matrix square
root, because svd builds S's blocks with ar.do("diag", ...) and a
diagonal matrix's elementwise and matrix square roots coincide. That is a fact
about S, not about sqrt: for a non-diagonal t, sqrt(t) @ sqrt(t)
!= t.
Negative or complex entries are the backend's business: sqrt(-1.0) is
nan under NumPy, JAX and torch alike, and nothing here clips, guards or
regularizes.
Runs over the coupled-sector matrices, not over the blocks cut out of them: a
coupled sector's matrix is exactly its blocks laid side by side, so an elementwise
map is the same numbers either way, and this one's fn is the library's own and
reads no shape. apply_blocks keeps the block route because there it is not.
braid ¶
braid(
t: SymmetricTensor,
axes: Sequence[int],
levels: Sequence[int],
) -> SymmetricTensor
The same abstract tensor after a braid: axes reordered, levels crossed.
levels[i] is the position of axis i's line in the diagram's incoming
planar order; axes is the outgoing order, as in tenet.transpose. Two
lines cross exactly when those two orders disagree about them, which makes
braid the two operations a planar embedding needs and transpose alone
cannot spell:
- monotone
levels— the incoming order is the leg order, so the crossings are the inversions ofaxesand the result is tenet.transpose(t, axes), through the very same plan object. axesthe identity, two levels inverted — the lines cross and come back to the leg order they started in: a crossing with no net permutation, the swap gate. Its coefficient is the grading sign(-1)^(p_i p_j), which is1for every bosonic provider (the crossing is then the identity morphism) and YASTN'sswap_gatefor fermion parity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor whose lines are braided. |
required |
axes
|
sequence of int
|
|
required |
levels
|
sequence of int
|
One height per OLD axis. Ties never cross; only the order matters, so
|
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The braided tensor; |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
CapabilityError
|
If the braid needs coefficients the provider does not state — a within-side
reorder without
PermutationCoefficients, a level
crossing without TwistData, or either on a
provider whose braiding is chiral ( |
Notes
Differentiability and tracing: exactly tenet.transpose's status — the plan is
frozen, array-free metadata and the blocks move through one transpose and one
real scalar each, so braid is shape-static and traces under jax.jit/
jax.grad. No custom VJP is registered for either, and none is needed.
This is TensorKit's braid(t, p, levels) widened by one step. There levels
only choose each crossing's sense, so under a symmetric braiding they drop out
and braid == permute; the crossings are always the inversions of p. Here
they also decide which pairs cross, which is what a planar embedding needs and
what a permutation cannot say. Sense stays irrelevant, as it must be for a
symmetric braiding.
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> tenet.braid(t, (1, 0), (0, 1)) == tenet.transpose(t, (1, 0)) # monotone: a transpose
True
>>> tenet.braid(t, (0, 1), (1, 0)).legs == t.legs # a crossing moves no leg
True
compose ¶
compose(
a: SymmetricTensor, b: SymmetricTensor
) -> SymmetricTensor
a ∘ b: b's codomain is consumed by a's domain. Spelled a @ b.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
SymmetricTensor
|
The outer morphism; its domain consumes |
required |
b
|
SymmetricTensor
|
The inner morphism. Its codomain must carry the same |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The composition; its public axis order is |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> bool(tenet.allclose(tenet.identity(a.codomain) @ a, a))
True
Notes
For a long chain at a fixed partition in eager NumPy, the matrix form can
be kept between steps by hand -- worth about 1.1x asymptotically, and the reason
the library does not persist the layout itself is that real tensordot chains
never hit such a cache and from_matrices is already zero-copy::
acc = to_matrices(ts[0])
for t in ts[1:]:
mb = to_matrices(t)
acc = {c: acc[c] @ mb[c] for c in acc}
out = from_matrices(TensorStructure((*ts[0].codomain, *ts[-1].domain)), acc)
conj ¶
conj(t: SymmetricTensor) -> SymmetricTensor
Complex-conjugate the reduced blocks; legs are left completely alone.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor to conjugate. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0) # real blocks
>>> bool(tenet.allclose(tenet.conj(a), a))
True
Notes
No dual flip, no side change: conjugation, duality and the categorical
adjoint are three different things (invariant 2). In particular t.conj()
is not what you contract t against to obtain ‖t‖² — that pairing
needs the adjoint, tenet.adjoint.
Blockwise conjugation equals dense-basis conjugation exactly because every provider here has real Clebsch-Gordan coefficients (all-ones for Trivial and U(1), Condon-Shortley for SU(2)).
direct_sum ¶
direct_sum(
t: SymmetricTensor,
u: SymmetricTensor,
axes: int | Sequence[int],
) -> SymmetricTensor
t ⊕ u along axes: sector-wise degeneracy sums, t leading.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The first summand; it takes the leading degeneracy slots. |
required |
u
|
SymmetricTensor
|
The second summand, in the trailing slots. Must be on |
required |
axes
|
int or sequence of int
|
The axes to sum along (negative indices allowed here). Every axis
not in |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The direct sum: on each summed axis the result's space has
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the operands' leg counts differ; if |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> b = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=1)
>>> tenet.direct_sum(a, b, axes=1).shape
(2, 4)
Notes
Placement is t in the leading degeneracy slots and u in the
trailing ones, on every summed axis — the same prefix convention embed
and svd(..., bond=) use, and the same order as TensorKit's catdomain.
With ONE summed axis every block splits into exactly two slabs, t's then
u's. With MORE than one, the "mixed" regions — leading on one summed axis
and trailing on another — are zero, because neither pad ever writes
them, and that is precisely what makes a two-sided direct sum the
block-diagonal map T ⊕ U.
Structure-static and traceable: the result structure comes from the operands'
legs and axes, which are metadata, so this composes inside jit and
grad. Linear in each operand separately.
divide ¶
divide(t: SymmetricTensor, s: Any) -> SymmetricTensor
t / s for a scalar s.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor to scale. |
required |
s
|
scalar
|
A Python number or a 0-d backend array; the same scalar rule as multiply. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> bool(tenet.allclose(tenet.divide(a, 2.0), 0.5 * a))
True
einsum ¶
einsum(
equation: str,
*operands: SymmetricTensor,
optimize: Any = "auto",
) -> SymmetricTensor
tenet.einsum("abc,cde,ef->abdf", A, B, C) — any number of operands.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
equation
|
str
|
The label equation, one comma-separated term per operand. Labels are
single ASCII letters, one per axis; a label occurs at most twice in
the whole equation (a wire has two ends). |
required |
*operands
|
SymmetricTensor
|
One or more tensors, in the equation's term order. |
()
|
optimize
|
str, path, or opt_einsum.paths.PathOptimizer
|
Consulted only with three or more operands, where it is handed to
|
'auto'
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The contraction, its public axes in the output labels' order; free legs come back exactly as they went in. |
Raises:
| Type | Description |
|---|---|
ValueError
|
The parser's refusals, each naming what to write instead: no operands; ellipsis (symmetric tensors do not broadcast); a non-ASCII or non-letter label; a term/operand count or length mismatch; a label occurring more than twice; a label repeated within one operand — a diagonal (not equivariant, invariant 11) or a single-operand trace (use trace); a repeated output label; an output label appearing in no input; or an input label missing from the output, which would sum an axis away (not equivariant, invariant 11). Also tensordot's refusals for each pairwise step, e.g. a shared label whose two legs are not contractible. |
CapabilityError
|
If a pairwise step needs a bend the provider cannot supply, as in tensordot. |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> b = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=1)
>>> c = tenet.einsum("ab,bc->ac", a, b)
>>> bool(tenet.allclose(c, tenet.tensordot(a, b, axes=((1,), (0,)))))
True
Notes
Repeated labels within one operand (a trace or a diagonal) and ellipsis are refused; see the message on each.
With one or two operands this is the pairwise lowering and optimize is
not consulted (opt_einsum is not even imported). With three or more the
pairwise order is chosen by opt_einsum.contract_path from the operands'
physical shape\ s, and optimize is handed
to it unchanged: a strategy name, an explicit path, or any
opt_einsum.paths.PathOptimizer — cotengra's optimizers are such objects
and work here without cotengra being imported. A strategy name asks for a
search, so its path is cached on (equation, shapes, name); a path and a
PathOptimizer are consulted on every call. Every step of the path is
then this same two-operand call, so the mathematics is unchanged; the path is
chosen from static structure only, and is therefore baked in at trace time
under jax.jit like every other structural decision.
Shared labels are contracted in order of first appearance in the first
operand — any order gives the same tensor, but a nondeterministic one would
fragment plan caches and make jit retrace. The final transpose is a
public permutation and therefore fully categorical (a Koszul sign for a
fermionic provider, a braid for SU(2)); it is never skipped, and
permutation_plan's case A already makes the identity permutation free.
einsum_chain ¶
einsum_chain(
steps: Sequence[
tuple[
str,
SymmetricTensor | None,
SymmetricTensor | None,
str,
]
],
) -> SymmetricTensor
A run of pair-contractions with nothing materialized between them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
steps
|
sequence of (equation, a, b, bend)
|
One entry per pair-contraction, in order. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The last step's result. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
CapabilityError
|
If a step needs a bend the provider cannot supply, as in tensordot. |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> b = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=1)
>>> c = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=2)
>>> chained = tenet.einsum_chain(
... [("ab,bc->ac", a, b, ""), ("ab,bc->ac", None, c, "")]
... )
>>> bool(tenet.allclose(chained, tenet.einsum("ab,bc->ac", tenet.einsum("ab,bc->ac", a, b), c)))
True
Notes
Step k's restore -- the repartition that puts the product back on its public legs,
with the final transpose already folded in -- and step k+1's operand lowering are
both plans of (source, target, coefficient) over blocks that are views into
step k's sector matrices. Composing them gives one plan from step k's
matrices to step k+1's: one strided pass per term, the coefficients multiplied
through, and no tensor written in between. The terms and their coefficients are the
ones the separate calls apply -- only when they are applied changes.
The steps are the caller's, not a path finder's: a chain states the intermediate leg
order and the bends at each pair, which is what the composition rule fixes and what
a single multi-operand equation would leave to opt_einsum.
flip_dual ¶
flip_dual(
t: SymmetricTensor,
axes: int | Hashable | Sequence[int | Hashable],
*,
inv: bool = False,
) -> SymmetricTensor
Toggle the dual flag of axes, keeping the tensor the same morphism.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor whose legs are flipped. |
required |
axes
|
int, leg name, or sequence of either
|
The legs to flip; |
required |
inv
|
bool
|
|
False
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The same morphism with each named leg's |
Raises:
| Type | Description |
|---|---|
ValueError
|
If an axis is out of range or repeated, if a leg name matches no leg, or if it matches more than one (use the axis index instead). |
CapabilityError
|
If the provider does not implement
FSIndicatorData and
TwistData — the scalar is
|
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> f = tenet.flip_dual(a, 0) # charge q relabelled as -q on a dual leg
>>> f.legs[0].dual, f.legs[0].space.sectors
(True, ((U1Sector(charge=-1), 1), (U1Sector(charge=0), 1)))
>>> bool(tenet.allclose(tenet.flip_dual(f, 0, inv=True), a))
True
Notes
Not numpy.flip: no axis is reversed and no element moves. Each named
leg's dual flag is toggled and its space is relabelled through
provider.dual (so a U(1) leg over charges {q} comes back over
{-q}), which is the V_a -> V_a^* isomorphism made explicit — the
operation TensorKit spells flip. The name is qualified here and not there
because Python has numpy.flip, which reverses element order along an axis
of the tensor while this toggles a flag on a leg -- a different operand and
a different operation under one name, reachable through autoray's module lookup.
YASTN, the Python API reference, qualifies the same operation the same
way (flip_signature / flip_charges); dual is this package's noun for
the flag. side and name are unchanged:
moving a leg between domain and codomain stays
repartition's job.
Two contracts, both TensorKit's: flipping the two legs of a
contractible pair leaves the contraction result unchanged, and flip_dual is
not an involution — flipping the same leg twice multiplies each tree by
chi_a * theta_a once (-1 on an SU(2) half-integer or odd
fermion-parity line), and inv=True is the exact inverse instead.
Because the relabel and the flag toggle cancel inside Leg.fused_sector,
every fusion-tree leaf -- and with it the block set, order, shapes and coupled-sector
layout -- is unchanged, so the whole operation is one scalar per block. That scalar
factorizes over the two trees, which makes it a diagonal scaling of the rows and of
the columns of each stored matrix: no block is ever cut out to be multiplied by a
number.
full_trace ¶
full_trace(t: SymmetricTensor) -> Any
Σ_c qdim(c) · tr(M_c) — the categorical trace of an endomorphism, a scalar.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
A square map: its codomain and domain must carry the same
|
required |
Returns:
| Type | Description |
|---|---|
scalar
|
The backend's own scalar — no |
Raises:
| Type | Description |
|---|---|
CapabilityError
|
If |
ValueError
|
If the map is not square space-wise ( |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> int(tenet.full_trace(tenet.identity((Leg(V, OUT),))))
2
Notes
Open diagrams are tensors; closed diagrams exit to backend scalars, explicitly
and by name. tensordot, einsum and
trace never return a
scalar — a contraction that closes a network is a ValueError, and a
SymmetricTensor still has no rank 0. Leaving the tensor world is a separate,
named call — norm, full_trace, inner — which returns the
backend's own scalar and is therefore traceable and differentiable.
The pair closed is the map view: codomain against domain, in order, the same
view eigh, expm, svd and to_matrices act through. Any rank with a
square map, so a rank-4 (V OUT, W OUT | V IN, W IN) gives np.einsum("abab->")
and not an axis-adjacent pairing; trace remains the way to close one chosen
pair and to keep a tensor.
The qdim weight is the same one norm carries, and it is what makes
full_trace(t) == np.trace(t.to_dense()) hold for a rank-2 map; dropping it is
wrong for any non-Abelian provider.
fuse ¶
fuse(
t: SymmetricTensor, axes: Sequence[int]
) -> SymmetricTensor
Fuse the given public axes into one leg, placed at min(axes).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor whose axes are fused. |
required |
axes
|
sequence of int
|
The public axes to fuse. They must share one side and, ordered by
public position, be exactly the first |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The fused tensor: the new leg sits at |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
TypeError
|
If an axis is not an |
NotImplementedError
|
If the axes are not a prefix of their side's order; fusing a non-prefix group needs an F-move, which is not implemented. |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, OUT), Leg(V, IN)), seed=0)
>>> f = tenet.fuse(t, (0, 1))
>>> f.ndim, f.shape
(2, (4, 2))
>>> bool(tenet.allclose(tenet.unfuse(f, 0, t.legs[:2]), t))
True
identity ¶
identity(
legs: Sequence[Leg],
*,
dtype: Any = float64,
like: Any = "numpy",
) -> SymmetricTensor
id on ProductSpace(legs): the legs mirrored as (OUT..., IN...).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
legs
|
sequence of Leg
|
The legs to build the identity on. |
required |
dtype
|
dtype - like
|
The blocks' dtype. Default |
float64
|
like
|
str or array
|
Anything |
'numpy'
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The identity morphism, legs |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> i = tenet.identity((Leg(V, OUT),))
>>> i.shape, bool(tenet.allclose(i @ a, a))
((2, 2), True)
Notes
space, dual and name are kept and only side is set, so that
identity(t.codomain) @ t == t. Dualizing the mirror would build a cup, a
different morphism.
B_c = eye for every coupled sector, and nothing else — which is also the
sharpest test of MapLayout: this is the identity morphism only because the
row and column orderings are derived from block_order rather than
invented, so mirrored legs give mirrored bands.
like is anything ar.do accepts — a backend name or a reference array —
and defaults to today's "numpy": identity(legs) has no tensor to infer
a backend from, so a caller that does have one (ops/contraction.py::trace)
passes it; hard-coding it would make trace on a torch tensor contract torch
blocks against NumPy ones.
inner ¶
inner(a: SymmetricTensor, b: SymmetricTensor) -> Any
<a|b> = Σ_τ qdim(c_τ) · <A_τ, B_τ> — norm's sesquilinear sibling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
SymmetricTensor
|
The bra side; the pairing is sesquilinear (conjugate-linear) in |
required |
b
|
SymmetricTensor
|
The ket side; must have the same structure as |
required |
Returns:
| Type | Description |
|---|---|
scalar
|
The backend's own scalar |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the structures do not match — different providers, different |
CapabilityError
|
If |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> round(float(tenet.inner(a, a) - tenet.norm(a) ** 2), 6)
0.0
Notes
Coefficient space, per fusion-tree block, qdim-weighted — no diagram.
This is literally norm's body with the square replaced by a
conjugated pair, so inner(a, a) == norm(a) ** 2 holds identically rather
than numerically, and the dense Σ conj(a) · b over to_dense is its
oracle. It is TensorKit's spelling as well
(src/tensors/vectorinterface.jl: Σ_c dim(c) · inner(block(t1, c), block(t2, c))
in both fusion-style branches), i.e. the pairing MPSKit's Krylov machinery runs on.
Drawing the pairing as a diagram would be wrong here. Contracting every axis but
the first, then closing, makes the still-open axis-0 lines cross the contracted
ones, and on a graded provider each crossing of two odd lines pays -1: an
invariant scalar has (axis-0 sector) = (sector of the rest), so exactly the
odd-sector blocks would enter the sum with the wrong sign and inner(t, t) would
differ from norm(t) ** 2. No diagram, no crossing, no twist — and no rank cap
either: this works at any rank.
Returns the backend's own scalar, so the whole function stays traceable and differentiable, as norm is.
Summed per coupled sector, for the reason norm is: the weight is
the coupled sector's, a sector's matrix is exactly its blocks laid side by side, and
the two structures share a layout because they share a structure -- so cell i of
a's matrix pairs with cell i of b's wherever the block walk paired them.
Cutting either operand's blocks out to spell the same sum is a pass over both
tensors in front of a reduction that was going to read every element anyway.
isometry ¶
isometry(
codomain: Sequence[Leg],
domain: Sequence[Leg],
*,
dtype: Any = float64,
) -> SymmetricTensor
The inclusion domain -> codomain: W† W = id(domain), (W W†)² = W W†.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
codomain
|
sequence of Leg
|
The larger side. |
required |
domain
|
sequence of Leg
|
The smaller side being included. |
required |
dtype
|
dtype - like
|
The blocks' dtype. Default |
float64
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The inclusion isometry, legs |
Raises:
| Type | Description |
|---|---|
ValueError
|
embed's refusals: a leg count, provider or |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> W = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> w = tenet.isometry((Leg(W, OUT),), (Leg(V, IN),))
>>> bool(tenet.allclose(tenet.adjoint(w) @ w, tenet.identity((Leg(V, IN),))))
True
Notes
The whole body, and every refusal, is embed of
identity: the blocks come from the identity morphism and the placement
— each degeneracy slot into the same slot of the larger leg — is embed's
prefix convention, the one svd(..., bond=) and restrict already share.
A per-coupled-sector rectangular eye would also produce an isometry, and it
was rejected for naming a different map: it sends the j-th column band to
the j-th row band, an arbitrary correspondence whenever the two sides' band
orders do not line up.
Containment is required per leg, which is stricter than the fused,
sector-wise domain ≾ codomain TensorKit imposes: a target where no single
leg contains its partner but the fusion does is refused here.
map_diagonal ¶
map_diagonal(m: SymmetricTensor) -> SymmetricTensor
The diagonal of a square map, in the reduced basis, on its codomain legs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
m
|
SymmetricTensor
|
A square map: its domain must be its codomain as |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The diagonal entries, on |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the map is not square — |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> legs = (Leg(V, OUT), Leg(V, OUT, dual=True))
>>> d = tenet.map_diagonal(tenet.identity(legs)) # the identity's diagonal is all ones
>>> d.legs == legs
True
>>> [b.tolist() for b in d.blocks]
[[[1.0, 1.0], [1.0, 1.0]], [[1.0]]]
Notes
Coefficient space, not dense space, in
apply_blocks' sense: the result holds the diagonal of the
matrix a solver iterates on, not the diagonal of the dense expansion of m. The two
coincide entry for entry only where the Clebsch-Gordan factor is all-ones.
For a vector v on the same legs, entry k of the result is
(m @ v)[k] when v is the k-th reduced-basis unit vector — which is
also <v|m|v> / <v|v>, and that reading is basis-free and survives dense
expansion.
Which basis, and why no recoupling coefficient appears. Composition is one
matmul per coupled sector and nothing else — the module docstring above
explains why — so the matrix of m in the reduced storage basis is
to_matrices(m), and the diagonal of that matrix is the diagonal of the
blocks whose two fusion trees coincide. The trees are drawn from one set:
Leg.fused_sector reads dual, never side, so a square map's
codomain and domain contribute identical uncoupled labels. No F-symbol, no
R-symbol, no twist and no bend is read here, and the operation therefore
requires no capability beyond the fusion rules every structure already needs.
That is not in tension with the fusion-tree basis being relational
(invariant 4). The reduced basis of a rank-N map is labelled by a pair of
trees, not by a tuple of external sectors, and this reads the labels: for
SU(2) at external tuple (1, ½, ½, 1) two inner lines share one sector
tuple and are two distinct blocks with unrelated diagonal entries. What cannot
be done is to manufacture the diagonal by
contracting per-leg diagonals of the operator's factors, which is a per-leg
reading of that relational basis and loses both the inner line and the
graded braiding sign. Given the map itself, both are already in its blocks.
The whole operation is one diagonal per coupled sector. The result carries
m's codomain legs and no domain, so its own domain admits only the empty fusion
tree and it has exactly one coupled sector: the unit. Its matrix is one column, its
row bands are the codomain trees, and a row band's index flattens the codomain
degeneracies in out_axes order -- which is the order m's own rows flatten in,
and, m being square, the order its columns do too. The block at (tau, tau)
is therefore the diagonal square of m's unit matrix at that band, and the whole
column is that matrix's main diagonal. No block is cut out and no einsum runs
(invariant 8); the 26-subscript ceiling the einsum spelling carried is gone with it.
The unit-coupled reading. The result carries m's codomain legs, whose
blocks are the unit-coupled ones, and that is the whole diagonal of the
operator on the vectors tenet can represent: a SymmetricTensor on those
legs is invariant by construction (invariant 1), and a targeted charge is
carried by an explicit charge leg — which is then a leg of m too. A vector
stored on a different partition of the same legs (an MPS two-site tensor keeps
its right bond IN) reaches this basis by bend, which is one
scalar per block: a diagonal similarity, so it leaves both this diagonal and
the quotient q / (lambda - diag) entry for entry unchanged.
multiply ¶
multiply(t: SymmetricTensor, s: Any) -> SymmetricTensor
s * t for a scalar s — the only defined multiplication here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor to scale. |
required |
s
|
scalar
|
A Python number or a 0-d backend array. Never a |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> bool(tenet.allclose(tenet.multiply(a, 2.0), a + a))
True
negative ¶
negative(t: SymmetricTensor) -> SymmetricTensor
-t.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor to negate. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> round(float(tenet.norm(tenet.negative(a) + a)), 6)
0.0
norm ¶
norm(t: SymmetricTensor) -> Any
sqrt(Σ_τ qdim(c_τ) · ‖A_τ‖²) — the fusion-tree Frobenius norm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor whose norm is taken. |
required |
Returns:
| Type | Description |
|---|---|
scalar
|
The backend's own scalar (a float64 scalar on NumPy, a traceable 0-d
array on JAX), never a Python |
Raises:
| Type | Description |
|---|---|
CapabilityError
|
If |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> round(float(tenet.norm(a)), 6)
0.182373
Notes
The quantum-dimension weight is the point: each key contributes ‖A_τ‖²
once per coupled basis state and there are qdim(c) of them, so this
equals the dense Frobenius norm of t.to_dense() while never densifying
(that identity is an acceptance test). Dropping the
weight is wrong for any non-Abelian provider.
Uses qdim (capability QuantumDimensionData), not irrep_dim, so it is
defined even for providers with no dense expansion at all.
Returns the backend's own scalar, so the whole function is traceable and differentiable.
Summed per coupled sector, not per fusion tree. The weight depends on the
coupled sector alone and a sector's matrix is exactly its blocks laid side by side --
the grid is complete, so every cell is written once and none is left zero -- which
makes Σ_τ qdim(c_τ)·‖A_τ‖² and Σ_c qdim(c)·‖B_c‖² the same sum over the same
numbers, the identity the map view's own conventions are stated in. Reading blocks
to spell it the first way would cut every block of the tensor out of the matrices
the reduction is about to run over anyway.
random_isometry ¶
random_isometry(
codomain: Sequence[Leg],
domain: Sequence[Leg],
*,
seed: int | None = None,
dtype: Any = float64,
) -> SymmetricTensor
A Haar-random isometry: W† W = id(domain), independent per coupled sector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
codomain
|
sequence of Leg
|
The larger side. Requires |
required |
domain
|
sequence of Leg
|
The side the isometry is an isometry of: |
required |
seed
|
int or None
|
Seed for |
None
|
dtype
|
dtype - like
|
The blocks' dtype; a complex dtype gets a genuinely complex (Ginibre)
draw. Default |
float64
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
A Haar-random isometry, legs |
Raises:
| Type | Description |
|---|---|
ValueError
|
If some coupled sector has |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> W = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> w = tenet.random_isometry((Leg(W, OUT),), (Leg(V, IN),), seed=0)
>>> bool(tenet.allclose(tenet.adjoint(w) @ w, tenet.identity((Leg(V, IN),))))
True
Notes
Per coupled sector c, a (rows_c, cols_c) Gaussian draw, a QR, and the
sign fix that makes the result Haar-distributed.
"Haar per coupled sector" is the product of per-sector Haar measures, not Haar on the dense space. A symmetric isometry lives in a product of unitary groups, one per coupled sector, so the block-diagonal ensemble is the correct one — but a moment computed against a dense Haar formula will not match, and that is the ensemble, not a bug.
NumPy draws through np.random.default_rng(seed), exactly as
SymmetricTensor.random and identity's NumPy fill already do: a
constructor is not a traced operation, and t.to_backend("jax") is the route
onto a device. seed=None is non-reproducible.
A complex dtype gets a genuinely complex (Ginibre) draw, deliberately
departing from SymmetricTensor.random's "real draws cast to dtype"
shortcut: a real orthogonal matrix cast to complex128 is an isometry and
would pass every other criterion here, while being Haar on O(n) rather than
on U(n). Three lines, and it is a correctness trap otherwise.
restrict ¶
restrict(
t: SymmetricTensor,
legs: Sequence[Leg],
*,
atol: float | None = None,
) -> SymmetricTensor
t re-expressed on smaller legs: the leading degeneracy slots, nothing else.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor to cut down. |
required |
legs
|
sequence of Leg
|
The target legs, one per axis — the exact mirror of
embed's: |
required |
atol
|
float or None
|
The largest discarded residual accepted. |
None
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
CapabilityError
|
If the provider does not implement
QuantumDimensionData, which the
residual check needs ( |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> W = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> e = tenet.embed(a, (Leg(W, OUT), Leg(V, IN)))
>>> bool(tenet.allclose(tenet.restrict(e, a.legs), a))
True
Notes
Placement is the same prefix in the degeneracy index embed uses, so
the two agree about which slots are the "old" ones, and so does a subsequent
svd(..., bond=B).
Data outside the kept slots is refused, not discarded: the residual
sqrt(‖t‖² - ‖restrict(t)‖²) — exact by Pythagoras, since kept and dropped
occupy disjoint slots under the same qdim weight — is compared against
atol, which defaults to sqrt(eps(dtype)) * ‖t‖, the relative spelling
from_dense uses for the same units reason.
atol=tenet.PROJECT projects without checking, and that is
the form that goes inside jit; the comparison is a concrete-value question
and raises JAX's own ConcretizationTypeError under a trace otherwise.
This is not a truncation. The target comes from legs, static metadata
the caller chose, so restrict is shape-static and traceable and sits with
embed and svd(..., bond=) on the traceable side of
StructureChangingError. A target decided from the block values ("drop
whatever falls below 1e-8") is
tenet.linalg.svd_truncated's job.
subtract ¶
subtract(
a: SymmetricTensor, b: SymmetricTensor
) -> SymmetricTensor
a - b. Same structure rule as add.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
SymmetricTensor
|
The left operand. |
required |
b
|
SymmetricTensor
|
The right operand; its structure must equal |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The blockwise difference, on the operands' shared structure. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the structures differ, exactly as add refuses. |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> round(float(tenet.norm(tenet.subtract(a, a))), 6)
0.0
tensordot ¶
tensordot(
a: SymmetricTensor, b: SymmetricTensor, axes: Axes
) -> SymmetricTensor
Contract axes[0] of a against axes[1] of b, pairwise in order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
SymmetricTensor
|
The left operand; its free legs lead in the output. |
required |
b
|
SymmetricTensor
|
The right operand; must share |
required |
axes
|
tuple of two axis sequences, or int
|
|
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The contraction. Public axis order is |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
CapabilityError
|
If the axis pattern moves a leg between domain and codomain (a line
bend) and the provider does not implement |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> b = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=1)
>>> c = tenet.tensordot(a, b, axes=((1,), (0,)))
>>> c.legs == (a.legs[0], b.legs[1])
True
Notes
axes=((), ()) is the outer product and falls out of the
lowering rather than being special-cased.
A block-less operand — legs that cannot couple to any total charge — is legal
and needs no special case either. It lowers to no coupled-sector matrices, so every
coupled sector of the output is one the operands do not both carry, and the
composition's own missing-sector rule writes it as zeros: the result is the
structurally implied tensor, itself block-less whenever the free legs cannot couple
and explicit zeros wherever they can. Those zeros take their backend and dtype from
the other operand, and NumPy float64 when both operands are block-less.
All refusals, and all axis bookkeeping, live in contraction_plan;
what is left here is the execution of four already-tested operations.
to_symmetry ¶
to_symmetry(
t: SymmetricTensor,
target: _DualFusionRules,
*,
atol: float | None = None,
) -> SymmetricTensor
Restrict t to the smaller symmetry target, in the dense basis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor to restrict. Its provider must implement
BranchingRules (and |
required |
target
|
FusionRules
|
The smaller symmetry to restrict to. Must implement |
required |
atol
|
float or None
|
Forwarded to |
None
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The same physical object as a |
Raises:
| Type | Description |
|---|---|
CapabilityError
|
If |
ValueError
|
From |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import SU2, SU2Sector, U1
>>> V = GradedSpace.new(SU2, {SU2Sector(0): 1, SU2Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> u = tenet.to_symmetry(t, U1) # each SU(2) irrep j splits into weights 2Sz
>>> u.legs[0].space.sectors
((U1Sector(charge=-1), 1), (U1Sector(charge=0), 1), (U1Sector(charge=1), 1))
>>> u.shape == t.shape
True
Notes
atol is forwarded to from_dense, whose default
symmetry check is the free correctness oracle here — a wrong branching or a
wrong sort produces an array that is not target-symmetric and is refused.
atol=tenet.PROJECT skips it, which is what makes to_symmetry traceable.
Forgetting is not invertible: the result has strictly more free parameters than its source, so there is no inverse cast in this direction.
trace ¶
trace(
t: SymmetricTensor, axes: Sequence[int]
) -> SymmetricTensor
Close axis i of t onto axis j — the supertrace on a graded provider.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor to close one pair of legs on. Must keep at least one free leg afterwards. |
required |
axes
|
sequence of two ints
|
|
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
From the delegated tensordot call: a non-contractible pair, out-of-range axes, or a trace that would leave no free leg. |
CapabilityError
|
If closing a same-side pair needs a bend and the provider does not
implement |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> W = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(W, OUT), Leg(W, IN)), seed=0)
>>> tenet.trace(t, (1, 2)).ndim
1
Notes
The closed wire pays the ribbon twist, and that is what makes a loop's value unique. Of
the two wires this closure runs — one to each leg of the identity — exactly one has its
duality pairing against the direction the composition takes, and the categorical closure
differs from the naive one by theta there. Without it the same fermionic loop takes
different values depending on which of its wires was chosen to close, measured at a spread
of 2.0 on a 4-cycle; with it the choice does not matter to 1e-16
(tests/ops/test_twist.py). It is PEPSKit's str — tr for a bosonic braiding,
where theta is 1 and tenet.twist hands the tensor straight back. The map view's
closure, full_trace, is the pivotal trace and deliberately not this.
The identity's OUT leg meets
whichever of the two carries the dual object it needs, so a same-side pair
(which needs a bend) and an OUT/IN pair are the same code path — TensorKit
keeps a separate trace_permute! as a performance special case, not as
different mathematics.
The identity is built on t's own backend and dtype — the
dtype=ar.get_dtype_name(ref), like=ref spelling ops/map.py::compose
already uses for its zero-filled sectors. Without it a torch-backed
t would meet NumPy blocks in matmul. A block-less t carries neither,
and the identity is then NumPy float64 by the same rule the contraction's
zeros follow.
transpose ¶
transpose(
t: SymmetricTensor, axes: Sequence[int] | None = None
) -> SymmetricTensor
The same abstract tensor with public axes reordered as axes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor whose public axes are reordered. |
required |
axes
|
sequence of int or None
|
|
None
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The transposed tensor; |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
CapabilityError
|
If |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> W = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(W, OUT), Leg(W, IN)), seed=0)
>>> tenet.transpose(t, (2, 0, 1)).legs == (t.legs[2], t.legs[0], t.legs[1])
True
twist ¶
twist(
t: SymmetricTensor, axes: Sequence[int] | int
) -> SymmetricTensor
The ribbon twist theta on each named leg -- TensorKit's twist!.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor. Its legs, sides and structure are untouched: a twist is a scalar per block, not a permutation and not a bend. |
required |
axes
|
sequence of int, or int
|
The public axes whose lines are twisted. Repeats are not deduplicated --
twisting a leg twice pays |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
Raises:
| Type | Description |
|---|---|
CapabilityError
|
If the provider does not implement
TwistData, which is where |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> tenet.twist(t, 0) is t # theta = 1 here, so the tensor comes back untouched
True
Notes
The twist is what makes a closed line's value unique. sVect is a symmetric
ribbon category, so a closed diagram evaluates to one number whatever order it is
contracted in -- but only once every closure is the categorical one. A closure whose
duality pairing runs against the direction the composition takes differs from it by
theta on that line, and paying that is this function. It is PEPSKit's
twistdual/twistnondual (utility/util.jl) with the isdual test left to
the caller, because tenet spells V versus V* as side xor dual
(outward_dual) and the caller already knows which end it holds.
theta is (-1)^parity on a fermion-parity grading and 1 on every bosonic one,
the same grading datum braid reads its crossing sign from.
The structure is unchanged, so the coupled-sector layout is too, and the scalar per block factorizes: a twisted OUT leg reads the block's output tree only and a twisted IN leg its input tree only. The twist is therefore a diagonal scaling of the stored matrices, and no block is cut out to be multiplied by a sign.
unfuse ¶
unfuse(
t: SymmetricTensor, axis: int, legs: Sequence[Leg]
) -> SymmetricTensor
Split axis back into legs, inserted consecutively at axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor whose fused axis is split. |
required |
axis
|
int
|
The public axis to split. Must be the first axis of its side. |
required |
legs
|
sequence of Leg
|
The legs the axis should split into. They must share the axis's side
and reproduce |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The split tensor, |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
NotImplementedError
|
If |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, OUT), Leg(V, IN)), seed=0)
>>> f = tenet.fuse(t, (0, 1))
>>> bool(tenet.allclose(tenet.unfuse(f, 0, t.legs[:2]), t))
True
Notes
Because the split legs are inserted consecutively, fuse followed by
unfuse reproduces the original tensor exactly when the fused axes were
publicly consecutive; fusing publicly non-adjacent axes is not invertible
without the history this deliberately does not keep.
zip_blocks ¶
zip_blocks(
a: SymmetricTensor,
b: SymmetricTensor,
fn: Callable[[Array, Array], Array],
) -> SymmetricTensor
fn over the aligned block pairs of two tensors sharing one structure.
The two-argument sibling of apply_blocks, and coefficient space, not dense space in exactly the same sense.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
SymmetricTensor
|
The left operand; its structure is the result's. |
required |
b
|
SymmetricTensor
|
The right operand. Its structure must equal |
required |
fn
|
callable
|
An elementwise and shape-preserving function of two backend arrays of
equal shape. It is not checked (a shape change is caught by
|
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the structures differ — different providers, different |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> legs = (Leg(V, OUT), Leg(V, IN))
>>> q = SymmetricTensor.random(legs, seed=0)
>>> d = SymmetricTensor.random(legs, seed=1)
>>> p = tenet.zip_blocks(q, d, lambda x, y: x / (2.5 - y)) # a Jacobi step
>>> p.structure == q.structure
True
Notes
Why this does not reopen multiply's refusal. multiply
refuses a second SymmetricTensor because a * b asks for a categorical
operation and there is none: a tensor is Σ_τ A^(τ) ⊗ C^(τ), so an
entrywise dense product has no expression in the reduced blocks, and the
plausible-looking blockwise answer would be a silently different tensor —
multiply is defined by dense semantics and must keep them. This function
makes the opposite declaration in its name and signature: it is a map over
coefficients, the caller supplies the map, and no claim is made that it
commutes with to_dense. It cannot be reached by
an operator (*, /) and cannot be reached by accident, and requiring
one shared structure is what makes "the aligned block pair" mean something:
block_order is a pure function of the structure, so equal structures give
equal key tuples in equal order. The same argument already licenses the unary
apply_blocks; the arity is not what was ever in question.
The consumer this exists for is the Jacobi preconditioner of a Davidson step,
q / (lambda - diag) over the reduced storage a solver iterates on, with
diag from map_diagonal. That quotient is a
coefficient-space statement about the solver's own vector, not a statement
about the dense tensor, which is precisely the distinction multiply's
refusal draws.
Structure is untouched, so this is linear-algebra-free, backend-generic,
traceable and differentiable, exactly as apply_blocks is.
embed ¶
embed(
t: SymmetricTensor, legs: Sequence[Leg]
) -> SymmetricTensor
t re-expressed on larger legs: same data, leading slots, zeros elsewhere.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor to grow. |
required |
legs
|
sequence of Leg
|
The target legs, one per axis. |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> W = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> e = tenet.embed(a, (Leg(W, OUT), Leg(V, IN)))
>>> e.shape
(3, 2)
>>> bool(tenet.allclose(tenet.restrict(e, a.legs), a))
True
Notes
Since _block_order enumerates product(*(leg.sectors ...)) and
fusion_trees is a pure function of the sector tuple, containment makes the
source's keys a subset of the target's — which is the fact the loop below
rests on, and why no key can silently lose its data.
repartition ¶
repartition(
t: SymmetricTensor,
outputs: Sequence[int],
inputs: Sequence[int],
) -> SymmetricTensor
Public axes outputs become OUT and inputs become IN.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor to repartition. |
required |
outputs
|
sequence of int
|
The public axes (in |
required |
inputs
|
sequence of int
|
The public axes that end up IN. Together with |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The repartitioned tensor: public axis order exactly
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
CapabilityError
|
If a leg must cross between domain and codomain — a line bend — and
the provider does not implement |
Examples:
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> W = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(W, OUT), Leg(W, IN)), seed=0)
>>> r = tenet.repartition(t, (0,), (1, 2)) # axis 1 crosses to the domain
>>> r.structure.out_axes, r.structure.in_axes, r.legs[1].dual
((0,), (1, 2), True)
Notes
Owns no mathematics of its own: it transposes each crossing leg to the end,
bends it, and transposes once more to the requested order — the whole chain
composed once by repartition_plan and executed in a single pass, so
every block is copied once instead of once per step.
load ¶
load(path: str | PathLike) -> SymmetricTensor
Read a tensor written by save. NumPy blocks; structure exactly equal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or PathLike
|
The |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The saved tensor, blocks NumPy-backed; |
Raises:
| Type | Description |
|---|---|
ValueError
|
If — for SU(2), SU(N) or fZ2 — the file's gauge fingerprint is not the
running build's: block coefficients are only meaningful against the
CG / F / R conventions that produced them, so a gauge-mismatched file
is refused rather than silently misread. The one exception is the SU(2)
fingerprint listed in |
KeyError
|
For an unknown provider kind. |
Examples:
See save for the round trip.
Notes
Block count and per-block shape are validated by
SymmetricTensor.__post_init__, unmodified.
save ¶
save(
t: SymmetricTensor,
path: str | PathLike,
*,
compress: bool = False,
) -> None
Write t to path as a single .npz: a JSON header plus one array per block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
SymmetricTensor
|
The tensor written; any backend. Blocks are converted with
|
required |
path
|
str or PathLike
|
The destination file. |
required |
compress
|
bool
|
|
False
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If a |
ValueError
|
If a leg's provider is not one of the serializable kinds. |
Examples:
>>> import os, tempfile
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> with tempfile.TemporaryDirectory() as d:
... tenet.save(t, os.path.join(d, "t.npz"))
... t2 = tenet.load(os.path.join(d, "t.npz"))
>>> t2.structure == t.structure
True
>>> bool(tenet.allclose(t2, t))
True
Notes
A saved tensor loads back as NumPy — load(...).to_backend("jax") is the
documented restore, because a device placement is not a property of the tensor.
compress=False by default: reduced blocks are dense float arrays that do
not compress well, so paying zlib on every checkpoint buys nothing. The
zip container costs a constant overhead — a 4-block SU(2) tensor with 504
bytes of block data writes a 2282-byte file — which is not a bug.
enable_jax ¶
Turn on the JAX-facing features, in one call instead of three statements.
Two separate effects, and only the first is on by default:
-
The pytree registration (tenet.pytree) —
SymmetricTensorbecomes a JAX pytree whose leaves are its blocks and whose treedef is its TensorStructure, sojit,gradandvmapreach through it. This is local to this package: it registers our type with JAX and changes nothing about anyone else's. -
The broadened SVD/eigh VJPs (tenet.ad, with
ad=True) — the Lorentzian-broadened rules that stay finite at the degenerate spectra a non-Abelian symmetry produces. This one is process-global and reaches other libraries: the seam isautoray.register_function("jax", "linalg.svd", ...), autoray's own extension point, so afterwards anyar.do("linalg.svd", jax_array)in the process — quimb's included — gets the broadened VJP, and the broadened gradient is correct only for an objective that is gauge-invariant on each degenerate subspace. Mutating another library's dispatch table is the user's act, so it is opted into by name rather than defaulted on;tenet.ad's module docstring is the full argument.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ad
|
bool
|
Whether to also install |
False
|
Returns:
| Type | Description |
|---|---|
None
|
|
Raises:
| Type | Description |
|---|---|
ImportError
|
If JAX is not installed, naming the optional extra to install. |
Examples:
>>> import tenet
>>> tenet.enable_jax() # the pytree registration; calling it twice is a no-op
>>> tenet.enable_jax()
Notes
Idempotent, in both halves: re-importing tenet.pytree is a sys.modules hit and
tenet.ad.install() documents itself as idempotent, so repeat calls are harmless.
The older spellings are unchanged and keep working — import tenet.pytree and
tenet.ad.install() are what this function runs, and there is one implementation of
each. JAX stays an optional dependency: nothing here is imported by core.