In [ ]:
import psi4
import forte
import forte.utils
First we will run psi4 using the function forte.utils.psi4_scf
In [ ]:
# setup xyz geometry
geom = """
O
H 1 1.0
H 1 1.0 2 180.0
"""
(E_scf, wfn) = forte.utils.psi4_scf(geom,basis='sto-3g',reference='rhf')
print(f'SCF Energy = {E_scf}')
In [ ]:
forte.startup()
from forte import forte_options
options = psi4.core.get_options() # options = psi4 option object
options.set_current_module('FORTE') # read options labeled 'FORTE'
forte_options.get_options_from_psi4(options)
In [ ]:
# Setup forte and prepare the active space integral class
mos_spaces = {'FROZEN_DOCC' : [1,0,0,0,0,0,0,0], # freeze the O 1s orbital
'RESTRICTED_DOCC' : [1,0,0,0,0,1,0,0]}
mo_space_info = forte.make_mo_space_info_from_map(wfn,mos_spaces,[])
In [ ]:
mo_space_info.size('ACTIVE')
ForteIntegral
object to read integrals from psi4In Forte there are two classes responsible for handling integrals:
ForteIntegral
: reads the integrals from psi4 and stores them in varios formats (conventional, density fitting, Cholesky, ...).ActiveSpaceIntegrals
: stores a copy of all integrals and it is used by active space methods. This class only stores a subset of the integrals and includes an effective potential due to non-active doubly occupied orbitals.We will first build the ForteIntegral
object via the function make_forte_integrals
In [ ]:
ints = forte.make_forte_integrals(wfn, options, mo_space_info)
print(f'Number of molecular orbitals: {ints.nmo()}')
print(f'Number of correlated molecular orbitals: {ints.ncmo()}')
# the space that defines the active orbitals. We select only the 'ACTIVE' part
active_space = 'ACTIVE'
# the space(s) with non-active doubly occupied orbitals
core_spaces = ['RESTRICTED_DOCC']
as_ints = forte.make_active_space_ints(mo_space_info, ints, active_space, core_spaces)
print(f'Frozen-core energy = {as_ints.frozen_core_energy()}')
print(f'Nuclear repulsion energy = {as_ints.nuclear_repulsion_energy()}')
print(f'Scalar energy = {as_ints.scalar_energy()}')
In [ ]:
d = forte.Determinant()
In [ ]:
nact = mo_space_info.size('ACTIVE')
d.str(nact)
d.create_alfa_bit(1)
In [ ]:
d.str(nact)
d.create_alfa_bit(2)
In [ ]:
d.str(nact)
In [ ]:
d.destroy_alfa_bit(2)
In [ ]:
d.str(nact)
In [ ]:
nirrep = mo_space_info.nirrep()
nactpi = mo_space_info.dimension('ACTIVE').to_tuple()
# compute the number of alpha electrons per irrep
nact_aelpi = wfn.nalphapi() - mo_space_info.dimension('FROZEN_DOCC') - mo_space_info.dimension('RESTRICTED_DOCC')
nact_aelpi = nact_aelpi.to_tuple()
# compute the number of beta electrons per irrep
nact_belpi = wfn.nbetapi() - mo_space_info.dimension('FROZEN_DOCC') - mo_space_info.dimension('RESTRICTED_DOCC')
nact_belpi = nact_belpi.to_tuple()
print(f'Number of alpha electrons per irrep: {nact_aelpi}')
print(f'Number of beta electrons per irrep: {nact_belpi}')
print(f'Number of active orbtials per irrep: {nactpi}')
ref = forte.Determinant()
# we loop over each irrep and fill the occupied orbitals
irrep_start = [sum(nactpi[:h]) for h in range(nirrep)]
for h in range(nirrep):
for i in range(nact_aelpi[h]): ref.set_alfa_bit(irrep_start[h] + i, True)
for i in range(nact_belpi[h]): ref.set_beta_bit(irrep_start[h] + i, True)
print(f'Reference determinant: {ref.str(nact)}')
We can now compute the energy of the determinant as $\langle \Phi | \hat{H} | \Phi \rangle$ using the slater_rules
function in the ActiveSpaceIntegrals
class
In [ ]:
as_ints.slater_rules(ref,ref) + as_ints.scalar_energy() + as_ints.nuclear_repulsion_energy()
We can also print all the integrals at once (see the output.dat
file)
In [ ]:
import itertools
import functools
dets = []
orbs = range(nact)
# get the symmetry of each active orbital
act_sym = mo_space_info.symmetry('ACTIVE')
nact_ael = sum(nact_aelpi)
nact_bel = sum(nact_belpi)
print(f'Number of alpha electrons: {nact_ael}')
print(f'Number of beta electrons: {nact_bel}')
# specify the target symmetry
sym = 0
# generate all the alpha strings
for astr in itertools.combinations(orbs, nact_ael):
# compute the symmetry of the alpha string
asym = functools.reduce(lambda i, j: act_sym[i] ^ act_sym[j], astr)
# generate all the beta strings
for bstr in itertools.combinations(orbs, nact_bel):
# compute the symmetry of the beta string
bsym = functools.reduce(lambda i, j: act_sym[i] ^ act_sym[j], bstr)
# if the determinant has the correct symmetry save it
if (asym ^ bsym) == sym:
d = forte.Determinant()
for i in astr: d.set_alfa_bit(i, True)
for i in bstr: d.set_beta_bit(i, True)
dets.append(d)
print(f'==> List of FCI determinants <==')
for d in dets:
print(f'{d.str(4)}')
In [ ]:
import numpy as np
ndets = len(dets)
H = np.ndarray((ndets,ndets))
for I, detI in enumerate(dets):
for J, detJ in enumerate(dets):
H[I][J] = as_ints.slater_rules(detI,detJ)
# or we could use the more fancy looping below that avoid computing half of the matrix elements
# for I, detI in enumerate(dets):
# H[I][I] = as_ints.slater_rules(detI,detI) # diagonal term
# for J, detJ in enumerate(dets[:I]):
# HIJ = as_ints.slater_rules(detI,detJ) # off-diagonal term (only upper half)
# H[I][J] = H[J][I] = HIJ
print(H)
evals, evecs = np.linalg.eigh(H)
psi4_fci = -74.846380133240530
print(f'FCI Energy = {evals[0] + as_ints.scalar_energy() + as_ints.nuclear_repulsion_energy()}')
print(f'FCI Energy Error = {evals[0] + as_ints.scalar_energy() + as_ints.nuclear_repulsion_energy()- psi4_fci}')
index_hf = dets.index(ref)
print(f'Index of the HF determinant in the FCI vector {index_hf}')
In [ ]:
forte.cleanup()