Run online (zero install): Open in Colab – click and run; needs a Google account.

Run locally: clone the repo and install with the notebook extra:

git clone https://github.com/PREP-NexT/PREP-SHOT.git
cd PREP-SHOT
pip install -e .[notebook]
jupyter lab doc/source/Quickstart.ipynb
[1]:
# On Google Colab, install PREP-SHOT and clone the repo so the
# path-walk below can locate run.py / config.json. No-op on
# Local installs already have prepshot importable.
try:
    import google.colab  # type: ignore  # noqa: F401
    IS_COLAB = True
except ImportError:
    IS_COLAB = False

if IS_COLAB:
    import os
    if not os.path.isdir('/content/PREP-SHOT'):
        !git clone --depth=1 https://github.com/PREP-NexT/PREP-SHOT.git /content/PREP-SHOT
    %cd /content/PREP-SHOT
    !pip install --quiet -e .
    !pip install --quiet matplotlib h5netcdf

Quickstart (30 minutes)

Goal: take a fresh checkout of PREP-SHOT, solve the shipped example, read the results, change one input, and re-solve – all in about half an hour. No prior knowledge of capacity-expansion modeling is assumed; everything happens against the examples/three_zone/ dataset that ships with the repo.

If anything in this page does not work for you, please open a GitHub issue – we read every report and a broken Quickstart is the bug we most want to know about.

Scenario background

The shipped examples/three_zone/ dataset is inspired by real-world data, drawing primarily from the U.S. Energy Information Administration (EIA), the U.S. Army Corps of Engineers (USACE), and the U.S. National Renewable Energy Laboratory (NREL).

It models a cascading hydropower system of 15 interconnected stations across three balancing authorities – BA1, BA2, and BA3 – each with distinct jurisdictional connections to their local reservoirs. Aside from hydropower, no other existing generation or transmission is in place; the model decides what to build from four candidate technologies (coal, wind, solar, energy storage) along an electric-mix pathway from 2020 to 2030 toward zero-carbon emissions. We use a 48-hour representative period for the analysis.

Step 1 – Install (5 minutes)

PREP-SHOT requires Python 3.9 or newer. Conda is recommended so the optimization-solver dependencies stay isolated:

conda create -n prep-shot python=3.11 -y
conda activate prep-shot

git clone https://github.com/PREP-NexT/PREP-SHOT.git
cd PREP-SHOT
pip install -e .

The default solver is the open-source HiGHS, installed automatically as a wheel. To use a commercial solver (Gurobi, COPT, MOSEK) instead, set solver in the scenario’s config.json – see the Model Inputs/Outputs page.

Step 2 – Solve the shipped example (5 minutes)

The repo ships a self-contained 3-zone, 11-year example in examples/three_zone/. We solve it programmatically so this notebook works regardless of the working directory.

[2]:
import sys
import os
import io
import tempfile
import pathlib

# Notebooks inherit the kernel launcher's argv; clear it so
# prepshot.set_up.parse_cli_arguments() sees no scenario flags.
sys.argv = ['notebook']

# Locate the repo root (the dir containing run.py).
repo_root = pathlib.Path.cwd()
while not (repo_root / 'run.py').exists():
    if repo_root == repo_root.parent:
        raise RuntimeError(
            'Could not find PREP-SHOT repo root; run this notebook '
            'from a checkout of PREP-NexT/PREP-SHOT.'
        )
    repo_root = repo_root.parent
scenario = repo_root / 'examples' / 'three_zone'
print(f'Scenario: {scenario}')
Scenario: /Users/energy/01-doing/PREP-SHOT-tutorial/PREP-SHOT/examples/three_zone
[3]:
from prepshot.cli import main

# HiGHS writes its progress log via C-level stdout (file
# descriptor 1), which bypasses Python's sys.stdout. Redirect
# the file descriptor to a tempfile during the solve so the
# notebook output stays readable.
log = tempfile.NamedTemporaryFile('w+', delete=False, prefix='prepshot_solve_')
log_path = log.name
sys.stdout.flush()
saved = os.dup(1)
os.dup2(log.fileno(), 1)
cwd_before = os.getcwd()
os.chdir(scenario)  # solve_model writes output/ relative to cwd
try:
    solved = main(root_dir=str(scenario))
finally:
    sys.stdout.flush()
    os.dup2(saved, 1)
    os.close(saved)
    log.close()
    os.chdir(cwd_before)
print(f'log: {log_path}')
2026-05-05 10:08:15 INFO: Set parameter solver to value highs
2026-05-05 10:08:15 INFO: Set parameter input folder to value input
2026-05-05 10:08:15 INFO: Set parameter output_filename to value year.nc
2026-05-05 10:08:15 INFO: Set parameter time_length to value 48
2026-05-05 10:08:15 INFO: Start running 'create_model'
2026-05-05 10:08:15 INFO: Loaded HiGHS library: /Users/energy/miniconda3/envs/prep-shot/lib/python3.9/site-packages/highsbox/highs_dist/lib/libhighs.dylib
2026-05-05 10:08:15 INFO: Loaded highs library automatically.
2026-05-05 10:08:16 INFO: Finished 'create_model' in 1.33 seds
2026-05-05 10:08:16 INFO: Start running 'solve_model'
2026-05-05 10:08:16 INFO: Starting iteration recorded at 2026-05-05 10:08:16.
2026-05-05 10:08:31 INFO: Water head error: 371.93%
2026-05-05 10:11:14 INFO: Water head error: 23.38%
2026-05-05 10:12:11 INFO: Water head error: 19.46%
2026-05-05 10:12:11 WARNING: Ending iteration recorded at 2026-05-05 10:12:11.Failed to converge. Maximum iteration exceeded.
2026-05-05 10:12:11 INFO: Finished 'solve_model' in 234.56 seds
2026-05-05 10:12:11 INFO: Start running 'extract_results_hydro'
2026-05-05 10:12:11 INFO: Start running 'extract_results_non_hydro'
2026-05-05 10:12:11 INFO: Finished 'extract_results_non_hydro' in 0.03 seds
2026-05-05 10:12:11 INFO: Finished 'extract_results_hydro' in 0.04 seds
2026-05-05 10:12:11 INFO: Results are written to ./output/year.nc
2026-05-05 10:12:11 INFO: Start running 'save_to_excel'
2026-05-05 10:12:16 INFO: Finished 'save_to_excel' in 5.41 seds
2026-05-05 10:12:16 INFO: Results are written to separate excel files
log: /var/folders/y_/ypyrt83d1hl9fhjtt_ftpwg00000gn/T/prepshot_solve_nd6kj4sl
[4]:
# Pick out the final objective value from the captured log.
with open(log_path) as f:
    log = f.read()
objective_lines = [l for l in log.splitlines() if 'Objective value' in l]
if objective_lines:
    # The solve runs `iteration_number` head iterations; each
    # prints an objective. The last one is what matters.
    print(objective_lines[-1].strip())
print(f'Solved: {solved}')

Objective value     :  1.8804965672e+11
Solved: True

On commodity hardware the default settings (48 hours, 1 representative month, 3 head iterations) finish in around 2 minutes. The solver log will report Objective value : 1.8809...e+11, and a NetCDF file appears under output/. Everything in the rest of this notebook reads from that file.

The log lines Water head error: ... and the warning Failed to converge. Maximum iteration exceeded. are normal for the default settings: PREP-SHOT runs three head-iteration rounds and reports the final residual. Bump iteration_number in the scenario’s config.json if you want a tighter (slower) converged solve.

Step 3 – Open the results (10 minutes)

PREP-SHOT writes results in xarray’s NetCDF format, so any tool that reads NetCDF can consume them.

[5]:
import xarray as xr
import json

config = json.loads((scenario / 'config.json').read_text())
# config stores the filename WITHOUT extension; the model writes
# both .nc (xarray) and .xlsx alongside each other.
output_path = (
    scenario / 'output'
    / (config['general_parameters']['output_filename'] + '.nc')
)
print(f'Reading results from: {output_path}')

ds = xr.open_dataset(output_path)
print(sorted(ds.data_vars))

Reading results from: /Users/energy/01-doing/PREP-SHOT-tutorial/PREP-SHOT/examples/three_zone/output/year.nc
['carbon', 'carbon_breakdown', 'charge', 'cost', 'cost_fix', 'cost_fix_breakdown', 'cost_newline', 'cost_newline_breakdown', 'cost_newtech', 'cost_newtech_breakdown', 'cost_var', 'cost_var_breakdown', 'gen', 'genflow', 'income', 'install', 'public_debt_newtech', 'shadow_price_demand', 'spillflow', 'trans_export']

Three things worth a first look:

1. Total cost. A single number, the NPV of system cost over the planning horizon.

[6]:
print(f"Total cost: ${float(ds.cost):,.0f}")
Total cost: $188,049,656,721

2. Installed capacity over time. Which technologies expanded, in which zones, and when.

[7]:
install_by_year = ds["install"].sum("zone").to_pandas()
install_by_year
[7]:
tech Coal Solar Wind Storage Grand_Coulee Chief_Joseph Wells Rocky_Reach Rock_Island Wanapum Priest_Rapids Lower_Granite Little_Goose Lower_Monumental Ice_Harbor McNary John_Day The_Dalles Bonneville
year
2020 34.526238 0.000000 28044.058684 1.109202e-13 5054.0 2234.0 741.0 1126.0 482.0 890.0 828.0 747.0 748.0 784.0 525.0 1022.0 1943.0 1414.0 921.0
2021 34.526238 0.000000 28044.058684 1.109202e-13 5054.0 2234.0 741.0 1126.0 482.0 890.0 828.0 747.0 748.0 784.0 525.0 1022.0 1943.0 1414.0 921.0
2022 34.526238 0.000000 28044.058684 1.109202e-13 5054.0 2234.0 741.0 1126.0 482.0 890.0 828.0 747.0 748.0 784.0 525.0 1022.0 1943.0 1414.0 921.0
2023 34.526238 0.000000 28044.058684 1.109202e-13 5054.0 2234.0 741.0 1126.0 482.0 890.0 828.0 747.0 748.0 784.0 525.0 1022.0 1943.0 1414.0 921.0
2024 34.526238 19256.365123 28044.058684 1.109202e-13 5054.0 2234.0 741.0 1126.0 482.0 890.0 828.0 747.0 748.0 784.0 525.0 1022.0 1943.0 1414.0 921.0
2025 34.526238 19256.365123 28044.058684 1.109202e-13 5054.0 2234.0 741.0 1126.0 482.0 890.0 828.0 747.0 748.0 784.0 525.0 1022.0 1943.0 1414.0 921.0
2026 34.526238 26561.715951 29957.801139 6.589525e-14 5054.0 2234.0 741.0 1126.0 482.0 890.0 828.0 747.0 748.0 784.0 525.0 1022.0 1943.0 1414.0 921.0
2027 34.526238 26561.715951 29957.801139 6.589525e-14 5054.0 2234.0 741.0 1126.0 482.0 890.0 828.0 747.0 748.0 784.0 525.0 1022.0 1943.0 1414.0 921.0
2028 34.526238 26561.715951 29957.801139 6.589525e-14 5054.0 2234.0 741.0 1126.0 482.0 890.0 828.0 747.0 748.0 784.0 525.0 1022.0 1943.0 1414.0 921.0
2029 34.526238 39334.186600 29957.801139 6.589525e-14 5054.0 2234.0 741.0 1126.0 482.0 890.0 828.0 747.0 748.0 784.0 525.0 1022.0 1943.0 1414.0 921.0
2030 34.526238 39334.186600 29957.801139 6.589525e-14 5054.0 2234.0 741.0 1126.0 482.0 890.0 828.0 747.0 748.0 784.0 525.0 1022.0 1943.0 1414.0 921.0

3. Locational marginal prices (shadow prices on demand). This is the dual of the power-balance constraint – the marginal cost of one extra MWh of demand at each (hour, month, year, zone). Useful for diagnosing where the system is most stressed.

[8]:
# LMP at zone BA1 in 2025, averaged over the modeled month:
lmp = ds["shadow_price_demand"].sel(zone="BA1", year=2025).mean("month")
print(lmp)
<xarray.DataArray 'shadow_price_demand' (hour: 48)> Size: 384B
array([ 0.00000000e+00,  2.33266957e-13,  2.39959157e-13,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
       -9.41463675e-14,  0.00000000e+00,  0.00000000e+00,  1.93438793e-02,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
        0.00000000e+00,  0.00000000e+00,  0.00000000e+00,  0.00000000e+00])
Coordinates:
  * hour     (hour) int64 384B 1 2 3 4 5 6 7 8 9 ... 40 41 42 43 44 45 46 47 48
    year     int64 8B 2025
    zone     <U3 12B 'BA1'

Values in shadow_price_demand are NPV-discounted dollars per MWh. To recover undiscounted real-year prices, divide by the year’s variable-cost factor (computed in prepshot.load_data.compute_cost_factors).

A minimal generation-mix chart:

[9]:
%matplotlib inline
import matplotlib.pyplot as plt

gen_by_tech = (
    ds["gen"]
    .sum(["hour", "month", "zone"])  # sum over time and space
    .to_pandas()                     # rows=year, cols=tech
    .clip(lower=0)                   # storage net-discharge can be ~0; clamp
)
# Drop techs that never generate (e.g. storage round-trips to ~0):
gen_by_tech = gen_by_tech.loc[:, gen_by_tech.sum() > 0]

fig, ax = plt.subplots(figsize=(8, 4))
gen_by_tech.plot.area(ax=ax)
ax.set_ylabel("Generation (MWh)")
ax.set_title("Generation mix over the planning horizon")
ax.legend(loc="center left", bbox_to_anchor=(1.0, 0.5), fontsize=8)
fig.tight_layout()
plt.show()

_images/Quickstart_19_0.png

Step 4 – Change one input and re-solve (5 minutes)

Every input is a CSV under examples/three_zone/. To see how the model responds to a change, try one of these single-file edits.

Option A – bump demand 20% in one zone. Open examples/three_zone/demand.csv (long format: zone, year, month, hour, unit, value) and multiply the BA1 column by 1.2 in your editor of choice. Then re-run from examples/three_zone/ (cd examples/three_zone && python -m prepshot). The objective will rise – the model has to build more capacity or import more from neighboring zones to serve the extra load. shadow_price_demand at BA1 should also increase in the most constrained hours.

Option B – introduce a carbon tax. Open examples/three_zone/policy_carbon_tax.csv and replace the value column with a non-zero number (e.g. 50 USD/tonneCO2). Re-run from the scenario directory; the generation mix should shift away from coal and gas toward zero-carbon technologies, raising cost_carbon in the breakdown.

Tip – run scenarios without overwriting your baseline. Save your modified file as examples/three_zone/demand_high.csv and run python -m prepshot --demand=high from inside examples/three_zone/. PREP-SHOT appends the suffix to the file name, so the baseline demand.csv is left untouched. Output goes to examples/three_zone/output/year_high.nc.

Step 5 – Where to next

  • Model Inputs/Outputs – full input / output reference, including optional carbon-market and finance modules.

  • `examples/southeast_asia/SoutheastAsia.ipynb <../../examples/southeast_asia/SoutheastAsia.ipynb>`__ – a realistic case study (Lower Mekong basin, 5 countries, 57 cascading reservoirs).

  • Mathematical Notation – the underlying linear program.

  • Changelog – what’s new in each release.

If you used PREP-SHOT in published work, please cite it – see the Citation Guide. And if you ran into something rough, the fastest fix is to file an issue on GitHub.