Coverage for strongcoca/response/gpaw.py: 100%

98 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-10-26 18:44 +0000

1# This module contains components adapted from GPAW: 

2# gpaw.tddft.spectrum 

3# https://gitlab.com/gpaw/gpaw/-/blob/aca9ed6f520d6a855013247119b50c630f5eebc9/gpaw/tddft/spectrum.py 

4 

5import re 

6from collections import namedtuple 

7from functools import partial 

8from pathlib import Path 

9from typing import List, Optional, Tuple, Union 

10import numpy as np 

11from ase import Atoms 

12import ase.io 

13from . import TimeDomainResponse 

14from .utilities import Broadening 

15from ..types import Path_str 

16 

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

18 

19 

20def read_gpaw_tddft(dipole_files: Union[Path_str, List[Path_str]], 

21 broadening: Broadening, 

22 atoms_file: Optional[Path_str] = None, 

23 name: str = 'GPAWTDDFT') -> TimeDomainResponse: 

24 """Builds a :class:`~strongcoca.response.TimeDomainResponse` instance 

25 using time-dependent dipole moment data from GPAW real-time 

26 time-dependent density functional theory (RT-TDDFT) calculations. 

27 

28 Parameters 

29 ---------- 

30 dipole_files 

31 Name(s) of file(s) with time-dependent dipole moment data; each file 

32 is assumed to provide a different Cartesian direction; 

33 if there is only one file, the response is assumed to be diagonal. 

34 broadening 

35 Artificial broadening applied to the response. 

36 atoms_file 

37 Name of file from which to read atomic coordinates; needs to be readable 

38 by `ASE <https://ase-lib.org/ase/io/io.html>`_. 

39 name 

40 Name of the response. 

41 

42 Returns 

43 ------- 

44 response 

45 Time domain response. 

46 

47 Raises 

48 ------ 

49 ValueError 

50 If dipole moment files are not suitable or mutually compatible. 

51 """ 

52 atol_time = 1e-6 

53 allclose_time = partial(np.allclose, rtol=0, atol=atol_time) 

54 

55 if isinstance(dipole_files, (Path, str)): 

56 dipole_files = [dipole_files] 

57 elif len(dipole_files) != 3: 

58 raise ValueError('Three dipole files expected') 

59 

60 # Read files 

61 data_i = [] 

62 Data = namedtuple('Data', ['kickv', 'time_t', 'dm_tv']) 

63 for fpath in dipole_files: 

64 time_t, dm_tv, kick_v = _read_and_clean_dipole_moment_data(fpath) 

65 dm_tv -= dm_tv[0] 

66 # Find kick direction 

67 kickv = np.argmax(np.abs(kick_v)) 

68 for v in range(3): 

69 if v != kickv and not np.isclose(kick_v[v], 0, atol=1e-24): 

70 raise ValueError(f'Kick in {fpath} not aligned in ' 

71 f'Cartesian direction') 

72 data_i.append(Data(kickv, time_t, dm_tv / kick_v[kickv])) 

73 

74 # Check file consistency 

75 time_t = data_i[0].time_t 

76 if not all([allclose_time(d.time_t, time_t) for d in data_i]): 

77 raise ValueError('Inconsistent times in dipole files') 

78 

79 # Assume diagonal response if single dipole file given 

80 if len(data_i) == 1: 

81 data = data_i[0] 

82 dm_t = data.dm_tv[:, data.kickv] 

83 # Build dipole tensor 

84 dm_tvv = np.eye(3)[np.newaxis, :, :] * dm_t[:, np.newaxis, np.newaxis] 

85 else: 

86 # Check that all kick directions are included 

87 kickv_i = [d.kickv for d in data_i] 

88 for v in range(3): 

89 if v not in kickv_i: 

90 raise ValueError(f'Kick direction {"xyz"[v]} missing') 

91 

92 # Build dipole tensor 

93 dm_tvv = np.zeros((len(time_t), 3, 3)) 

94 for data in data_i: 

95 dm_tvv[:, data.kickv, :] = data.dm_tv 

96 

97 # Read atoms 

98 if atoms_file is not None: 

99 atoms = ase.io.read(atoms_file) 

100 assert isinstance(atoms, Atoms) 

101 else: 

102 atoms = None 

103 

104 # Initialize response object 

105 response = TimeDomainResponse(time_t, dm_tvv, broadening=broadening, 

106 name=name, units='au') 

107 response._atoms = atoms 

108 return response 

109 

110 

111def _read_and_clean_dipole_moment_data(filepath: Path_str, atol_time: float = 1e-6) \ 

112 -> Tuple[np.ndarray, np.ndarray, np.ndarray]: 

113 """Reads and cleans a time-dependent dipole moment file from a real-time 

114 time-dependent density functional theory calculation. 

115 

116 Parameters 

117 ---------- 

118 filepath 

119 File path. 

120 atol_time 

121 Absolute tolerance for comparing time values. 

122 

123 Returns 

124 ------- 

125 time_t 

126 Time values with time zero set to the kick time; shape {t}. 

127 dm_tv 

128 Dipole moment values, data epsilon before kick discarded; shape {t}x{3}. 

129 kick_v 

130 Kick strength; shape {3}. 

131 

132 Raises 

133 ------ 

134 ValueError 

135 If dipole moment file contains multiple kicks or 

136 if dipole moment file has varying time step. 

137 """ 

138 kick_i, time_t, _, dm_tv = _read_dipole_moment_file(filepath) 

139 

140 # Check kick 

141 if len(kick_i) > 1: 

142 raise ValueError(f'Multiple kicks in {filepath}') 

143 kick = kick_i[0] 

144 

145 # Set time zero to the kick time and discard data before kick 

146 time_t -= kick.time 

147 flt_t = time_t > 0 - atol_time 

148 time_t = time_t[flt_t] 

149 dm_tv = dm_tv[flt_t] 

150 

151 # Check time step 

152 dt_t = time_t[1:] - time_t[:-1] 

153 dt = dt_t[0] 

154 if not np.allclose(dt_t, dt, rtol=0, atol=atol_time): 

155 raise ValueError('Varying time step') 

156 

157 return time_t, dm_tv, kick.strength_v 

158 

159 

160def _read_dipole_moment_file(filepath: Path_str, remove_duplicates: bool = True) \ 

161 -> Tuple[List[Kick], np.ndarray, np.ndarray, np.ndarray]: 

162 """Reads a time-dependent dipole moment file generated by a real-time 

163 time-dependent density functional theory calculation. 

164 

165 Parameters 

166 ---------- 

167 filepath 

168 File path. 

169 remove_duplicates 

170 If true, data for overlapping time instances is removed; 

171 the data read first is kept. 

172 

173 Returns 

174 ------- 

175 kick_i 

176 List of kicks. 

177 time_t 

178 Time values; shape {t}. 

179 norm_t 

180 Norm values; shape {t}. 

181 dm_tv 

182 Dipole moment values; shape {t}x{3}. 

183 

184 Raises 

185 ------ 

186 ValueError 

187 If kick lines cannot be read. 

188 """ 

189 # Search and read kicks 

190 kick_re = re.compile(r'Kick = \[' 

191 r'(?P<k0>[-+0-9\.e\ ]+), ' 

192 r'(?P<k1>[-+0-9\.e\ ]+), ' 

193 r'(?P<k2>[-+0-9\.e\ ]+)\]') 

194 time_re = re.compile(r'Time = (?P<time>[-+0-9\.e\ ]+)') 

195 kick_i = [] 

196 with open(filepath, 'r') as f: 

197 for line in f: 

198 if line.startswith('# Kick'): 

199 # Kick strength 

200 m = kick_re.search(line) 

201 if m is None: 

202 raise ValueError('Kick line unreadable') 

203 strength_v = np.array([float(m.group(f'k{v}')) for v in range(3)]) 

204 # Kick time 

205 m = time_re.search(line) 

206 if m is None: 

207 time = 0.0 

208 else: 

209 time = float(m.group('time')) 

210 kick_i.append(Kick(time, strength_v)) 

211 

212 # Read data 

213 time_t, data_ti = _read_td_file(filepath, remove_duplicates) 

214 norm_t = data_ti[:, 0] 

215 dm_tv = data_ti[:, 1:] 

216 return kick_i, time_t, norm_t, dm_tv 

217 

218 

219def _read_td_file(filepath: Path_str, remove_duplicates: bool = True) \ 

220 -> Tuple[np.ndarray, np.ndarray]: 

221 """Reads a time data series from file. 

222 

223 The data file is assumed to be a plain-text file with the first column 

224 being the time values and the remaining columns the data values. 

225 

226 Parameters 

227 ---------- 

228 filepath 

229 File path. 

230 remove_duplicates 

231 If true, data for overlapping time instances is removed; 

232 the data read first is kept. 

233 

234 Returns 

235 ------- 

236 time_t 

237 Time values; shape `{t}`. 

238 data_ti 

239 Data values; shape {t}x{the number of data columns}. 

240 """ 

241 # Read data 

242 data_tj = np.loadtxt(filepath) 

243 time_t = data_tj[:, 0] 

244 data_ti = data_tj[:, 1:] 

245 

246 # Remove duplicates due to abruptly stopped and restarted calculation 

247 if remove_duplicates: 

248 flt_t = np.ones_like(time_t, dtype=bool) 

249 maxtime = time_t[0] 

250 for t in range(1, len(time_t)): 

251 if time_t[t] > maxtime: 

252 maxtime = time_t[t] 

253 else: 

254 flt_t[t] = False 

255 time_t = time_t[flt_t] 

256 data_ti = data_ti[flt_t] 

257 return time_t, data_ti