# PyPSA-BC Model Documentation

**Author:** Md Eliasinul Islam  
**Model framework:** [DeltaE/PyPSA_BC](https://github.com/DeltaE/PyPSA_BC), PyPSA v0.28+  
**Last updated:** 2026-04-24  
**Network file:** `BCPypsa_Base_CNZ_2035.nc`  
**Generated by:** `scripts/build_network_doc.py`

---

## 1. What is PyPSA-BC?

PyPSA-BC is an **open-source, hourly economic dispatch model** for British Columbia's electricity system, built on the [PyPSA framework](https://pypsa.org). It sits inside a three-model integrated framework for BC's energy transition analysis:

```
RESource  ──────────────────────────────────────────►  Spatially explicit VRE siting
  (land-use screening, ERA5 capacity factors)          (solar, wind potential maps)
          │
          ▼ candidate sites + capacity factors
BC-Nexus  ─────────────────────────────────────────►  Long-term investment planning
  (CLEWs-OSeMOSYS, 18–48 time slices)                 (capacity mix, technology costs)
          │
          ▼ investment decisions + scaled demand
BC-PyPSA  ─────────────────────────────────────────►  Hourly operational dispatch
  (this model, 8,760 snapshots)                        (congestion, spill, curtailment)
          │
          ▼ threshold feedback signals
BC-Nexus  ──  re-optimisation trigger
```

BC-PyPSA answers: *given a capacity portfolio (from BC-Nexus) and hourly weather/demand, can the system actually operate without violating transmission constraints or running out of stored hydro energy?*

---

## 2. Spatial Resolution and Aggregation

### 2.1 Node structure

The model represents BC at **28 GADM Level-2 regional district nodes**. This is a deliberate design choice balancing computational tractability (8,760 × LP) against spatial resolution sufficient to capture inter-regional transmission bottlenecks.

Each AC node corresponds to one of BC's 28 administrative regional districts. Components within a regional district are aggregated to that node's centroid.

**Aggregation workflow (see `PyPSA_builder.ipynb`, Cells 24 & 35):**
1. Raw network loads all individual substation buses and original high-voltage lines
2. `get_bc_busmap_dict()` assigns each substation to its containing GADM regional district polygon
3. `create_adm_buses()` creates 28 new AC buses at polygon centroids
4. `replace_bus_refs()` remaps all component `bus`, `bus0`, `bus1`, `bus2` fields
5. `aggregate_lines()` collapses parallel corridors between the same regional district pair into a single equivalent line (summed `s_nom`)
6. `remove_old_components()` removes the raw substation buses and superseded lines

![28-node aggregated network with hydro overlay](../Visuals/general/network_overlay_aggr.png)

*Figure 1. BC-PyPSA 28-node network after geographic aggregation. AC nodes (circles) at GADM Level-2 regional district centroids. Line width ∝ rated corridor capacity. Hydro Water-bus nodes (triangles) are connected to their AC discharge bus via Discharge Links (not shown at this scale). Network map produced by `plot_static(n, hydro_overlay=True)` in `PyPSA_builder.ipynb`.*

### 2.2 Bus inventory

| Bus category | Count | Carrier | Description |
|---|---|---|---|
| AC regional district | 28 | `AC` | One per GADM Level-2 regional district |
| AC intertie | ~8 | `AC` / `DC` | Alberta (AB), Washington State (WA), internal tie points |
| Water — GSS | ~20 | `Water` | Generating Station System — connects inflow to discharge |
| Water — RES | ~20 | `Water` | Reservoir — holds stored water volume (MWh_water) |
| Commodity — Biomass | 1 | `Biomass` | Spatially aggregated biomass fuel supply bus |
| Commodity — NG | 1 | `NG` | Spatially aggregated natural gas supply bus |
| **Total** | **~75** | | |

---

## 3. Temporal Resolution

| Parameter | Value |
|---|---|
| Snapshots | 8,760 hourly time steps |
| Simulation period | 2021-01-01 00:00 → 2021-12-31 23:00 UTC |
| Time step | 1 h (uniform) |
| Weather year | ERA5 reanalysis 2021 |

The model runs a **single-year operational dispatch**. Annual reservoir cycling is enforced via `e_cyclic=True` on all reservoir `Store` components — the end-of-year storage level must equal the initial level.

---

## 4. Transmission Network

### 4.1 AC lines

| Attribute | Value |
|---|---|
| Total inter-regional corridors | ~45 (post-aggregation) |
| Capacity range | ~63 MW – ~7,500 MW |
| Topology | Meshed; represents 500 kV and 230 kV inter-regional backbone |
| N-1 approximation | `s_max_pu = 0.85` applied (15% security margin) |
| Extendability | `s_nom_extendable = False` in fixed-transmission scenarios |

### 4.2 Reading the congestion map

![NE_FE 2040 transmission congestion](../Visuals/NE_FE_2040/BC2040_congestion_light.png)

*Figure 2. Transmission congestion, NE_FE 2040 scenario. Line colour: hours per year at ≥90% of rated capacity loading (white = no congestion; deep red = severely congested). Line width: rated corridor capacity (MW). Produced by `pypsa_explorer` notebook, Cell 7–8.*

**How to read:**
- **Colour** encodes congestion frequency — the number of hours per year a corridor operates at or above 90% of its rated capacity. This is a stronger metric than annual-average utilization because it captures how often the constraint is actually binding.
- **Width** encodes capacity — a narrow highly-congested line is a more critical bottleneck than a wide moderately-congested one.
- **White lines** are uncongested; **red lines** are binding constraints that propagate to nodal marginal prices.
- **Isolated red cluster in the south** (Fraser–Thompson–Okanagan, coastal–Vancouver): these are the system's structural bottlenecks. Power generated in northern BC cannot reach these load centres without reinforcing these corridors.

### 4.3 Line loading duration curves

![Line duration curves](../Visuals/NE_FE_2040/line_duration_curves.png)

*Figure 3. Loading duration curves for the top-5 most congested corridors. X-axis: hours per year sorted in descending order of loading (0 = most loaded hour). Y-axis: loading as fraction of rated capacity. The horizontal dashed line at 85% marks the operational threshold above which a line is considered congested.*

**How to read:**
- A curve that stays above 85% for many hours indicates a **persistently saturated** corridor — this corridor limits dispatch for a significant fraction of the year, not just during peak events.
- A steep drop-off below 85% indicates **peaky congestion** — the corridor binds only during extreme events (e.g., high-load winter peak + low wind output simultaneously).
- Curves that remain near 100% for hundreds of hours signal that the corridor should be in any transmission expansion plan.

---

## 5. Generation Fleet

### 5.1 Technology classification

Generator names are classified into technology types via name-pattern matching (see `PyPSA_builder.ipynb`, Cell 26). The `n.generators.tech` field is the standard grouping variable for all post-processing:

| `tech` value | Component type | Carrier convention |
|---|---|---|
| `ror_hydro` | Run-of-river generators | AC bus |
| `reservoir_inflow` | Hydro inflow sources (water volume, **not MW**) | Water bus |
| `solar` | Solar PV | AC bus |
| `wind` | Wind (onshore) | AC bus |
| `nuclear` | Nuclear (expansion site, not yet active) | AC bus |
| `backstop` | Slack/feasibility generator | AC bus |
| `other` | Thermal, biomass, cogeneration, etc. | AC or NG/Biomass bus |

> ⚠️ **Critical post-processing note:** `n.generators.carrier` is empty for most generators in this model — the model uses a bus-level carrier convention. **Always use `n.generators.tech` for technology disaggregation, not `n.generators.carrier`.** For capacity totals: `n.generators.groupby('tech').p_nom.sum()`.

### 5.2 VRE capacity factors

23 generators carry hourly `p_max_pu` profiles derived from ERA5 reanalysis via RESource. Key statistics:

| Technology | Typical mean CF | Notes |
|---|---|---|
| Solar PV | 0.12–0.18 | Near-zero Nov–Jan; strong summer peak |
| Wind (onshore) | 0.25–0.40 | Most sites; ERA5-derived at hub height |
| Wind — CFP24 Capital RD | **~0.029** | ⚠️ Anomalously low — verify bus assignment |

### 5.3 Generation dispatch stack

![Monthly dispatch stack](../Visuals/NE_FE_2040/dispatch_monthly.png)

*Figure 4. Monthly generation dispatch stack, NE_FE 2040 scenario. Each area corresponds to one technology group aggregated to monthly totals (TWh/month). The backstop area (if visible) represents unserved energy — load that no generation asset could serve within the transmission constraints.*

**How to read:**
- **Stack height = total served load** for that month. If it falls below the demand line, backstop generators filled the gap.
- **Hydro (reservoir + RoR) dominates** in all months — BC's grid is hydro-centric.
- **Seasonal hydro pattern:** Reservoir generation is typically highest in late summer/fall (drawing down freshet-charged storage) and lowest in late winter/early spring (minimum reservoir levels before freshet refill).
- **Wind contribution** is small in current scenarios due to land-use constraints on new sites.
- **Backstop months** (if any) identify when and where capacity adequacy fails — this directly maps to the marginal price heatmap.

### 5.4 VRE curtailment

![VRE curtailment by node](../Visuals/NE_FE_2040/curtailment.png)

*Figure 5. VRE curtailment by regional district bus (%), NE_FE 2040 scenario. Bars above the 5% threshold line (orange dashed) indicate nodes where VRE cannot be fully utilised. In a supply-constrained system, curtailment is expected to be near zero.*

**How to read:**
- **Near-zero curtailment** in a supply-constrained scenario (like NE_FE 2040 with ~15 TWh backstop) is expected and correct — the system can use every MWh it generates.
- **High curtailment** would indicate a **surplus** scenario: more VRE generation than the local demand + transmission capacity can absorb. This is the signal to add storage or increase transmission.
- **Curtailment + congestion occurring simultaneously** at the same node is the key diagnostic for transmission as the binding constraint, not generation capacity.

---

## 6. Hydro Architecture

### 6.1 Water-bus / Store / Link model

BC-PyPSA uses a custom **Water-bus architecture** — not PyPSA's native `StorageUnit`. This is necessary to correctly represent the multi-reservoir cascade systems on the Peace and Columbia rivers.

```
ERA5 Runoff Data
      │
      ▼
Inflow Generator ──────────► GSS Water Bus ──────────► Downstream GSS
(p_max_pu = hourly              │                        (via Release Link)
 inflow fraction;               │
 p_nom = very large)            ▼ Store Link
                          RES Water Bus ◄────────────────► Store
                              │                            (e_nom = reservoir capacity,
                              │                             e_cyclic = True,
                              ▼ Discharge Link              MWh_water)
                          AC Regional Bus
                         (turbine output, MW)
                              │
                              ▼ Spill Link
                          Downstream GSS or sink
                          (excess water bypasses turbines)
```

![BC hydro reservoir systems](../Visuals/general/reservoir_systems.png)

*Figure 6. BC major hydro reservoir systems modelled in PyPSA-BC. Peace River cascade (MCA, PCN, STC, ALU, GMS, etc.) and Columbia system (REV, HLK, etc.) are the primary seasonal storage providers.*

### 6.2 Component inventory

| Component | Count | Notes |
|---|---|---|
| Water buses (GSS) | ~20 | Generating station system buses — entry point for inflow |
| Water buses (RES) | ~20 | Reservoir buses — hold stored water volume |
| Discharge Links | ~20 | Water → electricity conversion (η = turbine efficiency) |
| Spill Links | ~20 | Non-generating water release (excess beyond turbine capacity) |
| Inflow Generators | ~18 | Hourly ERA5-derived water inflow profiles |
| Reservoir Stores | ~16 | `e_cyclic=True`; capacity in MWh_water |

### 6.3 Hydro seasonal dynamics

![Reservoir storage levels](../Visuals/NE_FE_2040/reservoir_levels.png)

*Figure 7. Reservoir storage level time series, NE_FE 2040 scenario. Each line = one reservoir Store, normalised to fraction of capacity (0 = empty, 1 = full). The seasonal envelope is driven by BC's snow-melt hydrology.*

**How to read:**
- **Peak storage: June–July** — snowmelt freshet fills reservoirs to near capacity.
- **Minimum storage: March–April** — end of winter drawdown before freshet refill.
- **Spread between reservoirs** reveals operational flexibility — some systems (e.g., Williston/Peace) have large multi-month storage; others (small RoR-like reservoirs) cycle daily/weekly.
- **Flat lines near minimum** indicate a reservoir operating at its `e_min` lower bound — the environmental flow constraint is binding.
- **Flat lines near maximum** indicate a spilling reservoir — inflow exceeds turbine + downstream release capacity.

![Reservoir storage heatmap](../Visuals/NE_FE_2040/reservoir_heatmap.png)

*Figure 8. Monthly × reservoir heatmap of storage level (fraction of capacity). Dark = low storage; light = high. Identifies which reservoirs are stressed in which months.*

### 6.4 Hydro spill

![Hydro spill time series](../Visuals/NE_FE_2040/hydro_spill.png)

*Figure 9. Hydro spill time series (total system, m³/s). Peaks coincide with the spring freshet (May–June) when inflow exceeds combined turbine and storage capacity. High sustained spill beyond the freshet indicates that storage capacity is insufficient to capture and defer surplus inflow.*

**How to read:**
- **Spring peak (May–June):** Normal and expected — no storage system can fully capture the freshet. The key question is whether spill occurs outside the freshet window.
- **Summer/fall spill** (outside freshet): Unusual — may indicate VRE curtailment and hydro spill co-occurring (surplus energy), or a reservoir that has been prematurely filled.
- **Winter spill:** Operationally problematic — this is the season when stored energy is most needed. Winter spill signals a model configuration issue (e.g., incorrect reservoir capacity or inflow scaling).
- **Threshold:** >100 h/year with non-zero spill is the BC-Nexus feedback trigger (see `postprocess_pypsa_bc.py`).

---

## 7. Demand

| Attribute | Value |
|---|---|
| Load buses | 25 (22 regional districts + 3 excluded: Stikine, CentralCoast, NorthernRockies) |
| Annual system load (Base_CNZ_2035) | ~100.5 TWh (verify after load update) |
| Peak demand | ~18,223 MW |
| Load disaggregation | Residential + Commercial/SMI sectors, GADM Level-2 spatial weights |
| Temporal shape | 2021 BCH BalancingAuthority hourly profile, scaled to CLEWs annual total |

> ⚠️ **Demand trajectory flag:** The `BCPypsa_Base_CNZ_2035.nc` file carries the same 100.53 TWh / 18,222 MW load profile as the NE_FE 2040 network. Confirm that `loads_t.p_set` has been correctly scaled for each scenario before interpreting results.

---

## 8. Marginal Prices

![Marginal price heatmap](../Visuals/NE_FE_2040/marginal_price_heatmap.png)

*Figure 10. Monthly × node heatmap of average nodal marginal prices ($/MWh), NE_FE 2040 scenario. Colour intensity = price level. Dark red = chronically constrained nodes with high scarcity rent.*

**How to read:**
- **Marginal price = shadow price of the power balance constraint** at each bus for each hour. It represents the incremental cost of serving one additional MWh at that node.
- **Price ~$0:** The node has surplus generation (hydro running at minimum, VRE fully dispatched, no congestion to adjacent nodes).
- **Price = marginal cost of the most expensive active generator:** Normal dispatch — the last generator needed to balance supply and demand at that node sets the price.
- **Price = backstop cost ($1,000/MWh):** The node is experiencing unserved energy — no generation asset within reach (accounting for transmission limits) can serve additional demand.
- **Spatial price gradient:** Price differences between adjacent nodes, sustained over many hours, indicate a congested transmission corridor separating cheap generation from expensive load.
- **Average ~$757/MWh in NE_FE 2040** is backstop-driven — a direct manifestation of the ~15 TWh generation shortfall. Once the capacity gap is closed, prices should fall to the $20–$80/MWh range typical of a hydro-dominant system.

---

## 9. Threshold Feedback Signals (BC-Nexus Interface)

Post-solve, `postprocess_pypsa_bc.py` screens results against these operational thresholds (from PhD proposal Table 7). Breach signals are written to `threshold_feedback_{scenario}.json` for BC-Nexus re-optimisation:

| Metric | Threshold | Signal meaning |
|---|---|---|
| VRE curtailment | > 5% of annual VRE generation | Oversupply or transmission-limited VRE integration |
| Line utilisation ≥ 85% | > 400 h/year on any corridor | Transmission bottleneck — evaluate reinforcement |
| Hydro spill | > 100 h/year on any reservoir | Storage–generation imbalance |
| Backstop dispatch | Any non-zero dispatch | Capacity adequacy failure — insufficient supply or deliverability |

> ⚠️ **Architecture mismatch in postprocess_pypsa_bc.py:** The current script was written for a `StorageUnit`-based hydro model. The actual network uses Water-bus / Store / Link architecture. Hydro spill must be read from `n.links_t.p0[spill_links]`, and reservoir SOC from `n.stores_t.e[reservoir_stores]`, not from `n.storage_units_t`. The script requires adaptation before use on current networks.

---

## 10. Scenario Registry

| Scenario tag | Demand trajectory | Year | Transmission | Status |
|---|---|---|---|---|
| `Base_CNZ_2035` | Canada Net Zero (BCH IRP) | 2035 | Fixed | Network built, solve status unknown |
| `NE_FE_2040` | Northeast BC Full Electrification | 2040 | Fixed | Solved (`prepared_net_2040.nc`) |
| `2021_Baseline` | 2021 BCH actual demand | 2021 | Fixed | **Target for calibration — not yet built** |

---

## 11. How to Regenerate This Document

Run the documentation builder script on any solved (or pre-solve) PyPSA network:

```bash
python scripts/build_network_doc.py \
  --network "network files/BCPypsa_Base_CNZ_2035.nc" \
  --scenario Base_CNZ_2035 \
  --visuals-dir "Visuals/Base_CNZ_2035" \
  --general-visuals-dir "Visuals/general" \
  --outfile "docs/PyPSA_BC_Model_Documentation.md"
```

The script inspects the network's static attributes (buses, generators, lines, links, stores) and builds a structured Markdown document with quantitative tables. Visual paths are inserted as relative Markdown image references — all figures must be present in the specified `--visuals-dir` before running.

See `scripts/build_network_doc.py` for full usage and customisation options.

---

*Documentation auto-generated from `BCPypsa_Base_CNZ_2035.nc` using `scripts/build_network_doc.py`. Quantitative values derived from network static attributes. Post-solve metrics require a solved network.*
