diff --git a/docs/examples/Generic Branch Example.ipynb b/docs/examples/Generic Branch Example.ipynb new file mode 100644 index 000000000..477b85fda --- /dev/null +++ b/docs/examples/Generic Branch Example.ipynb @@ -0,0 +1,1069 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Generic Branch Examples" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generic Branch Representation of a Three-winding Transformer \n", + "\n", + "The **Generic Branch** component can be used to model three-winding transformers by decomposing them into three interconnected two-winding transformers. Each of these branches is characterized by its electrical parameters, such as resistance, reactance, and admittance." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Network:\n", + "\n", + "\n", + " 5,Source -->|---Line, Br12----|\n", + " |\n", + " ------ 1, HV\n", + " |\n", + " 0 Br8, T1\n", + " 0\n", + " | 4, AUX\n", + " 2,MV x--------x---------x 3, LV\n", + " | |\n", + " 0 Br9,T2 0 Br10, T3\n", + " 0 0\n", + " | | \n", + " ---- -----> 7, Load\n", + " |\n", + " |\n", + " | Line, Br11\n", + " |\n", + " |\n", + " ----- 11, Station1\n", + " |\n", + " ----> 6, Load \n", + "\n", + "\n", + "\n", + "\n", + "========================================\n", + " Input Data\n", + "========================================\n", + "\n", + "node data\n", + "---------\n", + " id Name Voltage [V]\n", + " 1 HV 200.0\n", + " 2 MV 150.0\n", + " 3 LV 30.0\n", + " 4 AUX 200.0\n", + " 11 Station1 150.0\n", + " 14 Source 200.0\n", + "\n", + "\n", + "\n", + "generic branch data\n", + "-------------------\n", + " id Name from_node to_node from_status to_status R [Ohm] X [Ohm] G [S] B [S] k theta S [MVA]\n", + " 8 BHV 1 4 1 1 0.13 16.39 0.0 -0.0 1.0 0.0 450.0\n", + " 9 BMV 4 2 1 1 0.04 -0.89 0.0 -0.0 1.0 0.0 450.0\n", + " 10 BLV 4 3 1 1 0.01 0.76 0.0 -0.0 1.0 0.0 100.0\n", + " 12 BLine2 2 11 1 1 0.50 2.00 0.0 0.0 1.0 0.0 100.0\n", + " 13 BLine1 14 1 1 1 0.50 2.00 0.0 0.0 1.0 0.0 450.0\n", + "\n", + "load data\n", + "---------\n", + " id Name node status type P [W] Q [Var]\n", + " 6 L1 11 1 0 30.0 5.00\n", + " 7 L2 3 1 0 1.5 0.15\n", + "\n", + "source data\n", + "-----------\n", + " id Name node status Voltage [pu] u_ref_angle sk rx_ratio z01_ratio\n", + " 5 G 14 1 1.0 NaN 1.000000e+40 NaN NaN\n", + "\n", + "\n", + "========================================\n", + " Output Data\n", + "========================================\n", + "\n", + "node data\n", + "---------\n", + " id Name Voltage [pu] Voltage [kV] Angle [°] P [MW] Q [MVAr]\n", + " 1 HV 0.999 199.864 -0.002 0.00 0.00\n", + " 2 MV 0.997 149.571 -0.013 0.00 -0.00\n", + " 3 LV 0.997 30.104 -0.016 -1.50 -0.15\n", + " 4 AUX 0.997 199.398 -0.014 -0.00 0.00\n", + " 11 Station1 0.996 149.403 -0.016 -30.00 -5.00\n", + " 14 Source 1.000 200.000 -0.000 31.65 5.70\n", + "\n", + "generic branch data\n", + "-------------------\n", + " id Name From Node To Node Loading [%] From P [MW] From Q [MVAr] To P [MW] To Q [MVAr]\n", + " 8 BHV 1 4 7.1 31.64 5.65 -31.60 -5.22\n", + " 9 BMV 4 2 6.8 30.07 5.06 -30.02 -5.08\n", + " 10 BLV 4 3 1.5 1.53 0.16 -1.50 -0.15\n", + " 12 BLine2 2 11 30.4 30.02 5.08 -30.00 -5.00\n", + " 13 BLine1 14 1 7.1 31.65 5.70 -31.64 -5.65\n", + "\n", + "Total Power for all Branches\n", + "----------------------------\n", + "Total Active Power (P_total): 0.15 MW\n", + "Total Reactive Power (Q_total): 0.55 MVAr\n" + ] + }, + { + "data": { + "text/plain": [ + "(np.float64(0.1524792857302799), np.float64(0.5505383338017147))" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "import warnings\n", + "\n", + "with warnings.catch_warnings():\n", + " warnings.simplefilter(\"ignore\", category=DeprecationWarning)\n", + " # Suppress warning about pyarrow as future required dependency\n", + "\n", + "import pandas as pd\n", + "\n", + "from power_grid_model import LoadGenType, ComponentType, DatasetType\n", + "from power_grid_model import PowerGridModel, CalculationMethod, CalculationType\n", + "from power_grid_model import initialize_array\n", + "from power_grid_model.validation import assert_valid_input_data\n", + "\n", + "# network\n", + "network = \"\"\"\n", + "\n", + " 5,Source -->|---Line, Br12----|\n", + " |\n", + " ------ 1, HV\n", + " |\n", + " 0 Br8, T1\n", + " 0\n", + " | 4, AUX\n", + " 2,MV x--------x---------x 3, LV\n", + " | |\n", + " 0 Br9,T2 0 Br10, T3\n", + " 0 0\n", + " | | \n", + " ---- -----> 7, Load\n", + " |\n", + " |\n", + " | Line, Br11\n", + " |\n", + " |\n", + " ----- 11, Station1\n", + " |\n", + " ----> 6, Load \n", + "\n", + "\n", + "\n", + "\"\"\"\n", + "\n", + "\n", + "# Spannungsebenen des Transformators\n", + "u_rated = {\"HV\": 200e3, \"MV\": 150e3, \"LV\": 30.2e3, \"AUX\": 200e3, \"Station1\": 150e3, \"Source\": 200e3}\n", + "\n", + "# Eingespeiste Leistung (200 kV) auf der 380 kV Seite\n", + "source_voltage = u_rated[\"Source\"] # 200 kV\n", + "source_power = 1e40 # hohe Kurzschlussleistung -> v_pu = 1.0\n", + "\n", + "# Lasten\n", + "load_MV = {\"P\": 30e6, \"Q\": 5e6}\n", + "load_LV = {\"P\": 1.5e6, \"Q\": 0.15e6} # Eigenbedarf\n", + "\n", + "# Zuweisung der Knotennummern\n", + "nodes = {\"HV\": 1, \"MV\": 2, \"LV\": 3, \"AUX\": 4, \"Station1\": 11, \"Source\": 14}\n", + "non = len(nodes) # number of nodes\n", + "# tap-changer\n", + "v_tap = 6.0e3\n", + "\n", + "# Generiere Knoten\n", + "node_data = initialize_array(DatasetType.input, ComponentType.node, non)\n", + "node_data[\"id\"] = list(nodes.values())\n", + "node_data[\"u_rated\"] = list(u_rated.values())\n", + "\n", + "\n", + "# node_data[\"id\"] = [nodes['HV'], nodes['MV'], nodes['LV'], nodes['AUX'], nodes['Station1'],nodes['Source'] ]\n", + "\n", + "# node_data[\"u_rated\"] = [u_rated['HV'], u_rated['MV'], u_rated['LV'], u_rated['AUX'],u_rated['Station1'] ]\n", + "\n", + "# Quelle auf der 380kV-Seite (Slack)\n", + "source_data = initialize_array(DatasetType.input, ComponentType.source, 1)\n", + "source_data[\"id\"] = [5]\n", + "source_data[\"node\"] = [nodes[\"Source\"]]\n", + "source_data[\"status\"] = [1]\n", + "source_data[\"u_ref\"] = [1.0]\n", + "source_data[\"sk\"] = [source_power]\n", + "\n", + "# Last auf der 110kV-Seite\n", + "load_data = initialize_array(DatasetType.input, ComponentType.sym_load, 2)\n", + "load_data[\"id\"] = [6, 7]\n", + "load_data[\"type\"] = [LoadGenType.const_power, LoadGenType.const_power]\n", + "load_data[\"node\"] = [nodes[\"Station1\"], nodes[\"LV\"]]\n", + "load_data[\"p_specified\"] = [load_MV[\"P\"], load_LV[\"P\"]]\n", + "load_data[\"q_specified\"] = [load_MV[\"Q\"], load_LV[\"Q\"]]\n", + "load_data[\"status\"] = [1, 1]\n", + "\n", + "node_name = [\"HV\", \"MV\", \"LV\", \"AUX\", \"Station1\", \"Source\"]\n", + "branch_name = [\"BHV\", \"BMV\", \"BLV\", \"BLine2\", \"BLine1\"]\n", + "\n", + "# Generic Branch für jeden der zwei Wickler\n", + "branch_data = initialize_array(DatasetType.input, ComponentType.generic_branch, 5)\n", + "\n", + "# Verbindungen der zwei Wickler\n", + "branch_data[\"id\"] = [8, 9, 10, 12, 13]\n", + "branch_data[\"from_node\"] = [nodes[\"HV\"], nodes[\"AUX\"], nodes[\"AUX\"], nodes[\"MV\"], nodes[\"Source\"]]\n", + "branch_data[\"to_node\"] = [nodes[\"AUX\"], nodes[\"MV\"], nodes[\"LV\"], nodes[\"Station1\"], nodes[\"HV\"]]\n", + "branch_data[\"from_status\"] = [1, 1, 1, 1, 1]\n", + "branch_data[\"to_status\"] = [1, 1, 1, 1, 1]\n", + "\n", + "# T1 T2 T3 Line2 Line 1\n", + "branch_data[\"r1\"] = [0.129059, 0.039528, 0.013811, 0.5, 0.5]\n", + "branch_data[\"x1\"] = [16.385859, -0.889632, 0.757320, 2.0, 2.0]\n", + "branch_data[\"g1\"] = [8.692e-7, 0.000002, 0.000038, 0.0, 0.0]\n", + "branch_data[\"b1\"] = [-2.336e-7, -4.152e-7, -0.00001, 0.0, 0.0]\n", + "branch_data[\"k\"] = [1.0, 1.0, 1.0, 1.0, 1.0]\n", + "branch_data[\"theta\"] = [0.0, 0.0, 0.0, 0.0, 0.0]\n", + "branch_data[\"sn\"] = [450e6, 450e6, 100e6, 100e6, 450e6]\n", + "\n", + "# Input-Daten sammeln\n", + "input_data = {\n", + " ComponentType.node: node_data,\n", + " ComponentType.source: source_data,\n", + " ComponentType.sym_load: load_data,\n", + " ComponentType.generic_branch: branch_data,\n", + "}\n", + "\n", + "# Überprüfung der Eingabedaten\n", + "assert_valid_input_data(input_data=input_data, calculation_type=CalculationType.power_flow)\n", + "\n", + "# Power-Flow Modell erstellen und Berechnungen durchführen\n", + "model = PowerGridModel(input_data)\n", + "output_data = model.calculate_power_flow(\n", + " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.newton_raphson\n", + ")\n", + "\n", + "e3 = lambda x: x / 1e3\n", + "e6 = lambda x: x / 1e6\n", + "percent = lambda x: x * 100\n", + "\n", + "\n", + "def print_node_input(input_data):\n", + " node_in = input_data[ComponentType.node]\n", + " df = pd.DataFrame(node_in)\n", + " df.insert(1, \"Name\", node_name)\n", + "\n", + " map_column(df, \"u_rated\", rename_to=\"Voltage [V]\", unit_fn=e3, round_dec=0)\n", + "\n", + " print(\"\\nnode data\")\n", + " print(\"---------\")\n", + " print(df.to_string(index=False))\n", + " print(\"\\n\")\n", + "\n", + "\n", + "def print_branch_input(input_data):\n", + " genb_in = input_data[ComponentType.generic_branch]\n", + " df = pd.DataFrame(genb_in)\n", + " df.insert(1, \"Name\", branch_name)\n", + "\n", + " map_column(df, \"r1\", rename_to=\"R [Ohm]\", round_dec=2)\n", + " map_column(df, \"x1\", rename_to=\"X [Ohm]\", round_dec=2)\n", + " map_column(df, \"g1\", rename_to=\"G [S]\", round_dec=2)\n", + " map_column(df, \"b1\", rename_to=\"B [S]\", round_dec=2)\n", + " map_column(df, \"sn\", rename_to=\"S [MVA]\", unit_fn=e6, round_dec=2)\n", + "\n", + " print(\"\\ngeneric branch data\")\n", + " print(\"-------------------\")\n", + " print(df.to_string(index=False))\n", + "\n", + "\n", + "def print_load_input(input_data):\n", + " sym_load_in = input_data[ComponentType.sym_load]\n", + " df = pd.DataFrame(sym_load_in)\n", + " df.insert(1, \"Name\", [\"L1\", \"L2\"])\n", + "\n", + " map_column(df, \"p_specified\", rename_to=\"P [W]\", unit_fn=e6, round_dec=3)\n", + " map_column(df, \"q_specified\", rename_to=\"Q [Var]\", unit_fn=e6, round_dec=3)\n", + "\n", + " print(\"\\nload data\")\n", + " print(\"---------\")\n", + " print(df.to_string(index=False))\n", + "\n", + "\n", + "def print_source_input(input_data):\n", + " source_in = input_data[ComponentType.source]\n", + " df = pd.DataFrame(source_in)\n", + " df.insert(1, \"Name\", [\"G\"])\n", + "\n", + " map_column(df, \"u_ref\", rename_to=\"Voltage [pu]\", round_dec=3)\n", + "\n", + " print(\"\\nsource data\")\n", + " print(\"-----------\")\n", + " print(df.to_string(index=False))\n", + " print(\"\\n\")\n", + "\n", + "\n", + "def print_node_output(output_data):\n", + " node_out = output_data[ComponentType.node]\n", + "\n", + " df = pd.DataFrame(node_out)\n", + " df.insert(1, \"Name\", node_name)\n", + "\n", + " map_column(df, \"u_pu\", rename_to=\"Voltage [pu]\", round_dec=3)\n", + " map_column(df, \"u\", rename_to=\"Voltage [kV]\", unit_fn=e3, round_dec=3)\n", + " map_column(df, \"u_angle\", rename_to=\"Angle [°]\", round_dec=3)\n", + " map_column(df, \"p\", rename_to=\"P [MW]\", unit_fn=e6, round_dec=2)\n", + " map_column(df, \"q\", rename_to=\"Q [MVAr]\", unit_fn=e6, round_dec=2)\n", + "\n", + " df.drop([\"energized\"], axis=1, inplace=True)\n", + "\n", + " print(\"\\nnode data\")\n", + " print(\"---------\")\n", + " print(df.to_string(index=False))\n", + "\n", + "\n", + "def print_branch_output(output_data, input_data):\n", + " genb_out = output_data[ComponentType.generic_branch]\n", + " genb_in = input_data[ComponentType.generic_branch]\n", + " df = pd.DataFrame(genb_out)\n", + " df.insert(1, \"Name\", branch_name)\n", + " df.insert(2, \"From Node\", list(genb_in[\"from_node\"]))\n", + " df.insert(3, \"To Node\", list(genb_in[\"to_node\"]))\n", + "\n", + " map_column(df, \"loading\", rename_to=\"Loading [%]\", unit_fn=percent, round_dec=1)\n", + " map_column(df, \"p_from\", rename_to=\"From P [MW]\", unit_fn=e6, round_dec=2)\n", + " map_column(df, \"q_from\", rename_to=\"From Q [MVAr]\", unit_fn=e6, round_dec=2)\n", + " map_column(df, \"p_to\", rename_to=\"To P [MW]\", unit_fn=e6, round_dec=2)\n", + " map_column(df, \"q_to\", rename_to=\"To Q [MVAr]\", unit_fn=e6, round_dec=2)\n", + "\n", + " df.drop([\"energized\", \"i_from\", \"s_from\", \"i_to\", \"s_to\"], axis=1, inplace=True)\n", + "\n", + " print(\"\\ngeneric branch data\")\n", + " print(\"-------------------\")\n", + " print(df.to_string(index=False))\n", + "\n", + "\n", + "def map_column(df, col_name, rename_to=None, unit_fn=None, round_dec=None):\n", + " col = df[col_name] if unit_fn is None else unit_fn(df[col_name])\n", + " col = col if round_dec is None else col.round(round_dec)\n", + " df[col_name] = col\n", + " df.rename(columns={col_name: rename_to}, inplace=True)\n", + " return df\n", + "\n", + "\n", + "def calculate_total_power(output_data):\n", + " genb_out = output_data[ComponentType.generic_branch]\n", + " df = pd.DataFrame(genb_out)\n", + "\n", + " # Summiere die aktive und reaktive Leistung über alle Branches hinweg\n", + " P_total = e6(df[\"p_from\"].sum()) + e6(df[\"p_to\"].sum())\n", + " Q_total = e6(df[\"q_from\"].sum()) + e6(df[\"q_to\"].sum())\n", + "\n", + " # Ausgabe der Gesamtsummen\n", + " print(\"\\nTotal Power for all Branches\")\n", + " print(\"----------------------------\")\n", + " print(f\"Total Active Power (P_total): {P_total:.2f} MW\")\n", + " print(f\"Total Reactive Power (Q_total): {Q_total:.2f} MVAr\")\n", + "\n", + " return P_total, Q_total\n", + "\n", + "\n", + "print(\"Network:\")\n", + "print(network)\n", + "\n", + "print(\"========================================\")\n", + "print(\" Input Data\")\n", + "print(\"========================================\")\n", + "print_node_input(input_data)\n", + "print_branch_input(input_data)\n", + "print_load_input(input_data)\n", + "print_source_input(input_data)\n", + "\n", + "\n", + "# Ausgabe der Spannungen und Ströme\n", + "print(\"========================================\")\n", + "print(\" Output Data\")\n", + "print(\"========================================\")\n", + "print_node_output(output_data)\n", + "print_branch_output(output_data, input_data)\n", + "\n", + "calculate_total_power(output_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generic Branch Representation for Line, Transformer and PST\n", + "\n", + "In this example, the Generic Branch is used as a Phase Shifting Transformer (PST) to optimize the loading of cable L34. Depending on the shift angle \\( \\theta \\), the loading of the cable can be adjusted. With a shift angle of -0.1 rad, the cable loading is approximately 80%, while at a shift angle of 0.0 rad, it drops to just 53%." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generic Branch Example\n", + "Network:\n", + "\n", + "\n", + " (100km) (100km) B5,230kV\n", + " L 100MW <-|----L46------------| |-----------L45---------------|-> L 100 MW\n", + " B6,230kV ----- B4 |-\n", + " | |\n", + " L34 (cheap kabel, 40km) |\n", + " | L57 (100km)\n", + " ----- B3 |\n", + " | |\n", + " (100km) PST23 (100km) |-\n", + " L 200MW <-|-----L28-----------| | |----------L27---------------|-> L 200 MW\n", + " B8,230kV ------ B2,230kV B7,230kV\n", + " |\n", + " T12 Transformer\n", + " |\n", + " ----- B1, 15.5kV\n", + " ^\n", + " |\n", + " G (*Slack)\n", + "\n", + "\n", + "========================================\n", + " Input Data\n", + "========================================\n", + "\n", + "node data\n", + "---------\n", + " id Name Voltage [V]\n", + " 1 B1 16.0\n", + " 2 B2 230.0\n", + " 3 B3 230.0\n", + " 4 B4 230.0\n", + " 5 B5 230.0\n", + " 6 B6 230.0\n", + " 7 B7 230.0\n", + " 8 B8 230.0\n", + "\n", + "\n", + "\n", + "generic branch data\n", + "-------------------\n", + " id Name from_node to_node from_status to_status R [Ohm] X [Ohm] G [S] B [S] k theta S [MVA]\n", + " 14 L27 2 7 1 1 2.0 8.0 0.0 0.0 1.0 0.0 500.0\n", + " 15 L28 2 8 1 1 2.0 8.0 0.0 0.0 1.0 0.0 500.0\n", + " 16 L45 4 5 1 1 2.0 8.0 0.0 0.0 1.0 0.0 500.0\n", + " 17 L46 4 6 1 1 2.0 8.0 0.0 0.0 1.0 0.0 500.0\n", + " 18 L57 5 7 1 1 2.0 8.0 0.0 0.0 1.0 0.0 250.0\n", + " 19 L34 3 4 1 1 4.6 3.4 0.0 0.0 1.0 0.0 500.0\n", + " 20 T12 1 2 1 1 0.5 2.0 0.0 0.0 1.0 0.0 1000.0\n", + " 21 PST23 2 3 1 1 0.0 4.0 0.0 0.0 1.0 -0.1 1000.0\n", + "\n", + "load data\n", + "---------\n", + " id Name node status type P [W] Q [Var]\n", + " 9 L1 5 1 0 100.0 0.0\n", + " 10 L2 6 1 0 100.0 0.0\n", + " 11 L3 7 1 0 200.0 0.0\n", + " 12 L4 8 1 0 200.0 0.0\n", + "\n", + "source data\n", + "-----------\n", + " id Name node status Voltage [pu] u_ref_angle sk rx_ratio z01_ratio\n", + " 13 G 1 1 1.0 NaN 1.000000e+40 NaN NaN\n", + "\n", + "\n", + "========================================\n", + " Output Data\n", + "========================================\n", + "\n", + "node data\n", + "---------\n", + " id Name Voltage [pu] Voltage [kV] Angle [°] P [MW] Q [MVAr]\n", + " 1 B1 1.000 15.500 -0.000 619.35 -160.47\n", + " 2 B2 1.001 230.120 -0.025 -0.00 -0.00\n", + " 3 B3 1.017 233.948 0.050 0.00 0.00\n", + " 4 B4 0.997 229.217 0.019 0.00 0.00\n", + " 5 B5 0.992 228.208 -0.016 -100.00 -0.00\n", + " 6 B6 0.993 228.315 0.004 -100.00 -0.00\n", + " 7 B7 0.992 228.259 -0.036 -200.00 0.00\n", + " 8 B8 0.992 228.261 -0.055 -200.00 0.00\n", + "\n", + "generic branch data\n", + "-------------------\n", + " id Name From Node To Node Loading [%] From P [MW] From Q [MVAr] To P [MW] To Q [MVAr]\n", + " 14 L27 2 7 17.4 79.91 33.96 -79.63 -32.82\n", + " 15 L28 2 8 40.3 201.54 6.14 -200.00 0.00\n", + " 16 L45 4 5 44.8 222.88 -22.79 -220.97 30.43\n", + " 17 L46 4 6 20.1 100.38 1.53 -100.00 -0.00\n", + " 18 L57 5 7 49.9 120.97 -30.43 -120.37 32.82\n", + " 19 L34 3 4 80.9 334.03 -228.00 -323.26 21.25\n", + " 20 T12 1 2 64.0 619.35 -160.47 -615.48 175.94\n", + " 21 PST23 2 3 40.4 334.03 -216.05 -334.03 228.00\n", + "\n", + "Total Power for all Branches\n", + "----------------------------\n", + "Total Active Power (P_total): 19.35 MW\n", + "Total Reactive Power (Q_total): -160.47 MVAr\n" + ] + }, + { + "data": { + "text/plain": [ + "(np.float64(19.347916682930418), np.float64(-160.46709070282753))" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# some basic imports\n", + "import numpy as np\n", + "import warnings\n", + "\n", + "with warnings.catch_warnings():\n", + " warnings.simplefilter(\"ignore\", category=DeprecationWarning)\n", + " # Suppress warning about pyarrow as future required dependency\n", + "\n", + "import pandas as pd\n", + "\n", + "from power_grid_model import LoadGenType, ComponentType, DatasetType\n", + "from power_grid_model import PowerGridModel, CalculationMethod, CalculationType\n", + "from power_grid_model import initialize_array\n", + "from power_grid_model.validation import assert_valid_input_data\n", + "\n", + "# network\n", + "network = \"\"\"\n", + "\n", + " (100km) (100km) B5,230kV\n", + " L 100MW <-|----L46------------| |-----------L45---------------|-> L 100 MW\n", + " B6,230kV ----- B4 |-\n", + " | |\n", + " L34 (cheap kabel, 40km) |\n", + " | L57 (100km)\n", + " ----- B3 |\n", + " | |\n", + " (100km) PST23 (100km) |-\n", + " L 200MW <-|-----L28-----------| | |----------L27---------------|-> L 200 MW\n", + " B8,230kV ------ B2,230kV B7,230kV\n", + " |\n", + " T12 Transformer\n", + " |\n", + " ----- B1, 15.5kV\n", + " ^\n", + " |\n", + " G (*Slack)\n", + "\n", + "\"\"\"\n", + "# node\n", + "node = initialize_array(DatasetType.input, ComponentType.node, 8)\n", + "node[\"id\"] = [1, 2, 3, 4, 5, 6, 7, 8]\n", + "node[\"u_rated\"] = [15.5e3, 230e3, 230e3, 230e3, 230e3, 230e3, 230e3, 230e3]\n", + "\n", + "node_name = [\"B1\", \"B2\", \"B3\", \"B4\", \"B5\", \"B6\", \"B7\", \"B8\"]\n", + "branch_name = [\"L27\", \"L28\", \"L45\", \"L46\", \"L57\", \"L34\", \"T12\", \"PST23\"]\n", + "\n", + "# load\n", + "sym_load = initialize_array(DatasetType.input, ComponentType.sym_load, 4)\n", + "sym_load[\"id\"] = [9, 10, 11, 12]\n", + "sym_load[\"node\"] = [5, 6, 7, 8]\n", + "sym_load[\"status\"] = [1, 1, 1, 1]\n", + "sym_load[\"type\"] = [LoadGenType.const_power]\n", + "sym_load[\"p_specified\"] = [100e6, 100e6, 200e6, 200e6]\n", + "sym_load[\"q_specified\"] = [0.0, 0.0, 0.0, 0.0]\n", + "\n", + "# source\n", + "source = initialize_array(DatasetType.input, ComponentType.source, 1)\n", + "source[\"id\"] = [13]\n", + "source[\"node\"] = [1]\n", + "source[\"status\"] = [1]\n", + "source[\"u_ref\"] = [1.0]\n", + "source[\"sk\"] = [1e40]\n", + "\n", + "# ZBase T12 = 13.5^2/1000 -> Z =\n", + "# ZBase = 230*230/1000 = 52.9\n", + "# generic_branch\n", + "theta_pst = -0.1\n", + "gb = initialize_array(DatasetType.input, ComponentType.generic_branch, 8)\n", + "# L27 L28 L45 L46 L57 L34 T12 P23\n", + "gb[\"id\"] = [14, 15, 16, 17, 18, 19, 20, 21]\n", + "gb[\"from_node\"] = [2, 2, 4, 4, 5, 3, 1, 2]\n", + "gb[\"to_node\"] = [7, 8, 5, 6, 7, 4, 2, 3]\n", + "gb[\"from_status\"] = [1, 1, 1, 1, 1, 1, 1, 1]\n", + "gb[\"to_status\"] = [1, 1, 1, 1, 1, 1, 1, 1]\n", + "gb[\"r1\"] = [2.0, 2.0, 2.0, 2.0, 2.0, 4.6, 0.5, 0.0]\n", + "gb[\"x1\"] = [8.0, 8.0, 8.0, 8.0, 8.0, 3.4, 2.0, 4.0]\n", + "gb[\"g1\"] = [0.0, 0.0, 0.0, 0.0, 0.0, 4e-6, 0.0, 0.0]\n", + "gb[\"b1\"] = [0.0, 0.0, 0.0, 0.0, 0.0, 40e-4, 0.0, 0.0]\n", + "gb[\"k\"] = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]\n", + "gb[\"theta\"] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, theta_pst]\n", + "gb[\"sn\"] = [500e6, 500e6, 500e6, 500e6, 250e6, 500e6, 1000e6, 1000e6]\n", + "\n", + "# all\n", + "input_data = {\n", + " ComponentType.node: node,\n", + " ComponentType.generic_branch: gb,\n", + " ComponentType.sym_load: sym_load,\n", + " ComponentType.source: source,\n", + "}\n", + "\n", + "assert_valid_input_data(input_data=input_data, calculation_type=CalculationType.power_flow)\n", + "\n", + "model = PowerGridModel(input_data)\n", + "\n", + "output_data = model.calculate_power_flow(\n", + " symmetric=True, error_tolerance=1e-8, max_iterations=20, calculation_method=CalculationMethod.newton_raphson\n", + ")\n", + "\n", + "\n", + "pd.set_option(\"display.max_columns\", None)\n", + "pd.set_option(\"display.max_rows\", None)\n", + "pd.set_option(\"display.expand_frame_repr\", False)\n", + "pd.set_option(\"display.max_columns\", None)\n", + "pd.set_option(\"display.max_colwidth\", None)\n", + "\n", + "e3 = lambda x: x / 1e3\n", + "e6 = lambda x: x / 1e6\n", + "percent = lambda x: x * 100\n", + "\n", + "\n", + "def print_node_input(input_data):\n", + " node_in = input_data[ComponentType.node]\n", + " df = pd.DataFrame(node_in)\n", + " df.insert(1, \"Name\", node_name)\n", + "\n", + " map_column(df, \"u_rated\", rename_to=\"Voltage [V]\", unit_fn=e3, round_dec=0)\n", + "\n", + " print(\"\\nnode data\")\n", + " print(\"---------\")\n", + " print(df.to_string(index=False))\n", + " print(\"\\n\")\n", + "\n", + "\n", + "def print_branch_input(input_data):\n", + " genb_in = input_data[ComponentType.generic_branch]\n", + " df = pd.DataFrame(genb_in)\n", + " df.insert(1, \"Name\", branch_name)\n", + "\n", + " map_column(df, \"r1\", rename_to=\"R [Ohm]\", round_dec=2)\n", + " map_column(df, \"x1\", rename_to=\"X [Ohm]\", round_dec=2)\n", + " map_column(df, \"g1\", rename_to=\"G [S]\", round_dec=2)\n", + " map_column(df, \"b1\", rename_to=\"B [S]\", round_dec=2)\n", + " map_column(df, \"sn\", rename_to=\"S [MVA]\", unit_fn=e6, round_dec=2)\n", + "\n", + " print(\"\\ngeneric branch data\")\n", + " print(\"-------------------\")\n", + " print(df.to_string(index=False))\n", + "\n", + "\n", + "def print_load_input(input_data):\n", + " sym_load_in = input_data[ComponentType.sym_load]\n", + " df = pd.DataFrame(sym_load_in)\n", + " df.insert(1, \"Name\", [\"L1\", \"L2\", \"L3\", \"L4\"])\n", + "\n", + " map_column(df, \"p_specified\", rename_to=\"P [W]\", unit_fn=e6, round_dec=3)\n", + " map_column(df, \"q_specified\", rename_to=\"Q [Var]\", unit_fn=e6, round_dec=3)\n", + "\n", + " print(\"\\nload data\")\n", + " print(\"---------\")\n", + " print(df.to_string(index=False))\n", + "\n", + "\n", + "def print_source_input(input_data):\n", + " source_in = input_data[ComponentType.source]\n", + " df = pd.DataFrame(source_in)\n", + " df.insert(1, \"Name\", [\"G\"])\n", + "\n", + " map_column(df, \"u_ref\", rename_to=\"Voltage [pu]\", round_dec=3)\n", + "\n", + " print(\"\\nsource data\")\n", + " print(\"-----------\")\n", + " print(df.to_string(index=False))\n", + " print(\"\\n\")\n", + "\n", + "\n", + "def print_node_output(output_data):\n", + " node_out = output_data[ComponentType.node]\n", + "\n", + " df = pd.DataFrame(node_out)\n", + " df.insert(1, \"Name\", node_name)\n", + "\n", + " map_column(df, \"u_pu\", rename_to=\"Voltage [pu]\", round_dec=3)\n", + " map_column(df, \"u\", rename_to=\"Voltage [kV]\", unit_fn=e3, round_dec=3)\n", + " map_column(df, \"u_angle\", rename_to=\"Angle [°]\", round_dec=3)\n", + " map_column(df, \"p\", rename_to=\"P [MW]\", unit_fn=e6, round_dec=2)\n", + " map_column(df, \"q\", rename_to=\"Q [MVAr]\", unit_fn=e6, round_dec=2)\n", + "\n", + " df.drop([\"energized\"], axis=1, inplace=True)\n", + "\n", + " print(\"\\nnode data\")\n", + " print(\"---------\")\n", + " print(df.to_string(index=False))\n", + "\n", + "\n", + "def print_branch_output(output_data, input_data):\n", + " genb_out = output_data[ComponentType.generic_branch]\n", + " genb_in = input_data[ComponentType.generic_branch]\n", + " df = pd.DataFrame(genb_out)\n", + " df.insert(1, \"Name\", branch_name)\n", + " df.insert(2, \"From Node\", list(genb_in[\"from_node\"]))\n", + " df.insert(3, \"To Node\", list(genb_in[\"to_node\"]))\n", + "\n", + " map_column(df, \"loading\", rename_to=\"Loading [%]\", unit_fn=percent, round_dec=1)\n", + " map_column(df, \"p_from\", rename_to=\"From P [MW]\", unit_fn=e6, round_dec=2)\n", + " map_column(df, \"q_from\", rename_to=\"From Q [MVAr]\", unit_fn=e6, round_dec=2)\n", + " map_column(df, \"p_to\", rename_to=\"To P [MW]\", unit_fn=e6, round_dec=2)\n", + " map_column(df, \"q_to\", rename_to=\"To Q [MVAr]\", unit_fn=e6, round_dec=2)\n", + "\n", + " df.drop([\"energized\", \"i_from\", \"s_from\", \"i_to\", \"s_to\"], axis=1, inplace=True)\n", + "\n", + " print(\"\\ngeneric branch data\")\n", + " print(\"-------------------\")\n", + " print(df.to_string(index=False))\n", + "\n", + "\n", + "def map_column(df, col_name, rename_to=None, unit_fn=None, round_dec=None):\n", + " col = df[col_name] if unit_fn is None else unit_fn(df[col_name])\n", + " col = col if round_dec is None else col.round(round_dec)\n", + " df[col_name] = col\n", + " df.rename(columns={col_name: rename_to}, inplace=True)\n", + " return df\n", + "\n", + "\n", + "def calculate_total_power(output_data):\n", + " genb_out = output_data[ComponentType.generic_branch]\n", + " df = pd.DataFrame(genb_out)\n", + "\n", + " # Summiere die aktive und reaktive Leistung über alle Branches hinweg\n", + " P_total = e6(df[\"p_from\"].sum()) + e6(df[\"p_to\"].sum())\n", + " Q_total = e6(df[\"q_from\"].sum()) + e6(df[\"q_to\"].sum())\n", + "\n", + " # Ausgabe der Gesamtsummen\n", + " print(\"\\nTotal Power for all Branches\")\n", + " print(\"----------------------------\")\n", + " print(f\"Total Active Power (P_total): {P_total:.2f} MW\")\n", + " print(f\"Total Reactive Power (Q_total): {Q_total:.2f} MVAr\")\n", + "\n", + " return P_total, Q_total\n", + "\n", + "\n", + "print(\"Generic Branch Example\")\n", + "\n", + "print(\"Network:\")\n", + "print(network)\n", + "\n", + "print(\"========================================\")\n", + "print(\" Input Data\")\n", + "print(\"========================================\")\n", + "print_node_input(input_data)\n", + "print_branch_input(input_data)\n", + "print_load_input(input_data)\n", + "print_source_input(input_data)\n", + "\n", + "print(\"========================================\")\n", + "print(\" Output Data\")\n", + "print(\"========================================\")\n", + "print_node_output(output_data)\n", + "print_branch_output(output_data, input_data)\n", + "calculate_total_power(output_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using the Generic Branch with CIM/CGMES Data\n", + "\n", + "The Generic Branch enables the direct modeling of electrical assets such as lines, transformers, and equivalent branches using data from CIM/CGMES network datasets. Unlike traditional approaches that rely on manufacturer-specific parameters for modeling transformers and other equipment, the Generic Branch allows the direct use of electrical equivalent circuit parameters as specified in CIM/CGMES.\n", + "\n", + "This enhancement significantly simplifies the handling of grid models, especially when integrating external datasets into existing systems. It eliminates the need to convert equivalent circuit parameters into manufacturer-specific values, thereby improving data integrity and reducing potential sources of error.\n", + "\n", + "This chapter provides a guide on how to use the Generic Branch to import data from CIM/CGMES network datasets and integrate it into a PGM model.\n", + "\n", + "\n", + "> Note: The PhaseAngleClock of transformers is not used in this document because the generic branch in this context is only applied to symmetrically loaded phases. The rated power of transformes is only used to calculate the relative loading.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### CIM:ACLineSegment\n", + "\n", + "For **ACLineSegments**, the Generic Branch can be used directly. The equivalent circuit parameters are directly mapped to the Generic Branch. The length of the line is accounted for in the parameters $r$, $x$, $b$ and $g$:\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```\n", + "r1 = cim:ACLineSegment.r\n", + "x1 = cim:ACLineSegment.x\n", + "b1 = cim:ACLineSegment.bch\n", + "g1 = cim:ACLineSegment.gch\n", + "k = 1.0\n", + "theta = 0.0\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### CIM:EquivalentBranch\n", + "\n", + "**EquivalentBranches** which connect different voltage levels should be modeled as `generic_branch`.\n", + "\n", + "The conversion factors for impedance $z_{\\text{conv}}$ and admittance $y_{\\text{conv}}$ are calculated as follows:\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```\n", + "Note: We assume that the nominal voltage of terminal 1 is higher than the nominal voltage of terminal 2, u1 > u2\n", + "\n", + "z_conv = (u2 * u2) / (u1 * u1)\n", + "\n", + "r1 = cim:EquivalentBranch.r * z_conv\n", + "x1 = cim:EquivalentBranch.x * z_conv\n", + "\n", + "k = 1.0\n", + "theta = 0.0\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Two-winding Power Transformers\n", + "\n", + "**Two-winding PowerTransformers** are characterized by only two PowerTransformerEnds. The Generic Branch can be used to model these transformers. On the high-voltage side, non-zero values for $r$, $x$, $b$$ and $g$ are provided in the cim datasets, while on the low-voltage side, these values are set to zero.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```\n", + "end1 = [cim:PowerTransformerEnd]()\n", + "end2 = [cim:PowerTransformerEnd]()\n", + "\n", + "ratedS1 = end1.ratedS\n", + "ratedS2 = end2.ratedS\n", + "\n", + "rated_u1 = end1.ratedU\n", + "rated_u2 = end2.ratedU\n", + "\n", + "Note: We asume rated_u1 > rated_u2\n", + "\n", + "nom_u1 = end1.nominalVoltage\n", + "nom_u2 = end2.nominalVoltage\n", + "\n", + "\n", + "sn = max(ratedS1, ratedS2) * 1e6 # MVA -> VA\n", + "\n", + "z_conv = (rated_u2 * rated_u2) / (rated_u1 * rated_u1)\n", + "y_conv = 1 / z_conv\n", + "\n", + "\n", + "# if r, x, g and b provided in the CIM file as RatioTapChangerTablePoint one has to consider the tap changer position\n", + "r1 = cim:PowerTransformerEnd.r * z_conv\n", + "x1 = cim:PowerTransformerEnd.x * z_conv\n", + "b1 = cim:PowerTransformerEnd.bch * y_conv\n", + "g1 = cim:PowerTransformerEnd.gch * y_conv\n", + "\n", + "r, x, b, g = getRatioTapChangerTablePoint() # returns (0,0,0,0) if not provided in the CIM file\n", + "r1 = r1 + r1*r/100.0\n", + "x1 = x1 + x1*x/100.0\n", + "b1 = b1 + b1*b/100.0\n", + "g1 = g1 + g1*g/100.0\n", + "\n", + "\n", + "# Correction of the rated voltage depending on the tap changer position\n", + "regulate_side = get_side_from_tap_changer()\n", + "step_size = get_voltage_increment_from_tap_changer()\n", + "act_ratio = get_ratio()\n", + "if regulate_side == 0\n", + " step = step_neutral = 0\n", + "\n", + "# case 1: if a ratio is not given in the CIM file\n", + "if act_ratio is None:\n", + " rated_u = rated_u1 if regulate_side == 1 else rated_u2\n", + " corr_u = (step - step_neutral) * rated_u * (step_size / 100)\n", + " if regulate_side == 1:\n", + " rated_u1 = rated_u1 + corr_u\n", + " else:\n", + " rated_u2 = rated_u2 + corr_u\n", + "else:\n", + "# case 2: if a ratio is given in the CIM file CIM:RatioTapChangerTable\n", + " act_ratio = get_ratio()\n", + " if regulate_side == 1:\n", + " rated_u1 = rated_u1 * act_ratio\n", + " else:\n", + " rated_u2 = rated_u2 * act_ratio\n", + "\n", + "\n", + "k = (rated_u1 / rated_u2) / (nom_u1 / nom_u2)\n", + "theta = 0.0\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Pseudo Function `get_side_from_tap_changer()`\n", + "Purpose: Returns the side of the tap changer. If the tap changer is on the high voltage side, the side is 1. If the tap changer is on the low voltage side, the side is 2. If there is no tap changer, the side is 0.\n", + "\n", + "#### Pseudo Function `get_ratio()`\n", + "Purpose: Returns the ratio of the current step position of the tap changer from the CIM-File (CIM:RatioTapChangerTable). Returns None if the ratio is not given in the CIM file.\n", + "\n", + "#### Pseudo Function `get_voltage_increment_from_tap_changer()`\n", + "Purpose: Returns the voltage increment of the tap changer from the CIM-File.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Three-winding Power Transformers\n", + "\n", + "For **Three-winding PowerTransformers**, the three PowerTransformerEnds represent a star-equivalent circuit, with each leg in the star defined by its $r$, $x$, $g$ and $b$ values. This configuration requires three separate Generic Branches and an additional auxiliary node to connect them.\n", + "\n", + "```\n", + " GB_HV\n", + " |\n", + " GB_MV ------*------ GB_LV\n", + "```\n", + "\n", + "- **GB_HV**: Generic Branch for the high-voltage side\n", + "- **GB_MV**: Generic Branch for the medium-voltage side\n", + "- **GB_LV**: Generic Branch for the low-voltage side\n", + "- **\\***: Auxiliary node with voltage = HV nominal voltage\n", + "\n", + "Each transformer end (or winding) must provide:\n", + "- Nominal voltage\n", + "- Rated power\n", + "- Rated voltage\n", + "- Parameters $r$, $x$, $b$, $g$\n", + "- Optional: Tap changer side, position, neutral position and voltage step increment.\n", + "\n", + "The voltage on the auxiliary node is the nominal voltage of the high-voltage side. The tap changer's position must be determined to adjust the ratio for the regulated side, while the ratios for the other sides are set to $1.0$. For the generic branch on the regulated side, the ratio k should be calculated as described in section [2-Winding Power Transformers](##2-Winding-Power-Transformers). The electrical paramaters $r$, $x$, $b$, and $g$ can be used as given in the CIM-File without any further conversion. In contrast to 2-winding transformers, for 3-winding transformers the the parameters $r$, $x$, $b$, and $g$ are given for each TransformerEnd in the CIM-Dataset.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Phase Shift Transformers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Asymmetrical Phase Shifting Transformers\n", + "For a **3-Winding Transformer with Phase Shift**, the configuration mirrors the star-equivalent model described in section [3-Winding Power Transformers](3-Winding-Power-Transformers). However, one leg must account for the phase shift.\n", + "\n", + "```\n", + " GB_HV\n", + " |\n", + " GB_MV --//--*------ GB_LV\n", + "```\n", + "- **GB_HV**: Generic Branch for the high-voltage side\n", + "- **GB_MV**: Generic Branch for the medium-voltage side\n", + "- **GB_LV**: Generic Branch for the low-voltage side\n", + "- **\\***: Auxiliary node with voltage = HV nominal voltage\n", + "- **--//--**: Phase shift angle\n", + "\n", + "In the PI model of the Generic Branch, the controller (ratio + shift) is located on the **input side**, requiring adjustments for the phase shift angle as it is specified in CIM datasets on the **output side**.\n", + "```\n", + "u1 o---- -----Z_ser------------o u2\n", + " | | . .\n", + " | | . .\n", + " | | Y_shunt Y_shunt\n", + " | | . .\n", + " | | . .\n", + " o---- ----------------------o\n", + " k\n", + "```\n", + "$k = \\text{ratio} \\cdot e^{j \\cdot \\theta}$\n", + "\n", + "If the tap changer is located on the MV side, the phase shift angle must be multiplied by \\(-1.0\\) and alyways converted to radians. \n", + "```python\n", + "if regulate_side == 2:\n", + " deg_to_rad = math.pi / 180.0\n", + " theta = angle * deg_to_rad * -1.0\n", + "else:\n", + " deg_to_rad = math.pi / 180.0\n", + " theta = angle * deg_to_rad \n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The phase shift depends on the tap changer position, as specified in the CIM dataset e.g. CIM:PhaseTapChangerTabular or should be calculated using the formulas from \"Phase Shift Transformers Modelling\" ([ENTSOE_CGMES_v2.4_28May2014_PSTmodelling.pdf](https://eepublicdownloads.entsoe.eu/clean-documents/CIM_documents/Grid_Model_CIM/ENTSOE_CGMES_v2.4_28May2014_PSTmodelling.pdf)).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Symmetrical Phase Shifting Transformers\n", + "\n", + "A **Symmetrical Phase Shifting Transformer (PST)** can also be modeled using a Generic Branch. In this case:\n", + "- The nominal voltages on both sides are identical.\n", + "- The tap changer influences the total shift of the transformer.\n", + "\n", + "The ratio is set to \\(1.0\\), and the phase angle shift is either provided in the CIM file or calculated using the formulas from *Phase Shift Transformers Modelling* ([ENTSOE_CGMES_v2.4_28May2014_PSTmodelling.pdf](https://eepublicdownloads.entsoe.eu/clean-documents/CIM_documents/Grid_Model_CIM/ENTSOE_CGMES_v2.4_28May2014_PSTmodelling.pdf)). Similar to the asymmetrical case, the angle is converted to radians and multiplied by \\(-1.0\\) depending on the tap changer's location for proper modeling.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/examples/Generic Branch Example.ipynb.license b/docs/examples/Generic Branch Example.ipynb.license new file mode 100644 index 000000000..760105916 --- /dev/null +++ b/docs/examples/Generic Branch Example.ipynb.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Contributors to the Power Grid Model project + +SPDX-License-Identifier: MPL-2.0