Input File: Advanced Usage
Orbital Rotation
In this calculation we illustrate how to compute the ground state MPS in the given set of orbitals, find the (new) DMRG natural orbitals, transform integrals to new orbitals, transform the ground state MPS to new orbitals, and finally evaluate the energy of the transformed MPS in the new orbitals to verify the quality of the transformed MPS.
First, we compute the energy and 1-particle density matrix for the ground state using the following input file:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 0
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
onepdm
irrep_reorder
Note that we use the keyword irrep_reorder
to reorder the orbitals so that orbitals belonging to the same
point group irrep are grouped together. This can make the orbital rotation more local.
The DMRG occupation number (in original ordering) will be printed at the end of the calculation:
$ grep OCC dmrg-1.out
DMRG OCC = 1.957 1.625 1.870 1.870 0.361 0.098 0.098 0.006 0.008 0.008 0.008 0.013 0.014 0.014 0.011 0.006 0.006 0.006 0.005 0.005 0.002 0.002 0.002 0.001 0.001 0.001
$ grep Energy dmrg-1.out
DMRG Energy = -75.728467269121097
Second, we use the keyword nat_orbs
to compute the natural orbitals. The value of the keyword nat_orbs
specifies the filename for storing the rotated integrals (FCIDUMP).
If no value is associated with the keyword nat_orbs
, the rotated integrals will not be computed.
The keyword nat_orbs
can only be used together with restart_onepdm
or onepdm
, since natural orbitals
are found by diagonalizing 1-particle density matrix.
The following input file is used for this step (it can also be combined with the previous calculation):
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 0
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
restart_onepdm
nat_orbs C2.NAT.FCIDUMP
nat_km_reorder
nat_positive_def
irrep_reorder
Where the optional keyword nat_km_reorder
can be used to remove the artificial reordering in the natural orbitals
using Kuhn-Munkres algorithm. The optional keyword nat_positive_def
can be used to avoid artificial rotation in the
logarithm of the rotation matrix, by make the rotation matrix quasi-positive-definite, with “quasi” in the sense that
the rotation matrix is not Hermitian. The two options may be good for weakly correlated systems, but have limited effects
for highly correlated systems (but for highly correlated systems it is also recommended to be used).
The occupation number in natural orbitals will be printed at the end of the calculation:
$ grep OCC dmrg-2.out
DMRG OCC = 1.957 1.625 1.870 1.870 0.361 0.098 0.098 0.006 0.008 0.008 0.008 0.013 0.014 0.014 0.011 0.006 0.006 0.006 0.005 0.005 0.002 0.002 0.002 0.001 0.001 0.001
REORDERED OCC = 1.957 0.002 0.361 0.006 0.013 0.008 0.002 0.006 0.011 0.001 0.006 1.625 0.008 1.870 0.005 0.098 0.001 0.014 0.005 1.870 0.008 0.001 0.014 0.098 0.006 0.002
NAT OCC = 0.000465 0.003017 0.006424 0.007848 0.360936 1.968407 0.000081 0.000916 0.001991 0.004082 0.015623 1.628182 0.003669 0.008706 1.870680 0.000424 0.002862 0.110463 0.003667 0.008705 1.870678 0.000424 0.002862 0.110480 0.006422 0.001989
With the optional keyword nat_km_reorder
there will be an extra line:
REORDERED NAT OCC = 1.968407 0.000465 0.360936 0.006424 0.007848 0.003017 0.001991 0.000081 0.004082 0.000916 0.015623 1.628182 0.008706 1.870680 0.003669 0.110463 0.000424 0.002862 0.003667 1.870678 0.008705 0.000424 0.002862 0.110480 0.006422 0.001989
The rotation matrix for natural orbitals, the logarithm of the rotation matrix, and the occupation number in natural orbitals
are stored as nat_rotation.npy
, nat_kappa.npy
, nat_occs.npy
in scartch folder, respectively. In this example,
the rotated integral is stored as C2.NAT.FCIDUMP
in the working directory.
Third, we load the MPS in the old orbitals and transform it into the new orbitals. This is done using time evolution.
The keyword delta_t
is used to set a time step and indicate that this is a time evolution calculation.
The keyword orbital_rotation
is used to indicate that the operator (exponentiated) applied into the MPS should
be the orbital rotation operator (constructed from nat_kappa.npy
saved in the previous step).
Typically, a large bond dimension should be used depending how non-local the orbital rotation operator is.
The target_t
for orbital rotation is automatically set to 1.
The following input file is used for this step:
sym d2h
nelec 8
spin 0
irrep 1
hf_occ integral
schedule
0 1000 0 0
end
mps_tags BRA
orbital_rotation
delta_t 0.05
outputlevel 1
noreorder
Note that noreorder
must be used for orbital rotation. The orbital reordering
in previous step has already been taken into account.
The keyword te_type
can be used to set the time-evolution algorithm. The default is rk4
,
which is the original time-step-targeting (TST) method. Another possible choice is tdvp
,
which is the time dependent variational principle with the projector-splitting (TDVP-PS) algorithm.
The output looks like the following:
$ grep DW dmrg-3.out
Time elapsed = 2.263 | E = 0.0000000000 | Norm^2 = 0.9999999999 | DW = 1.76e-10
Time elapsed = 4.910 | E = -0.0000000000 | Norm^2 = 0.9999999997 | DW = 1.43e-10
Time elapsed = 1.663 | E = -0.0000000000 | Norm^2 = 0.9999999988 | DW = 4.46e-10
Time elapsed = 3.475 | E = 0.0000000000 | Norm^2 = 0.9999999983 | DW = 2.50e-10
... ...
Time elapsed = 3.011 | E = 0.0000000000 | Norm^2 = 0.9999999315 | DW = 1.04e-09
Time elapsed = 4.753 | E = 0.0000000000 | Norm^2 = 0.9999999284 | DW = 8.68e-10
Time elapsed = 1.786 | E = 0.0000000000 | Norm^2 = 0.9999999245 | DW = 1.07e-09
Time elapsed = 3.835 | E = 0.0000000000 | Norm^2 = 0.9999999213 | DW = 9.09e-10
Since in every time step an orthogonal transformation is applied on the MPS, the expectation value of the orthogonal transformation (printed as the energy expectation) calculated on the MPS should always be zero.
Note that largest discarded weight is 1.07e-09
, and the norm of MPS is not far away from 1.
So the transormation should be relatively accurate.
Finally, we calculate the energy expectation value using the transformed integral (C2.NAT.FCIDUMP
)
and the transformed MPS (stored in the scratch folder), using the following input file:
sym d2h
orbitals C2.NAT.FCIDUMP
nelec 8
spin 0
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
mps_tags BRA
restart_oh
restart_onepdm
noreorder
Note that noreorder
must be used, since the MPS generated in the previous step is in
unreordered natural orbitals.
The keyword restart_oh
will calculate the expectation value of the given Hamiltonian
loaded from integrals on the MPS loaded from scartch folder.
We have the following output:
$ grep Energy dmrg-4.out
OH Energy = -75.728457535820155
The difference compared to the energy generated in the first step
DMRG Energy = -75.728467269121097
is only 9.7E-6.
One can increase the bond dimension in the evolution to make this closer to the value printed
in the first step.
MPS Transform
The MPS can be copied and saved using another tag. For SU2 (spin-adapted) MPS, it can also be transformed to SZ (non-spin-adapted) MPS and saved using another tag.
Limitations:
Total spin zero spin-adapted MPS can be transformed directly.
For non-zero total spin, the spin-adapted MPS must be in singlet embedding format. See next section.
First, we compute the energy for the spin-adapted ground state using the following input file:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 0
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
irrep_reorder
mps_tags KET
The following script will read the spin-adapted MPS and tranform it to a non-spin-adapted MPS:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 0
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
irrep_reorder
mps_tags KET
restart_copy_mps ZKET
trans_mps_to_sz
Here the keyword restart_copy_mps
indicates that the MPS will be copied, associated with a value
indicating the new tag for saving the copied MPS.
If the keyword trans_mps_to_sz
is present, the MPS will be transformed to non-spin-adapted before
being saved.
Finally, we calculate the energy expectation value using non-spin-adapted formalism and the transformed MPS (stored in the scratch folder), using the following input file:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 0
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
irrep_reorder
mps_tags ZKET
restart_oh
nonspinadapted
Some reference outputs for this example:
$ grep Energy dmrg-1.out
DMRG Energy = -75.728467269121083
$ grep MPS dmrg-2.out
MPS = KRRRRRRRRRRRRRRRRRRRRRRRRR 0 2
GS INIT MPS BOND DIMS = 1 3 10 35 120 263 326 500 500 500 500 500 500 500 500 500 500 500 498 500 407 219 94 32 10 3 1
$ grep 'MPS\|Energy' dmrg-3.out
MPS = KRRRRRRRRRRRRRRRRRRRRRRRRR 0 2
GS INIT MPS BOND DIMS = 1 4 16 64 246 578 712 1114 1097 1102 1110 1121 1126 1130 1116 1111 1111 1107 1074 1103 895 444 186 59 16 4 1
OH Energy = -75.728467269120898
We can see that the transformation from SU2 to SZ is nearly exact, and the required bond dimension for the SZ MPS is roughly two times of the SU2 bond dimension.
Singlet Embedding
For spin-adapted calculation with total spin not equal to zero, there can be some convergence problem
even if in one-site algorithm. One way to solve this problem is to use singlet embedding.
In StackBlock
singlet embedding is used by default.
In block2
, by default singlet embedding is not used. If one adds the keyword singlet_embedding
to the input file,
the singlet embedding scheme will be used. For most total spin not equal to zero calculation,
singlet embedding may be more stable. One cannot calculate transition density matrix between states with different total spins
using singlet embedding. To do that one can translate the MPS between singlet embedding format and non-singlet-embedding format.
When total spin is equal to zero, the keyword singlet_embedding
will not have any effect.
If restarting a calculation, normally, the keyword singlet_embedding
is not required since the format of the MPS
can be automatically recognized.
For translating SU2 MPS to SZ MPS with total spin not equal to zero, the SU2 MPS must be in singlet embedding format.
First, we compute the energy for the spin-adapted with non-zero total spin using the following input file:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 2
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
irrep_reorder
mps_tags KET
The above input file indicates that singlet embedding is not used. The output is:
$ grep 'MPS = ' dmrg-1.out
MPS = CCRRRRRRRRRRRRRRRRRRRRRRRR 0 2 < N=8 S=1 PG=0 >
$ grep Energy dmrg-1.out
DMRG Energy = -75.423916647509742
Here the printed target quantum number of the MPS indicates that it is a triplet.
We can add the keyword singlet_embedding
to do a singlet embedding calculation:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 2
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
irrep_reorder
mps_tags SEKET
singlet_embedding
When singlet embedding is used, the output is:
$ grep 'MPS = ' dmrg-2.out
MPS = CCRRRRRRRRRRRRRRRRRRRRRRRR 0 2 < N=10 S=0 PG=0 >
$ grep Energy dmrg-2.out
DMRG Energy = -75.423879916245895
Here the printed target quantum number of the MPS indicates that it is a singlet (including some ghost particles).
One can use the keywords trans_mps_to_singlet_embedding
and trans_mps_from_singlet_embedding
combined with restart_copy_mps
or copy_mps
to translate between singlet embedding and normal formats.
The following script transforms the MPS from singlet embedding to normal format:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 2
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
irrep_reorder
mps_tags SEKET
restart_copy_mps TKET
trans_mps_from_singlet_embedding
We can verify that the transformed non-singlet-embedding MPS has the same energy as the singlet embedding MPS:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 2
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
irrep_reorder
mps_tags TKET
restart_oh
With the outputs:
$ grep 'MPS = ' dmrg-4.out
MPS = KRRRRRRRRRRRRRRRRRRRRRRRRR 0 2 < N=8 S=1 PG=0 >
$ grep Energy dmrg-4.out
OH Energy = -75.423879916245824
The following script will read the spin-adapted singlet embedding MPS and tranform it to a non-spin-adapted MPS:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 2
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
irrep_reorder
mps_tags SEKET
restart_copy_mps ZKETM2
trans_mps_to_sz
resolve_twosz -2
normalize_mps
Here the keyword resolve_twosz
indicates that the transformed SZ MPS will have projected spin 2 * SZ = -2
.
For this case since 2 * S = 2
, the possible values for resolve_twosz
are -2, 0, 2
.
If the keyword resolve_twosz
is not given, an MPS with ensemble of all possible projected spins will be produced
(which is often not very useful).
Getting one component of the SU2 MPS means that the SZ MPS will not have the same norm as the SU2 MPS.
If the keyword normalize_mps
is added, the transformed SZ MPS will be normalized. The keyword normalize_mps
can only have effect when trans_mps_to_sz
is present.
Finally, we calculate the energy expectation value using non-spin-adapted formalism and the transformed MPS (stored in the scratch folder), using the following input file:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin -2
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
irrep_reorder
mps_tags ZKETM2
restart_oh
nonspinadapted
Some reference outputs for this example:
$ grep MPS dmrg-6.out
MPS = KRRRRRRRRRRRRRRRRRRRRRRRRR 0 2 < N=8 SZ=-1 PG=0 >
GS INIT MPS BOND DIMS = 1 12 48 192 601 1145 1398 1474 1476 1468 1466 1441 1356 1316 1255 1240 1217 1206 1198 1176 904 422 183 59 16 4 1
$ grep Energy dmrg-6.out
OH Energy = -75.423879916245909
We can see that the transformation from SU2 to SZ is nearly exact. The other two components of the SU2 MPS will also have the same energy as this one.
CSF or Determinant Sampling
The overlap between the spin-adapted MPS and Configuration State Functions (CSFs), or between the non-spin-adapted MPS and determinants can be calculated. Since there are exponentially many CSFs or determinants (when the number of electrons is close to the number of orbitals), normally it only makes sense to sample CSFs or determinants with (absolute value of) the overlap larger than a threshold. The sampling is deterministic, meaning that all overlap above the given threshold will be printed.
The keyword sample
or restart_sample
can be used to sample CSFs or determinants
after DMRG or from an MPS loaded from disk. The value associated with the keyword
sample
or restart_sample
is the threshold for sampling.
Setting the threshold to zero is allowed, but this may only be useful for some very small systems.
Limitations: For non-zero total spin CSF sampling, the spin-adapted MPS must be in singlet embedding format. See the previous section.
The following is an example of the input file:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 0
irrep 1
hf_occ integral
schedule default
maxM 500
maxiter 30
irrep_reorder
mps_tags KET
sample 0.05
Some reference outputs for this example:
$ grep CSF dmrg-1.out
Number of CSF = 17 (cutoff = 0.05)
Sum of weights of sampled CSF = 0.909360149891891
CSF 0 20000000000202000002000000 = 0.828657540546610
CSF 1 20200000000002000002000000 = -0.330323898091116
CSF 2 20+00000000+0200000-000-00 = -0.140063445607095
CSF 3 20+00000000+0-0-0002000000 = -0.140041987646036
... ...
CSF 16 200000000002000+0-02000000 = 0.050020205617060
When there are more than 50 determinants, only the first 50 with largest weights
will be printed. The complete list of determinants and coefficients are stored in
sample-dets.npy
and sample-vals.npy
in the scratch folder, respectively.
So the restricted Hartree-Fock determinant/CSF has a very large coefficient (0.83).
To verify this, we can also directly compress the ground-state MPS to bond dimension 1, to get the CSF with the largest coefficient. Note that the compression method may converge to some other CSFs if there are many determinants with similar coefficients.
MPS Compression
MPS compression can be used to compress or fit a given MPS to a different (larger or smaller) bond dimension.
The following is an example of the input file for the compression (which will load the MPS obtailed from the previous ground-state DMRG):
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nelec 8
spin 0
irrep 1
hf_occ integral
schedule
0 250 0 0
2 125 0 0
4 62 0 0
6 31 0 0
8 15 0 0
10 7 0 0
12 3 0 0
14 1 0 0
end
maxiter 16
compression
overlap
read_mps_tags KET
mps_tags BRA
irrep_reorder
Here the keyword compression
indicates that this is a compression calculation.
When the keyword overlap
is given, the loaded MPS will be compressed,
otherwise, the result of H|MPS> will be compressed.
The tag of the input MPS is given by read_mps_tags
,
and the tag of the output MPS is given by mps_tags
.
Some reference outputs for this example:
$ grep 'Compression overlap' dmrg-2.out
Compression overlap = 0.828657540546619
We can see that the value obtained from compression is very close to the sampled value. But when a lower bound of the overlap is known, the sampling method should be more reliable and efficient for obtaining the CSF with the largest weight.
If the CSF or determinat pattern is required, one can do a quick sampling on the compressed
MPS using the keyword restart_sample 0
.
If the given MPS has a very small bond dimension, or the target (output) MPS has a very large bond dimension
(namely, “decompression”), one should use the keyword random_mps_init
to allow a better random
initial guess for the target MPS. Otherwise, the generated output MPS may be inaccurate.
LZ Symmetry
For diatomic molecules or model Hamiltonian with translational symmetry (such as 1D Hubbard model in momentum space),
it is possible to utilize additional K space symmetry.
To support the K space symmetry, the code must be compiled with the option -DUSE_KSYMM=ON
(default).
One can add the keyword k_symmetry
in the input file to use this additional symmetry.
Point group symmetry can be used together with k symmetry.
Therefore, even for system without K space symmetry, the calculation can still run as normal when the keyword k_symmetry
is added.
Note, however, the MPS or MPO generated from an input file with/without the keyword k_symmetry
,
cannot be reloaded with an input file without/with the keyword k_symmetry
.
For molecules, the integral file (FCIDUMP file) must be generated in a special way so that the K/LZ symmetry can be used. the following python script can be used to generate the integral with \(C_2 \otimes L_z\) symmetry:
import numpy as np
from functools import reduce
from pyscf import gto, scf, ao2mo, symm, tools, lib
from block2 import FCIDUMP, VectorUInt8, VectorInt
# adapted from https://github.com/hczhai/pyscf/blob/1.6/examples/symm/33-lz_adaption.py
# with the sign of lz
def lz_symm_adaptation(mol):
z_irrep_map = {} # map from dooh to lz
g_irrep_map = {} # map from dooh to c2
symm_orb_map = {} # orbital rotation
for ix in mol.irrep_id:
rx, qx = ix % 10, ix // 10
g_irrep_map[ix] = rx & 4
z_irrep_map[ix] = (-1) ** ((rx & 1) == ((rx & 4) >> 2)) * ((qx << 1) + ((rx & 2) >> 1))
if z_irrep_map[ix] == 0:
symm_orb_map[(ix, ix)] = 1
else:
if (rx & 1) == ((rx & 4) >> 2):
symm_orb_map[(ix, ix)] = -np.sqrt(0.5) * ((rx & 2) - 1)
else:
symm_orb_map[(ix, ix)] = -np.sqrt(0.5) * 1j
symm_orb_map[(ix, ix ^ 1)] = symm_orb_map[(ix, ix)] * 1j
z_irrep_map = [z_irrep_map[ix] for ix in mol.irrep_id]
g_irrep_map = [g_irrep_map[ix] for ix in mol.irrep_id]
rev_symm_orb = [np.zeros_like(x) for x in mol.symm_orb]
for iix, ix in enumerate(mol.irrep_id):
for iiy, iy in enumerate(mol.irrep_id):
if (ix, iy) in symm_orb_map:
rev_symm_orb[iix] = rev_symm_orb[iix] + symm_orb_map[(ix, iy)] * mol.symm_orb[iiy]
return rev_symm_orb, z_irrep_map, g_irrep_map
# copied from https://github.com/hczhai/pyscf/blob/1.6/pyscf/symm/addons.py#L29
# with the support for complex orbitals
def label_orb_symm(mol, irrep_name, symm_orb, mo, s=None, check=True, tol=1e-9):
nmo = mo.shape[1]
if s is None:
s = mol.intor_symmetric('int1e_ovlp')
s_mo = np.dot(s, mo)
norm = np.zeros((len(irrep_name), nmo))
for i, csym in enumerate(symm_orb):
moso = np.dot(csym.conj().T, s_mo)
ovlpso = reduce(np.dot, (csym.conj().T, s, csym))
try:
s_moso = lib.cho_solve(ovlpso, moso)
except:
ovlpso[np.diag_indices(csym.shape[1])] += 1e-12
s_moso = lib.cho_solve(ovlpso, moso)
norm[i] = np.einsum('ki,ki->i', moso.conj(), s_moso).real
norm /= np.sum(norm, axis=0) # for orbitals which are not normalized
iridx = np.argmax(norm, axis=0)
orbsym = np.asarray([irrep_name[i] for i in iridx])
if check:
largest_norm = norm[iridx,np.arange(nmo)]
orbidx = np.where(largest_norm < 1-tol)[0]
if orbidx.size > 0:
idx = np.where(largest_norm < 1-tol*1e2)[0]
if idx.size > 0:
raise ValueError('orbitals %s not symmetrized, norm = %s' %
(idx, largest_norm[idx]))
else:
raise ValueError('orbitals %s not strictly symmetrized.',
np.unique(orbidx))
return orbsym
mol = gto.M(
atom=[["C", (0, 0, 0)],
["C", (0, 0, 1.2425)]],
basis='ccpvdz',
symmetry='dooh')
mol.symm_orb, z_irrep, g_irrep = lz_symm_adaptation(mol)
mf = scf.RHF(mol)
mf.run()
h1e = mf.mo_coeff.conj().T @ mf.get_hcore() @ mf.mo_coeff
print('h1e imag = ', np.linalg.norm(h1e.imag))
assert np.linalg.norm(h1e.imag) < 1E-14
e_core = mol.energy_nuc()
h1e = h1e.real.ravel()
_eri = ao2mo.restore(1, mf._eri, mol.nao)
g2e = np.einsum('pqrs,pi,qj,rk,sl->ijkl', _eri,
mf.mo_coeff.conj(), mf.mo_coeff, mf.mo_coeff.conj(), mf.mo_coeff, optimize=True)
print('g2e imag = ', np.linalg.norm(g2e.imag))
assert np.linalg.norm(g2e.imag) < 1E-14
print('g2e symm = ', np.linalg.norm(g2e - g2e.transpose((1, 0, 3, 2))))
print('g2e symm = ', np.linalg.norm(g2e - g2e.transpose((2, 3, 0, 1))))
print('g2e symm = ', np.linalg.norm(g2e - g2e.transpose((3, 2, 1, 0))))
g2e = g2e.real.ravel()
fcidump_tol = 1E-13
na = nb = mol.nelectron // 2
n_mo = mol.nao
h1e[np.abs(h1e) < fcidump_tol] = 0
g2e[np.abs(g2e) < fcidump_tol] = 0
orb_sym_z = label_orb_symm(mol, z_irrep, mol.symm_orb, mf.mo_coeff, check=True)
orb_sym_g = label_orb_symm(mol, g_irrep, mol.symm_orb, mf.mo_coeff, check=True)
print(orb_sym_z)
fcidump = FCIDUMP()
fcidump.initialize_su2(n_mo, na + nb, na - nb, 1, e_core, h1e, g2e)
orb_sym_mp = VectorUInt8([tools.fcidump.ORBSYM_MAP['D2h'][i] for i in orb_sym_g])
fcidump.orb_sym = VectorUInt8(orb_sym_mp)
print('g symm error = ', fcidump.symmetrize(VectorUInt8(orb_sym_g)))
fcidump.k_sym = VectorInt(orb_sym_z)
fcidump.k_mod = 0
print('z symm error = ', fcidump.symmetrize(fcidump.k_sym, fcidump.k_mod))
fcidump.write('FCIDUMP')
Note that, if only the LZ symmetry is required, one can simply set orb_sym_g[:] = 0
.
The following input file can be used to perform the calculation with \(C_2 \otimes L_z\) symmetry:
sym d2h
orbitals FCIDUMP
k_symmetry
k_irrep 0
nelec 12
spin 0
irrep 1
hf_occ integral
schedule
0 500 1E-8 1E-3
4 500 1E-8 1E-4
8 500 1E-9 1E-5
12 500 1E-9 0
end
maxiter 30
Where the k_irrep
can be used to set the eigenvalue of LZ in the target state.
Note that it can be easier for the Davidson procedure to get stuck in local minima with high symmetry.
It is therefore recommended to use a custom schedule with larger noise and smaller Davidson threshold.
Some reference outputs for this input file:
$ grep 'Time elapsed' dmrg-1.out | tail -1
Time elapsed = 73.529 | E = -75.7291544157 | DE = -6.31e-07 | DW = 1.28e-05
$ grep 'DMRG Energy' dmrg-1.out
DMRG Energy = -75.729154415733063
When there are too many orbitals, and the default warmup fci
initial guess is used,
the initial MPS can have very large bond dimension
(especially when the LZ symmetry is used, since LZ is not a finite group)
and the first sweep will take very long time.
One way to solve this is to limit the LZ to a finite group, using modular arithmetic.
We can limit LZ to Z4 or Z2. The efficiency gain will be smaller, but the convergence may be more stable.
The keyword k_mod
can be used to set the modulus. When k_mod = 0
, it is the original infinite LZ group.
The following input file can be used to perform the calculation with \(C_2 \otimes Z_4\) symmetry:
sym d2h
orbitals FCIDUMP
k_symmetry
k_irrep 0
k_mod 4
nelec 12
spin 0
irrep 1
hf_occ integral
schedule
0 500 1E-8 1E-3
4 500 1E-8 1E-4
8 500 1E-9 1E-5
12 500 1E-9 0
end
maxiter 30
Some reference outputs for this input file:
$ grep 'Time elapsed' dmrg-2.out | tail -1
Time elapsed = 111.491 | E = -75.7292222457 | DE = -8.17e-08 | DW = 1.28e-05
$ grep 'DMRG Energy' dmrg-2.out
DMRG Energy = -75.729222245693876
Similarly, setting k_mod 2
gives the following output:
$ grep 'Time elapsed' dmrg-3.out | tail -1
Time elapsed = 135.394 | E = -75.7314583188 | DE = -3.97e-07 | DW = 1.49e-05
$ grep 'DMRG Energy' dmrg-3.out
DMRG Energy = -75.731458318751280
Initial Guess with Occupation Numbers
Once can use warmup occ
initial guess to solve the initial guess problem, where another keywrod occ
should be used,
followed by a list of (fractional) occupation numbers separated by the space character, to set the occupation numbers.
The occupation numbers can be obtained from a DMRG calculation using the same integral with/without K symmetry (or some other methods like CCSD and MP2).
If onepdm
is in the input file, the occupation numbers will be printed at the end of the output.
The following input file will perform the DMRG calculation using the same integral without the K symmetry (but with C2 symmetry):
sym d2h
orbitals FCIDUMP
nelec 12
spin 0
irrep 1
hf_occ integral
schedule
0 500 1E-8 1E-3
4 500 1E-8 1E-4
8 500 1E-9 1E-5
12 500 1E-9 0
end
maxiter 30
onepdm
Some reference outputs for this input file:
$ grep 'Time elapsed' dmrg-1.out | tail -2 | head -1
Time elapsed = 190.549 | E = -75.7314655815 | DE = -1.88e-07 | DW = 1.53e-05
$ grep 'DMRG Energy' dmrg-1.out
DMRG Energy = -75.731465581478815
$ grep 'DMRG OCC' dmrg-1.out
DMRG OCC = 2.000 2.000 1.957 1.626 1.870 1.870 0.360 0.098 0.098 0.006 0.008 0.008 0.008 0.013 0.014 0.014 0.011 0.006 0.006 0.006 0.005 0.005 0.002 0.002 0.002 0.001 0.001 0.001
The following input file will perform the DMRG calculation using the K symmetry, but with initial guess generated from occupation numbers:
sym d2h
orbitals FCIDUMP
k_symmetry
k_irrep 0
warmup occ
occ 2.000 2.000 1.957 1.626 1.870 1.870 0.360 0.098 0.098 0.006 0.008 0.008 0.008 0.013 0.014 0.014 0.011 0.006 0.006 0.006 0.005 0.005 0.002 0.002 0.002 0.001 0.001 0.001
cbias 0.2
nelec 12
spin 0
irrep 1
hf_occ integral
schedule
0 500 1E-8 1E-3
4 500 1E-8 1E-4
8 500 1E-9 1E-5
12 500 1E-9 0
end
maxiter 30
Here cbias
is the keyword to add a constant bias to the occ, so that 2.0 becomes 2.0 - cbias, and 0.098 becomes 0.098 + cbias.
Without the bias it is also easy to converge to a local minima.
Some reference outputs for this input file:
$ grep 'Time elapsed' dmrg-3.out | tail -1
Time elapsed = 55.938 | E = -75.7244716369 | DE = -5.25e-07 | DW = 7.45e-06
$ grep 'DMRG Energy' dmrg-3.out
DMRG Energy = -75.724471636942383
Here the calculation runs faster because the better initial guess, but the energy becomes worse.
Time Evolution
Now we give an example on how to do time evolution. The computation will apply \(|MPS_{out}\rangle = \exp (-t H) |MPS_{in}\rangle\) (with multiple steps). When \(t\) is a real floating point value, we will do imaginary time evolution of the MPS (namely, optimizing to ground state or finite-temperature state). When \(t\) is a pure imaginary value, we will do real time evolution of the MPS (namely, solving the time dependent Schrodinger equation).
To get accurate results, the time step has to be sufficiently small. The keyword delta_t
is used to set a time step \(\Delta t\) and indicate that this is a time evolution calculation. The keyword target_t
is used to set a target “stopping” time, namely, the \(t\). The “starting” time is considered as zero. Therefore, the number of time steps is computed as \(nsteps = t / \Delta t\) and printed.
If delta_t
is too big, the time step error will be large. If delta_t
is small, for fixed target time we have to do more time steps, with MPS bond dimension truncation happening after each sweep. So if delta_t
is too small, the accumulated bond dimension truncation error will be large. Some meaningful time steps may be 0.01 to 0.1.
Real Time Evolution
First, we do a state-averaged calculation for the lowest two states using the following input file:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nroots 2
hf_occ integral
schedule default
maxM 500
maxiter 30
noreorder
Note that the orbital reordering is disabled. The output:
$ grep elapsed dmrg-1.out | tail -1
Time elapsed = 5.762 | E[ 2] = -75.7268133875 -75.6376794953 | DE = -8.89e-08 | DW = 6.38e-05
$ grep Final dmrg-1.out
Final canonical form = LLLLLLLLLLLLLLLLLLLLLLLLLJ 25
The energy of the MPS at the last site is actually -75.72629673 and -75.63717415, which are slightly different from the above values.
Second, we can use the following input file to load the state-averaged MPS and then split it into individual MPSs:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
nroots 2
hf_occ integral
schedule default
maxM 500
maxiter 30
restart_copy_mps
split_states
trans_mps_to_complex
noreorder
Note that here nroots
must be the same as the previous case (or smaller, but larger than one),
otherwise the state-averaged MPS cannot be correctly loaded. The state-averaged MPS has the default tag KET.
We use calculation type keyword restart_copy_mps
to do this transformation.
The new keyword split_states
indicates that we want to split the MPS, this keyword should only be used
together with restart_copy_mps
.
The extra keyword trans_mps_to_complex
will further make the MPS a complex MPS. This is required for
real time evolution, where delta_t
can be imaginary.
For imaginary time evolution and real delta_t
and real target_t
, everything will be real during the time evolution, so normally we do not need this extra keyword trans_mps_to_complex
(but if you add it it is also okay).
The output looks like :
$ tail -7 dmrg-2.out
----- root = 0 / 2 -----
final tag = KET-CPX-0
final canonical form = LLLLLLLLLLLLLLLLLLLLLLLLLT
----- root = 1 / 2 -----
final tag = KET-CPX-1
final canonical form = LLLLLLLLLLLLLLLLLLLLLLLLLT
MPI FINALIZE: rank 0 of 1
By default, the tranformed MPS will have tags KET-0
, KET-1
etc, if it is real, or
KET-CPX-0
, KET-CPX-1
etc if it is complex.
If you set a custom tag, for example, when the input is like restart_copy_mps SKET
, the
tranformed MPS will have tags SKET-0
, SKET-1
, etc, no matter it is real or complex.
Third, we use the following script to do real time evolution:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
hf_occ integral
schedule
0 500 0 0
end
maxiter 10
read_mps_tags KET-CPX-0
mps_tags BRA
delta_t 0.05i
target_t 0.20i
complex_mps
noreorder
Note that a custom sweep schedule has to be used, to set the bond dimension to 500
(for example).
The keyword maxiter
and noise
in the sweep schedule are ignored.
For every time step, there can be multiple sweeps, called “sub sweeps”. The total number of sweeps is n_sweeps = nsteps * n_sub_sweeps
. The keyword n_sub_sweeps
can be used to set the number of sub sweeps. Default value is 2.
For real time evolution, delta_t
and target_t
should be pure imaginary values.
But they can also be general complex values.
When doing imaginary time evolution, delta_t
and target_t
should be all real.
The tag of the input MPS (old MPS) is given by read_mps_tags
.
The tag of the output MPS (new MPS) is given by mps_tags
. The two tags cannot be the same.
They should (better) not have common prefix. For example, KET
and KET-1
may not be used together, as -1
may be used by the code internally which will lead to confusion.
For this example, target_t
is four times delta_t
, so we will have 4 steps. Each time step has 2 sweeps. In total there will be 8 sweeps. The output is the result of applying \exp(-0.2i H)
to the input.
Whenever a complex MPS is used, the keyword complex_mps
should be used, otherwise the code will load the MPS incorrectly.
The output :
$ grep 'final' dmrg-3.out
mps final tag = BRA
mps final canonical form = MRRRRRRRRRRRRRRRRRRRRRRRRR
$ grep '<E>' dmrg-3.out
T = RE 0.00000 + IM 0.05000 <E> = -75.726309692728165 <Norm^2> = 0.999999608946318
T = RE 0.00000 + IM 0.10000 <E> = -75.726336818185246 <Norm^2> = 0.999994467614067
T = RE 0.00000 + IM 0.15000 <E> = -75.726364807114123 <Norm^2> = 0.999990200387707
T = RE 0.00000 + IM 0.20000 <E> = -75.726389514836484 <Norm^2> = 0.999986418355937
Here we see that the expectation value is printed after each time step. The energy is roughly conserved (similar to the DMRG output -75.72629673), and the norm is roughly one. Decreasing the time step may give more accurate results.
We can do the same for the excited state:
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
hf_occ integral
schedule
0 500 0 0
end
maxiter 10
read_mps_tags KET-CPX-1
mps_tags BRAEX
delta_t 0.05i
target_t 0.20i
complex_mps
noreorder
The output :
$ grep 'final' dmrg-4.out
mps final tag = BRAEX
mps final canonical form = MRRRRRRRRRRRRRRRRRRRRRRRRR
$ grep '<E>' dmrg-4.out
T = RE 0.00000 + IM 0.05000 <E> = -75.637185795841717 <Norm^2> = 0.999999661398567
T = RE 0.00000 + IM 0.10000 <E> = -75.637212093724074 <Norm^2> = 0.999995415040728
T = RE 0.00000 + IM 0.15000 <E> = -75.637238086798163 <Norm^2> = 0.999991630799571
T = RE 0.00000 + IM 0.20000 <E> = -75.637260508028248 <Norm^2> = 0.999988252849994
The energy is close to the DMRG value -75.63717415.
For imaginary time evolution, since the propagator is not unitary, the norm will increase exponentially.
You may use the extra keyword normalize_mps
to normalize MPS after each time step. The norm will still be computed and printed, but it will not be accumulated.
Finally, we can verify the energy at T = 0.0
and T = 0.2
and compute the overlap for these states.
The overlap between the all four states can be computed using the following input :
sym d2h
orbitals C2.CAS.PVDZ.FCIDUMP.ORIG
hf_occ integral
schedule
0 500 0 0
end
maxiter 10
mps_tags KET-CPX-0 BRA KET-CPX-1 BRAEX
restart_tran_oh
complex_mps
overlap
noreorder
The output is:
$ grep 'OH' dmrg-5.out
OH Energy 0 - 0 = RE 1.000000000000002 + IM 0.000000000000000
OH Energy 1 - 0 = RE -0.845792004408687 + IM -0.533433527528264
OH Energy 1 - 1 = RE 0.999986418355938 + IM 0.000000000000000
OH Energy 2 - 0 = RE -0.000000000000000 + IM 0.000000000000000
OH Energy 2 - 1 = RE -0.000000827506956 + IM -0.000000742303613
OH Energy 2 - 2 = RE 1.000000000000004 + IM 0.000000000000000
OH Energy 3 - 0 = RE 0.000001731091412 + IM -0.000000316659748
OH Energy 3 - 1 = RE -0.000001122421894 + IM 0.000002348984005
OH Energy 3 - 2 = RE -0.836158473098047 + IM -0.548435696470209
OH Energy 3 - 3 = RE 0.999988252849993 + IM 0.000000000000000
Here in the output each MPS gets a number, according to the order of tags in mps_tags
.
We have 0 (KET-CPX-0), 1 (BRA), 2 (KET-CPX-1)
and 3 (BRAEX)
.
Note that state 1 (not normalized) is time evolved from state 0 (normalized).
We see that the overlap <1|1>
is exactly 1. To get the overlap between the normalized states, we have:
< normlized(0) | normlized(1) >
= <0|1> / sqrt(<0|0> * <1|1>)
= (-0.845792004408687 -0.533433527528264j) / sqrt( 0.999986418355938 * 1.000000000000002)
= -0.8457977480901698 -0.5334371500173138j
The absolute value and the angle of this complex overlap is :
np.abs( -0.8457977480901698 -0.5334371500173138j ) = 0.9999645112167714
np.angle ( -0.8457977480901698 -0.5334371500173138j ) = -2.578911293480138
The absolute value is close to one. So the time evolution simply introduced a complex phase factor for the state, as expected. The complex phase factor can be computed as the remainder of E t
divided by 2 pi
:
-75.72638951483646 * 0.2 % (2 * np.pi) - 2 * np.pi = -2.5789072886081197
Which is close to the printed value.
Also note that the overlap between the ground state and the excited state <2|0>
is exactly zero. The corresponding overlap between the time evolved states <3|1>
is slightly different from zero, mainly due to the time step error and truncation error.
We can also get the energy expetation, by removing the keyword overlap
:
$ grep 'OH' dmrg-6.out
OH Energy 0 - 0 = RE -75.726296730204453 + IM 0.000000000000000
OH Energy 1 - 0 = RE 64.049088006450049 + IM 40.394772180607831
OH Energy 1 - 1 = RE -75.725361025967970 + IM -0.000000000000007
OH Energy 2 - 0 = RE 0.000000000000008 + IM 0.000000000000000
OH Energy 2 - 1 = RE 0.000061050951670 + IM 0.000056012958492
OH Energy 2 - 2 = RE -75.637174152353893 + IM 0.000000000000000
OH Energy 3 - 0 = RE -0.000132735557064 + IM 0.000024638559206
OH Energy 3 - 1 = RE 0.000086585167013 + IM -0.000178008928209
OH Energy 3 - 2 = RE 63.244928578558032 + IM 41.482021915322555
OH Energy 3 - 3 = RE -75.636371985782972 + IM 0.000000000000000
Note that here not all states are normalized, the printed value is not directly the energy.
The printed value is <A|H|B>
, but the energy is <A|H|B>/<A|B>
.
So the printed value should be divided by the square of the norm of the MPS (see previous output). For example, for state 1 we have :
-75.725361025967970 / 0.999986418355938 = -75.72638951483646
Which is the same as the number <E>
printed by the time evolution (-75.726389514836484).