ekwan16 commited on
Commit
64c992d
·
verified ·
1 Parent(s): fefb9a6

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/workflows/ci.yml +44 -0
  2. .github/workflows/publish-pypi.yml +34 -0
  3. .github/workflows/sync-to-hf.yml +51 -0
  4. analysis/code/build_nb_dataset_summary.py +90 -0
  5. analysis/code/build_nb_leveling.py +295 -0
  6. analysis/code/carbon_monoxide.py +63 -0
  7. analysis/code/cp3.py +55 -0
  8. analysis/code/delta22_plots.py +606 -0
  9. analysis/code/shared/fixed_point.py +21 -0
  10. analysis/code/shared/spreadsheet.py +8 -0
  11. analysis/code/shared/stats.py +58 -0
  12. analysis/code/shared/test_fixed_point.py +42 -0
  13. analysis/code/shared/test_spreadsheet.py +19 -0
  14. analysis/code/shared/test_stats.py +46 -0
  15. analysis/code/test_cp3.py +39 -0
  16. analysis/code/test_magnet_benchmark.py +125 -0
  17. analysis/code/test_paths.py +94 -0
  18. analysis/code/test_scaling_factors.py +272 -0
  19. analysis/manuscript_figures/fig1a_cp3.ipynb +143 -0
  20. analysis/manuscript_figures/fig2a_pareto.ipynb +82 -0
  21. analysis/manuscript_figures/fig2b_correlation.ipynb +111 -0
  22. analysis/manuscript_figures/fig2d_convergence.ipynb +148 -0
  23. analysis/manuscript_figures/fig3a_pcm_benefit.ipynb +151 -0
  24. analysis/manuscript_figures/fig3b_shifts.ipynb +105 -0
  25. analysis/manuscript_figures/fig3d_solvent_corrections.ipynb +149 -0
  26. analysis/manuscript_figures/fig4a_implicit.ipynb +113 -0
  27. analysis/manuscript_figures/fig4b_explicit.ipynb +113 -0
  28. analysis/manuscript_figures/fig4c_rmse_range.ipynb +127 -0
  29. analysis/manuscript_figures/fig5b_dft8k.ipynb +188 -0
  30. analysis/manuscript_figures/fig5c.ipynb +115 -0
  31. analysis/manuscript_figures/fig5d.ipynb +145 -0
  32. analysis/si_figures/si_composite_models_ablations.ipynb +176 -0
  33. analysis/si_figures/si_figure_s01.ipynb +85 -0
  34. analysis/si_figures/si_figure_s02_diels_alder.ipynb +130 -0
  35. analysis/si_figures/si_figure_s04.ipynb +167 -0
  36. analysis/si_figures/si_figure_s05.ipynb +124 -0
  37. analysis/si_figures/si_figure_s06.ipynb +152 -0
  38. analysis/si_figures/si_figure_s07.ipynb +100 -0
  39. analysis/si_figures/si_figure_s08_panelA_vomicine.ipynb +121 -0
  40. analysis/si_figures/si_figure_s08_panelsBCD.ipynb +248 -0
  41. analysis/si_figures/si_figure_s15.ipynb +286 -0
  42. analysis/si_figures/si_figure_s16_s18.ipynb +143 -0
  43. analysis/si_tables/si_table_s01_s02_pareto.ipynb +125 -0
  44. analysis/si_tables/si_table_s03_s04_performance.ipynb +122 -0
  45. analysis/si_tables/si_table_s05_qcd.ipynb +115 -0
  46. analysis/si_tables/si_table_s06_solvent_corrections.ipynb +115 -0
  47. analysis/si_tables/si_table_s08_summary.ipynb +102 -0
  48. analysis/si_tables/si_table_s10_s11_scaling.ipynb +161 -0
  49. data/scaling_factors/scaling_factors_symmetrized_H.csv +13 -0
  50. data/scaling_factors/test_scaling_factors_export.py +52 -0
.github/workflows/ci.yml ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ python-version: ["3.9", "3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install dependencies
20
+ run: pip install -r requirements.txt "pytest>=7"
21
+ - name: Run tests
22
+ run: pytest -q
23
+ # Tests run against a synthetic fixture; tests requiring the large
24
+ # Hugging Face-hosted .hdf5 files skip automatically when those files are absent.
25
+
26
+ # The job above never installs torch, so conftest.py skips the whole magnet/ package. This job
27
+ # installs the CPU inference stack and actually imports + tests magnet/, catching packaging and API
28
+ # regressions (e.g. a clean-install `import magnet` failure). The weights-gated inference tests still
29
+ # skip here (checkpoints are not in the repo); everything else runs.
30
+ model:
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ - uses: actions/setup-python@v5
35
+ with:
36
+ python-version: "3.12"
37
+ - name: Install the CPU inference stack
38
+ run: |
39
+ pip install torch==2.5.0 --index-url https://download.pytorch.org/whl/cpu
40
+ pip install -r magnet/requirements.txt
41
+ pip install --no-deps ./magnet
42
+ pip install "pytest>=7"
43
+ - name: Run the magnet package tests
44
+ run: pytest magnet/ -q
.github/workflows/publish-pypi.yml ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: publish to PyPI
2
+
3
+ # Builds the magnet-nmr package and publishes it to PyPI when you push a version tag (e.g. v0.1.0).
4
+ #
5
+ # Uses PyPI Trusted Publishing (OIDC), so there is no token to store: configure the trusted publisher
6
+ # once at https://pypi.org/manage/project/magnet-nmr/settings/publishing/ with
7
+ # owner=ekwan repo=MagNET workflow=publish-pypi.yml environment=pypi
8
+ #
9
+ # The package lives in the magnet/ subdirectory (its own pyproject.toml), so every step runs there.
10
+
11
+ on:
12
+ push:
13
+ tags: ["v*"]
14
+
15
+ jobs:
16
+ publish:
17
+ runs-on: ubuntu-latest
18
+ environment: pypi
19
+ permissions:
20
+ id-token: write # required for Trusted Publishing; no API token needed
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: "3.12"
26
+ - name: Build sdist and wheel
27
+ working-directory: magnet
28
+ run: |
29
+ pip install build
30
+ python -m build
31
+ - name: Publish to PyPI
32
+ uses: pypa/gh-action-pypi-publish@release/v1
33
+ with:
34
+ packages-dir: magnet/dist/
.github/workflows/sync-to-hf.yml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: sync code to hugging face
2
+
3
+ # Mirrors the CODE to the Hugging Face model repo ekwan16/MagNET on every push to main.
4
+ #
5
+ # The large model weights and datasets live ONLY on Hugging Face (as Git LFS objects). They are
6
+ # gitignored here, so a fresh checkout never contains them, and they are pushed once by hand from a
7
+ # workstation (see publish/build_hf_repo.py). This job must therefore NEVER delete anything on the
8
+ # Hub.
9
+ #
10
+ # `hf upload` is additive/overwrite-only: it removes remote files ONLY when given an explicit
11
+ # --delete flag.
12
+ # ==> NEVER add a --delete flag below. It would wipe the ~42 GB of LFS weights and datasets. <==
13
+ # The --exclude patterns below are defense-in-depth for the big-file extensions (a fresh checkout
14
+ # has none of them anyway); the actual safety comes from omitting --delete.
15
+
16
+ on:
17
+ push:
18
+ branches: [main]
19
+
20
+ concurrency:
21
+ group: sync-to-hf
22
+ cancel-in-progress: true
23
+
24
+ jobs:
25
+ sync:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: "3.12"
32
+ - name: Install the Hugging Face CLI
33
+ # Pinned as defense-in-depth so a future CLI default cannot silently change behavior.
34
+ # Tighten to the exact version you tested with once you have run this successfully.
35
+ run: pip install "huggingface_hub[cli]>=0.34,<1.0"
36
+ - name: Ensure the target repo exists and is private
37
+ # `hf upload` auto-creates a MISSING repo as PUBLIC. If this Action ever fires before the
38
+ # repo has been seeded by hand, this guard creates it private first so nothing is exposed
39
+ # early. If the repo already exists this create call errors and `|| true` ignores it (it
40
+ # never changes an existing repo's visibility); a bad token still fails loudly at upload.
41
+ env:
42
+ HF_TOKEN: ${{ secrets.HF_TOKEN }} # must be a WRITE token for ekwan16/MagNET
43
+ run: hf repo create ekwan16/MagNET --repo-type=model --private || true
44
+ - name: Upload code to the Hub (no deletions)
45
+ env:
46
+ HF_TOKEN: ${{ secrets.HF_TOKEN }} # must be a WRITE token for ekwan16/MagNET
47
+ run: |
48
+ hf upload ekwan16/MagNET . . \
49
+ --repo-type=model \
50
+ --exclude ".git/**" ".github/**" "*.hdf5" "*.mnova" "*.ckpt" \
51
+ --commit-message "sync code from github@${{ github.sha }}"
analysis/code/build_nb_dataset_summary.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Source of truth for the Table S8 notebook (Dataset Summary Statistics). Edit the cell sources
2
+ here, then regenerate:
3
+
4
+ python3 build_nb_dataset_summary.py si_table_s08_summary
5
+ jupyter nbconvert --to notebook --execute --inplace ../si_tables/si_table_s08_summary.ipynb
6
+
7
+ Regenerating overwrites the .ipynb (clearing its execution outputs). All real code lives in
8
+ dataset_summary.py (the counting logic and the PUBLISHED_S8 reference values); the notebook only
9
+ runs it over the released data/ files. The notebook lives in analysis/si_tables/ (grouped by role), not here.
10
+
11
+ Run with no arguments to list the available notebook names.
12
+ """
13
+ import os
14
+ import sys
15
+
16
+ from nb_build import md, code, save_notebook as _save
17
+
18
+
19
+ _BOOTSTRAP = r"""
20
+ import os, sys
21
+
22
+ # make the in-repo modules importable (not pip-installed)
23
+ REPO = os.path.abspath("../..")
24
+ for _p in ("analysis/code", "analysis/code/shared"):
25
+ sys.path.insert(0, os.path.join(REPO, _p))
26
+ """
27
+
28
+ _IMPORTS = r"""
29
+ import pandas as pd
30
+ import dataset_summary
31
+ """
32
+
33
+ _SETUP = r"""
34
+ DATA_DIR = os.path.join(REPO, "data")
35
+
36
+ def document_path(name):
37
+ os.makedirs("documents", exist_ok=True)
38
+ return os.path.join("documents", name)
39
+ """
40
+
41
+ si_table_s08_summary = [
42
+ md(r"""
43
+ # Table S8: Dataset Summary Statistics
44
+
45
+ Molecule and ¹H/¹³C site counts for each training dataset (site counts from each HDF5's
46
+ `atomic_numbers`, ¹H=1/¹³C=6; MagNET-Zero combines both sigma-pepper rounds with sigma-concentrate).
47
+ """),
48
+ code(_BOOTSTRAP),
49
+ code(_IMPORTS),
50
+ code(_SETUP),
51
+ code(r"""
52
+ table_s8 = dataset_summary.summary_table(DATA_DIR)
53
+ display(table_s8)
54
+
55
+ # write the table to this notebook's documents/ folder
56
+ out = document_path("si_table_s08_summary.xlsx")
57
+ with pd.ExcelWriter(out) as writer:
58
+ table_s8.to_excel(writer, sheet_name="Table S8", index=False)
59
+ print("wrote", os.path.relpath(out, REPO))
60
+ """),
61
+ md("## Exact-reproduction check"),
62
+ code(r"""
63
+ # every count should match the published SI Table S8 value exactly
64
+ for _, row in table_s8.iterrows():
65
+ pub = dataset_summary.PUBLISHED_S8[row["dataset"]]
66
+ got = (row["molecules"], row["n_1H_sites"], row["n_13C_sites"])
67
+ assert got == pub, f"{row['dataset']}: {got} != published {pub}"
68
+ print("all rows match the published SI Table S8 exactly")
69
+ """),
70
+ ]
71
+
72
+
73
+ # name -> (cells, path relative to repo root)
74
+ NOTEBOOKS = {
75
+ "si_table_s08_summary": (si_table_s08_summary, "analysis/si_tables/si_table_s08_summary.ipynb"),
76
+ }
77
+
78
+ if __name__ == "__main__":
79
+ here = os.path.dirname(os.path.abspath(__file__))
80
+ repo = os.path.abspath(os.path.join(here, "..", ".."))
81
+ names = sys.argv[1:]
82
+ if not names:
83
+ print("available names:", ", ".join(NOTEBOOKS))
84
+ sys.exit(0)
85
+ unknown = [n for n in names if n not in NOTEBOOKS]
86
+ if unknown:
87
+ raise SystemExit(f"unknown notebook name(s): {unknown}; available: {', '.join(NOTEBOOKS)}")
88
+ for name in names:
89
+ cells, relpath = NOTEBOOKS[name]
90
+ _save(cells, os.path.join(repo, relpath))
analysis/code/build_nb_leveling.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # build_nb_leveling.py -- generator for the SI Figure S3 leveling notebook.
2
+ #
3
+ # Source of truth for the notebook. Edit the cell sources here, then regenerate and re-execute:
4
+ #
5
+ # python3 build_nb_leveling.py si_figure_s03_leveling
6
+ # jupyter nbconvert --to notebook --execute --inplace ../si_figures/si_figure_s03_leveling.ipynb
7
+ #
8
+ # Regenerating overwrites the .ipynb (clearing its execution outputs), so do not hand-edit it.
9
+ # All real code lives in leveling.py; the notebook only runs the protocol and shows results.
10
+ #
11
+ # Covers NS372 across all 8 nuclei, plus delta22 (the same leveling.py analysis run on both
12
+ # datasets). Main-text Figure 2B (the clean delta22 correlation matrix) is built separately in
13
+ # analysis/manuscript_figures/fig2b_correlation.ipynb; Figure 2C is a hand-drawn schematic, not
14
+ # reproduced in code. This notebook computes both res_ns372 and res_delta22 (via the shared
15
+ # setup/load cells below) so its combined cross-dataset summary table is self-contained.
16
+ from nb_build import md, code, save_notebook as _save
17
+
18
+
19
+ _TITLE_S3 = r"""
20
+ # SI Figure S3: inter-method correlation and PCA of NS372 and delta-22 shieldings
21
+
22
+ Inter-method correlation matrices and PC1/PC2 loadings for NS372 (44 functionals x 8 nuclei) and
23
+ delta22 (18 functionals, ¹H/¹³C), plus a combined summary table.
24
+ """
25
+
26
+ _METHOD_INTRO = r"""
27
+ Two independent NMR shielding datasets, analyzed separately (never pooled):
28
+
29
+ | Dataset | Source | Methods | Nuclei | Reference |
30
+ |---|---|---|---|---|
31
+ | **NS372** | Schattenberg & Kaupp, *JCTC* **17**, 7602 (2021) | 44 DFT/WFT functionals | ¹H ¹¹B ¹³C ¹⁵N ¹⁷O ¹⁹F ³¹P ³³S | CCSD(T)/pcSseg-3 |
32
+ | **delta22** | in-house `delta22.hdf5` | 18 gas-phase functionals, largest basis (pcSseg-3; mp2 → pcSseg-2), PBE0/cc-pVTZ geometry | ¹H ¹³C | DSD-PBEP86 (highest-rung in-set method, stands in for CCSD(T)) |
33
+ """
34
+
35
+ _CONFIG_MD = "## 1. Configuration"
36
+
37
+ _BOOTSTRAP = r"""
38
+ import os, sys
39
+
40
+ REPO = os.path.abspath("../..")
41
+ for _p in ("analysis/code", "analysis/code/shared"):
42
+ sys.path.insert(0, os.path.join(REPO, _p))
43
+ """
44
+
45
+ _IMPORTS = r"""
46
+ import glob
47
+ import pandas as pd
48
+ import matplotlib.pyplot as plt
49
+
50
+ import paths
51
+ import leveling
52
+ import leveling_plots
53
+ """
54
+
55
+ _PATH_SETUP = r"""
56
+ # inputs:
57
+ # - the Kaupp NS372 spreadsheet is small and ships in the repo's data/ns372/ folder
58
+ # - delta22.hdf5 is large: it resolves from the repo's data/delta22/ folder, carried by the
59
+ # Hugging Face checkout via Git LFS (see analysis/code/paths.py)
60
+ # the delta22 file is encoded as whole numbers (real value x 10,000); the loader
61
+ # in leveling.py decodes it on read (a plain-decimal copy also works, since the
62
+ # decode step is a no-op on floating-point data).
63
+ KAUPP_XLSX = os.path.join(REPO, "data", "ns372", "ct1c00919_si_002.xlsx")
64
+ DELTA22_H5 = paths.dataset_file("delta22", root=REPO)
65
+ SAVE_DPI = 200 # SI-quality raster output
66
+
67
+ def figure_path(name):
68
+ os.makedirs("figures", exist_ok=True)
69
+ return os.path.join("figures", name)
70
+
71
+ # self-clean: this notebook builds PNG names dynamically (one per nucleus), so drop any
72
+ # previously written figures before regenerating (glob on a missing folder returns [])
73
+ for _stale in glob.glob(os.path.join("figures", "*.png")):
74
+ os.remove(_stale)
75
+
76
+ pd.set_option('display.width', 150)
77
+ pd.set_option('display.max_columns', 25)
78
+ """
79
+
80
+ _STYLE_CONFIG = r"""
81
+ # functional-family colours used for every figure (family ordering lives in leveling.py)
82
+ FAMILY_COLORS = {'LDA':'#777777','GGA':'#1f77b4','mGGA':'#17becf','GH':'#2ca02c',
83
+ 'RSH':'#9467bd','LH':'#8c564b','DH':'#ff7f0e','WFT':'#d62728',
84
+ 'ref':'#FF1493'}
85
+
86
+ # proper Unicode-superscript labels for nuclei in figure titles
87
+ NUC_DISPLAY = {'1H':'¹H','11B':'¹¹B','13C':'¹³C',
88
+ '15N':'¹⁵N','17O':'¹⁷O','19F':'¹⁹F',
89
+ '31P':'³¹P','33S':'³³S'}
90
+
91
+ # adjustText parameters tuned per nucleus -- 1H and 13C have very dense central
92
+ # clusters and need stronger expansion; the paramagnetic-shielding nuclei
93
+ # (15N/17O/19F) already spread methods along the parabola and need a gentler
94
+ # pass to avoid over-flinging labels.
95
+ PCA_ADJUST_DEFAULT = dict(
96
+ force_text=(0.35, 0.55), force_explode=(0.25, 0.40),
97
+ force_static=(0.10, 0.15), force_pull=(0.02, 0.02),
98
+ expand=(1.25, 1.35), time_lim=4,
99
+ )
100
+ PCA_ADJUST = {
101
+ '1H': {**PCA_ADJUST_DEFAULT, 'force_text':(0.75, 1.05),
102
+ 'force_explode':(0.65, 0.90), 'expand':(1.7, 1.9), 'time_lim':7},
103
+ '11B': {**PCA_ADJUST_DEFAULT, 'force_text':(0.50, 0.75),
104
+ 'force_explode':(0.40, 0.60), 'expand':(1.4, 1.55), 'time_lim':5},
105
+ '13C': {**PCA_ADJUST_DEFAULT, 'force_text':(0.70, 1.00),
106
+ 'force_explode':(0.60, 0.85), 'expand':(1.6, 1.8), 'time_lim':6},
107
+ '15N': {**PCA_ADJUST_DEFAULT, 'force_text':(0.60, 0.85),
108
+ 'force_explode':(0.50, 0.70), 'expand':(1.5, 1.65), 'time_lim':6},
109
+ '17O': {**PCA_ADJUST_DEFAULT, 'force_text':(0.60, 0.85),
110
+ 'force_explode':(0.50, 0.70), 'expand':(1.5, 1.65), 'time_lim':6},
111
+ '31P': {**PCA_ADJUST_DEFAULT, 'force_text':(0.65, 0.90),
112
+ 'force_explode':(0.55, 0.75), 'expand':(1.55, 1.7), 'time_lim':6},
113
+ '33S': {**PCA_ADJUST_DEFAULT, 'force_text':(0.65, 0.90),
114
+ 'force_explode':(0.55, 0.75), 'expand':(1.55, 1.7), 'time_lim':6},
115
+ }
116
+ """
117
+
118
+ _NS372_DEF_MD = r"""
119
+ ## 2. NS372 (Kaupp) - definitions
120
+
121
+ Conventional GIAO shieldings for 44 functionals across 8 main-group nuclei, with a
122
+ CCSD(T)/pcSseg-3 reference. Kaupp Reduced-Set exclusions (F₃⁻, O₃, BH - multireference
123
+ outliers) are applied. Input: `ct1c00919_si_002.xlsx`.
124
+ """
125
+
126
+ _DELTA22_DEF_MD = r"""
127
+ ## 3. delta22 - definitions
128
+
129
+ Gas-phase conventional GIAO shieldings from `delta22.hdf5`: 18 functionals at their
130
+ largest available basis (pcSseg-3; plain `mp2` only to pcSseg-2), at the PBE0/cc-pVTZ
131
+ geometry. Observations are pooled ¹H / ¹³C atom sites across all 22 solutes. There is no
132
+ CCSD(T) reference in the file; **DSD-PBEP86 is used as the reference** for delta22 - it
133
+ is the highest-rung double-hybrid available in this method set and stands in for CCSD(T)
134
+ in the same role. The stored whole-number shieldings are decoded on read.
135
+ """
136
+
137
+ _LOAD_MD = r"""
138
+ ## 4. Load datasets + global colour scale
139
+
140
+ Run the loaders, analyse every nucleus, and compute the figure-wide
141
+ `-log10(1-|r|)` maximum (`GLOBAL_VMAX`) used as the colour scale on every correlation matrix
142
+ below, for NS372 and delta22 alike.
143
+ """
144
+
145
+ _LOAD = r"""
146
+ ns372 = leveling.load_ns372(KAUPP_XLSX)
147
+ delta22 = leveling.load_delta22(DELTA22_H5)
148
+
149
+ res_ns372 = {nuc: leveling.analyze_nucleus(d['M'], d['methods'], d['ref'])
150
+ for nuc, d in ns372.items()}
151
+ res_delta22 = {nuc: leveling.analyze_nucleus(d['M'], d['methods'], d['ref'])
152
+ for nuc, d in delta22.items()}
153
+
154
+ GLOBAL_VMAX = max(leveling.dataset_logr_max(res_ns372), leveling.dataset_logr_max(res_delta22))
155
+ print(f'NS372 : {len(ns372)} nuclei')
156
+ print(f'delta22: {len(delta22)} nuclei (reference = {leveling.DELTA22_REF})')
157
+ print(f'global colour scale: 0 -> {GLOBAL_VMAX} on the -log10(1-|r|) axis')
158
+ for nuc, d in ns372.items():
159
+ print(f' NS372 {nuc:4s}: {d["M"].shape[0]:4d} mols x {d["M"].shape[1]-1} methods + CCSD(T)')
160
+ for nuc, d in delta22.items():
161
+ print(f' delta22 {nuc:4s}: {d["M"].shape[0]:4d} sites x {d["M"].shape[1]} methods')
162
+ """
163
+
164
+ SHARED = [
165
+ md(_CONFIG_MD),
166
+ code(_BOOTSTRAP),
167
+ code(_IMPORTS),
168
+ code(_PATH_SETUP),
169
+ code(_STYLE_CONFIG),
170
+ md(_NS372_DEF_MD),
171
+ md(_DELTA22_DEF_MD),
172
+ md(_LOAD_MD),
173
+ code(_LOAD),
174
+ ]
175
+
176
+ # ----------------------------------------------------------------------------
177
+ # SI Figure S3: NS372 results across all 8 nuclei + the combined cross-dataset summary
178
+ # ----------------------------------------------------------------------------
179
+ si_figure_s03_leveling = [
180
+ md(_TITLE_S3),
181
+ md(_METHOD_INTRO),
182
+ *SHARED,
183
+ md('## 5. NS372 results'),
184
+ md('### 5.1 Leveling diagnostics - NS372'),
185
+ code(r"""
186
+ sum_ns372 = leveling.summarize(ns372, res_ns372)
187
+ print('NS372 - leveling diagnostics (CCSD(T) included as a method column):')
188
+ sum_ns372.round(5)
189
+ """),
190
+ md(r"""
191
+ ### 5.2 Per-method scaled RMSE vs CCSD(T)
192
+
193
+ `scaled_rmse` = RMSE of residuals after a per-method linear fit
194
+ `sigma_method ~ a*sigma_CCSD(T) + b` - the error that survives empirical linear scaling.
195
+ """),
196
+ code(r"""
197
+ rmse_ns372 = pd.DataFrame({nuc: res_ns372[nuc]['scaled_rmse'] for nuc in res_ns372})
198
+ rmse_ns372 = rmse_ns372.drop(index='CCSD(T)', errors='ignore')
199
+ rmse_ns372 = rmse_ns372.reindex(ns372['1H']['methods'][:-1]) # family order
200
+ print('NS372 - per-method scaled RMSE vs CCSD(T) (ppm):')
201
+ rmse_ns372.round(3)
202
+ """),
203
+ md('### 5.3 Inter-method correlation matrices - NS372 (one nucleus per file)'),
204
+ code(r"""
205
+ NS372_NUCS = ['1H', '11B', '13C', '15N', '17O', '19F', '31P', '33S']
206
+ for nuc in NS372_NUCS:
207
+ fams = {nuc: ns372[nuc]['families']}
208
+ fig = leveling_plots.plot_corr_matrix('NS372', [nuc], res_ns372, fams,
209
+ GLOBAL_VMAX, ref_name='CCSD(T)',
210
+ nuc_display=NUC_DISPLAY)
211
+ fig.savefig(figure_path(f'si_figure_s03_corr_{nuc}.png'),
212
+ dpi=SAVE_DPI, bbox_inches='tight')
213
+ plt.show()
214
+ """),
215
+ md('### 5.4 PC1/PC2 structure - NS372 (one nucleus per file)'),
216
+ code(r"""
217
+ for nuc in NS372_NUCS:
218
+ fams = {nuc: ns372[nuc]['families']}
219
+ fig = leveling_plots.plot_pca_pair('NS372', [nuc], res_ns372, fams,
220
+ family_colors=FAMILY_COLORS, nuc_display=NUC_DISPLAY,
221
+ pca_adjust=PCA_ADJUST, pca_adjust_default=PCA_ADJUST_DEFAULT)
222
+ fig.savefig(figure_path(f'si_figure_s03_pca_{nuc}.png'),
223
+ dpi=SAVE_DPI, bbox_inches='tight')
224
+ plt.show()
225
+ """),
226
+ md(r"""
227
+ ### 5.5 delta22 panels (lead the published figure: corr ¹H/¹³C + PCA)
228
+
229
+ The same correlation + PCA layout on the delta22 gas-phase set. DSD-PBEP86 is the reference (delta22
230
+ has no CCSD(T)); it appears as an ordinary double-hybrid point in the PCA, with no CCSD(T) star.
231
+ """),
232
+ code(r"""
233
+ # delta22 correlation matrices (canonical S3 A = 1H, B = 13C)
234
+ for nuc in ['1H', '13C']:
235
+ fams = {nuc: delta22[nuc]['families']}
236
+ fig = leveling_plots.plot_corr_matrix('delta22', [nuc], res_delta22, fams,
237
+ GLOBAL_VMAX, ref_name=leveling.DELTA22_REF,
238
+ nuc_display=NUC_DISPLAY)
239
+ fig.savefig(figure_path(f'si_figure_s03_delta22_corr_{nuc}.png'),
240
+ dpi=SAVE_DPI, bbox_inches='tight')
241
+ plt.show()
242
+
243
+ # delta22 PCA projection: 1H and 13C stacked in one figure (canonical S3 C)
244
+ fams = {nuc: delta22[nuc]['families'] for nuc in ['1H', '13C']}
245
+ fig = leveling_plots.plot_pca_pair('delta22', ['1H', '13C'], res_delta22, fams,
246
+ family_colors=FAMILY_COLORS, nuc_display=NUC_DISPLAY,
247
+ pca_adjust=PCA_ADJUST, pca_adjust_default=PCA_ADJUST_DEFAULT)
248
+ fig.savefig(figure_path('si_figure_s03_delta22_pca.png'),
249
+ dpi=SAVE_DPI, bbox_inches='tight')
250
+ plt.show()
251
+ """),
252
+ md('## 6. Combined summary (both datasets)'),
253
+ code(r"""
254
+ # delta22's per-nucleus diagnostics (same summarize() used for NS372 above), computed here
255
+ # too so this combined table is self-contained -- it reuses res_delta22 from the shared load
256
+ # cell above, so this is cheap (no new correlation/PCA computation).
257
+ sum_delta22 = leveling.summarize(delta22, res_delta22)
258
+
259
+ combined = pd.concat([sum_ns372.assign(dataset='NS372'),
260
+ sum_delta22.assign(dataset='delta22')], ignore_index=True)
261
+ combined = combined[['dataset','nucleus','n_obs','n_methods','r_min','r_median',
262
+ 'PC1_pct','PC2_pct','PC3plus_pct','parabola_R2']]
263
+ print('Leveling effect - both datasets (analyzed separately):')
264
+ print(f' PC1 range : {combined.PC1_pct.min():.2f}% - {combined.PC1_pct.max():.2f}%')
265
+ print(f' min pairwise r : {combined.r_min.min():.5f} (worst case, all nuclei)')
266
+ print(f' parabola R2 range : {combined.parabola_R2.min():.3f} - {combined.parabola_R2.max():.3f}')
267
+ print(f' global colour scale : 0 -> {GLOBAL_VMAX} on -log10(1-|r|)')
268
+ combined.round(5)
269
+ """),
270
+ ]
271
+
272
+ # name -> (cells, path relative to repo root). Notebooks are grouped by role (manuscript/si_figures/
273
+ # si_tables), not by dataset.
274
+ NOTEBOOKS = {
275
+ "si_figure_s03_leveling": (si_figure_s03_leveling, "analysis/si_figures/si_figure_s03_leveling.ipynb"),
276
+ }
277
+
278
+ if __name__ == "__main__":
279
+ import os
280
+ import sys
281
+ here = os.path.dirname(os.path.abspath(__file__))
282
+ repo = os.path.abspath(os.path.join(here, "..", ".."))
283
+ names = sys.argv[1:]
284
+ if not names:
285
+ print("usage: python3 build_nb_leveling.py <name> [<name> ...]")
286
+ print("regenerates ONLY the named notebook(s) -- pick just the one(s) you edited,")
287
+ print("since regenerating clears a notebook's execution outputs.")
288
+ print("available names:", ", ".join(NOTEBOOKS))
289
+ raise SystemExit(1)
290
+ unknown = [n for n in names if n not in NOTEBOOKS]
291
+ if unknown:
292
+ raise SystemExit(f"unknown notebook name(s): {unknown}; available: {', '.join(NOTEBOOKS)}")
293
+ for name in names:
294
+ cells, relpath = NOTEBOOKS[name]
295
+ _save(cells, os.path.join(repo, relpath))
analysis/code/carbon_monoxide.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Figure S12: MagNET vs DFT carbon-13 shielding of carbon monoxide across bond lengths.
2
+
3
+ The CO bond is stretched/compressed; at each length the 13C shielding is computed by DFT
4
+ (PBE1PBE/pcSseg-1, gas, Gaussian) and by the MagNET foundation model. This module holds only the
5
+ loading; the plot is drawn inline in the figure notebook.
6
+
7
+ Two files ship alongside:
8
+ - `co_magnet_vs_gaussian.csv` - one row per bond length: DFT and MagNET shieldings and their
9
+ difference, for carbon and oxygen.
10
+ - `co_bond_lengths_histogram.csv` - carbon-oxygen bond-length distribution across real molecules,
11
+ as (bin left edge, count) rows, for the shaded histogram.
12
+ """
13
+ import os
14
+
15
+ import pandas as pd
16
+
17
+ from stats import mae
18
+
19
+ HERE = os.path.dirname(os.path.abspath(__file__))
20
+ COMPARISON_CSV = os.path.join(HERE, "..", "..", "data", "carbon_monoxide", "co_magnet_vs_gaussian.csv")
21
+ HISTOGRAM_CSV = os.path.join(HERE, "..", "..", "data", "carbon_monoxide", "co_bond_lengths_histogram.csv")
22
+
23
+
24
+ def load_comparison(path=COMPARISON_CSV):
25
+ """The bond-length scan as a DataFrame, sorted by bond length. Columns: `bond_length` (angstrom),
26
+ and for each of carbon and oxygen the `gaussian`, `magnet`, and `delta` (MagNET minus DFT)
27
+ shieldings in ppm."""
28
+ raw = pd.read_csv(path)
29
+ return pd.DataFrame(
30
+ {
31
+ "bond_length": raw["bond_length_angstrom"].astype(float),
32
+ "gaussian_c": raw["gaussian_shielding_c_ppm"].astype(float),
33
+ "magnet_c": raw["magnet_shielding_c_ppm"].astype(float),
34
+ "delta_c": raw["delta_c_ppm"].astype(float),
35
+ "gaussian_o": raw["gaussian_shielding_o_ppm"].astype(float),
36
+ "magnet_o": raw["magnet_shielding_o_ppm"].astype(float),
37
+ "delta_o": raw["delta_o_ppm"].astype(float),
38
+ }
39
+ ).sort_values("bond_length", ignore_index=True)
40
+
41
+
42
+ def load_bond_length_histogram(path=HISTOGRAM_CSV):
43
+ """The sampled carbon-oxygen bond-length distribution as `(centers, counts, bin_width)`: the bin
44
+ centers in angstrom, the count in each bin, and the (uniform) bin width. The stored table holds
45
+ left bin edges and counts."""
46
+ table = pd.read_csv(path)
47
+ left_edges = table["bin_left_angstrom"].to_numpy(dtype=float)
48
+ counts = table["count"].to_numpy(dtype=float)
49
+ bin_width = float(left_edges[1] - left_edges[0]) if len(left_edges) > 1 else 0.01
50
+ centers = left_edges + bin_width / 2.0
51
+ return centers, counts, bin_width
52
+
53
+
54
+ def equilibrium_vs_extreme_error(comparison=None, window=(1.0, 1.3), extreme=(0.9, 1.4)):
55
+ """The figure's claim as two numbers: the mean absolute carbon error (ppm) for bond lengths
56
+ inside the near-equilibrium `window`, and for the non-equilibrium tails outside `extreme`. The
57
+ second is far larger, which is the whole point of the panel."""
58
+ df = load_comparison() if comparison is None else comparison
59
+ near = df[(df["bond_length"] >= window[0]) & (df["bond_length"] <= window[1])]
60
+ tails = df[(df["bond_length"] < extreme[0]) | (df["bond_length"] > extreme[1])]
61
+ return mae(near["delta_c"], 0), mae(tails["delta_c"], 0)
62
+
63
+
analysis/code/cp3.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Figure 1A data and analysis: informative 1H shift differences are too small for DFT to resolve.
2
+
3
+ Loads per-site 1H shift spread across the Goodman CP3 stereoisomers and classifies each site against
4
+ the experimental noise floor (0.02 ppm) and DFT's resolving power (0.10 ppm); ~36% fall in between.
5
+ Data: `goodman2009_cp3.xlsx` (Goodman, J. Org. Chem. 2009, 74, 4597), shipped alongside. Plotting is
6
+ in the figure notebook.
7
+ """
8
+ import os
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+
13
+ HERE = os.path.dirname(os.path.abspath(__file__))
14
+ CP3_XLSX = os.path.join(HERE, "..", "..", "data", "cp3", "goodman2009_cp3.xlsx")
15
+
16
+ # accuracy thresholds in ppm: the experimental noise floor, the DFT resolving limit, and the cutoff
17
+ # above which a variation counts as large. These define the zones a site's variation falls into.
18
+ EXPERIMENTAL_LIMIT = 0.02
19
+ DFT_LIMIT = 0.10
20
+ LARGE_VARIATION = 0.30
21
+
22
+
23
+ def load_variations(path=CP3_XLSX, nucleus="H"):
24
+ """The per-site chemical-shift standard deviations across the CP3 stereoisomers for one nucleus
25
+ ("H" or "C"), as a 1D array of finite values (ppm)."""
26
+ df = pd.read_excel(path)
27
+ x = df[df["nucleus"] == nucleus]["stdev"].to_numpy(dtype=float)
28
+ return x[np.isfinite(x)]
29
+
30
+
31
+ def _zone(value):
32
+ """Classify one variation (ppm) into its accuracy zone: within experimental noise, informative
33
+ but below the DFT resolving limit, resolvable by DFT, or large."""
34
+ if value < EXPERIMENTAL_LIMIT:
35
+ return "below_experimental"
36
+ if value < DFT_LIMIT:
37
+ return "below_dft"
38
+ if value < LARGE_VARIATION:
39
+ return "dft_zone"
40
+ return "large"
41
+
42
+
43
+ def fraction_below_dft(variations, lo=EXPERIMENTAL_LIMIT, hi=DFT_LIMIT, bins=30):
44
+ """The fraction of the variation histogram's area between the experimental floor and the DFT
45
+ limit: the sites whose shift variation is informative but too small for DFT to resolve (the
46
+ paper's 36%). Computed as histogram area in [lo, hi] over total area, splitting the bins that
47
+ straddle a boundary, exactly as the figure does."""
48
+ edges = np.linspace(0.0, float(variations.max()), bins + 1)
49
+ counts, edge = np.histogram(variations, bins=edges)
50
+ total = window = 0.0
51
+ for height, left, right in zip(counts, edge[:-1], edge[1:]):
52
+ total += height * (right - left)
53
+ overlap = max(0.0, min(right, hi) - max(left, lo))
54
+ window += height * overlap
55
+ return window / total if total > 0 else float("nan")
analysis/code/delta22_plots.py ADDED
@@ -0,0 +1,606 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Plotting engines for the delta-22 SI figure notebooks (analysis/si_figures/*.ipynb).
2
+
3
+ Each function is the drawing engine behind one delta-22 SI figure panel. Notebook globals an engine
4
+ needs (color maps, axis-label lookups, N_SPLITS, solvent lists, etc.) are explicit function
5
+ parameters, passed in by the notebook at the call site. The numbers come from delta22.py; this
6
+ module only draws, except `ss_fits`, a small data-prep helper used only by the si_figure_s14
7
+ notebook.
8
+ """
9
+ import numpy as np
10
+ import pandas as pd
11
+ import seaborn as sns
12
+ import matplotlib.pyplot as plt
13
+ from matplotlib.lines import Line2D
14
+ from matplotlib.colors import ListedColormap
15
+ from scipy.stats import pearsonr, linregress
16
+ from adjustText import adjust_text
17
+
18
+ import delta22
19
+
20
+
21
+ # ----------------------------------------------------------------------------
22
+ # color helpers shared by the engine/source panels (si_figure_s08, si_figure_s13)
23
+ # ----------------------------------------------------------------------------
24
+ def darken_color(hex_color, factor=0.7):
25
+ """Scale a "#rrggbb" color's channels by factor (< 1 darkens)."""
26
+ hex_color = hex_color.lstrip("#")
27
+ r, g, b = (int(hex_color[i:i + 2], 16) for i in (0, 2, 4))
28
+ r, g, b = (int(c * factor) for c in (r, g, b))
29
+ return f"#{r:02x}{g:02x}{b:02x}"
30
+
31
+
32
+ def lighten_color(hex_color, amount=0.45):
33
+ """Blend a "#rrggbb" color toward white by amount (0-1)."""
34
+ hex_color = hex_color.lstrip("#")
35
+ r, g, b = (int(hex_color[i:i + 2], 16) for i in (0, 2, 4))
36
+ r, g, b = (int(c + (255 - c) * amount) for c in (r, g, b))
37
+ return f"#{r:02x}{g:02x}{b:02x}"
38
+
39
+
40
+ def display_solvent_name(name):
41
+ """Human-readable solvent label ("Water (TIP4P)" for TIP4P, else capitalized)."""
42
+ return "Water (TIP4P)" if name == "TIP4P" else name.capitalize()
43
+
44
+
45
+ # ----------------------------------------------------------------------------
46
+ # SI Figure S4: correlation of implicit (PCM) corrections across solvents and methods
47
+ # ----------------------------------------------------------------------------
48
+ def plot_correlation_matrix(corr_matrix, title, caption, colormap="Reds", show_values=True, save_path=None):
49
+ """Lower-triangle heatmap of -log10(1 - r) with a Pearson-R colorbar; a value of 3 means r=0.999."""
50
+ arr = corr_matrix.to_numpy(dtype=float, copy=True)
51
+ np.fill_diagonal(arr, np.nan)
52
+ transformed = pd.DataFrame(-np.log10(1 - arr), index=corr_matrix.index, columns=corr_matrix.columns)
53
+ mask = np.tril(np.ones_like(transformed, dtype=bool), k=0).T
54
+ vmin, vmax = -np.log10(1 - 0.9), -np.log10(1 - 0.9999)
55
+ fig = plt.figure(figsize=(8, 8))
56
+ ax = sns.heatmap(transformed, mask=mask, annot=transformed if show_values else False, fmt=".1f",
57
+ cmap=colormap, cbar_kws={"shrink": 0.5, "pad": -0.13},
58
+ vmin=vmin, vmax=vmax, square=True)
59
+ cbar = ax.collections[0].colorbar
60
+ cbar.set_ticks([-np.log10(1 - r) for r in (0.99, 0.999, 0.9999)])
61
+ cbar.set_ticklabels(["0.99", "0.999", "0.9999"])
62
+ cbar.ax.set_title("Pearson R", fontweight="bold", pad=25)
63
+ ax.set_xlabel(""); ax.set_ylabel("")
64
+ ax.set_xticklabels(ax.get_xticklabels(), fontweight="bold")
65
+ ax.set_yticklabels(ax.get_yticklabels(), fontweight="bold")
66
+ ax.set_yticks(ax.get_yticks()[1:]); ax.set_xticks(ax.get_xticks()[:-1])
67
+ plt.title(title, fontweight="bold")
68
+ plt.figtext(0.59, 0.895, caption, wrap=True, horizontalalignment="center", fontsize=10)
69
+ for side in ("top", "right", "bottom", "left"):
70
+ plt.gca().spines[side].set_visible(True)
71
+ plt.tight_layout()
72
+ if save_path:
73
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
74
+ plt.show()
75
+
76
+
77
+ def plot_pcm_scatter(x_vals, y_vals, solvent_x, solvent_y, nucleus, save_path=None):
78
+ """Scatter of PCM corrections in one solvent vs another, with a best-fit line, slope, and Pearson R."""
79
+ pearson_r = x_vals.corr(y_vals)
80
+ slope, _ = np.polyfit(x_vals, y_vals, 1)
81
+ cap = lambda s: s[0].upper() + s[1:]
82
+ fig = plt.figure(figsize=(6.5, 6.5))
83
+ sns.scatterplot(x=x_vals, y=y_vals, s=60, color="black", edgecolor="none")
84
+ sns.regplot(x=x_vals, y=y_vals, scatter=False, color="black", ci=None,
85
+ line_kws={"linewidth": 1, "linestyle": "--"})
86
+ plt.xlabel(f"{cap(solvent_x)} PCM correction")
87
+ plt.ylabel(f"{cap(solvent_y)} PCM correction")
88
+ plt.title(f"PCM corrections: {cap(solvent_x)} vs {cap(solvent_y)} ({nucleus})", fontweight="bold")
89
+ plt.text(0.05, 0.96, f"Slope = {slope:.3f}", transform=plt.gca().transAxes, va="top")
90
+ plt.text(0.05, 0.92, f"Pearson R = {pearson_r:.3f}", transform=plt.gca().transAxes, va="top")
91
+ plt.tight_layout()
92
+ if save_path:
93
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
94
+ plt.show()
95
+
96
+
97
+ # ----------------------------------------------------------------------------
98
+ # SI Figure S5: per-solvent PCM benefit vs bulk dielectric constant and polarizability
99
+ # ----------------------------------------------------------------------------
100
+ def plot_pcm_benefit_vs_properties(benefit, dielectric, polarizability, nucleus_label,
101
+ exclude=(), title_extra="", figsize=(14, 6), save_path=None):
102
+ """Per-solvent PCM benefit (percent test-RMSE reduction) vs dielectric constant and
103
+ polarizability, one subplot each, with a best-fit line and Pearson R. exclude drops solvents."""
104
+ solvents = [s for s in benefit.index if s not in set(exclude) and s in dielectric]
105
+ y = np.array([benefit[s] for s in solvents], dtype=float)
106
+ fig, axes = plt.subplots(1, 2, figsize=figsize)
107
+ for ax, prop, xlabel in [(axes[0], dielectric, "Dielectric Constant"),
108
+ (axes[1], polarizability, "Polarizability")]:
109
+ x = np.array([prop[s] for s in solvents], dtype=float)
110
+ r, _ = pearsonr(x, y)
111
+ slope, intercept, _, _, _ = linregress(x, y)
112
+ ax.scatter(x, y, s=100, color="black", alpha=1, edgecolors="black", linewidth=1, zorder=3)
113
+ ax.axhline(0, color="gray", linestyle="--", alpha=0.4, linewidth=1, zorder=1)
114
+ xr = np.linspace(x.min(), x.max(), 100)
115
+ ax.plot(xr, slope * xr + intercept, color="gray", linestyle="--", linewidth=2,
116
+ label=f"R = {r:.3f}", zorder=2)
117
+ ax.set_xlabel(xlabel, fontsize=13, fontweight="bold")
118
+ ax.set_ylabel("Percent Reduction in Test RMSE (%)", fontsize=13, fontweight="bold")
119
+ title = f"{nucleus_label} Nucleus: PCM Benefit vs {xlabel}"
120
+ if title_extra:
121
+ title += f"\n({title_extra})"
122
+ ax.set_title(title, fontsize=14, fontweight="bold")
123
+ ax.grid(True, alpha=0.3)
124
+ ax.legend(fontsize=12)
125
+ texts = [ax.text(sx, sy, name, fontsize=9, fontweight="bold")
126
+ for sx, sy, name in zip(x, y, solvents)]
127
+ adjust_text(texts, ax=ax, arrowprops=dict(arrowstyle="-", color="gray", lw=0.5))
128
+ fig.tight_layout()
129
+ if save_path:
130
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
131
+ plt.show()
132
+ return fig
133
+
134
+
135
+ # ----------------------------------------------------------------------------
136
+ # SI Figure S6: per-solvent test RMSE of six solvent-correction models
137
+ # ----------------------------------------------------------------------------
138
+ def plot_formula_ladder_boxplot(results_df, formula_labels, solvent_groups, nucleus="H",
139
+ colors=None, figsize=(14, 7), title="", save_path=None):
140
+ """Per-solvent box-and-whisker of the test RMSE for a solvent-correction formula ladder, with
141
+ solvents grouped by class (a dashed line separates classes)."""
142
+ formulas = list(formula_labels)
143
+ n = len(formulas)
144
+ if colors is None:
145
+ colors = sns.color_palette("colorblind", n)
146
+ # lay the solvents out class by class (only those present), tracking where classes end
147
+ flat, boundaries = [], []
148
+ for group in solvent_groups:
149
+ present = [s for s in solvent_groups[group] if s in set(results_df["solvent"])]
150
+ flat.extend(present)
151
+ boundaries.append(len(flat))
152
+ fig, ax = plt.subplots(figsize=figsize)
153
+ box_width = 0.8 / n
154
+ gap = 0.6
155
+ centers, separators = [], []
156
+ pos = 0.0
157
+ for si, solvent in enumerate(flat):
158
+ centers.append(pos)
159
+ for fi, formula in enumerate(formulas):
160
+ vals = results_df[(results_df["formula"] == formula)
161
+ & (results_df["solvent"] == solvent)]["test_RMSE"].dropna().to_numpy()
162
+ bp = ax.boxplot([vals], positions=[pos + (fi - (n - 1) / 2) * box_width],
163
+ widths=box_width * 0.9, showfliers=False, patch_artist=True,
164
+ manage_ticks=False)
165
+ bp["boxes"][0].set(facecolor=colors[fi], alpha=0.9)
166
+ bp["medians"][0].set(color="black")
167
+ if si == 0:
168
+ bp["boxes"][0].set_label(formula_labels[formula])
169
+ pos += 1.0
170
+ if (si + 1) in boundaries[:-1]: # a class just ended (not the last)
171
+ separators.append(pos - 0.5 + gap / 2)
172
+ pos += gap
173
+ for x in separators:
174
+ ax.axvline(x, color="gray", linestyle="--", linewidth=1)
175
+ ax.set_xticks(centers)
176
+ ax.set_xticklabels(flat, rotation=60, ha="right", fontweight="bold")
177
+ ax.set_ylabel("Test RMSE (ppm)", fontweight="bold")
178
+ ax.set_ylim(bottom=0)
179
+ ax.set_title(title, fontweight="bold")
180
+ ax.legend(fontsize=9, loc="upper left")
181
+ fig.tight_layout()
182
+ if save_path:
183
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
184
+ return fig
185
+
186
+
187
+ # ----------------------------------------------------------------------------
188
+ # SI Figure S7: DFT 13C explicit-solvent correction, Desmond vs OpenMM
189
+ # ----------------------------------------------------------------------------
190
+ def plot_desmond_vs_openmm_grid(pairs_by_solvent, figsize=(10, 10), save_path=None):
191
+ """Per OpenMM solvent, a scatter of the Desmond vs OpenMM explicit correction at each site.
192
+ Tight clustering on the diagonal shows the correction is independent of the MD engine."""
193
+ solvents = list(pairs_by_solvent)
194
+ fig, axes = plt.subplots(2, 2, figsize=figsize)
195
+ axes = np.atleast_1d(axes).flatten()
196
+ for ax, solvent in zip(axes, solvents):
197
+ p = pairs_by_solvent[solvent]
198
+ ax.scatter(p["openMM"], p["desmond"], c="k", s=5)
199
+ ax.set_xlabel("OpenMM solvent correction (ppm)", fontweight="bold")
200
+ ax.set_ylabel("Desmond solvent correction (ppm)", fontweight="bold")
201
+ title = "Water (TIP4P)" if solvent == "TIP4P" else solvent.capitalize()
202
+ ax.set_title(title, fontweight="bold")
203
+ for ax in axes[len(solvents):]:
204
+ ax.set_visible(False)
205
+ fig.tight_layout()
206
+ if save_path:
207
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
208
+ return fig
209
+
210
+
211
+ # ----------------------------------------------------------------------------
212
+ # SI Figure S8 (panels B-D): explicit-solvent correction vs MD-frame count
213
+ # ----------------------------------------------------------------------------
214
+ def plot_frame_convergence(running_by_label, finals_by_label, label_colors, label_names,
215
+ title="", xlabel="Number of Frames", figsize=(6, 5), save_path=None):
216
+ """Running-average correction vs number of frames, one line per label, with each label's
217
+ converged value drawn as a dashed horizontal line."""
218
+ fig, ax = plt.subplots(figsize=figsize)
219
+ for label, running in running_by_label.items():
220
+ color = label_colors.get(label)
221
+ valid = running[~np.isnan(running)]
222
+ display = label_names.get(label, label)
223
+ ax.plot(np.arange(1, len(valid) + 1), valid, color=color, lw=1.1, label=display)
224
+ final = finals_by_label[label]
225
+ ax.axhline(final, color=color, ls="--", lw=1, label=f"{display} final: {final:.3f} ppm")
226
+ ax.set_xlabel(xlabel, fontweight="bold")
227
+ ax.set_ylabel("Running Average Correction (ppm)", fontweight="bold")
228
+ ax.set_title(title, fontweight="bold")
229
+ ax.legend(fontsize=8)
230
+ fig.tight_layout()
231
+ if save_path:
232
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
233
+ return fig
234
+
235
+
236
+ def plot_frame_correction_histogram(values_by_label, total_frames_by_label, label_colors, label_names,
237
+ bins=30, title="", figsize=(6, 4.5), save_path=None):
238
+ """Distribution of valid per-frame corrections, one overlaid histogram per label; each bin's
239
+ height is a fraction of that label's total frame count, with mean/std in the legend."""
240
+ fig, ax = plt.subplots(figsize=figsize)
241
+ for label, values in values_by_label.items():
242
+ valid = values[~np.isnan(values)]
243
+ weights = np.full(len(valid), 1.0 / total_frames_by_label[label])
244
+ display = label_names.get(label, label)
245
+ ax.hist(valid, bins=bins, weights=weights, color=label_colors.get(label), alpha=0.55,
246
+ edgecolor="black", linewidth=0.3,
247
+ label=f"{display} ($\\mu$={valid.mean():.3f}, $\\sigma$={valid.std():.3f})")
248
+ ax.set_xlabel("Correction per Frame (ppm)", fontweight="bold")
249
+ ax.set_ylabel("Frequency / Frame Count", fontweight="bold")
250
+ ax.set_title(title, fontweight="bold")
251
+ ax.legend(fontsize=8)
252
+ fig.tight_layout()
253
+ if save_path:
254
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
255
+ return fig
256
+
257
+
258
+ def plot_frame_autocorrelation(autocorr_by_label, label_colors, label_names, title="",
259
+ figsize=(8, 4.5), save_path=None):
260
+ """Autocorrelation of the per-frame correction vs lag (frames), one line per label."""
261
+ fig, ax = plt.subplots(figsize=figsize)
262
+ for label, autocorr in autocorr_by_label.items():
263
+ ax.plot(np.arange(len(autocorr)), autocorr, color=label_colors.get(label), lw=1,
264
+ label=label_names.get(label, label))
265
+ ax.axhline(0, color="0.8", lw=0.8)
266
+ ax.set_xlabel("Lag (frames)", fontweight="bold")
267
+ ax.set_ylabel("Autocorrelation", fontweight="bold")
268
+ ax.set_title(title, fontweight="bold")
269
+ ax.legend()
270
+ fig.tight_layout()
271
+ if save_path:
272
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
273
+ return fig
274
+
275
+
276
+ def plot_frame_validity_heatmaps(grids_by_engine, solvents_by_engine, engine_labels, solute="AcOH",
277
+ figsize=(10, 8), save_path=None):
278
+ """One heatmap per MD engine: which trajectory frames have computed DFT shielding data (light
279
+ blue) vs not (dark gray), one row per solvent and one column per frame index."""
280
+ engines = list(grids_by_engine)
281
+ fig, axes = plt.subplots(len(engines), 1, figsize=figsize, squeeze=False)
282
+ cmap = ListedColormap(["#1a1a1a", "#a8dadc"]) # False = dark gray, True = light blue
283
+ for ax, engine in zip(axes[:, 0], engines):
284
+ ax.imshow(grids_by_engine[engine], aspect="auto", cmap=cmap, vmin=0, vmax=1,
285
+ interpolation="nearest")
286
+ ax.set_yticks(range(len(solvents_by_engine[engine])))
287
+ ax.set_yticklabels(solvents_by_engine[engine], fontsize=8)
288
+ ax.set_title(f"{solute} Frame Validities ({engine_labels.get(engine, engine)})",
289
+ fontweight="bold", fontsize=10)
290
+ axes[-1, 0].set_xlabel("Frame index")
291
+ fig.tight_layout()
292
+ if save_path:
293
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
294
+ return fig
295
+
296
+
297
+ # ----------------------------------------------------------------------------
298
+ # SI Figure S11: MagNET vs DFT rovibrational (QCD) corrections
299
+ # ----------------------------------------------------------------------------
300
+ def plot_qcd_scatter(qcd, save_path=None):
301
+ """NN vs DFT QCD correction, one subplot per nucleus, with a y=x guide and an R^2/RMSE/MAE
302
+ stats box measured against that line."""
303
+ fig, axes = plt.subplots(1, 2, figsize=(12, 6))
304
+ for ax, nucleus in zip(axes, ["H", "C"]):
305
+ sub = qcd[qcd["nucleus"] == nucleus]
306
+ x, y = sub["qcd_dft"].to_numpy(), sub["qcd_nn"].to_numpy()
307
+ ss_res, ss_tot = np.sum((y - x) ** 2), np.sum((y - y.mean()) ** 2)
308
+ r2 = 1 - ss_res / ss_tot if ss_tot != 0 else np.nan
309
+ rmse, mae = np.sqrt(np.mean((y - x) ** 2)), np.mean(np.abs(y - x))
310
+ print(f"{nucleus}: R^2={r2:.4f}, RMSE={rmse:.4f}, MAE={mae:.4f}")
311
+ ax.scatter(x, y, color="black")
312
+ raw = [min(x.min(), y.min()), max(x.max(), y.max())]
313
+ buf = 0.05 * (raw[1] - raw[0])
314
+ lims = [raw[0] - buf, raw[1] + buf]
315
+ ax.plot(lims, lims, linestyle="--", color="gray", label="NN matches DFT", zorder=-1)
316
+ ax.set_xlim(lims); ax.set_ylim(lims)
317
+ ax.set_xlabel("DFT QCD"); ax.set_ylabel("NN QCD")
318
+ ax.set_title(f"DFT vs NN QCD ({nucleus})", fontweight="bold")
319
+ ax.legend()
320
+ ax.text(0.02, 0.98, f"R^2 = {r2:.3f}\nRMSE = {rmse:.3f}\nMAE = {mae:.3f}",
321
+ transform=ax.transAxes, ha="left", va="top", fontsize=12, fontweight="bold",
322
+ bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="gray", alpha=0.8))
323
+ fig.tight_layout()
324
+ if save_path:
325
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
326
+ return fig
327
+
328
+
329
+ def plot_qcd_error_histogram(qcd, save_path=None):
330
+ """Distribution of the NN-minus-DFT QCD error, one subplot per nucleus."""
331
+ fig, axes = plt.subplots(1, 2, figsize=(12, 4), sharey=True)
332
+ for ax, nucleus in zip(axes, ["H", "C"]):
333
+ data = qcd[qcd["nucleus"] == nucleus]["error"]
334
+ ax.hist(data, bins=30, color="gray", alpha=0.8, edgecolor="black")
335
+ ax.set_title(f"QCD Error Distribution (NN - DFT) for {nucleus}", fontweight="bold")
336
+ ax.set_xlabel("Error"); ax.set_ylabel("Count")
337
+ fig.tight_layout()
338
+ if save_path:
339
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
340
+ return fig
341
+
342
+
343
+ def plot_qcd_correction_by_site(by_site, nucleus, legend_loc, dft_color="#A0A0A0", nn_color="#707070",
344
+ save_path=None):
345
+ """The DFT and NN QCD correction for every site, one column of points per solute; sites within
346
+ a solute are jittered horizontally and joined by a faint vertical line."""
347
+ fig, ax = plt.subplots(figsize=(12, 6))
348
+ solute_order = sorted(by_site["solute"].unique())
349
+ x_pos = {s: i for i, s in enumerate(solute_order)}
350
+ jitter = 0.02
351
+ site_offset = {}
352
+ for solute, group in by_site.groupby("solute"):
353
+ sites = sorted(group["site"].unique())
354
+ if len(sites) == 1:
355
+ site_offset[(solute, sites[0])] = 0.0
356
+ else:
357
+ for site, off in zip(sites, np.linspace(-jitter, jitter, len(sites))):
358
+ site_offset[(solute, site)] = off
359
+ for _, row in by_site.iterrows():
360
+ x = x_pos[row["solute"]] + site_offset[(row["solute"], row["site"])]
361
+ ax.vlines(x, row["qcd_dft"], row["qcd_nn"], color=dft_color, alpha=0.2, linewidth=1)
362
+ ax.scatter(x, row["qcd_dft"], color=dft_color, s=32, edgecolor="black", linewidth=0.3, alpha=0.85)
363
+ ax.scatter(x, row["qcd_nn"], color=nn_color, s=32, edgecolor="black", linewidth=0.3, alpha=0.85)
364
+ ax.set_xticks(range(len(solute_order)))
365
+ ax.set_xticklabels(solute_order, rotation=65, ha="right", fontweight="bold")
366
+ ax.set_ylabel("QCD correction (ppm)")
367
+ ax.set_title(f"QCD: DFT vs NN ({nucleus})", fontweight="bold")
368
+ ax.grid(axis="y", linestyle="--", alpha=0.3)
369
+ handles = [Line2D([0], [0], color=dft_color, marker="o", linestyle="-", label="DFT QCD"),
370
+ Line2D([0], [0], color=nn_color, marker="o", linestyle="-", label="NN QCD")]
371
+ ax.legend(handles=handles, ncol=1, fontsize=9, frameon=True, loc=legend_loc)
372
+ fig.tight_layout()
373
+ if save_path:
374
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
375
+ return fig
376
+
377
+
378
+ # ----------------------------------------------------------------------------
379
+ # SI Figure S13: MagNET-x vs DFT explicit-solvent corrections
380
+ # ----------------------------------------------------------------------------
381
+ def plot_dft_nn_scatter_by_engine(compare_df, engine_colors, engine_labels, save_path=None):
382
+ """NN vs DFT explicit correction at each site, both nuclei, Desmond and OpenMM overlaid, with
383
+ a y=x guide."""
384
+ fig, axes = plt.subplots(1, 2, figsize=(12, 6))
385
+ for ax, nucleus in zip(axes, ["H", "C"]):
386
+ sub = compare_df[compare_df["nucleus"] == nucleus]
387
+ allv = []
388
+ for engine in ["desmond", "openMM"]:
389
+ d = sub[sub["engine"] == engine]
390
+ ax.scatter(d["dft_value"], d["nn_value"], alpha=0.9, color=engine_colors[engine],
391
+ label=engine_labels[engine])
392
+ allv += [d["dft_value"].to_numpy(), d["nn_value"].to_numpy()]
393
+ allv = np.concatenate(allv)
394
+ raw = [allv.min(), allv.max()]
395
+ buf = 0.05 * (raw[1] - raw[0])
396
+ lims = [raw[0] - buf, raw[1] + buf]
397
+ ax.plot(lims, lims, linestyle="--", color="gray", label="NN matches DFT", zorder=-1)
398
+ ax.set_xlim(lims); ax.set_ylim(lims)
399
+ ax.set_xlabel("DFT Explicit Correction"); ax.set_ylabel("NN Explicit Correction")
400
+ ax.set_title(f"DFT vs NN Explicit Corrections ({nucleus})", fontweight="bold")
401
+ ax.legend()
402
+ fig.tight_layout()
403
+ if save_path:
404
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
405
+ return fig
406
+
407
+
408
+ def plot_dft_nn_error_histogram_by_engine(compare_df, engine_colors, engine_labels, save_path=None):
409
+ """Distribution of the NN-minus-DFT explicit-correction error, both nuclei, Desmond and OpenMM
410
+ overlaid."""
411
+ fig, axes = plt.subplots(1, 2, figsize=(12, 4), sharey=True)
412
+ for ax, nucleus in zip(axes, ["H", "C"]):
413
+ sub = compare_df[compare_df["nucleus"] == nucleus]
414
+ for engine in ["desmond", "openMM"]:
415
+ data = sub[sub["engine"] == engine]["error"].to_numpy()
416
+ if len(data) == 0:
417
+ continue
418
+ ax.hist(data, bins=30, color=engine_colors[engine], alpha=0.6, edgecolor="black",
419
+ label=engine_labels[engine])
420
+ ax.set_title(f"Explicit Correction Error (NN - DFT) for {nucleus}", fontweight="bold")
421
+ ax.set_xlabel("Error"); ax.set_ylabel("Count")
422
+ ax.legend()
423
+ fig.tight_layout()
424
+ if save_path:
425
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
426
+ return fig
427
+
428
+
429
+ def plot_explicit_correction_by_site(pairs_df, title, engine_colors, save_path=None):
430
+ """The Desmond and OpenMM explicit correction from both DFT (base hue) and NN (darkened), one
431
+ column per solute; the two engines are offset, sites are jittered, and each DFT/NN pair is
432
+ joined by a faint vertical line."""
433
+ fig, ax = plt.subplots(figsize=(12, 6))
434
+ solute_order = sorted(pairs_df["solute"].unique())
435
+ x_pos = {s: i for i, s in enumerate(solute_order)}
436
+ engine_offset = {"desmond": -0.12, "openMM": 0.12}
437
+ jitter = 0.02
438
+ site_offset = {}
439
+ for solute, group in pairs_df.groupby("solute"):
440
+ sites = sorted(group["site"].unique())
441
+ if len(sites) == 1:
442
+ site_offset[(solute, sites[0])] = 0.0
443
+ else:
444
+ for site, off in zip(sites, np.linspace(-jitter, jitter, len(sites))):
445
+ site_offset[(solute, site)] = off
446
+ dft_color = {e: engine_colors[e] for e in ("desmond", "openMM")}
447
+ nn_color = {e: darken_color(engine_colors[e]) for e in ("desmond", "openMM")}
448
+ for engine in ["desmond", "openMM"]:
449
+ for _, row in pairs_df.iterrows():
450
+ x = x_pos[row["solute"]] + engine_offset[engine] + site_offset[(row["solute"], row["site"])]
451
+ dft_val, nn_val = row[f"{engine}_dft"], row[f"{engine}_nn"]
452
+ ax.vlines(x, dft_val, nn_val, color=dft_color[engine], alpha=0.18, linewidth=1)
453
+ ax.scatter(x, dft_val, color=dft_color[engine], s=32, edgecolor="black", linewidth=0.3, alpha=0.85)
454
+ ax.scatter(x, nn_val, color=nn_color[engine], s=32, edgecolor="black", linewidth=0.3, alpha=0.85)
455
+ ax.set_xticks(range(len(solute_order)))
456
+ ax.set_xticklabels(solute_order, rotation=65, ha="right", fontweight="bold")
457
+ ax.set_ylabel("Explicit Solvent Correction (ppm)")
458
+ ax.set_title(title, fontweight="bold")
459
+ ax.grid(axis="y", linestyle="--", alpha=0.3)
460
+ handles = [Line2D([0], [0], color=dft_color["desmond"], marker="o", linestyle="-", label="DFT Desmond"),
461
+ Line2D([0], [0], color=nn_color["desmond"], marker="o", linestyle="-", label="NN Desmond"),
462
+ Line2D([0], [0], color=dft_color["openMM"], marker="o", linestyle="-", label="DFT OpenMM"),
463
+ Line2D([0], [0], color=nn_color["openMM"], marker="o", linestyle="-", label="NN OpenMM")]
464
+ ax.legend(handles=handles, ncol=2, fontsize=9, frameon=True, loc="upper left")
465
+ fig.tight_layout()
466
+ if save_path:
467
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
468
+ return fig
469
+
470
+
471
+ def plot_fitting_accuracy_boxplot(results_df, solvents, nuc_label, engine_colors, save_path=None):
472
+ """The semi-parsimonious composite model's test RMSE vs experiment, four boxes per solvent
473
+ (Desmond DFT/NN, OpenMM DFT/NN) so DFT-based and MagNET-x-based explicit terms sit side by side."""
474
+ groups = [("desmond", "DFT", "Desmond (DFT)"), ("desmond", "NN", "Desmond (NN)"),
475
+ ("openMM", "DFT", "OpenMM (DFT)"), ("openMM", "NN", "OpenMM (NN)")]
476
+ colors = [lighten_color(engine_colors["desmond"], 0.15), lighten_color(engine_colors["desmond"], 0.55),
477
+ lighten_color(engine_colors["openMM"], 0.15), lighten_color(engine_colors["openMM"], 0.55)]
478
+ n = len(groups)
479
+ width = 0.8 / n
480
+ x_base = np.arange(len(solvents))
481
+ fig, ax = plt.subplots(figsize=(11, 5))
482
+ for gi, (engine, source, label) in enumerate(groups):
483
+ data = [results_df[(results_df["solvent"] == sv) & (results_df["engine"] == engine)
484
+ & (results_df["source"] == source)]["test_RMSE"].dropna().to_numpy()
485
+ for sv in solvents]
486
+ positions = x_base + (gi - (n - 1) / 2) * width
487
+ bp = ax.boxplot(data, positions=positions, widths=width * 0.9, patch_artist=True,
488
+ showfliers=False, manage_ticks=False)
489
+ for box in bp["boxes"]:
490
+ box.set(facecolor=colors[gi], alpha=0.9)
491
+ for median in bp["medians"]:
492
+ median.set(color="black")
493
+ bp["boxes"][0].set_label(label)
494
+ ax.set_xticks(x_base)
495
+ ax.set_xticklabels(solvents)
496
+ ax.set_ylabel(f"Accuracy vs. Experiment (RMSE, {nuc_label} ppm)")
497
+ ax.set_title(f"DFT vs NN Explicit Corrections, Fitting Accuracy ({nuc_label})")
498
+ ax.legend(ncol=2, fontsize=9)
499
+ fig.tight_layout()
500
+ if save_path:
501
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
502
+ return fig
503
+
504
+
505
+ # ----------------------------------------------------------------------------
506
+ # SI Figure S14: test RMSE with DFT vs MagNET features, by solvent
507
+ # ----------------------------------------------------------------------------
508
+ def ss_fits(query, nucleus, formulas, solvents, n_splits, dft=False):
509
+ """Solvent-specific test RMSEs over the explicit solvents (SI Figure S14), nitromethane dropped."""
510
+ q = query[(query["nucleus"] == nucleus) & (query["solute"] != "nitromethane")]
511
+ if dft:
512
+ # DFT features need one method per nucleus (the MagNET-Zero training reference: WP04/pcSseg2
513
+ # for 1H, wB97X-D/pcSseg2 for 13C, AIMNet2 geometries) or the fit pools every method together.
514
+ # The NN table has only MagNET, so it needs no such filter.
515
+ method = delta22.MAGNET_PCM_OUTPUT_METHODS[nucleus]
516
+ q = q[(q["sap_nmr_method"] == method) & (q["sap_basis"] == "pcSseg2")
517
+ & (q["sap_geometry_type"] == "aimnet2")]
518
+ solutes = sorted(q["solute"].unique())
519
+ return delta22.run_fits(q, solvents, formulas, n_splits=n_splits, solutes=solutes)
520
+
521
+
522
+ def plot_ss_boxplot_dft_vs_nn(dft_df, nn_df, solvents, formulas, labels, nucleus,
523
+ solvent_labels=None, colors=("#A72608", "#5D737E", "#D9FFF5"),
524
+ figsize=(14, 8), box_span=0.7, group_gap=0.35, save_path=None):
525
+ """Per solvent, box plots of the solvent-specific test-RMSE distributions for each composite
526
+ formula, computed with DFT features (solid) and MagNET/NN features (lightened), side by side."""
527
+ def series(df, formula, solvent):
528
+ return df[(df["formula"] == formula) & (df["solvent"] == solvent)]["test_RMSE"].dropna().values
529
+
530
+ labels_x = [(solvent_labels or {}).get(s, s) for s in solvents]
531
+ n_formulas = len(formulas)
532
+ n_boxes = 2 * n_formulas # all DFT boxes, then all NN boxes, per solvent
533
+ box_w = box_span / n_boxes
534
+ centers = [i * (box_span + group_gap) for i in range(len(solvents))]
535
+ fig, ax = plt.subplots(figsize=figsize)
536
+ handles = []
537
+ # draw the DFT boxes (base colors) first, then the NN boxes (lightened), matching the SI legend
538
+ for source_index, (df, tag, lighten) in enumerate([(dft_df, "DFT", False), (nn_df, "NN", True)]):
539
+ for fi, (formula, label, color) in enumerate(zip(formulas, labels, colors)):
540
+ rgb = plt.cm.colors.to_rgb(color)
541
+ face = tuple(min(1.0, c + 0.3) for c in rgb) if lighten else color
542
+ slot = source_index * n_formulas + fi
543
+ offset = (slot - (n_boxes - 1) / 2) * box_w
544
+ data = [series(df, formula, s) for s in solvents]
545
+ ax.boxplot(data, positions=[c + offset for c in centers], widths=box_w * 0.9,
546
+ patch_artist=True,
547
+ boxprops=dict(color="gray", facecolor=face, alpha=0.9),
548
+ medianprops=dict(color="black", linewidth=0.5),
549
+ whiskerprops=dict(linewidth=0.4), capprops=dict(linewidth=0.4),
550
+ flierprops=dict(marker="o", markersize=0))
551
+ handles.append(plt.Rectangle((0, 0), 1, 1, fc=face, ec="gray", alpha=0.9,
552
+ label=f"{label} ({tag})"))
553
+ ax.set_xticks(centers)
554
+ ax.set_xticklabels(labels_x, rotation=45, ha="right", fontweight="bold")
555
+ ax.set_ylabel("Test RMSE (ppm)", fontweight="bold")
556
+ ax.set_title(f"DFT vs NN Solvent-Specific Test RMSEs\nModel Comparison ({nucleus} nucleus)",
557
+ fontweight="bold")
558
+ ax.set_ylim(bottom=0)
559
+ ax.legend(handles=handles, loc="upper left", fontsize=9)
560
+ fig.tight_layout()
561
+ if save_path:
562
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
563
+ return fig
564
+
565
+
566
+ # ----------------------------------------------------------------------------
567
+ # SI Figures S16-S18: predicted vs experimental solvent-induced shifts by reference solvent
568
+ # ----------------------------------------------------------------------------
569
+ def plot_shift_prediction_scatter_grid(diff_df, solvents, reference_label, axis_label, title_label,
570
+ n_cols=4, save_path=None):
571
+ """Per solvent, the implicit (PCM) and explicit (Desmond) predicted solvent-induced shift
572
+ differences (y) against the measured ones (x), with a y=x guide."""
573
+ n = len(solvents)
574
+ n_rows = (n + n_cols - 1) // n_cols
575
+ fig, axes = plt.subplots(n_rows, n_cols, figsize=(5 * n_cols, 5 * n_rows))
576
+ axes = np.atleast_1d(axes).flatten()
577
+ series = [("implicit_diff", "#FF6B6B", "D"), ("explicit_diff", "#4ECDC4", "o")]
578
+ for ax, solvent in zip(axes, solvents):
579
+ d = diff_df[diff_df["solvent"] == solvent]
580
+ vals = np.concatenate([d["exp_diff"].to_numpy()] + [d[c].to_numpy() for c, *_ in series])
581
+ lim = float(np.nanmax(np.abs(vals))) * 1.05 if len(vals) else 1.0
582
+ ax.plot([-lim, lim], [-lim, lim], "k--", alpha=0.3, lw=1.5, zorder=0)
583
+ for col, color, marker in series:
584
+ ax.scatter(d["exp_diff"], d[col], s=40, color=color, marker=marker, zorder=2)
585
+ tok = axis_label[solvent]
586
+ ax.set_xlabel(f"Experimental delta ({tok} - {reference_label}) [ppm]")
587
+ ax.set_ylabel(f"Correction delta ({reference_label} - {tok}) [ppm]")
588
+ ax.set_title(title_label[solvent], fontweight="bold")
589
+ ax.set_xlim(-lim, lim); ax.set_ylim(-lim, lim); ax.set_aspect("equal")
590
+ legend_handles = [
591
+ Line2D([0], [0], marker="D", color="w", markerfacecolor="#FF6B6B", markersize=9, label="Implicit"),
592
+ Line2D([0], [0], marker="o", color="w", markerfacecolor="#4ECDC4", markersize=9, label="Explicit"),
593
+ Line2D([0], [0], color="gray", linestyle="--", label="Ideal (y = x)")]
594
+ empty = list(axes[n:])
595
+ for ax in empty:
596
+ ax.set_visible(False)
597
+ if empty: # put the legend in the first empty grid slot (S16/S17)
598
+ empty[0].set_visible(True); empty[0].axis("off")
599
+ empty[0].legend(handles=legend_handles, loc="center", frameon=False, fontsize=12)
600
+ fig.tight_layout()
601
+ else: # full 12-panel grid (S18): legend centred below the grid
602
+ fig.tight_layout(rect=[0, 0.045, 1, 1])
603
+ fig.legend(handles=legend_handles, loc="lower center", ncol=3, frameon=False, fontsize=12)
604
+ if save_path:
605
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
606
+ return fig
analysis/code/shared/fixed_point.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Decodes the release's fixed-point integer encoding for shieldings and similar fields.
2
+
3
+ Values are stored as whole numbers equal to the real value times `scale`, with a reserved marker for
4
+ values that were never computed. This turns them back into real values (NaN where missing); float
5
+ input passes through untouched, so the same decoder reads either the released int32 encoding or an
6
+ older plain-float copy.
7
+ """
8
+ import numpy as np
9
+
10
+ FIXED_POINT_SCALE = 1e4
11
+ MISSING_MARKER = -2147483648
12
+
13
+
14
+ def decode_fixed_point(values, scale=FIXED_POINT_SCALE, missing_marker=MISSING_MARKER):
15
+ """Convert fixed-point integers back to real-valued floats, mapping the missing marker to NaN."""
16
+ values = np.asarray(values)
17
+ if np.issubdtype(values.dtype, np.integer):
18
+ out = values.astype(np.float64) / scale
19
+ out[values == missing_marker] = np.nan
20
+ return out
21
+ return np.asarray(values, dtype=np.float64)
analysis/code/shared/spreadsheet.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """Helpers for parsing the experimental spreadsheets' conventions."""
2
+
3
+
4
+ def site_atom_indices(atom_numbers):
5
+ """0-based atom indices for a site from a spreadsheet's 1-based comma-separated atom_numbers
6
+ (e.g. "17,19" or "21"), the convention used by both the delta-22 and applications experimental
7
+ spreadsheets."""
8
+ return [int(x) - 1 for x in str(atom_numbers).split(",")]
analysis/code/shared/stats.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Error statistics and least-squares fitting shared across the analysis modules.
2
+
3
+ numpy.linalg.lstsq, not statsmodels: fitting by formula string through statsmodels' patsy parser is
4
+ about 1000x slower in a loop that fits many (solvent, solute, formula) combinations, so every fitting
5
+ harness here builds a plain design matrix and calls ols_fit directly. Where a module still needs to
6
+ report a p-value or similar statsmodels-only diagnostic, keep a separate, explicitly-named
7
+ statsmodels code path as a test oracle, not the harness itself.
8
+ """
9
+ import numpy as np
10
+
11
+
12
+ def rmse(predicted, actual):
13
+ predicted = np.asarray(predicted, dtype=float)
14
+ actual = np.asarray(actual, dtype=float)
15
+ return float(np.sqrt(np.mean(np.square(predicted - actual))))
16
+
17
+
18
+ def mae(predicted, actual):
19
+ predicted = np.asarray(predicted, dtype=float)
20
+ actual = np.asarray(actual, dtype=float)
21
+ return float(np.mean(np.abs(predicted - actual)))
22
+
23
+
24
+ def median_ae(predicted, actual):
25
+ predicted = np.asarray(predicted, dtype=float)
26
+ actual = np.asarray(actual, dtype=float)
27
+ return float(np.median(np.abs(predicted - actual)))
28
+
29
+
30
+ def summarize_errors(predicted, actual):
31
+ """{"n", "rmse", "mae", "median_ae"} for one (predicted, actual) pair."""
32
+ predicted = np.asarray(predicted, dtype=float)
33
+ actual = np.asarray(actual, dtype=float)
34
+ return {
35
+ "n": int(predicted.size),
36
+ "rmse": rmse(predicted, actual),
37
+ "mae": mae(predicted, actual),
38
+ "median_ae": median_ae(predicted, actual),
39
+ }
40
+
41
+
42
+ def ols_fit(design, response):
43
+ """Least-squares coefficient vector for `design @ beta ~ response`. Callers build `design`
44
+ (including any intercept column) and drop non-finite rows themselves -- this is a thin,
45
+ single-purpose wrapper, not a formula parser."""
46
+ design = np.asarray(design, dtype=float)
47
+ response = np.asarray(response, dtype=float)
48
+ beta, _residuals, _rank, _sv = np.linalg.lstsq(design, response, rcond=None)
49
+ return beta
50
+
51
+
52
+ def linear_fit_1d(x, y):
53
+ """(intercept, slope) of the least-squares line through (x, y)."""
54
+ x = np.asarray(x, dtype=float)
55
+ y = np.asarray(y, dtype=float)
56
+ design = np.column_stack([np.ones(len(x)), x])
57
+ intercept, slope = ols_fit(design, y)
58
+ return float(intercept), float(slope)
analysis/code/shared/test_fixed_point.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ import numpy as np
5
+
6
+ HERE = os.path.dirname(os.path.abspath(__file__))
7
+ sys.path.insert(0, HERE)
8
+
9
+ import fixed_point as F # noqa: E402
10
+
11
+
12
+ def test_decode_int32_with_missing_marker():
13
+ values = np.array([10000, -2147483648, 25000], dtype=np.int32)
14
+ out = F.decode_fixed_point(values)
15
+ assert out[0] == 1.0
16
+ assert np.isnan(out[1])
17
+ assert out[2] == 2.5
18
+
19
+
20
+ def test_decode_int64_also_works():
21
+ # leveling_effect's existing test feeds int64, not just int32 -- the shared decoder must accept
22
+ # any integer dtype, not just int32.
23
+ values = np.array([10000, -2147483648, 25000], dtype=np.int64)
24
+ out = F.decode_fixed_point(values)
25
+ assert out[0] == 1.0
26
+ assert np.isnan(out[1])
27
+ assert out[2] == 2.5
28
+
29
+
30
+ def test_float_input_passes_through():
31
+ values = np.array([1.0, 2.5, float("nan")])
32
+ out = F.decode_fixed_point(values)
33
+ assert out[0] == 1.0
34
+ assert out[1] == 2.5
35
+ assert np.isnan(out[2])
36
+
37
+
38
+ def test_custom_scale_and_marker():
39
+ values = np.array([100, -1], dtype=np.int32)
40
+ out = F.decode_fixed_point(values, scale=100.0, missing_marker=-1)
41
+ assert out[0] == 1.0
42
+ assert np.isnan(out[1])
analysis/code/shared/test_spreadsheet.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ HERE = os.path.dirname(os.path.abspath(__file__))
5
+ sys.path.insert(0, HERE)
6
+
7
+ import spreadsheet as S # noqa: E402
8
+
9
+
10
+ def test_site_atom_indices_single():
11
+ assert S.site_atom_indices("21") == [20]
12
+
13
+
14
+ def test_site_atom_indices_multiple():
15
+ assert S.site_atom_indices("17,19") == [16, 18]
16
+
17
+
18
+ def test_site_atom_indices_accepts_int_input():
19
+ assert S.site_atom_indices(21) == [20]
analysis/code/shared/test_stats.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ import numpy as np
5
+ import pytest
6
+
7
+ HERE = os.path.dirname(os.path.abspath(__file__))
8
+ sys.path.insert(0, HERE)
9
+
10
+ import stats as S # noqa: E402
11
+
12
+
13
+ def test_rmse_mae_median_ae_hand_computed():
14
+ predicted = [1, 2, 3]
15
+ actual = [1, 2, 4]
16
+ # errors = [0, 0, -1]
17
+ assert S.rmse(predicted, actual) == np.sqrt(1 / 3)
18
+ assert S.mae(predicted, actual) == 1 / 3
19
+ assert S.median_ae(predicted, actual) == 0.0
20
+
21
+
22
+ def test_summarize_errors():
23
+ out = S.summarize_errors([1, 2, 3], [1, 2, 4])
24
+ assert out["n"] == 3
25
+ assert out["rmse"] == pytest.approx(np.sqrt(1 / 3))
26
+ assert out["mae"] == pytest.approx(1 / 3)
27
+ assert out["median_ae"] == 0.0
28
+
29
+
30
+ def test_linear_fit_1d_exact_recovery_no_noise():
31
+ x = np.linspace(0, 10, 20)
32
+ y = 2 * x + 1
33
+ intercept, slope = S.linear_fit_1d(x, y)
34
+ assert abs(intercept - 1) < 1e-8
35
+ assert abs(slope - 2) < 1e-8
36
+
37
+
38
+ def test_ols_fit_matches_polyfit():
39
+ rng = np.random.default_rng(0)
40
+ x = rng.normal(size=200)
41
+ y = 3 * x - 2 + rng.normal(scale=0.01, size=200)
42
+ design = np.column_stack([np.ones(len(x)), x])
43
+ beta = S.ols_fit(design, y)
44
+ ref_slope, ref_intercept = np.polyfit(x, y, 1)
45
+ assert abs(beta[0] - ref_intercept) < 1e-6
46
+ assert abs(beta[1] - ref_slope) < 1e-6
analysis/code/test_cp3.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for analysis/code/cp3.py. The fraction-below-DFT statistic is a quantitative claim in the
2
+ paper (36% of proton sites), so it is checked against the shipped Goodman CP3 data, along with the
3
+ zone logic on a synthetic array."""
4
+ import os
5
+ import sys
6
+
7
+ import numpy as np
8
+ import pytest
9
+
10
+ HERE = os.path.dirname(os.path.abspath(__file__))
11
+ sys.path.insert(0, HERE)
12
+
13
+ import cp3 # noqa: E402
14
+
15
+
16
+ def test_fraction_below_dft_synthetic():
17
+ # 10 values: 2 below the experimental floor, 5 in [0.02, 0.10), 3 at/above 0.10. Equal-width
18
+ # bins make the area fraction in [0.02, 0.10] equal to the count fraction there.
19
+ x = np.array([0.005, 0.015, 0.03, 0.04, 0.05, 0.06, 0.07, 0.12, 0.2, 0.3])
20
+ frac = cp3.fraction_below_dft(x, bins=100)
21
+ assert frac == pytest.approx(0.5, abs=0.02) # 5 of 10 in [0.02, 0.10)
22
+
23
+
24
+ def test_zone_boundaries():
25
+ assert cp3._zone(0.01) == "below_experimental"
26
+ assert cp3._zone(0.05) == "below_dft"
27
+ assert cp3._zone(0.2) == "dft_zone"
28
+ assert cp3._zone(0.4) == "large"
29
+
30
+
31
+ REAL = os.path.join(HERE, "..", "..", "data", "cp3", "goodman2009_cp3.xlsx")
32
+
33
+
34
+ @pytest.mark.skipif(not os.path.exists(REAL), reason="goodman2009_cp3.xlsx not present")
35
+ def test_reproduces_published_36_percent():
36
+ variations = cp3.load_variations(REAL, nucleus="H")
37
+ assert len(variations) == 168 # the 1H sites in the CP3 set
38
+ frac = cp3.fraction_below_dft(variations)
39
+ assert frac == pytest.approx(0.36, abs=0.01) # paper: 36% of proton sites below DFT
analysis/code/test_magnet_benchmark.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for analysis/code/magnet_benchmark.py.
2
+
3
+ The error statistics and the published-table structure are checked synthetically, so they run in CI
4
+ without any model or large dataset. Two more tests are opt-in, gated on real data being present
5
+ locally: test_shipped_results_reproduce_published_median_and_mae (the sampled, checkpoint-based
6
+ reproduction; see magnet_benchmark_run.py) and test_exact_stats_table_reproduces_published_values_
7
+ exactly (the full, unsampled reproduction from data/magnet_test_predictions/, which is what the
8
+ shipped notebook actually uses -- see the module docstring's two reproduction paths).
9
+ """
10
+ import os
11
+ import sys
12
+
13
+ import numpy as np
14
+ import pytest
15
+
16
+ HERE = os.path.dirname(os.path.abspath(__file__))
17
+ sys.path.insert(0, HERE)
18
+ import paths
19
+
20
+ import magnet_benchmark as M # noqa: E402
21
+
22
+
23
+ def test_collect_abs_errors_filters_by_element_and_nan():
24
+ an = [np.array([1, 6, 1, 8])]
25
+ pred = [np.array([10.0, 100.0, 11.0, np.nan])]
26
+ dft = [np.array([10.1, 102.0, 10.9, 50.0])]
27
+ h = M.collect_abs_errors(pred, dft, an, z=1)
28
+ c = M.collect_abs_errors(pred, dft, an, z=6)
29
+ assert np.allclose(np.sort(h), [0.1, 0.1]) # the two hydrogens
30
+ assert np.allclose(c, [2.0]) # the one carbon
31
+ # oxygen has a NaN prediction -> dropped, so no oxygen errors survive
32
+ assert M.collect_abs_errors(pred, dft, an, z=8).size == 0
33
+
34
+
35
+ def test_summarize_matches_numpy():
36
+ e = np.array([0.1, 0.2, 0.3, 0.4])
37
+ s = M.summarize(e)
38
+ assert s["median_ae"] == pytest.approx(0.25)
39
+ assert s["mae"] == pytest.approx(0.25)
40
+ assert s["rmse"] == pytest.approx(np.sqrt(np.mean(e ** 2)))
41
+ assert s["n"] == 4
42
+ empty = M.summarize(np.array([]))
43
+ assert empty["n"] == 0 and np.isnan(empty["mae"])
44
+
45
+
46
+ def test_error_table_both_nuclei():
47
+ an = [np.array([1, 6])]
48
+ pred = [np.array([10.0, 100.0])]
49
+ dft = [np.array([10.5, 98.0])]
50
+ t = M.error_table(pred, dft, an)
51
+ assert t["1H"]["mae"] == pytest.approx(0.5)
52
+ assert t["13C"]["mae"] == pytest.approx(2.0)
53
+
54
+
55
+ def test_filter_supported_drops_out_of_vocabulary():
56
+ # second structure contains phosphorus (15), which MagNET was never trained on
57
+ ans = [np.array([6, 1, 1]), np.array([6, 1, 15]), np.array([8, 1, 17])]
58
+ geos = [np.zeros((3, 3))] * 3
59
+ dfts = [np.zeros(3)] * 3
60
+ a, g, d, dropped = M.filter_supported(ans, geos, dfts)
61
+ assert dropped == 1
62
+ assert len(a) == len(g) == len(d) == 2
63
+ assert all(set(np.unique(x).tolist()) <= M.SUPPORTED_ELEMENTS for x in a)
64
+
65
+
66
+ RESULTS = os.path.join(HERE, "..", "..", "data", "magnet_benchmark", "performance_results.csv")
67
+
68
+
69
+ @pytest.mark.skipif(not os.path.exists(RESULTS), reason="performance_results.csv not present")
70
+ def test_shipped_results_reproduce_published_median_and_mae():
71
+ """The shipped reproduced numbers match the published median and mean absolute error (the robust
72
+ statistics). RMSE is intentionally not checked: it is outlier-dominated for the vibrated sets and
73
+ needs the full test set (see the module docstring)."""
74
+ import csv
75
+ # tolerance per nucleus: 1H errors are ~0.03 ppm, 13C ~0.4 ppm, so allow a sampling margin
76
+ TOL = {"1H": 0.015, "13C": 0.12}
77
+ rows = list(csv.DictReader(open(RESULTS)))
78
+ assert len(rows) == 2 * len(M.MODELS) * len(M.TEST_SETS) # 16 rows
79
+ for r in rows:
80
+ pub_median, pub_mae, _ = M.PUBLISHED[r["nucleus"]][(r["model"], r["test_set"])]
81
+ tol = TOL[r["nucleus"]]
82
+ tag = f"{r['model']}/{r['test_set']}/{r['nucleus']}"
83
+ assert abs(float(r["median_ae"]) - pub_median) < tol, f"{tag} median {r['median_ae']} vs {pub_median}"
84
+ assert abs(float(r["mae"]) - pub_mae) < tol, f"{tag} mae {r['mae']} vs {pub_mae}"
85
+
86
+
87
+ def test_predictions_group_matches_documented_naming_scheme():
88
+ # spot-check against the exact group names defined in magnet_benchmark.test_predictions_group
89
+ assert M.test_predictions_group("MagNET", "vibrated_external", "13C") == "gasphasedft8k_C_pretrained_C_vib1"
90
+ assert M.test_predictions_group("MagNET", "isolated_chloroform", "1H") == "solutesmd500isolated_chloroform_H_pretrained_H"
91
+ assert M.test_predictions_group("MagNET-x", "stationary_internal", "1H") == "gasphaseinternal_H_chloroform_H_vib0"
92
+ with pytest.raises(ValueError):
93
+ M.test_predictions_group("MagNET", "not_a_real_test_set", "1H")
94
+
95
+
96
+ PREDICTIONS_H5 = paths.dataset_file("magnet_test_predictions", file=__file__)
97
+
98
+
99
+ @pytest.mark.skipif(not os.path.exists(PREDICTIONS_H5), reason="magnet_test_predictions.hdf5 not present")
100
+ def test_exact_stats_table_reproduces_published_values_exactly():
101
+ """Unlike the sampled test above, this reads the FULL released test sets (no sampling), so median,
102
+ MAE, and RMSE should all match the published SI values to float rounding, not just a sampling
103
+ tolerance -- this is what the shipped si_table_s03_s04_performance.ipynb notebook actually asserts."""
104
+ sys.path.insert(0, os.path.join(HERE, "..", "..", "data", "magnet_test_predictions"))
105
+ import magnet_test_predictions_reader as R
106
+ rows = M.exact_stats_table(PREDICTIONS_H5, R)
107
+ assert len(rows) == 2 * len(M.MODELS) * len(M.TEST_SETS)
108
+ for r in rows:
109
+ pub_median, pub_mae, pub_rmse = M.PUBLISHED[r["nucleus"]][(r["model"], r["test_set"])]
110
+ tag = f"{r['model']}/{r['test_set']}/{r['nucleus']}"
111
+ assert r["median_ae"] == pytest.approx(pub_median, abs=1e-3), tag
112
+ assert r["mae"] == pytest.approx(pub_mae, abs=1e-3), tag
113
+ assert r["rmse"] == pytest.approx(pub_rmse, abs=1e-3), tag
114
+
115
+
116
+ def test_published_table_structure():
117
+ # both nuclei, both models, all four test sets, triples of finite numbers
118
+ for nucleus in ("1H", "13C"):
119
+ for model in M.MODELS:
120
+ for ts in M.TEST_SETS:
121
+ triple = M.PUBLISHED[nucleus][(model, ts)]
122
+ assert len(triple) == 3
123
+ assert all(np.isfinite(v) and v > 0 for v in triple)
124
+ # the 13C vibrated-internal RMSE is the documented outlier-dominated value
125
+ assert M.PUBLISHED["13C"][("MagNET", "vibrated_internal")][2] == pytest.approx(7.1377, abs=1e-3)
analysis/code/test_paths.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ HERE = os.path.dirname(os.path.abspath(__file__))
5
+ sys.path.insert(0, HERE)
6
+
7
+ import paths as P # noqa: E402
8
+
9
+
10
+ def test_repo_root_resolves_this_file_two_levels_up():
11
+ # requirements.txt lives only at the repo root, so its presence is what proves repo_root landed
12
+ # in the right place; the checkout folder's name is not part of the contract (a CI checkout, for
13
+ # example, names it after the GitHub repo, not this developer's local folder name).
14
+ root = P.repo_root(__file__)
15
+ assert os.path.isfile(os.path.join(root, "requirements.txt"))
16
+ assert os.path.basename(root) != "code" # sanity: didn't stop one level too early
17
+
18
+
19
+ def test_repo_root_from_synthetic_tree(tmp_path):
20
+ fake_module = tmp_path / "analysis" / "code" / "x.py"
21
+ fake_module.parent.mkdir(parents=True)
22
+ fake_module.write_text("")
23
+ assert P.repo_root(str(fake_module)) == str(tmp_path)
24
+
25
+
26
+ def test_ensure_on_path_inserts_at_front_and_dedupes():
27
+ # sys.path is a single shared, mutable, process-global list -- other test modules in the same
28
+ # pytest session insert their own entries into the REAL sys.path (some also at index 0), so
29
+ # asserting against the real list is flaky no matter how carefully this test brackets its own
30
+ # calls. Swap in a private list for the duration of the test instead, so ensure_on_path's
31
+ # "insert at index 0, don't duplicate" contract can be checked hermetically.
32
+ real_sys_path = sys.path
33
+ sys.path = ["/some/other/existing/entry"]
34
+ try:
35
+ target = os.path.join(P.repo_root(__file__), "data", "delta22")
36
+ P.ensure_on_path("data", "delta22", file=__file__)
37
+ assert sys.path[0] == target # inserts at the FRONT, so local modules can shadow the rest
38
+ assert sys.path.count(target) == 1
39
+ # inserting the same target again must not duplicate it
40
+ P.ensure_on_path("data", "delta22", file=__file__)
41
+ assert sys.path.count(target) == 1
42
+ assert sys.path[0] == target
43
+ finally:
44
+ sys.path = real_sys_path
45
+
46
+
47
+ def test_ensure_on_path_needs_file_or_root():
48
+ try:
49
+ P.ensure_on_path("data")
50
+ assert False, "expected ValueError"
51
+ except ValueError:
52
+ pass
53
+
54
+
55
+ def test_dataset_file_resolves_in_repo(tmp_path):
56
+ got = P.dataset_file("delta22", root=str(tmp_path))
57
+ assert got == os.path.join(str(tmp_path), "data", "delta22", "delta22.hdf5")
58
+
59
+
60
+ def test_dataset_file_derives_root_from_file(tmp_path):
61
+ # the form most rewired modules use: dataset_file("<name>", file=__file__). A module at
62
+ # analysis/code/x.py must resolve the file two directories up, at <repo>/data/...
63
+ fake = tmp_path / "analysis" / "code" / "x.py"
64
+ fake.parent.mkdir(parents=True)
65
+ fake.write_text("")
66
+ got = P.dataset_file("delta22", file=str(fake))
67
+ assert got == os.path.join(str(tmp_path), "data", "delta22", "delta22.hdf5")
68
+
69
+
70
+ def test_dataset_file_custom_filename_and_needs_a_root():
71
+ got = P.dataset_file("applications", "applications_md_geometries.hdf5", root="/r")
72
+ assert got == os.path.join("/r", "data", "applications", "applications_md_geometries.hdf5")
73
+ try:
74
+ P.dataset_file("delta22") # no root, no file
75
+ assert False, "expected ValueError"
76
+ except ValueError:
77
+ pass
78
+
79
+
80
+ def test_checkpoints_root(monkeypatch, tmp_path):
81
+ # point the resolver at an empty tree, so its model_checkpoints/ is absent (as in CI)
82
+ empty = tmp_path / "repo"
83
+ empty.mkdir()
84
+ monkeypatch.setattr(P, "repo_root", lambda _file: str(empty))
85
+ assert P.checkpoints_root() is None
86
+ try:
87
+ P.checkpoints_root(required=True)
88
+ assert False, "expected RuntimeError"
89
+ except RuntimeError:
90
+ pass
91
+ # in-repo location: model_checkpoints/ present at the repo root
92
+ (empty / "model_checkpoints").mkdir()
93
+ assert P.checkpoints_root() == os.path.join(str(empty), "model_checkpoints")
94
+ assert P.checkpoints_root(required=True) == os.path.join(str(empty), "model_checkpoints")
analysis/code/test_scaling_factors.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for analysis/code/scaling_factors.py.
2
+
3
+ Synthetic tests exercise the two table builders and the prediction equation without any large file,
4
+ so the core math runs in CI. The opt-in real-data test reproduces the published SI numbers (Tables
5
+ S10 and S11) from the released delta-22 data when it is present.
6
+ """
7
+ import os
8
+ import sys
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+ import pytest
13
+
14
+ HERE = os.path.dirname(os.path.abspath(__file__))
15
+ sys.path.insert(0, HERE)
16
+
17
+ import scaling_factors as S # noqa: E402
18
+ import paths as P # noqa: E402
19
+
20
+ REPO = os.path.abspath(os.path.join(HERE, "..", ".."))
21
+ REAL_H5 = P.dataset_file("delta22", root=REPO)
22
+ REAL_XLSX = os.path.join(REPO, "data", "delta22", "delta22_experimental.xlsx")
23
+
24
+
25
+ # --------------------------------------------------------------------------- synthetic builders
26
+
27
+ def _proton_nn(coeffs, n=6, seed=0):
28
+ """Synthetic proton table where experimental = a + b*stationary + c*pcm exactly, per solvent.
29
+ coeffs maps solvent -> (a, b, c)."""
30
+ rng = np.random.default_rng(seed)
31
+ rows = []
32
+ for solvent, (a, b, c) in coeffs.items():
33
+ for i in range(n):
34
+ stat = float(rng.normal(30, 3))
35
+ pcm = float(rng.normal(0, 0.5))
36
+ rows.append(dict(nucleus="H", solute=f"m{i}", site=f"m{i}_0", solvent=solvent,
37
+ stationary=stat, pcm=pcm, experimental=a + b * stat + c * pcm))
38
+ return pd.DataFrame(rows)
39
+
40
+
41
+ def _carbon_nn(coeffs, factors, n=6, seed=1):
42
+ """Synthetic carbon table where experimental = a + b*(stationary + factor*pcm) exactly, per
43
+ solvent. coeffs maps solvent -> (a, b); factors maps solvent -> conversion factor."""
44
+ rng = np.random.default_rng(seed)
45
+ rows = []
46
+ for solvent, (a, b) in coeffs.items():
47
+ factor = factors[solvent]
48
+ for i in range(n):
49
+ stat = float(rng.normal(100, 20))
50
+ pcm = float(rng.normal(0, 0.5))
51
+ rows.append(dict(nucleus="C", solute=f"m{i}", site=f"m{i}_0", solvent=solvent,
52
+ stationary=stat, pcm=pcm, experimental=a + b * (stat + factor * pcm)))
53
+ return pd.DataFrame(rows)
54
+
55
+
56
+ def _nitromethane_rows(nucleus, solvents):
57
+ """One garbage nitromethane row per solvent; the fits must exclude it."""
58
+ return pd.DataFrame([dict(nucleus=nucleus, solute="nitromethane", site="x", solvent=s,
59
+ stationary=0.0, pcm=0.0, experimental=999.0) for s in solvents])
60
+
61
+
62
+ # --------------------------------------------------------------------------- synthetic tests
63
+
64
+ def test_recommended_model_forms():
65
+ # Proton is the three-parameter model, carbon the two-parameter model (John's fitting trials).
66
+ assert S.RECOMMENDED_MODEL == {"H": "three_parameter", "C": "two_parameter"}
67
+
68
+
69
+ def test_predict_shift_equation():
70
+ table = pd.DataFrame(
71
+ {"intercept": [31.0], "stationary": [-0.95], "pcm": [-0.85]},
72
+ index=pd.Index(["chloroform"], name="solvent"),
73
+ )
74
+ got = S.predict_shift(table, "chloroform", magnet_zero_shielding=25.0,
75
+ magnet_pcm_chloroform_correction=-0.1)
76
+ assert got == pytest.approx(31.0 - 0.95 * 25.0 - 0.85 * -0.1)
77
+ got_vec = S.predict_shift(table, "chloroform", [25.0, 26.0], [-0.1, 0.2])
78
+ assert np.allclose(got_vec, [31.0 - 0.95 * 25.0 - 0.85 * -0.1,
79
+ 31.0 - 0.95 * 26.0 - 0.85 * 0.2])
80
+
81
+
82
+ def test_proton_table_recovers_known_line_and_drops_nitromethane():
83
+ coeffs = {"chloroform": (31.0, -0.97, -0.85), "benzene": (32.0, -1.00, 2.20)}
84
+ nn = pd.concat([_proton_nn(coeffs), _nitromethane_rows("H", coeffs)], ignore_index=True)
85
+ table = S.proton_scaling_table(nn, solvents=list(coeffs))
86
+ assert list(table.columns) == ["intercept", "stationary", "pcm"]
87
+ assert table.index.name == "solvent"
88
+ for solvent, (a, b, c) in coeffs.items():
89
+ row = table.loc[solvent]
90
+ # exact recovery (and the nitromethane garbage row did not perturb it -> it was excluded)
91
+ assert row["intercept"] == pytest.approx(a, abs=1e-6)
92
+ assert row["stationary"] == pytest.approx(b, abs=1e-6)
93
+ assert row["pcm"] == pytest.approx(c, abs=1e-6)
94
+
95
+
96
+ def test_carbon_table_reconstruction_and_drops_nitromethane():
97
+ coeffs = {"chloroform": (171.0, -0.92), "benzene": (172.0, -0.93)}
98
+ factors = {"chloroform": 1.0, "benzene": 0.63}
99
+ nn = pd.concat([_carbon_nn(coeffs, factors), _nitromethane_rows("C", coeffs)], ignore_index=True)
100
+ table = S.carbon_scaling_table(nn, solvents=list(coeffs), conversion_factors=factors)
101
+ assert list(table.columns) == ["intercept", "stationary", "pcm"]
102
+ for solvent, (a, b) in coeffs.items():
103
+ row = table.loc[solvent]
104
+ assert row["intercept"] == pytest.approx(a, abs=1e-6)
105
+ assert row["stationary"] == pytest.approx(b, abs=1e-6)
106
+ # the reported pcm is the shared slope times the conversion factor
107
+ assert row["pcm"] == pytest.approx(b * factors[solvent], abs=1e-6)
108
+ assert row["pcm"] == pytest.approx(row["stationary"] * factors[solvent], abs=1e-12)
109
+
110
+
111
+ def test_carbon_empty_or_nan_factor_gives_nan_row():
112
+ # chloroform has data and a finite factor; benzene has no rows and a NaN factor.
113
+ nn = _carbon_nn({"chloroform": (171.0, -0.92)}, {"chloroform": 1.0})
114
+ table = S.carbon_scaling_table(nn, solvents=["chloroform", "benzene"],
115
+ conversion_factors={"chloroform": 1.0, "benzene": np.nan})
116
+ assert np.isfinite(table.loc["chloroform", "intercept"])
117
+ # a degenerate solvent must be NaN, not a fake (0, 0, 0) fit
118
+ assert table.loc["benzene"].isna().all()
119
+
120
+
121
+ def test_carbon_requires_factors_or_dft():
122
+ with pytest.raises(ValueError):
123
+ S.carbon_scaling_table(_carbon_nn({"chloroform": (171.0, -0.92)}, {"chloroform": 1.0}),
124
+ solvents=["chloroform"])
125
+
126
+
127
+ # --------------------------------------------------------------------------- opt-in real-data test
128
+
129
+ # Published SI values: parameter tuples are (intercept, stationary, pcm) per solvent, all 12 solvents.
130
+ # These also pin the shipped published_scaling_tables() copy (no-data test below): shipped CSV ==
131
+ # this dict == build_scaling_tables() from delta-22 (real-data test). The chain keeps all three in sync.
132
+ PUBLISHED_S10_H = {
133
+ "chloroform": (31.294997, -0.9757947, -0.8526936),
134
+ "tetrahydrofuran": (31.321805, -0.9801755, -0.786672),
135
+ "dichloromethane": (31.3773285, -0.979871, -0.8085722),
136
+ "acetone": (31.512167, -0.9872612, -1.2383371),
137
+ "acetonitrile": (31.4961183, -0.9857168, -0.9744366),
138
+ "dimethylsulfoxide": (31.5987046, -0.9911032, -1.3551614),
139
+ "trifluoroethanol": (30.8759924, -0.9599975, -0.9589483),
140
+ "methanol": (31.2560285, -0.9764463, -1.2500843),
141
+ "TIP4P": (31.3591125, -0.978808, -1.4782606),
142
+ "benzene": (31.9876742, -1.0052929, 2.23638523),
143
+ "toluene": (31.7170517, -0.9967364, 1.94472899),
144
+ "chlorobenzene": (31.6814988, -0.9938483, 0.83014615),
145
+ }
146
+ PUBLISHED_S11_C = {
147
+ "chloroform": (171.728792, -0.9242313, -0.9368043),
148
+ "tetrahydrofuran": (171.054483, -0.9190001, -1.069509),
149
+ "dichloromethane": (171.509383, -0.9211671, -1.1150154),
150
+ "acetone": (171.308017, -0.9196101, -1.2395026),
151
+ "acetonitrile": (171.879832, -0.9226291, -1.2876643),
152
+ "dimethylsulfoxide": (170.598418, -0.9186745, -1.2965007),
153
+ "trifluoroethanol": (174.237665, -0.9390125, -1.2900728),
154
+ "methanol": (172.426154, -0.9275656, -1.2888469),
155
+ "TIP4P": (173.696364, -0.937854, -1.3426678),
156
+ "benzene": (171.967174, -0.9270331, -0.5937164),
157
+ "toluene": (171.690904, -0.9249871, -0.6184099),
158
+ "chlorobenzene": (171.075905, -0.921238, -0.997641),
159
+ }
160
+
161
+
162
+ def test_shipped_published_tables_match_si_values():
163
+ """The shipped published_scaling_tables() copy (no data download needed) equals the published SI
164
+ values for all 12 solvents and both nuclei. Runs in CI without delta-22; the real-data test below
165
+ ties those same SI values back to a fit on the raw data, so the shipped copy cannot drift."""
166
+ tables = S.published_scaling_tables()
167
+ for nucleus, published in (("H", PUBLISHED_S10_H), ("C", PUBLISHED_S11_C)):
168
+ table = tables[nucleus]
169
+ assert list(table.columns) == ["intercept", "stationary", "pcm"]
170
+ assert sorted(table.index) == sorted(published)
171
+ for solvent, (intercept, stationary, pcm) in published.items():
172
+ row = table.loc[solvent]
173
+ assert row["intercept"] == pytest.approx(intercept, abs=5e-4), f"{nucleus} {solvent} int"
174
+ assert row["stationary"] == pytest.approx(stationary, abs=5e-4), f"{nucleus} {solvent} stat"
175
+ assert row["pcm"] == pytest.approx(pcm, abs=5e-4), f"{nucleus} {solvent} pcm"
176
+
177
+
178
+ @pytest.mark.skipif(not (os.path.exists(REAL_H5) and os.path.exists(REAL_XLSX)),
179
+ reason="real delta22.hdf5 / experimental xlsx not present")
180
+ def test_reproduces_published_si_tables():
181
+ """Both recommended-scaling tables reproduce the published SI numbers for all 12 solvents. The
182
+ tolerance is set above the int32-encoding floor (worst case ~1.3e-4, the benzene proton pcm) but
183
+ tight enough to catch a real regression."""
184
+ tables = S.build_scaling_tables(REAL_H5, REAL_XLSX)
185
+ for nucleus, published in (("H", PUBLISHED_S10_H), ("C", PUBLISHED_S11_C)):
186
+ table = tables[nucleus]
187
+ assert list(table.columns) == ["intercept", "stationary", "pcm"]
188
+ assert sorted(table.index) == sorted(published)
189
+ for solvent, (intercept, stationary, pcm) in published.items():
190
+ row = table.loc[solvent]
191
+ assert row["intercept"] == pytest.approx(intercept, abs=5e-4), f"{nucleus} {solvent} int"
192
+ assert row["stationary"] == pytest.approx(stationary, abs=5e-4), f"{nucleus} {solvent} stat"
193
+ assert row["pcm"] == pytest.approx(pcm, abs=5e-4), f"{nucleus} {solvent} pcm"
194
+
195
+
196
+ # --------------------------------------------------------------------------- symmetrized-inference smoke test
197
+
198
+ CKPT_ROOT = P.checkpoints_root() or "" # "" (deposit env unset) -> os.path.exists(...) below is False
199
+ CKPT_ZERO = os.path.join(CKPT_ROOT, "MagNET-Zero")
200
+ CKPT_PCM = os.path.join(CKPT_ROOT, "MagNET-PCM")
201
+ _HAS_CHECKPOINTS = os.path.exists(CKPT_ZERO) and os.path.exists(CKPT_PCM)
202
+
203
+
204
+ @pytest.mark.skipif(not (os.path.exists(REAL_H5) and os.path.exists(REAL_XLSX) and _HAS_CHECKPOINTS),
205
+ reason="real delta22.hdf5 / experimental xlsx / model checkpoints not present")
206
+ def test_symmetrized_build_scaling_tables_runs_and_is_close_to_published():
207
+ """symmetrized=True re-derives the tables from live, reflection-symmetrized inference instead
208
+ of the HDF5's stored (unsymmetrized) shieldings. This is a deployment-quality table (e.g. for
209
+ serving MagNET-Zero/PCM on new molecules), not a replacement for the published SI numbers --
210
+ it should be close (delta-22 solutes are small, so the correction is modest) but need not match
211
+ to the same tight tolerance as test_reproduces_published_si_tables."""
212
+ pytest.importorskip("torch")
213
+ tables = S.build_scaling_tables(REAL_H5, REAL_XLSX, symmetrized=True, n_passes=2)
214
+ max_abs_delta = 0.0
215
+ for nucleus, published in (("H", PUBLISHED_S10_H), ("C", PUBLISHED_S11_C)):
216
+ table = tables[nucleus]
217
+ assert list(table.columns) == ["intercept", "stationary", "pcm"]
218
+ assert sorted(table.index) == sorted(published)
219
+ for solvent, (intercept, stationary, pcm) in published.items():
220
+ row = table.loc[solvent]
221
+ assert np.isfinite(row["intercept"]) and np.isfinite(row["stationary"]) and np.isfinite(row["pcm"])
222
+ # loose tolerance: this is a methodology check (does it run and land in the right
223
+ # ballpark), not a bit-for-bit reproduction -- see module docstring for measured deltas
224
+ assert row["intercept"] == pytest.approx(intercept, abs=0.05), f"{nucleus} {solvent} int"
225
+ assert row["stationary"] == pytest.approx(stationary, abs=0.01), f"{nucleus} {solvent} stat"
226
+ assert row["pcm"] == pytest.approx(pcm, abs=0.1), f"{nucleus} {solvent} pcm"
227
+ max_abs_delta = max(max_abs_delta, abs(row["intercept"] - intercept),
228
+ abs(row["stationary"] - stationary), abs(row["pcm"] - pcm))
229
+ # symmetrized inference must actually change something -- if nn_shieldings_override_df were
230
+ # silently dropped and this fell back to the exact HDF5 path, every delta above would be 0.0
231
+ # and still pass the loose tolerances; this catches that failure mode specifically
232
+ assert max_abs_delta > 1e-4, "symmetrized=True produced numbers identical to published -- the override path did not run"
233
+
234
+
235
+ @pytest.mark.skipif(not (os.path.exists(REAL_H5) and os.path.exists(REAL_XLSX) and _HAS_CHECKPOINTS),
236
+ reason="real delta22.hdf5 / experimental xlsx / model checkpoints not present")
237
+ def test_shipped_symmetrized_csvs_reproduce_from_live_inference():
238
+ """Value-traceability for the deployment CSVs in data/scaling_factors/: they must reproduce from
239
+ a live symmetrized build at the n_passes they were generated with (10), not merely parse. A
240
+ swapped column, wrong solvent order, or stale hand-edit of the shipped CSVs fails here. The
241
+ tolerance absorbs the model's pass-to-pass inference noise (a single forward pass is not
242
+ deterministic) but is far tighter than any structural error."""
243
+ pytest.importorskip("torch")
244
+ sys.path.insert(0, os.path.join(REPO, "data", "scaling_factors"))
245
+ import scaling_factors_reader as R # noqa: E402
246
+ shipped = R.load_symmetrized_tables()
247
+ fresh = S.build_scaling_tables(REAL_H5, REAL_XLSX, symmetrized=True, n_passes=10)
248
+ for nucleus in ("H", "C"):
249
+ s, f = shipped[nucleus], fresh[nucleus]
250
+ assert list(s.columns) == list(f.columns) == ["intercept", "stationary", "pcm"]
251
+ assert sorted(s.index) == sorted(f.index)
252
+ for solvent in s.index:
253
+ assert s.loc[solvent, "intercept"] == pytest.approx(f.loc[solvent, "intercept"], abs=0.03), f"{nucleus} {solvent} intercept"
254
+ assert s.loc[solvent, "stationary"] == pytest.approx(f.loc[solvent, "stationary"], abs=0.01), f"{nucleus} {solvent} stationary"
255
+ assert s.loc[solvent, "pcm"] == pytest.approx(f.loc[solvent, "pcm"], abs=0.02), f"{nucleus} {solvent} pcm"
256
+
257
+
258
+ def test_magnet_package_scaling_matches_canonical_tables():
259
+ """The tiny copy shipped in the importable package (magnet.scaling, used internally by
260
+ magnet.predict_shifts) must equal the canonical SI tables here, so the two can never drift."""
261
+ pytest.importorskip("torch") # importing the magnet package pulls in the torch stack
262
+ from magnet import scaling as MS
263
+ canonical = S.published_scaling_tables() # DataFrames indexed by solvent
264
+ tiny = MS.published_scaling_tables() # {solvent: {column: value}} dicts
265
+ for nucleus in ("H", "C"):
266
+ assert set(tiny[nucleus]) == set(canonical[nucleus].index)
267
+ for solvent, coeffs in tiny[nucleus].items():
268
+ for column in ("intercept", "stationary", "pcm"):
269
+ assert coeffs[column] == pytest.approx(float(canonical[nucleus].loc[solvent, column]))
270
+ # the two predict_shift implementations give the same shift
271
+ assert float(MS.predict_shift(tiny["C"], "chloroform", 170.0, -0.3)) == pytest.approx(
272
+ float(S.predict_shift(canonical["C"], "chloroform", 170.0, -0.3)))
analysis/manuscript_figures/fig1a_cp3.ipynb ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "01850ff6",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 1A: CP3 per-site ¹H shift spread vs the DFT and experimental resolution limits\n",
9
+ "\n",
10
+ "Histogram of per-site ¹H shift spread across the Goodman CP3 stereoisomers (Goodman, JOC 2009, 74,\n",
11
+ "4597), against the experimental noise floor (0.02 ppm) and DFT's resolving power (0.10 ppm)."
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "id": "a460d80b",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "import os, sys\n",
22
+ "\n",
23
+ "# make the in-repo modules importable (not pip-installed)\n",
24
+ "REPO = os.path.abspath(\"../..\")\n",
25
+ "for _p in (\"analysis/code\",):\n",
26
+ " sys.path.insert(0, os.path.join(REPO, _p))"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "id": "e0adf094",
33
+ "metadata": {},
34
+ "outputs": [],
35
+ "source": [
36
+ "import numpy as np\n",
37
+ "import matplotlib.pyplot as plt\n",
38
+ "import seaborn as sns\n",
39
+ "from matplotlib.ticker import FixedLocator\n",
40
+ "\n",
41
+ "import cp3"
42
+ ]
43
+ },
44
+ {
45
+ "cell_type": "code",
46
+ "execution_count": null,
47
+ "id": "b68e0918",
48
+ "metadata": {},
49
+ "outputs": [],
50
+ "source": [
51
+ "def figure_path(name):\n",
52
+ " os.makedirs(\"figures\", exist_ok=True)\n",
53
+ " return os.path.join(\"figures\", name)"
54
+ ]
55
+ },
56
+ {
57
+ "cell_type": "code",
58
+ "execution_count": null,
59
+ "id": "f599e59b",
60
+ "metadata": {},
61
+ "outputs": [],
62
+ "source": [
63
+ "variations = cp3.load_variations(nucleus=\"H\")\n",
64
+ "frac = cp3.fraction_below_dft(variations)\n",
65
+ "print(f\"{len(variations)} proton sites; {frac:.1%} fall between the experimental floor \"\n",
66
+ " f\"({cp3.EXPERIMENTAL_LIMIT} ppm) and the DFT limit ({cp3.DFT_LIMIT} ppm)\")"
67
+ ]
68
+ },
69
+ {
70
+ "cell_type": "code",
71
+ "execution_count": null,
72
+ "id": "c88605cd",
73
+ "metadata": {},
74
+ "outputs": [],
75
+ "source": [
76
+ "# color per zone; keys match cp3._zone's return values\n",
77
+ "zone_colors = {\n",
78
+ " \"below_experimental\": \"#2c4553\", # < 0.02 ppm: within experimental noise\n",
79
+ " \"below_dft\": \"#a72608\", # 0.02 - 0.10 ppm: informative but DFT cannot resolve\n",
80
+ " \"dft_zone\": \"#dacd82\", # 0.10 - 0.30 ppm: resolvable by DFT\n",
81
+ " \"large\": \"#68a79a\", # >= 0.30 ppm\n",
82
+ "}\n",
83
+ "boundaries = (cp3.EXPERIMENTAL_LIMIT, cp3.DFT_LIMIT, cp3.LARGE_VARIATION)"
84
+ ]
85
+ },
86
+ {
87
+ "cell_type": "code",
88
+ "execution_count": null,
89
+ "id": "03efe205",
90
+ "metadata": {},
91
+ "outputs": [],
92
+ "source": [
93
+ "sns.set_theme(context=\"paper\", style=\"white\")\n",
94
+ "fig, ax = plt.subplots(figsize=(8, 4))\n",
95
+ "\n",
96
+ "bins = 30\n",
97
+ "edges = np.linspace(0.0, float(variations.max()), bins + 1)\n",
98
+ "counts, edge = np.histogram(variations, bins=edges)\n",
99
+ "\n",
100
+ "# Draw each bar, splitting any bin that straddles a zone boundary so each segment gets its color.\n",
101
+ "for height, left, right in zip(counts, edge[:-1], edge[1:]):\n",
102
+ " splits = sorted({left, right} | {b for b in boundaries if left < b < right})\n",
103
+ " for a, b in zip(splits[:-1], splits[1:]):\n",
104
+ " ax.bar(a, height, width=b - a, align=\"edge\",\n",
105
+ " color=zone_colors[cp3._zone(0.5 * (a + b))],\n",
106
+ " edgecolor=\"white\", linewidth=0.6, alpha=0.9)\n",
107
+ "\n",
108
+ "ax.set_xlim(left=0.0)\n",
109
+ "ax.set_ylim(0, ax.get_ylim()[1])\n",
110
+ "ax.margins(x=0)\n",
111
+ "ax.set_title(\"Chemical shift variation (¹H)\", pad=8, fontsize=16)\n",
112
+ "ax.set_xlabel(\"Variation at Site (standard deviation, ¹H ppm)\", fontsize=13)\n",
113
+ "ax.set_ylabel(\"Count\", fontsize=13)\n",
114
+ "sns.despine(ax=ax, top=True, right=True)\n",
115
+ "for side in (\"left\", \"bottom\"):\n",
116
+ " ax.spines[side].set_linewidth(0.6)\n",
117
+ "ax.xaxis.set_major_locator(FixedLocator([0.0, 0.1, 0.2, 0.3, 0.4, 0.5]))\n",
118
+ "ax.tick_params(axis=\"both\", which=\"major\", labelsize=10, size=1.5, width=0.6)\n",
119
+ "ax.minorticks_off()\n",
120
+ "fig.tight_layout()\n",
121
+ "\n",
122
+ "fig.savefig(figure_path(\"fig1a_cp3_1H.png\"), dpi=300, bbox_inches=\"tight\", pad_inches=0.02)\n",
123
+ "plt.show()"
124
+ ]
125
+ }
126
+ ],
127
+ "metadata": {
128
+ "language_info": {
129
+ "codemirror_mode": {
130
+ "name": "ipython",
131
+ "version": 3
132
+ },
133
+ "file_extension": ".py",
134
+ "mimetype": "text/x-python",
135
+ "name": "python",
136
+ "nbconvert_exporter": "python",
137
+ "pygments_lexer": "ipython3",
138
+ "version": "3.12.13"
139
+ }
140
+ },
141
+ "nbformat": 4,
142
+ "nbformat_minor": 5
143
+ }
analysis/manuscript_figures/fig2a_pareto.ipynb ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "55d51b05",
6
+ "metadata": {},
7
+ "source": "# Figure 2A: accuracy-vs-compute-cost Pareto frontier (delta-22 ¹H, CDCl3)\n\nFitting RMSE vs total compute time (geometry + shielding), colored by method type, shaped by geometry source, sized by basis."
8
+ },
9
+ {
10
+ "cell_type": "code",
11
+ "execution_count": null,
12
+ "id": "a5fb8cc9",
13
+ "metadata": {},
14
+ "outputs": [],
15
+ "source": "import os, sys\n\n# make the in-repo modules importable (not pip-installed)\nREPO = os.path.abspath(\"../..\")\nfor _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n sys.path.insert(0, os.path.join(REPO, _p))"
16
+ },
17
+ {
18
+ "cell_type": "code",
19
+ "id": "24c54da5",
20
+ "source": "import delta22\nimport pareto_plot\nimport paths",
21
+ "metadata": {},
22
+ "execution_count": null,
23
+ "outputs": []
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "id": "c2b77287",
28
+ "source": "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\nXLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n\ndef figure_path(name):\n os.makedirs(\"figures\", exist_ok=True)\n return os.path.join(\"figures\", name)",
29
+ "metadata": {},
30
+ "execution_count": null,
31
+ "outputs": []
32
+ },
33
+ {
34
+ "cell_type": "code",
35
+ "execution_count": null,
36
+ "id": "c8daed8f",
37
+ "metadata": {},
38
+ "outputs": [],
39
+ "source": "dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)\nnn = delta22.load_query_df_nn(DELTA22_HDF5, XLSX, verbose=False)\ndft_gas_timings, nn_timings = delta22.load_pareto_timings(DELTA22_HDF5)\n\n# one point per method/basis/geometry/nucleus/solvent (plus solvent-averaged rows); MagNET is a single\n# method (aimnet2, basis \"N/A\"). 100 seeded splits (train on the first 10 shuffled solutes, test on the\n# rest) to match the published panel.\npoints = delta22.fig2a_pareto_points(dft, nn, dft_gas_timings, nn_timings, n_splits=100)\nprint(points[\"nmr_method\"].nunique(), \"methods;\",\n \"MagNET total time\", float(points.query(\"nmr_method=='MagNET'\")[\"total_time\"].iloc[0]), \"s\")"
40
+ },
41
+ {
42
+ "cell_type": "markdown",
43
+ "id": "556ad0b0",
44
+ "metadata": {},
45
+ "source": [
46
+ "## Proton Pareto panel"
47
+ ]
48
+ },
49
+ {
50
+ "cell_type": "code",
51
+ "execution_count": null,
52
+ "id": "9c9a93fd",
53
+ "metadata": {},
54
+ "outputs": [],
55
+ "source": "method_legend_label_map = {\n \"MagNET-Zero\": \"MagNET-Zero\", \"ab initio\": r\"$\\it{ab\\ initio}$\", \"double hybrids\": \"double hybrid\",\n \"NMR-specific\": \"NMR-specific\", \"DFT\": \"DFT\",\n}\ngeometry_legend_label_map = {\"pbe0_tz\": \"DFT (PBE0/cc-pVTZ)\", \"aimnet2\": \"ML (AIMNet2)\"}\nmethod_color_map = {\"ab initio\": \"#53585f\", \"MagNET-Zero\": \"#000000\", \"double hybrids\": \"#b3a560\",\n \"NMR-specific\": \"#a72608\", \"DFT\": \"#4a8075\"}\n\n# which points to draw: MagNET-Zero, all PBE0-geometry DFT methods, plus the wp04 NMR-specific point\nquery_str = (\"nucleus=='{nucleus}' and solvent == 'chloroform' and (\"\n \" (geometry_type=='aimnet2' and nmr_method.str.startswith('MagNET')) or\"\n \" (geometry_type=='pbe0_tz' and not nmr_method.str.startswith('MagNET')) or\"\n \" (geometry_type=='aimnet2' and nmr_method=='wp04' and basis=='pcSseg2' and nucleus=='H'))\")\n\n# hand-tuned label offsets (points, matching the published panel)\nmanual_label_positions = {\n (\"MagNET-Zero\", \"N/A\", \"aimnet2\"): {\"xytext\": (-28, 14)},\n (\"wp04\", \"pcSseg2\", \"aimnet2\"): {\"xytext\": (6, 4)},\n (\"pbe0\", \"pcSseg1\", \"pbe0_tz\"): {\"xytext\": (-22, -2)},\n (\"b2gp_plyp\", \"pcSseg1\", \"pbe0_tz\"): {\"xytext\": (4, 6)},\n (\"revdsd_pbep86\", \"pcSseg1\", \"pbe0_tz\"): {\"xytext\": (6, -4)},\n (\"revdsd_pbep86\", \"pcSseg2\", \"pbe0_tz\"): {\"xytext\": (6, 6)},\n (\"tpsstpss\", \"pcSseg2\", \"pbe0_tz\"): {\"xytext\": (6, -2)},\n (\"b97d3\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (-28, -2)},\n (\"mpw2plyp\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (8, 2)},\n (\"b2plyp\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (8, 0)},\n (\"tpsstpss\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (6, -12)},\n}"
56
+ },
57
+ {
58
+ "cell_type": "code",
59
+ "id": "8d87338a",
60
+ "source": "pareto_plot.plot_pareto_panel(\n points, query_str, nucleus=\"H\",\n method_color_map=method_color_map,\n method_legend_label_map=method_legend_label_map,\n geometry_legend_label_map=geometry_legend_label_map,\n geometry_marker_map={\"aimnet2\": \"x\", \"pbe0_tz\": \"o\"},\n xlim_left=(1.7, 1.8), xlim_right=(3.8, 5.5), ylim=(0.08, 0.3),\n figsize=(12, 6), marker_alpha=0.8,\n manual_label_positions=manual_label_positions,\n save_png=figure_path(\"fig2a_pareto_1H.png\"),\n)",
61
+ "metadata": {},
62
+ "execution_count": null,
63
+ "outputs": []
64
+ }
65
+ ],
66
+ "metadata": {
67
+ "language_info": {
68
+ "codemirror_mode": {
69
+ "name": "ipython",
70
+ "version": 3
71
+ },
72
+ "file_extension": ".py",
73
+ "mimetype": "text/x-python",
74
+ "name": "python",
75
+ "nbconvert_exporter": "python",
76
+ "pygments_lexer": "ipython3",
77
+ "version": "3.12.13"
78
+ }
79
+ },
80
+ "nbformat": 4,
81
+ "nbformat_minor": 5
82
+ }
analysis/manuscript_figures/fig2b_correlation.ipynb ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "e8c73ac7",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 2B: inter-method correlation of delta-22 ¹H stationary shieldings\n",
9
+ "\n",
10
+ "Lower-triangle Pearson correlation matrix of per-site ¹H delta-22 stationary shieldings across methods\n",
11
+ "(pcSseg-2 basis). The colorbar is Pearson r; the published panel mislabels it r², a typo, corrected here."
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "id": "ae047e23",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "import os, sys\n",
22
+ "\n",
23
+ "# make the in-repo modules importable (not pip-installed)\n",
24
+ "REPO = os.path.abspath(\"../..\")\n",
25
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
26
+ " sys.path.insert(0, os.path.join(REPO, _p))"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "id": "3d783476",
33
+ "metadata": {},
34
+ "outputs": [],
35
+ "source": [
36
+ "import numpy as np\n",
37
+ "\n",
38
+ "import delta22\n",
39
+ "import paths\n",
40
+ "import fig2b_plots"
41
+ ]
42
+ },
43
+ {
44
+ "cell_type": "code",
45
+ "execution_count": null,
46
+ "id": "9f91feb0",
47
+ "metadata": {},
48
+ "outputs": [],
49
+ "source": [
50
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
51
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
52
+ "\n",
53
+ "def figure_path(name):\n",
54
+ " os.makedirs(\"figures\", exist_ok=True)\n",
55
+ " return os.path.join(\"figures\", name)"
56
+ ]
57
+ },
58
+ {
59
+ "cell_type": "code",
60
+ "execution_count": null,
61
+ "id": "569d9eef",
62
+ "metadata": {},
63
+ "outputs": [],
64
+ "source": [
65
+ "# Gas-phase (\"stationary\") shieldings, one value per (solute, geometry, basis, site, nucleus, method).\n",
66
+ "# The stationary shielding does not depend on solvent, so keep one solvent (TIP4P) to deduplicate.\n",
67
+ "NUCLEUS = \"H\"\n",
68
+ "BASIS = \"pcSseg2\"\n",
69
+ "\n",
70
+ "dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)\n",
71
+ "corr_df = dft[[\"solute\", \"sap_geometry_type\", \"sap_nmr_method\", \"sap_basis\",\n",
72
+ " \"nucleus\", \"site\", \"solvent\", \"stationary\"]].copy()\n",
73
+ "corr_df.columns = [c.replace(\"sap_\", \"\") for c in corr_df.columns]\n",
74
+ "corr_df = corr_df.query('solvent == \"TIP4P\"').drop(columns=[\"solvent\"])\n",
75
+ "\n",
76
+ "pivot = corr_df.pivot(index=[\"solute\", \"geometry_type\", \"basis\", \"site\", \"nucleus\"],\n",
77
+ " columns=\"nmr_method\", values=\"stationary\")\n",
78
+ "sub = pivot.query(\"nucleus == @NUCLEUS and basis == @BASIS\")\n",
79
+ "correlations = sub.corr()\n",
80
+ "off_diag = correlations.where(~np.eye(len(correlations), dtype=bool))\n",
81
+ "print(f\"{correlations.shape[0]} methods; worst pairwise Pearson r = {off_diag.min().min():.5f}\")"
82
+ ]
83
+ },
84
+ {
85
+ "cell_type": "code",
86
+ "execution_count": null,
87
+ "id": "f866aab1",
88
+ "metadata": {},
89
+ "outputs": [],
90
+ "source": [
91
+ "fig2b_plots.plot_correlation_matrix(correlations, save_png=figure_path(\"fig2b_correlation_1H.png\"))"
92
+ ]
93
+ }
94
+ ],
95
+ "metadata": {
96
+ "language_info": {
97
+ "codemirror_mode": {
98
+ "name": "ipython",
99
+ "version": 3
100
+ },
101
+ "file_extension": ".py",
102
+ "mimetype": "text/x-python",
103
+ "name": "python",
104
+ "nbconvert_exporter": "python",
105
+ "pygments_lexer": "ipython3",
106
+ "version": "3.12.13"
107
+ }
108
+ },
109
+ "nbformat": 4,
110
+ "nbformat_minor": 5
111
+ }
analysis/manuscript_figures/fig2d_convergence.ipynb ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "8e86520a",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 2D: per-method scaled error and slope vs a CCSD(T) reference (NS372)\n",
9
+ "\n",
10
+ "Each conventional method placed by **scaled error** (RMSE after rescaling into CCSD(T) units via the\n",
11
+ "per-method fit) and **slope** of that fit, vs a CCSD(T)/pcSseg-3 reference on the NS372 benchmark\n",
12
+ "(proton shieldings); third axis is the method's introduction year, colored by Jacob's-ladder rung."
13
+ ]
14
+ },
15
+ {
16
+ "cell_type": "code",
17
+ "execution_count": null,
18
+ "id": "37fd61c1",
19
+ "metadata": {},
20
+ "outputs": [],
21
+ "source": [
22
+ "import os, sys\n",
23
+ "\n",
24
+ "# make the in-repo modules importable (not pip-installed)\n",
25
+ "REPO = os.path.abspath(\"../..\")\n",
26
+ "for _p in (\"analysis/code\", \"analysis/code/shared\"):\n",
27
+ " sys.path.insert(0, os.path.join(REPO, _p))"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "code",
32
+ "execution_count": null,
33
+ "id": "3de7e8c2",
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": "import leveling\nimport convergence_plot"
37
+ },
38
+ {
39
+ "cell_type": "code",
40
+ "execution_count": null,
41
+ "id": "de9503d1",
42
+ "metadata": {},
43
+ "outputs": [],
44
+ "source": [
45
+ "# the Kaupp NS372 spreadsheet is small and ships in the repo's data/ns372/ folder\n",
46
+ "KAUPP_XLSX = os.path.join(REPO, \"data\", \"ns372\", \"ct1c00919_si_002.xlsx\")\n",
47
+ "\n",
48
+ "def figure_path(name):\n",
49
+ " os.makedirs(\"figures\", exist_ok=True)\n",
50
+ " return os.path.join(\"figures\", name)"
51
+ ]
52
+ },
53
+ {
54
+ "cell_type": "code",
55
+ "execution_count": null,
56
+ "id": "a3bba2c1",
57
+ "metadata": {},
58
+ "outputs": [],
59
+ "source": [
60
+ "# the 1H / pcSseg-3 slice as a raw-named table, then per-method statistics (slope and scaled RMSE vs CCSD(T)).\n",
61
+ "cc_df = leveling.load_ns372_1h_convergence(KAUPP_XLSX)\n",
62
+ "fig2c_results_df = leveling.convergence_statistics(cc_df)\n",
63
+ "print(f\"{len(fig2c_results_df)} methods vs CCSD(T)/pcSseg-3\")\n",
64
+ "fig2c_results_df.sort_values(\"RMSE (scaled)\").head(6)"
65
+ ]
66
+ },
67
+ {
68
+ "cell_type": "code",
69
+ "execution_count": null,
70
+ "id": "afb2a097",
71
+ "metadata": {},
72
+ "outputs": [],
73
+ "source": [
74
+ "# rung colors follow Jacob's ladder (DFT approximation hierarchy).\n",
75
+ "METHOD_COLOR_MAP = {\n",
76
+ " \"Ab Initio\": \"#2B2B2B\",\n",
77
+ " \"LDA\": \"#3B82F6\",\n",
78
+ " \"GGA\": \"#1A2A6B\",\n",
79
+ " \"Meta-GGA\": \"#1E7A8B\",\n",
80
+ " \"Global Hybrid\": \"#22C55E\",\n",
81
+ " \"Range-Separated Hybrid\": \"#F59E0B\",\n",
82
+ " \"Local Hybrid\": \"#EF4444\",\n",
83
+ " \"Double Hybrid\": \"#A855F7\",\n",
84
+ "}\n",
85
+ "\n",
86
+ "methods_categorization = {\n",
87
+ " \"HF\": \"Ab Initio\", \"MP2\": \"Ab Initio\",\n",
88
+ " \"SVWN\": \"LDA\",\n",
89
+ " \"BP86\": \"GGA\", \"BLYP\": \"GGA\", \"PBE\": \"GGA\", \"KT1\": \"GGA\", \"KT2\": \"GGA\",\n",
90
+ " \"KT3\": \"GGA\", \"HCTH\": \"GGA\", \"B97D\": \"GGA\",\n",
91
+ " \"TPSS\": \"Meta-GGA\", \"τ-HCTH\": \"Meta-GGA\", \"M06-L\": \"Meta-GGA\", \"VSXC\": \"Meta-GGA\",\n",
92
+ " \"MN15-L\": \"Meta-GGA\", \"B97M-V\": \"Meta-GGA\", \"SCAN\": \"Meta-GGA\", \"rSCAN\": \"Meta-GGA\",\n",
93
+ " \"r2SCAN\": \"Meta-GGA\",\n",
94
+ " \"TPSSh\": \"Global Hybrid\", \"B3LYP\": \"Global Hybrid\", \"B97-2\": \"Global Hybrid\",\n",
95
+ " \"PBE0\": \"Global Hybrid\", \"M06\": \"Global Hybrid\", \"PW6B95\": \"Global Hybrid\",\n",
96
+ " \"BHLYP\": \"Global Hybrid\", \"MN15\": \"Global Hybrid\", \"M06-2X\": \"Global Hybrid\",\n",
97
+ " \"CAM-B3LYP\": \"Range-Separated Hybrid\", \"ωB97X-D\": \"Range-Separated Hybrid\",\n",
98
+ " \"ωB97X-V\": \"Range-Separated Hybrid\", \"ωB97M-V\": \"Range-Separated Hybrid\",\n",
99
+ " \"LH07s-SVWN\": \"Local Hybrid\", \"MPSTS\": \"Local Hybrid\", \"LHJ14\": \"Local Hybrid\",\n",
100
+ " \"LH07t-SVWN\": \"Local Hybrid\", \"LH12ct-SsirPW92\": \"Local Hybrid\",\n",
101
+ " \"LH12ct-SsifPW92\": \"Local Hybrid\", \"LH14t-calPBE\": \"Local Hybrid\", \"LH20t\": \"Local Hybrid\",\n",
102
+ " \"B2PLYP\": \"Double Hybrid\", \"B2GP-PLYP\": \"Double Hybrid\", \"DSD-PBEP86\": \"Double Hybrid\",\n",
103
+ "}\n",
104
+ "\n",
105
+ "year_map = {\n",
106
+ " \"ωB97X-D\": 2008, \"ωB97X-V\": 2014, \"r2SCAN\": 2020, \"M06-L\": 2006, \"B97D\": 1997,\n",
107
+ " \"SVWN\": 1980, \"MP2\": 1934, \"HF\": 1930, \"DSD-PBEP86\": 2011, \"BP86\": 1988, \"BLYP\": 1988,\n",
108
+ " \"PBE\": 1996, \"KT1\": 2003, \"KT2\": 2003, \"HCTH\": 1998, \"TPSS\": 2003, \"τ-HCTH\": 2002,\n",
109
+ " \"VSXC\": 1998, \"MN15-L\": 2016, \"B97M-V\": 2015, \"KT3\": 2004, \"SCAN\": 2015, \"rSCAN\": 2019,\n",
110
+ " \"TPSSh\": 2003, \"B3LYP\": 1994, \"B97-2\": 1998, \"PBE0\": 1999, \"M06\": 2008, \"PW6B95\": 2005,\n",
111
+ " \"BHLYP\": 1993, \"MN15\": 2016, \"M06-2X\": 2008, \"CAM-B3LYP\": 2004, \"ωB97M-V\": 2016,\n",
112
+ " \"LH07s-SVWN\": 2007, \"LH07t-SVWN\": 2007, \"LHJ14\": 2014, \"LH12ct-SsirPW92\": 2017,\n",
113
+ " \"LH12ct-SsifPW92\": 2017, \"LH14t-calPBE\": 2014, \"LH20t\": 2020, \"B2PLYP\": 2006,\n",
114
+ " \"B2GP-PLYP\": 2006, \"MPSTS\": 2021,\n",
115
+ "}"
116
+ ]
117
+ },
118
+ {
119
+ "cell_type": "code",
120
+ "execution_count": null,
121
+ "id": "62e50faa",
122
+ "metadata": {},
123
+ "outputs": [],
124
+ "source": "highlight_methods = [\n \"DSD-PBEP86\", \"LH07t-SVWN\", \"LH14t-calPBE\", \"ωB97X-D\", \"ωB97X-V\",\n \"BHLYP\", \"M06-L\", \"r2SCAN\", \"B97D\", \"SVWN\", \"MP2\", \"HF\",\n]\n\nfig, ax, saved = convergence_plot.plot_statistics_3d_emphasize_rungs(\n x_param=\"Slope\",\n y_param=\"RMSE (scaled)\",\n z_param=None,\n results_df=fig2c_results_df,\n methods_categorization=methods_categorization,\n method_color_map=METHOD_COLOR_MAP,\n year_map=year_map,\n save_path=figure_path(\"fig2d_convergence_1H.png\"),\n dpi=200,\n highlight_methods=highlight_methods,\n highlight_marker_size=80, # uniform with the non-highlighted points\n highlight_edgewidth=1.3,\n highlight_text=True,\n highlight_text_size=8,\n highlight_text_dx_frac=0.03,\n highlight_text_dy_frac=-0.05,\n highlight_text_dz=2.0,\n year_tick_step=5,\n z_jitter_by_category=0.03,\n z_lane_by_category=True,\n z_lane_width=2,\n elev=12,\n azim=25,\n xlim=(0.99, 1.105),\n ylim=(0, 0.40),\n z_break=(1935, 1980),\n z_break_gap=8,\n zlim=(1930, 2025),\n xy_lane_by_category=True,\n xy_lane_strength=0.1,\n xy_lane_style=\"circle\",\n lane_exclude_categories=(\"ab initio\",),\n group_by_decade=False,\n)"
125
+ }
126
+ ],
127
+ "metadata": {
128
+ "kernelspec": {
129
+ "display_name": "magnet-venv",
130
+ "language": "python",
131
+ "name": "magnet-venv"
132
+ },
133
+ "language_info": {
134
+ "codemirror_mode": {
135
+ "name": "ipython",
136
+ "version": 3
137
+ },
138
+ "file_extension": ".py",
139
+ "mimetype": "text/x-python",
140
+ "name": "python",
141
+ "nbconvert_exporter": "python",
142
+ "pygments_lexer": "ipython3",
143
+ "version": "3.12.13"
144
+ }
145
+ },
146
+ "nbformat": 4,
147
+ "nbformat_minor": 5
148
+ }
analysis/manuscript_figures/fig3a_pcm_benefit.ipynb ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "d13d8aa5",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 3A: implicit-solvent (PCM) benefit by method, chloroform vs benzene\n",
9
+ "\n",
10
+ "Per-split percent benefit of adding a PCM solvent correction, for each method, in chloroform and benzene."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "id": "80ceb0d1",
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "\n",
22
+ "# make the in-repo modules importable (not pip-installed)\n",
23
+ "REPO = os.path.abspath(\"../..\")\n",
24
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
25
+ " sys.path.insert(0, os.path.join(REPO, _p))"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": null,
31
+ "id": "88b3d113",
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "import delta22\n",
36
+ "import paths\n",
37
+ "import fig3_plots"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "id": "e830f642",
44
+ "metadata": {},
45
+ "outputs": [],
46
+ "source": [
47
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
48
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
49
+ "\n",
50
+ "def figure_path(name):\n",
51
+ " os.makedirs(\"figures\", exist_ok=True)\n",
52
+ " return os.path.join(\"figures\", name)"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "execution_count": null,
58
+ "id": "df43c551",
59
+ "metadata": {},
60
+ "outputs": [],
61
+ "source": [
62
+ "# the published panels use 250 seeded train/test splits\n",
63
+ "N_SPLITS = 250"
64
+ ]
65
+ },
66
+ {
67
+ "cell_type": "code",
68
+ "execution_count": null,
69
+ "id": "19a780b7",
70
+ "metadata": {},
71
+ "outputs": [],
72
+ "source": [
73
+ "query = delta22.add_composite_columns(delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False))\n",
74
+ "solutes = sorted(query[\"solute\"].unique())\n",
75
+ "print(len(query), \"rows;\", len(solutes), \"solutes;\", query[\"sap_nmr_method\"].nunique(), \"methods\")"
76
+ ]
77
+ },
78
+ {
79
+ "cell_type": "code",
80
+ "execution_count": null,
81
+ "id": "6c68c523",
82
+ "metadata": {},
83
+ "outputs": [],
84
+ "source": [
85
+ "fig3a_by_solvent = delta22.fig3a_pcm_benefit_by_solvent(query, [\"chloroform\", \"benzene\"],\n",
86
+ " n_splits=N_SPLITS, solutes=solutes)\n",
87
+ "\n",
88
+ "# per-method median percent benefit, then mean +/- std across methods\n",
89
+ "chloro = fig3a_by_solvent[fig3a_by_solvent[\"solvent\"] == \"chloroform\"]\n",
90
+ "benz = fig3a_by_solvent[fig3a_by_solvent[\"solvent\"] == \"benzene\"]\n",
91
+ "chloro_by_method = chloro.groupby(\"sap_nmr_method\")[\"percent_benefit\"].median()\n",
92
+ "benz_by_method = benz.groupby(\"sap_nmr_method\")[\"percent_benefit\"].median()\n",
93
+ "print(f\"PCM benefit in chloroform: {chloro_by_method.mean():+.1f} +/- {chloro_by_method.std():.1f}%\"\n",
94
+ " f\" (positive = PCM helps)\")\n",
95
+ "print(f\"PCM benefit in benzene: {benz_by_method.mean():+.1f} +/- {benz_by_method.std():.1f}%\"\n",
96
+ " f\" (negative = PCM hurts)\")"
97
+ ]
98
+ },
99
+ {
100
+ "cell_type": "code",
101
+ "execution_count": null,
102
+ "id": "f282780f",
103
+ "metadata": {},
104
+ "outputs": [],
105
+ "source": [
106
+ "# Fixed method order, grouped by family (ab initio -> conventional DFT -> NMR-specific -> double\n",
107
+ "# hybrid), each as (method, basis, geometry). This ordering is what gives the panel its family\n",
108
+ "# structure; sorting by benefit value would scramble it.\n",
109
+ "FIG3A_COMBOS = [\n",
110
+ " (\"hf\", \"pcSseg3\", \"pbe0_tz\"), (\"mp2\", \"pcSseg2\", \"pbe0_tz\"), (\"dlpno_mp2\", \"pcSseg3\", \"pbe0_tz\"),\n",
111
+ " (\"b3lyp_d3bj\", \"pcSseg3\", \"pbe0_tz\"), (\"pbe0_d3bj\", \"pcSseg2\", \"pbe0_tz\"),\n",
112
+ " (\"m062x_d3\", \"pcSseg3\", \"pbe0_tz\"), (\"b97d3_d3bj\", \"pcSseg3\", \"pbe0_tz\"),\n",
113
+ " (\"wb97xd\", \"pcSseg3\", \"pbe0_tz\"), (\"tpsstpss_d3bj\", \"pcSseg3\", \"pbe0_tz\"),\n",
114
+ " (\"bp86_d3bj\", \"pcSseg3\", \"pbe0_tz\"), (\"blyp_d3bj\", \"pcSseg3\", \"pbe0_tz\"),\n",
115
+ " (\"wp04\", \"pcSseg3\", \"pbe0_tz\"), (\"wc04\", \"pcSseg3\", \"pbe0_tz\"),\n",
116
+ " (\"dsd_pbep86\", \"pcSseg3\", \"pbe0_tz\"), (\"B2GP_PLYP\", \"pcSseg3\", \"pbe0_tz\"),\n",
117
+ " (\"B2PLYP\", \"pcSseg3\", \"pbe0_tz\"), (\"mPW2PLYP\", \"pcSseg3\", \"pbe0_tz\"),\n",
118
+ " (\"revdsd_pbep86\", \"pcSseg3\", \"pbe0_tz\"),\n",
119
+ "]\n",
120
+ "FIG3A_COLORS = {\"chloroform\": \"#44AA99\", \"benzene\": \"#DDCC77\"} # teal / tan, as published"
121
+ ]
122
+ },
123
+ {
124
+ "cell_type": "code",
125
+ "execution_count": null,
126
+ "id": "2fa6355b",
127
+ "metadata": {},
128
+ "outputs": [],
129
+ "source": [
130
+ "fig3_plots.plot_pcm_benefit_with_arrows(fig3a_by_solvent, FIG3A_COMBOS, FIG3A_COLORS,\n",
131
+ " save_path=figure_path(\"fig3a_pcm_benefit_1H.png\"))"
132
+ ]
133
+ }
134
+ ],
135
+ "metadata": {
136
+ "language_info": {
137
+ "codemirror_mode": {
138
+ "name": "ipython",
139
+ "version": 3
140
+ },
141
+ "file_extension": ".py",
142
+ "mimetype": "text/x-python",
143
+ "name": "python",
144
+ "nbconvert_exporter": "python",
145
+ "pygments_lexer": "ipython3",
146
+ "version": "3.12.13"
147
+ }
148
+ },
149
+ "nbformat": 4,
150
+ "nbformat_minor": 5
151
+ }
analysis/manuscript_figures/fig3b_shifts.ipynb ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "182eaf82",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 3B: solvent-induced shift differences (experiment vs PCM)\n",
9
+ "\n",
10
+ "Measured vs PCM-predicted differences between solvent-induced ¹H shifts (methanol/benzene vs chloroform reference)."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "id": "89397a6b",
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "\n",
22
+ "# make the in-repo modules importable (not pip-installed)\n",
23
+ "REPO = os.path.abspath(\"../..\")\n",
24
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
25
+ " sys.path.insert(0, os.path.join(REPO, _p))"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": null,
31
+ "id": "cc6077cf",
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "import delta22\n",
36
+ "import paths\n",
37
+ "import fig3_plots"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "id": "a093d2ad",
44
+ "metadata": {},
45
+ "outputs": [],
46
+ "source": [
47
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
48
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
49
+ "\n",
50
+ "def figure_path(name):\n",
51
+ " os.makedirs(\"figures\", exist_ok=True)\n",
52
+ " return os.path.join(\"figures\", name)"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "execution_count": null,
58
+ "id": "0ec2ee33",
59
+ "metadata": {},
60
+ "outputs": [],
61
+ "source": [
62
+ "query = delta22.add_composite_columns(delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False))\n",
63
+ "print(len(query), \"rows;\", query[\"sap_nmr_method\"].nunique(), \"methods\")"
64
+ ]
65
+ },
66
+ {
67
+ "cell_type": "code",
68
+ "execution_count": null,
69
+ "id": "141afd11",
70
+ "metadata": {},
71
+ "outputs": [],
72
+ "source": [
73
+ "shift = delta22.fig3b_shift_differences(query, \"wp04\", \"pcSseg2\", \"aimnet2\", nucleus=\"H\",\n",
74
+ " x_solvent=\"methanol\", y_solvent=\"benzene\", reference=\"chloroform\")\n",
75
+ "print(len(shift), \"sites\")"
76
+ ]
77
+ },
78
+ {
79
+ "cell_type": "code",
80
+ "execution_count": null,
81
+ "id": "7edca767",
82
+ "metadata": {},
83
+ "outputs": [],
84
+ "source": [
85
+ "fig3_plots.plot_shift_vs_pcm(shift, save_path=figure_path(\"fig3b_shifts_1H.png\"))"
86
+ ]
87
+ }
88
+ ],
89
+ "metadata": {
90
+ "language_info": {
91
+ "codemirror_mode": {
92
+ "name": "ipython",
93
+ "version": 3
94
+ },
95
+ "file_extension": ".py",
96
+ "mimetype": "text/x-python",
97
+ "name": "python",
98
+ "nbconvert_exporter": "python",
99
+ "pygments_lexer": "ipython3",
100
+ "version": "3.12.13"
101
+ }
102
+ },
103
+ "nbformat": 4,
104
+ "nbformat_minor": 5
105
+ }
analysis/manuscript_figures/fig3d_solvent_corrections.ipynb ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "0954a86e",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 3D: implicit vs explicit solvent corrections, one box per solvent\n",
9
+ "\n",
10
+ "All 12 solvents individually (polar aprotic -> polar protic -> aromatic), four schemes each: implicit (PCM), implicit + vibrations, explicit, explicit + vibrations."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "id": "5172978e",
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "\n",
22
+ "# make the in-repo modules importable (not pip-installed)\n",
23
+ "REPO = os.path.abspath(\"../..\")\n",
24
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
25
+ " sys.path.insert(0, os.path.join(REPO, _p))"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": null,
31
+ "id": "c7e70d68",
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "import delta22\n",
36
+ "import paths\n",
37
+ "import fig3_plots"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "id": "55397f4b",
44
+ "metadata": {},
45
+ "outputs": [],
46
+ "source": [
47
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
48
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
49
+ "\n",
50
+ "def figure_path(name):\n",
51
+ " os.makedirs(\"figures\", exist_ok=True)\n",
52
+ " return os.path.join(\"figures\", name)"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "execution_count": null,
58
+ "id": "06416ecb",
59
+ "metadata": {},
60
+ "outputs": [],
61
+ "source": [
62
+ "# the published panels use 250 seeded train/test splits\n",
63
+ "N_SPLITS = 250"
64
+ ]
65
+ },
66
+ {
67
+ "cell_type": "code",
68
+ "execution_count": null,
69
+ "id": "e892c346",
70
+ "metadata": {},
71
+ "outputs": [],
72
+ "source": [
73
+ "query = delta22.add_composite_columns(delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False))\n",
74
+ "solutes = sorted(query[\"solute\"].unique())\n",
75
+ "print(len(query), \"rows;\", len(solutes), \"solutes;\", query[\"sap_nmr_method\"].nunique(), \"methods\")"
76
+ ]
77
+ },
78
+ {
79
+ "cell_type": "code",
80
+ "execution_count": null,
81
+ "id": "32748b6d",
82
+ "metadata": {},
83
+ "outputs": [],
84
+ "source": [
85
+ "FIG3D_LABELS = {\n",
86
+ " \"stationary + pcm\": \"Implicit Solvent (PCM)\",\n",
87
+ " \"stationary_plus_qcd + pcm\": \"Implicit Solvent (PCM) + Vibrations\",\n",
88
+ " \"stationary + desmond\": \"Explicit Solvent\",\n",
89
+ " \"stationary_plus_qcd + desmond\": \"Explicit Solvent + Vibrations\",\n",
90
+ "}\n",
91
+ "fig3d_results = delta22.fig3d_formula_regressions(\n",
92
+ " query, \"dsd_pbep86\", \"pcSseg3\", \"pbe0_tz\", list(FIG3D_LABELS),\n",
93
+ " delta22.DESMOND_SOLVENTS, n_splits=N_SPLITS, solutes=solutes)\n",
94
+ "\n",
95
+ "# report implicit vs explicit median test RMSE per solvent (not pooled by class)\n",
96
+ "for solvent in delta22.DESMOND_SOLVENTS:\n",
97
+ " sub = fig3d_results[fig3d_results[\"solvent\"] == solvent]\n",
98
+ " mi = sub[sub[\"formula\"] == \"stationary + pcm\"][\"test_RMSE\"].median()\n",
99
+ " me = sub[sub[\"formula\"] == \"stationary + desmond\"][\"test_RMSE\"].median()\n",
100
+ " print(f\"{solvent:20s} implicit={mi:.4f} explicit={me:.4f}\")"
101
+ ]
102
+ },
103
+ {
104
+ "cell_type": "code",
105
+ "execution_count": null,
106
+ "id": "6e826d46",
107
+ "metadata": {},
108
+ "outputs": [],
109
+ "source": [
110
+ "FIG3D_COLORS = [\"#A72608\", \"#F4BAAD\", \"#5D737E\", \"#D9FFF5\"] # dark red, pink, gray, mint (as published)\n",
111
+ "FIG3D_SOLVENT_LABELS = {\n",
112
+ " \"chloroform\": r\"CDCl$_3$\", \"dichloromethane\": \"DCM\", \"tetrahydrofuran\": \"THF\",\n",
113
+ " \"acetonitrile\": \"MeCN\", \"dimethylsulfoxide\": \"DMSO\", \"methanol\": \"MeOD\",\n",
114
+ " \"trifluoroethanol\": \"TFE\", \"chlorobenzene\": \"PhCl\",\n",
115
+ "}\n",
116
+ "# solvent order: polar aprotic -> polar protic -> aromatic (group ordering from delta22.SOLVENT_GROUPS)\n",
117
+ "FIG3D_SOLVENT_ORDER = [s for group in delta22.SOLVENT_GROUPS.values() for s in group]"
118
+ ]
119
+ },
120
+ {
121
+ "cell_type": "code",
122
+ "execution_count": null,
123
+ "id": "c0c66631",
124
+ "metadata": {},
125
+ "outputs": [],
126
+ "source": [
127
+ "fig3_plots.plot_solvent_correction_boxplot(fig3d_results, FIG3D_LABELS, FIG3D_SOLVENT_ORDER,\n",
128
+ " FIG3D_COLORS, FIG3D_SOLVENT_LABELS,\n",
129
+ " save_path=figure_path(\"fig3d_explicit_solvation_1H.png\"))"
130
+ ]
131
+ }
132
+ ],
133
+ "metadata": {
134
+ "language_info": {
135
+ "codemirror_mode": {
136
+ "name": "ipython",
137
+ "version": 3
138
+ },
139
+ "file_extension": ".py",
140
+ "mimetype": "text/x-python",
141
+ "name": "python",
142
+ "nbconvert_exporter": "python",
143
+ "pygments_lexer": "ipython3",
144
+ "version": "3.12.13"
145
+ }
146
+ },
147
+ "nbformat": 4,
148
+ "nbformat_minor": 5
149
+ }
analysis/manuscript_figures/fig4a_implicit.ipynb ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "a1be4f68",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 4A: predicted vs measured solvent-induced shift, implicit (PCM)\n",
9
+ "\n",
10
+ "Implicit (PCM) predicted vs measured solvent-induced shifts (Δδ vs. CDCl3, ¹H) for one solvent per class."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "id": "27283d57",
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "\n",
22
+ "# make the in-repo modules importable (not pip-installed)\n",
23
+ "REPO = os.path.abspath(\"../..\")\n",
24
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
25
+ " sys.path.insert(0, os.path.join(REPO, _p))"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": null,
31
+ "id": "576ad573",
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "import delta22\n",
36
+ "import paths\n",
37
+ "import fig4_plots"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "id": "fff8d490",
44
+ "metadata": {},
45
+ "outputs": [],
46
+ "source": [
47
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
48
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
49
+ "\n",
50
+ "def figure_path(name):\n",
51
+ " os.makedirs(\"figures\", exist_ok=True)\n",
52
+ " return os.path.join(\"figures\", name)"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "execution_count": null,
58
+ "id": "236a76a8",
59
+ "metadata": {},
60
+ "outputs": [],
61
+ "source": [
62
+ "METHOD, BASIS, GEOM = \"b3lyp_d3bj\", \"pcSseg2\", \"aimnet2\"\n",
63
+ "q = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)\n",
64
+ "one = q[(q[\"sap_nmr_method\"] == METHOD) & (q[\"sap_basis\"] == BASIS) & (q[\"sap_geometry_type\"] == GEOM)]\n",
65
+ "one = delta22.add_solvent_mean(one)\n",
66
+ "print(len(one), \"rows for\", METHOD, BASIS, GEOM)"
67
+ ]
68
+ },
69
+ {
70
+ "cell_type": "code",
71
+ "execution_count": null,
72
+ "id": "f28bd2a4",
73
+ "metadata": {},
74
+ "outputs": [],
75
+ "source": [
76
+ "shifts_chcl3 = delta22.solvent_induced_shifts(one, \"chloroform\", delta22.DESMOND_SOLVENTS,\n",
77
+ " nucleus=\"H\", explicit=\"desmond\")\n",
78
+ "print(len(shifts_chcl3), \"site/solvent differences\")"
79
+ ]
80
+ },
81
+ {
82
+ "cell_type": "code",
83
+ "execution_count": null,
84
+ "id": "6f3c02f9",
85
+ "metadata": {},
86
+ "outputs": [],
87
+ "source": [
88
+ "# Water == TIP4P\n",
89
+ "FIG4_HIGHLIGHT = {\"dimethylsulfoxide\": \"DMSO\", \"TIP4P\": \"Water\", \"benzene\": \"Benzene\"}\n",
90
+ "FIG4_ORDER = [\"DMSO\", \"Water\", \"Benzene\"]\n",
91
+ "FIG4_PALETTE = {\"DMSO\": \"#61a89a\", \"Water\": \"#A72608\", \"Benzene\": \"#dacd82\"}\n",
92
+ "FIG4_MARKERS = {\"DMSO\": \"o\", \"Water\": \"^\", \"Benzene\": \"X\"}"
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "code",
97
+ "execution_count": null,
98
+ "id": "59bb0a61",
99
+ "metadata": {},
100
+ "outputs": [],
101
+ "source": [
102
+ "fig4_plots.save_panel(\n",
103
+ " lambda ax: fig4_plots.draw_shift_scatter(ax, shifts_chcl3, \"implicit_diff\",\n",
104
+ " FIG4_HIGHLIGHT, FIG4_ORDER, FIG4_PALETTE, FIG4_MARKERS),\n",
105
+ " figure_path(\"fig4a_implicit_1H.png\"),\n",
106
+ ")"
107
+ ]
108
+ }
109
+ ],
110
+ "metadata": {},
111
+ "nbformat": 4,
112
+ "nbformat_minor": 5
113
+ }
analysis/manuscript_figures/fig4b_explicit.ipynb ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "915187f5",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 4B: predicted vs measured solvent-induced shift, explicit (Desmond)\n",
9
+ "\n",
10
+ "Explicit (Desmond) predicted vs measured solvent-induced shifts (Δδ vs. CDCl3, ¹H) for one solvent per class."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "id": "3d55e803",
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "\n",
22
+ "# make the in-repo modules importable (not pip-installed)\n",
23
+ "REPO = os.path.abspath(\"../..\")\n",
24
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
25
+ " sys.path.insert(0, os.path.join(REPO, _p))"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": null,
31
+ "id": "18a69a1a",
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "import delta22\n",
36
+ "import paths\n",
37
+ "import fig4_plots"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "id": "24c48a7e",
44
+ "metadata": {},
45
+ "outputs": [],
46
+ "source": [
47
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
48
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
49
+ "\n",
50
+ "def figure_path(name):\n",
51
+ " os.makedirs(\"figures\", exist_ok=True)\n",
52
+ " return os.path.join(\"figures\", name)"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "execution_count": null,
58
+ "id": "a56abe4a",
59
+ "metadata": {},
60
+ "outputs": [],
61
+ "source": [
62
+ "METHOD, BASIS, GEOM = \"b3lyp_d3bj\", \"pcSseg2\", \"aimnet2\"\n",
63
+ "q = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)\n",
64
+ "one = q[(q[\"sap_nmr_method\"] == METHOD) & (q[\"sap_basis\"] == BASIS) & (q[\"sap_geometry_type\"] == GEOM)]\n",
65
+ "one = delta22.add_solvent_mean(one)\n",
66
+ "print(len(one), \"rows for\", METHOD, BASIS, GEOM)"
67
+ ]
68
+ },
69
+ {
70
+ "cell_type": "code",
71
+ "execution_count": null,
72
+ "id": "d3b2dc95",
73
+ "metadata": {},
74
+ "outputs": [],
75
+ "source": [
76
+ "shifts_chcl3 = delta22.solvent_induced_shifts(one, \"chloroform\", delta22.DESMOND_SOLVENTS,\n",
77
+ " nucleus=\"H\", explicit=\"desmond\")\n",
78
+ "print(len(shifts_chcl3), \"site/solvent differences\")"
79
+ ]
80
+ },
81
+ {
82
+ "cell_type": "code",
83
+ "execution_count": null,
84
+ "id": "bd10f351",
85
+ "metadata": {},
86
+ "outputs": [],
87
+ "source": [
88
+ "# Water == TIP4P\n",
89
+ "FIG4_HIGHLIGHT = {\"dimethylsulfoxide\": \"DMSO\", \"TIP4P\": \"Water\", \"benzene\": \"Benzene\"}\n",
90
+ "FIG4_ORDER = [\"DMSO\", \"Water\", \"Benzene\"]\n",
91
+ "FIG4_PALETTE = {\"DMSO\": \"#61a89a\", \"Water\": \"#A72608\", \"Benzene\": \"#dacd82\"}\n",
92
+ "FIG4_MARKERS = {\"DMSO\": \"o\", \"Water\": \"^\", \"Benzene\": \"X\"}"
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "code",
97
+ "execution_count": null,
98
+ "id": "9a7b4432",
99
+ "metadata": {},
100
+ "outputs": [],
101
+ "source": [
102
+ "fig4_plots.save_panel(\n",
103
+ " lambda ax: fig4_plots.draw_shift_scatter(ax, shifts_chcl3, \"explicit_diff\",\n",
104
+ " FIG4_HIGHLIGHT, FIG4_ORDER, FIG4_PALETTE, FIG4_MARKERS),\n",
105
+ " figure_path(\"fig4b_explicit_1H.png\"),\n",
106
+ ")"
107
+ ]
108
+ }
109
+ ],
110
+ "metadata": {},
111
+ "nbformat": 4,
112
+ "nbformat_minor": 5
113
+ }
analysis/manuscript_figures/fig4c_rmse_range.ipynb ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "6d0c4a9d",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 4C: fit RMSE vs experimental range, implicit vs explicit\n",
9
+ "\n",
10
+ "Per-solvent fit RMSE vs experimental shift range (¹H), implicit (PCM) vs explicit (Desmond)."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "id": "8a3ec572",
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "\n",
22
+ "# make the in-repo modules importable (not pip-installed)\n",
23
+ "REPO = os.path.abspath(\"../..\")\n",
24
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
25
+ " sys.path.insert(0, os.path.join(REPO, _p))"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": null,
31
+ "id": "79b3bd64",
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "import delta22\n",
36
+ "import paths\n",
37
+ "import fig4_plots"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "id": "79171fdb",
44
+ "metadata": {},
45
+ "outputs": [],
46
+ "source": [
47
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
48
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
49
+ "\n",
50
+ "def figure_path(name):\n",
51
+ " os.makedirs(\"figures\", exist_ok=True)\n",
52
+ " return os.path.join(\"figures\", name)"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "execution_count": null,
58
+ "id": "fdaaf850",
59
+ "metadata": {},
60
+ "outputs": [],
61
+ "source": [
62
+ "# solvent_mean is a synthetic pseudo-solvent representing the average across solvents; it is used below as the reference baseline.\n",
63
+ "METHOD, BASIS, GEOM = \"b3lyp_d3bj\", \"pcSseg2\", \"aimnet2\"\n",
64
+ "q = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)\n",
65
+ "one = q[(q[\"sap_nmr_method\"] == METHOD) & (q[\"sap_basis\"] == BASIS) & (q[\"sap_geometry_type\"] == GEOM)]\n",
66
+ "one = delta22.add_solvent_mean(one)\n",
67
+ "print(len(one), \"rows for\", METHOD, BASIS, GEOM)"
68
+ ]
69
+ },
70
+ {
71
+ "cell_type": "code",
72
+ "execution_count": null,
73
+ "id": "e5527018",
74
+ "metadata": {},
75
+ "outputs": [],
76
+ "source": [
77
+ "FIG4C_LABELS = {\n",
78
+ " \"chloroform\": \"CDCl3\", \"dichloromethane\": \"DCM\", \"tetrahydrofuran\": \"THF\",\n",
79
+ " \"acetonitrile\": \"MeCN\", \"dimethylsulfoxide\": \"DMSO\", \"acetone\": \"acetone\",\n",
80
+ " \"methanol\": \"MeOD\", \"TIP4P\": \"TIP4P\", \"trifluoroethanol\": \"trifluoroethanol\",\n",
81
+ " \"benzene\": \"benzene\", \"toluene\": \"toluene\", \"chlorobenzene\": \"chlorobenzene\",\n",
82
+ "}\n",
83
+ "FIG4C_COLORS = {\"Implicit (PCM)\": \"#A72608\", \"Explicit (Desmond)\": \"#61a89a\"} # red diamond / teal circle"
84
+ ]
85
+ },
86
+ {
87
+ "cell_type": "code",
88
+ "execution_count": null,
89
+ "id": "3cdab0a7",
90
+ "metadata": {},
91
+ "outputs": [],
92
+ "source": [
93
+ "# differences are taken against the solvent-averaged pseudo-solvent so every real solvent, including chloroform, appears as a point.\n",
94
+ "rmse_range_rows = []\n",
95
+ "for solvent in delta22.DESMOND_SOLVENTS:\n",
96
+ " sp = delta22.solvent_pair_differences(one, solvent, \"solvent_mean\", nucleus=\"H\", explicit=\"desmond\")\n",
97
+ " if len(sp) == 0:\n",
98
+ " continue\n",
99
+ " rmse_range_rows.append({\n",
100
+ " \"solvent\": solvent,\n",
101
+ " \"range\": float(sp[\"exp_diff\"].max() - sp[\"exp_diff\"].min()),\n",
102
+ " \"implicit\": delta22.fit_differences_to_experimental(sp, \"implicit_diff\")[\"rmse\"],\n",
103
+ " \"explicit\": delta22.fit_differences_to_experimental(sp, \"explicit_diff\")[\"rmse\"],\n",
104
+ " })\n",
105
+ "for r in sorted(rmse_range_rows, key=lambda r: r[\"range\"]):\n",
106
+ " print(f\"{FIG4C_LABELS[r['solvent']]:16s} range={r['range']:.3f} \"\n",
107
+ " f\"implicit={r['implicit']:.3f} explicit={r['explicit']:.3f}\")"
108
+ ]
109
+ },
110
+ {
111
+ "cell_type": "code",
112
+ "execution_count": null,
113
+ "id": "4601c2c7",
114
+ "metadata": {},
115
+ "outputs": [],
116
+ "source": [
117
+ "fig4_plots.save_panel(\n",
118
+ " lambda ax: fig4_plots.draw_rmse_dumbbell(ax, rmse_range_rows, FIG4C_LABELS, FIG4C_COLORS),\n",
119
+ " figure_path(\"fig4c_rmse_range_1H.png\"),\n",
120
+ ")"
121
+ ]
122
+ }
123
+ ],
124
+ "metadata": {},
125
+ "nbformat": 4,
126
+ "nbformat_minor": 5
127
+ }
analysis/manuscript_figures/fig5b_dft8k.ipynb ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "706410fd",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 5B: MagNET-Zero vs DFT shieldings on the DFT8K benchmark\n",
9
+ "\n",
10
+ "Reproduces Figure 5B: MagNET-Zero gas-phase shielding predictions vs DFT on the DFT8K benchmark (~7,000\n",
11
+ "organics, untrained). Protons vs WP04/pcSseg-2, carbons vs wB97X-D/pcSseg-2; residual = DFT minus\n",
12
+ "MagNET."
13
+ ]
14
+ },
15
+ {
16
+ "cell_type": "code",
17
+ "execution_count": null,
18
+ "id": "7e0a3b95",
19
+ "metadata": {},
20
+ "outputs": [],
21
+ "source": [
22
+ "import os, sys\n",
23
+ "\n",
24
+ "# make the in-repo modules importable (not pip-installed)\n",
25
+ "REPO = os.path.abspath(\"../..\")\n",
26
+ "for _p in (\"data/dft8k\", \"analysis/code\", \"analysis/code/shared\"):\n",
27
+ " sys.path.insert(0, os.path.join(REPO, _p))"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "code",
32
+ "execution_count": null,
33
+ "id": "c9838352",
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": [
37
+ "import dft8k_residuals\n",
38
+ "import paths\n",
39
+ "import fig5b_plots"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "code",
44
+ "execution_count": null,
45
+ "id": "d2231e1e",
46
+ "metadata": {},
47
+ "outputs": [],
48
+ "source": [
49
+ "DFT8K_HDF5 = paths.dataset_file(\"dft8k\", root=REPO)\n",
50
+ "\n",
51
+ "def figure_path(name):\n",
52
+ " os.makedirs(\"figures\", exist_ok=True)\n",
53
+ " return os.path.join(\"figures\", name)"
54
+ ]
55
+ },
56
+ {
57
+ "cell_type": "code",
58
+ "execution_count": null,
59
+ "id": "b7d02a23",
60
+ "metadata": {},
61
+ "outputs": [],
62
+ "source": [
63
+ "shieldings = dft8k_residuals.load_shieldings(DFT8K_HDF5)\n",
64
+ "print(\"atoms in DFT8K:\", len(shieldings[\"atomic_numbers\"]))"
65
+ ]
66
+ },
67
+ {
68
+ "cell_type": "markdown",
69
+ "id": "867ed2b4",
70
+ "metadata": {},
71
+ "source": [
72
+ "## Residual statistics"
73
+ ]
74
+ },
75
+ {
76
+ "cell_type": "code",
77
+ "execution_count": null,
78
+ "id": "02ca1b09",
79
+ "metadata": {},
80
+ "outputs": [],
81
+ "source": [
82
+ "for nucleus in [\"H\", \"C\"]:\n",
83
+ " errors = dft8k_residuals.residuals(shieldings, nucleus)\n",
84
+ " stats = dft8k_residuals.residual_stats(errors)\n",
85
+ " print(f\"{nucleus}: n={stats['n']:,} RMSE={stats['rmse']:.4f} ppm MAE={stats['mae']:.4f} ppm \"\n",
86
+ " f\"95% abs={stats['abs_p95']:.4f} ppm fraction < 0.1 ppm={stats['frac_below']:.3f}\")"
87
+ ]
88
+ },
89
+ {
90
+ "cell_type": "markdown",
91
+ "id": "02d95406",
92
+ "metadata": {},
93
+ "source": [
94
+ "## Residual histograms"
95
+ ]
96
+ },
97
+ {
98
+ "cell_type": "code",
99
+ "execution_count": null,
100
+ "id": "bf9148b3",
101
+ "metadata": {},
102
+ "outputs": [],
103
+ "source": [
104
+ "for nucleus in [\"H\", \"C\"]:\n",
105
+ " errors = dft8k_residuals.residuals(shieldings, nucleus)\n",
106
+ " stats = dft8k_residuals.residual_stats(errors)\n",
107
+ " print(f\"{nucleus}: RMSE={stats['rmse']:.3f} ppm frac within 0.1 ppm={stats['frac_below']:.3f}\")\n",
108
+ " if nucleus == \"H\":\n",
109
+ " fig5b_plots.plot_dft8k_residual_histogram(errors, figure_path(\"fig5b_residuals_1H.png\"),\n",
110
+ " bin_width=0.0025, xlim=0.23, grey_box=0.1, x_tick=0.05)\n",
111
+ " else: # 13C: wider window, no +/-0.1 box since that threshold is 1H-specific\n",
112
+ " fig5b_plots.plot_dft8k_residual_histogram(errors, None,\n",
113
+ " bin_width=0.05, xlim=5.0, grey_box=None, x_tick=1.0)"
114
+ ]
115
+ },
116
+ {
117
+ "cell_type": "markdown",
118
+ "id": "5eab006a",
119
+ "metadata": {},
120
+ "source": [
121
+ "## Extreme-residual callouts\n",
122
+ "\n",
123
+ "The largest positive and negative ¹H residuals over the full set. The published negative callout (a\n",
124
+ "sulfonium zwitterion, -1.040 ppm) is not the true global minimum -- see the printed note."
125
+ ]
126
+ },
127
+ {
128
+ "cell_type": "code",
129
+ "execution_count": null,
130
+ "id": "b1f2315b",
131
+ "metadata": {},
132
+ "outputs": [],
133
+ "source": [
134
+ "extreme_max = dft8k_residuals.find_extreme_residual(DFT8K_HDF5, \"H\", sign=\"max\")\n",
135
+ "print(\"largest positive 1H residual:\", extreme_max)\n",
136
+ "fig5b_plots.show_dft8k_molecule(extreme_max[\"smiles\"])\n",
137
+ "\n",
138
+ "zwitterion = dft8k_residuals.molecule_by_id(DFT8K_HDF5, \"H\", molecule_id=88779)\n",
139
+ "print(\"published sulfonium-zwitterion callout (id 88779):\", zwitterion)\n",
140
+ "fig5b_plots.show_dft8k_molecule(zwitterion[\"smiles\"])"
141
+ ]
142
+ },
143
+ {
144
+ "cell_type": "markdown",
145
+ "id": "5f4276e7",
146
+ "metadata": {},
147
+ "source": [
148
+ "## Functional-group error breakdown\n",
149
+ "\n",
150
+ "Mean absolute residual for six functional groups (Carbonyls, Amines, Sulfonyl, Pyridines, Furans,\n",
151
+ "Nitroso), via RDKit SMARTS matching over every molecule's SMILES\n",
152
+ "(`dft8k_residuals.FUNCTIONAL_GROUP_SMARTS`)."
153
+ ]
154
+ },
155
+ {
156
+ "cell_type": "code",
157
+ "execution_count": null,
158
+ "id": "f95f9d58",
159
+ "metadata": {},
160
+ "outputs": [],
161
+ "source": [
162
+ "for nucleus in [\"H\", \"C\"]:\n",
163
+ " group_errors = dft8k_residuals.functional_group_errors(DFT8K_HDF5, nucleus)\n",
164
+ " print(f\"--- {nucleus} ---\")\n",
165
+ " for name, v in group_errors.items():\n",
166
+ " print(f\"{name:12s} mean|error|={v['mean_abs_error']:.4f} ppm n_molecules={v['n_molecules']:,}\")\n",
167
+ " save = figure_path(\"fig5b_functional_groups_1H.png\") if nucleus == \"H\" else None\n",
168
+ " fig5b_plots.plot_dft8k_functional_group_errors(group_errors, nucleus, save)"
169
+ ]
170
+ }
171
+ ],
172
+ "metadata": {
173
+ "language_info": {
174
+ "codemirror_mode": {
175
+ "name": "ipython",
176
+ "version": 3
177
+ },
178
+ "file_extension": ".py",
179
+ "mimetype": "text/x-python",
180
+ "name": "python",
181
+ "nbconvert_exporter": "python",
182
+ "pygments_lexer": "ipython3",
183
+ "version": "3.12.13"
184
+ }
185
+ },
186
+ "nbformat": 4,
187
+ "nbformat_minor": 5
188
+ }
analysis/manuscript_figures/fig5c.ipynb ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "9f479dac",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 5C: implicit vs explicit correction RMSE, delta-22 vs the natural-products test set\n",
9
+ "\n",
10
+ "Bootstrap RMSE of implicit (PCM) vs explicit (OpenMM + vibrations) corrections, delta-22 vs the pooled complex/natural-product test set, chloroform and benzene (bars = mean, error bars = 2.5-97.5 percentile). Produces the ¹H panel (saved) and a ¹³C companion (inline only)."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "id": "c4fd38cc",
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "\n",
22
+ "# make the in-repo modules importable (not pip-installed)\n",
23
+ "REPO = os.path.abspath(\"../..\")\n",
24
+ "for _p in (\"data/applications\", \"analysis/code\", \"analysis/code/shared\"):\n",
25
+ " sys.path.insert(0, os.path.join(REPO, _p))"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": null,
31
+ "id": "aef1da3f",
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "from applications_reader import Applications\n",
36
+ "import applications\n",
37
+ "import applications_plots\n",
38
+ "import paths"
39
+ ]
40
+ },
41
+ {
42
+ "cell_type": "code",
43
+ "execution_count": null,
44
+ "id": "178e31b6",
45
+ "metadata": {},
46
+ "outputs": [],
47
+ "source": [
48
+ "APPLICATIONS_HDF5 = paths.dataset_file(\"applications\", root=REPO)\n",
49
+ "XLSX = os.path.join(REPO, \"data\", \"applications\", \"applications_experimental.xlsx\")\n",
50
+ "\n",
51
+ "def figure_path(name):\n",
52
+ " os.makedirs(\"figures\", exist_ok=True)\n",
53
+ " return os.path.join(\"figures\", name)"
54
+ ]
55
+ },
56
+ {
57
+ "cell_type": "code",
58
+ "execution_count": null,
59
+ "id": "a013c941",
60
+ "metadata": {},
61
+ "outputs": [],
62
+ "source": [
63
+ "loader = Applications(APPLICATIONS_HDF5, XLSX)\n",
64
+ "query_df_nn = applications.build_query_df_nn(loader)\n",
65
+ "seed = applications.build_bootstrap_seed_coeffs(loader)\n",
66
+ "\n",
67
+ "# pooled \"Test Set\" bootstrap RMSE distributions (one bin over all 13 complex molecules)\n",
68
+ "grouped = {}\n",
69
+ "for nuc in [\"H\", \"C\"]:\n",
70
+ " preds = applications.apply_bootstrap_params_to_full_dataset(query_df_nn, seed[nuc], nucleus=nuc)\n",
71
+ " grouped[nuc] = applications.compute_grouped_rmse(preds, applications.ALL_IN_ONE_BIN)"
72
+ ]
73
+ },
74
+ {
75
+ "cell_type": "code",
76
+ "execution_count": null,
77
+ "id": "3d3cf53a",
78
+ "metadata": {},
79
+ "outputs": [],
80
+ "source": [
81
+ "applications_plots.plot_nps_benefit_barplot(\n",
82
+ " loader.rmse_distribution(\"H\"), grouped[\"H\"], nucleus=\"H\",\n",
83
+ " solvents=[[\"chloroform\"], [\"benzene\"]], np_solute_groups=applications.ALL_IN_ONE_BIN,\n",
84
+ " formulas=[\"stationary_plus_pcm\", \"stationary_plus_qcd + openMM\"],\n",
85
+ " labels=[\"Implicit Solvent (SotA)\", \"Explicit Solvent + Vibrations (OpenMM)\"],\n",
86
+ " colors=[\"#A72608\", \"#61a89a\"], formula_remap=applications.FORMULA_REMAP,\n",
87
+ " figsize=(6, 5), y_min=0.0, y_max=0.37, save_path=figure_path(\"fig5c_benefit_1H.png\"))\n",
88
+ "\n",
89
+ "applications_plots.plot_nps_benefit_barplot(\n",
90
+ " loader.rmse_distribution(\"C\"), grouped[\"C\"], nucleus=\"C\",\n",
91
+ " solvents=[[\"chloroform\"], [\"benzene\"]], np_solute_groups=applications.ALL_IN_ONE_BIN,\n",
92
+ " formulas=[\"stationary_plus_pcm\", \"stationary_plus_op_vib + openMM\"],\n",
93
+ " labels=[\"Implicit Solvent (SotA)\", \"Explicit Solvent + Vibrations (OpenMM)\"],\n",
94
+ " colors=[\"#A72608\", \"#61a89a\"], formula_remap=applications.FORMULA_REMAP,\n",
95
+ " figsize=(6, 5), y_min=0.0, save_path=None) # inline-only companion, not saved to disk"
96
+ ]
97
+ }
98
+ ],
99
+ "metadata": {
100
+ "language_info": {
101
+ "codemirror_mode": {
102
+ "name": "ipython",
103
+ "version": 3
104
+ },
105
+ "file_extension": ".py",
106
+ "mimetype": "text/x-python",
107
+ "name": "python",
108
+ "nbconvert_exporter": "python",
109
+ "pygments_lexer": "ipython3",
110
+ "version": "3.12.13"
111
+ }
112
+ },
113
+ "nbformat": 4,
114
+ "nbformat_minor": 5
115
+ }
analysis/manuscript_figures/fig5d.ipynb ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "4b208bf1",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Figure 5D: per-solute explicit-correction RMSE across the test set\n",
9
+ "\n",
10
+ "Per-solute bootstrap RMSE (¹H, solvent-averaged) for the explicit-solvent correction, ordered delta-22 -> olefin/pyridine isomers -> natural products, with a dashed \"scaled to solute\" baseline and a red \"scaled to test set\" line."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "id": "66e2a22e",
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "\n",
22
+ "# make the in-repo modules importable (not pip-installed)\n",
23
+ "REPO = os.path.abspath(\"../..\")\n",
24
+ "for _p in (\"data/applications\", \"analysis/code\", \"analysis/code/shared\"):\n",
25
+ " sys.path.insert(0, os.path.join(REPO, _p))"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": null,
31
+ "id": "9b70b4ca",
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "from applications_reader import Applications\n",
36
+ "import applications\n",
37
+ "import applications_plots\n",
38
+ "import paths"
39
+ ]
40
+ },
41
+ {
42
+ "cell_type": "code",
43
+ "execution_count": null,
44
+ "id": "2da375fc",
45
+ "metadata": {},
46
+ "outputs": [],
47
+ "source": [
48
+ "APPLICATIONS_HDF5 = paths.dataset_file(\"applications\", root=REPO)\n",
49
+ "XLSX = os.path.join(REPO, \"data\", \"applications\", \"applications_experimental.xlsx\")\n",
50
+ "\n",
51
+ "def figure_path(name):\n",
52
+ " os.makedirs(\"figures\", exist_ok=True)\n",
53
+ " return os.path.join(\"figures\", name)"
54
+ ]
55
+ },
56
+ {
57
+ "cell_type": "code",
58
+ "execution_count": null,
59
+ "id": "a6acf673",
60
+ "metadata": {},
61
+ "outputs": [],
62
+ "source": [
63
+ "loader = Applications(APPLICATIONS_HDF5, XLSX)\n",
64
+ "query_df_nn = applications.build_query_df_nn(loader)\n",
65
+ "seed = applications.build_bootstrap_seed_coeffs(loader)"
66
+ ]
67
+ },
68
+ {
69
+ "cell_type": "code",
70
+ "execution_count": null,
71
+ "id": "889345fc",
72
+ "metadata": {},
73
+ "outputs": [],
74
+ "source": [
75
+ "site_counts = (query_df_nn.drop_duplicates(subset=[\"solute\", \"nucleus\", \"site\"])\n",
76
+ " .groupby([\"solute\", \"nucleus\"]).size().unstack(fill_value=0))\n",
77
+ "per_solute = applications.per_solute_fits(query_df_nn)\n",
78
+ "all_solute = applications.per_solvent_fits(query_df_nn)\n",
79
+ "preds_h = applications.apply_bootstrap_params_to_full_dataset(query_df_nn, seed[\"H\"], nucleus=\"H\")\n",
80
+ "bootstrap_rmses_h = applications.compute_solute_rmses(preds_h)"
81
+ ]
82
+ },
83
+ {
84
+ "cell_type": "code",
85
+ "execution_count": null,
86
+ "id": "08869e35",
87
+ "metadata": {},
88
+ "outputs": [],
89
+ "source": [
90
+ "FIG5D_LABELS = {\n",
91
+ " \"isomer_1E\": \"Olefin 1 (E)\", \"isomer_1Z\": \"Olefin 1 (Z)\",\n",
92
+ " \"isomer_2E\": \"Olefin 2 (E)\", \"isomer_2Z\": \"Olefin 2 (Z)\",\n",
93
+ " \"isomer_3E\": \"Olefin 3 (E)\", \"isomer_3Z\": \"Olefin 3 (Z)\",\n",
94
+ " \"isomer_4N\": \"Pyridone 4\", \"isomer_4O\": \"Pyridine 4\",\n",
95
+ " \"vomicine\": \"Vomicine\", \"prednisone\": \"Prednisone\",\n",
96
+ " \"peptide\": \"Acetyl-L-alanyl-L-\\nglutamine\", \"flavone\": \"Flavone\",\n",
97
+ " \"dihydrotanshinone_I\": \"Dihydrotanshinone I\",\n",
98
+ "}\n",
99
+ "FIG5D_ORDER = [\"isomer_1E\", \"isomer_1Z\", \"isomer_2E\", \"isomer_2Z\", \"isomer_3E\", \"isomer_3Z\",\n",
100
+ " \"isomer_4N\", \"isomer_4O\", \"vomicine\", \"prednisone\", \"peptide\", \"flavone\",\n",
101
+ " \"dihydrotanshinone_I\"]"
102
+ ]
103
+ },
104
+ {
105
+ "cell_type": "code",
106
+ "execution_count": null,
107
+ "id": "1ad5844c",
108
+ "metadata": {},
109
+ "outputs": [],
110
+ "source": [
111
+ "applications_plots.plot_nps_on_boxplot_delta22_simplified(\n",
112
+ " loader.rmse_distribution(\"H\"), bootstrap_rmses_h, per_solute[\"H\"],\n",
113
+ " nucleus=\"H\", formulas=[\"stationary_plus_qcd + openMM\"], colors=[\"#61a89a\"],\n",
114
+ " site_counts=site_counts, formula_remap=applications.FORMULA_REMAP,\n",
115
+ " solute_remap=FIG5D_LABELS, solute_order=FIG5D_ORDER,\n",
116
+ " title=\"Complex Molecules are Dominated by Additional Physics\",\n",
117
+ " solute_color_remap=applications.PEPTIDE_HIGHLIGHT_H,\n",
118
+ " figsize=(14, 8), box_width=0.20, box_gap=0.1,\n",
119
+ " show_baseline=True, baseline_annotation_text=\"Scaled to solute\",\n",
120
+ " baseline_annotation_x=0.215, baseline_annotation_y=0.39,\n",
121
+ " show_full_fit_line=True, full_fit_line_label=\"Scaled to Test Set\", full_fit_line_label_x=0.835,\n",
122
+ " all_solute_fitting_results=all_solute,\n",
123
+ " max_bar_height=0.06, site_count_axis_mode=\"inset\", site_count_inset_area_frac=0.1,\n",
124
+ " site_count_inset_axis_offset=-0.06,\n",
125
+ " save_path=figure_path(\"fig5d_complex_1H.png\"))"
126
+ ]
127
+ }
128
+ ],
129
+ "metadata": {
130
+ "language_info": {
131
+ "codemirror_mode": {
132
+ "name": "ipython",
133
+ "version": 3
134
+ },
135
+ "file_extension": ".py",
136
+ "mimetype": "text/x-python",
137
+ "name": "python",
138
+ "nbconvert_exporter": "python",
139
+ "pygments_lexer": "ipython3",
140
+ "version": "3.12.13"
141
+ }
142
+ },
143
+ "nbformat": 4,
144
+ "nbformat_minor": 5
145
+ }
analysis/si_figures/si_composite_models_ablations.ipynb ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "bfdd4bf5",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Figure S15: composite-formula ablation workbook (`ablations.xlsx`)\n",
9
+ "\n",
10
+ "Rebuilds the `ablations.xlsx` workbook behind ~32 of SI Figure S15's images: ~25 composite-formula\n",
11
+ "variants (stationary geometry plus some combination of implicit PCM, explicit Desmond, and\n",
12
+ "rovibrational QCD corrections) across all 12 delta-22 solvents, at two reference levels:\n",
13
+ "DSD-PBEP86/pcSseg-3 (geometry PBE0/tz) and the MagNET-Zero training reference (WP04/pcSseg-2 for ¹H,\n",
14
+ "wB97X-D/pcSseg-2 for ¹³C)."
15
+ ]
16
+ },
17
+ {
18
+ "cell_type": "code",
19
+ "execution_count": null,
20
+ "id": "cb68d1d6",
21
+ "metadata": {},
22
+ "outputs": [],
23
+ "source": [
24
+ "import os, sys\n",
25
+ "\n",
26
+ "# make the in-repo modules importable (not pip-installed)\n",
27
+ "REPO = os.path.abspath(\"../..\")\n",
28
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
29
+ " sys.path.insert(0, os.path.join(REPO, _p))"
30
+ ]
31
+ },
32
+ {
33
+ "cell_type": "code",
34
+ "execution_count": null,
35
+ "id": "20914013",
36
+ "metadata": {},
37
+ "outputs": [],
38
+ "source": [
39
+ "import matplotlib.pyplot as plt\n",
40
+ "\n",
41
+ "import delta22\n",
42
+ "import composite_models\n",
43
+ "import composite_plots\n",
44
+ "import paths"
45
+ ]
46
+ },
47
+ {
48
+ "cell_type": "code",
49
+ "execution_count": null,
50
+ "id": "26ab5657",
51
+ "metadata": {},
52
+ "outputs": [],
53
+ "source": [
54
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
55
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
56
+ "\n",
57
+ "def figure_path(name):\n",
58
+ " os.makedirs(\"figures\", exist_ok=True)\n",
59
+ " return os.path.join(\"figures\", name)\n",
60
+ "\n",
61
+ "def document_path(name):\n",
62
+ " os.makedirs(\"documents\", exist_ok=True)\n",
63
+ " return os.path.join(\"documents\", name)\n",
64
+ "\n",
65
+ "# the shipped ablations.xlsx used 250 seeded train/test splits per (formula, solvent)\n",
66
+ "N_SPLITS = 250"
67
+ ]
68
+ },
69
+ {
70
+ "cell_type": "code",
71
+ "execution_count": null,
72
+ "id": "76d31292",
73
+ "metadata": {},
74
+ "outputs": [],
75
+ "source": [
76
+ "query_df_dft = delta22.add_composite_columns(delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False))\n",
77
+ "solutes = delta22.delta22_solutes(DELTA22_HDF5)\n",
78
+ "print(len(query_df_dft), \"rows;\", len(solutes), \"solutes\")"
79
+ ]
80
+ },
81
+ {
82
+ "cell_type": "markdown",
83
+ "id": "2c351f43",
84
+ "metadata": {},
85
+ "source": [
86
+ "## Preview: one formula's mean test RMSE at the DSD-PBEP86 reference level"
87
+ ]
88
+ },
89
+ {
90
+ "cell_type": "code",
91
+ "execution_count": null,
92
+ "id": "eb03ec52",
93
+ "metadata": {},
94
+ "outputs": [],
95
+ "source": [
96
+ "# preview before the full build (all ~25 formulas x 12 solvents x 250 splits x 2 nuclei x 2\n",
97
+ "# reference levels)\n",
98
+ "level = composite_models.REFERENCE_LEVELS[\"dsd\"]\n",
99
+ "preview = composite_models.ablation_rmse_table(query_df_dft, \"H\", level[\"method_h\"], level[\"basis_h\"],\n",
100
+ " level[\"geometry_h\"], solutes, n_splits=20)\n",
101
+ "preview[[\"chloroform\", \"benzene\", \"Mean Test RMSE\"]].round(3)"
102
+ ]
103
+ },
104
+ {
105
+ "cell_type": "markdown",
106
+ "id": "ee9136d7",
107
+ "metadata": {},
108
+ "source": [
109
+ "## Build the full ablations workbook (both reference levels, both nuclei)"
110
+ ]
111
+ },
112
+ {
113
+ "cell_type": "code",
114
+ "execution_count": null,
115
+ "id": "5c9fbbd3",
116
+ "metadata": {},
117
+ "outputs": [],
118
+ "source": [
119
+ "output_path = document_path(\"ablations.xlsx\")\n",
120
+ "composite_models.build_ablations_workbook(query_df_dft, solutes, output_path, n_splits=N_SPLITS)"
121
+ ]
122
+ },
123
+ {
124
+ "cell_type": "markdown",
125
+ "id": "78a693c7",
126
+ "metadata": {},
127
+ "source": [
128
+ "## Correlations Between Features\n",
129
+ "\n",
130
+ "Solvent-averaged Pearson r correlation matrix between the five composite-model features (stationary\n",
131
+ "shielding, PCM, Desmond, its vibrational analogue, QCD), both nuclei, plus a per-solvent\n",
132
+ "PCM-vs-Desmond table."
133
+ ]
134
+ },
135
+ {
136
+ "cell_type": "code",
137
+ "execution_count": null,
138
+ "id": "9ef593a6",
139
+ "metadata": {},
140
+ "outputs": [],
141
+ "source": [
142
+ "for nucleus, label in [(\"H\", \"Proton\"), (\"C\", \"Carbon\")]:\n",
143
+ " corr = delta22.si_s15_feature_correlations(query_df_dft, nucleus)\n",
144
+ " nuc_label = \"1H\" if nucleus == \"H\" else \"13C\"\n",
145
+ " nuc_title = \"$^{1}$H\" if nucleus == \"H\" else \"$^{13}$C\"\n",
146
+ " composite_plots.plot_feature_correlation_heatmap(\n",
147
+ " corr[\"r\"], vmin=-1, vmax=1, cmap=\"RdBu\",\n",
148
+ " title=f\"Solvent-Averaged Pearson $r$ Correlation Matrix for {nuc_title}\",\n",
149
+ " save_path=figure_path(f\"si_figure_s15_feature_corr_r_{nuc_label}.png\"))\n",
150
+ "plt.show()"
151
+ ]
152
+ },
153
+ {
154
+ "cell_type": "code",
155
+ "execution_count": null,
156
+ "id": "5bc382f2",
157
+ "metadata": {},
158
+ "outputs": [],
159
+ "source": [
160
+ "pcm_desmond_corr = delta22.pcm_desmond_correlation_by_solvent(query_df_dft)\n",
161
+ "print(\"PCM vs. Desmond Pearson R by nucleus and solvent:\")\n",
162
+ "display(pcm_desmond_corr.round(3))\n",
163
+ "composite_plots.plot_pcm_desmond_correlation_table(pcm_desmond_corr,\n",
164
+ " save_path=figure_path(\"si_figure_s15_pcm_desmond_corr_table.png\"))\n",
165
+ "plt.show()"
166
+ ]
167
+ }
168
+ ],
169
+ "metadata": {
170
+ "language_info": {
171
+ "name": "python"
172
+ }
173
+ },
174
+ "nbformat": 4,
175
+ "nbformat_minor": 5
176
+ }
analysis/si_figures/si_figure_s01.ipynb ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "6086dfef",
6
+ "metadata": {},
7
+ "source": "# SI Figure S1: accuracy-vs-cost Pareto frontier (proton and carbon)\n\nFigure 2A for both nuclei in CDCl3: fitting RMSE vs total compute time, colored by method type, shaped by geometry source, sized by basis (panel A proton, B carbon)."
8
+ },
9
+ {
10
+ "cell_type": "code",
11
+ "execution_count": null,
12
+ "id": "66768963",
13
+ "metadata": {},
14
+ "outputs": [],
15
+ "source": "import os, sys\n\n# make the in-repo modules importable (not pip-installed)\nREPO = os.path.abspath(\"../..\")\nfor _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n sys.path.insert(0, os.path.join(REPO, _p))"
16
+ },
17
+ {
18
+ "cell_type": "code",
19
+ "id": "ded31d36",
20
+ "source": "import delta22\nimport pareto_plot\nimport paths",
21
+ "metadata": {},
22
+ "execution_count": null,
23
+ "outputs": []
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "id": "354b195e",
28
+ "source": "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\nXLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n\ndef figure_path(name):\n os.makedirs(\"figures\", exist_ok=True)\n return os.path.join(\"figures\", name)",
29
+ "metadata": {},
30
+ "execution_count": null,
31
+ "outputs": []
32
+ },
33
+ {
34
+ "cell_type": "code",
35
+ "execution_count": null,
36
+ "id": "bd7563ca",
37
+ "metadata": {},
38
+ "outputs": [],
39
+ "source": "dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)\nnn = delta22.load_query_df_nn(DELTA22_HDF5, XLSX, verbose=False)\ndft_gas_timings, nn_timings = delta22.load_pareto_timings(DELTA22_HDF5)\n\npoints = delta22.fig2a_pareto_points(dft, nn, dft_gas_timings, nn_timings, n_splits=100)\nprint(points[\"nmr_method\"].nunique(), \"methods;\",\n \"MagNET total time\", float(points.query(\"nmr_method=='MagNET'\")[\"total_time\"].iloc[0]), \"s\")"
40
+ },
41
+ {
42
+ "cell_type": "markdown",
43
+ "id": "06d579ec",
44
+ "metadata": {},
45
+ "source": [
46
+ "## Proton and carbon Pareto panels\n",
47
+ "\n",
48
+ "Panel A is the proton frontier, B the carbon; they differ only in y-range, the NMR reference point\n",
49
+ "(wp04 for ¹H, wb97xd for ¹³C), and label offsets."
50
+ ]
51
+ },
52
+ {
53
+ "cell_type": "code",
54
+ "execution_count": null,
55
+ "id": "c5a5ed42",
56
+ "metadata": {},
57
+ "outputs": [],
58
+ "source": "method_legend_label_map = {\n \"MagNET-Zero\": \"MagNET-Zero\", \"ab initio\": r\"$\\it{ab\\ initio}$\", \"double hybrids\": \"double hybrid\",\n \"NMR-specific\": \"NMR-specific\", \"DFT\": \"DFT\",\n}\ngeometry_legend_label_map = {\"pbe0_tz\": \"DFT (PBE0/cc-pVTZ)\", \"aimnet2\": \"ML (AIMNet2)\"}\nmethod_color_map = {\"ab initio\": \"#53585f\", \"MagNET-Zero\": \"#000000\", \"double hybrids\": \"#b3a560\",\n \"NMR-specific\": \"#a72608\", \"DFT\": \"#4a8075\"}\ngeometry_marker_map = {\"aimnet2\": \"x\", \"pbe0_tz\": \"o\"}\n\n# which points to draw in each panel: MagNET-Zero, all PBE0-geometry DFT methods, plus one\n# AIMNet2-geometry NMR-specific reference (wp04 for proton, wb97xd for carbon)\nquery_str = (\"nucleus=='{nucleus}' and solvent == 'chloroform' and (\"\n \" (geometry_type=='aimnet2' and nmr_method.str.startswith('MagNET')) or\"\n \" (geometry_type=='pbe0_tz' and not nmr_method.str.startswith('MagNET')) or\"\n \" (geometry_type=='aimnet2' and nmr_method=='wp04' and basis=='pcSseg2' and nucleus=='H') or\"\n \" (geometry_type=='aimnet2' and nmr_method=='wb97xd' and basis=='pcSseg2' and nucleus=='C'))\")\n\nproton_label_positions = {\n (\"MagNET-Zero\", \"N/A\", \"aimnet2\"): {\"xytext\": (-28, 14)},\n (\"wp04\", \"pcSseg2\", \"aimnet2\"): {\"xytext\": (6, 4)},\n (\"pbe0\", \"pcSseg1\", \"pbe0_tz\"): {\"xytext\": (-22, -2)},\n (\"b2gp_plyp\", \"pcSseg1\", \"pbe0_tz\"): {\"xytext\": (4, 6)},\n (\"revdsd_pbep86\", \"pcSseg1\", \"pbe0_tz\"): {\"xytext\": (6, -4)},\n (\"revdsd_pbep86\", \"pcSseg2\", \"pbe0_tz\"): {\"xytext\": (6, 6)},\n (\"tpsstpss\", \"pcSseg2\", \"pbe0_tz\"): {\"xytext\": (6, -2)},\n (\"b97d3\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (-28, -2)},\n (\"mpw2plyp\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (8, 2)},\n (\"b2plyp\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (8, 0)},\n (\"tpsstpss\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (6, -12)},\n}\ncarbon_label_positions = {\n (\"MagNET-Zero\", \"N/A\", \"aimnet2\"): {\"xytext\": (-28, 14)},\n (\"wb97xd\", \"pcSseg2\", \"aimnet2\"): {\"xytext\": (6, -4)},\n (\"b2gp_plyp\", \"pcSseg1\", \"pbe0_tz\"): {\"xytext\": (-20, 8)},\n (\"revdsd_pbep86\", \"pcSseg1\", \"pbe0_tz\"): {\"xytext\": (0, -12)},\n (\"hf\", \"pcSseg1\", \"pbe0_tz\"): {\"xytext\": (6, -6)},\n (\"revdsd_pbep86\", \"pcSseg2\", \"pbe0_tz\"): {\"xytext\": (6, 0)},\n (\"tpsstpss\", \"pcSseg2\", \"pbe0_tz\"): {\"xytext\": (6, -4)},\n (\"mpw2plyp\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (8, 2)},\n (\"dsd_pbep86\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (8, 2)},\n (\"revdsd_pbep86\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (8, -6)},\n (\"m062x\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (8, 0)},\n (\"wp04\", \"pcSseg3\", \"pbe0_tz\"): {\"xytext\": (8, 0)},\n}"
59
+ },
60
+ {
61
+ "cell_type": "code",
62
+ "id": "ba4a0799",
63
+ "source": "pareto_plot.plot_pareto_panel(\n points, query_str, nucleus=\"H\", suptitle=\"MagNET Extends a Flat Pareto Frontier\",\n method_color_map=method_color_map, method_legend_label_map=method_legend_label_map,\n geometry_legend_label_map=geometry_legend_label_map, geometry_marker_map=geometry_marker_map,\n xlim_left=(1.7, 1.8), xlim_right=(3.8, 5.5), ylim=(0.08, 0.3),\n figsize=(12, 6), marker_alpha=0.8, manual_label_positions=proton_label_positions,\n save_png=figure_path(\"si_figure_s01_pareto_1H.png\"),\n)\n\npareto_plot.plot_pareto_panel(\n points, query_str, nucleus=\"C\", suptitle=\"MagNET Extends a Flat Pareto Frontier\",\n method_color_map=method_color_map, method_legend_label_map=method_legend_label_map,\n geometry_legend_label_map=geometry_legend_label_map, geometry_marker_map=geometry_marker_map,\n xlim_left=(1.7, 1.8), xlim_right=(3.8, 5.5), ylim=(1.2, 4.2),\n figsize=(12, 6), marker_alpha=0.8, manual_label_positions=carbon_label_positions,\n save_png=figure_path(\"si_figure_s01_pareto_13C.png\"),\n)",
64
+ "metadata": {},
65
+ "execution_count": null,
66
+ "outputs": []
67
+ }
68
+ ],
69
+ "metadata": {
70
+ "language_info": {
71
+ "codemirror_mode": {
72
+ "name": "ipython",
73
+ "version": 3
74
+ },
75
+ "file_extension": ".py",
76
+ "mimetype": "text/x-python",
77
+ "name": "python",
78
+ "nbconvert_exporter": "python",
79
+ "pygments_lexer": "ipython3",
80
+ "version": "3.12.13"
81
+ }
82
+ },
83
+ "nbformat": 4,
84
+ "nbformat_minor": 5
85
+ }
analysis/si_figures/si_figure_s02_diels_alder.ipynb ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "69a77df0",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Figure S2: literature Diels-Alder barrier predictions for ethylene + butadiene\n",
9
+ "\n",
10
+ "Literature quantum-chemistry activation barriers for the ethylene + butadiene Diels-Alder reaction, spanning ~15-45 kcal/mol (experiment ~23.3 kcal/mol)."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "id": "876d6185",
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "\n",
22
+ "# make the in-repo modules importable (not pip-installed)\n",
23
+ "REPO = os.path.abspath(\"../..\")\n",
24
+ "for _p in (\"analysis/code\", \"analysis/code/shared\"):\n",
25
+ " sys.path.insert(0, os.path.join(REPO, _p))"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": null,
31
+ "id": "65812dad",
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "import matplotlib.pyplot as plt\n",
36
+ "from matplotlib.colors import TwoSlopeNorm\n",
37
+ "\n",
38
+ "import diels_alder"
39
+ ]
40
+ },
41
+ {
42
+ "cell_type": "code",
43
+ "execution_count": null,
44
+ "id": "09cc398b",
45
+ "metadata": {},
46
+ "outputs": [],
47
+ "source": [
48
+ "def figure_path(name):\n",
49
+ " os.makedirs(\"figures\", exist_ok=True)\n",
50
+ " return os.path.join(\"figures\", name)"
51
+ ]
52
+ },
53
+ {
54
+ "cell_type": "code",
55
+ "execution_count": null,
56
+ "id": "f4057ed3",
57
+ "metadata": {},
58
+ "outputs": [],
59
+ "source": [
60
+ "print(len(diels_alder.DATA), 'literature predictions; experimental barrier', diels_alder.EXPERIMENTAL_BARRIER, 'kcal/mol')\n",
61
+ "print('range:', min(e for _, e in diels_alder.DATA), 'to', max(e for _, e in diels_alder.DATA), 'kcal/mol')"
62
+ ]
63
+ },
64
+ {
65
+ "cell_type": "code",
66
+ "execution_count": null,
67
+ "id": "9054cad6",
68
+ "metadata": {},
69
+ "outputs": [],
70
+ "source": [
71
+ "# One bar per literature prediction, grouped by method family (diels_alder.ordered_layout) and sorted by\n",
72
+ "# barrier within each group, colored by distance from the experimental value (red above, blue below),\n",
73
+ "# with a dashed line at the experimental barrier.\n",
74
+ "layout = diels_alder.ordered_layout(diels_alder.DATA)\n",
75
+ "methods, energies, positions = layout[\"methods\"], layout[\"energies\"], layout[\"positions\"]\n",
76
+ "\n",
77
+ "norm = TwoSlopeNorm(vmin=min(energies), vcenter=diels_alder.EXPERIMENTAL_BARRIER, vmax=max(energies))\n",
78
+ "colormap = plt.cm.RdBu_r\n",
79
+ "colors = [colormap(norm(e)) for e in energies]\n",
80
+ "\n",
81
+ "fig, ax = plt.subplots(figsize=(16, 8))\n",
82
+ "ax.bar(positions, energies, color=colors, alpha=0.8, edgecolor=\"black\", linewidth=0.5)\n",
83
+ "ax.axhline(y=diels_alder.EXPERIMENTAL_BARRIER, color=\"black\", linestyle=\"--\", linewidth=2)\n",
84
+ "\n",
85
+ "# callout on the B3LYP/6-31G* bar at 23.1 kcal/mol (the level used elsewhere in the paper)\n",
86
+ "b3lyp = next((i for i, (m, e) in enumerate(zip(methods, energies))\n",
87
+ " if \"B3LYP/6-31G*\" in m and abs(e - 23.1) < 0.1), None)\n",
88
+ "if b3lyp is not None:\n",
89
+ " ax.annotate(f\"Experimental: {diels_alder.EXPERIMENTAL_BARRIER} kcal/mol\",\n",
90
+ " xy=(positions[b3lyp], diels_alder.EXPERIMENTAL_BARRIER),\n",
91
+ " xytext=(positions[b3lyp], diels_alder.EXPERIMENTAL_BARRIER + 8),\n",
92
+ " arrowprops=dict(arrowstyle=\"->\", color=\"black\", lw=1),\n",
93
+ " fontsize=10, fontweight=\"bold\", ha=\"center\",\n",
94
+ " bbox=dict(boxstyle=\"round,pad=0.3\", facecolor=\"white\", edgecolor=\"black\", alpha=0.8))\n",
95
+ "\n",
96
+ "ax.set_ylabel(\"Predicted barrier (kcal/mol)\")\n",
97
+ "ax.set_xticks(positions)\n",
98
+ "ax.set_xticklabels(methods, rotation=45, ha=\"right\", fontsize=8)\n",
99
+ "if b3lyp is not None:\n",
100
+ " ax.get_xticklabels()[b3lyp].set_fontweight(\"bold\")\n",
101
+ "\n",
102
+ "y_range = ax.get_ylim()[1] - ax.get_ylim()[0]\n",
103
+ "label_y = ax.get_ylim()[0] - y_range * 0.25\n",
104
+ "for category, (start, end) in zip(layout[\"group_labels\"], layout[\"group_boundaries\"]):\n",
105
+ " ax.text((start + end) / 2, label_y, category, ha=\"center\", va=\"top\",\n",
106
+ " fontweight=\"bold\", fontsize=11)\n",
107
+ "\n",
108
+ "fig.tight_layout()\n",
109
+ "fig.savefig(figure_path(\"si_figure_s02_diels_alder.png\"), dpi=300, bbox_inches=\"tight\")\n",
110
+ "plt.show()"
111
+ ]
112
+ }
113
+ ],
114
+ "metadata": {
115
+ "language_info": {
116
+ "codemirror_mode": {
117
+ "name": "ipython",
118
+ "version": 3
119
+ },
120
+ "file_extension": ".py",
121
+ "mimetype": "text/x-python",
122
+ "name": "python",
123
+ "nbconvert_exporter": "python",
124
+ "pygments_lexer": "ipython3",
125
+ "version": "3.12.13"
126
+ }
127
+ },
128
+ "nbformat": 4,
129
+ "nbformat_minor": 5
130
+ }
analysis/si_figures/si_figure_s04.ipynb ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "007f5b29",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Figure S4: correlation of implicit (PCM) corrections across solvents and methods\n",
9
+ "\n",
10
+ "**S4A** correlation across solvents (both nuclei), **S4B** one solvent vs another (¹H), **S4C**\n",
11
+ "correlation across methods (both nuclei). Cells show -log10(1-r), so 3 means r=0.999."
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "id": "34163231",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "import os, sys\n",
22
+ "\n",
23
+ "# make the in-repo modules importable (not pip-installed)\n",
24
+ "REPO = os.path.abspath(\"../..\")\n",
25
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
26
+ " sys.path.insert(0, os.path.join(REPO, _p))"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "id": "d4fced82",
33
+ "metadata": {},
34
+ "outputs": [],
35
+ "source": [
36
+ "import numpy as np\n",
37
+ "import matplotlib.pyplot as plt\n",
38
+ "\n",
39
+ "import delta22\n",
40
+ "import delta22_plots\n",
41
+ "import paths"
42
+ ]
43
+ },
44
+ {
45
+ "cell_type": "code",
46
+ "execution_count": null,
47
+ "id": "2d316f79",
48
+ "metadata": {},
49
+ "outputs": [],
50
+ "source": [
51
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
52
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
53
+ "\n",
54
+ "def figure_path(name):\n",
55
+ " os.makedirs(\"figures\", exist_ok=True)\n",
56
+ " return os.path.join(\"figures\", name)"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "execution_count": null,
62
+ "id": "cb9e3ebb",
63
+ "metadata": {},
64
+ "outputs": [],
65
+ "source": [
66
+ "METHOD, BASIS, GEOM = \"b3lyp_d3bj\", \"pcSseg2\", \"pbe0_tz\"\n",
67
+ "dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)\n",
68
+ "base = dft[(dft[\"sap_nmr_method\"] == METHOD) & (dft[\"sap_basis\"] == BASIS)\n",
69
+ " & (dft[\"sap_geometry_type\"] == GEOM)]\n",
70
+ "one = base[base[\"nucleus\"] == \"H\"]\n",
71
+ "\n",
72
+ "# solvent order that groups polar-aprotic -> polar-protic -> aromatic (matches the published panel)\n",
73
+ "ORDERED_SOLVENTS = [\"chloroform\", \"tetrahydrofuran\", \"dichloromethane\", \"acetone\", \"acetonitrile\",\n",
74
+ " \"dimethylsulfoxide\", \"trifluoroethanol\", \"methanol\", \"TIP4P\",\n",
75
+ " \"benzene\", \"toluene\", \"chlorobenzene\"]"
76
+ ]
77
+ },
78
+ {
79
+ "cell_type": "markdown",
80
+ "id": "ee2d20ba",
81
+ "metadata": {},
82
+ "source": [
83
+ "## S4A: solvent-vs-solvent PCM correlation (¹H and ¹³C)"
84
+ ]
85
+ },
86
+ {
87
+ "cell_type": "code",
88
+ "execution_count": null,
89
+ "id": "c8664491",
90
+ "metadata": {},
91
+ "outputs": [],
92
+ "source": [
93
+ "for nucleus, label in [(\"H\", \"1H\"), (\"C\", \"13C\")]:\n",
94
+ " solvent_corr = delta22.correlation_matrix(base[base[\"nucleus\"] == nucleus], \"pcm\", [\"solute\", \"site\"], \"solvent\")\n",
95
+ " solvent_corr = solvent_corr.reindex(index=ORDERED_SOLVENTS, columns=ORDERED_SOLVENTS)\n",
96
+ " vals = solvent_corr.values[np.triu_indices_from(solvent_corr.values, k=1)]\n",
97
+ " print(f\"solvent PCM correlation ({label}): mean r={np.nanmean(vals):.4f} min r={np.nanmin(vals):.4f}\")\n",
98
+ " caption = (\"Correlation coefficients between solvents across all 22 solutes.\\n\"\n",
99
+ " f\"PCM corrections computed with {METHOD} with the {BASIS} basis.\\n\"\n",
100
+ " \"A value of 3 means the coefficient is 0.999.\")\n",
101
+ " delta22_plots.plot_correlation_matrix(solvent_corr, f\"Solvents are Highly Correlated ({nucleus})\", caption,\n",
102
+ " colormap=\"Reds\", show_values=True,\n",
103
+ " save_path=figure_path(f\"si_figure_s04a_{label}.png\"))"
104
+ ]
105
+ },
106
+ {
107
+ "cell_type": "markdown",
108
+ "id": "133ed792",
109
+ "metadata": {},
110
+ "source": [
111
+ "## S4B: PCM corrections, chloroform vs acetonitrile (1H) -- one square of S4A"
112
+ ]
113
+ },
114
+ {
115
+ "cell_type": "code",
116
+ "execution_count": null,
117
+ "id": "21db6810",
118
+ "metadata": {},
119
+ "outputs": [],
120
+ "source": [
121
+ "pair = one.pivot_table(index=[\"solute\", \"site\"], columns=\"solvent\", values=\"pcm\")[\n",
122
+ " [\"chloroform\", \"acetonitrile\"]].dropna()\n",
123
+ "delta22_plots.plot_pcm_scatter(pair[\"chloroform\"], pair[\"acetonitrile\"], \"chloroform\", \"acetonitrile\", \"H\",\n",
124
+ " save_path=figure_path(\"si_figure_s04b_1H.png\"))"
125
+ ]
126
+ },
127
+ {
128
+ "cell_type": "markdown",
129
+ "id": "1f0e76f9",
130
+ "metadata": {},
131
+ "source": [
132
+ "## S4C: method-vs-method correlation (chloroform, double hybrids excluded, ¹H and ¹³C)"
133
+ ]
134
+ },
135
+ {
136
+ "cell_type": "code",
137
+ "execution_count": null,
138
+ "id": "f75897e7",
139
+ "metadata": {},
140
+ "outputs": [],
141
+ "source": [
142
+ "# one solvent, exclude the double hybrids whose PCM is the substituted reference value\n",
143
+ "for nucleus, label in [(\"H\", \"1H\"), (\"C\", \"13C\")]:\n",
144
+ " chcl3 = dft[(dft[\"sap_basis\"] == BASIS) & (dft[\"sap_geometry_type\"] == GEOM)\n",
145
+ " & (dft[\"nucleus\"] == nucleus) & (dft[\"solvent\"] == \"chloroform\")\n",
146
+ " & (~dft[\"sap_nmr_method\"].isin(delta22.DOUBLE_HYBRID_METHODS))]\n",
147
+ " method_corr = delta22.correlation_matrix(chcl3, \"pcm\", [\"solute\", \"site\"], \"sap_nmr_method\")\n",
148
+ " method_order = sorted(method_corr.index) # alphabetical, matches the published panel\n",
149
+ " method_corr = method_corr.reindex(index=method_order, columns=method_order)\n",
150
+ " mvals = method_corr.values[np.triu_indices_from(method_corr.values, k=1)]\n",
151
+ " print(f\"method PCM correlation ({label}): mean r={np.nanmean(mvals):.4f} min r={np.nanmin(mvals):.4f}\")\n",
152
+ " caption = (\"Correlation coefficients between NMR methods across all 22 solutes.\\n\"\n",
153
+ " f\"PCM corrections for chloroform with the {BASIS} basis.\")\n",
154
+ " delta22_plots.plot_correlation_matrix(method_corr, f\"NMR Methods are Highly Correlated ({nucleus})\", caption,\n",
155
+ " colormap=\"Reds\", show_values=True,\n",
156
+ " save_path=figure_path(f\"si_figure_s04c_{label}.png\"))"
157
+ ]
158
+ }
159
+ ],
160
+ "metadata": {
161
+ "language_info": {
162
+ "name": "python"
163
+ }
164
+ },
165
+ "nbformat": 4,
166
+ "nbformat_minor": 5
167
+ }
analysis/si_figures/si_figure_s05.ipynb ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "1344ef50",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Figure S5: per-solvent PCM benefit vs bulk dielectric constant and polarizability\n",
9
+ "\n",
10
+ "Per-solvent PCM benefit (% test-RMSE reduction) vs bulk dielectric constant and polarizability, for\n",
11
+ "the MagNET-Zero reference method per nucleus (WP04 ¹H, wB97X-D ¹³C). Panels A (¹H) and B (¹³C)\n",
12
+ "show all solvents; C repeats ¹H excluding aromatics and trifluoroethanol."
13
+ ]
14
+ },
15
+ {
16
+ "cell_type": "code",
17
+ "execution_count": null,
18
+ "id": "dfffb6dc",
19
+ "metadata": {},
20
+ "outputs": [],
21
+ "source": [
22
+ "import os, sys\n",
23
+ "\n",
24
+ "# make the in-repo modules importable (not pip-installed)\n",
25
+ "REPO = os.path.abspath(\"../..\")\n",
26
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
27
+ " sys.path.insert(0, os.path.join(REPO, _p))"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "code",
32
+ "execution_count": null,
33
+ "id": "74921dd6",
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": [
37
+ "import matplotlib.pyplot as plt\n",
38
+ "\n",
39
+ "import delta22\n",
40
+ "import delta22_plots\n",
41
+ "import paths"
42
+ ]
43
+ },
44
+ {
45
+ "cell_type": "code",
46
+ "execution_count": null,
47
+ "id": "c3053b56",
48
+ "metadata": {},
49
+ "outputs": [],
50
+ "source": [
51
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
52
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
53
+ "\n",
54
+ "def figure_path(name):\n",
55
+ " os.makedirs(\"figures\", exist_ok=True)\n",
56
+ " return os.path.join(\"figures\", name)"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "execution_count": null,
62
+ "id": "99b0ba9e",
63
+ "metadata": {},
64
+ "outputs": [],
65
+ "source": [
66
+ "# the published panels use 250 seeded train/test splits\n",
67
+ "N_SPLITS = 250"
68
+ ]
69
+ },
70
+ {
71
+ "cell_type": "code",
72
+ "execution_count": null,
73
+ "id": "9aa30554",
74
+ "metadata": {},
75
+ "outputs": [],
76
+ "source": [
77
+ "dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)\n",
78
+ "solutes = sorted(dft[\"solute\"].unique())\n",
79
+ "benefit = {}\n",
80
+ "for nucleus, label in [(\"H\", \"1H\"), (\"C\", \"13C\")]:\n",
81
+ " method = delta22.MAGNET_PCM_OUTPUT_METHODS[nucleus]\n",
82
+ " benefit[nucleus] = delta22.pcm_benefit_per_solvent(dft, method, \"pcSseg2\", \"aimnet2\",\n",
83
+ " delta22.DESMOND_SOLVENTS, n_splits=N_SPLITS,\n",
84
+ " solutes=solutes, nucleus=nucleus)\n",
85
+ " print(f\"{label} ({method}):\"); print(benefit[nucleus].round(1).to_string())"
86
+ ]
87
+ },
88
+ {
89
+ "cell_type": "markdown",
90
+ "id": "329088d2",
91
+ "metadata": {},
92
+ "source": [
93
+ "## S5A (1H, all solvents), S5B (13C, all solvents), and S5C (1H, excluding aromatics and trifluoroethanol)"
94
+ ]
95
+ },
96
+ {
97
+ "cell_type": "code",
98
+ "execution_count": null,
99
+ "id": "2d519d66",
100
+ "metadata": {},
101
+ "outputs": [],
102
+ "source": [
103
+ "# panels A (1H) and B (13C): all solvents\n",
104
+ "for nucleus, letter, nuc_label in [(\"H\", \"A\", \"H\"), (\"C\", \"B\", \"C\")]:\n",
105
+ " delta22_plots.plot_pcm_benefit_vs_properties(\n",
106
+ " benefit[nucleus], delta22.SOLVENT_DIELECTRIC, delta22.SOLVENT_POLARIZABILITY, nuc_label,\n",
107
+ " save_path=figure_path(f\"si_figure_s05{letter}_all.png\"))\n",
108
+ "# panel C is 1H only, excluding the aromatic solvents and trifluoroethanol (matches the published SI)\n",
109
+ "delta22_plots.plot_pcm_benefit_vs_properties(\n",
110
+ " benefit[\"H\"], delta22.SOLVENT_DIELECTRIC, delta22.SOLVENT_POLARIZABILITY, \"H\",\n",
111
+ " exclude=[\"benzene\", \"toluene\", \"chlorobenzene\", \"trifluoroethanol\"],\n",
112
+ " title_extra=\"Excluding Aromatic Solvents and Trifluoroethanol\",\n",
113
+ " save_path=figure_path(\"si_figure_s05A_no_aromatics_tfe.png\"))"
114
+ ]
115
+ }
116
+ ],
117
+ "metadata": {
118
+ "language_info": {
119
+ "name": "python"
120
+ }
121
+ },
122
+ "nbformat": 4,
123
+ "nbformat_minor": 5
124
+ }
analysis/si_figures/si_figure_s06.ipynb ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "29f74212",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Figure S6: per-solvent test RMSE of six solvent-correction models\n",
9
+ "\n",
10
+ "Per nucleus, the per-solvent test RMSE of six solvent-correction models: SotA implicit (single-slope\n",
11
+ "PCM), the paper's implicit fit (stationary + PCM as separate terms), and explicit (Desmond), each\n",
12
+ "with and without a vibrational correction (QCD for ¹H, Desmond vibration for ¹³C). Level of theory:\n",
13
+ "dsd_pbep86/pcSseg3 on pbe0_tz geometries, PCM substituted from b3lyp_d3bj/pcSseg3."
14
+ ]
15
+ },
16
+ {
17
+ "cell_type": "code",
18
+ "execution_count": null,
19
+ "id": "6871225d",
20
+ "metadata": {},
21
+ "outputs": [],
22
+ "source": [
23
+ "import os, sys\n",
24
+ "\n",
25
+ "# make the in-repo modules importable (not pip-installed)\n",
26
+ "REPO = os.path.abspath(\"../..\")\n",
27
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
28
+ " sys.path.insert(0, os.path.join(REPO, _p))"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "execution_count": null,
34
+ "id": "3bad055e",
35
+ "metadata": {},
36
+ "outputs": [],
37
+ "source": [
38
+ "import matplotlib.pyplot as plt\n",
39
+ "\n",
40
+ "import delta22\n",
41
+ "import delta22_plots\n",
42
+ "import paths"
43
+ ]
44
+ },
45
+ {
46
+ "cell_type": "code",
47
+ "execution_count": null,
48
+ "id": "a55272ae",
49
+ "metadata": {},
50
+ "outputs": [],
51
+ "source": [
52
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
53
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
54
+ "\n",
55
+ "def figure_path(name):\n",
56
+ " os.makedirs(\"figures\", exist_ok=True)\n",
57
+ " return os.path.join(\"figures\", name)"
58
+ ]
59
+ },
60
+ {
61
+ "cell_type": "code",
62
+ "execution_count": null,
63
+ "id": "221a152c",
64
+ "metadata": {},
65
+ "outputs": [],
66
+ "source": [
67
+ "query = delta22.add_composite_columns(delta22.load_query_df_dft(\n",
68
+ " DELTA22_HDF5, XLSX, pcm_reference_method=\"b3lyp_d3bj\", pcm_reference_basis=\"pcSseg3\", verbose=False))\n",
69
+ "solutes = sorted(query[\"solute\"].unique())\n",
70
+ "\n",
71
+ "METHOD, BASIS, GEOM = \"dsd_pbep86\", \"pcSseg3\", \"pbe0_tz\"\n",
72
+ "\n",
73
+ "# same six categories for both nuclei; the \"+ Vibrations\" term differs (qcd for H, desmond_vib for C)\n",
74
+ "LADDER = {\n",
75
+ " \"H\": {\n",
76
+ " \"stationary_plus_pcm\": \"SotA Implicit Solvent\",\n",
77
+ " \"stationary_plus_pcm_plus_qcd\": \"SotA Implicit Solvent + Vibrations\",\n",
78
+ " \"stationary + pcm\": \"Implicit Solvent\",\n",
79
+ " \"stationary_plus_qcd + pcm\": \"Implicit Solvent + Vibrations\",\n",
80
+ " \"stationary + desmond\": \"Explicit Solvent\",\n",
81
+ " \"stationary_plus_qcd + desmond\": \"Explicit Solvent + Vibrations\"},\n",
82
+ " \"C\": {\n",
83
+ " \"stationary_plus_pcm\": \"SotA Implicit Solvent\",\n",
84
+ " \"stationary_plus_pcm_plus_des_vib\": \"SotA Implicit Solvent + Vibrations\",\n",
85
+ " \"stationary + pcm\": \"Implicit Solvent\",\n",
86
+ " \"stationary_plus_des_vib + pcm\": \"Implicit Solvent + Vibrations\",\n",
87
+ " \"stationary + desmond\": \"Explicit Solvent\",\n",
88
+ " \"stationary_plus_des_vib + desmond\": \"Explicit Solvent + Vibrations\"},\n",
89
+ "}"
90
+ ]
91
+ },
92
+ {
93
+ "cell_type": "code",
94
+ "execution_count": null,
95
+ "id": "d1b678b5",
96
+ "metadata": {},
97
+ "outputs": [],
98
+ "source": [
99
+ "# one dark/light pair per solvent-treatment family (SotA implicit / implicit / explicit), light = +Vibrations\n",
100
+ "S6_COLORS = [\"#C4B037\", \"#F5EDA0\", \"#A72608\", \"#E4A0A0\", \"#5F7C8A\", \"#B9E7DF\"]"
101
+ ]
102
+ },
103
+ {
104
+ "cell_type": "markdown",
105
+ "id": "53817ec5",
106
+ "metadata": {},
107
+ "source": [
108
+ "## Formula ladder per nucleus"
109
+ ]
110
+ },
111
+ {
112
+ "cell_type": "code",
113
+ "execution_count": null,
114
+ "id": "6cd062c4",
115
+ "metadata": {},
116
+ "outputs": [],
117
+ "source": [
118
+ "# the published panels use 250 seeded train/test splits\n",
119
+ "N_SPLITS = 250"
120
+ ]
121
+ },
122
+ {
123
+ "cell_type": "code",
124
+ "execution_count": null,
125
+ "id": "d57a9788",
126
+ "metadata": {},
127
+ "outputs": [],
128
+ "source": [
129
+ "for nucleus, labels in LADDER.items():\n",
130
+ " results = delta22.fig3d_formula_regressions(query, METHOD, BASIS, GEOM, list(labels),\n",
131
+ " delta22.DESMOND_SOLVENTS, n_splits=N_SPLITS,\n",
132
+ " solutes=solutes, nucleus=nucleus)\n",
133
+ " med = results.groupby(\"formula\")[\"test_RMSE\"].median()\n",
134
+ " print(f\"--- {nucleus} ({METHOD}) median test RMSE ---\")\n",
135
+ " for formula in labels:\n",
136
+ " print(f\" {labels[formula]:38s} {med[formula]:.4f}\")\n",
137
+ " delta22_plots.plot_formula_ladder_boxplot(\n",
138
+ " results, labels, delta22.SOLVENT_GROUPS, nucleus=nucleus, colors=S6_COLORS,\n",
139
+ " title=f\"Explicit Solvation + Vibrational Effects Improve Predictions Across Solvents ({nucleus} Nucleus)\",\n",
140
+ " save_path=figure_path(f\"si_figure_s06_ladder_{'1H' if nucleus == 'H' else '13C'}.png\"))\n",
141
+ "plt.show()"
142
+ ]
143
+ }
144
+ ],
145
+ "metadata": {
146
+ "language_info": {
147
+ "name": "python"
148
+ }
149
+ },
150
+ "nbformat": 4,
151
+ "nbformat_minor": 5
152
+ }
analysis/si_figures/si_figure_s07.ipynb ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "99d967bc",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Figure S7: DFT ¹³C explicit-solvent correction, Desmond vs OpenMM\n",
9
+ "\n",
10
+ "For the four OpenMM solvents, the DFT ¹³C explicit-solvent correction from Desmond MD vs from OpenMM\n",
11
+ "(y=x reference line)."
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "id": "b506b1dc",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "import os, sys\n",
22
+ "\n",
23
+ "# make the in-repo modules importable (not pip-installed)\n",
24
+ "REPO = os.path.abspath(\"../..\")\n",
25
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
26
+ " sys.path.insert(0, os.path.join(REPO, _p))"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "id": "97266e80",
33
+ "metadata": {},
34
+ "outputs": [],
35
+ "source": [
36
+ "import numpy as np\n",
37
+ "import matplotlib.pyplot as plt\n",
38
+ "\n",
39
+ "import delta22\n",
40
+ "import delta22_plots\n",
41
+ "import paths"
42
+ ]
43
+ },
44
+ {
45
+ "cell_type": "code",
46
+ "execution_count": null,
47
+ "id": "01aca94d",
48
+ "metadata": {},
49
+ "outputs": [],
50
+ "source": [
51
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
52
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
53
+ "\n",
54
+ "def figure_path(name):\n",
55
+ " os.makedirs(\"figures\", exist_ok=True)\n",
56
+ " return os.path.join(\"figures\", name)"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "execution_count": null,
62
+ "id": "f78afb6a",
63
+ "metadata": {},
64
+ "outputs": [],
65
+ "source": [
66
+ "dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)"
67
+ ]
68
+ },
69
+ {
70
+ "cell_type": "markdown",
71
+ "id": "b83900b6",
72
+ "metadata": {},
73
+ "source": [
74
+ "## DFT explicit corrections: Desmond vs OpenMM"
75
+ ]
76
+ },
77
+ {
78
+ "cell_type": "code",
79
+ "execution_count": null,
80
+ "id": "23f9e6f4",
81
+ "metadata": {},
82
+ "outputs": [],
83
+ "source": [
84
+ "SOLVENT_ORDER = [\"chloroform\", \"methanol\", \"TIP4P\", \"benzene\"]\n",
85
+ "dft_pairs = {sv: delta22.explicit_correction_pairs(dft, sv, \"C\") for sv in SOLVENT_ORDER}\n",
86
+ "for sv, p in dft_pairs.items():\n",
87
+ " print(f\"DFT {sv:12s} n={len(p):3d} r={np.corrcoef(p['desmond'], p['openMM'])[0,1]:.3f}\")\n",
88
+ "delta22_plots.plot_desmond_vs_openmm_grid(dft_pairs, save_path=figure_path(\"si_figure_s07.png\"))\n",
89
+ "plt.show()"
90
+ ]
91
+ }
92
+ ],
93
+ "metadata": {
94
+ "language_info": {
95
+ "name": "python"
96
+ }
97
+ },
98
+ "nbformat": 4,
99
+ "nbformat_minor": 5
100
+ }
analysis/si_figures/si_figure_s08_panelA_vomicine.ipynb ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "eef9d8ce",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Figure S8 (panel A): explicit-solvent correction vs solvent-shell size (vomicine)\n",
9
+ "\n",
10
+ "The MagNET-x explicit-solvent correction (solvated minus isolated, averaged over a proton site's\n",
11
+ "atoms and MD frames) vs solvent-shell size (heavy-atom count of the solute-plus-solvent cluster),\n",
12
+ "for vomicine, one panel per solvent."
13
+ ]
14
+ },
15
+ {
16
+ "cell_type": "code",
17
+ "execution_count": null,
18
+ "id": "797c7b29",
19
+ "metadata": {},
20
+ "outputs": [],
21
+ "source": [
22
+ "import os, sys\n",
23
+ "\n",
24
+ "# make the in-repo modules importable (not pip-installed)\n",
25
+ "REPO = os.path.abspath(\"../..\")\n",
26
+ "for _p in (\"data/applications\", \"analysis/code\", \"analysis/code/shared\"):\n",
27
+ " sys.path.insert(0, os.path.join(REPO, _p))"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "code",
32
+ "execution_count": null,
33
+ "id": "1f174bf0",
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": [
37
+ "import matplotlib.pyplot as plt\n",
38
+ "\n",
39
+ "from applications_reader import Applications, SHELL_SIZES\n",
40
+ "import applications\n",
41
+ "import paths"
42
+ ]
43
+ },
44
+ {
45
+ "cell_type": "code",
46
+ "execution_count": null,
47
+ "id": "d616ff60",
48
+ "metadata": {},
49
+ "outputs": [],
50
+ "source": [
51
+ "DATA = os.path.join(REPO, \"data\", \"applications\")\n",
52
+ "\n",
53
+ "def figure_path(name):\n",
54
+ " os.makedirs(\"figures\", exist_ok=True)\n",
55
+ " return os.path.join(\"figures\", name)"
56
+ ]
57
+ },
58
+ {
59
+ "cell_type": "code",
60
+ "execution_count": null,
61
+ "id": "eab9a523",
62
+ "metadata": {},
63
+ "outputs": [],
64
+ "source": [
65
+ "loader = Applications(paths.dataset_file(\"applications\", root=REPO),\n",
66
+ " os.path.join(DATA, \"applications_experimental.xlsx\"))\n",
67
+ "\n",
68
+ "# compute the per-site, per-solvent shell-convergence corrections for vomicine (proton sites)\n",
69
+ "SOLUTE, NUCLEUS = \"vomicine\", \"H\"\n",
70
+ "corrections = applications.shell_convergence_corrections(loader, SOLUTE, NUCLEUS)\n",
71
+ "corrections.head()"
72
+ ]
73
+ },
74
+ {
75
+ "cell_type": "code",
76
+ "execution_count": null,
77
+ "id": "1f7d00a0",
78
+ "metadata": {},
79
+ "outputs": [],
80
+ "source": [
81
+ "# panel layout and font styling for the 2x2 grid (one panel per solvent)\n",
82
+ "solvent_order = [\"chloroform\", \"methanol\", \"TIP4P\", \"benzene\"]\n",
83
+ "shell_cols = [f\"shell_{s}\" for s in SHELL_SIZES]\n",
84
+ "plt.rcParams.update({\"font.family\": \"serif\", \"font.serif\": [\"Arial\", \"DejaVu Serif\"],\n",
85
+ " \"axes.labelsize\": 12, \"axes.titlesize\": 14})"
86
+ ]
87
+ },
88
+ {
89
+ "cell_type": "code",
90
+ "execution_count": null,
91
+ "id": "0350cca5",
92
+ "metadata": {},
93
+ "outputs": [],
94
+ "source": [
95
+ "# 2x2 panel, one solvent each; one line per proton site\n",
96
+ "fig, axes = plt.subplots(2, 2, figsize=(11, 8), sharex=True, sharey=True)\n",
97
+ "for ax, solvent in zip(axes.ravel(), solvent_order):\n",
98
+ " sub = corrections.xs(solvent, level=\"solvent\")\n",
99
+ " sub = sub.dropna(how=\"all\")\n",
100
+ " ax.set_title(\"TIP4P\" if solvent == \"TIP4P\" else solvent.capitalize(), fontweight=\"bold\")\n",
101
+ " for site, row in sub.iterrows():\n",
102
+ " ax.plot(SHELL_SIZES, row[shell_cols].values.astype(float),\n",
103
+ " marker=\"o\", linewidth=1.5, alpha=0.75, label=site)\n",
104
+ " ax.grid(True, alpha=0.3)\n",
105
+ " ax.set_xlabel(\"Shell Size (Heavy Atom Count)\", fontweight=\"bold\")\n",
106
+ " ax.set_ylabel(\"OpenMM correction (ppm)\", fontweight=\"bold\")\n",
107
+ "fig.suptitle(\"Shell-size Convergence for Vomicine\", fontweight=\"bold\", fontsize=18)\n",
108
+ "fig.tight_layout()\n",
109
+ "fig.savefig(figure_path(\"si_figure_s08_vomicine.png\"), dpi=300, bbox_inches=\"tight\")\n",
110
+ "plt.show()"
111
+ ]
112
+ }
113
+ ],
114
+ "metadata": {
115
+ "language_info": {
116
+ "name": "python"
117
+ }
118
+ },
119
+ "nbformat": 4,
120
+ "nbformat_minor": 5
121
+ }
analysis/si_figures/si_figure_s08_panelsBCD.ipynb ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "d45e897a",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Figure S8 (panels B-D): explicit-solvent correction vs MD-frame count\n",
9
+ "\n",
10
+ "For an example site (AcOH proton in chloroform), the explicit-solvent correction vs number of MD frames included (running average): OpenMM vs Desmond (B1) and DFT vs MagNET-x/\"NN\" at fixed OpenMM (B3), with per-frame distributions (B2/B4) and autocorrelation (C). Panel D shows which frames have computed DFT data per solvent (also the source of B's histogram normalization)."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "execution_count": null,
16
+ "id": "e0fcff5a",
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "\n",
22
+ "# make the in-repo modules importable (not pip-installed)\n",
23
+ "REPO = os.path.abspath(\"../..\")\n",
24
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
25
+ " sys.path.insert(0, os.path.join(REPO, _p))"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": null,
31
+ "id": "62fa04d3",
32
+ "metadata": {},
33
+ "outputs": [],
34
+ "source": [
35
+ "import numpy as np\n",
36
+ "import matplotlib.pyplot as plt\n",
37
+ "\n",
38
+ "import delta22\n",
39
+ "import delta22_reader\n",
40
+ "import delta22_plots\n",
41
+ "import paths"
42
+ ]
43
+ },
44
+ {
45
+ "cell_type": "code",
46
+ "execution_count": null,
47
+ "id": "5ba77909",
48
+ "metadata": {},
49
+ "outputs": [],
50
+ "source": [
51
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
52
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
53
+ "\n",
54
+ "def figure_path(name):\n",
55
+ " os.makedirs(\"figures\", exist_ok=True)\n",
56
+ " return os.path.join(\"figures\", name)"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "execution_count": null,
62
+ "id": "a93dfea1",
63
+ "metadata": {},
64
+ "outputs": [],
65
+ "source": [
66
+ "site_atoms = delta22_reader.load_site_atom_data(XLSX, verbose=False)\n",
67
+ "idx = delta22.site_atom_indices(site_atoms.loc[(\"AcOH\", \"H\", \"H\"), \"atom_numbers\"])\n",
68
+ "SOLUTE, SOLVENT = \"AcOH\", \"chloroform\""
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "code",
73
+ "execution_count": null,
74
+ "id": "f50407ff",
75
+ "metadata": {},
76
+ "outputs": [],
77
+ "source": [
78
+ "# OpenMM/Desmond engine hues; DFT vs NN reuse the OpenMM hue (DFT lightened, NN full strength)\n",
79
+ "ENGINE_COLORS = {\"openMM\": \"#2E86AB\", \"desmond\": \"#A23B72\"}\n",
80
+ "ENGINE_LABELS = {\"openMM\": \"OpenMM\", \"desmond\": \"Desmond\"}\n",
81
+ "\n",
82
+ "SOURCE_COLORS = {\"dft\": delta22_plots.lighten_color(ENGINE_COLORS[\"openMM\"]), \"nn\": ENGINE_COLORS[\"openMM\"]}\n",
83
+ "SOURCE_LABELS = {\"dft\": \"DFT\", \"nn\": \"NN\"}\n",
84
+ "LABEL_COLORS = {**ENGINE_COLORS, **SOURCE_COLORS}\n",
85
+ "LABEL_NAMES = {**ENGINE_LABELS, **SOURCE_LABELS}"
86
+ ]
87
+ },
88
+ {
89
+ "cell_type": "markdown",
90
+ "id": "398912a2",
91
+ "metadata": {},
92
+ "source": [
93
+ "## Panel B1/B2: OpenMM vs Desmond\n",
94
+ "\n",
95
+ "Running-average convergence (B1) and per-frame distribution (B2), comparing the two MD engines."
96
+ ]
97
+ },
98
+ {
99
+ "cell_type": "code",
100
+ "execution_count": null,
101
+ "id": "32f05604",
102
+ "metadata": {},
103
+ "outputs": [],
104
+ "source": [
105
+ "running_by_engine, finals_by_engine, per_frame_by_engine, n_frames_by_engine = {}, {}, {}, {}\n",
106
+ "for engine in [\"openMM\", \"desmond\"]:\n",
107
+ " perturbed = delta22_reader.load_perturbed_shieldings(DELTA22_HDF5, SOLUTE, SOLVENT, engine, \"dft\")\n",
108
+ " per_frame = delta22.frame_corrections(perturbed, idx)\n",
109
+ " running = delta22.running_average(per_frame)\n",
110
+ " running_by_engine[engine] = running\n",
111
+ " finals_by_engine[engine] = running[~np.isnan(running)][-1]\n",
112
+ " per_frame_by_engine[engine] = per_frame\n",
113
+ " n_frames_by_engine[engine] = len(per_frame) # the total trajectory length, valid or not\n",
114
+ " print(f\"{engine}: {int(np.sum(~np.isnan(per_frame)))} / {len(per_frame)} valid frames, \"\n",
115
+ " f\"converged correction {finals_by_engine[engine]:.4f} ppm\")\n",
116
+ "\n",
117
+ "delta22_plots.plot_frame_convergence(\n",
118
+ " running_by_engine, finals_by_engine, LABEL_COLORS, LABEL_NAMES,\n",
119
+ " title=f\"Convergence of Explicit Solvent Corrections\\n({SOLUTE} in {SOLVENT})\",\n",
120
+ " save_path=figure_path(\"si_figure_s08_b1_convergence_engine.png\"))\n",
121
+ "plt.show()\n",
122
+ "\n",
123
+ "delta22_plots.plot_frame_correction_histogram(\n",
124
+ " per_frame_by_engine, n_frames_by_engine, LABEL_COLORS, LABEL_NAMES,\n",
125
+ " title=\"Distribution of Frame-wise Corrections\",\n",
126
+ " save_path=figure_path(\"si_figure_s08_b2_histogram_engine.png\"))\n",
127
+ "plt.show()"
128
+ ]
129
+ },
130
+ {
131
+ "cell_type": "markdown",
132
+ "id": "be656fba",
133
+ "metadata": {},
134
+ "source": [
135
+ "## Panel B3/B4: DFT vs NN\n",
136
+ "\n",
137
+ "Same running average (B3) and per-frame distribution (B4), at fixed OpenMM engine, comparing DFT vs\n",
138
+ "MagNET-x (\"NN\") shieldings."
139
+ ]
140
+ },
141
+ {
142
+ "cell_type": "code",
143
+ "execution_count": null,
144
+ "id": "9f996246",
145
+ "metadata": {},
146
+ "outputs": [],
147
+ "source": [
148
+ "running_by_source, finals_by_source, per_frame_by_source, n_frames_by_source = {}, {}, {}, {}\n",
149
+ "for source in [\"dft\", \"nn\"]:\n",
150
+ " perturbed = delta22_reader.load_perturbed_shieldings(DELTA22_HDF5, SOLUTE, SOLVENT, \"openMM\", source)\n",
151
+ " per_frame = delta22.frame_corrections(perturbed, idx)\n",
152
+ " running = delta22.running_average(per_frame)\n",
153
+ " running_by_source[source] = running\n",
154
+ " finals_by_source[source] = running[~np.isnan(running)][-1]\n",
155
+ " per_frame_by_source[source] = per_frame\n",
156
+ " n_frames_by_source[source] = len(per_frame)\n",
157
+ " print(f\"{source}: {int(np.sum(~np.isnan(per_frame)))} / {len(per_frame)} valid frames, \"\n",
158
+ " f\"converged correction {finals_by_source[source]:.4f} ppm\")\n",
159
+ "\n",
160
+ "delta22_plots.plot_frame_convergence(\n",
161
+ " running_by_source, finals_by_source, LABEL_COLORS, LABEL_NAMES, xlabel=\"Number of MD Frames\",\n",
162
+ " title=f\"OpenMM DFT vs. NN Convergence Comparison\\n({SOLUTE} in {SOLVENT})\",\n",
163
+ " save_path=figure_path(\"si_figure_s08_b3_convergence_source.png\"))\n",
164
+ "plt.show()\n",
165
+ "\n",
166
+ "delta22_plots.plot_frame_correction_histogram(\n",
167
+ " per_frame_by_source, n_frames_by_source, LABEL_COLORS, LABEL_NAMES,\n",
168
+ " title=\"Distribution of Frame-wise Corrections\",\n",
169
+ " save_path=figure_path(\"si_figure_s08_b4_histogram_source.png\"))\n",
170
+ "plt.show()"
171
+ ]
172
+ },
173
+ {
174
+ "cell_type": "markdown",
175
+ "id": "483a55bb",
176
+ "metadata": {},
177
+ "source": [
178
+ "## Panel C: autocorrelation, DFT vs NN\n",
179
+ "\n",
180
+ "Autocorrelation of the per-frame correction to lag 200 (~100 frames per trajectory repeat), DFT vs NN,\n",
181
+ "same OpenMM trajectory as B3/B4."
182
+ ]
183
+ },
184
+ {
185
+ "cell_type": "code",
186
+ "execution_count": null,
187
+ "id": "4b365dfe",
188
+ "metadata": {},
189
+ "outputs": [],
190
+ "source": [
191
+ "autocorr_by_source = {source: delta22.autocorrelation(per_frame_by_source[source], max_lag=200)\n",
192
+ " for source in [\"dft\", \"nn\"]}\n",
193
+ "for source, autocorr in autocorr_by_source.items():\n",
194
+ " print(f\"{source}: lag-1 autocorrelation {autocorr[1]:.3f}\")\n",
195
+ "\n",
196
+ "delta22_plots.plot_frame_autocorrelation(\n",
197
+ " autocorr_by_source, LABEL_COLORS, LABEL_NAMES,\n",
198
+ " title=f\"Autocorrelation of Frame-wise Corrections\\n({SOLUTE} in {SOLVENT})\",\n",
199
+ " save_path=figure_path(\"si_figure_s08_c_autocorrelation.png\"))\n",
200
+ "plt.show()"
201
+ ]
202
+ },
203
+ {
204
+ "cell_type": "markdown",
205
+ "id": "bc704f01",
206
+ "metadata": {},
207
+ "source": [
208
+ "## Panel D: frame data availability\n",
209
+ "\n",
210
+ "Which trajectory frames have computed DFT shielding for AcOH, across all 12 Desmond and 4 OpenMM\n",
211
+ "solvents. DFT was computed for a non-contiguous subset (compute-budget limits; jobs queued randomly)."
212
+ ]
213
+ },
214
+ {
215
+ "cell_type": "code",
216
+ "execution_count": null,
217
+ "id": "ece733ef",
218
+ "metadata": {},
219
+ "outputs": [],
220
+ "source": [
221
+ "# row order matching the published panel: chloroform first, then by solvent class (aprotic ->\n",
222
+ "# protic -> aromatic); OpenMM only has the four explicit-solvent solvents\n",
223
+ "DESMOND_ORDER = [\"chloroform\", \"tetrahydrofuran\", \"dichloromethane\", \"acetone\", \"acetonitrile\",\n",
224
+ " \"dimethylsulfoxide\", \"trifluoroethanol\", \"methanol\", \"TIP4P\",\n",
225
+ " \"benzene\", \"toluene\", \"chlorobenzene\"]\n",
226
+ "OPENMM_ORDER = [\"chloroform\", \"methanol\", \"TIP4P\", \"benzene\"]\n",
227
+ "\n",
228
+ "grids_by_engine, solvents_by_engine = {}, {}\n",
229
+ "for engine, solvents in [(\"desmond\", DESMOND_ORDER), (\"openMM\", OPENMM_ORDER)]:\n",
230
+ " grid, frame_counts = delta22.frame_validity_grid(DELTA22_HDF5, SOLUTE, solvents, engine, shield_type=\"dft\")\n",
231
+ " grids_by_engine[engine] = grid\n",
232
+ " solvents_by_engine[engine] = [delta22_plots.display_solvent_name(s) for s in solvents]\n",
233
+ " print(f\"{engine}: {len(solvents)} solvents, frame counts {dict(zip(solvents, frame_counts))}\")\n",
234
+ "\n",
235
+ "delta22_plots.plot_frame_validity_heatmaps(grids_by_engine, solvents_by_engine, ENGINE_LABELS, solute=SOLUTE,\n",
236
+ " save_path=figure_path(\"si_figure_s08_d_frame_validity.png\"))\n",
237
+ "plt.show()"
238
+ ]
239
+ }
240
+ ],
241
+ "metadata": {
242
+ "language_info": {
243
+ "name": "python"
244
+ }
245
+ },
246
+ "nbformat": 4,
247
+ "nbformat_minor": 5
248
+ }
analysis/si_figures/si_figure_s15.ipynb ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "a8256e57",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Figure S15: per-solute RMSE with delta-22 composite coefficients applied\n",
9
+ "\n",
10
+ "Per-solute ¹H and ¹³C RMSE for the natural-products / olefin-isomer test set with delta-22\n",
11
+ "coefficients applied (reproduces Figure 5C in ¹H, plus the ¹³C analogue), plus fitting-RMSE\n",
12
+ "comparisons across coefficient choices, feature-space coverage by solvent, delta-22-plane residuals,\n",
13
+ "and the RMSE distribution shift by solvent."
14
+ ]
15
+ },
16
+ {
17
+ "cell_type": "code",
18
+ "execution_count": null,
19
+ "id": "64581ad1",
20
+ "metadata": {},
21
+ "outputs": [],
22
+ "source": [
23
+ "import os, sys\n",
24
+ "\n",
25
+ "# make the in-repo modules importable (not pip-installed)\n",
26
+ "REPO = os.path.abspath(\"../..\")\n",
27
+ "for _p in (\"data/applications\", \"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
28
+ " sys.path.insert(0, os.path.join(REPO, _p))"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "execution_count": null,
34
+ "id": "206af146",
35
+ "metadata": {},
36
+ "outputs": [],
37
+ "source": [
38
+ "import matplotlib.pyplot as plt\n",
39
+ "\n",
40
+ "from applications_reader import Applications\n",
41
+ "import applications\n",
42
+ "import applications_plots\n",
43
+ "import delta22\n",
44
+ "import paths"
45
+ ]
46
+ },
47
+ {
48
+ "cell_type": "code",
49
+ "execution_count": null,
50
+ "id": "69c8b5ee",
51
+ "metadata": {},
52
+ "outputs": [],
53
+ "source": [
54
+ "DATA = os.path.join(REPO, \"data\", \"applications\")\n",
55
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
56
+ "DELTA22_XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
57
+ "\n",
58
+ "def figure_path(name):\n",
59
+ " os.makedirs(\"figures\", exist_ok=True)\n",
60
+ " return os.path.join(\"figures\", name)"
61
+ ]
62
+ },
63
+ {
64
+ "cell_type": "code",
65
+ "execution_count": null,
66
+ "id": "9cde73dc",
67
+ "metadata": {},
68
+ "outputs": [],
69
+ "source": [
70
+ "loader = Applications(paths.dataset_file(\"applications\", root=REPO),\n",
71
+ " os.path.join(DATA, \"applications_experimental.xlsx\"))\n",
72
+ "\n",
73
+ "# assemble the feature table and run the composite-model fits + bootstrap\n",
74
+ "query_df_nn = applications.build_query_df_nn(loader)\n",
75
+ "site_counts = (query_df_nn.drop_duplicates(subset=[\"solute\", \"nucleus\", \"site\"])\n",
76
+ " .groupby([\"solute\", \"nucleus\"]).size().unstack(fill_value=0))\n",
77
+ "\n",
78
+ "per_solute = applications.per_solute_fits(query_df_nn) # \"scaled to solute\" baseline\n",
79
+ "all_solute = applications.per_solvent_fits(query_df_nn) # \"scaled to test set\" full fit\n",
80
+ "\n",
81
+ "seed = applications.build_bootstrap_seed_coeffs(loader)\n",
82
+ "bootstrap_rmses = {}\n",
83
+ "for nuc in [\"H\", \"C\"]:\n",
84
+ " preds = applications.apply_bootstrap_params_to_full_dataset(query_df_nn, seed[nuc], nucleus=nuc)\n",
85
+ " bootstrap_rmses[nuc] = applications.compute_solute_rmses(preds)"
86
+ ]
87
+ },
88
+ {
89
+ "cell_type": "code",
90
+ "execution_count": null,
91
+ "id": "d218de52",
92
+ "metadata": {},
93
+ "outputs": [],
94
+ "source": [
95
+ "# published solute order (isomers, then the two pyridines, then the natural products); matches\n",
96
+ "# main-text Figure 5D. delta-22 is drawn first by the box-plot engine, so it is not listed here.\n",
97
+ "S15_SOLUTE_ORDER = [\"isomer_1E\", \"isomer_1Z\", \"isomer_2E\", \"isomer_2Z\", \"isomer_3E\", \"isomer_3Z\",\n",
98
+ " \"isomer_4N\", \"isomer_4O\", \"vomicine\", \"prednisone\", \"peptide\", \"flavone\",\n",
99
+ " \"dihydrotanshinone_I\"]"
100
+ ]
101
+ },
102
+ {
103
+ "cell_type": "code",
104
+ "execution_count": null,
105
+ "id": "85097f2d",
106
+ "metadata": {},
107
+ "outputs": [],
108
+ "source": [
109
+ "# 1H panel (reproduces Figure 5C)\n",
110
+ "applications_plots.plot_nps_on_boxplot_delta22_simplified(\n",
111
+ " loader.rmse_distribution(\"H\"), bootstrap_rmses[\"H\"], per_solute[\"H\"],\n",
112
+ " nucleus=\"H\", formulas=[\"stationary_plus_qcd + openMM\"], colors=[\"#61a89a\"],\n",
113
+ " site_counts=site_counts, formula_remap=applications.FORMULA_REMAP,\n",
114
+ " solute_remap=applications.SOLUTE_DISPLAY, solute_order=S15_SOLUTE_ORDER,\n",
115
+ " solute_color_remap=applications.PEPTIDE_HIGHLIGHT_H,\n",
116
+ " figsize=(14, 8), box_width=0.20, box_gap=0.1,\n",
117
+ " show_baseline=True, baseline_annotation_text=\"Scaled to solute\",\n",
118
+ " baseline_annotation_x=0.215, baseline_annotation_y=0.39,\n",
119
+ " show_full_fit_line=True, full_fit_line_label=\"Scaled to Test Set\", full_fit_line_label_x=0.835,\n",
120
+ " all_solute_fitting_results=all_solute,\n",
121
+ " max_bar_height=0.06, site_count_axis_mode=\"inset\", site_count_inset_area_frac=0.1,\n",
122
+ " site_count_inset_axis_offset=-0.06, site_count_inset_axis_label_pad=0.045,\n",
123
+ " save_path=figure_path(\"si_figure_s15_1H.png\"),\n",
124
+ ")"
125
+ ]
126
+ },
127
+ {
128
+ "cell_type": "code",
129
+ "execution_count": null,
130
+ "id": "973af7b2",
131
+ "metadata": {},
132
+ "outputs": [],
133
+ "source": [
134
+ "# 13C panel\n",
135
+ "applications_plots.plot_nps_on_boxplot_delta22_simplified(\n",
136
+ " loader.rmse_distribution(\"C\"), bootstrap_rmses[\"C\"], per_solute[\"C\"],\n",
137
+ " nucleus=\"C\", formulas=[\"stationary_plus_op_vib + openMM\"], colors=[\"#61a89a\"],\n",
138
+ " site_counts=site_counts, formula_remap=applications.FORMULA_REMAP,\n",
139
+ " solute_remap=applications.SOLUTE_DISPLAY, solute_order=S15_SOLUTE_ORDER,\n",
140
+ " solute_color_remap=applications.PEPTIDE_HIGHLIGHT_C,\n",
141
+ " figsize=(14, 8), box_width=0.20, box_gap=0.1,\n",
142
+ " show_baseline=True, baseline_annotation_text=\"Scaled to solute\",\n",
143
+ " baseline_annotation_x=0.28, baseline_annotation_y=0.355,\n",
144
+ " show_full_fit_line=True, full_fit_line_label=\"Scaled to Test Set\", full_fit_line_label_x=0.98,\n",
145
+ " all_solute_fitting_results=all_solute,\n",
146
+ " max_bar_height=0.8, site_count_axis_mode=\"inset\", site_count_inset_area_frac=0.1,\n",
147
+ " site_count_inset_axis_offset=-0.06, site_count_inset_axis_label_pad=0.045,\n",
148
+ " save_path=figure_path(\"si_figure_s15_13C.png\"),\n",
149
+ ")"
150
+ ]
151
+ },
152
+ {
153
+ "cell_type": "markdown",
154
+ "id": "f079c075",
155
+ "metadata": {},
156
+ "source": [
157
+ "## Fitting RMSE Comparisons (chloroform)\n",
158
+ "\n",
159
+ "Per-solute RMSE under three coefficient choices (Scaled to Solute / Scaled to Test Set / Extrapolated\n",
160
+ "from delta22)."
161
+ ]
162
+ },
163
+ {
164
+ "cell_type": "code",
165
+ "execution_count": null,
166
+ "id": "11ea3200",
167
+ "metadata": {},
168
+ "outputs": [],
169
+ "source": [
170
+ "for nucleus, formula in [(\"H\", \"stationary_plus_qcd + openMM\"), (\"C\", \"stationary_plus_op_vib + openMM\")]:\n",
171
+ " table = applications.fitting_rmse_comparison_table(query_df_nn, per_solute, all_solute, bootstrap_rmses[nucleus],\n",
172
+ " nucleus, \"chloroform\", formula)\n",
173
+ " table = table.rename(index=applications.SOLUTE_DISPLAY)\n",
174
+ " order = [applications.SOLUTE_DISPLAY.get(k, k) for k in S15_SOLUTE_ORDER] # published order; peptide has no chloroform data\n",
175
+ " table = table.reindex([n for n in order if n in table.index])\n",
176
+ " print(f\"--- {nucleus} ---\")\n",
177
+ " display(table.round(3))\n",
178
+ " applications_plots.plot_fitting_rmse_comparison_bars(\n",
179
+ " table, nucleus, \"chloroform\",\n",
180
+ " save_path=figure_path(f\"si_figure_s15_fitting_rmse_comparisons_{'1H' if nucleus == 'H' else '13C'}.png\"))\n",
181
+ "plt.show()"
182
+ ]
183
+ },
184
+ {
185
+ "cell_type": "markdown",
186
+ "id": "47773c94",
187
+ "metadata": {},
188
+ "source": [
189
+ "## Feature Space Coverage by Solvent\n",
190
+ "\n",
191
+ "Test-set vs delta-22 mean-centered feature values across the four explicit-solvent solvents."
192
+ ]
193
+ },
194
+ {
195
+ "cell_type": "code",
196
+ "execution_count": null,
197
+ "id": "fa1de7fe",
198
+ "metadata": {},
199
+ "outputs": [],
200
+ "source": [
201
+ "delta22_query_df_nn = delta22.add_composite_columns(delta22.load_query_df_nn(\n",
202
+ " DELTA22_HDF5, DELTA22_XLSX, verbose=False))"
203
+ ]
204
+ },
205
+ {
206
+ "cell_type": "code",
207
+ "execution_count": null,
208
+ "id": "a19b095b",
209
+ "metadata": {},
210
+ "outputs": [],
211
+ "source": [
212
+ "_FEATURE_X_LABELS = {\n",
213
+ " \"H\": \"Gas-Phase Shielding + QCD Correction (centered, ppm)\",\n",
214
+ " \"C\": \"Gas-Phase Shielding + OpenMM Vibrational Correction (centered, ppm)\",\n",
215
+ "}\n",
216
+ "for nucleus in [\"H\", \"C\"]:\n",
217
+ " coverage = applications.feature_space_coverage_table(query_df_nn, delta22_query_df_nn, nucleus)\n",
218
+ " applications_plots.plot_feature_space_coverage_grid(\n",
219
+ " coverage, nucleus, _FEATURE_X_LABELS[nucleus],\n",
220
+ " save_path=figure_path(f\"si_figure_s15_feature_space_coverage_{'1H' if nucleus == 'H' else '13C'}.png\"))\n",
221
+ "plt.show()"
222
+ ]
223
+ },
224
+ {
225
+ "cell_type": "markdown",
226
+ "id": "5d7bc6b3",
227
+ "metadata": {},
228
+ "source": [
229
+ "## Residuals for Delta22 Fitting Coefficients\n",
230
+ "\n",
231
+ "Residuals of a delta-22-only 2-feature plane applied to both delta-22 and the test set, vs\n",
232
+ "experimental shielding."
233
+ ]
234
+ },
235
+ {
236
+ "cell_type": "code",
237
+ "execution_count": null,
238
+ "id": "8dea4de5",
239
+ "metadata": {},
240
+ "outputs": [],
241
+ "source": [
242
+ "for nucleus in [\"H\", \"C\"]:\n",
243
+ " residuals = applications.delta22_plane_residuals_table(query_df_nn, delta22_query_df_nn, nucleus)\n",
244
+ " applications_plots.plot_delta22_plane_residuals_grid(\n",
245
+ " residuals, nucleus,\n",
246
+ " save_path=figure_path(f\"si_figure_s15_delta22_plane_residuals_{'1H' if nucleus == 'H' else '13C'}.png\"))\n",
247
+ "plt.show()"
248
+ ]
249
+ },
250
+ {
251
+ "cell_type": "markdown",
252
+ "id": "3fd43b43",
253
+ "metadata": {},
254
+ "source": [
255
+ "## Distribution Shift by Solvent\n",
256
+ "\n",
257
+ "Per-solvent mean test-set RMSE under the three coefficient choices."
258
+ ]
259
+ },
260
+ {
261
+ "cell_type": "code",
262
+ "execution_count": null,
263
+ "id": "24d4b26e",
264
+ "metadata": {},
265
+ "outputs": [],
266
+ "source": [
267
+ "for nucleus, formula in [(\"H\", \"stationary_plus_qcd + openMM\"), (\"C\", \"stationary_plus_op_vib + openMM\")]:\n",
268
+ " shift = applications.distribution_shift_by_solvent_table(per_solute, all_solute, bootstrap_rmses[nucleus],\n",
269
+ " nucleus, formula)\n",
270
+ " print(f\"--- {nucleus} ---\")\n",
271
+ " display(shift.round(3))\n",
272
+ " applications_plots.plot_distribution_shift_by_solvent_bars(\n",
273
+ " shift, nucleus,\n",
274
+ " save_path=figure_path(f\"si_figure_s15_distribution_shift_by_solvent_{'1H' if nucleus == 'H' else '13C'}.png\"))\n",
275
+ "plt.show()"
276
+ ]
277
+ }
278
+ ],
279
+ "metadata": {
280
+ "language_info": {
281
+ "name": "python"
282
+ }
283
+ },
284
+ "nbformat": 4,
285
+ "nbformat_minor": 5
286
+ }
analysis/si_figures/si_figure_s16_s18.ipynb ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "36541c20",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Figures S16-S18: predicted vs experimental solvent-induced shifts by reference solvent\n",
9
+ "\n",
10
+ "Per solvent, implicit (PCM) and explicit (Desmond) predicted solvent-induced shift differences vs\n",
11
+ "measured (y=x ideal), for three reference solvents: **S16** chloroform, **S17** benzene, **S18**\n",
12
+ "solvent-averaged. One panel per solvent, each point a proton site."
13
+ ]
14
+ },
15
+ {
16
+ "cell_type": "code",
17
+ "execution_count": null,
18
+ "id": "8464e639",
19
+ "metadata": {},
20
+ "outputs": [],
21
+ "source": [
22
+ "import os, sys\n",
23
+ "\n",
24
+ "# make the in-repo modules importable (not pip-installed)\n",
25
+ "REPO = os.path.abspath(\"../..\")\n",
26
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
27
+ " sys.path.insert(0, os.path.join(REPO, _p))"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "code",
32
+ "execution_count": null,
33
+ "id": "354b6ae2",
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": [
37
+ "import matplotlib.pyplot as plt\n",
38
+ "\n",
39
+ "import delta22\n",
40
+ "import delta22_plots\n",
41
+ "import paths"
42
+ ]
43
+ },
44
+ {
45
+ "cell_type": "code",
46
+ "execution_count": null,
47
+ "id": "1b63be9a",
48
+ "metadata": {},
49
+ "outputs": [],
50
+ "source": [
51
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
52
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
53
+ "\n",
54
+ "def figure_path(name):\n",
55
+ " os.makedirs(\"figures\", exist_ok=True)\n",
56
+ " return os.path.join(\"figures\", name)"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "execution_count": null,
62
+ "id": "710b94a1",
63
+ "metadata": {},
64
+ "outputs": [],
65
+ "source": [
66
+ "# Figure 4 uses one DFT method for the proton sites; differences are taken against a reference\n",
67
+ "# solvent, and the solvent_mean pseudo-solvent is added for the solvent-averaged reference.\n",
68
+ "METHOD, BASIS, GEOM = \"b3lyp_d3bj\", \"pcSseg2\", \"aimnet2\"\n",
69
+ "q = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)\n",
70
+ "one = q[(q[\"sap_nmr_method\"] == METHOD) & (q[\"sap_basis\"] == BASIS) & (q[\"sap_geometry_type\"] == GEOM)]\n",
71
+ "one = delta22.add_solvent_mean(one)\n",
72
+ "print(len(one), \"rows for\", METHOD, BASIS, GEOM)"
73
+ ]
74
+ },
75
+ {
76
+ "cell_type": "code",
77
+ "execution_count": null,
78
+ "id": "94ad7330",
79
+ "metadata": {},
80
+ "outputs": [],
81
+ "source": [
82
+ "FIG4C_LABELS = {\n",
83
+ " \"chloroform\": \"CDCl3\", \"dichloromethane\": \"DCM\", \"tetrahydrofuran\": \"THF\",\n",
84
+ " \"acetonitrile\": \"MeCN\", \"dimethylsulfoxide\": \"DMSO\", \"acetone\": \"acetone\",\n",
85
+ " \"methanol\": \"MeOH\", \"TIP4P\": \"TIP4P\", \"trifluoroethanol\": \"TFE\",\n",
86
+ " \"benzene\": \"benzene\", \"toluene\": \"toluene\", \"chlorobenzene\": \"chlorobenzene\",\n",
87
+ "}"
88
+ ]
89
+ },
90
+ {
91
+ "cell_type": "code",
92
+ "execution_count": null,
93
+ "id": "ecc3f9be",
94
+ "metadata": {},
95
+ "outputs": [],
96
+ "source": [
97
+ "# panel order matches the canonical figures; a different order from delta22.DESMOND_SOLVENTS\n",
98
+ "SI_S16_18_SOLVENT_ORDER = [\n",
99
+ " \"chloroform\", \"tetrahydrofuran\", \"dichloromethane\", \"acetone\", \"acetonitrile\",\n",
100
+ " \"dimethylsulfoxide\", \"trifluoroethanol\", \"methanol\", \"TIP4P\", \"benzene\", \"toluene\", \"chlorobenzene\",\n",
101
+ "]\n",
102
+ "\n",
103
+ "# panel titles use the full solvent name; axis labels use the compact NMR abbreviations (THF, DCM,\n",
104
+ "# DMSO, TFE, CDCl3); the reference is CDCl3 / Benzene / \"Avg Corr.\" for S16 / S17 / S18.\n",
105
+ "TITLE_LABEL = {\"chloroform\": \"Chloroform\", \"tetrahydrofuran\": \"Tetrahydrofuran\",\n",
106
+ " \"dichloromethane\": \"Dichloromethane\", \"acetone\": \"Acetone\", \"acetonitrile\": \"Acetonitrile\",\n",
107
+ " \"dimethylsulfoxide\": \"Dimethylsulfoxide\", \"trifluoroethanol\": \"Trifluoroethanol\",\n",
108
+ " \"methanol\": \"Methanol\", \"TIP4P\": \"TIP4P\", \"benzene\": \"Benzene\", \"toluene\": \"Toluene\",\n",
109
+ " \"chlorobenzene\": \"Chlorobenzene\"}\n",
110
+ "AXIS_LABEL = {**TITLE_LABEL, \"chloroform\": \"CDCl3\", \"tetrahydrofuran\": \"THF\",\n",
111
+ " \"dichloromethane\": \"DCM\", \"dimethylsulfoxide\": \"DMSO\", \"trifluoroethanol\": \"TFE\"}\n",
112
+ "REF_LABEL = {\"chloroform\": \"CDCl3\", \"benzene\": \"Benzene\", \"solvent_mean\": \"Avg Corr.\"}"
113
+ ]
114
+ },
115
+ {
116
+ "cell_type": "code",
117
+ "execution_count": null,
118
+ "id": "dea8a27d",
119
+ "metadata": {},
120
+ "outputs": [],
121
+ "source": [
122
+ "for figure, reference in [(\"S16\", \"chloroform\"), (\"S17\", \"benzene\"), (\"S18\", \"solvent_mean\")]:\n",
123
+ " sh = delta22.solvent_induced_shifts(one, reference, SI_S16_18_SOLVENT_ORDER, nucleus=\"H\", explicit=\"desmond\")\n",
124
+ " implicit_fit = delta22.fit_differences_to_experimental(sh, \"implicit_diff\")\n",
125
+ " explicit_fit = delta22.fit_differences_to_experimental(sh, \"explicit_diff\")\n",
126
+ " print(f\"{figure}: reference={reference:12s} n={explicit_fit['n']:4d} \"\n",
127
+ " f\"implicit fit RMSE={implicit_fit['rmse']:.3f} explicit fit RMSE={explicit_fit['rmse']:.3f}\")\n",
128
+ " panel_solvents = [s for s in SI_S16_18_SOLVENT_ORDER if s != reference]\n",
129
+ " delta22_plots.plot_shift_prediction_scatter_grid(\n",
130
+ " sh, panel_solvents, REF_LABEL[reference], AXIS_LABEL, TITLE_LABEL,\n",
131
+ " save_path=figure_path(f\"si_figure_{figure.lower()}.png\"))\n",
132
+ " plt.show()"
133
+ ]
134
+ }
135
+ ],
136
+ "metadata": {
137
+ "language_info": {
138
+ "name": "python"
139
+ }
140
+ },
141
+ "nbformat": 4,
142
+ "nbformat_minor": 5
143
+ }
analysis/si_tables/si_table_s01_s02_pareto.ipynb ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "0d263621",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Tables S1 and S2: the Pareto plot data\n",
9
+ "\n",
10
+ "Each method's total compute time and CDCl3 test RMSE, for proton (S1) and carbon (S2): a curated\n",
11
+ "55-row subset (MagNET, one AIMNet2-geometry row, and the DFT grid on PBE0/cc-pVTZ geometries)."
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "id": "b29adee9",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "import os, sys\n",
22
+ "\n",
23
+ "# make the in-repo modules importable (not pip-installed)\n",
24
+ "REPO = os.path.abspath(\"../..\")\n",
25
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
26
+ " sys.path.insert(0, os.path.join(REPO, _p))"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "id": "dc1b81aa",
33
+ "metadata": {},
34
+ "outputs": [],
35
+ "source": [
36
+ "import numpy as np\n",
37
+ "import pandas as pd\n",
38
+ "\n",
39
+ "import delta22\n",
40
+ "import paths"
41
+ ]
42
+ },
43
+ {
44
+ "cell_type": "code",
45
+ "execution_count": null,
46
+ "id": "1045389a",
47
+ "metadata": {},
48
+ "outputs": [],
49
+ "source": [
50
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
51
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
52
+ "\n",
53
+ "def document_path(name):\n",
54
+ " os.makedirs(\"documents\", exist_ok=True)\n",
55
+ " return os.path.join(\"documents\", name)"
56
+ ]
57
+ },
58
+ {
59
+ "cell_type": "code",
60
+ "execution_count": null,
61
+ "id": "8673cb6b",
62
+ "metadata": {},
63
+ "outputs": [],
64
+ "source": [
65
+ "# Load the flat DFT and MagNET tables and the timing tables.\n",
66
+ "dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)\n",
67
+ "nn = delta22.load_query_df_nn(DELTA22_HDF5, XLSX, verbose=False)\n",
68
+ "dft_gas_timings, nn_timings = delta22.load_pareto_timings(DELTA22_HDF5)\n",
69
+ "\n",
70
+ "# One point per method/basis/geometry/nucleus/solvent, plus the solvent-averaged rows, each with its\n",
71
+ "# mean test RMSE and total compute time. MagNET appears as a single method (aimnet2, basis \"N/A\").\n",
72
+ "# Figure 2A uses 100 seeded splits, not the 250 the other delta-22 panels use, and trains on the\n",
73
+ "# first 10 shuffled solutes / tests on the rest; fig2a_pareto_points handles that split convention.\n",
74
+ "PARETO_N_SPLITS = 100\n",
75
+ "points = delta22.fig2a_pareto_points(dft, nn, dft_gas_timings, nn_timings, n_splits=PARETO_N_SPLITS)\n",
76
+ "print(points[\"nmr_method\"].nunique(), \"methods;\",\n",
77
+ " \"MagNET total time\", float(points.query(\"nmr_method=='MagNET'\")[\"total_time\"].iloc[0]), \"s\")"
78
+ ]
79
+ },
80
+ {
81
+ "cell_type": "markdown",
82
+ "id": "19d525a3",
83
+ "metadata": {},
84
+ "source": [
85
+ "## Tables S1 and S2"
86
+ ]
87
+ },
88
+ {
89
+ "cell_type": "code",
90
+ "execution_count": null,
91
+ "id": "d3cb7258",
92
+ "metadata": {},
93
+ "outputs": [],
94
+ "source": [
95
+ "# full column set matching the SI tables: identity + fitting RMSE + the three time components and\n",
96
+ "# their log10 (all solvents here are chloroform). geometry_time and nmr_time sum to total_time.\n",
97
+ "COLUMNS = [\"geometry_type\", \"nmr_method\", \"basis\", \"solvent\", \"fitting_RMSE\",\n",
98
+ " \"geometry_time\", \"nmr_time\", \"total_time\", \"log10_total_time\"]\n",
99
+ "\n",
100
+ "curated = {}\n",
101
+ "for nucleus, label in [(\"H\", \"S1\"), (\"C\", \"S2\")]:\n",
102
+ " sub = delta22.pareto_table_curated(points, nucleus).copy()\n",
103
+ " sub[\"log10_total_time\"] = np.log10(sub[\"total_time\"])\n",
104
+ " sub = sub[COLUMNS]\n",
105
+ " curated[label] = sub\n",
106
+ " print(f\"Table {label} ({nucleus}): {len(sub)} rows x {sub.shape[1]} columns\")\n",
107
+ " print(sub.to_string(index=False), \"\\n\")\n",
108
+ "\n",
109
+ "# write the two tables to this notebook's documents/ folder, one sheet per SI table\n",
110
+ "out = document_path(\"si_table_s01_s02_pareto.xlsx\")\n",
111
+ "with pd.ExcelWriter(out) as writer:\n",
112
+ " curated[\"S1\"].to_excel(writer, sheet_name=\"Table S1 (1H)\", index=False)\n",
113
+ " curated[\"S2\"].to_excel(writer, sheet_name=\"Table S2 (13C)\", index=False)\n",
114
+ "print(\"wrote\", os.path.relpath(out, REPO))"
115
+ ]
116
+ }
117
+ ],
118
+ "metadata": {
119
+ "language_info": {
120
+ "name": "python"
121
+ }
122
+ },
123
+ "nbformat": 4,
124
+ "nbformat_minor": 5
125
+ }
analysis/si_tables/si_table_s03_s04_performance.ipynb ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "de02451b",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Tables S3 and S4: MagNET performance across geometries\n",
9
+ "\n",
10
+ "How accurately MagNET reproduces its DFT training reference (PBE0/pcSseg-1) as geometries move from\n",
11
+ "stationary to vibrated to solvated, for the foundation model and the chloroform MagNET-x."
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "id": "b6531dbc",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "import os, sys\n",
22
+ "\n",
23
+ "# make the in-repo modules importable (not pip-installed)\n",
24
+ "REPO = os.path.abspath(\"../..\")\n",
25
+ "for _p in (\"data/magnet_test_predictions\", \"analysis/code\", \"analysis/code/shared\"):\n",
26
+ " sys.path.insert(0, os.path.join(REPO, _p))"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "id": "2ab667bf",
33
+ "metadata": {},
34
+ "outputs": [],
35
+ "source": [
36
+ "import pandas as pd\n",
37
+ "import magnet_test_predictions_reader\n",
38
+ "import magnet_benchmark\n",
39
+ "import paths"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "code",
44
+ "execution_count": null,
45
+ "id": "979ab00c",
46
+ "metadata": {},
47
+ "outputs": [],
48
+ "source": [
49
+ "PREDICTIONS = paths.dataset_file(\"magnet_test_predictions\", root=REPO)\n",
50
+ "\n",
51
+ "def document_path(name):\n",
52
+ " # table/spreadsheet outputs go under this notebook's documents/ folder, created on first save\n",
53
+ " os.makedirs(\"documents\", exist_ok=True)\n",
54
+ " return os.path.join(\"documents\", name)"
55
+ ]
56
+ },
57
+ {
58
+ "cell_type": "code",
59
+ "execution_count": null,
60
+ "id": "059bba13",
61
+ "metadata": {},
62
+ "outputs": [],
63
+ "source": [
64
+ "rows = magnet_benchmark.exact_stats_table(PREDICTIONS, magnet_test_predictions_reader)\n",
65
+ "res = pd.DataFrame(rows).set_index([\"nucleus\", \"model\", \"test_set\"])\n",
66
+ "\n",
67
+ "table_rows = []\n",
68
+ "for nucleus in (\"1H\", \"13C\"):\n",
69
+ " for model in magnet_benchmark.MODELS:\n",
70
+ " for ts in magnet_benchmark.TEST_SETS:\n",
71
+ " r = res.loc[(nucleus, model, ts)]\n",
72
+ " pmed, pmae, prmse = magnet_benchmark.PUBLISHED[nucleus][(model, ts)]\n",
73
+ " table_rows.append(dict(nucleus=nucleus, model=model, test_set=ts, n=int(r.n),\n",
74
+ " median_repro=round(r.median_ae, 6), median_SI=round(pmed, 6),\n",
75
+ " mae_repro=round(r.mae, 6), mae_SI=round(pmae, 6),\n",
76
+ " rmse_repro=round(r.rmse, 5), rmse_SI=round(prmse, 5)))\n",
77
+ "table = pd.DataFrame(table_rows)\n",
78
+ "table_s3 = table[table.nucleus == \"1H\"].drop(columns=\"nucleus\")\n",
79
+ "table_s4 = table[table.nucleus == \"13C\"].drop(columns=\"nucleus\")\n",
80
+ "print(\"Table S3 (1H):\"); display(table_s3)\n",
81
+ "print(\"Table S4 (13C):\"); display(table_s4)\n",
82
+ "\n",
83
+ "# write the reproduced tables to this notebook's documents/ folder, one sheet per SI table\n",
84
+ "out = document_path(\"si_table_s03_s04_performance.xlsx\")\n",
85
+ "with pd.ExcelWriter(out) as writer:\n",
86
+ " table_s3.to_excel(writer, sheet_name=\"Table S3 (1H)\", index=False)\n",
87
+ " table_s4.to_excel(writer, sheet_name=\"Table S4 (13C)\", index=False)\n",
88
+ "print(\"wrote\", os.path.relpath(out, REPO))"
89
+ ]
90
+ },
91
+ {
92
+ "cell_type": "markdown",
93
+ "id": "e5d795f1",
94
+ "metadata": {},
95
+ "source": [
96
+ "## Exact-reproduction check"
97
+ ]
98
+ },
99
+ {
100
+ "cell_type": "code",
101
+ "execution_count": null,
102
+ "id": "fdb61e94",
103
+ "metadata": {},
104
+ "outputs": [],
105
+ "source": [
106
+ "# every row's reproduced median/MAE/RMSE should match the published SI value to a few parts per million\n",
107
+ "max_dev = (table[[\"median_repro\", \"median_SI\"]].diff(axis=1).iloc[:, -1].abs().max(),\n",
108
+ " table[[\"mae_repro\", \"mae_SI\"]].diff(axis=1).iloc[:, -1].abs().max(),\n",
109
+ " table[[\"rmse_repro\", \"rmse_SI\"]].diff(axis=1).iloc[:, -1].abs().max())\n",
110
+ "print(\"largest reproduced-vs-published deviation (median, MAE, RMSE):\", max_dev)\n",
111
+ "assert max(max_dev) < 1e-3, \"a row diverged from the SI by more than float rounding\""
112
+ ]
113
+ }
114
+ ],
115
+ "metadata": {
116
+ "language_info": {
117
+ "name": "python"
118
+ }
119
+ },
120
+ "nbformat": 4,
121
+ "nbformat_minor": 5
122
+ }
analysis/si_tables/si_table_s05_qcd.ipynb ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "8a468c14",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Table S5: Performance statistics for predicting QCD corrections\n",
9
+ "\n",
10
+ "Foundation MagNET's accuracy at predicting the rovibrational (QCD) correction (stationary vs\n",
11
+ "trajectory-averaged shielding) over qcdtraj2500 (2500 molecules), ¹H and ¹³C, all shieldings at\n",
12
+ "PBE0/pcSseg-1."
13
+ ]
14
+ },
15
+ {
16
+ "cell_type": "code",
17
+ "execution_count": null,
18
+ "id": "aca3e24b",
19
+ "metadata": {},
20
+ "outputs": [],
21
+ "source": [
22
+ "import os, sys\n",
23
+ "\n",
24
+ "# make the in-repo modules importable (not pip-installed)\n",
25
+ "REPO = os.path.abspath(\"../..\")\n",
26
+ "for _p in (\"data/magnet_test_predictions\", \"analysis/code\", \"analysis/code/shared\"):\n",
27
+ " sys.path.insert(0, os.path.join(REPO, _p))"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "code",
32
+ "execution_count": null,
33
+ "id": "bbdcbe1d",
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": [
37
+ "import pandas as pd\n",
38
+ "import magnet_test_predictions_reader\n",
39
+ "import magnet_benchmark\n",
40
+ "import paths"
41
+ ]
42
+ },
43
+ {
44
+ "cell_type": "code",
45
+ "execution_count": null,
46
+ "id": "c98484b1",
47
+ "metadata": {},
48
+ "outputs": [],
49
+ "source": [
50
+ "PREDICTIONS = paths.dataset_file(\"magnet_test_predictions\", root=REPO)\n",
51
+ "\n",
52
+ "def document_path(name):\n",
53
+ " # table/spreadsheet outputs go under this notebook's documents/ folder, created on first save\n",
54
+ " os.makedirs(\"documents\", exist_ok=True)\n",
55
+ " return os.path.join(\"documents\", name)"
56
+ ]
57
+ },
58
+ {
59
+ "cell_type": "code",
60
+ "execution_count": null,
61
+ "id": "da4e04e1",
62
+ "metadata": {},
63
+ "outputs": [],
64
+ "source": [
65
+ "rows = magnet_benchmark.qcd_stats_table(PREDICTIONS, magnet_test_predictions_reader)\n",
66
+ "table_rows = []\n",
67
+ "for r in rows:\n",
68
+ " nucleus = r[\"model\"].split(\"(\")[1].rstrip(\")\") # \"1H\" / \"13C\"\n",
69
+ " pmed, pmae, prmse = magnet_benchmark.PUBLISHED_S5[nucleus]\n",
70
+ " table_rows.append(dict(model=r[\"model\"], n=int(r[\"n\"]),\n",
71
+ " median_repro=round(r[\"median_ae\"], 8), median_SI=round(pmed, 8),\n",
72
+ " mae_repro=round(r[\"mae\"], 8), mae_SI=round(pmae, 8),\n",
73
+ " rmse_repro=round(r[\"rmse\"], 8), rmse_SI=round(prmse, 8)))\n",
74
+ "table_s5 = pd.DataFrame(table_rows)\n",
75
+ "print(\"Table S5 (QCD corrections):\"); display(table_s5)\n",
76
+ "\n",
77
+ "# write the reproduced table to this notebook's documents/ folder\n",
78
+ "out = document_path(\"si_table_s05_qcd.xlsx\")\n",
79
+ "with pd.ExcelWriter(out) as writer:\n",
80
+ " table_s5.to_excel(writer, sheet_name=\"Table S5\", index=False)\n",
81
+ "print(\"wrote\", os.path.relpath(out, REPO))"
82
+ ]
83
+ },
84
+ {
85
+ "cell_type": "markdown",
86
+ "id": "9971c179",
87
+ "metadata": {},
88
+ "source": [
89
+ "## Exact-reproduction check"
90
+ ]
91
+ },
92
+ {
93
+ "cell_type": "code",
94
+ "execution_count": null,
95
+ "id": "b2c792c0",
96
+ "metadata": {},
97
+ "outputs": [],
98
+ "source": [
99
+ "# every reproduced median/MAE/RMSE should match the published SI value to a few parts per million\n",
100
+ "dev = max((table_s5[[\"median_repro\", \"median_SI\"]].diff(axis=1).iloc[:, -1].abs().max(),\n",
101
+ " table_s5[[\"mae_repro\", \"mae_SI\"]].diff(axis=1).iloc[:, -1].abs().max(),\n",
102
+ " table_s5[[\"rmse_repro\", \"rmse_SI\"]].diff(axis=1).iloc[:, -1].abs().max()))\n",
103
+ "print(\"largest reproduced-vs-published deviation:\", dev)\n",
104
+ "assert dev < 1e-3, \"a row diverged from the SI by more than float rounding\""
105
+ ]
106
+ }
107
+ ],
108
+ "metadata": {
109
+ "language_info": {
110
+ "name": "python"
111
+ }
112
+ },
113
+ "nbformat": 4,
114
+ "nbformat_minor": 5
115
+ }
analysis/si_tables/si_table_s06_solvent_corrections.ipynb ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "aa1b7ba9",
6
+ "metadata": {},
7
+ "source": [
8
+ "# SI Table S6: per-solvent implicit vs explicit fit RMSE (chloroform reference, ¹H)\n",
9
+ "\n",
10
+ "Per solvent (chloroform reference), fit RMSE of experimental solvent-induced ¹H shifts using implicit\n",
11
+ "(PCM) vs explicit (Desmond), and the percent improvement."
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "id": "f812183a",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "import os, sys\n",
22
+ "\n",
23
+ "# make the in-repo modules importable (not pip-installed)\n",
24
+ "REPO = os.path.abspath(\"../..\")\n",
25
+ "for _p in (\"data/delta22\", \"analysis/code\", \"analysis/code/shared\"):\n",
26
+ " sys.path.insert(0, os.path.join(REPO, _p))"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "id": "f643aecf",
33
+ "metadata": {},
34
+ "outputs": [],
35
+ "source": [
36
+ "import pandas as pd\n",
37
+ "\n",
38
+ "import delta22\n",
39
+ "import paths"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "code",
44
+ "execution_count": null,
45
+ "id": "cfa3b9d1",
46
+ "metadata": {},
47
+ "outputs": [],
48
+ "source": [
49
+ "DELTA22_HDF5 = paths.dataset_file(\"delta22\", root=REPO)\n",
50
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
51
+ "\n",
52
+ "def document_path(name):\n",
53
+ " os.makedirs(\"documents\", exist_ok=True)\n",
54
+ " return os.path.join(\"documents\", name)"
55
+ ]
56
+ },
57
+ {
58
+ "cell_type": "code",
59
+ "execution_count": null,
60
+ "id": "130a1bdc",
61
+ "metadata": {},
62
+ "outputs": [],
63
+ "source": [
64
+ "# Figure 4 uses one DFT method for the proton sites; differences are taken against a reference\n",
65
+ "# solvent, and the solvent_mean pseudo-solvent is added for the solvent-averaged reference.\n",
66
+ "METHOD, BASIS, GEOM = \"b3lyp_d3bj\", \"pcSseg2\", \"aimnet2\"\n",
67
+ "q = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)\n",
68
+ "one = q[(q[\"sap_nmr_method\"] == METHOD) & (q[\"sap_basis\"] == BASIS) & (q[\"sap_geometry_type\"] == GEOM)]\n",
69
+ "one = delta22.add_solvent_mean(one)\n",
70
+ "print(len(one), \"rows for\", METHOD, BASIS, GEOM)"
71
+ ]
72
+ },
73
+ {
74
+ "cell_type": "markdown",
75
+ "id": "fbf8e51b",
76
+ "metadata": {},
77
+ "source": [
78
+ "## Table S6: per-solvent implicit vs explicit fit RMSE (chloroform reference, 1H)"
79
+ ]
80
+ },
81
+ {
82
+ "cell_type": "code",
83
+ "execution_count": null,
84
+ "id": "d80ea495",
85
+ "metadata": {},
86
+ "outputs": [],
87
+ "source": [
88
+ "rows = []\n",
89
+ "for solvent in ['tetrahydrofuran', 'dichloromethane', 'acetone', 'acetonitrile', 'dimethylsulfoxide', 'trifluoroethanol', 'methanol', 'TIP4P', 'benzene', 'toluene', 'chlorobenzene']:\n",
90
+ " sp = delta22.solvent_pair_differences(one, solvent, \"chloroform\", nucleus=\"H\", explicit=\"desmond\")\n",
91
+ " implicit = delta22.fit_differences_to_experimental(sp, \"implicit_diff\")[\"rmse\"]\n",
92
+ " explicit = delta22.fit_differences_to_experimental(sp, \"explicit_diff\")[\"rmse\"]\n",
93
+ " rows.append({\"solvent\": solvent,\n",
94
+ " \"implicit_fitting_rmse\": round(implicit, 4),\n",
95
+ " \"explicit_fitting_rmse\": round(explicit, 4),\n",
96
+ " \"explicit_improvement_pct\": round(100.0 * (implicit - explicit) / implicit, 2)})\n",
97
+ "table_s6 = pd.DataFrame(rows)\n",
98
+ "print(table_s6.to_string(index=False))\n",
99
+ "\n",
100
+ "# write the table to this notebook's documents/ folder\n",
101
+ "out = document_path(\"si_table_s06_solvent_corrections.xlsx\")\n",
102
+ "with pd.ExcelWriter(out) as writer:\n",
103
+ " table_s6.to_excel(writer, sheet_name=\"Table S6\", index=False)\n",
104
+ "print(\"wrote\", os.path.relpath(out, REPO))"
105
+ ]
106
+ }
107
+ ],
108
+ "metadata": {
109
+ "language_info": {
110
+ "name": "python"
111
+ }
112
+ },
113
+ "nbformat": 4,
114
+ "nbformat_minor": 5
115
+ }
analysis/si_tables/si_table_s08_summary.ipynb ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "dce84895",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Table S8: Dataset Summary Statistics\n",
9
+ "\n",
10
+ "Molecule and ¹H/¹³C site counts for each training dataset (site counts from each HDF5's\n",
11
+ "`atomic_numbers`, ¹H=1/¹³C=6; MagNET-Zero combines both sigma-pepper rounds with sigma-concentrate)."
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "id": "df321bd6",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "import os, sys\n",
22
+ "\n",
23
+ "# make the in-repo modules importable (not pip-installed)\n",
24
+ "REPO = os.path.abspath(\"../..\")\n",
25
+ "for _p in (\"analysis/code\", \"analysis/code/shared\"):\n",
26
+ " sys.path.insert(0, os.path.join(REPO, _p))"
27
+ ]
28
+ },
29
+ {
30
+ "cell_type": "code",
31
+ "execution_count": null,
32
+ "id": "88710ad5",
33
+ "metadata": {},
34
+ "outputs": [],
35
+ "source": [
36
+ "import pandas as pd\n",
37
+ "import dataset_summary"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "id": "84e5a441",
44
+ "metadata": {},
45
+ "outputs": [],
46
+ "source": [
47
+ "DATA_DIR = os.path.join(REPO, \"data\")\n",
48
+ "\n",
49
+ "def document_path(name):\n",
50
+ " os.makedirs(\"documents\", exist_ok=True)\n",
51
+ " return os.path.join(\"documents\", name)"
52
+ ]
53
+ },
54
+ {
55
+ "cell_type": "code",
56
+ "execution_count": null,
57
+ "id": "6a606891",
58
+ "metadata": {},
59
+ "outputs": [],
60
+ "source": [
61
+ "table_s8 = dataset_summary.summary_table(DATA_DIR)\n",
62
+ "display(table_s8)\n",
63
+ "\n",
64
+ "# write the table to this notebook's documents/ folder\n",
65
+ "out = document_path(\"si_table_s08_summary.xlsx\")\n",
66
+ "with pd.ExcelWriter(out) as writer:\n",
67
+ " table_s8.to_excel(writer, sheet_name=\"Table S8\", index=False)\n",
68
+ "print(\"wrote\", os.path.relpath(out, REPO))"
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "markdown",
73
+ "id": "3bf67791",
74
+ "metadata": {},
75
+ "source": [
76
+ "## Exact-reproduction check"
77
+ ]
78
+ },
79
+ {
80
+ "cell_type": "code",
81
+ "execution_count": null,
82
+ "id": "4d5f4416",
83
+ "metadata": {},
84
+ "outputs": [],
85
+ "source": [
86
+ "# every count should match the published SI Table S8 value exactly\n",
87
+ "for _, row in table_s8.iterrows():\n",
88
+ " pub = dataset_summary.PUBLISHED_S8[row[\"dataset\"]]\n",
89
+ " got = (row[\"molecules\"], row[\"n_1H_sites\"], row[\"n_13C_sites\"])\n",
90
+ " assert got == pub, f\"{row['dataset']}: {got} != published {pub}\"\n",
91
+ "print(\"all rows match the published SI Table S8 exactly\")"
92
+ ]
93
+ }
94
+ ],
95
+ "metadata": {
96
+ "language_info": {
97
+ "name": "python"
98
+ }
99
+ },
100
+ "nbformat": 4,
101
+ "nbformat_minor": 5
102
+ }
analysis/si_tables/si_table_s10_s11_scaling.ipynb ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "badbc639",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Tables S10 and S11: MagNET-Zero/MagNET-PCM scaling factors\n",
9
+ "\n",
10
+ "Per-solvent linear coefficients (intercept, stationary, pcm) mapping MagNET-Zero shieldings plus a\n",
11
+ "MagNET-PCM correction to predicted shifts: ¹H (S10) and ¹³C (S11)."
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "id": "672ff802",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "import os, sys\n",
22
+ "\n",
23
+ "# make the in-repo modules importable (not pip-installed)\n",
24
+ "REPO = os.path.abspath(\"../..\")\n",
25
+ "for _p in (\"data/delta22\", \"data/scaling_factors\", \"data/applications\",\n",
26
+ " \"analysis/code\", \"analysis/code/shared\"):\n",
27
+ " sys.path.insert(0, os.path.join(REPO, _p))"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "code",
32
+ "execution_count": null,
33
+ "id": "1a1fa320",
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": [
37
+ "import numpy as np\n",
38
+ "import pandas as pd\n",
39
+ "import scaling_factors\n",
40
+ "import scaling_factors_reader\n",
41
+ "import build_composite_model\n",
42
+ "import paths\n",
43
+ "from applications_reader import Applications"
44
+ ]
45
+ },
46
+ {
47
+ "cell_type": "code",
48
+ "execution_count": null,
49
+ "id": "deba4a1e",
50
+ "metadata": {},
51
+ "outputs": [],
52
+ "source": [
53
+ "DELTA22 = paths.dataset_file(\"delta22\", root=REPO)\n",
54
+ "XLSX = os.path.join(REPO, \"data\", \"delta22\", \"delta22_experimental.xlsx\")\n",
55
+ "\n",
56
+ "def document_path(name):\n",
57
+ " os.makedirs(\"documents\", exist_ok=True)\n",
58
+ " return os.path.join(\"documents\", name)"
59
+ ]
60
+ },
61
+ {
62
+ "cell_type": "code",
63
+ "execution_count": null,
64
+ "id": "e408fac2",
65
+ "metadata": {},
66
+ "outputs": [],
67
+ "source": [
68
+ "tables = scaling_factors.published_scaling_tables() # {\"H\": Table S10, \"C\": Table S11}\n",
69
+ "print(\"Table S10 (1H):\"); display(tables[\"H\"])\n",
70
+ "print(\"Table S11 (13C):\"); display(tables[\"C\"])\n",
71
+ "\n",
72
+ "out = document_path(\"si_table_s10_s11_scaling.xlsx\")\n",
73
+ "with pd.ExcelWriter(out) as writer:\n",
74
+ " tables[\"H\"].reset_index().to_excel(writer, sheet_name=\"Table S10 (1H)\", index=False)\n",
75
+ " tables[\"C\"].reset_index().to_excel(writer, sheet_name=\"Table S11 (13C)\", index=False)\n",
76
+ "print(\"wrote\", os.path.relpath(out, REPO))"
77
+ ]
78
+ },
79
+ {
80
+ "cell_type": "markdown",
81
+ "id": "b6094980",
82
+ "metadata": {},
83
+ "source": [
84
+ "## Reproducibility check: re-derive from delta-22"
85
+ ]
86
+ },
87
+ {
88
+ "cell_type": "code",
89
+ "execution_count": null,
90
+ "id": "19f08d14",
91
+ "metadata": {},
92
+ "outputs": [],
93
+ "source": [
94
+ "derived = scaling_factors.build_scaling_tables(DELTA22, XLSX)\n",
95
+ "for nucleus in (\"H\", \"C\"):\n",
96
+ " p = tables[nucleus]\n",
97
+ " d = derived[nucleus].reindex(p.index)[p.columns]\n",
98
+ " max_dev = float(np.abs(p.values - d.values).max())\n",
99
+ " print(f\"{nucleus}: largest published-vs-rederived deviation = {max_dev:.2e}\")\n",
100
+ " assert max_dev < 1e-5, f\"{nucleus} scaling table diverged from the published values\"\n",
101
+ "print(\"both tables reproduce from delta-22\")"
102
+ ]
103
+ },
104
+ {
105
+ "cell_type": "markdown",
106
+ "id": "8edf48b3",
107
+ "metadata": {},
108
+ "source": [
109
+ "## Deployment tables with reflection symmetrization\n",
110
+ "\n",
111
+ "The tables above match the published SI exactly. For serving new molecules, the recommended tables\n",
112
+ "average each prediction with its mirror image, correcting a reflection-parity error. Reproducing\n",
113
+ "them needs the model checkpoints; the shipped tables are shown below."
114
+ ]
115
+ },
116
+ {
117
+ "cell_type": "code",
118
+ "execution_count": null,
119
+ "id": "448f2e9f",
120
+ "metadata": {},
121
+ "outputs": [],
122
+ "source": [
123
+ "symmetrized = scaling_factors_reader.load_symmetrized_tables()\n",
124
+ "print(\"Symmetrized deployment Table S10 (1H):\"); display(symmetrized[\"H\"])\n",
125
+ "print(\"Symmetrized deployment Table S11 (13C):\"); display(symmetrized[\"C\"])"
126
+ ]
127
+ },
128
+ {
129
+ "cell_type": "markdown",
130
+ "id": "ca457bc6",
131
+ "metadata": {},
132
+ "source": [
133
+ "## Composite-model coefficients (Figure 5C/5D, SI S15)\n",
134
+ "\n",
135
+ "The natural-products figures use a larger family of per-solvent coefficients fit on delta-22 (the\n",
136
+ "`composite_model` group in `applications.hdf5`): OLS fits, 1000-seed bootstrap resamples, their RMSE\n",
137
+ "distributions, and the PCM conversion factors. The group regenerates from released inputs alone."
138
+ ]
139
+ },
140
+ {
141
+ "cell_type": "code",
142
+ "execution_count": null,
143
+ "id": "47c3d1e7",
144
+ "metadata": {},
145
+ "outputs": [],
146
+ "source": [
147
+ "reader = Applications(paths.dataset_file(\"applications\", root=REPO))\n",
148
+ "regenerated = build_composite_model.regenerate(reader)\n",
149
+ "max_dev = build_composite_model.verify(reader, regenerated)\n",
150
+ "print(f\"composite_model largest regenerated-vs-stored deviation = {max_dev:.2e}\")"
151
+ ]
152
+ }
153
+ ],
154
+ "metadata": {
155
+ "language_info": {
156
+ "name": "python"
157
+ }
158
+ },
159
+ "nbformat": 4,
160
+ "nbformat_minor": 5
161
+ }
data/scaling_factors/scaling_factors_symmetrized_H.csv ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ solvent,intercept,stationary,pcm
2
+ chloroform,31.291983,-0.975682,-0.851715
3
+ tetrahydrofuran,31.319664,-0.980082,-0.781373
4
+ dichloromethane,31.374485,-0.979760,-0.806215
5
+ acetone,31.510823,-0.987188,-1.229850
6
+ acetonitrile,31.494061,-0.985628,-0.969617
7
+ dimethylsulfoxide,31.597261,-0.991031,-1.348009
8
+ trifluoroethanol,30.873172,-0.959885,-0.955808
9
+ methanol,31.253627,-0.976349,-1.246680
10
+ TIP4P,31.356622,-0.978710,-1.475907
11
+ benzene,31.984616,-1.005170,2.239317
12
+ toluene,31.714067,-0.996613,1.948607
13
+ chlorobenzene,31.677950,-0.993715,0.830908
data/scaling_factors/test_scaling_factors_export.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Structural checks for the shipped symmetrized scaling tables (data/scaling_factors/).
2
+
3
+ These run on the small CSVs that ship in git, so they need no download. The file is named
4
+ test_scaling_factors_export.py (not test_scaling_factors.py) on purpose: analysis/code/ already has a
5
+ test_scaling_factors.py, and pytest's default import mode cannot collect two test modules that share
6
+ a basename.
7
+ """
8
+ import os
9
+ import sys
10
+
11
+ import pandas as pd
12
+ import pytest
13
+
14
+ HERE = os.path.dirname(os.path.abspath(__file__))
15
+ sys.path.insert(0, HERE)
16
+
17
+ import scaling_factors_reader as R # noqa: E402
18
+
19
+
20
+ def test_both_tables_load_with_expected_shape_and_columns():
21
+ tables = R.load_symmetrized_tables()
22
+ assert set(tables) == {"H", "C"}
23
+ for nucleus, df in tables.items():
24
+ assert df.shape == (12, 3), nucleus
25
+ assert list(df.columns) == ["intercept", "stationary", "pcm"], nucleus
26
+ assert "chloroform" in df.index
27
+ assert df.notnull().all().all(), nucleus
28
+
29
+
30
+ def test_slopes_are_negative_and_intercepts_in_range():
31
+ tables = R.load_symmetrized_tables()
32
+ # the gas-phase (stationary) slope is a shielding-to-shift conversion, always near -1
33
+ for nucleus, df in tables.items():
34
+ assert (df["stationary"] < 0).all(), nucleus
35
+ # proton reference intercept sits near a bare-proton shielding (~31 ppm), carbon much higher
36
+ assert 25 < tables["H"]["intercept"].mean() < 35
37
+ assert 150 < tables["C"]["intercept"].mean() < 200
38
+
39
+
40
+ def test_predict_shift_uses_the_documented_linear_formula():
41
+ # a synthetic one-row table with round coefficients, so the expected value is hand-computed
42
+ # independently of predict_shift's own arithmetic (this catches a sign flip or a swap of the
43
+ # stationary/pcm coefficients, which recomputing the same formula inline would not):
44
+ # 10 + (-1)*5 + 2*3 = 11
45
+ table = pd.DataFrame({"intercept": [10.0], "stationary": [-1.0], "pcm": [2.0]}, index=["x"])
46
+ table.index.name = "solvent"
47
+ assert R.predict_shift(table, "x", shielding=5.0, correction=3.0) == pytest.approx(11.0)
48
+
49
+
50
+ def test_reader_rejects_unknown_nucleus():
51
+ with pytest.raises(ValueError):
52
+ R._csv_path("X")