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.
Note
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:
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'smin_outflowexceeds its natural inflow, water balance can drain storage belowstorage_min-> the sub-problem is infeasible. v1.15+ will add cross-window cascade state (carry the upstream's lastmax_delayoutflow values into the next window'sinflow_rulelookup).Carbon cap rescaling: the cap from
policy_carbon_emission_ limit.csvis 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 bywindow_hours / hours_in_yearin 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 CEMbaseline.ncwritten byrun.py. Theinstalldata array (dimsyear x zone x tech) is selected for the chosen year..csv-- a tidy table with columnszone, 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)[source]¶
Return
{(zone, tech): capacity_MW}fortarget_year.sourcemay be a path to:a CEM
baseline.nc(xarray dataset with aninstallarray indexed byyear x zone x tech), ora tidy CSV with columns
zone, tech, year, capacity.
Relative paths are resolved against
scenario_dir.- Parameters
source (Path) --
target_year (int) --
scenario_dir (Path) --
- Return type
dict
- 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)[source]¶
Solve PCM dispatch over one year with rolling horizon.
- Parameters
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.ncor 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 toNone(run all hours).allow_load_shedding (bool, optional) -- Allow non-negative slack (
lns) on every nodal power balance, priced atvollin the objective. Lets PCM windows complete even when the dispatch can't physically meet demand at some (hour, zone). Overridesreliability_parameters.allow_load_sheddingfromconfig.jsonwhen set.voll (float, optional) -- Value of lost load (USD/MWh) for the load-shedding penalty. Overrides
reliability_parameters.vollfromconfig.jsonwhen set. Defaults to 10000.
- Return type
None