Skip to content

tenet.symmetry

The symmetry providers and their sectors: U(1), Z2, fermionic Z2, SU(2), SU(N) and products, plus the capability protocols and the coherence validators.

SU(N) lives in tenet.symmetry.sun and is not re-exported here: a provider carries its own n, so there is no singleton to export. Its coefficients, and SU(2)'s, come from racah-py, a core dependency, so import tenet.symmetry.sun works on a plain pip install symtenet.

tenet.symmetry

Symmetry providers and sector labels.

A provider answers every categorical question about one symmetry — fusion rules, quantum dimensions, duality, F/R symbols, braiding — and a sector labels one irrep. Trivial, Z2, fZ2, U1 and SU2 are the shipped instances, each beside its class and its *Sector type, and ProductProvider composes two into one.

The capability protocol is the rest of the page: supports and requires ask and demand, the *Data protocols name what a provider may implement, and CapabilityError is the refusal raised when one is missing.

Trivial module-attribute

Trivial = TrivialProvider()

Module-level singleton, used as GradedSpace(provider=Trivial, ...).

fZ2 module-attribute

fZ2 = FZ2Provider()

Module-level singleton, used as GradedSpace(provider=fZ2, ...).

SU2 module-attribute

SU2 = SU2Provider()

Module-level singleton, used as GradedSpace(provider=SU2, ...).

U1 module-attribute

U1 = U1Provider()

Module-level singleton, used as GradedSpace(provider=U1, ...).

Z2 module-attribute

Z2 = Z2Provider()

Module-level singleton, used as GradedSpace(provider=Z2, ...).

AssociatorData

Bases: Protocol

Providers that supply the associator F, scalar-valued.

Scalar-valued is a multiplicity-free assumption: a provider with n_symbol > 1 must raise rather than truncate a matrix-valued symbol, and supply FMatrixData beside this protocol instead.

f_symbol

f_symbol(
    a: Sector,
    b: Sector,
    c: Sector,
    d: Sector,
    e: Sector,
    f: Sector,
) -> complex

[F^{abc}_d]_{e,f}; e is the inner line of ((ab)c), f of (a(bc)).

Parameters:

Name Type Description Default
a Sector

The three fused sectors, in order.

required
b Sector

The three fused sectors, in order.

required
c Sector

The three fused sectors, in order.

required
d Sector

The total (coupled) sector.

required
e Sector

The inner line of the ((ab)c) association.

required
f Sector

The inner line of the (a(bc)) association.

required

Returns:

Type Description
complex

The recoupling coefficient; exactly 0 for a structurally forbidden labelling.

Raises:

Type Description
ValueError

If any vertex of the labelling has n_symbol > 1 — the scalar symbol must refuse rather than truncate a matrix-valued one.

BendingCoefficients

Bases: Protocol

Providers that can move one line between the two trees of a block key.

bend_right

bend_right(
    key: FusionBlockKey, *, dual: bool
) -> tuple[tuple[FusionBlockKey, complex], ...]

Move the LAST uncoupled line of output_tree onto the END of input_tree, dualized.

Parameters:

Name Type Description Default
key FusionBlockKey

The block key whose output tree loses its last line.

required
dual bool

The moved leg's current dual flag; it selects the Frobenius-Schur factor.

required

Returns:

Type Description
tuple of (FusionBlockKey, complex)

((key', coeff), ...), the expansion of the bent key.

bend_left

bend_left(
    key: FusionBlockKey, *, dual: bool
) -> tuple[tuple[FusionBlockKey, complex], ...]

The inverse direction: last line of input_tree onto output_tree.

Parameters:

Name Type Description Default
key FusionBlockKey

The block key whose input tree loses its last line.

required
dual bool

The moved leg's current dual flag.

required

Returns:

Type Description
tuple of (FusionBlockKey, complex)

((key', coeff), ...), the expansion of the bent key.

BMatrixData

Bases: Protocol

Array-valued bend — DualityData's sibling for N^c_ab > 1.

b_matrix

b_matrix(a: Sector, b: Sector, c: Sector) -> ndarray

B^{ab}_c, shape (N^c_ab, N^a_{c,dual(b)}).

Parameters:

Name Type Description Default
a Sector

The vertex's two fused sectors; b is the line bent away.

required
b Sector

The vertex's two fused sectors; b is the line bent away.

required
c Sector

The vertex's fusion channel.

required

Returns:

Type Description
ndarray

Shape (N^c_ab, N^a_{c,dual(b)}): the source vertex label against the destination's new vertex label.

BraidingData

Bases: Protocol

Providers that supply the braiding R, scalar-valued.

Notes

Having R does not make the braiding symmetric: transpose gates on this protocol plus the symmetric-braiding property (R == R**-1), and a chiral provider is refused rather than handed one of two inequivalent braids. Multiplicity-bearing providers supply RMatrixData beside this one.

r_symbol

r_symbol(a: Sector, b: Sector, c: Sector) -> complex

R^{ab}_c, the coefficient of braiding a past b inside c.

Parameters:

Name Type Description Default
a Sector

The two braided sectors, in order.

required
b Sector

The two braided sectors, in order.

required
c Sector

The fusion channel they are braided inside.

required

Returns:

Type Description
complex

The braiding phase; unit modulus in a unitary gauge.

Raises:

Type Description
ValueError

If N^c_ab > 1 — a matrix-valued braiding must be served through RMatrixData, never truncated.

BranchingRules

Bases: Protocol

Providers that can be restricted to a smaller symmetry in the dense basis.

branch

branch(
    target: FusionRules, a: Sector
) -> tuple[Sector, ...]

The target sector of each of a's d_a dense basis vectors.

Parameters:

Name Type Description Default
target FusionRules

The smaller symmetry restricted to; must be abelian.

required
a Sector

The sector of this provider being decomposed.

required

Returns:

Type Description
tuple of Sector

One target sector per dense basis vector, length exactly irrep_dim(a).

Raises:

Type Description
CapabilityError

For a target this provider cannot restrict to.

Notes

The order is the provider's own dense (magnetic) order — the same order cgc and z_matrix use, so this composes with to_dense's alpha * d_a + m layout without a second convention. Every returned sector must satisfy target.irrep_dim(...) == 1: one target label per basis vector is only well-defined when the target's irreps are one-dimensional, i.e. when the target is abelian.

CapabilityError

Bases: TypeError

Raised when a provider lacks a capability an operation requires.

ClebschGordanData

Bases: Protocol

Providers that can supply explicit Clebsch-Gordan tensors.

Notes

A dense-basis capability: it exists exactly when the sectors are representations of something. An anyonic provider (Fibonacci, Ising) has no cgc and no irrep_dim at all, which is why quantum dimensions live on QuantumDimensionData instead.

irrep_dim

irrep_dim(a: Sector) -> int

The dense dimension d_a of the irrep labelled a.

Parameters:

Name Type Description Default
a Sector

The sector whose irrep dimension is asked.

required

Returns:

Type Description
int

d_a >= 1, the length of each of cgc's first three axes.

cgc

cgc(a: Sector, b: Sector, c: Sector) -> ndarray

Shape (d_a, d_b, d_c, N^c_ab); last axis is the multiplicity label mu.

Parameters:

Name Type Description Default
a Sector

The two fused sectors.

required
b Sector

The two fused sectors.

required
c Sector

The fusion channel selected.

required

Returns:

Type Description
ndarray

The Clebsch-Gordan tensor of a x b -> c in the provider's own dense basis, shape (d_a, d_b, d_c, N^c_ab) — the trailing axis is the multiplicity label mu and is present even when it has size 1.

Raises:

Type Description
ValueError

If c is not a fusion channel of a x b.

DaggerData

Bases: Protocol

Marker for the dagger structure adjoint/conj rely on.

Every provider tenet ships is unitary in a real-or-unitary gauge, so the dagger needs no per-provider data yet and every provider satisfies this protocol. A provider with complex Clebsch-Gordan tensors or a non-unitary F/R gauge is the trigger for making it a real capability gate.

DualBasis

Bases: Protocol

Providers whose V_a -> V_a^* isomorphism is available in the dense basis.

z_matrix

z_matrix(a: Sector) -> ndarray

Z_a, shape (d_a, d_dual(a)), in the provider's own dense basis.

Parameters:

Name Type Description Default
a Sector

The sector whose V_a -> V_a^* isomorphism is asked.

required

Returns:

Type Description
ndarray

Z_a, shape (d_a, d_dual(a)), in the same gauge as cgc.

DualityData

Bases: Protocol

Providers that supply rigidity: the dual label map and the bend B.

Notes

dual alone (the label map every provider carries) is not rigidity; the B-symbol is what prices an evaluation/coevaluation bend. Multiplicity-bearing providers supply BMatrixData beside this one.

dual

dual(a: Sector) -> Sector

The dual (conjugate) label of a.

Parameters:

Name Type Description Default
a Sector

The sector dualized.

required

Returns:

Type Description
Sector

dual(a), with dual(dual(a)) == a. A self-dual label (dual(a) == a) does not make the V_a -> V_a^* isomorphism the identity — that is z_matrix's content.

b_symbol

b_symbol(a: Sector, b: Sector, c: Sector) -> complex

B^{ab}_c, the duality coefficient bending b out of a x b -> c.

Parameters:

Name Type Description Default
a Sector

The vertex's two fused sectors; b is the line bent away.

required
b Sector

The vertex's two fused sectors; b is the line bent away.

required
c Sector

The vertex's fusion channel.

required

Returns:

Type Description
complex

The bend coefficient.

Raises:

Type Description
ValueError

If any vertex of the labelling has n_symbol > 1 — matrix-valued bends are served through BMatrixData.

FMatrixData

Bases: Protocol

Array-valued associator for providers with N^c_ab > 1.

AssociatorData's array-valued sibling, a capability beside it: the scalar symbol stays exactly as it is for every multiplicity-free provider, and _artin_braid takes the array path only when the provider also implements the array-valued protocols.

f_matrix

f_matrix(
    a: Sector,
    b: Sector,
    c: Sector,
    d: Sector,
    e: Sector,
    f: Sector,
) -> ndarray

[F^{abc}_d]_{e,f}, shape (N^e_ab, N^d_ec, N^f_bc, N^d_af).

Parameters:

Name Type Description Default
a Sector

The three fused sectors, in order.

required
b Sector

The three fused sectors, in order.

required
c Sector

The three fused sectors, in order.

required
d Sector

The total (coupled) sector.

required
e Sector

The inner line of the ((ab)c) association.

required
f Sector

The inner line of the (a(bc)) association.

required

Returns:

Type Description
ndarray

Shape (N^e_ab, N^d_ec, N^f_bc, N^d_af). The four axes are the four vertex labels of ((ab)c)_d and (a(bc))_d, in that order.

FSIndicatorData

Bases: Protocol

Providers that supply the Frobenius-Schur indicator chi.

frobenius_schur

frobenius_schur(a: Sector) -> complex

chi_a, the Frobenius-Schur phase of the V_a -> V_a^* isomorphism.

Parameters:

Name Type Description Default
a Sector

The sector whose indicator is asked.

required

Returns:

Type Description
complex

chi_a, a phase; +-1 for every real-or-unitary gauge tenet ships.

FusionRules

Bases: Protocol

The fusion ring: named sector labels, the unit, channels and multiplicities.

Deliberately not called a fusion category: fusion rules N^c_ab do not determine one — a fusion category is the rules plus an associator (AssociatorData) plus rigidity (DualityData), and the same rules with different associators are genuinely different categories (Vec_G versus Vec_G^omega). dual is duality data and lives on DualityData; the label-level dual map every provider carries is annotated through _DualFusionRules where it is actually read.

Notes

"Fusion category" is the name of a combinationFusionRules + AssociatorData + DualityData with tenet.symmetry.coherence.validate_pentagon and validate_snake passing — and it is a name in the docs, never a class here.

name property

name: str

The provider's label, e.g. "U1" — a display string, never dispatched on.

unit property

unit: Sector

The unit (vacuum) sector: the identity of fusion, unit x a -> a.

fusion

fusion(a: Sector, b: Sector) -> tuple[Sector, ...]

The fusion channels of a x b, in the provider's canonical order.

Parameters:

Name Type Description Default
a Sector

The two sectors fused, in this provider's own sector type.

required
b Sector

The two sectors fused, in this provider's own sector type.

required

Returns:

Type Description
tuple of Sector

Every c with N^c_ab > 0, each listed once regardless of its multiplicity, in a deterministic canonical (ascending) order — block enumeration is derived from this order, so it must never depend on dict iteration or insertion history.

n_symbol

n_symbol(a: Sector, b: Sector, c: Sector) -> int

Multiplicity N^c_ab. Multiplicity-free providers return 0 or 1.

Parameters:

Name Type Description Default
a Sector

The two sectors fused.

required
b Sector

The two sectors fused.

required
c Sector

The candidate fusion channel.

required

Returns:

Type Description
int

The number of independent vertices a x b -> c; 0 when the channel is forbidden.

PermutationCoefficients

Bases: Protocol

Providers that can expand a fusion tree with permuted uncoupled lines.

permute_tree

permute_tree(
    tree: FusionTree, perm: tuple[int, ...]
) -> tuple[tuple[FusionTree, complex], ...]

((tree', coeff), ...) with the permuted tree equal to Σ coeff · tree'.

Parameters:

Name Type Description Default
tree FusionTree

The left-associated tree whose uncoupled lines are permuted.

required
perm tuple of int

perm[j] is the old uncoupled position that becomes position j (the same convention as transpose's axes).

required

Returns:

Type Description
tuple of (FusionTree, complex)

The expansion of the permuted tree over canonical left-associated trees with the same coupled sector.

Notes

The coefficient stays scalar even under n_symbol > 1, because the multiplicity label lives inside the tree: a matrix-valued F is still one number per (tree, tree') pair, it just makes the expansion longer.

PivotalData

Bases: Protocol

Marker for the pivotal convention the bend helpers hardcode.

The pivotal isomorphism today is one fixed choice — TensorKit's bendright, the sqrt(qdim(c)/qdim(a)) split of the bend's normalization in bend_braided — shared by every provider, so this protocol carries no method yet and every provider satisfies it.

Notes

It exists so the operations that depend on a pivotal structure (bend, full_trace) can name it in their contracts now; a non-spherical pivotal provider is the trigger for giving it a real method (the per-object pivotal phase) instead of the hardcoded expression.

QuantumDimensionData

Bases: Protocol

Providers that define a quantum dimension.

Notes

qdim need not be an integer (Fibonacci's tau has qdim == phi) and is independent of any dense expansion (ClebschGordanData).

qdim

qdim(a: Sector) -> float

The quantum dimension d_a.

Parameters:

Name Type Description Default
a Sector

The sector whose quantum dimension is asked.

required

Returns:

Type Description
float

d_a > 0. Equal to irrep_dim(a) whenever the provider also has ClebschGordanData, but not an integer in general.

RMatrixData

Bases: Protocol

Array-valued braiding — BraidingData's sibling for N^c_ab > 1.

r_matrix

r_matrix(a: Sector, b: Sector, c: Sector) -> ndarray

R^{ab}_c, shape (N^c_ab, N^c_ba).

Parameters:

Name Type Description Default
a Sector

The two braided sectors, in order.

required
b Sector

The two braided sectors, in order.

required
c Sector

The fusion channel they are braided inside.

required

Returns:

Type Description
ndarray

Shape (N^c_ab, N^c_ba): the vertex label before against the vertex label after the braid.

Sector dataclass

Sector()

Marker base for sector labels.

Subclasses are frozen, slotted, ordered dataclasses, so hashing and canonical sorting come for free. Comparison is only defined within one sector type; comparing different sector types raises TypeError.

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.

TrivialProvider dataclass

TrivialProvider(name: str = 'Trivial')

Degenerate reference provider: one sector, everything trivial.

permute_tree

permute_tree(
    tree: FusionTree, perm: tuple[int, ...]
) -> tuple[tuple[FusionTree, complex], ...]

One term, coefficient 1: there is a single sector and F = R = 1.

bend_right

bend_right(
    key: FusionBlockKey, *, dual: bool
) -> tuple[tuple[FusionBlockKey, complex], ...]

One term, coefficient 1: B follows from F ≡ 1 and dim ≡ 1.

z_matrix

z_matrix(a: Sector) -> ndarray

Z = [[1]], read-only: one-dimensional irrep, Frobenius-Schur phase 1.

frobenius_schur

frobenius_schur(a: Sector) -> float

chi_a = 1: one sector, everything trivial.

twist

twist(a: Sector) -> float

theta_a = 1: one sector, everything trivial.

TrivialSector dataclass

TrivialSector()

Bases: Sector

The single sector of the trivial symmetry.

TwistData

Bases: Protocol

Providers that supply the ribbon twist theta.

Notes

Separate from the chi * theta flip product, because closed loops (trace, adjoint, any fermion loop) need the bare twist. 1 for every symmetric bosonic category, (-1)^parity for fermion parity, e^{4 pi i / 5} on Fibonacci's tau.

twist

twist(a: Sector) -> complex

theta_a, the ribbon twist of a.

Parameters:

Name Type Description Default
a Sector

The sector whose twist is asked.

required

Returns:

Type Description
complex

theta_a, a phase; 1 for every symmetric bosonic category, (-1)^parity for fermion parity.

FZ2Provider dataclass

FZ2Provider(name: str = 'fZ2')

Z2 fusion with fermionic braiding: SVect. Abelian, multiplicity-free, d = 1.

Use the module-level singleton fZ2 rather than constructing one; name is an identity label that participates in equality, not a configuration knob. The capability contract of every method is documented once, on the protocols in tenet.symmetry — only behaviour that differs from a protocol carries a docstring here, and the whole fermionic content is the Koszul sign in permute_tree and the -1 in twist.

Examples:

>>> from tenet.symmetry import fZ2, FZ2Sector
>>> fZ2.fusion(FZ2Sector(1), FZ2Sector(1))
(FZ2Sector(parity=0),)
>>> int(fZ2.twist(FZ2Sector(1)))  # the sign a closed odd loop pays
-1
>>> int(fZ2.frobenius_schur(FZ2Sector(1)))  # the FS phase stays +1
1

dual

dual(a: FZ2Sector) -> FZ2Sector

Both sectors are self-dual (dual(f) = f).

cgc

cgc(a: FZ2Sector, b: FZ2Sector, c: FZ2Sector) -> ndarray

Shape (1, 1, 1, 1), read-only. Raises on a fusion-forbidden triple.

permute_tree

permute_tree(
    tree: FusionTree, perm: tuple[int, ...]
) -> tuple[tuple[FusionTree, complex], ...]

Exactly one term: the tree is unique per (uncoupled, coupled) and the coefficient is the Koszul sign of perm restricted to the odd lines.

The coefficient reads only the uncoupled parities — not the coupled sector and not the inner lines — because F ≡ 1 makes the tree shape drop out. That is not true for SU(2), whose exchange sign (-1)^(j_a+j_b-j_c) reads the fused channel; the spine may be ignored here and nowhere else.

bend_right

bend_right(
    key: FusionBlockKey, *, dual: bool
) -> tuple[tuple[FusionBlockKey, complex], ...]

One term, coefficient 1: dim = 1, B = N, FS = +1.

TensorKitSectors defines no frobenius_schur_phase for FermionParity; it falls through to frobenius_schur_phase_from_Fsymbol(a) = sign(Fsymbol(a, dual(a), a, a, leftunit(a), rightunit(a))), and since dual(f) = f and every allowed F-symbol is +1, FS(odd) == +1. Bsymbol likewise falls through to Bsymbol_from_Fsymbol, giving +1. https://github.com/QuantumKitHub/TensorKitSectors.jl/blob/bd1bf8296876103870d6158c0918987256396880/src/sectors.jl#L535-L543 The -1 of fermion parity lives in twist, not in FS — see the module docstring.

z_matrix

z_matrix(a: FZ2Sector) -> ndarray

Z = [[1]], read-only: V_a is one-dimensional and FS == +1.

frobenius_schur

frobenius_schur(a: FZ2Sector) -> float

chi_a = +1 for both sectors: TensorKit keeps a regular trace and the parity sign lives entirely in twist — see the module docstring.

twist

twist(a: FZ2Sector) -> float

theta_a = (-1)^parity: the fermionic twist, the sign a closed odd loop pays.

FZ2Sector dataclass

FZ2Sector(parity: int)

Bases: Sector

A fermion-parity sector: parity in {0, 1}. 0 is even (the unit), 1 is odd.

Parameters:

Name Type Description Default
parity int

0 or 1. A bool is refused even though it is an int subclass, so FZ2Sector(True) != FZ2Sector(1) can never become a live question.

required

Raises:

Type Description
TypeError

If parity is not an int (bool included).

ValueError

If parity is not 0 or 1.

ProductProvider dataclass

ProductProvider(factors: tuple[_DualFusionRules, ...])

The Deligne product of two or more providers. Frozen, hashable, array-free.

Every core quantity (unit, dual, fusion, n_symbol, qdim, irrep_dim, cgc, the phases) is componentwise, so the capability contract stays the one documented on the protocols in tenet.symmetry; the methods here document only what forwarding adds.

Parameters:

Name Type Description Default
factors tuple of provider

Two or more providers, in order; the order is the component order of every ProductSector.

required

Raises:

Type Description
ValueError

If fewer than two factors are given — a one-factor product is a confusing no-op wrapper, not a feature.

Examples:

>>> from tenet.symmetry import U1, U1Sector, Z2, Z2Sector
>>> from tenet.symmetry import ProductProvider, ProductSector
>>> p = ProductProvider((U1, Z2))
>>> p.name
'U1 x Z2'
>>> a = ProductSector((U1Sector(1), Z2Sector(1)))
>>> p.fusion(a, a)
(ProductSector(components=(U1Sector(charge=2), Z2Sector(parity=0))),)
>>> p.dual(a)
ProductSector(components=(U1Sector(charge=-1), Z2Sector(parity=1)))
Notes

Optional capabilities are forwarded iff every factor has them, and the forwarding methods are defined unconditionally so that the error can name the offending factor. The consequence is a real wart, handled by raising rather than by lying: runtime_checkable protocols check only for the presence of a method, so isinstance(ProductProvider((SU2, U1)), PermutationCoefficients) is True even though the call raises. The call is authoritative, isinstance is optimistic. Synthesizing a subclass per capability combination — a factory for one product — is rejected on sight.

Equality and hashing are on factors alone; name is a property, not a field, so it can never be a second source of truth.

fusion

fusion(a: Sector, b: Sector) -> tuple[ProductSector, ...]

Componentwise, and ascending: itertools.product is lexicographic and ProductSector compares lexicographically, so the canonical order of each factor's fusion survives (asserted by test, not assumed).

frobenius_schur

frobenius_schur(a: Sector) -> complex

chi of a Deligne product factorizes, like every other phase.

twist

twist(a: Sector) -> complex

theta of a Deligne product factorizes, like every other phase.

cgc

cgc(a: Sector, b: Sector, c: Sector) -> ndarray

Shape (prod d_a, prod d_b, prod d_c, prod n), the iterated outer product of the factors' cgcs flattened in C order — which is exactly the mixed radix of mu_encode. Raises ValueError (from the factor) on a fusion-forbidden triple.

z_matrix

z_matrix(a: Sector) -> ndarray

Z_a: V_a -> V_a^*, the Kronecker product of the factors' duality maps.

Flattened in the same C order as cgc, and for the same reason: np.kron of the factor matrices is the mixed radix with the leading factor most significant, so the two flattenings agree by construction rather than by matching arithmetic.

bend_right

bend_right(
    key: FusionBlockKey, *, dual: bool
) -> tuple[tuple[FusionBlockKey, complex], ...]

The distributed product of the factors' bends, as permute_tree is.

A bend moves one line from the end of the output tree to the end of the input tree, and in a product that one line carries one component per factor, each moving under its own factor's bending coefficient. So the bent product key is ⊗_i (Σ_α c_α^(i) k_α^(i)), which multiplies out to this. Written for many terms per factor because SU(2)'s bend reaches more than one.

The dual flag is passed through unchanged rather than distributed: each factor applies its own Frobenius-Schur phase, and the product's frobenius_schur is exactly the product of those, so the phases compose to the right one without being counted here as well.

bend_left

bend_left(
    key: FusionBlockKey, *, dual: bool
) -> tuple[tuple[FusionBlockKey, complex], ...]

The inverse direction of bend_right, assembled the same way.

permute_tree

permute_tree(
    tree: FusionTree, perm: tuple[int, ...]
) -> tuple[tuple[FusionTree, complex], ...]

The distributed product of the factors' expansions.

The braiding of a Deligne product is the tensor product of the factors' braidings and the associator factorizes, so the permuted product tree is literally ⊗_i (Σ_α c_α^(i) t_α^(i)), which multiplies out to this. Written for many terms per factor, which SU(2)'s braid expansion actually reaches — a single-term special case would be a correctness trap.

ProductSector dataclass

ProductSector(components: tuple[Sector, ...])

Bases: Sector

One sector per factor, in factor order. Ordering is lexicographic by component.

Parameters:

Name Type Description Default
components tuple of Sector

One sector per factor of the ProductProvider, in factor order.

required

SU2Provider dataclass

SU2Provider(name: str = 'SU2')

SU(2) provider. Array-free and hashable; CG arrays live in a module-level cache.

Use the module-level singleton SU2 rather than constructing one; name is an identity label that participates in equality, not a configuration knob. The capability contract of every method is documented once, on the protocols in tenet.symmetry — only behaviour that differs from a protocol carries a docstring here.

Examples:

>>> from tenet.symmetry import SU2, SU2Sector
>>> half = SU2Sector(1)                    # two_j = 1 is spin-1/2
>>> SU2.fusion(half, half)                 # singlet and triplet
(SU2Sector(two_j=0), SU2Sector(two_j=2))
>>> SU2.irrep_dim(SU2Sector(2))
3
>>> int(SU2.frobenius_schur(half))         # half-integer spin: chi = -1
-1

fusion

fusion(a: SU2Sector, b: SU2Sector) -> tuple[SU2Sector, ...]

|ja-jb| <= jc <= ja+jb in integer steps, ascending in two_j.

cgc

cgc(a: SU2Sector, b: SU2Sector, c: SU2Sector) -> ndarray

Shape (d_a, d_b, d_c, 1), read-only; magnetic indices descending.

The trailing axis is the multiplicity label mu; SU(2) is multiplicity-free so it always has size 1, but consumers must still index it.

f_symbol

f_symbol(
    a: SU2Sector,
    b: SU2Sector,
    c: SU2Sector,
    d: SU2Sector,
    e: SU2Sector,
    f: SU2Sector,
) -> float

[F^{abc}_d]_{e,f}; e is the inner line of ((ab)c), f of (a(bc)).

Real in this gauge, so downstream conjugation of domain-side coefficients is a no-op. Raises if any vertex has n_symbol > 1 (unreachable for SU(2), asserted so the scalar-valued contract is not merely documentation).

Exactly 0.0 when any of the four vertices is empty. That guard is load-bearing: racah raises ValueError on an N = 0 vertex, where SU(2)'s contract is a true zero.

r_symbol

r_symbol(a: SU2Sector, b: SU2Sector, c: SU2Sector) -> int

R^{ab}_c = (-1)^(ja + jb - jc), exactly +-1; SU(2) braiding is symmetric.

racah's exact tier returns the closed-form sign directly, so the float is already exactly +-1.0; it is still snapped to int behind the unit-modulus assert, because the exact integer is the contract every braid phase in permute_braided_tree multiplies through and the assert is what would catch a tier that stopped being exact.

b_symbol

b_symbol(a: SU2Sector, b: SU2Sector, c: SU2Sector) -> float

B^{ab}_c, derived from f_symbol; real, of unit modulus.

permute_tree

permute_tree(
    tree: FusionTree, perm: tuple[int, ...]
) -> tuple[tuple[FusionTree, complex], ...]

Artin-braid expansion; SU(2) is symmetric, so perm fixes the braid.

bend_right

bend_right(
    key: FusionBlockKey, *, dual: bool
) -> tuple[tuple[FusionBlockKey, complex], ...]

One term: sqrt(qdim(c)/qdim(a))·B(a,b,c), times chi if already dual.

frobenius_schur

frobenius_schur(a: SU2Sector) -> int

chi_a = (-1)^(2j): +1 for integer spin, -1 for half-integer.

twist

twist(a: SU2Sector) -> int

theta_a = 1: SU(2) braiding is symmetric, so the twist is trivial.

z_matrix

z_matrix(a: SU2Sector) -> ndarray

Z_a: V_a -> V_a^*, shape (d_a, d_dual(a)) == (d_a, d_a); read-only.

Z[i, d_a - 1 - i] = (-1)**i in the descending-m basis of cgc. Derived, not declared: the singlet in V_j (x) V_j is the duality pairing, so it cannot drift out of gauge with the CG tensors to_dense contracts it against — see tenet.symmetry._sun_coeff.z_matrix. Not the identity for two_j >= 1, even though dual(a) == a.

branch

branch(
    target: FusionRules, a: SU2Sector
) -> tuple[Sector, ...]

SU(2) -> U(1): the magnetic quantum numbers, doubled, descending.

cgc's magnetic indices run descending, so index k of V_j carries S_z = j - k and charge 2 S_z = two_j - 2 k. Identical to froSTspin's np.arange(irr - 1, -irr - 1, -2).

Dual-agnostic: on a dual leg to_dense applies z_matrix, an antidiagonal ±1 signed permutation, so reversing the magnetic order and negating the weight are the same operation and cancel.

SU2Sector dataclass

SU2Sector(two_j: int)

Bases: Sector

An SU(2) irrep labelled by two_j = 2j >= 0.

Parameters:

Name Type Description Default
two_j int

The doubled spin, so labels stay exact integers: 0 is the singlet, 1 spin-1/2, 2 spin-1.

required

Raises:

Type Description
TypeError

If two_j is not an int (bool included).

ValueError

If two_j is negative.

SUNProvider dataclass

SUNProvider(n: int, name: str = 'SUN')

SU(N) provider for n >= 2. Array-free and hashable; arrays come from racah.

SUNProvider(3) is SU(3). Every sector-taking method validates that the Dynkin label has n - 1 entries, so feeding an SU(4) sector to an SU(3) provider fails at the first query rather than producing nonsense.

The capability contract of every method is documented once, on the protocols in tenet.symmetry — only behaviour that differs from a protocol carries a docstring here. Every coefficient (fusion, CG, F/R/B matrices, FS) is delegated to the racah crate through tenet.symmetry._sun_coeff, whose fingerprint is the gauge.

Parameters:

Name Type Description Default
n int

The N of SU(N); at least 2.

required
name str

An identity label that participates in equality, not a configuration knob; leave it at the default.

'SUN'

Raises:

Type Description
TypeError

If n is not an int (bool included).

ValueError

If n < 2.

Examples:

>>> from tenet.symmetry.sun import SUNProvider, SUNSector
>>> su3 = SUNProvider(3)
>>> f, fbar = SUNSector((1, 0)), SUNSector((0, 1))
>>> su3.fusion(f, fbar)                          # 3 x 3bar = 1 + 8
(SUNSector(dynkin=(0, 0)), SUNSector(dynkin=(1, 1)))
>>> adj = SUNSector((1, 1))
>>> su3.n_symbol(adj, adj, adj)                  # 8 x 8 -> 8 has multiplicity 2
2
>>> su3.dual(f)                                  # the conjugate irrep
SUNSector(dynkin=(0, 1))

dual

dual(a: SUNSector) -> SUNSector

The conjugate irrep: the Dynkin label reversed. Not the identity for N > 2.

fusion

fusion(a: SUNSector, b: SUNSector) -> tuple[SUNSector, ...]

The Littlewood-Richardson decomposition of a x b, ascending in Dynkin order.

cgc

cgc(a: SUNSector, b: SUNSector, c: SUNSector) -> ndarray

Shape (d_a, d_b, d_c, N^c_ab), read-only; last axis is the multiplicity label.

f_matrix

f_matrix(
    a: SUNSector,
    b: SUNSector,
    c: SUNSector,
    d: SUNSector,
    e: SUNSector,
    f: SUNSector,
) -> ndarray

[F^{abc}_d]_{e,f}, shape (N^e_ab, N^d_ec, N^f_bc, N^d_af), read-only.

r_matrix

r_matrix(
    a: SUNSector, b: SUNSector, c: SUNSector
) -> ndarray

R^{ab}_c, shape (N^c_ab, N^c_ba), read-only.

b_matrix

b_matrix(
    a: SUNSector, b: SUNSector, c: SUNSector
) -> ndarray

B^{ab}_c, shape (N^c_ab, N^a_{c,dual(b)}).

f_symbol

f_symbol(
    a: SUNSector,
    b: SUNSector,
    c: SUNSector,
    d: SUNSector,
    e: SUNSector,
    f: SUNSector,
) -> float

[F^{abc}_d]_{e,f}. Raises if any vertex has n_symbol > 1.

r_symbol

r_symbol(a: SUNSector, b: SUNSector, c: SUNSector) -> float

R^{ab}_c. Raises if N^c_ab > 1.

b_symbol

b_symbol(a: SUNSector, b: SUNSector, c: SUNSector) -> float

B^{ab}_c. Raises if any vertex has n_symbol > 1.

frobenius_schur

frobenius_schur(a: SUNSector) -> int

chi_a = sign([F^{a dual(a) a}_a]_{1,1}), the TensorKit definition.

twist

twist(a: SUNSector) -> int

theta_a = 1: SU(N) braiding is symmetric, so the twist is trivial.

z_matrix

z_matrix(a: SUNSector) -> ndarray

Z_a: V_a -> V_a^*, shape (d_a, d_dual(a)); read-only.

Derived from the CG singlet of V_a (x) V_dual(a), so it is in the same gauge as cgc by construction. Never the identity for d_a > 1, and its two axes index different sectors whenever dual(a) != a.

permute_tree

permute_tree(
    tree: FusionTree, perm: tuple[int, ...]
) -> tuple[tuple[FusionTree, complex], ...]

Artin-braid expansion; SU(N) is symmetric, so perm fixes the braid.

bend_right

bend_right(
    key: FusionBlockKey, *, dual: bool
) -> tuple[tuple[FusionBlockKey, complex], ...]

sqrt(qdim(c)/qdim(a))·B(a,b,c), expanded over the new vertex's multiplicity.

SUNSector dataclass

SUNSector(dynkin: tuple[int, ...])

Bases: Sector

An SU(N) irrep labelled by its Dynkin label (a_1, ..., a_{N-1}).

Parameters:

Name Type Description Default
dynkin tuple of int

The Dynkin label, one non-negative entry per node of the A_{N-1} diagram. A list is accepted and stored as a tuple, so a sector survives a JSON round trip through tenet.save / tenet.load unchanged.

required

Raises:

Type Description
TypeError

If dynkin is not a sequence of int entries (bool included).

ValueError

If dynkin is empty or carries a negative entry.

Examples:

>>> from tenet.symmetry.sun import SUNSector
>>> SUNSector((1, 0))       # the fundamental 3 of SU(3)
SUNSector(dynkin=(1, 0))

U1Provider dataclass

U1Provider(name: str = 'U1')

U(1): abelian, multiplicity-free, one-dimensional irreps.

Use the module-level singleton U1 rather than constructing one; name is an identity label that participates in equality, not a configuration knob. The capability contract of every method is documented once, on the protocols in tenet.symmetry — only behaviour that differs from a protocol carries a docstring here.

Examples:

>>> from tenet.symmetry import U1, U1Sector
>>> U1.fusion(U1Sector(-1), U1Sector(1))
(U1Sector(charge=0),)
>>> U1.n_symbol(U1Sector(1), U1Sector(1), U1Sector(2))
1
>>> U1.irrep_dim(U1Sector(5))
1

cgc

cgc(a: U1Sector, b: U1Sector, c: U1Sector) -> ndarray

Shape (1, 1, 1, 1), read-only. Raises on a fusion-forbidden triple.

permute_tree

permute_tree(
    tree: FusionTree, perm: tuple[int, ...]
) -> tuple[tuple[FusionTree, complex], ...]

One term, coefficient 1: charge addition is commutative and F = R = 1.

bend_right

bend_right(
    key: FusionBlockKey, *, dual: bool
) -> tuple[tuple[FusionBlockKey, complex], ...]

One term, coefficient 1: for an Abelian irrep B = N, dim = 1, FS = 1.

z_matrix

z_matrix(a: U1Sector) -> ndarray

Z = [[1]], read-only: V_q is one-dimensional and the FS phase is 1.

frobenius_schur

frobenius_schur(a: U1Sector) -> float

chi_a = 1: every U(1) irrep is one-dimensional with a real pairing.

twist

twist(a: U1Sector) -> float

theta_a = 1: bosonic Abelian, trivial twist.

U1Sector dataclass

U1Sector(charge: int)

Bases: Sector

A U(1) irrep, labelled by its integer charge.

Parameters:

Name Type Description Default
charge int

The conserved charge; negation is duality.

required

Examples:

>>> from tenet.symmetry import U1, U1Sector
>>> U1.fusion(U1Sector(1), U1Sector(2))
(U1Sector(charge=3),)
>>> U1.dual(U1Sector(1))
U1Sector(charge=-1)

Z2Provider dataclass

Z2Provider(name: str = 'Z2')

Z2 with bosonic braiding: Vect_Z2. Abelian, multiplicity-free, d = 1, R = +1.

Use the module-level singleton Z2 rather than constructing one; name is an identity label that participates in equality, not a configuration knob. The capability contract of every method is documented once, on the protocols in tenet.symmetry — only behaviour that differs from a protocol carries a docstring here.

Examples:

>>> from tenet.symmetry import Z2, Z2Sector
>>> Z2.fusion(Z2Sector(1), Z2Sector(1))
(Z2Sector(parity=0),)
>>> int(Z2.twist(Z2Sector(1)))  # bosonic: no sign, unlike fermion parity
1

dual

dual(a: Z2Sector) -> Z2Sector

Both sectors are self-dual (dual(q) = q).

cgc

cgc(a: Z2Sector, b: Z2Sector, c: Z2Sector) -> ndarray

Shape (1, 1, 1, 1), read-only. Raises on a fusion-forbidden triple.

permute_tree

permute_tree(
    tree: FusionTree, perm: tuple[int, ...]
) -> tuple[tuple[FusionTree, complex], ...]

One term, coefficient 1 — the braiding is symmetric and F = R = +1.

This is the one method that separates this provider from the fermionic one in fz2.py, which returns the same tree with a Koszul sign. See the module docstring: the permission to delegate is the braiding, never the uniqueness of the fusion.

bend_right

bend_right(
    key: FusionBlockKey, *, dual: bool
) -> tuple[tuple[FusionBlockKey, complex], ...]

One term, coefficient 1: for an Abelian irrep B = N, dim = 1, FS = 1.

z_matrix

z_matrix(a: Z2Sector) -> ndarray

Z = [[1]], read-only: V_q is one-dimensional and the FS phase is 1.

frobenius_schur

frobenius_schur(a: Z2Sector) -> float

chi_a = 1: every Z2 irrep is one-dimensional with a real pairing.

twist

twist(a: Z2Sector) -> float

theta_a = 1: bosonic Abelian, trivial twist.

Z2Sector dataclass

Z2Sector(parity: int)

Bases: Sector

A Z2 charge: parity in {0, 1}. 0 is even (the unit), 1 is odd.

Parameters:

Name Type Description Default
parity int

0 or 1. A bool is refused even though it is an int subclass, so Z2Sector(True) != Z2Sector(1) can never become a live question.

required

Raises:

Type Description
TypeError

If parity is not an int (bool included).

ValueError

If parity is not 0 or 1.

bend_braided

bend_braided(
    provider: _TreeBender,
    key: FusionBlockKey,
    *,
    right: bool,
    dual: bool,
) -> tuple[tuple[FusionBlockKey, complex], ...]

bend_right/bend_left for any provider supplying B, FS and qdim.

Parameters:

Name Type Description Default
provider FusionRules

A provider with DualityData, FSIndicatorData, QuantumDimensionData and PivotalData (the _TreeBender annotation — enforced by the type checker, not a runtime gate; see the comment in the body).

required
key FusionBlockKey

The block key one line is moved across.

required
right bool

True moves the output tree's last line onto the input tree (bend_right); False is the inverse direction.

required
dual bool

The moved leg's current dual flag; it keys the Frobenius-Schur factor.

required

Returns:

Type Description
tuple of (FusionBlockKey, complex)

One term for a multiplicity-free provider; genuinely multi-term when the provider supplies BMatrixData.

Raises:

Type Description
ValueError

If the source tree is empty (rank 0), so there is no line to bend.

CapabilityError

If the bent vertex has n_symbol > 1 and the provider does not supply BMatrixData.

Notes

The source tree's last vertex is a x b -> c (a the unit when the source has rank 1). The source loses b and re-couples to a; the destination gains dual(b) at its end and couples to a too. Both spines are recomputed from the new uncoupled tuples — the source spine is truncated and the destination's old coupled sector becomes its new last inner line — never relabelled.

The coefficient is TensorKit's bendright::

coeff = sqrt(qdim(c) / qdim(a)) · B(a, b, c)          (× conj(chi_dual(b))
                                                       if the moved leg
                                                       was already dual)

and bend_left is bendleft: the same expression read off the input tree, then conjugated — which is what makes it the exact inverse of bend_right rather than a reciprocal guess (a unitary's inverse is its conjugate transpose, and multiplicity-free bending is one entry per key). Note the Frobenius-Schur factor is keyed on the moved leg's current flag, so it appears once across a round trip, never twice and never zero times.

Single-term because the provider is multiplicity-free: one key in, one key out. n_symbol > 1 needs matrix-valued coefficients, which a provider supplies by also implementing BMatrixData; then the destination's new vertex label is expanded over B's second axis instead of being pinned to 0, and the result is genuinely multi-term. Without that capability the existing CapabilityError still fires.

bend_unique

bend_unique(
    provider: _DualFusionRules,
    key: FusionBlockKey,
    *,
    right: bool,
    dual: bool,
) -> tuple[tuple[FusionBlockKey, complex], ...]

bend_right/bend_left for providers whose fusion is unique and B is 1.

Parameters:

Name Type Description Default
provider FusionRules

The provider whose keys are bent; it must have opted in by defining bend_right/bend_left in terms of this helper.

required
key FusionBlockKey

The block key one line is moved across.

required
right bool

True moves the output tree's last line onto the input tree (bend_right); False is the inverse direction.

required
dual bool

The moved leg's current dual flag. Accepted and provably ignored here — see Notes.

required

Returns:

Type Description
tuple of (FusionBlockKey, complex)

Exactly one term: the recomputed key with coefficient 1.0.

Raises:

Type Description
ValueError

If the source tree is empty (rank 0), so there is no line to bend.

CapabilityError

If dropping the moved line leaves uncoupled labels with anything other than exactly one coupled sector, or more than one tree — this helper is only correct for unique fusion.

Notes

Shared by Trivial and U(1), exactly as permute_unique_tree is. The source tree loses its last uncoupled line, the destination tree gains dual of it at the end, and the new coupled sector is the source tree's last inner line (the unit when the source had rank 1). Both spines are recomputed from the new uncoupled tuples, never relabelled.

The coefficient is sqrt(dim(c)/dim(a)) · B(a,b,c), times the conjugate Frobenius-Schur phase of dual(b) when the moved line is already dual. For every Abelian irrep all three factors are exactly 1 (all dim == 1, B == N ∈ {0,1}, frobenius_schur_phase == 1), and Trivial reaches the same value through F ≡ 1. So dual is accepted and provably ignored here; a provider whose FS phase is not 1 must not route through this helper.

Not a capability check: a provider opts in by defining bend_right / bend_left. Uniqueness of fusion alone is never permission (a fermionic parity provider has unique fusion and a non-trivial bend).

permute_braided_tree cached

permute_braided_tree(
    provider: _TreeBraider,
    tree: FusionTree,
    perm: tuple[int, ...],
) -> tuple[tuple[FusionTree, complex], ...]

permute_tree for any provider supplying F- and R-symbols.

Parameters:

Name Type Description Default
provider FusionRules

A provider with AssociatorData and BraidingData (checked with requires on entry).

required
tree FusionTree

The left-associated tree whose uncoupled lines are permuted.

required
perm tuple of int

perm[j] is the OLD uncoupled position that becomes position j.

required

Returns:

Type Description
tuple of (FusionTree, complex)

The accumulated {tree: coeff} expansion of the permuted tree.

Raises:

Type Description
CapabilityError

If the provider lacks FusionRules, AssociatorData or BraidingData.

Notes

Bubble-decomposes perm into adjacent transpositions (Artin generators) and applies _artin_braid to each, accumulating a {tree: coeff} expansion.

Symmetric-category only: the caller gets no over/under choice, because R == R**-1 for the providers this serves (invariant 12). A provider whose braiding is genuinely chiral must not reuse this helper — it needs levels and an explicit braid API.

The coefficients are conjugated nowhere, which is correct exactly while the provider's gauge is real: TensorKit braids the domain-side (fusion) tree with the conjugate coefficient, and for a complex-gauge provider the caller must conjugate on the domain side. SU(2) in the racah/TensorKitSectors gauge has real F and R, so the two agree and permutation_plan may treat the two trees of a block key symmetrically.

No tolerance-based pruning: a structurally forbidden term is already exactly 0.0 through f_symbol, and the surviving float residues from the sum over d cost plan size, never correctness.

permute_unique_tree

permute_unique_tree(
    provider: FusionRules,
    tree: FusionTree,
    perm: tuple[int, ...],
) -> tuple[tuple[FusionTree, complex], ...]

permute_tree for providers whose fusion is unique and whose F/R are 1.

Parameters:

Name Type Description Default
provider FusionRules

The provider whose trees are permuted; it must have opted in by defining permute_tree in terms of this helper.

required
tree FusionTree

The left-associated tree whose uncoupled lines are permuted.

required
perm tuple of int

perm[j] is the old uncoupled position that becomes position j.

required

Returns:

Type Description
tuple of (FusionTree, complex)

Exactly one term: the recomputed tree with coefficient 1.0.

Raises:

Type Description
CapabilityError

If the permuted uncoupled labels admit anything other than exactly one tree coupling to the same sector — this helper is only correct for unique fusion.

Notes

Shared by Trivial and U(1): permuting the uncoupled labels leaves exactly one left-associated tree with the same coupled sector, so the whole expansion is that tree with coefficient 1. The spine is recomputed rather than permuted, which is what makes this correct rather than a relabelling.

Not a capability check: a provider must opt in by defining permute_tree (fermionic parity also has unique multiplicity-free fusion and still carries a sign, so uniqueness alone must never be taken as permission).

requires

requires(provider: object, capability: type) -> None

Raise CapabilityError unless provider implements capability.

Parameters:

Name Type Description Default
provider object

The provider gated, usually read off a leg's space.

required
capability type

One of the runtime_checkable capability protocols of tenet.symmetry.

required

Raises:

Type Description
CapabilityError

If provider does not implement capability, naming both.

Examples:

>>> from tenet.symmetry import U1, Z2, ClebschGordanData, BranchingRules, requires
>>> requires(U1, ClebschGordanData)  # U(1) has CG tensors: no raise
>>> requires(Z2, BranchingRules)
Traceback (most recent call last):
    ...
tenet.symmetry.base.CapabilityError: Z2Provider does not provide capability BranchingRules

supports

supports(provider: object, capability: type) -> bool

True iff provider implements capabilityrequires' non-raising sibling, so the capability graph is queryable without try/except CapabilityError.

Parameters:

Name Type Description Default
provider object

The provider queried.

required
capability type

One of the runtime_checkable capability protocols of tenet.symmetry.

required

Returns:

Type Description
bool

Whether provider implements capability.

Examples:

>>> from tenet.symmetry import U1, Z2, ClebschGordanData, BranchingRules, supports
>>> supports(U1, ClebschGordanData)
True
>>> supports(Z2, BranchingRules)
False