get_peta_demand()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 42
Code Lines 16

Duplication

Lines 42
Ratio 100 %

Importance

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