Simplest Model
This page runs the current model (engine 2026-09-07.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.
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
// Expert priors panel — collapsible, allows overriding the 3 biggest cost-driver distributions
viewof 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(new Event('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(new Event('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';
});
function getVal(name) {
const v = parseFloat(container.querySelector(`[name=${name}]`)?.value);
return isNaN(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 = 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)
});
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_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
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
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)}); return 'index.html?'+p.toString(); })()}" style="font-weight:600;">→ Advanced Model (adapt these settings)</a>
</div>
</div>`