File size: 6,018 Bytes
fefb9a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"""
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"<GDBQCD: {self.n_molecules:,} molecules, "
                f"{int(self.f.attrs['n_trajectories']):,} trajectories, "
                f"{self.n_frames} frames each, {self.nmr_level_of_theory}>")


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")