# When will ME be cured? Open-source forecast model (updated October 19, 2026)

Source: https://lcmedata.org/cure
Built: 2026-10-19
Licence: MIT. You may use, change and share this model. Please credit lcmedata.org.

This file has four parts:
1. A prompt to paste into your AI assistant
2. What the model forecasts, and every default assumption
3. The full model code (engine.js), the exact code the web page runs
4. The data the defaults come from

---

## 1. Prompt for your AI assistant

Copy everything in this file into ChatGPT, Claude, Gemini or another assistant, then add this prompt:

> I am sharing an open-source model that forecasts when a clinical trial will first show a cure for ME (myalgic encephalomyelitis). It comes from lcmedata.org/cure. Please read the whole file: the event definition, the assumptions and the code.
>
> Then help me with these tasks:
> 1. Explain how any part of the model works, in plain language, when I ask.
> 2. Run scenarios. When I change an assumption, change the matching value in DEFAULTS (or the constant in the code), run the model if you can run JavaScript, and tell me the new median year and the chance of a cure by 2030, 2035 and 2040. If you cannot run code, say so and reason from the code instead.
> 3. Challenge the assumptions. Tell me which inputs are weakest, what evidence would improve them, and how much each one could move the answer.
>
> Keep the definition of a cure fixed unless I ask you to change it. Tell me clearly when a number is data and when it is a judgement.

---

## 2. What the model forecasts

**The event (a "cure"):** a registered, placebo-controlled trial shows that a treatment puts at least half of all people with ME into remission. Remission means all three: they no longer meet the criteria for ME; they have no significant post-exertional malaise (PEM); they have at least 90% of their function from before they got ill. If a trial enrolls only patients picked by a biomarker, its remission rate counts for that share of ME. The date is the day the result becomes public, including as a preprint. Approval, price and access are not part of the event.

**Second date (a cure for at least one subgroup):** the same result in a biomarker-defined group that is at least 1 in 10 people with ME. The run() output calls it `sub`.

**How it works.** The model plays out 10,000 possible futures in steps of 3 months, from October 2026 to 2050. In each step:

- Chance of finding a candidate treatment (a new or repurposed drug that works for some patients) = starting rate x amount of research (compared with 2025) x AI speed-up.
- Each candidate treatment waits for a funded trial slot, then runs a trial of 40 patients. The result becomes public one trial timeline later.
- The trial's remission rate for all of ME is the share of ME the treatment works for times how well it works. If at least 20 of 40 reach remission on that basis, that future has its cure date.
- Trials of drugs that do not work also run, and a few of them show false results (rare, because the cure needs a placebo-controlled trial).
- Trial reform (default: some reform) adds drug trials, some of which find real candidates, and shortens the trial timeline.
- A simpler model (a cure comes a few years after expert-level AI) is computed as a comparison (`simple`), not blended in. 3% of futures never reach a cure before 2050.
- The page reports the median of the 10,000 cure dates.

**Default assumptions** (the `DEFAULTS` object and constants in the code):

| Input | Default | Data or judgement |
|---|---|---|
| Starting rate, no biomarker | 0.35% per year (0 cures in 108,885 papers across 8 diseases with no biomarker, scaled to ME's 772 papers in 2025) | data |
| Starting rate, with a biomarker | 1.05% per year (10 cures in 772,008 papers across 15 diseases with a biomarker) | data |
| How hard ME is compared with these diseases | same (baseMult 1) | judgement |
| Later candidate treatments | about one every 6 years after the first, before research and AI multipliers | data (gap between first and next treatments in other diseases) |
| Human research growth | 18% per year continues (mechanistic ME papers, 3-year growth 2023-2026, 2026 at full-year pace; 2019-2025 was 12%) | data, extended by judgement |
| Extra funding | +$25M a year beyond the paper trend (Germany's program from 2026, new funders); 70% to research at $150k per paper, 30% to trials at $3M per trial; no effect for 18 months, full by 2033; diminishing returns that ease by 2036 | judgement, from rough funding data |
| Expert-level AI (median) | early 2030: half of futures from the AI Futures Project (AI 2027 team) forecasts, half from Manifold "AGI before [year]" markets | data (forecasts) |
| AI speed-up before expert-level AI | 1.25x now, rising to 3x at arrival | judgement, from AI Futures uplift estimates |
| AI speed-up after arrival | 10x within 0-2 years, then 30x, then 100x, capped at 10x (choices: 3x, 10x, 100x) | judgement |
| Biomarker validated | median early 2032 (25% by end of 2028, 55% by end of 2032, then about 7-13% per year); the slider moves the median | judgement (past rate would give about 5% by 2028) |
| Trials that use the biomarker to pick patients | 80% | judgement |
| Share of patients a treatment works for | 26% on average (first cures in 8 diseases with mixed patient groups), drawn fresh per candidate from Beta(3, 8.5) | data |
| Ceiling on one drug's reach | set once per future from Beta(3, 4), mean 43% of all people with ME; 30% of futures have no ceiling (one shared mechanism) | judgement |
| Combinations | two or three drugs that each reach 10-50% can be combined; the combination trial starts 6 months after the last part reports, and 70% of combinations are safe to use together | judgement |
| "How hard ME is" multiplier spread | shared lognormal, sd 0.8 in log, mean 1 | judgement |
| Hard-ME futures | in 20% of futures every drug reaches only a quarter as much of ME | judgement |
| Preclinical and setup before a trial | half of candidates are new compounds: 3.5 years today, 2.0 after expert-level AI (animal safety, manufacturing, phase 1; published candidate-to-IND ranges are 18-36 months and phase 1 adds about a year). The other half are repurposed drugs: 1.0 year, 0.5 after AI (funding, ethics, opening a site). Trial reform cuts the wait: new compounds by 15% (some) or 30% (major), repurposed drugs by 40% or 60% | judgement, from published IND timelines |
| Immune-reset tools from other diseases | 5% chance they reach the all-ME cure bar, 15% a subgroup cure; result between 2029.5 and 2037, most likely 2032 | judgement |
| Trial timeline | 2.8 years today (2.0 years to run, measured on 23 completed ME drug trials, plus 0.8 years to a preprint or first report, assumed), 1.5 years with strong AI | data and judgement |
| Trial capacity | 1.5 new drug trials a year today, growing with the field and with the square root of AI | data today, judgement after |
| Trial reform | some reform: 2x drug trials, trial timeline 5 months shorter (half with strong AI), 0.5% chance each extra trial finds a real candidate, 5% faster discovery from failed-trial data; phases in 2028-2032 (options: none, some, major = 4x, 10 months, 15%) | judgement, from clinical trial abundance sources |
| False signals | 1 in 5,000 trials of a drug that does not work shows 50% remission (placebo-controlled) | judgement |
| Cost and reform uncertainty | cost per paper and per trial vary about 0.65-1.55x between futures; the reform hit chance varies about 0.45-2.2x | judgement |

**Results with the defaults (updated October 19, 2026):** a cure for ME in 2041 (median); about 1% by 2030, 19% by 2035, 50% by 2040, 82% by 2050. A cure for at least one subgroup: 2036. The largest levers are when expert-level AI arrives, how much AI can speed up biology, research growth and trial reform.

**How to run it.** Save the code in part 3 as `engine.js`, then with Node.js:

```
node -e "const CM=require('./engine.js'); const s=CM.summary(CM.run({},10000).T3); console.log(s.median, s.by(2035))"
node -e "const CM=require('./engine.js'); const s=CM.summary(CM.run({aiMedian:2035, capW:[1,0,0]},10000).T3); console.log(s.median)"
```

`run(params, N)` returns arrays of dates for each future: T3 is the cure (50% remission), T2 and T1 are easier milestones (25% and 10%), T4 is a second trial confirming a 25% result. `summary(x)` gives the median, the 10th and 90th percentiles, and `by(year)`, the chance by the end of that year.

---

## 3. The full model code (engine.js)

```javascript
/*
 * ME cure forecast - model engine (JavaScript port of remission_model_v8.py).
 * Runs in the browser and in Node (for validation against the Python model).
 *
 * Each simulation has its own seeded random streams, so a change to one
 * slider changes only the paths that depend on it. The headline number then
 * moves smoothly instead of jumping with random noise.
 */
(function (root) {
  'use strict';

  var NOW = 2026.75, END = 2050.0, HORIZON = 2200.0;
  var NSTEP = Math.round((END - NOW) / 0.25);
  var LOGV = [0, Math.log(3), Math.log(10), Math.log(30), Math.log(100)];
  // AI tools already speed up research before expert level. 1.25x in late 2026: coding runs about 2x faster
  // (AI Futures Project, Aug 2026), and about 40% of a research cycle is thinking work. Rises log-linearly to 3x at T.
  var AI_NOW = 1.25;
  var FUND_COST = 0.15, PAPERS_2025 = 383;   // $M per mechanistic paper; mechanistic ME papers in 2025
  var TRIAL_COST = 3, TRIAL_SHARE = 0.3;     // $M per academic ME drug trial; share of extra money that funds trials
  var CAPS = [3, 10, 100];
  var THRESH = [0.10, 0.25, 0.50];
  var SPURIOUS = [0.03, 0.01, 0.002];
  var TRIALS_PER_YR = 1.5, N_TRIAL = 40;
  // stress test, 17 Oct 2026:
  // - half of candidates are new compounds, which need animal safety work, manufacturing and a phase 1
  //   before an ME trial (2.5 years today, 1.5 after expert-level AI); approved drugs need funding and ethics (0.5, 0.25)
  // - one drug reaches at most a share of ME set once per future (Beta(3,4), mean 43%); 30% of futures have no ceiling
  // - drugs that each help a different part of ME can be combined; 70% of combinations are safe to use together
  // - immune-reset tools from other diseases (CAR-T and similar): 5% chance of a cure, 15% of a subgroup cure, 2029.5-2037
  var NEW_SHARE = 0.5, D_NEW = [3.5, 2.0], D_REP = [1.0, 0.5];
  // trial reform cuts this wait: most of a repurposed drug's wait is funding, ethics and opening a site,
  // which reform targets directly. A new compound's animal dosing periods cannot be reformed away.
  var RF_NEW = [1, 0.85, 0.7], RF_REP = [1, 0.6, 0.4];
  var CEIL_NONE = 0.3, COMBO_OK = 0.7, OUT_CURE = 0.05, OUT_SUB = 0.15;

  var AUTHORS = [[2028.9, 0.58], [2031.3, 0.90], [2032.5, 1.00]];
  var MANIFOLD = [[2026.75, 0.00], [2027, 0.02], [2028, 0.27], [2029, 0.43],
    [2030, 0.50], [2031, 0.55], [2032, 0.58], [2033, 0.60], [2034, 0.61],
    [2035, 0.63], [2036, 0.68], [2037, 0.69], [2038, 0.70], [2039, 0.71],
    [2040, 0.72], [2041, 0.73], [2042, 0.76], [2043, 0.78], [2044, 0.80],
    [2045, 0.83], [2046, 0.84], [2047, 0.85], [2048, 0.87], [2060, 0.93]];
  var AII_T0 = 2023.8, AII_MU = Math.log(2047.0 - 2023.8);
  var AII_SD = (AII_MU - Math.log(2027.0 - 2023.8)) / 1.2816;

  var DEFAULTS = {
    aiW: [1, 1, 0],            // AI Futures authors, Manifold, (AI Impacts 2023: dropped 23 Sep 2026)
    capW: [0, 1, 0],           // AI speed-up for biology capped at 3x, 10x, 100x; our guess 10x (was a 20/30/50 mix until 26 Sep 2026)
    markerMode: 'piecewise',   // or 'historical'
    p28: 0.25, p32: 0.55,      // P(marker replicates) by end 2028 / end 2032
    pEnrich: 0.8,              // P(a trial selects patients on the marker); fixed, not a page control
    lagNow: 2.8,               // trial start to public result today: 2.0 run (23 completed ME drug trials) + 0.8 to preprint (assumed); 26 Sep 2026
    lagAi: 1.5,                // same, once AI is strong (center of a 1-year range)
    spurMult: 0.1,             // false results: 10x rarer because the cure needs a placebo-controlled trial (26 Sep 2026)
    effortR0: 0.1655,          // research growth rate now: ln 1.18, 3-year trailing growth of mechanistic papers 2023-2026 (2026 at full-year pace); 26 Sep 2026
    effortREnd: 0.1655,        // long-run rate: steady 18%, the 3-year trailing growth 2023-2026
    effortTau: 8,              // years for most of that easing
    extraFund: 25,             // $M a year beyond the 2019-2025 trend: Germany's program and new funders (26 Sep 2026)
    baseMult: 1.0,             // how hard ME is compared with other diseases
    rSymptom: 0.0035,          // cure rate/yr, symptom-defined diseases (0 cures / 108,885 papers)
    rMarker: 0.0105,           // cure rate/yr, all 15 diseases with a test (10 cures / 772,008 papers)
    wA: 1, block: 0.03,        // wA: share of futures from the main model; the simple model is shown separately (review, 26 Sep 2026)
    reform: 1                  // trial reform: 0 none, 1 some (our guess), 2 major
  };

  /* ------------------------------------------------------------ random */
  function Rng(a, b, c, d) {
    this.a = a >>> 0; this.b = b >>> 0; this.c = c >>> 0; this.d = d >>> 0;
    for (var i = 0; i < 12; i++) this.u();
  }
  Rng.prototype.u = function () {          // sfc32
    var a = this.a, b = this.b, c = this.c, d = this.d;
    var t = (((a + b) | 0) + d) | 0;
    d = (d + 1) | 0;
    a = b ^ (b >>> 9);
    b = (c + (c << 3)) | 0;
    c = (c << 21) | (c >>> 11);
    c = (c + t) | 0;
    this.a = a; this.b = b; this.c = c; this.d = d;
    return (t >>> 0) / 4294967296;
  };
  function stream(seed, i, k) {
    return new Rng(seed ^ 0x9E3779B9, (i * 2654435761) ^ k, (k * 40503 + 1013904223) ^ i, i + 7 * k + 1);
  }

  // inverse normal CDF (Acklam), one uniform per normal keeps draw counts fixed
  var A = [-3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02, 1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00];
  var B = [-5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02, 6.680131188771972e+01, -1.328068155288572e+01];
  var C = [-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00, -2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00];
  var D = [7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00, 3.754408661907416e+00];
  function invNorm(p) {
    if (p <= 0) p = 1e-12; if (p >= 1) p = 1 - 1e-12;
    var q, r;
    if (p < 0.02425) {
      q = Math.sqrt(-2 * Math.log(p));
      return (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5]) / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1);
    }
    if (p > 1 - 0.02425) {
      q = Math.sqrt(-2 * Math.log(1 - p));
      return -(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5]) / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1);
    }
    q = p - 0.5; r = q * q;
    return (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1);
  }
  function normCdf(x) {                     // Abramowitz-Stegun 7.1.26 via erf
    var t = 1 / (1 + 0.3275911 * Math.abs(x) / Math.SQRT2);
    var y = 1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * Math.exp(-x * x / 2);
    return x >= 0 ? 0.5 * (1 + y) : 0.5 * (1 - y);
  }
  function gamma(r, k) {                    // Marsaglia-Tsang, k >= 1
    var d = k - 1 / 3, c = 1 / Math.sqrt(9 * d);
    for (;;) {
      var x, v;
      do { x = invNorm(r.u()); v = 1 + c * x; } while (v <= 0);
      v = v * v * v;
      var u = r.u();
      if (u < 1 - 0.0331 * x * x * x * x) return d * v;
      if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) return d * v;
    }
  }
  function beta(r, a, b) { var x = gamma(r, a), y = gamma(r, b); return x / (x + y); }
  function poisson(r, lam, max) {
    if (lam <= 0) return 0;
    if (lam > 30) { var k = Math.round(lam + Math.sqrt(lam) * invNorm(r.u())); return Math.max(0, Math.min(k, max)); }
    var L = Math.exp(-lam), n = 0, p = r.u();
    while (p > L && n < max) { n++; p *= r.u(); }
    return n;
  }
  // one uniform per draw (inverse CDF), not n; same distribution, about 10x faster (29 Sep 2026)
  function binom(r, n, p) {
    if (p <= 0) return 0;
    if (p >= 1) return n;
    if (p > 0.5) return n - binom(r, n, 1 - p);
    var u = r.u(), ratio = p / (1 - p), pk = Math.pow(1 - p, n), c = pk, k = 0;
    while (u > c && k < n) { pk *= (n - k) / (k + 1) * ratio; k++; c += pk; }
    return k;
  }

  /* ------------------------------------------------------------ model parts */
  function aiMult(y, T, D, cap) {
    var K1 = T + D + 0.01, K2 = T + D + 2, K3 = T + D + 4, o;
    if (y <= T) { var fr = (y - NOW) / Math.max(T - NOW, 0.01); if (fr < 0) fr = 0; o = Math.log(AI_NOW) + fr * (LOGV[1] - Math.log(AI_NOW)); }
    else if (y <= K1) o = LOGV[1] + (y - T) / (K1 - T) * (LOGV[2] - LOGV[1]);
    else if (y <= K2) o = LOGV[2] + (y - K1) / (K2 - K1) * (LOGV[3] - LOGV[2]);
    else if (y <= K3) o = LOGV[3] + (y - K2) / (K3 - K2) * (LOGV[4] - LOGV[3]);
    else o = LOGV[4];
    return Math.min(Math.exp(o), cap);
  }
  var ebP = null, ebCache = null;           // effortBase is called with the same years many times per run
  function effortBase(y, P) {              // human research relative to 2025, without extra funding
    if (P !== ebP) { ebP = P; ebCache = new Map(); }
    var v = ebCache.get(y);
    if (v !== undefined) return v;
    var t = y > 2025 ? y - 2025 : 0, tau = P.effortTau;
    v = Math.exp(P.effortREnd * t + (P.effortR0 - P.effortREnd) * tau * (1 - Math.exp(-t / tau)));
    if (ebCache.size < 20000) ebCache.set(y, v);
    return v;
  }
  // new money does nothing for about 18 months (calls, review, hiring), then builds to full effect by 2033
  // (was 2031; lengthened 26 Sep 2026 after the funding-lag literature)
  function fundRamp(y) { var r = (y - 2028.5) / 4.5; return r < 0 ? 0 : (r > 1 ? 1 : r); }
  function fundExp(y) { var r = (y - 2028.5) / 7.5; return 0.7 + 0.2 * (r < 0 ? 0 : (r > 1 ? 1 : r)); }
  function effort(y, P, fc) {
    var base = effortBase(y, P);
    if (!P.extraFund) return base;
    // 70% of extra money buys mechanistic papers at FUND_COST $M each, with diminishing returns
    // because experienced ME researchers are scarce: exponent 0.7 at first, easing to 0.9 by 2036 as
    // researchers join from other fields and AI makes experience matter less (added 24 Sep 2026)
    var x = fundRamp(y) * ((1 - TRIAL_SHARE) * P.extraFund / (FUND_COST * (fc || 1))) / (PAPERS_2025 * base);
    return base * Math.pow(1 + x, fundExp(y));
  }
  // trial slots per year for candidate treatments: 1.5 today, growing with the field and with sqrt(AI),
  // plus 30% of extra money at TRIAL_COST per trial
  function trialCapacity(y, P, ai, tc) {
    return TRIALS_PER_YR * effortBase(y, P) * Math.sqrt(ai) * (1 + (reformOf(P)[0] - 1) * reformRamp(y)) + fundRamp(y) * TRIAL_SHARE * (P.extraFund || 0) / (TRIAL_COST * (tc || 1));
  }
  /* trial reform (added 26 Sep 2026): [more drug trials, chance an extra trial finds a real treatment,
     faster discovery from failed-trial data, years cut from the trial timeline]; phases in 2028-2032 */
  var REFORMS = [[1, 0, 0, 0], [2, 0.005, 0.05, 0.4], [4, 0.005, 0.15, 0.8]];
  function reformRamp(y) { var r = (y - 2027.5) / 4; return r < 0 ? 0 : (r > 1 ? 1 : r); }
  function reformOf(P) { return REFORMS[P.reform == null ? 1 : P.reform] || REFORMS[0]; }
  function lagR(y, T, Lai, P) {
    var c = reformOf(P)[3] * reformRamp(y);
    return lagAt(y, T, Lai - c / 2, P.lagNow - c);
  }
  function lagAt(y, T, Lai, Lnow) {
    var f = (y - T) / 2; f = f < 0 ? 0 : (f > 1 ? 1 : f);
    return Lnow + f * (Lai - Lnow);
  }
  function manifoldInv(u) {
    var last = MANIFOLD[MANIFOLD.length - 1];
    if (u > last[1]) return HORIZON;
    for (var i = 1; i < MANIFOLD.length; i++) {
      if (u <= MANIFOLD[i][1]) {
        var p0 = MANIFOLD[i - 1], p1 = MANIFOLD[i];
        return p0[0] + (u - p0[1]) / (p1[1] - p0[1] || 1) * (p1[0] - p0[0]);
      }
    }
    return HORIZON;
  }
  var AII_FNOW = normCdf((Math.log(NOW - AII_T0) - AII_MU) / AII_SD);
  function drawAI(src, u1, u2, u3, u4) {
    if (src === 0) {
      var a = AUTHORS[Math.min(2, Math.floor(u1 * 3))];
      return NOW + (a[0] - NOW) * Math.exp(invNorm(u2) * a[1]);
    }
    if (src === 1) return manifoldInv(u3);
    var p = AII_FNOW + u4 * (1 - AII_FNOW);   // conditioned on not yet arrived
    return AII_T0 + Math.exp(AII_MU + AII_SD * invNorm(p));
  }
  function pick(w, u) {
    var s = 0, i; for (i = 0; i < w.length; i++) s += w[i];
    if (s <= 0) return 0;
    var acc = 0; for (i = 0; i < w.length; i++) { acc += w[i] / s; if (u < acc) return i; }
    return w.length - 1;
  }

  /* ------------------------------------------------------------ run */
  function run(params, N, seed) {
    var P = {}, k;
    for (k in DEFAULTS) P[k] = DEFAULTS[k];
    for (k in (params || {})) P[k] = params[k];
    N = N || 10000; seed = seed || 20260923;

    var T1 = new Float64Array(N), T2 = new Float64Array(N), T3 = new Float64Array(N),
        T4 = new Float64Array(N), MK = new Float64Array(N), AI = new Float64Array(N),
        WD = new Float64Array(N), WS = new Float64Array(N), WK = new Uint8Array(N),   // winning candidate: discovery, trial start, route
        A1 = new Float64Array(N), A2 = new Float64Array(N), A3 = new Float64Array(N),
        A4 = new Float64Array(N), TB = new Float64Array(N), USEA = new Uint8Array(N),
        BLK = new Uint8Array(N), FF = new Float64Array(N), TBo = new Float64Array(N), SUB = new Float64Array(N);
    var lagHalf = 0.5, kAI = aiStretch(P), kMK = markerStretch(P);
    var LaiLo = Math.max(0.5, P.lagAi - lagHalf), LaiHi = P.lagAi + lagHalf;

    for (var i = 0; i < N; i++) {
      /* fixed-count shared draws */
      var r = stream(seed, i, 1);
      var src = pick(P.aiW, r.u());
      var T = drawAI(src, r.u(), r.u(), r.u(), r.u());
      if (T < HORIZON) T = NOW + (T - NOW) * kAI;       // move the median to the chosen year
      if (T < NOW + 0.01) T = NOW + 0.01;
      var Dd = 2 * r.u();
      var cap = CAPS[pick(P.capW, r.u())];
      var Lai = LaiLo + (LaiHi - LaiLo) * r.u();
      var hard = r.u() < 0.2 ? 0.25 : (1 - 0.2 * 0.25) / (1 - 0.2);
      var z = 0.8 * invNorm(r.u()) - 0.32;   // mean of exp(z) is 1, so the published rates stay the mean (was median; 17 Oct 2026)
      var rSym = P.rSymptom * Math.exp(z) * P.baseMult, rMk = P.rMarker * Math.exp(z) * P.baseMult;
      var rpost = Math.exp(Math.log(0.16) + 0.4 * invNorm(r.u()));
      var hpre = 0.003 + 0.007 * r.u();
      var tpre = NOW - Math.log(1 - r.u()) / hpre;
      var bNorm = invNorm(r.u());
      BLK[i] = r.u() < P.block ? 1 : 0;
      USEA[i] = r.u() < P.wA ? 1 : 0;
      var mu = r.u(), mu2 = r.u();
      // uncertainty in costs and in the reform hit chance (review, 26 Sep 2026): 80% ranges about 0.65-1.55x, 0.45-2.2x
      var fc = Math.exp(0.35 * invNorm(r.u())), tc = Math.exp(0.35 * invNorm(r.u())), pr = Math.exp(0.6 * invNorm(r.u()));
      AI[i] = T;

      /* marker date */
      var M = HORIZON, hstart = NOW;
      if (P.markerMode !== 'historical') {
        var p32 = Math.max(P.p32, P.p28);
        if (mu < P.p28) M = NOW + (2029 - NOW) * mu2;
        else if (mu < p32) M = 2029 + 4 * mu2;
        hstart = 2033;
      }
      if (M === HORIZON) {
        var rm = stream(seed, i, 2);
        for (var ym = hstart; ym < END; ym += 0.25) {
          var h = 0.026 * Math.min(effort(ym, P, fc), 5) * Math.sqrt(aiMult(ym, T, Dd, cap));   // grows with research and funding
          if (rm.u() < 1 - Math.exp(-h * 0.25)) { M = ym + 0.25 * rm.u(); break; }
        }
      }
      if (P.markerMode !== 'historical' && P.markerMedian && M < HORIZON) M = NOW + (M - NOW) * kMK;   // move the median
      MK[i] = M;

      /* structure A */
      var rl = stream(seed, i, 3);
      var rc = stream(seed, i, 4);                     // stress-test draws, on their own stream
      var CEIL = rc.u() < CEIL_NONE ? 0.95 : Math.min(beta(rc, 3, 4), 0.95);
      var parts = [], comboDone = false, wDisc = Infinity, wStart = Infinity, wKind = 0;
      var t10 = Infinity, t25 = Infinity, t50 = Infinity, t4 = Infinity, found = false, nextFree = -Infinity, ff = Infinity, tsub = Infinity;
      var minLag = Math.min(Lai, P.lagNow);
      for (var s = 0, sEnd = NSTEP + Math.ceil(P.lagNow * 4); s < sEnd; s++) {
        var y = NOW + 0.25 * s;
        var ys = NOW - P.lagNow + 0.25 * s;              // discovery date; trials already running were found up to one timeline ago
        var ai = aiMult(ys, T, Dd, cap), mk = M <= ys, eff = effort(ys, P, fc);
        var base = found ? rpost : (mk ? rMk : rSym);
        var RF = reformOf(P), rr = reformRamp(ys);
        var xtra = TRIALS_PER_YR * effortBase(ys, P) * Math.sqrt(ai) * (RF[0] - 1) * rr;   // extra reform trials
        var kk = poisson(rl, (base * eff * ai * (1 + RF[2] * rr) + xtra * RF[1] * pr) * 0.25, 5);
        for (var j = 0; j < kk; j++) {
          // wait in line for a funded trial slot
          var ri = P.reform == null ? 1 : P.reform, rfr = reformRamp(ys);
          var isNew = rc.u() < NEW_SHARE;
          var dBase = (isNew ? D_NEW : D_REP)[ys >= T ? 1 : 0];
          var dCut = 1 - (1 - (isNew ? RF_NEW : RF_REP)[ri]) * rfr;
          var ready = ys + dBase * dCut;                 // preclinical / setup time, shortened by reform
          var st0 = ready > nextFree ? ready : nextFree;
          nextFree = st0 + 1 / trialCapacity(st0, P, aiMult(st0, T, Dd, cap), tc);
          var rpt = st0 + lagR(st0, T, Lai, P);          // the timeline that applies when the trial starts
          if (rpt < NOW) continue;                       // would already have reported
          found = true; if (ff === Infinity) ff = ys;     // only finds still in the pipeline count (review round 2)
          var enr = M <= st0 && rl.u() < P.pEnrich;
          var cov = Math.min(beta(rl, P.covA || 3, P.covB || 8.5) * hard, CEIL);   // 2x AI boost removed: it counted better ideas twice (17 Oct 2026)
          var ef = 0.6 + 0.4 * rl.u(), ppv = 0.5 + 0.4 * rl.u();
          // every milestone counts the share of ALL people with ME (26 Sep 2026): a biomarker-selected trial
          // counts its remission rate times the share of ME its group covers, so this is coverage x effect either way
          var tr = cov * ef;
          // subgroup cure: a biomarker-selected trial reaches 50% remission in a group that is at least 1 in 10 people with ME
          if (enr && cov >= 0.1 && binom(rl, N_TRIAL, ef * ppv) / N_TRIAL >= 0.5 && rpt < tsub) tsub = rpt;
          var obs = binom(rl, N_TRIAL, tr) / N_TRIAL;
          if (obs >= 0.10 && rpt < t10) t10 = rpt;
          if (obs >= 0.25 && rpt < t25) t25 = rpt;
          if (obs >= 0.50 && rpt < t50) { t50 = rpt; wDisc = ys; wStart = st0; wKind = isNew ? 1 : 2; }
          if (!comboDone && obs >= 0.10 && obs < 0.50) {   // a partial success: combine the best three
            parts.push({ tr: tr, rpt: rpt });
            parts.sort(function (a, b) { return b.tr - a.tr; });
            var miss = 1, last = 0;
            for (var q = 0; q < parts.length && q < 3; q++) { miss *= 1 - parts[q].tr; if (parts[q].rpt > last) last = parts[q].rpt; }
            if (miss <= 0.5) {
              comboDone = true;
              var cs = last + 0.5, cr = cs + lagR(cs, T, Lai, P);
              if (rc.u() < COMBO_OK && binom(rc, N_TRIAL, 1 - miss) / N_TRIAL >= 0.5 && cr < t50) { t50 = cr; wStart = cs; wKind = 3; }
            }
          }
          if (obs >= 0.25) {                       // independent replication (T4)
            var st = rpt + 0.5 + rl.u();
            var rep = st + lagR(st, T, Lai, P);
            if (binom(rl, N_TRIAL, tr) / N_TRIAL >= 0.25 && rep < t4) t4 = rep;
          }
        }
        if (P.spurMult > 0) {                      // trials of drugs that do not work
          var ntr = poisson(rl, TRIALS_PER_YR * effort(y, P, fc) * Math.sqrt(aiMult(y, T, Dd, cap)) * (1 + (reformOf(P)[0] - 1) * reformRamp(y)) * 0.25, 400);
          if (ntr > 0) {
            // one draw decides the best false result this quarter, so a false 50% also counts as 10% and 25%
            var uf = rl.u(), rl2 = rl.u(), rl3 = rl.u();
            var q50 = 1 - Math.pow(1 - Math.min(SPURIOUS[2] * P.spurMult, 1), ntr),
                q25 = 1 - Math.pow(1 - Math.min(SPURIOUS[1] * P.spurMult, 1), ntr),
                q10 = 1 - Math.pow(1 - Math.min(SPURIOUS[0] * P.spurMult, 1), ntr);
            if (uf < q50) { if (y < t50) t50 = y; if (y < tsub) tsub = y; if (y < t25) t25 = y; if (y < t10) t10 = y; }
            else if (uf < q25) { if (y < t25) t25 = y; if (y < t10) t10 = y; }
            else if (uf < q10) { if (y < t10) t10 = y; }
          }
        }
        // later finds cannot report earlier than the cure, and T10/T25/sub are never later than it, so stop at the cure.
        // T4 (confirmation) is no longer shown and stays a lower bound past this point (29 Sep 2026, speed).
        if (t50 <= ys) break;
      }
      var uo = rc.u(), us = rc.u(), ut = rc.u();          // outside track: one result date, triangular 2029.5-2037, mode 2032
      var tOut = ut < 1 / 3 ? 2029.5 + Math.sqrt(ut * 7.5 * 2.5) : 2037 - Math.sqrt((1 - ut) * 7.5 * 5);
      if (uo < OUT_CURE && tOut < t50) { t50 = tOut; wDisc = Infinity; wStart = Infinity; wKind = 4; }
      if (us < OUT_SUB && tOut < tsub) tsub = tOut;
      WD[i] = wDisc; WS[i] = wStart; WK[i] = wKind;
      A1[i] = t10; A2[i] = t25; A3[i] = t50; A4[i] = t4; FF[i] = ff; SUB[i] = BLK[i] ? Infinity : Math.min(tsub, t50);

      /* structure B (reduced form) */
      var start = T + Dd;
      var med = 2.5 * Math.pow(100 / cap, 0.3);
      // extra funding speeds up the work after AI arrives, as in structure A (added 24 Sep 2026)
      var fB = effort(start, P, fc) / effortBase(start, P) * (1 + reformOf(P)[2] * reformRamp(start));
      var cB = reformOf(P)[3] * reformRamp(start) / 2;
      var tpost = start + med * Math.exp(0.6 * bNorm) / fB + (Lai - cB - 1.5);
      TB[i] = tpre < start ? Math.min(tpre, tpost) : tpost;
    }

    /* B models T2 only: place T1 / T3 / T4 at A's median gap from T2 */
    function gap(X) {
      var d = [];
      for (var i = 0; i < N; i++) if (isFinite(X[i]) && isFinite(A2[i])) d.push(X[i] - A2[i]);
      if (!d.length) return 0;
      d.sort(function (a, b) { return a - b; });
      return d[Math.floor(d.length / 2)];
    }
    var g1 = gap(A1), g3 = gap(A3), g4 = gap(A4);
    for (i = 0; i < N; i++) {
      if (BLK[i]) { T1[i] = T2[i] = T3[i] = T4[i] = Infinity; continue; }
      if (USEA[i]) { T1[i] = A1[i]; T2[i] = A2[i]; T3[i] = A3[i]; T4[i] = A4[i]; }
      else { T2[i] = TB[i]; T1[i] = Math.max(NOW, Math.min(TB[i] + g1, TB[i])); T3[i] = Math.max(TB[i] + g3, TB[i]); T4[i] = Math.max(TB[i] + g4, TB[i]); }
      TBo[i] = BLK[i] ? Infinity : Math.max(TB[i] + g3, TB[i]);
    }
    return { T1: T1, T2: T2, T3: T3, T4: T4, marker: MK, ai: AI, find: FF, simple: TBo, sub: SUB, N: N, wd: WD, ws: WS, wk: WK };
  }

  /* ------------------------------------------------------------ summaries */
  function sorted(X) { var a = Float64Array.from(X); a.sort(); return a; }
  function quantile(S, p) {                    // S sorted; Infinity = not by 2050
    var v = S[Math.min(S.length - 1, Math.floor(p * S.length))];
    return v >= END ? Infinity : v;
  }
  function cdfAt(S, y) {                      // P(event before end of year y)
    var lo = 0, hi = S.length;
    while (lo < hi) { var m = (lo + hi) >> 1; if (S[m] < y + 1) lo = m + 1; else hi = m; }
    return lo / S.length;
  }
  function summary(X) {
    var S = sorted(X);
    return {
      sorted: S,
      median: quantile(S, 0.5), p10: quantile(S, 0.1), p90: quantile(S, 0.9),
      by: function (y) { return cdfAt(S, y); }
    };
  }

  /* factor ranges for the impact lines and the tornado chart */
  var IMPACT_RANGES = {   // each range runs from the first to the last choice on the page, with the same names
    effort: ['Research growth', [{ effortR0: -0.0305, effortREnd: -0.0305 }, 'Shrinks 3%'], [{ effortR0: 0.2151, effortREnd: 0.2151 }, 'Grows 24%']],
    base: ['How hard ME is', [{ baseMult: 0.25 }, 'Much harder'], [{ baseMult: 4 }, 'Much easier']],
    ai: ['When AI arrives', [{ aiMedian: 2045 }, 'median 2045'], [{ aiMedian: 2027 }, 'median 2027']],
    cap: ['AI speed-up limit', [{ capW: [1, 0, 0] }, 'Up to 3x'], [{ capW: [0, 0, 1] }, 'Up to 100x']],
    marker: ['Biomarker validated', [{ markerMedian: 2045 }, 'median 2045'], [{ markerMedian: 2027 }, 'median 2027']],
    fund: ['Extra funding', [{ extraFund: 0 }, '$0'], [{ extraFund: 1000 }, '+$1B']],
    lag: ['Trial timeline with AI', [{ lagAi: 4 }, '4 years'], [{ lagAi: 0.5 }, '6 months']],
    reform: ['Trial reform', [{ reform: 0 }, 'No reform'], [{ reform: 2 }, 'Major reform']]
  };


  var EFFORT_SCENARIOS = [
    ['Shrinks 3%', { effortR0: -0.0305, effortREnd: -0.0305, effortTau: 8 }],
    ['Grows 6%', { effortR0: 0.0583, effortREnd: 0.0583, effortTau: 8 }],
    ['Grows 12%', { effortR0: 0.1133, effortREnd: 0.1133, effortTau: 8 }],
    ['Grows 18% (our guess)', { effortR0: 0.1655, effortREnd: 0.1655, effortTau: 8 }],
    ['Grows 24%', { effortR0: 0.2151, effortREnd: 0.2151, effortTau: 8 }]];
  function effortLevel(y, sc) { var P = {}; for (var k in DEFAULTS) P[k] = DEFAULTS[k]; for (k in sc) P[k] = sc[k]; return effort(y, P); }

  /* median of the piecewise biomarker schedule (p28 by end 2028, p32 by end 2032, spread evenly) */
  function markerBaseMedian(P) {
    var p32 = Math.max(P.p32, P.p28);
    if (P.p28 >= 0.5) return NOW + (2029 - NOW) * 0.5 / P.p28;
    if (p32 >= 0.5) return 2029 + 4 * (0.5 - P.p28) / (p32 - P.p28);
    return null;
  }
  function markerStretch(P) {
    var m0 = markerBaseMedian(P);
    return (P.markerMedian && m0) ? (P.markerMedian - NOW) / (m0 - NOW) : 1;
  }
  function aiImpactsCdf(y) { return y <= NOW ? 0 : (normCdf((Math.log(y - AII_T0) - AII_MU) / AII_SD) - AII_FNOW) / (1 - AII_FNOW); }
  function authorsCdf(y) {
    if (y <= NOW) return 0; var s = 0;
    for (var i = 0; i < 3; i++) s += normCdf(Math.log((y - NOW) / (AUTHORS[i][0] - NOW)) / AUTHORS[i][1]);
    return s / 3;
  }
  function manifoldCdf(y) {
    if (y <= NOW) return 0;
    for (var i = 1; i < MANIFOLD.length; i++) if (y <= MANIFOLD[i][0]) {
      var a = MANIFOLD[i - 1], b = MANIFOLD[i];
      return a[1] + (y - a[0]) / (b[0] - a[0]) * (b[1] - a[1]); }
    return MANIFOLD[MANIFOLD.length - 1][1];
  }
  function mixCdf(y, w) {
    var s = w[0] + w[1] + w[2];
    return (w[0] * authorsCdf(y) + w[1] * manifoldCdf(y) + w[2] * aiImpactsCdf(y)) / s;
  }
  function mixMedian(w) {
    var lo = NOW, hi = 2100;
    for (var i = 0; i < 60; i++) { var m = (lo + hi) / 2; if (mixCdf(m, w) < 0.5) lo = m; else hi = m; }
    return (lo + hi) / 2;
  }
  /* stretch factor that moves the mix's median to the chosen year, keeping its shape */
  function aiStretch(P) { return P.aiMedian ? (P.aiMedian - NOW) / (mixMedian(P.aiW) - NOW) : 1; }
  function yourAiCdf(y, P) { var k = aiStretch(P); return y <= NOW ? 0 : mixCdf(NOW + (y - NOW) / k, P.aiW); }
  DEFAULTS.aiMedian = Math.round(mixMedian(DEFAULTS.aiW) * 100) / 100;
  DEFAULTS.markerMedian = Math.round(markerBaseMedian(DEFAULTS) * 100) / 100;

  var api = { run: run, summary: summary, EFFORT_SCENARIOS: EFFORT_SCENARIOS, effortLevel: effortLevel, DEFAULTS: DEFAULTS, IMPACT_RANGES: IMPACT_RANGES, NOW: NOW, END: END,
              MANIFOLD: MANIFOLD, AUTHORS: AUTHORS, aiImpactsCdf: aiImpactsCdf, authorsCdf: authorsCdf, manifoldCdf: manifoldCdf,
              mixCdf: mixCdf, mixMedian: mixMedian, yourAiCdf: yourAiCdf, FUND_COST: FUND_COST, PAPERS_2025: PAPERS_2025, TRIAL_COST: TRIAL_COST, TRIAL_SHARE: TRIAL_SHARE };
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
  else root.CureModel = api;
})(this);
```

---

## 4. The data behind the defaults

**ME research papers per year** (OpenAlex; papers naming ME or CFS in the title or abstract; "mechanistic" adds biology terms such as pathophysiology, biomarker, immune, mitochondrial, omics):

| Year | All ME papers | Mechanistic papers |
|---|---|---|
| 1990 | 79 | 13 |
| 1991 | 190 | 47 |
| 1992 | 193 | 54 |
| 1993 | 156 | 17 |
| 1994 | 245 | 49 |
| 1995 | 254 | 50 |
| 1996 | 271 | 44 |
| 1997 | 337 | 48 |
| 1998 | 364 | 79 |
| 1999 | 322 | 73 |
| 2000 | 347 | 63 |
| 2001 | 329 | 68 |
| 2002 | 400 | 74 |
| 2003 | 354 | 89 |
| 2004 | 371 | 79 |
| 2005 | 314 | 79 |
| 2006 | 389 | 111 |
| 2007 | 383 | 108 |
| 2008 | 392 | 117 |
| 2009 | 445 | 101 |
| 2010 | 471 | 121 |
| 2011 | 546 | 145 |
| 2012 | 477 | 131 |
| 2013 | 497 | 145 |
| 2014 | 428 | 144 |
| 2015 | 543 | 164 |
| 2016 | 535 | 188 |
| 2017 | 490 | 126 |
| 2018 | 464 | 157 |
| 2019 | 499 | 182 |
| 2020 | 561 | 184 |
| 2021 | 603 | 233 |
| 2022 | 544 | 229 |
| 2023 | 654 | 296 |
| 2024 | 595 | 275 |
| 2025 | 772 | 383 |

**The 23 diseases used for the starting rate** (papers counted in OpenAlex from 1950 to the first cure, or to today if none):

| Disease | Biomarker | First cure | Papers before it |
|---|---|---|---|
| Rheumatoid arthritis | yes | 1988 | 25,389 |
| Hepatitis C | yes | 1991 | 2,159 |
| Hepatitis B (chronic) | yes | 1992 | 18,591 |
| Multiple sclerosis | yes | 1993 | 15,048 |
| Peptic ulcer disease | yes | 1994 | 34,629 |
| HIV/AIDS | yes | 1996 | 42,633 |
| Crohn's disease | yes | 1998 | 12,823 |
| Cystic fibrosis | yes | 2012 | 46,167 |
| Castleman disease (iMCD) | yes | 2014 | 3,370 |
| Chronic urticaria | yes | 2014 | 3,794 |
| Narcolepsy type 1 | yes | none | 11,801 |
| ALS | yes | none | 57,345 |
| Alzheimer's | yes | none | 296,608 |
| Parkinson's | yes | none | 192,462 |
| Sjogren's | yes | none | 9,189 |
| Fibromyalgia | no | none | 31,221 |
| IBS | no | none | 38,468 |
| Gulf War illness | no | none | 1,966 |
| POTS | no | none | 3,165 |
| Post-treatment Lyme | no | none | 720 |
| Interstitial cystitis | no | none | 8,433 |
| Chronic prostatitis | no | none | 7,741 |
| Long COVID | no | none | 17,171 |

"Cure" for these diseases means the first treatment that put a real share of patients into lasting remission or control, such as combination therapy for HIV in 1996. Four diseases with one clear cause (Whipple disease, Lyme arthritis, CIDP, anti-NMDAR encephalitis) were cured within 8 years, after fewer than 200 papers each. They are left out, because ME is past that stage.

**Trials (ClinicalTrials.gov, Sep 2026):** 118 registered ME treatment trials, 43 of them drug trials; 0 pick patients with a biomarker. Median ME drug trial: 1.9 years from start to last patient, then 1.3 years to publication (42 trials). Long COVID and fibromyalgia trials: 1.1-1.2 years start to finish.

**AI-designed drugs:** 80-90% pass safety trials (other drugs about 50%); about 40% pass efficacy trials (other drugs about 40%); about 15 more efficacy results due by the end of 2028.

**Biomarkers:** about 29 candidates proposed since about 2005; none independently validated as a test for one person.

**Funding:** roughly $35-50 million a year worldwide on ME research (NIH about $13 million in 2024, less in 2025; Germany 50 million euros a year from 2026, shared with Long COVID; private funders).

Sources are listed at the bottom of https://lcmedata.org/cure.
