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

data.datasets.heat_supply.individual_heating   F

Complexity

Total Complexity 88

Size/Duplication

Total Lines 1871
Duplicated Lines 1.28 %

Importance

Changes 0
Metric Value
wmc 88
eloc 783
dl 24
loc 1871
rs 1.817
c 0
b 0
f 0

39 Functions

Rating   Name   Duplication   Size   Complexity  
A delete_hp_capacity() 0 15 2
A determine_hp_cap_buildings_eGon100RE() 0 58 4
A get_zensus_cells_with_decentral_heat_demand_in_mv_grid() 0 60 2
A plot_heat_supply() 24 31 2
A delete_hp_capacity_100RE() 0 4 1
A delete_hp_capacity_2035() 0 4 1
A get_peta_demand() 0 42 2
A timeit() 0 15 1
A adapt_numpy_int64() 0 2 1
A delete_mvgd_ts_100RE() 0 6 1
A split_mvgds_into_bulks() 0 39 2
A delete_mvgd_ts() 0 15 2
A get_daily_demand_share() 0 32 2
A get_cts_buildings_with_decentral_heat_demand_in_mv_grid() 0 47 2
B cascade_per_technology() 0 114 6
B determine_hp_cap_peak_load_mvgd_ts_2035() 0 131 2
B determine_hp_cap_peak_load_mvgd_ts_pypsa_eur_sec() 0 104 2
A export_min_cap_to_csv() 0 26 3
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
A catch_missing_buidings() 0 31 3
B determine_buildings_with_hp_in_mv_grid() 0 99 3
A adapt_numpy_float64() 0 2 1
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 aggregate_residential_and_cts_profiles() 0 45 1
A get_buildings_with_decentral_heat_demand_in_mv_grid() 0 43 1
A get_residential_heat_profile_ids() 0 52 2
A desaggregate_hp_capacity() 0 35 1
A get_daily_profiles() 0 33 2
A delete_mvgd_ts_2035() 0 6 1
A export_to_db() 0 57 3
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 cascade_heat_supply_indiv() 0 89 4
A delete_heat_peak_loads_2035() 0 8 2
A delete_heat_peak_loads_100RE() 0 8 2

3 Methods

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