| """Computes delta-22's MagNET-Zero / MagNET-PCM shieldings via live, symmetrized inference, |
| instead of reading the pre-baked (unsymmetrized, single-pass) values stored in delta22.hdf5. |
| |
| Why: `data/delta22/delta22_reader.py::load_stationary_and_pcm_nn_shieldings` decodes |
| `nn_gas_shieldings`/`nn_pcm_corrections` straight out of the HDF5. Those were computed with a |
| single, unsymmetrized forward pass (see `magnet/run_magnet.py`'s module docstring: "The released |
| reference shieldings are NOT symmetrized"). Isotropic shielding is parity-even, so the SO(3)-only |
| backbone's un-symmetrized predictions carry a spurious reflection error that is not always |
| negligible, even on small molecules (delta-22's solutes are 6-22 atoms). `predict_shieldings(..., |
| symmetrize=True)` fixes this for free by averaging the prediction on the original geometry and its |
| mirror image. |
| |
| This module reproduces the SI's published Tables S10/S11 (they ARE the reflection-symmetrized tables, |
| shipped in data/scaling_factors/scaling_factors_symmetrized_{H,C}.csv). |
| scaling_factors.build_scaling_tables(symmetrized=True) calls in here to regenerate them from live |
| inference; its checkpoint-free symmetrized=False path fits the raw single-pass HDF5 shieldings instead |
| and lands ~0.01 ppm off. |
| """ |
| import contextlib |
| import os |
| import time |
|
|
| import h5py |
| import numpy as np |
| import pandas as pd |
|
|
| from paths import repo_root, ensure_on_path, checkpoints_root |
|
|
| _REPO = repo_root(__file__) |
| |
| |
| |
| |
| ensure_on_path(root=_REPO) |
| ensure_on_path("magnet", file=__file__) |
| ensure_on_path("data", "delta22", file=__file__) |
|
|
| import run_magnet |
| from run_magnet import compute_MagNET_Zero_shieldings, compute_MagNET_PCM_corrections |
| from delta22_reader import load_solutes, load_stationary_geometries |
|
|
| @contextlib.contextmanager |
| def _resolved_checkpoints(): |
| """Temporarily points run_magnet.MODEL_CHECKPOINTS at absolute paths, restoring the original |
| (relative-path) dict afterward -- run_magnet.MODEL_CHECKPOINTS is a shared module-level dict, |
| and other code importing run_magnet in the same process expects the original relative paths. |
| Its values already start with "model_checkpoints/", so join each against the repo root.""" |
| base = os.path.dirname(checkpoints_root(required=True)) |
| original = run_magnet.MODEL_CHECKPOINTS |
| run_magnet.MODEL_CHECKPOINTS = { |
| key: os.path.join(base, rel_path) for key, rel_path in original.items() |
| } |
| try: |
| yield |
| finally: |
| run_magnet.MODEL_CHECKPOINTS = original |
|
|
| _NN_COLUMNS = ["solute", "sap_geometry_type", "sap_nmr_method", "sap_basis", |
| "solvent_model", "solvent", "shieldings", "geometry_time", "nmr_time"] |
|
|
|
|
| def _load_atomic_numbers(delta22_path, solute): |
| with h5py.File(delta22_path, "r") as f: |
| return np.array(f["solutes"][solute]["atomic_numbers"]) |
|
|
|
|
| def _load_aimnet2_geometry_time(delta22_path, solute): |
| with h5py.File(delta22_path, "r") as f: |
| return float(np.array(f["solutes"][solute]["stationary_and_pcm"]["geometry_optimization_timings"])[0]) |
|
|
|
|
| def compute_symmetrized_nn_shieldings_df(delta22_path, n_passes=10, device=None, verbose=True): |
| """Same row shape as load_stationary_and_pcm_nn_shieldings (one "gas" + one "pcm_correction" |
| row per solute), but the shieldings come from live predict_shieldings(symmetrize=True) |
| inference instead of the HDF5's stored, unsymmetrized values. |
| |
| n_passes=10 with symmetrize=True means 10 forward passes on the original geometry and 10 on |
| its mirror image, all 20 averaged together. |
| |
| Returns a DataFrame with the same 9 columns load_stationary_and_pcm_nn_shieldings produces; |
| pass it as `nn_shieldings_override_df` to delta22_reader.load_delta22_nn_data. |
| """ |
| solutes = load_solutes(delta22_path) |
| atomic_numbers_list = [_load_atomic_numbers(delta22_path, s) for s in solutes] |
| geometries_list = [load_stationary_geometries(delta22_path, s)["aimnet2"] for s in solutes] |
|
|
| if verbose: |
| print(f"computing symmetrized MagNET-Zero/PCM shieldings for {len(solutes)} solutes " |
| f"(n_passes={n_passes}, symmetrize=True)...", flush=True) |
|
|
| t0 = time.time() |
| with _resolved_checkpoints(): |
| zero_shieldings = compute_MagNET_Zero_shieldings( |
| atomic_numbers_list, geometries_list, n_passes=n_passes, symmetrize=True, device=device) |
| t1 = time.time() |
| pcm_corrections = compute_MagNET_PCM_corrections( |
| atomic_numbers_list, geometries_list, n_passes=n_passes, symmetrize=True, device=device) |
| t2 = time.time() |
|
|
| if verbose: |
| print(f" MagNET-Zero: {t1-t0:.1f}s total, MagNET-PCM: {t2-t1:.1f}s total", flush=True) |
|
|
| zero_time_per_solute = (t1 - t0) / len(solutes) |
| pcm_time_per_solute = (t2 - t1) / len(solutes) |
|
|
| rows = [] |
| for solute, gas, pcm in zip(solutes, zero_shieldings, pcm_corrections): |
| geom_time = _load_aimnet2_geometry_time(delta22_path, solute) |
| rows.append([solute, "aimnet2", "MagNET", "N/A", "gas", "none", |
| np.asarray(gas, dtype=float), geom_time, zero_time_per_solute]) |
| rows.append([solute, "aimnet2", "MagNET", "N/A", "pcm_correction", "chloroform", |
| np.asarray(pcm, dtype=float), geom_time, pcm_time_per_solute]) |
|
|
| return pd.DataFrame(rows, columns=_NN_COLUMNS) |
|
|