Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions client/src/data/rto-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* RTO/ISO reserve margins and AI-load signals - NERC LTRA 2025 (2026
* projections). Single source of truth: the Power map, its operator table,
* and the overview's Grid Headroom gauge all read from here.
*/
export interface RTOConfig {
label: string;
reserveMargin: number;
aiSignal: "Critical" | "Elevated" | "Moderate" | "Low";
}

export const RTO_CONFIG: Record<string, RTOConfig> = {
PJM: { label: "PJM", reserveMargin: 17.5, aiSignal: "Elevated" },
MISO: { label: "MISO", reserveMargin: 13.4, aiSignal: "Critical" },
ERCOT: { label: "ERCOT", reserveMargin: 15.8, aiSignal: "Critical" },
WECC: { label: "WECC", reserveMargin: 24.6, aiSignal: "Moderate" },
SERC: { label: "SERC", reserveMargin: 23.1, aiSignal: "Moderate" },
SPP: { label: "SPP", reserveMargin: 27.8, aiSignal: "Low" },
NPCC: { label: "NPCC", reserveMargin: 26.4, aiSignal: "Low" },
};

export const RTO_SOURCE_NOTE = "NERC LTRA 2025 (2026 projections)";
165 changes: 165 additions & 0 deletions client/src/lib/__tests__/real-gauges.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* Real gauges: the numbers replacing the synthetic indices. A silent bug
* here puts a wrong headline on the front page - full coverage.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
buildBuildoutHistory,
computeTrackedPower,
filterTrackedFacilities,
fmtGW,
parseOpenDate,
tightestRTO,
type FacilityLite,
} from "../real-gauges";

const F = (status: string, powerMW: number | null, openDate?: string, name?: string): FacilityLite => ({
status,
powerMW,
openDate,
name,
});

describe("filterTrackedFacilities", () => {
it("applies the same >=400 MW floor the Power map advertises", () => {
const kept = filterTrackedFacilities([F("operational", 400), F("operational", 399), F("operational", null)]);
assert.equal(kept.length, 1);
assert.equal(kept[0].powerMW, 400);
});
it("empty and missing input yield empty output", () => {
assert.deepEqual(filterTrackedFacilities([]), []);
assert.deepEqual(filterTrackedFacilities(undefined as never), []);
});
});

describe("computeTrackedPower", () => {
it("buckets MW and counts by status; tracked = operational + construction", () => {
const t = computeTrackedPower([
F("operational", 1000),
F("operational", 500),
F("construction", 700),
F("announced", 9000),
]);
assert.equal(t.operationalMW, 1500);
assert.equal(t.constructionMW, 700);
assert.equal(t.announcedMW, 9000);
assert.equal(t.trackedMW, 2200);
assert.equal(t.operationalCount, 2);
assert.equal(t.constructionCount, 1);
assert.equal(t.announcedCount, 1);
});
it("announced is never in the headline number", () => {
const t = computeTrackedPower([F("announced", 5000)]);
assert.equal(t.trackedMW, 0);
});
it("null/negative/NaN MW counts the facility but adds zero", () => {
const t = computeTrackedPower([F("operational", null), F("operational", -50), F("operational", NaN)]);
assert.equal(t.operationalMW, 0);
assert.equal(t.operationalCount, 3);
});
it("unknown statuses are ignored; empty input is all zeros", () => {
assert.equal(computeTrackedPower([F("retired", 100)]).trackedMW, 0);
assert.equal(computeTrackedPower([]).trackedMW, 0);
assert.equal(computeTrackedPower(undefined as never).trackedMW, 0);
});
});

describe("fmtGW", () => {
it("formats MW as GW", () => {
assert.equal(fmtGW(23600), "23.6 GW");
assert.equal(fmtGW(500), "0.5 GW");
assert.equal(fmtGW(30200, 2), "30.20 GW");
});
});

describe("parseOpenDate", () => {
it("bare year -> Jan 1", () => {
assert.equal(parseOpenDate("2023"), Date.UTC(2023, 0, 1));
});
it("year + quarter -> quarter start", () => {
assert.equal(parseOpenDate("2026 Q1"), Date.UTC(2026, 0, 1));
assert.equal(parseOpenDate("2026 Q2"), Date.UTC(2026, 3, 1));
assert.equal(parseOpenDate("2026 Q4"), Date.UTC(2026, 9, 1));
});
it("tolerates spacing", () => {
assert.equal(parseOpenDate(" 2025 Q3 "), Date.UTC(2025, 6, 1));
});
it("garbage, empty, and null are excluded, not guessed", () => {
for (const bad of [null, undefined, "", "soon", "Q3 2026", "2026 Q5", "2026-03"]) {
assert.equal(parseOpenDate(bad as never), null, String(bad));
}
});
});

describe("buildBuildoutHistory", () => {
it("cumulative operational series ordered by open date", () => {
const h = buildBuildoutHistory([
F("operational", 500, "2024", "B"),
F("operational", 1000, "2022", "A"),
F("construction", 700, "2026 Q3", "C"),
]);
assert.deepEqual(h.online.map((p) => p.cumMW), [1000, 1500]);
assert.deepEqual(h.online.map((p) => p.addedMW), [1000, 500]);
assert.ok(h.online[0].t < h.online[1].t);
});
it("pipeline continues cumulatively from the operational total", () => {
const h = buildBuildoutHistory([
F("operational", 1000, "2022"),
F("construction", 700, "2026 Q3"),
F("construction", 300, "2027"),
]);
assert.deepEqual(h.pipeline.map((p) => p.cumMW), [1700, 2000]);
});
it("same-date facilities merge into one step and drop the single-name label", () => {
const h = buildBuildoutHistory([
F("operational", 100, "2023", "X"),
F("operational", 200, "2023", "Y"),
]);
assert.equal(h.online.length, 1);
assert.equal(h.online[0].addedMW, 300);
assert.equal(h.online[0].name, undefined);
});
it("single facility at a date keeps its name", () => {
const h = buildBuildoutHistory([F("operational", 100, "2023", "Solo")]);
assert.equal(h.online[0].name, "Solo");
});
it("undated facilities are excluded and counted honestly", () => {
const h = buildBuildoutHistory([
F("operational", 100, "2023"),
F("operational", 200, undefined),
F("construction", 300, "soon"),
]);
assert.equal(h.online.length, 1);
assert.equal(h.undatedCount, 2);
});
it("announced facilities never enter either series", () => {
const h = buildBuildoutHistory([F("announced", 5000, "2028")]);
assert.deepEqual(h.online, []);
assert.deepEqual(h.pipeline, []);
});
it("empty input yields empty series", () => {
const h = buildBuildoutHistory([]);
assert.deepEqual(h.online, []);
assert.deepEqual(h.pipeline, []);
assert.equal(h.undatedCount, 0);
});
});

describe("tightestRTO", () => {
it("picks the minimum reserve margin", () => {
const t = tightestRTO({
ERCOT: { label: "ERCOT", reserveMargin: 15.8, aiSignal: "Critical" },
MISO: { label: "MISO", reserveMargin: 13.4, aiSignal: "Critical" },
SPP: { label: "SPP", reserveMargin: 27.8, aiSignal: "Low" },
});
assert.ok(t);
assert.equal(t.rto, "MISO");
assert.equal(t.reserveMarginPct, 13.4);
});
it("skips non-finite margins; null on empty", () => {
const t = tightestRTO({ X: { label: "X", reserveMargin: NaN, aiSignal: "Low" } });
assert.equal(t, null);
assert.equal(tightestRTO({}), null);
});
});
174 changes: 174 additions & 0 deletions client/src/lib/real-gauges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* Real headline gauges (owner-directed replacement for the synthetic
* AI Power Demand / NPI / Grid Stress indices). Every number here is a
* direct computation over sourced data - no baselines, no rebasing, no
* sentiment formulas. Pure module, 100% covered in Lake-8 style.
*
* Sources:
* - Tracked AI DC power: the verified facility dataset (/api/datacenters)
* - Cost of AI compute: the GPU rental index (/api/gpu-prices/metrics)
* - Grid headroom: NERC LTRA reserve margins (data/rto-config)
*/
import type { RTOConfig } from "@/data/rto-config";

/**
* Same hyperscale floor the Power map advertises: only >=400 MW sites are
* "tracked". The two surfaces must agree or the headline contradicts its
* own drill-down.
*/
export const MIN_TRACKED_MW = 400;

export function filterTrackedFacilities<T extends FacilityLite>(facilities: T[]): T[] {
return (facilities ?? []).filter(
(f) => typeof f.powerMW === "number" && Number.isFinite(f.powerMW) && f.powerMW >= MIN_TRACKED_MW,
);
}

export interface FacilityLite {
powerMW: number | null | undefined;
status: string;
openDate?: string | null;
name?: string;
}

// ─── Tracked AI DC power ────────────────────────────────────────────────────

export interface TrackedPower {
operationalMW: number;
constructionMW: number;
announcedMW: number;
/** headline: operational + construction (committed steel, not press releases) */
trackedMW: number;
operationalCount: number;
constructionCount: number;
announcedCount: number;
}

export function computeTrackedPower(facilities: FacilityLite[]): TrackedPower {
const t: TrackedPower = {
operationalMW: 0,
constructionMW: 0,
announcedMW: 0,
trackedMW: 0,
operationalCount: 0,
constructionCount: 0,
announcedCount: 0,
};
for (const f of facilities ?? []) {
const mw = typeof f.powerMW === "number" && Number.isFinite(f.powerMW) && f.powerMW > 0 ? f.powerMW : 0;
if (f.status === "operational") {
t.operationalMW += mw;
t.operationalCount++;
} else if (f.status === "construction") {
t.constructionMW += mw;
t.constructionCount++;
} else if (f.status === "announced") {
t.announcedMW += mw;
t.announcedCount++;
}
}
t.trackedMW = t.operationalMW + t.constructionMW;
return t;
}

export function fmtGW(mw: number, digits = 1): string {
return `${(mw / 1000).toFixed(digits)} GW`;
}

// ─── Buildout history (real series from facility open dates) ────────────────

/**
* "2023" -> Jan 1 2023; "2026 Q3" -> first day of that quarter. Null on
* anything else - unparseable dates are excluded, never guessed.
*/
export function parseOpenDate(openDate: string | null | undefined): number | null {
if (!openDate) return null;
const m = /^(\d{4})(?:\s*Q([1-4]))?$/.exec(openDate.trim());
if (!m) return null;
const year = Number(m[1]);
const quarter = m[2] ? Number(m[2]) : null;
const month = quarter ? (quarter - 1) * 3 : 0;
const t = Date.UTC(year, month, 1);
return Number.isFinite(t) ? t : null;
}

export interface BuildoutPoint {
t: number;
/** cumulative MW online (or committed, for the pipeline series) at t */
cumMW: number;
addedMW: number;
name?: string;
}

export interface BuildoutHistory {
/** operational facilities by open date, cumulative - observed history */
online: BuildoutPoint[];
/** construction facilities by planned open date, cumulative ON TOP of the
* operational total - the committed pipeline, rendered dashed */
pipeline: BuildoutPoint[];
/** facilities excluded because their open date could not be parsed */
undatedCount: number;
}

/**
* Cumulative tracked capacity over time. Facilities at the same date
* aggregate into one step. Announced facilities are excluded entirely -
* press releases are not steel.
*/
export function buildBuildoutHistory(facilities: FacilityLite[]): BuildoutHistory {
let undatedCount = 0;
const collect = (status: string) => {
const byT = new Map<number, { mw: number; names: string[] }>();
for (const f of facilities ?? []) {
if (f.status !== status) continue;
const mw = typeof f.powerMW === "number" && Number.isFinite(f.powerMW) && f.powerMW > 0 ? f.powerMW : 0;
const t = parseOpenDate(f.openDate);
if (t === null) {
undatedCount++;
continue;
}
const cur = byT.get(t) ?? { mw: 0, names: [] };
cur.mw += mw;
if (f.name) cur.names.push(f.name);
byT.set(t, cur);
}
return Array.from(byT.entries()).sort((a, b) => a[0] - b[0]);
};

const online: BuildoutPoint[] = [];
let cum = 0;
for (const [t, { mw, names }] of collect("operational")) {
cum += mw;
online.push({ t, cumMW: cum, addedMW: mw, name: names.length === 1 ? names[0] : undefined });
}

const pipeline: BuildoutPoint[] = [];
let pcum = cum; // pipeline continues from the operational total
for (const [t, { mw, names }] of collect("construction")) {
pcum += mw;
pipeline.push({ t, cumMW: pcum, addedMW: mw, name: names.length === 1 ? names[0] : undefined });
}

return { online, pipeline, undatedCount };
}

// ─── Grid headroom ──────────────────────────────────────────────────────────

export interface GridHeadroom {
rto: string;
label: string;
reserveMarginPct: number;
aiSignal: RTOConfig["aiSignal"];
}

/** The tightest reserve margin among tracked RTOs - the binding constraint. */
export function tightestRTO(config: Record<string, RTOConfig>): GridHeadroom | null {
let best: GridHeadroom | null = null;
for (const [rto, c] of Object.entries(config ?? {})) {
if (!Number.isFinite(c.reserveMargin)) continue;
if (!best || c.reserveMargin < best.reserveMarginPct) {
best = { rto, label: c.label, reserveMarginPct: c.reserveMargin, aiSignal: c.aiSignal };
}
}
return best;
}
Loading