def __init__(self, molecule, options, basisset): # verify that the minimal version is used if CPPE is provided # from outside the Psi4 ecosystem min_version = "0.3.1" if parse_version(cppe.__version__) < parse_version(min_version): raise ModuleNotFoundError("CPPE version {} is required at least. " "Version {}" " was found.".format(min_version, cppe.__version__)) # setup the initial CppeState self.molecule = molecule self.options = options self.pe_ecp = self.options.pop("pe_ecp", False) self.basisset = basisset self.mints = core.MintsHelper(self.basisset) def callback(output): core.print_out(f"{output}\n") self.cppe_state = cppe.CppeState(self.options, psi4mol_to_cppemol(self.molecule), callback) core.print_out("CPPE Options:\n") core.print_out(f"PE(ECP) repulsive potentials = {self.pe_ecp}\n") for k in cppe.valid_option_keys: core.print_out(f"{k} = {self.cppe_state.options[k]}\n") core.print_out("-------------------------\n\n") self.cppe_state.calculate_static_energies_and_fields() # obtain coordinates of polarizable sites self._enable_induction = False if self.cppe_state.get_polarizable_site_number(): self._enable_induction = True coords = self.cppe_state.positions_polarizable self.polarizable_coords = core.Matrix.from_array(coords) self.V_es = None if self.pe_ecp: self._setup_pe_ecp()
def __init__(self, orbital, use_c): self.orbital = orbital self.nbf = self.orbital.nbf() self.use_c = use_c self.mints = pc.MintsHelper(orbital) self.eri = np.array(self.mints.ao_eri())
def __init__(self, molecule, options, basisset): # verify that the minimal version is used if CPPE is provided # from outside the Psi4 ecosystem min_version = "0.2.0" if parse_version(cppe.__version__) < parse_version(min_version): raise ModuleNotFoundError("CPPE version {} is required at least. " "Version {}" " was found.".format(min_version, cppe.__version__)) # setup the initial CppeState self.molecule = molecule self.options = options self.basisset = basisset self.mints = core.MintsHelper(self.basisset) def callback(output): core.print_out("{}\n".format(output)) self.cppe_state = cppe.CppeState(self.options, psi4mol_to_cppemol(self.molecule), callback) self.cppe_state.calculate_static_energies_and_fields() # obtain coordinates of polarizable sites self._enable_induction = False if self.cppe_state.get_polarizable_site_number(): self._enable_induction = True coords = np.array([site.position for site in self.cppe_state.potentials if site.is_polarizable]) self.polarizable_coords = core.Matrix.from_array(coords) self.V_es = None
def _setup_pe_ecp(self): mol_clone = self.molecule.clone() geom, _, elems, _, _ = mol_clone.to_arrays() n_qmatoms = len(elems) geom = geom.tolist() elems = elems.tolist() for p in self.cppe_state.potentials: if p.element == "X": continue elems.append(f"{p.element}_pe") geom.append([p.x, p.y, p.z]) qmmm_mol = core.Molecule.from_arrays(geom=geom, elbl=elems, units="Bohr", fix_com=True, fix_orientation=True, fix_symmetry="c1") n_qmmmatoms = len(elems) def __basisspec_pe_ecp(mol, role): global_basis = core.get_global_option("BASIS") for i in range(n_qmatoms): mol.set_basis_by_number(i, global_basis, role=role) for i in range(n_qmatoms, n_qmmmatoms): mol.set_basis_by_number(i, "pe_ecp", role=role) return {} libmintsbasisset.basishorde["PE_ECP_BASIS"] = __basisspec_pe_ecp self.pe_ecp_basis = core.BasisSet.build(qmmm_mol, "BASIS", "PE_ECP_BASIS") ecp_mints = core.MintsHelper(self.pe_ecp_basis) self.V_pe_ecp = ecp_mints.ao_ecp().np
def __init__(self, orbital, auxiliary, use_c): self.orbital = orbital self.auxiliary = auxiliary self.zero_basis = pc.BasisSet.zero_ao_basis_set() self.nbf = self.orbital.nbf() self.naux = self.auxiliary.nbf() self.use_c = use_c # Build required integrals self.mints = pc.MintsHelper(orbital) # Form (P|Q) ^ (-0.5) metric = self.mints.ao_eri(self.auxiliary, self.zero_basis, self.auxiliary, self.zero_basis) metric.power(-0.5, 1.e-14) self.metric = np.array(metric).squeeze() # Form AO Integrals Qpq = np.asarray( self.mints.ao_eri(self.auxiliary, self.zero_basis, self.orbital, self.orbital)).squeeze() # Apply metric contraction self.Ppq = np.dot(self.metric, Qpq.reshape(self.naux, -1)).reshape(self.naux, self.nbf, self.nbf)
def scf_initialize(self): """Specialized initialization, compute integrals and does everything to prepare for iterations""" self.iteration_ = 0 efp_enabled = hasattr(self.molecule(), 'EFP') if core.get_option('SCF', "PRINT") > 0: core.print_out(" ==> Pre-Iterations <==\n\n") self.print_preiterations() if efp_enabled: # EFP: Set QM system, options, and callback. Display efp geom in [A] efpobj = self.molecule().EFP core.print_out(efpobj.banner()) core.print_out(efpobj.geometry_summary(units_to_bohr=constants.bohr2angstroms)) efpptc, efpcoords, efpopts = get_qm_atoms_opts(self.molecule()) efpobj.set_point_charges(efpptc, efpcoords) efpobj.set_opts(efpopts, label='psi', append='psi') efpobj.set_electron_density_field_fn(field_fn) if self.attempt_number_ == 1: mints = core.MintsHelper(self.basisset()) if core.get_global_option('RELATIVISTIC') in ['X2C', 'DKH']: mints.set_rel_basisset(self.get_basisset('BASIS_RELATIVISTIC')) mints.one_electron_integrals() self.integrals() core.timer_on("HF: Form core H") self.form_H() core.timer_off("HF: Form core H") if efp_enabled: # EFP: Add in permanent moment contribution and cache core.timer_on("HF: Form Vefp") verbose = core.get_option('SCF', "PRINT") Vefp = modify_Fock_permanent(self.molecule(), mints, verbose=verbose-1) Vefp = core.Matrix.from_array(Vefp) self.H().add(Vefp) Horig = self.H().clone() self.Horig = Horig core.print_out(" QM/EFP: iterating Total Energy including QM/EFP Induction\n") core.timer_off("HF: Form Vefp") core.timer_on("HF: Form S/X") self.form_Shalf() core.timer_off("HF: Form S/X") core.timer_on("HF: Guess") self.guess() core.timer_off("HF: Guess") else: # We're reading the orbitals from the previous set of iterations. self.form_D() self.set_energies("Total Energy", self.compute_initial_E())
def _initialize_mints(self, basis, v_only=False): mints = core.MintsHelper(basis) V = mints.ao_potential() if v_only: return V T = mints.ao_kinetic() S = mints.ao_overlap() return V, T, S
def check_iwl_file_from_scf_type(scf_type, wfn): """ Ensures that a IWL file has been written based on input SCF type. """ if scf_type in ['DF', 'DISK_DF', 'MEM_DF', 'CD', 'PK', 'DIRECT']: mints = core.MintsHelper(wfn.basisset()) if core.get_global_option("RELATIVISTIC") in ["X2C", "DKH"]: rel_bas = core.BasisSet.build(wfn.molecule(), "BASIS_RELATIVISTIC", core.get_option("SCF", "BASIS_RELATIVISTIC"), "DECON", core.get_global_option('BASIS'), puream=wfn.basisset().has_puream()) mints.set_basisset('BASIS_RELATIVISTIC',rel_bas) mints.set_print(1) mints.integrals()
def compute_energy(self): print('Building MO integrals.') # Integral generation from Psi4's MintsHelper t = time.time() mints = pc.MintsHelper(self.basis) Co = pc.Matrix.from_array(self.C[:, :self.ndocc]) Cv = pc.Matrix.from_array(self.C[:, self.ndocc:]) MO = np.asarray(mints.mo_eri(Co, Cv, Co, Cv)) Eocc = self.eps[:self.ndocc] Evirt = self.eps[self.ndocc:] print('Shape of MO integrals: %s' % str(MO.shape)) print('\n...finished SCF and integral build in %.3f seconds.\n' % (time.time() - t)) print('Computing MP2 energy...') t = time.time() e_denom = 1 / (Eocc.reshape(-1, 1, 1, 1) - Evirt.reshape(-1, 1, 1) + Eocc.reshape(-1, 1) - Evirt) # Get the two spin cases MP2corr_OS = np.einsum('iajb,iajb,iajb->', MO, MO, e_denom) MP2corr_SS = np.einsum('iajb,iajb,iajb->', MO - MO.swapaxes(1, 3), MO, e_denom) print('...MP2 energy computed in %.3f seconds.\n' % (time.time() - t)) MP2corr_E = MP2corr_SS + MP2corr_OS MP2_E = self.SCF_E + MP2corr_E SCS_MP2corr_E = MP2corr_SS / 3 + MP2corr_OS * (6. / 5) SCS_MP2_E = self.SCF_E + SCS_MP2corr_E print('MP2 SS correlation energy: %16.10f' % MP2corr_SS) print('MP2 OS correlation energy: %16.10f' % MP2corr_OS) print('\nMP2 correlation energy: %16.10f' % MP2corr_E) print('MP2 total energy: %16.10f' % MP2_E) print('\nSCS-MP2 correlation energy: %16.10f' % MP2corr_SS) print('SCS-MP2 total energy: %16.10f' % SCS_MP2_E) return MP2_E
def __init__(self, molecule, options, basisset): # setup the initial CppeState self.molecule = molecule self.options = options self.basisset = basisset self.mints = core.MintsHelper(self.basisset) def callback(output): core.print_out("{}\n".format(output)) self.cppe_state = cppe.CppeState(self.options, psi4mol_to_cppemol(self.molecule), callback) self.cppe_state.calculate_static_energies_and_fields() # obtain coordinates of polarizable sites self._enable_induction = False if self.cppe_state.get_polarizable_site_number(): self._enable_induction = True coords = np.array([site.position for site in self.cppe_state.potentials if site.is_polarizable]) self.polarizable_coords = core.Matrix.from_array(coords) self.V_es = None
def scf_finalize_energy(self): """Performs stability analysis and calls back SCF with new guess if needed, Returns the SCF energy. This function should be called once orbitals are ready for energy/property computations, usually after iterations() is called. """ # post-scf vv10 correlation if core.get_option( 'SCF', "DFT_VV10_POSTSCF") and self.functional().vv10_b() > 0.0: self.functional().set_lock(False) self.functional().set_do_vv10(True) self.functional().set_lock(True) core.print_out( " ==> Computing Non-Self-Consistent VV10 Energy Correction <==\n\n" ) SCFE = 0.0 self.form_V() SCFE += self.compute_E() self.set_energies("Total Energy", SCFE) # Perform wavefunction stability analysis before doing # anything on a wavefunction that may not be truly converged. if core.get_option('SCF', 'STABILITY_ANALYSIS') != "NONE": # Don't bother computing needed integrals if we can't do anything with them. if self.functional().needs_xc(): raise ValidationError( "Stability analysis not yet supported for XC functionals.") # We need the integral file, make sure it is written and # compute it if needed if core.get_option('SCF', 'REFERENCE') != "UHF": psio = core.IO.shared_object() #psio.open(constants.PSIF_SO_TEI, 1) # PSIO_OPEN_OLD #try: # psio.tocscan(constants.PSIF_SO_TEI, "IWL Buffers") #except TypeError: # # "IWL Buffers" actually found but psio_tocentry can't be returned to Py # psio.close(constants.PSIF_SO_TEI, 1) #else: # # tocscan returned None # psio.close(constants.PSIF_SO_TEI, 1) # logic above foiled by psio_tocentry not returning None<--nullptr in pb11 2.2.1 # so forcibly recomputing for now until stability revamp core.print_out(" SO Integrals not on disk. Computing...") mints = core.MintsHelper(self.basisset()) #next 2 lines fix a bug that prohibits relativistic stability analysis if core.get_global_option('RELATIVISTIC') in ['X2C', 'DKH']: mints.set_rel_basisset(self.get_basisset('BASIS_RELATIVISTIC')) mints.integrals() core.print_out("done.\n") # Q: Not worth exporting all the layers of psio, right? follow = self.stability_analysis() while follow and self.attempt_number_ <= core.get_option( 'SCF', 'MAX_ATTEMPTS'): self.attempt_number_ += 1 core.print_out( " Running SCF again with the rotated orbitals.\n") if self.initialized_diis_manager_: self.diis_manager().reset_subspace() # reading the rotated orbitals in before starting iterations self.form_D() self.set_energies("Total Energy", self.compute_initial_E()) self.iterations() follow = self.stability_analysis() if follow and self.attempt_number_ > core.get_option( 'SCF', 'MAX_ATTEMPTS'): core.print_out( " There's still a negative eigenvalue. Try modifying FOLLOW_STEP_SCALE\n" ) core.print_out( " or increasing MAX_ATTEMPTS (not available for PK integrals).\n" ) # At this point, we are not doing any more SCF cycles # and we can compute and print final quantities. if hasattr(self.molecule(), 'EFP'): efpobj = self.molecule().EFP efpobj.compute() # do_gradient=do_gradient) efpene = efpobj.get_energy(label='psi') efp_wfn_independent_energy = efpene['total'] - efpene['ind'] self.set_energies("EFP", efpene['total']) SCFE = self.get_energies("Total Energy") SCFE += efp_wfn_independent_energy self.set_energies("Total Energy", SCFE) core.print_out(efpobj.energy_summary(scfefp=SCFE, label='psi')) core.set_variable( 'EFP ELST ENERGY', efpene['electrostatic'] + efpene['charge_penetration'] + efpene['electrostatic_point_charges']) core.set_variable('EFP IND ENERGY', efpene['polarization']) core.set_variable('EFP DISP ENERGY', efpene['dispersion']) core.set_variable('EFP EXCH ENERGY', efpene['exchange_repulsion']) core.set_variable('EFP TOTAL ENERGY', efpene['total']) core.set_variable('CURRENT ENERGY', efpene['total']) core.print_out("\n ==> Post-Iterations <==\n\n") self.check_phases() self.compute_spin_contamination() self.frac_renormalize() reference = core.get_option("SCF", "REFERENCE") energy = self.get_energies("Total Energy") # fail_on_maxiter = core.get_option("SCF", "FAIL_ON_MAXITER") # if converged or not fail_on_maxiter: # # if print_lvl > 0: # self.print_orbitals() # # if converged: # core.print_out(" Energy converged.\n\n") # else: # core.print_out(" Energy did not converge, but proceeding anyway.\n\n") if core.get_option('SCF', 'PRINT') > 0: self.print_orbitals() is_dfjk = core.get_global_option('SCF_TYPE').endswith('DF') core.print_out(" @%s%s Final Energy: %20.14f" % ('DF-' if is_dfjk else '', reference, energy)) # if (perturb_h_) { # core.print_out(" with %f %f %f perturbation" % # (dipole_field_strength_[0], dipole_field_strength_[1], dipole_field_strength_[2])) # } core.print_out("\n\n") self.print_energies() self.clear_external_potentials() if core.get_option('SCF', 'PCM'): calc_type = core.PCM.CalcType.Total if core.get_option("PCM", "PCM_SCF_TYPE") == "SEPARATE": calc_type = core.PCM.CalcType.NucAndEle Dt = self.Da().clone() Dt.add(self.Db()) _, Vpcm = self.get_PCM().compute_PCM_terms(Dt, calc_type) self.push_back_external_potential(Vpcm) # Properties # Comments so that autodoc utility will find these PSI variables # Process::environment.globals["SCF DIPOLE X"] = # Process::environment.globals["SCF DIPOLE Y"] = # Process::environment.globals["SCF DIPOLE Z"] = # Process::environment.globals["SCF QUADRUPOLE XX"] = # Process::environment.globals["SCF QUADRUPOLE XY"] = # Process::environment.globals["SCF QUADRUPOLE XZ"] = # Process::environment.globals["SCF QUADRUPOLE YY"] = # Process::environment.globals["SCF QUADRUPOLE YZ"] = # Process::environment.globals["SCF QUADRUPOLE ZZ"] = # Orbitals are always saved, in case an MO guess is requested later # save_orbitals() # Shove variables into global space for k, v in self.variables().items(): core.set_variable(k, v) self.finalize() if self.V_potential(): self.V_potential().clear_collocation_cache() core.print_out("\nComputation Completed\n") return energy
def build_sapt_jk_cache(wfn_A, wfn_B, jk, do_print=True): """ Constructs the DCBS cache data required to compute ELST/EXCH/IND """ core.print_out("\n ==> Preparing SAPT Data Cache <== \n\n") jk.print_header() cache = {} cache["wfn_A"] = wfn_A cache["wfn_B"] = wfn_B # First grab the orbitals cache["Cocc_A"] = wfn_A.Ca_subset("AO", "OCC") cache["Cvir_A"] = wfn_A.Ca_subset("AO", "VIR") cache["Cocc_B"] = wfn_B.Ca_subset("AO", "OCC") cache["Cvir_B"] = wfn_B.Ca_subset("AO", "VIR") cache["eps_occ_A"] = wfn_A.epsilon_a_subset("AO", "OCC") cache["eps_vir_A"] = wfn_A.epsilon_a_subset("AO", "VIR") cache["eps_occ_B"] = wfn_B.epsilon_a_subset("AO", "OCC") cache["eps_vir_B"] = wfn_B.epsilon_a_subset("AO", "VIR") # Build the densities as HF takes an extra "step" cache["D_A"] = core.Matrix.doublet( cache["Cocc_A"], cache["Cocc_A"], False, True) cache["D_B"] = core.Matrix.doublet( cache["Cocc_B"], cache["Cocc_B"], False, True) cache["P_A"] = core.Matrix.doublet( cache["Cvir_A"], cache["Cvir_A"], False, True) cache["P_B"] = core.Matrix.doublet( cache["Cvir_B"], cache["Cvir_B"], False, True) # Potential ints mints = core.MintsHelper(wfn_A.basisset()) cache["V_A"] = mints.ao_potential() # cache["V_A"].axpy(1.0, wfn_A.Va()) mints = core.MintsHelper(wfn_B.basisset()) cache["V_B"] = mints.ao_potential() # cache["V_B"].axpy(1.0, wfn_B.Va()) # Anything else we might need cache["S"] = wfn_A.S().clone() # J and K matrices jk.C_clear() # Normal J/K for Monomer A jk.C_left_add(wfn_A.Ca_subset("SO", "OCC")) jk.C_right_add(wfn_A.Ca_subset("SO", "OCC")) # Normal J/K for Monomer B jk.C_left_add(wfn_B.Ca_subset("SO", "OCC")) jk.C_right_add(wfn_B.Ca_subset("SO", "OCC")) # K_O J/K C_O_A = core.Matrix.triplet( cache["D_B"], cache["S"], cache["Cocc_A"], False, False, False) jk.C_left_add(C_O_A) jk.C_right_add(cache["Cocc_A"]) jk.compute() # Clone them as the JK object will overwrite. cache["J_A"] = jk.J()[0].clone() cache["K_A"] = jk.K()[0].clone() cache["J_B"] = jk.J()[1].clone() cache["K_B"] = jk.K()[1].clone() cache["J_O"] = jk.J()[2].clone() cache["K_O"] = jk.K()[2].clone() cache["K_O"].transpose_this() monA_nr = wfn_A.molecule().nuclear_repulsion_energy() monB_nr = wfn_B.molecule().nuclear_repulsion_energy() dimer_nr = wfn_A.molecule().extract_subsets( [1, 2]).nuclear_repulsion_energy() cache["nuclear_repulsion_energy"] = dimer_nr - monA_nr - monB_nr return cache
def scf_initialize(self): """Specialized initialization, compute integrals and does everything to prepare for iterations""" # Figure out memory distributions # Get memory in terms of doubles total_memory = (core.get_memory() / 8) * core.get_global_option("SCF_MEM_SAFETY_FACTOR") # Figure out how large the DFT collocation matrices are vbase = self.V_potential() if vbase: collocation_size = vbase.grid().collocation_size() if vbase.functional().ansatz() == 1: collocation_size *= 4 # First derivs elif vbase.functional().ansatz() == 2: collocation_size *= 10 # Second derivs else: collocation_size = 0 # Change allocation for collocation matrices based on DFT type scf_type = core.get_global_option('SCF_TYPE').upper() nbf = self.get_basisset("ORBITAL").nbf() naux = self.get_basisset("DF_BASIS_SCF").nbf() if "DIRECT" == scf_type: jk_size = total_memory * 0.1 elif scf_type.endswith('DF'): jk_size = naux * nbf * nbf else: jk_size = nbf**4 # Give remaining to collocation if total_memory > jk_size: collocation_memory = total_memory - jk_size # Give up to 10% to collocation elif (total_memory * 0.1) > collocation_size: collocation_memory = collocation_size else: collocation_memory = total_memory * 0.1 if collocation_memory > collocation_size: collocation_memory = collocation_size # Set constants self.iteration_ = 0 self.memory_jk_ = int(total_memory - collocation_memory) self.memory_collocation_ = int(collocation_memory) # Print out initial docc/socc/etc data if core.get_option('SCF', "PRINT") > 0: core.print_out(" ==> Pre-Iterations <==\n\n") self.print_preiterations() # Initialize EFP efp_enabled = hasattr(self.molecule(), 'EFP') if efp_enabled: # EFP: Set QM system, options, and callback. Display efp geom in [A] efpobj = self.molecule().EFP core.print_out(efpobj.banner()) core.print_out( efpobj.geometry_summary(units_to_bohr=constants.bohr2angstroms)) efpptc, efpcoords, efpopts = get_qm_atoms_opts(self.molecule()) efpobj.set_point_charges(efpptc, efpcoords) efpobj.set_opts(efpopts, label='psi', append='psi') efpobj.set_electron_density_field_fn(field_fn) # Initilize all integratals and perform the first guess if self.attempt_number_ == 1: mints = core.MintsHelper(self.basisset()) if core.get_global_option('RELATIVISTIC') in ['X2C', 'DKH']: mints.set_rel_basisset(self.get_basisset('BASIS_RELATIVISTIC')) mints.one_electron_integrals() self.initialize_jk(self.memory_jk_) if self.V_potential(): self.V_potential().build_collocation_cache( self.memory_collocation_) core.timer_on("HF: Form core H") self.form_H() core.timer_off("HF: Form core H") if efp_enabled: # EFP: Add in permanent moment contribution and cache core.timer_on("HF: Form Vefp") verbose = core.get_option('SCF', "PRINT") Vefp = modify_Fock_permanent(self.molecule(), mints, verbose=verbose - 1) Vefp = core.Matrix.from_array(Vefp) self.H().add(Vefp) Horig = self.H().clone() self.Horig = Horig core.print_out( " QM/EFP: iterating Total Energy including QM/EFP Induction\n" ) core.timer_off("HF: Form Vefp") core.timer_on("HF: Form S/X") self.form_Shalf() core.timer_off("HF: Form S/X") core.timer_on("HF: Guess") self.guess() core.timer_off("HF: Guess") else: # We're reading the orbitals from the previous set of iterations. self.form_D() self.set_energies("Total Energy", self.compute_initial_E()) # turn off VV10 for iterations if core.get_option( 'SCF', "DFT_VV10_POSTSCF") and self.functional().vv10_b() > 0.0: core.print_out(" VV10: post-SCF option active \n \n") self.functional().set_lock(False) self.functional().set_do_vv10(False) self.functional().set_lock(True)
def scf_iterate(self, e_conv=None, d_conv=None): is_dfjk = core.get_global_option('SCF_TYPE').endswith('DF') verbose = core.get_option('SCF', "PRINT") reference = core.get_option('SCF', "REFERENCE") # self.member_data_ signals are non-local, used internally by c-side fns self.diis_enabled_ = _validate_diis() self.MOM_excited_ = _validate_MOM() self.diis_start_ = core.get_option('SCF', 'DIIS_START') damping_enabled = _validate_damping() soscf_enabled = _validate_soscf() frac_enabled = _validate_frac() efp_enabled = hasattr(self.molecule(), 'EFP') if self.iteration_ < 2: core.print_out(" ==> Iterations <==\n\n") core.print_out( "%s Total Energy Delta E RMS |[F,P]|\n\n" % (" " if is_dfjk else "")) # SCF iterations! SCFE_old = 0.0 SCFE = 0.0 Drms = 0.0 while True: self.iteration_ += 1 diis_performed = False soscf_performed = False self.frac_performed_ = False #self.MOM_performed_ = False # redundant from common_init() self.save_density_and_energy() if efp_enabled: # EFP: Add efp contribution to Fock matrix self.H().copy(self.Horig) global mints_psi4_yo mints_psi4_yo = core.MintsHelper(self.basisset()) Vefp = modify_Fock_induced(self.molecule().EFP, mints_psi4_yo, verbose=verbose - 1) Vefp = core.Matrix.from_array(Vefp) self.H().add(Vefp) SCFE = 0.0 self.clear_external_potentials() core.timer_on("HF: Form G") self.form_G() core.timer_off("HF: Form G") # reset fractional SAD occupation if (self.iteration_ == 0) and self.reset_occ_: self.reset_occupation() upcm = 0.0 if core.get_option('SCF', 'PCM'): calc_type = core.PCM.CalcType.Total if core.get_option("PCM", "PCM_SCF_TYPE") == "SEPARATE": calc_type = core.PCM.CalcType.NucAndEle Dt = self.Da().clone() Dt.add(self.Db()) upcm, Vpcm = self.get_PCM().compute_PCM_terms(Dt, calc_type) SCFE += upcm self.push_back_external_potential(Vpcm) self.set_variable("PCM POLARIZATION ENERGY", upcm) self.set_energies("PCM Polarization", upcm) core.timer_on("HF: Form F") self.form_F() core.timer_off("HF: Form F") if verbose > 3: self.Fa().print_out() self.Fb().print_out() SCFE += self.compute_E() if efp_enabled: global efp_Dt_psi4_yo # EFP: Add efp contribution to energy efp_Dt_psi4_yo = self.Da().clone() efp_Dt_psi4_yo.add(self.Db()) SCFE += self.molecule().EFP.get_wavefunction_dependent_energy() self.set_energies("Total Energy", SCFE) Ediff = SCFE - SCFE_old SCFE_old = SCFE status = [] # We either do SOSCF or DIIS if (soscf_enabled and (self.iteration_ > 3) and (Drms < core.get_option('SCF', 'SOSCF_START_CONVERGENCE'))): Drms = self.compute_orbital_gradient( False, core.get_option('SCF', 'DIIS_MAX_VECS')) diis_performed = False if self.functional().needs_xc(): base_name = "SOKS, nmicro=" else: base_name = "SOSCF, nmicro=" if not _converged(Ediff, Drms, e_conv=e_conv, d_conv=d_conv): nmicro = self.soscf_update( core.get_option('SCF', 'SOSCF_CONV'), core.get_option('SCF', 'SOSCF_MIN_ITER'), core.get_option('SCF', 'SOSCF_MAX_ITER'), core.get_option('SCF', 'SOSCF_PRINT')) if nmicro > 0: # if zero, the soscf call bounced for some reason self.find_occupation() status.append(base_name + str(nmicro)) soscf_performed = True # Stops DIIS else: if verbose > 0: core.print_out( "Did not take a SOSCF step, using normal convergence methods\n" ) soscf_performed = False # Back to DIIS else: # need to ensure orthogonal orbitals and set epsilon status.append(base_name + "conv") core.timer_on("HF: Form C") self.form_C() core.timer_off("HF: Form C") soscf_performed = True # Stops DIIS if not soscf_performed: # Normal convergence procedures if we do not do SOSCF core.timer_on("HF: DIIS") diis_performed = False add_to_diis_subspace = False if self.diis_enabled_ and self.iteration_ >= self.diis_start_: add_to_diis_subspace = True Drms = self.compute_orbital_gradient( add_to_diis_subspace, core.get_option('SCF', 'DIIS_MAX_VECS')) if (self.diis_enabled_ and self.iteration_ >= self.diis_start_ + core.get_option('SCF', 'DIIS_MIN_VECS') - 1): diis_performed = self.diis() if diis_performed: status.append("DIIS") core.timer_off("HF: DIIS") if verbose > 4 and diis_performed: core.print_out(" After DIIS:\n") self.Fa().print_out() self.Fb().print_out() # frac, MOM invoked here from Wfn::HF::find_occupation core.timer_on("HF: Form C") self.form_C() core.timer_off("HF: Form C") if self.MOM_performed_: status.append("MOM") if self.frac_performed_: status.append("FRAC") core.timer_on("HF: Form D") self.form_D() core.timer_off("HF: Form D") core.set_variable("SCF ITERATION ENERGY", SCFE) # After we've built the new D, damp the update if (damping_enabled and self.iteration_ > 1 and Drms > core.get_option('SCF', 'DAMPING_CONVERGENCE')): damping_percentage = core.get_option('SCF', "DAMPING_PERCENTAGE") self.damping_update(damping_percentage * 0.01) status.append("DAMP={}%".format(round(damping_percentage))) if verbose > 3: self.Ca().print_out() self.Cb().print_out() self.Da().print_out() self.Db().print_out() # Print out the iteration core.print_out(" @%s%s iter %3d: %20.14f %12.5e %-11.5e %s\n" % ("DF-" if is_dfjk else "", reference, self.iteration_, SCFE, Ediff, Drms, '/'.join(status))) # if a an excited MOM is requested but not started, don't stop yet if self.MOM_excited_ and not self.MOM_performed_: continue # if a fractional occupation is requested but not started, don't stop yet if frac_enabled and not self.frac_performed_: continue # Call any postiteration callbacks if _converged(Ediff, Drms, e_conv=e_conv, d_conv=d_conv): break if self.iteration_ >= core.get_option('SCF', 'MAXITER'): raise SCFConvergenceError("""SCF iterations""", self.iteration_, self, Ediff, Drms)
def scf_iterate(self, e_conv=None, d_conv=None): is_dfjk = core.get_global_option('SCF_TYPE').endswith('DF') verbose = core.get_option('SCF', "PRINT") reference = core.get_option('SCF', "REFERENCE") # self.member_data_ signals are non-local, used internally by c-side fns self.diis_enabled_ = self.validate_diis() self.MOM_excited_ = _validate_MOM() self.diis_start_ = core.get_option('SCF', 'DIIS_START') damping_enabled = _validate_damping() soscf_enabled = _validate_soscf() frac_enabled = _validate_frac() efp_enabled = hasattr(self.molecule(), 'EFP') # SCF iterations! SCFE_old = 0.0 Dnorm = 0.0 while True: self.iteration_ += 1 diis_performed = False soscf_performed = False self.frac_performed_ = False #self.MOM_performed_ = False # redundant from common_init() self.save_density_and_energy() if efp_enabled: # EFP: Add efp contribution to Fock matrix self.H().copy(self.Horig) global mints_psi4_yo mints_psi4_yo = core.MintsHelper(self.basisset()) Vefp = modify_Fock_induced(self.molecule().EFP, mints_psi4_yo, verbose=verbose - 1) Vefp = core.Matrix.from_array(Vefp) self.H().add(Vefp) SCFE = 0.0 self.clear_external_potentials() core.timer_on("HF: Form G") self.form_G() core.timer_off("HF: Form G") incfock_performed = hasattr( self.jk(), "do_incfock_iter") and self.jk().do_incfock_iter() upcm = 0.0 if core.get_option('SCF', 'PCM'): calc_type = core.PCM.CalcType.Total if core.get_option("PCM", "PCM_SCF_TYPE") == "SEPARATE": calc_type = core.PCM.CalcType.NucAndEle Dt = self.Da().clone() Dt.add(self.Db()) upcm, Vpcm = self.get_PCM().compute_PCM_terms(Dt, calc_type) SCFE += upcm self.push_back_external_potential(Vpcm) self.set_variable("PCM POLARIZATION ENERGY", upcm) # P::e PCM self.set_energies("PCM Polarization", upcm) upe = 0.0 if core.get_option('SCF', 'PE'): Dt = self.Da().clone() Dt.add(self.Db()) upe, Vpe = self.pe_state.get_pe_contribution(Dt, elec_only=False) SCFE += upe self.push_back_external_potential(Vpe) self.set_variable("PE ENERGY", upe) # P::e PE self.set_energies("PE Energy", upe) core.timer_on("HF: Form F") # SAD: since we don't have orbitals yet, we might not be able # to form the real Fock matrix. Instead, build an initial one if (self.iteration_ == 0) and self.sad_: self.form_initial_F() else: self.form_F() core.timer_off("HF: Form F") if verbose > 3: self.Fa().print_out() self.Fb().print_out() SCFE += self.compute_E() if efp_enabled: global efp_Dt_psi4_yo # EFP: Add efp contribution to energy efp_Dt_psi4_yo = self.Da().clone() efp_Dt_psi4_yo.add(self.Db()) SCFE += self.molecule().EFP.get_wavefunction_dependent_energy() self.set_energies("Total Energy", SCFE) core.set_variable("SCF ITERATION ENERGY", SCFE) Ediff = SCFE - SCFE_old SCFE_old = SCFE status = [] # Check if we are doing SOSCF if (soscf_enabled and (self.iteration_ >= 3) and (Dnorm < core.get_option('SCF', 'SOSCF_START_CONVERGENCE'))): Dnorm = self.compute_orbital_gradient( False, core.get_option('SCF', 'DIIS_MAX_VECS')) diis_performed = False if self.functional().needs_xc(): base_name = "SOKS, nmicro=" else: base_name = "SOSCF, nmicro=" if not _converged(Ediff, Dnorm, e_conv=e_conv, d_conv=d_conv): nmicro = self.soscf_update( core.get_option('SCF', 'SOSCF_CONV'), core.get_option('SCF', 'SOSCF_MIN_ITER'), core.get_option('SCF', 'SOSCF_MAX_ITER'), core.get_option('SCF', 'SOSCF_PRINT')) # if zero, the soscf call bounced for some reason soscf_performed = (nmicro > 0) if soscf_performed: self.find_occupation() status.append(base_name + str(nmicro)) else: if verbose > 0: core.print_out( "Did not take a SOSCF step, using normal convergence methods\n" ) else: # need to ensure orthogonal orbitals and set epsilon status.append(base_name + "conv") core.timer_on("HF: Form C") self.form_C() core.timer_off("HF: Form C") soscf_performed = True # Stops DIIS if not soscf_performed: # Normal convergence procedures if we do not do SOSCF # SAD: form initial orbitals from the initial Fock matrix, and # reset the occupations. The reset is necessary because SAD # nalpha_ and nbeta_ are not guaranteed physical. # From here on, the density matrices are correct. if (self.iteration_ == 0) and self.sad_: self.form_initial_C() self.reset_occupation() self.find_occupation() else: # Run DIIS core.timer_on("HF: DIIS") diis_performed = False add_to_diis_subspace = self.diis_enabled_ and self.iteration_ >= self.diis_start_ Dnorm = self.compute_orbital_gradient( add_to_diis_subspace, core.get_option('SCF', 'DIIS_MAX_VECS')) if add_to_diis_subspace: for engine_used in self.diis(Dnorm): status.append(engine_used) core.timer_off("HF: DIIS") if verbose > 4 and diis_performed: core.print_out(" After DIIS:\n") self.Fa().print_out() self.Fb().print_out() # frac, MOM invoked here from Wfn::HF::find_occupation core.timer_on("HF: Form C") level_shift = core.get_option("SCF", "LEVEL_SHIFT") if level_shift > 0 and Dnorm > core.get_option( 'SCF', 'LEVEL_SHIFT_CUTOFF'): status.append("SHIFT") self.form_C(level_shift) else: self.form_C() core.timer_off("HF: Form C") if self.MOM_performed_: status.append("MOM") if self.frac_performed_: status.append("FRAC") if incfock_performed: status.append("INCFOCK") # Reset occupations if necessary if (self.iteration_ == 0) and self.reset_occ_: self.reset_occupation() self.find_occupation() # Form new density matrix core.timer_on("HF: Form D") self.form_D() core.timer_off("HF: Form D") self.set_variable("SCF ITERATION ENERGY", SCFE) core.set_variable("SCF D NORM", Dnorm) # After we've built the new D, damp the update if (damping_enabled and self.iteration_ > 1 and Dnorm > core.get_option('SCF', 'DAMPING_CONVERGENCE')): damping_percentage = core.get_option('SCF', "DAMPING_PERCENTAGE") self.damping_update(damping_percentage * 0.01) status.append("DAMP={}%".format(round(damping_percentage))) if core.has_option_changed("SCF", "ORBITALS_WRITE"): filename = core.get_option("SCF", "ORBITALS_WRITE") self.to_file(filename) if verbose > 3: self.Ca().print_out() self.Cb().print_out() self.Da().print_out() self.Db().print_out() # Print out the iteration core.print_out( " @%s%s iter %3s: %20.14f %12.5e %-11.5e %s\n" % ("DF-" if is_dfjk else "", reference, "SAD" if ((self.iteration_ == 0) and self.sad_) else self.iteration_, SCFE, Ediff, Dnorm, '/'.join(status))) # if a an excited MOM is requested but not started, don't stop yet # Note that MOM_performed_ just checks initialization, and our convergence measures used the pre-MOM orbitals if self.MOM_excited_ and ((not self.MOM_performed_) or self.iteration_ == core.get_option('SCF', "MOM_START")): continue # if a fractional occupation is requested but not started, don't stop yet if frac_enabled and not self.frac_performed_: continue # Call any postiteration callbacks if not ((self.iteration_ == 0) and self.sad_) and _converged( Ediff, Dnorm, e_conv=e_conv, d_conv=d_conv): break if self.iteration_ >= core.get_option('SCF', 'MAXITER'): raise SCFConvergenceError("""SCF iterations""", self.iteration_, self, Ediff, Dnorm)
def run_psi4(para: dict): try: if (not para.__contains__("multiplicity")): para["multiplicity"] = 1 if (not para.__contains__("charge")): para["charge"] = 0 if (not para.__contains__("basis")): para["basis"] = 0 if (not para.__contains__("EQ_TOLERANCE")): para["EQ_TOLERANCE"] = 1e-8 str_mol = para["mol"] str_mol = str_mol + "\n symmetry c1" mol = geometry(str_mol, "mol") core.IO.set_default_namespace("mol") mol.set_multiplicity(para["multiplicity"]) mol.set_molecular_charge(para["charge"]) core.set_global_option("BASIS", para["basis"]) core.set_global_option("SCF_TYPE", "pk") core.set_global_option("SOSCF", "false") core.set_global_option("FREEZE_CORE", "false") core.set_global_option("DF_SCF_GUESS", "false") core.set_global_option("OPDM", "true") core.set_global_option("TPDM", "true") core.set_global_option("MAXITER", 1e6) core.set_global_option("NUM_AMPS_PRINT", 1e6) core.set_global_option("R_CONVERGENCE", 1e-6) core.set_global_option("D_CONVERGENCE", 1e-6) core.set_global_option("E_CONVERGENCE", 1e-6) core.set_global_option("DAMPING_PERCENTAGE", 0) if mol.multiplicity == 1: core.set_global_option("REFERENCE", "rhf") core.set_global_option("GUESS", "sad") else: core.set_global_option("REFERENCE", "rohf") core.set_global_option("GUESS", "gwh") hf_energy, hf_wavefunction = energy('scf', return_wfn=True) except Exception as exception: return (-1, traceback.format_exc()) else: nuclear_repulsion = mol.nuclear_repulsion_energy() canonical_orbitals = numpy.asarray(hf_wavefunction.Ca()) mints = core.MintsHelper(hf_wavefunction.basisset()) fermion_str = get_molecular_hamiltonian(mints, canonical_orbitals, nuclear_repulsion, para["EQ_TOLERANCE"]) return (0, fermion_str)
def cpscf_linear_response(wfn, *args, **kwargs): """ Compute the static properties from a reference wavefunction. The currently implemented properties are - dipole polarizability - quadrupole polarizability Parameters ---------- wfn : psi4 wavefunction The reference wavefunction. args : list The list of arguments. For each argument, such as ``dipole polarizability``, will return the corresponding response. The user may also choose to pass a list or tuple of custom vectors. kwargs : dict Options that control how the response is computed. The following options are supported (with default values): - ``conv_tol``: 1e-5 - ``max_iter``: 10 - ``print_lvl``: 2 Returns ------- responses : list The list of responses. """ mints = core.MintsHelper(wfn.basisset()) # list of dictionaries to control response calculations, count how many user-supplied vectors we have complete_dict = [] n_user = 0 for arg in args: # for each string keyword, append the appropriate dictionary (vide supra) to our list if isinstance(arg, str): ret = property_dicts.get(arg) if ret: complete_dict.append(ret) else: raise ValidationError( 'Do not understand {}. Abort.'.format(arg)) # the user passed a list of vectors. absorb them into a dictionary elif isinstance(arg, tuple) or isinstance(arg, list): complete_dict.append({ 'name': 'User Vectors', 'length': len(arg), 'vectors': arg, 'vector names': [ 'User Vector {}_{}'.format(n_user, i) for i in range(len(arg)) ] }) n_user += len(arg) # single vector passed. stored in a dictionary as a list of length 1 (can be handled as the case above that way) # note: the length is set to '0' to designate that it was not really passed as a list else: complete_dict.append({ 'name': 'User Vector', 'length': 0, 'vectors': [arg], 'vector names': ['User Vector {}'.format(n_user)] }) n_user += 1 # vectors will be passed to the cphf solver, vector_names stores the corresponding names vectors = [] vector_names = [] # construct the list of vectors. for the keywords, fetch the appropriate tensors from MintsHelper for prop in complete_dict: if 'User' in prop['name']: for name, vec in zip(prop['vector names'], prop['vectors']): vectors.append(vec) vector_names.append(name) else: tmp_vectors = prop['mints_function'](mints) for tmp in tmp_vectors: tmp.scale(-2.0) # RHF only vectors.append(tmp) vector_names.append(tmp.name) # do we have any vectors to work with? if len(vectors) == 0: raise ValidationError('I have no vectors to work with. Aborting.') # print information on module, vectors that will be used _print_header(complete_dict, n_user) # fetch wavefunction information nbf = wfn.nmo() ndocc = wfn.nalpha() nvirt = nbf - ndocc c_occ = wfn.Ca_subset("AO", "OCC") c_vir = wfn.Ca_subset("AO", "VIR") # the vectors need to be in the MO basis. if they have the shape nbf x nbf, transform. for i in range(len(vectors)): shape = vectors[i].shape if shape == (nbf, nbf): vectors[i] = core.triplet(c_occ, vectors[i], c_vir, True, False, False) # verify that this vector already has the correct shape elif shape != (ndocc, nvirt): raise ValidationError( 'ERROR: "{}" has an unrecognized shape. Must be either ({}, {}) or ({}, {})' .format(vector_names[i], nbf, nbf, ndocc, nvirt)) # compute response vectors for each input vector params = [ kwargs.pop("conv_tol", 1.e-5), kwargs.pop("max_iter", 10), kwargs.pop("print_lvl", 2) ] responses = wfn.cphf_solve(vectors, *params) # zip vectors, responses for easy access vectors = {k: v for k, v in zip(vector_names, vectors)} responses = {k: v for k, v in zip(vector_names, responses)} # compute response values, format output output = [] for prop in complete_dict: # try to replicate the data structure of the input if 'User' in prop['name']: if prop['length'] == 0: output.append(responses[prop['vector names'][0]]) else: buf = [] for name in prop['vector names']: buf.append(responses[name]) output.append(buf) else: names = prop['vector names'] dim = len(names) buf = np.zeros((dim, dim)) for i, i_name in enumerate(names): for j, j_name in enumerate(names): buf[i, j] = -1.0 * vectors[i_name].vector_dot( responses[j_name]) output.append(buf) _print_output(complete_dict, output) return output
def scf_initialize(self): """Specialized initialization, compute integrals and does everything to prepare for iterations""" # Figure out memory distributions # Get memory in terms of doubles total_memory = (core.get_memory() / 8) * core.get_global_option("SCF_MEM_SAFETY_FACTOR") # Figure out how large the DFT collocation matrices are vbase = self.V_potential() if vbase: collocation_size = vbase.grid().collocation_size() if vbase.functional().ansatz() == 1: collocation_size *= 4 # First derivs elif vbase.functional().ansatz() == 2: collocation_size *= 10 # Second derivs else: collocation_size = 0 # Change allocation for collocation matrices based on DFT type jk = _build_jk(self, total_memory) jk_size = jk.memory_estimate() # Give remaining to collocation if total_memory > jk_size: collocation_memory = total_memory - jk_size # Give up to 10% to collocation elif (total_memory * 0.1) > collocation_size: collocation_memory = collocation_size else: collocation_memory = total_memory * 0.1 if collocation_memory > collocation_size: collocation_memory = collocation_size # Set constants self.iteration_ = 0 self.memory_jk_ = int(total_memory - collocation_memory) self.memory_collocation_ = int(collocation_memory) if self.get_print(): core.print_out(" ==> Integral Setup <==\n\n") # Initialize EFP efp_enabled = hasattr(self.molecule(), 'EFP') if efp_enabled: # EFP: Set QM system, options, and callback. Display efp geom in [A] efpobj = self.molecule().EFP core.print_out(efpobj.banner()) core.print_out( efpobj.geometry_summary(units_to_bohr=constants.bohr2angstroms)) efpptc, efpcoords, efpopts = get_qm_atoms_opts(self.molecule()) efpobj.set_point_charges(efpptc, efpcoords) efpobj.set_opts(efpopts, label='psi', append='psi') efpobj.set_electron_density_field_fn(efp_field_fn) # Initialize all integrals and perform the first guess if self.attempt_number_ == 1: mints = core.MintsHelper(self.basisset()) self.initialize_jk(self.memory_jk_, jk=jk) if self.V_potential(): self.V_potential().build_collocation_cache( self.memory_collocation_) core.timer_on("HF: Form core H") self.form_H() core.timer_off("HF: Form core H") if efp_enabled: # EFP: Add in permanent moment contribution and cache core.timer_on("HF: Form Vefp") verbose = core.get_option('SCF', "PRINT") Vefp = modify_Fock_permanent(self.molecule(), mints, verbose=verbose - 1) Vefp = core.Matrix.from_array(Vefp) self.H().add(Vefp) Horig = self.H().clone() self.Horig = Horig core.print_out( " QM/EFP: iterating Total Energy including QM/EFP Induction\n" ) core.timer_off("HF: Form Vefp") core.timer_on("HF: Form S/X") self.form_Shalf() core.timer_off("HF: Form S/X") core.print_out("\n ==> Pre-Iterations <==\n\n") core.timer_on("HF: Guess") self.guess() core.timer_off("HF: Guess") # Print out initial docc/socc/etc data if self.get_print(): lack_occupancy = core.get_local_option('SCF', 'GUESS') in ['SAD'] if core.get_global_option('GUESS') in ['SAD']: lack_occupancy = core.get_local_option('SCF', 'GUESS') in ['AUTO'] self.print_preiterations(small=lack_occupancy) else: self.print_preiterations(small=lack_occupancy) else: # We're reading the orbitals from the previous set of iterations. self.form_D() self.set_energies("Total Energy", self.compute_initial_E()) # turn off VV10 for iterations if core.get_option( 'SCF', "DFT_VV10_POSTSCF") and self.functional().vv10_b() > 0.0: core.print_out(" VV10: post-SCF option active \n \n") self.functional().set_lock(False) self.functional().set_do_vv10(False) self.functional().set_lock(True) # Print iteration header is_dfjk = core.get_global_option('SCF_TYPE').endswith('DF') diis_rms = core.get_option('SCF', 'DIIS_RMS_ERROR') core.print_out(" ==> Iterations <==\n\n") core.print_out( "%s Total Energy Delta E %s |[F,P]|\n\n" % (" " if is_dfjk else "", "RMS" if diis_rms else "MAX"))
def cpscf_linear_response(wfn, *args, **kwargs): """ Compute the static properties from a reference wavefunction. The currently implemented properties are - dipole polarizability - quadrupole polarizability Parameters ---------- wfn : psi4 wavefunction The reference wavefunction. args : list The list of arguments. For each argument, such as ``dipole polarizability``, will return the corresponding response. kwargs : dict Options that control how the response is computed. The following options are supported (with default values): - ``conv_tol``: 1e-5 - ``max_iter``: 10 - ``print_lvl``: 2 Returns ------- responses : list The list of response tensors. """ mints = core.MintsHelper(wfn.basisset()) # list of dictionaries to control response calculations complete_dict = [] for arg in args: # for each string keyword, append the appropriate dictionary (vide supra) to our list if not isinstance(arg, str): # TODO: better to raise TypeError? raise ValidationError("Property name must be of type string.") ret = property_dicts.get(arg) if ret: complete_dict.append(ret) else: raise ValidationError(f"Do not understand '{arg}'.") # vectors will be passed to the cphf solver, vector_names stores the corresponding names vectors = [] vector_names = [] restricted = wfn.same_a_b_orbs() # construct the list of vectors. for the keywords, fetch the appropriate tensors from MintsHelper for prop in complete_dict: tmp_vectors = prop['mints_function'](mints) for tmp in tmp_vectors: tmp.scale(-1.0) vectors.append(tmp) vector_names.append(tmp.name) # do we have any vectors to work with? if len(vectors) == 0: raise ValidationError('No vectors to work with. Aborting.') # print information on module, vectors that will be used _print_header(complete_dict) nbf = wfn.basisset().nbf() Co = [wfn.Ca_subset("AO", "OCC"), wfn.Cb_subset("AO", "OCC")] Cv = [wfn.Ca_subset("AO", "VIR"), wfn.Cb_subset("AO", "VIR")] vectors_transformed = [] for vector in vectors: if vector.shape != (nbf, nbf): raise ValidationError( f"Vector must be of shape ({nbf}, {nbf}) for transformation" " to the SO basis.") v_a = core.triplet(Co[0], vector, Cv[0], True, False, False) vectors_transformed.append(v_a) if not restricted: v_b = core.triplet(Co[1], vector, Cv[1], True, False, False) vectors_transformed.append(v_b) # compute response vectors for each input vector params = [ kwargs.pop("conv_tol", 1.e-5), kwargs.pop("max_iter", 10), kwargs.pop("print_lvl", 2) ] responses_list = wfn.cphf_solve(vectors_transformed, *params) # zip vectors, responses for easy access if restricted: vectors = { f"{k}_a": v for k, v in zip(vector_names, vectors_transformed) } responses = {f"{k}_a": v for k, v in zip(vector_names, responses_list)} else: vectors = { f"{k}_a": v for k, v in zip(vector_names, vectors_transformed[::2]) } vectors.update({ f"{k}_b": v for k, v in zip(vector_names, vectors_transformed[1::2]) }) responses = { f"{k}_a": v for k, v in zip(vector_names, responses_list[::2]) } responses.update( {f"{k}_b": v for k, v in zip(vector_names, responses_list[1::2])}) # compute response values, format output output = [] pref = -4.0 if restricted else -2.0 for prop in complete_dict: names = prop['vector names'] dim = len(names) buf = np.zeros((dim, dim)) for i, i_name in enumerate(names): for j, j_name in enumerate(names): buf[i, j] = pref * vectors[f"{i_name}_a"].vector_dot( responses[f"{j_name}_a"]) if not restricted: buf[i, j] += pref * vectors[f"{i_name}_b"].vector_dot( responses[f"{j_name}_b"]) output.append(buf) _print_output(complete_dict, output) return output
def _solve_loop(wfn, ptype, solve_function, states_per_irrep: List[int], maxiter: int, restricted: bool = True, spin_mult: str = "singlet") -> List[_TDSCFResults]: """ References ---------- For the expression of the transition moments in length and velocity gauges: - T. B. Pedersen, A. E. Hansen, "Ab Initio Calculation and Display of the Rotary Strength Tensor in the Random Phase Approximation. Method and Model Studies." Chem. Phys. Lett., 246, 1 (1995) - P. J. Lestrange, F. Egidi, X. Li, "The Consequences of Improperly Describing Oscillator Strengths beyond the Electric Dipole Approximation." J. Chem. Phys., 143, 234103 (2015) """ core.print_out("\n ==> Requested Excitations <==\n\n") for nstate, state_sym in zip(states_per_irrep, wfn.molecule().irrep_labels()): core.print_out( f" {nstate} {spin_mult} states with {state_sym} symmetry\n") # construct the engine if restricted: if spin_mult == "triplet": engine = TDRSCFEngine(wfn, ptype=ptype.lower(), triplet=True) else: engine = TDRSCFEngine(wfn, ptype=ptype.lower(), triplet=False) else: engine = TDUSCFEngine(wfn, ptype=ptype.lower()) # collect results and compute some spectroscopic observables mints = core.MintsHelper(wfn.basisset()) results = [] irrep_GS = wfn.molecule().irrep_labels()[engine.G_gs] for state_sym, nstates in enumerate(states_per_irrep): if nstates == 0: continue irrep_ES = wfn.molecule().irrep_labels()[state_sym] core.print_out( f"\n\n ==> Seeking the lowest {nstates} {spin_mult} states with {irrep_ES} symmetry" ) engine.reset_for_state_symm(state_sym) guess_ = engine.generate_guess(nstates * 4) # ret = {"eigvals": ee, "eigvecs": (rvecs, rvecs), "stats": stats} (TDA) # ret = {"eigvals": ee, "eigvecs": (rvecs, lvecs), "stats": stats} (RPA) ret = solve_function(engine, nstates, guess_, maxiter) # check whether all roots converged if not ret["stats"][-1]["done"]: # raise error raise TDSCFConvergenceError( maxiter, wfn, f"singlet excitations in irrep {irrep_ES}", ret["stats"][-1]) # flatten dictionary: helps with sorting by energy # also append state symmetry to return value for e, (R, L) in zip(ret["eigvals"], ret["eigvecs"]): irrep_trans = wfn.molecule().irrep_labels()[engine.G_gs ^ state_sym] # length-gauge electric dipole transition moment edtm_length = engine.residue(R, mints.so_dipole()) # length-gauge oscillator strength f_length = ((2 * e) / 3) * np.sum(edtm_length**2) # velocity-gauge electric dipole transition moment edtm_velocity = engine.residue(L, mints.so_nabla()) ## velocity-gauge oscillator strength f_velocity = (2 / (3 * e)) * np.sum(edtm_velocity**2) # length gauge magnetic dipole transition moment # 1/2 is the Bohr magneton in atomic units mdtm = 0.5 * engine.residue(L, mints.so_angular_momentum()) # NOTE The signs for rotatory strengths are opposite WRT the cited paper. # This is becasue Psi4 defines length-gauge dipole integral to include the electron charge (-1.0) # length gauge rotatory strength R_length = np.einsum("i,i", edtm_length, mdtm) # velocity gauge rotatory strength R_velocity = -np.einsum("i,i", edtm_velocity, mdtm) / e results.append( _TDSCFResults(e, irrep_GS, irrep_ES, irrep_trans, edtm_length, f_length, edtm_velocity, f_velocity, mdtm, R_length, R_velocity, spin_mult, R, L)) return results
def compute_sapt_sf(dimer, jk, wfn_A, wfn_B, do_print=True): """ Computes Elst and Spin-Flip SAPT0 for ROHF wavefunctions """ if do_print: core.print_out("\n ==> Preparing SF-SAPT Data Cache <== \n\n") jk.print_header() ### Build intermediates # Pull out Wavefunction A quantities ndocc_A = wfn_A.doccpi().sum() nsocc_A = wfn_A.soccpi().sum() Cocc_A = np.asarray(wfn_A.Ca_subset("AO", "OCC")) Ci = Cocc_A[:, :ndocc_A] Ca = Cocc_A[:, ndocc_A:] Pi = np.dot(Ci, Ci.T) Pa = np.dot(Ca, Ca.T) mints = core.MintsHelper(wfn_A.basisset()) V_A = mints.ao_potential() # Pull out Wavefunction B quantities ndocc_B = wfn_B.doccpi().sum() nsocc_B = wfn_B.soccpi().sum() Cocc_B = np.asarray(wfn_B.Ca_subset("AO", "OCC")) Cj = Cocc_B[:, :ndocc_B] Cb = Cocc_B[:, ndocc_B:] Pj = np.dot(Cj, Cj.T) Pb = np.dot(Cb, Cb.T) mints = core.MintsHelper(wfn_B.basisset()) V_B = mints.ao_potential() # Pull out generic quantities S = np.asarray(wfn_A.S()) intermonomer_nuclear_repulsion = dimer.nuclear_repulsion_energy() intermonomer_nuclear_repulsion -= wfn_A.molecule( ).nuclear_repulsion_energy() intermonomer_nuclear_repulsion -= wfn_B.molecule( ).nuclear_repulsion_energy() num_el_A = (2 * ndocc_A + nsocc_A) num_el_B = (2 * ndocc_B + nsocc_B) ### Build JK Terms if do_print: core.print_out("\n ==> Computing required JK matrices <== \n\n") # Writen so that we can reorganize order to save on DF-JK cost. pairs = [("ii", Ci, None, Ci), ("ij", Ci, _chain_dot(Ci.T, S, Cj), Cj), ("jj", Cj, None, Cj), ("aa", Ca, None, Ca), ("aj", Ca, _chain_dot(Ca.T, S, Cj), Cj), ("ib", Ci, _chain_dot(Ci.T, S, Cb), Cb), ("bb", Cb, None, Cb), ("ab", Ca, _chain_dot(Ca.T, S, Cb), Cb)] # Reorganize names = [x[0] for x in pairs] Cleft = [x[1] for x in pairs] rotations = [x[2] for x in pairs] Cright = [x[3] for x in pairs] tmp_J, tmp_K = _sf_compute_JK(jk, Cleft, Cright, rotations) J = {key: val for key, val in zip(names, tmp_J)} K = {key: val for key, val in zip(names, tmp_K)} ### Compute Terms if do_print: core.print_out( "\n ==> Computing Spin-Flip Exchange and Electrostatics <== \n\n") w_A = V_A + 2 * J["ii"] + J["aa"] w_B = V_B + 2 * J["jj"] + J["bb"] h_Aa = V_A + 2 * J["ii"] + J["aa"] - K["ii"] - K["aa"] h_Ab = V_A + 2 * J["ii"] + J["aa"] - K["ii"] h_Ba = V_B + 2 * J["jj"] + J["bb"] - K["jj"] h_Bb = V_B + 2 * J["jj"] + J["bb"] - K["jj"] - K["bb"] ### Build electrostatics # socc/socc term two_el_repulsion = np.vdot(Pa, J["bb"]) attractive_a = np.vdot(V_A, Pb) * nsocc_A / num_el_A attractive_b = np.vdot(V_B, Pa) * nsocc_B / num_el_B nuclear_repulsion = intermonomer_nuclear_repulsion * nsocc_A * nsocc_B / ( num_el_A * num_el_B) elst_abab = two_el_repulsion + attractive_a + attractive_b + nuclear_repulsion # docc/socc term two_el_repulsion = np.vdot(Pi, J["bb"]) attractive_a = np.vdot(V_A, Pb) * ndocc_A / num_el_A attractive_b = np.vdot(V_B, Pi) * nsocc_B / num_el_B nuclear_repulsion = intermonomer_nuclear_repulsion * ndocc_A * nsocc_B / ( num_el_A * num_el_B) elst_ibib = 2 * (two_el_repulsion + attractive_a + attractive_b + nuclear_repulsion) # socc/docc term two_el_repulsion = np.vdot(Pa, J["jj"]) attractive_a = np.vdot(V_A, Pj) * nsocc_A / num_el_A attractive_b = np.vdot(V_B, Pa) * ndocc_B / num_el_B nuclear_repulsion = intermonomer_nuclear_repulsion * nsocc_A * ndocc_B / ( num_el_A * num_el_B) elst_jaja = 2 * (two_el_repulsion + attractive_a + attractive_b + nuclear_repulsion) # docc/docc term two_el_repulsion = np.vdot(Pi, J["jj"]) attractive_a = np.vdot(V_A, Pj) * ndocc_A / num_el_A attractive_b = np.vdot(V_B, Pi) * ndocc_B / num_el_B nuclear_repulsion = intermonomer_nuclear_repulsion * ndocc_A * ndocc_B / ( num_el_A * num_el_B) elst_ijij = 4 * (two_el_repulsion + attractive_a + attractive_b + nuclear_repulsion) elst = elst_abab + elst_ibib + elst_jaja + elst_ijij # print(print_sapt_var("Elst,10", elst)) ### Start diagonal exchange exch_diag = 0.0 exch_diag -= np.vdot(Pj, 2 * K["ii"] + K["aa"]) exch_diag -= np.vdot(Pb, K["ii"]) exch_diag -= np.vdot(_chain_dot(Pi, S, Pj), (h_Aa + h_Ab + h_Ba + h_Bb)) exch_diag -= np.vdot(_chain_dot(Pa, S, Pj), (h_Aa + h_Ba)) exch_diag -= np.vdot(_chain_dot(Pi, S, Pb), (h_Ab + h_Bb)) exch_diag += 2.0 * np.vdot(_chain_dot(Pj, S, Pi, S, Pb), w_A) exch_diag += 2.0 * np.vdot(_chain_dot(Pj, S, Pi, S, Pj), w_A) exch_diag += np.vdot(_chain_dot(Pb, S, Pi, S, Pb), w_A) exch_diag += np.vdot(_chain_dot(Pj, S, Pa, S, Pj), w_A) exch_diag += 2.0 * np.vdot(_chain_dot(Pi, S, Pj, S, Pi), w_B) exch_diag += 2.0 * np.vdot(_chain_dot(Pi, S, Pj, S, Pa), w_B) exch_diag += np.vdot(_chain_dot(Pi, S, Pb, S, Pi), w_B) exch_diag += np.vdot(_chain_dot(Pa, S, Pj, S, Pa), w_B) exch_diag -= 2.0 * np.vdot(_chain_dot(Pi, S, Pj), K["ij"]) exch_diag -= 2.0 * np.vdot(_chain_dot(Pa, S, Pj), K["ij"]) exch_diag -= 2.0 * np.vdot(_chain_dot(Pi, S, Pb), K["ij"]) exch_diag -= np.vdot(_chain_dot(Pa, S, Pj), K["aj"]) exch_diag -= np.vdot(_chain_dot(Pi, S, Pb), K["ib"]) # print(print_sapt_var("Exch10,offdiagonal", exch_diag)) ### Start off-diagonal exchange exch_offdiag = 0.0 exch_offdiag -= np.vdot(Pb, K["aa"]) exch_offdiag -= np.vdot(_chain_dot(Pa, S, Pb), (h_Aa + h_Bb)) exch_offdiag += np.vdot(_chain_dot(Pa, S, Pj), K["bb"]) exch_offdiag += np.vdot(_chain_dot(Pi, S, Pb), K["aa"]) exch_offdiag += 2.0 * np.vdot(_chain_dot(Pj, S, Pa, S, Pb), w_A) exch_offdiag += np.vdot(_chain_dot(Pb, S, Pa, S, Pb), w_A) exch_offdiag += 2.0 * np.vdot(_chain_dot(Pi, S, Pb, S, Pa), w_B) exch_offdiag += np.vdot(_chain_dot(Pa, S, Pb, S, Pa), w_B) exch_offdiag -= 2.0 * np.vdot(_chain_dot(Pa, S, Pb), K["ij"]) exch_offdiag -= 2.0 * np.vdot(_chain_dot(Pa, S, Pb), K["ib"]) exch_offdiag -= 2.0 * np.vdot(_chain_dot(Pa, S, Pj), K["ab"]) exch_offdiag -= 2.0 * np.vdot(_chain_dot(Pa, S, Pj), K["ib"]) exch_offdiag -= np.vdot(_chain_dot(Pa, S, Pb), K["ab"]) # print(print_sapt_var("Exch10,off-diagonal", exch_offdiag)) # print(print_sapt_var("Exch10(S^2)", exch_offdiag + exch_diag)) ret_values = OrderedDict({ "Elst10": elst, "Exch10(S^2) [diagonal]": exch_diag, "Exch10(S^2) [off-diagonal]": exch_offdiag, "Exch10(S^2) [highspin]": exch_offdiag + exch_diag, }) return ret_values
def harmonic_analysis(hess, geom, mass, basisset, irrep_labels, project_trans=True, project_rot=True): """Like so much other Psi4 goodness, originally by @andysim Parameters ---------- hess : ndarray of float (3*nat, 3*nat) non-mass-weighted Hessian in atomic units, [Eh/a0/a0]. geom : ndarray of float (nat, 3) geometry [a0] at which Hessian computed. mass : ndarray of float (nat,) atomic masses [u]. basisset : psi4.core.BasisSet Basis set object (can be dummy, e.g., STO-3G) for SALCs. irrep_labels : list of str Irreducible representation labels. project_trans : bool, optional Idealized translations projected out of final vibrational analysis. project_rot : bool, optional Idealized rotations projected out of final vibrational analysis. Returns ------- dict, text Returns dictionary of vibration QCAspect objects (fields: lbl units data comment). Also returns text suitable for printing. .. _`table:vibaspectinfo`: +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | key | description (lbl & comment) | units | data (real/imaginary modes) | +===============+============================================+===========+======================================================+ | omega | frequency | cm^-1 | nd.array(ndof) complex (real/imag) | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | q | normal mode, normalized mass-weighted | a0 u^1/2 | ndarray(ndof, ndof) float | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | w | normal mode, un-mass-weighted | a0 | ndarray(ndof, ndof) float | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | x | normal mode, normalized un-mass-weighted | a0 | ndarray(ndof, ndof) float | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | degeneracy | degree of degeneracy | | ndarray(ndof) int | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | TRV | translation/rotation/vibration | | ndarray(ndof) str 'TR' or 'V' or '-' for partial | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | gamma | irreducible representation | | ndarray(ndof) str irrep or None if unclassifiable | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | mu | reduced mass | u | ndarray(ndof) float (+/+) | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | k | force constant | mDyne/A | ndarray(ndof) float (+/-) | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | DQ0 | RMS deviation v=0 | a0 u^1/2 | ndarray(ndof) float (+/0) | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | Qtp0 | Turning point v=0 | a0 u^1/2 | ndarray(ndof) float (+/0) | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | Xtp0 | Turning point v=0 | a0 | ndarray(ndof) float (+/0) | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ | theta_vib | char temp | K | ndarray(ndof) float (+/0) | +---------------+--------------------------------------------+-----------+------------------------------------------------------+ Examples -------- # displacement of first atom in highest energy mode >>> vibinfo['x'].data[:, -1].reshape(nat, 3)[0] # remove translations & rotations >>> vibonly = filter_nonvib(vibinfo) """ from psi4 import core if (mass.shape[0] == geom.shape[0] == (hess.shape[0] // 3) == (hess.shape[1] // 3)) and (geom.shape[1] == 3): pass else: raise ValidationError( """Dimension mismatch among mass ({}), geometry ({}), and Hessian ({})""" .format(mass.shape, geom.shape, hess.shape)) def mat_symm_info(a, atol=1e-14, lbl='array', stol=None): symm = np.allclose(a, a.T, atol=atol) herm = np.allclose(a, a.conj().T, atol=atol) ivrt = a.shape[0] - np.linalg.matrix_rank(a, tol=stol) return """ {:32} Symmetric? {} Hermitian? {} Lin Dep Dim? {:2}""".format( lbl + ':', symm, herm, ivrt) def vec_in_space(vec, space, tol=1.0e-4): merged = np.vstack((space, vec)) u, s, v = np.linalg.svd(merged) return (s[-1] < tol) vibinfo = {} text = [] nat = len(mass) text.append("""\n\n ==> Harmonic Vibrational Analysis <==\n""") if nat == 1: nrt_expected = 3 elif np.linalg.matrix_rank(geom) == 1: nrt_expected = 5 else: nrt_expected = 6 nmwhess = hess.copy() text.append( mat_symm_info(nmwhess, lbl='non-mass-weighted Hessian') + ' (0)') # get SALC object, possibly w/o trans & rot mints = core.MintsHelper(basisset) cdsalcs = mints.cdsalcs(0xFF, project_trans, project_rot) Uh = collections.OrderedDict() for h, lbl in enumerate(irrep_labels): tmp = np.asarray(cdsalcs.matrix_irrep(h)) if tmp.size > 0: Uh[lbl] = tmp # form projector of translations and rotations space = ('T' if project_trans else '') + ('R' if project_rot else '') TRspace = _get_TR_space(mass, geom, space=space, tol=LINEAR_A_TOL) nrt = TRspace.shape[0] text.append( ' projection of translations ({}) and rotations ({}) removed {} degrees of freedom ({})' .format(project_trans, project_rot, nrt, nrt_expected)) P = np.identity(3 * nat) for irt in TRspace: P -= np.outer(irt, irt) text.append(mat_symm_info(P, lbl='total projector') + ' ({})'.format(nrt)) # mass-weight & solve sqrtmmm = np.repeat(np.sqrt(mass), 3) sqrtmmminv = np.divide(1.0, sqrtmmm) mwhess = np.einsum('i,ij,j->ij', sqrtmmminv, nmwhess, sqrtmmminv) text.append(mat_symm_info(mwhess, lbl='mass-weighted Hessian') + ' (0)') pre_force_constant_au = np.linalg.eigvalsh(mwhess) idx = np.argsort(pre_force_constant_au) pre_force_constant_au = pre_force_constant_au[idx] uconv_cm_1 = np.sqrt(qcel.constants.na * qcel.constants.hartree2J * 1.0e19) / (2 * np.pi * qcel.constants.c * qcel.constants.bohr2angstroms) pre_frequency_cm_1 = np.lib.scimath.sqrt( pre_force_constant_au) * uconv_cm_1 pre_lowfreq = np.where(np.real(pre_frequency_cm_1) < 100.0)[0] pre_lowfreq = np.append( pre_lowfreq, np.arange(nrt_expected)) # catch at least nrt modes for lf in set(pre_lowfreq): vlf = pre_frequency_cm_1[lf] if vlf.imag > vlf.real: text.append( ' pre-proj low-frequency mode: {:9.4f}i [cm^-1]'.format( vlf.real, vlf.imag)) else: text.append( ' pre-proj low-frequency mode: {:9.4f} [cm^-1]'.format( vlf.real, '')) text.append(' pre-proj all modes:' + str(_format_omega(pre_frequency_cm_1, 4))) # project & solve mwhess_proj = np.dot(P.T, mwhess).dot(P) text.append( mat_symm_info(mwhess_proj, lbl='projected mass-weighted Hessian') + ' ({})'.format(nrt)) #print('projhess = ', np.array_repr(mwhess_proj)) force_constant_au, qL = np.linalg.eigh(mwhess_proj) # expected order for vibrations is steepest downhill to steepest uphill idx = np.argsort(force_constant_au) force_constant_au = force_constant_au[idx] qL = qL[:, idx] qL = _phase_cols_to_max_element(qL) vibinfo['q'] = QCAspect('normal mode', 'a0 u^1/2', qL, 'normalized mass-weighted') # frequency, LAB II.17 frequency_cm_1 = np.lib.scimath.sqrt(force_constant_au) * uconv_cm_1 vibinfo['omega'] = QCAspect('frequency', 'cm^-1', frequency_cm_1, '') # degeneracies ufreq, uinv, ucts = np.unique(np.around(frequency_cm_1, 2), return_inverse=True, return_counts=True) vibinfo['degeneracy'] = QCAspect('degeneracy', '', ucts[uinv], '') # look among the symmetry subspaces h for one to which the normco # of vib does *not* add an extra dof to the vector space active = [] irrep_classification = [] for idx, vib in enumerate(frequency_cm_1): if vec_in_space(qL[:, idx], TRspace, 1.0e-4): active.append('TR') irrep_classification.append(None) else: active.append('V') for h in Uh.keys(): if vec_in_space(qL[:, idx], Uh[h], 1.0e-4): irrep_classification.append(h) break else: irrep_classification.append(None) # catch partial Hessians if np.linalg.norm(vib) < 1.e-3: active[-1] = '-' vibinfo['TRV'] = QCAspect('translation/rotation/vibration', '', active, '') vibinfo['gamma'] = QCAspect('irreducible representation', '', irrep_classification, '') lowfreq = np.where(np.real(frequency_cm_1) < 100.0)[0] lowfreq = np.append(lowfreq, np.arange(nrt_expected)) # catch at least nrt modes for lf in set(lowfreq): vlf = frequency_cm_1[lf] if vlf.imag > vlf.real: text.append( ' post-proj low-frequency mode: {:9.4f}i [cm^-1] ({})'.format( vlf.imag, active[lf])) else: text.append( ' post-proj low-frequency mode: {:9.4f} [cm^-1] ({})'.format( vlf.real, active[lf])) text.append(' post-proj all modes:' + str(_format_omega(frequency_cm_1, 4)) + '\n') if project_trans and not project_rot: text.append( ' Note that "Vibration"s include {} un-projected rotation-like modes.' .format(nrt_expected - 3)) elif not project_trans and not project_rot: text.append( ' Note that "Vibration"s include {} un-projected rotation-like and translation-like modes.' .format(nrt_expected)) # general conversion factors, LAB II.11 uconv_K = (qcel.constants.h * qcel.constants.na * 1.0e21) / (8 * np.pi * np.pi * qcel.constants.c) uconv_S = np.sqrt( (qcel.constants.c * (2 * np.pi * qcel.constants.bohr2angstroms)**2) / (qcel.constants.h * qcel.constants.na * 1.0e21)) # normco & reduced mass, LAB II.14 & II.15 wL = np.einsum('i,ij->ij', sqrtmmminv, qL) vibinfo['w'] = QCAspect('normal mode', 'a0', wL, 'un-mass-weighted') reduced_mass_u = np.divide(1.0, np.linalg.norm(wL, axis=0)**2) vibinfo['mu'] = QCAspect('reduced mass', 'u', reduced_mass_u, '') xL = np.sqrt(reduced_mass_u) * wL vibinfo['x'] = QCAspect('normal mode', 'a0', xL, 'normalized un-mass-weighted') # force constants, LAB II.16 (real compensates for earlier sqrt) uconv_mdyne_a = (0.1 * (2 * np.pi * qcel.constants.c)**2) / qcel.constants.na force_constant_mdyne_a = reduced_mass_u * ( frequency_cm_1 * frequency_cm_1).real * uconv_mdyne_a vibinfo['k'] = QCAspect('force constant', 'mDyne/A', force_constant_mdyne_a, '') force_constant_cm_1_bb = reduced_mass_u * ( frequency_cm_1 * frequency_cm_1).real * uconv_S * uconv_S QCAspect('force constant', 'cm^-1/a0^2', force_constant_cm_1_bb, "Hooke's Law") # turning points, LAB II.20 (real & zero since turning point silly for imag modes) nu = 0 turning_point_rnc = np.sqrt(2.0 * nu + 1.0) with np.errstate(divide='ignore'): turning_point_bohr_u = turning_point_rnc / ( np.sqrt(frequency_cm_1.real) * uconv_S) turning_point_bohr_u[turning_point_bohr_u == np.inf] = 0. vibinfo['Qtp0'] = QCAspect('Turning point v=0', 'a0 u^1/2', turning_point_bohr_u, '') with np.errstate(divide='ignore'): turning_point_bohr = turning_point_rnc / ( np.sqrt(frequency_cm_1.real * reduced_mass_u) * uconv_S) turning_point_bohr[turning_point_bohr == np.inf] = 0. vibinfo['Xtp0'] = QCAspect('Turning point v=0', 'a0', turning_point_bohr, '') rms_deviation_bohr_u = turning_point_bohr_u / np.sqrt(2.0) vibinfo['DQ0'] = QCAspect('RMS deviation v=0', 'a0 u^1/2', rms_deviation_bohr_u, '') # characteristic vibrational temperature, RAK thermo & https://en.wikipedia.org/wiki/Vibrational_temperature # (imag freq zeroed) uconv_K = 100 * qcel.constants.h * qcel.constants.c / qcel.constants.kb vib_temperature_K = frequency_cm_1.real * uconv_K vibinfo['theta_vib'] = QCAspect('char temp', 'K', vib_temperature_K, '') return vibinfo, '\n'.join(text)
def __init__(self, molecule, basis, numpy_memory=2.e9, scf_type="PK", use_c=False): # Set defaults maxiter = 40 E_conv = 1.0E-6 D_conv = 1.0E-3 # Integral generation from Psi4's MintsHelper start_time = time.time() self.basis_name = basis self.molecule = molecule self.basis = pc.BasisSet.build(self.molecule, "ORBITAL", basis) self.mints = pc.MintsHelper(self.basis) self.S = np.asarray(self.mints.ao_overlap()) # Get nbf and ndocc for closed shell molecules self.nbf = self.S.shape[0] self.nel = sum( self.molecule.Z(n) for n in range(self.molecule.natom())) self.nel -= self.molecule.molecular_charge() if not (self.nel / 2.0).is_integer(): raise ValueError( "RHF: Molecule did not have an even number of electrons!") self.ndocc = int(self.nel / 2.0) print('\nNumber of occupied orbitals: %d' % self.ndocc) print('Number of basis functions: %d' % self.nbf) # Run a quick check to make sure everything will fit into memory I_Size = (self.nbf**4) * 8.e-9 print("\nSize of the ERI tensor will be %4.2f GB." % I_Size) # Estimate memory usage memory_footprint = I_Size * 1.5 if I_Size > numpy_memory: pc.clean() raise Exception( "Estimated memory utilization (%4.2f GB) exceeds numpy_memory \ limit of %4.2f GB." % (memory_footprint, numpy_memory)) # Compute required quantities for SCF self.V = np.asarray(self.mints.ao_potential()) self.T = np.asarray(self.mints.ao_kinetic()) # self.I = np.asarray(self.mints.ao_eri()) self.JK = jk.build_JK(self.molecule, self.basis_name, scf_type, use_c) self.Enuc = self.molecule.nuclear_repulsion_energy() print('\nTotal time taken for integrals: %.3f seconds.' % (time.time() - start_time)) t = time.time() # Build H_core self.H = self.T + self.V # Orthogonalizer A = S^(-1/2) using Psi4's matrix power. A = self.mints.ao_overlap() A.power(-0.5, 1.e-16) self.A = np.asarray(A) print('\nTotal time taken for setup: %.3f seconds' % (time.time() - start_time))
def _write_nbo(self, name: str): """Write wavefunction information in *wfn* to *name* in NBO format. Parameters ---------- name Destination file name for NBO file. """ basisset = self.basisset() mints = core.MintsHelper(basisset) mol = self.molecule() # Populate header and coordinates. NBO_file = f" $GENNBO NATOMS = {mol.natom()} NBAS = {basisset.nbf()} BODM " if self.nalpha() != self.nbeta(): NBO_file += f" OPEN" NBO_file += " $END\n $NBO $END\n $COORD\n" NBO_file += " GENNBO expects one comment line here. So, here's a comment line.\n" for atom in range(mol.natom()): NBO_file += f"{mol.true_atomic_number(atom):2d} {int(mol.Z(atom)):2d} {constants.bohr2angstroms * mol.x(atom):20.12f} {constants.bohr2angstroms * mol.y(atom):20.12f} {constants.bohr2angstroms * mol.z(atom):20.12f}\n" NBO_file += " $END\n" # Populate basis function information. pure_order = [ [1], # s [103, 101, 102], # p [255, 252, 253, 254, 251], # d: z2 xz yz x2-y2 xy [351, 352, 353, 354, 355, 356, 357], # f [451, 452, 453, 454, 455, 456, 457, 458, 459], #g [551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561] #h ] # For historical reasons, the code loops over shells first and then basis functions within the shell. # This turns out to not give the same ordering as looping over basis functions directly. NBO_file += " $BASIS\n" center_string = "" label_string = "" count = 0 for i in range(basisset.nshell()): shell = basisset.shell(i) am = shell.am for j in range(shell.nfunction): if not (count % 10): center_string += "\n CENTER =" if not i else "\n " label_string += "\n LABEL =" if not i else "\n " center_string += f" {shell.ncenter + 1:6d}" if basisset.has_puream(): label = pure_order[am][j] else: label = 100 * am + j + 1 label_string += f" {label:6d}" count += 1 NBO_file += center_string + label_string + "\n $END\n" # Populate contraction information.Start with exponents. NBO_file += f" $CONTRACT\n NSHELL = {basisset.nshell():6d}\n NEXP = {basisset.nprimitive():6d}\n" function_nums = "" prim_nums = "" prim_indices = "" exponents = "" prim_index = 0 coefficients = [] # [(AM int, coefficient), ...] for i in range(basisset.nshell()): if not (i % 10): function_nums += "\n NCOMP =" if not i else "\n " prim_nums += "\n NPRIM =" if not i else "\n " prim_indices += "\n NPTR =" if not i else "\n " shell = basisset.shell(i) nprim = shell.nprimitive function_nums += f" {shell.nfunction:6d}" prim_nums += f" {nprim:6d}" prim_indices += f" {prim_index + 1:6d}" for j in range(nprim): if not (prim_index % 4): exponents += "\n EXP =" if not prim_index else "\n " exponents += f"{shell.exp(j):15.6E}" prim_index += 1 coefficients.append((shell.am, shell.coef(j))) NBO_file += function_nums + prim_nums + prim_indices + exponents # Populate contraction coefficients.Because some basis sets(Poples with S and P) use the same # coefficients for multiple angular momenta, we must supply coefficients for all primitives, for all # angular momenta.This leads to many zero elements. am_labels = ["S", "P", "D", "F", "G", "H"] for current_nbo_section_am in range(basisset.max_am() + 1): for i, (shell_am, coefficient) in enumerate(coefficients): if not (i % 4): NBO_file += f"\n C{am_labels[current_nbo_section_am]} =" if not i else "\n " if shell_am != current_nbo_section_am: coefficient = 0 NBO_file += f"{coefficient:15.6E}" NBO_file += "\n $END" # That finishes most of the basis information. Next is the overlap. It would be great if we could just dump Psi's AO # overlap matrix, but we can 't. Per CCA guidelines, Psi' s Cartesian d and higher AM AOs aren't normalized to 1. # While NBO can "fix" this itself, it changes other AO quantities to match and gets the Fock matrix wrong. # Let's normalize ourselves instead. ao_overlap = mints.ao_overlap().np nbf = ao_overlap.shape[0] ao_normalizer = ao_overlap.diagonal()**(-1 / 2) def normalize(matrix, normalizer): return ((matrix * normalizer).T * normalizer).T normalized_ao_overlap = normalize(ao_overlap, ao_normalizer) def write_ao_quantity(*args): string = "" count = 0 for quantity in args: for i in range(nbf): for j in range(nbf): if not (count % 5): string += "\n " string += f"{quantity[i][j]:15.6E}" count += 1 return string NBO_file += "\n $OVERLAP" NBO_file += write_ao_quantity(normalized_ao_overlap) NBO_file += "\n $END" normalized_alpha_density = normalize(self.Da_subset("AO"), 1 / ao_normalizer) normalized_beta_density = normalize(self.Db_subset("AO"), 1 / ao_normalizer) normalized_alpha_fock = normalize(self.Fa_subset("AO"), ao_normalizer) NBO_file += "\n $DENSITY" if self.same_a_b_dens(): density = normalized_alpha_density + normalized_beta_density NBO_file += write_ao_quantity(density) else: NBO_file += write_ao_quantity(normalized_alpha_density, normalized_beta_density) NBO_file += "\n $END" NBO_file += "\n $FOCK" if not self.same_a_b_dens(): normalized_beta_fock = normalize(self.Fb_subset("AO"), ao_normalizer) NBO_file += write_ao_quantity(normalized_alpha_fock, normalized_beta_fock) else: NBO_file += write_ao_quantity(normalized_alpha_fock) NBO_file += "\n $END" # The last step is to write the MO coefficients. NBO_file += "\n $LCAOMO" def write_C_matrix(C, count): # The C coefficients supplied the missing multiplication by the ao_normalizer in the overlap matrix before. # For NBO, we need that multiplication gone. C = (C.np.T / ao_normalizer).T string = "" for i in range(self.nmo()): for mu in range(nbf): count += 1 if (count % 5 == 1): string += ("\n ") string += f"{C[mu][i]:15.6E}" # Pad linear dependencies for i in range((nbf - self.nmo()) * nbf): count += 1 if (count % 5 == 1): string += ("\n ") string += f"{0:15.6E}" return count, string count, alpha_LCAOMO = write_C_matrix(self.Ca_subset("AO", "ALL"), 0) NBO_file += alpha_LCAOMO if not self.same_a_b_orbs(): NBO_file += write_C_matrix(self.Cb_subset("AO", "ALL"), count)[1] NBO_file += "\n $END\n" #Now time to write ! with open(name, 'w') as f: f.write(NBO_file)
def __init__(self, molecule, basis, ribasis, polarizable_atoms, point_charges, polarizabilities=_theoretical_polarizabilities, cutoff_alpha=4.0, same_site_integrals='exact', dipole_damping='Thole', monopole_damping='Thole', verbose=1): """ two-electron, one-electron and zero-electron contributions to electronic Hamiltonian due to the presence of polarizable sites Parameters ---------- molecule : psi4.core.Molecule QM part basis : psi4.core.BasisSet basis set for QM part ribasis : psi4.core.BasisSet auxiliary basis set for QM part (used for resolution of identity) polarizable_atoms : psi4.core.Molecule polarizable atoms in MM part point_charges : psi4.core.Molecule MM atoms which carry point charges The values of the point charges can be set via `point_charges.set_nuclearo_charge(atom_id, charge)` polarizabilities : dict dictionary with atomic polarizabilities for each atom type (in Bohr^3) cutoff_alpha : float (in Bohr) exponent alpha in cutoff function C(r)=(1-exp(-alpha r^2))^q same_site_integrals : str 'exact' - use analytically exact polarization integrals whenever possible 'R.I.' - treat all integrals on the same footing by using the resolution of identity This only affects the 1e part of the Hamiltonian. dipole_damping : str The classical dipole polarizability may diverge for certain geometries, unless the dipole-dipole interaction is damped at close distances. The following damping functions are available: * 'Thole' - the dipole field tensor is modified according to eqns. A.1 and A.2 in [Thole]. * 'CPP' - the same cutoff function as for the CPP integrals is used (see eqn. 4 in [CPP]) (not recommended) * None - no damping at all (not recommended) monompole_damping : str The field of a point charge at the position of a polarizable atom becomes infinite as the two approach. This can be avoided if the point charge (QM nuclei and MM partial charges) are replaced by smeared out charge distributions so that the field approaches a finite value as the point charge and the polarizable atom fuse. * 'Thole' - replace the field of a point charge by eqn. A4 in [Thole] * None - use field of a point charge, E(i) = Q(i) * r(i)/r^3 (not recommended) verbose : int controls amount of output written, 0 - silent """ self.verbose = verbose self.molecule = molecule self.basis = basis # auxiliary basis set for resolution of identity self.ribasis = ribasis # number of polarizable sites self.npol = polarizable_atoms.natom() # number of basis functions self.nbf = self.basis.nbf() self.polarizable_atoms = polarizable_atoms assert self.polarizable_atoms.natom() > 0, "No polarizable atoms specified" self.point_charges = point_charges self.polarizabilities = polarizabilities if (self.verbose > 0): print(f"same-site polarization integrals are treated by method : '{same_site_integrals}'") self.same_site_integrals = same_site_integrals self.dipole_damping = dipole_damping if (self.verbose > 0): print(f"damping of dipole-dipole interaction : {dipole_damping}") self.monopole_damping = monopole_damping if (self.verbose > 0): print(f"damping of monopole field : {monopole_damping}") # cutoff function C(r) = (1- exp(-alpha r^2))^q self.cutoff_alpha = cutoff_alpha self.cutoff_power = 2 # integrals self.mints = core.MintsHelper(self.basis) # compute quantities needed for constructing two-electron, one-electron and zero-electron # contributions to the Hamiltonian due to the presence of polarizable atoms self.alpha = self._atomic_polarizabilities() self.A = self._effective_polarizability() self.F_elec = self._polarization_integrals_F() if (self.same_site_integrals == 'exact'): self.I_1e = self._polarization_integrals_I() self.F_nucl = self._nuclear_fields() # If the polarizable atoms were replaced by a single polarizable site, what # would be its polarizability tensor? self._molecular_polarizability() # for resolution of identity we need the inverse of the overlap matrix mints_ri = core.MintsHelper(self.ribasis) S = mints_ri.ao_overlap() self.Sinv = la.inv(S)