""" decode_gdb_qcd.py - reader/decoder for the gdb_qcd HDF5 dataset. gdb_qcd is a quasiclassical-dynamics benchmark: replicate molecular-dynamics trajectories for 2461 small molecules drawn from GDB (the MagNET / sigma-shake test set). It is used to benchmark rovibrational corrections, which are computed by comparing NMR shieldings on the moving (displaced) geometries against the resting (stationary) geometry. For each molecule there are several trajectories; each trajectory has 33 frames; each frame gives every atom's position and its PBE0/pcSseg-1 NMR shielding. Trajectory counts vary by molecule (about 23 on average); the frame count is always 33. The resting (stationary) reference shieldings are not included here: they are the test-set entries of the MagNET training data (sigma-shake), matched by the same molecule id. Storage format: - Per-atom arrays are concatenated across molecules. Molecule i has `n_atoms[i]` atoms and `n_trajectories[i]` trajectories. Its atomic numbers occupy `n_atoms[i]` rows; its coordinates and shieldings occupy `n_trajectories[i] * 33 * n_atoms[i]` rows, which reshape to (n_trajectories, 33, n_atoms, ...). Offsets are reconstructed once, on open. - coordinates and shieldings are int32 fixed point: physical = stored * 1e-4, so reconstruction error is <= 5e-5 (Angstrom or ppm). A missing value would be the marker -2147483648 -> NaN (none occur in this dataset). - `molecule_ids` are integers and match the sigma-shake test-set ids. Requires: Python >= 3.7, numpy, h5py. No other dependencies. """ from __future__ import annotations import numpy as np import h5py _SCALE = 1e-4 _MARKER = -2147483648 def _decode(values: np.ndarray) -> np.ndarray: """int32 fixed-point -> float64 physical units; marker -> NaN. No-op on float input.""" values = np.asarray(values) if not np.issubdtype(values.dtype, np.integer): return np.asarray(values, dtype=np.float64) out = values.astype(np.float64) * _SCALE out[values == _MARKER] = np.nan return out class GDBQCD: """Reader for gdb_qcd.hdf5. Use as a context manager; index molecules by position or by id.""" def __init__(self, path: str): self.f = h5py.File(path, "r") self.molecule_ids = self.f["molecule_ids"][:] self.n_molecules = int(self.f.attrs["n_molecules"]) self.n_frames = int(self.f.attrs["n_frames"]) self.nmr_level_of_theory = str(self.f.attrs["nmr_level_of_theory"]) self._n_atoms = self.f["n_atoms"][:].astype(np.int64) self._n_traj = self.f["n_trajectories"][:].astype(np.int64) # atom-row offsets (atomic_numbers) and coordinate/shielding-row offsets self._astart = np.empty(self.n_molecules + 1, np.int64) self._astart[0] = 0 np.cumsum(self._n_atoms, out=self._astart[1:]) block = self._n_traj * self.n_frames * self._n_atoms self._cstart = np.empty(self.n_molecules + 1, np.int64) self._cstart[0] = 0 np.cumsum(block, out=self._cstart[1:]) self._id_to_index = {int(m): i for i, m in enumerate(self.molecule_ids)} def _check(self, index: int): if not 0 <= index < self.n_molecules: raise IndexError(f"molecule index {index} out of range [0, {self.n_molecules})") def index_of(self, molecule_id: int) -> int: """Position of the molecule with this id; raises KeyError if absent.""" if int(molecule_id) not in self._id_to_index: raise KeyError(f"molecule id {molecule_id} not in gdb_qcd") return self._id_to_index[int(molecule_id)] def n_trajectories(self, index: int) -> int: """How many trajectories molecule i has.""" self._check(index) return int(self._n_traj[index]) def molecule(self, index: int) -> dict: """Return one molecule's trajectory data (physical units) by position. Keys: id - integer molecule id (matches the sigma-shake test set) atomic_numbers - (n_atoms,) uint8 coordinates - (n_trajectories, 33, n_atoms, 3) Angstrom shieldings - (n_trajectories, 33, n_atoms) ppm, PBE0/pcSseg-1 """ self._check(index) na = int(self._n_atoms[index]) nt = int(self._n_traj[index]) asl = slice(int(self._astart[index]), int(self._astart[index + 1])) csl = slice(int(self._cstart[index]), int(self._cstart[index + 1])) coords = _decode(self.f["coordinates"][csl]).reshape(nt, self.n_frames, na, 3) shields = _decode(self.f["shieldings"][csl]).reshape(nt, self.n_frames, na) return { "id": int(self.molecule_ids[index]), "atomic_numbers": self.f["atomic_numbers"][asl], "coordinates": coords, "shieldings": shields, } def molecule_by_id(self, molecule_id: int) -> dict: """Return one molecule's trajectory data (see `molecule`) by id.""" return self.molecule(self.index_of(molecule_id)) def close(self): """Close the underlying HDF5 file.""" self.f.close() def __enter__(self): return self def __exit__(self, *a): self.close() def __len__(self): return self.n_molecules def __repr__(self): return (f"") if __name__ == "__main__": import sys path = sys.argv[1] if len(sys.argv) > 1 else "gdb_qcd.hdf5" with GDBQCD(path) as ds: print(ds) m = ds.molecule(0) print(f" molecule id={m['id']}: {len(m['atomic_numbers'])} atoms, " f"coordinates {m['coordinates'].shape}, shieldings {m['shieldings'].shape}") print(f" trajectory 0, frame 0, atom 0: xyz={np.round(m['coordinates'][0,0,0], 4)} " f"shielding={m['shieldings'][0,0,0]:.4f} ppm")