18
18
19
19
from forte .data import ForteData
20
20
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
23
22
from forte .proc .external_active_space_solver import write_wavefunction , read_wavefunction
24
23
25
24
from .module import Module
@@ -69,12 +68,6 @@ def _run(self, data: ForteData = None) -> ForteData:
69
68
# Make an integral object from the psi4 wavefunction object
70
69
data .ints = make_ints_from_psi4 (data .psi_wfn , data .options , data .scf_info , data .mo_space_info )
71
70
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
-
78
71
return data
79
72
80
73
def prepare_forte_objects (self , data , ** kwargs ):
@@ -97,29 +90,32 @@ def prepare_forte_objects(self, data, **kwargs):
97
90
options = data .options
98
91
p4print ("\n \n Preparing forte objects from a Psi4 Wavefunction object" )
99
92
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." )
107
107
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
112
110
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 )
115
113
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 )
118
116
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 )
123
119
124
120
# Step 6: Inject DF and MINAO basis sets in the psi4 wavefunction if specified in options
125
121
self .set_basis_sets (data )
@@ -135,55 +131,107 @@ def prepare_forte_objects(self, data, **kwargs):
135
131
# Once we are done, all the objects are stored in the ForteData object
136
132
return data
137
133
138
- def get_molecule (self , ** kwargs ):
134
+ def get_molecule (self , molecule , ref_wfn ):
139
135
"""
140
136
Get the molecule object from the kwargs or the ref_wfn.
141
137
Keyword arguments supersede the ref_wfn.
142
138
If no molecule is provided, raise an error.
143
139
144
140
Parameters
145
141
----------
146
- kwargs : dict
142
+ molecule : psi4.core.Molecule
143
+ The molecule object
144
+ ref_wfn : psi4.core.Wavefunction
145
+ The reference wavefunction object
147
146
148
147
Returns
149
148
-------
150
149
molecule : psi4.core.Molecule
151
150
The molecule object
152
151
"""
153
- molecule = kwargs . pop ( "molecule" , None )
154
- if molecule :
152
+ # molecule provided in kwargs takes precedence
153
+ if molecule is not None :
155
154
return molecule
156
155
157
156
# 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 :
160
158
return ref_wfn .molecule ()
161
159
raise ValueError ("No molecule provided to prepare_forte_objects." )
162
160
163
- def get_psi4_wavefunction (self , data , ** kwargs ):
161
+ def get_psi4_wavefunction (self , data , ref_wfn , ** kwargs ):
164
162
"""
165
163
Get or compute the reference Psi4 Wavefunction.
166
164
167
- Returns a tuple of (ref_wfn, is_fresh) where is_fresh is a boolean indicating whether the ref_wfn is fresh.
168
-
169
165
Parameters
170
166
----------
171
167
data : ForteData
172
168
The ForteData object containing options and molecule
173
- kwargs : dict
174
- Keyword arguments
175
169
"""
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." )
187
235
188
236
def run_psi4 (self , ref_type , molecule , print_warning = False , ** kwargs ):
189
237
"""
@@ -207,8 +255,6 @@ def run_psi4(self, ref_type, molecule, print_warning=False, **kwargs):
207
255
"""
208
256
ref_type = ref_type .lower ().strip ()
209
257
210
- print (self .options )
211
-
212
258
# capitalize the options
213
259
psi4_options = {k .upper (): v for k , v in self .options .items ()} if self .options else {}
214
260
@@ -236,37 +282,6 @@ def run_psi4(self, ref_type, molecule, print_warning=False, **kwargs):
236
282
237
283
return wfn
238
284
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
-
270
285
def create_mo_space_info (self , data , ** kwargs ):
271
286
"""
272
287
Create the MOSpaceInfo object from options or kwargs.
@@ -286,53 +301,6 @@ def create_mo_space_info(self, data, **kwargs):
286
301
mo_space_info = make_mo_space_info_from_map (nmopi , point_group , mo_spaces )
287
302
return mo_space_info
288
303
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
-
336
304
def set_basis_sets (self , data ):
337
305
"""
338
306
Set up DF and MINAO basis sets if specified in options.
0 commit comments