Skip to content

tenet.models

The standard local sites and the named Hamiltonians on them. See the guide, Building a Hamiltonian.

function what it builds
heisenberg the XXX chain, U1 or SU2
xxz the anisotropic chain, U1
transverse_field_ising the Ising chain in a field, Z2
hubbard the spinful Hubbard chain, fZ2
spinless_tv spinless fermions with nearest-neighbour repulsion, fZ2
sun_heisenberg the SU(\(N\)) fundamental exchange chain
sun_exchange that chain's bond term, on its own

tenet.models

tenet.models: the standard sites and the named Hamiltonians on them.

An optional layer above the core, imported explicitly (from tenet.models import spin_half) and re-exported nowhere: tenet.network never imports it, which is what keeps the driver layer from deciding anything about what a caller's operators mean. tests/network/test_hygiene.py enforces that edge.

The sites are spin_half, spinless_fermion, spinful_fermion and hard_core_boson, each returning a Site -- a physical GradedSpace, a name-to-operator mapping in the shape MPO.from_arrays calls ops, and the dense matrices behind them.

The named Hamiltonians are one function per model, each returning an MPO over an open chain and taking the symmetry as a parameter: heisenberg, xxz, transverse_field_ising, hubbard, spinless_tv and sun_heisenberg (whose bond term, sun_exchange, is public too, since measuring it is how a bulk energy density is read). There is no lattice geometry and no parameter sweep: what a function does not name -- a next-nearest-neighbour coupling, a ladder, a chemical potential -- stays the caller's term list, and every one of these models is a short one.

Examples:

>>> from tenet.models import heisenberg, spin_half
>>> from tenet.network import MPO
>>> heisenberg(4).to_dense().shape       # the chain in one call
(16, 16)
>>> site = spin_half()                   # or the site, and the terms are yours
>>> n = 4
>>> blocks = [
...     ("Sz Sz", [(i, i + 1) for i in range(n - 1)], [1.0] * (n - 1)),
...     ("S+ S-", [(i, i + 1) for i in range(n - 1)], [0.5] * (n - 1)),
...     ("S- S+", [(i, i + 1) for i in range(n - 1)], [0.5] * (n - 1)),
... ]
>>> h = MPO.from_arrays(n, site.ops, blocks)
>>> h.to_dense().shape
(16, 16)

The build carries no keyword: a builder hands back the site tensors, and a finite-range model's MPO bond is narrow enough that Env.heff2's prepared, symbolic path costs more per sweep than it returns, so the sites are what the sweep should run on. An ab initio Hamiltonian writes symbolic=True and keeps the description.

Site dataclass

Site(
    phys: GradedSpace,
    ops: Mapping[str, SymmetricTensor],
    matrices: Mapping[str, ndarray],
)

One lattice site: its physical space, its term operators, and their matrices.

Parameters:

Name Type Description Default
phys GradedSpace

The physical space. Every operator in ops lives on it, and it is what MPS.product / MPS.random take.

required
ops Mapping[str, SymmetricTensor]

Name to term operator, in whichever of local_op's two forms the grading admits: rank 3 with a charge leg where the symmetry is Abelian, rank 2k invariant where it is not (k = 1 included: a one-site term is invariant too). Where it is rank 3 this mapping is exactly what MPO.from_arrays calls ops; where it is not, from_arrays cannot express the term at all and MPO.from_terms is the door.

required
matrices Mapping[str, ndarray]

The dense matrix behind each operator, under the same key, plus any matrix the site knows whose grading admits no entry in ops (the spin-1/2 S.S under U1). It is here for the forms the term API does not build -- rank-2 measurement operators for expectation_1site, invariant k-site ones for expectation_2site, and dense oracles -- so that reaching for one is not a reason to write a spin matrix out again.

required

Examples:

>>> from tenet.models import spin_half
>>> site = spin_half()
>>> site.phys.dim
2
>>> sorted(site.ops)
['S+', 'S-', 'Sz']
>>> site.ops["S+"].ndim  # the charge-leg form
3
>>> sorted(site.matrices)  # S.S has no rank-3 form, so it is a matrix here
['S+', 'S-', 'S.S', 'Sz']

heisenberg

heisenberg(
    n: int,
    symmetry: _DualFusionRules = U1,
    *,
    J: float = 1.0,
    spin: float = 0.5,
    symbolic: bool = False,
) -> MPO

The XXX chain \(H = J \sum_i \vec{S}_i \cdot \vec{S}_{i+1}\), open boundaries.

Parameters:

Name Type Description Default
n int

Chain length.

required
symmetry FusionRules

U1 (the default), the charge \(2S^z\), where the term list is \(S^z S^z + \frac{1}{2}(S^+ S^- + S^- S^+)\) over the rank-3 operators of spin_half; or SU2, where the whole bond term is that site's single invariant operator S.S. Any other provider is refused.

U1
J float

The coupling. Positive is antiferromagnetic. Default 1.0. Keyword-only.

1.0
spin float

The site spin. Only 0.5 is shipped. Default 0.5. Keyword-only.

0.5
symbolic bool

Passed to MPO.from_terms. Default False. Keyword-only.

False

Returns:

Type Description
MPO

The Hamiltonian on n sites.

Raises:

Type Description
ValueError

If spin is not 0.5, or if symmetry is neither U1 nor SU2.

Examples:

>>> from tenet.models import heisenberg
>>> from tenet.symmetry import SU2
>>> heisenberg(4).to_dense().shape
(16, 16)
>>> len(heisenberg(4, SU2))
4

hubbard

hubbard(
    n: int,
    *,
    t: float = 1.0,
    U: float = 4.0,
    symbolic: bool = False,
) -> MPO

\(H = -t \sum_{i\sigma} (c^\dagger_{i\sigma} c_{i+1\sigma} + h.c.) + U \sum_i n_{i\uparrow} n_{i\downarrow}\).

On spinful_fermion, graded by fZ2: there is no Jordan-Wigner operator in the terms, because the string is the braiding an odd MPO bond pays when it crosses a physical line. Both hopping directions are written out -- \(c^\dagger_i c_{i+1}\) and \(c^\dagger_{i+1} c_i\), spin by spin, up before down -- and the on-site repulsion uses the site's pre-multiplied n_up n_dn.

Parameters:

Name Type Description Default
n int

Chain length.

required
t float

The hopping. It enters as \(-t\), so t > 0 is the usual sign. Default 1.0. Keyword-only.

1.0
U float

The on-site repulsion, positive for repulsive. Default 4.0. Keyword-only.

4.0
symbolic bool

Passed to MPO.from_terms. Default False. Keyword-only.

False

Returns:

Type Description
MPO

The Hamiltonian on n sites, graded by fZ2.

Examples:

>>> from tenet.models import hubbard
>>> hubbard(3, U=8.0).to_dense().shape
(64, 64)

spinless_tv

spinless_tv(
    n: int,
    *,
    t: float = 1.0,
    V: float = 1.0,
    symbolic: bool = False,
) -> MPO

\(H = -t \sum_i (c^\dagger_i c_{i+1} + h.c.) + V \sum_i n_i n_{i+1}\).

On spinless_fermion, graded by fZ2, with the same convention as hubbard: both hopping directions written out, and no Jordan-Wigner operator in the terms.

Parameters:

Name Type Description Default
n int

Chain length.

required
t float

The hopping, entering as \(-t\). Default 1.0. Keyword-only.

1.0
V float

The nearest-neighbour repulsion, positive for repulsive. Default 1.0. Keyword-only.

1.0
symbolic bool

Passed to MPO.from_terms. Default False. Keyword-only.

False

Returns:

Type Description
MPO

The Hamiltonian on n sites, graded by fZ2.

Examples:

>>> from tenet.models import spinless_tv
>>> spinless_tv(4, V=2.0).to_dense().shape
(16, 16)

sun_exchange

sun_exchange(N: int) -> SymmetricTensor

The two-site exchange \(P\) on a pair of SU(\(N\)) fundamentals, as a rank-4 operator.

\(\mathbf{N} \otimes \mathbf{N}\) is the symmetric part (Dynkin label \((2, 0, \ldots)\)) plus the antisymmetric one (\((0, 1, 0, \ldots)\), the singlet at \(N = 2\)), and \(P\) is \(+1\) on the first and \(-1\) on the second. That is one block per coupled sector, so the operator is written with SymmetricTensor.from_blocks and no Clebsch-Gordan array is spelled out. The legs are two OUT (ket) then two IN (bra), the ordering MPO.from_terms and expectation_2site both read a term through.

Parameters:

Name Type Description Default
N int

The number of colours, N >= 2.

required

Returns:

Type Description
SymmetricTensor

\(P\), on (phys, phys, phys*, phys*) over one fundamental multiplet.

Raises:

Type Description
ValueError

If N < 2.

Examples:

>>> from tenet.models import sun_exchange
>>> sun_exchange(3).to_dense().shape
(3, 3, 3, 3)

sun_heisenberg

sun_heisenberg(
    n: int,
    N: int,
    *,
    J: float = 1.0,
    symbolic: bool = False,
) -> MPO

\(H = J \sum_i P_{i,i+1}\) on a chain of SU(\(N\)) fundamentals, open boundaries.

\(P\) is sun_exchange, the permutation of two neighbouring fundamentals, and each bond is one invariant two-site term. At \(N = 2\) it is the spin-1/2 chain up to a constant, \(P = 2\,\vec{S}\cdot\vec{S} + \frac{1}{2}\) per bond.

Parameters:

Name Type Description Default
n int

Chain length.

required
N int

The number of colours, N >= 2.

required
J float

The coupling. Positive is antiferromagnetic. Default 1.0. Keyword-only.

1.0
symbolic bool

Passed to MPO.from_terms. Default False. Keyword-only.

False

Returns:

Type Description
MPO

The Hamiltonian on n sites, graded by SU(\(N\)).

Raises:

Type Description
ValueError

If N < 2.

Examples:

>>> from tenet.models import sun_heisenberg
>>> len(sun_heisenberg(4, 3))
4

transverse_field_ising

transverse_field_ising(
    n: int,
    *,
    J: float = 1.0,
    g: float = 1.0,
    symbolic: bool = False,
) -> MPO

\(H = -J\left(\sum_i \sigma^z_i \sigma^z_{i+1} + g \sum_i \sigma^x_i\right)\), on Z2.

The Pauli matrices, not the spin operators: the critical point is \(g = 1\). The site is graded by the Z2 spin flip, so the physical basis is the \(\sigma^x\) eigenbasis \((\lvert +\rangle, \lvert -\rangle)\) in charge order -- \(\sigma^x\) is the diagonal even operator there and \(\sigma^z\) the off-diagonal odd one, and it is \(\sigma^z\) that the grading forbids alone and admits in pairs.

Parameters:

Name Type Description Default
n int

Chain length.

required
J float

The coupling, multiplying both terms. Default 1.0. Keyword-only.

1.0
g float

The transverse field, in units of J. Default 1.0, the critical point. Keyword-only.

1.0
symbolic bool

Passed to MPO.from_terms. Default False. Keyword-only.

False

Returns:

Type Description
MPO

The Hamiltonian on n sites, graded by Z2.

Examples:

>>> from tenet.models import transverse_field_ising
>>> transverse_field_ising(4, g=0.5).to_dense().shape
(16, 16)

xxz

xxz(
    n: int,
    *,
    Delta: float = 1.0,
    J: float = 1.0,
    spin: float = 0.5,
    symbolic: bool = False,
) -> MPO

\(H = J \sum_i S^x_i S^x_{i+1} + S^y_i S^y_{i+1} + \Delta S^z_i S^z_{i+1}\), on U(1).

The transverse half is written as \(\frac{1}{2}(S^+_i S^-_{i+1} + S^-_i S^+_{i+1})\), which is what the U(1) grading (charge \(2S^z\)) has operators for: \(S^\pm\) carry charge \(\mp 2\) each and enter paired across a bond. Delta = 1 is heisenberg under the same grading, term for term.

Parameters:

Name Type Description Default
n int

Chain length.

required
Delta float

The Ising anisotropy of the \(S^z S^z\) term. Default 1.0. Keyword-only.

1.0
J float

The overall coupling. Positive is antiferromagnetic. Default 1.0. Keyword-only.

1.0
spin float

The site spin. Only 0.5 is shipped. Default 0.5. Keyword-only.

0.5
symbolic bool

Passed to MPO.from_terms. Default False. Keyword-only.

False

Returns:

Type Description
MPO

The Hamiltonian on n sites, graded by U1.

Raises:

Type Description
ValueError

If spin is not 0.5.

Examples:

>>> from tenet.models import xxz
>>> xxz(4, Delta=0.5).to_dense().shape
(16, 16)

hard_core_boson

hard_core_boson(symmetry: _DualFusionRules = U1) -> Site

The hard-core boson site, {|0>, |1>}, graded by U1 (the number) or ungraded.

Parameters:

Name Type Description Default
symmetry FusionRules

U1 (the default), whose charge is the occupation n, or Trivial for the ungraded d=2 site. Any other provider is refused.

U1

Returns:

Type Description
Site

b, b+ and n.

Raises:

Type Description
ValueError

If symmetry is neither U1 nor Trivial.

Examples:

>>> from tenet.models import hard_core_boson
>>> from tenet.symmetry import Trivial
>>> sorted(hard_core_boson().ops)
['b', 'b+', 'n']
>>> hard_core_boson(Trivial).phys.dim
2
Notes

The matrices are the spin-1/2 ladder in disguise (b = S^-, n = S^z + 1/2); the site exists separately because the grading is the difference that matters at a call site -- U1 here counts particles from 0, where spin_half counts 2 S^z from -1, and the two are not interchangeable in a term list. Under Trivial nothing is conserved, so every operator still arrives rank 3 on a D=1 trivial leg and MPO.from_terms works unchanged.

spin_half

spin_half(symmetry: _DualFusionRules = U1) -> Site

The spin-1/2 site, graded by U1 (2 S^z) or by SU2.

Parameters:

Name Type Description Default
symmetry FusionRules

U1 (the default), whose charge is 2 S^z so the doublet is {-1, +1}, or SU2, one j = 1/2 multiplet. Any other provider is refused.

U1

Returns:

Type Description
Site

Under U1: Sz, S+, S- as rank-3 charge-leg operators, with the invariant two-site S.S reachable as a matrix. Under SU2: S.S alone, as the rank-4 invariant operator (see Notes).

Raises:

Type Description
ValueError

If symmetry is neither U1 nor SU2.

Examples:

>>> from tenet.models import spin_half
>>> from tenet.symmetry import SU2
>>> sorted(spin_half().ops)
['S+', 'S-', 'Sz']
>>> sorted(spin_half(SU2).ops)
['S.S']
>>> spin_half(SU2).ops["S.S"].ndim  # rank 2k, k = 2: one whole term
4
Notes

Under SU(2) the set is {S.S} and that is the whole answer. S+ is not an SU(2) operator and it is not omitted by preference: the charge-leg form needs a D=1 sector on the emitted leg, and the only leg a spin-1 tensor operator could emit onto is the j=1 multiplet, whose dense dimension is 3 -- so local_op(sz, phys=phys, charge=SU2Sector(2)) raises on the shape, and no irreducible tensor operator of nonzero rank exists in this API to hand back. What exists is the invariant k-site form, and S.S is it: one whole Heisenberg bond term, whose coupling lives inside its own blocks, which MPO.from_terms splits with an SVD. The same object is invariant under U1 too, where S.S is what expectation_2site measures a bond energy with; it sits in matrices there rather than in ops, because a U1 term list is written from the rank-3 three and a rank-4 entry in ops would be one from_arrays refuses.

spinful_fermion

spinful_fermion(symmetry: _DualFusionRules = fZ2) -> Site

The spinful fermion site, graded by fZ2 or by fZ2 x U1 x SU2.

Parameters:

Name Type Description Default
symmetry FusionRules

fZ2 (the default), the fermion parity alone, whose d=4 basis is (|0>, |ud>, |u>, |d>); or ProductProvider((fZ2, U1, SU2)) -- parity, particle number and total spin -- on which the singly-occupied states are one j = 1/2 multiplet. Any other provider is refused, and so is a product whose factors are not exactly those three in that order.

fZ2

Returns:

Type Description
Site

Under fZ2: c_up, c+_up, c_dn, c+_dn (odd), and n_up, n_dn, n, n_up n_dn (even). The last is the Hubbard U operator, pre-multiplied because MPO.from_terms places one operator per site; under MPO.from_arrays the same spelling is a two-name block expression on two coincident site indices, which its merge multiplies into this very operator. Under fZ2 x U1 x SU2: the invariant set n, n_up n_dn, hop, S.S and nothing else (see Notes).

Raises:

Type Description
ValueError

If symmetry is neither fZ2 nor ProductProvider((fZ2, U1, SU2)).

Examples:

>>> from tenet.models import spinful_fermion
>>> from tenet.symmetry import SU2, U1, ProductProvider, fZ2
>>> site = spinful_fermion()
>>> site.phys.dim
4
>>> site.matrices["n"].diagonal().tolist()
[0.0, 2.0, 1.0, 1.0]
>>> su2 = spinful_fermion(ProductProvider((fZ2, U1, SU2)))
>>> su2.phys.dim, su2.phys.reduced_dim  # four states, three multiplets
(4, 3)
>>> sorted(su2.ops)
['S.S', 'hop', 'n', 'n_up n_dn']
>>> su2.ops["hop"].ndim  # rank 2k, k = 2: one whole bond
4
Notes

The basis is graded -- the even sector {|0>, |ud>} before the odd {|u>, |d>} -- because a dense array over a GradedSpace is laid out sector by sector in canonical order. Modes run up before down, |ud> = c+_up c+_dn |0>, so c_up carries no intra-site sign and c_dn pays the Jordan-Wigner Z on the up mode (c_dn |ud> = -|u>). Inter-site strings are the braiding's business, not a matrix's, and this convention is pinned against a dense oracle.

fZ2 x U1 x SU2 splits the same four states into (even, q=0, j=0), (even, q=2, j=0) and the doublet (odd, q=1, j=1/2), in that canonical order, so it is the same even-before-odd dense basis and every matrix above is still the matrix of the same operator. What changes is which of them is a tensor: the set is {n, n_up n_dn, hop, S.S} and that is the whole answer. c_up and its five relatives are not omitted by preference -- none of them is an SU(2)-invariant tensor, so the invariant form refuses them, and the charge-leg form cannot carry them either, for the reason spin_half has no S+: that form puts the emitted sector on a D=1 dense leg, and the sector c_up emits is the j = 1/2 doublet, of dense dimension 2. n_up and n_dn are invariant under neither, individually; only their sum and their product are.

What is left is enough for the Hubbard model, because the pieces that are not invariant one-site operators are invariant bond operators: hop is the whole hopping term sum_sigma (c+_{i,sigma} c_{j,sigma} + c+_{j,sigma} c_{i,sigma}) on one bond, the analogue of spin_half(SU2)'s S.S, and MPO.from_terms splits it across that bond with an SVD. n and n_up n_dn arrive in the invariant one-site form, rank 2, which is both what expectation_1site measures with and what a one-site term is: a U term goes into a term list on its own site, and the charge-leg form is not needed for it.

spinless_fermion

spinless_fermion() -> Site

The spinless fermion site on fZ2: {|0>, |1>}, even sector first.

Returns:

Type Description
Site

c, c+ (odd, charge FZ2Sector(1)) and n (even).

Examples:

>>> from tenet.models import spinless_fermion
>>> site = spinless_fermion()
>>> sorted(site.ops)
['c', 'c+', 'n']
>>> site.matrices["n"].diagonal().tolist()
[0.0, 1.0]
Notes

There is no JW operator to ship and no place to put one: the Jordan-Wigner string is the fZ2 braiding an odd MPO bond pays when it crosses a physical line, so a term list over these operators is already the fermionic Hamiltonian.