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 throughdmrg_'sorthogonal_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_*andappend_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, andupdate_/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 ofAandflip(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_metricsupplies -- 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 |
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 |
cutoff |
(float, optional)
|
The singular-value cutoff handed to the same SVD. Default |
noise |
(float, optional)
|
Relative strength of the perturbation sweep_
mixes in at each split. Default |
noise_type |
(str, optional)
|
Which perturbation |
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 ¶
<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 |
None
|
compile
|
Callable or None
|
Wraps the prepared matvec once per structure key -- |
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
|
Returns:
| Type | Description |
|---|---|
Env
|
|
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If |
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_ ¶
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: |
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_ ¶
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, |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
|
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_wof 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 issymbolic=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 |
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,
|
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The same rank-4 structure on the bra chain's bonds:
|
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 ¶
<psi|H|psi> without the eigensolver, on a private left-to-right pass.
Returns:
| Type | Description |
|---|---|
float
|
|
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; |
converged |
bool
|
Whether |
EnvCTM ¶
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'
|
bra
|
Peps or None
|
An independent bra for the double layer. Default |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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.
wire ¶
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_ ¶
Seed every corner and edge, YASTN reset_:239.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
init
|
str
|
|
'eye'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
update_ ¶
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
|
moves
|
str
|
A sequence of moves. |
'hv'
|
cutoff
|
float or None
|
Relative singular-value cutoff for the projector truncation.
Default |
1e-14
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
corner_spectra ¶
Every corner's singular values, largest scaled to one -- YASTN
calculate_corner_svd:468.
Returns:
| Type | Description |
|---|---|
dict
|
|
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
|
moves
|
str
|
The sweep's moves; |
'hv'
|
max_sweeps
|
int
|
The sweep budget. Default |
100
|
corner_tol
|
float or None
|
Stop when the worst corner's spectrum moves less than this. |
1e-10
|
cutoff
|
float or None
|
Relative singular-value cutoff for the projector truncation.
Default |
1e-14
|
Returns:
| Type | Description |
|---|---|
CTMRG_out
|
|
Raises:
| Type | Description |
|---|---|
StructureChangingError
|
Under |
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 |
required |
q1
|
SymmetricTensor
|
The two reduced site tensors, rank 5, whose bond legs the metric is on --
truncate_'s |
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
|
|
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
Rank 4, |
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 |
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.
EnvCTMc4v ¶
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'
|
bra
|
Peps or None
|
An independent bra for the double layer. Default |
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 |
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_ ¶
Seed the corner and the edge, YASTN reset_.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
init
|
str
|
|
'eye'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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
|
moves
|
str
|
|
'd'
|
cutoff
|
float or None
|
Relative singular-value cutoff for the projector truncation.
Default |
1e-14
|
bond
|
GradedSpace or None
|
A frozen environment bond. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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. |
t |
SymmetricTensor or None
|
The edge, rank 3 (single layer) or rank 4 (double layer). |
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 ¶
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 |
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: |
required |
which
|
str
|
The cluster. |
'NN'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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
|
|
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
Rank 4, |
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
|
|
nonhermitian_part |
float
|
|
min_eigenvalue |
float or None
|
The smallest eigenvalue of the symmetrized metric over |
wrong_eigenvalues |
float or None
|
The fraction of eigenvalues below the error scale, which |
iterations |
int
|
Least-squares sweeps taken, |
pinv_cutoff |
float or None
|
The pseudo-inverse cutoff the last solve chose off
|
Examples:
Gate ¶
Bases: NamedTuple
A nearest-neighbour two-site gate, already split across its bond.
Attributes:
| Name | Type | Description |
|---|---|---|
g0 |
SymmetricTensor
|
The half acting on |
g1 |
SymmetricTensor
|
The half acting on |
bond |
Bond
|
The bond, oriented in the fermionic order -- |
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 ¶
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
Lattice ¶
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 |
required |
objects
|
optional
|
One object, a nested sequence, or a |
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.
RectangularUnitcell ¶
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 |
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).
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 ¶
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 |
(2, 2)
|
boundary
|
str
|
|
'infinite'
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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.
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
|
|
None
|
reverse
|
bool
|
Reverse the sequence (and, for |
False
|
Returns:
| Type | Description |
|---|---|
tuple[Bond, ...]
|
Each bond with |
nn_site ¶
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; |
required |
d
|
str or tuple[int, int]
|
One of |
required |
Returns:
| Type | Description |
|---|---|
Site or None
|
|
nn_bond_dirn ¶
'lr', 'tb', 'rl' or 'bt' for a nearest-neighbour pair.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the two sites are not nearest neighbours. |
f_ordered ¶
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 ¶
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 |
None
|
edges
|
EdgeTable or None
|
The edge description the builders keep under |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither or both of |
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 ¶
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 |
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 |
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 |
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 |
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 |
required |
start
|
int
|
The row of |
required |
end
|
int
|
The column of |
required |
Returns:
| Type | Description |
|---|---|
MPO
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
From |
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
|
required |
symbolic
|
bool
|
Keep the finite-state-machine description, so
Env.heff2 runs the term-family matvec on the
prepared path. Default |
False
|
Returns:
| Type | Description |
|---|---|
MPO
|
The assembled operator as rank-4 site tensors, so it takes
Env.heff2's site-tensor path. With
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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
|
|
required |
cutoff
|
float or None
|
The two compressing SVD sweeps' cutoff. |
1e-13
|
symbolic
|
bool
|
Keep the finite-state-machine description, so
Env.heff2 runs the term-family matvec on the
prepared path. Default |
False
|
Returns:
| Type | Description |
|---|---|
MPO
|
The assembled operator as rank-4 site tensors, so it takes
Env.heff2's site-tensor path. With
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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
ksites, any symmetry. tenet.linalg.svd_truncated peels it intokMPO 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 = 1is a one-site term: no cut is made, so the operator rides the identity channel between twoD=1unit bonds. It is the form an on-siteU, a chemical potential or a field takes on every grading, and the only one available whereirrep_dim > 1, since the charge-leg form's emitted leg has to beD=1dense.
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=Nonefor 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 noWcontraction, which is the cheaper sweep.- a float
cutofffor 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
|
required |
blocks
|
Iterable
|
One |
required |
cutoff
|
float or None
|
The compressing SVD sweeps' cutoff, with
from_terms's three-way meaning
unchanged. Default |
1e-13
|
screen
|
float
|
Coefficient magnitude threshold, applied after the merge:
a merged term survives when |
1e-12
|
symbolic
|
bool
|
Keep the finite-state-machine description, so
Env.heff2 runs the term-family matvec on the
prepared path. Default |
False
|
Returns:
| Type | Description |
|---|---|
MPO
|
The assembled operator as rank-4 site tensors, so it takes
Env.heff2's site-tensor path. With
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If an entry of |
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->rulefrom_termsapplies 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 con one site, say) is dropped. This is the burdenfrom_termsrefuses 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 ¶
The full d**N x d**N operator, D=1 boundaries dropped.
Returns:
| Type | Description |
|---|---|
array
|
The backend's dense matrix of shape |
Notes
MPS.to_dense's twin, with its warning: exponential in N,
an oracle exit
for tests, and nothing an algorithm calls.
apply ¶
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 |
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 |
required |
Returns:
| Type | Description |
|---|---|
float
|
The energy variance. Zero for an exact eigenstate, and it falls as |
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 |
required |
center
|
int or None
|
The orthogonality centre; |
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.
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 |
required |
seed
|
int
|
Site |
0
|
Returns:
| Type | Description |
|---|---|
MPS
|
A random state with |
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 |
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 |
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 |
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
|
Returns:
| Type | Description |
|---|---|
MPS
|
|
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If |
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 ¶
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 ¶
The full d**N amplitude array, D=1 boundaries dropped.
Returns:
| Type | Description |
|---|---|
array
|
The backend's dense amplitude array of shape |
Notes
Exponential in N: an oracle exit for tests, and nothing an algorithm calls.
compress_ ¶
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
|
Returns:
| Type | Description |
|---|---|
float
|
|
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 ¶
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 |
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 ¶
The entanglement entropy across every bond, in nats.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
float
|
The Renyi index handed to entropy. Default |
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 ¶
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 |
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, |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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, |
bra |
SymmetricTensor
|
Its partner, same leg order with every side flipped -- |
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.
Peps ¶
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 |
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 ¶
Whether the sites carry a physical leg (rank 5) or not (rank 4).
Peps2Layers ¶
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
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
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 |
required |
a
|
SymmetricTensor
|
The operands. Typed |
required |
b
|
SymmetricTensor
|
The operands. Typed |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
The contraction, taken in whichever operand order bends the fewer wires, as a
one-step |
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 |
required |
alpha
|
float
|
The Renyi index. Default |
1.0
|
Returns:
| Type | Description |
|---|---|
float
|
The entropy across the cut, in nats. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
Examples:
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, |
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 |
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
|
|
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
|
None
|
chi
|
int or None
|
Flat bond-dimension cap for every sweep. Default |
None
|
cutoff
|
float or None
|
Flat singular-value cutoff for every sweep. Default |
None
|
energy_tol
|
float
|
Energy-change convergence threshold. Default |
1e-12
|
schmidt_tol
|
float
|
Worst-cut Schmidt-change convergence threshold. Default |
1e-08
|
max_sweeps
|
int
|
Sweep budget. Default |
40
|
ncv
|
int
|
Krylov-space dimension for lanczos.
Default |
3
|
orthogonal_to
|
Sequence of MPS or None
|
Already-converged states to hold |
None
|
seed
|
int
|
Feeds sweep_'s noise draw, distinctly per
sweep; a schedule with |
0
|
callback
|
Callable[[DMRG_out], None] or None
|
Invoked once per sweep with that sweep's DMRG_out.
Default |
None
|
compile
|
Callable or None
|
Handed verbatim to Env, which wraps the prepared
two-site matvec with it once per structure key. |
None
|
Returns:
| Type | Description |
|---|---|
DMRG_out
|
The last sweep's record; its |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
required |
ncv
|
int
|
Krylov-space dimension. Default |
3
|
tol
|
float
|
The happy-breakdown threshold on the recurrence norm |
1e-13
|
orthogonal_to
|
Sequence of SymmetricTensor
|
Vectors on |
()
|
Returns:
| Name | Type | Description |
|---|---|---|
value |
float
|
The smallest ('SR') Ritz value. |
vector |
SymmetricTensor
|
The matching normalized Ritz vector, on |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
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
|
noise
|
float
|
Relative strength of the perturbation mixed in after the eigensolver
and before each split; |
0.0
|
noise_type
|
(wavefunction, perturbative)
|
Which perturbation |
"wavefunction"
|
orthogonal_to
|
Sequence of Env
|
Two-state environments, one per converged state to hold |
()
|
seed
|
int
|
Makes the noise draw at bond |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
energy |
float
|
The last |
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 |
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
|
required |
b
|
SymmetricTensor
|
The right operator, same form. |
required |
pairs
|
Sequence of (int, int) or None
|
The |
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 |
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 ¶
<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
|
|
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
|
|
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 |
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 |
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 ¶
flip(t: SymmetricTensor) -> SymmetricTensor
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
|
|
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 |
required |
r1
|
SymmetricTensor
|
The two |
required |
max_bond
|
int or None
|
The environment bond-dimension cap. Default |
None
|
cutoff
|
float or None
|
Relative singular-value cutoff for the truncation. Default |
1e-14
|
Returns:
| Name | Type | Description |
|---|---|---|
p0 |
SymmetricTensor
|
|
p1 |
SymmetricTensor
|
|
Raises:
| Type | Description |
|---|---|
StructureChangingError
|
Under |
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'
|
Returns:
| Type | Description |
|---|---|
float
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
required |
a1
|
SymmetricTensor
|
The rank-5 site tensors of |
required |
gate
|
Gate
|
The gate. |
required |
Returns:
| Type | Description |
|---|---|
tuple[SymmetricTensor, SymmetricTensor]
|
Rank 6 each: |
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 |
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_: |
{}
|
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 |
required |
step
|
float
|
The Trotter step. |
required |
bond
|
Bond or tuple[Site, Site]
|
The bond the gate belongs to, |
required |
Returns:
| Type | Description |
|---|---|
Gate
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
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
|
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 |
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
|
max_bond
|
int or None
|
The bond-dimension cap. Default |
None
|
cutoff
|
float or None
|
Relative singular-value cutoff of the initializing SVD. Default |
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 |
0.0
|
max_iter
|
int
|
Least-squares sweeps. Default |
20
|
tol_iter
|
float
|
Stop once the squared error falls below this. Default |
1e-13
|
pinv_cutoffs
|
Sequence of float
|
The pseudo-inverse ladder each solve chooses from. Default
|
PINV_CUTOFFS
|
Returns:
| Type | Description |
|---|---|
Evolution_out
|
The bond, the truncation error and what the metric was found to be. The error is
|
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 |
required |
n
|
int
|
The site, |
required |
Returns:
| Type | Description |
|---|---|
float
|
The normalized expectation value. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
required |
n
|
int
|
The pair's left site, |
required |
Returns:
| Type | Description |
|---|---|
float
|
The normalized expectation value on the adjacent pair. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
required |
Returns:
| Type | Description |
|---|---|
list of float
|
One normalized expectation value per site, in site order. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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: |
required |
phys
|
GradedSpace
|
The physical space ( |
required |
charge
|
Sector or None
|
The sector the operator emits onto its MPO bond; |
None
|
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
Rank 3 on |
Raises:
| Type | Description |
|---|---|
ValueError
|
With |
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 ¶
<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
|
|
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, |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
Rank 6, |
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, |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
Rank 6, |
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, |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
Rank 6, |
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, |
required |
Returns:
| Type | Description |
|---|---|
SymmetricTensor
|
Rank 6, |
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 ¶
cor_bl(a: DoublePepsTensor) -> SymmetricTensor
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 |
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 ¶
cor_br(a: DoublePepsTensor) -> SymmetricTensor
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 |
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 ¶
cor_tl(a: DoublePepsTensor) -> SymmetricTensor
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 |
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 ¶
cor_tr(a: DoublePepsTensor) -> SymmetricTensor
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 |
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 ¶
edge_b(a: DoublePepsTensor) -> SymmetricTensor
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 |
Notes
Wires b and s, both supplied IN by the bra: bra is operand 1, nothing
bends.
edge_l ¶
edge_l(a: DoublePepsTensor) -> SymmetricTensor
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 |
Notes
Wires l and s, both supplied IN by the bra: bra is operand 1, nothing
bends.
edge_r ¶
edge_r(a: DoublePepsTensor) -> SymmetricTensor
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 |
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 ¶
edge_t(a: DoublePepsTensor) -> SymmetricTensor
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 |
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.