District Heating Network#

Topology of the district heating network

Figure: Topology of the district heating network#

Topology of the district heating network

Figure: Topology of the district heating network#

The model used in this example is shown the figure. It consists of a central heating plant and a consumer, represented by a heat exchanger with a control valve. A much more complex district heating system is included in the advanced tutorials section.

Download the full script here: district_heating.py

Setting up the System#

For this model we have to import the Network and Connection classes as well as the respective components. After setting up the network we can create the components, connect them to the network (as shown in the other) examples. As a fluid, we will use the incompressibles back end of CoolProp, since we only need liquid water. The incompressible back end has much higher access speed while preserving high accuracy.

Tip

For more information on the fluid properties in TESPy, check out this page.

Click to expand to code section
from tespy.components.basics.cycle_closer import CycleCloser
from tespy.networks import Network
from tespy.components import (
    CycleCloser, Pipe, Pump, Valve, SimpleHeatExchanger
)
from tespy.connections import Connection

nw = Network()
nw.set_attr(T_unit='C', p_unit='bar', h_unit='kJ / kg')

# central heating plant
hs = SimpleHeatExchanger('heat source')
cc = CycleCloser('cycle closer')
pu = Pump('feed pump')

# consumer
cons = SimpleHeatExchanger('consumer')
val = Valve('control valve')

# pipes
pipe_feed = Pipe('feed pipe')
pipe_return = Pipe('return pipe')

# connections
c0 = Connection(cc, "out1", hs, "in1", label="0")
c1 = Connection(hs, "out1", pu, "in1", label="1")
c2 = Connection(pu, "out1", pipe_feed, "in1", label="2")
c3 = Connection(pipe_feed, "out1", cons, "in1", label="3")
c4 = Connection(cons, "out1", val, "in1", label="4")
c5 = Connection(val, "out1", pipe_return, "in1", label="5")
c6 = Connection(pipe_return, "out1", cc, "in1", label="6")
nw.add_conns(c0, c1, c2, c3, c4, c5, c6)

In the first step, we assume we have a specific heat demand of the consumer and constant pressure and thermal losses in the pipes. Furthermore, the pump produces a constant pressure at the feed part of the system. With the control valve in place the pressure of the return part of the system is then decoupled from that value. Therefore, we need to set a pressure value at the sink as well, which should be equal to the pressure at the pump’s inlet. The pressure drop in the valve will then be the residual pressure drop between the feed and the return part of the system. Lastly, we fix the feed flow and the return flow (at connection 4) temperature values.

cons.set_attr(Q=-10000, pr=0.98)
hs.set_attr(pr=1)
pu.set_attr(eta_s=0.75)
pipe_feed.set_attr(Q=-250, pr=0.98)
pipe_return.set_attr(Q=-200, pr=0.98)

c1.set_attr(T=90, p=10, fluid={'INCOMP::Water': 1})
c2.set_attr(p=13)
c4.set_attr(T=65)

nw.solve(mode="design")
nw.print_results()

Design Pipe Dimensions#

In the second step we will design the pipe’s dimensions. There are two tasks for this:

  • Calculate the necessary pipe diameter given a target pressure loss as well as length and pipe roughness.

  • Calculate the necessary insulation of the pipe based on assumptions regarding the heat loss at a given ambient temperature value.

For the first step, we set lengths and roughness of the pipe and the diameter to "var", indicating the diameter of the pipe should be a variable value in the calculation.

pipe_feed.set_attr(
    ks=0.0005,  # pipe's roughness in meters
    L=100,  # length in m
    D="var",  # diameter in m
)
pipe_return.set_attr(
    ks=0.0005,  # pipe's roughness in meters
    L=100,  # length in m
    D="var",  # diameter in m
)
nw.solve(mode="design")
nw.print_results()

In the second step we can fix the diameter to its resulting value and therefore unset the desired pressure loss first. Then, we set the ambient temperature of the pipes (we assume the temperature of the ambient is not affected by the heat loss of the pipe). With the given heat loss, the kA value can be calculated. It is the area independent heat transfer coefficient.

pipe_feed.set_attr(D=pipe_feed.D.val, pr=None)
pipe_return.set_attr(D=pipe_return.D.val, pr=None)
pipe_feed.set_attr(
    Tamb=0,  # ambient temperature level in network's temperature unit
    kA="var"  # area independent heat transfer coefficient
)
pipe_return.set_attr(
    Tamb=0,  # ambient temperature level in network's temperature unit
    kA="var"  # area independent heat transfer coefficient
)
nw.solve(mode="design")
nw.print_results()

Note

In the results you can see, that the pipes’ pressure losses are still at the desired value after remove the pressure ration specification and using the calculated value of the diameter instead.

Changing Operation Conditions#

Next, we want to investigate what happens, in case the

  • ambient temperature changes.

  • heat load varies.

  • overall temperature level in the heating system is reduced.

To do that, we will use similar setups as show in the Rankine cycle introduction. The KA value of both pipes is assumed to be fixed, the efficiency of the pump and pressure losses in consumer and heat source are constant as well.

Click to expand to code section
nw.set_attr(iterinfo=False)
pipe_feed.set_attr(Tamb=0, kA=pipe_feed.kA.val, Q=None)
pipe_return.set_attr(Tamb=0, kA=pipe_return.kA.val, Q=None)

import matplotlib.pyplot as plt
import numpy as np

# make text reasonably sized
plt.rc('font', **{'size': 18})

data = {
    'T_ambient': np.linspace(-10, 20, 7),
    'heat_load': np.linspace(3, 12, 10),
    'T_level': np.linspace(90, 60, 7)
}
eta = {
    'T_ambient': [],
    'heat_load': [],
    'T_level': []
}
heat_loss = {
    'T_ambient': [],
    'heat_load': [],
    'T_level': []
}

for T in data['T_ambient']:
    pipe_feed.set_attr(Tamb=T)
    pipe_return.set_attr(Tamb=T)
    nw.solve('design')
    eta['T_ambient'] += [abs(cons.Q.val) / hs.Q.val * 100]
    heat_loss['T_ambient'] += [abs(pipe_feed.Q.val + pipe_return.Q.val)]

# reset to base temperature
pipe_feed.set_attr(Tamb=0)
pipe_return.set_attr(Tamb=0)

for Q in data['heat_load']:
    cons.set_attr(Q=-1e3 * Q)
    nw.solve('design')
    eta['heat_load'] += [abs(cons.Q.val) / hs.Q.val * 100]
    heat_loss['heat_load'] += [abs(pipe_feed.Q.val + pipe_return.Q.val)]

# reset to base temperature
cons.set_attr(Q=-10e3)

for T in data['T_level']:
    c1.set_attr(T=T)
    c4.set_attr(T=T - 20)  # return flow temperature assumed 20 °C lower than feed
    nw.solve('design')
    eta['T_level'] += [abs(cons.Q.val) / hs.Q.val * 100]
    heat_loss['T_level'] += [abs(pipe_feed.Q.val + pipe_return.Q.val)]

fig, ax = plt.subplots(2, 3, figsize=(16, 8), sharex='col', sharey='row')

ax = ax.flatten()
[a.grid() for a in ax]

i = 0
for key in data:
    ax[i].scatter(data[key], eta[key], s=100, color="#1f567d")
    ax[i + 3].scatter(data[key], heat_loss[key], s=100, color="#18a999")
    i += 1

ax[0].set_ylabel('Efficiency in %')
ax[3].set_ylabel('Heat losses in W')
ax[3].set_xlabel('Ambient temperature in °C')
ax[4].set_xlabel('Consumer heat load in kW')
ax[5].set_xlabel('District heating temperature level in °C')
plt.tight_layout()
fig.savefig('district_heating_partload.svg')
plt.close()
Performance of the district heating system at changing operating conditions

Figure: Performance of the district heating system at changing operating conditions.#

Performance of the district heating system at changing operating conditions

Figure: Performance of the district heating system at changing operating conditions.#

Note

The efficiency value is defined as ratio of the heat delivered to the consumer to the heat production in the central heating plant.

\[\eta = \frac{\dot{Q}_\text{consumer}}{\dot{Q}_\text{production}}\]