| """MagNET-Zero vs DFT residuals on the DFT8K benchmark (~7,000 organics, out of sample). No plotting. |
| |
| Each nucleus is compared against the level MagNET-Zero reproduces: protons vs WP04/pcSseg-2, carbons |
| vs wB97X-D/pcSseg-2, gas phase on AIMNet2 geometries; residual = DFT shielding minus MagNET. Read via |
| data/dft8k/dft8k_reader.py. The figure notebook and tests import from here. |
| """ |
| import numpy as np |
|
|
| from stats import summarize_errors |
|
|
| |
| NUCLEI = { |
| "H": {"atomic_number": 1, "dft": "wp04_pcSseg2", "nn": "nn_wp04_pcSseg2"}, |
| "C": {"atomic_number": 6, "dft": "wb97xd_pcSseg2", "nn": "nn_wb97xd_pcSseg2"}, |
| } |
|
|
|
|
| def load_shieldings(dft8k_path): |
| """The flat per-atom shielding arrays from the DFT8K AIMNet2 group, through the released |
| reader. Returns the reader's all_shieldings dict (atomic_numbers plus the DFT and MagNET |
| shieldings, each ppm, NaN where not computed).""" |
| from dft8k_reader import DFT8k |
| with DFT8k(dft8k_path) as ds: |
| return ds.aimnet2.all_shieldings() |
|
|
|
|
| def residuals(shieldings, nucleus): |
| """The DFT-minus-MagNET shielding residuals (ppm) for one nucleus, over the atoms of that |
| element where MagNET makes a prediction. shieldings is the load_shieldings dict; nucleus is |
| "H" or "C". Atoms the model does not predict (its prediction is NaN) are dropped.""" |
| spec = NUCLEI[nucleus] |
| z = np.asarray(shieldings["atomic_numbers"]) |
| dft = np.asarray(shieldings[spec["dft"]], dtype=float) |
| nn = np.asarray(shieldings[spec["nn"]], dtype=float) |
| keep = (z == spec["atomic_number"]) & np.isfinite(dft) & np.isfinite(nn) |
| return dft[keep] - nn[keep] |
|
|
|
|
| def residual_stats(errors, small_threshold=0.1): |
| """Summary statistics of a residual array: count, RMSE, MAE, the 95th percentile of the |
| absolute error, and the fraction of atoms whose absolute error is below small_threshold ppm |
| (the paper reports >95% of proton sites below 0.1 ppm).""" |
| errors = np.asarray(errors, dtype=float) |
| abs_err = np.abs(errors) |
| if not errors.size: |
| return {"n": 0, "rmse": float("nan"), "mae": float("nan"), "median_ae": float("nan"), |
| "abs_p95": float("nan"), "frac_below": float("nan")} |
| stats = summarize_errors(errors, np.zeros_like(errors)) |
| stats["abs_p95"] = float(np.quantile(abs_err, 0.95)) |
| stats["frac_below"] = float(np.mean(abs_err < small_threshold)) |
| return stats |
|
|
|
|
| def residual_summary(dft8k_path, small_threshold=0.1): |
| """Load the DFT8K shieldings and return {nucleus: residual_stats} for both nuclei.""" |
| shieldings = load_shieldings(dft8k_path) |
| return {nucleus: residual_stats(residuals(shieldings, nucleus), small_threshold) |
| for nucleus in NUCLEI} |
|
|
|
|
| |
| |
| |
| |
|
|
| def iter_molecule_residuals(dft8k_path, nucleus): |
| """Yield (molecule_index, molecule_id, smiles, residuals) for every DFT8K molecule, where |
| residuals is that molecule's DFT-minus-MagNET residual array (ppm) for its atoms of the given |
| nucleus (empty if the molecule has none of that element, or MagNET made no prediction there). |
| Molecules with no SMILES on file (the literal "none") are still yielded; callers that need a |
| parseable structure should skip those themselves.""" |
| from dft8k_reader import DFT8k |
| spec = NUCLEI[nucleus] |
| with DFT8k(dft8k_path) as ds: |
| g = ds.aimnet2 |
| |
| |
| |
| flat = g.all_shieldings() |
| z_all, dft_all, nn_all = flat["atomic_numbers"], flat[spec["dft"]], flat[spec["nn"]] |
| start = g._start |
| for i in range(g.n_molecules): |
| sl = slice(int(start[i]), int(start[i + 1])) |
| z, dft, nn = z_all[sl], dft_all[sl], nn_all[sl] |
| keep = (z == spec["atomic_number"]) & np.isfinite(dft) & np.isfinite(nn) |
| yield i, int(g.molecule_ids[i]), g.smiles(i), (dft[keep] - nn[keep]) |
|
|
|
|
| def find_extreme_residual(dft8k_path, nucleus, sign="max"): |
| """The single most extreme DFT-minus-MagNET residual anywhere in the DFT8K set for one |
| nucleus: the largest positive residual if sign="max", or the most negative if sign="min". The |
| exact extremum over the full released test set (not a random sample), so it is deterministic. |
| For 1H, sign="max" reproduces the published figure's positive-tail callout exactly (a porphyrin |
| ring, +17.62 ppm). sign="min" gives a different, chemically unremarkable ester outlier at |
| -4.06 ppm rather than the figure's negative-tail callout (a sulfonium zwitterion, -1.04 ppm): |
| that callout is a hand-picked illustrative example, not the strict extremum. See |
| molecule_by_id/find_extreme_zwitterion_residual for how that specific example was located. |
| Returns a dict with the parent molecule's id/smiles and the residual value, or None if no atom |
| of that nucleus has a prediction anywhere in the set.""" |
| if sign not in ("max", "min"): |
| raise ValueError(f"sign must be 'max' or 'min', got {sign!r}") |
| best = None |
| for i, mol_id, smiles, res in iter_molecule_residuals(dft8k_path, nucleus): |
| if res.size == 0: |
| continue |
| val = float(res.max()) if sign == "max" else float(res.min()) |
| if best is None or (sign == "max" and val > best["residual"]) or (sign == "min" and val < best["residual"]): |
| best = {"molecule_index": i, "molecule_id": int(mol_id), "smiles": smiles, "residual": val} |
| return best |
|
|
|
|
| def molecule_by_id(dft8k_path, nucleus, molecule_id): |
| """The residual data for one specific DFT8K molecule, by its integer id. Used to look up |
| Figure 5B's sulfonium-zwitterion callout (id 88779, identified by brute-force scanning the |
| most-negative 1H residuals for a match to the published -1.040 ppm value -- see |
| find_extreme_zwitterion_residual for the automated version of that search) once its id is |
| known, without re-scanning the whole dataset. Returns the same dict shape as |
| find_extreme_residual (residual is that molecule's single most negative residual for the given |
| nucleus), or None if the molecule has no atom of that nucleus with a prediction.""" |
| for i, mol_id, smiles, res in iter_molecule_residuals(dft8k_path, nucleus): |
| if mol_id == molecule_id: |
| if res.size == 0: |
| return None |
| return {"molecule_index": i, "molecule_id": int(mol_id), "smiles": smiles, |
| "residual": float(res.min())} |
| return None |
|
|
|
|
| def find_extreme_zwitterion_residual(dft8k_path, nucleus, sign="min"): |
| """Like find_extreme_residual, but restricted to molecules containing BOTH a formally |
| positively-charged sulfur ([S+]) and a formally negatively-charged atom ([-]) -- i.e. |
| sulfonium zwitterions. Even this narrower filter does not land on the same molecule as the |
| published callout: a different, more negative sulfonium zwitterion exists in the set. Provided |
| for exploration, not as an exact reproduction. The published example itself is available via |
| molecule_by_id(..., molecule_id=88779).""" |
| from rdkit import Chem |
| if sign not in ("max", "min"): |
| raise ValueError(f"sign must be 'max' or 'min', got {sign!r}") |
| pos_s = Chem.MolFromSmarts("[S+]") |
| neg = Chem.MolFromSmarts("[-]") |
| best = None |
| for i, mol_id, smiles, res in iter_molecule_residuals(dft8k_path, nucleus): |
| if res.size == 0 or smiles in _UNPARSEABLE_SMILES: |
| continue |
| mol = Chem.MolFromSmiles(smiles) |
| if mol is None or not (mol.HasSubstructMatch(pos_s) and mol.HasSubstructMatch(neg)): |
| continue |
| val = float(res.max()) if sign == "max" else float(res.min()) |
| if best is None or (sign == "max" and val > best["residual"]) or (sign == "min" and val < best["residual"]): |
| best = {"molecule_index": i, "molecule_id": int(mol_id), "smiles": smiles, "residual": val} |
| return best |
|
|
|
|
| |
| |
| |
| |
| _UNPARSEABLE_SMILES = {"none", "nan", ""} |
|
|
|
|
| |
| |
| |
| |
| |
| |
| FUNCTIONAL_GROUP_SMARTS = { |
| "Carbonyls": "[CX3]=[OX1]", |
| "Amines": "[NX3;H2,H1,H0;!$(NC=O);!$(N=[!#6]);!a]", |
| "Sulfonyl": "[SX4](=[OX1])(=[OX1])", |
| "Pyridines": "[n;r6]", |
| "Furans": "[o;r5]", |
| "Nitroso": "[NX2]=[OX1]", |
| } |
|
|
|
|
| def functional_group_errors(dft8k_path, nucleus, patterns=None): |
| """Mean absolute DFT-minus-MagNET residual (ppm), for every atom of `nucleus`, in every DFT8K |
| molecule whose SMILES matches each functional-group SMARTS pattern (default |
| FUNCTIONAL_GROUP_SMARTS). A molecule can match more than one group (e.g. a pyridine bearing a |
| carbonyl counts in both), matching the natural reading of the published panel's per-group boxes |
| as independent slices of the same dataset, not a mutually exclusive partition. |
| |
| Returns {group_name: {"mean_abs_error": ppm, "n_molecules": matching molecule count, |
| "n_atoms": total atoms of this nucleus pooled across those molecules}}. A group with no matches |
| in the released set gets NaN/0/0. |
| """ |
| from rdkit import Chem |
| if patterns is None: |
| patterns = FUNCTIONAL_GROUP_SMARTS |
| compiled = {name: Chem.MolFromSmarts(smarts) for name, smarts in patterns.items()} |
| for name, patt in compiled.items(): |
| if patt is None: |
| raise ValueError(f"invalid SMARTS for group {name!r}: {patterns[name]!r}") |
| pooled_residuals = {name: [] for name in patterns} |
| n_molecules = {name: 0 for name in patterns} |
| for _, _, smiles, res in iter_molecule_residuals(dft8k_path, nucleus): |
| if smiles in _UNPARSEABLE_SMILES or res.size == 0: |
| continue |
| mol = Chem.MolFromSmiles(smiles) |
| if mol is None: |
| continue |
| for name, patt in compiled.items(): |
| if mol.HasSubstructMatch(patt): |
| pooled_residuals[name].append(res) |
| n_molecules[name] += 1 |
| out = {} |
| for name in patterns: |
| chunks = pooled_residuals[name] |
| if chunks: |
| all_res = np.concatenate(chunks) |
| out[name] = {"mean_abs_error": float(np.mean(np.abs(all_res))), |
| "n_molecules": n_molecules[name], "n_atoms": int(all_res.size)} |
| else: |
| out[name] = {"mean_abs_error": float("nan"), "n_molecules": 0, "n_atoms": 0} |
| return out |
|
|