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(log_to_console=False)
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.02s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 32 primals, 36 duals Objective: 3.56e+05 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-com-p-lower, Generator-com-p-upper, Generator-com-transition-start-up, Generator-com-transition-shut-down, Generator-com-status-min_up_time_must_stay_up were not assigned to the network.
('ok', 'optimal')
nu.generators_t.status
| name | coal | gas |
|---|---|---|
| snapshot | ||
| 0 | 1.0 | 0.0 |
| 1 | 1.0 | 0.0 |
| 2 | 1.0 | 0.0 |
| 3 | 0.0 | 1.0 |
nu.generators_t.p
| name | coal | gas |
|---|---|---|
| snapshot | ||
| 0 | 4000.0 | 0.0 |
| 1 | 6000.0 | 0.0 |
| 2 | 5000.0 | 0.0 |
| 3 | 0.0 | 800.0 |
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(log_to_console=False)
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.02s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 32 primals, 39 duals Objective: 3.06e+05 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-com-p-lower, Generator-com-p-upper, Generator-com-transition-start-up, Generator-com-transition-shut-down, Generator-com-up-time, Generator-com-status-min_up_time_must_stay_up were not assigned to the network.
('ok', 'optimal')
nu.generators_t.status
| name | coal | gas |
|---|---|---|
| snapshot | ||
| 0 | 1.0 | 1.0 |
| 1 | 0.0 | 1.0 |
| 2 | 1.0 | 1.0 |
| 3 | 1.0 | 0.0 |
nu.objective
306150.0
nu.generators_t.p
| name | coal | gas |
|---|---|---|
| snapshot | ||
| 0 | 3900.0 | 100.0 |
| 1 | 0.0 | 800.0 |
| 2 | 4900.0 | 100.0 |
| 3 | 3000.0 | -0.0 |
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(log_to_console=False)
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
WARNING:pypsa.consistency:The following committable generators were both up and down before the simulation: Index(['coal'], dtype='object', name='name'). This could cause an infeasibility.
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.02s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 32 primals, 40 duals Objective: 4.86e+05 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-com-p-lower, Generator-com-p-upper, Generator-com-transition-start-up, Generator-com-transition-shut-down, Generator-com-down-time, Generator-com-status-min_up_time_must_stay_up, Generator-com-status-min_down_time_must_stay_up were not assigned to the network.
('ok', 'optimal')
nu.objective
486000.0
nu.generators_t.status
| name | coal | gas |
|---|---|---|
| snapshot | ||
| 0 | 0.0 | 1.0 |
| 1 | 0.0 | 1.0 |
| 2 | 1.0 | 0.0 |
| 3 | 1.0 | 0.0 |
nu.generators_t.p
| name | coal | gas |
|---|---|---|
| snapshot | ||
| 0 | 0.0 | 3000.0 |
| 1 | 0.0 | 800.0 |
| 2 | 3000.0 | 0.0 |
| 3 | 8000.0 | 0.0 |
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(log_to_console=False)
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.02s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 32 primals, 39 duals Objective: 4.91e+05 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-com-p-lower, Generator-com-p-upper, Generator-com-transition-start-up, Generator-com-transition-shut-down, Generator-com-down-time, Generator-com-status-min_up_time_must_stay_up were not assigned to the network.
('ok', 'optimal')
nu.objective
491025.0
nu.generators_t.status
| name | coal | gas |
|---|---|---|
| snapshot | ||
| 0 | 0.0 | 1.0 |
| 1 | 0.0 | 1.0 |
| 2 | 1.0 | 0.0 |
| 3 | 1.0 | 0.0 |
nu.generators_t.p
| name | coal | gas |
|---|---|---|
| snapshot | ||
| 0 | 0.0 | 3000.0 |
| 1 | 0.0 | 800.0 |
| 2 | 3000.0 | 0.0 |
| 3 | 8000.0 | 0.0 |
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(log_to_console=False)
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.01s
WARNING:linopy.constants:Optimization potentially failed: Status: warning Termination condition: infeasible Solution: 0 primals, 0 duals Objective: nan Solver model: available Solver message: Infeasible
('warning', 'infeasible')
nu.generators_t.p
| name |
|---|
| snapshot |
| 0 |
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
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(log_to_console=False)
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.02s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 13 primals, 41 duals Objective: 1.68e+06 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Generator-ext-p-lower, Generator-ext-p-upper, Generator-ext-p-ramp_limit_up, Generator-ext-p-ramp_limit_down were not assigned to the network.
('ok', 'optimal')
nu.generators.p_nom_opt
name coal 5000.0 gas 4000.0 Name: p_nom_opt, dtype: float64
nu.generators_t.p
| name | coal | gas |
|---|---|---|
| snapshot | ||
| 0 | 4000.0 | -0.0 |
| 1 | 4500.0 | 2500.0 |
| 2 | 5000.0 | 2000.0 |
| 3 | 5000.0 | 2000.0 |
| 4 | 4000.0 | 3000.0 |
| 5 | 3000.0 | -0.0 |
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(log_to_console=False)
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.03s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 35 primals, 61 duals Objective: 1.15e+06 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Generator-com-p-lower, Generator-com-p-upper, Generator-com-transition-start-up, Generator-com-transition-shut-down, Generator-com-status-min_up_time_must_stay_up, Generator-com-p-ramp_limit_up, Generator-com-p-ramp_limit_down were not assigned to the network.
('ok', 'optimal')
nu.generators_t.p
| name | coal | gas |
|---|---|---|
| snapshot | ||
| 0 | 0.0 | 0.0 |
| 1 | 0.0 | 200.0 |
| 2 | 1000.0 | 6000.0 |
| 3 | 3000.0 | 4000.0 |
| 4 | 4000.0 | 3000.0 |
| 5 | 1500.0 | 500.0 |
| 6 | 0.0 | 0.0 |
nu.generators_t.status
| name | coal | gas |
|---|---|---|
| snapshot | ||
| 0 | 0.0 | 0.0 |
| 1 | 0.0 | 0.0 |
| 2 | 1.0 | 0.0 |
| 3 | 1.0 | 0.0 |
| 4 | 1.0 | 0.0 |
| 5 | 1.0 | 0.0 |
| 6 | 0.0 | 0.0 |
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, log_to_console=False)
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.03s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 56 primals, 96 duals Objective: 5.55e+05 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-com-p-lower, Generator-com-p-upper, Generator-com-transition-start-up, Generator-com-transition-shut-down, Generator-com-up-time, Generator-com-down-time, Generator-com-status-min_up_time_must_stay_up, Generator-com-p-ramp_limit_up, Generator-com-p-ramp_limit_down were not assigned to the network.
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
WARNING:pypsa.consistency:The following sub_networks have carriers which are not defined: Index(['0'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.03s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 56 primals, 97 duals Objective: 5.50e+05 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-com-p-lower, Generator-com-p-upper, Generator-com-transition-start-up, Generator-com-transition-shut-down, Generator-com-up-time, Generator-com-down-time, Generator-com-status-min_up_time_must_stay_up, Generator-com-p-ramp_limit_up, Generator-com-p-ramp_limit_down were not assigned to the network.
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
WARNING:pypsa.consistency:The following sub_networks have carriers which are not defined: Index(['0'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.03s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 56 primals, 97 duals Objective: 5.50e+05 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-com-p-lower, Generator-com-p-upper, Generator-com-transition-start-up, Generator-com-transition-shut-down, Generator-com-up-time, Generator-com-down-time, Generator-com-status-min_up_time_must_stay_up, Generator-com-p-ramp_limit_up, Generator-com-p-ramp_limit_down were not assigned to the network.
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
WARNING:pypsa.consistency:The following sub_networks have carriers which are not defined: Index(['0'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.03s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 56 primals, 97 duals Objective: 5.50e+05 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-com-p-lower, Generator-com-p-upper, Generator-com-transition-start-up, Generator-com-transition-shut-down, Generator-com-up-time, Generator-com-down-time, Generator-com-status-min_up_time_must_stay_up, Generator-com-p-ramp_limit_up, Generator-com-p-ramp_limit_down were not assigned to the network.
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
WARNING:pypsa.consistency:The following sub_networks have carriers which are not defined: Index(['0'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.03s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 56 primals, 97 duals Objective: 5.50e+05 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-com-p-lower, Generator-com-p-upper, Generator-com-transition-start-up, Generator-com-transition-shut-down, Generator-com-up-time, Generator-com-down-time, Generator-com-status-min_up_time_must_stay_up, Generator-com-p-ramp_limit_up, Generator-com-p-ramp_limit_down were not assigned to the network.
WARNING:pypsa.consistency:The following buses have carriers which are not defined: Index(['bus'], dtype='object', name='name')
WARNING:pypsa.consistency:The following sub_networks have carriers which are not defined: Index(['0'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.03s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 40 primals, 69 duals Objective: 3.70e+05 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-com-p-lower, Generator-com-p-upper, Generator-com-transition-start-up, Generator-com-transition-shut-down, Generator-com-up-time, Generator-com-down-time, Generator-com-status-min_up_time_must_stay_up, Generator-com-p-ramp_limit_up, Generator-com-p-ramp_limit_down were not assigned to the network.
pd.concat(
{"Active": nu.generators_t.status.astype(bool), "Output": nu.generators_t.p}, axis=1
)
| Active | Output | |||
|---|---|---|---|---|
| name | coal | gas | coal | gas |
| snapshot | ||||
| 0 | True | True | 3900.0 | 100.0 |
| 1 | True | True | 4900.0 | 100.0 |
| 2 | False | True | 0.0 | 700.0 |
| 3 | False | True | 0.0 | 800.0 |
| 4 | True | False | 4000.0 | 0.0 |
| 5 | True | False | 4000.0 | 0.0 |
| 6 | True | True | 4900.0 | 100.0 |
| 7 | False | True | 0.0 | 700.0 |
| 8 | False | True | 0.0 | 800.0 |
| 9 | True | False | 4000.0 | 0.0 |
| 10 | True | False | 4000.0 | 0.0 |
| 11 | True | True | 4900.0 | 100.0 |
| 12 | False | True | 0.0 | 700.0 |
| 13 | False | True | 0.0 | 800.0 |
| 14 | True | False | 4000.0 | 0.0 |
| 15 | True | False | 4000.0 | 0.0 |
| 16 | True | True | 4900.0 | 100.0 |
| 17 | False | True | 0.0 | 700.0 |
| 18 | False | True | 0.0 | 800.0 |
| 19 | True | False | 4000.0 | 0.0 |
| 20 | True | False | 4000.0 | 0.0 |
| 21 | True | True | 4900.0 | 100.0 |
| 22 | False | True | 0.0 | 700.0 |
| 23 | False | True | 0.0 | 800.0 |
| 24 | True | False | 4000.0 | 0.0 |
| 25 | True | False | 4000.0 | 0.0 |
| 26 | True | False | 5000.0 | 0.0 |
| 27 | False | True | 0.0 | 700.0 |
| 28 | False | True | 0.0 | 800.0 |
| 29 | True | True | 3900.0 | 100.0 |