Core – Symmetries, Indices, and Tensors¶
Symmetry¶
Symmetry group definitions for symmetric tensor networks.
This module provides the mathematical foundation for symmetric tensors. All symmetry classes operate on numpy integer arrays of charge values. No JAX dependency — pure Python/numpy arithmetic.
- class tenax.core.symmetry.BraidingStyle(*values)[source]¶
Bases:
EnumClassification of particle exchange statistics.
BOSONIC: Trivial exchange (sign = +1 always). FERMIONIC: Anti-commuting exchange (sign = (-1)^{p_a * p_b}). ANYONIC: General phase on exchange (reserved for future use).
- BOSONIC = 'bosonic'¶
- FERMIONIC = 'fermionic'¶
- ANYONIC = 'anyonic'¶
- class tenax.core.symmetry.BaseSymmetry[source]¶
Bases:
ABCAbstract base for symmetry groups governing tensor index charges.
A symmetry group defines how charges combine (fuse) when tensor legs are merged, and how charges transform when a leg’s flow is reversed (dual).
Concrete subclasses must implement fuse, dual, identity, and n_values. All concrete classes should implement __eq__ and __hash__ so they can serve as dict keys and be compared for compatibility checks.
- abstractmethod fuse(charges_a, charges_b)[source]¶
Fuse two charge arrays element-wise under group multiplication.
- abstractmethod dual(charges)[source]¶
Return the group inverse (dual) of each charge.
Used to flip a leg’s flow direction while preserving compatibility.
- abstractmethod identity()[source]¶
Return the identity element (neutral charge, typically 0).
- Return type:
- abstractmethod n_values()[source]¶
Return the number of distinct charge values, or None if infinite (U(1)).
- property braiding_style: BraidingStyle¶
Exchange statistics of this symmetry (bosonic by default).
- parity(charges)[source]¶
Return the Z2 parity grading of each charge (0=even, 1=odd).
For bosonic symmetries, all charges have even parity.
- exchange_sign(charge_a, charge_b)[source]¶
Sign from exchanging two particles with given charges.
Returns (-1)^{p_a * p_b} where p is the parity grading. For bosonic symmetries, always returns +1.
- exchange_phase(charge_a, charge_b)[source]¶
Phase from exchanging two particles (generalizes exchange_sign).
For fermionic symmetries this equals exchange_sign. For anyonic symmetries this can be a general complex phase.
- twist_phase(charge)[source]¶
Topological twist (ribbon element) for a charge sector.
For fermions: (-1)^p where p is the parity of the charge. For bosons: always 1.
- class tenax.core.symmetry.U1Symmetry[source]¶
Bases:
BaseSymmetryU(1) symmetry: integer charges, fusion by addition.
Represents the continuous U(1) group (particle number conservation, total Sz conservation, etc.). Charges are unbounded integers.
Example
>>> sym = U1Symmetry() >>> sym.fuse(np.array([0, 1, -1]), np.array([1, -1, 0])) array([1, 0, -1])
- class tenax.core.symmetry.ZnSymmetry(n)[source]¶
Bases:
BaseSymmetryZ_n symmetry: integer charges mod n, fusion by addition mod n.
Represents the discrete cyclic group Z_n (e.g., Z_2 for fermion parity, Z_3 for three-state Potts model, etc.).
- Parameters:
n (
int) – The order of the group. Must be >= 2.
Example
>>> sym = ZnSymmetry(3) >>> sym.fuse(np.array([1, 2]), np.array([2, 2])) array([0, 1])
- class tenax.core.symmetry.BaseNonAbelianSymmetry[source]¶
Bases:
BaseSymmetryStub base class for non-Abelian symmetries (e.g., SU(2)).
Non-Abelian symmetries require Clebsch-Gordan / recoupling coefficients for tensor contractions, making them significantly more complex than Abelian symmetries. This base provides the interface contract for future concrete implementations.
Note
Concrete subclasses must implement recoupling_coefficients and allowed_fusions in addition to the BaseSymmetry abstract methods. The fuse/dual/identity/n_values methods for non-Abelian groups operate on irrep labels (e.g., spin quantum numbers).
- abstractmethod recoupling_coefficients(j1, j2, j3)[source]¶
Return Clebsch-Gordan or 6j-symbol coefficients for recoupling.
These coefficients are needed when contracting non-Abelian symmetric tensors. The specific form depends on the group (CG coefficients for SU(2), etc.).
- class tenax.core.symmetry.FermionParity[source]¶
Bases:
BaseSymmetryZ2 symmetry with fermionic braiding statistics.
Charges are 0 (even/bosonic) or 1 (odd/fermionic). Fusion is addition mod 2. Exchanging two odd-parity objects yields a minus sign.
Example
>>> sym = FermionParity() >>> sym.exchange_sign(1, 1) -1 >>> sym.exchange_sign(0, 1) 1
- property braiding_style: BraidingStyle¶
Exchange statistics of this symmetry (bosonic by default).
- dual(charges)[source]¶
Return the group inverse (dual) of each charge.
Used to flip a leg’s flow direction while preserving compatibility.
- n_values()[source]¶
Return the number of distinct charge values, or None if infinite (U(1)).
- Return type:
- parity(charges)[source]¶
Return the Z2 parity grading of each charge (0=even, 1=odd).
For bosonic symmetries, all charges have even parity.
- exchange_sign(charge_a, charge_b)[source]¶
Sign from exchanging two particles with given charges.
Returns (-1)^{p_a * p_b} where p is the parity grading. For bosonic symmetries, always returns +1.
- class tenax.core.symmetry.FermionicU1(grading=None, grading_key='abs_mod_2')[source]¶
Bases:
BaseSymmetryU(1) symmetry with fermionic exchange statistics.
Charges are unbounded integers (like U1Symmetry), but a grading function maps each charge to a Z2 parity (0=even, 1=odd).
- Parameters:
Example
>>> sym = FermionicU1() >>> sym.parity(np.array([0, 1, 2, 3])) array([0, 1, 0, 1])
- property braiding_style: BraidingStyle¶
Exchange statistics of this symmetry (bosonic by default).
- dual(charges)[source]¶
Return the group inverse (dual) of each charge.
Used to flip a leg’s flow direction while preserving compatibility.
- n_values()[source]¶
Return the number of distinct charge values, or None if infinite (U(1)).
- Return type:
- parity(charges)[source]¶
Return the Z2 parity grading of each charge (0=even, 1=odd).
For bosonic symmetries, all charges have even parity.
- exchange_sign(charge_a, charge_b)[source]¶
Sign from exchanging two particles with given charges.
Returns (-1)^{p_a * p_b} where p is the parity grading. For bosonic symmetries, always returns +1.
- class tenax.core.symmetry.ProductSymmetry(sym1, sym2)[source]¶
Bases:
BaseSymmetryDirect product of two symmetries with bit-packed charges.
Charges from the two factor symmetries are encoded into a single int32 via bit-packing:
encoded = (q2 << 16) | (q1 & 0xFFFF). Component charges are limited to the int16 range [-32768, 32767].- Parameters:
sym1 (
BaseSymmetry) – First factor symmetry.sym2 (
BaseSymmetry) – Second factor symmetry.
- Raises:
TypeError – If either factor is itself a ProductSymmetry (no nesting).
Example
>>> sym = ProductSymmetry(FermionParity(), U1Symmetry()) >>> encoded = ProductSymmetry.encode(1, 3) >>> ProductSymmetry.decode(encoded) (1, 3)
- dual(charges)[source]¶
Return the group inverse (dual) of each charge.
Used to flip a leg’s flow direction while preserving compatibility.
- property braiding_style: BraidingStyle¶
Exchange statistics of this symmetry (bosonic by default).
- parity(charges)[source]¶
Return the Z2 parity grading of each charge (0=even, 1=odd).
For bosonic symmetries, all charges have even parity.
- exchange_sign(charge_a, charge_b)[source]¶
Sign from exchanging two particles with given charges.
Returns (-1)^{p_a * p_b} where p is the parity grading. For bosonic symmetries, always returns +1.
Index¶
- class tenax.core.index.FlowDirection(*values)[source]¶
Bases:
IntEnumFlow direction of a tensor leg.
- IN (+1): Incoming leg — corresponds to a “ket” index, arrow pointing
into the tensor. Positive charge flows in.
- OUT (-1): Outgoing leg — corresponds to a “bra” index, arrow pointing
out of the tensor. Positive charge flows out.
Conservation law: sum_i(flow_i * charge_i) == symmetry.identity() for any valid block of a symmetric tensor.
- IN = 1¶
- OUT = -1¶
- class tenax.core.index.TensorIndex(symmetry, charges, flow, label='')[source]
Bases:
objectMetadata for one leg (index) of a symmetric tensor.
TensorIndex is frozen and slots-based for memory efficiency — large networks create millions of these objects.
- Parameters:
symmetry (BaseSymmetry)
charges (ndarray)
flow (FlowDirection)
- symmetry
The symmetry group governing charges on this leg.
- charges
1-D numpy int32 array of length D (bond dimension). charges[i] is the charge of basis state i.
- flow
Whether this leg is incoming (IN) or outgoing (OUT).
- label
Human-readable or integer identifier for this leg. Shared labels across tensors drive automatic contraction.
Example
>>> u1 = U1Symmetry() >>> idx = TensorIndex(u1, np.array([-1, 0, 1], dtype=np.int32), FlowDirection.IN, label="left") >>> idx.dim 3 >>> idx.dual().flow <FlowDirection.OUT: -1>
- symmetry: BaseSymmetry
- charges: ndarray
- flow: FlowDirection
- property dim: int
Bond dimension of this leg (number of basis states).
- dual()[source]
Return a new TensorIndex with flipped flow and dual charges.
Used when reversing a leg direction. The dual of an IN leg is an OUT leg with negated (for U(1)) or modular-negated (for Zn) charges.
- Return type:
TensorIndex- Returns:
New TensorIndex with opposite flow and dual charges.
- relabel(new_label)[source]
Return a new TensorIndex with a different label, otherwise identical.
- is_dual_of(other)[source]
Check if this index is the exact dual of other.
Strict check: requires opposite flows AND charge arrays that are dual of each other. Used to validate that two legs can be contracted while preserving exact charge conservation.
- Parameters:
other (
TensorIndex) – Another TensorIndex to compare against.- Return type:
- Returns:
True if self and other are exact duals.
- compatible_with(other)[source]
Check if this index can be connected to other in a network.
Looser than is_dual_of: requires same symmetry type, same bond dimension, and opposite flows. Does not require exact charge matching (useful for connecting tensors where charges may differ by relabeling).
- Parameters:
other (
TensorIndex) – Another TensorIndex to check compatibility with.- Return type:
- Returns:
True if the two indices can be connected.
Tensor¶
- class tenax.core.tensor.DenseTensor(data, indices)[source]¶
Bases:
TensorA tensor stored as a plain JAX array with index metadata.
Used when no symmetry structure is exploited. Full compatibility with jax.jit, jax.vmap, jax.grad via pytree registration.
- Pytree structure:
Leaves: (data_array,) Aux data: indices tuple (static, not traced by JAX)
- Parameters:
Example
>>> data = jnp.ones((2, 3)) >>> t = DenseTensor(data, (idx_a, idx_b)) >>> t.norm() DeviceArray(2.4494898, dtype=float32)
- transpose(axes)[source]¶
Permute tensor legs.
- Parameters:
- Return type:
- Returns:
New DenseTensor with permuted data and reordered indices.
- relabels(mapping)[source]¶
Return a copy with multiple leg labels renamed at once.
- class tenax.core.tensor.SymmetricTensor(blocks, indices)[source]¶
Bases:
TensorBlock-sparse tensor storing only symmetry-allowed charge sectors.
Storage model:
_blocks:dict[BlockKey, jax.Array]– Key is a tuple of one representative charge per leg. Value is a JAX array of shape(n_states_leg0, ..., n_states_legN)for that charge sector._indices:tuple[TensorIndex, ...]– Full index metadata per leg.
Conservation law enforced on all stored blocks:
sum_i(flow_i * charge_i) == symmetry.identity()
- Pytree structure:
Leaves: list of block arrays [blocks[k] for k in sorted_keys] Aux data: (sorted_keys, indices) — static, not traced by JAX
- Note on JAX JIT compatibility:
Block structure (keys) is static Python data. jax.jit recompiles only when the set of keys changes (i.e., when bond dimension changes after SVD truncation). Within a DMRG sweep at fixed bond dim, no recompilation occurs.
- Parameters:
Example
>>> t = SymmetricTensor.zeros(indices=(idx_in, idx_out)) >>> t.n_blocks 3 # one block per unique charge value
- classmethod zeros(indices, dtype=<class 'jax.numpy.float64'>)[source]¶
Create a zero tensor with all valid charge sectors initialized to zero.
- Parameters:
- Return type:
- Returns:
SymmetricTensor with all valid blocks set to zero.
- classmethod random_normal(indices, key, dtype=<class 'jax.numpy.float64'>, stddev=1.0)[source]¶
Create a random tensor with blocks drawn from N(0, stddev).
Splits the JAX random key sequentially over blocks.
- Parameters:
- Return type:
- Returns:
SymmetricTensor with random entries in all valid blocks.
- classmethod from_dense(data, indices, tol=1e-12)[source]¶
Extract block-sparse structure from a dense JAX array.
Elements outside valid charge sectors must be zero (within tol) or a ValueError is raised.
- Parameters:
- Return type:
- Returns:
SymmetricTensor with blocks extracted from data.
- Raises:
ValueError – If data has non-zero elements outside valid sectors.
- todense()[source]¶
Materialize the full dense tensor (for testing/debugging only).
Warning: Creates an array of full size; avoid for large tensors.
- Return type:
- Returns:
Dense JAX array of shape tuple(idx.dim for idx in indices).
- dagger()[source]¶
Conjugate transpose with fermionic twist phases.
For each block, applies complex conjugation, reverses all leg flows (via dual), and multiplies by the product of twist phases for all charges in the block key. For bosonic symmetries this is equivalent to
conj()with dualled indices.- Return type:
- Returns:
New SymmetricTensor with conjugated data, dual indices, and twist phase corrections.
- transpose(axes)[source]¶
Permute tensor legs.
For fermionic symmetries, each block acquires a Koszul sign determined by the charges’ parities and the permutation.
- Parameters:
- Return type:
- Returns:
New SymmetricTensor with permuted blocks and reordered indices.
- relabels(mapping)[source]¶
Return a copy with multiple leg labels renamed at once.