Source code for gpaw.tddft.spectrum

import re
import numpy as np

from gpaw import __version__ as version
from gpaw.mpi import world
from gpaw.tddft.units import au_to_as, au_to_fs, au_to_eV, rot_au_to_cgs
from gpaw.tddft.folding import FoldedFrequencies
from gpaw.tddft.folding import Folding


def calculate_fourier_transform(x_t, y_ti, foldedfrequencies):
    ff = foldedfrequencies
    X_w = ff.frequencies
    envelope = ff.folding.envelope

    # Construct integration weights:
    # We use trapezoidal rule except the end point is accounted with
    # full weight. This ensures better numerical compatibility
    # with the on-the-fly Fourier integrators.
    # This shouldn't affect in usual scenarios as the envelope
    # should damp the data to zero at the end point in any case.
    dx_t1 = x_t[1:] - x_t[:-1]
    dx_t = 0.5 * (np.insert(dx_t1, 0, 0.0) + np.append(dx_t1, dx_t1[-1]))

    # Integrate
    f_wt = np.exp(1.0j * np.outer(X_w, x_t))
    y_it = np.swapaxes(y_ti, 0, 1)
    Y_wi = np.tensordot(f_wt, dx_t * envelope(x_t) * y_it, axes=(1, 1))
    return Y_wi


def read_td_file_data(fname, remove_duplicates=True):
    """Read data from time-dependent data file.

    Parameters
    ----------
    fname
        File path
    remove_duplicates
        If true, remove data from overlapping time values.
        The first encountered values are kept.

    Returns
    -------
    time_t
        Array of time values
    data_ti
        Array of data values
    """
    # Read data
    data_tj = np.loadtxt(fname)
    time_t = data_tj[:, 0]
    data_ti = data_tj[:, 1:]

    # Remove duplicates due to abruptly stopped and restarted calculation
    if remove_duplicates:
        flt_t = np.ones_like(time_t, dtype=bool)
        maxtime = time_t[0]
        for t in range(1, len(time_t)):
            # Note about ">=" here:
            # The equality is included here in order to
            # retain step-like data (for example, the data both just before
            # and just after the kick is kept).
            if time_t[t] >= maxtime:
                maxtime = time_t[t]
            else:
                flt_t[t] = False
        time_t = time_t[flt_t]
        data_ti = data_ti[flt_t]
        ndup = len(flt_t) - flt_t.sum()
        if ndup > 0:
            print('Removed %d duplicates' % ndup)
    return time_t, data_ti


def read_td_file_kicks(fname):
    """Read kicks from time-dependent data file.

    Parameters
    ----------
    fname
        File path

    Returns
    -------
    kick_i
        List of kicks.
        Each kick is a dictionary with keys
        ``strength_v`` and ``time``.
    """
    def parse_kick_line(line):
        # Kick
        regexp = (r"Kick = \["
                  r"(?P<k0>[-+0-9\.e\ ]+), "
                  r"(?P<k1>[-+0-9\.e\ ]+), "
                  r"(?P<k2>[-+0-9\.e\ ]+)\]")
        m = re.search(regexp, line)
        assert m is not None, 'Kick not found'
        kick_v = np.array([float(m.group('k%d' % v)) for v in range(3)])
        # Time
        regexp = r"Time = (?P<time>[-+0-9\.e\ ]+)"
        m = re.search(regexp, line)
        if m is None:
            print('time not found')
            time = 0.0
        else:
            time = float(m.group('time'))
        return kick_v, time

    # Search kicks
    kick_i = []
    with open(fname) as f:
        for line in f:
            if line.startswith('# Kick'):
                kick_v, time = parse_kick_line(line)
                kick_i.append({'strength_v': kick_v, 'time': time})
    return kick_i


def clean_td_data(kick_i, time_t, data_ti):
    """Prune time-dependent data.

    This function checks that there is only one kick
    in the kick list and moves time zero to the kick
    time (discarding all preceding data).

    Parameters
    ----------
    kick_i
        List of kicks.
    time_t
        Array of time values
    data_ti
        Array of data values

    Returns
    -------
    kick_i
        List of kicks.
        Each kick is a dictionary with keys
        ``strength_v`` and ``time``.

    Raises
    ------
    RuntimeError
        If kick list contains multiple kicks.
    """
    # Check kicks
    if len(kick_i) > 1:
        raise RuntimeError('Multiple kicks')
    kick = kick_i[0]
    kick_v = kick['strength_v']
    kick_time = kick['time']

    # Discard times before kick
    flt_t = time_t >= kick_time
    time_t = time_t[flt_t]
    data_ti = data_ti[flt_t]

    # Move time zero to kick time
    time_t -= kick_time
    assert time_t[0] == 0.0

    return kick_v, time_t, data_ti


def read_dipole_moment_file(fname, remove_duplicates=True):
    """Read time-dependent dipole moment data file.

    Parameters
    ----------
    fname
        File path
    remove_duplicates
        If true, remove data from overlapping time values.
        The first encountered values are kept.

    Returns
    -------
    kick_i
        List of kicks.
        Each kick is a dictionary with keys
        ``strength_v`` and ``time``.
    time_t
        Array of time values
    norm_t
        Array of norm values
    dm_tv
        Array of dipole moment values
    """
    time_t, data_ti = read_td_file_data(fname, remove_duplicates)
    kick_i = read_td_file_kicks(fname)
    norm_t = data_ti[:, 0]
    dm_tv = data_ti[:, 1:]
    return kick_i, time_t, norm_t, dm_tv


def calculate_polarizability(kick_v, time_t, dm_tv, foldedfrequencies):
    dm_tv = dm_tv - dm_tv[0]
    alpha_wv = calculate_fourier_transform(time_t, dm_tv, foldedfrequencies)
    kick_magnitude = np.sqrt(np.sum(kick_v**2))
    alpha_wv /= kick_magnitude
    return alpha_wv


def calculate_photoabsorption(kick_v, time_t, dm_tv, foldedfrequencies):
    omega_w = foldedfrequencies.frequencies
    alpha_wv = calculate_polarizability(kick_v, time_t, dm_tv,
                                        foldedfrequencies)
    abs_wv = 2 / np.pi * omega_w[:, np.newaxis] * alpha_wv.imag

    kick_magnitude = np.sqrt(np.sum(kick_v**2))
    abs_wv *= kick_v / kick_magnitude
    return abs_wv


def read_magnetic_moment_file(fname, remove_duplicates=True):
    """Read time-dependent magnetic moment data file.

    Parameters
    ----------
    fname
        File path
    remove_duplicates
        If true, remove data from overlapping time values.
        The first encountered values are kept.

    Returns
    -------
    kick_i
        List of kicks.
        Each kick is a dictionary with keys
        ``strength_v`` and ``time``.
    time_t
        Array of time values
    mm_tv
        Array of magnetic moment values
    """
    time_t, mm_tv = read_td_file_data(fname, remove_duplicates)
    kick_i = read_td_file_kicks(fname)
    return kick_i, time_t, mm_tv


def calculate_rotatory_strength_components(kick_v, time_t, mm_tv,
                                           foldedfrequencies):
    assert np.all(mm_tv[0] == 0.0)
    mm_wv = calculate_fourier_transform(time_t, mm_tv, foldedfrequencies)
    kick_magnitude = np.sqrt(np.sum(kick_v**2))
    rot_wv = mm_wv.real / (np.pi * kick_magnitude)
    return rot_wv


def write_spectrum(dipole_moment_file, spectrum_file,
                   folding, width, e_min, e_max, delta_e,
                   title, symbol, calculate):
    def str_list(v_i, fmt='%g'):
        return '[%s]' % ', '.join(map(lambda v: fmt % v, v_i))

    kick_i, time_t, _, dm_tv = read_dipole_moment_file(dipole_moment_file)
    kick_v, time_t, dm_tv = clean_td_data(kick_i, time_t, dm_tv)
    dt_t = time_t[1:] - time_t[:-1]

    freqs = np.arange(e_min, e_max + 0.5 * delta_e, delta_e)
    folding = Folding(folding, width)
    ff = FoldedFrequencies(freqs, folding)
    omega_w = ff.frequencies
    spec_wv = calculate(kick_v, time_t, dm_tv, ff)

    # Write spectrum file header
    with open(spectrum_file, 'w') as f:
        def w(s):
            f.write('%s\n' % s)

        w('# %s spectrum from real-time propagation' % title)
        w('# GPAW version: %s' % version)
        w('# Total time = %.4f fs, Time steps = %s as' %
          (dt_t.sum() * au_to_fs,
           str_list(np.unique(np.around(dt_t, 6)) * au_to_as, '%.4f')))
        w('# Kick = %s' % str_list(kick_v))
        w('# %sian folding, Width = %.4f eV = %lf Hartree'
          ' <=> FWHM = %lf eV' %
          (folding.folding, folding.width * au_to_eV, folding.width,
           folding.fwhm * au_to_eV))

        col_i = []
        data_iw = [omega_w * au_to_eV]
        for v in range(len(kick_v)):
            h = '{}_{}'.format(symbol, 'xyz'[v])
            if spec_wv.dtype == complex:
                col_i.append('Re[%s]' % h)
                data_iw.append(spec_wv[:, v].real)
                col_i.append('Im[%s]' % h)
                data_iw.append(spec_wv[:, v].imag)
            else:
                col_i.append(h)
                data_iw.append(spec_wv[:, v])

        w('# %10s %s' % ('om (eV)', ' '.join(['%20s' % s for s in col_i])))

    # Write spectrum file data
    with open(spectrum_file, 'ab') as f:
        np.savetxt(f, np.array(data_iw).T,
                   fmt='%12.6lf' + (' %20.10le' * len(col_i)))

    return folding.envelope(time_t[-1])


[docs]def photoabsorption_spectrum(dipole_moment_file: str, spectrum_file: str, folding: str = 'Gauss', width: float = 0.2123, e_min: float = 0.0, e_max: float = 30.0, delta_e: float = 0.05): """Calculates photoabsorption spectrum from the time-dependent dipole moment. The spectrum is represented as a dipole strength function in units of 1/eV. Thus, the resulting spectrum should integrate to the number of valence electrons in the system. Parameters ---------- dipole_moment_file Name of the time-dependent dipole moment file from which the spectrum is calculated spectrum_file Name of the spectrum file folding Gaussian (``'Gauss'``) or Lorentzian (``'Lorentz'``) folding width Width of the Gaussian (sigma) or Lorentzian (Gamma) Gaussian = 1/(sigma sqrt(2pi)) exp(-(1/2)(omega/sigma)^2) Lorentzian = (1/pi) (1/2) Gamma / [omega^2 + ((1/2) Gamma)^2] e_min Minimum energy shown in the spectrum (eV) e_max Maximum energy shown in the spectrum (eV) delta_e Energy resolution (eV) """ if world.rank == 0: print('Calculating photoabsorption spectrum from file "%s"' % dipole_moment_file) def calculate(*args): return calculate_photoabsorption(*args) / au_to_eV sinc = write_spectrum(dipole_moment_file, spectrum_file, folding, width, e_min, e_max, delta_e, 'Photoabsorption', 'S', calculate) print('Sinc contamination %.8f' % sinc) print('Calculated photoabsorption spectrum saved to file "%s"' % spectrum_file)
def polarizability_spectrum(dipole_moment_file, spectrum_file, folding='Gauss', width=0.2123, e_min=0.0, e_max=30.0, delta_e=0.05): """Calculates polarizability spectrum from the time-dependent dipole moment. Parameters: dipole_moment_file: string Name of the time-dependent dipole moment file from which the spectrum is calculated spectrum_file: string Name of the spectrum file folding: 'Gauss' or 'Lorentz' Whether to use Gaussian or Lorentzian folding width: float Width of the Gaussian (sigma) or Lorentzian (Gamma) Gaussian = 1/(sigma sqrt(2pi)) exp(-(1/2)(omega/sigma)^2) Lorentzian = (1/pi) (1/2) Gamma / [omega^2 + ((1/2) Gamma)^2] e_min: float Minimum energy shown in the spectrum (eV) e_max: float Maximum energy shown in the spectrum (eV) delta_e: float Energy resolution (eV) """ if world.rank == 0: print('Calculating polarizability spectrum from file "%s"' % dipole_moment_file) def calculate(*args): return calculate_polarizability(*args) / au_to_eV**2 sinc = write_spectrum(dipole_moment_file, spectrum_file, folding, width, e_min, e_max, delta_e, 'Polarizability', 'alpha', calculate) print('Sinc contamination %.8f' % sinc) print('Calculated polarizability spectrum saved to file "%s"' % spectrum_file) def rotatory_strength_spectrum(magnetic_moment_files, spectrum_file, folding='Gauss', width=0.2123, e_min=0.0, e_max=30.0, delta_e=0.05): """Calculates rotatory strength spectrum from the time-dependent magnetic moment. Parameters ---------- magnetic_moment_files: list of string Time-dependent magnetic moment files for x, y, and z kicks spectrum_file: string Name of the spectrum file folding: 'Gauss' or 'Lorentz' Whether to use Gaussian or Lorentzian folding width: float Width of the Gaussian (sigma) or Lorentzian (Gamma) Gaussian = 1/(sigma sqrt(2pi)) exp(-(1/2)(omega/sigma)^2) Lorentzian = (1/pi) (1/2) Gamma / [omega^2 + ((1/2) Gamma)^2] e_min: float Minimum energy shown in the spectrum (eV) e_max: float Maximum energy shown in the spectrum (eV) delta_e: float Energy resolution (eV) """ if world.rank != 0: return freqs = np.arange(e_min, e_max + 0.5 * delta_e, delta_e) folding = Folding(folding, width) ff = FoldedFrequencies(freqs, folding) omega_w = ff.frequencies * au_to_eV rot_w = np.zeros_like(omega_w) tot_time = np.inf time_steps = [] kick_strength = None for v, fpath in enumerate(magnetic_moment_files): kick_i, time_t, mm_tv = read_magnetic_moment_file(fpath) kick_v, time_t, mm_tv = clean_td_data(kick_i, time_t, mm_tv) tot_time = min(tot_time, time_t[-1]) time_steps.append(np.around(time_t[1:] - time_t[:-1], 6)) # Check kicks for v0 in range(3): if v0 == v: continue if kick_v[v0] != 0.0: raise RuntimeError('The magnetic moment files must be ' 'for kicks in x, y, and z directions.') if kick_strength is None: kick_strength = np.sqrt(np.sum(kick_v**2)) if np.sqrt(np.sum(kick_v**2)) != kick_strength: raise RuntimeError('The magnetic moment files must have ' 'been calculated with the same kick strength.') rot_wv = calculate_rotatory_strength_components(kick_v, time_t, mm_tv, ff) rot_w += rot_wv[:, v] rot_w *= rot_au_to_cgs * 1e40 / au_to_eV # Unique non-zero time steps time_steps = np.unique(time_steps) time_steps = time_steps[time_steps != 0] with open(spectrum_file, 'w') as fd: steps_str = ', '.join(f'{val:.4f}' for val in time_steps * au_to_as) lines = ['Rotatory strength spectrum from real-time propagations', f'Total time = {tot_time * au_to_fs:.4f} fs, ' f'Time steps = [{steps_str}] as', f'Kick strength = {kick_strength}', f'{folding.folding}ian folding, ' f'width = {folding.width * au_to_eV:.4f} eV ' f'<=> FWHM = {folding.fwhm * au_to_eV:.4f} eV'] fd.write('# ' + '\n# '.join(lines) + '\n') fd.write(f'# {"Energy (eV)":>12} {"R (1e-40 cgs / eV)":>20}\n') data_wi = np.vstack((omega_w, rot_w)).T np.savetxt(fd, data_wi, fmt='%14.6lf' + (' %20.10le' * (data_wi.shape[1] - 1)))