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

data.datasets.heat_supply.individual_heating   F

Complexity

Total Complexity 87

Size/Duplication

Total Lines 1935
Duplicated Lines 1.24 %

Importance

Changes 0
Metric Value
wmc 87
eloc 774
dl 24
loc 1935
rs 1.826
c 0
b 0
f 0

3 Methods

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

38 Functions

Rating   Name   Duplication   Size   Complexity  
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 delete_heat_peak_loads_2035() 0 8 2
A adapt_numpy_int64() 0 2 1
A delete_hp_capacity() 0 15 2
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 delete_heat_peak_loads_100RE() 0 8 2
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

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