diff --git a/input/ma_thesis/szenarien.ipynb b/input/ma_thesis/szenarien.ipynb new file mode 100644 index 00000000..40b61b8c --- /dev/null +++ b/input/ma_thesis/szenarien.ipynb @@ -0,0 +1,358 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Some jupyter notebook magic to reload modules automaticaally when they change\n", + "# not neccessary for this specific notebook but useful in general\n", + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "# Gives you high resolution images within the notebook\n", + "%config InlineBackend.figure_format = 'retina'\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from pypsdm.ma_thesis import *\n", + "from pypsdm.ma_thesis.utils import get_output_path\n", + "\n", + "\n", + "grid_name = \"semiurb-combined-1\"\n", + "result_folder_name = \"Simulationen\"\n", + "result_base_name = \"Szenario\"\n", + "szenarios = [1,2,3,4]\n", + "\n", + "upper_limit = 1.05\n", + "lower_limit = 0.95\n", + "\n", + "dotted = [lower_limit, upper_limit]\n", + "tap_dotted = [2, -2]\n", + "\n", + "def res_out(scenario:str, filename: str, extension: str = \"pdf\"):\n", + " return get_output_path(result_base_name+\"-\"+scenario, filename+\".\"+extension)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2024-07-31 08:23:09.327\u001b[0m | \u001b[34m\u001b[1mDEBUG \u001b[0m | \u001b[36mpypsdm.models.primary_data\u001b[0m:\u001b[36mfrom_csv\u001b[0m:\u001b[36m266\u001b[0m - \u001b[34m\u001b[1mNo primary data in path C:\\Users\\mariu\\PycharmProjects\\pypsdm\\input\\ma_thesis\\grids\\semiurb-combined-1\u001b[0m\n" + ] + } + ], + "source": [ + "# Read grid and results\n", + "\n", + "grid, results = read_scenarios(grid_name, result_folder_name, result_base_name, szenarios)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Informationen\n", + "from pypsdm.ma_thesis.subgrid import SubGrid\n", + "\n", + "subgrids = SubGrid.build(grid)\n", + "\n", + "transformer_uuids: dict[str, list[str]] = {\n", + " \"1-2\": get_transformers_between(subgrids[1], subgrids[2]),\n", + " \"2-3\": get_transformers_between(subgrids[2], subgrids[3]),\n", + " \"2-4\": get_transformers_between(subgrids[2], subgrids[4]),\n", + " \"2-5\": get_transformers_between(subgrids[2], subgrids[5]),\n", + "}\n" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": {}, + "outputs": [], + "source": [ + "# Erkennung\n", + "\n", + "detection_scenario = \"1\"\n", + "\n", + "detection_results = results[result_base_name+\"-\"+detection_scenario]\n", + "detection_subgrid_info = {nr: SubGridInfo(sub, detection_results) for nr, sub in subgrids.items()}\n", + "detection_gwr = GridWithResults(grid, detection_results)\n", + "\n", + "## Voltage\n", + "\n", + "#congestion_v2 = plot_voltage_with_congestion(detection_subgrid_info[2], detection_results, dotted=dotted)\n", + "#ax_add_vlines(congestion_v2[1], [29, 78])\n", + "\n", + "#congestion_v3 = plot_voltage_with_congestion(detection_subgrid_info[3], detection_results, dotted=dotted)\n", + "#ax_add_vlines(congestion_v3[1], [104, 10, 122, 148])\n", + "\n", + "#congestion_v4 = plot_voltage_with_congestion(detection_subgrid_info[4], detection_results, dotted=dotted)\n", + "#congestion_v5 = plot_voltage_with_congestion(detection_subgrid_info[25], detection_results, dotted=dotted)\n", + "\n", + "\n", + "## Line\n", + "#congestion_l2 = plot_line_utilization_with_congestion(detection_subgrid_info[2], detection_results)\n", + "\n", + "## Transformer\n", + "#congestion_t12 = plot_transformer_utilization_with_congestion(detection_subgrid_info[2], transformer_uuids[\"1-2\"], detection_gwr)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# Stufung 1\n", + "\n", + "tapping_results1 = results[result_base_name+\"-\"+\"1\"]\n", + "\n", + "# subgrid 2\n", + "#volt1_2 = plot_voltage_with_tapping(subgrids[2], transformer_uuids[\"1-2\"], tapping_results1, dotted=dotted) \n", + "#line1_2 = plot_line_utilization_with_congestion(SubGridInfo(subgrids[2], tapping_results1), tapping_results1)\n", + "\n", + "#subgrid 3\n", + "#volt1_3 = plot_voltage_with_tapping(subgrids[3], transformer_uuids[\"2-3\"], tapping_results1, dotted=dotted, tap_dotted=tap_dotted) \n", + "\n", + "# subgrid 4\n", + "#volt1_4 = plot_voltage_with_tapping(subgrids[4], transformer_uuids[\"2-4\"], tapping_results1, dotted=dotted, tap_dotted=tap_dotted) \n", + "\n", + "# subgrid 5\n", + "#volt1_5 = plot_voltage_with_tapping(subgrids[5], transformer_uuids[\"2-5\"], tapping_results1, dotted=dotted, tap_dotted=tap_dotted) \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Stufung 2\n", + "\n", + "tapping_results2 = results[result_base_name+\"-\"+\"2\"]\n", + "\n", + "# subgrid 2\n", + "#volt2_2 = plot_voltage_with_tapping(subgrids[2], transformer_uuids[\"1-2\"], tapping_results2, dotted=dotted) \n", + "#line2_2 = plot_line_utilization_with_congestion(SubGridInfo(subgrids[2], tapping_results2), tapping_results2)\n", + "\n", + "#subgrid 3\n", + "#volt2_3 = plot_voltage_with_tapping(subgrids[3], transformer_uuids[\"2-3\"], tapping_results2, dotted=dotted, tap_dotted=tap_dotted) \n", + "\n", + "# subgrid 4\n", + "#volt2_4 = plot_voltage_with_tapping(subgrids[4], transformer_uuids[\"2-4\"], tapping_results2, dotted=dotted, tap_dotted=tap_dotted) \n", + "\n", + "# subgrid 5\n", + "#volt2_5 = plot_voltage_with_tapping(subgrids[5], transformer_uuids[\"2-5\"], tapping_results2, dotted=dotted, tap_dotted=tap_dotted) \n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# Stufung 3\n", + "\n", + "tapping_results3 = results[result_base_name+\"-\"+\"3\"]\n", + "\n", + "# subgrid 2\n", + "#volt3_2 = plot_voltage_with_tapping(subgrids[2], transformer_uuids[\"1-2\"], tapping_results3, dotted=dotted) \n", + "#line3_2 = plot_line_utilization_with_congestion(SubGridInfo(subgrids[2], tapping_results3), tapping_results3)\n", + "\n", + "#subgrid 3\n", + "#volt3_3 = plot_voltage_with_tapping(subgrids[3], transformer_uuids[\"2-3\"], tapping_results3, dotted=dotted, tap_dotted=tap_dotted) \n", + "\n", + "# subgrid 4\n", + "#volt3_4 = plot_voltage_with_tapping(subgrids[4], transformer_uuids[\"2-4\"], tapping_results3, dotted=dotted, tap_dotted=tap_dotted) \n", + "\n", + "# subgrid 5\n", + "#volt3_5 = plot_voltage_with_tapping(subgrids[5], transformer_uuids[\"2-5\"], tapping_results3, dotted=dotted, tap_dotted=tap_dotted) \n" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [], + "source": [ + "# Stufung 4\n", + "\n", + "tapping_results4 = results[result_base_name+\"-\"+\"4\"]\n", + "\n", + "# subgrid 2\n", + "#volt4_2 = plot_voltage_with_tapping(subgrids[2], transformer_uuids[\"1-2\"], tapping_results4, dotted=dotted) \n", + "#line4_2 = plot_line_utilization_with_congestion(SubGridInfo(subgrids[2], tapping_results4), tapping_results4)\n", + "\n", + "#subgrid 3\n", + "#volt2_3 = plot_voltage_with_tapping(subgrids[3], transformer_uuids[\"2-3\"], tapping_results4, dotted=dotted, tap_dotted=tap_dotted) \n", + "\n", + "# subgrid 4\n", + "#volt4_4 = plot_voltage_with_tapping(subgrids[4], transformer_uuids[\"2-4\"], tapping_results4, dotted=dotted, tap_dotted=tap_dotted) \n", + "\n", + "# subgrid 5\n", + "#volt4_5 = plot_voltage_with_tapping(subgrids[5], transformer_uuids[\"2-5\"], tapping_results4, dotted=dotted, tap_dotted=tap_dotted) \n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Szenario-2 Spannungen\n", + "\n", + "#res_2 = results[result_base_name+\"-\"+\"2\"]\n", + "#s = 3\n", + "#fig, axes = create_fig(nrows=1, width=8, height=2)\n", + "#length = ax_plot_both_voltages(axes, subgrids[s], res_2.nodes, dotted)\n", + "#axes.set_ylabel(\"Spannung in pu\", fontsize=11)\n", + "#format_x_axis(axes, length)\n", + "#fig.savefig(res_out(\"2\", \"voltage-\"+str(s)))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LV3.202 (min) 1.047213\n", + "LV3.202 (max) 1.050146\n", + "Name: 2016-07-30 05:00:00, dtype: float64" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = results[result_base_name+\"-\"+\"1\"]\n", + "\n", + "\n", + "info = SubGridInfo(subgrids[3], res)\n", + "info.node_min_max.loc[\"2016-07-30 05:00:00\"]\n", + "\n", + "\n", + "#df = res.transformers_2w[transformer_uuids[\"1-2\"][0]].data\n", + "#df[df[\"tap_pos\"].values == 2]\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Ohne Engpassmanagement 523.666667\n", + "Zwei Stufungen pro Tag 538.000000\n", + "Stufung zu jedem Zeitpunkt 1014.000000\n", + "dtype: float64" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Laufzeit\n", + "\n", + "import pandas as pd\n", + "\n", + "data = {\n", + " \"Ohne Engpassmanagement\": [8*60+47, 8*60+44, 8*60+40],\n", + " \"Zwei Stufungen pro Tag\": [9*60+1, 8*60+57, 8*60+56],\n", + " \"Stufung zu jedem Zeitpunkt\": [16*60+54, 16*60+56, 16*60+52]\n", + "}\n", + "\n", + "df = pd.DataFrame(data)\n", + "\n", + "#ax = df.plot.barh(legend=False)\n", + "#ax.set_xlabel(\"Zeit in Sekunden\")\n", + "\n", + "#df.mean()\n", + "\n", + "df.mean()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# Spannungsvergleich\n", + "from pypsdm.ma_thesis.plots import plot_voltages_with_scenario\n", + "\n", + "\n", + "#_ = plot_voltages_with_scenario(subgrids[2], results, upper_limit=upper_limit, lower_limit=lower_limit).savefig(res_out(\"Spannung-2-all\"))\n", + "#_ = plot_voltages_with_scenario(subgrids[3], results, upper_limit=upper_limit, lower_limit=lower_limit).savefig(res_out(\"Spannung-3-all\"))\n", + "#_ = plot_voltages_with_scenario(subgrids[4], results, upper_limit=upper_limit, lower_limit=lower_limit).savefig(res_out(\"Spannung-4-all\"))\n", + "#_ = plot_voltages_with_scenario(subgrids[5], results, upper_limit=upper_limit, lower_limit=lower_limit).savefig(res_out(\"Spannung-5-all\"))\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "# _ = plot_with_highlights(gwr.grid)\n", + "# _ = plot_voltage_subgrids(subgrids, dotted=[1.03, 0.97], width=30, height=20)\n", + "# _ = plot_voltage_with_tapping(subgrids[1], subgrids[2], gwr.transformers_2_w, gwr.transformers_2_w_res, width=30, height=10, dotted=[1.03, 0.97])\n", + "# _ = plot_line_utilizations(subgrids[4], threshold=0.33, show_legend=False, width=30)\n", + "# _ = plot_line_utilizations(subgrids[2], threshold=0.33, show_legend=False, width=30)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "gui-MypUtHgG-py3.11", + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/poetry.lock b/poetry.lock index d8efb001..995cd8ec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -2094,10 +2094,7 @@ files = [ ] [package.dependencies] -numpy = [ - {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, -] +numpy = {version = ">=1.23.2", markers = "python_version >= \"3.11\""} python-dateutil = ">=2.8.2" pytz = ">=2020.1" tzdata = ">=2022.1" @@ -2772,6 +2769,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -2779,8 +2777,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -2797,6 +2802,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -2804,6 +2810,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -3369,7 +3376,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} typing-extensions = ">=4.6.0" [package.extras] diff --git a/pyproject.toml b/pyproject.toml index 9c599c3b..53045221 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ pyhocon = "^0.3.60" psycopg2 = "^2.9.9" numba = "^0.59.1" + [tool.poetry.group.dev.dependencies] pytest = "^7.2.1" flake8 = "^6.0.0" diff --git a/pypsdm/io/utils.py b/pypsdm/io/utils.py index 172f123f..e268b740 100644 --- a/pypsdm/io/utils.py +++ b/pypsdm/io/utils.py @@ -48,10 +48,10 @@ def get_file_path(path: str | Path, file_name: str): def read_csv( - path: str | Path, - file_name: str, - delimiter: str | None = None, - index_col: Optional[str] = None, + path: str | Path, + file_name: str, + delimiter: str | None = None, + index_col: Optional[str] = None, ) -> DataFrame: full_path = get_file_path(path, file_name) if not full_path.exists(): @@ -89,7 +89,7 @@ def to_date_time(zoned_date_time: str) -> datetime: def csv_to_grpd_df( - file_name: str, simulation_data_path: str, delimiter: str | None = None + file_name: str, simulation_data_path: str, by: str, delimiter: str | None = None ) -> DataFrameGroupBy: """ Reads in a PSDM csv results file cleans it up and groups it by input_archive model. @@ -97,6 +97,7 @@ def csv_to_grpd_df( Args: file_name: name of the file to read simulation_data_path: base directory of the result data + by: the column to group by delimiter: the csv delimiter Returns: @@ -106,7 +107,7 @@ def csv_to_grpd_df( if "uuid" in data.columns: data = data.drop(columns=["uuid"]) - return data.groupby(by="input_model") + return data.groupby(by=by) def check_filter(filter_start: Optional[datetime], filter_end: Optional[datetime]): @@ -119,13 +120,13 @@ def check_filter(filter_start: Optional[datetime], filter_end: Optional[datetime def df_to_csv( - df: DataFrame, - path: Union[str, Path], - file_name: str, - mkdirs=False, - delimiter: str = ",", - index_label="uuid", - datetime_pattern=DateTimePattern.UTC_TIME_PATTERN, + df: DataFrame, + path: Union[str, Path], + file_name: str, + mkdirs=False, + delimiter: str = ",", + index_label="uuid", + datetime_pattern=DateTimePattern.UTC_TIME_PATTERN, ): df = df.copy(deep=True) if isinstance(path, Path): diff --git a/pypsdm/ma_thesis/__init__.py b/pypsdm/ma_thesis/__init__.py new file mode 100644 index 00000000..bb42c555 --- /dev/null +++ b/pypsdm/ma_thesis/__init__.py @@ -0,0 +1,6 @@ +from .analyse import * +from .highlight_utils import * +from .plots import * +from .subgrid import * +from .utils import * + diff --git a/pypsdm/ma_thesis/analyse.py b/pypsdm/ma_thesis/analyse.py new file mode 100644 index 00000000..10391613 --- /dev/null +++ b/pypsdm/ma_thesis/analyse.py @@ -0,0 +1,57 @@ +import pandas as pd +from pandas import DataFrame + +from pypsdm import GridContainer, NodesResult, LinesResult, GridWithResults + + +def analyse_nodes(name, grid: GridContainer, results: NodesResult) -> (DataFrame, DataFrame): + nodes = grid.nodes.data.index.to_list() + results = {node: results[node].data["v_mag"] for node in nodes} + node_res = pd.concat(results, axis=1) + node_min_max = pd.concat({name + " (min)": node_res.min(axis=1), name + " (max)": node_res.max(axis=1)}, axis=1) + + return node_res, node_min_max + + +def get_max_voltages(grid: GridContainer, results: dict[str, NodesResult]) -> DataFrame: + nodes = grid.nodes.data.index.to_list() + + def get_max(result: NodesResult): + return pd.concat({node: result[node].data["v_mag"] for node in nodes}, axis=1).max(axis=1) + + scenario_results = {scenario: get_max(result) for scenario, result in results.items()} + + return pd.concat(scenario_results, axis=1) + + +def get_min_voltages(grid: GridContainer, results: dict[str, NodesResult]) -> DataFrame: + nodes = grid.nodes.data.index.to_list() + + def get_min(result: NodesResult): + return pd.concat({node: result[node].data["v_mag"] for node in nodes}, axis=1).min(axis=1) + + scenario_results = {scenario: get_min(result) for scenario, result in results.items()} + + return pd.concat(scenario_results, axis=1) + + +def analyse_lines(name, grid: GridContainer, results: LinesResult) -> (DataFrame, DataFrame): + uuids = grid.lines.data.index.to_list() + line_res = results.subset(uuids).utilisation(grid.lines) + line_max = line_res.max(axis=1).to_frame(name) + + return line_res, line_max + + +def analyse_transformers2w(uuids: list[str], gwr: GridWithResults) -> DataFrame: + df = gwr.transformers_2_w.data + + def uuid_to_id(uuid: str): + return df[df.index.values == uuid]["id"].to_list()[0] + + transformer2w_res = pd.concat( + {uuid_to_id(uuid): gwr.transformers_2_w_res[uuid].utilisation(uuid, gwr, "lv") for uuid in uuids}, + axis=1 + ) + + return transformer2w_res diff --git a/pypsdm/ma_thesis/highlight_utils.py b/pypsdm/ma_thesis/highlight_utils.py new file mode 100644 index 00000000..c1bf8f7e --- /dev/null +++ b/pypsdm/ma_thesis/highlight_utils.py @@ -0,0 +1,17 @@ +from typing import Union + +from pandas import DataFrame + +from pypsdm.models.input.container import GridContainer, RawGridContainer +from pypsdm.models.gwr import GridWithResults + + +def get_nodes(grid: Union[GridContainer | RawGridContainer | GridWithResults], volt_lvl: float) -> list[str]: + nodes: DataFrame = grid.nodes.data + + return nodes[nodes["v_rated"].values == volt_lvl].index.to_list() + + +def get_lines(grid: Union[GridContainer | RawGridContainer | GridWithResults], volt_lvl: float) -> list[str]: + nodes = get_nodes(grid, volt_lvl) + return grid.filter_by_nodes(nodes).lines.data.index.to_list() diff --git a/pypsdm/ma_thesis/plots.py b/pypsdm/ma_thesis/plots.py new file mode 100644 index 00000000..3ffb9261 --- /dev/null +++ b/pypsdm/ma_thesis/plots.py @@ -0,0 +1,460 @@ +from typing import Union + +from matplotlib import pyplot as plt +from pandas import DataFrame + +from pypsdm import GridWithResults, Transformers2W, GridResultContainer, NodesResult, GridContainer +from pypsdm.ma_thesis import get_nodes, get_lines +from pypsdm.ma_thesis.analyse import analyse_nodes, get_max_voltages, get_min_voltages, analyse_transformers2w +from pypsdm.ma_thesis.subgrid import SubGridInfo, SubGrid +from pypsdm.ma_thesis.utils import get_trafo_2w_info, hours_index +from pypsdm.models.result.container.raw_grid import Transformers2WResult +from pypsdm.plots.common.utils import * +from pypsdm.plots.grid import grid_plot + +fontsize = 12 + +# Detection + +def plot_voltage_with_congestion( + subgrid: SubGridInfo, + result: GridResultContainer, + dotted: Union[float | list[float]] = None, + width: int = 8, + height: int = 4 +): + fig, axes = create_fig(width=width, height=height) + congestions = hours_index(result.congestions[subgrid.sub_grid.nr].voltage) * 1 + + hours_index(subgrid.node_min_max).plot(ax=axes[0]) + ax_add_dotted(axes[0], dotted) + congestions.plot(ax=axes[1], drawstyle="steps-post") + + axes[0].set_ylabel("Spannung in pu", fontsize=fontsize) + format_x_axis(axes[0], len(subgrid.node_min_max.index) - 1) + format_x_axis(axes[1], len(subgrid.node_min_max.index) - 1) + axes[1].set_yticks([0, 1]) + axes[1].set_yticklabels(["nein", "ja"], fontsize=fontsize) + axes[1].set_ylabel("Engpass?", fontsize=fontsize) + + return fig, axes[0], axes[1] + + +def plot_voltage_congestion( + subgrid: SubGridInfo, + result: GridResultContainer, + width: int = 8, + height: int = 2 +): + fig, axes = create_fig(nrows=1, width=width, height=height) + congestions = hours_index(result.congestions[subgrid.sub_grid.nr].voltage) * 1 + lim = len(subgrid.node_min_max.index) - 1 + ax_plot_congestion(axes, congestions, lim) + return fig + + +def plot_line_utilization_with_congestion( + subgrid: SubGridInfo, + result: GridResultContainer, + dotted: float = 100.0, + width: int = 8, + height: int = 4 +): + fig, axes = create_fig(width=width, height=height) + congestions = hours_index(result.congestions[subgrid.sub_grid.nr].line) * 1 + + hours_index(subgrid.line_max * 100).plot(ax=axes[0]) + ax_add_dotted(axes[0], dotted) + congestions.plot(ax=axes[1], drawstyle="steps-post") + + axes[0].set_ylabel("Auslastung in %", fontsize=fontsize) + format_x_axis(axes[0], len(subgrid.node_min_max.index) - 1) + format_x_axis(axes[1], len(subgrid.node_min_max.index) - 1) + axes[1].set_yticks([0, 1]) + axes[1].set_yticklabels(["nein", "ja"], fontsize=fontsize) + axes[1].set_ylabel("Engpass?", fontsize=fontsize) + + return fig, axes[0], axes[1] + + +def plot_line_congestion( + subgrid: SubGridInfo, + result: GridResultContainer, + width: int = 8, + height: int = 2 +): + fig, axes = create_fig(nrows=1, width=width, height=height) + congestions = hours_index(result.congestions[subgrid.sub_grid.nr].line) * 1 + lim = len(subgrid.node_min_max.index) - 1 + ax_plot_congestion(axes, congestions, lim) + return fig + + +def plot_transformer_utilization_with_congestion( + subgrid: SubGridInfo, + transformer_uuids: list[str], + gwr: GridWithResults, + dotted: float = 100.0, + width: int = 8, + height: int = 4 +): + fig, axes = create_fig(width=width, height=height) + congestions = hours_index(gwr.results.congestions[subgrid.sub_grid.nr].transformer) * 1 + + transformer_max = analyse_transformers2w(transformer_uuids, gwr) + + hours_index(transformer_max * 100).plot(ax=axes[0]) + ax_add_dotted(axes[0], dotted) + congestions.plot(ax=axes[1], drawstyle="steps-post") + + axes[0].set_ylabel("Auslastung in %", fontsize=fontsize) + format_x_axis(axes[0], len(subgrid.node_min_max.index) - 1) + format_x_axis(axes[1], len(subgrid.node_min_max.index) - 1) + axes[1].set_yticks([0, 1]) + axes[1].set_yticklabels(["nein", "ja"], fontsize=fontsize) + axes[1].set_ylabel("Engpass?", fontsize=fontsize) + + return fig, axes[0], axes[1] + + +def plot_transformer_congestion( + subgrid: SubGridInfo, + gwr: GridWithResults, + width: int = 8, + height: int = 2 +): + fig, axes = create_fig(nrows=1, width=width, height=height) + congestions = hours_index(gwr.results.congestions[subgrid.sub_grid.nr].transformer) * 1 + lim = len(subgrid.node_min_max.index) - 1 + ax_plot_congestion(axes, congestions, lim) + return fig + + +# Stufung + +def plot_voltage_with_tapping( + subgrid: SubGrid, + transformer_uuids: list[str], + results: GridResultContainer, + dotted: Union[float | list[float]] = None, + tap_dotted: list[int, int] = None, + width: int = 8, + height: int = 4 +): + fig, axes = create_fig(width=width, height=height) + + ax_plot_tapping(axes[0], transformer_uuids, subgrid.grid.transformers_2_w, results.transformers_2w, tap_dotted) + length = ax_plot_both_voltages(axes[1], subgrid, results.nodes, dotted) + + axes[0].set_ylabel("Stufe", fontsize=fontsize) + axes[1].set_ylabel("Spannung in pu", fontsize=fontsize) + format_x_axis(axes[0], length) + format_x_axis(axes[1], length) + + return fig, axes[0], axes[1] + + +def plot_tapping( + subgrid: SubGrid, + transformer_uuids: list[str], + results: GridResultContainer, + tap_dotted: list[int, int] = None, + width: int = 8, + height: int = 2 +): + fig, axes = create_fig(nrows=1, width=width, height=height) + length = ax_plot_tapping(axes, transformer_uuids, subgrid.grid.transformers_2_w, results.transformers_2w, tap_dotted) + + axes.set_ylabel("Stufung", fontsize=fontsize) + format_x_axis(axes, length) + return fig + + +# results + +def plot_voltage( + subgrid: SubGridInfo, + dotted: Union[float | list[float]] = None, + width: int = 8, + height: int = 2 +): + fig, axes = create_fig(nrows=1, width=width, height=height) + hours_index(subgrid.node_min_max).plot(ax=axes) + ax_add_dotted(axes, dotted) + + axes.set_ylabel("Spannung in pu", fontsize=fontsize) + format_x_axis(axes, len(subgrid.node_min_max.index) - 1) + return fig + + +def plot_line_utilization( + subgrid: SubGridInfo, + dotted: float = 100.0, + width: int = 8, + height: int = 2 +): + fig, axes = create_fig(nrows=1, width=width, height=height) + hours_index(subgrid.line_max * 100).plot(ax=axes) + ax_add_dotted(axes, dotted) + + axes.set_ylabel("Auslastung in %", fontsize=fontsize) + format_x_axis(axes, len(subgrid.node_min_max.index) - 1) + return fig + + +def plot_transformer_utilization( + transformer_uuids: list[str], + gwr: GridWithResults, + dotted: float = 100.0, + width: int = 8, + height: int = 2 +): + fig, axes = create_fig(nrows=1, width=width, height=height) + transformer_max = analyse_transformers2w(transformer_uuids, gwr) + + hours_index(transformer_max * 100).plot(ax=axes) + ax_add_dotted(axes, dotted) + + axes.set_ylabel("Auslastung in %", fontsize=fontsize) + format_x_axis(axes, len(transformer_max.index) - 1) + return fig + + +# utils + +def create_fig( + nrows: int = 2, + ncolumns: int = 1, + sharex: bool = True, + width: int = 8, + height: int = 4 +): + fig, axes = plt.subplots(nrows, ncolumns, figsize=(width, height), sharex=sharex, tight_layout=True) + return fig, axes + + +def format_x_axis( + ax: Axes, + lim: int, + step: int = 12, +): + if lim <= 168: + xticks = [i for i in range(0, lim, step)] + xticklabel = [x + 1 for x in xticks] + label = "Zeit in Stunden" + else: + xticks = [i for i in range(0, lim, 24*7*3)] + xticklabel = [i * 3 for i, _ in enumerate(xticks)] + label = "Zeit in Wochen" + + ax.set_xlim(0, lim) + ax.set_xticks(xticks) + ax.tick_params(axis='x', which='minor', bottom=False) + ax.set_xticklabels(xticklabel) + ax.set_xlabel(label, fontsize=fontsize) + + +def ax_plot_both_voltages( + axes: Axes, + subgrid: SubGrid, + results: NodesResult, + dotted: Union[float | list[float]] = None +) -> int: + _, node_min_max_res = analyse_nodes(subgrid.name, subgrid.grid, results) + + hours_index(node_min_max_res).plot(ax=axes) + ax_add_dotted(axes, dotted) + + return len(node_min_max_res.index) - 1 + + +def ax_add_dotted( + axes: Axes, + dotted: Union[float | list[float]] = None +): + if dotted: + if isinstance(dotted, float): + axes.axhline(dotted, color="red", linestyle="--") + else: + [axes.axhline(dot, color="red", linestyle="--") for dot in dotted] + + +def ax_add_vlines(axes: Axes, values: list[float] = None): + if values: + [axes.axvline(x, color="purple", linestyle="--") for x in values] + + +def ax_plot_congestion( + axes: Axes, + congestions: DataFrame, + lim: int +): + congestions.plot(ax=axes, drawstyle="steps-post") + + format_x_axis(axes, lim) + axes.set_yticks([0, 1]) + axes.set_yticklabels(["nein", "ja"], fontsize=fontsize) + axes.set_ylabel("Engpass?", fontsize=fontsize) + + +def ax_plot_tapping( + axes: Axes, + uuids: list[str], + transformers: Transformers2W, + result: Transformers2WResult, + tap_dotted: list[int, int] = None +): + tap_pos = pd.concat({transformers[uuid].id: result[uuid].data["tap_pos"] for uuid in uuids}, axis=1) + hours_index(tap_pos).plot(ax=axes, drawstyle="steps-post") + + values = tap_pos[transformers[uuids[0]].id].drop_duplicates() + if len(values) == 1 and not tap_dotted: + value = values[0] + ticks = [value-1, value, value+1] + axes.set_yticks(ticks) + axes.set_yticklabels(ticks) + else: + ax_add_dotted(axes, tap_dotted) + + return len(tap_pos.index) - 1 + +# other + +def plot_voltages_with_scenario( + subgrid: SubGrid, + results: dict[str, GridResultContainer], + upper_limit: float = None, + lower_limit: float = None, + width: int = 16, + height: int = 6 +): + fig, axes = create_fig(width=width, height=height) + + node_res = {name: res.nodes for name, res in results.items()} + get_max_voltages(subgrid.grid, node_res).plot(ax=axes[0]) + get_min_voltages(subgrid.grid, node_res).plot(ax=axes[1]) + + ax_add_dotted(axes[0], upper_limit) + ax_add_dotted(axes[1], lower_limit) + + return fig + + +def plot_voltages_with_tapping( + subgrid1: SubGridInfo, + subgrid2: SubGridInfo, + transformers: Transformers2W, + transformer_res: Transformers2WResult, + dotted: Union[float | list[float]] = None, + width: int = 16, + height: int = 9 +): + connectors = list(set(subgrid1.get_transformers()).intersection(subgrid2.get_transformers())) + + tap_pos = pd.concat({transformers[uuid].id: transformer_res[uuid].data["tap_pos"] for uuid in connectors}, axis=1) + + fig, axes = create_fig(nrows=3, width=width, height=height) + + subgrid1.node_min_max.plot(ax=axes[0]) + subgrid2.node_min_max.plot(ax=axes[1]) + tap_pos.plot(ax=axes[2]) + + ax_add_dotted(axes[0], dotted) + ax_add_dotted(axes[1], dotted) + + return fig + + +def plot_voltage_subgrid(subgrid: SubGridInfo, width: int = 16, height: int = 6): + return subgrid.node_res.plot(figsize=(width, height), legend=False) + + +def plot_subgrid_with_versions( + subgrids: dict[str, SubGridInfo], + dotted: Union[float | list[float]] = None, + width: int = 16, + height: int = 6 +): + fig, axes = create_fig(nrows=len(subgrids), width=width, height=height) + + for i, key in enumerate(subgrids): + subgrids[key].node_min_max.plot(ax=axes[i]) + + ax_add_dotted(axes[i], dotted) + axes[i].set_title(key) + + return fig + + +def plot_voltage_subgrids( + subgrids: dict[int, SubGridInfo], + dotted: Union[float | list[float]] = None, + width: int = 16, + height: int = 9, + subplots: bool = True +): + if subplots: + fig, axes = create_fig(nrows=len(subgrids), width=width, height=height) + + for i, subgrid in enumerate(subgrids.values()): + subgrid.node_min_max.plot(ax=axes[i]) + + ax_add_dotted(axes[i], dotted) + + else: + values = [subgrid.node_min_max for subgrid in subgrids.values()] + fig, axes = pd.concat(values, axis=1).plot() + + ax_add_dotted(axes, dotted) + + return fig + + +def plot_transformer_tappings(gwr: GridWithResults, width: int = 16, height: int = 6, subplots: bool = True): + transformers = get_trafo_2w_info(gwr) + transformer_results = gwr.transformers_2_w_res + + res = {tr_info.id: transformer_results[uuid].data["tap_pos"] for uuid, tr_info in transformers.items()} + res_sorted = dict(sorted(res.items())) + + tap_pos = pd.concat(res_sorted, axis=1) + fig = tap_pos.plot(subplots=subplots, figsize=(width, height)) + + fig.tight_layout() + return fig + + +def plot_line_utilizations(subgrid: SubGridInfo, threshold: float = 0.5, width: int = 16, height: int = 6, + show_legend: bool = False): + line_utilisation = subgrid.line_utilisation + df = line_utilisation[[i for i, value in line_utilisation.max().to_dict().items() if value > threshold]] + + if df.empty: + fig = plt.plot() + else: + fig = df.plot(figsize=(width, height), legend=show_legend) + + fig.tight_layout() + return fig + + +def plot_with_highlights(grid: GridContainer): + mv_20 = get_nodes(grid, 20.0) + mv_10 = get_nodes(grid, 10.0) + mv_30 = get_nodes(grid, 30.0) + hv = get_nodes(grid, 110.0) + + mv_lines = get_lines(grid, 10.0) + get_lines(grid, 20.0) + get_lines(grid, 30.0) + + node_highlights = { + RED: mv_10, + ORANGE: mv_20, + BROWN: mv_30, + PURPLE: hv + } + + line_highlights = { + YELLOW: mv_lines + } + + return grid_plot(grid, background="white-bg", node_highlights=node_highlights, line_highlights=line_highlights) diff --git a/pypsdm/ma_thesis/subgrid.py b/pypsdm/ma_thesis/subgrid.py new file mode 100644 index 00000000..5515fd97 --- /dev/null +++ b/pypsdm/ma_thesis/subgrid.py @@ -0,0 +1,54 @@ +from pandas import DataFrame + +from pypsdm import GridResultContainer, GridContainer, GridWithResults + + +class SubGrid: + nr: int + name: str + grid: GridContainer + + def __init__(self, nr: int, name: str, grid: GridContainer): + self.nr = nr + self.name = name + self.grid = grid + + def transformers(self) -> list[str]: + return self.grid.transformers_2_w.data.index.to_list() + + @classmethod + def build(cls, grid: GridContainer) -> dict[int, "SubGrid"]: + from pypsdm.ma_thesis.utils import split_into_subgrids, get_subgrid_to_names, sort + + subgrids = split_into_subgrids(grid) + names = get_subgrid_to_names(grid) + + return sort( + {subgrid: SubGrid(subgrid, names[subgrid], subgrids[subgrid]) for subgrid in subgrids.keys()}) + + +class SubGridInfo: + sub_grid: SubGrid + node_res: DataFrame + node_min_max: DataFrame + line_utilisation: DataFrame + line_max: DataFrame + transformer_max: DataFrame + + def __init__(self, sub_grid: SubGrid, results: GridResultContainer): + from pypsdm.ma_thesis.analyse import analyse_nodes, analyse_lines + + self.sub_grid = sub_grid + self.node_res, self.node_min_max = analyse_nodes(sub_grid.name, sub_grid.grid, results.nodes) + self.line_utilisation, self.line_max = analyse_lines(sub_grid.name, sub_grid.grid, results.lines) + + + def get_transformers(self) -> list[str]: + return self.sub_grid.grid.transformers_2_w.data.index.to_list() + + @classmethod + def build(cls, gwr: GridWithResults) -> dict[int, "SubGridInfo"]: + from pypsdm.ma_thesis.utils import sort + + sub_grids = SubGrid.build(gwr.grid) + return sort({subgrid: cls(sub_grids[subgrid], gwr.results) for subgrid in sub_grids.keys()}) diff --git a/pypsdm/ma_thesis/utils.py b/pypsdm/ma_thesis/utils.py new file mode 100644 index 00000000..e4752f9d --- /dev/null +++ b/pypsdm/ma_thesis/utils.py @@ -0,0 +1,85 @@ +from os.path import join + +from pandas import Series, DataFrame, Timedelta + +from definitions import ROOT_DIR +from pypsdm import GridContainer, GridWithResults, GridResultContainer +from pypsdm.ma_thesis.subgrid import SubGrid, SubGridInfo + + +def get_base_path(): + return join(ROOT_DIR, "input", "ma_thesis") + + +def plot_output_folder(): + return join(get_base_path(), "plots") + + +def get_output_path(result_base_name: str, filename: str): + return join(get_base_path(), "plots", result_base_name, filename) + + +def get_result(result_folder_name, result_name, delimiter: str = ",") -> str: + path = join(get_base_path(), "results", result_folder_name, result_name, "rawOutputData") + return GridResultContainer.from_csv(path, delimiter=delimiter) + + +def read_scenarios( + grid_name: str, + result_folder_name: str, + result_base_name: str, + scenarios: list[str], + grid_delimiter: str = ",", + result_delimiter: str = ",", +) -> (GridContainer, dict[str, GridResultContainer]): + grid_path = join(get_base_path(), "grids", grid_name) + grid = GridContainer.from_csv(path=grid_path, delimiter=grid_delimiter) + + result_names = [result_base_name + "-" + str(nr) for nr in scenarios] + results = {result_name: get_result(result_folder_name, result_name, result_delimiter) for result_name in + result_names} + + return grid, results + + +def get_nodes_to_subgrid(grid: GridContainer) -> dict[int, list[str]]: + nodes = grid.nodes + subnets = nodes.subnet.drop_duplicates().to_list() + return {subnet: nodes[nodes.subnet.values == subnet].index.to_list() for subnet in subnets} + + +def get_subgrid_to_names(grid: GridContainer) -> dict[int, str]: + nodes = grid.nodes + nodes_to_subnet = get_nodes_to_subgrid(grid) + return {subnet: nodes[nodes_to_subnet[subnet][0]].id.split(" Bus")[0] for subnet in nodes_to_subnet.keys()} + + +def split_into_subgrids(grid: GridContainer) -> dict[int, GridContainer]: + nodes_to_subnet = get_nodes_to_subgrid(grid) + return {subnet: grid.filter_by_nodes(nodes) for subnet, nodes in nodes_to_subnet.items()} + + +def get_trafo_2w_info(gwr: GridWithResults) -> dict[str, Series]: + transformers = gwr.transformers_2_w + return {uuid: transformers[uuid] for uuid in transformers.data.index.to_list()} + + +def get_transformers_between(subgrid1: SubGrid, subgrid2: SubGrid): + return list(set(subgrid1.transformers()).intersection(subgrid2.transformers())) + + +def get_subgrid_with_version(subgrid: int, subgrids: dict[str, dict[int, SubGridInfo]]) -> dict[str, SubGridInfo]: + return {name: info[subgrid] for name, info in subgrids.items()} + + +def sort(dictionary: dict): + return dict(sorted(dictionary.items())) + + +def hours_index(df: DataFrame): + copy = df.copy() + new_index = [str(i) for i, _ in enumerate(copy.index.to_list())] + + copy.index = new_index + return copy + diff --git a/pypsdm/models/enums.py b/pypsdm/models/enums.py index 0067d16e..9b1c71a6 100644 --- a/pypsdm/models/enums.py +++ b/pypsdm/models/enums.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Type, TypeVar if TYPE_CHECKING: - from pypsdm.models.result.participant.dict import EntitiesResultDictMixin + from pypsdm.models.result.participant.dict import ResultDictMixin from pypsdm.models.ts.base import TimeSeries @@ -28,7 +28,10 @@ def get_csv_input_file_name(self): return self.value + "_input.csv" def get_csv_result_file_name(self): - return self.value + "_res.csv" + if self.value == "subgrid": + return "congestion_res.csv" + else: + return self.value + "_res.csv" def get_type_file_name(self): assert self.has_type() is True @@ -42,6 +45,7 @@ def get_result_type(self) -> type[TimeSeries]: from pypsdm.models.result.grid.connector import ConnectorCurrent from pypsdm.models.result.grid.switch import SwitchResult from pypsdm.models.result.grid.transformer import Transformer2WResult + from pypsdm.models.result.grid.congestions import CongestionResult from pypsdm.models.ts.types import ( ComplexPower, ComplexPowerWithSoc, @@ -63,6 +67,8 @@ def get_result_type(self) -> type[TimeSeries]: return ConnectorCurrent case RawGridElementsEnum.SWITCH: return SwitchResult + case RawGridElementsEnum.SUBGRID: + return CongestionResult case _: raise NotImplementedError( f"Result type {self} not implemented yet!" @@ -70,11 +76,12 @@ def get_result_type(self) -> type[TimeSeries]: else: raise ValueError(f"Entity type {self} not supported!") - def get_result_dict_type(self) -> Type["EntitiesResultDictMixin"]: + def get_result_dict_type(self) -> Type["ResultDictMixin"]: from pypsdm.models.result.grid.line import LinesResult from pypsdm.models.result.grid.node import NodesResult from pypsdm.models.result.grid.switch import SwitchesResult from pypsdm.models.result.grid.transformer import Transformers2WResult + from pypsdm.models.result.grid.congestions import CongestionsResult from pypsdm.models.result.participant.dict import ( EmsResult, EvcsResult, @@ -97,6 +104,8 @@ def get_result_dict_type(self) -> Type["EntitiesResultDictMixin"]: return Transformers2WResult case RawGridElementsEnum.SWITCH: return SwitchesResult + case RawGridElementsEnum.SUBGRID: + return CongestionsResult case SystemParticipantsEnum.ELECTRIC_VEHICLE: return EvsResult case SystemParticipantsEnum.EV_CHARGING_STATION: @@ -158,6 +167,7 @@ class RawGridElementsEnum(EntitiesEnum): TRANSFROMER_3_W = "transformer_3_w" SWITCH = "switch" MEASUREMENT_UNIT = "measurement_unit" + SUBGRID = "subgrid" class ThermalGridElementsEnum(EntitiesEnum): diff --git a/pypsdm/models/gwr.py b/pypsdm/models/gwr.py index 6c2c5cae..36857203 100644 --- a/pypsdm/models/gwr.py +++ b/pypsdm/models/gwr.py @@ -106,6 +106,10 @@ def transformers_2_w_res(self): def switches_res(self): return self.raw_grid_res.switches + @property + def congestions_res(self): + return self.results.raw_grid.congestions + @property def participants_res(self): return self.results.participants diff --git a/pypsdm/models/input/container/mixins.py b/pypsdm/models/input/container/mixins.py index c5673417..70dcb880 100644 --- a/pypsdm/models/input/container/mixins.py +++ b/pypsdm/models/input/container/mixins.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: from pypsdm.models.input.container.grid import GridContainer - from pypsdm.models.result.participant.dict import EntitiesResultDictMixin + from pypsdm.models.result.participant.dict import ResultDictMixin class ContainerMixin(ABC): @@ -118,8 +118,8 @@ def entities_from_csv( delimiter: str | None = None, filter_start: datetime | None = None, filter_end: datetime | None = None, - ) -> dict[EntitiesEnum, EntitiesResultDictMixin]: - from pypsdm.models.result.participant.dict import EntitiesResultDictMixin + ) -> dict[EntitiesEnum, ResultDictMixin]: + from pypsdm.models.result.participant.dict import ResultDictMixin res_files = [ f for f in os.listdir(simulation_data_path) if f.endswith("_res.csv") @@ -135,7 +135,7 @@ def entities_from_csv( with concurrent.futures.ProcessPoolExecutor() as executor: # warning: Breakpoints in the underlying method might not work when started from ipynb pa_from_csv_for_participant = partial( - EntitiesResultDictMixin.from_csv_for_entity, + ResultDictMixin.from_csv_for_entity, simulation_data_path, simulation_end, grid_container, diff --git a/pypsdm/models/input/container/raw_grid.py b/pypsdm/models/input/container/raw_grid.py index 09768cd9..fa928c67 100644 --- a/pypsdm/models/input/container/raw_grid.py +++ b/pypsdm/models/input/container/raw_grid.py @@ -77,6 +77,8 @@ def get_with_enum(self, enum: RawGridElementsEnum): return self.transformers_2_w case RawGridElementsEnum.SWITCH: return self.switches + case RawGridElementsEnum.SUBGRID: + return None case _: raise ValueError(f"Unknown enum {enum}") diff --git a/pypsdm/models/result/container/grid.py b/pypsdm/models/result/container/grid.py index 150cfd80..057ea8da 100644 --- a/pypsdm/models/result/container/grid.py +++ b/pypsdm/models/result/container/grid.py @@ -40,6 +40,10 @@ def transformers_2w(self): def switches(self): return self.raw_grid.switches + @property + def congestions(self): + return self.raw_grid.congestions + @property def ems(self): return self.participants.ems diff --git a/pypsdm/models/result/container/raw_grid.py b/pypsdm/models/result/container/raw_grid.py index 57ce3f15..d38d2b1a 100644 --- a/pypsdm/models/result/container/raw_grid.py +++ b/pypsdm/models/result/container/raw_grid.py @@ -9,6 +9,7 @@ from pypsdm.models.result.grid.node import NodesResult from pypsdm.models.result.grid.switch import SwitchesResult from pypsdm.models.result.grid.transformer import Transformers2WResult +from pypsdm.models.result.grid.congestions import CongestionsResult from pypsdm.models.ts.base import EntityKey @@ -18,6 +19,7 @@ class RawGridResultContainer(ResultContainerMixin): lines: LinesResult transformers_2w: Transformers2WResult switches: SwitchesResult + congestions: CongestionsResult def __init__(self, dct): def get_or_empty(key: RawGridElementsEnum, dict_type): @@ -36,6 +38,7 @@ def get_or_empty(key: RawGridElementsEnum, dict_type): RawGridElementsEnum.TRANSFORMER_2_W, Transformers2WResult ) self.switches = get_or_empty(RawGridElementsEnum.SWITCH, SwitchesResult) + self.congestions = get_or_empty(RawGridElementsEnum.SUBGRID, CongestionsResult) def __len__(self): return sum(len(v) for v in self.to_dict().values()) @@ -54,6 +57,7 @@ def to_dict(self, include_empty: bool = False) -> dict: RawGridElementsEnum.LINE: self.lines, RawGridElementsEnum.TRANSFORMER_2_W: self.transformers_2w, RawGridElementsEnum.SWITCH: self.switches, + RawGridElementsEnum.SUBGRID: self.congestions } if not include_empty: res = {k: v for k, v in res.items() if v} diff --git a/pypsdm/models/result/grid/__init__.py b/pypsdm/models/result/grid/__init__.py index 6d974433..4fc734dc 100644 --- a/pypsdm/models/result/grid/__init__.py +++ b/pypsdm/models/result/grid/__init__.py @@ -3,6 +3,7 @@ from .node import NodesResult from .switch import SwitchesResult, SwitchResult from .transformer import Transformer2WResult, Transformers2WResult +from .congestions import CongestionResult, CongestionsResult __all__ = [ "ConnectorCurrent", @@ -13,4 +14,5 @@ "LinesResult", "SwitchResult", "SwitchesResult", + "CongestionsResult" ] diff --git a/pypsdm/models/result/grid/congestions.py b/pypsdm/models/result/grid/congestions.py new file mode 100644 index 00000000..13f4f104 --- /dev/null +++ b/pypsdm/models/result/grid/congestions.py @@ -0,0 +1,91 @@ +from dataclasses import dataclass +from datetime import datetime + +from pandas import DataFrame + +from pypsdm.models.enums import RawGridElementsEnum +from pypsdm.models.result.participant.dict import SubgridResultDictMixin +from pypsdm.models.ts.base import ( + SubGridKey, + TimeSeries, + TimeSeriesDict, + TimeSeriesDictMixin +) + + +@dataclass +class CongestionResult(TimeSeries): + def __init__(self, data: DataFrame, end: datetime | None = None): + super().__init__(data, end) + + def __eq__(self, other: object) -> bool: + return super().__eq__(other) + + def __add__(self, _): + return NotImplemented + + @property + def vMin(self) -> float: + return self.data["vMin"].drop_duplicates()[0] + + @property + def vMax(self) -> float: + return self.data["vMax"].drop_duplicates()[0] + + @property + def subnet(self) -> int: + return self.data["subgrid"].drop_duplicates()[0] + + @property + def voltage(self): + return self.data["voltage"] + + @property + def line(self): + return self.data["line"] + + @property + def transformer(self): + return self.data["transformer"] + + @staticmethod + def attributes() -> list[str]: + return ["vMin", "vMax", "subgrid", "voltage", "line", "transformer"] + + +class CongestionsResult( + TimeSeriesDict[SubGridKey, CongestionResult], + TimeSeriesDictMixin, + SubgridResultDictMixin +): + def __eq__(self, other: object) -> bool: + return super().__eq__(other) + + @property + def vMin(self) -> DataFrame: + return self.attr_df("vMin") + + @property + def vMax(self) -> DataFrame: + return self.attr_df("vMax") + + @property + def subnet(self) -> DataFrame: + return self.attr_df("subgrid") + + @property + def voltage(self) -> DataFrame: + return self.attr_df("voltage") + + @property + def line(self) -> DataFrame: + return self.attr_df("line") + + @property + def transformer(self) -> DataFrame: + return self.attr_df("transformer") + + @classmethod + def entity_type(cls) -> RawGridElementsEnum: + return RawGridElementsEnum.SUBGRID + diff --git a/pypsdm/models/result/grid/line.py b/pypsdm/models/result/grid/line.py index 2a21c02a..7b45e0f8 100644 --- a/pypsdm/models/result/grid/line.py +++ b/pypsdm/models/result/grid/line.py @@ -23,7 +23,7 @@ def utilisation(self, lines: Lines, side: Literal["a", "b"] = "a") -> pd.DataFra i_max = lines.i_max data = pd.DataFrame( { - line_uuid: line.utilisation(i_max[line_uuid.uuid], side) + line_uuid.uuid: line.utilisation(i_max[line_uuid.uuid], side) for line_uuid, line in self.items() } ).sort_index() diff --git a/pypsdm/models/result/participant/dict.py b/pypsdm/models/result/participant/dict.py index dc6bd9d4..f44f81d5 100644 --- a/pypsdm/models/result/participant/dict.py +++ b/pypsdm/models/result/participant/dict.py @@ -2,7 +2,7 @@ import os import uuid -from abc import abstractmethod +from abc import abstractmethod, ABC from datetime import datetime from typing import TYPE_CHECKING, Self, Tuple, Type @@ -12,7 +12,7 @@ from pypsdm.io.utils import check_filter, csv_to_grpd_df, get_file_path, to_date_time from pypsdm.models.enums import EntitiesEnum, SystemParticipantsEnum from pypsdm.models.input.entity import Entities -from pypsdm.models.ts.base import EntityKey, TimeSeries +from pypsdm.models.ts.base import EntityKey, SubGridKey, TimeSeries from pypsdm.models.ts.types import ( ComplexPower, ComplexPowerDict, @@ -24,9 +24,7 @@ from pypsdm.models.input.container.grid import GridContainer -class EntitiesResultDictMixin: - def uuids(self) -> set[str]: - return {key.uuid for key in self.keys()} # type: ignore +class ResultDictMixin: @classmethod @abstractmethod @@ -37,6 +35,121 @@ def entity_type(cls) -> EntitiesEnum: def result_type(cls) -> Type[TimeSeries]: return cls.entity_type().get_result_type() + @classmethod + def from_csv( + cls, + simulation_data_path: str, + delimiter: str | None = None, + simulation_end: datetime | None = None, + input_entities: Entities | None = None, + filter_start: datetime | None = None, + filter_end: datetime | None = None, + must_exist: bool = True, + ) -> Self: + raise NotImplementedError + + @abstractmethod + def to_csv( + self, + path: str, + delimiter=",", + mkdirs=False, + resample_rate: str | None = None, + ): + return NotImplemented + + @staticmethod + def from_csv_for_entity( + simulation_data_path: str, + simulation_end: datetime | None, + grid_container: GridContainer | None, + entity: EntitiesEnum, + delimiter: str | None = None, + ) -> "ResultDictMixin" | Tuple[Exception, EntitiesEnum]: + try: + if grid_container: + input_entities = grid_container.get_with_enum(entity) + else: + input_entities = None + dict_type = entity.get_result_dict_type() + return dict_type.from_csv( + simulation_data_path, + delimiter=delimiter, + simulation_end=simulation_end, + input_entities=input_entities, + must_exist=False, + ) + + except Exception as e: + return e, entity + + +class SubgridResultDictMixin(ResultDictMixin): + def subgrids(self) -> set[int]: + return {key.subgrid for key in self.keys()} # type: ignore + + @classmethod + def from_csv( + cls, + simulation_data_path: str, + delimiter: str | None = None, + simulation_end: datetime | None = None, + input_entities: Entities | None = None, + filter_start: datetime | None = None, + filter_end: datetime | None = None, + must_exist: bool = True, + ) -> Self: + check_filter(filter_start, filter_end) + + file_name = cls.entity_type().get_csv_result_file_name() + path = get_file_path(simulation_data_path, file_name) + if path.exists(): + grpd_df = csv_to_grpd_df(file_name, simulation_data_path, "subgrid", delimiter) + else: + if must_exist: + raise FileNotFoundError(f"File {path} does not exist") + else: + return cls.empty() # type: ignore + + if len(grpd_df) == 0: + return cls.empty() # type: ignore + + if simulation_end is None: + simulation_end = to_date_time(grpd_df["time"].max().max()) # type: ignore + + ts_dict = {} + for key, grp in grpd_df: + result_key = None + + if isinstance(key, int): + result_key = SubGridKey(key, None) + else: + logger.warning("Entity {} is not a subgrid result.".format(key)) + + ts = cls.result_type()(grp, simulation_end) + ts_dict[result_key] = ts + + res = cls(ts_dict) + return ( + res + if not filter_start + else res.filter_for_time_interval(filter_start, filter_end) # type: ignore + ) + + def to_csv( + self, + path: str, + delimiter=",", + mkdirs=False, + resample_rate: str | None = None, + ): + return NotImplemented + + +class EntitiesResultDictMixin(ResultDictMixin): + def uuids(self) -> set[str]: + return {key.uuid for key in self.keys()} # type: ignore + @classmethod def from_csv( cls, @@ -53,7 +166,7 @@ def from_csv( file_name = cls.entity_type().get_csv_result_file_name() path = get_file_path(simulation_data_path, file_name) if path.exists(): - grpd_df = csv_to_grpd_df(file_name, simulation_data_path, delimiter) + grpd_df = csv_to_grpd_df(file_name, simulation_data_path, "input_model", delimiter) else: if must_exist: raise FileNotFoundError(f"File {path} does not exist") @@ -87,11 +200,11 @@ def from_csv( ) def to_csv( - self, - path: str, - delimiter=",", - mkdirs=False, - resample_rate: str | None = None, + self, + path: str, + delimiter=",", + mkdirs=False, + resample_rate: str | None = None, ): if mkdirs: os.makedirs(path, exist_ok=True) @@ -117,31 +230,6 @@ def prepare_data(data: pd.DataFrame, input_model: str): df = pd.concat(dfs) df.to_csv(os.path.join(path, file_name), sep=delimiter, index=True) - @staticmethod - def from_csv_for_entity( - simulation_data_path: str, - simulation_end: datetime | None, - grid_container: GridContainer | None, - entity: EntitiesEnum, - delimiter: str | None = None, - ) -> "EntitiesResultDictMixin" | Tuple[Exception, EntitiesEnum]: - try: - if grid_container: - input_entities = grid_container.get_with_enum(entity) - else: - input_entities = None - dict_type = entity.get_result_dict_type() - return dict_type.from_csv( - simulation_data_path, - delimiter=delimiter, - simulation_end=simulation_end, - input_entities=input_entities, - must_exist=False, - ) - - except Exception as e: - return (e, entity) - class EmsResult(ComplexPowerDict[EntityKey], EntitiesResultDictMixin): def __init__(self, data: dict[EntityKey, ComplexPower]): diff --git a/pypsdm/models/ts/base.py b/pypsdm/models/ts/base.py index 55b47dad..bac1ccc3 100644 --- a/pypsdm/models/ts/base.py +++ b/pypsdm/models/ts/base.py @@ -1,5 +1,5 @@ import copy -from abc import ABC +from abc import ABC, abstractmethod from collections import UserDict from dataclasses import dataclass from datetime import datetime @@ -200,7 +200,14 @@ def interval(self, start: datetime, end: datetime) -> Self: @dataclass(frozen=True) -class EntityKey: +class ResultKey: + @abstractmethod + def id(self): + return NotImplemented + + +@dataclass(frozen=True) +class EntityKey(ResultKey): uuid: str name: str | None = None @@ -219,6 +226,26 @@ def id(self) -> str: return self.name if self.name else self.uuid +@dataclass(frozen=True) +class SubGridKey(ResultKey): + subgrid: int + name: str | None = None + + def __eq__(self, other: object) -> bool: + if isinstance(other, SubGridKey): + return self.subgrid == other.subgrid + if isinstance(other, int): + return self.subgrid == other + return False + + def __hash__(self) -> int: + return hash(self.subgrid) + + @property + def id(self) -> str: + return self.name if self.name else self.subgrid + + class TimeSeriesDict(UserDict[K, V]): def __init__(self, data: dict[K, V]): for ts in data.values(): diff --git a/pypsdm/plots/grid.py b/pypsdm/plots/grid.py index 8764e3c2..5041636c 100644 --- a/pypsdm/plots/grid.py +++ b/pypsdm/plots/grid.py @@ -6,11 +6,12 @@ from shapely.geometry import LineString from pypsdm.models.input.container.grid import GridContainer -from pypsdm.plots.common.utils import BLUE, GREEN, GREY, RED, RGB, rgb_to_hex +from pypsdm.plots.common.utils import BLUE, GREEN, GREY, RED, RGB, rgb_to_hex, PURPLE def grid_plot( grid: GridContainer, + background: str = "open-street-map", node_highlights: Optional[Union[dict[RGB, list[str]], list[str]]] = None, line_highlights: Optional[Union[dict[RGB, list[str]], list[str]]] = None, highlight_disconnected: Optional[bool] = False, @@ -74,13 +75,15 @@ def grid_plot( fig.update_layout( # mapbox = {"zoom"=10}, showlegend=False, - mapbox_style="open-street-map", + mapbox_style=background, margin={"r": 0, "t": 0, "l": 0, "b": 0}, mapbox=dict( center=dict(lat=center_lat, lon=center_lon), zoom=zoom, # Adjust the zoom level as per the calculated heuristic - style="open-street-map", + style=background, ), + width=1920, + height=1080 ) return fig @@ -171,7 +174,7 @@ def to_hover_text(node_data: Series): ) def _node_trace(data, color): - text = data.apply(lambda node_data: to_hover_text(node_data), axis=1).to_list() + text = data.apply(lambda node_data: to_hover_text(node_data), axis=1) fig.add_trace( go.Scattermapbox(