Algebra#
Canonical form by QR#
There are different algorithms, which can bring MPS/MPO into a canonical form.
The cheapest way is by application of QR decomposition
.
# Generate random MPS with no symmetry
psi = mps.random_dense_mps(N=16, D=15, d=2)
# rigth canonical form
#
# --A*-- --
# | | = |Identity
# --A--- --
#
psi.canonize_(to='first')
# left canonical form
#
# --A*-- --
# | | = Identity|
# --A--- --
#
psi.canonize_(to='last')
Check if MPS/MPO is in left/right canonical form by verifying if each tensor forms an isometry after appropriate contraction with its conjugate.
def check_canonize(psi, tol):
""" Canonize mps to left and right, running tests if it is canonical. """
ref_s = (-1, 1, 1) if psi.nr_phys == 1 else (-1, 1, 1, -1)
norm = psi.norm()
for to in ('last', 'first'):
psi.canonize_(to=to, normalize=False)
assert psi.is_canonical(to=to, tol=tol)
assert all(psi[site].s == ref_s for site in psi.sweep())
assert psi.pC is None
assert len(psi.A) == len(psi)
assert abs(psi.factor / norm - 1) < tol
assert abs(mps.vdot(psi, psi) / norm ** 2 - 1) < tol
for to in ('last', 'first'):
phi = psi.shallow_copy()
phi.canonize_(to=to)
assert abs(phi.factor - 1) < tol
assert abs(mps.vdot(phi, phi) - 1) < tol
Canonical form by SVD#
Bringing MPS/MPO into canonical form through SVD decomposition
is computationally more expensive than QR, but allows for truncation.
Truncation is governed by options passed as opts_dict
to yastn.linalg.truncation_mask()
that is applied
after each SVD during the sweep through MPS/MPO.
Note
Faithfull SVD truncation requires MPS/MPO to be in the canonical form of the opposite direction to the direction of the truncation sweep.
# There are different options which we can pass, see `truncation_mask`.
# Defaults are assumed for options not explictly specified.
#
opts_svd = {
'D_total': 4, # total number of singular values to keep
'D_block': 2, # maximal number of singular values
# to keep in a single block
'tol': 1e-6, # relative tolerance of singular values
# below which to truncate across all blocks
'tol_blocks': 1e-6 # relative tolerance of singular values
# within individual blocks below which to
}
# Generate random MPS with no symmetry
#
psi = mps.random_dense_mps(N=16, D=15, d=2)
# Bring MPS to canonical form and truncate (here, right canonical form).
# For MPS we usually normalize the state.
#
psi.canonize_(to='last')
psi.truncate_(to='first', opts_svd=opts_svd)
# Generate random MPO with no symmetry
#
H = mps.random_dense_mpo(N=16, D=25, d=2)
# Bring MPO to canonical form and truncate (here, left canonical form).
# For MPO we do not want to change overall scale, thus no normalization.
#
H.canonize_(to='first', normalize=False)
H.truncate_(to='last', opts_svd=opts_svd, normalize=False)
Multiplication#
We can test the multiplication of MPO and MPS using an example of a ground state obtained with DMRG.
def multiplication_example_gs(config=None, tol=1e-12):
"""
Calculate ground state and tests, within eigen-condition,
functions mps.multiply, __mul__, mps.zipper and mps.add
This test presents multiplication as part of the DMRG study.
We use multiplication to get expectation values from a state.
Knowing the exact solution, we will compare it to the value we obtain.
"""
N = 7
Eng = -3.427339492125
t, mu = 1.0, 0.2
#
opts_config = {} if config is None else \
{'backend': config.backend,
'default_device': config.default_device}
# pytest uses config to inject various backends and devices for testing
ops = yastn.operators.SpinlessFermions(sym='U1', **opts_config)
#
# The Hamiltonian is obtained using Hterm.
#
I = mps.product_mpo(ops.I(), N) # identity MPO
terms = []
for i in range(N - 1):
terms.append(mps.Hterm(t, (i, i+1), (ops.cp(), ops.c())))
terms.append(mps.Hterm(t, (i+1, i), (ops.cp(), ops.c())))
for i in range(N):
terms.append(mps.Hterm(mu, [i], [ops.cp() @ ops.c()]))
H = mps.generate_mpo(I, terms)
#
# To standardize this test we fix a seed for random MPS we use.
#
ops.random_seed(seed=0)
#
# In this example we use yastn.Tensor with U1 symmetry.
#
total_charge = 3
psi = mps.random_mps(I, D_total=8, n=total_charge)
#
# We set truncation for DMRG and run the algorithm in '2site' version.
#
opts_svd = {'D_total': 8}
opts_dmrg = {'max_sweeps': 20, 'Schmidt_tol': 1e-14}
out = mps.dmrg_(psi, H, method='2site', **opts_dmrg, opts_svd=opts_svd)
out = mps.dmrg_(psi, H, method='1site', **opts_dmrg)
#
# Test if we obtained the exact solution for energy?:
#
assert abs(out.energy - Eng) < tol
#
# We should have the ground state.
# Now we calculate the variation of energy <H^2>-<H>^2=<(H-Eng)^2>
# to check if DMRG converged properly to tol.
# There are a few ways to do that:
#
# case 1/
# Find approximate Mps representing H @ psi. This is done in two steps.
# First, zipper function gives a good first approximation
# though a series of svd.
#
Hpsi = mps.zipper(H, psi, opts_svd={"D_total": 8}) # here, Hpsi is normalized
#
# Second, compression is optimizing the overlap
# between approximation and a product H @ psi.
#
mps.compression_(Hpsi, (H, psi), method='2site', max_sweeps=5,
Schmidt_tol=1e-6, opts_svd={"D_total": 8},
normalize=False) # norm of Hpsi approximates norm of H @ psi
#
# Use mps.norm() to get variation.
# Norm canonizes a copy of the state and, close to zero,
# is more precise than direct contraction using vdot.
#
p0 = Eng * psi - Hpsi
assert p0.norm() < tol
#
# case 2/
# Here H @ psi is calculated exactly with resulting
# bond dimension being a product of bond dimensions of H and psi.
#
Hpsi = mps.multiply(H, psi)
p0 = Eng * psi - Hpsi
assert p0.norm() < tol
#
# Equivalently we can call.
Hpsi = H @ psi
p0 = Eng * psi - Hpsi
assert p0.norm() < tol