Skip to content

tenet.network

MPS and MPO containers, DMRG, the CTMRG environment machinery, and the 2D layer: lattice geometry, Peps, the lazy double layer and its contraction primitives, and the directional EnvCTM environment and its C4v specialization EnvCTMc4v built on them, plus the time evolution over that layer — Trotter gates, the bond metric each environment supplies, and the truncation that consumes it.

tenet.network

Finite tensor-network algorithms — DMRG and CTMRG — over the public tenet API.

Two families, independent of each other:

  • Matrix product states. MPS and MPO are the containers, MPO.from_terms builds a Hamiltonian from a term list, Env caches the <psi|H|psi> partial contractions, and dmrg_ / sweep_ / lanczos run the variational search -- the ground state, and excited states through dmrg_'s orthogonal_to=. MPS.product, MPS.compress_, MPS.save / MPS.load and the two expectation values surround it.
  • Two-dimensional states. SquareLattice and its two pattern subclasses carry the geometry, Lattice the one object per unique site, Peps the rank-5 state and Peps2Layers the view whose items are lazy DoublePepsTensor pairs -- the bra-ket product is never formed. cor_*, edge_* and append_vec_* are the twelve contractions every 2D environment is built from, and EnvCTM is the directional corner-transfer environment over them: four corners and four edges per site (EnvLocal), eight projectors (EnvProjectors) built by corner2x2 and proj_corners, and update_/iterate_ reporting a CTMRG_out. Its projectors assume nothing about the corner's Hermiticity. EnvCTMc4v is its C4v specialization: one corner and one edge (EnvLocalC4v) for a point-group-symmetric ansatz, whose four identical virtual legs tile the plane as a checkerboard of A and flip(A) through flip and PepsFlip.
  • Time evolution on that layer. gate_nn exponentiates a bond Hamiltonian and splits it across the bond, gates_nn distributes one over a lattice, apply_gate puts a Gate on its two sites and truncate_ reduces the bond it enlarged, in the metric bond_metric supplies -- EnvCTM.bond_metric from the six surrounding environment tensors, or EnvNTU from the local 'NN' cluster. evolution_step_ runs a list of gates and reports one Evolution_out per bond -- the truncation error and what the metric was found to be, which accumulated_truncation_error adds up over a trajectory.

spectrum and ones are shared by both; spectrum_sectors and entropy sit beside them and are what MPS.schmidt_values, MPS.schmidt_sectors and MPS.entanglement_entropy read a bond with.

Tracing. Everything here runs outside jax.jit/jax.grad by construction and makes no differentiability claim: svd_truncated re-decides a bond GradedSpace at every bond of every sweep. Two exceptions state themselves on their own functions — Env.heff2's prepared matvec is fixed-structure and traceable through an injected compile=, and the fixed-bond CTM move EnvCTMc4v.update_(bond=B) is shape-static and differentiable while the bond-deciding form is not.

The composition rule every two-operand tenet.einsum here obeys: operand 1 supplies the IN end of every shared wire. Meeting IN against OUT is not enough, because that condition is symmetric and fixes contractibility only, while the sign a cap pays depends on which operand supplies which end. A wire that genuinely turns around is bent explicitly with tenet.repartition before the contraction. This module uses the public tenet API only, enforced by tests/network/test_hygiene.py.

DMRG_out

Bases: NamedTuple

YASTN's DMRG_out (_dmrg.py:33-39), plus the two things a test needs.

Attributes:

Name Type Description
sweeps int

Number of sweeps run.

energy float

The last sweep's energy.

denergy float

The last sweep's energy change.

max_dSchmidt float

The last sweep's worst-cut Schmidt change.

max_discarded_weight float

The last sweep's maximum per-bond discarded weight.

history list of tuple

One (energy, denergy, dSchmidt, discarded) tuple per sweep.

schedule list of Sweep

The realized schedule, one Sweep per sweep run.

psi MPS

The converged state -- the same object the caller passed in.

Notes

history is one (energy, denergy, dSchmidt, discarded) tuple per sweep, and it says everything YASTN's iterator=True generator protocol says to a test without the protocol. schedule is the realized schedule, one Sweep per sweep run -- zip(out.schedule, out.history) is exact, and out.schedule alone answers whether a run actually reached its final chi or converged earlier. psi is the converged MPS.

No Schmidt-spectrum field. The sweep computes the spectrum at every bond and this record reports max_dSchmidt, how much it moved, which is the convergence criterion and is all this record is for. The spectrum itself is a property of the state, not of the run that produced it, and MPS.schmidt_values, MPS.schmidt_sectors and MPS.entanglement_entropy answer for psi exactly and in any gauge. Carrying the sweep's dict here instead would publish a truncated spectrum taken at whichever direction happened to visit the bond last, and would freeze the convergence test's internal shape into the public output for a number the state already gives. TenPy reports a per-sweep S in sweep_stats because its max_S_err criterion is computed from it; tenet's criterion is the Schmidt change and it is already reported.

Sweep

Bases: NamedTuple

One schedule entry: what a single sweep does.

Attributes:

Name Type Description
chi (int, optional)

The bond-dimension cap handed to svd_truncated at every bond of the sweep. Default 64.

cutoff (float, optional)

The singular-value cutoff handed to the same SVD. Default 1e-14.

noise (float, optional)

Relative strength of the perturbation sweep_ mixes in at each split. Default 0.0 (no perturbation, no random number drawn, and the plain SVD split).

noise_type (str, optional)

Which perturbation noise means -- "wavefunction" (the default) or "perturbative". It also decides the decimation: see sweep_'s table. Ignored when noise is 0.0.

Notes

The defaults equal dmrg_'s flat defaults, so Sweep() is today's sweep and schedule=[Sweep()] is the flat run.

One record per sweep rather than parallel per-knob lists (block2's bond_dims / noises / thrds), so a wrong-length list is impossible to write. The loop tolerances (energy_tol, schmidt_tol, max_sweeps, ncv) are properties of dmrg_'s loop, not of a sweep, and stay flat kwargs there.

Env

Env(
    psi: MPS,
    h: MPO,
    *,
    bra: MPS | None = None,
    compile: Callable | None = None,
)

<bra|H|psi> partial contractions for one (psi, h) pair -- bra = psi by default.

Parameters:

Name Type Description Default
psi MPS

The ket; the cache holds views into its current tensors.

required
h MPO

The Hamiltonian.

required
bra MPS or None

The bra, when it is not psi. Default None, meaning bra is psi -- today's one-state environment, <psi|H|psi>. With a second state given, every environment is the mixed transfer contraction and Env(psi, h, bra=phi).measure() is <phi|H|psi>. Keyword-only.

None
compile Callable or None

Wraps the prepared matvec once per structure key -- jax.jit at the application level; this layer names no accelerator and None (the default) runs the plain Python function. Keyword-only.

None
Notes

F[(n, n + 1)]: (ket IN, mpo OUT, bra OUT), built from sites <= n; F[(n, n - 1)]: (ket OUT, mpo IN, bra IN), built from sites >= n.

The two orientations make every contraction in update_ and heff2 meet IN against OUT -- and that condition is not enough, because it is symmetric: it fixes contractibility only, while the cap sign depends on which operand supplies which end. Every contraction here is therefore a composition with operand 1 supplying IN, and the wires that genuinely bend -- the MPS bond arrow and the MPO bond arrow cross the two-site cell in opposite directions, so closing either cap turns one rail around -- are bent explicitly through _composed.

A plain dict keyed by directed bond, exactly YASTN's Env (yastn/tn/mps/_env.py:94-125). A list-of-left / list-of-right would hide the invalidation discipline, which is the entire correctness content of an environment cache -- a stale F[(n, n+1)] after site n changed gives an energy that is plausible and wrong, the worst failure mode a DMRG has. clear_ therefore pops both directed bonds per site, and it runs before the replacement is written, so a missed update is a KeyError rather than a wrong number.

The two-state form (bra=phi) builds <phi| ... |psi> instead, which is the engine half of the excited-state and measurement machinery: block2's ext_mes are moving environments between two different states (sweep_algorithm.hpp:1195-1206). What it asks of its two states is nothing: update_'s folds (_fold_last/_fold_first and the site-tensor branch alike) are exact for any pair of chains, and MPS._braket's docstring already records the same fact one level down -- the two chains may carry different bond spaces, because the transfer tensor holds one index from each. Two-state environments are therefore gauge-free, and measure on one is <phi|H|psi> undivided, for a phi and a psi in any gauge and at any norm.

What the two-state form does not support is heff2: the prepared matvec's one-sided terms read the IdL/IdR environment channels as gauge identities, which is true of a left-orthonormal chain against itself and false of a mixed transfer, so it refuses rather than returning a plausible wrong operator. block2 does not iterate on its ext_mes either -- it calls multiply on them, once per bond, to produce a projection vector. project2 is that call.

setup_

setup_(to: int = 0) -> Env

Build every environment directed towards site to, and return self.

Parameters:

Name Type Description Default
to int

The target site. Only 0 is implemented. Default 0.

0

Returns:

Type Description
Env

self, its right-directed environments built.

Raises:

Type Description
NotImplementedError

If to is not 0; MPS.canonize_ has the same note.

Notes

to=0 is YASTN's setup_(to='first') (_env.py:104-125): for a right-canonical psi this is every right-directed environment, and it is the state a left-to-right sweep starts from.

update_

update_(n: int, *, to: str) -> None

Write one directed-bond entry from its neighbour -- YASTN _env.py:152-168.

Parameters:

Name Type Description Default
n int

The site whose directed-bond entry is written.

required
to str

The direction to write toward: 'last' writes F[(n, n+1)] from F[(n-1, n)], 'first' writes F[(n, n-1)] from F[(n+1, n)]. A direction, not a site -- unlike MPS.canonize_'s to, which is an int site index. Keyword-only.

required
Notes

to='last' writes F[(n, n+1)] from F[(n-1, n)]; to='first' writes F[(n, n-1)] from F[(n+1, n)]. Site-tensor path: three pairwise tenet.einsum calls each -- environment first, then the ket, then the MPO, then the bra. With an edge-block table present the step goes edge-aware instead (_fold_last / _fold_first): the identity channels ride idmap with no W contraction, only the operator-carrying blocks pay one, and unlike heff2 this path is exact for any state -- no gauge assumption.

clear_

clear_(*sites: int) -> None

Pop both directed bonds touching each changed site -- YASTN clear_site_.

Parameters:

Name Type Description Default
*sites int

The sites whose tensors changed.

()

heff2

heff2(n: int, aa: SymmetricTensor) -> SymmetricTensor

H_eff on the two-site tensor at bond (n, n+1). Two paths, one output.

Parameters:

Name Type Description Default
n int

The bond's left site.

required
aa SymmetricTensor

The two-site tensor, (left bond OUT, p OUT, q OUT, right bond IN).

required

Returns:

Type Description
SymmetricTensor

H_eff @ aa, with aa's structure exactly.

Notes

Two paths, and the operator's representation is which one, decided at build time. An MPO that carries only site tensors takes the site-tensor contraction: from_w, an MPO built from bare tensors, whatever MPO.materialize hands back, and from_terms, from_arrays and from_entries at their default. An MPO built symbolic=True carries an edge description, at either cutoff, and that is what the keyword buys: the prepared, symbolic, term-family matvec. There is no runtime dispatch either way: no bond-width threshold, no chi threshold, no probe, no path= keyword. The caller states the representation when the operator is built, exactly as from_terms' cutoff=None against a float already states which operator is built.

The rule, in terms a caller reads off their own model:

  • a finite-range lattice model -- a narrow MPO bond, D_w of order ten -- wants the site-tensor path, which is the bare builder, no keyword. The prepared machinery's per-bond cores and structure-keyed cache cost more per steady sweep than they buy back at that width;
  • quantum chemistry -- O(K^4) terms, a bond in the thousands -- wants the description kept, which is symbolic=True. There the prepared path is the faster of the two and the only route that fits a large orbital count in memory at all.

A symbolic operator gets exactly one engine: the prepared term-family matvec, at either cutoff, with later parallelism and accelerator work attaching there and nowhere else. That is block2's engine design in tenet's form -- its EffectiveHamiltonian never forms the effective Hamiltonian and instead dispatches the symbolic operator sum term by term against the wavefunction (effective_hamiltonian.hpp:230-243). The site-tensor path is what an externally-built MPO gets, because symbols cannot be recovered from a numeric W in general: a compressed W retains no edge structure.

cutoff is the other build-time knob, and it is orthogonal to this one. cutoff=None keeps the exact finite-state machine, whose bond is already minimal for a finite-range lattice model and whose identity channels ride idmap/spec_op with no W contraction at all; a float cutoff compresses, which is what an ab initio Hamiltonian needs and which -- because the rotation mixes the open states -- turns every open state into an operator-carrying one, so on a lattice model cutoff=None is the cheaper of the two.

The path in detail. The two environments are folded into the site blocks once per bond (_build2, MPSKit's AC2_hamiltonian) and cached against the environment tensors' identity, so one lanczos solve at ncv=3 pays the fold once and applies _apply2 three times; absent fields are None and are skipped, which is where the structural zeros go. Like MPSKit's matvec, the IdL/IdR-anchored terms are one-sided: they use the sweep's mixed-canonical gauge -- sites left of the bond left-orthonormal, sites right of it right-orthonormal, which sweep_ maintains at every bond -- as the standing precondition. It is the one thing this path asks of its caller, and it is not chosen at run time either: a caller whose environments come from a differently gauged state hands over h.materialize(), which drops the description and takes the branch below. The apply itself is compiled through compile= once per structure key -- the bond, and the tuple of aa's legs, which between them fix every leg the traced graph sees -- and the cache holds one entry per bond, its callable kept across a revisit and retraced only when the key moves.

The site-tensor path, for an MPO with no description at all (from_w, bare site tensors, or MPO.materialize): right environment, then W2, then W1, then the left environment -- YASTN's Env_mps_mpo_mps.Heff2 order (_env.py:496-518) with precompute=False, which _dmrg.py:102-108 documents as O(D^3 M d + D^2 M^2 d^2).

The two paths agree as operators but sum their terms in a different order, so they agree to solver precision, never bitwise. In and out on (left bond OUT, p OUT, q OUT, right bond IN): the bra legs of the two environments become the output's bonds while the ket legs close against the input's, which is why the result has aa's structure exactly and lanczos can add the two.

heff2_families

heff2_families(
    n: int, aa: SymmetricTensor
) -> tuple[SymmetricTensor, ...]

heff2's term families, applied separately, unsummed.

Parameters:

Name Type Description Default
n int

The bond's left site.

required
aa SymmetricTensor

The two-site tensor, exactly as heff2 takes it.

required

Returns:

Type Description
tuple of SymmetricTensor

One partial application per populated family, each with aa's structure; their sum is heff2(n, aa) term for term.

Examples:

>>> import tenet
>>> from tenet import GradedSpace
>>> from tenet.network import MPO, MPS, Env, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> import numpy as np
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> sz = local_op(np.diag([-0.5, 0.5]), phys=phys, charge=U1Sector(0))
>>> terms = [(1.0, [(sz, i), (sz, i + 1)]) for i in range(2)]
>>> h = MPO.from_terms(3, terms, symbolic=True)  # families need the description
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1), U1Sector(1)]).canonize_()
>>> env = Env(psi, h).setup_()
>>> aa = tenet.einsum("apx,xqr->apqr", psi[0], psi[1])
>>> parts = env.heff2_families(0, aa)
>>> total = parts[0]
>>> for part in parts[1:]:
...     total = tenet.add(total, part)
>>> bool(tenet.allclose(total, env.heff2(0, aa)))
True
Notes

block2's perturbative_noise (effective_hamiltonian.hpp:263-360) builds one perturbation vector per sub-label of the symbolic operator sum; the families _cores2 already holds -- the identity-through ride, the two one-sided anchored sums, and the two open-to-open AA remainders -- are this engine's version of that resolution, and this is the read sweep_'s perturbative noise uses. It is a read, not a second engine: the same _prepare2 cache, the same contractions, only not added up.

The site-tensor path -- an MPO with no edge description, which is what every builder returns unless the caller writes symbolic=True -- has no families to resolve, so the default is the single vector (heff2(n, aa),): the operator's own action on the state, unresolved. A one-vector mixer is weaker than a family-resolved one, and it is what an operator that carries no symbols can offer. On a finite-range lattice model, which is not bond-limited at a usable chi, the weaker mixer costs a sweep of head start and no accuracy: both mixers reach the same converged energy. An operator that is bond-limited is a different case and keeps its description.

Not compiled: compile= wraps the summed matvec, which is what a Krylov solve calls thousands of times; this is called once per bond visit.

project2

project2(n: int, aa: SymmetricTensor) -> SymmetricTensor

The bond's projection vector: aa carried from the ket's bonds to the bra's.

Parameters:

Name Type Description Default
n int

The bond's left site.

required
aa SymmetricTensor

The ket chain's two-site tensor at that bond, (left bond OUT, p OUT, q OUT, right bond IN).

required

Returns:

Type Description
SymmetricTensor

The same rank-4 structure on the bra chain's bonds: <bra env| H_n H_{n+1} |aa, ket env>. With h the identity (MPO.identity) this is the two-site reduced form of the ket state in the bra state's environment gauge, and tenet.inner(p, bb) is then <ket|bra'> for any bb in the bra's two-site variational space.

Examples:

>>> import tenet
>>> from tenet import GradedSpace
>>> from tenet.network import MPO, MPS, Env
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> states = [U1Sector(1), U1Sector(-1), U1Sector(1), U1Sector(-1)]
>>> phi = MPS.product(phys, states).canonize_()
>>> psi = MPS.product(phys, states).canonize_()
>>> env = Env(phi, MPO.identity(4, phys), bra=psi).setup_()
>>> pair = tenet.einsum("apx,xqr->apqr", phi[0], phi[1])
>>> p = env.project2(0, pair)
>>> bb = tenet.einsum("apx,xqr->apqr", psi[0], psi[1])
>>> round(float(tenet.inner(p, bb)), 12)  # <psi|phi> read at one bond
1.0
Notes

block2's i_eff->multiply on a two-state moving environment (sweep_algorithm.hpp:1195-1206), which is how it builds one ortho_bra entry per converged state before handing the collection to eigs (:1244-1249). Its ext_mes are built with the identity MPO -- the driver reads get_identity_mpo() for exactly this (pyblock2/driver/core.py:4817-4830) -- so the vector is an overlap, not an energy; MPO.identity is the same spelling here.

What the contraction requires of its two states is only what the caller reads it as. The contraction itself is exact in any gauge. But the use -- "project the bra's two-site variational space against the ket state" -- is a statement about an orthonormal basis, so it holds exactly when the bra chain is mixed-canonical at bond n, which sweep_ maintains at every bond it visits. The ket needs nothing: a gauge transformation on any of its bonds cancels between the two environments and the two-site tensor, which is why the converged states of an orthogonal_to= run are held fixed rather than canonicalized alongside the sweep the way block2 canonicalizes its ext_mpss (:893-917).

The four contractions are heff2's site-tensor path, shared verbatim: the one path in this class that reads no channel as a gauge identity, hence the one that survives bra is not psi.

measure

measure() -> float

<psi|H|psi> without the eigensolver, on a private left-to-right pass.

Returns:

Type Description
float

<bra|H|psi>, not divided by any norm -- <psi|H|psi> on a one-state Env and <phi|H|psi> on an Env(psi, h, bra=phi).

Notes

The first thing in this repository that measures a converged energy independently of the lanczos Rayleigh quotient that produced it. On a two-state Env it is instead the engine fact the measurement API stands on: Env(psi, h, bra=phi).measure() is <phi|H|psi>, and with h the identity (MPO.identity) it is the plain overlap <phi|psi>. No gauge is assumed of either chain. YASTN's measure is the same closing contraction one level down (_env.py:462-468, vdot(vecL, vecR)); the pass is built in a fresh Env so a measurement never writes into a sweep's cache.

CTMRG_out

Bases: NamedTuple

What a iterate_ run reports, YASTN's CTMRG_out.

Attributes:

Name Type Description
sweeps int

Sweeps performed.

max_dsv float

The worst corner's spectrum change over the last sweep; inf after the first sweep, which has nothing to compare against.

converged bool

Whether max_dsv < corner_tol when the loop stopped.

EnvCTM

EnvCTM(psi: Any, init: str | None = 'eye', bra: Any = None)

A directional CTM environment: four corners and four edges per unique site.

Parameters:

Name Type Description Default
psi Peps

The network. A rank-5 state becomes a Peps2Layers view; a rank-4 one -- a classical partition function -- is used as it is.

required
init str or None

'eye' (the default) for the one-dimensional identity boundary, 'dl' for one un-truncated absorption on top of it, or None to leave the environment empty.

'eye'
bra Peps or None

An independent bra for the double layer. Default None, i.e. <psi|psi>.

None

Raises:

Type Description
ValueError

If init is not one of the three.

Examples:

>>> import numpy as np
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import EnvCTM, Peps, SquareLattice
>>> from tenet.symmetry import Z2, Z2Sector
>>> beta = 0.3
>>> c, s = np.sqrt(np.cosh(beta)), np.sqrt(np.sinh(beta))
>>> w = np.array([[c, s], [c, -s]])
>>> block = np.einsum("st,sl,sb,sr->tlbr", w, w, w, w)
>>> V = GradedSpace.new(Z2, {Z2Sector(0): 1, Z2Sector(1): 1})
>>> legs = (Leg(V, IN), Leg(V, OUT), Leg(V, OUT), Leg(V, IN))
>>> psi = Peps(SquareLattice(dims=(1, 1)), SymmetricTensor.from_dense(block, legs))
>>> env = EnvCTM(psi)
>>> out = env.iterate_(max_bond=8, max_sweeps=50, corner_tol=1e-10)
>>> out.converged
True
Notes

The environment is a Lattice of EnvLocal records and the projectors a second one of EnvProjectors, so both fold through the geometry the way the state does: one record per unique site, read at any site of the plane.

Nx property

Nx: int

Rows in the unit cell.

Ny property

Ny: int

Columns in the unit cell.

sites

sites(reverse: bool = False) -> tuple[Site, ...]

The unique sites.

nn_site

nn_site(site: Any, d: Any) -> Site | None

The neighbour of site in direction d.

wire

wire(d: str) -> str

The einsum letters one site leg occupies: two for a double layer, one for a single one. 't' is 'tT' or 't', and so on for l, b, r.

site_legs

site_legs(site: Any, d: str) -> tuple[Leg, ...]

The legs an environment tensor needs to meet the site's d leg.

reset_

reset_(init: str = 'eye') -> None

Seed every corner and edge, YASTN reset_:239.

Parameters:

Name Type Description Default
init str

'eye' (the default) puts a one-dimensional environment bond everywhere: each corner is the scalar one and each edge is the identity that closes the ket against the bra (a single layer's free boundary, all ones). 'dl' absorbs one layer of the network into that seed without truncating, which is the environment YASTN's expand_outward_ builds -- one un-truncated 'hv' sweep reaches it, in the projectors' singular basis rather than YASTN's product basis, and an environment is defined only up to a gauge on its bonds.

'eye'

Raises:

Type Description
ValueError

If init is not 'eye' or 'dl'.

update_

update_(
    max_bond: int | None = None,
    moves: str = "hv",
    cutoff: float | None = 1e-14,
) -> None

One sweep: each letter of moves, in order, in place.

Parameters:

Name Type Description Default
max_bond int or None

The environment bond-dimension cap. Default None, i.e. no cap.

None
moves str

A sequence of moves. 'h' and 'v' update every site simultaneously into a fresh environment; 'l', 'r', 't' and 'b' run causally, column after column or row after row. Default 'hv'.

'hv'
cutoff float or None

Relative singular-value cutoff for the projector truncation. Default 1e-14.

1e-14

Raises:

Type Description
ValueError

If moves contains anything but 'hvlrtb'.

corner_spectra

corner_spectra() -> dict[tuple[Any, str], list[float]]

Every corner's singular values, largest scaled to one -- YASTN calculate_corner_svd:468.

Returns:

Type Description
dict

(site, corner name) -> descending singular values.

iterate_

iterate_(
    max_bond: int | None = None,
    moves: str = "hv",
    max_sweeps: int = 100,
    corner_tol: float | None = 1e-10,
    cutoff: float | None = 1e-14,
) -> CTMRG_out

Sweep until the corner spectra stop moving, YASTN iterate_:841.

Parameters:

Name Type Description Default
max_bond int or None

The environment bond-dimension cap. Default None.

None
moves str

The sweep's moves; 'hv' (the default) and 'lrtb' are the two sensible ones.

'hv'
max_sweeps int

The sweep budget. Default 100.

100
corner_tol float or None

Stop when the worst corner's spectrum moves less than this. None runs the budget out. Default 1e-10.

1e-10
cutoff float or None

Relative singular-value cutoff for the projector truncation. Default 1e-14.

1e-14

Returns:

Type Description
CTMRG_out

sweeps, max_dsv and converged.

Raises:

Type Description
StructureChangingError

Under jax.jit/jax.grad: the loop reads a spectrum to decide when to stop and reads singular values to decide a bond.

bond_metric

bond_metric(
    q0: SymmetricTensor,
    q1: SymmetricTensor,
    s0: Any,
    s1: Any,
    dirn: str,
) -> SymmetricTensor

The full-update bond metric: the six environment tensors closed round a bond.

Parameters:

Name Type Description Default
q0 SymmetricTensor

The two reduced site tensors, rank 5, whose bond legs the metric is on -- truncate_'s qr isometries, not the state's own tensors.

required
q1 SymmetricTensor

The two reduced site tensors, rank 5, whose bond legs the metric is on -- truncate_'s qr isometries, not the state's own tensors.

required
s0 Site or tuple[int, int]

Their sites, in the fermionic order.

required
s1 Site or tuple[int, int]

Their sites, in the fermionic order.

required
dirn str

'lr' for a horizontal bond, 'tb' for a vertical one.

required

Returns:

Type Description
SymmetricTensor

Rank 4, (bra0, bra1, ket0, ket1): a map from the pair of ket bond legs to the pair of bra ones, not symmetrized and not checked for positivity. truncate_ measures both and says what it found.

Raises:

Type Description
ValueError

If the state is a single layer, which has no bond to truncate, or if the two sites are not adjacent in the direction dirn names.

Examples:

>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import EnvCTM, Peps, SquareLattice
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> legs = (Leg(V, IN), Leg(V, OUT), Leg(V, OUT), Leg(V, IN), Leg(V, OUT))
>>> psi = Peps(SquareLattice(dims=(2, 2)), SymmetricTensor.random(legs, seed=0))
>>> env = EnvCTM(psi, init="dl")
>>> env.bond_metric(psi[0, 0], psi[0, 1], (0, 0), (0, 1), "lr").ndim
4
Notes

YASTN's bond_metric:770. The picture, for dirn == 'lr'::

tl -- t ------- t -- tr
 |    |         |    |
 l -- Q0 --   -- Q1 -- r
 |    |         |    |
bl -- b ------- b -- br

The two 2x2 enlarged corners are corner2x2 with the reduced tensor passed in place of the site's -- the environment ring is the site's, the tensor inside it is not -- so this adds no contraction primitive to the twelve. The remaining four tensors close the top and bottom (or left and right) of the picture, two compositions each.

The last composition closes the environment ring, and it pays the ribbon twist closed cannot read. An environment leg is the boundary bond of the double layer -- one line of the bra network and one of the ket at once -- so joining the two halves over the two remaining environment wires closes a cycle in each layer. The bend rule reads one orientation per leg, and both these legs supply IN from the same half, so it finds nothing to bend and pays nothing; the closure is there all the same, and tenet.twist on both of them is its theta, one per layer. Measured against a cluster contracted site by site with no environment at all: exact on every loop-free cluster either way (the ring is one-dimensional there, so theta is 1), and 1.6 and 0.79 out on the lr and tb bonds of a 2x2 patch under fermion parity without it -- 4e-15 and 1.7e-15 with it, and the same on an interior bond of a 3x3, where both environment wires carry sectors and twisting only one is wrong.

items

items() -> Iterator[tuple[Site, EnvLocal]]

(site, environment) for every unique site.

EnvCTMc4v

EnvCTMc4v(
    psi: Any, init: str | None = "eye", bra: Any = None
)

Bases: EnvCTM

A C4v CTM environment: one corner, one edge, one move.

Parameters:

Name Type Description Default
psi Peps

The network, on a one-site geometry. Its site tensor must have four identical virtual legs -- the C4v ansatz constraint, without which no rotation acts on it.

required
init str or None

'eye' (the default), 'dl' or None, as EnvCTM takes them.

'eye'
bra Peps or None

An independent bra for the double layer. Default None.

None

Raises:

Type Description
ValueError

If the geometry has more than one unique site, if the four virtual legs of the site tensor are not identical, or if init is not one of the three.

Examples:

>>> import numpy as np
>>> from tenet import OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import EnvCTMc4v, Peps, SquareLattice
>>> from tenet.symmetry import Z2, Z2Sector
>>> beta = 0.3
>>> c, s = np.sqrt(np.cosh(beta)), np.sqrt(np.sinh(beta))
>>> w = np.array([[c, s], [c, -s]])
>>> block = np.einsum("st,sl,sb,sr->tlbr", w, w, w, w)
>>> V = GradedSpace.new(Z2, {Z2Sector(0): 1, Z2Sector(1): 1})
>>> legs = (Leg(V, OUT),) * 4
>>> psi = Peps(SquareLattice(dims=(1, 1)), SymmetricTensor.from_dense(block, legs))
>>> env = EnvCTMc4v(psi)
>>> env.iterate_(max_bond=8, max_sweeps=50, corner_tol=1e-10).converged
True
Notes

The geometry is one unique site and the checkerboard lives in the views: every site of the plane folds onto the same record, and PepsFlip (and its environment twin) flip what an odd site hands back. That is why there is one C and one T and not two of each.

reset_

reset_(init: str = 'eye') -> None

Seed the corner and the edge, YASTN reset_.

Parameters:

Name Type Description Default
init str

'eye' (the default) puts a one-dimensional environment bond on both: the corner is the scalar one and the edge closes the ket against the bra (a single layer's free boundary is all ones). 'dl' absorbs one layer into that seed without truncating.

'eye'

Raises:

Type Description
ValueError

If init is not 'eye' or 'dl'.

update_

update_(
    max_bond: int | None = None,
    moves: str = "d",
    cutoff: float | None = 1e-14,
    bond: Any = None,
) -> None

One sweep of the single move, in place.

Parameters:

Name Type Description Default
max_bond int or None

The environment bond-dimension cap. Default None, i.e. no cap.

None
moves str

'd', the only move a C4v environment has. Default 'd'.

'd'
cutoff float or None

Relative singular-value cutoff for the projector truncation. Default 1e-14.

1e-14
bond GradedSpace or None

A frozen environment bond. None (the default) decides one from the singular values, which no trace allows; a bond reuses one decided outside and makes the move shape-static and differentiable.

None

Raises:

Type Description
ValueError

If moves is anything but 'd'.

iterate_

iterate_(
    max_bond: int | None = None,
    moves: str = "d",
    max_sweeps: int = 100,
    corner_tol: float | None = 1e-10,
    cutoff: float | None = 1e-14,
) -> Any

The parent's sweep loop, with 'd' as the move -- YASTN iterate_.

EnvLocal dataclass

EnvLocal(
    tl: SymmetricTensor | None = None,
    tr: SymmetricTensor | None = None,
    bl: SymmetricTensor | None = None,
    br: SymmetricTensor | None = None,
    t: SymmetricTensor | None = None,
    l: SymmetricTensor | None = None,
    b: SymmetricTensor | None = None,
    r: SymmetricTensor | None = None,
)

One site's environment: four corners and four edges, any of them still unset.

Attributes:

Name Type Description
tl, tr, bl, br SymmetricTensor or None

The corners, rank 2.

t, l, b, r SymmetricTensor or None

The edges, rank 3 (single layer) or rank 4 (double layer).

Notes

A plain mutable record, YASTN's EnvCTM_local. It is a dataclass rather than a NamedTuple because a move writes one field at a time into a fresh record and then swaps the records over -- the "all sites simultaneously" the 'h' and 'v' moves promise is exactly that no site reads a field another site has already written.

EnvLocalC4v dataclass

EnvLocalC4v(
    tl: SymmetricTensor | None = None,
    t: SymmetricTensor | None = None,
)

The C4v environment of one site: one corner and one edge, read under eight names.

Attributes:

Name Type Description
tl SymmetricTensor or None

The corner, rank 2. tr, bl and br read the same tensor.

t SymmetricTensor or None

The edge, rank 3 (single layer) or rank 4 (double layer). l, b and r read the same tensor.

Notes

YASTN's EnvCTM_c4v_local. The aliases are what let corner2x2 and every measurement written against EnvLocal run here unchanged.

EnvProjectors dataclass

EnvProjectors(
    hlt: SymmetricTensor | None = None,
    hlb: SymmetricTensor | None = None,
    hrt: SymmetricTensor | None = None,
    hrb: SymmetricTensor | None = None,
    vtl: SymmetricTensor | None = None,
    vtr: SymmetricTensor | None = None,
    vbl: SymmetricTensor | None = None,
    vbr: SymmetricTensor | None = None,
)

One site's eight projectors, YASTN's EnvCTM_projectors.

Attributes:

Name Type Description
hlt, hlb, hrt, hrb SymmetricTensor or None

The horizontal move's projectors: left/right, top/bottom half.

vtl, vtr, vbl, vbr SymmetricTensor or None

The vertical move's.

Notes

Each is rank 3 (single layer) or rank 4 (double layer): the environment leg it absorbs, the site's leg once per layer, and the truncated bond it produces.

PepsFlip

PepsFlip(base: Any)

A read-only view of a network whose odd sites come back flipped.

Parameters:

Name Type Description Default
base Peps or Peps2Layers

The network holding A. Every other attribute is forwarded to it.

required

Examples:

>>> from tenet import OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import Peps, PepsFlip, SquareLattice
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT),) * 4, seed=0)
>>> psi = PepsFlip(Peps(SquareLattice(dims=(1, 1)), a))
>>> psi[0, 0].legs[0].side, psi[0, 1].legs[0].side
(<Side.OUT: 'out'>, <Side.IN: 'in'>)
Notes

YASTN's PsiFlip, wrapped outside the double layer rather than inside it, so a DoublePepsTensor's bra is flipped with its ket. The two agree: flip(adjoint(a)) is adjoint(flip(a)).

EnvNTU

EnvNTU(psi: Peps, which: str = 'NN')

The neighbourhood-tensor-update environment: a bond metric from the local cluster.

Parameters:

Name Type Description Default
psi Peps

The state being evolved. Held, not copied: truncate_ writes into it.

required
which str

The cluster. 'NN' (the default) is the only one transcribed -- the six sites around the bond, exactly, with the boundary closed by rank-1 hairs.

'NN'

Raises:

Type Description
ValueError

If which is not 'NN', or if psi has no physical leg.

Examples:

>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import EnvNTU, Peps, SquareLattice
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> legs = (Leg(V, IN), Leg(V, OUT), Leg(V, OUT), Leg(V, IN), Leg(V, OUT))
>>> env = EnvNTU(Peps(SquareLattice(dims=(2, 2)), SymmetricTensor.random(legs, seed=0)))
>>> env.which
'NN'
Notes

The cluster is::

    (-1 +0)==(-1 +1)
       |        |
(+0 -1)==Q0== ==Q1==(+0 +2)
       |        |
    (+1 +0)==(+1 +1)

for a horizontal bond and its transpose for a vertical one. The four corner sites enter through cor_* and the two ends through edge_l/edge_r with a hair -- the far neighbour closed on its other three virtual legs -- which is YASTN's hair_l/hair_r. Every contraction is exact, so the metric is Hermitian and positive up to floating point; truncate_ measures that rather than relying on it, and the module docstring says which direction still misses its Gram form.

The two hairs whose operand order the composition rule leaves tied are 'l' and 'b' -- two ket-IN wires against two bra-IN ones, two bends either way -- and there the ket leads. The tie is not a choice: a step that closes a cycle pays the ribbon twist, and with it paid both operand orders land on the same tensor.

Simplification: the larger clusters are not here. 'NN+' and 'NN++' add rings approximated by rank-1 SVDs of the boundary, 'NNN' adds the four diagonal sites exactly, and the ladder mode is a different geometry again; each is a different contraction of the same primitives, and the 'NN' cluster is the one that makes the metric-vs-CTM comparison in tests/network/test_evolution.py.

bond_metric

bond_metric(
    q0: SymmetricTensor,
    q1: SymmetricTensor,
    s0: Any,
    s1: Any,
    dirn: str,
) -> SymmetricTensor

The 'NN' cluster closed around the bond. YASTN EnvNTU._g_NN.

Parameters:

Name Type Description Default
q0 SymmetricTensor

The two reduced site tensors, rank 5, whose bond legs the metric is on.

required
q1 SymmetricTensor

The two reduced site tensors, rank 5, whose bond legs the metric is on.

required
s0 Site

Their sites, in the fermionic order.

required
s1 Site

Their sites, in the fermionic order.

required
dirn str

'lr' or 'tb'.

required

Returns:

Type Description
SymmetricTensor

Rank 4, (bra0, bra1, ket0, ket1).

Examples:

>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import EnvNTU, Peps, SquareLattice
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> legs = (Leg(V, IN), Leg(V, OUT), Leg(V, OUT), Leg(V, IN), Leg(V, OUT))
>>> psi = Peps(SquareLattice(dims=(2, 2)), SymmetricTensor.random(legs, seed=0))
>>> env = EnvNTU(psi)
>>> g = env.bond_metric(psi[0, 0], psi[0, 1], (0, 0), (0, 1), "lr")
>>> g.ndim
4

Evolution_out

Bases: NamedTuple

What one bond's truncation measured. Every error and eigenvalue is relative.

Attributes:

Name Type Description
bond Bond or None

The bond that was truncated.

truncation_error float

||RR - MM||_g / ||RR||_g: the norm of what the truncation discarded, measured in the environment's own metric rather than in the plain one.

nonhermitian_part float

||g - g^dagger|| / (2 ||g||) before the symmetrization. An estimate of the environment's error, not of the truncation's.

min_eigenvalue float or None

The smallest eigenvalue of the symmetrized metric over ||g||. Negative means the environment produced something that is not a form; None when fix_metric was None and no eigendecomposition was taken.

wrong_eigenvalues float or None

The fraction of eigenvalues below the error scale, which fix_metric replaced.

iterations int

Least-squares sweeps taken, 0 when the SVD initialization was kept.

pinv_cutoff float or None

The pseudo-inverse cutoff the last solve chose off PINV_CUTOFFS.

Examples:

>>> from tenet.network import Evolution_out
>>> Evolution_out(truncation_error=1e-7).iterations
0

Gate

Bases: NamedTuple

A nearest-neighbour two-site gate, already split across its bond.

Attributes:

Name Type Description
g0 SymmetricTensor

The half acting on bond.site0: (phys OUT, phys IN, aux IN).

g1 SymmetricTensor

The half acting on bond.site1: (aux OUT, phys OUT, phys IN).

bond Bond

The bond, oriented in the fermionic order -- site0 before site1.

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import Bond, Site, gate_nn, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(0): 2})
>>> sz = np.diag([-0.5, 0.5])
>>> h = local_op(np.kron(sz, sz), phys=phys)
>>> g = gate_nn(h, 0.1, Bond(Site(0, 0), Site(0, 1)))
>>> g.g0.ndim, g.g1.ndim
(3, 3)

Bond

Bases: NamedTuple

A nearest-neighbour bond, site0 before site1 in the fermionic order.

Attributes:

Name Type Description
site0 Site

The left (horizontal bond) or top (vertical bond) end.

site1 Site

The right or bottom end.

CheckerboardLattice

CheckerboardLattice()

Bases: SquareLattice

The infinite bipartite lattice: a 2x2 cell holding two unique tensors.

Examples:

>>> from tenet.network import CheckerboardLattice
>>> lat = CheckerboardLattice()
>>> lat.site2index((0, 0)), lat.site2index((0, 1)), lat.site2index((1, 1))
(0, 1, 0)
>>> len(lat.sites())
2

site2index

site2index(site: Any) -> Any

(nx + ny) % 2 -- the two sublattices.

Lattice

Lattice(geometry: Any, objects: Any = None)

A geometry plus one object per unique site: the container everything 2D subclasses.

Parameters:

Name Type Description Default
geometry SquareLattice

The geometry, or anything carrying one as .geometry.

required
objects optional

One object, a nested sequence, or a {site: object} mapping. A single object is spread over every unique site.

None

Raises:

Type Description
ValueError

If an assignment lands outside the geometry, if two different objects reach one unique site, or if some unique site is left unassigned.

Examples:

>>> from tenet.network import CheckerboardLattice, Lattice
>>> lat = Lattice(CheckerboardLattice(), {(0, 0): "A", (0, 1): "B"})
>>> lat[0, 0], lat[1, 1], lat[0, 3]
('A', 'A', 'B')
Notes

Reads and writes go through site2index, so lat[5, 7] is the tensor of the unique site (5, 7) folds onto -- the container stores one entry per unique site and nothing per lattice site. It is a mutable container of immutable tensors, the arrangement MPS and Env already use.

Nx property

Nx: int

Rows in the unit cell.

Ny property

Ny: int

Columns in the unit cell.

dims property

dims: tuple[int, int]

Unit-cell size, (rows, columns).

boundary property

boundary: str

The geometry's boundary condition.

sites

sites(reverse: bool = False) -> tuple[Site, ...]

The unique sites.

bonds

bonds(
    dirn: str | None = None, reverse: bool = False
) -> tuple[Bond, ...]

The unique bonds.

nn_site

nn_site(site: Any, d: str | tuple[int, int]) -> Site | None

The neighbour of site in direction d.

nn_bond_dirn

nn_bond_dirn(s0: Any, s1: Any = None) -> str

The orientation of a nearest-neighbour bond.

f_ordered

f_ordered(s0: Any, s1: Any) -> bool

Whether two sites are in the fermionic order.

site2index

site2index(site: Any) -> Any

The unique-tensor key of site.

items

items() -> Iterator[tuple[Site, Any]]

(site, object) for every unique site.

RectangularUnitcell

RectangularUnitcell(
    pattern: Sequence[Sequence[Any]]
    | dict[tuple[int, int], Any],
)

Bases: SquareLattice

An infinite lattice tiled by a rectangular pattern of unique-tensor labels.

Parameters:

Name Type Description Default
pattern Sequence[Sequence] or dict[tuple[int, int], object]

Labels of the unique tensors at each site of the cell. A dict must cover the rectangle from (0, 0) to (Nx - 1, Ny - 1).

required

Raises:

Type Description
ValueError

If the pattern is not a rectangle, if its labels are unhashable, or if two sites carrying the same label do not see the same four neighbours.

Examples:

>>> from tenet.network import RectangularUnitcell
>>> lat = RectangularUnitcell([[0, 1], [1, 0]])   # the checkerboard, spelled out
>>> lat.sites()
(Site(nx=0, ny=0), Site(nx=0, ny=1))
>>> RectangularUnitcell([[0, 1], [1, 1]])
Traceback (most recent call last):
    ...
ValueError: RectangularUnitcell: each unique label must have the same neighbours
Notes

The neighbourhood check is the class's whole reason to exist. A pattern such as [[0, 1], [1, 1]] assigns one tensor to sites whose environments differ, so a single environment per label cannot describe it -- YASTN states that as a warning (_geometry.py:255) and raises; here it is the same refusal.

Only patterns of a single momentum Q survive the check, which is exactly the family this parameterization can represent (after B. Ponsioen's ad-peps).

site2index

site2index(site: Any) -> Any

The pattern's label at site, folded into the unit cell.

Site

Bases: NamedTuple

A lattice site, (row, column).

Attributes:

Name Type Description
nx int

Row index; increases downwards.

ny int

Column index; increases rightwards.

SquareLattice

SquareLattice(
    dims: tuple[int, int] = (2, 2),
    boundary: str = "infinite",
)

Geometry of a 2D square lattice: which sites exist, and who neighbours whom.

Parameters:

Name Type Description Default
dims tuple[int, int]

Unit-cell size as (rows, columns).

(2, 2)
boundary str

'infinite' (the default), 'obc' for a finite patch, or 'cylinder' for a finite cylinder periodic along the rows.

'infinite'

Raises:

Type Description
ValueError

If boundary is not one of the three, or dims is not two positive ints.

Examples:

>>> from tenet.network import SquareLattice
>>> lat = SquareLattice(dims=(2, 3))
>>> lat.nn_site((0, 2), "r")            # infinite: wraps by the unit cell
Site(nx=0, ny=3)
>>> lat.site2index((0, 3))
(0, 0)
>>> SquareLattice(dims=(2, 3), boundary="obc").nn_site((0, 2), "r") is None
True
Notes

An infinite lattice never returns None from nn_site: it shifts, and site2index folds back into the unit cell later. That split -- which site versus which tensor -- is what lets an algorithm walk the plane in absolute coordinates and only fold when it reads a tensor.

Nx property

Nx: int

Rows in the unit cell.

Ny property

Ny: int

Columns in the unit cell.

dims property

dims: tuple[int, int]

Unit-cell size, (rows, columns).

sites

sites(reverse: bool = False) -> tuple[Site, ...]

The unique sites, column-major (the fermionic order).

bonds

bonds(
    dirn: str | None = None, reverse: bool = False
) -> tuple[Bond, ...]

The unique nearest-neighbour bonds.

Parameters:

Name Type Description Default
dirn str or None

'h' for horizontal, 'v' for vertical, None for horizontal followed by vertical.

None
reverse bool

Reverse the sequence (and, for None, the two groups as well).

False

Returns:

Type Description
tuple[Bond, ...]

Each bond with site0 before site1 in the fermionic order.

nn_site

nn_site(
    site: Site | tuple[int, int] | None,
    d: str | tuple[int, int],
) -> Site | None

The site reached from site by the shift d, or None if there is none.

Parameters:

Name Type Description Default
site Site or tuple[int, int] or None

The starting site; None propagates.

required
d str or tuple[int, int]

One of 't', 'b', 'l', 'r', 'tl', 'tr', 'bl', 'br', or an explicit (dx, dy).

required

Returns:

Type Description
Site or None

None when the shift leaves an open boundary. On an infinite axis the site is returned unfolded; on a periodic axis it wraps.

nn_bond_dirn

nn_bond_dirn(s0: Any, s1: Any = None) -> str

'lr', 'tb', 'rl' or 'bt' for a nearest-neighbour pair.

Raises:

Type Description
ValueError

If the two sites are not nearest neighbours.

f_ordered

f_ordered(s0: Any, s1: Any) -> bool

Are s0 and s1 in the fermionic order (column-major), or identical?

Left before right, and within a column top before bottom -- the order bonds already orients every bond by.

site2index

site2index(site: Any) -> Any

Fold any site of the plane onto the key of the unique tensor that sits there.

MPO

MPO(
    sites: Iterable[SymmetricTensor] | None = None,
    *,
    edges: EdgeTable | None = None,
)

A finite MPO: one rank-4 SymmetricTensor per site, (wl IN, p OUT, p IN, wr OUT).

Parameters:

Name Type Description Default
sites Iterable of SymmetricTensor or None

The rank-4 site tensors, left to right. Exactly one of sites and edges is given.

None
edges EdgeTable or None

The edge description the builders keep under symbolic=True -- the finite-state machine at cutoff=None, the compressed sites and their per-cut slabs at a float cutoff -- from which sites and block tables come on request; None for every other MPO, including every builder's default. Keyword-only.

None

Raises:

Type Description
ValueError

If neither or both of sites and edges are given.

Notes

Invariance reads q(p_out) + q(wr) = q(wl) + q(p_in). The first and last sites carry a D=1 boundary MPO bond, which is what makes every W_n rank 4 and removes the boundary-vector special case.

A separate class from MPS, with no shape flag.

Two internal representations, and edges is the description. Given an EdgeTable the container may hold no tensor at all: self[n] materialises site n on request and caches it, and edge_blocks does the same for the site's block table, so an MPO whose only consumer is the prepared two-site matvec never allocates a full-width rank-4 W. A compressed description already holds its sites -- the two truncating sweeps built them -- and answers both accessors off those. Given site tensors and no description the container holds exactly those and edge_blocks is None throughout: from_w's numeric path and to_dense need the sites, so both representations live in one class.

edges and edge_blocks are the two read-only accessors beyond the container protocol, and they exist so that Env can reach the symbolic structure without touching a private name.

sites property

sites: list[SymmetricTensor]

The site tensors, materialising every one that is not built yet.

edge_blocks

edge_blocks(n: int) -> EdgeBlocks | None

Site n's EdgeBlocks, or None when no table survived.

Parameters:

Name Type Description Default
n int

The site.

required

Returns:

Type Description
EdgeBlocks or None

The site's block table, or None when the MPO carries none.

Notes

Every operator built symbolic=True carries a table, at either cutoff: both compressing sweeps pin the two corner channels, so a compressed bond still decomposes as IdL (+) open (+) IdR -- one open state per cut rather than one per open string. from_w carries no description and returns None, which routes Env.heff2 onto its site-tensor path, as do the three builders at their default and materialize.

The table is built here rather than stored here: the call goes through to EdgeTable.edge_blocks, which places one site's blocks against the group slot maps and caches them. No full-width site tensor is involved, which is what lets a Hamiltonian be assembled and swept without one ever existing.

materialize

materialize() -> MPO

The same operator as plain site tensors, with the edge description dropped.

Returns:

Type Description
MPO

An MPO holding this one's rank-4 site tensors and no edges, so Env.heff2 takes its site-tensor path. Called on an MPO that already carries no description, it copies the container.

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import MPO, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> sz = local_op(np.diag([-0.5, 0.5]), phys=phys, charge=U1Sector(0))
>>> terms = [(1.0, [(sz, i), (sz, i + 1)]) for i in range(2)]
>>> h = MPO.from_terms(3, terms, symbolic=True)
>>> h.edges is not None, h.materialize().edges is None
(True, True)
Notes

The route from the symbolic representation to the numeric one. The three builders return site tensors by default, so a lattice model needs nothing here; this is for an operator built symbolic=True that a caller later wants on the site-tensor path. from_terms applies it internally at its own default.

What is given up is stated in heff2_families: a materialized operator has no term families, so sweep_'s noise_type="perturbative" falls back to the single-vector mixer. An operator that wants the family-resolved mixer does not call this.

Nothing is recomputed that a consumer would not have asked for anyway: sites materialises each site once through the description's own door and the new container holds those tensors.

identity classmethod

identity(n_sites: int, phys: GradedSpace) -> MPO

The identity operator as an MPO: D=1 bonds, eye on every site.

Parameters:

Name Type Description Default
n_sites int

Chain length.

required
phys GradedSpace

The physical space of every site.

required

Returns:

Type Description
MPO

An n_sites-site MPO with unit-sector D=1 bonds throughout and no edge description, so every consumer takes the full-contraction path.

Examples:

>>> from tenet import GradedSpace
>>> from tenet.network import MPO, MPS, Env
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1)]).canonize_()
>>> phi = MPS.product(phys, [U1Sector(1), U1Sector(-1)]).canonize_()
>>> round(Env(psi, MPO.identity(2, phys), bra=phi).measure(), 12)
1.0
Notes

It exists because an overlap is an environment: <phi|psi> and its per-bond projection vectors are what a two-state Env over this operator produces, which is exactly how block2 builds the ext_mes its excited-state projection reads -- impo = self.get_identity_mpo() (pyblock2/driver/core.py:4817-4830). One operator spelled once beats a second environment class whose contractions would be this one's with a leg deleted.

Deliberately carries no EdgeTable: the prepared matvec's one-sided terms are a gauge statement about a state against itself, and this operator's only caller is the two-state path where that statement is false. The site tensor is tenet.identity on (unit, phys) transposed into the MPO's (wl IN, p OUT, p IN, wr OUT) axis order -- no einsum, so no composition rule to state.

from_w classmethod

from_w(
    w: Any,
    n_sites: int,
    *,
    phys: GradedSpace,
    bond: GradedSpace,
    boundary: GradedSpace,
    start: int,
    end: int,
) -> MPO

One dense bulk W plus a graded MPO bond -> first / bulk / last.

Parameters:

Name Type Description Default
w array_like

The dense bulk tensor, indexed [wl, p_out, p_in, wr].

required
n_sites int

Chain length.

required
phys GradedSpace

The physical space. Keyword-only, as are all the following.

required
bond GradedSpace

The graded MPO bond of the bulk.

required
boundary GradedSpace

The D=1 space of the two boundary legs.

required
start int

The row of w the first site keeps.

required
end int

The column of w the last site keeps.

required

Returns:

Type Description
MPO

[first, bulk * (n_sites - 2), last], no edge-block table.

Raises:

Type Description
ValueError

From from_dense at its default relative atol: a wrong grading raises rather than projecting (see Notes).

Notes

w is indexed [wl, p_out, p_in, wr]. The first site keeps only row start and the last only column end, each on a D=1 boundary MPO leg.

SymmetricTensor.from_dense is called at its default relative atol (src/tenet/ops/dense.py:301), so a wrong grading raises rather than projecting -- and that refusal is the proof the grading is right in a way a passing allclose is not.

The builder that shows what an MPO is, and the one a reader needs before they can debug one. from_terms is the other route; it is not a replacement, and neither is deprecated or aliased to the other.

from_entries classmethod

from_entries(
    entries: Iterable[Mapping[tuple[int, int], Any]],
    *,
    symbolic: bool = False,
) -> MPO

The non-zero (i, j) entries of each site's W, as a graded MPO with symbols.

Parameters:

Name Type Description Default
entries Iterable of Mapping

One mapping per site, left to right, from the (row, column) index pair of that site's W to what sits there. 0 is the IdL channel of a bond and -1 its IdR channel, at every bond (see Notes); the open channels are 1, 2, ... and need not be contiguous. Chain length is len(entries), so a uniform bulk is [w] * n_sites. An entry is

  • None -- the identity, which on (i, i) is a spectator ride;
  • a number c -- c times the identity;
  • a rank-3 charge-leg operator from local_op;
  • the pair (c, op) -- c times that operator.
required
symbolic bool

Keep the finite-state-machine description, so Env.heff2 runs the term-family matvec on the prepared path. Default False: the builder returns the site tensors, which is what a finite-range lattice model wants. Keyword-only.

False

Returns:

Type Description
MPO

The assembled operator as rank-4 site tensors, so it takes Env.heff2's site-tensor path. With symbolic=True it carries the per-site edge_blocks table instead and takes the prepared, symbolic path; see Notes.

Raises:

Type Description
ValueError

If entries is empty, or every entry is an identity (the physical space is read off an operator); if an entry is none of the four forms above, or holds local_op's invariant rank-2k operator, which spans k sites and has nowhere to put them in one site's W; if the operators disagree about the physical space; if a key is not a pair of int\ s, or names a bond index below -1, or enters IdL ((i, 0) with i != 0) or leaves IdR ((-1, j) with j != -1); if (0, 0) or (-1, -1) holds anything but the identity; if two entries reach one bond state with different charges; if a term closing into IdR has not brought the bond charge back to the unit sector; or if an interior bond state is dead -- unreachable from IdL or unable to reach IdR.

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import MPO, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> sp = np.array([[0.0, 0.0], [1.0, 0.0]])
>>> opp = local_op(sp, phys=phys, charge=U1Sector(-2))
>>> opm = local_op(sp.T, phys=phys, charge=U1Sector(2))
>>> w = {  # the 3-site XY chain's bulk W, written as the textbook prints it
...     (0, 0): None,
...     (0, 1): (0.5, opp),
...     (1, -1): opm,
...     (0, 2): (0.5, opm),
...     (2, -1): opp,
...     (-1, -1): None,
... }
>>> h = MPO.from_entries([w] * 3)
>>> len(h), h.to_dense().shape, h.edges is None
(3, (8, 8), True)
Notes

The hand-build entry that derives its bonds. from_w takes a fully-formed rank-4 W, so its caller writes every zero of the finite-state machine out, gets four legs and a dual convention right, and hands over a grading by hand -- and what comes back is numeric, with no edge description, so it routes onto Env.heff2's site-tensor path. This builder takes the same W, named entry by entry, and produces an EdgeTable: the very object from_terms produces, indistinguishable to Env and routed exactly as that one is. The bond spaces are derived, never declared -- the charge is on the operator's third leg, each state's GradedSpace is the running fused charge, and the bond at a cut is the direct sum over its states -- so there is no grading argument and no dual convention to state. from_w is unchanged and is not deprecated: it is the entry for a W that arrives as a dense array (a paper, another library), where the entries are numbers and no charge can be recovered from them.

IdL is index 0 and IdR is index -1, by convention, at every bond. MPSKit fixes the same two by position -- V[1] = V[end] = _rightunit in mpohamiltonian.jl, the (1 C D; . A B; . . 1) partition EdgeBlocks implements -- and tenet's own bond layout already assumes it: _merge direct-sums a cut in [_IDL, *open, _IDR] order and the pinned sweeps read the two corners off the first and last slot of the unit sector. TenPy carries IdL/IdR explicitly alongside its W list because its MPOGraph keys are arbitrary hashables with no order to lean on; here an explicit pair would be a second source of truth that _merge could contradict. Python's -1 is what makes the convention cost nothing: no bond width is ever declared or inferred, because the last index needs no width to name. The convention is then made self-enforcing rather than assumed -- an entry into IdL or out of IdR, or a non-identity on either corner, is refused by name, which is the same four zeros the corner-exactness property asserts.

The two boundary bonds are D=1, so bond 0 keeps only its IdL channel and bond len(entries) only its IdR one. Everything else at those two bonds is dropped silently -- that is exactly from_w's start row and end column, and it is what lets one bulk W be handed over for every site including the first and the last. Only interior dead states raise.

Every operator is rank 3, for from_arrays's reason: one W entry sits on one site, and local_op's invariant k-site form spans k sites through an SVD, so it is refused with a pointer to from_terms. YASTN is the third reference and its contribution is a negative one: between a fully formed tensor (A[n] = t) and a term list (Hterm, generate_mpo) it offers nothing at all, which is the gap this builder fills.

No compressing sweep runs and there is no cutoff: the caller wrote the bond, so there is nothing combinatorial to cut down. from_terms' sweeps exist because its finite-state machine is built from a term list and can be numerically low-rank -- a power law, an integral file -- which is a property of the term list, not of a W somebody sat down and wrote. An operator that wants the sweeps wants from_terms, which is where its cutoff lives. Outside jit/grad like the rest of this module, because the assembly decides GradedSpace\ s.

Which engine path this operator takes, and how a caller chooses. By default this builder hands back the site tensors: the description is what produces them and is then dropped, so Env.heff2 takes its site-tensor contraction. That is the path a finite-range lattice model wants -- at D_w of order ten the prepared machinery's per-bond cores and structure-keyed cache cost more per sweep than they buy back. symbolic=True keeps the description and routes heff2 onto the prepared, symbolic term-family matvec, which is what quantum chemistry wants: O(K^4) terms over a bond in the thousands, where the prepared path is the faster of the two and the only one that fits a large orbital count in memory at all. Nothing dispatches at run time; the caller states it here, at build time. An operator built symbolic=True moves to the site-tensor path afterwards with materialize.

from_terms classmethod

from_terms(
    n_sites: int,
    terms: Iterable,
    *,
    cutoff: float | None = 1e-13,
    symbolic: bool = False,
) -> MPO

A term list [(coeff, [(op, sites), ...]), ...] as a graded MPO.

Parameters:

Name Type Description Default
n_sites int

Chain length.

required
terms Iterable

[(coeff, [(op, sites), ...]), ...]: each op comes from local_op in one of its two forms, and sites is an int or a tuple of that many site indices -- i and (i,) are the same thing.

required
cutoff float or None

The two compressing SVD sweeps' cutoff. 0.0 keeps every singular value; None skips both sweeps entirely and keeps the finite-state machine's block table (see Notes for the three-way behaviour and what None does not affect). Default 1e-13. Keyword-only.

1e-13
symbolic bool

Keep the finite-state-machine description, so Env.heff2 runs the term-family matvec on the prepared path. Default False: the builder returns the site tensors, which is what a finite-range lattice model wants. Keyword-only.

False

Returns:

Type Description
MPO

The assembled operator as rank-4 site tensors, so it takes Env.heff2's site-tensor path. With symbolic=True it carries the per-site edge_blocks table instead and takes the prepared, symbolic path; see Notes.

Raises:

Type Description
ValueError

If terms is empty (the physical space is read off an operator); if an operator is neither rank 3 nor rank 2k, sits on the wrong number of sites, leaves range(n_sites), or shares a site with another operator of its term; if the operators disagree about the physical space; if two k-site operators of one term interleave; if a term's operator charges do not sum to the unit sector (both MPO boundaries are the trivial D=1 leg, so a charged term has nowhere to end); if a charge leg carries more than one sector at degeneracy 1; or if a non-Abelian term is spelled as a list of charge-leg operators -- the DSL has no slot for a coupling tree, and the message says to hand the whole term over as one invariant k-site operator instead.

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import MPO, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> sp = np.array([[0.0, 0.0], [1.0, 0.0]])
>>> opp = local_op(sp, phys=phys, charge=U1Sector(-2))
>>> opm = local_op(sp.T, phys=phys, charge=U1Sector(2))
>>> terms = []  # the 3-site XY chain
>>> for i in range(2):
...     terms.append((0.5, [(opp, i), (opm, i + 1)]))
...     terms.append((0.5, [(opm, i), (opp, i + 1)]))
>>> h = MPO.from_terms(3, terms)
>>> len(h), h.to_dense().shape
(3, (8, 8))
Notes

The dispatch is on op.ndim alone:

  • rank 3, the charge-leg form, one site, Abelian only;
  • rank 2k, an invariant k-site term on k sites, any symmetry. tenet.linalg.svd_truncated peels it into k MPO tensors and the aux bond it runs through is the one the SVD found, so a non-Abelian term needs no coupling tree and no multiplicity label: both live inside the operator's own blocks. The sites need not be adjacent -- the derived bond, graded or not, runs through the identities on the sites in between. k = 1 is a one-site term: no cut is made, so the operator rides the identity channel between two D=1 unit bonds. It is the form an on-site U, a chemical potential or a field takes on every grading, and the only one available where irrep_dim > 1, since the charge-leg form's emitted leg has to be D=1 dense.

A term is a coefficient and a list of (operator, sites) pairs, with identities implied on every untouched site. The sum is assembled symbolically, as a finite-state machine over labelled bond states: identity-left, identity-right, and one state per distinct open left-partial-string, so terms that share an opening share a state and the closing edge carries the coefficient. States unreachable from the left identity or unable to reach the right one are pruned, each edge is placed as one rank-4 tensor, and a site is the plain sum of its edges. The bond handed to the compressing sweep is therefore the FSM's — one state per open string — never one channel per term.

The MPO bond spaces are derived, never declared. Charges enter once, as local_op's charge; from there every FSM state carries its own space — the running fused charge of the rank-3 operators to its left, or the graded Leg a k-site operator's internal SVD produced — and the bond at each cut is the direct sum of its states' spaces. There is no place left to write a wrong grading down, and nothing re-decides the grading afterwards.

cutoff controls the two compressing SVD sweeps that run after assembly (right to left, then left to right), taking the FSM bond down to the operator Schmidt rank -- worth it exactly for couplings with numerical low rank the graph cannot see, such as power laws. cutoff=0.0 keeps every singular value; cutoff=None skips both sweeps entirely: the MPO is the finite-state machine, its bond dimension is combinatorial, and no floating-point tolerance participates in the assembly. A k-site operator's internal SVD -- the one that peels it into k tensors -- is a different SVD and is unaffected by cutoff=None; it runs at the default 1e-13 in that case.

cutoff is the regime knob, and it is a build-time choice rather than a hidden runtime one, and it is orthogonal to symbolic. Under symbolic=True both settings keep the block table edge_blocks exposes, so both reach Env.heff2's prepared path, because the sweeps pin the IdL/IdR channels through their SVDs instead of rotating them away. What cutoff decides either way is the operator the engine runs on:

  • cutoff=None for a finite-range lattice model. The compressing sweeps reduce its bond by exactly nothing, and the finite-state machine keeps its identity channels separable, so every spectator site rides a rank-2 map with no W contraction, which is the cheaper sweep.
  • a float cutoff for power-law couplings and ab initio integrals, where the sweep takes the bond down by orders of magnitude and the operator does not fit otherwise. The rotation mixes the open states, so no spectator separates any more and every open state is operator-carrying; that is a real constant factor and it is the same uniform mechanism block2 uses, which carries its identity as an ordinary entry in its operator map.

The default is 1e-13.

Which engine path this operator takes, and how a caller chooses. By default this builder hands back the site tensors: the description is what produces them and is then dropped, so Env.heff2 takes its site-tensor contraction. That is the path a finite-range lattice model wants -- at D_w of order ten the prepared machinery's per-bond cores and structure-keyed cache cost more per sweep than they buy back. symbolic=True keeps the description and routes heff2 onto the prepared, symbolic term-family matvec, which is what quantum chemistry wants: O(K^4) terms over a bond in the thousands, where the prepared path is the faster of the two and the only one that fits a large orbital count in memory at all. Nothing dispatches at run time; the caller states it here, at build time, exactly as cutoff does. An operator built symbolic=True moves to the site-tensor path afterwards with materialize.

symbolic and cutoff are independent. cutoff decides whether the operator is compressed; symbolic decides whether the description is kept. cutoff=None with the default therefore yields exact, uncompressed site tensors -- on a finite-range lattice model the minimal bond anyway.

There is no phys= argument: the operators carry the physical space and a second source of truth could disagree with them, which would surface as a structure error instead of a message about phys. Uniform physical space only, and every term's charges must sum to the unit sector.

Fermionic terms build like any other graded terms, and the braided route needs no Jordan-Wigner operator in the API: an odd FSM bond crossing a physical line is the string, paid by the Koszul braiding under the package's composition rule. Two conventions follow from that. A term's operator list is the ordered product of its operators -- [(c, i), (c+, j)] is c_i c+_j, which for i != j is -c+_j c_i -- with the reordering-to-site-order sign paid on the coefficient. And intra-site ordering for a multi-flavour site (spinful d=4) is a property of the on-site matrices, documented where they are defined, not of this assembler. Non-Abelian terms spelled as a list of charge-leg operators are refused with a message rather than accepted. Outside jit/grad like the rest of this module, because the assembly decides GradedSpace\ s.

from_arrays classmethod

from_arrays(
    n_sites: int,
    ops: Mapping[str, SymmetricTensor],
    blocks: Iterable[tuple[str, Any, Any]],
    *,
    cutoff: float | None = 1e-13,
    screen: float = 1e-12,
    symbolic: bool = False,
) -> MPO

Blocks of terms as arrays -- (expr, indices, data) -- as a graded MPO.

Parameters:

Name Type Description Default
n_sites int

Chain length.

required
ops Mapping of str to SymmetricTensor

The caller's operator table: a name to the rank-3 charge-leg form of local_op. The names are what an expr spells; nothing is registered anywhere and operator identity stays object identity.

required
blocks Iterable

One (expr, indices, data) triple per operator pattern. expr is a string of names -- whitespace-separated, or one name per character when it holds no whitespace. indices is an integer array of shape (T, L) with L == len(expr) naming the site of each operator (a 1-D array of T * L entries is reshaped). data is the length-T array of coefficients, and its dtype decides the MPO's.

required
cutoff float or None

The compressing SVD sweeps' cutoff, with from_terms's three-way meaning unchanged. Default 1e-13. Keyword-only.

1e-13
screen float

Coefficient magnitude threshold, applied after the merge: a merged term survives when abs(coeff) > screen. Default 1e-12. Keyword-only.

1e-12
symbolic bool

Keep the finite-state-machine description, so Env.heff2 runs the term-family matvec on the prepared path. Default False: the builder returns the site tensors, which is what a finite-range lattice model wants. Keyword-only.

False

Returns:

Type Description
MPO

The assembled operator as rank-4 site tensors, so it takes Env.heff2's site-tensor path. With symbolic=True it carries the per-site edge_blocks table instead and takes the prepared, symbolic path; see Notes.

Raises:

Type Description
ValueError

If an entry of ops is not rank 3, or the operators disagree about the physical space, or a charge leg is non-Abelian (the checks from_terms makes); if ops is empty; if a block's expr names an operator the table does not define, or names none at all; if data is not 1-D, or len(expr) * len(data) != indices.size; if a site index leaves range(n_sites); or if no term survives the merge and the screen.

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import MPO, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> sp = np.array([[0.0, 0.0], [1.0, 0.0]])
>>> ops = {
...     "+": local_op(sp, phys=phys, charge=U1Sector(-2)),
...     "-": local_op(sp.T, phys=phys, charge=U1Sector(2)),
... }
>>> bonds = np.array([[0, 1], [1, 2]])  # the 3-site XY chain, two blocks
>>> blocks = [("+-", bonds, np.full(2, 0.5)), ("-+", bonds, np.full(2, 0.5))]
>>> h = MPO.from_arrays(3, ops, blocks)
>>> len(h), h.to_dense().shape
(3, (8, 8))
Notes

The same operator from_terms builds, from the input shape block2 uses (integral_general.hpp:45-57): three parallel arrays per operator pattern, transposed into a triple because Python has no reason to keep them apart. It exists for the input where the term count is the wall -- an ab initio Hamiltonian is O(K^4) terms over a handful of patterns -- and the difference is that the pattern's work is done once per block in numpy instead of once per term in Python. from_terms' list is the right shape for a lattice model and is unchanged; neither is deprecated or aliased to the other.

Every operator is rank 3. A block gives one site index per name, so local_op's invariant k-site form -- which spans k sites through an SVD -- has nowhere to put its extra indices and is refused with a message pointing at from_terms. The MPO bond spaces are still derived and never declared, and the assembler is the same finite-state machine walk; only the way terms arrive is new.

Three things happen before the walk, and all three are whole-array work:

  • each row is sorted into site order by a stable argsort, paying the Koszul sign of every inversion of two sign-braiding operators -- the same strict-> rule from_terms applies to a term's operator list, so the two spellings of one fermionic term agree by construction;
  • operators that coincide on a site are pre-multiplied into one on-site operator, cached per run of names, and a term whose on-site product vanishes (c c on one site, say) is dropped. This is the burden from_terms refuses with "two operators of one term sit on site N; multiply them first";
  • terms agreeing on (operator labels, sites) are fused, their coefficients summed. Permutational symmetry is therefore expanded by the caller and merged here, which is block2's own order and the only correct one: the eight images of (ij|kl) are eight different operator strings, so folding the orbit into one coefficient builds a different operator.

screen runs on what the merge leaves, which is the only position that can see a cancellation, and it is one knob where block2 has four. At its default it removes the symmetry-forbidden ~1e-15 entries a real integral file carries and nothing else; it is an accuracy/size trade the caller can take deliberately at 1e-4 and above, not a performance lever.

Which engine path this operator takes, and how a caller chooses. By default this builder hands back the site tensors: the description is what produces them and is then dropped, so Env.heff2 takes its site-tensor contraction. That is the path a finite-range lattice model wants -- at D_w of order ten the prepared machinery's per-bond cores and structure-keyed cache cost more per sweep than they buy back. symbolic=True keeps the description and routes heff2 onto the prepared, symbolic term-family matvec, which is what quantum chemistry wants: O(K^4) terms over a bond in the thousands, where the prepared path is the faster of the two and the only one that fits a large orbital count in memory at all. Nothing dispatches at run time; the caller states it here, at build time, exactly as cutoff does. An operator built symbolic=True moves to the site-tensor path afterwards with materialize.

symbolic and cutoff are independent. cutoff decides whether the operator is compressed; symbolic decides whether the description is kept. cutoff=None with the default therefore yields exact, uncompressed site tensors -- on a finite-range lattice model the minimal bond anyway.

This builder's shape is the ab initio one, but it is not an ab initio front end only: docs/guide/models-and-sites.md teaches it for lattice models too, because a Site's ops is exactly the table it takes. That is why the default here is the lattice one, the same as the other two builders' -- the representation follows the model, not the builder that was called.

to_dense

to_dense() -> Any

The full d**N x d**N operator, D=1 boundaries dropped.

Returns:

Type Description
array

The backend's dense matrix of shape (d**N, d**N).

Notes

MPS.to_dense's twin, with its warning: exponential in N, an oracle exit for tests, and nothing an algorithm calls.

apply

apply(psi: MPS) -> MPS

H|psi> as a new MPS, untruncated; psi is untouched.

Parameters:

Name Type Description Default
psi MPS

The state; any gauge, any norm. Not modified -- the product is built from its frozen tensors into a new container.

required

Returns:

Type Description
MPS

The product state, on the site convention MPS.__setitem__ enforces. Its bond at every cut is the fusion of this operator's bond with psi's, so the bond dimension is the product of the two and the result is exact.

Raises:

Type Description
ValueError

If the operator and the state have different lengths.

Examples:

>>> from tenet import GradedSpace
>>> from tenet.network import MPO, MPS, overlap
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1), U1Sector(1)])
>>> ident = MPO.identity(3, phys)
>>> round(overlap(psi, ident.apply(psi)), 12)
1.0
Notes

Truncation is not hidden here and is not a keyword: it is MPS.compress_, by name.

phi = h.apply(psi)
discarded = phi.compress_(chi=64, cutoff=1e-12)

That call already takes the chi/cutoff pair Sweep and the sweep take and already returns the total discarded weight sqrt(sum_bond dw), which is the convention this question wants and which sweep_'s per-bond maximum deliberately is not. Giving apply its own chi= would put a second name on that number and be the one place the two conventions could blur. Simplification: the untruncated product costs D_w times psi's bond, so for a wide operator compress promptly rather than holding the product; the zip-up apply that truncates during the sweep (YASTN's zipper, TenPy's apply_zipup) is the named upgrade and is a change with a measurement attached.

A deferred operator is materialised, site by site, through MPO.__getitem__ -- so an MPO built at cutoff=None, which carries an edge description and no tensors, pays one full W per site here. That is stated rather than avoided: this is a whole-state product, not a sweep step, and there is no bond at which a symbolic operator could be kept symbolic. The sweep's own path is untouched.

The virtual leg is turned around once, and that is the whole graded content. The operator's bond and the state's bond cross a site in opposite directions -- so they cannot be fused until one of them is turned. Turning the operator's left virtual leg is a duality relabel, tenet.flip_dual, which charges chi * theta per fusion tree: +1 on every bosonic sector and -1 on an odd fermionic one, which is exactly the sign that is missing if the fusion is written without it. The direction is fixed by the leg rather than by the flag: inv=not dual charges the same categorical map whether the leg was written dual or plain, and it has to be, because from_terms's two representations write that flag differently -- compressed bonds come back dual, a deferred table's do not. Charging by the flag instead would make H|psi> depend on which representation built H, silently and only for fermions. Both are tested against the dense oracle, under fermionic parity and under SU(2).

variance

variance(psi: MPS) -> float

<psi|H^2|psi> / <psi|psi> - E**2 -- the convergence check that is not a change test.

Parameters:

Name Type Description Default
psi MPS

The state, normally a converged DMRG_out's psi; any gauge, any norm, and not modified.

required

Returns:

Type Description
float

The energy variance. Zero for an exact eigenstate, and it falls as chi grows for a state that is converging on one.

Examples:

>>> from tenet import GradedSpace
>>> from tenet.network import MPO, MPS
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1)])
>>> round(MPO.identity(2, phys).variance(psi), 12)  # every state is an eigenstate of 1
0.0
Notes

TenPy names the same quantity MPO.variance(psi, exp_val=None). dmrg_'s own convergence test (network/dmrg.py) is a change test -- the energy stopped moving and the Schmidt values stopped moving -- and both references say plainly that a change test can be satisfied by a run stuck on a wrong bond structure. This is the check that is not a change test, and docs/tutorials/dmrg.md shows it beside the convergence discussion.

One line over apply and overlap, and no MPO @ MPO. With |Hpsi> = H|psi> exact, <psi|H^2|psi> is <Hpsi|Hpsi> and <psi|H|psi> is <psi|Hpsi> -- three overlaps and one product. Expanding H**2 as a term list would be quadratic in the term count and would ask the caller to multiply every operator pair by hand; there is no operator algebra here.

The product is untruncated, so this is the variance of psi under the exact H and not of a compressed approximation to it; the cost is one state of bond D_w times psi's. <psi|H|psi> read this way agrees with Env.measure to solver precision, which is tested and is the statement that the apply is the operator it claims to be.

MPS

MPS(
    sites: Iterable[SymmetricTensor],
    center: int | None = None,
)

A finite open-boundary MPS: a mutable list of frozen SymmetricTensors.

Parameters:

Name Type Description Default
sites Iterable of SymmetricTensor

The site tensors, each passed through the __setitem__ write barrier onto (l OUT, p OUT | r IN).

required
center int or None

The orthogonality centre; None (the default) means "no claim made".

None

Examples:

>>> from tenet import GradedSpace
>>> from tenet.network import MPS
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1), U1Sector(1)])
>>> len(psi)
3
>>> round(psi.norm(), 6)
1.0
Notes

Site convention, pinned here once and enforced on every write::

A_n : (left bond OUT, physical OUT, right bond IN)

Charge flows left to right, bond_n (x) phys_n -> bond_{n+1}, and both end bonds have D=1; a non-unit sector on bond 0 targets that total charge.

A mutable container. The immutability rule protects categorical objects -- Leg, GradedSpace, TensorStructure, SymmetricTensor -- whose identity is their metadata, and every tensor this class holds is still frozen. An MPS is a container of those plus an orthogonality centre that moves. In-place methods therefore carry a trailing underscore (canonize_), so that a mutation such as env.clear_(n, n + 1) reads as one at the call site.

center is one int | None, None meaning "no claim made": a single orthogonality centre rather than a per-site form table or a separate central block.

copy

copy() -> MPS

A new container over the same frozen tensors.

Returns:

Type Description
MPS

A fresh MPS holding the same tensors and center.

random classmethod

random(
    phys: GradedSpace,
    bonds: Sequence[GradedSpace],
    *,
    seed: int = 0,
) -> MPS

A random MPS on len(bonds) - 1 sites over the given bond spaces.

Parameters:

Name Type Description Default
phys GradedSpace

The physical space of every site.

required
bonds Sequence of GradedSpace

The n_sites + 1 virtual spaces, both ends included.

required
seed int

Site i draws with seed + i. Default 0. Keyword-only.

0

Returns:

Type Description
MPS

A random state with center=None.

Notes

The library takes bond spaces; deciding which are reachable for a given symmetry and target charge is physics and stays in the caller (examples/toy_codes/dmrg.py::bond_spaces).

product classmethod

product(phys: GradedSpace, states: Sequence[Sector]) -> MPS

A product state: one physical sector per site, bonds derived rather than declared.

Parameters:

Name Type Description Default
phys GradedSpace

The physical space of every site.

required
states Sequence of Sector

One sector of phys per site, each at degeneracy 1, naming the basis vector that site carries.

required

Returns:

Type Description
MPS

A norm-1 product state whose bond 0 carries the total charge.

Raises:

Type Description
ValueError

If a sector is not in phys; if a sector has degeneracy > 1 in phys -- this constructor has no slot for the degeneracy index, so seed with MPS.random and a bond profile whose boundary leg carries the target charge; or if a fusion along the backwards bond derivation has more than one channel -- the constructor is Abelian-only and refuses rather than picking a channel.

Examples:

>>> from tenet import GradedSpace
>>> from tenet.network import MPS
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(1)])
>>> psi[0].legs[0].space.sectors  # bond 0 carries the total charge
((U1Sector(charge=2), 1),)
>>> round(psi.norm(), 6)
1.0
Notes

states[n] names the basis vector site n carries -- a sector of phys at degeneracy 1 -- and the bond spaces fall out of the charges: bond len(states) is the unit sector and the loop runs backwards through the provider's dual, so bond 0 carries the total charge, which is where the target-sector statement lives (YASTN's charged-first-virtual-leg recipe, _initialize.py:194). psi[0].legs[0].space is then printable and assertable: on the U(1) spin chain a D=1 boundary leg carrying U1Sector(q) targets S^z_tot = q/2 (the leg is dual there when q is non-unit, which is what puts the recipe's sign on the space). The result has norm 1 and a single dense amplitude of exactly 1.0.

Abelian-only, by construction and permanently: when a fusion has more than one channel the bonds are not determined -- a single sector is not a non-Abelian multiplet -- and the constructor refuses rather than picking a channel. To target a sector under a non-Abelian symmetry, seed with MPS.random and put the target on a charged D=1 boundary leg of bond 0.

from_tensors classmethod

from_tensors(tensors: Iterable[SymmetricTensor]) -> MPS

An MPS over already-built site tensors, each through the write barrier.

Parameters:

Name Type Description Default
tensors Iterable of SymmetricTensor

The rank-3 site tensors, left to right.

required

Returns:

Type Description
MPS

The state, with center=None.

canonize_

canonize_(to: int = 0) -> MPS

Right-canonicalize in place and return self -- YASTN canonize_(to='first').

Parameters:

Name Type Description Default
to int

The target centre. Only 0 (fully right-canonical) is supported. Default 0.

0

Returns:

Type Description
MPS

self, normalized, with center = to.

Raises:

Type Description
NotImplementedError

If to is not 0.

Notes

One tenet.linalg.lq per site from the right, mirroring orthogonalize_site_ (_mps_obc.py:245-300): A_n = L . Q with Q on the MPS convention and L absorbed into A_{n-1}. lq rather than qr because qr would put the new bond on the right of the factor and leave the site tensor's left leg IN.

Setup only: a two-site sweep leaves the state canonical by construction on the side it came from, which is precisely what an int centre records.

norm

norm() -> float

sqrt(<psi|psi>) by one bra-ket transfer pass, closed with tenet.full_trace.

Returns:

Type Description
float

The 2-norm of the state.

Notes

No dense expansion and no environment object: two tenet.einsum calls per site, the same pairwise shape Env.update_ uses with the MPO row removed. Written through overlap, so the one-state and two-state readings of the same transfer pass cannot drift.

to_dense

to_dense() -> Any

The full d**N amplitude array, D=1 boundaries dropped.

Returns:

Type Description
array

The backend's dense amplitude array of shape (d,) * N.

Notes

Exponential in N: an oracle exit for tests, and nothing an algorithm calls.

compress_

compress_(*, chi: int, cutoff: float = 0.0) -> float

Truncate to bond chi in place; return the total discarded weight.

Parameters:

Name Type Description Default
chi int

The bond-dimension cap handed to svd_truncated at every bond. Keyword-only.

required
cutoff float

The singular-value cutoff handed to the same SVD. Default 0.0. Keyword-only.

0.0

Returns:

Type Description
float

sqrt(sum_bond dw), the total discarded weight of the sweep.

Notes

canonize_ then one left-to-right svd_truncated sweep -- the per-bond body of sweep_ with the eigensolver removed -- leaving center = len(self) - 1. YASTN's truncate_ (_mps_obc.py:379-413) instead assumes a canonical input and takes to=; canonizing here costs an lq pass the caller usually needs anyway and removes the silently-wrong result on a non-canonical state.

The returned convention differs from sweep_'s on purpose. sweep_ returns the per-bond maximum, because it feeds a per-sweep convergence report where the worst bond is the diagnostic; this returns sqrt(sum_bond dw) (YASTN's "norm of the truncated elements normalized by the norm of the untruncated state"), because its caller is asking how much of the state it just threw away. Two conventions, two names -- which is the mitigation.

schmidt_values

schmidt_values() -> dict[int, list[float]]

The Schmidt values across every bond, flattened and descending.

Returns:

Type Description
dict of int to list of float

One entry per internal bond, keyed by the bond's left site -- the key sweep_'s schmidt dict already uses -- holding that cut's values, sqrt(qdim)-weighted, normalized and sorted descending. A one-site state has no internal bond and gives {}. The SVD here is the exact tenet.linalg.svd, so a sector the bond carries but the state does not occupy contributes an explicit 0.0 rather than being dropped the way a truncating sweep drops it.

Examples:

>>> from tenet import GradedSpace
>>> from tenet.network import MPS
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1), U1Sector(1)])
>>> {n: [round(v, 6) for v in vals] for n, vals in psi.schmidt_values().items()}
{0: [1.0, 0.0], 1: [1.0, 0.0]}
Notes

A method on the state rather than an output of an algorithm: the sweep reports only how much these numbers moved, which is a convergence diagnostic and not an answer about the converged state.

A canonical copy is taken first, so this never re-gauges the state it reads and never reports the values of a non-canonical gauge. Each of the three readers here pays its own SVD sweep; a caller wanting two of them on a large state should keep the first result rather than call twice.

schmidt_sectors

schmidt_sectors() -> dict[int, dict[Sector, list[float]]]

schmidt_values, resolved by symmetry sector.

Returns:

Type Description
dict of int to (dict of Sector to list of float)

One entry per internal bond, keyed by the bond's left site; each is that cut's spectrum split by coupled sector, values descending within a sector.

Examples:

>>> from tenet import GradedSpace
>>> from tenet.network import MPS
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1), U1Sector(1)])
>>> [(sector.charge, len(vals)) for sector, vals in psi.schmidt_sectors()[0].items()]
[(-2, 1), (0, 1)]
Notes

TenPy's entanglement_spectrum(by_charge=True), and the read a graded bond is for: which sector carries the entanglement is a question a flat list cannot answer and a labelled bond answers for free. The sqrt(qdim) weight is applied in spectrum_sectors and nowhere else.

entanglement_entropy

entanglement_entropy(
    *, alpha: float = 1.0
) -> dict[int, float]

The entanglement entropy across every bond, in nats.

Parameters:

Name Type Description Default
alpha float

The Renyi index handed to entropy. Default 1.0, the von Neumann entropy. Keyword-only.

1.0

Returns:

Type Description
dict of int to float

One entropy per internal bond, keyed by the bond's left site.

Examples:

>>> from tenet import GradedSpace
>>> from tenet.network import MPS
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1)])
>>> psi.entanglement_entropy()  # a product state is unentangled
{0: -0.0}
Notes

Nats, not bits: the convention is stated on entropy, which does the arithmetic, because the two references disagree about it. The multiplet weight that makes an SU(2) state agree with the same state under U(1) is stated there too.

TenPy's entanglement_entropy(n=1) and YASTN's get_entropy(alpha=1). Both return one value per cut including the two trivial boundary cuts; this returns the N - 1 internal bonds only, because a boundary cut of a finite open chain is zero by construction and the key here is a bond's left site, which a boundary cut does not have.

save

save(path: str | Path) -> None

Write a directory: one NNN.npz per site plus mps.json.

Parameters:

Name Type Description Default
path str or Path

The destination directory; created if absent.

required

Raises:

Type Description
FileExistsError

If path exists and is not empty -- refused before anything is written (see Notes for why).

Notes

Per-tensor through tenet.save, and that is the whole reason for the shape: tenet.load verifies the SU(2) and fermionic-parity coefficient gauges (serialize.py:196-206), so a serializer that bypassed it would be the one place gauge-mismatched coefficients could enter silently. A directory rather than a zip-of-npz because np.load refuses nesting.

mps.json carries exactly format (MPS_FORMAT_VERSION), n_sites and center; center=None is JSON null. Blocks save as NumPy whatever the backend, so MPS.load(...) then to_backend("jax") per site is the restore -- a device placement is not a property of a tensor.

A non-empty destination is refused before anything is written: writing an 8-site MPS over a 12-site directory would leave 008.npz onwards behind and the loader would then reject the result, destroying the previous good checkpoint and producing an unreadable new one.

load classmethod

load(path: str | Path) -> MPS

Read a directory written by save. NumPy blocks; structures exactly equal.

Parameters:

Name Type Description Default
path str or Path

The MPS directory.

required

Returns:

Type Description
MPS

The restored state, center included.

Raises:

Type Description
ValueError

If mps.json is missing or names a newer format than this tenet's MPS_FORMAT_VERSION; if the file set does not match n_sites; if center is neither null nor in range; or if two consecutive sites' bond spaces disagree -- a corrupt directory.

Notes

Per-tensor version, gauge, block-count and member checks stay tenet.load's job, unmodified. Only what the directory owns is added here: a present and readable mps.json, an exact file set, an in-range center, and consecutive sites whose bond spaces agree.

The neighbour-bond check lives here and deliberately not in __setitem__. A half-written directory is a corrupt file and this is the trust boundary; a sweeping MPS is transiently inconsistent by construction -- sweep_ writes psi[n + 1] = vh before psi[n] = u (dmrg.py:120-127) -- so the same check in the write barrier would fire on correct code. It is not to be "fixed" upward.

DoublePepsTensor

Bases: NamedTuple

One site of a bra-ket double layer, held as its two factors and never multiplied.

Attributes:

Name Type Description
ket SymmetricTensor

The rank-5 site tensor, (t IN, l OUT, b OUT, r IN, phys OUT).

bra SymmetricTensor

Its partner, same leg order with every side flipped -- tenet.adjoint(ket) unless a different bra was supplied.

Examples:

>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import DoublePepsTensor
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> legs = (Leg(V, IN), Leg(V, OUT), Leg(V, OUT), Leg(V, IN), Leg(V, OUT))
>>> a = SymmetricTensor.random(legs, seed=0)
>>> t = DoublePepsTensor(a, tenet.adjoint(a))
>>> t.ndim, len(t.legs)
(4, 4)
Notes

ndim is 4 and legs returns four pairs: the object behaves like the rank-4 tensor a CTM environment sees, while the twelve primitives in this module reach past it into ket and bra. YASTN's DoublePepsTensor is the same idea with a trans field restricting transposes to the leg-order-preserving ones; that field is not here because no caller transposes a double layer -- the primitives take the direction in their names.

ndim property

ndim: int

Four: the double layer hides the physical wire it already closed.

legs property

legs: tuple[tuple[Any, Any], ...]

((t_ket, t_bra), (l_ket, l_bra), (b_ket, b_bra), (r_ket, r_bra)).

Peps

Peps(geometry: Any, tensors: Any = None)

Bases: Lattice

A PEPS: one rank-5 (or rank-4) tensor per unique site of a lattice.

Parameters:

Name Type Description Default
geometry SquareLattice or Lattice

The lattice the state lives on.

required
tensors optional

One tensor, a nested sequence, or a {site: tensor} mapping, as Lattice takes them.

None

Raises:

Type Description
ValueError

If a tensor is not rank 4 or rank 5, if the ranks disagree between sites, or from Lattice's own assignment checks.

Examples:

>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import CheckerboardLattice, Peps
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> legs = (Leg(V, IN), Leg(V, OUT), Leg(V, OUT), Leg(V, IN), Leg(V, OUT))
>>> a = SymmetricTensor.random(legs, seed=0)
>>> psi = Peps(CheckerboardLattice(), a)
>>> psi.has_physical(), psi[1, 1] is a
(True, True)
Notes

Leg order is (t, l, b, r, phys); see the module docstring for the signature. Nothing here checks that a site's r leg meets its right neighbour's l leg -- that check belongs to the first contraction that tries it, and it is the one place a wrong space produces a message naming both legs.

has_physical

has_physical() -> bool

Whether the sites carry a physical leg (rank 5) or not (rank 4).

Peps2Layers

Peps2Layers(ket: Peps, bra: Peps | None = None)

Bases: Lattice

A view of a bra and a ket as one double-layer network.

Parameters:

Name Type Description Default
ket Peps

The state.

required
bra Peps or None

Its partner. None (the default) means tenet.adjoint of every ket tensor, which is the <psi|psi> network.

None

Raises:

Type Description
ValueError

If ket has no physical leg, or if bra's geometry differs.

Examples:

>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import CheckerboardLattice, Peps, Peps2Layers
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> legs = (Leg(V, IN), Leg(V, OUT), Leg(V, OUT), Leg(V, IN), Leg(V, OUT))
>>> psi = Peps(CheckerboardLattice(), SymmetricTensor.random(legs, seed=0))
>>> net = Peps2Layers(psi)
>>> net[0, 1].ndim
4
Notes

Indexing builds a DoublePepsTensor, which costs a tuple. The bra tensors are built once, at construction, rather than per read: an adjoint is a pass over every block, and a CTMRG sweep reads each site many times.

closed

closed(
    steps: Sequence[tuple[str, Any, Any, str]],
) -> SymmetricTensor

tenet.einsum_chain, with the ribbon twist paid wherever a step closes a cycle.

Parameters:

Name Type Description Default
steps sequence of (str, SymmetricTensor or None, SymmetricTensor, str)

The chain's steps, exactly as tenet.einsum_chain takes them: the equation, the two operands (operand 1 is None on a continued step) and the wires the step bends.

required

Returns:

Type Description
SymmetricTensor

The chain's result.

Examples:

>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import closed
>>> 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)
>>> closed([("ij,jk->ik", a, b, "")]).ndim
2
Notes

Two disjoint blobs joined over k wires close k - 1 cycles, so a step is a tree edge exactly when it contracts one wire and closes something whenever it contracts more. A closure's value depends on the direction its duality pairing runs and the wires running against the composition are the ones the step has to bend -- so the step pays theta on its bent wires, which is tenet.twist and PEPSKit's twistdual discipline. A one-wire step bends without closing anything and pays nothing, which is why the 1D drivers (network/env.py) are untouched.

This reads one orientation per leg, so it is right while one leg is one line of the diagram. A CTM environment bond is not: it carries the bra network's line and the ket network's at once, and a step joining two halves over two of them closes a cycle in each layer while both legs may supply IN from the same half -- an empty bend set for a real closure. That twist is paid where the fused wires are known, in EnvCTM.bond_metric (docs/design.md, M84).

theta is 1 on every bosonic grading, where tenet.twist hands the tensor straight back, so nothing but a fermionic network moves. Either end of a wire carries the same sector, so a continued step (operand 1 None) pays on operand 2 instead.

composed

composed(equation: str, a: Any, b: Any) -> SymmetricTensor

One tenet.einsum_chain step whose operand order and bends are derived.

Parameters:

Name Type Description Default
equation str

A two-operand einsum equation. The output order is the caller's and is not touched; only which operand einsum sees first can change.

required
a SymmetricTensor

The operands. Typed Any because most callers read them off a record whose fields are optional until a move has filled them -- an unfilled one is a caller error, not a case to branch on.

required
b SymmetricTensor

The operands. Typed Any because most callers read them off a record whose fields are optional until a move has filled them -- an unfilled one is a caller error, not a case to branch on.

required

Returns:

Type Description
SymmetricTensor

The contraction, taken in whichever operand order bends the fewer wires, as a one-step tenet.einsum_chain whose bend field names those wires -- so what the chain composes is a composition.

Examples:

>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import composed
>>> 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)
>>> composed("ij,jk->ik", a, b).ndim
2
Notes

network/env.py's _composed is handed its bend set; here both the order and the set are computed. Bend-minimality is the criterion: a bend is a real categorical operation, so a spelling that turns two wires where one suffices lands on a different tensor, and the minimal one is the planar diagram's. Every wire has exactly one IN end, so the two orders' bend counts sum to the number of shared wires and the minimum is well defined; a tie keeps the stated order, which is YASTN's throughout the callers.

The step goes through closed, so a call that contracts more than one wire closes a cycle and pays the ribbon twist on the wires it bends. That is also what makes the tie above free: with the twist paid the two operand orders are the same tensor under fermion parity as well.

entropy

entropy(s: SymmetricTensor, *, alpha: float = 1.0) -> float

The entanglement entropy of a bond, in nats -- von Neumann at alpha=1.

Parameters:

Name Type Description Default
s SymmetricTensor

The diagonal singular-value tensor a tenet.linalg.svd_truncated returned, on a bond of a canonical state; its values are normalized here, so an unnormalized s is read the same way.

required
alpha float

The Renyi index. Default 1.0, the von Neumann entropy -sum_i p_i log p_i; any other positive value gives log(sum_i p_i**alpha) / (1 - alpha). Keyword-only.

1.0

Returns:

Type Description
float

The entropy across the cut, in nats.

Raises:

Type Description
ValueError

If alpha is not positive.

Examples:

>>> import numpy as np
>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import entropy
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> s = SymmetricTensor.from_dense(
...     np.eye(2) / 2**0.5, (Leg(V, OUT), Leg(V, IN))
... )
>>> round(entropy(s), 6)  # a maximally entangled pair across the cut
0.693147
Notes

Nats, and the callable says so because the two references disagree: YASTN's get_entropy is base 2, TenPy's entanglement_entropy is natural. Natural is taken because it is what a central-charge fit wants -- S = (c/6) log(x) on an open chain -- and because every other logarithm in this package is natural. Divide by log(2) for bits.

The multiplet weight is where a non-Abelian bond is easy to get wrong. A sector of quantum dimension d holds d copies of each of its reduced values in the dense Schmidt spectrum, so the probability of one copy is p_i / d where p_i is the sqrt(qdim)-weighted value squared, and the sum over copies restores the d:

``S = -sum_i p_i log(p_i / d_i)``

``S_alpha = log(sum_i d_i (p_i / d_i)**alpha) / (1 - alpha)``

Reading -sum p log p off the flattened spectrum instead would report 0 for an SU(2) singlet, whose whole entanglement lives in one j = 1/2 multiplet. That equality -- an SU(2) state and the same state under U(1) giving the same number -- is what pins the weight rather than merely making it consistent, and tests/network/test_entanglement.py is where it is pinned.

ones

ones(legs: Sequence[Leg]) -> SymmetricTensor

A tensor of ones on legs -- examples/toy_codes/ctmrg.py::init_env's seed spelling.

Parameters:

Name Type Description Default
legs Sequence of Leg

The legs of the tensor to build.

required

Returns:

Type Description
SymmetricTensor

A tensor over legs with every structurally allowed entry equal to 1.

Examples:

>>> from tenet import OUT, GradedSpace, Leg
>>> from tenet.network import ones
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2})
>>> ones((Leg(V, OUT),)).to_dense()
array([1., 1.])

spectrum

spectrum(s: SymmetricTensor) -> list[float]

The singular values on a bond, descending -- the spectrum of an svd output.

Spectrum of what: of the diagonal tensor an svd_truncated returned, and of nothing else. Both callers hand it exactly that, and read it for two different things -- network/dmrg.py for the Schmidt values of a bond, network/envctm.py for the corner spectrum whose convergence ends a sweep -- which is why the name stays the general one rather than either caller's.

Parameters:

Name Type Description Default
s SymmetricTensor

The diagonal singular-value tensor a tenet.linalg.svd_truncated returned.

required

Returns:

Type Description
list of float

Every diagonal value, sqrt(qdim)-weighted, sorted descending.

Examples:

>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import spectrum
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 2})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> _, s, _ = tenet.linalg.svd(t, ((0,), (1,)))
>>> vals = spectrum(s)
>>> len(vals)
4
>>> vals == sorted(vals, reverse=True)
True
Notes

s comes from tenet.linalg.svd_truncated and is diagonal by construction, so this reads its diagonal; the sqrt(qdim) weight is the same one tenet.norm carries, and it is 1 throughout for U(1).

spectrum_sectors

spectrum_sectors(
    s: SymmetricTensor,
) -> dict[Sector, list[float]]

spectrum, resolved by the sector of the bond.

The same values, unflattened: on a GradedSpace bond the singular values arrive already labelled, and spectrum sorts that label away because its two callers -- network/dmrg.py and network/envctm.py -- both want one flat convergence diagnostic. A user asking which symmetry sector carries the entanglement wants the label back, and TenPy spells that entanglement_spectrum(by_charge=True).

Parameters:

Name Type Description Default
s SymmetricTensor

The diagonal singular-value tensor a tenet.linalg.svd_truncated returned.

required

Returns:

Type Description
dict of Sector to list of float

Per coupled sector, its diagonal values sqrt(qdim)-weighted and sorted descending. Concatenating and re-sorting the values reproduces spectrum exactly, which is how that function is written.

Examples:

>>> import tenet
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import spectrum_sectors
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 2, U1Sector(1): 2})
>>> t = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> _, s, _ = tenet.linalg.svd(t, ((0,), (1,)))
>>> sorted((sector.charge, len(vals)) for sector, vals in spectrum_sectors(s).items())
[(0, 2), (1, 2)]
Notes

The sqrt(qdim) weight is applied here and nowhere else in this package. Both spectrum and entropy read it off this function, so there is one place where a non-Abelian bond's multiplet weight is decided. It is the weight tenet.norm carries, and it is 1 throughout for U(1).

supplies_in

supplies_in(leg: Leg) -> bool

Whether leg is the IN end of its wire.

Parameters:

Name Type Description Default
leg Leg

The leg to read.

required

Returns:

Type Description
bool

True when the leg supplies the IN end of the wire it sits on.

Examples:

>>> from tenet import IN, GradedSpace, Leg
>>> from tenet.network import supplies_in
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1})
>>> supplies_in(Leg(V, IN)), supplies_in(Leg(V, IN, dual=True))
(True, False)
Notes

side alone answers it for a leg nobody has moved. A leg that came back from a qr was repartitioned across the map's two sides, which flips side and dual together and leaves the same wire in the opposite spelling -- so the predicate is the pair, not side.

dmrg_

dmrg_(
    psi: MPS,
    h: MPO,
    *,
    schedule: Sequence[Sweep] | None = None,
    chi: int | None = None,
    cutoff: float | None = None,
    energy_tol: float = 1e-12,
    schmidt_tol: float = 1e-08,
    max_sweeps: int = 40,
    ncv: int = 3,
    orthogonal_to: Sequence[MPS] | None = None,
    seed: int = 0,
    callback: Callable[[DMRG_out], None] | None = None,
    compile: Callable | None = None,
) -> DMRG_out

Sweep psi to the ground state of h in place and return a DMRG_out.

Parameters:

Name Type Description Default
psi MPS

The starting state, swept in place; a freshly seeded random MPS is the expected input.

required
h MPO

The Hamiltonian.

required
schedule Sequence of Sweep or None

Per-sweep settings; the last entry repeats until convergence or max_sweeps. Exclusive with chi/cutoff. Default None. Keyword-only, as are all the following.

None
chi int or None

Flat bond-dimension cap for every sweep. Default None, meaning 64.

None
cutoff float or None

Flat singular-value cutoff for every sweep. Default None, meaning 1e-14.

None
energy_tol float

Energy-change convergence threshold. Default 1e-12.

1e-12
schmidt_tol float

Worst-cut Schmidt-change convergence threshold. Default 1e-8.

1e-08
max_sweeps int

Sweep budget. Default 40.

40
ncv int

Krylov-space dimension for lanczos. Default 3.

3
orthogonal_to Sequence of MPS or None

Already-converged states to hold psi orthogonal to, turning the run from a ground-state search into an excited-state one: with the ground state psi1 in hand, dmrg_(psi2, h, orthogonal_to=[psi1]) targets the first excited state. The states are not modified and need no particular gauge. Default None, which projects nothing and is today's behaviour. See Notes.

None
seed int

Feeds sweep_'s noise draw, distinctly per sweep; a schedule with noise=0.0 everywhere draws nothing. Default 0.

0
callback Callable[[DMRG_out], None] or None

Invoked once per sweep with that sweep's DMRG_out. Default None.

None
compile Callable or None

Handed verbatim to Env, which wraps the prepared two-site matvec with it once per structure key. jax.jit is the intended argument and this layer names no accelerator, so the caller supplies it and the jax extra. It changes the run's performance regime rather than its accuracy, and the payoff is a matvec one: with jax.jit the two-site matvec runs several times faster, by a factor that shrinks as the MPO bond widens and the work moves into BLAS. The sweep around it is not traceable -- the truncating SVD re-decides the bond space every sweep -- so on the JAX backend a compiled run is slower end to end than the plain NumPy one, and Env re-invokes compile at every bond visit. Default None, which runs the plain Python function.

None

Returns:

Type Description
DMRG_out

The last sweep's record; its psi is the object passed in.

Raises:

Type Description
ValueError

If schedule is passed together with chi or cutoff -- silently letting one win is how a run reports a chi it did not use -- or if schedule is empty.

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import MPO, MPS, dmrg_, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})  # 2 S^z
>>> sz, sp = np.diag([-0.5, 0.5]), np.array([[0.0, 0.0], [1.0, 0.0]])
>>> op = {q: local_op(o, phys=phys, charge=U1Sector(q))
...       for q, o in ((0, sz), (-2, sp), (2, sp.T))}
>>> terms = []
>>> for i in range(3):  # 4-site Heisenberg chain
...     terms.append((1.0, [(op[0], i), (op[0], i + 1)]))
...     terms.append((0.5, [(op[-2], i), (op[2], i + 1)]))
...     terms.append((0.5, [(op[2], i), (op[-2], i + 1)]))
>>> h = MPO.from_terms(4, terms)
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1)] * 2)  # Neel seed
>>> out = dmrg_(psi, h, chi=8)
>>> round(out.energy, 6)  # the exact open-chain ground energy, to 6 places
-1.616025
Notes

psi is right-canonicalized first (MPS.canonize_), so a freshly seeded random MPS is the expected input and a caller keeping the returned out.psi and the one it passed in holds the same object.

Two spellings of what the sweeps do, exclusive: the flat chi / cutoff kwargs (defaults 64 and 1e-14), or schedule, a non-empty sequence of Sweep entries whose last entry repeats until convergence or max_sweeps -- so schedule=[Sweep(chi=64)] is exactly the flat run, and schedule=[Sweep(32, noise=1e-4)] * 4 + [Sweep(64)] is a ramp that cools down at chi=64 for as long as max_sweeps allows.

Convergence uses both of YASTN's criteria (_dmrg.py:180-195): the energy change |E_old - E| < energy_tol and the worst-cut Schmidt change max_k ||S_k - S_k^old|| < schmidt_tol, and the loop stops only when both are met in one sweep. The Schmidt criterion is the sensitive one, and it is what catches a run whose energy has plateaued on a wrong bond structure. Convergence is never declared on a sweep that is still noisy or still inside the schedule: the loop exits only when the sweep just run used the schedule's last entry and that entry's noise is 0.0 (block2's guard, sweep_algorithm.hpp:3103-3105, and its docstring's "and the noise for the current sweep is zero") -- an energy that stopped moving under noise at a ramp's intermediate chi has converged to the wrong thing, and reporting it as converged is worse than sweeping on.

orthogonal_to, and why the name. YASTN spells the same argument project and TenPy orthogonal_to; the second is taken, because project names the mechanism and this argument is one of two mechanisms that implement it -- block2 has both, hard projection and a level shift (see lanczos) -- while orthogonal_to names the result, which is the same under either. The machinery is one two-state Env per given state over MPO.identity, set up here and swept alongside env; what each contributes at a bond is a projection vector handed to lanczos as an argument of the solve. Sector targeting is unaffected and composes with it: a charged D=1 boundary leg on bond 0 fixes the sector, orthogonality then walks up the spectrum inside it, and a converged state whose boundary legs put it in a different sector is dropped from the projection -- it is orthogonal to psi by the symmetry, before the sweep does anything, which is right rather than merely harmless.

The reported energy is the projected operator's own Ritz value, so it is the excited energy directly and needs no shift subtracted.

callback, if given, is invoked once per sweep with the DMRG_out built for that sweep, after history is appended, so it sees the sweep that just finished. Its return value is ignored: there is no early-stop protocol.

lanczos

lanczos(
    matvec: Callable[[SymmetricTensor], SymmetricTensor],
    v: SymmetricTensor,
    *,
    ncv: int = 3,
    tol: float = 1e-13,
    orthogonal_to: Sequence[SymmetricTensor] = (),
) -> tuple[float, SymmetricTensor]

Ground eigenpair (value, vector) of a Hermitian matvec over SymmetricTensors.

Parameters:

Name Type Description Default
matvec Callable[[SymmetricTensor], SymmetricTensor]

The Hermitian operator, as a function applying it to one vector.

required
v SymmetricTensor

The starting vector; any tensor with matvec's input structure.

required
ncv int

Krylov-space dimension. Default 3, and meant to stay small (see Notes). Keyword-only.

3
tol float

The happy-breakdown threshold on the recurrence norm beta. Default 1e-13. Keyword-only.

1e-13
orthogonal_to Sequence of SymmetricTensor

Vectors on v's structure to hold the solve orthogonal to. Default (), which projects nothing, allocates nothing and leaves the recurrence exactly as it was. Keyword-only.

()

Returns:

Name Type Description
value float

The smallest ('SR') Ritz value.

vector SymmetricTensor

The matching normalized Ritz vector, on v's structure.

Raises:

Type Description
ValueError

If orthogonal_to spans the whole space v lives in, so that the projected start vector is numerically zero.

Examples:

>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import lanczos
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> v = SymmetricTensor.random((Leg(V, OUT), Leg(V, IN)), seed=0)
>>> value, vector = lanczos(lambda t: t * 2.0, v)  # matvec = 2 * identity
>>> round(value, 6)
2.0
Notes

YASTN's three-term recurrence (yastn/tensor/_krylov.py:34-42) and its happy breakdown (H[(j+1,j)] < tol -> stop and drop the row, :39-43), then eigh of the (m, m) tridiagonal and one recombination (yastn/krylov/_krylov.py:226-239, a single iteration with no restart). hermitian=True, ncv=3, which='SR' are YASTN's own DMRG defaults (_dmrg.py:151-152) and are not knobs this layer tunes.

The only tensor operations are tenet.add/subtract, scalar multiply/divide, tenet.norm and tenet.inner -- a Krylov solver needs a vector space and nothing else, and a SymmetricTensor is one.

This is an inner solver inside an outer sweep, not a standalone eigensolver: the recurrence is not reorthogonalized, so ncv is meant to stay small.

orthogonal_to is hard projection, not a level shift, and both are block2's: with ors and no weights the basis vectors are projected by 1 - |v><v| (iterative_matrix_functions.hpp:1198-1200, :1226-1237), while a non-empty projection_weights instead replaces H by H + sum_k w_k |v_k><v_k| (:1201-1204, :1250-1253) -- the level-shift approach its own documentation names as such (docs/source/user/keywords.rst, proj_mps_tags), and which it warns reports unphysical eigenvalues E_k + w_k when a weight is smaller than the gap. Hard projection is what statespecific alone does, it has no parameter to get wrong, and the eigenvalue it returns is the projected operator's own -- so it is what is adopted here and no weight argument exists.

The projector is applied to the start vector and to every matvec result, which is plain Lanczos on P H P restricted to range(P): the recurrence stays a valid three-term one for a Hermitian operator, rather than a perturbed one for H.

sweep_

sweep_(
    psi: MPS,
    h: MPO,
    env: Env,
    schmidt: dict[int, list[float]],
    *,
    chi: int,
    cutoff: float,
    ncv: int = 3,
    noise: float = 0.0,
    noise_type: str = "wavefunction",
    orthogonal_to: Sequence[Env] = (),
    seed: int = 0,
) -> tuple[float, float]

One left-to-right then right-to-left two-site sweep. psi and env mutate.

Parameters:

Name Type Description Default
psi MPS

The state, mutated in place; expected mixed-canonical the way dmrg_ prepares it.

required
h MPO

The Hamiltonian.

required
env Env

The environment cache for (psi, h), mutated in place.

required
schmidt dict[int, list[float]]

Per-bond Schmidt spectra, updated in place -- the second convergence criterion's input.

required
chi int

The bond-dimension cap handed to svd_truncated at every bond. Keyword-only.

required
cutoff float

The singular-value cutoff handed to the same SVD. Keyword-only.

required
ncv int

Krylov-space dimension for lanczos. Default 3. Keyword-only.

3
noise float

Relative strength of the perturbation mixed in after the eigensolver and before each split; 0.0 (the default) draws no random number, builds no density matrix, and the sweep is bit-identical to a sweep without the keyword. Keyword-only.

0.0
noise_type (wavefunction, perturbative)

Which perturbation noise means, and therefore which decimation the sweep runs -- the table in Notes. Default "wavefunction", today's behaviour. Read only when noise is nonzero. Keyword-only.

"wavefunction"
orthogonal_to Sequence of Env

Two-state environments, one per converged state to hold psi orthogonal to -- each an Env(phi, MPO.identity(...), bra=psi), built and handed over the way env is, and mutated in place alongside it. Default (), which projects nothing. dmrg_ builds them from a list of MPS and is the spelling a caller wants. Keyword-only.

()
seed int

Makes the noise draw at bond n reproducible as seed + n. Unused by noise_type="perturbative", which draws nothing. Default 0. Keyword-only.

0

Returns:

Name Type Description
energy float

The last lanczos Ritz value of the sweep.

max_discarded_weight float

The maximum per-bond discarded weight (see Notes for why the maximum rather than the total).

Raises:

Type Description
ValueError

If noise_type is neither "wavefunction" nor "perturbative" -- a typo that silently ran the default would misreport the mixer.

Notes

Which decimation runs is decided by the two keywords and by nothing else. No bond width, no chi, no runtime probe:

======================================== ================================== (noise, noise_type) the split ======================================== ================================== noise == 0.0, any noise_type svd_truncated of aa > 0, "wavefunction" svd_truncated of a perturbed aa > 0, "perturbative" eigh of a perturbed rho ======================================== ==================================

A caller reads the rule off the Sweep entry: the density-matrix split engages exactly when perturbative noise is asked for, and a noiseless sweep -- including the cooling tail of a ramp, and every sweep of a run that never mentions noise -- takes the SVD split. That is deliberate rather than a default falling out: squaring the two-site tensor into rho resolves a singular value sigma through sigma**2, so the split's own accuracy floor moves from machine epsilon to its square root, and a converged noiseless sweep is exactly where that costs something. block2 makes the same pairing in the other direction -- its wavefunction noise exists only on its SVD branch (sweep_algorithm.hpp:964-978) and its density-matrix branch (:930-953) is where the perturbative noise goes.

YASTN's _dmrg_sweep_2site_ (_dmrg.py:222-249) and its (('last', 0), ('first', 1)) two-direction loop, five steps per bond: merge, eigs, split, clear_site_, update_env_.

svd_truncated decides the bond GradedSpace here, every bond and every sweep, and the discarded weight is Pythagoras exactly as its docstring prescribes: U S Vh is isometric on both sides, so norm(U S Vh) = norm(S) and the dropped fraction of the (unit-norm) two-site tensor is 1 - norm(S)**2.

vh comes back on the map's partition and is stored straight into psi: the MPS.__setitem__ write barrier is what puts it back on (l, p | r), which is why no caller in this package ever spells a repartition.

The discarded weight here is the maximum over bonds, because it feeds a per-sweep convergence report where the worst bond is the diagnostic; MPS.compress_ returns the total instead, because its caller is asking how much of the state was thrown away. Two conventions, two names.

Wavefunction noise (noise > 0, noise_type="wavefunction"): a random symmetric tensor over the two-site tensor's own legs is added after the eigensolver and before the split, at relative strength noise (block2's NoiseTypes::Wavefunction scaling, operator_functions.hpp:777-815, so noise is dimensionless and block2's 1e-4..1e-5 range transfers), and the two-site tensor is renormalized so the Pythagoras discarded weight stays a fraction of a unit-norm tensor. The perturbation fills every structurally allowed coupled sector of the (l, p | q, r) map -- including the ones the eigensolver left numerically empty and which svd_truncated therefore omits from the bond, which is the local minimum a symmetric DMRG falls into: a sector that is zero stays zero forever, because nothing else in the sweep can create it. It cannot reach outside bond_l (x) phys -- no wavefunction noise can, and neither can two-site DMRG itself.

Perturbative noise (noise > 0, noise_type="perturbative"): block2's default (noise_type = NoiseTypes::DensityMatrix with decomp_type = DecompositionTypes::DensityMatrix, sweep_algorithm.hpp:104-106), and not randomness: the perturbation vectors are the operator's own action on the current two-site tensor, resolved by term family through Env.heff2_families. The split becomes an eigh of rho = tr aa aa^dag with rho += noise * sum_k p_k p_k^dag folded in (moving_environment.hpp:3554, :3636, :4250), each p_k normalized and the collection scaled to total squared norm noise (:3698-3713), so noise stays dimensionless in the same 1e-4..1e-5 range. This reaches the sectors the Hamiltonian couples to, which is the difference from the random draw: it cannot waste the perturbation on a direction the operator never visits, and on a sector-poor bond it fills what H can actually populate. The resolution needs the operator's description, so it is an operator built symbolic=True that gets the family-resolved mixer; every other MPO -- the builders' default among them -- gets heff2_families' single-vector fallback, which on a finite-range lattice model costs one sweep of head start and no accuracy.

Neither noise is variational: a noisy sweep's energy may sit above the same sweep at noise=0.0.

Excited states (orthogonal_to): at each bond, every handed-over two-state environment produces one projection vector -- its converged state's two-site reduced form in psi's environment gauge -- and the collection is handed to lanczos as arguments of the solve, which is the shape block2's eigs(..., ortho_bra, projection_weights) has (sweep_algorithm.hpp:1190-1206, :1244-1249). The converged states are held fixed: their per-bond reduced forms are recomputed at every bond, and their environments follow psi exactly as env does. block2 instead canonicalizes and propagates its ext_mpss alongside the sweep (:893-917), which its eff_ham machinery needs; the contraction does not, because a gauge transformation on any bond of the converged state cancels between the two environments and its two-site tensor. What is required is that psi be mixed-canonical at the bond, which this sweep maintains anyway and which the projection's meaning rests on.

correlation_function

correlation_function(
    psi: MPS,
    a: SymmetricTensor,
    b: SymmetricTensor,
    *,
    pairs: Sequence[tuple[int, int]] | None = None,
) -> dict[tuple[int, int], float]

<psi|a_i b_j|psi> / <psi|psi> at a distance, for the pairs a caller asks for.

Parameters:

Name Type Description Default
psi MPS

The state; any gauge, any norm, and not modified.

required
a SymmetricTensor

The left operator, in local_op's rank-3 charged form (phys OUT, phys IN, charge OUT) -- the form MPO.from_terms takes, and the form a fermionic operator has to have, since c is not invariant as a rank-2 tensor.

required
b SymmetricTensor

The right operator, same form.

required
pairs Sequence of (int, int) or None

The (i, j) pairs to measure, 0 <= i < j < len(psi). Default None, meaning every such pair -- YASTN's measure_2site(bonds='<'). Keyword-only.

None

Returns:

Type Description
dict of (int, int) to float

One normalized value per requested pair, keyed by the pair.

Raises:

Type Description
ValueError

If any requested pair is not 0 <= i < j < len(psi).

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import MPS, correlation_function, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> sz = local_op(np.diag([-0.5, 0.5]), phys=phys, charge=U1Sector(0))
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1), U1Sector(1)])
>>> {k: round(v, 6) for k, v in correlation_function(psi, sz, sz).items()}
{(0, 1): -0.25, (0, 2): 0.25, (1, 2): -0.25}
Notes

YASTN's measure_2site(bra, O, P, ket, bonds='<') and TenPy's correlation_function(ops1, ops2, sites1, sites2). The name is TenPy's because it is the term of art; the house's own _2site vocabulary is already spoken for by expectation_2site, which takes one invariant rank-4 operator on an adjacent pair and keeps its signature untouched.

Fermions are correct here because nothing new decides their sign. Each pair is one two-operator term through MPO.from_terms, read with Env: the Jordan-Wigner string across the sites between i and j is the fermionic-parity braiding the term builder already inserts, and the composition rule is obeyed by the contractions those two are built from. A hand-written transfer walk carrying the charge leg between the two sites would be the faster route and would re-decide that sign outside the audited machinery.

The cost, stated rather than hidden: one from_terms build and one Env.measure pass per requested pair, so the default all-pairs call is O(N**2) builds and O(N**3) transfer contractions. That is the ceiling; pairs= is the way around it for the row or the distance a caller actually wants. The named upgrade is YASTN's cached transfer walk (_measure.py:130).

measure_mpo

measure_mpo(bra: MPS, h: MPO, ket: MPS) -> float

<bra|H|ket> -- an MPO between two states, undivided by either norm.

Parameters:

Name Type Description Default
bra MPS

The state that is conjugated; any gauge, any norm.

required
h MPO

The operator.

required
ket MPS

The state that is not; any gauge, any norm.

required

Returns:

Type Description
float

<bra|H|ket>.

Examples:

>>> from tenet import GradedSpace
>>> from tenet.network import MPO, MPS, measure_mpo
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> states = [U1Sector(1), U1Sector(-1), U1Sector(1), U1Sector(-1)]
>>> psi = MPS.product(phys, states)
>>> phi = MPS.product(phys, states)
>>> round(measure_mpo(phi, MPO.identity(4, phys), psi), 12)  # the plain overlap
1.0
Notes

YASTN's measure_mpo(bra, op, ket), argument order included; TenPy spells the same thing MPOEnvironment(bra, H, ket).full_contraction(0). It is one line over Env -- Env(ket, h, bra=bra).measure() -- and exists because that spelling requires a reader to know that Env's first positional argument is the ket, which is the constructor's shape and not a measurement's.

Undivided, for overlap's reason, stated there. With h the identity (MPO.identity) this is overlap(bra, ket), computed the long way through the environment cache; the agreement is tested rather than assumed. No gauge is assumed of either state, and the pass is built in a fresh Env, so a measurement never writes into a sweep's cache.

corner2x2

corner2x2(
    env: EnvCTM, which: str, site: Any, a: Any = None
) -> SymmetricTensor

One 2x2 enlarged corner: two edges, the corner between them, and the site.

Parameters:

Name Type Description Default
env EnvCTM

The environment to read.

required
which str

'tl', 'tr', 'bl' or 'br'.

required
site Site or tuple[int, int]

The site whose ring supplies the three environment tensors.

required
a DoublePepsTensor or SymmetricTensor or None

The tensor to absorb, in place of the one the state has at site. Default None, i.e. the state's. bond_metric passes the qr-reduced site here, which is the only reason the parameter exists: the environment ring is the site's, the tensor inside it is not.

None

Returns:

Type Description
SymmetricTensor

Rank 4 for a single layer, rank 6 for a double one, and in two groups of equal size: the legs pointing one way out of the corner, then the legs pointing the other way. Which two ways depends on which -- 'tl' returns (down, right), 'tr' (left, down), 'br' (up, left), 'bl' (right, up) -- so that tl @ tr, br @ bl, bl @ tl and tr @ br each close the four halves of the 4x4 patch.

Notes

YASTN's corner2x2 (_env_contractions.py:429) is t1 @ c @ t2 followed by a tensordot onto the site, and for a double layer that tensordot is the matching append_vec_*. The grouping falls out for free: append_vec_tl already returns (x, b, y, r), which is those two groups in that order, so nothing is fused and nothing is transposed here.

flip

The sublattice partner of t: every leg's side reversed, the blocks kept.

Parameters:

Name Type Description Default
t SymmetricTensor

Any tensor.

required

Returns:

Type Description
SymmetricTensor

tenet.conj(tenet.adjoint(t)) -- each leg keeps its space, dual, name and position and reverses its side, so every leg of the result contracts with the leg of t that sits in the same position.

Examples:

>>> import tenet
>>> from tenet import OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import flip
>>> from tenet.symmetry import U1, U1Sector
>>> V = GradedSpace.new(U1, {U1Sector(0): 1, U1Sector(1): 1})
>>> a = SymmetricTensor.random((Leg(V, OUT), Leg(V, OUT)), seed=0)
>>> b = flip(a)
>>> b.legs[0].side, b.legs[0].dual
(<Side.IN: 'in'>, False)
>>> bool(tenet.allclose(flip(b), a))
True
Notes

YASTN's flip_signature. tenet.adjoint alone conjugates the blocks, which would make the B sublattice the complex conjugate of the A one and give a different network; the tenet.conj puts them back.

proj_corners

proj_corners(
    r0: SymmetricTensor,
    r1: SymmetricTensor,
    *,
    max_bond: int | None = None,
    cutoff: float | None = 1e-14,
) -> tuple[SymmetricTensor, SymmetricTensor]

The projector pair across a cut, from r0 @ r1^T -- YASTN proj_corners:1209.

Parameters:

Name Type Description Default
r0 SymmetricTensor

The two R factors, each (bond, *the cut's legs); their cut legs are duals of each other.

required
r1 SymmetricTensor

The two R factors, each (bond, *the cut's legs); their cut legs are duals of each other.

required
max_bond int or None

The environment bond-dimension cap. Default None.

None
cutoff float or None

Relative singular-value cutoff for the truncation. Default 1e-14.

1e-14

Returns:

Name Type Description
p0 SymmetricTensor

(*r1's cut legs, new bond IN).

p1 SymmetricTensor

(*r0's cut legs, new bond OUT).

Raises:

Type Description
StructureChangingError

Under jax.jit/jax.grad: svd_truncated reads singular values to decide which sectors survive.

Notes

rr = r0 @ r1^T = u s v, rs = s^(-1/2), p0 = r1 (rs v)^dagger and p1 = r0 (u rs)^dagger, so p1^T p0 inserts rs s rs = 1 on the cut. No step assumes the cut is Hermitian: the two sides enter as two different tensors and leave as two different projectors, and there is no eigendecomposition and no single isometry reused on both index groups. The property a C4v single-move projector needs and the ansatz does not supply is a property this construction never asks for.

The two new bond legs come out on opposite sides, IN for p0 and OUT for p1, which is what makes the moved edge's two ends meet the moved corners'.

accumulated_truncation_error

accumulated_truncation_error(
    infoss: Sequence[Sequence[Evolution_out]],
    statistics: str = "mean",
) -> float

The truncation error accumulated over a sequence of evolution steps.

Parameters:

Name Type Description Default
infoss Sequence of Sequence of Evolution_out

One evolution_step_ output per step.

required
statistics str

'mean' (the default) or 'max' over the bonds of a step.

'mean'

Returns:

Type Description
float

sum_steps statistics_bond [ sum_gates truncation_error ].

Raises:

Type Description
ValueError

If statistics is neither 'mean' nor 'max'.

Examples:

>>> from tenet.network import Evolution_out, accumulated_truncation_error
>>> from tenet.network import Bond, Site
>>> b = Bond(Site(0, 0), Site(0, 1))
>>> step = [Evolution_out(bond=b, truncation_error=0.1)] * 2
>>> accumulated_truncation_error([step, step])
0.4
Notes

YASTN's accumulated_truncation_error verbatim. It is an estimate: the errors are measured in different metrics at different steps and adding them assumes they do not cancel, which is the conservative direction.

apply_gate

apply_gate(
    a0: SymmetricTensor, a1: SymmetricTensor, gate: Gate
) -> tuple[Any, Any]

The two site tensors with the gate's halves on their physical legs.

Parameters:

Name Type Description Default
a0 SymmetricTensor

The rank-5 site tensors of gate.bond, in the fermionic order.

required
a1 SymmetricTensor

The rank-5 site tensors of gate.bond, in the fermionic order.

required
gate Gate

The gate.

required

Returns:

Type Description
tuple[SymmetricTensor, SymmetricTensor]

Rank 6 each: (t, l, b, r, aux, phys). The auxiliary wire is the bond's enlargement and the qr in truncate_ consumes it.

Examples:

>>> import numpy as np
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import Bond, Site, apply_gate, gate_nn, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(0): 2})
>>> V = GradedSpace.new(U1, {U1Sector(0): 1})
>>> legs = (Leg(V, IN), Leg(V, OUT), Leg(V, OUT), Leg(V, IN), Leg(phys, OUT))
>>> a = SymmetricTensor.random(legs, seed=0)
>>> sz = np.diag([-0.5, 0.5])
>>> g = gate_nn(local_op(np.kron(sz, sz), phys=phys), 0.1, Bond(Site(0, 0), Site(0, 1)))
>>> b0, b1 = apply_gate(a, a, g)
>>> b0.ndim, b1.ndim
(6, 6)
Notes

One composition per site, and the operand order is the physical wire's: the gate half supplies IN there and the site supplies OUT, so the gate is operand 1 and nothing bends. The module docstring says why no swap gate is needed and why the auxiliary leg's position is free.

evolution_step_

evolution_step_(
    env: Any, gates: Iterable[Gate], **kwargs: Any
) -> list[Evolution_out]

Apply every gate to env's state, truncating after each one. In place.

Parameters:

Name Type Description Default
env EnvCTM or EnvNTU

The environment; its psi is evolved in place.

required
gates Iterable of Gate

The Trotter gates, in the order to apply them -- gates_nn builds the homogeneous list.

required
**kwargs Any

Passed to truncate_: max_bond, cutoff, fix_metric, max_iter, tol_iter, pinv_cutoffs.

{}

Returns:

Type Description
list[Evolution_out]

One record per gate, in the order applied.

Examples:

>>> import numpy as np
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import EnvNTU, Peps, SquareLattice, evolution_step_, gates_nn, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(0): 2})
>>> V = GradedSpace.new(U1, {U1Sector(0): 1})
>>> legs = (Leg(V, IN), Leg(V, OUT), Leg(V, OUT), Leg(V, IN), Leg(phys, OUT))
>>> psi = Peps(SquareLattice(dims=(2, 2)), SymmetricTensor.random(legs, seed=0))
>>> sz = np.diag([-0.5, 0.5])
>>> h = local_op(np.kron(sz, sz), phys=phys)
>>> infos = evolution_step_(EnvNTU(psi), gates_nn(psi.geometry, h, 0.1), max_bond=2)
>>> len(infos)
16
Notes

YASTN's evolution_step_, minus the patch mechanism (a provisional per-site update that lets an EnvCTM postpone rebuilding its corners) and minus multi-site gates. An EnvCTM here is not re-converged between gates: the caller decides how often to call update_, which is the same choice YASTN's post_truncation_ makes for it and one this layer has no policy about yet.

gate_nn

gate_nn(h: SymmetricTensor, step: float, bond: Any) -> Gate

exp(-step * h) for one bond, split into the two halves a PEPS wants.

Parameters:

Name Type Description Default
h SymmetricTensor

The bond Hamiltonian, rank 4 on (phys OUT, phys OUT, phys IN, phys IN) -- local_op's invariant two-site form, the same object expectation_2site reads.

required
step float

The Trotter step. exp(-step h) is the imaginary-time gate; a complex step is the real-time one, and nothing here assumes otherwise.

required
bond Bond or tuple[Site, Site]

The bond the gate belongs to, site0 before site1 in the fermionic order.

required

Returns:

Type Description
Gate

g0, g1 and the bond.

Raises:

Type Description
ValueError

If h is not rank 4.

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import Bond, Site, gate_nn, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(0): 2})
>>> sz = np.diag([-0.5, 0.5])
>>> h = local_op(np.kron(sz, sz), phys=phys)
>>> gate_nn(h, 0.1, Bond(Site(0, 0), Site(1, 0))).g0.shape
(2, 2, 2)
Notes

tenet.linalg.expm lowers the pair to a square map on the partition ((0, 1), (2, 3)) -- the pair's outputs against its inputs -- and exponentiates one dense matrix per coupled sector, which is the spelling examples/toy_codes/tebd.py uses at 1D. The split is YASTN's decompose_nn_gate: an SVD across ((0, 2), (1, 3)), the first site's pair of legs against the second's, with the singular values shared as sqrt(s) so neither half carries the whole scale. YASTN's gate_nn_hopping / gate_nn_Ising build the same object in closed form for two particular models; the exponential is the general one and the two closed forms are an optimization this layer does not need.

gates_nn

gates_nn(
    geometry: Any,
    h: SymmetricTensor,
    step: float,
    *,
    symmetrize: bool = True,
) -> tuple[Gate, ...]

One gate_nn per nearest-neighbour bond of a lattice.

Parameters:

Name Type Description Default
geometry SquareLattice or Lattice or Peps

Anything with a bonds(); the gates come out in its order.

required
h SymmetricTensor

The bond Hamiltonian, the same on every bond.

required
step float

The Trotter step of the whole sequence.

required
symmetrize bool

True (the default) halves the step and appends the reversed sequence, so a step is a symmetric product and the Trotter error is O(step**3). False gives one pass at the full step.

True

Returns:

Type Description
tuple[Gate, ...]

The gates, in the order to apply them.

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import SquareLattice, gates_nn, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(0): 2})
>>> h = local_op(np.kron(np.diag([-0.5, 0.5]), np.diag([-0.5, 0.5])), phys=phys)
>>> len(gates_nn(SquareLattice(dims=(2, 2), boundary="obc"), h, 0.1))
8
Notes

YASTN's distribute, minus the local gates and minus the per-site Hamiltonian: a lattice whose bonds do not all carry the same term builds its own list, one gate_nn per bond, and hands it to evolution_step_.

truncate_

truncate_(
    env: Any,
    bond: Any,
    *,
    gate: Gate | None = None,
    max_bond: int | None = None,
    cutoff: float | None = 1e-14,
    fix_metric: float | None = 0.0,
    max_iter: int = 20,
    tol_iter: float = 1e-13,
    pinv_cutoffs: Sequence[float] = PINV_CUTOFFS,
) -> Evolution_out

Reduce one bond of env's state, in the metric env supplies. In place.

Parameters:

Name Type Description Default
env EnvCTM or EnvNTU

The environment. Its psi is the state being evolved and is written back into; its bond_metric is what "best" means here.

required
bond Bond or tuple[Site, Site]

The bond to truncate, either orientation.

required
gate Gate or None

A gate to apply first, enlarging the bond. None (the default) truncates the bond as it stands, which is YASTN's truncate_ without a gate.

None
max_bond int or None

The bond-dimension cap. Default None, i.e. no cap.

None
cutoff float or None

Relative singular-value cutoff of the initializing SVD. Default 1e-14.

1e-14
fix_metric float or None

Replace every eigenvalue below the metric's own error scale (the non-Hermitian norm plus the most negative eigenvalue) by fix_metric times that scale. 0.0 is the default and 1.0 the other sensible value; None skips the eigendecomposition altogether and reports no eigenvalues.

0.0
max_iter int

Least-squares sweeps. Default 20; 0 keeps the SVD initialization.

20
tol_iter float

Stop once the squared error falls below this. Default 1e-13.

1e-13
pinv_cutoffs Sequence of float

The pseudo-inverse ladder each solve chooses from. Default PINV_CUTOFFS.

PINV_CUTOFFS

Returns:

Type Description
Evolution_out

The bond, the truncation error and what the metric was found to be. The error is nan in the one case where the metric measured nothing: fix_metric replaced every eigenvalue, which happens when the metric's own error scale reaches its largest eigenvalue. The plain SVD stands, and a nan is reported rather than a zero that would read as "lossless".

Raises:

Type Description
ValueError

If the two sites are not nearest neighbours (from nn_bond_dirn).

Examples:

>>> import numpy as np
>>> from tenet import IN, OUT, GradedSpace, Leg, SymmetricTensor
>>> from tenet.network import EnvNTU, Peps, SquareLattice, gate_nn, local_op, truncate_
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(0): 2})
>>> V = GradedSpace.new(U1, {U1Sector(0): 1})
>>> legs = (Leg(V, IN), Leg(V, OUT), Leg(V, OUT), Leg(V, IN), Leg(phys, OUT))
>>> psi = Peps(SquareLattice(dims=(2, 2)), SymmetricTensor.random(legs, seed=0))
>>> env = EnvNTU(psi)
>>> sz = np.diag([-0.5, 0.5])
>>> h = local_op(np.kron(sz, sz), phys=phys)
>>> g = gate_nn(h, 0.05, psi.bonds()[0])
>>> out = truncate_(env, g.bond, gate=g, max_bond=2)
>>> out.truncation_error < 1e-8
True
Notes

The shape is YASTN's: qr each site into the isometry that stays and the small factor that moves, take the metric on the two reduced legs, truncate the product of the factors, and put the survivors back. The isometries are what makes the metric a metric on a small space -- D by D rather than the whole site.

expectation_1site

expectation_1site(
    psi: MPS, o: SymmetricTensor, n: int
) -> float

<psi|o_n|psi> / <psi|psi>, with o rank 2 on (phys OUT, phys IN).

Parameters:

Name Type Description Default
psi MPS

The state; any gauge, any norm.

required
o SymmetricTensor

The operator, rank 2 on (phys OUT, phys IN) -- local_op's invariant one-site form.

required
n int

The site, 0 <= n <= len(psi) - 1.

required

Returns:

Type Description
float

The normalized expectation value.

Raises:

Type Description
ValueError

If n is out of range, or if o is not rank 2.

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import MPS, expectation_1site, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1)])
>>> sz = local_op(np.diag([-0.5, 0.5]), phys=phys)
>>> round(expectation_1site(psi, sz, 0), 6)
0.5
Notes

Divided by the norm, and the name carries that. YASTN spells the pair measure_1site/measure_2site and does not divide; tenpy spells it expectation_value and does. The references disagree, so conformance decides nothing: in a package whose Env.measure also returns <psi|H|psi> undivided, a second measure_* that quietly did the same would make one verb mean two things. No normalize= opt-out -- a config for a value that never changes.

expectation_2site

expectation_2site(
    psi: MPS, o: SymmetricTensor, n: int
) -> float

<psi|o_{n,n+1}|psi> / <psi|psi>, o rank 4 on (p OUT, p OUT, p IN, p IN).

Parameters:

Name Type Description Default
psi MPS

The state; any gauge, any norm.

required
o SymmetricTensor

The two-site operator, rank 4 -- local_op's invariant form on np.kron(a, b).

required
n int

The pair's left site, 0 <= n <= len(psi) - 2.

required

Returns:

Type Description
float

The normalized expectation value on the adjacent pair.

Raises:

Type Description
ValueError

If n is out of range, or if o is not rank 4.

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import MPS, expectation_2site, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1)])
>>> sz = np.diag([-0.5, 0.5])
>>> szsz = local_op(np.kron(sz, sz), phys=phys)
>>> round(expectation_2site(psi, szsz, 0), 6)
-0.25
Notes

Divided by <psi|psi> for expectation_1site's reason, stated there. Adjacent sites only: at arbitrary separation this becomes a transfer-matrix walk with a caching strategy (YASTN's measure_2site(bonds=), a 75-line body; tenpy's correlation_function), and every Hamiltonian here is nearest-neighbour. The operator-applied pair is split by the exact tenet.linalg.svd purely to hand _braket two rank-3 sites again.

expectation_profile

expectation_profile(
    psi: MPS, o: SymmetricTensor
) -> list[float]

<psi|o_n|psi> / <psi|psi> at every site, in one pass over the chain.

Parameters:

Name Type Description Default
psi MPS

The state; any gauge, any norm, and not modified -- the walk runs on a copy.

required
o SymmetricTensor

The operator, rank 2 on (phys OUT, phys IN) -- local_op's invariant one-site form, exactly as expectation_1site takes it.

required

Returns:

Type Description
list of float

One normalized expectation value per site, in site order.

Raises:

Type Description
ValueError

If o is not rank 2.

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import MPS, expectation_profile, local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1), U1Sector(1)])
>>> sz = local_op(np.diag([-0.5, 0.5]), phys=phys)
>>> [round(v, 6) for v in expectation_profile(psi, sz)]
[0.5, -0.5, 0.5]
Notes

One pass, not one pass per site. [expectation_1site(psi, o, n) for n in range(len(psi))] is the same numbers and costs two full-chain transfer passes per site, i.e. O(N**2) transfer contractions for the profile every DMRG user plots. This walks the chain once, moving the orthogonality centre right by a qr at each step and reading the operator off the centre -- which a canonical MPS makes exact, because everything left of the centre is left-orthonormal and everything right of it right-orthonormal, so both halves of the transfer close to the identity and <o_n> = <A_n|o|A_n>. Both references do exactly this (YASTN measure_1site(..., sites=None), TenPy expectation_value(ops)), and tests/network/test_measure.py counts the contractions rather than claiming them.

psi.copy().canonize_(0) first, so no gauge is assumed of the input and the input is not re-gauged: the same choice MPS.schmidt_values makes, for the same reason. The normalization is then free -- canonize_ leaves a unit-norm state -- which is why no second transfer pass computes <psi|psi>.

Divided by <psi|psi>, matching expectation_1site; the reason the divided and undivided readings carry different names is stated there.

local_op

local_op(
    dense: Any,
    *,
    phys: GradedSpace,
    charge: Sector | None = None,
) -> SymmetricTensor

A dense operator as a term operator: rank 3 with a charge leg, or invariant on k sites.

Parameters:

Name Type Description Default
dense array_like

The operator's dense matrix: (d, d) with charge, or (d**k, d**k) / (d,) * 2k without one, k inferred.

required
phys GradedSpace

The physical space (d = phys.dim). Keyword-only.

required
charge Sector or None

The sector the operator emits onto its MPO bond; None (the default) builds the invariant k-site form. Keyword-only.

None

Returns:

Type Description
SymmetricTensor

Rank 3 on (phys OUT, phys IN, charge OUT) with charge, rank 2k on (phys OUT)*k then (phys IN)*k without.

Raises:

Type Description
ValueError

With charge, if the array is not (d, d) on this phys; with charge=None, if no integer k makes the shape (d**k, d**k) or (d,) * 2k. A symmetry-forbidden array raises inside from_dense rather than being projected (see Notes).

Examples:

>>> import numpy as np
>>> from tenet import GradedSpace
>>> from tenet.network import local_op
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> sp = np.array([[0.0, 0.0], [1.0, 0.0]])  # S^+ raises 2 S^z by 2
>>> local_op(sp, phys=phys, charge=U1Sector(-2)).ndim  # the charge-leg form
3
>>> sz = np.diag([-0.5, 0.5])
>>> local_op(np.kron(sz, sz), phys=phys).ndim  # one invariant 2-site term
4
Notes

With charge, a (d, d) array becomes rank 3 on (phys OUT, phys IN, charge OUT). The third leg is why that form exists: S^+ raises 2 S^z by 2, so as a rank-2 tensor it is symmetry-forbidden and from_dense refuses it, correctly; the charge has to live on a leg. Invariance reads q(p_out) + q(charge) = q(p_in), so charge is literally the MPO bond the operator emits.

With charge=None the array is one whole term spanning k sites -- (d**k, d**k) or (d,)*2k, k inferred from d, the layout np.kron(a, b) already has -- and the result is rank 2k on (phys OUT)*k then (phys IN)*k with no auxiliary leg at all. A term is a scalar under the symmetry, so this form cannot express a symmetry-breaking term: on SU(2) legs Sz (x) Sz alone raises and S.S builds. A non-Abelian term's coupling lives inside the array's own blocks, which is why it needs no coupling-tree argument; MPO.from_terms splits it with svd_truncated and the MPO bond comes out of that SVD.

k = 1 is a one-site term, rank 2 and the same (d, d) shape the charge-leg form takes. It is the form a one-site term takes on any grading and the only one it can take where irrep_dim > 1: the charge-leg form emits onto a D=1 dense leg, which a multi-dimensional irrep is not. Where both forms exist they build the same MPO.

Both forms are built at from_dense's default relative atol, so an array that does not match what it was declared to be raises. The matrices themselves are physics and stay in the caller.

overlap

overlap(bra: MPS, ket: MPS) -> float

<bra|ket> by one transfer pass, undivided by either state's norm.

Parameters:

Name Type Description Default
bra MPS

The state that is conjugated; any gauge, any norm.

required
ket MPS

The state that is not; any gauge, any norm. The two may carry different bond spaces, and must have the same number of sites.

required

Returns:

Type Description
float

<bra|ket>.

Raises:

Type Description
ValueError

If the two states have different lengths.

Examples:

>>> from tenet import GradedSpace
>>> from tenet.network import MPS, overlap
>>> from tenet.symmetry import U1, U1Sector
>>> phys = GradedSpace.new(U1, {U1Sector(-1): 1, U1Sector(1): 1})
>>> psi = MPS.product(phys, [U1Sector(1), U1Sector(-1)])
>>> phi = MPS.product(phys, [U1Sector(1), U1Sector(-1)])
>>> round(overlap(phi, psi), 12)
1.0
>>> round(overlap(psi, psi) - psi.norm() ** 2, 12)
0.0
Notes

Undivided, the convention Env.measure already keeps and the one both references keep for this function -- YASTN's measure_overlap(bra, ket) and vdot, TenPy's MPS.overlap(other). A fidelity is overlap(phi, psi) / (phi.norm() * psi.norm()) and the caller spells the division, because the two states it needs are the caller's. expectation_1site divides and says so in its own name; a measure_-shaped name here would make one verb mean two things.

The two chains may carry different bond spaces: the transfer tensor holds one index from each, which is the same fact Env(psi, h, bra=phi) rests on one level up. Two states whose boundary legs sit in different sectors have no coupled sector at all and the overlap is structurally zero rather than numerically small.

MPS.norm is overlap(psi, psi) ** 0.5, and Env(psi, MPO.identity(len(psi), phys), bra=phi).measure() is this number computed the long way, through the environment cache; both agreements are tested.

append_vec_bl

append_vec_bl(
    a: DoublePepsTensor, vec: SymmetricTensor
) -> SymmetricTensor

YASTN append_vec_bl. Absorb a into a bottom-left vector.

Parameters:

Name Type Description Default
a DoublePepsTensor

The site to absorb.

required
vec SymmetricTensor

Rank 6, (x, b_ket, b_bra, l_ket, l_bra, y).

required

Returns:

Type Description
SymmetricTensor

Rank 6, (x, r_ket, r_bra, y, t_ket, t_bra) -- YASTN's x [r r'] y [t t'].

Notes

The bend-free corner, matching cor_bl. Step 1: the bra supplies IN on both b_bra and l_bra, so the bra is operand 1. Step 2: the running result supplies IN on b_ket, l_ket and phys alike, so it is operand 1. Three wires, three agreements, nothing bends.

append_vec_br

append_vec_br(
    a: DoublePepsTensor, vec: SymmetricTensor
) -> SymmetricTensor

YASTN append_vec_br. Absorb a into a bottom-right vector.

Parameters:

Name Type Description Default
a DoublePepsTensor

The site to absorb.

required
vec SymmetricTensor

Rank 6, (x, r_ket, r_bra, b_ket, b_bra, y).

required

Returns:

Type Description
SymmetricTensor

Rank 6, (x, t_ket, t_bra, y, l_ket, l_bra) -- YASTN's x [t t'] y [l l'].

Notes

The mirror of append_vec_tl. Step 1: the vector supplies IN on r_bra, the bra on b_bra, so the vector is operand 1 and b_bra bends. Step 2: the running result supplies IN on b_ket and phys, the ket on r_ket, so the running result is operand 1 and r_ket bends. YASTN's gates: b b' x r' before the bra, l l' x t' after the ket.

append_vec_tl

append_vec_tl(
    a: DoublePepsTensor, vec: SymmetricTensor
) -> SymmetricTensor

YASTN append_vec_tl. Absorb a into a top-left vector.

Parameters:

Name Type Description Default
a DoublePepsTensor

The site to absorb.

required
vec SymmetricTensor

Rank 6, (x, l_ket, l_bra, t_ket, t_bra, y).

required

Returns:

Type Description
SymmetricTensor

Rank 6, (x, b_ket, b_bra, y, r_ket, r_bra) -- YASTN's x [b b'] y [r r'].

Notes

Step 1 contracts the vector's bra pair into the bra: wire t_bra has the vector supplying IN and wire l_bra has the bra supplying it, so the vector is operand 1 and l_bra bends. Step 2 contracts l_ket, t_ket and the physical wire into the ket: the running result supplies IN on l_ket (from the vector) and on phys (from the bra), the ket supplies it on t_ket, so the running result is operand 1 and t_ket bends. YASTN's two gates here are t' x l l' before the bra and b b' x r' after the ket.

append_vec_tr

append_vec_tr(
    a: DoublePepsTensor, vec: SymmetricTensor
) -> SymmetricTensor

YASTN append_vec_tr. Absorb a into a top-right vector.

Parameters:

Name Type Description Default
a DoublePepsTensor

The site to absorb.

required
vec SymmetricTensor

Rank 6, (x, t_ket, t_bra, r_ket, r_bra, y).

required

Returns:

Type Description
SymmetricTensor

Rank 6, (x, l_ket, l_bra, y, b_ket, b_bra) -- YASTN's x [l l'] y [b b'].

Notes

Step 1 needs no bend: the vector supplies IN on both t_bra and r_bra, so the vector is operand 1 and both wires already run the right way. Step 2 is the one place a site leads: the ket supplies IN on t_ket and r_ket against the running result's one wire (phys), so the ket is operand 1 and the physical wire bends -- the same asymmetry cor_tr shows, for the same reason.

cor_bl

YASTN cor_bl. Close l, b and phys; keep (r_k, r_b, t_k, t_b).

Parameters:

Name Type Description Default
a DoublePepsTensor

The site.

required

Returns:

Type Description
SymmetricTensor

Rank 4, YASTN's [r r'] [t t'] with each pair left unfused.

Notes

Wires l, b, s; IN comes from bra on all three, so bra is operand 1 and nothing bends. YASTN's cor_bl is likewise its one corner with no swap_gate at all -- the two conventions agree on which corner is the free one.

cor_br

YASTN cor_br. Close b, r and phys; keep (t_k, t_b, l_k, l_b).

Parameters:

Name Type Description Default
a DoublePepsTensor

The site.

required

Returns:

Type Description
SymmetricTensor

Rank 4, YASTN's [t t'] [l l'] with each pair left unfused.

Notes

Wires b, r, s; IN comes from bra, ket, bra. Bra is operand 1 and r is bent -- against YASTN's cbr.swap_gate(axes=((1, 3), 2)), the l l' x t' string.

cor_tl

YASTN cor_tl. Close t, l and phys; keep (b_k, b_b, r_k, r_b).

Parameters:

Name Type Description Default
a DoublePepsTensor

The site.

required

Returns:

Type Description
SymmetricTensor

Rank 4, YASTN's [b b'] [r r'] with each pair left unfused.

Notes

Wires t, l, s; IN comes from ket, bra, bra. Two of three say bra, so bra is operand 1 and t is bent. YASTN's counterpart is ctl.swap_gate(axes=((0, 2), 3)), the b b' x r' string it writes before fusing.

cor_tr

YASTN cor_tr. Close t, r and phys; keep (l_k, l_b, b_k, b_b).

Parameters:

Name Type Description Default
a DoublePepsTensor

The site.

required

Returns:

Type Description
SymmetricTensor

Rank 4, YASTN's [l l'] [b b'] with each pair left unfused.

Notes

Wires t, r, s; IN comes from ket, ket, bra. This is the one corner where the ket is operand 1, and the physical wire is the bent one. YASTN reaches the same object from the other end: it swap-gates both layers up front (A.swap_gate(axes=(0, 1, 2, 3)), t x l and b x r) and then needs no post-contraction gate.

edge_b

YASTN edge_b. Close b and phys; keep r, t, l pairs.

Parameters:

Name Type Description Default
a DoublePepsTensor

The site.

required

Returns:

Type Description
SymmetricTensor

Rank 6, YASTN's [r r'] [t t'] [l l'] with each pair left unfused.

Notes

Wires b and s, both supplied IN by the bra: bra is operand 1, nothing bends.

edge_l

YASTN edge_l. Close l and phys; keep b, r, t pairs.

Parameters:

Name Type Description Default
a DoublePepsTensor

The site.

required

Returns:

Type Description
SymmetricTensor

Rank 6, YASTN's [b b'] [r r'] [t t'] with each pair left unfused.

Notes

Wires l and s, both supplied IN by the bra: bra is operand 1, nothing bends.

edge_r

YASTN edge_r. Close r and phys; keep t, l, b pairs.

Parameters:

Name Type Description Default
a DoublePepsTensor

The site.

required

Returns:

Type Description
SymmetricTensor

Rank 6, YASTN's [t t'] [l l'] [b b'] with each pair left unfused.

Notes

Wires r (ket) and s (bra); tie broken toward the bra as in edge_t, so bra is operand 1 and r bends.

edge_t

YASTN edge_t. Close t and phys; keep l, b, r pairs.

Parameters:

Name Type Description Default
a DoublePepsTensor

The site.

required

Returns:

Type Description
SymmetricTensor

Rank 6, YASTN's [l l'] [b b'] [r r'] with each pair left unfused.

Notes

Wires t (ket) and s (bra): one each, so the count does not decide and the tie is broken the way three of the four corners fall -- bra is operand 1, t bends. One bend either way; taking the same operand order as cor_tl/cor_bl/ cor_br is what keeps the family readable.