X-risk mitigation value model
  • Model
  • 2023 report
    • Replication
    • Formulas
    • Departures from the report
    • Report errata (Notion)
  • RP 2026 GCR model (exploratory)
    • Interactive model (unofficial reconstruction)
    • What drives the result
    • How close is the reconstruction
    • The 10³² puzzle
    • Questions for RP
    • How it relates to the 2023 report
    • Technical notes
  • Background & feedback
    • Donor Compass
    • Literature
    • Context
    • Shared scenarios
    • How to comment
    • About this tool
  • 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.

RP’s 2026 GCR fund model: interactive reconstruction

How many years of life does $1M to a global catastrophic risk fund buy? Change the assumptions and see

Unofficial reconstruction from RP's public documentation, not RP's model. Draft for review: exploratory and premature. It was largely generated by AI and hasn't been verified by us or reviewed by Rethink Priorities. Don't cite these numbers as RP's, or use them for giving decisions.
Help improve this draftComment with Hypothes.is; flag @daaronr for a response.

Select any text, chart or number and annotate it with Hypothes.is (the tab at the top right) to flag errors, missing sources, or a clearer way to show something. David plans to respond, especially where a comment mentions @daaronr. Public Hypothes.is annotations can be read by anyone who opens this page, so don't post anything confidential.

How this was made: an AI agent (Claude, working for The Unjournal) rebuilt Rethink Priorities' GCR fund model from RP's public write-up, update log and Donor Compass data, and built these pages. David Reinstein set the task. Neither he nor RP has checked the numbers yet, and RP hasn't seen this page.

How to use Hypothes.is Read comments on this page

Model What drives the result How close is it The 1032 puzzle Questions for RP 2023 report vs 2026 model Technical notes

Rethink Priorities (RP) uses a model to estimate how many years of human life $1M to each of three funds working on global catastrophic risks (GCR) buys, on average. It simulates many possible futures. In each, the risk of an existential catastrophe rises to a peak in the coming decades and then falls to a low long-run level, which sets the chance that civilization survives each year; each surviving year is worth some number of life-years (far more if humanity settles the stars); and a fund’s money trims its cause’s share of the risk for a few years, or does nothing, or backfires. The extra expected life-years are split into six time windows, from the next 5 years to beyond 500. These numbers feed RP’s Cross-Cause Fund and Donor Compass.

This page runs our reconstruction of that model in your browser. Start from a preset, or change any assumption on the left; results update in a few seconds. Hover over or tap any ? for an explanation.

D = import(new URL("./engine/dashboard.mjs", window.location.href).href)
UI = import(new URL("./panel.mjs", window.location.href).href)
Plot = import("https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6.17/+esm")
snap = fetch(new URL("../rp-donor-compass.json", window.location.href).href).then(r => { if (!r.ok) throw new Error(`couldn't load RP's data (${r.status})`); return r.json(); })
rp = D.rpPublished(snap)
// Starting values: RP's (as reconstructed), overridden by any ?key=value in the link.
fromUrl = D.settingsFromQuery(window.__RG_URL_STATE__ || {})
tipEl = UI.tipEl
// 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); };
  const ro = new ResizeObserver(later);
  ro.observe(el);
  window.addEventListener("resize", later);
  measure();
  return () => { ro.disconnect(); window.removeEventListener("resize", later); clearTimeout(timer); };
})
chartW = Math.max(300, Math.min(1000, fillWidth - 8))
narrow = chartW < 600
fundName = f => narrow ? f.tiny : f.short
fundScale = ({domain: D.FUNDS.map(f => f.key), range: D.FUNDS.map(f => f.color)})
atDefaults = D.changedKeys(settings).filter(k => k !== "n" && k !== "seed").length === 0
// Ticks on a signed-log axis: every 1, 2 or 4 powers of ten, depending on the range.
slogTicks = (lo, hi) => { const span = hi - lo, step = span > 30 ? 4 : span > 14 ? 2 : 1, t = []; for (let k = Math.ceil(lo / step) * step; k <= hi + 1e-9; k += step) t.push(k); return t; }
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);
}
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; }
  }
}
// Presets: each starts from RP's values and changes one thing.
{
  const cur = D.matchPreset(settings);
  const wrap = html`<div class="rg-presets" role="group" aria-label="Presets"><span class="rg-presets-label">Start from:</span></div>`;
  for (const p of D.PRESETS) {
    const b = html`<button type="button">${p.label}</button>`;
    if (cur === p.id) b.classList.add("on");
    b.setAttribute("aria-pressed", cur === p.id ? "true" : "false");
    b.title = p.note;
    b.onclick = () => viewof settings.setSettings(D.presetSettings(p.id, settings));
    wrap.append(b);
  }
  const nChanged = D.changedKeys(settings).filter(k => k !== "n" && k !== "seed").length;
  const note = cur ? D.PRESETS.find(p => p.id === cur).note : `Your own settings: ${nChanged} changed from RP's values (marked "your choice" on the left).`;
  return html`${wrap}<div class="rg-preset-note" aria-live="polite">${note}</div>
    <p class="rg-phone-only rg-note"><a href="#xr-settings">Fine-tune every setting</a> (below the results).</p>`;
}
viewof settings = UI.controlsPanel(D, fromUrl.settings)

Each control moves the centre of RP’s distribution for that input and keeps its relative spread. The line under each control shows the distribution actually used.

worker = {
  const w = new Worker(new URL("./engine/worker.mjs", window.location.href).href, {type: "module"});
  invalidation.then(() => w.terminate());
  return w;
}
workerResult = Generators.observe(notify => {
  const f = e => { if (e.data && e.data.type === "result") notify(e.data); };
  worker.addEventListener("message", f);
  return () => worker.removeEventListener("message", f);
})
workerStatus = Generators.observe(notify => {
  const f = e => { if (e.data && ["progress", "result", "error"].includes(e.data.type)) notify(e.data); };
  const g = e => notify({type: "error", message: (e && e.message) || "the simulation couldn't start in this browser"});
  worker.addEventListener("message", f);
  worker.addEventListener("error", g);
  notify({type: "starting"});
  return () => { worker.removeEventListener("message", f); worker.removeEventListener("error", g); };
})
// Wait until the controls have been still for a moment, so dragging a slider doesn't queue runs.
settled = { await new Promise(r => setTimeout(r, 350)); return settings; }
runRequest = {
  const id = `run-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
  worker.postMessage({type: "run", id, settings: settled});
  return {id, key: D.settingsKey(settled)};
}
stale = !(workerStatus.type === "result" && workerStatus.key === D.settingsKey(settings))
markStale = { document.body.classList.toggle("rg-stale", stale); return stale; }
statusBox = {
  const st = workerStatus;
  if (st.type === "error") return html`<div class="rg-status err" role="alert"><strong>The simulation stopped:</strong> ${st.message}. Try reloading the page. If it keeps happening, please leave a comment.</div>`;
  if (st.type !== "result") {
    const done = st.done || 0, total = st.total || settled.n;
    return html`<div class="rg-status" role="status"><span>Simulating ${done ? `${done.toLocaleString("en-US")} of ${total.toLocaleString("en-US")}` : `${total.toLocaleString("en-US")}`} draws…</span><span class="rg-progress" aria-hidden="true"><span style="width:${Math.round(100 * done / total)}%"></span></span></div>`;
  }
  const more = html`<button type="button" class="rg-btn">Run ${st.settings.n.toLocaleString("en-US")} more draws</button>`;
  more.title = "Add draws to this run. The averages get steadier, especially the 500+ window.";
  more.disabled = stale;
  more.onclick = () => { more.disabled = true; worker.postMessage({type: "more", id: `more-${Date.now()}`, key: st.key, add: st.settings.n}); };
  const notes = (st.notes || []).length ? html`<div style="flex-basis:100%" class="rg-note">${st.notes.join(" ")}</div>` : "";
  return html`<div class="rg-status" role="status"><span>${stale ? "Updating for your new settings… " : ""}Based on <strong>${st.n.toLocaleString("en-US")}</strong> simulated worlds (seed ${st.seed}), ${(st.ms / 1000).toFixed(1)} s. ${tipEl("Each draw is one simulated world: one value for every uncertain input, and the funds' effects in that world. The averages are over draws. The '±' figures are rough 95% Monte Carlo ranges (two standard errors from ten batches of draws); for the 500+ window they understate the noise, because its average rests on a few hundred rare draws. RP uses 10 million draws.")}</span>${more}${notes}</div>`;
}
fromUrl.issues.length ? html`<div class="rg-issues" role="status"><strong>Some values in the link couldn't be used as given:</strong><ul>${fromUrl.issues.map(i => html`<li>${i}</li>`)}</ul></div>` : html`<span></span>`
S = D.summarize(workerResult)
resSettings = workerResult.settings
resAtDefaults = D.changedKeys(resSettings).filter(k => k !== "n" && k !== "seed").length === 0
beyondText = s => {
  if (!(s.all > 0) || !(s.total500rb > 0)) return "n/a (mixed signs)";
  const rest = s.near / s.all;
  return rest > 0 && rest < 1e-3 ? `all but 10${D.sup(-Math.round(-Math.log10(rest)))}` : D.fmtPct(s.shareBeyond500, 3);
}
pm = s => `±${D.fmtPct(Math.abs(2 * s.se.all / s.all), 2)}`
cards = {
  const card = f => {
    const s = S.funds[f.key], r = rp.funds[f.key];
    return html`<div class="rg-card" style="--fund:${f.color}">
      <h3>${f.short}</h3>
      <div class="rg-big" title=${`${D.fmtFull(s.all)} life-years per $1M`}>${D.fmt(s.all)}</div>
      <div class="rg-unit-line">expected life-years per $1M, all time ${tipEl("Average over the simulated worlds of the extra years of human life the $1M buys, added up over all six windows. The 500+ window uses a lower-noise estimate that averages over whether humanity settles the stars instead of sampling it (same expected value).")}</div>
      <dl>
        <dt><span tabindex="0" data-tip="Expected life-years per $1M in the first 500 years: existential plus non-existential effects.">First 500 years only</span></dt><dd title=${D.fmtFull(s.near)}>${D.fmt(s.near)}</dd>
        <dt><span tabindex="0" data-tip="How much of the expected total comes from after year 500.">Share after year 500</span></dt><dd>${beyondText(s)}</dd>
        <dt><span tabindex="0" data-tip="Share of simulated worlds in which the fund's money raises risk, so its total effect is negative.">Chance net harmful</span></dt><dd class=${s.pHarm > 0.1 ? "harm" : ""}>${D.fmtPct(s.pHarm, 2)}</dd>
        <dt><span tabindex="0" data-tip="Share of simulated worlds in which the money changes nothing.">Chance of no effect</span></dt><dd>${D.fmtPct(s.pZero, 2)}</dd>
        <dt><span tabindex="0" data-tip="Rough 95% range from sampling noise alone (two standard errors). Run more draws to narrow it.">Monte Carlo range</span></dt><dd>${pm(s)}</dd>
        <dt><span tabindex="0" data-tip=${`RP's Donor Compass data, ${rp.name} (risk-neutral), all windows added up. It is for RP's own settings, so it stays fixed when you change a control.`}>RP's published value</span></dt><dd title=${D.fmtFull(r.all)}>${D.fmt(r.all)}</dd>
      </dl></div>`;
  };
  const ai = S.aiToBio, rpAll = rp.funds.longview_ai.all / rp.funds.sentinel_bio.all, rp20 = rp.funds.longview_ai.total[3] / rp.funds.sentinel_bio.total[3];
  const ratio = x => Number.isFinite(x) ? (Math.abs(x) >= 100 || Math.abs(x) < 0.01 ? D.fmt(x, 2) : `${(+x.toPrecision(2))}`) : "n/a";
  return html`<div class="rg-cards">${D.FUNDS.map(card)}</div>
    <p class="rg-ratio-line"><strong>AI fund versus Sentinel Bio:</strong> ${ratio(ai.all)} times as much, all time; ${ratio(ai.w20_100)} times over years 20-100 (RP's data: ${ratio(rpAll)} and ${ratio(rp20)}). ${tipEl("The AI fund cuts a smaller share of its cause's risk per $1M (0.004% vs 0.02%), but AI's share of all risk is 30 times bio's in RP's inputs, so it comes out ahead. See 'Ranking and robustness'.")}</p>`;
}
  • By time window
  • Spread of outcomes
  • Where the value sits
  • Donor-Compass-style score
  • Ranking and robustness
winRows = D.FUNDS.flatMap(f => D.WINDOWS.map((w, k) => {
  const s = S.funds[f.key];
  const ours = k < 5 ? s.total[k] : s.total500rb, e = k < 5 ? s.se.win[k] : s.se.w500rb;
  const rpv = rp.funds[f.key].total[k];
  return {fund: f.key, f, window: w, k, ours, se: e, lo: ours - 2 * e, hi: ours + 2 * e, rp: rpv, ratio: ours / rpv};
}))
winChart = {
  const ys = winRows.flatMap(d => [D.slog(d.lo), D.slog(d.hi), D.slog(d.rp), D.slog(d.ours)]);
  const y0 = Math.min(0, Math.floor(Math.min(...ys))), y1 = Math.ceil(Math.max(...ys)) + 1;
  return Plot.plot({
    width: chartW, height: narrow ? 360 : 400, marginLeft: 52, marginBottom: narrow ? 58 : 34, marginTop: 26,
    ariaLabel: "Expected life-years per $1M by time window for the three funds, with RP's published values",
    ariaDescription: `Signed log scale. Our values for Sentinel Bio, Longview Nuclear and Longview Frontier AI rise from about 10 to the 3rd in the 0-5 year window to about 10 to the ${Math.round(D.slog(S.funds.sentinel_bio.total500rb))}th in the 500+ window. RP's published values are shown as hollow diamonds. The table below has every number.`,
    fx: {domain: D.WINDOWS, label: null, padding: 0.08, tickRotate: narrow ? -35 : 0, tickFormat: w => `${w} yrs`},
    x: {domain: D.FUNDS.map(f => f.key), axis: null, padding: 0.3},
    y: {domain: [y0, y1], ticks: slogTicks(y0, y1), tickFormat: D.slogTick, grid: true, label: "↑ Life-years per $1M (log scale)"},
    color: fundScale,
    marks: [
      Plot.ruleY([0], {stroke: "#a0aec0"}),
      Plot.ruleX(winRows, {fx: "window", x: "fund", y1: d => D.slog(d.lo), y2: d => D.slog(d.hi), stroke: "fund", strokeWidth: 3, strokeOpacity: 0.45}),
      Plot.dot(winRows, {fx: "window", x: "fund", y: d => D.slog(d.ours), r: 5, fill: "fund", stroke: "white", strokeWidth: 1.5}),
      Plot.dot(winRows, {fx: "window", x: "fund", y: d => D.slog(d.rp), symbol: "diamond", r: 6.5, stroke: "#1a202c", strokeWidth: 1.5, fill: "none"}),
      Plot.tip(winRows, Plot.pointer({fx: "window", x: "fund", y: d => D.slog(d.ours),
        title: d => `${d.f.short}, ${d.window} years\nThis reconstruction: ${D.fmt(d.ours)} (±${D.fmt(2 * d.se, 2)})\nRP's published value: ${D.fmt(d.rp)}\nOurs / RP: ${D.fmt(d.ratio, 2)}`}))
    ]
  });
}
html`<div class="rg-legend">${D.FUNDS.map(f => html`<span><i class="rg-swatch" style="background:${f.color}"></i>${f.short} (this reconstruction; bar = ±2 standard errors)</span>`)}<span><i class="rg-swatch rp"></i>RP's published value (June 2026 data)</span></div>`

Each step up the axis is ten times more. The early windows hold thousands of life-years per $1M, the 500+ window about \(10^{20}\): almost all of each fund’s expected value is in the far future. The diamonds are RP’s numbers for RP’s own settings, so they stay put when you change a control; at RP’s values the dots should sit on them.

winTable = {
  const cls = r => !resAtDefaults ? "" : Math.abs(Math.log(r)) < Math.log(1.1) ? "rg-ok" : Math.abs(Math.log(r)) < Math.log(1.25) ? "rg-warn" : "rg-bad";
  const rows = D.FUNDS.flatMap(f => winRows.filter(d => d.fund === f.key).map((d, i) => html`<tr>
    <td>${i === 0 ? html`<strong>${f.short}</strong>` : ""}</td><td>${d.window}</td>
    <td class="num" title=${D.fmtFull(d.ours)}>${D.fmt(d.ours)}</td><td class="num">±${D.fmt(2 * d.se, 2)}</td>
    <td class="num" title=${D.fmtFull(d.rp)}>${D.fmt(d.rp)}</td><td class=${"num " + cls(d.ratio)}>${D.fmt(d.ratio, 2)}</td></tr>`));
  return html`<details><summary>The numbers in the chart</summary><table class="table rg-table"><thead><tr><th>Fund</th><th>Years</th><th class="num">This reconstruction</th><th class="num">± (MC)</th><th class="num">RP's data</th><th class="num">Ours / RP</th></tr></thead>
    <tbody>${rows}</tbody><tfoot><tr><td colspan="6">Expected life-years per $1M, existential plus non-existential effects. RP's data: Donor Compass dataset ${rp.name}, risk-neutral column. ${resAtDefaults ? "Shading: within 10% of RP (green), 10-25% (yellow), further (red)." : "Your settings differ from RP's, so the ratio shows how far your change moves each number from RP's."} The 500+ row uses the lower-noise estimate.</td></tr></tfoot></table></details>`;
}

How the outcome for $1M is spread across simulated worlds, all windows added up. In about half the worlds the money has no effect (the vertical jump at zero); in a few it raises risk (left of zero); the average is set by rare worlds far to the right.

cdfChart = {
  const rows = D.FUNDS.flatMap(f => S.funds[f.key].cdf.map(pt => ({fund: f.key, f, p: pt.p, v: pt.x, x: D.slog(pt.x)})));
  const rpPts = resAtDefaults ? D.FUNDS.flatMap(f => [[1, "p1"], [5, "p5"], [10, "p10"], [50, "p50"], [90, "p90"], [95, "p95"], [99, "p99"]].map(([p, k]) => ({fund: f.key, f, p: p / 100, v: D.RP_ALL_EFFECTS[f.key][k], x: D.slog(D.RP_ALL_EFFECTS[f.key][k])}))) : [];
  const xs = [...rows, ...rpPts].map(d => d.x);
  const lo = Math.floor(Math.min(...xs)) - 1, hi = Math.ceil(Math.max(...xs)) + 1;
  return Plot.plot({
    width: chartW, height: 340, marginLeft: 50, marginBottom: 40, marginTop: 26,
    ariaLabel: "Cumulative distribution of life-years per $1M across simulated worlds, for each fund",
    ariaDescription: "Signed log scale on the horizontal axis. Each line jumps at zero, where about half the draws sit; a small share of draws is negative; the 99th percentile is around 10 to the 15th or 16th.",
    x: {domain: [lo, hi], ticks: slogTicks(lo, hi), tickFormat: D.slogTick, grid: true, label: "Life-years per $1M, all windows (signed log scale) →"},
    y: {domain: [0, 1], tickFormat: "%", grid: true, label: "↑ Share of worlds at or below"},
    color: fundScale,
    marks: [
      Plot.ruleX([0], {stroke: "#4a5568"}),
      Plot.line(rows, {x: "x", y: "p", stroke: "fund", strokeWidth: 2, z: "fund"}),
      Plot.dot(rpPts, {x: "x", y: "p", stroke: "fund", symbol: "diamond", r: 4.5, strokeWidth: 1.5, fill: "white"}),
      Plot.tip(rows, Plot.pointerY({x: "x", y: "p", title: d => `${d.f.short}\n${D.fmtPct(d.p, 3)} of worlds at or below ${D.fmt(d.v)} life-years per $1M`}))
    ]
  });
}
html`<div class="rg-legend">${D.FUNDS.map(f => html`<span><i class="rg-swatch" style="background:${f.color}"></i>${f.short}</span>`)}${resAtDefaults ? html`<span><i class="rg-swatch rp"></i>RP's published percentiles (write-up table)</span>` : ""}</div>
  <p class="rg-fig-note">Sentinel Bio's line is hidden under the nuclear fund's: the two share every input except when their effects start, so their spreads are almost identical.</p>`
pctTable = {
  const ps = [1, 5, 10, 50, 90, 95, 99, 99.9];
  const rpKey = {1: "p1", 5: "p5", 10: "p10", 50: "p50", 90: "p90", 95: "p95", 99: "p99"};
  const rows = D.FUNDS.flatMap(f => {
    const s = S.funds[f.key], r = D.RP_ALL_EFFECTS[f.key];
    return [html`<tr><td><strong>${f.short}</strong></td><td>ours</td><td class="num">${D.fmtPct(s.pHarm, 2)}</td>${ps.map(p => html`<td class="num" title=${D.fmtFull(s.pct[p])}>${D.fmt(s.pct[p], 2)}</td>`)}<td class="num">${D.fmt(s.all, 2)}</td></tr>`,
      html`<tr><td></td><td>RP</td><td class="num"></td>${ps.map(p => html`<td class="num">${rpKey[p] ? D.fmt(r[rpKey[p]], 1) : ""}</td>`)}<td class="num">${D.fmt(r.mean, 1)}</td></tr>`];
  });
  return html`<table class="table rg-table"><thead><tr><th>Fund</th><th></th><th class="num">Harmful</th>${ps.map(p => html`<th class="num">${D.ordinal(p)}</th>`)}<th class="num">Mean</th></tr></thead><tbody>${rows}</tbody>
    <tfoot><tr><td colspan="12">Percentiles of life-years per $1M across simulated worlds, all windows. "Harmful": share of worlds with a negative total. RP's row is the "All Effects" table in RP's write-up (one significant figure), for RP's settings.</td></tr></tfoot></table>`;
}
valueText = {
  const b = S.funds.sentinel_bio, v = S.valueOfFuture;
  return html`<div class="rg-key"><p><strong>Almost all of it is after year 500.</strong> For Sentinel Bio the first 500 years add up to ${D.fmt(b.near)} life-years per $1M; the 500+ window holds ${D.fmt(b.total500rb)}. The pattern is the same for the other two funds.</p>
  <p><strong>Why:</strong> a fund cuts risk only for a decade or so, but surviving that decade raises the chance of being alive in every later year. The expected value of the whole future is about ${D.fmt(v.meanRB)} life-years (RP: 6 × 10²⁶). In ${D.fmtPct(v.shareSpaceDraws, 2)} of simulated worlds humanity settles the stars, and those worlds carry ${D.nearlyAll(v.spaceShareOfMean)} of that expected value.</p></div>`;
}

The first 500 years, window by window. Share of each fund’s expected life-years in the first 500 years, and how much of each window comes from preventing existential catastrophe rather than smaller catastrophes (10 million to 8 billion deaths, counted for the next 100 years only).

nearTable = {
  const rows = D.WINDOWS.slice(0, 5).map((w, k) => html`<tr><td>${w}</td>${D.FUNDS.map(f => {
    const s = S.funds[f.key], tot = s.total[k];
    const share = tot / s.near, exShare = s.ext[k] / tot;
    return html`<td class="num" title=${D.fmtFull(tot)}>${D.fmtPct(share, 2)}</td><td class="num">${Number.isFinite(exShare) ? D.fmtPct(exShare, 2) : "n/a"}</td>`;
  })}</tr>`);
  return html`<table class="table rg-table"><thead><tr><th rowspan="2">Years</th>${D.FUNDS.map(f => html`<th colspan="2" class="num">${f.short}</th>`)}</tr>
    <tr>${D.FUNDS.map(() => html`<th class="num">Share of first 500 years</th><th class="num">Existential part</th>`)}</tr></thead><tbody>${rows}</tbody>
    <tfoot><tr><td colspan="7">Non-existential effects dominate the first 20 years; after year 20 nearly everything comes from cutting existential risk.</td></tr></tfoot></table>`;
}

The value of the future itself (life-years, before any fund acts), percentiles across simulated worlds, against RP’s published table.

vofChart = {
  const v = S.valueOfFuture, R = D.RP_VALUE_OF_FUTURE;
  const ps = [[0.1, "p0_1"], [1, "p1"], [5, "p5"], [50, "p50"], [95, "p95"], [99, "p99"], [99.9, "p99_9"]];
  const rows = ps.flatMap(([p, k]) => [{who: "This reconstruction", p: D.ordinal(p), x: Math.log10(v.pct[p]), v: v.pct[p]}, {who: "RP", p: D.ordinal(p), x: Math.log10(R[k]), v: R[k]}]);
  rows.push({who: "This reconstruction", p: "Mean", x: Math.log10(v.meanRB), v: v.meanRB}, {who: "RP", p: "Mean", x: Math.log10(R.mean), v: R.mean});
  const lo = Math.floor(Math.min(...rows.map(d => d.x))) - 1, hi = Math.ceil(Math.max(...rows.map(d => d.x))) + 1;
  return Plot.plot({
    width: Math.min(chartW, 760), height: 260, marginLeft: 60, marginBottom: 38,
    ariaLabel: "Value of the future: percentiles in this reconstruction and in RP's table",
    ariaDescription: "Log scale. The median is about 2 times 10 to the 16th life-years; the 99th percentile about 10 to the 27th; the mean about 6 times 10 to the 26th, close to RP's.",
    x: {domain: [lo, hi], ticks: slogTicks(lo, hi), tickFormat: k => D.slogTick(k), grid: true, label: "Value of the future, life-years (log scale) →"},
    y: {domain: [...ps.map(([p]) => D.ordinal(p)), "Mean"], label: null},
    color: {domain: ["This reconstruction", "RP"], range: ["#2b6cb0", "#1a202c"], legend: true},
    marks: [
      Plot.dot(rows.filter(d => d.who === "This reconstruction"), {x: "x", y: "p", fill: "who", r: 5}),
      Plot.dot(rows.filter(d => d.who === "RP"), {x: "x", y: "p", stroke: "who", symbol: "diamond", r: 6, strokeWidth: 1.5, fill: "none"}),
      Plot.tip(rows, Plot.pointer({x: "x", y: "p", title: d => `${d.who}, ${d.p}: ${D.fmt(d.v)} life-years`}))
    ]
  });
}

RP’s percentiles are from its write-up (one significant figure). Our 99.9th percentile is about 8 times RP’s while the mean and the rest match; see How close is it. RP’s table is for RP’s settings; ours follows yours.

RP’s Donor Compass doesn’t use the total. Each worldview weights the six windows and then turns the spread of outcomes into one number using a risk attitude. This tab does a simple version of that for the three funds, so you can see how much the ranking and even the sign depend on those two choices.

viewof factors = UI.factorsInput(D, D.FACTOR_SETS[0].factors)
viewof attitude = Inputs.select(D.ATTITUDES.map(a => a.id), {label: "Risk attitude", format: id => D.ATTITUDES.find(a => a.id === id).label, value: "neutral"})
viewof cutoff = Inputs.select(D.CUTOFFS, {label: "Cap at percentile", format: p => D.ordinal(p), value: 99, disabled: attitude === "neutral"})
viewof nonAiCut = Inputs.toggle({label: "Apply RP's 40% cut to the non-AI funds", value: false})
score = D.scoreFunds(workerResult, factors, attitude, cutoff, {nonAiCut: nonAiCut ? 0.4 : 0})
scoreView = {
  const rows = D.FUNDS.map(f => ({fund: f.key, f, label: fundName(f), score: score.funds[f.key].score, se: score.funds[f.key].se}));
  const xs = rows.map(d => D.slog(d.score));
  const lo = Math.min(0, Math.floor(Math.min(...xs))) - (Math.min(...xs) < 0 ? 1 : 0), hi = Math.max(0, Math.ceil(Math.max(...xs))) + 2;
  const chart = Plot.plot({
    width: Math.min(chartW, 860), height: 170, marginLeft: narrow ? 70 : 150, marginRight: 20, marginBottom: 38,
    ariaLabel: "Score per fund under the chosen weights and risk attitude",
    ariaDescription: rows.map(d => `${d.f.short}: ${D.fmt(d.score)}`).join("; "),
    x: {domain: [lo, hi], ticks: slogTicks(lo, hi), tickFormat: D.slogTick, grid: true, label: "Score (signed log scale) →"},
    y: {domain: rows.map(d => d.label), label: null},
    color: fundScale,
    marks: [
      Plot.barX(rows, {y: "label", x1: 0, x2: d => D.slog(d.score), fill: "fund", rx: 3, insetTop: 4, insetBottom: 4}),
      Plot.ruleX([0], {stroke: "#1a202c"}),
      Plot.text(rows.filter(d => d.score >= 0), {y: "label", x: d => D.slog(d.score), text: d => D.fmt(d.score), dx: 6, textAnchor: "start", fill: "#1a202c", fontSize: 12}),
      Plot.text(rows.filter(d => d.score < 0), {y: "label", x: d => D.slog(d.score), text: d => D.fmt(d.score), dx: -6, textAnchor: "end", fill: "#1a202c", fontSize: 12}),
      Plot.tip(rows, Plot.pointerY({y: "label", x: d => D.slog(d.score), title: d => `${d.f.short}: ${D.fmt(d.score)} (±${D.fmt(2 * d.se, 2)})`}))
    ]
  });
  const order = score.ranking.order.map(k => D.FUND_BY_KEY[k].short);
  const neg = rows.filter(d => d.score < 0).map(d => d.f.short);
  const att = D.ATTITUDES.find(a => a.id === attitude);
  const s500 = rows.map(d => score.funds[d.fund].share500).filter(x => x !== null);
  return html`${chart}
    <ul class="rg-score-list">
      <li><strong>Ranking:</strong> ${order.join(" > ")}. The same order holds in ${score.ranking.sameOrder} of ${score.ranking.batches} equal batches of draws ${tipEl("A rough check on Monte Carlo noise: the draws are split into ten batches and the funds ranked within each.")}.</li>
      <li>${neg.length ? html`<span class="rg-neg">Negative for ${neg.join(", ")}:</span> under these choices the harmful outcomes outweigh the helpful ones.` : html`<span class="rg-pos">All three funds score above zero.</span>`}</li>
      ${attitude === "neutral" && s500.length ? html`<li>The 500+ window supplies ${s500.every(x => x > 0.999) ? "essentially all" : s500.map(x => D.fmtPct(x, 2)).join(", ")} of each score. Set its weight to 0 ("Nothing after 500 years") to see the first 500 years alone.</li>` : ""}
      <li class="rg-note">${att.note}${attitude !== "neutral" ? " It uses the outcomes as drawn, including the coin flip for settling the stars." : ""}</li>
    </ul>`;
}

Our simplification: no moral weights, no diminishing returns, no comparison with the non-GCR funds, and our own version of the risk attitudes. RP’s actual worldviews, risk profiles and allocation are on the Donor Compass page.

rankView = {
  const name = k => D.FUND_BY_KEY[k].short;
  const r = S.ranking;
  const byWin = S.rankingByWindow.map(x => {
    const k = D.WINDOWS.indexOf(x.window);
    const rpOrder = [...D.FUND_KEYS].sort((a, b) => rp.funds[b].total[k] - rp.funds[a].total[k]);
    return html`<tr><td>${x.window}</td><td>${x.order.map(name).join(" > ")}</td><td>${rpOrder.map(name).join(" > ")}</td></tr>`;
  });
  const bioNuc = r.pairs["sentinel_bio>longview_nuclear"];
  return html`<div class="rg-key"><p><strong>At these settings:</strong> ${r.order.map(name).join(" > ")} (all time). The same order holds in ${r.sameOrder} of ${r.batches} equal batches of draws, and ${name(r.order[0])} comes first in ${r.sameTop} of ${r.batches}. Sentinel Bio beats the nuclear fund in ${bioNuc} of ${r.batches} batches: ${bioNuc >= 3 && bioNuc <= 7 ? "they are effectively tied" : "a clear gap"}.</p></div>
    <table class="table rg-table"><thead><tr><th>Window</th><th>This reconstruction</th><th>RP's data</th></tr></thead><tbody>${byWin}</tbody></table>`;
}

What could flip it. From the sensitivity runs at RP’s values (What drives the result):

  • The AI fund comes first in every structural variant we tried, by about 2.3 to 2.6 times in both the 20-100 and 500+ windows: other hazard shapes, another way of applying the cut, other outcome models, Milky Way only, the pre-June star value, independent draws per fund.
  • Its lead rests on two inputs. AI’s share of the risk is 30 times bio’s in RP’s inputs; at the 95th percentile of the bio share (14%), Sentinel Bio would beat the AI fund. And at the 95th percentile of the AI fund’s chance of raising risk (45%), its expected value turns negative. Try the “More backfire risk” preset, or the AI fund’s “Chance it raises risk instead” slider.
  • Bio and nuclear are effectively tied. They share every input except when their effects start, so their order flips between seeds, as it does between RP’s own windows (nuclear ahead over 20-100 years, bio ahead after 500).
  • Changing long-run risk to a lognormal scrambles everything, because the averages stop being stable (the 10^32 puzzle).

Share these settings

shareBox = {
  const q = D.settingsToQuery(settings);
  const base = window.location.origin + window.location.pathname;
  const link = base + (q ? "?" + q : "");
  const drivers = new URL("drivers.html", window.location.href).href.split("?")[0] + (q ? "?" + q : "");
  const fb = html`<span class="rg-note" aria-live="polite"></span>`;
  const copy = html`<button type="button" class="rg-btn primary">Copy a link to these settings</button>`;
  copy.onclick = async () => { fb.textContent = (await copyText(link)) ? "Copied." : "Couldn't copy; the link is below."; };
  const csv = html`<button type="button" class="rg-btn">Download these results (CSV)</button>`;
  csv.onclick = () => {
    const lines = [["fund", "window", "life_years_per_1M", "mc_2se", "rp_published"].join(",")];
    for (const d of winRows) lines.push([d.fund, d.window, d.ours, 2 * d.se, d.rp].join(","));
    lines.push(`# ${D.MODEL_LABEL}. ${S.n} draws, seed ${S.seed}. Settings: ${q || "RP's values (as reconstructed)"}`);
    download("rp-gcr-reconstruction-results.csv", lines.join("\n") + "\n", "text/csv");
  };
  return html`<div class="rg-share-row">${copy}<a class="rg-btn" href=${drivers}>What drives the result at these settings</a>${csv}${fb}</div>
    <p class="rg-note" style="word-break:break-all">${link}</p>
    <p class="rg-note">The link carries only the settings you changed. Anyone you send it to sees the same settings; nothing is stored on a server.</p>`;
}
// Tell screen readers the new headline once a run finishes.
announce = {
  const el = document.getElementById("rg-live");
  if (el) el.textContent = `Updated: ${D.FUNDS.map(f => `${f.short} ${D.fmt(S.funds[f.key].all)} life-years per million dollars`).join("; ")}.`;
  return true;
}
 

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