MagNET / analysis /code /test_delta50_residuals.py
ekwan16's picture
sync code from github@6f6c7d83845ee0307e16b2b9bb1b389c5bfc5007
23c6f41 verified
Raw
History Blame Contribute Delete
6.95 kB
"""Tests for analysis/code/delta50_residuals.py.
Synthetic tests check residual selection and statistics on a hand-built shieldings dict. Opt-in
tests run against the real delta50.hdf5 and reproduce the paper's DELTA50 observation: MagNET-Zero
tracks its DFT reference to well under 0.5 ppm (1H) and a few ppm (13C), with nitromethane,
nitroethane, and 2-methyl-2-nitropropane the significant 13C outliers (up to ~7 ppm).
"""
import os
import sys
import numpy as np
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "..", "data", "delta50")) # decode_delta50
sys.path.insert(0, HERE)
import paths # noqa: E402
import delta50_residuals as D # noqa: E402
def make_shieldings():
"""Flat shieldings dict like the reader's all_atoms: H, C, C, H, N. MagNET-Zero is one array
(WP04 quality at H, wB97X-D quality at C); the DFT reference is split by nucleus."""
return {
"atomic_numbers": np.array([1, 6, 6, 1, 7]),
"shielding_wp04_pcSseg2": np.array([30.5, 150.5, 151.5, 31.5, 200.5]),
"shielding_wb97xd_pcSseg2": np.array([30.7, 150.7, 151.7, 31.7, 200.7]),
"nn_magnet_zero": np.array([30.6, 150.6, 151.6, 31.3, np.nan]),
}
def test_residuals_select_nucleus_against_the_right_dft_level():
s = make_shieldings()
h = D.residuals(s, "H") # DFT WP04 minus MagNET at the two H atoms (0, 3)
np.testing.assert_allclose(sorted(h), sorted([30.5 - 30.6, 31.5 - 31.3]))
c = D.residuals(s, "C") # DFT wB97X-D minus MagNET at the two C atoms (1, 2)
np.testing.assert_allclose(sorted(c), sorted([150.7 - 150.6, 151.7 - 151.6]))
assert len(h) == 2 and len(c) == 2
def test_residual_stats():
errors = np.array([-0.1, 0.2, 0.05, -0.05])
st = D.residual_stats(errors)
assert st["n"] == 4
assert st["rmse"] == pytest.approx(np.sqrt(np.mean(errors ** 2)))
assert st["mae"] == pytest.approx(np.mean(np.abs(errors)))
assert st["max_ae"] == pytest.approx(0.2)
def test_residual_stats_empty():
st = D.residual_stats(np.array([]))
assert st["n"] == 0 and np.isnan(st["rmse"])
# --- opt-in: reproduce the DELTA50 residual numbers from the released data -----------------------
REAL = paths.dataset_file("delta50", file=__file__)
@pytest.mark.skipif(not os.path.exists(REAL), reason="real delta50.hdf5 not present")
def test_reproduces_delta50_magnet_zero_vs_dft_residuals():
summary = D.residual_summary(REAL)
# 1H: MagNET-Zero tracks WP04 to ~0.05 ppm RMSE, max residual ~0.47 ppm ("up to 0.4 ppm")
assert summary["H"]["rmse"] == pytest.approx(0.055, abs=0.01)
assert summary["H"]["max_ae"] == pytest.approx(0.469, abs=0.02)
# 13C: RMSE ~0.61 ppm, max residual ~6.65 ppm ("up to 7 ppm")
assert summary["C"]["rmse"] == pytest.approx(0.607, abs=0.03)
assert summary["C"]["max_ae"] == pytest.approx(6.654, abs=0.05)
@pytest.mark.skipif(not os.path.exists(REAL), reason="real delta50.hdf5 not present")
def test_named_13c_outliers_are_the_three_nitroalkanes():
per_mol = D.per_molecule_max_residual(REAL)
top3 = sorted(per_mol, key=lambda nm: -per_mol[nm]["C"])[:3]
assert set(top3) == {"nitromethane", "nitroethane", "2-methyl-2-nitropropane"}
# nitromethane is the single largest 13C outlier, ~6.65 ppm
assert per_mol["nitromethane"]["C"] == pytest.approx(6.654, abs=0.05)
# nitrobenzene, by contrast, is accurately predicted (paper's explicit contrast)
assert per_mol["nitrobenzene"]["C"] < 0.5
# Golden experimental shifts (ppm) for the molecules whose atom order is most likely to be mismapped:
# the three symmetry-fallback cases and the near-degenerate distinct environments (amide N-methyls,
# diastereotopic methyls). Values are the published DELTA50 shifts; a mapping regression changes them.
_GOLDEN_MULTISET = {
"cyclohexanone": {"C": [25.02, 27.03, 27.03, 42.0, 42.0, 212.15]},
"methyl acetate": {"C": [20.71, 51.62, 171.55]},
"2-methyl-2-nitropropane": {"C": [27.87, 27.87, 27.87, 85.06]},
"n,n-dimethylformamide": {"C": [31.44, 36.48, 162.52]},
"n,n-dimethylacetamide": {"C": [21.58, 35.2, 38.05, 170.66]},
"2-methyl-2-butene": {"C": [13.41, 17.32, 25.63, 118.44, 132.1]},
}
# (0-based carbon index -> shift): pins each shift to a specific atom, so a swap of two same-value-set
# environments (e.g. the two DMF N-methyls) fails even though the multiset is unchanged.
_GOLDEN_ATOM = {
"n,n-dimethylformamide": {0: 162.52, 3: 31.44, 4: 36.48},
"n,n-dimethylacetamide": {1: 170.66, 3: 35.2, 4: 38.05, 5: 21.58},
"2-methyl-2-butene": {0: 17.32, 1: 132.1, 2: 118.44, 3: 13.41, 4: 25.63},
"cyclohexanone": {1: 212.15, 2: 42.0, 3: 42.0, 4: 27.03, 5: 27.03, 6: 25.02},
}
@pytest.mark.skipif(not os.path.exists(REAL), reason="real delta50.hdf5 not present")
def test_experimental_multiset_matches_published_delta50():
from decode_delta50 import Delta50
with Delta50(REAL) as ds:
for name, expected in _GOLDEN_MULTISET.items():
m = ds.molecule_by_name(name)
got = sorted(round(float(x), 2) for x in m["experimental_shift"][m["atomic_numbers"] == 6]
if np.isfinite(x))
assert got == sorted(expected["C"]), name
@pytest.mark.skipif(not os.path.exists(REAL), reason="real delta50.hdf5 not present")
def test_experimental_swap_sensitive_carbons_pinned_to_atoms():
from decode_delta50 import Delta50
with Delta50(REAL) as ds:
for name, atoms in _GOLDEN_ATOM.items():
m = ds.molecule_by_name(name)
for idx, shift in atoms.items():
assert m["atomic_numbers"][idx] == 6, (name, idx)
assert m["experimental_shift"][idx] == pytest.approx(shift, abs=1e-3), (name, idx)
@pytest.mark.skipif(not os.path.exists(REAL), reason="real delta50.hdf5 not present")
def test_experimental_shift_present_and_agrees_with_dft():
from decode_delta50 import Delta50
with Delta50(REAL) as ds:
atoms = ds.all_atoms()
z = atoms["atomic_numbers"]
exp = atoms["experimental_shift"]
# experimental is present for every H and C atom, absent for heteroatoms
assert np.isfinite(exp[(z == 1) | (z == 6)]).all()
assert np.isnan(exp[(z != 1) & (z != 6)]).all()
# A correct atom mapping makes scaled DFT track experiment; a scrambled one would not. These
# tolerances are loose (our DFT is gas phase on AIMNet2 geometries, not DELTA50's PCM level) but
# far below the ppm-scale RMSE a wrong assignment would produce.
for zz, key, tol in ((1, "shielding_wp04_pcSseg2", 0.5), (6, "shielding_wb97xd_pcSseg2", 5.0)):
m = (z == zz) & np.isfinite(exp)
sigma = atoms[key][m]
A = np.vstack([sigma, np.ones(m.sum())]).T
coef, *_ = np.linalg.lstsq(A, exp[m], rcond=None)
rmse = np.sqrt(np.mean((A @ coef - exp[m]) ** 2))
assert rmse < tol, f"Z={zz} scaled-DFT-vs-exp RMSE {rmse:.3f} too large"