Note
You can download this example as a Jupyter notebook or start it in interactive mode.
Optimization with Linopy - Migrate Extra Functionalities#
The extra funcionalities for the native pypsa optimization code are mostly using the function of the pypsa.linopt
model. Here we show how you can recycle large parts of your code by using the compatibility functions from the pypsa.optimization.compat
module.
These are
define_variables
define_constraints
get_var
linexpr
You might want to use them if you have extra_functionalities
written for the native optimization code. However, expect some hick-ups, as some operations might behave differently.
Let’s import pypsa and the compat functions:
[1]:
import pypsa
from pypsa.optimization.compat import define_constraints, get_var, join_exprs, linexpr
We load the same network from the previous section into memory:
[2]:
n = pypsa.examples.ac_dc_meshed(from_master=True)
n.generators.loc[n.generators.carrier == "gas", "p_nom_extendable"] = False
n.generators.loc[n.generators.carrier == "gas", "ramp_limit_down"] = 0.2
n.generators.loc[n.generators.carrier == "gas", "ramp_limit_up"] = 0.2
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
n.add("Bus", "storebus", carrier="hydro", x=-5, y=55)
n.add(
"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.add(
"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,
);
WARNING:pypsa.io:Importing network from PyPSA version v0.17.1 while current version is v0.31.0. 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
And define the extra functionalities as we defined them for the native code in here
[3]:
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")
With the compat functions, this will work as expected. Let’s go on to the next one.
[4]:
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.loc["battery_power"]), (-eff, vars_link.loc["battery_discharge"])
)
define_constraints(n, lhs, "=", 0, "battery_discharge", attr="fixratio")
This function as well should not make any problems. Let’s go on.
[5]:
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"
)
Here, we come into difficult terrain. The groupby function won’t work since the linexpr
functions returns some sort of a xarray object (a LinearExpression
object, derived from xarray.Dataset
).
Instead, we have to rewrite parts:
remove the
axis
argumentuse a xaray DataArray as argument
explicitly sum over all snapshots afterwards. This has nothing to do with groupby but with the fact that we want to limit to total production.
[6]:
def fix_bus_production_new(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.to_xarray())
.sum()
.sum()
)
define_constraints(
n, prod_per_bus, ">=", total_demand / 5, "Bus", "production_share"
)
[7]:
def extra_functionalities(n, snapshots):
minimal_state_of_charge(n, snapshots)
fix_link_cap_ratio(n, snapshots)
fix_bus_production_new(n, snapshots)
[8]:
n.optimize(
extra_functionality=extra_functionalities,
)
WARNING:pypsa.consistency:The following buses have carriers which are not defined:
Index(['London', 'Norwich', 'Norwich DC', 'Manchester', 'Bremen', 'Bremen DC',
'Frankfurt', 'Norway', 'Norway DC', 'storebus'],
dtype='object', name='Bus')
WARNING:pypsa.consistency:The following lines have carriers which are not defined:
Index(['0', '1', '2', '3', '4', '5', '6'], dtype='object', name='Line')
WARNING:pypsa.consistency:The following lines have zero x, which could break the linear load flow:
Index(['2', '3', '4'], dtype='object', name='Line')
WARNING:pypsa.consistency:The following lines have zero r, which could break the linear load flow:
Index(['0', '1', '5', '6'], dtype='object', name='Line')
WARNING:pypsa.consistency:The following stores have carriers which are not defined:
Index(['store'], dtype='object', name='Store')
WARNING:pypsa.consistency:The following links have carriers which are not defined:
Index(['Norwich Converter', 'Norway Converter', 'Bremen Converter', 'DC link',
'battery_power', 'battery_discharge'],
dtype='object', name='Link')
WARNING:pypsa.consistency:The following buses have carriers which are not defined:
Index(['London', 'Norwich', 'Norwich DC', 'Manchester', 'Bremen', 'Bremen DC',
'Frankfurt', 'Norway', 'Norway DC', 'storebus'],
dtype='object', name='Bus')
WARNING:pypsa.consistency:The following lines have carriers which are not defined:
Index(['0', '1', '2', '3', '4', '5', '6'], dtype='object', name='Line')
WARNING:pypsa.consistency:The following lines have zero x, which could break the linear load flow:
Index(['2', '3', '4'], dtype='object', name='Line')
WARNING:pypsa.consistency:The following lines have zero r, which could break the linear load flow:
Index(['0', '1', '5', '6'], dtype='object', name='Line')
WARNING:pypsa.consistency:The following stores have carriers which are not defined:
Index(['store'], dtype='object', name='Store')
WARNING:pypsa.consistency:The following links have carriers which are not defined:
Index(['Norwich Converter', 'Norway Converter', 'Bremen Converter', 'DC link',
'battery_power', 'battery_discharge'],
dtype='object', name='Link')
/tmp/ipykernel_2878/4161170827.py:2: DeprecatedWarning: get_var is deprecated as of 0.29 and will be removed in 1.0. Use native linopy syntax instead.
vars_soc = get_var(n, "StorageUnit", "state_of_charge")
/tmp/ipykernel_2878/4161170827.py:3: DeprecatedWarning: linexpr is deprecated as of 0.29 and will be removed in 1.0. Use native linopy syntax instead.
lhs = linexpr((1, vars_soc))
/tmp/ipykernel_2878/4161170827.py:4: DeprecatedWarning: define_constraints is deprecated as of 0.29 and will be removed in 1.0. Use native linopy syntax instead.
define_constraints(n, lhs, ">", 50, "StorageUnit", "soc_lower_bound")
/tmp/ipykernel_2878/340402375.py:2: DeprecatedWarning: get_var is deprecated as of 0.29 and will be removed in 1.0. Use native linopy syntax instead.
vars_link = get_var(n, "Link", "p_nom")
/tmp/ipykernel_2878/340402375.py:4: DeprecatedWarning: linexpr is deprecated as of 0.29 and will be removed in 1.0. Use native linopy syntax instead.
lhs = linexpr(
/tmp/ipykernel_2878/340402375.py:7: DeprecatedWarning: define_constraints is deprecated as of 0.29 and will be removed in 1.0. Use native linopy syntax instead.
define_constraints(n, lhs, "=", 0, "battery_discharge", attr="fixratio")
/tmp/ipykernel_2878/3751128348.py:4: DeprecatedWarning: get_var is deprecated as of 0.29 and will be removed in 1.0. Use native linopy syntax instead.
linexpr((1, get_var(n, "Generator", "p")))
/tmp/ipykernel_2878/3751128348.py:4: DeprecatedWarning: linexpr is deprecated as of 0.29 and will be removed in 1.0. Use native linopy syntax instead.
linexpr((1, get_var(n, "Generator", "p")))
/tmp/ipykernel_2878/3751128348.py:9: DeprecatedWarning: define_constraints is deprecated as of 0.29 and will be removed in 1.0. Use native linopy syntax instead.
define_constraints(
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.io: Writing time: 0.21s
INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 300 primals, 770 duals
Objective: 1.42e+07
Solver model: available
Solver message: optimal
Running HiGHS 1.8.1 (git hash: 4a7f24a): Copyright (c) 2024 HiGHS under MIT licence terms
Coefficient ranges:
Matrix [1e-02, 2e+00]
Cost [9e-03, 3e+03]
Bound [5e+01, 8e+05]
RHS [9e-01, 8e+04]
Presolving model
525 rows, 296 cols, 1384 nonzeros 0s
429 rows, 200 cols, 1470 nonzeros 0s
401 rows, 199 cols, 1426 nonzeros 0s
Presolve : Reductions: rows 401(-369); columns 199(-101); elements 1426(-227)
Solving the presolved LP
Using EKK dual simplex solver - serial
Iteration Objective Infeasibilities num(sum)
0 -1.3357258588e+05 Pr: 124(106427); Du: 0(3.31433e-11) 0s
179 1.4159599683e+07 Pr: 0(0); Du: 0(1.7053e-13) 0s
Solving the original LP from the solution after postsolve
Model name : linopy-problem-vuyo1hn2
Model status : Optimal
Simplex iterations: 179
Objective value : 1.4159599683e+07
Relative P-D gap : 6.5773227738e-16
HiGHS run time : 0.01
Writing the solution to /tmp/linopy-solve-mirkgt5r.sol
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-fix-p-ramp_limit_up, Generator-fix-p-ramp_limit_down, Line-ext-s-lower, Line-ext-s-upper, Link-ext-p-lower, Link-ext-p-upper, Store-ext-e-lower, Store-ext-e-upper, StorageUnit-ext-p_dispatch-lower, StorageUnit-ext-p_dispatch-upper, StorageUnit-ext-p_store-lower, StorageUnit-ext-p_store-upper, StorageUnit-ext-state_of_charge-lower, StorageUnit-ext-state_of_charge-upper, StorageUnit-state_of_charge_set, Kirchhoff-Voltage-Law, StorageUnit-energy_balance, Store-energy_balance, StorageUnit-soc_lower_bound were not assigned to the network.
[8]:
('ok', 'optimal')