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

data.datasets.heat_supply.individual_heating   F

Complexity

Total Complexity 81

Size/Duplication

Total Lines 1807
Duplicated Lines 1.33 %

Importance

Changes 0
Metric Value
wmc 81
eloc 749
dl 24
loc 1807
rs 1.851
c 0
b 0
f 0

34 Functions

Rating   Name   Duplication   Size   Complexity  
A export_min_cap_to_csv() 0 21 3
A get_zensus_cells_with_decentral_heat_demand_in_mv_grid() 0 60 2
A plot_heat_supply() 24 31 2
A get_daily_demand_share() 0 32 2
A get_cts_buildings_with_decentral_heat_demand_in_mv_grid() 0 47 2
A determine_min_hp_cap_buildings_pypsa_eur_sec() 0 31 2
A get_residential_buildings_with_decentral_heat_demand_in_mv_grid() 0 50 2
B determine_buildings_with_hp_in_mv_grid() 0 98 3
A get_total_heat_pump_capacity_of_mv_grid() 0 38 3
A determine_minimum_hp_capacity_per_building() 0 24 1
A determine_hp_cap_buildings_eGon2035_per_mvgd() 0 47 3
A get_buildings_with_decentral_heat_demand_in_mv_grid() 0 41 1
A desaggregate_hp_capacity() 0 35 1
A calc_residential_heat_profiles_per_mvgd() 0 86 3
A determine_hp_cap_buildings_eGon100RE_per_mvgd() 0 45 2
A get_heat_peak_demand_per_building() 0 27 3
A determine_hp_cap_buildings_eGon100RE() 0 60 4
A create_hp_capacity_table() 0 4 1
A get_peta_demand() 0 42 2
A timeit() 0 15 1
A adapt_numpy_int64() 0 2 1
A delete_hp_capacity() 0 15 2
A split_mvgds_into_bulks() 0 39 2
A delete_heat_peak_loads_eGon2035() 0 10 2
B cascade_per_technology() 0 114 6
B determine_hp_cap_peak_load_mvgd_ts_2035() 0 121 2
B determine_hp_cap_peak_load_mvgd_ts_pypsa_eur_sec() 0 104 2
A catch_missing_buidings() 0 31 3
A adapt_numpy_float64() 0 2 1
A aggregate_residential_and_cts_profiles() 0 45 1
A get_residential_heat_profile_ids() 0 52 2
A get_daily_profiles() 0 33 2
A export_to_db() 0 59 3
A cascade_heat_supply_indiv() 0 89 4

3 Methods

Rating   Name   Duplication   Size   Complexity  
A HeatPumps2050.__init__() 0 6 1
A HeatPumpsPypsaEurSec.__init__() 0 44 2
A HeatPumps2035.__init__() 0 46 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like data.datasets.heat_supply.individual_heating often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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