{
"cells": [
{
"cell_type": "markdown",
"id": "ed13a6b8-1d5b-4ce0-9cd4-b81bac7b07dd",
"metadata": {},
"source": [
"# Plotting"
]
},
{
"cell_type": "markdown",
"id": "fcfe6476",
"metadata": {},
"source": [
"This notebook explains the main plotting functions available for a `WindFarmNetwork` object.\n",
"\n",
"These plotting methods are natively optimized to return high-performance, lightweight, and modern vector **SVG representations** (`svgplot`/`svgpplot`) by default inside notebooks. However, they also seamlessly switch to **Matplotlib backends** (`gplot`/`pplot`) when you supply a custom target axes (e.g. `ax=...`), giving you the best of both worlds: clean direct display and advanced layout customizability."
]
},
{
"cell_type": "markdown",
"id": "3bad0d73",
"metadata": {},
"source": [
"## Import required modules"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "b0f61785-6be5-4a0f-8282-5c17f4cc28ad",
"metadata": {},
"outputs": [],
"source": [
"from optiwindnet.api import WindFarmNetwork, load_repository"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "2f7f3fb1",
"metadata": {},
"outputs": [],
"source": [
"# Display figures as SVG in Jupyter notebooks\n",
"%config InlineBackend.figure_formats = ['svg']"
]
},
{
"cell_type": "markdown",
"id": "cb7f8713-8170-4ada-a2a9-849fbd88771f",
"metadata": {},
"source": [
"## Create a sample network"
]
},
{
"cell_type": "markdown",
"id": "3f8789d4-5f85-4e50-a7b6-dd9b57f0e4a2",
"metadata": {},
"source": [
"> Note: `load_repository()` loads multiple locations from a given path. If no path is given, it loads the built-in locations (more details in [Load Location Data Repositories](a03_load_repositories.ipynb))."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "4e702047",
"metadata": {},
"outputs": [],
"source": [
"locations = load_repository()\n",
"wfn = WindFarmNetwork(L=locations.anholt, cables=[(2, 1500.0), (5, 1800.0)])"
]
},
{
"cell_type": "markdown",
"id": "5fab2b53",
"metadata": {},
"source": [
"## Plot location geometry (`L`):\n",
"\n",
"`L` contains only the **raw location data**, such as turbine and substation coordinates, borders, and obstacles, **without any links or connections** between nodes.\n",
"\n",
"As a result, `wfn.plot_location()` will appear as an **edgeless graph**, showing only the borders and node elements without network links/edges. Turbines are plotted as *circles*, and substations as *squares*.\n",
"\n",
"This plot is useful to check if the location data was correctly loaded into the `WindFarmNetwork()` instance, before running the optimization."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "f88ccb3c",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wfn.plot_location()"
]
},
{
"cell_type": "markdown",
"id": "e01d4da1",
"metadata": {},
"source": [
">**Tip**: In a notebook, just put `wfn` as the last line of a cell for plotting G.\n",
">\n",
">* Before optimization (no `G` yet), it renders the location geometry `L`.\n",
">* If `G` exists (e.g. after a call to `optimize()`), it automatically renders `G`."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "eeb09ee1",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wfn"
]
},
{
"cell_type": "markdown",
"id": "eec3ae91-1043-48cf-96ed-dfd3304916bb",
"metadata": {},
"source": [
"## Optimize"
]
},
{
"cell_type": "markdown",
"id": "13554c48",
"metadata": {},
"source": [
"To plot and visualize the optimized network, an optimization needs to be performed first. Here, the default router (`Heuristics-Esau_Williams`) is used."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "f888963b",
"metadata": {},
"outputs": [],
"source": [
"res = wfn.optimize()"
]
},
{
"cell_type": "markdown",
"id": "b5a529f4-edb9-48ac-8aa0-0ce46f3002e0",
"metadata": {},
"source": [
"## Plot the Optimized Network Graph"
]
},
{
"cell_type": "markdown",
"id": "9a43d824",
"metadata": {},
"source": [
"The optimized network graph (G) represents the optimized network with actual routes.\n",
"\n",
"Accordingly, `wfn.plot()` displays the routes for selected links in the **optimized network**. The detoured routes are displayed via dashed lines. Detours are the routes created to resolve crossing feeders.\n",
"\n",
"Turbines that belong to the same subtree are assigned the same color. Each cable type is represented with a distinct line thickness (which gets thicker as the cable capacity increases)."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "883efe11",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wfn.plot()"
]
},
{
"cell_type": "markdown",
"id": "8814596b",
"metadata": {},
"source": [
"> ! Reminder: In a notebook, we can plot the optimized network simply by calling `wfn`:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "29dcc789",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wfn"
]
},
{
"cell_type": "markdown",
"id": "69938619-68ab-46a9-a819-e8254e2fe092",
"metadata": {},
"source": [
"## Plot in a pre-existing axes"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "46890d30-d033-4da5-a513-1f25b7e91766",
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"plt.rcParams['svg.fonttype'] = 'none'"
]
},
{
"cell_type": "markdown",
"id": "9cc236bf-61b1-43b7-90d0-a866e32eb1f2",
"metadata": {},
"source": [
"It is also possible to provide a pre-existing `plt.Axes` object within a Matplotlib figure:\n",
"\n",
"> **Tip:** When you pass the `ax` keyword argument to any `wfn` plotting method, it automatically switches under the hood from the native SVG generator to the Matplotlib backend (`gplot`/`pplot`). This allows you to combine and structure multiple plots in standard Matplotlib subplots.\n",
"\n",
"> **Shortcut (No Imports):** If you want to use the Matplotlib backend (`gplot`/`pplot`) but don't want to import Matplotlib/Pyplot in your code, you can simply pass `ax=None` (e.g., `ax = wfn.plot(ax=None)`). The plotting method will detect the `ax` key, switch to the Matplotlib backend, internally import Matplotlib to create a new figure and axes, and return the active `plt.Axes` object."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "64669b15",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
""
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, (ax1, ax2) = plt.subplots(1, 2, facecolor='none', figsize=(8, 4))\n",
"\n",
"wfn.plot_location(ax=ax1)\n",
"wfn.plot(ax=ax2, legend=False)"
]
},
{
"cell_type": "markdown",
"id": "bc70750c",
"metadata": {},
"source": [
"## 📊 Optional parameters"
]
},
{
"cell_type": "markdown",
"id": "47537ebb-91fe-41cd-b997-0ed290a697ec",
"metadata": {},
"source": [
"### 🏷️ `node_tag` (text labels)"
]
},
{
"cell_type": "markdown",
"id": "f1e56e24-6b4e-4d63-b24a-73e69eb51652",
"metadata": {},
"source": [
"Node tags make it easier to identify each turbine and substation in the plot. Here is a table of possible values and their effects:\n",
"\n",
"| `node_tag` value | Behavior |\n",
"| -------------- | --------------------------------------------------------------------------- |\n",
"| `node_tag=True` | Node id number used as tag (id is set from the order of appearance in data, starting from 0 for turbines and negative for substations).|\n",
"| `node_tag='label'` | Node label used as tag. Labels can be defined in the `.yaml` file or as a node tag in `.osm.pbf`. Programmatically, labels can be assigned as node attribute: `G.nodes[id]['label'] = 'some_label'`|\n",
"| `node_tag='load'` | Use the value of the load exported by the node.|\n",
"| `node_tag='custom_attribute'` | Use the value of `custom_attribute` from node. |"
]
},
{
"cell_type": "markdown",
"id": "a41f283c-8d65-4f29-a5e7-2ef05f0ecbf3",
"metadata": {},
"source": [
"> **Note**: The default symbol sizes are optimized for tags with up to **three characters** using the default font size, longer tags might not fit inside the symbol and become difficult to read."
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "6a4b7e32",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wfn.plot(node_tag=True)"
]
},
{
"cell_type": "markdown",
"id": "b840b66f-3a7e-4c49-9e66-a9a9d80bb5f1",
"metadata": {},
"source": [
"**Labels in location data files**"
]
},
{
"cell_type": "markdown",
"id": "27aa4058-0985-45fd-b701-dfd9dbdb5a3a",
"metadata": {},
"source": [
"Labels can be defined in both `.yaml` and `.osm.pbf` files."
]
},
{
"cell_type": "markdown",
"id": "9037985d-df5d-4a91-a5e5-399bde59ec1c",
"metadata": {},
"source": [
"- **`.yaml` files:**\n",
"Any identifier placed *before* the coordinates is loaded as a node label (`label` attribute).\n",
"Example:\n",
"\n",
"```yaml\n",
"SUBSTATIONS: |-\n",
" OSS 56°35.748'N 11°09.174'E\n",
"\n",
"TURBINES: |-\n",
" A01 56°30.477'N 11°11.026'E\n",
" A02 56°30.810'N 11°11.078'E\n",
"```\n",
"\n",
"Here, `OSS`, `A01` and `A02` are node labels."
]
},
{
"cell_type": "markdown",
"id": "c218572d",
"metadata": {},
"source": [
"- **`.osm.pbf` files:**\n",
"Node tags `'name'` or `'ref'` are loaded as a node label (`label` attribute).\n",
"\n",
"Example (OSM XML structure):\n",
"\n",
"```xml\n",
"\n",
" \n",
" \n",
"\n",
"```\n",
"\n",
"In this case, the substation node will be tagged with the label `'OSS'`."
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "4e41efbe-054c-4949-9880-95c9029e3017",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wfn.plot(node_tag='label', infobox=False, legend=True)"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "dd74ab60",
"metadata": {},
"outputs": [],
"source": [
"wfn.L.nodes[0]['example_label']='T0'"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "dc6d5497",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wfn.plot_location(node_tag='example_label')"
]
},
{
"cell_type": "markdown",
"id": "2a5748af-1d93-4920-a952-3e93e27c8d50",
"metadata": {},
"source": [
"The nodes can also be tagged with the amount of power (normalized by the turbine nominal power) they export towards the substation:\n",
"- The turbine tag represents the number of turbines exporting power through it, (i.e. number of turbines upstream, including itself).\n",
"- The substation tag represents the total number of connected turbines."
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "3c8bbff6-e73b-44f8-932f-4d485763faa8",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wfn.plot(node_tag='load')"
]
},
{
"cell_type": "markdown",
"id": "88b49207-5bf5-4dd0-b22a-e1e035335d91",
"metadata": {},
"source": [
"### 🎨 `dark` (color theme)"
]
},
{
"cell_type": "markdown",
"id": "fbbf39e7-b228-4142-93d5-1f757d8c0e5f",
"metadata": {},
"source": [
"The plotting methods try to match the operating system's theme via Python package `darkdetect`. This package detects the system's theme and not JupyterLab's theme, so it may lead to non-ideal results if they do not match.\n",
"\n",
"The example notebooks are created from a dark-themed system, but the figures might have a light background when the notebooks are run on a different computer.\n",
"\n",
"The theme can be enforced programmatically by setting the argument `dark` to `True` or `False`:"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "09b9bc08-7048-4116-b5d2-0226ebeb2037",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wfn.plot(dark=False)"
]
},
{
"cell_type": "markdown",
"id": "334b548e-acd4-41c4-93e0-d715b1b0c5f4",
"metadata": {},
"source": [
"### 🔄 `landscape_angle` (rotation)"
]
},
{
"cell_type": "markdown",
"id": "f0f10ebf-ffcb-4cd9-85fc-c95b7d0a2416",
"metadata": {},
"source": [
"Each location included in `OptiWindNet` has a graph attribute called `landscape_angle`."
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "c38128f0-22b3-400b-9604-48d423d6c25e",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"81.8"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wfn.L.graph['landscape_angle']"
]
},
{
"cell_type": "markdown",
"id": "78a9cadd-93d2-4fa8-a37f-f08947b6fa3e",
"metadata": {},
"source": [
"This is the angle by which to rotate the site so as to orient the widest dimension horizontally, which suits a landscape figure (as opposed to portrait). For orienting the location so that the north is up, pass the argument `landscape=False` (or set `wfn.L.graph['landscape_angle'] = 0`)."
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "5a69abdd",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
""
],
"text/plain": [
""
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"wfn.plot(landscape=False)"
]
},
{
"cell_type": "markdown",
"id": "f94d14b7",
"metadata": {},
"source": [
"## Plotting Auxiliary Graphs"
]
},
{
"cell_type": "markdown",
"id": "ef306c5b",
"metadata": {},
"source": [
"Of the **five plot methods**, `wfn.plot_location()` and `wfn.plot()` have been presented above.\n",
"\n",
"The remaining plot methods are for the auxiliary graphs `navigation_mesh`, `available_links`, and `selected_links`, which are relevant mainly for **debugging** and **development**. For a more detailed explanation of their meaning and purpose, consult the [paper](https://doi.org/10.1016/j.ejor.2025.07.069)."
]
},
{
"cell_type": "markdown",
"id": "32641de9",
"metadata": {},
"source": [
"### Navigation mesh"
]
},
{
"cell_type": "markdown",
"id": "2a6bf78f",
"metadata": {},
"source": [
"The *navigation mesh* is a triangulation of all vertices (coordinate pairs that define either turbines, substations or obstacles) of the location. An additional triangle is added (the supertriangle) such as to contain all vertices inside it (it does not fit in the plot, but can be inferred by the edges interrupted at the figure's borders). This mesh is obtained via a **constrained Delaunay triangulation** and stored as a planar embedding in `wfn.P`."
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "494da4c3",
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"