Structure-Activity Relationship Wizard. A rapid-turnaround study of whether the descriptor target a molecular foundation model is pretrained against determines which downstream endpoints it serves best.
CheMeleon is a directed message-passing neural network (D-MPNN, Chemprop) pretrained self-supervised to regress a block of precomputed molecular descriptors from the molecular graph. SARizard trains several "flavors" of that foundation, each varying only the descriptor block it learns to predict (osmordred, RDKit descriptors, fingerprints, pharmacophores, 3D descriptors, and learned-model targets such as minimol embeddings and surrogate ADME predictions). Every flavor is pretrained separately on one shared corpus, never concatenated, then finetuned on real ADMET benchmark endpoints and evaluated. The result is a fit-to-purpose catalog: which foundational pretraining target lends itself to which endpoint and endpoint family.
- One shared corpus. A fixed 250K-molecule subset of the original CheMeleon PubChem corpus (downsampled once, single seed). Every flavor computes its target on these same molecules so the report-card columns are apples-to-apples.
- Pretrain per flavor. Each flavor pretrains a D-MPNN to regress its descriptor block under one fixed reduced regime (shared corpus, capped epochs, shared LR schedule). Continuous targets train with MSE, binary fingerprint targets with BCE.
- Convert. The pretrained message-passing block is exported to the checkpoint
format
openadmet-modelsconsumes viafrom_foundation. - Finetune per endpoint. For every flavor, each ADMET benchmark endpoint is
finetuned with
openadmet anvil, initialized from that flavor's foundation. - Evaluate and compare. Two artifacts: a pair of report-card heatmaps (endpoints by flavors) and a meta-model that stacks the per-flavor predictions per endpoint to test whether an ensemble of foundations beats the best single one. The R-squared card leads with the stock-CheMeleon baseline column, divided from the flavor block by a heavy rule; the MAE %-change card colors each flavor's change against that baseline, white where Dunnett's test does not separate the two. Each card is written as a PNG and as an equivalent HTML table. Endpoint rows are bracketed by source dataset, and the three ChEMBL-derived sources are named for the assay they measure (ChEMBL CLint, ChEMBL CYP, and, for the single hERG row, ChEMBL) rather than for a release number.
| Flavor | Pretraining target | dim | Loss | Target source |
|---|---|---|---|---|
| osmordred | osmordred descriptors | 3585 | MSE | direct compute (isolated env) |
| rdkit2d | RDKit 2D descriptors | 200 | MSE | direct compute |
| erg | ERG pharmacophore | 315 | MSE | direct compute |
| ecfp | ECFP bits | 2048 | BCE | direct compute |
| atompair | Atom-pair bits | 2048 | BCE | direct compute |
| pubchem | PubChem keys | 881 | BCE | direct compute |
| usrcat | USRCAT (3D) | 60 | MSE | direct compute (conformers) |
| whim | WHIM (3D) | 114 | MSE | direct compute (conformers) |
| e3fp | E3FP bits (3D) | 1024 | BCE | direct compute (conformers) |
| jazzy | hydration energy + H-bond strengths | 6 | MSE | direct compute (isolated env) |
| minimol | minimol embedding | 512 | MSE | learned model (isolated env) |
| surrogate_adme | 25 ADME predictions | 25 | MSE | released dataset (native corpus) |
Binary fingerprint targets are deterministic from the input graph, so they are a
leaky and likely weak pretext; their position on the report card is itself a result.
The 3D flavors require conformer generation (RDKit ETKDG + MMFF94) and their targets are
not bit-reproducible across runs (conformers are stochastic). The learned-model flavors generate their target by running the source model over the shared
corpus. surrogate_adme is the exception: the Novartis work ships a 273K-row prediction CSV
(CC BY 4.0) with 25 ADME values per molecule, so this flavor uses those molecules and labels
directly as its own pretraining corpus rather than predicting over the shared 250K set (see
sarizard/pretraining/features/surrogate_target.py).
Each calculator writes a plain cache/targets/<flavor>/target.npy in the flavor's own
environment, then a pack step in the main environment converts it to the chunked
cache/targets/<flavor>/target.zarr the trainer reads. This keeps zarr (and its Python
3.11+ requirement) out of the old, conflicting learned-model environments.
All Python code lives in the importable sarizard/ package; the repo root holds inputs,
recipes, and regenerable artifacts.
sarizard/is the package (pip install -e .); run modules from the repo root withpython -m sarizard.<sub>.<module>:sarizard/corpus/prepares the shared 250K SMILES subset from the original CheMeleon corpus.sarizard/pretraining/vendors and adaptshow-to-train-your-chemeleon: per-flavor target calculators infeatures/, the pretrain config,prescaling.py(the toggleable descriptor preprocessing and its ablation registry), andconvert_checkpoint.py.sarizard/configs/is the per-flavor recipe generator.sarizard/analysis/is the report card and meta-model;sarizard/analysis/paths.pyis the single source of truth for on-disk locations.
configs/_baseline/holds the committed stock-CheMeleon recipe templates; generated per-flavor recipes land inconfigs/<flavor>__s<seed>/(gitignored).corpus/,data/hold inputs;cache/,foundations/,results/,plots/hold regenerable artifacts (all gitignored;data/is committed, seedata/README.md).slurm/holds the sbatch job-array scripts for parallel pretraining and finetuning.wiki/is an Obsidian vault with detailed per-flavor and per-endpoint write-ups and figures.
The project targets Python 3.11+ and uses conda for environments (see AGENTS.md).
The fastest path on a cluster is setup.sh, which builds every environment from envs/*.yml,
installs SARizard into each (with --no-deps so the isolated envs' pinned stacks are left
intact), runs the test suite in the main env, and prints okay if everything passed:
bash setup.sh # all envs, then test
bash setup.sh main osmordred # restrict to specific envs (envs/<name>.yml basenames)
FORCE=1 bash setup.sh # recreate envs that already exist
SKIP_OSMORDRED_BUILD=1 bash setup.sh # skip the slow osmordred source buildTo set up only the main environment by hand:
conda env create -f envs/main.yml
conda activate sarizard
pip install -e ../openadmet-models
pip install -e .
Three target generators with conflicting stacks have isolated environments
(envs/osmordred.yml, envs/jazzy.yml, envs/minimol.yml). Each writes a plain target.npy to cache/ that the main
environment packs to zarr, so their dependencies never reach this training env. osmordred has
no package release and is built from source into its env with envs/build_osmordred.sh;
minimol needs the PyG extension wheels declared in its env file's find-links.
surrogate_adme runs in the main environment; it reads the Novartis released CSV directly
and requires no isolated env.
Finetuning and analysis run in a separate openadmet environment (envs/openadmet.yml, built
by setup.sh), not the main env: they drive the openadmet-models CLI, whose stack (pandas 2.x,
torch 2.7, Python 3.12) conflicts with the main training env (pandas 3.x, torch 2.12, Python
3.11). Point OPENADMET_ENV at it (default openadmet).
Build the environments first (bash setup.sh, see Setup), then:
# surrogate_adme needs the Novartis released CSV; the pipeline defaults to
# cache/surrogate/protacdb2.0_zinc_chembl_dataset.csv (see wiki/surrogate_adme.md to download).
# Only export SURROGATE_CSV to point somewhere else:
# export SURROGATE_CSV=/elsewhere/protacdb2.0_zinc_chembl_dataset.csv
# submit the full pipeline as a SLURM dependency chain and walk away
bash slurm/run_all.sh
# optional: pretrain each flavor at several seeds to average out initialization noise
FLAVOR_SEEDS="1 2 3" bash slurm/run_all.shrun_all.sh generates the per-(flavor, seed) finetuning configs, then submits six stages in
order (corpus preparation, target computation, split, pretraining, finetuning, analysis), each
gated by the previous stage completing without errors. Results land in results/ and plots/
when the final job finishes. FLAVOR_SEEDS (default one seed) pretrains each flavor at several
seeds, tagged <flavor>__s<seed>; the report card and meta-model average them back per flavor,
and re-running with more seeds fills in only the new ones. See slurm/README.md for the
per-stage scripts and how to resubmit after a partial failure.
After the sweep, bash slurm/run_lr_experiments.sh repeats the finetuning from the same
foundations with the MPNN backbone partially unfrozen (reduced) or fully unfrozen
(unlocked) and compares both against the frozen sweep in plots/lr_ranking_r2.csv.
Before the flavor sweep, decide how continuous descriptor targets are preprocessed. The
triage drives one representative flavor (osmordred) through every prescaling recipe in
sarizard/pretraining/prescaling.py with the backbone, corpus, and regime held fixed, then compares
downstream endpoint performance so the difference is the prescaling alone:
conda env create -f envs/osmordred.yml # the triage flavor's target environment
conda activate sarizard-osmordred && bash envs/build_osmordred.sh && conda deactivate
bash slurm/run_ablations.shRead plots/prescaling_ranking_r2.csv and the ablation report card to pick the
production recipe, then bake it into the core workflow before running the flavor sweep.
The scikit-fingerprints flavors (ecfp, rdkit2d, erg, atompair, pubchem, usrcat,
whim, e3fp) and surrogate_adme compute in the main sarizard env; only osmordred,
jazzy, and minimol have isolated envs. Compute the target in the flavor's env
(get_flavor(name).env), pack and train in the main env. Example with ecfp (main env
throughout) at the default seed 42:
conda activate sarizard
python -m sarizard.pretraining.features.compute_target --flavor ecfp
python -m sarizard.pretraining.features.pack_target --flavor ecfp
# split (seed-independent), then pretrain the foundation at seed 42
cd sarizard/pretraining
python split.py --input-zarr ../../cache/targets/ecfp/target.zarr \
--input-smiles ../../corpus/corpus_250k.parquet --outdir ../../cache/splits/ecfp \
--flavor ecfp --force
python train.py --flavor ecfp --input-dir ../../cache/splits/ecfp \
--output-dir runs/ecfp__s42 --foundation-name ecfp__s42_mp.pt --seed 42
cd ../..
# generate the seeded recipes and finetune one endpoint
python -m sarizard.configs.generate --flavors ecfp
openadmet anvil --recipe-path configs/ecfp__s42/cyp_mt.yaml --output-dir results/ecfp__s42/cyp_mt/An isolated flavor differs only in the compute step: conda activate sarizard-osmordred
(after bash envs/build_osmordred.sh) for compute_target --flavor osmordred, then switch
back to sarizard for pack, split, train, and finetune.
Datasets and the pretraining corpus are not redistributed; regenerate or obtain them and place them as described before training.
