November 7 (engine 2026-11-07.2): forming cost. The optional forming switch now adds $0.5–2.5 per kg of hybrid product, sourced from GFI’s 2021 extrusion-plant costs, instead of an unsourced $2–15 per kg of cells. Default results are unchanged (the switch is off).
November 7 (engine 2026-11-07.1): physical consistency, provisional. Default results change. Medium use per kg can no longer fall below what the medium’s nutrients can build (a nutrient floor); recovery losses, failed or contaminated runs (more likely in long perfusion and continuous runs) and seed-train capital are now included; and the food-grade reactor cost ratio is 0.15–0.55 of pharma-grade, down from 0.35–0.85. An oxygen-transfer cap on density is available, off by default. The default median rises from $213 to about $276/kg dry ($16.90 to $21.25 per kg of hybrid product), and the share of draws under $10/kg hybrid falls from 23.6% to 11.6%. These changes have not yet been reviewed by engineers or cell-culture experts; the ranges are placeholders with stated sources. Each can be switched off under Structural stress tests. See physical consistency.
October 26 (target and basis, documentation only): the team set the model’s target: unconditional and hypothetical cost of a large plant built with the best technology available in 2036, whether or not such plants exist, in 2026 US dollars (estimand). It also set a planned standard output basis, washed cell biomass at a fixed solids content plus cost per kg of protein (output basis). The “downstream processing” switch is relabelled “forming and texturization” (outside the estimate). No model outputs changed.
October 26 (engine 2026-10-26.2): results are also shown per kg at a target solids content and per kg of protein, using placeholder conversions you can change under Structural stress tests. A placeholder harvest, washing and dewatering cost is available under Model Structure, off by default. Default results are unchanged.
October 26 (engine 2026-10-26.3): a third Fresh-media calculation option under Structural stress tests derives medium per kg from nutrient yield (grams of cell solids per litre of medium), so it no longer falls automatically as density rises. Off by default; default results are unchanged. See the structure check.
October 29 (engine 2026-10-29.1): hybrid product for comparisons. Products will not be made entirely of cultivated cells, so the number shown beside each dry-cell-mass cost, and the probability thresholds, are now for a kg of hybrid product: 25% cultivated cells (at chicken-meat moisture) plus 75% plant-based ingredients at $3/kg. The default median is about $17/kg of hybrid product ($213/kg dry). Change the inclusion rate under Hybrid product in the sidebar.
October 26 (engine 2026-10-26.4): default results change. The headline cost is now per kg of dry cell mass (about five times the old wet figures), thresholds then used a chicken-meat equivalent (all cells; replaced by the hybrid product on October 29), density inputs are g dry/L and $/kg inputs are per kg dry. Cost basis “wet” under Structural stress tests reproduces the earlier results exactly. See Basis: dry cell mass.
Two supplied research critiques prompted optional median-preserving priors, explicit growth-factor price ranges, and structural comparisons for media use, GF dosage, and maturity dependence. Baseline numerical assumptions are retained. New controls include provenance tooltips and links to the discussion responses, justification, and remaining questions. These additions are AI-implemented scenario tests, pending expert review.
September 20: the model pages now connect the technical limitations to the workshop’s skeptical–optimistic disagreement map and structured response round. This adds no new parameter values or headline forecast. It makes the top-down microbial benchmark, interaction risk, and remaining measurement needs visible, with provenance and caveats in the response note.
October 26: an opt-in Process-mode selection setting in Structural stress tests replaces the mode weights with feasibility probabilities and uses the cheapest feasible mode in each simulated world, optionally with decision noise. The default (draw mode by weights) and its results are unchanged. See least-cost mode selection for the method and caveats.
This model is largely AI-generated and, as of September 2026, has not been independently validated. It is provided to fix ideas, make assumptions inspectable, and support discussion and comparison. Do not treat the outputs as authoritative cost estimates.
Several limitations are decision-relevant, not cosmetic: the single latent-maturity factor is ad hoc; source dollars are not yet normalized to one real-dollar year; source data on different weight bases are converted with a single assumed solids content (see Basis: dry cell mass); and the parameter ranges have not yet been checked against the target quantity decided in October 2026, the cost of a large plant built with the best technology available in 2036, whether or not such plants exist (estimand). The sensitivity chart is a dollar-swing ranking, not a variance decomposition. We are addressing these incrementally.
We especially welcome expert review of: growth factor quantities and prices, bioreactor CAPEX ranges, and the maturity correlation structure — see below and the Sources section.
Since October 26, 2026 (engine 2026-10-26.4), the headline cost is per kg of dry cell mass, not per kg of wet biomass. Dry-mass figures are about five times the old wet figures (the default median is about $213/kg dry, against $43 per wet kg before), because a kg of harvested cells is only about 20% solids. Nothing about the underlying scenario changed.
Why. Harvested cells are mostly water, and washing and dewatering change what a “wet kg” contains: a paste at 15% solids carries half the protein of one at 30%. Dry mass does not change with washing or dewatering, maps to nutrition, and is the natural basis for nutrient stoichiometry (how much medium it takes to build a gram of cells).
What else you’ll see. Beside every dry-basis cost, the cost per kg of a hybrid product: 25% cultivated cells, counted at the water content of chicken meat (27.5% solids, a convention agreed on 28 October 2026), and 75% plant-based ingredients at $3/kg. Products will not be made entirely of cells, and there is no consensus inclusion rate, so 25% is a convention you can change in the sidebar. The probability thresholds ($5, $10, $25, $50) and chicken comparisons use the hybrid product. Cost per kg of protein is reported too. Cell density is entered in g dry cells per litre, and $/kg inputs are per kg dry. The old wet-basis output is one switch away (Structural stress tests → Cost basis) and reproduces the earlier numbers exactly. Shared links made before this change that set density or $/kg priors will now be read as dry-basis values. Why the basis matters, and how sources report it →
AI-drafted (Claude, 26 October 2026) from David Reinstein’s design decision; not yet reviewed by a person.
From workshop evidence to model revision. The May 8, 2026 workshop is complete. Its outputs now form a linked evidence-to-model workflow: the summary records the technical arguments, the beliefs analysis shows the forecast spread, the demand bridge connects factory-gate cost to an adoption model, and the proposed modeling hack turns the remaining disagreements into testable model changes.
The follow-up now separates disagreements about definitions, factual estimates, evidence, source credibility, interpretation, model structure, and the importance of a claim to the final result. This matters because agreement that one input can become cheaper does not establish that the total system becomes cheap. The workshop synthesis explains the distinction, and the crux response form asks skeptics and optimists to diagnose it directly.
The named beliefs-analysis dashboard remains an internal review artifact and is intentionally not linked from this public site; a redacted public synthesis should be produced separately.
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. For a side-by-side comparison of how published TEAs differ in their assumptions and estimates, see our TEA Comparison page. This model stops at a factory-gate cost; for what a given cost implies for consumer adoption and market share, see our Cost → Demand bridge, which connects to Pablo AMC’s demand-side model.
CELL BANK → SEED TRAIN → PRODUCTION → HARVEST → PRODUCT
[O] [OOO] [OOOOOOO] [===] [≡≡≡]
This model uses Monte Carlo simulation: we sample thousands of possible futures and see what distribution of costs emerges.
Maturity factor, adoption probabilities, utilization rate (0–1 share). For switching parameters (e.g., hydrolysate adoption), the model first samples an adoption probability from a Beta distribution, then uses a Bernoulli draw to determine whether each run uses the "adopted" or "non-adopted" cost regime.
Equal probability within bounds; used when we have only a plausible range and no reason to favor values within it
Asset life (years), CAPEX scale exponent, fixed OPEX scaling factor
Background reading. Each distribution name above links to its Wikipedia article. For a visual, non-technical introduction to distributions, see Seeing Theory: Probability Distributions (Brown University). The switching parameters also use a Bernoulli (yes/no) draw. How the model builds a lognormal from the p5/p95 (or p10/p90) values you enter is explained in Model formulas: how the distribution is constructed.
We are open to considering alternative distributional forms (e.g., triangular, PERT, or mixture distributions) and to making the modeling flexible enough for users to select or compare different distributional assumptions. Suggestions are welcome via Hypothesis annotations.
Reading the Results:
p5 (5th percentile): “Optimistic” - only 5% of simulations are cheaper
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 makes favorable assumptions co-occur more often. Technical success with difficult financing remains possible and should not be ruled out.
This approach draws on the concept of copula-based correlation in Monte Carlo simulation, where a shared latent variable induces dependence among otherwise independent marginal distributions. See methodological references.
More detail on the maturity factor implementation
The maturity factor is sampled once per simulation from a Beta distribution (mean set by the “Industry Maturity” slider, standard deviation 0.20). It then shifts the adoption probabilities (hydrolysates, food-grade, recombinant GFs) and adjusts WACC downward in high-maturity draws. This creates positive correlation among “good news” variables without imposing a rigid structure. The strength of the correlation is moderate by design — maturity explains some but not all of the variation in each parameter.
Caveat: This correlation structure is a modelling convenience, not an empirically calibrated design. The choice of a single latent variable, the specific sensitivity coefficients, and the functional form are all ad hoc. Results — especially in tail scenarios — may be sensitive to these choices. We encourage users to stress-test by varying the maturity slider widely and treat correlated-scenario outputs as illustrative rather than calibrated.
// URL state helpers: slider defaults pick up values from a synchronous// <head> script that extracts ?key=val on page load, stashes them in// window.__CM_URL_STATE__, and strips the query string BEFORE Hypothes.is// loads. See the include-in-header block at the top of this file.// Reading from the global (not location.search) is critical — by the time// OJS runs, the query string has already been stripped so that Hypothes.is// can find annotations anchored to the bare canonical URL.urlParams =window.__CM_URL_STATE__|| {}
Code
urlNum =function(key, def) {const v = urlParams[key];if (v ===undefined) return def;const n =Number(v);returnNumber.isFinite(n) ? n : def;}
Code
urlBool =function(key, def) {const v = urlParams[key];if (v ===undefined) return def;return v ==="1"|| v ==="true";}
html`<div id="simplified-note" style="display: ${simpleMode ?'block':'none'}; background: #f0f4e8; border-left: 4px solid #5a7a5a; padding: 0.7rem 0.9rem; margin-bottom: 1rem; font-size: 0.82em; line-height: 1.5; border-radius: 0 4px 4px 0;"><strong>Simplified view:</strong> Less pivotal parameters (plant capacity, uptime, financing costs, media-use multiplier) are set to reasonable defaults.These inputs are fixed for a shorter interface, not because their importance is guaranteed to be small. Inspect the background assumptions and use Advanced to vary them. The association chart ranks conditional-mean differences under the selected scenario; it does not measure shares of variance.<em>Switch off to adjust all parameters.</em></div>`
html`<p style="font-size: 0.85em; color: #555; margin: 0 0 0.4rem;">Products will not be made entirely of cultivated cells. The comparison cost beside each dry-cell-mass figure, and the probability thresholds, are for a kg of hybrid product with this share of cells (counted at chicken-meat moisture); the rest is plant-based ingredients at the filler cost. There is no consensus inclusion rate; 25% is a convention.</p>`
viewof include_capex = Inputs.toggle({label:"Include capital costs (CAPEX)",value:urlBool("include_capex",true)})viewof include_fixed_opex = Inputs.toggle({label:"Include plant overhead OPEX",value:urlBool("include_fixed_opex",true)})viewof include_downstream = Inputs.toggle({label:"Include forming in the hybrid product ($0.5–2.5 per kg of product)",value:urlBool("include_downstream",false)})viewof include_harvest_dewater = Inputs.toggle({label:"Include harvest, washing and dewatering (placeholder)",value:urlBool("include_harvest_dewater",false)})
What are these options? Why would you toggle them?
CAPEX: Bioreactor and facility capital costs, annualized. On by default because capital costs are a substantial portion of total production cost. You might toggle this off to isolate variable operating costs or to compare with analyses that report only VOC.
Plant overhead OPEX: Labor, maintenance, plant overhead. (Conventionally called “Fixed OPEX” in TEA literature, but it’s only fixed per plant — the total scales sub-linearly with plant size, so per-kg overhead falls as plants get larger.) On by default for the same reason as CAPEX. Toggle off to focus on marginal costs only.
Forming and texturization: Scaffolding, texturization, forming, blending and packaging for structured products. Off by default because these steps are outside the estimate, which is unstructured cell biomass. Toggle on to add the cost of blending and shaping the hybrid product: $0.5–2.5 per kg of product (5th–95th percentile), from GFI’s 2021 bottom-up costs for plant-protein extrusion plants. It is added to the hybrid-product cost only, not to the cell-mass cost. Scaffolded whole cuts (steaks, breast) would cost more and are not modeled. (Before November 7, 2026 this switch added an unsourced $2–15 per kg of cells; its URL parameter is still include_downstream.) Harvest, washing and dewatering to the standard solids basis are a different step: they belong inside the estimate but are not yet modeled explicitly. See downstream processing.
Harvest, washing and dewatering (placeholder): adds a placeholder $0.50–3.00/kg (5th–95th percentile) for bringing harvested cells to the standard solids basis. Off by default so published results do not change; the range is not sourced and will be replaced with values from a process model.
Most published TEAs report costs with CAPEX and plant overhead OPEX included. Toggling them off produces numbers that are not comparable to standard literature estimates.
Media Accounting
Code
viewof bundled_media = Inputs.toggle({label:html`Bundled media pricing (TEA-comparable) <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="When ON: basal media + growth factors + supplemental proteins are replaced by a single 'complete media' $/L figure, matching the convention used in research-grade published TEAs (Humbird 2021 etc.). Default OFF = separable accounting, which is better for reasoning about future cost reduction.">(?)</abbr>`,value:urlBool("bundled_media",false)})
Code
html`<div style="padding: 0.5rem 0.8rem; background: #f0f4fa; border-left: 3px solid #3498db; font-size: 0.84em; margin: 0.25rem 0 0.4rem 0; line-height: 1.5;"><strong>Bundled mode:</strong> Complete media $/L includes basal nutrients, growth factors, and supplemental proteins. Separate protein costs are zero; a separate GF override is ignored.Range ($50–$500/L default) reflects current research-grade complete media with pharma GFs.For 2036 projection, consider lowering the range. <a href="docs.html#bundled-media-pricing" target="_blank" style="white-space:nowrap;">Details ↗</a></div>`
Code
viewof bundled_media_p5 = Inputs.range([5,200], {value:urlNum("bundled_media_p5",50),step:5,label:"Complete media p5 ($/L)"})viewof bundled_media_p95 = Inputs.range([50,1000], {value:urlNum("bundled_media_p95",500),step:10,label:"Complete media p95 ($/L)"})
Production Model
Code
viewof cdmo_mode = Inputs.toggle({label:html`CDMO / Contract Manufacturing mode <abbr style="cursor:help; text-decoration:underline dotted; font-size:0.85em; color:#888;" title="A CDMO (Contract Development & Manufacturing Organization) owns the bioreactors and facility and charges a per-kg toll, replacing your CAPEX and plant overhead. Your VOC costs (media, growth factors) remain direct.">(?)</abbr>`,value:urlBool("cdmo_mode",false)})
Code
html`<div style="padding: 0.5rem 0.8rem; background: #fef9e7; border-left: 3px solid #f39c12; font-size: 0.84em; margin: 0.25rem 0 0.5rem 0; line-height: 1.5;">CAPEX and Plant Overhead OPEX replaced by a lognormal <em>CDMO toll fee</em>.VOC (media, GFs, other variable) and downstream remain direct costs.A side-by-side comparison appears in the results below. <a href="docs.html#cdmo-production-model" target="_blank" style="white-space:nowrap;">Full explanation ↗</a></div>`
These sliders set the center of each parameter’s distribution used in the Monte Carlo simulation. They are not simple point estimates – the simulation samples around these values. For example, setting Plant Capacity to 20 kTA means the simulation draws plant sizes from a lognormal distribution centered near 20 kTA (specifically, p5=10 and p95=40). All charts and results below update dynamically as you adjust these sliders.
What is this? Where do these ranges come from? (click to expand)
Annual production capacity in thousand metric tons (kTA). Current pilots: <1 kTA. Commercial target: 10-50 kTA.
Slider range (5-100 kTA): The lower bound (5 kTA) represents a modest first-generation commercial facility. The upper bound (100 kTA) represents an optimistic large-scale facility. The default (20 kTA) matches the reference scale in Risner et al. (2021) and is a commonly used benchmark in TEA literature.
What plant size affects:
Cost Component
Effect of Larger Plant
CAPEX per kg
↓ Scales sub-linearly (power law ~0.6-0.9)
Plant overhead 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.
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:urlNum("maturity",0.5),step:0.05,label:"Industry Maturity by 2036"})
What is this?
A synthetic latent variable (0 = nascent, 1 = mature) that correlates technology adoption, reactor costs, and financing. It is not calibrated to observed historical trajectories and does not encode an annual learning rate. The slider is a scenario index: it represents your judgment about how many favorable conditions co-occur, not a measured pace of progress.
Slider
Rough scenario interpretation
0.1–0.2
Industry largely stalls; few process improvements reach commercial use and financing remains difficult
0.3–0.4
Some technical progress, but major input, scale-up, or financing constraints remain
0.5
Neutral coupling assumption; the model uses the base technology probabilities without claiming this matches a historical trend
0.6–0.7
Several favorable technical and supply-chain developments co-occur
0.8–0.9
Strongly favorable bundle; most modeled pathways succeed and financing conditions improve
Recent evidence should update the individual parameter distributions, not be translated into a maturity learning rate. For example, GFI’s 2026 industry report records both tighter funding and 20,000-L production progress, while the FDA’s February 2026 inventory lists five completed cultured-cell food consultations. Those signals point in different directions and should not be compressed into a single estimated trend.
To approximate “today’s conditions” (minimal technical progress): set maturity to 0.1 and target year to 2026. Note that even then the parameter ranges reflect forward projections — the model has no explicitly calibrated 2024 cost baseline.
Earlier years → lower effective maturity, less technology adoption. The model scales maturity by a factor of (0.5 + 0.5 × (year − 2024) / 20), so:
2026 → maturity scaled to ~55% of slider value
2036 (default) → maturity scaled to ~80% of slider value
2044 → maturity at full slider value
This scaling is mechanical (linear interpolation), not fit to historical data. To approximate minimal progress from today’s state: combine year = 2026 with maturity = 0.1.
Probability of Each Process Mode (?)
Code
viewof p_fedbatch = Inputs.range([0,1], {value:urlNum("p_fedbatch",0.20),step:0.05,label:html`P(Fed-batch) <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="Low density (1–6 g dry/L, i.e. 5–30 g wet/L at 20% solids), moderate media use (1–2×). Nutrient-concentrated feeds added periodically. Simpler to operate but lower cell density than perfusion.">(?)</abbr>`})viewof p_perfusion = Inputs.range([0,1], {value:urlNum("p_perfusion",0.50),step:0.05,label:html`P(Perfusion) <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="Medium-high density (6–30 g dry/L, i.e. 30–150 g wet/L), higher media throughput (1–5×). Continuous media exchange with cell retention. Currently the industry standard for high-density CM production.">(?)</abbr>`})viewof p_continuous = Inputs.range([0,1], {value:urlNum("p_continuous",0.30),step:0.05,label:html`P(Continuous) <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="Highest density (10–40 g dry/L, i.e. 50–200 g wet/L), efficient media use (0.5–3×). Near-steady-state operation; cells grown and harvested continuously with optimized recycling.">(?)</abbr>`})
Code
// The mode-selection fields belong to structure_settings (defined in the sidebar's// structural panel) but are shown here, next to the weights they replace.(viewof structure_settings).modeSection
Code
{const tot = p_fedbatch + p_perfusion + p_continuous;if (tot <=0) returnhtml``;const rawPct = (tot *100).toFixed(0);const fb = (p_fedbatch / tot *100).toFixed(0);const pf = (p_perfusion / tot *100).toFixed(0);const ct = (p_continuous / tot *100).toFixed(0);const sumExact =Math.abs(tot -1) <0.001;const sumColor = sumExact ?"#27ae60":"#e67e22";const sumNote = sumExact?"":` — weights sum to ${rawPct}%, normalized to 100% in simulation`;const pct = v => (v *100).toFixed(0);if (results.mode_selection_stats) returnhtml`<div style="font-size:0.84em; color:#555; margin: -0.2rem 0 0.4rem 0;"> <strong>Weights above not used:</strong> <em>Cheapest feasible mode</em> is selected in the box just above; choose <em>Draw mode by weights</em> there to use them.<br> Selected: Fed-batch <strong>${pct(results.pct_fedbatch)}%</strong> · Perfusion <strong>${pct(results.pct_perfusion)}%</strong> · Continuous <strong>${pct(results.pct_continuous)}%</strong><br> Feasibility decides the mode in ${pct(results.mode_selection_stats.feasibility_binding_share)}% of worlds. </div>`;returnhtml`<div style="font-size:0.84em; color:#555; margin: -0.2rem 0 0.4rem 0;"> <span style="color:${sumColor}; font-weight:600;">Sum: ${rawPct}%${sumNote}</span><br> Using: Fed-batch <strong>${fb}%</strong> · Perfusion <strong>${pf}%</strong> · Continuous <strong>${ct}%</strong> </div>`;}
What are these modes and why does it matter? (click to expand)
These weights set the probability of each bioreactor process mode being drawn per Monte Carlo simulation. Each simulated plant runs in a single mode; the weights describe uncertainty about which mode the industry ends up using, not a mix of modes inside one facility. Within each mode, cell density and media-use multiplier are sampled from mode-specific ranges — preventing physically incoherent combinations.
How to set them. The question is which mode will actually be the lowest-cost method that is technically feasible and permitted by regulators in the projection year. By default the model does not choose the cheapest mode in each simulated world; it draws the mode independently of other parameters. So a fed-batch weight of 20% says: “in 20% of worlds, something (scale-up problems, contamination risk, regulation) rules out the higher-density modes and plants fall back on fed-batch.”
Alternative: cheapest feasible mode. Under How the mode is chosen, just below the weights, you can instead give the probability that perfusion and continuous culture each work at commercial scale. The model then prices every world under each mode and uses the cheapest feasible one; fed-batch is the fallback. The selected shares become an output. With default priors fed-batch is almost never the cheapest mode in a world, and continuous is cheaper than perfusion in about 84% of worlds, so feasibility of 70% (perfusion) and 30% (continuous) gives roughly the same shares and costs as the 20/50/30 weights. The rule favors whichever mode has the fewest downsides modeled: mode-specific equipment, consumable and contamination costs are not yet included. Method and caveats.
For reference, at default settings plants drawn as fed-batch come in under $25 per wet kg in only about 2% of draws (about 4% even with optimistic maturity, hydrolysate and growth-factor settings), compared with about 24% for perfusion and 52% for continuous (wet-basis figures from before the October 26, 2026 basis change). Treat this with caution: it follows largely from our fed-batch density prior (5–30 g wet/L, 1–6 g dry/L). Humbird (2021) reaches about 110 g/L in fed-batch with a metabolically improved cell line, and in his base case fed-batch is the cheaper mode ($37/kg vs $51/kg for perfusion). The 20/50/30 default weights are not anchored to a specific source.
Continuous exchange with cell retention (hollow fiber / centrifuge)
Continuous
10–40
50–200
0.5–3.0
Near-steady-state with recycling; highest densities
The dry ranges are the earlier wet ranges converted at 20% solids; the draws are identical.
Pure batch (single fill-and-dump) is excluded — not considered commercially viable for cultured meat at scale (expert reviewer feedback, April 2026).
Note: the default mix (20% / 50% / 30%) produces somewhat higher media usage per kg than the previous unconstrained defaults, because fed-batch and perfusion at medium density use more liquid than the prior lognormal(30–200 g/L) assumption. This is more physically realistic.
In full view, an “Override process mode constraints” toggle restores manual density and media-use sliders.
Technology Adoption by
Probability that the selected hypothetical plant uses each cost-reducing technology by the projection year. These are scenario probabilities, not industry production shares.
viewof p_hydro = Inputs.range([0.3,0.95], {value:urlNum("p_hydro",0.75),step:0.05,label:"P(Hydrolysates for basal media)"})
Basal media = the bulk nutrient broth cells grow in (water + amino acids + glucose + salts + vitamins) — everything except the expensive recombinant growth factors, which we model on a separate line. Hydrolysates are cheap enzymatically-digested plant or yeast proteins; they replace the purified pharmaceutical-grade amino acids (vegan but ~10× more expensive) that dominate Humbird’s basal-media cost.
The worked figures in this note are per kg wet biomass with wet densities; per kg dry cell mass at 20% solids, multiply litres and dollars by 5 and divide densities by 5.
Note on $/L vs $/kg output: The model uses $/L as an internal parameter, but the economically meaningful unit is $/kg of cell biomass produced — which equals ($/L) × (L consumed per kg output). L per kg = 1000/density(g/L) × media_turnover_multiplier. In an illustrative scenario (60 g/L density, 1.5× multiplier), this is ~25 L/kg: a $0.70/L hydrolysate medium costs ~$17.5/kg output; a $1.25/L pharma-grade medium costs ~$31/kg. At 200 g/L perfusion with 1.0× turnover (~5 L/kg), the same formulations cost $3.5/kg and $6.25/kg respectively — illustrating the inverse-density relationship when all other inputs are fixed; both density and price have unit elasticity in this formula. The “Media” bar in the cost breakdown chart already shows this $/kg result directly.
Note on correlation with density: $/L and cell density (g/L) are not fully independent — richer media is often needed to sustain higher densities, particularly in fed-batch systems where cells deplete the available nutrient pool. Combining a very low $/L with a very high density assumption in the same scenario may therefore overstate cost savings. The model partially addresses this via process-mode-specific density ranges, but does not impose explicit correlation between $/L and density within a given mode. See Model Limits for discussion.
What is this? (click to expand)
Reminder:Basal media is the bulk nutrient broth cells grow in (water, amino acids, glucose, salts, vitamins) — everything except the expensive recombinant growth factors, which we model as a separate line.
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 basically 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
↗ Indirect benefit: can reduce GF requirements by improving cell health; cannot replace GF signaling
Cost comparison:
Media Type
Cost ($/L)
Source
Pharma-grade amino acids
$0.50 – $2.50
Revised down from $1-4 based on GFI amino acid report (2025): real supplier quotes found Humbird’s AA prices 2-10x too high
Why 75% baseline? This is a judgmental scenario default, not an empirically estimated adoption rate. Pasitka et al. (2024) demonstrated a low-cost animal-component-free medium in one chicken-cell process, while Mi et al. (2025) found that selected plant hydrolysates reduced serum use by up to 70% in a porcine-cell prototype. These are meaningful feasibility results, but they do not establish full substitution, lot-to-lot consistency, or commercial performance across cell lines and large reactors.
How pivotal is this parameter? The unresolved questions are technical, operational, and regulatory: amino-acid completeness, supplementation needs, batch variability, quality control, performance at scale, residuals, and product-specific safety review. Current US and UK guidance focuses on process controls, hazards, and evidence for the submitted product; we have not found support for the stronger claim that regulators generally require pharmaceutical-grade ingredients. Treat 75% as a scenario to stress-test, not a consensus forecast.
See Maturity Correlation for how adoption probability is adjusted.
Note: basal micronutrients are folded into media $/L (click to expand)
This slider has been removed. A previous version of the model carried a separate “food-grade micronutrients” cost line (vitamins, minerals, trace salts). That structure double-counted basal media, which the model already describes as providing “amino acids, glucose, vitamins.”
Basal micronutrients (vitamins, minerals, trace salts) are not modeled separately here. Basal media on this page already includes vitamins, and the literature suggests vitamin/mineral sourcing is relatively low-cost compared with growth factors and recombinant proteins (O’Neill et al. 2021: vitamin sourcing is a “less pressing issue” and required minerals can “generally be obtained relatively inexpensively”). We therefore include basal micronutrients inside media $/L and reserve separate uncertainty terms for the supplemental recombinant proteins (insulin, transferrin, albumin) now modeled separately — see the model change log below.
Growth factors (GFs) signal cells to proliferate — at current research-grade prices, they can dominate media costs. The slider below sets P(at least one scalable production route — e.g., autocrine cell lines, plant-based farming, or precision fermentation — reaches commercial scale by the projection year), switching between “expensive” and “cheap” GF price regimes. For background on what growth factors do and why they’re expensive, see Learn → Step 5: Growth Factors; the production routes this slider refers to are compared in Solutions being developed.
Code
viewof p_recfactors = Inputs.range([0.1,0.9], {value:urlNum("p_recfactors",0.5),step:0.05,label:html`P(Scalable <abbr style="cursor:help;text-decoration:underline dotted;" title="Growth Factor — signaling proteins like FGF-2, IGF-1, TGF-β that tell cells to proliferate. Currently the most expensive media component.">Growth Factor (GF)</abbr> technology)`})
Code
viewof gf_progress = Inputs.range([0,100], {value:urlNum("gf_progress",50),step:5,label:html`GF price progress index for ${target_year} (%)`})
What do these sliders control? (click to expand)
Code
{const ranges = costModel.gfPriceRanges(gf_progress);returnhtml`<p title="Reports 5 and 6 requested visible control semantics. These are the engine's existing assumed price distributions, not newly fitted evidence."><strong>Current GF price ranges (p5–p95):</strong> cheap regime $${ranges.cheap[0].toFixed(2)}–$${ranges.cheap[1].toFixed(2)}/g; expensive regime $${ranges.expensive[0].toFixed(2)}–$${ranges.expensive[1].toFixed(2)}/g. Adoption selects the regime; progress shifts both ranges. Direct GF-cost priors and complete medium replace this calculation.</p>`;}
Two controls for growth factors: (1) P(Scalable GF technology) sets the probability of a breakthrough reaching commercial scale (switches between “expensive” and “cheap” price regimes). (2) GF cost reduction progress sets how far prices have fallen within each regime by the projection year — 0% = current prices, 100% = industry targets achieved.
Growth factors are signaling proteins (FGF-2, IGF-1, TGF-β, etc.) that tell cells to proliferate. Currently the most expensive media component — often 55-95% of media cost at research scale.
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 (Humbird formulation × 8–60 L/kg media use)
0.001 – 0.006
$500 – $50,000
$0.5 – $300
Breakthrough achieved (≈3× usage reduction)
0.0005 – 0.002
$10 – $1,000
Endpoint products $0.005–2 (not a cost credible interval)
Where the quantity range comes from. GFI’s 2023 recombinant-protein cost analysis reproduces the Humbird medium formulation with FGF at 0.1 mg/L and TGFβ at 0.002 mg/L — about 0.102 mg/L of true growth factors combined. GFI’s efficient-future scenarios assume roughly 8–13 L media per kg meat; less-optimized processes may use up to ~60 L/kg. Multiplying through gives ≈0.0008–0.006 g true GF/kg as the defensible cited range, with Pasitka et al. (2024)’s empirical ACF chicken process landing at ~0.00187 g/kg — comfortably inside this band. An earlier version of this model used 0.0001–0.02 g/kg, which was too broad on both tails.
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. Note that tightening the quantity range means the price regime is now doing more of the work in the tornado — the swing in cost comes primarily from the $/g uncertainty, not the g/kg uncertainty.
viewof p_supp_protein = Inputs.range([0.20,0.95], {value:urlNum("p_supp_protein",0.70),step:0.05,label:html`P(Albumin/transferrin/insulin affordable by ${target_year}) <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="Probability that food-grade recombinant production (from yeast, bacteria, or plant systems) brings albumin + transferrin + insulin costs to under ~$1/g by the target year, making per-kg-cell-mass contribution below ~$0.50/kg. Without breakthrough: pharma-grade sourcing could add $0.50-4/kg. Default 70%: considered more tractable than GF breakthrough because albumin at scale is closer to an engineering problem than a discovery problem.">(?)</abbr>`})
About supplemental proteins and cost ranges (click to expand)
Albumin, transferrin, and insulin are media additives — proteins grouped separately here for cost accounting (insulin also has signaling functions). They are widely used in serum-free cell culture but rarely modeled explicitly in CM TEAs (usually bundled into “other VOC” or media cost).
Per GFI’s 2024 supply-chain analysis, albumin is expected to represent ~97% of anticipated recombinant protein production volume for CM. At typical cell-culture concentrations (~1 mg/mL albumin) and the model’s L/kg range, the per-kg cost contribution is:
Sourcing
Albumin price
Contribution to cell mass cost
Food-grade recombinant (yeast)
$0.10–1/g
~$0.03–0.50/kg
Pharma-grade recombinant
$5–50/g
~$0.50–4/kg
Albumin-free formulation
$0/g
~$0/kg
Insulin and transferrin are used at much lower concentrations and contribute <$0.05/kg in all scenarios.
Financing
Code
viewof wacc_lo = Inputs.range([5,20], {value:urlNum("wacc_lo",8),step:1,label:"WACC Low (%)"})viewof wacc_hi = Inputs.range([10,35], {value:urlNum("wacc_hi",20),step:1,label:"WACC High (%)"})viewof asset_life_lo = Inputs.range([5,15], {value:urlNum("asset_life_lo",8),step:1,label:"Asset Life Low (years)"})viewof asset_life_hi = Inputs.range([10,30], {value:urlNum("asset_life_hi",20),step:1,label:"Asset Life High (years)"})
Code
{// Reactive diagnostic: show CAPEX share and explain why WACC might seem insensitiveconst capexMean =mean(results.cost_capex);const totalMean =mean(results.unit_cost);const capexPct = totalMean >0? (capexMean / totalMean *100).toFixed(0) :0;const isOff =!include_capex &&!simpleMode;const isCDMO = cdmo_mode;let msg ='';let color ='#27ae60';if (isOff) { msg ='CAPEX is currently excluded — financing parameters have no effect. Re-enable in Model Structure above.'; color ='#c0392b'; } elseif (isCDMO) { msg ='CDMO mode is on — the CDMO toll replaces CAPEX/WACC. These financing sliders have no effect.'; color ='#e67e22'; } elseif (capexPct <15) { msg =`CAPEX is only ${capexPct}% of mean total cost right now — media costs dominate (often driven by fed-batch process mode). Large WACC changes will cause small visible shifts. Try reducing the fed-batch weight above to increase CAPEX sensitivity.`; color ='#e67e22'; } else { msg =`CAPEX = ${capexPct}% of mean total cost. Changes to WACC and Asset Life will have visible effects.`; color ='#27ae60'; }returnhtml`<div style="font-size:0.82em; margin: 0.25rem 0 0.4rem; padding:0.4rem 0.7rem; background:#fafafa; border-left:3px solid ${color}; border-radius:0 4px 4px 0; line-height:1.5;">${msg} </div>`;}
What are WACC and Asset Life? (click to expand)
WACC (Weighted Average Cost of Capital): The blended rate of return required by debt and equity investors, used to annualize capital costs via the Capital Recovery Factor (CRF). A WACC of 8% is typical for established food manufacturing; 20%+ reflects the high risk premium investors demand for an unproven technology. The simulation samples WACC from a lognormal distribution between these bounds, further adjusted by the maturity factor.
Asset Life: How many years bioreactor and facility equipment is depreciated over. These two sliders define a uniform distribution over the plausible range — not a single fixed value. The simulation draws a different asset life for each of the 30,000 Monte Carlo runs. Shorter life (8 years) means higher annual capital charges; longer life (20 years) spreads costs but assumes equipment remains productive. To set a specific value, set both sliders to the same number.
These two parameters together determine the Capital Recovery Factor (CRF).
Cell Density / Media-Use Override (?)
Code
viewof override_mode_constraints = Inputs.toggle({label:html`Override process mode constraints <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="When ON: process-mode sampling is bypassed and you can specify density and media-use ranges directly. Useful for experts wanting to model specific bioreactor configurations.">(?)</abbr>`,value:urlBool("override_mode_constraints",false)})
Code
viewof density_lo = Inputs.range([2,20], {value:urlNum("density_lo",6),step:1,label:"Cell Density Low (g dry cells/L)"})viewof density_hi = Inputs.range([10,60], {value:urlNum("density_hi",40),step:2,label:"Cell Density High (g dry cells/L)"})
What is cell density and why does it matter so much? (click to expand)
Cell density (grams of dry cell mass per litre at harvest, since October 26, 2026) determines how much meat you get per liter of bioreactor volume. Under the default medium rule, higher density means less media per kilogram of product, which directly reduces the largest variable cost.
Units. Sources report density in three ways: wet weight per litre, dry weight per litre, or cell counts per millilitre times an assumed mass per cell. Dry and wet differ by the solids content (about 20% for harvested cells, so 50 g wet/L ≈ 10 g dry/L). Cell counts depend on an assumed mass per cell, which varies roughly twofold with growth rate and phase. The figures below give both.
Density (wet, as usually reported)
Density (dry, at 20% solids)
Media per kg wet / per kg dry
Typical context
10 g/L
2 g/L
~100 / ~500 L
Current lab scale
50 g/L
10 g/L
~20 / ~100 L
Near-term commercial target
200 g/L
40 g/L
~5 / ~25 L
Optimistic TEA projection
This is multiplicative. If media costs $1/L, going from 10 to 50 g wet/L cuts media cost from $100 to $20 per wet kg ($500 to $100 per dry kg). Going to 200 g wet/L cuts it to $5 per wet kg. Cell density is arguably the single most important technical parameter for cost reduction.
Current state: Most published data shows 10-50 g/L (wet). Some companies claim higher, but these claims are difficult to verify independently. Lever VC’s 2025 report claims 60-90 g/L has been achieved by “second generation” companies. Whether 200 g/L wet (40 g dry/L) is achievable by 2036 is a genuine open question.
What about bioreactor volume / tank size? (click to expand)
Bioreactor volume is another major uncertainty that is currently implicit in this model rather than a direct parameter.
The model computes total working volume as: total_volume = annual_output / (density × productivity × 365). It then applies a power-law scaling for CAPEX. But individual bioreactor tank size matters for several reasons:
Factor
Small tanks (2,000-5,000L)
Large tanks (20,000-50,000L)
Cost per liter
Higher
Lower (economies of scale)
Contamination risk
Lower
Higher (single failure = large loss)
Mixing/O2 transfer
Easier
Harder at scale
Flexibility
More modular
Less redundancy
Industry precedent
Pharma standard
Requires new engineering
Key debate: Some companies (e.g., Vow) claim to have built 20,000L bioreactors for under $1M in 14 weeks using custom food-grade designs. If true, this dramatically changes the CAPEX picture. Humbird’s analysis assumed pharma-grade bioreactors at $50-500/L.
Why it’s not a direct slider (yet): Adding individual tank size would require modeling the number of tanks, contamination batch-failure rates, and the trade-off between scale and reliability. This is a planned enhancement. For now, the Plant Capacity and Cell Density parameters together determine total working volume, and the custom reactor ratio (in full view) captures the pharma-vs-food-grade cost difference.
Workshop discussion: This was one of the key cruxes for the May 8, 2026 CM workshop — what bioreactor scale is realistic, and what does it cost?
Advanced: Media-use multiplier (×)
What is this — and why can it be below 1? (click to expand)
The model computes media volume per kg as (1000 / density) × multiplier. A value of 1 is traditional batch mode (fill reactor once, harvest); >1 is perfusion (multiple media-volume equivalents flow through during the run); <1 represents media recycling, fed-batch with concentrated feeds, or harvest-side cell concentration. The Learn page walks through all three mechanisms.
Range interpretation: The manual override uses p5–p95 0.5–3.0; default sampling instead uses process-specific ranges. These are lognormal percentiles, not hard bounds. Values below one need explicit medium recovery and consistently priced replacement nutrients. For example, 8 L/kg at 90 g/L implies a multiplier of 0.72, while the same medium use at 200 g/L implies 1.6. A lower multiplier is not the only way to achieve a particular L/kg value.
// URL state writer: serialize every viewof value that DIFFERS FROM ITS// DEFAULT into ?key=val pairs, then debounce-write to the URL via// history.replaceState. Critical invariant: if every slider is at its// default, the URL stays bare (pathname + hash only) — no query string.// This is required so Hypothes.is can find annotations on the canonical// bare URL; a polluted URL breaks annotation lookup for every visitor.// The writer depends on every viewof name below so OJS re-runs it// whenever any input changes. Reads nothing from urlParams.{// Hard-coded defaults must stay in sync with each Inputs.range() /// Inputs.toggle() declaration above and with the reset_adoption button.const defaults = {...costModel.DEFAULT_PARAMS,simpleMode:true,blending_share:0.25,filler_cost:3,include_capex:true,include_fixed_opex:true,include_downstream:false,include_harvest_dewater:false,cdmo_mode:false,cdmo_toll_p5:20,cdmo_toll_p95:200,bundled_media:false,bundled_media_p5:50,bundled_media_p95:500,plant_capacity:20,uptime:0.90,maturity:0.5,target_year:2036,p_fedbatch:0.20,p_perfusion:0.50,p_continuous:0.30,override_mode_constraints:false,p_hydro:0.75,p_recfactors:0.5,gf_progress:50,p_supp_protein:0.70,wacc_lo:8,wacc_hi:20,asset_life_lo:8,asset_life_hi:20,density_lo:6,density_hi:40,media_turnover_lo:0.5,media_turnover_hi:3.0 };const state = {...structure_settings,...Object.fromEntries(Object.entries(expert_priors_adv).map(([k, v]) => [`ep_${k}`, v])), simpleMode, blending_share, filler_cost, include_capex, include_fixed_opex, include_downstream, include_harvest_dewater, cdmo_mode, cdmo_toll_p5, cdmo_toll_p95, bundled_media, bundled_media_p5, bundled_media_p95, plant_capacity, uptime, maturity, target_year, p_fedbatch, p_perfusion, p_continuous, override_mode_constraints, p_hydro, p_recfactors, gf_progress, p_supp_protein, wacc_lo, wacc_hi, asset_life_lo, asset_life_hi, density_lo, density_hi, media_turnover_lo, media_turnover_hi };const usp =newURLSearchParams();let hasDiff =false;for (const [k, v] ofObject.entries(state)) {const def = defaults[k];let matches;if (typeof v ==="boolean") matches = (v === def);elseif (typeof v ==="number") matches =Math.abs(v - def) <1e-9;else matches = (v === def);if (!matches) { hasDiff =true;if (typeof v ==="boolean") usp.set(k, v ?"1":"0");elseif (typeof v ==="number"&&Number.isFinite(v)) usp.set(k,String(v));elseif (typeof v ==="string") usp.set(k, v); } }if (window._urlWriteTimer) clearTimeout(window._urlWriteTimer);window._urlWriteTimer=setTimeout(() => {try {const newUrl = hasDiff? (location.pathname+"?"+ usp.toString() + location.hash): (location.pathname+ location.hash); history.replaceState(null,"", newUrl); } catch (e) {console.warn("URL state update failed:", e); } },300);// Return an invisible empty element instead of `null` — when an OJS cell// returns null/undefined, the cell renders the literal text "null" in the// output. This is a no-op span used purely for its side effect (URL sync).returnhtml`<span style="display:none;"></span>`;}
settledParams = debouncedParams ??newPromise(() => {})// Primary simulation run (uses whatever mode is selected)// N=30,000 balances browser responsiveness and sampling precision.// Repeat-seed variability is reported by scripts/model-audit.mjs.results =simulate(30000,42, settledParams)// Comparison run: when CDMO mode is on, also run the in-house baseline// (same seed so VOC components are paired; only capital structure differs)// Returns an invisible span (not null) when CDMO is off — returning null// causes OJS to render the literal text "null" in the document.inhouse_results = {if (!cdmo_mode) returnhtml`<span style="display:none;"></span>`;returnsimulate(30000,42, {...settledParams,cdmo_mode:false});}// Units for labels. On the dry basis, thresholds and chicken comparisons use a hybrid// product (cells at meat moisture plus plant filler); on the wet basis, wet cost.basisLabels = {const dry = results.basis.cost_basis==="dry";const hybrid = modelControls.hybridLabel(results.basis);return { dry,cost: dry ?"$/kg dry cell mass":"$/kg wet biomass",costShort: dry ?"dry":"wet", hybrid,threshold: dry ?`$/kg ${hybrid}`:"$/kg wet biomass",thresholdShort: dry ?"hybrid product":"wet",// Where a hybrid-product threshold sits on the headline cost axistoCostAxis: t => dry ? modelControls.hybridToDry(t, results.basis) : t,// Probability-card price points: lower on the hybrid product than on wet cellsthresholds: dry ? [5,10,25,50] : [10,25,50,100],// Conventional chicken at the wholesale level (decided 5 November 2026), on the cost axis.// On the wet basis it is compared with wet cells directly.chicken: modelControls.CHICKEN_WHOLESALE,chickenAxis: dry ? modelControls.hybridToDry(modelControls.CHICKEN_WHOLESALE.central, results.basis) : modelControls.CHICKEN_WHOLESALE.central,chickenNote: modelControls.chickenWholesaleNote(results.basis),hybridNote: modelControls.hybridNote(results.basis),// Second x axis in hybrid-product dollars on dry-cost charts; none on the wet basismeatAxes: domainMax => dry ? modelControls.hybridAxes(Plot, domainMax, results.basis) : [],marginTop: dry ?45:undefined };}// Calculate statistics (cell cost and the threshold basis: hybrid product on dry)stats = {const uc = results.unit_cost;const tc = results.threshold_cost;const pct = (arr, threshold) => arr.filter(x => x < threshold).length/ arr.length*100;return {n: uc.length,p5:quantile(uc,0.05),p50:quantile(uc,0.50),p95:quantile(uc,0.95),t_p5:quantile(tc,0.05),t_p50:quantile(tc,0.50),t_p95:quantile(tc,0.95),probs: basisLabels.thresholds.map(x => ({threshold: x,prob:pct(tc, x)})) };}
Results Summary
Quantity being modeled: manufacturing cost per kg of cultured-chicken dry cell mass, shown alongside the ingredient cost of a kg of hybrid product (25% cells at chicken-meat moisture, 75% plant-based ingredients at $3/kg; adjustable). Wet biomass is one switch away under Structural stress tests. Each figure is for a hypothetical plant/process scenario. Each draw samples one process; process weights are probabilities across scenarios, not production shares within a simulated industry. The output is not an industry-average forecast or a frontier estimate. (With cheapest-feasible mode selection on, each draw uses the lowest-cost feasible mode, a step toward a frontier estimate for the process choice only.) Commercialization probability is unmodeled; dollars retain mixed source years.
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 manufacturing cost per kg of cultured chicken cell biomass (${basisLabels.dry?html`<span title="Per kg dry cell mass. Dry mass does not change with washing or dewatering, maps to nutrition, and is the natural basis for nutrient stoichiometry. Density inputs are g dry cells per litre. Wet-basis sources and built-in priors are converted at ${(results.basis.solids_fraction*100).toFixed(0)}% solids. Probability thresholds use the hybrid product: ${(results.basis.cell_inclusion_rate*100).toFixed(0)}% cells at ${(results.basis.target_solids_fraction*100).toFixed(1)}% solids plus plant-based filler. Includes media, growth factors, annualized bioreactor capital, plant overhead, and utilities. Excludes product formulation, texturization, packaging, distribution, and retail margins." style="text-decoration: underline dotted; cursor: help;">dry cell mass ⓘ</span>`:html`<span title="Per kg wet biomass at ${(results.basis.solids_fraction*100).toFixed(0)}% solids: the headline basis before October 26, 2026. Wet kg is not a stable unit, because washing and dewatering change what it contains." style="text-decoration: underline dotted; cursor: help;">wet-biomass basis ⓘ</span>`}) in ${target_year}, based on ${stats.n.toLocaleString()} Monte Carlo simulations. This is a factory-gate ingredient-cost estimate, not a consumer-product price. <a href="compare.html" style="font-size: 0.9em;">[Compare to published TEAs →]</a><br><br><strong><span title="UPSIDE Foods' chicken cutlet is a blend of cultured chicken cells and plant-based ingredients. SuperMeat's chicken burger used ~30% cultured cells. The GFI State of the Industry 2024 report notes that 'hybrid products combining cultivated and plant-based ingredients are the most likely near-term path to market.' Eat Just/GOOD Meat's Singapore-approved product uses cultured chicken in a plant-protein matrix." style="text-decoration: underline dotted; cursor: help;">Pure cells vs. consumer products:</span></strong> Most cultivated meat products on the market or in development are <em>hybrid products</em> — blending a fraction of cultured cells with plant-based or mycoprotein ingredients. That is why the second number on each card is for a hybrid product (${(results.basis.cell_inclusion_rate*100).toFixed(0)}% cells by default). <span title="${basisLabels.chickenNote}" style="text-decoration: underline dotted; cursor: help;">We compare it with conventional chicken at the wholesale level</span>: US wholesale boneless chicken averaged $${basisLabels.chicken.central.toFixed(2)}/kg in 2026 dollars over September 2022 to September 2026 (middle 80% of months $${basisLabels.chicken.low.toFixed(1)}–$${basisLabels.chicken.high.toFixed(1)}), the same point in the supply chain as this factory-gate cost.<br><br><strong>Three cautions before using the output:</strong> (1) source costs are not yet normalized to one real-dollar base year; (2) the CM_01 beliefs question is now unconditional and hypothetical: its central part (A1) asks for "the production-weighted average cost per kg of cultivated-chicken cell biomass in 2036, at a scale of at least 2,000 tonnes per plant per year", including hypothetical production if no such plants operate (<a href="https://uj-cm-workshop.netlify.app/question-changes.html">question changes</a>). This model now targets the same unconditional, hypothetical framing, but A1 asks for a production-weighted average in 2025 dollars while the model's decided target is the best available plant in 2026 dollars (<a href="limits.html#estimand">estimand note</a>); and (3) compare the output with conventional chicken at wholesale (about $${basisLabels.chicken.central.toFixed(2)}/kg boneless), not with the retail price (about $${basisLabels.chicken.retail.toFixed(2)}/kg): US retail prices include processing, distribution and retail margins this model does not cost. Even at wholesale, the hybrid product still leaves out forming and texturization, and its inclusion rate and filler cost are conventions, so it is an ingredient-cost scenario, not a parity result (<a href="docs.html#chicken-comparison">comparison point</a>).</div>`
Code
{// Dry cost and the hybrid-product cost at equal size; the wet basis shows one number.// Built per card: a DOM node can only appear in one place.const meatTag = () =>html`<span title="${basisLabels.hybridNote}" style="cursor: help; text-decoration: underline dotted;">${basisLabels.hybrid} ⓘ</span>`;const costs = (dryCost, meatCost) => basisLabels.dry?html`<h2 style="margin: 0.5rem 0 0;">$${Math.round(dryCost)}<span style="font-size: 0.55em; font-weight: 400;">/kg dry cell mass</span></h2><h2 style="margin: 0 0 0.5rem;">$${modelControls.formatMeatEquivalent(meatCost)}<span style="font-size: 0.55em; font-weight: 400;">/kg ${meatTag()}</span></h2>`:html`<h2 style="margin: 0.5rem 0;">$${Math.round(dryCost)}/kg wet</h2>`;returnhtml`<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>${costs(stats.p50, stats.t_p50)}<small>${basisLabels.dry?"":"Per kg wet biomass at harvest. "}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>${costs(stats.p5, stats.t_p5)}<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>${costs(stats.p95, stats.t_p95)}<small>95% of simulations cheaper</small></div></div><div style="background: #f4f6f8; border-left: 4px solid #7f8c8d; padding: 0.8rem 1rem; margin-bottom: 0.5rem; font-size: 0.9em;">${basisLabels.dry?html`<strong>Two numbers, one cost.</strong> Dry cell mass is the model's basis: it does not change with washing or dewatering and is the natural unit for nutrients. The second number is what that cost means for a product: a kg of hybrid product with ${(results.basis.cell_inclusion_rate*100).toFixed(0)}% cultivated cells (counted at chicken-meat moisture, ${(results.basis.target_solids_fraction*100).toFixed(1)}% solids) and the rest plant-based ingredients at $${results.basis.filler_cost}/kg. Ingredient cost only. All-cell meat at chicken-meat moisture would be <strong>$${modelControls.formatMeatEquivalent(quantile(results.unit_cost_meat_equiv,0.5))}/kg</strong> (median). Change the inclusion rate under <em>Hybrid product</em> in the sidebar.`:""} <strong>Other bases:</strong> median <strong>$${quantile(results.unit_cost_protein,0.5).toFixed(0)}/kg protein</strong> (${(results.basis.protein_fraction_of_solids*100).toFixed(0)}% of dry mass) and <strong>$${quantile(results.unit_cost_wet,0.5).toFixed(1)}/kg wet biomass</strong> at ${(results.basis.solids_fraction*100).toFixed(0)}% solids, the headline basis before October 26, 2026.${basisLabels.dry?"":html` On the default dry basis: <strong>$${quantile(results.unit_cost_dry,0.5).toFixed(0)}/kg dry cell mass</strong> and <strong>$${modelControls.formatMeatEquivalent(quantile(results.unit_cost_hybrid,0.5))}/kg ${basisLabels.hybrid}</strong>.`} The solids and protein shares are placeholders; washing losses are not modeled. Change them, or switch the headline basis, under <em>Structural stress tests</em>. <a href="docs.html#output-basis">Why the basis matters</a></div></div>`;}
Code
// Save / share bar: copy the current shareable link, or export the full// simulation (CSV of all 30,000 samples, or a compact JSON with the// parameters + summary stats + component means). Buttons are plain DOM so// we can attach click handlers that read the current `results` / `stats`// closure — the cell re-runs on every results change, which is fine.{functiondownloadBlob(content, filename, mime) {const blob =newBlob([content], {type: mime});const url = URL.createObjectURL(blob);const a =document.createElement("a"); a.href= url; a.download= filename;document.body.appendChild(a); a.click();document.body.removeChild(a);setTimeout(() => URL.revokeObjectURL(url),1000); }const stamp = () =>newDate().toISOString().slice(0,10);functiondownloadCSV() {const cols = ["unit_cost","cost_media","cost_recf","cost_supp_protein","cost_other_var","cost_capex","cost_fixed","cost_cdmo_toll","cost_downstream","cost_harvest","unit_cost_dry","unit_cost_wet","unit_cost_meat_equiv","unit_cost_protein"];const metaCols = ["model_version","seed","sample_count","effective_params_json"];const quote = v =>'"'+String(v).replaceAll('"','""') +'"';const meta = [results.model_version, results.seed, results.sample_count].map(quote);const rows = [[...cols,...metaCols].join(",")];const n = results.unit_cost.length;for (let i =0; i < n; i++) {// The full configuration appears once, on the first sample row. rows.push([...cols.map(c => results[c][i].toFixed(4)),...meta, i ===0?quote(JSON.stringify(results.effective_params)) :'""'].join(",")); }downloadBlob(rows.join("\n"),`cm_pq_results_${stamp()}.csv`,"text/csv"); }functiondownloadJSON() {const config = {model_version: costModel.MODEL_VERSION,review_provenance: results.provenance,structural_comparison: structural_comparison_rows,effective_simulation_parameters: results.effective_params,warnings: results.warnings,generated_at:newDate().toISOString(),dashboard_url:window.location.origin+window.location.pathname,shareable_link:window.location.href,parameters: { simpleMode, blending_share, filler_cost, include_capex, include_fixed_opex, include_downstream, include_harvest_dewater, cdmo_mode, cdmo_toll_p5, cdmo_toll_p95, plant_capacity, uptime, maturity, target_year, p_hydro, p_recfactors, gf_progress, wacc_lo, wacc_hi, asset_life_lo, asset_life_hi, density_lo, density_hi, media_turnover_lo, media_turnover_hi },summary_stats_unit_cost_per_kg: {n_simulations: stats.n,p5:+stats.p5.toFixed(2),p50:+stats.p50.toFixed(2),p95:+stats.p95.toFixed(2),prob_under:Object.fromEntries(stats.probs.map(d => [d.threshold,+d.prob.toFixed(1)])),cost_unit: basisLabels.cost,threshold_unit: basisLabels.threshold },process_mode: {rule: results.effective_params.mode_selection,shares: {fedbatch: results.pct_fedbatch,perfusion: results.pct_perfusion,continuous: results.pct_continuous},selection_stats: results.mode_selection_stats },component_means_per_kg: {media:+mean(results.cost_media).toFixed(2),growth_factors:+mean(results.cost_recf).toFixed(2),supplemental_proteins:+mean(results.cost_supp_protein).toFixed(2),other_voc:+mean(results.cost_other_var).toFixed(2),capex_annualized:+mean(results.cost_capex).toFixed(2),fixed_opex:+mean(results.cost_fixed).toFixed(2),cdmo_toll:+mean(results.cost_cdmo_toll).toFixed(2),forming_per_kg_product:+mean(results.forming_per_kg_product).toFixed(2),harvest_wash_dewater:+mean(results.cost_harvest).toFixed(2) },median_per_kg_by_basis: {dry_cell_mass:+quantile(results.unit_cost_dry,0.5).toFixed(2),chicken_meat_equivalent:+quantile(results.unit_cost_meat_equiv,0.5).toFixed(2),protein:+quantile(results.unit_cost_protein,0.5).toFixed(2),wet_biomass:+quantile(results.unit_cost_wet,0.5).toFixed(2),basis: results.basis },notes: {units:`USD per kg; headline ${basisLabels.cost}; thresholds ${basisLabels.threshold}`,currency_basis:"Mixed source-year USD; not yet normalized to a common real-dollar year",conditioning:"Intended estimand (decided 2026-10-26): unconditional and hypothetical cost of a large plant built with the best technology available in the target year, whether or not such plants exist; parameter ranges not yet re-checked against this target",basis_change:"Since engine 2026-10-26.4 the default basis is kg dry cell mass (density inputs g dry/L; $/kg inputs per kg dry). cost_basis 'wet' reproduces the earlier wet-basis results",simulation_seed:42,monte_carlo_samples:30000 } };downloadBlob(JSON.stringify(config,null,2),`cm_pq_config_${stamp()}.json`,"application/json"); }const btnStyle ="padding: 0.45rem 0.85rem; font-size: 0.85rem; border: 1px solid #5a7a5a; border-radius: 4px; background: #e8f4e8; color: #2d4a2d; cursor: pointer; white-space: nowrap; font-weight: 500;";const container =html`<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; margin: 0 0 1.25rem 0; padding: 0.6rem 0.85rem; background: #fbfcf9; border: 1px solid #d4e0d4; border-radius: 6px;"> <strong style="font-size: 0.85rem; color: #2d4a2d; margin-right: 0.25rem;">Save / share this scenario:</strong> <button data-btn="copy" style="${btnStyle}" title="Copy a URL that captures every slider and toggle value. Paste it anywhere to restore the exact scenario.">📋 Copy shareable link</button> <button data-btn="csv" style="${btnStyle}" title="Download all 30,000 Monte Carlo samples as CSV (unit cost + every component).">⬇ Results CSV</button> <button data-btn="json" style="${btnStyle}" title="Download a compact JSON with your parameter values, summary percentiles, and component means.">⬇ Config + summary JSON</button> <span data-status="" style="font-size: 0.82rem; font-weight: 500;"></span> </div>`;const statusEl = container.querySelector("[data-status]");functionflash(msg, ok =true) { statusEl.textContent= msg; statusEl.style.color= ok ?"#27ae60":"#c0392b";setTimeout(() => { statusEl.textContent=""; },2200); } container.querySelector("[data-btn=copy]").addEventListener("click", () => {if (navigator.clipboard&&navigator.clipboard.writeText) {navigator.clipboard.writeText(window.location.href).then(() =>flash("✓ Link copied to clipboard")).catch(() =>flash("× Clipboard denied — copy the URL from the address bar",false)); } else {flash("× Clipboard API unavailable — copy the URL from the address bar",false); } }); container.querySelector("[data-btn=csv]").addEventListener("click", () => {try { downloadCSV();flash("✓ CSV downloaded"); }catch (e) { flash("× CSV export failed: "+ e.message,false); } }); container.querySelector("[data-btn=json]").addEventListener("click", () => {try { downloadJSON();flash("✓ JSON downloaded"); }catch (e) { flash("× JSON export failed: "+ e.message,false); } });return container;}
Tip: the URL in your address bar updates live as you move sliders, so the “Copy shareable link” button always reflects the scenario you’re currently viewing.
{const uc = results.unit_cost;const clipVal =quantile(uc,0.98);const clipped = uc.filter(x => x <= clipVal);const p20 =quantile(uc,0.20);const p80 =quantile(uc,0.80);// Helper: build the distribution chart at a given pixel width/heightfunctionmakeDistChart(w, h, fontSize) {const yLabel1 =Math.round(h *6.0);const yLabel2 =Math.round(h *7.5);return Plot.plot({width: w,height: h,marginLeft:60,marginBottom:50,marginTop: basisLabels.marginTop,x: { label:`Cell Biomass Manufacturing Cost (${basisLabels.cost})`,domain: [0, clipVal *1.05] },y: { label:"Frequency",grid:true },style: { fontSize: fontSize ||13 },marks: [...basisLabels.meatAxes(clipVal *1.05), Plot.rectY(clipped, Plot.binX({y:"count"}, {x: d => d,fill:"steelblue",fillOpacity:0.7})),// p5 / p50 / p95 coloured lines 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"}),// p20 / p80 grey dashed Plot.ruleX([p20], {stroke:"#888",strokeWidth:1.5,strokeDasharray:"4,4",strokeOpacity:0.85}), Plot.ruleX([p80], {stroke:"#888",strokeWidth:1.5,strokeDasharray:"4,4",strokeOpacity:0.85}),// Reference cost thresholds ($10 and $25, hybrid product on the dry basis) Plot.ruleX([basisLabels.toCostAxis(10)], {stroke:"darkgreen",strokeWidth:2,strokeDasharray:"2,2",strokeOpacity:0.6}), Plot.ruleX([basisLabels.toCostAxis(25)], {stroke:"orange",strokeWidth:2,strokeDasharray:"2,2",strokeOpacity:0.6}),// US wholesale boneless chicken; omitted if the filler alone costs more (off the axis)...(basisLabels.chickenAxis>0? [ Plot.ruleX([basisLabels.chickenAxis], {stroke:"#8e44ad",strokeWidth:2,strokeDasharray:"6,3",strokeOpacity:0.85}), Plot.text([{x: basisLabels.chickenAxis,y:Math.round(h *3.0),text:`wholesale chicken $${basisLabels.chicken.central.toFixed(2)}`}], {x:"x",y:"y",text:"text",textAnchor:"start",dx:4,fontSize: (fontSize ||12) -2,fill:"#8e44ad"}) ] : []),// Labels Plot.text([ {x: stats.p5+1.5,y: yLabel1,text:`p5: $${stats.p5.toFixed(1)}`}, {x: stats.p50+1.5,y: yLabel2,text:`p50: $${stats.p50.toFixed(1)}`}, {x: stats.p95+1.5,y: yLabel1,text:`p95: $${stats.p95.toFixed(1)}`}, {x: p20 +1.5,y:Math.round(h *4.5),text:`p20: $${p20.toFixed(1)}`,fill:"#666"}, {x: p80 +1.5,y:Math.round(h *4.5),text:`p80: $${p80.toFixed(1)}`,fill:"#666"} ], {x:"x",y:"y",text:"text",fontSize: (fontSize ||12) -1,fill: d => d.fill||"black"}) ],title:`Projected ${target_year} Cultured Chicken Production Cost Distribution` }); }const wrapper =document.createElement("div"); wrapper.style.cssText="position:relative; display:inline-block; width:100%;";// Fullscreen expand buttonconst fsBtn =document.createElement("button"); fsBtn.textContent="⛶"; fsBtn.title="Expand to full screen (press Escape to close)"; fsBtn.style.cssText="position:absolute; top:6px; right:6px; z-index:10; padding:3px 7px; font-size:15px; cursor:pointer; border:1px solid #ccc; border-radius:4px; background:rgba(255,255,255,0.9); line-height:1;";// Fullscreen overlayconst overlay =document.createElement("div"); overlay.id="dist-chart-overlay"; overlay.style.cssText="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:white; z-index:9500; padding:2rem; box-sizing:border-box; overflow:auto;";const closeBtn =document.createElement("button"); closeBtn.textContent="✕ Close"; closeBtn.style.cssText="position:fixed; top:16px; right:20px; padding:6px 14px; font-size:14px; cursor:pointer; border:1px solid #ccc; border-radius:6px; background:#f8f9fa; z-index:9501;"; closeBtn.onclick= () => { overlay.style.display="none"; }; overlay.appendChild(closeBtn);// Keyboard closedocument.addEventListener("keydown", e => {if (e.key==="Escape"&& overlay.style.display!=="none") overlay.style.display="none"; }); fsBtn.onclick= () => { overlay.style.display="block";// Build large chart at near-full viewport sizeconst w =Math.min(window.innerWidth-80,1600);const h =Math.min(window.innerHeight-120,900);// Clear old chartwhile (overlay.children.length>2) overlay.removeChild(overlay.lastChild); overlay.appendChild(makeDistChart(w, h,14)); };document.body.appendChild(overlay); wrapper.appendChild(makeDistChart(800,400,12));const tailNote =document.createElement("p"); tailNote.style.cssText="font-size:0.85em;color:#666;"; tailNote.textContent="Histogram shows the lowest 98% of draws. Summary statistics and threshold probabilities use every draw."; wrapper.appendChild(tailNote); wrapper.appendChild(fsBtn);return wrapper;}
How 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 (ingredient cost only; not a price-parity line)
Orange dotted: $25/kg reference
Purple dashed: US wholesale boneless chicken, $3.50/kg (average September 2022 to September 2026 in 2026 dollars; USDA ERS wholesale prices). This is the comparison point for conventional chicken: the same stage of the supply chain as the model’s factory-gate cost, not the retail shelf price (about $9.27/kg).
On the default dry basis the reference lines are per kg of hybrid product (25% cells): $10 and $25 sit at about $113 and $331 per kg dry cell mass, and wholesale chicken at about $18 per kg dry. Because the plant-based filler alone costs $2.25 per kg of hybrid product, the cells have to cost less than about $18 per kg dry for the hybrid to match wholesale chicken. On the wet basis the purple line compares wet cells directly with chicken meat. Why wholesale, and the range →
The width of the distribution shows uncertainty. Narrow = confident. Wide = uncertain.
These percentages answer: “What’s the chance that production costs fall below key price points?” — based on the parameter distributions you’ve set above. On the default dry basis the price points apply to a kg of hybrid product: 25% cultivated cells (counted at chicken-meat moisture, 27.5% solids) and 75% plant-based ingredients at $3/kg, both adjustable in the sidebar. Each card also gives the same price point per kg dry cell mass. These are ingredient costs; forming, packaging and margins are excluded. If you switch Structural stress tests → Cost basis to wet, the price points ($10 to $100) apply per kg of wet cell biomass instead, as they did before October 26, 2026.
Code
{functioncard(threshold, prob, color) {const borderColor = prob >30? color :'#ddd';const what = basisLabels.dry?"Hybrid product":"Cells (wet)";return`<div class="card" style="border: 2px solid ${borderColor}; padding: 1rem; border-radius: 8px; text-align: center;"> <h5 style="margin-bottom:0.3rem;">P(${what} < $${threshold}/kg${basisLabels.dry?` = $${Math.round(basisLabels.toCostAxis(threshold))}/kg dry`:""})</h5> <h2 style="color: ${color}; margin:0.2rem 0;">${prob.toFixed(1)}%</h2> <small style="color:#555;">illustrative ingredient-cost threshold</small> </div>`; }const colors = ['#27ae60','#3498db','#f39c12','#e74c3c'];const grid =`<div class="grid" style="grid-template-columns: repeat(4, 1fr); gap: 1rem; margin: 1.5rem 0;">${stats.probs.map((d, i) =>card(d.threshold, d.prob, colors[i])).join("")} </div>`;const c = basisLabels.chicken;const share = t => results.threshold_cost.filter(x => x < t).length/ results.threshold_cost.length*100;const what = basisLabels.dry?"hybrid product":"wet cells";const fmt = p => p <0.05&& p >0?"under 0.1%":`${p.toFixed(1)}%`;const chickenLine =`<p style="font-size:0.92em; margin:-0.5rem 0 1.5rem;" title="${basisLabels.chickenNote.replace(/"/g,'"')}"><strong>Compared with conventional chicken at wholesale:</strong> P(${what} < $${c.central.toFixed(2)}/kg, US wholesale boneless chicken) = <strong>${fmt(share(c.central))}</strong>; across the middle 80% of monthly prices, $${c.low.toFixed(1)}–$${c.high.toFixed(1)}/kg, it is ${fmt(share(c.low))} to ${fmt(share(c.high))}. <span style="color:#666;">The $5–$50 cards are illustrative cost levels; none is a parity line. Retail chicken (about $${c.retail.toFixed(2)}/kg) is not a like-for-like comparison.</span></p>`;returnhtml([grid + chickenLine]);}
Cost Breakdown
Where does the cost come from? This chart shows the mean contribution of each cost component across all 30,000 simulations (mean, not median — skewed components like growth factors will appear larger here than at the median). The largest bars are the cost drivers to focus on — these are where technological progress or parameter uncertainty has the most impact. How each component is calculated →
Code
{const mediaLabel = bundled_media ?"Complete Media (all proteins included)":"Media (incl. basal micros)";const allComponents = [ {name: mediaLabel,value:mean(results.cost_media),color:"#27ae60"}, {name:"Growth Factors",value:mean(results.cost_recf),color:"#9b59b6"}, {name:"Supplemental Proteins",value:mean(results.cost_supp_protein),color:"#e67e22"}, {name:"Other VOC",value:mean(results.cost_other_var),color:"#7f8c8d"}, {name:"CAPEX (annualized)",value:mean(results.cost_capex),color:"#e74c3c"}, {name:"Plant overhead OPEX",value:mean(results.cost_fixed),color:"#f39c12"}, {name:"CDMO Toll",value:mean(results.cost_cdmo_toll),color:"#e67e22"}, {name:"Harvest, wash & dewater",value:mean(results.cost_harvest),color:"#16a085"} ];// 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);const chartContainer =document.createElement("div"); chartContainer.style.position="relative";// Expand/collapse buttonconst expandBtn =document.createElement("button"); expandBtn.textContent="Expand Chart"; expandBtn.style.cssText="padding: 0.3rem 0.7rem; font-size: 0.8rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f8f9fa; margin-bottom: 0.5rem;";let expanded =false;// Chart wrapper: clear-and-append avoids replaceWith()+querySelector on SVG// which fails silently in Safari (classList.add on SVG unreliable in WebKit).const chartWrapper =document.createElement("div"); expandBtn.onclick= () => { expanded =!expanded; expandBtn.textContent= expanded ?"Collapse Chart":"Expand Chart"; chartWrapper.innerHTML=""; chartWrapper.appendChild(makeChart(expanded)); }; chartContainer.appendChild(expandBtn);functionmakeChart(large) {const w = large ?1200:1000;const h = large ?700:580;const fontSize = large ?14:13;const xMax =Math.max(...components.map(c => c.value)) *1.05;return Plot.plot({width: w,height: h,marginLeft:200,marginRight:140,marginTop: basisLabels.marginTop,x: {label:`Average Cost (${basisLabels.cost})`,grid:true,domain: [0, xMax] },y: {label:null },marks: [...(basisLabels.dry? [Plot.gridX(),...basisLabels.meatAxes(xMax)] : []), 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: fontSize }) ],title:`Cost Breakdown by Component (Total: $${Math.round(total)}/kg)` }); } chartWrapper.appendChild(makeChart(false)); chartContainer.appendChild(chartWrapper);return chartContainer;}
Understanding the cost components
Variable Operating Costs (VOC):
Media: Nutrient broth for cell growth (amino acids, glucose, vitamins, minerals, trace salts) — basal micronutrients are included here, not as a separate line item
Capacity-anchored Costs (per-plant, scale sub-linearly with plant size):
CAPEX: Capital costs (reactors, facilities) annualized via Capital Recovery Factor
Plant overhead OPEX: Labor, maintenance, plant overhead (conventionally called “Fixed OPEX” — total scales sub-linearly with plant size, so per-kg overhead falls as plants grow)
The relative sizes shift dramatically based on technology adoption assumptions.
Component Distributions
Show per-component distributions (click to expand)
Individual distributions for each cost driver, updating dynamically as you adjust parameters.
Each mini-histogram shows the distribution for one cost component across 30,000 simulations:
Solid black line: Median (p50) - the middle value
Dashed black lines: 5th and 95th percentiles (90% confidence interval)
Width of distribution: Uncertainty - wider means more uncertain
These distributions are generated from the simulation using the parameters you set in the sidebar above. When you adjust the sliders, these distributions update accordingly.
Realized adoption rates (click to expand)
Actual shares across the 30,000 draws. Near-identical to slider inputs at default maturity (0.5); deviates when maturity ≠ 0.5, because the maturity distribution shifts each draw’s adoption probability before sampling. Process mode shares always equal slider inputs exactly and are omitted here.
Code
html`<div style="display:flex; gap:2rem; flex-wrap:wrap; font-size:0.95em; padding: 0.5rem 0;"> <span>Hydrolysates: <strong style="color:#27ae60;">${(results.pct_hydro*100).toFixed(0)}%</strong> of draws</span> <span>Cheap GFs: <strong style="color:#9b59b6;">${(results.pct_recf_cheap*100).toFixed(0)}%</strong> of draws</span></div>`
Sensitivity analysis (tornado chart): association under the current uncertainty model
Which parameters have the most impact on the final cost? Each bar shows the dollar swing in mean unit cost between simulations where the parameter is in its top 10% versus its bottom 10%. Larger bars indicate stronger associations under the selected assumptions. This is one of several ways to ask the question, and not the most robust one; the sensitivity methods page explains what the bars do and do not measure and compares alternatives on the same scenario.
{const uc = results.unit_cost;// The displayed inputs and their activity rules live in sensitivity-methods.mjs,// shared with the sensitivity page. L/kg and the two regime switches are absent// by design: L/kg is density × media-use multiplier, and the switches sit inside// the Media $/L and GF bars.const activeParams = sensitivityMethods.displayedInputs(results, settledParams);// Compute signed swings: positive = high param → higher cost (red); negative = high param → lower cost (green)const swingsRaw = activeParams.map(p => ({name: p.name,kind: p.kind,rawSwing:conditionalSwing(p.data, uc,0.10) }));const swings = swingsRaw.map(s => ({name: s.name,kind: s.kind,swing:Math.abs(s.rawSwing),// Red: high value raises cost; Green: high value lowers costfill: s.rawSwing>=0?"#c0392b":"#27ae60" }));const sorted = [...swings].sort((a, b) => b.swing- a.swing);const maxAbs =Math.max(...sorted.map(s => s.swing),1);const pad = maxAbs *0.25;const tornadoPlot = Plot.plot({width:900,height:440,marginLeft:290,marginRight:120,x: {label:`Dollar swing in mean unit cost (${basisLabels.cost}) — larger bar = stronger cost association`,domain: [0, maxAbs + pad],grid:true,labelOffset:40,tickFormat: d =>"$"+ d.toFixed(0) },y: { label:null,tickFormat: d => d,tickSize:0 },style: { fontSize:"13px" },marks: [ Plot.barX(sorted, {y:"name",x:"swing",fill: d => d.fill,fillOpacity:0.80,sort: {y:"-x"} }), Plot.text(sorted, {y:"name",x: d => d.swing+ maxAbs *0.02,text: d =>"$"+ d.swing.toFixed(1) +"/kg",textAnchor:"start",fontSize:12,fontWeight:500 }) ] });returnhtml`<div style="font-size: 1em;"> <div style="font-weight: normal; font-size: 1.05em; margin-bottom: 0.5rem; color: #333;">Which sampled inputs are associated with higher or lower costs?</div>${tornadoPlot} </div>`;}
Red bars: high values occur with higher costs. Green bars: high values occur with lower costs. Bar length = conditional-mean dollar swing between top 10% vs. bottom 10% of that parameter’s distribution across the 30,000 Monte Carlo draws. This is an association, not an intervention effect or variance decomposition. Correlated inputs overlap; bars should not be summed or interpreted as research value. Industry Maturity is a latent variable whose bar captures the bundled effect of correlated improvements. Bars of a few dollars per wet kilogram (about five times that per dry kilogram) are within simulation noise.Full methodology →
These runs keep your other inputs and seed (42) fixed. Each row changes one structural choice relative to your selected scenario. Fresh-media endpoints and GF concentration use the values in Structural stress tests. Comparisons reflect those provisional inputs as well as model form; they are not empirical validation or a variance decomposition. Overrides may make a comparison inactive; see the notes column.
Code
structural_comparison_rows = {const alternatives = [ ["Selected scenario", {}], ["Shared maturity", {dependence_mode:"shared"}], ["Independent maturity channels", {dependence_mode:"independent_maturity"}], ["Density-derived media", {media_use_model:"density"}], ["Direct fresh media", {media_use_model:"direct"}], ["Media from nutrient yield", {media_use_model:"yield"}], ["Separate GF dose prior", {gf_dose_model:"per_kg"}], ["Media-linked GF dose", {gf_dose_model:"media_linked"}], ["Mode drawn by weights", {mode_selection:"mixture"}], ["Cheapest feasible mode", {mode_selection:"least_cost"}] ];// Up to nine extra engine runs (three engine passes each under cheapest-feasible// selection). Yield to the browser between runs so input stays responsive, and// stop early if the inputs change.let stale =false; invalidation.then(() => { stale =true; });const rows = [];for (const [scenario, changes] of alternatives) {awaitnewPromise(resolve =>setTimeout(resolve,0));if (stale) return rows;const unchanged =Object.entries(changes).every(([key, value]) => results.effective_params[key] === value);const r = unchanged ? results :simulate(30000,42, {...settledParams,...changes}); rows.push({scenario,p50:quantile(r.unit_cost,0.5),p90:quantile(r.unit_cost,0.9),under25:100* r.threshold_cost.filter(c => c <25).length/ r.threshold_cost.length,notes: r.warnings.join(" ")}); }return rows;}html`<div class="structural-comparison-table"><style>.structural-comparison-table table { table-layout:fixed; width:100%; border-collapse:collapse; }.structural-comparison-table th, .structural-comparison-table td { white-space:normal; vertical-align:top; line-height:1.3; padding:.4rem .55rem; border-bottom:1px solid #ddd; text-align:left; }@media(max-width:600px) { .structural-comparison-table th, .structural-comparison-table td { padding:.35rem .2rem; font-size:.8rem; overflow-wrap:anywhere; } }</style><table> <colgroup><col style="width:27%"><col style="width:13%"><col style="width:13%"><col style="width:13%"><col style="width:34%"></colgroup> <caption>Same seed and settings; one structural choice changed per row.</caption> <thead><tr><th scope="col">Structure</th><th scope="col">p50 ($/kg ${basisLabels.costShort})</th><th scope="col">p90 ($/kg ${basisLabels.costShort})</th><th scope="col">P(<$25 ${basisLabels.thresholdShort}), %</th><th scope="col">Override notes</th></tr></thead> <tbody>${structural_comparison_rows.map(r =>html`<tr><th scope="row">${r.scenario}</th><td>${r.p50.toFixed(2)}</td><td>${r.p90.toFixed(2)}</td><td>${r.under25.toFixed(1)}</td><td>${r.notes||"None"}</td></tr>`)}</tbody></table></div>`
The maturity factor is a continuous variable ranging from 0 (nascent industry) to 1 (fully mature) that correlates multiple model inputs. It is not binary — intermediate values represent partial industry development. Higher maturity means:
Not calibrated to observed historical trajectories. The maturity slider is a forward-looking judgment parameter — it does not incorporate data on how fast CM costs have actually fallen since 2015. The model’s parameter ranges (e.g., media $/L, GF $/g) were set from TEA literature snapshots (Humbird 2021, Pasitka 2024, GFI 2025 amino acid report) — point estimates, not a fitted time series. The year → maturity scaling formula (linear from 2024 to 2044) is mechanical convenience, not a regression on industry data.
What this means: “maturity = 0.5” does not mean “the historical rate of progress continues.” It means “the model’s default assumption is that by 2036 the industry lands roughly in the middle of the plausible range.” Whether that default is consistent with observed progress since 2020 is a substantive question this model cannot answer on its own — and a good one to raise with experts at the workshop.
Correlation structure caveat (important for interpretation): The model uses a single shared latent factor to induce correlation across technology adoption, reactor costs, and financing. This is an ad hoc modelling convenience — not an empirically calibrated copula. The specific sensitivity coefficients (e.g., ±0.25 shifts to adoption probabilities, ±0.03 shifts to WACC) and the choice of a single dimension are choices made for tractability, not derived from data.
What this means for interpretation: - Scenarios where all dimensions co-move together (high maturity = everything goes well; low maturity = everything is hard) will be overrepresented relative to a fully independent model - Scenarios where, say, bioprocess technology matures rapidly but financing remains difficult are partly suppressed - Stress test: Set the maturity slider to 0.1 (pessimistic) and 0.9 (optimistic) and compare — the difference captures the full correlated scenario range. Then check whether P(cost < $25/kg) changes materially - Setting the maturity mean to 0.5 does not remove dependence: maturity still varies across draws. A true independence stress test must change dependence while preserving marginal assumptions.
Use Structural stress tests → Maturity dependence to compare shared and independent maturity channels. The explanations below describe the shared baseline. In independent-channel mode the displayed reference maturity distribution is the common marginal distribution; no single maturity draw drives the whole plant scenario.
Color gradient: Higher density (yellow) tends to cluster at lower costs
Spread: Even at high maturity, costs vary due to other random factors
This is an assumed dependence structure. Technical success with difficult financing, and the reverse, remain plausible scenarios.
Quick Start
Explore the “Learn” sections at the top to understand how cultured chicken production works and how this model handles uncertainty.
Adjust parameters in the left sidebar. Each parameter has an expandable explanation.
Results update automatically as you change the Basic Parameters sliders — the summary cards, cost distribution, and probability thresholds all recalculate. (The Component Distributions section shows the underlying distributions and updates when you change the relevant sliders.)
Navigate the results to see:
Cost Distribution: Full probability distribution with key percentiles
Probability Thresholds: Chances of hitting key price points
A pivotal question is one where resolving the uncertainty would significantly change optimal resource allocation. For cultured chicken:
If factory-gate costs approach US wholesale chicken (about $3.50/kg of product) by 2036: large-scale displacement of conventional chicken becomes plausible → prioritize support for industry scale-up
If costs remain far above it (say, over $50/kg): focus shifts to alternative interventions (plant-based, policy, etc.)
Both sides of that comparison are at the factory gate: the model’s cost and the wholesale price of boneless chicken (comparison point), not the retail price.
The pivotal question framework helps prioritize research and forecasting efforts where they matter most.
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 versioned September audit compares the current defaults across process scenarios and multiple random seeds. Use the live results above for your selected settings. The largest remaining questions concern coherent process productivity and medium requirements, commercial-success conditioning, and evidence for the joint priors.
Limitations & Caveats
Model status (reviewed July 20, 2026): This model is largely AI-generated and we cannot yet vouch for all parameter values. It is provided to fix ideas, give a sense of the sort of modeling we’re interested in, and present a framework for discussion and comparison — not a definitive cost estimate. We welcome scrutiny and suggestions via Hypothesis annotations or email.
Static snapshot model — Projects costs for a single projection year (adjustable 2026-2050). Does not model year-over-year learning curves; instead, the “maturity” parameter serves as a proxy for cumulative industry development.
Forming and texturization are outside the estimate — Forming can be added to the hybrid product via toggle ($0.5–2.5 per kg of product, GFI 2021; scaffolded whole cuts are not modeled). Harvest, washing and dewatering belong inside the estimate but are not yet modeled explicitly.
No geography — Assumes generic global costs, not region-specific labor, energy, or regulatory factors.
Limited contamination modeling — No batch failure or contamination event distributions.
Sources & How They Inform the Model
Honesty note on sourcing
This model was largely AI-generated. We have grounded key parameter ranges in published TEAs and recently revised the pharma-grade media cost range downward (from $1-4/L to $0.50-2.50/L) based on GFI’s 2025 amino acid supplier data. However, we have not yet systematically verified that every hardcoded range traces to a specific source. Cycle days is the main remaining parameter that still lacks a direct citation; growth factor quantities and the media-use multiplier were both tightened in April 2026 based on GFI 2023 / Humbird / Pasitka primary sources. This is one reason we are seeking expert review.
Model change log — basal micronutrients folded into media (April 2026)
Earlier versions of this model carried a separate food-grade micronutrient cost line (vitamins, minerals, trace elements) with its own adoption toggle, usage range (0.1–10 g/kg), and price range ($0.02–20/g). External review flagged two problems:
Category confusion / double counting. The page already describes basal media as containing “amino acids, glucose, vitamins,” and the model comments described media as “basal media, excluding growth factors.” A separate line for vitamins/minerals was therefore adding a second charge for the same inputs.
Ranges not coherent with the literature.O’Neill et al. (2021) describe vitamin sourcing as a “less pressing issue” and note required minerals can “generally be obtained relatively inexpensively.” Meanwhile Pasitka et al. (2024) report an ACF-medium supplement (vitamins + minerals) at roughly 0.151 kg per kg wet biomass — orders of magnitude above the slider’s 0.1–2 g/kg range. So the old slider was not tracking any coherent category.
What changed: The separate slider and cost line were removed. Basal vitamins, minerals, and trace salts are now included inside media $/L along with amino acids and glucose, consistent with how the media-cost literature is reported. Supplemental recombinant proteins (insulin, transferrin, albumin) remain unmodeled as a standalone line for now; if we reintroduce a separate uncertainty term, it will be repurposed to these proteins where per-g prices and per-kg usage can be sourced.
Sources that directly informed model structure and parameter ranges (click to expand)
Optimistic TEA ($6/lb); also provides an empirical ACF chicken process figure of ~0.00187 g growth factor/kg wet biomass, which is used as a cross-check for the GFI-derived quantity range (April 2026)
Independent evaluation of RP forecasting paper — informed our project framing
Parameters where source grounding is weakest — review priority (click to expand)
Supplemental recombinant proteins (insulin, transferrin, albumin) are now broken out as a separate cost line in separable-media mode. Their ranges remain provisional and need supplier quotes or process-linked dosage evidence. In bundled complete-medium mode they are included in media $/L and are not added again.
Growth factor quantities — tightened April 2026 from a very broad 0.0001–0.02 g/kg stress-test range to a cited 0.0005–0.006 g/kg default, derived from the Humbird medium formulation reproduced in GFI 2023 (FGF 0.1 mg/L + TGFβ 0.002 mg/L) × media-use assumptions of 8–60 L/kg, and cross-checked against Pasitka et al. (2024)’s ~0.00187 g/kg empirical ACF figure. Both dosage and price remain uncertain; the current per-kg dosage draw is not coupled to realized medium use.
Media-use multiplier — renamed and tightened April 2026 from “Media turnover (1–10×)” to “Media-use multiplier (0.5–3.0×)”. The rename flags that the parameter is a bundled ratio (fresh media / nominal reactor volume), not a literal count of reactor-volume changes, so values below 1 (recycling, fed-batch, explicit medium recovery) are now allowed. The new range lets the model represent GFI 2023 cost-competitive scenarios (8–13 L/kg ≈ 0.5–1.2× at 60–90 g/L); the old floor of 1.0 excluded some combinations at fixed density. See Learn: media-use mechanisms for the derivation.
Cycle days (0.5-5.0 days) — hardcoded without source attribution
Plant factor (1.5-3.5×) — standard engineering multiplier range, not CM-specific
See the Technical Documentation for detailed parameter definitions and the expandable details under each slider for parameter-specific discussion.
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-in-header: text: | <script> // Run synchronously in the <head>, BEFORE Hypothes.is loads. // If the URL has a query string (shared link or a legacy write // from a previous session), extract the params into a global, // then strip the query from the URL. This way Hypothes.is sees // the bare canonical URL and can find annotations anchored to // it, while OJS still gets the slider values via the global. (function () { try { if (!window.location.search) return; var usp = new URLSearchParams(window.location.search); window.__CM_URL_STATE__ = {}; usp.forEach(function (v, k) { window.__CM_URL_STATE__[k] = v; }); history.replaceState(null, "", window.location.pathname + window.location.hash); } catch (e) { console.warn("CM URL state extraction failed:", e); } })(); </script> include-after-body: text: | <script src="https://hypothes.is/embed.js" async></script>---```{=html}<div style="margin:6px 0 10px; display:flex; gap:8px; align-items:center; flex-wrap:wrap;"> <a href="#model-parameters" style="display:inline-block; padding:6px 14px; background:#2d4a2d; color:white; border-radius:6px; text-decoration:none; font-size:13px; font-weight:600;">↓ Go to parameters</a> <a href="simple.html" style="display:inline-block; padding:6px 14px; background:#f0f8ff; color:#1a5276; border:1px solid #3498db; border-radius:6px; text-decoration:none; font-size:13px;" title="New to this model? Start with the Simplest Model: a shorter version focusing on key levers, with line-of-sight explanations. You can carry your settings over to this Advanced Model.">Simplest Model →</a></div><!-- The page notes used to be seven stacked folding boxes (David, 29 Oct 2026: too much vertical space). Now one row of toggles; each opens its panel below, one at a time. --><div class="info-strip"> <span class="info-strip-label">About this page:</span> <button type="button" data-panel="info-updates" aria-controls="info-updates" aria-expanded="false">What's new (Sept–Oct)</button> <button type="button" data-panel="info-basis" aria-controls="info-basis" aria-expanded="false" class="info-key">Basis: dry cell mass</button> <button type="button" data-panel="info-status" aria-controls="info-status" aria-expanded="false" class="info-warn">Status & limitations</button> <button type="button" data-panel="info-workshop" aria-controls="info-workshop" aria-expanded="false">Workshop evidence</button> <button type="button" data-panel="info-audio" aria-controls="info-audio" aria-expanded="false">Audio overviews</button> <button type="button" data-panel="info-feedback" aria-controls="info-feedback" aria-expanded="false">Feedback</button> <button type="button" data-panel="info-basics" aria-controls="info-basics" aria-expanded="false">New to cultured meat?</button> <button type="button" data-panel="info-uncertainty" aria-controls="info-uncertainty" aria-expanded="false">Uncertainty & distributions</button></div>```::: {#info-updates .info-panel}**November 7 (engine `2026-11-07.2`): forming cost.** The optional forming switch now adds \$0.5–2.5 per kg of hybrid product, sourced from GFI's 2021 extrusion-plant costs, instead of an unsourced \$2–15 per kg of cells. Default results are unchanged (the switch is off).**November 7 (engine `2026-11-07.1`): physical consistency, provisional. Default results change.** Medium use per kg can no longer fall below what the medium's nutrients can build (a nutrient floor); recovery losses, failed or contaminated runs (more likely in long perfusion and continuous runs) and seed-train capital are now included; and the food-grade reactor cost ratio is 0.15–0.55 of pharma-grade, down from 0.35–0.85. An oxygen-transfer cap on density is available, off by default. The default median rises from \$213 to about \$276/kg dry (\$16.90 to \$21.25 per kg of hybrid product), and the share of draws under \$10/kg hybrid falls from 23.6% to 11.6%. **These changes have not yet been reviewed by engineers or cell-culture experts**; the ranges are placeholders with stated sources. Each can be switched off under *Structural stress tests*. See [physical consistency](docs.qmd#physical-consistency).**October 26 (target and basis, documentation only):** the team set the model's target: unconditional and hypothetical cost of a large plant built with the best technology available in 2036, whether or not such plants exist, in 2026 US dollars ([estimand](limits.qmd#estimand)). It also set a planned standard output basis, washed cell biomass at a fixed solids content plus cost per kg of protein ([output basis](docs.qmd#output-basis)). The "downstream processing" switch is relabelled "forming and texturization" (outside the estimate). No model outputs changed.**October 26 (engine `2026-10-26.2`):** results are also shown per kg at a target solids content and per kg of protein, using placeholder conversions you can change under *Structural stress tests*. A placeholder harvest, washing and dewatering cost is available under *Model Structure*, off by default. Default results are unchanged.**October 26 (engine `2026-10-26.3`):** a third *Fresh-media calculation* option under *Structural stress tests* derives medium per kg from nutrient yield (grams of cell solids per litre of medium), so it no longer falls automatically as density rises. Off by default; default results are unchanged. See the [structure check](sensitivity.qmd#structure-check).**October 29 (engine `2026-10-29.1`): hybrid product for comparisons.** Products will not be made entirely of cultivated cells, so the number shown beside each dry-cell-mass cost, and the probability thresholds, are now for a kg of **hybrid product**: 25% cultivated cells (at chicken-meat moisture) plus 75% plant-based ingredients at \$3/kg. The default median is about \$17/kg of hybrid product (\$213/kg dry). Change the inclusion rate under *Hybrid product* in the sidebar.**October 26 (engine `2026-10-26.4`): default results change.** The headline cost is now per kg of dry cell mass (about five times the old wet figures), thresholds then used a chicken-meat equivalent (all cells; replaced by the hybrid product on October 29), density inputs are g dry/L and \$/kg inputs are per kg dry. Cost basis "wet" under *Structural stress tests* reproduces the earlier results exactly. See *Basis: dry cell mass*.This page runs the **current model** (engine `2026-11-07.2`). See the [September 2026 review and correction log](review-2026-09.qmd). For comparisons with beliefs recorded at the May 8 workshop, use the [hosted workshop-era model](archive/workshop-2026-05-08/) or its [tagged source](https://github.com/unjournal/cm_pq_modeling/tree/workshop-2026-05-08). The archived model is preserved as it was deployed and includes errors corrected here.Two supplied research critiques prompted optional median-preserving priors, explicit growth-factor price ranges, and structural comparisons for media use, GF dosage, and maturity dependence. Baseline numerical assumptions are retained. New controls include provenance tooltips and links to the [discussion responses, justification, and remaining questions](review-response-2026-09.qmd). These additions are AI-implemented scenario tests, pending expert review.**September 20:** the model pages now connect the technical limitations to the workshop's skeptical–optimistic disagreement map and structured response round. This adds no new parameter values or headline forecast. It makes the top-down microbial benchmark, interaction risk, and remaining measurement needs visible, with provenance and caveats in the [response note](review-response-2026-09.qmd#top-down).**October 26:** an opt-in *Process-mode selection* setting in **Structural stress tests** replaces the mode weights with feasibility probabilities and uses the cheapest feasible mode in each simulated world, optionally with decision noise. The default (draw mode by weights) and its results are unchanged. See [least-cost mode selection](docs.qmd#least-cost-mode-selection) for the method and caveats.:::```{=html}<div id="info-audio" class="info-panel"><div style="font-size:12px;color:#888;margin-bottom:8px;">AI-generated · <span title="The two earlier overviews use OpenAI TTS-1-HD from May 2026. The critique and revision briefing uses the local Kokoro neural voice af_heart from September 2026. Each transcript gives its scope and caveats." style="cursor:help;border-bottom:1px dotted #aaa;">provenance</span></div><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:10px;"><div style="padding:10px 12px;background:white;border:1px solid #dde8dd;border-radius:6px;"><div style="font-weight:600;font-size:13px;color:#2d4a2d;margin-bottom:4px;">How Cultured Meat is Made <span style="font-weight:400;color:#888;font-size:11px;">~11 min</span></div><audio controls style="width:100%;height:30px;display:block;margin-bottom:4px;"><source src="https://unjournal.github.io/cm_pq_modeling/process_overview.mp3" type="audio/mpeg"></audio><div style="font-size:11px;color:#777;display:flex;gap:10px;"><a href="https://unjournal.github.io/cm_pq_modeling/process_overview.mp3" download style="color:#5a7a5a;">Download MP3</a><a href="https://unjournal.github.io/cm_pq_modeling/process-overview-script.html" target="_blank" rel="noopener" style="color:#5a7a5a;">View/annotate script</a></div></div><div style="padding:10px 12px;background:white;border:1px solid #dde8dd;border-radius:6px;"><div style="font-weight:600;font-size:13px;color:#2d4a2d;margin-bottom:4px;">The Cost Model Explained <span style="font-weight:400;color:#888;font-size:11px;">~10 min</span></div><audio controls style="width:100%;height:30px;display:block;margin-bottom:4px;"><source src="https://unjournal.github.io/cm_pq_modeling/model_review_report.mp3" type="audio/mpeg"></audio><div style="font-size:11px;color:#777;display:flex;gap:10px;"><a href="https://unjournal.github.io/cm_pq_modeling/model_review_report.mp3" download style="color:#5a7a5a;">Download MP3</a><a href="https://unjournal.github.io/cm_pq_modeling/model-overview-script.html" target="_blank" rel="noopener" style="color:#5a7a5a;">View/annotate script</a></div></div><div style="padding:10px 12px;background:white;border:1px solid #c6d8c6;border-radius:6px;"><div style="font-weight:600;font-size:13px;color:#2d4a2d;margin-bottom:4px;">Critique, revisions, and open questions <span style="font-weight:400;color:#888;font-size:11px;">~32 min</span></div><audio controls preload="metadata" style="width:100%;height:30px;display:block;margin-bottom:4px;"><source src="cm-model-critique-revision-briefing-2026-09-17-ai-narration.mp3" type="audio/mpeg"></audio><div style="font-size:11px;color:#777;display:flex;gap:10px;flex-wrap:wrap;"><a href="cm-model-critique-revision-briefing-2026-09-17-ai-narration.mp3" download style="color:#5a7a5a;">Download MP3</a><a href="model-critique-revision-briefing-2026-09-17.html" target="_blank" rel="noopener" style="color:#5a7a5a;">Transcript and provenance</a></div></div></div></div>```::: {#info-status .info-panel}This model is *largely AI-generated* and, as of September 2026, has not been independently validated. It is provided to fix ideas, make assumptions inspectable, and support discussion and comparison. *Do not treat the outputs as authoritative cost estimates.***Several limitations are decision-relevant, not cosmetic:** the single latent-maturity factor is ad hoc; source dollars are not yet normalized to one real-dollar year; source data on different weight bases are converted with a single assumed solids content (see *Basis: dry cell mass*); and the parameter ranges have not yet been checked against the target quantity decided in October 2026, the cost of a large plant built with the best technology available in 2036, whether or not such plants exist ([estimand](limits.qmd#estimand)). The sensitivity chart is a dollar-swing ranking, not a variance decomposition. We are addressing these incrementally.**[Read the full critique and our responses → Limits/Critique](limits.html)**We especially welcome expert review of: growth factor quantities and prices, bioreactor CAPEX ranges, and the maturity correlation structure — see below and the [Sources section](#sources-how-they-inform-the-model).:::::: {#info-basis .info-panel}**Since October 26, 2026 (engine `2026-10-26.4`), the headline cost is per kg of dry cell mass**, not per kg of wet biomass. Dry-mass figures are about five times the old wet figures (the default median is about \$213/kg dry, against \$43 per wet kg before), because a kg of harvested cells is only about 20% solids. Nothing about the underlying scenario changed.**Why.** Harvested cells are mostly water, and washing and dewatering change what a "wet kg" contains: a paste at 15% solids carries half the protein of one at 30%. Dry mass does not change with washing or dewatering, maps to nutrition, and is the natural basis for nutrient stoichiometry (how much medium it takes to build a gram of cells).**What else you'll see.** Beside every dry-basis cost, the cost per kg of a **hybrid product**: 25% cultivated cells, counted at the water content of chicken meat (27.5% solids, a convention agreed on 28 October 2026), and 75% plant-based ingredients at \$3/kg. Products will not be made entirely of cells, and there is no consensus inclusion rate, so 25% is a convention you can change in the sidebar. The probability thresholds (\$5, \$10, \$25, \$50) and chicken comparisons use the hybrid product. Cost per kg of **protein** is reported too. Cell density is entered in **g dry cells per litre**, and \$/kg inputs are per kg dry. **The old wet-basis output is one switch away** (*Structural stress tests → Cost basis*) and reproduces the earlier numbers exactly. Shared links made before this change that set density or \$/kg priors will now be read as dry-basis values. [Why the basis matters, and how sources report it →](docs.qmd#output-basis)*AI-drafted (Claude, 26 October 2026) from David Reinstein's design decision; not yet reviewed by a person.*:::::: {#info-workshop .info-panel}**From workshop evidence to model revision.** The May 8, 2026 workshop is complete. Its outputs now form a linked evidence-to-model workflow: the summary records the technical arguments, the beliefs analysis shows the forecast spread, the demand bridge connects factory-gate cost to an adoption model, and the proposed modeling hack turns the remaining disagreements into testable model changes.The follow-up now separates disagreements about definitions, factual estimates, evidence, source credibility, interpretation, model structure, and the importance of a claim to the final result. This matters because agreement that one input can become cheaper does not establish that the total system becomes cheap. The [workshop synthesis](https://uj-cm-workshop.netlify.app/synthesis.html#sources-of-disagreement) explains the distinction, and the [crux response form](https://uj-cm-workshop.netlify.app/crux-map.html#respond) asks skeptics and optimists to diagnose it directly.**[Workshop summary →](https://uj-cm-workshop.netlify.app/summary.html)** · **[Where disagreement sits →](https://uj-cm-workshop.netlify.app/synthesis.html#sources-of-disagreement)** · **[Respond to the crux map →](https://uj-cm-workshop.netlify.app/crux-map.html#respond)** · **[Cost → demand bridge →](demand.qmd)** · **[Sensitivity methods →](sensitivity.qmd)**The named beliefs-analysis dashboard remains an internal review artifact and is intentionally not linked from this public site; a redacted public synthesis should be produced separately.:::::: {#info-feedback .info-panel}<!-- GitHub Discussions call-to-action hidden for now (David, 28 Oct 2026: too much friction for most people). Restore from git history when wanted. -->**Comment on specific text or a parameter** with [Hypothesis](https://hypothes.is/) (click the `<` tab on the right edge). For longer comments, email [contact@unjournal.org](mailto:contact@unjournal.org) or [open a GitHub issue](https://github.com/unjournal/cm_pq_modeling/issues/new).**[Listen: Technical Review (22 min MP3)](model_review_report.mp3)** — Audio walkthrough of model architecture and areas for review:::```{=html}<script>// Page-note toggles: one panel open at a time; clicking the open one closes it.document.querySelectorAll('.info-strip button[data-panel]').forEach(btn => { btn.addEventListener('click', () => { const wasOpen = btn.getAttribute('aria-expanded') === 'true'; document.querySelectorAll('.info-strip button[data-panel]').forEach(b => { b.setAttribute('aria-expanded', 'false'); document.getElementById(b.dataset.panel)?.classList.remove('open'); }); if (!wasOpen) { btn.setAttribute('aria-expanded', 'true'); document.getElementById(btn.dataset.panel)?.classList.add('open'); } });});</script>``````{=html}<script>function toggleHypothesis() { const btn = document.getElementById('toggle-hypothesis') || document.getElementById('vc-hyp'); // Track state via body class (avoids checking Hypothesis's internal display/transform state) const hidden = document.body.classList.contains('hyp-force-hidden'); if (hidden) { document.body.classList.remove('hyp-force-hidden'); if (btn) btn.textContent = '◉ Annotations'; } else { document.body.classList.add('hyp-force-hidden'); if (btn) btn.textContent = '▷ Annotations'; }}function toggleFullWidth() { const body = document.body; const btn = document.getElementById('toggle-fullwidth') || document.getElementById('vc-wide'); // Quarto full-page layout uses column classes and sidebar const contentSelectors = [ 'main.content', 'main', '.page-columns', '#quarto-content', '.column-page', '.column-body', '.column-body-outset', '.panel-fill', '.panel-sidebar', '#quarto-sidebar', '.page-layout-full .content' ]; if (body.classList.contains('fullwidth-mode')) { body.classList.remove('fullwidth-mode'); // Remove injected style const injected = document.getElementById('fullwidth-style'); if (injected) injected.remove(); btn.textContent = 'Expand Content'; } else { body.classList.add('fullwidth-mode'); // Inject a style tag to override Quarto's layout constraints if (!document.getElementById('fullwidth-style')) { const style = document.createElement('style'); style.id = 'fullwidth-style'; style.textContent = ` body.fullwidth-mode #quarto-content, body.fullwidth-mode main.content, body.fullwidth-mode main, body.fullwidth-mode .page-columns, body.fullwidth-mode .column-body, body.fullwidth-mode .column-page { max-width: 100% !important; width: 100% !important; padding-left: 1rem !important; padding-right: 1rem !important; } body.fullwidth-mode #quarto-sidebar, body.fullwidth-mode #quarto-margin-sidebar, body.fullwidth-mode .sidebar { display: none !important; } body.fullwidth-mode .page-columns { grid-template-columns: 1fr !important; } `; document.head.appendChild(style); } btn.textContent = 'Normal Width'; // Also hide hypothesis when going fullwidth hideHypothesis(); }}function toggleParams() { const btn = document.getElementById('toggle-params'); const vcBtn = document.getElementById('vc-params'); const sidebar = document.querySelector('.panel-sidebar'); const fill = document.querySelector('.panel-fill'); if (!sidebar) return; const isHidden = window.getComputedStyle(sidebar).display === 'none'; if (isHidden) { sidebar.classList.remove('params-hidden'); sidebar.style.removeProperty('display'); if (fill) { fill.style.removeProperty('grid-column'); fill.style.removeProperty('width'); } if (btn) btn.textContent = '◀ Hide Parameters (expand charts)'; if (vcBtn) { vcBtn.textContent = '◀ Parameters'; vcBtn.style.background = '#e8f4e8'; vcBtn.style.borderColor = '#5a7a5a'; } } else { sidebar.classList.add('params-hidden'); if (fill) fill.style.gridColumn = '1 / -1'; if (btn) btn.textContent = '▶ Show Parameters'; if (vcBtn) { vcBtn.textContent = '▶ Parameters'; vcBtn.style.background = '#f8f9fa'; vcBtn.style.borderColor = '#ccc'; } }}// Wide sidebar: turn the parameters panel into a fixed-position drawer overlay.// Using position:fixed avoids all CSS grid conflicts — the sidebar lifts out of// the document flow and floats on top. A dimmed backdrop lets users click outside// to close. The OJS cells inside still work because the DOM nodes are the same.function toggleWideSidebar() { const body = document.body; const sidebar = document.querySelector('.panel-sidebar'); const backdrop = document.getElementById('params-backdrop'); const btnV = document.getElementById('vc-wide-params'); const btnI = document.getElementById('toggle-wide-params'); if (body.classList.contains('sidebar-wide')) { // Close drawer body.classList.remove('sidebar-wide'); const s = document.getElementById('sidebar-wide-style'); if (s) s.remove(); if (backdrop) backdrop.style.display = 'none'; if (btnV) { btnV.textContent = '⟺ Wide params'; btnV.style.background = '#f8f9fa'; btnV.style.borderColor = '#ccc'; } if (btnI) btnI.textContent = '⟺ Wide view'; } else { // Open drawer: make sidebar a fixed overlay panel body.classList.add('sidebar-wide'); if (!document.getElementById('sidebar-wide-style')) { const s = document.createElement('style'); s.id = 'sidebar-wide-style'; s.textContent = ` body.sidebar-wide .panel-sidebar { position: fixed !important; top: var(--quarto-navbar-height, 62px) !important; left: 0 !important; width: min(62vw, 820px) !important; max-width: 92vw !important; height: calc(100vh - var(--quarto-navbar-height, 62px)) !important; max-height: calc(100vh - var(--quarto-navbar-height, 62px)) !important; overflow-y: auto !important; overflow-x: hidden !important; z-index: 6000 !important; background: white !important; box-shadow: 8px 0 32px rgba(0,0,0,0.22) !important; padding: 0.5rem 2rem 5rem 1.5rem !important; border-right: 3px solid #3498db !important; scrollbar-width: thin !important; } `; document.head.appendChild(s); } if (backdrop) backdrop.style.display = 'block'; if (btnV) { btnV.textContent = '↘ Narrow params'; btnV.style.background = '#e8f0fa'; btnV.style.borderColor = '#3498db'; } if (btnI) btnI.textContent = '↘ Narrow view'; // Scroll drawer to top when opening if (sidebar) sidebar.scrollTop = 0; }}// Re-initialise Tippy tooltips for abbr[title] and span[title] elements added by OJS.// OJS renders after Quarto's DOMContentLoaded Tippy pass, so elements inside OJS// html`` templates are missed on first pass. Span elements (used for ⓘ inline notes// in the main content area) are also included — native browser title tooltips clip// long text and are unreliable across browsers.function initOjsTooltips() { if (typeof tippy === 'undefined') return; document.querySelectorAll( '.panel-sidebar abbr[title], .panel-fill abbr[title], .panel-sidebar span[title], .panel-fill span[title]' ).forEach(function(el) { const t = el.getAttribute('title'); if (!el._tippy && t) { el.removeAttribute('title'); // prevent duplicate native browser tooltip tippy(el, { content: t, placement: 'bottom', maxWidth: 500, allowHTML: false }); } });}setTimeout(initOjsTooltips, 2500); // first pass after OJS settles// Re-run whenever sidebar OR main content changes (OJS re-renders on slider change)document.addEventListener('DOMContentLoaded', function() { var obs = new MutationObserver(function() { initOjsTooltips(); }); var sidebar = document.querySelector('.panel-sidebar'); var fill = document.querySelector('.panel-fill'); if (sidebar) obs.observe(sidebar, { childList: true, subtree: true }); if (fill) obs.observe(fill, { childList: true, subtree: true });});// Reset all parameters: clear URL state and reload (most reliable approach,// since OJS viewof inputs can't be reset reliably from plain JS)function resetAllDefaults() { window.location.href = window.location.pathname + window.location.hash;}var _expandAllActive = false;var _expandAllObserver = null;function collapseAllDetails() { _expandAllActive = false; if (_expandAllObserver) { _expandAllObserver.disconnect(); _expandAllObserver = null; } document.querySelectorAll('details[open]').forEach(function(d) { d.removeAttribute('open'); });}function expandAllDetails() { _expandAllActive = true; document.querySelectorAll('details').forEach(function(d) { d.setAttribute('open', ''); }); // OJS re-renders reactive cells after every slider change, replacing <details> elements // with fresh closed ones. The MutationObserver re-applies 'open' to any new ones. if (!_expandAllObserver) { _expandAllObserver = new MutationObserver(function(mutations) { if (!_expandAllActive) return; mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType !== 1) return; if (node.tagName === 'DETAILS') node.setAttribute('open', ''); if (node.querySelectorAll) node.querySelectorAll('details').forEach(function(d) { d.setAttribute('open', ''); }); }); }); }); _expandAllObserver.observe(document.body, { childList: true, subtree: true }); }}function toggleViewControls() { const content = document.getElementById('view-controls-content'); const icon = document.getElementById('vc-minimize'); if (!content) return; const isHidden = content.style.display === 'none'; content.style.display = isHidden ? 'flex' : 'none'; if (icon) icon.textContent = isHidden ? '−' : '≡';}function toggleToc() { const btn = document.getElementById('toggle-toc') || document.getElementById('vc-toc'); // Quarto TOC can be in several locations const selectors = [ '#quarto-margin-sidebar', '#TOC', '.sidebar.toc-left', 'nav.toc', '#quarto-sidebar' ]; let toc = null; for (const sel of selectors) { toc = document.querySelector(sel); if (toc) break; } if (!toc) { btn.textContent = 'TOC not found'; return; } if (toc.style.display === 'none') { toc.style.display = ''; btn.textContent = 'Hide Table of Contents'; } else { toc.style.display = 'none'; btn.textContent = 'Show Table of Contents'; }}function hideHypothesis() { document.body.classList.add('hyp-force-hidden'); const btn = document.getElementById('toggle-hypothesis') || document.getElementById('vc-hyp'); if (btn) btn.textContent = '▷ Annotations';}</script><!-- Backdrop for wide-sidebar drawer: click to close --><div id="params-backdrop" onclick="toggleWideSidebar()" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.35); z-index:5999; cursor:pointer;"></div><!-- Fixed floating view controls — lower RIGHT, elevated 90px above Hypothes.is badge. Collapsible: click the VIEW header row to minimize to a slim bar. --><div id="view-controls" style="position: fixed; bottom: 90px; right: 20px; z-index: 10001; background: white; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.15); font-size: 12px; min-width: 145px; overflow: hidden;"> <div onclick="toggleViewControls()" title="Click to expand/collapse" style="display: flex; align-items: center; padding: 6px 9px; cursor: pointer; border-bottom: 1px solid #eee; user-select: none; background: #fafafa;"> <span style="font-size: 11px; color: #888; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; flex: 1;">View</span> <span id="vc-minimize" style="font-size: 15px; color: #aaa; line-height: 1;">≡</span> </div> <div id="view-controls-content" style="display: none; flex-direction: column; gap: 5px; padding: 8px 9px;"> <button onclick="toggleParams()" id="vc-params" style="padding: 5px 8px; font-size: 12px; cursor: pointer; border: 1px solid #5a7a5a; border-radius: 4px; background: #e8f4e8; color: #2d4a2d; text-align: left;">◀ Parameters</button> <button onclick="toggleWideSidebar()" id="vc-wide-params" style="padding: 5px 8px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f8f9fa; text-align: left;">⟺ Wide params</button> <button onclick="toggleToc()" id="vc-toc" style="padding: 5px 8px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f8f9fa; text-align: left;">▷ Contents</button> <button onclick="toggleHypothesis()" id="vc-hyp" style="padding: 5px 8px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f8f9fa; text-align: left;">▷ Annotations</button> <button onclick="toggleFullWidth()" id="vc-wide" style="padding: 5px 8px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f8f9fa; text-align: left;">↔ Expand</button> <hr style="margin: 3px 0; border-color: #eee;"> <button onclick="collapseAllDetails()" style="padding: 5px 8px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f8f9fa; text-align: left;">▲ Collapse all</button> <button onclick="expandAllDetails()" style="padding: 5px 8px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f8f9fa; text-align: left;">▼ Expand all</button> </div></div><style>/* ── Fullwidth mode: hide parameter sidebar, let charts fill the page ── */.fullwidth-mode .page-columns { grid-template-columns: 1fr !important; max-width: 100% !important; width: 100% !important;}.fullwidth-mode #quarto-content,.fullwidth-mode main.content,.fullwidth-mode main,.fullwidth-mode .column-page,.fullwidth-mode .column-body,.fullwidth-mode .panel-fill { max-width: 100% !important; width: 100% !important; padding-left: 1rem !important; padding-right: 1rem !important;}/* Hide parameter sidebar and TOC in fullwidth mode */.fullwidth-mode .panel-sidebar,.fullwidth-mode #quarto-sidebar,.fullwidth-mode #quarto-margin-sidebar,.fullwidth-mode .sidebar { display: none !important;}/* Hide Hypothesis in fullwidth mode, and when manually toggled off */.fullwidth-mode .annotator-frame,.fullwidth-mode .hypothesis-sidebar,.fullwidth-mode hypothesis-sidebar,.hyp-force-hidden .annotator-frame,.hyp-force-hidden .hypothesis-sidebar,.hyp-force-hidden hypothesis-sidebar,.hyp-force-hidden iframe[src*="hypothes.is"] { display: none !important; visibility: hidden !important; pointer-events: none !important;}/* Sidebar overflow fix — offset by Quarto navbar so top buttons aren't hidden */.panel-sidebar { overflow-y: auto !important; overflow-x: hidden !important; position: sticky !important; top: var(--quarto-navbar-height, 62px) !important; max-height: calc(100vh - var(--quarto-navbar-height, 62px)) !important;}.panel-sidebar details[open] { max-width: 100%; overflow-wrap: break-word;}/* When params sidebar is hidden, let charts take full width */.params-hidden { display: none !important;}</style>```::: {#info-basics .info-panel}**[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**. For a side-by-side comparison of how published TEAs differ in their assumptions and estimates, see our **[TEA Comparison page](compare.qmd)**. This model stops at a factory-gate **cost**; for what a given cost implies for consumer **adoption and market share**, see our **[Cost → Demand bridge](demand.qmd)**, which connects to Pablo AMC's demand-side model.``` CELL BANK → SEED TRAIN → PRODUCTION → HARVEST → PRODUCT [O] [OOO] [OOOOOOO] [===] [≡≡≡]```:::::: {#info-uncertainty .info-panel}This model uses **Monte Carlo simulation**: we sample thousands of possible futures and see what distribution of costs emerges.**Distribution Types Used:**```{=html}<table style="width: 100%; table-layout: fixed; font-size: 14px; border-collapse: collapse;"><colgroup> <col style="width: 18%;"> <col style="width: 12%;"> <col style="width: 30%;"> <col style="width: 40%;"></colgroup><thead><tr style="border-bottom: 2px solid #ddd; text-align: left;"> <th style="padding: 8px;">Parameter Type</th> <th style="padding: 8px;">Distribution</th> <th style="padding: 8px;">Why</th> <th style="padding: 8px;">Examples in model</th></tr></thead><tbody><tr style="border-bottom: 1px solid #eee; vertical-align: top;"> <td style="padding: 8px;">Costs, intensities</td> <td style="padding: 8px;"><strong><a href="https://en.wikipedia.org/wiki/Log-normal_distribution" target="_blank" rel="noopener">Lognormal</a></strong></td> <td style="padding: 8px;">Always positive, right-skewed (some very high values possible)</td> <td style="padding: 8px;">Media cost per liter, GF price per gram, cell density</td></tr><tr style="border-bottom: 1px solid #eee; vertical-align: top;"> <td style="padding: 8px;">Probabilities, fractions</td> <td style="padding: 8px;"><strong><a href="https://en.wikipedia.org/wiki/Beta_distribution" target="_blank" rel="noopener">Beta</a></strong></td> <td style="padding: 8px;">Bounded between 0 and 1, flexible shape</td> <td style="padding: 8px;">Maturity factor, adoption probabilities, utilization rate (0–1 share). For switching parameters (e.g., hydrolysate adoption), the model first samples an adoption <em>probability</em> from a Beta distribution, then uses a Bernoulli draw to determine whether each run uses the "adopted" or "non-adopted" cost regime.</td></tr><tr style="vertical-align: top;"> <td style="padding: 8px;">Bounded ranges</td> <td style="padding: 8px;"><strong><a href="https://en.wikipedia.org/wiki/Continuous_uniform_distribution" target="_blank" rel="noopener">Uniform</a></strong></td> <td style="padding: 8px;">Equal probability within bounds; used when we have only a plausible range and no reason to favor values within it</td> <td style="padding: 8px;">Asset life (years), CAPEX scale exponent, fixed OPEX scaling factor</td></tr></tbody></table>```**Background reading.** Each distribution name above links to its Wikipedia article. For a visual, non-technical introduction to distributions, see [Seeing Theory: Probability Distributions](https://seeing-theory.brown.edu/probability-distributions/index.html) (Brown University). The switching parameters also use a [Bernoulli](https://en.wikipedia.org/wiki/Bernoulli_distribution) (yes/no) draw. How the model builds a lognormal from the p5/p95 (or p10/p90) values you enter is explained in [Model formulas: how the distribution is constructed](docs.html#how-the-distribution-is-constructed).We are open to considering alternative distributional forms (e.g., triangular, PERT, or mixture distributions) and to making the modeling flexible enough for users to select or compare different distributional assumptions. Suggestions are welcome via Hypothesis annotations.**Reading the Results:**- **p5 (5th percentile)**: "Optimistic" - only 5% of simulations are cheaper- **p50 (median)**: Middle outcome - half above, half below- **p95 (95th percentile)**: "Pessimistic" - 95% of simulations cheaper**The "Maturity" Correlation:**A key feature is the **latent maturity factor** that links:- Technology adoption rates- Reactor costs- Financing costs (WACC)In "good worlds" for cultured chicken (high maturity), multiple things improve together. This makes favorable assumptions co-occur more often. Technical success with difficult financing remains possible and should not be ruled out.This approach draws on the concept of *copula-based correlation* in Monte Carlo simulation, where a shared latent variable induces dependence among otherwise independent marginal distributions. <abbr title="Vose (2008), Risk Analysis: A Quantitative Guide (Wiley) — textbook treatment of correlated Monte Carlo sampling; Morgan & Henrion (1990), Uncertainty (Cambridge UP) — foundational discussion of dependent uncertainties.">See methodological references.</abbr><details><summary>More detail on the maturity factor implementation</summary>The maturity factor is sampled once per simulation from a Beta distribution (mean set by the "Industry Maturity" slider, standard deviation 0.20). It then *shifts* the adoption probabilities (hydrolysates, food-grade, recombinant GFs) and *adjusts* WACC downward in high-maturity draws. This creates positive correlation among "good news" variables without imposing a rigid structure. The strength of the correlation is moderate by design — maturity explains some but not all of the variation in each parameter.*Caveat:* This correlation structure is a modelling convenience, not an empirically calibrated design. The choice of a single latent variable, the specific sensitivity coefficients, and the functional form are all ad hoc. Results — especially in tail scenarios — may be sensitive to these choices. We encourage users to stress-test by varying the maturity slider widely and treat correlated-scenario outputs as illustrative rather than calibrated.</details>:::---## Interactive Model```{ojs}//| echo: false// ============================================================// SEEDED RANDOM NUMBER GENERATOR// ============================================================// Simple mulberry32 PRNG (fast, good quality for Monte Carlo)costModel = import(new URL("./cost-model.mjs", window.location.href).href)simulate = costModel.simulatequantile = costModel.quantilemean = costModel.meanconditionalSwing = costModel.conditionalSwingspearmanCorr = costModel.spearmanCorr``````{ojs}//| echo: false// URL state helpers: slider defaults pick up values from a synchronous// <head> script that extracts ?key=val on page load, stashes them in// window.__CM_URL_STATE__, and strips the query string BEFORE Hypothes.is// loads. See the include-in-header block at the top of this file.// Reading from the global (not location.search) is critical — by the time// OJS runs, the query string has already been stripped so that Hypothes.is// can find annotations anchored to the bare canonical URL.urlParams = window.__CM_URL_STATE__ || {}``````{ojs}//| echo: falseurlNum = function(key, def) { const v = urlParams[key]; if (v === undefined) return def; const n = Number(v); return Number.isFinite(n) ? n : def;}``````{ojs}//| echo: falseurlBool = function(key, def) { const v = urlParams[key]; if (v === undefined) return def; return v === "1" || v === "true";}```### Model Parameters::: {.panel-sidebar}```{=html}<div style="display: flex; gap: 5px; margin-bottom: 0.75rem; position: sticky; top: 0; background: white; padding: 0.5rem 0 0.5rem; border-bottom: 1px solid #eee; z-index: 5; margin-top: -0.5rem; flex-wrap: wrap;"> <button id="toggle-params" onclick="toggleParams()" style="flex: 1; min-width: 90px; padding: 0.5rem; font-size: 0.82rem; cursor: pointer; border: 1px solid #5a7a5a; border-radius: 6px; background: #e8f4e8; color: #2d4a2d; font-weight: 500;"> ◀ Hide </button> <button id="toggle-wide-params" onclick="toggleWideSidebar()" title="Expand sidebar for detailed reading" style="padding: 0.5rem 0.5rem; font-size: 0.82rem; cursor: pointer; border: 1px solid #3498db; border-radius: 6px; background: #f0f8ff; color: #1a5276; font-weight: 500; white-space: nowrap;"> ⟺ Wide </button> <button id="reset-all-btn" onclick="resetAllDefaults()" title="Reset every slider and toggle back to its default value" style="padding: 0.5rem 0.5rem; font-size: 0.82rem; cursor: pointer; border: 1px solid #c0392b; border-radius: 6px; background: #fef9f9; color: #922b21; font-weight: 500; white-space: nowrap;"> ↺ Reset all </button> <button onclick="expandAllDetails()" title="Expand all parameter detail sections" style="padding: 0.5rem 0.4rem; font-size: 0.82rem; cursor: pointer; border: 1px solid #aaa; border-radius: 6px; background: #f9f9f9; color: #555; white-space: nowrap;">▼ All</button> <button onclick="collapseAllDetails()" title="Collapse all parameter detail sections" style="padding: 0.5rem 0.4rem; font-size: 0.82rem; cursor: pointer; border: 1px solid #aaa; border-radius: 6px; background: #f9f9f9; color: #555; white-space: nowrap;">▲ All</button></div>``````{ojs}//| echo: falseviewof simpleMode = Inputs.toggle({label: "Simplified view (recommended)", value: urlBool("simpleMode", true)})``````{ojs}//| echo: falsehtml`<div id="simplified-note" style="display: ${simpleMode ? 'block' : 'none'}; background: #f0f4e8; border-left: 4px solid #5a7a5a; padding: 0.7rem 0.9rem; margin-bottom: 1rem; font-size: 0.82em; line-height: 1.5; border-radius: 0 4px 4px 0;"><strong>Simplified view:</strong> Less pivotal parameters (plant capacity, uptime, financing costs, media-use multiplier) are set to reasonable defaults.These inputs are fixed for a shorter interface, not because their importance is guaranteed to be small. Inspect the background assumptions and use Advanced to vary them. The association chart ranks conditional-mean differences under the selected scenario; it does not measure shares of variance.<em>Switch off to adjust all parameters.</em></div>```````{ojs}//| echo: false// Reactive style block to hide/show full-mode-only and cdmo-only inputshtml`<style> .full-mode-only { display: ${simpleMode ? 'none' : 'block'}; } .cdmo-only { display: ${cdmo_mode ? 'block' : 'none'}; } .override-mode-only { display: ${override_mode_constraints ? 'block' : 'none'}; } .separable-only { display: ${bundled_media ? 'none' : 'block'}; } .bundled-only { display: ${bundled_media ? 'block' : 'none'}; }</style>````**Hybrid product**```{ojs}//| echo: falsehtml`<p style="font-size: 0.85em; color: #555; margin: 0 0 0.4rem;">Products will not be made entirely of cultivated cells. The comparison cost beside each dry-cell-mass figure, and the probability thresholds, are for a kg of hybrid product with this share of cells (counted at chicken-meat moisture); the rest is plant-based ingredients at the filler cost. There is no consensus inclusion rate; 25% is a convention.</p>`viewof blending_share = Inputs.range([0.05, 1.0], { value: urlNum("blending_share", 0.25), step: 0.05, label: "Cell inclusion rate (share of product)" })viewof filler_cost = Inputs.range([0, 10], { value: urlNum("filler_cost", 3), step: 0.5, label: "Plant-based filler cost ($/kg)" })``````{=html}<div class="full-mode-only">```**Model Structure**```{ojs}//| echo: falseviewof include_capex = Inputs.toggle({label: "Include capital costs (CAPEX)", value: urlBool("include_capex", true)})viewof include_fixed_opex = Inputs.toggle({label: "Include plant overhead OPEX", value: urlBool("include_fixed_opex", true)})viewof include_downstream = Inputs.toggle({label: "Include forming in the hybrid product ($0.5–2.5 per kg of product)", value: urlBool("include_downstream", false)})viewof include_harvest_dewater = Inputs.toggle({label: "Include harvest, washing and dewatering (placeholder)", value: urlBool("include_harvest_dewater", false)})```<details><summary>What are these options? Why would you toggle them?</summary>- *CAPEX*: Bioreactor and facility capital costs, annualized. On by default because capital costs are a substantial portion of total production cost. You might toggle this off to isolate variable operating costs or to compare with analyses that report only VOC.- *Plant overhead OPEX*: Labor, maintenance, plant overhead. *(Conventionally called "Fixed OPEX" in TEA literature, but it's only fixed per plant — the total scales sub-linearly with plant size, so per-kg overhead falls as plants get larger.)* On by default for the same reason as CAPEX. Toggle off to focus on marginal costs only.- *Forming and texturization*: Scaffolding, texturization, forming, blending and packaging for structured products. Off by default because these steps are **outside** the estimate, which is unstructured cell biomass. Toggle on to add the cost of blending and shaping the hybrid product: \$0.5–2.5 per kg of product (5th–95th percentile), from GFI's 2021 bottom-up costs for plant-protein extrusion plants. It is added to the hybrid-product cost only, not to the cell-mass cost. Scaffolded whole cuts (steaks, breast) would cost more and are not modeled. (Before November 7, 2026 this switch added an unsourced \$2–15 per kg of cells; its URL parameter is still `include_downstream`.) Harvest, washing and dewatering to the standard solids basis are a different step: they belong **inside** the estimate but are not yet modeled explicitly. See [downstream processing](docs.qmd#downstream-scope).- *Harvest, washing and dewatering (placeholder)*: adds a placeholder $0.50–3.00/kg (5th–95th percentile) for bringing harvested cells to the standard solids basis. Off by default so published results do not change; the range is not sourced and will be replaced with values from a process model.Most published TEAs report costs with CAPEX and plant overhead OPEX included. Toggling them off produces numbers that are not comparable to standard literature estimates.</details>---**Media Accounting**```{ojs}//| echo: falseviewof bundled_media = Inputs.toggle({ label: html`Bundled media pricing (TEA-comparable) <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="When ON: basal media + growth factors + supplemental proteins are replaced by a single 'complete media' $/L figure, matching the convention used in research-grade published TEAs (Humbird 2021 etc.). Default OFF = separable accounting, which is better for reasoning about future cost reduction.">(?)</abbr>`, value: urlBool("bundled_media", false)})``````{=html}<div class="bundled-only">``````{ojs}//| echo: falsehtml`<div style="padding: 0.5rem 0.8rem; background: #f0f4fa; border-left: 3px solid #3498db; font-size: 0.84em; margin: 0.25rem 0 0.4rem 0; line-height: 1.5;"><strong>Bundled mode:</strong> Complete media $/L includes basal nutrients, growth factors, and supplemental proteins. Separate protein costs are zero; a separate GF override is ignored.Range ($50–$500/L default) reflects current research-grade complete media with pharma GFs.For 2036 projection, consider lowering the range. <a href="docs.html#bundled-media-pricing" target="_blank" style="white-space:nowrap;">Details ↗</a></div>```````{ojs}//| echo: falseviewof bundled_media_p5 = Inputs.range([5, 200], { value: urlNum("bundled_media_p5", 50), step: 5, label: "Complete media p5 ($/L)"})viewof bundled_media_p95 = Inputs.range([50, 1000], { value: urlNum("bundled_media_p95", 500), step: 10, label: "Complete media p95 ($/L)"})``````{=html}</div>``````{=html}</div>```---**Production Model**```{ojs}//| echo: falseviewof cdmo_mode = Inputs.toggle({ label: html`CDMO / Contract Manufacturing mode <abbr style="cursor:help; text-decoration:underline dotted; font-size:0.85em; color:#888;" title="A CDMO (Contract Development & Manufacturing Organization) owns the bioreactors and facility and charges a per-kg toll, replacing your CAPEX and plant overhead. Your VOC costs (media, growth factors) remain direct.">(?)</abbr>`, value: urlBool("cdmo_mode", false)})``````{=html}<div class="cdmo-only">``````{ojs}//| echo: falsehtml`<div style="padding: 0.5rem 0.8rem; background: #fef9e7; border-left: 3px solid #f39c12; font-size: 0.84em; margin: 0.25rem 0 0.5rem 0; line-height: 1.5;">CAPEX and Plant Overhead OPEX replaced by a lognormal <em>CDMO toll fee</em>.VOC (media, GFs, other variable) and downstream remain direct costs.A side-by-side comparison appears in the results below. <a href="docs.html#cdmo-production-model" target="_blank" style="white-space:nowrap;">Full explanation ↗</a></div>```````{ojs}//| echo: falseviewof cdmo_toll_p5 = Inputs.range([5, 100], { value: urlNum("cdmo_toll_p5", 20), step: 5, label: "CDMO Toll p5 ($/kg dry cell mass)"})viewof cdmo_toll_p95 = Inputs.range([50, 500], { value: urlNum("cdmo_toll_p95", 200), step: 10, label: "CDMO Toll p95 ($/kg dry cell mass)"})``````{=html}</div>```---**Key Parameters**```{=html}<div class="full-mode-only">```These sliders set the **center** of each parameter's distribution used in the Monte Carlo simulation. They are not simple point estimates -- the simulation samples around these values. For example, setting Plant Capacity to 20 kTA means the simulation draws plant sizes from a lognormal distribution centered near 20 kTA (specifically, p5=10 and p95=40). All charts and results below update dynamically as you adjust these sliders.```{ojs}//| echo: falseviewof plant_capacity = Inputs.range([5, 100], { value: urlNum("plant_capacity", 20), step: 5, label: "Plant Capacity (kTA/yr)"})```<details><summary>What is this? Where do these ranges come from? (click to expand)</summary>**Annual production capacity** in thousand metric tons (kTA). Current pilots: <1 kTA. Commercial target: 10-50 kTA.**Slider range (5-100 kTA):** The lower bound (5 kTA) represents a modest first-generation commercial facility. The upper bound (100 kTA) represents an optimistic large-scale facility. The default (20 kTA) matches the reference scale in [Risner et al. (2021)](https://www.mdpi.com/2304-8158/10/1/3) and is a commonly used benchmark in TEA literature.**What plant size affects:**| Cost Component | Effect of Larger Plant ||----------------|----------------------|| CAPEX per kg | ↓ Scales sub-linearly (power law ~0.6-0.9) || Plant overhead 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: falseviewof uptime = Inputs.range([0.6, 0.99], { value: urlNum("uptime", 0.90), step: 0.01, label: "Utilization Rate"})```<details><summary>What is this?</summary>Fraction of capacity actually used. Accounts for downtime, cleaning, maintenance. 90% is optimistic for new industry.</details>```{=html}</div>``````{ojs}//| echo: falseviewof maturity = Inputs.range([0.1, 0.9], { value: urlNum("maturity", 0.5), step: 0.05, label: "Industry Maturity by 2036"})```<details><summary>What is this?</summary>A synthetic latent variable (0 = nascent, 1 = mature) that correlates technology adoption, reactor costs, and financing. **It is not calibrated to observed historical trajectories and does not encode an annual learning rate.** The slider is a scenario index: it represents your judgment about how many favorable conditions co-occur, not a measured pace of progress.| Slider | Rough scenario interpretation ||---|---|| 0.1–0.2 | Industry largely stalls; few process improvements reach commercial use and financing remains difficult || 0.3–0.4 | Some technical progress, but major input, scale-up, or financing constraints remain || **0.5** | **Neutral coupling assumption**; the model uses the base technology probabilities without claiming this matches a historical trend || 0.6–0.7 | Several favorable technical and supply-chain developments co-occur || 0.8–0.9 | Strongly favorable bundle; most modeled pathways succeed and financing conditions improve |Recent evidence should update the **individual parameter distributions**, not be translated into a maturity learning rate. For example, GFI's [2026 industry report](https://gfi.org/resource/cultivated-meat-seafood-and-ingredients-state-of-the-industry/) records both tighter funding and 20,000-L production progress, while the FDA's [February 2026 inventory](https://hfpappexternal.fda.gov/scripts/fdcc/index.cfm?set=animalcellculturefoods) lists five completed cultured-cell food consultations. Those signals point in different directions and should not be compressed into a single estimated trend.**To approximate "today's conditions" (minimal technical progress):** set maturity to 0.1 and target year to 2026. Note that even then the parameter ranges reflect forward projections — the model has no explicitly calibrated 2024 cost baseline.[Full explanation, calibration caveats, and stress-test guidance →](#industry-maturity-deep-dive)</details>---**Target Year**```{ojs}//| echo: falseviewof target_year = Inputs.range([2026, 2050], { value: urlNum("target_year", 2036), step: 1, label: "Projection Year"})```<details><summary>What does the year affect?</summary>Earlier years → lower effective maturity, less technology adoption. The model scales maturity by a factor of (0.5 + 0.5 × (year − 2024) / 20), so:- 2026 → maturity scaled to ~55% of slider value- 2036 (default) → maturity scaled to ~80% of slider value- 2044 → maturity at full slider valueThis scaling is mechanical (linear interpolation), not fit to historical data. **To approximate minimal progress from today's state:** combine year = 2026 with maturity = 0.1.</details>---<span id="process-mode-controls"></span>**Probability of Each Process Mode** <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#555;" title="Read each weight as your probability that this mode is the one commercial plants actually use in the projection year, i.e. the lowest-cost method that is technically feasible and permitted. By default the model does not pick the cheapest mode for you: it draws a mode for each simulated plant using these weights. (How the mode is chosen, just below, offers a cheapest-feasible alternative that ignores these weights.) The three values are normalized to sum to 100%, so only their ratios matter. E.g., Fed-batch 0.2, Perfusion 0.5, Continuous 0.3 means 20% of simulated plants use fed-batch, 50% perfusion, 30% continuous.">(?)</abbr>```{ojs}//| echo: falseviewof p_fedbatch = Inputs.range([0, 1], { value: urlNum("p_fedbatch", 0.20), step: 0.05, label: html`P(Fed-batch) <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="Low density (1–6 g dry/L, i.e. 5–30 g wet/L at 20% solids), moderate media use (1–2×). Nutrient-concentrated feeds added periodically. Simpler to operate but lower cell density than perfusion.">(?)</abbr>`})viewof p_perfusion = Inputs.range([0, 1], { value: urlNum("p_perfusion", 0.50), step: 0.05, label: html`P(Perfusion) <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="Medium-high density (6–30 g dry/L, i.e. 30–150 g wet/L), higher media throughput (1–5×). Continuous media exchange with cell retention. Currently the industry standard for high-density CM production.">(?)</abbr>`})viewof p_continuous = Inputs.range([0, 1], { value: urlNum("p_continuous", 0.30), step: 0.05, label: html`P(Continuous) <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="Highest density (10–40 g dry/L, i.e. 50–200 g wet/L), efficient media use (0.5–3×). Near-steady-state operation; cells grown and harvested continuously with optimized recycling.">(?)</abbr>`})``````{ojs}//| echo: false// The mode-selection fields belong to structure_settings (defined in the sidebar's// structural panel) but are shown here, next to the weights they replace.(viewof structure_settings).modeSection``````{ojs}//| echo: false{ const tot = p_fedbatch + p_perfusion + p_continuous; if (tot <= 0) return html``; const rawPct = (tot * 100).toFixed(0); const fb = (p_fedbatch / tot * 100).toFixed(0); const pf = (p_perfusion / tot * 100).toFixed(0); const ct = (p_continuous / tot * 100).toFixed(0); const sumExact = Math.abs(tot - 1) < 0.001; const sumColor = sumExact ? "#27ae60" : "#e67e22"; const sumNote = sumExact ? "" : ` — weights sum to ${rawPct}%, normalized to 100% in simulation`; const pct = v => (v * 100).toFixed(0); if (results.mode_selection_stats) return html`<div style="font-size:0.84em; color:#555; margin: -0.2rem 0 0.4rem 0;"> <strong>Weights above not used:</strong> <em>Cheapest feasible mode</em> is selected in the box just above; choose <em>Draw mode by weights</em> there to use them.<br> Selected: Fed-batch <strong>${pct(results.pct_fedbatch)}%</strong> · Perfusion <strong>${pct(results.pct_perfusion)}%</strong> · Continuous <strong>${pct(results.pct_continuous)}%</strong><br> Feasibility decides the mode in ${pct(results.mode_selection_stats.feasibility_binding_share)}% of worlds. </div>`; return html`<div style="font-size:0.84em; color:#555; margin: -0.2rem 0 0.4rem 0;"> <span style="color:${sumColor}; font-weight:600;">Sum: ${rawPct}%${sumNote}</span><br> Using: Fed-batch <strong>${fb}%</strong> · Perfusion <strong>${pf}%</strong> · Continuous <strong>${ct}%</strong> </div>`;}```<details><summary>What are these modes and why does it matter? (click to expand)</summary>These weights set the probability of each bioreactor process mode being drawn per Monte Carlo simulation. Each simulated plant runs in a single mode; the weights describe uncertainty about which mode the industry ends up using, not a mix of modes inside one facility. Within each mode, cell density and media-use multiplier are sampled from mode-specific ranges — preventing physically incoherent combinations.**How to set them.** The question is which mode will actually be the lowest-cost method that is technically feasible and permitted by regulators in the projection year. By default the model does not choose the cheapest mode in each simulated world; it draws the mode independently of other parameters. So a fed-batch weight of 20% says: "in 20% of worlds, something (scale-up problems, contamination risk, regulation) rules out the higher-density modes and plants fall back on fed-batch."**Alternative: cheapest feasible mode.** Under **How the mode is chosen**, just below the weights, you can instead give the probability that perfusion and continuous culture each *work* at commercial scale. The model then prices every world under each mode and uses the cheapest feasible one; fed-batch is the fallback. The selected shares become an output. With default priors fed-batch is almost never the cheapest mode in a world, and continuous is cheaper than perfusion in about 84% of worlds, so feasibility of 70% (perfusion) and 30% (continuous) gives roughly the same shares and costs as the 20/50/30 weights. The rule favors whichever mode has the fewest downsides modeled: mode-specific equipment, consumable and contamination costs are not yet included. [Method and caveats](docs.qmd#least-cost-mode-selection).For reference, at default settings plants drawn as fed-batch come in under \$25 per wet kg in only about 2% of draws (about 4% even with optimistic maturity, hydrolysate and growth-factor settings), compared with about 24% for perfusion and 52% for continuous (wet-basis figures from before the October 26, 2026 basis change). Treat this with caution: it follows largely from our fed-batch density prior (5–30 g wet/L, 1–6 g dry/L). [Humbird (2021)](https://pmc.ncbi.nlm.nih.gov/articles/PMC8362201/) reaches about 110 g/L in fed-batch with a metabolically improved cell line, and in his base case fed-batch is the *cheaper* mode (\$37/kg vs \$51/kg for perfusion). The 20/50/30 default weights are not anchored to a specific source.| Mode | Density (g dry/L) | Same, wet (g/L at 20% solids) | Media-use (×) | Typical application ||------|--------------|--------------|--------------|---------------------|| Fed-batch | 1–6 | 5–30 | 1.0–2.0 | Periodic nutrient addition; lower achievable densities || Perfusion | 6–30 | 30–150 | 1.0–5.0 | Continuous exchange with cell retention (hollow fiber / centrifuge) || Continuous | 10–40 | 50–200 | 0.5–3.0 | Near-steady-state with recycling; highest densities |*The dry ranges are the earlier wet ranges converted at 20% solids; the draws are identical.*Pure batch (single fill-and-dump) is excluded — not considered commercially viable for cultured meat at scale (expert reviewer feedback, April 2026).*Note: the default mix (20% / 50% / 30%) produces somewhat higher media usage per kg than the previous unconstrained defaults, because fed-batch and perfusion at medium density use more liquid than the prior lognormal(30–200 g/L) assumption. This is more physically realistic.**In full view, an "Override process mode constraints" toggle restores manual density and media-use sliders.*</details>---**Technology Adoption by ${target_year}**Probability that the selected hypothetical plant uses each cost-reducing technology by the projection year. These are scenario probabilities, not industry production shares.```{=html}<div class="reset-button-row">``````{ojs}//| echo: falseviewof reset_adoption = Inputs.button("↺ Reset adoption defaults", { reduce: () => { // Set viewof values back to defaults viewof p_hydro.value = 0.75; viewof p_hydro.dispatchEvent(new Event("input", {bubbles: true})); viewof p_recfactors.value = 0.5; viewof p_recfactors.dispatchEvent(new Event("input", {bubbles: true})); viewof gf_progress.value = 50; viewof gf_progress.dispatchEvent(new Event("input", {bubbles: true})); viewof p_supp_protein.value = 0.70; viewof p_supp_protein.dispatchEvent(new Event("input", {bubbles: true})); }})``````{=html}</div>``````{ojs}//| echo: falseviewof p_hydro = Inputs.range([0.3, 0.95], { value: urlNum("p_hydro", 0.75), step: 0.05, label: "P(Hydrolysates for basal media)"})```::: {.callout-note appearance="minimal" icon=false collapse=false}**Basal media** = the bulk nutrient broth cells grow in (water + amino acids + glucose + salts + vitamins) — *everything except* the expensive recombinant growth factors, which we model on a separate line. **Hydrolysates** are cheap enzymatically-digested plant or yeast proteins; they replace the *purified pharmaceutical-grade amino acids* (vegan but ~10× more expensive) that dominate Humbird's basal-media cost.*The worked figures in this note are per kg wet biomass with wet densities; per kg dry cell mass at 20% solids, multiply litres and dollars by 5 and divide densities by 5.***Note on \$/L vs \$/kg output:** The model uses \$/L as an internal parameter, but the economically meaningful unit is **\$/kg of cell biomass produced** — which equals (\$/L) × (L consumed per kg output). L per kg = 1000/density(g/L) × media_turnover_multiplier. In an illustrative scenario (60 g/L density, 1.5× multiplier), this is ~25 L/kg: a \$0.70/L hydrolysate medium costs ~\$17.5/kg output; a \$1.25/L pharma-grade medium costs ~\$31/kg. At 200 g/L perfusion with 1.0× turnover (~5 L/kg), the same formulations cost \$3.5/kg and \$6.25/kg respectively — illustrating the inverse-density relationship when all other inputs are fixed; both density and price have unit elasticity in this formula. The "Media" bar in the cost breakdown chart already shows this \$/kg result directly.**Note on correlation with density:** \$/L and cell density (g/L) are not fully independent — richer media is often needed to sustain higher densities, particularly in fed-batch systems where cells deplete the available nutrient pool. Combining a very low $/L with a very high density assumption in the same scenario may therefore overstate cost savings. The model partially addresses this via process-mode-specific density ranges, but does not impose explicit correlation between $/L and density within a given mode. See [Model Limits](limits.qmd#parameter-grounding) for discussion.:::<details><summary>What is this? (click to expand)</summary>*Reminder:* <abbr title="Basal media = the bulk nutrient broth cells grow in: water + amino acids + glucose + salts + vitamins/minerals. It is everything except the expensive recombinant growth factors, which are added separately.">**Basal media**</abbr> is the bulk nutrient broth cells grow in (water, amino acids, glucose, salts, vitamins) — everything except the expensive recombinant growth factors, which we model as a separate line.<abbr title="What hydrolysates CAN do for growth factors: (1) Replace amino acid/peptide nutritional content that serum also provides. (2) Possibly contain trace bioactive peptides with modest stimulatory effects. (3) Reduce the amount of GFs needed by improving overall cell health and nutrition — but they cannot replace GF signaling function itself.">**Hydrolysates**</abbr> 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 basically 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 | ↗ Indirect benefit: can reduce GF requirements by improving cell health; cannot replace GF signaling |**Cost comparison:**| Media Type | Cost ($/L) | Source ||------------|-----------|--------|| Pharma-grade amino acids | $0.50 – $2.50 | Revised down from $1-4 based on [GFI amino acid report (2025)](https://gfi.org/resource/amino-acid-cost-and-supply-chain-analysis-for-cultivated-meat/): real supplier quotes found Humbird's AA prices 2-10x too high || Hydrolysate-based | $0.20 – $1.20 | [Humbird 2021](https://pmc.ncbi.nlm.nih.gov/articles/PMC11663224/): ~$2/kg amino acids; [O'Neill et al. 2021](https://ift.onlinelibrary.wiley.com/doi/abs/10.1111/1541-4337.12678) |**Why 75% baseline?** This is a judgmental scenario default, not an empirically estimated adoption rate. [Pasitka et al. (2024)](https://www.nature.com/articles/s43016-024-01022-w) demonstrated a low-cost animal-component-free medium in one chicken-cell process, while [Mi et al. (2025)](https://doi.org/10.1016/j.cej.2025.164496) found that selected plant hydrolysates reduced serum use by up to 70% in a porcine-cell prototype. These are meaningful feasibility results, but they do not establish full substitution, lot-to-lot consistency, or commercial performance across cell lines and large reactors.**How pivotal is this parameter?** The unresolved questions are technical, operational, and regulatory: amino-acid completeness, supplementation needs, batch variability, quality control, performance at scale, residuals, and product-specific safety review. Current US and UK guidance focuses on process controls, hazards, and evidence for the submitted product; we have not found support for the stronger claim that regulators generally require pharmaceutical-grade ingredients. Treat 75% as a scenario to stress-test, not a consensus forecast.*See [Maturity Correlation](#industry-maturity-deep-dive) for how adoption probability is adjusted.*</details><details><summary>Note: basal micronutrients are folded into media $/L (click to expand)</summary>**This slider has been removed.** A previous version of the model carried a separate "food-grade micronutrients" cost line (vitamins, minerals, trace salts). That structure double-counted basal media, which the model already describes as providing "amino acids, glucose, vitamins."Basal micronutrients (vitamins, minerals, trace salts) are not modeled separately here. Basal media on this page already includes vitamins, and the literature suggests vitamin/mineral sourcing is relatively low-cost compared with growth factors and recombinant proteins ([O'Neill et al. 2021](https://ift.onlinelibrary.wiley.com/doi/abs/10.1111/1541-4337.12678): vitamin sourcing is a "less pressing issue" and required minerals can "generally be obtained relatively inexpensively"). We therefore include basal micronutrients inside media $/L and reserve separate uncertainty terms for the supplemental recombinant proteins (insulin, transferrin, albumin) now modeled separately — see the [model change log](#micronutrient-change-note) below.</details><a id="growth-factors-explanation"></a>```{=html}<div class="separable-only">```::: {.callout-note appearance="minimal" icon=false}<abbr title="Growth factors (GFs) are signaling proteins — FGF-2, IGF-1, TGF-β, etc. — that tell cells to divide. At current research-grade prices they can dominate media costs (55–95% of media $/L).">**Growth factors (GFs)**</abbr> signal cells to proliferate — at current research-grade prices, they can dominate media costs. The slider below sets P(at least one scalable production route — e.g., autocrine cell lines, plant-based farming, or precision fermentation — reaches commercial scale by the projection year), switching between "expensive" and "cheap" GF price regimes. *For background on what growth factors do and why they're expensive, see [Learn → Step 5: Growth Factors](learn.html#step-5-growth-factors-a-key-cost-driver); the production routes this slider refers to are compared in [Solutions being developed](learn.html#gf-solutions).*:::```{ojs}//| echo: falseviewof p_recfactors = Inputs.range([0.1, 0.9], { value: urlNum("p_recfactors", 0.5), step: 0.05, label: html`P(Scalable <abbr style="cursor:help;text-decoration:underline dotted;" title="Growth Factor — signaling proteins like FGF-2, IGF-1, TGF-β that tell cells to proliferate. Currently the most expensive media component.">Growth Factor (GF)</abbr> technology)`})``````{ojs}//| echo: falseviewof gf_progress = Inputs.range([0, 100], { value: urlNum("gf_progress", 50), step: 5, label: html`GF price progress index for ${target_year} (%)`})```<details><summary>What do these sliders control? (click to expand)</summary>```{ojs}//| echo: false{ const ranges = costModel.gfPriceRanges(gf_progress); return html`<p title="Reports 5 and 6 requested visible control semantics. These are the engine's existing assumed price distributions, not newly fitted evidence."><strong>Current GF price ranges (p5–p95):</strong> cheap regime $${ranges.cheap[0].toFixed(2)}–$${ranges.cheap[1].toFixed(2)}/g; expensive regime $${ranges.expensive[0].toFixed(2)}–$${ranges.expensive[1].toFixed(2)}/g. Adoption selects the regime; progress shifts both ranges. Direct GF-cost priors and complete medium replace this calculation.</p>`;}```**Two controls for growth factors:** (1) **P(Scalable GF technology)** sets the probability of a breakthrough reaching commercial scale (switches between "expensive" and "cheap" price regimes). (2) **GF cost reduction progress** sets how far prices have fallen within each regime by the projection year — 0% = current prices, 100% = industry targets achieved.**Growth factors** are signaling proteins (FGF-2, IGF-1, TGF-β, etc.) that tell cells to proliferate. Currently **the most expensive media component** — often 55-95% of media cost at research scale.**Current vs. projected prices:**| Scenario | Price ($/g) | Status | Source ||----------|-------------|--------|--------|| Research-grade FGF-2 | $50,000 | Current (2024) | [CEN](https://cen.acs.org/food/Inside-effort-cut-cost-cultivated/101/i33) || Research-grade TGF-β | $1,000,000 | Current | GFI analysis || Model "expensive" | $500 - $50,000 | Limited progress | Conservative projection || Model "cheap" | $1 - $100 | Breakthrough achieved | Industry target || Plant-based (BioBetter) | ~$1 | Target | [FoodNavigator](https://www.foodnavigator.com/Article/2022/09/12/biobetter-s-growth-factor-innovation-cuts-cost-of-cultured-meat/) |---<details><summary><strong>What triggers the "cheap" scenario? (Key breakthroughs)</strong></summary>The probability slider represents whether **at least one** of these technology approaches succeeds at commercial scale by the projection year:| Technology | Mechanism | Target price | Status (2025) ||------------|-----------|--------------|---------------|| **Autocrine cell lines** | Engineer cells to produce their own FGF2, eliminating external supply | ~$0/g (no purchase needed) | [Proof of concept 2023](https://pmc.ncbi.nlm.nih.gov/articles/PMC10153192/) — Mosa Meat, Tufts collaborations || **Plant molecular farming** | Express GFs in transgenic tobacco/lettuce | $1-10/g | [BioBetter](https://www.foodnavigator.com/Article/2022/09/12/biobetter-s-growth-factor-innovation-cuts-cost-of-cultured-meat/), [ORF Genetics](https://orfgenetics.com/) pilots || **Precision fermentation** | High-density E. coli/yeast expression (like insulin) | $10-100/g | [GFI 2024 analysis](https://gfi.org/science/the-science-of-cultivated-meat/) — scaling remains key challenge || **Polyphenol substitution** | Curcumin (NaCur) activates FGF receptors, reduces FGF2 needs by 80% | N/A (reduces quantity) | [Research 2024](https://pmc.ncbi.nlm.nih.gov/articles/PMC10119461/) || **Thermostable variants** | FGF2-G3 with 20-day half-life (vs hours) | Same $/g, less usage | [Enantis](https://www.enantis.com/) commercial product |**Why this is modeled as a binary switch + continuous progress:**- The **switch** represents whether *any* scalable technology reaches commercial viability- The **progress slider** represents how far costs have dropped within that regime- Even in the "expensive" scenario (no breakthrough), incremental improvements occur- This structure captures the bimodal nature of the uncertainty: either scalable production is achieved, or it isn't**Model cost impact:**| Scenario | Usage (g/kg) | Price ($/g) | **Cost/kg chicken** ||----------|-------------|-------------|---------------------|| No breakthrough (Humbird formulation × 8–60 L/kg media use) | 0.001 – 0.006 | $500 – $50,000 | **$0.5 – $300** || Breakthrough achieved (≈3× usage reduction) | 0.0005 – 0.002 | $10 – $1,000 | Endpoint products $0.005–2 (not a cost credible interval) |**Where the quantity range comes from.** GFI's 2023 recombinant-protein cost analysis reproduces the Humbird medium formulation with FGF at 0.1 mg/L and TGFβ at 0.002 mg/L — about 0.102 mg/L of true growth factors combined. GFI's efficient-future scenarios assume roughly 8–13 L media per kg meat; less-optimized processes may use up to ~60 L/kg. Multiplying through gives ≈**0.0008–0.006 g true GF/kg** as the defensible cited range, with Pasitka et al. (2024)'s empirical ACF chicken process landing at ~0.00187 g/kg — comfortably inside this band. An earlier version of this model used 0.0001–0.02 g/kg, which was too broad on both tails.**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. Note that tightening the quantity range means the **price** regime is now doing more of the work in the tornado — the swing in cost comes primarily from the \$/g uncertainty, not the g/kg uncertainty.*See [Maturity Correlation](#industry-maturity-deep-dive) for how adoption probability is adjusted.*</details></details>```{=html}</div>```---**Supplemental Recombinant Proteins** <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="Albumin, transferrin, and insulin — used as media additives to support cell survival and growth. Distinct from growth factors (FGF/IGF/TGF-β). GFI 2024 projects albumin alone as 96.6% of anticipated CM recombinant protein production volume. Currently priced at pharma-grade ($10-200/g); food-grade recombinant production (from yeast) could reduce this dramatically.">(?)</abbr>```{ojs}//| echo: falseviewof p_supp_protein = Inputs.range([0.20, 0.95], { value: urlNum("p_supp_protein", 0.70), step: 0.05, label: html`P(Albumin/transferrin/insulin affordable by ${target_year}) <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="Probability that food-grade recombinant production (from yeast, bacteria, or plant systems) brings albumin + transferrin + insulin costs to under ~$1/g by the target year, making per-kg-cell-mass contribution below ~$0.50/kg. Without breakthrough: pharma-grade sourcing could add $0.50-4/kg. Default 70%: considered more tractable than GF breakthrough because albumin at scale is closer to an engineering problem than a discovery problem.">(?)</abbr>`})```<details><summary>About supplemental proteins and cost ranges (click to expand)</summary>**Albumin, transferrin, and insulin** are media additives — proteins grouped separately here for cost accounting (insulin also has signaling functions). They are widely used in serum-free cell culture but rarely modeled explicitly in CM TEAs (usually bundled into "other VOC" or media cost).Per GFI's 2024 supply-chain analysis, albumin is expected to represent ~97% of anticipated recombinant protein production volume for CM. At typical cell-culture concentrations (~1 mg/mL albumin) and the model's L/kg range, the per-kg cost contribution is:| Sourcing | Albumin price | Contribution to cell mass cost ||----------|--------------|-------------------------------|| Food-grade recombinant (yeast) | $0.10–1/g | ~$0.03–0.50/kg || Pharma-grade recombinant | $5–50/g | ~$0.50–4/kg || Albumin-free formulation | $0/g | ~$0/kg |Insulin and transferrin are used at much lower concentrations and contribute <$0.05/kg in all scenarios.</details>```{=html}<div class="full-mode-only">```---**Financing**```{ojs}//| echo: falseviewof wacc_lo = Inputs.range([5, 20], { value: urlNum("wacc_lo", 8), step: 1, label: "WACC Low (%)"})viewof wacc_hi = Inputs.range([10, 35], { value: urlNum("wacc_hi", 20), step: 1, label: "WACC High (%)"})viewof asset_life_lo = Inputs.range([5, 15], { value: urlNum("asset_life_lo", 8), step: 1, label: "Asset Life Low (years)"})viewof asset_life_hi = Inputs.range([10, 30], { value: urlNum("asset_life_hi", 20), step: 1, label: "Asset Life High (years)"})``````{ojs}//| echo: false{ // Reactive diagnostic: show CAPEX share and explain why WACC might seem insensitive const capexMean = mean(results.cost_capex); const totalMean = mean(results.unit_cost); const capexPct = totalMean > 0 ? (capexMean / totalMean * 100).toFixed(0) : 0; const isOff = !include_capex && !simpleMode; const isCDMO = cdmo_mode; let msg = ''; let color = '#27ae60'; if (isOff) { msg = 'CAPEX is currently excluded — financing parameters have no effect. Re-enable in Model Structure above.'; color = '#c0392b'; } else if (isCDMO) { msg = 'CDMO mode is on — the CDMO toll replaces CAPEX/WACC. These financing sliders have no effect.'; color = '#e67e22'; } else if (capexPct < 15) { msg = `CAPEX is only ${capexPct}% of mean total cost right now — media costs dominate (often driven by fed-batch process mode). Large WACC changes will cause small visible shifts. Try reducing the fed-batch weight above to increase CAPEX sensitivity.`; color = '#e67e22'; } else { msg = `CAPEX = ${capexPct}% of mean total cost. Changes to WACC and Asset Life will have visible effects.`; color = '#27ae60'; } return html`<div style="font-size:0.82em; margin: 0.25rem 0 0.4rem; padding:0.4rem 0.7rem; background:#fafafa; border-left:3px solid ${color}; border-radius:0 4px 4px 0; line-height:1.5;"> ${msg} </div>`;}```<details><summary>What are WACC and Asset Life? (click to expand)</summary>*WACC (Weighted Average Cost of Capital):* The blended rate of return required by debt and equity investors, used to annualize capital costs via the Capital Recovery Factor (CRF). A WACC of 8% is typical for established food manufacturing; 20%+ reflects the high risk premium investors demand for an unproven technology. The simulation samples WACC from a lognormal distribution between these bounds, further adjusted by the maturity factor.*Asset Life:* How many years bioreactor and facility equipment is depreciated over. These two sliders define a **uniform distribution** over the plausible range — not a single fixed value. The simulation draws a different asset life for each of the 30,000 Monte Carlo runs. Shorter life (8 years) means higher annual capital charges; longer life (20 years) spreads costs but assumes equipment remains productive. To set a specific value, set both sliders to the same number.These two parameters together determine the **Capital Recovery Factor** (<abbr title="CRF = r(1+r)^n / ((1+r)^n - 1), where r = WACC and n = asset life in years. A higher WACC or shorter asset life means larger annual payments to service the capital, which increases cost/kg.">CRF</abbr>).</details>```{=html}</div>```---**Cell Density / Media-Use Override** <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#555;" title="In the default model, cell density is not a free slider — it is sampled automatically from mode-specific ranges (fed-batch: 1–6 g dry/L; perfusion: 6–30; continuous: 10–40, i.e. 5–30, 30–150 and 50–200 g wet/L at 20% solids) based on the process mode probabilities above. This reflects the fact that density is physically determined by operating mode, not independently chosen. Enable 'Override' below to set density ranges directly — useful if you have a specific bioreactor configuration in mind.">(?)</abbr>```{ojs}//| echo: falseviewof override_mode_constraints = Inputs.toggle({ label: html`Override process mode constraints <abbr style="cursor:help;text-decoration:underline dotted;font-size:0.85em;color:#888;" title="When ON: process-mode sampling is bypassed and you can specify density and media-use ranges directly. Useful for experts wanting to model specific bioreactor configurations.">(?)</abbr>`, value: urlBool("override_mode_constraints", false)})``````{=html}<div class="override-mode-only">``````{ojs}//| echo: falseviewof density_lo = Inputs.range([2, 20], { value: urlNum("density_lo", 6), step: 1, label: "Cell Density Low (g dry cells/L)"})viewof density_hi = Inputs.range([10, 60], { value: urlNum("density_hi", 40), step: 2, label: "Cell Density High (g dry cells/L)"})```<details><summary>What is cell density and why does it matter so much? (click to expand)</summary>**Cell density** (grams of dry cell mass per litre at harvest, since October 26, 2026) determines how much meat you get per liter of bioreactor volume. Under the default medium rule, higher density means less media per kilogram of product, which directly reduces the largest variable cost.**Units.** Sources report density in three ways: wet weight per litre, dry weight per litre, or cell counts per millilitre times an assumed mass per cell. Dry and wet differ by the solids content (about 20% for harvested cells, so 50 g wet/L ≈ 10 g dry/L). Cell counts depend on an assumed mass per cell, which varies roughly twofold with growth rate and phase. The figures below give both.| Density (wet, as usually reported) | Density (dry, at 20% solids) | Media per kg wet / per kg dry | Typical context ||---------|---------|-------------|-----------------|| 10 g/L | 2 g/L | ~100 / ~500 L | Current lab scale || 50 g/L | 10 g/L | ~20 / ~100 L | Near-term commercial target || 200 g/L | 40 g/L | ~5 / ~25 L | Optimistic TEA projection |**This is multiplicative.** If media costs $1/L, going from 10 to 50 g wet/L cuts media cost from $100 to $20 per wet kg ($500 to $100 per dry kg). Going to 200 g wet/L cuts it to $5 per wet kg. Cell density is arguably the single most important technical parameter for cost reduction.**Current state:** Most published data shows 10-50 g/L (wet). Some companies claim higher, but these claims are difficult to verify independently. Lever VC's 2025 report claims 60-90 g/L has been achieved by "second generation" companies. Whether 200 g/L wet (40 g dry/L) is achievable by 2036 is a genuine open question.</details><details><summary>What about bioreactor volume / tank size? (click to expand)</summary>**Bioreactor volume** is another major uncertainty that is currently *implicit* in this model rather than a direct parameter.The model computes total working volume as: `total_volume = annual_output / (density × productivity × 365)`. It then applies a power-law scaling for CAPEX. But individual bioreactor tank size matters for several reasons:| Factor | Small tanks (2,000-5,000L) | Large tanks (20,000-50,000L) ||--------|--------------------------|------------------------------|| Cost per liter | Higher | Lower (economies of scale) || Contamination risk | Lower | Higher (single failure = large loss) || Mixing/O2 transfer | Easier | Harder at scale || Flexibility | More modular | Less redundancy || Industry precedent | Pharma standard | Requires new engineering |**Key debate:** Some companies (e.g., Vow) claim to have built 20,000L bioreactors for under $1M in 14 weeks using custom food-grade designs. If true, this dramatically changes the CAPEX picture. Humbird's analysis assumed pharma-grade bioreactors at $50-500/L.**Why it's not a direct slider (yet):** Adding individual tank size would require modeling the number of tanks, contamination batch-failure rates, and the trade-off between scale and reliability. This is a planned enhancement. For now, the **Plant Capacity** and **Cell Density** parameters together determine total working volume, and the **custom reactor ratio** (in full view) captures the pharma-vs-food-grade cost difference.**Workshop discussion:** This was one of the key cruxes for the [May 8, 2026 CM workshop](https://uj-cm-workshop.netlify.app/) — what bioreactor scale is realistic, and what does it cost?</details>```{=html}<div class="full-mode-only">```**Advanced: <span title="Net fresh media consumed per kg of cells, divided by the nominal (1000/density) reactor-fill volume. 1 = batch; >1 = perfusion; <1 = recycling / fed-batch / explicit medium recovery.">Media-use multiplier (×)</span>**<details><summary>What is this — and why can it be below 1? (click to expand)</summary>The model computes media volume per kg as `(1000 / density) × multiplier`. A value of **1** is traditional batch mode (fill reactor once, harvest); **>1** is perfusion (multiple media-volume equivalents flow through during the run); **<1** represents media recycling, fed-batch with concentrated feeds, or harvest-side cell concentration. The [Learn page](learn.html#media-use-mechanisms) walks through all three mechanisms.**Range interpretation:** The manual override uses p5–p95 0.5–3.0; default sampling instead uses process-specific ranges. These are lognormal percentiles, not hard bounds. Values below one need explicit medium recovery and consistently priced replacement nutrients. For example, 8 L/kg at 90 g/L implies a multiplier of 0.72, while the same medium use at 200 g/L implies 1.6. A lower multiplier is not the only way to achieve a particular L/kg value.</details><details><summary>Show multiplier sliders</summary>```{ojs}//| echo: falseviewof media_turnover_lo = Inputs.range([0.25, 2], { value: urlNum("media_turnover_lo", 0.5), step: 0.05, label: "Media-use multiplier p5 (low end)"})viewof media_turnover_hi = Inputs.range([1, 10], { value: urlNum("media_turnover_hi", 3.0), step: 0.1, label: "Media-use multiplier p95 (high end)"})```</details>```{=html}</div>``````{=html}</div>``````{ojs}//| echo: false// URL state writer: serialize every viewof value that DIFFERS FROM ITS// DEFAULT into ?key=val pairs, then debounce-write to the URL via// history.replaceState. Critical invariant: if every slider is at its// default, the URL stays bare (pathname + hash only) — no query string.// This is required so Hypothes.is can find annotations on the canonical// bare URL; a polluted URL breaks annotation lookup for every visitor.// The writer depends on every viewof name below so OJS re-runs it// whenever any input changes. Reads nothing from urlParams.{ // Hard-coded defaults must stay in sync with each Inputs.range() / // Inputs.toggle() declaration above and with the reset_adoption button. const defaults = { ...costModel.DEFAULT_PARAMS, simpleMode: true, blending_share: 0.25, filler_cost: 3, include_capex: true, include_fixed_opex: true, include_downstream: false, include_harvest_dewater: false, cdmo_mode: false, cdmo_toll_p5: 20, cdmo_toll_p95: 200, bundled_media: false, bundled_media_p5: 50, bundled_media_p95: 500, plant_capacity: 20, uptime: 0.90, maturity: 0.5, target_year: 2036, p_fedbatch: 0.20, p_perfusion: 0.50, p_continuous: 0.30, override_mode_constraints: false, p_hydro: 0.75, p_recfactors: 0.5, gf_progress: 50, p_supp_protein: 0.70, wacc_lo: 8, wacc_hi: 20, asset_life_lo: 8, asset_life_hi: 20, density_lo: 6, density_hi: 40, media_turnover_lo: 0.5, media_turnover_hi: 3.0 }; const state = { ...structure_settings, ...Object.fromEntries(Object.entries(expert_priors_adv).map(([k, v]) => [`ep_${k}`, v])), simpleMode, blending_share, filler_cost, include_capex, include_fixed_opex, include_downstream, include_harvest_dewater, cdmo_mode, cdmo_toll_p5, cdmo_toll_p95, bundled_media, bundled_media_p5, bundled_media_p95, plant_capacity, uptime, maturity, target_year, p_fedbatch, p_perfusion, p_continuous, override_mode_constraints, p_hydro, p_recfactors, gf_progress, p_supp_protein, wacc_lo, wacc_hi, asset_life_lo, asset_life_hi, density_lo, density_hi, media_turnover_lo, media_turnover_hi }; const usp = new URLSearchParams(); let hasDiff = false; for (const [k, v] of Object.entries(state)) { const def = defaults[k]; let matches; if (typeof v === "boolean") matches = (v === def); else if (typeof v === "number") matches = Math.abs(v - def) < 1e-9; else matches = (v === def); if (!matches) { hasDiff = true; if (typeof v === "boolean") usp.set(k, v ? "1" : "0"); else if (typeof v === "number" && Number.isFinite(v)) usp.set(k, String(v)); else if (typeof v === "string") usp.set(k, v); } } if (window._urlWriteTimer) clearTimeout(window._urlWriteTimer); window._urlWriteTimer = setTimeout(() => { try { const newUrl = hasDiff ? (location.pathname + "?" + usp.toString() + location.hash) : (location.pathname + location.hash); history.replaceState(null, "", newUrl); } catch (e) { console.warn("URL state update failed:", e); } }, 300); // Return an invisible empty element instead of `null` — when an OJS cell // returns null/undefined, the cell renders the literal text "null" in the // output. This is a no-op span used purely for its side effect (URL sync). return html`<span style="display:none;"></span>`;}``````{ojs}//| echo: false// Expert priors — collapsible panel for the three biggest cost driversmodelControls = import(new URL("./model-controls.mjs", window.location.href).href)viewof expert_priors_adv = modelControls.expertPriorControl(new URLSearchParams(urlParams))viewof structure_settings = modelControls.structuralControl(new URLSearchParams(urlParams))```:::::: {.panel-fill}```{ojs}//| echo: false// Shared parameter object — consumed by both the primary run and the comparison runsimParams = { const adjustedMaturity = costModel.maturityForYear(maturity, target_year); return { ...structure_settings, // Hybrid product used for the comparison cost and thresholds (dry basis) cell_inclusion_rate: blending_share, filler_cost: filler_cost, target_year, plant_kta_p5: simpleMode ? 10 : plant_capacity * 0.5, plant_kta_p95: simpleMode ? 40 : plant_capacity * 2.0, uptime_mean: simpleMode ? 0.90 : uptime, maturity_mean: adjustedMaturity, p_hydro_mean: p_hydro, p_recfactors_mean: p_recfactors, p_supp_protein_mean: simpleMode ? 0.70 : p_supp_protein, wacc_p5: simpleMode ? 0.08 : wacc_lo / 100, wacc_p95: simpleMode ? 0.20 : wacc_hi / 100, asset_life_lo: simpleMode ? 8 : asset_life_lo, asset_life_hi: simpleMode ? 20 : asset_life_hi, density_gL_p5: density_lo, density_gL_p95: density_hi, media_turnover_p5: simpleMode ? 0.5 : media_turnover_lo, media_turnover_p95: simpleMode ? 3.0 : media_turnover_hi, include_capex: simpleMode ? true : include_capex, include_fixed_opex: simpleMode ? true : include_fixed_opex, include_downstream: simpleMode ? false : include_downstream, include_harvest_dewater: simpleMode ? false : include_harvest_dewater, gf_progress: gf_progress, cdmo_mode: cdmo_mode, cdmo_toll_p5: cdmo_toll_p5, cdmo_toll_p95: cdmo_toll_p95, // Process mode (always active; override only available in full view) p_fedbatch: p_fedbatch, p_perfusion: p_perfusion, p_continuous: p_continuous, override_mode_constraints: simpleMode ? false : override_mode_constraints, // Bundled media (full view only) bundled_media: simpleMode ? false : bundled_media, bundled_media_p5: bundled_media_p5, bundled_media_p95: bundled_media_p95, // Expert prior overrides — null means use model defaults ep_media_p10: expert_priors_adv.media_p10, ep_media_p50: expert_priors_adv.media_p50, ep_media_p90: expert_priors_adv.media_p90, ep_gf_p10: expert_priors_adv.gf_p10, ep_gf_p50: expert_priors_adv.gf_p50, ep_gf_p90: expert_priors_adv.gf_p90, ep_density_p10: expert_priors_adv.density_p10, ep_density_p50: expert_priors_adv.density_p50, ep_density_p90: expert_priors_adv.density_p90 };}``````{ojs}//| echo: falsehtml`<div role="status">${results.warnings.map(w => html`<p><strong>Scenario note:</strong> ${w}</p>`)}</div>```````{ojs}//| echo: false// Wait for inputs to settle before simulating. Each change of simParams invalidates// the cell below, which cancels its pending timer, so the model runs once after// 300 ms without changes instead of on every step of a slider drag or keystroke.// (An `await` delay inside one cell is not enough: the runtime queues each new// evaluation behind the previous one, which throttles rather than debounces.)mutable debouncedParams = null{ const params = simParams; const timer = setTimeout(() => { mutable debouncedParams = params; }, 300); invalidation.then(() => clearTimeout(timer)); return html`<span style="display:none;"></span>`;}// Downstream cells stay pending until the first settled value arrives.settledParams = debouncedParams ?? new Promise(() => {})// Primary simulation run (uses whatever mode is selected)// N=30,000 balances browser responsiveness and sampling precision.// Repeat-seed variability is reported by scripts/model-audit.mjs.results = simulate(30000, 42, settledParams)// Comparison run: when CDMO mode is on, also run the in-house baseline// (same seed so VOC components are paired; only capital structure differs)// Returns an invisible span (not null) when CDMO is off — returning null// causes OJS to render the literal text "null" in the document.inhouse_results = { if (!cdmo_mode) return html`<span style="display:none;"></span>`; return simulate(30000, 42, {...settledParams, cdmo_mode: false});}// Units for labels. On the dry basis, thresholds and chicken comparisons use a hybrid// product (cells at meat moisture plus plant filler); on the wet basis, wet cost.basisLabels = { const dry = results.basis.cost_basis === "dry"; const hybrid = modelControls.hybridLabel(results.basis); return { dry, cost: dry ? "$/kg dry cell mass" : "$/kg wet biomass", costShort: dry ? "dry" : "wet", hybrid, threshold: dry ? `$/kg ${hybrid}` : "$/kg wet biomass", thresholdShort: dry ? "hybrid product" : "wet", // Where a hybrid-product threshold sits on the headline cost axis toCostAxis: t => dry ? modelControls.hybridToDry(t, results.basis) : t, // Probability-card price points: lower on the hybrid product than on wet cells thresholds: dry ? [5, 10, 25, 50] : [10, 25, 50, 100], // Conventional chicken at the wholesale level (decided 5 November 2026), on the cost axis. // On the wet basis it is compared with wet cells directly. chicken: modelControls.CHICKEN_WHOLESALE, chickenAxis: dry ? modelControls.hybridToDry(modelControls.CHICKEN_WHOLESALE.central, results.basis) : modelControls.CHICKEN_WHOLESALE.central, chickenNote: modelControls.chickenWholesaleNote(results.basis), hybridNote: modelControls.hybridNote(results.basis), // Second x axis in hybrid-product dollars on dry-cost charts; none on the wet basis meatAxes: domainMax => dry ? modelControls.hybridAxes(Plot, domainMax, results.basis) : [], marginTop: dry ? 45 : undefined };}// Calculate statistics (cell cost and the threshold basis: hybrid product on dry)stats = { const uc = results.unit_cost; const tc = results.threshold_cost; const pct = (arr, threshold) => arr.filter(x => x < threshold).length / arr.length * 100; return { n: uc.length, p5: quantile(uc, 0.05), p50: quantile(uc, 0.50), p95: quantile(uc, 0.95), t_p5: quantile(tc, 0.05), t_p50: quantile(tc, 0.50), t_p95: quantile(tc, 0.95), probs: basisLabels.thresholds.map(x => ({threshold: x, prob: pct(tc, x)})) };}```### Results Summary**Quantity being modeled:** manufacturing cost per kg of cultured-chicken dry cell mass, shown alongside the ingredient cost of a kg of **hybrid product** (25% cells at chicken-meat moisture, 75% plant-based ingredients at \$3/kg; adjustable). Wet biomass is one switch away under *Structural stress tests*. Each figure is for a hypothetical plant/process scenario. Each draw samples one process; process weights are probabilities across scenarios, not production shares within a simulated industry. The output is not an industry-average forecast or a frontier estimate. (With cheapest-feasible mode selection on, each draw uses the lowest-cost feasible mode, a step toward a frontier estimate for the process choice only.) Commercialization probability is unmodeled; dollars retain mixed source years.[]{#output-cautions}```{ojs}//| echo: falsehtml`<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 manufacturing cost per kg of cultured chicken cell biomass (${basisLabels.dry ? html`<span title="Per kg dry cell mass. Dry mass does not change with washing or dewatering, maps to nutrition, and is the natural basis for nutrient stoichiometry. Density inputs are g dry cells per litre. Wet-basis sources and built-in priors are converted at ${(results.basis.solids_fraction * 100).toFixed(0)}% solids. Probability thresholds use the hybrid product: ${(results.basis.cell_inclusion_rate * 100).toFixed(0)}% cells at ${(results.basis.target_solids_fraction * 100).toFixed(1)}% solids plus plant-based filler. Includes media, growth factors, annualized bioreactor capital, plant overhead, and utilities. Excludes product formulation, texturization, packaging, distribution, and retail margins." style="text-decoration: underline dotted; cursor: help;">dry cell mass ⓘ</span>` : html`<span title="Per kg wet biomass at ${(results.basis.solids_fraction * 100).toFixed(0)}% solids: the headline basis before October 26, 2026. Wet kg is not a stable unit, because washing and dewatering change what it contains." style="text-decoration: underline dotted; cursor: help;">wet-biomass basis ⓘ</span>`}) in ${target_year}, based on ${stats.n.toLocaleString()} Monte Carlo simulations. This is a factory-gate ingredient-cost estimate, not a consumer-product price. <a href="compare.html" style="font-size: 0.9em;">[Compare to published TEAs →]</a><br><br><strong><span title="UPSIDE Foods' chicken cutlet is a blend of cultured chicken cells and plant-based ingredients. SuperMeat's chicken burger used ~30% cultured cells. The GFI State of the Industry 2024 report notes that 'hybrid products combining cultivated and plant-based ingredients are the most likely near-term path to market.' Eat Just/GOOD Meat's Singapore-approved product uses cultured chicken in a plant-protein matrix." style="text-decoration: underline dotted; cursor: help;">Pure cells vs. consumer products:</span></strong> Most cultivated meat products on the market or in development are <em>hybrid products</em> — blending a fraction of cultured cells with plant-based or mycoprotein ingredients. That is why the second number on each card is for a hybrid product (${(results.basis.cell_inclusion_rate * 100).toFixed(0)}% cells by default). <span title="${basisLabels.chickenNote}" style="text-decoration: underline dotted; cursor: help;">We compare it with conventional chicken at the wholesale level</span>: US wholesale boneless chicken averaged $${basisLabels.chicken.central.toFixed(2)}/kg in 2026 dollars over September 2022 to September 2026 (middle 80% of months $${basisLabels.chicken.low.toFixed(1)}–$${basisLabels.chicken.high.toFixed(1)}), the same point in the supply chain as this factory-gate cost.<br><br><strong>Three cautions before using the output:</strong> (1) source costs are not yet normalized to one real-dollar base year; (2) the CM_01 beliefs question is now unconditional and hypothetical: its central part (A1) asks for "the production-weighted average cost per kg of cultivated-chicken cell biomass in 2036, at a scale of at least 2,000 tonnes per plant per year", including hypothetical production if no such plants operate (<a href="https://uj-cm-workshop.netlify.app/question-changes.html">question changes</a>). This model now targets the same unconditional, hypothetical framing, but A1 asks for a production-weighted average in 2025 dollars while the model's decided target is the best available plant in 2026 dollars (<a href="limits.html#estimand">estimand note</a>); and (3) compare the output with conventional chicken at wholesale (about $${basisLabels.chicken.central.toFixed(2)}/kg boneless), not with the retail price (about $${basisLabels.chicken.retail.toFixed(2)}/kg): US retail prices include processing, distribution and retail margins this model does not cost. Even at wholesale, the hybrid product still leaves out forming and texturization, and its inclusion rate and filler cost are conventions, so it is an ingredient-cost scenario, not a parity result (<a href="docs.html#chicken-comparison">comparison point</a>).</div>```````{ojs}//| echo: false{ // Dry cost and the hybrid-product cost at equal size; the wet basis shows one number. // Built per card: a DOM node can only appear in one place. const meatTag = () => html`<span title="${basisLabels.hybridNote}" style="cursor: help; text-decoration: underline dotted;">${basisLabels.hybrid} ⓘ</span>`; const costs = (dryCost, meatCost) => basisLabels.dry ? html`<h2 style="margin: 0.5rem 0 0;">$${Math.round(dryCost)}<span style="font-size: 0.55em; font-weight: 400;">/kg dry cell mass</span></h2><h2 style="margin: 0 0 0.5rem;">$${modelControls.formatMeatEquivalent(meatCost)}<span style="font-size: 0.55em; font-weight: 400;">/kg ${meatTag()}</span></h2>` : html`<h2 style="margin: 0.5rem 0;">$${Math.round(dryCost)}/kg wet</h2>`; return 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>${costs(stats.p50, stats.t_p50)}<small>${basisLabels.dry ? "" : "Per kg wet biomass at harvest. "}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>${costs(stats.p5, stats.t_p5)}<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>${costs(stats.p95, stats.t_p95)}<small>95% of simulations cheaper</small></div></div><div style="background: #f4f6f8; border-left: 4px solid #7f8c8d; padding: 0.8rem 1rem; margin-bottom: 0.5rem; font-size: 0.9em;">${basisLabels.dry ? html`<strong>Two numbers, one cost.</strong> Dry cell mass is the model's basis: it does not change with washing or dewatering and is the natural unit for nutrients. The second number is what that cost means for a product: a kg of hybrid product with ${(results.basis.cell_inclusion_rate * 100).toFixed(0)}% cultivated cells (counted at chicken-meat moisture, ${(results.basis.target_solids_fraction * 100).toFixed(1)}% solids) and the rest plant-based ingredients at $${results.basis.filler_cost}/kg. Ingredient cost only. All-cell meat at chicken-meat moisture would be <strong>$${modelControls.formatMeatEquivalent(quantile(results.unit_cost_meat_equiv, 0.5))}/kg</strong> (median). Change the inclusion rate under <em>Hybrid product</em> in the sidebar.` : ""} <strong>Other bases:</strong> median <strong>$${quantile(results.unit_cost_protein, 0.5).toFixed(0)}/kg protein</strong> (${(results.basis.protein_fraction_of_solids * 100).toFixed(0)}% of dry mass) and <strong>$${quantile(results.unit_cost_wet, 0.5).toFixed(1)}/kg wet biomass</strong> at ${(results.basis.solids_fraction * 100).toFixed(0)}% solids, the headline basis before October 26, 2026.${basisLabels.dry ? "" : html` On the default dry basis: <strong>$${quantile(results.unit_cost_dry, 0.5).toFixed(0)}/kg dry cell mass</strong> and <strong>$${modelControls.formatMeatEquivalent(quantile(results.unit_cost_hybrid, 0.5))}/kg ${basisLabels.hybrid}</strong>.`} The solids and protein shares are placeholders; washing losses are not modeled. Change them, or switch the headline basis, under <em>Structural stress tests</em>. <a href="docs.html#output-basis">Why the basis matters</a></div></div>`;}``````{ojs}//| echo: false// Save / share bar: copy the current shareable link, or export the full// simulation (CSV of all 30,000 samples, or a compact JSON with the// parameters + summary stats + component means). Buttons are plain DOM so// we can attach click handlers that read the current `results` / `stats`// closure — the cell re-runs on every results change, which is fine.{ function downloadBlob(content, filename, mime) { const blob = new Blob([content], {type: mime}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); } const stamp = () => new Date().toISOString().slice(0, 10); function downloadCSV() { const cols = ["unit_cost", "cost_media", "cost_recf", "cost_supp_protein", "cost_other_var", "cost_capex", "cost_fixed", "cost_cdmo_toll", "cost_downstream", "cost_harvest", "unit_cost_dry", "unit_cost_wet", "unit_cost_meat_equiv", "unit_cost_protein"]; const metaCols = ["model_version", "seed", "sample_count", "effective_params_json"]; const quote = v => '"' + String(v).replaceAll('"', '""') + '"'; const meta = [results.model_version, results.seed, results.sample_count].map(quote); const rows = [[...cols, ...metaCols].join(",")]; const n = results.unit_cost.length; for (let i = 0; i < n; i++) { // The full configuration appears once, on the first sample row. rows.push([...cols.map(c => results[c][i].toFixed(4)), ...meta, i === 0 ? quote(JSON.stringify(results.effective_params)) : '""'].join(",")); } downloadBlob(rows.join("\n"), `cm_pq_results_${stamp()}.csv`, "text/csv"); } function downloadJSON() { const config = { model_version: costModel.MODEL_VERSION, review_provenance: results.provenance, structural_comparison: structural_comparison_rows, effective_simulation_parameters: results.effective_params, warnings: results.warnings, generated_at: new Date().toISOString(), dashboard_url: window.location.origin + window.location.pathname, shareable_link: window.location.href, parameters: { simpleMode, blending_share, filler_cost, include_capex, include_fixed_opex, include_downstream, include_harvest_dewater, cdmo_mode, cdmo_toll_p5, cdmo_toll_p95, plant_capacity, uptime, maturity, target_year, p_hydro, p_recfactors, gf_progress, wacc_lo, wacc_hi, asset_life_lo, asset_life_hi, density_lo, density_hi, media_turnover_lo, media_turnover_hi }, summary_stats_unit_cost_per_kg: { n_simulations: stats.n, p5: +stats.p5.toFixed(2), p50: +stats.p50.toFixed(2), p95: +stats.p95.toFixed(2), prob_under: Object.fromEntries(stats.probs.map(d => [d.threshold, +d.prob.toFixed(1)])), cost_unit: basisLabels.cost, threshold_unit: basisLabels.threshold }, process_mode: { rule: results.effective_params.mode_selection, shares: {fedbatch: results.pct_fedbatch, perfusion: results.pct_perfusion, continuous: results.pct_continuous}, selection_stats: results.mode_selection_stats }, component_means_per_kg: { media: +mean(results.cost_media).toFixed(2), growth_factors: +mean(results.cost_recf).toFixed(2), supplemental_proteins: +mean(results.cost_supp_protein).toFixed(2), other_voc: +mean(results.cost_other_var).toFixed(2), capex_annualized: +mean(results.cost_capex).toFixed(2), fixed_opex: +mean(results.cost_fixed).toFixed(2), cdmo_toll: +mean(results.cost_cdmo_toll).toFixed(2), forming_per_kg_product: +mean(results.forming_per_kg_product).toFixed(2), harvest_wash_dewater: +mean(results.cost_harvest).toFixed(2) }, median_per_kg_by_basis: { dry_cell_mass: +quantile(results.unit_cost_dry, 0.5).toFixed(2), chicken_meat_equivalent: +quantile(results.unit_cost_meat_equiv, 0.5).toFixed(2), protein: +quantile(results.unit_cost_protein, 0.5).toFixed(2), wet_biomass: +quantile(results.unit_cost_wet, 0.5).toFixed(2), basis: results.basis }, notes: { units: `USD per kg; headline ${basisLabels.cost}; thresholds ${basisLabels.threshold}`, currency_basis: "Mixed source-year USD; not yet normalized to a common real-dollar year", conditioning: "Intended estimand (decided 2026-10-26): unconditional and hypothetical cost of a large plant built with the best technology available in the target year, whether or not such plants exist; parameter ranges not yet re-checked against this target", basis_change: "Since engine 2026-10-26.4 the default basis is kg dry cell mass (density inputs g dry/L; $/kg inputs per kg dry). cost_basis 'wet' reproduces the earlier wet-basis results", simulation_seed: 42, monte_carlo_samples: 30000 } }; downloadBlob(JSON.stringify(config, null, 2), `cm_pq_config_${stamp()}.json`, "application/json"); } const btnStyle = "padding: 0.45rem 0.85rem; font-size: 0.85rem; border: 1px solid #5a7a5a; border-radius: 4px; background: #e8f4e8; color: #2d4a2d; cursor: pointer; white-space: nowrap; font-weight: 500;"; const container = html`<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; margin: 0 0 1.25rem 0; padding: 0.6rem 0.85rem; background: #fbfcf9; border: 1px solid #d4e0d4; border-radius: 6px;"> <strong style="font-size: 0.85rem; color: #2d4a2d; margin-right: 0.25rem;">Save / share this scenario:</strong> <button data-btn="copy" style="${btnStyle}" title="Copy a URL that captures every slider and toggle value. Paste it anywhere to restore the exact scenario.">📋 Copy shareable link</button> <button data-btn="csv" style="${btnStyle}" title="Download all 30,000 Monte Carlo samples as CSV (unit cost + every component).">⬇ Results CSV</button> <button data-btn="json" style="${btnStyle}" title="Download a compact JSON with your parameter values, summary percentiles, and component means.">⬇ Config + summary JSON</button> <span data-status="" style="font-size: 0.82rem; font-weight: 500;"></span> </div>`; const statusEl = container.querySelector("[data-status]"); function flash(msg, ok = true) { statusEl.textContent = msg; statusEl.style.color = ok ? "#27ae60" : "#c0392b"; setTimeout(() => { statusEl.textContent = ""; }, 2200); } container.querySelector("[data-btn=copy]").addEventListener("click", () => { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(window.location.href) .then(() => flash("✓ Link copied to clipboard")) .catch(() => flash("× Clipboard denied — copy the URL from the address bar", false)); } else { flash("× Clipboard API unavailable — copy the URL from the address bar", false); } }); container.querySelector("[data-btn=csv]").addEventListener("click", () => { try { downloadCSV(); flash("✓ CSV downloaded"); } catch (e) { flash("× CSV export failed: " + e.message, false); } }); container.querySelector("[data-btn=json]").addEventListener("click", () => { try { downloadJSON(); flash("✓ JSON downloaded"); } catch (e) { flash("× JSON export failed: " + e.message, false); } }); return container;}```*Tip: the URL in your address bar updates live as you move sliders, so the "Copy shareable link" button always reflects the scenario you're currently viewing.*### Cost Distribution```{ojs}//| echo: falsePlot = import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm")``````{ojs}//| echo: false{ const uc = results.unit_cost; const clipVal = quantile(uc, 0.98); const clipped = uc.filter(x => x <= clipVal); const p20 = quantile(uc, 0.20); const p80 = quantile(uc, 0.80); // Helper: build the distribution chart at a given pixel width/height function makeDistChart(w, h, fontSize) { const yLabel1 = Math.round(h * 6.0); const yLabel2 = Math.round(h * 7.5); return Plot.plot({ width: w, height: h, marginLeft: 60, marginBottom: 50, marginTop: basisLabels.marginTop, x: { label: `Cell Biomass Manufacturing Cost (${basisLabels.cost})`, domain: [0, clipVal * 1.05] }, y: { label: "Frequency", grid: true }, style: { fontSize: fontSize || 13 }, marks: [ ...basisLabels.meatAxes(clipVal * 1.05), Plot.rectY(clipped, Plot.binX({y: "count"}, {x: d => d, fill: "steelblue", fillOpacity: 0.7})), // p5 / p50 / p95 coloured lines 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"}), // p20 / p80 grey dashed Plot.ruleX([p20], {stroke: "#888", strokeWidth: 1.5, strokeDasharray: "4,4", strokeOpacity: 0.85}), Plot.ruleX([p80], {stroke: "#888", strokeWidth: 1.5, strokeDasharray: "4,4", strokeOpacity: 0.85}), // Reference cost thresholds ($10 and $25, hybrid product on the dry basis) Plot.ruleX([basisLabels.toCostAxis(10)], {stroke: "darkgreen", strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}), Plot.ruleX([basisLabels.toCostAxis(25)], {stroke: "orange", strokeWidth: 2, strokeDasharray: "2,2", strokeOpacity: 0.6}), // US wholesale boneless chicken; omitted if the filler alone costs more (off the axis) ...(basisLabels.chickenAxis > 0 ? [ Plot.ruleX([basisLabels.chickenAxis], {stroke: "#8e44ad", strokeWidth: 2, strokeDasharray: "6,3", strokeOpacity: 0.85}), Plot.text([{x: basisLabels.chickenAxis, y: Math.round(h * 3.0), text: `wholesale chicken $${basisLabels.chicken.central.toFixed(2)}`}], {x: "x", y: "y", text: "text", textAnchor: "start", dx: 4, fontSize: (fontSize || 12) - 2, fill: "#8e44ad"}) ] : []), // Labels Plot.text([ {x: stats.p5 + 1.5, y: yLabel1, text: `p5: $${stats.p5.toFixed(1)}`}, {x: stats.p50 + 1.5, y: yLabel2, text: `p50: $${stats.p50.toFixed(1)}`}, {x: stats.p95 + 1.5, y: yLabel1, text: `p95: $${stats.p95.toFixed(1)}`}, {x: p20 + 1.5, y: Math.round(h * 4.5), text: `p20: $${p20.toFixed(1)}`, fill: "#666"}, {x: p80 + 1.5, y: Math.round(h * 4.5), text: `p80: $${p80.toFixed(1)}`, fill: "#666"} ], {x: "x", y: "y", text: "text", fontSize: (fontSize || 12) - 1, fill: d => d.fill || "black"}) ], title: `Projected ${target_year} Cultured Chicken Production Cost Distribution` }); } const wrapper = document.createElement("div"); wrapper.style.cssText = "position:relative; display:inline-block; width:100%;"; // Fullscreen expand button const fsBtn = document.createElement("button"); fsBtn.textContent = "⛶"; fsBtn.title = "Expand to full screen (press Escape to close)"; fsBtn.style.cssText = "position:absolute; top:6px; right:6px; z-index:10; padding:3px 7px; font-size:15px; cursor:pointer; border:1px solid #ccc; border-radius:4px; background:rgba(255,255,255,0.9); line-height:1;"; // Fullscreen overlay const overlay = document.createElement("div"); overlay.id = "dist-chart-overlay"; overlay.style.cssText = "display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:white; z-index:9500; padding:2rem; box-sizing:border-box; overflow:auto;"; const closeBtn = document.createElement("button"); closeBtn.textContent = "✕ Close"; closeBtn.style.cssText = "position:fixed; top:16px; right:20px; padding:6px 14px; font-size:14px; cursor:pointer; border:1px solid #ccc; border-radius:6px; background:#f8f9fa; z-index:9501;"; closeBtn.onclick = () => { overlay.style.display = "none"; }; overlay.appendChild(closeBtn); // Keyboard close document.addEventListener("keydown", e => { if (e.key === "Escape" && overlay.style.display !== "none") overlay.style.display = "none"; }); fsBtn.onclick = () => { overlay.style.display = "block"; // Build large chart at near-full viewport size const w = Math.min(window.innerWidth - 80, 1600); const h = Math.min(window.innerHeight - 120, 900); // Clear old chart while (overlay.children.length > 2) overlay.removeChild(overlay.lastChild); overlay.appendChild(makeDistChart(w, h, 14)); }; document.body.appendChild(overlay); wrapper.appendChild(makeDistChart(800, 400, 12)); const tailNote = document.createElement("p"); tailNote.style.cssText = "font-size:0.85em;color:#666;"; tailNote.textContent = "Histogram shows the lowest 98% of draws. Summary statistics and threshold probabilities use every draw."; wrapper.appendChild(tailNote); wrapper.appendChild(fsBtn); return wrapper;}```<details><summary>How 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 (ingredient cost only; not a price-parity line)- **Orange dotted**: \$25/kg reference- **Purple dashed**: US wholesale boneless chicken, \$3.50/kg (average September 2022 to September 2026 in 2026 dollars; [USDA ERS](https://www.ers.usda.gov/data-products/livestock-and-meat-domestic-data) wholesale prices). This is the comparison point for conventional chicken: the same stage of the supply chain as the model's factory-gate cost, not the retail shelf price (about \$9.27/kg).On the default dry basis the reference lines are per kg of *hybrid product* (25% cells): \$10 and \$25 sit at about \$113 and \$331 per kg dry cell mass, and wholesale chicken at about \$18 per kg dry. Because the plant-based filler alone costs \$2.25 per kg of hybrid product, the cells have to cost less than about \$18 per kg dry for the hybrid to match wholesale chicken. On the wet basis the purple line compares wet cells directly with chicken meat. [Why wholesale, and the range →](docs.qmd#chicken-comparison)The **width** of the distribution shows uncertainty. Narrow = confident. Wide = uncertain.</details><a id="in-house-vs-cdmo-comparison"></a>```{ojs}//| echo: false{ if (!cdmo_mode || !inhouse_results) return html``; const uc_cdmo = results.unit_cost; const uc_inhouse = inhouse_results.unit_cost; const clipVal = Math.min(quantile(uc_inhouse, 0.97), basisLabels.dry ? 1250 : 250); const cdmo_p5 = quantile(uc_cdmo, 0.05); const cdmo_p50 = quantile(uc_cdmo, 0.50); const cdmo_p95 = quantile(uc_cdmo, 0.95); const ih_p5 = quantile(uc_inhouse, 0.05); const ih_p50 = quantile(uc_inhouse, 0.50); const ih_p95 = quantile(uc_inhouse, 0.95); const diff_p50 = cdmo_p50 - ih_p50; const diffSign = diff_p50 >= 0 ? "+" : "−"; const diffAbs = Math.abs(diff_p50).toFixed(1); // Build plot data: subsample to 10k each for performance const step = Math.floor(30000 / 10000); const plotData = []; for (let i = 0; i < 30000; i += step) { if (uc_cdmo[i] <= clipVal) plotData.push({x: uc_cdmo[i], mode: "CDMO"}); if (uc_inhouse[i] <= clipVal) plotData.push({x: uc_inhouse[i], mode: "In-House"}); } const compPlot = Plot.plot({ width: 820, height: 340, marginLeft: 60, marginBottom: 50, marginTop: basisLabels.marginTop, x: {label: `Manufacturing Cost (${basisLabels.cost})`, domain: [0, clipVal * 1.02]}, y: {label: "Frequency", grid: true}, color: { domain: ["In-House", "CDMO"], range: ["#3498db", "#e67e22"], legend: true }, marks: [ ...basisLabels.meatAxes(clipVal * 1.02), Plot.rectY(plotData.filter(d => d.mode === "In-House"), Plot.binX({y: "count"}, {x: "x", fill: "#3498db", fillOpacity: 0.55})), Plot.rectY(plotData.filter(d => d.mode === "CDMO"), Plot.binX({y: "count"}, {x: "x", fill: "#e67e22", fillOpacity: 0.55})), Plot.ruleX([ih_p50], {stroke: "#3498db", strokeWidth: 2.5}), Plot.ruleX([cdmo_p50], {stroke: "#e67e22", strokeWidth: 2.5}), Plot.ruleX([basisLabels.toCostAxis(10)], {stroke: "darkgreen", strokeWidth: 1.5, strokeDasharray: "3,3", strokeOpacity: 0.6}), Plot.ruleX([basisLabels.toCostAxis(25)], {stroke: "orange", strokeWidth: 1.5, strokeDasharray: "3,3", strokeOpacity: 0.6}), Plot.text([ {x: ih_p50 + 1.5, y: 900, text: `In-House p50: $${ih_p50.toFixed(0)}`}, {x: cdmo_p50 + 1.5, y: 750, text: `CDMO p50: $${cdmo_p50.toFixed(0)}`} ], {x: "x", y: "y", text: "text", fontSize: 12}) ], title: `In-House vs. CDMO: Projected ${target_year} Cost` }); const rows = [ {label: "p5 (optimistic)", ih: ih_p5, cdmo: cdmo_p5}, {label: "p50 (median)", ih: ih_p50, cdmo: cdmo_p50}, {label: "p95 (pessimistic)", ih: ih_p95, cdmo: cdmo_p95} ]; const tbody = document.createElement("tbody"); rows.forEach(r => { const d = r.cdmo - r.ih; const dStr = (d >= 0 ? "+" : "−") + "$" + Math.abs(d).toFixed(0); const dColor = d >= 0 ? "#c0392b" : "#27ae60"; const tr = document.createElement("tr"); tr.innerHTML = `<td style="padding:5px 8px;">${r.label}</td>` + `<td style="padding:5px 8px; text-align:right; color:#3498db;"><strong>$${r.ih.toFixed(0)}</strong></td>` + `<td style="padding:5px 8px; text-align:right; color:#e67e22;"><strong>$${r.cdmo.toFixed(0)}</strong></td>` + `<td style="padding:5px 8px; text-align:right; color:${dColor};"><strong>${dStr}</strong></td>`; tbody.appendChild(tr); }); return html`<div style="margin: 1.5rem 0;"> <h3 style="margin-bottom:0.3rem;">In-House vs. CDMO Comparison</h3> <p style="font-size:0.88em; color:#555; margin-top:0;">Overlapping distributions for the same VOC parameters. Difference = CDMO − In-House; negative means CDMO is cheaper.</p> ${compPlot} <table style="margin-top:1rem; font-size:0.9em; border-collapse:collapse; min-width:360px;"> <thead><tr style="border-bottom:2px solid #ddd;"> <th style="padding:5px 8px; text-align:left;"></th> <th style="padding:5px 8px; text-align:right; color:#3498db;">In-House</th> <th style="padding:5px 8px; text-align:right; color:#e67e22;">CDMO</th> <th style="padding:5px 8px; text-align:right;">Difference</th> </tr></thead> ${tbody} </table> <p style="font-size:0.82em; color:#777; margin-top:0.6rem;"> Both runs use seed 42 and identical VOC parameters. The difference isolates the capital cost structure: in-house (sampled CAPEX + fixed OPEX) vs. CDMO toll (lognormal p5=$${cdmo_toll_p5}, p95=$${cdmo_toll_p95} per kg dry cell mass). </p> </div>`;}```### Probability ThresholdsThese percentages answer: "What's the chance that production costs fall below key price points?" — based on the parameter distributions you've set above. On the default dry basis the price points apply to a kg of **hybrid product**: 25% cultivated cells (counted at chicken-meat moisture, 27.5% solids) and 75% plant-based ingredients at \$3/kg, both adjustable in the sidebar. Each card also gives the same price point per kg dry cell mass. These are ingredient costs; forming, packaging and margins are excluded. If you switch *Structural stress tests → Cost basis* to wet, the price points (\$10 to \$100) apply per kg of wet cell biomass instead, as they did before October 26, 2026.```{ojs}//| echo: false{ function card(threshold, prob, color) { const borderColor = prob > 30 ? color : '#ddd'; const what = basisLabels.dry ? "Hybrid product" : "Cells (wet)"; return `<div class="card" style="border: 2px solid ${borderColor}; padding: 1rem; border-radius: 8px; text-align: center;"> <h5 style="margin-bottom:0.3rem;">P(${what} < $${threshold}/kg${basisLabels.dry ? ` = $${Math.round(basisLabels.toCostAxis(threshold))}/kg dry` : ""})</h5> <h2 style="color: ${color}; margin:0.2rem 0;">${prob.toFixed(1)}%</h2> <small style="color:#555;">illustrative ingredient-cost threshold</small> </div>`; } const colors = ['#27ae60', '#3498db', '#f39c12', '#e74c3c']; const grid = `<div class="grid" style="grid-template-columns: repeat(4, 1fr); gap: 1rem; margin: 1.5rem 0;"> ${stats.probs.map((d, i) => card(d.threshold, d.prob, colors[i])).join("")} </div>`; const c = basisLabels.chicken; const share = t => results.threshold_cost.filter(x => x < t).length / results.threshold_cost.length * 100; const what = basisLabels.dry ? "hybrid product" : "wet cells"; const fmt = p => p < 0.05 && p > 0 ? "under 0.1%" : `${p.toFixed(1)}%`; const chickenLine = `<p style="font-size:0.92em; margin:-0.5rem 0 1.5rem;" title="${basisLabels.chickenNote.replace(/"/g, '"')}"><strong>Compared with conventional chicken at wholesale:</strong> P(${what} < $${c.central.toFixed(2)}/kg, US wholesale boneless chicken) = <strong>${fmt(share(c.central))}</strong>; across the middle 80% of monthly prices, $${c.low.toFixed(1)}–$${c.high.toFixed(1)}/kg, it is ${fmt(share(c.low))} to ${fmt(share(c.high))}. <span style="color:#666;">The $5–$50 cards are illustrative cost levels; none is a parity line. Retail chicken (about $${c.retail.toFixed(2)}/kg) is not a like-for-like comparison.</span></p>`; return html([grid + chickenLine]);}```### Cost Breakdown**Where does the cost come from?** This chart shows the **mean** contribution of each cost component across all 30,000 simulations (mean, not median — skewed components like growth factors will appear larger here than at the median). The largest bars are the cost drivers to focus on — these are where technological progress or parameter uncertainty has the most impact. [How each component is calculated →](docs.html#total-unit-cost)```{ojs}//| echo: false{ const mediaLabel = bundled_media ? "Complete Media (all proteins included)" : "Media (incl. basal micros)"; const allComponents = [ {name: mediaLabel, value: mean(results.cost_media), color: "#27ae60"}, {name: "Growth Factors", value: mean(results.cost_recf), color: "#9b59b6"}, {name: "Supplemental Proteins", value: mean(results.cost_supp_protein), color: "#e67e22"}, {name: "Other VOC", value: mean(results.cost_other_var), color: "#7f8c8d"}, {name: "CAPEX (annualized)", value: mean(results.cost_capex), color: "#e74c3c"}, {name: "Plant overhead OPEX", value: mean(results.cost_fixed), color: "#f39c12"}, {name: "CDMO Toll", value: mean(results.cost_cdmo_toll), color: "#e67e22"}, {name: "Harvest, wash & dewater", value: mean(results.cost_harvest), color: "#16a085"} ]; // 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); const chartContainer = document.createElement("div"); chartContainer.style.position = "relative"; // Expand/collapse button const expandBtn = document.createElement("button"); expandBtn.textContent = "Expand Chart"; expandBtn.style.cssText = "padding: 0.3rem 0.7rem; font-size: 0.8rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f8f9fa; margin-bottom: 0.5rem;"; let expanded = false; // Chart wrapper: clear-and-append avoids replaceWith()+querySelector on SVG // which fails silently in Safari (classList.add on SVG unreliable in WebKit). const chartWrapper = document.createElement("div"); expandBtn.onclick = () => { expanded = !expanded; expandBtn.textContent = expanded ? "Collapse Chart" : "Expand Chart"; chartWrapper.innerHTML = ""; chartWrapper.appendChild(makeChart(expanded)); }; chartContainer.appendChild(expandBtn); function makeChart(large) { const w = large ? 1200 : 1000; const h = large ? 700 : 580; const fontSize = large ? 14 : 13; const xMax = Math.max(...components.map(c => c.value)) * 1.05; return Plot.plot({ width: w, height: h, marginLeft: 200, marginRight: 140, marginTop: basisLabels.marginTop, x: { label: `Average Cost (${basisLabels.cost})`, grid: true, domain: [0, xMax] }, y: { label: null }, marks: [ ...(basisLabels.dry ? [Plot.gridX(), ...basisLabels.meatAxes(xMax)] : []), 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: fontSize }) ], title: `Cost Breakdown by Component (Total: $${Math.round(total)}/kg)` }); } chartWrapper.appendChild(makeChart(false)); chartContainer.appendChild(chartWrapper); return chartContainer;}```<details><summary>Understanding the cost components</summary>**Variable Operating Costs (VOC):**- **[Media](learn.html#media-composition-40-70-of-total-cost)**: Nutrient broth for cell growth (amino acids, glucose, vitamins, minerals, trace salts) — basal micronutrients are included here, not as a separate line item- **[Recombinant](learn.html#step-5-growth-factors-a-key-cost-driver)**: Growth factors (proteins signaling cell division)- **Other VOC**: Utilities, consumables, waste disposal**Capacity-anchored Costs (per-plant, scale sub-linearly with plant size):**- **CAPEX**: Capital costs (reactors, facilities) annualized via Capital Recovery Factor- **Plant overhead OPEX**: Labor, maintenance, plant overhead (conventionally called "Fixed OPEX" — total scales sub-linearly with plant size, so per-kg overhead falls as plants grow)The relative sizes shift dramatically based on technology adoption assumptions.</details>### Component Distributions<details><summary>Show per-component distributions (click to expand)</summary>Individual distributions for each cost driver, updating dynamically as you adjust parameters.```{ojs}//| echo: false// Format cost with appropriate precisionfunction 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: bundled_media ? "Complete Media (all proteins included)" : "Media (incl. basal micros)", data: results.cost_media, color: "#27ae60"}, {name: "Growth Factors", data: results.cost_recf, color: "#9b59b6"}, {name: "Supplemental Proteins (albumin/transferrin/insulin)", data: results.cost_supp_protein, color: "#e67e22"}, {name: "Other VOC", data: results.cost_other_var, color: "#7f8c8d"}, {name: "CAPEX (annualized)", data: results.cost_capex, color: "#e74c3c"}, {name: "Plant overhead OPEX", data: results.cost_fixed, color: "#f39c12"}, {name: "CDMO Toll", data: results.cost_cdmo_toll, color: "#e67e22"}, {name: "Harvest, wash & dewater", data: results.cost_harvest, color: "#16a085"} ]; // 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 uncertainThese distributions are generated from the simulation using the parameters you set in the sidebar above. When you adjust the sliders, these distributions update accordingly.</details></details><details><summary style="cursor:pointer; color: #666; font-size: 0.9em; margin: 0.5rem 0 1.5rem;">Realized adoption rates (click to expand)</summary>*Actual shares across the 30,000 draws. Near-identical to slider inputs at default maturity (0.5); deviates when maturity ≠ 0.5, because the maturity distribution shifts each draw's adoption probability before sampling. Process mode shares always equal slider inputs exactly and are omitted here.*```{ojs}//| echo: falsehtml`<div style="display:flex; gap:2rem; flex-wrap:wrap; font-size:0.95em; padding: 0.5rem 0;"> <span>Hydrolysates: <strong style="color:#27ae60;">${(results.pct_hydro * 100).toFixed(0)}%</strong> of draws</span> <span>Cheap GFs: <strong style="color:#9b59b6;">${(results.pct_recf_cheap * 100).toFixed(0)}%</strong> of draws</span></div>````</details>### Sensitivity analysis (tornado chart): association under the current uncertainty model {#sensitivity-analysis-tornado-chart}Which parameters have the most impact on the final cost? Each bar shows the **dollar swing** in mean unit cost between simulations where the parameter is in its top 10% versus its bottom 10%. Larger bars indicate stronger associations under the selected assumptions. This is one of several ways to ask the question, and not the most robust one; the [sensitivity methods page](sensitivity.qmd) explains what the bars do and do not measure and compares alternatives on the same scenario.```{ojs}//| echo: falsesensitivityMethods = import(new URL("./sensitivity-methods.mjs", window.location.href).href)``````{ojs}//| echo: false{ const uc = results.unit_cost; // The displayed inputs and their activity rules live in sensitivity-methods.mjs, // shared with the sensitivity page. L/kg and the two regime switches are absent // by design: L/kg is density × media-use multiplier, and the switches sit inside // the Media $/L and GF bars. const activeParams = sensitivityMethods.displayedInputs(results, settledParams); // Compute signed swings: positive = high param → higher cost (red); negative = high param → lower cost (green) const swingsRaw = activeParams.map(p => ({ name: p.name, kind: p.kind, rawSwing: conditionalSwing(p.data, uc, 0.10) })); const swings = swingsRaw.map(s => ({ name: s.name, kind: s.kind, swing: Math.abs(s.rawSwing), // Red: high value raises cost; Green: high value lowers cost fill: s.rawSwing >= 0 ? "#c0392b" : "#27ae60" })); const sorted = [...swings].sort((a, b) => b.swing - a.swing); const maxAbs = Math.max(...sorted.map(s => s.swing), 1); const pad = maxAbs * 0.25; const tornadoPlot = Plot.plot({ width: 900, height: 440, marginLeft: 290, marginRight: 120, x: { label: `Dollar swing in mean unit cost (${basisLabels.cost}) — larger bar = stronger cost association`, domain: [0, maxAbs + pad], grid: true, labelOffset: 40, tickFormat: d => "$" + d.toFixed(0) }, y: { label: null, tickFormat: d => d, tickSize: 0 }, style: { fontSize: "13px" }, marks: [ Plot.barX(sorted, { y: "name", x: "swing", fill: d => d.fill, fillOpacity: 0.80, sort: {y: "-x"} }), Plot.text(sorted, { y: "name", x: d => d.swing + maxAbs * 0.02, text: d => "$" + d.swing.toFixed(1) + "/kg", textAnchor: "start", fontSize: 12, fontWeight: 500 }) ] }); return html`<div style="font-size: 1em;"> <div style="font-weight: normal; font-size: 1.05em; margin-bottom: 0.5rem; color: #333;">Which sampled inputs are associated with higher or lower costs?</div> ${tornadoPlot} </div>`;}```*Red bars: high values occur with higher costs. Green bars: high values occur with lower costs. Bar length = conditional-mean dollar swing between top 10% vs. bottom 10% of that parameter's distribution across the 30,000 Monte Carlo draws. This is an association, not an intervention effect or variance decomposition. Correlated inputs overlap; bars should not be summed or interpreted as research value. Industry Maturity is a latent variable whose bar captures the bundled effect of correlated improvements. Bars of a few dollars per wet kilogram (about five times that per dry kilogram) are within simulation noise.* [Full methodology →](docs.html#sensitivity-analysis-dollar-swing-metric){target="_blank"}```{=html}<p style="margin:0.5rem 0 1.5rem;"> <a href="sensitivity.html" onclick="this.href = 'sensitivity.html' + window.location.search;" style="display:inline-block; padding:6px 14px; background:#f0f8ff; color:#1a5276; border:1px solid #3498db; border-radius:6px; text-decoration:none; font-size:13px;">Analyze this scenario on the sensitivity methods page →</a> <span style="font-size:0.85em; color:#666; margin-left:0.5rem;">Other tail widths, other output statistics, conditioning versus intervention, expected uncertainty after learning one input, rank regression, and Shapley effects.</span></p>```### Structural comparison {#structural-comparison}These runs keep your other inputs and seed (42) fixed. Each row changes one structural choice relative to your selected scenario. Fresh-media endpoints and GF concentration use the values in **Structural stress tests**. Comparisons reflect those provisional inputs as well as model form; they are not empirical validation or a variance decomposition. Overrides may make a comparison inactive; see the notes column.```{ojs}//| echo: falsestructural_comparison_rows = { const alternatives = [ ["Selected scenario", {}], ["Shared maturity", {dependence_mode: "shared"}], ["Independent maturity channels", {dependence_mode: "independent_maturity"}], ["Density-derived media", {media_use_model: "density"}], ["Direct fresh media", {media_use_model: "direct"}], ["Media from nutrient yield", {media_use_model: "yield"}], ["Separate GF dose prior", {gf_dose_model: "per_kg"}], ["Media-linked GF dose", {gf_dose_model: "media_linked"}], ["Mode drawn by weights", {mode_selection: "mixture"}], ["Cheapest feasible mode", {mode_selection: "least_cost"}] ]; // Up to nine extra engine runs (three engine passes each under cheapest-feasible // selection). Yield to the browser between runs so input stays responsive, and // stop early if the inputs change. let stale = false; invalidation.then(() => { stale = true; }); const rows = []; for (const [scenario, changes] of alternatives) { await new Promise(resolve => setTimeout(resolve, 0)); if (stale) return rows; const unchanged = Object.entries(changes).every(([key, value]) => results.effective_params[key] === value); const r = unchanged ? results : simulate(30000, 42, {...settledParams, ...changes}); rows.push({scenario, p50: quantile(r.unit_cost, 0.5), p90: quantile(r.unit_cost, 0.9), under25: 100 * r.threshold_cost.filter(c => c < 25).length / r.threshold_cost.length, notes: r.warnings.join(" ")}); } return rows;}html`<div class="structural-comparison-table"><style>.structural-comparison-table table { table-layout:fixed; width:100%; border-collapse:collapse; }.structural-comparison-table th, .structural-comparison-table td { white-space:normal; vertical-align:top; line-height:1.3; padding:.4rem .55rem; border-bottom:1px solid #ddd; text-align:left; }@media(max-width:600px) { .structural-comparison-table th, .structural-comparison-table td { padding:.35rem .2rem; font-size:.8rem; overflow-wrap:anywhere; } }</style><table> <colgroup><col style="width:27%"><col style="width:13%"><col style="width:13%"><col style="width:13%"><col style="width:34%"></colgroup> <caption>Same seed and settings; one structural choice changed per row.</caption> <thead><tr><th scope="col">Structure</th><th scope="col">p50 ($/kg ${basisLabels.costShort})</th><th scope="col">p90 ($/kg ${basisLabels.costShort})</th><th scope="col">P(<$25 ${basisLabels.thresholdShort}), %</th><th scope="col">Override notes</th></tr></thead> <tbody>${structural_comparison_rows.map(r => html`<tr><th scope="row">${r.scenario}</th><td>${r.p50.toFixed(2)}</td><td>${r.p90.toFixed(2)}</td><td>${r.under25.toFixed(1)}</td><td>${r.notes || "None"}</td></tr>`)}</tbody></table></div>````[Why these alternatives, and what remains unmodeled](review-response-2026-09.qmd#structures).### Industry Maturity Deep DiveThe **maturity factor** is a continuous variable ranging from 0 (nascent industry) to 1 (fully mature) that correlates multiple model inputs. It is not binary — intermediate values represent partial industry development. Higher maturity means:- Higher technology adoption rates (hydrolysates, scalable GFs, affordable supplemental proteins)- Lower reactor costs (more custom/food-grade equipment)- Lower financing costs (WACC)::: {.callout-warning appearance="minimal"}**Not calibrated to observed historical trajectories.** The maturity slider is a forward-looking judgment parameter — it does not incorporate data on how fast CM costs have actually fallen since 2015. The model's parameter ranges (e.g., media $/L, GF $/g) were set from TEA literature snapshots (Humbird 2021, Pasitka 2024, GFI 2025 amino acid report) — point estimates, not a fitted time series. The year → maturity scaling formula (linear from 2024 to 2044) is mechanical convenience, not a regression on industry data.**What this means:** "maturity = 0.5" does not mean "the historical rate of progress continues." It means "the model's default assumption is that by 2036 the industry lands roughly in the middle of the plausible range." Whether that default is consistent with observed progress since 2020 is a substantive question this model cannot answer on its own — and a good one to raise with experts at the workshop.**Correlation structure caveat (important for interpretation):** The model uses a single shared latent factor to induce correlation across technology adoption, reactor costs, and financing. This is an *ad hoc modelling convenience* — not an empirically calibrated copula. The specific sensitivity coefficients (e.g., ±0.25 shifts to adoption probabilities, ±0.03 shifts to WACC) and the choice of a single dimension are choices made for tractability, not derived from data.**What this means for interpretation:**- Scenarios where all dimensions co-move together (high maturity = everything goes well; low maturity = everything is hard) will be overrepresented relative to a fully independent model- Scenarios where, say, bioprocess technology matures rapidly but financing remains difficult are partly suppressed- **Stress test:** Set the maturity slider to 0.1 (pessimistic) and 0.9 (optimistic) and compare — the difference captures the full correlated scenario range. Then check whether P(cost < $25/kg) changes materially- Setting the maturity mean to 0.5 does not remove dependence: maturity still varies across draws. A true independence stress test must change dependence while preserving marginal assumptions.Use **Structural stress tests → Maturity dependence** to compare shared and independent maturity channels. The explanations below describe the shared baseline. In independent-channel mode the displayed reference maturity distribution is the common marginal distribution; no single maturity draw drives the whole plant scenario.:::```{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 dry cells/L)", domain: [0, Math.min(quantile(results.density_samples, 0.98) * 1.1, 70)] }, 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(1)} g dry/L, 90% CI: ${denP5.toFixed(1)} – ${denP95.toFixed(1)})`; 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) if (settledParams.dependence_mode !== "shared") return html`<p>The common-maturity scatter plot is omitted: independent-channel mode has no single maturity draw driving total cost.</p>`; 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 (${basisLabels.cost})`, domain: [0, Math.min(quantile(results.unit_cost, 0.95), basisLabels.dry ? 750 : 150)], grid: true }, color: { label: "Cell Density (g dry/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 factorsThis is an assumed dependence structure. Technical success with difficult financing, and the reverse, remain plausible scenarios.</details>:::---## Quick Start1. **Explore the "Learn" sections** at the top to understand how cultured chicken production works and how this model handles uncertainty.2. **Adjust parameters** in the left sidebar. Each parameter has an expandable explanation.3. **Results update automatically** as you change the Basic Parameters sliders — the summary cards, cost distribution, and probability thresholds all recalculate. (The Component Distributions section shows the underlying distributions and updates when you change the relevant sliders.)4. **Navigate the results** to see: - **Cost Distribution**: Full probability distribution with key percentiles - **Probability Thresholds**: Chances of hitting key price points - **Cost Breakdown**: Which components drive costs - **Technology Adoption**: Realized adoption rates## Pivotal Questions ContextThis 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 factory-gate costs approach **US wholesale chicken (about \$3.50/kg of product) by 2036**: large-scale displacement of conventional chicken becomes plausible → prioritize support for industry scale-up- If costs remain **far above it** (say, over \$50/kg): focus shifts to alternative interventions (plant-based, policy, etc.)Both sides of that comparison are at the factory gate: the model's cost and the wholesale price of boneless chicken ([comparison point](docs.qmd#chicken-comparison)), not the retail price.The pivotal question framework helps prioritize research and forecasting efforts where they matter most.See: [The Unjournal's Pivotal Questions documentation](https://globalimpact.gitbook.io/the-unjournal-project-and-communication-space/pivotal-questions):::**The cultured chicken pivotal question asks:**> What will be the production cost of cultured chicken by the projection year, and what does this imply for animal welfare interventions?**Key links:**| Resource | Description ||----------|-------------|| [Pivotal Questions overview](https://globalimpact.gitbook.io/the-unjournal-project-and-communication-space/pivotal-questions) | Full documentation on the PQ framework || [Cultured meat on Metaculus](https://www.metaculus.com/questions/?search=cultured%20meat) | Forecasting questions with community predictions || [The Unjournal](https://unjournal.org) | Independent evaluations of impactful research || [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 |---## Model formulas::: {.callout-note}**[View Full Model Formulas page](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}}$Jump to each term on the formulas page: [Total unit cost](docs.html#total-unit-cost) · [VOC (media, growth factors, other)](docs.html#variable-operating-costs-voc) · [Growth factors](docs.html#growth-factor-cost) · [Annualized CAPEX](docs.html#annualized-capital-costs) · [Maturity and adoption](docs.html#maturity-factor) · [Parameter definitions](docs.html#parameter-definitions) · [Equations at a glance](docs.html#model-formulas)The model runs **30,000 Monte Carlo simulations**, sampling from parameter distributions and technology adoption switches to produce cost distributions and probability thresholds.:::---## Key InsightsThe [versioned September audit](review-2026-09.qmd#what-the-revised-defaults-imply) compares the current defaults across process scenarios and multiple random seeds. Use the live results above for your selected settings. The largest remaining questions concern coherent process productivity and medium requirements, commercial-success conditioning, and evidence for the joint priors.---::: {.callout-warning collapse="true"}## Limitations & Caveats**Model status (reviewed July 20, 2026):** This model is largely AI-generated and we cannot yet vouch for all parameter values. It is provided to fix ideas, give a sense of the sort of modeling we're interested in, and present a framework for discussion and comparison — not a definitive cost estimate. We welcome scrutiny and suggestions via [Hypothesis annotations](https://hypothes.is/) or [email](mailto:contact@unjournal.org).1. **Static snapshot model** — Projects costs for a single projection year (adjustable 2026-2050). Does not model year-over-year learning curves; instead, the "maturity" parameter serves as a proxy for cumulative industry development.2. **Forming and texturization are outside the estimate** — Forming can be added to the hybrid product via toggle (\$0.5–2.5 per kg of product, GFI 2021; scaffolded whole cuts are not modeled). Harvest, washing and dewatering belong inside the estimate but are not yet modeled explicitly.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 & How They Inform the Model::: {.callout-warning collapse="true"}## Honesty note on sourcingThis model was largely AI-generated. We have grounded key parameter ranges in published TEAs and recently revised the pharma-grade media cost range downward (from $1-4/L to $0.50-2.50/L) based on GFI's 2025 amino acid supplier data. However, **we have not yet systematically verified that every hardcoded range traces to a specific source.** Cycle days is the main remaining parameter that still lacks a direct citation; growth factor quantities and the media-use multiplier were both tightened in April 2026 based on GFI 2023 / Humbird / Pasitka primary sources. This is one reason we are seeking expert review.:::::: {#micronutrient-change-note .callout-note collapse="true"}## Model change log — basal micronutrients folded into media (April 2026)Earlier versions of this model carried a separate **food-grade micronutrient** cost line (vitamins, minerals, trace elements) with its own adoption toggle, usage range (0.1–10 g/kg), and price range (\$0.02–20/g). External review flagged two problems:1. **Category confusion / double counting.** The page already describes basal media as containing "amino acids, glucose, vitamins," and the model comments described media as "basal media, excluding growth factors." A separate line for vitamins/minerals was therefore adding a second charge for the same inputs.2. **Ranges not coherent with the literature.** [O'Neill et al. (2021)](https://ift.onlinelibrary.wiley.com/doi/abs/10.1111/1541-4337.12678) describe vitamin sourcing as a "less pressing issue" and note required minerals can "generally be obtained relatively inexpensively." Meanwhile [Pasitka et al. (2024)](https://www.nature.com/articles/s43016-024-01022-w) report an ACF-medium supplement (vitamins + minerals) at roughly 0.151 kg per kg wet biomass — orders of magnitude above the slider's 0.1–2 g/kg range. So the old slider was not tracking any coherent category.**What changed:** The separate slider and cost line were removed. Basal vitamins, minerals, and trace salts are now included inside media \$/L along with amino acids and glucose, consistent with how the media-cost literature is reported. Supplemental **recombinant proteins** (insulin, transferrin, albumin) remain unmodeled as a standalone line for now; if we reintroduce a separate uncertainty term, it will be repurposed to these proteins where per-g prices and per-kg usage can be sourced.:::<details><summary>Sources that directly informed model structure and parameter ranges (click to expand)</summary>| Source | What it informed in the model | How ||--------|------|-----|| [Risner et al. (2021)](https://www.mdpi.com/2304-8158/10/1/3) | Reference plant scale (20 kTA), reactor cost structure | Default plant capacity; CAPEX scaling reference || [Humbird (2021)](https://doi.org/10.1002/bit.27848) | Pharma-grade media costs ($1-4/L), reactor costs ($50-500/L), scale exponents (0.6-0.9) | Directly used for pharma-grade parameter ranges in simulate() || [O'Neill et al. (2021)](https://ift.onlinelibrary.wiley.com/doi/abs/10.1111/1541-4337.12678) | Serum-free media cost benchmarks | Informed hydrolysate media cost range ($0.20-1.20/L) || [GFI amino acid report (2025)](https://gfi.org/resource/amino-acid-cost-and-supply-chain-analysis-for-cultivated-meat/) | Pharma-grade media cost revised downward ($1-4/L → $0.50-2.50/L) | Real supplier quotes found Humbird AA prices 2-10x too high; since AA dominates basal media cost, this lowers the pharma range || [GFI recombinant-protein cost analysis (2023)](https://gfi.org/wp-content/uploads/2023/01/GFI-report_Anticipated-growth-factor-and-recombinant-protein-costs-and-volumes-necessary-for-cost-competitive-cultivated-meat_2023-1.pdf) | **Growth-factor quantity range (tightened April 2026)** | Reproduces Humbird formulation (FGF 0.1 mg/L, TGFβ 0.002 mg/L) and cost-competitive media-use assumption (8–13 L/kg); drives the revised 0.0005–0.006 g/kg quantity range || [GFI State of Industry (2024)](https://gfi.org/resource/cultivated-meat-eggs-and-dairy-state-of-the-industry-report/) | Growth factor cost targets, technology status | Informed GF price regime bounds and adoption probability defaults |</details><details><summary>Sources cited for context but not directly integrated into parameter values (click to expand)</summary>| Source | Relevance ||--------|-----------|| [Pasitka et al. (2024)](https://www.nature.com/articles/s43016-024-01022-w) | Optimistic TEA ($6/lb); also provides an empirical ACF chicken process figure of ~0.00187 g growth factor/kg wet biomass, which is used as a **cross-check** for the GFI-derived quantity range (April 2026) || [Goodwin et al. (2024)](https://www.nature.com/articles/s43016-024-01061-3) | Scoping review comparing TEAs — informed our understanding of where TEAs disagree || [CE Delft (2021)](https://cedelft.eu/publications/tea-of-cultivated-meat/) | European cost benchmarks — cited in documentation but not directly used for parameter ranges || [Lever VC (2025)](https://www.levervc.com/) | Industry claims on cell density (60-90 g/L) — cited as context for density range upper bound || [The Unjournal evaluations](https://unjournal.pubpub.org/pub/evalsumlimitedmeadprod/) | Independent evaluation of RP forecasting paper — informed our project framing |</details><details><summary>Parameters where source grounding is weakest — review priority (click to expand)</summary>- Supplemental recombinant proteins (insulin, transferrin, albumin) are now broken out as a separate cost line in separable-media mode. Their ranges remain provisional and need supplier quotes or process-linked dosage evidence. In bundled complete-medium mode they are included in media $/L and are not added again.- Growth factor quantities — **tightened April 2026** from a very broad 0.0001–0.02 g/kg stress-test range to a cited 0.0005–0.006 g/kg default, derived from the Humbird medium formulation reproduced in [GFI 2023](https://gfi.org/wp-content/uploads/2023/01/GFI-report_Anticipated-growth-factor-and-recombinant-protein-costs-and-volumes-necessary-for-cost-competitive-cultivated-meat_2023-1.pdf) (FGF 0.1 mg/L + TGFβ 0.002 mg/L) × media-use assumptions of 8–60 L/kg, and cross-checked against [Pasitka et al. (2024)](https://www.nature.com/articles/s43016-024-01022-w)'s ~0.00187 g/kg empirical ACF figure. Both dosage and price remain uncertain; the current per-kg dosage draw is not coupled to realized medium use.- Media-use multiplier — **renamed and tightened April 2026** from "Media turnover (1–10×)" to "Media-use multiplier (0.5–3.0×)". The rename flags that the parameter is a bundled ratio (fresh media / nominal reactor volume), not a literal count of reactor-volume changes, so values below 1 (recycling, fed-batch, explicit medium recovery) are now allowed. The new range lets the model represent GFI 2023 cost-competitive scenarios (8–13 L/kg ≈ 0.5–1.2× at 60–90 g/L); the old floor of 1.0 excluded some combinations at fixed density. See [Learn: media-use mechanisms](learn.html#media-use-mechanisms) for the derivation.- Cycle days (0.5-5.0 days) — hardcoded without source attribution- Plant factor (1.5-3.5×) — standard engineering multiplier range, not CM-specificSee the [Technical Documentation](docs.qmd) for detailed parameter definitions and the expandable details under each slider for parameter-specific discussion.</details>