Skip to content

Commit ff87781

Browse files
Remix of PR #420 (#423)
* new ortho orbs * add test case * update read-orbitals-1 * Fix test case * Fix some issues * Update test case * Fix test case * REmoved comment * Renaming * Clean test cases * Fix issue with pytest * Handle edge case * Add printing for edge case * Implement changes suggested by York (broken) * Merge branch 'main' into york-orho_orbs * Handle Cb case * Removed import and commented test case * Add missing function --------- Co-authored-by: lcyyork <[email protected]>
1 parent 33005af commit ff87781

29 files changed

+6860
-844
lines changed

forte/base_classes/active_space_method.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ std::shared_ptr<ActiveSpaceMethod> make_active_space_method(
161161
method = std::make_unique<Block2DMRGSolver>(state, nroot, scf_info, options, mo_space_info,
162162
as_ints);
163163
#else
164-
throw std::runtime_error("BLOCK2 is not available! Please compile with ENABLE_BLOCK2=ON.");
164+
throw std::runtime_error("BLOCK2 is not available! Please compile with ENABLE_block2=ON.");
165165
#endif
166166
} else {
167167
std::string msg = "make_active_space_method: type = " + type + " was not recognized";

forte/modules/objects_factory_psi4.py

+97-129
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818

1919
from forte.data import ForteData
2020

21-
from forte.proc.orbital_helpers import orbital_projection
22-
from forte.proc.orbital_helpers import read_orbitals, ortho_orbs_forte
21+
from forte.proc.orbital_helpers import add_orthogonal_vectors, orbital_projection
2322
from forte.proc.external_active_space_solver import write_wavefunction, read_wavefunction
2423

2524
from .module import Module
@@ -69,12 +68,6 @@ def _run(self, data: ForteData = None) -> ForteData:
6968
# Make an integral object from the psi4 wavefunction object
7069
data.ints = make_ints_from_psi4(data.psi_wfn, data.options, data.scf_info, data.mo_space_info)
7170

72-
# print(data.options)
73-
# print(data.scf_info)
74-
# print(data.state_weights_map)
75-
# print(data.ints)
76-
# print(data.mo_space_info)
77-
7871
return data
7972

8073
def prepare_forte_objects(self, data, **kwargs):
@@ -97,29 +90,32 @@ def prepare_forte_objects(self, data, **kwargs):
9790
options = data.options
9891
p4print("\n\n Preparing forte objects from a Psi4 Wavefunction object")
9992

100-
# Here we handle different scenarios:
101-
# The user specifies a molecule and guess orbitals via the psi_wfn
102-
# The user specifies a molecule and we compute the reference wavefunction with psi4
103-
104-
# Step 1. Get the molecule from the kwargs or molecule in the psi_wfn
105-
data.molecule = self.get_molecule(**kwargs)
106-
kwargs.pop("molecule", None)
93+
# Step 1. Check if a molecule and a psi4 wavefunction are passed in the kwargs and if the basis set is specified in the options
94+
molecule = kwargs.pop("molecule", None)
95+
ref_wfn = kwargs.pop("ref_wfn", None)
96+
# we store the basis information in kwargs, so we can pass it to the psi4 wavefunction
97+
# we can use the basis set from the options or the one passed in kwargs, but if both are provided, we throw an error
98+
if "basis" not in kwargs:
99+
basis = data.options.get_str("BASIS")
100+
if not basis:
101+
basis = psi4.core.get_global_option("BASIS")
102+
p4print(f" Using basis set {basis} from Psi4 global options.")
103+
kwargs["basis"] = basis
104+
else:
105+
if data.options.get_str("BASIS"):
106+
raise ValueError("Both basis set in options and kwargs are provided. Please provide only one.")
107107

108-
# Step 2. Get or compute the reference wavefunction
109-
psi_wfn, is_psi_wfn_fresh = self.get_psi4_wavefunction(data, **kwargs)
110-
kwargs.pop("ref_wfn", None)
111-
data.psi_wfn = psi_wfn
108+
if "basis" not in kwargs:
109+
kwargs["basis"] = basis
112110

113-
# Step 3: Create MO space information (this should be avoided if possible)
114-
temp_mo_space_info = self.create_mo_space_info(data, **kwargs)
111+
# Step 2: Get the molecule object from the kwargs or the ref_wfn
112+
data.molecule = self.get_molecule(molecule, ref_wfn)
115113

116-
# Step 4: Read the orbitals from a file if READ_ORBITALS is set. If not reading orbitals, Ca is None.
117-
Ca = self.get_orbitals_from_file(data)
114+
# Step 3. Get or compute the reference Psi4 Wavefunction
115+
data.psi_wfn = self.get_psi4_wavefunction(data, ref_wfn, **kwargs)
118116

119-
# Step 5: Check if the orbitals we plan to use (which might come from a file or a computation done at at different geometry/state)
120-
# are orthonormal. If not, orthonormalize them. If the psi_wfn is not fresh, we will perform a new SCF at the current geometry.
121-
# This is necessary because there seems to be a memory effect in Psi4 when using different geometries.
122-
self.add_and_orthonormalize_orbitals(data, is_psi_wfn_fresh, Ca, temp_mo_space_info, **kwargs)
117+
# Step 4: Create MO space information (this should be avoided if possible)
118+
temp_mo_space_info = self.create_mo_space_info(data, **kwargs)
123119

124120
# Step 6: Inject DF and MINAO basis sets in the psi4 wavefunction if specified in options
125121
self.set_basis_sets(data)
@@ -135,55 +131,107 @@ def prepare_forte_objects(self, data, **kwargs):
135131
# Once we are done, all the objects are stored in the ForteData object
136132
return data
137133

138-
def get_molecule(self, **kwargs):
134+
def get_molecule(self, molecule, ref_wfn):
139135
"""
140136
Get the molecule object from the kwargs or the ref_wfn.
141137
Keyword arguments supersede the ref_wfn.
142138
If no molecule is provided, raise an error.
143139
144140
Parameters
145141
----------
146-
kwargs : dict
142+
molecule : psi4.core.Molecule
143+
The molecule object
144+
ref_wfn : psi4.core.Wavefunction
145+
The reference wavefunction object
147146
148147
Returns
149148
-------
150149
molecule : psi4.core.Molecule
151150
The molecule object
152151
"""
153-
molecule = kwargs.pop("molecule", None)
154-
if molecule:
152+
# molecule provided in kwargs takes precedence
153+
if molecule is not None:
155154
return molecule
156155

157156
# no molecule provided, try to get it from a ref_wfn passed in kwargs
158-
ref_wfn = kwargs.get("ref_wfn", None)
159-
if ref_wfn:
157+
if ref_wfn is not None:
160158
return ref_wfn.molecule()
161159
raise ValueError("No molecule provided to prepare_forte_objects.")
162160

163-
def get_psi4_wavefunction(self, data, **kwargs):
161+
def get_psi4_wavefunction(self, data, ref_wfn, **kwargs):
164162
"""
165163
Get or compute the reference Psi4 Wavefunction.
166164
167-
Returns a tuple of (ref_wfn, is_fresh) where is_fresh is a boolean indicating whether the ref_wfn is fresh.
168-
169165
Parameters
170166
----------
171167
data : ForteData
172168
The ForteData object containing options and molecule
173-
kwargs : dict
174-
Keyword arguments
175169
"""
176-
ref_wfn = kwargs.get("ref_wfn", None)
177-
if ref_wfn:
178-
return ref_wfn, False
179-
# if no ref_wfn provided, compute it
180-
ref_type = data.options.get_str("REF_TYPE")
181-
p4print(f"\n No reference wavefunction provided. Computing {ref_type} orbitals using Psi4...\n")
182-
# Decide whether to run SCF or MCSCF based on REF_TYPE
183-
job_type = data.options.get_str("JOB_TYPE")
184-
do_mcscf = job_type in ["CASSCF", "MCSCF_TWO_STEP"] or data.options.get_bool("MCSCF_REFERENCE")
185-
ref_wfn = self.run_psi4(ref_type, data.molecule, not do_mcscf, **kwargs)
186-
return ref_wfn, True
170+
# Here we handle twp different scenarios:
171+
# Case | ref_wfn | Action
172+
# -------------------------------------
173+
# 1 | None | Run new SCF or MCSCF from Psi4 and return the wavefunction
174+
# 2 | Passed | Use ref_wfn
175+
176+
if not ref_wfn:
177+
ref_type = data.options.get_str("REF_TYPE")
178+
job_type = data.options.get_str("JOB_TYPE")
179+
do_mcscf = job_type in ["CASSCF", "MCSCF_TWO_STEP"] or data.options.get_bool("MCSCF_REFERENCE")
180+
ref_wfn = self.run_psi4(ref_type, data.molecule, not do_mcscf, **kwargs)
181+
return ref_wfn
182+
elif ref_wfn:
183+
# to test if we can use the orbitals directly, we compare the SO overlap matrices
184+
# metric matrix for the molecule built using the basis set from kwargs
185+
mol_S = psi4.core.Wavefunction.build(data.molecule, kwargs["basis"]).S()
186+
# metric matrix for the reference wavefunction
187+
ref_S = ref_wfn.S()
188+
189+
# catch the case where the number of irreps is different
190+
if mol_S.nirrep() != ref_S.nirrep():
191+
raise ValueError(
192+
"Different number of irreps in the overlap matrices of the reference wavefunction and the molecule."
193+
)
194+
195+
# if the dimensions are the same, compute the difference between the two matrices
196+
if mol_S.rowdim() == ref_S.rowdim() and mol_S.coldim() == ref_S.coldim():
197+
diff_S = mol_S.clone()
198+
diff_S.subtract(ref_S)
199+
norm = diff_S.rms()
200+
# if the norm of the difference is less than 1e-6, we can use the orbitals directly
201+
if norm < 1e-10:
202+
return ref_wfn
203+
204+
# compute a new Wavefunction object
205+
new_wfn = self.run_psi4("scf", data.molecule, False, **kwargs)
206+
207+
# project the orbitals from the reference wavefunction to the new wavefunction
208+
# note that this function may produce fewer orbitals than the new wavefunction
209+
pCa = new_wfn.basis_projection(ref_wfn.Ca(), ref_wfn.nmopi(), ref_wfn.basisset(), new_wfn.basisset())
210+
pCb = new_wfn.basis_projection(ref_wfn.Cb(), ref_wfn.nmopi(), ref_wfn.basisset(), new_wfn.basisset())
211+
212+
# If the number of orbitals is different, we add orthogonal vectors to the projected orbitals
213+
if pCa.coldim() != new_wfn.Ca().coldim():
214+
nirrep = new_wfn.nirrep()
215+
Snp = new_wfn.S().nph
216+
pCanp = pCa.nph
217+
pCbnp = pCb.nph
218+
fullCa = []
219+
fullCb = []
220+
for h in range(nirrep):
221+
fullCa.append(add_orthogonal_vectors(pCanp[h], Snp[h]))
222+
fullCb.append(add_orthogonal_vectors(pCbnp[h], Snp[h]))
223+
224+
# redefine the projected orbitals
225+
pCa = psi4.core.Matrix.from_array(fullCa)
226+
pCb = psi4.core.Matrix.from_array(fullCb)
227+
228+
# copy the projected orbitals to the new wavefunction
229+
new_wfn.Ca().copy(pCa)
230+
new_wfn.Cb().copy(pCa)
231+
232+
return new_wfn
233+
else:
234+
raise ValueError("Other cases not supported yet.")
187235

188236
def run_psi4(self, ref_type, molecule, print_warning=False, **kwargs):
189237
"""
@@ -207,8 +255,6 @@ def run_psi4(self, ref_type, molecule, print_warning=False, **kwargs):
207255
"""
208256
ref_type = ref_type.lower().strip()
209257

210-
print(self.options)
211-
212258
# capitalize the options
213259
psi4_options = {k.upper(): v for k, v in self.options.items()} if self.options else {}
214260

@@ -236,37 +282,6 @@ def run_psi4(self, ref_type, molecule, print_warning=False, **kwargs):
236282

237283
return wfn
238284

239-
def get_orbitals_from_file(self, data):
240-
"""
241-
Read orbitals from a file if the option READ_ORBITALS is set to True.
242-
243-
Parameters
244-
----------
245-
data : ForteData
246-
The ForteData object containing options or reference wavefunction
247-
"""
248-
# Read orbitals from file if specified or else use the orbitals from the psi_wfn
249-
Ca = read_orbitals() if data.options.get_bool("READ_ORBITALS") else None
250-
if Ca:
251-
# test if input Ca has the correct dimension
252-
nsopi = data.psi_wfn.nsopi()
253-
nmopi = data.psi_wfn.nmopi()
254-
255-
if Ca.rowdim() != nsopi or Ca.coldim() != nmopi:
256-
p4print("\n Expecting orbital dimensions:\n")
257-
p4print("\n row: ")
258-
nsopi.print_out()
259-
p4print(" column: ")
260-
nmopi.print_out()
261-
p4print("\n Actual orbital dimensions:\n")
262-
p4print("\n row: ")
263-
Ca.rowdim().print_out()
264-
p4print(" column: ")
265-
Ca.coldim().print_out()
266-
msg = "Invalid orbitals: different basis set / molecule! Check output for more."
267-
raise ValueError(msg)
268-
return Ca
269-
270285
def create_mo_space_info(self, data, **kwargs):
271286
"""
272287
Create the MOSpaceInfo object from options or kwargs.
@@ -286,53 +301,6 @@ def create_mo_space_info(self, data, **kwargs):
286301
mo_space_info = make_mo_space_info_from_map(nmopi, point_group, mo_spaces)
287302
return mo_space_info
288303

289-
def add_and_orthonormalize_orbitals(self, data, is_psi_wfn_fresh, Ca, temp_mo_space_info, **kwargs):
290-
"""
291-
Check if orbitals are orthonormal and orthonormalize them if necessary.
292-
293-
Parameters
294-
----------
295-
data : ForteData
296-
The ForteData object
297-
is_psi_wfn_fresh : bool
298-
A boolean indicating whether the reference wavefunction is freshly computed
299-
Ca : psi4.core.Matrix
300-
The MO coefficient matrix
301-
temp_mo_space_info : Forte MOSpaceInfo
302-
The MO space information
303-
kwargs : dict
304-
Keyword arguments
305-
"""
306-
# no orbitals read, then we use the orbitals from the psi_wfn
307-
if Ca is None:
308-
Ca = data.psi_wfn.Ca()
309-
# if this is the first time we are using the psi_wfn, we can use the orbitals directly
310-
if is_psi_wfn_fresh:
311-
return
312-
313-
# Build overlap matrix
314-
new_S = psi4.core.Wavefunction.build(data.molecule, data.options.get_str("BASIS")).S()
315-
316-
# if the orbitals are orthonormal, we can use them directly
317-
if check_mo_orthonormality(new_S, Ca):
318-
data.psi_wfn.Ca().copy(Ca)
319-
else:
320-
# if the psi_wfn is fresh just orthonormalize the orbitals
321-
if is_psi_wfn_fresh:
322-
data.psi_wfn.Ca().copy(ortho_orbs_forte(data.psi_wfn, temp_mo_space_info, Ca))
323-
# if the psi_wfn is not fresh, we need to perform a new SCF and orthonormalize the orbitals
324-
else:
325-
p4print("\n Perform new SCF at current geometry ...\n")
326-
327-
# run a new SCF
328-
wfn_new = self.run_psi4("scf", data.molecule, False, **kwargs)
329-
330-
# orthonormalize orbitals
331-
wfn_new.Ca().copy(ortho_orbs_forte(wfn_new, temp_mo_space_info, Ca))
332-
333-
# copy wfn_new to psi_wfn
334-
data.psi_wfn.shallow_copy(wfn_new)
335-
336304
def set_basis_sets(self, data):
337305
"""
338306
Set up DF and MINAO basis sets if specified in options.

0 commit comments

Comments
 (0)