1. Executive Summary

This project implements a full-stack Capacity Expansion Planning (CEP) model for the Southwest Power Pool (SPP) Regional Transmission Organization. Starting from the full Eastern Interconnection (EI) power system model (92,511 real buses), a 10-step subgraph-based Ward reduction pipeline produces a tractable network of 1,304 buses and 3,626 equivalent lines. A mixed-integer-free DCOPF-based CEP model then optimizes generation and transmission investment decisions across six planning years (2024–2049) at 4% annual load growth.

92,511
Full EI Real Buses
1,304
Retained Buses
3,626
Equivalent Lines
$305.9B
Total Optimal Cost
23
SPP Areas (MMWG v43)
OPTIMAL
Solver Status
Data Source The base case is the 2024 ITP Final (24S) PSS/E RAW file from SPP, parsed using VeraGrid. Technology costs are from NREL ATB 2024 and EIA AEO 2025 (all in 2022 USD).

2. Pipeline Architecture

The reduction is implemented in reduction/run_subgraph_ward.py, a single end-to-end script with 10 sequential steps. The pipeline preserves total generation and load via the Ward equivalencing load-flow (LF) matrix.

2.1 Pipeline Steps

1

Load Full EI Network

Parse PSS/E RAW via VeraGrid circuit object. 92,511 real buses + 3,867 three-winding star buses.

2

Star Bus Area Fix (Border-Aware)

Three-winding transformer star buses have area=0 in PSS/E. Internal stars (all SPP windings) get assigned their SPP area (1,375 reassigned). Mixed-winding stars (16 border transformers) stay non-SPP; their winding connections become tie-lines.

3

Filter to SPP-23

Retain only buses in the 23 active SPP areas. Result: 10,965 real + 1,375 star buses. Branches crossing the SPP boundary are converted to tie-line load/generation injections.

4

Drop Island Fragments

Connected component analysis on the SPP subgraph. Main component: 10,821 real buses. Dropped: 144 real buses in 46 fragments (677 MW load, 1,212 MW generation) that connect to the SPP mainland only through external areas.

5

Subgraph Consolidation

Three criteria identify buses that should be collapsed into single subgraphs: (1) transformers at any voltage, (2) low-impedance bus ties (Z < 0.001 pu AND |B| < 0.001 pu), (3) short lines (< 2 miles). Result: 2,051 subgraphs, 4,978 real collapsed buses.

6

Radial Absorption

Iteratively merge degree-1 (radial) buses into their single neighbor. Converges in 19 iterations. Radial buses represent dead-end lines that do not participate in through-flow.

7

Retention Selection

Retain expanded subgraph representatives + single degree ≥ 3 junction buses, all ≥ 69 kV. Degree-2 passthrough buses are NOT retained (Ward preserves their impedance paths). Result: 1,304 retained buses.

8

Ward Reduction

DC Ward equivalencing with zeroed shunts, charging, and taps. Produces Y_eq (1,304 x 1,304) equivalent admittance matrix and LF load-flow redistribution matrix.

8.5

Network Reconstruction

Extract equivalent lines from Y_eq off-diagonal entries. Map generators and loads to retained buses via the LF matrix. Conservation verified: total generation and load match pre-reduction values.

9

Impedance Analysis

6,390 total equivalent lines extracted from Y_eq. Impedance distribution analyzed: median |Z| = 0.27 pu, range 0.000001 to >100 pu.

10

Line Pruning

Drop lines with |Z| > 5 pu (2,764 lines, 43%). These are Ward fill-in artifacts carrying negligible power. Uses |Z| = sqrt(R² + X²), not just |X|. Result: 3,626 lines (median |Z| = 0.27 pu, max 5.0 pu).

2.2 End-to-End Chain

#ScriptInputOutput
1run_subgraph_ward.py2024ITPFinal-24S_fixed.rawWard reduction + reconstruction CSVs
2export_full_lines.pyReconstructed linesEqLineCap/spp.xlsx
3Julia test_cap_spp.jlspp.xlsxspp_results.xlsx (3,626/3,626 OPTIMAL)
4load_analysis.pyFERC 714, retained busescep_load.csv, cep_peak.csv, cep_blockhours.csv
5build_cep_full_dataset.pyAll reduction + cost dataIn_DatasetSPP.xlsx (18 sheets)
6cep_model.py --no-crossoverIn_DatasetSPP.xlsxCEP results CSVs
7cep_dashboard.pyCEP resultsInteractive Plotly/Leaflet dashboard

3. SPP Area Definition

SPP area membership follows the MMWG Procedural Manual v43, Appendix V (page 62). SPP has 25 operational areas, of which 23 have active buses in the 2024 ITP model.

3.1 The 25 SPP Areas

AreaCodeAreaCodeAreaCode
506MJMEUC523GRDA541EMMW
511AECC524OKGE542KACY
515SWPA525WFEC544EMDE
520AEPW526SPS545INDN
527OMPA531MIDW546SPRM
534SUNC/MKEC536EKC540GMO
640NPPD/MEAN641HAST642GRIS
645OPPD650LES652WAPA/CBPC
659BEPC

Plus areas 997, 998, 999 (HVDC terminals to WECC/ERCOT) -- not load/generation areas.

3.2 Area Classification

CategoryAreasNotes
Active SPP (23) 506, 515, 520, 523, 524, 525, 526, 527, 531, 534, 536, 541, 542, 544, 545, 546, 640, 641, 642, 645, 650, 652, 659 Configured in constants.yaml
Empty SPP (2) 511 (AECC), 540 (GMO) Zero buses in 2024 ITP case
External SERC 327 (Entergy-AR), 330 (AECI), 356 (Ameren-MO) NOT SPP; previously misidentified
External MISO 600, 608, 613, 615, 620, 627, 633, 635, 661, 663, 667, 672, 680, 694, 696, 697, 698 600-series are MISO/MRO

3.3 Hanging Fragments

144 real buses in 46 fragments are dropped in Step 4. These buses connect to the SPP mainland only through external areas (327, 330, 356). The largest fragments are in area 515 (66 buses via areas 327/330/356) and area 523 (24 buses via 330). Total dropped: 677 MW load, 1,212 MW generation.

4. Subgraph Consolidation

Buses that are electrically at the same physical location (same substation or switchyard) are collapsed into single-bus subgraphs. Three criteria determine membership:

4.1 Consolidation Criteria

CriterionConditionRationale
Transformers Any 2W or 3W transformer connection, any voltage Transformers connect buses within the same substation
Low-Z bus ties Z < 0.001 pu AND |B| < 0.001 pu Near-zero impedance connections (bus couplers, switches). The susceptance filter excludes cables (which have high B but are real lines).
Short lines Length < 2 miles Lines shorter than 2 miles indicate same physical location (adjacent substations)

4.2 Representative Selection

Each subgraph elects one representative bus using these tiebreaking rules (in order):

  1. Exclude star-point buses (≥ 1,000,000)
  2. Prefer SPP-area buses over external
  3. Highest voltage (BASE_KV)
  4. Highest generation capacity (PMAX) tiebreak
  5. Lowest bus number (deterministic)

4.3 Retention Criteria

CategoryConditionCount
Expanded subgraph repsSubgraph with absorbed radials, ≥ 69 kV~1,050
Junction busesSingle buses with degree ≥ 3, ≥ 69 kV~254
Total retained1,304
Degree-2 passthrough buses are NOT collapsed. These buses sit on a path between two junctions. Collapsing them would lose real impedance information. Instead, the Ward reduction naturally handles their elimination while preserving the equivalent electrical distance.

5. Three-Winding Transformer Handling

PSS/E models three-winding transformers with an internal "star bus" (bus number ≥ 1,000,000, area = 0). The pipeline must correctly classify each star bus as SPP or non-SPP.

5.1 Classification Rules

CaseWinding AreasStar Bus TreatmentCount
Internal All windings in SPP Star assigned to SPP area (majority vote) 1,375
Border Mixed SPP + non-SPP windings Star stays non-SPP; winding connections become tie-lines 16

5.2 Key Collision Fix

The xfmr_3w_star_points dictionary uses compound keys that include the star bus ID to avoid collisions when multiple three-winding transformers share endpoint buses. The full set contains 3,867 entries with zero lost due to key collision.

6. Line Expansion Costs

Line expansion cost (ec, in M$/MW) has three components, computed in build_cep_full_dataset.py. Source data: In_TxInvestmentCost.xlsx.

6.1 Cost Components

ComponentFormulaSource SheetUnit
Line cost MDpMWMI × distance_miles costmwmile M$/MW
Transformer cost DpMVA / 1,000,000 trans M$/MW (at unity PF)
Substation expansion MDpMW at both endpoints ACSubExpCost M$/MW
EC Floor: All lines have a minimum ec of 0.1 M$/MW. This prevents near-zero investment costs on very short or zero-distance equivalent lines from distorting the optimization.

6.2 Line Cost by Voltage (MDpMWMI)

Voltage (kV)34.569 115138161 230345500 765
MDpMWMI 0.40000.2100 0.11250.09600.0640 0.04080.00840.0050 0.0031

Lower-voltage lines have higher per-MW-mile costs due to surge impedance loading (SIL) scaling: a 69 kV line carries far fewer MW than a 345 kV line, so the cost per MW is proportionally higher. This is correct physics.

6.3 Transformer Cost (DpMVA) -- Selected Pairs

Pair (kV)DpMVA ($/MVA)Pair (kV)DpMVA ($/MVA)
69 / 1384,581138 / 3456,241
69 / 2305,348230 / 3456,566
69 / 3456,566345 / 5008,466
115 / 2305,348345 / 76510,286
138 / 2305,348500 / 76511,347

6.4 Substation Expansion Cost (MDpMW)

Voltage (kV)69115 138161230 345500765
MDpMW 0.10080.0575 0.05100.03710.0259 0.01220.00800.0074

Substation expansion cost is applied at both endpoints of every new line (new bay: circuit breakers, disconnect switches, bus work, protection equipment). Voltage-mismatch lines additionally include the transformer cost at the lower-kV end.

7. CEP Model

The CEP model (reduction/cep_model.py) is a linear program (LP) implementing DCOPF-coupled capacity expansion planning, solved with Gurobi's barrier method.

7.1 Model Parameters

ParameterValueNotes
Planning years2024, 2029, 2034, 2039, 2044, 20496 five-year periods
Time blocks174 seasons × 4 blocks + 1 Peak block
Total hours modeled8,760Covers full year
Load growth4% annual (compound)Reference scenario
Discount rate5.4%NREL ATB 2024 value
VoLL$9,000/MWhValue of lost load (LBNL/Carvallo 2024 central)
PRM15%Planning Reserve Margin
End-effect years30Years of operation after last investment year
SBASE100 MVAPSS/E standard per-unit base
Theta bound1.5 radVoltage angle limit (~86 degrees)
Min admittance (DCOPF)1e-6 puLines below this are transport-only; upper cutoff (MAX_ADM) 100 pu excludes from angle coupling
Max admittance (DCOPF)100 puClamped to improve condition number

7.2 Solver Configuration

SettingValue
SolverGurobi (barrier, Method=2)
CrossoverDisabled (--no-crossover)
Barrier convergence tolerance1e-6
Homogeneous barrierEnabled (robustness)
OBJ_SCALE1000x (improves conditioning)
Cost unitM$ (millions of dollars)

7.3 Generation Technologies

CategoryFull NameHR (MMBtu/MWh)InvestableCapacity Credit
WindOnshore Wind0.0Yes0.20 (ELCC)
SolarSolar PV0.0YesSeasonal CF
CCCombined Cycle Gas6.3Yes1.0
GasCTCombustion Turbine Gas9.7Yes1.0
GasSTSteam Turbine Gas9.7Existing only1.0
CoalSTSteam Turbine Coal10.0No (existing)1.0
NuclearNuclear10.5Yes1.0
HydroConventional Hydro0.0No (existing)CF-based
OilGTOil Gas Turbine10.0No (existing)1.0
BiomassBiomass/Renewable13.5No (existing)1.0
STOBattery Storage0.0No (disabled)--
PSPumped Storage Hydro0.0No (disabled)--

7.4 Capacity Factors

Wind and Solar capacity factors are derived from HRRR weather data, mapped to each of the 1,304 retained buses via NREL renewable site databases (60 km search radius). The CapFac sheet contains 1,065 bus-level seasonal/block capacity factors. Wind capacity credit for the PRM constraint is 0.20, validated against SPP's 2025 ELCC value (5,504 MW / 27,400 MW nameplate = 20.1%).

7.5 DCOPF Formulation

The model uses a DC optimal power flow formulation with these key features:

7.6 Cost Accounting

ComponentFormulaUnits
GenInv (OCC)OCC ($/kW) × invested_MW × 1e-3M$
FOMFOM ($/kW-yr) × total_cap_MW × gapfac × 1e-3M$
VOMVOM ($/MWh) × gen_MW × hours × gapfac × 1e-6M$
FuelFuelPrice × HR × gen_MW × hours × gapfac × 1e-6M$
LineInvec (M$/MW) × invested_MWM$
ShedVoLL × shed_MW × hours × gapfac × 1e-6M$

8. Results

8.1 Cost Summary

$135.6B
Gen Investment (OCC)
$2.7B
Line Investment
$88.4B
Fixed O&M
$7.9B
Variable O&M
$60.7B
Fuel
$52M
Load Shedding

Reference scenario (4% load growth), DCOPF, corrected re-run 2026-06-15 (commit bbac115).

Cost ComponentValue (M$)Share (%)
Generation Investment (OCC)135,56445.9
Fixed O&M88,39829.9
Fuel60,65120.5
Variable O&M7,9152.7
Line Investment2,6800.9
Load Shedding520.02
Total295,261100.0

8.2 Investment Summary

CategoryCount (decisions)Notes
Generation investments1,190Bus-tech-year triples
Line investments403Line-year pairs
Generation retirements0Endogenous only (none economic at current FOM)

8.3 Load Shedding Analysis

Total reference-scenario shedding is 1,804 MWh across 52 buses (over the full 25-year horizon, all 17 time blocks) — economically optimal at VoLL = $9,000/MWh and a negligible 0.02% of total cost. All shedding is FW-constrained (line capacity), never DCOPF-constrained (angle). The low (1.5%) scenario sheds 662 MWh; the high (7%) scenario sheds 0.93 TWh — physical, driven by ~5× demand growth by 2049, not the phantom 14 TWh that earlier appeared in a stale pre-fix run (see Audit §9.8).

Validation: shedding is economic, not physical infeasibility — it scales monotonically with load growth (662 MWh / 1,804 MWh / 0.93 TWh for low/ref/high) and with VoLL. Removing theta bounds changes ref shedding by only ~2%. Per-bus candidate capacity is not binding at the ref growth rate.

9. Audit Findings & Fixes

Three independent audit agents reviewed the pipeline on 2026-04-12. All critical findings were resolved; remaining items are deferred decision points.

9.1 Fixed Issues

IDFindingFixImpact
C2 Nuclear fuel price = $0 Added $0.76/MMBtu (EIA AEO 2025) ~$48M fuel increase
H1 |X| filter instead of |Z| for line pruning Changed to |Z| = sqrt(R² + X²) 75 phantom high-admittance lines removed
M2 Shed penalty used dfac instead of gapfac Changed to gapfac (consistent with VOM/Fuel) Shedding 4.5x costlier; dropped 1,659 to 817 MWh
C3 Line ec missing transformer + substation costs Added DpMVA and ACSubExpCost from In_TxInvestmentCost.xlsx 282 to 80 lines at EC floor
M3 EC floor applied only to ec=0 Applied 0.1 M$/MW minimum to all lines below threshold Prevents trivially cheap investment
M5 Wind capacity credit unvalidated Validated CC = 0.20 against SPP 2025 ELCC (5,504/27,400 = 20.1%) No change needed
M6 Low-kV line costs appear high Confirmed correct (SIL scaling physics) No change needed

9.2 Sensitivity Tests

TestShed (MWh)ChangeFinding
Pre-audit baseline1,659----
Post-audit (gapfac etc.)817-51%gapfac fix was biggest driver
VoLL = $500,000 (10x)596-64%Economic, not physical
No theta bounds (100 rad)1,624-2%Theta NOT the bottleneck
MIN_ADM = 0.51,537-7%Redistributes, does not eliminate

10. Known Limitations & Future Work

10.1 Decision Points (Deferred)

IDIssueStatusImpact
D1 Line investment increases capacity (FW) only, not admittance in DCOPF. In reality, parallel lines both increase capacity and halve impedance. Deferred Not currently binding: all shedding is FW-bound, never angle-bound
D2 Battery storage (STO) and pumped storage (PS) have IsInv=0 -- cannot invest. GenCost has STO OCC trajectory ($1,770 to $919/kW by 2049) but unused. Needs candidate sites Model may over-invest in GasCT for flexibility
D3 Endogenous retirement is wired in (pv_GenRetYear variable + capacity balance constraint in cep_model.py:472,610) and works — the 2026-04-13 run retired 9.4 GW of wind, 0 GW of coal/gas. What is not wired are the scenario-level aggregate retirement targets Coal_Ret, Gas_Oil_Ret, Nuclear_Ret defined in the Scens sheet (build_cep_full_dataset.py:961 — never read by cep_model.py) and no constraint enforces announced EIA-860 retirement dates. Partially implemented Optimizer chooses endogenously; real-world policy retirements (e.g. coal-by-2030 commitments) are not enforced. Coal currently stays flat at 21.8 GW across all 25 years.
D4 Single scenario only (reference, 4% load growth). Need: low (1.5%), high (7%), no-carbon, low-discount-rate. Scens sheet defined Results represent one scenario only

10.2 High Priority (Before Publication)

IDIssueNotes
H1 Forced outage rates (FOR) not modeled Tech sheet has FOR values (3.25-5.93%) but thermal is modeled at 100% availability
H2 102 generators (9,827 MW) have non-exact technology match 78 inferred-capacity, 13 inferred, 5 exact_collision. Manual review needed.

10.3 Medium Priority

IDIssue
M1Refactor identify_transformer_buses to include 3W_STAR rows in xfmr_2w output
M2Y_eq dense iteration in network_reconstruction.py is O(n²); works for 1,304 buses but will not scale
M3Per-generator heat rates (currently per-technology; EIA-860 has plant-level data)
M4FERC 714 multi-year load validation (currently single year 2024)
M5Solar CandCap capping (top site: 339 GW at one bus -- NREL aggregation artifact)

10.4 Accepted Limitations

10.5 2026-04-19 deep-audit summary

The 2026-04-19 independent audit (full report: AUDIT_2026_04_19.html) raised additional items on top of 9.1 above. Severity legend: CRIT — blocks publishable use; MAJ — materially biases results; MIN — does not change top-line numbers; OBS — observational.

SevFindingStatus (2026-04-19)
CRIT MMWG-derived PSSE case is CEII-restricted under SPP/FERC. Resolved. 50 MB .sav untracked and purged from git history (git filter-repo, force-push completed). Local copy retained; .gitignore broadened.
MAJ OSM overlay data has no ODbL attribution in the React dashboard. Resolved. Credits added under the map (dashboard/src/tabs/MapView.tsx).
MIN tsc disabled in CI (.github/workflows/build.yml). Resolved. Type-check step re-enabled; build 33 deployed.
MIN 18 legacy tmp_*.py, utils.py, amapgenload.py scratch files at repo root. Resolved. Deleted; -6,839 LoC.
CRIT Single deterministic scenario; VoLL code-default $50k suppresses shed. Resolved. Code-default and Scalars both VoLL=$9,000 (build_cep_full_dataset.py:919, LBNL central). All three load-growth scenarios (low/ref/high) re-run 2026-06-15 and monotonic. (No-carbon variant still deferred.)
MAJ Ward reduction never validated against full-network PTDF/LODF. Open. Diagnostic scripts (compare_dc_flows.py, compare_full_vs_reduced.py) exist; need one committed report under docs/validation/.
MAJ Line investment adds MW but not admittance. Open, acknowledged. Tracked under D1.
MAJ FOR defined but not applied to thermal firm capacity in PRM constraint. Open. Tracked under H1.
MAJ Storage IsInv=0, cost trajectory loaded but unused. Open. Tracked under D2.
MAJ SPP PRM now 16% summer / 36% winter; model still uses 15% annual. Open. Raise PRM & split by season.
MAJ Full-network JSON payloads ~35 MB uncompressed; LCP above 2.5 s on 4G. Open. Candidate fix: pmtiles or deck.gl binary attributes.
MAJ 4,336 of 90,455 buses are topology-inferred (centroid smoothing), not validated. Open. Need stratified ground-truth sample & per-tier accuracy report.
MAJ 374 manual corrections in bus_locations_corrected.csv have no audit trail. Open. Document sampling protocol and before/after impact.
MIN Voltage colour scheme in dashboard not CVD-safe (mixed warm/cool, red on top). Open. Consider viridis/cividis perceptually-uniform scale.
OBS Compression ratio 92,511→1,304 buses (1.4% retention, EI basis; 11.9% SPP-internal basis). Aggressive vs Shi 2012 (5–10% typical). Within-SPP retention is defensible; add PTDF fidelity evidence (see 10.5 above).

11. Data Files

11.1 Ward Reduction Outputs

Directory: data/processed/reduction_subgraph/

FileDescriptionRecords
reduced_buses.csvRetained bus list with area, kV, reason1,304 buses
retained_buses_subgraph.csvRetained buses (alternate format)1,304 buses
reconstructed_generators_ward.csvGenerators mapped to retained buses via LF347 gens, 77,910 MW
reconstructed_lines_ward.csvEquivalent lines after |Z| pruning3,626 lines
reconstructed_loads_ward.csvLoads aggregated via Ward formula1,304 loads, 58,438 MW
equivalent_lines_all.csvAll Ward equivalent lines (diagnostic)6,390 lines
equivalent_lines_subgraph.csvPruned equivalent lines (diagnostic)3,626 lines
Y_eq_subgraph.npzWard equivalent admittance matrix1,304 x 1,304
LF_subgraph.npzLoad-flow redistribution matrix1,304 x (eliminated)
Y_spp_subgraph.npzFull SPP admittance matrix (pre-Ward)Full SPP size
bus_index_map_subgraph.csvBus ID to Y-matrix index mappingAll SPP buses
boundary_tie_gens.csvTie-line generators (empty in carve-out mode)0
cep_load.csvHourly-to-block load by bus1,304 buses
cep_peak.csvPeak load by bus1,304 buses
cep_blockhours.csvHours per season/block (sums to 8,760)17 blocks
bus_load_shares.csvBus-level load share fractions1,304 buses
bus_hourly_loads_2024.parquetHourly bus loads (intermediate)1,304 x 8,760

11.2 CEP Input

Directory: data/processed/cep/

FileDescription
In_DatasetSPP.xlsx18-sheet CEP input workbook (GAMS-compatible format)

The 18 sheets are: Buses, Lines, Gens, Tech, Times, Years, Scalars, Controls, GenCost, Fuel_Prices, CarbonEmRed, Scens, CapFac, Load, Peak, DFac, ACSubExpCost, TransCost.

11.3 CEP Results

Directory: data/processed/cep/results/

FileDescription
cep_cost_summary.csvObjective breakdown: GenInv, LineInv, FOM, VOM, Fuel, Shed, Total
cep_gen_investment.csvGeneration investment decisions (1,044 rows)
cep_line_investment.csvLine investment decisions (428 rows)
cep_gen_retirement.csvGeneration retirement decisions (52 rows)
cep_gen_capacity.csvTotal generation capacity by bus/tech/year
cep_dispatch_summary.csvDispatch by technology and year
cep_dispatch_detail.csvDetailed dispatch by bus/tech/year/season/block
cep_load_shedding.csvLoad shedding events by bus/year/season/block

11.4 Dashboards

Directory: data/processed/cep/

FileDescription
cep_dashboard.htmlInteractive Plotly/Leaflet dashboard (latest)
cep_dashboard_full_v7.htmlFull-featured dashboard (version 7)

12. Technology Stack

12.1 Core Tools

ComponentVersionPurpose
Python3.9 (spp conda env)Pipeline, CEP model, dashboards
Julia1.11Equivalent line capacity optimization (EqLineCap)
Gurobi11.xLP solver (barrier method)
VeraGrid--PSS/E RAW file parser and circuit object model

12.2 Python Libraries

LibraryPurpose
gurobipyGurobi Python interface for CEP model
pandas / numpyData manipulation and numerical computation
scipy.sparseSparse matrix operations (Y-bus, Ward reduction)
networkxGraph analysis (connectivity, subgraphs, degree)
plotlyInteractive charts in dashboards
folium / leafletGeographic map visualizations
openpyxlExcel I/O for CEP dataset and cost data
PyYAMLConfiguration file parsing

12.3 Deployment

ComponentDetail
Repositoryjahanbani/spp-acep-demo (GitHub)
Branchextended-boundary
Serverspp.ali.ece.iastate.edu
ContainerDocker + Nginx (port 3002)
OrchestrationPortainer (auto-deploys from GitHub polling)
Build triggerIncrement # build: N in docker-compose.yml

13. Comparison with SPP / NREL Authoritative Studies

The 2026-04-19 audit flagged order-of-magnitude deltas between the CEP demo and SPP’s own Integrated Transmission Plan (ITP) and NREL’s ReEDS mid-case. This section reports actual numbers extracted from the current deployed scenario (site/data/scenarios/scen_ref/*.parquet) and explains each delta.

13.1 Transmission investment: our $2.68 B / 25 y vs. SPP ITP10 $7.7 B / 10 y

MetricOur model (25 y, 2024–2049)SPP ITP10 approved 2024 (10 y, 2024–2034)
Total transmission $$2,683 M$7,700 M
Corridors upgraded22189 projects
MW added13,937 MW thermal uprate~2,333 mi new + 495 mi rebuild (different unit)
Timing66% of MW in final year 2049; back-loadedFront-loaded for immediate reliability drivers
Size distribution185 corridors under 100 MW (30% of total MW); 5 corridors over 500 MW (30%)Most ITP10 projects are bulk 345 kV or 765 kV, new-build

Our line-investment totals are about one-eighth of SPP’s own 10-year bulk plan when normalised to a 10-year window. The audit called this a red flag; the deep-dive shows several defensible reasons:

  1. The reduced network has no candidate for many ITP corridors. SPP’s 89 projects include new greenfield 345 kV corridors between substations that are collapsed in the Ward reduction (they sit inside retained-bus subgraphs or were absorbed as radial sub-trees). The CEP cannot build lines that don’t exist as candidates.
  2. Line investment does not relieve impedance. Under D1, expansion only raises MW cap. A parallel 345 kV circuit in reality halves reactance and doubles PTDF-weighted flow headroom — a benefit the objective cannot see. Lines that would be worth building for congestion relief look unbinding to the optimiser.
  3. No N-1 security constraint. A large fraction of ITP10 investment is reliability-driven (N-1, N-1-1) rather than economic-congestion-driven. The CEP formulation has no such constraint.
  4. No public-policy drivers. ITP includes transmission needed for wind integration under SPP’s renewable mandates, queue-based interconnection upgrades, and state-mandated siting. None of these enter the objective.
  5. Solver numerical floor dampens small investments. With OBJ_SCALE=1 (natural M$; Gurobi ScaleFlag=2 handles conditioning) and BarConvTol=1e-6, investments below the solver’s numerical floor (tens of MW at the bus level) can be dismissed as noise. This also explains the heavy back-loading into 2049 — the model defers builds until they are economically unambiguous.
  6. Scope: ITP10 covers bulk facilities ≥100 kV; our pruning floor is 69 kV. Line-cost per MW at sub-transmission voltages differs substantially, so absolute $ comparison needs normalisation by MW-miles and voltage class.

Action items: fix the admittance-scaling issue (see D1), add N-1 security (run OPF with standard contingency set and bind flow at 0.8x rating or use LODF), improve numerical conditioning (see §14). After these, rerun and expect transmission spend in the $4–7 B range, closer to ITP10 when normalised.

13.2 Generation investment: our 284 GW / 25 y vs. NREL ReEDS SPP 45–95 GW

The audit cited a 140 GW investment figure; the actual aggregate across all new-build technologies in the 2026-04-13 run is 284.2 GW (confirmed from gen_investment.parquet). Breakdown:

TechnologyInvested (GW)Final 2049 capacity (GW)Delta from 2024 (GW)
Wind161.1173.2+161.1
Solar50.250.5+50.2
GasCT43.050.9+42.9
CC29.940.2+19.2
Storage (STO)000
CoalST / GasST / Hydro / Nuclear0 (not investable)37.10
Total284.2352.5+273.5

Reality-check against the PRM constraint: 2024 peak ~58 GW, 4%/y growth → 2049 peak ~155 GW. At PRM=15% the firm requirement is ~178 GW. Our firm contributions: Wind 173 × 0.20 = 35; Solar 50.5 × ~0.15 = 7.6; GasCT 51 + CC 40 + Coal 22 + GasST 10 + Hydro 3.5 + Nuclear 2 + OilGT 0.5 ≈ 129 thermal-firm. Sum ≈ 171 GW firm — tightly matches the ~178 GW PRM target. Nameplate 352 GW looks oversized only because the CC factor of wind (20%) and solar (~15%) is low: the optimiser must build ~5 MW of wind nameplate for every 1 MW of firm capacity credit.

Why we’re much higher than ReEDS

  1. Storage is disabled (D2). In ReEDS mid-case, 4-hour batteries substitute for 15–30 GW of thermal firm capacity and lift effective VRE CC by 10–20 percentage points. Without storage, our model must over-build VRE nameplate and keep GasCT to cover the residual peak. This alone accounts for roughly 80–100 GW of the delta.
  2. Higher load growth. The reference scenario is 4%/yr (aggressive); NREL ReEDS mid-case uses the EIA AEO reference trajectory which is roughly 1.5–2%/yr on SPP. Over 25 years, that’s a 2× difference in terminal peak load.
  3. No coal retirement. ReEDS endogenously retires coal on economics; we have endogenous retirement as a decision variable but the optimiser keeps all 21.8 GW online. NREL/EIA-860 has every SPP coal plant with a scheduled retirement year — wiring those as mandatory constraints would shift build mix toward Wind+Storage replacement capacity and reduce the 352 GW terminal footprint.
  4. No line-admittance upgrades. Transmission congestion is the main reason ReEDS curtails wind rather than building more of it. With flat impedance, our model sees no such cap and keeps adding wind until wind-CC-weighted capacity balance is met.

Action items: enable storage investment (D2), wire EIA-860 announced retirements (D3), run low (1.5%) growth scenario (D4). After these, expect the Wind+Solar+Storage envelope to be in the NREL ReEDS range and total nameplate around 200–230 GW.

13.3 Retirement: clarification of prior audit

The 2026-04-19 audit stated retirement was “disabled.” This is incorrect. Endogenous retirement via pv_GenRetYear is wired into cep_model.py:472, 610 and is part of the capacity balance constraint. What is not implemented is (a) scenario-level aggregate retirement targets (Coal_Ret/Gas_Oil_Ret/Nuclear_Ret parameters in the Scens sheet, read at build_cep_full_dataset.py:961 but never consumed by the MIP) and (b) exogenous EIA-860 announced-retirement dates. See corrected D3 above.

14. Solver Numerical Conditioning

14.1 Current state

The LP is built with coefficients whose magnitudes span many orders of magnitude:

QuantityNatural unitOrder of magnitudeReason
Objective coefficients (OCC)M$/MW-y0.5–5ATB CAPEX (Solar ∼ $0.8M/MW, Nuclear ∼ $8M/MW)
Shed penaltyM$/MWh9e-3–5e-2VoLL/1e6; 9k–50k $/MWh
Line flow boundsMW50–5×1052·θ·adm·SBASE; can be huge where adm is large
Angle θrad1.5THETA_BOUND
Admittance1/pu0.001–103Short ties vs long corridors
Cumulative hoursh102–104Block hours × years
OBJ_SCALEdimensionless103Workaround for conditioning

The resulting coefficient range spans ~1010 (1e-3 shed penalty × 1e4 hours on one side; 1e5 MW flow × 103 admittance on the other). Gurobi’s barrier with BarConvTol=1e-6 targets a normalised residual; the effective physical tolerance is tol × MaxCoeff / OBJ_SCALE ≈ 1e-6 × 1010 / 103 = 10 MW at worst-case conditioning. That matches the observed “tens-of-MW noise per bus-block” reported by the audit. (A former OBJ_SCALE=1000 masked it by uniformly shrinking the objective without changing the ratio; the model now runs at OBJ_SCALE=1 and relies on Gurobi ScaleFlag=2 for conditioning.)

14.2 Root-cause fix: matrix equilibration, not tolerance tuning

The right fix is to make the coefficient matrix well-conditioned by unit choice and per-unit normalisation, so Gurobi’s scaling sees a tight range (1e-3 to 1e+3 at most). Concrete steps:

  1. Represent capacity in GW not MW everywhere (PSCALE=1000 already does this for some variables; extend to all capacity/flow/load variables so the numeric range is 0–150 GW rather than 0–150,000 MW).
  2. Remove the per-line custom flow bound 2θ·adm·SBASE — this multiplies admittance (which can be 1000) by SBASE (100) yielding bounds on the order of 105. Replace with explicit FW + CandCap bound everywhere; the DCOPF physics is still enforced by the flow == adm · (θi − θj) constraint. The custom bound is redundant and noisy.
  3. Normalise admittance to base-case reactance. Use x_pu × Sref scaling where Sref = 1 GW so angle-flow coupling is O(1), not O(10–1000).
  4. Set OBJ_SCALE = 1 once the ranges are fixed. Re-introduce only if Gurobi reports NumericalIssue.
  5. Turn on Gurobi auto-scaling explicitly: model.Params.ScaleFlag = 2 (aggressive equilibration), and verify with model.Params.InfUnbdInfo = 1 + model.printStats() that Matrix range is ≤ 104.
  6. Enable crossover for the reported run (Crossover=0 currently). Crossover guarantees a basic-optimal solution rather than an interior point that may lie on a face of degenerate constraints. The runtime cost is typically 10–30% more wall clock; the benefit is extremely clean MW numbers for every bus-block. For sensitivity runs we can keep barrier-only.

A one-day scaling refactor plus a validation run should collapse the shedding’s numeric floor from ~10 MW to <0.1 MW. Only after this is it meaningful to re-run with stricter tolerances (BarConvTol=1e-8) and to make numerical claims about individual bus-block outcomes.

15. |Z| Pruning Sensitivity for Ward Fill-in

15.1 Current state

After the DCPF Ward reduction, the equivalent admittance matrix Y_eq reconstructs 6,390 equivalent lines. Lines with |Z| > 5 pu are pruned, leaving 3,626. The 5 pu threshold is un-sensitized; the audit flagged it as a “magic number” (reduction/modules/network_reconstruction.py).

15.2 Why pruning is needed and what it drops

Ward reduction introduces fictitious “fill-in” edges whenever two retained buses connected through external-bus Schur complements end up with a non-zero equivalent admittance. Many of these fills are physically negligible (very high equivalent impedance ⇔ very weak coupling) but syntactically present. Keeping them (a) inflates the DC-OPF constraint count, and (b) biases PTDFs by introducing parallel paths that don’t exist in the real grid.

However, not every high-|Z| line is fill-in — long high-voltage inter-area ties genuinely have |X| ≈ 2–5 pu. A blind 5 pu cut risks removing real inter-area flowgates.

15.3 Recommended methodology (two-part fix)

Part A — flow-based screening instead of pure impedance threshold. Run a base-case DC power flow on the full reconstruction, then drop only lines whose |flow| < 0.5% of base-case rating and |Z| > 3 pu. This keeps high-|Z| inter-area ties that actually carry flow and discards the Ward fill-in that is electrically invisible. Cross-reference with base-case full-network PTDFs to validate that dropped lines contribute <1% to any retained PTDF row.

Part B — commit a sensitivity sweep. Add a script reduction/sensitivity_zprune.py that runs the pipeline at |Z|max ∈ {2, 3, 5, 7, 10} pu and reports:

Acceptance: choose the smallest threshold that keeps PTDF RMSE < 5% and angular error < 5°. Literature benchmark: Shi (2012) uses 2–3 pu after flow screening; ReEDS uses a multi-step screening that rarely drops below 4 pu.

15.4 Interaction with |X| → |Z| audit fix (H1 in §9.1)

The earlier H1 fix (use |Z| = √(R²+X²), not just |X|) removed 75 phantom high-admittance lines. Part A above is the next step: move from any impedance-only threshold to a flow-sensitive one.

16. Outstanding TODOs (consolidated)

Single source of truth, synthesised from §9, §10, the 2026-04-12 audit, and the 2026-04-19 deep audit. Ordered by expected impact on headline results; ID prefix indicates priority: P0 blocking, P1 high, P2 medium, P3 nice-to-have.

IDItemCategoryExpected impact
P0-1 Apply FOR to thermal firm capacity in the PRM constraint (CC_effective = CC · (1 − FOR), FOR already in Tech sheet). CEP logic Firm capacity shortfall 3–6%, adds several GW of thermal or storage builds.
P0-2 Switch PRM to 16% summer / 36% winter (current SPP standard, effective 2026), split the PRM constraint by season. CEP logic Adds ~20 GW winter firm capacity; storage+CC become very valuable.
P0-3 Enable storage investment: set IsInv=1 for STO, define per-bus CandCap from the SPP GI queue. CEP logic Large reduction in Wind/GasCT nameplate; shifts 40–80 GW of firm capacity to 4-hour batteries.
P0-4 Matrix coefficient conditioning (§14): GW units everywhere, remove custom 2θ·adm·Sbase flow bound, OBJ_SCALE=1, re-enable crossover for headline runs. Numerics Shedding numeric floor drops from ~10 MW to <0.1 MW. Prerequisite for defensible bus-level results.
P0-5 Correct the code-default VoLL from $50,000 to $9,000. DONEbuild_cep_full_dataset.py:919 writes $9,000 (LBNL/Carvallo central) on every rebuild; Scalars sheet matches. Economics No more silent regression on dataset rebuild.
P1-1 Commit a PTDF-fidelity report under docs/validation/: RMSE of |PTDF_reduced − PTDF_full| across 30 random source-sink pairs. Network reduction Evidence for reviewers; cheap (existing compare_dc_flows.py).
P1-2 Enforce EIA-860 announced retirement dates for coal & gas-ST (exogenous constraints on pv_GenRetYear). CEP logic Shifts 20+ GW coal capacity out of the mix; changes replacement build profile.
P1-3 Add N-1 security: for each monitored line, enforce |flow| ≤ 0.8 · rating in the base case, or use LODF screening for the top 50 contingencies. CEP logic Brings transmission investment closer to SPP ITP10 levels.
P1-4 Flow-based |Z| pruning (§15) and sensitivity sweep at {2, 3, 5, 7, 10} pu. Network reduction Removes “magic number” objection; tightens PTDF fidelity.
P1-5 Line-investment admittance scaling: a parallel circuit should halve X and double admittance, not just add MW cap. CEP logic Currently not binding but unverifiable under scenarios the model doesn’t run. Enables angle-bound shedding to be resolved by transmission.
P1-6 Scenario set: run at least 3 growth (1.5, 4, 7%) × 2 fuel (AEO vs. low-gas) × 2 policy (BAU vs. IRA) = 12 runs. Publish a fan chart. Uncertainty Moves the demo from “one outcome” to “plausible range.”
P1-7 Add Winter Storm Uri (Feb 2021) and Elliott (Dec 2022) as mandatory stress blocks in the Times sheet (7 days each, fixed load profile). Resilience Identifies winter firm-capacity shortfall beyond the PRM constraint.
P1-8 Dashboard payload: move full_*_lines.json (12–15 MB uncompressed) to pmtiles or deck.gl binary attributes. Target first-paint under 2 MB gzipped. Dashboard LCP under 2.5 s on 4G; Google Web Vitals compliant.
P1-9 Dollar-year harmonisation (cep_model.py:8 TODO). Choose USD 2024 as reporting year and apply GDP deflator to ATB-2022 values consistently. Economics Numbers become directly comparable to published benchmarks.
P2-1 102 generators (9,827 MW) with non-exact technology match — manual review. Data quality Small shift in starting capacity by tech; tracked as H2.
P2-2 Bus-location ground-truth validation: stratify 100 buses by confidence tier, hand-verify against Google imagery. Data quality Empirical accuracy per tier; lets us mark topology-inferred buses visually.
P2-3 Document the 374 manual corrections in bus_locations_corrected.csv: sampling protocol + before/after impact report. Data quality Audit trail for reviewers.
P2-4 Voltage colour scheme in dashboard: switch to viridis/cividis (perceptually uniform, CVD-safe). Dashboard Accessibility; no content change.
P2-5 Carbon as price, not only cap: run a scenario where Carb_Emission_Flag=0 and add a $/tCO2 price line; report implicit price of the current cap. Economics Standard sensitivity; cheap.
P2-6 AC-OPF validation of the 2049 optimal dispatch against the unreduced 96 k-bus case: report max V, MVA, and reactive violations. Validation Strong evidence of AC feasibility of the DC-OPF-derived plan.
P2-7 Add a PyTest that loads In_DatasetSPP.xlsx, solves a 2-year CEP on a toy 20-bus subset, and asserts objective within ±0.1% of a golden value. Reproducibility CI regression coverage; currently only vite build.
P2-8 Pin Python deps (requirements.lock via pip-tools or uv). Reproducibility Standard hygiene; currently only lower bounds.
P3-1 Fold legacy static dashboards (/dashboards/cep_dashboard_*.html) into the React SPA or drop them. Dashboard Single maintained surface.
P3-2 Alternative-reducer comparison: K-means on PTDF rows vs. spectral clustering vs. current Ward at similar retention. Research Defensible choice of method, not just “the one we ran.”
P3-3 Retire run_reduction.py (legacy, 2184 lines) to reduction/legacy/; run_subgraph_ward.py is canonical. Hygiene Reduces confusion for new contributors.
P3-4 Dashboard: add per-source “methodology” info-drawer for PSSE / PSSE-clipped / OSM so users understand coverage differences. Dashboard Prevents misleading conclusions (“OSM disagrees with PSSE”).

SPP Capacity Expansion Planning — Master Technical Documentation
Iowa State University, Department of Electrical & Computer Engineering
Revision 2026-04-19 (deep-audit integration). Source: reduction/run_subgraph_ward.py (rev 2026-04-11).
Full audit report: AUDIT_2026_04_19.html