Unit Commitment¶
This tutorial runs through examples of unit commitment for generators at a single bus. Examples of minimum part-load, minimum up time, minimum down time, start up costs, shut down costs and ramp rate restrictions are shown, as well as how to set up a rolling horizon optimization.
To enable unit commitment on a component (Link or Generator), set its attribute committable=True.
import pandas as pd
import pypsa
Minimum Part Load¶
In the final snapshot, the load goes below the part-load limit of the coal generator (30%), forcing gas to commit.
nu = pypsa.Network(snapshots=range(4))
nu.add("Bus", "bus")
nu.add(
"Generator",
"coal",
bus="bus",
committable=True,
p_min_pu=0.3,
marginal_cost=20,
p_nom=10_000,
)
nu.add(
"Generator",
"gas",
bus="bus",
committable=True,
marginal_cost=70,
p_min_pu=0.1,
p_nom=1_000,
)
nu.add("Load", "load", bus="bus", p_set=[4_000, 6_000, 5_000, 800])
nu.optimize()
nu.generators_t.status
nu.generators_t.p
Minimum Up Time¶
Gas has a minimum up time, forcing it to be online longer than otherwise necessary, which incurs a standby cost for status up without generation.
nu = pypsa.Network(snapshots=range(4))
nu.add("Bus", "bus")
nu.add(
"Generator",
"coal",
bus="bus",
committable=True,
p_min_pu=0.3,
marginal_cost=20,
p_nom=10000,
)
nu.add(
"Generator",
"gas",
bus="bus",
committable=True,
stand_by_cost=50,
marginal_cost=70,
p_min_pu=0.1,
up_time_before=0,
min_up_time=3,
p_nom=1_000,
)
nu.add("Load", "load", bus="bus", p_set=[4_000, 800, 5_000, 3_000])
nu.optimize()
nu.generators_t.status
nu.objective
nu.generators_t.p
Minimum Down Time¶
Coal has a minimum down time, forcing it to go off longer than otherwise cost-optimal.
nu = pypsa.Network(snapshots=range(4))
nu.add("Bus", "bus")
nu.add(
"Generator",
"coal",
bus="bus",
committable=True,
p_min_pu=0.3,
marginal_cost=20,
min_down_time=2,
down_time_before=1,
p_nom=10_000,
)
nu.add(
"Generator",
"gas",
bus="bus",
committable=True,
marginal_cost=70,
p_min_pu=0.1,
p_nom=4_000,
)
nu.add("Load", "load", bus="bus", p_set=[3_000, 800, 3_000, 8_000])
nu.optimize()
nu.objective
nu.generators_t.status
nu.generators_t.p
Start Up and Shut Down Costs¶
Now there are costs associated with shut down and start up events, which could incentivise longer up times of generators with high start-up and shut-down costs.
nu = pypsa.Network(snapshots=range(4))
nu.add("Bus", "bus")
nu.add(
"Generator",
"coal",
bus="bus",
committable=True,
p_min_pu=0.3,
marginal_cost=20,
min_down_time=2,
start_up_cost=5_000,
p_nom=10_000,
)
nu.add(
"Generator",
"gas",
bus="bus",
committable=True,
marginal_cost=70,
p_min_pu=0.1,
shut_down_cost=25,
p_nom=4_000,
)
nu.add("Load", "load", bus="bus", p_set=[3_000, 800, 3_000, 8_000])
nu.optimize()
nu.objective
nu.generators_t.status
nu.generators_t.p
Ramp Rate Limits¶
Ramp rate limits can be set for ramping up and down and are given as percentage of the nominal power that can be ramped up or down per snapshot. Note that the ramp limits apply per snapshot and are not weighted by the time step duration (nu.snapshot_weightings).
nu = pypsa.Network(snapshots=range(6))
nu.add("Bus", "bus")
nu.add(
"Generator",
"coal",
bus="bus",
marginal_cost=20,
ramp_limit_up=0.1,
ramp_limit_down=0.2,
p_nom=10_000,
)
nu.add("Generator", "gas", bus="bus", marginal_cost=70, p_nom=4_000)
nu.add("Load", "load", bus="bus", p_set=[4_000, 7_000, 7_000, 7_000, 7_000, 3_000])
nu.optimize()
nu.generators_t.p
With capacity expansion (as long as unit is not committable):
nu = pypsa.Network(snapshots=range(6))
nu.add("Bus", "bus")
nu.add(
"Generator",
"coal",
bus="bus",
marginal_cost=20,
ramp_limit_up=0.1,
ramp_limit_down=0.2,
p_nom_extendable=True,
capital_cost=1e2,
)
nu.add("Generator", "gas", bus="bus", marginal_cost=70, p_nom=4000)
nu.add("Load", "load", bus="bus", p_set=[4000, 7000, 7000, 7000, 7000, 3000])
nu.optimize()
nu.generators.p_nom_opt
nu.generators_t.p
Watch out for bad interactions, for example, when the ramp limit at start up or shut down is bigger than the regular ramp limit or minimum part load, which can lead to infeasibilities.
nu = pypsa.Network(snapshots=range(7))
nu.add("Bus", "bus")
# Can get bad interactions if SU > RU and p_min_pu; similarly if SD > RD
nu.add(
"Generator",
"coal",
bus="bus",
marginal_cost=20,
committable=True,
p_min_pu=0.05,
initial_status=0,
ramp_limit_start_up=0.1,
ramp_limit_up=0.2,
ramp_limit_down=0.25,
ramp_limit_shut_down=0.15,
p_nom=10_000,
)
nu.add("Generator", "gas", bus="bus", marginal_cost=70, p_nom=10_000)
nu.add("Load", "load", bus="bus", p_set=[0, 200, 7_000, 7_000, 7_000, 2_000, 0])
nu.optimize()
nu.generators_t.p
nu.generators_t.status
Rolling Horizon¶
The unit commitment optimisation can be combined with a rolling horizon optimisation, i.e. solving the snapshots sequentially in batches. This can be done manually (as shown here) or automatically, using nu.optimize.optimize_with_rolling_horizon().
sets_of_snapshots = 6
p_set = [4_000, 5_000, 700, 800, 4_000]
nu = pypsa.Network(snapshots=range(len(p_set) * sets_of_snapshots))
nu.add("Bus", "bus")
nu.add(
"Generator",
"coal",
bus="bus",
committable=True,
p_min_pu=0.3,
marginal_cost=20,
min_down_time=2,
min_up_time=3,
up_time_before=1,
ramp_limit_up=1,
ramp_limit_down=1,
ramp_limit_start_up=1,
ramp_limit_shut_down=1,
shut_down_cost=150,
start_up_cost=200,
p_nom=10_000,
)
nu.add(
"Generator",
"gas",
bus="bus",
committable=True,
marginal_cost=70,
p_min_pu=0.1,
up_time_before=2,
min_up_time=3,
shut_down_cost=20,
start_up_cost=50,
p_nom=1_000,
)
nu.add("Load", "load", bus="bus", p_set=p_set * sets_of_snapshots)
overlap = 2
for i in range(sets_of_snapshots):
snapshots = nu.snapshots[i * len(p_set) : (i + 1) * len(p_set) + overlap]
nu.optimize(snapshots=snapshots)
pd.concat(
{"Active": nu.generators_t.status.astype(bool), "Output": nu.generators_t.p}, axis=1
)