| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| from nb_build import md, code, save_notebook as _save |
|
|
|
|
| |
| _BOOTSTRAP = r""" |
| import os, sys |
| |
| # make the in-repo modules importable (not pip-installed) |
| REPO = os.path.abspath("../..") |
| for _p in ("data/delta22", "analysis/code", "analysis/code/shared"): |
| sys.path.insert(0, os.path.join(REPO, _p)) |
| """ |
|
|
| |
| _IMPORTS = r""" |
| import matplotlib.pyplot as plt |
| |
| import delta22 |
| import delta22_plots |
| import paths |
| """ |
|
|
| |
| _SETUP = r""" |
| DELTA22_HDF5 = paths.dataset_file("delta22", root=REPO) |
| XLSX = os.path.join(REPO, "data", "delta22", "delta22_experimental.xlsx") |
| |
| def figure_path(name): |
| os.makedirs("figures", exist_ok=True) |
| return os.path.join("figures", name) |
| """ |
|
|
| |
| _IMPORTS_TABLE = r""" |
| import delta22 |
| import paths |
| """ |
|
|
| |
| _IMPORTS_NP = _IMPORTS.replace("import matplotlib.pyplot as plt", |
| "import numpy as np\nimport matplotlib.pyplot as plt", 1) |
| |
| _IMPORTS_TABLE_NPPD = _IMPORTS_TABLE.replace("import delta22", |
| "import numpy as np\nimport pandas as pd\n\nimport delta22", 1) |
| _IMPORTS_TABLE_PD = _IMPORTS_TABLE.replace("import delta22", |
| "import pandas as pd\n\nimport delta22", 1) |
|
|
| |
| _SETUP_TABLE = r""" |
| DELTA22_HDF5 = paths.dataset_file("delta22", root=REPO) |
| XLSX = os.path.join(REPO, "data", "delta22", "delta22_experimental.xlsx") |
| |
| def document_path(name): |
| os.makedirs("documents", exist_ok=True) |
| return os.path.join("documents", name) |
| """ |
|
|
| |
| |
| _N_SPLITS_CELL = r""" |
| # the published panels use 250 seeded train/test splits |
| N_SPLITS = 250 |
| """ |
|
|
|
|
| |
| |
| |
|
|
|
|
| |
| |
| |
| _FIG4_LOAD = r""" |
| # Figure 4 uses one DFT method for the proton sites; differences are taken against a reference |
| # solvent, and the solvent_mean pseudo-solvent is added for the solvent-averaged reference. |
| METHOD, BASIS, GEOM = "b3lyp_d3bj", "pcSseg2", "aimnet2" |
| q = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False) |
| one = q[(q["sap_nmr_method"] == METHOD) & (q["sap_basis"] == BASIS) & (q["sap_geometry_type"] == GEOM)] |
| one = delta22.add_solvent_mean(one) |
| print(len(one), "rows for", METHOD, BASIS, GEOM) |
| """ |
|
|
| |
| _SOLVENT_LABELS = r""" |
| FIG4C_LABELS = { |
| "chloroform": "CDCl3", "dichloromethane": "DCM", "tetrahydrofuran": "THF", |
| "acetonitrile": "MeCN", "dimethylsulfoxide": "DMSO", "acetone": "acetone", |
| "methanol": "MeOH", "TIP4P": "TIP4P", "trifluoroethanol": "TFE", |
| "benzene": "benzene", "toluene": "toluene", "chlorobenzene": "chlorobenzene", |
| } |
| """ |
|
|
| |
| |
| |
| |
|
|
|
|
| si_figure_s16_s18 = [ |
| md(r""" |
| # SI Figures S16-S18: predicted vs experimental solvent-induced shifts by reference solvent |
| |
| Per solvent, implicit (PCM) and explicit (Desmond) predicted solvent-induced shift differences vs |
| measured (y=x ideal), for three reference solvents: **S16** chloroform, **S17** benzene, **S18** |
| solvent-averaged. One panel per solvent, each point a proton site. |
| """), |
| code(_BOOTSTRAP), |
| code(_IMPORTS), |
| code(_SETUP), |
| code(_FIG4_LOAD), |
| code(_SOLVENT_LABELS), |
| code(r""" |
| # panel order matches the canonical figures; a different order from delta22.DESMOND_SOLVENTS |
| SI_S16_18_SOLVENT_ORDER = [ |
| "chloroform", "tetrahydrofuran", "dichloromethane", "acetone", "acetonitrile", |
| "dimethylsulfoxide", "trifluoroethanol", "methanol", "TIP4P", "benzene", "toluene", "chlorobenzene", |
| ] |
| |
| # panel titles use the full solvent name; axis labels use the compact NMR abbreviations (THF, DCM, |
| # DMSO, TFE, CDCl3); the reference is CDCl3 / Benzene / "Avg Corr." for S16 / S17 / S18. |
| TITLE_LABEL = {"chloroform": "Chloroform", "tetrahydrofuran": "Tetrahydrofuran", |
| "dichloromethane": "Dichloromethane", "acetone": "Acetone", "acetonitrile": "Acetonitrile", |
| "dimethylsulfoxide": "Dimethylsulfoxide", "trifluoroethanol": "Trifluoroethanol", |
| "methanol": "Methanol", "TIP4P": "TIP4P", "benzene": "Benzene", "toluene": "Toluene", |
| "chlorobenzene": "Chlorobenzene"} |
| AXIS_LABEL = {**TITLE_LABEL, "chloroform": "CDCl3", "tetrahydrofuran": "THF", |
| "dichloromethane": "DCM", "dimethylsulfoxide": "DMSO", "trifluoroethanol": "TFE"} |
| REF_LABEL = {"chloroform": "CDCl3", "benzene": "Benzene", "solvent_mean": "Avg Corr."} |
| """), |
| code(r""" |
| for figure, reference in [("S16", "chloroform"), ("S17", "benzene"), ("S18", "solvent_mean")]: |
| sh = delta22.solvent_induced_shifts(one, reference, SI_S16_18_SOLVENT_ORDER, nucleus="H", explicit="desmond") |
| implicit_fit = delta22.fit_differences_to_experimental(sh, "implicit_diff") |
| explicit_fit = delta22.fit_differences_to_experimental(sh, "explicit_diff") |
| print(f"{figure}: reference={reference:12s} n={explicit_fit['n']:4d} " |
| f"implicit fit RMSE={implicit_fit['rmse']:.3f} explicit fit RMSE={explicit_fit['rmse']:.3f}") |
| panel_solvents = [s for s in SI_S16_18_SOLVENT_ORDER if s != reference] |
| delta22_plots.plot_shift_prediction_scatter_grid( |
| sh, panel_solvents, REF_LABEL[reference], AXIS_LABEL, TITLE_LABEL, |
| save_path=figure_path(f"si_figure_{figure.lower()}.png")) |
| plt.show() |
| """), |
| ] |
|
|
|
|
| |
| |
| |
| si_figure_s07 = [ |
| md(r""" |
| # SI Figure S7: DFT ¹³C explicit-solvent correction, Desmond vs OpenMM |
| |
| For the four OpenMM solvents, the DFT ¹³C explicit-solvent correction from Desmond MD vs from OpenMM |
| (y=x reference line). |
| """), |
| code(_BOOTSTRAP), |
| code(_IMPORTS_NP), |
| code(_SETUP), |
| code(r""" |
| dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False) |
| """), |
| md("## DFT explicit corrections: Desmond vs OpenMM"), |
| code(r""" |
| SOLVENT_ORDER = ["chloroform", "methanol", "TIP4P", "benzene"] |
| dft_pairs = {sv: delta22.explicit_correction_pairs(dft, sv, "C") for sv in SOLVENT_ORDER} |
| for sv, p in dft_pairs.items(): |
| print(f"DFT {sv:12s} n={len(p):3d} r={np.corrcoef(p['desmond'], p['openMM'])[0,1]:.3f}") |
| delta22_plots.plot_desmond_vs_openmm_grid(dft_pairs, save_path=figure_path("si_figure_s07.png")) |
| plt.show() |
| """), |
| ] |
|
|
|
|
| |
| |
| |
| si_figure_s04 = [ |
| md(r""" |
| # SI Figure S4: correlation of implicit (PCM) corrections across solvents and methods |
| |
| **S4A** correlation across solvents (both nuclei), **S4B** one solvent vs another (¹H), **S4C** |
| correlation across methods (both nuclei). Cells show -log10(1-r), so 3 means r=0.999. |
| """), |
| code(_BOOTSTRAP), |
| code(_IMPORTS_NP), |
| code(_SETUP), |
| code(r""" |
| METHOD, BASIS, GEOM = "b3lyp_d3bj", "pcSseg2", "pbe0_tz" |
| dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False) |
| base = dft[(dft["sap_nmr_method"] == METHOD) & (dft["sap_basis"] == BASIS) |
| & (dft["sap_geometry_type"] == GEOM)] |
| one = base[base["nucleus"] == "H"] |
| |
| # solvent order that groups polar-aprotic -> polar-protic -> aromatic (matches the published panel) |
| ORDERED_SOLVENTS = ["chloroform", "tetrahydrofuran", "dichloromethane", "acetone", "acetonitrile", |
| "dimethylsulfoxide", "trifluoroethanol", "methanol", "TIP4P", |
| "benzene", "toluene", "chlorobenzene"] |
| """), |
| md("## S4A: solvent-vs-solvent PCM correlation (¹H and ¹³C)"), |
| code(r""" |
| for nucleus, label in [("H", "1H"), ("C", "13C")]: |
| solvent_corr = delta22.correlation_matrix(base[base["nucleus"] == nucleus], "pcm", ["solute", "site"], "solvent") |
| solvent_corr = solvent_corr.reindex(index=ORDERED_SOLVENTS, columns=ORDERED_SOLVENTS) |
| vals = solvent_corr.values[np.triu_indices_from(solvent_corr.values, k=1)] |
| print(f"solvent PCM correlation ({label}): mean r={np.nanmean(vals):.4f} min r={np.nanmin(vals):.4f}") |
| caption = ("Correlation coefficients between solvents across all 22 solutes.\n" |
| f"PCM corrections computed with {METHOD} with the {BASIS} basis.\n" |
| "A value of 3 means the coefficient is 0.999.") |
| delta22_plots.plot_correlation_matrix(solvent_corr, f"Solvents are Highly Correlated ({nucleus})", caption, |
| colormap="Reds", show_values=True, |
| save_path=figure_path(f"si_figure_s04a_{label}.png")) |
| """), |
| md("## S4B: PCM corrections, chloroform vs acetonitrile (1H) -- one square of S4A"), |
| code(r""" |
| pair = one.pivot_table(index=["solute", "site"], columns="solvent", values="pcm")[ |
| ["chloroform", "acetonitrile"]].dropna() |
| delta22_plots.plot_pcm_scatter(pair["chloroform"], pair["acetonitrile"], "chloroform", "acetonitrile", "H", |
| save_path=figure_path("si_figure_s04b_1H.png")) |
| """), |
| md("## S4C: method-vs-method correlation (chloroform, double hybrids excluded, ¹H and ¹³C)"), |
| code(r""" |
| # one solvent, exclude the double hybrids whose PCM is the substituted reference value |
| for nucleus, label in [("H", "1H"), ("C", "13C")]: |
| chcl3 = dft[(dft["sap_basis"] == BASIS) & (dft["sap_geometry_type"] == GEOM) |
| & (dft["nucleus"] == nucleus) & (dft["solvent"] == "chloroform") |
| & (~dft["sap_nmr_method"].isin(delta22.DOUBLE_HYBRID_METHODS))] |
| method_corr = delta22.correlation_matrix(chcl3, "pcm", ["solute", "site"], "sap_nmr_method") |
| method_order = sorted(method_corr.index) # alphabetical, matches the published panel |
| method_corr = method_corr.reindex(index=method_order, columns=method_order) |
| mvals = method_corr.values[np.triu_indices_from(method_corr.values, k=1)] |
| print(f"method PCM correlation ({label}): mean r={np.nanmean(mvals):.4f} min r={np.nanmin(mvals):.4f}") |
| caption = ("Correlation coefficients between NMR methods across all 22 solutes.\n" |
| f"PCM corrections for chloroform with the {BASIS} basis.") |
| delta22_plots.plot_correlation_matrix(method_corr, f"NMR Methods are Highly Correlated ({nucleus})", caption, |
| colormap="Reds", show_values=True, |
| save_path=figure_path(f"si_figure_s04c_{label}.png")) |
| """), |
| ] |
|
|
|
|
| |
| |
| |
| _DFT_NN_LOAD = r""" |
| dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False) |
| nn = delta22.load_query_df_nn(DELTA22_HDF5, XLSX, verbose=False) |
| """ |
|
|
| si_figure_s11 = [ |
| md(r""" |
| # SI Figure S11: MagNET vs DFT rovibrational (QCD) corrections |
| |
| Four panels: **A** MagNET (NN) vs B3LYP/cc-pVDZ rovibrational (QCD) correction per site, both nuclei; |
| **B** the NN-minus-DFT error distribution; **C/D** every proton/carbon site's DFT and NN QCD |
| correction, one column per solute. (QCD is gas-phase, so no solvent axis.) |
| """), |
| code(_BOOTSTRAP), |
| code(_IMPORTS), |
| code(_SETUP), |
| code(_DFT_NN_LOAD), |
| md("## Panels A and B: MagNET vs DFT QCD correction, both nuclei"), |
| code(r""" |
| qcd = delta22.compare_dft_nn(dft, nn, "qcd", keys=("solute", "site", "nucleus")) |
| delta22_plots.plot_qcd_scatter(qcd, save_path=figure_path("si_figure_s11a_scatter.png")) |
| delta22_plots.plot_qcd_error_histogram(qcd, save_path=figure_path("si_figure_s11b_hist.png")) |
| plt.show() |
| """), |
| md("## Panel C: every proton site's QCD correction, one column per solute"), |
| code(r""" |
| delta22_plots.plot_qcd_correction_by_site(delta22.qcd_correction_by_site(dft, nn, nucleus="H"), "H", "upper left", |
| save_path=figure_path("si_figure_s11c_sites_1H.png")) |
| plt.show() |
| """), |
| md("## Panel D: every carbon site's QCD correction, one column per solute"), |
| code(r""" |
| delta22_plots.plot_qcd_correction_by_site(delta22.qcd_correction_by_site(dft, nn, nucleus="C"), "C", "upper right", |
| save_path=figure_path("si_figure_s11d_sites_13C.png")) |
| plt.show() |
| """), |
| ] |
|
|
| si_figure_s13 = [ |
| md(r""" |
| # SI Figure S13: MagNET-x vs DFT explicit-solvent corrections |
| |
| Four panels, both nuclei: **A** MagNET-x (NN) vs DFT explicit-solvent correction per site/solvent, |
| Desmond and OpenMM overlaid; **B** the NN-minus-DFT error distribution; **C** every site's DFT and NN |
| correction in chloroform, one column per solute; **D** the semi-parsimonious composite model's fit accuracy vs experiment across the four OpenMM solvents, split by engine and |
| DFT-vs-NN source. |
| """), |
| code(_BOOTSTRAP), |
| code(_IMPORTS), |
| code(_SETUP), |
| code(_DFT_NN_LOAD), |
| code(r""" |
| # openMM/Desmond engine hues (used by the scatter/histogram/site/fitting-accuracy panels below) |
| ENGINE_COLORS = {"openMM": "#2E86AB", "desmond": "#A23B72"} |
| ENGINE_LABELS = {"desmond": "Desmond", "openMM": "OpenMM"} |
| """), |
| md("## Panels A and B: MagNET-x vs DFT explicit corrections, Desmond and OpenMM overlaid"), |
| code(r""" |
| explicit_by_engine = delta22.compare_dft_nn_by_engine(dft, nn, keys=("solute", "site", "nucleus", "solvent")) |
| delta22_plots.plot_dft_nn_scatter_by_engine(explicit_by_engine, ENGINE_COLORS, ENGINE_LABELS, |
| save_path=figure_path("si_figure_s13a_scatter.png")) |
| delta22_plots.plot_dft_nn_error_histogram_by_engine(explicit_by_engine, ENGINE_COLORS, ENGINE_LABELS, |
| save_path=figure_path("si_figure_s13b_hist.png")) |
| plt.show() |
| """), |
| md("## Panel C: every proton/carbon site's explicit correction in chloroform"), |
| code(r""" |
| for nucleus, label in [("H", "Proton"), ("C", "Carbon")]: |
| pairs = delta22.explicit_correction_dft_nn_pairs(dft, nn, "chloroform", nucleus) |
| delta22_plots.plot_explicit_correction_by_site( |
| pairs, f"All {label} Sites: DFT vs NN Explicit Corrections (chloroform)", ENGINE_COLORS, |
| save_path=figure_path(f"si_figure_s13c_sites_{'1H' if nucleus == 'H' else '13C'}.png")) |
| plt.show() |
| """), |
| md("## Panel D: fitting accuracy of the semi-parsimonious composite model"), |
| code(_N_SPLITS_CELL), |
| code(r""" |
| SOLVENT_ORDER = ["chloroform", "methanol", "TIP4P", "benzene"] |
| solutes = sorted(dft["solute"].unique()) |
| for nucleus, label in [("H", "1H"), ("C", "13C")]: |
| fitting = delta22.si_s13d_fitting_accuracy(dft, nn, SOLVENT_ORDER, N_SPLITS, solutes, nucleus=nucleus) |
| delta22_plots.plot_fitting_accuracy_boxplot( |
| fitting, SOLVENT_ORDER, label, ENGINE_COLORS, |
| save_path=figure_path(f"si_figure_s13d_fitting_{'1H' if nucleus == 'H' else '13C'}.png")) |
| plt.show() |
| """), |
| ] |
|
|
|
|
| |
| |
| |
| si_figure_s05 = [ |
| md(r""" |
| # SI Figure S5: per-solvent PCM benefit vs bulk dielectric constant and polarizability |
| |
| Per-solvent PCM benefit (% test-RMSE reduction) vs bulk dielectric constant and polarizability, for |
| the MagNET-Zero reference method per nucleus (WP04 ¹H, wB97X-D ¹³C). Panels A (¹H) and B (¹³C) |
| show all solvents; C repeats ¹H excluding aromatics and trifluoroethanol. |
| """), |
| code(_BOOTSTRAP), |
| code(_IMPORTS), |
| code(_SETUP), |
| code(_N_SPLITS_CELL), |
| code(r""" |
| dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False) |
| solutes = sorted(dft["solute"].unique()) |
| benefit = {} |
| for nucleus, label in [("H", "1H"), ("C", "13C")]: |
| method = delta22.MAGNET_PCM_OUTPUT_METHODS[nucleus] |
| benefit[nucleus] = delta22.pcm_benefit_per_solvent(dft, method, "pcSseg2", "aimnet2", |
| delta22.DESMOND_SOLVENTS, n_splits=N_SPLITS, |
| solutes=solutes, nucleus=nucleus) |
| print(f"{label} ({method}):"); print(benefit[nucleus].round(1).to_string()) |
| """), |
| md("## S5A (1H, all solvents), S5B (13C, all solvents), and S5C (1H, excluding aromatics and trifluoroethanol)"), |
| code(r""" |
| # panels A (1H) and B (13C): all solvents |
| for nucleus, letter, nuc_label in [("H", "A", "H"), ("C", "B", "C")]: |
| delta22_plots.plot_pcm_benefit_vs_properties( |
| benefit[nucleus], delta22.SOLVENT_DIELECTRIC, delta22.SOLVENT_POLARIZABILITY, nuc_label, |
| save_path=figure_path(f"si_figure_s05{letter}_all.png")) |
| # panel C is 1H only, excluding the aromatic solvents and trifluoroethanol (matches the published SI) |
| delta22_plots.plot_pcm_benefit_vs_properties( |
| benefit["H"], delta22.SOLVENT_DIELECTRIC, delta22.SOLVENT_POLARIZABILITY, "H", |
| exclude=["benzene", "toluene", "chlorobenzene", "trifluoroethanol"], |
| title_extra="Excluding Aromatic Solvents and Trifluoroethanol", |
| save_path=figure_path("si_figure_s05A_no_aromatics_tfe.png")) |
| """), |
| ] |
|
|
|
|
| |
| |
| |
| si_figure_s08 = [ |
| md(r""" |
| # SI Figure S8 (panels B-D): explicit-solvent correction vs MD-frame count |
| |
| 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). |
| """), |
| code(_BOOTSTRAP), |
| code(r""" |
| import numpy as np |
| import matplotlib.pyplot as plt |
| |
| import delta22 |
| import delta22_reader |
| import delta22_plots |
| import paths |
| """), |
| code(_SETUP), |
| code(r""" |
| site_atoms = delta22_reader.load_site_atom_data(XLSX, verbose=False) |
| idx = delta22.site_atom_indices(site_atoms.loc[("AcOH", "H", "H"), "atom_numbers"]) |
| SOLUTE, SOLVENT = "AcOH", "chloroform" |
| """), |
| code(r""" |
| # OpenMM/Desmond engine hues; DFT vs NN reuse the OpenMM hue (DFT lightened, NN full strength) |
| ENGINE_COLORS = {"openMM": "#2E86AB", "desmond": "#A23B72"} |
| ENGINE_LABELS = {"openMM": "OpenMM", "desmond": "Desmond"} |
| |
| SOURCE_COLORS = {"dft": delta22_plots.lighten_color(ENGINE_COLORS["openMM"]), "nn": ENGINE_COLORS["openMM"]} |
| SOURCE_LABELS = {"dft": "DFT", "nn": "NN"} |
| LABEL_COLORS = {**ENGINE_COLORS, **SOURCE_COLORS} |
| LABEL_NAMES = {**ENGINE_LABELS, **SOURCE_LABELS} |
| """), |
| md(r""" |
| ## Panel B1/B2: OpenMM vs Desmond |
| |
| Running-average convergence (B1) and per-frame distribution (B2), comparing the two MD engines. |
| """), |
| code(r""" |
| running_by_engine, finals_by_engine, per_frame_by_engine, n_frames_by_engine = {}, {}, {}, {} |
| for engine in ["openMM", "desmond"]: |
| perturbed = delta22_reader.load_perturbed_shieldings(DELTA22_HDF5, SOLUTE, SOLVENT, engine, "dft") |
| per_frame = delta22.frame_corrections(perturbed, idx) |
| running = delta22.running_average(per_frame) |
| running_by_engine[engine] = running |
| finals_by_engine[engine] = running[~np.isnan(running)][-1] |
| per_frame_by_engine[engine] = per_frame |
| n_frames_by_engine[engine] = len(per_frame) # the total trajectory length, valid or not |
| print(f"{engine}: {int(np.sum(~np.isnan(per_frame)))} / {len(per_frame)} valid frames, " |
| f"converged correction {finals_by_engine[engine]:.4f} ppm") |
| |
| delta22_plots.plot_frame_convergence( |
| running_by_engine, finals_by_engine, LABEL_COLORS, LABEL_NAMES, |
| title=f"Convergence of Explicit Solvent Corrections\n({SOLUTE} in {SOLVENT})", |
| save_path=figure_path("si_figure_s08_b1_convergence_engine.png")) |
| plt.show() |
| |
| delta22_plots.plot_frame_correction_histogram( |
| per_frame_by_engine, n_frames_by_engine, LABEL_COLORS, LABEL_NAMES, |
| title="Distribution of Frame-wise Corrections", |
| save_path=figure_path("si_figure_s08_b2_histogram_engine.png")) |
| plt.show() |
| """), |
| md(r""" |
| ## Panel B3/B4: DFT vs NN |
| |
| Same running average (B3) and per-frame distribution (B4), at fixed OpenMM engine, comparing DFT vs |
| MagNET-x ("NN") shieldings. |
| """), |
| code(r""" |
| running_by_source, finals_by_source, per_frame_by_source, n_frames_by_source = {}, {}, {}, {} |
| for source in ["dft", "nn"]: |
| perturbed = delta22_reader.load_perturbed_shieldings(DELTA22_HDF5, SOLUTE, SOLVENT, "openMM", source) |
| per_frame = delta22.frame_corrections(perturbed, idx) |
| running = delta22.running_average(per_frame) |
| running_by_source[source] = running |
| finals_by_source[source] = running[~np.isnan(running)][-1] |
| per_frame_by_source[source] = per_frame |
| n_frames_by_source[source] = len(per_frame) |
| print(f"{source}: {int(np.sum(~np.isnan(per_frame)))} / {len(per_frame)} valid frames, " |
| f"converged correction {finals_by_source[source]:.4f} ppm") |
| |
| delta22_plots.plot_frame_convergence( |
| running_by_source, finals_by_source, LABEL_COLORS, LABEL_NAMES, xlabel="Number of MD Frames", |
| title=f"OpenMM DFT vs. NN Convergence Comparison\n({SOLUTE} in {SOLVENT})", |
| save_path=figure_path("si_figure_s08_b3_convergence_source.png")) |
| plt.show() |
| |
| delta22_plots.plot_frame_correction_histogram( |
| per_frame_by_source, n_frames_by_source, LABEL_COLORS, LABEL_NAMES, |
| title="Distribution of Frame-wise Corrections", |
| save_path=figure_path("si_figure_s08_b4_histogram_source.png")) |
| plt.show() |
| """), |
| md(r""" |
| ## Panel C: autocorrelation, DFT vs NN |
| |
| Autocorrelation of the per-frame correction to lag 200 (~100 frames per trajectory repeat), DFT vs NN, |
| same OpenMM trajectory as B3/B4. |
| """), |
| code(r""" |
| autocorr_by_source = {source: delta22.autocorrelation(per_frame_by_source[source], max_lag=200) |
| for source in ["dft", "nn"]} |
| for source, autocorr in autocorr_by_source.items(): |
| print(f"{source}: lag-1 autocorrelation {autocorr[1]:.3f}") |
| |
| delta22_plots.plot_frame_autocorrelation( |
| autocorr_by_source, LABEL_COLORS, LABEL_NAMES, |
| title=f"Autocorrelation of Frame-wise Corrections\n({SOLUTE} in {SOLVENT})", |
| save_path=figure_path("si_figure_s08_c_autocorrelation.png")) |
| plt.show() |
| """), |
| md(r""" |
| ## Panel D: frame data availability |
| |
| Which trajectory frames have computed DFT shielding for AcOH, across all 12 Desmond and 4 OpenMM |
| solvents. DFT was computed for a non-contiguous subset (compute-budget limits; jobs queued randomly). |
| """), |
| code(r""" |
| # row order matching the published panel: chloroform first, then by solvent class (aprotic -> |
| # protic -> aromatic); OpenMM only has the four explicit-solvent solvents |
| DESMOND_ORDER = ["chloroform", "tetrahydrofuran", "dichloromethane", "acetone", "acetonitrile", |
| "dimethylsulfoxide", "trifluoroethanol", "methanol", "TIP4P", |
| "benzene", "toluene", "chlorobenzene"] |
| OPENMM_ORDER = ["chloroform", "methanol", "TIP4P", "benzene"] |
| |
| grids_by_engine, solvents_by_engine = {}, {} |
| for engine, solvents in [("desmond", DESMOND_ORDER), ("openMM", OPENMM_ORDER)]: |
| grid, frame_counts = delta22.frame_validity_grid(DELTA22_HDF5, SOLUTE, solvents, engine, shield_type="dft") |
| grids_by_engine[engine] = grid |
| solvents_by_engine[engine] = [delta22_plots.display_solvent_name(s) for s in solvents] |
| print(f"{engine}: {len(solvents)} solvents, frame counts {dict(zip(solvents, frame_counts))}") |
| |
| delta22_plots.plot_frame_validity_heatmaps(grids_by_engine, solvents_by_engine, ENGINE_LABELS, solute=SOLUTE, |
| save_path=figure_path("si_figure_s08_d_frame_validity.png")) |
| plt.show() |
| """), |
| ] |
|
|
|
|
| |
| |
| |
| _PARETO_LOAD = r""" |
| # Load the flat DFT and MagNET tables and the timing tables. |
| dft = delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False) |
| nn = delta22.load_query_df_nn(DELTA22_HDF5, XLSX, verbose=False) |
| dft_gas_timings, nn_timings = delta22.load_pareto_timings(DELTA22_HDF5) |
| |
| # One point per method/basis/geometry/nucleus/solvent, plus the solvent-averaged rows, each with its |
| # mean test RMSE and total compute time. MagNET appears as a single method (aimnet2, basis "N/A"). |
| # Figure 2A uses 100 seeded splits, not the 250 the other delta-22 panels use, and trains on the |
| # first 10 shuffled solutes / tests on the rest; fig2a_pareto_points handles that split convention. |
| PARETO_N_SPLITS = 100 |
| points = delta22.fig2a_pareto_points(dft, nn, dft_gas_timings, nn_timings, n_splits=PARETO_N_SPLITS) |
| print(points["nmr_method"].nunique(), "methods;", |
| "MagNET total time", float(points.query("nmr_method=='MagNET'")["total_time"].iloc[0]), "s") |
| """ |
|
|
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
| si_table_s01_s02_pareto = [ |
| md(r""" |
| # SI Tables S1 and S2: the Pareto plot data |
| |
| Each method's total compute time and CDCl3 test RMSE, for proton (S1) and carbon (S2): a curated |
| 55-row subset (MagNET, one AIMNet2-geometry row, and the DFT grid on PBE0/cc-pVTZ geometries). |
| """), |
| code(_BOOTSTRAP), |
| code(_IMPORTS_TABLE_NPPD), |
| code(_SETUP_TABLE), |
| code(_PARETO_LOAD), |
| md("## Tables S1 and S2"), |
| code(r""" |
| # full column set matching the SI tables: identity + fitting RMSE + the three time components and |
| # their log10 (all solvents here are chloroform). geometry_time and nmr_time sum to total_time. |
| COLUMNS = ["geometry_type", "nmr_method", "basis", "solvent", "fitting_RMSE", |
| "geometry_time", "nmr_time", "total_time", "log10_total_time"] |
| |
| curated = {} |
| for nucleus, label in [("H", "S1"), ("C", "S2")]: |
| sub = delta22.pareto_table_curated(points, nucleus).copy() |
| sub["log10_total_time"] = np.log10(sub["total_time"]) |
| sub = sub[COLUMNS] |
| curated[label] = sub |
| print(f"Table {label} ({nucleus}): {len(sub)} rows x {sub.shape[1]} columns") |
| print(sub.to_string(index=False), "\n") |
| |
| # write the two tables to this notebook's documents/ folder, one sheet per SI table |
| out = document_path("si_table_s01_s02_pareto.xlsx") |
| with pd.ExcelWriter(out) as writer: |
| curated["S1"].to_excel(writer, sheet_name="Table S1 (1H)", index=False) |
| curated["S2"].to_excel(writer, sheet_name="Table S2 (13C)", index=False) |
| print("wrote", os.path.relpath(out, REPO)) |
| """), |
| ] |
|
|
|
|
| |
| |
| |
| si_figure_s06 = [ |
| md(r""" |
| # SI Figure S6: per-solvent test RMSE of six solvent-correction models |
| |
| Per nucleus, the per-solvent test RMSE of six solvent-correction models: SotA implicit (single-slope |
| PCM), the paper's implicit fit (stationary + PCM as separate terms), and explicit (Desmond), each |
| with and without a vibrational correction (QCD for ¹H, Desmond vibration for ¹³C). Level of theory: |
| dsd_pbep86/pcSseg3 on pbe0_tz geometries, PCM substituted from b3lyp_d3bj/pcSseg3. |
| """), |
| code(_BOOTSTRAP), |
| code(_IMPORTS), |
| code(_SETUP), |
| code(r""" |
| query = delta22.add_composite_columns(delta22.load_query_df_dft( |
| DELTA22_HDF5, XLSX, pcm_reference_method="b3lyp_d3bj", pcm_reference_basis="pcSseg3", verbose=False)) |
| solutes = sorted(query["solute"].unique()) |
| |
| METHOD, BASIS, GEOM = "dsd_pbep86", "pcSseg3", "pbe0_tz" |
| |
| # same six categories for both nuclei; the "+ Vibrations" term differs (qcd for H, desmond_vib for C) |
| LADDER = { |
| "H": { |
| "stationary_plus_pcm": "SotA Implicit Solvent", |
| "stationary_plus_pcm_plus_qcd": "SotA Implicit Solvent + Vibrations", |
| "stationary + pcm": "Implicit Solvent", |
| "stationary_plus_qcd + pcm": "Implicit Solvent + Vibrations", |
| "stationary + desmond": "Explicit Solvent", |
| "stationary_plus_qcd + desmond": "Explicit Solvent + Vibrations"}, |
| "C": { |
| "stationary_plus_pcm": "SotA Implicit Solvent", |
| "stationary_plus_pcm_plus_des_vib": "SotA Implicit Solvent + Vibrations", |
| "stationary + pcm": "Implicit Solvent", |
| "stationary_plus_des_vib + pcm": "Implicit Solvent + Vibrations", |
| "stationary + desmond": "Explicit Solvent", |
| "stationary_plus_des_vib + desmond": "Explicit Solvent + Vibrations"}, |
| } |
| """), |
| code(r""" |
| # one dark/light pair per solvent-treatment family (SotA implicit / implicit / explicit), light = +Vibrations |
| S6_COLORS = ["#C4B037", "#F5EDA0", "#A72608", "#E4A0A0", "#5F7C8A", "#B9E7DF"] |
| """), |
| md("## Formula ladder per nucleus"), |
| code(_N_SPLITS_CELL), |
| code(r""" |
| for nucleus, labels in LADDER.items(): |
| results = delta22.fig3d_formula_regressions(query, METHOD, BASIS, GEOM, list(labels), |
| delta22.DESMOND_SOLVENTS, n_splits=N_SPLITS, |
| solutes=solutes, nucleus=nucleus) |
| med = results.groupby("formula")["test_RMSE"].median() |
| print(f"--- {nucleus} ({METHOD}) median test RMSE ---") |
| for formula in labels: |
| print(f" {labels[formula]:38s} {med[formula]:.4f}") |
| delta22_plots.plot_formula_ladder_boxplot( |
| results, labels, delta22.SOLVENT_GROUPS, nucleus=nucleus, colors=S6_COLORS, |
| title=f"Explicit Solvation + Vibrational Effects Improve Predictions Across Solvents ({nucleus} Nucleus)", |
| save_path=figure_path(f"si_figure_s06_ladder_{'1H' if nucleus == 'H' else '13C'}.png")) |
| plt.show() |
| """), |
| ] |
|
|
|
|
| |
| |
| |
| |
| _S6_SOLVENT_ORDER = [ |
| "tetrahydrofuran", "dichloromethane", "acetone", "acetonitrile", "dimethylsulfoxide", |
| "trifluoroethanol", "methanol", "TIP4P", "benzene", "toluene", "chlorobenzene", |
| ] |
| si_table_s06 = [ |
| md(r""" |
| # SI Table S6: per-solvent implicit vs explicit fit RMSE (chloroform reference, ¹H) |
| |
| Per solvent (chloroform reference), fit RMSE of experimental solvent-induced ¹H shifts using implicit |
| (PCM) vs explicit (Desmond), and the percent improvement. |
| """), |
| code(_BOOTSTRAP), |
| code(_IMPORTS_TABLE_PD), |
| code(_SETUP_TABLE), |
| code(_FIG4_LOAD), |
| md("## Table S6: per-solvent implicit vs explicit fit RMSE (chloroform reference, 1H)"), |
| code(r""" |
| rows = [] |
| for solvent in _S6_SOLVENT_ORDER: |
| sp = delta22.solvent_pair_differences(one, solvent, "chloroform", nucleus="H", explicit="desmond") |
| implicit = delta22.fit_differences_to_experimental(sp, "implicit_diff")["rmse"] |
| explicit = delta22.fit_differences_to_experimental(sp, "explicit_diff")["rmse"] |
| rows.append({"solvent": solvent, |
| "implicit_fitting_rmse": round(implicit, 4), |
| "explicit_fitting_rmse": round(explicit, 4), |
| "explicit_improvement_pct": round(100.0 * (implicit - explicit) / implicit, 2)}) |
| table_s6 = pd.DataFrame(rows) |
| print(table_s6.to_string(index=False)) |
| |
| # write the table to this notebook's documents/ folder |
| out = document_path("si_table_s06_solvent_corrections.xlsx") |
| with pd.ExcelWriter(out) as writer: |
| table_s6.to_excel(writer, sheet_name="Table S6", index=False) |
| print("wrote", os.path.relpath(out, REPO)) |
| """.replace("_S6_SOLVENT_ORDER", repr(_S6_SOLVENT_ORDER))), |
| ] |
|
|
|
|
| |
| |
| |
| si_figure_s14 = [ |
| md(r""" |
| # SI Figure S14: test RMSE with DFT vs MagNET features, by solvent |
| |
| For the four explicit-solvent solvents (chloroform, methanol, TIP4P water, benzene), per-solvent test |
| RMSE of predicting experimental ¹H/¹³C shifts with **DFT** features (solid) vs end-to-end **MagNET/NN** |
| features (lightened), each under three conditions (implicit SOTA, explicit, explicit + vibrations). |
| Two pages, one per MD engine (Desmond, OpenMM); nitromethane dropped. |
| """), |
| code(_BOOTSTRAP), |
| code(_IMPORTS), |
| code(_SETUP), |
| code(_N_SPLITS_CELL), |
| code(r""" |
| dft = delta22.add_composite_columns(delta22.load_query_df_dft(DELTA22_HDF5, XLSX, verbose=False)) |
| nn = delta22.add_composite_columns(delta22.load_query_df_nn(DELTA22_HDF5, XLSX, verbose=False)) |
| print(len(dft), "DFT rows;", len(nn), "NN rows") |
| |
| LABELS = ["Implicit Solvent (SOTA)", "Explicit Solvent", "Explicit Solvent + Vibrations"] |
| SS_LABELS = {"TIP4P": "Water (TIP4P)"} |
| S14_SOLVENTS = ["chloroform", "methanol", "TIP4P", "benzene"] # the 4 explicit solvents, SI order |
| """), |
| md("## Desmond page (DFT vs NN)"), |
| code(r""" |
| # implicit = 2-term (stationary + pcm); explicit = stationary + desmond; |
| # explicit + vibrations = stationary_plus_qcd + desmond (1H) / stationary_plus_des_vib + desmond (13C) |
| DESMOND_FORMULAS = { |
| "H": ["stationary + pcm", "stationary + desmond", "stationary_plus_qcd + desmond"], |
| "C": ["stationary + pcm", "stationary + desmond", "stationary_plus_des_vib + desmond"], |
| } |
| for nucleus, label in [("H", "1H"), ("C", "13C")]: |
| formulas = DESMOND_FORMULAS[nucleus] |
| delta22_plots.plot_ss_boxplot_dft_vs_nn( |
| delta22_plots.ss_fits(dft, nucleus, formulas, S14_SOLVENTS, N_SPLITS, dft=True), |
| delta22_plots.ss_fits(nn, nucleus, formulas, S14_SOLVENTS, N_SPLITS), |
| S14_SOLVENTS, formulas, LABELS, label, solvent_labels=SS_LABELS, |
| save_path=figure_path(f"si_figure_s14_desmond_{label}.png")) |
| plt.show() |
| """), |
| md("## OpenMM page (DFT vs NN)"), |
| code(r""" |
| # implicit = 1-term composite (stationary_plus_pcm); explicit = stationary + openMM; |
| # explicit + vibrations = stationary_plus_qcd + openMM (1H) / stationary_plus_op_vib + openMM (13C) |
| OPENMM_FORMULAS = { |
| "H": ["stationary_plus_pcm", "stationary + openMM", "stationary_plus_qcd + openMM"], |
| "C": ["stationary_plus_pcm", "stationary + openMM", "stationary_plus_op_vib + openMM"], |
| } |
| for nucleus, label in [("H", "1H"), ("C", "13C")]: |
| formulas = OPENMM_FORMULAS[nucleus] |
| delta22_plots.plot_ss_boxplot_dft_vs_nn( |
| delta22_plots.ss_fits(dft, nucleus, formulas, S14_SOLVENTS, N_SPLITS, dft=True), |
| delta22_plots.ss_fits(nn, nucleus, formulas, S14_SOLVENTS, N_SPLITS), |
| S14_SOLVENTS, formulas, LABELS, label, solvent_labels=SS_LABELS, |
| save_path=figure_path(f"si_figure_s14_openmm_{label}.png")) |
| plt.show() |
| """), |
| ] |
|
|
|
|
| |
| |
| NOTEBOOKS = { |
| "si_figure_s06": (si_figure_s06, "analysis/si_figures/si_figure_s06.ipynb"), |
| "si_figure_s14": (si_figure_s14, "analysis/si_figures/si_figure_s14.ipynb"), |
| "si_table_s06": (si_table_s06, "analysis/si_tables/si_table_s06_solvent_corrections.ipynb"), |
| "si_table_s01_s02_pareto": (si_table_s01_s02_pareto, "analysis/si_tables/si_table_s01_s02_pareto.ipynb"), |
| "si_figure_s16_s18": (si_figure_s16_s18, "analysis/si_figures/si_figure_s16_s18.ipynb"), |
| "si_figure_s07": (si_figure_s07, "analysis/si_figures/si_figure_s07.ipynb"), |
| "si_figure_s04": (si_figure_s04, "analysis/si_figures/si_figure_s04.ipynb"), |
| "si_figure_s11": (si_figure_s11, "analysis/si_figures/si_figure_s11.ipynb"), |
| "si_figure_s13": (si_figure_s13, "analysis/si_figures/si_figure_s13.ipynb"), |
| "si_figure_s05": (si_figure_s05, "analysis/si_figures/si_figure_s05.ipynb"), |
| "si_figure_s08": (si_figure_s08, "analysis/si_figures/si_figure_s08_panelsBCD.ipynb"), |
| } |
|
|
| if __name__ == "__main__": |
| import os |
| import sys |
| here = os.path.dirname(os.path.abspath(__file__)) |
| repo = os.path.abspath(os.path.join(here, "..", "..")) |
| names = sys.argv[1:] |
| if not names: |
| print("usage: python3 build_nb_delta22.py <name> [<name> ...]") |
| print("regenerates ONLY the named notebook(s) -- pick just the one(s) you edited,") |
| print("since regenerating clears a notebook's execution outputs.") |
| print("available names:", ", ".join(NOTEBOOKS)) |
| raise SystemExit(1) |
| unknown = [n for n in names if n not in NOTEBOOKS] |
| if unknown: |
| raise SystemExit(f"unknown notebook name(s): {unknown}; available: {', '.join(NOTEBOOKS)}") |
| for name in names: |
| cells, relpath = NOTEBOOKS[name] |
| _save(cells, os.path.join(repo, relpath)) |
|
|