Source code for strongcoca.response.gpaw

# This module contains components adapted from GPAW:
# gpaw.tddft.spectrum
# https://gitlab.com/gpaw/gpaw/-/blob/aca9ed6f520d6a855013247119b50c630f5eebc9/gpaw/tddft/spectrum.py

import re
from collections import namedtuple
from functools import partial
from pathlib import Path
from typing import List, Optional, Tuple, Union
import numpy as np
from ase import Atoms
import ase.io
from . import TimeDomainResponse
from .utilities import Broadening
from ..types import Path_str

Kick = namedtuple('Kick', ['time', 'strength_v'])


[docs] def read_gpaw_tddft(dipole_files: Union[Path_str, List[Path_str]], broadening: Broadening, atoms_file: Optional[Path_str] = None, name: str = 'GPAWTDDFT') -> TimeDomainResponse: """Builds a :class:`~strongcoca.response.TimeDomainResponse` instance using time-dependent dipole moment data from GPAW real-time time-dependent density functional theory (RT-TDDFT) calculations. Parameters ---------- dipole_files Name(s) of file(s) with time-dependent dipole moment data; each file is assumed to provide a different Cartesian direction; if there is only one file, the response is assumed to be diagonal. broadening Artificial broadening applied to the response. atoms_file Name of file from which to read atomic coordinates; needs to be readable by `ASE <https://ase-lib.org/ase/io/io.html>`_. name Name of the response. Returns ------- response Time domain response. Raises ------ ValueError If dipole moment files are not suitable or mutually compatible. """ atol_time = 1e-6 allclose_time = partial(np.allclose, rtol=0, atol=atol_time) if isinstance(dipole_files, (Path, str)): dipole_files = [dipole_files] elif len(dipole_files) != 3: raise ValueError('Three dipole files expected') # Read files data_i = [] Data = namedtuple('Data', ['kickv', 'time_t', 'dm_tv']) for fpath in dipole_files: time_t, dm_tv, kick_v = _read_and_clean_dipole_moment_data(fpath) dm_tv -= dm_tv[0] # Find kick direction kickv = np.argmax(np.abs(kick_v)) for v in range(3): if v != kickv and not np.isclose(kick_v[v], 0, atol=1e-24): raise ValueError(f'Kick in {fpath} not aligned in ' f'Cartesian direction') data_i.append(Data(kickv, time_t, dm_tv / kick_v[kickv])) # Check file consistency time_t = data_i[0].time_t if not all([allclose_time(d.time_t, time_t) for d in data_i]): raise ValueError('Inconsistent times in dipole files') # Assume diagonal response if single dipole file given if len(data_i) == 1: data = data_i[0] dm_t = data.dm_tv[:, data.kickv] # Build dipole tensor dm_tvv = np.eye(3)[np.newaxis, :, :] * dm_t[:, np.newaxis, np.newaxis] else: # Check that all kick directions are included kickv_i = [d.kickv for d in data_i] for v in range(3): if v not in kickv_i: raise ValueError(f'Kick direction {"xyz"[v]} missing') # Build dipole tensor dm_tvv = np.zeros((len(time_t), 3, 3)) for data in data_i: dm_tvv[:, data.kickv, :] = data.dm_tv # Read atoms if atoms_file is not None: atoms = ase.io.read(atoms_file) assert isinstance(atoms, Atoms) else: atoms = None # Initialize response object response = TimeDomainResponse(time_t, dm_tvv, broadening=broadening, name=name, units='au') response._atoms = atoms return response
def _read_and_clean_dipole_moment_data(filepath: Path_str, atol_time: float = 1e-6) \ -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """Reads and cleans a time-dependent dipole moment file from a real-time time-dependent density functional theory calculation. Parameters ---------- filepath File path. atol_time Absolute tolerance for comparing time values. Returns ------- time_t Time values with time zero set to the kick time; shape {t}. dm_tv Dipole moment values, data epsilon before kick discarded; shape {t}x{3}. kick_v Kick strength; shape {3}. Raises ------ ValueError If dipole moment file contains multiple kicks or if dipole moment file has varying time step. """ kick_i, time_t, _, dm_tv = _read_dipole_moment_file(filepath) # Check kick if len(kick_i) > 1: raise ValueError(f'Multiple kicks in {filepath}') kick = kick_i[0] # Set time zero to the kick time and discard data before kick time_t -= kick.time flt_t = time_t > 0 - atol_time time_t = time_t[flt_t] dm_tv = dm_tv[flt_t] # Check time step dt_t = time_t[1:] - time_t[:-1] dt = dt_t[0] if not np.allclose(dt_t, dt, rtol=0, atol=atol_time): raise ValueError('Varying time step') return time_t, dm_tv, kick.strength_v def _read_dipole_moment_file(filepath: Path_str, remove_duplicates: bool = True) \ -> Tuple[List[Kick], np.ndarray, np.ndarray, np.ndarray]: """Reads a time-dependent dipole moment file generated by a real-time time-dependent density functional theory calculation. Parameters ---------- filepath File path. remove_duplicates If true, data for overlapping time instances is removed; the data read first is kept. Returns ------- kick_i List of kicks. time_t Time values; shape {t}. norm_t Norm values; shape {t}. dm_tv Dipole moment values; shape {t}x{3}. Raises ------ ValueError If kick lines cannot be read. """ # Search and read kicks kick_re = re.compile(r'Kick = \[' r'(?P<k0>[-+0-9\.e\ ]+), ' r'(?P<k1>[-+0-9\.e\ ]+), ' r'(?P<k2>[-+0-9\.e\ ]+)\]') time_re = re.compile(r'Time = (?P<time>[-+0-9\.e\ ]+)') kick_i = [] with open(filepath, 'r') as f: for line in f: if line.startswith('# Kick'): # Kick strength m = kick_re.search(line) if m is None: raise ValueError('Kick line unreadable') strength_v = np.array([float(m.group(f'k{v}')) for v in range(3)]) # Kick time m = time_re.search(line) if m is None: time = 0.0 else: time = float(m.group('time')) kick_i.append(Kick(time, strength_v)) # Read data time_t, data_ti = _read_td_file(filepath, remove_duplicates) norm_t = data_ti[:, 0] dm_tv = data_ti[:, 1:] return kick_i, time_t, norm_t, dm_tv def _read_td_file(filepath: Path_str, remove_duplicates: bool = True) \ -> Tuple[np.ndarray, np.ndarray]: """Reads a time data series from file. The data file is assumed to be a plain-text file with the first column being the time values and the remaining columns the data values. Parameters ---------- filepath File path. remove_duplicates If true, data for overlapping time instances is removed; the data read first is kept. Returns ------- time_t Time values; shape `{t}`. data_ti Data values; shape {t}x{the number of data columns}. """ # Read data data_tj = np.loadtxt(filepath) 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)): 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] return time_t, data_ti