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
  • Limitations
  • Sources

Cultured Chicken Production Cost Model

  • Show All Code
  • Hide All Code

  • View Source

Interactive Monte Carlo TEA for Cost Projections

Share Your Feedback (click to expand)

This model is under active development. We welcome your comments and suggestions!

  • Inline comments: Use the Hypothesis sidebar (click < on the right edge to open/close)
  • General feedback: Open a GitHub issue
  • Questions: 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
Costs, intensities Lognormal Always positive, right-skewed (some very high values possible)
Probabilities, fractions Beta Bounded between 0 and 1, flexible shape
Bounded ranges Uniform Equal probability within bounds

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.


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

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})
What are these options?
  • CAPEX: Bioreactor and facility capital costs, annualized
  • Fixed OPEX: Labor, maintenance, overhead (scales sub-linearly with plant size)
  • Downstream: Scaffolding, texturization, and forming for structured products
Note: Downstream processing adds $2-15/kg for structured products (steaks, chicken breast). Not needed for ground meat applications.

Basic Parameters

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

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

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 target year.

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: "GF cost reduction progress (%)"
})
What do these sliders control? (click to expand)

Two controls for growth factors:

  1. P(Scalable GF technology) — Probability that at least one breakthrough production method reaches commercial scale. This switches between “expensive” and “cheap” price distributions.

  2. GF cost reduction progress — How far along the price reduction curve we are within each regime:

    • 0% = Current prices ($5,000-500,000/g expensive; $100-10,000/g cheap)
    • 50% = Midway
    • 100% = Industry target achieved ($1-100/g cheap; $50-5,000/g expensive)

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 target 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)"
})

Advanced: Process Intensities

Show advanced parameters
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)"
})
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: plant_capacity * 0.5,
    plant_kta_p95: plant_capacity * 2.0,
    uptime_mean: uptime,
    maturity_mean: adjustedMaturity,
    p_hydro_mean: p_hydro,
    p_foodgrade_mean: p_foodgrade,
    p_recfactors_mean: p_recfactors,
    wacc_p5: wacc_lo / 100,
    wacc_p95: wacc_hi / 100,
    asset_life_lo: asset_life_lo,
    asset_life_hi: asset_life_hi,
    density_gL_p5: density_lo,
    density_gL_p95: density_hi,
    media_turnover_p5: media_turnover_lo,
    media_turnover_p95: media_turnover_hi,
    include_capex: include_capex,
    include_fixed_opex: include_fixed_opex,
    include_downstream: include_downstream,
    gf_progress: gf_progress
  };

  return simulate(30000, 42, params);
}

// Calculate statistics
stats = {
  const uc = results.unit_cost;
  return {
    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
  };
}

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 cultured chicken</strong> (wet weight, unprocessed) in <strong>${targetYear}</strong>, based on ${n.toLocaleString()} Monte Carlo simulations. This is the cost to produce meat in a bioreactor — not retail price, which would include processing, distribution, and margins.
<br><br>
<strong>Why it matters:</strong> If production costs reach <strong>~$10/kg</strong> (comparable to conventional chicken), cultured meat could compete at scale. If costs remain <strong>>$50/kg</strong>, the technology may remain niche. 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>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>`

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)",
      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>Price-competitive with conventional chicken</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: 800,
    height: 350,
    marginLeft: 150,
    marginRight: 100,
    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: $${total.toFixed(2)}/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:

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 are like Squiggle/Guesstimate visualizations - they show 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 latent variable (0 = nascent, 1 = mature) that correlates multiple model inputs. 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 parameters - no need to click a button.

  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 [target 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 model reveals several important patterns:

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

  1. Static snapshot model — Projects costs for a single target 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

  • Risner et al. (2021) - UC Davis ACBM Calculator
  • The Unjournal - Cultured Meat Evaluations
  • Good Food Institute - State of the Industry Reports
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-tip collapse="true"}
## Share Your Feedback (click to expand)

This model is under active development. We welcome your comments and suggestions!

- **Inline comments**: Use the [Hypothesis](https://hypothes.is/) sidebar (click `<` on the right edge to open/close)
- **General feedback**: [Open a GitHub issue](https://github.com/unjournal/cm_pq_modeling/issues/new)
- **Questions**: 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 frame = document.querySelector('.annotator-frame');
  const btn = document.getElementById('toggle-hypothesis');
  if (frame) {
    if (frame.style.display === 'none') {
      frame.style.display = '';
      btn.textContent = 'Hide Annotations Sidebar';
    } else {
      frame.style.display = 'none';
      btn.textContent = 'Show Annotations Sidebar';
    }
  }
}

function toggleFullWidth() {
  const body = document.body;
  const btn = document.getElementById('toggle-fullwidth');
  const main = document.querySelector('main.content');

  if (body.classList.contains('fullwidth-mode')) {
    body.classList.remove('fullwidth-mode');
    if (main) main.style.maxWidth = '';
    btn.textContent = 'Expand Content';
  } else {
    body.classList.add('fullwidth-mode');
    if (main) main.style.maxWidth = '100%';
    btn.textContent = 'Normal Width';
    // Also hide hypothesis when going fullwidth
    const frame = document.querySelector('.annotator-frame');
    if (frame) frame.style.display = 'none';
    document.getElementById('toggle-hypothesis').textContent = 'Show Annotations Sidebar';
  }
}
</script>

<style>
.fullwidth-mode .page-columns {
  max-width: 100% !important;
}
.fullwidth-mode main.content {
  max-width: 100% !important;
  width: 100% !important;
}
.fullwidth-mode .column-page {
  max-width: 100% !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 |
|---------------|--------------|-----|
| Costs, intensities | **Lognormal** | Always positive, right-skewed (some very high values possible) |
| Probabilities, fractions | **Beta** | Bounded between 0 and 1, flexible shape |
| Bounded ranges | **Uniform** | Equal probability within bounds |

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

---

## 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}

**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})
```
<details><summary>What are these options?</summary>

- **CAPEX**: Bioreactor and facility capital costs, annualized
- **Fixed OPEX**: Labor, maintenance, overhead (scales sub-linearly with plant size)
- **Downstream**: Scaffolding, texturization, and forming for structured products

**Note:** Downstream processing adds $2-15/kg for structured products (steaks, chicken breast). Not needed for ground meat applications.
</details>

---

**Basic Parameters**

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

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

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

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

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

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

<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: "GF cost reduction progress (%)"
})
```
<details><summary>What do these sliders control? (click to expand)</summary>

**Two controls for growth factors:**

1. **P(Scalable GF technology)** — Probability that *at least one* breakthrough production method reaches commercial scale. This switches between "expensive" and "cheap" price distributions.

2. **GF cost reduction progress** — How far along the price reduction curve we are within each regime:
   - **0%** = Current prices ($5,000-500,000/g expensive; $100-10,000/g cheap)
   - **50%** = Midway
   - **100%** = Industry target achieved ($1-100/g cheap; $50-5,000/g expensive)

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

---

**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)"
})
```

---

**Advanced: Process Intensities**

<details>
<summary>Show advanced parameters</summary>

```{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)"
})
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>

:::

::: {.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: plant_capacity * 0.5,
    plant_kta_p95: plant_capacity * 2.0,
    uptime_mean: uptime,
    maturity_mean: adjustedMaturity,
    p_hydro_mean: p_hydro,
    p_foodgrade_mean: p_foodgrade,
    p_recfactors_mean: p_recfactors,
    wacc_p5: wacc_lo / 100,
    wacc_p95: wacc_hi / 100,
    asset_life_lo: asset_life_lo,
    asset_life_hi: asset_life_hi,
    density_gL_p5: density_lo,
    density_gL_p95: density_hi,
    media_turnover_p5: media_turnover_lo,
    media_turnover_p95: media_turnover_hi,
    include_capex: include_capex,
    include_fixed_opex: include_fixed_opex,
    include_downstream: include_downstream,
    gf_progress: gf_progress
  };

  return simulate(30000, 42, params);
}

// Calculate statistics
stats = {
  const uc = results.unit_cost;
  return {
    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
  };
}
```

### 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 cultured chicken</strong> (wet weight, unprocessed) in <strong>${targetYear}</strong>, based on ${n.toLocaleString()} Monte Carlo simulations. This is the cost to produce meat in a bioreactor — not retail price, which would include processing, distribution, and margins.
<br><br>
<strong>Why it matters:</strong> If production costs reach <strong>~$10/kg</strong> (comparable to conventional chicken), cultured meat could compete at scale. If costs remain <strong>>$50/kg</strong>, the technology may remain niche. 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>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>`
```

### 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)",
      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>Price-competitive with conventional chicken</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: 800,
    height: 350,
    marginLeft: 150,
    marginRight: 100,
    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: $${total.toFixed(2)}/kg)`
  })
}
```

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

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

### Component Distributions

Individual distributions for each cost driver:

```{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 are like Squiggle/Guesstimate visualizations - they show 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 latent variable (0 = nascent, 1 = mature) that correlates multiple model inputs. 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 parameters - no need to click a button.

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 [target 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 model reveals several important patterns:

| 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

1. **Static snapshot model** — Projects costs for a single target 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

- [Risner et al. (2021) - UC Davis ACBM Calculator](https://www.mdpi.com/2304-8158/10/1/3)
- [The Unjournal - Cultured Meat Evaluations](https://unjournal.org)
- [Good Food Institute - State of the Industry Reports](https://gfi.org)
 

Built by The Unjournal | Source Code