def test_restricted_RPA_triplet_c1(): "Build out the full CIS/TDA hamiltonian (A) col by col with the product engine" h2o = psi4.geometry(""" O H 1 0.96 H 1 0.96 2 104.5 symmetry c1 """) psi4.set_options({"scf_type": "pk", 'save_jk': True}) e, wfn = psi4.energy("hf/cc-pvdz", molecule=h2o, return_wfn=True) A_ref, B_ref = build_RHF_AB_C1_triplet(wfn) ni, na, _, _ = A_ref.shape nia = ni * na A_ref = A_ref.reshape((nia, nia)) B_ref = B_ref.reshape((nia, nia)) P_ref = A_ref + B_ref M_ref = A_ref - B_ref # Build engine eng = TDRSCFEngine(wfn, ptype='rpa', triplet=True) # our "guess"" vectors ID = [psi4.core.Matrix.from_array(v.reshape((ni, na))) for v in tuple(np.eye(nia).T)] Px, Mx = eng.compute_products(ID)[:-1] P_test = np.column_stack([x.to_array().flatten() for x in Px]) assert compare_arrays(P_ref, P_test, 8, "RHF (A+B)x C1 products") M_test = np.column_stack([x.to_array().flatten() for x in Mx]) assert compare_arrays(M_ref, M_test, 8, "RHF (A-B)x C1 products")
def test_restricted_TDA_singlet_df(): "Build out the full CIS/TDA hamiltonian (A) col by col with the product engine" h2o = psi4.geometry(""" O H 1 0.96 H 1 0.96 2 104.5 """) psi4.set_options({"scf_type": "df", 'save_jk': True}) e, wfn = psi4.energy("hf/cc-pvdz", molecule=h2o, return_wfn=True) A_blocks, B_blocks = build_RHF_AB_singlet_df(wfn) eng = TDRSCFEngine(wfn, ptype='tda', triplet=False) vir_dim = wfn.nmopi() - wfn.doccpi() for hia, A_block in enumerate(A_blocks): ID = [] # Construct a matrix for each (O, V) pair with hia symmetry. for hi in range(wfn.nirrep()): for i in range(wfn.Ca_subset("SO", "OCC").coldim()[hi]): for a in range(wfn.Ca_subset("SO", "VIR").coldim()[hi ^ hia]): matrix = psi4.core.Matrix("Test Matrix", wfn.doccpi(), vir_dim, hia) matrix.set(hi, i, a, 1) ID.append(matrix) x = eng.compute_products(ID)[0][0] # Assemble the A values as a single (ia, jb) matrix, all possible ia and jb of symmetry hia. A_test = np.column_stack([np.concatenate([y.flatten() for y in x.to_array()]) for x in eng.compute_products(ID)[0]]) assert compare_arrays(A_block, A_test, 8, "DF-RHF Ax C2v products")
def test_RU_TDA_C1(): h2o = psi4.geometry("""0 1 O 0.000000 0.000000 0.135446 H -0.000000 0.866812 -0.541782 H -0.000000 -0.866812 -0.541782 symmetry c1 no_reorient no_com """) psi4.set_options({"scf_type": "pk", 'save_jk': True}) e, wfn = psi4.energy("hf/sto-3g", molecule=h2o, return_wfn=True) A_ref, _ = build_UHF_AB_C1(wfn) ni, na, _, _ = A_ref['IAJB'].shape nia = ni * na A_sing_ref = A_ref['IAJB'] + A_ref['IAjb'] A_sing_ref = A_sing_ref.reshape(nia, nia) A_trip_ref = A_ref['IAJB'] - A_ref['IAjb'] A_trip_ref = A_trip_ref.reshape(nia, nia) sing_vals, _ = np.linalg.eigh(A_sing_ref) trip_vals, _ = np.linalg.eigh(A_trip_ref) trip_eng = TDRSCFEngine(wfn, ptype='tda', triplet=True) sing_eng = TDRSCFEngine(wfn, ptype='tda', triplet=False) ID = [psi4.core.Matrix.from_array(v.reshape((ni, na))) for v in tuple(np.eye(nia).T)] psi4.core.print_out("\nA sing:\n" + str(A_sing_ref) + "\n\n") psi4.core.print_out("\nA trip:\n" + str(A_trip_ref) + "\n\n") A_trip_test = np.column_stack([x.to_array().flatten() for x in trip_eng.compute_products(ID)[0]]) assert compare_arrays(A_trip_ref, A_trip_test, 8, "Triplet Ax C1 products") A_sing_test = np.column_stack([x.to_array().flatten() for x in sing_eng.compute_products(ID)[0]]) assert compare_arrays(A_sing_ref, A_sing_test, 8, "Singlet Ax C1 products") sing_vals_2, _ = np.linalg.eigh(A_sing_test) trip_vals_2, _ = np.linalg.eigh(A_trip_test) psi4.core.print_out("\n\n SINGLET EIGENVALUES\n") for x, y in zip(sing_vals, sing_vals_2): psi4.core.print_out("{:10.6f} {:10.6f}\n".format(x, y)) # assert compare_values(x, y, 4, "Singlet ROOT") psi4.core.print_out("\n\n Triplet EIGENVALUES\n") for x, y in zip(trip_vals, trip_vals_2): psi4.core.print_out("{:10.6f} {:10.6f}\n".format(x, y)) # assert compare_values(x, y, 4, "Triplet Root") for x, y in zip(sing_vals, sing_vals_2): assert compare_values(x, y, 4, "Singlet ROOT") for x, y in zip(trip_vals, trip_vals_2): assert compare_values(x, y, 4, "Triplet Root")
def test_restricted_TDA_singlet_df_c1(): "Build out the full CIS/TDA hamiltonian (A) col by col with the product engine" h2o = psi4.geometry(""" O H 1 0.96 H 1 0.96 2 104.5 symmetry c1 """) psi4.set_options({"scf_type": "df", 'save_jk': True}) e, wfn = psi4.energy("hf/cc-pvdz", molecule=h2o, return_wfn=True) A_ref, _ = build_RHF_AB_C1_singlet_df(wfn) ni, na, _, _ = A_ref.shape nia = ni * na A_ref = A_ref.reshape((nia, nia)) # Build engine eng = TDRSCFEngine(wfn, ptype='tda', triplet=False) # our "guess"" vectors ID = [psi4.core.Matrix.from_array(v.reshape((ni, na))) for v in tuple(np.eye(nia).T)] A_test = np.column_stack([x.to_array().flatten() for x in eng.compute_products(ID)[0]]) assert compare_arrays(A_ref, A_test, 8, "DF-RHF Ax C1 products")
def test_RU_TDA_C1(): h2o = psi4.geometry("""0 1 O 0.000000 0.000000 0.135446 H -0.000000 0.866812 -0.541782 H -0.000000 -0.866812 -0.541782 symmetry c1 no_reorient no_com """) psi4.set_options({"scf_type": "pk", 'save_jk': True}) e, wfn = psi4.energy("hf/sto-3g", molecule=h2o, return_wfn=True) A_ref, _ = build_UHF_AB_C1(wfn) ni, na, _, _ = A_ref['IAJB'].shape nia = ni * na A_sing_ref = A_ref['IAJB'] + A_ref['IAjb'] A_sing_ref = A_sing_ref.reshape(nia, nia) A_trip_ref = A_ref['IAJB'] - A_ref['IAjb'] A_trip_ref = A_trip_ref.reshape(nia, nia) sing_vals, _ = np.linalg.eigh(A_sing_ref) trip_vals, _ = np.linalg.eigh(A_trip_ref) trip_eng = TDRSCFEngine(wfn, ptype='tda', triplet=True) sing_eng = TDRSCFEngine(wfn, ptype='tda', triplet=False) ID = [ psi4.core.Matrix.from_array(v.reshape((ni, na))) for v in tuple(np.eye(nia).T) ] psi4.core.print_out("\nA sing:\n" + str(A_sing_ref) + "\n\n") psi4.core.print_out("\nA trip:\n" + str(A_trip_ref) + "\n\n") A_trip_test = np.column_stack( [x.to_array().flatten() for x in trip_eng.compute_products(ID)[0]]) assert compare_arrays(A_trip_ref, A_trip_test, 8, "Triplet Ax C1 products") A_sing_test = np.column_stack( [x.to_array().flatten() for x in sing_eng.compute_products(ID)[0]]) assert compare_arrays(A_sing_ref, A_sing_test, 8, "Singlet Ax C1 products") sing_vals_2, _ = np.linalg.eigh(A_sing_test) trip_vals_2, _ = np.linalg.eigh(A_trip_test) psi4.core.print_out("\n\n SINGLET EIGENVALUES\n") for x, y in zip(sing_vals, sing_vals_2): psi4.core.print_out("{:10.6f} {:10.6f}\n".format(x, y)) # assert compare_values(x, y, 4, "Singlet ROOT") psi4.core.print_out("\n\n Triplet EIGENVALUES\n") for x, y in zip(trip_vals, trip_vals_2): psi4.core.print_out("{:10.6f} {:10.6f}\n".format(x, y)) # assert compare_values(x, y, 4, "Triplet Root") for x, y in zip(sing_vals, sing_vals_2): assert compare_values(x, y, 4, "Singlet ROOT") for x, y in zip(trip_vals, trip_vals_2): assert compare_values(x, y, 4, "Triplet Root")
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
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