Passed
Pull Request — dev (#934)
by
unknown
01:28
created

data.datasets.sanity_checks   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 1144
Duplicated Lines 5.59 %

Importance

Changes 0
Metric Value
wmc 45
eloc 629
dl 64
loc 1144
rs 8.771
c 0
b 0
f 0

4 Functions

Rating   Name   Duplication   Size   Complexity  
B sanitycheck_pv_rooftop_buildings() 0 67 3
F sanitycheck_emobility_mit() 0 554 24
F sanitycheck_eGon2035_electricity() 64 231 16
B sanitycheck_eGon2035_heat() 0 217 1

1 Method

Rating   Name   Duplication   Size   Complexity  
A SanityChecks.__init__() 0 10 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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