Passed
Push — dev ( dd0cc3...4bb0ab )
by
unknown
02:31 queued 01:04
created

SanityChecks.__init__()   A

Complexity

Conditions 1

Size

Total Lines 13
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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