Here we demonstrate 2-site periodic DMRG for finding the groundstate of the spin-1/2 Heisenberg model, and performing a couple of calculations efficiently with the resulting periodic MPS.
In [1]:
from quimb import *
from quimb.tensor import *
In [2]:
H = MPO_ham_heis(300, cyclic=True)
quimb has the function heisenberg_energy which can calculate the analytic energy we are looking for:
In [3]:
E_exact = heisenberg_energy(300)
E_exact
Out[3]:
Let's create the core DMRG object that handles all the algorithm:
In [4]:
dmrg = DMRG2(H)
DMRG2 internally forms the needed energy and norm overlaps, reusing views of the same data. We can graph, for example, the full energy expectation:
In [5]:
%matplotlib inline
dmrg.TN_energy.graph(color=['_KET', '_HAM', '_BRA']) # might be slow as uses force repulsion
Or if we want to plot with fixed positions:
In [6]:
from cmath import exp, pi
fix = {
**{(f'I{i}', '_KET'): (100 * exp(2j*pi * i / 300).real, 100 * exp(2j*pi * i / 300).imag) for i in range(300)},
**{(f'I{i}', '_HAM'): (105 * exp(2j*pi * i / 300).real, 105 * exp(2j*pi * i / 300).imag) for i in range(300)},
**{(f'I{i}', '_BRA'): (110 * exp(2j*pi * i / 300).real, 110 * exp(2j*pi * i / 300).imag) for i in range(300)},
}
In [7]:
dmrg.TN_energy.graph(color=['_KET', '_HAM', '_BRA'], fix=fix, iterations=0)
The default algorithm settings are reasonable enough to get started with:
In [8]:
dmrg.solve(max_sweeps=4, verbosity=1, cutoffs=1e-6)
Out[8]:
We are getting pretty close to the known energy already (closer than OBC at this length can get). The relative error is:
In [9]:
(dmrg.energy - E_exact) / abs(E_exact)
Out[9]:
Note that for PBC, the algorithm splits the chain into segments, and approximates the other segments with a SVD (the accuracies of the energies above are limited by this). Thus progress appears to pause at these points. The number of singular values kept for this environment approximation is recorded in dmrg.bond_sizes_ham and dmrg.bond_sizes_norm:
In [10]:
dmrg.bond_sizes_norm
Out[10]:
In [11]:
dmrg.bond_sizes_ham
Out[11]:
To progress further might require tweaking the advanced options, for example, setting tighter tolerances for some of the settings found in:
In [12]:
dmrg.opts
Out[12]:
See quimb.tensor.tensor_dmrg.get_default_opts for detailed explanations of these quantities.
One could also supply custom sequences for the maximum allowed bond dimensions (e.g. dmrg.solve(..., bond_dims=[70, 80, 90])) or bond compression cutoffs (e.g. dmrg.solve(..., cutoffs=[1e-9, 3e-10, 1e-10])).
PBC DMRG error is, in particular, limited by the segment compression tolerances.
The full state can be retrieved from dmrg.state:
In [13]:
gs = dmrg.state
gs.max_bond()
Out[13]:
In [14]:
sz = spin_operator('Z').real
gs.correlation(sz, 0, 1)
Out[14]:
In [15]:
%debug
However, if one was computing this for many sites, it would make sense to manually reuse parts of each contraction.
For example, if we are only interested in the first n sites, we can approximate the rest with an SVD:
In [16]:
# Set up an overlap
p = dmrg.state
p.add_tag('KET')
q = p.H.retag({'KET': 'BRA'})
qp = q & p
# Replace all but 20 sites with an SVD
qp.replace_section_with_svd(20, 300, eps=1e-6, inplace=True, ltags='L', rtags='R')
qp.graph(color=['BRA', 'KET', 'L', 'R'])
Now we can define a correlation function on this much smaller network:
In [17]:
def sz_corr(i, j):
itag = f"I{i}"
jtag = f"I{j}"
qp_i = qp.insert_operator(sz, ('KET', itag), ('BRA', itag))
c_i = qp_i ^ all
qp_j = qp.insert_operator(sz, ('KET', jtag), ('BRA', jtag))
c_j = qp_j ^ all
qp_ij = qp_i.insert_operator(sz, ('KET', jtag), ('BRA', jtag))
c_ij = qp_ij ^ all
return c_ij - c_i * c_j
We can then use this to compute the 20 correlations efficiently:
In [18]:
js = range(1, 20)
cs = [sz_corr(0, j) for j in js]
In [19]:
import matplotlib.pyplot as plt
plt.plot(js, cs)
Out[19]:
Which looks as expected.
In [20]:
sysa = range(0, 50)
sysb = range(50, 100)
rho_ab = gs.partial_trace_compress(sysa, sysb, max_bond=2**6, method='isvd')
In [21]:
rho_ab.ind_sizes()
Out[21]:
Let's plot this:
In [22]:
# specify some coordinates to plot the remaining tensors
fix = {('_UP', '_SYSA'): (-1, +1), ('_DOWN', '_SYSA'): (-1, -1), 'kA': (-1, 1.5), 'bA': (-1, -1.5),
('_UP', '_SYSB'): (+1, +1), ('_DOWN', '_SYSB'): (+1, -1), 'kB': (+1, 1.5), 'bB': (+1, -1.5)}
In [23]:
rho_ab.graph(color=['_SYSA', '_ENVR', '_SYSB'], show_inds=False, fix=fix)
You can see that because the state has PBC, there is a split 'environment' tensor carrying correlations the 'long-way-round'.
We can also check it's still normalized:
In [24]:
rho_ab.trace(['kA', 'kB'], ['bA', 'bB'])
Out[24]:
We could also estimate the genuine entanglement between the two subsytems. First we convert the compressed representation into a dense matrix, whilst also partially transposing one side:
In [25]:
# form single tensor
rho_ab_d = rho_ab ^ all
# turn tensor into a normal array whilst also partially transposing
rho_ab_pt_d = rho_ab_d.to_dense(['kA', 'bB'],
['bA', 'kB'])
rho_ab_pt_d.shape
Out[25]:
Finally compute $\log_2 \left|\rho_{AB}^{T_B} \right|$:
In [26]:
E = log2(sum(abs(eigvalsh(rho_ab_pt_d))))
Which gives the logarithmic negativity between the two regions as (approximately because of the limited bond in the compression):
In [27]:
E
Out[27]: