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

data.datasets.heat_supply.individual_heating   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 1711
Duplicated Lines 1.4 %

Importance

Changes 0
Metric Value
wmc 69
eloc 693
dl 24
loc 1711
rs 2.787
c 0
b 0
f 0

2 Methods

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

33 Functions

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