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

desaggregate_hp_capacity()   A

Complexity

Conditions 1

Size

Total Lines 35
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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