{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Hosting Capacity\n", "\n", "The term PV hosting capacity is defined as the maximum PV capacity which can be connected to a specific grid, while still complying with relevant grid codes and grid planning principles. \n", "\n", "Here we will introduce a basic algorithm to calculate PV hosting capacity with pandapower.\n", "\n", "The basic idea of calculating hosting capacity is to increase PV installation until a violation of any planning principle or constraint occurs. To analyse hosting capacity, we need three basic building blocks:\n", "1. Evaluating constraint violations\n", "2. Chosing connection points for new PV plants\n", "3. Defining the installed power of new PV plants " ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "jupyter": { "outputs_hidden": true } }, "source": [ "### Evaluation of constraint violations\n", "\n", "Our example function that evaluates constraint violation is defined as:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-10-27T09:24:21.119957Z", "start_time": "2025-10-27T09:24:19.790663Z" } }, "outputs": [], "source": [ "import pandas as pd\n", "from numpy.random import default_rng\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "\n", "from pandapower.run import runpp\n", "from pandapower.create import create_sgen\n", "from pandapower.networks import mv_oberrhein" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-10-27T09:24:21.135973Z", "start_time": "2025-10-27T09:24:21.126973Z" } }, "outputs": [], "source": [ "rng = default_rng(0)\n", "\n", "def violations(net):\n", " runpp(net)\n", " if net.res_line.loading_percent.max() > 50:\n", " return (True, \"Line \\n Overloading\")\n", " elif net.res_trafo.loading_percent.max() > 50:\n", " return (True, \"Transformer \\n Overloading\")\n", " elif net.res_bus.vm_pu.max() > 1.04:\n", " return (True, \"Voltage \\n Violation\")\n", " else:\n", " return (False, None)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The function runs a power flow and then checks for line loading and transformer loading (both of which have to be below 50%) and for voltage rise (which has to be below 1.04 pu). The function returns a boolean flag to signal if any constraint is violated as well as a string that indicates the type of constraint violation." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Chosing a connection bus\n", "\n", "If new PV plants are installed, a connection bus has to be chosen. Here, we chose one random bus of each of the buses that have a load connection:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-10-27T09:24:21.150975Z", "start_time": "2025-10-27T09:24:21.143976Z" } }, "outputs": [], "source": [ "def chose_bus(net):\n", " return rng.choice(net.load.bus.values)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Chosing a PV plant size\n", "\n", "The function that returns a plant size is given as:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-10-27T09:24:21.166990Z", "start_time": "2025-10-27T09:24:21.157976Z" } }, "outputs": [], "source": [ "def get_plant_size_mw():\n", " return rng.normal(loc=0.5, scale=0.05)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This function returns a random value from a normal distribution with a mean of 0.5 MW and a standard deviation of 0.05 MW. Depending on the existing information, it would also be possible to use other probability distributions, such as a Weibull distribution, or to draw values from existing plant sizes." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Evaluating Hosting Capacity\n", "\n", "We now use these building blocks to evaluate hosting capacity in a generic network. We use the MV Oberrhein network from the pandapower networks package as an example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-10-27T09:24:21.182972Z", "start_time": "2025-10-27T09:24:21.172977Z" } }, "outputs": [], "source": [ "def load_network():\n", " return mv_oberrhein(scenario=\"generation\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The hosting capacity is then evaluated like this:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-10-27T09:25:24.763841Z", "start_time": "2025-10-27T09:24:21.188978Z" } }, "outputs": [], "source": [ "iterations = 50\n", "results = pd.DataFrame(columns=[\"installed\", \"violation\"])\n", "\n", "for i in range(iterations):\n", " net = load_network()\n", " installed_mw = 0\n", " while 1:\n", " violated, violation_type = violations(net)\n", " if violated:\n", " results.loc[i] = [installed_mw, violation_type]\n", " break\n", " else:\n", " plant_size = get_plant_size_mw()\n", " create_sgen(net, chose_bus(net), p_mw=plant_size, q_mvar=0)\n", " installed_mw += plant_size" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This algorithm adds new PV plants until a violation of any constraint occurs. Then, it saves the installed PV capacity. This is carried out for a number of iteration (here: 50) to get a distribution of hosting capacity values depending on connection points and plant sizes.\n", "\n", "The results can be visualized using matplotlib and seaborn:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { "end_time": "2025-10-27T09:25:25.093313Z", "start_time": "2025-10-27T09:25:24.925287Z" } }, "outputs": [], "source": [ "%matplotlib inline\n", "plt.rc('xtick', labelsize=18) # fontsize of the tick labels\n", "plt.rc('ytick', labelsize=18) # fontsize of the tick labels\n", "plt.rc('legend', fontsize=18) # fontsize of the tick labels\n", "plt.rc('axes', labelsize=20) # fontsize of the tick labels\n", "plt.rcParams['font.size'] = 20\n", "\n", "sns.set_style(\"whitegrid\", {'axes.grid' : False})\n", "fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10,5))\n", "ax = axes[0]\n", "sns.boxplot(results.installed, width=.1, ax=ax, orient=\"v\")\n", "ax.set_ylabel(\"Installed Capacity [MW]\")\n", "\n", "ax = axes[1]\n", "ax.axis(\"equal\")\n", "results.violation.value_counts().plot(kind=\"pie\", ax=ax, autopct=lambda x:\"%.0f %%\"%x)\n", "ax.set_ylabel(\"\")\n", "ax.set_xlabel(\"\")\n", "sns.despine()\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true, "jupyter": { "outputs_hidden": true } }, "source": [ "Note that this is only an example for a basic algorithm in order to demonstrate how such problems can be tackled with pandapower. Algorithms applied in real case studies might include Q-control of PV plants, transformer tap controllers, more sophisticated distribution of PV plants, probability distribution different buses, binary search for the hosting capacity evaluation etc." ] } ], "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": 4 }