pcm

Production-cost-model (PCM) mode with rolling horizon.

Companion mode to the default capacity-expansion (CEM) flow in run.py. PCM takes a fixed fleet (from a prior CEM solve, or a user-supplied capacity CSV) and solves only dispatch over a chosen year, hour-by-hour, in overlapping windows -- the same machinery that PowNet and PyPSA's optimize_with_rolling_horizon use.

备注

v1.14.1 status (alpha). The rolling driver is wired end-to-end with single-window solves working cleanly. Multi-window rolling has TWO known limitations:

  1. Cascading hydro cross-window state: at the start of any non-first window, downstream stations see only their natural (incremental) inflow because the upstream's outflow during the lookback period (hour[0] - delay .. hour[0]) is in the previous window's solve and is not yet carried forward. When the downstream's min_outflow exceeds its natural inflow, water balance can drain storage below storage_min -> the sub-problem is infeasible. v1.15+ will add cross-window cascade state (carry the upstream's last max_delay outflow values into the next window's inflow_rule lookup).

  2. Carbon cap rescaling: the cap from policy_carbon_emission_ limit.csv is annual (full-year tonneCO2) but each window only covers a fraction of the year. The naive filter applies the full annual cap to a 48-hour window -- not binding for our small examples but wrong on principle. Will rescale by window_hours / hours_in_year in v1.15.

For now, recommend --horizon == --step == period_length (single-window PCM = fixed-capacity dispatch validation). Multi-window rolling works on scenarios without binding min-outflow constraints on cascaded hydro.

Why rolling horizon?

Solving dispatch on the full 8760-hour annual MILP/LP at once is intractable for realistic networks. Rolling horizon decomposes the problem into a sequence of short, overlapping subproblems:

while t < 8760:

build LP for hours [t, t + horizon) pin initial state (storage SOC, hydro reservoir level) from the

previous window's terminal solution

solve persist dispatch decisions for [t, t + step) advance t by step

Each window's lookahead (horizon - step) absorbs the end-of-window distortion. PowNet uses horizon = 48 h, step = 24 h; we default to the same.

Capacity source

Two formats are accepted via --cap-source PATH:

  • .nc -- a CEM baseline.nc written by run.py. The install data array (dims year x zone x tech) is selected for the chosen year.

  • .csv -- a tidy table with columns zone, tech, year, capacity (MW). Useful when running PCM standalone, no CEM needed.

If neither is supplied, the existing fleet from tech_existing.csv is used as-is (no expansion). This is the "validate-the-existing-build" mode.

Config

Add a pcm_parameters block to config.json:

"pcm_parameters": {
    "horizon_h": 48,
    "step_h": 24,
    "year": 2030,
    "total_h": 168
}

Or pass as CLI flags. CLI overrides config.

total_h (optional) caps how many hours from hour[0] are simulated -- useful as a smoke test on large nodal models, where a single 48 h window can take tens of minutes to build. Omit (or set null) to run the full year.

Output

PCM dispatch lands in output/baseline_pcm/ -- one Parquet sidecar per output variable (gen.parquet, trans_export.parquet, lns.parquet, ...), keyed in long format so the file size scales with non-zero entries (not the dense (h, m, y, z, ...) product). A manifest.json next to the Parquets records the schema. The companion CEM baseline.nc is left untouched.

CLI

cd examples/three_zone
python -m prepshot.pcm --year 2025 --horizon 48 --step 24

# smoke test: only the first 48 h, one window
python -m prepshot.pcm --year 2025 --horizon 48 --step 48 --total-h 48

Or programmatically:

from prepshot.pcm import run_pcm
run_pcm('examples/three_zone', year=2025)
run_pcm('examples/three_zone', year=2025, total_h=48)
prepshot.pcm.load_fixed_capacity(source, target_year, scenario_dir)[源代码]

Return {(zone, tech): capacity_MW} for target_year.

source may be a path to:

  • a CEM baseline.nc (xarray dataset with an install array indexed by year x zone x tech), or

  • a tidy CSV with columns zone, tech, year, capacity.

Relative paths are resolved against scenario_dir.

参数
  • source (Path) --

  • target_year (int) --

  • scenario_dir (Path) --

返回类型

dict

prepshot.pcm.main()[源代码]

CLI entry point: python -m prepshot.pcm.

返回类型

None

prepshot.pcm.run_pcm(scenario_dir, *, year=None, horizon_h=48, step_h=24, cap_source=None, total_h=None, allow_load_shedding=None, voll=None)[源代码]

Solve PCM dispatch over one year with rolling horizon.

参数
  • scenario_dir (path-like) -- Directory containing config.json, params.json, input/.

  • year (int, optional) -- Which model year to dispatch. Defaults to the first year of the config's planning horizon.

  • horizon_h (int) -- Window length in hours. Defaults to 48.

  • step_h (int) -- Window advance in hours (committed segment). Defaults to 24.

  • cap_source (str, optional) -- Path to a CEM baseline.nc or capacity CSV. If omitted, the CEM-shipped existing fleet is used unchanged.

  • total_h (int, optional) -- Cap on the number of hours simulated from hour[0]. Useful as a smoke test on large nodal models where the full year is intractable. Defaults to None (run all hours).

  • allow_load_shedding (bool, optional) -- Allow non-negative slack (lns) on every nodal power balance, priced at voll in the objective. Lets PCM windows complete even when the dispatch can't physically meet demand at some (hour, zone). Overrides reliability_parameters.allow_load_shedding from config.json when set.

  • voll (float, optional) -- Value of lost load (USD/MWh) for the load-shedding penalty. Overrides reliability_parameters.voll from config.json when set. Defaults to 10000.

返回类型

None