A small, self-contained simulation of a fed-batch CHO (mammalian) cell culture, built to answer three questions clearly:
- What are the feed strategies, and how do they differ? →
model.py+compare.py - How do you optimize each one? →
optimize.py - How do you select between them? → the ranked table from
compare.py
The model is the usual mass balance + Monod kinetics for
[Xv, Glc, Lac, P, V] (viable cells, glucose, lactate, titer, volume).
Parameters are illustrative, not literature-calibrated — the point is the
shape of each strategy, not exact numbers.
A feed law like if Glc < 5: pump on else off is discontinuous. Feed that
straight into an adaptive ODE solver and the answer drifts with the solver
tolerance — it is numerically ill-posed. Every strategy here is instead
integrated so the right-hand side is smooth over each segment:
- clock-based feeds (bolus) are integrated between their known switch times;
- state-based feeds (the relay controller) are integrated up to the exact switch time, located by a root-finder, then restarted with the pump flipped.
That is the whole reason model.py exists and simple_cho_fedbatch.py (the
naive starting point) does not.
| strategy | feed law | knob |
|---|---|---|
batch |
no feed — starves and crashes (baseline) | — |
constant |
fixed slow trickle | F_const |
bolus |
one shot of feed per day | F_bolus |
hysteresis |
relay: feed below a setpoint, stop above | glc_on |
smooth |
the relay with a soft (tanh) switch | glc_on |
Comparison at default parameters (compare.py). Batch starves once glucose
runs out; every feed strategy keeps glucose up so cells survive and titer keeps
climbing.
| strategy | titer (mg/L) | peak VCD | final lactate (mM) | pump switches |
|---|---|---|---|---|
| bolus | 1412.5 | 24.8 | 312.9 | 28 |
| smooth | 1213.4 | 25.1 | 307.8 | 0 |
| hysteresis | 1211.1 | 25.0 | 307.1 | 456 |
| constant | 957.3 | 12.8 | 181.0 | 0 |
| batch | 342.6 | 4.6 | 42.0 | 0 |
Selection is not just "highest titer": hysteresis and smooth reach nearly
the same titer, but smooth does it with zero pump chatter while the relay
switches hundreds of times — so smooth is the better real-world choice at a
similar result.
Optimizing each strategy (optimize.py). Sweep each strategy's one knob:
too little feed starves, too much dilutes the product, and the peak in between
is the optimum.
| strategy | knob | best value | best titer (mg/L) |
|---|---|---|---|
| constant | F_const |
0.0024 | 1406.8 |
| bolus | F_bolus |
0.0586 | 1432.1 |
| hysteresis | glc_on |
18.0 | 1241.5 |
model.py # the model + 5 strategies + metrics (import this)
compare.py # run all strategies -> ranked table + overlay plot
optimize.py # scan each strategy's knob -> best setting per strategy
simple_cho_fedbatch.py # the naive Day-1 starting point (batch only)
results/ # generated figures and CSVs
pip install -r requirements.txt
python compare.py # differences + selection
python optimize.py # per-strategy tuningEverything writes into results/.

