Maps (Static)¶
PyPSA has a built-in method to map parameters and results in a static plot using n.plot(). For interactive maps, please see Interactive Maps
Input data¶
import geopandas as gpd
import pypsa
n = pypsa.examples.scigrid_de()
INFO:pypsa.network.io:Imported network 'SciGrid-DE' has buses, carriers, generators, lines, loads, storage_units, transformers
Preparation¶
For illustrative purposes and a simpler postprocessing workflow, we cluster the network based on federal states. For a more detailed guide on clustering, please go to Network Clustering. Within the scope of this guide, you can ignore the following blocks.
n.calculate_dependent_values()
n.lines = n.lines.reindex(columns=n.components["Line"]["defaults"].index[1:])
n.lines["type"] = "Al/St 240/40 2-bundle 220.0"
n.buses = n.buses.reindex(columns=n.components["Bus"]["defaults"].index[1:])
n.buses["frequency"] = 50
url = "https://media.githubusercontent.com/media/wmgeolab/geoBoundaries/9469f09592ced973a3448cf66b6100b741b64c0d/releaseData/gbOpen/DEU/ADM1/geoBoundaries-DEU-ADM1-all.zip"
states = gpd.read_file(url, layer="geoBoundaries-DEU-ADM1_simplified")
states["shapeName"] = states["shapeName"].apply(
lambda x: x.encode("latin1").decode("utf-8")
) # fix encoding issue
bus_coords = gpd.GeoDataFrame(
geometry=gpd.points_from_xy(n.buses.x, n.buses.y, crs=4326), index=n.buses.index
)
busmap = bus_coords.to_crs(3035).sjoin_nearest(states.to_crs(3035), how="left").shapeISO
nc = n.cluster.spatial.cluster_by_busmap(busmap)
By default, calling n.plot() will render all network components based on the x and y coordinates defined in n.buses. This allows us to get a first visual overview on the two networks, before and after clustering.
n.plot()
{'nodes': {'Bus': <matplotlib.collections.PatchCollection at 0x7e6e71fe17f0>},
'branches': {'Line': <matplotlib.collections.LineCollection at 0x7e6e71fe2510>,
'Transformer': <matplotlib.collections.LineCollection at 0x7e6e71d04f50>},
'flows': {}}
nc.plot()
{'nodes': {'Bus': <matplotlib.collections.PatchCollection at 0x7e6e71e2efd0>},
'branches': {'Line': <matplotlib.collections.LineCollection at 0x7e6e71e2f110>},
'flows': {}}
Retrieving Results Data¶
To map result to parameters of n.plot(), we first solve the network and then use n.statistics() to calculate relevant metrics.
# We reduce logging output for clarity
import logging
logging.getLogger("pypsa").setLevel(logging.ERROR)
logging.getLogger("linopy").setLevel(logging.ERROR)
nc.optimize()
/tmp/ipykernel_9221/319203369.py:7: 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. nc.optimize()
Writing constraints.: 0%| | 0/16 [00:00<?, ?it/s]
Writing constraints.: 100%|██████████| 16/16 [00:00<00:00, 171.92it/s]
Writing continuous variables.: 0%| | 0/5 [00:00<?, ?it/s]
Writing continuous variables.: 100%|██████████| 5/5 [00:00<00:00, 303.00it/s]
('ok', 'optimal')
From above we learned that bus_size accepts parameters of type float, dict, and pd.Series. When passing a multi-index pd.Series, its values will be mapped to pie chart slices.
eb = (
nc.statistics.energy_balance(
groupby=["bus", "carrier"],
components=["Generator", "Load", "StorageUnit"],
)
.groupby(["bus", "carrier"])
.sum()
)
We also extract branch results, e.g., line and link flows in this example.
line_flow = nc.lines_t.p0.sum(axis=0)
link_flow = nc.links_t.p0.sum(axis=0)
Note that for the pie slices to be plotted and colored correctly, passing a multi-index pd.Series requires all carrier colors to exist. Colors can be specified by their hex code representation or from the list of matplotlib names. In n.statistics.energy_balance() load is also included, so we also need to include a color for the load carrier.
colors = {
"Multiple": "pink",
"AC": "black",
"Brown Coal": "saddlebrown",
"Gas": "darkorange",
"Geothermal": "firebrick",
"Hard Coal": "darkslategray",
"Nuclear": "mediumorchid",
"Oil": "peru",
"Other": "dimgray",
"Pumped Hydro": "cornflowerblue",
"Run of River": "royalblue",
"Solar": "gold",
"Storage Hydro": "navy",
"Waste": "olive",
"Wind Offshore": "teal",
"Wind Onshore": "turquoise",
}
nc.carriers.color = nc.carriers.index.map(colors)
As the carriers for loads are missing, we need to add them, manually.
nc.carriers.loc["", "color"] = "darkred"
nc.carriers.loc["-", "color"] = "darkred"
Balances and flow¶
We first choose a suitable projection for the plot by importing cartopy and matplotlib. Commonly used projections include ccrs.Mercator() or ccrs.EqualEarth(). We can also pass geomap_color=True to get default colorings for land and water bodies. Note that this requires cartopy to be installed.
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 7), subplot_kw={"projection": ccrs.EqualEarth()})
nc.plot(ax=ax, geomap_color=True)
{'nodes': {'Bus': <matplotlib.collections.PatchCollection at 0x7e6e71cec690>},
'branches': {'Line': <matplotlib.collections.LineCollection at 0x7e6e71cec7d0>},
'flows': {}}
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/envs/latest/lib/python3.13/site-packages/cartopy/io/__init__.py:242: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/50m_physical/ne_50m_land.zip
warnings.warn(f'Downloading: {url}', DownloadWarning)
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/envs/latest/lib/python3.13/site-packages/cartopy/io/__init__.py:242: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/50m_physical/ne_50m_ocean.zip
warnings.warn(f'Downloading: {url}', DownloadWarning)
Next, let's pass the results to n.explore(). Setting bus_split_circle=True maps negative values to the bottom half and positive values to the positive half. If set to False, bottom half circles are not used and negative values will automatically be omitted. Note that the area of bus_size scales proportionally to the values passed. To get a useful map, we need to scale the values, according to personal preference.
bus_size_factor = 3e6
branch_width_factor = 2e4
branch_flow_factor = 5e4
nc.plot(
ax=ax,
bus_size=eb / bus_size_factor,
bus_split_circle=True,
line_width=line_flow / branch_width_factor,
link_width=link_flow / branch_width_factor,
line_flow=line_flow / branch_flow_factor,
)
fig
Legends¶
We import additional functions from pypsa.plot to add legends to our figure. It makes sense to select values that are close to values represented in the figure, e.g. the maximum, minimum values and something in between.
from pypsa.plot import add_legend_lines, add_legend_patches, add_legend_semicircles
print(f"Max flow: {line_flow.abs().max()}")
print(f"Min flow: {line_flow.abs().min()}")
Max flow: 183089.05512238853 Min flow: 62.45143031258283
Based on the values, we choose the following. Not that the values need to be scaled with the same factors determined before.
add_legend_lines(
ax,
sizes=[branch / branch_width_factor for branch in [150000, 100000, 10000]],
labels=["150", "100", "10"],
legend_kw={"loc": "lower right", "frameon": False, "title": "Line flow (GWh)"},
)
fig
Now we apply the same process for bus sizes.
print(f"Max gen./load: {eb.groupby('bus').sum().abs().max()}")
print(f"Max gen./load: {eb.groupby('bus').sum().abs().min()}")
Max gen./load: 122882.44354000001 Max gen./load: 62.45143000000007
add_legend_semicircles(
ax,
sizes=[bus / bus_size_factor for bus in [120000, -120000]],
labels=["+120 GWh", "-120 GWh"],
legend_kw={
"loc": "upper left",
"frameon": False,
"bbox_to_anchor": (0.02, 0.98),
},
)
fig
/tmp/ipykernel_9221/932776199.py:1: UserWarning: When combining n.plot() with other plots on a geographical axis, ensure n.plot() is called first or the final axis extent is set initially (ax.set_extent(boundaries, crs=crs)) for consistent legend semicircle sizes. add_legend_semicircles(
To add legend entries for each bus carrier, we use add_legend_patches.
add_legend_patches(
ax,
colors=list(nc.carriers.color), # colors
labels=list(nc.carriers.index), # labels
legend_kw={
"loc": "lower center",
"bbox_to_anchor": (0.5, -0.25), # For offsetting
"ncol": 4,
"frameon": False,
},
)
fig
Export¶
We can export the figure to any desired format, i.e., .png, .jpg, .pdf etc.
fig.savefig("static-plot.jpg", dpi=150)