diff --git a/power-grid-model-ds/advanced.ipynb b/power-grid-model-ds/advanced.ipynb new file mode 100644 index 0000000..81993ce --- /dev/null +++ b/power-grid-model-ds/advanced.ipynb @@ -0,0 +1,524 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "59d49e29", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install pandas power-grid-model-ds[visualizer] --quiet" + ] + }, + { + "cell_type": "markdown", + "id": "d5e1958f", + "metadata": {}, + "source": [ + "# ⚡ Advanced PGM-DS Workshop: Solving an Overload Scenario\n", + "\n", + "You're a senior grid analyst at GridNova Utilities, responsible for operating a legacy distribution grid in a rural area. The grid is radially operated, with some cables inactive as back-up in case failures. Recently, customer load growth has increased dramatically, particularly in areas served by several long feeders. This has pushed some branches past their capacity, triggering repeated overloads.\n", + "\n", + "Your task: upgrade the grid by adding a second substation and relieving the overloaded feeder through new connections to the new substation.\n", + "\n", + "This hands-on simulation walks you through each step of diagnosing, planning, and solving this overload using the Power Grid Model Data Science library.\n", + "\n", + "## 🎯 Workshop Goals\n", + "- Detect a line overload using PGM load flow calculations.\n", + "- Find a suitable node to create a connection to the new substation.\n", + "- Strategically open a line to reroute power and relieve the feeder.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf475775", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from dataclasses import dataclass\n", + "from power_grid_model_ds import Grid, GraphContainer\n", + "from power_grid_model_ds.arrays import LineArray, NodeArray, SourceArray\n", + "from power_grid_model_ds.enums import NodeType\n", + "from power_grid_model_ds.visualizer import visualize" + ] + }, + { + "cell_type": "markdown", + "id": "8a4a9dbb", + "metadata": {}, + "source": [ + "# 🧪 Step 1: Extend the Data Model\n", + "Goal: Add coordinate fields and tracking for simulated voltages and line currents. This allows us to store and analyse metadata of the grid needed to to decide where to invest in the grid.\n", + "\n", + "You’ll subclass NodeArray and LineArray to add:\n", + "\n", + "- x, y coordinates for spatial logic and plotting\n", + "- u for node voltage results\n", + "- i_from for line currents\n", + "- A computed .is_overloaded property for easy filtering\n", + "\n", + "This shows how the Grid can be extended to suit the needs of a specific project." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37f4bd29", + "metadata": {}, + "outputs": [], + "source": [ + "# 📦 Extend the grid with x, y, u (node) and i_from (line)\n", + "from numpy.typing import NDArray\n", + "\n", + "class MyNodeArray(NodeArray):\n", + " _defaults = {\"x\": 0.0, \"y\": 0.0, \"u\": 0.0}\n", + " x: NDArray[np.float64]\n", + " y: NDArray[np.float64]\n", + " u: NDArray[np.float64]\n", + "\n", + "class MyLineArray(LineArray):\n", + " _defaults = {\"i_from\": 0.0, \"overload_status\": 0}\n", + " i_from: NDArray[np.float64]\n", + " overload_status: NDArray[np.int8]\n", + "\n", + " def set_overload_status(self):\n", + " \"\"\"Set the overload status based on the current and nominal current.\"\"\"\n", + " self.overload_status = np.where(self.i_from > self.i_n, 1, 0)\n", + "\n", + " @property\n", + " def is_overloaded(self) -> NDArray[np.bool_]:\n", + " \"\"\"Check if the line is overloaded.\"\"\"\n", + " self.set_overload_status()\n", + " return self.overload_status == 1\n", + "\n", + "@dataclass\n", + "class MyGrid(Grid):\n", + " node: MyNodeArray\n", + " line: MyLineArray\n", + " graphs: GraphContainer" + ] + }, + { + "cell_type": "markdown", + "id": "8e2d5fbe", + "metadata": {}, + "source": [ + "# 🏗️ Step 2: Load and Prepare the Grid\n", + "Goal: Load a synthetic medium-voltage grid from the provided data\n", + "(Code is already given in helper.py file, take a look to see how the grid data is loaded!)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78d0639e", + "metadata": {}, + "outputs": [], + "source": [ + "from helper import load_dummy_grid\n", + "\n", + "grid = load_dummy_grid(MyGrid)" + ] + }, + { + "cell_type": "markdown", + "id": "4d2df7f4", + "metadata": {}, + "source": [ + "We now loaded our the network visualised here\n", + "\n", + "![input_network.png](input_network.png)" + ] + }, + { + "cell_type": "markdown", + "id": "6ac4eb43", + "metadata": {}, + "source": [ + "# 🧯 Step 3: Detect the Overload\n", + "This is your first excercise.\n", + "\n", + "Goal: Identify which line(s) are exceeding their rated current (the `i_n` property).\n", + "\n", + "You can do this step by step (don't forget to check the PGM-DS documentation):\n", + "\n", + "1. Use the PowerGridModelInterface to calculate power flow\n", + "2. Update the Grid object with the calculated values\n", + "3. Return the lines (LineArray) that are overloaded\n", + "\n", + "**💡 Hint**: You can use the `is_overloaded` property of the `MyLineArray` class to check for overloaded lines.\n", + "\n", + "**💡 Hint**: https://power-grid-model-ds.readthedocs.io/en/stable/quick_start.html#performing-power-flow-calculations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "206be67b", + "metadata": {}, + "outputs": [], + "source": [ + "def check_for_capacity_issues(grid: Grid) -> LineArray:\n", + " \"\"\"Check for capacity issues on the grid.\n", + " Return the lines that with capacity issues.\n", + " \"\"\"\n", + "\n", + "print(check_for_capacity_issues(grid))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18973302-105f-42c3-9ffd-1408b90aeb10", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/advanced_3_check_for_capacity_issues.py" + ] + }, + { + "cell_type": "markdown", + "id": "4bd3aea9", + "metadata": {}, + "source": [ + "We can use PGM-DSs visualization function to explore the resulting grid. Check out the highlighting parts of the grid based on it's attributes to find out where the overload occurs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1774170b", + "metadata": {}, + "outputs": [], + "source": [ + "visualize(grid)" + ] + }, + { + "cell_type": "markdown", + "id": "008fafb9", + "metadata": {}, + "source": [ + "# 🧭 Step 4: Plan a Relief Strategy\n", + "\n", + "If you visualize the grid and highlight the overloaded cables, this is what you will see:\n", + "\n", + "![input_network_with_overload.png](input_network_with_overload.png)\n", + "\n", + "We found out the north-east part of the area is overloaded.\n", + "Goal: Place a second substation near the overloaded path. In the next steps we will use this substation to relieve overloaded cables.\n", + "\n", + "You’ll:\n", + "- Create a new substations using the NodeArrayobject at the correct location.\n", + "\n", + "This substation will act as a new injection point for rerouting load.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae5d93b7", + "metadata": {}, + "outputs": [], + "source": [ + "# Check the introduction workshop on adding a substation\n", + "\n", + "def build_new_substation(grid: Grid, location: tuple[float, float]) -> NodeArray:\n", + " \"\"\"Build a new substation at the given location.\n", + " Return the new substation.\n", + " \"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bff1e30f-5dd3-4774-977b-5707322a8f59", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/advanced_4_build_new_substation.py" + ] + }, + { + "cell_type": "markdown", + "id": "8f2b17e3", + "metadata": {}, + "source": [ + "# 🔗 Step 5: Analyze and Connect the Overloaded Route\n", + "Goal: Identify the best way to connect the new substation to the overloaded routes.\n", + "\n", + "You’ll:\n", + "- Compute which routes (/feeders) are overloaded to see where we need to intervene.\n", + "- Find which node on an overloaded route is geographically closed to the new substation.\n", + "- Create a new cable to connect the closest node to the new substation.\n", + "\n", + "**💡 Hint**: The lines have been extended with extra properties in Step 1\n", + "\n", + "**💡 Hint**: The arrays in the grid have a filter option, https://power-grid-model-ds.readthedocs.io/en/stable/examples/model/array_examples.html#using-filters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1bfe4e6e", + "metadata": {}, + "outputs": [], + "source": [ + "def get_all_congested_routes(grid: Grid) -> list[NodeArray]:\n", + " \"\"\"Get all nodes on routes that contain an overloaded line.\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc494d36-5ee7-401c-8a3b-59e701777f8d", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/advanced_5_1_get_all_congested_routes.py" + ] + }, + { + "cell_type": "markdown", + "id": "ee84e60c", + "metadata": {}, + "source": [ + "Next we will use the nodes x and y coordinates to find a suitable node to connect to the new substation. You will create a find_connection_point function that return the Node in a route which is closest to the new_substation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32b32015", + "metadata": {}, + "outputs": [], + "source": [ + "def find_connection_point(route: NodeArray, new_substation: NodeArray) -> NodeArray:\n", + " \"\"\"Calculate the connection point for the new route.\n", + " This should be the geographically closest node to the new substation.\n", + " \"\"\"\n", + " # Calculate the distance of each node in the route to the new_substation\n", + " # Return the closest one" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ce16108-4580-4c32-9e18-60239144ae4f", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/advanced_5_2_find_connection_point.py" + ] + }, + { + "cell_type": "markdown", + "id": "a75e9b73", + "metadata": {}, + "source": [ + "Finally we build a function that creates a new line between the connection point and the new substation.\n", + "\n", + "❗ **IMPORTANT** ❗ The new line should first be created with an open connection; we will optimize the location of the line opening in the next step.\n", + "\n", + "**💡 Hint**: In the introduction you learned how to add a LineArray to the grid." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "263818f6", + "metadata": {}, + "outputs": [], + "source": [ + "def connect_to_route(grid: Grid, connection_point: NodeArray, new_substation: NodeArray) -> None:\n", + " \"\"\"Connect the new substation node to the connection point.\n", + " \"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e711bc3c-a09c-4454-b3a5-be4bf15fa077", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/advanced_5_3_connect_to_route.py" + ] + }, + { + "cell_type": "markdown", + "id": "4a9ef582", + "metadata": {}, + "source": [ + "# 🔌 Step 6: Open the Right Line\n", + "Goal: Find the optimal line to open to relieve the original overloaded feeder.\n", + "\n", + "You’ll:\n", + "- Trace a path from the newly created cable to the old substation\n", + "- Evaluate each line on the path by running `check_for_capacity_issues()` and find the optimal line to open\n", + "- Open the correct line\n", + "- Confirm the overload is resolved\n", + "\n", + "This final step demonstrates how network topology can be programmatically optimized using the Power Grid Model Data Science toolkit!\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a494d54c", + "metadata": {}, + "outputs": [], + "source": [ + "def optimize_route_transfer(grid: Grid, connection_point: NodeArray, new_substation: NodeArray) -> None:\n", + " \"\"\"Attempt to optimize the route transfer moving the normally open point (NOP) upstream towards the old substation.\n", + " This way, the new substation will take over more nodes of the original route.\n", + " \"\"\"\n", + " # Get the path from the connection point to the old substation\n", + " ...\n", + "\n", + " # filter the first branch in the path\n", + " ...\n", + "\n", + " # Iterate over the path and check if the route is still overloaded\n", + " for from_node, to_node in zip(path[0:-1], path[1:]):\n", + " # Check if the route is still overloaded\n", + " ...\n", + " \n", + " # Move the Open Point (NOP) upstream\n", + " ...\n", + " \n", + " grid.set_feeder_ids()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "471b9ff0-43ca-432c-9ce9-71fc06d95b86", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/advanced_6_optimize_route_transfer.py" + ] + }, + { + "cell_type": "markdown", + "id": "dbf3243e", + "metadata": {}, + "source": [ + "Now we combine the functions you created to solve the issues in the network" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb4092a1", + "metadata": {}, + "outputs": [], + "source": [ + "def transfer_routes(grid: Grid, new_substation: NodeArray) -> NodeArray:\n", + " \"\"\"Migrate a subset of the routes of the old substation to the new substation.\n", + " Each route can be migrated fully or partially.\n", + "\n", + " \"\"\"\n", + " congested_routes = get_all_congested_routes(grid)\n", + "\n", + " for route in congested_routes:\n", + " closest_node = find_connection_point(\n", + " route=route,\n", + " new_substation=new_substation\n", + " )\n", + "\n", + " connect_to_route(\n", + " grid=grid,\n", + " connection_point=closest_node,\n", + " new_substation=new_substation,\n", + " )\n", + "\n", + " optimize_route_transfer(\n", + " grid=grid,\n", + " connection_point=closest_node,\n", + " new_substation=new_substation)\n", + " \n", + " print(f\"Connected new substation to node {closest_node.id}\")\n", + "\n", + "transfer_routes(grid=grid, new_substation=new_substation)" + ] + }, + { + "cell_type": "markdown", + "id": "82546c38", + "metadata": {}, + "source": [ + "Check we resolved all contingencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8226a9c1", + "metadata": {}, + "outputs": [], + "source": [ + "print(check_for_capacity_issues(grid))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83099a5e", + "metadata": {}, + "outputs": [], + "source": [ + "visualize(grid) " + ] + }, + { + "cell_type": "markdown", + "id": "a2532f9d-60a9-4fcb-ac23-af6bd3ba47a2", + "metadata": {}, + "source": [ + "*Note: Jupyter notebook only supports one visualizer instance at a time. You might need to restart the kernel and re-run some cells for this final visualizer to work properly. If you do, make sure to not run earlier cells that contain `visualize(grid)`*" + ] + }, + { + "cell_type": "markdown", + "id": "1c2de102", + "metadata": {}, + "source": [ + "# ✅ Wrap-Up\n", + "You’ve just:\n", + "\n", + "- Loaded a grid topology and grid loads from a file\n", + "- Analyse grid components that are or will soon be overloaded using load flow analysis\n", + "- Automatically optimize a solution to relieve (future) congestions on the energy grid\n", + "\n", + "We hope you enjoyed working with Power Grid Model DS and would love to hear your feedback" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/power-grid-model-ds/advanced.ipynb.license b/power-grid-model-ds/advanced.ipynb.license new file mode 100644 index 0000000..7fa378e --- /dev/null +++ b/power-grid-model-ds/advanced.ipynb.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/power-grid-model-ds/data/lines.csv b/power-grid-model-ds/data/lines.csv new file mode 100644 index 0000000..6e2c374 --- /dev/null +++ b/power-grid-model-ds/data/lines.csv @@ -0,0 +1,25 @@ +id,from_node,to_node,from_status,to_status,i_from,r1,x1,c1,tan1,i_n +301,101,1,1,1,0,0.04172331553871052,0.052980486528544264,0.0,0.0,210.28427536474146 +302,1,2,1,1,0,0.15725110380730004,0.022290287012795527,0.0,0.0,105.94268839245518 +303,2,3,1,1,0,0.15725110380730004,0.022290287012795527,0.0,0.0,105.94268839245518 +304,3,4,1,1,0,0.025752163570459393,0.017975680453095257,0.0,0.0,252.81330598432984 +305,4,5,1,1,0,0.184580749945473,0.05679935115815177,0.0,0.0,308.29168286950096 +306,5,6,1,1,0,0.09247280056072502,0.04061706379498719,0.0,0.0,146.12039826984656 +307,6,7,1,1,0,0.09247280056072502,0.04061706379498719,0.0,0.0,146.12039826984656 +308,7,101,1,0,0,0.13774802878276293,0.023316760013206598,0.0,0.0,435.44293210319637 +309,101,8,1,1,0,0.13774802878276293,0.023316760013206598,0.0,0.0,435.44293210319637 +310,8,9,1,1,0,0.03155099592582141,0.1684585911623038,0.0,0.0,386.9416504130695 +311,9,10,1,1,0,0.38045029098187605,0.0063983451879243095,0.0,0.0,595.6252329736612 +312,10,11,1,1,0,0.38045029098187605,0.0063983451879243095,0.0,0.0,595.6252329736612 +313,11,12,1,0,0,0.2711350129219581,0.0239320695659479,0.0,0.0,109.56302502830336 +314,12,13,1,1,0,0.7512185900820556,0.016025736292976843,0.0,0.0,320.73771673448846 +315,13,14,1,1,0,0.2332922408038184,0.010579215551474988,0.0,0.0,221.24699130886745 +316,14,15,1,1,0,0.2884431875887676,0.016206534923606564,0.0,0.0,305.34636498186455 +317,15,16,1,1,0,0.06893013265387658,0.002501770712404082,0.0,0.0,129.78347503297758 +318,16,101,1,1,0,0.17466926178911735,0.04197366431254047,0.0,0.0,223.462291908927 +319,101,17,1,1,0,0.0014230825354318865,0.005366123634368465,0.0,0.0,143.19692487769424 +320,17,18,1,1,0,0.05535894789343593,0.015888270665417342,0.0,0.0,348.82546139151896 +321,18,19,1,1,0,1.1336902294005273,0.0034372075909574274,0.0,0.0,109.4472896415015 +322,19,20,1,1,0,0.02756399437186089,0.001442850824658642,0.0,0.0,315.15332070790726 +323,20,21,1,1,0,0.15000233423633422,0.012783549273682825,0.0,0.0,505.6064937043503 +324,21,101,1,0,0,0.4527663172740473,0.010942584326900677,0.0,0.0,235.29434874845845 diff --git a/power-grid-model-ds/data/lines.csv.license b/power-grid-model-ds/data/lines.csv.license new file mode 100644 index 0000000..7fa378e --- /dev/null +++ b/power-grid-model-ds/data/lines.csv.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/power-grid-model-ds/data/loads.csv b/power-grid-model-ds/data/loads.csv new file mode 100644 index 0000000..6ca1fe4 --- /dev/null +++ b/power-grid-model-ds/data/loads.csv @@ -0,0 +1,22 @@ +id,node,p_specified,q_specified +201,1,75559.96424922015,2119.7561886191743 +202,2,3897910.78949213034,-8632.07774532976 +203,3,95478.41471359684,18229.00620748075 +204,4,-63249.17346947564,-712.0333973476627 +205,5,-64900.69141993268,14743.624463381835 +206,6,348049.77201387816,36026.00482909736 +207,7,21984.741221515593,24426.02990667891 +208,8,-67789.02445002584,32094.560258638798 +209,9,194810.9302324328,4783.500971894942 +210,10,-65128.11331122106,-4447.890651114186 +211,11,143731.66910079535,5949.794993746957 +212,12,224756.36715177132,39976.29538075794 +213,13,80358.3966250818,22745.972387370846 +214,14,313227.5266984018,23551.04963072692 +215,15,234550.7662600798,19216.914424467115 +216,16,293012.8372641854,-9912.346527315487 +217,17,386582.49982988625,-2654.9452654344077 +218,18,-68922.11726350724,13959.630269749312 +219,19,329471.2892289656,-5256.568898739658 +220,20,3828501.06297316383,27023.499817300326 +221,21,55155.355929427635,37523.58862659218 diff --git a/power-grid-model-ds/data/loads.csv.license b/power-grid-model-ds/data/loads.csv.license new file mode 100644 index 0000000..7fa378e --- /dev/null +++ b/power-grid-model-ds/data/loads.csv.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/power-grid-model-ds/data/nodes.csv b/power-grid-model-ds/data/nodes.csv new file mode 100644 index 0000000..eec47d2 --- /dev/null +++ b/power-grid-model-ds/data/nodes.csv @@ -0,0 +1,23 @@ +id,u_rated,node_type,x,y +1,10500.0,0,0,200 +2,10500.0,0,0,400 +3,10500.0,0,0,600 +4,10500.0,0,0,800 +5,10500.0,0,-200,800 +6,10500.0,0,-400,600 +7,10500.0,0,-400,400 +8,10500.0,0,-200,0 +9,10500.0,0,-400,0 +10,10500.0,0,-600,0 +11,10500.0,0,-600,-200 +12,10500.0,0,-600,-400 +13,10500.0,0,-400,-400 +14,10500.0,0,-200,-400 +15,10500.0,0,0,-400 +16,10500.0,0,0,-200 +17,10500.0,0,200,0 +18,10500.0,0,400,0 +19,10500.0,0,600,200 +20,10500.0,0,400,200 +21,10500.0,0,200,200 +101,10500.0,1,0,0 diff --git a/power-grid-model-ds/data/nodes.csv.license b/power-grid-model-ds/data/nodes.csv.license new file mode 100644 index 0000000..7fa378e --- /dev/null +++ b/power-grid-model-ds/data/nodes.csv.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/power-grid-model-ds/helper.py b/power-grid-model-ds/helper.py new file mode 100644 index 0000000..4158cba --- /dev/null +++ b/power-grid-model-ds/helper.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +import pandas as pd +from power_grid_model_ds.arrays import SymLoadArray, SourceArray + +def load_dummy_grid(grid_class): + nodes_csv = pd.read_csv("data/nodes.csv") + loads_csv = pd.read_csv("data/loads.csv") + lines_csv = pd.read_csv("data/lines.csv") + + grid = grid_class.empty() + + nodes = grid.node.__class__( + id=nodes_csv.id.tolist(), + u_rated=nodes_csv.u_rated.tolist(), + node_type=nodes_csv.node_type.tolist(), + x=nodes_csv.x.tolist(), + y=nodes_csv.y.tolist(), + u=[-1.0] * len(nodes_csv), + ) + loads = SymLoadArray( + id=loads_csv.id.tolist(), + node=loads_csv.node.tolist(), + status=[1] * len(loads_csv), + type=[0] * len(loads_csv), + p_specified=loads_csv.p_specified.tolist(), + q_specified=loads_csv.q_specified.tolist(), + ) + lines = grid.line.__class__( + id=lines_csv.id.tolist(), + from_node=lines_csv.from_node.tolist(), + to_node=lines_csv.to_node.tolist(), + from_status=lines_csv.from_status.tolist(), # 1 means active connection from-side, + to_status=lines_csv.to_status.tolist(), # 1 means active connection to-side + i_n=lines_csv.i_n.tolist(), # line max current capacity (e.g., 200 A) + r1=lines_csv.r1.tolist(), # line resistance + x1=lines_csv.x1.tolist(), # line reactance + c1=lines_csv.c1.tolist(), # line capacitance + tan1=lines_csv.tan1.tolist() # line loss tangent + ) + sources = SourceArray( + id=[401], + node=[101], + status=[1], + u_ref=[1.0], + ) + + grid.append(nodes, check_max_id=False) + grid.append(loads, check_max_id=False) + grid.append(lines, check_max_id=False) + grid.append(sources, check_max_id=False) + + grid.set_feeder_ids() + + return grid \ No newline at end of file diff --git a/power-grid-model-ds/input_network.png b/power-grid-model-ds/input_network.png new file mode 100644 index 0000000..6090091 Binary files /dev/null and b/power-grid-model-ds/input_network.png differ diff --git a/power-grid-model-ds/input_network.png.license b/power-grid-model-ds/input_network.png.license new file mode 100644 index 0000000..7fa378e --- /dev/null +++ b/power-grid-model-ds/input_network.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/power-grid-model-ds/input_network_with_overload.png b/power-grid-model-ds/input_network_with_overload.png new file mode 100644 index 0000000..f3f8732 Binary files /dev/null and b/power-grid-model-ds/input_network_with_overload.png differ diff --git a/power-grid-model-ds/input_network_with_overload.png.license b/power-grid-model-ds/input_network_with_overload.png.license new file mode 100644 index 0000000..7fa378e --- /dev/null +++ b/power-grid-model-ds/input_network_with_overload.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/power-grid-model-ds/introduction.ipynb b/power-grid-model-ds/introduction.ipynb new file mode 100644 index 0000000..c219ca0 --- /dev/null +++ b/power-grid-model-ds/introduction.ipynb @@ -0,0 +1,562 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e708f200", + "metadata": {}, + "source": [ + "# Hands-on Workshop: Extending and Modifying a Power Grid Model DS Grid\n", + "\n", + "## Introduction & Setup\n", + "Power Grid Model DS (PGM-DS) is an open-source Python toolkit for modeling electric power grids. It extends the core power-grid-model engine with a user-friendly interface for data science applications​. In PGM-DS, a power network is represented by a Grid dataclass that manages all network components (nodes, lines, transformers, loads, etc.) and ensures their consistency​.\n", + "\n", + "This library provides a graph-based representation of the network and an API to modify the network structure (e.g. adding or removing equipment), which is very useful for simulation studies​. In this workshop, you will perform a series of short exercises to get hands-on experience with two key features of PGM-DS:\n", + "- Extending a grid with custom properties – how to subclass PGM-DS data structures to add your own fields.\n", + "- Modifying a grid object – how to build and alter a grid (add nodes, lines, etc.) programmatically.\n", + "\n", + "We assume you are comfortable with Python and Jupyter notebooks. Before we begin, make sure you have PGM-DS installed:\n", + "\n", + "```bash\n", + "pip install power-grid-model-ds[visualizer]\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86c14151", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install power-grid-model-ds[visualizer] --quiet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd2741c2", + "metadata": {}, + "outputs": [], + "source": [ + "# ⚙️ Setup\n", + "import numpy as np\n", + "from dataclasses import dataclass\n", + "\n", + "from power_grid_model_ds import Grid\n", + "from power_grid_model_ds.arrays import NodeArray, LineArray, SymLoadArray, SourceArray\n", + "from power_grid_model_ds.enums import NodeType\n", + "\n", + "from numpy.typing import NDArray" + ] + }, + { + "cell_type": "markdown", + "id": "b53605bc", + "metadata": {}, + "source": [ + "## Exercise 1: Extending the Grid with Custom Properties\n", + "Context: By default, the PGM-DS Grid includes standard attributes for each component (like node voltage ratings, line impedances, etc.). However, certain project-specific data (for example, simulation results or custom labels) are not included out of the box​. PGM-DS allows you to extend the grid data model by subclassing its array classes to add new columns. In other words, you can inherit the existing functionality and add your own fields​.\n", + "\n", + "In this exercise, we will extend the grid to include an output voltage u for nodes and an output current i_from for lines, which are not present in the basic grid.\n", + "\n", + "### Step 1: Define Extended Node and Line arrays\n", + "We create subclasses of NodeArray and LineArray that include the new properties. We'll call them MyNodeArray and MyLineArray. Each subclass defines a class attribute for the new column and (optionally) a default value for that column via a _defaults dictionary.\n", + "\n", + "**⚙️ Assignment**: Create two Array extensions to hold the x, y, u (extending NodeArray) and i_from (extending LineArray) attributes\n", + "\n", + "**💡 Hint**: https://power-grid-model-ds.readthedocs.io/en/stable/examples/model/grid_extensions_examples.html" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7cfe11a7-80ed-4fe9-8c7f-0daf0ec1c27a", + "metadata": {}, + "outputs": [], + "source": [ + "# Build your solution here..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ada3e60-8bd7-4687-9f29-3ae5ff029228", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/introduction_1_1_define_array_extensions" + ] + }, + { + "cell_type": "markdown", + "id": "8ef5b9fa", + "metadata": {}, + "source": [ + "### Step 2: Create an Extended Grid class\n", + "Now we'll integrate these new arrays into a custom Grid class. We do this by subclassing the PGM-DS Grid and specifying that our grid should use MyNodeArray and MyLineArray instead of the default NodeArray and LineArray. We'll use Python's dataclass to define the new Grid schema:\n", + "\n", + "**⚙️ Assignment**: Create a new grid class that uses the extended arrays.\n", + "\n", + "**💡 Hint**: https://power-grid-model-ds.readthedocs.io/en/stable/examples/model/grid_extensions_examples.html#adding-the-new-arrays-to-the-grid\n", + "\n", + "**💡 Hint**: Make sure to add the `@dataclass` decorator to your grid." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de236a67-1632-4f81-9145-7a904c63746b", + "metadata": {}, + "outputs": [], + "source": [ + "# Build your solution here..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7512d0d-5d85-44b6-b42e-11be0fa6802a", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/introduction_1_2_define_my_grid" + ] + }, + { + "cell_type": "markdown", + "id": "75264a22", + "metadata": {}, + "source": [ + "This ExtendedGrid class inherits all the behavior of Grid but with our extended node and line definitions​. Essentially, we've informed the Grid that whenever it creates or manipulates the node or line arrays, it should use the extended versions that include the extra columns." + ] + }, + { + "cell_type": "markdown", + "id": "430f88b5", + "metadata": {}, + "source": [ + "### Step 3: Initialize an Extended Grid\n", + "With the classes defined, let's create an instance of our extended grid. PGM-DS provides a convenient class method Grid.empty() to initialize an empty grid. We'll call this on our ExtendedGrid:\n", + "\n", + "**⚙️ Assignment**: Instantiate an empty extended grid\n", + "\n", + "**💡 Hint**: https://power-grid-model-ds.readthedocs.io/en/stable/examples/model/grid_examples.html#creating-an-empty-grid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6040e5b", + "metadata": {}, + "outputs": [], + "source": [ + "# Build your solution here..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d5464bf-5689-4919-a85b-b6548b475537", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/introduction_1_3_grid_empty" + ] + }, + { + "cell_type": "markdown", + "id": "06983bc4", + "metadata": {}, + "source": [ + "Verification: To ensure our extended properties exist, you can access the new attributes:\n", + "\n", + "**⚙️ Assignment**: Print some information about the grid.\n", + "\n", + "**💡 Hint**: Be creative! You can use the grid's attributes and methods to get information about the grid. Using `print()` on an array will format it for better readability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee1afd73", + "metadata": {}, + "outputs": [], + "source": [ + "# Build your solution here..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4d131c6-094a-49a2-a283-5bdbd05e961d", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/introduction_1_3_grid_verification" + ] + }, + { + "cell_type": "markdown", + "id": "7eb8b79f", + "metadata": {}, + "source": [ + "Since we haven't added any nodes or lines yet, these arrays are empty" + ] + }, + { + "cell_type": "markdown", + "id": "c0b7b9e7", + "metadata": {}, + "source": [ + "## Exercise 2: Building and Modifying the Grid Structure\n", + "\n", + "Context: The Grid object allows you to add, remove, and manipulate grid components in-memory. We will now construct a simple network step-by-step using our ext_grid. This will demonstrate how to modify a grid object by adding nodes and branches. In practice, you can start with an empty grid and programmatically add substations, lines, transformers, loads, sources, etc., as needed​. We will create a minimal example with two nodes (a source node and a load node) connected by a line, and verify that the grid is updated accordingly. (For brevity, we'll use a very small grid, but the same methods apply to larger networks.)\n", + "\n", + "Note: PGM-DS typically distinguishes node types (e.g., substation vs. regular node) and requires unique IDs for each element. We will manually specify IDs for clarity. The library’s Grid.append() method will be used to add new component records to the grid​.\n" + ] + }, + { + "cell_type": "markdown", + "id": "6b2d3489", + "metadata": {}, + "source": [ + "### Step 1: Add a substation node\n", + "First, let's add a substation node to the grid. We create an MyNodeArray with one entry representing the substation. We need to provide at least an id, a rated voltage (u_rated), and a node type.\n", + "We will use the enum NodeType.SUBSTATION_NODE for the type.\n", + "In this example, we will assign the substation an ID of 101 and a rated voltage of 10500.0 (which could represent 10.5 kV):\n", + "\n", + "**⚙️ Assignment**: Add a substation to the grid.\n", + "\n", + "**💡 Hint**: https://power-grid-model-ds.readthedocs.io/en/stable/examples/model/grid_examples.html#adding-substations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c03c85f4", + "metadata": {}, + "outputs": [], + "source": [ + "# Build your solution here..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c641c07-1cc7-44bc-87ff-49bed38e7d5c", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/introduction_2_1_add_substation" + ] + }, + { + "cell_type": "markdown", + "id": "91ca3ea2", + "metadata": {}, + "source": [ + "Here we constructed a MyNodeArray with one record and then appended it to grid. We set check_max_id=False to disable internal ID checks since we're manually managing IDs in this exercise. After running this, the grid now contains one node. Verification: Check that the node was added. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8695367f", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Nodes in the grid:\")\n", + "print(grid.node)" + ] + }, + { + "cell_type": "markdown", + "id": "88cd0bb5", + "metadata": {}, + "source": [ + "### Step 2: Add a second node\n", + "Next, we'll add another node to represent a load or another bus in the grid. This node will be of a generic type (we'll use NodeType.UNSPECIFIED, which equals 0 in the enum). We'll give it an ID of 102 and the same base voltage (10.5 kV).\n", + "\n", + "**⚙️ Assignment**: Add a node to the grid. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "780b1c35", + "metadata": {}, + "outputs": [], + "source": [ + "# Build your solution here..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f987c7c-ea3b-472c-b9f9-f379a478bc4e", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/introduction_2_2_add_node" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3791cef2", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Nodes in the grid:\")\n", + "print(grid.node)" + ] + }, + { + "cell_type": "markdown", + "id": "a6f17195", + "metadata": {}, + "source": [ + "### Step 3: Add a line connecting the two nodes\n", + "Now that we have two nodes, we will connect them with a line. We'll use our MyLineArray to create a single line record. We need to specify an ID for the line (let's use 201), the from_node and to_node it connects (101 to 102), and statuses to indicate the line is active. We should also provide line electrical parameters (resistance, reactance, etc.) – we'll use some placeholder values here for demonstration:\n", + "\n", + "**⚙️ Assignment**: Add a line to the grid.\n", + "\n", + "**💡 Hint**: https://power-grid-model-ds.readthedocs.io/en/stable/examples/model/grid_examples.html#adding-lines" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4baa9b6", + "metadata": {}, + "outputs": [], + "source": [ + "# Build your solution here..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0211f7ec-0890-4880-b78a-65610e1eaf10", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/introduction_2_3_add_line" + ] + }, + { + "cell_type": "markdown", + "id": "fa1e3f2f", + "metadata": {}, + "source": [ + "This adds a line (ID 201) connecting node 101 to 102. We marked the line as active by setting both from_status and to_status to 1. We also provided some dummy impedance values. The approach of constructing a LineArray (or in our case, MyLineArray) with the necessary fields and appending it to the grid is shown in the official examples​. Verification: Check that the line was added to the grid:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2914592", + "metadata": {}, + "outputs": [], + "source": [ + "print(grid.line)" + ] + }, + { + "cell_type": "markdown", + "id": "f7a5bc49", + "metadata": {}, + "source": [ + "### Step 4: Add a load to the second node\n", + "We'll now add a load connected to node 102. PGM-DS uses a SymLoadArray for symmetrical loads. We will create a single load with an ID of 401 at node 102. We need to specify the node it is attached to, a load type code (we'll use 1 for a basic load type), the specified active (p_specified) and reactive (q_specified) power (let's say 1e6 each, representing 1 MW and 1 Mvar for example), and set its status to active (1):\n", + "\n", + "**⚙️ Assignment**: Add a load to the grid.\n", + "\n", + "**💡 Hint**: https://power-grid-model-ds.readthedocs.io/en/stable/examples/model/grid_examples.html#adding-loads" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ddd3445c", + "metadata": {}, + "outputs": [], + "source": [ + "# Build your solution here..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5499a30c-8820-4066-a322-ee03d5cfb8cf", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/introduction_2_4_add_load" + ] + }, + { + "cell_type": "markdown", + "id": "972efb50", + "metadata": {}, + "source": [ + "This adds one load to node 102. In practice, adding loads ensures that node 102 will be consuming power in any simulation. Verification: Check that the load appears in the grid:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6e900eb", + "metadata": {}, + "outputs": [], + "source": [ + "print(grid.sym_load)" + ] + }, + { + "cell_type": "markdown", + "id": "da53ba14", + "metadata": {}, + "source": [ + "### Step 5: Add a source to the substation node\n", + "Finally, we'll add a power source to supply the grid at the substation (node 101). We'll use SourceArray for this. We'll create a source with ID 501 at node 101, status active (1), and set a reference voltage u_ref. Typically, u_ref might be the slack/reference voltage magnitude or angle; we'll use 0.0 as a reference angle (assuming the default usage):\n", + "\n", + "**⚙️ Assignment**: Add a source to the grid.\n", + "\n", + "**💡 Hint**: https://power-grid-model-ds.readthedocs.io/en/stable/examples/model/grid_examples.html#adding-a-source" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e741dec4", + "metadata": {}, + "outputs": [], + "source": [ + "# Build your solution here..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ab72aec-acc5-42ac-b429-18f608d6bd38", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/introduction_2_5_add_source" + ] + }, + { + "cell_type": "markdown", + "id": "bda9752e", + "metadata": {}, + "source": [ + "This adds a source (e.g., a generator or slack source) at node 101, so the grid now has a supply. Verification: Check the source:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3047ecf6", + "metadata": {}, + "outputs": [], + "source": [ + "print(grid.source)" + ] + }, + { + "cell_type": "markdown", + "id": "e5e6eb1b", + "metadata": {}, + "source": [ + "You should see [501] as the source ID and [101] as the node, indicating the source is at node 101. The count of sources should be 1. Now we have built a simple grid with 2 nodes (101 and 102), 1 line connecting them, 1 load at node 102, and 1 source at node 101. It's good practice to ensure all IDs are unique and there are no inconsistencies. PGM-DS provides a method grid.check_ids() to validate this.\n", + "\n", + "**⚙️ Assignment**: Check whether all IDs are correct.\n", + "\n", + "**💡 Hint**: The grid has a method for that!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b756a4d5", + "metadata": {}, + "outputs": [], + "source": [ + "# Build your solution here..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "403f00f3-d0bd-497a-8b93-8eaba629d68b", + "metadata": {}, + "outputs": [], + "source": [ + "# %load solutions/introduction_2_6_check_ids" + ] + }, + { + "cell_type": "markdown", + "id": "26f831f3", + "metadata": {}, + "source": [ + "If everything is correct, this should execute without errors (it will raise an exception if any duplicate or conflicting IDs were found). We expect no issues since we chose unique IDs for each element type. For a final summary, let's print out the contents of our grid's key components:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c94ab378", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Nodes:\", grid.node.id) # Expect [101 102]\n", + "print(\"Lines:\", grid.line.id) # Expect [201]\n", + "print(\"Loads:\", grid.sym_load.id) # Expect [401]\n", + "print(\"Sources:\", grid.source.id) # Expect [501]" + ] + }, + { + "cell_type": "markdown", + "id": "a1abb080", + "metadata": {}, + "source": [ + "Now we can visualize the resulting network" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49c3e647", + "metadata": {}, + "outputs": [], + "source": [ + "from power_grid_model_ds.visualizer import visualize\n", + "\n", + "visualize(grid)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/power-grid-model-ds/introduction.ipynb.license b/power-grid-model-ds/introduction.ipynb.license new file mode 100644 index 0000000..7fa378e --- /dev/null +++ b/power-grid-model-ds/introduction.ipynb.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0 diff --git a/power-grid-model-ds/solutions/advanced_3_check_for_capacity_issues.py b/power-grid-model-ds/solutions/advanced_3_check_for_capacity_issues.py new file mode 100644 index 0000000..13c9869 --- /dev/null +++ b/power-grid-model-ds/solutions/advanced_3_check_for_capacity_issues.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +from power_grid_model_ds import PowerGridModelInterface + +def check_for_capacity_issues(grid: Grid) -> LineArray: + """Check for capacity issues on the grid. + Return the lines that with capacity issues. + """ + pgm_interface = PowerGridModelInterface(grid) + pgm_interface.calculate_power_flow() + pgm_interface.update_grid() + + return grid.line[grid.line.is_overloaded] + +print(check_for_capacity_issues(grid)) diff --git a/power-grid-model-ds/solutions/advanced_4_build_new_substation.py b/power-grid-model-ds/solutions/advanced_4_build_new_substation.py new file mode 100644 index 0000000..13c9869 --- /dev/null +++ b/power-grid-model-ds/solutions/advanced_4_build_new_substation.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +from power_grid_model_ds import PowerGridModelInterface + +def check_for_capacity_issues(grid: Grid) -> LineArray: + """Check for capacity issues on the grid. + Return the lines that with capacity issues. + """ + pgm_interface = PowerGridModelInterface(grid) + pgm_interface.calculate_power_flow() + pgm_interface.update_grid() + + return grid.line[grid.line.is_overloaded] + +print(check_for_capacity_issues(grid)) diff --git a/power-grid-model-ds/solutions/advanced_5_1_get_all_congested_routes.py b/power-grid-model-ds/solutions/advanced_5_1_get_all_congested_routes.py new file mode 100644 index 0000000..f4f4bd3 --- /dev/null +++ b/power-grid-model-ds/solutions/advanced_5_1_get_all_congested_routes.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +def get_all_congested_routes(grid: Grid) -> list[NodeArray]: + """Get all nodes on routes that contain an overloaded line.""" + grid.set_feeder_ids() + lines_with_congestion = check_for_capacity_issues(grid) + feeder_branch_ids_with_congestion = np.unique(lines_with_congestion['feeder_branch_id']) + return [grid.node.filter(feeder_branch_id=branch_id) for branch_id in feeder_branch_ids_with_congestion] + +congested_routes = get_all_congested_routes(grid) \ No newline at end of file diff --git a/power-grid-model-ds/solutions/advanced_5_2_find_connection_point.py b/power-grid-model-ds/solutions/advanced_5_2_find_connection_point.py new file mode 100644 index 0000000..58e0b63 --- /dev/null +++ b/power-grid-model-ds/solutions/advanced_5_2_find_connection_point.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +def find_connection_point(route: NodeArray, new_substation: NodeArray) -> NodeArray: + """Calculate the connection point for the new route. + This should be the geographically closest node to the new substation. + """ + x_difference = route.x - new_substation.x + y_difference = route.y - new_substation.y + distances = (x_difference**2 + y_difference**2) ** 0.5 + + idx_closest_node = np.argmin(distances) + closest_node = route[idx_closest_node] + return closest_node \ No newline at end of file diff --git a/power-grid-model-ds/solutions/advanced_5_3_connect_to_route.py b/power-grid-model-ds/solutions/advanced_5_3_connect_to_route.py new file mode 100644 index 0000000..f4d1705 --- /dev/null +++ b/power-grid-model-ds/solutions/advanced_5_3_connect_to_route.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +def connect_to_route(grid: Grid, connection_point: NodeArray, new_substation: NodeArray) -> None: + """Connect the new substation node to the connection point. + """ + # Create a new line that connects the two nodes + new_line = MyLineArray( + from_node=[new_substation.id], + to_node=[connection_point.id], + from_status=[0], # status is 0 to make sure the line is not active + to_status=[1], + i_n=[360.0], + r1=[0.05], x1=[0.01], c1=[0.0], tan1=[0.0] + ) + grid.append(new_line) diff --git a/power-grid-model-ds/solutions/advanced_6_optimize_route_transfer.py b/power-grid-model-ds/solutions/advanced_6_optimize_route_transfer.py new file mode 100644 index 0000000..e4db730 --- /dev/null +++ b/power-grid-model-ds/solutions/advanced_6_optimize_route_transfer.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +def optimize_route_transfer(grid: Grid, connection_point: NodeArray, new_substation: NodeArray) -> None: + """Attempt to optimize the route transfer moving the naturally open point (NOP) upstream towards the old substation. + This way, the new substation will take over more nodes of the original route. + """ + old_substation_node_id = connection_point.feeder_node_id.item() + path, _ = grid.graphs.active_graph.get_shortest_path(connection_point.id.item(), old_substation_node_id) + print("Path from overload to old substation:", path) + + current_branch = grid.line.filter( + from_node=[connection_point.id, new_substation.id], + to_node=[connection_point.id, new_substation.id] + ) + for from_node, to_node in zip(path[0:-1], path[1:]): + # Check if the route is still overloaded + capacity_issues = check_for_capacity_issues(grid) + route_capacity_issues = capacity_issues.filter(feeder_branch_id=connection_point.feeder_branch_id) + if not any(route_capacity_issues): + break + + grid.make_active(current_branch) + current_branch = grid.line.filter(from_node=[from_node, to_node], to_node=[from_node, to_node]) + grid.make_inactive(current_branch) + + grid.set_feeder_ids() \ No newline at end of file diff --git a/power-grid-model-ds/solutions/introduction_1_1_define_array_extensions.py b/power-grid-model-ds/solutions/introduction_1_1_define_array_extensions.py new file mode 100644 index 0000000..6682d2b --- /dev/null +++ b/power-grid-model-ds/solutions/introduction_1_1_define_array_extensions.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +class MyNodeArray(NodeArray): + _defaults = {"x": 0.0, "y": 0.0, "u": 0.0} + x: NDArray[np.float64] + y: NDArray[np.float64] + u: NDArray[np.float64] + +class MyLineArray(LineArray): + _defaults = {"i_from": 0.0} + i_from: NDArray[np.float64] \ No newline at end of file diff --git a/power-grid-model-ds/solutions/introduction_1_2_define_my_grid.py b/power-grid-model-ds/solutions/introduction_1_2_define_my_grid.py new file mode 100644 index 0000000..40ea049 --- /dev/null +++ b/power-grid-model-ds/solutions/introduction_1_2_define_my_grid.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +@dataclass +class ExtendedGrid(Grid): + node: MyNodeArray + line: MyLineArray \ No newline at end of file diff --git a/power-grid-model-ds/solutions/introduction_1_3_grid_empty.py b/power-grid-model-ds/solutions/introduction_1_3_grid_empty.py new file mode 100644 index 0000000..e4ff136 --- /dev/null +++ b/power-grid-model-ds/solutions/introduction_1_3_grid_empty.py @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +grid = ExtendedGrid.empty() +grid \ No newline at end of file diff --git a/power-grid-model-ds/solutions/introduction_1_3_grid_verification.py b/power-grid-model-ds/solutions/introduction_1_3_grid_verification.py new file mode 100644 index 0000000..9291ef7 --- /dev/null +++ b/power-grid-model-ds/solutions/introduction_1_3_grid_verification.py @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +print("Node 'u' field exists?", hasattr(grid.node, "u")) +print("Line 'i_from' field exists?", hasattr(grid.line, "i_from")) +print("Node array:", grid.node) \ No newline at end of file diff --git a/power-grid-model-ds/solutions/introduction_2_1_add_substation.py b/power-grid-model-ds/solutions/introduction_2_1_add_substation.py new file mode 100644 index 0000000..023e8ef --- /dev/null +++ b/power-grid-model-ds/solutions/introduction_2_1_add_substation.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +# Create a substation node entry +substation_node = MyNodeArray( + id=[101], + u_rated=[10500.0], # e.g., 10.5 kV base voltage + node_type=[NodeType.SUBSTATION_NODE.value] # type = 1 (Substation node) +) +# Append this node to the grid +grid.append(substation_node, check_max_id=False) \ No newline at end of file diff --git a/power-grid-model-ds/solutions/introduction_2_2_add_node.py b/power-grid-model-ds/solutions/introduction_2_2_add_node.py new file mode 100644 index 0000000..fca1ad6 --- /dev/null +++ b/power-grid-model-ds/solutions/introduction_2_2_add_node.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +# Create another node +load_node = MyNodeArray( + id=[102], + u_rated=[10500.0], + node_type=[NodeType.UNSPECIFIED.value] +) +grid.append(load_node, check_max_id=False) \ No newline at end of file diff --git a/power-grid-model-ds/solutions/introduction_2_3_add_line.py b/power-grid-model-ds/solutions/introduction_2_3_add_line.py new file mode 100644 index 0000000..feafe8a --- /dev/null +++ b/power-grid-model-ds/solutions/introduction_2_3_add_line.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +# Create a line between node 101 and 102 +new_line = MyLineArray( + id=[201], + from_node=[101], + to_node=[102], + from_status=[1], # 1 = active from-side, + to_status=[1], # 1 = active to-side (both ends active) + i_n=[200.0], # line current capacity (e.g., 200 A) + r1=[0.1], x1=[0.03], # line resistance and reactance + c1=[0.0], tan1=[0.0] # line capacitance and loss tangent (0 for simplicity) +) +grid.append(new_line, check_max_id=False) \ No newline at end of file diff --git a/power-grid-model-ds/solutions/introduction_2_4_add_load.py b/power-grid-model-ds/solutions/introduction_2_4_add_load.py new file mode 100644 index 0000000..ffd72cc --- /dev/null +++ b/power-grid-model-ds/solutions/introduction_2_4_add_load.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +# Create a load at node 102 +load = SymLoadArray( + id=[401], + node=[102], + type=[1], # load type (e.g., 1 for constant power load) + p_specified=[1_000_000.0], # 1e6 W = 1 MW + q_specified=[1_000_000.0], # 1e6 VAR + status=[1] # active +) +grid.append(load, check_max_id=False) \ No newline at end of file diff --git a/power-grid-model-ds/solutions/introduction_2_5_add_source.py b/power-grid-model-ds/solutions/introduction_2_5_add_source.py new file mode 100644 index 0000000..08384e1 --- /dev/null +++ b/power-grid-model-ds/solutions/introduction_2_5_add_source.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +# Create a source at node 101 +source = SourceArray( + id=[501], + node=[101], + status=[1], + u_ref=[1.0] +) +grid.append(source, check_max_id=False) \ No newline at end of file diff --git a/power-grid-model-ds/solutions/introduction_2_6_check_ids.py b/power-grid-model-ds/solutions/introduction_2_6_check_ids.py new file mode 100644 index 0000000..d8a1bc7 --- /dev/null +++ b/power-grid-model-ds/solutions/introduction_2_6_check_ids.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 Contributors to the Power Grid Model project +# +# SPDX-License-Identifier: MPL-2.0 + +grid.check_ids() \ No newline at end of file