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

create_hp_capacity_table()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nop 0
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.0",
108
            dependencies=dependencies,
109
            # tasks=({*dyn_parallel_tasks_pypsa_eur_sec()},),
110
            tasks=(*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.0",
156
            dependencies=dependencies,
157
            tasks=(
158
                delete_heat_peak_loads_eGon2035,
159
                # {*dyn_parallel_tasks_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.0",
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.
876
877
    Parameters
878
    -----------
879
    mv_grid_id : int
880
        ID of MV grid.
881
    scenario : str
882
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
883
884
    Returns
885
    --------
886
    pd.Index(int)
887
        Building IDs (as int) of buildings with decentral heating system in
888
        given MV grid. Type is pandas Index to avoid errors later on when it is
889
        used in a query.
890
891
    """
892
    # get residential buildings with decentral heating systems
893
    buildings_decentral_heating_res = (
894
        get_residential_buildings_with_decentral_heat_demand_in_mv_grid(
895
            scenario, mvgd
896
        )
897
    )
898
899
    # get CTS buildings with decentral heating systems
900
    buildings_decentral_heating_cts = (
901
        get_cts_buildings_with_decentral_heat_demand_in_mv_grid(scenario, mvgd)
902
    )
903
904
    # merge residential and CTS buildings
905
    buildings_decentral_heating = buildings_decentral_heating_res.append(
906
        buildings_decentral_heating_cts
907
    ).unique()
908
909
    return buildings_decentral_heating
910
911
912
def get_total_heat_pump_capacity_of_mv_grid(scenario, mv_grid_id):
913
    """
914
    Returns total heat pump capacity per grid that was previously defined
915
    (by NEP or pypsa-eur-sec).
916
917
    Parameters
918
    -----------
919
    scenario : str
920
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
921
    mv_grid_id : int
922
        ID of MV grid.
923
924
    Returns
925
    --------
926
    float
927
        Total heat pump capacity in MW in given MV grid.
928
929
    """
930
    from egon.data.datasets.heat_supply import EgonIndividualHeatingSupply
931
932
    with db.session_scope() as session:
933
        query = (
934
            session.query(
935
                EgonIndividualHeatingSupply.mv_grid_id,
936
                EgonIndividualHeatingSupply.capacity,
937
            )
938
            .filter(EgonIndividualHeatingSupply.scenario == scenario)
939
            .filter(EgonIndividualHeatingSupply.carrier == "heat_pump")
940
            .filter(EgonIndividualHeatingSupply.mv_grid_id == mv_grid_id)
941
        )
942
943
        hp_cap_mv_grid = pd.read_sql(
944
            query.statement, query.session.bind, index_col="mv_grid_id"
945
        )
946
    if hp_cap_mv_grid.empty:
947
        return 0.0
948
    else:
949
        return hp_cap_mv_grid.capacity.values[0]
950
951
952
def get_heat_peak_demand_per_building(scenario, building_ids):
953
    """"""
954
955
    with db.session_scope() as session:
956
        query = (
957
            session.query(
958
                BuildingHeatPeakLoads.building_id,
959
                BuildingHeatPeakLoads.peak_load_in_w,
960
            )
961
            .filter(BuildingHeatPeakLoads.scenario == scenario)
962
            .filter(BuildingHeatPeakLoads.building_id.in_(building_ids))
963
        )
964
965
        df_heat_peak_demand = pd.read_sql(
966
            query.statement, query.session.bind, index_col=None
967
        )
968
969
    # TODO remove check
970
    if df_heat_peak_demand.duplicated("building_id").any():
971
        raise ValueError("Duplicate building_id")
972
973
    # convert to series and from W to MW
974
    df_heat_peak_demand = (
975
        df_heat_peak_demand.set_index("building_id").loc[:, "peak_load_in_w"]
976
        * 1e6
977
    )
978
    return df_heat_peak_demand
979
980
981
def determine_minimum_hp_capacity_per_building(
982
    peak_heat_demand, flexibility_factor=24 / 18, cop=1.7
983
):
984
    """
985
    Determines minimum required heat pump capacity.
986
987
    Parameters
988
    ----------
989
    peak_heat_demand : pd.Series
990
        Series with peak heat demand per building in MW. Index contains the
991
        building ID.
992
    flexibility_factor : float
993
        Factor to overdimension the heat pump to allow for some flexible
994
        dispatch in times of high heat demand. Per default, a factor of 24/18
995
        is used, to take into account
996
997
    Returns
998
    -------
999
    pd.Series
1000
        Pandas series with minimum required heat pump capacity per building in
1001
        MW.
1002
1003
    """
1004
    return peak_heat_demand * flexibility_factor / cop
1005
1006
1007
def determine_buildings_with_hp_in_mv_grid(
1008
    hp_cap_mv_grid, min_hp_cap_per_building
1009
):
1010
    """
1011
    Distributes given total heat pump capacity to buildings based on their peak
1012
    heat demand.
1013
1014
    Parameters
1015
    -----------
1016
    hp_cap_mv_grid : float
1017
        Total heat pump capacity in MW in given MV grid.
1018
    min_hp_cap_per_building : pd.Series
1019
        Pandas series with minimum required heat pump capacity per building
1020
         in MW.
1021
1022
    Returns
1023
    -------
1024
    pd.Index(int)
1025
        Building IDs (as int) of buildings to get heat demand time series for.
1026
1027
    """
1028
    building_ids = min_hp_cap_per_building.index
1029
1030
    # get buildings with PV to give them a higher priority when selecting
1031
    # buildings a heat pump will be allocated to
1032
    saio.register_schema("supply", engine)
1033
    from saio.supply import egon_power_plants_pv_roof_building
1034
1035
    with db.session_scope() as session:
1036
        query = session.query(
1037
            egon_power_plants_pv_roof_building.building_id
1038
        ).filter(
1039
            egon_power_plants_pv_roof_building.building_id.in_(building_ids)
1040
        )
1041
1042
        buildings_with_pv = pd.read_sql(
1043
            query.statement, query.session.bind, index_col=None
1044
        ).building_id.values
1045
    # set different weights for buildings with PV and without PV
1046
    weight_with_pv = 1.5
1047
    weight_without_pv = 1.0
1048
    weights = pd.concat(
1049
        [
1050
            pd.DataFrame(
1051
                {"weight": weight_without_pv},
1052
                index=building_ids.drop(buildings_with_pv, errors="ignore"),
1053
            ),
1054
            pd.DataFrame({"weight": weight_with_pv}, index=buildings_with_pv),
1055
        ]
1056
    )
1057
    # normalise weights (probability needs to add up to 1)
1058
    weights.weight = weights.weight / weights.weight.sum()
1059
1060
    # get random order at which buildings are chosen
1061
    np.random.seed(db.credentials()["--random-seed"])
1062
    buildings_with_hp_order = np.random.choice(
1063
        weights.index,
1064
        size=len(weights),
1065
        replace=False,
1066
        p=weights.weight.values,
1067
    )
1068
1069
    # select buildings until HP capacity in MV grid is reached (some rest
1070
    # capacity will remain)
1071
    hp_cumsum = min_hp_cap_per_building.loc[buildings_with_hp_order].cumsum()
1072
    buildings_with_hp = hp_cumsum[hp_cumsum <= hp_cap_mv_grid].index
1073
1074
    # choose random heat pumps until remaining heat pumps are larger than
1075
    # remaining heat pump capacity
1076
    remaining_hp_cap = (
1077
        hp_cap_mv_grid - min_hp_cap_per_building.loc[buildings_with_hp].sum()
1078
    )
1079
    min_cap_buildings_wo_hp = min_hp_cap_per_building.loc[
1080
        building_ids.drop(buildings_with_hp)
1081
    ]
1082
    possible_buildings = min_cap_buildings_wo_hp[
1083
        min_cap_buildings_wo_hp <= remaining_hp_cap
1084
    ].index
1085
    while len(possible_buildings) > 0:
1086
        random.seed(db.credentials()["--random-seed"])
1087
        new_hp_building = random.choice(possible_buildings)
1088
        # add new building to building with HP
1089
        buildings_with_hp = buildings_with_hp.append(
1090
            pd.Index([new_hp_building])
1091
        )
1092
        # determine if there are still possible buildings
1093
        remaining_hp_cap = (
1094
            hp_cap_mv_grid
1095
            - min_hp_cap_per_building.loc[buildings_with_hp].sum()
1096
        )
1097
        min_cap_buildings_wo_hp = min_hp_cap_per_building.loc[
1098
            building_ids.drop(buildings_with_hp)
1099
        ]
1100
        possible_buildings = min_cap_buildings_wo_hp[
1101
            min_cap_buildings_wo_hp <= remaining_hp_cap
1102
        ].index
1103
1104
    return buildings_with_hp
1105
1106
1107
def desaggregate_hp_capacity(min_hp_cap_per_building, hp_cap_mv_grid):
1108
    """
1109
    Desaggregates the required total heat pump capacity to buildings.
1110
1111
    All buildings are previously assigned a minimum required heat pump
1112
    capacity. If the total heat pump capacity exceeds this, larger heat pumps
1113
    are assigned.
1114
1115
    Parameters
1116
    ------------
1117
    min_hp_cap_per_building : pd.Series
1118
        Pandas series with minimum required heat pump capacity per building
1119
         in MW.
1120
    hp_cap_mv_grid : float
1121
        Total heat pump capacity in MW in given MV grid.
1122
1123
    Returns
1124
    --------
1125
    pd.Series
1126
        Pandas series with heat pump capacity per building in MW.
1127
1128
    """
1129
    # distribute remaining capacity to all buildings with HP depending on
1130
    # installed HP capacity
1131
1132
    allocated_cap = min_hp_cap_per_building.sum()
1133
    remaining_cap = hp_cap_mv_grid - allocated_cap
1134
1135
    fac = remaining_cap / allocated_cap
1136
    hp_cap_per_building = (
1137
        min_hp_cap_per_building * fac + min_hp_cap_per_building
1138
    )
1139
    hp_cap_per_building.index.name = "building_id"
1140
1141
    return hp_cap_per_building
1142
1143
1144
def determine_min_hp_cap_buildings_pypsa_eur_sec(
1145
    peak_heat_demand, building_ids
1146
):
1147
    """
1148
    Determines minimum required HP capacity in MV grid in MW as input for
1149
    pypsa-eur-sec.
1150
1151
    Parameters
1152
    ----------
1153
    peak_heat_demand : pd.Series
1154
        Series with peak heat demand per building in MW. Index contains the
1155
        building ID.
1156
    building_ids : pd.Index(int)
1157
        Building IDs (as int) of buildings with decentral heating system in
1158
        given MV grid.
1159
1160
    Returns
1161
    --------
1162
    float
1163
        Minimum required HP capacity in MV grid in MW.
1164
1165
    """
1166
    if len(building_ids) > 0:
1167
        peak_heat_demand = peak_heat_demand.loc[building_ids]
1168
        # determine minimum required heat pump capacity per building
1169
        min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
1170
            peak_heat_demand
1171
        )
1172
        return min_hp_cap_buildings.sum()
1173
    else:
1174
        return 0.0
1175
1176
1177
def determine_hp_cap_buildings_eGon2035_per_mvgd(
1178
    mv_grid_id, peak_heat_demand, building_ids
1179
):
1180
    """
1181
    Determines which buildings in the MV grid will have a HP (buildings with PV
1182
    rooftop are more likely to be assigned) in the eGon2035 scenario, as well
1183
    as their respective HP capacity in MW.
1184
1185
    Parameters
1186
    -----------
1187
    mv_grid_id : int
1188
        ID of MV grid.
1189
    peak_heat_demand : pd.Series
1190
        Series with peak heat demand per building in MW. Index contains the
1191
        building ID.
1192
    building_ids : pd.Index(int)
1193
        Building IDs (as int) of buildings with decentral heating system in
1194
        given MV grid.
1195
1196
    """
1197
1198
    hp_cap_grid = get_total_heat_pump_capacity_of_mv_grid(
1199
        "eGon2035", mv_grid_id
1200
    )
1201
1202
    if len(building_ids) > 0 and hp_cap_grid > 0.0:
1203
        peak_heat_demand = peak_heat_demand.loc[building_ids]
1204
1205
        # determine minimum required heat pump capacity per building
1206
        min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
1207
            peak_heat_demand
1208
        )
1209
1210
        # select buildings that will have a heat pump
1211
        buildings_with_hp = determine_buildings_with_hp_in_mv_grid(
1212
            hp_cap_grid, min_hp_cap_buildings
1213
        )
1214
1215
        # distribute total heat pump capacity to all buildings with HP
1216
        hp_cap_per_building = desaggregate_hp_capacity(
1217
            min_hp_cap_buildings.loc[buildings_with_hp], hp_cap_grid
1218
        )
1219
1220
        return hp_cap_per_building.rename("hp_capacity")
1221
1222
    else:
1223
        return pd.Series(dtype="float64").rename("hp_capacity")
1224
1225
1226
def determine_hp_cap_buildings_eGon100RE_per_mvgd(mv_grid_id):
1227
    """
1228
    Determines HP capacity per building in eGon100RE scenario.
1229
1230
    In eGon100RE scenario all buildings without district heating get a heat
1231
    pump.
1232
1233
    Returns
1234
    --------
1235
    pd.Series
1236
        Pandas series with heat pump capacity per building in MW.
1237
1238
    """
1239
1240
    hp_cap_grid = get_total_heat_pump_capacity_of_mv_grid(
1241
        "eGon100RE", mv_grid_id
1242
    )
1243
1244
    if hp_cap_grid > 0.0:
1245
1246
        # get buildings with decentral heating systems
1247
        building_ids = get_buildings_with_decentral_heat_demand_in_mv_grid(
1248
            mv_grid_id, scenario="eGon100RE"
1249
        )
1250
1251
        logger.info(f"MVGD={mv_grid_id} | Get peak loads from DB")
1252
        df_peak_heat_demand = get_heat_peak_demand_per_building(
1253
            "eGon100RE", building_ids
1254
        )
1255
1256
        logger.info(f"MVGD={mv_grid_id} | Determine HP capacities.")
1257
        # determine minimum required heat pump capacity per building
1258
        min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
1259
            df_peak_heat_demand, flexibility_factor=24 / 18, cop=1.7
1260
        )
1261
1262
        logger.info(f"MVGD={mv_grid_id} | Desaggregate HP capacities.")
1263
        # distribute total heat pump capacity to all buildings with HP
1264
        hp_cap_per_building = desaggregate_hp_capacity(
1265
            min_hp_cap_buildings, hp_cap_grid
1266
        )
1267
1268
        return hp_cap_per_building.rename("hp_capacity")
1269
    else:
1270
        return pd.Series(dtype="float64").rename("hp_capacity")
1271
1272
1273
def determine_hp_cap_buildings_eGon100RE():
1274
    """
1275
    Main function to determine HP capacity per building in eGon100RE scenario.
1276
1277
    """
1278
1279
    # ========== Register np datatypes with SQLA ==========
1280
    register_adapter(np.float64, adapt_numpy_float64)
1281
    register_adapter(np.int64, adapt_numpy_int64)
1282
    # =====================================================
1283
1284
    with db.session_scope() as session:
1285
        query = (
1286
            session.query(
1287
                MapZensusGridDistricts.bus_id,
1288
            )
1289
            .filter(
1290
                MapZensusGridDistricts.zensus_population_id
1291
                == EgonPetaHeat.zensus_population_id
1292
            )
1293
            .distinct(MapZensusGridDistricts.bus_id)
1294
        )
1295
        mvgd_ids = pd.read_sql(
1296
            query.statement, query.session.bind, index_col=None
1297
        )
1298
    mvgd_ids = mvgd_ids.sort_values("bus_id")
1299
    mvgd_ids = mvgd_ids["bus_id"].values
1300
1301
    df_hp_cap_per_building_100RE_db = pd.DataFrame(
1302
        columns=["building_id", "hp_capacity"]
1303
    )
1304
1305
    for mvgd_id in mvgd_ids:
1306
1307
        logger.info(f"MVGD={mvgd_id} | Start")
1308
1309
        hp_cap_per_building_100RE = (
1310
            determine_hp_cap_buildings_eGon100RE_per_mvgd(mvgd_id)
1311
        )
1312
1313
        if not hp_cap_per_building_100RE.empty:
1314
            df_hp_cap_per_building_100RE_db = pd.concat(
1315
                [
1316
                    df_hp_cap_per_building_100RE_db,
1317
                    hp_cap_per_building_100RE.reset_index(),
1318
                ],
1319
                axis=0,
1320
            )
1321
1322
    logger.info(f"MVGD={min(mvgd_ids)} - {max(mvgd_ids)} | Write data to db.")
1323
    df_hp_cap_per_building_100RE_db["scenario"] = "eGon100RE"
1324
1325
    EgonHpCapacityBuildings.__table__.create(bind=engine, checkfirst=True)
1326
    delete_hp_capacity(scenario="eGon100RE")
1327
1328
    write_table_to_postgres(
1329
        df_hp_cap_per_building_100RE_db,
1330
        EgonHpCapacityBuildings,
1331
        drop=False,
1332
    )
1333
1334
1335
def aggregate_residential_and_cts_profiles(mvgd, scenario):
1336
    """
1337
    Gets residential and CTS heat demand profiles per building and aggregates
1338
    them.
1339
1340
    Parameters
1341
    ----------
1342
    mvgd : int
1343
        MV grid ID.
1344
    scenario : str
1345
        Possible options are eGon2035 or eGon100RE.
1346
1347
    Returns
1348
    --------
1349
    pd.DataFrame
1350
        Table of demand profile per building. Column names are building IDs and
1351
        index is hour of the year as int (0-8759).
1352
1353
    """
1354
    # ############### get residential heat demand profiles ###############
1355
    df_heat_ts = calc_residential_heat_profiles_per_mvgd(
1356
        mvgd=mvgd, scenario=scenario
1357
    )
1358
1359
    # pivot to allow aggregation with CTS profiles
1360
    df_heat_ts = df_heat_ts.pivot(
1361
        index=["day_of_year", "hour"],
1362
        columns="building_id",
1363
        values="demand_ts",
1364
    )
1365
    df_heat_ts = df_heat_ts.sort_index().reset_index(drop=True)
1366
1367
    # ############### get CTS heat demand profiles ###############
1368
    heat_demand_cts_ts = calc_cts_building_profiles(
1369
        bus_ids=[mvgd],
1370
        scenario=scenario,
1371
        sector="heat",
1372
    )
1373
1374
    # ############# aggregate residential and CTS demand profiles #############
1375
    df_heat_ts = pd.concat([df_heat_ts, heat_demand_cts_ts], axis=1)
1376
1377
    df_heat_ts = df_heat_ts.groupby(axis=1, level=0).sum()
1378
1379
    return df_heat_ts
1380
1381
1382
def export_to_db(df_peak_loads_db, df_heat_mvgd_ts_db, drop=False):
1383
    """
1384
    Function to export the collected results of all MVGDs per bulk to DB.
1385
1386
        Parameters
1387
    ----------
1388
    df_peak_loads_db : pd.DataFrame
1389
        Table of building peak loads of all MVGDs per bulk
1390
    df_heat_mvgd_ts_db : pd.DataFrame
1391
        Table of all aggregated MVGD profiles per bulk
1392
    drop : boolean
1393
        Drop and recreate table if True
1394
1395
    """
1396
1397
    df_peak_loads_db = df_peak_loads_db.melt(
1398
        id_vars="building_id",
1399
        var_name="scenario",
1400
        value_name="peak_load_in_w",
1401
    )
1402
    df_peak_loads_db["building_id"] = df_peak_loads_db["building_id"].astype(
1403
        int
1404
    )
1405
    df_peak_loads_db["sector"] = "residential+cts"
1406
    # From MW to W
1407
    df_peak_loads_db["peak_load_in_w"] = (
1408
        df_peak_loads_db["peak_load_in_w"] * 1e6
1409
    )
1410
    write_table_to_postgres(df_peak_loads_db, BuildingHeatPeakLoads, drop=drop)
1411
1412
    dtypes = {
1413
        column.key: column.type
1414
        for column in EgonEtragoTimeseriesIndividualHeating.__table__.columns
1415
    }
1416
    df_heat_mvgd_ts_db = df_heat_mvgd_ts_db.loc[:, dtypes.keys()]
1417
1418
    if drop:
1419
        logger.info(
1420
            f"Drop and recreate table "
1421
            f"{EgonEtragoTimeseriesIndividualHeating.__table__.name}."
1422
        )
1423
        EgonEtragoTimeseriesIndividualHeating.__table__.drop(
1424
            bind=engine, checkfirst=True
1425
        )
1426
        EgonEtragoTimeseriesIndividualHeating.__table__.create(
1427
            bind=engine, checkfirst=True
1428
        )
1429
1430
    with db.session_scope() as session:
1431
        df_heat_mvgd_ts_db.to_sql(
1432
            name=EgonEtragoTimeseriesIndividualHeating.__table__.name,
1433
            schema=EgonEtragoTimeseriesIndividualHeating.__table__.schema,
1434
            con=session.connection(),
1435
            if_exists="append",
1436
            method="multi",
1437
            index=False,
1438
            dtype=dtypes,
1439
        )
1440
1441
1442
def export_min_cap_to_csv(df_hp_min_cap_mv_grid_pypsa_eur_sec):
1443
    """Export minimum capacity of heat pumps for pypsa eur sec to csv"""
1444
1445
    df_hp_min_cap_mv_grid_pypsa_eur_sec.index.name = "mvgd_id"
1446
    df_hp_min_cap_mv_grid_pypsa_eur_sec = (
1447
        df_hp_min_cap_mv_grid_pypsa_eur_sec.to_frame(
1448
            name="min_hp_capacity"
1449
        ).reset_index()
1450
    )
1451
1452
    folder = Path(".") / "input-pypsa-eur-sec"
1453
    file = folder / "minimum_hp_capacity_mv_grid_2035.csv"
1454
    # Create the folder, if it does not exist already
1455
    if not os.path.exists(folder):
1456
        os.mkdir(folder)
1457
    # TODO check append
1458
    if not file.is_file():
1459
        df_hp_min_cap_mv_grid_pypsa_eur_sec.to_csv(file)
1460
    else:
1461
        df_hp_min_cap_mv_grid_pypsa_eur_sec.to_csv(
1462
            file, mode="a", header=False
1463
        )
1464
        # TODO delete file if task is cleared?!
1465
1466
1467
def catch_missing_buidings(buildings_decentral_heating, peak_load):
1468
    """
1469
    Check for missing buildings and reduce the list of buildings with
1470
    decentral heating if no peak loads available. This should only happen
1471
    in case of cutout SH
1472
1473
    Parameters
1474
    -----------
1475
    buildings_decentral_heating : list(int)
1476
        Array or list of buildings with decentral heating
1477
1478
    peak_load : pd.Series
1479
        Peak loads of all building within the mvgd
1480
1481
    """
1482
    # Catch missing buildings key error
1483
    # should only happen within cutout SH
1484
    if (
1485
        not all(buildings_decentral_heating.isin(peak_load.index))
1486
        and config.settings()["egon-data"]["--dataset-boundary"]
1487
        == "Schleswig-Holstein"
1488
    ):
1489
        diff = buildings_decentral_heating.difference(peak_load.index)
1490
        logger.warning(
1491
            f"Dropped {len(diff)} building ids due to missing peak "
1492
            f"loads. {len(buildings_decentral_heating)} left."
1493
        )
1494
        logger.info(f"Dropped buildings: {diff.values}")
1495
        buildings_decentral_heating = buildings_decentral_heating.drop(diff)
1496
1497
    return buildings_decentral_heating
1498
1499
1500
def determine_hp_cap_peak_load_mvgd_ts_2035(mvgd_ids):
1501
    """
1502
    Main function to determine HP capacity per building in eGon2035 scenario.
1503
    Further, creates heat demand time series for all buildings with heat pumps
1504
    in MV grid, as well as for all buildings with gas boilers, used in eTraGo.
1505
1506
    Parameters
1507
    -----------
1508
    mvgd_ids : list(int)
1509
        List of MV grid IDs to determine data for.
1510
1511
    """
1512
1513
    # ========== Register np datatypes with SQLA ==========
1514
    register_adapter(np.float64, adapt_numpy_float64)
1515
    register_adapter(np.int64, adapt_numpy_int64)
1516
    # =====================================================
1517
1518
    df_peak_loads_db = pd.DataFrame()
1519
    df_hp_cap_per_building_2035_db = pd.DataFrame()
1520
    df_heat_mvgd_ts_db = pd.DataFrame()
1521
1522
    for mvgd in mvgd_ids:
1523
1524
        logger.info(f"MVGD={mvgd} | Start")
1525
1526
        # ############# aggregate residential and CTS demand profiles #####
1527
1528
        df_heat_ts = aggregate_residential_and_cts_profiles(
1529
            mvgd, scenario="eGon2035"
1530
        )
1531
1532
        # ##################### determine peak loads ###################
1533
        logger.info(f"MVGD={mvgd} | Determine peak loads.")
1534
1535
        peak_load_2035 = df_heat_ts.max().rename("eGon2035")
1536
1537
        # ######## determine HP capacity per building #########
1538
        logger.info(f"MVGD={mvgd} | Determine HP capacities.")
1539
1540
        buildings_decentral_heating = (
1541
            get_buildings_with_decentral_heat_demand_in_mv_grid(
1542
                mvgd, scenario="eGon2035"
1543
            )
1544
        )
1545
1546
        # Reduce list of decentral heating if no Peak load available
1547
        # TODO maybe remove after succesfull DE run
1548
        buildings_decentral_heating = catch_missing_buidings(
1549
            buildings_decentral_heating, peak_load_2035
1550
        )
1551
1552
        hp_cap_per_building_2035 = (
1553
            determine_hp_cap_buildings_eGon2035_per_mvgd(
1554
                mvgd,
1555
                peak_load_2035,
1556
                buildings_decentral_heating,
1557
            )
1558
        )
1559
        buildings_gas_2035 = pd.Index(buildings_decentral_heating).drop(
1560
            hp_cap_per_building_2035.index
1561
        )
1562
1563
        # ################ aggregated heat profiles ###################
1564
        logger.info(f"MVGD={mvgd} | Aggregate heat profiles.")
1565
1566
        df_mvgd_ts_2035_hp = df_heat_ts.loc[
1567
            :,
1568
            hp_cap_per_building_2035.index,
1569
        ].sum(axis=1)
1570
1571
        # heat demand time series for buildings with gas boiler
1572
        df_mvgd_ts_2035_gas = df_heat_ts.loc[:, buildings_gas_2035].sum(axis=1)
1573
1574
        df_heat_mvgd_ts = pd.DataFrame(
1575
            data={
1576
                "carrier": ["heat_pump", "CH4"],
1577
                "bus_id": mvgd,
1578
                "scenario": ["eGon2035", "eGon2035"],
1579
                "dist_aggregated_mw": [
1580
                    df_mvgd_ts_2035_hp.to_list(),
1581
                    df_mvgd_ts_2035_gas.to_list(),
1582
                ],
1583
            }
1584
        )
1585
1586
        # ################ collect results ##################
1587
        logger.info(f"MVGD={mvgd} | Collect results.")
1588
1589
        df_peak_loads_db = pd.concat(
1590
            [df_peak_loads_db, peak_load_2035.reset_index()],
1591
            axis=0,
1592
            ignore_index=True,
1593
        )
1594
1595
        df_heat_mvgd_ts_db = pd.concat(
1596
            [df_heat_mvgd_ts_db, df_heat_mvgd_ts], axis=0, ignore_index=True
1597
        )
1598
1599
        df_hp_cap_per_building_2035_db = pd.concat(
1600
            [
1601
                df_hp_cap_per_building_2035_db,
1602
                hp_cap_per_building_2035.reset_index(),
1603
            ],
1604
            axis=0,
1605
        )
1606
1607
    # ################ export to db #######################
1608
    logger.info(f"MVGD={min(mvgd_ids)} - {max(mvgd_ids)} | Write data to db.")
1609
    export_to_db(df_peak_loads_db, df_heat_mvgd_ts_db, drop=False)
1610
1611
    df_hp_cap_per_building_2035_db["scenario"] = "eGon2035"
1612
1613
    EgonHpCapacityBuildings.__table__.create(bind=engine, checkfirst=True)
1614
    delete_hp_capacity(scenario="eGon2035")
1615
1616
    write_table_to_postgres(
1617
        df_hp_cap_per_building_2035_db,
1618
        EgonHpCapacityBuildings,
1619
        drop=False,
1620
    )
1621
1622
1623
def determine_hp_cap_peak_load_mvgd_ts_pypsa_eur_sec(mvgd_ids):
1624
    """
1625
    Main function to determine minimum required HP capacity in MV for
1626
    pypsa-eur-sec. Further, creates heat demand time series for all buildings
1627
    with heat pumps in MV grid in eGon100RE scenario, used in eTraGo.
1628
1629
    Parameters
1630
    -----------
1631
    mvgd_ids : list(int)
1632
        List of MV grid IDs to determine data for.
1633
1634
    """
1635
1636
    # ========== Register np datatypes with SQLA ==========
1637
    register_adapter(np.float64, adapt_numpy_float64)
1638
    register_adapter(np.int64, adapt_numpy_int64)
1639
    # =====================================================
1640
1641
    df_peak_loads_db = pd.DataFrame()
1642
    df_heat_mvgd_ts_db = pd.DataFrame()
1643
    df_hp_min_cap_mv_grid_pypsa_eur_sec = pd.Series(dtype="float64")
1644
1645
    for mvgd in mvgd_ids:
1646
1647
        logger.info(f"MVGD={mvgd} | Start")
1648
1649
        # ############# aggregate residential and CTS demand profiles #####
1650
1651
        df_heat_ts = aggregate_residential_and_cts_profiles(
1652
            mvgd, scenario="eGon100RE"
1653
        )
1654
1655
        # ##################### determine peak loads ###################
1656
        logger.info(f"MVGD={mvgd} | Determine peak loads.")
1657
1658
        peak_load_100RE = df_heat_ts.max().rename("eGon100RE")
1659
1660
        # ######## determine minimum HP capacity pypsa-eur-sec ###########
1661
        logger.info(f"MVGD={mvgd} | Determine minimum HP capacity.")
1662
1663
        buildings_decentral_heating = (
1664
            get_buildings_with_decentral_heat_demand_in_mv_grid(
1665
                mvgd, scenario="eGon100RE"
1666
            )
1667
        )
1668
1669
        # Reduce list of decentral heating if no Peak load available
1670
        # TODO maybe remove after succesfull DE run
1671
        buildings_decentral_heating = catch_missing_buidings(
1672
            buildings_decentral_heating, peak_load_100RE
1673
        )
1674
1675
        hp_min_cap_mv_grid_pypsa_eur_sec = (
1676
            determine_min_hp_cap_buildings_pypsa_eur_sec(
1677
                peak_load_100RE,
1678
                buildings_decentral_heating,
1679
            )
1680
        )
1681
1682
        # ################ aggregated heat profiles ###################
1683
        logger.info(f"MVGD={mvgd} | Aggregate heat profiles.")
1684
1685
        df_mvgd_ts_hp = df_heat_ts.loc[
1686
            :,
1687
            buildings_decentral_heating,
1688
        ].sum(axis=1)
1689
1690
        df_heat_mvgd_ts = pd.DataFrame(
1691
            data={
1692
                "carrier": "heat_pump",
1693
                "bus_id": mvgd,
1694
                "scenario": "eGon100RE",
1695
                "dist_aggregated_mw": [df_mvgd_ts_hp.to_list()],
1696
            }
1697
        )
1698
1699
        # ################ collect results ##################
1700
        logger.info(f"MVGD={mvgd} | Collect results.")
1701
1702
        df_peak_loads_db = pd.concat(
1703
            [df_peak_loads_db, peak_load_100RE.reset_index()],
1704
            axis=0,
1705
            ignore_index=True,
1706
        )
1707
1708
        df_heat_mvgd_ts_db = pd.concat(
1709
            [df_heat_mvgd_ts_db, df_heat_mvgd_ts], axis=0, ignore_index=True
1710
        )
1711
1712
        df_hp_min_cap_mv_grid_pypsa_eur_sec.loc[
1713
            mvgd
1714
        ] = hp_min_cap_mv_grid_pypsa_eur_sec
1715
1716
    # ################ export to db and csv ######################
1717
    logger.info(f"MVGD={min(mvgd_ids)} - {max(mvgd_ids)} | Write data to db.")
1718
1719
    export_to_db(df_peak_loads_db, df_heat_mvgd_ts_db, drop=True)
1720
1721
    logger.info(
1722
        f"MVGD={min(mvgd_ids)} - {max(mvgd_ids)} | Write "
1723
        f"pypsa-eur-sec min "
1724
        f"HP capacities to csv."
1725
    )
1726
    export_min_cap_to_csv(df_hp_min_cap_mv_grid_pypsa_eur_sec)
1727
1728
1729
def split_mvgds_into_bulks(n, max_n, func):
1730
    """
1731
    Generic function to split task into multiple parallel tasks,
1732
    dividing the number of MVGDs into even bulks.
1733
1734
    Parameters
1735
    -----------
1736
    n : int
1737
        Number of bulk
1738
    max_n: int
1739
        Maximum number of bulks
1740
    func : function
1741
        The funnction which is then called with the list of MVGD as
1742
        parameter.
1743
    """
1744
1745
    with db.session_scope() as session:
1746
        query = (
1747
            session.query(
1748
                MapZensusGridDistricts.bus_id,
1749
            )
1750
            .filter(
1751
                MapZensusGridDistricts.zensus_population_id
1752
                == EgonPetaHeat.zensus_population_id
1753
            )
1754
            .distinct(MapZensusGridDistricts.bus_id)
1755
        )
1756
        mvgd_ids = pd.read_sql(
1757
            query.statement, query.session.bind, index_col=None
1758
        )
1759
1760
    mvgd_ids = mvgd_ids.sort_values("bus_id").reset_index(drop=True)
1761
1762
    mvgd_ids = np.array_split(mvgd_ids["bus_id"].values, max_n)
1763
    # Only take split n
1764
    mvgd_ids = mvgd_ids[n]
1765
1766
    logger.info(f"Bulk takes care of MVGD: {min(mvgd_ids)} - {max(mvgd_ids)}")
1767
    func(mvgd_ids)
1768
1769
1770
def create_hp_capacity_table():
1771
1772
    EgonHpCapacityBuildings.__table__.drop(bind=engine, checkfirst=True)
1773
    EgonHpCapacityBuildings.__table__.create(bind=engine, checkfirst=True)
1774
1775
1776
def delete_hp_capacity(scenario):
1777
    """Remove all hp capacities for the selected scenario
1778
1779
    Parameters
1780
    -----------
1781
    scenario : string
1782
        Either eGon2035 or eGon100RE
1783
1784
    """
1785
1786
    with db.session_scope() as session:
1787
        # Buses
1788
        session.query(EgonHpCapacityBuildings).filter(
1789
            EgonHpCapacityBuildings.scenario == scenario
1790
        ).delete(synchronize_session=False)
1791
1792
1793
def delete_heat_peak_loads_eGon2035():
1794
    """Remove all heat peak loads for eGon2035.
1795
1796
    This is not necessary for eGon100RE as these peak loads are calculated in
1797
    HeatPumpsPypsaEurSec and tables are recreated during this dataset."""
1798
    with db.session_scope() as session:
1799
        # Buses
1800
        session.query(BuildingHeatPeakLoads).filter(
1801
            BuildingHeatPeakLoads.scenario == "eGon2035"
1802
        ).delete(synchronize_session=False)
1803