Passed
Pull Request — dev (#1181)
by
unknown
05:34
created

get_residential_heat_profile_ids()   A

Complexity

Conditions 2

Size

Total Lines 52
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 52
rs 9.304
c 0
b 0
f 0
cc 2
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
from pathlib import Path
183
import os
184
import random
185
186
from airflow.operators.python import PythonOperator
187
from psycopg2.extensions import AsIs, register_adapter
188
from sqlalchemy import ARRAY, REAL, Column, Integer, String
189
from sqlalchemy.ext.declarative import declarative_base
190
import geopandas as gpd
191
import numpy as np
192
import pandas as pd
193
import saio
194
195
from egon.data import config, db, logger
196
from egon.data.datasets import Dataset, wrapped_partial
197
from egon.data.datasets.district_heating_areas import (
198
    MapZensusDistrictHeatingAreas,
199
)
200
from egon.data.datasets.electricity_demand_timeseries.cts_buildings import (
201
    calc_cts_building_profiles,
202
)
203
from egon.data.datasets.electricity_demand_timeseries.mapping import (
204
    EgonMapZensusMvgdBuildings,
205
)
206
from egon.data.datasets.electricity_demand_timeseries.tools import (
207
    write_table_to_postgres,
208
)
209
from egon.data.datasets.emobility.motorized_individual_travel.helpers import (
210
    reduce_mem_usage,
211
)
212
from egon.data.datasets.heat_demand import EgonPetaHeat
213
from egon.data.datasets.heat_demand_timeseries.daily import (
214
    EgonDailyHeatDemandPerClimateZone,
215
    EgonMapZensusClimateZones,
216
)
217
from egon.data.datasets.heat_demand_timeseries.idp_pool import (
218
    EgonHeatTimeseries,
219
)
220
221
# get zensus cells with district heating
222
from egon.data.datasets.zensus_mv_grid_districts import MapZensusGridDistricts
223
224
engine = db.engine()
225
Base = declarative_base()
226
227
scenarios = config.settings()["egon-data"]["--scenarios"]
228
229
230
class EgonEtragoTimeseriesIndividualHeating(Base):
231
    __tablename__ = "egon_etrago_timeseries_individual_heating"
232
    __table_args__ = {"schema": "demand"}
233
    bus_id = Column(Integer, primary_key=True)
234
    scenario = Column(String, primary_key=True)
235
    carrier = Column(String, primary_key=True)
236
    dist_aggregated_mw = Column(ARRAY(REAL))
237
238
239
class EgonHpCapacityBuildings(Base):
240
    __tablename__ = "egon_hp_capacity_buildings"
241
    __table_args__ = {"schema": "demand"}
242
    building_id = Column(Integer, primary_key=True)
243
    scenario = Column(String, primary_key=True)
244
    hp_capacity = Column(REAL)
245
246
247
class HeatPumpsPypsaEur(Dataset):
248
    def __init__(self, dependencies):
249
        def dyn_parallel_tasks_pypsa_eur():
250
            """Dynamically generate tasks
251
            The goal is to speed up tasks by parallelising bulks of mvgds.
252
253
            The number of parallel tasks is defined via parameter
254
            `parallel_tasks` in the dataset config `datasets.yml`.
255
256
            Returns
257
            -------
258
            set of airflow.PythonOperators
259
                The tasks. Each element is of
260
                :func:`egon.data.datasets.heat_supply.individual_heating.
261
                determine_hp_cap_peak_load_mvgd_ts_pypsa_eur`
262
            """
263
            parallel_tasks = config.datasets()["demand_timeseries_mvgd"].get(
264
                "parallel_tasks", 1
265
            )
266
267
            tasks = set()
268
269
            for i in range(parallel_tasks):
270
                tasks.add(
271
                    PythonOperator(
272
                        task_id=(
273
                            f"individual_heating."
274
                            f"determine-hp-capacity-pypsa-eur-"
275
                            f"mvgd-bulk{i}"
276
                        ),
277
                        python_callable=split_mvgds_into_bulks,
278
                        op_kwargs={
279
                            "n": i,
280
                            "max_n": parallel_tasks,
281
                            "func": determine_hp_cap_peak_load_mvgd_ts_pypsa_eur,  # noqa: E501
282
                        },
283
                    )
284
                )
285
            return tasks
286
287
        tasks_HeatPumpsPypsaEur = set()
288
289
        if "eGon100RE" in scenarios:
290
            tasks_HeatPumpsPypsaEur = (
291
                delete_pypsa_eur_sec_csv_file,
292
                delete_mvgd_ts_100RE,
293
                delete_heat_peak_loads_100RE,
294
                {*dyn_parallel_tasks_pypsa_eur()},
295
            )
296
        else:
297
            tasks_HeatPumpsPypsaEur = (
298
                PythonOperator(
299
                    task_id="HeatPumpsPypsaEur_skipped",
300
                    python_callable=skip_task,
301
                    op_kwargs={
302
                        "scn": "eGon100RE",
303
                        "task": "HeatPumpsPypsaEur",
304
                    },
305
                ),
306
            )
307
308
        super().__init__(
309
            name="HeatPumpsPypsaEurSec",
310
            version="0.0.3",
311
            dependencies=dependencies,
312
            tasks=tasks_HeatPumpsPypsaEur,
313
        )
314
315
316
class HeatPumpsStatusQuo(Dataset):
317
    def __init__(self, dependencies):
318
        def dyn_parallel_tasks_status_quo(scenario):
319
            """Dynamically generate tasks
320
321
            The goal is to speed up tasks by parallelising bulks of mvgds.
322
323
            The number of parallel tasks is defined via parameter
324
            `parallel_tasks` in the dataset config `datasets.yml`.
325
326
            Returns
327
            -------
328
            set of airflow.PythonOperators
329
                The tasks. Each element is of
330
                :func:`egon.data.datasets.heat_supply.individual_heating.
331
                determine_hp_cap_peak_load_mvgd_ts_status_quo`
332
            """
333
            parallel_tasks = config.datasets()["demand_timeseries_mvgd"].get(
334
                "parallel_tasks", 1
335
            )
336
337
            tasks = set()
338
339
            for i in range(parallel_tasks):
340
                tasks.add(
341
                    PythonOperator(
342
                        task_id=(
343
                            "individual_heating."
344
                            f"determine-hp-capacity-{scenario}-"
345
                            f"mvgd-bulk{i}"
346
                        ),
347
                        python_callable=split_mvgds_into_bulks,
348
                        op_kwargs={
349
                            "n": i,
350
                            "max_n": parallel_tasks,
351
                            "scenario": scenario,
352
                            "func": determine_hp_cap_peak_load_mvgd_ts_status_quo,
353
                        },
354
                    )
355
                )
356
            return tasks
357
358
        if any("status" in scenario for scenario in config.settings()["egon-data"]["--scenarios"]):
359
            tasks = ()
360
361
            for scenario in config.settings()["egon-data"]["--scenarios"]:
362
                if "status" in scenario:
363
                    postfix = f"_{scenario[-4:]}"
364
365
                    tasks += (
366
                        wrapped_partial(
367
                            delete_heat_peak_loads_status_quo,
368
                            scenario=scenario,
369
                            postfix=postfix,
370
                        ),
371
                        wrapped_partial(
372
                            delete_hp_capacity_status_quo,
373
                            scenario=scenario,
374
                            postfix=postfix,
375
                        ),
376
                        wrapped_partial(
377
                            delete_mvgd_ts_status_quo,
378
                            scenario=scenario,
379
                            postfix=postfix,
380
                        ),
381
                    )
382
383
                    tasks += (
384
                        {*dyn_parallel_tasks_status_quo(scenario)},
385
                    )
386
        else:
387
            tasks = (
388
                PythonOperator(
389
                    task_id="HeatPumpsSQ_skipped",
390
                    python_callable=skip_task,
391
                    op_kwargs={"scn": "sq", "task": "HeatPumpsStatusQuo"},
392
                ),
393
            )
394
395
        super().__init__(
396
            name="HeatPumpsStatusQuo",
397
            version="0.0.4",
398
            dependencies=dependencies,
399
            tasks=tasks,
400
        )
401
402
403
class HeatPumps2035(Dataset):
404
    def __init__(self, dependencies):
405
        def dyn_parallel_tasks_2035():
406
            """Dynamically generate tasks
407
408
            The goal is to speed up tasks by parallelising bulks of mvgds.
409
410
            The number of parallel tasks is defined via parameter
411
            `parallel_tasks` in the dataset config `datasets.yml`.
412
413
            Returns
414
            -------
415
            set of airflow.PythonOperators
416
                The tasks. Each element is of
417
                :func:`egon.data.datasets.heat_supply.individual_heating.
418
                determine_hp_cap_peak_load_mvgd_ts_2035`
419
            """
420
            parallel_tasks = config.datasets()["demand_timeseries_mvgd"].get(
421
                "parallel_tasks", 1
422
            )
423
424
            tasks = set()
425
426
            for i in range(parallel_tasks):
427
                tasks.add(
428
                    PythonOperator(
429
                        task_id=(
430
                            "individual_heating."
431
                            f"determine-hp-capacity-2035-"
432
                            f"mvgd-bulk{i}"
433
                        ),
434
                        python_callable=split_mvgds_into_bulks,
435
                        op_kwargs={
436
                            "n": i,
437
                            "max_n": parallel_tasks,
438
                            "func": determine_hp_cap_peak_load_mvgd_ts_2035,
439
                        },
440
                    )
441
                )
442
            return tasks
443
444
        if "eGon2035" in scenarios:
445
            tasks_HeatPumps2035 = (
446
                delete_heat_peak_loads_2035,
447
                delete_hp_capacity_2035,
448
                delete_mvgd_ts_2035,
449
                {*dyn_parallel_tasks_2035()},
450
            )
451
        else:
452
            tasks_HeatPumps2035 = (
453
                PythonOperator(
454
                    task_id="HeatPumps2035_skipped",
455
                    python_callable=skip_task,
456
                    op_kwargs={"scn": "eGon2035", "task": "HeatPumps2035"},
457
                ),
458
            )
459
460
        super().__init__(
461
            name="HeatPumps2035",
462
            version="0.0.3",
463
            dependencies=dependencies,
464
            tasks=tasks_HeatPumps2035,
465
        )
466
467
468
class HeatPumps2050(Dataset):
469
    def __init__(self, dependencies):
470
        tasks_HeatPumps2050 = set()
471
472
        if "eGon100RE" in scenarios:
473
            tasks_HeatPumps2050 = (
474
                delete_hp_capacity_100RE,
475
                determine_hp_cap_buildings_eGon100RE,
476
            )
477
        else:
478
            tasks_HeatPumps2050 = (
479
                PythonOperator(
480
                    task_id="HeatPumps2050_skipped",
481
                    python_callable=skip_task,
482
                    op_kwargs={"scn": "eGon100RE", "task": "HeatPumps2050"},
483
                ),
484
            )
485
486
        super().__init__(
487
            name="HeatPumps2050",
488
            version="0.0.3",
489
            dependencies=dependencies,
490
            tasks=tasks_HeatPumps2050,
491
        )
492
493
494
class BuildingHeatPeakLoads(Base):
495
    __tablename__ = "egon_building_heat_peak_loads"
496
    __table_args__ = {"schema": "demand"}
497
498
    building_id = Column(Integer, primary_key=True)
499
    scenario = Column(String, primary_key=True)
500
    sector = Column(String, primary_key=True)
501
    peak_load_in_w = Column(REAL)
502
503
504
def skip_task(scn=str, task=str):
505
    def not_executed():
506
        logger.info(
507
            f"{scn} is not in the list of scenarios. {task} dataset is skipped."
508
        )
509
510
    return not_executed
511
512
513
def adapt_numpy_float64(numpy_float64):
514
    return AsIs(numpy_float64)
515
516
517
def adapt_numpy_int64(numpy_int64):
518
    return AsIs(numpy_int64)
519
520
521
def cascade_per_technology(
522
    heat_per_mv,
523
    technologies,
524
    scenario,
525
    distribution_level,
526
    max_size_individual_chp=0.05,
527
):
528
    """Add plants for individual heat.
529
    Currently only on mv grid district level.
530
531
    Parameters
532
    ----------
533
    mv_grid_districts : geopandas.geodataframe.GeoDataFrame
534
        MV grid districts including the heat demand
535
    technologies : pandas.DataFrame
536
        List of supply technologies and their parameters
537
    scenario : str
538
        Name of the scenario
539
    max_size_individual_chp : float
540
        Maximum capacity of an individual chp in MW
541
    Returns
542
    -------
543
    mv_grid_districts : geopandas.geodataframe.GeoDataFrame
544
        MV grid district which need additional individual heat supply
545
    technologies : pandas.DataFrame
546
        List of supply technologies and their parameters
547
    append_df : pandas.DataFrame
548
        List of plants per mv grid for the selected technology
549
550
    """
551
    sources = config.datasets()["heat_supply"]["sources"]
552
553
    tech = technologies[technologies.priority == technologies.priority.max()]
554
555
    # Distribute heat pumps linear to remaining demand.
556
    if tech.index == "heat_pump":
557
        if distribution_level == "federal_state":
558
            # Select target values per federal state
559
            target = db.select_dataframe(
560
                f"""
561
                    SELECT DISTINCT ON (gen) gen as state, capacity
562
                    FROM {sources['scenario_capacities']['schema']}.
563
                    {sources['scenario_capacities']['table']} a
564
                    JOIN {sources['federal_states']['schema']}.
565
                    {sources['federal_states']['table']} b
566
                    ON a.nuts = b.nuts
567
                    WHERE scenario_name = '{scenario}'
568
                    AND carrier = 'residential_rural_heat_pump'
569
                    """,
570
                index_col="state",
571
            )
572
573
            heat_per_mv["share"] = heat_per_mv.groupby(
574
                "state"
575
            ).remaining_demand.apply(lambda grp: grp / grp.sum())
576
577
            append_df = (
578
                heat_per_mv["share"]
579
                .mul(target.capacity[heat_per_mv["state"]].values)
580
                .reset_index()
581
            )
582
        else:
583
            # Select target value for Germany
584
            target = db.select_dataframe(
585
                f"""
586
                    SELECT SUM(capacity) AS capacity
587
                    FROM {sources['scenario_capacities']['schema']}.
588
                    {sources['scenario_capacities']['table']} a
589
                    WHERE scenario_name = '{scenario}'
590
                    AND carrier = 'residential_rural_heat_pump'
591
                    """
592
            )
593
594
            heat_per_mv["share"] = (
595
                heat_per_mv.remaining_demand
596
                / heat_per_mv.remaining_demand.sum()
597
            )
598
599
            append_df = (
600
                heat_per_mv["share"].mul(target.capacity[0]).reset_index()
601
            )
602
603
        append_df.rename(
604
            {"bus_id": "mv_grid_id", "share": "capacity"}, axis=1, inplace=True
605
        )
606
607
    elif tech.index == "gas_boiler":
608
        append_df = pd.DataFrame(
609
            data={
610
                "capacity": heat_per_mv.remaining_demand.div(
611
                    tech.estimated_flh.values[0]
612
                ),
613
                "carrier": "residential_rural_gas_boiler",
614
                "mv_grid_id": heat_per_mv.index,
615
                "scenario": scenario,
616
            }
617
        )
618
619
    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...
620
        append_df["carrier"] = tech.index[0]
621
        heat_per_mv.loc[
622
            append_df.mv_grid_id, "remaining_demand"
623
        ] -= append_df.set_index("mv_grid_id").capacity.mul(
624
            tech.estimated_flh.values[0]
625
        )
626
627
    heat_per_mv = heat_per_mv[heat_per_mv.remaining_demand >= 0]
628
629
    technologies = technologies.drop(tech.index)
630
631
    return heat_per_mv, technologies, append_df
632
633
634
def cascade_heat_supply_indiv(scenario, distribution_level, plotting=True):
635
    """Assigns supply strategy for individual heating in four steps.
636
637
    1.) all small scale CHP are connected.
638
    2.) If this is not suitable, the mv grid is also supplied by heat pumps.
639
    3.) The last option are individual gas boilers.
640
641
    Parameters
642
    ----------
643
    scenario : str
644
        Name of scenario
645
    plotting : bool, optional
646
        Choose if individual heating supply is plotted. The default is True.
647
648
    Returns
649
    -------
650
    resulting_capacities : pandas.DataFrame
651
        List of plants per mv grid
652
653
    """
654
655
    sources = config.datasets()["heat_supply"]["sources"]
656
657
    # Select residential heat demand per mv grid district and federal state
658
    heat_per_mv = db.select_geodataframe(
659
        f"""
660
        SELECT d.bus_id as bus_id, SUM(demand) as demand,
661
        c.vg250_lan as state, d.geom
662
        FROM {sources['heat_demand']['schema']}.
663
        {sources['heat_demand']['table']} a
664
        JOIN {sources['map_zensus_grid']['schema']}.
665
        {sources['map_zensus_grid']['table']} b
666
        ON a.zensus_population_id = b.zensus_population_id
667
        JOIN {sources['map_vg250_grid']['schema']}.
668
        {sources['map_vg250_grid']['table']} c
669
        ON b.bus_id = c.bus_id
670
        JOIN {sources['mv_grids']['schema']}.
671
        {sources['mv_grids']['table']} d
672
        ON d.bus_id = c.bus_id
673
        WHERE scenario = '{scenario}'
674
        AND a.zensus_population_id NOT IN (
675
            SELECT zensus_population_id
676
            FROM {sources['map_dh']['schema']}.{sources['map_dh']['table']}
677
            WHERE scenario = '{scenario}')
678
        GROUP BY d.bus_id, vg250_lan, geom
679
        """,
680
        index_col="bus_id",
681
    )
682
683
    # Store geometry of mv grid
684
    geom_mv = heat_per_mv.geom.centroid.copy()
685
686
    # Initalize Dataframe for results
687
    resulting_capacities = pd.DataFrame(
688
        columns=["mv_grid_id", "carrier", "capacity"]
689
    )
690
691
    # Set technology data according to
692
    # http://www.wbzu.de/seminare/infopool/infopool-bhkw
693
    if scenario == "eGon2035":
694
        technologies = pd.DataFrame(
695
            index=["heat_pump", "gas_boiler"],
696
            columns=["estimated_flh", "priority"],
697
            data={"estimated_flh": [4000, 8000], "priority": [2, 1]},
698
        )
699
    elif scenario == "eGon100RE":
700
        technologies = pd.DataFrame(
701
            index=["heat_pump"],
702
            columns=["estimated_flh", "priority"],
703
            data={"estimated_flh": [4000], "priority": [1]},
704
        )
705
    elif "status" in scenario:
706
        technologies = pd.DataFrame(
707
            index=["heat_pump"],
708
            columns=["estimated_flh", "priority"],
709
            data={"estimated_flh": [4000], "priority": [1]},
710
        )
711
    else:
712
        raise ValueError(f"{scenario=} is not valid.")
713
714
    # In the beginning, the remaining demand equals demand
715
    heat_per_mv["remaining_demand"] = heat_per_mv["demand"]
716
717
    # Connect new technologies, if there is still heat demand left
718
    while (len(technologies) > 0) and (len(heat_per_mv) > 0):
719
        # Attach new supply technology
720
        heat_per_mv, technologies, append_df = cascade_per_technology(
721
            heat_per_mv, technologies, scenario, distribution_level
722
        )
723
        # Collect resulting capacities
724
        resulting_capacities = pd.concat(
725
            [resulting_capacities, append_df], ignore_index=True
726
        )
727
728
    if plotting:
729
        plot_heat_supply(resulting_capacities)
730
731
    return gpd.GeoDataFrame(
732
        resulting_capacities,
733
        geometry=geom_mv[resulting_capacities.mv_grid_id].values,
734
    )
735
736
737 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...
738
    """
739
    Retrieve annual peta heat demand for residential buildings for either
740
    eGon2035 or eGon100RE scenario.
741
742
    Parameters
743
    ----------
744
    mvgd : int
745
        MV grid ID.
746
    scenario : str
747
        Possible options are eGon2035 or eGon100RE
748
749
    Returns
750
    -------
751
    df_peta_demand : pd.DataFrame
752
        Annual residential heat demand per building and scenario. Columns of
753
        the dataframe are zensus_population_id and demand.
754
755
    """
756
757
    with db.session_scope() as session:
758
        query = (
759
            session.query(
760
                MapZensusGridDistricts.zensus_population_id,
761
                EgonPetaHeat.demand,
762
            )
763
            .filter(MapZensusGridDistricts.bus_id == mvgd)
764
            .filter(
765
                MapZensusGridDistricts.zensus_population_id
766
                == EgonPetaHeat.zensus_population_id
767
            )
768
            .filter(
769
                EgonPetaHeat.sector == "residential",
770
                EgonPetaHeat.scenario == scenario,
771
            )
772
        )
773
774
        df_peta_demand = pd.read_sql(
775
            query.statement, query.session.bind, index_col=None
776
        )
777
778
    return df_peta_demand
779
780
781
def get_residential_heat_profile_ids(mvgd):
782
    """
783
    Retrieve 365 daily heat profiles ids per residential building and selected
784
    mvgd.
785
786
    Parameters
787
    ----------
788
    mvgd : int
789
        ID of MVGD
790
791
    Returns
792
    -------
793
    df_profiles_ids : pd.DataFrame
794
        Residential daily heat profile ID's per building. Columns of the
795
        dataframe are zensus_population_id, building_id,
796
        selected_idp_profiles, buildings and day_of_year.
797
798
    """
799
    with db.session_scope() as session:
800
        query = (
801
            session.query(
802
                MapZensusGridDistricts.zensus_population_id,
803
                EgonHeatTimeseries.building_id,
804
                EgonHeatTimeseries.selected_idp_profiles,
805
            )
806
            .filter(MapZensusGridDistricts.bus_id == mvgd)
807
            .filter(
808
                MapZensusGridDistricts.zensus_population_id
809
                == EgonHeatTimeseries.zensus_population_id
810
            )
811
        )
812
813
        df_profiles_ids = pd.read_sql(
814
            query.statement, query.session.bind, index_col=None
815
        )
816
    # Add building count per cell
817
    df_profiles_ids = pd.merge(
818
        left=df_profiles_ids,
819
        right=df_profiles_ids.groupby("zensus_population_id")["building_id"]
820
        .count()
821
        .rename("buildings"),
822
        left_on="zensus_population_id",
823
        right_index=True,
824
    )
825
826
    # unnest array of ids per building
827
    df_profiles_ids = df_profiles_ids.explode("selected_idp_profiles")
828
    # add day of year column by order of list
829
    df_profiles_ids["day_of_year"] = (
830
        df_profiles_ids.groupby("building_id").cumcount() + 1
831
    )
832
    return df_profiles_ids
833
834
835
def get_daily_profiles(profile_ids):
836
    """
837
    Parameters
838
    ----------
839
    profile_ids : list(int)
840
        daily heat profile ID's
841
842
    Returns
843
    -------
844
    df_profiles : pd.DataFrame
845
        Residential daily heat profiles. Columns of the dataframe are idp,
846
        house, temperature_class and hour.
847
848
    """
849
850
    saio.register_schema("demand", db.engine())
851
    from saio.demand import egon_heat_idp_pool
852
853
    with db.session_scope() as session:
854
        query = session.query(egon_heat_idp_pool).filter(
855
            egon_heat_idp_pool.index.in_(profile_ids)
856
        )
857
858
        df_profiles = pd.read_sql(
859
            query.statement, query.session.bind, index_col="index"
860
        )
861
862
    # unnest array of profile values per id
863
    df_profiles = df_profiles.explode("idp")
864
    # Add column for hour of day
865
    df_profiles["hour"] = df_profiles.groupby(axis=0, level=0).cumcount() + 1
866
867
    return df_profiles
868
869
870
def get_daily_demand_share(mvgd):
871
    """per census cell
872
    Parameters
873
    ----------
874
    mvgd : int
875
        MVGD id
876
877
    Returns
878
    -------
879
    df_daily_demand_share : pd.DataFrame
880
        Daily annual demand share per cencus cell. Columns of the dataframe
881
        are zensus_population_id, day_of_year and daily_demand_share.
882
883
    """
884
885
    with db.session_scope() as session:
886
        query = session.query(
887
            MapZensusGridDistricts.zensus_population_id,
888
            EgonDailyHeatDemandPerClimateZone.day_of_year,
889
            EgonDailyHeatDemandPerClimateZone.daily_demand_share,
890
        ).filter(
891
            EgonMapZensusClimateZones.climate_zone
892
            == EgonDailyHeatDemandPerClimateZone.climate_zone,
893
            MapZensusGridDistricts.zensus_population_id
894
            == EgonMapZensusClimateZones.zensus_population_id,
895
            MapZensusGridDistricts.bus_id == mvgd,
896
        )
897
898
        df_daily_demand_share = pd.read_sql(
899
            query.statement, query.session.bind, index_col=None
900
        )
901
    return df_daily_demand_share
902
903
904
def calc_residential_heat_profiles_per_mvgd(mvgd, scenario):
905
    """
906
    Gets residential heat profiles per building in MV grid for either eGon2035
907
    or eGon100RE scenario.
908
909
    Parameters
910
    ----------
911
    mvgd : int
912
        MV grid ID.
913
    scenario : str
914
        Possible options are eGon2035 or eGon100RE.
915
916
    Returns
917
    --------
918
    pd.DataFrame
919
        Heat demand profiles of buildings. Columns are:
920
            * zensus_population_id : int
921
                Zensus cell ID building is in.
922
            * building_id : int
923
                ID of building.
924
            * day_of_year : int
925
                Day of the year (1 - 365).
926
            * hour : int
927
                Hour of the day (1 - 24).
928
            * demand_ts : float
929
                Building's residential heat demand in MW, for specified hour
930
                of the year (specified through columns `day_of_year` and
931
                `hour`).
932
    """
933
934
    columns = [
935
        "zensus_population_id",
936
        "building_id",
937
        "day_of_year",
938
        "hour",
939
        "demand_ts",
940
    ]
941
942
    df_peta_demand = get_peta_demand(mvgd, scenario)
943
    df_peta_demand = reduce_mem_usage(df_peta_demand)
944
945
    # TODO maybe return empty dataframe
946
    if df_peta_demand.empty:
947
        logger.info(f"No demand for MVGD: {mvgd}")
948
        return pd.DataFrame(columns=columns)
949
950
    df_profiles_ids = get_residential_heat_profile_ids(mvgd)
951
952
    if df_profiles_ids.empty:
953
        logger.info(f"No profiles for MVGD: {mvgd}")
954
        return pd.DataFrame(columns=columns)
955
956
    df_profiles = get_daily_profiles(
957
        df_profiles_ids["selected_idp_profiles"].unique()
958
    )
959
960
    df_daily_demand_share = get_daily_demand_share(mvgd)
961
962
    # Merge profile ids to peta demand by zensus_population_id
963
    df_profile_merge = pd.merge(
964
        left=df_peta_demand, right=df_profiles_ids, on="zensus_population_id"
965
    )
966
967
    df_profile_merge.demand = df_profile_merge.demand.div(
968
        df_profile_merge.buildings
969
    )
970
    df_profile_merge.drop("buildings", axis="columns", inplace=True)
971
972
    # Merge daily demand to daily profile ids by zensus_population_id and day
973
    df_profile_merge = pd.merge(
974
        left=df_profile_merge,
975
        right=df_daily_demand_share,
976
        on=["zensus_population_id", "day_of_year"],
977
    )
978
    df_profile_merge.demand = df_profile_merge.demand.mul(
979
        df_profile_merge.daily_demand_share
980
    )
981
    df_profile_merge.drop("daily_demand_share", axis="columns", inplace=True)
982
    df_profile_merge = reduce_mem_usage(df_profile_merge)
983
984
    # Merge daily profiles by profile id
985
    df_profile_merge = pd.merge(
986
        left=df_profile_merge,
987
        right=df_profiles[["idp", "hour"]],
988
        left_on="selected_idp_profiles",
989
        right_index=True,
990
    )
991
    df_profile_merge = reduce_mem_usage(df_profile_merge)
992
993
    df_profile_merge.demand = df_profile_merge.demand.mul(
994
        df_profile_merge.idp.astype(float)
995
    )
996
    df_profile_merge.drop("idp", axis="columns", inplace=True)
997
998
    df_profile_merge.rename(
999
        {"demand": "demand_ts"}, axis="columns", inplace=True
1000
    )
1001
1002
    df_profile_merge = reduce_mem_usage(df_profile_merge)
1003
1004
    return df_profile_merge.loc[:, columns]
1005
1006
1007 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...
1008
    from matplotlib import pyplot as plt
1009
1010
    mv_grids = db.select_geodataframe(
1011
        """
1012
        SELECT * FROM grid.egon_mv_grid_district
1013
        """,
1014
        index_col="bus_id",
1015
    )
1016
1017
    for c in ["CHP", "heat_pump"]:
1018
        mv_grids[c] = (
1019
            resulting_capacities[resulting_capacities.carrier == c]
1020
            .set_index("mv_grid_id")
1021
            .capacity
1022
        )
1023
1024
        fig, ax = plt.subplots(1, 1)
1025
        mv_grids.boundary.plot(linewidth=0.2, ax=ax, color="black")
1026
        mv_grids.plot(
1027
            ax=ax,
1028
            column=c,
1029
            cmap="magma_r",
1030
            legend=True,
1031
            legend_kwds={
1032
                "label": f"Installed {c} in MW",
1033
                "orientation": "vertical",
1034
            },
1035
        )
1036
        plt.savefig(f"plots/individual_heat_supply_{c}.png", dpi=300)
1037
1038
1039
def get_zensus_cells_with_decentral_heat_demand_in_mv_grid(
1040
    scenario, mv_grid_id
1041
):
1042
    """
1043
    Returns zensus cell IDs with decentral heating systems in given MV grid.
1044
1045
    As cells with district heating differ between scenarios, this is also
1046
    depending on the scenario.
1047
1048
    Parameters
1049
    -----------
1050
    scenario : str
1051
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
1052
    mv_grid_id : int
1053
        ID of MV grid.
1054
1055
    Returns
1056
    --------
1057
    pd.Index(int)
1058
        Zensus cell IDs (as int) of buildings with decentral heating systems in
1059
        given MV grid. Type is pandas Index to avoid errors later on when it is
1060
        used in a query.
1061
1062
    """
1063
1064
    # get zensus cells in grid
1065
    zensus_population_ids = db.select_dataframe(
1066
        f"""
1067
        SELECT zensus_population_id
1068
        FROM boundaries.egon_map_zensus_grid_districts
1069
        WHERE bus_id = {mv_grid_id}
1070
        """,
1071
        index_col=None,
1072
    ).zensus_population_id.values
1073
1074
    # maybe use adapter
1075
    # convert to pd.Index (otherwise type is np.int64, which will for some
1076
    # reason throw an error when used in a query)
1077
    zensus_population_ids = pd.Index(zensus_population_ids)
1078
1079
    # get zensus cells with district heating
1080
    with db.session_scope() as session:
1081
        query = session.query(
1082
            MapZensusDistrictHeatingAreas.zensus_population_id,
1083
        ).filter(
1084
            MapZensusDistrictHeatingAreas.scenario == scenario,
1085
            MapZensusDistrictHeatingAreas.zensus_population_id.in_(
1086
                zensus_population_ids
1087
            ),
1088
        )
1089
1090
        cells_with_dh = pd.read_sql(
1091
            query.statement, query.session.bind, index_col=None
1092
        ).zensus_population_id.values
1093
1094
    # remove zensus cells with district heating
1095
    zensus_population_ids = zensus_population_ids.drop(
1096
        cells_with_dh, errors="ignore"
1097
    )
1098
    return pd.Index(zensus_population_ids)
1099
1100
1101
def get_residential_buildings_with_decentral_heat_demand_in_mv_grid(
1102
    scenario, mv_grid_id
1103
):
1104
    """
1105
    Returns building IDs of buildings with decentral residential heat demand in
1106
    given MV grid.
1107
1108
    As cells with district heating differ between scenarios, this is also
1109
    depending on the scenario.
1110
1111
    Parameters
1112
    -----------
1113
    scenario : str
1114
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
1115
    mv_grid_id : int
1116
        ID of MV grid.
1117
1118
    Returns
1119
    --------
1120
    pd.Index(int)
1121
        Building IDs (as int) of buildings with decentral heating system in
1122
        given MV grid. Type is pandas Index to avoid errors later on when it is
1123
        used in a query.
1124
1125
    """
1126
    # get zensus cells with decentral heating
1127
    zensus_population_ids = (
1128
        get_zensus_cells_with_decentral_heat_demand_in_mv_grid(
1129
            scenario, mv_grid_id
1130
        )
1131
    )
1132
1133
    # get buildings with decentral heat demand
1134
    saio.register_schema("demand", engine)
1135
    from saio.demand import egon_heat_timeseries_selected_profiles
1136
1137
    with db.session_scope() as session:
1138
        query = session.query(
1139
            egon_heat_timeseries_selected_profiles.building_id,
1140
        ).filter(
1141
            egon_heat_timeseries_selected_profiles.zensus_population_id.in_(
1142
                zensus_population_ids
1143
            )
1144
        )
1145
1146
        buildings_with_heat_demand = pd.read_sql(
1147
            query.statement, query.session.bind, index_col=None
1148
        ).building_id.values
1149
1150
    return pd.Index(buildings_with_heat_demand)
1151
1152
1153
def get_cts_buildings_with_decentral_heat_demand_in_mv_grid(
1154
    scenario, mv_grid_id
1155
):
1156
    """
1157
    Returns building IDs of buildings with decentral CTS heat demand in
1158
    given MV grid.
1159
1160
    As cells with district heating differ between scenarios, this is also
1161
    depending on the scenario.
1162
1163
    Parameters
1164
    -----------
1165
    scenario : str
1166
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
1167
    mv_grid_id : int
1168
        ID of MV grid.
1169
1170
    Returns
1171
    --------
1172
    pd.Index(int)
1173
        Building IDs (as int) of buildings with decentral heating system in
1174
        given MV grid. Type is pandas Index to avoid errors later on when it is
1175
        used in a query.
1176
1177
    """
1178
1179
    # get zensus cells with decentral heating
1180
    zensus_population_ids = (
1181
        get_zensus_cells_with_decentral_heat_demand_in_mv_grid(
1182
            scenario, mv_grid_id
1183
        )
1184
    )
1185
1186
    # get buildings with decentral heat demand
1187
    with db.session_scope() as session:
1188
        query = session.query(EgonMapZensusMvgdBuildings.building_id).filter(
1189
            EgonMapZensusMvgdBuildings.sector == "cts",
1190
            EgonMapZensusMvgdBuildings.zensus_population_id.in_(
1191
                zensus_population_ids
1192
            ),
1193
        )
1194
1195
        buildings_with_heat_demand = pd.read_sql(
1196
            query.statement, query.session.bind, index_col=None
1197
        ).building_id.values
1198
1199
    return pd.Index(buildings_with_heat_demand)
1200
1201
1202
def get_buildings_with_decentral_heat_demand_in_mv_grid(mvgd, scenario):
1203
    """
1204
    Returns building IDs of buildings with decentral heat demand in
1205
    given MV grid.
1206
1207
    As cells with district heating differ between scenarios, this is also
1208
    depending on the scenario. CTS and residential have to be retrieved
1209
    seperatly as some residential buildings only have electricity but no
1210
    heat demand. This does not occure in CTS.
1211
1212
    Parameters
1213
    -----------
1214
    mvgd : int
1215
        ID of MV grid.
1216
    scenario : str
1217
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
1218
1219
    Returns
1220
    --------
1221
    pd.Index(int)
1222
        Building IDs (as int) of buildings with decentral heating system in
1223
        given MV grid. Type is pandas Index to avoid errors later on when it is
1224
        used in a query.
1225
1226
    """
1227
    # get residential buildings with decentral heating systems
1228
    buildings_decentral_heating_res = (
1229
        get_residential_buildings_with_decentral_heat_demand_in_mv_grid(
1230
            scenario, mvgd
1231
        )
1232
    )
1233
1234
    # get CTS buildings with decentral heating systems
1235
    buildings_decentral_heating_cts = (
1236
        get_cts_buildings_with_decentral_heat_demand_in_mv_grid(scenario, mvgd)
1237
    )
1238
1239
    # merge residential and CTS buildings
1240
    buildings_decentral_heating = buildings_decentral_heating_res.union(
1241
        buildings_decentral_heating_cts
1242
    ).unique()
1243
1244
    return buildings_decentral_heating
1245
1246
1247
def get_total_heat_pump_capacity_of_mv_grid(scenario, mv_grid_id):
1248
    """
1249
    Returns total heat pump capacity per grid that was previously defined
1250
    (by NEP or pypsa-eur-sec).
1251
1252
    Parameters
1253
    -----------
1254
    scenario : str
1255
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
1256
    mv_grid_id : int
1257
        ID of MV grid.
1258
1259
    Returns
1260
    --------
1261
    float
1262
        Total heat pump capacity in MW in given MV grid.
1263
1264
    """
1265
    from egon.data.datasets.heat_supply import EgonIndividualHeatingSupply
1266
1267
    with db.session_scope() as session:
1268
        query = (
1269
            session.query(
1270
                EgonIndividualHeatingSupply.mv_grid_id,
1271
                EgonIndividualHeatingSupply.capacity,
1272
            )
1273
            .filter(EgonIndividualHeatingSupply.scenario == scenario)
1274
            .filter(EgonIndividualHeatingSupply.carrier == "heat_pump")
1275
            .filter(EgonIndividualHeatingSupply.mv_grid_id == mv_grid_id)
1276
        )
1277
1278
        hp_cap_mv_grid = pd.read_sql(
1279
            query.statement, query.session.bind, index_col="mv_grid_id"
1280
        )
1281
    if hp_cap_mv_grid.empty:
1282
        return 0.0
1283
    else:
1284
        return hp_cap_mv_grid.capacity.values[0]
1285
1286
1287
def get_heat_peak_demand_per_building(scenario, building_ids):
1288
    """"""
1289
1290
    with db.session_scope() as session:
1291
        query = (
1292
            session.query(
1293
                BuildingHeatPeakLoads.building_id,
1294
                BuildingHeatPeakLoads.peak_load_in_w,
1295
            )
1296
            .filter(BuildingHeatPeakLoads.scenario == scenario)
1297
            .filter(BuildingHeatPeakLoads.building_id.in_(building_ids))
1298
        )
1299
1300
        df_heat_peak_demand = pd.read_sql(
1301
            query.statement, query.session.bind, index_col=None
1302
        )
1303
1304
    # TODO remove check
1305
    if df_heat_peak_demand.duplicated("building_id").any():
1306
        raise ValueError("Duplicate building_id")
1307
1308
    # convert to series and from W to MW
1309
    df_heat_peak_demand = (
1310
        df_heat_peak_demand.set_index("building_id").loc[:, "peak_load_in_w"]
1311
        * 1e6
1312
    )
1313
    return df_heat_peak_demand
1314
1315
1316
def determine_minimum_hp_capacity_per_building(
1317
    peak_heat_demand, flexibility_factor=24 / 18, cop=1.7
1318
):
1319
    """
1320
    Determines minimum required heat pump capacity.
1321
1322
    Parameters
1323
    ----------
1324
    peak_heat_demand : pd.Series
1325
        Series with peak heat demand per building in MW. Index contains the
1326
        building ID.
1327
    flexibility_factor : float
1328
        Factor to overdimension the heat pump to allow for some flexible
1329
        dispatch in times of high heat demand. Per default, a factor of 24/18
1330
        is used, to take into account
1331
1332
    Returns
1333
    -------
1334
    pd.Series
1335
        Pandas series with minimum required heat pump capacity per building in
1336
        MW.
1337
1338
    """
1339
    return peak_heat_demand * flexibility_factor / cop
1340
1341
1342
def determine_buildings_with_hp_in_mv_grid(
1343
    hp_cap_mv_grid, min_hp_cap_per_building
1344
):
1345
    """
1346
    Distributes given total heat pump capacity to buildings based on their peak
1347
    heat demand.
1348
1349
    Parameters
1350
    -----------
1351
    hp_cap_mv_grid : float
1352
        Total heat pump capacity in MW in given MV grid.
1353
    min_hp_cap_per_building : pd.Series
1354
        Pandas series with minimum required heat pump capacity per building
1355
         in MW.
1356
1357
    Returns
1358
    -------
1359
    pd.Index(int)
1360
        Building IDs (as int) of buildings to get heat demand time series for.
1361
1362
    """
1363
    building_ids = min_hp_cap_per_building.index
1364
1365
    # get buildings with PV to give them a higher priority when selecting
1366
    # buildings a heat pump will be allocated to
1367
    saio.register_schema("supply", engine)
1368
    from saio.supply import egon_power_plants_pv_roof_building
1369
1370
    with db.session_scope() as session:
1371
        query = session.query(
1372
            egon_power_plants_pv_roof_building.building_id
1373
        ).filter(
1374
            egon_power_plants_pv_roof_building.building_id.in_(building_ids),
1375
            egon_power_plants_pv_roof_building.scenario == "eGon2035",
1376
        )
1377
1378
        buildings_with_pv = pd.read_sql(
1379
            query.statement, query.session.bind, index_col=None
1380
        ).building_id.values
1381
    # set different weights for buildings with PV and without PV
1382
    weight_with_pv = 1.5
1383
    weight_without_pv = 1.0
1384
    weights = pd.concat(
1385
        [
1386
            pd.DataFrame(
1387
                {"weight": weight_without_pv},
1388
                index=building_ids.drop(buildings_with_pv, errors="ignore"),
1389
            ),
1390
            pd.DataFrame({"weight": weight_with_pv}, index=buildings_with_pv),
1391
        ]
1392
    )
1393
    # normalise weights (probability needs to add up to 1)
1394
    weights.weight = weights.weight / weights.weight.sum()
1395
1396
    # get random order at which buildings are chosen
1397
    np.random.seed(db.credentials()["--random-seed"])
1398
    buildings_with_hp_order = np.random.choice(
1399
        weights.index,
1400
        size=len(weights),
1401
        replace=False,
1402
        p=weights.weight.values,
1403
    )
1404
1405
    # select buildings until HP capacity in MV grid is reached (some rest
1406
    # capacity will remain)
1407
    hp_cumsum = min_hp_cap_per_building.loc[buildings_with_hp_order].cumsum()
1408
    buildings_with_hp = hp_cumsum[hp_cumsum <= hp_cap_mv_grid].index
1409
1410
    # choose random heat pumps until remaining heat pumps are larger than
1411
    # remaining heat pump capacity
1412
    remaining_hp_cap = (
1413
        hp_cap_mv_grid - min_hp_cap_per_building.loc[buildings_with_hp].sum()
1414
    )
1415
    min_cap_buildings_wo_hp = min_hp_cap_per_building.loc[
1416
        building_ids.drop(buildings_with_hp)
1417
    ]
1418
    possible_buildings = min_cap_buildings_wo_hp[
1419
        min_cap_buildings_wo_hp <= remaining_hp_cap
1420
    ].index
1421
    while len(possible_buildings) > 0:
1422
        random.seed(db.credentials()["--random-seed"])
1423
        new_hp_building = random.choice(possible_buildings)
1424
        # add new building to building with HP
1425
        buildings_with_hp = buildings_with_hp.union(
1426
            pd.Index([new_hp_building])
1427
        )
1428
        # determine if there are still possible buildings
1429
        remaining_hp_cap = (
1430
            hp_cap_mv_grid
1431
            - min_hp_cap_per_building.loc[buildings_with_hp].sum()
1432
        )
1433
        min_cap_buildings_wo_hp = min_hp_cap_per_building.loc[
1434
            building_ids.drop(buildings_with_hp)
1435
        ]
1436
        possible_buildings = min_cap_buildings_wo_hp[
1437
            min_cap_buildings_wo_hp <= remaining_hp_cap
1438
        ].index
1439
1440
    return buildings_with_hp
1441
1442
1443
def desaggregate_hp_capacity(min_hp_cap_per_building, hp_cap_mv_grid):
1444
    """
1445
    Desaggregates the required total heat pump capacity to buildings.
1446
1447
    All buildings are previously assigned a minimum required heat pump
1448
    capacity. If the total heat pump capacity exceeds this, larger heat pumps
1449
    are assigned.
1450
1451
    Parameters
1452
    ------------
1453
    min_hp_cap_per_building : pd.Series
1454
        Pandas series with minimum required heat pump capacity per building
1455
         in MW.
1456
    hp_cap_mv_grid : float
1457
        Total heat pump capacity in MW in given MV grid.
1458
1459
    Returns
1460
    --------
1461
    pd.Series
1462
        Pandas series with heat pump capacity per building in MW.
1463
1464
    """
1465
    # distribute remaining capacity to all buildings with HP depending on
1466
    # installed HP capacity
1467
1468
    allocated_cap = min_hp_cap_per_building.sum()
1469
    remaining_cap = hp_cap_mv_grid - allocated_cap
1470
1471
    fac = remaining_cap / allocated_cap
1472
    hp_cap_per_building = (
1473
        min_hp_cap_per_building * fac + min_hp_cap_per_building
1474
    )
1475
    hp_cap_per_building.index.name = "building_id"
1476
1477
    return hp_cap_per_building
1478
1479
1480
def determine_min_hp_cap_buildings_pypsa_eur_sec(
1481
    peak_heat_demand, building_ids
1482
):
1483
    """
1484
    Determines minimum required HP capacity in MV grid in MW as input for
1485
    pypsa-eur-sec.
1486
1487
    Parameters
1488
    ----------
1489
    peak_heat_demand : pd.Series
1490
        Series with peak heat demand per building in MW. Index contains the
1491
        building ID.
1492
    building_ids : pd.Index(int)
1493
        Building IDs (as int) of buildings with decentral heating system in
1494
        given MV grid.
1495
1496
    Returns
1497
    --------
1498
    float
1499
        Minimum required HP capacity in MV grid in MW.
1500
1501
    """
1502
    if len(building_ids) > 0:
1503
        peak_heat_demand = peak_heat_demand.loc[building_ids]
1504
        # determine minimum required heat pump capacity per building
1505
        min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
1506
            peak_heat_demand
1507
        )
1508
        return min_hp_cap_buildings.sum()
1509
    else:
1510
        return 0.0
1511
1512
1513
def determine_hp_cap_buildings_pvbased_per_mvgd(
1514
    scenario, mv_grid_id, peak_heat_demand, building_ids
1515
):
1516
    """
1517
    Determines which buildings in the MV grid will have a HP (buildings with PV
1518
    rooftop are more likely to be assigned) in the eGon2035 scenario, as well
1519
    as their respective HP capacity in MW.
1520
1521
    Parameters
1522
    -----------
1523
    mv_grid_id : int
1524
        ID of MV grid.
1525
    peak_heat_demand : pd.Series
1526
        Series with peak heat demand per building in MW. Index contains the
1527
        building ID.
1528
    building_ids : pd.Index(int)
1529
        Building IDs (as int) of buildings with decentral heating system in
1530
        given MV grid.
1531
1532
    """
1533
1534
    hp_cap_grid = get_total_heat_pump_capacity_of_mv_grid(scenario, mv_grid_id)
1535
1536
    if len(building_ids) > 0 and hp_cap_grid > 0.0:
1537
        peak_heat_demand = peak_heat_demand.loc[building_ids]
1538
1539
        # determine minimum required heat pump capacity per building
1540
        min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
1541
            peak_heat_demand
1542
        )
1543
1544
        # select buildings that will have a heat pump
1545
        buildings_with_hp = determine_buildings_with_hp_in_mv_grid(
1546
            hp_cap_grid, min_hp_cap_buildings
1547
        )
1548
1549
        # distribute total heat pump capacity to all buildings with HP
1550
        hp_cap_per_building = desaggregate_hp_capacity(
1551
            min_hp_cap_buildings.loc[buildings_with_hp], hp_cap_grid
1552
        )
1553
1554
        return hp_cap_per_building.rename("hp_capacity")
1555
1556
    else:
1557
        return pd.Series(dtype="float64").rename("hp_capacity")
1558
1559
1560
def determine_hp_cap_buildings_eGon100RE_per_mvgd(mv_grid_id):
1561
    """
1562
    Determines HP capacity per building in eGon100RE scenario.
1563
1564
    In eGon100RE scenario all buildings without district heating get a heat
1565
    pump.
1566
1567
    Returns
1568
    --------
1569
    pd.Series
1570
        Pandas series with heat pump capacity per building in MW.
1571
1572
    """
1573
1574
    hp_cap_grid = get_total_heat_pump_capacity_of_mv_grid(
1575
        "eGon100RE", mv_grid_id
1576
    )
1577
1578
    if hp_cap_grid > 0.0:
1579
        # get buildings with decentral heating systems
1580
        building_ids = get_buildings_with_decentral_heat_demand_in_mv_grid(
1581
            mv_grid_id, scenario="eGon100RE"
1582
        )
1583
1584
        logger.info(f"MVGD={mv_grid_id} | Get peak loads from DB")
1585
        df_peak_heat_demand = get_heat_peak_demand_per_building(
1586
            "eGon100RE", building_ids
1587
        )
1588
1589
        logger.info(f"MVGD={mv_grid_id} | Determine HP capacities.")
1590
        # determine minimum required heat pump capacity per building
1591
        min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
1592
            df_peak_heat_demand, flexibility_factor=24 / 18, cop=1.7
1593
        )
1594
1595
        logger.info(f"MVGD={mv_grid_id} | Desaggregate HP capacities.")
1596
        # distribute total heat pump capacity to all buildings with HP
1597
        hp_cap_per_building = desaggregate_hp_capacity(
1598
            min_hp_cap_buildings, hp_cap_grid
1599
        )
1600
1601
        return hp_cap_per_building.rename("hp_capacity")
1602
    else:
1603
        return pd.Series(dtype="float64").rename("hp_capacity")
1604
1605
1606
def determine_hp_cap_buildings_eGon100RE():
1607
    """
1608
    Main function to determine HP capacity per building in eGon100RE scenario.
1609
1610
    """
1611
1612
    # ========== Register np datatypes with SQLA ==========
1613
    register_adapter(np.float64, adapt_numpy_float64)
1614
    register_adapter(np.int64, adapt_numpy_int64)
1615
    # =====================================================
1616
1617
    with db.session_scope() as session:
1618
        query = (
1619
            session.query(
1620
                MapZensusGridDistricts.bus_id,
1621
            )
1622
            .filter(
1623
                MapZensusGridDistricts.zensus_population_id
1624
                == EgonPetaHeat.zensus_population_id
1625
            )
1626
            .distinct(MapZensusGridDistricts.bus_id)
1627
        )
1628
        mvgd_ids = pd.read_sql(
1629
            query.statement, query.session.bind, index_col=None
1630
        )
1631
    mvgd_ids = mvgd_ids.sort_values("bus_id")
1632
    mvgd_ids = mvgd_ids["bus_id"].values
1633
1634
    df_hp_cap_per_building_100RE_db = pd.DataFrame(
1635
        columns=["building_id", "hp_capacity"]
1636
    )
1637
1638
    for mvgd_id in mvgd_ids:
1639
        logger.info(f"MVGD={mvgd_id} | Start")
1640
1641
        hp_cap_per_building_100RE = (
1642
            determine_hp_cap_buildings_eGon100RE_per_mvgd(mvgd_id)
1643
        )
1644
1645
        if not hp_cap_per_building_100RE.empty:
1646
            df_hp_cap_per_building_100RE_db = pd.concat(
1647
                [
1648
                    df_hp_cap_per_building_100RE_db,
1649
                    hp_cap_per_building_100RE.reset_index(),
1650
                ],
1651
                axis=0,
1652
            )
1653
1654
    logger.info(f"MVGD={min(mvgd_ids)} : {max(mvgd_ids)} | Write data to db.")
1655
    df_hp_cap_per_building_100RE_db["scenario"] = "eGon100RE"
1656
1657
    EgonHpCapacityBuildings.__table__.create(bind=engine, checkfirst=True)
1658
1659
    write_table_to_postgres(
1660
        df_hp_cap_per_building_100RE_db,
1661
        EgonHpCapacityBuildings,
1662
        drop=False,
1663
    )
1664
1665
1666
def aggregate_residential_and_cts_profiles(mvgd, scenario):
1667
    """
1668
    Gets residential and CTS heat demand profiles per building and aggregates
1669
    them.
1670
1671
    Parameters
1672
    ----------
1673
    mvgd : int
1674
        MV grid ID.
1675
    scenario : str
1676
        Possible options are eGon2035 or eGon100RE.
1677
1678
    Returns
1679
    --------
1680
    pd.DataFrame
1681
        Table of demand profile per building. Column names are building IDs and
1682
        index is hour of the year as int (0-8759).
1683
1684
    """
1685
    # ############### get residential heat demand profiles ###############
1686
    df_heat_ts = calc_residential_heat_profiles_per_mvgd(
1687
        mvgd=mvgd, scenario=scenario
1688
    )
1689
1690
    # pivot to allow aggregation with CTS profiles
1691
    df_heat_ts = df_heat_ts.pivot(
1692
        index=["day_of_year", "hour"],
1693
        columns="building_id",
1694
        values="demand_ts",
1695
    )
1696
    df_heat_ts = df_heat_ts.sort_index().reset_index(drop=True)
1697
1698
    # ############### get CTS heat demand profiles ###############
1699
    heat_demand_cts_ts = calc_cts_building_profiles(
1700
        bus_ids=[mvgd],
1701
        scenario=scenario,
1702
        sector="heat",
1703
    )
1704
1705
    # ############# aggregate residential and CTS demand profiles #############
1706
    df_heat_ts = pd.concat([df_heat_ts, heat_demand_cts_ts], axis=1)
1707
1708
    df_heat_ts = df_heat_ts.groupby(axis=1, level=0).sum()
1709
1710
    return df_heat_ts
1711
1712
1713
def export_to_db(df_peak_loads_db, df_heat_mvgd_ts_db, drop=False):
1714
    """
1715
    Function to export the collected results of all MVGDs per bulk to DB.
1716
1717
        Parameters
1718
    ----------
1719
    df_peak_loads_db : pd.DataFrame
1720
        Table of building peak loads of all MVGDs per bulk
1721
    df_heat_mvgd_ts_db : pd.DataFrame
1722
        Table of all aggregated MVGD profiles per bulk
1723
    drop : boolean
1724
        Drop and recreate table if True
1725
1726
    """
1727
1728
    df_peak_loads_db = df_peak_loads_db.melt(
1729
        id_vars="building_id",
1730
        var_name="scenario",
1731
        value_name="peak_load_in_w",
1732
    )
1733
    df_peak_loads_db["building_id"] = df_peak_loads_db["building_id"].astype(
1734
        int
1735
    )
1736
    df_peak_loads_db["sector"] = "residential+cts"
1737
    # From MW to W
1738
    df_peak_loads_db["peak_load_in_w"] = (
1739
        df_peak_loads_db["peak_load_in_w"] * 1e6
1740
    )
1741
    write_table_to_postgres(df_peak_loads_db, BuildingHeatPeakLoads, drop=drop)
1742
1743
    dtypes = {
1744
        column.key: column.type
1745
        for column in EgonEtragoTimeseriesIndividualHeating.__table__.columns
1746
    }
1747
    df_heat_mvgd_ts_db = df_heat_mvgd_ts_db.loc[:, dtypes.keys()]
1748
1749
    if drop:
1750
        logger.info(
1751
            f"Drop and recreate table "
1752
            f"{EgonEtragoTimeseriesIndividualHeating.__table__.name}."
1753
        )
1754
        EgonEtragoTimeseriesIndividualHeating.__table__.drop(
1755
            bind=engine, checkfirst=True
1756
        )
1757
        EgonEtragoTimeseriesIndividualHeating.__table__.create(
1758
            bind=engine, checkfirst=True
1759
        )
1760
1761
    with db.session_scope() as session:
1762
        df_heat_mvgd_ts_db.to_sql(
1763
            name=EgonEtragoTimeseriesIndividualHeating.__table__.name,
1764
            schema=EgonEtragoTimeseriesIndividualHeating.__table__.schema,
1765
            con=session.connection(),
1766
            if_exists="append",
1767
            method="multi",
1768
            index=False,
1769
            dtype=dtypes,
1770
        )
1771
1772
1773
def export_min_cap_to_csv(df_hp_min_cap_mv_grid_pypsa_eur_sec):
1774
    """Export minimum capacity of heat pumps for pypsa eur sec to csv"""
1775
1776
    df_hp_min_cap_mv_grid_pypsa_eur_sec.index.name = "mvgd_id"
1777
    df_hp_min_cap_mv_grid_pypsa_eur_sec = (
1778
        df_hp_min_cap_mv_grid_pypsa_eur_sec.to_frame(
1779
            name="min_hp_capacity"
1780
        ).reset_index()
1781
    )
1782
1783
    folder = Path(".") / "input-pypsa-eur-sec"
1784
    file = folder / "minimum_hp_capacity_mv_grid_100RE.csv"
1785
    # Create the folder, if it does not exist already
1786
    if not os.path.exists(folder):
1787
        os.mkdir(folder)
1788
    if not file.is_file():
1789
        logger.info(f"Create {file}")
1790
        df_hp_min_cap_mv_grid_pypsa_eur_sec.to_csv(file, mode="w", header=True)
1791
    else:
1792
        df_hp_min_cap_mv_grid_pypsa_eur_sec.to_csv(
1793
            file, mode="a", header=False
1794
        )
1795
1796
1797
def delete_pypsa_eur_sec_csv_file():
1798
    """Delete pypsa eur sec minimum heat pump capacity csv before new run"""
1799
1800
    folder = Path(".") / "input-pypsa-eur-sec"
1801
    file = folder / "minimum_hp_capacity_mv_grid_100RE.csv"
1802
    if file.is_file():
1803
        logger.info(f"Delete {file}")
1804
        os.remove(file)
1805
1806
1807
def catch_missing_buidings(buildings_decentral_heating, peak_load):
1808
    """
1809
    Check for missing buildings and reduce the list of buildings with
1810
    decentral heating if no peak loads available. This should only happen
1811
    in case of cutout SH
1812
1813
    Parameters
1814
    -----------
1815
    buildings_decentral_heating : list(int)
1816
        Array or list of buildings with decentral heating
1817
1818
    peak_load : pd.Series
1819
        Peak loads of all building within the mvgd
1820
1821
    """
1822
    # Catch missing buildings key error
1823
    # should only happen within cutout SH
1824
    if (
1825
        not all(buildings_decentral_heating.isin(peak_load.index))
1826
        and config.settings()["egon-data"]["--dataset-boundary"]
1827
        == "Schleswig-Holstein"
1828
    ):
1829
        diff = buildings_decentral_heating.difference(peak_load.index)
1830
        logger.warning(
1831
            f"Dropped {len(diff)} building ids due to missing peak "
1832
            f"loads. {len(buildings_decentral_heating)} left."
1833
        )
1834
        logger.info(f"Dropped buildings: {diff.values}")
1835
        buildings_decentral_heating = buildings_decentral_heating.drop(diff)
1836
1837
    return buildings_decentral_heating
1838
1839
1840
def determine_hp_cap_peak_load_mvgd_ts_2035(mvgd_ids):
1841
    """
1842
    Main function to determine HP capacity per building in eGon2035 scenario.
1843
    Further, creates heat demand time series for all buildings with heat pumps
1844
    in MV grid, as well as for all buildings with gas boilers, used in eTraGo.
1845
1846
    Parameters
1847
    -----------
1848
    mvgd_ids : list(int)
1849
        List of MV grid IDs to determine data for.
1850
1851
    """
1852
1853
    # ========== Register np datatypes with SQLA ==========
1854
    register_adapter(np.float64, adapt_numpy_float64)
1855
    register_adapter(np.int64, adapt_numpy_int64)
1856
    # =====================================================
1857
1858
    df_peak_loads_db = pd.DataFrame()
1859
    df_hp_cap_per_building_2035_db = pd.DataFrame()
1860
    df_heat_mvgd_ts_db = pd.DataFrame()
1861
1862
    for mvgd in mvgd_ids:
1863
        logger.info(f"MVGD={mvgd} | Start")
1864
1865
        # ############# aggregate residential and CTS demand profiles #####
1866
1867
        df_heat_ts = aggregate_residential_and_cts_profiles(
1868
            mvgd, scenario="eGon2035"
1869
        )
1870
1871
        # ##################### determine peak loads ###################
1872
        logger.info(f"MVGD={mvgd} | Determine peak loads.")
1873
1874
        peak_load_2035 = df_heat_ts.max().rename("eGon2035")
1875
1876
        # ######## determine HP capacity per building #########
1877
        logger.info(f"MVGD={mvgd} | Determine HP capacities.")
1878
1879
        buildings_decentral_heating = (
1880
            get_buildings_with_decentral_heat_demand_in_mv_grid(
1881
                mvgd, scenario="eGon2035"
1882
            )
1883
        )
1884
1885
        # Reduce list of decentral heating if no Peak load available
1886
        # TODO maybe remove after succesfull DE run
1887
        # Might be fixed in #990
1888
        buildings_decentral_heating = catch_missing_buidings(
1889
            buildings_decentral_heating, peak_load_2035
1890
        )
1891
1892
        hp_cap_per_building_2035 = determine_hp_cap_buildings_pvbased_per_mvgd(
1893
            "eGon2035",
1894
            mvgd,
1895
            peak_load_2035,
1896
            buildings_decentral_heating,
1897
        )
1898
        buildings_gas_2035 = pd.Index(buildings_decentral_heating).drop(
1899
            hp_cap_per_building_2035.index
1900
        )
1901
1902
        # ################ aggregated heat profiles ###################
1903
        logger.info(f"MVGD={mvgd} | Aggregate heat profiles.")
1904
1905
        df_mvgd_ts_2035_hp = df_heat_ts.loc[
1906
            :,
1907
            hp_cap_per_building_2035.index,
1908
        ].sum(axis=1)
1909
1910
        # heat demand time series for buildings with gas boiler
1911
        df_mvgd_ts_2035_gas = df_heat_ts.loc[:, buildings_gas_2035].sum(axis=1)
1912
1913
        df_heat_mvgd_ts = pd.DataFrame(
1914
            data={
1915
                "carrier": ["heat_pump", "CH4"],
1916
                "bus_id": mvgd,
1917
                "scenario": ["eGon2035", "eGon2035"],
1918
                "dist_aggregated_mw": [
1919
                    df_mvgd_ts_2035_hp.to_list(),
1920
                    df_mvgd_ts_2035_gas.to_list(),
1921
                ],
1922
            }
1923
        )
1924
1925
        # ################ collect results ##################
1926
        logger.info(f"MVGD={mvgd} | Collect results.")
1927
1928
        df_peak_loads_db = pd.concat(
1929
            [df_peak_loads_db, peak_load_2035.reset_index()],
1930
            axis=0,
1931
            ignore_index=True,
1932
        )
1933
1934
        df_heat_mvgd_ts_db = pd.concat(
1935
            [df_heat_mvgd_ts_db, df_heat_mvgd_ts], axis=0, ignore_index=True
1936
        )
1937
1938
        df_hp_cap_per_building_2035_db = pd.concat(
1939
            [
1940
                df_hp_cap_per_building_2035_db,
1941
                hp_cap_per_building_2035.reset_index(),
1942
            ],
1943
            axis=0,
1944
        )
1945
1946
    # ################ export to db #######################
1947
    logger.info(f"MVGD={min(mvgd_ids)} : {max(mvgd_ids)} | Write data to db.")
1948
1949
    export_to_db(df_peak_loads_db, df_heat_mvgd_ts_db, drop=False)
1950
1951
    df_hp_cap_per_building_2035_db["scenario"] = "eGon2035"
1952
1953
    # TODO debug duplicated building_ids
1954
    duplicates = df_hp_cap_per_building_2035_db.loc[
1955
        df_hp_cap_per_building_2035_db.duplicated("building_id", keep=False)
1956
    ]
1957
1958
    if not duplicates.empty:
1959
        logger.info(
1960
            f"Dropped duplicated buildings: "
1961
            f"{duplicates.loc[:,['building_id', 'hp_capacity']]}"
1962
        )
1963
1964
    df_hp_cap_per_building_2035_db.drop_duplicates("building_id", inplace=True)
1965
1966
    df_hp_cap_per_building_2035_db.building_id = (
1967
        df_hp_cap_per_building_2035_db.building_id.astype(int)
1968
    )
1969
1970
    write_table_to_postgres(
1971
        df_hp_cap_per_building_2035_db,
1972
        EgonHpCapacityBuildings,
1973
        drop=False,
1974
    )
1975
1976
1977
def determine_hp_cap_peak_load_mvgd_ts_status_quo(mvgd_ids, scenario):
1978
    """
1979
    Main function to determine HP capacity per building in status quo scenario.
1980
    Further, creates heat demand time series for all buildings with heat pumps
1981
    in MV grid, as well as for all buildings with gas boilers, used in eTraGo.
1982
1983
    Parameters
1984
    -----------
1985
    mvgd_ids : list(int)
1986
        List of MV grid IDs to determine data for.
1987
1988
    """
1989
1990
    # ========== Register np datatypes with SQLA ==========
1991
    register_adapter(np.float64, adapt_numpy_float64)
1992
    register_adapter(np.int64, adapt_numpy_int64)
1993
    # =====================================================
1994
1995
    df_peak_loads_db = pd.DataFrame()
1996
    df_hp_cap_per_building_status_quo_db = pd.DataFrame()
1997
    df_heat_mvgd_ts_db = pd.DataFrame()
1998
1999
    for mvgd in mvgd_ids:
2000
        logger.info(f"MVGD={mvgd} | Start")
2001
2002
        # ############# aggregate residential and CTS demand profiles #####
2003
2004
        df_heat_ts = aggregate_residential_and_cts_profiles(
2005
            mvgd, scenario=scenario
2006
        )
2007
2008
        # ##################### determine peak loads ###################
2009
        logger.info(f"MVGD={mvgd} | Determine peak loads.")
2010
2011
        peak_load_status_quo = df_heat_ts.max().rename(scenario)
2012
2013
        # ######## determine HP capacity per building #########
2014
        logger.info(f"MVGD={mvgd} | Determine HP capacities.")
2015
2016
        buildings_decentral_heating = (
2017
            get_buildings_with_decentral_heat_demand_in_mv_grid(
2018
                mvgd, scenario=scenario
2019
            )
2020
        )
2021
2022
        # Reduce list of decentral heating if no Peak load available
2023
        # TODO maybe remove after succesfull DE run
2024
        # Might be fixed in #990
2025
        buildings_decentral_heating = catch_missing_buidings(
2026
            buildings_decentral_heating, peak_load_status_quo
2027
        )
2028
2029
        hp_cap_per_building_status_quo = determine_hp_cap_buildings_pvbased_per_mvgd(
2030
            scenario,
2031
            mvgd,
2032
            peak_load_status_quo,
2033
            buildings_decentral_heating,
2034
        )
2035
2036
        # ################ aggregated heat profiles ###################
2037
        logger.info(f"MVGD={mvgd} | Aggregate heat profiles.")
2038
2039
        df_mvgd_ts_status_quo_hp = df_heat_ts.loc[
2040
            :,
2041
            hp_cap_per_building_status_quo.index,
2042
        ].sum(axis=1)
2043
2044
        df_heat_mvgd_ts = pd.DataFrame(
2045
            data={
2046
                "carrier": "heat_pump",
2047
                "bus_id": mvgd,
2048
                "scenario": scenario,
2049
                "dist_aggregated_mw": [df_mvgd_ts_status_quo_hp.to_list()],
2050
            }
2051
        )
2052
2053
        # ################ collect results ##################
2054
        logger.info(f"MVGD={mvgd} | Collect results.")
2055
2056
        df_peak_loads_db = pd.concat(
2057
            [df_peak_loads_db, peak_load_status_quo.reset_index()],
2058
            axis=0,
2059
            ignore_index=True,
2060
        )
2061
2062
        df_heat_mvgd_ts_db = pd.concat(
2063
            [df_heat_mvgd_ts_db, df_heat_mvgd_ts], axis=0, ignore_index=True
2064
        )
2065
2066
        df_hp_cap_per_building_status_quo_db = pd.concat(
2067
            [
2068
                df_hp_cap_per_building_status_quo_db,
2069
                hp_cap_per_building_status_quo.reset_index(),
2070
            ],
2071
            axis=0,
2072
        )
2073
2074
    # ################ export to db #######################
2075
    logger.info(f"MVGD={min(mvgd_ids)} : {max(mvgd_ids)} | Write data to db.")
2076
2077
    export_to_db(df_peak_loads_db, df_heat_mvgd_ts_db, drop=False)
2078
2079
    df_hp_cap_per_building_status_quo_db["scenario"] = scenario
2080
2081
    # TODO debug duplicated building_ids
2082
    duplicates = df_hp_cap_per_building_status_quo_db.loc[
2083
        df_hp_cap_per_building_status_quo_db.duplicated("building_id", keep=False)
2084
    ]
2085
2086
    if not duplicates.empty:
2087
        logger.info(
2088
            f"Dropped duplicated buildings: "
2089
            f"{duplicates.loc[:,['building_id', 'hp_capacity']]}"
2090
        )
2091
2092
    df_hp_cap_per_building_status_quo_db.drop_duplicates("building_id", inplace=True)
2093
2094
    df_hp_cap_per_building_status_quo_db.building_id = (
2095
        df_hp_cap_per_building_status_quo_db.building_id.astype(int)
2096
    )
2097
2098
    write_table_to_postgres(
2099
        df_hp_cap_per_building_status_quo_db,
2100
        EgonHpCapacityBuildings,
2101
        drop=False,
2102
    )
2103
2104
2105
def determine_hp_cap_peak_load_mvgd_ts_pypsa_eur(mvgd_ids):
2106
    """
2107
    Main function to determine minimum required HP capacity in MV for
2108
    pypsa-eur-sec. Further, creates heat demand time series for all buildings
2109
    with heat pumps in MV grid in eGon100RE scenario, used in eTraGo.
2110
2111
    Parameters
2112
    -----------
2113
    mvgd_ids : list(int)
2114
        List of MV grid IDs to determine data for.
2115
2116
    """
2117
2118
    # ========== Register np datatypes with SQLA ==========
2119
    register_adapter(np.float64, adapt_numpy_float64)
2120
    register_adapter(np.int64, adapt_numpy_int64)
2121
    # =====================================================
2122
2123
    df_peak_loads_db = pd.DataFrame()
2124
    df_heat_mvgd_ts_db = pd.DataFrame()
2125
    df_hp_min_cap_mv_grid_pypsa_eur_sec = pd.Series(dtype="float64")
2126
2127
    for mvgd in mvgd_ids:
2128
        logger.info(f"MVGD={mvgd} | Start")
2129
2130
        # ############# aggregate residential and CTS demand profiles #####
2131
2132
        df_heat_ts = aggregate_residential_and_cts_profiles(
2133
            mvgd, scenario="eGon100RE"
2134
        )
2135
2136
        # ##################### determine peak loads ###################
2137
        logger.info(f"MVGD={mvgd} | Determine peak loads.")
2138
2139
        peak_load_100RE = df_heat_ts.max().rename("eGon100RE")
2140
2141
        # ######## determine minimum HP capacity pypsa-eur-sec ###########
2142
        logger.info(f"MVGD={mvgd} | Determine minimum HP capacity.")
2143
2144
        buildings_decentral_heating = (
2145
            get_buildings_with_decentral_heat_demand_in_mv_grid(
2146
                mvgd, scenario="eGon100RE"
2147
            )
2148
        )
2149
2150
        # Reduce list of decentral heating if no Peak load available
2151
        # TODO maybe remove after succesfull DE run
2152
        buildings_decentral_heating = catch_missing_buidings(
2153
            buildings_decentral_heating, peak_load_100RE
2154
        )
2155
2156
        hp_min_cap_mv_grid_pypsa_eur_sec = (
2157
            determine_min_hp_cap_buildings_pypsa_eur_sec(
2158
                peak_load_100RE,
2159
                buildings_decentral_heating,
2160
            )
2161
        )
2162
2163
        # ################ aggregated heat profiles ###################
2164
        logger.info(f"MVGD={mvgd} | Aggregate heat profiles.")
2165
2166
        df_mvgd_ts_hp = df_heat_ts.loc[
2167
            :,
2168
            buildings_decentral_heating,
2169
        ].sum(axis=1)
2170
2171
        df_heat_mvgd_ts = pd.DataFrame(
2172
            data={
2173
                "carrier": "heat_pump",
2174
                "bus_id": mvgd,
2175
                "scenario": "eGon100RE",
2176
                "dist_aggregated_mw": [df_mvgd_ts_hp.to_list()],
2177
            }
2178
        )
2179
2180
        # ################ collect results ##################
2181
        logger.info(f"MVGD={mvgd} | Collect results.")
2182
2183
        df_peak_loads_db = pd.concat(
2184
            [df_peak_loads_db, peak_load_100RE.reset_index()],
2185
            axis=0,
2186
            ignore_index=True,
2187
        )
2188
2189
        df_heat_mvgd_ts_db = pd.concat(
2190
            [df_heat_mvgd_ts_db, df_heat_mvgd_ts], axis=0, ignore_index=True
2191
        )
2192
2193
        df_hp_min_cap_mv_grid_pypsa_eur_sec.loc[mvgd] = (
2194
            hp_min_cap_mv_grid_pypsa_eur_sec
2195
        )
2196
2197
    # ################ export to db and csv ######################
2198
    logger.info(f"MVGD={min(mvgd_ids)} : {max(mvgd_ids)} | Write data to db.")
2199
2200
    export_to_db(df_peak_loads_db, df_heat_mvgd_ts_db, drop=False)
2201
2202
    logger.info(
2203
        f"MVGD={min(mvgd_ids)} : {max(mvgd_ids)} | Write "
2204
        f"pypsa-eur-sec min "
2205
        f"HP capacities to csv."
2206
    )
2207
    export_min_cap_to_csv(df_hp_min_cap_mv_grid_pypsa_eur_sec)
2208
2209
2210
def split_mvgds_into_bulks(n, max_n, func, scenario=None):
2211
    """
2212
    Generic function to split task into multiple parallel tasks,
2213
    dividing the number of MVGDs into even bulks.
2214
2215
    Parameters
2216
    -----------
2217
    n : int
2218
        Number of bulk
2219
    max_n: int
2220
        Maximum number of bulks
2221
    func : function
2222
        The funnction which is then called with the list of MVGD as
2223
        parameter.
2224
    """
2225
2226
    with db.session_scope() as session:
2227
        query = (
2228
            session.query(
2229
                MapZensusGridDistricts.bus_id,
2230
            )
2231
            .filter(
2232
                MapZensusGridDistricts.zensus_population_id
2233
                == EgonPetaHeat.zensus_population_id
2234
            )
2235
            .distinct(MapZensusGridDistricts.bus_id)
2236
        )
2237
        mvgd_ids = pd.read_sql(
2238
            query.statement, query.session.bind, index_col=None
2239
        )
2240
2241
    mvgd_ids = mvgd_ids.sort_values("bus_id").reset_index(drop=True)
2242
2243
    mvgd_ids = np.array_split(mvgd_ids["bus_id"].values, max_n)
2244
    # Only take split n
2245
    mvgd_ids = mvgd_ids[n]
2246
2247
    logger.info(f"Bulk takes care of MVGD: {min(mvgd_ids)} : {max(mvgd_ids)}")
2248
2249
    if scenario is not None:
2250
        func(mvgd_ids, scenario=scenario)
2251
    else:
2252
        func(mvgd_ids)
2253
2254
2255
def delete_hp_capacity(scenario):
2256
    """Remove all hp capacities for the selected scenario
2257
2258
    Parameters
2259
    -----------
2260
    scenario : string
2261
        Either eGon2035 or eGon100RE
2262
2263
    """
2264
2265
    with db.session_scope() as session:
2266
        # Buses
2267
        session.query(EgonHpCapacityBuildings).filter(
2268
            EgonHpCapacityBuildings.scenario == scenario
2269
        ).delete(synchronize_session=False)
2270
2271
2272
def delete_mvgd_ts(scenario):
2273
    """Remove all hp capacities for the selected scenario
2274
2275
    Parameters
2276
    -----------
2277
    scenario : string
2278
        Either eGon2035 or eGon100RE
2279
2280
    """
2281
2282
    with db.session_scope() as session:
2283
        # Buses
2284
        session.query(EgonEtragoTimeseriesIndividualHeating).filter(
2285
            EgonEtragoTimeseriesIndividualHeating.scenario == scenario
2286
        ).delete(synchronize_session=False)
2287
2288
2289
def delete_hp_capacity_100RE():
2290
    """Remove all hp capacities for the selected eGon100RE"""
2291
    EgonHpCapacityBuildings.__table__.create(bind=engine, checkfirst=True)
2292
    delete_hp_capacity(scenario="eGon100RE")
2293
2294
2295
def delete_hp_capacity_status_quo(scenario):
2296
    """Remove all hp capacities for the selected status quo"""
2297
    EgonHpCapacityBuildings.__table__.create(bind=engine, checkfirst=True)
2298
    delete_hp_capacity(scenario=scenario)
2299
2300
2301
def delete_hp_capacity_2035():
2302
    """Remove all hp capacities for the selected eGon2035"""
2303
    EgonHpCapacityBuildings.__table__.create(bind=engine, checkfirst=True)
2304
    delete_hp_capacity(scenario="eGon2035")
2305
2306
2307
def delete_mvgd_ts_status_quo(scenario):
2308
    """Remove all mvgd ts for the selected status quo"""
2309
    EgonEtragoTimeseriesIndividualHeating.__table__.create(
2310
        bind=engine, checkfirst=True
2311
    )
2312
    delete_mvgd_ts(scenario=scenario)
2313
2314
2315
def delete_mvgd_ts_2035():
2316
    """Remove all mvgd ts for the selected eGon2035"""
2317
    EgonEtragoTimeseriesIndividualHeating.__table__.create(
2318
        bind=engine, checkfirst=True
2319
    )
2320
    delete_mvgd_ts(scenario="eGon2035")
2321
2322
2323
def delete_mvgd_ts_100RE():
2324
    """Remove all mvgd ts for the selected eGon100RE"""
2325
    EgonEtragoTimeseriesIndividualHeating.__table__.create(
2326
        bind=engine, checkfirst=True
2327
    )
2328
    delete_mvgd_ts(scenario="eGon100RE")
2329
2330
2331
def delete_heat_peak_loads_status_quo(scenario):
2332
    """Remove all heat peak loads for status quo."""
2333
    BuildingHeatPeakLoads.__table__.create(bind=engine, checkfirst=True)
2334
    with db.session_scope() as session:
2335
        # Buses
2336
        session.query(BuildingHeatPeakLoads).filter(
2337
            BuildingHeatPeakLoads.scenario == scenario
2338
        ).delete(synchronize_session=False)
2339
2340
2341
def delete_heat_peak_loads_2035():
2342
    """Remove all heat peak loads for eGon2035."""
2343
    BuildingHeatPeakLoads.__table__.create(bind=engine, checkfirst=True)
2344
    with db.session_scope() as session:
2345
        # Buses
2346
        session.query(BuildingHeatPeakLoads).filter(
2347
            BuildingHeatPeakLoads.scenario == "eGon2035"
2348
        ).delete(synchronize_session=False)
2349
2350
2351
def delete_heat_peak_loads_100RE():
2352
    """Remove all heat peak loads for eGon100RE."""
2353
    BuildingHeatPeakLoads.__table__.create(bind=engine, checkfirst=True)
2354
    with db.session_scope() as session:
2355
        # Buses
2356
        session.query(BuildingHeatPeakLoads).filter(
2357
            BuildingHeatPeakLoads.scenario == "eGon100RE"
2358
        ).delete(synchronize_session=False)
2359