예제 #1
0
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
예제 #2
0
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
예제 #3
0
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")
예제 #4
0
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
        }
    }
예제 #5
0
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")
예제 #6
0
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
예제 #7
0
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