Cultured Chicken Cost Model
  • Simplest Model
  • Advanced Model
  • Learn
  • TEA Comparison
  • Cost → Demand
  • Model formulas & metrics
  • Sensitivity
  • Discuss
  • Limits/Critique
  • About
  • 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-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.

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.

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, 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
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>
  .blending-only-s { display: ${include_blending_s ? 'block' : 'none'}; }
</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.


Blended Product (?)

Code
viewof include_blending_s = Inputs.toggle({
  label: "Show blended product analysis",
  value: urlBool_s("include_blending", false)
})
Code
viewof blending_share_s = Inputs.range([5, 95], {
  value: Math.round(urlNum_s("blending_share", 0.25) * 100), step: 5,
  label: "CM inclusion rate (%)"
})

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

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),
    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
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
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
  };
}
Code
Plot_s = import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm")

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 &lt; $${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 &lt; $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 &lt; $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 &lt; $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>`
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-09-17.1`). 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-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-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, 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.
:::

```{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>
  .blending-only-s { display: ${include_blending_s ? 'block' : 'none'}; }
</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.*

---

**Blended Product** <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="Show blended product costs: cultured meat mixed with plant-based filler to lower the per-kg cost. The toggle adds a second set of probability/cost cards based on a CM-plus-filler product, which is the form most consumer-facing cultured meat is expected to take.">(?)</abbr>

```{ojs}
//| echo: false
viewof include_blending_s = Inputs.toggle({
  label: "Show blended product analysis",
  value: urlBool_s("include_blending", false)
})
```

```{=html}
<div class="blending-only-s">
```

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

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

```{=html}
</div>
```

---

**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;">5–30 g/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;">30–150 g/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;">50–200 g/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/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 |

[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),
    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>`;
}
```

:::

::: {.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,
    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
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
  };
}
```

```{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 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.


```{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;">
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>`
```

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

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

```{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,
      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;
}
```

<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 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](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),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>`
```

:::
 

Built by The Unjournal | Source Code