sync code from github@2bc62543318f07e5f8b641923b7789a8438d06bd
Browse files- analysis/code/build_nb_scaling.py +16 -20
- analysis/code/scaling_factors.py +27 -56
- analysis/code/scaling_factors_symmetrized.py +5 -4
- analysis/code/test_scaling_factors.py +72 -58
- analysis/si_tables/si_table_s10_s11_scaling.ipynb +23 -41
- data/scaling_factors/scaling_factors_reader.py +5 -6
- magnet/scaling.py +27 -25
analysis/code/build_nb_scaling.py
CHANGED
|
@@ -21,7 +21,7 @@ import os, sys
|
|
| 21 |
|
| 22 |
# make the in-repo modules importable (not pip-installed)
|
| 23 |
REPO = os.path.abspath("../..")
|
| 24 |
-
for _p in ("data/delta22", "data/
|
| 25 |
"analysis/code", "analysis/code/shared"):
|
| 26 |
sys.path.insert(0, os.path.join(REPO, _p))
|
| 27 |
"""
|
|
@@ -30,7 +30,6 @@ _IMPORTS = r"""
|
|
| 30 |
import numpy as np
|
| 31 |
import pandas as pd
|
| 32 |
import scaling_factors
|
| 33 |
-
import scaling_factors_reader
|
| 34 |
import build_composite_model
|
| 35 |
import paths
|
| 36 |
from applications_reader import Applications
|
|
@@ -50,7 +49,9 @@ si_table_s10_s11_scaling = [
|
|
| 50 |
# Tables S10 and S11: MagNET-Zero/MagNET-PCM scaling factors
|
| 51 |
|
| 52 |
Per-solvent linear coefficients (intercept, stationary, pcm) mapping MagNET-Zero shieldings plus a
|
| 53 |
-
MagNET-PCM correction to predicted shifts: ¹H (S10) and ¹³C (S11).
|
|
|
|
|
|
|
| 54 |
"""),
|
| 55 |
code(_BOOTSTRAP),
|
| 56 |
code(_IMPORTS),
|
|
@@ -66,28 +67,23 @@ with pd.ExcelWriter(out) as writer:
|
|
| 66 |
tables["C"].reset_index().to_excel(writer, sheet_name="Table S11 (13C)", index=False)
|
| 67 |
print("wrote", os.path.relpath(out, REPO))
|
| 68 |
"""),
|
| 69 |
-
md("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
code(r"""
|
| 71 |
-
derived = scaling_factors.build_scaling_tables(DELTA22, XLSX)
|
| 72 |
for nucleus in ("H", "C"):
|
| 73 |
p = tables[nucleus]
|
| 74 |
d = derived[nucleus].reindex(p.index)[p.columns]
|
| 75 |
max_dev = float(np.abs(p.values - d.values).max())
|
| 76 |
-
print(f"{nucleus}: largest published-vs-
|
| 77 |
-
assert max_dev <
|
| 78 |
-
print("
|
| 79 |
-
"""),
|
| 80 |
-
md(r"""
|
| 81 |
-
## Deployment tables with reflection symmetrization
|
| 82 |
-
|
| 83 |
-
The tables above match the published SI exactly. For serving new molecules, the recommended tables
|
| 84 |
-
average each prediction with its mirror image, correcting a reflection-parity error. Reproducing
|
| 85 |
-
them needs the model checkpoints; the shipped tables are shown below.
|
| 86 |
-
"""),
|
| 87 |
-
code(r"""
|
| 88 |
-
symmetrized = scaling_factors_reader.load_symmetrized_tables()
|
| 89 |
-
print("Symmetrized deployment Table S10 (1H):"); display(symmetrized["H"])
|
| 90 |
-
print("Symmetrized deployment Table S11 (13C):"); display(symmetrized["C"])
|
| 91 |
"""),
|
| 92 |
md(r"""
|
| 93 |
## Composite-model coefficients (Figure 5C/5D, SI S15)
|
|
|
|
| 21 |
|
| 22 |
# make the in-repo modules importable (not pip-installed)
|
| 23 |
REPO = os.path.abspath("../..")
|
| 24 |
+
for _p in ("data/delta22", "data/applications",
|
| 25 |
"analysis/code", "analysis/code/shared"):
|
| 26 |
sys.path.insert(0, os.path.join(REPO, _p))
|
| 27 |
"""
|
|
|
|
| 30 |
import numpy as np
|
| 31 |
import pandas as pd
|
| 32 |
import scaling_factors
|
|
|
|
| 33 |
import build_composite_model
|
| 34 |
import paths
|
| 35 |
from applications_reader import Applications
|
|
|
|
| 49 |
# Tables S10 and S11: MagNET-Zero/MagNET-PCM scaling factors
|
| 50 |
|
| 51 |
Per-solvent linear coefficients (intercept, stationary, pcm) mapping MagNET-Zero shieldings plus a
|
| 52 |
+
MagNET-PCM correction to predicted shifts: ¹H (S10) and ¹³C (S11). The published coefficients are
|
| 53 |
+
reflection-symmetrized (each MagNET-Zero/PCM shielding averaged over twenty forward passes, the input
|
| 54 |
+
geometry mirrored for half), which removes the model's reflection-parity error.
|
| 55 |
"""),
|
| 56 |
code(_BOOTSTRAP),
|
| 57 |
code(_IMPORTS),
|
|
|
|
| 67 |
tables["C"].reset_index().to_excel(writer, sheet_name="Table S11 (13C)", index=False)
|
| 68 |
print("wrote", os.path.relpath(out, REPO))
|
| 69 |
"""),
|
| 70 |
+
md(r"""
|
| 71 |
+
## Reproducibility check: re-derive from delta-22
|
| 72 |
+
|
| 73 |
+
The published tables are reflection-symmetrized, so reproducing them exactly needs the model
|
| 74 |
+
checkpoints (`build_scaling_tables(symmetrized=True)`). The checkpoint-free
|
| 75 |
+
`build_scaling_tables(symmetrized=False)` below fits the raw single-pass shieldings stored in the
|
| 76 |
+
released delta-22 file and lands within ~0.01 ppm.
|
| 77 |
+
"""),
|
| 78 |
code(r"""
|
| 79 |
+
derived = scaling_factors.build_scaling_tables(DELTA22, XLSX) # symmetrized=False (checkpoint-free)
|
| 80 |
for nucleus in ("H", "C"):
|
| 81 |
p = tables[nucleus]
|
| 82 |
d = derived[nucleus].reindex(p.index)[p.columns]
|
| 83 |
max_dev = float(np.abs(p.values - d.values).max())
|
| 84 |
+
print(f"{nucleus}: largest published-vs-raw-refit deviation = {max_dev:.2e} ppm")
|
| 85 |
+
assert max_dev < 1.5e-2, f"{nucleus} raw refit drifted too far from the published values"
|
| 86 |
+
print("raw delta-22 refit reproduces the published tables to the reflection-parity floor")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
"""),
|
| 88 |
md(r"""
|
| 89 |
## Composite-model coefficients (Figure 5C/5D, SI S15)
|
analysis/code/scaling_factors.py
CHANGED
|
@@ -11,10 +11,13 @@ uses a TWO-parameter model: the gas and PCM terms share one per-solvent slope (P
|
|
| 11 |
by a conversion factor first), which fit held-out carbons slightly better; the reported `pcm` is that
|
| 12 |
slope times the conversion factor.
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
| 18 |
"""
|
| 19 |
import os
|
| 20 |
import sys
|
|
@@ -50,50 +53,20 @@ RECOMMENDED_MODEL = {"H": "three_parameter", "C": "two_parameter"}
|
|
| 50 |
|
| 51 |
_COLUMNS = ["intercept", "stationary", "pcm"]
|
| 52 |
|
| 53 |
-
#
|
| 54 |
-
|
| 55 |
-
# build_scaling_tables() reproduces from delta-22; test_scaling_factors.py asserts the two agree, so
|
| 56 |
-
# they cannot silently drift. Prefer published_scaling_tables() for a quick lookup, and
|
| 57 |
-
# build_scaling_tables() when you want to re-derive them from the raw data.
|
| 58 |
-
_PUBLISHED_TABLE_CSV = {
|
| 59 |
-
"H": """solvent,intercept,stationary,pcm
|
| 60 |
-
tetrahydrofuran,31.321785,-0.980175,-0.786672
|
| 61 |
-
dichloromethane,31.377310,-0.979870,-0.808573
|
| 62 |
-
chloroform,31.294992,-0.975794,-0.852690
|
| 63 |
-
toluene,31.716957,-0.996733,1.944856
|
| 64 |
-
benzene,31.987569,-1.005289,2.236515
|
| 65 |
-
chlorobenzene,31.681438,-0.993846,0.830236
|
| 66 |
-
acetone,31.512157,-0.987261,-1.238414
|
| 67 |
-
dimethylsulfoxide,31.598698,-0.991103,-1.355248
|
| 68 |
-
acetonitrile,31.496110,-0.985716,-0.974465
|
| 69 |
-
trifluoroethanol,30.876004,-0.959997,-0.958852
|
| 70 |
-
methanol,31.256030,-0.976446,-1.250121
|
| 71 |
-
TIP4P,31.359134,-0.978809,-1.478298
|
| 72 |
-
""",
|
| 73 |
-
"C": """solvent,intercept,stationary,pcm
|
| 74 |
-
tetrahydrofuran,171.054488,-0.919000,-1.069509
|
| 75 |
-
dichloromethane,171.509389,-0.921167,-1.115015
|
| 76 |
-
chloroform,171.728797,-0.924231,-0.936804
|
| 77 |
-
toluene,171.690907,-0.924987,-0.618410
|
| 78 |
-
benzene,171.967177,-0.927033,-0.593716
|
| 79 |
-
chlorobenzene,171.075910,-0.921238,-0.997641
|
| 80 |
-
acetone,171.308023,-0.919610,-1.239503
|
| 81 |
-
dimethylsulfoxide,170.598425,-0.918674,-1.296501
|
| 82 |
-
acetonitrile,171.879839,-0.922629,-1.287664
|
| 83 |
-
trifluoroethanol,174.237671,-0.939012,-1.290073
|
| 84 |
-
methanol,172.426161,-0.927566,-1.288847
|
| 85 |
-
TIP4P,173.696379,-0.937854,-1.342668
|
| 86 |
-
""",
|
| 87 |
-
}
|
| 88 |
|
| 89 |
|
| 90 |
def published_scaling_tables():
|
| 91 |
"""The published SI scaling tables, {"H": Table S10, "C": Table S11}, as solvent-indexed
|
| 92 |
-
DataFrames
|
| 93 |
-
shieldings into
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
def proton_scaling_table(query_df_nn, solvents=SOLVENTS, exclude_solutes=EXCLUDE_SOLUTES):
|
|
@@ -155,19 +128,15 @@ def carbon_scaling_table(query_df_nn, query_df_dft=None, solvents=SOLVENTS,
|
|
| 155 |
|
| 156 |
|
| 157 |
def build_scaling_tables(delta22_path, experimental_path, symmetrized=False, n_passes=10):
|
| 158 |
-
"""
|
| 159 |
|
| 160 |
-
symmetrized=
|
| 161 |
-
|
| 162 |
-
and
|
| 163 |
|
| 164 |
-
symmetrized=
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
values (they come from a single unsymmetrized forward pass). Use this for a deployment-quality
|
| 168 |
-
scaling table -- e.g. serving MagNET-Zero/MagNET-PCM on new "everyday" molecules -- rather than
|
| 169 |
-
for reproducing the SI. Requires the magnet package and released model checkpoints; slower
|
| 170 |
-
(re-runs inference on all 22 solutes instead of reading pre-baked values).
|
| 171 |
|
| 172 |
Returns {"H": Table S10 DataFrame, "C": Table S11 DataFrame}.
|
| 173 |
"""
|
|
@@ -214,7 +183,9 @@ def predict_shift(table, solvent, magnet_zero_shielding, magnet_pcm_chloroform_c
|
|
| 214 |
if __name__ == "__main__":
|
| 215 |
h5 = dataset_file("delta22", root=_REPO)
|
| 216 |
xlsx = os.path.join(_REPO, "data", "delta22", "delta22_experimental.xlsx")
|
|
|
|
|
|
|
| 217 |
tables = build_scaling_tables(h5, xlsx)
|
| 218 |
for nucleus, label in (("H", "Table S10 (1H)"), ("C", "Table S11 (13C)")):
|
| 219 |
-
print(f"\n=== {label}: {RECOMMENDED_MODEL[nucleus]} model ===")
|
| 220 |
print(tables[nucleus].round(6).to_string())
|
|
|
|
| 11 |
by a conversion factor first), which fit held-out carbons slightly better; the reported `pcm` is that
|
| 12 |
slope times the conversion factor.
|
| 13 |
|
| 14 |
+
The published tables are REFLECTION-SYMMETRIZED: each shielding is averaged over 20 forward passes
|
| 15 |
+
with the geometry mirrored for half (n_passes=10, symmetrize=True), cancelling the SO(3)-only model's
|
| 16 |
+
reflection-parity error. They ship verbatim in
|
| 17 |
+
data/scaling_factors/scaling_factors_symmetrized_{H,C}.csv; published_scaling_tables() returns them,
|
| 18 |
+
and build_scaling_tables(symmetrized=True) reproduces them from live inference (needs checkpoints).
|
| 19 |
+
build_scaling_tables(symmetrized=False) fits the raw single-pass shieldings in delta22.hdf5 instead
|
| 20 |
+
and lands ~0.01 ppm off. test_scaling_factors.py locks this down.
|
| 21 |
"""
|
| 22 |
import os
|
| 23 |
import sys
|
|
|
|
| 53 |
|
| 54 |
_COLUMNS = ["intercept", "stationary", "pcm"]
|
| 55 |
|
| 56 |
+
# published_scaling_tables() reads the shipped SI tables from here (provenance in the module docstring).
|
| 57 |
+
_SYMMETRIZED_CSV_DIR = os.path.join(_REPO, "data", "scaling_factors")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
def published_scaling_tables():
|
| 61 |
"""The published SI scaling tables, {"H": Table S10, "C": Table S11}, as solvent-indexed
|
| 62 |
+
DataFrames (columns intercept / stationary / pcm). Read from the shipped CSVs, so no delta-22
|
| 63 |
+
download is needed; pass to predict_shift to turn MagNET-Zero/PCM shieldings into shifts. See the
|
| 64 |
+
module docstring for provenance."""
|
| 65 |
+
tables = {}
|
| 66 |
+
for nucleus in ("H", "C"):
|
| 67 |
+
path = os.path.join(_SYMMETRIZED_CSV_DIR, f"scaling_factors_symmetrized_{nucleus}.csv")
|
| 68 |
+
tables[nucleus] = pd.read_csv(path).set_index("solvent")[_COLUMNS]
|
| 69 |
+
return tables
|
| 70 |
|
| 71 |
|
| 72 |
def proton_scaling_table(query_df_nn, solvents=SOLVENTS, exclude_solutes=EXCLUDE_SOLUTES):
|
|
|
|
| 128 |
|
| 129 |
|
| 130 |
def build_scaling_tables(delta22_path, experimental_path, symmetrized=False, n_passes=10):
|
| 131 |
+
"""Re-derive both recommended-scaling tables from the released delta-22 data.
|
| 132 |
|
| 133 |
+
symmetrized=True reproduces the published SI Tables S10/S11 the way they were generated: live,
|
| 134 |
+
reflection-symmetrized inference (scaling_factors_symmetrized.py). Needs the magnet package and
|
| 135 |
+
model checkpoints, and is slow (re-runs inference on all 22 solutes).
|
| 136 |
|
| 137 |
+
symmetrized=False (default) fits the raw single-pass MagNET-Zero/PCM shieldings stored in the
|
| 138 |
+
HDF5, which carry the reflection-parity error the published tables correct, so it lands ~0.01 ppm
|
| 139 |
+
off the published SI. Use it for a checkpoint-free re-derivation.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
Returns {"H": Table S10 DataFrame, "C": Table S11 DataFrame}.
|
| 142 |
"""
|
|
|
|
| 183 |
if __name__ == "__main__":
|
| 184 |
h5 = dataset_file("delta22", root=_REPO)
|
| 185 |
xlsx = os.path.join(_REPO, "data", "delta22", "delta22_experimental.xlsx")
|
| 186 |
+
# symmetrized=False: the checkpoint-free raw-data fit, within ~0.01 ppm of the published SI
|
| 187 |
+
# (published_scaling_tables() returns the exact symmetrized SI values).
|
| 188 |
tables = build_scaling_tables(h5, xlsx)
|
| 189 |
for nucleus, label in (("H", "Table S10 (1H)"), ("C", "Table S11 (13C)")):
|
| 190 |
+
print(f"\n=== {label} (unsymmetrized re-derivation): {RECOMMENDED_MODEL[nucleus]} model ===")
|
| 191 |
print(tables[nucleus].round(6).to_string())
|
analysis/code/scaling_factors_symmetrized.py
CHANGED
|
@@ -10,10 +10,11 @@ negligible, even on small molecules (delta-22's solutes are 6-22 atoms). `predic
|
|
| 10 |
symmetrize=True)` fixes this for free by averaging the prediction on the original geometry and its
|
| 11 |
mirror image.
|
| 12 |
|
| 13 |
-
This module
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
| 17 |
"""
|
| 18 |
import contextlib
|
| 19 |
import os
|
|
|
|
| 10 |
symmetrize=True)` fixes this for free by averaging the prediction on the original geometry and its
|
| 11 |
mirror image.
|
| 12 |
|
| 13 |
+
This module reproduces the SI's published Tables S10/S11 (they ARE the reflection-symmetrized tables,
|
| 14 |
+
shipped in data/scaling_factors/scaling_factors_symmetrized_{H,C}.csv).
|
| 15 |
+
scaling_factors.build_scaling_tables(symmetrized=True) calls in here to regenerate them from live
|
| 16 |
+
inference; its checkpoint-free symmetrized=False path fits the raw single-pass HDF5 shieldings instead
|
| 17 |
+
and lands ~0.01 ppm off.
|
| 18 |
"""
|
| 19 |
import contextlib
|
| 20 |
import os
|
analysis/code/test_scaling_factors.py
CHANGED
|
@@ -126,43 +126,46 @@ def test_carbon_requires_factors_or_dft():
|
|
| 126 |
|
| 127 |
# --------------------------------------------------------------------------- opt-in real-data test
|
| 128 |
|
| 129 |
-
# Published SI values
|
| 130 |
-
#
|
| 131 |
-
#
|
|
|
|
| 132 |
PUBLISHED_S10_H = {
|
| 133 |
-
"chloroform": (31.
|
| 134 |
-
"tetrahydrofuran": (31.
|
| 135 |
-
"dichloromethane": (31.
|
| 136 |
-
"acetone": (31.
|
| 137 |
-
"acetonitrile": (31.
|
| 138 |
-
"dimethylsulfoxide": (31.
|
| 139 |
-
"trifluoroethanol": (30.
|
| 140 |
-
"methanol": (31.
|
| 141 |
-
"TIP4P": (31.
|
| 142 |
-
"benzene": (31.
|
| 143 |
-
"toluene": (31.
|
| 144 |
-
"chlorobenzene": (31.
|
| 145 |
}
|
| 146 |
PUBLISHED_S11_C = {
|
| 147 |
-
"chloroform": (171.
|
| 148 |
-
"tetrahydrofuran": (171.
|
| 149 |
-
"dichloromethane": (171.
|
| 150 |
-
"acetone": (171.
|
| 151 |
-
"acetonitrile": (171.
|
| 152 |
-
"dimethylsulfoxide": (170.
|
| 153 |
-
"trifluoroethanol": (174.
|
| 154 |
-
"methanol": (172.
|
| 155 |
-
"TIP4P": (173.
|
| 156 |
-
"benzene": (171.
|
| 157 |
-
"toluene": (171.
|
| 158 |
-
"chlorobenzene": (171.
|
| 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
|
| 164 |
-
values for all 12 solvents and both nuclei. Runs in CI without
|
| 165 |
-
|
|
|
|
|
|
|
| 166 |
tables = S.published_scaling_tables()
|
| 167 |
for nucleus, published in (("H", PUBLISHED_S10_H), ("C", PUBLISHED_S11_C)):
|
| 168 |
table = tables[nucleus]
|
|
@@ -177,20 +180,29 @@ def test_shipped_published_tables_match_si_values():
|
|
| 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
|
| 181 |
-
"""
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
tables
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 192 |
-
assert row["stationary"] == pytest.approx(stationary, abs=5e-
|
| 193 |
-
assert row["pcm"] == pytest.approx(pcm, abs=5e-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
|
| 196 |
# --------------------------------------------------------------------------- symmetrized-inference smoke test
|
|
@@ -203,41 +215,43 @@ _HAS_CHECKPOINTS = os.path.exists(CKPT_ZERO) and os.path.exists(CKPT_PCM)
|
|
| 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
|
| 207 |
-
"""symmetrized=True re-derives the tables from live, reflection-symmetrized
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
to the
|
| 212 |
pytest.importorskip("torch")
|
| 213 |
-
|
| 214 |
-
|
|
|
|
| 215 |
for nucleus, published in (("H", PUBLISHED_S10_H), ("C", PUBLISHED_S11_C)):
|
| 216 |
-
table =
|
| 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 |
-
#
|
| 223 |
-
#
|
| 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 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
#
|
| 231 |
-
#
|
| 232 |
-
assert
|
| 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
|
| 239 |
-
a live symmetrized build at the n_passes they were generated with (10), not
|
| 240 |
-
swapped column, wrong solvent order, or stale hand-edit of the shipped CSVs fails
|
|
|
|
| 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")
|
|
|
|
| 126 |
|
| 127 |
# --------------------------------------------------------------------------- opt-in real-data test
|
| 128 |
|
| 129 |
+
# Published SI values (reflection-symmetrized Tables S10/S11): parameter tuples are
|
| 130 |
+
# (intercept, stationary, pcm) per solvent, all 12 solvents. These pin the shipped
|
| 131 |
+
# published_scaling_tables() copy (no-data test below): shipped symmetrized CSV == this dict, and the
|
| 132 |
+
# raw-data build lands within ~0.003 ppm of it (real-data test). The chain keeps everything in sync.
|
| 133 |
PUBLISHED_S10_H = {
|
| 134 |
+
"chloroform": (31.291983, -0.975682, -0.851715),
|
| 135 |
+
"tetrahydrofuran": (31.319664, -0.980082, -0.781373),
|
| 136 |
+
"dichloromethane": (31.374485, -0.979760, -0.806215),
|
| 137 |
+
"acetone": (31.510823, -0.987188, -1.229850),
|
| 138 |
+
"acetonitrile": (31.494061, -0.985628, -0.969617),
|
| 139 |
+
"dimethylsulfoxide": (31.597261, -0.991031, -1.348009),
|
| 140 |
+
"trifluoroethanol": (30.873172, -0.959885, -0.955808),
|
| 141 |
+
"methanol": (31.253627, -0.976349, -1.246680),
|
| 142 |
+
"TIP4P": (31.356622, -0.978710, -1.475907),
|
| 143 |
+
"benzene": (31.984616, -1.005170, 2.239317),
|
| 144 |
+
"toluene": (31.714067, -0.996613, 1.948607),
|
| 145 |
+
"chlorobenzene": (31.677950, -0.993715, 0.830908),
|
| 146 |
}
|
| 147 |
PUBLISHED_S11_C = {
|
| 148 |
+
"chloroform": (171.726919, -0.924228, -0.936801),
|
| 149 |
+
"tetrahydrofuran": (171.052274, -0.918997, -1.069506),
|
| 150 |
+
"dichloromethane": (171.507154, -0.921165, -1.115013),
|
| 151 |
+
"acetone": (171.305850, -0.919608, -1.239500),
|
| 152 |
+
"acetonitrile": (171.877492, -0.922626, -1.287661),
|
| 153 |
+
"dimethylsulfoxide": (170.596065, -0.918672, -1.296497),
|
| 154 |
+
"trifluoroethanol": (174.235231, -0.939011, -1.290071),
|
| 155 |
+
"methanol": (172.423799, -0.927563, -1.288843),
|
| 156 |
+
"TIP4P": (173.692750, -0.937848, -1.342659),
|
| 157 |
+
"benzene": (171.965715, -0.927029, -0.593714),
|
| 158 |
+
"toluene": (171.689276, -0.924983, -0.618407),
|
| 159 |
+
"chlorobenzene": (171.073820, -0.921235, -0.997638),
|
| 160 |
}
|
| 161 |
|
| 162 |
|
| 163 |
def test_shipped_published_tables_match_si_values():
|
| 164 |
+
"""The shipped published_scaling_tables() copy (no data download needed) equals the published
|
| 165 |
+
(reflection-symmetrized) SI values for all 12 solvents and both nuclei. Runs in CI without
|
| 166 |
+
delta-22; the real-data tests below tie those same SI values to a raw-data fit (within the
|
| 167 |
+
reflection-parity gap) and to live symmetrized inference (exact), so the shipped copy cannot
|
| 168 |
+
drift."""
|
| 169 |
tables = S.published_scaling_tables()
|
| 170 |
for nucleus, published in (("H", PUBLISHED_S10_H), ("C", PUBLISHED_S11_C)):
|
| 171 |
table = tables[nucleus]
|
|
|
|
| 180 |
|
| 181 |
@pytest.mark.skipif(not (os.path.exists(REAL_H5) and os.path.exists(REAL_XLSX)),
|
| 182 |
reason="real delta22.hdf5 / experimental xlsx not present")
|
| 183 |
+
def test_unsymmetrized_fit_lands_near_published_si():
|
| 184 |
+
"""The checkpoint-free raw-data fit (symmetrized=False, from the HDF5's stored single-pass
|
| 185 |
+
shieldings) lands within ~0.01 ppm of the published, reflection-symmetrized SI tables for all 12
|
| 186 |
+
solvents (the largest gaps are on the polar-solvent pcm coefficients, ~0.009). The gap is the
|
| 187 |
+
reflection-parity correction the published tables apply; it is small on delta-22's tiny solutes
|
| 188 |
+
but nonzero, so this must NOT match to the encoding floor. The 1.5e-2 bound documents the gap and
|
| 189 |
+
still catches a real regression (coefficients are O(1) to O(170))."""
|
| 190 |
+
tables = S.build_scaling_tables(REAL_H5, REAL_XLSX) # symmetrized=False
|
| 191 |
+
max_abs_delta = 0.0
|
| 192 |
for nucleus, published in (("H", PUBLISHED_S10_H), ("C", PUBLISHED_S11_C)):
|
| 193 |
table = tables[nucleus]
|
| 194 |
assert list(table.columns) == ["intercept", "stationary", "pcm"]
|
| 195 |
assert sorted(table.index) == sorted(published)
|
| 196 |
for solvent, (intercept, stationary, pcm) in published.items():
|
| 197 |
row = table.loc[solvent]
|
| 198 |
+
assert row["intercept"] == pytest.approx(intercept, abs=1.5e-2), f"{nucleus} {solvent} int"
|
| 199 |
+
assert row["stationary"] == pytest.approx(stationary, abs=1.5e-2), f"{nucleus} {solvent} stat"
|
| 200 |
+
assert row["pcm"] == pytest.approx(pcm, abs=1.5e-2), f"{nucleus} {solvent} pcm"
|
| 201 |
+
max_abs_delta = max(max_abs_delta, abs(row["intercept"] - intercept),
|
| 202 |
+
abs(row["stationary"] - stationary), abs(row["pcm"] - pcm))
|
| 203 |
+
# the raw fit is genuinely the unsymmetrized one, not the published values by accident: the
|
| 204 |
+
# reflection correction shifts at least one coefficient above the int32-encoding floor
|
| 205 |
+
assert max_abs_delta > 1e-4, "raw-data fit is identical to the published SI -- reflection gap missing"
|
| 206 |
|
| 207 |
|
| 208 |
# --------------------------------------------------------------------------- symmetrized-inference smoke test
|
|
|
|
| 215 |
|
| 216 |
@pytest.mark.skipif(not (os.path.exists(REAL_H5) and os.path.exists(REAL_XLSX) and _HAS_CHECKPOINTS),
|
| 217 |
reason="real delta22.hdf5 / experimental xlsx / model checkpoints not present")
|
| 218 |
+
def test_symmetrized_build_reproduces_published_si_and_differs_from_raw():
|
| 219 |
+
"""symmetrized=True re-derives the published SI tables from live, reflection-symmetrized
|
| 220 |
+
inference -- the same procedure that generated them -- so it reproduces them (within the
|
| 221 |
+
pass-to-pass inference noise of the small n_passes used here). It must also differ from the
|
| 222 |
+
checkpoint-free symmetrized=False raw fit, proving the override path actually ran and symmetrized
|
| 223 |
+
rather than silently falling back to the HDF5's stored single-pass shieldings."""
|
| 224 |
pytest.importorskip("torch")
|
| 225 |
+
sym = S.build_scaling_tables(REAL_H5, REAL_XLSX, symmetrized=True, n_passes=2)
|
| 226 |
+
raw = S.build_scaling_tables(REAL_H5, REAL_XLSX) # symmetrized=False
|
| 227 |
+
max_vs_raw = 0.0
|
| 228 |
for nucleus, published in (("H", PUBLISHED_S10_H), ("C", PUBLISHED_S11_C)):
|
| 229 |
+
table = sym[nucleus]
|
| 230 |
assert list(table.columns) == ["intercept", "stationary", "pcm"]
|
| 231 |
assert sorted(table.index) == sorted(published)
|
| 232 |
for solvent, (intercept, stationary, pcm) in published.items():
|
| 233 |
row = table.loc[solvent]
|
| 234 |
assert np.isfinite(row["intercept"]) and np.isfinite(row["stationary"]) and np.isfinite(row["pcm"])
|
| 235 |
+
# close to the published SI (loose: n_passes=2 here vs 10 for the shipped tables, so
|
| 236 |
+
# inference noise dominates the tiny reflection correction)
|
| 237 |
assert row["intercept"] == pytest.approx(intercept, abs=0.05), f"{nucleus} {solvent} int"
|
| 238 |
assert row["stationary"] == pytest.approx(stationary, abs=0.01), f"{nucleus} {solvent} stat"
|
| 239 |
assert row["pcm"] == pytest.approx(pcm, abs=0.1), f"{nucleus} {solvent} pcm"
|
| 240 |
+
r = raw[nucleus].loc[solvent]
|
| 241 |
+
max_vs_raw = max(max_vs_raw, abs(row["intercept"] - r["intercept"]),
|
| 242 |
+
abs(row["stationary"] - r["stationary"]), abs(row["pcm"] - r["pcm"]))
|
| 243 |
+
# the override path must actually symmetrize: if nn_shieldings_override_df were silently dropped,
|
| 244 |
+
# symmetrized=True would equal the symmetrized=False raw fit exactly
|
| 245 |
+
assert max_vs_raw > 1e-4, "symmetrized=True equals the raw unsymmetrized fit -- the override path did not run"
|
| 246 |
|
| 247 |
|
| 248 |
@pytest.mark.skipif(not (os.path.exists(REAL_H5) and os.path.exists(REAL_XLSX) and _HAS_CHECKPOINTS),
|
| 249 |
reason="real delta22.hdf5 / experimental xlsx / model checkpoints not present")
|
| 250 |
def test_shipped_symmetrized_csvs_reproduce_from_live_inference():
|
| 251 |
+
"""Value-traceability for the published SI tables shipped as CSVs in data/scaling_factors/: they
|
| 252 |
+
must reproduce from a live symmetrized build at the n_passes they were generated with (10), not
|
| 253 |
+
merely parse. A swapped column, wrong solvent order, or stale hand-edit of the shipped CSVs fails
|
| 254 |
+
here. The
|
| 255 |
tolerance absorbs the model's pass-to-pass inference noise (a single forward pass is not
|
| 256 |
deterministic) but is far tighter than any structural error."""
|
| 257 |
pytest.importorskip("torch")
|
analysis/si_tables/si_table_s10_s11_scaling.ipynb
CHANGED
|
@@ -2,19 +2,21 @@
|
|
| 2 |
"cells": [
|
| 3 |
{
|
| 4 |
"cell_type": "markdown",
|
| 5 |
-
"id": "
|
| 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": "
|
| 18 |
"metadata": {},
|
| 19 |
"outputs": [],
|
| 20 |
"source": [
|
|
@@ -22,7 +24,7 @@
|
|
| 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/
|
| 26 |
" \"analysis/code\", \"analysis/code/shared\"):\n",
|
| 27 |
" sys.path.insert(0, os.path.join(REPO, _p))"
|
| 28 |
]
|
|
@@ -30,14 +32,13 @@
|
|
| 30 |
{
|
| 31 |
"cell_type": "code",
|
| 32 |
"execution_count": null,
|
| 33 |
-
"id": "
|
| 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"
|
|
@@ -46,7 +47,7 @@
|
|
| 46 |
{
|
| 47 |
"cell_type": "code",
|
| 48 |
"execution_count": null,
|
| 49 |
-
"id": "
|
| 50 |
"metadata": {},
|
| 51 |
"outputs": [],
|
| 52 |
"source": [
|
|
@@ -61,7 +62,7 @@
|
|
| 61 |
{
|
| 62 |
"cell_type": "code",
|
| 63 |
"execution_count": null,
|
| 64 |
-
"id": "
|
| 65 |
"metadata": {},
|
| 66 |
"outputs": [],
|
| 67 |
"source": [
|
|
@@ -78,56 +79,37 @@
|
|
| 78 |
},
|
| 79 |
{
|
| 80 |
"cell_type": "markdown",
|
| 81 |
-
"id": "
|
| 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": "
|
| 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-
|
| 100 |
-
" assert max_dev <
|
| 101 |
-
"print(\"
|
| 102 |
-
]
|
| 103 |
-
},
|
| 104 |
-
{
|
| 105 |
-
"cell_type": "markdown",
|
| 106 |
-
"id": "14d0ed95",
|
| 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": "111de099",
|
| 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": "
|
| 131 |
"metadata": {},
|
| 132 |
"source": [
|
| 133 |
"## Composite-model coefficients (Figure 5C/5D, SI S15)\n",
|
|
@@ -140,7 +122,7 @@
|
|
| 140 |
{
|
| 141 |
"cell_type": "code",
|
| 142 |
"execution_count": null,
|
| 143 |
-
"id": "
|
| 144 |
"metadata": {},
|
| 145 |
"outputs": [],
|
| 146 |
"source": [
|
|
|
|
| 2 |
"cells": [
|
| 3 |
{
|
| 4 |
"cell_type": "markdown",
|
| 5 |
+
"id": "cec7d5d0",
|
| 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). The published coefficients are\n",
|
| 12 |
+
"reflection-symmetrized (each MagNET-Zero/PCM shielding averaged over twenty forward passes, the input\n",
|
| 13 |
+
"geometry mirrored for half), which removes the model's reflection-parity error."
|
| 14 |
]
|
| 15 |
},
|
| 16 |
{
|
| 17 |
"cell_type": "code",
|
| 18 |
"execution_count": null,
|
| 19 |
+
"id": "19128274",
|
| 20 |
"metadata": {},
|
| 21 |
"outputs": [],
|
| 22 |
"source": [
|
|
|
|
| 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\", \"data/applications\",\n",
|
| 28 |
" \"analysis/code\", \"analysis/code/shared\"):\n",
|
| 29 |
" sys.path.insert(0, os.path.join(REPO, _p))"
|
| 30 |
]
|
|
|
|
| 32 |
{
|
| 33 |
"cell_type": "code",
|
| 34 |
"execution_count": null,
|
| 35 |
+
"id": "3b8faf06",
|
| 36 |
"metadata": {},
|
| 37 |
"outputs": [],
|
| 38 |
"source": [
|
| 39 |
"import numpy as np\n",
|
| 40 |
"import pandas as pd\n",
|
| 41 |
"import scaling_factors\n",
|
|
|
|
| 42 |
"import build_composite_model\n",
|
| 43 |
"import paths\n",
|
| 44 |
"from applications_reader import Applications"
|
|
|
|
| 47 |
{
|
| 48 |
"cell_type": "code",
|
| 49 |
"execution_count": null,
|
| 50 |
+
"id": "083045eb",
|
| 51 |
"metadata": {},
|
| 52 |
"outputs": [],
|
| 53 |
"source": [
|
|
|
|
| 62 |
{
|
| 63 |
"cell_type": "code",
|
| 64 |
"execution_count": null,
|
| 65 |
+
"id": "c14ddb1c",
|
| 66 |
"metadata": {},
|
| 67 |
"outputs": [],
|
| 68 |
"source": [
|
|
|
|
| 79 |
},
|
| 80 |
{
|
| 81 |
"cell_type": "markdown",
|
| 82 |
+
"id": "96b870fe",
|
| 83 |
"metadata": {},
|
| 84 |
"source": [
|
| 85 |
+
"## Reproducibility check: re-derive from delta-22\n",
|
| 86 |
+
"\n",
|
| 87 |
+
"The published tables are reflection-symmetrized, so reproducing them exactly needs the model\n",
|
| 88 |
+
"checkpoints (`build_scaling_tables(symmetrized=True)`). The checkpoint-free\n",
|
| 89 |
+
"`build_scaling_tables(symmetrized=False)` below fits the raw single-pass shieldings stored in the\n",
|
| 90 |
+
"released delta-22 file and lands within ~0.01 ppm."
|
| 91 |
]
|
| 92 |
},
|
| 93 |
{
|
| 94 |
"cell_type": "code",
|
| 95 |
"execution_count": null,
|
| 96 |
+
"id": "2563a4e8",
|
| 97 |
"metadata": {},
|
| 98 |
"outputs": [],
|
| 99 |
"source": [
|
| 100 |
+
"derived = scaling_factors.build_scaling_tables(DELTA22, XLSX) # symmetrized=False (checkpoint-free)\n",
|
| 101 |
"for nucleus in (\"H\", \"C\"):\n",
|
| 102 |
" p = tables[nucleus]\n",
|
| 103 |
" d = derived[nucleus].reindex(p.index)[p.columns]\n",
|
| 104 |
" max_dev = float(np.abs(p.values - d.values).max())\n",
|
| 105 |
+
" print(f\"{nucleus}: largest published-vs-raw-refit deviation = {max_dev:.2e} ppm\")\n",
|
| 106 |
+
" assert max_dev < 1.5e-2, f\"{nucleus} raw refit drifted too far from the published values\"\n",
|
| 107 |
+
"print(\"raw delta-22 refit reproduces the published tables to the reflection-parity floor\")"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
]
|
| 109 |
},
|
| 110 |
{
|
| 111 |
"cell_type": "markdown",
|
| 112 |
+
"id": "3ab9ee49",
|
| 113 |
"metadata": {},
|
| 114 |
"source": [
|
| 115 |
"## Composite-model coefficients (Figure 5C/5D, SI S15)\n",
|
|
|
|
| 122 |
{
|
| 123 |
"cell_type": "code",
|
| 124 |
"execution_count": null,
|
| 125 |
+
"id": "7fb3fab2",
|
| 126 |
"metadata": {},
|
| 127 |
"outputs": [],
|
| 128 |
"source": [
|
data/scaling_factors/scaling_factors_reader.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
-
"""Reader for the
|
| 2 |
|
| 3 |
-
These two CSV files (one per nucleus)
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
tables use the unsymmetrized shieldings and live in analysis/code/scaling_factors.py.
|
| 8 |
|
| 9 |
Each row is one solvent; columns are intercept, stationary (the MagNET-Zero slope), and pcm (the
|
| 10 |
MagNET-PCM-correction slope). Predicted shift = intercept + stationary * shielding + pcm * correction.
|
|
|
|
| 1 |
+
"""Reader for the published MagNET-Zero/MagNET-PCM linear-scaling tables in this folder.
|
| 2 |
|
| 3 |
+
These two CSV files (one per nucleus) ARE Supporting Information Tables S10 (proton) and S11 (carbon),
|
| 4 |
+
verbatim: the reflection-symmetrized (n_passes=10, symmetrize=True) per-solvent coefficients that turn
|
| 5 |
+
a MagNET-Zero gas-phase shielding plus a MagNET-PCM chloroform correction into a predicted shift.
|
| 6 |
+
analysis/code/scaling_factors.py derives them; see its module docstring for the method.
|
|
|
|
| 7 |
|
| 8 |
Each row is one solvent; columns are intercept, stationary (the MagNET-Zero slope), and pcm (the
|
| 9 |
MagNET-PCM-correction slope). Predicted shift = intercept + stationary * shielding + pcm * correction.
|
magnet/scaling.py
CHANGED
|
@@ -19,35 +19,37 @@ import io
|
|
| 19 |
|
| 20 |
import numpy as np
|
| 21 |
|
| 22 |
-
# Published SI Tables S10 (proton) and S11 (carbon), verbatim
|
|
|
|
|
|
|
| 23 |
_PUBLISHED_TABLE_CSV = {
|
| 24 |
"H": """solvent,intercept,stationary,pcm
|
| 25 |
-
tetrahydrofuran,31.
|
| 26 |
-
dichloromethane,31.
|
| 27 |
-
chloroform,31.
|
| 28 |
-
toluene,31.
|
| 29 |
-
benzene,31.
|
| 30 |
-
chlorobenzene,31.
|
| 31 |
-
acetone,31.
|
| 32 |
-
dimethylsulfoxide,31.
|
| 33 |
-
acetonitrile,31.
|
| 34 |
-
trifluoroethanol,30.
|
| 35 |
-
methanol,31.
|
| 36 |
-
TIP4P,31.
|
| 37 |
""",
|
| 38 |
"C": """solvent,intercept,stationary,pcm
|
| 39 |
-
tetrahydrofuran,171.
|
| 40 |
-
dichloromethane,171.
|
| 41 |
-
chloroform,171.
|
| 42 |
-
toluene,171.
|
| 43 |
-
benzene,171.
|
| 44 |
-
chlorobenzene,171.
|
| 45 |
-
acetone,171.
|
| 46 |
-
dimethylsulfoxide,170.
|
| 47 |
-
acetonitrile,171.
|
| 48 |
-
trifluoroethanol,174.
|
| 49 |
-
methanol,172.
|
| 50 |
-
TIP4P,173.
|
| 51 |
""",
|
| 52 |
}
|
| 53 |
|
|
|
|
| 19 |
|
| 20 |
import numpy as np
|
| 21 |
|
| 22 |
+
# Published SI Tables S10 (proton) and S11 (carbon), verbatim (reflection-symmetrized: n_passes=10,
|
| 23 |
+
# symmetrize=True). Columns: intercept, stationary, pcm. Kept byte-for-byte in sync with
|
| 24 |
+
# data/scaling_factors/scaling_factors_symmetrized_{H,C}.csv by test_scaling_factors.py.
|
| 25 |
_PUBLISHED_TABLE_CSV = {
|
| 26 |
"H": """solvent,intercept,stationary,pcm
|
| 27 |
+
tetrahydrofuran,31.319664,-0.980082,-0.781373
|
| 28 |
+
dichloromethane,31.374485,-0.979760,-0.806215
|
| 29 |
+
chloroform,31.291983,-0.975682,-0.851715
|
| 30 |
+
toluene,31.714067,-0.996613,1.948607
|
| 31 |
+
benzene,31.984616,-1.005170,2.239317
|
| 32 |
+
chlorobenzene,31.677950,-0.993715,0.830908
|
| 33 |
+
acetone,31.510823,-0.987188,-1.229850
|
| 34 |
+
dimethylsulfoxide,31.597261,-0.991031,-1.348009
|
| 35 |
+
acetonitrile,31.494061,-0.985628,-0.969617
|
| 36 |
+
trifluoroethanol,30.873172,-0.959885,-0.955808
|
| 37 |
+
methanol,31.253627,-0.976349,-1.246680
|
| 38 |
+
TIP4P,31.356622,-0.978710,-1.475907
|
| 39 |
""",
|
| 40 |
"C": """solvent,intercept,stationary,pcm
|
| 41 |
+
tetrahydrofuran,171.052274,-0.918997,-1.069506
|
| 42 |
+
dichloromethane,171.507154,-0.921165,-1.115013
|
| 43 |
+
chloroform,171.726919,-0.924228,-0.936801
|
| 44 |
+
toluene,171.689276,-0.924983,-0.618407
|
| 45 |
+
benzene,171.965715,-0.927029,-0.593714
|
| 46 |
+
chlorobenzene,171.073820,-0.921235,-0.997638
|
| 47 |
+
acetone,171.305850,-0.919608,-1.239500
|
| 48 |
+
dimethylsulfoxide,170.596065,-0.918672,-1.296497
|
| 49 |
+
acetonitrile,171.877492,-0.922626,-1.287661
|
| 50 |
+
trifluoroethanol,174.235231,-0.939011,-1.290071
|
| 51 |
+
methanol,172.423799,-0.927563,-1.288843
|
| 52 |
+
TIP4P,173.692750,-0.937848,-1.342659
|
| 53 |
""",
|
| 54 |
}
|
| 55 |
|