Passed
Pull Request — dev (#1006)
by
unknown
01:35
created

sanity_check_H2_saltcavern_stores()   A

Complexity

Conditions 1

Size

Total Lines 50
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 50
rs 9.65
c 0
b 0
f 0
cc 1
nop 1
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
from pathlib import Path
9
import ast
10
11
from sqlalchemy import Numeric
12
from sqlalchemy.sql import and_, cast, func, or_
13
import numpy as np
14
import pandas as pd
15
16
from egon.data import config, db, logger
17
from egon.data.datasets import Dataset
18
from egon.data.datasets.electricity_demand_timeseries.cts_buildings import (
19
    EgonCtsElectricityDemandBuildingShare,
20
    EgonCtsHeatDemandBuildingShare,
21
)
22
from egon.data.datasets.emobility.motorized_individual_travel.db_classes import (
23
    EgonEvCountMunicipality,
24
    EgonEvCountMvGridDistrict,
25
    EgonEvCountRegistrationDistrict,
26
    EgonEvMvGridDistrict,
27
    EgonEvPool,
28
    EgonEvTrip,
29
)
30
from egon.data.datasets.emobility.motorized_individual_travel.helpers import (
31
    DATASET_CFG,
32
    read_simbev_metadata_file,
33
)
34
from egon.data.datasets.etrago_setup import (
35
    EgonPfHvLink,
36
    EgonPfHvLinkTimeseries,
37
    EgonPfHvLoad,
38
    EgonPfHvLoadTimeseries,
39
    EgonPfHvStore,
40
    EgonPfHvStoreTimeseries,
41
)
42
from egon.data.datasets.gas_grid import (
43
    define_gas_nodes_list,
44
    define_gas_pipeline_list,
45
    insert_gas_buses_abroad,
46
)
47
from egon.data.datasets.hydrogen_etrago.storage import (
48
    calculate_and_map_saltcavern_storage_potential,
49
)
50
from egon.data.datasets.pypsaeursec import read_network
51
from egon.data.datasets.scenario_parameters import get_sector_parameters
52
53
TESTMODE_OFF = (
54
    config.settings()["egon-data"]["--dataset-boundary"] == "Everything"
55
)
56
57
58
class SanityChecks(Dataset):
59
    #:
60
    name: str = "SanityChecks"
61
    #:
62
    version: str = "0.0.6"
63
64
    def __init__(self, dependencies):
65
        super().__init__(
66
            name=self.name,
67
            version=self.version,
68
            dependencies=dependencies,
69
            tasks={
70
                etrago_eGon2035_electricity,
71
                etrago_eGon2035_heat,
72
                residential_electricity_annual_sum,
73
                residential_electricity_hh_refinement,
74
                cts_electricity_demand_share,
75
                cts_heat_demand_share,
76
                sanitycheck_emobility_mit,
77
                etrago_eGon100RE_gas,
78
                etrago_eGon2035_gas,
79
            },
80
        )
81
82
83
def etrago_eGon2035_electricity():
84
    """Execute basic sanity checks.
85
86
    Returns print statements as sanity checks for the electricity sector in
87
    the eGon2035 scenario.
88
89
    Parameters
90
    ----------
91
    None
92
93
    Returns
94
    -------
95
    None
96
    """
97
98
    scn = "eGon2035"
99
100
    # Section to check generator capacities
101
    logger.info(f"Sanity checks for scenario {scn}")
102
    logger.info(
103
        "For German electricity generators the following deviations between "
104
        "the inputs and outputs can be observed:"
105
    )
106
107
    carriers_electricity = [
108
        "others",
109
        "reservoir",
110
        "run_of_river",
111
        "oil",
112
        "wind_onshore",
113
        "wind_offshore",
114
        "solar",
115
        "solar_rooftop",
116
        "biomass",
117
    ]
118
119
    for carrier in carriers_electricity:
120
121
        if carrier == "biomass":
122
            sum_output = db.select_dataframe(
123
                """SELECT scn_name, SUM(p_nom::numeric) as output_capacity_mw
124
                    FROM grid.egon_etrago_generator
125
                    WHERE bus IN (
126
                        SELECT bus_id FROM grid.egon_etrago_bus
127
                        WHERE scn_name = 'eGon2035'
128
                        AND country = 'DE')
129
                    AND carrier IN ('biomass', 'industrial_biomass_CHP',
130
                    'central_biomass_CHP')
131
                    GROUP BY (scn_name);
132
                """,
133
                warning=False,
134
            )
135
136
        else:
137
            sum_output = db.select_dataframe(
138
                f"""SELECT scn_name,
139
                 SUM(p_nom::numeric) as output_capacity_mw
140
                         FROM grid.egon_etrago_generator
141
                         WHERE scn_name = '{scn}'
142
                         AND carrier IN ('{carrier}')
143
                         AND bus IN
144
                             (SELECT bus_id
145
                               FROM grid.egon_etrago_bus
146
                               WHERE scn_name = 'eGon2035'
147
                               AND country = 'DE')
148
                         GROUP BY (scn_name);
149
                    """,
150
                warning=False,
151
            )
152
153
        sum_input = db.select_dataframe(
154
            f"""SELECT carrier, SUM(capacity::numeric) as input_capacity_mw
155
                     FROM supply.egon_scenario_capacities
156
                     WHERE carrier= '{carrier}'
157
                     AND scenario_name ='{scn}'
158
                     GROUP BY (carrier);
159
                """,
160
            warning=False,
161
        )
162
163 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
164
            sum_output.output_capacity_mw.sum() == 0
165
            and sum_input.input_capacity_mw.sum() == 0
166
        ):
167
            logger.info(
168
                f"No capacity for carrier '{carrier}' needed to be"
169
                f" distributed. Everything is fine"
170
            )
171
172
        elif (
173
            sum_input.input_capacity_mw.sum() > 0
174
            and sum_output.output_capacity_mw.sum() == 0
175
        ):
176
            logger.info(
177
                f"Error: Capacity for carrier '{carrier}' was not distributed "
178
                f"at all!"
179
            )
180
181
        elif (
182
            sum_output.output_capacity_mw.sum() > 0
183
            and sum_input.input_capacity_mw.sum() == 0
184
        ):
185
            logger.info(
186
                f"Error: Eventhough no input capacity was provided for carrier"
187
                f"'{carrier}' a capacity got distributed!"
188
            )
189
190
        else:
191
            sum_input["error"] = (
192
                (sum_output.output_capacity_mw - sum_input.input_capacity_mw)
193
                / sum_input.input_capacity_mw
194
            ) * 100
195
            g = sum_input["error"].values[0]
196
197
            logger.info(f"{carrier}: " + str(round(g, 2)) + " %")
198
199
    # Section to check storage units
200
201
    logger.info(f"Sanity checks for scenario {scn}")
202
    logger.info(
203
        "For German electrical storage units the following deviations between"
204
        "the inputs and outputs can be observed:"
205
    )
206
207
    carriers_electricity = ["pumped_hydro"]
208
209
    for carrier in carriers_electricity:
210
211
        sum_output = db.select_dataframe(
212
            f"""SELECT scn_name, SUM(p_nom::numeric) as output_capacity_mw
213
                         FROM grid.egon_etrago_storage
214
                         WHERE scn_name = '{scn}'
215
                         AND carrier IN ('{carrier}')
216
                         AND bus IN
217
                             (SELECT bus_id
218
                               FROM grid.egon_etrago_bus
219
                               WHERE scn_name = 'eGon2035'
220
                               AND country = 'DE')
221
                         GROUP BY (scn_name);
222
                    """,
223
            warning=False,
224
        )
225
226
        sum_input = db.select_dataframe(
227
            f"""SELECT carrier, SUM(capacity::numeric) as input_capacity_mw
228
                     FROM supply.egon_scenario_capacities
229
                     WHERE carrier= '{carrier}'
230
                     AND scenario_name ='{scn}'
231
                     GROUP BY (carrier);
232
                """,
233
            warning=False,
234
        )
235
236 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
237
            sum_output.output_capacity_mw.sum() == 0
238
            and sum_input.input_capacity_mw.sum() == 0
239
        ):
240
            print(
241
                f"No capacity for carrier '{carrier}' needed to be "
242
                f"distributed. Everything is fine"
243
            )
244
245
        elif (
246
            sum_input.input_capacity_mw.sum() > 0
247
            and sum_output.output_capacity_mw.sum() == 0
248
        ):
249
            print(
250
                f"Error: Capacity for carrier '{carrier}' was not distributed"
251
                f" at all!"
252
            )
253
254
        elif (
255
            sum_output.output_capacity_mw.sum() > 0
256
            and sum_input.input_capacity_mw.sum() == 0
257
        ):
258
            print(
259
                f"Error: Eventhough no input capacity was provided for carrier"
260
                f" '{carrier}' a capacity got distributed!"
261
            )
262
263
        else:
264
            sum_input["error"] = (
265
                (sum_output.output_capacity_mw - sum_input.input_capacity_mw)
266
                / sum_input.input_capacity_mw
267
            ) * 100
268
            g = sum_input["error"].values[0]
269
270
            print(f"{carrier}: " + str(round(g, 2)) + " %")
271
272
    # Section to check loads
273
274
    print(
275
        "For German electricity loads the following deviations between the"
276
        " input and output can be observed:"
277
    )
278
279
    output_demand = db.select_dataframe(
280
        """SELECT a.scn_name, a.carrier,  SUM((SELECT SUM(p)
281
        FROM UNNEST(b.p_set) p))/1000000::numeric as load_twh
282
            FROM grid.egon_etrago_load a
283
            JOIN grid.egon_etrago_load_timeseries b
284
            ON (a.load_id = b.load_id)
285
            JOIN grid.egon_etrago_bus c
286
            ON (a.bus=c.bus_id)
287
            AND b.scn_name = 'eGon2035'
288
            AND a.scn_name = 'eGon2035'
289
            AND a.carrier = 'AC'
290
            AND c.scn_name= 'eGon2035'
291
            AND c.country='DE'
292
            GROUP BY (a.scn_name, a.carrier);
293
294
    """,
295
        warning=False,
296
    )["load_twh"].values[0]
297
298
    input_cts_ind = db.select_dataframe(
299
        """SELECT scenario,
300
         SUM(demand::numeric/1000000) as demand_mw_regio_cts_ind
301
            FROM demand.egon_demandregio_cts_ind
302
            WHERE scenario= 'eGon2035'
303
            AND year IN ('2035')
304
            GROUP BY (scenario);
305
306
        """,
307
        warning=False,
308
    )["demand_mw_regio_cts_ind"].values[0]
309
310
    input_hh = db.select_dataframe(
311
        """SELECT scenario, SUM(demand::numeric/1000000) as demand_mw_regio_hh
312
            FROM demand.egon_demandregio_hh
313
            WHERE scenario= 'eGon2035'
314
            AND year IN ('2035')
315
            GROUP BY (scenario);
316
        """,
317
        warning=False,
318
    )["demand_mw_regio_hh"].values[0]
319
320
    input_demand = input_hh + input_cts_ind
321
322
    e = round((output_demand - input_demand) / input_demand, 2) * 100
323
324
    print(f"electricity demand: {e} %")
325
326
327
def etrago_eGon2035_heat():
328
    """Execute basic sanity checks.
329
330
    Returns print statements as sanity checks for the heat sector in
331
    the eGon2035 scenario.
332
333
    Parameters
334
    ----------
335
    None
336
337
    Returns
338
    -------
339
    None
340
    """
341
342
    # Check input and output values for the carriers "others",
343
    # "reservoir", "run_of_river" and "oil"
344
345
    scn = "eGon2035"
346
347
    # Section to check generator capacities
348
    print(f"Sanity checks for scenario {scn}")
349
    print(
350
        "For German heat demands the following deviations between the inputs"
351
        " and outputs can be observed:"
352
    )
353
354
    # Sanity checks for heat demand
355
356
    output_heat_demand = db.select_dataframe(
357
        """SELECT a.scn_name,
358
          (SUM(
359
          (SELECT SUM(p) FROM UNNEST(b.p_set) p))/1000000)::numeric as load_twh
360
            FROM grid.egon_etrago_load a
361
            JOIN grid.egon_etrago_load_timeseries b
362
            ON (a.load_id = b.load_id)
363
            JOIN grid.egon_etrago_bus c
364
            ON (a.bus=c.bus_id)
365
            AND b.scn_name = 'eGon2035'
366
            AND a.scn_name = 'eGon2035'
367
            AND c.scn_name= 'eGon2035'
368
            AND c.country='DE'
369
            AND a.carrier IN ('rural_heat', 'central_heat')
370
            GROUP BY (a.scn_name);
371
        """,
372
        warning=False,
373
    )["load_twh"].values[0]
374
375
    input_heat_demand = db.select_dataframe(
376
        """SELECT scenario, SUM(demand::numeric/1000000) as demand_mw_peta_heat
377
            FROM demand.egon_peta_heat
378
            WHERE scenario= 'eGon2035'
379
            GROUP BY (scenario);
380
        """,
381
        warning=False,
382
    )["demand_mw_peta_heat"].values[0]
383
384
    e_demand = (
385
        round((output_heat_demand - input_heat_demand) / input_heat_demand, 2)
386
        * 100
387
    )
388
389
    logger.info(f"heat demand: {e_demand} %")
390
391
    # Sanity checks for heat supply
392
393
    logger.info(
394
        "For German heat supplies the following deviations between the inputs "
395
        "and outputs can be observed:"
396
    )
397
398
    # Comparison for central heat pumps
399
    heat_pump_input = db.select_dataframe(
400
        """SELECT carrier, SUM(capacity::numeric) as Urban_central_heat_pump_mw
401
            FROM supply.egon_scenario_capacities
402
            WHERE carrier= 'urban_central_heat_pump'
403
            AND scenario_name IN ('eGon2035')
404
            GROUP BY (carrier);
405
        """,
406
        warning=False,
407
    )["urban_central_heat_pump_mw"].values[0]
408
409
    heat_pump_output = db.select_dataframe(
410
        """SELECT carrier, SUM(p_nom::numeric) as Central_heat_pump_mw
411
            FROM grid.egon_etrago_link
412
            WHERE carrier= 'central_heat_pump'
413
            AND scn_name IN ('eGon2035')
414
            GROUP BY (carrier);
415
    """,
416
        warning=False,
417
    )["central_heat_pump_mw"].values[0]
418
419
    e_heat_pump = (
420
        round((heat_pump_output - heat_pump_input) / heat_pump_output, 2) * 100
421
    )
422
423
    logger.info(f"'central_heat_pump': {e_heat_pump} % ")
424
425
    # Comparison for residential heat pumps
426
427
    input_residential_heat_pump = db.select_dataframe(
428
        """SELECT carrier, SUM(capacity::numeric) as residential_heat_pump_mw
429
            FROM supply.egon_scenario_capacities
430
            WHERE carrier= 'residential_rural_heat_pump'
431
            AND scenario_name IN ('eGon2035')
432
            GROUP BY (carrier);
433
        """,
434
        warning=False,
435
    )["residential_heat_pump_mw"].values[0]
436
437
    output_residential_heat_pump = db.select_dataframe(
438
        """SELECT carrier, SUM(p_nom::numeric) as rural_heat_pump_mw
439
            FROM grid.egon_etrago_link
440
            WHERE carrier= 'rural_heat_pump'
441
            AND scn_name IN ('eGon2035')
442
            GROUP BY (carrier);
443
    """,
444
        warning=False,
445
    )["rural_heat_pump_mw"].values[0]
446
447
    e_residential_heat_pump = (
448
        round(
449
            (output_residential_heat_pump - input_residential_heat_pump)
450
            / input_residential_heat_pump,
451
            2,
452
        )
453
        * 100
454
    )
455
    logger.info(f"'residential heat pumps': {e_residential_heat_pump} %")
456
457
    # Comparison for resistive heater
458
    resistive_heater_input = db.select_dataframe(
459
        """SELECT carrier,
460
         SUM(capacity::numeric) as Urban_central_resistive_heater_MW
461
            FROM supply.egon_scenario_capacities
462
            WHERE carrier= 'urban_central_resistive_heater'
463
            AND scenario_name IN ('eGon2035')
464
            GROUP BY (carrier);
465
        """,
466
        warning=False,
467
    )["urban_central_resistive_heater_mw"].values[0]
468
469
    resistive_heater_output = db.select_dataframe(
470
        """SELECT carrier, SUM(p_nom::numeric) as central_resistive_heater_MW
471
            FROM grid.egon_etrago_link
472
            WHERE carrier= 'central_resistive_heater'
473
            AND scn_name IN ('eGon2035')
474
            GROUP BY (carrier);
475
        """,
476
        warning=False,
477
    )["central_resistive_heater_mw"].values[0]
478
479
    e_resistive_heater = (
480
        round(
481
            (resistive_heater_output - resistive_heater_input)
482
            / resistive_heater_input,
483
            2,
484
        )
485
        * 100
486
    )
487
488
    logger.info(f"'resistive heater': {e_resistive_heater} %")
489
490
    # Comparison for solar thermal collectors
491
492
    input_solar_thermal = db.select_dataframe(
493
        """SELECT carrier, SUM(capacity::numeric) as solar_thermal_collector_mw
494
            FROM supply.egon_scenario_capacities
495
            WHERE carrier= 'urban_central_solar_thermal_collector'
496
            AND scenario_name IN ('eGon2035')
497
            GROUP BY (carrier);
498
        """,
499
        warning=False,
500
    )["solar_thermal_collector_mw"].values[0]
501
502
    output_solar_thermal = db.select_dataframe(
503
        """SELECT carrier, SUM(p_nom::numeric) as solar_thermal_collector_mw
504
            FROM grid.egon_etrago_generator
505
            WHERE carrier= 'solar_thermal_collector'
506
            AND scn_name IN ('eGon2035')
507
            GROUP BY (carrier);
508
        """,
509
        warning=False,
510
    )["solar_thermal_collector_mw"].values[0]
511
512
    e_solar_thermal = (
513
        round(
514
            (output_solar_thermal - input_solar_thermal) / input_solar_thermal,
515
            2,
516
        )
517
        * 100
518
    )
519
    logger.info(f"'solar thermal collector': {e_solar_thermal} %")
520
521
    # Comparison for geothermal
522
523
    input_geo_thermal = db.select_dataframe(
524
        """SELECT carrier,
525
         SUM(capacity::numeric) as Urban_central_geo_thermal_MW
526
            FROM supply.egon_scenario_capacities
527
            WHERE carrier= 'urban_central_geo_thermal'
528
            AND scenario_name IN ('eGon2035')
529
            GROUP BY (carrier);
530
        """,
531
        warning=False,
532
    )["urban_central_geo_thermal_mw"].values[0]
533
534
    output_geo_thermal = db.select_dataframe(
535
        """SELECT carrier, SUM(p_nom::numeric) as geo_thermal_MW
536
            FROM grid.egon_etrago_generator
537
            WHERE carrier= 'geo_thermal'
538
            AND scn_name IN ('eGon2035')
539
            GROUP BY (carrier);
540
    """,
541
        warning=False,
542
    )["geo_thermal_mw"].values[0]
543
544
    e_geo_thermal = (
545
        round((output_geo_thermal - input_geo_thermal) / input_geo_thermal, 2)
546
        * 100
547
    )
548
    logger.info(f"'geothermal': {e_geo_thermal} %")
549
550
551
def residential_electricity_annual_sum(rtol=1e-5):
552
    """Sanity check for dataset electricity_demand_timeseries :
553
    Demand_Building_Assignment
554
555
    Aggregate the annual demand of all census cells at NUTS3 to compare
556
    with initial scaling parameters from DemandRegio.
557
    """
558
559
    df_nuts3_annual_sum = db.select_dataframe(
560
        sql="""
561
        SELECT dr.nuts3, dr.scenario, dr.demand_regio_sum, profiles.profile_sum
562
        FROM (
563
            SELECT scenario, SUM(demand) AS profile_sum, vg250_nuts3
564
            FROM demand.egon_demandregio_zensus_electricity AS egon,
565
             boundaries.egon_map_zensus_vg250 AS boundaries
566
            Where egon.zensus_population_id = boundaries.zensus_population_id
567
            AND sector = 'residential'
568
            GROUP BY vg250_nuts3, scenario
569
            ) AS profiles
570
        JOIN (
571
            SELECT nuts3, scenario, sum(demand) AS demand_regio_sum
572
            FROM demand.egon_demandregio_hh
573
            GROUP BY year, scenario, nuts3
574
              ) AS dr
575
        ON profiles.vg250_nuts3 = dr.nuts3 and profiles.scenario  = dr.scenario
576
        """
577
    )
578
579
    np.testing.assert_allclose(
580
        actual=df_nuts3_annual_sum["profile_sum"],
581
        desired=df_nuts3_annual_sum["demand_regio_sum"],
582
        rtol=rtol,
583
        verbose=False,
584
    )
585
586
    logger.info(
587
        "Aggregated annual residential electricity demand"
588
        " matches with DemandRegio at NUTS-3."
589
    )
590
591
592
def residential_electricity_hh_refinement(rtol=1e-5):
593
    """Sanity check for dataset electricity_demand_timeseries :
594
    Household Demands
595
596
    Check sum of aggregated household types after refinement method
597
    was applied and compare it to the original census values."""
598
599
    df_refinement = db.select_dataframe(
600
        sql="""
601
        SELECT refined.nuts3, refined.characteristics_code,
602
                refined.sum_refined::int, census.sum_census::int
603
        FROM(
604
            SELECT nuts3, characteristics_code, SUM(hh_10types) as sum_refined
605
            FROM society.egon_destatis_zensus_household_per_ha_refined
606
            GROUP BY nuts3, characteristics_code)
607
            AS refined
608
        JOIN(
609
            SELECT t.nuts3, t.characteristics_code, sum(orig) as sum_census
610
            FROM(
611
                SELECT nuts3, cell_id, characteristics_code,
612
                        sum(DISTINCT(hh_5types))as orig
613
                FROM society.egon_destatis_zensus_household_per_ha_refined
614
                GROUP BY cell_id, characteristics_code, nuts3) AS t
615
            GROUP BY t.nuts3, t.characteristics_code    ) AS census
616
        ON refined.nuts3 = census.nuts3
617
        AND refined.characteristics_code = census.characteristics_code
618
    """
619
    )
620
621
    np.testing.assert_allclose(
622
        actual=df_refinement["sum_refined"],
623
        desired=df_refinement["sum_census"],
624
        rtol=rtol,
625
        verbose=False,
626
    )
627
628
    logger.info("All Aggregated household types match at NUTS-3.")
629
630
631
def cts_electricity_demand_share(rtol=1e-5):
632
    """Sanity check for dataset electricity_demand_timeseries :
633
    CtsBuildings
634
635
    Check sum of aggregated cts electricity demand share which equals to one
636
    for every substation as the substation profile is linearly disaggregated
637
    to all buildings."""
638
639
    with db.session_scope() as session:
640
        cells_query = session.query(EgonCtsElectricityDemandBuildingShare)
641
642
    df_demand_share = pd.read_sql(
643
        cells_query.statement, cells_query.session.bind, index_col=None
644
    )
645
646
    np.testing.assert_allclose(
647
        actual=df_demand_share.groupby(["bus_id", "scenario"])[
648
            "profile_share"
649
        ].sum(),
650
        desired=1,
651
        rtol=rtol,
652
        verbose=False,
653
    )
654
655
    logger.info("The aggregated demand shares equal to one!.")
656
657
658
def cts_heat_demand_share(rtol=1e-5):
659
    """Sanity check for dataset electricity_demand_timeseries
660
    : CtsBuildings
661
662
    Check sum of aggregated cts heat demand share which equals to one
663
    for every substation as the substation profile is linearly disaggregated
664
    to all buildings."""
665
666
    with db.session_scope() as session:
667
        cells_query = session.query(EgonCtsHeatDemandBuildingShare)
668
669
    df_demand_share = pd.read_sql(
670
        cells_query.statement, cells_query.session.bind, index_col=None
671
    )
672
673
    np.testing.assert_allclose(
674
        actual=df_demand_share.groupby(["bus_id", "scenario"])[
675
            "profile_share"
676
        ].sum(),
677
        desired=1,
678
        rtol=rtol,
679
        verbose=False,
680
    )
681
682
    logger.info("The aggregated demand shares equal to one!.")
683
684
685
def sanitycheck_emobility_mit():
686
    """Execute sanity checks for eMobility: motorized individual travel
687
688
    Checks data integrity for eGon2035, eGon2035_lowflex and eGon100RE scenario
689
    using assertions:
690
      1. Allocated EV numbers and EVs allocated to grid districts
691
      2. Trip data (original inout data from simBEV)
692
      3. Model data in eTraGo PF tables (grid.egon_etrago_*)
693
694
    Parameters
695
    ----------
696
    None
697
698
    Returns
699
    -------
700
    None
701
    """
702
703
    def check_ev_allocation():
704
        # Get target number for scenario
705
        ev_count_target = scenario_variation_parameters["ev_count"]
706
        print(f"  Target count: {str(ev_count_target)}")
707
708
        # Get allocated numbers
709
        ev_counts_dict = {}
710
        with db.session_scope() as session:
711
            for table, level in zip(
712
                [
713
                    EgonEvCountMvGridDistrict,
714
                    EgonEvCountMunicipality,
715
                    EgonEvCountRegistrationDistrict,
716
                ],
717
                ["Grid District", "Municipality", "Registration District"],
718
            ):
719
                query = session.query(
720
                    func.sum(
721
                        table.bev_mini
722
                        + table.bev_medium
723
                        + table.bev_luxury
724
                        + table.phev_mini
725
                        + table.phev_medium
726
                        + table.phev_luxury
727
                    ).label("ev_count")
728
                ).filter(
729
                    table.scenario == scenario_name,
730
                    table.scenario_variation == scenario_var_name,
731
                )
732
733
                ev_counts = pd.read_sql(
734
                    query.statement, query.session.bind, index_col=None
735
                )
736
                ev_counts_dict[level] = ev_counts.iloc[0].ev_count
737
                print(
738
                    f"    Count table: Total count for level {level} "
739
                    f"(table: {table.__table__}): "
740
                    f"{str(ev_counts_dict[level])}"
741
                )
742
743
        # Compare with scenario target (only if not in testmode)
744
        if TESTMODE_OFF:
745
            for level, count in ev_counts_dict.items():
746
                np.testing.assert_allclose(
747
                    count,
748
                    ev_count_target,
749
                    rtol=0.0001,
750
                    err_msg=f"EV numbers in {level} seems to be flawed.",
751
                )
752
        else:
753
            print("    Testmode is on, skipping sanity check...")
754
755
        # Get allocated EVs in grid districts
756
        with db.session_scope() as session:
757
            query = session.query(
758
                func.count(EgonEvMvGridDistrict.egon_ev_pool_ev_id).label(
759
                    "ev_count"
760
                ),
761
            ).filter(
762
                EgonEvMvGridDistrict.scenario == scenario_name,
763
                EgonEvMvGridDistrict.scenario_variation == scenario_var_name,
764
            )
765
        ev_count_alloc = (
766
            pd.read_sql(query.statement, query.session.bind, index_col=None)
767
            .iloc[0]
768
            .ev_count
769
        )
770
        print(
771
            f"    EVs allocated to Grid Districts "
772
            f"(table: {EgonEvMvGridDistrict.__table__}) total count: "
773
            f"{str(ev_count_alloc)}"
774
        )
775
776
        # Compare with scenario target (only if not in testmode)
777
        if TESTMODE_OFF:
778
            np.testing.assert_allclose(
779
                ev_count_alloc,
780
                ev_count_target,
781
                rtol=0.0001,
782
                err_msg=(
783
                    "EV numbers allocated to Grid Districts seems to be "
784
                    "flawed."
785
                ),
786
            )
787
        else:
788
            print("    Testmode is on, skipping sanity check...")
789
790
        return ev_count_alloc
791
792
    def check_trip_data():
793
        # Check if trips start at timestep 0 and have a max. of 35040 steps
794
        # (8760h in 15min steps)
795
        print("  Checking timeranges...")
796
        with db.session_scope() as session:
797
            query = session.query(
798
                func.count(EgonEvTrip.event_id).label("cnt")
799
            ).filter(
800
                or_(
801
                    and_(
802
                        EgonEvTrip.park_start > 0,
803
                        EgonEvTrip.simbev_event_id == 0,
804
                    ),
805
                    EgonEvTrip.park_end
806
                    > (60 / int(meta_run_config.stepsize)) * 8760,
807
                ),
808
                EgonEvTrip.scenario == scenario_name,
809
            )
810
        invalid_trips = pd.read_sql(
811
            query.statement, query.session.bind, index_col=None
812
        )
813
        np.testing.assert_equal(
814
            invalid_trips.iloc[0].cnt,
815
            0,
816
            err_msg=(
817
                f"{str(invalid_trips.iloc[0].cnt)} trips in table "
818
                f"{EgonEvTrip.__table__} have invalid timesteps."
819
            ),
820
        )
821
822
        # Check if charging demand can be covered by available charging energy
823
        # while parking
824
        print("  Compare charging demand with available power...")
825
        with db.session_scope() as session:
826
            query = session.query(
827
                func.count(EgonEvTrip.event_id).label("cnt")
828
            ).filter(
829
                func.round(
830
                    cast(
831
                        (EgonEvTrip.park_end - EgonEvTrip.park_start + 1)
832
                        * EgonEvTrip.charging_capacity_nominal
833
                        * (int(meta_run_config.stepsize) / 60),
834
                        Numeric,
835
                    ),
836
                    3,
837
                )
838
                < cast(EgonEvTrip.charging_demand, Numeric),
839
                EgonEvTrip.scenario == scenario_name,
840
            )
841
        invalid_trips = pd.read_sql(
842
            query.statement, query.session.bind, index_col=None
843
        )
844
        np.testing.assert_equal(
845
            invalid_trips.iloc[0].cnt,
846
            0,
847
            err_msg=(
848
                f"In {str(invalid_trips.iloc[0].cnt)} trips (table: "
849
                f"{EgonEvTrip.__table__}) the charging demand cannot be "
850
                f"covered by available charging power."
851
            ),
852
        )
853
854
    def check_model_data():
855
        # Check if model components were fully created
856
        print("  Check if all model components were created...")
857
        # Get MVGDs which got EV allocated
858
        with db.session_scope() as session:
859
            query = (
860
                session.query(
861
                    EgonEvMvGridDistrict.bus_id,
862
                )
863
                .filter(
864
                    EgonEvMvGridDistrict.scenario == scenario_name,
865
                    EgonEvMvGridDistrict.scenario_variation
866
                    == scenario_var_name,
867
                )
868
                .group_by(EgonEvMvGridDistrict.bus_id)
869
            )
870
        mvgds_with_ev = (
871
            pd.read_sql(query.statement, query.session.bind, index_col=None)
872
            .bus_id.sort_values()
873
            .to_list()
874
        )
875
876
        # Load model components
877
        with db.session_scope() as session:
878
            query = (
879
                session.query(
880
                    EgonPfHvLink.bus0.label("mvgd_bus_id"),
881
                    EgonPfHvLoad.bus.label("emob_bus_id"),
882
                    EgonPfHvLoad.load_id.label("load_id"),
883
                    EgonPfHvStore.store_id.label("store_id"),
884
                )
885
                .select_from(EgonPfHvLoad, EgonPfHvStore)
886
                .join(
887
                    EgonPfHvLoadTimeseries,
888
                    EgonPfHvLoadTimeseries.load_id == EgonPfHvLoad.load_id,
889
                )
890
                .join(
891
                    EgonPfHvStoreTimeseries,
892
                    EgonPfHvStoreTimeseries.store_id == EgonPfHvStore.store_id,
893
                )
894
                .filter(
895
                    EgonPfHvLoad.carrier == "land transport EV",
896
                    EgonPfHvLoad.scn_name == scenario_name,
897
                    EgonPfHvLoadTimeseries.scn_name == scenario_name,
898
                    EgonPfHvStore.carrier == "battery storage",
899
                    EgonPfHvStore.scn_name == scenario_name,
900
                    EgonPfHvStoreTimeseries.scn_name == scenario_name,
901
                    EgonPfHvLink.scn_name == scenario_name,
902
                    EgonPfHvLink.bus1 == EgonPfHvLoad.bus,
903
                    EgonPfHvLink.bus1 == EgonPfHvStore.bus,
904
                )
905
            )
906
        model_components = pd.read_sql(
907
            query.statement, query.session.bind, index_col=None
908
        )
909
910
        # Check number of buses with model components connected
911
        mvgd_buses_with_ev = model_components.loc[
912
            model_components.mvgd_bus_id.isin(mvgds_with_ev)
913
        ]
914
        np.testing.assert_equal(
915
            len(mvgds_with_ev),
916
            len(mvgd_buses_with_ev),
917
            err_msg=(
918
                f"Number of Grid Districts with connected model components "
919
                f"({str(len(mvgd_buses_with_ev))} in tables egon_etrago_*) "
920
                f"differ from number of Grid Districts that got EVs "
921
                f"allocated ({len(mvgds_with_ev)} in table "
922
                f"{EgonEvMvGridDistrict.__table__})."
923
            ),
924
        )
925
926
        # Check if all required components exist (if no id is NaN)
927
        np.testing.assert_equal(
928
            model_components.drop_duplicates().isna().any().any(),
929
            False,
930
            err_msg=(
931
                f"Some components are missing (see True values): "
932
                f"{model_components.drop_duplicates().isna().any()}"
933
            ),
934
        )
935
936
        # Get all model timeseries
937
        print("  Loading model timeseries...")
938
        # Get all model timeseries
939
        model_ts_dict = {
940
            "Load": {
941
                "carrier": "land transport EV",
942
                "table": EgonPfHvLoad,
943
                "table_ts": EgonPfHvLoadTimeseries,
944
                "column_id": "load_id",
945
                "columns_ts": ["p_set"],
946
                "ts": None,
947
            },
948
            "Link": {
949
                "carrier": "BEV charger",
950
                "table": EgonPfHvLink,
951
                "table_ts": EgonPfHvLinkTimeseries,
952
                "column_id": "link_id",
953
                "columns_ts": ["p_max_pu"],
954
                "ts": None,
955
            },
956
            "Store": {
957
                "carrier": "battery storage",
958
                "table": EgonPfHvStore,
959
                "table_ts": EgonPfHvStoreTimeseries,
960
                "column_id": "store_id",
961
                "columns_ts": ["e_min_pu", "e_max_pu"],
962
                "ts": None,
963
            },
964
        }
965
966
        with db.session_scope() as session:
967
            for node, attrs in model_ts_dict.items():
968
                print(f"    Loading {node} timeseries...")
969
                subquery = (
970
                    session.query(getattr(attrs["table"], attrs["column_id"]))
971
                    .filter(attrs["table"].carrier == attrs["carrier"])
972
                    .filter(attrs["table"].scn_name == scenario_name)
973
                    .subquery()
974
                )
975
976
                cols = [
977
                    getattr(attrs["table_ts"], c) for c in attrs["columns_ts"]
978
                ]
979
                query = session.query(
980
                    getattr(attrs["table_ts"], attrs["column_id"]), *cols
981
                ).filter(
982
                    getattr(attrs["table_ts"], attrs["column_id"]).in_(
983
                        subquery
984
                    ),
985
                    attrs["table_ts"].scn_name == scenario_name,
986
                )
987
                attrs["ts"] = pd.read_sql(
988
                    query.statement,
989
                    query.session.bind,
990
                    index_col=attrs["column_id"],
991
                )
992
993
        # Check if all timeseries have 8760 steps
994
        print("    Checking timeranges...")
995
        for node, attrs in model_ts_dict.items():
996
            for col in attrs["columns_ts"]:
997
                ts = attrs["ts"]
998
                invalid_ts = ts.loc[ts[col].apply(lambda _: len(_)) != 8760][
999
                    col
1000
                ].apply(len)
1001
                np.testing.assert_equal(
1002
                    len(invalid_ts),
1003
                    0,
1004
                    err_msg=(
1005
                        f"{str(len(invalid_ts))} rows in timeseries do not "
1006
                        f"have 8760 timesteps. Table: "
1007
                        f"{attrs['table_ts'].__table__}, Column: {col}, IDs: "
1008
                        f"{str(list(invalid_ts.index))}"
1009
                    ),
1010
                )
1011
1012
        # Compare total energy demand in model with some approximate values
1013
        # (per EV: 14,000 km/a, 0.17 kWh/km)
1014
        print("  Checking energy demand in model...")
1015
        total_energy_model = (
1016
            model_ts_dict["Load"]["ts"].p_set.apply(lambda _: sum(_)).sum()
1017
            / 1e6
1018
        )
1019
        print(f"    Total energy amount in model: {total_energy_model} TWh")
1020
        total_energy_scenario_approx = ev_count_alloc * 14000 * 0.17 / 1e9
1021
        print(
1022
            f"    Total approximated energy amount in scenario: "
1023
            f"{total_energy_scenario_approx} TWh"
1024
        )
1025
        np.testing.assert_allclose(
1026
            total_energy_model,
1027
            total_energy_scenario_approx,
1028
            rtol=0.1,
1029
            err_msg=(
1030
                "The total energy amount in the model deviates heavily "
1031
                "from the approximated value for current scenario."
1032
            ),
1033
        )
1034
1035
        # Compare total storage capacity
1036
        print("  Checking storage capacity...")
1037
        # Load storage capacities from model
1038
        with db.session_scope() as session:
1039
            query = session.query(
1040
                func.sum(EgonPfHvStore.e_nom).label("e_nom")
1041
            ).filter(
1042
                EgonPfHvStore.scn_name == scenario_name,
1043
                EgonPfHvStore.carrier == "battery storage",
1044
            )
1045
        storage_capacity_model = (
1046
            pd.read_sql(
1047
                query.statement, query.session.bind, index_col=None
1048
            ).e_nom.sum()
1049
            / 1e3
1050
        )
1051
        print(
1052
            f"    Total storage capacity ({EgonPfHvStore.__table__}): "
1053
            f"{round(storage_capacity_model, 1)} GWh"
1054
        )
1055
1056
        # Load occurences of each EV
1057
        with db.session_scope() as session:
1058
            query = (
1059
                session.query(
1060
                    EgonEvMvGridDistrict.bus_id,
1061
                    EgonEvPool.type,
1062
                    func.count(EgonEvMvGridDistrict.egon_ev_pool_ev_id).label(
1063
                        "count"
1064
                    ),
1065
                )
1066
                .join(
1067
                    EgonEvPool,
1068
                    EgonEvPool.ev_id
1069
                    == EgonEvMvGridDistrict.egon_ev_pool_ev_id,
1070
                )
1071
                .filter(
1072
                    EgonEvMvGridDistrict.scenario == scenario_name,
1073
                    EgonEvMvGridDistrict.scenario_variation
1074
                    == scenario_var_name,
1075
                    EgonEvPool.scenario == scenario_name,
1076
                )
1077
                .group_by(EgonEvMvGridDistrict.bus_id, EgonEvPool.type)
1078
            )
1079
        count_per_ev_all = pd.read_sql(
1080
            query.statement, query.session.bind, index_col="bus_id"
1081
        )
1082
        count_per_ev_all["bat_cap"] = count_per_ev_all.type.map(
1083
            meta_tech_data.battery_capacity
1084
        )
1085
        count_per_ev_all["bat_cap_total_MWh"] = (
1086
            count_per_ev_all["count"] * count_per_ev_all.bat_cap / 1e3
1087
        )
1088
        storage_capacity_simbev = count_per_ev_all.bat_cap_total_MWh.div(
1089
            1e3
1090
        ).sum()
1091
        print(
1092
            f"    Total storage capacity (simBEV): "
1093
            f"{round(storage_capacity_simbev, 1)} GWh"
1094
        )
1095
1096
        np.testing.assert_allclose(
1097
            storage_capacity_model,
1098
            storage_capacity_simbev,
1099
            rtol=0.01,
1100
            err_msg=(
1101
                "The total storage capacity in the model deviates heavily "
1102
                "from the input data provided by simBEV for current scenario."
1103
            ),
1104
        )
1105
1106
        # Check SoC storage constraint: e_min_pu < e_max_pu for all timesteps
1107
        print("  Validating SoC constraints...")
1108
        stores_with_invalid_soc = []
1109
        for idx, row in model_ts_dict["Store"]["ts"].iterrows():
1110
            ts = row[["e_min_pu", "e_max_pu"]]
1111
            x = np.array(ts.e_min_pu) > np.array(ts.e_max_pu)
1112
            if x.any():
1113
                stores_with_invalid_soc.append(idx)
1114
1115
        np.testing.assert_equal(
1116
            len(stores_with_invalid_soc),
1117
            0,
1118
            err_msg=(
1119
                f"The store constraint e_min_pu < e_max_pu does not apply "
1120
                f"for some storages in {EgonPfHvStoreTimeseries.__table__}. "
1121
                f"Invalid store_ids: {stores_with_invalid_soc}"
1122
            ),
1123
        )
1124
1125
    def check_model_data_lowflex_eGon2035():
1126
        # TODO: Add eGon100RE_lowflex
1127
        print("")
1128
        print("SCENARIO: eGon2035_lowflex")
1129
1130
        # Compare driving load and charging load
1131
        print("  Loading eGon2035 model timeseries: driving load...")
1132
        with db.session_scope() as session:
1133
            query = (
1134
                session.query(
1135
                    EgonPfHvLoad.load_id,
1136
                    EgonPfHvLoadTimeseries.p_set,
1137
                )
1138
                .join(
1139
                    EgonPfHvLoadTimeseries,
1140
                    EgonPfHvLoadTimeseries.load_id == EgonPfHvLoad.load_id,
1141
                )
1142
                .filter(
1143
                    EgonPfHvLoad.carrier == "land transport EV",
1144
                    EgonPfHvLoad.scn_name == "eGon2035",
1145
                    EgonPfHvLoadTimeseries.scn_name == "eGon2035",
1146
                )
1147
            )
1148
        model_driving_load = pd.read_sql(
1149
            query.statement, query.session.bind, index_col=None
1150
        )
1151
        driving_load = np.array(model_driving_load.p_set.to_list()).sum(axis=0)
1152
1153
        print(
1154
            "  Loading eGon2035_lowflex model timeseries: dumb charging "
1155
            "load..."
1156
        )
1157
        with db.session_scope() as session:
1158
            query = (
1159
                session.query(
1160
                    EgonPfHvLoad.load_id,
1161
                    EgonPfHvLoadTimeseries.p_set,
1162
                )
1163
                .join(
1164
                    EgonPfHvLoadTimeseries,
1165
                    EgonPfHvLoadTimeseries.load_id == EgonPfHvLoad.load_id,
1166
                )
1167
                .filter(
1168
                    EgonPfHvLoad.carrier == "land transport EV",
1169
                    EgonPfHvLoad.scn_name == "eGon2035_lowflex",
1170
                    EgonPfHvLoadTimeseries.scn_name == "eGon2035_lowflex",
1171
                )
1172
            )
1173
        model_charging_load_lowflex = pd.read_sql(
1174
            query.statement, query.session.bind, index_col=None
1175
        )
1176
        charging_load = np.array(
1177
            model_charging_load_lowflex.p_set.to_list()
1178
        ).sum(axis=0)
1179
1180
        # Ratio of driving and charging load should be 0.9 due to charging
1181
        # efficiency
1182
        print("  Compare cumulative loads...")
1183
        print(f"    Driving load (eGon2035): {driving_load.sum() / 1e6} TWh")
1184
        print(
1185
            f"    Dumb charging load (eGon2035_lowflex): "
1186
            f"{charging_load.sum() / 1e6} TWh"
1187
        )
1188
        driving_load_theoretical = (
1189
            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 1207 is not entered. Are you sure this can never be the case?
Loading history...
1190
        )
1191
        np.testing.assert_allclose(
1192
            driving_load.sum(),
1193
            driving_load_theoretical,
1194
            rtol=0.01,
1195
            err_msg=(
1196
                f"The driving load (eGon2035) deviates by more than 1% "
1197
                f"from the theoretical driving load calculated from charging "
1198
                f"load (eGon2035_lowflex) with an efficiency of "
1199
                f"{float(meta_run_config.eta_cp)}."
1200
            ),
1201
        )
1202
1203
    print("=====================================================")
1204
    print("=== SANITY CHECKS FOR MOTORIZED INDIVIDUAL TRAVEL ===")
1205
    print("=====================================================")
1206
1207
    for scenario_name in ["eGon2035", "eGon100RE"]:
1208
        scenario_var_name = DATASET_CFG["scenario"]["variation"][scenario_name]
1209
1210
        print("")
1211
        print(f"SCENARIO: {scenario_name}, VARIATION: {scenario_var_name}")
1212
1213
        # Load scenario params for scenario and scenario variation
1214
        scenario_variation_parameters = get_sector_parameters(
1215
            "mobility", scenario=scenario_name
1216
        )["motorized_individual_travel"][scenario_var_name]
1217
1218
        # Load simBEV run config and tech data
1219
        meta_run_config = read_simbev_metadata_file(
1220
            scenario_name, "config"
1221
        ).loc["basic"]
1222
        meta_tech_data = read_simbev_metadata_file(scenario_name, "tech_data")
1223
1224
        print("")
1225
        print("Checking EV counts...")
1226
        ev_count_alloc = check_ev_allocation()
1227
1228
        print("")
1229
        print("Checking trip data...")
1230
        check_trip_data()
1231
1232
        print("")
1233
        print("Checking model data...")
1234
        check_model_data()
1235
1236
    print("")
1237
    check_model_data_lowflex_eGon2035()
1238
1239
    print("=====================================================")
1240
1241
1242
def sanity_check_gas_buses(scn):
1243
    """Execute sanity checks for the gas buses in Germany
1244
1245
    Returns print statements as sanity checks for the CH4 and
1246
    H2_grid grid buses in Germany. The deviation is calculated between
1247
    the number gas grid buses in the database and the original
1248
    Scigrid_gas number of gas buses.
1249
1250
    Parameters
1251
    ----------
1252
    scn_name : str
1253
        Name of the scenario
1254
1255
    """
1256
    logger.info(f"BUSES")
1257
1258
    target_file = (
1259
        Path(".") / "datasets" / "gas_data" / "data" / "IGGIELGN_Nodes.csv"
1260
    )
1261
1262
    Grid_buses_list = pd.read_csv(
1263
        target_file,
1264
        delimiter=";",
1265
        decimal=".",
1266
        usecols=["country_code"],
1267
    )
1268
1269
    Grid_buses_list = Grid_buses_list[
1270
        Grid_buses_list["country_code"].str.match("DE")
1271
    ]
1272
    input_grid_buses = len(Grid_buses_list.index)
1273
1274
    for carrier in ["CH4", "H2_grid"]:
1275
1276
        output_grid_buses_df = db.select_dataframe(
1277
            f"""
1278
            SELECT bus_id
1279
            FROM grid.egon_etrago_bus
1280
            WHERE scn_name = '{scn}'
1281
            AND country = 'DE'
1282
            AND carrier = '{carrier}';
1283
            """,
1284
            warning=False,
1285
        )
1286
        output_grid_buses = len(output_grid_buses_df.index)
1287
1288
        e_grid_buses = (
1289
            round(
1290
                (output_grid_buses - input_grid_buses) / input_grid_buses,
1291
                2,
1292
            )
1293
            * 100
1294
        )
1295
        logger.info(f"Deviation {carrier} buses: {e_grid_buses} %")
1296
1297
1298
def sanity_check_CH4_stores(scn):
1299
    """Execute sanity checks for the CH4 stores in Germany
1300
1301
    Returns print statements as sanity checks for the CH4 stores
1302
    capacity in Germany. The deviation is calculated between:
1303
      * the sum of the capacities of the stores with carrier 'CH4'
1304
        in the database (for one scenario) and
1305
      * the sum of:
1306
          * the capacity the gas grid allocated to CH4 (total capacity
1307
            in eGon2035 and capacity reduced the share of the grid
1308
            allocated to H2 in eGon100RE) and
1309
          * the sum of the capacities of the stores in the source
1310
            document (Storages from the SciGRID_gas data)
1311
1312
    Parameters
1313
    ----------
1314
    scn_name : str
1315
        Name of the scenario
1316
1317
    """
1318
    output_CH4_stores = db.select_dataframe(
1319
        f"""SELECT SUM(e_nom::numeric) as e_nom_germany
1320
                FROM grid.egon_etrago_store
1321
                WHERE scn_name = '{scn}'
1322
                AND carrier = 'CH4'
1323
                AND bus IN
1324
                    (SELECT bus_id
1325
                    FROM grid.egon_etrago_bus
1326
                    WHERE scn_name = '{scn}'
1327
                    AND country = 'DE'
1328
                    AND carrier = 'CH4');
1329
                """,
1330
        warning=False,
1331
    )["e_nom_germany"].values[0]
1332
1333
    target_file = (
1334
        Path(".") / "datasets" / "gas_data" / "data" / "IGGIELGN_Storages.csv"
1335
    )
1336
1337
    CH4_storages_list = pd.read_csv(
1338
        target_file,
1339
        delimiter=";",
1340
        decimal=".",
1341
        usecols=["country_code", "param"],
1342
    )
1343
1344
    CH4_storages_list = CH4_storages_list[
1345
        CH4_storages_list["country_code"].str.match("DE")
1346
    ]
1347
1348
    max_workingGas_M_m3 = []
1349
    end_year = []
1350
    for index, row in CH4_storages_list.iterrows():
1351
        param = ast.literal_eval(row["param"])
1352
        end_year.append(param["end_year"])
1353
        max_workingGas_M_m3.append(param["max_workingGas_M_m3"])
1354
    CH4_storages_list["max_workingGas_M_m3"] = max_workingGas_M_m3
1355
    CH4_storages_list["end_year"] = [
1356
        float("inf") if x == None else x for x in end_year
1357
    ]
1358
1359
    # Remove unused storage units
1360
    CH4_storages_list = CH4_storages_list[
1361
        CH4_storages_list["end_year"]
1362
        >= get_sector_parameters("global", scn)["population_year"]
1363
    ]
1364
1365
    if scn == "eGon2035":
1366
        grid_cap = 130000
1367
    elif scn == "eGon100RE":
1368
        grid_cap = 13000 * (
1369
            1
1370
            - get_sector_parameters("gas", "eGon100RE")[
1371
                "retrofitted_CH4pipeline-to-H2pipeline_share"
1372
            ]
1373
        )
1374
    conv_factor = 10830  # gross calorific value = 39 MJ/m3 (eurogas.org)
1375
    input_CH4_stores = (
1376
        conv_factor * sum(CH4_storages_list["max_workingGas_M_m3"].to_list())
1377
        + grid_cap
0 ignored issues
show
introduced by
The variable grid_cap does not seem to be defined for all execution paths.
Loading history...
1378
    )
1379
1380
    e_CH4_stores = (
1381
        round(
1382
            (output_CH4_stores - input_CH4_stores) / input_CH4_stores,
1383
            2,
1384
        )
1385
        * 100
1386
    )
1387
    logger.info(f"Deviation CH4 stores: {e_CH4_stores} %")
1388
1389
1390
def sanity_check_H2_saltcavern_stores(scn):
1391
    """Execute sanity checks for the H2 saltcavern stores in Germany
1392
1393
    Returns print as sanity checks for the H2 saltcavern potential
1394
    storage capacity in Germany. The deviation is calculated between:
1395
      * the sum of the of the H2 saltcavern potential storage capacity
1396
        (e_nom_max) in the database and
1397
      * the sum of the H2 saltcavern potential storage capacity
1398
        assumed to be the ratio of the areas of 500 m radius around
1399
        substations in each german federal state and the estimated
1400
        total hydrogen storage potential of the corresponding federal
1401
        state (data from InSpEE-DS report).
1402
1403
    This test works also in test mode.
1404
1405
    Parameters
1406
    ----------
1407
    scn_name : str
1408
        Name of the scenario
1409
1410
    """
1411
    output_H2_stores = db.select_dataframe(
1412
        f"""SELECT SUM(e_nom_max::numeric) as e_nom_max_germany
1413
                FROM grid.egon_etrago_store
1414
                WHERE scn_name = '{scn}'
1415
                AND carrier = 'H2_underground'
1416
                AND bus IN
1417
                    (SELECT bus_id
1418
                    FROM grid.egon_etrago_bus
1419
                    WHERE scn_name = '{scn}'
1420
                    AND country = 'DE'
1421
                    AND carrier = 'H2_saltcavern');
1422
                """,
1423
        warning=False,
1424
    )["e_nom_max_germany"].values[0]
1425
1426
    storage_potentials = calculate_and_map_saltcavern_storage_potential()
1427
    storage_potentials["storage_potential"] = (
1428
        storage_potentials["area_fraction"] * storage_potentials["potential"]
1429
    )
1430
    input_H2_stores = sum(storage_potentials["storage_potential"].to_list())
1431
1432
    e_H2_stores = (
1433
        round(
1434
            (output_H2_stores - input_H2_stores) / input_H2_stores,
1435
            2,
1436
        )
1437
        * 100
1438
    )
1439
    logger.info(f"Deviation H2 saltcavern stores: {e_H2_stores} %")
1440
1441
1442
def sanity_check_CH4_grid(scn):
1443
    """Execute sanity checks for the gas grid capacity in Germany
1444
1445
    Returns print statements as sanity checks for the CH4 links
1446
    (pipelines) in Germany. The deviation is calculated between
1447
    the sum of the power (p_nom) of all the CH4 pipelines in Germany
1448
    for one scenario in the database and the sum of the powers of the
1449
    imported pipelines.
1450
    In eGon100RE, the sum is reduced by the share of the grid that is
1451
    allocated to hydrogen (share calculated by PyPSA-eur-sec).
1452
1453
    This test works also in test mode.
1454
1455
    Parameters
1456
    ----------
1457
    scn_name : str
1458
        Name of the scenario
1459
1460
    Returns
1461
    -------
1462
    scn_name : float
1463
        Sum of the power (p_nom) of all the pipelines in Germany
1464
1465
    """
1466
    grid_carrier = "CH4"
1467
    output_gas_grid = db.select_dataframe(
1468
        f"""SELECT SUM(p_nom::numeric) as p_nom_germany
1469
            FROM grid.egon_etrago_link
1470
            WHERE scn_name = '{scn}'
1471
            AND carrier = '{grid_carrier}'
1472
            AND bus0 IN
1473
                (SELECT bus_id
1474
                FROM grid.egon_etrago_bus
1475
                WHERE scn_name = '{scn}'
1476
                AND country = 'DE'
1477
                AND carrier = '{grid_carrier}')
1478
            AND bus1 IN
1479
                (SELECT bus_id
1480
                FROM grid.egon_etrago_bus
1481
                WHERE scn_name = '{scn}'
1482
                AND country = 'DE'
1483
                AND carrier = '{grid_carrier}')
1484
                ;
1485
            """,
1486
        warning=False,
1487
    )["p_nom_germany"].values[0]
1488
1489
    gas_nodes_list = define_gas_nodes_list()
1490
    abroad_gas_nodes_list = insert_gas_buses_abroad()
1491
    gas_grid = define_gas_pipeline_list(gas_nodes_list, abroad_gas_nodes_list)
1492
    gas_grid_germany = gas_grid[
1493
        (gas_grid["country_0"] == "DE") & (gas_grid["country_1"] == "DE")
1494
    ]
1495
    p_nom_total = sum(gas_grid_germany["p_nom"].to_list())
1496
1497
    if scn == "eGon2035":
1498
        input_gas_grid = p_nom_total
1499
    if scn == "eGon100RE":
1500
        input_gas_grid = p_nom_total * (
1501
            1
1502
            - get_sector_parameters("gas", "eGon100RE")[
1503
                "retrofitted_CH4pipeline-to-H2pipeline_share"
1504
            ]
1505
        )
1506
1507
    e_gas_grid = (
1508
        round(
1509
            (output_gas_grid - input_gas_grid) / input_gas_grid,
0 ignored issues
show
introduced by
The variable input_gas_grid does not seem to be defined in case scn == "eGon2035" on line 1497 is False. Are you sure this can never be the case?
Loading history...
1510
            2,
1511
        )
1512
        * 100
1513
    )
1514
    logger.info(f"Deviation of the capacity of the CH4 grid: {e_gas_grid} %")
1515
1516
    return p_nom_total
1517
1518
1519
def etrago_eGon2035_gas():
1520
    """Execute basic sanity checks for the gas sector in eGon2035
1521
1522
    Returns print statements as sanity checks for the gas sector in
1523
    the eGon2035 scenario for the following components in Germany:
1524
      * Buses: with the function :py:func:`sanity_check_gas_buses`
1525
      * Loads: for the carriers 'CH4_for_industry' and 'H2_for_industry'
1526
        the deviation is calculated between the sum of the loads in the
1527
        database and the sum the loads in the sources document
1528
        (opendata.ffe database)
1529
      * Generators: the deviation is calculated between the sums of the
1530
        nominal powers of the gas generators in the database and of
1531
        the ones in the sources document (Biogaspartner Einspeiseatlas
1532
        Deutschland from the dena and Productions from the SciGRID_gas
1533
        data)
1534
      * Stores: deviations for stores with following carriers are
1535
        calculated:
1536
          * 'CH4': with the function :py:func:`sanity_check_CH4_stores`
1537
          * 'H2_underground': with the function :py:func:`sanity_check_H2_saltcavern_stores`
1538
      * Links: with the function :py:func:`sanity_check_CH4_grid`
1539
1540
    """
1541
    scn = "eGon2035"
1542
1543
    if TESTMODE_OFF:
1544
        logger.info(f"Gas sanity checks for scenario {scn}")
1545
1546
        # Buses
1547
        sanity_check_gas_buses(scn)
1548
1549
        # Loads
1550
        logger.info(f"LOADS")
1551
1552
        path = Path(".") / "datasets" / "gas_data" / "demand"
1553
        corr_file = path / "region_corr.json"
1554
        df_corr = pd.read_json(corr_file)
1555
        df_corr = df_corr.loc[:, ["id_region", "name_short"]]
1556
        df_corr.set_index("id_region", inplace=True)
1557
1558
        for carrier in ["CH4_for_industry", "H2_for_industry"]:
1559
1560
            output_gas_demand = db.select_dataframe(
1561
                f"""SELECT (SUM(
1562
                    (SELECT SUM(p)
1563
                    FROM UNNEST(b.p_set) p))/1000000)::numeric as load_twh
1564
                    FROM grid.egon_etrago_load a
1565
                    JOIN grid.egon_etrago_load_timeseries b
1566
                    ON (a.load_id = b.load_id)
1567
                    JOIN grid.egon_etrago_bus c
1568
                    ON (a.bus=c.bus_id)
1569
                    AND b.scn_name = '{scn}'
1570
                    AND a.scn_name = '{scn}'
1571
                    AND c.scn_name = '{scn}'
1572
                    AND c.country = 'DE'
1573
                    AND a.carrier = '{carrier}';
1574
                """,
1575
                warning=False,
1576
            )["load_twh"].values[0]
1577
1578
            input_gas_demand = pd.read_json(
1579
                path / (carrier + "_eGon2035.json")
1580
            )
1581
            input_gas_demand = input_gas_demand.loc[:, ["id_region", "value"]]
1582
            input_gas_demand.set_index("id_region", inplace=True)
1583
            input_gas_demand = pd.concat(
1584
                [input_gas_demand, df_corr], axis=1, join="inner"
1585
            )
1586
            input_gas_demand["NUTS0"] = (input_gas_demand["name_short"].str)[
1587
                0:2
1588
            ]
1589
            input_gas_demand = input_gas_demand[
1590
                input_gas_demand["NUTS0"].str.match("DE")
1591
            ]
1592
            input_gas_demand = sum(input_gas_demand.value.to_list()) / 1000000
1593
1594
            e_demand = (
1595
                round(
1596
                    (output_gas_demand - input_gas_demand) / input_gas_demand,
1597
                    2,
1598
                )
1599
                * 100
1600
            )
1601
            logger.info(f"Deviation {carrier}: {e_demand} %")
1602
1603
        # Generators
1604
        logger.info(f"GENERATORS")
1605
        carrier_generator = "CH4"
1606
1607
        output_gas_generation = db.select_dataframe(
1608
            f"""SELECT SUM(p_nom::numeric) as p_nom_germany
1609
                    FROM grid.egon_etrago_generator
1610
                    WHERE scn_name = '{scn}'
1611
                    AND carrier = '{carrier_generator}'
1612
                    AND bus IN
1613
                        (SELECT bus_id
1614
                        FROM grid.egon_etrago_bus
1615
                        WHERE scn_name = '{scn}'
1616
                        AND country = 'DE'
1617
                        AND carrier = '{carrier_generator}');
1618
                    """,
1619
            warning=False,
1620
        )["p_nom_germany"].values[0]
1621
1622
        target_file = (
1623
            Path(".")
1624
            / "datasets"
1625
            / "gas_data"
1626
            / "data"
1627
            / "IGGIELGN_Productions.csv"
1628
        )
1629
1630
        NG_generators_list = pd.read_csv(
1631
            target_file,
1632
            delimiter=";",
1633
            decimal=".",
1634
            usecols=["country_code", "param"],
1635
        )
1636
1637
        NG_generators_list = NG_generators_list[
1638
            NG_generators_list["country_code"].str.match("DE")
1639
        ]
1640
1641
        p_NG = 0
1642
        for index, row in NG_generators_list.iterrows():
1643
            param = ast.literal_eval(row["param"])
1644
            p_NG = p_NG + param["max_supply_M_m3_per_d"]
1645
        conversion_factor = 437.5  # MCM/day to MWh/h
1646
        p_NG = p_NG * conversion_factor
1647
1648
        basename = "Biogaspartner_Einspeiseatlas_Deutschland_2021.xlsx"
1649
        target_file = Path(".") / "datasets" / "gas_data" / basename
1650
1651
        conversion_factor_b = 0.01083  # m^3/h to MWh/h
1652
        p_biogas = (
1653
            pd.read_excel(
1654
                target_file,
1655
                usecols=["Einspeisung Biomethan [(N*m^3)/h)]"],
1656
            )["Einspeisung Biomethan [(N*m^3)/h)]"].sum()
1657
            * conversion_factor_b
1658
        )
1659
1660
        input_gas_generation = p_NG + p_biogas
1661
        e_generation = (
1662
            round(
1663
                (output_gas_generation - input_gas_generation)
1664
                / input_gas_generation,
1665
                2,
1666
            )
1667
            * 100
1668
        )
1669
        logger.info(
1670
            f"Deviation {carrier_generator} generation: {e_generation} %"
1671
        )
1672
1673
        # Stores
1674
        logger.info(f"STORES")
1675
        sanity_check_CH4_stores(scn)
1676
        sanity_check_H2_saltcavern_stores(scn)
1677
1678
        # Links
1679
        logger.info(f"LINKS")
1680
        sanity_check_CH4_grid(scn)
1681
1682
    else:
1683
        print("Testmode is on, skipping sanity check.")
1684
1685
1686
def etrago_eGon100RE_gas():
1687
    """Execute basic sanity checks for the gas sector in eGon100RE
1688
1689
    Returns print statements as sanity checks for the gas sector in
1690
    the eGon100RE scenario for the following components in Germany:
1691
      * Buses: with the function :py:func:`sanity_check_gas_buses`
1692
      * Loads: for the carriers 'CH4_for_industry' and 'H2_for_industry'
1693
        the deviation is calculated between the sum of the loads in the
1694
        database and the value calculated by PyPSA-eur-sec for Germany
1695
        (that as been spatial distributed)
1696
      * Generators: the deviation is calculated between the sums of the
1697
        nominal powers of the biogas generators in the database and of
1698
        the ones in the source document (Biogaspartner Einspeiseatlas
1699
        Deutschland from the dena)
1700
      * Stores: deviations for stores with following carriers are
1701
        calculated:
1702
          * 'CH4': with the function :py:func:`sanity_check_CH4_stores`
1703
          * 'H2_underground': with the function :py:func:`sanity_check_H2_saltcavern_stores`
1704
          * 'H2': the deviation is calculated between the store
1705
            capacity the gas grid allocated to H2 (total capacity
1706
            multiplied by the share of the grid associated to H2) and
1707
            the sum of the capacities of the storages with carrier 'H2'
1708
            in the database.
1709
      * Links: only the gas transport links do have sanity checks. The
1710
        CH4 pipelines with the function :py:func:`sanity_check_CH4_grid`.
1711
        For the H2 pipelines, the deviation is calculated between the
1712
        sum of the power (p_nom) of all the H2 pipelines in Germany in
1713
        the database and the sum of the powers of the imported pipelines.
1714
        multiplied by the share of the grid allocated to hydrogen
1715
        (share calculated by PyPSA-eur-sec). (This test works also in
1716
        test mode.)
1717
1718
    """
1719
    scn = "eGon100RE"
1720
1721
    if TESTMODE_OFF:
1722
        logger.info(f"Gas sanity checks for scenario {scn}")
1723
1724
        # Buses
1725
        sanity_check_gas_buses(scn)
1726
1727
        # Loads
1728
        logger.info(f"LOADS")
1729
1730
        for carrier in ["CH4_for_industry", "H2_for_industry"]:
1731
1732
            output_gas_demand = db.select_dataframe(
1733
                f"""SELECT (SUM(
1734
                    (SELECT SUM(p) 
1735
                    FROM UNNEST(b.p_set) p))/1000000)::numeric as load_twh
1736
                    FROM grid.egon_etrago_load a
1737
                    JOIN grid.egon_etrago_load_timeseries b
1738
                    ON (a.load_id = b.load_id)
1739
                    JOIN grid.egon_etrago_bus c
1740
                    ON (a.bus=c.bus_id)
1741
                    AND b.scn_name = '{scn}'
1742
                    AND a.scn_name = '{scn}'
1743
                    AND c.scn_name = '{scn}'
1744
                    AND c.country = 'DE'
1745
                    AND a.carrier = '{carrier}';
1746
                """,
1747
                warning=False,
1748
            )["load_twh"].values[0]
1749
1750
            n = read_network()
1751
            node_pes = {
1752
                "CH4_for_industry": "DE0 0 gas for industry",
1753
                "H2_for_industry": "DE0 0 H2 for industry",
1754
            }
1755
            input_gas_demand = (
1756
                n.loads.loc[node_pes[carrier], "p_set"] * 8760 / 1000000
1757
            )
1758
1759
            e_demand = (
1760
                round(
1761
                    (output_gas_demand - input_gas_demand) / input_gas_demand,
1762
                    2,
1763
                )
1764
                * 100
1765
            )
1766
            logger.info(f"Deviation {carrier}: {e_demand} %")
1767
1768
        # Generators
1769
        logger.info(f"GENERATORS")
1770
        carrier_generator = "CH4"
1771
1772
        output_biogas_generation = db.select_dataframe(
1773
            f"""SELECT SUM(p_nom::numeric) as p_nom_germany
1774
                    FROM grid.egon_etrago_generator
1775
                    WHERE scn_name = '{scn}'
1776
                    AND carrier = '{carrier_generator}'
1777
                    AND bus IN
1778
                        (SELECT bus_id
1779
                        FROM grid.egon_etrago_bus
1780
                        WHERE scn_name = '{scn}'
1781
                        AND country = 'DE'
1782
                        AND carrier = '{carrier_generator}');
1783
                    """,
1784
            warning=False,
1785
        )["p_nom_germany"].values[0]
1786
1787
        basename = "Biogaspartner_Einspeiseatlas_Deutschland_2021.xlsx"
1788
        target_file = Path(".") / "datasets" / "gas_data" / basename
1789
1790
        conversion_factor_b = 0.01083  # m^3/h to MWh/h
1791
        input_biogas_generation = (
1792
            pd.read_excel(
1793
                target_file,
1794
                usecols=["Einspeisung Biomethan [(N*m^3)/h)]"],
1795
            )["Einspeisung Biomethan [(N*m^3)/h)]"].sum()
1796
            * conversion_factor_b
1797
        )
1798
1799
        e_biogas_generation = (
1800
            round(
1801
                (output_biogas_generation - input_biogas_generation)
1802
                / input_biogas_generation,
1803
                2,
1804
            )
1805
            * 100
1806
        )
1807
        logger.info(f"Deviation biogas generation: {e_biogas_generation} %")
1808
1809
        # Stores
1810
        logger.info(f"STORES")
1811
        sanity_check_CH4_stores(scn)
1812
        sanity_check_H2_saltcavern_stores(scn)
1813
1814
        output_H2_grid_cap_store = db.select_dataframe(
1815
            f"""SELECT SUM(e_nom::numeric) as e_nom_germany
1816
                FROM grid.egon_etrago_store
1817
                WHERE scn_name = '{scn}'
1818
                AND carrier = 'H2'
1819
                AND bus IN
1820
                    (SELECT bus_id
1821
                    FROM grid.egon_etrago_bus
1822
                    WHERE scn_name = '{scn}'
1823
                    AND country = 'DE'
1824
                    AND carrier = 'H2_grid');
1825
                """,
1826
            warning=False,
1827
        )["e_nom_germany"].values[0]
1828
1829
        input_H2_grid_cap_store = 13000 * (
1830
            get_sector_parameters("gas", "eGon100RE")[
1831
                "retrofitted_CH4pipeline-to-H2pipeline_share"
1832
            ]
1833
        )
1834
1835
        e_H2_grid_cap_store = (
1836
            round(
1837
                (output_H2_grid_cap_store - input_H2_grid_cap_store)
1838
                / input_H2_grid_cap_store,
1839
                2,
1840
            )
1841
            * 100
1842
        )
1843
        logger.info(
1844
            f"Deviation H2 grid capacity stores: {e_H2_grid_cap_store} %"
1845
        )
1846
1847
        # Links
1848
        logger.info(f"LINKS")
1849
        p_nom_total = sanity_check_CH4_grid(scn)
1850
1851
        output_H2_grid = db.select_dataframe(
1852
            f"""SELECT SUM(p_nom::numeric) as p_nom_germany
1853
                FROM grid.egon_etrago_link
1854
                WHERE scn_name = '{scn}'
1855
                AND carrier = 'H2_retrofit'
1856
                AND bus0 IN
1857
                    (SELECT bus_id
1858
                    FROM grid.egon_etrago_bus
1859
                    WHERE scn_name = '{scn}'
1860
                    AND country = 'DE'
1861
                    AND carrier = 'H2_grid')
1862
                AND bus1 IN
1863
                    (SELECT bus_id
1864
                    FROM grid.egon_etrago_bus
1865
                    WHERE scn_name = '{scn}'
1866
                    AND country = 'DE'
1867
                    AND carrier = 'H2_grid')
1868
                    ;
1869
                """,
1870
            warning=False,
1871
        )["p_nom_germany"].values[0]
1872
1873
        input_H2_grid = p_nom_total * (
1874
            get_sector_parameters("gas", "eGon100RE")[
1875
                "retrofitted_CH4pipeline-to-H2pipeline_share"
1876
            ]
1877
        )
1878
1879
        e_H2_grid = (
1880
            round(
1881
                (output_H2_grid - input_H2_grid) / input_H2_grid,
1882
                2,
1883
            )
1884
            * 100
1885
        )
1886
        logger.info(f"Deviation of the capacity of the H2 grid: {e_H2_grid} %")
1887
1888
    else:
1889
        print("Testmode is on, skipping sanity check.")
1890