| """ |
| Tests for the dft8k reader (dft8k_reader.py). |
| |
| Most tests build a tiny synthetic dft8k.hdf5 in the exact on-disk format (so the fixture |
| doubles as a spec of what the reader expects) and need no large data. One opt-in test runs |
| against the real dft8k.hdf5 if it is present (skipped otherwise, e.g. in CI). |
| |
| Run: pytest test_dft8k.py -q |
| Requires: pytest, numpy, h5py. |
| """ |
| import os |
| import numpy as np |
| import h5py |
| import pytest |
|
|
| from dft8k_reader import DFT8k, _decode, _MARKER |
|
|
| SCALE = 1e4 |
|
|
|
|
| def _enc(arr): |
| """Encode floats the way the build script does: int32 = round(value * 1e4), NaN -> marker.""" |
| a = np.asarray(arr, dtype=np.float64) |
| nan = np.isnan(a) |
| out = np.rint(np.where(nan, 0.0, a) * SCALE).astype(np.int32) |
| out[nan] = _MARKER |
| return out |
|
|
|
|
| |
| |
| |
| def test_decode_round_trip_and_marker(): |
| values = np.array([0.0, 207.07, -355.5, np.nan, 875.0], dtype=np.float64) |
| decoded = _decode(_enc(values)) |
| assert np.isnan(decoded[3]) |
| np.testing.assert_allclose(decoded[~np.isnan(decoded)], |
| values[~np.isnan(values)], atol=5e-5) |
|
|
|
|
| def test_decode_passes_through_float(): |
| |
| values = np.array([1.0, 2.5, -7.25], dtype=np.float64) |
| np.testing.assert_array_equal(_decode(values), values) |
|
|
|
|
| def test_decode_handles_int64(): |
| |
| vals = np.array([10000, -3550000, _MARKER], dtype=np.int64) |
| out = _decode(vals) |
| np.testing.assert_allclose(out[:2], [1.0, -355.0]) |
| assert np.isnan(out[2]) |
|
|
|
|
| |
| |
| |
| |
| A_IDS = [100, 200] |
| A_COUNTS = [2, 3] |
| A_Z = [1, 6, 6, 1, 7] |
| |
| A_PBE0 = [30.0, 150.0, 151.0, 31.0, 200.0] |
| A_WP04 = [30.5, 150.5, 151.5, 31.5, 200.5] |
| A_WB97 = [30.7, 150.7, 151.7, 31.7, 200.7] |
| |
| A_NN_WP04 = [30.6, np.nan, np.nan, 31.6, np.nan] |
| A_NN_WB97 = [np.nan, 150.6, 151.6, np.nan, np.nan] |
|
|
|
|
| def _make_synthetic_dft8k(path): |
| with h5py.File(path, "w") as f: |
| g = f.create_group("aimnet2_wp04_wb97xd_pcsseg2") |
| g.attrs["geometry"] = "AIMNet2" |
| g.attrs["level_of_theory"] = "PBE0/pcSseg-1; WP04/pcSseg-2; wB97X-D/pcSseg-2" |
| g.attrs["n_molecules"] = len(A_IDS) |
| g.attrs["n_atoms"] = sum(A_COUNTS) |
| g.attrs["scale"] = 1e-4 |
| g.create_dataset("molecule_ids", data=np.array(A_IDS, np.uint32)) |
| g.create_dataset("n_atoms", data=np.array(A_COUNTS, np.uint8)) |
| |
| g.create_dataset("smiles", data=np.array(["CC", "none"], dtype=object), |
| dtype=h5py.string_dtype(encoding="utf-8")) |
| g.create_dataset("atomic_numbers", data=np.array(A_Z, np.int8)) |
| coords = np.arange(sum(A_COUNTS) * 3, dtype=np.float64).reshape(-1, 3) * 0.1 |
| g.create_dataset("coordinates", data=_enc(coords)) |
| g.create_dataset("shielding_pbe0_pcSseg1", data=_enc(A_PBE0)) |
| g.create_dataset("shielding_wp04_pcSseg2", data=_enc(A_WP04)) |
| g.create_dataset("shielding_wb97xd_pcSseg2", data=_enc(A_WB97)) |
| g.create_dataset("nn_shielding_wp04_pcSseg2", data=_enc(A_NN_WP04)) |
| g.create_dataset("nn_shielding_wb97xd_pcSseg2", data=_enc(A_NN_WB97)) |
|
|
| |
| b = f.create_group("b3lyp_pbe0_pcsseg1") |
| b.attrs["geometry"] = "B3LYP/pcSseg-1" |
| b.attrs["level_of_theory"] = "PBE0/pcSseg-1 on B3LYP/pcSseg-1 geometries" |
| b.attrs["n_molecules"] = 2 |
| b.attrs["n_atoms"] = 5 |
| b.attrs["n_geometries"] = 3 |
| b.attrs["scale"] = 1e-4 |
| b.create_dataset("molecule_ids", data=np.array([200, 100], np.uint32)) |
| b.create_dataset("n_atoms", data=np.array([3, 2], np.uint8)) |
| b.create_dataset("atomic_numbers", data=np.array([6, 1, 7, 1, 6], np.int8)) |
| bcoords = np.arange(3 * 5 * 3, dtype=np.float64).reshape(3, 5, 3) * 0.01 |
| b.create_dataset("coordinates", data=_enc(bcoords)) |
| bshield = np.arange(3 * 5, dtype=np.float64).reshape(3, 5) + 100.0 |
| b.create_dataset("shielding", data=_enc(bshield)) |
| b.create_dataset("weights", data=np.array([1, 1, 1, 0.5, 0.5], np.float32)) |
| |
| |
| |
| b.create_dataset("symmetric_atoms_n_groups", data=np.array([2, 1], np.int32)) |
| b.create_dataset("symmetric_atoms_group_lengths", data=np.array([2, 1, 2], np.int32)) |
| b.create_dataset("symmetric_atoms_members", data=np.array([1, 2, 0, 0, 1], np.int32)) |
|
|
|
|
| def test_aimnet2_group(tmp_path): |
| p = tmp_path / "dft8k.hdf5" |
| _make_synthetic_dft8k(str(p)) |
| with DFT8k(str(p)) as ds: |
| assert len(ds.aimnet2) == 2 |
| m = ds.aimnet2.molecule(0) |
| assert m["id"] == 100 |
| assert m["smiles"] == "CC" |
| assert ds.aimnet2.smiles(1) == "none" |
| np.testing.assert_array_equal(m["atomic_numbers"], [1, 6]) |
| np.testing.assert_allclose(m["wp04_pcSseg2"], [30.5, 150.5], atol=5e-5) |
| |
| assert not np.isnan(m["nn_wp04_pcSseg2"][0]) |
| assert np.isnan(m["nn_wp04_pcSseg2"][1]) |
| |
| assert np.isnan(m["nn_wb97xd_pcSseg2"][0]) |
| assert not np.isnan(m["nn_wb97xd_pcSseg2"][1]) |
| np.testing.assert_allclose(m["nn_wb97xd_pcSseg2"][1], 150.6, atol=5e-5) |
|
|
|
|
| def test_all_shieldings_flat(tmp_path): |
| p = tmp_path / "dft8k.hdf5" |
| _make_synthetic_dft8k(str(p)) |
| with DFT8k(str(p)) as ds: |
| flat = ds.aimnet2.all_shieldings() |
| |
| np.testing.assert_array_equal(flat["atomic_numbers"], [1, 6, 6, 1, 7]) |
| np.testing.assert_allclose(flat["wp04_pcSseg2"], A_WP04, atol=5e-5) |
| |
| assert np.isnan(flat["nn_wp04_pcSseg2"]).tolist() == [False, True, True, False, True] |
| |
| Z = flat["atomic_numbers"] |
| h = (Z == 1) & np.isfinite(flat["nn_wp04_pcSseg2"]) |
| np.testing.assert_allclose(flat["wp04_pcSseg2"][h] - flat["nn_wp04_pcSseg2"][h], |
| [-0.1, -0.1], atol=5e-5) |
|
|
|
|
| def test_b3lyp_group_and_symmetry(tmp_path): |
| p = tmp_path / "dft8k.hdf5" |
| _make_synthetic_dft8k(str(p)) |
| with DFT8k(str(p)) as ds: |
| b = ds.b3lyp |
| assert b.n_geometries == 3 |
| m = b.molecule(0) |
| assert m["id"] == 200 |
| assert m["coordinates"].shape == (3, 3, 3) |
| assert m["shielding"].shape == (3, 3) |
| assert m["symmetric_atoms"] == [[1, 2], [0]] |
| m2 = b.molecule(1) |
| assert m2["symmetric_atoms"] == [[0, 1]] |
| np.testing.assert_allclose(m2["weights"], [0.5, 0.5]) |
| |
| for group in m["symmetric_atoms"] + m2["symmetric_atoms"]: |
| for atom_index in group: |
| assert 0 <= atom_index < len(m["atomic_numbers"]) or \ |
| 0 <= atom_index < len(m2["atomic_numbers"]) |
|
|
|
|
| def test_match_across_groups_by_id(tmp_path): |
| p = tmp_path / "dft8k.hdf5" |
| _make_synthetic_dft8k(str(p)) |
| with DFT8k(str(p)) as ds: |
| |
| a = ds.aimnet2.molecule_by_id(200) |
| b = ds.b3lyp.molecule_by_id(200) |
| assert a["id"] == b["id"] == 200 |
| np.testing.assert_array_equal(a["atomic_numbers"], b["atomic_numbers"]) |
|
|
|
|
| |
| |
| |
| _HERE = os.path.dirname(os.path.abspath(__file__)) |
| _REAL = os.path.join(_HERE, "dft8k.hdf5") |
|
|
|
|
| @pytest.mark.skipif(not os.path.exists(_REAL), reason="real dft8k.hdf5 not present") |
| def test_real_dft8k(): |
| with DFT8k(_REAL) as ds: |
| assert len(ds.aimnet2) == 7111 |
| assert len(ds.b3lyp) == 7111 |
| assert ds.b3lyp.n_geometries == 3 |
| |
| assert set(int(x) for x in ds.aimnet2.molecule_ids) == \ |
| set(int(x) for x in ds.b3lyp.molecule_ids) |
| m = ds.aimnet2.molecule(0) |
| |
| n_real_smiles = sum(1 for i in range(len(ds.aimnet2)) if ds.aimnet2.smiles(i) != "none") |
| assert n_real_smiles == 6780 |
| assert isinstance(m["smiles"], str) and m["smiles"] != "none" |
| z = m["atomic_numbers"] |
| h = z == 1 |
| c = z == 6 |
| other = ~(h | c) |
| |
| |
| assert not np.isnan(m["nn_wp04_pcSseg2"][h]).any() |
| assert np.isnan(m["nn_wp04_pcSseg2"][~h]).all() |
| assert not np.isnan(m["nn_wb97xd_pcSseg2"][c]).any() |
| assert np.isnan(m["nn_wb97xd_pcSseg2"][~c]).all() |
| assert other.sum() == 0 or np.isnan(m["nn_wp04_pcSseg2"][other]).all() |
| |
| b = ds.b3lyp.molecule_by_id(m["id"]) |
| np.testing.assert_array_equal(m["atomic_numbers"], b["atomic_numbers"]) |
| |
| for group in b["symmetric_atoms"]: |
| for atom_index in group: |
| assert 0 <= atom_index < len(b["atomic_numbers"]) |
|
|