Passed
Push — dev ( 3c9491...dd0cc3 )
by
unknown
02:45 queued 01:08
created

etrago_eGon2035_electricity()   F

Complexity

Conditions 16

Size

Total Lines 243
Code Lines 97

Duplication

Lines 64
Ratio 26.34 %

Importance

Changes 0
Metric Value
eloc 97
dl 64
loc 243
rs 1.7781
c 0
b 0
f 0
cc 16
nop 0

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.sanity_checks.etrago_eGon2035_electricity() 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
"""
2
This module does sanity checks for both the eGon2035 and the eGon100RE scenario
3
separately where a percentage error is given to showcase difference in output
4
and input values. Please note that there are missing input technologies in the
5
supply tables.
6
Authors: @ALonso, @dana
7
"""
8
9
from sqlalchemy import Numeric
10
from sqlalchemy.sql import and_, cast, func, or_
11
import numpy as np
12
import pandas as pd
13
14
from egon.data import config, db
15
from egon.data.datasets import Dataset
16
from egon.data.datasets.emobility.motorized_individual_travel.db_classes import (
17
    EgonEvCountMunicipality,
18
    EgonEvCountMvGridDistrict,
19
    EgonEvCountRegistrationDistrict,
20
    EgonEvMvGridDistrict,
21
    EgonEvPool,
22
    EgonEvTrip,
23
)
24
from egon.data.datasets.emobility.motorized_individual_travel.helpers import (
25
    DATASET_CFG,
26
    read_simbev_metadata_file,
27
)
28
from egon.data.datasets.etrago_setup import (
29
    EgonPfHvLink,
30
    EgonPfHvLinkTimeseries,
31
    EgonPfHvLoad,
32
    EgonPfHvLoadTimeseries,
33
    EgonPfHvStore,
34
    EgonPfHvStoreTimeseries,
35
)
36
from egon.data.datasets.scenario_parameters import get_sector_parameters
37
38
TESTMODE_OFF = (
39
    config.settings()["egon-data"]["--dataset-boundary"] == "Everything"
40
)
41
42
43
class SanityChecks(Dataset):
44
    def __init__(self, dependencies):
45
        super().__init__(
46
            name="SanityChecks",
47
            version="0.0.4",
48
            dependencies=dependencies,
49
            tasks={
50
                etrago_eGon2035_electricity,
51
                etrago_eGon2035_heat,
52
                residential_electricity_annual_sum,
53
                residential_electricity_hh_refinement,
54
                sanitycheck_emobility_mit,
55
            },
56
        )
57
58
59
def etrago_eGon2035_electricity():
60
    """Execute basic sanity checks.
61
62
    Returns print statements as sanity checks for the electricity sector in
63
    the eGon2035 scenario.
64
65
    Parameters
66
    ----------
67
    None
68
69
    Returns
70
    -------
71
    None
72
    """
73
74
    scn = "eGon2035"
75
76
    # Section to check generator capacities
77
    print(f"Sanity checks for scenario {scn}")
78
    print(
79
        "For German electricity generators the following deviations between "
80
        "the inputs and outputs can be observed:"
81
    )
82
83
    carriers_electricity = [
84
        "other_non_renewable",
85
        "other_renewable",
86
        "reservoir",
87
        "run_of_river",
88
        "oil",
89
        "wind_onshore",
90
        "wind_offshore",
91
        "solar",
92
        "solar_rooftop",
93
        "biomass",
94
    ]
95
96
    for carrier in carriers_electricity:
97
98
        if carrier == "biomass":
99
            sum_output = db.select_dataframe(
100
                """SELECT scn_name, SUM(p_nom::numeric) as output_capacity_mw
101
                    FROM grid.egon_etrago_generator
102
                    WHERE bus IN (
103
                        SELECT bus_id FROM grid.egon_etrago_bus
104
                        WHERE scn_name = 'eGon2035'
105
                        AND country = 'DE')
106
                    AND carrier IN ('biomass', 'industrial_biomass_CHP',
107
                    'central_biomass_CHP')
108
                    GROUP BY (scn_name);
109
                """,
110
                warning=False,
111
            )
112
113
        else:
114
            sum_output = db.select_dataframe(
115
                f"""SELECT scn_name,
116
                 SUM(p_nom::numeric) as output_capacity_mw
117
                         FROM grid.egon_etrago_generator
118
                         WHERE scn_name = '{scn}'
119
                         AND carrier IN ('{carrier}')
120
                         AND bus IN
121
                             (SELECT bus_id
122
                               FROM grid.egon_etrago_bus
123
                               WHERE scn_name = 'eGon2035'
124
                               AND country = 'DE')
125
                         GROUP BY (scn_name);
126
                    """,
127
                warning=False,
128
            )
129
130
        sum_input = db.select_dataframe(
131
            f"""SELECT carrier, SUM(capacity::numeric) as input_capacity_mw
132
                     FROM supply.egon_scenario_capacities
133
                     WHERE carrier= '{carrier}'
134
                     AND scenario_name ='{scn}'
135
                     GROUP BY (carrier);
136
                """,
137
            warning=False,
138
        )
139
140 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
141
            sum_output.output_capacity_mw.sum() == 0
142
            and sum_input.input_capacity_mw.sum() == 0
143
        ):
144
            print(
145
                f"No capacity for carrier '{carrier}' needed to be"
146
                f" distributed. Everything is fine"
147
            )
148
149
        elif (
150
            sum_input.input_capacity_mw.sum() > 0
151
            and sum_output.output_capacity_mw.sum() == 0
152
        ):
153
            print(
154
                f"Error: Capacity for carrier '{carrier}' was not distributed "
155
                f"at all!"
156
            )
157
158
        elif (
159
            sum_output.output_capacity_mw.sum() > 0
160
            and sum_input.input_capacity_mw.sum() == 0
161
        ):
162
            print(
163
                f"Error: Eventhough no input capacity was provided for carrier"
164
                f"'{carrier}' a capacity got distributed!"
165
            )
166
167
        else:
168
            sum_input["error"] = (
169
                (sum_output.output_capacity_mw - sum_input.input_capacity_mw)
170
                / sum_input.input_capacity_mw
171
            ) * 100
172
            g = sum_input["error"].values[0]
173
174
            print(f"{carrier}: " + str(round(g, 2)) + " %")
175
176
    # Section to check storage units
177
178
    print(f"Sanity checks for scenario {scn}")
179
    print(
180
        "For German electrical storage units the following deviations between"
181
        "the inputs and outputs can be observed:"
182
    )
183
184
    carriers_electricity = ["pumped_hydro"]
185
186
    for carrier in carriers_electricity:
187
188
        sum_output = db.select_dataframe(
189
            f"""SELECT scn_name, SUM(p_nom::numeric) as output_capacity_mw
190
                         FROM grid.egon_etrago_storage
191
                         WHERE scn_name = '{scn}'
192
                         AND carrier IN ('{carrier}')
193
                         AND bus IN
194
                             (SELECT bus_id
195
                               FROM grid.egon_etrago_bus
196
                               WHERE scn_name = 'eGon2035'
197
                               AND country = 'DE')
198
                         GROUP BY (scn_name);
199
                    """,
200
            warning=False,
201
        )
202
203
        sum_input = db.select_dataframe(
204
            f"""SELECT carrier, SUM(capacity::numeric) as input_capacity_mw
205
                     FROM supply.egon_scenario_capacities
206
                     WHERE carrier= '{carrier}'
207
                     AND scenario_name ='{scn}'
208
                     GROUP BY (carrier);
209
                """,
210
            warning=False,
211
        )
212
213 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
214
            sum_output.output_capacity_mw.sum() == 0
215
            and sum_input.input_capacity_mw.sum() == 0
216
        ):
217
            print(
218
                f"No capacity for carrier '{carrier}' needed to be "
219
                f"distributed. Everything is fine"
220
            )
221
222
        elif (
223
            sum_input.input_capacity_mw.sum() > 0
224
            and sum_output.output_capacity_mw.sum() == 0
225
        ):
226
            print(
227
                f"Error: Capacity for carrier '{carrier}' was not distributed"
228
                f" at all!"
229
            )
230
231
        elif (
232
            sum_output.output_capacity_mw.sum() > 0
233
            and sum_input.input_capacity_mw.sum() == 0
234
        ):
235
            print(
236
                f"Error: Eventhough no input capacity was provided for carrier"
237
                f" '{carrier}' a capacity got distributed!"
238
            )
239
240
        else:
241
            sum_input["error"] = (
242
                (sum_output.output_capacity_mw - sum_input.input_capacity_mw)
243
                / sum_input.input_capacity_mw
244
            ) * 100
245
            g = sum_input["error"].values[0]
246
247
            print(f"{carrier}: " + str(round(g, 2)) + " %")
248
249
    # Section to check loads
250
251
    print(
252
        "For German electricity loads the following deviations between the"
253
        " input and output can be observed:"
254
    )
255
256
    output_demand = db.select_dataframe(
257
        """SELECT a.scn_name, a.carrier,  SUM((SELECT SUM(p)
258
        FROM UNNEST(b.p_set) p))/1000000::numeric as load_twh
259
            FROM grid.egon_etrago_load a
260
            JOIN grid.egon_etrago_load_timeseries b
261
            ON (a.load_id = b.load_id)
262
            JOIN grid.egon_etrago_bus c
263
            ON (a.bus=c.bus_id)
264
            AND b.scn_name = 'eGon2035'
265
            AND a.scn_name = 'eGon2035'
266
            AND a.carrier = 'AC'
267
            AND c.scn_name= 'eGon2035'
268
            AND c.country='DE'
269
            GROUP BY (a.scn_name, a.carrier);
270
271
    """,
272
        warning=False,
273
    )["load_twh"].values[0]
274
275
    input_cts_ind = db.select_dataframe(
276
        """SELECT scenario,
277
         SUM(demand::numeric/1000000) as demand_mw_regio_cts_ind
278
            FROM demand.egon_demandregio_cts_ind
279
            WHERE scenario= 'eGon2035'
280
            AND year IN ('2035')
281
            GROUP BY (scenario);
282
283
        """,
284
        warning=False,
285
    )["demand_mw_regio_cts_ind"].values[0]
286
287
    input_hh = db.select_dataframe(
288
        """SELECT scenario, SUM(demand::numeric/1000000) as demand_mw_regio_hh
289
            FROM demand.egon_demandregio_hh
290
            WHERE scenario= 'eGon2035'
291
            AND year IN ('2035')
292
            GROUP BY (scenario);
293
        """,
294
        warning=False,
295
    )["demand_mw_regio_hh"].values[0]
296
297
    input_demand = input_hh + input_cts_ind
298
299
    e = round((output_demand - input_demand) / input_demand, 2) * 100
300
301
    print(f"electricity demand: {e} %")
302
303
304
def etrago_eGon2035_heat():
305
    """Execute basic sanity checks.
306
307
    Returns print statements as sanity checks for the heat sector in
308
    the eGon2035 scenario.
309
310
    Parameters
311
    ----------
312
    None
313
314
    Returns
315
    -------
316
    None
317
    """
318
319
    # Check input and output values for the carriers "other_non_renewable",
320
    # "other_renewable", "reservoir", "run_of_river" and "oil"
321
322
    scn = "eGon2035"
323
324
    # Section to check generator capacities
325
    print(f"Sanity checks for scenario {scn}")
326
    print(
327
        "For German heat demands the following deviations between the inputs"
328
        " and outputs can be observed:"
329
    )
330
331
    # Sanity checks for heat demand
332
333
    output_heat_demand = db.select_dataframe(
334
        """SELECT a.scn_name,
335
          (SUM(
336
          (SELECT SUM(p) FROM UNNEST(b.p_set) p))/1000000)::numeric as load_twh
337
            FROM grid.egon_etrago_load a
338
            JOIN grid.egon_etrago_load_timeseries b
339
            ON (a.load_id = b.load_id)
340
            JOIN grid.egon_etrago_bus c
341
            ON (a.bus=c.bus_id)
342
            AND b.scn_name = 'eGon2035'
343
            AND a.scn_name = 'eGon2035'
344
            AND c.scn_name= 'eGon2035'
345
            AND c.country='DE'
346
            AND a.carrier IN ('rural_heat', 'central_heat')
347
            GROUP BY (a.scn_name);
348
        """,
349
        warning=False,
350
    )["load_twh"].values[0]
351
352
    input_heat_demand = db.select_dataframe(
353
        """SELECT scenario, SUM(demand::numeric/1000000) as demand_mw_peta_heat
354
            FROM demand.egon_peta_heat
355
            WHERE scenario= 'eGon2035'
356
            GROUP BY (scenario);
357
        """,
358
        warning=False,
359
    )["demand_mw_peta_heat"].values[0]
360
361
    e_demand = (
362
        round((output_heat_demand - input_heat_demand) / input_heat_demand, 2)
363
        * 100
364
    )
365
366
    print(f"heat demand: {e_demand} %")
367
368
    # Sanity checks for heat supply
369
370
    print(
371
        "For German heat supplies the following deviations between the inputs "
372
        "and outputs can be observed:"
373
    )
374
375
    # Comparison for central heat pumps
376
    heat_pump_input = db.select_dataframe(
377
        """SELECT carrier, SUM(capacity::numeric) as Urban_central_heat_pump_mw
378
            FROM supply.egon_scenario_capacities
379
            WHERE carrier= 'urban_central_heat_pump'
380
            AND scenario_name IN ('eGon2035')
381
            GROUP BY (carrier);
382
        """,
383
        warning=False,
384
    )["urban_central_heat_pump_mw"].values[0]
385
386
    heat_pump_output = db.select_dataframe(
387
        """SELECT carrier, SUM(p_nom::numeric) as Central_heat_pump_mw
388
            FROM grid.egon_etrago_link
389
            WHERE carrier= 'central_heat_pump'
390
            AND scn_name IN ('eGon2035')
391
            GROUP BY (carrier);
392
    """,
393
        warning=False,
394
    )["central_heat_pump_mw"].values[0]
395
396
    e_heat_pump = (
397
        round((heat_pump_output - heat_pump_input) / heat_pump_output, 2) * 100
398
    )
399
400
    print(f"'central_heat_pump': {e_heat_pump} % ")
401
402
    # Comparison for residential heat pumps
403
404
    input_residential_heat_pump = db.select_dataframe(
405
        """SELECT carrier, SUM(capacity::numeric) as residential_heat_pump_mw
406
            FROM supply.egon_scenario_capacities
407
            WHERE carrier= 'residential_rural_heat_pump'
408
            AND scenario_name IN ('eGon2035')
409
            GROUP BY (carrier);
410
        """,
411
        warning=False,
412
    )["residential_heat_pump_mw"].values[0]
413
414
    output_residential_heat_pump = db.select_dataframe(
415
        """SELECT carrier, SUM(p_nom::numeric) as rural_heat_pump_mw
416
            FROM grid.egon_etrago_link
417
            WHERE carrier= 'rural_heat_pump'
418
            AND scn_name IN ('eGon2035')
419
            GROUP BY (carrier);
420
    """,
421
        warning=False,
422
    )["rural_heat_pump_mw"].values[0]
423
424
    e_residential_heat_pump = (
425
        round(
426
            (output_residential_heat_pump - input_residential_heat_pump)
427
            / input_residential_heat_pump,
428
            2,
429
        )
430
        * 100
431
    )
432
    print(f"'residential heat pumps': {e_residential_heat_pump} %")
433
434
    # Comparison for resistive heater
435
    resistive_heater_input = db.select_dataframe(
436
        """SELECT carrier,
437
         SUM(capacity::numeric) as Urban_central_resistive_heater_MW
438
            FROM supply.egon_scenario_capacities
439
            WHERE carrier= 'urban_central_resistive_heater'
440
            AND scenario_name IN ('eGon2035')
441
            GROUP BY (carrier);
442
        """,
443
        warning=False,
444
    )["urban_central_resistive_heater_mw"].values[0]
445
446
    resistive_heater_output = db.select_dataframe(
447
        """SELECT carrier, SUM(p_nom::numeric) as central_resistive_heater_MW
448
            FROM grid.egon_etrago_link
449
            WHERE carrier= 'central_resistive_heater'
450
            AND scn_name IN ('eGon2035')
451
            GROUP BY (carrier);
452
        """,
453
        warning=False,
454
    )["central_resistive_heater_mw"].values[0]
455
456
    e_resistive_heater = (
457
        round(
458
            (resistive_heater_output - resistive_heater_input)
459
            / resistive_heater_input,
460
            2,
461
        )
462
        * 100
463
    )
464
465
    print(f"'resistive heater': {e_resistive_heater} %")
466
467
    # Comparison for solar thermal collectors
468
469
    input_solar_thermal = db.select_dataframe(
470
        """SELECT carrier, SUM(capacity::numeric) as solar_thermal_collector_mw
471
            FROM supply.egon_scenario_capacities
472
            WHERE carrier= 'urban_central_solar_thermal_collector'
473
            AND scenario_name IN ('eGon2035')
474
            GROUP BY (carrier);
475
        """,
476
        warning=False,
477
    )["solar_thermal_collector_mw"].values[0]
478
479
    output_solar_thermal = db.select_dataframe(
480
        """SELECT carrier, SUM(p_nom::numeric) as solar_thermal_collector_mw
481
            FROM grid.egon_etrago_generator
482
            WHERE carrier= 'solar_thermal_collector'
483
            AND scn_name IN ('eGon2035')
484
            GROUP BY (carrier);
485
        """,
486
        warning=False,
487
    )["solar_thermal_collector_mw"].values[0]
488
489
    e_solar_thermal = (
490
        round(
491
            (output_solar_thermal - input_solar_thermal) / input_solar_thermal,
492
            2,
493
        )
494
        * 100
495
    )
496
    print(f"'solar thermal collector': {e_solar_thermal} %")
497
498
    # Comparison for geothermal
499
500
    input_geo_thermal = db.select_dataframe(
501
        """SELECT carrier,
502
         SUM(capacity::numeric) as Urban_central_geo_thermal_MW
503
            FROM supply.egon_scenario_capacities
504
            WHERE carrier= 'urban_central_geo_thermal'
505
            AND scenario_name IN ('eGon2035')
506
            GROUP BY (carrier);
507
        """,
508
        warning=False,
509
    )["urban_central_geo_thermal_mw"].values[0]
510
511
    output_geo_thermal = db.select_dataframe(
512
        """SELECT carrier, SUM(p_nom::numeric) as geo_thermal_MW
513
            FROM grid.egon_etrago_generator
514
            WHERE carrier= 'geo_thermal'
515
            AND scn_name IN ('eGon2035')
516
            GROUP BY (carrier);
517
    """,
518
        warning=False,
519
    )["geo_thermal_mw"].values[0]
520
521
    e_geo_thermal = (
522
        round((output_geo_thermal - input_geo_thermal) / input_geo_thermal, 2)
523
        * 100
524
    )
525
    print(f"'geothermal': {e_geo_thermal} %")
526
527
528
def sanitycheck_emobility_mit():
529
    """Execute sanity checks for eMobility: motorized individual travel
530
531
    Checks data integrity for eGon2035, eGon2035_lowflex and eGon100RE scenario
532
    using assertions:
533
      1. Allocated EV numbers and EVs allocated to grid districts
534
      2. Trip data (original inout data from simBEV)
535
      3. Model data in eTraGo PF tables (grid.egon_etrago_*)
536
537
    Parameters
538
    ----------
539
    None
540
541
    Returns
542
    -------
543
    None
544
    """
545
546
    def check_ev_allocation():
547
        # Get target number for scenario
548
        ev_count_target = scenario_variation_parameters["ev_count"]
549
        print(f"  Target count: {str(ev_count_target)}")
550
551
        # Get allocated numbers
552
        ev_counts_dict = {}
553
        with db.session_scope() as session:
554
            for table, level in zip(
555
                [
556
                    EgonEvCountMvGridDistrict,
557
                    EgonEvCountMunicipality,
558
                    EgonEvCountRegistrationDistrict,
559
                ],
560
                ["Grid District", "Municipality", "Registration District"],
561
            ):
562
                query = session.query(
563
                    func.sum(
564
                        table.bev_mini
565
                        + table.bev_medium
566
                        + table.bev_luxury
567
                        + table.phev_mini
568
                        + table.phev_medium
569
                        + table.phev_luxury
570
                    ).label("ev_count")
571
                ).filter(
572
                    table.scenario == scenario_name,
573
                    table.scenario_variation == scenario_var_name,
574
                )
575
576
                ev_counts = pd.read_sql(
577
                    query.statement, query.session.bind, index_col=None
578
                )
579
                ev_counts_dict[level] = ev_counts.iloc[0].ev_count
580
                print(
581
                    f"    Count table: Total count for level {level} "
582
                    f"(table: {table.__table__}): "
583
                    f"{str(ev_counts_dict[level])}"
584
                )
585
586
        # Compare with scenario target (only if not in testmode)
587
        if TESTMODE_OFF:
588
            for level, count in ev_counts_dict.items():
589
                np.testing.assert_allclose(
590
                    count,
591
                    ev_count_target,
592
                    rtol=0.0001,
593
                    err_msg=f"EV numbers in {level} seems to be flawed.",
594
                )
595
        else:
596
            print("    Testmode is on, skipping sanity check...")
597
598
        # Get allocated EVs in grid districts
599
        with db.session_scope() as session:
600
            query = session.query(
601
                func.count(EgonEvMvGridDistrict.egon_ev_pool_ev_id).label(
602
                    "ev_count"
603
                ),
604
            ).filter(
605
                EgonEvMvGridDistrict.scenario == scenario_name,
606
                EgonEvMvGridDistrict.scenario_variation == scenario_var_name,
607
            )
608
        ev_count_alloc = (
609
            pd.read_sql(query.statement, query.session.bind, index_col=None)
610
            .iloc[0]
611
            .ev_count
612
        )
613
        print(
614
            f"    EVs allocated to Grid Districts "
615
            f"(table: {EgonEvMvGridDistrict.__table__}) total count: "
616
            f"{str(ev_count_alloc)}"
617
        )
618
619
        # Compare with scenario target (only if not in testmode)
620
        if TESTMODE_OFF:
621
            np.testing.assert_allclose(
622
                ev_count_alloc,
623
                ev_count_target,
624
                rtol=0.0001,
625
                err_msg=(
626
                    "EV numbers allocated to Grid Districts seems to be "
627
                    "flawed."
628
                ),
629
            )
630
        else:
631
            print("    Testmode is on, skipping sanity check...")
632
633
        return ev_count_alloc
634
635
    def check_trip_data():
636
        # Check if trips start at timestep 0 and have a max. of 35040 steps
637
        # (8760h in 15min steps)
638
        print("  Checking timeranges...")
639
        with db.session_scope() as session:
640
            query = session.query(
641
                func.count(EgonEvTrip.event_id).label("cnt")
642
            ).filter(
643
                or_(
644
                    and_(
645
                        EgonEvTrip.park_start > 0,
646
                        EgonEvTrip.simbev_event_id == 0,
647
                    ),
648
                    EgonEvTrip.park_end
649
                    > (60 / int(meta_run_config.stepsize)) * 8760,
650
                ),
651
                EgonEvTrip.scenario == scenario_name,
652
            )
653
        invalid_trips = pd.read_sql(
654
            query.statement, query.session.bind, index_col=None
655
        )
656
        np.testing.assert_equal(
657
            invalid_trips.iloc[0].cnt,
658
            0,
659
            err_msg=(
660
                f"{str(invalid_trips.iloc[0].cnt)} trips in table "
661
                f"{EgonEvTrip.__table__} have invalid timesteps."
662
            ),
663
        )
664
665
        # Check if charging demand can be covered by available charging energy
666
        # while parking
667
        print("  Compare charging demand with available power...")
668
        with db.session_scope() as session:
669
            query = session.query(
670
                func.count(EgonEvTrip.event_id).label("cnt")
671
            ).filter(
672
                func.round(
673
                    cast(
674
                        (EgonEvTrip.park_end - EgonEvTrip.park_start + 1)
675
                        * EgonEvTrip.charging_capacity_nominal
676
                        * (int(meta_run_config.stepsize) / 60),
677
                        Numeric,
678
                    ),
679
                    3,
680
                )
681
                < cast(EgonEvTrip.charging_demand, Numeric),
682
                EgonEvTrip.scenario == scenario_name,
683
            )
684
        invalid_trips = pd.read_sql(
685
            query.statement, query.session.bind, index_col=None
686
        )
687
        np.testing.assert_equal(
688
            invalid_trips.iloc[0].cnt,
689
            0,
690
            err_msg=(
691
                f"In {str(invalid_trips.iloc[0].cnt)} trips (table: "
692
                f"{EgonEvTrip.__table__}) the charging demand cannot be "
693
                f"covered by available charging power."
694
            ),
695
        )
696
697
    def check_model_data():
698
        # Check if model components were fully created
699
        print("  Check if all model components were created...")
700
        # Get MVGDs which got EV allocated
701
        with db.session_scope() as session:
702
            query = (
703
                session.query(
704
                    EgonEvMvGridDistrict.bus_id,
705
                )
706
                .filter(
707
                    EgonEvMvGridDistrict.scenario == scenario_name,
708
                    EgonEvMvGridDistrict.scenario_variation
709
                    == scenario_var_name,
710
                )
711
                .group_by(EgonEvMvGridDistrict.bus_id)
712
            )
713
        mvgds_with_ev = (
714
            pd.read_sql(query.statement, query.session.bind, index_col=None)
715
            .bus_id.sort_values()
716
            .to_list()
717
        )
718
719
        # Load model components
720
        with db.session_scope() as session:
721
            query = (
722
                session.query(
723
                    EgonPfHvLink.bus0.label("mvgd_bus_id"),
724
                    EgonPfHvLoad.bus.label("emob_bus_id"),
725
                    EgonPfHvLoad.load_id.label("load_id"),
726
                    EgonPfHvStore.store_id.label("store_id"),
727
                )
728
                .select_from(EgonPfHvLoad, EgonPfHvStore)
729
                .join(
730
                    EgonPfHvLoadTimeseries,
731
                    EgonPfHvLoadTimeseries.load_id == EgonPfHvLoad.load_id,
732
                )
733
                .join(
734
                    EgonPfHvStoreTimeseries,
735
                    EgonPfHvStoreTimeseries.store_id == EgonPfHvStore.store_id,
736
                )
737
                .filter(
738
                    EgonPfHvLoad.carrier == "land transport EV",
739
                    EgonPfHvLoad.scn_name == scenario_name,
740
                    EgonPfHvLoadTimeseries.scn_name == scenario_name,
741
                    EgonPfHvStore.carrier == "battery storage",
742
                    EgonPfHvStore.scn_name == scenario_name,
743
                    EgonPfHvStoreTimeseries.scn_name == scenario_name,
744
                    EgonPfHvLink.scn_name == scenario_name,
745
                    EgonPfHvLink.bus1 == EgonPfHvLoad.bus,
746
                    EgonPfHvLink.bus1 == EgonPfHvStore.bus,
747
                )
748
            )
749
        model_components = pd.read_sql(
750
            query.statement, query.session.bind, index_col=None
751
        )
752
753
        # Check number of buses with model components connected
754
        mvgd_buses_with_ev = model_components.loc[
755
            model_components.mvgd_bus_id.isin(mvgds_with_ev)
756
        ]
757
        np.testing.assert_equal(
758
            len(mvgds_with_ev),
759
            len(mvgd_buses_with_ev),
760
            err_msg=(
761
                f"Number of Grid Districts with connected model components "
762
                f"({str(len(mvgd_buses_with_ev))} in tables egon_etrago_*) "
763
                f"differ from number of Grid Districts that got EVs "
764
                f"allocated ({len(mvgds_with_ev)} in table "
765
                f"{EgonEvMvGridDistrict.__table__})."
766
            ),
767
        )
768
769
        # Check if all required components exist (if no id is NaN)
770
        np.testing.assert_equal(
771
            model_components.drop_duplicates().isna().any().any(),
772
            False,
773
            err_msg=(
774
                f"Some components are missing (see True values): "
775
                f"{model_components.drop_duplicates().isna().any()}"
776
            ),
777
        )
778
779
        # Get all model timeseries
780
        print("  Loading model timeseries...")
781
        # Get all model timeseries
782
        model_ts_dict = {
783
            "Load": {
784
                "carrier": "land transport EV",
785
                "table": EgonPfHvLoad,
786
                "table_ts": EgonPfHvLoadTimeseries,
787
                "column_id": "load_id",
788
                "columns_ts": ["p_set"],
789
                "ts": None,
790
            },
791
            "Link": {
792
                "carrier": "BEV charger",
793
                "table": EgonPfHvLink,
794
                "table_ts": EgonPfHvLinkTimeseries,
795
                "column_id": "link_id",
796
                "columns_ts": ["p_max_pu"],
797
                "ts": None,
798
            },
799
            "Store": {
800
                "carrier": "battery storage",
801
                "table": EgonPfHvStore,
802
                "table_ts": EgonPfHvStoreTimeseries,
803
                "column_id": "store_id",
804
                "columns_ts": ["e_min_pu", "e_max_pu"],
805
                "ts": None,
806
            },
807
        }
808
809
        with db.session_scope() as session:
810
            for node, attrs in model_ts_dict.items():
811
                print(f"    Loading {node} timeseries...")
812
                subquery = (
813
                    session.query(getattr(attrs["table"], attrs["column_id"]))
814
                    .filter(attrs["table"].carrier == attrs["carrier"])
815
                    .filter(attrs["table"].scn_name == scenario_name)
816
                    .subquery()
817
                )
818
819
                cols = [
820
                    getattr(attrs["table_ts"], c) for c in attrs["columns_ts"]
821
                ]
822
                query = session.query(
823
                    getattr(attrs["table_ts"], attrs["column_id"]), *cols
824
                ).filter(
825
                    getattr(attrs["table_ts"], attrs["column_id"]).in_(
826
                        subquery
827
                    ),
828
                    attrs["table_ts"].scn_name == scenario_name,
829
                )
830
                attrs["ts"] = pd.read_sql(
831
                    query.statement,
832
                    query.session.bind,
833
                    index_col=attrs["column_id"],
834
                )
835
836
        # Check if all timeseries have 8760 steps
837
        print("    Checking timeranges...")
838
        for node, attrs in model_ts_dict.items():
839
            for col in attrs["columns_ts"]:
840
                ts = attrs["ts"]
841
                invalid_ts = ts.loc[ts[col].apply(lambda _: len(_)) != 8760][
842
                    col
843
                ].apply(len)
844
                np.testing.assert_equal(
845
                    len(invalid_ts),
846
                    0,
847
                    err_msg=(
848
                        f"{str(len(invalid_ts))} rows in timeseries do not "
849
                        f"have 8760 timesteps. Table: "
850
                        f"{attrs['table_ts'].__table__}, Column: {col}, IDs: "
851
                        f"{str(list(invalid_ts.index))}"
852
                    ),
853
                )
854
855
        # Compare total energy demand in model with some approximate values
856
        # (per EV: 14,000 km/a, 0.17 kWh/km)
857
        print("  Checking energy demand in model...")
858
        total_energy_model = (
859
            model_ts_dict["Load"]["ts"].p_set.apply(lambda _: sum(_)).sum()
860
            / 1e6
861
        )
862
        print(f"    Total energy amount in model: {total_energy_model} TWh")
863
        total_energy_scenario_approx = ev_count_alloc * 14000 * 0.17 / 1e9
864
        print(
865
            f"    Total approximated energy amount in scenario: "
866
            f"{total_energy_scenario_approx} TWh"
867
        )
868
        np.testing.assert_allclose(
869
            total_energy_model,
870
            total_energy_scenario_approx,
871
            rtol=0.1,
872
            err_msg=(
873
                "The total energy amount in the model deviates heavily "
874
                "from the approximated value for current scenario."
875
            ),
876
        )
877
878
        # Compare total storage capacity
879
        print("  Checking storage capacity...")
880
        # Load storage capacities from model
881
        with db.session_scope() as session:
882
            query = session.query(
883
                func.sum(EgonPfHvStore.e_nom).label("e_nom")
884
            ).filter(
885
                EgonPfHvStore.scn_name == scenario_name,
886
                EgonPfHvStore.carrier == "battery storage",
887
            )
888
        storage_capacity_model = (
889
            pd.read_sql(
890
                query.statement, query.session.bind, index_col=None
891
            ).e_nom.sum()
892
            / 1e3
893
        )
894
        print(
895
            f"    Total storage capacity ({EgonPfHvStore.__table__}): "
896
            f"{round(storage_capacity_model, 1)} GWh"
897
        )
898
899
        # Load occurences of each EV
900
        with db.session_scope() as session:
901
            query = (
902
                session.query(
903
                    EgonEvMvGridDistrict.bus_id,
904
                    EgonEvPool.type,
905
                    func.count(EgonEvMvGridDistrict.egon_ev_pool_ev_id).label(
906
                        "count"
907
                    ),
908
                )
909
                .join(
910
                    EgonEvPool,
911
                    EgonEvPool.ev_id
912
                    == EgonEvMvGridDistrict.egon_ev_pool_ev_id,
913
                )
914
                .filter(
915
                    EgonEvMvGridDistrict.scenario == scenario_name,
916
                    EgonEvMvGridDistrict.scenario_variation
917
                    == scenario_var_name,
918
                    EgonEvPool.scenario == scenario_name,
919
                )
920
                .group_by(EgonEvMvGridDistrict.bus_id, EgonEvPool.type)
921
            )
922
        count_per_ev_all = pd.read_sql(
923
            query.statement, query.session.bind, index_col="bus_id"
924
        )
925
        count_per_ev_all["bat_cap"] = count_per_ev_all.type.map(
926
            meta_tech_data.battery_capacity
927
        )
928
        count_per_ev_all["bat_cap_total_MWh"] = (
929
            count_per_ev_all["count"] * count_per_ev_all.bat_cap / 1e3
930
        )
931
        storage_capacity_simbev = count_per_ev_all.bat_cap_total_MWh.div(
932
            1e3
933
        ).sum()
934
        print(
935
            f"    Total storage capacity (simBEV): "
936
            f"{round(storage_capacity_simbev, 1)} GWh"
937
        )
938
939
        np.testing.assert_allclose(
940
            storage_capacity_model,
941
            storage_capacity_simbev,
942
            rtol=0.01,
943
            err_msg=(
944
                "The total storage capacity in the model deviates heavily "
945
                "from the input data provided by simBEV for current scenario."
946
            ),
947
        )
948
949
        # Check SoC storage constraint: e_min_pu < e_max_pu for all timesteps
950
        print("  Validating SoC constraints...")
951
        stores_with_invalid_soc = []
952
        for idx, row in model_ts_dict["Store"]["ts"].iterrows():
953
            ts = row[["e_min_pu", "e_max_pu"]]
954
            x = np.array(ts.e_min_pu) > np.array(ts.e_max_pu)
955
            if x.any():
956
                stores_with_invalid_soc.append(idx)
957
958
        np.testing.assert_equal(
959
            len(stores_with_invalid_soc),
960
            0,
961
            err_msg=(
962
                f"The store constraint e_min_pu < e_max_pu does not apply "
963
                f"for some storages in {EgonPfHvStoreTimeseries.__table__}. "
964
                f"Invalid store_ids: {stores_with_invalid_soc}"
965
            ),
966
        )
967
968
    def check_model_data_lowflex_eGon2035():
969
        # TODO: Add eGon100RE_lowflex
970
        print("")
971
        print("SCENARIO: eGon2035_lowflex")
972
973
        # Compare driving load and charging load
974
        print("  Loading eGon2035 model timeseries: driving load...")
975
        with db.session_scope() as session:
976
            query = (
977
                session.query(
978
                    EgonPfHvLoad.load_id,
979
                    EgonPfHvLoadTimeseries.p_set,
980
                )
981
                .join(
982
                    EgonPfHvLoadTimeseries,
983
                    EgonPfHvLoadTimeseries.load_id == EgonPfHvLoad.load_id,
984
                )
985
                .filter(
986
                    EgonPfHvLoad.carrier == "land transport EV",
987
                    EgonPfHvLoad.scn_name == "eGon2035",
988
                    EgonPfHvLoadTimeseries.scn_name == "eGon2035",
989
                )
990
            )
991
        model_driving_load = pd.read_sql(
992
            query.statement, query.session.bind, index_col=None
993
        )
994
        driving_load = np.array(model_driving_load.p_set.to_list()).sum(axis=0)
995
996
        print(
997
            "  Loading eGon2035_lowflex model timeseries: dumb charging "
998
            "load..."
999
        )
1000
        with db.session_scope() as session:
1001
            query = (
1002
                session.query(
1003
                    EgonPfHvLoad.load_id,
1004
                    EgonPfHvLoadTimeseries.p_set,
1005
                )
1006
                .join(
1007
                    EgonPfHvLoadTimeseries,
1008
                    EgonPfHvLoadTimeseries.load_id == EgonPfHvLoad.load_id,
1009
                )
1010
                .filter(
1011
                    EgonPfHvLoad.carrier == "land transport EV",
1012
                    EgonPfHvLoad.scn_name == "eGon2035_lowflex",
1013
                    EgonPfHvLoadTimeseries.scn_name == "eGon2035_lowflex",
1014
                )
1015
            )
1016
        model_charging_load_lowflex = pd.read_sql(
1017
            query.statement, query.session.bind, index_col=None
1018
        )
1019
        charging_load = np.array(
1020
            model_charging_load_lowflex.p_set.to_list()
1021
        ).sum(axis=0)
1022
1023
        # Ratio of driving and charging load should be 0.9 due to charging
1024
        # efficiency
1025
        print("  Compare cumulative loads...")
1026
        print(f"    Driving load (eGon2035): {driving_load.sum() / 1e6} TWh")
1027
        print(
1028
            f"    Dumb charging load (eGon2035_lowflex): "
1029
            f"{charging_load.sum() / 1e6} TWh"
1030
        )
1031
        driving_load_theoretical = (
1032
            float(meta_run_config.eta_cp) * charging_load.sum()
0 ignored issues
show
introduced by
The variable meta_run_config does not seem to be defined in case the for loop on line 1050 is not entered. Are you sure this can never be the case?
Loading history...
1033
        )
1034
        np.testing.assert_allclose(
1035
            driving_load.sum(),
1036
            driving_load_theoretical,
1037
            rtol=0.01,
1038
            err_msg=(
1039
                f"The driving load (eGon2035) deviates by more than 1% "
1040
                f"from the theoretical driving load calculated from charging "
1041
                f"load (eGon2035_lowflex) with an efficiency of "
1042
                f"{float(meta_run_config.eta_cp)}."
1043
            ),
1044
        )
1045
1046
    print("=====================================================")
1047
    print("=== SANITY CHECKS FOR MOTORIZED INDIVIDUAL TRAVEL ===")
1048
    print("=====================================================")
1049
1050
    for scenario_name in ["eGon2035", "eGon100RE"]:
1051
        scenario_var_name = DATASET_CFG["scenario"]["variation"][scenario_name]
1052
1053
        print("")
1054
        print(f"SCENARIO: {scenario_name}, VARIATION: {scenario_var_name}")
1055
1056
        # Load scenario params for scenario and scenario variation
1057
        scenario_variation_parameters = get_sector_parameters(
1058
            "mobility", scenario=scenario_name
1059
        )["motorized_individual_travel"][scenario_var_name]
1060
1061
        # Load simBEV run config and tech data
1062
        meta_run_config = read_simbev_metadata_file(
1063
            scenario_name, "config"
1064
        ).loc["basic"]
1065
        meta_tech_data = read_simbev_metadata_file(scenario_name, "tech_data")
1066
1067
        print("")
1068
        print("Checking EV counts...")
1069
        ev_count_alloc = check_ev_allocation()
1070
1071
        print("")
1072
        print("Checking trip data...")
1073
        check_trip_data()
1074
1075
        print("")
1076
        print("Checking model data...")
1077
        check_model_data()
1078
1079
    print("")
1080
    check_model_data_lowflex_eGon2035()
1081
1082
    print("=====================================================")
1083
1084
1085
def residential_electricity_annual_sum(rtol=1e-5):
1086
    """Sanity check for dataset electricity_demand_timeseries
1087
1088
    Aggregate the annual demand of all census cells at NUTS3 to compare
1089
    with initial scaling parameters from DemandRegio.
1090
    """
1091
1092
    df_nuts3_annual_sum = db.select_dataframe(
1093
        sql="""
1094
        SELECT dr.nuts3, dr.scenario, dr.demand_regio_sum, profiles.profile_sum
1095
        FROM (
1096
            SELECT scenario, SUM(demand) AS profile_sum, vg250_nuts3
1097
            FROM demand.egon_demandregio_zensus_electricity AS egon,
1098
             boundaries.egon_map_zensus_vg250 AS boundaries
1099
            Where egon.zensus_population_id = boundaries.zensus_population_id
1100
            AND sector = 'residential'
1101
            GROUP BY vg250_nuts3, scenario
1102
            ) AS profiles
1103
        JOIN (
1104
            SELECT nuts3, scenario, sum(demand) AS demand_regio_sum
1105
            FROM demand.egon_demandregio_hh
1106
            GROUP BY year, scenario, nuts3
1107
              ) AS dr
1108
        ON profiles.vg250_nuts3 = dr.nuts3 and profiles.scenario  = dr.scenario
1109
        """
1110
    )
1111
1112
    np.testing.assert_allclose(
1113
        actual=df_nuts3_annual_sum["profile_sum"],
1114
        desired=df_nuts3_annual_sum["demand_regio_sum"],
1115
        rtol=rtol,
1116
        verbose=False,
1117
    )
1118
1119
    print(
1120
        "Aggregated annual residential electricity demand"
1121
        " matches with DemandRegio at NUTS-3."
1122
    )
1123
1124
1125
def residential_electricity_hh_refinement(rtol=1e-5):
1126
    """Sanity check for dataset electricity_demand_timeseries
1127
1128
    Check sum of aggregated household types after refinement method
1129
    was applied and compare it to the original census values."""
1130
1131
    df_refinement = db.select_dataframe(
1132
        sql="""
1133
        SELECT refined.nuts3, refined.characteristics_code,
1134
                refined.sum_refined::int, census.sum_census::int
1135
        FROM(
1136
            SELECT nuts3, characteristics_code, SUM(hh_10types) as sum_refined
1137
            FROM society.egon_destatis_zensus_household_per_ha_refined
1138
            GROUP BY nuts3, characteristics_code)
1139
            AS refined
1140
        JOIN(
1141
            SELECT t.nuts3, t.characteristics_code, sum(orig) as sum_census
1142
            FROM(
1143
                SELECT nuts3, cell_id, characteristics_code,
1144
                        sum(DISTINCT(hh_5types))as orig
1145
                FROM society.egon_destatis_zensus_household_per_ha_refined
1146
                GROUP BY cell_id, characteristics_code, nuts3) AS t
1147
            GROUP BY t.nuts3, t.characteristics_code    ) AS census
1148
        ON refined.nuts3 = census.nuts3
1149
        AND refined.characteristics_code = census.characteristics_code
1150
    """
1151
    )
1152
1153
    np.testing.assert_allclose(
1154
        actual=df_refinement["sum_refined"],
1155
        desired=df_refinement["sum_census"],
1156
        rtol=rtol,
1157
        verbose=False,
1158
    )
1159
1160
    print("All Aggregated household types match at NUTS-3.")
1161