Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .github/workflows/ci.yml +44 -0
- .github/workflows/publish-pypi.yml +34 -0
- .github/workflows/sync-to-hf.yml +51 -0
- analysis/code/build_nb_dataset_summary.py +90 -0
- analysis/code/build_nb_leveling.py +295 -0
- analysis/code/carbon_monoxide.py +63 -0
- analysis/code/cp3.py +55 -0
- analysis/code/delta22_plots.py +606 -0
- analysis/code/shared/fixed_point.py +21 -0
- analysis/code/shared/spreadsheet.py +8 -0
- analysis/code/shared/stats.py +58 -0
- analysis/code/shared/test_fixed_point.py +42 -0
- analysis/code/shared/test_spreadsheet.py +19 -0
- analysis/code/shared/test_stats.py +46 -0
- analysis/code/test_cp3.py +39 -0
- analysis/code/test_magnet_benchmark.py +125 -0
- analysis/code/test_paths.py +94 -0
- analysis/code/test_scaling_factors.py +272 -0
- analysis/manuscript_figures/fig1a_cp3.ipynb +143 -0
- analysis/manuscript_figures/fig2a_pareto.ipynb +82 -0
- analysis/manuscript_figures/fig2b_correlation.ipynb +111 -0
- analysis/manuscript_figures/fig2d_convergence.ipynb +148 -0
- analysis/manuscript_figures/fig3a_pcm_benefit.ipynb +151 -0
- analysis/manuscript_figures/fig3b_shifts.ipynb +105 -0
- analysis/manuscript_figures/fig3d_solvent_corrections.ipynb +149 -0
- analysis/manuscript_figures/fig4a_implicit.ipynb +113 -0
- analysis/manuscript_figures/fig4b_explicit.ipynb +113 -0
- analysis/manuscript_figures/fig4c_rmse_range.ipynb +127 -0
- analysis/manuscript_figures/fig5b_dft8k.ipynb +188 -0
- analysis/manuscript_figures/fig5c.ipynb +115 -0
- analysis/manuscript_figures/fig5d.ipynb +145 -0
- analysis/si_figures/si_composite_models_ablations.ipynb +176 -0
- analysis/si_figures/si_figure_s01.ipynb +85 -0
- analysis/si_figures/si_figure_s02_diels_alder.ipynb +130 -0
- analysis/si_figures/si_figure_s04.ipynb +167 -0
- analysis/si_figures/si_figure_s05.ipynb +124 -0
- analysis/si_figures/si_figure_s06.ipynb +152 -0
- analysis/si_figures/si_figure_s07.ipynb +100 -0
- analysis/si_figures/si_figure_s08_panelA_vomicine.ipynb +121 -0
- analysis/si_figures/si_figure_s08_panelsBCD.ipynb +248 -0
- analysis/si_figures/si_figure_s15.ipynb +286 -0
- analysis/si_figures/si_figure_s16_s18.ipynb +143 -0
- analysis/si_tables/si_table_s01_s02_pareto.ipynb +125 -0
- analysis/si_tables/si_table_s03_s04_performance.ipynb +122 -0
- analysis/si_tables/si_table_s05_qcd.ipynb +115 -0
- analysis/si_tables/si_table_s06_solvent_corrections.ipynb +115 -0
- analysis/si_tables/si_table_s08_summary.ipynb +102 -0
- analysis/si_tables/si_table_s10_s11_scaling.ipynb +161 -0
- data/scaling_factors/scaling_factors_symmetrized_H.csv +13 -0
- 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")
|