Cultured Chicken Cost Model
  • Simplest Model
  • Advanced Model
  • Learn
    • How cultured chicken is made
    • TEA comparison
    • Cost → Demand
  • Methods
    • Model formulas & metrics
    • Sensitivity
  • Limits & review
    • Limits/Critique
    • Model review: September 2026
    • Responses to two model critiques
  • About
    • About this model
    • Discussion map
    • Workshop site ↗
  • an Unjournal project ↗

Simplest Model

  • Show All Code
  • Hide All Code

  • View Source
Model version and September 17 update

This page runs the current model (engine 2026-11-09.1; headline cost per kg dry cell mass since October 26, shown beside the cost of a hybrid product since October 29). Since November 7 the model includes a nutrient floor on medium use, recovery losses, failed runs and seed-train capital, which raised the default median from about $213 to $276 per kg dry; these additions are provisional and not yet reviewed by engineers (details). Since November 9 growth-factor dose follows medium use, which raises it to about $289 (correction). 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.

Corrections, 9 November 2026 (engine 2026-11-09.1). (1) Growth-factor dose was drawn independently of medium use, which allowed physically impossible combinations; it now follows medium use (concentration × litres per kg). At the defaults (seed 42) the median moves from $276 to $289/kg dry, the 90th percentile from $920 to $1,167, and the share under $25/kg hybrid from 58.2% to 55.6% (under $10: 11.6% to 12.0%). (2) The note under the results said recovery losses were not modeled; losses in harvest and washing and failed runs have been in the model since November 7. (3) Retail prices on the demand page are now labelled as retail. Details.

Preliminary model — for exploration only

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.

Basis change: costs are now per kg of DRY cell mass

Since October 26, 2026, the headline cost is per kg of dry cell mass, about five times the old per-wet-kg figures (a kg of harvested cells is only about 20% solids). Nothing about the underlying scenario changed.

Why. Washing and dewatering change what a “wet kg” contains: a paste at 15% solids carries half the protein of one at 30%. Dry mass does not change with washing, maps to nutrition, and is the natural basis for nutrient stoichiometry.

Beside every dry-basis cost, and for the probability thresholds, the page shows the cost per kg of a hybrid product: 25% cultivated cells (counted at chicken-meat moisture, 27.5% solids) and 75% plant-based ingredients at $3/kg. Products will not be made entirely of cells; there is no consensus inclusion rate, so 25% is a convention you can change below. The old wet-basis view is available on the Advanced Model under Structural stress tests → Cost basis. Why the basis matters →

AI-drafted (Claude, 26 October 2026) from David Reinstein’s design decision; not yet reviewed by a person.

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.

Parameters exposed here: Projection Year, P(Growth Factor Breakthrough), P(Hydrolysates adopted), Process Mode Mix, Hybrid product inclusion rate

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
costModel = import(new URL("./cost-model.mjs", window.location.href).href)
simulate = costModel.simulate
quantile = costModel.quantile
mean = costModel.mean
conditionalSwing = costModel.conditionalSwing
spearmanCorr = costModel.spearmanCorr
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
// Reactive CSS for blending-only visibility
html`<style>
</style>`

Adjustable Parameters

Advanced Model →

Projection Year

Code
viewof target_year_s = Inputs.range([2026, 2050], {
  value: urlNum_s("target_year", 2036), step: 1,
  label: "Projection year"
})

Further-out years give more time for cost reductions and industry scale-up.


Will a Growth Factor (GF) breakthrough happen?

Code
viewof p_recfactors_s = Inputs.range([0, 100], {
  value: Math.round(urlNum_s("p_recfactors", 0.50) * 100), step: 5,
  label: "P(GF breakthrough) %"
})

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
viewof p_hydro_s = Inputs.range([0, 100], {
  value: Math.round(urlNum_s("p_hydro", 0.75) * 100), step: 5,
  label: "P(Hydrolysates adopted) %"
})

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.


Hybrid product (?)

Code
viewof blending_share_s = Inputs.range([5, 100], {
  value: Math.round(urlNum_s("blending_share", 0.25) * 100), step: 5,
  label: "Cell inclusion rate (%)"
})

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 1–6 g dry/L 1–2× Higher cost (less dense)
Perfusion 6–30 g dry/L 1–5× Medium cost
Continuous 10–40 g dry/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 per wet kg ($5–30 per kg dry) at reference 20 kTA (thousand tonnes of wet biomass a year) scale; scales sub-linearly. Labour, maintenance, plant overhead.
Forming and texturization Not included: outside the estimate. Scaffolding, texturization and forming can be added in the Advanced Model. Output here is unstructured cell mass at the bioreactor gate. Harvest, washing and dewatering belong inside the estimate but are not yet modeled explicitly.
GF cost progress 50% Midpoint toward industry price targets
Filler cost $3/kg Plant protein / mycoprotein estimate

Full parameter definitions → Model formulas & metrics

Code
modelControls = import(new URL("./model-controls.mjs", window.location.href).href)
viewof expert_priors = modelControls.expertPriorControl(new URLSearchParams(urlParams_s))
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),
    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,
    cell_inclusion_rate: blending_share_s / 100,
    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
results_s = simulate(30000, 42, simParams_simple)
scenarioNotes = html`<div role="status">${results_s.warnings.map(w => html`<p><strong>Scenario note:</strong> ${w}</p>`)}</div>`
Code
// Headline cost is per kg dry cell mass; thresholds use the hybrid product
// (cells at meat moisture plus plant filler), as on the Advanced page.
stats_s = {
  const uc = results_s.unit_cost;
  const tc = results_s.threshold_cost;
  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),
    t_p5: quantile(tc, 0.05), t_p50: quantile(tc, 0.50), t_p95: quantile(tc, 0.95),
    toAxis: t => modelControls.hybridToDry(t, results_s.basis),
    probs: [5, 10, 25, 50].map(x => ({threshold: x, prob: pct(tc, x)})),
    // Conventional chicken at the wholesale level (decided 5 November 2026)
    chickenAxis: modelControls.hybridToDry(modelControls.CHICKEN_WHOLESALE.central, results_s.basis),
    chickenProbs: ['low', 'central', 'high'].map(k => pct(tc, modelControls.CHICKEN_WHOLESALE[k])),
    n: uc.length
  };
}
Code
Plot_s = import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm")

Results

Quantity being modeled: manufacturing cost per kg of cultured-chicken dry cell mass 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;">
Each headline value is given twice: <strong>per kg of dry cell mass</strong>, the model's basis, and per kg of <strong>${modelControls.hybridLabel(results_s.basis)}</strong>: ${(results_s.basis.cell_inclusion_rate * 100).toFixed(0)}% cultivated cells, counted at the water content of chicken meat (${(results_s.basis.target_solids_fraction * 100).toFixed(1)}% solids), and the rest plant-based ingredients at $${results_s.basis.filler_cost}/kg. The probability thresholds use the hybrid product. All-cell meat at chicken-meat moisture would be $${modelControls.formatMeatEquivalent(quantile(results_s.unit_cost_meat_equiv, 0.5))}/kg (median). Both are factory-gate ingredient costs, not consumer-product prices, so the comparison point for conventional chicken is its US wholesale price (boneless, about $${modelControls.CHICKEN_WHOLESALE.central.toFixed(2)}/kg), not the retail price. Recovery losses in harvest and washing (${(results_s.effective_params.recovery_loss_p5 * 100).toFixed(0)}–${(results_s.effective_params.recovery_loss_p95 * 100).toFixed(0)}%) and failed runs are included and raise costs per delivered kg by a median of ${((quantile(results_s.loss_factor_samples, 0.5) - 1) * 100).toFixed(0)}%; the cost of the washing and dewatering steps themselves is not. Growth-factor dose follows medium use. Source dollars are not yet normalized to one real-dollar year. The intended target is unconditional and hypothetical (the cost of a large plant built with the best technology available in the target year, whether or not such plants exist); parameter ranges have not yet been re-checked against it. Based on ${stats_s.n.toLocaleString()} Monte Carlo simulations.
</div>`
Code
{
  // Dry cost and the hybrid-product cost at equal size, as on the Advanced page.
  // Built per card: a DOM node can only appear in one place.
  const meatTag = () => html`<span title="${modelControls.hybridNote(results_s.basis)}" style="cursor: help; text-decoration: underline dotted;">${modelControls.hybridLabel(results_s.basis)} &#9432;</span>`;
  const small = text => html`<span style="font-size: 0.55em; font-weight: 400;">${text}</span>`;
  const costs = (dryCost, meatCost) => html`<h2 style="margin:0.5rem 0 0;">$${Math.round(dryCost)}${small("/kg dry cell mass")}</h2>
  <h2 style="margin:0 0 0.5rem;">$${modelControls.formatMeatEquivalent(meatCost)}${small(html`/kg ${meatTag()}`)}</h2>`;
  return 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>
  ${costs(stats_s.p50, stats_s.t_p50)}
  <small>Half of simulations above, half below</small>
</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>
  ${costs(stats_s.p5, stats_s.t_p5)}
  <small>Only 5% of simulations cheaper</small>
</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>
  ${costs(stats_s.p95, stats_s.t_p95)}
  <small>95% of simulations cheaper</small>
</div>

</div>`;
}

Probability Thresholds

Code
{
  function card(thresh, prob, 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(Hybrid product &lt; $${thresh}/kg = $${Math.round(stats_s.toAxis(thresh))}/kg dry)</h5>
      <h2 style="color:${color}; margin:0.2rem 0;">${prob.toFixed(1)}%</h2>
      <small style="color:#666;">illustrative ingredient-cost threshold</small>
    </div>`;
  }
  const colors = ['#27ae60', '#3498db', '#f39c12', '#e74c3c'];
  const grid = `<div class="grid" style="grid-template-columns:repeat(4,1fr); gap:0.75rem; margin-bottom:1.5rem;">
    ${stats_s.probs.map((d, i) => card(d.threshold, d.prob, colors[i])).join("")}
  </div>`;
  const c = modelControls.CHICKEN_WHOLESALE;
  const [pLow, pMid, pHigh] = stats_s.chickenProbs;
  const fmt = p => p < 0.05 && p > 0 ? "under 0.1%" : `${p.toFixed(1)}%`;
  const note = modelControls.chickenWholesaleNote(results_s.basis).replace(/"/g, '&quot;');
  const chickenLine = `<p style="font-size:0.9em; margin:-0.75rem 0 1.5rem;" title="${note}"><strong>Compared with conventional chicken at wholesale:</strong> P(hybrid product &lt; $${c.central.toFixed(2)}/kg, US wholesale boneless chicken) = <strong>${fmt(pMid)}</strong> (${fmt(pLow)} to ${fmt(pHigh)} across the middle 80% of monthly prices, $${c.low.toFixed(1)}–$${c.high.toFixed(1)}/kg). <span style="color:#666;">The cards above are illustrative cost levels, not parity lines. Retail chicken (about $${c.retail.toFixed(2)}/kg) is not a like-for-like comparison. <a href="docs.html#chicken-comparison">Why wholesale →</a></span></p>`;
  return html([grid + chickenLine]);
}

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, marginTop: 45,
      x: { label: "Cell Biomass Manufacturing Cost ($/kg dry cell mass)", domain: [0, clipVal * 1.05] },
      y: { label: "Frequency", grid: true },
      marks: [
        ...modelControls.hybridAxes(Plot_s, clipVal * 1.05, results_s.basis),
        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}),
        // $10 and $25 hybrid product, placed on the dry-cost axis
        Plot_s.ruleX([stats_s.toAxis(10)], {stroke: "darkgreen", strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}),
        Plot_s.ruleX([stats_s.toAxis(25)], {stroke: "orange",    strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}),
        // US wholesale boneless chicken; omitted if the filler alone costs more (off the axis)
        ...(stats_s.chickenAxis > 0 ? [
          Plot_s.ruleX([stats_s.chickenAxis], {stroke: "#8e44ad", strokeWidth: 2, strokeDasharray: "6,3", strokeOpacity: 0.85}),
          Plot_s.text([{x: stats_s.chickenAxis, y: h*3, text: `wholesale chicken $${modelControls.CHICKEN_WHOLESALE.central.toFixed(2)}`}],
            {x: "x", y: "y", text: "text", textAnchor: "start", dx: 4, fontSize: 10, fill: "#8e44ad"})
        ] : []),
        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) and price ($/g), with a binary regime switch based on P(GF breakthrough). Since November 9, 2026 the quantity follows medium use: litres of medium per kg × a fixed growth-factor concentration (0.102 mg/L), reduced to 40% of that in the breakthrough regime.
  • 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),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>`
Source Code
---
title: "Simplest Model"
format:
  html:
    page-layout: full
    css: styles.css
    include-in-header:
      text: |
        <script>
        // Strip URL query params before Hypothes.is loads so annotations
        // anchor to the canonical bare URL, while OJS gets params via global.
        (function () {
          try {
            if (!window.location.search) return;
            var usp = new URLSearchParams(window.location.search);
            window.__CM_URL_STATE__ = {};
            usp.forEach(function (v, k) { window.__CM_URL_STATE__[k] = v; });
            history.replaceState(null, "", window.location.pathname + window.location.hash);
          } catch (e) {}
        })();
        </script>
    include-after-body:
      text: |
        <script src="https://hypothes.is/embed.js" async></script>
---

::: {.callout-note collapse="true"}
## Model version and September 17 update

This page runs the **current model** (engine `2026-11-09.1`; headline cost per kg dry cell mass since October 26, shown beside the cost of a hybrid product since October 29). Since November 7 the model includes a nutrient floor on medium use, recovery losses, failed runs and seed-train capital, which raised the default median from about \$213 to \$276 per kg dry; these additions are provisional and not yet reviewed by engineers ([details](docs.qmd#physical-consistency)). Since November 9 growth-factor dose follows medium use, which raises it to about \$289 ([correction](docs.qmd#gf-dose-linked)). See the [September 2026 review and correction log](review-2026-09.qmd). For comparisons with beliefs recorded at the May 8 workshop, use the [hosted workshop-era model](archive/workshop-2026-05-08/) or its [tagged source](https://github.com/unjournal/cm_pq_modeling/tree/workshop-2026-05-08). 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](review-response-2026-09.qmd). These additions are AI-implemented scenario tests, pending expert review.
:::


::: {.callout-important appearance="simple"}
**Corrections, 9 November 2026 (engine `2026-11-09.1`).** (1) Growth-factor dose was drawn independently of medium use, which allowed physically impossible combinations; it now follows medium use (concentration × litres per kg). At the defaults (seed 42) the median moves from \$276 to \$289/kg dry, the 90th percentile from \$920 to \$1,167, and the share under \$25/kg hybrid from 58.2% to 55.6% (under \$10: 11.6% to 12.0%). (2) The note under the results said recovery losses were not modeled; losses in harvest and washing and failed runs have been in the model since November 7. (3) Retail prices on the [demand page](demand.qmd) are now labelled as retail. [Details](review-response-2026-09.qmd#corrections-2026-11-09).
:::

::: {.callout-warning}
## Preliminary model — for exploration only
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](index.qmd).
:::

::: {.callout-important collapse="true"}
## Basis change: costs are now per kg of DRY cell mass
**Since October 26, 2026, the headline cost is per kg of dry cell mass**, about five times the old per-wet-kg figures (a kg of harvested cells is only about 20% solids). Nothing about the underlying scenario changed.

**Why.** Washing and dewatering change what a "wet kg" contains: a paste at 15% solids carries half the protein of one at 30%. Dry mass does not change with washing, maps to nutrition, and is the natural basis for nutrient stoichiometry.

**Beside every dry-basis cost**, and for the probability thresholds, the page shows the cost per kg of a **hybrid product**: 25% cultivated cells (counted at chicken-meat moisture, 27.5% solids) and 75% plant-based ingredients at \$3/kg. Products will not be made entirely of cells; there is no consensus inclusion rate, so 25% is a convention you can change below. The old wet-basis view is available on the [Advanced Model](index.qmd) under *Structural stress tests → Cost basis*. [Why the basis matters →](docs.qmd#output-basis)

*AI-drafted (Claude, 26 October 2026) from David Reinstein's design decision; not yet reviewed by a person.*
:::

::: {.callout-note collapse="true"}
## 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.

**Parameters exposed here:** Projection Year, P(Growth Factor Breakthrough), P(Hydrolysates adopted), Process Mode Mix, Hybrid product inclusion rate

**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.
:::

```{ojs}
//| echo: false

// ============================================================
// SEEDED RANDOM NUMBER GENERATOR
// ============================================================
costModel = import(new URL("./cost-model.mjs", window.location.href).href)
simulate = costModel.simulate
quantile = costModel.quantile
mean = costModel.mean
conditionalSwing = costModel.conditionalSwing
spearmanCorr = costModel.spearmanCorr

```

```{ojs}
//| echo: false
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";
}
```

```{ojs}
//| echo: false
// Reactive CSS for blending-only visibility
html`<style>
</style>`
```

::: {.panel-sidebar}

### Adjustable Parameters

```{=html}
<div style="display: flex; gap: 5px; margin-bottom: 0.75rem; position: sticky; top: var(--quarto-navbar-height, 62px); background: white; padding: 0.5rem 0; border-bottom: 1px solid #eee; z-index: 5; margin-top: -0.5rem;">
  <a href="index.html" style="flex:1; text-align:center; padding:0.4rem 0.3rem; font-size:0.82rem; border:1px solid #3498db; border-radius:6px; background:#f0f8ff; color:#1a5276; font-weight:500; text-decoration:none;">
    Advanced Model →
  </a>
  <button onclick="window.location.href=window.location.pathname" title="Reset all to defaults" style="padding:0.4rem 0.5rem; font-size:0.82rem; cursor:pointer; border:1px solid #c0392b; border-radius:6px; background:#fef9f9; color:#922b21; font-weight:500;">
    ↺ Reset
  </button>
  <button onclick="document.querySelectorAll('details').forEach(function(d){d.setAttribute('open','')})" title="Expand all sections" style="padding:0.4rem 0.4rem; font-size:0.82rem; cursor:pointer; border:1px solid #aaa; border-radius:6px; background:#f9f9f9; color:#555;">▼ All</button>
  <button onclick="document.querySelectorAll('details[open]').forEach(function(d){d.removeAttribute('open')})" title="Collapse all sections" style="padding:0.4rem 0.4rem; font-size:0.82rem; cursor:pointer; border:1px solid #aaa; border-radius:6px; background:#f9f9f9; color:#555;">▲ All</button>
</div>
```

---

**Projection Year**

```{ojs}
//| echo: false
viewof target_year_s = Inputs.range([2026, 2050], {
  value: urlNum_s("target_year", 2036), step: 1,
  label: "Projection year"
})
```

*Further-out years give more time for cost reductions and industry scale-up.*

---

**Will a Growth Factor (GF) breakthrough happen?**

```{ojs}
//| echo: false
viewof p_recfactors_s = Inputs.range([0, 100], {
  value: Math.round(urlNum_s("p_recfactors", 0.50) * 100), step: 5,
  label: "P(GF breakthrough) %"
})
```

*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?**

```{ojs}
//| echo: false
viewof p_hydro_s = Inputs.range([0, 100], {
  value: Math.round(urlNum_s("p_hydro", 0.75) * 100), step: 5,
  label: "P(Hydrolysates adopted) %"
})
```

*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.*

---

**Hybrid product** <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="Cultivated meat products will not be made entirely of cells. The second cost on each card, and the probability thresholds, are for a kg of product with this share of cultivated cells (counted at chicken-meat moisture) and the rest plant-based ingredients at $3/kg. GOOD Meat 3 had 3% cells; hybrids are often described as 20–50% non-meat. 25% is a convention.">(?)</abbr>

```{ojs}
//| echo: false
viewof blending_share_s = Inputs.range([5, 100], {
  value: Math.round(urlNum_s("blending_share", 0.25) * 100), step: 5,
  label: "Cell inclusion rate (%)"
})
```

---

**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%.*

```{ojs}
//| echo: false
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 %"
})
```

```{ojs}
//| echo: false
{
  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>`;
}
```

```{=html}
<table style="width:100%; font-size:0.82em; border-collapse:collapse; margin-bottom:0.5rem;">
<thead><tr style="border-bottom:1px solid #ddd; color:#555;">
  <th style="padding:3px 4px; text-align:left;">Mode</th>
  <th style="padding:3px 4px; text-align:left;">Density</th>
  <th style="padding:3px 4px; text-align:left;">Media use</th>
  <th style="padding:3px 4px; text-align:left;">Cost implication</th>
</tr></thead>
<tbody>
<tr style="border-bottom:1px solid #f0f0f0;">
  <td style="padding:3px 4px;"><strong>Fed-batch</strong></td>
  <td style="padding:3px 4px;">1–6 g dry/L</td>
  <td style="padding:3px 4px;">1–2×</td>
  <td style="padding:3px 4px; color:#c0392b;">Higher cost (less dense)</td>
</tr>
<tr style="border-bottom:1px solid #f0f0f0;">
  <td style="padding:3px 4px;"><strong>Perfusion</strong></td>
  <td style="padding:3px 4px;">6–30 g dry/L</td>
  <td style="padding:3px 4px;">1–5×</td>
  <td style="padding:3px 4px; color:#e67e22;">Medium cost</td>
</tr>
<tr>
  <td style="padding:3px 4px;"><strong>Continuous</strong></td>
  <td style="padding:3px 4px;">10–40 g dry/L</td>
  <td style="padding:3px 4px;">0.5–3×</td>
  <td style="padding:3px 4px; color:#27ae60;">Lower cost (denser)</td>
</tr>
</tbody>
</table>
```

*Pure batch (single fill-and-dump) is excluded — not considered commercially viable at scale.*

---

<details>
<summary><a href="docs.html">Background parameters</a> held constant in this model</summary>

| 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. |
| <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> (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 per wet kg (\$5–30 per kg dry) at reference 20 kTA (thousand tonnes of wet biomass a year) scale; scales sub-linearly. | Labour, maintenance, plant overhead. |
| Forming and texturization | Not included: outside the estimate. Scaffolding, texturization and forming can be added in the Advanced Model. | Output here is unstructured cell mass at the bioreactor gate. Harvest, washing and dewatering belong inside the estimate but are not yet modeled explicitly. |
| GF cost progress | 50% | Midpoint toward industry price targets |
| Filler cost | $3/kg | Plant protein / mycoprotein estimate |

[Full parameter definitions → Model formulas & metrics](docs.html)

</details>

```{ojs}
//| echo: false
// Expert priors panel — collapsible, allows overriding the 3 biggest cost-driver distributions
modelControls = import(new URL("./model-controls.mjs", window.location.href).href)
viewof expert_priors = modelControls.expertPriorControl(new URLSearchParams(urlParams_s))
```

```{ojs}
//| echo: false
// "→ 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),
    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>`;
}
```

:::

::: {.panel-fill}

```{ojs}
//| echo: false
// 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,
    cell_inclusion_rate: blending_share_s / 100,
    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
  };
}
```

```{ojs}
//| echo: false
results_s = simulate(30000, 42, simParams_simple)
scenarioNotes = html`<div role="status">${results_s.warnings.map(w => html`<p><strong>Scenario note:</strong> ${w}</p>`)}</div>`
```

```{ojs}
//| echo: false
// Headline cost is per kg dry cell mass; thresholds use the hybrid product
// (cells at meat moisture plus plant filler), as on the Advanced page.
stats_s = {
  const uc = results_s.unit_cost;
  const tc = results_s.threshold_cost;
  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),
    t_p5: quantile(tc, 0.05), t_p50: quantile(tc, 0.50), t_p95: quantile(tc, 0.95),
    toAxis: t => modelControls.hybridToDry(t, results_s.basis),
    probs: [5, 10, 25, 50].map(x => ({threshold: x, prob: pct(tc, x)})),
    // Conventional chicken at the wholesale level (decided 5 November 2026)
    chickenAxis: modelControls.hybridToDry(modelControls.CHICKEN_WHOLESALE.central, results_s.basis),
    chickenProbs: ['low', 'central', 'high'].map(k => pct(tc, modelControls.CHICKEN_WHOLESALE[k])),
    n: uc.length
  };
}
```

```{ojs}
//| echo: false
Plot_s = import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm")
```

### Results

**Quantity being modeled:** manufacturing cost per kg of cultured-chicken dry cell mass 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.


```{ojs}
//| echo: false
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;">
Each headline value is given twice: <strong>per kg of dry cell mass</strong>, the model's basis, and per kg of <strong>${modelControls.hybridLabel(results_s.basis)}</strong>: ${(results_s.basis.cell_inclusion_rate * 100).toFixed(0)}% cultivated cells, counted at the water content of chicken meat (${(results_s.basis.target_solids_fraction * 100).toFixed(1)}% solids), and the rest plant-based ingredients at $${results_s.basis.filler_cost}/kg. The probability thresholds use the hybrid product. All-cell meat at chicken-meat moisture would be $${modelControls.formatMeatEquivalent(quantile(results_s.unit_cost_meat_equiv, 0.5))}/kg (median). Both are factory-gate ingredient costs, not consumer-product prices, so the comparison point for conventional chicken is its US wholesale price (boneless, about $${modelControls.CHICKEN_WHOLESALE.central.toFixed(2)}/kg), not the retail price. Recovery losses in harvest and washing (${(results_s.effective_params.recovery_loss_p5 * 100).toFixed(0)}–${(results_s.effective_params.recovery_loss_p95 * 100).toFixed(0)}%) and failed runs are included and raise costs per delivered kg by a median of ${((quantile(results_s.loss_factor_samples, 0.5) - 1) * 100).toFixed(0)}%; the cost of the washing and dewatering steps themselves is not. Growth-factor dose follows medium use. Source dollars are not yet normalized to one real-dollar year. The intended target is unconditional and hypothetical (the cost of a large plant built with the best technology available in the target year, whether or not such plants exist); parameter ranges have not yet been re-checked against it. Based on ${stats_s.n.toLocaleString()} Monte Carlo simulations.
</div>`
```

```{ojs}
//| echo: false
{
  // Dry cost and the hybrid-product cost at equal size, as on the Advanced page.
  // Built per card: a DOM node can only appear in one place.
  const meatTag = () => html`<span title="${modelControls.hybridNote(results_s.basis)}" style="cursor: help; text-decoration: underline dotted;">${modelControls.hybridLabel(results_s.basis)} &#9432;</span>`;
  const small = text => html`<span style="font-size: 0.55em; font-weight: 400;">${text}</span>`;
  const costs = (dryCost, meatCost) => html`<h2 style="margin:0.5rem 0 0;">$${Math.round(dryCost)}${small("/kg dry cell mass")}</h2>
  <h2 style="margin:0 0 0.5rem;">$${modelControls.formatMeatEquivalent(meatCost)}${small(html`/kg ${meatTag()}`)}</h2>`;
  return 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>
  ${costs(stats_s.p50, stats_s.t_p50)}
  <small>Half of simulations above, half below</small>
</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>
  ${costs(stats_s.p5, stats_s.t_p5)}
  <small>Only 5% of simulations cheaper</small>
</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>
  ${costs(stats_s.p95, stats_s.t_p95)}
  <small>95% of simulations cheaper</small>
</div>

</div>`;
}
```

### Probability Thresholds

```{ojs}
//| echo: false
{
  function card(thresh, prob, 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(Hybrid product &lt; $${thresh}/kg = $${Math.round(stats_s.toAxis(thresh))}/kg dry)</h5>
      <h2 style="color:${color}; margin:0.2rem 0;">${prob.toFixed(1)}%</h2>
      <small style="color:#666;">illustrative ingredient-cost threshold</small>
    </div>`;
  }
  const colors = ['#27ae60', '#3498db', '#f39c12', '#e74c3c'];
  const grid = `<div class="grid" style="grid-template-columns:repeat(4,1fr); gap:0.75rem; margin-bottom:1.5rem;">
    ${stats_s.probs.map((d, i) => card(d.threshold, d.prob, colors[i])).join("")}
  </div>`;
  const c = modelControls.CHICKEN_WHOLESALE;
  const [pLow, pMid, pHigh] = stats_s.chickenProbs;
  const fmt = p => p < 0.05 && p > 0 ? "under 0.1%" : `${p.toFixed(1)}%`;
  const note = modelControls.chickenWholesaleNote(results_s.basis).replace(/"/g, '&quot;');
  const chickenLine = `<p style="font-size:0.9em; margin:-0.75rem 0 1.5rem;" title="${note}"><strong>Compared with conventional chicken at wholesale:</strong> P(hybrid product &lt; $${c.central.toFixed(2)}/kg, US wholesale boneless chicken) = <strong>${fmt(pMid)}</strong> (${fmt(pLow)} to ${fmt(pHigh)} across the middle 80% of monthly prices, $${c.low.toFixed(1)}–$${c.high.toFixed(1)}/kg). <span style="color:#666;">The cards above are illustrative cost levels, not parity lines. Retail chicken (about $${c.retail.toFixed(2)}/kg) is not a like-for-like comparison. <a href="docs.html#chicken-comparison">Why wholesale →</a></span></p>`;
  return html([grid + chickenLine]);
}
```

### Cost Distribution

```{ojs}
//| echo: false
{
  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, marginTop: 45,
      x: { label: "Cell Biomass Manufacturing Cost ($/kg dry cell mass)", domain: [0, clipVal * 1.05] },
      y: { label: "Frequency", grid: true },
      marks: [
        ...modelControls.hybridAxes(Plot_s, clipVal * 1.05, results_s.basis),
        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}),
        // $10 and $25 hybrid product, placed on the dry-cost axis
        Plot_s.ruleX([stats_s.toAxis(10)], {stroke: "darkgreen", strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}),
        Plot_s.ruleX([stats_s.toAxis(25)], {stroke: "orange",    strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}),
        // US wholesale boneless chicken; omitted if the filler alone costs more (off the axis)
        ...(stats_s.chickenAxis > 0 ? [
          Plot_s.ruleX([stats_s.chickenAxis], {stroke: "#8e44ad", strokeWidth: 2, strokeDasharray: "6,3", strokeOpacity: 0.85}),
          Plot_s.text([{x: stats_s.chickenAxis, y: h*3, text: `wholesale chicken $${modelControls.CHICKEN_WHOLESALE.central.toFixed(2)}`}],
            {x: "x", y: "y", text: "text", textAnchor: "start", dx: 4, fontSize: 10, fill: "#8e44ad"})
        ] : []),
        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;
}
```

<details>
<summary>How is this cost calculated?</summary>

$$\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) and price ($/g), with a binary regime switch based on P(GF breakthrough). Since November 9, 2026 the quantity follows medium use: litres of medium per kg × a fixed growth-factor concentration (0.102 mg/L), reduced to 40% of that in the breakthrough regime.
- **CAPEX** is annualised via the Capital Recovery Factor: CRF = r(1+r)^n / ((1+r)^n − 1).

[Full formula documentation → Model formulas & metrics](docs.html) (the formulas are the same as the Advanced Model — only the background parameters listed in the sidebar are held constant here.)

</details>

```{ojs}
//| echo: false
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),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>`
```

:::
 

Built by The Unjournal | Source Code