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

HeatPumpsEtrago.__init__()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 6
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 loguru import logger
6
import numpy as np
7
import pandas as pd
8
import random
9
import saio
10
11
from pathlib import Path
12
import time
13
14
from psycopg2.extensions import AsIs, register_adapter
15
from sqlalchemy import ARRAY, REAL, Column, Integer, String
16
from sqlalchemy.ext.declarative import declarative_base
17
import geopandas as gpd
18
19
20
from egon.data import config, db
21
from egon.data.datasets import Dataset
22
from egon.data.datasets.electricity_demand_timeseries.cts_buildings import (
23
    calc_cts_building_profiles,
24
)
25
from egon.data.datasets.electricity_demand_timeseries.tools import (
26
    write_table_to_postgres,
27
)
28
from egon.data.datasets.heat_demand import EgonPetaHeat
29
from egon.data.datasets.heat_demand_timeseries.daily import (
30
    EgonDailyHeatDemandPerClimateZone,
31
    EgonMapZensusClimateZones,
32
)
33
from egon.data.datasets.heat_demand_timeseries.idp_pool import (
34
    EgonHeatTimeseries,
35
)
36
# get zensus cells with district heating
37
from egon.data.datasets.zensus_mv_grid_districts import MapZensusGridDistricts
38
39
engine = db.engine()
40
Base = declarative_base()
41
42
43
class EgonEtragoTimeseriesIndividualHeating(Base):
44
    __tablename__ = "egon_etrago_timeseries_individual_heating"
45
    __table_args__ = {"schema": "demand"}
46
    bus_id = Column(Integer, primary_key=True)
47
    scenario = Column(String, primary_key=True)
48
    carrier = Column(String, primary_key=True)
49
    dist_aggregated_mw = Column(ARRAY(REAL))
50
51
52
# ToDo @Julian muss angepasst werden?
53
class HeatPumpsEtrago(Dataset):
54
    def __init__(self, dependencies):
55
        super().__init__(
56
            name="HeatPumpsEtrago",
57
            version="0.0.0",
58
            dependencies=dependencies,
59
            tasks=(determine_hp_cap_pypsa_eur_sec,),
60
        )
61
62
63
# ToDo @Julian muss angepasst werden?
64
class HeatPumps2035(Dataset):
65
    def __init__(self, dependencies):
66
        super().__init__(
67
            name="HeatPumps2035",
68
            version="0.0.0",
69
            dependencies=dependencies,
70
            tasks=(determine_hp_cap_eGon2035,),
71
        )
72
73
74
# ToDo @Julian muss angepasst werden?
75
class HeatPumps2050(Dataset):
76
    def __init__(self, dependencies):
77
        super().__init__(
78
            name="HeatPumps2050",
79
            version="0.0.0",
80
            dependencies=dependencies,
81
            tasks=(determine_hp_cap_eGon100RE),
82
        )
83
84
85
class BuildingHeatPeakLoads(Base):
86
    __tablename__ = "egon_building_heat_peak_loads"
87
    __table_args__ = {"schema": "demand"}
88
89
    building_id = Column(Integer, primary_key=True)
90
    scenario = Column(String, primary_key=True)
91
    sector = Column(String, primary_key=True)
92
    peak_load_in_w = Column(REAL)
93
94
95
def adapt_numpy_float64(numpy_float64):
96
    return AsIs(numpy_float64)
97
98
99
def adapt_numpy_int64(numpy_int64):
100
    return AsIs(numpy_int64)
101
102
103
def log_to_file(name):
104
    """Simple only file logger"""
105
    logger.remove()
106
    logger.add(
107
        Path(f"{name}.log"),
108
        format="{time} {level} {message}",
109
        # filter="my_module",
110
        level="TRACE",
111
    )
112
    logger.trace("Start trace logging")
113
    return logger
114
115
116
def timeit(func):
117
    """
118
    Decorator for measuring function's running time.
119
    """
120
121
    def measure_time(*args, **kw):
122
        start_time = time.time()
123
        result = func(*args, **kw)
124
        print(
125
            "Processing time of %s(): %.2f seconds."
126
            % (func.__qualname__, time.time() - start_time)
127
        )
128
        return result
129
130
    return measure_time
131
132
133
def timeitlog(func):
134
    """
135
    Decorator for measuring running time of residential heat peak load and
136
    logging it.
137
    """
138
139
    def measure_time(*args, **kw):
140
        start_time = time.time()
141
        result = func(*args, **kw)
142
        process_time = time.time() - start_time
143
        try:
144
            mvgd = kw["mvgd"]
145
        except KeyError:
146
            mvgd = "bulk"
147
        statement = (
148
            f"MVGD={mvgd} | Processing time of {func.__qualname__} | "
149
            f"{time.strftime('%H h, %M min, %S s', time.gmtime(process_time))}"
150
        )
151
        logger.trace(statement)
152
        print(statement)
153
        return result
154
155
    return measure_time
156
157
158
def cascade_per_technology(
159
    heat_per_mv,
160
    technologies,
161
    scenario,
162
    distribution_level,
163
    max_size_individual_chp=0.05,
164
):
165
166
    """Add plants for individual heat.
167
    Currently only on mv grid district level.
168
169
    Parameters
170
    ----------
171
    mv_grid_districts : geopandas.geodataframe.GeoDataFrame
172
        MV grid districts including the heat demand
173
    technologies : pandas.DataFrame
174
        List of supply technologies and their parameters
175
    scenario : str
176
        Name of the scenario
177
    max_size_individual_chp : float
178
        Maximum capacity of an individual chp in MW
179
    Returns
180
    -------
181
    mv_grid_districts : geopandas.geodataframe.GeoDataFrame
182
        MV grid district which need additional individual heat supply
183
    technologies : pandas.DataFrame
184
        List of supply technologies and their parameters
185
    append_df : pandas.DataFrame
186
        List of plants per mv grid for the selected technology
187
188
    """
189
    sources = config.datasets()["heat_supply"]["sources"]
190
191
    tech = technologies[technologies.priority == technologies.priority.max()]
192
193
    # Distribute heat pumps linear to remaining demand.
194
    if tech.index == "heat_pump":
195
196
        if distribution_level == "federal_state":
197
            # Select target values per federal state
198
            target = db.select_dataframe(
199
                f"""
200
                    SELECT DISTINCT ON (gen) gen as state, capacity
201
                    FROM {sources['scenario_capacities']['schema']}.
202
                    {sources['scenario_capacities']['table']} a
203
                    JOIN {sources['federal_states']['schema']}.
204
                    {sources['federal_states']['table']} b
205
                    ON a.nuts = b.nuts
206
                    WHERE scenario_name = '{scenario}'
207
                    AND carrier = 'residential_rural_heat_pump'
208
                    """,
209
                index_col="state",
210
            )
211
212
            heat_per_mv["share"] = heat_per_mv.groupby(
213
                "state"
214
            ).remaining_demand.apply(lambda grp: grp / grp.sum())
215
216
            append_df = (
217
                heat_per_mv["share"]
218
                .mul(target.capacity[heat_per_mv["state"]].values)
219
                .reset_index()
220
            )
221
        else:
222
            # Select target value for Germany
223
            target = db.select_dataframe(
224
                f"""
225
                    SELECT SUM(capacity) AS capacity
226
                    FROM {sources['scenario_capacities']['schema']}.
227
                    {sources['scenario_capacities']['table']} a
228
                    WHERE scenario_name = '{scenario}'
229
                    AND carrier = 'residential_rural_heat_pump'
230
                    """
231
            )
232
233
            heat_per_mv["share"] = (
234
                heat_per_mv.remaining_demand
235
                / heat_per_mv.remaining_demand.sum()
236
            )
237
238
            append_df = (
239
                heat_per_mv["share"].mul(target.capacity[0]).reset_index()
240
            )
241
242
        append_df.rename(
243
            {"bus_id": "mv_grid_id", "share": "capacity"}, axis=1, inplace=True
244
        )
245
246
    elif tech.index == "gas_boiler":
247
248
        append_df = pd.DataFrame(
249
            data={
250
                "capacity": heat_per_mv.remaining_demand.div(
251
                    tech.estimated_flh.values[0]
252
                ),
253
                "carrier": "residential_rural_gas_boiler",
254
                "mv_grid_id": heat_per_mv.index,
255
                "scenario": scenario,
256
            }
257
        )
258
259
    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...
260
        append_df["carrier"] = tech.index[0]
261
        heat_per_mv.loc[
262
            append_df.mv_grid_id, "remaining_demand"
263
        ] -= append_df.set_index("mv_grid_id").capacity.mul(
264
            tech.estimated_flh.values[0]
265
        )
266
267
    heat_per_mv = heat_per_mv[heat_per_mv.remaining_demand >= 0]
268
269
    technologies = technologies.drop(tech.index)
270
271
    return heat_per_mv, technologies, append_df
272
273
274
def cascade_heat_supply_indiv(scenario, distribution_level, plotting=True):
275
    """Assigns supply strategy for individual heating in four steps.
276
277
    1.) all small scale CHP are connected.
278
    2.) If the supply can not  meet the heat demand, solar thermal collectors
279
        are attached. This is not implemented yet, since individual
280
        solar thermal plants are not considered in eGon2035 scenario.
281
    3.) If this is not suitable, the mv grid is also supplied by heat pumps.
282
    4.) The last option are individual gas boilers.
283
284
    Parameters
285
    ----------
286
    scenario : str
287
        Name of scenario
288
    plotting : bool, optional
289
        Choose if individual heating supply is plotted. The default is True.
290
291
    Returns
292
    -------
293
    resulting_capacities : pandas.DataFrame
294
        List of plants per mv grid
295
296
    """
297
298
    sources = config.datasets()["heat_supply"]["sources"]
299
300
    # Select residential heat demand per mv grid district and federal state
301
    heat_per_mv = db.select_geodataframe(
302
        f"""
303
        SELECT d.bus_id as bus_id, SUM(demand) as demand,
304
        c.vg250_lan as state, d.geom
305
        FROM {sources['heat_demand']['schema']}.
306
        {sources['heat_demand']['table']} a
307
        JOIN {sources['map_zensus_grid']['schema']}.
308
        {sources['map_zensus_grid']['table']} b
309
        ON a.zensus_population_id = b.zensus_population_id
310
        JOIN {sources['map_vg250_grid']['schema']}.
311
        {sources['map_vg250_grid']['table']} c
312
        ON b.bus_id = c.bus_id
313
        JOIN {sources['mv_grids']['schema']}.
314
        {sources['mv_grids']['table']} d
315
        ON d.bus_id = c.bus_id
316
        WHERE scenario = '{scenario}'
317
        AND a.zensus_population_id NOT IN (
318
            SELECT zensus_population_id
319
            FROM {sources['map_dh']['schema']}.{sources['map_dh']['table']}
320
            WHERE scenario = '{scenario}')
321
        GROUP BY d.bus_id, vg250_lan, geom
322
        """,
323
        index_col="bus_id",
324
    )
325
326
    # Store geometry of mv grid
327
    geom_mv = heat_per_mv.geom.centroid.copy()
328
329
    # Initalize Dataframe for results
330
    resulting_capacities = pd.DataFrame(
331
        columns=["mv_grid_id", "carrier", "capacity"]
332
    )
333
334
    # Set technology data according to
335
    # http://www.wbzu.de/seminare/infopool/infopool-bhkw
336
    # TODO: Add gas boilers and solar themal (eGon100RE)
337
    technologies = pd.DataFrame(
338
        index=["heat_pump", "gas_boiler"],
339
        columns=["estimated_flh", "priority"],
340
        data={"estimated_flh": [4000, 8000], "priority": [2, 1]},
341
    )
342
343
    # In the beginning, the remaining demand equals demand
344
    heat_per_mv["remaining_demand"] = heat_per_mv["demand"]
345
346
    # Connect new technologies, if there is still heat demand left
347
    while (len(technologies) > 0) and (len(heat_per_mv) > 0):
348
        # Attach new supply technology
349
        heat_per_mv, technologies, append_df = cascade_per_technology(
350
            heat_per_mv, technologies, scenario, distribution_level
351
        )
352
        # Collect resulting capacities
353
        resulting_capacities = resulting_capacities.append(
354
            append_df, ignore_index=True
355
        )
356
357
    if plotting:
358
        plot_heat_supply(resulting_capacities)
359
360
    return gpd.GeoDataFrame(
361
        resulting_capacities,
362
        geometry=geom_mv[resulting_capacities.mv_grid_id].values,
363
    )
364
365
366
# @timeit
367
def get_peta_demand(mvgd):
368
    """only residential"""
369
370
    with db.session_scope() as session:
371
        query = (
372
            session.query(
373
                MapZensusGridDistricts.zensus_population_id,
374
                EgonPetaHeat.demand.label("peta_2035"),
375
            )
376
            .filter(MapZensusGridDistricts.bus_id == mvgd)
377
            .filter(
378
                MapZensusGridDistricts.zensus_population_id
379
                == EgonPetaHeat.zensus_population_id
380
            )
381
            .filter(EgonPetaHeat.scenario == "eGon2035")
382
            .filter(EgonPetaHeat.sector == "residential")
383
        )
384
385
    df_peta_2035 = pd.read_sql(
386
        query.statement, query.session.bind, index_col="zensus_population_id"
387
    )
388
389
    with db.session_scope() as session:
390
        query = (
391
            session.query(
392
                MapZensusGridDistricts.zensus_population_id,
393
                EgonPetaHeat.demand.label("peta_2050"),
394
            )
395
            .filter(MapZensusGridDistricts.bus_id == mvgd)
396
            .filter(
397
                MapZensusGridDistricts.zensus_population_id
398
                == EgonPetaHeat.zensus_population_id
399
            )
400
            .filter(EgonPetaHeat.scenario == "eGon100RE")
401
            .filter(EgonPetaHeat.sector == "residential")
402
        )
403
404
    df_peta_100RE = pd.read_sql(
405
        query.statement, query.session.bind, index_col="zensus_population_id"
406
    )
407
408
    df_peta_demand = pd.concat(
409
        [df_peta_2035, df_peta_100RE], axis=1
410
    ).reset_index()
411
412
    return df_peta_demand
413
414
415
# @timeit
416
def get_profile_ids(mvgd):
417
    with db.session_scope() as session:
418
        query = (
419
            session.query(
420
                MapZensusGridDistricts.zensus_population_id,
421
                EgonHeatTimeseries.building_id,
422
                EgonHeatTimeseries.selected_idp_profiles,
423
            )
424
            .filter(MapZensusGridDistricts.bus_id == mvgd)
425
            .filter(
426
                MapZensusGridDistricts.zensus_population_id
427
                == EgonHeatTimeseries.zensus_population_id
428
            )
429
        )
430
431
    df_profiles_ids = pd.read_sql(
432
        query.statement, query.session.bind, index_col=None
433
    )
434
    # Add building count per cell
435
    df_profiles_ids = pd.merge(
436
        left=df_profiles_ids,
437
        right=df_profiles_ids.groupby("zensus_population_id")["building_id"]
438
        .count()
439
        .rename("buildings"),
440
        left_on="zensus_population_id",
441
        right_index=True,
442
    )
443
444
    df_profiles_ids = df_profiles_ids.explode("selected_idp_profiles")
445
    df_profiles_ids["day_of_year"] = (
446
        df_profiles_ids.groupby("building_id").cumcount() + 1
447
    )
448
    return df_profiles_ids
449
450
451
# @timeit
452
def get_daily_profiles(profile_ids):
453
    saio.register_schema("demand", db.engine())
454
    from saio.demand import egon_heat_idp_pool
455
456
    with db.session_scope() as session:
457
        query = session.query(egon_heat_idp_pool).filter(
458
            egon_heat_idp_pool.index.in_(profile_ids)
459
        )
460
461
    df_profiles = pd.read_sql(
462
        query.statement, query.session.bind, index_col="index"
463
    )
464
465
    df_profiles = df_profiles.explode("idp")
466
    df_profiles["hour"] = df_profiles.groupby(axis=0, level=0).cumcount() + 1
467
468
    return df_profiles
469
470
471
# @timeit
472
def get_daily_demand_share(mvgd):
473
474
    with db.session_scope() as session:
475
        query = (
476
            session.query(
477
                MapZensusGridDistricts.zensus_population_id,
478
                EgonDailyHeatDemandPerClimateZone.day_of_year,
479
                EgonDailyHeatDemandPerClimateZone.daily_demand_share,
480
            )
481
            .filter(
482
                EgonMapZensusClimateZones.climate_zone
483
                == EgonDailyHeatDemandPerClimateZone.climate_zone
484
            )
485
            .filter(
486
                MapZensusGridDistricts.zensus_population_id
487
                == EgonMapZensusClimateZones.zensus_population_id
488
            )
489
            .filter(MapZensusGridDistricts.bus_id == mvgd)
490
        )
491
492
    df_daily_demand_share = pd.read_sql(
493
        query.statement, query.session.bind, index_col=None
494
    )
495
    return df_daily_demand_share
496
497
498
@timeitlog
499
def calc_residential_heat_profiles_per_mvgd(mvgd):
500
    """
501
    Gets residential heat profiles per building in MV grid for both eGon2035 and
502
    eGon100RE scenario.
503
504
    Parameters
505
    ----------
506
    mvgd : int
507
        MV grid ID.
508
509
    Returns
510
    --------
511
    pd.DataFrame
512
        Heat demand profiles of buildings. Columns are:
513
            * zensus_population_id : int
514
                Zensus cell ID building is in.
515
            * building_id : int
516
                ID of building.
517
            * day_of_year : int
518
                Day of the year (1 - 365).
519
            * hour : int
520
                Hour of the day (1 - 24).
521
            * eGon2035 : float
522
                Building's residential heat demand in MW, for specified hour of the
523
                year (specified through columns `day_of_year` and `hour`).
524
            * eGon100RE : float
525
                Building's residential heat demand in MW, for specified hour of the
526
                year (specified through columns `day_of_year` and `hour`).
527
528
    """
529
    df_peta_demand = get_peta_demand(mvgd)
530
531
    if df_peta_demand.empty:
532
        return None
533
534
    df_profiles_ids = get_profile_ids(mvgd)
535
536
    if df_profiles_ids.empty:
537
        return None
538
539
    df_profiles = get_daily_profiles(
540
        df_profiles_ids["selected_idp_profiles"].unique()
541
    )
542
543
    df_daily_demand_share = get_daily_demand_share(mvgd)
544
545
    # Merge profile ids to peta demand by zensus_population_id
546
    df_profile_merge = pd.merge(
547
        left=df_peta_demand, right=df_profiles_ids, on="zensus_population_id"
548
    )
549
550
    # Merge daily demand to daily profile ids by zensus_population_id and day
551
    df_profile_merge = pd.merge(
552
        left=df_profile_merge,
553
        right=df_daily_demand_share,
554
        on=["zensus_population_id", "day_of_year"],
555
    )
556
557
    # Merge daily profiles by profile id
558
    df_profile_merge = pd.merge(
559
        left=df_profile_merge,
560
        right=df_profiles[["idp", "hour"]],
561
        left_on="selected_idp_profiles",
562
        right_index=True,
563
    )
564
565
    # Scale profiles
566
    df_profile_merge["eGon2035"] = (
567
        df_profile_merge["idp"]
568
        .mul(df_profile_merge["daily_demand_share"])
569
        .mul(df_profile_merge["peta_2035"])
570
        .div(df_profile_merge["buildings"])
571
    )
572
573
    df_profile_merge["eGon100RE"] = (
574
        df_profile_merge["idp"]
575
        .mul(df_profile_merge["daily_demand_share"])
576
        .mul(df_profile_merge["peta_2050"])
577
        .div(df_profile_merge["buildings"])
578
    )
579
580
    columns = ["zensus_population_id", "building_id", "day_of_year", "hour",
581
               "eGon2035", "eGon100RE"]
582
583
    return df_profile_merge.loc[:, columns]
584
585
586 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...
587
588
    from matplotlib import pyplot as plt
589
590
    mv_grids = db.select_geodataframe(
591
        """
592
        SELECT * FROM grid.egon_mv_grid_district
593
        """,
594
        index_col="bus_id",
595
    )
596
597
    for c in ["CHP", "heat_pump"]:
598
        mv_grids[c] = (
599
            resulting_capacities[resulting_capacities.carrier == c]
600
            .set_index("mv_grid_id")
601
            .capacity
602
        )
603
604
        fig, ax = plt.subplots(1, 1)
605
        mv_grids.boundary.plot(linewidth=0.2, ax=ax, color="black")
606
        mv_grids.plot(
607
            ax=ax,
608
            column=c,
609
            cmap="magma_r",
610
            legend=True,
611
            legend_kwds={
612
                "label": f"Installed {c} in MW",
613
                "orientation": "vertical",
614
            },
615
        )
616
        plt.savefig(f"plots/individual_heat_supply_{c}.png", dpi=300)
617
618
619
@timeit
620
def get_buildings_with_decentral_heat_demand_in_mv_grid(scenario, mv_grid_id):
621
    """
622
    Returns building IDs of buildings with decentral heat demand in given MV
623
    grid.
624
625
    As cells with district heating differ between scenarios, this is also
626
    depending on the scenario.
627
628
    Parameters
629
    -----------
630
    scenario : str
631
        Name of scenario. Can be either "eGon2035" or "eGon100RE".
632
    mv_grid_id : int
633
        ID of MV grid.
634
635
    Returns
636
    --------
637
    pd.Index(int)
638
        Building IDs (as int) of buildings with decentral heating system in given
639
        MV grid. Type is pandas Index to avoid errors later on when it is
640
        used in a query.
641
642
    """
643
644
    # get zensus cells in grid
645
    zensus_population_ids = db.select_dataframe(
646
        f"""
647
        SELECT zensus_population_id
648
        FROM boundaries.egon_map_zensus_grid_districts
649
        WHERE bus_id = {mv_grid_id}
650
        """,
651
        index_col=None,
652
    ).zensus_population_id.values
653
654
    # TODO replace with sql adapter?
655
    # ========== Register np datatypes with SQLA ==========
656
    register_adapter(np.float64, adapt_numpy_float64)
657
    register_adapter(np.int64, adapt_numpy_int64)
658
    # =====================================================
659
    # convert to pd.Index (otherwise type is np.int64, which will for some
660
    # reason throw an error when used in a query)
661
    zensus_population_ids = pd.Index(zensus_population_ids)
662
663
    # get zensus cells with district heating
664
    from egon.data.datasets.district_heating_areas import (
665
        MapZensusDistrictHeatingAreas,
666
    )
667
668
    with db.session_scope() as session:
669
        query = session.query(
670
            MapZensusDistrictHeatingAreas.zensus_population_id,
671
        ).filter(
672
            MapZensusDistrictHeatingAreas.scenario == scenario,
673
            MapZensusDistrictHeatingAreas.zensus_population_id.in_(
674
                zensus_population_ids
675
            ),
676
        )
677
678
    cells_with_dh = pd.read_sql(
679
        query.statement, query.session.bind, index_col=None
680
    ).zensus_population_id.values
681
682
    # remove zensus cells with district heating
683
    zensus_population_ids = zensus_population_ids.drop(
684
        cells_with_dh, errors="ignore"
685
    )
686
687
    # get buildings with decentral heat demand
688
    engine = db.engine()
689
    saio.register_schema("demand", engine)
690
    from saio.demand import egon_heat_timeseries_selected_profiles
691
692
    with db.session_scope() as session:
693
        query = session.query(
694
            egon_heat_timeseries_selected_profiles.building_id,
695
        ).filter(
696
            egon_heat_timeseries_selected_profiles.zensus_population_id.in_(
697
                zensus_population_ids
698
            )
699
        )
700
701
    buildings_with_heat_demand = pd.read_sql(
702
        query.statement, query.session.bind, index_col=None
703
    ).building_id.values
704
705
    return buildings_with_heat_demand
706
707
708
def get_total_heat_pump_capacity_of_mv_grid(scenario, mv_grid_id):
709
    """
710
    Returns total heat pump capacity per grid that was previously defined
711
    (by NEP or pypsa-eur-sec).
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
    float
723
        Total heat pump capacity in MW in given MV grid.
724
725
    """
726
    from egon.data.datasets.heat_supply import EgonIndividualHeatingSupply
727
728
    with db.session_scope() as session:
729
        query = (
730
            session.query(
731
                EgonIndividualHeatingSupply.mv_grid_id,
732
                EgonIndividualHeatingSupply.capacity,
733
            )
734
            .filter(EgonIndividualHeatingSupply.scenario == scenario)
735
            .filter(EgonIndividualHeatingSupply.carrier == "heat_pump")
736
            .filter(EgonIndividualHeatingSupply.mv_grid_id == mv_grid_id)
737
        )
738
739
    hp_cap_mv_grid = pd.read_sql(
740
        query.statement, query.session.bind, index_col="mv_grid_id"
741
    ).capacity.values[0]
742
743
    return hp_cap_mv_grid
744
745
746
def determine_minimum_hp_capacity_per_building(
747
    peak_heat_demand, flexibility_factor=24 / 18, cop=1.7
748
):
749
    """
750
    Determines minimum required heat pump capacity.
751
752
    Parameters
753
    ----------
754
    peak_heat_demand : pd.Series
755
        Series with peak heat demand per building in MW. Index contains the
756
        building ID.
757
    flexibility_factor : float
758
        Factor to overdimension the heat pump to allow for some flexible
759
        dispatch in times of high heat demand. Per default, a factor of 24/18
760
        is used, to take into account
761
762
    Returns
763
    -------
764
    pd.Series
765
        Pandas series with minimum required heat pump capacity per building in
766
        MW.
767
768
    """
769
    return peak_heat_demand * flexibility_factor / cop
770
771
772
def determine_buildings_with_hp_in_mv_grid(
773
    hp_cap_mv_grid, min_hp_cap_per_building
774
):
775
    """
776
    Distributes given total heat pump capacity to buildings based on their peak
777
    heat demand.
778
779
    Parameters
780
    -----------
781
    hp_cap_mv_grid : float
782
        Total heat pump capacity in MW in given MV grid.
783
    min_hp_cap_per_building : pd.Series
784
        Pandas series with minimum required heat pump capacity per building
785
         in MW.
786
787
    Returns
788
    -------
789
    pd.Index(int)
790
        Building IDs (as int) of buildings to get heat demand time series for.
791
792
    """
793
    building_ids = min_hp_cap_per_building.index
794
795
    # get buildings with PV to give them a higher priority when selecting
796
    # buildings a heat pump will be allocated to
797
    engine = db.engine()
798
    saio.register_schema("supply", engine)
799
    # TODO Adhoc Pv rooftop fix
800
    # from saio.supply import egon_power_plants_pv_roof_building
801
    #
802
    # with db.session_scope() as session:
803
    #     query = session.query(
804
    #         egon_power_plants_pv_roof_building.building_id
805
    #     ).filter(
806
    #         egon_power_plants_pv_roof_building.building_id.in_(building_ids)
807
    #     )
808
    #
809
    # buildings_with_pv = pd.read_sql(
810
    #     query.statement, query.session.bind, index_col=None
811
    # ).building_id.values
812
    buildings_with_pv = []
813
    # set different weights for buildings with PV and without PV
814
    weight_with_pv = 1.5
815
    weight_without_pv = 1.0
816
    weights = pd.concat(
817
        [
818
            pd.DataFrame(
819
                {"weight": weight_without_pv},
820
                index=building_ids.drop(buildings_with_pv, errors="ignore"),
821
            ),
822
            pd.DataFrame({"weight": weight_with_pv}, index=buildings_with_pv),
823
        ]
824
    )
825
    # normalise weights (probability needs to add up to 1)
826
    weights.weight = weights.weight / weights.weight.sum()
827
828
    # get random order at which buildings are chosen
829
    np.random.seed(db.credentials()["--random-seed"])
830
    buildings_with_hp_order = np.random.choice(
831
        weights.index,
832
        size=len(weights),
833
        replace=False,
834
        p=weights.weight.values,
835
    )
836
837
    # select buildings until HP capacity in MV grid is reached (some rest
838
    # capacity will remain)
839
    hp_cumsum = min_hp_cap_per_building.loc[buildings_with_hp_order].cumsum()
840
    buildings_with_hp = hp_cumsum[hp_cumsum <= hp_cap_mv_grid].index
841
842
    # choose random heat pumps until remaining heat pumps are larger than remaining
843
    # heat pump capacity
844
    remaining_hp_cap = (
845
        hp_cap_mv_grid - min_hp_cap_per_building.loc[buildings_with_hp].sum())
846
    min_cap_buildings_wo_hp = min_hp_cap_per_building.loc[
847
        building_ids.drop(buildings_with_hp)]
848
    possible_buildings = min_cap_buildings_wo_hp[
849
        min_cap_buildings_wo_hp <= remaining_hp_cap].index
850
    while len(possible_buildings) > 0:
851
        random.seed(db.credentials()["--random-seed"])
852
        new_hp_building = random.choice(possible_buildings)
853
        # add new building to building with HP
854
        buildings_with_hp = buildings_with_hp.append(pd.Index([new_hp_building]))
855
        # determine if there are still possible buildings
856
        remaining_hp_cap = (
857
            hp_cap_mv_grid - min_hp_cap_per_building.loc[buildings_with_hp].sum())
858
        min_cap_buildings_wo_hp = min_hp_cap_per_building.loc[
859
            building_ids.drop(buildings_with_hp)]
860
        possible_buildings = min_cap_buildings_wo_hp[
861
            min_cap_buildings_wo_hp <= remaining_hp_cap].index
862
863
    return buildings_with_hp
864
865
866
def desaggregate_hp_capacity(min_hp_cap_per_building, hp_cap_mv_grid):
867
    """
868
    Desaggregates the required total heat pump capacity to buildings.
869
870
    All buildings are previously assigned a minimum required heat pump
871
    capacity. If the total heat pump capacity exceeds this, larger heat pumps
872
    are assigned.
873
874
    Parameters
875
    ------------
876
    min_hp_cap_per_building : pd.Series
877
        Pandas series with minimum required heat pump capacity per building
878
         in MW.
879
    hp_cap_mv_grid : float
880
        Total heat pump capacity in MW in given MV grid.
881
882
    Returns
883
    --------
884
    pd.Series
885
        Pandas series with heat pump capacity per building in MW.
886
887
    """
888
    # distribute remaining capacity to all buildings with HP depending on
889
    # installed HP capacity
890
891
    allocated_cap = min_hp_cap_per_building.sum()
892
    remaining_cap = hp_cap_mv_grid - allocated_cap
893
894
    fac = remaining_cap / allocated_cap
895
    hp_cap_per_building = (
896
        min_hp_cap_per_building * fac + min_hp_cap_per_building
897
    )
898
    return hp_cap_per_building
899
900
901
def determine_hp_cap_pypsa_eur_sec(peak_heat_demand, building_ids):
902
    """
903
    Determines minimum required HP capacity in MV grid in MW as input for
904
    pypsa-eur-sec.
905
906
    Parameters
907
    ----------
908
    peak_heat_demand : pd.Series
909
        Series with peak heat demand per building in MW. Index contains the
910
        building ID.
911
    building_ids : pd.Index(int)
912
        Building IDs (as int) of buildings with decentral heating system in given
913
        MV grid.
914
915
    Returns
916
    --------
917
    float
918
        Minimum required HP capacity in MV grid in MW.
919
920
    """
921
    if len(building_ids) > 0:
922
        peak_heat_demand = peak_heat_demand.loc[building_ids]
923
        # determine minimum required heat pump capacity per building
924
        min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
925
            peak_heat_demand
926
        )
927
        return min_hp_cap_buildings.sum()
928
    else:
929
        return 0.0
930
931
932
def determine_hp_cap_eGon2035(mv_grid_id, peak_heat_demand, building_ids):
933
    """
934
    Determines which buildings in the MV grid will have a HP (buildings with PV
935
    rooftop are more likely to be assigned) in the eGon2035 scenario, as well as
936
    their respective HP capacity in MW.
937
938
    Parameters
939
    -----------
940
    mv_grid_id : int
941
        ID of MV grid.
942
    peak_heat_demand : pd.Series
943
        Series with peak heat demand per building in MW. Index contains the
944
        building ID.
945
    building_ids : pd.Index(int)
946
        Building IDs (as int) of buildings with decentral heating system in
947
        given MV grid.
948
949
    """
950
951
    if len(building_ids) > 0:
952
        peak_heat_demand = peak_heat_demand.loc[building_ids]
953
954
        # determine minimum required heat pump capacity per building
955
        min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
956
            peak_heat_demand
957
        )
958
959
        # select buildings that will have a heat pump
960
        hp_cap_grid = get_total_heat_pump_capacity_of_mv_grid(
961
            "eGon2035", mv_grid_id
962
        )
963
        buildings_with_hp = determine_buildings_with_hp_in_mv_grid(
964
            hp_cap_grid, min_hp_cap_buildings
965
        )
966
967
        # distribute total heat pump capacity to all buildings with HP
968
        hp_cap_per_building = desaggregate_hp_capacity(
969
            min_hp_cap_buildings.loc[buildings_with_hp], hp_cap_grid
970
        )
971
972
        return hp_cap_per_building
973
974
    else:
975
        return pd.Series()
976
977
978
def determine_hp_cap_eGon100RE(mv_grid_id):
979
    """Wrapper function to determine Heat Pump capacities
980
    for scenario eGon100RE. All buildings without district heating get a heat
981
    pump capacity assigned.
982
    """
983
984
    # determine minimum required heat pump capacity per building
985
    building_ids = get_buildings_with_decentral_heat_demand_in_mv_grid(
986
        "eGon100RE", mv_grid_id
987
    )
988
989
    # TODO get peak demand from db
990
    peak_heat_demand = get_peak_demand_per_building(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable get_peak_demand_per_building does not seem to be defined.
Loading history...
991
        "eGon100RE", building_ids
992
    )
993
994
    # determine minimum required heat pump capacity per building
995
    min_hp_cap_buildings = determine_minimum_hp_capacity_per_building(
996
        peak_heat_demand, flexibility_factor=24 / 18, cop=1.7
997
    )
998
999
    # distribute total heat pump capacity to all buildings with HP
1000
    hp_cap_grid = get_total_heat_pump_capacity_of_mv_grid(
1001
        "eGon100RE", mv_grid_id
1002
    )
1003
    hp_cap_per_building = desaggregate_hp_capacity(
1004
        min_hp_cap_buildings, hp_cap_grid
1005
    )
1006
1007
    # ToDo Write desaggregated HP capacity to table
1008
1009
1010
@timeitlog
1011
def residential_heat_peak_load_export_bulk(n, max_n=5):
1012
    """n= [1;max_n]"""
1013
1014
    # ========== Register np datatypes with SQLA ==========
1015
    register_adapter(np.float64, adapt_numpy_float64)
1016
    register_adapter(np.int64, adapt_numpy_int64)
1017
    # =====================================================
1018
1019
    log_to_file(residential_heat_peak_load_export_bulk.__qualname__ + f"_{n}")
1020
    if n == 0:
1021
        raise KeyError("n >= 1")
1022
1023
    # ToDo @Julian warum ist Abfrage so umständlich?
1024
    with db.session_scope() as session:
1025
        query = (
1026
            session.query(
1027
                MapZensusGridDistricts.bus_id,
1028
            )
1029
            .filter(
1030
                MapZensusGridDistricts.zensus_population_id
1031
                == EgonPetaHeat.zensus_population_id
1032
            )
1033
            .filter(EgonPetaHeat.sector == "residential")
1034
            .distinct(MapZensusGridDistricts.bus_id)
1035
        )
1036
    mvgd_ids = pd.read_sql(query.statement, query.session.bind, index_col=None)
1037
1038
    mvgd_ids = mvgd_ids.sort_values("bus_id").reset_index(drop=True)
1039
1040
    mvgd_ids = np.array_split(mvgd_ids["bus_id"].values, max_n)
1041
1042
    # TODO mvgd_ids = [kleines mvgd]
1043
    for mvgd in [1556]: #mvgd_ids[n - 1]:
1044
1045
        logger.trace(f"MVGD={mvgd} | Start")
1046
1047
        # ############### get residential heat demand profiles ###############
1048
        df_heat_ts = calc_residential_heat_profiles_per_mvgd(
1049
            mvgd=mvgd
1050
        )
1051
1052
        # pivot to allow aggregation with CTS profiles
1053
        df_heat_ts_2035 = df_heat_ts.loc[
1054
                          :, ["building_id", "day_of_year", "hour", "eGon2035"]]
1055
        df_heat_ts_2035 = df_heat_ts_2035.pivot(
1056
            index=["day_of_year", "hour"],
1057
            columns="building_id",
1058
            values="eGon2035",
1059
        )
1060
        df_heat_ts_2035 = df_heat_ts_2035.sort_index().reset_index(drop=True)
1061
1062
        df_heat_ts_100RE = df_heat_ts.loc[
1063
                          :, ["building_id", "day_of_year", "hour", "eGon100RE"]]
1064
        df_heat_ts_100RE = df_heat_ts_100RE.pivot(
1065
            index=["day_of_year", "hour"],
1066
            columns="building_id",
1067
            values="eGon100RE",
1068
        )
1069
        df_heat_ts_100RE = df_heat_ts_100RE.sort_index().reset_index(drop=True)
1070
1071
        del df_heat_ts
1072
1073
        # ############### get CTS heat demand profiles ###############
1074
        heat_demand_cts_ts_2035 = calc_cts_building_profiles(
1075
            egon_building_ids=[644, 645],
1076
            bus_ids=[1366],
1077
            scenario="eGon2035",
1078
            sector="heat",
1079
        )
1080
        heat_demand_cts_ts_2035.rename(
1081
            columns={644: 1225533, 645: 1225527}, inplace=True)
1082
        heat_demand_cts_ts_100RE = calc_cts_building_profiles(
1083
            egon_building_ids=[644, 645],
1084
            bus_ids=[1366],
1085
            scenario="eGon100RE",
1086
            sector="heat",
1087
        )
1088
        heat_demand_cts_ts_100RE.rename(
1089
            columns={644: 1225533, 645: 1225527}, inplace=True)
1090
        # ToDo change back
1091
        # heat_demand_cts_ts_2035 = calc_cts_building_profiles(
1092
        #     egon_building_ids=df_heat_ts.building_id.unique(),
1093
        #     bus_ids=[mvgd],
1094
        #     scenario="eGon2035",
1095
        #     sector="heat",
1096
        # )
1097
        # heat_demand_cts_ts_100RE = calc_cts_building_profiles(
1098
        #     egon_building_ids=df_heat_ts.building_id.unique(),
1099
        #     bus_ids=[mvgd],
1100
        #     scenario="eGon100RE",
1101
        #     sector="heat",
1102
        # )
1103
1104
        # ############# aggregate residential and CTS demand profiles #############
1105
        df_heat_ts_2035 = pd.concat(
1106
            [df_heat_ts_2035, heat_demand_cts_ts_2035], axis=1
1107
        )
1108
        df_heat_ts_2035 = df_heat_ts_2035.groupby(axis=1, level=0).sum()
1109
1110
        df_heat_ts_100RE = pd.concat(
1111
            [df_heat_ts_100RE, heat_demand_cts_ts_100RE], axis=1
1112
        )
1113
        df_heat_ts_100RE = df_heat_ts_100RE.groupby(axis=1, level=0).sum()
1114
1115
        del heat_demand_cts_ts_2035, heat_demand_cts_ts_100RE
1116
1117
        # ##################### export peak loads to DB ###################
1118
1119
        # ToDo @Julian kombinierte peak load oder getrennt nach residential und CTS?
1120
        df_peak_loads_2035 = df_heat_ts_2035.max()
1121
        df_peak_loads_100RE = df_heat_ts_100RE.max()
1122
1123
        df_peak_loads_db_2035 = df_peak_loads_2035.reset_index().melt(
1124
            id_vars="building_id",
1125
            var_name="scenario",
1126
            value_name="peak_load_in_w",
1127
        )
1128
        df_peak_loads_db_2035["scenario"] = "eGon2035"
1129
        df_peak_loads_db_100RE = df_peak_loads_100RE.reset_index().melt(
1130
            id_vars="building_id",
1131
            var_name="scenario",
1132
            value_name="peak_load_in_w",
1133
        )
1134
        df_peak_loads_db_100RE["scenario"] = "eGon100RE"
1135
        df_peak_loads_db = pd.concat(
1136
            [df_peak_loads_db_2035, df_peak_loads_db_100RE])
1137
1138
        del df_peak_loads_db_2035, df_peak_loads_db_100RE
1139
1140
        df_peak_loads_db["sector"] = "residential+CTS"
1141
        # From MW to W
1142
        # ToDo @Julian warum in W?
1143
        df_peak_loads_db["peak_load_in_w"] = df_peak_loads_db["peak_load_in_w"] * 1e6
1144
1145
        logger.trace(f"MVGD={mvgd} | Export to DB")
1146
1147
        # TODO export peak loads all buildings both scenarios to db
1148
        # write_table_to_postgres(
1149
        #     df_peak_loads_db, BuildingHeatPeakLoads, engine=engine
1150
        # )
1151
        # logger.trace(f"MVGD={mvgd} | Done")
1152
1153
        # ######## determine HP capacity for NEP scenario and pypsa-eur-sec ##########
1154
1155
        # get buildings with decentral heating systems in both scenarios
1156
        buildings_decentral_heating_2035 = (
1157
            get_buildings_with_decentral_heat_demand_in_mv_grid(
1158
                "eGon2035", mvgd
1159
            )
1160
        )
1161
        buildings_decentral_heating_100RE = (
1162
            get_buildings_with_decentral_heat_demand_in_mv_grid(
1163
                "eGon100RE", mvgd
1164
            )
1165
        )
1166
1167
        # determine HP capacity per building for NEP2035 scenario
1168
        hp_cap_per_building_2035 = determine_hp_cap_eGon2035(
1169
            mvgd, df_peak_loads_2035, buildings_decentral_heating_2035)
1170
        buildings_hp_2035 = hp_cap_per_building_2035.index
1171
        buildings_gas_2035 = pd.Index(buildings_decentral_heating_2035).drop(
1172
            buildings_hp_2035)
1173
1174
        # determine minimum HP capacity per building for pypsa-eur-sec
1175
        hp_min_cap_mv_grid_pypsa_eur_sec = determine_hp_cap_pypsa_eur_sec(
1176
            df_peak_loads_100RE, buildings_decentral_heating_100RE)
1177
1178
        # ######################## write HP capacities to DB ######################
1179
1180
        # ToDo Write HP capacity per building in 2035 (hp_cap_per_building_2035) to
1181
        #  db table
1182
1183
        # ToDo Write minimum required capacity in pypsa-eur-sec
1184
        #  (hp_min_cap_mv_grid_pypsa_eur_sec) to
1185
        #  db table for pypsa-eur-sec input
1186
1187
        # ################ write aggregated heat profiles to DB ###################
1188
1189
        # heat demand time series for buildings with heat pumps
1190
1191
        # ToDo Write aggregated heat demand time series of buildings with HP to
1192
        #  table to be used in eTraGo - egon_etrago_timeseries_individual_heating
1193
        # TODO Clara uses this table already
1194
        #     but will not need it anymore for pypsa eur sec - @Julian?
1195
        # EgonEtragoTimeseriesIndividualHeating
1196
        df_heat_ts_2035.loc[:, buildings_hp_2035].sum(axis=1)
1197
        df_heat_ts_100RE.loc[:, buildings_decentral_heating_100RE].sum(axis=1)
1198
1199
        # Change format
1200
        # ToDo @Julian noch notwendig?
1201
        # data = CTS_grid.drop(columns="scenario")
1202
        # df_etrago_cts_heat_profiles = pd.DataFrame(
1203
        #     index=data.index, columns=["scn_name", "p_set"]
1204
        # )
1205
        # df_etrago_cts_heat_profiles.p_set = data.values.tolist()
1206
        # df_etrago_cts_heat_profiles.scn_name = CTS_grid["scenario"]
1207
        # df_etrago_cts_heat_profiles.reset_index(inplace=True)
1208
1209
        # # Drop and recreate Table if exists
1210
        # EgonEtragoTimeseriesIndividualHeating.__table__.drop(bind=db.engine(),
1211
        #                                                      checkfirst=True)
1212
        # EgonEtragoTimeseriesIndividualHeating.__table__.create(bind=db.engine(),
1213
        #                                                        checkfirst=True)
1214
        #
1215
        # # Write heat ts into db
1216
        # with db.session_scope() as session:
1217
        #     session.bulk_insert_mappings(
1218
        #         EgonEtragoTimeseriesIndividualHeating,
1219
        #         df_etrago_cts_heat_profiles.to_dict(orient="records"),
1220
        #     )
1221
1222
        # heat demand time series for buildings with gas boilers (only 2035 scenario)
1223
        df_heat_ts_2035.loc[:, buildings_gas_2035].sum(axis=1)
1224
        # ToDo Write other heat demand time series to database - gas voronoi
1225
        #  (grid - egon_gas_voronoi mit carrier CH4)
1226
        #  erstmal intermediate table
1227
1228
1229
def residential_heat_peak_load_export_bulk_1():
1230
    residential_heat_peak_load_export_bulk(1, max_n=5)
1231
1232
1233
def residential_heat_peak_load_export_bulk_2():
1234
    residential_heat_peak_load_export_bulk(2, max_n=5)
1235
1236
1237
def residential_heat_peak_load_export_bulk_3():
1238
    residential_heat_peak_load_export_bulk(3, max_n=5)
1239
1240
1241
def residential_heat_peak_load_export_bulk_4():
1242
    residential_heat_peak_load_export_bulk(4, max_n=5)
1243
1244
1245
def residential_heat_peak_load_export_bulk_5():
1246
    residential_heat_peak_load_export_bulk(5, max_n=5)
1247
1248
1249
def create_peak_load_table():
1250
1251
    BuildingHeatPeakLoads.__table__.create(bind=engine, checkfirst=True)
1252
1253
1254
def delete_peak_loads_if_existing():
1255
    """Remove all entries"""
1256
1257
    with db.session_scope() as session:
1258
        # Buses
1259
        session.query(BuildingHeatPeakLoads).filter(
1260
            BuildingHeatPeakLoads.sector == "residential"
1261
        ).delete(synchronize_session=False)
1262
1263
1264
if __name__ == "__main__":
1265
    #calc_residential_heat_profiles_per_mvgd(mvgd)
1266
    residential_heat_peak_load_export_bulk_1()
1267