{ "cells": [ { "cell_type": "markdown", "id": "f7b072be-a181-48f3-9a7c-5d8f16012d49", "metadata": {}, "source": [ "# WindFarmNetwork/Router" ] }, { "cell_type": "markdown", "id": "2ce7a38d", "metadata": {}, "source": [ "This notebook is a practical guide to *OptiWindNet*’s Network/Router API (i.e. high-level API). It covers creating a wind farm model, using different routers (`EWRouter`, `HGSRouter`, `MILPRouter`), and applying key `WindFarmNetwork` functionalities. Specifically, we will:\n", "\n", "* Explore the `WindFarmNetwork` class\n", "* Compare available routers and their use cases\n", "* Run a complete optimization example" ] }, { "cell_type": "markdown", "id": "80c8c206-b99d-4ec4-a593-a10e8fbd57ed", "metadata": {}, "source": [ "## ✅ WindFarmNetwork" ] }, { "cell_type": "markdown", "id": "2196d3c5", "metadata": {}, "source": [ "The `WindFarmNetwork` class is the **central user-facing component** in the OptiWindNet Network/Router-API. It is flexible and extensible, supporting multiple input formats and routers (electrical network optimizers), and is used to model and optimize the electrical network of a wind farm." ] }, { "cell_type": "markdown", "id": "a8acf159", "metadata": {}, "source": [ "### To create a `WindFarmNetwork` instance:\n", "\n", "**Required:**\n", "\n", "* Turbine and substation coordinates *(see [Data Input](a01_data_input.ipynb) for formats)*\n", "* Cable data (capacities and costs, or at least maximum capacity as a single number)\n", "\n", "**Optional:**\n", "\n", "* Borders and obstacles: to add spatial constraints\n", "* Router: optimization strategy (defaults to `EWRouter` if not specified)\n", "* verbose: log/hide logging messages (default to `Fault`)." ] }, { "cell_type": "markdown", "id": "4c944a01", "metadata": {}, "source": [ "### Key Responsibilities" ] }, { "cell_type": "markdown", "id": "d128ab24", "metadata": {}, "source": [ "| Feature | Description |\n", "| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |\n", "| **Initialization & Parsing** | Accepts turbine/substation coordinates or the Location geometry `L` (Can be constructed from YAML, PBF, or WindIO formats for integration with real-world datasets), validates cable data, and constructs the internal graph structure. |\n", "| **Optimization** | Interfaces smoothly with different routers (`EWRouter`, `HGSRouter`, `MILPRouter`) to optimize the network (cable routing) for cost and cable length. |\n", "| **Visualization** | Provides plotting functions for location geometry, links, mesh, and the optimized network. |\n", "| **Gradient & Update Graph** | Computes gradients for optimization and allows updating the electrical network using a compact \"terse link\" format. |\n" ] }, { "cell_type": "markdown", "id": "a41b0f1b", "metadata": {}, "source": [ "### Example Workflow" ] }, { "cell_type": "markdown", "id": "1d7ab495", "metadata": {}, "source": [ "```python\n", "# Initialize with coordinates and cable types\n", "wfn = WindFarmNetwork(\n", " cables=[(2, 1500.0), (5, 1800.0)],\n", " turbinesC=...,\n", " substationsC=...,\n", ")\n", "\n", "# Optimize electrical network using the default router (EWRouter)\n", "wfn.optimize()\n", "\n", "# Access total cost or cable length\n", "total_cost = wfn.cost()\n", "total_length = wfn.length()\n", "\n", "# Visualize network\n", "wfn.plot() # or simply wfn if you are running on notebooks\n", "```" ] }, { "cell_type": "markdown", "id": "dcf9eec1", "metadata": {}, "source": [ "## 🧭 **Router**" ] }, { "cell_type": "markdown", "id": "ffd963f6", "metadata": {}, "source": [ "In OptiWindNet, a `Router` is used to compute the optimal network (cable routing) of the wind farm’s electrical network. Given turbine and substation positions (layout), available cable options, and routing constraints, a router determines *which turbines connect to which substations* and how cables should be laid to *minimize length (and consequently cost)*." ] }, { "cell_type": "markdown", "id": "d9f536d0", "metadata": {}, "source": [ "### Currently Available Routers" ] }, { "cell_type": "markdown", "id": "dbf55f1a", "metadata": {}, "source": [ "The routers currently included in the `optiwindnet.api` module are:\n", "\n", "* **EWRouter**\n", "\n", " * *Fastest option* — completes in a fraction of a second\n", " * Great for quick network generation\n", " * Produces only **branched-topology** solutions\n", " * Heuristic method (EW = modified Esau-Williams) — solutions may be far from the optimum\n", "\n", "* **HGSRouter**\n", "\n", " * *Still fast* — can provide high-quality solutions in **0.5–2 seconds**\n", " * Produces only **radial-topology** solutions (no branching)\n", " * A maximum number of feeders can be enforced\n", " * Meta-heuristic method (HGS = Hybrid Genetic Search)\n", "\n", "* **MILPRouter**\n", "\n", " * Delivers solutions with **quality guarantees** (a bound on the distance from the optimum)\n", " * May take a **few to several minutes** depending on the problem size and quality required\n", " * Full set of model options to choose from\n", " * Should be used when optimality is more important than speed\n", " * MILP = Mixed Integer Linear Programming" ] }, { "cell_type": "markdown", "id": "961f9090", "metadata": {}, "source": [ "## Run an example" ] }, { "cell_type": "markdown", "id": "39bcea21", "metadata": {}, "source": [ "In this section we will:\n", "- Create a simple wind farm network\n", "- Use heuristic and MILP optimization\n", "- Explore key methods such as:\n", " - `.optimize()`\n", " - `.plot()`\n", " - `.cost()`, `.length()`\n", " - `.terse_links()`, `.update_from_terse_links()`\n", " - `.gradient()`\n", " - `.get_network()`\n", " - `.add_buffer()`" ] }, { "cell_type": "markdown", "id": "de0f0c52", "metadata": {}, "source": [ "### Create a `WindFarmNetwork` instance" ] }, { "cell_type": "markdown", "id": "c36112a8", "metadata": {}, "source": [ "Import required modules" ] }, { "cell_type": "code", "execution_count": 1, "id": "afeaaf92", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from optiwindnet.api import WindFarmNetwork, EWRouter, MILPRouter, ModelOptions" ] }, { "cell_type": "code", "execution_count": 2, "id": "d8fb593b", "metadata": {}, "outputs": [], "source": [ "# Display figures as SVG in Jupyter notebooks\n", "%config InlineBackend.figure_formats = ['svg']" ] }, { "cell_type": "markdown", "id": "d5373461", "metadata": {}, "source": [ "#### Load location data" ] }, { "cell_type": "markdown", "id": "4ff13e41", "metadata": {}, "source": [ "OptiWindNet operates on location geometries, which define turbine and substation positions (plus optional borders/obstacles).\n", "\n", "You can load a location from:\n", "- `.yaml` or `.osm.pbf` files;\n", "- The included locations repository;\n", "- Your own coordinate arrays.\n", "\n", "For more details see [Data Input](a01_data_input.ipynb).\n", "\n", "We start with a simple geometry (5 turbines and 1 substation), and define a simple set of cables." ] }, { "cell_type": "code", "execution_count": 3, "id": "910dc391-6bf5-4fda-9a0b-a4faab58248a", "metadata": {}, "outputs": [], "source": [ "wfn = WindFarmNetwork(\n", " cables=[(2, 1500.0), (5, 1800.0)],\n", " turbinesC=np.array([[0, 0], [1, 1], [2, 0], [3, 1], [4, 0]]),\n", " substationsC=np.array([[2, -2]]),\n", " borderC=np.array([[0, -3], [0, 2], [4, 2.1], [5, 1], [4, -3]]),\n", " obstacleC_=[np.array([[0.2, -2.5], [1.5, -2.5], [0.2, -1]])]\n", ")" ] }, { "cell_type": "markdown", "id": "fcafb617", "metadata": {}, "source": [ "##### Plot location\n", "\n", "> **Note:** Many of the Jupyter notebooks provided include SVG figures as output. To ensure these visuals are displayed correctly in JupyterLab or Jupyter Notebook, make sure the notebook is marked as **trusted**.\n", "> *In JupyterLab, you can do this by pressing* `Ctrl + Shift + C` *and selecting* **Trust Notebook**." ] }, { "cell_type": "code", "execution_count": 4, "id": "c2b946a4", "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "" ], "text/plain": [ "" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wfn" ] }, { "cell_type": "markdown", "id": "9cd882d6", "metadata": {}, "source": [ "### Method: `optimize(router=...)`" ] }, { "cell_type": "markdown", "id": "03cf721a", "metadata": {}, "source": [ "This is the main method that optimizes the electrical network using the given router (heuristic, metaheuristic or MILP).\n", " > Note that the router could be passed directly to `WindFarmNetwork(router=...)`." ] }, { "cell_type": "markdown", "id": "2865ca33-b8d1-42c9-bb49-5d088e866a9d", "metadata": {}, "source": [ "#### Use a heuristic router (Esau-Williams)" ] }, { "cell_type": "code", "execution_count": 5, "id": "43d027d8-1b4c-4b78-96ba-2207a6a6056a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 1, 2, -1, 2, 3])" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wfn.optimize(router=EWRouter())" ] }, { "cell_type": "markdown", "id": "b2bb6e6d", "metadata": {}, "source": [ "Plot the result" ] }, { "cell_type": "code", "execution_count": 6, "id": "1388e2d8", "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "12085 €Σλ = 7.6569 m(+0) [-1]: 1κ = 5, T = 5" ], "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wfn" ] }, { "cell_type": "markdown", "id": "775d7118", "metadata": {}, "source": [ "#### Use a MILP router with full control over topology and feeders\n", "\n", "The solution from the previous call to `wfn.optimize()` is stored within the `wfn` object.\n", "If this solution is *feasible under the current `MILPRouter` settings*, it will be *automatically used as a warm start*.\n", "Otherwise, if it does not meet the current `ModelOptions` constraints, it will be *ignored*, and the MILP solver will start from scratch.\n" ] }, { "cell_type": "code", "execution_count": 7, "id": "a5621cbd", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Warning: No warmstarting (even though a solution is available) due to the following reason(s):\n", " - branched network incompatible with model option: topology=\"radial\"\n", "\n" ] }, { "data": { "image/svg+xml": [ "14425 €Σλ = 8.4853 m(+0) [-1]: 1κ = 5, T = 5" ], "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "model_opts = ModelOptions(\n", " topology='radial',\n", " feeder_limit='minimum',\n", " feeder_route='straight',\n", ")\n", "wfn.optimize(\n", " router=MILPRouter(\n", " solver_name='ortools',\n", " time_limit=2,\n", " mip_gap=0.01,\n", " model_options=model_opts\n", " ),\n", " verbose=True,\n", ")\n", "wfn" ] }, { "cell_type": "markdown", "id": "2511d0d4-06d9-4baf-8de9-7939373fa5e8", "metadata": {}, "source": [ "### Method: `plot()` and Variants" ] }, { "cell_type": "markdown", "id": "3dbd5d88", "metadata": {}, "source": [ "These methods help you visualize different stages of the optimization:\n", "\n", "- `plot_location()`: plot turbine/substation coordinates and borders\n", "- `plot()`: plot optimized electrical network\n", "\n", "> For more details, see the [Plotting](a04_Plotting.ipynb) tutorial." ] }, { "cell_type": "code", "execution_count": 8, "id": "89161332", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2025-08-28T18:56:24.982726\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.10.1, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2025-08-28T18:56:25.026480\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.10.1, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " κ = 5, T = 5\n", " (+0) [-1]: 1\n", " Σλ = 8.4853 m\n", " 14425 €\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "wfn.plot_location()\n", "wfn.plot() # wfn will do the same on notebooks (and if no G is available it will display L)" ] }, { "cell_type": "markdown", "id": "b93855a6-3175-4c75-8358-0bc56e8e787b", "metadata": {}, "source": [ "### Method: `cost()` and `length()`" ] }, { "cell_type": "markdown", "id": "711e2851-3c45-4e25-b6d2-30db7f0d56da", "metadata": {}, "source": [ "Returns the total cost and cable length of the optimized network." ] }, { "cell_type": "code", "execution_count": 9, "id": "72ef1d9a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Network cost: 14424.97833620557\n", "Network length: 8.485281374238571\n" ] } ], "source": [ "print(\"Network cost:\", wfn.cost())\n", "print(\"Network length:\", wfn.length())" ] }, { "cell_type": "markdown", "id": "405e5e24-76d9-481f-bf5b-3e2b84feeb46", "metadata": {}, "source": [ "### Method: `terse_links()`" ] }, { "cell_type": "markdown", "id": "5a5a48f5-c2b7-449b-8a10-0b2f62d19ca6", "metadata": {}, "source": [ "A terse link is a compact way to describe how each turbine is connected in the electrical network. It’s just a list (or array) where:\n", "\n", "* Each position `i` represents turbine `i`\n", "\n", "* The value at position `i` is the node that turbine `i` connects to\n", "(this could be another turbine or a substation)" ] }, { "cell_type": "code", "execution_count": 10, "id": "deb7d87f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Terse link array: [ 1 2 3 4 -1]\n" ] } ], "source": [ "terse = wfn.terse_links()\n", "print(\"Terse link array:\", terse)" ] }, { "cell_type": "markdown", "id": "831e287d-0cf1-44cb-9b0c-11433a5006ea", "metadata": {}, "source": [ "### Method: `update_from_terse_links()`" ] }, { "cell_type": "markdown", "id": "3e95d580-3031-43bd-b277-f020b96731b6", "metadata": {}, "source": [ "`update_from_terse_links()` allows you to reconstruct the network from a known `terse_link`, optionally updating coordinates. This method assumes a **valid and feasible network**, so it’s your responsibility to ensure that:\n", "* The connections form a proper **feeder tree**\n", "* Every turbine is (indirectly or directly) connected to a **substation**\n", "* Capacity constraints are not violated\n", "\n", "Suppose we have a wind farm with:\n", "* **5 turbines** → nodes `0, 1, 2, 3, 4`\n", "* **1 substation** → node `-1` (*Note: In OptiWindNet, substations are assigned negative indices*)\n", "\n", "We want the following connections:\n", "* Turbine 0 → Substation (-1)\n", "* Turbine 1 → Turbine 0\n", "* Turbine 2 → Substation (-1)\n", "* Turbine 3 → Turbine 2\n", "* Turbine 4 → Turbine 3\n", "\n", "This gives us the `terse_links` array:\n", "\n", "```python\n", "terse_links = [-1, 0, -1, 2, 3]" ] }, { "cell_type": "code", "execution_count": 11, "id": "af4c603a", "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "14207 €Σλ = 9.0711 m(+1) [-1]: 2κ = 5, T = 5" ], "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "new_terse_links = np.array([-1, 0, -1, 2, 3])\n", "\n", "# Apply the new configuration\n", "wfn.update_from_terse_links(new_terse_links)\n", "\n", "# Visualize the updated network (cable routing)\n", "wfn\n" ] }, { "cell_type": "markdown", "id": "3f277b56", "metadata": {}, "source": [ "### Method: `gradient()`" ] }, { "cell_type": "markdown", "id": "96a70239", "metadata": {}, "source": [ "This method computes the gradient of the cost or length with respect to turbine/substation positions.\n", "Useful for hybrid optimization or sensitivity analysis.\n", " > Note: default of `gradient_type` is `length`." ] }, { "cell_type": "code", "execution_count": 12, "id": "3fe297b4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- gradient_type=length ---\n", "\n", "Gradient (w.r.t. turbines):\n", " [[-1.41421356 0. ]\n", " [ 0.70710678 0.70710678]\n", " [-0.70710678 0.29289322]\n", " [ 0. 1.41421356]\n", " [ 0.70710678 -0.70710678]] \n", "\n", "Gradient (w.r.t. substations):\n", " [[ 0.70710678 -1.70710678]]\n", "\n", "\n", "--- gradient_type=length ---\n", "\n", "Gradient (w.r.t. turbines):\n", " [[-2121.32034356 0. ]\n", " [ 1060.66017178 1060.66017178]\n", " [-1060.66017178 739.33982822]\n", " [ 0. 2121.32034356]\n", " [ 1060.66017178 -1060.66017178]] \n", "\n", "Gradient (w.r.t. substations):\n", " [[ 1060.66017178 -2860.66017178]]\n" ] } ], "source": [ "print('--- gradient_type=length ---\\n')\n", "grad_turb, grad_subs = wfn.gradient()\n", "print(\"Gradient (w.r.t. turbines):\\n\", grad_turb, \"\\n\")\n", "print(\"Gradient (w.r.t. substations):\\n\", grad_subs)\n", "print('\\n')\n", "print('--- gradient_type=length ---\\n')\n", "grad_turb, grad_subs = wfn.gradient(gradient_type='cost')\n", "print(\"Gradient (w.r.t. turbines):\\n\", grad_turb, \"\\n\")\n", "print(\"Gradient (w.r.t. substations):\\n\", grad_subs)" ] }, { "cell_type": "markdown", "id": "95831235", "metadata": {}, "source": [ "### Method: `get_network()`" ] }, { "cell_type": "markdown", "id": "4eea9d03", "metadata": {}, "source": [ "This method returns the **final optimized network** as a **structured NumPy array**, where each row represents an edge in the network.\n", "\n", "Each edge includes detailed attributes such as:\n", "\n", "* **`src`**: index of the source node\n", "* **`tgt`**: index of the target (destination node)\n", "* **`length`**: physical cable length\n", "* **`load`**: electrical load carried through the cable (number of turbines)\n", "* **`cable`**: index of the cable type used (e.g., 0 for first type in cable list)\n", "* **`cost`**: cost associated with the used cable (if cost is provided by the user)" ] }, { "cell_type": "code", "execution_count": 13, "id": "9a0b2118", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([(1, 0, 1.41421356, 1., 0), (0, -1, 2.82842712, 2., 0)],\n", " dtype=[('src', '" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2025-08-28T18:56:25.672326\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.10.1, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " Original and Buffered Shapes\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " Original Border\n", " \n", " \n", " \n", " \n", " \n", " Original Obstacle\n", " \n", " \n", " \n", " \n", " \n", " Buffered Border\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "wfn.plot_original_vs_buffered()" ] }, { "cell_type": "code", "execution_count": 16, "id": "77dab41a", "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "14425 €Σλ = 8.4853 m(+0) [-1]: 1κ = 5, T = 5" ], "text/plain": [ "" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wfn.optimize()\n", "wfn" ] }, { "cell_type": "markdown", "id": "9276fb5d", "metadata": {}, "source": [ "### In this section:\n", "\n", "we explored the most useful methods of the `WindFarmNetwork` class:\n", "\n", "| Method | Purpose |\n", "|--------|---------|\n", "| `optimize()` | Run optimization with a router |\n", "| `plot()` | Visualize the network |\n", "| `cost()`, `length()` | Get total cost and length |\n", "| `terse_links()` | Get compact link encoding |\n", "| `update_from_terse_links()` | Apply terse links manually |\n", "| `gradient()` | Compute network's gradient |\n", "| `get_network()` | Export the optimized network data |\n", "| `add_buffer()`| Expand border - Shrink obstacles|\n", "\n", "For deeper insights into individual methods, refer to the dedicated notebooks provided.\n" ] } ], "metadata": { "kernelspec": { "display_name": "OptiWindNet", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.9" } }, "nbformat": 4, "nbformat_minor": 5 }