Download analysis/code/build_composite_model.py from ekwan16/MagNET: direct link, hf CLI and curl.
- Browser
- Download file 15 kB
-
https://huggingface.co/ekwan16/MagNET/resolve/fe222d1894afb2d954fe8d2e67e70731a006c8d2/analysis/code/build_composite_model.py
- Command line
-
hf download hf://ekwan16/MagNET@fe222d1894afb2d954fe8d2e67e70731a006c8d2/analysis/code/build_composite_model.py
-
curl -L -o build_composite_model.py https://huggingface.co/ekwan16/MagNET/resolve/fe222d1894afb2d954fe8d2e67e70731a006c8d2/analysis/code/build_composite_model.py
15 kB
| """Regenerate the composite_model/ group in applications.hdf5 from the shipped delta-22 data. | |
| Replaces the old build_coefficients.py, which copied pre-fit CSVs out of a private tree via | |
| environment variables. Everything here is fit in-repo from delta22.hdf5, so the natural-products | |
| composite-model coefficients (Figure 5C/5D, SI S15) are reproducible from released inputs alone. | |
| The stored group's structure (formula keys, parameter order, solvent order, seed count) is read | |
| from the current applications.hdf5 and used as a template; only the numbers are regenerated. | |
| """ | |
| import os | |
| import random | |
| import sys | |
| import h5py | |
| import numpy as np | |
| import pandas as pd | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| REPO = os.path.abspath(os.path.join(HERE, "..", "..")) | |
| for _p in ("analysis/code", "analysis/code/shared", "data/delta22", "data/applications"): | |
| sys.path.insert(0, os.path.join(REPO, _p)) | |
| import delta22 as D # noqa: E402 | |
| import paths # noqa: E402 | |
| # The 22 delta-22 solutes in first-appearance order: the universe random.choices() draws from. | |
| # nitromethane stays in the draw pool (index 14) but its rows are dropped from the fit data, exactly | |
| # as the original pipeline did. | |
| SOLUTE_ORDER = ["AcOH", "EtOAc", "EtOH", "THF", "acetone", "acetonitrile", "benzaldehyde", "dmf", | |
| "dmso", "ethane", "ethylene", "glycol", "imidazole", "isopropanol", "nitromethane", | |
| "propylene", "pyridine", "pyrrole", "pyrrolidine", "tbme", "toluene", "triethylamine"] | |
| SEED_OFFSET = 100 | |
| BASE_FEATURES = ["experimental", "stationary", "qcd", "pcm", "desmond", "desmond_vib", | |
| "openMM", "openMM_vib"] | |
| ALIASES = {"des_vib": "desmond_vib", "op_vib": "openMM_vib"} | |
| def _load_tables(): | |
| """Three delta-22 NN tables (nitromethane rows removed) plus the pcm conversion factors: | |
| full_tab - every row; the OLS point fits use it and drop only each formula's own missing | |
| features, so pcm/qcd formulas fit all 12 solvents. | |
| fit_tab - strict: all base features present (only the 4 explicit-solvent solvents survive); | |
| the bootstrap resamples fit here, matching the stored bootstrap's 4 solvents. | |
| apply_tab - desmond or openMM present; the RMSE predictions are evaluated over it. | |
| """ | |
| h5 = paths.dataset_file("delta22", root=REPO) | |
| xlsx = os.path.join(REPO, "data", "delta22", "delta22_experimental.xlsx") | |
| nn = D.load_query_df_nn(h5, xlsx, verbose=False) | |
| dft = D.load_query_df_dft(h5, xlsx, verbose=False) | |
| factors = {"H": D.pcm_conversion_factors(dft, "H"), "C": D.pcm_conversion_factors(dft, "C")} | |
| nn = nn[nn["solute"] != "nitromethane"].copy() | |
| # stationary_plus_pcm folds in the per-solvent conversion factor; every other _plus_ column is a | |
| # plain sum, built on demand by _ensure_column. | |
| fac = nn.apply(lambda r: factors[r["nucleus"]].get(r["solvent"], np.nan), axis=1) | |
| nn["stationary_plus_pcm"] = nn["stationary"] + nn["pcm"] * fac | |
| full_tab = nn.reset_index(drop=True) | |
| fit_tab = nn.dropna(subset=BASE_FEATURES).reset_index(drop=True) | |
| apply_tab = nn.dropna(subset=["desmond", "openMM"], how="all").reset_index(drop=True) | |
| return full_tab, fit_tab, apply_tab, factors | |
| def _canonical(name): | |
| return ALIASES.get(name, name) | |
| def _regressor_columns(formula_key): | |
| """The design-matrix regressors for a formula key. '_AND_' separates independent regressors; | |
| a '_plus_' name is one precomputed sum column. Returns the list of column names to fit on.""" | |
| return [g for g in formula_key.split("_AND_")] | |
| def _plus_parts(regressor): | |
| """Base features a '_plus_' regressor decomposes into (canonical names); [] if it is itself a | |
| base feature.""" | |
| if "_plus_" not in regressor: | |
| return [] | |
| return [_canonical(p) for p in regressor.split("_plus_") if p] | |
| def _ensure_column(df, regressor): | |
| """Make sure a precomputed _plus_ regressor column exists on df (sum of its parts; the pcm part | |
| already carries the conversion factor via stationary_plus_pcm).""" | |
| if regressor in df.columns: | |
| return | |
| parts = regressor.split("_plus_") | |
| assert _canonical(parts[0]) == "stationary", f"unexpected base term in {regressor}" | |
| # build cumulatively so stationary_plus_pcm_plus_op_vib = stationary_plus_pcm + openMM_vib. The | |
| # pcm chains start from the scaled stationary_plus_pcm column; everything else from raw stationary. | |
| if parts[1:2] == ["pcm"]: | |
| col = df["stationary_plus_pcm"].to_numpy(float).copy() | |
| rest = parts[2:] | |
| else: | |
| col = df["stationary"].to_numpy(float).copy() | |
| rest = parts[1:] | |
| for p in rest: | |
| col = col + df[_canonical(p)].to_numpy(float) | |
| df[regressor] = col | |
| def _fit(df, regressors): | |
| """OLS of experimental on the given regressor columns (with intercept); returns {name: coeff} | |
| keyed by 'Intercept' and each regressor. NaN rows are dropped, matching statsmodels.""" | |
| y = pd.to_numeric(df["experimental"], errors="coerce").to_numpy(float) | |
| cols = [df[r].to_numpy(float) for r in regressors] | |
| X = np.column_stack([np.ones(len(df))] + cols) | |
| mask = np.isfinite(y) & np.all(np.isfinite(X), axis=1) | |
| beta, *_ = np.linalg.lstsq(X[mask], y[mask], rcond=None) | |
| return dict(zip(["Intercept"] + list(regressors), beta)) | |
| def _decompose(coeffs, regressors, solvent, nucleus, factors): | |
| """Expand each _plus_ regressor's coefficient onto its base features (same value copied). The pcm | |
| row is scaled by the per-solvent conversion factor ONLY when the formula used a | |
| stationary_plus_pcm term (a free _AND_pcm regressor stays raw). Returns {base_feature: coeff}.""" | |
| out = {"intercept": coeffs["Intercept"]} | |
| for reg, val in coeffs.items(): | |
| if reg == "Intercept": | |
| continue | |
| parts = _plus_parts(reg) | |
| for base in (parts if parts else [_canonical(reg)]): | |
| out[base] = val | |
| uses_spp = any(_plus_parts(r)[1:2] == ["pcm"] for r in regressors) | |
| if uses_spp and "pcm" in out: | |
| out["pcm"] = out["pcm"] * float(factors[nucleus].get(solvent, np.nan)) | |
| return out | |
| def _bootstrap_sample(fit_tab, nucleus, solvent, seed): | |
| """The rows fit for one bootstrap draw: seed the RNG with seed+100, draw 22 solutes with | |
| replacement from SOLUTE_ORDER, keep that solvent/nucleus's rows for the drawn solutes.""" | |
| random.seed(int(seed) + SEED_OFFSET) | |
| drawn = random.choices(SOLUTE_ORDER, k=22) | |
| return fit_tab[(fit_tab["nucleus"] == nucleus) & (fit_tab["solvent"] == solvent) | |
| & (fit_tab["solute"].isin(drawn))] | |
| def _rmse(fit_tab, apply_tab, display_formula, nucleus, solvent, seed, factors): | |
| """Bootstrap RMSE: fit the formula on one bootstrap draw, apply those raw regressor coefficients | |
| to every apply-table row for the solvent, and take the RMSE over labeled rows. The RMSE display | |
| names join regressors with ' + ' (each is a column, possibly a precomputed _plus_ one).""" | |
| regs = [t.strip() for t in display_formula.split(" + ")] | |
| for r in (fit_tab, apply_tab): | |
| for reg in regs: | |
| _ensure_column(r, reg) | |
| coeffs = _fit(_bootstrap_sample(fit_tab, nucleus, solvent, seed), regs) | |
| a = apply_tab[(apply_tab["nucleus"] == nucleus) & (apply_tab["solvent"] == solvent)] | |
| pred = coeffs["Intercept"] + sum(coeffs[r] * a[r].to_numpy(float) for r in regs) | |
| exp = pd.to_numeric(a["experimental"], errors="coerce").to_numpy(float) | |
| mask = np.isfinite(pred) & np.isfinite(exp) | |
| return float(np.sqrt(np.mean((pred[mask] - exp[mask]) ** 2))) | |
| def _pcm_table(factors, nucleus): | |
| """pcm_conversion_factors subgroup for one nucleus: solvent, pcm_conversion_factor. Stored under | |
| the 'water' name rather than the loader's 'TIP4P'.""" | |
| s = factors[nucleus].rename(index={"TIP4P": "water"}) | |
| return pd.DataFrame({"solvent": s.index, "pcm_conversion_factor": s.to_numpy()}) | |
| def regenerate(reader): | |
| """Regenerate every composite_model subgroup from delta-22, keyed and ordered to match the stored | |
| group. Returns {"ols_coefficients"|"bootstrap_coefficients"|"rmse_distributions"| | |
| "pcm_conversion_factors": {key: DataFrame}}.""" | |
| full_tab, fit_tab, apply_tab, factors = _load_tables() | |
| out = {k: {} for k in ("ols_coefficients", "bootstrap_coefficients", | |
| "rmse_distributions", "pcm_conversion_factors")} | |
| for f in reader.composite_formulas("ols_coefficients"): | |
| regs = _regressor_columns(f) | |
| for reg in regs: | |
| _ensure_column(full_tab, reg) | |
| for nuc in ("H", "C"): | |
| try: | |
| template = reader.ols_coefficients(f, nuc) | |
| except KeyError: | |
| continue | |
| params = template["parameter"].tolist() | |
| solvents = [c for c in template.columns if c != "parameter"] | |
| data = {"parameter": params} | |
| for solvent in solvents: | |
| sub = full_tab[(full_tab["nucleus"] == nuc) & (full_tab["solvent"] == solvent)] | |
| dec = _decompose(_fit(sub, regs), regs, solvent, nuc, factors) | |
| data[solvent] = [dec[p] for p in params] | |
| out["ols_coefficients"][(f, nuc)] = pd.DataFrame(data) | |
| for f in reader.composite_formulas("bootstrap_coefficients"): | |
| regs = _regressor_columns(f) | |
| for reg in regs: | |
| _ensure_column(fit_tab, reg) | |
| for nuc in ("H", "C"): | |
| try: | |
| template = reader.bootstrap_coefficients(f, nuc) | |
| except KeyError: | |
| continue | |
| params = [c for c in template.columns if c not in ("solvent", "seed")] | |
| rows = [] | |
| for solvent in dict.fromkeys(template["solvent"]): | |
| seeds = sorted(template[template["solvent"] == solvent]["seed"].unique()) | |
| for seed in seeds: | |
| dec = _decompose(_fit(_bootstrap_sample(fit_tab, nuc, solvent, seed), regs), | |
| regs, solvent, nuc, factors) | |
| row = {"solvent": solvent, "seed": seed} | |
| for p in params: | |
| row[p] = dec["intercept"] if p == "Intercept" else dec[p] | |
| rows.append(row) | |
| out["bootstrap_coefficients"][(f, nuc)] = pd.DataFrame(rows)[template.columns.tolist()] | |
| for nuc in ("H", "C"): | |
| template = reader.rmse_distribution(nuc) | |
| rows = [] | |
| for f in dict.fromkeys(template["formula"]): | |
| sub = template[template["formula"] == f] | |
| for solvent in dict.fromkeys(sub["solvent"]): | |
| for seed in sorted(sub[sub["solvent"] == solvent]["seed"].unique()): | |
| rows.append({"solvent": solvent, "nucleus": nuc, "formula": f, "seed": seed, | |
| "Bootstrap_RMSE": _rmse(fit_tab, apply_tab, f, nuc, solvent, seed, factors)}) | |
| out["rmse_distributions"][nuc] = pd.DataFrame(rows)[template.columns.tolist()] | |
| out["pcm_conversion_factors"][nuc] = _pcm_table(factors, nuc) | |
| return out | |
| def verify(reader, regenerated, atol=5e-3): | |
| """Compare regenerated numeric values against the stored group. Returns the global max abs diff. | |
| The floor is the fixed-point encoding granularity (~1e-3), far below any recipe error.""" | |
| worst = 0.0 | |
| for (f, nuc), df in regenerated["ols_coefficients"].items(): | |
| st = reader.ols_coefficients(f, nuc) | |
| worst = max(worst, np.abs(df.drop(columns="parameter").to_numpy(float) | |
| - st.drop(columns="parameter").to_numpy(float)).max()) | |
| for (f, nuc), df in regenerated["bootstrap_coefficients"].items(): | |
| st = reader.bootstrap_coefficients(f, nuc).sort_values(["solvent", "seed"]).reset_index(drop=True) | |
| df = df.sort_values(["solvent", "seed"]).reset_index(drop=True) | |
| num = [c for c in df.columns if c not in ("solvent",)] | |
| worst = max(worst, np.abs(df[num].to_numpy(float) - st[num].to_numpy(float)).max()) | |
| for nuc, df in regenerated["rmse_distributions"].items(): | |
| st = reader.rmse_distribution(nuc).sort_values(["formula", "solvent", "seed"]).reset_index(drop=True) | |
| df = df.sort_values(["formula", "solvent", "seed"]).reset_index(drop=True) | |
| worst = max(worst, np.abs(df["Bootstrap_RMSE"].to_numpy(float) | |
| - st["Bootstrap_RMSE"].to_numpy(float)).max()) | |
| for nuc, df in regenerated["pcm_conversion_factors"].items(): | |
| st = reader.pcm_conversion_factors(nuc).set_index("solvent")["pcm_conversion_factor"] | |
| got = df.set_index("solvent")["pcm_conversion_factor"] | |
| worst = max(worst, np.abs(got - st.reindex(got.index)).max()) | |
| return float(worst) | |
| _NOTE = ("Linear composite-model coefficients fit on the delta-22 dataset and applied to the " | |
| "natural products (Figure 5C/5D, SI S15). Stored as CSV text; read via applications_reader. " | |
| "Regenerated in-repo from delta22.hdf5 by build_composite_model.py.") | |
| def write(hdf5_path, regenerated): | |
| """Overwrite the composite_model/ group in applications.hdf5 with the regenerated CSV text.""" | |
| with h5py.File(hdf5_path, "a") as h: | |
| if "composite_model" in h: | |
| del h["composite_model"] | |
| cm = h.create_group("composite_model") | |
| cm.attrs["note"] = _NOTE | |
| for (formula, nucleus), df in regenerated["ols_coefficients"].items(): | |
| cm.require_group(f"ols_coefficients/{formula}").create_dataset(nucleus, data=df.to_csv(index=False)) | |
| for (formula, nucleus), df in regenerated["bootstrap_coefficients"].items(): | |
| cm.require_group(f"bootstrap_coefficients/{formula}").create_dataset(nucleus, data=df.to_csv(index=False)) | |
| for nucleus, df in regenerated["rmse_distributions"].items(): | |
| cm.require_group("rmse_distributions").create_dataset(nucleus, data=df.to_csv(index=False)) | |
| for nucleus, df in regenerated["pcm_conversion_factors"].items(): | |
| cm.require_group("pcm_conversion_factors").create_dataset(nucleus, data=df.to_csv(index=False)) | |
| def main(): | |
| import argparse | |
| ap = argparse.ArgumentParser(description="Regenerate composite_model/ in applications.hdf5 from delta-22.") | |
| ap.add_argument("--write", action="store_true", | |
| help="overwrite the composite_model group in applications.hdf5 (default: verify only)") | |
| args = ap.parse_args() | |
| sys.path.insert(0, os.path.join(REPO, "data", "applications")) | |
| from applications_reader import Applications | |
| hdf5_path = paths.dataset_file("applications", root=REPO) | |
| reader = Applications(hdf5_path) | |
| regenerated = regenerate(reader) | |
| max_diff = verify(reader, regenerated) | |
| print(f"composite_model regenerated from delta-22; max abs diff vs stored = {max_diff:.2e}") | |
| if args.write: | |
| write(hdf5_path, regenerated) | |
| print(f"wrote composite_model/ into {hdf5_path}") | |
| if __name__ == "__main__": | |
| main() | |