Source code for dynasor.trajectory.trajectory_frame

from dataclasses import dataclass
from typing import Optional, Sequence
import numpy as np
from numpy.typing import NDArray


per_atom_properties = ('positions', 'velocities', 'forces')
"""Names of the per-atom properties that a trajectory frame can carry, in the order in
which a frame reports them."""


@dataclass
class ReaderFrame:
    """Trivial data struct holding MD-data for one time frame.

    Parameters
    ----------
    frame_index
        Trajectory index of the snapshot (frame).
    cell
        Simulation cell as 3 row vectors (Å).
    n_atoms
        Number of atoms.
    positions
        Particle positions as an array with shape ``(n_atoms, 3)`` (Å).
    velocities
        Particle velocities as an array with shape ``(n_atoms, 3)`` (Å/fs);
        may not be available, depending on reader and trajectory file format.
    forces
        Forces on the particles as an array with shape ``(n_atoms, 3)`` (eV/Å);
        may not be available, depending on reader and trajectory file format.
    atom_types
        Array with the type of each atom;
        may not be available, depending on reader and trajectory file format.
    """
    frame_index: int
    cell: NDArray[float]
    n_atoms: int
    positions: NDArray[float]
    velocities: Optional[NDArray[float]] = None
    forces: Optional[NDArray[float]] = None
    atom_types: Optional[NDArray[str]] = None


[docs] class TrajectoryFrame: """ Class holding positions and optionally velocities and forces, split by atom type, for one snapshot (frame) in a trajectory. Each quantity is provided as a dictionary keyed by atom type, such that `positions_by_type['Cs']` is a numpy array with shape `(n_atoms_Cs, 3)` and `positions_by_type['Pb']` is a numpy array with shape `(n_atoms_Pb, 3)`. The `get_*_as_array` methods return a quantity for all atoms in a single array ordered by atom index. A quantity that the frame does not carry is reported as `None`. Parameters ---------- atomic_indices Dictionary specifying which indices (values) belong to which atom type (keys). frame_index Trajectory index of the snapshot (frame). positions Positions as an array with shape `(n_atoms, 3)`. velocities Velocities as an array with shape `(n_atoms, 3)`; defaults to `None`. forces Forces as an array with shape `(n_atoms, 3)`; defaults to `None`. properties Names of the per-atom properties to keep, a subset of :data:`per_atom_properties`; defaults to `None`, which keeps every quantity that is provided. """ def __init__(self, atomic_indices: dict[str, list[int]], frame_index: int, positions: Optional[NDArray[float]] = None, velocities: Optional[NDArray[float]] = None, forces: Optional[NDArray[float]] = None, properties: Optional[Sequence[str]] = None): self._frame_index = frame_index # Indexing along the atom axis only puts no constraint on the shape of a per-atom # quantity, and already returns a copy rather than a view of the reader array. arrays = dict(positions=positions, velocities=velocities, forces=forces) self._arrays_by_type = dict() for name in per_atom_properties: array = arrays[name] if array is None: continue if properties is not None and name not in properties: continue self._arrays_by_type[name] = { atom_type: np.asarray(array)[indices] for atom_type, indices in atomic_indices.items()} @property def positions_by_type(self) -> Optional[dict[str, NDArray[float]]]: """ Positions split by atom type; ``None`` if the frame carries no positions. """ return self._arrays_by_type.get('positions') @property def velocities_by_type(self) -> Optional[dict[str, NDArray[float]]]: """ Velocities split by atom type; ``None`` if the frame carries no velocities. """ return self._arrays_by_type.get('velocities') @property def forces_by_type(self) -> Optional[dict[str, NDArray[float]]]: """ Forces split by atom type; ``None`` if the frame carries no forces. """ return self._arrays_by_type.get('forces') def _get_as_array(self, name: str, atomic_indices: dict[str, list[int]]) -> NDArray[float]: """ Return the per-atom quantity :attr:`name` reassembled into a single array whose first dimension runs over all atoms. Parameters ---------- name Name of the quantity, one of ``'positions'``, ``'velocities'``, ``'forces'``. atomic_indices Dictionary specifying which indices (values) belong to which atom type (keys). """ arrays_by_type = self._arrays_by_type.get(name) if arrays_by_type is None: raise ValueError(f'This frame provides no {name}.') # check that atomic_indices is complete n_atoms = np.max([np.max(indices) for indices in atomic_indices.values()]) + 1 all_inds = [i for indices in atomic_indices.values() for i in indices] if len(all_inds) != n_atoms or len(set(all_inds)) != n_atoms: raise ValueError('atomic_indices is incomplete') # Collect the quantity into a single array. Only the shape is taken from the # per-type arrays, so that the returned array is float64 whatever the precision the # reader provides, which some of them read as float32. reference = next(iter(arrays_by_type.values())) array = np.empty((n_atoms, ) + reference.shape[1:]) for atom_type, indices in atomic_indices.items(): array[indices] = arrays_by_type[atom_type] return array
[docs] def get_positions_as_array(self, atomic_indices: dict[str, list[int]]) -> NDArray[float]: """ Return the full positions array with shape ``(n_atoms, 3)``. Parameters ---------- atomic_indices Dictionary specifying which indices (values) belong to which atom type (keys). """ return self._get_as_array('positions', atomic_indices)
[docs] def get_velocities_as_array(self, atomic_indices: dict[str, list[int]]) -> NDArray[float]: """ Return the full velocities array with shape ``(n_atoms, 3)``. Parameters ---------- atomic_indices Dictionary specifying which indices (values) belong to which atom type (keys). """ return self._get_as_array('velocities', atomic_indices)
[docs] def get_forces_as_array(self, atomic_indices: dict[str, list[int]]) -> NDArray[float]: """ Return the full forces array with shape ``(n_atoms, 3)``. Parameters ---------- atomic_indices Dictionary specifying which indices (values) belong to which atom type (keys). """ return self._get_as_array('forces', atomic_indices)
@property def frame_index(self) -> int: """ Index of the frame. """ return self._frame_index def __str__(self) -> str: s = [f'Frame index {self.frame_index}'] for name, arrays_by_type in self._arrays_by_type.items(): for key, val in arrays_by_type.items(): s.append(f' {name:10} : {key} shape : {val.shape}') return '\n'.join(s) def __repr__(self) -> str: return str(self) def _repr_html_(self) -> str: s = [f'<h3>{self.__class__.__name__}</h3>'] s += ['<table border="1" class="dataframe">'] s += ['<thead><tr><th style="text-align: left;">Field</th>' '<th>Value/Shape</th></tr></thead>'] s += ['<tbody>'] s += [f'<tr><td style="text-align: left;">Index</td><td>{self.frame_index}</td></tr>'] for name, arrays_by_type in self._arrays_by_type.items(): for key, val in arrays_by_type.items(): s += [f'<tr><td style="text-align: left;">{name.capitalize()} {key}</td>' f'<td>{val.shape}</td></tr>'] s += ['</tbody>'] s += ['</table>'] return '\n'.join(s)