-
Notifications
You must be signed in to change notification settings - Fork 87
Add BadPointPressureLiftController tutorial notebook for district heating network #711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 13 commits
5cbe9a7
abe6a37
a5269ab
a595d70
5e6281a
a6913ca
7a7813f
b7361cc
a1ada19
2ec057f
03f323f
6b750f5
21fad29
4c3de27
8f99a6f
51c0b0c
286731c
f35bf41
bd56886
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from pandapipes.control.controller.bad_point_pressure_lift_controller import BadPointPressureLiftController |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not using:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point! Old implementation of mine, never optimized it. I'm going to simplify the code to use pandas operations instead of the manual loop: |
||
|
|
||
| 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.loc[:, "plift_bar"] = self.min_plift # Minimum lift pressure | ||
| net.circ_pump_pressure.loc[:, "p_flow_bar"] = self.min_pflow # Minimum flow pressure | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why all circ pumps?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right. I've never had a problem, as I always only use one circ_pump_pressure. I'm going to change it from .loc[:, ...] to .at[self.circ_pump_pressure_idx, ...] so it only affects the single controlled pump, not all pumps in the network. |
||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to me a bit random and error prone. Usually you keep one pressure fixed: flow or return and only adapt the pressure lift accordingly.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my understanding: For this application we need to adjust both p_flow_bar If we only adjust plift_bar while keeping p_flow_bar constant, increasing plift_bar would By adjusting both pressures with the same proportional gain, we make sure, that the return pressure stays in place. If that's not the case, I'd like to discuss it, before finishing this pull request.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we should discuss this first I guess. |
||
|
|
||
| return super(BadPointPressureLiftController, self).control_step(net) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you adapt the docstrings to Sphinx / reStructuredText (reST) style