Passed
Pull Request — dev (#905)
by
unknown
02:05
created

data.datasets.heat_supply.individual_heating   F

Complexity

Total Complexity 68

Size/Duplication

Total Lines 1644
Duplicated Lines 1.46 %

Importance

Changes 0
Metric Value
wmc 68
eloc 716
dl 24
loc 1644
rs 2.844
c 0
b 0
f 0

35 Functions

Rating   Name   Duplication   Size   Complexity  
B determine_buildings_with_hp_in_mv_grid() 0 100 2
A determine_minimum_hp_capacity_per_building() 0 24 1
A get_heat_peak_demand_per_building() 0 21 3
A determine_hp_cap_buildings_eGon100RE() 0 39 1
A create_hp_capacity_table() 0 4 1
A get_zensus_cells_with_decentral_heat_demand_in_mv_grid() 0 61 2
A delete_peak_loads_if_existing() 0 9 2
A plot_heat_supply() 24 31 2
A aggregate_heat_profiles() 0 49 1
A get_peta_demand() 0 39 2
A timeit() 0 15 1
A adapt_numpy_int64() 0 2 1
A get_daily_demand_share() 0 30 2
A create_egon_etrago_timeseries_individual_heating() 0 7 1
A get_cts_buildings_with_decentral_heat_demand_in_mv_grid() 0 48 2
B cascade_per_technology() 0 114 6
A timeitlog() 0 23 2
A determine_min_hp_cap_pypsa_eur_sec() 0 29 2
A determine_hp_capacity() 0 20 1
A get_residential_buildings_with_decentral_heat_demand_in_mv_grid() 0 51 2
A adapt_numpy_float64() 0 2 1
A get_total_heat_pump_capacity_of_mv_grid() 0 48 2
A aggregate_residential_and_cts_profiles() 0 55 1
A get_buildings_with_decentral_heat_demand_in_mv_grid() 0 48 1
A get_residential_heat_profile_ids() 0 49 2
A desaggregate_hp_capacity() 0 35 1
A determine_hp_cap_buildings_eGon2035() 0 46 2
A get_daily_profiles() 0 30 2
A export_to_db() 0 46 1
B calc_residential_heat_profiles_per_mvgd() 0 96 3
B determine_hp_cap_peak_load_mvgd_ts() 0 113 2
A export_to_csv() 0 12 3
A log_to_file() 0 14 1
A create_peak_load_table() 0 4 1
A cascade_heat_supply_indiv() 0 89 4

2 Methods

Rating   Name   Duplication   Size   Complexity  
A HeatPumps2050.__init__() 0 6 1
B HeatPumpsPypsaEurSecAnd2035.__init__() 0 71 3

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