Simplest Model
This page runs the current model (engine 2026-09-17.1). See the September 2026 review and correction log. For comparisons with beliefs recorded at the May 8 workshop, use the hosted workshop-era model or its tagged source. The archived model is preserved as it was deployed and includes errors corrected here.
Two supplied research critiques prompted optional median-preserving priors, explicit growth-factor price ranges, and structural comparisons for media use, GF dosage, and maturity dependence. Baseline numerical assumptions are retained. New controls include provenance tooltips and links to the discussion responses, justification, and remaining questions. These additions are AI-implemented scenario tests, pending expert review.
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.
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.
Parameters exposed here: Projection Year, P(Growth Factor Breakthrough), P(Hydrolysates adopted), Process Mode Mix, Blended Product
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
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); return Number.isFinite(n) ? n : def;
}
urlBool_s = function(key, def) {
const v = urlParams_s[key];
if (v === undefined) return def;
return v === "1" || v === "true";
}Code
Adjustable Parameters
Projection Year
Code
Further-out years give more time for cost reductions and industry scale-up.
Will a Growth Factor (GF) breakthrough happen?
Code
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?
Code
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.
Blended Product (?)
Code
Code
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%.
Code
viewof p_fedbatch_s = Inputs.range([0, 100], {
value: Math.round(urlNum_s("p_fedbatch", 0.20) * 100), step: 5,
label: "Fed-batch %"
})
viewof p_perfusion_s = Inputs.range([0, 100], {
value: Math.round(urlNum_s("p_perfusion", 0.50) * 100), step: 5,
label: "Perfusion %"
})
viewof p_continuous_s = Inputs.range([0, 100], {
value: Math.round(urlNum_s("p_continuous", 0.30) * 100), step: 5,
label: "Continuous %"
})Code
{
const sum = p_fedbatch_s + p_perfusion_s + p_continuous_s;
const exact = Math.abs(sum - 100) < 1;
const color = exact ? "#27ae60" : "#e67e22";
return html`<div style="font-size:0.85em; padding:0.3rem 0.5rem; background:#fafafa; border-radius:4px; margin-bottom:0.3rem;">
Sum: <strong style="color:${color}">${sum}%</strong>
${exact
? html` <span style="color:#27ae60;">(adds to 100%)</span>`
: html` <span style="color:${color};">— simulation will normalize to 100%</span>`}
</div>`;
}| Mode | Density | Media use | Cost implication |
|---|---|---|---|
| Fed-batch | 5–30 g/L | 1–2× | Higher cost (less dense) |
| Perfusion | 30–150 g/L | 1–5× | Medium cost |
| Continuous | 50–200 g/L | 0.5–3× | Lower cost (denser) |
Pure batch (single fill-and-dump) is excluded — not considered commercially viable at scale.
Background parameters held constant in this model
| Parameter | Value | Why fixed |
|---|---|---|
| Industry Maturity | Base index 0.5 | The shared year mapping gives a mean of 0.275 in 2026, 0.40 in 2036, and 0.50 from 2044 onward. Individual draws still vary and correlate adoption with financing. This is a scenario mapping, not an estimated learning curve. |
| 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. |
| GF cost progress | 50% | Midpoint toward industry price targets |
| Filler cost | $3/kg | Plant protein / mycoprotein estimate |
Code
Code
// "→ Advanced Model" carry-over link
{
const tot = Math.max(p_fedbatch_s + p_perfusion_s + p_continuous_s, 1);
const p = new URLSearchParams({
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)
});
for (const [key, value] of Object.entries(expert_priors)) if (value != null) p.set(`ep_${key}`, value);
return html`<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 hardcoded
simParams_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 {
...costModel.DEFAULT_PARAMS,
maturity_mean: costModel.maturityForYear(0.5, target_year_s),
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 defaults
ep_media_p10: expert_priors.media_p10,
ep_media_p50: expert_priors.media_p50,
ep_media_p90: expert_priors.media_p90,
ep_gf_p10: expert_priors.gf_p10,
ep_gf_p50: expert_priors.gf_p50,
ep_gf_p90: expert_priors.gf_p90,
ep_density_p10: expert_priors.density_p10,
ep_density_p50: expert_priors.density_p50,
ep_density_p90: expert_priors.density_p90
};
}Code
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
};
}Results
Quantity being modeled: manufacturing cost per kg of wet cultured-chicken biomass for a hypothetical plant/process scenario. Each draw samples one process; process weights are probabilities across scenarios, not production shares within a simulated industry. The output is not an industry-average forecast or a frontier estimate. Commercialization probability is unmodeled; dollars retain mixed source years.
Code
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 on the model's wet-biomass basis</strong> — a factory-gate ingredient cost, not a consumer-product price. The code treats density as wet-biomass g/L; it does not separately standardize hydration, dry matter, protein content, or recovery yield. Source dollars are not yet normalized to one real-dollar year, and the model does not separate P(commercial scale exists) from cost conditional on success. 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>`Code
html`<div class="grid" style="grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem;">
<div style="background: linear-gradient(135deg, #3498db, #2980b9); color: white; padding: 1.5rem; border-radius: 8px;">
<h4 style="margin:0; opacity:0.9; font-size:0.9rem;">Median Cost (p50)</h4>
<h2 style="margin:0.5rem 0;">$${Math.round(stats_s.p50)}/kg</h2>
<small>Half of simulations above, half below</small>
${include_blending_s ? html`<div style="margin-top:0.5rem; font-size:0.85em; opacity:0.9;">Blended: $${stats_s.blended_p50.toFixed(1)}/kg</div>` : ''}
</div>
<div style="background: linear-gradient(135deg, #27ae60, #1e8449); color: white; padding: 1.5rem; border-radius: 8px;">
<h4 style="margin:0; opacity:0.9; font-size:0.9rem;">Optimistic (p5)</h4>
<h2 style="margin:0.5rem 0;">$${Math.round(stats_s.p5)}/kg</h2>
<small>Only 5% of simulations cheaper</small>
${include_blending_s ? html`<div style="margin-top:0.5rem; font-size:0.85em; opacity:0.9;">Blended p5: $${stats_s.blended_p5.toFixed(1)}/kg</div>` : ''}
</div>
<div style="background: linear-gradient(135deg, #e74c3c, #c0392b); color: white; padding: 1.5rem; border-radius: 8px;">
<h4 style="margin:0; opacity:0.9; font-size:0.9rem;">Pessimistic (p95)</h4>
<h2 style="margin:0.5rem 0;">$${Math.round(stats_s.p95)}/kg</h2>
<small>95% of simulations cheaper</small>
${include_blending_s ? html`<div style="margin-top:0.5rem; font-size:0.85em; opacity:0.9;">Blended p95: $${stats_s.blended_p95.toFixed(1)}/kg</div>` : ''}
</div>
</div>`Probability Thresholds
Code
{
// Pure-cell-mass cards: a single set of thresholds. When blending is enabled,
// the blended-product probabilities are shown ONLY in the dedicated blend
// row below — never embedded inside the pure-cell cards — to avoid showing
// overlapping but slightly different threshold sets in the same place.
function card(thresh, prob, label, color) {
const bc = prob > 30 ? color : '#ddd';
return `<div style="border:2px solid ${bc}; padding:0.9rem; border-radius:8px; text-align:center;">
<h5 style="margin:0 0 0.2rem;">P(Pure cells < $${thresh}/kg)</h5>
<h2 style="color:${color}; margin:0.2rem 0;">${prob.toFixed(1)}%</h2>
<small style="color:#666;">${label}</small>
</div>`;
}
const grid = `<div class="grid" style="grid-template-columns:repeat(4,1fr); gap:0.75rem; margin-bottom:1.5rem;">
${card(10, stats_s.prob_10, 'illustrative manufacturing-cost threshold', '#27ae60')}
${card(25, stats_s.prob_25, 'illustrative manufacturing-cost threshold', '#3498db')}
${card(50, stats_s.prob_50, 'illustrative manufacturing-cost threshold', '#f39c12')}
${card(100, stats_s.prob_100, 'illustrative manufacturing-cost threshold', '#e74c3c')}
</div>`;
const blendRow = include_blending_s ? `
<p style="font-size:0.88em; color:#1a5276; font-weight:500; margin:0.5rem 0 0.3rem;">
Blended product (${stats_s.bs*100|0}% CM + ${((1-stats_s.bs)*100)|0}% filler at $3/kg) — ingredient costs only:
</p>
<div class="grid" style="grid-template-columns:repeat(3,1fr); gap:0.6rem; margin-bottom:1.5rem;">
<div style="border:2px solid ${stats_s.bprob_5>20?'#27ae60':'#ddd'}; padding:0.8rem; border-radius:8px; text-align:center;">
<h5 style="font-size:0.85em; margin:0 0 0.2rem;">P(Blend < $5/kg)</h5>
<h2 style="color:#27ae60; margin:0.2rem 0;">${stats_s.bprob_5.toFixed(1)}%</h2>
<small>ingredients only; excludes retail costs</small>
</div>
<div style="border:2px solid ${stats_s.bprob_8>30?'#3498db':'#ddd'}; padding:0.8rem; border-radius:8px; text-align:center;">
<h5 style="font-size:0.85em; margin:0 0 0.2rem;">P(Blend < $8/kg)</h5>
<h2 style="color:#3498db; margin:0.2rem 0;">${stats_s.bprob_8.toFixed(1)}%</h2>
<small>ingredients only; excludes retail costs</small>
</div>
<div style="border:2px solid ${stats_s.bprob_12>50?'#f39c12':'#ddd'}; padding:0.8rem; border-radius:8px; text-align:center;">
<h5 style="font-size:0.85em; margin:0 0 0.2rem;">P(Blend < $12/kg)</h5>
<h2 style="color:#f39c12; margin:0.2rem 0;">${stats_s.bprob_12.toFixed(1)}%</h2>
<small>ingredients only; excludes retail costs</small>
</div>
</div>` : '';
return html([grid + blendRow]);
}Cost Distribution
Code
{
const uc = results_s.unit_cost;
const clipVal = quantile(uc, 0.98);
const clipped = uc.filter(x => x <= clipVal);
const p20 = stats_s.p20; const p80 = stats_s.p80;
const fsBtn = document.createElement("button");
fsBtn.textContent = "⛶";
fsBtn.title = "Expand to full screen";
fsBtn.style.cssText = "position:absolute; top:4px; right:4px; z-index:10; padding:3px 7px; font-size:14px; cursor:pointer; border:1px solid #ccc; border-radius:4px; background:rgba(255,255,255,0.9);";
function makeChart(w, h) {
return Plot_s.plot({
width: w, height: h, marginLeft: 60, marginBottom: 45,
x: { label: "Cell Biomass Manufacturing Cost ($/kg, wet weight)", domain: [0, clipVal * 1.05] },
y: { label: "Frequency", grid: true },
marks: [
Plot_s.rectY(clipped, Plot_s.binX({y: "count"}, {x: d => d, fill: "steelblue", fillOpacity: 0.7})),
Plot_s.ruleX([stats_s.p5], {stroke: "green", strokeWidth: 2, strokeDasharray: "5,5"}),
Plot_s.ruleX([stats_s.p50], {stroke: "blue", strokeWidth: 3}),
Plot_s.ruleX([stats_s.p95], {stroke: "red", strokeWidth: 2, strokeDasharray: "5,5"}),
Plot_s.ruleX([p20], {stroke: "#888", strokeWidth: 1.5, strokeDasharray: "4,4", strokeOpacity: 0.85}),
Plot_s.ruleX([p80], {stroke: "#888", strokeWidth: 1.5, strokeDasharray: "4,4", strokeOpacity: 0.85}),
Plot_s.ruleX([10], {stroke: "darkgreen", strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}),
Plot_s.ruleX([25], {stroke: "orange", strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}),
Plot_s.text([
{x: stats_s.p5+1.5, y: h*6, text: `p5: $${stats_s.p5.toFixed(0)}`},
{x: stats_s.p50+1.5, y: h*7.5, text: `p50: $${stats_s.p50.toFixed(0)}`},
{x: stats_s.p95+1.5, y: h*6, text: `p95: $${stats_s.p95.toFixed(0)}`},
{x: p20+1.5, y: h*4.5, text: `p20: $${p20.toFixed(0)}`, fill: "#666"},
{x: p80+1.5, y: h*4.5, text: `p80: $${p80.toFixed(0)}`, fill: "#666"}
], {x:"x", y:"y", text:"text", fontSize: 11, fill: d => d.fill || "black"})
],
title: `Projected ${target_year_s} Cost Distribution`
});
}
const overlay = document.createElement("div");
overlay.style.cssText = "display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:white; z-index:9500; padding:2rem; box-sizing:border-box;";
const closeBtn = document.createElement("button");
closeBtn.textContent = "✕ Close";
closeBtn.style.cssText = "position:fixed; top:16px; right:20px; padding:6px 14px; font-size:14px; cursor:pointer; border:1px solid #ccc; border-radius:6px; background:#f8f9fa; z-index:9501;";
closeBtn.onclick = () => { overlay.style.display = "none"; };
overlay.appendChild(closeBtn);
document.addEventListener("keydown", e => { if (e.key==="Escape") overlay.style.display="none"; });
fsBtn.onclick = () => {
overlay.style.display = "block";
while (overlay.children.length > 2) overlay.removeChild(overlay.lastChild);
overlay.appendChild(makeChart(Math.min(window.innerWidth-80, 1600), Math.min(window.innerHeight-120, 900)));
};
document.body.appendChild(overlay);
const wrapper = document.createElement("div");
wrapper.style.cssText = "position:relative; display:inline-block; width:100%;";
wrapper.appendChild(makeChart(780, 360));
const tailNote = document.createElement("p");
tailNote.style.cssText = "font-size:0.85em;color:#666;";
tailNote.textContent = "Histogram shows the lowest 98% of draws. Summary statistics and threshold probabilities use every draw.";
wrapper.appendChild(tailNote);
wrapper.appendChild(fsBtn);
return wrapper;
}How is this cost calculated?
\[\text{Unit Cost} = \underbrace{\text{Media}}_{\text{amino acids + nutrients}} + \underbrace{\text{Growth Factors}}_{\text{FGF-2, IGF-1, etc.}} + \text{Supplemental proteins} + \underbrace{\text{Other VOC}}_{\text{utilities, consumables}} + \underbrace{\text{CAPEX/kg}}_{\text{bioreactors, annualised}} + \underbrace{\text{Overhead/kg}}_{\text{labour, maintenance}}\]
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).
Full formula documentation → Model formulas & metrics (the formulas are the same as the Advanced Model — only the background parameters listed in the sidebar are held constant here.)
Code
html`<div style="margin-top:1.5rem; padding:0.8rem; background:#f0f8ff; border:1px solid #3498db; border-radius:6px; font-size:0.88em;">
<strong>Want more control?</strong> The <a href="index.html">Advanced Model</a> exposes many more parameters: financing (<abbr title="Weighted Average Cost of Capital: the expected return investors require, blending equity and debt financing costs. Higher WACC = more expensive capital = higher CAPEX per kg.">WACC</abbr>, asset life), plant capacity, cell density, media-use multiplier, CDMO mode, bundled media pricing, and more.
<div style="margin-top:0.5rem;">
<a href="${(() => { const tot=Math.max(p_fedbatch_s+p_perfusion_s+p_continuous_s,1); const p=new URLSearchParams({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)}); for (const [key,value] of Object.entries(expert_priors)) if (value != null) p.set(`ep_${key}`,value); return 'index.html?'+p.toString(); })()}" style="font-weight:600;">→ Advanced Model (adapt these settings)</a>
</div>
</div>`