{ "cells": [ { "cell_type": "markdown", "id": "metropolitan-humor", "metadata": {}, "source": [ "### Time series calculation with OPF" ] }, { "cell_type": "markdown", "id": "chinese-wilderness", "metadata": {}, "source": [ "This tutorial shows how a simple time series simulation with optimal power flow is performed with the timeseries and control module in pandapower. " ] }, { "cell_type": "code", "execution_count": null, "id": "outer-scroll", "metadata": {}, "outputs": [], "source": [ "import os\n", "import tempfile\n", "import pandas as pd\n", "import numpy as np\n", "\n", "from pandapower.timeseries import DFData, OutputWriter, run_timeseries\n", "from pandapower.control import ConstControl\n", "from pandapower.create import (\n", " create_empty_network,\n", " create_bus,\n", " create_ext_grid,\n", " create_load,\n", " create_poly_cost,\n", " create_lines,\n", " create_sgen\n", ")\n", "from pandapower.run import runopp\n", "\n", "import matplotlib.pyplot as plt\n", "\n", "rng = np.random.default_rng(10)\n", "%matplotlib inline " ] }, { "cell_type": "markdown", "id": "shared-state", "metadata": {}, "source": [ "We created a simple network and then set the constraints for buses, lines, and external grid. We set the costs for the external grid and sgen. The cost of sgen is kept negative to maximize the generation, sgen can be controlled with OPF but not the load." ] }, { "cell_type": "code", "execution_count": null, "id": "negative-owner", "metadata": {}, "outputs": [], "source": [ "def simple_test_net():\n", " net = create_empty_network()\n", "\n", " b0 = create_bus(net, 110, min_vm_pu=0.98, max_vm_pu=1.05)\n", " b1 = create_bus(net, 20, min_vm_pu=0.98, max_vm_pu=1.05)\n", " b2 = create_bus(net, 20, min_vm_pu=0.98, max_vm_pu=1.05)\n", " b3 = create_bus(net, 20, min_vm_pu=0.9, max_vm_pu=1.05)\n", " \n", " e = create_ext_grid(net, b0, min_p_mw=-200, max_p_mw=200)\n", " costeg = create_poly_cost(net, e, 'ext_grid', cp1_eur_per_mw=10)\n", " \n", " create_lines(net, [b0, b1, b1], [b1, b2, b3], 10, \"149-AL1/24-ST1A 110.0\", max_loading_percent=80)\n", "\n", " create_load(net, b3, p_mw=10., q_mvar=-5., name='load1', controllable=False)\n", " g1=create_sgen(net, b2, p_mw=0., q_mvar=-2, min_p_mw=0, max_p_mw=30, min_q_mvar=-3, max_q_mvar=3, name='sgen1', controllable=True)\n", " create_poly_cost(net, g1, 'sgen1', cp1_eur_per_mw=-1)\n", " \n", " return net\n" ] }, { "cell_type": "markdown", "id": "generous-punishment", "metadata": {}, "source": [ "We created the datasource (which contains the time series P values) and defined the range of the load power values so that the line that connects it will not be overloaded because the load will not be controlled with the OPF." ] }, { "cell_type": "code", "execution_count": null, "id": "under-lewis", "metadata": {}, "outputs": [], "source": [ "def create_data_source(n_timesteps=24):\n", " profiles = pd.DataFrame()\n", " profiles['load1_p'] = rng.random(n_timesteps) * 10.\n", " profiles['sgen1_p'] = rng.random(n_timesteps) * 20.\n", "\n", " ds = DFData(profiles)\n", "\n", " return profiles, ds" ] }, { "cell_type": "markdown", "id": "identical-madagascar", "metadata": {}, "source": [ "We created the controllers to update the P values of the load and the sgen\n" ] }, { "cell_type": "code", "execution_count": null, "id": "dutch-three", "metadata": {}, "outputs": [], "source": [ "def create_controllers(net, ds):\n", " ConstControl(net, element='load', variable='p_mw', element_index=[0],\n", " data_source=ds, profile_name=[\"load1_p\"])\n", " ConstControl(net, element='sgen', variable='p_mw', element_index=[0],\n", " data_source=ds, profile_name=[\"sgen1_p\"])" ] }, { "cell_type": "markdown", "id": "patient-battle", "metadata": {}, "source": [ "Instead of saving the whole net (which takes a lot of time), we extract only predefined outputs. The variables of create_output_writer are saved to the hard drive after the time series loop.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "under-console", "metadata": {}, "outputs": [], "source": [ "def create_output_writer(net, time_steps, output_dir):\n", " ow = OutputWriter(net, time_steps, output_path=output_dir, output_file_type=\".xlsx\", log_variables=[])\n", " ow.log_variable('res_sgen', 'p_mw')\n", " ow.log_variable('res_bus', 'vm_pu')\n", " ow.log_variable('res_line', 'loading_percent')\n", " ow.log_variable('res_line', 'i_ka')\n", " return ow" ] }, { "cell_type": "markdown", "id": "adjacent-adventure", "metadata": {}, "source": [ "Lets run the code for the timeseries simulation with OPF. Note that parameter 'run' is set to the function that runs OPF (run=runopp)." ] }, { "cell_type": "code", "execution_count": null, "id": "polyphonic-distributor", "metadata": {}, "outputs": [], "source": [ "output_dir = os.path.join(tempfile.gettempdir(), \"time_series_example\")\n", "print(\"Results can be found in your local temp folder: {}\".format(output_dir))\n", "if not os.path.exists(output_dir):\n", " os.mkdir(output_dir)\n", " \n", "# create the network\n", "net = simple_test_net()\n", "\n", "# create (random) data source\n", "n_timesteps = 24\n", "profiles, ds = create_data_source(n_timesteps)\n", "\n", "# create controllers (to control P values of the load and the sgen)\n", "create_controllers(net, ds)\n", "\n", "# time steps to be calculated. Could also be a list with non-consecutive time steps\n", "time_steps = range(0, n_timesteps)\n", "\n", "# the output writer with the desired results to be stored to files.\n", "ow = create_output_writer(net, time_steps, output_dir=output_dir)\n", "\n", "# the main time series function with optimal power flow\n", "run_timeseries(net, time_steps, run=runopp, delta=1e-16)" ] }, { "cell_type": "markdown", "id": "alternate-science", "metadata": {}, "source": [ "We can see that all of the bus voltages are in the defined constraint range according to the optimal power flow." ] }, { "cell_type": "code", "execution_count": null, "id": "collected-marshall", "metadata": {}, "outputs": [], "source": [ "x_label = \"time step\"\n", "# voltage results\n", "vm_pu_file = os.path.join(output_dir, \"res_bus\", \"vm_pu.xlsx\")\n", "vm_pu = pd.read_excel(vm_pu_file, index_col=0)\n", "vm_pu.plot(label=\"vm_pu\")\n", "plt.xlabel(x_label)\n", "plt.ylabel(\"voltage mag. [p.u.]\")\n", "plt.title(\"Voltage Magnitude\")\n", "plt.grid()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "native-delicious", "metadata": {}, "source": [ "The loading_percent of the lines are also below 80% as defined the constraints for optimal power flow." ] }, { "cell_type": "code", "execution_count": null, "id": "reasonable-tennis", "metadata": {}, "outputs": [], "source": [ "# line loading results\n", "ll_file = os.path.join(output_dir, \"res_line\", \"loading_percent.xlsx\")\n", "line_loading = pd.read_excel(ll_file, index_col=0)\n", "line_loading.plot()\n", "plt.xlabel(x_label)\n", "plt.ylabel(\"line loading [%]\")\n", "plt.title(\"Line Loading\")\n", "plt.grid()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "popular-shareware", "metadata": {}, "source": [ "Here we compared the sgen power generation before and after OPF." ] }, { "cell_type": "code", "execution_count": null, "id": "suitable-indonesia", "metadata": {}, "outputs": [], "source": [ "# sgen results\n", "sgen_file = os.path.join(output_dir, \"res_sgen\", \"p_mw.xlsx\")\n", "sgen = pd.read_excel(sgen_file, index_col=0)\n", "ax=sgen[0].plot(label=\"sgen (after OPF)\")\n", "ds.df.sgen1_p.plot(ax=ax, label=\"sgen (original)\", linestyle='--')\n", "ax.legend()\n", "plt.xlabel(x_label)\n", "plt.ylabel(\"P [MW]\")\n", "plt.grid()\n", "plt.show()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 }