Optimisation models

Generally, this library should allow the optimisation of district heating grids with various configurations settings and different approaches. The optimisation methods of this library are tools to assist the planning process of DHS projects and to analyze the economic feasibility of DHS for a given district, community or city - either by focusing on the DHS itself, or by also considering the overall energy system of a district, which could not just be the heating sector, but also the electricity, mobility sector or the gas infrastructure.

At the moment, there is one approach using oemof-solph as linear optimisation library implemented. This approach is explained in the following sections. It totally makes sense to have some experiences with oemof-solph to understand this toolbox more easily.

Scope

The following questions can be addressed using the optimize_investment method of the ThermalNetwork:

  • What is the cost-optimal topology and dimensioning of a DHS piping system, given the locations of potential central heat supply plants, the potential locations for the DHS piping system (e.g. street network), and the position of consumers?

  • In addition to the first question, what is the cost-optimal expansion of a given DHS system?

  • Is it cost-efficient to build a DHS at all, if there a consumer-wise heat supply alternatives? (Comparison of central and de-central supply strategies)

  • What is the optimal dispatch of the heat producers? (In case there are no expansion options, but just existing DHS pipes)

  • Planned: Streets-wise aggregation option

To answer these questions, at the moment, the LP and MILP optimisation library oemof.solph is used. Other approaches, e.g. heuristic approaches, might follow.

The following sections will give an overview about the general usage/workflow, (the necessary input data, the different optimisation settings and options, the results), and second, the underlying mathematical description.

Usage

Links to the subsections:

Overview

The optimisation of a given ThermalNetwork is executed by:

import dhnx

tnw = dhnx.network.ThermalNetwork()

tnw = network.from_csv_folder('path/to/thermal_network')

invest_opt = dhnx.input_output.load_invest_options('path/to/invest_options')

tnw.optimize_investment(invest_options=invest_opt)

For executing an optimisation, you must provide investment options additional to the previous data, which defines a ThermalNetwork. Both are explained in the following section.

Input Data

In this section, it is firstly revised, what input data is exactly necessary from the ThemalNetwork class, and then explained, what data needs to be provided as investment options, and what optimisation settings you can apply.

The following figure provides an overview of the input data:

optimization_input_data.svg

Fig. 1: Optimisation Input Data

The structure of the input data might look a bit confusing at the beginning, but provides a lot of options for building up complex district heating models. There are two groups of data: Firstly, data that describes the components and the connectivity of the network, required by the ThermalNetwork class. Secondly, data that is necessary for the investment optimization. For now, all data needs to be provided in csv files. This means that you do not need to provide a geo-reference for applying an district heating network optimisation model at all. Probably, in many cases, it is the export of four geo-referenced layers (e.g. geopandasdataframe, shp-file, or any other), which are a line layer representing the potential places for the DHS-trenches, and three point layers for the producers, the consumers, and the potential forks of the DHS system. All geometry information of the network system is passed by an id for each element. Thus, the line layer connects all points and provides the spatial relation with the attributes from_node, to_node, and length. If you prepare the data, be careful that every consumer is connected to an pipe, and every piping network system is connected to at least one producer.

ThermalNetwork

The data for the ThermalNetwork must be provided in the structure as defined for the .csv reader. The following data is required for applying an optimisation:

tree
├── pipes.csv                       # (required)
├── consumers.csv                   # (required)
├── forks.csv                       # (required)
├── producers.csv                   # (required)
└── sequences                       # (optional)
    └── consumers-heat_flow.csv

The attributes, which are required, and which are optional with respect to the optimisation, are presented in detail in the following:

Pipes

The basis for the district heating system optimisation is a table of potential pipes. The following attributes of the ThermalNetwork must be given:

The following attributes are additional attributes of the optimisation module. These attributes are optional for the optimisation:

attribute

type

unit

default

description

status

requirement

existing

bool

n/a

0

Binary indicating and existing pipe

Input

optional

capacity

float

kW

0

Capacity for existing pipe

Input

optional

hp_type

object

n/a

‘nan’

Type_label of existing pipe

Input

optional

active

bool

n/a

1

Binary indicating that edge is available

Input

optional

add_fix_costs

float

Eur/m

0

Additional fix investment costs

Input

optional

  • existing: Binary indicating an existing pipe. If there is no column existing given, all Pipes are free for optimisation.

  • capacity: Capacity of existing pipes. If existing is True, a capacity must be given.

  • hp_type: Label of the type of pipe. The hp_type refers to a set of parameters of a pipeline component. The parameters for the hp_type must be given in the following table (see network/pipes.csv). If existing is True, a hp_type must be given.

  • active: Binary indicating that this pipe is considered. If no column active is given, all pipe-options are active. With this attribute, single pipes can be switched on and off. This can be very useful, if different scenarios should be analyzed, e.g. you might like to make a given street/pipes unavailable.

Consumers

The following attributes of the ThermalNetwork must be given:

The following attributes are additional attributes of the optimisation module, and optional:

attribute

type

unit

default

description

status

requirement

active

bool

n/a

1

Binary indicating that consumer is active

Input

optional

P_heat_max

float

kW

n/a

Maximum heat load of consumer

Input

optional

  • active: Binary indicating that consumer-xy is considered. If no column active is given, all consumers are active. With this attribute, single consumers can be switched on and off (e.g. for scenario analysis with different connection quotes).

  • P_heat_max: Maximum heat load of consumer. If no column P_heat_max is given, the maximum heat load is calculated from the heat demand series (see consumers-heat_flow.csv). Depending on the optimisation setting, P_heat_max or the demand series is used for the optimisation (see Optimisation settings for further information).

Producers

The following attributes of the ThermalNetwork must be given:

The following attributes are additional attributes of the optimisation module, and optional:

attribute

type

unit

default

description

status

requirement

active

bool

n/a

1

Binary indicating that producer is active

Input

optional

  • active: Binary indicating that producer is active. If no column active is given, all producers are active. With this attribute, single producers can be switched on and off (e.g. for scenario analysis for different supply plant positions.

Forks

The following attributes of the ThermalNetwork must be given:

For Forks, no additional required or optional attributes are needed by the optimisation module.

Consumers-heat_flow

Providing consumers heat flow time series is optional, but either the consumers demand must be given in form of P_heat_max as attribute of the consumers, or in form of a heat_flow time series with the minimum length of 1.

The following table shows an example of a consumers-heat_flow:

timestep

0

1

0

8

12

1

10

10

2

9

7

The column index must be the consumers id (And be careful that the dtype also matches the id of the consumers!).

Investment and additional options

If you want to do an investment or an simple unit commitment optimisation using the optimize_investment() method of the ThermalNetwork, you need to provide some additional data providing the investment parameter. The following sheme illustrates the structure of the investment input data:

tree
├── network
|   └── pipes.csv           # (required)
|
├── consumers
|   ├── bus.csv             # (required)
|   ├── demand.csv          # (required)
|   ├── source.csv          # (optional)
|   ├── storages.csv        # (optional)
|   └── transformer.csv     # (optional)
|
└── producers
    ├── bus.csv             # (required)
    ├── demand.csv          # (optional)
    ├── source.csv          # (required)
    ├── storages.csv        # (optional)
    └── transformer.csv     # (optional)

The investment input data provides mainly all remaining parameters of the oemof solph components, which are not specific for a single pipe, producer or consumer.

The minimum of required data is a specification of the pipe parameters (costs, and losses), a (heat) bus and a heat demand at the consumers, and a (heat) bus and a heat source at the producers. The detailed attributes are described in the following sections.

network/pipes.csv

You need to provide data on the investment options for the piping system. The following table shows the minimal required data you need to provide:

label_3

active

nonconvex

l_factor

l_factor_fix

cap_max

cap_min

capex_pipes

fix_costs

pipe-typ-A

1

0

0

0

100000

0

0.5

0

Each row represents an investment option. Note this investment option creates an oemof-solph Heatpipeline component for each active pipe. The units are given es examples. There are no units implemented, everybody needs to care about consistent units in his own model. At the same time, everybody is free to choose his own units (energy, mass flow, etc.).

  • label_3: Label of the third tag. See Label system.

  • active: (0/1). If active is 0, this heatpipeline component is not considered. This attribute helps for easy selecting and deselecting different investment options.

  • nonconvex: (0/1). Choose whether a convex or a nonconvex investment should be performed. With nonconvex set to 1, fix losses and fix costs independent of the dimension of the pipelines capacity can be considered. It is recommended to set nonconvex to 1, as the construction of DHS pipelines is usually characterized by a high share of fixed costs. If nonconvex is 0, the costs-curve is a line through origin.

  • l_factor: Relative thermal loss per length unit (e.g. [kW_loss/(m*kW_installed)]. Defines the loss factor depending on the installed heat transport capacity of the pipe. The l_factor is multiplied by the invested capacity in investment case, and by the given capacity for a specific pipe in case of existing DHS pipes.

  • l_factor_fix: Absolute thermal loss per length unit (e.g. [kW/m]). In case of nonconvex is 1, the l_factor_fix is zero if no investement in a specific pipe element is done. Be careful, if nonconvex is 0, this creates a fixed thermal loss. Recommended to use with nonconvex is True.

  • cap_max: Maximum installable capacity (e.g. [kW]).

  • cap_min: Minimum installable capacity (e.g. [kW]). Note that there is a difference if a nonconvex investment is applied (see oemof-solph documentation for further information).

  • capex_pipes: Variable investment costs depending on the installed heat transport capacity (e.g. [€/kW]).

  • fix_costs: Fix investment costs independent of the installed capacity (e.g. [€]) This attribute requires nonconvex is set to True.

See the Heatpipeline API for further details about the attributes.

The module dhnx.optimization.precalc_hydraulic provides helpful functions for the calculation of the pipeline parameters (see dhnx.optimization.precalc_hydraulic). See also the precalulation example in the example folder of the optimisation examples.

consumers/.

All data for initialising oemof-solph components at the consumers are provided by the .csv files of the consumers folder. For a principal understanding, check out the excel reader example of oemof-solph, which works the same way: oemof-solph excel reader example.

The minimum requirement for doing an DHS optimisation is to provide an demand at the consumers. Therefore, you need the following two .csv files: bus.csv specifies the oemof-solph Bus components, and demand.csv defines the oemof.solph.Sink.

Example for table of Buses

label_2

active

excess

shortage

shortage costs

excess costs

heat

1

0

0

99999

99999

You must provide at least one bus, which has a label (label_2, see Label system), and needs to be active. Optionally, you can add an excess or a shortage with shortage costs or excess costs respectively. This might help to get an feasible optimisation problem, in case your solver says, ‘infeasible’, for finding the error.

demand.csv

label_2

active

nominal_value

heat

1

1

The demand also needs to have a label (label_2, see Label system), has the option for deactivating certain demands by using the attribute active, and needs to have a specification for the nominal_value. The nominal_value scales your demand.

producers/.

The producers look quite similar as the consumers. The consumers are taking energy from the DHS system. That means, the energy need to be supplied somewhere, which makes some kind of source necessary. To connect a source in the oemof logic, there needs to be a oemof.solph.Bus to which the source is connected. The two files bus.csv and source.csv need to be provided:

Example for table of Buses

label_2

active

excess

shortage

shortage costs

excess costs

heat

1

0

0

99999

99999

The bus.csv table works analog to the consumers (see consumers/.).

source.csv

label_2

active

heat

1

You need to provide at least one source at the source.csv table. Additionally, there are already a couple of options for adding additional attributes of the oemof.solph.FLow to the source, e.g. variable_costs, fix feed-in series, and min and max restrictions.

Generally, with this structure at every producer and consumer multiple oemof components, like transformer and storages can be already added.

Optimisation settings

The following table shows all options for the optimisation settings (See also setup_optimise_investment()):

attribute

type

default

description

heat_demand

str

‘scalar’

‘scalar’ or ‘series’. ‘scalar’: Peak heat load. ‘series’: time-series is used as heat demand.

simultaneity

float

1

Simultaneity or concurrency factor

num_ts

int

1

Number of time steps of optimisation

time_res

float

1

Time resolution

start_date

str

‘1/1/2018’

Startdate for oemof optimisation

frequence

str

‘H’

Lenght of period

solver

str

‘cbc’

Name of solver

solve_kw

dict

{‘tee’: True}

Solver kwargs

bidirectional_pipes

bool

False

Bidirectional pipes leads to bi-directional flow attributes at the heatpipeline components {‘min’: -1, bidirectional: True}

dump_path

str

None

If a dump path is provided, the oemof dump file is stored.

dump_name

str

dump.oemof

Name of dump file

print_logging_info

bool

False

There are still some helpful print statements.

write_lp_file

bool

False

Option of writing lp-file. The lp-file is stored in ‘User/.oemof/lp_files/DHNx.lp’

Some more explanation:

  • heat_demand: If you set heat_demand to ‘scalar’, num_ts is automatically 1, and the peak heat load is used as heat demand for the consumers. If you want to use a time series as heat demand, apply ‘series’.

Label systematic

In order to access the oemof-solph optimisation results, a label systematic containing a tuple with 4 items is used. Please check the basic example of oemof-solph for using tuple as label (oemof-solph example tuple as label).

The following table illustrates the systematic:

Labelling system (bold: obligatory; italic: examples)

tag1: general classification

tag2: commodity

tag3: specification / oemof object

tag4: Specific id

consumers

heat

source

forks-34

producers

electricity

demand

consumers-15

infrastructure

gas

excess

prdocuers-4

hydrogen

shortage

forks-14-forks-27

pipe-typ-A

forks-24-consumers-122

storage_xy

boiler_typ_xy

The labels are partly given automatically by the oemof-solph model builder:

  • tag1: general classification: This tag is given automatically depending on the spatial belonging. Tag1 can be either consumers (consumer point layer), producers (producer point layer) or infrastructure (pipes and forks layer). See Thermal Network.

  • tag2: commodity: This tag specifies the commodity, e.g. all buses and transformer (heatpipelines) of the DHS pipeline system have automatically the heat as tag2. For a transformer of the consumers or the producers the tag2 is None, because a transformer usually connects two commodities, e.g. gas –> heat.

  • tag3: specification / oemof object: The third tag indicates either the oemof object and is generated automatically (this is the case for demand.csv, source.csv and bus.csv), or is the specific label_3 of the pipes.csv, transformer.csv or storages.csv.

  • tag4: id: The last tag shows the specific spatial position and is generated automatically.

Results

For checking and analysing the results you can either select to write the investment results of the heatpipeline components in the Thermalnetwork. You will find the results there:

# pipe-specific investment results
results = network.results.optimization['components']['pipes']

The following tables provides an overview of the results table:

attribute

type

unit

description

status

id

object

n/a

Unique id (see pipes of network)

Input

from_node

object

n/a

Node where Edge begins (see pipes of network)

Input

to_node

object

n/a

Node where Edge ends (see pipes of network)

Input

length

float

m

Length of pipe (see pipes of network)

Input

hp_type

object

n/a

Label of pipe which got selected from network/pipes.csv

Result

capacity

float

kW

Installed pipe capacity

Result

direction

float

-1/0/1

Flow direction of pipe: 1 if direction corresponds to the from_node/to_node notation. -1: opposite direction. 0: no investment. This works only if the setting option bidirectional_pipes is set False.

Result

costs

float

Eur

Total cost of pipe element.

Result

losses

float

kW

Total losses of pipe element.

Result

You can also check out the detailed results of the oemof model, which are stored at:

# oemof-solph results "main"
r_oemof_main = network.results.optimization['oemof']

# oemof-solph results "meta"
r_oemof_meta = network.results.optimization['oemof_meta']

Or you can also dump the oemof results and analyze the results as described in oemof-solph handling results. The labelling systematic will help you to easily get want you want, check Label system.

Introducing example

The following sections illustrates some features of the DHNx investment optimisation library.

You can execute and reproduce the example with all figures, check the introduction_example.

import matplotlib.pyplot as plt
import dhnx


# Initialize thermal network
network = dhnx.network.ThermalNetwork()
network = network.from_csv_folder('twn_data')

# Load investment parameter
invest_opt = dhnx.input_output.load_invest_options('invest_data')

# plot network
static_map = dhnx.plotting.StaticMap(network)
static_map.draw(background_map=False)
plt.title('Given network')
plt.scatter(network.components.consumers['lon'], network.components.consumers['lat'],
            color='tab:green', label='consumers', zorder=2.5, s=50)
plt.scatter(network.components.producers['lon'], network.components.producers['lat'],
            color='tab:red', label='producers', zorder=2.5, s=50)
plt.scatter(network.components.forks['lon'], network.components.forks['lat'],
            color='tab:grey', label='forks', zorder=2.5, s=50)
plt.text(-2, 32, 'P0', fontsize=14)
plt.text(82, 0, 'P1', fontsize=14)
plt.legend()
plt.show()

The following figure shows the initial status of an (thermal) network, which is examined in the following sections:

intro_opti_network.svg

Fig. 2: Introduction example

The network of Fig. 2 consists of two options for the heat producers (“P0” and “P1”), eight consumers, and 11 forks. Before running the whole script, we will have a brief look at some input data. Let’s start with the consumers.csv (“twn_data/consumers.csv”):

consumers.csv

id

lat

lon

P_heat_max

0

30

40

15

1

10

40

18

2

10

60

25

3

30

70

36

4

50

60

25

5

90

40

12

6

60

10

50

7

60

30

20

A peak heating load P_heat_max is given for every consumer within the thermal network input data (see Thermal Network Input). The heat load needs to be pre-calculated, or assumed. The geographical attributes lat and lon are optional, but needed for plotting purpose. The next table shows the input data of the heat pipeline elements (“invest_data/network/pipes.csv”):

pipes.csv

label_3

active

nonconvex

l_factor

l_factor_fix

cap_max

cap_min

capex_pipes

fix_costs

pipe-typ-A

1

0

0.00001

0

100000

0

2

0

In the simplest (and most approximate) case, a linear correlation between the thermal capacity and the investment costs can be used. In this example, we assume costs of 2 € per kilowatt installed thermal capacity and meter trench length. As maximum capacity cap_max, we take a very high value to make sure that the total heat load of all consumers (including losses) can be supplied. Additionally, we assume a heat loss of 0.00001 kW/m. The parameters of the district heating pipes need to be pre-calculated depending on the piping system and technical data sheet of the manufacturer. (In future, some pre-calculation function might be added.) The length of each pipe, the costs and the losses are related to, must be given in the pipes.csv table of the Thermal Network Input). Next, we optimise the network and get the results:

network.optimize_investment(invest_options=invest_opt)

# get results
results_pipes = network.results.optimization['components']['pipes']
print(results_pipes[['from_node', 'to_node', 'hp_type', 'capacity', 'heat_loss[kW]',
                     'invest_costs[€]']])

Since we do not have any other costs than investment costs, we can check if our results have been correctly processed by comparing the objective of the optimisation problem with the sum of the investment costs of the single pipes, which should be the same:

# sum of the investment costs of all pipes
print(results_pipes[['invest_costs[€]']].sum())

# objective value of optimisation problem
print(network.results.optimization['oemof_meta']['objective'])

Next, we can transfer the results to a ThermalNetwork, which contains only the pipes with an investment (to avoid possible numerical inaccuracy, the criterion is > 0.001):

# assign new ThermalNetwork with invested pipes
twn_results = network
twn_results.components['pipes'] = results_pipes[results_pipes['capacity'] > 0.001]

Now, lets have a look at the optimisation results, and plot the pipes:

# plot invested pipes
static_map_2 = dhnx.plotting.StaticMap(twn_results)
static_map_2.draw(background_map=False)
plt.title('Given network')
plt.scatter(network.components.consumers['lon'], network.components.consumers['lat'],
            color='tab:green', label='consumers', zorder=2.5, s=50)
plt.scatter(network.components.producers['lon'], network.components.producers['lat'],
            color='tab:red', label='producers', zorder=2.5, s=50)
plt.scatter(network.components.forks['lon'], network.components.forks['lat'],
            color='tab:grey', label='forks', zorder=2.5, s=50)
plt.text(-2, 32, 'P0', fontsize=14)
plt.text(82, 0, 'P1', fontsize=14)
plt.legend()
plt.show()

… which should give:

intro_opti_network_results.svg

Fig. 3: Pipes with investment

The next thing is to deactivate one heat producer by setting the attribute active of producer P1 to 0 (compare Thermal Network Input):

producers.csv

id

lat

lon

active

0

30

0

1

1

0

80

0

Now, the plot of pipes with a positive investment should look like this:

intro_opti_network_results_2.svg

Fig. 4: Pipes with investment (only P0)

There are many other options already implemented. For example:

  • Using time series as heat demand

  • Doing redundancy analysis by setting min and max attributes to the producers’ sources

  • Adding other oemof-solph objects like Transformer, Storages, further Buses, Sinks and Sources to each producer and consumer

  • Using discrete pipe data by using the nonconvex investment options

Have fun!