Passed
Pull Request — dev (#1344)
by
unknown
01:57
created

data.datasets.heat_demand_timeseries.HeatTimeSeries.__init__()   A

Complexity

Conditions 1

Size

Total Lines 16
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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