Note

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

Optimization without pyomo

In this example we demonstrate the behaviour of the Linear Optimal Power Flow (LOPF) calculation without using pyomo. This requires to set pyomo to False in the lopf function. Then, the communication with the solvers happens via in-house functions which leads to a much faster solving process.

[1]:
import pypsa
import pandas as pd
import os
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [1], line 1
----> 1 import pypsa
      2 import pandas as pd
      3 import os

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/pypsa/__init__.py:10
      1 # -*- coding: utf-8 -*-
      4 """
      5 Python for Power Systems Analysis (PyPSA)
      6
      7 Grid calculation library.
      8 """
---> 10 from pypsa import (
     11     components,
     12     contingency,
     13     descriptors,
     14     examples,
     15     geo,
     16     io,
     17     linopf,
     18     linopt,
     19     networkclustering,
     20     opf,
     21     opt,
     22     optimization,
     23     pf,
     24     plot,
     25 )
     26 from pypsa.components import Network, SubNetwork
     28 __version__ = "0.21.2"

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/pypsa/components.py:50
     37 from pypsa.io import (
     38     export_to_csv_folder,
     39     export_to_hdf5,
   (...)
     47     import_series_from_dataframe,
     48 )
     49 from pypsa.opf import network_lopf, network_opf
---> 50 from pypsa.optimization.optimize import OptimizationAccessor
     51 from pypsa.pf import (
     52     calculate_B_H,
     53     calculate_dependent_values,
   (...)
     62     sub_network_pf,
     63 )
     64 from pypsa.plot import iplot, plot

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/pypsa/optimization/__init__.py:7
      1 #!/usr/bin/env python3
      2 # -*- coding: utf-8 -*-
      3 """
      4 Build optimisation problems from PyPSA networks with Linopy.
      5 """
----> 7 from pypsa.optimization import abstract, constraints, optimize, variables
      8 from pypsa.optimization.optimize import create_model

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/pypsa/optimization/constraints.py:9
      6 import logging
      8 import pandas as pd
----> 9 from linopy.expressions import LinearExpression, merge
     10 from numpy import arange, cumsum, inf, nan, roll
     11 from scipy import sparse

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/linopy/__init__.py:9
      1 #!/usr/bin/env python3
      2 # -*- coding: utf-8 -*-
      3 """
      4 Created on Wed Mar 10 11:03:06 2021.
      5
      6 @author: fabulous
      7 """
----> 9 from linopy import model, remote
     10 from linopy.expressions import merge
     11 from linopy.io import read_netcdf

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/linopy/model.py:22
     20 from linopy import solvers
     21 from linopy.common import best_int, replace_by_map
---> 22 from linopy.constraints import (
     23     AnonymousConstraint,
     24     AnonymousScalarConstraint,
     25     Constraints,
     26 )
     27 from linopy.eval import Expr
     28 from linopy.expressions import LinearExpression, ScalarLinearExpression

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/linopy/constraints.py:21
     18 from scipy.sparse import coo_matrix
     19 from xarray import DataArray, Dataset
---> 21 from linopy import expressions, variables
     22 from linopy.common import (
     23     _merge_inplace,
     24     has_assigned_model,
   (...)
     27     replace_by_map,
     28 )
     31 class Constraint(DataArray):

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/linopy/expressions.py:23
     20 from xarray.core.dataarray import DataArrayCoordinates
     21 from xarray.core.groupby import _maybe_reorder, peek_at
---> 23 from linopy import constraints, variables
     24 from linopy.common import as_dataarray
     27 def exprwrap(method, *default_args, **new_default_kwargs):

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/linopy/variables.py:398
    393     roll = varwrap(DataArray.roll)
    395     rolling = varwrap(DataArray.rolling)
--> 398 @dataclass(repr=False)
    399 class Variables:
    400     """
    401     A variables container used for storing multiple variable arrays.
    402     """
    404     labels: Dataset = Dataset()

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/dataclasses.py:1211, in dataclass.<locals>.wrap(cls)
   1210 def wrap(cls):
-> 1211     return _process_class(cls, init, repr, eq, order, unsafe_hash,
   1212                           frozen, match_args, kw_only, slots,
   1213                           weakref_slot)

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/dataclasses.py:959, in _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot)
    956         kw_only = True
    957     else:
    958         # Otherwise it's a field of some type.
--> 959         cls_fields.append(_get_field(cls, name, type, kw_only))
    961 for f in cls_fields:
    962     fields[f.name] = f

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/dataclasses.py:816, in _get_field(cls, a_name, a_type, default_kw_only)
    812 # For real fields, disallow mutable defaults.  Use unhashable as a proxy
    813 # indicator for mutability.  Read the __hash__ attribute from the class,
    814 # not the instance.
    815 if f._field_type is _FIELD and f.default.__class__.__hash__ is None:
--> 816     raise ValueError(f'mutable default {type(f.default)} for field '
    817                      f'{f.name} is not allowed: use default_factory')
    819 return f

ValueError: mutable default <class 'xarray.core.dataset.Dataset'> for field labels is not allowed: use default_factory
[2]:
n = pypsa.examples.ac_dc_meshed(from_master=True)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [2], line 1
----> 1 n = pypsa.examples.ac_dc_meshed(from_master=True)

NameError: name 'pypsa' is not defined

Modify the network a bit: We set gas generators to non-extendable

[3]:
n.generators.loc[n.generators.carrier == "gas", "p_nom_extendable"] = False
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [3], line 1
----> 1 n.generators.loc[n.generators.carrier == "gas", "p_nom_extendable"] = False

NameError: name 'n' is not defined

Add ramp limit

[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
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [4], line 1
----> 1 n.generators.loc[n.generators.carrier == "gas", "ramp_limit_down"] = 0.2
      2 n.generators.loc[n.generators.carrier == "gas", "ramp_limit_up"] = 0.2

NameError: name 'n' is not defined

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
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [5], line 1
----> 1 n.add(
      2     "StorageUnit",
      3     "su",
      4     bus="Manchester",
      5     marginal_cost=10,
      6     inflow=50,
      7     p_nom_extendable=True,
      8     capital_cost=10,
      9     p_nom=2000,
     10     efficiency_dispatch=0.5,
     11     cyclic_state_of_charge=True,
     12     state_of_charge_initial=1000,
     13 )
     15 n.add(
     16     "StorageUnit",
     17     "su2",
   (...)
     26     state_of_charge_initial=1000,
     27 )
     29 n.storage_units_t.state_of_charge_set.loc[n.snapshots[7], "su"] = 100

NameError: name 'n' is not defined

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,
);
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [6], line 1
----> 1 n.add("Bus", "storebus", carrier="hydro", x=-5, y=55)
      2 n.madd(
      3     "Link",
      4     ["battery_power", "battery_discharge"],
   (...)
     11     p_nom_max=1000,
     12 )
     13 n.madd(
     14     "Store",
     15     ["store"],
   (...)
     23     e_cyclic=True,
     24 );

NameError: name 'n' is not defined

Extra functionalities:

[7]:
from pypsa.linopt import get_var, linexpr, join_exprs, define_constraints

One of the most important functions is linexpr which take one or more tuples of coefficient and variable pairs which should go into the left hand side (lhs) of the constraint.

  1. Add mimimum for state_of_charge

[8]:
def minimal_state_of_charge(n, snapshots):
    vars_soc = get_var(n, "StorageUnit", "state_of_charge")
    lhs = linexpr((1, vars_soc))
    define_constraints(n, lhs, ">", 50, "StorageUnit", "soc_lower_bound")
  1. Fix the ratio between ingoing and outgoing capacity of the Store

[9]:
def fix_link_cap_ratio(n, snapshots):
    vars_link = get_var(n, "Link", "p_nom")
    eff = n.links.at["battery_power", "efficiency"]
    lhs = linexpr(
        (1, vars_link["battery_power"]), (-eff, vars_link["battery_discharge"])
    )
    define_constraints(n, lhs, "=", 0, "battery_discharge", attr="fixratio")
  1. Every bus must in total produce the 20% of the total demand

This requires the function pypsa.linopt.join_exprs which sums up arrays of linear expressions

[10]:
def fix_bus_production(n, snapshots):
    total_demand = n.loads_t.p_set.sum().sum()
    prod_per_bus = (
        linexpr((1, get_var(n, "Generator", "p")))
        .groupby(n.generators.bus, axis=1)
        .apply(join_exprs)
    )
    define_constraints(
        n, prod_per_bus, ">=", total_demand / 5, "Bus", "production_share"
    )

Combine them …

[11]:
def extra_functionalities(n, snapshots):
    minimal_state_of_charge(n, snapshots)
    fix_link_cap_ratio(n, snapshots)
    fix_bus_production(n, snapshots)

…and run the lopf with pyomo=False

[12]:
n.lopf(
    pyomo=False,
    extra_functionality=extra_functionalities,
    keep_shadowprices=["Bus", "battery_discharge", "StorageUnit"],
)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [12], line 1
----> 1 n.lopf(
      2     pyomo=False,
      3     extra_functionality=extra_functionalities,
      4     keep_shadowprices=["Bus", "battery_discharge", "StorageUnit"],
      5 )

NameError: name 'n' is not defined

The keep_shadowprices argument in the lopf now decides which shadow prices (SP) should be retrieved. It can either be set to True, then all SP are kept. It also can be a list of names of the constraints. Therefore the name argument in define_constraints is necessary, in our case ‘battery_discharge’, ‘StorageUnit’ and ‘Bus’.

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

[13]:
n.constraints
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [13], line 1
----> 1 n.constraints

NameError: name 'n' is not defined

The last three entries show our constraints. As ‘soc_lower_bound’ is time-dependent, the pnl value is set to True.

Let’s check whether out two custom constraint are fulfilled:

[14]:
n.links.loc[["battery_power", "battery_discharge"], ["p_nom_opt"]]
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [14], line 1
----> 1 n.links.loc[["battery_power", "battery_discharge"], ["p_nom_opt"]]

NameError: name 'n' is not defined
[15]:
n.storage_units_t.state_of_charge
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [15], line 1
----> 1 n.storage_units_t.state_of_charge

NameError: name 'n' is not defined
[16]:
n.generators_t.p.groupby(n.generators.bus, axis=1).sum().sum() / n.loads_t.p.sum().sum()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [16], line 1
----> 1 n.generators_t.p.groupby(n.generators.bus, axis=1).sum().sum() / n.loads_t.p.sum().sum()

NameError: name 'n' is not defined

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

[17]:
n.dualvalues
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [17], line 1
----> 1 n.dualvalues

NameError: name 'n' is not defined

Again we see the last two entries reflect our constraints (the values in the columns play only a minor role). Having a look what the values are:

[18]:
from pypsa.linopt import get_dual
[19]:
get_dual(n, "StorageUnit", "soc_lower_bound")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [19], line 1
----> 1 get_dual(n, "StorageUnit", "soc_lower_bound")

NameError: name 'n' is not defined
[20]:
get_dual(n, "battery_discharge", "fixratio")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [20], line 1
----> 1 get_dual(n, "battery_discharge", "fixratio")

NameError: name 'n' is not defined
[21]:
get_dual(n, "Bus", "production_share")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [21], line 1
----> 1 get_dual(n, "Bus", "production_share")

NameError: name 'n' is not defined

Side note

Some of the predefined constraints are stored in components itself like n.lines_t.mu_upper or n.buses_t.marginal_price, this is the case if their are designated columns are spots for those. All other dual are under the hook stored in n.duals