Heisenberg, SU(2)¶
The same Hamiltonian as Heisenberg, U(1),
declared under its full SU(2) symmetry instead of the U(1) subgroup it contains.
What changes. \(S^+\), \(S^-\) and \(S^z\) do not appear: none of them is an SU(2)-invariant
tensor, and the rank-3 charge-leg form has no leg to emit a spin-1 sector onto. What
exists is the invariant two-site operator \(\mathbf{S}\cdot\mathbf{S}\), one whole bond term
— which is exactly what tenet.models.heisenberg(N, SU2) hands MPO.from_terms, and the
builder splits it with an SVD — so the MPO bond grading comes out of the
operator's own blocks and no coupling tree is named anywhere.
What it buys. A U(1) bond stores \(\chi\) states; an SU(2) bond stores multiplets, and a \(j\) multiplet is worth \(2j+1\) dense states at the cost of one degeneracy index. The Wigner-Eckart theorem is what makes this exact rather than an approximation: within a multiplet the coefficients are fixed by the symmetry and only the reduced matrix element is data.
Checks.
- Both runs reach the same energy to \(10^{-10}\) — the same physical state, two declarations.
- The SU(2) mid-bond holds 22 multiplets spanning 62 dense states where the U(1) mid-bond
holds 64 states, and
reduced_dim < dimis asserted rather than printed.
Explained in the SU(2) tutorial; what max_bond bounds on a
multiplet bond is Truncation.
Source¶
"""The same Heisenberg chain under SU(2): one invariant term, multiplet compression.
Run it standalone::
uv run python examples/su2_heisenberg.py
Under SU(2), ``S^z`` alone is symmetry-forbidden -- only the invariant two-site
``S . S`` is a term, and it is the *entire* operator set
:func:`tenet.models.spin_half` returns for this grading -- which is what
:func:`tenet.models.heisenberg` hands ``MPO.from_terms`` under this symmetry. The
builder splits that term, deriving the graded MPO bond from the operator's own blocks.
The seed is :meth:`MPS.random`: ``MPS.product`` refuses non-Abelian symmetries by
construction (a single spin-up is not an SU(2) multiplet).
The point is the printed table -- same energy as the U(1) run this file computes by
importing ``examples/heisenberg.py``, from a mid-chain bond of far fewer multiplets
than the dense states ``chi`` counts.
"""
import heisenberg
from tenet import GradedSpace
from tenet.models import heisenberg as heisenberg_mpo
from tenet.models import spin_half
from tenet.network import MPS, dmrg_
from tenet.symmetry import SU2, SU2Sector
SITE = spin_half(SU2) # one spin-1/2 multiplet, dense dim 2
PHYS = SITE.phys
def bond_spaces(n_sites: int) -> list[GradedSpace]:
"""A small full-rank-enough seed: singlet boundaries, {j=0,1/2,1} in the middle."""
# SU2Sector holds 2j, so 0 is the singlet: a one-dimensional trivial space at each
# end is the open boundary, and it pins the whole chain to a total spin zero state.
tri = GradedSpace.new(SU2, {SU2Sector(0): 1})
# Interior bonds offer j = 0, 1/2, 1 with a couple of copies each. Only the *set* of
# irreps has to be right -- multiplicities the ground state needs grow under the
# sweeps, and ones it does not are truncated away -- so this stays deliberately small.
mid = GradedSpace.new(SU2, {SU2Sector(0): 2, SU2Sector(1): 2, SU2Sector(2): 1})
return [tri] + [mid] * (n_sites - 1) + [tri]
def main(n_sites: int = 20, chi: int = 64):
"""Run SU(2) and U(1) side by side; returns both DMRG_outs and the SU(2) mid bond."""
# A random seed already fills every offered sector, so no noise is needed to reach
# them; the fixed seed only makes the printed sweep count reproducible.
psi = MPS.random(PHYS, bond_spaces(n_sites), seed=0)
# Flat chi, no schedule: chi counts dense states here too, so an SU(2) bond keeps
# (2j+1)-fold more physical states per unit of cost than the U(1) run below.
# ``models.heisenberg(n, SU2)`` is one invariant two-site term per bond: the site pair
# (i, i+1) enters as a tuple, not two one-site factors, because S.S is irreducible
# under SU(2) and from_terms splits it across the bond itself by fusing the two
# sites and cutting the result -- the j=0 and j=1 channels of that split are the
# MPO bond, and they are what the SU(2) grading is made of.
su2 = dmrg_(psi, heisenberg_mpo(n_sites, SU2), chi=chi)
u1, _ = heisenberg.main(n_sites, chi)
# Same cut, both runs. dim counts dense states, reduced_dim counts multiplets: the
# gap between them is the (2j+1) degeneracy SU(2) never has to store.
mid = su2.psi[n_sites // 2].legs[0].space
mid_u1 = u1.psi[n_sites // 2].legs[0].space
print(f"U(1) : ~{u1.sweeps} sweeps E = {u1.energy:.12f} mid bond {mid_u1.dim} states")
print(
f"SU(2): ~{su2.sweeps} sweeps E = {su2.energy:.12f} "
f"mid bond {mid.reduced_dim} multiplets, {mid.dim} dense"
)
print(f"|E_su2 - E_u1| = {abs(su2.energy - u1.energy):.1e}")
return su2, u1, mid
if __name__ == "__main__":
main()
Output¶
Produced by su2_heisenberg.main() at its defaults — exactly
python examples/su2_heisenberg.py — as run by tests/test_examples.py.
N=20 ~9 sweeps E = -8.682473334397722 mid bond: 64 states
bond energies: -0.6534 -0.2943 -0.5664 -0.3370 -0.5401 -0.3540 -0.5286 -0.3616 -0.5239 -0.3638 -0.5239 -0.3616 -0.5286 -0.3540 -0.5401 -0.3370 -0.5664 -0.2943 -0.6534
sum of bond energies = -8.682473334397697 vs out.energy = -8.682473334397722
max_n |<S^z_n>| = 3.9e-13
U(1) : ~9 sweeps E = -8.682473334398 mid bond 64 states
SU(2): ~5 sweeps E = -8.682473334397 mid bond 22 multiplets, 62 dense
|E_su2 - E_u1| = 4.5e-13