diff --git a/doc/source/controller/controller_classes.rst b/doc/source/controller/controller_classes.rst index ed9af08c0..3e48b846e 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: BadPointPressureLiftController +========================================================== + +A practical example of a custom controller implementation is provided in the Jupyter Notebook +`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 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..bc6531c77 --- /dev/null +++ b/src/pandapipes/control/controller/bad_point_pressure_lift_controller.py @@ -0,0 +1,174 @@ +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. + - **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. + + :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 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, 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 + + 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 + + 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. + + :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"] + + # 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() + idx_min = diff_active.idxmin() + + return dp_min, idx_min + + def time_step(self, net, time_step): + """ + Reset the iteration counter at the start of each 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) + + return time_step + + def is_converged(self, net): + """ + Check if the controller has converged. + + :param net: The pandapipes network + :type net: pandapipesNet + :return: True if converged, False otherwise + :rtype: bool + """ + + 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. + + :param net: The pandapipes network + :type net: pandapipesNet + """ + # 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.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 + 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 + + 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 new file mode 100644 index 000000000..020229dc0 --- /dev/null +++ b/src/pandapipes/test/control/test_bad_point_pressure_lift_controller.py @@ -0,0 +1,126 @@ +import numpy as np +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(): + """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 (default mode: fixed_preturn).""" + net = district_heating_net + target_dp = 1.5 + tolerance = 0.1 + + # 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 + + 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.""" + 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 + +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 new file mode 100644 index 000000000..0b34a7652 --- /dev/null +++ b/tutorials/BadPointPressureLiftController.ipynb @@ -0,0 +1,436 @@ +{ + "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 (German: 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": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from pandapipes.control import BadPointPressureLiftController" + ] + }, + { + "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 this implementation, a dp_min of 1 bar is used in the Controller." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "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", + " flow_pressure_pump=4,\n", + " lift_pressure_pump=1.5,\n", + " pipetype=\"110/202 PLUS\"):\n", + "\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_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", + " 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", + " 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", + "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", + "run_control(net, mode=\"bidirectional\", iter=100)\n", + "\n", + "\n", + "print(\"Controller converged successfully!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 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." + ] + } + ], + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}