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

cascade_per_technology()   B

Complexity

Conditions 6

Size

Total Lines 114
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 46
nop 5
dl 0
loc 114
rs 7.8339
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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