This is Pipeline 2 of the repo (see the top-level README for the conceptual map). It takes the curated dataset produced by data_extraction/, computes per-monomer descriptors, trains the architecture-classification model, and serves predictions via a FastAPI service.
processed_data.csv ← dataset (4,969 measurement rows,
▲ 3,791 unique reactions, 1,206 papers)
│
│ ↓ monomer_feature_calculation.py (XTB descriptors)
│ ↓ create_data_split.py (stratified by monomer pair)
▼
artifacts/data_splits/{train,val,test}.csv
│
│ ↓ train_final_model.py (XGBoost + calibration)
▼
artifacts/model_bundle/ ← the released model
│
│ ↓ api/app.py (FastAPI service)
▼
POST /predict, /preprocess_all, /optimize_reaction, ← what powers
/find_architecture_switch, /paper_metrics, ... polycarp.cheminfo.org
artifacts/model_bundle/ + artifacts/data_splits/
│
│ ↓ reproduce_paper_metrics.py ← regression check
▼
artifacts/paper_metrics.json + REPRODUCED on stdout
The classifier predicts one of three architecture classes, derived from the reactivity-ratio product r₁·r₂:
| index | name | rule | meaning |
|---|---|---|---|
| 0 | alternating |
r₁·r₂ < 1 |
monomers prefer the comonomer over self-addition |
| 1 | random |
1 ≤ r₁·r₂ ≤ 25 |
catch-all (covers azeotropic, mildly-alternating, and mildly-blocky systems) |
| 2 | gradient |
r₁·r₂ > 25 |
monomers prefer self-addition; composition drift along the chain |
The canonical mapping lives in api/class_labels.py and is imported everywhere the names appear — single source of truth.
Two architecture-prediction models cooperate:
- XGBoost on the XTB-derived feature vector — the classifier proper.
- Lookup: the architecture of the top-1 nearest-neighbour literature reaction (Tanimoto on Morgan fingerprints of the monomer pair + solvent).
The voting layer keeps only predictions where the two agree (the "coverage" metric in the paper). Disagreement is exposed on every /preprocess_all response so the web UI can flag low-confidence cases.
copol_prediction/
├── processed_data.csv ← live dataset (the API reads this for the
│ nearest-neighbour lookup pool)
│
├── create_data_split.py ← monomer-pair-stratified 70/10/20 split
├── train_final_model.py ← XGBoost training + calibration
├── monomer_feature_calculation.py ← XTB descriptor pipeline
├── mayo_lewis_classification.py ← r1·r2 → class assignment
├── reproduce_paper_metrics.py ← canonical regression check
├── preprocess_splits_full_features.py ← splits with full feature set (perm. importance)
├── REPRODUCE.md ← reproduction recipe details
│
├── analysis/ ← paper figures (plot_model_figure, plot_class_curves)
├── api/ ← FastAPI service deployed at polycarp.cheminfo.org
├── filter/ ← curation + augmented-negatives pool
├── utils/ ← load_data_split helpers
│
└── artifacts/
├── model_bundle/ ← XGBoost model + calibration + metadata
├── data_splits/ ← train/val/test (6,774 rows / 3,387 reactions)
├── data_splits_full_features/ ← splits with descriptors for permutation analysis
└── paper_metrics.json ← cached output of reproduce_paper_metrics.py
served unmodified by GET /paper_metrics
python reproduce_paper_metrics.pyLoads artifacts/model_bundle/ + artifacts/data_splits/, evaluates plain XGBoost and the voting model on both splits, asserts every cell of the paper's table reproduces within ±0.005. Exits non-zero on drift. Also the basis of tests/test_api_parity.py::test_paper_metrics_endpoint.
python create_data_split.pyReads processed_data.csv, applies the paper filter (r₁·r₂ ≥ 0, r₁·r₂ not null, drop rows with NaN features), stratifies by monomer pair (frozenset({canon(m1), canon(m2)})) so all rows for a given pair land in the same split (prevents leakage), writes artifacts/data_splits/{train,val,test}.csv + split_info.json.
python train_final_model.pyReads the splits, runs RandomizedSearchCV over XGBoost hyper-parameters (5-fold GroupKFold by monomer_pair_key), fits the final model on train+val, calibrates on a held-out subset, writes the new artifacts/model_bundle/. Run python reproduce_paper_metrics.py afterwards to confirm the new bundle matches (or to capture the new numbers if you intend to update the paper).
python monomer_feature_calculation.pyRuns XTB for each unique monomer SMILES that doesn't yet have a cached descriptor file under api/molecule_properties/. First-time computation per monomer takes ~1–5 min; subsequent runs are cached.
See api/README.md. One-liner with Docker:
cd api && docker compose upfrom copolpredictor.inference import CopolymerPredictor
predictor = CopolymerPredictor("artifacts/model_bundle")
result = predictor.predict_with_confidence(features)See src/copolpredictor/ for module docs.
artifacts/model_bundle/
├── meta.json ← feature list, class labels, hyper-parameters, training metrics
├── all_metrics.txt ← train/test per-class metrics (paper Table)
├── voting_test_metrics.json ← voting-layer metrics
├── model.joblib ← XGBoost classifier
├── model.xgb.json ← XGBoost native format (for cross-framework loading)
└── calibration.joblib ← isotonic calibrator
meta.json is the source of truth for feature schema and class definitions used at inference time; the API loads it on startup and exposes it at GET /model/info.