Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Variational embedding #118

Merged
merged 30 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
40b2963
Add diagonalisation between fixed FCI wavefunctions.
cjcscott Feb 13, 2023
c498b43
Added projecting of RFCI wavefunction with density- and hole-density-…
cjcscott Feb 13, 2023
46f69e4
Remove debug print statements and small refactor of projecting FCIWav…
cjcscott Feb 14, 2023
af2c1c6
Update to allow NO-CAS-CI diagonalisation to include mean-field state…
cjcscott Feb 17, 2023
cc3d8b3
Added all-in-one variational optimisation of local wavefunctions.
cjcscott Mar 16, 2023
aef26af
Small refactor of variational embedding functionality, and added line…
cjcscott Mar 20, 2023
7f731a8
Almost functional UHF implementation of variational embedding.
cjcscott Mar 23, 2023
acb67b9
Working UHF implementations of all variational embedding techniques.
cjcscott Mar 28, 2023
bdfd9e2
Added example for variational embedding calculation.
cjcscott Jun 16, 2023
b6f0a2e
temp fix: avoid t_rdm1 to see whether that caused the slowdown
Jul 7, 2023
7822dca
updated the ci no couplings for pygnme update, and unequal active spaces
Jul 11, 2023
b9b44c4
Merge pull request #116 from BoothGroup/var_emb_unequal_active_and_py…
abhishekkhedkar09 Jul 11, 2023
5cf21d2
added variational embedding test for unrestricted and restricted case…
Jul 19, 2023
218892d
Merge branch 'master' into variational_embedding
abhishekkhedkar09 Jul 19, 2023
2f13ac5
Update pyproject.toml
abhishekkhedkar09 Aug 3, 2023
8c52121
Update ci.yaml
abhishekkhedkar09 Aug 3, 2023
c200ae6
Update ci.yaml
abhishekkhedkar09 Aug 3, 2023
8902d69
Update test_varembedding.py
abhishekkhedkar09 Aug 3, 2023
fb73842
Update ci.yaml
abhishekkhedkar09 Aug 9, 2023
9792a8d
Update ci.yaml
abhishekkhedkar09 Aug 10, 2023
5f1aaa3
Global installation of pybind11 installs files required by cmake
abhishekkhedkar09 Aug 10, 2023
0a7532e
error fix: added import pytest statement, set pygnme to install edita…
Aug 10, 2023
894ce05
pygnme error fix: adding site_packages to LD_LIBRARY_PATH
abhishekkhedkar09 Aug 11, 2023
4a37f35
fix for an error related to pygnme library linking:github CI
abhishekkhedkar09 Aug 11, 2023
cd2a7a2
Update ci.yaml
abhishekkhedkar09 Aug 13, 2023
7e155ca
Merge branch 'master' into variational_embedding
abhishekkhedkar09 Aug 13, 2023
630cde3
debug #Update ci.yaml
abhishekkhedkar09 Aug 14, 2023
8f3b107
postpone GitHub CI setup for pygnme
abhishekkhedkar09 Aug 15, 2023
f9a1654
Add pytest.skip test if pygnme not found
Aug 15, 2023
03655fb
re-enabled rdm1 that was disabled at debug time.
Aug 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
python -m pip install wheel --user
python -m pip install setuptools --upgrade --user
python -m pip install https://github.com/BoothGroup/dyson/archive/master.zip
python -m pip install -e .[dmet,ebcc] --user
python -m pip install -e .[dmet,ebcc] --user
- name: Run unit tests
run: |
python -m pip install pytest pytest-cov --user
Expand Down
161 changes: 161 additions & 0 deletions examples/variational_embedding/01-simple-vembedding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import vayesta.ewf

from pyscf import gto, scf, lib, cc, fci
from vayesta.misc.variational_embedding import variational_params
from vayesta.misc.molecules import ring

import numpy as np


def run_ewf(natom, r, n_per_frag=1, bath_options={"bathtype":"dmet"}):
if abs(natom / n_per_frag - natom // n_per_frag) > 1e-6:
raise ValueError(f"Atoms per fragment ({n_per_frag}) doesn't exactly divide natoms ({natom})")

nfrag = natom // n_per_frag

mol = gto.M()
mol.atom = ring("H", natom, bond_length=r)
mol.basis = "sto-3g"
mol.spin = 0
mol.charge = 0
mol.build()

rmf = scf.RHF(mol).density_fit();
rmf.conv_tol = 1e-12;
rmf.kernel()

out = rmf.stability(external=True)

rewf = vayesta.ewf.EWF(rmf, solver="FCI", bath_options=bath_options)
with rewf.iao_fragmentation() as f:
with f.rotational_symmetry(nfrag, axis="z"):
f.add_atomic_fragment(atoms=list(range(n_per_frag)))
rewf.kernel()
return rewf, rmf


def get_wf_composite(emb, inc_mf=False):
"""Compute energy resulting from generalised eigenproblem between all local cluster wavefunctions."""
h, s, dm = variational_params.get_wf_couplings(emb, inc_mf=inc_mf)
w_bare, v, seig = lib.linalg_helper.safe_eigh(h, s, lindep=1e-12)
# Return lowest eigenvalue.
return w_bare[0]


def get_density_projected(emb, inc_mf=False):
p_frags = [x.get_fragment_projector(x.cluster.c_active) / emb.mf.mol.nelectron for x in emb.fragments]
barewfs = [x.results.wf for x in emb.fragments]
wfs = [x.project(y) for x, y in zip(barewfs, p_frags)]
h, s, dm = variational_params.get_wf_couplings(emb, emb.fragments, wfs, inc_mf=inc_mf)
sum_energy = sum(h.reshape(-1)) / sum(s.reshape(-1))
w, v, seig = lib.linalg_helper.safe_eigh(h, s, lindep=1e-12)
return sum_energy, w[0]


def get_occ_projected(emb):
p_frags = [x.get_overlap('frag|cluster-occ') for x in emb.fragments]
p_frags = [np.dot(x.T, x) for x in p_frags]
wfs = [x.results.wf.project_occ(y) for x, y in zip(emb.fragments, p_frags)]
h, s, dm = variational_params.get_wf_couplings(emb, wfs=wfs, inc_mf=True)
sum_energy = sum(h.reshape(-1)) / sum(s.reshape(-1))
w, v, seig = lib.linalg_helper.safe_eigh(h, s, lindep=1e-12)
return sum_energy, w[0]


def gen_comp_graph(fname):
import matplotlib.pyplot as plt
fig, (ax0, ax1) = plt.subplots(2, 1, sharex=True)
fig.set_tight_layout(True)
plot_results(fname, False, ax=ax0)
plot_results(fname, True, ax=ax1)
fig.set_size_inches(9, 6)
fig.set_tight_layout(True)
plt.draw()


def draw_comparison_error(fname1, fname2):
import matplotlib.pyplot as plt
fig, (ax0, ax1) = plt.subplots(1, 2, sharex=True, sharey=True)
fig.set_tight_layout(True)
plot_results(fname1, True, ax0)
plot_results(fname2, True, ax1)
fig.set_size_inches(9, 6)
plt.show(block=False)
plt.draw()


def plot_results(fname="results.txt", vsfci=False, ax=None, nodmenergy=True):
import matplotlib.pyplot as plt
if ax is None:
ax = plt.subplots(1, 1)[1]
res = np.genfromtxt(fname)
labs = ["$r_{HH}/\AA$", "HF", "FCI", "CCSD", "EWF-proj-wf", "EWF-dm-wf", "NO-CAS-CI", "NO-dproj", "NO-dproj-CAS-CI",
"NO-oproj", "NO-oproj-CAS-CI", "var-NO-FCI"]

def remove_ind(results, labels, i):
labels = labels[:i] + labels[i + 1:]
results = np.concatenate([results[:, :i], results[:, i + 1:]], axis=1)
return results, labels

if nodmenergy:
res, labs = remove_ind(res, labs, 5)

if vsfci:
fcires = res[:, 2]
res, labs = remove_ind(res, labs, 2)
res[:, 1:] = (res[:, 1:].T - fcires).T
for i in range(1, res.shape[-1]):
ax.plot(res[:, 0], res[:, i], label=labs[i])
ax.set_xlabel(labs[0])

leg = ax.legend().set_draggable(True)

if vsfci:
ax.set_ylabel("E-$E_{FCI}/E_h$")
else:
ax.set_ylabel("E/$E_h$")

plt.show(block=False)


if __name__ == "__main__":
import sys

nat = int(sys.argv[1])
n_per_frag = int(sys.argv[2])
for r in list(np.arange(0.6, 2.0, 0.1)) + list(np.arange(2.5, 10.0, 0.5)):
emb, mf = run_ewf(nat, r, n_per_frag)
# These calculate the standard EWF energy estimators.
eewf_wf = emb.get_wf_energy()
eewf_dm = emb.get_dm_energy()
# This calculates the energy of the variationally optimal combination of the local wavefunctions in each case.
# This uses the bare local wavefunctions...
e_barewf = get_wf_composite(emb)
# This uses the density projected local wavefunctions, and also returns the energy of a sum of these
# wavefunctions.
e_dense_proj, e_dense_opt = get_density_projected(emb)
# This does the same, but with the local wavefunction projected via the occupied projector (as in standard EWF).
e_occ_proj, e_occ_opt = get_occ_projected(emb)
# This variationally optimises all coefficients in the local wavefunctions simulanteously; the value of
# emb.fragments[x].results.wf is updated to the local portion of the global wavefunction.
e_opt = variational_params.optimise_full_varwav(emb)

# This computes the FCI and CCSD energies for comparison.
myfci = fci.FCI(mf)
efci = myfci.kernel()[0]
mycc = cc.CCSD(mf)
try:
mycc.kernel()

if mycc.converged:
ecc = mycc.e_tot
else:
ecc = np.nan
except np.linalg.LinAlgError:
ecc = np.nan

res = (mf.e_tot, efci, ecc, eewf_wf, eewf_dm, e_barewf, e_dense_proj, e_dense_opt, e_occ_proj, e_occ_opt, e_opt)

with open("results.txt", "a") as f:

f.write((f" {r:4.2f} ") + (" {:12.8f} " * len(res)).format(*res) + "\n")
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ dyson = [
ebcc = [
"ebcc"
]

pygnme = [
"pygnme @ git+https://github.com/BoothGroup/pygnme@master"
]

dev = [
"cvxpy>=1.1",
"mpi4py>=3.0.0",
Expand Down
2 changes: 2 additions & 0 deletions vayesta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ def import_package(name, required=True):
import_package('cvxpy', False)
dyson = import_package('dyson', False)
ebcc = import_package('ebcc', False)
import_package('pygnme', False)


# --- Git hashes

Expand Down
101 changes: 99 additions & 2 deletions vayesta/core/types/wf/fci.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import numpy as np
import pyscf
import pyscf.fci
from vayesta.core.util import decompress_axes, dot, einsum, tril_indices_ndim, replace_attr
from vayesta.core.util import decompress_axes, dot, einsum, tril_indices_ndim, callif, replace_attr
from vayesta.core.types import wf as wf_types
import scipy.sparse.linalg
from vayesta.core import spinalg

def FCI_WaveFunction(mo, ci, **kwargs):
if mo.nspin == 1:
Expand All @@ -18,6 +20,10 @@
super().__init__(mo, projector=projector)
self.ci = ci

@property
def nfci(self):
return self.ci.size

Check warning on line 25 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L25

Added line #L25 was not covered by tests

def make_rdm1(self, ao_basis=False, with_mf=True):
dm1 = pyscf.fci.direct_spin1.make_rdm1(self.ci, self.norb, self.nelec)
if not with_mf:
Expand Down Expand Up @@ -47,7 +53,53 @@
return einsum('ijkl,ai,bj,ck,dl->abcd', dm2, *(4*[self.mo.coeff]))

def project(self, projector, inplace=False):
raise NotImplementedError
"""Apply one-body projector to the FCI wavefunction using pyscf.
This is assumed to indicate a one-body
"""
wf = self if inplace else self.copy()

Check warning on line 59 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L59

Added line #L59 was not covered by tests
# Apply one-body operator of projector to ci string.
wf.ci = self._apply_onebody(projector)
return wf

Check warning on line 62 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L61-L62

Added lines #L61 - L62 were not covered by tests

def project_occ(self, projector, inplace=False):
"""Apply projector onto the occupied indices of all CI coefficient tensors.
Note that `projector` is nocc x nocc.

Action of occupied projector can be written as
P^{x}_{occ} = P_{ij}^{x}
"""
c0 = self.c0

Check warning on line 71 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L71

Added line #L71 was not covered by tests
# Get result of applying bare projector; need to keep original ci string just in case
ci0 = self.ci

Check warning on line 73 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L73

Added line #L73 was not covered by tests
# Pad projector from occupied to full orbital space.
projector = np.pad(projector, ((0, self.nvir),)).T

Check warning on line 75 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L75

Added line #L75 was not covered by tests

wf = self.project(projector, inplace)
wf.ci = (2*sum(np.diag(projector)) * ci0 - wf.ci) / c0

Check warning on line 78 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L77-L78

Added lines #L77 - L78 were not covered by tests

# Now just have to divide each coefficient by its excitation level; this corresponds to action of
# R^{-1} = (1 + \sum_{i\in occ} i i^+)^{-1} = (N_{elec} + 1 - \sum_{i\in occ} i^+ i)^{-1}
# So we seek x to solve
# x = R^{-1} a
# which could be obtained straightforwardly by solving
# Rx = a.
# In practice, it is more stable to just compute the excitation level of each state and divide by it.
mf_vdensity_op = np.eye(self.norb) - np.pad(np.eye(self.nocc), ((0, self.nvir),))

Check warning on line 87 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L87

Added line #L87 was not covered by tests

# Set up sparse LinearOperator object to apply the hole counting operator to the FCI string.
def myop(ci):
return self._apply_onebody(mf_vdensity_op, ci)

Check warning on line 91 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L90-L91

Added lines #L90 - L91 were not covered by tests

cishape = wf.ci.shape

Check warning on line 93 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L93

Added line #L93 was not covered by tests
# Calculate excitation level+1 for all states using this operation.
ex_lvl = myop(np.full_like(wf.ci, fill_value=1.0).reshape(-1)).reshape(cishape)
ex_lvl[0,0] = 1.0
wf.ci = einsum("pq,pq->pq", ex_lvl**(-1), wf.ci)
return wf

Check warning on line 98 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L95-L98

Added lines #L95 - L98 were not covered by tests

def _apply_onebody(self, proj, ci=None):
ci = self.ci if ci is None else ci
return pyscf.fci.direct_spin1.contract_1e(proj, ci, self.norb, self.nelec)

Check warning on line 102 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L101-L102

Added lines #L101 - L102 were not covered by tests

def restore(self, projector=None, inplace=False):
raise NotImplementedError
Expand All @@ -56,6 +108,10 @@
def c0(self):
return self.ci[0,0]

def copy(self):
proj = callif(spinalg.copy, self.projector)
return type(self)(self.mo.copy(), spinalg.copy(self.ci), projector=proj)

Check warning on line 113 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L112-L113

Added lines #L112 - L113 were not covered by tests

def as_unrestricted(self):
mo = self.mo.to_spin_orbitals()
return UFCI_WaveFunction(mo, self.ci)
Expand Down Expand Up @@ -213,6 +269,47 @@
einsum('ijkl,ai,bj,ck,dl->abcd', dm2[1], *[moa, moa, mob, mob]),
einsum('ijkl,ai,bj,ck,dl->abcd', dm2[2], *(4*[mob])))

def project_occ(self, projector, inplace=False):
"""Apply projector onto the occupied indices of all CI coefficient tensors.
Note that `projector` is nocc x nocc.

Action of occupied projector can be written as
P^{x}_{occ} = P_{ij}^{x}
"""
c0 = self.c0

Check warning on line 279 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L279

Added line #L279 was not covered by tests
# Get result of applying bare projector; need to keep original ci string just in case
ci0 = self.ci

Check warning on line 281 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L281

Added line #L281 was not covered by tests
# Pad projector from occupied to full orbital space.
projector = (np.pad(projector[0], ((0, self.nvir[0]),)).T, np.pad(projector[1], ((0, self.nvir[1]),)).T)

Check warning on line 283 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L283

Added line #L283 was not covered by tests

wf = self.project(projector, inplace)
wf.ci = ((projector[0].trace() + projector[1].trace()) * ci0 - wf.ci) / c0

Check warning on line 286 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L285-L286

Added lines #L285 - L286 were not covered by tests

# Now just have to divide each coefficient by its excitation level; this corresponds to action of
# R^{-1} = (1 + \sum_{i\in occ} i i^+)^{-1} = (N_{elec} + 1 - \sum_{i\in occ} i^+ i)^{-1}
# So we seek x to solve
# x = R^{-1} a
# which could be obtained straightforwardly by solving
# Rx = a.
# In practice, it is more stable to just compute the excitation level of each state and divide by it.
mf_vdensity_op = tuple([np.eye(self.norb[i]) - np.pad(np.eye(self.nocc[i]), ((0, self.nvir[i]),)) for i in [0, 1]])

# Set up sparse LinearOperator object to apply the hole counting operator to the FCI string.
def myop(ci):
return self._apply_onebody(mf_vdensity_op, ci)

Check warning on line 299 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L298-L299

Added lines #L298 - L299 were not covered by tests

cishape = wf.ci.shape

Check warning on line 301 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L301

Added line #L301 was not covered by tests
# Calculate excitation level+1 for all states using this operation.
ex_lvl = myop(np.full_like(wf.ci, fill_value=1.0).reshape(-1)).reshape(cishape)
ex_lvl[0,0] = 1.0
wf.ci = einsum("pq,pq->pq", ex_lvl**(-1), wf.ci)
return wf

Check warning on line 306 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L303-L306

Added lines #L303 - L306 were not covered by tests

def _apply_onebody(self, proj, ci=None):
ci = self.ci if ci is None else ci
assert(self.norb[0] == self.norb[1])
return pyscf.fci.direct_uhf.contract_1e(proj, ci, self.norb[0], self.nelec)

Check warning on line 311 in vayesta/core/types/wf/fci.py

View check run for this annotation

Codecov / codecov/patch

vayesta/core/types/wf/fci.py#L309-L311

Added lines #L309 - L311 were not covered by tests

def as_cisd(self, c0=None):
if self.projector is not None:
raise NotImplementedError
Expand Down
4 changes: 4 additions & 0 deletions vayesta/misc/variational_embedding/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
try:
import pygnme
except ModuleNotFoundError as e:
raise ModuleNotFoundError("pygnme is required for variational embedding approaches.") from e

Check warning on line 4 in vayesta/misc/variational_embedding/__init__.py

View check run for this annotation

Codecov / codecov/patch

vayesta/misc/variational_embedding/__init__.py#L1-L4

Added lines #L1 - L4 were not covered by tests
Loading
Loading