create_district_heating_profile_python_like()   F
last analyzed

Complexity

Conditions 11

Size

Total Lines 220
Code Lines 101

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 101
dl 0
loc 220
rs 3.78
c 0
b 0
f 0
cc 11
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like data.datasets.heat_demand_timeseries.create_district_heating_profile_python_like() 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
from datetime import date, datetime
2
from pathlib import Path
3
import json
4
import os
5
import time
6
import warnings
7
8
from sqlalchemy import ARRAY, Column, Float, Integer, String, Text
9
from sqlalchemy.ext.declarative import declarative_base
10
import geopandas as gpd
11
import numpy as np
12
import pandas as pd
13
14
from egon.data import config, db
15
import egon.data.datasets.era5 as era
16
17
try:
18
    from disaggregator import temporal
19
except ImportError as e:
20
    pass
21
22
from math import ceil
23
24
from egon.data.datasets import Dataset
25
from egon.data.datasets.heat_demand_timeseries.daily import (
26
    daily_demand_shares_per_climate_zone,
27
    map_climate_zones_to_zensus,
28
)
29
from egon.data.datasets.heat_demand_timeseries.idp_pool import create, select
30
from egon.data.datasets.heat_demand_timeseries.service_sector import (
31
    CTS_demand_scale,
32
)
33
from egon.data.metadata import (
34
    context,
35
    license_egon_data_odbl,
36
    meta_metadata,
37
    sources,
38
)
39
40
Base = declarative_base()
41
42
43
class EgonTimeseriesDistrictHeating(Base):
44
    __tablename__ = "egon_timeseries_district_heating"
45
    __table_args__ = {"schema": "demand"}
46
    area_id = Column(Integer, primary_key=True)
47
    scenario = Column(Text, primary_key=True)
48
    dist_aggregated_mw = Column(ARRAY(Float(53)))
49
50
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(Text, primary_key=True)
56
    dist_aggregated_mw = Column(ARRAY(Float(53)))
57
58
59
class EgonIndividualHeatingPeakLoads(Base):
60
    __tablename__ = "egon_individual_heating_peak_loads"
61
    __table_args__ = {"schema": "demand"}
62
    building_id = Column(Integer, primary_key=True)
63
    scenario = Column(Text, primary_key=True)
64
    w_th = Column(Float(53))
65
66
67
class EgonEtragoHeatCts(Base):
68
    __tablename__ = "egon_etrago_heat_cts"
69
    __table_args__ = {"schema": "demand"}
70
71
    bus_id = Column(Integer, primary_key=True)
72
    scn_name = Column(String, primary_key=True)
73
    p_set = Column(ARRAY(Float))
74
75
76
def create_timeseries_for_building(building_id, scenario):
77
    """Generates final heat demand timeseries for a specific building
78
79
    Parameters
80
    ----------
81
    building_id : int
82
        Index of the selected building
83
    scenario : str
84
        Name of the selected scenario.
85
86
    Returns
87
    -------
88
    pandas.DataFrame
89
        Hourly heat demand timeseries in MW for the selected building
90
91
    """
92
93
    return db.select_dataframe(
94
        f"""
95
        SELECT building_demand * UNNEST(idp) as demand
96
        FROM
97
        (
98
        SELECT
99
            demand.demand
100
            / building.count
101
            * daily_demand.daily_demand_share as building_demand,
102
            daily_demand.day_of_year
103
        FROM
104
105
        (SELECT demand FROM
106
        demand.egon_peta_heat
107
        WHERE scenario = '{scenario}'
108
        AND sector = 'residential'
109
        AND zensus_population_id IN(
110
        SELECT zensus_population_id FROM
111
        demand.egon_heat_timeseries_selected_profiles
112
        WHERE building_id  = {building_id})) as demand,
113
114
        (SELECT COUNT(building_id)
115
        FROM demand.egon_heat_timeseries_selected_profiles
116
        WHERE zensus_population_id IN(
117
        SELECT zensus_population_id FROM
118
        demand.egon_heat_timeseries_selected_profiles
119
        WHERE building_id  = {building_id})) as building,
120
121
        (SELECT daily_demand_share, day_of_year FROM
122
        demand.egon_daily_heat_demand_per_climate_zone
123
        WHERE climate_zone = (
124
            SELECT climate_zone FROM boundaries.egon_map_zensus_climate_zones
125
            WHERE zensus_population_id =
126
            (
127
                SELECT zensus_population_id
128
                FROM demand.egon_heat_timeseries_selected_profiles
129
                WHERE building_id = {building_id}
130
            )
131
        )) as daily_demand) as daily_demand
132
133
        JOIN (SELECT b.idp, ordinality as day
134
        FROM demand.egon_heat_timeseries_selected_profiles a,
135
        UNNEST (a.selected_idp_profiles) WITH ORDINALITY as selected_idp
136
        JOIN demand.egon_heat_idp_pool b
137
        ON selected_idp = b.index
138
        WHERE a.building_id = {building_id}) as demand_profile
139
        ON demand_profile.day = daily_demand.day_of_year
140
        """
141
    )
142
143
144
def create_district_heating_profile(scenario, area_id):
145
    """Create a heat demand profile for a district heating grid.
146
147
    The created heat demand profile includes the demands of households
148
    and the service sector.
149
150
    Parameters
151
    ----------
152
    scenario : str
153
        The name of the selected scenario.
154
    area_id : int
155
        The index of the selected district heating grid.
156
157
    Returns
158
    -------
159
    pd.DataFrame
160
        An hourly heat demand timeseries in MW for the selected district
161
        heating grid.
162
163
    """
164
165
    start_time = datetime.now()
166
167
    df = db.select_dataframe(
168
        f"""
169
170
        SELECT SUM(building_demand_per_hour) as demand_profile, hour_of_year
171
        FROM
172
173
        (
174
        SELECT demand.demand  *
175
        c.daily_demand_share * hourly_demand as building_demand_per_hour,
176
        ordinality + 24* (c.day_of_year-1) as hour_of_year,
177
        demand_profile.building_id,
178
        c.day_of_year,
179
        ordinality
180
181
        FROM
182
183
        (SELECT zensus_population_id, demand FROM
184
        demand.egon_peta_heat
185
        WHERE scenario = '{scenario}'
186
        AND sector = 'residential'
187
        AND zensus_population_id IN(
188
        SELECT zensus_population_id FROM
189
        demand.egon_map_zensus_district_heating_areas
190
        WHERE scenario = '{scenario}'
191
        AND area_id = {area_id}
192
        )) as demand
193
194
        JOIN boundaries.egon_map_zensus_climate_zones b
195
        ON demand.zensus_population_id = b.zensus_population_id
196
197
        JOIN demand.egon_daily_heat_demand_per_climate_zone c
198
        ON c.climate_zone = b.climate_zone
199
200
        JOIN (
201
        SELECT e.idp, ordinality as day, zensus_population_id, building_id
202
        FROM demand.egon_heat_timeseries_selected_profiles d,
203
        UNNEST (d.selected_idp_profiles) WITH ORDINALITY as selected_idp
204
        JOIN demand.egon_heat_idp_pool e
205
        ON selected_idp = e.index
206
        WHERE zensus_population_id IN (
207
        SELECT zensus_population_id FROM
208
        demand.egon_map_zensus_district_heating_areas
209
        WHERE scenario = '{scenario}'
210
        AND area_id = {area_id}
211
        ))  demand_profile
212
        ON (demand_profile.day = c.day_of_year AND
213
            demand_profile.zensus_population_id = b.zensus_population_id)
214
215
        JOIN (SELECT COUNT(building_id), zensus_population_id
216
        FROM demand.egon_heat_timeseries_selected_profiles
217
        WHERE zensus_population_id IN(
218
        SELECT zensus_population_id FROM
219
        demand.egon_heat_timeseries_selected_profiles
220
       WHERE zensus_population_id IN (
221
       SELECT zensus_population_id FROM
222
       demand.egon_map_zensus_district_heating_areas
223
       WHERE scenario = '{scenario}'
224
       AND area_id = {area_id}
225
       ))
226
        GROUP BY zensus_population_id) building
227
        ON building.zensus_population_id = b.zensus_population_id,
228
229
        UNNEST(demand_profile.idp) WITH ORDINALITY as hourly_demand
230
        )   result
231
232
233
        GROUP BY hour_of_year
234
235
        """
236
    )
237
238
    print(
239
        f"Time to create time series for district heating grid {scenario}"
240
        f" {area_id}:\n{datetime.now() - start_time}"
241
    )
242
243
    return df
244
245
246
def create_district_heating_profile_python_like(scenario="eGon2035"):
247
    """Creates profiles for all district heating grids in one scenario.
248
    Similar to create_district_heating_profile but faster and needs more RAM.
249
    The results are directly written into the database.
250
251
    Parameters
252
    ----------
253
    scenario : str
254
        Name of the selected scenario.
255
256
    Returns
257
    -------
258
    None.
259
260
    """
261
262
    start_time = datetime.now()
263
264
    idp_df = db.select_dataframe(
265
        """
266
        SELECT index, idp FROM demand.egon_heat_idp_pool
267
        """,
268
        index_col="index",
269
    )
270
271
    district_heating_grids = db.select_dataframe(
272
        f"""
273
        SELECT area_id
274
        FROM demand.egon_district_heating_areas
275
        WHERE scenario = '{scenario}'
276
        """
277
    )
278
279
    annual_demand = db.select_dataframe(
280
        f"""
281
        SELECT
282
            a.zensus_population_id,
283
            demand / c.count as per_building,
284
            area_id,
285
            demand as demand_total
286
        FROM
287
        demand.egon_peta_heat a
288
        INNER JOIN (
289
            SELECT * FROM demand.egon_map_zensus_district_heating_areas
290
            WHERE scenario = '{scenario}'
291
        ) b ON a.zensus_population_id = b.zensus_population_id
292
293
        JOIN (SELECT COUNT(building_id), zensus_population_id
294
        FROM demand.egon_heat_timeseries_selected_profiles
295
        WHERE zensus_population_id IN(
296
        SELECT zensus_population_id FROM
297
        demand.egon_heat_timeseries_selected_profiles
298
        WHERE zensus_population_id IN (
299
        SELECT zensus_population_id FROM
300
        boundaries.egon_map_zensus_grid_districts
301
       ))
302
        GROUP BY zensus_population_id)c
303
        ON a.zensus_population_id = c.zensus_population_id
304
305
        WHERE a.scenario = '{scenario}'
306
        AND a.sector = 'residential'
307
308
        """,
309
        index_col="zensus_population_id",
310
    )
311
312
    # Assign zensus cells that are part of two district heating grids to
313
    # one of them
314
    annual_demand = annual_demand[
315
        ~annual_demand.index.duplicated(keep="first")
316
    ]
317
318
    daily_demand_shares = db.select_dataframe(
319
        """
320
        SELECT climate_zone, day_of_year as day, daily_demand_share FROM
321
        demand.egon_daily_heat_demand_per_climate_zone
322
        """
323
    )
324
325
    CTS_demand_dist, CTS_demand_grid, CTS_demand_zensus = CTS_demand_scale(
326
        aggregation_level="district"
327
    )
328
329
    print(datetime.now() - start_time)
330
331
    start_time = datetime.now()
332
    for area in district_heating_grids.area_id.unique():
333
        with db.session_scope() as session:
334
            selected_profiles = db.select_dataframe(
335
                f"""
336
                SELECT a.zensus_population_id, building_id, c.climate_zone,
337
                selected_idp, ordinality as day, b.area_id
338
                FROM demand.egon_heat_timeseries_selected_profiles a
339
                INNER JOIN boundaries.egon_map_zensus_climate_zones c
340
                ON a.zensus_population_id = c.zensus_population_id
341
                INNER JOIN (
342
                    SELECT * FROM demand.egon_map_zensus_district_heating_areas
343
                    WHERE scenario = '{scenario}'
344
                    AND area_id = '{area}'
345
                ) b ON a.zensus_population_id = b.zensus_population_id        ,
346
347
                UNNEST (selected_idp_profiles) WITH ORDINALITY as selected_idp
348
349
                """
350
            )
351
352
            # Exclude profiles of zensus cells that are in two district
353
            # heating grids and added to the other one in the lines above
354
            selected_profiles = selected_profiles[
355
                selected_profiles.zensus_population_id.isin(
356
                    annual_demand[annual_demand.area_id == area].index
357
                )
358
            ]
359
360
            if not selected_profiles.empty:
361
                df = pd.merge(
362
                    selected_profiles,
363
                    daily_demand_shares,
364
                    on=["day", "climate_zone"],
365
                )
366
367
                slice_df = pd.merge(
368
                    df[df.area_id == area],
369
                    idp_df,
370
                    left_on="selected_idp",
371
                    right_on="index",
372
                )
373
374
                # Drop cells without a demand or outside of MVGD
375
                slice_df = slice_df[
376
                    slice_df.zensus_population_id.isin(annual_demand.index)
377
                ]
378
379
                for hour in range(24):
380
                    slice_df[hour] = (
381
                        slice_df.idp.str[hour]
382
                        .mul(slice_df.daily_demand_share)
383
                        .mul(
384
                            annual_demand.loc[
385
                                slice_df.zensus_population_id.values,
386
                                "per_building",
387
                            ].values
388
                        )
389
                    )
390
391
                diff = (
392
                    slice_df[range(24)].sum().sum()
393
                    - annual_demand[
394
                        annual_demand.area_id == area
395
                    ].demand_total.sum()
396
                ) / (
397
                    annual_demand[annual_demand.area_id == area].demand_total.sum()
398
                )
399
400
                assert (
401
                    abs(diff) < 0.04
402
                ), f"""Deviation of residential heat demand time
403
                series for district heating grid {str(area)} is {diff}"""
404
405
                if abs(diff) > 0.03:
406
                    warnings.warn(
407
                        f"""Deviation of residential heat demand time
408
                    series for district heating grid {str(area)} is {diff}"""
409
                    )
410
411
                hh = np.concatenate(
412
                    slice_df.drop(
413
                        [
414
                            "zensus_population_id",
415
                            "building_id",
416
                            "climate_zone",
417
                            "selected_idp",
418
                            "area_id",
419
                            "daily_demand_share",
420
                            "idp",
421
                        ],
422
                        axis="columns",
423
                    )
424
                    .groupby("day")
425
                    .sum()[range(24)]
426
                    .values
427
                ).ravel()
428
429
            cts = CTS_demand_dist[
430
                (CTS_demand_dist.scenario == scenario)
431
                & (CTS_demand_dist.index == area)
432
            ].drop("scenario", axis="columns")
433
434
            if (not selected_profiles.empty) and not cts.empty:
435
                entry = EgonTimeseriesDistrictHeating(
436
                    area_id=int(area),
437
                    scenario=scenario,
438
                    dist_aggregated_mw=(hh + cts.values[0]).tolist(),
0 ignored issues
show
introduced by
The variable hh does not seem to be defined for all execution paths.
Loading history...
439
                )
440
            elif (not selected_profiles.empty) and cts.empty:
441
                entry = EgonTimeseriesDistrictHeating(
442
                    area_id=int(area),
443
                    scenario=scenario,
444
                    dist_aggregated_mw=(hh).tolist(),
445
                )
446
            elif not cts.empty:
447
                entry = EgonTimeseriesDistrictHeating(
448
                    area_id=int(area),
449
                    scenario=scenario,
450
                    dist_aggregated_mw=(cts.values[0]).tolist(),
451
                )
452
            else:
453
                entry = EgonTimeseriesDistrictHeating(
454
                    area_id=int(area),
455
                    scenario=scenario,
456
                    dist_aggregated_mw=np.repeat(0, 8760).tolist(),
457
                )
458
459
            session.add(entry)
460
        session.commit()
461
462
    print(
463
        f"Time to create time series for district heating scenario {scenario}"
464
    )
465
    print(datetime.now() - start_time)
466
467
468
def create_individual_heat_per_mv_grid(scenario="eGon2035", mv_grid_id=1564):
469
    start_time = datetime.now()
470
    df = db.select_dataframe(
471
        f"""
472
473
        SELECT SUM(building_demand_per_hour) as demand_profile, hour_of_year
474
        FROM
475
476
        (
477
        SELECT demand.demand  *
478
        c.daily_demand_share * hourly_demand as building_demand_per_hour,
479
        ordinality + 24* (c.day_of_year-1) as hour_of_year,
480
        demand_profile.building_id,
481
        c.day_of_year,
482
        ordinality
483
484
        FROM
485
486
        (SELECT zensus_population_id, demand FROM
487
        demand.egon_peta_heat
488
        WHERE scenario = '{scenario}'
489
        AND sector = 'residential'
490
        AND zensus_population_id IN (
491
        SELECT zensus_population_id FROM
492
        boundaries.egon_map_zensus_grid_districts
493
        WHERE bus_id = {mv_grid_id}
494
        )) as demand
495
496
        JOIN boundaries.egon_map_zensus_climate_zones b
497
        ON demand.zensus_population_id = b.zensus_population_id
498
499
        JOIN demand.egon_daily_heat_demand_per_climate_zone c
500
        ON c.climate_zone = b.climate_zone
501
502
        JOIN (
503
        SELECT
504
            e.idp, ordinality as day, zensus_population_id, building_id
505
        FROM demand.egon_heat_timeseries_selected_profiles d,
506
        UNNEST (d.selected_idp_profiles) WITH ORDINALITY as selected_idp
507
        JOIN demand.egon_heat_idp_pool e
508
        ON selected_idp = e.index
509
        WHERE zensus_population_id IN (
510
        SELECT zensus_population_id FROM
511
        boundaries.egon_map_zensus_grid_districts
512
        WHERE bus_id = {mv_grid_id}
513
        ))  demand_profile
514
        ON (demand_profile.day = c.day_of_year AND
515
            demand_profile.zensus_population_id = b.zensus_population_id)
516
517
        JOIN (SELECT COUNT(building_id), zensus_population_id
518
        FROM demand.egon_heat_timeseries_selected_profiles
519
        WHERE zensus_population_id IN(
520
        SELECT zensus_population_id FROM
521
        demand.egon_heat_timeseries_selected_profiles
522
        WHERE zensus_population_id IN (
523
        SELECT zensus_population_id FROM
524
        boundaries.egon_map_zensus_grid_districts
525
        WHERE bus_id = {mv_grid_id}
526
       ))
527
        GROUP BY zensus_population_id) building
528
        ON building.zensus_population_id = b.zensus_population_id,
529
530
        UNNEST(demand_profile.idp) WITH ORDINALITY as hourly_demand
531
        )   result
532
533
534
        GROUP BY hour_of_year
535
536
        """
537
    )
538
539
    print(f"Time to create time series for mv grid {scenario} {mv_grid_id}:")
540
    print(datetime.now() - start_time)
541
542
    return df
543
544
545
def calulate_peak_load(df, scenario):
546
    # peat load in W_th
547
    data = (
548
        df.groupby("building_id")
549
        .max()[range(24)]
550
        .max(axis=1)
551
        .mul(1000000)
552
        .astype(int)
553
        .reset_index()
554
    )
555
556
    data["scenario"] = scenario
557
558
    data.rename({0: "w_th"}, axis="columns", inplace=True)
559
560
    data.to_sql(
561
        EgonIndividualHeatingPeakLoads.__table__.name,
562
        schema=EgonIndividualHeatingPeakLoads.__table__.schema,
563
        con=db.engine(),
564
        if_exists="append",
565
        index=False,
566
    )
567
568
569
def create_individual_heating_peak_loads(scenario="eGon2035"):
570
    engine = db.engine()
571
572
    EgonIndividualHeatingPeakLoads.__table__.drop(bind=engine, checkfirst=True)
573
574
    EgonIndividualHeatingPeakLoads.__table__.create(
575
        bind=engine, checkfirst=True
576
    )
577
578
    start_time = datetime.now()
579
580
    idp_df = db.select_dataframe(
581
        """
582
        SELECT index, idp FROM demand.egon_heat_idp_pool
583
        """,
584
        index_col="index",
585
    )
586
587
    annual_demand = db.select_dataframe(
588
        f"""
589
        SELECT a.zensus_population_id, demand/c.count as per_building, bus_id
590
        FROM demand.egon_peta_heat a
591
592
593
        JOIN (SELECT COUNT(building_id), zensus_population_id
594
        FROM demand.egon_heat_timeseries_selected_profiles
595
        WHERE zensus_population_id IN(
596
        SELECT zensus_population_id FROM
597
        demand.egon_heat_timeseries_selected_profiles
598
        WHERE zensus_population_id IN (
599
        SELECT zensus_population_id FROM
600
        boundaries.egon_map_zensus_grid_districts
601
       ))
602
        GROUP BY zensus_population_id)c
603
        ON a.zensus_population_id = c.zensus_population_id
604
605
        JOIN boundaries.egon_map_zensus_grid_districts d
606
        ON a.zensus_population_id = d.zensus_population_id
607
608
        WHERE a.scenario = '{scenario}'
609
        AND a.sector = 'residential'
610
        AND a.zensus_population_id NOT IN (
611
            SELECT zensus_population_id
612
            FROM demand.egon_map_zensus_district_heating_areas
613
            WHERE scenario = '{scenario}'
614
        )
615
616
        """,
617
        index_col="zensus_population_id",
618
    )
619
620
    daily_demand_shares = db.select_dataframe(
621
        """
622
        SELECT climate_zone, day_of_year as day, daily_demand_share FROM
623
        demand.egon_daily_heat_demand_per_climate_zone
624
        """
625
    )
626
627
    start_time = datetime.now()
628
    for grid in annual_demand.bus_id.unique():
629
        selected_profiles = db.select_dataframe(
630
            f"""
631
            SELECT a.zensus_population_id, building_id, c.climate_zone,
632
            selected_idp, ordinality as day
633
            FROM demand.egon_heat_timeseries_selected_profiles a
634
            INNER JOIN boundaries.egon_map_zensus_climate_zones c
635
            ON a.zensus_population_id = c.zensus_population_id
636
            ,
637
638
            UNNEST (selected_idp_profiles) WITH ORDINALITY as selected_idp
639
640
            WHERE a.zensus_population_id NOT IN (
641
                SELECT zensus_population_id
642
                FROM demand.egon_map_zensus_district_heating_areas
643
                WHERE scenario = '{scenario}'
644
            )
645
            AND a.zensus_population_id IN (
646
                SELECT zensus_population_id
647
                FROM boundaries.egon_map_zensus_grid_districts
648
                WHERE bus_id = '{grid}'
649
            )
650
651
            """
652
        )
653
654
        df = pd.merge(
655
            selected_profiles, daily_demand_shares, on=["day", "climate_zone"]
656
        )
657
658
        slice_df = pd.merge(
659
            df, idp_df, left_on="selected_idp", right_on="index"
660
        )
661
662
        for hour in range(24):
663
            slice_df[hour] = (
664
                slice_df.idp.str[hour]
665
                .mul(slice_df.daily_demand_share)
666
                .mul(
667
                    annual_demand.loc[
668
                        slice_df.zensus_population_id.values, "per_building"
669
                    ].values
670
                )
671
            )
672
673
        calulate_peak_load(slice_df, scenario)
674
675
    print(f"Time to create peak loads per building for {scenario}")
676
    print(datetime.now() - start_time)
677
678
679
def create_individual_heating_profile_python_like(scenario="eGon2035"):
680
    start_time = datetime.now()
681
682
    idp_df = db.select_dataframe(
683
        f"""
684
        SELECT index, idp FROM demand.egon_heat_idp_pool
685
        """,
686
        index_col="index",
687
    )
688
689
    annual_demand = db.select_dataframe(
690
        f"""
691
        SELECT
692
            a.zensus_population_id,
693
            demand / c.count as per_building,
694
            demand as demand_total,
695
            bus_id
696
        FROM demand.egon_peta_heat a
697
698
699
        JOIN (SELECT COUNT(building_id), zensus_population_id
700
        FROM demand.egon_heat_timeseries_selected_profiles
701
        WHERE zensus_population_id IN(
702
        SELECT zensus_population_id FROM
703
        demand.egon_heat_timeseries_selected_profiles
704
        WHERE zensus_population_id IN (
705
        SELECT zensus_population_id FROM
706
        boundaries.egon_map_zensus_grid_districts
707
       ))
708
        GROUP BY zensus_population_id)c
709
        ON a.zensus_population_id = c.zensus_population_id
710
711
        JOIN boundaries.egon_map_zensus_grid_districts d
712
        ON a.zensus_population_id = d.zensus_population_id
713
714
        WHERE a.scenario = '{scenario}'
715
        AND a.sector = 'residential'
716
        AND a.zensus_population_id NOT IN (
717
            SELECT zensus_population_id
718
            FROM demand.egon_map_zensus_district_heating_areas
719
            WHERE scenario = '{scenario}'
720
        )
721
722
        """,
723
        index_col="zensus_population_id",
724
    )
725
726
    daily_demand_shares = db.select_dataframe(
727
        """
728
        SELECT climate_zone, day_of_year as day, daily_demand_share FROM
729
        demand.egon_daily_heat_demand_per_climate_zone
730
        """
731
    )
732
733
    CTS_demand_dist, CTS_demand_grid, CTS_demand_zensus = CTS_demand_scale(
734
        aggregation_level="district"
735
    )
736
737
    # TODO: use session_scope!
738
    from sqlalchemy.orm import sessionmaker
739
740
    session = sessionmaker(bind=db.engine())()
741
742
    print(
743
        f"Time to create overhead for time series for district heating scenario {scenario}"
744
    )
745
    print(datetime.now() - start_time)
746
747
    start_time = datetime.now()
748
    for grid in annual_demand.bus_id.unique():
749
        selected_profiles = db.select_dataframe(
750
            f"""
751
            SELECT a.zensus_population_id, building_id, c.climate_zone,
752
            selected_idp, ordinality as day
753
            FROM demand.egon_heat_timeseries_selected_profiles a
754
            INNER JOIN boundaries.egon_map_zensus_climate_zones c
755
            ON a.zensus_population_id = c.zensus_population_id
756
            ,
757
758
            UNNEST (selected_idp_profiles) WITH ORDINALITY as selected_idp
759
760
            WHERE a.zensus_population_id NOT IN (
761
                SELECT zensus_population_id FROM demand.egon_map_zensus_district_heating_areas
762
                WHERE scenario = '{scenario}'
763
            )
764
            AND a.zensus_population_id IN (
765
                SELECT zensus_population_id
766
                FROM boundaries.egon_map_zensus_grid_districts
767
                WHERE bus_id = '{grid}'
768
            )
769
770
            """
771
        )
772
773
        df = pd.merge(
774
            selected_profiles, daily_demand_shares, on=["day", "climate_zone"]
775
        )
776
777
        slice_df = pd.merge(
778
            df, idp_df, left_on="selected_idp", right_on="index"
779
        )
780
781
        for hour in range(24):
782
            slice_df[hour] = (
783
                slice_df.idp.str[hour]
784
                .mul(slice_df.daily_demand_share)
785
                .mul(
786
                    annual_demand.loc[
787
                        slice_df.zensus_population_id.values, "per_building"
788
                    ].values
789
                )
790
            )
791
792
        cts = CTS_demand_grid[
793
            (CTS_demand_grid.scenario == scenario)
794
            & (CTS_demand_grid.index == grid)
795
        ].drop("scenario", axis="columns")
796
797
        hh = np.concatenate(
798
            slice_df.groupby("day").sum()[range(24)].values
799
        ).ravel()
800
801
        diff = (
802
            slice_df.groupby("day").sum()[range(24)].sum().sum()
803
            - annual_demand[annual_demand.bus_id == grid].demand_total.sum()
804
        ) / (annual_demand[annual_demand.bus_id == grid].demand_total.sum())
805
806
        assert abs(diff) < 0.03, (
807
            "Deviation of residential heat demand time series for mv"
808
            f" grid {grid} is {diff}"
809
        )
810
811
        if not (slice_df[hour].empty or cts.empty):
0 ignored issues
show
introduced by
The variable hour does not seem to be defined for all execution paths.
Loading history...
812
            entry = EgonEtragoTimeseriesIndividualHeating(
813
                bus_id=int(grid),
814
                scenario=scenario,
815
                dist_aggregated_mw=(hh + cts.values[0]).tolist(),
816
            )
817
        elif not slice_df[hour].empty:
818
            entry = EgonEtragoTimeseriesIndividualHeating(
819
                bus_id=int(grid),
820
                scenario=scenario,
821
                dist_aggregated_mw=(hh).tolist(),
822
            )
823
        elif not cts.empty:
824
            entry = EgonEtragoTimeseriesIndividualHeating(
825
                bus_id=int(grid),
826
                scenario=scenario,
827
                dist_aggregated_mw=(cts).tolist(),
828
            )
829
830
        session.add(entry)
0 ignored issues
show
introduced by
The variable entry does not seem to be defined for all execution paths.
Loading history...
831
832
    session.commit()
833
834
    print(
835
        f"Time to create time series for district heating scenario {scenario}"
836
    )
837
    print(datetime.now() - start_time)
838
839
840
def district_heating(method="python"):
841
    engine = db.engine()
842
    EgonTimeseriesDistrictHeating.__table__.drop(bind=engine, checkfirst=True)
843
    EgonTimeseriesDistrictHeating.__table__.create(
844
        bind=engine, checkfirst=True
845
    )
846
847
    if method == "python":
848
        for scenario in config.settings()["egon-data"]["--scenarios"]:
849
            create_district_heating_profile_python_like(scenario)
850
851
    else:
852
        CTS_demand_dist, CTS_demand_grid, CTS_demand_zensus = CTS_demand_scale(
853
            aggregation_level="district"
854
        )
855
856
        ids = db.select_dataframe(
857
            """
858
            SELECT area_id, scenario
859
            FROM demand.egon_district_heating_areas
860
            """
861
        )
862
863
        df = pd.DataFrame(
864
            columns=["area_id", "scenario", "dist_aggregated_mw"]
865
        )
866
867
        for index, row in ids.iterrows():
868
            series = create_district_heating_profile(
869
                scenario=row.scenario, area_id=row.area_id
870
            )
871
872
            cts = (
873
                CTS_demand_dist[
874
                    (CTS_demand_dist.scenario == row.scenario)
875
                    & (CTS_demand_dist.index == row.area_id)
876
                ]
877
                .drop("scenario", axis="columns")
878
                .transpose()
879
            )
880
881
            if not cts.empty:
882
                data = (
883
                    cts[row.area_id] + series.demand_profile
884
                ).values.tolist()
885
            else:
886
                data = series.demand_profile.values.tolist()
887
888
            df = df.append(
889
                pd.Series(
890
                    data={
891
                        "area_id": row.area_id,
892
                        "scenario": row.scenario,
893
                        "dist_aggregated_mw": data,
894
                    },
895
                ),
896
                ignore_index=True,
897
            )
898
899
        df.to_sql(
900
            "egon_timeseries_district_heating",
901
            schema="demand",
902
            con=db.engine(),
903
            if_exists="append",
904
            index=False,
905
        )
906
907
908
def individual_heating_per_mv_grid_tables(method="python"):
909
    engine = db.engine()
910
    EgonEtragoTimeseriesIndividualHeating.__table__.drop(
911
        bind=engine, checkfirst=True
912
    )
913
    EgonEtragoTimeseriesIndividualHeating.__table__.create(
914
        bind=engine, checkfirst=True
915
    )
916
917
918
def individual_heating_per_mv_grid_2035(method="python"):
919
    create_individual_heating_profile_python_like("eGon2035")
920
921
922
def individual_heating_per_mv_grid_100(method="python"):
923
    create_individual_heating_profile_python_like("eGon100RE")
924
925
926
def individual_heating_per_mv_grid(method="python"):
927
    if method == "python":
928
        engine = db.engine()
929
        EgonEtragoTimeseriesIndividualHeating.__table__.drop(
930
            bind=engine, checkfirst=True
931
        )
932
        EgonEtragoTimeseriesIndividualHeating.__table__.create(
933
            bind=engine, checkfirst=True
934
        )
935
936
        create_individual_heating_profile_python_like("eGon2035")
937
        create_individual_heating_profile_python_like("eGon100RE")
938
939
    else:
940
        engine = db.engine()
941
        EgonEtragoTimeseriesIndividualHeating.__table__.drop(
942
            bind=engine, checkfirst=True
943
        )
944
        EgonEtragoTimeseriesIndividualHeating.__table__.create(
945
            bind=engine, checkfirst=True
946
        )
947
948
        CTS_demand_dist, CTS_demand_grid, CTS_demand_zensus = CTS_demand_scale(
949
            aggregation_level="district"
950
        )
951
        df = pd.DataFrame(columns=["bus_id", "scenario", "dist_aggregated_mw"])
952
953
        ids = db.select_dataframe(
954
            """
955
            SELECT bus_id
956
            FROM grid.egon_mv_grid_district
957
            """
958
        )
959
960
        for index, row in ids.iterrows():
961
            for scenario in ["eGon2035", "eGon100RE"]:
962
                series = create_individual_heat_per_mv_grid(
963
                    scenario, row.bus_id
964
                )
965
                cts = (
966
                    CTS_demand_grid[
967
                        (CTS_demand_grid.scenario == scenario)
968
                        & (CTS_demand_grid.index == row.bus_id)
969
                    ]
970
                    .drop("scenario", axis="columns")
971
                    .transpose()
972
                )
973
                if not cts.empty:
974
                    data = (
975
                        cts[row.bus_id] + series.demand_profile
976
                    ).values.tolist()
977
                else:
978
                    data = series.demand_profile.values.tolist()
979
980
                df = df.append(
981
                    pd.Series(
982
                        data={
983
                            "bus_id": row.bus_id,
984
                            "scenario": scenario,
985
                            "dist_aggregated_mw": data,
986
                        },
987
                    ),
988
                    ignore_index=True,
989
                )
990
991
        df.to_sql(
992
            "egon_etrago_timeseries_individual_heating",
993
            schema="demand",
994
            con=db.engine(),
995
            if_exists="append",
996
            index=False,
997
        )
998
999
1000
def store_national_profiles():
1001
    scenario = "eGon100RE"
1002
1003
    df = db.select_dataframe(
1004
        f"""
1005
1006
        SELECT SUM(building_demand_per_hour) as "residential rural"
1007
        FROM
1008
1009
        (
1010
        SELECT demand.demand  / building.count *
1011
        c.daily_demand_share * hourly_demand as building_demand_per_hour,
1012
        ordinality + 24* (c.day_of_year-1) as hour_of_year,
1013
        demand_profile.building_id,
1014
        c.day_of_year,
1015
        ordinality
1016
1017
        FROM
1018
1019
        (SELECT zensus_population_id, demand FROM
1020
        demand.egon_peta_heat
1021
        WHERE scenario = '{scenario}'
1022
        AND sector = 'residential'
1023
       ) as demand
1024
1025
        JOIN boundaries.egon_map_zensus_climate_zones b
1026
        ON demand.zensus_population_id = b.zensus_population_id
1027
1028
        JOIN demand.egon_daily_heat_demand_per_climate_zone c
1029
        ON c.climate_zone = b.climate_zone
1030
1031
        JOIN (
1032
        SELECT e.idp, ordinality as day, zensus_population_id, building_id
1033
        FROM demand.egon_heat_timeseries_selected_profiles d,
1034
        UNNEST (d.selected_idp_profiles) WITH ORDINALITY as selected_idp
1035
        JOIN demand.egon_heat_idp_pool e
1036
        ON selected_idp = e.index
1037
        )  demand_profile
1038
        ON (demand_profile.day = c.day_of_year AND
1039
            demand_profile.zensus_population_id = b.zensus_population_id)
1040
1041
        JOIN (SELECT COUNT(building_id), zensus_population_id
1042
        FROM demand.egon_heat_timeseries_selected_profiles
1043
        WHERE zensus_population_id IN(
1044
        SELECT zensus_population_id FROM
1045
        demand.egon_heat_timeseries_selected_profiles
1046
        )
1047
        GROUP BY zensus_population_id) building
1048
        ON building.zensus_population_id = b.zensus_population_id,
1049
1050
        UNNEST(demand_profile.idp) WITH ORDINALITY as hourly_demand
1051
        )   result
1052
1053
1054
        GROUP BY hour_of_year
1055
1056
        """
1057
    )
1058
1059
    CTS_demand_dist, CTS_demand_grid, CTS_demand_zensus = CTS_demand_scale(
1060
        aggregation_level="district"
1061
    )
1062
1063
    df["service rural"] = (
1064
        CTS_demand_dist.loc[CTS_demand_dist.scenario == scenario]
1065
        .drop("scenario", axis=1)
1066
        .sum()
1067
    )
1068
1069
    df["urban central"] = db.select_dataframe(
1070
        f"""
1071
        SELECT sum(nullif(demand, 'NaN')) as "urban central"
1072
1073
        FROM demand.egon_timeseries_district_heating,
1074
        UNNEST (dist_aggregated_mw) WITH ORDINALITY as demand
1075
1076
        WHERE scenario = '{scenario}'
1077
1078
        GROUP BY ordinality
1079
1080
        """
1081
    )
1082
1083
    folder = Path(".") / "input-pypsa-eur-sec"
1084
    # Create the folder, if it does not exists already
1085
    if not os.path.exists(folder):
1086
        os.mkdir(folder)
1087
1088
    df.to_csv(folder / f"heat_demand_timeseries_DE_{scenario}.csv")
1089
1090
1091
def export_etrago_cts_heat_profiles():
1092
    """Export heat cts load profiles at mv substation level
1093
    to etrago-table in the database
1094
1095
    Returns
1096
    -------
1097
    None.
1098
1099
    """
1100
1101
    # Calculate cts heat profiles at substation
1102
    _, CTS_grid, _ = CTS_demand_scale("district")
1103
1104
    # Change format
1105
    data = CTS_grid.drop(columns="scenario")
1106
    df_etrago_cts_heat_profiles = pd.DataFrame(
1107
        index=data.index, columns=["scn_name", "p_set"]
1108
    )
1109
    df_etrago_cts_heat_profiles.p_set = data.values.tolist()
1110
    df_etrago_cts_heat_profiles.scn_name = CTS_grid["scenario"]
1111
    df_etrago_cts_heat_profiles.reset_index(inplace=True)
1112
1113
    # Drop and recreate Table if exists
1114
    EgonEtragoHeatCts.__table__.drop(bind=db.engine(), checkfirst=True)
1115
    EgonEtragoHeatCts.__table__.create(bind=db.engine(), checkfirst=True)
1116
1117
    # Write heat ts into db
1118
    with db.session_scope() as session:
1119
        session.bulk_insert_mappings(
1120
            EgonEtragoHeatCts,
1121
            df_etrago_cts_heat_profiles.to_dict(orient="records"),
1122
        )
1123
1124
1125
def metadata():
1126
    fields = [
1127
        {
1128
            "description": "Index of corresponding district heating area",
1129
            "name": "area_id",
1130
            "type": "integer",
1131
            "unit": "none",
1132
        },
1133
        {
1134
            "description": "Name of scenario",
1135
            "name": "scenario",
1136
            "type": "str",
1137
            "unit": "none",
1138
        },
1139
        {
1140
            "description": "Heat demand time series",
1141
            "name": "dist_aggregated_mw",
1142
            "type": "array of floats",
1143
            "unit": "MW",
1144
        },
1145
    ]
1146
1147
    meta_district = {
1148
        "name": "demand.egon_timeseries_district_heating",
1149
        "title": "eGon heat demand time series for district heating grids",
1150
        "id": "WILL_BE_SET_AT_PUBLICATION",
1151
        "description": "Heat demand time series for district heating grids",
1152
        "language": ["EN"],
1153
        "publicationDate": date.today().isoformat(),
1154
        "context": context(),
1155
        "spatial": {
1156
            "location": None,
1157
            "extent": "Germany",
1158
            "resolution": None,
1159
        },
1160
        "sources": [
1161
            sources()["era5"],
1162
            sources()["vg250"],
1163
            sources()["egon-data"],
1164
            sources()["egon-data_bundle"],
1165
            sources()["peta"],
1166
        ],
1167
        "licenses": [license_egon_data_odbl()],
1168
        "contributors": [
1169
            {
1170
                "title": "Clara Büttner",
1171
                "email": "http://github.com/ClaraBuettner",
1172
                "date": time.strftime("%Y-%m-%d"),
1173
                "object": None,
1174
                "comment": "Imported data",
1175
            },
1176
        ],
1177
        "resources": [
1178
            {
1179
                "profile": "tabular-data-resource",
1180
                "name": "demand.egon_timeseries_district_heating",
1181
                "path": None,
1182
                "format": "PostgreSQL",
1183
                "encoding": "UTF-8",
1184
                "schema": {
1185
                    "fields": fields,
1186
                    "primaryKey": ["index"],
1187
                    "foreignKeys": [],
1188
                },
1189
                "dialect": {"delimiter": None, "decimalSeparator": "."},
1190
            }
1191
        ],
1192
        "metaMetadata": meta_metadata(),
1193
    }
1194
1195
    # Add metadata as a comment to the table
1196
    db.submit_comment(
1197
        "'" + json.dumps(meta_district) + "'",
1198
        EgonTimeseriesDistrictHeating.__table__.schema,
1199
        EgonTimeseriesDistrictHeating.__table__.name,
1200
    )
1201
1202
1203
class HeatTimeSeries(Dataset):
1204
    """
1205
    Chooses heat demand profiles for each residential and CTS building
1206
1207
    This dataset creates heat demand profiles in an hourly resoultion.
1208
    Time series for CTS buildings are created using the SLP-gas method implemented
1209
    in the demandregio disagregator with the function :py:func:`export_etrago_cts_heat_profiles`
1210
    and stored in the database.
1211
    Time series for residential buildings are created based on a variety of synthetical created
1212
    individual demand profiles that are part of :py:class:`DataBundle <egon.data.datasets.data_bundle.DataBundle>`.
1213
    This method is desribed within the functions and in this publication:
1214
1215
    C. Büttner, J. Amme, J. Endres, A. Malla, B. Schachler, I. Cußmann,
1216
    Open modeling of electricity and heat demand curves for all
1217
    residential buildings in Germany, Energy Informatics 5 (1) (2022) 21.
1218
    doi:10.1186/s42162-022-00201-y.
1219
1220
1221
    *Dependencies*
1222
      * :py:class:`DataBundle <egon.data.datasets.data_bundle.DataBundle>`
1223
      * :py:class:`DemandRegio <egon.data.datasets.demandregio.DemandRegio>`
1224
      * :py:class:`HeatDemandImport <egon.data.datasets.heat_demand.HeatDemandImport>`
1225
      * :py:class:`DistrictHeatingAreas <egon.data.datasets.district_heating_areas.DistrictHeatingAreas>`
1226
      * :py:class:`Vg250 <egon.data.datasets.vg250.Vg250>`
1227
      * :py:class:`ZensusMvGridDistricts <egon.data.datasets.zensus_mv_grid_districts.ZensusMvGridDistricts>`
1228
      * :py:func:`hh_demand_buildings_setup <egon.data.datasets.electricity_demand_timeseries.hh_buildings.map_houseprofiles_to_buildings>`
1229
      * :py:class:`WeatherData <egon.data.datasets.era5.WeatherData>`
1230
1231
1232
    *Resulting tables*
1233
      * :py:class:`demand.egon_timeseries_district_heating <egon.data.datasets.heat_demand_timeseries.EgonTimeseriesDistrictHeating>` is created and filled
1234
      * :py:class:`demand.egon_etrago_heat_cts <egon.data.datasets.heat_demand_timeseries.EgonEtragoHeatCts>` is created and filled
1235
      * :py:class:`demand.egon_heat_timeseries_selected_profiles <egon.data.datasets.heat_demand_timeseries.idp_pool.EgonHeatTimeseries>` is created and filled
1236
      * :py:class:`demand.egon_daily_heat_demand_per_climate_zone <egon.data.datasets.heat_demand_timeseries.daily.EgonDailyHeatDemandPerClimateZone>`
1237
        is created and filled
1238
      * :py:class:`boundaries.egon_map_zensus_climate_zones <egon.data.datasets.heat_demand_timeseries.daily.EgonMapZensusClimateZones>` is created and filled
1239
1240
    """
1241
1242
    #:
1243
    name: str = "HeatTimeSeries"
1244
    #:
1245
    version: str = "0.0.12"
1246
1247
    def __init__(self, dependencies):
1248
        super().__init__(
1249
            name=self.name,
1250
            version=self.version,
1251
            dependencies=dependencies,
1252
            tasks=(
1253
                {
1254
                    export_etrago_cts_heat_profiles,
1255
                    map_climate_zones_to_zensus,
1256
                    daily_demand_shares_per_climate_zone,
1257
                    create,
1258
                },
1259
                select,
1260
                district_heating,
1261
                metadata,
1262
                store_national_profiles,
1263
            ),
1264
        )
1265