Skip to content

feat(demand): CPUC SERVM 2026 demand profile with selectable weather year (CPUC stack 2/5) - #798

Closed
ktehranchi wants to merge 1 commit into
feat/servm-load-weightsfrom
feat/servm-demand
Closed

ktehranchi wants to merge 1 commit into
feat/servm-load-weightsfrom
feat/servm-demand

Conversation

@ktehranchi

Copy link
Copy Markdown
Collaborator

Stacks on #796 (base is set to feat/servm-load-weights; retarget to develop after #796 merges).

Changes proposed in this Pull Request

New electricity demand profile servm: hourly load for the six CPUC SERVM California regions, per-forecast-year files (2026–2045, nine vintages), 25 selectable weather years (2000–2024), medians consistent with the 2025 IEPR Planning Scenario.

  • Retrieve: retrieve_cpuc_servm_load (per-forecast-year CSVs, ~118 MB each) and retrieve_cpuc_baseline_generators from files.cpuc.ca.gov.
  • ReadServm (build_demand.py): parses the 3-row-header CSVs (incl. the stray ('Region','Unit Type','Hour of Day') label quirk); validates planning_horizons ⊆ {2026,2028,2030,2032,2035,2037,2040,2042,2045} (hard error) and Net Load presence for all six regions (loud failure on CPUC format drift); filters to the configured weather year; shifts fixed PST→UTC (+8 h — no DST in the data, verified empirically via the BTM-PV solar-noon centroid Dec vs Jul); maps the 8760-hour strip positionally onto the network's actual per-period snapshots (leap-safe — avoids the synthetic date_range pattern that breaks for leap horizons).
  • Weather-year key: electricity.demand.scenario.servm_weather_years: [2019] — a list; a single entry is deterministic, multiple entries raise NotImplementedError (reserved for stochastic scenarios); mismatch with renewable_weather_years warns and is documented as not recommended.
  • WriteServm: disaggregates region demand to buses as a matrix product against the feat(demand): SERVM region load-allocation weights for California (CPUC stack 1/5) #796 weights (exact for clusters straddling regions).
  • Component preservation: modeled load is Net Load, but every SERVM component (BTMPV, EV, BTM storage, AAEE/AAFS, climate, data-center) is retained in a new zonal artifact {end_use}_zonal_components_s{simpl}.parquet (written for all profiles) as the feedstock for later component-level modeling.

Two accepted calendar caveats, documented in code and docs: SERVM strips are Monday-start synthetic calendars (day-of-week misalignment vs real calendars), and leap weather years include Feb 29 / drop Dec 31 while model snapshots do the opposite (post-February hours shift one calendar day).

Testing

  • Tier A pytest -m fast: 195 passed, 1 pre-existing skip (combined stack).
  • New tests: test_build_demand_servm.py (16) + test_build_demand_servm_write.py (5) — 21 passed, including a leap-horizon (2028) snapshot-alignment test; existing demand-reader tests unaffected (38 passed across servm+eer+weights).
  • DAG dry-run with profile: servm, planning_horizons: [2030, 2035] resolves (11 jobs incl. both retrieves and the weights rule). Real-data spot check: 2026 forecast × weather-year 2019 totals 267 TWh CA net load (PGE 100.5, SCE 97.8, LADWP 26.5, NCNC 20.2, SDGE 18.1, IID 4.2 TWh).
  • Tier B not run for this PR (the CA integration harness lands in stack PR 4). No equivalence run for this branch (new profile is opt-in; existing profiles unchanged apart from the additive zonal-components output).

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; parquet uses the already-pinned pyarrow).
  • 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

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>
@ktehranchi

Copy link
Copy Markdown
Collaborator Author

Superseded by #801 — the five-PR stack was collapsed into a single PR at the author's request. All commits (plus Tier B fixes) are on feat/cpuc-servm-california.

@ktehranchi ktehranchi closed this Aug 30, 2026
@ktehranchi
ktehranchi deleted the feat/servm-demand branch August 30, 2026 01:44
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