Skip to content
Open
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5cbe9a7
add BadPointPressureLiftController tutorial notebook for district hea…
JonasPfeiffer123 May 22, 2025
abe6a37
Merge branch 'e2nIEE:develop' into Tutorials-Examples-District-Heatin…
JonasPfeiffer123 Aug 28, 2025
a5269ab
Create MinimumSupplyTemperatureController.ipynb
JonasPfeiffer123 Aug 28, 2025
a595d70
The previous commit was an accident in this branch.
JonasPfeiffer123 Aug 28, 2025
5e6281a
Added BadPointPressureController reference in the documentation
JonasPfeiffer123 Sep 2, 2025
a6913ca
Merge branch 'e2nIEE:develop' into Tutorials-Examples-District-Heatin…
JonasPfeiffer123 Nov 4, 2025
7a7813f
feat: Add BadPointPressureLiftController as standalone controller module
JonasPfeiffer123 Nov 4, 2025
b7361cc
style: Remove trailing whitespace in BadPointPressureLiftController
JonasPfeiffer123 Nov 4, 2025
a1ada19
fix: Remove unnecessary whitespace in BadPointPressureLiftController
JonasPfeiffer123 Nov 4, 2025
2ec057f
Merge branch 'develop' into Tutorials-Examples-District-Heating-Contr…
EPrade Nov 5, 2025
03f323f
Merge branch 'develop' into Tutorials-Examples-District-Heating-Contr…
SimonRubenDrauz Nov 7, 2025
6b750f5
test: Add test suite for BadPointPressureLiftController
JonasPfeiffer123 Nov 7, 2025
21fad29
test: Organize controller test in control subdirectory
JonasPfeiffer123 Nov 7, 2025
4c3de27
Merge branch 'develop' into Tutorials-Examples-District-Heating-Contr…
EPrade Nov 7, 2025
8f99a6f
refactor: Improve BadPointPressureLiftController implementation
JonasPfeiffer123 Nov 7, 2025
51c0b0c
Merge branch 'Tutorials-Examples-District-Heating-Controllers-' of ht…
JonasPfeiffer123 Nov 7, 2025
286731c
docs: Convert controller docstrings to Sphinx reST format
JonasPfeiffer123 Nov 7, 2025
f35bf41
refactor: Clean up whitespace in BadPointPressureLiftController press…
JonasPfeiffer123 Nov 7, 2025
bd56886
Add dual-mode operation to BadPointPressureLiftController
JonasPfeiffer123 Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 300 additions & 0 deletions tutorials/BadPointPressureController.ipynb
Original file line number Diff line number Diff line change
@@ -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
}