Passed
Pull Request — dev (#905)
by
unknown
01:39
created

get_daily_profiles()   A

Complexity

Conditions 2

Size

Total Lines 33
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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