Skip to content

feat: California configuration on CPUC SERVM 2026 demand (weights, demand profile, import caps, CA config, benchmark) - #801

Merged
ktehranchi merged 27 commits into
developfrom
feat/cpuc-servm-california
Sep 1, 2026
Merged

ktehranchi merged 27 commits into
developfrom
feat/cpuc-servm-california

Conversation

@ktehranchi

Copy link
Copy Markdown
Collaborator

Supersedes and collapses the five-PR stack #796 / #797 / #798 / #799 / #800 into a single PR (author's request). Commit history preserves the original PR boundaries.

Changes proposed in this Pull Request

A California-only PyPSA-USA configuration driven by the CPUC 2026 SERVM update, with selectable demand weather year (2000–2024) per run:

  1. SERVM load-allocation weights — fractional (region × cluster-bus) allocation factors built by composing the busmaps against population load_weight; exact for clusters straddling regions and agnostic to simpl/topological_boundaries (county resolution works unchanged). CA slivers served by non-CPUC-region BAs (CISO-VEA, PACW, WALC, NEVP; 0.48% of CA load weight) are explicitly excluded and logged.
  2. profile: servm demand — retrieve rules for the nine per-forecast-year CPUC CSVs (2026–2045) + Baseline Generator List; ReadServm (3-row-header parse with format-drift guards, weather-year filter via electricity.demand.scenario.servm_weather_years, fixed-PST→UTC shift verified empirically, leap-safe positional snapshot mapping); WriteServm matrix-product disaggregation; every SERVM load component preserved in a new zonal parquet artifact for later component-level modeling (phase 2/3 feedstock).
  3. Aggregate interface transmission limits — rewires the dead transmission_interface_limits.csv / model_topology.interface_transmission_limits pair into a per-snapshot cap on import/export link flows (RESOLVE CAISO_Imports 9,728/10,208 MW), alongside the existing per-flowgate NARIS caps and percent-of-load volume limits.
  4. config.california.yaml — maintained CA config: western + include: {reeds_state: ['CA']}, reeds_zone/4-cluster default with a documented county-mode alternative, planning_horizons: [2030, 2035, 2040, 2045], imports/exports enabled (wholesale costs, capacity + 25%/yr volume caps), interface limits on. Plus config.test.california.yaml + integration tests, docs across data-demand/model-constraints/config-spatial/configtables, and phase-2 (conventional.ambient_derate guard) / phase-3 (multi-weather-year NotImplementedError) stubs.
  5. CPUC Baseline Generator List capacity benchmark — validate-stage rule (run.benchmark_cpuc) comparing model fleet vs CPUC baseline by benchmark region × technology category per horizon (CSV + deviation heatmap); unmapped techs/regions surface explicitly, never dropped.
  6. Tier B fixes — making pytest -m integration actually executable surfaced four pre-existing develop defects, fixed in the final commit: config.test.yaml could not parse (missing solving block), the AEO fuel-cost scenario fallback used the wrong case ("Reference" vs the parquet's reference), godeeep_wind_height shipped as "_100m" (no Zenodo record exists for that height — wind profiles could not build), and an unvalidated substation-count bound.

Testing

  • Tier A pytest -m fast: 195 passed, 1 pre-existing skip.
  • Tier B pytest -m integration: 32 passed — first real execution of this tier; builds the CA test slice through cluster_network and the SERVM chain through add_demand (downloads one ~118 MB CPUC CSV).
  • Real-data validation: zonal Net Load matches the raw CPUC file per region to rounding; bus-level total equals zonal total exactly (299.1937 TWh, 2030 × WY2019); CA peak lands hour 17 PST late-August (62.6 GW), confirming the timezone handling.
  • pre-commit clean on all changed files. No equivalence run for this branch (new opt-in profile/config; the only behavior changes to existing paths are the Tier B defect fixes in commit a13d7ee, of which godeeep_wind_height: _125m changes renewable profiles for any config that relied on the broken shipped value — which could not have built at all).

Checklist

  • I tested my contribution locally and it seems to work fine.
  • pre-commit and pytest -m fast pass locally.
  • Code and workflow changes are sufficiently documented.
  • Changed dependencies are added to workflow/envs/environment.yaml (no dependency changes).
  • Changes in configuration options are added in workflow/repo_data/config/config.default.yaml.
  • Changes in configuration options are also documented in docs/source/configtables/*.csv.

🤖 Generated with Claude Code

ktehranchi and others added 9 commits August 29, 2026 15:09
Builds a fractional per-(SERVM region, cluster bus) allocation table so CPUC
SERVM demand for the six California load regions can be disaggregated onto the
simplified network.

The shares are derived by composing the two busmaps (base bus -> substation ->
{simpl} cluster bus) against elec_base_network.nc rather than reading a bus
column of the clustered network: aggregate_to_substations drops
`balancing_area` from the buses, and a cluster can straddle two SERVM regions
anyway (LA County holds both LDWP and CISO-SCE buses). The base network is the
last stage that still carries both `balancing_area` and the population-based
`load_weight`, so the composition recovers where each base bus's weight lands.
A straddling cluster therefore appears once per region it overlaps, each row
carrying only that region's share, and the factors sum to 1.0 within a region.

Balancing areas that are deliberately out of scope (CISO-VEA, Nevada
footprint) keep a row with an empty region in servm_region_map.csv, so an
unknown balancing area introduced by upstream relabeling still hard-fails
while the excluded one is dropped with a logged load share.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…p, PR 3)

Rewires the two dead config keys `model_topology.interface_transmission_limits`
and `electricity.transmission_interface_limits`: the RESOLVE interface table is
now applied as a per-snapshot MW cap on the aggregate flow across each
interface.

Scope is the electricity import/export Links added by add_extra_components, so
the constraint is inert when trade is disabled. Region_2 entries that are inside
the network contribute no trade links, so internal AC lines escape the cap; that
understatement is documented in the module, not corrected here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds `electricity.demand.profile: servm`, wiring CPUC SERVM 2026 hourly load
for the six California load regions (IID, LADWP, NCNC, PGE, SCE, SDGE) through
the existing read/write demand strategies.

Retrieve: `retrieve_cpuc_servm_load` pulls one ~118 MB CSV per forecast year
(2026-2045) from files.cpuc.ca.gov, and `retrieve_cpuc_baseline_generators`
pulls the CAISO baseline generator list, both through a thin
`retrieve_cpuc_data.py` downloader patterned on `retrieve_eer_data.py`.

ReadServm parses the three-row header positionally: the seventh index column
("Hour of Day") carries the stray ('Region', 'Unit Type') labels, so the
calendar block cannot be found by testing the upper header levels for
blankness. Every published component is kept on the `subsector` index level so
the zonal artifact stays component-resolved; only `Net Load` reaches the model,
selected by a new `ReadStrategy.default_subsector` hook that `Context` applies
so `main()` stays profile-agnostic. Files are indexed by the forecast year
parsed from each basename rather than by list order. Strips are fixed PST
(UTC-8, no DST -- verified against the BTMPV solar-noon centroid, Dec 12.52 vs
Jul 12.68) and are rolled to UTC with the same `np.roll` convention ReadEer
uses for CST.

Two calendar misalignments are accepted and documented on
`ReadServm._assign_snapshots`:

1. SERVM lays its hours on a synthetic Monday-start calendar, so weekday and
   weekend hours do not line up with the real weekdays of the planning horizon.
2. For a leap *weather* year the strip contains February 29 and omits
   December 31, while the model snapshots do the opposite, so every hour after
   February lands one calendar day earlier than it sat in the source file.

The 8760-hour strip is therefore mapped positionally onto the network's own
per-period snapshots rather than onto a synthesised `date_range`, which would
run a day short of December 31 for the leap planning horizons (2028/2032/2040)
because `get_snapshots` drops February 29 from them.

WriteServm disaggregates by matrix product against the PR-1 weights table
pivoted to (region x bus), rather than the base class's one-zone-per-bus
mapping: a cluster bus can straddle two SERVM regions (LA County holds both
LDWP and CISO-SCE buses), and the product gives such a bus the sum of its share
of every region it overlaps.

`build_electrical_demand` gains a second output, the reader's zonal
component-resolved demand written before disaggregation. It is produced for
every profile (for efs/eer it is simply the single subsector="all" slice), and
is captured off the `Context` rather than re-read.

Config: `electricity.demand.scenario.servm_weather_years` selects the weather
year from the stacked 2000-2024 record. A single-entry list is deterministic;
multiple entries raise NotImplementedError pending stochastic scenarios. A
value differing from the top-level `renewable_weather_years` logs a warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ubs (PR 4)

Ties the SERVM demand profile (PR 2) and the aggregate interface transmission
limits (PR 3) together into a runnable, maintained California entry point.

New `workflow/repo_data/config/config.california.yaml`: CA-only Western model
on CPUC SERVM 2026 demand, at REeDS-zone resolution (clusters must be 4 — the
ReEDS backbone cannot cluster below the four CA zones p8/p9/p10/p11), with
planning horizons restricted to SERVM forecast years (2030/2035/2040/2045),
the RESOLVE CAISO interface caps switched on, and imports/exports enabled at a
25%-of-demand annual volume limit. A clearly-marked commented alternative
carries the county-resolution variant (clusters: 58, simpl: 'county'); the
county NARIS flowgate file is selected automatically by
`add_extra_components` from `topological_boundaries`.

Phase-2 hook `conventional.ambient_derate` is stubbed in both the California
and default configs and guarded in `add_electricity`: enabling it raises
NotImplementedError rather than silently doing nothing. The comments record
that when implemented it REPLACES the EIA-860 seasonal derate and must never
stack with it or with a UCAP-derated capacity credit. `retrieve.smk` reserves
`retrieve_cpuc_thermal_derate` for the profiles it will need.

Tests: `config.test.california.yaml` is a SERVM variant of the CA test harness
config. It keeps a full 8760-hour snapshot year — `ReadServm._assign_snapshots`
maps its hourly strips positionally onto the network's own per-period snapshots
and raises unless the horizon carries exactly 8760, so a truncated window
cannot build SERVM demand. Three dry-run cases are added (california ->
cluster_network, california -> solve_network, and a county-mode case driven by
`--config` overrides), and `tests/integration/test_servm_demand_artifacts.py`
asserts weight conservation, demand/network alignment, and the PST->UTC roll
against a `--until add_demand` build.

Docs: a SERVM section in data-demand.md (source URL pattern, the nine forecast
years, the six regions and their balancing-area mapping including the CISO-VEA
exclusion, Net-Load-only dispatch with the component-resolved zonal artifact,
weather-year semantics, fixed PST, and both calendar caveats); the interface
constraint documented in model-constraints.md and data-transmission.md
including the p8 gap; the `transmission_interface_limits` config row corrected
from "not currently consumed"; a California example in config-spatial.md; and a
release-notes entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compares installed capacity between the CPUC Baseline Generator List and
the model's generator fleet, aggregated by SERVM benchmark region and
technology category, per planning horizon. Emits a long-format comparison
CSV and a per-horizon deviation heatmap under the run's figures/benchmark/
directory. Gated by the new run.benchmark_cpuc flag (default false), which
is independent of the demand profile: the fleet benchmark is useful for any
California run.

Region attribution: EIA reports every CAISO plant under the single BA code
CISO, so powerplants.csv cannot separate PGE from SCE from SDGE. The
benchmark therefore runs at the coarsest resolution both sides support —
CAISO (= PGE + SCE + SDGE), LADWP (LDWP), IID, NCNC (BANC + TIDC) — with
the collapse stated in repo_data/CPUC/servm_benchmark_regions.csv. The
model side is restricted to state == CA so EIA's CISO code does not drag in
the Nevada CISO-VEA footprint.

Technology attribution: repo_data/CPUC/servm_tech_map.csv maps both SERVM
tech categories and PyPSA carriers into a shared compare_category. Anything
unmapped on either side becomes an explicit UNMAPPED:<name> row rather than
being dropped.

Vintage/retirement filtering is shared by both sides and mirrors
add_electricity.load_powerplants, so the benchmark measures the fleet the
model actually builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- servm_region_map.csv: PACW/WALC/NEVP added as known-but-excluded rows —
  CA slivers served by non-CPUC-region BAs (0.48% of CA load weight);
  their load lives in CPUC's forthcoming non-CA regional files.
- conftest: --rerun-triggers mtime so a fresh worktree sharing data/ via
  symlink does not re-download (and rewrite) existing retrieve outputs.
- config.test.california.yaml / config.california.yaml: add the solving
  block validate.smk reads at parse time, costs.aeo.scenario, and the
  godeeep renewable_land_access pin; test solver gurobi for the
  distribute_clusters QP (fixture never reaches solve_network).
- test_row_count_matches_snapshots: compare against the demand-attached
  network — elec_s{simpl}.nc still carries PyPSA's default ['now'] index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tier B (pytest -m integration) had never actually run — CI was silently
skipping it — and its first execution surfaced four latent defects:

- config.test.yaml could not even parse: validate.smk reads
  config["solving"] at Snakefile parse time. Added the minimal solving
  block (+ costs.aeo.scenario, renewable_land_access pins).
- build_cost_data.py: the AEO scenario fallback was "Reference", but the
  PUDL parquet's model_case_eiaaeo vocabulary is lowercase ("reference")
  -> empty frame and IndexError whenever a config omitted the key.
- godeeep_wind_height shipped as "_100m", a hub height with no published
  Zenodo record (only wind_125m_* exist) -> downloader returns None and
  xarray fails with a misleading "no IO backend" error. Config value and
  script fallback both set to "_125m".
- test_bus_count_reasonable expected O(50) substations, but on the ReEDS
  transport path aggregate_to_substations keeps TAMU granularity
  (CA-only Western ~2,000 buses); bound widened with an explanatory note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ktehranchi and others added 16 commits August 30, 2026 10:09
The meaningful fleet-vs-fleet comparison is today's plants against today's
baseline: at 2026 every unit on both sides exists, so there is no forecast
ambiguity — and the CPUC list is a baseline (near-static to 2045), so later
horizons only measure model expansion against it.

- rule benchmark_cpuc_baseline no longer consumes the assembled network:
  it needs only powerplants.csv and the CPUC workbook, so enabling
  run.benchmark_cpuc never triggers a model build (2-job DAG). Outputs move
  to results/<run>/cpuc_benchmark/ without cluster wildcards; the network
  reconciliation stays available as a library function.
- new run.benchmark_cpuc_horizons key (default [2026]) decouples the
  benchmark year from scenario planning_horizons; empty list falls back.
- drop a mid-file `from __future__` import that broke under snakemake's
  script preamble injection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CPUC Baseline benchmark exposed the mechanism: pudl_path pinned
v2025.5.0, whose most-recent filings freeze the fleet at the 2023/24
EIA-860 vintage — missing exactly the fastest-growing CAISO categories
(the 2026 benchmark was short 7.9 GW of battery and 3.9 GW of solar).
v2026.8.0 carries filings through report_date 2026-01; the existing
most-recent-non-null query picks them up with no schema changes
(verified column-complete on S3). data_year (the EIA-923 heat-rate /
fuel-cost window) moves 2023 -> 2025, the latest complete year.

CAISO 2026 benchmark after the bump: total gap -13.0% -> -8.3%
(battery -7.9 -> -5.7 GW, solar -3.9 -> -2.1 GW); thermal deltas are
unchanged, consistent with those being classification, not vintage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lants

EIA leaves nerc_region NULL for plants that first appear in a recent 860
vintage, and add_electricity maps nerc_region -> interconnect, so the
plain isin() filter silently deleted new-build (2.2 GW of CA renewables
under PUDL v2025.5.0; 204 plants / 7.3 GW nationally under v2026.8.0).
Found by unit-level RCA against the CPUC Baseline Generator List.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
load_powerplants pinned every existing/proposed unit to a 2100
retirement, discarding EIA's planned_generator_retirement_date — the
CPUC benchmark showed this keeps ~2.9 GW of retired CAISO OTC steam
(Ormond Beach, Alamitos, Huntington Beach) in the fleet forever.

- add_electricity.load_powerplants: existing/proposed units now retire
  on their announced date where one exists (config gate
  electricity.honor_planned_retirements, default true). Applies at the
  first investment period; post-clustering periods aggregate
  build_year/lifetime capacity-weighted, so per-period exogenous
  retirement inside a cluster remains out of scope (documented).
- build_powerplants: planned_generator_retirement_date now takes the
  LATEST filing's value as-is instead of most-recent-non-null — a newer
  NULL means the announcement was withdrawn (Diablo Canyon post-SB846
  carried a stale 2025-08 date from an old filing, which would have
  retired Unit 2 spuriously).
- benchmark_cpuc_baseline.prepare_model_plants mirrors the new rule.
- New unit tests for the retirement semantics; benchmark test updated.

CAISO 2026 benchmark: Gas CT/ICE/Steam +5,720 -> +4,387 (Alamitos +
Huntington Beach out at 2026; Ormond Beach stays until its current EIA
date of 2027-01 — a genuine EIA-vs-CPUC source disagreement, no longer
a model artifact). Tier A 200 passed, Tier B 32 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CPUC's SERVM regions are contractual ledgers: they carry AZ/NV/UT/BC/
Mexico resources (Palo Verde share, Intermountain, Apex, Hoover shares,
Powerex dynamic hydro, Mexicali CCs, Energia Sierra Juarez, Desert Star,
and ~2 GW of out-of-state solar/battery contracts) under California
region labels. A physically-located model can never carry these, so the
benchmark scored 10.9 GW of them as model shortfall.

- repo_data/CPUC/servm_out_of_state_units.csv: 74 units, 10,873 MW,
  each verified at unit level (EIA plant ids / CAISO queue / physical
  county), with evidence strings.
- benchmark_cpuc_baseline: the listed units leave the scored regions and
  surface on an "EXCLUDED: out-of-state contracted" row per category
  (kept out of the deviation heatmap, like the RECONCILE rows); stale
  list entries warn loudly instead of silently un-excluding.

CAISO 2026 after exclusion: scored CPUC 95.2 GW vs model 93.9 GW
(-1.4%, from -9.5%). Nuclear lands at +23 MW (Diablo nameplate delta),
LADWP Gas CC at +13 MW. Known residuals now stand exposed instead of
being offset: Solar +1,105 (model-only small plants), Gas CC +2,442
(cogen reclassification + nameplate convention), Battery -3,744 (real
CODs 2027-29 behind CPUC 2026 dates + ~1.2 GW with no EIA identity).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
conventional.unit_commitment: true for the CA configs (linearized UC —
the solve stays an LP). Making the flag real surfaced three latent
defects, all fixed:

- p_min_pu was computed in the unit_commitment branch of
  attach_conventional_generators but never passed to n.add — minimum
  stable levels were silently dropped.
- PyPSA's up_time_before=1 default forces every committable unit online
  into the first snapshot; with min_up_time > 1 that pins minimum load
  onto the January-night load trough and is infeasible without load
  shedding (reproduced on a toy). Committable units now start with
  up_time_before=0.
- min_up/min_down are in snapshots and ramps are per-snapshot in PyPSA,
  but the data is hours / pu-per-hour. prepare_network now rescales them
  to the {opts} temporal resolution (no-op when nothing is committable).

Data sanitation (build_powerplants.sanitize_uc_parameters): per-carrier
UC_BOUNDS keep in-band ADS values untouched and clamp/fill only the
physically impossible — absolute-$ start costs imputed onto sub-MW units
(up to $1.03M/MW), 108h min-up on OCGT peakers, p_min_pu up to 31x
nameplate, $0 geothermal cycling, and NaNs that PyPSA reads as
infinitely flexible. 17,749 clamped / 6,087 filled across 17,502
committable rows, logged per carrier; non-committable rows verified
untouched. apply_ucap now scales committable p_min_pu alongside
p_max_pu so the feasibility invariant survives.

Clustering strategies: start_up_cost capacity_weighted_average -> sum
(the aggregate acts as one unit; the average understated cycling cost
~N-fold) and ramp limits max -> capacity_weighted_average.

40 new UC tests; fast tier 241 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cluster_simpl shipped resolve_simpl_mode() and build_county_busmap()
but main() never called them — it int()'d the wildcard, so
simpl='county' crashed with ValueError at runtime (the county-mode DAG
dry-run test could not catch an execution-time failure). The county
branch now passes the county-FIPS busmap through
clustering_for_n_clusters' custom_busmap path: no k-means, no
cluster-count QP. Verified: CA-only western at simpl='county' yields
the expected 58 county buses (e.g. p10_p06037 = Los Angeles).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two defects in add_elec_imports_exports, exposed by the first county-
resolution run with trade enabled:

- the exports call omitted zone_col, silently falling back to the
  "reeds_zone" default. On county networks every county then fails the
  zone test and boundary rows get their inside/outside roles scrambled
  (an Arizona county classified as inside).
- the cost cache checked `zone_outside not in costs` but wrote
  costs[zone_inside]; once an earlier row's inside zone matched a later
  row's outside zone, the write was skipped and costs[zone_inside]
  raised KeyError ('p04012', La Paz County AZ).

reeds_zone runs were unaffected (default coincided; key collision not
triggered at that cardinality).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… CA buses

CPUC attributes ~10.9 GW of physically out-of-state resources to CA
regions. New opt-in option (default off; on for the CA configs) attaches
the 65 units with EIA identities as generators/storage at CA buses:

- unit <-> EIA matching reused from repo_data/CPUC/servm_out_of_state_units.csv
  (the benchmark exclusion ledger); techno-economics (heat rates, costs,
  derates, sanitized UC params) come from the same PUDL powerplants build,
  captured before the CA footprint filter drops out-of-state plants.
- p_nom = min(contracted Capmax, live plant capacity); bus = max-LAF bus
  of the unit's SERVM region from the servm_load_weights artifact.
- batteries attach as StorageUnits (max_hours from EIA MWh, sqrt(eta)
  convention matching attach_battery_storage); remote VRE copies the
  attachment bus's same-carrier profile; firm units get the identical
  seasonal derate treatment applied to their own columns only.
- rows with no EIA identity (Mexicali CCs, Powerex BC hydro, ESJ Baja
  wind, Tahoe/NV entitlements - 1,923 MW) are skipped with a summary
  warning and remain represented by the import machinery.
- non-extendable by construction (contracted shares, not expansion
  candidates).

Attach summary for the CA config: 8,818 MW across solar 3,546 / CCGT
2,117 / battery 1,715 / nuclear 635 (Palo Verde) / hydro 592 (Hoover) /
OCGT 108 / geothermal 105. 14 new tests; 99 targeted tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The land-access refactor (#745) left the compressed historical records
with a single weather year (2012), stranding the pre-#745 bus-aggregated
archives (solar 1980-2022, wind 100m 2001-2022; Zenodo 18293999 /
18331699) that cover every other year. This restores them as a fallback:

- historical years with a compressed record (2012) route through the
  screened NREL land-access path unchanged; other years download the
  aggregated archive and remap substation-keyed profiles through
  busmap_s{simpl} with a caps-weighted mean (verified end-to-end on the
  real 2019 solar file: 1,972 in-footprint substations -> 75 clusters,
  schema identical to the compressed path).
- HARD GATES per issue #803: explicit opt-in
  (godeeep_allow_unscreened_fallback, default false); hard error when
  any renewable carrier is extendable (unscreened profiles must never
  drive siting against screened p_nom_max); hard error when a run mixes
  screened and unscreened years (checked for every historical run, not
  just fallback-routed ones - a [2012, 2019] config previously slipped
  through); provenance attrs (land_access, hub_height, source) stamped
  on the output .nc; loud three-caveat warning.
- NREL caps remain the p_nom_max source (weather-year independent);
  the screened-caps/unscreened-profile inconsistency is documented and
  rendered moot by the extendable gate.

52 new tests. Wind archive structure inferred from the old code + solar
file; first real wind fallback run is the confirmation.

Refs #803.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bility

- new docs/source/california-model.md: definitive CA reference — data
  inventory (SERVM demand, PUDL v2026.8.0 fleet, GODEEEP renewables,
  NARIS/RESOLVE trade limits, CPUC benchmark + out-of-state ledger,
  remote contracted resources), demand weather-year options (2000-2024
  via servm_weather_years), spatial resolutions, UC defaults, caveats.
- renewable weather-year availability stated precisely per path:
  screened compressed = 2012 only (125m wind); unscreened aggregated
  fallback (opt-in, #803 gates) = solar 1980-2022 / wind 2001-2022
  (100m); climate scenarios = planning-horizon-indexed 2030/2040/2050.
  Stale "one historical year" claims replaced in data-generators.md and
  config-configuration.md.
- fixed the self-contradictory land-access ordering (empirically
  limited < reference < open, from the avail_* artifacts).
- data-demand.md: excluded-BA list corrected (VEA + PACW + WALC + NEVP).
- config.california.yaml: warn that rcp85cooler has no 2035/2045 CF
  records (documented landmine; resolution deferred).
- tests/docs NREL_EXCLUSION key set updated (pre-existing failure).

Sphinx builds clean; tests/docs 39 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Mateo

assign_missing_regions overwrote ALL seven region attributes whenever a
bus was missing ANY of them. The Trans Bay Cable's Potrero terminal
(sub 36177) resolves its county correctly to p06075 (San Francisco) but
sits in a reeds_shapes gap, so the whole-row copy replaced its county
with the nearest complete bus's — San Mateo. SF's only HVDC infeed
(375 MW, from the TAMU dcline data, present and correctly rated all
along) delivered one county south of reality; 11 of 4,248 western buses
were misassigned by the same mechanism.

Fix: fill only the attributes that are actually NaN. Six regression
tests, including the Potrero case.

Empirical confirmation (county-58 WY2019 operational run): the cable now
terminates at p06075 and load shedding collapses 805.4 -> 25.4 MWh
(19 -> 2 hours, peak 125.8 -> 17.3 MW); the SF evening-ramp deliverability
bind is resolved. Found by an HVDC-modeling audit of the shedding result.

Related follow-up (logged, not fixed): the ReEDS DC ITL files
(transmission_capacity_init_nonAC_{ba,county}.csv - PDCI, IPP DC) exist
in repo_data and are consumed nowhere; irrelevant for CA-only runs but a
real gap for western/usa transport-model runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mark

The EXCLUDED aggregate row becomes per-benchmark-region rows
("EXCLUDED: OOS CAISO" etc.) carrying BOTH the CPUC ledger MW and the
share the remote_contracted_resources option can attach on the model
side (ledger rows with an EIA identity whose plant is live at the
horizon, capped at the contracted Capmax; multi-plant entitlements like
Hoover's "154;8902" are summed). A new third output,
cpuc_capacity_composition.pdf, renders stacked in-state + out-of-state
bars per region for both sides.

CA 2026: ledger 10,873 MW, model-attachable 8,946 MW; the 1,927 MW
residual is exactly the no-EIA-identity rows (Mexicali CCs, Powerex BC
hydro, ESJ Baja wind, Tahoe NV, Fish Lake) that stay with the import
machinery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolves conflicts from #806 (GODEEEP CF registry, no fallback paths).
Resolution policy: the registry wins everywhere it overlaps —

- build_renewable_profiles.py / zenodo_downloader.py: theirs wholesale;
  the interim unscreened aggregated fallback (and its opt-in flag,
  configtable row, docs and 52 tests) is retired. Screened historical
  years 1980-2022 now come from the registry's SHA256-verified mirror,
  which resolves #803 properly.
- config.common.yaml: registry semantics for godeeep_wind_height (no
  default); our PUDL v2026.8.0 pin and nrel_caps_reassign block kept.
- docs: availability rewritten registry-era in data-generators.md,
  config-configuration.md and california-model.md (source × years
  matrix; every SERVM demand year 2000-2022 now pairs with a screened
  profile of the same year).
- config.california.yaml switched to renewable_scenarios: ['historical']
  + WY2019: the registry validates configured (dataset, year) pairs at
  parse time, and the previous rcp85cooler pairing with 2035/2045
  horizons (a documented landmine) became a hard parse failure. Verified
  parsing (35-job data_model DAG).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ktehranchi and others added 2 commits August 31, 2026 16:20
Adapts the California/SERVM feature set to the auto-loaded layered base
and schema validation from #794:

- schema: admit our config surface in the closed sections —
  run.benchmark_cpuc / benchmark_cpuc_horizons;
  electricity.honor_planned_retirements, remote_contracted_resources,
  demand.profile enum += servm, demand.scenario.servm_weather_years;
  conventional.ambient_derate.
- config.california.yaml and config.test.california.yaml rewritten as
  SPARSE OVERLAYS (programmatic deep-diff against the merged base); the
  test overlay drops its now-redundant solving/costs/land-access blocks
  and its stale pre-fix clustering strategies, inheriting the corrected
  defaults (start_up_cost: sum, capacity-weighted ramps).
- policy_constraints paths follow the tracked repo_data/config/ layout
  (solve rules' interface_limits input, configtables, conftest, docs).
- test_dag_dryrun: their repo_data paths + our California cases (incl.
  county-mode override) as a union.
- configtables: their reference-default land-access row (ordering now
  consistent); our un-deadified transmission_interface_limits
  description kept, path-updated.

Verified: both CA overlays parse against the schema (35-job / 26-job
data_model DAGs); fast tier 350 passed, docs 41 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ate ledger

CMRRON_2_CMNWD1 (300 MW) is the third Energia Sierra Juarez phase in
La Rumorosa, Mexico -- contracted to Silicon Valley Power and delivered
over the existing ESJ cross-border 230 kV line. No EIA-860 record exists
(Mexican plant), so it is excluded from scoring rather than attachable.
CAISO Wind raw benchmark delta moves from -256 MW to +44 MW.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ktehranchi
ktehranchi merged commit 34b643f into develop Sep 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant