Cultured Chicken Cost Model
  • Dashboard
  • Learn
  • Technical Reference
  • About

On this page

  • Interactive Model
    • Model Parameters
  • Quick Start
  • Pivotal Questions Context
  • Technical Reference
  • Key Insights
  • Sources

Cultured Chicken Production Cost Model

  • Show All Code
  • Hide All Code

  • View Source

Interactive Monte Carlo TEA for Cost Projections

Upcoming Workshop: Cultivated Meat Cost Trajectories

We’re organizing an online expert workshop (late April / early May 2026) to dig into the key cost cruxes — media costs, bioreactor scale-up, and the gap between TEA projections and commercial reality. This model is one of the tools we’ll use.

Workshop details & signup → · State your cost beliefs →

We Want Your Feedback!

Comment directly on this page using Hypothesis — click the < tab on the right edge. Highlight any text, parameter, or result to annotate it. We actively monitor comments and will respond to questions, incorporate suggestions, and improve the model based on your feedback.

🎧 Listen: Technical Review (22 min MP3) — Audio walkthrough of model architecture and areas for review

Other ways to reach us: Open a GitHub issue · Email contact@unjournal.org

New to Cultured Meat?

Read our deep dive: How Cultured Chicken is Made — a detailed guide covering cell banking, bioreactors, media composition, growth factors, and why each step affects costs.

Quick summary: Cultured chicken is produced by growing avian muscle cells in bioreactors. The main cost drivers are media (amino acids, nutrients), growth factors (signaling proteins), bioreactors (capital equipment), and operating costs.

   CELL BANK  →  SEED TRAIN  →  PRODUCTION  →  HARVEST  →  PRODUCT
      [O]          [OOO]        [OOOOOOO]       [===]       [≡≡≡]
Learn: Understanding Uncertainty & Distributions

This model uses Monte Carlo simulation: we sample thousands of possible futures and see what distribution of costs emerges.

Distribution Types Used:

Parameter Type Distribution Why Examples in model
Costs, intensities Lognormal Always positive, right-skewed (some very high values possible) Media cost per liter, GF price per gram, cell density
Probabilities, fractions Beta Bounded between 0 and 1, flexible shape Maturity factor, adoption probabilities, uptime. For switching parameters (e.g., hydrolysate adoption), the model first samples an adoption probability from a Beta distribution, then uses a Bernoulli draw to determine whether each simulation run uses the “adopted” or “non-adopted” cost regime.
Bounded ranges Uniform Equal probability within bounds, used when we have only a plausible range and no reason to favor values within it Asset life (years), CAPEX scale exponent, fixed OPEX scaling factor

We are open to considering alternative distributional forms (e.g., triangular, PERT, or mixture distributions) and to making the modeling flexible enough for users to select or compare different distributional assumptions. Suggestions are welcome via Hypothesis annotations.

Reading the Results:

  • p5 (5th percentile): “Optimistic” - only 5% of simulations are cheaper
  • p50 (median): Middle outcome - half above, half below
  • p95 (95th percentile): “Pessimistic” - 95% of simulations cheaper

The “Maturity” Correlation:

A key feature is the latent maturity factor that links:

  • Technology adoption rates
  • Reactor costs
  • Financing costs (WACC)

In “good worlds” for cultured chicken (high maturity), multiple things improve together. This prevents unrealistic scenarios where technology succeeds but financing remains difficult.

This approach draws on the concept of copula-based correlation in Monte Carlo simulation, where a shared latent variable induces dependence among otherwise independent marginal distributions. See Vose (2008), Risk Analysis for a textbook treatment of correlated sampling in cost models, and Morgan & Henrion (1990), Uncertainty for foundational discussion of dependent uncertainties.

More detail on the maturity factor implementation The maturity factor is sampled once per simulation from a Beta distribution (mean set by the “Industry Maturity” slider, standard deviation 0.20). It then shifts the adoption probabilities (hydrolysates, food-grade, recombinant GFs) and adjusts WACC downward in high-maturity draws. This creates positive correlation among “good news” variables without imposing a rigid structure. The strength of the correlation is moderate by design – maturity explains some but not all of the variation in each parameter.

Interactive Model

Code
function mulberry32(seed) {
  return function() {
    let t = seed += 0x6D2B79F5;
    t = Math.imul(t ^ t >>> 15, t | 1);
    t ^= t + Math.imul(t ^ t >>> 7, t | 61);
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  }
}

// ============================================================
// STATISTICAL SAMPLING FUNCTIONS
// ============================================================

// Box-Muller transform for standard normal
function boxMuller(rng) {
  const u1 = rng();
  const u2 = rng();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

// Sample from lognormal given p5 and p95
function sampleLognormalP5P95(rng, p5, p95, n) {
  const Z_95 = 1.6448536269514722;
  const mu = (Math.log(p5) + Math.log(p95)) / 2;
  const sigma = (Math.log(p95) - Math.log(p5)) / (2 * Z_95);

  const samples = new Array(n);
  for (let i = 0; i < n; i++) {
    samples[i] = Math.exp(mu + sigma * boxMuller(rng));
  }
  return samples;
}

// Sample uniform
function sampleUniform(rng, lo, hi, n) {
  const samples = new Array(n);
  for (let i = 0; i < n; i++) {
    samples[i] = lo + (hi - lo) * rng();
  }
  return samples;
}

// Beta distribution parameters from mean/stdev
function betaFromMeanStdev(mean, stdev) {
  let variance = stdev * stdev;
  const maxVar = mean * (1 - mean);
  if (variance <= 0 || variance >= maxVar) {
    stdev = Math.sqrt(maxVar * 0.99);
    variance = stdev * stdev;
  }
  const t = maxVar / variance - 1;
  const a = Math.max(mean * t, 0.01);
  const b = Math.max((1 - mean) * t, 0.01);
  return [a, b];
}

// Sample from beta using Joehnk's algorithm (simple, works for all a,b > 0)
function sampleBeta(rng, a, b) {
  // Use gamma ratio method
  const gammaA = sampleGamma(rng, a);
  const gammaB = sampleGamma(rng, b);
  return gammaA / (gammaA + gammaB);
}

// Sample from gamma distribution (Marsaglia and Tsang's method)
function sampleGamma(rng, shape) {
  if (shape < 1) {
    return sampleGamma(rng, shape + 1) * Math.pow(rng(), 1 / shape);
  }

  const d = shape - 1/3;
  const c = 1 / Math.sqrt(9 * d);

  while (true) {
    let x, v;
    do {
      x = boxMuller(rng);
      v = 1 + c * x;
    } while (v <= 0);

    v = v * v * v;
    const u = rng();

    if (u < 1 - 0.0331 * (x * x) * (x * x)) return d * v;
    if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) return d * v;
  }
}

// Sample from beta given mean/stdev
function sampleBetaMeanStdev(rng, mean, stdev, n) {
  const [a, b] = betaFromMeanStdev(mean, stdev);
  const samples = new Array(n);
  for (let i = 0; i < n; i++) {
    samples[i] = sampleBeta(rng, a, b);
  }
  return samples;
}

// Capital Recovery Factor
function crf(wacc, nYears) {
  return wacc * Math.pow(1 + wacc, nYears) / (Math.pow(1 + wacc, nYears) - 1);
}

// Clip values between min and max
function clip(arr, min, max) {
  return arr.map(v => Math.min(Math.max(v, min), max));
}

// Element-wise operations
function add(a, b) { return a.map((v, i) => v + b[i]); }
function mul(a, b) { return a.map((v, i) => v * b[i]); }
function div(a, b) { return a.map((v, i) => v / b[i]); }
function scale(arr, s) { return arr.map(v => v * s); }

// ============================================================
// MAIN SIMULATION FUNCTION
// ============================================================

function simulate(n, seed, params) {
  const rng = mulberry32(seed);

  // Latent maturity factor
  const maturity = sampleBetaMeanStdev(rng, params.maturity_mean, 0.20, n);

  // Scale and uptime
  const plant_kta = sampleLognormalP5P95(rng, params.plant_kta_p5, params.plant_kta_p95, n);
  const uptime = sampleBetaMeanStdev(rng, params.uptime_mean, 0.05, n);
  const output_kgpy = mul(scale(plant_kta, 1e6), uptime);

  // Adoption probabilities (maturity-adjusted)
  let p_hydro = sampleBetaMeanStdev(rng, params.p_hydro_mean, 0.10, n);
  p_hydro = clip(add(p_hydro, scale(maturity.map(m => m - 0.5), 0.25)), 0, 1);

  let p_foodgrade = sampleBetaMeanStdev(rng, params.p_foodgrade_mean, 0.10, n);
  p_foodgrade = clip(add(p_foodgrade, scale(maturity.map(m => m - 0.5), 0.20)), 0, 1);

  let p_recf = sampleBetaMeanStdev(rng, params.p_recfactors_mean, 0.15, n);
  p_recf = clip(add(p_recf, scale(maturity.map(m => m - 0.5), 0.25)), 0, 1);

  // Bernoulli draws for adoption
  const is_hydro = p_hydro.map(p => rng() < p);
  const is_foodgrade = p_foodgrade.map(p => rng() < p);
  const is_recf_cheap = p_recf.map(p => rng() < p);

  // Process intensities
  const density_gL = sampleLognormalP5P95(rng, params.density_gL_p5, params.density_gL_p95, n);
  const cycle_days = sampleLognormalP5P95(rng, 0.5, 5.0, n);
  const media_turnover = sampleLognormalP5P95(rng, params.media_turnover_p5, params.media_turnover_p95, n);
  const L_per_kg = mul(div(density_gL.map(_ => 1000), density_gL), media_turnover);

  // Media cost
  const media_cost_hydro = sampleLognormalP5P95(rng, 0.2, 1.2, n);
  const media_cost_pharma = sampleLognormalP5P95(rng, 1.0, 4.0, n);
  const media_cost_L = is_hydro.map((h, i) => h ? media_cost_hydro[i] : media_cost_pharma[i]);
  const cost_media = mul(L_per_kg, media_cost_L);

  // Commodity micronutrients
  const g_comm_food = sampleLognormalP5P95(rng, 0.1, 2.0, n);
  const g_comm_pharma = sampleLognormalP5P95(rng, 1.0, 10.0, n);
  const g_comm = is_foodgrade.map((f, i) => f ? g_comm_food[i] : g_comm_pharma[i]);

  const price_comm_food = sampleLognormalP5P95(rng, 0.02, 2.0, n);
  const price_comm_pharma = sampleLognormalP5P95(rng, 0.5, 20.0, n);
  const price_comm = is_foodgrade.map((f, i) => f ? price_comm_food[i] : price_comm_pharma[i]);
  const cost_comm_micros = mul(g_comm, price_comm);

  // Recombinant growth factors (FGF-2, IGF-1, TGF-β, etc.)
  // CRITICAL: Literature shows GFs can be 55-95% of media cost at current prices
  // Current prices: FGF-2 ~$50,000/g, TGF-β up to $1M/g
  // Target prices: $1-10/g with scaled recombinant production

  // Quantity (g/kg meat) - based on typical concentrations in media
  // ~10-100 ng/mL in media × 20-60 L/kg = 0.0002-0.006 g/kg
  const g_recf_cheap = sampleLognormalP5P95(rng, 1e-4, 5e-3, n);  // Reduced usage with optimization
  const g_recf_exp = sampleLognormalP5P95(rng, 5e-4, 2e-2, n);   // Standard usage
  const g_recf = is_recf_cheap.map((c, i) => c ? g_recf_cheap[i] : g_recf_exp[i]);

  // Price ($/g) - Scaled by gf_progress parameter (0-100%)
  // At 0% progress: current prices ($5k-500k expensive, $100-10k cheap)
  // At 100% progress: target prices ($50-5k expensive, $1-100 cheap)
  const progress = params.gf_progress / 100;  // 0 to 1

  // Interpolate price ranges based on progress
  // Cheap scenario: $100-10,000 at 0% → $1-100 at 100%
  const cheap_p5 = 100 * Math.pow(0.01, progress);   // 100 → 1
  const cheap_p95 = 10000 * Math.pow(0.01, progress); // 10000 → 100
  const price_recf_cheap = sampleLognormalP5P95(rng, cheap_p5, cheap_p95, n);

  // Expensive scenario: $5,000-500,000 at 0% → $50-5,000 at 100%
  const exp_p5 = 5000 * Math.pow(0.01, progress);    // 5000 → 50
  const exp_p95 = 500000 * Math.pow(0.01, progress); // 500000 → 5000
  const price_recf_exp = sampleLognormalP5P95(rng, exp_p5, exp_p95, n);

  const price_recf = is_recf_cheap.map((c, i) => c ? price_recf_cheap[i] : price_recf_exp[i]);
  const cost_recf = mul(g_recf, price_recf);

  // Other variable costs
  const other_var = sampleLognormalP5P95(rng, 0.5, 5.0, n);

  // VOC total
  const voc = add(add(add(cost_media, cost_comm_micros), cost_recf), other_var);

  // CAPEX calculation
  let capex_perkg = new Array(n).fill(0);
  if (params.include_capex) {
    const prod_kg_L_day = div(scale(density_gL, 1/1000), cycle_days);
    const total_working_volume_L = div(output_kgpy, scale(prod_kg_L_day, 365));
    const reactor_cost_L_pharma = sampleLognormalP5P95(rng, 50, 500, n);
    const custom_ratio = sampleUniform(rng, 0.35, 0.85, n);
    let custom_share = sampleBetaMeanStdev(rng, 0.55, 0.15, n);
    custom_share = clip(add(custom_share, scale(maturity.map(m => m - 0.5), 0.30)), 0, 1);

    const reactor_cost_L_avg = reactor_cost_L_pharma.map((p, i) =>
      p * (custom_share[i] * custom_ratio[i] + (1 - custom_share[i]))
    );

    const capex_s = sampleUniform(rng, 0.6, 0.9, n);
    const plant_factor = sampleLognormalP5P95(rng, 1.5, 3.5, n);
    const V_ref = 1e6;

    const capex_total = reactor_cost_L_avg.map((r, i) =>
      r * total_working_volume_L[i] * Math.pow(total_working_volume_L[i] / V_ref, capex_s[i] - 1) * plant_factor[i]
    );

    let wacc = sampleLognormalP5P95(rng, params.wacc_p5, params.wacc_p95, n);
    wacc = clip(wacc.map((w, i) => w - 0.03 * (maturity[i] - 0.5)), 0.03, 1);

    const asset_life = sampleUniform(rng, params.asset_life_lo, params.asset_life_hi, n);
    const crf_val = wacc.map((w, i) => crf(w, asset_life[i]));

    capex_perkg = capex_total.map((c, i) => (c * crf_val[i]) / output_kgpy[i]);
  }

  // Fixed OPEX calculation
  let fixed_perkg = new Array(n).fill(0);
  if (params.include_fixed_opex) {
    const ref_output = 20e6 * 0.9;
    const fixed_perkg_ref = sampleUniform(rng, 1.0, 6.0, n);
    const fixed_annual_ref = scale(fixed_perkg_ref, ref_output);
    const fixed_scale = sampleUniform(rng, 0.6, 1.0, n);

    const fixed_annual = fixed_annual_ref.map((f, i) =>
      f * Math.pow(output_kgpy[i] / ref_output, fixed_scale[i])
    );
    fixed_perkg = div(fixed_annual, output_kgpy);
  }

  // Downstream processing (scaffolding, texturization) for structured products
  let downstream_perkg = new Array(n).fill(0);
  if (params.include_downstream) {
    // Downstream adds $2-15/kg for structured products
    downstream_perkg = sampleLognormalP5P95(rng, 2.0, 15.0, n);
  }

  // Total unit cost
  const unit_cost = add(add(add(voc, capex_perkg), fixed_perkg), downstream_perkg);

  return {
    unit_cost,
    cost_media,
    cost_comm_micros,
    cost_recf,
    cost_other_var: other_var,
    cost_capex: capex_perkg,
    cost_fixed: fixed_perkg,
    cost_downstream: downstream_perkg,
    pct_hydro: is_hydro.filter(x => x).length / n,
    pct_foodgrade: is_foodgrade.filter(x => x).length / n,
    pct_recf_cheap: is_recf_cheap.filter(x => x).length / n,
    // Input parameters for sensitivity analysis
    maturity_samples: maturity,
    density_samples: density_gL,
    media_turnover_samples: media_turnover,
    L_per_kg_samples: L_per_kg,
    media_cost_L_samples: media_cost_L,
    is_hydro_samples: is_hydro.map(x => x ? 1 : 0),
    is_foodgrade_samples: is_foodgrade.map(x => x ? 1 : 0),
    is_recf_cheap_samples: is_recf_cheap.map(x => x ? 1 : 0),
    g_recf_samples: g_recf,
    price_recf_samples: price_recf,
    plant_kta_samples: plant_kta,
    uptime_samples: uptime
  };
}

// Statistical helpers
function quantile(arr, q) {
  const sorted = [...arr].sort((a, b) => a - b);
  const idx = (sorted.length - 1) * q;
  const lo = Math.floor(idx);
  const hi = Math.ceil(idx);
  const frac = idx - lo;
  return sorted[lo] * (1 - frac) + sorted[hi] * frac;
}

function mean(arr) {
  return arr.reduce((a, b) => a + b, 0) / arr.length;
}

// Spearman rank correlation coefficient
function spearmanCorr(x, y) {
  const n = x.length;

  // Rank function (handles ties with average rank)
  function rank(arr) {
    const sorted = arr.map((v, i) => ({v, i})).sort((a, b) => a.v - b.v);
    const ranks = new Array(n);
    let i = 0;
    while (i < n) {
      let j = i;
      while (j < n - 1 && sorted[j + 1].v === sorted[j].v) j++;
      const avgRank = (i + j) / 2 + 1;
      for (let k = i; k <= j; k++) ranks[sorted[k].i] = avgRank;
      i = j + 1;
    }
    return ranks;
  }

  const rx = rank(x);
  const ry = rank(y);

  // Pearson correlation of ranks
  const meanRx = mean(rx);
  const meanRy = mean(ry);

  let num = 0, denX = 0, denY = 0;
  for (let i = 0; i < n; i++) {
    const dx = rx[i] - meanRx;
    const dy = ry[i] - meanRy;
    num += dx * dy;
    denX += dx * dx;
    denY += dy * dy;
  }

  return num / Math.sqrt(denX * denY);
}

Model Parameters

Code
viewof simpleMode = Inputs.toggle({label: "Simplified view (recommended)", value: true})
Code
html`<div id="simplified-note" style="display: ${simpleMode ? 'block' : 'none'}; background: #f0f4e8; border-left: 4px solid #5a7a5a; padding: 0.7rem 0.9rem; margin-bottom: 1rem; font-size: 0.82em; line-height: 1.5; border-radius: 0 4px 4px 0;">
<strong>Simplified view:</strong> Less pivotal parameters (plant capacity, uptime, financing costs, food-grade micronutrients, media turnover) are set to reasonable defaults.
In our sensitivity analysis, these contribute less than 10% of the variance in cost estimates.
<em>Switch off to adjust all parameters.</em>
</div>`
Code
// Reactive style block to hide/show full-mode-only inputs
html`<style>
  .full-mode-only { display: ${simpleMode ? 'none' : 'block'}; }
</style>`

Model Structure

Code
viewof include_capex = Inputs.toggle({label: "Include capital costs (CAPEX)", value: true})
viewof include_fixed_opex = Inputs.toggle({label: "Include fixed operating costs", value: true})
viewof include_downstream = Inputs.toggle({label: "Include downstream processing", value: false})
viewof include_blending = Inputs.toggle({label: "Show blended product cost", value: false})
Code
viewof blending_share = Inputs.range([0.05, 1.0], {
    value: 0.25, step: 0.05,
    label: "CM inclusion rate",
    disabled: !include_blending
  })
viewof filler_cost = Inputs.range([1, 10], {
    value: 3, step: 0.5,
    label: "Filler cost ($/kg)",
    disabled: !include_blending
  })
What are these options? Why would you toggle them?
  • CAPEX: Bioreactor and facility capital costs, annualized. On by default because capital costs are a substantial portion of total production cost. You might toggle this off to isolate variable operating costs or to compare with analyses that report only VOC.
  • Fixed OPEX: Labor, maintenance, overhead (scales sub-linearly with plant size). On by default for the same reason. Toggle off to focus on marginal costs only.
  • Downstream: Scaffolding, texturization, and forming for structured products. Off by default because the base model estimates cost of unstructured cell mass. Toggle on if you are interested in structured products (steaks, chicken breast), which add $2-15/kg.
  • Blended product cost: Most cultivated meat products in development are hybrids — e.g., 10-30% cultured cells blended with plant-based filler. Toggle on to see estimated blended ingredient cost alongside pure cell cost. Default: 25% CM inclusion, $3/kg filler (plant protein / mycoprotein).
Note: Most published TEAs report costs with CAPEX and Fixed OPEX included. Toggling them off produces numbers that are not comparable to standard literature estimates.

Key Parameters

These sliders set the center of each parameter’s distribution used in the Monte Carlo simulation. They are not simple point estimates – the simulation samples around these values. For example, setting Plant Capacity to 20 kTA means the simulation draws plant sizes from a lognormal distribution centered near 20 kTA (specifically, p5=10 and p95=40). All charts and results below update dynamically as you adjust these sliders.

Code
viewof plant_capacity = Inputs.range([5, 100], {
  value: 20, step: 5,
  label: "Plant Capacity (kTA/yr)"
})
What is this? Where do these ranges come from? (click to expand)

Annual production capacity in thousand metric tons (kTA). Current pilots: <1 kTA. Commercial target: 10-50 kTA.

Slider range (5-100 kTA): The lower bound (5 kTA) represents a modest first-generation commercial facility. The upper bound (100 kTA) represents an optimistic large-scale facility. The default (20 kTA) matches the reference scale in Risner et al. (2021) and is a commonly used benchmark in TEA literature.

What plant size affects:

Cost Component Effect of Larger Plant
CAPEX per kg ↓ Scales sub-linearly (power law ~0.6-0.9)
Fixed OPEX per kg ↓ Overhead spread over more output
Variable costs per kg — No direct effect (same $/L, $/g)
Larger plants have lower per-kg costs due to economies of scale in capital and fixed costs. Variable costs (media, growth factors) don’t change with scale — they depend on technology adoption.
Code
viewof uptime = Inputs.range([0.6, 0.99], {
  value: 0.90, step: 0.01,
  label: "Utilization Rate"
})
What is this? Fraction of capacity actually used. Accounts for downtime, cleaning, maintenance. 90% is optimistic for new industry.
Code
viewof maturity = Inputs.range([0.1, 0.9], {
  value: 0.5, step: 0.05,
  label: "Industry Maturity by 2036"
})
What is this? A latent factor (0=nascent, 1=mature) that affects all technology adoption, reactor costs, and financing. High maturity = correlated improvements.

Target Year

Code
viewof target_year = Inputs.range([2026, 2050], {
  value: 2036, step: 1,
  label: "Projection Year"
})
What does the year affect? Earlier years → lower maturity, less technology adoption. Later years → more time for cost reductions and scale-up. The model adjusts maturity expectations based on years from 2024.

Technology Adoption by

Probability that each cost-reducing technology is widely adopted by the projection year.

Code
viewof reset_adoption = Inputs.button("Reset adoption defaults", {
  reduce: () => {
    // Set viewof values back to defaults
    viewof p_hydro.value = 0.75;
    viewof p_hydro.dispatchEvent(new Event("input", {bubbles: true}));
    viewof p_foodgrade.value = 0.65;
    viewof p_foodgrade.dispatchEvent(new Event("input", {bubbles: true}));
    viewof p_recfactors.value = 0.5;
    viewof p_recfactors.dispatchEvent(new Event("input", {bubbles: true}));
    viewof gf_progress.value = 50;
    viewof gf_progress.dispatchEvent(new Event("input", {bubbles: true}));
  }
})
Code
viewof p_hydro = Inputs.range([0.3, 0.95], {
  value: 0.75, step: 0.05,
  label: "P(Hydrolysates for basal media)"
})
What is this? (click to expand)

Hydrolysates are enzymatically digested plant/yeast proteins that provide amino acids for basal media — the bulk nutrient broth that cells grow in.

Important clarification: Hydrolysates replace amino acids, NOT growth factors. These are completely separate cost categories:

Component What it provides Hydrolysate impact
Basal media Amino acids, glucose, vitamins ✅ Hydrolysates reduce cost ~70%
Growth factors FGF-2, IGF-1, TGF-β signaling proteins ❌ Hydrolysates don’t help

Cost comparison:

Media Type Cost ($/L) Source
Pharma-grade amino acids $1.00 – $4.00 Model range
Hydrolysate-based $0.20 – $1.20 Humbird 2021: ~$2/kg amino acids

Why 75% baseline? Recent research (2024-2025) shows hydrolysates are already validated across plant, yeast, insect, and fish sources. Companies like IntegriCulture have reduced media components by replacing amino acids with yeast extract. The main uncertainty is regulatory approval for food products, not technical feasibility.

See Maturity Correlation for how adoption probability is adjusted.
Code
viewof p_foodgrade = Inputs.range([0.1, 0.9], {
  value: 0.65, step: 0.05,
  label: "P(Food-grade micronutrients)"
})
What is this? (click to expand)

Micronutrients include vitamins, minerals, and trace elements needed for cell metabolism.

Why it matters:

Grade Usage (g/kg meat) Price ($/g) Cost Impact
Pharma-grade 1.0 - 10.0 $0.50 - $20 $0.50 - $200/kg
Food-grade 0.1 - 2.0 $0.02 - $2 $0.002 - $4/kg

Pharma-grade uses ultra-pure compounds at precise concentrations. Food-grade uses commodity ingredients acceptable for human consumption.

See Maturity Correlation for how adoption probability is adjusted.

Code
viewof p_recfactors = Inputs.range([0.1, 0.9], {
  value: 0.5, step: 0.05,
  label: "P(Scalable GF technology)"
})
Code
viewof gf_progress = Inputs.range([0, 100], {
  value: 50, step: 5,
  label: html`GF cost reduction by ${target_year} (%)`
})
What do these sliders control? (click to expand)

Two controls for growth factors: (1) P(Scalable GF technology) sets the probability of a breakthrough reaching commercial scale (switches between “expensive” and “cheap” price regimes). (2) GF cost reduction progress sets how far prices have fallen within each regime by the projection year — 0% = current prices, 100% = industry targets achieved.

Growth factors are signaling proteins (FGF-2, IGF-1, TGF-β, etc.) that tell cells to proliferate. Currently the most expensive media component — often 55-95% of media cost at research scale.

Current vs. projected prices:

Scenario Price ($/g) Status Source
Research-grade FGF-2 $50,000 Current (2024) CEN
Research-grade TGF-β $1,000,000 Current GFI analysis
Model “expensive” $500 - $50,000 Limited progress Conservative projection
Model “cheap” $1 - $100 Breakthrough achieved Industry target
Plant-based (BioBetter) ~$1 Target FoodNavigator

What triggers the “cheap” scenario? (Key breakthroughs)

The probability slider represents whether at least one of these technology approaches succeeds at commercial scale by the projection year:

Technology Mechanism Target price Status (2025)
Autocrine cell lines Engineer cells to produce their own FGF2, eliminating external supply ~$0/g (no purchase needed) Proof of concept 2023 — Mosa Meat, Tufts collaborations
Plant molecular farming Express GFs in transgenic tobacco/lettuce $1-10/g BioBetter, ORF Genetics pilots
Precision fermentation High-density E. coli/yeast expression (like insulin) $10-100/g GFI 2024 analysis — scaling remains key challenge
Polyphenol substitution Curcumin (NaCur) activates FGF receptors, reduces FGF2 needs by 80% N/A (reduces quantity) Research 2024
Thermostable variants FGF2-G3 with 20-day half-life (vs hours) Same $/g, less usage Enantis commercial product

Why this is modeled as a binary switch + continuous progress:

  • The switch represents whether any scalable technology reaches commercial viability
  • The progress slider represents how far costs have dropped within that regime
  • Even in the “expensive” scenario (no breakthrough), incremental improvements occur
  • This structure captures the bimodal nature of the uncertainty: either scalable production is achieved, or it isn’t

Model cost impact:

Scenario Usage (g/kg) Price ($/g) Cost/kg chicken
No breakthrough 0.0005 - 0.02 $500 - $50,000 $0.25 - $1,000
Breakthrough achieved 0.0001 - 0.005 $1 - $100 $0.0001 - $0.50

This is a pivotal uncertainty. The GFI 2024 State of the Industry report identifies growth factor cost reduction as one of the top technical challenges. If solved, GFs become negligible; if not, they dominate the cost structure.

See Maturity Correlation for how adoption probability is adjusted.

Financing

Code
viewof wacc_lo = Inputs.range([5, 20], {
  value: 8, step: 1,
  label: "WACC Low (%)"
})
viewof wacc_hi = Inputs.range([10, 35], {
  value: 20, step: 1,
  label: "WACC High (%)"
})
viewof asset_life_lo = Inputs.range([5, 15], {
  value: 8, step: 1,
  label: "Asset Life Low (years)"
})
viewof asset_life_hi = Inputs.range([10, 30], {
  value: 20, step: 1,
  label: "Asset Life High (years)"
})
What are WACC and Asset Life? (click to expand)

WACC (Weighted Average Cost of Capital): The blended rate of return required by debt and equity investors, used to annualize capital costs via the Capital Recovery Factor (CRF). A WACC of 8% is typical for established food manufacturing; 20%+ reflects the high risk premium investors demand for an unproven technology. The simulation samples WACC from a lognormal distribution between these bounds, further adjusted by the maturity factor.

Asset Life: How many years bioreactor and facility equipment is depreciated over. Shorter life (8 years) means higher annual capital charges; longer life (20 years) spreads costs but assumes equipment remains productive. The range reflects uncertainty about equipment durability in a novel industry.

These two parameters together determine the Capital Recovery Factor: CRF = r(1+r)^n / ((1+r)^n - 1), where r = WACC and n = asset life in years.

Cell Density Range

Code
viewof density_lo = Inputs.range([10, 100], {
  value: 30, step: 10,
  label: "Cell Density Low (g/L)"
})
viewof density_hi = Inputs.range([50, 300], {
  value: 200, step: 10,
  label: "Cell Density High (g/L)"
})
What is cell density? (click to expand) Cell density (g/L) determines how much meat you get per liter of media. Higher density = less media per kg of product = lower cost. Current lab scale: 10-50 g/L. TEA projections: 50-200 g/L. This is a key driver of media efficiency.

Advanced: Media Turnover

Show media turnover parameters
Code
viewof media_turnover_lo = Inputs.range([1, 5], {
  value: 1, step: 1,
  label: "Media Turnover Low"
})
viewof media_turnover_hi = Inputs.range([2, 20], {
  value: 10, step: 1,
  label: "Media Turnover High"
})
Code
results = {
  // Adjust maturity based on target year (2024 baseline)
  // Later years allow more time for industry development
  const yearFactor = Math.max(0, Math.min(1, (target_year - 2024) / 20));
  const adjustedMaturity = maturity * (0.5 + 0.5 * yearFactor);

  const params = {
    plant_kta_p5: simpleMode ? 10 : plant_capacity * 0.5,
    plant_kta_p95: simpleMode ? 40 : plant_capacity * 2.0,
    uptime_mean: simpleMode ? 0.90 : uptime,
    maturity_mean: adjustedMaturity,
    p_hydro_mean: p_hydro,
    p_foodgrade_mean: simpleMode ? 0.65 : p_foodgrade,
    p_recfactors_mean: p_recfactors,
    wacc_p5: simpleMode ? 0.08 : wacc_lo / 100,
    wacc_p95: simpleMode ? 0.20 : wacc_hi / 100,
    asset_life_lo: simpleMode ? 8 : asset_life_lo,
    asset_life_hi: simpleMode ? 20 : asset_life_hi,
    density_gL_p5: density_lo,
    density_gL_p95: density_hi,
    media_turnover_p5: simpleMode ? 1 : media_turnover_lo,
    media_turnover_p95: simpleMode ? 10 : media_turnover_hi,
    include_capex: simpleMode ? true : include_capex,
    include_fixed_opex: simpleMode ? true : include_fixed_opex,
    include_downstream: simpleMode ? false : include_downstream,
    gf_progress: gf_progress
  };

  return simulate(30000, 42, params);
}

// Calculate statistics
stats = {
  const uc = results.unit_cost;
  const bs = typeof blending_share === "number" ? blending_share : 0.25;
  const fc = typeof filler_cost === "number" ? filler_cost : 3;
  const blended = uc.map(c => c * bs + fc * (1 - bs));
  return {
    n: uc.length,
    p5: quantile(uc, 0.05),
    p50: quantile(uc, 0.50),
    p95: quantile(uc, 0.95),
    prob_10: uc.filter(x => x < 10).length / uc.length * 100,
    prob_25: uc.filter(x => x < 25).length / uc.length * 100,
    prob_50: uc.filter(x => x < 50).length / uc.length * 100,
    prob_100: uc.filter(x => x < 100).length / uc.length * 100,
    blended_p50: quantile(blended, 0.50),
    blended_p5: quantile(blended, 0.05),
    blended_p95: quantile(blended, 0.95)
  };
}

Results Summary

Code
html`<div style="background: #f8f9fa; padding: 1rem 1.25rem; border-left: 4px solid #3498db; margin-bottom: 1.5rem; font-size: 0.95em; line-height: 1.6;">
<strong>What these numbers represent:</strong> Simulated <strong>production cost per kilogram of pure cultured chicken cells</strong> (wet weight, unprocessed) in <strong>${target_year}</strong>, based on ${stats.n.toLocaleString()} Monte Carlo simulations. This is the cost to produce cell mass in a bioreactor — not the cost of a consumer product, and not retail price.
<br><br>
<strong><span title="UPSIDE Foods' chicken cutlet is a blend of cultured chicken cells and plant-based ingredients. SuperMeat's chicken burger used ~30% cultured cells. The GFI State of the Industry 2024 report notes that 'hybrid products combining cultivated and plant-based ingredients are the most likely near-term path to market.' Eat Just/GOOD Meat's Singapore-approved product uses cultured chicken in a plant-protein matrix.">Pure cells vs. consumer products:</span></strong> Most cultivated meat products on the market or in development are <em>hybrid products</em> — blending a fraction of cultured cells with plant-based or mycoprotein ingredients. A product with (say) 20% cultured cells and 80% plant-based filler at $3/kg would have a blended ingredient cost far below the pure-cell cost shown here. The "price parity with conventional meat" threshold may therefore be achievable at higher per-kg cell costs than these numbers suggest.
<br><br>
<strong>Why it matters:</strong> If production costs for pure cells reach <strong>~$10/kg</strong>, even 100% cultured products could compete with conventional chicken. At <strong>$25-50/kg</strong>, hybrid products with moderate cell inclusion rates may still reach price parity. If costs remain <strong>>$100/kg</strong>, even hybrid products face significant price premiums. These thresholds inform whether animal welfare interventions should prioritize supporting this industry.
</div>`
Code
html`<div class="grid" style="grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem;">

<div class="card" style="background: linear-gradient(135deg, #3498db, #2980b9); color: white; padding: 1.5rem; border-radius: 8px;">
<h4 style="margin: 0; opacity: 0.9;">Median Cost (p50)</h4>
<h2 style="margin: 0.5rem 0;">$${stats.p50.toFixed(1)}/kg</h2>
<small>Pure cell mass — half of simulations above, half below</small>
</div>

<div class="card" style="background: linear-gradient(135deg, #27ae60, #1e8449); color: white; padding: 1.5rem; border-radius: 8px;">
<h4 style="margin: 0; opacity: 0.9;">Optimistic (p5)</h4>
<h2 style="margin: 0.5rem 0;">$${stats.p5.toFixed(1)}/kg</h2>
<small>Only 5% of simulations cheaper</small>
</div>

<div class="card" style="background: linear-gradient(135deg, #e74c3c, #c0392b); color: white; padding: 1.5rem; border-radius: 8px;">
<h4 style="margin: 0; opacity: 0.9;">Pessimistic (p95)</h4>
<h2 style="margin: 0.5rem 0;">$${stats.p95.toFixed(1)}/kg</h2>
<small>95% of simulations cheaper</small>
</div>

</div>

${include_blending ? html`<div style="background: #eaf7ea; border-left: 4px solid #27ae60; padding: 0.8rem 1rem; margin-top: 0.5rem; font-size: 0.9em;">
<strong>Blended product estimate (${Math.round(blending_share * 100)}% CM, ${Math.round((1-blending_share)*100)}% filler at $${filler_cost}/kg):</strong>
Median <strong>$${stats.blended_p50.toFixed(1)}/kg</strong> · 90% CI: $${stats.blended_p5.toFixed(1)} – $${stats.blended_p95.toFixed(1)}/kg
</div>` : html`<div style="background: #fef9e7; border-left: 4px solid #f39c12; padding: 0.8rem 1rem; margin-top: 0.5rem; font-size: 0.9em;">
<strong>Hybrid product estimate:</strong> At a CM inclusion rate of ~25% with plant-based filler at ~$3/kg, the blended ingredient cost would be approximately <strong>$${(stats.p50 * 0.25 + 3 * 0.75).toFixed(1)}/kg</strong> (median). Enable "Show blended product cost" in the sidebar to adjust these assumptions.
</div>`}

</div>`

Cost Distribution

Code
Plot = import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm")
Code
{
  const uc = results.unit_cost;
  const clipVal = Math.min(quantile(uc, 0.98), 200);
  const clipped = uc.filter(x => x <= clipVal);

  return Plot.plot({
    width: 800,
    height: 400,
    marginLeft: 60,
    marginBottom: 50,
    x: {
      label: "Unit Production Cost ($/kg pure cell mass)",
      domain: [0, clipVal * 1.05]
    },
    y: {
      label: "Frequency",
      grid: true
    },
    marks: [
      Plot.rectY(clipped, Plot.binX({y: "count"}, {x: d => d, fill: "steelblue", fillOpacity: 0.7})),
      Plot.ruleX([stats.p5], {stroke: "green", strokeWidth: 2, strokeDasharray: "5,5"}),
      Plot.ruleX([stats.p50], {stroke: "blue", strokeWidth: 3}),
      Plot.ruleX([stats.p95], {stroke: "red", strokeWidth: 2, strokeDasharray: "5,5"}),
      Plot.ruleX([10], {stroke: "darkgreen", strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}),
      Plot.ruleX([25], {stroke: "orange", strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}),
      Plot.text([
        {x: stats.p5 + 2, y: 2500, text: `p5: $${stats.p5.toFixed(1)}`},
        {x: stats.p50 + 2, y: 3000, text: `p50: $${stats.p50.toFixed(1)}`},
        {x: stats.p95 + 2, y: 2500, text: `p95: $${stats.p95.toFixed(1)}`}
      ], {x: "x", y: "y", text: "text", fontSize: 12})
    ],
    title: `Projected ${target_year} Cultured Chicken Production Cost Distribution`
  })
}
How to read this chart

The histogram shows the distribution of simulated production costs.

  • Blue bars: Frequency of each cost level across 30,000 simulations
  • Green dashed line: 5th percentile (optimistic scenario)
  • Blue solid line: Median (50th percentile)
  • Red dashed line: 95th percentile (pessimistic scenario)
  • Dark green dotted: $10/kg reference (competitive with conventional chicken)
  • Orange dotted: $25/kg reference (premium product viable)
The width of the distribution shows uncertainty. Narrow = confident. Wide = uncertain.

Probability Thresholds

These percentages answer: “What’s the chance that production costs fall below key price points?” — based on the parameter distributions you’ve set above. These are the decision-relevant outputs for evaluating whether cultured meat can compete with conventional chicken.

Code
html`<div class="grid" style="grid-template-columns: repeat(4, 1fr); gap: 1rem; margin: 2rem 0;">

<div class="card" style="border: 2px solid ${stats.prob_10 > 25 ? '#27ae60' : '#ddd'}; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>P(Cost < $10/kg)</h5>
<h2 style="color: #27ae60;">${stats.prob_10.toFixed(1)}%</h2>
<small>Pure cells competitive with conventional chicken (~$5-10/kg)</small>
</div>

<div class="card" style="border: 2px solid ${stats.prob_25 > 50 ? '#3498db' : '#ddd'}; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>P(Cost < $25/kg)</h5>
<h2 style="color: #3498db;">${stats.prob_25.toFixed(1)}%</h2>
<small>Premium product viable</small>
</div>

<div class="card" style="border: 2px solid ${stats.prob_50 > 50 ? '#f39c12' : '#ddd'}; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>P(Cost < $50/kg)</h5>
<h2 style="color: #f39c12;">${stats.prob_50.toFixed(1)}%</h2>
<small>Specialty/niche market</small>
</div>

<div class="card" style="border: 2px solid #ddd; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>P(Cost < $100/kg)</h5>
<h2 style="color: #e74c3c;">${stats.prob_100.toFixed(1)}%</h2>
<small>Below early estimates</small>
</div>

</div>`

Cost Breakdown

Where does the cost come from? This chart shows the average contribution of each cost component across all simulations. The largest bars are the cost drivers to focus on — these are where technological progress or parameter uncertainty has the most impact.

Code
{
  const allComponents = [
    {name: "Media", value: mean(results.cost_media), color: "#27ae60"},
    {name: "Micronutrients", value: mean(results.cost_comm_micros), color: "#3498db"},
    {name: "Growth Factors", value: mean(results.cost_recf), color: "#9b59b6"},
    {name: "Other VOC", value: mean(results.cost_other_var), color: "#7f8c8d"},
    {name: "CAPEX (annualized)", value: mean(results.cost_capex), color: "#e74c3c"},
    {name: "Fixed OPEX", value: mean(results.cost_fixed), color: "#f39c12"},
    {name: "Downstream", value: mean(results.cost_downstream), color: "#1abc9c"}
  ];
  // Filter out zero-value components (e.g., downstream when not included)
  const components = allComponents.filter(c => c.value > 0.001).sort((a, b) => b.value - a.value);

  const total = components.reduce((s, c) => s + c.value, 0);

  return Plot.plot({
    width: 1000,
    height: 520,
    marginLeft: 180,
    marginRight: 120,
    x: {
      label: "Average Cost ($/kg)",
      grid: true
    },
    y: {
      label: null
    },
    marks: [
      Plot.barX(components, {
        y: "name",
        x: "value",
        fill: "color",
        sort: {y: "-x"}
      }),
      Plot.text(components, {
        y: "name",
        x: d => d.value + 0.5,
        text: d => `$${d.value.toFixed(2)} (${(d.value/total*100).toFixed(0)}%)`,
        textAnchor: "start",
        fontSize: 12
      })
    ],
    title: `Cost Breakdown by Component (Total: $${Math.round(total)}/kg)`
  })
}
Understanding the cost components

Variable Operating Costs (VOC):

  • Media: Nutrient broth for cell growth (amino acids, glucose, vitamins)
  • Comm. Micros: Commercial micronutrients (minerals, trace elements)
  • Recombinant: Growth factors (proteins signaling cell division)
  • Other VOC: Utilities, consumables, waste disposal

Fixed Costs:

  • CAPEX: Capital costs (reactors, facilities) annualized via Capital Recovery Factor
  • Fixed OPEX: Labor, maintenance, overhead (scales with plant size)
The relative sizes shift dramatically based on technology adoption assumptions.

Component Distributions

Individual distributions for each cost driver. These charts update dynamically when you change the sidebar parameters. If you adjust plant capacity, the CAPEX and Fixed OPEX distributions will shift; if you change technology adoption probabilities, the media and growth factor distributions will change. (Note: some parameters, like plant capacity, primarily affect CAPEX and fixed costs rather than variable costs like media.)

Code
function formatCost(val) {
  if (val >= 30) return Math.round(val).toString();
  if (val >= 1) return val.toFixed(1);
  if (val >= 0.1) return val.toFixed(2);
  return val.toFixed(3);
}

{
  const allComponents = [
    {name: "Media", data: results.cost_media, color: "#27ae60"},
    {name: "Micronutrients", data: results.cost_comm_micros, color: "#3498db"},
    {name: "Growth Factors", data: results.cost_recf, color: "#9b59b6"},
    {name: "Other VOC", data: results.cost_other_var, color: "#7f8c8d"},
    {name: "CAPEX (annualized)", data: results.cost_capex, color: "#e74c3c"},
    {name: "Fixed OPEX", data: results.cost_fixed, color: "#f39c12"},
    {name: "Downstream", data: results.cost_downstream, color: "#1abc9c"}
  ];

  // Filter out components with all zeros (e.g., downstream when not included)
  const components = allComponents.filter(c => mean(c.data) > 0.001);

  const plotData = components.map(comp => {
    const p5 = quantile(comp.data, 0.05);
    const p50 = quantile(comp.data, 0.50);
    const p95 = quantile(comp.data, 0.95);
    const clipVal = Math.max(quantile(comp.data, 0.98), 0.1);
    const clipped = comp.data.filter(x => x <= clipVal && x >= 0);

    const plot = Plot.plot({
      width: 420,
      height: 180,
      marginLeft: 45,
      marginBottom: 35,
      marginTop: 10,
      x: {
        label: "$/kg",
        domain: [0, clipVal * 1.1]
      },
      y: {
        label: null,
        ticks: []
      },
      marks: [
        Plot.rectY(clipped, Plot.binX({y: "count"}, {x: d => d, fill: comp.color, fillOpacity: 0.7})),
        Plot.ruleX([p5], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"}),
        Plot.ruleX([p50], {stroke: "black", strokeWidth: 2}),
        Plot.ruleX([p95], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"})
      ]
    });

    const label = `${comp.name}: $${formatCost(p50)} (90% CI: ${formatCost(p5)} – ${formatCost(p95)})`;
    return {plot, label};
  });

  return html`<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin: 1rem 0;">
    ${plotData.map(d => html`<div style="font-size: 0.9em;">
      <div style="font-weight: normal; margin-bottom: 0.3rem; color: #333;">${d.label}</div>
      ${d.plot}
    </div>`)}
  </div>`;
}
Reading these distributions

Each mini-histogram shows the distribution for one cost component across 30,000 simulations:

  • Solid black line: Median (p50) - the middle value
  • Dashed black lines: 5th and 95th percentiles (90% confidence interval)
  • Width of distribution: Uncertainty - wider means more uncertain
These distributions are generated from the simulation using the parameters you set in the sidebar above. When you adjust the sliders, these distributions update accordingly. They are analogous to Squiggle/Guesstimate visualizations – showing the full range of possible values, not just a point estimate.

Technology Adoption (Realized)

Code
html`<div class="grid" style="grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 2rem 0;">

<div class="card" style="border: 1px solid #ddd; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>Hydrolysates Adopted</h5>
<h2 style="color: #27ae60;">${(results.pct_hydro * 100).toFixed(0)}%</h2>
<small>of simulations use hydrolysates</small>
</div>

<div class="card" style="border: 1px solid #ddd; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>Food-Grade Micros</h5>
<h2 style="color: #3498db;">${(results.pct_foodgrade * 100).toFixed(0)}%</h2>
<small>of simulations use food-grade</small>
</div>

<div class="card" style="border: 1px solid #ddd; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>Cheap Growth Factors</h5>
<h2 style="color: #9b59b6;">${(results.pct_recf_cheap * 100).toFixed(0)}%</h2>
<small>of simulations have cheap factors</small>
</div>

</div>`

Sensitivity Analysis (Tornado Chart)

Which parameters have the most impact on the final cost? This chart shows Spearman rank correlations between each input parameter and the unit cost.

Code
{
  // Calculate correlations between inputs and unit cost
  const uc = results.unit_cost;

  const correlations = [
    {name: "Cell Density (g/L)", corr: spearmanCorr(results.density_samples, uc), direction: "inverse"},
    {name: "Media Turnover", corr: spearmanCorr(results.media_turnover_samples, uc), direction: "direct"},
    {name: "L/kg (volume)", corr: spearmanCorr(results.L_per_kg_samples, uc), direction: "direct"},
    {name: "Media $/L", corr: spearmanCorr(results.media_cost_L_samples, uc), direction: "direct"},
    {name: "Uses Hydrolysates", corr: spearmanCorr(results.is_hydro_samples, uc), direction: "inverse"},
    {name: "Uses Food-Grade", corr: spearmanCorr(results.is_foodgrade_samples, uc), direction: "inverse"},
    {name: "Has Cheap GFs", corr: spearmanCorr(results.is_recf_cheap_samples, uc), direction: "inverse"},
    {name: "GF Quantity (g/kg)", corr: spearmanCorr(results.g_recf_samples, uc), direction: "direct"},
    {name: "GF Price ($/g)", corr: spearmanCorr(results.price_recf_samples, uc), direction: "direct"},
    {name: "Industry Maturity", corr: spearmanCorr(results.maturity_samples, uc), direction: "inverse"},
    {name: "Plant Capacity", corr: spearmanCorr(results.plant_kta_samples, uc), direction: "inverse"},
    {name: "Utilization Rate", corr: spearmanCorr(results.uptime_samples, uc), direction: "inverse"}
  ];

  // Sort by absolute correlation
  const sorted = correlations
    .map(c => ({...c, absCorr: Math.abs(c.corr)}))
    .sort((a, b) => b.absCorr - a.absCorr);

  const sensitivityPlot = Plot.plot({
    width: 850,
    height: 450,
    marginLeft: 170,
    marginRight: 70,
    x: {
      label: "Spearman Correlation with Unit Cost",
      domain: [-1, 1],
      grid: true,
      labelOffset: 40
    },
    y: {
      label: null,
      tickFormat: d => d,
      tickSize: 0
    },
    color: {
      domain: ["Increases cost", "Decreases cost"],
      range: ["#e74c3c", "#27ae60"]
    },
    style: {
      fontSize: "14px"
    },
    marks: [
      Plot.barX(sorted, {
        y: "name",
        x: "corr",
        fill: d => d.corr > 0 ? "Increases cost" : "Decreases cost",
        sort: {y: "-x", reduce: d => Math.abs(d)}
      }),
      Plot.ruleX([0], {stroke: "black", strokeWidth: 1}),
      Plot.text(sorted, {
        y: "name",
        x: d => d.corr > 0 ? d.corr + 0.04 : d.corr - 0.04,
        text: d => d.corr.toFixed(2),
        textAnchor: d => d.corr > 0 ? "start" : "end",
        fontSize: 13,
        fontWeight: 500
      })
    ]
  });

  return html`<div style="font-size: 1em;">
    <div style="font-weight: normal; font-size: 1.1em; margin-bottom: 0.5rem; color: #333;">Parameter Sensitivity: Correlation with Unit Cost</div>
    ${sensitivityPlot}
  </div>`;
}
How to read this chart (click to expand)

Tornado chart interpretation:

  • Bars extending right (red): Parameters that increase cost when they increase
    • Higher media $/L → higher cost
    • Higher GF price → higher cost
    • Higher L/kg volume → higher cost
  • Bars extending left (green): Parameters that decrease cost when they increase
    • Higher cell density → lower cost (less media needed)
    • Higher maturity → lower cost (better technology, cheaper inputs)
    • Using hydrolysates → lower cost (cheaper amino acids)
  • Bar length: How much that parameter affects the output
    • |correlation| > 0.5 = strong effect
    • |correlation| 0.3-0.5 = moderate effect
    • |correlation| < 0.3 = weak effect

This is Spearman rank correlation, which captures monotonic relationships (not just linear). A correlation of -0.7 means “when this parameter is high, costs tend to be low” across the 30,000 simulations.

Key insight: The parameters with the longest bars are the ones to focus on when: - Forecasting (these drive your uncertainty) - Prioritizing R&D (these are the levers to pull) - Gathering expert input (these are worth eliciting carefully)

Industry Maturity Deep Dive

The maturity factor is a continuous variable ranging from 0 (nascent industry) to 1 (fully mature) that correlates multiple model inputs. It is not binary — intermediate values represent partial industry development. Higher maturity means:

  • Higher technology adoption rates
  • Lower reactor costs (more custom/food-grade)
  • Lower financing costs (WACC)
Code
{
  // Maturity distribution
  const matP5 = quantile(results.maturity_samples, 0.05);
  const matP50 = quantile(results.maturity_samples, 0.50);
  const matP95 = quantile(results.maturity_samples, 0.95);

  const maturityPlot = Plot.plot({
    width: 400,
    height: 220,
    marginLeft: 50,
    marginBottom: 40,
    marginTop: 10,
    x: {
      label: "Maturity Factor",
      domain: [0, 1]
    },
    y: {
      label: "Frequency",
      grid: true
    },
    marks: [
      Plot.rectY(results.maturity_samples, Plot.binX({y: "count"}, {x: d => d, fill: "#8e44ad", fillOpacity: 0.7})),
      Plot.ruleX([matP50], {stroke: "black", strokeWidth: 2}),
      Plot.ruleX([matP5], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"}),
      Plot.ruleX([matP95], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"})
    ]
  });
  const matLabel = `Maturity Distribution (median: ${matP50.toFixed(2)}, 90% CI: ${matP5.toFixed(2)} – ${matP95.toFixed(2)})`;

  // Cell density distribution
  const denP5 = quantile(results.density_samples, 0.05);
  const denP50 = quantile(results.density_samples, 0.50);
  const denP95 = quantile(results.density_samples, 0.95);

  const densityPlot = Plot.plot({
    width: 400,
    height: 220,
    marginLeft: 50,
    marginBottom: 40,
    marginTop: 10,
    x: {
      label: "Cell Density (g/L)",
      domain: [0, Math.min(quantile(results.density_samples, 0.98) * 1.1, 350)]
    },
    y: {
      label: "Frequency",
      grid: true
    },
    marks: [
      Plot.rectY(results.density_samples, Plot.binX({y: "count"}, {x: d => d, fill: "#16a085", fillOpacity: 0.7})),
      Plot.ruleX([denP50], {stroke: "black", strokeWidth: 2}),
      Plot.ruleX([denP5], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"}),
      Plot.ruleX([denP95], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"})
    ]
  });
  const denLabel = `Cell Density Distribution (median: ${denP50.toFixed(0)} g/L, 90% CI: ${denP5.toFixed(0)} – ${denP95.toFixed(0)})`;

  return html`<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; margin: 1rem 0;">
    <div style="font-size: 0.9em;">
      <div style="font-weight: normal; margin-bottom: 0.3rem; color: #333;">${matLabel}</div>
      ${maturityPlot}
    </div>
    <div style="font-size: 0.9em;">
      <div style="font-weight: normal; margin-bottom: 0.3rem; color: #333;">${denLabel}</div>
      ${densityPlot}
    </div>
  </div>`;
}
Code
{
  // Sample subset for scatter plot (too many points otherwise)
  const sampleSize = 2000;
  const step = Math.floor(results.unit_cost.length / sampleSize);
  const scatterData = [];
  for (let i = 0; i < results.unit_cost.length; i += step) {
    scatterData.push({
      maturity: results.maturity_samples[i],
      cost: results.unit_cost[i],
      density: results.density_samples[i]
    });
  }

  const scatterPlot = Plot.plot({
    width: 800,
    height: 320,
    marginLeft: 60,
    marginBottom: 50,
    marginTop: 10,
    x: {
      label: "Industry Maturity Factor",
      domain: [0, 1]
    },
    y: {
      label: "Unit Cost ($/kg)",
      domain: [0, Math.min(quantile(results.unit_cost, 0.95), 150)],
      grid: true
    },
    color: {
      label: "Cell Density (g/L)",
      scheme: "viridis",
      legend: true
    },
    marks: [
      Plot.dot(scatterData, {
        x: "maturity",
        y: "cost",
        fill: "density",
        fillOpacity: 0.5,
        r: 3
      }),
      Plot.linearRegressionY(scatterData, {x: "maturity", y: "cost", stroke: "red", strokeWidth: 2})
    ]
  });

  return html`<div style="font-size: 0.95em;">
    <div style="font-weight: normal; margin-bottom: 0.3rem; color: #333;">Maturity vs Unit Cost (colored by cell density)</div>
    ${scatterPlot}
  </div>`;
}
Understanding the maturity correlation

The scatter plot shows how maturity (x-axis) relates to unit cost (y-axis), with points colored by cell density.

Key patterns:

  • Negative slope: Higher maturity → lower costs (red trend line)
  • Color gradient: Higher density (yellow) tends to cluster at lower costs
  • Spread: Even at high maturity, costs vary due to other random factors
This correlation structure prevents unrealistic scenarios where technology succeeds but financing remains difficult, or vice versa.

Quick Start

  1. Explore the “Learn” sections at the top to understand how cultured chicken production works and how this model handles uncertainty.

  2. Adjust parameters in the left sidebar. Each parameter has an expandable explanation.

  3. Results update automatically as you change the Basic Parameters sliders — the summary cards, cost distribution, and probability thresholds all recalculate. (The Component Distributions section shows the underlying distributions and updates when you change the relevant sliders.)

  4. Navigate the results to see:

    • Cost Distribution: Full probability distribution with key percentiles
    • Probability Thresholds: Chances of hitting key price points
    • Cost Breakdown: Which components drive costs
    • Technology Adoption: Realized adoption rates

Pivotal Questions Context

This model is part of The Unjournal’s Pivotal Questions initiative — a project to identify and quantify uncertainties that matter most for high-impact decisions.

What is a “Pivotal Question”?

A pivotal question is one where resolving the uncertainty would significantly change optimal resource allocation. For cultured chicken:

  • If costs reach <$10/kg by 2036: Large-scale displacement of conventional chicken becomes plausible → prioritize support for industry scale-up
  • If costs remain >$50/kg: Focus shifts to alternative interventions (plant-based, policy, etc.)

The pivotal question framework helps prioritize research and forecasting efforts where they matter most.

See: The Unjournal’s Pivotal Questions documentation

The cultured chicken pivotal question asks:

What will be the production cost of cultured chicken by the projection year, and what does this imply for animal welfare interventions?

Key links:

Resource Description
Pivotal Questions overview Full documentation on the PQ framework
Cultured meat on Metaculus Forecasting questions with community predictions
The Unjournal Independent evaluations of impactful research
Squiggle reference model Local reference implementation (parameters synced with this dashboard)
UC Davis ACBM Calculator Original academic cost model (Risner et al.)
Good Food Institute Industry reports and data

Technical Reference

Note

View Full Technical Documentation — Complete model formulas, parameter definitions, code reference, and methodology.

Quick reference:

\[\text{Unit Cost} = \text{VOC} + \text{CAPEX}_{/\text{kg}} + \text{Fixed OPEX}_{/\text{kg}} + \text{Downstream}_{/\text{kg}}\]

The model runs 30,000 Monte Carlo simulations, sampling from parameter distributions and technology adoption switches to produce cost distributions and probability thresholds.


Key Insights

The table below summarizes typical outcomes from this interactive model at different maturity settings. These are illustrative ranges – the actual results shown in the charts above update dynamically as you adjust the sidebar parameters.

Scenario Typical Median Cost Key Drivers
High maturity (0.7+) $15-30/kg All technologies adopted, low WACC
Baseline (0.5) $25-50/kg Mixed adoption, moderate uncertainty
Low maturity (0.3-) $50-100+/kg Pharma-grade inputs, high financing costs

Most sensitive parameters:

  1. Technology adoption (especially hydrolysates)
  2. Cell density at harvest
  3. Industry maturity (affects multiple factors)

Limitations & Caveats

Model status (March 2026): This model is largely AI-generated and we cannot yet vouch for all parameter values. It is provided to fix ideas, give a sense of the sort of modeling we’re interested in, and present a framework for discussion and comparison — not a definitive cost estimate. We welcome scrutiny and suggestions via Hypothesis annotations or email.

  1. Static snapshot model — Projects costs for a single projection year (adjustable 2026-2050). Does not model year-over-year learning curves; instead, the “maturity” parameter serves as a proxy for cumulative industry development.

  2. Downstream is optional — Scaffolding, texturization, and forming costs can be included via toggle (+$2-15/kg for structured products).

  3. No geography — Assumes generic global costs, not region-specific labor, energy, or regulatory factors.

  4. Limited contamination modeling — No batch failure or contamination event distributions.


Sources

The model draws on parameter ranges and structural assumptions from the following published techno-economic analyses and industry reports, among others:

  • Risner et al. (2021) - UC Davis ACBM Calculator – primary reference for plant scale, reactor costs, and media cost structure
  • Humbird (2021) - Scale-Up Economics for Cultured Meat – CAPEX estimates, scale exponents, and plant cost factors
  • CE Delft (2021) - TEA of Cultivated Meat – European cost benchmarks and energy analysis
  • GFI (2024) - State of the Industry Reports – industry cost targets and technology status
  • PMC Meta-analysis (2024) – cross-study comparison of cost projections
  • Nature Food Scoping Review (2024) – media cost reduction approaches
  • The Unjournal - Cultivated Meat Evaluations – independent peer evaluations of relevant research

Additional parameter-specific sources are cited in the expandable details under each slider and in the Technical Documentation.

Source Code
---
title: "Cultured Chicken Production Cost Model"
subtitle: "Interactive Monte Carlo TEA for Cost Projections"
format:
  html:
    page-layout: full
    css: styles.css
    include-after-body:
      text: |
        <script src="https://hypothes.is/embed.js" async></script>
---

::: {.callout-note collapse="true"}
## Upcoming Workshop: Cultivated Meat Cost Trajectories
We're organizing an online expert workshop (late April / early May 2026) to dig into the key cost cruxes — media costs, bioreactor scale-up, and the gap between TEA projections and commercial reality. This model is one of the tools we'll use.

**[Workshop details & signup →](https://uj-cm-workshop.netlify.app/)** · **[State your cost beliefs →](https://uj-cm-workshop.netlify.app/beliefs.html)**
:::

::: {.callout-note collapse="true"}
## We Want Your Feedback!
**Comment directly on this page** using [Hypothesis](https://hypothes.is/) — click the `<` tab on the right edge. Highlight any text, parameter, or result to annotate it. We actively monitor comments and will respond to questions, incorporate suggestions, and improve the model based on your feedback.

**[🎧 Listen: Technical Review (22 min MP3)](model_review_report.mp3)** — Audio walkthrough of model architecture and areas for review

**Other ways to reach us:** [Open a GitHub issue](https://github.com/unjournal/cm_pq_modeling/issues/new) · [Email contact@unjournal.org](mailto:contact@unjournal.org)
:::

```{=html}
<div style="display: flex; gap: 0.5rem; margin: 0.5rem 0 1rem 0;">
  <button id="toggle-hypothesis" onclick="toggleHypothesis()" style="padding: 0.4rem 0.8rem; font-size: 0.85rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f8f9fa;">
    Hide Annotations Sidebar
  </button>
  <button id="toggle-fullwidth" onclick="toggleFullWidth()" style="padding: 0.4rem 0.8rem; font-size: 0.85rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f8f9fa;">
    Expand Content
  </button>
</div>

<script>
function toggleHypothesis() {
  const btn = document.getElementById('toggle-hypothesis');
  // Hypothesis creates a sidebar with class 'hypothesis-sidebar' or as an annotator frame
  // Try multiple selectors to find the sidebar
  const selectors = [
    '.hypothesis-sidebar',
    '.annotator-frame',
    'hypothesis-sidebar',
    '[class*="hypothesis"][class*="sidebar"]',
    'iframe[src*="hypothes.is"]'
  ];

  let frame = null;
  for (const sel of selectors) {
    frame = document.querySelector(sel);
    if (frame) break;
  }

  if (frame) {
    if (frame.style.display === 'none' || frame.classList.contains('hypothesis-hidden')) {
      frame.style.display = '';
      frame.classList.remove('hypothesis-hidden');
      btn.textContent = 'Hide Annotations Sidebar';
    } else {
      frame.style.display = 'none';
      frame.classList.add('hypothesis-hidden');
      btn.textContent = 'Show Annotations Sidebar';
    }
  } else {
    // If we can't find the sidebar, it may not be loaded yet
    // Try using Hypothesis API if available
    if (window.hypothesisConfig) {
      console.log('Hypothesis config found but sidebar element not located');
    }
    btn.textContent = 'Sidebar not found';
    setTimeout(() => { btn.textContent = 'Hide Annotations Sidebar'; }, 2000);
  }
}

function toggleFullWidth() {
  const body = document.body;
  const btn = document.getElementById('toggle-fullwidth');
  // Quarto full-page layout uses column classes and sidebar
  const contentSelectors = [
    'main.content', 'main', '.page-columns', '#quarto-content',
    '.column-page', '.column-body', '.column-body-outset',
    '.panel-fill', '.panel-sidebar', '#quarto-sidebar',
    '.page-layout-full .content'
  ];

  if (body.classList.contains('fullwidth-mode')) {
    body.classList.remove('fullwidth-mode');
    // Remove injected style
    const injected = document.getElementById('fullwidth-style');
    if (injected) injected.remove();
    btn.textContent = 'Expand Content';
  } else {
    body.classList.add('fullwidth-mode');
    // Inject a style tag to override Quarto's layout constraints
    if (!document.getElementById('fullwidth-style')) {
      const style = document.createElement('style');
      style.id = 'fullwidth-style';
      style.textContent = `
        body.fullwidth-mode #quarto-content,
        body.fullwidth-mode main.content,
        body.fullwidth-mode main,
        body.fullwidth-mode .page-columns,
        body.fullwidth-mode .column-body,
        body.fullwidth-mode .column-page {
          max-width: 100% !important;
          width: 100% !important;
          padding-left: 1rem !important;
          padding-right: 1rem !important;
        }
        body.fullwidth-mode #quarto-sidebar,
        body.fullwidth-mode #quarto-margin-sidebar,
        body.fullwidth-mode .sidebar {
          display: none !important;
        }
        body.fullwidth-mode .page-columns {
          grid-template-columns: 1fr !important;
        }
      `;
      document.head.appendChild(style);
    }
    btn.textContent = 'Normal Width';
    // Also hide hypothesis when going fullwidth
    hideHypothesis();
  }
}

function hideHypothesis() {
  const btn = document.getElementById('toggle-hypothesis');
  const selectors = [
    '.hypothesis-sidebar',
    '.annotator-frame',
    'hypothesis-sidebar',
    '[class*="hypothesis"][class*="sidebar"]'
  ];
  for (const sel of selectors) {
    const frame = document.querySelector(sel);
    if (frame) {
      frame.style.display = 'none';
      frame.classList.add('hypothesis-hidden');
      btn.textContent = 'Show Annotations Sidebar';
      break;
    }
  }
}
</script>

<style>
.fullwidth-mode .page-columns,
.fullwidth-mode #quarto-content,
.fullwidth-mode main.content,
.fullwidth-mode main,
.fullwidth-mode .column-page,
.fullwidth-mode .column-body,
.fullwidth-mode .panel-fill,
.fullwidth-mode .panel-sidebar {
  max-width: 100% !important;
  width: 100% !important;
}
.fullwidth-mode .content {
  max-width: 100% !important;
}
/* Hide hypothesis sidebar when in fullwidth mode */
.fullwidth-mode .hypothesis-sidebar,
.fullwidth-mode .annotator-frame,
.fullwidth-mode hypothesis-sidebar {
  display: none !important;
}
.hypothesis-hidden {
  display: none !important;
}
</style>
```

::: {.callout-note}
## New to Cultured Meat?

**[Read our deep dive: How Cultured Chicken is Made](learn.qmd)** — a detailed guide covering cell banking, bioreactors, media composition, growth factors, and why each step affects costs.

Quick summary: Cultured chicken is produced by growing avian muscle cells in bioreactors. The main cost drivers are **media** (amino acids, nutrients), **growth factors** (signaling proteins), **bioreactors** (capital equipment), and **operating costs**.

```
   CELL BANK  →  SEED TRAIN  →  PRODUCTION  →  HARVEST  →  PRODUCT
      [O]          [OOO]        [OOOOOOO]       [===]       [≡≡≡]
```
:::

::: {.callout-note collapse="true"}
## Learn: Understanding Uncertainty & Distributions

This model uses **Monte Carlo simulation**: we sample thousands of possible futures and see what distribution of costs emerges.

**Distribution Types Used:**

| Parameter Type | Distribution | Why | Examples in model |
|---------------|--------------|-----|-------------------|
| Costs, intensities | **Lognormal** | Always positive, right-skewed (some very high values possible) | Media cost per liter, GF price per gram, cell density |
| Probabilities, fractions | **Beta** | Bounded between 0 and 1, flexible shape | Maturity factor, adoption probabilities, uptime. For switching parameters (e.g., hydrolysate adoption), the model first samples an adoption *probability* from a Beta distribution, then uses a Bernoulli draw to determine whether each simulation run uses the "adopted" or "non-adopted" cost regime. |
| Bounded ranges | **Uniform** | Equal probability within bounds, used when we have only a plausible range and no reason to favor values within it | Asset life (years), CAPEX scale exponent, fixed OPEX scaling factor |

We are open to considering alternative distributional forms (e.g., triangular, PERT, or mixture distributions) and to making the modeling flexible enough for users to select or compare different distributional assumptions. Suggestions are welcome via Hypothesis annotations.

**Reading the Results:**

- **p5 (5th percentile)**: "Optimistic" - only 5% of simulations are cheaper
- **p50 (median)**: Middle outcome - half above, half below
- **p95 (95th percentile)**: "Pessimistic" - 95% of simulations cheaper

**The "Maturity" Correlation:**

A key feature is the **latent maturity factor** that links:

- Technology adoption rates
- Reactor costs
- Financing costs (WACC)

In "good worlds" for cultured chicken (high maturity), multiple things improve together. This prevents unrealistic scenarios where technology succeeds but financing remains difficult.

This approach draws on the concept of **copula-based correlation** in Monte Carlo simulation, where a shared latent variable induces dependence among otherwise independent marginal distributions. See [Vose (2008), *Risk Analysis*](https://www.wiley.com/en-us/Risk+Analysis%3A+A+Quantitative+Guide-p-9780470512845) for a textbook treatment of correlated sampling in cost models, and [Morgan & Henrion (1990), *Uncertainty*](https://doi.org/10.1017/CBO9780511840609) for foundational discussion of dependent uncertainties.

<details><summary>More detail on the maturity factor implementation</summary>

The maturity factor is sampled once per simulation from a Beta distribution (mean set by the "Industry Maturity" slider, standard deviation 0.20). It then **shifts** the adoption probabilities (hydrolysates, food-grade, recombinant GFs) and **adjusts** WACC downward in high-maturity draws. This creates positive correlation among "good news" variables without imposing a rigid structure. The strength of the correlation is moderate by design -- maturity explains some but not all of the variation in each parameter.
</details>
:::

---

## Interactive Model

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

// ============================================================
// SEEDED RANDOM NUMBER GENERATOR
// ============================================================

// Simple mulberry32 PRNG (fast, good quality for Monte Carlo)
function mulberry32(seed) {
  return function() {
    let t = seed += 0x6D2B79F5;
    t = Math.imul(t ^ t >>> 15, t | 1);
    t ^= t + Math.imul(t ^ t >>> 7, t | 61);
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  }
}

// ============================================================
// STATISTICAL SAMPLING FUNCTIONS
// ============================================================

// Box-Muller transform for standard normal
function boxMuller(rng) {
  const u1 = rng();
  const u2 = rng();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

// Sample from lognormal given p5 and p95
function sampleLognormalP5P95(rng, p5, p95, n) {
  const Z_95 = 1.6448536269514722;
  const mu = (Math.log(p5) + Math.log(p95)) / 2;
  const sigma = (Math.log(p95) - Math.log(p5)) / (2 * Z_95);

  const samples = new Array(n);
  for (let i = 0; i < n; i++) {
    samples[i] = Math.exp(mu + sigma * boxMuller(rng));
  }
  return samples;
}

// Sample uniform
function sampleUniform(rng, lo, hi, n) {
  const samples = new Array(n);
  for (let i = 0; i < n; i++) {
    samples[i] = lo + (hi - lo) * rng();
  }
  return samples;
}

// Beta distribution parameters from mean/stdev
function betaFromMeanStdev(mean, stdev) {
  let variance = stdev * stdev;
  const maxVar = mean * (1 - mean);
  if (variance <= 0 || variance >= maxVar) {
    stdev = Math.sqrt(maxVar * 0.99);
    variance = stdev * stdev;
  }
  const t = maxVar / variance - 1;
  const a = Math.max(mean * t, 0.01);
  const b = Math.max((1 - mean) * t, 0.01);
  return [a, b];
}

// Sample from beta using Joehnk's algorithm (simple, works for all a,b > 0)
function sampleBeta(rng, a, b) {
  // Use gamma ratio method
  const gammaA = sampleGamma(rng, a);
  const gammaB = sampleGamma(rng, b);
  return gammaA / (gammaA + gammaB);
}

// Sample from gamma distribution (Marsaglia and Tsang's method)
function sampleGamma(rng, shape) {
  if (shape < 1) {
    return sampleGamma(rng, shape + 1) * Math.pow(rng(), 1 / shape);
  }

  const d = shape - 1/3;
  const c = 1 / Math.sqrt(9 * d);

  while (true) {
    let x, v;
    do {
      x = boxMuller(rng);
      v = 1 + c * x;
    } while (v <= 0);

    v = v * v * v;
    const u = rng();

    if (u < 1 - 0.0331 * (x * x) * (x * x)) return d * v;
    if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) return d * v;
  }
}

// Sample from beta given mean/stdev
function sampleBetaMeanStdev(rng, mean, stdev, n) {
  const [a, b] = betaFromMeanStdev(mean, stdev);
  const samples = new Array(n);
  for (let i = 0; i < n; i++) {
    samples[i] = sampleBeta(rng, a, b);
  }
  return samples;
}

// Capital Recovery Factor
function crf(wacc, nYears) {
  return wacc * Math.pow(1 + wacc, nYears) / (Math.pow(1 + wacc, nYears) - 1);
}

// Clip values between min and max
function clip(arr, min, max) {
  return arr.map(v => Math.min(Math.max(v, min), max));
}

// Element-wise operations
function add(a, b) { return a.map((v, i) => v + b[i]); }
function mul(a, b) { return a.map((v, i) => v * b[i]); }
function div(a, b) { return a.map((v, i) => v / b[i]); }
function scale(arr, s) { return arr.map(v => v * s); }

// ============================================================
// MAIN SIMULATION FUNCTION
// ============================================================

function simulate(n, seed, params) {
  const rng = mulberry32(seed);

  // Latent maturity factor
  const maturity = sampleBetaMeanStdev(rng, params.maturity_mean, 0.20, n);

  // Scale and uptime
  const plant_kta = sampleLognormalP5P95(rng, params.plant_kta_p5, params.plant_kta_p95, n);
  const uptime = sampleBetaMeanStdev(rng, params.uptime_mean, 0.05, n);
  const output_kgpy = mul(scale(plant_kta, 1e6), uptime);

  // Adoption probabilities (maturity-adjusted)
  let p_hydro = sampleBetaMeanStdev(rng, params.p_hydro_mean, 0.10, n);
  p_hydro = clip(add(p_hydro, scale(maturity.map(m => m - 0.5), 0.25)), 0, 1);

  let p_foodgrade = sampleBetaMeanStdev(rng, params.p_foodgrade_mean, 0.10, n);
  p_foodgrade = clip(add(p_foodgrade, scale(maturity.map(m => m - 0.5), 0.20)), 0, 1);

  let p_recf = sampleBetaMeanStdev(rng, params.p_recfactors_mean, 0.15, n);
  p_recf = clip(add(p_recf, scale(maturity.map(m => m - 0.5), 0.25)), 0, 1);

  // Bernoulli draws for adoption
  const is_hydro = p_hydro.map(p => rng() < p);
  const is_foodgrade = p_foodgrade.map(p => rng() < p);
  const is_recf_cheap = p_recf.map(p => rng() < p);

  // Process intensities
  const density_gL = sampleLognormalP5P95(rng, params.density_gL_p5, params.density_gL_p95, n);
  const cycle_days = sampleLognormalP5P95(rng, 0.5, 5.0, n);
  const media_turnover = sampleLognormalP5P95(rng, params.media_turnover_p5, params.media_turnover_p95, n);
  const L_per_kg = mul(div(density_gL.map(_ => 1000), density_gL), media_turnover);

  // Media cost
  const media_cost_hydro = sampleLognormalP5P95(rng, 0.2, 1.2, n);
  const media_cost_pharma = sampleLognormalP5P95(rng, 1.0, 4.0, n);
  const media_cost_L = is_hydro.map((h, i) => h ? media_cost_hydro[i] : media_cost_pharma[i]);
  const cost_media = mul(L_per_kg, media_cost_L);

  // Commodity micronutrients
  const g_comm_food = sampleLognormalP5P95(rng, 0.1, 2.0, n);
  const g_comm_pharma = sampleLognormalP5P95(rng, 1.0, 10.0, n);
  const g_comm = is_foodgrade.map((f, i) => f ? g_comm_food[i] : g_comm_pharma[i]);

  const price_comm_food = sampleLognormalP5P95(rng, 0.02, 2.0, n);
  const price_comm_pharma = sampleLognormalP5P95(rng, 0.5, 20.0, n);
  const price_comm = is_foodgrade.map((f, i) => f ? price_comm_food[i] : price_comm_pharma[i]);
  const cost_comm_micros = mul(g_comm, price_comm);

  // Recombinant growth factors (FGF-2, IGF-1, TGF-β, etc.)
  // CRITICAL: Literature shows GFs can be 55-95% of media cost at current prices
  // Current prices: FGF-2 ~$50,000/g, TGF-β up to $1M/g
  // Target prices: $1-10/g with scaled recombinant production

  // Quantity (g/kg meat) - based on typical concentrations in media
  // ~10-100 ng/mL in media × 20-60 L/kg = 0.0002-0.006 g/kg
  const g_recf_cheap = sampleLognormalP5P95(rng, 1e-4, 5e-3, n);  // Reduced usage with optimization
  const g_recf_exp = sampleLognormalP5P95(rng, 5e-4, 2e-2, n);   // Standard usage
  const g_recf = is_recf_cheap.map((c, i) => c ? g_recf_cheap[i] : g_recf_exp[i]);

  // Price ($/g) - Scaled by gf_progress parameter (0-100%)
  // At 0% progress: current prices ($5k-500k expensive, $100-10k cheap)
  // At 100% progress: target prices ($50-5k expensive, $1-100 cheap)
  const progress = params.gf_progress / 100;  // 0 to 1

  // Interpolate price ranges based on progress
  // Cheap scenario: $100-10,000 at 0% → $1-100 at 100%
  const cheap_p5 = 100 * Math.pow(0.01, progress);   // 100 → 1
  const cheap_p95 = 10000 * Math.pow(0.01, progress); // 10000 → 100
  const price_recf_cheap = sampleLognormalP5P95(rng, cheap_p5, cheap_p95, n);

  // Expensive scenario: $5,000-500,000 at 0% → $50-5,000 at 100%
  const exp_p5 = 5000 * Math.pow(0.01, progress);    // 5000 → 50
  const exp_p95 = 500000 * Math.pow(0.01, progress); // 500000 → 5000
  const price_recf_exp = sampleLognormalP5P95(rng, exp_p5, exp_p95, n);

  const price_recf = is_recf_cheap.map((c, i) => c ? price_recf_cheap[i] : price_recf_exp[i]);
  const cost_recf = mul(g_recf, price_recf);

  // Other variable costs
  const other_var = sampleLognormalP5P95(rng, 0.5, 5.0, n);

  // VOC total
  const voc = add(add(add(cost_media, cost_comm_micros), cost_recf), other_var);

  // CAPEX calculation
  let capex_perkg = new Array(n).fill(0);
  if (params.include_capex) {
    const prod_kg_L_day = div(scale(density_gL, 1/1000), cycle_days);
    const total_working_volume_L = div(output_kgpy, scale(prod_kg_L_day, 365));
    const reactor_cost_L_pharma = sampleLognormalP5P95(rng, 50, 500, n);
    const custom_ratio = sampleUniform(rng, 0.35, 0.85, n);
    let custom_share = sampleBetaMeanStdev(rng, 0.55, 0.15, n);
    custom_share = clip(add(custom_share, scale(maturity.map(m => m - 0.5), 0.30)), 0, 1);

    const reactor_cost_L_avg = reactor_cost_L_pharma.map((p, i) =>
      p * (custom_share[i] * custom_ratio[i] + (1 - custom_share[i]))
    );

    const capex_s = sampleUniform(rng, 0.6, 0.9, n);
    const plant_factor = sampleLognormalP5P95(rng, 1.5, 3.5, n);
    const V_ref = 1e6;

    const capex_total = reactor_cost_L_avg.map((r, i) =>
      r * total_working_volume_L[i] * Math.pow(total_working_volume_L[i] / V_ref, capex_s[i] - 1) * plant_factor[i]
    );

    let wacc = sampleLognormalP5P95(rng, params.wacc_p5, params.wacc_p95, n);
    wacc = clip(wacc.map((w, i) => w - 0.03 * (maturity[i] - 0.5)), 0.03, 1);

    const asset_life = sampleUniform(rng, params.asset_life_lo, params.asset_life_hi, n);
    const crf_val = wacc.map((w, i) => crf(w, asset_life[i]));

    capex_perkg = capex_total.map((c, i) => (c * crf_val[i]) / output_kgpy[i]);
  }

  // Fixed OPEX calculation
  let fixed_perkg = new Array(n).fill(0);
  if (params.include_fixed_opex) {
    const ref_output = 20e6 * 0.9;
    const fixed_perkg_ref = sampleUniform(rng, 1.0, 6.0, n);
    const fixed_annual_ref = scale(fixed_perkg_ref, ref_output);
    const fixed_scale = sampleUniform(rng, 0.6, 1.0, n);

    const fixed_annual = fixed_annual_ref.map((f, i) =>
      f * Math.pow(output_kgpy[i] / ref_output, fixed_scale[i])
    );
    fixed_perkg = div(fixed_annual, output_kgpy);
  }

  // Downstream processing (scaffolding, texturization) for structured products
  let downstream_perkg = new Array(n).fill(0);
  if (params.include_downstream) {
    // Downstream adds $2-15/kg for structured products
    downstream_perkg = sampleLognormalP5P95(rng, 2.0, 15.0, n);
  }

  // Total unit cost
  const unit_cost = add(add(add(voc, capex_perkg), fixed_perkg), downstream_perkg);

  return {
    unit_cost,
    cost_media,
    cost_comm_micros,
    cost_recf,
    cost_other_var: other_var,
    cost_capex: capex_perkg,
    cost_fixed: fixed_perkg,
    cost_downstream: downstream_perkg,
    pct_hydro: is_hydro.filter(x => x).length / n,
    pct_foodgrade: is_foodgrade.filter(x => x).length / n,
    pct_recf_cheap: is_recf_cheap.filter(x => x).length / n,
    // Input parameters for sensitivity analysis
    maturity_samples: maturity,
    density_samples: density_gL,
    media_turnover_samples: media_turnover,
    L_per_kg_samples: L_per_kg,
    media_cost_L_samples: media_cost_L,
    is_hydro_samples: is_hydro.map(x => x ? 1 : 0),
    is_foodgrade_samples: is_foodgrade.map(x => x ? 1 : 0),
    is_recf_cheap_samples: is_recf_cheap.map(x => x ? 1 : 0),
    g_recf_samples: g_recf,
    price_recf_samples: price_recf,
    plant_kta_samples: plant_kta,
    uptime_samples: uptime
  };
}

// Statistical helpers
function quantile(arr, q) {
  const sorted = [...arr].sort((a, b) => a - b);
  const idx = (sorted.length - 1) * q;
  const lo = Math.floor(idx);
  const hi = Math.ceil(idx);
  const frac = idx - lo;
  return sorted[lo] * (1 - frac) + sorted[hi] * frac;
}

function mean(arr) {
  return arr.reduce((a, b) => a + b, 0) / arr.length;
}

// Spearman rank correlation coefficient
function spearmanCorr(x, y) {
  const n = x.length;

  // Rank function (handles ties with average rank)
  function rank(arr) {
    const sorted = arr.map((v, i) => ({v, i})).sort((a, b) => a.v - b.v);
    const ranks = new Array(n);
    let i = 0;
    while (i < n) {
      let j = i;
      while (j < n - 1 && sorted[j + 1].v === sorted[j].v) j++;
      const avgRank = (i + j) / 2 + 1;
      for (let k = i; k <= j; k++) ranks[sorted[k].i] = avgRank;
      i = j + 1;
    }
    return ranks;
  }

  const rx = rank(x);
  const ry = rank(y);

  // Pearson correlation of ranks
  const meanRx = mean(rx);
  const meanRy = mean(ry);

  let num = 0, denX = 0, denY = 0;
  for (let i = 0; i < n; i++) {
    const dx = rx[i] - meanRx;
    const dy = ry[i] - meanRy;
    num += dx * dy;
    denX += dx * dx;
    denY += dy * dy;
  }

  return num / Math.sqrt(denX * denY);
}
```

### Model Parameters

::: {.panel-sidebar}

```{ojs}
//| echo: false
viewof simpleMode = Inputs.toggle({label: "Simplified view (recommended)", value: true})
```

```{ojs}
//| echo: false
html`<div id="simplified-note" style="display: ${simpleMode ? 'block' : 'none'}; background: #f0f4e8; border-left: 4px solid #5a7a5a; padding: 0.7rem 0.9rem; margin-bottom: 1rem; font-size: 0.82em; line-height: 1.5; border-radius: 0 4px 4px 0;">
<strong>Simplified view:</strong> Less pivotal parameters (plant capacity, uptime, financing costs, food-grade micronutrients, media turnover) are set to reasonable defaults.
In our sensitivity analysis, these contribute less than 10% of the variance in cost estimates.
<em>Switch off to adjust all parameters.</em>
</div>`
```

```{ojs}
//| echo: false
// Reactive style block to hide/show full-mode-only inputs
html`<style>
  .full-mode-only { display: ${simpleMode ? 'none' : 'block'}; }
</style>`
```

```{=html}
<div class="full-mode-only">
```

**Model Structure**

```{ojs}
//| echo: false
viewof include_capex = Inputs.toggle({label: "Include capital costs (CAPEX)", value: true})
viewof include_fixed_opex = Inputs.toggle({label: "Include fixed operating costs", value: true})
viewof include_downstream = Inputs.toggle({label: "Include downstream processing", value: false})
viewof include_blending = Inputs.toggle({label: "Show blended product cost", value: false})
```

```{ojs}
//| echo: false
viewof blending_share = Inputs.range([0.05, 1.0], {
    value: 0.25, step: 0.05,
    label: "CM inclusion rate",
    disabled: !include_blending
  })
viewof filler_cost = Inputs.range([1, 10], {
    value: 3, step: 0.5,
    label: "Filler cost ($/kg)",
    disabled: !include_blending
  })
```
<details><summary>What are these options? Why would you toggle them?</summary>

- **CAPEX**: Bioreactor and facility capital costs, annualized. **On by default** because capital costs are a substantial portion of total production cost. You might toggle this off to isolate variable operating costs or to compare with analyses that report only VOC.
- **Fixed OPEX**: Labor, maintenance, overhead (scales sub-linearly with plant size). **On by default** for the same reason. Toggle off to focus on marginal costs only.
- **Downstream**: Scaffolding, texturization, and forming for structured products. **Off by default** because the base model estimates cost of unstructured cell mass. Toggle on if you are interested in structured products (steaks, chicken breast), which add $2-15/kg.
- **Blended product cost**: Most cultivated meat products in development are hybrids — e.g., 10-30% cultured cells blended with plant-based filler. Toggle on to see estimated blended ingredient cost alongside pure cell cost. Default: 25% CM inclusion, $3/kg filler (plant protein / mycoprotein).

**Note:** Most published TEAs report costs with CAPEX and Fixed OPEX included. Toggling them off produces numbers that are not comparable to standard literature estimates.
</details>

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

---

**Key Parameters**

```{=html}
<div class="full-mode-only">
```

These sliders set the **center** of each parameter's distribution used in the Monte Carlo simulation. They are not simple point estimates -- the simulation samples around these values. For example, setting Plant Capacity to 20 kTA means the simulation draws plant sizes from a lognormal distribution centered near 20 kTA (specifically, p5=10 and p95=40). All charts and results below update dynamically as you adjust these sliders.

```{ojs}
//| echo: false
viewof plant_capacity = Inputs.range([5, 100], {
  value: 20, step: 5,
  label: "Plant Capacity (kTA/yr)"
})
```
<details><summary>What is this? Where do these ranges come from? (click to expand)</summary>

**Annual production capacity** in thousand metric tons (kTA). Current pilots: <1 kTA. Commercial target: 10-50 kTA.

**Slider range (5-100 kTA):** The lower bound (5 kTA) represents a modest first-generation commercial facility. The upper bound (100 kTA) represents an optimistic large-scale facility. The default (20 kTA) matches the reference scale in [Risner et al. (2021)](https://www.mdpi.com/2304-8158/10/1/3) and is a commonly used benchmark in TEA literature.

**What plant size affects:**

| Cost Component | Effect of Larger Plant |
|----------------|----------------------|
| CAPEX per kg | ↓ Scales sub-linearly (power law ~0.6-0.9) |
| Fixed OPEX per kg | ↓ Overhead spread over more output |
| Variable costs per kg | — No direct effect (same $/L, $/g) |

Larger plants have **lower per-kg costs** due to economies of scale in capital and fixed costs. Variable costs (media, growth factors) don't change with scale — they depend on technology adoption.
</details>

```{ojs}
//| echo: false
viewof uptime = Inputs.range([0.6, 0.99], {
  value: 0.90, step: 0.01,
  label: "Utilization Rate"
})
```
<details><summary>What is this?</summary>
Fraction of capacity actually used. Accounts for downtime, cleaning, maintenance. 90% is optimistic for new industry.
</details>

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

```{ojs}
//| echo: false
viewof maturity = Inputs.range([0.1, 0.9], {
  value: 0.5, step: 0.05,
  label: "Industry Maturity by 2036"
})
```
<details><summary>What is this?</summary>
A latent factor (0=nascent, 1=mature) that affects all technology adoption, reactor costs, and financing. High maturity = correlated improvements.
</details>

---

**Target Year**

```{ojs}
//| echo: false
viewof target_year = Inputs.range([2026, 2050], {
  value: 2036, step: 1,
  label: "Projection Year"
})
```
<details><summary>What does the year affect?</summary>
Earlier years → lower [maturity](#industry-maturity-deep-dive), less technology adoption.
Later years → more time for cost reductions and scale-up.
The model adjusts maturity expectations based on years from 2024.
</details>

---

**Technology Adoption by ${target_year}**

Probability that each cost-reducing technology is widely adopted by the projection year.

```{ojs}
//| echo: false
viewof reset_adoption = Inputs.button("Reset adoption defaults", {
  reduce: () => {
    // Set viewof values back to defaults
    viewof p_hydro.value = 0.75;
    viewof p_hydro.dispatchEvent(new Event("input", {bubbles: true}));
    viewof p_foodgrade.value = 0.65;
    viewof p_foodgrade.dispatchEvent(new Event("input", {bubbles: true}));
    viewof p_recfactors.value = 0.5;
    viewof p_recfactors.dispatchEvent(new Event("input", {bubbles: true}));
    viewof gf_progress.value = 50;
    viewof gf_progress.dispatchEvent(new Event("input", {bubbles: true}));
  }
})
```

```{ojs}
//| echo: false
viewof p_hydro = Inputs.range([0.3, 0.95], {
  value: 0.75, step: 0.05,
  label: "P(Hydrolysates for basal media)"
})
```
<details><summary>What is this? (click to expand)</summary>

**Hydrolysates** are enzymatically digested plant/yeast proteins that provide **amino acids for basal media** — the bulk nutrient broth that cells grow in.

**Important clarification:** Hydrolysates replace **amino acids**, NOT growth factors. These are completely separate cost categories:

| Component | What it provides | Hydrolysate impact |
|-----------|-----------------|-------------------|
| **Basal media** | Amino acids, glucose, vitamins | ✅ Hydrolysates reduce cost ~70% |
| **Growth factors** | FGF-2, IGF-1, TGF-β signaling proteins | ❌ Hydrolysates don't help |

**Cost comparison:**

| Media Type | Cost ($/L) | Source |
|------------|-----------|--------|
| Pharma-grade amino acids | $1.00 – $4.00 | Model range |
| Hydrolysate-based | $0.20 – $1.20 | [Humbird 2021](https://pmc.ncbi.nlm.nih.gov/articles/PMC11663224/): ~$2/kg amino acids |

**Why 75% baseline?** [Recent research (2024-2025)](https://www.nature.com/articles/s41538-024-00352-0) shows hydrolysates are already validated across plant, yeast, insect, and fish sources. Companies like IntegriCulture have reduced media components by replacing amino acids with yeast extract. The main uncertainty is regulatory approval for food products, not technical feasibility.

*See [Maturity Correlation](#industry-maturity-deep-dive) for how adoption probability is adjusted.*
</details>

```{=html}
<div class="full-mode-only">
```

```{ojs}
//| echo: false
viewof p_foodgrade = Inputs.range([0.1, 0.9], {
  value: 0.65, step: 0.05,
  label: "P(Food-grade micronutrients)"
})
```
<details><summary>What is this? (click to expand)</summary>

**Micronutrients** include vitamins, minerals, and trace elements needed for cell metabolism.

**Why it matters:**

| Grade | Usage (g/kg meat) | Price ($/g) | Cost Impact |
|-------|------------------|-------------|-------------|
| Pharma-grade | 1.0 - 10.0 | $0.50 - $20 | $0.50 - $200/kg |
| Food-grade | 0.1 - 2.0 | $0.02 - $2 | $0.002 - $4/kg |

Pharma-grade uses ultra-pure compounds at precise concentrations. Food-grade uses commodity ingredients acceptable for human consumption.

*See [Maturity Correlation](#industry-maturity-deep-dive) for how adoption probability is adjusted.*
</details>

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

<a id="growth-factors-explanation"></a>
```{ojs}
//| echo: false
viewof p_recfactors = Inputs.range([0.1, 0.9], {
  value: 0.5, step: 0.05,
  label: "P(Scalable GF technology)"
})
```

```{ojs}
//| echo: false
viewof gf_progress = Inputs.range([0, 100], {
  value: 50, step: 5,
  label: html`GF cost reduction by ${target_year} (%)`
})
```
<details><summary>What do these sliders control? (click to expand)</summary>

**Two controls for growth factors:** (1) **P(Scalable GF technology)** sets the probability of a breakthrough reaching commercial scale (switches between "expensive" and "cheap" price regimes). (2) **GF cost reduction progress** sets how far prices have fallen within each regime by the projection year — 0% = current prices, 100% = industry targets achieved.

**Growth factors** are signaling proteins (FGF-2, IGF-1, TGF-β, etc.) that tell cells to proliferate. Currently **the most expensive media component** — often 55-95% of media cost at research scale.

**Current vs. projected prices:**

| Scenario | Price ($/g) | Status | Source |
|----------|-------------|--------|--------|
| Research-grade FGF-2 | $50,000 | Current (2024) | [CEN](https://cen.acs.org/food/Inside-effort-cut-cost-cultivated/101/i33) |
| Research-grade TGF-β | $1,000,000 | Current | GFI analysis |
| Model "expensive" | $500 - $50,000 | Limited progress | Conservative projection |
| Model "cheap" | $1 - $100 | Breakthrough achieved | Industry target |
| Plant-based (BioBetter) | ~$1 | Target | [FoodNavigator](https://www.foodnavigator.com/Article/2022/09/12/biobetter-s-growth-factor-innovation-cuts-cost-of-cultured-meat/) |

---

**What triggers the "cheap" scenario? (Key breakthroughs)**

The probability slider represents whether **at least one** of these technology approaches succeeds at commercial scale by the projection year:

| Technology | Mechanism | Target price | Status (2025) |
|------------|-----------|--------------|---------------|
| **Autocrine cell lines** | Engineer cells to produce their own FGF2, eliminating external supply | ~$0/g (no purchase needed) | [Proof of concept 2023](https://pmc.ncbi.nlm.nih.gov/articles/PMC10153192/) — Mosa Meat, Tufts collaborations |
| **Plant molecular farming** | Express GFs in transgenic tobacco/lettuce | $1-10/g | [BioBetter](https://www.foodnavigator.com/Article/2022/09/12/biobetter-s-growth-factor-innovation-cuts-cost-of-cultured-meat/), [ORF Genetics](https://orfgenetics.com/) pilots |
| **Precision fermentation** | High-density E. coli/yeast expression (like insulin) | $10-100/g | [GFI 2024 analysis](https://gfi.org/science/the-science-of-cultivated-meat/) — scaling remains key challenge |
| **Polyphenol substitution** | Curcumin (NaCur) activates FGF receptors, reduces FGF2 needs by 80% | N/A (reduces quantity) | [Research 2024](https://pmc.ncbi.nlm.nih.gov/articles/PMC10119461/) |
| **Thermostable variants** | FGF2-G3 with 20-day half-life (vs hours) | Same $/g, less usage | [Enantis](https://www.enantis.com/) commercial product |

**Why this is modeled as a binary switch + continuous progress:**

- The **switch** represents whether *any* scalable technology reaches commercial viability
- The **progress slider** represents how far costs have dropped within that regime
- Even in the "expensive" scenario (no breakthrough), incremental improvements occur
- This structure captures the bimodal nature of the uncertainty: either scalable production is achieved, or it isn't

**Model cost impact:**

| Scenario | Usage (g/kg) | Price ($/g) | **Cost/kg chicken** |
|----------|-------------|-------------|---------------------|
| No breakthrough | 0.0005 - 0.02 | $500 - $50,000 | **$0.25 - $1,000** |
| Breakthrough achieved | 0.0001 - 0.005 | $1 - $100 | **$0.0001 - $0.50** |

**This is a pivotal uncertainty.** The [GFI 2024 State of the Industry report](https://gfi.org/resource/cultivated-meat-eggs-and-dairy-state-of-the-industry-report/) identifies growth factor cost reduction as one of the top technical challenges. If solved, GFs become negligible; if not, they dominate the cost structure.

*See [Maturity Correlation](#industry-maturity-deep-dive) for how adoption probability is adjusted.*
</details>

```{=html}
<div class="full-mode-only">
```

---

**Financing**

```{ojs}
//| echo: false
viewof wacc_lo = Inputs.range([5, 20], {
  value: 8, step: 1,
  label: "WACC Low (%)"
})
viewof wacc_hi = Inputs.range([10, 35], {
  value: 20, step: 1,
  label: "WACC High (%)"
})
viewof asset_life_lo = Inputs.range([5, 15], {
  value: 8, step: 1,
  label: "Asset Life Low (years)"
})
viewof asset_life_hi = Inputs.range([10, 30], {
  value: 20, step: 1,
  label: "Asset Life High (years)"
})
```
<details><summary>What are WACC and Asset Life? (click to expand)</summary>

**WACC (Weighted Average Cost of Capital):** The blended rate of return required by debt and equity investors, used to annualize capital costs via the Capital Recovery Factor (CRF). A WACC of 8% is typical for established food manufacturing; 20%+ reflects the high risk premium investors demand for an unproven technology. The simulation samples WACC from a lognormal distribution between these bounds, further adjusted by the maturity factor.

**Asset Life:** How many years bioreactor and facility equipment is depreciated over. Shorter life (8 years) means higher annual capital charges; longer life (20 years) spreads costs but assumes equipment remains productive. The range reflects uncertainty about equipment durability in a novel industry.

These two parameters together determine the **Capital Recovery Factor**: CRF = r(1+r)^n / ((1+r)^n - 1), where r = WACC and n = asset life in years.
</details>

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

---

**Cell Density Range**

```{ojs}
//| echo: false
viewof density_lo = Inputs.range([10, 100], {
  value: 30, step: 10,
  label: "Cell Density Low (g/L)"
})
viewof density_hi = Inputs.range([50, 300], {
  value: 200, step: 10,
  label: "Cell Density High (g/L)"
})
```
<details><summary>What is cell density? (click to expand)</summary>
Cell density (g/L) determines how much meat you get per liter of media. Higher density = less media per kg of product = lower cost. Current lab scale: 10-50 g/L. TEA projections: 50-200 g/L. This is a key driver of media efficiency.
</details>

```{=html}
<div class="full-mode-only">
```

**Advanced: Media Turnover**

<details>
<summary>Show media turnover parameters</summary>

```{ojs}
//| echo: false
viewof media_turnover_lo = Inputs.range([1, 5], {
  value: 1, step: 1,
  label: "Media Turnover Low"
})
viewof media_turnover_hi = Inputs.range([2, 20], {
  value: 10, step: 1,
  label: "Media Turnover High"
})
```
</details>

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

:::

::: {.panel-fill}

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

// Run simulation reactively whenever inputs change
results = {
  // Adjust maturity based on target year (2024 baseline)
  // Later years allow more time for industry development
  const yearFactor = Math.max(0, Math.min(1, (target_year - 2024) / 20));
  const adjustedMaturity = maturity * (0.5 + 0.5 * yearFactor);

  const params = {
    plant_kta_p5: simpleMode ? 10 : plant_capacity * 0.5,
    plant_kta_p95: simpleMode ? 40 : plant_capacity * 2.0,
    uptime_mean: simpleMode ? 0.90 : uptime,
    maturity_mean: adjustedMaturity,
    p_hydro_mean: p_hydro,
    p_foodgrade_mean: simpleMode ? 0.65 : p_foodgrade,
    p_recfactors_mean: p_recfactors,
    wacc_p5: simpleMode ? 0.08 : wacc_lo / 100,
    wacc_p95: simpleMode ? 0.20 : wacc_hi / 100,
    asset_life_lo: simpleMode ? 8 : asset_life_lo,
    asset_life_hi: simpleMode ? 20 : asset_life_hi,
    density_gL_p5: density_lo,
    density_gL_p95: density_hi,
    media_turnover_p5: simpleMode ? 1 : media_turnover_lo,
    media_turnover_p95: simpleMode ? 10 : media_turnover_hi,
    include_capex: simpleMode ? true : include_capex,
    include_fixed_opex: simpleMode ? true : include_fixed_opex,
    include_downstream: simpleMode ? false : include_downstream,
    gf_progress: gf_progress
  };

  return simulate(30000, 42, params);
}

// Calculate statistics
stats = {
  const uc = results.unit_cost;
  const bs = typeof blending_share === "number" ? blending_share : 0.25;
  const fc = typeof filler_cost === "number" ? filler_cost : 3;
  const blended = uc.map(c => c * bs + fc * (1 - bs));
  return {
    n: uc.length,
    p5: quantile(uc, 0.05),
    p50: quantile(uc, 0.50),
    p95: quantile(uc, 0.95),
    prob_10: uc.filter(x => x < 10).length / uc.length * 100,
    prob_25: uc.filter(x => x < 25).length / uc.length * 100,
    prob_50: uc.filter(x => x < 50).length / uc.length * 100,
    prob_100: uc.filter(x => x < 100).length / uc.length * 100,
    blended_p50: quantile(blended, 0.50),
    blended_p5: quantile(blended, 0.05),
    blended_p95: quantile(blended, 0.95)
  };
}
```

### Results Summary

```{ojs}
//| echo: false
html`<div style="background: #f8f9fa; padding: 1rem 1.25rem; border-left: 4px solid #3498db; margin-bottom: 1.5rem; font-size: 0.95em; line-height: 1.6;">
<strong>What these numbers represent:</strong> Simulated <strong>production cost per kilogram of pure cultured chicken cells</strong> (wet weight, unprocessed) in <strong>${target_year}</strong>, based on ${stats.n.toLocaleString()} Monte Carlo simulations. This is the cost to produce cell mass in a bioreactor — not the cost of a consumer product, and not retail price.
<br><br>
<strong><span title="UPSIDE Foods' chicken cutlet is a blend of cultured chicken cells and plant-based ingredients. SuperMeat's chicken burger used ~30% cultured cells. The GFI State of the Industry 2024 report notes that 'hybrid products combining cultivated and plant-based ingredients are the most likely near-term path to market.' Eat Just/GOOD Meat's Singapore-approved product uses cultured chicken in a plant-protein matrix.">Pure cells vs. consumer products:</span></strong> Most cultivated meat products on the market or in development are <em>hybrid products</em> — blending a fraction of cultured cells with plant-based or mycoprotein ingredients. A product with (say) 20% cultured cells and 80% plant-based filler at $3/kg would have a blended ingredient cost far below the pure-cell cost shown here. The "price parity with conventional meat" threshold may therefore be achievable at higher per-kg cell costs than these numbers suggest.
<br><br>
<strong>Why it matters:</strong> If production costs for pure cells reach <strong>~$10/kg</strong>, even 100% cultured products could compete with conventional chicken. At <strong>$25-50/kg</strong>, hybrid products with moderate cell inclusion rates may still reach price parity. If costs remain <strong>>$100/kg</strong>, even hybrid products face significant price premiums. These thresholds inform whether animal welfare interventions should prioritize supporting this industry.
</div>`
```

```{ojs}
//| echo: false
html`<div class="grid" style="grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem;">

<div class="card" style="background: linear-gradient(135deg, #3498db, #2980b9); color: white; padding: 1.5rem; border-radius: 8px;">
<h4 style="margin: 0; opacity: 0.9;">Median Cost (p50)</h4>
<h2 style="margin: 0.5rem 0;">$${stats.p50.toFixed(1)}/kg</h2>
<small>Pure cell mass — half of simulations above, half below</small>
</div>

<div class="card" style="background: linear-gradient(135deg, #27ae60, #1e8449); color: white; padding: 1.5rem; border-radius: 8px;">
<h4 style="margin: 0; opacity: 0.9;">Optimistic (p5)</h4>
<h2 style="margin: 0.5rem 0;">$${stats.p5.toFixed(1)}/kg</h2>
<small>Only 5% of simulations cheaper</small>
</div>

<div class="card" style="background: linear-gradient(135deg, #e74c3c, #c0392b); color: white; padding: 1.5rem; border-radius: 8px;">
<h4 style="margin: 0; opacity: 0.9;">Pessimistic (p95)</h4>
<h2 style="margin: 0.5rem 0;">$${stats.p95.toFixed(1)}/kg</h2>
<small>95% of simulations cheaper</small>
</div>

</div>

${include_blending ? html`<div style="background: #eaf7ea; border-left: 4px solid #27ae60; padding: 0.8rem 1rem; margin-top: 0.5rem; font-size: 0.9em;">
<strong>Blended product estimate (${Math.round(blending_share * 100)}% CM, ${Math.round((1-blending_share)*100)}% filler at $${filler_cost}/kg):</strong>
Median <strong>$${stats.blended_p50.toFixed(1)}/kg</strong> · 90% CI: $${stats.blended_p5.toFixed(1)} – $${stats.blended_p95.toFixed(1)}/kg
</div>` : html`<div style="background: #fef9e7; border-left: 4px solid #f39c12; padding: 0.8rem 1rem; margin-top: 0.5rem; font-size: 0.9em;">
<strong>Hybrid product estimate:</strong> At a CM inclusion rate of ~25% with plant-based filler at ~$3/kg, the blended ingredient cost would be approximately <strong>$${(stats.p50 * 0.25 + 3 * 0.75).toFixed(1)}/kg</strong> (median). Enable "Show blended product cost" in the sidebar to adjust these assumptions.
</div>`}

</div>`
```

### Cost Distribution

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

```{ojs}
//| echo: false
{
  const uc = results.unit_cost;
  const clipVal = Math.min(quantile(uc, 0.98), 200);
  const clipped = uc.filter(x => x <= clipVal);

  return Plot.plot({
    width: 800,
    height: 400,
    marginLeft: 60,
    marginBottom: 50,
    x: {
      label: "Unit Production Cost ($/kg pure cell mass)",
      domain: [0, clipVal * 1.05]
    },
    y: {
      label: "Frequency",
      grid: true
    },
    marks: [
      Plot.rectY(clipped, Plot.binX({y: "count"}, {x: d => d, fill: "steelblue", fillOpacity: 0.7})),
      Plot.ruleX([stats.p5], {stroke: "green", strokeWidth: 2, strokeDasharray: "5,5"}),
      Plot.ruleX([stats.p50], {stroke: "blue", strokeWidth: 3}),
      Plot.ruleX([stats.p95], {stroke: "red", strokeWidth: 2, strokeDasharray: "5,5"}),
      Plot.ruleX([10], {stroke: "darkgreen", strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}),
      Plot.ruleX([25], {stroke: "orange", strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}),
      Plot.text([
        {x: stats.p5 + 2, y: 2500, text: `p5: $${stats.p5.toFixed(1)}`},
        {x: stats.p50 + 2, y: 3000, text: `p50: $${stats.p50.toFixed(1)}`},
        {x: stats.p95 + 2, y: 2500, text: `p95: $${stats.p95.toFixed(1)}`}
      ], {x: "x", y: "y", text: "text", fontSize: 12})
    ],
    title: `Projected ${target_year} Cultured Chicken Production Cost Distribution`
  })
}
```

<details>
<summary>How to read this chart</summary>

**The histogram** shows the distribution of simulated production costs.

- **Blue bars**: Frequency of each cost level across 30,000 simulations
- **Green dashed line**: 5th percentile (optimistic scenario)
- **Blue solid line**: Median (50th percentile)
- **Red dashed line**: 95th percentile (pessimistic scenario)
- **Dark green dotted**: $10/kg reference (competitive with conventional chicken)
- **Orange dotted**: $25/kg reference (premium product viable)

The **width** of the distribution shows uncertainty. Narrow = confident. Wide = uncertain.
</details>

### Probability Thresholds

These percentages answer: **"What's the chance that production costs fall below key price points?"** — based on the parameter distributions you've set above. These are the decision-relevant outputs for evaluating whether cultured meat can compete with conventional chicken.

```{ojs}
//| echo: false
html`<div class="grid" style="grid-template-columns: repeat(4, 1fr); gap: 1rem; margin: 2rem 0;">

<div class="card" style="border: 2px solid ${stats.prob_10 > 25 ? '#27ae60' : '#ddd'}; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>P(Cost < $10/kg)</h5>
<h2 style="color: #27ae60;">${stats.prob_10.toFixed(1)}%</h2>
<small>Pure cells competitive with conventional chicken (~$5-10/kg)</small>
</div>

<div class="card" style="border: 2px solid ${stats.prob_25 > 50 ? '#3498db' : '#ddd'}; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>P(Cost < $25/kg)</h5>
<h2 style="color: #3498db;">${stats.prob_25.toFixed(1)}%</h2>
<small>Premium product viable</small>
</div>

<div class="card" style="border: 2px solid ${stats.prob_50 > 50 ? '#f39c12' : '#ddd'}; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>P(Cost < $50/kg)</h5>
<h2 style="color: #f39c12;">${stats.prob_50.toFixed(1)}%</h2>
<small>Specialty/niche market</small>
</div>

<div class="card" style="border: 2px solid #ddd; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>P(Cost < $100/kg)</h5>
<h2 style="color: #e74c3c;">${stats.prob_100.toFixed(1)}%</h2>
<small>Below early estimates</small>
</div>

</div>`
```

### Cost Breakdown

**Where does the cost come from?** This chart shows the average contribution of each cost component across all simulations. The largest bars are the cost drivers to focus on — these are where technological progress or parameter uncertainty has the most impact.

```{ojs}
//| echo: false
{
  const allComponents = [
    {name: "Media", value: mean(results.cost_media), color: "#27ae60"},
    {name: "Micronutrients", value: mean(results.cost_comm_micros), color: "#3498db"},
    {name: "Growth Factors", value: mean(results.cost_recf), color: "#9b59b6"},
    {name: "Other VOC", value: mean(results.cost_other_var), color: "#7f8c8d"},
    {name: "CAPEX (annualized)", value: mean(results.cost_capex), color: "#e74c3c"},
    {name: "Fixed OPEX", value: mean(results.cost_fixed), color: "#f39c12"},
    {name: "Downstream", value: mean(results.cost_downstream), color: "#1abc9c"}
  ];
  // Filter out zero-value components (e.g., downstream when not included)
  const components = allComponents.filter(c => c.value > 0.001).sort((a, b) => b.value - a.value);

  const total = components.reduce((s, c) => s + c.value, 0);

  return Plot.plot({
    width: 1000,
    height: 520,
    marginLeft: 180,
    marginRight: 120,
    x: {
      label: "Average Cost ($/kg)",
      grid: true
    },
    y: {
      label: null
    },
    marks: [
      Plot.barX(components, {
        y: "name",
        x: "value",
        fill: "color",
        sort: {y: "-x"}
      }),
      Plot.text(components, {
        y: "name",
        x: d => d.value + 0.5,
        text: d => `$${d.value.toFixed(2)} (${(d.value/total*100).toFixed(0)}%)`,
        textAnchor: "start",
        fontSize: 12
      })
    ],
    title: `Cost Breakdown by Component (Total: $${Math.round(total)}/kg)`
  })
}
```

<details>
<summary>Understanding the cost components</summary>

**Variable Operating Costs (VOC):**

- **[Media](learn.html#media-composition-40-70-of-total-cost)**: Nutrient broth for cell growth (amino acids, glucose, vitamins)
- **[Comm. Micros](learn.html#hydrolysates-the-big-win-for-amino-acids)**: Commercial micronutrients (minerals, trace elements)
- **[Recombinant](learn.html#step-5-growth-factors-a-key-cost-driver)**: Growth factors (proteins signaling cell division)
- **Other VOC**: Utilities, consumables, waste disposal

**Fixed Costs:**

- **CAPEX**: Capital costs (reactors, facilities) annualized via Capital Recovery Factor
- **Fixed OPEX**: Labor, maintenance, overhead (scales with plant size)

The relative sizes shift dramatically based on technology adoption assumptions.
</details>

### Component Distributions

Individual distributions for each cost driver. **These charts update dynamically** when you change the sidebar parameters. If you adjust plant capacity, the CAPEX and Fixed OPEX distributions will shift; if you change technology adoption probabilities, the media and growth factor distributions will change. (Note: some parameters, like plant capacity, primarily affect CAPEX and fixed costs rather than variable costs like media.)

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

// Format cost with appropriate precision
function formatCost(val) {
  if (val >= 30) return Math.round(val).toString();
  if (val >= 1) return val.toFixed(1);
  if (val >= 0.1) return val.toFixed(2);
  return val.toFixed(3);
}

{
  const allComponents = [
    {name: "Media", data: results.cost_media, color: "#27ae60"},
    {name: "Micronutrients", data: results.cost_comm_micros, color: "#3498db"},
    {name: "Growth Factors", data: results.cost_recf, color: "#9b59b6"},
    {name: "Other VOC", data: results.cost_other_var, color: "#7f8c8d"},
    {name: "CAPEX (annualized)", data: results.cost_capex, color: "#e74c3c"},
    {name: "Fixed OPEX", data: results.cost_fixed, color: "#f39c12"},
    {name: "Downstream", data: results.cost_downstream, color: "#1abc9c"}
  ];

  // Filter out components with all zeros (e.g., downstream when not included)
  const components = allComponents.filter(c => mean(c.data) > 0.001);

  const plotData = components.map(comp => {
    const p5 = quantile(comp.data, 0.05);
    const p50 = quantile(comp.data, 0.50);
    const p95 = quantile(comp.data, 0.95);
    const clipVal = Math.max(quantile(comp.data, 0.98), 0.1);
    const clipped = comp.data.filter(x => x <= clipVal && x >= 0);

    const plot = Plot.plot({
      width: 420,
      height: 180,
      marginLeft: 45,
      marginBottom: 35,
      marginTop: 10,
      x: {
        label: "$/kg",
        domain: [0, clipVal * 1.1]
      },
      y: {
        label: null,
        ticks: []
      },
      marks: [
        Plot.rectY(clipped, Plot.binX({y: "count"}, {x: d => d, fill: comp.color, fillOpacity: 0.7})),
        Plot.ruleX([p5], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"}),
        Plot.ruleX([p50], {stroke: "black", strokeWidth: 2}),
        Plot.ruleX([p95], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"})
      ]
    });

    const label = `${comp.name}: $${formatCost(p50)} (90% CI: ${formatCost(p5)} – ${formatCost(p95)})`;
    return {plot, label};
  });

  return html`<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin: 1rem 0;">
    ${plotData.map(d => html`<div style="font-size: 0.9em;">
      <div style="font-weight: normal; margin-bottom: 0.3rem; color: #333;">${d.label}</div>
      ${d.plot}
    </div>`)}
  </div>`;
}
```

<details>
<summary>Reading these distributions</summary>

Each mini-histogram shows the distribution for one cost component across 30,000 simulations:

- **Solid black line**: Median (p50) - the middle value
- **Dashed black lines**: 5th and 95th percentiles (90% confidence interval)
- **Width of distribution**: Uncertainty - wider means more uncertain

These distributions are generated from the simulation using the parameters you set in the sidebar above. When you adjust the sliders, these distributions update accordingly. They are analogous to Squiggle/Guesstimate visualizations -- showing the full range of possible values, not just a point estimate.
</details>

### Technology Adoption (Realized)

```{ojs}
//| echo: false
html`<div class="grid" style="grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 2rem 0;">

<div class="card" style="border: 1px solid #ddd; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>Hydrolysates Adopted</h5>
<h2 style="color: #27ae60;">${(results.pct_hydro * 100).toFixed(0)}%</h2>
<small>of simulations use hydrolysates</small>
</div>

<div class="card" style="border: 1px solid #ddd; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>Food-Grade Micros</h5>
<h2 style="color: #3498db;">${(results.pct_foodgrade * 100).toFixed(0)}%</h2>
<small>of simulations use food-grade</small>
</div>

<div class="card" style="border: 1px solid #ddd; padding: 1rem; border-radius: 8px; text-align: center;">
<h5>Cheap Growth Factors</h5>
<h2 style="color: #9b59b6;">${(results.pct_recf_cheap * 100).toFixed(0)}%</h2>
<small>of simulations have cheap factors</small>
</div>

</div>`
```

### Sensitivity Analysis (Tornado Chart)

Which parameters have the most impact on the final cost? This chart shows **Spearman rank correlations** between each input parameter and the unit cost.

```{ojs}
//| echo: false
{
  // Calculate correlations between inputs and unit cost
  const uc = results.unit_cost;

  const correlations = [
    {name: "Cell Density (g/L)", corr: spearmanCorr(results.density_samples, uc), direction: "inverse"},
    {name: "Media Turnover", corr: spearmanCorr(results.media_turnover_samples, uc), direction: "direct"},
    {name: "L/kg (volume)", corr: spearmanCorr(results.L_per_kg_samples, uc), direction: "direct"},
    {name: "Media $/L", corr: spearmanCorr(results.media_cost_L_samples, uc), direction: "direct"},
    {name: "Uses Hydrolysates", corr: spearmanCorr(results.is_hydro_samples, uc), direction: "inverse"},
    {name: "Uses Food-Grade", corr: spearmanCorr(results.is_foodgrade_samples, uc), direction: "inverse"},
    {name: "Has Cheap GFs", corr: spearmanCorr(results.is_recf_cheap_samples, uc), direction: "inverse"},
    {name: "GF Quantity (g/kg)", corr: spearmanCorr(results.g_recf_samples, uc), direction: "direct"},
    {name: "GF Price ($/g)", corr: spearmanCorr(results.price_recf_samples, uc), direction: "direct"},
    {name: "Industry Maturity", corr: spearmanCorr(results.maturity_samples, uc), direction: "inverse"},
    {name: "Plant Capacity", corr: spearmanCorr(results.plant_kta_samples, uc), direction: "inverse"},
    {name: "Utilization Rate", corr: spearmanCorr(results.uptime_samples, uc), direction: "inverse"}
  ];

  // Sort by absolute correlation
  const sorted = correlations
    .map(c => ({...c, absCorr: Math.abs(c.corr)}))
    .sort((a, b) => b.absCorr - a.absCorr);

  const sensitivityPlot = Plot.plot({
    width: 850,
    height: 450,
    marginLeft: 170,
    marginRight: 70,
    x: {
      label: "Spearman Correlation with Unit Cost",
      domain: [-1, 1],
      grid: true,
      labelOffset: 40
    },
    y: {
      label: null,
      tickFormat: d => d,
      tickSize: 0
    },
    color: {
      domain: ["Increases cost", "Decreases cost"],
      range: ["#e74c3c", "#27ae60"]
    },
    style: {
      fontSize: "14px"
    },
    marks: [
      Plot.barX(sorted, {
        y: "name",
        x: "corr",
        fill: d => d.corr > 0 ? "Increases cost" : "Decreases cost",
        sort: {y: "-x", reduce: d => Math.abs(d)}
      }),
      Plot.ruleX([0], {stroke: "black", strokeWidth: 1}),
      Plot.text(sorted, {
        y: "name",
        x: d => d.corr > 0 ? d.corr + 0.04 : d.corr - 0.04,
        text: d => d.corr.toFixed(2),
        textAnchor: d => d.corr > 0 ? "start" : "end",
        fontSize: 13,
        fontWeight: 500
      })
    ]
  });

  return html`<div style="font-size: 1em;">
    <div style="font-weight: normal; font-size: 1.1em; margin-bottom: 0.5rem; color: #333;">Parameter Sensitivity: Correlation with Unit Cost</div>
    ${sensitivityPlot}
  </div>`;
}
```

<details>
<summary>How to read this chart (click to expand)</summary>

**Tornado chart interpretation:**

- **Bars extending right (red)**: Parameters that **increase cost** when they increase
  - Higher media $/L → higher cost
  - Higher GF price → higher cost
  - Higher L/kg volume → higher cost

- **Bars extending left (green)**: Parameters that **decrease cost** when they increase
  - Higher cell density → lower cost (less media needed)
  - Higher maturity → lower cost (better technology, cheaper inputs)
  - Using hydrolysates → lower cost (cheaper amino acids)

- **Bar length**: How much that parameter affects the output
  - |correlation| > 0.5 = strong effect
  - |correlation| 0.3-0.5 = moderate effect
  - |correlation| < 0.3 = weak effect

**This is Spearman rank correlation**, which captures monotonic relationships (not just linear). A correlation of -0.7 means "when this parameter is high, costs tend to be low" across the 30,000 simulations.

**Key insight**: The parameters with the longest bars are the ones to focus on when:
- Forecasting (these drive your uncertainty)
- Prioritizing R&D (these are the levers to pull)
- Gathering expert input (these are worth eliciting carefully)
</details>

### Industry Maturity Deep Dive

The **maturity factor** is a continuous variable ranging from 0 (nascent industry) to 1 (fully mature) that correlates multiple model inputs. It is not binary — intermediate values represent partial industry development. Higher maturity means:

- Higher technology adoption rates
- Lower reactor costs (more custom/food-grade)
- Lower financing costs (WACC)

```{ojs}
//| echo: false
{
  // Maturity distribution
  const matP5 = quantile(results.maturity_samples, 0.05);
  const matP50 = quantile(results.maturity_samples, 0.50);
  const matP95 = quantile(results.maturity_samples, 0.95);

  const maturityPlot = Plot.plot({
    width: 400,
    height: 220,
    marginLeft: 50,
    marginBottom: 40,
    marginTop: 10,
    x: {
      label: "Maturity Factor",
      domain: [0, 1]
    },
    y: {
      label: "Frequency",
      grid: true
    },
    marks: [
      Plot.rectY(results.maturity_samples, Plot.binX({y: "count"}, {x: d => d, fill: "#8e44ad", fillOpacity: 0.7})),
      Plot.ruleX([matP50], {stroke: "black", strokeWidth: 2}),
      Plot.ruleX([matP5], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"}),
      Plot.ruleX([matP95], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"})
    ]
  });
  const matLabel = `Maturity Distribution (median: ${matP50.toFixed(2)}, 90% CI: ${matP5.toFixed(2)} – ${matP95.toFixed(2)})`;

  // Cell density distribution
  const denP5 = quantile(results.density_samples, 0.05);
  const denP50 = quantile(results.density_samples, 0.50);
  const denP95 = quantile(results.density_samples, 0.95);

  const densityPlot = Plot.plot({
    width: 400,
    height: 220,
    marginLeft: 50,
    marginBottom: 40,
    marginTop: 10,
    x: {
      label: "Cell Density (g/L)",
      domain: [0, Math.min(quantile(results.density_samples, 0.98) * 1.1, 350)]
    },
    y: {
      label: "Frequency",
      grid: true
    },
    marks: [
      Plot.rectY(results.density_samples, Plot.binX({y: "count"}, {x: d => d, fill: "#16a085", fillOpacity: 0.7})),
      Plot.ruleX([denP50], {stroke: "black", strokeWidth: 2}),
      Plot.ruleX([denP5], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"}),
      Plot.ruleX([denP95], {stroke: "black", strokeWidth: 1.5, strokeDasharray: "3,3"})
    ]
  });
  const denLabel = `Cell Density Distribution (median: ${denP50.toFixed(0)} g/L, 90% CI: ${denP5.toFixed(0)} – ${denP95.toFixed(0)})`;

  return html`<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; margin: 1rem 0;">
    <div style="font-size: 0.9em;">
      <div style="font-weight: normal; margin-bottom: 0.3rem; color: #333;">${matLabel}</div>
      ${maturityPlot}
    </div>
    <div style="font-size: 0.9em;">
      <div style="font-weight: normal; margin-bottom: 0.3rem; color: #333;">${denLabel}</div>
      ${densityPlot}
    </div>
  </div>`;
}
```

```{ojs}
//| echo: false
{
  // Sample subset for scatter plot (too many points otherwise)
  const sampleSize = 2000;
  const step = Math.floor(results.unit_cost.length / sampleSize);
  const scatterData = [];
  for (let i = 0; i < results.unit_cost.length; i += step) {
    scatterData.push({
      maturity: results.maturity_samples[i],
      cost: results.unit_cost[i],
      density: results.density_samples[i]
    });
  }

  const scatterPlot = Plot.plot({
    width: 800,
    height: 320,
    marginLeft: 60,
    marginBottom: 50,
    marginTop: 10,
    x: {
      label: "Industry Maturity Factor",
      domain: [0, 1]
    },
    y: {
      label: "Unit Cost ($/kg)",
      domain: [0, Math.min(quantile(results.unit_cost, 0.95), 150)],
      grid: true
    },
    color: {
      label: "Cell Density (g/L)",
      scheme: "viridis",
      legend: true
    },
    marks: [
      Plot.dot(scatterData, {
        x: "maturity",
        y: "cost",
        fill: "density",
        fillOpacity: 0.5,
        r: 3
      }),
      Plot.linearRegressionY(scatterData, {x: "maturity", y: "cost", stroke: "red", strokeWidth: 2})
    ]
  });

  return html`<div style="font-size: 0.95em;">
    <div style="font-weight: normal; margin-bottom: 0.3rem; color: #333;">Maturity vs Unit Cost (colored by cell density)</div>
    ${scatterPlot}
  </div>`;
}
```

<details>
<summary>Understanding the maturity correlation</summary>

The scatter plot shows how **maturity** (x-axis) relates to **unit cost** (y-axis), with points colored by **cell density**.

**Key patterns:**

- **Negative slope**: Higher maturity → lower costs (red trend line)
- **Color gradient**: Higher density (yellow) tends to cluster at lower costs
- **Spread**: Even at high maturity, costs vary due to other random factors

This correlation structure prevents unrealistic scenarios where technology succeeds but financing remains difficult, or vice versa.
</details>

:::

---

## Quick Start

1. **Explore the "Learn" sections** at the top to understand how cultured chicken production works and how this model handles uncertainty.

2. **Adjust parameters** in the left sidebar. Each parameter has an expandable explanation.

3. **Results update automatically** as you change the Basic Parameters sliders — the summary cards, cost distribution, and probability thresholds all recalculate. (The Component Distributions section shows the underlying distributions and updates when you change the relevant sliders.)

4. **Navigate the results** to see:
   - **Cost Distribution**: Full probability distribution with key percentiles
   - **Probability Thresholds**: Chances of hitting key price points
   - **Cost Breakdown**: Which components drive costs
   - **Technology Adoption**: Realized adoption rates

## Pivotal Questions Context

This model is part of [The Unjournal's **Pivotal Questions** initiative](https://globalimpact.gitbook.io/the-unjournal-project-and-communication-space/pivotal-questions) — a project to identify and quantify uncertainties that matter most for high-impact decisions.

::: {.callout-note collapse="true"}
## What is a "Pivotal Question"?

A **pivotal question** is one where resolving the uncertainty would significantly change optimal resource allocation. For cultured chicken:

- If costs reach **<$10/kg by 2036**: Large-scale displacement of conventional chicken becomes plausible → prioritize support for industry scale-up
- If costs remain **>$50/kg**: Focus shifts to alternative interventions (plant-based, policy, etc.)

The pivotal question framework helps prioritize research and forecasting efforts where they matter most.

See: [The Unjournal's Pivotal Questions documentation](https://globalimpact.gitbook.io/the-unjournal-project-and-communication-space/pivotal-questions)
:::

**The cultured chicken pivotal question asks:**

> What will be the production cost of cultured chicken by the projection year, and what does this imply for animal welfare interventions?

**Key links:**

| Resource | Description |
|----------|-------------|
| [Pivotal Questions overview](https://globalimpact.gitbook.io/the-unjournal-project-and-communication-space/pivotal-questions) | Full documentation on the PQ framework |
| [Cultured meat on Metaculus](https://www.metaculus.com/questions/?search=cultured%20meat) | Forecasting questions with community predictions |
| [The Unjournal](https://unjournal.org) | Independent evaluations of impactful research |
| [Squiggle reference model](../models/cm_cost_v0.2.squiggle) | Local reference implementation (parameters synced with this dashboard) |
| [UC Davis ACBM Calculator](https://acbmcostcalculator.ucdavis.edu/) | Original academic cost model (Risner et al.) |
| [Good Food Institute](https://gfi.org/resource/cultivated-meat-eggs-and-dairy-state-of-the-industry-report/) | Industry reports and data |

---

## Technical Reference

::: {.callout-note}
**[View Full Technical Documentation](docs.qmd)** — Complete model formulas, parameter definitions, code reference, and methodology.

Quick reference:

$$\text{Unit Cost} = \text{VOC} + \text{CAPEX}_{/\text{kg}} + \text{Fixed OPEX}_{/\text{kg}} + \text{Downstream}_{/\text{kg}}$$

The model runs **30,000 Monte Carlo simulations**, sampling from parameter distributions and technology adoption switches to produce cost distributions and probability thresholds.
:::

---

## Key Insights

The table below summarizes typical outcomes from this interactive model at different maturity settings. These are illustrative ranges -- the actual results shown in the charts above update dynamically as you adjust the sidebar parameters.

| Scenario | Typical Median Cost | Key Drivers |
|----------|---------------------|-------------|
| High maturity (0.7+) | $15-30/kg | All technologies adopted, low WACC |
| Baseline (0.5) | $25-50/kg | Mixed adoption, moderate uncertainty |
| Low maturity (0.3-) | $50-100+/kg | Pharma-grade inputs, high financing costs |

**Most sensitive parameters:**

1. Technology adoption (especially hydrolysates)
2. Cell density at harvest
3. Industry maturity (affects multiple factors)

---

::: {.callout-warning collapse="true"}
## Limitations & Caveats

**Model status (March 2026):** This model is largely AI-generated and we cannot yet vouch for all parameter values. It is provided to fix ideas, give a sense of the sort of modeling we're interested in, and present a framework for discussion and comparison — not a definitive cost estimate. We welcome scrutiny and suggestions via [Hypothesis annotations](https://hypothes.is/) or [email](mailto:contact@unjournal.org).

1. **Static snapshot model** — Projects costs for a single projection year (adjustable 2026-2050). Does not model year-over-year learning curves; instead, the "maturity" parameter serves as a proxy for cumulative industry development.

2. **Downstream is optional** — Scaffolding, texturization, and forming costs can be included via toggle (+$2-15/kg for structured products).

3. **No geography** — Assumes generic global costs, not region-specific labor, energy, or regulatory factors.

4. **Limited contamination modeling** — No batch failure or contamination event distributions.
:::

---

## Sources

The model draws on parameter ranges and structural assumptions from the following published techno-economic analyses and industry reports, among others:

- [Risner et al. (2021) - UC Davis ACBM Calculator](https://www.mdpi.com/2304-8158/10/1/3) -- primary reference for plant scale, reactor costs, and media cost structure
- [Humbird (2021) - Scale-Up Economics for Cultured Meat](https://doi.org/10.1002/bit.27848) -- CAPEX estimates, scale exponents, and plant cost factors
- [CE Delft (2021) - TEA of Cultivated Meat](https://cedelft.eu/publications/tea-of-cultivated-meat/) -- European cost benchmarks and energy analysis
- [GFI (2024) - State of the Industry Reports](https://gfi.org/resource/cultivated-meat-eggs-and-dairy-state-of-the-industry-report/) -- industry cost targets and technology status
- [PMC Meta-analysis (2024)](https://pmc.ncbi.nlm.nih.gov/articles/PMC11663224/) -- cross-study comparison of cost projections
- [Nature Food Scoping Review (2024)](https://www.nature.com/articles/s41538-024-00352-0) -- media cost reduction approaches
- [The Unjournal - Cultivated Meat Evaluations](https://unjournal.org) -- independent peer evaluations of relevant research

Additional parameter-specific sources are cited in the expandable details under each slider and in the [Technical Documentation](docs.qmd).
 

Built by The Unjournal | Source Code