Meshed AC-DC Networks¶
This example demonstrates how to optimise meshed AC-DC networks in PyPSA. The example has a 3-node AC network coupled via AC-DC converters to a 3-node DC network. There is also a single point-to-point DC connection using the Link component.
import pypsa
n = pypsa.examples.ac_dc_meshed()
n.links.loc["Norwich Converter", "p_nom_extendable"] = False
INFO:pypsa.network.io:Imported network 'AC-DC-Meshed' has buses, carriers, generators, global_constraints, lines, links, loads
line_color = n.lines.bus0.map(n.buses.carrier).map(
lambda ct: "r" if ct == "DC" else "b"
)
n.plot(
line_color=line_color,
link_color="c",
title="AC (blue) - DC (red) - P2P-DC (cyan)",
jitter=0.4,
)
{'nodes': {'Bus': <matplotlib.collections.PatchCollection at 0x725fa31a9a90>},
'branches': {'Link': <matplotlib.collections.LineCollection at 0x725fa31a9be0>,
'Line': <matplotlib.collections.LineCollection at 0x725fc490e990>},
'flows': {}}
We inspect the topology of the network. Therefore, use n.determine_network_topology() and inspect the subnetworks in n.sub_networks.
n.determine_network_topology()
n.sub_networks["n_branches"] = [len(sn.branches()) for sn in n.sub_networks.obj]
n.sub_networks["n_buses"] = [
len(sn.components.buses.static) for sn in n.sub_networks.obj
]
n.sub_networks
| carrier | slack_bus | obj | n_branches | n_buses | |
|---|---|---|---|---|---|
| name | |||||
| 0 | AC | Manchester | <pypsa.SubNetwork object at 0x725fa31... | 3 | 3 |
| 1 | DC | Norwich DC | <pypsa.SubNetwork object at 0x725fa31... | 3 | 3 |
| 2 | AC | Frankfurt | <pypsa.SubNetwork object at 0x725fa31... | 1 | 2 |
| 3 | AC | Norway | <pypsa.SubNetwork object at 0x725fa31... | 0 | 1 |
The network covers 10 time steps. These are given by the snapshots attribute.
n.snapshots
DatetimeIndex(['2015-01-01 00:00:00', '2015-01-01 01:00:00',
'2015-01-01 02:00:00', '2015-01-01 03:00:00',
'2015-01-01 04:00:00', '2015-01-01 05:00:00',
'2015-01-01 06:00:00', '2015-01-01 07:00:00',
'2015-01-01 08:00:00', '2015-01-01 09:00:00'],
dtype='datetime64[ns]', name='snapshot', freq=None)
There are 6 generators in the network, 3 wind and 3 gas. All are attached to AC buses:
n.generators
| bus | control | type | p_nom | p_nom_mod | p_nom_extendable | p_nom_min | p_nom_max | p_nom_set | p_min_pu | ... | min_up_time | min_down_time | up_time_before | down_time_before | ramp_limit_up | ramp_limit_down | ramp_limit_start_up | ramp_limit_shut_down | weight | p_nom_opt | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| name | |||||||||||||||||||||
| Manchester Wind | Manchester | Slack | 80.0 | 0.0 | True | 100.0 | inf | NaN | 0.0 | ... | 0 | 0 | 1 | 0 | NaN | NaN | NaN | NaN | 1.0 | 0.0 | |
| Manchester Gas | Manchester | PQ | 50000.0 | 0.0 | True | 0.0 | inf | NaN | 0.0 | ... | 0 | 0 | 1 | 0 | NaN | NaN | NaN | NaN | 1.0 | 0.0 | |
| Norway Wind | Norway | Slack | 100.0 | 0.0 | True | 100.0 | inf | NaN | 0.0 | ... | 0 | 0 | 1 | 0 | NaN | NaN | NaN | NaN | 1.0 | 0.0 | |
| Norway Gas | Norway | PQ | 20000.0 | 0.0 | True | 0.0 | inf | NaN | 0.0 | ... | 0 | 0 | 1 | 0 | NaN | NaN | NaN | NaN | 1.0 | 0.0 | |
| Frankfurt Wind | Frankfurt | Slack | 110.0 | 0.0 | True | 100.0 | inf | NaN | 0.0 | ... | 0 | 0 | 1 | 0 | NaN | NaN | NaN | NaN | 1.0 | 0.0 | |
| Frankfurt Gas | Frankfurt | PQ | 80000.0 | 0.0 | True | 0.0 | inf | NaN | 0.0 | ... | 0 | 0 | 1 | 0 | NaN | NaN | NaN | NaN | 1.0 | 0.0 |
6 rows × 42 columns
We see that the generators have different capital and marginal costs. All of them have a p_nom_extendable set to True, meaning that capacities can be extended in the optimisation. The wind generators have a per unit limit for each time step, given by the weather potentials at the site.
n.generators_t.p_max_pu.plot()
<Axes: xlabel='snapshot'>
Alright now we know how the network looks like, where the generators and lines are. Now, let's perform a optimization of the operation and capacities.
n.optimize();
/tmp/ipykernel_3368/1450685117.py:1: FutureWarning: The default value of `include_objective_constant` will change from True to False in version 2.0. Set `include_objective_constant` explicitly to suppress this warning. Using False improves LP numerical conditioning by not including the objective constant as a variable. n.optimize(); WARNING:pypsa.consistency:The following lines have zero x, which could break the linear load flow: Index(['2', '3', '4'], dtype='object', name='name')
WARNING:pypsa.consistency:The following lines have zero r, which could break the linear load flow: Index(['0', '1', '5', '6'], 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.07s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 187 primals, 467 duals Objective: -3.47e+06 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-ext-p-lower, Generator-ext-p-upper, Line-ext-s-lower, Line-ext-s-upper, Link-fix-p-lower, Link-fix-p-upper, Link-ext-p-lower, Link-ext-p-upper, Kirchhoff-Voltage-Law were not assigned to the network.
The objective is given by:
n.objective
-3474094.130816234
Why is this number negative? It considers the starting point of the optimisation, thus the existent capacities given by n.generators.p_nom are taken into account.
The real system cost are given by
n.objective + n.objective_constant
18440973.38746291
The optimal capacities are given by p_nom_opt for generators, links and storages and s_nom_opt for lines.
Let's look how the optimal capacities for the generators look like.
n.generators.p_nom_opt.div(1e3).round(2).sort_values()
name Manchester Gas -0.00 Norway Gas -0.00 Frankfurt Gas 0.98 Norway Wind 1.53 Frankfurt Wind 1.67 Manchester Wind 4.09 Name: p_nom_opt, dtype: float64
Their production is again given as a time-series in n.generators_t.
n.generators_t.p.div(1e3).plot.area(stacked=True, lw=0, ylabel="GW")
<Axes: xlabel='snapshot', ylabel='GW'>
What are the locational marginal prices in the network? From the optimisation these are given for each bus and snapshot.
n.buses_t.marginal_price.mean(axis=1).plot(figsize=(8, 3), ylabel="€/MWh")
<Axes: xlabel='snapshot', ylabel='€/MWh'>
We can inspect further quantities as the active power of AC-DC converters and HVDC link.
n.links_t.p0.round(2)
| name | Norwich Converter | Norway Converter | Bremen Converter | DC link |
|---|---|---|---|---|
| snapshot | ||||
| 2015-01-01 00:00:00 | -250.84 | 674.58 | -423.74 | -318.00 |
| 2015-01-01 01:00:00 | 315.07 | -116.73 | -198.34 | -318.00 |
| 2015-01-01 02:00:00 | 350.76 | 581.97 | -932.73 | -318.00 |
| 2015-01-01 03:00:00 | -85.77 | 272.56 | -186.79 | -318.00 |
| 2015-01-01 04:00:00 | 317.37 | -79.75 | -237.62 | -318.00 |
| 2015-01-01 05:00:00 | 386.75 | -494.20 | 107.45 | -318.00 |
| 2015-01-01 06:00:00 | 900.00 | -257.52 | -642.48 | 318.00 |
| 2015-01-01 07:00:00 | 123.68 | 971.92 | -1095.60 | -86.86 |
| 2015-01-01 08:00:00 | 244.72 | 850.88 | -1095.60 | 318.00 |
| 2015-01-01 09:00:00 | 820.02 | -86.85 | -733.17 | -83.68 |
n.lines_t.p0.round(2)
| name | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| snapshot | |||||||
| 2015-01-01 00:00:00 | 79.47 | -38.11 | -52.97 | -303.81 | 370.78 | -202.73 | -534.34 |
| 2015-01-01 01:00:00 | -449.46 | 787.04 | -211.27 | 103.80 | -12.93 | 209.36 | -823.21 |
| 2015-01-01 02:00:00 | -181.27 | 520.56 | -505.91 | -155.15 | 426.82 | -248.68 | 173.90 |
| 2015-01-01 03:00:00 | -45.47 | 234.47 | -34.04 | -119.81 | 152.75 | -232.72 | -743.79 |
| 2015-01-01 04:00:00 | -73.21 | 295.42 | -227.20 | 90.17 | 10.42 | -240.11 | -883.82 |
| 2015-01-01 05:00:00 | -594.50 | 1198.08 | -125.90 | 260.84 | -233.35 | 19.36 | -1030.85 |
| 2015-01-01 06:00:00 | -661.29 | 1378.42 | -632.37 | 267.63 | 10.11 | -53.45 | 319.39 |
| 2015-01-01 07:00:00 | -383.77 | 540.91 | -469.93 | -346.25 | 625.67 | 393.72 | 600.73 |
| 2015-01-01 08:00:00 | -778.28 | 1444.07 | -522.12 | -277.40 | 573.48 | 229.29 | 501.35 |
| 2015-01-01 09:00:00 | -465.58 | 899.57 | -632.37 | 187.65 | 100.80 | 78.62 | -248.57 |
...or the active power injection per bus.
n.buses_t.p.round(2)
| name | London | Norwich | Norwich DC | Manchester | Bremen | Bremen DC | Frankfurt | Norway | Norway DC |
|---|---|---|---|---|---|---|---|---|---|
| snapshot | |||||||||
| 2015-01-01 00:00:00 | 282.20 | -164.62 | -250.84 | -117.58 | -534.34 | -423.74 | 534.34 | 0.0 | 674.58 |
| 2015-01-01 01:00:00 | -658.83 | -577.67 | 315.07 | 1236.50 | -823.21 | -198.34 | 823.21 | 0.0 | -116.73 |
| 2015-01-01 02:00:00 | 67.41 | -769.24 | 350.76 | 701.83 | 173.90 | -932.73 | -173.90 | 0.0 | 581.97 |
| 2015-01-01 03:00:00 | 187.24 | -467.19 | -85.77 | 279.94 | -743.79 | -186.79 | 743.79 | 0.0 | 272.56 |
| 2015-01-01 04:00:00 | 166.90 | -535.53 | 317.37 | 368.63 | -883.82 | -237.62 | 883.82 | 0.0 | -79.75 |
| 2015-01-01 05:00:00 | -613.86 | -1178.72 | 386.75 | 1792.58 | -1030.85 | 107.45 | 1030.85 | 0.0 | -494.20 |
| 2015-01-01 06:00:00 | -607.85 | -1431.87 | 900.00 | 2039.72 | 319.39 | -642.48 | -319.39 | 0.0 | -257.52 |
| 2015-01-01 07:00:00 | -777.48 | -147.19 | 123.68 | 924.68 | 600.73 | -1095.60 | -600.73 | 0.0 | 971.92 |
| 2015-01-01 08:00:00 | -1007.58 | -1214.77 | 244.72 | 2222.35 | 501.35 | -1095.60 | -501.35 | 0.0 | 850.88 |
| 2015-01-01 09:00:00 | -544.19 | -820.95 | 820.02 | 1365.14 | -248.57 | -733.17 | 248.57 | 0.0 | -86.85 |