def prefactor_opa() -> float: r"""Prefactor for converting microscopic observable to decadic molar extinction coefficient in one-photon absorption. Returns ------- prefactor : float Notes ----- This function implements the calculation of the following prefactor: .. math:: k = \frac{4\pi^{2}N_{\mathrm{A}}}{3\times 1000\times \ln(10) (4 \pi \epsilon_{0}) n \hbar c} The prefactor is computed in SI units and then adjusted for the fact that we use atomic units to express microscopic observables: excitation energies and transition dipole moments. The refractive index :math:`n` is, in general, frequency-dependent. We assume it to be constant and equal to 1. """ N_A = constants.get("Avogadro constant") c = constants.get("speed of light in vacuum") hbar = constants.get("Planck constant over 2 pi") e_0 = constants.get("electric constant") au_to_Coulomb_centimeter = constants.get( "elementary charge") * constants.get( "Bohr radius") * constants.conversion_factor("m", "cm") numerator = 4.0 * np.pi**2 * N_A denominator = 3 * 1000 * np.log(10) * (4 * np.pi * e_0) * hbar * c return (numerator / denominator) * au_to_Coulomb_centimeter**2
def tdscf_excitations(wfn, *, states: Union[int, List[int]], triplets: str = "NONE", tda: bool = False, r_convergence: float = 1.0e-4, maxiter: int = 60, guess: str = "DENOMINATORS", verbose: int = 1): """Compute excitations from a SCF(HF/KS) wavefunction Parameters ----------- wfn : :py:class:`psi4.core.Wavefunction` The reference wavefunction states : Union[int, List[int]] How many roots (excited states) should the solver seek to converge? This function accepts either an integer or a list of integers: - The list has :math:`n_{\mathrm{irrep}}` elements and is only acceptable if the system has symmetry. It tells the solver how many states per irrep to calculate. - If an integer is given _and_ the system has symmetry, the states will be distributed among irreps. For example, ``states = 10`` for a D2h system will compute 10 states distributed as ``[2, 2, 1, 1, 1, 1, 1, 1]`` among irreps. triplets : {"NONE", "ONLY", "ALSO"} Should the solver seek to converge states of triplet symmetry? Default is `none`: do not seek to converge triplets. Valid options are: - `NONE`. Do not seek to converge triplets. - `ONLY`. Only seek to converge triplets. - `ALSO`. Seek to converge both triplets and singlets. This choice is only valid for restricted reference wavefunction. The number of states given will be apportioned roughly 50-50 between singlet and triplet states, preferring the former. For example: given ``state = 5, triplets = "ALSO"``, the solver will seek to converge 3 states of singlet spin symmetry and 2 of triplet spin symmetry. When asking for ``states = [3, 3, 3, 3], triplets = "ALSO"`` states (C2v symmetry), ``[2, 2, 2, 2]`` will be of singlet spin symmetry and ``[1, 1, 1, 1]``` will be of triplet spin symmetry. tda : bool, optional. Should the solver use the Tamm-Dancoff approximation (TDA) or the random-phase approximation (RPA)? Default is ``False``: use RPA. Note that TDA is equivalent to CIS for HF references. r_convergence : float, optional. The convergence threshold for the norm of the residual vector. Default: 1.0e-4 Using a tighter convergence threshold here requires tighter SCF ground state convergence threshold. As a rule of thumb, with the SCF ground state density converged to :math:`10^{-N}` (``D_CONVERGENGE = 1.0e-N``), you can afford converging a corresponding TDSCF calculation to :math:`10^{-(N-2)}`. The default value is consistent with the default value for ``D_CONVERGENCE``. maxiter : int, optional Maximum number of iterations. Default: 60 guess : str, optional. How should the starting trial vectors be generated? Default: `DENOMINATORS`, i.e. use orbital energy differences to generate guess vectors. verbose : int, optional. How verbose should the solver be? Default: 1 Notes ----- The algorithm employed to solve the non-Hermitian eigenvalue problem (``tda = False``) will fail when the SCF wavefunction has a triplet instability. This function can be used for: - restricted singlets: RPA or TDA, any functional - restricted triplets: RPA or TDA, Hartree-Fock only - unresctricted: RPA or TDA, Hartre-Fock and LDA only Tighter convergence thresholds will require a larger iterative subspace. The maximum size of the iterative subspace is calculated based on `r_convergence`: max_vecs_per_root = -np.log10(r_convergence) * 50 for the default converegence threshold this gives 200 trial vectors per root and a maximum subspace size of: max_ss_size = max_vecs_per_root * n where `n` are the number of roots to seek in the given irrep. For each irrep, the algorithm will store up to `max_ss_size` trial vectors before collapsing (restarting) the iterations from the `n` best approximations. """ # validate input parameters triplets = triplets.upper() guess = guess.upper() _validate_tdscf(wfn=wfn, states=states, triplets=triplets, guess=guess) restricted = wfn.same_a_b_orbs() # determine how many states per irrep to seek and apportion them between singlets/triplets and irreps. singlets_per_irrep = [] triplets_per_irrep = [] if isinstance(states, list): if triplets == "ONLY": triplets_per_irrep = states elif triplets == "ALSO": singlets_per_irrep = [(s // 2) + (s % 2) for s in states] triplets_per_irrep = [(s // 2) for s in states] else: singlets_per_irrep = states else: # total number of states given # first distribute them among singlets and triplets, preferring the # former then distribute them among irreps if triplets == "ONLY": triplets_per_irrep = _states_per_irrep(states, wfn.nirrep()) elif triplets == "ALSO": spi = (states // 2) + (states % 2) singlets_per_irrep = _states_per_irrep(spi, wfn.nirrep()) tpi = states - spi triplets_per_irrep = _states_per_irrep(tpi, wfn.nirrep()) else: singlets_per_irrep = _states_per_irrep(states, wfn.nirrep()) # tie maximum number of vectors per root to requested residual tolerance # This gives 200 vectors per root with default tolerance max_vecs_per_root = int(-np.log10(r_convergence) * 50) def rpa_solver(e, n, g, m): return solvers.hamiltonian_solver(engine=e, nroot=n, guess=g, r_convergence=r_convergence, max_ss_size=max_vecs_per_root * n, verbose=verbose) def tda_solver(e, n, g, m): return solvers.davidson_solver(engine=e, nroot=n, guess=g, r_convergence=r_convergence, max_ss_size=max_vecs_per_root * n, verbose=verbose) # determine which solver function to use: Davidson for TDA or Hamiltonian for RPA? if tda: ptype = "TDA" solve_function = tda_solver else: ptype = "RPA" solve_function = rpa_solver _print_tdscf_header(r_convergence=r_convergence, guess_type=guess, restricted=restricted, ptype=ptype) # collect solver results into a list _results = [] # singlets solve loop if triplets == "NONE" or triplets == "ALSO": res_1 = _solve_loop(wfn, ptype, solve_function, singlets_per_irrep, maxiter, restricted, "singlet") _results.extend(res_1) # triplets solve loop if triplets == "ALSO" or triplets == "ONLY": res_3 = _solve_loop(wfn, ptype, solve_function, triplets_per_irrep, maxiter, restricted, "triplet") _results.extend(res_3) # sort by energy _results = sorted(_results, key=lambda x: x.E_ex_au) core.print_out("\n{}\n".format("*"*90) + "{}{:^70}{}\n".format("*"*10, "WARNING", "*"*10) + "{}{:^70}{}\n".format("*"*10, "Length-gauge rotatory strengths are **NOT** gauge-origin invariant", "*"*10) + "{}\n\n".format("*"*90)) #yapf: disable # print results core.print_out(" " + (" " * 20) + " " + "Excitation Energy".center(31) + f" {'Total Energy':^15}" + "Oscillator Strength".center(31) + "Rotatory Strength".center(31) + "\n") core.print_out( f" {'#':^4} {'Sym: GS->ES (Trans)':^20} {'au':^15} {'eV':^15} {'au':^15} {'au (length)':^15} {'au (velocity)':^15} {'au (length)':^15} {'au (velocity)':^15}\n" ) core.print_out( f" {'-':->4} {'-':->20} {'-':->15} {'-':->15} {'-':->15} {'-':->15} {'-':->15} {'-':->15} {'-':->15}\n" ) # collect results solver_results = [] for i, x in enumerate(_results): sym_descr = f"{x.irrep_GS}->{x.irrep_ES} ({1 if x.spin_mult== 'singlet' else 3} {x.irrep_trans})" E_ex_ev = constants.conversion_factor('hartree', 'eV') * x.E_ex_au E_tot_au = wfn.energy() + x.E_ex_au # prepare return dictionary for this root solver_results.append({ "EXCITATION ENERGY": x.E_ex_au, "ELECTRIC DIPOLE TRANSITION MOMENT (LEN)": x.edtm_length, "OSCILLATOR STRENGTH (LEN)": x.f_length, "ELECTRIC DIPOLE TRANSITION MOMENT (VEL)": x.edtm_velocity, "OSCILLATOR STRENGTH (VEL)": x.f_velocity, "MAGNETIC DIPOLE TRANSITION MOMENT": x.mdtm, "ROTATORY STRENGTH (LEN)": x.R_length, "ROTATORY STRENGTH (VEL)": x.R_velocity, "SYMMETRY": x.irrep_trans, "SPIN": x.spin_mult, "RIGHT EIGENVECTOR ALPHA": x.R_eigvec if restricted else x.R_eigvec[0], "LEFT EIGENVECTOR ALPHA": x.L_eigvec if restricted else x.L_eigvec[0], "RIGHT EIGENVECTOR BETA": x.R_eigvec if restricted else x.R_eigvec[1], "LEFT EIGENVECTOR BETA": x.L_eigvec if restricted else x.L_eigvec[1], }) # stash in psivars/wfnvars ssuper_name = wfn.functional().name() wfn.set_variable( f"TD-{ssuper_name} ROOT {i+1} TOTAL ENERGY - {x.irrep_ES} SYMMETRY", E_tot_au) wfn.set_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} EXCITATION ENERGY - {x.irrep_ES} SYMMETRY", x.E_ex_au) wfn.set_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} OSCILLATOR STRENGTH (LEN) - {x.irrep_ES} SYMMETRY", x.f_length) wfn.set_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} OSCILLATOR STRENGTH (VEL) - {x.irrep_ES} SYMMETRY", x.f_velocity) wfn.set_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} ROTATORY STRENGTH (LEN) - {x.irrep_ES} SYMMETRY", x.R_length) wfn.set_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} ROTATORY STRENGTH (VEL) - {x.irrep_ES} SYMMETRY", x.R_velocity) wfn.set_array_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} ELECTRIC TRANSITION DIPOLE MOMENT (LEN) - {x.irrep_ES} SYMMETRY", core.Matrix.from_array(x.edtm_length.reshape((1, 3)))) wfn.set_array_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} ELECTRIC TRANSITION DIPOLE MOMENT (VEL) - {x.irrep_ES} SYMMETRY", core.Matrix.from_array(x.edtm_velocity.reshape((1, 3)))) wfn.set_array_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} MAGNETIC TRANSITION DIPOLE MOMENT - {x.irrep_ES} SYMMETRY", core.Matrix.from_array(x.mdtm.reshape((1, 3)))) wfn.set_array_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} RIGHT EIGENVECTOR ALPHA - {x.irrep_ES} SYMMETRY", x.R_eigvec if restricted else x.R_eigvec[0]) wfn.set_array_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} LEFT EIGENVECTOR ALPHA - {x.irrep_ES} SYMMETRY", x.L_eigvec if restricted else x.L_eigvec[0]) wfn.set_array_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} RIGHT EIGENVECTOR BETA - {x.irrep_ES} SYMMETRY", x.R_eigvec if restricted else x.R_eigvec[1]) wfn.set_array_variable( f"TD-{ssuper_name} ROOT 0 -> ROOT {i+1} LEFT EIGENVECTOR ALPHA - {x.irrep_ES} SYMMETRY", x.L_eigvec if restricted else x.L_eigvec[1]) core.print_out( f" {i+1:^4} {sym_descr:^20} {x.E_ex_au:< 15.5f} {E_ex_ev:< 15.5f} {E_tot_au:< 15.5f} {x.f_length:< 15.4f} {x.f_velocity:< 15.4f} {x.R_length:< 15.4f} {x.R_velocity:< 15.4f}\n" ) core.print_out("\n") return solver_results
def _analyze_tdscf_excitations(tdscf_results, wfn, tda, coeff_cutoff, tdm_print): restricted = wfn.same_a_b_orbs() # Print out requested dipole moment vectors _printable = { "E_TDM_LEN": { "title": "Electric Transition Dipole Moments (Length) (au)", "what": lambda x: x.edtm_length, }, "E_TDM_VEL": { "title": "Electric Transition Dipole Moments (Velocity) (au)", "what": lambda x: x.edtm_velocity, }, "M_TDM": { "title": "Magnetic Transition Dipole Moments (au)", "what": lambda x: x.mdtm, }, } for p in tdm_print: core.print_out( f"\n{_printable[p]['title']}:\nState X Y Z\n" ) for i, q in enumerate(tdscf_results): x, y, z = _printable[p]['what'](q) core.print_out(f" {i+1: 4d} {x:< 10.6f} {y:< 10.6f} {z:< 10.6f}\n") # Print contributing transitions... core.print_out( f"\n\nContributing excitations{'' if tda else ' and de-excitations'}") #...only currently for C1 symmetry if wfn.molecule().point_group().symbol() != 'c1': core.print_out("...only curently available with C1 symmetry\n") else: core.print_out( f"\nOnly contributions with coefficients >{coeff_cutoff: .2e} will be printed:\n" ) for i, x in enumerate(tdscf_results): E_ex_nm = 1e9 / (constants.conversion_factor('hartree', 'm^-1') * x.E_ex_au) core.print_out( f"\nExcited State {i+1:4d} ({1 if x.spin_mult== 'singlet' else 3} {x.irrep_ES}):" ) core.print_out( f"{x.E_ex_au:> 10.5f} au {E_ex_nm: >.2f} nm f = {x.f_length: >.4f}\n" ) if not restricted: core.print_out("Alpha orbitals:\n") # Extract contributing transitions from left and right eigenvectors from solver if tda: X = x.L_eigvec if restricted else x.L_eigvec[0] Xssq = X.sum_of_squares() core.print_out(f" Sums of squares: Xssq = {Xssq: .6e}\n") else: L = x.L_eigvec if restricted else x.L_eigvec[0] R = x.R_eigvec if restricted else x.R_eigvec[0] X = L.clone() X.add(R) X.scale(0.5) Y = R.clone() Y.subtract(L) Y.scale(0.5) Xssq = X.sum_of_squares() Yssq = Y.sum_of_squares() core.print_out( f" Sums of squares: Xssq = {Xssq: .6e}; Yssq = {Yssq: .6e}; Xssq - Yssq = {Xssq-Yssq: .6e}\n" ) nocc = X.rows() nvirt = X.cols() # Ignore any scaling for now div = 1 # Excitations for row in range(nocc): for col in range(nvirt): coef = X.get(row, col) / div if abs(coef) > coeff_cutoff: perc = 100 * coef**2 core.print_out( f" {row+1: 3} ->{col+1+nocc: 3} {coef: 10.6f} ({perc: >6.3f}%)\n" ) # De-excitations if not using TDA if not tda: for row in range(nocc): for col in range(nvirt): coef = Y.get(row, col) / div if abs(coef) > coeff_cutoff: perc = 100 * coef**2 core.print_out( f" {row+1: 3} <-{col+1+nocc: 3} {coef: 10.6f} ({perc: >6.3f}%)\n" ) # Now treat beta orbitals if needed if not restricted: core.print_out("Beta orbitals:\n") if tda: X = x.L_eigvec if restricted else x.L_eigvec[1] Xssq = X.sum_of_squares() core.print_out(f" Sums of squares: Xssq = {Xssq: .6e}\n") else: L = x.L_eigvec if restricted else x.L_eigvec[1] R = x.R_eigvec if restricted else x.R_eigvec[1] X = L.clone() X.add(R) X.scale(0.5) Y = R.clone() Y.subtract(L) Y.scale(0.5) Xssq = X.sum_of_squares() Yssq = Y.sum_of_squares() core.print_out( f" Sums of squares: Xssq = {Xssq: .6e}; Yssq = {Yssq: .6e}; Xssq - Yssq = {Xssq-Yssq: .6e}\n" ) nocc = X.rows() nvirt = X.cols() # Excitations (beta orbitals) for row in range(nocc): for col in range(nvirt): coef = X.get(row, col) / div if abs(coef) > coeff_cutoff: perc = 100 * coef**2 core.print_out( f" {row+1: 3}B->{col+1+nocc: 3}B {coef: 10.6f} ({perc: >6.3f}%)\n" ) # De-excitations if not using TDA (beta orbitals): if not tda: for row in range(nocc): for col in range(nvirt): coef = Y.get(row, col) / div if abs(coef) > coeff_cutoff: perc = 100 * coef**2 core.print_out( f" {row+1: 3}B<-{col+1+nocc: 3}B {coef: 10.6f} ({perc: >6.3f}%)\n" ) core.print_out("\n")
def spectrum(*, poles: Union[List[float], np.ndarray], residues: Union[List[float], np.ndarray], kind: str = "opa", lineshape: str = "gaussian", gamma: float = 0.2, npoints: int = 5000, out_units: str = "nm") -> Dict[str, np.ndarray]: r"""One-photon absorption (OPA) or electronic circular dichroism (ECD) spectra with phenomenological line broadening. This function gives arrays of values ready to be plotted as OPA spectrum: .. math:: \varepsilon(\omega) = \frac{4\pi^{2}N_{\mathrm{A}}\omega}{3\times 1000\times \ln(10) (4 \pi \epsilon_{0}) n \hbar c} \sum_{i \rightarrow j}g_{ij}(\omega)|\mathbf{\mu}_{ij}|^{2} or ECD spectrum: .. math:: \Delta\varepsilon(\omega) = \frac{16\pi^{2}N_{\mathrm{A}}\omega}{3\times 1000\times \ln(10) (4 \pi \epsilon_{0}) n \hbar c^{2}} \sum_{i \rightarrow j}g_{ij}(\omega)\Im(\mathbf{\mu}_{ij}\cdot\mathbf{m}_{ij}) in macroscopic units of :math:`\mathrm{L}\cdot\mathrm{mol}^{-1}\cdot\mathrm{cm}^{-1}`. The lineshape function :math:`g_{ij}(\omega)` with phenomenological broadening :math:`\gamma` is used for the convolution of the infinitely narrow results from a linear response calculation. Parameters ---------- poles Poles of the response function, i.e. the excitation energies. These are **expected** in atomic units of angular frequency. residues Residues of the linear response functions, i.e. transition dipole moments (OPA) and rotatory strengths (ECD). These are **expected** in atomic units. kind {"opa", "ecd"} Which kind of spectrum to generate, one-photon absorption ("opa") or electronic circular dichroism ("ecd"). Default is `opa`. lineshape {"gaussian", "lorentzian"} The lineshape function to use in the fitting. Default is `gaussian`. gamma Full width at half maximum of the lineshape function. Default is 0.2 au of angular frequency. This value is **expected** in atomic units of angular frequency. npoints How many points to generate for the x axis. Default is 5000. out_units Units for the output array `x`, the x axis of the spectrum plot. Default is wavelengths in nanometers. Valid (and case-insensitive) values for the units are: - `au` atomic units of angular frequency - `Eh` atomic units of energy - `eV` - `nm` - `THz` Returns ------- spectrum : Dict The fitted electronic absorption spectrum, with units for the x axis specified by the `out_units` parameter. This is a dictionary containing the convoluted (key: `convolution`) and the infinitely narrow spectra (key: `sticks`). .. code-block:: python {"convolution": {"x": np.ndarray, "y": np.ndarray}, "sticks": {"poles": np.ndarray, "residues": np.ndarray}} Notes ----- * Conversion of the broadening parameter :math:`\gamma`. The lineshape functions are formulated as functions of the angular frequency :math:`\omega`. When converting to other physical quantities, the broadening parameter has to be modified accordingly. If :math:`\gamma_{\omega}` is the chosen broadening parameter then: - Wavelength: :math:`gamma_{\lambda} = \frac{\lambda_{ij}^{2}}{2\pi c}\gamma_{\omega}` - Frequency: :math:`gamma_{\nu} = \frac{\gamma_{\omega}}{2\pi}` - Energy: :math:`gamma_{E} = \gamma_{\omega}\hbar` References ---------- A. Rizzo, S. Coriani, K. Ruud, "Response Function Theory Computational Approaches to Linear and Nonlinear Optical Spectroscopy". In Computational Strategies for Spectroscopy. """ # Transmute inputs to np.ndarray if isinstance(poles, list): poles = np.array(poles) if isinstance(residues, list): residues = np.array(residues) # Validate input arrays if poles.shape != residues.shape: raise ValueError( f"Shapes of poles ({poles.shape}) and residues ({residues.shape}) vectors do not match!" ) # Validate kind of spectrum kind = kind.lower() valid_kinds = ["opa", "ecd"] if kind not in valid_kinds: raise ValueError( f"Spectrum kind {kind} not among recognized ({valid_kinds})") # Validate output units out_units = out_units.lower() valid_out_units = ["au", "eh", "ev", "nm", "thz"] if out_units not in valid_out_units: raise ValueError( f"Output units {out_units} not among recognized ({valid_out_units})" ) c = constants.get("speed of light in vacuum") c_nm = c * constants.conversion_factor("m", "nm") hbar = constants.get("Planck constant over 2 pi") h = constants.get("Planck constant") Eh = constants.get("Hartree energy") au_to_nm = 2.0 * np.pi * c_nm * hbar / Eh au_to_THz = (Eh / h) * constants.conversion_factor("Hz", "THz") au_to_eV = constants.get("Hartree energy in eV") converters = { "au": lambda x: x, # Angular frequency in atomic units "eh": lambda x: x, # Energy in atomic units "ev": lambda x: x * au_to_eV, # Energy in electronvolts "nm": lambda x: au_to_nm / x, # Wavelength in nanometers "thz": lambda x: x * au_to_THz, # Frequency in terahertz } # Perform conversion of poles from au of angular frequency to output units poles = converters[out_units](poles) # Broadening functions gammas = { "au": lambda x_0: gamma, # Angular frequency in atomic units "eh": lambda x_0: gamma, # Energy in atomic units "ev": lambda x_0: gamma * au_to_eV, # Energy in electronvolts "nm": lambda x_0: ((x_0**2 * gamma * (Eh / hbar)) / (2 * np.pi * c_nm)), # Wavelength in nanometers "thz": lambda x_0: gamma * au_to_THz, # Frequency in terahertz } # Generate x axis # Add a fifth of the range on each side expand_side = (np.max(poles) - np.min(poles)) / 5 x = np.linspace( np.min(poles) - expand_side, np.max(poles) + expand_side, npoints) # Validate lineshape lineshape = lineshape.lower() valid_lineshapes = ["gaussian", "lorentzian"] if lineshape not in valid_lineshapes: raise ValueError( f"Lineshape {lineshape} not among recognized ({valid_lineshapes})") # Obtain lineshape function shape = Gaussian( x, gammas[out_units]) if lineshape == "gaussian" else Lorentzian( x, gammas[out_units]) # Generate y axis, i.e. molar decadic absorption coefficient prefactor = prefactor_opa() if kind == "opa" else prefactor_ecd() transform_residue = (lambda x: x**2) if kind == "opa" else (lambda x: x) y = prefactor * x * np.sum([ transform_residue(r) * shape.lineshape(p) for p, r in zip(poles, residues) ], axis=0) # Generate sticks sticks = prefactor * np.array([ p * transform_residue(r) * shape.maximum(p) for p, r in zip(poles, residues) ]) return { "convolution": { "x": x, "y": y }, "sticks": { "poles": poles, "residues": sticks } }
def _analyze_tdscf_excitations(tdscf_results, wfn, tda, coeff_cutoff, tdm_print): restricted = wfn.same_a_b_orbs() # Print out requested dipole moment vectors _printable = { "E_TDM_LEN": { "title": "Electric Transition Dipole Moments (Length) (au)", "what": lambda x: x.edtm_length, }, "E_TDM_VEL": { "title": "Electric Transition Dipole Moments (Velocity) (au)", "what": lambda x: x.edtm_velocity, }, "M_TDM": { "title": "Magnetic Transition Dipole Moments (au)", "what": lambda x: x.mdtm, }, } for p in tdm_print: core.print_out( f"\n{_printable[p]['title']}:\nState X Y Z\n" ) for i, q in enumerate(tdscf_results): x, y, z = _printable[p]['what'](q) core.print_out(f" {i+1: 4d} {x:< 10.6f} {y:< 10.6f} {z:< 10.6f}\n") # Print contributing transitions... core.print_out( f"\n\nContributing excitations{'' if tda else ' and de-excitations'}") core.print_out( f"\nOnly contributions with abs(coeff) >{coeff_cutoff: .2e} will be printed:\n" ) for i, x in enumerate(tdscf_results): E_ex_nm = 1e9 / (constants.conversion_factor('hartree', 'm^-1') * x.E_ex_au) core.print_out( f"\nExcited State {i+1:4d} ({1 if x.spin_mult== 'singlet' else 3} {x.irrep_ES}):" ) core.print_out( f"{x.E_ex_au:> 10.5f} au {E_ex_nm: >.2f} nm f = {x.f_length: >.4f}\n" ) if not restricted: core.print_out("Alpha orbitals:\n") # Extract contributing transitions from left and right eigenvectors from solver if tda: X = x.L_eigvec if restricted else x.L_eigvec[0] Xssq = X.sum_of_squares() core.print_out(f" Sums of squares: Xssq = {Xssq: .6e}\n") else: L = x.L_eigvec if restricted else x.L_eigvec[0] R = x.R_eigvec if restricted else x.R_eigvec[0] X = L.clone() X.add(R) X.scale(0.5) Y = R.clone() Y.subtract(L) Y.scale(0.5) Xssq = X.sum_of_squares() Yssq = Y.sum_of_squares() core.print_out( f" Sums of squares: Xssq = {Xssq: .6e}; Yssq = {Yssq: .6e}; Xssq - Yssq = {Xssq-Yssq: .6e}\n" ) nocc = wfn.epsilon_a_subset("SO", "OCC").shape nvir = wfn.epsilon_a_subset("SO", "VIR").shape # Ignore any scaling for now div = 1 # Excitations nirrep = X.nirrep() # Convert to list of lists for C1 case if nirrep == 1: nocc = (nocc, ) nvir = (nvir, ) for h in range(nirrep): h_vir = x.irrep_trans_index ^ h occ_irrep = wfn.molecule().irrep_labels()[h].lower() vir_irrep = wfn.molecule().irrep_labels()[h_vir].lower() for row in range(nocc[h][0]): for col in range(nvir[h_vir][0]): if nirrep == 1: coef = X.np[row][col] / div else: coef = X.nph[h][row][col] / div if abs(coef) > coeff_cutoff: perc = 100 * coef**2 core.print_out( f" {row+1: 4}{occ_irrep} {'' if restricted else '(a)'} ->{col+1+nocc[h_vir][0]: 4}{vir_irrep} {'' if restricted else '(a)'} {coef: 10.6f} ({perc: >6.3f}%)\n" ) # De-excitations if not using TDA if not tda: for h in range(nirrep): h_vir = x.irrep_trans_index ^ h occ_irrep = wfn.molecule().irrep_labels()[h].lower() vir_irrep = wfn.molecule().irrep_labels()[h_vir].lower() for row in range(nocc[h][0]): for col in range(nvir[h_vir][0]): if nirrep == 1: coef = Y.np[row][col] / div else: coef = Y.nph[h][row][col] / div if abs(coef) > coeff_cutoff: perc = 100 * coef**2 core.print_out( f" {row+1: 4}{occ_irrep} {'' if restricted else '(a)'} <-{col+1+nocc[h_vir][0]: 4}{vir_irrep} {'' if restricted else '(a)'} {coef: 10.6f} ({perc: >6.3f}%)\n" ) # Now treat beta orbitals if needed if not restricted: core.print_out("Beta orbitals:\n") if tda: X = x.L_eigvec if restricted else x.L_eigvec[1] Xssq = X.sum_of_squares() core.print_out(f" Sums of squares: Xssq = {Xssq: .6e}\n") else: L = x.L_eigvec if restricted else x.L_eigvec[1] R = x.R_eigvec if restricted else x.R_eigvec[1] X = L.clone() X.add(R) X.scale(0.5) Y = R.clone() Y.subtract(L) Y.scale(0.5) Xssq = X.sum_of_squares() Yssq = Y.sum_of_squares() core.print_out( f" Sums of squares: Xssq = {Xssq: .6e}; Yssq = {Yssq: .6e}; Xssq - Yssq = {Xssq-Yssq: .6e}\n" ) nocc = wfn.epsilon_b_subset("SO", "OCC").shape nvir = wfn.epsilon_b_subset("SO", "VIR").shape # Convert to list of lists for C1 case if nirrep == 1: nocc = (nocc, ) nvir = (nvir, ) # Excitations (beta orbitals) for h in range(nirrep): h_vir = x.irrep_trans_index ^ h occ_irrep = wfn.molecule().irrep_labels()[h].lower() vir_irrep = wfn.molecule().irrep_labels()[h_vir].lower() for row in range(nocc[h][0]): for col in range(nvir[h_vir][0]): if nirrep == 1: coef = X.np[row][col] / div else: coef = X.nph[h][row][col] / div if abs(coef) > coeff_cutoff: perc = 100 * coef**2 core.print_out( f" {row+1: 4}{occ_irrep} (b) ->{col+1+nocc[h_vir][0]: 4}{vir_irrep} (b) {coef: 10.6f} ({perc: >6.3f}%)\n" ) # De-excitations if not using TDA (beta orbitals): if not tda: for h in range(nirrep): h_vir = x.irrep_trans_index ^ h occ_irrep = wfn.molecule().irrep_labels()[h].lower() vir_irrep = wfn.molecule().irrep_labels()[h_vir].lower() for row in range(nocc[h][0]): for col in range(nvir[h_vir][0]): if nirrep == 1: coef = Y.np[row][col] / div else: coef = Y.nph[h][row][col] / div if abs(coef) > coeff_cutoff: perc = 100 * coef**2 core.print_out( f" {row+1: 4}{occ_irrep} (b) <-{col+1+nocc[h_vir][0]: 4}{vir_irrep} (b) {coef: 10.6f} ({perc: >6.3f}%)\n" ) core.print_out("\n")
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