D. Christopher Radford#, Matthew Tamasi#, Elena Di Mare, Adam J. Gormley*
Department of Biomedical Engineering, Rutgers, The State University of New Jersey, Piscataway, New Jersey 08854, USA
Read the full paper on ChemRxiv
Over the last forty years, biologics such as monoclonal antibodies have become an increasingly important therapeutic agent in the treatment of numerous diseases. Between 1986 and 2025, over 200 antibody-based treatments have been approved globally, most of which are manufactured as preformulated solutions for subsequent administration to patients. However, bioformulation of complex proteins is a difficult engineering challenge; formulations must be tailored to individual therapies, necessitating time- and material-intensive campaigns to select a combination of excipients to simultaneously optimize an array of design criteria. These many interacting additives complicate formulation design with unintuitive and non-linear relationships, thus creating a vast and multidimensional design space that is intrinsically difficult to navigate through and optimize using traditional techniques. To address this challenge, we investigated a high-throughput discovery pipeline using machine learning to model and predict formulation behavior of Generally Recognized as Safe (GRAS) excipients on a model antibody. This was supported by automation-assisted “on-demand” formulation to produce dozens of uniquely formulated antibody solutions with high reproducibility for downstream evaluation and biophysical characterization. This pipeline was then integrated into an iterative closed-loop cycle of automated Design-Build-Test-Learn (DBTL), where new rounds of experiments are designed by the ML model. The process yielded both optimized formulations as well as highly accurate predictive models of formulation behavior. This validates the utility of this technique to both map the underlying property-function landscape and effectively guide formulation development while balancing multiple competing design requirements.
This repository contains the code and data for the paper "Automation and Active Learning for the Multi-Objective Optimization of Antibody Formulations". It provides a machine learning pipeline for optimizing antibody formulations using Bayesian Optimization with BoTorch.
The pipeline performs the following steps:
- Data Loading & Preprocessing: Loads formulation data from a JSON file, processes array-based measurements, and standardizes features.
- Model Training: Trains Gaussian Process (GP) models for each objective (Tm, Diffusion, Viscosity) using
SingleTaskGP. - Model Validation: Performs Group K-Fold Cross-Validation to ensure model robustness.
- Optimization:
- Single-Objective: Optimizes each target individually. The acquisition function is configurable (
single_acqf): q-Expected Improvement (qEI, default), q-Upper Confidence Bound (qUCB), or a greedy constant-liar posterior-mean batch (greedy_cl). - Multi-Objective: Optimizes all targets simultaneously. The acquisition function is configurable (
multi_acqf): q-Expected Hypervolume Improvement (qEHVI, default) with a fixed reference point, or a greedy constant-liar batch (greedy_cl).
- Single-Objective: Optimizes each target individually. The acquisition function is configurable (
- Explainability:
- SHAP Analysis: Automatically generates SHAP values (as pickle files) to explain model predictions in real units for all objectives.
├── config/
│ └── config.py # Configuration settings (paths, objectives, acquisition, bounds)
├── data/
│ └── Antibody_... # Input JSON data
├── outputs/ # Generated models, logs, candidates, and SHAP results
├── scripts/
│ └── compare_models.py # Standalone model-comparison benchmark (GP vs. linear/tree/etc.)
├── src/
│ ├── pipeline.py # Core optimization logic (BoTorchPipeline with integrated SHAP)
│ ├── utils.py # Utility functions (data loading, logging)
│ └── shap_utils.py # SHAP analysis and visualization utilities
├── main.py # Entry point script (runs full pipeline including SHAP)
├── requirements.txt # Python dependencies
└── README.md # This file
We recommend using uv for fast and reliable dependency management.
-
Clone the repository.
-
Install
uv(if not already installed): -
pip install uv
-
Create a virtual environment and install dependencies:
# Create virtual environment uv venv # Install dependencies uv pip install -r requirements.txt
Alternatively, you can use standard pip:
pip install -r requirements.txtTo run the complete optimization pipeline (including automatic SHAP analysis):
Windows:
.venv\Scripts\python main.pymacOS/Linux:
.venv/bin/python main.pyThe pipeline automatically generates SHAP analysis results after model training, saving them to outputs/shap_results/.
You can customize the execution using command-line arguments:
.venv\Scripts\python main.py --config config/custom_config.yaml --output_dir outputs/experiment_1--config: Path to a YAML file that overrides top-level scalar config fields (e.g.single_acqf,batch_size,random_state,data_file). Nested settings such as theobjectivesdictionary are not overridable via YAML — change those directly inconfig/config.py(see Configuration Reference below).--output_dir: Directory to save results (models, logs, candidates).--debug: Enable debug logging.
The pipeline outputs:
- optimization_candidates.xlsx: Suggested formulation candidates for experimental validation.
- models/: Saved PyTorch/GPyTorch models (
.pklfiles). - optimization.log: Detailed execution log.
- shap_results/: SHAP analysis results including:
- Pickle files for each objective (e.g.,
tm_shap_results.pkl,tm_shap_real_units.pkl)
- Pickle files for each objective (e.g.,
scripts/compare_models.py benchmarks the production Gaussian-process (GP) surrogate against a panel of common regression models, under the same 10-fold GroupKFold cross-validation (grouped by Formulation ID) used by the main pipeline. This reproduces the expanded model comparison reported in the revised manuscript.
Models compared (per objective): ordinary linear regression, linear regression with pairwise interactions, a degree-2 ridge polynomial, RBF-kernel SVR, random forest, extremely randomized trees, gradient boosting, a small regularized MLP, and the production BoTorch GP.
python scripts/compare_models.pyOptions:
--config: YAML config override (same semantics asmain.py).--output-dir: Output directory (default:outputs/benchmark/).--seed: Overriderandom_state.--verbose: Enable INFO logging.
Outputs (in outputs/benchmark/):
model_comparison_metrics.csv— per model × objective metrics (pooled and per-fold R², MAE in scaled and real units).oof_predictions_scaled.csv— per-row out-of-fold predictions in scaled space.model_comparison_summary.md— a human-readable pooled-R² table.
Because the utility reads its objectives and features from config/config.py, it automatically benchmarks whatever objectives are defined there — no edits needed to point it at a different dataset (subject to the data-format requirements in Adapting the Pipeline to Your Own Data).
All settings live in config/config.py as a Config dataclass. Top-level scalar fields can also be overridden via a YAML file passed with --config; nested fields (notably objectives) must be edited directly in config.py.
| Field | Type | Default | Description |
|---|---|---|---|
random_state |
int | 42 |
Global random seed (model fitting, CV fold shuffling). |
device |
str | "auto" |
Compute device: "auto", "cpu", or "cuda". |
data_file |
str | data/Antibody_...JSON.json |
Path to the input JSON dataset. |
output_dir |
str | "outputs" |
Base directory for results. |
feature_columns |
list[str] | 7 columns | Input features used by the models, after array expansion. Includes Concentration (mg/mL). |
objectives |
dict | 3 objectives | Per-objective configuration (see below). |
excipient_columns |
list[str] | 6 columns | Excipient/formulation columns present in the raw JSON. |
metadata_columns |
list[str] | 3 columns | Non-feature metadata (Formulation ID, Objective, Generation). |
cv_folds |
int | 10 |
Number of GroupKFold splits (grouped by Formulation ID). |
min_samples |
int | 15 |
Minimum expanded rows required to train an objective's model. |
min_r2 |
float | 0.2 |
Minimum train-set R² for a fitted GP to be accepted. |
batch_size |
int | 6 |
Number of candidates (q) returned per acquisition. |
mc_samples |
int | 128 |
Monte Carlo samples for the qMC acquisition sampler. |
num_restarts |
int | 10 |
Restarts for acquisition-function optimization. |
reference_point |
list[float] | [0,0,0] |
Reference point for qEHVI (one entry per objective, in scaled/direction-adjusted space). |
single_acqf |
str | "qEI" |
Single-objective acquisition: "qEI", "qUCB", or "greedy_cl". |
multi_acqf |
str | "qEHVI" |
Multi-objective acquisition: "qEHVI" or "greedy_cl". |
ucb_beta |
float | 0.1 |
Exploration weight for qUCB (only used when single_acqf="qUCB"). |
valid_buffer_pH_pairs |
dict | — | Allowed pH values per buffer system (reference metadata). |
pred_conc |
dict | {tm:15, diffusion:15, viscosity:120} |
Antibody concentration (mg/mL) at which candidate predictions are reported per objective. |
save_models |
bool | True |
Save trained GP models to outputs/models/. |
save_candidates |
bool | True |
Save candidate formulations to Excel. |
run_shap_analysis |
bool | True |
Run SHAP explainability after training. |
shap_verbose |
bool | False |
Print SHAP progress. |
Each entry in objectives is keyed by a short objective name (tm, diffusion, viscosity) and contains:
| Key | Description |
|---|---|
target_column |
Name of the expanded mean column the model is trained on (e.g. "Tm (C) Mean"). |
std_column |
Name of the expanded standard-deviation column (e.g. "Tm (C) Std"). |
direction |
1.0 to maximize, -1.0 to minimize the objective. |
concentration |
Raw JSON key holding the per-measurement concentration array (e.g. "concentration_tm"). |
value |
Raw JSON key holding the per-measurement value array (e.g. "tm"). |
std |
Raw JSON key holding the per-measurement std array (e.g. "tm_std"). |
The defaults (single_acqf="qEI", multi_acqf="qEHVI") reproduce the published pipeline exactly. The alternatives are more exploitative and were added to test how acquisition policy affects candidate selection:
qUCB(single-objective) — q-Upper Confidence Bound with exploration weightucb_beta(small β ⇒ exploitative).greedy_cl(single- and multi-objective) — greedy posterior-mean selection with constant-liar (CL-min) batching (Ginsbourger et al., 2010): after each pick the surrogate is conditioned on the worst-case scaled outcome so the next pick moves elsewhere. This is the pure-exploitation extreme.
Data is a single JSON file with the structure:
{
"sheets": {
"Consolidated": [
{
"Formulation ID": 1,
"Molarity": 90.1, "NaCl": 46.9, "Sucrose": 1.56, "Arginine": 49.5, "pH": 6.0, "Buffer": 2,
"Objective": "Seed", "Generation": "Seed",
"concentration_tm": [2.5, 5.0, 10.0, 15.0],
"tm": [67.3, 66.3, 65.6, 65.5],
"tm_std": [0.22, 0.15, 0.24, 0.16],
"concentration_diff": [...], "diff": [...], "diff_std": [...],
"concentration_visc": [...], "visc": [...], "visc_std": [...]
}
]
}
}Key points:
- The loader uses the first sheet under
"sheets". - Each object is one formulation. Excipient/feature values are scalars; each objective's measurements are parallel arrays of
concentration,value, andstd. - The pipeline expands these arrays into one row per (formulation, concentration) measurement, and groups rows by
Formulation IDduring cross-validation so replicates of the same formulation never straddle train/test folds.
- Point at your data: set
data_fileinconfig.py(or via YAML). - Reuse the three existing objectives as-is if your JSON uses the same array keys (
tm/diff/visc,concentration_*,*_std). Just supply your own values — no code changes needed. - Change features: edit
feature_columns(model inputs) andexcipient_columns(raw scalar columns). KeepConcentration (mg/mL)infeature_columnsif your objectives are concentration-dependent. - Change objective directions / reporting: edit each objective's
direction(maximize vs. minimize), andpred_conc(the concentration at which candidate predictions are reported). Setreference_pointto have one entry per objective. - Add or rename an objective (requires a small code edit): the array-expansion step in
src/utils.py(expand_array_data) maps the raw array keys"tm"/"diff"/"visc"to the standardizedtarget_column/std_columnnames. To add a genuinely new measurement type (newvaluekey and newtarget_column), add a corresponding branch inexpand_array_datathat writes yourtarget_column/std_column, then add the matching entry toobjectives. - Validate: run
python scripts/compare_models.pyto confirm your data loads and models train, then runpython main.pyfor the full pipeline.
Set single_acqf / multi_acqf in the config. Use the qEI/qEHVI defaults for balanced exploration/exploitation; use qUCB (tune ucb_beta) or greedy_cl when you want more exploitative batches.