Пример #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 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
        }
    }