import logging
import numpy as np
import pyscf
import pyscf.pbc
import pyscf.pbc.gto
import pyscf.scf
import pyscf.lib
from pyscf.lib.parameters import BOHR
from vayesta.core.util import einsum
log = logging.getLogger(__name__)
[docs]class LatticeMole(pyscf.pbc.gto.Cell):
"""For PySCF compatibility
Needs to implement:
a
copy()
build()
intor_cross()
intor_symmetric()
pbc_intor()
basis
?
atom_coord
unit
"""
def __init__(self, nsite, nelectron=None, spin=0, order=None, incore_anyway=True, verbose=0, output=None):
"""
Parameters
----------
order:
Ordering of lattice sites.
"""
super().__init__(verbose=verbose, output=output)
self.nsite = nsite
if nelectron is None:
nelectron = nsite
self.nelectron = nelectron
self.spin = spin
self._basis = {self.atom_symbol(i): None for i in range(self.nsite)}
self._built = True
self.incore_anyway = incore_anyway
self.order = order
def __getattr__(self, attr):
raise AttributeError("Attribute %s" % attr)
@property
def natm(self):
return self.nsite
[docs] def nao_nr(self):
return self.nsite
[docs] def ao_labels(self, fmt=True):
if fmt:
return ["%s%d" % (self.atom_pure_symbol(i), i) for i in range(self.nsite)]
elif fmt is None:
return [(i, self.atom_pure_symbol(i), "", "") for i in range(self.nsite)]
[docs] def atom_symbol(self, site):
return "%s%d" % (self.atom_pure_symbol(site), site)
[docs] def atom_pure_symbol(self, site):
return "S"
# def build(self):
# pass
[docs] def search_ao_label(self):
raise NotImplementedError()
[docs]class Hubbard(LatticeMole):
"""Abstract Hubbard model class."""
def __init__(self, nsite, nelectron=None, spin=0, hubbard_t=1.0, hubbard_u=0.0, v_nn=0.0, **kwargs):
super().__init__(nsite, nelectron=nelectron, spin=spin, **kwargs)
self.hubbard_t = hubbard_t
self.hubbard_u = hubbard_u
self.v_nn = v_nn
[docs] def aoslice_by_atom(self):
"""One basis function per site ("atom")."""
aorange = np.stack(4 * [np.arange(self.nsite)], axis=1)
aorange[:, 1] += 1
aorange[:, 3] += 1
return aorange
[docs] def ao2mo(self, mo_coeffs, compact=False):
if compact:
raise NotImplementedError()
if self.v_nn:
raise NotImplementedError()
if isinstance(mo_coeffs, np.ndarray) and np.ndim(mo_coeffs) == 2:
eris = self.hubbard_u * einsum("ai,aj,ak,al->ijkl", mo_coeffs, mo_coeffs, mo_coeffs, mo_coeffs)
else:
eris = self.hubbard_u * einsum("ai,aj,ak,al->ijkl", *mo_coeffs)
eris = eris.reshape(eris.shape[0] * eris.shape[1], eris.shape[2] * eris.shape[3])
return eris
[docs]class Hubbard1D(Hubbard):
"""Hubbard model in 1D."""
def __init__(
self, nsite, nelectron=None, spin=0, hubbard_t=1.0, hubbard_u=0.0, v_nn=0.0, boundary="auto", **kwargs
):
super().__init__(nsite, nelectron, spin, hubbard_t, hubbard_u, v_nn=v_nn, **kwargs)
self.nsites = [nsite]
self.dimension = 1
if boundary == "auto":
if nsite % 4 == 2:
boundary = "PBC"
elif nsite % 4 == 0:
boundary = "APBC"
else:
raise ValueError()
log.debug("Automatically chosen boundary condition: %s", boundary)
self.boundary = boundary
if boundary.upper() == "PBC":
bfac = 1
elif boundary.upper() == "APBC":
bfac = -1
self.bfac = bfac
h1e = np.zeros((nsite, nsite))
for i in range(nsite - 1):
h1e[i, i + 1] = h1e[i + 1, i] = -hubbard_t
h1e[nsite - 1, 0] = h1e[0, nsite - 1] = bfac * -hubbard_t
if self.order is not None:
h1e = h1e[self.order][:, self.order]
self.h1e = h1e
[docs] def get_eri(self, hubbard_u=None, v_nn=None):
if hubbard_u is None:
hubbard_u = self.hubbard_u
if v_nn is None:
v_nn = self.v_nn
eri = np.zeros(4 * [self.nsite])
np.fill_diagonal(eri, hubbard_u)
# Nearest-neighbor interaction
if v_nn:
for i in range(self.nsite - 1):
eri[i, i, i + 1, i + 1] = eri[i + 1, i + 1, i, i] = v_nn
eri[self.nsite - 1, self.nsite - 1, 0, 0] = eri[0, 0, self.nsite - 1, self.nsite - 1] = v_nn
if self.order is not None:
# Not tested:
order = self.order
eri = eri[order][:, order][:, :, order][:, :, :, order]
return eri
[docs] def lattice_vectors(self):
"""Lattice vectors of 1D Hubbard model.
An arbitrary value of 1 A is assumed between sites. The lattice vectors, however, are saved in units of Bohr.
"""
rvecs = np.eye(3)
rvecs[0, 0] = self.nsite
return rvecs / BOHR
[docs] def atom_coords(self):
coords = np.zeros((self.nsite, 3))
coords[:, 0] = np.arange(self.nsite)
if self.order is not None:
coords = coords[self.order]
return coords / BOHR
[docs] def get_index(self, i):
bfac = self.bfac
fac = 1
if i % self.nsites[0] != i:
fac *= bfac[0] * (i // self.nsites[0])
idx = i
return idx, fac
[docs]class Hubbard2D(Hubbard):
def __init__(
self,
nsites,
nelectron=None,
spin=0,
hubbard_t=1.0,
hubbard_u=0.0,
boundary="auto",
tiles=(1, 1),
order=None,
**kwargs,
):
nsite = nsites[0] * nsites[1]
if order is None and tiles != (1, 1):
order = self.get_tiles_order(nsites, tiles)
super().__init__(nsite, nelectron, spin, hubbard_t, hubbard_u, order=order, **kwargs)
self.nsites = nsites
self.dimension = 2
if isinstance(boundary, str) and boundary.lower() == "auto":
if nelectron == nsite:
if self.nsites[0] % 4 == 0 and self.nsites[1] % 4 == 0:
boundary = ("PBC", "APBC")
elif self.nsites[0] % 4 == 0 and self.nsites[1] % 4 == 2:
boundary = ("APBC", "APBC")
# Also possible:
# boundary = ('APBC', 'PBC')
elif self.nsites[0] % 4 == 2 and self.nsites[1] % 4 == 0:
boundary = ("APBC", "APBC")
# Also possible:
# boundary = ('PBC', 'APBC')
elif self.nsites[0] % 4 == 2 and self.nsites[1] % 4 == 2:
boundary = ("PBC", "APBC")
else:
raise NotImplementedError("Please specify boundary conditions.")
else:
raise NotImplementedError("Please specify boundary conditions.")
if np.ndim(boundary) == 0:
boundary = (boundary, boundary)
self.boundary = boundary
bfac = 2 * [None]
for i in range(2):
if boundary[i].lower() == "open":
bfac[i] = 0
elif boundary[i].lower() in ("periodic", "pbc"):
bfac[i] = +1
elif boundary[i].lower() in ("anti-periodic", "apbc"):
bfac[i] = -1
else:
raise ValueError("Invalid boundary: %s" % boundary[i])
log.debugv("boundary phases= %r", bfac)
self.bfac = bfac
h1e = np.zeros((nsite, nsite))
for i in range(nsites[0]):
for j in range(nsites[1]):
idx, _ = self.get_index(i, j)
idx_l, fac_l = self.get_index(i, j - 1)
idx_r, fac_r = self.get_index(i, j + 1)
idx_u, fac_u = self.get_index(i - 1, j)
idx_d, fac_d = self.get_index(i + 1, j)
h1e[idx, idx_l] += fac_l * -hubbard_t
h1e[idx, idx_r] += fac_r * -hubbard_t
h1e[idx, idx_u] += fac_u * -hubbard_t
h1e[idx, idx_d] += fac_d * -hubbard_t
if self.order is not None:
h1e = h1e[self.order][:, self.order]
self.h1e = h1e
[docs] def get_index(self, i, j):
bfac = self.bfac
fac = 1
if i % self.nsites[0] != i:
fac *= bfac[0]
if j % self.nsites[1] != j:
fac *= bfac[1]
idx = (i % self.nsites[0]) * self.nsites[1] + (j % self.nsites[1])
return idx, fac
[docs] def get_eri(self, hubbard_u=None, v_nn=None):
if hubbard_u is None:
hubbard_u = self.hubbard_u
if v_nn is None:
v_nn = self.v_nn
eri = np.zeros(4 * [self.nsite])
np.fill_diagonal(eri, hubbard_u)
# Nearest-neighbor interaction
if v_nn:
raise NotImplementedError()
return eri
[docs] def lattice_vectors(self):
"""Lattice vectors of 1D Hubbard model.
An arbitrary value of 1 A is assumed between sites. The lattice vectors, however, are saved in units of Bohr.
"""
rvecs = np.eye(3)
rvecs[0, 0] = self.nsites[0]
rvecs[1, 1] = self.nsites[1]
return rvecs / BOHR
[docs] def atom_coords(self):
"""Sites are ordered by default as:
6 7 8
3 4 5
0 1 2
"""
coords = np.zeros((self.nsite, 3))
for row in range(self.nsites[1]):
slc = np.s_[row * self.nsites[0] : (row + 1) * self.nsites[0]]
coords[slc, 0] = np.arange(self.nsites[0])
coords[slc, 1] = row
if self.order is not None:
coords = coords[self.order]
return coords / BOHR
[docs] @staticmethod
def get_tiles_order(nsites, tiles):
assert nsites[0] % tiles[0] == 0
assert nsites[1] % tiles[1] == 0
ntiles = [nsites[0] // tiles[0], nsites[1] // tiles[1]]
tsize = tiles[0] * tiles[1]
def get_xy(site):
tile, pos = divmod(site, tsize)
ty, tx = divmod(tile, ntiles[0])
py, px = divmod(pos, tiles[0])
return tx * tiles[0] + px, ty * tiles[1] + py
nsite = nsites[0] * nsites[1]
order = []
for site in range(nsite):
x, y = get_xy(site)
idx = y * nsites[0] + x
order.append(idx)
return order
[docs]class HubbardDF:
def __init__(self, mol):
if mol.v_nn:
raise NotImplementedError()
self.mol = mol
self.blockdim = self.get_naoaux()
[docs] def ao2mo(self, *args, **kwargs):
return self.mol.ao2mo(*args, **kwargs)
[docs] def get_naoaux(self):
return self.mol.nsite
[docs] def loop(self, blksize=None):
"""Note that blksize is ignored."""
nsite = self.mol.nsite
j3c = np.zeros((nsite, nsite, nsite))
np.fill_diagonal(j3c, np.sqrt(self.mol.hubbard_u))
# Pack (Q|ab) -> (Q|A)
j3c = pyscf.lib.pack_tril(j3c)
yield j3c
[docs]class LatticeSCF:
def __init__(self, mol, *args, **kwargs):
super().__init__(mol, *args, **kwargs)
if self.mol.incore_anyway:
self._eri = mol.get_eri()
else:
self.density_fit()
@property
def cell(self):
return self.mol
[docs] def get_hcore(self, *args, **kwargs):
return self.mol.h1e
[docs] def get_ovlp(self, mol=None):
return np.eye(self.mol.nsite)
[docs] def density_fit(self):
self.with_df = HubbardDF(self.mol)
return self
[docs]class LatticeRHF(LatticeSCF, pyscf.scf.hf.RHF):
[docs] def get_init_guess(self, mol=None, key=None, s1e=None):
e, c = np.linalg.eigh(self.get_hcore())
nocc = self.mol.nelectron // 2
dm = 2 * np.dot(c[:, :nocc], c[:, :nocc].T)
return dm
[docs] def get_jk(self, mol=None, dm=None, *args, **kwargs):
if mol is None:
mol = self.mol
if dm is None:
dm = self.make_rdm1()
if self.mol.v_nn is not None and mol.v_nn != 0:
raise NotImplementedError()
j = np.diag(np.diag(dm)) * mol.hubbard_u
k = j
return j, k
[docs] def check_lattice_symmetry(self, dm=None):
if dm is None:
dm = self.make_rdm1()
occ = np.diag(dm)
if not np.all(np.isclose(occ[0], occ)):
log.warning("Mean-field not lattice symmetric! Site occupations=\n%r", occ)
else:
log.debugv("Mean-field site occupations=\n%r", occ)
[docs]class LatticeUHF(LatticeSCF, pyscf.scf.uhf.UHF):
[docs] def get_init_guess(self, mol=None, key=None, s1e=None):
e, c = np.linalg.eigh(self.get_hcore())
nocc = self.mol.nelec
dma = np.dot(c[:, : nocc[0]], c[:, : nocc[0]].T)
dmb = np.dot(c[:, : nocc[1]], c[:, : nocc[1]].T)
# Create small random offset to break symmetries.
offset = np.full_like(dma.diagonal(), fill_value=1e-2)
if self.mol.dimension == 1:
for x in range(self.mol.nsites[0]):
ind, fac = self.mol.get_index(x)
offset[ind] *= (-1) ** (x % 2)
elif self.mol.dimension == 2:
for x in range(self.mol.nsites[0]):
for y in range(self.mol.nsites[1]):
ind, fac = self.mol.get_index(x, y)
offset[ind] *= (-1) ** (x % 2 + y % 2)
else:
raise NotImplementedError("LatticeUHF only supports 1- and 2-D Hubbard models.")
dma[np.diag_indices_from(dma)] += offset
return (dma, dmb)
[docs] def get_jk(self, mol=None, dm=None, *args, **kwargs):
if mol is None:
mol = self.mol
if dm is None:
dm = self.make_rdm1()
if self.mol.v_nn is not None and mol.v_nn != 0:
raise NotImplementedError()
dma, dmb = dm
ja = np.diag(np.diag(dma)) * mol.hubbard_u
jb = np.diag(np.diag(dmb)) * mol.hubbard_u
ka, kb = ja, jb
return (ja, jb), (ka, kb)