Coverage for strongcoca/response/nwchem.py: 99%

178 statements  

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

1import logging 

2import os 

3from collections import OrderedDict 

4from typing import Any, Dict, IO, List, Union 

5 

6import numpy as np 

7from ase import Atom, Atoms 

8from ase.calculators.singlepoint import (SinglePointDFTCalculator, 

9 SinglePointKPoint) 

10 

11from ..units import eV_to_au, au_to_eV, au_to_eVA 

12from ..types import Path_str, Vector 

13from .casida import CasidaResponse 

14from .utilities import Broadening, NoArtificialBroadening 

15 

16 

17logger = logging.getLogger(__name__) 

18 

19 

20def read_nwchem_casida(filename: Path_str, 

21 broadening: Broadening = NoArtificialBroadening(), 

22 name: str = 'NWChem Casida Response') -> CasidaResponse: 

23 """Returns a freshly initialized :class:`~strongcoca.response.CasidaResponse` object 

24 using an NWChem output file to set up the components of the Casida representation. 

25 

26 Parameters 

27 ---------- 

28 filename 

29 Name of NWChem output file; can be compressed. 

30 broadening 

31 Artificial broadening used for the continuous response; 

32 defaults to no broadening. 

33 name 

34 Name of subsystem. 

35 """ 

36 logger.debug(f'Entering read_nwchem_casida: filename= {filename}') 

37 try: 

38 atoms = _read_nwchem_logfile(filename) 

39 except Exception as e: 

40 raise Exception(f'Failed to parse NWChem output file {filename}\n{str(e)}') 

41 if 'excited_states' not in atoms.info or len(atoms.info['excited_states']) is False: 

42 raise ValueError(f'NWChem output file {filename} does' 

43 ' not contain excited states information') 

44 

45 # include only singlet excitations 

46 excited_states = atoms.info['excited_states'] 

47 excited_states = [e for e in excited_states if e['type'] == 'singlet'] 

48 n = len(excited_states) 

49 

50 D_n = np.zeros(n) 

51 K_nn = np.zeros((n, n)) 

52 mu_nv = np.zeros((n, 3)) 

53 

54 for i, state in enumerate(excited_states): 

55 D_n[i] = state['energy'] * eV_to_au 

56 mu_nv[i, :] = state['dipole_moment'] 

57 

58 response = CasidaResponse(D_n, K_nn, mu_nv, broadening=broadening, 

59 name=name, units='au') 

60 response._atoms = atoms 

61 response._pbc = atoms.pbc 

62 return response 

63 

64 

65def _read_nwchem_logfile(filename: Path_str) -> Atoms: 

66 """This function parses the log file from a NWChem calculation and stores the 

67 data as an :class`ASE Atoms <~ase.Atoms>` object. Each element of the list 

68 corresponds to one ionic step. Additional information pertaining e.g., to 

69 eigenvalues or excitations are stored in the 'info' dictionary of the ASE 

70 atoms class. 

71 

72 Parameters 

73 ---------- 

74 filename 

75 Input file name. 

76 

77 Returns 

78 ------- 

79 Atomic configuration with associated information. 

80 """ 

81 import gzip 

82 # GzipFile doesn't appear as IO type. If it does at some point, 

83 # it should suffice to do just 

84 # fd: IO 

85 fd: Union[IO, gzip.GzipFile] 

86 

87 filename = os.path.expanduser(filename) 

88 if filename.endswith('.gz'): 

89 fd = gzip.open(filename) 

90 elif filename.endswith('.bz2'): 

91 import bz2 

92 fd = bz2.open(filename, 'rb') 

93 else: 

94 fd = open(filename, 'rb') 

95 data = [row.decode() if isinstance(row, bytes) else row for row in fd] 

96 

97 atoms = Atoms(pbc=False) 

98 energy: float = np.nan 

99 forces: List[Vector] = [] 

100 eKS_energies: List[float] = [] 

101 eKS_occupations: List[float] = [] 

102 parameters: Dict[str, Any] = OrderedDict() 

103 

104 for n, line in enumerate(data): 

105 

106 if 'echo of input deck' in line: 

107 input_deck = [] 

108 k = 1 

109 while '=====================' not in data[n+k]: 

110 line = data[n+k].strip() 

111 k += 1 

112 if len(line) == 0: 

113 continue 

114 input_deck.append(line) 

115 parameters['input_deck'] = input_deck 

116 

117 # Computational parameters 

118 elif 'XC Information' in line: 

119 parameters['xc'] = data[n+2].strip() 

120 

121 elif 'Charge :' in line: 

122 flds = line.split() 

123 assert len(flds) == 3, \ 

124 f'NWChem parser failed to read charge from line {n+1}\n --> {line}' 

125 parameters['charge'] = float(flds[2]) 

126 

127 elif 'Spin multiplicity:' in line: 

128 flds = line.split() 

129 assert len(flds) == 3, \ 

130 f'NWChem parser failed to read spin multiplicity from line {n+1}\n --> {line}' 

131 parameters['spin_multiplicity'] = int(flds[2]) 

132 

133 # Atomic configuration 

134 elif 'Output coordinates in angstroms' in line: 

135 atoms = Atoms(pbc=False) 

136 k = 4 

137 while len(data[n+k].strip()) > 0: 

138 flds = data[n+k].split() 

139 assert len(flds) == 6, \ 

140 'NWChem parser failed to read atomic number and' \ 

141 f' coordinates from line {n+k+1}\n --> {data[n+k]}' 

142 atom_type = flds[1] 

143 position = np.array([float(flds[3]), float(flds[4]), float(flds[5])]) 

144 atoms.append(Atom(atom_type, position)) 

145 k += 1 

146 

147 # Total energy and initialization of Atoms object 

148 elif 'Total DFT energy' in line: 

149 flds = line.split() 

150 assert len(flds) == 5, \ 

151 f'NWChem parser failed to read energy from line {n+1}\n --> {line}' 

152 energy = float(flds[4]) * au_to_eV 

153 

154 # Forces 

155 elif 'DFT ENERGY GRADIENTS' in line: 

156 k = 4 

157 forces = [] 

158 while len(data[n+k].strip()) > 0: 

159 flds = data[n+k].split() 

160 assert len(flds) == 8, \ 

161 f'NWChem parser failed to read force vector from line {n+k}\n --> {data[n+k]}' 

162 force = np.array([float(flds[5]), float(flds[6]), float(flds[7])]) 

163 force *= au_to_eVA 

164 forces.append(force) 

165 k += 1 

166 

167 # Kohn-Sham eigen energies 

168 elif 'DFT Final Molecular Orbital Analysis' in line: 

169 eKS_energies = [] 

170 eKS_occupations = [] 

171 

172 elif 'Vector' in line and 'Occ' in line: 

173 s = line 

174 s = s.replace('Vector', ' ') 

175 s = s.replace('Occ=', ' ') 

176 s = s.replace('E=', ' ') 

177 s = s.replace('D+', 'E+').replace('D-', 'E-') 

178 flds = s.split() 

179 assert len(flds) >= 3, \ 

180 f'NWChem parser failed to read KS eigenstate from line {n+1}\n --> {line}' 

181 eKS_occupations.append(float(flds[1])) 

182 eKS_energies.append(float(flds[2]) * au_to_eV) 

183 

184 # Excitations 

185 elif 'NWChem TDDFT Module' in line: 

186 maintag = 'excited_states' 

187 atoms.info[maintag] = [] 

188 

189 elif 'Root' in line: 

190 flds = line.replace('=', ' ').split() 

191 assert len(flds) > 5, \ 

192 f'NWChem parser failed to read Casida root from line {n+1}\n --> {line}' 

193 

194 maintag = 'excited_states' 

195 if maintag not in atoms.info: 

196 continue 

197 

198 transition: Dict[str, Any] = {} 

199 atoms.info[maintag].append(transition) 

200 

201 # basic information (energy, type, symmetry, ...) 

202 transition['type'] = flds[2] 

203 transition['symmetry'] = flds[3] 

204 transition['energy'] = float(flds[4]) * au_to_eV 

205 

206 transition['dipole_moment'] = np.zeros(3) 

207 transition['quadrupole_moment'] = np.zeros((3, 3)) 

208 

209 if 'Spin forbidden' in data[n+2]: 

210 continue 

211 

212 # transition moment, dipole 

213 lineno = n + 2 

214 flds = data[lineno].split() 

215 assert len(flds) == 8, \ 

216 'NWChem parser failed to read transition dipole moment' \ 

217 f' from line {lineno+1}\n --> {data[lineno]}' 

218 transition['dipole_moment'][0] = float(flds[3]) 

219 transition['dipole_moment'][1] = float(flds[5]) 

220 transition['dipole_moment'][2] = float(flds[7]) 

221 

222 # transition moment, quadrupole 

223 lineno = n + 3 

224 flds = data[lineno].split() 

225 assert len(flds) == 8, \ 

226 'NWChem parser failed to read transition quadrupole moment' \ 

227 f' from line {lineno+1}\n --> {data[lineno]} (1)' 

228 transition['quadrupole_moment'][0, 0] = float(flds[3]) 

229 transition['quadrupole_moment'][0, 1] = float(flds[5]) 

230 transition['quadrupole_moment'][0, 2] = float(flds[7]) 

231 lineno = n + 4 

232 flds = data[lineno].split() 

233 assert len(flds) == 8, \ 

234 'NWChem parser failed to read transition quadrupole moment' \ 

235 f' from line {lineno+1}\n --> {data[lineno]} (2)' 

236 transition['quadrupole_moment'][1, 1] = float(flds[3]) 

237 transition['quadrupole_moment'][1, 2] = float(flds[5]) 

238 transition['quadrupole_moment'][2, 2] = float(flds[7]) 

239 transition['quadrupole_moment'][1, 0] = transition['quadrupole_moment'][0, 1] 

240 transition['quadrupole_moment'][2, 0] = transition['quadrupole_moment'][0, 2] 

241 transition['quadrupole_moment'][2, 1] = transition['quadrupole_moment'][1, 2] 

242 

243 # Parsing Hessian from 'frequencies' run 

244 elif 'P.Frequency' in line: 

245 maintag = 'normal_modes' 

246 atoms.info[maintag] = {} 

247 offset = 0 

248 if len(atoms.info[maintag].keys()) > 0: 248 ↛ 249line 248 didn't jump to line 249 because the condition on line 248 was never true

249 offset = sorted(atoms.info[maintag].keys())[-1] 

250 

251 flds = line.split() 

252 for m, fld in enumerate(flds[1:]): 

253 mode = offset + m + 1 

254 atoms.info[maintag][mode] = {} 

255 atoms.info[maintag][mode]['frequency'] = float(fld) 

256 atoms.info[maintag][mode]['normal_mode'] = np.zeros((len(atoms), 3)) 

257 

258 for k in range(3 * len(atoms)): 

259 lineno = n + k + 2 

260 line = data[lineno].strip() 

261 flds = line.split() 

262 assert len(flds) > 2, \ 

263 f'NWChem parser failed to read normal mode from line {lineno+1}\n' \ 

264 f' --> {line} (1)' 

265 iat = int(k / 3) 

266 assert iat <= len(atoms), \ 

267 f'NWChem parser failed to read normal mode from line {lineno+1}\n' \ 

268 f' --> {line} (2)' 

269 alph = k % 3 

270 for m, fld in enumerate(flds[1:]): 

271 mode = offset + m + 1 

272 assert mode <= 3 * len(atoms), \ 

273 f'NWChem parser failed to read normal mode from line {lineno+1}\n' \ 

274 f' --> {line} (3)' 

275 atoms.info[maintag][mode]['normal_mode'][iat, alph] = float(fld) 

276 

277 # Compile Atoms object 

278 if energy is np.nan or len(atoms) == 0: 

279 raise ValueError(f'Failed to extract energy and configuration from input file {filename}') 

280 atoms.calc = SinglePointDFTCalculator(atoms, energy=energy, forces=forces) 

281 atoms.calc.name = 'nwchem' 

282 atoms.calc.parameters = parameters 

283 if len(eKS_energies) > 0 and len(eKS_occupations) > 0: 

284 atoms.calc.kpts = [SinglePointKPoint(1, 0, 0, eKS_energies, eKS_occupations)] 

285 

286 return atoms