jemonjam
engineer + builder + leader
Design: California Reservoirs, Droughts & Population — post + interactive model

Design: California Reservoirs, Droughts & Population — post + interactive model

Date: 2026-05-28 Status: Approved (design); ready for implementation planning Author: Jacob (with Claude)

1. Summary

A blog post in Jacob’s essayistic voice paired with an interactive, data-grounded water-balance model of California’s surface reservoirs (1950–2024). The reader drags levers — population, per-capita use, new reservoirs, a drier climate — and watches the statewide reservoir level recompute over time, along with a count of low-reservoir events avoided.

The post is exploratory but lands a thesis the model earns:

We argue endlessly about building more dams, but concrete is the weakest lever — reservoirs move water in time, they don’t create it. The scarcity gap is set by climate (which we don’t control) and by demand (dominated by agriculture, which stopped tracking population decades ago). Restraining demand does several times more than any reservoir we could realistically build. The popular answer is the least effective; the effective answers are the politically hardest.

This thesis is a synthesis between the two framings we started with (“supply can’t win” vs. “population / we stopped building”). The spike confirmed: supply genuinely can’t win, and population is a bigger lever than concrete (validating Jacob’s pushback) — but the true demand giant is agriculture, and climate dominates everything.

2. Spike findings (already run; these justify the thesis)

A prototype model (prototype.mjs, in the gitignored brainstorm scratch dir) ran the real series through an annual water balance and tested each lever. Results are robust across inflow calibrations (k = 2.0 / 2.2 / 2.5); the lever ranking is the load-bearing output, not the absolute MAF figures.

Share of the cumulative “supply gap” each lever closes:

Lever Gap closed
Cut agricultural use 25% 81–100%
Population frozen at 1950 53–75%
Population frozen at 1970 30–46%
Per-capita conservation (130 GPCD) 29–38%
Go further: +10 MAF capacity 9–31%
Build ALL proposed reservoirs (+3.3 MAF) 3–11%
Drier future (−15% runoff) −65% to −122% (worse)

Low-reservoir events avoided (of 25 baseline events; storage < 33% of capacity):

Lever Events avoided
Cut ag 25% 14
Combo (1970 pop + conserve + 10 MAF) 10
Population frozen at 1950 6
Per-capita conservation 6
Population frozen at 1970 3
Go further (+10 MAF) 2
Build ALL proposed reservoirs (+3.3 MAF) 0

The “cut ag” rows are diagnostic lever tests, not shipped controls — ag is not a user knob (see §3). They’re included to show how large the latent agricultural lever is relative to the controls readers can touch.

Key qualitative insight for the prose: in severe multi-year droughts (1977, 1989–92, 2014–15) the reservoirs drain to deadpool no matter which lever you pull — the water never fell. Lower demand helps in moderate dry years and speeds recovery. Wet-year spill barely changes as capacity is added, proving the binding constraint is inflow, not storage.

3. The model

Annual water balance over water years 1950–2024, in million acre-feet (MAF). Conceptually a single statewide surface-water bucket.

For each year t:

pop      = popOverride ?? CA_POP(t)
gpcd     = min(GPCD(t), gpcdCap ?? Infinity)
urban    = pop * gpcd * 365 / 325,851e6        // gallons -> MAF
ag       = AG(t)                                // real historical series; fixed (not a knob)
demand   = ag + urban

inflow   = RUNOFF(t) * K_INFLOW * climateMult   // Sac 4-river runoff scaled to statewide proxy
capacity = CAP(t) + addedCapacity               // physical buildout + toggled/slider reservoirs

avail       = storage_prev + inflow
delivered   = min(demand, max(0, avail - DEADPOOL))
shortfall   = demand - delivered                // "supply gap" (met in reality by groundwater)
storage     = avail - delivered
spill       = max(0, storage - capacity); storage = min(storage, capacity)

lowEvent    = storage < LOW_FRAC * CAP(t)        // 33% of *physical* capacity

Constants (prototype values; tune during build): K_INFLOW ≈ 2.2, DEADPOOL = 4.0, INIT_STORAGE = 20.0, LOW_FRAC = 0.33.

Calibration / honesty guardrail. The baseline run (all levers at “actual”) must reproduce the historical pattern: storage drains to deadpool in the real droughts and spills in the real wet years. We plot recorded statewide storage (CDEC, available from ~1970) as a faint reference so readers can see the model tracks reality before trusting the counterfactuals. Absolute MAF figures are illustrative; the lever ranking is the claim.

Levers (controls)

  1. Population scenario — override CA population (presets: actual / freeze at 1970 / freeze at 1950 / a slider). Scales urban demand only.
  2. Per-capita use (conservation) — cap or scale GPCD. Separate from headcount.
  3. New reservoirs — toggles for real projects, each adding capacity: Sites (+1.5 MAF), Temperance Flat (+1.26), Shasta raise (+0.63), B.F. Sisk raise (+0.13), Del Puerto (+0.08), Los Vaqueros expansion (+0.115), Pacheco (+0.13), Auburn (+2.3, never built). Plus a “go further” slider for hypothetical extra MAF.
  4. Drier future (climate) — scalar multiplying inflow (e.g. −10% / −20%).

Ag demand is a fixed real series (decision: not a user knob), but the prose/model notes that ag is the dominant share and the largest latent lever.

Outputs

4. Interactive piece — layout & architecture

Mirrors the existing lab/comp-viz/ precedent exactly.

lab/water-viz/
  index.html        full-page version (Layout B: controls in a left sidebar)
  embed.html        in-post iframe (Layout A: controls on top, charts stacked)
  styles.css
  embed.css
  src/
    data.js         real series as JSON consts, each with an inline source citation
    model.js        pure water-balance fn: (series, levers) -> {trace, metrics}
    charts.js       canvas/SVG time-series + bar chart
    main.js         full-page wiring
    embed-main.js   embed wiring

5. Post structure (Jacob’s voice)

Roman-numeral sections, concrete → abstract, measured claims, honest caveats, no neat bow. Rough arc:

Target rubric: LLM score < 30, Jacob score > 65 (per AGENTS.md).

6. Data sources & provenance

All series gathered in the research pass; stored in data.js with per-series citations.

Data fallbacks: where only benchmark years exist (capacity, ag, GPCD, population), linearly interpolate to annual and label as interpolated in data.js comments. Runoff is true annual.

7. Limitations (the post’s honesty section)

8. Out of scope (YAGNI)