Note

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

Optimization with Linopy

In PyPSA v0.22, an additional optimization module was instroduced to the package. It is built on Linopy and aims at

  • performance as we know it from the native PyPSA optimization (lopf with pyomo=False)

  • flexibility as we know from the Pyomo implementation

  • usability as know from pandas/xarray

Linopy is a stand-alone package and works similar to Pyomo but without the memory overhead and much faster. In the long-term we are planning to slowly migrate towards the Linopy-based optimization only. In order to facilitate the transission from the native PyPSA optimization (lopf with pyomo=False), the module pypsa.optimization.compat provides functions similar to pypsa.linopt. Have a look of our migration guide (next notebook).

If you don’t have any code to migrate, we recommend to directly use the linopy functions instead.

For additional information on the Linopy package, have a look at the documentation.

Let’s get started

Now, we demonstrate the behaviour of the optimization with linopy. The core functions for the optimization can be called via the Network.optimize accessor. The accessor is used for creating, solving, modifying the optimization problem. Further it supports to run different optimzation formulations and provides helper functions.

At first, we run the ordinary linearized optimal power flow (LOPF). We then extend the formulation by some additional constraints.

[1]:
import pypsa
import pandas as pd
[2]:
n = pypsa.examples.ac_dc_meshed(from_master=True)
WARNING:pypsa.io:Importing network from PyPSA version v0.17.1 while current version is v0.21.1. Read the release notes at https://pypsa.readthedocs.io/en/latest/release_notes.html to prepare your network for import.
INFO:pypsa.io:Imported network ac-dc-meshed.nc has buses, carriers, generators, global_constraints, lines, links, loads

In order to make the network a bit more interesting, we modify its data: We set gas generators to non-extendable,

[3]:
n.generators.loc[n.generators.carrier == "gas", "p_nom_extendable"] = False

… add ramp limits,

[4]:
n.generators.loc[n.generators.carrier == "gas", "ramp_limit_down"] = 0.2
n.generators.loc[n.generators.carrier == "gas", "ramp_limit_up"] = 0.2

… add additional storage units (cyclic and non-cyclic) and fix one state_of_charge,

[5]:
n.add(
    "StorageUnit",
    "su",
    bus="Manchester",
    marginal_cost=10,
    inflow=50,
    p_nom_extendable=True,
    capital_cost=10,
    p_nom=2000,
    efficiency_dispatch=0.5,
    cyclic_state_of_charge=True,
    state_of_charge_initial=1000,
)

n.add(
    "StorageUnit",
    "su2",
    bus="Manchester",
    marginal_cost=10,
    p_nom_extendable=True,
    capital_cost=50,
    p_nom=2000,
    efficiency_dispatch=0.5,
    carrier="gas",
    cyclic_state_of_charge=False,
    state_of_charge_initial=1000,
)

n.storage_units_t.state_of_charge_set.loc[n.snapshots[7], "su"] = 100

…and add an additional store.

[6]:
n.add("Bus", "storebus", carrier="hydro", x=-5, y=55)
n.madd(
    "Link",
    ["battery_power", "battery_discharge"],
    "",
    bus0=["Manchester", "storebus"],
    bus1=["storebus", "Manchester"],
    p_nom=100,
    efficiency=0.9,
    p_nom_extendable=True,
    p_nom_max=1000,
)
n.madd(
    "Store",
    ["store"],
    bus="storebus",
    e_nom=2000,
    e_nom_extendable=True,
    marginal_cost=10,
    capital_cost=10,
    e_nom_max=5000,
    e_initial=100,
    e_cyclic=True,
);

Ordinary Optimization

Per default the optimization based on linopy mimics the well-known n.lopf optimization. We run it by calling the optimize accessor.

[7]:
n.optimize()
WARNING:pypsa.components:The following lines have zero x, which could break the linear load flow:
Index(['2', '3', '4'], dtype='object', name='Line')
WARNING:pypsa.components:The following lines have zero r, which could break the linear load flow:
Index(['0', '1', '5', '6'], dtype='object', name='Line')
WARNING:pypsa.components:The following lines have zero x, which could break the linear load flow:
Index(['2', '3', '4'], dtype='object', name='Line')
WARNING:pypsa.components:The following lines have zero r, which could break the linear load flow:
Index(['0', '1', '5', '6'], dtype='object', name='Line')
INFO:linopy.model: Solve linear problem using Glpk solver
INFO:linopy.io: Writing time: 0.38s
INFO:linopy.model: Optimization successful. Objective value: 1.41e+07
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
 --lp /tmp/linopy-problem-z2kg71n_.lp --output /tmp/linopy-solve-pnk7abnb.sol
Reading problem data from '/tmp/linopy-problem-z2kg71n_.lp'...
748 rows, 300 columns, 1571 non-zeros
4989 lines were read
GLPK Simplex Optimizer 5.0
748 rows, 300 columns, 1571 non-zeros
Preprocessing...
544 rows, 288 columns, 1354 non-zeros
Scaling...
 A: min|aij| =  9.693e-03  max|aij| =  2.000e+00  ratio =  2.063e+02
GM: min|aij| =  5.541e-01  max|aij| =  1.805e+00  ratio =  3.257e+00
EQ: min|aij| =  3.118e-01  max|aij| =  1.000e+00  ratio =  3.207e+00
Constructing initial basis...
Size of triangular part is 543
      0: obj =   3.234550066e+03 inf =   7.824e+04 (101)
    136: obj =   2.915509215e+07 inf =   3.979e-13 (0) 1
*   256: obj =   1.412123759e+07 inf =   7.909e-13 (0) 1
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.9 Mb (962113 bytes)
Writing basic solution to '/tmp/linopy-solve-pnk7abnb.sol'...
[7]:
('ok', 'optimal')

Compared to the native optimization, we now have a model instance attached to our network. It is a container of all variables, constraints and the objective function. You can modify this as much as you please, by directly adding or deleting variables or constraints etc.

[8]:
n.model
[8]:
Linopy model
============

Variables:
----------
Dimensions:                      (Generator-ext: 3, Line-ext: 7, Link-ext: 6,
                                  Store-ext: 1, StorageUnit-ext: 2,
                                  snapshot: 10, Generator: 6, Line: 7, Link: 6,
                                  Store: 1, StorageUnit: 2)
Coordinates:
  * Generator-ext                (Generator-ext) object 'Manchester Wind' ......
  * Line-ext                     (Line-ext) object '0' '1' '2' '3' '4' '5' '6'
  * Link-ext                     (Link-ext) object 'Norwich Converter' ... 'b...
  * Store-ext                    (Store-ext) object 'store'
  * StorageUnit-ext              (StorageUnit-ext) object 'su' 'su2'
  * snapshot                     (snapshot) datetime64[ns] 2015-01-01 ... 201...
  * Generator                    (Generator) object 'Manchester Wind' ... 'Fr...
  * Line                         (Line) object '0' '1' '2' '3' '4' '5' '6'
  * Link                         (Link) object 'Norwich Converter' ... 'batte...
  * Store                        (Store) object 'store'
  * StorageUnit                  (StorageUnit) object 'su' 'su2'
Data variables: (12/15)
    Generator-p_nom              (Generator-ext) int64 0 1 2
    Line-s_nom                   (Line-ext) int64 3 4 5 6 7 8 9
    Link-p_nom                   (Link-ext) int64 10 11 12 13 14 15
    Store-e_nom                  (Store-ext) int64 16
    StorageUnit-p_nom            (StorageUnit-ext) int64 17 18
    Generator-p                  (snapshot, Generator) int64 19 20 21 ... 77 78
    ...                           ...
    StorageUnit-p_dispatch       (snapshot, StorageUnit) int64 219 220 ... 238
    StorageUnit-p_store          (snapshot, StorageUnit) int64 239 240 ... 258
    StorageUnit-state_of_charge  (snapshot, StorageUnit) int64 259 260 ... 278
    StorageUnit-spill            (snapshot, StorageUnit) int64 279 -1 ... 297 -1
    Store-p                      (snapshot, Store) int64 299 300 301 ... 307 308
    objective_constant           int64 309

Constraints:
------------
Dimensions:                                (snapshot: 10, Generator-ext: 3,
                                            Line-ext: 7, Link-ext: 6,
                                            Store-ext: 1, StorageUnit-ext: 2,
                                            Generator-fix: 3,
                                            StorageUnit-state_of_charge_set_i: 1,
                                            Bus: 10, cycles: 2, StorageUnit: 2,
                                            Store: 1)
Coordinates:
  * snapshot                               (snapshot) datetime64[ns] 2015-01-...
  * Generator-ext                          (Generator-ext) object 'Manchester...
  * Line-ext                               (Line-ext) object '0' '1' ... '5' '6'
  * Link-ext                               (Link-ext) object 'Norwich Convert...
  * Store-ext                              (Store-ext) object 'store'
  * StorageUnit-ext                        (StorageUnit-ext) object 'su' 'su2'
  * Generator-fix                          (Generator-fix) object 'Manchester...
  * StorageUnit-state_of_charge_set_i      (StorageUnit-state_of_charge_set_i) object ...
  * Bus                                    (Bus) object 'London' ... 'storebus'
  * cycles                                 (cycles) int64 0 1
  * StorageUnit                            (StorageUnit) object 'su' 'su2'
  * Store                                  (Store) object 'store'
Data variables: (12/34)
    Generator-ext-p_nom-lower              (Generator-ext) int64 0 1 2
    Generator-ext-p_nom-upper              (Generator-ext) int64 -1 -1 -1
    Line-ext-s_nom-lower                   (Line-ext) int64 6 7 8 9 10 11 12
    Line-ext-s_nom-upper                   (Line-ext) int64 -1 -1 -1 -1 -1 -1 -1
    Link-ext-p_nom-lower                   (Link-ext) int64 20 21 22 23 24 25
    Link-ext-p_nom-upper                   (Link-ext) int64 -1 -1 -1 -1 30 31
    ...                                     ...
    StorageUnit-state_of_charge_set        (snapshot, StorageUnit-state_of_charge_set_i) int64 ...
    Bus-nodal_balance                      (Bus, snapshot) int64 622 623 ... 721
    Kirchhoff-Voltage-Law                  (cycles, snapshot) int64 722 ... 741
    StorageUnit-energy-balance             (snapshot, StorageUnit) int64 742 ...
    Store-energy-balance                   (snapshot, Store) int64 762 ... 771
    GlobalConstraint-co2_limit             int64 772

Status:
-------
ok

Modify model, optimize and feed back to network

When you have a fresh network and you just want to create the model instance, run

[9]:
n.optimize.create_model();
WARNING:pypsa.components:The following lines have zero x, which could break the linear load flow:
Index(['2', '3', '4'], dtype='object', name='Line')
WARNING:pypsa.components:The following lines have zero r, which could break the linear load flow:
Index(['0', '1', '5', '6'], dtype='object', name='Line')
WARNING:pypsa.components:The following columns of time-varying attribute v_ang in buses_t have the wrong dtype:
Index(['Norway', 'storebus'], dtype='object', name='Bus')
They are:
Bus
Norway      int64
storebus    int64
dtype: object
but should be:
<class 'float'>

Through the model instance we gain a lot of flexibility. Let’s say for example we want to remove the Kirchhoff Voltage Law constraint, thus convert the model to a transport model. This can be done via

[10]:
n.model.constraints.remove("Kirchhoff-Voltage-Law")

Now, we want to optimize the altered model and feed to solution back to the network. Here again, we use the optimize accessor.

[11]:
n.optimize.solve_model()
INFO:linopy.model: Solve linear problem using Glpk solver
INFO:linopy.io: Writing time: 0.37s
INFO:linopy.model: Optimization successful. Objective value: 1.41e+07
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
 --lp /tmp/linopy-problem-wc6b73xw.lp --output /tmp/linopy-solve-cftkz4bk.sol
Reading problem data from '/tmp/linopy-problem-wc6b73xw.lp'...
728 rows, 300 columns, 1511 non-zeros
4849 lines were read
GLPK Simplex Optimizer 5.0
728 rows, 300 columns, 1511 non-zeros
Preprocessing...
524 rows, 288 columns, 1294 non-zeros
Scaling...
 A: min|aij| =  9.693e-03  max|aij| =  2.000e+00  ratio =  2.063e+02
GM: min|aij| =  5.546e-01  max|aij| =  1.803e+00  ratio =  3.251e+00
EQ: min|aij| =  3.122e-01  max|aij| =  1.000e+00  ratio =  3.203e+00
Constructing initial basis...
Size of triangular part is 523
      0: obj =  -1.290279434e+04 inf =   7.479e+04 (61)
    121: obj =   4.111694291e+07 inf =   1.990e-12 (0)
*   292: obj =   1.412107447e+07 inf =   3.711e-11 (0) 2
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.9 Mb (939361 bytes)
Writing basic solution to '/tmp/linopy-solve-cftkz4bk.sol'...
[11]:
('ok', 'optimal')

Here we followed the recommended way to run altered models:

  1. Create the model instance - n.optimize.create_model()

  2. Modify the model to your needs

  3. Solve and feed back - n.optimize.solve_model()

For compatibility reasons the optimize function, also allows to pass a extra_funcionality argument, as we know it from the lopf function. The above behaviour with use of the extra functionality is obtained through

[12]:
def remove_kvl(n, sns):
    print("KVL removed!")
    n.model.constraints.remove("Kirchhoff-Voltage-Law")


n.optimize(extra_functionality=remove_kvl)
WARNING:pypsa.components:The following lines have zero x, which could break the linear load flow:
Index(['2', '3', '4'], dtype='object', name='Line')
WARNING:pypsa.components:The following lines have zero r, which could break the linear load flow:
Index(['0', '1', '5', '6'], dtype='object', name='Line')
WARNING:pypsa.components:The following columns of time-varying attribute v_ang in buses_t have the wrong dtype:
Index(['Norway', 'storebus'], dtype='object', name='Bus')
They are:
Bus
Norway      int64
storebus    int64
dtype: object
but should be:
<class 'float'>
WARNING:pypsa.components:The following lines have zero x, which could break the linear load flow:
Index(['2', '3', '4'], dtype='object', name='Line')
WARNING:pypsa.components:The following lines have zero r, which could break the linear load flow:
Index(['0', '1', '5', '6'], dtype='object', name='Line')
WARNING:pypsa.components:The following columns of time-varying attribute v_ang in buses_t have the wrong dtype:
Index(['Norway', 'storebus'], dtype='object', name='Bus')
They are:
Bus
Norway      int64
storebus    int64
dtype: object
but should be:
<class 'float'>
INFO:linopy.model: Solve linear problem using Glpk solver
KVL removed!
INFO:linopy.io: Writing time: 0.37s
INFO:linopy.model: Optimization successful. Objective value: 1.41e+07
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
 --lp /tmp/linopy-problem-73rujbvi.lp --output /tmp/linopy-solve-ldh2_ky4.sol
Reading problem data from '/tmp/linopy-problem-73rujbvi.lp'...
728 rows, 300 columns, 1511 non-zeros
4849 lines were read
GLPK Simplex Optimizer 5.0
728 rows, 300 columns, 1511 non-zeros
Preprocessing...
524 rows, 288 columns, 1294 non-zeros
Scaling...
 A: min|aij| =  9.693e-03  max|aij| =  2.000e+00  ratio =  2.063e+02
GM: min|aij| =  5.546e-01  max|aij| =  1.803e+00  ratio =  3.251e+00
EQ: min|aij| =  3.122e-01  max|aij| =  1.000e+00  ratio =  3.203e+00
Constructing initial basis...
Size of triangular part is 523
      0: obj =  -1.290279434e+04 inf =   7.479e+04 (61)
    121: obj =   4.111694291e+07 inf =   1.990e-12 (0)
*   292: obj =   1.412107447e+07 inf =   3.711e-11 (0) 2
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.9 Mb (939361 bytes)
Writing basic solution to '/tmp/linopy-solve-ldh2_ky4.sol'...
[12]:
('ok', 'optimal')

Additional constraints

In the following we examplarily present a set of additional constraints. Note, the dual values of the additional constraints won’t be stored in default data fields in the PyPSA network. But in any case they are stored in the linopy.Model.

Again, we first build the optimization model, add our constraints and finally solve the network. For the first step we use again our accessor optimize to access the function create_model. This returns the linopy model that we can modify.

[13]:
m = n.optimize.create_model()  # the return value is the model, let's use it directly!
WARNING:pypsa.components:The following lines have zero x, which could break the linear load flow:
Index(['2', '3', '4'], dtype='object', name='Line')
WARNING:pypsa.components:The following lines have zero r, which could break the linear load flow:
Index(['0', '1', '5', '6'], dtype='object', name='Line')
WARNING:pypsa.components:The following columns of time-varying attribute v_ang in buses_t have the wrong dtype:
Index(['Norway', 'storebus'], dtype='object', name='Bus')
They are:
Bus
Norway      int64
storebus    int64
dtype: object
but should be:
<class 'float'>
  1. Minimum for state of charge

Assume we want to set a minimum state of charge of 50 MWh in our storage unit. This is done by:

[14]:
sus = m.variables["StorageUnit-state_of_charge"]
m.add_constraints(sus >= 50, name="StorageUnit-minimum_soc")
[14]:
<linopy.Constraint 'StorageUnit-minimum_soc' (snapshot: 10, StorageUnit: 2)>
array([[773, 774],
       [775, 776],
       [777, 778],
       [779, 780],
       [781, 782],
       [783, 784],
       [785, 786],
       [787, 788],
       [789, 790],
       [791, 792]])
Coordinates:
  * snapshot     (snapshot) datetime64[ns] 2015-01-01 ... 2015-01-01T09:00:00
  * StorageUnit  (StorageUnit) object 'su' 'su2'

The return value of the add_constraints function is a array with the labels of the constraints. You can access the constraint now through:

[15]:
m.constraints["StorageUnit-minimum_soc"]
[15]:
<linopy.Constraint 'StorageUnit-minimum_soc' (snapshot: 10, StorageUnit: 2)>
array([[773, 774],
       [775, 776],
       [777, 778],
       [779, 780],
       [781, 782],
       [783, 784],
       [785, 786],
       [787, 788],
       [789, 790],
       [791, 792]])
Coordinates:
  * snapshot     (snapshot) datetime64[ns] 2015-01-01 ... 2015-01-01T09:00:00
  * StorageUnit  (StorageUnit) object 'su' 'su2'

and inspects its attributes like lhs, sign and rhs, e.g.

[16]:
m.constraints["StorageUnit-minimum_soc"].rhs
[16]:
<xarray.DataArray 'StorageUnit-minimum_soc' ()>
array(50)
  1. Fix the ratio between ingoing and outgoing capacity of the Store

The battery in our system is modelled with two links and a store. We should make sure that its charging and discharging capacities, meaning their links, are somehow coupled.

[17]:
capacity = m.variables["Link-p_nom"]
eff = n.links.at["battery_power", "efficiency"]
lhs = capacity["battery_power"] - eff * capacity["battery_discharge"]
m.add_constraints(lhs == 0, name="Link-battery_fix_ratio")
[17]:
<linopy.Constraint 'Link-battery_fix_ratio' ()>
array(793)
  1. Every bus must in total produce the 20% of the total demand

For this, we use the linopy function groupby_sum which follows the pattern from pandas/xarray’s groupby function.

[18]:
total_demand = n.loads_t.p_set.sum().sum()
prod_per_bus = m.variables["Generator-p"].groupby_sum(n.generators.bus).sum("snapshot")
m.add_constraints(prod_per_bus >= total_demand / 5, name="Bus-minimum_production_share")
INFO:linopy.expressions:Converting group pandas.Series to xarray.DataArray
[18]:
<linopy.Constraint 'Bus-minimum_production_share' (bus: 3)>
array([794, 795, 796])
Coordinates:
  * bus      (bus) object 'Frankfurt' 'Manchester' 'Norway'
[19]:
con = prod_per_bus >= total_demand / 5
[20]:
con.lhs
[20]:
<linopy.LinearExpression>
Dimensions:  (bus: 3, _term: 20)
Coordinates:
  * bus      (bus) object 'Frankfurt' 'Manchester' 'Norway'
Dimensions without coordinates: _term
Data:
    coeffs   (bus, _term) int64 1 1 1 1 1 1 1 1 1 1 1 ... 1 1 1 1 1 1 1 1 1 1 1
    vars     (bus, _term) int64 23 29 35 41 47 53 59 65 ... 40 46 52 58 64 70 76

… and now let’s solve the network again.

[21]:
n.optimize.solve_model()
INFO:linopy.model: Solve linear problem using Glpk solver
INFO:linopy.io: Writing time: 0.41s
INFO:linopy.model: Optimization successful. Objective value: 1.43e+07
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
 --lp /tmp/linopy-problem-85dtyvwy.lp --output /tmp/linopy-solve-aeopcsvz.sol
Reading problem data from '/tmp/linopy-problem-85dtyvwy.lp'...
772 rows, 300 columns, 1653 non-zeros
5167 lines were read
GLPK Simplex Optimizer 5.0
772 rows, 300 columns, 1653 non-zeros
Preprocessing...
548 rows, 288 columns, 1416 non-zeros
Scaling...
 A: min|aij| =  9.693e-03  max|aij| =  2.000e+00  ratio =  2.063e+02
GM: min|aij| =  4.602e-01  max|aij| =  2.173e+00  ratio =  4.721e+00
EQ: min|aij| =  2.125e-01  max|aij| =  1.000e+00  ratio =  4.706e+00
Constructing initial basis...
Size of triangular part is 547
      0: obj =   3.234550066e+03 inf =   8.754e+04 (102)
    195: obj =   2.782468542e+07 inf =   2.842e-14 (0) 1
*   340: obj =   1.433711360e+07 inf =   4.356e-11 (0) 1
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.9 Mb (982675 bytes)
Writing basic solution to '/tmp/linopy-solve-aeopcsvz.sol'...
[21]:
('ok', 'optimal')

Analysing the constraints

Let’s see if the system got our own constraints. We look at n.constraints which combines summarises constraints going into the linear problem

[22]:
n.model.constraints
[22]:
linopy.model.Constraints
------------------------

Dimensions:                                (snapshot: 10, Generator-ext: 3,
                                            Line-ext: 7, Link-ext: 6,
                                            Store-ext: 1, StorageUnit-ext: 2,
                                            Generator-fix: 3,
                                            StorageUnit-state_of_charge_set_i: 1,
                                            Bus: 10, cycles: 2, StorageUnit: 2,
                                            Store: 1, bus: 3)
Coordinates: (12/13)
  * snapshot                               (snapshot) datetime64[ns] 2015-01-...
  * Generator-ext                          (Generator-ext) object 'Manchester...
  * Line-ext                               (Line-ext) object '0' '1' ... '5' '6'
  * Link-ext                               (Link-ext) object 'Norwich Convert...
  * Store-ext                              (Store-ext) object 'store'
  * StorageUnit-ext                        (StorageUnit-ext) object 'su' 'su2'
    ...                                     ...
  * StorageUnit-state_of_charge_set_i      (StorageUnit-state_of_charge_set_i) object ...
  * Bus                                    (Bus) object 'London' ... 'storebus'
  * cycles                                 (cycles) int64 0 1
  * StorageUnit                            (StorageUnit) object 'su' 'su2'
  * Store                                  (Store) object 'store'
  * bus                                    (bus) object 'Frankfurt' ... 'Norway'

Labels:
    Generator-ext-p_nom-lower              (Generator-ext) int64 0 1 2
    Generator-ext-p_nom-upper              (Generator-ext) int64 -1 -1 -1
    Line-ext-s_nom-lower                   (Line-ext) int64 6 7 8 9 10 11 12
    Line-ext-s_nom-upper                   (Line-ext) int64 -1 -1 -1 -1 -1 -1 -1
    Link-ext-p_nom-lower                   (Link-ext) int64 20 21 22 23 24 25
    Link-ext-p_nom-upper                   (Link-ext) int64 -1 -1 -1 -1 30 31
    ...                                     ...
    StorageUnit-energy-balance             (snapshot, StorageUnit) int64 742 ...
    Store-energy-balance                   (snapshot, Store) int64 762 ... 771
    GlobalConstraint-co2_limit             int64 772
    StorageUnit-minimum_soc                (snapshot, StorageUnit) int64 773 ...
    Link-battery_fix_ratio                 int64 793
    Bus-minimum_production_share           (bus) int64 794 795 796

Left-hand-side coefficients:
    Generator-ext-p_nom-lower              (Generator-ext, Generator-ext-p_nom-lower_term) float64 ...
    Generator-ext-p_nom-upper              (Generator-ext, Generator-ext-p_nom-upper_term) float64 ...
    Line-ext-s_nom-lower                   (Line-ext, Line-ext-s_nom-lower_term) float64 ...
    Line-ext-s_nom-upper                   (Line-ext, Line-ext-s_nom-upper_term) float64 ...
    Link-ext-p_nom-lower                   (Link-ext, Link-ext-p_nom-lower_term) float64 ...
    Link-ext-p_nom-upper                   (Link-ext, Link-ext-p_nom-upper_term) float64 ...
    ...                                     ...
    StorageUnit-energy-balance             (snapshot, StorageUnit, StorageUnit-energy-balance_term) float64 ...
    Store-energy-balance                   (snapshot, Store, Store-energy-balance_term) float64 ...
    GlobalConstraint-co2_limit             (GlobalConstraint-co2_limit_term) float64 ...
    StorageUnit-minimum_soc                (snapshot, StorageUnit, StorageUnit-minimum_soc_term) float64 ...
    Link-battery_fix_ratio                 (Link-battery_fix_ratio_term) float64 ...
    Bus-minimum_production_share           (bus, Bus-minimum_production_share_term) float64 ...

Left-hand-side variables:
    Generator-ext-p_nom-lower              (Generator-ext, Generator-ext-p_nom-lower_term) int64 ...
    Generator-ext-p_nom-upper              (Generator-ext, Generator-ext-p_nom-upper_term) int64 ...
    Line-ext-s_nom-lower                   (Line-ext, Line-ext-s_nom-lower_term) int64 ...
    Line-ext-s_nom-upper                   (Line-ext, Line-ext-s_nom-upper_term) int64 ...
    Link-ext-p_nom-lower                   (Link-ext, Link-ext-p_nom-lower_term) int64 ...
    Link-ext-p_nom-upper                   (Link-ext, Link-ext-p_nom-upper_term) int64 ...
    ...                                     ...
    StorageUnit-energy-balance             (snapshot, StorageUnit, StorageUnit-energy-balance_term) int64 ...
    Store-energy-balance                   (snapshot, Store, Store-energy-balance_term) int64 ...
    GlobalConstraint-co2_limit             (GlobalConstraint-co2_limit_term) int64 ...
    StorageUnit-minimum_soc                (snapshot, StorageUnit, StorageUnit-minimum_soc_term) int64 ...
    Link-battery_fix_ratio                 (Link-battery_fix_ratio_term) int64 ...
    Bus-minimum_production_share           (bus, Bus-minimum_production_share_term) int64 ...

Signs:
    Generator-ext-p_nom-lower              <U2 '>='
    Generator-ext-p_nom-upper              <U2 '<='
    Line-ext-s_nom-lower                   <U2 '>='
    Line-ext-s_nom-upper                   <U2 '<='
    Link-ext-p_nom-lower                   <U2 '>='
    Link-ext-p_nom-upper                   <U2 '<='
    ...                                     ...
    StorageUnit-energy-balance             <U1 '='
    Store-energy-balance                   <U1 '='
    GlobalConstraint-co2_limit             <U2 '<='
    StorageUnit-minimum_soc                <U2 '>='
    Link-battery_fix_ratio                 <U1 '='
    Bus-minimum_production_share           <U2 '>='

Right-hand-side constants:
    Generator-ext-p_nom-lower              (Generator-ext) float64 100.0 ... ...
    Generator-ext-p_nom-upper              (Generator-ext) float64 inf inf inf
    Line-ext-s_nom-lower                   (Line-ext) float64 0.0 0.0 ... 0.0
    Line-ext-s_nom-upper                   (Line-ext) float64 inf inf ... inf
    Link-ext-p_nom-lower                   (Link-ext) float64 0.0 0.0 ... 0.0
    Link-ext-p_nom-upper                   (Link-ext) float64 inf inf ... 1e+03
    ...                                     ...
    StorageUnit-energy-balance             (snapshot, StorageUnit) float64 -5...
    Store-energy-balance                   (Store, snapshot) float64 -0.0 ......
    GlobalConstraint-co2_limit             float64 760.0
    StorageUnit-minimum_soc                int64 50
    Link-battery_fix_ratio                 int64 0
    Bus-minimum_production_share           float64 6.51e+03

The last three entries show our constraints. Let’s check whether out two custom constraint are fulfilled:

[23]:
n.links.loc[["battery_power", "battery_discharge"], ["p_nom_opt"]]
[23]:
p_nom_opt
Link
battery_power 900.0
battery_discharge 1000.0
[24]:
n.storage_units_t.state_of_charge
[24]:
StorageUnit su su2
snapshot
2015-01-01 00:00:00 1835.74 1000.000
2015-01-01 01:00:00 1326.16 1000.000
2015-01-01 02:00:00 1376.16 1000.000
2015-01-01 03:00:00 1426.16 1000.000
2015-01-01 04:00:00 1986.06 1000.000
2015-01-01 05:00:00 50.00 50.000
2015-01-01 06:00:00 50.00 50.000
2015-01-01 07:00:00 100.00 156.727
2015-01-01 08:00:00 50.00 156.727
2015-01-01 09:00:00 50.00 50.000
[25]:
n.generators_t.p.groupby(n.generators.bus, axis=1).sum().sum() / n.loads_t.p.sum().sum()
[25]:
bus
Frankfurt     0.200000
Manchester    0.200000
Norway        0.637047
dtype: float64

Looks good! Now, let’s see which dual values were parsed. Therefore we have a look into n.model.dual

[26]:
n.model.dual
[26]:
<xarray.Dataset>
Dimensions:                                (Generator-ext: 3, Line-ext: 7,
                                            Link-ext: 6, Store-ext: 1,
                                            StorageUnit-ext: 2, snapshot: 10,
                                            Generator-fix: 3,
                                            StorageUnit-state_of_charge_set_i: 1,
                                            Bus: 10, cycles: 2, StorageUnit: 2,
                                            Store: 1, bus: 3)
Coordinates: (12/13)
  * Generator-ext                          (Generator-ext) object 'Manchester...
  * Line-ext                               (Line-ext) object '0' '1' ... '5' '6'
  * Link-ext                               (Link-ext) object 'Norwich Convert...
  * Store-ext                              (Store-ext) object 'store'
  * StorageUnit-ext                        (StorageUnit-ext) object 'su' 'su2'
  * snapshot                               (snapshot) datetime64[ns] 2015-01-...
    ...                                     ...
  * StorageUnit-state_of_charge_set_i      (StorageUnit-state_of_charge_set_i) object ...
  * Bus                                    (Bus) object 'London' ... 'storebus'
  * cycles                                 (cycles) int64 0 1
  * StorageUnit                            (StorageUnit) object 'su' 'su2'
  * Store                                  (Store) object 'store'
  * bus                                    (bus) object 'Frankfurt' ... 'Norway'
Data variables: (12/37)
    Generator-ext-p_nom-lower              (Generator-ext) float64 0.0 0.0 0.0
    Generator-ext-p_nom-upper              (Generator-ext) float64 nan nan nan
    Line-ext-s_nom-lower                   (Line-ext) float64 0.0 0.0 ... 0.0
    Line-ext-s_nom-upper                   (Line-ext) float64 nan nan ... nan
    Link-ext-p_nom-lower                   (Link-ext) float64 0.0 0.0 ... 0.0
    Link-ext-p_nom-upper                   (Link-ext) float64 nan nan ... -527.1
    ...                                     ...
    StorageUnit-energy-balance             (snapshot, StorageUnit) float64 29...
    Store-energy-balance                   (snapshot, Store) float64 521.7 .....
    GlobalConstraint-co2_limit             float64 -950.4
    StorageUnit-minimum_soc                (snapshot, StorageUnit) float64 0....
    Link-battery_fix_ratio                 float64 -567.9
    Bus-minimum_production_share           (bus) float64 35.2 45.42 0.0
[27]:
n.model.dual["StorageUnit-minimum_soc"]
[27]:
<xarray.DataArray 'StorageUnit-minimum_soc' (snapshot: 10, StorageUnit: 2)>
array([[  0.     ,   0.     ],
       [  0.     ,   0.     ],
       [  0.     ,   0.     ],
       [  0.     ,   0.     ],
       [  0.     ,   0.     ],
       [  0.     ,   0.     ],
       [157.423  ,   4.44444],
       [  0.     ,   0.     ],
       [  0.     ,   0.     ],
       [  5.55556,  67.8486 ]])
Coordinates:
  * snapshot     (snapshot) datetime64[ns] 2015-01-01 ... 2015-01-01T09:00:00
  * StorageUnit  (StorageUnit) object 'su' 'su2'
[28]:
n.model.dual["Link-battery_fix_ratio"]
[28]:
<xarray.DataArray 'Link-battery_fix_ratio' ()>
array(-567.89)
[29]:
n.model.dual["Bus-minimum_production_share"]
[29]:
<xarray.DataArray 'Bus-minimum_production_share' (bus: 3)>
array([35.2007, 45.4185,  0.    ])
Coordinates:
  * bus      (bus) object 'Frankfurt' 'Manchester' 'Norway'

These are the basic functionalities of the optimize accessor. There are many more functions like abstract optimziation formulations (security constraint optimization, iterative transmission expansion optimization, etc.) or helper functions (fixing optimized capacities, adding load shedding). Try them out if you want!

[30]:
print("\n".join([func for func in n.optimize.__dir__() if not func.startswith("_")]))
create_model
solve_model
assign_solution
assign_duals
post_processing
optimize_transmission_expansion_iteratively
optimize_security_constrained
fix_optimal_capacities
fix_optimal_dispatch
add_load_shedding