Coverage for dynasor/modes/project_modes.py: 96%
41 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 19:46 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 19:46 +0000
1from typing import Optional
3import numpy as np
5from ase import Atoms
6from numpy.typing import NDArray
8from dynasor.logging_tools import logger
9from dynasor.trajectory import Trajectory
10from dynasor.tools.structures import get_displacements_from_u
13def project_modes(
14 traj: Trajectory,
15 modes: NDArray[float],
16 ideal_supercell: Atoms,
17 check_mic: Optional[bool] = True,
18 logging_interval: Optional[int] = 1000,
19) -> tuple[NDArray[float], NDArray[float]]:
20 """Projects an atomic trajectory onto set of phonon modes.
22 Parameters
23 ----------
24 traj
25 Input trajectory.
26 modes
27 Modes to project on, as an array with shape ``(..., N, 3)`` where ``N`` is the
28 number of atoms in the supercell and the leading dimensions define the output shape.
29 ideal_supercell
30 Ideal supercell used to find atomic displacements. It should correspond to the ideal
31 structure. Be careful not to mess up the permutation: its atom count is checked
32 against :attr:`traj`; a mismatched cell only triggers a warning (some thermal
33 expansion relative to the reference structure is normal), and the atom ordering
34 is not checked at all.
35 check_mic
36 Whether to wrap the displacements or not, faster if no wrap.
37 logging_interval
38 Log progress at ``INFO`` level every this many frames. Set to ``0`` to disable
39 progress logging.
41 Returns
42 -------
43 A tuple comprising `(Q,P)` where `Q` are the mode coordinates as a complex array
44 with dimension (length of traj, number of modes) and `P` are the mode momenta as a
45 complex array with dimension (length of traj, number of modes). If :attr:`traj` does
46 not provide velocities, `P` is returned as all zeros.
47 """
48 # logger
49 logger.info('Running mode projection')
50 if not traj.has_positions: 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true
51 raise ValueError('project_modes requires positions to be available in the trajectory, '
52 'but traj does not provide positions.')
53 if not traj.has_velocities:
54 logger.info('traj does not provide velocities; P will be returned as zeros')
56 modes = np.asarray(modes)
58 original_mode_shape = modes.shape
60 if modes.ndim < 2 or modes.shape[-1] != 3 or modes.shape[-2] != traj.n_atoms:
61 raise ValueError(
62 f'modes must have shape (..., N, 3), where N = {traj.n_atoms} is the number of '
63 f'atoms in the trajectory, but has shape {modes.shape}.')
64 if traj.n_atoms != len(ideal_supercell):
65 raise ValueError('ideal_supercell must contain the same number of atoms as the trajectory.')
66 if not np.allclose(traj.cell, ideal_supercell.cell, atol=1e-5, rtol=0.0):
67 logger.warning('ideal_supercell cell does not match the trajectory cell.')
69 modes = modes.reshape((-1, modes.shape[-2], 3))
70 modes_conj = modes.conj()
72 Q_traj, P_traj = [], []
73 for it, frame in enumerate(traj):
74 if logging_interval and it % logging_interval == 0:
75 logger.info(f'Reading frame {it}')
76 else:
77 logger.debug(f'Reading frame {it}')
79 # Make positions into displacements
80 x = frame.get_positions_as_array(traj._atomic_indices)
81 u = x - ideal_supercell.positions
83 # Calculate Q
84 u = get_displacements_from_u(u, ideal_supercell.cell, check_mic=check_mic)
85 Q = np.einsum('mnx,nx->m', modes, u, optimize=True)
87 # Calculate P
88 if traj.has_velocities:
89 v = frame.get_velocities_as_array(traj._atomic_indices)
90 P = np.einsum('mnx,nx->m', modes_conj, v, optimize=True)
91 else:
92 P = np.zeros_like(Q)
94 Q_traj.append(Q)
95 P_traj.append(P)
97 Q_traj = np.array(Q_traj).reshape((len(Q_traj), *original_mode_shape[:-2]))
98 P_traj = np.array(P_traj).reshape((len(P_traj), *original_mode_shape[:-2]))
100 return Q_traj, P_traj