Coverage for dynasor/correlation_functions.py: 100%

285 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-20 20:02 +0000

1import concurrent 

2from functools import partial 

3from itertools import combinations_with_replacement 

4from typing import Optional 

5 

6import numba 

7import numpy as np 

8from ase import Atoms 

9from ase.units import fs 

10from numpy.typing import NDArray 

11 

12from dynasor.logging_tools import logger 

13from dynasor.trajectory import Trajectory, WindowIterator 

14from dynasor.sample import DynamicSample, StaticSample 

15 

16from dynasor.tools.acfs import psd_from_acf_2d 

17from dynasor.core.time_averager import TimeAverager, truncate_nans 

18from dynasor.core.reciprocal import calc_rho_q, calc_rho_j_q 

19from dynasor.tools.structures import get_offset_index 

20from dynasor.units import radians_per_fs_to_meV 

21 

22 

23def compute_dynamic_structure_factors( 

24 traj: Trajectory, 

25 q_points: NDArray[float], 

26 dt: float, 

27 window_size: int, 

28 window_step: Optional[int] = 1, 

29 calculate_currents: Optional[bool] = False, 

30 calculate_incoherent: Optional[bool] = False, 

31 logging_interval: Optional[int] = 1000, 

32) -> DynamicSample: 

33 r"""Compute the dynamic structure factors. The results are returned in the 

34 form of a :class:`DynamicSample <dynasor.sample.DynamicSample>` 

35 object. 

36 

37 Parameters 

38 ---------- 

39 traj 

40 Input trajectory. 

41 q_points 

42 Array of q-points in units of rad/Å with shape ``(N_qpoints, 3)`` in Cartesian coordinates. 

43 dt 

44 Time difference in femtoseconds between two consecutive snapshots 

45 in the trajectory. Note that you should *not* change :attr:`dt` if you change 

46 :attr:`frame_step <dynasor.trajectory.Trajectory.frame_step>` in :attr:`traj`. 

47 window_size 

48 Length of the trajectory frame window to use for time correlation calculation. 

49 It is expressed in terms of the number of time lags to consider 

50 and thus determines the smallest frequency resolved. 

51 window_step 

52 Window step (or stride) given as the number of frames between consecutive trajectory 

53 windows. This parameter does *not* affect the time between consecutive frames in the 

54 calculation. If, e.g., :attr:`window_step` > :attr:`window_size`, some frames will not 

55 be used. 

56 calculate_currents 

57 Calculate the current correlations. Requires velocities to be available in :attr:`traj`. 

58 calculate_incoherent 

59 Calculate the incoherent part (self-part) of :math:`F_\text{incoh}`. 

60 logging_interval 

61 Log progress at ``INFO`` level every this many windows. Set to ``0`` to disable 

62 progress logging. 

63 """ 

64 # sanity check input args 

65 if q_points.shape[1] != 3: 

66 raise ValueError('q-points array has the wrong shape.') 

67 if dt <= 0: 

68 raise ValueError(f'dt must be positive: dt= {dt}') 

69 if window_size <= 2: 

70 raise ValueError(f'window_size must be larger than 2: window_size= {window_size}') 

71 if window_step <= 0: 

72 raise ValueError(f'window_step must be positive: window_step= {window_step}') 

73 if calculate_currents and not traj.has_velocities: 

74 raise ValueError('calculate_currents=True requires velocities to be available in the ' 

75 'trajectory, but traj does not provide velocities.') 

76 

77 # define internal parameters 

78 n_qpoints = q_points.shape[0] 

79 delta_t = traj.frame_step * dt 

80 N_tc = window_size + 1 

81 

82 # log all setup information 

83 dw = np.pi / (window_size * delta_t) 

84 w_max = dw * window_size 

85 w_N = 2 * np.pi / (2 * delta_t) # Nyquist angular frequency 

86 

87 logger.info(f'Spacing between samples (frame_step): {traj.frame_step}') 

88 logger.info(f'Time between consecutive frames in input trajectory (dt): {dt} fs') 

89 logger.info(f'Time between consecutive frames used (dt * frame_step): {delta_t} fs') 

90 logger.info(f'Time window size (dt * frame_step * window_size): {delta_t * window_size:.1f} fs') 

91 logger.info(f'Angular frequency resolution: dw = {dw:.6f} rad/fs = ' 

92 f'{dw * radians_per_fs_to_meV:.3f} meV') 

93 logger.info(f'Maximum angular frequency (dw * window_size):' 

94 f' {w_max:.6f} rad/fs = {w_max * radians_per_fs_to_meV:.3f} meV') 

95 logger.info(f'Nyquist angular frequency (2pi / frame_step / dt / 2):' 

96 f' {w_N:.6f} rad/fs = {w_N * radians_per_fs_to_meV:.3f} meV') 

97 

98 if calculate_currents: 

99 logger.info('Calculating current (velocity) correlations') 

100 if calculate_incoherent: 

101 logger.info('Calculating incoherent part (self-part) of correlations') 

102 

103 # log some info regarding q-points 

104 logger.info(f'Number of q-points: {n_qpoints}') 

105 

106 q_directions = q_points.astype(float, copy=True) 

107 q_distances = np.linalg.norm(q_points, axis=1) 

108 nonzero = q_distances > 0 

109 q_directions[nonzero] /= q_distances[nonzero].reshape(-1, 1) 

110 

111 # setup functions to process frames 

112 def f2_rho(frame): 

113 rho_qs_dict = dict() 

114 for atom_type in frame.positions_by_type.keys(): 

115 x = frame.positions_by_type[atom_type] 

116 rho_qs_dict[atom_type] = calc_rho_q(x, q_points) 

117 frame.rho_qs_dict = rho_qs_dict 

118 return frame 

119 

120 def f2_rho_and_j(frame): 

121 rho_qs_dict = dict() 

122 jz_qs_dict = dict() 

123 jper_qs_dict = dict() 

124 

125 for atom_type in frame.positions_by_type.keys(): 

126 x = frame.positions_by_type[atom_type] 

127 v = frame.velocities_by_type[atom_type] 

128 rho_qs, j_qs = calc_rho_j_q(x, v, q_points) 

129 jz_qs = np.sum(j_qs * q_directions, axis=1) 

130 jper_qs = j_qs - (jz_qs[:, None] * q_directions) 

131 

132 rho_qs_dict[atom_type] = rho_qs 

133 jz_qs_dict[atom_type] = jz_qs 

134 jper_qs_dict[atom_type] = jper_qs 

135 

136 frame.rho_qs_dict = rho_qs_dict 

137 frame.jz_qs_dict = jz_qs_dict 

138 frame.jper_qs_dict = jper_qs_dict 

139 return frame 

140 

141 if calculate_currents: 

142 element_processor = f2_rho_and_j 

143 else: 

144 element_processor = f2_rho 

145 

146 # setup window iterator 

147 window_iterator = WindowIterator(traj, width=N_tc, window_step=window_step, 

148 element_processor=element_processor) 

149 

150 # define all pairs 

151 pairs = list(combinations_with_replacement(traj.atom_types, r=2)) 

152 particle_counts = {key: len(val) for key, val in traj.atomic_indices.items()} 

153 logger.debug('Considering pairs:') 

154 for pair in pairs: 

155 logger.debug(f' {pair}') 

156 

157 # set up all time averager instances 

158 F_q_t_averager = dict() 

159 for pair in pairs: 

160 F_q_t_averager[pair] = TimeAverager(N_tc, n_qpoints) 

161 if calculate_currents: 

162 Cl_q_t_averager = dict() 

163 Ct_q_t_averager = dict() 

164 for pair in pairs: 

165 Cl_q_t_averager[pair] = TimeAverager(N_tc, n_qpoints) 

166 Ct_q_t_averager[pair] = TimeAverager(N_tc, n_qpoints) 

167 if calculate_incoherent: 

168 F_s_q_t_averager = dict() 

169 for pair in traj.atom_types: 

170 F_s_q_t_averager[pair] = TimeAverager(N_tc, n_qpoints) 

171 

172 # define correlation function 

173 def calc_corr(window, time_i): 

174 # Calculate correlations between two frames in the window without normalization 1/N 

175 f0 = window[0] 

176 fi = window[time_i] 

177 for s1, s2 in pairs: 

178 Fqt = np.real(f0.rho_qs_dict[s1] * fi.rho_qs_dict[s2].conjugate()) 

179 if s1 != s2: 

180 Fqt += np.real(f0.rho_qs_dict[s2] * fi.rho_qs_dict[s1].conjugate()) 

181 F_q_t_averager[(s1, s2)].add_sample(time_i, Fqt) 

182 

183 if calculate_currents: 

184 for s1, s2 in pairs: 

185 Clqt = np.real(f0.jz_qs_dict[s1] * fi.jz_qs_dict[s2].conjugate()) 

186 Ctqt = 0.5 * np.real(np.sum(f0.jper_qs_dict[s1] * 

187 fi.jper_qs_dict[s2].conjugate(), axis=1)) 

188 if s1 != s2: 

189 Clqt += np.real(f0.jz_qs_dict[s2] * fi.jz_qs_dict[s1].conjugate()) 

190 Ctqt += 0.5 * np.real(np.sum(f0.jper_qs_dict[s2] * 

191 fi.jper_qs_dict[s1].conjugate(), axis=1)) 

192 

193 Cl_q_t_averager[(s1, s2)].add_sample(time_i, Clqt) 

194 Ct_q_t_averager[(s1, s2)].add_sample(time_i, Ctqt) 

195 

196 if calculate_incoherent: 

197 for atom_type in traj.atom_types: 

198 xi = fi.positions_by_type[atom_type] 

199 x0 = f0.positions_by_type[atom_type] 

200 Fsqt = np.real(calc_rho_q(xi - x0, q_points)) 

201 F_s_q_t_averager[atom_type].add_sample(time_i, Fsqt) 

202 

203 # run calculation 

204 with concurrent.futures.ThreadPoolExecutor() as tpe: 

205 # This is the "main loop" over the trajectory 

206 for window in window_iterator: 

207 if logging_interval and window[0].frame_index % logging_interval == 0: 

208 logger.info(f'Processing window {window[0].frame_index} to {window[-1].frame_index}') # noqa 

209 else: 

210 logger.debug(f'Processing window {window[0].frame_index} to {window[-1].frame_index}') # noqa 

211 

212 # The map conveniently applies calc_corr to all time-lags. However, 

213 # as everything is done in place nothing gets returned so in order 

214 # to start and wait for the processes to finish we must iterate 

215 # over the None values returned 

216 for _ in tpe.map(partial(calc_corr, window), range(len(window))): 

217 pass 

218 

219 # collect results into dict with numpy arrays (n_qpoints, N_tc) 

220 data_dict_corr = dict() 

221 data_dict_corr['q_points'] = q_points 

222 

223 time = None 

224 for pair in pairs: 

225 key = '_'.join(pair) 

226 F_q_t = 1 / traj.n_atoms * truncate_nans(F_q_t_averager[pair].get_average_all()) 

227 w, S_q_w = psd_from_acf_2d(F_q_t, delta_t) 

228 S_q_w = np.array(S_q_w) 

229 

230 if time is None: 

231 # Determine the actual length of time signal after truncation (if needed) 

232 time = delta_t * np.arange(F_q_t.shape[1], dtype=float) 

233 N_tc_actual = len(time) 

234 F_q_t_tot = np.zeros((n_qpoints, N_tc_actual)) 

235 S_q_w_tot = np.zeros((n_qpoints, N_tc_actual)) 

236 else: 

237 assert F_q_t.shape[1] == len(time) 

238 

239 data_dict_corr['omega'] = w 

240 data_dict_corr[f'Fqt_coh_{key}'] = F_q_t 

241 data_dict_corr[f'Sqw_coh_{key}'] = S_q_w 

242 

243 # sum all partials to the total 

244 F_q_t_tot += F_q_t 

245 S_q_w_tot += S_q_w 

246 

247 if N_tc_actual < N_tc: 

248 logger.warning('Truncating ACF due to NaNs, likely time_window is longer than length of Trajectory') # noqa 

249 data_dict_corr['time'] = time 

250 data_dict_corr['Fqt_coh'] = F_q_t_tot 

251 data_dict_corr['Sqw_coh'] = S_q_w_tot 

252 

253 if calculate_currents: 

254 Cl_q_t_tot = np.zeros((n_qpoints, N_tc_actual)) 

255 Ct_q_t_tot = np.zeros((n_qpoints, N_tc_actual)) 

256 Cl_q_w_tot = np.zeros((n_qpoints, N_tc_actual)) 

257 Ct_q_w_tot = np.zeros((n_qpoints, N_tc_actual)) 

258 for pair in pairs: 

259 key = '_'.join(pair) 

260 Cl_q_t = 1 / traj.n_atoms * truncate_nans(Cl_q_t_averager[pair].get_average_all()) 

261 Ct_q_t = 1 / traj.n_atoms * truncate_nans(Ct_q_t_averager[pair].get_average_all()) 

262 _, Cl_q_w = psd_from_acf_2d(Cl_q_t, delta_t) 

263 _, Ct_q_w = psd_from_acf_2d(Ct_q_t, delta_t) 

264 data_dict_corr[f'Clqt_{key}'] = Cl_q_t 

265 data_dict_corr[f'Ctqt_{key}'] = Ct_q_t 

266 data_dict_corr[f'Clqw_{key}'] = Cl_q_w 

267 data_dict_corr[f'Ctqw_{key}'] = Ct_q_w 

268 

269 # sum all partials to the total 

270 Cl_q_t_tot += Cl_q_t 

271 Ct_q_t_tot += Ct_q_t 

272 Cl_q_w_tot += Cl_q_w 

273 Ct_q_w_tot += Ct_q_w 

274 data_dict_corr['Clqt'] = Cl_q_t_tot 

275 data_dict_corr['Ctqt'] = Ct_q_t_tot 

276 data_dict_corr['Clqw'] = Cl_q_w_tot 

277 data_dict_corr['Ctqw'] = Ct_q_w_tot 

278 

279 if calculate_incoherent: 

280 Fs_q_t_tot = np.zeros((n_qpoints, N_tc_actual)) 

281 Ss_q_w_tot = np.zeros((n_qpoints, N_tc_actual)) 

282 for atom_type in traj.atom_types: 

283 Fs_q_t = 1 / traj.n_atoms * truncate_nans(F_s_q_t_averager[atom_type].get_average_all()) 

284 _, Ss_q_w = psd_from_acf_2d(Fs_q_t, delta_t) 

285 data_dict_corr[f'Fqt_incoh_{atom_type}'] = Fs_q_t 

286 data_dict_corr[f'Sqw_incoh_{atom_type}'] = Ss_q_w 

287 

288 # sum all partials to the total 

289 Fs_q_t_tot += Fs_q_t 

290 Ss_q_w_tot += Ss_q_w 

291 

292 data_dict_corr['Fqt_incoh'] = Fs_q_t_tot 

293 data_dict_corr['Sqw_incoh'] = Ss_q_w_tot 

294 

295 # finalize results with additional metadata 

296 new_sample = DynamicSample( 

297 data_dict_corr, 

298 simulation_data=dict( 

299 atom_types=traj.atom_types, pairs=pairs, 

300 particle_counts=particle_counts, 

301 cell=traj.cell, 

302 time_between_frames=delta_t, 

303 maximum_time_lag=delta_t * window_size, 

304 angular_frequency_resolution=dw, 

305 maximum_angular_frequency=w_max, 

306 number_of_frames=traj.number_of_frames_read, 

307 )) 

308 new_sample._append_history( 

309 'compute_dynamic_structure_factors', 

310 dict( 

311 dt=dt, 

312 window_size=window_size, 

313 window_step=window_step, 

314 calculate_currents=calculate_currents, 

315 calculate_incoherent=calculate_incoherent, 

316 )) 

317 

318 return new_sample 

319 

320 

321def compute_static_structure_factors( 

322 traj: Trajectory, 

323 q_points: NDArray[float], 

324 logging_interval: Optional[int] = 1000, 

325) -> StaticSample: 

326 r"""Compute the static structure factors. The results are returned in the 

327 form of a :class:`StaticSample <dynasor.sample.StaticSample>` 

328 object. 

329 

330 Parameters 

331 ---------- 

332 traj 

333 Input trajectory. 

334 q_points 

335 Array of q-points in units of rad/Å with shape ``(N_qpoints, 3)`` in Cartesian coordinates. 

336 logging_interval 

337 Log progress at ``INFO`` level every this many frames. Set to ``0`` to disable 

338 progress logging. 

339 """ 

340 # sanity check input args 

341 if q_points.shape[1] != 3: 

342 raise ValueError('q-points array has the wrong shape.') 

343 

344 n_qpoints = q_points.shape[0] 

345 logger.info(f'Number of q-points: {n_qpoints}') 

346 

347 # define all pairs 

348 pairs = list(combinations_with_replacement(traj.atom_types, r=2)) 

349 particle_counts = {key: len(val) for key, val in traj.atomic_indices.items()} 

350 logger.debug('Considering pairs:') 

351 for pair in pairs: 

352 logger.debug(f' {pair}') 

353 

354 # processing function 

355 def f2_rho(frame): 

356 rho_qs_dict = dict() 

357 for atom_type in frame.positions_by_type.keys(): 

358 x = frame.positions_by_type[atom_type] 

359 rho_qs_dict[atom_type] = calc_rho_q(x, q_points) 

360 frame.rho_qs_dict = rho_qs_dict 

361 return frame 

362 

363 # setup averager 

364 Sq_averager = dict() 

365 for pair in pairs: 

366 Sq_averager[pair] = TimeAverager(1, n_qpoints) # time average with only timelag=0 

367 

368 # main loop 

369 for frame in traj: 

370 

371 # process_frame 

372 f2_rho(frame) 

373 if logging_interval and frame.frame_index % logging_interval == 0: 

374 logger.info(f'Processing frame {frame.frame_index}') 

375 else: 

376 logger.debug(f'Processing frame {frame.frame_index}') 

377 

378 for s1, s2 in pairs: 

379 # compute correlation 

380 Sq_pair = np.real(frame.rho_qs_dict[s1] * frame.rho_qs_dict[s2].conjugate()) 

381 if s1 != s2: 

382 Sq_pair += np.real(frame.rho_qs_dict[s2] * frame.rho_qs_dict[s1].conjugate()) 

383 Sq_averager[(s1, s2)].add_sample(0, Sq_pair) 

384 

385 # collect results 

386 data_dict = dict() 

387 data_dict['q_points'] = q_points 

388 S_q_tot = np.zeros((n_qpoints, 1)) 

389 for s1, s2 in pairs: 

390 Sq = 1 / traj.n_atoms * Sq_averager[(s1, s2)].get_average_at_timelag(0).reshape(-1, 1) 

391 data_dict[f'Sq_{s1}_{s2}'] = Sq 

392 S_q_tot += Sq 

393 data_dict['Sq'] = S_q_tot 

394 

395 # finalize results 

396 new_sample = StaticSample( 

397 data_dict, 

398 simulation_data=dict( 

399 atom_types=traj.atom_types, 

400 pairs=pairs, 

401 particle_counts=particle_counts, 

402 cell=traj.cell, 

403 number_of_frames=traj.number_of_frames_read, 

404 )) 

405 new_sample._append_history('compute_static_structure_factors') 

406 

407 return new_sample 

408 

409 

410def compute_spectral_energy_density( 

411 traj: Trajectory, 

412 ideal_supercell: Atoms, 

413 primitive_cell: Atoms, 

414 q_points: NDArray[float], 

415 dt: float, 

416 partial: Optional[bool] = False, 

417 logging_interval: Optional[int] = 1000, 

418) -> tuple[NDArray[float], NDArray[float]]: 

419 r""" 

420 Compute the spectral energy density (SED) at specific q-points. The results 

421 are returned in the form of a tuple, which comprises the angular 

422 frequencies in an array of length ``N_times`` in units of rad/fs and the 

423 SED in units of eV/(rad/fs) as an array of shape ``(N_qpoints, N_times)``. 

424 The normalization is chosen such that integrating the SED of a q-point 

425 together with the supplied angular frequencies omega (rad/fs) yields 

426 `1/2 kB T` * number of bands (where number of bands = `len(prim) * 3`) 

427 

428 More details can be found in Thomas *et al.*, Physical Review B **81**, 081411 (2010), 

429 which should be cited when using this function along with the dynasor reference. 

430 

431 **Note 1:** 

432 SED analysis is only suitable for crystalline materials without diffusion as 

433 atoms are assumed to move around fixed reference positions throughout the entire trajectory. 

434 

435 **Note 2:** 

436 This implementation reads the full trajectory and can thus consume a lot of memory. 

437 

438 Parameters 

439 ---------- 

440 traj 

441 Input trajectory. 

442 ideal_supercell 

443 Ideal structure defining the reference positions. Do not change the masses 

444 in the ASE :class:`Atoms` objects to dynasor internal units, this will be 

445 done internally. 

446 primitive_cell 

447 Underlying primitive structure. Must be aligned correctly with :attr:`ideal_supercell`. 

448 q_points 

449 Array of q-points in units of rad/Å with shape ``(N_qpoints, 3)`` in Cartesian coordinates. 

450 dt 

451 Time difference in femtoseconds between two consecutive snapshots in 

452 the trajectory. Note that you should not change :attr:`dt` if you change 

453 :attr:`frame_step <dynasor.trajectory.Trajectory.frame_step>` in :attr:`traj`. 

454 partial 

455 If True the SED will be returned decomposed per basis and Cartesian direction. 

456 The shape is ``(N_qpoints, N_frequencies, len(primitive_cell), 3)``. 

457 logging_interval 

458 Log progress at ``INFO`` level every this many frames. Set to ``0`` to disable 

459 progress logging. 

460 """ 

461 

462 delta_t = traj.frame_step * dt 

463 

464 # logger 

465 logger.info('Running SED') 

466 logger.info(f'Time between consecutive frames (dt * frame_step): {delta_t} fs') 

467 logger.info(f'Number of atoms in primitive_cell: {len(primitive_cell)}') 

468 logger.info(f'Number of atoms in ideal_supercell: {len(ideal_supercell)}') 

469 logger.info(f'Number of q-points: {q_points.shape[0]}') 

470 

471 # check that the ideal supercell agrees with traj 

472 if traj.n_atoms != len(ideal_supercell): 

473 raise ValueError('ideal_supercell must contain the same number of atoms as the trajectory.') 

474 

475 if len(primitive_cell) >= len(ideal_supercell): 

476 raise ValueError('primitive_cell contains more atoms than ideal_supercell.') 

477 

478 if not traj.has_velocities: 

479 raise ValueError('compute_spectral_energy_density requires velocities to be available ' 

480 'in the trajectory, but traj does not provide velocities.') 

481 

482 # colllect all velocities, and scale with sqrt(masses) 

483 masses = ideal_supercell.get_masses().reshape(-1, 1) / fs**2 # From Dalton to dmu 

484 velocities = [] 

485 for it, frame in enumerate(traj): 

486 if logging_interval and it % logging_interval == 0: 

487 logger.info(f'Reading frame {it}') 

488 else: 

489 logger.debug(f'Reading frame {it}') 

490 v = frame.get_velocities_as_array(traj.atomic_indices) # in Å/fs 

491 velocities.append(np.sqrt(masses) * v) 

492 logger.info(f'Number of snapshots: {len(velocities)}') 

493 

494 # Perform the FFT on the last axis for extra speed (maybe not needed) 

495 N_samples = len(velocities) 

496 velocities = np.array(velocities) 

497 # places time index last and makes a copy for continuity 

498 velocities = velocities.transpose(1, 2, 0).copy() 

499 # #atoms in supercell x 3 directions x #frequencies 

500 velocities = np.fft.rfft(velocities, axis=2) 

501 

502 # Calculate indices and offsets needed for the SED method 

503 offsets, indices = get_offset_index(primitive_cell, ideal_supercell) 

504 

505 # Phase factor for use in FT. #qpoints x #atoms in supercell 

506 cell_positions = np.dot(offsets, primitive_cell.cell) 

507 phase = np.dot(q_points, cell_positions.T) # #qpoints x #unit cells 

508 phase_factors = np.exp(1.0j * phase) 

509 

510 # This dict maps the offsets to an index so ndarrays can be over 

511 # offset,index instead of atoms in supercell 

512 offset_dict = {off: n for n, off in enumerate(set(tuple(offset) for offset in offsets))} 

513 

514 # Pick out some shapes 

515 n_super, _, n_w = velocities.shape 

516 n_qpts = len(q_points) 

517 n_prim = len(primitive_cell) 

518 n_offsets = len(offset_dict) 

519 

520 # This new array will be indexed by index and offset instead (and also transposed) 

521 new_velocities = np.zeros((n_w, 3, n_prim, n_offsets), dtype=velocities.dtype) 

522 

523 for i in range(n_super): 

524 j = indices[i] # atom with index i in the supercell is of basis type j ... 

525 n = offset_dict[tuple(offsets[i])] # and its offset has index n 

526 new_velocities[:, :, j, n] = velocities[i].T 

527 

528 velocities = new_velocities 

529 

530 # Same story with the spatial phase factors 

531 new_phase_factors = np.zeros((n_qpts, n_prim, n_offsets), dtype=phase_factors.dtype) 

532 

533 for i in range(n_super): 

534 j = indices[i] 

535 n = offset_dict[tuple(offsets[i])] 

536 new_phase_factors[:, j, n] = phase_factors[:, i] 

537 

538 phase_factors = new_phase_factors 

539 

540 # calculate the density in a numba function 

541 density = _sed_inner_loop(phase_factors, velocities) 

542 

543 if not partial: 

544 density = np.sum(density, axis=(2, 3)) 

545 

546 # units 

547 # make so that the velocities were originally in Angstrom / fs to be compatible with eV and Da 

548 

549 # the time delta in the fourier transform 

550 density = density * delta_t**2 

551 

552 # Divide by the length of the time signal 

553 density = density / (N_samples * delta_t) 

554 

555 # Divide by the number of primitive cells 

556 density = density / (n_super / n_prim) 

557 

558 # Factor so the sed can be integrated together with the returned omega 

559 # numpy fft works with ordinary/linear frequencies and not angular freqs 

560 density = density / (2*np.pi) 

561 

562 # angular frequencies 

563 w = 2 * np.pi * np.fft.rfftfreq(N_samples, delta_t) # rad/fs 

564 

565 return w, density 

566 

567 

568@numba.njit(parallel=True, fastmath=True) 

569def _sed_inner_loop(phase_factors, velocities): 

570 """This numba function calculates the spatial 

571 Fourier transform using precomputed phase factors. 

572 

573 As the use case can be one or many q-points the parallelization is over the 

574 temporal frequency components instead. 

575 """ 

576 

577 n_qpts = phase_factors.shape[0] # q-point index 

578 n_prim = phase_factors.shape[1] # basis atom index 

579 n_super = phase_factors.shape[2] # unit cell index 

580 

581 n_freqs = velocities.shape[0] # frequency, direction, basis atom, unit cell 

582 

583 density = np.zeros((n_qpts, n_freqs, n_prim, 3), dtype=np.float64) 

584 

585 for w in numba.prange(n_freqs): 

586 for k in range(n_qpts): 

587 for a in range(3): 

588 for b in range(n_prim): 

589 tmp = 0.0j 

590 for n in range(n_super): 

591 tmp += phase_factors[k, b, n] * velocities[w, a, b, n] 

592 density[k, w, b, a] += np.abs(tmp)**2 

593 return density