diff --git a/tutorials/circular_flow_in_a_district_heating_grid.ipynb b/tutorials/circular_flow_in_a_district_heating_grid.ipynb deleted file mode 100644 index 0a57948ea..000000000 --- a/tutorials/circular_flow_in_a_district_heating_grid.ipynb +++ /dev/null @@ -1,249 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Circular flow in a district heating grid\n", - "\n", - "This example shows how to set up and solve the following network:\n", - "\n", - "\n", - "\n", - "In this example, we will not only calculate the pressure and velocity distribution in the network, but also determine the temperature levels. The pump feeds fluid of a given temperature into the grid. Due to losses, the temperature will fall. The heat exchanger removes more heat from the network. On its way back to the pump, the temperature will fall further. \n", - "\n", - "The network is based on the topology of a district heating grid, where the fluid returns to the pump after the consumers (heat exchangers) have been supplied.\n", - "\n", - "To set up this network, at first, the pandapipes package has to be imported. Additionally, a net container is created and, at the same time, water as a fluid is chosen." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pandapipes as pp\n", - "\n", - "# create empty net\n", - "net = pp.create_empty_network(fluid =\"water\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that the flow of the example network flows in a closed loop. We will need four junctions.\n", - "The parameters `pn_bar` and `tfluid_k` that have to be set in the `create_junction`-function\n", - "are\n", - "only used as starting points for the network simulation. The fix pressure and fluid temperature is\n", - "being determined by the circular pump component which will be created afterwards." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "j0 = pp.create_junction(net, pn_bar=5, tfluid_k=293.15, name=\"junction 0\")\n", - "j1 = pp.create_junction(net, pn_bar=5, tfluid_k=293.15, name=\"junction 1\")\n", - "j2 = pp.create_junction(net, pn_bar=5, tfluid_k=293.15, name=\"junction 2\")\n", - "j3 = pp.create_junction(net, pn_bar=5, tfluid_k=293.15, name=\"junction 3\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, the pump will be created. The type of pump we choose needs a mass flow, a pressure level and a\n", - "temperature as input. Note that the circular pump is a component, which internally consists of an\n", - "external grid, connected to the junction specified via the from_junction-parameter and a sink,\n", - "connected to the junction specified via the to_junction-parameter.\n", - "\n", - "However, the internal structure is not visible to the user, so that the circular pump component\n", - "supplies a fluid flow with the specified properties." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pp.create_circ_pump_const_mass_flow(net, return_junction=j3, flow_junction=j0, p_flow_bar=5,\n", - " mdot_flow_kg_per_s=20, t_flow_k=273.15+35)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, the heat exchanger component is created.\n", - "The most important parameter for this component is the heat flux `qext_w`. A positive value of\n", - "`qext_w` means that heat is withdrawn from the network and supplied to a consumer.\n", - "A negative value of `qext_w` corresponds to a heat source, i. e. thermal energy is being transfered\n", - "from the heat exchanger into the network." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pp.create_heat_exchanger(net, from_junction=j1, to_junction=j2, diameter_m=200e-3, qext_w = 100000)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The following commands defines the pipes between the components. Each pipe will consist of five\n", - "internal sections in order to improve the spatial resolution for the temperature calculation.\n", - "The parameter `text_k` specifies the ambient temperature on the outside of the pipe. It is used to\n", - "calculate energy losses." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pp.create_pipe_from_parameters(net, from_junction=j0, to_junction=j1, length_km=1,\n", - " diameter_m=200e-3, k_mm=.1, u_w_per_m2k=10, sections = 5, text_k=283)\n", - "pp.create_pipe_from_parameters(net, from_junction=j2, to_junction=j3, length_km=1,\n", - " diameter_m=200e-3, k_mm=.1, u_w_per_m2k=10, sections = 5, text_k=283)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "We now run a pipe flow.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pp.pipeflow(net, mode='sequential')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "By default, only the pressure and velocity distribution is calculated by the pipeflow function. If\n", - "the `mode`-parameter is set to \"all\", the heat transfer calculation is started automatically\n", - "after the hydraulics computation. Computed mass flows are used as an input for the temperature\n", - "calculation. After the computation, you can check the results for junctions and pipes:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "net.res_junction" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that a constant heat flow is extracted via the heat exchanger between nodes 1 and 2. Heat\n", - "losses due to the ambient temperature level are not taken into account. These are only included in\n", - "the pipe components. This also means that - if the extracted heat flow is large enough - the\n", - "temperature level behind the heat exchanger might be lower than the ambient temperature level. A\n", - "way to avoid this behaviour would be to create a controller which defines a function for the\n", - "extracted heat in dependence of the ambient temperature." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "net.res_pipe" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The command above shows the results for the pipe components. The temperatures of the adjacent\n", - "junctions are displayed. Due to heat losses, the temperatures at the to-nodes is lower than the\n", - "temperatures at the from-nodes. Note also that the junctions are not equal to the internal nodes,\n", - "introduced by the pipe sections we defined. To display the temperatures at the internal nodes, we\n", - "can retrieve the internal node values with the following commands:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pandapipes.component_models import Pipe\n", - "pipe_results = Pipe.get_internal_results(net, [0])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The parameters of the get_internal_results function correspond to the net and the pipes we want to\n", - "evaluate. In this case, only the results of pipe zero are retrieved. The returned value stored in\n", - "pipe_results is a dictionary, containing fields for the pressure, the velocity and the temperature.\n", - "The dictionary can either be used for own evaluations now or it can be used to plot the results over\n", - "the pipe length:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Pipe.plot_pipe(net, 0, pipe_results)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can see that the pressure level falls due to friction. As the fluid is incompressible, the\n", - "velocity remains constant over the pipe length. Because the temperature level at the pipe entry is\n", - "higher than the ambient temperature, the temperature level decreases." - ] - } - ], - "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.9.16" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tutorials/district_heating/circular_flow_in_a_district_heating_grid.ipynb b/tutorials/district_heating/circular_flow_in_a_district_heating_grid.ipynb new file mode 100644 index 000000000..f03f37c3a --- /dev/null +++ b/tutorials/district_heating/circular_flow_in_a_district_heating_grid.ipynb @@ -0,0 +1,307 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Circular flow in a district heating grid\n", + "\n", + "This example demonstrates how to model and analyze a simple district heating network with a circular flow using pandapipes.\n", + "\n", + "The network consists of a pump (representing a heat generator) that supplies hot water, which flows through consumers (heat exchangers), releases heat, and then returns to the pump. In addition to pressure and velocity distribution, the temperature profile throughout the network is also calculated. Due to heat losses and heat extraction by the consumer, the temperature will decrease along the flow path. Understanding these temperature drops is crucial for energy efficiency and proper system design in district heating networks.\n", + "\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Network Setup\n", + "\n", + "To set up this network, first import the pandapipes package. Additionally, create a net container and select water as the working fluid." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandapipes as pp\n", + "\n", + "# create empty net\n", + "net = pp.create_empty_network(fluid =\"water\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Junctions\n", + "\n", + "Note that the flow in this example network forms a closed loop. We will use four junctions for this demonstration. The parameters `pn_bar` (pressure) and `tfluid_k` (temperature) set in the `create_junction` function are only used as initial values for the network simulation. It is recommended to initialize the network with the flow temperature of the supplying pump. The actual pressure and fluid temperature are determined by the circular pump component, which will be created afterwards. For more complex networks, the number of junctions can be easily adapted." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "j0 = pp.create_junction(net, pn_bar=4, tfluid_k=293.15, name=\"junction 0\", geodata=(0, 0))\n", + "j1 = pp.create_junction(net, pn_bar=4, tfluid_k=293.15, name=\"junction 1\", geodata=(0, 10))\n", + "j2 = pp.create_junction(net, pn_bar=4, tfluid_k=293.15, name=\"junction 2\", geodata=(10, 10))\n", + "j3 = pp.create_junction(net, pn_bar=4, tfluid_k=293.15, name=\"junction 3\", geodata=(10, 0))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Circ Pump Const Pressure\n", + "\n", + "Now, the pump will be created. For a simple district heating network, the `circ_pump_const_pressure` component is used. This type of pump requires a flow pressure, a pressure lift, and a flow temperature as input. The mass flow is calculated during the pipeflow calculation by solving the mass balance in the network.\n", + "\n", + "The pressure lift represents the energy added to the fluid by the pump to overcome losses and supply the network with sufficient pressure.\n", + "\n", + "Note that the circular pump is a component which internally consists of an external grid (connected to the junction specified via the `from_junction` parameter) and a sink (connected to the junction specified via the `to_junction` parameter). However, this internal structure is not visible to the user, so the circular pump const pressure component supplies a fluid flow with the specified properties." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pp.create_circ_pump_const_pressure(net, return_junction=j0, flow_junction=j1, p_flow_bar=4, plift_bar=1.5, t_flow_k=273.15+70,\n", + " type=\"auto\", name=\"const_pressure_pump\", index=None, in_service=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Heat Consumer\n", + "\n", + "Next, the heat consumer component is created.\n", + "The key parameter for this component is the heat flux `qext_w`. A positive value of `qext_w` means that heat is withdrawn from the network and supplied to a consumer. A negative value of `qext_w` corresponds to a heat source, i.e., thermal energy is transferred from the heat exchanger into the network.\n", + "\n", + "The behavior of the heat consumer can be further specified using the following parameters:\n", + "\n", + "- If `treturn_k` (return temperature) is defined, no other parameter should be set. The system will then calculate the resulting mass flow in the heat consumer based on the transferred heat.\n", + "- If `controlled_mdot_kg_per_s` (controlled mass flow) is specified, a fixed mass flow is set in the heat consumer, and the temperatures are calculated by solving the mass and energy balances.\n", + "- If `deltat_k` (temperature difference) is used, a fixed temperature drop is set for the heat consumer, enabling the calculation of both mass flow and return temperature.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pp.create_heat_consumer(net, from_junction=j2, to_junction=j3, qext_w=10000, controlled_mdot_kg_per_s=None,\n", + " deltat_k=None, treturn_k=50, name=None, index=None, in_service=True, type=\"heat_consumer\")\n", + "#pp.create_heat_consumer(net, from_junction=j2, to_junction=j3, qext_w=10000, controlled_mdot_kg_per_s=2,\n", + "# deltat_k=None, treturn_k=None, name=None, index=None, in_service=True, type=\"heat_consumer\")\n", + "#pp.create_heat_consumer(net, from_junction=j2, to_junction=j3, qext_w=10000, controlled_mdot_kg_per_s=None,\n", + "# deltat_k=10, treturn_k=None, name=None, index=None, in_service=True, type=\"heat_consumer\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pipes\n", + "\n", + "The following commands define the pipes between the components. Each pipe will consist of (in this case) five internal sections to improve the spatial resolution for the temperature calculation. Increasing the number of sections improves the accuracy of the temperature profile, but also increases computation time.\n", + "\n", + "The parameter `text_k` specifies the ambient temperature on the outside of the pipe and is used to calculate energy losses. `k_mm` defines the roughness in the pipe, which is used in the pressure drop calculation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pp.create_pipe(net, from_junction=j1, to_junction=j2, std_type=\"110/202 PLUS\", length_km=1, k_mm=0.1, loss_coefficient=0,\n", + " sections=5, text_k=283, qext_w=0., name=\"pipe_0_1\", index=None, geodata=None, in_service=True, type=\"pipe\")\n", + "pp.create_pipe(net, from_junction=j3, to_junction=j0, std_type=\"110/202 PLUS\", length_km=1, k_mm=0.1, loss_coefficient=0,\n", + " sections=5, text_k=283, qext_w=0., name=\"pipe_1_2\", index=None, geodata=None, in_service=True, type=\"pipe\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting Net\n", + "\n", + "The following command creates a simple plot of the network layout using `pandapipes.plotting.simple_plot`. \n", + "This visualization helps to understand the structure and connectivity of the district heating grid, showing the positions of junctions, pipes, pumps, and consumers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pp.plotting.simple_plot(net)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pipeflow Calculation\n", + "\n", + "We now run a pipe flow calculation using the bidirectional calculation mode. This mode is especially important for networks with heat exchange, as it enables the simultaneous calculation of hydraulic and thermal behavior." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pp.pipeflow(net, mode='bidirectional', iter=100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default, only the pressure and velocity distribution is calculated by the `pipeflow` function. If the `mode` parameter is set to \"bidirectional\", the heat transfer calculation is started automatically after the hydraulics computation. Computed mass flows are used as input for the temperature calculation." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Results\n", + "\n", + "After the computation, you can check the results for junctions and pipes.\n", + "\n", + "Tip: If you observe unexpected results, such as negative temperatures, check the extracted heat flow and ambient temperature settings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "net.res_junction" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that a constant heat flow is extracted via the heat consumer between nodes 1 and 2. Heat losses due to the ambient temperature are not taken into account in the heat consumer; these are only included in the pipe components. This also means that, if the extracted heat flow is large enough, the temperature level behind the heat consumer might be lower than the ambient temperature. To avoid this behavior, you could create a controller that defines a function for the extracted heat depending on the ambient temperature.\n", + "\n", + "Tip: For larger networks, consider visualizing the results to better understand the temperature and pressure distribution throughout the system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "net.res_pipe" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The command above shows the results for the pipe components. The temperatures of the adjacent\n", + "junctions are displayed. Due to heat losses, the temperatures at the to-nodes is lower than the\n", + "temperatures at the from-nodes. Note also that the junctions are not equal to the internal nodes,\n", + "introduced by the pipe sections we defined. To display the temperatures at the internal nodes, we\n", + "can retrieve the internal node values with the following commands:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pandapipes.component_models import Pipe\n", + "pipe_results = Pipe.get_internal_results(net, [0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The parameters of the get_internal_results function correspond to the net and the pipes we want to\n", + "evaluate. In this case, only the results of pipe zero are retrieved. The returned value stored in\n", + "pipe_results is a dictionary, containing fields for the pressure, the velocity and the temperature.\n", + "The dictionary can either be used for own evaluations now or it can be used to plot the results over\n", + "the pipe length:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Pipe.plot_pipe(net, 0, pipe_results)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that the pressure level falls due to friction. As the fluid is incompressible, the\n", + "velocity remains constant over the pipe length. Because the temperature level at the pipe entry is\n", + "higher than the ambient temperature, the temperature level decreases." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## More Examples\n", + "\n", + "Further examples for closed district heating networks can be found in the notebooks: \n", + "- `multiple_pumps_flow_in_a_circular_district_heating_grid.ipynb` \n", + "- `time_series_in_a_circular_district_heating_grid.ipynb`" + ] + } + ], + "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 +} diff --git a/tutorials/district_heating/multiple_pumps_flow_in_a_circular_district_heating_grid.ipynb b/tutorials/district_heating/multiple_pumps_flow_in_a_circular_district_heating_grid.ipynb new file mode 100644 index 000000000..ad9801c72 --- /dev/null +++ b/tutorials/district_heating/multiple_pumps_flow_in_a_circular_district_heating_grid.ipynb @@ -0,0 +1,369 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Multiple pumps in a circular district heating grid\n", + "\n", + "This example builds upon the simple circular district heating grid described in the notebook 'circular_flow_in_a_district_heating_grid.ipynb'. If you are new to district heating modeling with pandapipes, it is recommended to start with that example to understand the basic concepts and components.\n", + "\n", + "In this notebook, we extend the simple network by introducing multiple pumps (heat generators) and multiple heat consumers. This allows us to analyze more complex operational scenarios, such as the interaction between several pumps and the distribution of heat to multiple consumers.\n", + "\n", + "The network setup, component creation, and analysis steps follow the same structure as in the simple example, but with additional elements to demonstrate the flexibility and scalability of pandapipes for district heating applications.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Network Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandapipes as pp\n", + "import numpy as np\n", + "\n", + "# create empty net\n", + "net = pp.create_empty_network(fluid =\"water\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The network consists of several junctions forming a closed loop, similar to the simple example. However, in this case, additional junctions are included to accommodate multiple pumps and heat consumers. The parameters `pn_bar` (pressure) and `tfluid_k` (temperature) are set as initial values for each junction. The actual values are determined by the pumps during the simulation.\n", + "\n", + "This setup allows for the investigation of how multiple heat generators and consumers interact within a district heating grid." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# define constants\n", + "qext_w = np.array([500000, 200000])\n", + "return_temperature_k = np.array([60,55]) + 273.15\n", + "supply_temperature_k = 85 + 273.15\n", + "\n", + "pipetype = \"110/202 PLUS\"\n", + "k = 0.1\n", + "\n", + "flow_pressure_pump = 4\n", + "lift_pressure_pump = 1.5\n", + "mass_pump_mass_flow = 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Junctions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Junctions for pump\n", + "j1 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Junction 1\", geodata=(0, 1000))\n", + "j2 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_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=supply_temperature_k, name=\"Junction 3\", geodata=(500, 0))\n", + "j4 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Junction 4\", geodata=(1000, 0))\n", + "\n", + "# Junctions for heat consumers\n", + "j5 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Junction 5\", geodata=(1500, 0))\n", + "j6 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Junction 6\", geodata=(1500, 1000))\n", + "\n", + "# Junctions for connection pipes return line\n", + "j7 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Junction 7\", geodata=(1000, 1000))\n", + "j8 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Junction 8\", geodata=(500, 1000))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Circ Pump Const Pressure, Pipes and Heat Consumers\n", + "\n", + "This example includes two types of pumps: a constant pressure pump and a constant mass flow pump. The constant pressure pump operates similarly to the one in the simple example, maintaining a specified pressure lift and flow temperature. The constant mass flow pump, on the other hand, enforces a fixed mass flow through the network segment.\n", + "\n", + "The combination of different pump types allows for the simulation of more advanced operational strategies and the study of their effects on the network's hydraulic and thermal behavior." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pump1 = pp.create_circ_pump_const_pressure(net, j1, j2, p_flow_bar=flow_pressure_pump,\n", + " plift_bar=lift_pressure_pump, t_flow_k=supply_temperature_k,\n", + " type=\"auto\", name=\"pump1\")\n", + "\n", + "pipe1 = pp.create_pipe(net, j2, j3, std_type=pipetype, length_km=0.5, k_mm=k, name=\"pipe1\", sections=5, text_k=283)\n", + "pipe2 = pp.create_pipe(net, j3, j4, std_type=pipetype, length_km=0.5, k_mm=k, name=\"pipe2\", sections=5, text_k=283)\n", + "pipe3 = pp.create_pipe(net, j4, j5, std_type=pipetype, length_km=0.5, k_mm=k, name=\"pipe3\", sections=5, text_k=283)\n", + "\n", + "pp.create_heat_consumer(net, from_junction=j5, to_junction=j6, qext_w=qext_w[0], treturn_k=return_temperature_k[0], name=\"Consumer A\")\n", + "pp.create_heat_consumer(net, from_junction=j4, to_junction=j7, qext_w=qext_w[1], treturn_k=return_temperature_k[1], name=\"Consumer B\")\n", + "\n", + "pipe4 = pp.create_pipe(net, j6, j7, std_type=pipetype, length_km=0.5, k_mm=k, name=\"pipe4\", sections=5, text_k=283)\n", + "pipe5 = pp.create_pipe(net, j7, j8, std_type=pipetype, length_km=0.5, k_mm=k, name=\"pipe5\", sections=5, text_k=283)\n", + "pipe6 = pp.create_pipe(net, j8, j1, std_type=pipetype, length_km=0.5, k_mm=k, name=\"pipe6\", sections=5, text_k=283)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Two heat consumers are included in this network. Each heat consumer extracts a specified amount of heat (`qext_w`) from the network. The return temperature (`treturn_k`) is set for each consumer, and the system calculates the resulting mass flow based on the transferred heat.\n", + "\n", + "This setup enables the analysis of how different consumer demands affect the temperature and pressure distribution in the network." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Circ Pump Const Mass Flow" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "### here comes the part with the additional circ_pump_const_mass_flow ###\n", + "# first of, the junctions\n", + "j9 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Junction 9\", geodata=(2000, 0))\n", + "j10 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Junction 10\", geodata=(2000, 1000))\n", + "j11 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Junction 11\", geodata=(2000, 500))\n", + "\n", + "pipe7 = pp.create_pipe(net, j5, j9, std_type=pipetype, length_km=0.5, k_mm=k, name=\"pipe7\", sections=5, text_k=283)\n", + "pipe8 = pp.create_pipe(net, j10, j6, std_type=pipetype, length_km=0.5, k_mm=k, name=\"pipe8\", sections=5, text_k=283)\n", + "\n", + "pump2 = pp.create_circ_pump_const_mass_flow(net, j10, j11, p_flow_bar=flow_pressure_pump, mdot_flow_kg_per_s=mass_pump_mass_flow, \n", + " t_flow_k=supply_temperature_k, type=\"auto\", name=\"pump2\")\n", + "\n", + "flow_control_pump2 = pp.create_flow_control(net, j11, j9, controlled_mdot_kg_per_s=mass_pump_mass_flow)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The flow control component is used to maintain a specified mass flow at a particular location in the network. This is especially important when using a constant mass flow pump, as it ensures the desired flow rate is achieved and helps stabilize the pressure distribution." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting Net\n", + "\n", + "The following command creates a simple plot of the network layout using `pandapipes.plotting.simple_plot`. \n", + "This visualization helps to understand the structure and connectivity of the district heating grid, showing the positions of junctions, pipes, pumps, and consumers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pp.plotting.simple_plot(net)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pipeflow Calculation\n", + "\n", + "We now run a pipe flow calculation using the bidirectional calculation mode. This mode is essential for networks with multiple heat generators and consumers, as it enables the simultaneous calculation of hydraulic and thermal behavior throughout the entire system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pp.pipeflow(net, mode='bidirectional', iter=100)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note: The `mode` parameter must be set to 'bidirectional' to enable heat transfer calculations. Increasing the number of iterations (e.g., `iter=100`) may be necessary to ensure convergence in more complex networks with multiple pumps and consumers." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "net.res_junction" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that a constant heat flow is extracted via the heat exchangers at the consumer locations. Heat losses due to the ambient temperature are only included in the pipe components, not in the heat exchangers themselves. This means that, if the extracted heat flow is large enough, the temperature level behind a heat exchanger might be lower than the ambient temperature. To avoid this behavior, you could implement a controller that adjusts the extracted heat based on the ambient temperature.\n", + "\n", + "Tip: For larger and more complex networks, visualizing the results can help you better understand the temperature and pressure distribution throughout the system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "net.res_pipe" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The command above shows the results for the pipe components. The temperatures of the adjacent\n", + "junctions are displayed. Due to heat losses, the temperatures at the to-nodes is lower than the\n", + "temperatures at the from-nodes. Note also that the junctions are not equal to the internal nodes,\n", + "introduced by the pipe sections we defined." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Results Plotting\n", + "\n", + "In the following cells, results are visualized using custom colored plots to provide a clear overview of the pipe data in the network. These visualizations help to analyze the distribution of key physical properties temperature, pressure, and velocity along each pipe segment.\n", + "\n", + "The plotting code uses a helper function, `plot_pipe_property`, which draws each pipe as a colored line between its start and end junctions. The color of each pipe is determined by the value of the selected property (e.g., temperature, pressure, or velocity), mapped to a color scale (colormap). This approach makes it easy to visually identify gradients and patterns in the network.\n", + "\n", + "Three subplots are generated side by side:\n", + "\n", + "- **Temperature Plot (left):** Shows the average temperature in each pipe (in °C), using a blue-to-red colormap. This allows you to see how heat is distributed and where the largest temperature drops occur due to heat extraction or losses.\n", + "- **Pressure Plot (center):** Displays the average pressure in each pipe (in bar), using a green-to-yellow colormap. This helps to identify pressure drops along the network and the effect of pumps.\n", + "- **Velocity Plot (right):** Illustrates the mean fluid velocity in each pipe (in m/s), using a purple-to-yellow colormap. This is useful for assessing flow distribution and identifying areas of high or low velocity.\n", + "\n", + "Each subplot includes a colorbar for reference, and the axes are labeled according to the network's spatial coordinates. The plots are generated using the `matplotlib` library, and the data for each property is taken from the `net.res_pipe` results table, which contains the simulation output for all pipes.\n", + "\n", + "This visualization technique is especially helpful for complex networks, as it provides an immediate, intuitive understanding of how the network is operating under the given conditions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, axes = plt.subplots(1, 3, figsize=(22, 7))\n", + "cmap = plt.cm.coolwarm\n", + "\n", + "# Helper to plot a colored pipe property\n", + "def plot_pipe_property(ax, property_name, label, vmin=None, vmax=None, cmap=plt.cm.viridis):\n", + " for idx, pipe in net.pipe.iterrows():\n", + " from_junc = pipe['from_junction']\n", + " to_junc = pipe['to_junction']\n", + " x = [net.junction_geodata.at[from_junc, 'x'], net.junction_geodata.at[to_junc, 'x']]\n", + " y = [net.junction_geodata.at[from_junc, 'y'], net.junction_geodata.at[to_junc, 'y']]\n", + " value = net.res_pipe.at[idx, property_name]\n", + " lc = ax.plot(x, y, color=cmap((value - vmin) / (vmax - vmin)), linewidth=6)\n", + " ax.set_title(label, fontsize=13)\n", + " ax.set_xlabel('x [m]')\n", + " ax.set_ylabel('y [m]')\n", + " ax.set_aspect('equal')\n", + " ax.grid(True, alpha=0.3)\n", + " sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin=vmin, vmax=vmax))\n", + " sm.set_array([])\n", + " plt.colorbar(sm, ax=ax, orientation='vertical', label=label)\n", + "\n", + "# Temperature plot (in °C)\n", + "tmin = net.res_pipe[['t_from_k', 't_to_k']].min().min() - 273.15\n", + "tmax = net.res_pipe[['t_from_k', 't_to_k']].max().max() - 273.15\n", + "net.res_pipe['t_avg_c'] = (net.res_pipe['t_from_k'] + net.res_pipe['t_to_k']) / 2 - 273.15\n", + "plot_pipe_property(axes[0], 't_avg_c', 'Temperature [°C]', vmin=tmin, vmax=tmax, cmap=plt.cm.coolwarm)\n", + "\n", + "# Pressure plot (in bar)\n", + "pmin = net.res_pipe[['p_from_bar', 'p_to_bar']].min().min()\n", + "pmax = net.res_pipe[['p_from_bar', 'p_to_bar']].max().max()\n", + "net.res_pipe['p_avg_bar'] = (net.res_pipe['p_from_bar'] + net.res_pipe['p_to_bar']) / 2\n", + "plot_pipe_property(axes[1], 'p_avg_bar', 'Pressure [bar]', vmin=pmin, vmax=pmax, cmap=plt.cm.viridis)\n", + "\n", + "# Velocity plot (in m/s)\n", + "vmin = net.res_pipe['v_mean_m_per_s'].min()\n", + "vmax = net.res_pipe['v_mean_m_per_s'].max()\n", + "plot_pipe_property(axes[2], 'v_mean_m_per_s', 'Velocity [m/s]', vmin=vmin, vmax=vmax, cmap=plt.cm.plasma)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The three plots above provide a detailed visual summary of the pipe results in the district heating network:\n", + "\n", + "- Temperature Plot (left): The color of each pipe represents its average temperature in degrees Celsius. You can see the temperature drop along the flow direction, especially after heat consumers, where heat is extracted from the network. Pipes with higher temperatures are shown in red, while cooler pipes appear blue.\n", + "\n", + "- Pressure Plot (center): This plot shows the average pressure in each pipe in bar. The pressure decreases along the flow path due to friction losses and heat extraction. The effect of the pumps is visible as a sudden increase in pressure at their locations. The color gradient helps to quickly identify regions of high and low pressure.\n", + "\n", + "- Velocity Plot (right): Here, the color indicates the mean fluid velocity in each pipe (in m/s). Higher velocities are typically found in pipes supplying multiple consumers or near the pumps, while lower velocities occur in return lines or branches with less flow. This plot helps to identify flow distribution and potential bottlenecks in the network.\n", + "\n", + "Together, these visualizations make it easy to interpret the hydraulic and thermal state of the network at a glance, highlighting the impact of pumps and consumers on the system." + ] + } + ], + "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 +} diff --git a/tutorials/district_heating/time_series_in_a_circular_district_heating_grid.ipynb b/tutorials/district_heating/time_series_in_a_circular_district_heating_grid.ipynb new file mode 100644 index 000000000..1c2b0517b --- /dev/null +++ b/tutorials/district_heating/time_series_in_a_circular_district_heating_grid.ipynb @@ -0,0 +1,449 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Time Series in a Circular District Heating Grid\n", + "\n", + "This example demonstrates how to perform a time series simulation for a district heating network using pandapipes. It builds upon the simple circular district heating grid described in the notebook 'circular_flow_in_a_district_heating_grid.ipynb'.\n", + "\n", + "In real-world district heating systems, the heat demand and operating conditions change over time. Time series simulations allow you to analyze the dynamic behavior of the network, such as how temperature, pressure, and flow rates evolve in response to varying consumer demands or external conditions.\n", + "\n", + "In this notebook, you will learn:\n", + "\n", + "- How to set up a district heating network for time series analysis with pandapipes.\n", + "- How to define time-dependent input data (e.g., heat demand profiles).\n", + "- How to run a time series simulation and log relevant results.\n", + "- How to visualize and interpret the results over multiple time steps.\n", + "\n", + "This approach is essential for designing and optimizing district heating systems, ensuring reliable operation under varying load conditions, and evaluating control strategies.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Network Setup\n", + "\n", + "The network used in this example is based on a circular district heating grid with several branches and consumers. The setup includes:\n", + "\n", + "- A main pump that supplies hot water to the network.\n", + "- Multiple junctions representing supply and return points for each consumer branch.\n", + "- Pipes connecting the junctions, forming a closed loop with additional branches for each consumer.\n", + "- Several heat consumers, each with its own heat demand and return temperature profile.\n", + "\n", + "This structure allows for the simulation of realistic operating scenarios, where heat demand and return temperatures can vary over time for each consumer. The network is initialized with water as the working fluid." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandapipes as pp\n", + "import numpy as np\n", + "\n", + "# create empty net\n", + "net = pp.create_empty_network(fluid =\"water\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# define constants\n", + "qext_w = np.array([100000, 80000, 120000])\n", + "return_temperature_k = np.array([60,55,65]) + 273.15\n", + "supply_temperature_k = 85 + 273.15\n", + "\n", + "pipetype = \"110/202 PLUS\"\n", + "k = 0.1\n", + "\n", + "flow_pressure_pump = 4\n", + "lift_pressure_pump = 1.5\n", + "mass_pump_mass_flow = 0.5" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define junctions\n", + "j1 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Pump Supply\", geodata=(0, 0))\n", + "j2 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Main Split Supply\", geodata=(10, 0))\n", + "j12 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Main Split Return\", geodata=(10, 10))\n", + "j13 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Pump Return\", geodata=(0, 10))\n", + "\n", + "# Additional junctions for new branches\n", + "j3 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Consumer B Supply\", geodata=(20, 0))\n", + "j4 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Consumer B Return\", geodata=(20, 10))\n", + "j5 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Consumer C Supply\", geodata=(30, 0))\n", + "j6 = pp.create_junction(net, pn_bar=1.05, tfluid_k=supply_temperature_k, name=\"Consumer C Return\", geodata=(30, 10))\n", + "\n", + "# Pump\n", + "pp.create_circ_pump_const_pressure(net, j13, j1, p_flow_bar=flow_pressure_pump, plift_bar=lift_pressure_pump, \n", + " t_flow_k=supply_temperature_k, type=\"auto\", name=\"Main Pump\")\n", + "\n", + "# Pipes for supply line\n", + "pp.create_pipe(net, j1, j2, std_type=pipetype, length_km=0.2, k_mm=k, name=\"Main Pipe Supply\")\n", + "pp.create_pipe(net, j2, j3, std_type=pipetype, length_km=0.3, k_mm=k, name=\"Branch B Pipe Supply\")\n", + "pp.create_pipe(net, j3, j5, std_type=pipetype, length_km=0.3, k_mm=k, name=\"Branch C Pipe Supply\")\n", + "\n", + "# Pipes for return line\n", + "pp.create_pipe(net, j12, j13, std_type=pipetype, length_km=0.2, k_mm=k, name=\"Main Pipe Return\")\n", + "pp.create_pipe(net, j4, j12, std_type=pipetype, length_km=0.3, k_mm=k, name=\"Branch B Pipe Return\")\n", + "pp.create_pipe(net, j6, j4, std_type=pipetype, length_km=0.3, k_mm=k, name=\"Branch C Pipe Return\")\n", + "\n", + "# Heat consumers\n", + "pp.create_heat_consumer(net, from_junction=j2, to_junction=j12, qext_w=qext_w[0], treturn_k=return_temperature_k[0], name=\"Consumer A\")\n", + "pp.create_heat_consumer(net, from_junction=j3, to_junction=j4, qext_w=qext_w[1], treturn_k=return_temperature_k[1], name=\"Consumer B\")\n", + "pp.create_heat_consumer(net, from_junction=j5, to_junction=j6, qext_w=qext_w[2], treturn_k=return_temperature_k[2], name=\"Consumer C\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting Net" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pp.plotting.simple_plot(net)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pipeflow Calculation\n", + "\n", + "To analyze the dynamic behavior of the network, we perform a pipe flow calculation for each time step. The `pipeflow` function is called in bidirectional mode, which is necessary for networks with heat exchange, as it enables the simultaneous calculation of hydraulic and thermal behavior.\n", + "\n", + "The number of iterations (`iter=100`) and the relaxation parameter (`alpha=0.2`) can be adjusted to ensure convergence, especially for larger or more complex networks. These settings help the solver handle changing conditions across the time series." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pp.pipeflow(net, mode='bidirectional', iter=100, alpha=0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Time Series Simulation\n", + "\n", + "In this section, we set up and run a time series simulation for the district heating network. The simulation considers time-dependent changes in both the supply temperature and the heat demand of each consumer.\n", + "\n", + "- The supply temperature at the pump is varied over time using a predefined temperature profile, representing realistic operational changes (e.g., day/night or seasonal effects).\n", + "- The heat demand for each consumer is defined as a time-dependent profile, allowing for different demand patterns at each time step. This enables the simulation of varying consumer behavior or external influences.\n", + "- These time-dependent values are assigned to the network using `ConstControl` controllers from pandapower.timeseries, which update the relevant parameters (supply temperature and consumer heat extraction) at each time step based on the provided profiles.\n", + "\n", + "The simulation is executed using the `run_time_series` function, which solves the network for each time step and logs the results using an `OutputWriter`. This approach enables the analysis of how the network responds to dynamic operating conditions, such as fluctuating supply temperatures and varying consumer demands.\n", + "\n", + "By combining time-dependent input data with automated controllers, you can analyze the behavior of the district heating grid and evaluate the impact of different control strategies or demand scenarios." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "start = 0\n", + "end = 10 # 8760 hours in a year\n", + "\n", + "# time steps with start and end\n", + "time_steps = np.arange(start, end, 1)\t# time steps in hours\n", + "\n", + "supply_temperature_profile_k = np.array([90, 85, 80, 75, 75, 75, 75, 80, 85, 95]) + 273.15\n", + "\n", + "# Define percentage profiles for each consumer (shape: [n_timesteps, n_consumers])\n", + "heat_demand_percentages = np.array([\n", + " [1.0, 0.8, 1.2], # t=0\n", + " [0.9, 0.7, 1.1], # t=1\n", + " [0.8, 0.6, 1.0], # t=2\n", + " [0.7, 0.5, 0.9], # t=3\n", + " [0.6, 0.4, 0.8], # t=4\n", + " [0.7, 0.5, 0.9], # t=5\n", + " [0.8, 0.6, 1.0], # t=6\n", + " [0.9, 0.7, 1.1], # t=7\n", + " [1.0, 0.8, 1.2], # t=8\n", + " [1.0, 0.8, 1.2], # t=9\n", + "])\n", + "\n", + "# Calculate time-dependent qext for each consumer\n", + "qext_profile = qext_w * heat_demand_percentages # shape: (n_timesteps, n_consumers)\n", + "print(f\"Time dependant heat demand profile for the three consumers: \\n {qext_profile}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data Setup and Controller Definition\n", + "\n", + "To enable time-dependent simulation, we need to provide the network with profiles for all parameters that change over time. In this example, both the supply temperature at the pump and the heat demand for each consumer are defined as time series.\n", + "\n", + "- **Heat demand profiles:** For each consumer, a time-dependent heat extraction profile (`qext_w`) is created. These profiles are stored in a pandas DataFrame and provided to the controllers using `DFData`.\n", + "- **Supply temperature profile:** The supply temperature at the pump is also defined as a time series and provided to the controller in the same way.\n", + "\n", + "The `ConstControl` class from `pandapower.control` is used to assign these profiles to the corresponding network elements. Each controller updates the specified parameter (e.g., `qext_w` for a heat consumer, `t_flow_k` for the pump) at every time step according to the profile.\n", + "\n", + "This setup allows the simulation to automatically adjust the network's operating conditions at each time step, enabling the analysis of dynamic scenarios and the impact of different demand or supply strategies." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from pandapower.timeseries import DFData\n", + "from pandapower.control.controller.const_control import ConstControl\n", + "\n", + "# Create a DataFrame for all qext profiles\n", + "profile_names = [f'qext_profile_{i}' for i in range(qext_profile.shape[1])]\n", + "qext_profile_df = pd.DataFrame(qext_profile, columns=profile_names)\n", + "qext_profile_data = DFData(qext_profile_df)\n", + "\n", + "for i, hc_idx in enumerate(net.heat_consumer.index):\n", + " ConstControl(\n", + " net,\n", + " element='heat_consumer',\n", + " variable='qext_w',\n", + " element_index=hc_idx,\n", + " profile_name=profile_names[i],\n", + " data_source=qext_profile_data\n", + " )\n", + "\n", + "# Add ConstControl for circ pump pressure temperature (t_flow_k)\n", + "supply_temp_profile_df = pd.DataFrame({'supply_temp_profile': supply_temperature_profile_k})\n", + "supply_temp_profile_data = DFData(supply_temp_profile_df)\n", + "\n", + "ConstControl(\n", + " net,\n", + " element='circ_pump_pressure',\n", + " variable='t_flow_k',\n", + " element_index=net.circ_pump_pressure.index[0],\n", + " profile_name='supply_temp_profile',\n", + " data_source=supply_temp_profile_data\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Running the Time Series Simulation\n", + "\n", + "With all time-dependent profiles and controllers set up, we can now execute the time series simulation. The `OutputWriter` is used to log selected results (such as pressures, temperatures, and heat extraction) at each time step for later analysis and visualization.\n", + "\n", + "The `run_time_series` function iterates through all defined time steps, automatically updating the network parameters according to the profiles and controllers. This process simulates the dynamic operation of the district heating grid under varying supply and demand conditions.\n", + "\n", + "After the simulation, the logged results can be extracted and visualized to gain insights into the network's transient behavior and to evaluate the effectiveness of different control strategies or demand scenarios." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pandapipes.timeseries import run_time_series\n", + "from pandapower.timeseries import OutputWriter\n", + "\n", + "log_variables = [\n", + " ('res_junction', 'p_bar'),\n", + " ('res_junction', 't_k'),\n", + " ('heat_consumer', 'qext_w'),\n", + " ('res_heat_consumer', 'vdot_m3_per_s'),\n", + " ('res_heat_consumer', 't_from_k'),\n", + " ('res_heat_consumer', 't_to_k'),\n", + " ('res_heat_consumer', 'mdot_from_kg_per_s'),\n", + " ('res_circ_pump_pressure', 'mdot_from_kg_per_s'),\n", + " ('res_circ_pump_pressure', 'p_to_bar'),\n", + " ('res_circ_pump_pressure', 'p_from_bar'),\n", + " ('res_circ_pump_pressure', 't_to_k'),\n", + " ('res_circ_pump_pressure', 't_from_k')\n", + " ]\n", + "\n", + "ow = OutputWriter(net, time_steps, output_path=None, log_variables=log_variables)\n", + "\n", + "run_time_series.run_timeseries(net, time_steps, mode=\"bidirectional\", iter=100, alpha=0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Results Extraction\n", + "\n", + "After running the time series simulation, the results are stored in the `OutputWriter` object. These results include time-dependent values for pressures, temperatures, mass flows, and heat extraction at various points in the network.\n", + "\n", + "You can extract and analyze these results to understand how the network responds to changing supply and demand conditions. For example, you might examine the supply temperature at the pump, the mass flow through the main pump, or the heat extraction at each consumer over time.\n", + "\n", + "The following code demonstrates how to access and print selected results from the simulation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "ruby" + } + }, + "outputs": [], + "source": [ + "# Extract time series results from OutputWriter\n", + "results = ow.output\n", + "\n", + "# Supply temperature at \"Pump Supply\"\n", + "pump_supply_idx = net.junction[net.junction['name'] == \"Pump Supply\"].index[0]\n", + "supply_temp = results['res_junction.t_k'][pump_supply_idx]\n", + "\n", + "# Mass flow at main pump (first and only pump)\n", + "mass_flow_pump = results['res_circ_pump_pressure.mdot_from_kg_per_s'][pump_supply_idx]\n", + "\n", + "print(\"Supply temperature at 'Pump Supply' over time:\")\n", + "print(supply_temp)\n", + "print(\"\\nMass flow at main pump over time:\")\n", + "print(mass_flow_pump)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualization of Time Series Results\n", + "\n", + "To better understand the dynamic behavior of the district heating network, we visualize key results from the time series simulation. The following plots show:\n", + "\n", + "- Temperatures at important junctions (e.g., pump supply, pump return, and consumer returns) over time.\n", + "- Pressures at selected nodes to observe hydraulic changes during the simulation.\n", + "- Heat extraction (`qext`) for each consumer, illustrating how demand profiles affect network operation.\n", + "\n", + "These visualizations help identify trends, validate the simulation, and provide insights into the performance and stability of the network under varying conditions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "# Load results from OutputWriter\n", + "results = ow.output\n", + "\n", + "# The time axis corresponds to the DataFrame index (time steps)\n", + "time_steps = results['res_junction.t_k'].index\n", + "\n", + "# --- Temperatures at selected nodes (e.g. Pump Supply, Consumer Returns) ---\n", + "pump_supply_idx = net.junction[net.junction['name'] == \"Pump Supply\"].index[0]\n", + "pump_return_idx = net.junction[net.junction['name'] == \"Pump Return\"].index[0]\n", + "consumer_return_idxs = [\n", + " net.junction[net.junction['name'] == \"Main Split Return\"].index[0],\n", + " net.junction[net.junction['name'] == \"Consumer B Return\"].index[0],\n", + " net.junction[net.junction['name'] == \"Consumer C Return\"].index[0],\n", + "]\n", + "\n", + "plt.figure(figsize=(10,5))\n", + "plt.plot(time_steps, results['res_junction.t_k'][pump_supply_idx] - 273.15, label='Pump Supply')\n", + "plt.plot(time_steps, results['res_junction.t_k'][pump_return_idx] - 273.15, label='Pump Return')\n", + "for i, idx in enumerate(consumer_return_idxs):\n", + " plt.plot(time_steps, results['res_junction.t_k'][idx] - 273.15, label=f'Consumer {chr(65+i)} Return')\n", + "plt.xlabel('Time step')\n", + "plt.ylabel('Temperature [°C]')\n", + "plt.title('Temperatures at Key Junctions Over Time')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# --- Pressures at selected nodes (e.g. Pump Supply, Consumer Returns) ---\n", + "plt.figure(figsize=(10,5))\n", + "plt.plot(time_steps, results['res_junction.p_bar'][pump_supply_idx], label='Pump Supply')\n", + "for i, idx in enumerate(consumer_return_idxs):\n", + " plt.plot(time_steps, results['res_junction.p_bar'][idx], label=f'Consumer {chr(65+i)} Return')\n", + "plt.xlabel('Time step')\n", + "plt.ylabel('Pressure [bar]')\n", + "plt.title('Pressures at Key Junctions Over Time')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# --- qext (Heat Extraction) for all Consumers ---\n", + "qext_df = results['heat_consumer.qext_w']\n", + "plt.figure(figsize=(10,5))\n", + "for i, col in enumerate(qext_df.columns):\n", + " plt.plot(qext_df.index, qext_df[col], label=f'Consumer {chr(65+i)}')\n", + "plt.xlabel('Time step')\n", + "plt.ylabel('qext [W]')\n", + "plt.title('Heat Extraction (qext) per Consumer Over Time')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Summary and Interpretation\n", + "\n", + "The time series simulation and visualizations provide valuable insights into the transient behavior of the district heating grid. By analyzing the results, you can:\n", + "\n", + "- Observe how supply temperature and consumer demands influence network temperatures, pressures, and flows over time.\n", + "- Identify periods of high or low demand and assess the network's ability to maintain stable operation.\n", + "- Evaluate the effectiveness of control strategies and the impact of different demand scenarios.\n", + "\n", + "These analyses are essential for optimizing district heating systems, improving energy efficiency, and ensuring reliable service under real-world operating conditions. You can further extend this approach by implementing advanced controllers, testing alternative demand profiles, or integrating additional components into the network model." + ] + } + ], + "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 +} diff --git a/tutorials/pics/district_heating/multiple_pumps_district_heating_net_raw.png b/tutorials/pics/district_heating/multiple_pumps_district_heating_net_raw.png new file mode 100644 index 000000000..d512a539c Binary files /dev/null and b/tutorials/pics/district_heating/multiple_pumps_district_heating_net_raw.png differ diff --git a/tutorials/pics/district_heating/multiple_pumps_district_heating_net_text.png b/tutorials/pics/district_heating/multiple_pumps_district_heating_net_text.png new file mode 100644 index 000000000..3ed1117d1 Binary files /dev/null and b/tutorials/pics/district_heating/multiple_pumps_district_heating_net_text.png differ diff --git a/tutorials/pics/district_heating/simple_district_heating_net_raw.png b/tutorials/pics/district_heating/simple_district_heating_net_raw.png new file mode 100644 index 000000000..80223e1cc Binary files /dev/null and b/tutorials/pics/district_heating/simple_district_heating_net_raw.png differ diff --git a/tutorials/pics/district_heating/simple_district_heating_net_text.png b/tutorials/pics/district_heating/simple_district_heating_net_text.png new file mode 100644 index 000000000..503b0c313 Binary files /dev/null and b/tutorials/pics/district_heating/simple_district_heating_net_text.png differ diff --git a/tutorials/pics/district_heating/time_series_district_heating_net_raw.png b/tutorials/pics/district_heating/time_series_district_heating_net_raw.png new file mode 100644 index 000000000..6914cdfd6 Binary files /dev/null and b/tutorials/pics/district_heating/time_series_district_heating_net_raw.png differ diff --git a/tutorials/pics/district_heating/time_series_district_heating_net_text.png b/tutorials/pics/district_heating/time_series_district_heating_net_text.png new file mode 100644 index 000000000..e59236b6b Binary files /dev/null and b/tutorials/pics/district_heating/time_series_district_heating_net_text.png differ