def test_unrestricted_RPA_C1(): ch2 = psi4.geometry(""" 0 3 c h 1 1.0 h 1 1.0 2 125.0 symmetry c1 """) psi4.set_options({"scf_type": "pk", 'reference': 'UHF', 'save_jk': True}) e, wfn = psi4.energy("hf/cc-pvdz", molecule=ch2, return_wfn=True) A_ref, B_ref = build_UHF_AB_C1(wfn) nI, nA, _, _ = A_ref['IAJB'].shape nIA = nI * nA ni, na, _, _ = A_ref['iajb'].shape nia = ni * na P_ref = {k: A_ref[k] + B_ref[k] for k in A_ref.keys()} M_ref = {k: A_ref[k] - B_ref[k] for k in A_ref.keys()} eng = TDUSCFEngine(wfn, ptype='rpa') X_jb = [psi4.core.Matrix.from_array(v.reshape((ni, na))) for v in tuple(np.eye(nia).T)] zero_jb = [psi4.core.Matrix(ni, na) for x in range(nIA)] X_JB = [psi4.core.Matrix.from_array(v.reshape((nI, nA))) for v in tuple(np.eye(nIA).T)] zero_JB = [psi4.core.Matrix(nI, nA) for x in range(nia)] # Guess Identity: # X_I0 X_0I = X_I0 X_0I # [ I{nOV x nOV} | 0{nOV x nov}] = [ X{KC,JB} | 0{KC, jb}] # [ 0{nov x nOV} | I{nov x nov}] [ 0{kc,JB} | X{kc, jb}] # Products: # [ A+/-B{IA, KC} A+/-B{IA, kc}] [ I{KC, JB} | 0{KC,jb}] = [A+/-B x X_I0] = [ (A+/-B)_IAJB, (A+/-B)_iaJB] # [ A+/-B{ia, KC} A+/-B{ia, kc}] [ O{kc, JB} | X{kc,jb}] [A+/-B x X_0I] = [ (A+/-B)_IAjb, (A+/-B)_iajb] X_I0 = [[x, zero] for x, zero in zip(X_JB, zero_jb)] X_0I = [[zero, x] for zero, x in zip(zero_JB, X_jb)] Px_I0, Mx_I0 = eng.compute_products(X_I0)[:-1] Px_0I, Mx_0I = eng.compute_products(X_0I)[:-1] P_IAJB_test = np.column_stack([x[0].to_array().flatten() for x in Px_I0]) assert compare_arrays(P_ref['IAJB'].reshape(nIA, nIA), P_IAJB_test, 8, "A_IAJB") M_IAJB_test = np.column_stack([x[0].to_array().flatten() for x in Mx_I0]) assert compare_arrays(M_ref['IAJB'].reshape(nIA, nIA), M_IAJB_test, 8, "A_IAJB") P_iaJB_test = np.column_stack([x[1].to_array().flatten() for x in Px_I0]) assert compare_arrays(P_ref['iaJB'].reshape(nia, nIA), P_iaJB_test, 8, "P_iaJB") M_iaJB_test = np.column_stack([x[1].to_array().flatten() for x in Mx_I0]) assert compare_arrays(M_ref['iaJB'].reshape(nia, nIA), M_iaJB_test, 8, "M_iaJB") P_IAjb_test = np.column_stack([x[0].to_array().flatten() for x in Px_0I]) assert compare_arrays(P_ref['IAjb'].reshape(nIA, nia), P_IAjb_test, 8, "P_IAjb") M_IAjb_test = np.column_stack([x[0].to_array().flatten() for x in Mx_0I]) assert compare_arrays(M_ref['IAjb'].reshape(nIA, nia), M_IAjb_test, 8, "M_IAjb") P_iajb_test = np.column_stack([x[1].to_array().flatten() for x in Px_0I]) assert compare_arrays(P_ref['iajb'].reshape(nia, nia), P_iajb_test, 8, "P_iajb") M_iajb_test = np.column_stack([x[1].to_array().flatten() for x in Mx_0I]) assert compare_arrays(M_ref['iajb'].reshape(nia, nia), M_iajb_test, 8, "M_iajb")
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 engines(): return { 'RHF-1': lambda w, p: TDRSCFEngine(w, ptype=p.lower(), triplet=False), 'RHF-3': lambda w, p: TDRSCFEngine(w, ptype=p.lower(), triplet=True), 'UHF': lambda w, p: TDUSCFEngine(w, ptype=p.lower()) }
def tdscf_excitations(wfn, **kwargs): """Compute excitations from a scf(HF/KS) wavefunction: Parameters ----------- wfn : :py:class:`psi4.core.Wavefunction` The reference wavefunction states_per_irrep : list (int), {optional} The solver will find this many lowest excitations for each irreducible representation of the computational point group. The default is to find the lowest excitation of each symmetry. If this option is provided it must have the same number of elements as the number of irreducible representations as the computational point group. triplets : str {optional, ``none``, ``only``, ``also``} The default ``none`` will solve for no triplet states, ``only`` will solve for triplet states only, and ``also`` will solve for the requested number of states of both triplet and singlet. This option is only valid for restricted references, and is ignored otherwise. The triplet and singlet solutions are found separately so using the ``also`` option will roughly double the computational cost of the calculation. tda : bool {optional ``False``} If true the Tamm-Dancoff approximation (TDA) will be employed. For HF references this is equivalent to CIS. e_tol : float, {optional, 1.0e-6} The convergence threshold for the excitation energy r_tol : float, {optional, 1.0e-8} The convergence threshold for the norm of the residual vector max_ss_vectors: int {optional} The maximum number of ss vectors that will be stored before a collapse is done. guess : str If string the guess that will be used. Allowed choices: - ``denominators``: {default} uses orbital energy differences to generate guess vectors. ..note:: The algorithm employed to solve the non-Hermitian eigenvalue problem (when ``tda`` is False) will fail when the SCF wavefunction has a triplet instability. """ # gather arguments e_tol = kwargs.pop('e_tol', 1.0e-6) r_tol = kwargs.pop('r_tol', 1.0e-8) max_ss_vec = kwargs.pop('max_ss_vectors', 50) verbose = kwargs.pop('print_lvl', 0) # how many states passed_spi = kwargs.pop('states_per_irrep', [0 for _ in range(wfn.nirrep())]) #TODO:states_per_irrep = _validate_states_args(wfn, passed_spi, nstates) states_per_irrep = passed_spi #TODO: guess types, user guess guess_type = kwargs.pop("guess", "denominators") if guess_type != "denominators": raise ValidationError("Guess type {} is not valid".format(guess_type)) # which problem ptype = 'rpa' solve_function = solvers.hamiltonian_solver if kwargs.pop('tda', False): ptype = 'tda' solve_function = solvers.davidson_solver restricted = wfn.same_a_b_orbs() if restricted: triplet = kwargs.pop('triplet', False) else: triplet = None _print_tdscf_header( etol=e_tol, rtol=r_tol, states=[(count, label) for count, label in zip(states_per_irrep, wfn.molecule().irrep_labels())], guess_type=guess_type, restricted=restricted, triplet=triplet, ptype=ptype) # construct the engine if restricted: engine = TDRSCFEngine(wfn, triplet=triplet, ptype=ptype) else: engine = TDUSCFEngine(wfn, ptype=ptype) # just energies for now solver_results = [] for state_sym, nstates in enumerate(states_per_irrep): if nstates == 0: continue engine.reset_for_state_symm(state_sym) guess_ = engine.generate_guess(nstates * 2) vecs_per_root = max_ss_vec // nstates # ret = (ee, rvecs, stats) (TDA) # ret = (ee, rvecs, lvecs, stats) (full TDSCF) ret = solve_function(engine=engine, e_tol=e_tol, r_tol=r_tol, max_vecs_per_root=vecs_per_root, nroot=nstates, guess=guess_, verbose=verbose) # store excitation energies tagged with final state symmetry (for printing) # TODO: handle R eigvecs (TDA) R/L eigvecs(full TDSCF): solver maybe should return dicts for ee in ret[0]: solver_results.append((ee, state_sym)) # sort by energy symmetry is just meta data solver_results.sort(key=lambda x: x[0]) # print excitation energies core.print_out("\n\nFinal Energetic Summary:\n") core.print_out(" " + (" " * 20) + " " + "Excitation Energy".center(31) + " {:^15}\n".format("Total Energy")) core.print_out(" {:^4} {:^20} {:^15} {:^15} {:^15}\n".format( "#", "Sym: GS->ES (Trans)", "[au]", "[eV]", "(au)")) core.print_out(" {:->4} {:->20} {:->15} {:->15} {:->15}\n".format( "-", "-", "-", "-", "-")) irrep_GS = wfn.molecule().irrep_labels()[engine.G_gs] for i, (E_ex_au, final_sym) in enumerate(solver_results): irrep_ES = wfn.molecule().irrep_labels()[final_sym] irrep_trans = wfn.molecule().irrep_labels()[engine.G_gs ^ final_sym] sym_descr = "{}->{} ({})".format(irrep_GS, irrep_ES, irrep_trans) #TODO: psivars/wfnvars E_ex_ev = constants.conversion_factor('hartree', 'eV') * E_ex_au E_tot_au = wfn.energy() + E_ex_au core.print_out( " {:^4} {:^20} {:< 15.5f} {:< 15.5f} {:< 15.5f}\n".format( i + 1, sym_descr, E_ex_au, E_ex_ev, E_tot_au)) core.print_out("\n") #TODO: output table #TODO: oscillator strengths #TODO: check/handle convergence failures return solver_results