X-risk mitigation value model
  • Model
  • Replication
  • Formulas
  • Departures
  • Context
  • Shared scenarios
  • Comment
  • About
  • The Unjournal ↗

Draft for comment. Built by The Unjournal with AI assistance; not yet reviewed by the report’s author. Comment on anything with Hypothes.is (the < tab at the top right), or see how to comment.

The value of existential risk mitigation: interactive model

Muñoz Morán’s model (Rethink Priorities, 2023), reproduced, with switches for the alternatives raised in The Unjournal’s evaluation

What is it worth to cut the risk of human extinction a little, for a few years? This page works out the answer under the model in Muñoz Morán’s 2023 report for Rethink Priorities, and lets you change each assumption to see how the answer moves. ? Choose settings on the left, or start from a preset.

The answer is the purple box, E(M): the expected value the action adds to the future, counted in years of today’s world. ? (how it’s computed). Each setting in use is tagged by where its value comes from: the 2023 report, Rethink Priorities’ 2026 model, an illustrative choice of ours, or you.

Hover over or tap any ? for an explanation, and use “Hide settings” to give the charts the full width. When you have a scenario you find plausible, you can share it or submit it.

NoteHow to read this page
  • The model in one line. In each year \(t\) the world goes extinct with probability \(r_t\); if it survives, that year produces value \(v_t\). A mitigation multiplies \(r_t\) by \((1-f)\) for \(P\) years. Its value is \(\mathbb{E}(M)=\mathbb{E}(w')-\mathbb{E}(w)\) ?: how much it raises the expected value of the future (formulas).
  • Left panel: pick a risk scenario and a value-growth path, then the mitigation’s efficacy \(f\) (the share of each year’s risk it removes), its persistence \(P\) (how many years its effect on risk lasts) and the horizon \(T\) (the last year counted). Only the settings that matter for your choices are shown. The page opens on the report’s defaults: Time of Perils, constant value, \(f = 0.0001\) (one basis point), \(P = 5\) years. “Hide settings” at the top of the panel folds it away; the “Settings” tab at the left edge brings it back.
  • Beyond the paper (amber box): each switch starts off, so the page opens as a replica of the report. Turn them on one at a time.
  • Units: \(v_c = 1\) is one year of a world like today’s (the report’s footnote 14 suggests roughly 8 billion people at 0.85 QALYs each). All results are multiples of \(v_c\).
  • Results, top to bottom: the headline numbers; how risk and value change over time; when the mitigation’s value arrives; how it depends on persistence; the report’s scenario grid at your settings; the two optional analyses (switches 2 and 4); and which inputs matter most.
  • Share, submit or comment: the share section at the bottom gives a link that reopens these settings, a text summary, downloads, and a form for adding your scenario to the shared list. Comment on anything with Hypothes.is (the < tab at the top right). Hypothes.is annotations are public, so don’t post anything confidential there; and a share link carries your settings, so anyone you send it to sees them.
  • Precision: headline numbers are shown to three significant figures (hover for more digits). The inputs aren’t known nearly that well; the extra digits are there for checking against the report, not because the answer is that precise.
TipWhat the model shows so far (short version)
  1. The report’s numbers reproduce. Under Time of Perils with constant value, \(f = 0.0001\) and \(P = 5\), the mitigation is worth about 0.009 years of today’s world; with cubic value growth, about 55,000. See replication.
  2. Most of the value sits in the far future. Under the report’s default scenario, 95% of \(\mathbb{E}(M)\) arrives after year 500 and 37% after year 10,000, even with constant value: the benefit is a higher chance of reaching the long low-risk era.
  3. Under Rethink Priorities’ 2026 central values (preset below; Earth-bound branch only), the same mitigation is worth about 0.36 years of today’s world, 40 times the report’s default, and 99.6% of it arrives after year 500. A donation tool that discounts the 500+ year window to 0.10 keeps about a tenth of it.
  4. Sign uncertainty matters more than the mean suggests. With a spread on \(f\) equal to its mean and a 10% chance of a delayed backfire (switch 2), there’s about a 19% chance the action is net harmful, while the mean falls only about 11%. The report reports means only.
  5. A pure delay of risk can be worthless or harmful. If the risk you avert is paid back right after the window (switch 3 with \(\beta = 1\), \(D = P\)), the value falls to almost zero with constant value and turns negative with cubic growth, because the risk comes back when each year is worth more.
M = import(new URL("./xrisk-model.mjs", window.location.href).href)
Plot = import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6.17/+esm")
urlParams = window.__XR_URL_STATE__ || {}
// Starting values: the report's defaults, overridden by any ?key=value in the link.
// The engine checks the link's values; anything it had to change is listed on the page.
parsedUrl = M.parseSettings(urlParams)
U = parsedUrl.settings
// Inputs built from the engine's schema, so the page, links and shared scenarios agree on ranges and menus.
rangeIn = (k, label) => { const [lo, hi, step] = M.SETTING_RANGES[k]; return Inputs.range([lo, hi], {value: U[k], step, label}); }
selectIn = (k, label, format = String) => Inputs.select(M.SETTING_OPTIONS[k], {value: U[k], label, format})
fmt = (x, d = 3) => M.fmtSci(x, d)
// Loop rather than Math.min(...arr): spreading a 400,000-element array overflows the call stack.
arrMin = a => { let m = Infinity; for (const x of a) if (x < m) m = x; return m; }
pct = (x, d = 1) => { if (!Number.isFinite(x)) return "n/a"; const s = (100 * x).toFixed(d); return (Number(s) === 0 ? s.replace("-", "") : s) + "%"; }
// Axis ticks: plain numbers in a readable range, otherwise powers of ten (avoids SI prefixes like "2m" for 0.002).
tick = d => { const a = Math.abs(d); if (a === 0) return "0"; if (a >= 0.01 && a < 1e5) return String(+d.toPrecision(3)); return d.toExponential(0).replace("e+", "e"); }
pctTick = d => { const a = Math.abs(d); if (a === 0) return "0%"; if (a >= 1e-7) return (+(100 * d).toPrecision(2)) + "%"; return (100 * d).toExponential(0) + "%"; }
// "?" markers. site.js shows the text on hover, keyboard focus or tap.
tipEl = text => { const s = document.createElement("span"); s.className = "tip"; s.tabIndex = 0; s.setAttribute("role", "button"); s.setAttribute("aria-label", "Explanation: " + text); s.dataset.tip = text; s.textContent = "?"; return s; }
lab = (text, tip) => { const s = document.createElement("span"); s.append(text, " ", tipEl(tip)); return s; }
// Width of the results column. Follows the "Hide settings" toggle and window resizes.
fillWidth = Generators.observe(notify => {
  const el = document.querySelector(".layout-sidebar > .panel-fill") || document.querySelector(".panel-fill");
  if (!el) { notify(document.body.clientWidth); return () => {}; }
  let last = 0, timer = null;
  const measure = () => { const w = Math.round(el.getBoundingClientRect().width); if (w > 0 && Math.abs(w - last) >= 16) { last = w; notify(w); } };
  const later = () => { clearTimeout(timer); timer = setTimeout(measure, 120); };
  // site.js fires a resize event when the settings panel is hidden or shown.
  const ro = new ResizeObserver(later);
  ro.observe(el);
  window.addEventListener("resize", later);
  measure();
  return () => { ro.disconnect(); window.removeEventListener("resize", later); clearTimeout(timer); };
})
w2 = Math.max(300, Math.min(760, (fillWidth - 40) / 2))
w1 = Math.max(320, Math.min(1200, fillWidth - 20))
copyText = async text => {
  try { await navigator.clipboard.writeText(text); return true; }
  catch (e) {
    try {
      const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
      document.body.appendChild(ta); ta.select(); const ok = document.execCommand("copy"); ta.remove(); return ok;
    } catch (e2) { return false; }
  }
}
download = (name, text, type) => {
  const a = document.createElement("a");
  a.href = URL.createObjectURL(new Blob([text], {type}));
  a.download = name; document.body.appendChild(a); a.click();
  setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
}
// Quick start: four plain questions. Each answer sets a few of the detailed settings below,
// and the answer that matches the current settings is highlighted.
{
  const nearestD = P => M.SETTING_OPTIONS.D.reduce((b, o) => Math.abs(Math.log(o / P)) < Math.abs(Math.log(b / P)) ? o : b);
  const vbase = settings.value === "fluctuating" ? settings.fbase : settings.value;
  const Q = [
    {q: "After the dangerous period (the first century), what is the risk of extinction per century?",
      tip: `This sets the long-run risk, which decides how long the future is expected to last; it is the biggest driver of the answer in most settings. During the dangerous period the risk is ${settings.rh}% per century (change that under Risk). Pick an answer or type your own number.`,
      opts: [
        {label: "0.01%", set: {risk: "top", rl: 0.01, T: 400000}, match: ["risk", "rl"]},
        {label: "0.08% (RP 2026)", set: {risk: "top", rl: 0.0833, T: 400000}, match: ["risk", "rl"]},
        {label: "1% (report)", set: {risk: "top", rl: 0.995, T: 120000}, match: ["risk", "rl"]},
        {label: "5%", set: {risk: "top", rl: 5, T: 120000}, match: ["risk", "rl"]},
        {label: `Never falls: ${settings.rh}% every century`, set: {risk: "constant"}}],
      custom: {key: "rl", label: "or type a number, % per century", set: x => ({risk: "top", rl: x}), current: settings.risk === "top" ? settings.rl : ""},
      fine: settings.risk === "constant" ? "rh" : "rl"},
    {q: "How much more could a year in the future be worth than a year today?",
      tip: "This sets the value-growth path. Growth makes later years count for more, and later years are what a small, early cut in risk mostly buys.",
      opts: [
        {label: "The same as today (report)", set: {value: "constant"}},
        {label: "Up to 1.6 times (RP 2026, Earth only)", set: {value: "logistic", cap: 0.2041, gamma: 0.01}},
        {label: "Up to a million times (report, logistic)", set: {value: "logistic", cap: 6, gamma: 0.03}},
        {label: "Growing without limit (report, cubic)", set: {value: "cubic", alpha: 0.01}}],
      fine: vbase === "logistic" ? "cap" : ["linear", "quadratic", "cubic", "polynomial"].includes(vbase) ? "alpha" : "value"},
    {q: `How long does the action's effect on risk (f = ${settings.f}) last?`,
      tip: "This sets persistence P and the start year. Change the size of the effect, f, under Mitigation.",
      opts: [
        {label: "5 years, starting now (report)", set: {P: 5, t0: 1}},
        {label: "11 years, from year 4 (RP 2026)", set: {P: 11, t0: 4}},
        {label: "50 years, starting now (report)", set: {P: 50, t0: 1}}],
      fine: "P"},
    {q: "How sure are you that the action lowers risk?",
      tip: "The report assumes the action always lowers risk by f. The middle answers turn on switch 2: the effect is drawn at random, so it can come out negative, and there can be a later backfire. The last turns on switch 3: all the risk averted comes back after the effect ends. The sizes here are illustrative; fine-tune them in the Beyond panel.",
      opts: [
        {label: "Sure: it always lowers risk (report)", set: {mc: 0, rb: 0}},
        {label: "Fairly sure: 1 in 6 chance it raises risk, 1 in 10 chance of a later backfire as large as the intended effect", set: {mc: 1, rb: 0, fsd: 1, pb: 0.1, bsize: 1}},
        {label: "Unsure: 1 in 3 chance it raises risk, 1 in 4 chance of a later backfire as large as the intended effect", set: {mc: 1, rb: 0, fsd: 2.3, pb: 0.25, bsize: 1}},
        {label: "It only delays risk: the averted risk comes back later", set: {mc: 0, rb: 1, beta: 1, perm: 0, D: nearestD(settings.P)}, match: ["mc", "rb", "beta", "perm"]}],
      note: settings.mc ? "With uncertainty on, the purple box also shows the average effect across draws (the net effect) and the chance of net harm; the full spread is in the Monte Carlo section." : "",
      fine: settings.mc ? "fsd" : settings.rb ? "beta" : "mc"}
  ];
  const on = o => (o.match || Object.keys(o.set)).every(k => String(settings[k]) === String(o.set[k]));
  const wasOpen = this && this.tagName === "DETAILS" ? this.open : true;
  const custom = c => {
    const [lo, hi] = M.SETTING_RANGES[c.key];
    const inp = html`<input type="number" class="qs-custom" min=${lo} max=${hi} step="any" inputmode="decimal" aria-label=${c.label}>`;
    inp.value = c.current;
    inp.onchange = () => { const x = Number(inp.value); if (Number.isFinite(x)) applySettings(c.set(Math.min(hi, Math.max(lo, x)))); };
    return html`<label class="qs-custom-label">${c.label} ${inp}</label>`;
  };
  const fineLink = key => { const a = html`<button type="button" class="qs-fine">fine-tune this setting</button>`; a.onclick = () => jumpTo(key); return a; };
  const box = html`<details class="quickstart"><summary>Quick start: four questions ${tipEl("Answer in plain terms; each answer sets a few of the detailed settings further down. 'fine-tune' jumps to the exact control. The highlighted answer is the one that matches the current settings.")}</summary>
    ${Q.map(q => html`<div class="qs-q"><div class="qs-text">${q.q} ${tipEl(q.tip)}</div><div class="qs-opts">${q.opts.map(o => {
      const b = html`<button type="button" class=${on(o) ? "on" : ""} aria-pressed=${on(o) ? "true" : "false"}>${o.label}</button>`;
      b.onclick = () => applySettings(o.set);
      return b;
    })}</div>${q.custom ? custom(q.custom) : ""}${q.note ? html`<div class="qs-note">${q.note}</div>` : ""}${fineLink(q.fine)}</div>`)}
    <div class="sidebar-note">The answers move the detailed settings below; the "Where the settings come from" line on the right tags each value.</div>
  </details>`;
  // Set the property, not the attribute: an attribute of "false" would still open it.
  box.open = wasOpen;
  return box;
}

Presets: Report default · Report, cubic value · Report figures (f = 0.5) · RP 2026 central values ?

Scenario

viewof riskType = Inputs.select(["top", "constant", "filters", "decay"], {
  value: U.risk, label: lab("Risk scenario", "How the yearly chance of extinction changes over time. The report's main case is Time of Perils: high risk for a century, then low risk forever. 'What the risk scenarios are' below has the details."),
  format: d => ({top: "Time of Perils (report section 3)", constant: "Constant risk (Ord's base case)", filters: "Great Filters (report section 3)", decay: "Exponential decay (report section 4)"})[d]
})
viewof valueType = Inputs.select(M.PAGE_VALUE_TYPES, {
  value: U.value, label: lab("Value growth", "How much each future year is worth compared with a year of today's world. With growth, far-future years count for more, so averting risk now is worth more. Options marked 'beyond' aren't in the report."),
  format: d => ({constant: "V0 Constant (report)", linear: "V1 Linear (report)", quadratic: "V2 Quadratic (report)", cubic: "V3 Cubic (report)", logistic: "V4 Logistic (report)",
    polynomial: "Polynomial of order n (beyond)", exponential: "Exponential, uncapped (beyond)", capped: "Exponential with hard cap (beyond)", fluctuating: "Fluctuating around a base path (beyond)"})[d]
})
What the risk scenarios are
  • Constant risk: \(r_t = r_{high}\) forever. Ord’s base case; with constant value, \(\mathbb{E}(w)=v_c(1-r_c)/r_c\).
  • Time of Perils: \(r_{high}\) for the first \(L\) years (report: 100), then \(r_{low}\) forever.
  • Great Filters: \(F\) high-risk eras of length \(\ell_d\) separated by low-risk eras of length \(\ell_s\), then low risk forever. Report: \(F=2\), \(\ell=(100,500,100,\ldots)\).
  • Exponential decay: \(r_t = r_0 e^{-\lambda (t-1)} + r_\infty\). Notebook defaults: \(\lambda = 10^{-4}\), \(r_\infty = 10^{-9}\). The report’s Figure 3 draws a much faster decay than the one used in its tables (see Departures).
Per-year risks come from per-century probabilities: 20% per century gives \(r_{high} = 1-0.8^{1/100} = 0.0022289\); 1% per century gives \(r_{low} \approx 0.0001\).

Risk

viewof rHighCentury = rangeIn("rh", lab("High risk (% per century)", "Chance of extinction per century during high-risk periods (or always, under constant risk). The report uses 20%, a common estimate for this century. The page converts it to a yearly rate."))
viewof rLowCentury = rangeIn("rl", lab("Low risk (% per century)", "Chance of extinction per century once the dangerous period is over. The report uses about 1% (a yearly risk of 0.0001). This number largely sets how long the future is expected to last, so it drives most results."))
rHigh = 1 - Math.pow(1 - rHighCentury / 100, 0.01)
rLow = 1 - Math.pow(1 - rLowCentury / 100, 0.01)
riskType === "constant"
  ? html`<div class="sidebar-note">Constant risk: the same ${rHighCentury}% per century every year forever, ${rHigh.toExponential(3)} a year (report: 2.229e-3). There's no separate low-risk era.</div>`
  : html`<div class="sidebar-note">Per year: high ${rHigh.toExponential(3)}${["top", "filters"].includes(riskType) ? `, low ${rLow.toExponential(3)}` : ""} (report: 2.229e-3 and 1.000e-4).</div>`
viewof perilsLength = rangeIn("pl", lab("Time of Perils length (years)", "How many years the high-risk period lasts before risk drops to the low level. The report uses 100."))
viewof nFilters = rangeIn("nf", lab("Number of great filters F", "How many separate high-risk eras the world has to get through, with safer eras in between. The report uses 2."))
viewof filterLength = rangeIn("fl", lab("Filter length (years)", "Length of each high-risk era."))
viewof safeLength = rangeIn("sl", lab("Safe era between filters (years)", "Length of each low-risk era between two filters."))
viewof decayLambdaLog = rangeIn("lam", lab("Decay rate: log10(lambda)", "How fast risk falls, on a log scale. -4 (the notebook's value) means risk shrinks by about 1% per century; -2 means it falls by about 63% per century."))
viewof decayRInfLog = rangeIn("rinf", lab("Decay floor: log10(r_inf)", "The yearly risk that remains forever once the decay has run its course, on a log scale. -9 means one in a billion per year."))
html`<div class="sidebar-note">lambda = ${Math.pow(10, decayLambdaLog).toExponential(2)} per year, r_inf = ${Math.pow(10, decayRInfLog).toExponential(2)}.</div>`

Value

viewof alpha = selectIn("alpha", lab("Growth scale alpha", "Sets how fast the polynomial paths grow: value in year t is (alpha t + 1 - alpha)^n. With the report's 0.01, linear value doubles after 100 years and cubic value is 8 times today's after 100 years."), d => d === 0.01 ? "0.01 (report: years)" : d === 1 ? "1 (centuries as periods)" : String(d))
viewof polyN = rangeIn("n", lab("Polynomial order n", "The power n in (alpha t + 1 - alpha)^n. 0 to 3 are the report's constant, linear, quadratic and cubic cases; higher values test whether cubic is special."))
viewof logisticCapLog = rangeIn("cap", lab("Logistic cap: log10(c)", "The ceiling on value, as a multiple of today's, on a log scale. 6 (the report's choice) means a million times today's value; 0.2 means about 1.6 times, as in Rethink Priorities' 2026 Earth-bound case."))
viewof logisticRate = rangeIn("gamma", lab("Logistic growth rate (per year)", "Growth rate while value is still far below the cap. The report uses 3% a year; Rethink Priorities' 2026 model uses 1%."))
html`<div class="sidebar-note">Cap c = ${fmt(Math.pow(10, logisticCapLog), 3)} times today's value (report: 1e6 at 3% a year).</div>`
viewof expGrowth = rangeIn("g", lab("Exponential growth g (per year)", "Value grows by this fraction every year, with no limit. If value grows faster than the chance of survival falls, the total has no finite value; the page warns when that happens."))
viewof hardCapLog = rangeIn("hcap", lab("Hard cap: log10(cap)", "Exponential growth stops at this multiple of today's value (log scale: 6 means a million times)."))
viewof fluctBase = Inputs.select(M.VALUE_TYPES, {value: U.fbase, label: lab("Base path", "The path the random fluctuations are centred on. On average, value follows this path.")})
viewof fluctRho = rangeIn("rho", lab("Persistence rho", "How long good and bad stretches last. Near 1, a bad era persists for many years; near 0, each year is independent."))
viewof fluctSigma = rangeIn("sig", lab("Innovation sd sigma", "Size of each year's random shock to (log) value."))
viewof fluctSeed = rangeIn("vseed", lab("Seed", "Picks one random path. Change it to see another."))
Value paths explained

The report (section 2.1 and section 11) uses \(v_t = (\alpha t + 1 - \alpha)^n v_c\) with \(\alpha = 1/100\), so that a sequence defined per century (value \(i^n\) in century \(i\)) is reproduced when periods are years; \(n=0,1,2,3\) give the constant, linear, quadratic and cubic cases. Logistic is \(v_t = c / (1 + \frac{c-s}{s}e^{-\gamma t})\) with \(s\) set so that \(v_1 = 1\); the notebook uses \(c = 10^6\) and \(\gamma = 0.03\).

Beyond the paper: polynomial of order n checks the evaluator’s point that cubic isn’t special (quartic gives even more); exponential grows without bound (the report notes its convergence proof fails here); hard cap is exponential growth that stops at a ceiling; fluctuating multiplies a base path by mean-one AR(1) noise in logs, so the expected path equals the base path but any single realization wobbles.

Mitigation

viewof fSel = selectIn("f", lab("Efficacy f", "Share of each year's extinction risk the action removes while it works. 0.0001 (one basis point) means risk falls from r to 0.9999 r. The report's tables use 0.0001; its figures use 0.5."), d => d === 1e-4 ? "0.0001 (report: one basis point)" : d === 0.5 ? "0.5 (report figures)" : String(d))
viewof PSel = selectIn("P", lab("Persistence P (years)", "How many years the risk reduction lasts. The report shows 1 to 2,000 years and suggests more than 50 is unlikely; Rethink Priorities' 2026 model uses 11."), d => [1, 5, 50, 500, 2000].includes(d) ? `${d} (report)` : d === 11 ? "11 (RP 2026 mean)" : String(d))
viewof startT = rangeIn("t0", lab("Effect starts in year t0 (report: 1)", "The first year the reduction applies; 1 is now. Rethink Priorities' 2026 model assumes effects start after about four years."))
viewof TSel = selectIn("T", lab("Horizon T (years)", "The last year counted. Anything after T is dropped; the 'Left out by stopping at T' box bounds how much that misses."), d => d === 120000 ? "120,000 (report's stated cap)" : d === 40000 ? "40,000 (notebook, ToP/constant)" : d.toLocaleString())
About f, P and T

The report’s headline table uses \(f = 0.0001\) (“a useful and more realistic efficacy is one basis point”); its figures use \(f = 0.5\) “for entirely pedagogical reasons”. Persistence \(P\) is the number of years the risk stays reduced; the report shows \(P \in \{1, 5, 50, 500, 2000\}\) and suggests persistence above 50 years is unlikely.

The report says the computation “assumes a cap of 120 thousand years”, but the companion notebook uses a different \(T\) for each cell of Table 3 (5,000 to 120,000). The replication page uses those per-cell values; here you choose one \(T\). The horizon table on the right shows how much of the answer sits beyond any given year, with a bound on what lies beyond \(T\) itself.

Beyond the paper

Each switch starts at the report’s assumption (off). ?

viewof floorOn = Inputs.toggle({label: lab("1. Risk floor c > 0", "Never let yearly risk fall below c. This bounds how long the future can be expected to last, so what matters becomes the share of value beyond a stated horizon."), value: !!U.floor})
viewof floorLog = rangeIn("c", lab("log10(c)", "The floor on yearly risk, on a log scale: -4 means 1 in 10,000 a year, so survival is below 1% after about 46,000 years."))
Why a floor? The evaluator argued that if some extinction risk \(c>0\) is always present, survival is bounded by \((1-c)^t\), so value has an “approximate support” in finite time and the question of infinite-series convergence matters less than the truncation error at a chosen horizon. The floor replaces \(r_t\) by \(\max(r_t, c)\). The horizon table reports the share of \(\mathbb{E}(w)\) and \(\mathbb{E}(M)\) beyond 100, 500, 1,000, 10,000 and 100,000 years, plus a bound on everything beyond \(T\).
viewof mcOn = Inputs.toggle({label: lab("2. Sign-uncertain mitigation (Monte Carlo)", "Treat the action's effect as uncertain, and possibly harmful, and show the spread of outcomes and the chance of net harm instead of one number."), value: !!U.mc})
viewof fMeanSel = selectIn("fm", lab("Mean of f", "Average efficacy across draws. 'same as f' uses the efficacy chosen above."), d => String(d))
viewof fSdRatio = rangeIn("fsd", lab("SD of f, as a multiple of its mean", "How spread out the efficacy is. At 1, the standard deviation equals the mean, so about 16% of draws raise risk instead of lowering it."))
viewof pBackfire = rangeIn("pb", lab("P(delayed backfire)", "Chance that the action also causes a later period of higher risk."))
viewof backfireRatio = rangeIn("bsize", lab("Backfire size (median), multiple of mean f", "Typical size of that later rise in risk, relative to the average efficacy. 1 means the backfire is as large as the intended effect."))
viewof backfireSigma = rangeIn("bsig", lab("Backfire size: lognormal sigma", "How much the backfire size varies between draws."))
viewof backfireStart = rangeIn("bstart", lab("Backfire starts in year", "First year of the backfire window."))
viewof backfireP = rangeIn("bp", lab("Backfire lasts (years)", "Length of the backfire window."))
viewof mcN = selectIn("mcn", lab("Draws", "Number of simulated draws. More draws give smoother results but take longer."))
viewof mcSeed = rangeIn("seed", lab("Seed", "Fixes the random draws, so the same settings always give the same results."))
What the Monte Carlo does The report treats \(f\) as a known positive number. Here \(f \sim \mathrm{Normal}(\mu_f, \sigma_f)\), clipped at 1, so negative draws (the action raises risk during its window) are possible; with probability \(p_b\) a delayed backfire multiplies risk by \((1+b)\) in a later window, with \(b\) lognormal. Each draw’s \(\mathbb{E}(M)\) is computed exactly, and the page reports the full distribution and \(\Pr[\mathbb{E}(M) < 0]\), not only the mean. The defaults are illustrative, not estimates. Seeded, so results are reproducible.
viewof reboundOn = Inputs.toggle({label: lab("3. Action-dependent risk: rebound after the window", "Let the action change later risk: some of the risk it averts comes back after its window ends. In the report, nothing you do changes later risk."), value: !!U.rb})
viewof reboundBeta = rangeIn("beta", lab("beta (with D = P, 1 means the averted risk is all paid back)", "How much of the averted risk comes back. 0 means none, 1 (with D = P) means all of it, and negative values mean risk keeps falling (a virtuous cycle)."))
viewof reboundD = selectIn("D", lab("Rebound lasts D (years)", "How many years the returning risk is spread over."))
viewof reboundPermanent = Inputs.toggle({label: lab("Permanent: every later year's risk x (1 + beta f). Much stronger than a payback", "Instead of a payback spread over D years, every later year's risk is raised by the factor (1 + beta f), forever."), value: !!U.perm})
What rebound means In the report the risk path is fixed: nothing you do now changes later risk. Here, after the mitigation window ends, risk is multiplied by \(1 + \beta f P / D\) for the next \(D\) years (so \(\beta = 1\), \(D = P\) adds the same proportional risk-years back later, a pure delay of risk), or by \(1+\beta f\) forever if permanent. Negative \(\beta\) is a virtuous cycle. This is the simplest version of the action-dependent risk the evaluator asked for; it’s one functional form among many.
viewof bucketsOn = Inputs.toggle({label: lab("4. Time-bucket discounting (Donor Compass style)", "Split the action's value by when it arrives and weight each time window, as Rethink Priorities' Donor Compass does."), value: !!U.bk})
viewof bucketFactorsText = Inputs.text({label: lab("Factors for 0-5, 5-10, 10-20, 20-100, 100-500, 500+ years", "Six weights, separated by commas, one per time window. 1 keeps all of that window's value, 0.1 keeps a tenth. The default is the Donor Compass walkthrough's example."), value: U.bf})
// Say so when the text can't be used, rather than quietly falling back to the defaults.
M.checkBucketFactors(bucketFactorsText)
  ? html`<div class="sidebar-note">Using: ${M.checkBucketFactors(bucketFactorsText).join(", ")}.</div>`
  : html`<div class="input-error" role="alert">That isn't six non-negative numbers separated by commas, so the results use the default factors (${M.SETTING_DEFAULTS.bf}) until it is.</div>`
What this illustrates Rethink Priorities’ Donor Compass splits each fund’s modeled impact into six time windows and multiplies each by a factor the user’s worldview sets (its walkthrough’s example: 1.00, 0.95, 0.85, 0.70, 0.50, 0.10). This switch applies the same aggregation to this model’s \(\mathbb{E}(M)\), so you can see what share lands in the 500+ year window and how much a given set of factors keeps. RP’s current model of global catastrophic risk funds lists this report as an influence; see Departures.
{
  const btn = html`<button type="button" class="settings-toggle">Copy link to these settings</button>`;
  const note = html`<div class="sidebar-note" role="status" aria-live="polite">Settings that differ from the report's defaults go into the link. <a href="#share">More ways to share or submit</a>.</div>`;
  btn.onclick = async () => { note.textContent = (await copyText(shareUrl)) ? "Link copied." : "Couldn't copy here; the Share section at the bottom shows the link."; };
  return html`<div style="margin-top:0.7rem">${btn}${note}</div>`;
}
// Show only the controls that matter for the current choices.
showControls = {
  const show = (id, on) => { const el = document.getElementById(id); if (el) el.style.display = on ? "" : "none"; };
  const vbase = valueType === "fluctuating" ? fluctBase : valueType;
  show("grp-rlow", ["top", "filters"].includes(riskType));
  // Under constant risk there is no "high" period, so name the slider for what it is.
  const rhLabel = viewof rHighCentury.querySelector("label > span");
  if (rhLabel && rhLabel.firstChild) rhLabel.firstChild.textContent = riskType === "constant" ? "Risk, every year (% per century)" : riskType === "decay" ? "Starting risk (% per century)" : "High risk (% per century)";
  show("grp-top", riskType === "top");
  show("grp-filters", riskType === "filters");
  show("grp-decay", riskType === "decay");
  const alphaOn = ["linear", "quadratic", "cubic", "polynomial"].includes(vbase);
  const polyOn = valueType === "polynomial";
  const logisticOn = vbase === "logistic";
  const expOn = ["exponential", "capped"].includes(valueType);
  const hcapOn = valueType === "capped";
  const fluctOn = valueType === "fluctuating";
  show("grp-alpha", alphaOn); show("grp-poly", polyOn); show("grp-logistic", logisticOn);
  show("grp-exp", expOn); show("grp-hcap", hcapOn); show("grp-fluct", fluctOn);
  show("grp-value-params", alphaOn || polyOn || logisticOn || expOn || hcapOn || fluctOn);
  show("grp-floor", floorOn); show("grp-mc", mcOn); show("grp-rb", reboundOn); show("grp-bk", bucketsOn);
  return html`<span></span>`;
}
settings = ({risk: riskType, value: valueType, rh: rHighCentury, rl: rLowCentury, pl: perilsLength, nf: nFilters, fl: filterLength, sl: safeLength,
  lam: decayLambdaLog, rinf: decayRInfLog, alpha, n: polyN, cap: logisticCapLog, gamma: logisticRate, g: expGrowth, hcap: hardCapLog,
  fbase: fluctBase, rho: fluctRho, sig: fluctSigma, vseed: fluctSeed, f: fSel, P: PSel, t0: startT, T: TSel,
  floor: floorOn ? 1 : 0, c: floorLog, mc: mcOn ? 1 : 0, fm: fMeanSel, fsd: fSdRatio, pb: pBackfire, bsize: backfireRatio, bsig: backfireSigma,
  bstart: backfireStart, bp: backfireP, mcn: mcN, seed: mcSeed, rb: reboundOn ? 1 : 0, beta: reboundBeta, D: reboundD, perm: reboundPermanent ? 1 : 0,
  bk: bucketsOn ? 1 : 0, bf: bucketFactorsText})
params = M.paramsFromSettings(settings)
shareUrl = { const q = M.settingsToQuery(settings); return window.location.origin + window.location.pathname + (q ? "?" + q : ""); }
scenario = M.computeScenario(params)
// Heavier analyses (scenario grid, persistence curve, sensitivity, Monte Carlo) wait until the
// inputs have been still for a moment, so dragging a slider stays smooth. A newer value
// supersedes a pending one, so this works as a debounce.
paramsSettled = { await new Promise(r => setTimeout(r, 300)); return params; }
tornadoItems = M.tornado(paramsSettled).filter(d => Number.isFinite(d.logLow) && Number.isFinite(d.logHigh))
  .sort((a, b) => Math.max(Math.abs(b.logLow), Math.abs(b.logHigh)) - Math.max(Math.abs(a.logLow), Math.abs(a.logHigh)))
provenance = M.settingProvenance(settings)
// Written at deploy time by scripts/deploy.sh (engine version, schema, git commit). Absent in local previews.
manifest = fetch(new URL("./model-manifest.json", window.location.href).href).then(r => r.ok ? r.json() : null).catch(() => null)
// Every settings key mapped to its input, so the quick start and the comparison can set several at once.
views = ({risk: viewof riskType, value: viewof valueType, rh: viewof rHighCentury, rl: viewof rLowCentury, pl: viewof perilsLength,
  nf: viewof nFilters, fl: viewof filterLength, sl: viewof safeLength, lam: viewof decayLambdaLog, rinf: viewof decayRInfLog,
  alpha: viewof alpha, n: viewof polyN, cap: viewof logisticCapLog, gamma: viewof logisticRate, g: viewof expGrowth, hcap: viewof hardCapLog,
  fbase: viewof fluctBase, rho: viewof fluctRho, sig: viewof fluctSigma, vseed: viewof fluctSeed, f: viewof fSel, P: viewof PSel,
  t0: viewof startT, T: viewof TSel, floor: viewof floorOn, c: viewof floorLog, mc: viewof mcOn, fm: viewof fMeanSel, fsd: viewof fSdRatio,
  pb: viewof pBackfire, bsize: viewof backfireRatio, bsig: viewof backfireSigma, bstart: viewof backfireStart, bp: viewof backfireP,
  mcn: viewof mcN, seed: viewof mcSeed, rb: viewof reboundOn, beta: viewof reboundBeta, D: viewof reboundD, perm: viewof reboundPermanent,
  bk: viewof bucketsOn, bf: viewof bucketFactorsText})
// Scroll to a setting's control (reopening the settings panel if it's hidden) and highlight it briefly.
jumpTo = key => {
  const view = views[key]; if (!view) return;
  if (document.body.classList.contains("settings-hidden") && window.xrSetSettingsHidden) window.xrSetSettingsHidden(false);
  const target = view.closest("form") || view;
  if (target.offsetParent === null) return; // its group is hidden for the current scenario
  target.scrollIntoView({behavior: "smooth", block: "center"});
  target.classList.remove("flash"); void target.offsetWidth; target.classList.add("flash");
  setTimeout(() => target.classList.remove("flash"), 2400);
  // Focus the slider, menu or switch rather than the number box, so a tablet keyboard doesn't pop up.
  const inp = target.querySelector("input[type=range], select, input[type=checkbox], input[type=text]");
  if (inp) inp.focus({preventScroll: true});
}
applySettings = partial => {
  for (const [k, v] of Object.entries(partial)) {
    const view = views[k]; if (!view) continue;
    const val = ["floor", "mc", "rb", "perm", "bk"].includes(k) ? !!v : v;
    if (view.value === val) continue;
    view.value = val;
    view.dispatchEvent(new Event("input", {bubbles: true}));
  }
}
// A scenario pinned for comparison. Kept in this browser only (best effort).
mutable pinned = { try { const p = JSON.parse(localStorage.getItem("xr-pinned")); return p && p.settings ? {...p, settings: M.settingsFromQuery(p.settings)} : null; } catch (e) { return null; } }
pinScenario = s => {
  const p = s ? {settings: {...s}, at: new Date().toISOString().slice(0, 16).replace("T", " ")} : null;
  try { if (p) localStorage.setItem("xr-pinned", JSON.stringify(p)); else localStorage.removeItem("xr-pinned"); } catch (e) { /* storage may be blocked */ }
  mutable pinned = p;
}
// If the link held values the page couldn't use, say what was changed.
parsedUrl.issues.length
  ? html`<div class="link-issues" role="status"><strong>Some values in the link you opened couldn't be used as given</strong>, so the page changed them:
      <ul>${parsedUrl.issues.map(i => html`<li>${i.message}</li>`)}</ul></div>`
  : html`<span></span>`
{
  const d = M.describeSettings(settings), pv = provenance;
  // `this` is the box from the previous run: keep the details open if the reader opened them.
  const wasOpen = !!(this && this.querySelector && this.querySelector("details.src-details")?.open);
  const chip =(src, n) => html`<span class="src-chip src-${src}">${M.SOURCE_LABELS[src]}: ${n}</span>`;
  const shown = v => typeof v === "number" ? (+v.toPrecision(4)).toLocaleString("en-US", {maximumFractionDigits: 6}) : String(v);
  const jumpBtn = (key, text) => { const b = html`<button type="button" class="jump-link" title="Go to this setting">${text}</button>`; b.onclick = () => jumpTo(key); return b; };
  const pinBtn = html`<button type="button" class="mini-btn">Pin to compare</button>`;
  pinBtn.onclick = () => pinScenario(settings);
  const box = html`<div class="settings-summary"><strong>Current settings:</strong> ${d.risk}; ${d.value}; ${d.mitigation}; ${d.horizon}.
    <span class=${d.beyond.length ? "beyond-on" : ""}>Beyond the paper: ${d.beyond.length ? d.beyond.join("; ") : pv.rows.every(r => r.source === "report") ? "none, so this is the report's model with the report's values" : "none"}.</span>
    <a href="#share">Share or submit</a> ${pinBtn}
    <div class="src-line"><span class="src-title">Where the ${pv.rows.length} settings in use come from:</span> ${tipEl("Each setting that affects this result, tagged by the source of its current value. '2023 report': the report or its notebook. 'RP 2026 model': Rethink Priorities' current model of global catastrophic risk funds, as we read it. 'illustrative': our default for an extension, with no empirical basis. 'your choice': anything else, including values from a shared link.")}
      ${chip("report", pv.counts.report)} ${chip("rp2026", pv.counts.rp2026)} ${chip("illustrative", pv.counts.illustrative)} ${chip("user", pv.counts.user)}
      <details class="src-details"><summary>show each setting (click a name to adjust it)</summary>
        <table class="compact table src-table"><thead><tr><th>Setting</th><th>Value</th><th>Source</th></tr></thead>
        <tbody>${pv.rows.map(r => html`<tr><td>${jumpBtn(r.key, r.key === "rh" && settings.risk === "constant" ? "Risk (every year)" : r.label)}</td><td>${shown(r.value)}${r.unit ? " " + r.unit : ""}</td><td><span class="src-chip src-${r.source}">${M.SOURCE_LABELS[r.source]}</span>${r.alsoRp2026 ? " (also RP 2026)" : ""}</td></tr>`)}</tbody></table>
      </details></div></div>`;
  box.querySelector("details.src-details").open = wasOpen;
  return box;
}
stats = {
  const s = scenario, EM = s.EM, ew = s.base.ev;
  const closed = (params.risk.type === "constant" && !floorOn && ["constant", "linear", "quadratic", "cubic"].includes(params.value.type))
    ? M.closedFormConstantRisk(params.value.type, params.risk.rHigh, params.value.alpha) : null;
  const rMin = arrMin(s.rMit);
  const bound = M.tailBound(params.value, rMin, params.T);
  const sh500 = M.horizonShares(s.diffCum, [500])[0];
  // When gains and losses over time largely cancel (e.g. rebound), a share of the net is meaningless.
  let maxAbs = 0; for (let t = 0; t < s.diffCum.length; t++) { const a = Math.abs(s.diffCum[t]); if (a > maxAbs) maxAbs = a; }
  const offsetting = maxAbs > 10 * Math.abs(EM);
  return {EM, ew, ewm: s.mit.ev, closed, rMin, bound, share500: offsetting ? NaN : sh500.shareBeyond, offsetting,
    leftOut: Number.isFinite(bound) ? bound / (ew + bound) : Infinity};
}
{
  const st = stats, EM = st.EM;
  // Three significant figures on screen; the full value on hover, for checking against the report.
  const exact = x => `${x.toPrecision(10)} v_c`;
  return html`<div class="stat-grid">
    <div class="stat"><div class="label">Value of the future, E(w) ${tipEl("Expected value of the whole future without the action: each year's value times the chance the world survives to that year, added up to the horizon T. Measured in years of today's world (v_c).")}</div><div class="value" title=${exact(st.ew)}>${fmt(st.ew, 3)} v_c</div><div class="note">${st.closed !== null ? `closed form (T infinite): ${fmt(st.closed, 3)}` : `truncated at T = ${params.T.toLocaleString()}`}</div></div>
    <div class="stat"><div class="label">With mitigation, E(w') ${tipEl("The same sum when the action cuts risk during its window. It is usually almost identical to E(w), because the action changes risk by a tiny fraction.")}</div><div class="value" title=${exact(st.ewm)}>${fmt(st.ewm, 3)} v_c</div><div class="note">effect in years ${params.mitigation.start} to ${params.mitigation.start + params.mitigation.P - 1}${reboundOn ? ", then rebound" : ""}</div></div>
    <div class="stat stat-main"><div class="label">Value of the mitigation, E(M) ${tipEl("The answer: E(w') minus E(w), the expected gain from the action, in years of today's world. This is the number in the report's tables. A negative value means the action does harm on balance.")}</div><div class="value" title=${exact(EM)} style="color:${EM >= 0 ? "#1a6b3a" : "#b91c1c"}">${fmt(EM, 3)} v_c</div><div class="note">${EM < 0 ? html`<strong>Net harmful at these settings.</strong> ` : ""}${fmt(EM / st.ew, 3)} of E(w)${Number.isFinite(EM / params.mitigation.f) ? "; " + fmt(EM / params.mitigation.f, 3) + " per unit of f" : ""}</div>
      ${mc ? html`<div class="mc-line">Uncertain effect (switch 2): average <strong>${fmt(mc.mean, 3)}</strong>; <strong>${pct(mc.pHarmful)}</strong> chance of net harm. The number above is for an effect of exactly f. <a href="#mc-section">Spread</a></div>` : ""}</div>
    <div class="stat"><div class="label">Share of E(M) after year 500 ${tipEl("How much of the action's value comes from years after 500. Under Time of Perils most of it does: the action's benefit is a slightly higher chance of reaching the long, safer era.")}</div><div class="value">${st.offsetting ? "n/a" : pct(st.share500, 1)}</div><div class="note">${st.offsetting ? "gains and losses at different times largely cancel; see the horizon table" : "see the horizon table below"}</div></div>
    <div class="stat"><div class="label">Left out by stopping at T ${tipEl("An upper bound on the share of E(w) that lies beyond the horizon T, using the lowest yearly risk in the path. If value grows faster than survival falls, there is no finite bound.")}</div><div class="value">${Number.isFinite(st.bound) ? pct(st.leftOut, 2) : "diverges"}</div><div class="note">${Number.isFinite(st.bound) ? `at most ${fmt(st.bound, 3)} v_c of E(w), given min risk ${st.rMin.toExponential(2)}` : "series need not converge: (1+g)(1 - r_min) >= 1"}</div></div>
  </div>`;
}
// Plain-language account of what drives the number, from the one-at-a-time sensitivity below.
{
  const items = tornadoItems;
  if (!items.length) return html`<span></span>`;
  const r = x => { const v = x / items[0].base; return Number.isFinite(v) ? (Math.abs(v) >= 100 || Math.abs(v) < 0.01 ? v.toExponential(1) : String(+v.toPrecision(2))) : "n/a"; };
  // The engine's short labels ("/2", "2P", "T/10") in words.
  const word = l => ({"/2": "halved", "x2": "doubled", "f/2": "halved", "2f": "doubled", "P/2": "halved", "2P": "doubled",
    "T/10": "cut to a tenth", "2T": "doubled", "headroom /10": "with a tenth of the headroom", "x10": "with ten times the headroom"})[l] || l;
  const [a, b, c] = items;
  const next = [b, c].filter(Boolean).map(d => `${d.label} (${r(d.low)} to ${r(d.high)} times)`).join(" and ");
  const text = `At these settings E(M) is most sensitive to ${a.label}: ${word(a.lowLabel)}, E(M) becomes ${r(a.low)} times its current value; ${word(a.highLabel)}, ${r(a.high)} times.` +
    (next ? ` Next come ${next}.` : "");
  return html`<p class="drivers"><strong>What drives this number.</strong> ${text} <a href="#sensitivity-one-input-at-a-time">Sensitivity chart</a>.</p>`;
}
// Side-by-side comparison with a pinned scenario.
{
  if (!pinned) return html`<span></span>`;
  const a = pinned.settings, sa = M.computeScenario(M.paramsFromSettings(a)), sb = scenario;
  const after500 = s => { const h = M.horizonShares(s.diffCum, [500])[0].shareBeyond; return h >= 0 && h <= 1 ? pct(h, 1) : "n/a"; };
  const diffs = Object.keys(M.SETTING_DEFAULTS).filter(k => !M.SETTING_INFO[k].technical && String(a[k]) !== String(settings[k]));
  const unpin = html`<button type="button" class="mini-btn">Unpin</button>`; unpin.onclick = () => pinScenario(null);
  const load = html`<button type="button" class="mini-btn">Load the pinned settings</button>`; load.onclick = () => applySettings(a);
  const repin = html`<button type="button" class="mini-btn">Pin the current settings instead</button>`; repin.onclick = () => pinScenario(settings);
  return html`<div class="compare-box"><strong>Comparison with the scenario you pinned</strong> <span class="muted">(${pinned.at})</span>
    <table class="compact table"><thead><tr><th></th><th>Pinned</th><th>Current</th><th>Current / pinned</th></tr></thead><tbody>
      <tr><td>E(M), value of the mitigation</td><td>${fmt(sa.EM)}</td><td>${fmt(sb.EM)}</td><td>${fmt(sb.EM / sa.EM, 2)}</td></tr>
      <tr><td>E(w), value of the future</td><td>${fmt(sa.base.ev)}</td><td>${fmt(sb.base.ev)}</td><td>${fmt(sb.base.ev / sa.base.ev, 2)}</td></tr>
      <tr><td>Share of E(M) after year 500</td><td>${after500(sa)}</td><td>${after500(sb)}</td><td></td></tr>
    </tbody></table>
    <div class="muted" style="font-size:0.88rem;">${diffs.length ? html`Settings that differ: ${diffs.map((k, i) => html`${i ? "; " : ""}${M.SETTING_INFO[k].label} ${String(a[k])} → ${String(settings[k])}`)}.` : "The settings are the same."} Monte Carlo results aren't compared here.</div>
    <div style="margin-top:0.4rem;display:flex;gap:0.4rem;flex-wrap:wrap;">${load}${repin}${unpin}</div></div>`;
}
// Tell screen readers the new headline once the inputs settle (the live region is static, further down).
announce = {
  paramsSettled;
  const el = document.getElementById("xr-live");
  if (el) el.textContent = `Value of the mitigation, E(M): ${fmt(stats.EM, 3)} years of today's world${stats.EM < 0 ? ", net harmful" : ""}.`;
  return true;
}
{
  const notes = [];
  const rb = M.reboundMultiplier(params.mitigation.f, params.mitigation.P, params.beyond.rebound);
  if (reboundOn) notes.push(`Rebound multiplier on post-window risk: ${rb.toFixed(6)} for ${reboundPermanent ? "all later years" : reboundD + " years"}.`);
  if (floorOn) notes.push(`Risk floor c = ${Math.pow(10, floorLog).toExponential(1)}: survival is at most (1-c)^t, so at most 1% survival remains after ${Math.round(M.approximateSupport(Math.pow(10, floorLog), 0.01)).toLocaleString()} years and 0.1% after ${Math.round(M.approximateSupport(Math.pow(10, floorLog), 0.001)).toLocaleString()} years.`);
  if (valueType === "exponential") notes.push(`Uncapped exponential value: the ratio test needs (1+g)(1-r) < 1 in the long run; with g = ${expGrowth} and long-run risk ${arrMin(scenario.r).toExponential(2)} it is ${((1 + expGrowth) * (1 - arrMin(scenario.r))).toFixed(5)}.`);
  if (valueType === "fluctuating") notes.push(`Fluctuating value shows one seeded realization (seed ${fluctSeed}); its expectation is the ${fluctBase} path.`);
  return notes.length ? html`<div style="font-size:0.92em;color:#2d3748;">${notes.map(n => html`<p style="margin:0.2em 0">${n}</p>`)}</div>` : html`<span></span>`;
}

Risk and value paths

Left: the yearly chance of extinction, with and without the action. The action’s effect is the small dip in its first years; both axes are logarithmic. Right: what each year is worth compared with a year of today’s world.

sampleIdx = (T, n = 400) => {
  const idx = new Set([0, T - 1]);
  for (let i = 0; i < n; i++) idx.add(Math.min(T - 1, Math.round(Math.pow(T, i / (n - 1))) - 1));
  for (let t = 0; t < Math.min(T, 300); t++) idx.add(t);
  return Array.from(idx).sort((a, b) => a - b);
}
pathIdx = sampleIdx(params.T)
{
  const rows = [];
  for (const i of pathIdx) { rows.push({t: i + 1, series: "baseline r_t", y: scenario.r[i]}); rows.push({t: i + 1, series: "with mitigation r'_t", y: scenario.rMit[i]}); }
  const riskPlot = Plot.plot({width: w2, height: 260, marginLeft: 60,
    ariaLabel: "Extinction risk per year, with and without the mitigation",
    ariaDescription: `Yearly risk starts at ${pct(scenario.r[0], 3)} and falls to ${pct(arrMin(scenario.r), 4)} at its lowest. The mitigation lowers it in years ${params.mitigation.start} to ${params.mitigation.start + params.mitigation.P - 1}.`,
    x: {type: "log", label: "year t (log scale)", tickFormat: tick}, y: {type: "log", label: "extinction risk per year", grid: true, tickFormat: pctTick},
    color: {legend: true, domain: ["baseline r_t", "with mitigation r'_t"], range: ["#2b6cb0", "#dd6b20"]},
    marks: [Plot.line(rows, {x: "t", y: "y", stroke: "series", strokeWidth: 1.5, curve: "step-after"})]});
  const vrows = pathIdx.map(i => ({t: i + 1, v: scenario.v[i]}));
  const valuePlot = Plot.plot({width: w2, height: 260, marginLeft: 60,
    ariaLabel: "Value of each year, relative to a year of today's world",
    ariaDescription: `Value is ${fmt(scenario.v[0])} in year 1, ${fmt(scenario.v[Math.min(999, params.T - 1)])} in year ${Math.min(1000, params.T).toLocaleString()} and ${fmt(scenario.v[params.T - 1])} in year ${params.T.toLocaleString()}.`,
    x: {type: "log", label: "year t (log scale)", tickFormat: tick}, y: {type: "log", label: "value of year t (v_c)", grid: true, tickFormat: tick},
    marks: [Plot.line(vrows, {x: "t", y: "v", stroke: "#2f855a", strokeWidth: 1.5})]});
  return html`<div style="display:flex;flex-wrap:wrap;gap:1rem;">${riskPlot}${valuePlot}</div>`;
}

Expected value versus horizon

Each curve is the value of the future if everything ended at that year, with and without the mitigation; the right panel is the cumulative value of the mitigation. Where the curves flatten is where the report’s answer “converges”; how far right that happens shows how much rides on the far future.

{
  const rows = [];
  for (const i of pathIdx) {
    rows.push({t: i + 1, series: "E(w) baseline", y: scenario.base.cumulative[i]});
    rows.push({t: i + 1, series: "E(w') with mitigation", y: scenario.mit.cumulative[i]});
  }
  const em = pathIdx.map(i => ({t: i + 1, y: scenario.diffCum[i]}));
  const evPlot = Plot.plot({width: w2, height: 280, marginLeft: 70, ariaLabel: "Expected value of the future counted up to each horizon, with and without the mitigation",
    x: {type: "log", label: "horizon (years, log scale)", tickFormat: tick}, y: {label: "E(w) up to the horizon", grid: true, tickFormat: tick},
    color: {legend: true, domain: ["E(w) baseline", "E(w') with mitigation"], range: ["#2b6cb0", "#dd6b20"]},
    marks: [Plot.line(rows, {x: "t", y: "y", stroke: "series", strokeWidth: 1.5})]});
  const emPlot = Plot.plot({width: w2, height: 280, marginLeft: 70, ariaLabel: "Value of the mitigation counted up to each horizon",
    x: {type: "log", label: "horizon (years, log scale)", tickFormat: tick}, y: {label: "E(M) up to the horizon", grid: true, tickFormat: tick},
    marks: [Plot.line(em, {x: "t", y: "y", stroke: "#805ad5", strokeWidth: 1.5}), Plot.ruleY([0])]});
  return html`<div style="display:flex;flex-wrap:wrap;gap:1rem;">${evPlot}${emPlot}</div>
    <p class="fig-note">The two curves on the left usually overlap: the mitigation changes E(w) by a tiny fraction. The right panel shows that difference.</p>`;
}

When the value arrives (truncation diagnostics)

Read across a row: how much of the future’s value, and of the mitigation’s value, arrives before and after year H. If much of E(M) comes after year 10,000, the answer depends on beliefs about the very far future, which no one can check.

{
  const Hs = [100, 500, 1000, 1e4, 1e5];
  const w = M.horizonShares(scenario.base.cumulative, Hs), m = M.horizonShares(scenario.diffCum, Hs);
  const c = floorOn ? Math.pow(10, floorLog) : arrMin(scenario.rMit);
  const boundT = M.tailBound(params.value, c, params.T);
  const rows = Hs.map((H, i) => html`<tr>
    <td>${H.toLocaleString()}${w[i].withinT ? "" : " (beyond T)"}</td>
    <td>${pct(1 - w[i].shareBeyond, 2)}</td><td>${pct(w[i].shareBeyond, 2)}</td>
    <td>${pct(1 - m[i].shareBeyond, 2)}</td><td>${pct(m[i].shareBeyond, 2)}</td>
    <td>${Number.isFinite(boundT) && H <= params.T ? fmt(M.tailBound(params.value, c, H), 3) : "n/a"}</td>
  </tr>`);
  return html`<table class="compact table">
    <thead><tr><th>Year H</th><th>E(w) before H ${tipEl("Share of the future's total expected value that arrives before year H.")}</th><th>E(w) after H</th><th>E(M) before H ${tipEl("Share of the mitigation's value that arrives before year H.")}</th><th>E(M) after H</th><th>Bound on E(w) after H (floor c = ${c.toExponential(1)}) ${tipEl("A worst-case cap on how much value could lie after year H, assuming risk never falls below c. Useful for judging how much a finite horizon leaves out.")}</th></tr></thead>
    <tbody>${rows}</tbody>
    <tfoot><tr><td colspan="6" style="font-size:0.85em;">Shares are relative to the total at T = ${params.T.toLocaleString()}. The last column is the analytic upper bound sum_{t>H} (1-c)^t v_t, using the floor if it's on and otherwise the smallest risk in the (mitigated) path. It also bounds what stopping at T leaves out: ${Number.isFinite(boundT) ? fmt(boundT, 3) + " v_c" : "no finite bound, the series need not converge"}.</td></tr></tfoot>
  </table>`;
}
{
  const rows = [];
  const EM = scenario.EM, ew = scenario.base.ev;
  for (const i of pathIdx) {
    rows.push({t: i + 1, series: "share of E(w)", y: scenario.base.cumulative[i] / ew});
    rows.push({t: i + 1, series: "share of E(M)", y: scenario.diffCum[i] / EM});
  }
  const halfBy = (cum, total) => { if (!(total > 0)) return null; for (let t = 0; t < cum.length; t++) if (cum[t] / total >= 0.5) return t + 1; return null; };
  const hw = halfBy(scenario.base.cumulative, ew), hm = stats.offsetting ? null : halfBy(scenario.diffCum, EM);
  const halfText = `Half of E(w) has arrived by year ${hw ? hw.toLocaleString() : "n/a"}${hm ? `; half of E(M) by year ${hm.toLocaleString()}` : stats.offsetting ? "; E(M) has gains and losses that largely cancel, so its share isn't meaningful" : ""}.`;
  return html`${Plot.plot({width: w1, height: 260, marginLeft: 60,
    ariaLabel: "Cumulative share of the future's value and of the mitigation's value, by year", ariaDescription: halfText,
    x: {type: "log", label: "year (log scale)", tickFormat: tick}, y: {label: "cumulative share of the total", domain: [Math.min(0, ...rows.map(d => d.y)), 1], grid: true, tickFormat: d => (100 * d).toFixed(0) + "%"},
    color: {legend: true, domain: ["share of E(w)", "share of E(M)"], range: ["#2b6cb0", "#805ad5"]},
    marks: [Plot.line(rows, {x: "t", y: "y", stroke: "series", strokeWidth: 1.5}),
      Plot.ruleX([100, 500, 1000, 1e4], {stroke: "#bbb", strokeDasharray: "3,3"}), Plot.ruleY([0.5], {stroke: "#bbb", strokeDasharray: "3,3"})]})}
    <p class="fig-note"><strong>${halfText}</strong> Where each curve crosses the dashed 50% line is the year by which half of that total has arrived. Dashed vertical lines mark 100, 500, 1,000 and 10,000 years.</p>`;
}

Persistence curve

\(\mathbb{E}(M)\) as persistence \(P\) varies, everything else as set (compare the report’s Figure 8 and Table 3). The circled point is your current \(P\). Returns to persistence diminish when the mitigated years are similar; under Time of Perils the curve kinks where the window leaves the high-risk era.

{
  const pp = paramsSettled;
  const Ps = [...new Set([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, pp.mitigation.P])].filter(P => P <= pp.T).sort((a, b) => a - b);
  const rows = Ps.map(P => { const p = structuredClone(pp); p.mitigation.P = P; return {P, EM: M.computeScenario(p).EM}; });
  const allPos = rows.every(d => d.EM > 0);
  const first = rows[0], last = rows[rows.length - 1];
  return html`<div style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-start;">
    ${Plot.plot({width: w2, height: 260, marginLeft: 70, ariaLabel: "Value of the mitigation as persistence varies",
      ariaDescription: `E(M) is ${fmt(first.EM)} at P = ${first.P} and ${fmt(last.EM)} at P = ${last.P.toLocaleString()}.`,
      x: {type: "log", label: "persistence P (years, log scale)", tickFormat: tick},
      y: {type: allPos ? "log" : "linear", label: "E(M)", grid: true, tickFormat: tick},
      marks: [Plot.line(rows, {x: "P", y: "EM", stroke: "#805ad5", strokeWidth: 1.5}), Plot.dot(rows, {x: "P", y: "EM", fill: "#805ad5"}),
        Plot.dot(rows.filter(d => d.P === pp.mitigation.P), {x: "P", y: "EM", r: 6, stroke: "#dd6b20", fill: "none"})]})}
    <table class="compact table" style="width:auto;"><thead><tr><th>P</th><th>E(M)</th><th>per year of P ${tipEl("E(M) divided by P: the value of each year of persistence. If this falls as P grows, extra years of persistence are worth less than the first ones.")}</th></tr></thead>
    <tbody>${rows.map(d => html`<tr class=${d.P === pp.mitigation.P ? "current-row" : ""}><td>${d.P}</td><td>${fmt(d.EM)}</td><td>${fmt(d.EM / d.P)}</td></tr>`)}</tbody></table>
  </div>`;
}

Scenario grid at the current settings

The report’s Table 3 layout (4 risk cases by 5 value cases), computed with the current \(f\), \(P\), \(T\) and any switches that are on. Each cell is \(\mathbb{E}(M)\) for that pair; the smaller number under it is the ratio to the constant-risk, constant-value cell (the report’s Figure 6 convention). Reading down a column shows how much the risk scenario matters; reading across a row shows how much value growth matters.

{
  const pp = paramsSettled;
  const risks = ["constant", "top", "filters", "decay"], values = ["constant", "linear", "quadratic", "cubic", "logistic"];
  const grid = {};
  for (const rt of risks) { grid[rt] = {}; for (const vt of values) {
    const p = structuredClone(pp); p.risk = {...p.risk, type: rt}; p.value = {...p.value, type: vt};
    try { grid[rt][vt] = M.computeScenario(p).EM; } catch (e) { grid[rt][vt] = NaN; }
  } }
  const unit = grid.constant.constant;
  return html`<table class="compact table"><thead><tr><th>Risk / value</th>${values.map(v => html`<th>${M.VALUE_LABELS[v]}</th>`)}</tr></thead>
    <tbody>${risks.map(rt => html`<tr><td><strong>${M.RISK_LABELS[rt]}</strong></td>${values.map(vt => html`<td class=${rt === pp.risk.type && vt === pp.value.type ? "current-cell" : ""}>${fmt(grid[rt][vt])}<br><span class="muted" style="font-size:0.85em">x ${fmt(grid[rt][vt] / unit, 2)}</span></td>`)}</tr>`)}</tbody>
    <tfoot><tr><td colspan="6" style="font-size:0.85em;">f = ${pp.mitigation.f}, P = ${pp.mitigation.P}, T = ${pp.T.toLocaleString()}${pp.beyond.floor.enabled ? ", floor on" : ""}${pp.beyond.rebound.enabled ? ", rebound on" : ""}. Highlighted: your current scenario. The report's Table 3 (per-cell T, no switches) is reproduced exactly on the <a href="replication.html">replication page</a>.</td></tr></tfoot></table>`;
}

Distribution of the value of mitigation under sign uncertainty

mc = paramsSettled.beyond.uncertainty.enabled ? M.monteCarlo(paramsSettled) : null
// Shown in place of a result whose switch is off, with a button that turns the switch on.
switchPrompt = (n, name, what, view) => {
  const b = html`<button type="button">Turn on switch ${n}</button>`;
  b.onclick = () => { view.value = true; view.dispatchEvent(new Event("input", {bubbles: true})); };
  return html`<div class="switch-prompt"><p>Switch <strong>${n}. ${name}</strong> is off. ${what}</p>${b}</div>`;
}
{
  if (!mc) return switchPrompt(2, "Sign-uncertain mitigation", "Turn it on to treat the action's effect as uncertain, with some chance of raising risk or backfiring later, and see the spread of outcomes and the chance the action does net harm.", viewof mcOn);
  const EM = Array.from(mc.EM);
  const lo = M.quantile(EM, 0.005), hi = M.quantile(EM, 0.995);
  const clipped = EM.filter(x => x >= lo && x <= hi);
  const hist = Plot.plot({width: w1, height: 300, marginLeft: 60, ariaLabel: "Histogram of the value of the mitigation across Monte Carlo draws",
    ariaDescription: `Mean ${fmt(mc.mean)}, median ${fmt(mc.median)}, 5th to 95th percentile ${fmt(mc.p5)} to ${fmt(mc.p95)}. ${pct(mc.pHarmful)} of draws are net harmful.`,
    x: {label: "E(M) per draw (v_c)", domain: [lo, hi], tickFormat: tick}, y: {label: "draws", grid: true},
    marks: [Plot.rectY(clipped, Plot.binX({y: "count"}, {x: d => d, fill: d => d < 0 ? "#c53030" : "#2b6cb0", thresholds: 80})),
      Plot.ruleX([0], {stroke: "#000"}), Plot.ruleX([mc.mean], {stroke: "#805ad5", strokeWidth: 2}),
      Plot.ruleX([mc.p5, mc.p95], {stroke: "#888", strokeDasharray: "4,4"})]});
  return html`<p class="how-to-read">Each draw picks an efficacy (and maybe a later backfire) at random and computes E(M) exactly. The histogram shows how the answer is spread; the red share is the draws where the action does harm.</p>
    <div class="stat-grid">
      <div class="stat"><div class="label">Mean E(M)</div><div class="value">${fmt(mc.mean)}</div><div class="note">without uncertainty: ${fmt(scenario.EM)}</div></div>
      <div class="stat"><div class="label">Median ${tipEl("Half the draws are above this value and half below. p5 and p95 bracket the middle 90% of draws.")}</div><div class="value">${fmt(mc.median)}</div><div class="note">p5 ${fmt(mc.p5)}, p95 ${fmt(mc.p95)}</div></div>
      <div class="stat stat-main"><div class="label">Chance the action is net harmful ${tipEl("Share of draws where E(M) is below zero: the action lowers the expected value of the future. A table of means can't show this.")}</div><div class="value" style="color:#b91c1c">${pct(mc.pHarmful)}</div><div class="note">P(f drawn negative) = ${pct(mc.pNegativeF)}; P(backfire) = ${pct(pBackfire, 0)}</div></div>
      <div class="stat"><div class="label">Draws</div><div class="value">${mc.n.toLocaleString()}</div><div class="note">seed ${mc.seed}${mc.valuePaths > 1 ? `, ${mc.valuePaths} value paths` : ""}${mc.levels > 1 ? `, ${mc.levels} rebound levels` : ""}</div></div>
    </div>${hist}
    <p class="fig-note">Histogram clipped to the central 99%. Red bars: draws where the action lowers expected value. Purple line: the mean; dashed lines: p5 and p95. Backfire window: years ${backfireStart} to ${backfireStart + backfireP - 1}.</p>`;
}

Time-bucket discounting

{
  if (!bucketsOn) return switchPrompt(4, "Time-bucket discounting", "Turn it on to see when the mitigation's value arrives, split into six time windows, and how much a Donor Compass style weighting of those windows keeps.", viewof bucketsOn);
  const b = M.bucketValue(scenario.diff, params.beyond.buckets.edges, params.beyond.buckets.factors);
  const wb = M.bucketValue(scenario.base.contrib, params.beyond.buckets.edges, params.beyond.buckets.factors);
  return html`<p class="how-to-read">The mitigation's value split by when it arrives, before and after multiplying each window by its factor. Edit the factors in the Beyond panel.</p>
    <table class="compact table"><thead><tr><th>Years</th><th>Factor</th><th>E(M) in window</th><th>Share of E(M)</th><th>After factor</th><th>Share of E(w) in window</th></tr></thead>
    <tbody>${b.rows.map((r, i) => html`<tr><td>${r.bucket}</td><td>${r.factor}</td><td>${fmt(r.raw)}</td><td>${pct(r.shareRaw)}</td><td>${fmt(r.discounted)}</td><td>${pct(wb.rows[i].shareRaw)}</td></tr>`)}</tbody>
    <tfoot><tr><td colspan="6"><strong>Total E(M): ${fmt(b.raw)}; after the factors: ${fmt(b.discounted)} (${pct(b.ratio)} kept). Share of E(M) after 500 years: ${pct(b.rows[5].shareRaw)}.</strong></td></tr></tfoot></table>
    ${Plot.plot({width: w1, height: 220, marginLeft: 60, ariaLabel: "Share of the mitigation's value in each time window, before and after the factors", x: {label: null, domain: b.rows.map(r => r.bucket)}, y: {label: "share of E(M)", grid: true, tickFormat: d => (100 * d).toFixed(0) + "%"},
      marks: [Plot.barY(b.rows, {x: "bucket", y: "shareRaw", fill: "#805ad5", fillOpacity: 0.8}), Plot.barY(b.rows, {x: "bucket", y: "shareDiscounted", fill: "#dd6b20", fillOpacity: 0.6})]})}
    <p class="fig-note">Purple: share by window before the factors. Orange: share after the factors (renormalized). The model's value is already weighted by the chance of surviving; the factors are an extra, explicitly time-based haircut of the kind Donor Compass applies.</p>`;
}

Sensitivity: one input at a time

Each bar halves or doubles one input, holding the others at their current values, and shows the resulting \(\mathbb{E}(M)\) as a log10 ratio to the current \(\mathbb{E}(M)\): a bar reaching 1 means the answer becomes 10 times larger, -1 means 10 times smaller. Longer bars are the inputs that matter most. This is a local sensitivity, not a variance decomposition.

{
  const items = tornadoItems;
  const rows = items.flatMap(d => [{label: d.label, side: `low (${d.lowLabel})`, x: d.logLow, val: d.low}, {label: d.label, side: `high (${d.highLabel})`, x: d.logHigh, val: d.high}]);
  const order = items.map(d => d.label);
  return html`${Plot.plot({width: w1, height: 40 + 32 * items.length, marginLeft: 150, marginRight: 40,
    ariaLabel: "One-at-a-time sensitivity of the value of the mitigation",
    ariaDescription: items.length ? `Largest effect: ${items[0].label}. Inputs from largest to smallest effect: ${order.join(", ")}.` : "",
    x: {label: "log10( E(M) after the change / E(M) now )", grid: true}, y: {domain: order, label: null},
    color: {legend: true, domain: ["low", "high"], range: ["#c05621", "#2b6cb0"]},
    marks: [Plot.barX(rows, {y: "label", x: "x", fill: d => d.side.startsWith("low") ? "low" : "high", fillOpacity: 0.75}),
      Plot.ruleX([0])]})}
  <table class="compact table" style="width:auto;"><thead><tr><th>Input</th><th>E(M) at low</th><th>E(M) now</th><th>E(M) at high</th></tr></thead>
  <tbody>${items.map(d => html`<tr><td>${d.label} (${d.lowLabel} / ${d.highLabel})</td><td>${fmt(d.low)}</td><td>${fmt(d.base)}</td><td>${fmt(d.high)}</td></tr>`)}</tbody></table>`;
}

Share or submit your results

Three ways to share what you found. Copy a link that reopens this page with your settings. Copy, email or download a summary of the results. Or submit the scenario to the shared list, with a few words on why you chose these values; we’d especially like scenarios from people who have thought hard about one of the inputs. You can also paste the summary into a Hypothes.is annotation on this page.

Two things to know. The link and the summary carry all your settings, so anyone you send them to sees your assumptions. And Hypothes.is annotations are public, so don’t paste anything confidential into one. The downloads include the engine version, settings schema and build, so a result can be checked against the model that produced it.

summaryText = {
  const st = stats, d = M.describeSettings(settings);
  const lines = [
    `X-risk mitigation value model (The Unjournal), engine ${M.MODEL_VERSION}${manifest && manifest.git_commit ? `, build ${manifest.git_commit.slice(0, 7)}` : ""}`,
    shareUrl, "",
    `Risk: ${d.risk}`, `Value: ${d.value}`, `Mitigation: ${d.mitigation}`, `Horizon: ${d.horizon}`,
    `Beyond the paper: ${d.beyond.length ? d.beyond.join("; ") : "none (the report's model)"}`,
    `Where the settings come from: ${Object.entries(provenance.counts).map(([k, n]) => `${n} ${M.SOURCE_LABELS[k]}`).join(", ")}${provenance.rows.some(r => r.source === "user") ? ` (yours: ${provenance.rows.filter(r => r.source === "user").map(r => r.label).join(", ")})` : ""}`, "",
    `E(w), value of the future: ${fmt(st.ew, 4)} v_c`,
    `E(w'), with the mitigation: ${fmt(st.ewm, 4)} v_c`,
    `E(M), value of the mitigation: ${fmt(st.EM, 4)} v_c (years of today's world)`,
    `Share of E(M) after year 500: ${st.offsetting ? "n/a (gains and losses largely cancel)" : pct(st.share500, 1)}`
  ];
  if (mc) lines.push(`Monte Carlo (${mc.n} draws, seed ${mc.seed}): mean ${fmt(mc.mean)}, median ${fmt(mc.median)}, p5 ${fmt(mc.p5)}, p95 ${fmt(mc.p95)}, chance net harmful ${pct(mc.pHarmful)}`);
  if (bucketsOn) {
    const b = M.bucketValue(scenario.diff, params.beyond.buckets.edges, params.beyond.buckets.factors);
    lines.push(`Time buckets: ${pct(b.ratio)} of E(M) kept after the factors; ${pct(b.rows[5].shareRaw)} of E(M) arrives after 500 years`);
  }
  return lines.join("\n");
}
resultsRecord = ({
  tool: "X-risk mitigation value model (The Unjournal)", engine_version: M.MODEL_VERSION, settings_schema: M.SETTINGS_SCHEMA_VERSION,
  build: manifest ? {git_commit: manifest.git_commit, built: manifest.built} : null, url: shareUrl, created: new Date().toISOString(),
  settings, description: M.describeSettings(settings).text,
  sources: Object.fromEntries(provenance.rows.map(r => [r.key, M.SOURCE_LABELS[r.source]])),
  results: {E_w: stats.ew, E_w_mitigated: stats.ewm, E_M: stats.EM, share_of_E_M_after_500: Number.isFinite(stats.share500) ? stats.share500 : null,
    bound_on_share_left_out_by_T: Number.isFinite(stats.leftOut) ? stats.leftOut : null},
  monte_carlo: mc ? {n: mc.n, seed: mc.seed, mean: mc.mean, median: mc.median, p5: mc.p5, p95: mc.p95, p_net_harmful: mc.pHarmful, p_f_negative: mc.pNegativeF} : null,
  time_buckets: bucketsOn ? M.bucketValue(scenario.diff, params.beyond.buckets.edges, params.beyond.buckets.factors).rows : null
})
{
  const fb = html`<div class="share-feedback" role="status" aria-live="polite"></div>`;
  const say = t => { fb.textContent = t; };
  const stamp = new Date().toISOString().slice(0, 10);
  const b1 = html`<button type="button">Copy link</button>`;
  b1.onclick = async () => say((await copyText(shareUrl)) ? "Link copied." : "Couldn't copy here; select the link below.");
  const b2 = html`<button type="button">Copy summary</button>`;
  b2.onclick = async () => say((await copyText(summaryText)) ? "Summary copied." : "Couldn't copy here; select the text below.");
  const b3 = html`<button type="button">Download JSON</button>`;
  b3.onclick = () => download(`xrisk-scenario-${stamp}.json`, JSON.stringify(resultsRecord, null, 2), "application/json");
  const b4 = html`<button type="button">Download CSV</button>`;
  b4.onclick = () => {
    const esc = v => { const s = v === null || v === undefined ? "" : String(v); return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; };
    const rows = [["item", "value"], ["engine_version", M.MODEL_VERSION], ["url", shareUrl], ["description", resultsRecord.description],
      ...Object.entries(resultsRecord.results), ...(resultsRecord.monte_carlo ? Object.entries(resultsRecord.monte_carlo).map(([k, v]) => ["monte_carlo_" + k, v]) : []),
      ...Object.entries(settings).map(([k, v]) => ["setting_" + k, v])];
    download(`xrisk-scenario-${stamp}.csv`, rows.map(r => r.map(esc).join(",")).join("\n"), "text/csv");
  };
  const mail = `mailto:?subject=${encodeURIComponent("A scenario in the x-risk mitigation value model")}&body=${encodeURIComponent(summaryText)}`;
  const ta = html`<textarea class="share-summary" readonly aria-label="Summary of these results"></textarea>`;
  ta.value = summaryText;
  return html`<div class="share-box">
    <div class="share-actions">${b1}${b2}${b3}${b4}<a class="btn-like" href=${mail}>Email it</a></div>
    ${fb}
    <p style="margin:0.2rem 0 0.4rem;word-break:break-all;font-size:0.88rem;"><a href=${shareUrl}>${shareUrl}</a></p>
    ${ta}
  </div>`;
}

Submit this scenario to the shared list

The form sends your current settings and results along with what you write below. We read every submission before anything goes on the shared scenarios page, and we publish it only if you tick the box. Your email address is never published.

// Keep the form's hidden fields in step with the current settings and results.
syncSubmitForm = {
  const f = document.getElementById("xr-submit-form");
  if (f) {
    f.elements.settings_url.value = shareUrl;
    f.elements.settings_query.value = M.settingsToQuery(settings);
    f.elements.results_summary.value = summaryText;
    f.elements.engine_version.value = M.MODEL_VERSION;
  }
  return true;
}

Formulas and parameters: Formulas. Exact comparison with the report: Replication. Everything that differs from the 2023 report, and how Rethink Priorities’ current model compares: Departures. How this tool relates to other models, tools and research: Context. Scenarios other people have submitted: Shared scenarios. Questions, corrections and suggestions: Comment.

 

Built by The Unjournal with AI assistance · Draft for comment; not reviewed or endorsed by the report’s author or Rethink Priorities · How to comment · contact@unjournal.org