Note

You can download this example as a Jupyter notebook or start it in interactive mode.

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.

To enable unit commitment on a generator, set its attribute committable = True.

[1]:
import pypsa
import pandas as pd

Minimum part load demonstration

In final hour load goes below part-load limit of coal gen (30%), forcing gas to commit.

[2]:
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,
    marginal_cost=70,
    p_min_pu=0.1,
    p_nom=1000,
)

nu.add("Load", "load", bus="bus", p_set=[4000, 6000, 5000, 800])
[3]:
nu.lopf()
WARNING:pypsa.linopf:Unit commitment is not yet completely implemented for optimising without pyomo. Thus minimum up time, minimum down time, start up costs, shut down costs will be ignored.
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.06s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
WARNING:pypsa.linopt:Shadow prices of MILP couldn't be parsed
INFO:pypsa.linopf:Optimization successful. Objective value: 3.56e+05
[3]:
('ok', 'optimal')
[4]:
nu.generators_t.status
[4]:
Generator coal gas
snapshot
0 1.0 0.0
1 1.0 0.0
2 1.0 0.0
3 0.0 1.0
[5]:
nu.generators_t.p
[5]:
Generator 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 demonstration

Gas has minimum up time, forcing it to be online longer

[6]:
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,
    marginal_cost=70,
    p_min_pu=0.1,
    up_time_before=0,
    min_up_time=3,
    p_nom=1000,
)

nu.add("Load", "load", bus="bus", p_set=[4000, 800, 5000, 3000])
[7]:
nu.lopf()
WARNING:pypsa.linopf:Unit commitment is not yet completely implemented for optimising without pyomo. Thus minimum up time, minimum down time, start up costs, shut down costs will be ignored.
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.06s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
WARNING:pypsa.linopt:Shadow prices of MILP couldn't be parsed
INFO:pypsa.linopf:Optimization successful. Objective value: 2.96e+05
[7]:
('ok', 'optimal')
[8]:
nu.generators_t.status
[8]:
Generator coal gas
snapshot
0 1.0 0.0
1 0.0 1.0
2 1.0 0.0
3 1.0 0.0
[9]:
nu.generators_t.p
[9]:
Generator coal gas
snapshot
0 4000.0 0.0
1 0.0 800.0
2 5000.0 0.0
3 3000.0 0.0

Minimum down time demonstration

Coal has a minimum down time, forcing it to go off longer.

[10]:
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=10000,
)

nu.add(
    "Generator",
    "gas",
    bus="bus",
    committable=True,
    marginal_cost=70,
    p_min_pu=0.1,
    p_nom=4000,
)

nu.add("Load", "load", bus="bus", p_set=[3000, 800, 3000, 8000])
[11]:
nu.lopf()
WARNING:pypsa.linopf:Unit commitment is not yet completely implemented for optimising without pyomo. Thus minimum up time, minimum down time, start up costs, shut down costs will be ignored.
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.06s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
WARNING:pypsa.linopt:Shadow prices of MILP couldn't be parsed
INFO:pypsa.linopf:Optimization successful. Objective value: 3.36e+05
[11]:
('ok', 'optimal')
[12]:
nu.objective
[12]:
336000.0
[13]:
nu.generators_t.status
[13]:
Generator coal gas
snapshot
0 1.0 0.0
1 0.0 1.0
2 1.0 0.0
3 1.0 0.0
[14]:
nu.generators_t.p
[14]:
Generator coal gas
snapshot
0 3000.0 0.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 associated costs for shutting down, etc

[15]:
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=5000,
    p_nom=10000,
)

nu.add(
    "Generator",
    "gas",
    bus="bus",
    committable=True,
    marginal_cost=70,
    p_min_pu=0.1,
    shut_down_cost=25,
    p_nom=4000,
)

nu.add("Load", "load", bus="bus", p_set=[3000, 800, 3000, 8000])
[16]:
nu.lopf(nu.snapshots)
WARNING:pypsa.linopf:Unit commitment is not yet completely implemented for optimising without pyomo. Thus minimum up time, minimum down time, start up costs, shut down costs will be ignored.
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.06s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
WARNING:pypsa.linopt:Shadow prices of MILP couldn't be parsed
INFO:pypsa.linopf:Optimization successful. Objective value: 3.36e+05
[16]:
('ok', 'optimal')
[17]:
nu.objective
[17]:
336000.0
[18]:
nu.generators_t.status
[18]:
Generator coal gas
snapshot
0 1.0 0.0
1 0.0 1.0
2 1.0 0.0
3 1.0 0.0
[19]:
nu.generators_t.p
[19]:
Generator coal gas
snapshot
0 3000.0 0.0
1 0.0 800.0
2 3000.0 0.0
3 8000.0 0.0

Ramp rate limits

[20]:
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=10000,
)

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])
[21]:
nu.lopf()
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.08s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
INFO:pypsa.linopf:Optimization successful. Objective value: 9.50e+05
[21]:
('ok', 'optimal')
[22]:
nu.generators_t.p
[22]:
Generator coal gas
snapshot
0 4000.0 0.0
1 5000.0 2000.0
2 6000.0 1000.0
3 7000.0 0.0
4 5000.0 2000.0
5 3000.0 0.0
[23]:
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])
[24]:
nu.lopf(nu.snapshots)
INFO:pypsa.linopf:Prepare linear problem
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopt.py:474: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
  ref_dict.pnl[attr].loc[df.index, df.columns] = df
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopt.py:474: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
  ref_dict.pnl[attr].loc[df.index, df.columns] = df
INFO:pypsa.linopf:Total preparation time: 0.08s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
INFO:pypsa.linopf:Optimization successful. Objective value: 1.68e+06
[24]:
('ok', 'optimal')
[25]:
nu.generators.p_nom_opt
[25]:
Generator
coal    5000.0
gas     4000.0
Name: p_nom_opt, dtype: float64
[26]:
nu.generators_t.p
[26]:
Generator 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
[27]:
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=10000.0,
)

nu.add("Generator", "gas", bus="bus", marginal_cost=70, p_nom=10000)

nu.add("Load", "load", bus="bus", p_set=[0.0, 200.0, 7000, 7000, 7000, 2000, 0])
WARNING:pypsa.components:Generator has no attribute initial_status, ignoring this passed value.
[28]:
nu.lopf()
WARNING:pypsa.linopf:Unit commitment is not yet completely implemented for optimising without pyomo. Thus minimum up time, minimum down time, start up costs, shut down costs will be ignored.
INFO:pypsa.linopf:Prepare linear problem
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopt.py:474: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
  ref_dict.pnl[attr].loc[df.index, df.columns] = df
INFO:pypsa.linopf:Total preparation time: 0.1s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
WARNING:pypsa.linopt:Shadow prices of MILP couldn't be parsed
INFO:pypsa.linopf:Optimization successful. Objective value: 1.15e+06
[28]:
('ok', 'optimal')
[29]:
nu.generators_t.p
[29]:
Generator 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
[30]:
nu.generators_t.status
[30]:
Generator coal
snapshot
0 0.0
1 0.0
2 1.0
3 1.0
4 1.0
5 1.0
6 0.0
[31]:
nu.generators.loc["coal"]
[31]:
attribute
bus                         bus
control                   Slack
type
p_nom                   10000.0
p_nom_extendable          False
p_nom_min                   0.0
p_nom_max                   inf
p_min_pu                   0.05
p_max_pu                    1.0
p_set                       0.0
q_set                       0.0
sign                        1.0
carrier
marginal_cost              20.0
build_year                    0
lifetime                    inf
capital_cost                0.0
efficiency                  1.0
committable                True
start_up_cost               0.0
shut_down_cost              0.0
min_up_time                   0
min_down_time                 0
up_time_before                1
down_time_before              0
ramp_limit_up               0.2
ramp_limit_down            0.25
ramp_limit_start_up         0.1
ramp_limit_shut_down       0.15
p_nom_opt               10000.0
Name: coal, dtype: object

Rolling horizon example

This example solves sequentially in batches

[32]:
sets_of_snapshots = 6
p_set = [4000, 5000, 700, 800, 4000]

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=10000,
)

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=1000,
)

nu.add("Load", "load", bus="bus", p_set=p_set * sets_of_snapshots)
[33]:
overlap = 2
for i in range(sets_of_snapshots):
    nu.lopf(nu.snapshots[i * len(p_set) : (i + 1) * len(p_set) + overlap], pyomo=False)
WARNING:pypsa.linopf:Unit commitment is not yet completely implemented for optimising without pyomo. Thus minimum up time, minimum down time, start up costs, shut down costs will be ignored.
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.1s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
WARNING:pypsa.linopt:Shadow prices of MILP couldn't be parsed
INFO:pypsa.linopf:Optimization successful. Objective value: 5.45e+05
WARNING:pypsa.linopf:Unit commitment is not yet completely implemented for optimising without pyomo. Thus minimum up time, minimum down time, start up costs, shut down costs will be ignored.
INFO:pypsa.linopf:Prepare linear problem
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopf.py:338: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  rhs.loc[sns[0]] += (limit_up - limit_start) * status_prev_fix
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopf.py:380: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  rhs.loc[sns[0]] += -limit_shut * status_prev_fix
INFO:pypsa.linopf:Total preparation time: 0.1s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
WARNING:pypsa.linopt:Shadow prices of MILP couldn't be parsed
INFO:pypsa.linopf:Optimization successful. Objective value: 5.45e+05
WARNING:pypsa.linopf:Unit commitment is not yet completely implemented for optimising without pyomo. Thus minimum up time, minimum down time, start up costs, shut down costs will be ignored.
INFO:pypsa.linopf:Prepare linear problem
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopf.py:338: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  rhs.loc[sns[0]] += (limit_up - limit_start) * status_prev_fix
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopf.py:380: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  rhs.loc[sns[0]] += -limit_shut * status_prev_fix
INFO:pypsa.linopf:Total preparation time: 0.1s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
WARNING:pypsa.linopt:Shadow prices of MILP couldn't be parsed
INFO:pypsa.linopf:Optimization successful. Objective value: 5.45e+05
WARNING:pypsa.linopf:Unit commitment is not yet completely implemented for optimising without pyomo. Thus minimum up time, minimum down time, start up costs, shut down costs will be ignored.
INFO:pypsa.linopf:Prepare linear problem
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopf.py:338: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  rhs.loc[sns[0]] += (limit_up - limit_start) * status_prev_fix
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopf.py:380: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  rhs.loc[sns[0]] += -limit_shut * status_prev_fix
INFO:pypsa.linopf:Total preparation time: 0.1s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
WARNING:pypsa.linopt:Shadow prices of MILP couldn't be parsed
INFO:pypsa.linopf:Optimization successful. Objective value: 5.45e+05
WARNING:pypsa.linopf:Unit commitment is not yet completely implemented for optimising without pyomo. Thus minimum up time, minimum down time, start up costs, shut down costs will be ignored.
INFO:pypsa.linopf:Prepare linear problem
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopf.py:338: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  rhs.loc[sns[0]] += (limit_up - limit_start) * status_prev_fix
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopf.py:380: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  rhs.loc[sns[0]] += -limit_shut * status_prev_fix
INFO:pypsa.linopf:Total preparation time: 0.1s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
WARNING:pypsa.linopt:Shadow prices of MILP couldn't be parsed
INFO:pypsa.linopf:Optimization successful. Objective value: 5.45e+05
WARNING:pypsa.linopf:Unit commitment is not yet completely implemented for optimising without pyomo. Thus minimum up time, minimum down time, start up costs, shut down costs will be ignored.
INFO:pypsa.linopf:Prepare linear problem
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopf.py:338: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  rhs.loc[sns[0]] += (limit_up - limit_start) * status_prev_fix
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.1/lib/python3.10/site-packages/pypsa/linopf.py:380: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  rhs.loc[sns[0]] += -limit_shut * status_prev_fix
INFO:pypsa.linopf:Total preparation time: 0.1s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
WARNING:pypsa.linopt:Shadow prices of MILP couldn't be parsed
INFO:pypsa.linopf:Optimization successful. Objective value: 3.65e+05
[34]:
pd.concat(
    {"Active": nu.generators_t.status.astype(bool), "Output": nu.generators_t.p}, axis=1
)
[34]:
Active Output
Generator coal gas coal gas
snapshot
0 True False 4000.0 0.0
1 True False 5000.0 0.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 False 5000.0 0.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 False 5000.0 0.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 False 5000.0 0.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 False 5000.0 0.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 False 4000.0 0.0