From 5cbe9a72a65e149c54b9172e87825f2782ea43ac Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Thu, 22 May 2025 17:17:05 +0200 Subject: [PATCH 01/13] add BadPointPressureLiftController tutorial notebook for district heating network --- tutorials/BadPointPressureController.ipynb | 300 +++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 tutorials/BadPointPressureController.ipynb diff --git a/tutorials/BadPointPressureController.ipynb b/tutorials/BadPointPressureController.ipynb new file mode 100644 index 000000000..bfe9a1b76 --- /dev/null +++ b/tutorials/BadPointPressureController.ipynb @@ -0,0 +1,300 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Building a bad point pressure lift Controller for a district heating network" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## BadPointPressureLiftController: Pressure Control at the Worst Point\n", + "\n", + "The `BadPointPressureLiftController` is a custom controller designed for district heating networks modeled with pandapipes. Its main purpose is to maintain a minimum pressure difference at the network's \"worst point\"—the heat exchanger with the lowest pressure difference (Schlechtpunktregelung).\n", + "\n", + "### Key Features\n", + "\n", + "- **Automatic Worst Point Detection:** Identifies the heat exchanger with the lowest pressure difference where heat flow is present.\n", + "- **Pressure Regulation:** Adjusts the circulation pump's lift and flow pressures to ensure the pressure difference at the worst point meets a specified minimum target.\n", + "- **Proportional Control:** Uses a proportional gain to determine the adjustment magnitude based on the deviation from the target pressure difference.\n", + "- **Standby Mode:** If no heat flow is detected, the controller switches the pump to a standby mode with minimum lift and flow pressures.\n", + "- **Convergence Check:** Determines if the pressure difference is within a specified tolerance of the target, signaling convergence.\n", + "\n", + "### Usage\n", + "\n", + "- **Initialization:** The controller is initialized with the network, pump index, target pressure difference, tolerance, proportional gain, and minimum pressure settings.\n", + "- **Integration:** It can be integrated into a simulation loop, automatically adjusting pump pressures at each time step to maintain optimal network operation.\n", + "\n", + "This controller is particularly useful for ensuring reliable and efficient operation in district heating systems, where maintaining a minimum pressure difference at the most critical point is essential for system stability and performance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pandapower.control.basic_controller import BasicCtrl\n", + "\n", + "\n", + "class BadPointPressureLiftController(BasicCtrl):\n", + " \"\"\"\n", + " A controller for maintaining the pressure difference at the worst point (German: Differenzdruckregelung im Schlechtpunkt) in the network.\n", + " \n", + " Args:\n", + " net (pandapipesNet): The pandapipes network.\n", + " circ_pump_pressure_idx (int, optional): Index of the circulation pump. Defaults to 0.\n", + " target_dp_min_bar (float, optional): Target minimum pressure difference in bar. Defaults to 1.\n", + " tolerance (float, optional): Tolerance for pressure difference. Defaults to 0.2.\n", + " proportional_gain (float, optional): Proportional gain for the controller. Defaults to 0.2.\n", + " min_plift (float, optional): Minimum lift pressure in bar. Defaults to 1.5.\n", + " min_pflow (float, optional): Minimum flow pressure in bar. Defaults to 3.5.\n", + " **kwargs: Additional keyword arguments.\n", + " \"\"\"\n", + " def __init__(self, net, circ_pump_pressure_idx=0, target_dp_min_bar=1, tolerance=0.2, proportional_gain=0.2, min_plift=1.5, min_pflow=3.5, **kwargs):\n", + " super(BadPointPressureLiftController, self).__init__(net, **kwargs)\n", + " self.circ_pump_pressure_idx = circ_pump_pressure_idx\n", + " self.target_dp_min_bar = target_dp_min_bar\n", + " self.tolerance = tolerance\n", + " self.proportional_gain = proportional_gain\n", + "\n", + " self.min_plift = min_plift # Minimum pressure in bar\n", + " self.min_pflow = min_pflow # Minimum lift pressure in bar\n", + "\n", + " self.iteration = 0 # Add iteration counter\n", + "\n", + " self.dp_min, self.heat_consumer_idx = self.calculate_worst_point(net)\n", + "\n", + " def calculate_worst_point(self, net):\n", + " \"\"\"Calculate the worst point in the heating network, defined as the heat exchanger with the lowest pressure difference.\n", + "\n", + " Args:\n", + " net (pandapipesNet): The pandapipes network.\n", + "\n", + " Returns:\n", + " tuple: The minimum pressure difference and the index of the worst point.\n", + " \"\"\"\n", + " \n", + " dp = []\n", + "\n", + " for idx, qext, p_from, p_to in zip(net.heat_consumer.index, net.heat_consumer[\"qext_w\"], net.res_heat_consumer[\"p_from_bar\"], net.res_heat_consumer[\"p_to_bar\"]):\n", + " if qext != 0:\n", + " dp_diff = p_from - p_to\n", + " dp.append((dp_diff, idx))\n", + "\n", + " if not dp:\n", + " return 0, -1\n", + "\n", + " # Find the minimum delta p where the heat flow is not zero\n", + " dp_min, idx_min = min(dp, key=lambda x: x[0])\n", + "\n", + " return dp_min, idx_min\n", + "\n", + " def time_step(self, net, time_step):\n", + " \"\"\"Reset the iteration counter at the start of each time step.\n", + "\n", + " Args:\n", + " net (pandapipesNet): The pandapipes network.\n", + " time_step (int): The current time step.\n", + "\n", + " Returns:\n", + " int: The current time step.\n", + " \"\"\"\n", + " self.iteration = 0 # reset iteration counter\n", + " self.dp_min, self.heat_consumer_idx = self.calculate_worst_point(net)\n", + "\n", + " return time_step\n", + "\n", + " def is_converged(self, net):\n", + " \"\"\"Check if the controller has converged.\n", + "\n", + " Args:\n", + " net (pandapipesNet): The pandapipes network.\n", + "\n", + " Returns:\n", + " bool: True if converged, False otherwise.\n", + " \"\"\"\n", + "\n", + " if all(net.heat_consumer[\"qext_w\"] == 0):\n", + " return True\n", + " \n", + " current_dp_bar = net.res_heat_consumer[\"p_from_bar\"].at[self.heat_consumer_idx] - net.res_heat_consumer[\"p_to_bar\"].at[self.heat_consumer_idx]\n", + "\n", + " # Check if the pressure difference is within tolerance\n", + " dp_within_tolerance = abs(current_dp_bar - self.target_dp_min_bar) < self.tolerance\n", + "\n", + " if dp_within_tolerance == True:\n", + " return dp_within_tolerance\n", + "\n", + " def control_step(self, net):\n", + " \"\"\"Adjust the pump pressure to maintain the target pressure difference.\n", + "\n", + " Args:\n", + " net (pandapipesNet): The pandapipes network.\n", + " \"\"\"\n", + " # Increment iteration counter\n", + " self.iteration += 1\n", + "\n", + " \"\"\"Adjust the pump pressure or switch to standby mode when heat flow is zero.\"\"\"\n", + " if all(net.heat_consumer[\"qext_w\"] == 0):\n", + " # Switch to standby mode\n", + " print(\"No heat flow detected. Switching to standby mode.\")\n", + " net.circ_pump_pressure[\"plift_bar\"].iloc[:] = self.min_plift # Minimum lift pressure\n", + " net.circ_pump_pressure[\"p_flow_bar\"].iloc[:] = self.min_pflow # Minimum flow pressure\n", + " return super(BadPointPressureLiftController, self).control_step(net)\n", + "\n", + " # Check whether the heat flow in the heat exchanger is zero\n", + " current_dp_bar = net.res_heat_consumer[\"p_from_bar\"].at[self.heat_consumer_idx] - net.res_heat_consumer[\"p_to_bar\"].at[self.heat_consumer_idx]\n", + " current_plift_bar = net.circ_pump_pressure[\"plift_bar\"].at[self.circ_pump_pressure_idx]\n", + " current_pflow_bar = net.circ_pump_pressure[\"p_flow_bar\"].at[self.circ_pump_pressure_idx]\n", + "\n", + " dp_error = self.target_dp_min_bar - current_dp_bar\n", + " \n", + " plift_adjustment = dp_error * self.proportional_gain\n", + " pflow_adjustment = dp_error * self.proportional_gain \n", + "\n", + " new_plift = current_plift_bar + plift_adjustment\n", + " new_pflow = current_pflow_bar + pflow_adjustment\n", + " \n", + " net.circ_pump_pressure[\"plift_bar\"].at[self.circ_pump_pressure_idx] = new_plift\n", + " net.circ_pump_pressure[\"p_flow_bar\"].at[self.circ_pump_pressure_idx] = new_pflow\n", + "\n", + " return super(BadPointPressureLiftController, self).control_step(net)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example Usage of the BadPointPressureLiftController\n", + "\n", + "To demonstrate the usage of the `BadPointPressureLiftController`, we provide an example with a simple test network. The network is initialized using a `initialize_test_net` function, which sets up a district heating system with two heat consumers, a circulation pump, and several pipes and junctions.\n", + "\n", + "The controller is instantiated and added to the network as follows:\n", + "\n", + "```python\n", + "net = initialize_test_net()\n", + "\n", + "dp_controller = BadPointPressureLiftController(net)\n", + "net.controller.loc[len(net.controller)] = [dp_controller, True, -1, -1, False, False]\n", + "```\n", + "\n", + "This function performs the following steps:\n", + "- Creates a pandapipes network with water as the working fluid.\n", + "- Adds junctions for the pump, pipes, and heat exchangers.\n", + "- Installs a circulation pump with specified flow and lift pressures.\n", + "- Adds two heat consumers with configurable heat extraction and return temperatures.\n", + "- Connects all components with pipes.\n", + "- Runs an initial pipeflow calculation.\n", + "- Instantiates the `BadPointPressureLiftController` and registers it in the network's controller table.\n", + "\n", + "Once the network is initialized, the controller will automatically regulate the pump pressures during simulation to maintain the minimum pressure difference at the worst point (the heat exchanger with the lowest pressure difference). This ensures reliable operation and helps prevent under-supply at critical points in the network.\n", + "\n", + "In my implementation, a dp_min of 1 bar is used in the Controller." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandapipes as pp\n", + "import numpy as np\n", + "\n", + "def initialize_test_net(qext_w=np.array([100000, 200000]),\n", + " return_temperature=np.array([55, 60]),\n", + " supply_temperature=85, \n", + " flow_pressure_pump=4,\n", + " lift_pressure_pump=1.5,\n", + " pipetype=\"110/202 PLUS\"):\n", + " \n", + " print(\"Running the test network initialization script.\")\n", + " net = pp.create_empty_network(fluid=\"water\")\n", + "\n", + " k = 0.1 # roughness defaults to 0.1\n", + "\n", + " suply_temperature_k = supply_temperature + 273.15\n", + " return_temperature_k = return_temperature + 273.15\n", + "\n", + " # Junctions for pump\n", + " j1 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 1\", geodata=(0, 10))\n", + " j2 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 2\", geodata=(0, 0))\n", + "\n", + " # Junctions for connection pipes forward line\n", + " j3 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 3\", geodata=(10, 0))\n", + " j4 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 4\", geodata=(60, 0))\n", + "\n", + " # Junctions for heat exchangers\n", + " j5 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 5\", geodata=(85, 0))\n", + " j6 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 6\", geodata=(85, 10))\n", + " \n", + " # Junctions for connection pipes return line\n", + " j7 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 7\", geodata=(60, 10))\n", + " j8 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 8\", geodata=(10, 10))\n", + "\n", + " pump1 = pp.create_circ_pump_const_pressure(net, j1, j2, p_flow_bar=flow_pressure_pump, plift_bar=lift_pressure_pump, \n", + " t_flow_k=suply_temperature_k, type=\"auto\", name=\"pump1\")\n", + "\n", + " pipe1 = pp.create_pipe(net, j2, j3, std_type=pipetype, length_km=0.01, k_mm=k, name=\"pipe1\", sections=5, text_k=283)\n", + " pipe2 = pp.create_pipe(net, j3, j4, std_type=pipetype, length_km=0.05, k_mm=k, name=\"pipe2\", sections=5, text_k=283)\n", + " pipe3 = pp.create_pipe(net, j4, j5, std_type=pipetype, length_km=0.025,k_mm=k, name=\"pipe3\", sections=5, text_k=283)\n", + "\n", + " heat_cosnumer1 = pp.create_heat_consumer(net, from_junction=j5, to_junction=j6, loss_coefficient=0, qext_w=qext_w[0], \n", + " treturn_k=return_temperature_k[0], name=\"heat_consumer_1\") # treturn_k=t when implemented in function\n", + " \n", + "\n", + " heat_cosnumer2 = pp.create_heat_consumer(net, from_junction=j4, to_junction=j7, loss_coefficient=0, qext_w=qext_w[1], \n", + " treturn_k=return_temperature_k[1], name=\"heat_consumer_2\") # treturn_k=t when implemented in function\n", + " \n", + " pipe4 = pp.create_pipe(net, j6, j7, std_type=pipetype, length_km=0.25, k_mm=k, name=\"pipe4\", sections=5, text_k=283)\n", + " pipe5 = pp.create_pipe(net, j7, j8, std_type=pipetype, length_km=0.05, k_mm=k, name=\"pipe5\", sections=5, text_k=283)\n", + " pipe6 = pp.create_pipe(net, j8, j1, std_type=pipetype, length_km=0.01, k_mm=k, name=\"pipe6\", sections=5, text_k=283)\n", + "\n", + " pp.pipeflow(net, mode=\"bidirectional\", iter=100)\n", + "\n", + " return net\n", + "\n", + "net = initialize_test_net()\n", + "\n", + "dp_controller = BadPointPressureLiftController(net)\n", + "net.controller.loc[len(net.controller)] = [dp_controller, True, -1, -1, False, False]\n", + "\n", + "pp.pipeflow(net, mode=\"bidirectional\", iter=100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can now proceed to run time-series simulations or further analyses, and the controller will handle pressure adjustments as needed. \n", + "\n", + "Suggestions for improvements or alternative approaches are appreciated. Please feel free to contribute." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.8.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From a5269abc99ed48d6e3bb6001fd75a8082fb75d0f Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Thu, 28 Aug 2025 10:12:05 +0200 Subject: [PATCH 02/13] Create MinimumSupplyTemperatureController.ipynb --- .../MinimumSupplyTemperatureController.ipynb | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 tutorials/MinimumSupplyTemperatureController.ipynb diff --git a/tutorials/MinimumSupplyTemperatureController.ipynb b/tutorials/MinimumSupplyTemperatureController.ipynb new file mode 100644 index 000000000..2e39d00fd --- /dev/null +++ b/tutorials/MinimumSupplyTemperatureController.ipynb @@ -0,0 +1,207 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Building a minimum supply temperature controller for heat consumers in a district heating network" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## MinimumSupplyTemperatureController\n", + "\n", + "The `MinimumSupplyTemperatureController` is designed to ensure that the supply temperature at each heat consumer in a district heating network does not fall below a specified minimum value. This is particularly important during periods of low heat demand, when water circulation slows and the supply temperature can drop—sometimes even below the return temperature—leading to operational issues and inefficient heating.\n", + "\n", + "This controller monitors the supply temperature at the heat consumer and, if it detects that the temperature is below the required minimum, it incrementally increases the target return temperature. This adjustment forces the network to increase the supply temperature, thereby maintaining the desired minimum level. The controller uses a PID-like approach with configurable proportional, integral, and derivative gains, as well as a tolerance for convergence and a maximum number of iterations per time step.\n", + "\n", + "Typical use cases include:\n", + "- Preventing supply temperatures from dropping below return temperatures during low load or standby conditions.\n", + "- Maintaining reliable and efficient operation of district heating systems, especially in networks with variable or low demand.\n", + "\n", + "The controller is flexible and can be integrated into time-series simulations, supporting dynamic adjustment of the minimum supply temperature via an external data source if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from pandapower.control.basic_controller import BasicCtrl\n", + "\n", + "\n", + "class MinimumSupplyTemperatureController(BasicCtrl):\n", + " \"\"\"\n", + " A controller for maintaining the min supply temperature of the heat consumers in the network.\n", + " \n", + " Args:\n", + " net (pandapipesNet): The pandapipes network.\n", + " heat_consumer_idx (int): Index of the heat consumer.\n", + " min_supply_temperature (float, optional): Minimum supply temperature. Defaults to 65.\n", + " tolerance (float, optional): Tolerance for temperature difference. Defaults to 2.\n", + " max_iterations (int, optional): Maximum number of iterations. Defaults to 100.\n", + " temperature_adjustment_step (float, optional): Step to adjust the target return temperature. Defaults to 1.\n", + " debug (bool, optional): Flag to enable debug output. Defaults to False.\n", + " **kwargs: Additional keyword arguments.\n", + " \"\"\"\n", + " def __init__(self, net, heat_consumer_idx, min_supply_temperature=65, tolerance=2, max_iterations=100, temperature_adjustment_step=1, debug=False, **kwargs):\n", + " super(MinimumSupplyTemperatureController, self).__init__(net, **kwargs)\n", + " self.heat_consumer_idx = heat_consumer_idx\n", + " self.min_supply_temperature = min_supply_temperature\n", + " self.tolerance = tolerance\n", + " self.max_iterations = max_iterations\n", + " self.temperature_adjustment_step = temperature_adjustment_step # Step to adjust the target return temperature\n", + " self.debug = debug\n", + "\n", + " self.data_source = None\n", + " self.iteration = 0 # Add iteration counter\n", + " self.previous_temperatures = [] # Use a list to store previous temperatures\n", + "\n", + " def time_step(self, net, time_step):\n", + " \"\"\"Reset the controller parameters at the start of each time step.\n", + "\n", + " Args:\n", + " net (pandapipesNet): The pandapipes network.\n", + " time_step (int): The current time step.\n", + "\n", + " Returns:\n", + " int: The current time step.\n", + " \"\"\"\n", + " self.iteration = 0 # reset iteration counter\n", + " self.previous_temperatures = [] # Reset to an empty list\n", + "\n", + " if time_step == 0:\n", + " # Store the standard return temperature for the heat consumer\n", + " self.standard_return_temperature = net.heat_consumer[\"treturn_k\"].at[self.heat_consumer_idx]\n", + "\n", + " else:\n", + " # Restore the standard return temperature for the heat consumer\n", + " net.heat_consumer[\"treturn_k\"].at[self.heat_consumer_idx] = self.standard_return_temperature\n", + "\n", + " # Check if a data source exists and get the target temperature for the current time step\n", + " if self.data_source is not None:\n", + " self.min_supply_temperature = self.data_source.df.at[time_step, f'min_supply_temperature']\n", + " \n", + " return time_step\n", + "\n", + " def get_weighted_average_temperature(self):\n", + " \"\"\"Calculate the weighted average of the previous temperatures.\n", + "\n", + " Returns:\n", + " float: The weighted average temperature.\n", + " \"\"\"\n", + " if len(self.previous_temperatures) == 0:\n", + " return None\n", + " weights = np.arange(1, len(self.previous_temperatures) + 1)\n", + " weighted_avg = np.dot(self.previous_temperatures, weights) / weights.sum()\n", + " return weighted_avg\n", + "\n", + " def control_step(self, net):\n", + " \"\"\"Adjust the mass flow to maintain the target return temperature.\n", + "\n", + " Args:\n", + " net (pandapipesNet): The pandapipes network.\n", + " \"\"\"\n", + " # Increment iteration counter\n", + " self.iteration += 1\n", + " \n", + " if all(net.heat_consumer[\"qext_w\"] == 0):\n", + " # Switch to standby mode\n", + " print(\"No heat flow detected. Switching to standby mode.\")\n", + " return super(MinimumSupplyTemperatureController, self).control_step(net)\n", + "\n", + " # Calculate new mass flow\n", + " current_T_out = net.res_heat_consumer[\"t_to_k\"].at[self.heat_consumer_idx] - 273.15\n", + " current_T_in = net.res_heat_consumer[\"t_from_k\"].at[self.heat_consumer_idx] - 273.15\n", + "\n", + " weighted_avg_T_in = self.get_weighted_average_temperature()\n", + " if weighted_avg_T_in is not None:\n", + " current_T_in = weighted_avg_T_in\n", + "\n", + " current_mass_flow = net.res_heat_consumer[\"mdot_from_kg_per_s\"].at[self.heat_consumer_idx]\n", + "\n", + " # Ensure the supply temperature does not fall below the minimum supply temperature\n", + " if current_T_in < self.min_supply_temperature:\n", + " new_T_out = net.heat_consumer[\"treturn_k\"].at[self.heat_consumer_idx] + self.temperature_adjustment_step\n", + " net.heat_consumer[\"treturn_k\"].at[self.heat_consumer_idx] = new_T_out\n", + " \n", + " if self.debug:\n", + " print(f\"Minimum supply temperature not met. Adjusted target output temperature to {new_T_out} °C.\")\n", + " return super(MinimumSupplyTemperatureController, self).control_step(net)\n", + "\n", + " def is_converged(self, net):\n", + " \"\"\"Check if the controller has converged.\n", + "\n", + " Args:\n", + " net (pandapipesNet): The pandapipes network.\n", + "\n", + " Returns:\n", + " bool: True if converged, False otherwise.\n", + " \"\"\"\n", + " # not converging under that value\n", + " if all(net.heat_consumer[\"qext_w\"] == 0):\n", + " return True\n", + " \n", + " # Check whether the temperatures have changed within the specified tolerance\n", + " current_T_out = net.res_heat_consumer[\"t_to_k\"].at[self.heat_consumer_idx] - 273.15\n", + " current_T_in = net.res_heat_consumer[\"t_from_k\"].at[self.heat_consumer_idx] - 273.15\n", + " previous_T_in = self.previous_temperatures[-1] if self.previous_temperatures else None\n", + "\n", + " # Testing for convergence\n", + " temperature_change = abs(current_T_in - previous_T_in) if previous_T_in is not None else float('inf')\n", + " converged_T_in = temperature_change < self.tolerance\n", + "\n", + " # Update the list of previous temperatures\n", + " self.previous_temperatures.append(current_T_in)\n", + " if len(self.previous_temperatures) > 2: # Keep the last two temperatures\n", + " self.previous_temperatures.pop(0)\n", + "\n", + " current_mass_flow = net.res_heat_consumer[\"mdot_from_kg_per_s\"].at[self.heat_consumer_idx]\n", + " \n", + " # Convergence based on the minimum supply temperature\n", + " if current_T_in < self.min_supply_temperature:\n", + " if self.debug:\n", + " print(f\"Supply temperature not met for heat_consumer_idx: {self.heat_consumer_idx}. current_temperature_in: {current_T_in}), current_temperature_out: {current_T_out}), current_mass_flow: {current_mass_flow}\")\n", + " return False\n", + " \n", + " if converged_T_in:\n", + " if self.debug:\n", + " print(f'Regler konvergiert: heat_consumer_idx: {self.heat_consumer_idx}, current_temperature_in: {current_T_in}), current_temperature_out: {current_T_out}), current_mass_flow: {current_mass_flow}')\n", + " return True\n", + "\n", + " # Check if the maximum number of iterations has been reached\n", + " if self.iteration >= self.max_iterations:\n", + " if self.debug:\n", + " print(f\"Max iterations reached for heat_consumer_idx: {self.heat_consumer_idx}\")\n", + " return True\n", + "\n", + " return False" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.8.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From a595d70e6ea77c2fb32d37bfd9170b173d7f81bb Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Thu, 28 Aug 2025 10:19:43 +0200 Subject: [PATCH 03/13] The previous commit was an accident in this branch. --- .../MinimumSupplyTemperatureController.ipynb | 207 ------------------ 1 file changed, 207 deletions(-) delete mode 100644 tutorials/MinimumSupplyTemperatureController.ipynb diff --git a/tutorials/MinimumSupplyTemperatureController.ipynb b/tutorials/MinimumSupplyTemperatureController.ipynb deleted file mode 100644 index 2e39d00fd..000000000 --- a/tutorials/MinimumSupplyTemperatureController.ipynb +++ /dev/null @@ -1,207 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Building a minimum supply temperature controller for heat consumers in a district heating network" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## MinimumSupplyTemperatureController\n", - "\n", - "The `MinimumSupplyTemperatureController` is designed to ensure that the supply temperature at each heat consumer in a district heating network does not fall below a specified minimum value. This is particularly important during periods of low heat demand, when water circulation slows and the supply temperature can drop—sometimes even below the return temperature—leading to operational issues and inefficient heating.\n", - "\n", - "This controller monitors the supply temperature at the heat consumer and, if it detects that the temperature is below the required minimum, it incrementally increases the target return temperature. This adjustment forces the network to increase the supply temperature, thereby maintaining the desired minimum level. The controller uses a PID-like approach with configurable proportional, integral, and derivative gains, as well as a tolerance for convergence and a maximum number of iterations per time step.\n", - "\n", - "Typical use cases include:\n", - "- Preventing supply temperatures from dropping below return temperatures during low load or standby conditions.\n", - "- Maintaining reliable and efficient operation of district heating systems, especially in networks with variable or low demand.\n", - "\n", - "The controller is flexible and can be integrated into time-series simulations, supporting dynamic adjustment of the minimum supply temperature via an external data source if needed." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from pandapower.control.basic_controller import BasicCtrl\n", - "\n", - "\n", - "class MinimumSupplyTemperatureController(BasicCtrl):\n", - " \"\"\"\n", - " A controller for maintaining the min supply temperature of the heat consumers in the network.\n", - " \n", - " Args:\n", - " net (pandapipesNet): The pandapipes network.\n", - " heat_consumer_idx (int): Index of the heat consumer.\n", - " min_supply_temperature (float, optional): Minimum supply temperature. Defaults to 65.\n", - " tolerance (float, optional): Tolerance for temperature difference. Defaults to 2.\n", - " max_iterations (int, optional): Maximum number of iterations. Defaults to 100.\n", - " temperature_adjustment_step (float, optional): Step to adjust the target return temperature. Defaults to 1.\n", - " debug (bool, optional): Flag to enable debug output. Defaults to False.\n", - " **kwargs: Additional keyword arguments.\n", - " \"\"\"\n", - " def __init__(self, net, heat_consumer_idx, min_supply_temperature=65, tolerance=2, max_iterations=100, temperature_adjustment_step=1, debug=False, **kwargs):\n", - " super(MinimumSupplyTemperatureController, self).__init__(net, **kwargs)\n", - " self.heat_consumer_idx = heat_consumer_idx\n", - " self.min_supply_temperature = min_supply_temperature\n", - " self.tolerance = tolerance\n", - " self.max_iterations = max_iterations\n", - " self.temperature_adjustment_step = temperature_adjustment_step # Step to adjust the target return temperature\n", - " self.debug = debug\n", - "\n", - " self.data_source = None\n", - " self.iteration = 0 # Add iteration counter\n", - " self.previous_temperatures = [] # Use a list to store previous temperatures\n", - "\n", - " def time_step(self, net, time_step):\n", - " \"\"\"Reset the controller parameters at the start of each time step.\n", - "\n", - " Args:\n", - " net (pandapipesNet): The pandapipes network.\n", - " time_step (int): The current time step.\n", - "\n", - " Returns:\n", - " int: The current time step.\n", - " \"\"\"\n", - " self.iteration = 0 # reset iteration counter\n", - " self.previous_temperatures = [] # Reset to an empty list\n", - "\n", - " if time_step == 0:\n", - " # Store the standard return temperature for the heat consumer\n", - " self.standard_return_temperature = net.heat_consumer[\"treturn_k\"].at[self.heat_consumer_idx]\n", - "\n", - " else:\n", - " # Restore the standard return temperature for the heat consumer\n", - " net.heat_consumer[\"treturn_k\"].at[self.heat_consumer_idx] = self.standard_return_temperature\n", - "\n", - " # Check if a data source exists and get the target temperature for the current time step\n", - " if self.data_source is not None:\n", - " self.min_supply_temperature = self.data_source.df.at[time_step, f'min_supply_temperature']\n", - " \n", - " return time_step\n", - "\n", - " def get_weighted_average_temperature(self):\n", - " \"\"\"Calculate the weighted average of the previous temperatures.\n", - "\n", - " Returns:\n", - " float: The weighted average temperature.\n", - " \"\"\"\n", - " if len(self.previous_temperatures) == 0:\n", - " return None\n", - " weights = np.arange(1, len(self.previous_temperatures) + 1)\n", - " weighted_avg = np.dot(self.previous_temperatures, weights) / weights.sum()\n", - " return weighted_avg\n", - "\n", - " def control_step(self, net):\n", - " \"\"\"Adjust the mass flow to maintain the target return temperature.\n", - "\n", - " Args:\n", - " net (pandapipesNet): The pandapipes network.\n", - " \"\"\"\n", - " # Increment iteration counter\n", - " self.iteration += 1\n", - " \n", - " if all(net.heat_consumer[\"qext_w\"] == 0):\n", - " # Switch to standby mode\n", - " print(\"No heat flow detected. Switching to standby mode.\")\n", - " return super(MinimumSupplyTemperatureController, self).control_step(net)\n", - "\n", - " # Calculate new mass flow\n", - " current_T_out = net.res_heat_consumer[\"t_to_k\"].at[self.heat_consumer_idx] - 273.15\n", - " current_T_in = net.res_heat_consumer[\"t_from_k\"].at[self.heat_consumer_idx] - 273.15\n", - "\n", - " weighted_avg_T_in = self.get_weighted_average_temperature()\n", - " if weighted_avg_T_in is not None:\n", - " current_T_in = weighted_avg_T_in\n", - "\n", - " current_mass_flow = net.res_heat_consumer[\"mdot_from_kg_per_s\"].at[self.heat_consumer_idx]\n", - "\n", - " # Ensure the supply temperature does not fall below the minimum supply temperature\n", - " if current_T_in < self.min_supply_temperature:\n", - " new_T_out = net.heat_consumer[\"treturn_k\"].at[self.heat_consumer_idx] + self.temperature_adjustment_step\n", - " net.heat_consumer[\"treturn_k\"].at[self.heat_consumer_idx] = new_T_out\n", - " \n", - " if self.debug:\n", - " print(f\"Minimum supply temperature not met. Adjusted target output temperature to {new_T_out} °C.\")\n", - " return super(MinimumSupplyTemperatureController, self).control_step(net)\n", - "\n", - " def is_converged(self, net):\n", - " \"\"\"Check if the controller has converged.\n", - "\n", - " Args:\n", - " net (pandapipesNet): The pandapipes network.\n", - "\n", - " Returns:\n", - " bool: True if converged, False otherwise.\n", - " \"\"\"\n", - " # not converging under that value\n", - " if all(net.heat_consumer[\"qext_w\"] == 0):\n", - " return True\n", - " \n", - " # Check whether the temperatures have changed within the specified tolerance\n", - " current_T_out = net.res_heat_consumer[\"t_to_k\"].at[self.heat_consumer_idx] - 273.15\n", - " current_T_in = net.res_heat_consumer[\"t_from_k\"].at[self.heat_consumer_idx] - 273.15\n", - " previous_T_in = self.previous_temperatures[-1] if self.previous_temperatures else None\n", - "\n", - " # Testing for convergence\n", - " temperature_change = abs(current_T_in - previous_T_in) if previous_T_in is not None else float('inf')\n", - " converged_T_in = temperature_change < self.tolerance\n", - "\n", - " # Update the list of previous temperatures\n", - " self.previous_temperatures.append(current_T_in)\n", - " if len(self.previous_temperatures) > 2: # Keep the last two temperatures\n", - " self.previous_temperatures.pop(0)\n", - "\n", - " current_mass_flow = net.res_heat_consumer[\"mdot_from_kg_per_s\"].at[self.heat_consumer_idx]\n", - " \n", - " # Convergence based on the minimum supply temperature\n", - " if current_T_in < self.min_supply_temperature:\n", - " if self.debug:\n", - " print(f\"Supply temperature not met for heat_consumer_idx: {self.heat_consumer_idx}. current_temperature_in: {current_T_in}), current_temperature_out: {current_T_out}), current_mass_flow: {current_mass_flow}\")\n", - " return False\n", - " \n", - " if converged_T_in:\n", - " if self.debug:\n", - " print(f'Regler konvergiert: heat_consumer_idx: {self.heat_consumer_idx}, current_temperature_in: {current_T_in}), current_temperature_out: {current_T_out}), current_mass_flow: {current_mass_flow}')\n", - " return True\n", - "\n", - " # Check if the maximum number of iterations has been reached\n", - " if self.iteration >= self.max_iterations:\n", - " if self.debug:\n", - " print(f\"Max iterations reached for heat_consumer_idx: {self.heat_consumer_idx}\")\n", - " return True\n", - "\n", - " return False" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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.8.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 5e6281ac89e88040d02fd63492b39db02f4cd149 Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Tue, 2 Sep 2025 15:24:48 +0200 Subject: [PATCH 04/13] Added BadPointPressureController reference in the documentation --- doc/source/controller/controller_classes.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/source/controller/controller_classes.rst b/doc/source/controller/controller_classes.rst index ed9af08c0..9ac1aae15 100644 --- a/doc/source/controller/controller_classes.rst +++ b/doc/source/controller/controller_classes.rst @@ -23,4 +23,13 @@ This is used to read the data from a DataSource and write it to a network. .. _ConstControl: .. autoclass:: pandapower.control.controller.const_control.ConstControl - :members: \ No newline at end of file + :members: + +Custom Controller Example: BadPointPressureController +==================================================== + +A practical example of a custom controller implementation is provided in the Jupyter Notebook +:ref:`BadPointPressureController `. +This notebook demonstrates how to create a controller that maintains a minimum pressure at the worst point in a thermal network, which is a common requirement in district heating systems. + +The BadPointPressureController serves as both a template for user-defined controllers and as an important tool for operating thermal grids with pandapipes. \ No newline at end of file From 7a7813fa471c79b8b2ebe3eb082e08165d50338e Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Tue, 4 Nov 2025 10:17:00 +0100 Subject: [PATCH 05/13] feat: Add BadPointPressureLiftController as standalone controller module Move BadPointPressureLiftController from Jupyter notebook to pandapipes.control package: - Add new controller module at src/pandapipes/control/controller/bad_point_pressure_lift_controller.py - Export controller from pandapipes.control for easy import - Update BadPointPressureController.ipynb to import controller from pandapipes instead of defining it inline - Fix documentation formatting and links in controller_classes.rst - Add controller subdirectory with proper __init__.py Documentation improvements: - Fix title underline length in controller_classes.rst to resolve Sphinx warning - Correct external link syntax for BadPointPressureController notebook - Update controller name to BadPointPressureLiftController (actual class name) The BadPointPressureLiftController maintains minimum pressure difference at the worst point (Schlechtpunkt) in district heating networks by automatically detecting the heat exchanger with the lowest pressure difference and adjusting circulation pump pressures accordingly. Features: - Automatic worst point detection - Proportional pressure control - Standby mode when no heat flow detected - Convergence checking with configurable tolerance --- doc/source/controller/controller_classes.rst | 10 +- src/pandapipes/control/__init__.py | 1 + src/pandapipes/control/controller/__init__.py | 1 + .../bad_point_pressure_lift_controller.py | 144 ++++++++++++++++++ ...b => BadPointPressureLiftController.ipynb} | 141 +---------------- 5 files changed, 159 insertions(+), 138 deletions(-) create mode 100644 src/pandapipes/control/controller/__init__.py create mode 100644 src/pandapipes/control/controller/bad_point_pressure_lift_controller.py rename tutorials/{BadPointPressureController.ipynb => BadPointPressureLiftController.ipynb} (54%) diff --git a/doc/source/controller/controller_classes.rst b/doc/source/controller/controller_classes.rst index 9ac1aae15..3e48b846e 100644 --- a/doc/source/controller/controller_classes.rst +++ b/doc/source/controller/controller_classes.rst @@ -25,11 +25,11 @@ This is used to read the data from a DataSource and write it to a network. .. autoclass:: pandapower.control.controller.const_control.ConstControl :members: -Custom Controller Example: BadPointPressureController -==================================================== +Custom Controller Example: BadPointPressureLiftController +========================================================== A practical example of a custom controller implementation is provided in the Jupyter Notebook -:ref:`BadPointPressureController `. -This notebook demonstrates how to create a controller that maintains a minimum pressure at the worst point in a thermal network, which is a common requirement in district heating systems. +`BadPointPressureLiftController.ipynb `_. +This notebook demonstrates how to create a controller that maintains a minimum pressure difference at the worst point in a district heating network, which is a common requirement in district heating systems. -The BadPointPressureController serves as both a template for user-defined controllers and as an important tool for operating thermal grids with pandapipes. \ No newline at end of file +The BadPointPressureLiftController is now available as a standalone controller class in :code:`pandapipes.control` and serves as both a template for user-defined controllers and as an important tool for operating thermal grids with pandapipes. \ No newline at end of file diff --git a/src/pandapipes/control/__init__.py b/src/pandapipes/control/__init__.py index 3265e1159..930535718 100644 --- a/src/pandapipes/control/__init__.py +++ b/src/pandapipes/control/__init__.py @@ -3,3 +3,4 @@ # Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. from pandapipes.control.run_control import run_control +from pandapipes.control.controller import BadPointPressureLiftController \ No newline at end of file diff --git a/src/pandapipes/control/controller/__init__.py b/src/pandapipes/control/controller/__init__.py new file mode 100644 index 000000000..20588d51a --- /dev/null +++ b/src/pandapipes/control/controller/__init__.py @@ -0,0 +1 @@ +from pandapipes.control.controller.bad_point_pressure_lift_controller import BadPointPressureLiftController \ No newline at end of file diff --git a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py new file mode 100644 index 000000000..325455fde --- /dev/null +++ b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py @@ -0,0 +1,144 @@ +from pandapower.control.basic_controller import BasicCtrl + +class BadPointPressureLiftController(BasicCtrl): + """ + A controller for maintaining the pressure difference at the worst point (German: Differenzdruckregelung im Schlechtpunkt) in the network. + + The BadPointPressureLiftController is a custom controller designed for district heating networks + modeled with pandapipes. Its main purpose is to maintain a minimum pressure difference at the + network's "worst point"—the heat exchanger with the lowest pressure difference (Schlechtpunktregelung). + + Key Features: + - **Automatic Worst Point Detection:** Identifies the heat exchanger with the lowest pressure difference where heat flow is present. + - **Pressure Regulation:** Adjusts the circulation pump's lift and flow pressures to ensure the pressure difference at the worst point meets a specified minimum target. + - **Proportional Control:** Uses a proportional gain to determine the adjustment magnitude based on the deviation from the target pressure difference. + - **Standby Mode:** If no heat flow is detected, the controller switches the pump to a standby mode with minimum lift and flow pressures. + - **Convergence Check:** Determines if the pressure difference is within a specified tolerance of the target, signaling convergence. + + Args: + net (pandapipesNet): The pandapipes network. + circ_pump_pressure_idx (int, optional): Index of the circulation pump. Defaults to 0. + target_dp_min_bar (float, optional): Target minimum pressure difference in bar. Defaults to 1. + tolerance (float, optional): Tolerance for pressure difference. Defaults to 0.2. + proportional_gain (float, optional): Proportional gain for the controller. Defaults to 0.2. + min_plift (float, optional): Minimum lift pressure in bar. Defaults to 1.5. + min_pflow (float, optional): Minimum flow pressure in bar. Defaults to 3.5. + **kwargs: Additional keyword arguments. + """ + def __init__(self, net, circ_pump_pressure_idx=0, target_dp_min_bar=1, tolerance=0.2, + proportional_gain=0.2, min_plift=1.5, min_pflow=3.5, **kwargs): + super(BadPointPressureLiftController, self).__init__(net, **kwargs) + self.circ_pump_pressure_idx = circ_pump_pressure_idx + self.target_dp_min_bar = target_dp_min_bar + self.tolerance = tolerance + self.proportional_gain = proportional_gain + + self.min_plift = min_plift # Minimum pressure in bar + self.min_pflow = min_pflow # Minimum lift pressure in bar + + self.iteration = 0 # Add iteration counter + + self.dp_min, self.heat_consumer_idx = self.calculate_worst_point(net) + + def calculate_worst_point(self, net): + """ + Calculate the worst point in the heating network, defined as the heat exchanger with the lowest pressure difference. + + Args: + net (pandapipesNet): The pandapipes network. + + Returns: + tuple: The minimum pressure difference and the index of the worst point. + """ + + dp = [] + + for idx, qext, p_from, p_to in zip(net.heat_consumer.index, net.heat_consumer["qext_w"], + net.res_heat_consumer["p_from_bar"], net.res_heat_consumer["p_to_bar"]): + if qext != 0: + dp_diff = p_from - p_to + dp.append((dp_diff, idx)) + + if not dp: + return 0, -1 + + # Find the minimum delta p where the heat flow is not zero + dp_min, idx_min = min(dp, key=lambda x: x[0]) + + return dp_min, idx_min + + def time_step(self, net, time_step): + """ + Reset the iteration counter at the start of each time step. + + Args: + net (pandapipesNet): The pandapipes network. + time_step (int): The current time step. + + Returns: + int: The current time step. + """ + self.iteration = 0 # reset iteration counter + self.dp_min, self.heat_consumer_idx = self.calculate_worst_point(net) + + return time_step + + def is_converged(self, net): + """ + Check if the controller has converged. + + Args: + net (pandapipesNet): The pandapipes network. + + Returns: + bool: True if converged, False otherwise. + """ + + if all(net.heat_consumer["qext_w"] == 0): + return True + + current_dp_bar = net.res_heat_consumer["p_from_bar"].at[self.heat_consumer_idx] - \ + net.res_heat_consumer["p_to_bar"].at[self.heat_consumer_idx] + + # Check if the pressure difference is within tolerance + dp_within_tolerance = abs(current_dp_bar - self.target_dp_min_bar) < self.tolerance + + if dp_within_tolerance: + return dp_within_tolerance + + def control_step(self, net): + """ + Adjust the pump pressure to maintain the target pressure difference. + + Args: + net (pandapipesNet): The pandapipes network. + """ + # Increment iteration counter + self.iteration += 1 + + # Adjust the pump pressure or switch to standby mode when heat flow is zero + if all(net.heat_consumer["qext_w"] == 0): + # Switch to standby mode + print("No heat flow detected. Switching to standby mode.") + net.circ_pump_pressure["plift_bar"].iloc[:] = self.min_plift # Minimum lift pressure + net.circ_pump_pressure["p_flow_bar"].iloc[:] = self.min_pflow # Minimum flow pressure + return super(BadPointPressureLiftController, self).control_step(net) + + # Check whether the heat flow in the heat exchanger is zero + current_dp_bar = net.res_heat_consumer["p_from_bar"].at[self.heat_consumer_idx] - \ + net.res_heat_consumer["p_to_bar"].at[self.heat_consumer_idx] + current_plift_bar = net.circ_pump_pressure["plift_bar"].at[self.circ_pump_pressure_idx] + current_pflow_bar = net.circ_pump_pressure["p_flow_bar"].at[self.circ_pump_pressure_idx] + + dp_error = self.target_dp_min_bar - current_dp_bar + + plift_adjustment = dp_error * self.proportional_gain + pflow_adjustment = dp_error * self.proportional_gain + + new_plift = current_plift_bar + plift_adjustment + new_pflow = current_pflow_bar + pflow_adjustment + + net.circ_pump_pressure["plift_bar"].at[self.circ_pump_pressure_idx] = new_plift + net.circ_pump_pressure["p_flow_bar"].at[self.circ_pump_pressure_idx] = new_pflow + + return super(BadPointPressureLiftController, self).control_step(net) diff --git a/tutorials/BadPointPressureController.ipynb b/tutorials/BadPointPressureLiftController.ipynb similarity index 54% rename from tutorials/BadPointPressureController.ipynb rename to tutorials/BadPointPressureLiftController.ipynb index bfe9a1b76..2052522ef 100644 --- a/tutorials/BadPointPressureController.ipynb +++ b/tutorials/BadPointPressureLiftController.ipynb @@ -37,132 +37,7 @@ "metadata": {}, "outputs": [], "source": [ - "from pandapower.control.basic_controller import BasicCtrl\n", - "\n", - "\n", - "class BadPointPressureLiftController(BasicCtrl):\n", - " \"\"\"\n", - " A controller for maintaining the pressure difference at the worst point (German: Differenzdruckregelung im Schlechtpunkt) in the network.\n", - " \n", - " Args:\n", - " net (pandapipesNet): The pandapipes network.\n", - " circ_pump_pressure_idx (int, optional): Index of the circulation pump. Defaults to 0.\n", - " target_dp_min_bar (float, optional): Target minimum pressure difference in bar. Defaults to 1.\n", - " tolerance (float, optional): Tolerance for pressure difference. Defaults to 0.2.\n", - " proportional_gain (float, optional): Proportional gain for the controller. Defaults to 0.2.\n", - " min_plift (float, optional): Minimum lift pressure in bar. Defaults to 1.5.\n", - " min_pflow (float, optional): Minimum flow pressure in bar. Defaults to 3.5.\n", - " **kwargs: Additional keyword arguments.\n", - " \"\"\"\n", - " def __init__(self, net, circ_pump_pressure_idx=0, target_dp_min_bar=1, tolerance=0.2, proportional_gain=0.2, min_plift=1.5, min_pflow=3.5, **kwargs):\n", - " super(BadPointPressureLiftController, self).__init__(net, **kwargs)\n", - " self.circ_pump_pressure_idx = circ_pump_pressure_idx\n", - " self.target_dp_min_bar = target_dp_min_bar\n", - " self.tolerance = tolerance\n", - " self.proportional_gain = proportional_gain\n", - "\n", - " self.min_plift = min_plift # Minimum pressure in bar\n", - " self.min_pflow = min_pflow # Minimum lift pressure in bar\n", - "\n", - " self.iteration = 0 # Add iteration counter\n", - "\n", - " self.dp_min, self.heat_consumer_idx = self.calculate_worst_point(net)\n", - "\n", - " def calculate_worst_point(self, net):\n", - " \"\"\"Calculate the worst point in the heating network, defined as the heat exchanger with the lowest pressure difference.\n", - "\n", - " Args:\n", - " net (pandapipesNet): The pandapipes network.\n", - "\n", - " Returns:\n", - " tuple: The minimum pressure difference and the index of the worst point.\n", - " \"\"\"\n", - " \n", - " dp = []\n", - "\n", - " for idx, qext, p_from, p_to in zip(net.heat_consumer.index, net.heat_consumer[\"qext_w\"], net.res_heat_consumer[\"p_from_bar\"], net.res_heat_consumer[\"p_to_bar\"]):\n", - " if qext != 0:\n", - " dp_diff = p_from - p_to\n", - " dp.append((dp_diff, idx))\n", - "\n", - " if not dp:\n", - " return 0, -1\n", - "\n", - " # Find the minimum delta p where the heat flow is not zero\n", - " dp_min, idx_min = min(dp, key=lambda x: x[0])\n", - "\n", - " return dp_min, idx_min\n", - "\n", - " def time_step(self, net, time_step):\n", - " \"\"\"Reset the iteration counter at the start of each time step.\n", - "\n", - " Args:\n", - " net (pandapipesNet): The pandapipes network.\n", - " time_step (int): The current time step.\n", - "\n", - " Returns:\n", - " int: The current time step.\n", - " \"\"\"\n", - " self.iteration = 0 # reset iteration counter\n", - " self.dp_min, self.heat_consumer_idx = self.calculate_worst_point(net)\n", - "\n", - " return time_step\n", - "\n", - " def is_converged(self, net):\n", - " \"\"\"Check if the controller has converged.\n", - "\n", - " Args:\n", - " net (pandapipesNet): The pandapipes network.\n", - "\n", - " Returns:\n", - " bool: True if converged, False otherwise.\n", - " \"\"\"\n", - "\n", - " if all(net.heat_consumer[\"qext_w\"] == 0):\n", - " return True\n", - " \n", - " current_dp_bar = net.res_heat_consumer[\"p_from_bar\"].at[self.heat_consumer_idx] - net.res_heat_consumer[\"p_to_bar\"].at[self.heat_consumer_idx]\n", - "\n", - " # Check if the pressure difference is within tolerance\n", - " dp_within_tolerance = abs(current_dp_bar - self.target_dp_min_bar) < self.tolerance\n", - "\n", - " if dp_within_tolerance == True:\n", - " return dp_within_tolerance\n", - "\n", - " def control_step(self, net):\n", - " \"\"\"Adjust the pump pressure to maintain the target pressure difference.\n", - "\n", - " Args:\n", - " net (pandapipesNet): The pandapipes network.\n", - " \"\"\"\n", - " # Increment iteration counter\n", - " self.iteration += 1\n", - "\n", - " \"\"\"Adjust the pump pressure or switch to standby mode when heat flow is zero.\"\"\"\n", - " if all(net.heat_consumer[\"qext_w\"] == 0):\n", - " # Switch to standby mode\n", - " print(\"No heat flow detected. Switching to standby mode.\")\n", - " net.circ_pump_pressure[\"plift_bar\"].iloc[:] = self.min_plift # Minimum lift pressure\n", - " net.circ_pump_pressure[\"p_flow_bar\"].iloc[:] = self.min_pflow # Minimum flow pressure\n", - " return super(BadPointPressureLiftController, self).control_step(net)\n", - "\n", - " # Check whether the heat flow in the heat exchanger is zero\n", - " current_dp_bar = net.res_heat_consumer[\"p_from_bar\"].at[self.heat_consumer_idx] - net.res_heat_consumer[\"p_to_bar\"].at[self.heat_consumer_idx]\n", - " current_plift_bar = net.circ_pump_pressure[\"plift_bar\"].at[self.circ_pump_pressure_idx]\n", - " current_pflow_bar = net.circ_pump_pressure[\"p_flow_bar\"].at[self.circ_pump_pressure_idx]\n", - "\n", - " dp_error = self.target_dp_min_bar - current_dp_bar\n", - " \n", - " plift_adjustment = dp_error * self.proportional_gain\n", - " pflow_adjustment = dp_error * self.proportional_gain \n", - "\n", - " new_plift = current_plift_bar + plift_adjustment\n", - " new_pflow = current_pflow_bar + pflow_adjustment\n", - " \n", - " net.circ_pump_pressure[\"plift_bar\"].at[self.circ_pump_pressure_idx] = new_plift\n", - " net.circ_pump_pressure[\"p_flow_bar\"].at[self.circ_pump_pressure_idx] = new_pflow\n", - "\n", - " return super(BadPointPressureLiftController, self).control_step(net)\n" + "from pandapipes.control import BadPointPressureLiftController" ] }, { @@ -231,7 +106,7 @@ " # Junctions for heat exchangers\n", " j5 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 5\", geodata=(85, 0))\n", " j6 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 6\", geodata=(85, 10))\n", - " \n", + "\n", " # Junctions for connection pipes return line\n", " j7 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 7\", geodata=(60, 10))\n", " j8 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 8\", geodata=(10, 10))\n", @@ -241,14 +116,14 @@ "\n", " pipe1 = pp.create_pipe(net, j2, j3, std_type=pipetype, length_km=0.01, k_mm=k, name=\"pipe1\", sections=5, text_k=283)\n", " pipe2 = pp.create_pipe(net, j3, j4, std_type=pipetype, length_km=0.05, k_mm=k, name=\"pipe2\", sections=5, text_k=283)\n", - " pipe3 = pp.create_pipe(net, j4, j5, std_type=pipetype, length_km=0.025,k_mm=k, name=\"pipe3\", sections=5, text_k=283)\n", + " pipe3 = pp.create_pipe(net, j4, j5, std_type=pipetype, length_km=0.025, k_mm=k, name=\"pipe3\", sections=5, text_k=283)\n", "\n", - " heat_cosnumer1 = pp.create_heat_consumer(net, from_junction=j5, to_junction=j6, loss_coefficient=0, qext_w=qext_w[0], \n", - " treturn_k=return_temperature_k[0], name=\"heat_consumer_1\") # treturn_k=t when implemented in function\n", + " heat_consumer1 = pp.create_heat_consumer(net, from_junction=j5, to_junction=j6, loss_coefficient=0, qext_w=qext_w[0], \n", + " treturn_k=return_temperature_k[0], name=\"heat_consumer_1\")\n", " \n", "\n", - " heat_cosnumer2 = pp.create_heat_consumer(net, from_junction=j4, to_junction=j7, loss_coefficient=0, qext_w=qext_w[1], \n", - " treturn_k=return_temperature_k[1], name=\"heat_consumer_2\") # treturn_k=t when implemented in function\n", + " heat_consumer2 = pp.create_heat_consumer(net, from_junction=j4, to_junction=j7, loss_coefficient=0, qext_w=qext_w[1], \n", + " treturn_k=return_temperature_k[1], name=\"heat_consumer_2\")\n", " \n", " pipe4 = pp.create_pipe(net, j6, j7, std_type=pipetype, length_km=0.25, k_mm=k, name=\"pipe4\", sections=5, text_k=283)\n", " pipe5 = pp.create_pipe(net, j7, j8, std_type=pipetype, length_km=0.05, k_mm=k, name=\"pipe5\", sections=5, text_k=283)\n", @@ -292,7 +167,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.11.9" } }, "nbformat": 4, From b7361ccd5b7d84aad59ea62d472e7cb13fd9c737 Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Tue, 4 Nov 2025 10:56:13 +0100 Subject: [PATCH 06/13] style: Remove trailing whitespace in BadPointPressureLiftController Fix Codacy code style issues by removing trailing whitespace: - Line 28: Remove trailing space from __init__ method signature - Line 53: Remove trailing whitespace from empty line after docstring - Line 136: Remove trailing spaces from pflow_adjustment assignment - Line 139: Remove trailing whitespace from empty line No functional changes, code style cleanup only. --- .../controller/bad_point_pressure_lift_controller.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py index 325455fde..272f01f01 100644 --- a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py +++ b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py @@ -25,7 +25,7 @@ class BadPointPressureLiftController(BasicCtrl): min_pflow (float, optional): Minimum flow pressure in bar. Defaults to 3.5. **kwargs: Additional keyword arguments. """ - def __init__(self, net, circ_pump_pressure_idx=0, target_dp_min_bar=1, tolerance=0.2, + def __init__(self, net, circ_pump_pressure_idx=0, target_dp_min_bar=1, tolerance=0.2, proportional_gain=0.2, min_plift=1.5, min_pflow=3.5, **kwargs): super(BadPointPressureLiftController, self).__init__(net, **kwargs) self.circ_pump_pressure_idx = circ_pump_pressure_idx @@ -50,7 +50,7 @@ def calculate_worst_point(self, net): Returns: tuple: The minimum pressure difference and the index of the worst point. """ - + dp = [] for idx, qext, p_from, p_to in zip(net.heat_consumer.index, net.heat_consumer["qext_w"], @@ -131,14 +131,14 @@ def control_step(self, net): current_pflow_bar = net.circ_pump_pressure["p_flow_bar"].at[self.circ_pump_pressure_idx] dp_error = self.target_dp_min_bar - current_dp_bar - + plift_adjustment = dp_error * self.proportional_gain - pflow_adjustment = dp_error * self.proportional_gain + pflow_adjustment = dp_error * self.proportional_gain new_plift = current_plift_bar + plift_adjustment new_pflow = current_pflow_bar + pflow_adjustment - + net.circ_pump_pressure["plift_bar"].at[self.circ_pump_pressure_idx] = new_plift net.circ_pump_pressure["p_flow_bar"].at[self.circ_pump_pressure_idx] = new_pflow - return super(BadPointPressureLiftController, self).control_step(net) + return super(BadPointPressureLiftController, self).control_step(net) \ No newline at end of file From a1ada19d9583050862de5680f1767477dba5e176 Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Tue, 4 Nov 2025 12:04:39 +0100 Subject: [PATCH 07/13] fix: Remove unnecessary whitespace in BadPointPressureLiftController --- .../control/controller/bad_point_pressure_lift_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py index 272f01f01..f3794881d 100644 --- a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py +++ b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py @@ -53,7 +53,7 @@ def calculate_worst_point(self, net): dp = [] - for idx, qext, p_from, p_to in zip(net.heat_consumer.index, net.heat_consumer["qext_w"], + for idx, qext, p_from, p_to in zip(net.heat_consumer.index, net.heat_consumer["qext_w"], net.res_heat_consumer["p_from_bar"], net.res_heat_consumer["p_to_bar"]): if qext != 0: dp_diff = p_from - p_to @@ -96,7 +96,7 @@ def is_converged(self, net): if all(net.heat_consumer["qext_w"] == 0): return True - + current_dp_bar = net.res_heat_consumer["p_from_bar"].at[self.heat_consumer_idx] - \ net.res_heat_consumer["p_to_bar"].at[self.heat_consumer_idx] From 6b750f55949f32a4707212db57fb72b89b082092 Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Fri, 7 Nov 2025 13:29:07 +0100 Subject: [PATCH 08/13] test: Add test suite for BadPointPressureLiftController Add test file with 2 focused test cases covering core controller functionality: Tests added: - Controller maintains target pressure difference at worst point in district heating network - Standby mode activation when no heat demand is present Additional improvements: - Fix pandas FutureWarning in controller code by replacing chained assignment with proper .loc syntax (net.circ_pump_pressure.loc[:, "column"]) All tests pass successfully with no warnings. Test location: src/pandapipes/test/api/test_components/test_bad_point_pressure_lift_controller.py --- .../bad_point_pressure_lift_controller.py | 4 +- ...test_bad_point_pressure_lift_controller.py | 81 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/pandapipes/test/api/test_components/test_bad_point_pressure_lift_controller.py diff --git a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py index f3794881d..991a9be83 100644 --- a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py +++ b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py @@ -120,8 +120,8 @@ def control_step(self, net): if all(net.heat_consumer["qext_w"] == 0): # Switch to standby mode print("No heat flow detected. Switching to standby mode.") - net.circ_pump_pressure["plift_bar"].iloc[:] = self.min_plift # Minimum lift pressure - net.circ_pump_pressure["p_flow_bar"].iloc[:] = self.min_pflow # Minimum flow pressure + net.circ_pump_pressure.loc[:, "plift_bar"] = self.min_plift # Minimum lift pressure + net.circ_pump_pressure.loc[:, "p_flow_bar"] = self.min_pflow # Minimum flow pressure return super(BadPointPressureLiftController, self).control_step(net) # Check whether the heat flow in the heat exchanger is zero diff --git a/src/pandapipes/test/api/test_components/test_bad_point_pressure_lift_controller.py b/src/pandapipes/test/api/test_components/test_bad_point_pressure_lift_controller.py new file mode 100644 index 000000000..6c285519b --- /dev/null +++ b/src/pandapipes/test/api/test_components/test_bad_point_pressure_lift_controller.py @@ -0,0 +1,81 @@ +import numpy as np +import pytest +import pandapipes as pp +from pandapipes.control import BadPointPressureLiftController + +@pytest.fixture +def district_heating_net(): + """Create a simple district heating network with pump and heat consumers.""" + net = pp.create_empty_network(fluid="water") + + supply_temperature_k = 85 + 273.15 + pipetype = "110/202 PLUS" + + # Create ring network + j1 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, geodata=(0, 10)) + j2 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, geodata=(0, 0)) + j3 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, geodata=(10, 0)) + j4 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, geodata=(60, 0)) + j5 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, geodata=(85, 0)) + j6 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, geodata=(85, 10)) + j7 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, geodata=(60, 10)) + j8 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, geodata=(10, 10)) + + pp.create_circ_pump_const_pressure(net, j1, j2, p_flow_bar=4, plift_bar=1.5, + t_flow_k=supply_temperature_k, type="auto") + + pp.create_pipe(net, j2, j3, std_type=pipetype, length_km=0.01, k_mm=0.1, sections=5, text_k=283) + pp.create_pipe(net, j3, j4, std_type=pipetype, length_km=0.05, k_mm=0.1, sections=5, text_k=283) + pp.create_pipe(net, j4, j5, std_type=pipetype, length_km=0.025, k_mm=0.1, sections=5, text_k=283) + pp.create_pipe(net, j6, j7, std_type=pipetype, length_km=0.25, k_mm=0.1, sections=5, text_k=283) + pp.create_pipe(net, j7, j8, std_type=pipetype, length_km=0.05, k_mm=0.1, sections=5, text_k=283) + pp.create_pipe(net, j8, j1, std_type=pipetype, length_km=0.01, k_mm=0.1, sections=5, text_k=283) + + pp.create_heat_consumer(net, j5, j6, loss_coefficient=0, qext_w=100000, treturn_k=328.15) + pp.create_heat_consumer(net, j4, j7, loss_coefficient=0, qext_w=200000, treturn_k=333.15) + + pp.pipeflow(net, mode="bidirectional", iter=100) + return net + +def test_bad_point_pressure_lift_controller(district_heating_net): + """Test that controller maintains target pressure difference at worst point.""" + net = district_heating_net + target_dp = 1.5 + tolerance = 0.1 + + # Add controller to network + controller = BadPointPressureLiftController(net, target_dp_min_bar=target_dp, + tolerance=tolerance, proportional_gain=0.3) + net.controller.loc[len(net.controller)] = [controller, True, -1, -1, False, False] + + # Run pipeflow with control + for _ in range(5): + pp.pipeflow(net, mode="bidirectional", iter=100) + if controller.is_converged(net): + break + + # Verify convergence and target achievement + dp_min, worst_point_idx = controller.calculate_worst_point(net) + + assert net.converged, "Network should converge with controller" + assert worst_point_idx >= 0, "Controller should identify worst point" + assert abs(dp_min - target_dp) < tolerance, \ + f"Controller should reach target {target_dp} bar, got {dp_min:.3f} bar" + +def test_bad_point_controller_standby_mode(district_heating_net): + """Test that controller enters standby mode when no heat demand is present.""" + net = district_heating_net + + # Remove heat demand + net.heat_consumer["qext_w"] = 0 + + controller = BadPointPressureLiftController(net, target_dp_min_bar=1.5, + min_plift=1.5, min_pflow=3.5) + controller.control_step(net) + + # Verify standby mode sets minimum pressures + assert net.circ_pump_pressure["plift_bar"].iloc[0] == 1.5 + assert net.circ_pump_pressure["p_flow_bar"].iloc[0] == 3.5 + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 21fad2963a46925621d697a53e17834458e0ddc2 Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Fri, 7 Nov 2025 13:58:31 +0100 Subject: [PATCH 09/13] test: Organize controller test in control subdirectory Move BadPointPressureLiftController tests to test/control/ --- .../test_bad_point_pressure_lift_controller.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/pandapipes/test/{api/test_components => control}/test_bad_point_pressure_lift_controller.py (100%) diff --git a/src/pandapipes/test/api/test_components/test_bad_point_pressure_lift_controller.py b/src/pandapipes/test/control/test_bad_point_pressure_lift_controller.py similarity index 100% rename from src/pandapipes/test/api/test_components/test_bad_point_pressure_lift_controller.py rename to src/pandapipes/test/control/test_bad_point_pressure_lift_controller.py From 8f99a6f4e4e436372fa66deb72c3aa65aef29729 Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Fri, 7 Nov 2025 15:37:41 +0100 Subject: [PATCH 10/13] refactor: Improve BadPointPressureLiftController implementation Address code review feedback: - Simplify worst point calculation using pandas operations (min/idxmin) instead of manual loop for better performance and readability - Fix standby mode to only affect the controlled pump using .at indexing instead of .loc[:, ...] which would affect all pumps in the network Changes based on review by SimonRubenDrauz. All tests continue to pass. --- .../bad_point_pressure_lift_controller.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py index 991a9be83..a56879a80 100644 --- a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py +++ b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py @@ -50,20 +50,20 @@ def calculate_worst_point(self, net): Returns: tuple: The minimum pressure difference and the index of the worst point. """ - - dp = [] - - for idx, qext, p_from, p_to in zip(net.heat_consumer.index, net.heat_consumer["qext_w"], - net.res_heat_consumer["p_from_bar"], net.res_heat_consumer["p_to_bar"]): - if qext != 0: - dp_diff = p_from - p_to - dp.append((dp_diff, idx)) - - if not dp: + # Calculate pressure difference for all heat consumers with heat flow + diff = net.res_heat_consumer["p_from_bar"] - net.res_heat_consumer["p_to_bar"] + + # Filter out heat consumers with no heat flow + qext = net.heat_consumer["qext_w"] + active_consumers = qext != 0 + + if not active_consumers.any(): return 0, -1 - + # Find the minimum delta p where the heat flow is not zero - dp_min, idx_min = min(dp, key=lambda x: x[0]) + diff_active = diff[active_consumers] + dp_min = diff_active.min() + idx_min = diff_active.idxmin() return dp_min, idx_min @@ -120,8 +120,8 @@ def control_step(self, net): if all(net.heat_consumer["qext_w"] == 0): # Switch to standby mode print("No heat flow detected. Switching to standby mode.") - net.circ_pump_pressure.loc[:, "plift_bar"] = self.min_plift # Minimum lift pressure - net.circ_pump_pressure.loc[:, "p_flow_bar"] = self.min_pflow # Minimum flow pressure + net.circ_pump_pressure.at[self.circ_pump_pressure_idx, "plift_bar"] = self.min_plift # Minimum lift pressure + net.circ_pump_pressure.at[self.circ_pump_pressure_idx, "p_flow_bar"] = self.min_pflow # Minimum flow pressure return super(BadPointPressureLiftController, self).control_step(net) # Check whether the heat flow in the heat exchanger is zero From 286731c281bc11fce8f0cbc95c234cafa4b2925b Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Fri, 7 Nov 2025 15:42:02 +0100 Subject: [PATCH 11/13] docs: Convert controller docstrings to Sphinx reST format Changes based on code review feedback from SimonRubenDrauz. --- .../bad_point_pressure_lift_controller.py | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py index a56879a80..9a525180a 100644 --- a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py +++ b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py @@ -2,28 +2,37 @@ class BadPointPressureLiftController(BasicCtrl): """ - A controller for maintaining the pressure difference at the worst point (German: Differenzdruckregelung im Schlechtpunkt) in the network. + A controller for maintaining the pressure difference at the worst point + (German: Differenzdruckregelung im Schlechtpunkt) in the network. The BadPointPressureLiftController is a custom controller designed for district heating networks modeled with pandapipes. Its main purpose is to maintain a minimum pressure difference at the network's "worst point"—the heat exchanger with the lowest pressure difference (Schlechtpunktregelung). Key Features: + - **Automatic Worst Point Detection:** Identifies the heat exchanger with the lowest pressure difference where heat flow is present. - **Pressure Regulation:** Adjusts the circulation pump's lift and flow pressures to ensure the pressure difference at the worst point meets a specified minimum target. - **Proportional Control:** Uses a proportional gain to determine the adjustment magnitude based on the deviation from the target pressure difference. - **Standby Mode:** If no heat flow is detected, the controller switches the pump to a standby mode with minimum lift and flow pressures. - **Convergence Check:** Determines if the pressure difference is within a specified tolerance of the target, signaling convergence. - Args: - net (pandapipesNet): The pandapipes network. - circ_pump_pressure_idx (int, optional): Index of the circulation pump. Defaults to 0. - target_dp_min_bar (float, optional): Target minimum pressure difference in bar. Defaults to 1. - tolerance (float, optional): Tolerance for pressure difference. Defaults to 0.2. - proportional_gain (float, optional): Proportional gain for the controller. Defaults to 0.2. - min_plift (float, optional): Minimum lift pressure in bar. Defaults to 1.5. - min_pflow (float, optional): Minimum flow pressure in bar. Defaults to 3.5. - **kwargs: Additional keyword arguments. + :param net: The pandapipes network + :type net: pandapipesNet + :param circ_pump_pressure_idx: Index of the circulation pump, defaults to 0 + :type circ_pump_pressure_idx: int, optional + :param target_dp_min_bar: Target minimum pressure difference in bar, defaults to 1 + :type target_dp_min_bar: float, optional + :param tolerance: Tolerance for pressure difference, defaults to 0.2 + :type tolerance: float, optional + :param proportional_gain: Proportional gain for the controller, defaults to 0.2 + :type proportional_gain: float, optional + :param min_plift: Minimum lift pressure in bar, defaults to 1.5 + :type min_plift: float, optional + :param min_pflow: Minimum flow pressure in bar, defaults to 3.5 + :type min_pflow: float, optional + :param kwargs: Additional keyword arguments + :type kwargs: dict, optional """ def __init__(self, net, circ_pump_pressure_idx=0, target_dp_min_bar=1, tolerance=0.2, proportional_gain=0.2, min_plift=1.5, min_pflow=3.5, **kwargs): @@ -42,13 +51,13 @@ def __init__(self, net, circ_pump_pressure_idx=0, target_dp_min_bar=1, tolerance def calculate_worst_point(self, net): """ - Calculate the worst point in the heating network, defined as the heat exchanger with the lowest pressure difference. - - Args: - net (pandapipesNet): The pandapipes network. + Calculate the worst point in the heating network, defined as the heat exchanger + with the lowest pressure difference. - Returns: - tuple: The minimum pressure difference and the index of the worst point. + :param net: The pandapipes network + :type net: pandapipesNet + :return: Tuple of (minimum pressure difference, index of worst point) + :rtype: tuple(float, int) """ # Calculate pressure difference for all heat consumers with heat flow diff = net.res_heat_consumer["p_from_bar"] - net.res_heat_consumer["p_to_bar"] @@ -71,12 +80,12 @@ def time_step(self, net, time_step): """ Reset the iteration counter at the start of each time step. - Args: - net (pandapipesNet): The pandapipes network. - time_step (int): The current time step. - - Returns: - int: The current time step. + :param net: The pandapipes network + :type net: pandapipesNet + :param time_step: The current time step + :type time_step: int + :return: The current time step + :rtype: int """ self.iteration = 0 # reset iteration counter self.dp_min, self.heat_consumer_idx = self.calculate_worst_point(net) @@ -87,11 +96,10 @@ def is_converged(self, net): """ Check if the controller has converged. - Args: - net (pandapipesNet): The pandapipes network. - - Returns: - bool: True if converged, False otherwise. + :param net: The pandapipes network + :type net: pandapipesNet + :return: True if converged, False otherwise + :rtype: bool """ if all(net.heat_consumer["qext_w"] == 0): @@ -110,8 +118,8 @@ def control_step(self, net): """ Adjust the pump pressure to maintain the target pressure difference. - Args: - net (pandapipesNet): The pandapipes network. + :param net: The pandapipes network + :type net: pandapipesNet """ # Increment iteration counter self.iteration += 1 From f35bf4189fa35050b8d75d5bcca651b921d7321d Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Fri, 7 Nov 2025 16:31:26 +0100 Subject: [PATCH 12/13] refactor: Clean up whitespace in BadPointPressureLiftController pressure difference calculation --- .../controller/bad_point_pressure_lift_controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py index 9a525180a..bc2329f03 100644 --- a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py +++ b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py @@ -61,14 +61,14 @@ def calculate_worst_point(self, net): """ # Calculate pressure difference for all heat consumers with heat flow diff = net.res_heat_consumer["p_from_bar"] - net.res_heat_consumer["p_to_bar"] - + # Filter out heat consumers with no heat flow qext = net.heat_consumer["qext_w"] active_consumers = qext != 0 - + if not active_consumers.any(): return 0, -1 - + # Find the minimum delta p where the heat flow is not zero diff_active = diff[active_consumers] dp_min = diff_active.min() From bd56886d4681f357cb5711e9699682ad67cb0d42 Mon Sep 17 00:00:00 2001 From: Jonas Pfeiffer Date: Mon, 24 Nov 2025 10:30:02 +0100 Subject: [PATCH 13/13] Add dual-mode operation to BadPointPressureLiftController Implemented two operating modes to provide flexibility in pressure control strategy: - **fixed_preturn mode (default)**: Adjusts both p_flow_bar and plift_bar simultaneously to maintain constant return pressure. Ideal when return pressure stability is critical for the overall network. - **fixed_pflow mode**: Keeps flow pressure constant by adjusting only plift_bar. Return pressure may vary but is protected by min_preturn parameter. Useful when flow pressure must remain fixed due to supply line limitations. This resolves the implementation question of which control strategy is more appropriate by allowing users to choose based on their specific network requirements. Both modes successfully maintain the target pressure difference at the worst point while respecting different operational constraints. Changes: - Added 'mode' parameter with validation ('fixed_pflow'/'fixed_preturn') - Added 'min_preturn' parameter to prevent return pressure drops in fixed_pflow mode - Updated control_step() logic to handle both modes - Extended tests to verify both modes work correctly - Updated documentation and tutorial notebook with mode comparison --- .../bad_point_pressure_lift_controller.py | 44 ++- ...test_bad_point_pressure_lift_controller.py | 57 +++- .../BadPointPressureLiftController.ipynb | 289 +++++++++++++++++- 3 files changed, 359 insertions(+), 31 deletions(-) diff --git a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py index bc2329f03..bc6531c77 100644 --- a/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py +++ b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py @@ -14,6 +14,7 @@ class BadPointPressureLiftController(BasicCtrl): - **Automatic Worst Point Detection:** Identifies the heat exchanger with the lowest pressure difference where heat flow is present. - **Pressure Regulation:** Adjusts the circulation pump's lift and flow pressures to ensure the pressure difference at the worst point meets a specified minimum target. - **Proportional Control:** Uses a proportional gain to determine the adjustment magnitude based on the deviation from the target pressure difference. + - **Control Modes:** Supports two operating modes - fixed flow pressure (adjusts only plift) or fixed return pressure (adjusts both plift and pflow). - **Standby Mode:** If no heat flow is detected, the controller switches the pump to a standby mode with minimum lift and flow pressures. - **Convergence Check:** Determines if the pressure difference is within a specified tolerance of the target, signaling convergence. @@ -27,23 +28,33 @@ class BadPointPressureLiftController(BasicCtrl): :type tolerance: float, optional :param proportional_gain: Proportional gain for the controller, defaults to 0.2 :type proportional_gain: float, optional + :param mode: Control mode - 'fixed_pflow' (adjusts only plift, keeps p_flow constant) or 'fixed_preturn' (adjusts both plift and pflow, keeps p_return constant), defaults to 'fixed_preturn' + :type mode: str, optional :param min_plift: Minimum lift pressure in bar, defaults to 1.5 :type min_plift: float, optional :param min_pflow: Minimum flow pressure in bar, defaults to 3.5 :type min_pflow: float, optional + :param min_preturn: Minimum return pressure in bar (for fixed_pflow mode), defaults to 2.0 + :type min_preturn: float, optional :param kwargs: Additional keyword arguments :type kwargs: dict, optional """ def __init__(self, net, circ_pump_pressure_idx=0, target_dp_min_bar=1, tolerance=0.2, - proportional_gain=0.2, min_plift=1.5, min_pflow=3.5, **kwargs): + proportional_gain=0.2, mode='fixed_preturn', min_plift=1.5, min_pflow=3.5, + min_preturn=2.0, **kwargs): super(BadPointPressureLiftController, self).__init__(net, **kwargs) self.circ_pump_pressure_idx = circ_pump_pressure_idx self.target_dp_min_bar = target_dp_min_bar self.tolerance = tolerance self.proportional_gain = proportional_gain - self.min_plift = min_plift # Minimum pressure in bar - self.min_pflow = min_pflow # Minimum lift pressure in bar + if mode not in ['fixed_pflow', 'fixed_preturn']: + raise ValueError("mode must be either 'fixed_pflow' or 'fixed_preturn'") + self.mode = mode + + self.min_plift = min_plift # Minimum lift pressure in bar + self.min_pflow = min_pflow # Minimum flow pressure in bar + self.min_preturn = min_preturn # Minimum return pressure in bar self.iteration = 0 # Add iteration counter @@ -140,13 +151,24 @@ def control_step(self, net): dp_error = self.target_dp_min_bar - current_dp_bar - plift_adjustment = dp_error * self.proportional_gain - pflow_adjustment = dp_error * self.proportional_gain - - new_plift = current_plift_bar + plift_adjustment - new_pflow = current_pflow_bar + pflow_adjustment - - net.circ_pump_pressure["plift_bar"].at[self.circ_pump_pressure_idx] = new_plift - net.circ_pump_pressure["p_flow_bar"].at[self.circ_pump_pressure_idx] = new_pflow + if self.mode == 'fixed_pflow': + # Mode 1: Keep p_flow constant, adjust only plift + plift_adjustment = dp_error * self.proportional_gain + new_plift = current_plift_bar + plift_adjustment + + # Check if p_return would fall below minimum + new_preturn = current_pflow_bar - new_plift + if new_preturn < self.min_preturn: + new_plift = current_pflow_bar - self.min_preturn + + net.circ_pump_pressure["plift_bar"].at[self.circ_pump_pressure_idx] = new_plift + else: # fixed_preturn + # Mode 2: Keep p_return constant, adjust both plift and pflow + plift_adjustment = dp_error * self.proportional_gain + pflow_adjustment = dp_error * self.proportional_gain + new_plift = current_plift_bar + plift_adjustment + new_pflow = current_pflow_bar + pflow_adjustment + net.circ_pump_pressure["plift_bar"].at[self.circ_pump_pressure_idx] = new_plift + net.circ_pump_pressure["p_flow_bar"].at[self.circ_pump_pressure_idx] = new_pflow return super(BadPointPressureLiftController, self).control_step(net) \ No newline at end of file diff --git a/src/pandapipes/test/control/test_bad_point_pressure_lift_controller.py b/src/pandapipes/test/control/test_bad_point_pressure_lift_controller.py index 6c285519b..020229dc0 100644 --- a/src/pandapipes/test/control/test_bad_point_pressure_lift_controller.py +++ b/src/pandapipes/test/control/test_bad_point_pressure_lift_controller.py @@ -2,6 +2,7 @@ import pytest import pandapipes as pp from pandapipes.control import BadPointPressureLiftController +from pandapipes.control.run_control import run_control @pytest.fixture def district_heating_net(): @@ -38,29 +39,38 @@ def district_heating_net(): return net def test_bad_point_pressure_lift_controller(district_heating_net): - """Test that controller maintains target pressure difference at worst point.""" + """Test that controller maintains target pressure difference at worst point (default mode: fixed_preturn).""" net = district_heating_net target_dp = 1.5 tolerance = 0.1 - # Add controller to network + # Add controller to network with default mode (fixed_preturn) controller = BadPointPressureLiftController(net, target_dp_min_bar=target_dp, tolerance=tolerance, proportional_gain=0.3) net.controller.loc[len(net.controller)] = [controller, True, -1, -1, False, False] + # Get initial return pressure + initial_pflow = net.circ_pump_pressure["p_flow_bar"].iloc[0] + initial_plift = net.circ_pump_pressure["plift_bar"].iloc[0] + initial_preturn = initial_pflow - initial_plift + # Run pipeflow with control - for _ in range(5): - pp.pipeflow(net, mode="bidirectional", iter=100) - if controller.is_converged(net): - break + + run_control(net, mode="bidirectional", max_iter=100) # Verify convergence and target achievement dp_min, worst_point_idx = controller.calculate_worst_point(net) + final_pflow = net.circ_pump_pressure["p_flow_bar"].iloc[0] + final_plift = net.circ_pump_pressure["plift_bar"].iloc[0] + final_preturn = final_pflow - final_plift assert net.converged, "Network should converge with controller" assert worst_point_idx >= 0, "Controller should identify worst point" assert abs(dp_min - target_dp) < tolerance, \ f"Controller should reach target {target_dp} bar, got {dp_min:.3f} bar" + # In fixed_preturn mode, return pressure should remain constant + assert abs(final_preturn - initial_preturn) < 0.01, \ + f"Return pressure should remain constant in fixed_preturn mode, changed by {final_preturn - initial_preturn:.4f} bar" def test_bad_point_controller_standby_mode(district_heating_net): """Test that controller enters standby mode when no heat demand is present.""" @@ -77,5 +87,40 @@ def test_bad_point_controller_standby_mode(district_heating_net): assert net.circ_pump_pressure["plift_bar"].iloc[0] == 1.5 assert net.circ_pump_pressure["p_flow_bar"].iloc[0] == 3.5 +def test_bad_point_controller_fixed_pflow_mode(district_heating_net): + """Test that controller works correctly in fixed_pflow mode.""" + net = district_heating_net + target_dp = 1.5 + tolerance = 0.1 + + # Add controller in fixed_pflow mode + controller = BadPointPressureLiftController(net, target_dp_min_bar=target_dp, + tolerance=tolerance, proportional_gain=0.3, + mode='fixed_pflow', min_preturn=2.0) + net.controller.loc[len(net.controller)] = [controller, True, -1, -1, False, False] + + # Get initial values + initial_pflow = net.circ_pump_pressure["p_flow_bar"].iloc[0] + + # Run pipeflow with control + run_control(net, mode="bidirectional", max_iter=100) + + # Verify convergence and target achievement + dp_min, worst_point_idx = controller.calculate_worst_point(net) + final_pflow = net.circ_pump_pressure["p_flow_bar"].iloc[0] + final_plift = net.circ_pump_pressure["plift_bar"].iloc[0] + final_preturn = final_pflow - final_plift + + assert net.converged, "Network should converge with controller" + assert worst_point_idx >= 0, "Controller should identify worst point" + assert abs(dp_min - target_dp) < tolerance, \ + f"Controller should reach target {target_dp} bar, got {dp_min:.3f} bar" + # In fixed_pflow mode, flow pressure should remain constant + assert abs(final_pflow - initial_pflow) < 0.01, \ + f"Flow pressure should remain constant in fixed_pflow mode, changed by {final_pflow - initial_pflow:.4f} bar" + # Return pressure should not fall below minimum + assert final_preturn >= 2.0 - 0.01, \ + f"Return pressure should not fall below min_preturn (2.0 bar), got {final_preturn:.4f} bar" + if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tutorials/BadPointPressureLiftController.ipynb b/tutorials/BadPointPressureLiftController.ipynb index 2052522ef..0b34a7652 100644 --- a/tutorials/BadPointPressureLiftController.ipynb +++ b/tutorials/BadPointPressureLiftController.ipynb @@ -13,7 +13,7 @@ "source": [ "## BadPointPressureLiftController: Pressure Control at the Worst Point\n", "\n", - "The `BadPointPressureLiftController` is a custom controller designed for district heating networks modeled with pandapipes. Its main purpose is to maintain a minimum pressure difference at the network's \"worst point\"—the heat exchanger with the lowest pressure difference (Schlechtpunktregelung).\n", + "The `BadPointPressureLiftController` is a custom controller designed for district heating networks modeled with pandapipes. Its main purpose is to maintain a minimum pressure difference at the network's \"worst point\", the heat exchanger with the lowest pressure difference (German: Schlechtpunktregelung).\n", "\n", "### Key Features\n", "\n", @@ -33,7 +33,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -68,26 +68,38 @@ "\n", "Once the network is initialized, the controller will automatically regulate the pump pressures during simulation to maintain the minimum pressure difference at the worst point (the heat exchanger with the lowest pressure difference). This ensures reliable operation and helps prevent under-supply at critical points in the network.\n", "\n", - "In my implementation, a dp_min of 1 bar is used in the Controller." + "In this implementation, a dp_min of 1 bar is used in the Controller." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Setting up test network...\n", + "\n", + "Controller converged successfully!\n", + "Controller converged successfully!\n" + ] + } + ], "source": [ "import pandapipes as pp\n", "import numpy as np\n", "\n", + "from pandapipes.control.run_control import run_control\n", + "\n", "def initialize_test_net(qext_w=np.array([100000, 200000]),\n", " return_temperature=np.array([55, 60]),\n", - " supply_temperature=85, \n", + " supply_temperature=85,\n", " flow_pressure_pump=4,\n", " lift_pressure_pump=1.5,\n", " pipetype=\"110/202 PLUS\"):\n", - " \n", - " print(\"Running the test network initialization script.\")\n", + "\n", " net = pp.create_empty_network(fluid=\"water\")\n", "\n", " k = 0.1 # roughness defaults to 0.1\n", @@ -111,20 +123,20 @@ " j7 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 7\", geodata=(60, 10))\n", " j8 = pp.create_junction(net, pn_bar=1.05, tfluid_k=suply_temperature_k, name=\"Junction 8\", geodata=(10, 10))\n", "\n", - " pump1 = pp.create_circ_pump_const_pressure(net, j1, j2, p_flow_bar=flow_pressure_pump, plift_bar=lift_pressure_pump, \n", + " pump1 = pp.create_circ_pump_const_pressure(net, j1, j2, p_flow_bar=flow_pressure_pump, plift_bar=lift_pressure_pump,\n", " t_flow_k=suply_temperature_k, type=\"auto\", name=\"pump1\")\n", "\n", " pipe1 = pp.create_pipe(net, j2, j3, std_type=pipetype, length_km=0.01, k_mm=k, name=\"pipe1\", sections=5, text_k=283)\n", " pipe2 = pp.create_pipe(net, j3, j4, std_type=pipetype, length_km=0.05, k_mm=k, name=\"pipe2\", sections=5, text_k=283)\n", " pipe3 = pp.create_pipe(net, j4, j5, std_type=pipetype, length_km=0.025, k_mm=k, name=\"pipe3\", sections=5, text_k=283)\n", "\n", - " heat_consumer1 = pp.create_heat_consumer(net, from_junction=j5, to_junction=j6, loss_coefficient=0, qext_w=qext_w[0], \n", + " heat_consumer1 = pp.create_heat_consumer(net, from_junction=j5, to_junction=j6, loss_coefficient=0, qext_w=qext_w[0],\n", " treturn_k=return_temperature_k[0], name=\"heat_consumer_1\")\n", - " \n", "\n", - " heat_consumer2 = pp.create_heat_consumer(net, from_junction=j4, to_junction=j7, loss_coefficient=0, qext_w=qext_w[1], \n", + "\n", + " heat_consumer2 = pp.create_heat_consumer(net, from_junction=j4, to_junction=j7, loss_coefficient=0, qext_w=qext_w[1],\n", " treturn_k=return_temperature_k[1], name=\"heat_consumer_2\")\n", - " \n", + "\n", " pipe4 = pp.create_pipe(net, j6, j7, std_type=pipetype, length_km=0.25, k_mm=k, name=\"pipe4\", sections=5, text_k=283)\n", " pipe5 = pp.create_pipe(net, j7, j8, std_type=pipetype, length_km=0.05, k_mm=k, name=\"pipe5\", sections=5, text_k=283)\n", " pipe6 = pp.create_pipe(net, j8, j1, std_type=pipetype, length_km=0.01, k_mm=k, name=\"pipe6\", sections=5, text_k=283)\n", @@ -133,19 +145,268 @@ "\n", " return net\n", "\n", + "print(\"Setting up test network...\")\n", "net = initialize_test_net()\n", "\n", "dp_controller = BadPointPressureLiftController(net)\n", "net.controller.loc[len(net.controller)] = [dp_controller, True, -1, -1, False, False]\n", "\n", - "pp.pipeflow(net, mode=\"bidirectional\", iter=100)" + "run_control(net, mode=\"bidirectional\", iter=100)\n", + "\n", + "\n", + "print(\"Controller converged successfully!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "You can now proceed to run time-series simulations or further analyses, and the controller will handle pressure adjustments as needed. \n", + "## Comparison of Controller Modes\n", + "\n", + "The `BadPointPressureLiftController` supports two operating modes:\n", + "\n", + "1. **`mode='fixed_preturn'` (default)**: Keeps the return pressure constant by adjusting both `p_flow_bar` and `plift_bar` simultaneously.\n", + "2. **`mode='fixed_pflow'`**: Keeps the flow pressure constant by adjusting only `plift_bar`. The return pressure may vary in this mode.\n", + "\n", + "Let's compare both modes to understand their behavior and impact on the network." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ParameterMode: fixed_preturnMode: fixed_pflow
0p_flow (initial)4.0000 bar4.0000 bar
1p_flow (final)3.7149 bar4.0000 bar
2p_flow (change)-0.2851 bar+0.0000 bar
3plift (initial)1.5000 bar1.5000 bar
4plift (final)1.2149 bar1.2149 bar
5plift (change)-0.2851 bar-0.2851 bar
6p_return (initial)2.5000 bar2.5000 bar
7p_return (final)2.5000 bar2.7851 bar
8p_return (change)+0.0000 bar+0.2851 bar
9dp_min (final)1.4828 bar1.4828 bar
10ConvergedTrueTrue
11Iterations44
\n", + "
" + ], + "text/plain": [ + " Parameter Mode: fixed_preturn Mode: fixed_pflow\n", + "0 p_flow (initial) 4.0000 bar 4.0000 bar\n", + "1 p_flow (final) 3.7149 bar 4.0000 bar\n", + "2 p_flow (change) -0.2851 bar +0.0000 bar\n", + "3 plift (initial) 1.5000 bar 1.5000 bar\n", + "4 plift (final) 1.2149 bar 1.2149 bar\n", + "5 plift (change) -0.2851 bar -0.2851 bar\n", + "6 p_return (initial) 2.5000 bar 2.5000 bar\n", + "7 p_return (final) 2.5000 bar 2.7851 bar\n", + "8 p_return (change) +0.0000 bar +0.2851 bar\n", + "9 dp_min (final) 1.4828 bar 1.4828 bar\n", + "10 Converged True True\n", + "11 Iterations 4 4" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "# Test Mode 1: fixed_preturn (keeps return pressure constant)\n", + "net1 = initialize_test_net()\n", + "controller1 = BadPointPressureLiftController(net1, mode='fixed_preturn')\n", + "net1.controller.loc[len(net1.controller)] = [controller1, True, -1, -1, False, False]\n", + "\n", + "# Get initial values\n", + "initial_pflow_1 = net1.circ_pump_pressure[\"p_flow_bar\"].iloc[0]\n", + "initial_plift_1 = net1.circ_pump_pressure[\"plift_bar\"].iloc[0]\n", + "initial_preturn_1 = initial_pflow_1 - initial_plift_1\n", + "\n", + "# Run control (suppress output)\n", + "run_control(net1, max_iter=10)\n", + "\n", + "# Get final values\n", + "final_pflow_1 = net1.circ_pump_pressure[\"p_flow_bar\"].iloc[0]\n", + "final_plift_1 = net1.circ_pump_pressure[\"plift_bar\"].iloc[0]\n", + "final_preturn_1 = final_pflow_1 - final_plift_1\n", + "final_dp_min_1 = controller1.dp_min\n", + "\n", + "# Test Mode 2: fixed_pflow (keeps flow pressure constant)\n", + "net2 = initialize_test_net()\n", + "controller2 = BadPointPressureLiftController(net2, mode='fixed_pflow', min_preturn=2.0)\n", + "net2.controller.loc[len(net2.controller)] = [controller2, True, -1, -1, False, False]\n", + "\n", + "# Get initial values\n", + "initial_pflow_2 = net2.circ_pump_pressure[\"p_flow_bar\"].iloc[0]\n", + "initial_plift_2 = net2.circ_pump_pressure[\"plift_bar\"].iloc[0]\n", + "initial_preturn_2 = initial_pflow_2 - initial_plift_2\n", + "\n", + "# Run control (suppress output)\n", + "run_control(net2, max_iter=10)\n", + "\n", + "# Get final values\n", + "final_pflow_2 = net2.circ_pump_pressure[\"p_flow_bar\"].iloc[0]\n", + "final_plift_2 = net2.circ_pump_pressure[\"plift_bar\"].iloc[0]\n", + "final_preturn_2 = final_pflow_2 - final_plift_2\n", + "final_dp_min_2 = controller2.dp_min\n", + "\n", + "# Create comparison table\n", + "comparison_data = {\n", + " 'Parameter': ['p_flow (initial)', 'p_flow (final)', 'p_flow (change)',\n", + " 'plift (initial)', 'plift (final)', 'plift (change)',\n", + " 'p_return (initial)', 'p_return (final)', 'p_return (change)',\n", + " 'dp_min (final)', 'Converged', 'Iterations'],\n", + " 'Mode: fixed_preturn': [\n", + " f\"{initial_pflow_1:.4f} bar\",\n", + " f\"{final_pflow_1:.4f} bar\",\n", + " f\"{final_pflow_1 - initial_pflow_1:+.4f} bar\",\n", + " f\"{initial_plift_1:.4f} bar\",\n", + " f\"{final_plift_1:.4f} bar\",\n", + " f\"{final_plift_1 - initial_plift_1:+.4f} bar\",\n", + " f\"{initial_preturn_1:.4f} bar\",\n", + " f\"{final_preturn_1:.4f} bar\",\n", + " f\"{final_preturn_1 - initial_preturn_1:+.4f} bar\",\n", + " f\"{final_dp_min_1:.4f} bar\",\n", + " str(controller1.is_converged(net1)),\n", + " str(controller1.iteration)\n", + " ],\n", + " 'Mode: fixed_pflow': [\n", + " f\"{initial_pflow_2:.4f} bar\",\n", + " f\"{final_pflow_2:.4f} bar\",\n", + " f\"{final_pflow_2 - initial_pflow_2:+.4f} bar\",\n", + " f\"{initial_plift_2:.4f} bar\",\n", + " f\"{final_plift_2:.4f} bar\",\n", + " f\"{final_plift_2 - initial_plift_2:+.4f} bar\",\n", + " f\"{initial_preturn_2:.4f} bar\",\n", + " f\"{final_preturn_2:.4f} bar\",\n", + " f\"{final_preturn_2 - initial_preturn_2:+.4f} bar\",\n", + " f\"{final_dp_min_2:.4f} bar\",\n", + " str(controller2.is_converged(net2)),\n", + " str(controller2.iteration)\n", + " ]\n", + "}\n", + "\n", + "df_comparison = pd.DataFrame(comparison_data)\n", + "\n", + "# Display the comparison table\n", + "df_comparison" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Analysis of Results\n", + "\n", + "**Mode 1: `fixed_preturn`**\n", + "- The return pressure (`p_return`) remains constant throughout the control process\n", + "- Both `p_flow_bar` and `plift_bar` are adjusted by the same amount\n", + "- This mode is ideal when maintaining a stable return pressure is critical for the system\n", + "\n", + "**Mode 2: `fixed_pflow`**\n", + "- The flow pressure (`p_flow_bar`) remains constant\n", + "- Only `plift_bar` is adjusted\n", + "- The return pressure (`p_return`) will decrease as `plift_bar` increases\n", + "- The `min_preturn` parameter prevents the return pressure from falling below a safe minimum\n", + "\n", + "**Which mode to choose?**\n", + "- Use `fixed_preturn` (default) when return pressure stability is important for the overall network\n", + "- Use `fixed_pflow` when the flow pressure must remain constant (e.g., due to pressure limitations in the supply line)\n", + "\n", + "---\n", + "\n", + "You can now proceed to run time-series simulations or further analyses, and the controller will handle pressure adjustments as needed.\n", "\n", "Suggestions for improvements or alternative approaches are appreciated. Please feel free to contribute." ]