File size: 5,575 Bytes
fefb9a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23c6f41
 
 
fefb9a6
 
 
 
 
 
 
 
 
23c6f41
fefb9a6
 
 
 
 
 
23c6f41
 
 
 
fefb9a6
 
23c6f41
 
fefb9a6
 
 
 
 
 
 
 
 
 
 
 
 
23c6f41
 
 
fefb9a6
 
 
 
 
 
 
 
 
 
 
23c6f41
fefb9a6
 
 
 
 
23c6f41
 
 
 
 
fefb9a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23c6f41
 
 
fefb9a6
 
23c6f41
 
 
 
 
fefb9a6
23c6f41
 
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
"""Tests for data/delta50/decode_delta50.py.

A tiny synthetic file in the stored int32 format exercises the reader, the per-molecule slicing,
the fixed-point decode, and the PCM-correction helper, without the real file. An opt-in smoke test
runs against the real file when present.
"""
import os
import sys

import numpy as np
import pytest
import h5py

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)

import decode_delta50 as D  # noqa: E402

SCALE = 1e-4
MARKER = -2147483648
REAL = os.path.join(HERE, "delta50.hdf5")


def _encode(values):
    v = np.asarray(values, dtype=np.float64)
    finite = np.isfinite(v)
    out = np.full(v.shape, MARKER, dtype=np.int64)
    out[finite] = np.round(v[finite] / SCALE).astype(np.int64)
    return out.astype(np.int32)


def _build_synthetic(path):
    """Two molecules (3 and 5 atoms) in the stored int32 format, plus the decoded truth to check against."""
    rng = np.random.default_rng(0)
    names = ["alpha", "beta"]
    specs = [3, 5]  # n_atoms
    n_atoms = np.array(specs, dtype=np.int32)
    an, coords, zero, b3, pcm, dwp, dwb, exp, truth = [], [], [], [], [], [], [], [], []
    for na in specs:
        a = rng.integers(1, 9, size=na).astype(np.uint8)
        c = rng.normal(0, 2, size=(na, 3))
        z = rng.normal(100, 30, size=na)
        g = rng.normal(100, 30, size=na)
        p = g + rng.normal(0, 0.2, size=na)
        dw = z + rng.normal(0, 0.1, size=na)
        db = z + rng.normal(0, 0.1, size=na)
        e = rng.normal(5, 2, size=na)
        e[a > 6] = np.nan  # heteroatoms have no experimental shift
        an.append(a); coords.append(_encode(c)); zero.append(_encode(z))
        b3.append(_encode(g)); pcm.append(_encode(p))
        dwp.append(_encode(dw)); dwb.append(_encode(db)); exp.append(_encode(e))
        truth.append((a, c, z, g, p, dw, db, e))
    opts = dict(compression="gzip", shuffle=True)
    with h5py.File(path, "w") as f:
        f.attrs["n_molecules"] = 2
        f.attrs["scale"] = SCALE
        f.attrs["missing_value_marker"] = MARKER
        f.create_dataset("molecule_names", data=np.array(names, dtype=object),
                         dtype=h5py.string_dtype("utf-8"))
        f.create_dataset("n_atoms", data=n_atoms, **opts)
        f.create_dataset("atomic_numbers", data=np.concatenate(an), **opts)
        f.create_dataset("coordinates", data=np.concatenate(coords), **opts)
        f.create_dataset("nn_magnet_zero", data=np.concatenate(zero), **opts)
        f.create_dataset("nn_b3lyp", data=np.concatenate(b3), **opts)
        f.create_dataset("nn_b3lyp_pcm", data=np.concatenate(pcm), **opts)
        f.create_dataset("shielding_wp04_pcSseg2", data=np.concatenate(dwp), **opts)
        f.create_dataset("shielding_wb97xd_pcSseg2", data=np.concatenate(dwb), **opts)
        f.create_dataset("experimental_shift", data=np.concatenate(exp), **opts)
    return names, specs, truth


def test_reader_and_decode(tmp_path):
    path = str(tmp_path / "tiny.hdf5")
    names, specs, truth = _build_synthetic(path)
    with D.Delta50(path) as ds:
        assert len(ds) == 2
        assert ds.molecule_names == names
        for i, na in enumerate(specs):
            m = ds.molecule(i)
            a, c, z, g, p, dw, db, e = truth[i]
            assert m["name"] == names[i]
            assert np.array_equal(m["atomic_numbers"], a)
            assert m["coordinates"].shape == (na, 3)
            assert np.abs(m["coordinates"] - c).max() <= 5e-5
            assert np.abs(m["nn_magnet_zero"] - z).max() <= 5e-5
            assert np.abs(m["shielding_wp04_pcSseg2"] - dw).max() <= 5e-5
            assert np.abs(m["shielding_wb97xd_pcSseg2"] - db).max() <= 5e-5
            finite = np.isfinite(e)
            assert np.abs(m["experimental_shift"][finite] - e[finite]).max() <= 5e-5
            assert np.isnan(m["experimental_shift"][~finite]).all()
            assert np.allclose(ds.pcm_correction(i), p - g, atol=1e-4)


def test_lookup_and_errors(tmp_path):
    path = str(tmp_path / "tiny.hdf5")
    names, _, _ = _build_synthetic(path)
    with D.Delta50(path) as ds:
        assert ds.molecule_by_name("beta")["name"] == "beta"
        with pytest.raises(KeyError):
            ds.index_of("missing")
        with pytest.raises(IndexError):
            ds.molecule(9)


def test_decode_marker_to_nan():
    out = D._decode(np.array([0, 10000, MARKER], dtype=np.int32))
    assert out[1] == pytest.approx(1.0) and np.isnan(out[2])


@pytest.mark.skipif(not os.path.exists(REAL), reason="real delta50.hdf5 not present")
def test_real_file_smoke():
    with D.Delta50(REAL) as ds:
        assert len(ds) == 50
        assert "nitromethane" in ds.molecule_names
        # the DELTA50 nitrate-ester misnomer is corrected to the C-nitro name here
        assert "2-methyl-2-nitropropane" in ds.molecule_names
        assert "t-butyl nitrate" not in ds.molecule_names
        m = ds.molecule_by_name("nitromethane")
        assert np.isfinite(m["nn_magnet_zero"]).all()
        for key in ("shielding_wp04_pcSseg2", "shielding_wb97xd_pcSseg2", "experimental_shift"):
            assert m[key].shape == m["atomic_numbers"].shape
        # nitromethane: WP04 at H, wB97X-D at C, and an experimental shift on every H and C
        z = m["atomic_numbers"]
        assert np.isfinite(m["experimental_shift"][(z == 1) | (z == 6)]).all()
        assert ds.pcm_correction(ds.index_of("nitromethane")).shape == m["atomic_numbers"].shape
        atoms = ds.all_atoms()
        assert atoms["atomic_numbers"].shape == atoms["experimental_shift"].shape