data.datasets.heat_supply.individual_heating   F
last analyzed

Complexity

Total Complexity 89

Size/Duplication

Total Lines 2054
Duplicated Lines 3.21 %

Importance

Changes 0
Metric Value
wmc 89
eloc 788
dl 66
loc 2054
rs 1.812
c 0
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
A HeatPumps2050.__init__() 0 8 1
A HeatPumpsPypsaEurSec.__init__() 0 47 2
A HeatPumps2035.__init__() 0 47 2

39 Functions

Rating   Name   Duplication   Size   Complexity  
A determine_hp_cap_buildings_eGon100RE() 0 58 4
A get_zensus_cells_with_decentral_heat_demand_in_mv_grid() 0 60 2
A plot_heat_supply() 24 31 2
A delete_hp_capacity_100RE() 0 4 1
A delete_hp_capacity_2035() 0 4 1
A get_peta_demand() 42 42 2
A delete_heat_peak_loads_2035() 0 8 2
A adapt_numpy_int64() 0 2 1
A delete_hp_capacity() 0 15 2
A delete_mvgd_ts_100RE() 0 6 1
A split_mvgds_into_bulks() 0 39 2
A delete_mvgd_ts() 0 15 2
A get_daily_demand_share() 0 32 2
A get_cts_buildings_with_decentral_heat_demand_in_mv_grid() 0 47 2
B cascade_per_technology() 0 114 6
B determine_hp_cap_peak_load_mvgd_ts_2035() 0 131 2
B determine_hp_cap_peak_load_mvgd_ts_pypsa_eur_sec() 0 104 2
A export_min_cap_to_csv() 0 23 3
A determine_min_hp_cap_buildings_pypsa_eur_sec() 0 31 2
A get_residential_buildings_with_decentral_heat_demand_in_mv_grid() 0 50 2
A catch_missing_buidings() 0 31 3
B determine_buildings_with_hp_in_mv_grid() 0 99 3
A adapt_numpy_float64() 0 2 1
A delete_heat_peak_loads_100RE() 0 8 2
A get_total_heat_pump_capacity_of_mv_grid() 0 38 3
A determine_minimum_hp_capacity_per_building() 0 24 1
A determine_hp_cap_buildings_eGon2035_per_mvgd() 0 47 3
A aggregate_residential_and_cts_profiles() 0 45 1
A get_buildings_with_decentral_heat_demand_in_mv_grid() 0 43 1
A get_residential_heat_profile_ids() 0 52 2
A desaggregate_hp_capacity() 0 35 1
A delete_pypsa_eur_sec_csv_file() 0 8 2
A get_daily_profiles() 0 33 2
A delete_mvgd_ts_2035() 0 6 1
A export_to_db() 0 57 3
B calc_residential_heat_profiles_per_mvgd() 0 95 3
A determine_hp_cap_buildings_eGon100RE_per_mvgd() 0 45 2
A get_heat_peak_demand_per_building() 0 27 3
A cascade_heat_supply_indiv() 0 89 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like data.datasets.heat_supply.individual_heating often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""The central module containing all code dealing with individual heat supply.
2
3
The following main things are done in this module:
4
5
* ..
6
* Desaggregation of heat pump capacities to individual buildings
7
* Determination of minimum required heat pump capacity for pypsa-eur-sec
8
9
The determination of the minimum required heat pump capacity for pypsa-eur-sec takes
10
place in the dataset 'HeatPumpsPypsaEurSec'. The goal is to ensure that the heat pump
11
capacities determined in pypsa-eur-sec are large enough to serve the heat demand of
12
individual buildings after the desaggregation from a few nodes in pypsa-eur-sec to the
13
individual buildings.
14
To determine minimum required heat pump capacity per building the buildings heat peak
15
load in the eGon100RE scenario is used (as pypsa-eur-sec serves as the scenario
16
generator for the eGon100RE scenario; see
17
:func:`determine_minimum_hp_capacity_per_building` for information on how minimum
18
required heat pump capacity is determined). As the heat peak load is not previously
19
determined, it is as well done in the course of this task.
20
Further, as determining heat peak load requires heat load
21
profiles of the buildings to be set up, this task is also utilised to set up
22
heat load profiles of all buildings with heat pumps within a grid in the eGon100RE
23
scenario used in eTraGo.
24
The resulting data is stored in separate tables respectively a csv file:
25
26
* `input-pypsa-eur-sec/minimum_hp_capacity_mv_grid_100RE.csv`:
27
    This csv file contains minimum required heat pump capacity per MV grid in MW as
28
    input for pypsa-eur-sec. It is created within :func:`export_min_cap_to_csv`.
29
* `demand.egon_etrago_timeseries_individual_heating`:
30
    This table contains aggregated heat load profiles of all buildings with heat pumps
31
    within an MV grid in the eGon100RE scenario used in eTraGo. It is created within
32
    :func:`individual_heating_per_mv_grid_tables`.
33
* `demand.egon_building_heat_peak_loads`:
34
    Mapping of peak heat demand and buildings including cell_id,
35
    building, area and peak load. This table is created in
36
    :func:`delete_heat_peak_loads_100RE`.
37
38
The desaggregation of heat pump capcacities to individual buildings takes place in two
39
separate datasets: 'HeatPumps2035' for eGon2035 scenario and 'HeatPumps2050' for
40
eGon100RE.
41
It is done separately because for one reason in case of the eGon100RE scenario the
42
minimum required heat pump capacity per building can directly be determined using the
43
heat peak load per building determined in the dataset 'HeatPumpsPypsaEurSec', whereas
44
heat peak load data does not yet exist for the eGon2035 scenario. Another reason is,
45
that in case of the eGon100RE scenario all buildings with individual heating have a
46
heat pump whereas in the eGon2035 scenario buildings are randomly selected until the
47
installed heat pump capacity per MV grid is met. All other buildings with individual
48
heating but no heat pump are assigned a gas boiler.
49
50
In the 'HeatPumps2035' dataset the following things are done.
51
First, the building's heat peak load in the eGon2035 scenario is determined for sizing
52
the heat pumps. To this end, heat load profiles per building are set up.
53
Using the heat peak load per building the minimum required heat pump capacity per
54
building is determined (see :func:`determine_minimum_hp_capacity_per_building`).
55
Afterwards, the total heat pump capacity per MV grid is desaggregated to individual
56
buildings in the MV grid, wherefore buildings are randomly chosen until the MV grid's total
57
heat pump capacity is reached (see :func:`determine_buildings_with_hp_in_mv_grid`).
58
Buildings with PV rooftop plants are more likely to be assigned a heat pump. In case
59
the minimum heat pump capacity of all chosen buildings is smaller than the total
60
heat pump capacity of the MV grid but adding another building would exceed the total
61
heat pump capacity of the MV grid, the remaining capacity is distributed to all
62
buildings with heat pumps proportionally to the size of their respective minimum
63
heat pump capacity. Therefore, the heat pump capacity of a building can be larger
64
than the minimum required heat pump capacity.
65
The generated heat load profiles per building are in a last step utilised to set up
66
heat load profiles of all buildings with heat pumps within a grid as well as for all
67
buildings with a gas boiler (i.e. all buildings with decentral heating system minus
68
buildings with heat pump) needed in eTraGo.
69
The resulting data is stored in the following tables:
70
71
* `demand.egon_hp_capacity_buildings`:
72
    This table contains the heat pump capacity of all buildings with a heat pump.
73
    It is created within :func:`delete_hp_capacity_2035`.
74
* `demand.egon_etrago_timeseries_individual_heating`:
75
    This table contains aggregated heat load profiles of all buildings with heat pumps
76
    within an MV grid as well as of all buildings with gas boilers within an MV grid in
77
    the eGon100RE scenario used in eTraGo. It is created within
78
    :func:`individual_heating_per_mv_grid_tables`.
79
* `demand.egon_building_heat_peak_loads`:
80
    Mapping of heat demand time series and buildings including cell_id,
81
    building, area and peak load. This table is created in
82
    :func:`delete_heat_peak_loads_2035`.
83
84
In the 'HeatPumps2050' dataset the total heat pump capacity in each MV grid can be
85
directly desaggregated to individual buildings, as the building's heat peak load was
86
already determined in the 'HeatPumpsPypsaEurSec' dataset. Also in contrast to the
87
'HeatPumps2035' dataset, all buildings with decentral heating system are assigned a
88
heat pump, wherefore no random sampling of buildings needs to be conducted.
89
The resulting data is stored in the following table:
90
91
* `demand.egon_hp_capacity_buildings`:
92
    This table contains the heat pump capacity of all buildings with a heat pump.
93
    It is created within :func:`delete_hp_capacity_2035`.
94
95
**The following datasets from the database are mainly used for creation:**
96
97
* `boundaries.egon_map_zensus_grid_districts`:
98
99
100
* `boundaries.egon_map_zensus_district_heating_areas`:
101
102
103
* `demand.egon_peta_heat`:
104
    Table of annual heat load demand for residential and cts at census cell
105
    level from peta5.
106
* `demand.egon_heat_timeseries_selected_profiles`:
107
108
109
* `demand.egon_heat_idp_pool`:
110
111
112
* `demand.egon_daily_heat_demand_per_climate_zone`:
113
114
115
* `boundaries.egon_map_zensus_mvgd_buildings`:
116
    A final mapping table including all buildings used for residential and
117
    cts, heat and electricity timeseries. Including census cells, mvgd bus_id,
118
    building type (osm or synthetic)
119
120
* `supply.egon_individual_heating`:
121
122
123
* `demand.egon_cts_heat_demand_building_share`:
124
    Table including the mv substation heat profile share of all selected
125
    cts buildings for scenario eGon2035 and eGon100RE. This table is created
126
    within :func:`cts_heat()`
127
128
129
**What is the goal?**
130
131
The goal is threefold. Primarily, heat pump capacity of individual buildings is
132
determined as it is necessary for distribution grid analysis. Secondly, as heat
133
demand profiles need to be set up during the process, the heat demand profiles of all
134
buildings with individual heat pumps respectively gas boilers per MV grid are set up
135
to be used in eTraGo. Thirdly, minimum heat pump capacity is determined as input for
136
pypsa-eur-sec to avoid that heat pump capacity per building is too little to meet
137
the heat demand after desaggregation to individual buildings.
138
139
**What is the challenge?**
140
141
The main challenge lies in the set up of heat demand profiles per building in
142
:func:`aggregate_residential_and_cts_profiles()` as it takes alot of time and
143
in grids with a high number of buildings requires alot of RAM. Both runtime and
144
RAM usage needed to be improved several times. To speed up the process, tasks are set
145
up to run in parallel. This currently leads to alot of connections being opened and
146
at a certain point to a runtime error due to too many open connections.
147
148
**What are central assumptions during the data processing?**
149
150
Central assumption for determining minimum heat pump capacity and desaggregating
151
heat pump capacity to individual buildings is that the required heat pump capacity
152
is determined using an approach from the
153
`network development plan <https://www.netzentwicklungsplan.de/sites/default/files/paragraphs-files/Szenariorahmenentwurf_NEP2035_2021_1.pdf>`_
154
(pp.46-47) (see :func:`determine_minimum_hp_capacity_per_building()`). There, the heat
155
pump capacity is determined by multiplying the heat peak
156
demand of the building by a minimum assumed COP of 1.7 and a flexibility factor of
157
24/18, taking into account that power supply of heat pumps can be interrupted for up
158
to six hours by the local distribution grid operator.
159
Another central assumption is, that buildings with PV rooftop plants are more likely
160
to have a heat pump than other buildings (see
161
:func:`determine_buildings_with_hp_in_mv_grid()` for details)
162
163
**Drawbacks and limitations of the data**
164
165
In the eGon2035 scenario buildings with heat pumps are selected randomly with a higher
166
probability for a heat pump for buildings with PV rooftop (see
167
:func:`determine_buildings_with_hp_in_mv_grid()` for details).
168
Another limitation may be the sizing of the heat pumps, as in the eGon2035 scenario
169
their size rigidly depends on the heat peak load and a fixed flexibility factor. During
170
the coldest days of the year, heat pump flexibility strongly depends on this
171
assumption and cannot be dynamically enlarged to provide more flexibility (or only
172
slightly through larger heat storage units).
173
174
Notes
175
-----
176
177
This module docstring is rather a dataset documentation. Once, a decision
178
is made in ... the content of this module docstring needs to be moved to
179
docs attribute of the respective dataset class.
180
"""
181
182
183
from pathlib import Path
184
import os
185
import random
186
187
from airflow.operators.python_operator import PythonOperator
188
from psycopg2.extensions import AsIs, register_adapter
189
from sqlalchemy import ARRAY, REAL, Column, Integer, String
190
from sqlalchemy.ext.declarative import declarative_base
191
import geopandas as gpd
192
import numpy as np
193
import pandas as pd
194
import saio
195
196
from egon.data import config, db, logger
197
from egon.data.datasets import Dataset
198
from egon.data.datasets.district_heating_areas import (
199
    MapZensusDistrictHeatingAreas,
200
)
201
from egon.data.datasets.electricity_demand_timeseries.cts_buildings import (
202
    calc_cts_building_profiles,
203
)
204
from egon.data.datasets.electricity_demand_timeseries.mapping import (
205
    EgonMapZensusMvgdBuildings,
206
)
207
from egon.data.datasets.electricity_demand_timeseries.tools import (
208
    write_table_to_postgres,
209
)
210
from egon.data.datasets.emobility.motorized_individual_travel.helpers import (
211
    reduce_mem_usage
212
)
213
from egon.data.datasets.heat_demand import EgonPetaHeat
214
from egon.data.datasets.heat_demand_timeseries.daily import (
215
    EgonDailyHeatDemandPerClimateZone,
216
    EgonMapZensusClimateZones,
217
)
218
from egon.data.datasets.heat_demand_timeseries.idp_pool import (
219
    EgonHeatTimeseries,
220
)
221
222
# get zensus cells with district heating
223
from egon.data.datasets.zensus_mv_grid_districts import MapZensusGridDistricts
224
225
engine = db.engine()
226
Base = declarative_base()
227
228
229
class EgonEtragoTimeseriesIndividualHeating(Base):
230
    __tablename__ = "egon_etrago_timeseries_individual_heating"
231
    __table_args__ = {"schema": "demand"}
232
    bus_id = Column(Integer, primary_key=True)
233
    scenario = Column(String, primary_key=True)
234
    carrier = Column(String, primary_key=True)
235
    dist_aggregated_mw = Column(ARRAY(REAL))
236
237
238
class EgonHpCapacityBuildings(Base):
239
    __tablename__ = "egon_hp_capacity_buildings"
240
    __table_args__ = {"schema": "demand"}
241
    building_id = Column(Integer, primary_key=True)
242
    scenario = Column(String, primary_key=True)
243
    hp_capacity = Column(REAL)
244
245
246
class HeatPumpsPypsaEurSec(Dataset):
247
    def __init__(self, dependencies):
248
        def dyn_parallel_tasks_pypsa_eur_sec():
249
            """Dynamically generate tasks
250
            The goal is to speed up tasks by parallelising bulks of mvgds.
251
252
            The number of parallel tasks is defined via parameter
253
            `parallel_tasks` in the dataset config `datasets.yml`.
254
255
            Returns
256
            -------
257
            set of airflow.PythonOperators
258
                The tasks. Each element is of
259
                :func:`egon.data.datasets.heat_supply.individual_heating.
260
                determine_hp_cap_peak_load_mvgd_ts_pypsa_eur_sec`
261
            """
262
            parallel_tasks = config.datasets()["demand_timeseries_mvgd"].get(
263
                "parallel_tasks", 1
264
            )
265
266
            tasks = set()
267
            for i in range(parallel_tasks):
268
                tasks.add(
269
                    PythonOperator(
270
                        task_id=(
271
                            f"individual_heating."
272
                            f"determine-hp-capacity-pypsa-eur-sec-"
273
                            f"mvgd-bulk{i}"
274
                        ),
275
                        python_callable=split_mvgds_into_bulks,
276
                        op_kwargs={
277
                            "n": i,
278
                            "max_n": parallel_tasks,
279
                            "func": determine_hp_cap_peak_load_mvgd_ts_pypsa_eur_sec,  # noqa: E501
280
                        },
281
                    )
282
                )
283
            return tasks
284
285
        super().__init__(
286
            name="HeatPumpsPypsaEurSec",
287
            version="0.0.2",
288
            dependencies=dependencies,
289
            tasks=(
290
                delete_pypsa_eur_sec_csv_file,
291
                delete_mvgd_ts_100RE,
292
                delete_heat_peak_loads_100RE,
293
                {*dyn_parallel_tasks_pypsa_eur_sec()},
294
            ),
295
        )
296
297
298
class HeatPumps2035(Dataset):
299
    def __init__(self, dependencies):
300
        def dyn_parallel_tasks_2035():
301
            """Dynamically generate tasks
302
303
            The goal is to speed up tasks by parallelising bulks of mvgds.
304
305
            The number of parallel tasks is defined via parameter
306
            `parallel_tasks` in the dataset config `datasets.yml`.
307
308
            Returns
309
            -------
310
            set of airflow.PythonOperators
311
                The tasks. Each element is of
312
                :func:`egon.data.datasets.heat_supply.individual_heating.
313
                determine_hp_cap_peak_load_mvgd_ts_2035`
314
            """
315
            parallel_tasks = config.datasets()["demand_timeseries_mvgd"].get(
316
                "parallel_tasks", 1
317
            )
318
            tasks = set()
319
            for i in range(parallel_tasks):
320
                tasks.add(
321
                    PythonOperator(
322
                        task_id=(
323
                            "individual_heating."
324
                            f"determine-hp-capacity-2035-"
325
                            f"mvgd-bulk{i}"
326
                        ),
327
                        python_callable=split_mvgds_into_bulks,
328
                        op_kwargs={
329
                            "n": i,
330
                            "max_n": parallel_tasks,
331
                            "func": determine_hp_cap_peak_load_mvgd_ts_2035,
332
                        },
333
                    )
334
                )
335
            return tasks
336
337
        super().__init__(
338
            name="HeatPumps2035",
339
            version="0.0.2",
340
            dependencies=dependencies,
341
            tasks=(
342
                delete_heat_peak_loads_2035,
343
                delete_hp_capacity_2035,
344
                delete_mvgd_ts_2035,
345
                {*dyn_parallel_tasks_2035()},
346
            ),
347
        )
348
349
350
class HeatPumps2050(Dataset):
351
    def __init__(self, dependencies):
352
        super().__init__(
353
            name="HeatPumps2050",
354
            version="0.0.2",
355
            dependencies=dependencies,
356
            tasks=(
357
                delete_hp_capacity_100RE,
358
                determine_hp_cap_buildings_eGon100RE,
359
            ),
360
        )
361
362
363
class BuildingHeatPeakLoads(Base):
364
    __tablename__ = "egon_building_heat_peak_loads"
365
    __table_args__ = {"schema": "demand"}
366
367
    building_id = Column(Integer, primary_key=True)
368
    scenario = Column(String, primary_key=True)
369
    sector = Column(String, primary_key=True)
370
    peak_load_in_w = Column(REAL)
371
372
373
def adapt_numpy_float64(numpy_float64):
374
    return AsIs(numpy_float64)
375
376
377
def adapt_numpy_int64(numpy_int64):
378
    return AsIs(numpy_int64)
379
380
381
def cascade_per_technology(
382
    heat_per_mv,
383
    technologies,
384
    scenario,
385
    distribution_level,
386
    max_size_individual_chp=0.05,
387
):
388
389
    """Add plants for individual heat.
390
    Currently only on mv grid district level.
391
392
    Parameters
393
    ----------
394
    mv_grid_districts : geopandas.geodataframe.GeoDataFrame
395
        MV grid districts including the heat demand
396
    technologies : pandas.DataFrame
397
        List of supply technologies and their parameters
398
    scenario : str
399
        Name of the scenario
400
    max_size_individual_chp : float
401
        Maximum capacity of an individual chp in MW
402
    Returns
403
    -------
404
    mv_grid_districts : geopandas.geodataframe.GeoDataFrame
405
        MV grid district which need additional individual heat supply
406
    technologies : pandas.DataFrame
407
        List of supply technologies and their parameters
408
    append_df : pandas.DataFrame
409
        List of plants per mv grid for the selected technology
410
411
    """
412
    sources = config.datasets()["heat_supply"]["sources"]
413
414
    tech = technologies[technologies.priority == technologies.priority.max()]
415
416
    # Distribute heat pumps linear to remaining demand.
417
    if tech.index == "heat_pump":
418
419
        if distribution_level == "federal_state":
420
            # Select target values per federal state
421
            target = db.select_dataframe(
422
                f"""
423
                    SELECT DISTINCT ON (gen) gen as state, capacity
424
                    FROM {sources['scenario_capacities']['schema']}.
425
                    {sources['scenario_capacities']['table']} a
426
                    JOIN {sources['federal_states']['schema']}.
427
                    {sources['federal_states']['table']} b
428
                    ON a.nuts = b.nuts
429
                    WHERE scenario_name = '{scenario}'
430
                    AND carrier = 'residential_rural_heat_pump'
431
                    """,
432
                index_col="state",
433
            )
434
435
            heat_per_mv["share"] = heat_per_mv.groupby(
436
                "state"
437
            ).remaining_demand.apply(lambda grp: grp / grp.sum())
438
439
            append_df = (
440
                heat_per_mv["share"]
441
                .mul(target.capacity[heat_per_mv["state"]].values)
442
                .reset_index()
443
            )
444
        else:
445
            # Select target value for Germany
446
            target = db.select_dataframe(
447
                f"""
448
                    SELECT SUM(capacity) AS capacity
449
                    FROM {sources['scenario_capacities']['schema']}.
450
                    {sources['scenario_capacities']['table']} a
451
                    WHERE scenario_name = '{scenario}'
452
                    AND carrier = 'residential_rural_heat_pump'
453
                    """
454
            )
455
456
            heat_per_mv["share"] = (
457
                heat_per_mv.remaining_demand
458
                / heat_per_mv.remaining_demand.sum()
459
            )
460
461
            append_df = (
462
                heat_per_mv["share"].mul(target.capacity[0]).reset_index()
463
            )
464
465
        append_df.rename(
466
            {"bus_id": "mv_grid_id", "share": "capacity"}, axis=1, inplace=True
467
        )
468
469
    elif tech.index == "gas_boiler":
470
471
        append_df = pd.DataFrame(
472
            data={
473
                "capacity": heat_per_mv.remaining_demand.div(
474
                    tech.estimated_flh.values[0]
475
                ),
476
                "carrier": "residential_rural_gas_boiler",
477
                "mv_grid_id": heat_per_mv.index,
478
                "scenario": scenario,
479
            }
480
        )
481
482
    if append_df.size > 0:
0 ignored issues
show
introduced by
The variable append_df does not seem to be defined for all execution paths.
Loading history...
483
        append_df["carrier"] = tech.index[0]
484
        heat_per_mv.loc[
485
            append_df.mv_grid_id, "remaining_demand"
486
        ] -= append_df.set_index("mv_grid_id").capacity.mul(
487
            tech.estimated_flh.values[0]
488
        )
489
490
    heat_per_mv = heat_per_mv[heat_per_mv.remaining_demand >= 0]
491
492
    technologies = technologies.drop(tech.index)
493
494
    return heat_per_mv, technologies, append_df
495
496
497
def cascade_heat_supply_indiv(scenario, distribution_level, plotting=True):
498
    """Assigns supply strategy for individual heating in four steps.
499
500
    1.) all small scale CHP are connected.
501
    2.) If the supply can not  meet the heat demand, solar thermal collectors
502
        are attached. This is not implemented yet, since individual
503
        solar thermal plants are not considered in eGon2035 scenario.
504
    3.) If this is not suitable, the mv grid is also supplied by heat pumps.
505
    4.) The last option are individual gas boilers.
506
507
    Parameters
508
    ----------
509
    scenario : str
510
        Name of scenario
511
    plotting : bool, optional
512
        Choose if individual heating supply is plotted. The default is True.
513
514
    Returns
515
    -------
516
    resulting_capacities : pandas.DataFrame
517
        List of plants per mv grid
518
519
    """
520
521
    sources = config.datasets()["heat_supply"]["sources"]
522
523
    # Select residential heat demand per mv grid district and federal state
524
    heat_per_mv = db.select_geodataframe(
525
        f"""
526
        SELECT d.bus_id as bus_id, SUM(demand) as demand,
527
        c.vg250_lan as state, d.geom
528
        FROM {sources['heat_demand']['schema']}.
529
        {sources['heat_demand']['table']} a
530
        JOIN {sources['map_zensus_grid']['schema']}.
531
        {sources['map_zensus_grid']['table']} b
532
        ON a.zensus_population_id = b.zensus_population_id
533
        JOIN {sources['map_vg250_grid']['schema']}.
534
        {sources['map_vg250_grid']['table']} c
535
        ON b.bus_id = c.bus_id
536
        JOIN {sources['mv_grids']['schema']}.
537
        {sources['mv_grids']['table']} d
538
        ON d.bus_id = c.bus_id
539
        WHERE scenario = '{scenario}'
540
        AND a.zensus_population_id NOT IN (
541
            SELECT zensus_population_id
542
            FROM {sources['map_dh']['schema']}.{sources['map_dh']['table']}
543
            WHERE scenario = '{scenario}')
544
        GROUP BY d.bus_id, vg250_lan, geom
545
        """,
546
        index_col="bus_id",
547
    )
548
549
    # Store geometry of mv grid
550
    geom_mv = heat_per_mv.geom.centroid.copy()
551
552
    # Initalize Dataframe for results
553
    resulting_capacities = pd.DataFrame(
554
        columns=["mv_grid_id", "carrier", "capacity"]
555
    )
556
557
    # Set technology data according to
558
    # http://www.wbzu.de/seminare/infopool/infopool-bhkw
559
    # TODO: Add gas boilers and solar themal (eGon100RE)
560
    technologies = pd.DataFrame(
561
        index=["heat_pump", "gas_boiler"],
562
        columns=["estimated_flh", "priority"],
563
        data={"estimated_flh": [4000, 8000], "priority": [2, 1]},
564
    )
565
566
    # In the beginning, the remaining demand equals demand
567
    heat_per_mv["remaining_demand"] = heat_per_mv["demand"]
568
569
    # Connect new technologies, if there is still heat demand left
570
    while (len(technologies) > 0) and (len(heat_per_mv) > 0):
571
        # Attach new supply technology
572
        heat_per_mv, technologies, append_df = cascade_per_technology(
573
            heat_per_mv, technologies, scenario, distribution_level
574
        )
575
        # Collect resulting capacities
576
        resulting_capacities = resulting_capacities.append(
577
            append_df, ignore_index=True
578
        )
579
580
    if plotting:
581
        plot_heat_supply(resulting_capacities)
582
583
    return gpd.GeoDataFrame(
584
        resulting_capacities,
585
        geometry=geom_mv[resulting_capacities.mv_grid_id].values,
586
    )
587
588
589 View Code Duplication
def get_peta_demand(mvgd, scenario):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
590
    """
591
    Retrieve annual peta heat demand for residential buildings for either
592
    eGon2035 or eGon100RE scenario.
593
594
    Parameters
595
    ----------
596
    mvgd : int
597
        MV grid ID.
598
    scenario : str
599
        Possible options are eGon2035 or eGon100RE
600
601
    Returns
602
    -------
603
    df_peta_demand : pd.DataFrame
604
        Annual residential heat demand per building and scenario. Columns of
605
        the dataframe are zensus_population_id and demand.
606
607
    """
608
609
    with db.session_scope() as session:
610
        query = (
611
            session.query(
612
                MapZensusGridDistricts.zensus_population_id,
613
                EgonPetaHeat.demand,
614
            )
615
            .filter(MapZensusGridDistricts.bus_id == mvgd)
616
            .filter(
617
                MapZensusGridDistricts.zensus_population_id
618
                == EgonPetaHeat.zensus_population_id
619
            )
620
            .filter(
621
                EgonPetaHeat.sector == "residential",
622
                EgonPetaHeat.scenario == scenario,
623
            )
624
        )
625
626
        df_peta_demand = pd.read_sql(
627
            query.statement, query.session.bind, index_col=None
628
        )
629
630
    return df_peta_demand
631
632
633
def get_residential_heat_profile_ids(mvgd):
634
    """
635
    Retrieve 365 daily heat profiles ids per residential building and selected
636
    mvgd.
637
638
    Parameters
639
    ----------
640
    mvgd : int
641
        ID of MVGD
642
643
    Returns
644
    -------
645
    df_profiles_ids : pd.DataFrame
646
        Residential daily heat profile ID's per building. Columns of the
647
        dataframe are zensus_population_id, building_id,
648
        selected_idp_profiles, buildings and day_of_year.
649
650
    """
651
    with db.session_scope() as session:
652
        query = (
653
            session.query(
654
                MapZensusGridDistricts.zensus_population_id,
655
                EgonHeatTimeseries.building_id,
656
                EgonHeatTimeseries.selected_idp_profiles,
657
            )
658
            .filter(MapZensusGridDistricts.bus_id == mvgd)
659
            .filter(
660
                MapZensusGridDistricts.zensus_population_id
661
                == EgonHeatTimeseries.zensus_population_id
662
            )
663
        )
664
665
        df_profiles_ids = pd.read_sql(
666
            query.statement, query.session.bind, index_col=None
667
        )
668
    # Add building count per cell
669
    df_profiles_ids = pd.merge(
670
        left=df_profiles_ids,
671
        right=df_profiles_ids.groupby("zensus_population_id")["building_id"]
672
        .count()
673
        .rename("buildings"),
674
        left_on="zensus_population_id",
675
        right_index=True,
676
    )
677
678
    # unnest array of ids per building
679
    df_profiles_ids = df_profiles_ids.explode("selected_idp_profiles")
680
    # add day of year column by order of list
681
    df_profiles_ids["day_of_year"] = (
682
        df_profiles_ids.groupby("building_id").cumcount() + 1
683
    )
684
    return df_profiles_ids
685
686
687
def get_daily_profiles(profile_ids):
688
    """
689
    Parameters
690
    ----------
691
    profile_ids : list(int)
692
        daily heat profile ID's
693
694
    Returns
695
    -------
696
    df_profiles : pd.DataFrame
697
        Residential daily heat profiles. Columns of the dataframe are idp,
698
        house, temperature_class and hour.
699
700
    """
701
702
    saio.register_schema("demand", db.engine())
703
    from saio.demand import egon_heat_idp_pool
704
705
    with db.session_scope() as session:
706
        query = session.query(egon_heat_idp_pool).filter(
707
            egon_heat_idp_pool.index.in_(profile_ids)
708
        )
709
710
        df_profiles = pd.read_sql(
711
            query.statement, query.session.bind, index_col="index"
712
        )
713
714
    # unnest array of profile values per id
715
    df_profiles = df_profiles.explode("idp")
716
    # Add column for hour of day
717
    df_profiles["hour"] = df_profiles.groupby(axis=0, level=0).cumcount() + 1
718
719
    return df_profiles
720
721
722
def get_daily_demand_share(mvgd):
723
    """per census cell
724
    Parameters
725
    ----------
726
    mvgd : int
727
        MVGD id
728
729
    Returns
730
    -------
731
    df_daily_demand_share : pd.DataFrame
732
        Daily annual demand share per cencus cell. Columns of the dataframe
733
        are zensus_population_id, day_of_year and daily_demand_share.
734
735
    """
736
737
    with db.session_scope() as session:
738
        query = session.query(
739
            MapZensusGridDistricts.zensus_population_id,
740
            EgonDailyHeatDemandPerClimateZone.day_of_year,
741
            EgonDailyHeatDemandPerClimateZone.daily_demand_share,
742
        ).filter(
743
            EgonMapZensusClimateZones.climate_zone
744
            == EgonDailyHeatDemandPerClimateZone.climate_zone,
745
            MapZensusGridDistricts.zensus_population_id
746
            == EgonMapZensusClimateZones.zensus_population_id,
747
            MapZensusGridDistricts.bus_id == mvgd,
748
        )
749
750
        df_daily_demand_share = pd.read_sql(
751
            query.statement, query.session.bind, index_col=None
752
        )
753
    return df_daily_demand_share
754
755
756
def calc_residential_heat_profiles_per_mvgd(mvgd, scenario):
757
    """
758
    Gets residential heat profiles per building in MV grid for either eGon2035
759
    or eGon100RE scenario.
760
761
    Parameters
762
    ----------
763
    mvgd : int
764
        MV grid ID.
765
    scenario : str
766
        Possible options are eGon2035 or eGon100RE.
767
768
    Returns
769
    --------
770
    pd.DataFrame
771
        Heat demand profiles of buildings. Columns are:
772
            * zensus_population_id : int
773
                Zensus cell ID building is in.
774
            * building_id : int
775
                ID of building.
776
            * day_of_year : int
777
                Day of the year (1 - 365).
778
            * hour : int
779
                Hour of the day (1 - 24).
780
            * demand_ts : float
781
                Building's residential heat demand in MW, for specified hour
782
                of the year (specified through columns `day_of_year` and
783
                `hour`).
784
    """
785
786
    columns = [
787
        "zensus_population_id",
788
        "building_id",
789
        "day_of_year",
790
        "hour",
791
        "demand_ts",
792
    ]
793
794
    df_peta_demand = get_peta_demand(mvgd, scenario)
795
    df_peta_demand = reduce_mem_usage(df_peta_demand)
796
797
    # TODO maybe return empty dataframe
798
    if df_peta_demand.empty:
799
        logger.info(f"No demand for MVGD: {mvgd}")
800
        return pd.DataFrame(columns=columns)
801
802
    df_profiles_ids = get_residential_heat_profile_ids(mvgd)
803
804
    if df_profiles_ids.empty:
805
        logger.info(f"No profiles for MVGD: {mvgd}")
806
        return pd.DataFrame(columns=columns)
807
808
    df_profiles = get_daily_profiles(
809
        df_profiles_ids["selected_idp_profiles"].unique()
810
    )
811
812
    df_daily_demand_share = get_daily_demand_share(mvgd)
813
814
    # Merge profile ids to peta demand by zensus_population_id
815
    df_profile_merge = pd.merge(
816
        left=df_peta_demand, right=df_profiles_ids, on="zensus_population_id"
817
    )
818
819
    df_profile_merge.demand = df_profile_merge.demand.div(df_profile_merge.buildings)
820
    df_profile_merge.drop('buildings', axis='columns', inplace=True)
821
822
    # Merge daily demand to daily profile ids by zensus_population_id and day
823
    df_profile_merge = pd.merge(
824
        left=df_profile_merge,
825
        right=df_daily_demand_share,
826
        on=["zensus_population_id", "day_of_year"],
827
    )
828
    df_profile_merge.demand = df_profile_merge.demand.mul(
829
        df_profile_merge.daily_demand_share)
830
    df_profile_merge.drop('daily_demand_share', axis='columns', inplace=True)
831
    df_profile_merge = reduce_mem_usage(df_profile_merge)
832
833
    # Merge daily profiles by profile id
834
    df_profile_merge = pd.merge(
835
        left=df_profile_merge,
836
        right=df_profiles[["idp", "hour"]],
837
        left_on="selected_idp_profiles",
838
        right_index=True,
839
    )
840
    df_profile_merge = reduce_mem_usage(df_profile_merge)
841
842
    df_profile_merge.demand = df_profile_merge.demand.mul(
843
        df_profile_merge.idp.astype(float))
844
    df_profile_merge.drop('idp', axis='columns', inplace=True)
845
846
    df_profile_merge.rename({'demand': 'demand_ts'}, axis='columns', inplace=True)
847
848
    df_profile_merge = reduce_mem_usage(df_profile_merge)
849
850
    return df_profile_merge.loc[:, columns]
851
852
853 View Code Duplication
def plot_heat_supply(resulting_capacities):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
854
855
    from matplotlib import pyplot as plt
856
857
    mv_grids = db.select_geodataframe(
858
        """
859
        SELECT * FROM grid.egon_mv_grid_district
860
        """,
861
        index_col="bus_id",
862
    )
863
864
    for c in ["CHP", "heat_pump"]:
865
        mv_grids[c] = (
866
            resulting_capacities[resulting_capacities.carrier == c]
867
            .set_index("mv_grid_id")
868
            .capacity
869
        )
870
871
        fig, ax = plt.subplots(1, 1)
872
        mv_grids.boundary.plot(linewidth=0.2, ax=ax, color="black")
873
        mv_grids.plot(
874
            ax=ax,
875
            column=c,
876
            cmap="magma_r",
877
            legend=True,
878
            legend_kwds={
879
                "label": f"Installed {c} in MW",
880
                "orientation": "vertical",
881
            },
882
        )
883
        plt.savefig(f"plots/individual_heat_supply_{c}.png", dpi=300)
884
885
886
def get_zensus_cells_with_decentral_heat_demand_in_mv_grid(
887
    scenario, mv_grid_id
888
):
889
    """
890
    Returns zensus cell IDs with decentral heating systems in given MV grid.
891
892
    As cells with district heating differ between scenarios, this is also
893
    depending on the scenario.
894
895
    Parameters
896
    -----------
897
    scenario : str
898
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
899
    mv_grid_id : int
900
        ID of MV grid.
901
902
    Returns
903
    --------
904
    pd.Index(int)
905
        Zensus cell IDs (as int) of buildings with decentral heating systems in
906
        given MV grid. Type is pandas Index to avoid errors later on when it is
907
        used in a query.
908
909
    """
910
911
    # get zensus cells in grid
912
    zensus_population_ids = db.select_dataframe(
913
        f"""
914
        SELECT zensus_population_id
915
        FROM boundaries.egon_map_zensus_grid_districts
916
        WHERE bus_id = {mv_grid_id}
917
        """,
918
        index_col=None,
919
    ).zensus_population_id.values
920
921
    # maybe use adapter
922
    # convert to pd.Index (otherwise type is np.int64, which will for some
923
    # reason throw an error when used in a query)
924
    zensus_population_ids = pd.Index(zensus_population_ids)
925
926
    # get zensus cells with district heating
927
    with db.session_scope() as session:
928
        query = session.query(
929
            MapZensusDistrictHeatingAreas.zensus_population_id,
930
        ).filter(
931
            MapZensusDistrictHeatingAreas.scenario == scenario,
932
            MapZensusDistrictHeatingAreas.zensus_population_id.in_(
933
                zensus_population_ids
934
            ),
935
        )
936
937
        cells_with_dh = pd.read_sql(
938
            query.statement, query.session.bind, index_col=None
939
        ).zensus_population_id.values
940
941
    # remove zensus cells with district heating
942
    zensus_population_ids = zensus_population_ids.drop(
943
        cells_with_dh, errors="ignore"
944
    )
945
    return pd.Index(zensus_population_ids)
946
947
948
def get_residential_buildings_with_decentral_heat_demand_in_mv_grid(
949
    scenario, mv_grid_id
950
):
951
    """
952
    Returns building IDs of buildings with decentral residential heat demand in
953
    given MV grid.
954
955
    As cells with district heating differ between scenarios, this is also
956
    depending on the scenario.
957
958
    Parameters
959
    -----------
960
    scenario : str
961
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
962
    mv_grid_id : int
963
        ID of MV grid.
964
965
    Returns
966
    --------
967
    pd.Index(int)
968
        Building IDs (as int) of buildings with decentral heating system in
969
        given MV grid. Type is pandas Index to avoid errors later on when it is
970
        used in a query.
971
972
    """
973
    # get zensus cells with decentral heating
974
    zensus_population_ids = (
975
        get_zensus_cells_with_decentral_heat_demand_in_mv_grid(
976
            scenario, mv_grid_id
977
        )
978
    )
979
980
    # get buildings with decentral heat demand
981
    saio.register_schema("demand", engine)
982
    from saio.demand import egon_heat_timeseries_selected_profiles
983
984
    with db.session_scope() as session:
985
        query = session.query(
986
            egon_heat_timeseries_selected_profiles.building_id,
987
        ).filter(
988
            egon_heat_timeseries_selected_profiles.zensus_population_id.in_(
989
                zensus_population_ids
990
            )
991
        )
992
993
        buildings_with_heat_demand = pd.read_sql(
994
            query.statement, query.session.bind, index_col=None
995
        ).building_id.values
996
997
    return pd.Index(buildings_with_heat_demand)
998
999
1000
def get_cts_buildings_with_decentral_heat_demand_in_mv_grid(
1001
    scenario, mv_grid_id
1002
):
1003
    """
1004
    Returns building IDs of buildings with decentral CTS heat demand in
1005
    given MV grid.
1006
1007
    As cells with district heating differ between scenarios, this is also
1008
    depending on the scenario.
1009
1010
    Parameters
1011
    -----------
1012
    scenario : str
1013
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
1014
    mv_grid_id : int
1015
        ID of MV grid.
1016
1017
    Returns
1018
    --------
1019
    pd.Index(int)
1020
        Building IDs (as int) of buildings with decentral heating system in
1021
        given MV grid. Type is pandas Index to avoid errors later on when it is
1022
        used in a query.
1023
1024
    """
1025
1026
    # get zensus cells with decentral heating
1027
    zensus_population_ids = (
1028
        get_zensus_cells_with_decentral_heat_demand_in_mv_grid(
1029
            scenario, mv_grid_id
1030
        )
1031
    )
1032
1033
    # get buildings with decentral heat demand
1034
    with db.session_scope() as session:
1035
        query = session.query(EgonMapZensusMvgdBuildings.building_id).filter(
1036
            EgonMapZensusMvgdBuildings.sector == "cts",
1037
            EgonMapZensusMvgdBuildings.zensus_population_id.in_(
1038
                zensus_population_ids
1039
            ),
1040
        )
1041
1042
        buildings_with_heat_demand = pd.read_sql(
1043
            query.statement, query.session.bind, index_col=None
1044
        ).building_id.values
1045
1046
    return pd.Index(buildings_with_heat_demand)
1047
1048
1049
def get_buildings_with_decentral_heat_demand_in_mv_grid(mvgd, scenario):
1050
    """
1051
    Returns building IDs of buildings with decentral heat demand in
1052
    given MV grid.
1053
1054
    As cells with district heating differ between scenarios, this is also
1055
    depending on the scenario. CTS and residential have to be retrieved
1056
    seperatly as some residential buildings only have electricity but no
1057
    heat demand. This does not occure in CTS.
1058
1059
    Parameters
1060
    -----------
1061
    mvgd : int
1062
        ID of MV grid.
1063
    scenario : str
1064
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
1065
1066
    Returns
1067
    --------
1068
    pd.Index(int)
1069
        Building IDs (as int) of buildings with decentral heating system in
1070
        given MV grid. Type is pandas Index to avoid errors later on when it is
1071
        used in a query.
1072
1073
    """
1074
    # get residential buildings with decentral heating systems
1075
    buildings_decentral_heating_res = (
1076
        get_residential_buildings_with_decentral_heat_demand_in_mv_grid(
1077
            scenario, mvgd
1078
        )
1079
    )
1080
1081
    # get CTS buildings with decentral heating systems
1082
    buildings_decentral_heating_cts = (
1083
        get_cts_buildings_with_decentral_heat_demand_in_mv_grid(scenario, mvgd)
1084
    )
1085
1086
    # merge residential and CTS buildings
1087
    buildings_decentral_heating = buildings_decentral_heating_res.append(
1088
        buildings_decentral_heating_cts
1089
    ).unique()
1090
1091
    return buildings_decentral_heating
1092
1093
1094
def get_total_heat_pump_capacity_of_mv_grid(scenario, mv_grid_id):
1095
    """
1096
    Returns total heat pump capacity per grid that was previously defined
1097
    (by NEP or pypsa-eur-sec).
1098
1099
    Parameters
1100
    -----------
1101
    scenario : str
1102
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
1103
    mv_grid_id : int
1104
        ID of MV grid.
1105
1106
    Returns
1107
    --------
1108
    float
1109
        Total heat pump capacity in MW in given MV grid.
1110
1111
    """
1112
    from egon.data.datasets.heat_supply import EgonIndividualHeatingSupply
1113
1114
    with db.session_scope() as session:
1115
        query = (
1116
            session.query(
1117
                EgonIndividualHeatingSupply.mv_grid_id,
1118
                EgonIndividualHeatingSupply.capacity,
1119
            )
1120
            .filter(EgonIndividualHeatingSupply.scenario == scenario)
1121
            .filter(EgonIndividualHeatingSupply.carrier == "heat_pump")
1122
            .filter(EgonIndividualHeatingSupply.mv_grid_id == mv_grid_id)
1123
        )
1124
1125
        hp_cap_mv_grid = pd.read_sql(
1126
            query.statement, query.session.bind, index_col="mv_grid_id"
1127
        )
1128
    if hp_cap_mv_grid.empty:
1129
        return 0.0
1130
    else:
1131
        return hp_cap_mv_grid.capacity.values[0]
1132
1133
1134
def get_heat_peak_demand_per_building(scenario, building_ids):
1135
    """"""
1136
1137
    with db.session_scope() as session:
1138
        query = (
1139
            session.query(
1140
                BuildingHeatPeakLoads.building_id,
1141
                BuildingHeatPeakLoads.peak_load_in_w,
1142
            )
1143
            .filter(BuildingHeatPeakLoads.scenario == scenario)
1144
            .filter(BuildingHeatPeakLoads.building_id.in_(building_ids))
1145
        )
1146
1147
        df_heat_peak_demand = pd.read_sql(
1148
            query.statement, query.session.bind, index_col=None
1149
        )
1150
1151
    # TODO remove check
1152
    if df_heat_peak_demand.duplicated("building_id").any():
1153
        raise ValueError("Duplicate building_id")
1154
1155
    # convert to series and from W to MW
1156
    df_heat_peak_demand = (
1157
        df_heat_peak_demand.set_index("building_id").loc[:, "peak_load_in_w"]
1158
        * 1e6
1159
    )
1160
    return df_heat_peak_demand
1161
1162
1163
def determine_minimum_hp_capacity_per_building(
1164
    peak_heat_demand, flexibility_factor=24 / 18, cop=1.7
1165
):
1166
    """
1167
    Determines minimum required heat pump capacity.
1168
1169
    Parameters
1170
    ----------
1171
    peak_heat_demand : pd.Series
1172
        Series with peak heat demand per building in MW. Index contains the
1173
        building ID.
1174
    flexibility_factor : float
1175
        Factor to overdimension the heat pump to allow for some flexible
1176
        dispatch in times of high heat demand. Per default, a factor of 24/18
1177
        is used, to take into account
1178
1179
    Returns
1180
    -------
1181
    pd.Series
1182
        Pandas series with minimum required heat pump capacity per building in
1183
        MW.
1184
1185
    """
1186
    return peak_heat_demand * flexibility_factor / cop
1187
1188
1189
def determine_buildings_with_hp_in_mv_grid(
1190
    hp_cap_mv_grid, min_hp_cap_per_building
1191
):
1192
    """
1193
    Distributes given total heat pump capacity to buildings based on their peak
1194
    heat demand.
1195
1196
    Parameters
1197
    -----------
1198
    hp_cap_mv_grid : float
1199
        Total heat pump capacity in MW in given MV grid.
1200
    min_hp_cap_per_building : pd.Series
1201
        Pandas series with minimum required heat pump capacity per building
1202
         in MW.
1203
1204
    Returns
1205
    -------
1206
    pd.Index(int)
1207
        Building IDs (as int) of buildings to get heat demand time series for.
1208
1209
    """
1210
    building_ids = min_hp_cap_per_building.index
1211
1212
    # get buildings with PV to give them a higher priority when selecting
1213
    # buildings a heat pump will be allocated to
1214
    saio.register_schema("supply", engine)
1215
    from saio.supply import egon_power_plants_pv_roof_building
1216
1217
    with db.session_scope() as session:
1218
        query = session.query(
1219
            egon_power_plants_pv_roof_building.building_id
1220
        ).filter(
1221
            egon_power_plants_pv_roof_building.building_id.in_(building_ids),
1222
            egon_power_plants_pv_roof_building.scenario == "eGon2035",
1223
        )
1224
1225
        buildings_with_pv = pd.read_sql(
1226
            query.statement, query.session.bind, index_col=None
1227
        ).building_id.values
1228
    # set different weights for buildings with PV and without PV
1229
    weight_with_pv = 1.5
1230
    weight_without_pv = 1.0
1231
    weights = pd.concat(
1232
        [
1233
            pd.DataFrame(
1234
                {"weight": weight_without_pv},
1235
                index=building_ids.drop(buildings_with_pv, errors="ignore"),
1236
            ),
1237
            pd.DataFrame({"weight": weight_with_pv}, index=buildings_with_pv),
1238
        ]
1239
    )
1240
    # normalise weights (probability needs to add up to 1)
1241
    weights.weight = weights.weight / weights.weight.sum()
1242
1243
    # get random order at which buildings are chosen
1244
    np.random.seed(db.credentials()["--random-seed"])
1245
    buildings_with_hp_order = np.random.choice(
1246
        weights.index,
1247
        size=len(weights),
1248
        replace=False,
1249
        p=weights.weight.values,
1250
    )
1251
1252
    # select buildings until HP capacity in MV grid is reached (some rest
1253
    # capacity will remain)
1254
    hp_cumsum = min_hp_cap_per_building.loc[buildings_with_hp_order].cumsum()
1255
    buildings_with_hp = hp_cumsum[hp_cumsum <= hp_cap_mv_grid].index
1256
1257
    # choose random heat pumps until remaining heat pumps are larger than
1258
    # remaining heat pump capacity
1259
    remaining_hp_cap = (
1260
        hp_cap_mv_grid - min_hp_cap_per_building.loc[buildings_with_hp].sum()
1261
    )
1262
    min_cap_buildings_wo_hp = min_hp_cap_per_building.loc[
1263
        building_ids.drop(buildings_with_hp)
1264
    ]
1265
    possible_buildings = min_cap_buildings_wo_hp[
1266
        min_cap_buildings_wo_hp <= remaining_hp_cap
1267
    ].index
1268
    while len(possible_buildings) > 0:
1269
        random.seed(db.credentials()["--random-seed"])
1270
        new_hp_building = random.choice(possible_buildings)
1271
        # add new building to building with HP
1272
        buildings_with_hp = buildings_with_hp.append(
1273
            pd.Index([new_hp_building])
1274
        )
1275
        # determine if there are still possible buildings
1276
        remaining_hp_cap = (
1277
            hp_cap_mv_grid
1278
            - min_hp_cap_per_building.loc[buildings_with_hp].sum()
1279
        )
1280
        min_cap_buildings_wo_hp = min_hp_cap_per_building.loc[
1281
            building_ids.drop(buildings_with_hp)
1282
        ]
1283
        possible_buildings = min_cap_buildings_wo_hp[
1284
            min_cap_buildings_wo_hp <= remaining_hp_cap
1285
        ].index
1286
1287
    return buildings_with_hp
1288
1289
1290
def desaggregate_hp_capacity(min_hp_cap_per_building, hp_cap_mv_grid):
1291
    """
1292
    Desaggregates the required total heat pump capacity to buildings.
1293
1294
    All buildings are previously assigned a minimum required heat pump
1295
    capacity. If the total heat pump capacity exceeds this, larger heat pumps
1296
    are assigned.
1297
1298
    Parameters
1299
    ------------
1300
    min_hp_cap_per_building : pd.Series
1301
        Pandas series with minimum required heat pump capacity per building
1302
         in MW.
1303
    hp_cap_mv_grid : float
1304
        Total heat pump capacity in MW in given MV grid.
1305
1306
    Returns
1307
    --------
1308
    pd.Series
1309
        Pandas series with heat pump capacity per building in MW.
1310
1311
    """
1312
    # distribute remaining capacity to all buildings with HP depending on
1313
    # installed HP capacity
1314
1315
    allocated_cap = min_hp_cap_per_building.sum()
1316
    remaining_cap = hp_cap_mv_grid - allocated_cap
1317
1318
    fac = remaining_cap / allocated_cap
1319
    hp_cap_per_building = (
1320
        min_hp_cap_per_building * fac + min_hp_cap_per_building
1321
    )
1322
    hp_cap_per_building.index.name = "building_id"
1323
1324
    return hp_cap_per_building
1325
1326
1327
def determine_min_hp_cap_buildings_pypsa_eur_sec(
1328
    peak_heat_demand, building_ids
1329
):
1330
    """
1331
    Determines minimum required HP capacity in MV grid in MW as input for
1332
    pypsa-eur-sec.
1333
1334
    Parameters
1335
    ----------
1336
    peak_heat_demand : pd.Series
1337
        Series with peak heat demand per building in MW. Index contains the
1338
        building ID.
1339
    building_ids : pd.Index(int)
1340
        Building IDs (as int) of buildings with decentral heating system in
1341
        given MV grid.
1342
1343
    Returns
1344
    --------
1345
    float
1346
        Minimum required HP capacity in MV grid in MW.
1347
1348
    """
1349
    if len(building_ids) > 0:
1350
        peak_heat_demand = peak_heat_demand.loc[building_ids]
1351
        # determine minimum required heat pump capacity per building
1352
        min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
1353
            peak_heat_demand
1354
        )
1355
        return min_hp_cap_buildings.sum()
1356
    else:
1357
        return 0.0
1358
1359
1360
def determine_hp_cap_buildings_eGon2035_per_mvgd(
1361
    mv_grid_id, peak_heat_demand, building_ids
1362
):
1363
    """
1364
    Determines which buildings in the MV grid will have a HP (buildings with PV
1365
    rooftop are more likely to be assigned) in the eGon2035 scenario, as well
1366
    as their respective HP capacity in MW.
1367
1368
    Parameters
1369
    -----------
1370
    mv_grid_id : int
1371
        ID of MV grid.
1372
    peak_heat_demand : pd.Series
1373
        Series with peak heat demand per building in MW. Index contains the
1374
        building ID.
1375
    building_ids : pd.Index(int)
1376
        Building IDs (as int) of buildings with decentral heating system in
1377
        given MV grid.
1378
1379
    """
1380
1381
    hp_cap_grid = get_total_heat_pump_capacity_of_mv_grid(
1382
        "eGon2035", mv_grid_id
1383
    )
1384
1385
    if len(building_ids) > 0 and hp_cap_grid > 0.0:
1386
        peak_heat_demand = peak_heat_demand.loc[building_ids]
1387
1388
        # determine minimum required heat pump capacity per building
1389
        min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
1390
            peak_heat_demand
1391
        )
1392
1393
        # select buildings that will have a heat pump
1394
        buildings_with_hp = determine_buildings_with_hp_in_mv_grid(
1395
            hp_cap_grid, min_hp_cap_buildings
1396
        )
1397
1398
        # distribute total heat pump capacity to all buildings with HP
1399
        hp_cap_per_building = desaggregate_hp_capacity(
1400
            min_hp_cap_buildings.loc[buildings_with_hp], hp_cap_grid
1401
        )
1402
1403
        return hp_cap_per_building.rename("hp_capacity")
1404
1405
    else:
1406
        return pd.Series(dtype="float64").rename("hp_capacity")
1407
1408
1409
def determine_hp_cap_buildings_eGon100RE_per_mvgd(mv_grid_id):
1410
    """
1411
    Determines HP capacity per building in eGon100RE scenario.
1412
1413
    In eGon100RE scenario all buildings without district heating get a heat
1414
    pump.
1415
1416
    Returns
1417
    --------
1418
    pd.Series
1419
        Pandas series with heat pump capacity per building in MW.
1420
1421
    """
1422
1423
    hp_cap_grid = get_total_heat_pump_capacity_of_mv_grid(
1424
        "eGon100RE", mv_grid_id
1425
    )
1426
1427
    if hp_cap_grid > 0.0:
1428
1429
        # get buildings with decentral heating systems
1430
        building_ids = get_buildings_with_decentral_heat_demand_in_mv_grid(
1431
            mv_grid_id, scenario="eGon100RE"
1432
        )
1433
1434
        logger.info(f"MVGD={mv_grid_id} | Get peak loads from DB")
1435
        df_peak_heat_demand = get_heat_peak_demand_per_building(
1436
            "eGon100RE", building_ids
1437
        )
1438
1439
        logger.info(f"MVGD={mv_grid_id} | Determine HP capacities.")
1440
        # determine minimum required heat pump capacity per building
1441
        min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
1442
            df_peak_heat_demand, flexibility_factor=24 / 18, cop=1.7
1443
        )
1444
1445
        logger.info(f"MVGD={mv_grid_id} | Desaggregate HP capacities.")
1446
        # distribute total heat pump capacity to all buildings with HP
1447
        hp_cap_per_building = desaggregate_hp_capacity(
1448
            min_hp_cap_buildings, hp_cap_grid
1449
        )
1450
1451
        return hp_cap_per_building.rename("hp_capacity")
1452
    else:
1453
        return pd.Series(dtype="float64").rename("hp_capacity")
1454
1455
1456
def determine_hp_cap_buildings_eGon100RE():
1457
    """
1458
    Main function to determine HP capacity per building in eGon100RE scenario.
1459
1460
    """
1461
1462
    # ========== Register np datatypes with SQLA ==========
1463
    register_adapter(np.float64, adapt_numpy_float64)
1464
    register_adapter(np.int64, adapt_numpy_int64)
1465
    # =====================================================
1466
1467
    with db.session_scope() as session:
1468
        query = (
1469
            session.query(
1470
                MapZensusGridDistricts.bus_id,
1471
            )
1472
            .filter(
1473
                MapZensusGridDistricts.zensus_population_id
1474
                == EgonPetaHeat.zensus_population_id
1475
            )
1476
            .distinct(MapZensusGridDistricts.bus_id)
1477
        )
1478
        mvgd_ids = pd.read_sql(
1479
            query.statement, query.session.bind, index_col=None
1480
        )
1481
    mvgd_ids = mvgd_ids.sort_values("bus_id")
1482
    mvgd_ids = mvgd_ids["bus_id"].values
1483
1484
    df_hp_cap_per_building_100RE_db = pd.DataFrame(
1485
        columns=["building_id", "hp_capacity"]
1486
    )
1487
1488
    for mvgd_id in mvgd_ids:
1489
1490
        logger.info(f"MVGD={mvgd_id} | Start")
1491
1492
        hp_cap_per_building_100RE = (
1493
            determine_hp_cap_buildings_eGon100RE_per_mvgd(mvgd_id)
1494
        )
1495
1496
        if not hp_cap_per_building_100RE.empty:
1497
            df_hp_cap_per_building_100RE_db = pd.concat(
1498
                [
1499
                    df_hp_cap_per_building_100RE_db,
1500
                    hp_cap_per_building_100RE.reset_index(),
1501
                ],
1502
                axis=0,
1503
            )
1504
1505
    logger.info(f"MVGD={min(mvgd_ids)} : {max(mvgd_ids)} | Write data to db.")
1506
    df_hp_cap_per_building_100RE_db["scenario"] = "eGon100RE"
1507
1508
    EgonHpCapacityBuildings.__table__.create(bind=engine, checkfirst=True)
1509
1510
    write_table_to_postgres(
1511
        df_hp_cap_per_building_100RE_db,
1512
        EgonHpCapacityBuildings,
1513
        drop=False,
1514
    )
1515
1516
1517
def aggregate_residential_and_cts_profiles(mvgd, scenario):
1518
    """
1519
    Gets residential and CTS heat demand profiles per building and aggregates
1520
    them.
1521
1522
    Parameters
1523
    ----------
1524
    mvgd : int
1525
        MV grid ID.
1526
    scenario : str
1527
        Possible options are eGon2035 or eGon100RE.
1528
1529
    Returns
1530
    --------
1531
    pd.DataFrame
1532
        Table of demand profile per building. Column names are building IDs and
1533
        index is hour of the year as int (0-8759).
1534
1535
    """
1536
    # ############### get residential heat demand profiles ###############
1537
    df_heat_ts = calc_residential_heat_profiles_per_mvgd(
1538
        mvgd=mvgd, scenario=scenario
1539
    )
1540
1541
    # pivot to allow aggregation with CTS profiles
1542
    df_heat_ts = df_heat_ts.pivot(
1543
        index=["day_of_year", "hour"],
1544
        columns="building_id",
1545
        values="demand_ts",
1546
    )
1547
    df_heat_ts = df_heat_ts.sort_index().reset_index(drop=True)
1548
1549
    # ############### get CTS heat demand profiles ###############
1550
    heat_demand_cts_ts = calc_cts_building_profiles(
1551
        bus_ids=[mvgd],
1552
        scenario=scenario,
1553
        sector="heat",
1554
    )
1555
1556
    # ############# aggregate residential and CTS demand profiles #############
1557
    df_heat_ts = pd.concat([df_heat_ts, heat_demand_cts_ts], axis=1)
1558
1559
    df_heat_ts = df_heat_ts.groupby(axis=1, level=0).sum()
1560
1561
    return df_heat_ts
1562
1563
1564
def export_to_db(df_peak_loads_db, df_heat_mvgd_ts_db, drop=False):
1565
    """
1566
    Function to export the collected results of all MVGDs per bulk to DB.
1567
1568
        Parameters
1569
    ----------
1570
    df_peak_loads_db : pd.DataFrame
1571
        Table of building peak loads of all MVGDs per bulk
1572
    df_heat_mvgd_ts_db : pd.DataFrame
1573
        Table of all aggregated MVGD profiles per bulk
1574
    drop : boolean
1575
        Drop and recreate table if True
1576
1577
    """
1578
1579
    df_peak_loads_db = df_peak_loads_db.melt(
1580
        id_vars="building_id",
1581
        var_name="scenario",
1582
        value_name="peak_load_in_w",
1583
    )
1584
    df_peak_loads_db["building_id"] = df_peak_loads_db["building_id"].astype(
1585
        int
1586
    )
1587
    df_peak_loads_db["sector"] = "residential+cts"
1588
    # From MW to W
1589
    df_peak_loads_db["peak_load_in_w"] = (
1590
        df_peak_loads_db["peak_load_in_w"] * 1e6
1591
    )
1592
    write_table_to_postgres(df_peak_loads_db, BuildingHeatPeakLoads, drop=drop)
1593
1594
    dtypes = {
1595
        column.key: column.type
1596
        for column in EgonEtragoTimeseriesIndividualHeating.__table__.columns
1597
    }
1598
    df_heat_mvgd_ts_db = df_heat_mvgd_ts_db.loc[:, dtypes.keys()]
1599
1600
    if drop:
1601
        logger.info(
1602
            f"Drop and recreate table "
1603
            f"{EgonEtragoTimeseriesIndividualHeating.__table__.name}."
1604
        )
1605
        EgonEtragoTimeseriesIndividualHeating.__table__.drop(
1606
            bind=engine, checkfirst=True
1607
        )
1608
        EgonEtragoTimeseriesIndividualHeating.__table__.create(
1609
            bind=engine, checkfirst=True
1610
        )
1611
1612
    with db.session_scope() as session:
1613
        df_heat_mvgd_ts_db.to_sql(
1614
            name=EgonEtragoTimeseriesIndividualHeating.__table__.name,
1615
            schema=EgonEtragoTimeseriesIndividualHeating.__table__.schema,
1616
            con=session.connection(),
1617
            if_exists="append",
1618
            method="multi",
1619
            index=False,
1620
            dtype=dtypes,
1621
        )
1622
1623
1624
def export_min_cap_to_csv(df_hp_min_cap_mv_grid_pypsa_eur_sec):
1625
    """Export minimum capacity of heat pumps for pypsa eur sec to csv"""
1626
1627
    df_hp_min_cap_mv_grid_pypsa_eur_sec.index.name = "mvgd_id"
1628
    df_hp_min_cap_mv_grid_pypsa_eur_sec = (
1629
        df_hp_min_cap_mv_grid_pypsa_eur_sec.to_frame(
1630
            name="min_hp_capacity"
1631
        ).reset_index()
1632
    )
1633
1634
    folder = Path(".") / "input-pypsa-eur-sec"
1635
    file = folder / "minimum_hp_capacity_mv_grid_100RE.csv"
1636
    # Create the folder, if it does not exist already
1637
    if not os.path.exists(folder):
1638
        os.mkdir(folder)
1639
    if not file.is_file():
1640
        logger.info(f"Create {file}")
1641
        df_hp_min_cap_mv_grid_pypsa_eur_sec.to_csv(
1642
            file, mode="w", header=True
1643
        )
1644
    else:
1645
        df_hp_min_cap_mv_grid_pypsa_eur_sec.to_csv(
1646
            file, mode="a", header=False
1647
        )
1648
1649
1650
def delete_pypsa_eur_sec_csv_file():
1651
    """Delete pypsa eur sec minimum heat pump capacity csv before new run"""
1652
1653
    folder = Path(".") / "input-pypsa-eur-sec"
1654
    file = folder / "minimum_hp_capacity_mv_grid_100RE.csv"
1655
    if file.is_file():
1656
        logger.info(f"Delete {file}")
1657
        os.remove(file)
1658
1659
1660
def catch_missing_buidings(buildings_decentral_heating, peak_load):
1661
    """
1662
    Check for missing buildings and reduce the list of buildings with
1663
    decentral heating if no peak loads available. This should only happen
1664
    in case of cutout SH
1665
1666
    Parameters
1667
    -----------
1668
    buildings_decentral_heating : list(int)
1669
        Array or list of buildings with decentral heating
1670
1671
    peak_load : pd.Series
1672
        Peak loads of all building within the mvgd
1673
1674
    """
1675
    # Catch missing buildings key error
1676
    # should only happen within cutout SH
1677
    if (
1678
        not all(buildings_decentral_heating.isin(peak_load.index))
1679
        and config.settings()["egon-data"]["--dataset-boundary"]
1680
        == "Schleswig-Holstein"
1681
    ):
1682
        diff = buildings_decentral_heating.difference(peak_load.index)
1683
        logger.warning(
1684
            f"Dropped {len(diff)} building ids due to missing peak "
1685
            f"loads. {len(buildings_decentral_heating)} left."
1686
        )
1687
        logger.info(f"Dropped buildings: {diff.values}")
1688
        buildings_decentral_heating = buildings_decentral_heating.drop(diff)
1689
1690
    return buildings_decentral_heating
1691
1692
1693
def determine_hp_cap_peak_load_mvgd_ts_2035(mvgd_ids):
1694
    """
1695
    Main function to determine HP capacity per building in eGon2035 scenario.
1696
    Further, creates heat demand time series for all buildings with heat pumps
1697
    in MV grid, as well as for all buildings with gas boilers, used in eTraGo.
1698
1699
    Parameters
1700
    -----------
1701
    mvgd_ids : list(int)
1702
        List of MV grid IDs to determine data for.
1703
1704
    """
1705
1706
    # ========== Register np datatypes with SQLA ==========
1707
    register_adapter(np.float64, adapt_numpy_float64)
1708
    register_adapter(np.int64, adapt_numpy_int64)
1709
    # =====================================================
1710
1711
    df_peak_loads_db = pd.DataFrame()
1712
    df_hp_cap_per_building_2035_db = pd.DataFrame()
1713
    df_heat_mvgd_ts_db = pd.DataFrame()
1714
1715
    for mvgd in mvgd_ids:
1716
1717
        logger.info(f"MVGD={mvgd} | Start")
1718
1719
        # ############# aggregate residential and CTS demand profiles #####
1720
1721
        df_heat_ts = aggregate_residential_and_cts_profiles(
1722
            mvgd, scenario="eGon2035"
1723
        )
1724
1725
        # ##################### determine peak loads ###################
1726
        logger.info(f"MVGD={mvgd} | Determine peak loads.")
1727
1728
        peak_load_2035 = df_heat_ts.max().rename("eGon2035")
1729
1730
        # ######## determine HP capacity per building #########
1731
        logger.info(f"MVGD={mvgd} | Determine HP capacities.")
1732
1733
        buildings_decentral_heating = (
1734
            get_buildings_with_decentral_heat_demand_in_mv_grid(
1735
                mvgd, scenario="eGon2035"
1736
            )
1737
        )
1738
1739
        # Reduce list of decentral heating if no Peak load available
1740
        # TODO maybe remove after succesfull DE run
1741
        # Might be fixed in #990
1742
        buildings_decentral_heating = catch_missing_buidings(
1743
            buildings_decentral_heating, peak_load_2035
1744
        )
1745
1746
        hp_cap_per_building_2035 = (
1747
            determine_hp_cap_buildings_eGon2035_per_mvgd(
1748
                mvgd,
1749
                peak_load_2035,
1750
                buildings_decentral_heating,
1751
            )
1752
        )
1753
        buildings_gas_2035 = pd.Index(buildings_decentral_heating).drop(
1754
            hp_cap_per_building_2035.index
1755
        )
1756
1757
        # ################ aggregated heat profiles ###################
1758
        logger.info(f"MVGD={mvgd} | Aggregate heat profiles.")
1759
1760
        df_mvgd_ts_2035_hp = df_heat_ts.loc[
1761
            :,
1762
            hp_cap_per_building_2035.index,
1763
        ].sum(axis=1)
1764
1765
        # heat demand time series for buildings with gas boiler
1766
        df_mvgd_ts_2035_gas = df_heat_ts.loc[:, buildings_gas_2035].sum(axis=1)
1767
1768
        df_heat_mvgd_ts = pd.DataFrame(
1769
            data={
1770
                "carrier": ["heat_pump", "CH4"],
1771
                "bus_id": mvgd,
1772
                "scenario": ["eGon2035", "eGon2035"],
1773
                "dist_aggregated_mw": [
1774
                    df_mvgd_ts_2035_hp.to_list(),
1775
                    df_mvgd_ts_2035_gas.to_list(),
1776
                ],
1777
            }
1778
        )
1779
1780
        # ################ collect results ##################
1781
        logger.info(f"MVGD={mvgd} | Collect results.")
1782
1783
        df_peak_loads_db = pd.concat(
1784
            [df_peak_loads_db, peak_load_2035.reset_index()],
1785
            axis=0,
1786
            ignore_index=True,
1787
        )
1788
1789
        df_heat_mvgd_ts_db = pd.concat(
1790
            [df_heat_mvgd_ts_db, df_heat_mvgd_ts], axis=0, ignore_index=True
1791
        )
1792
1793
        df_hp_cap_per_building_2035_db = pd.concat(
1794
            [
1795
                df_hp_cap_per_building_2035_db,
1796
                hp_cap_per_building_2035.reset_index(),
1797
            ],
1798
            axis=0,
1799
        )
1800
1801
    # ################ export to db #######################
1802
    logger.info(f"MVGD={min(mvgd_ids)} : {max(mvgd_ids)} | Write data to db.")
1803
1804
    export_to_db(df_peak_loads_db, df_heat_mvgd_ts_db, drop=False)
1805
1806
    df_hp_cap_per_building_2035_db["scenario"] = "eGon2035"
1807
1808
    # TODO debug duplicated building_ids
1809
    duplicates = df_hp_cap_per_building_2035_db.loc[
1810
        df_hp_cap_per_building_2035_db.duplicated("building_id", keep=False)
1811
    ]
1812
1813
    logger.info(
1814
        f"Dropped duplicated buildings: "
1815
        f"{duplicates.loc[:,['building_id', 'hp_capacity']]}"
1816
    )
1817
1818
    df_hp_cap_per_building_2035_db.drop_duplicates("building_id", inplace=True)
1819
1820
    write_table_to_postgres(
1821
        df_hp_cap_per_building_2035_db,
1822
        EgonHpCapacityBuildings,
1823
        drop=False,
1824
    )
1825
1826
1827
def determine_hp_cap_peak_load_mvgd_ts_pypsa_eur_sec(mvgd_ids):
1828
    """
1829
    Main function to determine minimum required HP capacity in MV for
1830
    pypsa-eur-sec. Further, creates heat demand time series for all buildings
1831
    with heat pumps in MV grid in eGon100RE scenario, used in eTraGo.
1832
1833
    Parameters
1834
    -----------
1835
    mvgd_ids : list(int)
1836
        List of MV grid IDs to determine data for.
1837
1838
    """
1839
1840
    # ========== Register np datatypes with SQLA ==========
1841
    register_adapter(np.float64, adapt_numpy_float64)
1842
    register_adapter(np.int64, adapt_numpy_int64)
1843
    # =====================================================
1844
1845
    df_peak_loads_db = pd.DataFrame()
1846
    df_heat_mvgd_ts_db = pd.DataFrame()
1847
    df_hp_min_cap_mv_grid_pypsa_eur_sec = pd.Series(dtype="float64")
1848
1849
    for mvgd in mvgd_ids:
1850
1851
        logger.info(f"MVGD={mvgd} | Start")
1852
1853
        # ############# aggregate residential and CTS demand profiles #####
1854
1855
        df_heat_ts = aggregate_residential_and_cts_profiles(
1856
            mvgd, scenario="eGon100RE"
1857
        )
1858
1859
        # ##################### determine peak loads ###################
1860
        logger.info(f"MVGD={mvgd} | Determine peak loads.")
1861
1862
        peak_load_100RE = df_heat_ts.max().rename("eGon100RE")
1863
1864
        # ######## determine minimum HP capacity pypsa-eur-sec ###########
1865
        logger.info(f"MVGD={mvgd} | Determine minimum HP capacity.")
1866
1867
        buildings_decentral_heating = (
1868
            get_buildings_with_decentral_heat_demand_in_mv_grid(
1869
                mvgd, scenario="eGon100RE"
1870
            )
1871
        )
1872
1873
        # Reduce list of decentral heating if no Peak load available
1874
        # TODO maybe remove after succesfull DE run
1875
        buildings_decentral_heating = catch_missing_buidings(
1876
            buildings_decentral_heating, peak_load_100RE
1877
        )
1878
1879
        hp_min_cap_mv_grid_pypsa_eur_sec = (
1880
            determine_min_hp_cap_buildings_pypsa_eur_sec(
1881
                peak_load_100RE,
1882
                buildings_decentral_heating,
1883
            )
1884
        )
1885
1886
        # ################ aggregated heat profiles ###################
1887
        logger.info(f"MVGD={mvgd} | Aggregate heat profiles.")
1888
1889
        df_mvgd_ts_hp = df_heat_ts.loc[
1890
            :,
1891
            buildings_decentral_heating,
1892
        ].sum(axis=1)
1893
1894
        df_heat_mvgd_ts = pd.DataFrame(
1895
            data={
1896
                "carrier": "heat_pump",
1897
                "bus_id": mvgd,
1898
                "scenario": "eGon100RE",
1899
                "dist_aggregated_mw": [df_mvgd_ts_hp.to_list()],
1900
            }
1901
        )
1902
1903
        # ################ collect results ##################
1904
        logger.info(f"MVGD={mvgd} | Collect results.")
1905
1906
        df_peak_loads_db = pd.concat(
1907
            [df_peak_loads_db, peak_load_100RE.reset_index()],
1908
            axis=0,
1909
            ignore_index=True,
1910
        )
1911
1912
        df_heat_mvgd_ts_db = pd.concat(
1913
            [df_heat_mvgd_ts_db, df_heat_mvgd_ts], axis=0, ignore_index=True
1914
        )
1915
1916
        df_hp_min_cap_mv_grid_pypsa_eur_sec.loc[
1917
            mvgd
1918
        ] = hp_min_cap_mv_grid_pypsa_eur_sec
1919
1920
    # ################ export to db and csv ######################
1921
    logger.info(f"MVGD={min(mvgd_ids)} : {max(mvgd_ids)} | Write data to db.")
1922
1923
    export_to_db(df_peak_loads_db, df_heat_mvgd_ts_db, drop=False)
1924
1925
    logger.info(
1926
        f"MVGD={min(mvgd_ids)} : {max(mvgd_ids)} | Write "
1927
        f"pypsa-eur-sec min "
1928
        f"HP capacities to csv."
1929
    )
1930
    export_min_cap_to_csv(df_hp_min_cap_mv_grid_pypsa_eur_sec)
1931
1932
1933
def split_mvgds_into_bulks(n, max_n, func):
1934
    """
1935
    Generic function to split task into multiple parallel tasks,
1936
    dividing the number of MVGDs into even bulks.
1937
1938
    Parameters
1939
    -----------
1940
    n : int
1941
        Number of bulk
1942
    max_n: int
1943
        Maximum number of bulks
1944
    func : function
1945
        The funnction which is then called with the list of MVGD as
1946
        parameter.
1947
    """
1948
1949
    with db.session_scope() as session:
1950
        query = (
1951
            session.query(
1952
                MapZensusGridDistricts.bus_id,
1953
            )
1954
            .filter(
1955
                MapZensusGridDistricts.zensus_population_id
1956
                == EgonPetaHeat.zensus_population_id
1957
            )
1958
            .distinct(MapZensusGridDistricts.bus_id)
1959
        )
1960
        mvgd_ids = pd.read_sql(
1961
            query.statement, query.session.bind, index_col=None
1962
        )
1963
1964
    mvgd_ids = mvgd_ids.sort_values("bus_id").reset_index(drop=True)
1965
1966
    mvgd_ids = np.array_split(mvgd_ids["bus_id"].values, max_n)
1967
    # Only take split n
1968
    mvgd_ids = mvgd_ids[n]
1969
1970
    logger.info(f"Bulk takes care of MVGD: {min(mvgd_ids)} : {max(mvgd_ids)}")
1971
    func(mvgd_ids)
1972
1973
1974
def delete_hp_capacity(scenario):
1975
    """Remove all hp capacities for the selected scenario
1976
1977
    Parameters
1978
    -----------
1979
    scenario : string
1980
        Either eGon2035 or eGon100RE
1981
1982
    """
1983
1984
    with db.session_scope() as session:
1985
        # Buses
1986
        session.query(EgonHpCapacityBuildings).filter(
1987
            EgonHpCapacityBuildings.scenario == scenario
1988
        ).delete(synchronize_session=False)
1989
1990
1991
def delete_mvgd_ts(scenario):
1992
    """Remove all hp capacities for the selected scenario
1993
1994
    Parameters
1995
    -----------
1996
    scenario : string
1997
        Either eGon2035 or eGon100RE
1998
1999
    """
2000
2001
    with db.session_scope() as session:
2002
        # Buses
2003
        session.query(EgonEtragoTimeseriesIndividualHeating).filter(
2004
            EgonEtragoTimeseriesIndividualHeating.scenario == scenario
2005
        ).delete(synchronize_session=False)
2006
2007
2008
def delete_hp_capacity_100RE():
2009
    """Remove all hp capacities for the selected eGon100RE"""
2010
    EgonHpCapacityBuildings.__table__.create(bind=engine, checkfirst=True)
2011
    delete_hp_capacity(scenario="eGon100RE")
2012
2013
2014
def delete_hp_capacity_2035():
2015
    """Remove all hp capacities for the selected eGon2035"""
2016
    EgonHpCapacityBuildings.__table__.create(bind=engine, checkfirst=True)
2017
    delete_hp_capacity(scenario="eGon2035")
2018
2019
2020
def delete_mvgd_ts_2035():
2021
    """Remove all mvgd ts for the selected eGon2035"""
2022
    EgonEtragoTimeseriesIndividualHeating.__table__.create(
2023
        bind=engine, checkfirst=True
2024
    )
2025
    delete_mvgd_ts(scenario="eGon2035")
2026
2027
2028
def delete_mvgd_ts_100RE():
2029
    """Remove all mvgd ts for the selected eGon100RE"""
2030
    EgonEtragoTimeseriesIndividualHeating.__table__.create(
2031
        bind=engine, checkfirst=True
2032
    )
2033
    delete_mvgd_ts(scenario="eGon100RE")
2034
2035
2036
def delete_heat_peak_loads_2035():
2037
    """Remove all heat peak loads for eGon2035."""
2038
    BuildingHeatPeakLoads.__table__.create(bind=engine, checkfirst=True)
2039
    with db.session_scope() as session:
2040
        # Buses
2041
        session.query(BuildingHeatPeakLoads).filter(
2042
            BuildingHeatPeakLoads.scenario == "eGon2035"
2043
        ).delete(synchronize_session=False)
2044
2045
2046
def delete_heat_peak_loads_100RE():
2047
    """Remove all heat peak loads for eGon100RE."""
2048
    BuildingHeatPeakLoads.__table__.create(bind=engine, checkfirst=True)
2049
    with db.session_scope() as session:
2050
        # Buses
2051
        session.query(BuildingHeatPeakLoads).filter(
2052
            BuildingHeatPeakLoads.scenario == "eGon100RE"
2053
        ).delete(synchronize_session=False)
2054