This model is largely AI-generated and has not been fully validated. It is provided to fix ideas, illustrate the modeling approach, and enable exploration — not as authoritative cost estimates. For a more detailed exploration with many more parameters, use the Advanced Model.
How to use this page
This Simplest Model focuses on a few key levers on cultured chicken production cost. Each parameter has an inline explanation — no further reading required to understand what you’re adjusting.
Once you’ve explored here, click → Advanced Model at the bottom of the sidebar to carry your settings over to a fuller parameter set.
Everything else (WACC, plant size, cell density, media-use multiplier, asset life, downstream costs) is held at reasonable defaults — see the “Background parameters” section in the sidebar.
Code
functionmulberry32(seed) {returnfunction() {let t = seed +=0x6D2B79F5; t =Math.imul(t ^ t >>>15, t |1); t ^= t +Math.imul(t ^ t >>>7, t |61);return ((t ^ t >>>14) >>>0) /4294967296; }}// ============================================================// STATISTICAL SAMPLING FUNCTIONS// ============================================================functionboxMuller(rng) {const u1 =rng();const u2 =rng();returnMath.sqrt(-2*Math.log(u1)) *Math.cos(2*Math.PI* u2);}functionsampleLognormalP5P95(rng, p5, p95, n) {const Z_95 =1.6448536269514722;const mu = (Math.log(p5) +Math.log(p95)) /2;const sigma = (Math.log(p95) -Math.log(p5)) / (2* Z_95);const samples =newArray(n);for (let i =0; i < n; i++) samples[i] =Math.exp(mu + sigma *boxMuller(rng));return samples;}// Like sampleLognormalP5P95 but takes an 80% CI (p10/p90) — matches the beliefs form.functionsampleLognormalP10P90(rng, p10, p90, n) {const Z_90 =1.2815515655446004;// qnorm(0.90)const mu = (Math.log(p10) +Math.log(p90)) /2;const sigma = (Math.log(p90) -Math.log(p10)) / (2* Z_90);const samples =newArray(n);for (let i =0; i < n; i++) samples[i] =Math.exp(mu + sigma *boxMuller(rng));return samples;}functionsampleUniform(rng, lo, hi, n) {const samples =newArray(n);for (let i =0; i < n; i++) samples[i] = lo + (hi - lo) *rng();return samples;}functionbetaFromMeanStdev(mean, stdev) {let variance = stdev * stdev;const maxVar = mean * (1- mean);if (variance <=0|| variance >= maxVar) { stdev =Math.sqrt(maxVar *0.99); variance = stdev * stdev; }const t = maxVar / variance -1;return [Math.max(mean * t,0.01),Math.max((1- mean) * t,0.01)];}functionsampleBeta(rng, a, b) {const gammaA =sampleGamma(rng, a);const gammaB =sampleGamma(rng, b);return gammaA / (gammaA + gammaB);}functionsampleGamma(rng, shape) {if (shape <1) returnsampleGamma(rng, shape +1) *Math.pow(rng(),1/ shape);const d = shape -1/3;const c =1/Math.sqrt(9* d);while (true) {let x, v;do { x =boxMuller(rng); v =1+ c * x; } while (v <=0); v = v * v * v;const u =rng();if (u <1-0.0331* (x*x) * (x*x)) return d * v;if (Math.log(u) <0.5* x * x + d * (1- v +Math.log(v))) return d * v; }}functionsampleBetaMeanStdev(rng, mean, stdev, n) {// Guard: Beta is undefined at the boundaries — return degenerate arrays// Without this, betaFromMeanStdev(0 or 1, ...) produces NaN parameters,// sampleBeta returns NaN, and rng() < NaN is always false, flipping// boolean outcomes to the wrong regime (e.g. p_recf=100% → all expensive).if (mean <=0) returnnewArray(n).fill(0);if (mean >=1) returnnewArray(n).fill(1);const [a, b] =betaFromMeanStdev(mean, stdev);const samples =newArray(n);for (let i =0; i < n; i++) samples[i] =sampleBeta(rng, a, b);return samples;}functioncrf(wacc, nYears) {return wacc *Math.pow(1+ wacc, nYears) / (Math.pow(1+ wacc, nYears) -1);}functionclip(arr, min, max) { return arr.map(v =>Math.min(Math.max(v, min), max)); }functionadd(a, b) { return a.map((v, i) => v + b[i]); }functionmul(a, b) { return a.map((v, i) => v * b[i]); }functiondiv(a, b) { return a.map((v, i) => v / b[i]); }functionscale(arr, s) { return arr.map(v => v * s); }// ============================================================// MAIN SIMULATION FUNCTION (identical to Advanced Model)// ============================================================functionsimulate(n, seed, params) {const rng =mulberry32(seed);// Year-based maturity: 0.30 at 2026, 0.50 at 2036, 0.70 at 2050// Later years -> higher industry maturity -> lower CAPEX (via WACC and custom-reactor share)// and higher effective tech-adoption probabilities.const effectiveMaturityMean = params.maturity_mean+ (params.target_year? (params.target_year-2036) /14*0.20:0);const clampedMaturityMean =Math.min(0.85,Math.max(0.15, effectiveMaturityMean));const maturity =sampleBetaMeanStdev(rng, clampedMaturityMean,0.20, n);const plant_kta =sampleLognormalP5P95(rng, params.plant_kta_p5, params.plant_kta_p95, n);const uptime =sampleBetaMeanStdev(rng, params.uptime_mean,0.05, n);const output_kgpy =mul(scale(plant_kta,1e6), uptime);let p_hydro =sampleBetaMeanStdev(rng, params.p_hydro_mean,0.10, n); p_hydro =clip(add(p_hydro,scale(maturity.map(m => m -0.5),0.25)),0,1);let p_recf =sampleBetaMeanStdev(rng, params.p_recfactors_mean,0.15, n); p_recf =clip(add(p_recf,scale(maturity.map(m => m -0.5),0.25)),0,1);const is_hydro = p_hydro.map(p =>rng() < p);const is_recf_cheap = p_recf.map(p =>rng() < p);const cycle_days =sampleLognormalP5P95(rng,0.5,5.0, n);let density_gL, media_turnover, mode_labels;const p_total = params.p_fedbatch+ params.p_perfusion+ params.p_continuous;const p0 = params.p_fedbatch/ p_total;const p01 = p0 + params.p_perfusion/ p_total;const d_fb =sampleLognormalP5P95(rng,5,30, n);const d_pf =sampleLognormalP5P95(rng,30,150, n);const d_ct =sampleLognormalP5P95(rng,50,200, n);const t_fb =sampleLognormalP5P95(rng,1.0,2.0, n);const t_pf =sampleLognormalP5P95(rng,1.0,5.0, n);const t_ct =sampleLognormalP5P95(rng,0.5,3.0, n);const mode_rand =sampleUniform(rng,0,1, n); mode_labels = mode_rand.map(r => r < p0 ?"fedbatch": r < p01 ?"perfusion":"continuous"); density_gL = mode_labels.map((m, i) => m ==="fedbatch"? d_fb[i] : m ==="perfusion"? d_pf[i] : d_ct[i]); media_turnover = mode_labels.map((m, i) => m ==="fedbatch"? t_fb[i] : m ==="perfusion"? t_pf[i] : t_ct[i]);// Cell density override — must come before L_per_kg so CAPEX calculation also sees the new densityif (params.ep_density_p10&& params.ep_density_p90&& params.ep_density_p10< params.ep_density_p90) { density_gL =sampleLognormalP10P90(rng, params.ep_density_p10, params.ep_density_p90, n); }const L_per_kg =mul(div(density_gL.map(_ =>1000), density_gL), media_turnover);const media_cost_hydro =sampleLognormalP5P95(rng,0.2,1.2, n);const media_cost_pharma =sampleLognormalP5P95(rng,0.5,2.5, n);const media_cost_L = is_hydro.map((h, i) => h ? media_cost_hydro[i] : media_cost_pharma[i]);// Media cost override: if user specified their own $/kg biomass p10/p90, use that directlyconst cost_media = (params.ep_media_p10&& params.ep_media_p90&& params.ep_media_p10< params.ep_media_p90)?sampleLognormalP10P90(rng, params.ep_media_p10, params.ep_media_p90, n):mul(L_per_kg, media_cost_L);const g_recf_exp =sampleLognormalP5P95(rng,1e-3,6e-3, n);const g_recf_cheap =sampleLognormalP5P95(rng,5e-4,2e-3, n);const g_recf = is_recf_cheap.map((c, i) => c ? g_recf_cheap[i] : g_recf_exp[i]);const progress = params.gf_progress/100;const cheap_p5 =100*Math.pow(0.01, progress);const cheap_p95 =10000*Math.pow(0.01, progress);const price_recf_cheap =sampleLognormalP5P95(rng, cheap_p5, cheap_p95, n);const exp_p5 =5000*Math.pow(0.01, progress);const exp_p95 =500000*Math.pow(0.01, progress);const price_recf_exp =sampleLognormalP5P95(rng, exp_p5, exp_p95, n);const price_recf = is_recf_cheap.map((c, i) => c ? price_recf_cheap[i] : price_recf_exp[i]);// GF cost override: if user specified their own $/kg biomass p10/p90, bypass the regime modelconst cost_recf = (params.ep_gf_p10&& params.ep_gf_p90&& params.ep_gf_p10< params.ep_gf_p90)?sampleLognormalP10P90(rng, params.ep_gf_p10, params.ep_gf_p90, n):mul(g_recf, price_recf);const other_var =sampleLognormalP5P95(rng,0.5,5.0, n);const voc =add(add(cost_media, cost_recf), other_var);let cdmo_toll_perkg =newArray(n).fill(0);let capex_perkg =newArray(n).fill(0);if (params.include_capex) {const prod_kg_L_day =div(scale(density_gL,1/1000), cycle_days);const total_working_volume_L =div(output_kgpy,scale(prod_kg_L_day,365));const reactor_cost_L_pharma =sampleLognormalP5P95(rng,50,500, n);const custom_ratio =sampleUniform(rng,0.35,0.85, n);let custom_share =sampleBetaMeanStdev(rng,0.55,0.15, n); custom_share =clip(add(custom_share,scale(maturity.map(m => m -0.5),0.30)),0,1);const reactor_cost_L_avg = reactor_cost_L_pharma.map((p, i) => p * (custom_share[i] * custom_ratio[i] + (1- custom_share[i])));const capex_s =sampleUniform(rng,0.6,0.9, n);const plant_factor =sampleLognormalP5P95(rng,1.5,3.5, n);const V_ref =1e6;const capex_total = reactor_cost_L_avg.map((r, i) => r * total_working_volume_L[i] *Math.pow(total_working_volume_L[i] / V_ref, capex_s[i] -1) * plant_factor[i]);let wacc =sampleLognormalP5P95(rng, params.wacc_p5, params.wacc_p95, n); wacc =clip(wacc.map((w, i) => w -0.03* (maturity[i] -0.5)),0.03,1);const asset_life =sampleUniform(rng, params.asset_life_lo, params.asset_life_hi, n);const crf_val = wacc.map((w, i) =>crf(w, asset_life[i])); capex_perkg = capex_total.map((c, i) => (c * crf_val[i]) / output_kgpy[i]); }let fixed_perkg =newArray(n).fill(0);if (params.include_fixed_opex) {const ref_output =20e6*0.9;const fixed_perkg_ref =sampleUniform(rng,1.0,6.0, n);const fixed_annual_ref =scale(fixed_perkg_ref, ref_output);const fixed_scale =sampleUniform(rng,0.6,1.0, n);const fixed_annual = fixed_annual_ref.map((f, i) => f *Math.pow(output_kgpy[i] / ref_output, fixed_scale[i])); fixed_perkg =div(fixed_annual, output_kgpy); }const unit_cost =add(add(add(voc, capex_perkg), fixed_perkg), cdmo_toll_perkg);return { unit_cost,pct_hydro: is_hydro.filter(x => x).length/ n,pct_recf_cheap: is_recf_cheap.filter(x => x).length/ n,pct_fedbatch: mode_labels.filter(m => m ==="fedbatch").length/ n,pct_perfusion: mode_labels.filter(m => m ==="perfusion").length/ n,pct_continuous: mode_labels.filter(m => m ==="continuous").length/ n, };}// ============================================================// STATISTICS HELPERS// ============================================================functionquantile(arr, q) {const sorted = [...arr].sort((a, b) => a - b);const idx = (sorted.length-1) * q;const lo =Math.floor(idx);const hi =Math.ceil(idx);return sorted[lo] * (1- (idx - lo)) + sorted[hi] * (idx - lo);}functionmean(arr) { return arr.reduce((a, b) => a + b,0) / arr.length; }
Code
urlParams_s =window.__CM_URL_STATE__|| {}urlNum_s =function(key, def) {const v = urlParams_s[key];if (v ===undefined) return def;const n =Number(v);returnNumber.isFinite(n) ? n : def;}urlBool_s =function(key, def) {const v = urlParams_s[key];if (v ===undefined) return def;return v ==="1"|| v ==="true";}
Growth factors (FGF-2, IGF-1, TGF-β) are the most expensive media ingredient — often 55-95% of media cost at current research-grade prices. A “breakthrough” means at least one of these reaches commercial scale cheaply: autocrine cell lines (cells make their own), plant molecular farming, or precision fermentation. If no breakthrough: GF costs could dominate the total.
Will hydrolysates replace pharma-grade amino acids?
The nutrient broth cells grow in (basal media) requires amino acids. “Hydrolysates” are cheap plant/yeast protein digests that replace expensive pharmaceutical-grade amino acids. Hydrolysates: ~$0.20-1.20/L vs pharma-grade: ~$0.50-2.50/L — a ~70% cost reduction for media.
Most commercial products blend cultured cells with plant-based filler. E.g., 25% CM cells + 75% plant protein at ~$3/kg filler. Even if pure cells are expensive, a blended product can be price-competitive.
Probability of each process mode
Set all three; the simulation normalizes them internally so they always sum to 100%. The indicator below shows whether your raw inputs already total 100%.
At exactly 0.5, the maturity adjustment term (±0.25 × (m−0.5)) is zero — so technology adoption probabilities are set purely by your sliders, and financing costs use the midpoint WACC. The Projection Year slider then nudges the effective maturity toward 0.30 at 2026 (early industry) and 0.70 at 2050 (mature industry).
WACC (cost of capital)
8–20% range
Sampled from lognormal distribution; p5=8%, p95=20%. Food/biotech industry range from Humbird (2021) and CE Delft (2021).
Asset life
8–20 years (uniform)
Typical bioreactor/facility lifecycle; Risner et al. and Humbird use 10–15 yr.
Plant capacity
10–40 kTA (lognormal)
Ranges from small-scale (10 kTA) to large (40 kTA) commercial facilities.
CAPEX
Included
Bioreactor and facility capital costs, annualised via CRF.
Fixed overhead
Included. $1–6/kg at reference 20 kTA scale; scales sub-linearly.
Labour, maintenance, plant overhead.
Downstream costs
Not included (pure cell-mass basis). Downstream costs (scaffolding, texturization) are available in the Advanced Model.
Output here is unstructured cell mass at the bioreactor gate.
// Expert priors panel — collapsible, allows overriding the 3 biggest cost-driver distributionsviewof expert_priors = {const inp = (name, placeholder, step) => {const el =document.createElement('input');Object.assign(el, {type:'number', name,min:0, step, placeholder}); el.style.cssText='width:72px;padding:3px 5px;border:1px solid #b0c8c0;border-radius:4px;font-size:0.77rem;'; el.addEventListener('input', () => container.dispatchEvent(newEvent('input', {bubbles:true})));return el; };const resetBtn = (targets) => {const b =document.createElement('button'); b.type='button'; b.textContent='✕'; b.title='Clear — revert to model default'; b.style.cssText='padding:2px 6px;font-size:0.7rem;border:1px solid #ccc;border-radius:4px;background:#f9f9f9;color:#999;cursor:pointer;align-self:flex-end;'; b.onclick= () => { targets.forEach(n => { const el = container.querySelector(`[name=${n}]`);if(el) el.value=''; }); container.dispatchEvent(newEvent('input', {bubbles:true})); };return b; };const row = (labelText, hint, names, placeholders, steps) => {const wrap =document.createElement('div'); wrap.style.cssText='display:flex;flex-direction:column;gap:3px;';const head =document.createElement('div'); head.style.cssText='display:flex;align-items:center;gap:5px;font-size:0.78rem;font-weight:600;color:#2d4a2d;'; head.innerHTML= labelText +`<span title="${hint}" style="font-size:0.65rem;color:#aaa;cursor:help;border-bottom:1px dotted #ccc;">(?)</span>`;const inputs =document.createElement('div'); inputs.style.cssText='display:flex;gap:8px;align-items:flex-end;'; names.forEach((name, i) => {const col =document.createElement('div'); col.style.cssText='display:flex;flex-direction:column;gap:1px;';const lbl =document.createElement('div'); lbl.style.cssText='font-size:0.62rem;color:#888;'; lbl.textContent= i ===0?'p10 — optimistic':'p90 — pessimistic'; col.append(lbl,inp(name, placeholders[i], steps[i])); inputs.appendChild(col); }); inputs.appendChild(resetBtn(names)); wrap.append(head, inputs);return wrap; };const container =document.createElement('div');const details =document.createElement('details'); details.style.cssText='border:1.5px solid #3498db;border-radius:6px;overflow:hidden;margin:8px 0 2px;';const summary =document.createElement('summary'); summary.style.cssText='padding:7px 10px;background:#f0f8ff;cursor:pointer;font-size:0.82rem;font-weight:600;color:#1a5276;list-style:none;display:flex;align-items:center;gap:6px;user-select:none;'; summary.innerHTML='◧ Set my own uncertainty ranges <span style="font-size:0.68rem;font-weight:400;color:#888;margin-left:auto;" title="Override the model\'s built-in uncertainty ranges for the three biggest cost drivers. Uses 80% credible intervals (p10/p90) — the same format as the beliefs form. Leave blank to use model defaults.">(?)</span>';const body =document.createElement('div'); body.style.cssText='padding:10px 12px;display:flex;flex-direction:column;gap:10px;';const intro =document.createElement('p'); intro.style.cssText='margin:0;font-size:0.71rem;color:#555;line-height:1.45;'; intro.innerHTML='Replace the model\'s built-in ranges with your own <strong>80% credible interval</strong> for each key cost driver. Leave blank to use defaults. <a href="docs.html#expert-priors" style="color:#3498db;" target="_blank">How this works →</a>';const activeNote =document.createElement('div'); activeNote.id='ep-active-note'; activeNote.style.cssText='display:none;font-size:0.68rem;color:#c0392b;font-weight:600;padding:2px 4px;background:#fef9f9;border-radius:3px;'; activeNote.textContent='⚠ Custom ranges active — results reflect your priors, not model defaults'; body.append( intro, activeNote,row('Media cost ($/kg biomass)','Total cell culture media cost per kg of harvested cell biomass — combines $/L cost × liters consumed. Default model range roughly p10≈5, p90≈120. CM_14 in the beliefs form.', ['media_p10','media_p90'], ['e.g. 5','e.g. 80'], [1,5]),row('Growth factor cost ($/kg biomass)','Total growth factor cost per kg of biomass — combining quantity (g/kg) × price ($/g). Overriding this bypasses the breakthrough-regime model. CM_13 in the beliefs form.', ['gf_p10','gf_p90'], ['e.g. 2','e.g. 60'], [0.5,5]),row('Cell density (g/L at harvest)','Wet-weight cell density in the bioreactor at harvest. Higher density → fewer liters per kg → lower media and CAPEX costs. Default model ranges: fed-batch ≈5–30, perfusion ≈30–150. CM_16 in the beliefs form.', ['density_p10','density_p90'], ['e.g. 8','e.g. 50'], [1,5]) ); details.append(summary, body); container.appendChild(details); container.addEventListener('input', () => {const anyActive = ['media_p10','gf_p10','density_p10'].some(n => container.querySelector(`[name=${n}]`)?.value!==''); activeNote.style.display= anyActive ?'block':'none'; });functiongetVal(name) {const v =parseFloat(container.querySelector(`[name=${name}]`)?.value);returnisNaN(v) || v <=0?null: v; }Object.defineProperty(container,'value', {get: () => ({media_p10:getVal('media_p10'),media_p90:getVal('media_p90'),gf_p10:getVal('gf_p10'),gf_p90:getVal('gf_p90'),density_p10:getVal('density_p10'),density_p90:getVal('density_p90') }) });return container;}
Code
// "→ Advanced Model" carry-over link{const tot =Math.max(p_fedbatch_s + p_perfusion_s + p_continuous_s,1);const p =newURLSearchParams({target_year: target_year_s,p_hydro: (p_hydro_s /100).toFixed(2),p_recfactors: (p_recfactors_s /100).toFixed(2),p_fedbatch: (p_fedbatch_s / tot).toFixed(2),p_perfusion: (p_perfusion_s / tot).toFixed(2),p_continuous: (p_continuous_s / tot).toFixed(2),include_blending: include_blending_s ?1:0,blending_share: (blending_share_s /100).toFixed(2) });returnhtml`<div style="margin-top:1rem; padding-top:0.8rem; border-top:2px solid #eee;"> <a href="index.html?${p.toString()}" style="display:block; text-align:center; padding:0.7rem; background:#2980b9; color:white; border-radius:6px; text-decoration:none; font-weight:600; font-size:0.95rem;"> → Advanced Model (adapt these settings) </a> <div style="font-size:0.78em; color:#888; margin-top:0.4rem; text-align:center;">These parameters will be pre-set in the Advanced Model, where many more parameters become adjustable</div> </div>`;}
Code
// Build params object for simulation — all background params hardcodedsimParams_simple = {// All three process-mode probabilities are user-set; we normalize internally// so the weights always sum to 1, even if the raw slider values don't sum to 100.const tot =Math.max(p_fedbatch_s + p_perfusion_s + p_continuous_s,1);return {maturity_mean:0.5,target_year: target_year_s,p_hydro_mean: p_hydro_s /100,p_recfactors_mean: p_recfactors_s /100,gf_progress:50,p_fedbatch: p_fedbatch_s / tot,p_perfusion: p_perfusion_s / tot,p_continuous: p_continuous_s / tot,override_mode_constraints:false,plant_kta_p5:10,plant_kta_p95:40,uptime_mean:0.90,wacc_p5:0.08,wacc_p95:0.20,asset_life_lo:8,asset_life_hi:20,density_gL_p5:30,density_gL_p95:200,media_turnover_p5:0.5,media_turnover_p95:3.0,include_capex:true,include_fixed_opex:true,include_downstream:false,cdmo_mode:false,bundled_media:false,bundled_media_p5:50,bundled_media_p95:500,cdmo_toll_p5:4,cdmo_toll_p95:40,// Expert prior overrides — null means use model defaultsep_media_p10: expert_priors.media_p10,ep_media_p90: expert_priors.media_p90,ep_gf_p10: expert_priors.gf_p10,ep_gf_p90: expert_priors.gf_p90,ep_density_p10: expert_priors.density_p10,ep_density_p90: expert_priors.density_p90 };}
Code
results_s =simulate(30000,42, simParams_simple)
Code
stats_s = {const uc = results_s.unit_cost;const bs = blending_share_s /100;const fc =3.0;const blended = uc.map(c => c * bs + fc * (1- bs));const pct = (arr, t) => arr.filter(x => x < t).length/ arr.length*100;return {p5:quantile(uc,0.05),p20:quantile(uc,0.20),p50:quantile(uc,0.50),p80:quantile(uc,0.80),p95:quantile(uc,0.95),prob_10:pct(uc,10),prob_25:pct(uc,25),prob_50:pct(uc,50),prob_100:pct(uc,100),bprob_5:pct(blended,5),bprob_8:pct(blended,8),bprob_12:pct(blended,12),bprob_10:pct(blended,10),bprob_25:pct(blended,25),blended_p50:quantile(blended,0.50),blended_p5:quantile(blended,0.05),blended_p95:quantile(blended,0.95), bs,n: uc.length };}
html`<div style="background:#f8f9fa; padding:0.8rem 1rem; border-left:4px solid #3498db; margin-bottom:1.5rem; font-size:0.9em; line-height:1.6;">All values are <strong>manufacturing cost per kg of cultured chicken cell biomass (wet weight, at harvest)</strong> — the factory-gate cost, not a consumer product price. Based on ${stats_s.n.toLocaleString()} Monte Carlo simulations.${include_blending_s ?html` Blended product estimates use ${stats_s.bs*100|0}% CM + ${(1-stats_s.bs)*100|0}% plant-based filler at $3/kg.`:''}</div>`
The model draws 30,000 random samples for each uncertain parameter (cell density, media price, growth factor quantity/price, reactor costs, asset life, WACC, plant capacity, uptime, etc.) and computes a unit cost for each draw. The histogram above shows the resulting distribution of unit costs; the cards above summarize what fraction of those samples fall below each threshold.
Media cost depends on cell density (g/L) and media-use multiplier (× of reactor volume), both determined by process mode.
Growth factor cost depends on quantity (g/kg meat) and price ($/g), with a binary regime switch based on P(GF breakthrough).
CAPEX is annualised via the Capital Recovery Factor: CRF = r(1+r)^n / ((1+r)^n − 1).