Completed
Push — dev ( 733e9e...770207 )
by
unknown
23s queued 17s
created

sanity_check_gas_links()   A

Complexity

Conditions 3

Size

Total Lines 49
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 49
rs 9.4
c 0
b 0
f 0
cc 3
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, @khelfen
7
"""
8
from math import isclose
9
from pathlib import Path
10
import ast
11
12
from sqlalchemy import Numeric
13
from sqlalchemy.sql import and_, cast, func, or_
14
import matplotlib.pyplot as plt
15
import numpy as np
16
import pandas as pd
17
import seaborn as sns
18
19
from egon.data import config, db, logger
20
from egon.data.datasets import Dataset
21
from egon.data.datasets.electricity_demand_timeseries.cts_buildings import (
22
    EgonCtsElectricityDemandBuildingShare,
23
    EgonCtsHeatDemandBuildingShare,
24
)
25
from egon.data.datasets.emobility.motorized_individual_travel.db_classes import (  # noqa: E501
26
    EgonEvCountMunicipality,
27
    EgonEvCountMvGridDistrict,
28
    EgonEvCountRegistrationDistrict,
29
    EgonEvMvGridDistrict,
30
    EgonEvPool,
31
    EgonEvTrip,
32
)
33
from egon.data.datasets.emobility.motorized_individual_travel.helpers import (
34
    DATASET_CFG,
35
    read_simbev_metadata_file,
36
)
37
from egon.data.datasets.etrago_setup import (
38
    EgonPfHvLink,
39
    EgonPfHvLinkTimeseries,
40
    EgonPfHvLoad,
41
    EgonPfHvLoadTimeseries,
42
    EgonPfHvStore,
43
    EgonPfHvStoreTimeseries,
44
)
45
from egon.data.datasets.gas_grid import (
46
    define_gas_buses_abroad,
47
    define_gas_nodes_list,
48
    define_gas_pipeline_list,
49
)
50
from egon.data.datasets.gas_neighbours.eGon2035 import (
51
    calc_capacities,
52
    calc_ch4_storage_capacities,
53
    calc_global_ch4_demand,
54
    calc_global_power_to_h2_demand,
55
    calculate_ch4_grid_capacities,
56
    import_ch4_demandTS,
57
)
58
from egon.data.datasets.hydrogen_etrago.storage import (
59
    calculate_and_map_saltcavern_storage_potential,
60
)
61
from egon.data.datasets.power_plants.pv_rooftop_buildings import (
62
    PV_CAP_PER_SQ_M,
63
    ROOF_FACTOR,
64
    SCENARIOS,
65
    load_building_data,
66
    scenario_data,
67
)
68
from egon.data.datasets.pypsaeursec import read_network
69
from egon.data.datasets.scenario_parameters import get_sector_parameters
70
from egon.data.datasets.storages.home_batteries import get_cbat_pbat_ratio
71
import egon.data
72
73
TESTMODE_OFF = (
74
    config.settings()["egon-data"]["--dataset-boundary"] == "Everything"
75
)
76
77
78
class SanityChecks(Dataset):
79
    #:
80
    name: str = "SanityChecks"
81
    #:
82
    version: str = "0.0.8"
83
84
    def __init__(self, dependencies):
85
        super().__init__(
86
            name=self.name,
87
            version=self.version,
88
            dependencies=dependencies,
89
            tasks={
90
                etrago_eGon2035_electricity,
91
                etrago_eGon2035_heat,
92
                residential_electricity_annual_sum,
93
                residential_electricity_hh_refinement,
94
                cts_electricity_demand_share,
95
                cts_heat_demand_share,
96
                sanitycheck_emobility_mit,
97
                sanitycheck_pv_rooftop_buildings,
98
                sanitycheck_home_batteries,
99
                etrago_eGon2035_gas_DE,
100
                etrago_eGon2035_gas_abroad,
101
                sanitycheck_dsm,
102
            },
103
        )
104
105
106
def etrago_eGon2035_electricity():
107
    """Execute basic sanity checks.
108
109
    Returns print statements as sanity checks for the electricity sector in
110
    the eGon2035 scenario.
111
112
    Parameters
113
    ----------
114
    None
115
116
    Returns
117
    -------
118
    None
119
    """
120
121
    scn = "eGon2035"
122
123
    # Section to check generator capacities
124
    logger.info(f"Sanity checks for scenario {scn}")
125
    logger.info(
126
        "For German electricity generators the following deviations between "
127
        "the inputs and outputs can be observed:"
128
    )
129
130
    carriers_electricity = [
131
        "others",
132
        "reservoir",
133
        "run_of_river",
134
        "oil",
135
        "wind_onshore",
136
        "wind_offshore",
137
        "solar",
138
        "solar_rooftop",
139
        "biomass",
140
    ]
141
142
    for carrier in carriers_electricity:
143
144
        if carrier == "biomass":
145
            sum_output = db.select_dataframe(
146
                """SELECT scn_name, SUM(p_nom::numeric) as output_capacity_mw
147
                    FROM grid.egon_etrago_generator
148
                    WHERE bus IN (
149
                        SELECT bus_id FROM grid.egon_etrago_bus
150
                        WHERE scn_name = 'eGon2035'
151
                        AND country = 'DE')
152
                    AND carrier IN ('biomass', 'industrial_biomass_CHP',
153
                    'central_biomass_CHP')
154
                    GROUP BY (scn_name);
155
                """,
156
                warning=False,
157
            )
158
159
        else:
160
            sum_output = db.select_dataframe(
161
                f"""SELECT scn_name,
162
                 SUM(p_nom::numeric) as output_capacity_mw
163
                         FROM grid.egon_etrago_generator
164
                         WHERE scn_name = '{scn}'
165
                         AND carrier IN ('{carrier}')
166
                         AND bus IN
167
                             (SELECT bus_id
168
                               FROM grid.egon_etrago_bus
169
                               WHERE scn_name = 'eGon2035'
170
                               AND country = 'DE')
171
                         GROUP BY (scn_name);
172
                    """,
173
                warning=False,
174
            )
175
176
        sum_input = db.select_dataframe(
177
            f"""SELECT carrier, SUM(capacity::numeric) as input_capacity_mw
178
                     FROM supply.egon_scenario_capacities
179
                     WHERE carrier= '{carrier}'
180
                     AND scenario_name ='{scn}'
181
                     GROUP BY (carrier);
182
                """,
183
            warning=False,
184
        )
185
186 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
187
            sum_output.output_capacity_mw.sum() == 0
188
            and sum_input.input_capacity_mw.sum() == 0
189
        ):
190
            logger.info(
191
                f"No capacity for carrier '{carrier}' needed to be"
192
                f" distributed. Everything is fine"
193
            )
194
195
        elif (
196
            sum_input.input_capacity_mw.sum() > 0
197
            and sum_output.output_capacity_mw.sum() == 0
198
        ):
199
            logger.info(
200
                f"Error: Capacity for carrier '{carrier}' was not distributed "
201
                f"at all!"
202
            )
203
204
        elif (
205
            sum_output.output_capacity_mw.sum() > 0
206
            and sum_input.input_capacity_mw.sum() == 0
207
        ):
208
            logger.info(
209
                f"Error: Eventhough no input capacity was provided for carrier"
210
                f"'{carrier}' a capacity got distributed!"
211
            )
212
213
        else:
214
            sum_input["error"] = (
215
                (sum_output.output_capacity_mw - sum_input.input_capacity_mw)
216
                / sum_input.input_capacity_mw
217
            ) * 100
218
            g = sum_input["error"].values[0]
219
220
            logger.info(f"{carrier}: " + str(round(g, 2)) + " %")
221
222
    # Section to check storage units
223
224
    logger.info(f"Sanity checks for scenario {scn}")
225
    logger.info(
226
        "For German electrical storage units the following deviations between"
227
        "the inputs and outputs can be observed:"
228
    )
229
230
    carriers_electricity = ["pumped_hydro"]
231
232
    for carrier in carriers_electricity:
233
234
        sum_output = db.select_dataframe(
235
            f"""SELECT scn_name, SUM(p_nom::numeric) as output_capacity_mw
236
                         FROM grid.egon_etrago_storage
237
                         WHERE scn_name = '{scn}'
238
                         AND carrier IN ('{carrier}')
239
                         AND bus IN
240
                             (SELECT bus_id
241
                               FROM grid.egon_etrago_bus
242
                               WHERE scn_name = 'eGon2035'
243
                               AND country = 'DE')
244
                         GROUP BY (scn_name);
245
                    """,
246
            warning=False,
247
        )
248
249
        sum_input = db.select_dataframe(
250
            f"""SELECT carrier, SUM(capacity::numeric) as input_capacity_mw
251
                     FROM supply.egon_scenario_capacities
252
                     WHERE carrier= '{carrier}'
253
                     AND scenario_name ='{scn}'
254
                     GROUP BY (carrier);
255
                """,
256
            warning=False,
257
        )
258
259 View Code Duplication
        if (
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
260
            sum_output.output_capacity_mw.sum() == 0
261
            and sum_input.input_capacity_mw.sum() == 0
262
        ):
263
            print(
264
                f"No capacity for carrier '{carrier}' needed to be "
265
                f"distributed. Everything is fine"
266
            )
267
268
        elif (
269
            sum_input.input_capacity_mw.sum() > 0
270
            and sum_output.output_capacity_mw.sum() == 0
271
        ):
272
            print(
273
                f"Error: Capacity for carrier '{carrier}' was not distributed"
274
                f" at all!"
275
            )
276
277
        elif (
278
            sum_output.output_capacity_mw.sum() > 0
279
            and sum_input.input_capacity_mw.sum() == 0
280
        ):
281
            print(
282
                f"Error: Eventhough no input capacity was provided for carrier"
283
                f" '{carrier}' a capacity got distributed!"
284
            )
285
286
        else:
287
            sum_input["error"] = (
288
                (sum_output.output_capacity_mw - sum_input.input_capacity_mw)
289
                / sum_input.input_capacity_mw
290
            ) * 100
291
            g = sum_input["error"].values[0]
292
293
            print(f"{carrier}: " + str(round(g, 2)) + " %")
294
295
    # Section to check loads
296
297
    print(
298
        "For German electricity loads the following deviations between the"
299
        " input and output can be observed:"
300
    )
301
302
    output_demand = db.select_dataframe(
303
        """SELECT a.scn_name, a.carrier,  SUM((SELECT SUM(p)
304
        FROM UNNEST(b.p_set) p))/1000000::numeric as load_twh
305
            FROM grid.egon_etrago_load a
306
            JOIN grid.egon_etrago_load_timeseries b
307
            ON (a.load_id = b.load_id)
308
            JOIN grid.egon_etrago_bus c
309
            ON (a.bus=c.bus_id)
310
            AND b.scn_name = 'eGon2035'
311
            AND a.scn_name = 'eGon2035'
312
            AND a.carrier = 'AC'
313
            AND c.scn_name= 'eGon2035'
314
            AND c.country='DE'
315
            GROUP BY (a.scn_name, a.carrier);
316
317
    """,
318
        warning=False,
319
    )["load_twh"].values[0]
320
321
    input_cts_ind = db.select_dataframe(
322
        """SELECT scenario,
323
         SUM(demand::numeric/1000000) as demand_mw_regio_cts_ind
324
            FROM demand.egon_demandregio_cts_ind
325
            WHERE scenario= 'eGon2035'
326
            AND year IN ('2035')
327
            GROUP BY (scenario);
328
329
        """,
330
        warning=False,
331
    )["demand_mw_regio_cts_ind"].values[0]
332
333
    input_hh = db.select_dataframe(
334
        """SELECT scenario, SUM(demand::numeric/1000000) as demand_mw_regio_hh
335
            FROM demand.egon_demandregio_hh
336
            WHERE scenario= 'eGon2035'
337
            AND year IN ('2035')
338
            GROUP BY (scenario);
339
        """,
340
        warning=False,
341
    )["demand_mw_regio_hh"].values[0]
342
343
    input_demand = input_hh + input_cts_ind
344
345
    e = round((output_demand - input_demand) / input_demand, 2) * 100
346
347
    print(f"electricity demand: {e} %")
348
349
350
def etrago_eGon2035_heat():
351
    """Execute basic sanity checks.
352
353
    Returns print statements as sanity checks for the heat sector in
354
    the eGon2035 scenario.
355
356
    Parameters
357
    ----------
358
    None
359
360
    Returns
361
    -------
362
    None
363
    """
364
365
    # Check input and output values for the carriers "others",
366
    # "reservoir", "run_of_river" and "oil"
367
368
    scn = "eGon2035"
369
370
    # Section to check generator capacities
371
    print(f"Sanity checks for scenario {scn}")
372
    print(
373
        "For German heat demands the following deviations between the inputs"
374
        " and outputs can be observed:"
375
    )
376
377
    # Sanity checks for heat demand
378
379
    output_heat_demand = db.select_dataframe(
380
        """SELECT a.scn_name,
381
          (SUM(
382
          (SELECT SUM(p) FROM UNNEST(b.p_set) p))/1000000)::numeric as load_twh
383
            FROM grid.egon_etrago_load a
384
            JOIN grid.egon_etrago_load_timeseries b
385
            ON (a.load_id = b.load_id)
386
            JOIN grid.egon_etrago_bus c
387
            ON (a.bus=c.bus_id)
388
            AND b.scn_name = 'eGon2035'
389
            AND a.scn_name = 'eGon2035'
390
            AND c.scn_name= 'eGon2035'
391
            AND c.country='DE'
392
            AND a.carrier IN ('rural_heat', 'central_heat')
393
            GROUP BY (a.scn_name);
394
        """,
395
        warning=False,
396
    )["load_twh"].values[0]
397
398
    input_heat_demand = db.select_dataframe(
399
        """SELECT scenario, SUM(demand::numeric/1000000) as demand_mw_peta_heat
400
            FROM demand.egon_peta_heat
401
            WHERE scenario= 'eGon2035'
402
            GROUP BY (scenario);
403
        """,
404
        warning=False,
405
    )["demand_mw_peta_heat"].values[0]
406
407
    e_demand = (
408
        round((output_heat_demand - input_heat_demand) / input_heat_demand, 2)
409
        * 100
410
    )
411
412
    logger.info(f"heat demand: {e_demand} %")
413
414
    # Sanity checks for heat supply
415
416
    logger.info(
417
        "For German heat supplies the following deviations between the inputs "
418
        "and outputs can be observed:"
419
    )
420
421
    # Comparison for central heat pumps
422
    heat_pump_input = db.select_dataframe(
423
        """SELECT carrier, SUM(capacity::numeric) as Urban_central_heat_pump_mw
424
            FROM supply.egon_scenario_capacities
425
            WHERE carrier= 'urban_central_heat_pump'
426
            AND scenario_name IN ('eGon2035')
427
            GROUP BY (carrier);
428
        """,
429
        warning=False,
430
    )["urban_central_heat_pump_mw"].values[0]
431
432
    heat_pump_output = db.select_dataframe(
433
        """SELECT carrier, SUM(p_nom::numeric) as Central_heat_pump_mw
434
            FROM grid.egon_etrago_link
435
            WHERE carrier= 'central_heat_pump'
436
            AND scn_name IN ('eGon2035')
437
            GROUP BY (carrier);
438
    """,
439
        warning=False,
440
    )["central_heat_pump_mw"].values[0]
441
442
    e_heat_pump = (
443
        round((heat_pump_output - heat_pump_input) / heat_pump_output, 2) * 100
444
    )
445
446
    logger.info(f"'central_heat_pump': {e_heat_pump} % ")
447
448
    # Comparison for residential heat pumps
449
450
    input_residential_heat_pump = db.select_dataframe(
451
        """SELECT carrier, SUM(capacity::numeric) as residential_heat_pump_mw
452
            FROM supply.egon_scenario_capacities
453
            WHERE carrier= 'residential_rural_heat_pump'
454
            AND scenario_name IN ('eGon2035')
455
            GROUP BY (carrier);
456
        """,
457
        warning=False,
458
    )["residential_heat_pump_mw"].values[0]
459
460
    output_residential_heat_pump = db.select_dataframe(
461
        """SELECT carrier, SUM(p_nom::numeric) as rural_heat_pump_mw
462
            FROM grid.egon_etrago_link
463
            WHERE carrier= 'rural_heat_pump'
464
            AND scn_name IN ('eGon2035')
465
            GROUP BY (carrier);
466
    """,
467
        warning=False,
468
    )["rural_heat_pump_mw"].values[0]
469
470
    e_residential_heat_pump = (
471
        round(
472
            (output_residential_heat_pump - input_residential_heat_pump)
473
            / input_residential_heat_pump,
474
            2,
475
        )
476
        * 100
477
    )
478
    logger.info(f"'residential heat pumps': {e_residential_heat_pump} %")
479
480
    # Comparison for resistive heater
481
    resistive_heater_input = db.select_dataframe(
482
        """SELECT carrier,
483
         SUM(capacity::numeric) as Urban_central_resistive_heater_MW
484
            FROM supply.egon_scenario_capacities
485
            WHERE carrier= 'urban_central_resistive_heater'
486
            AND scenario_name IN ('eGon2035')
487
            GROUP BY (carrier);
488
        """,
489
        warning=False,
490
    )["urban_central_resistive_heater_mw"].values[0]
491
492
    resistive_heater_output = db.select_dataframe(
493
        """SELECT carrier, SUM(p_nom::numeric) as central_resistive_heater_MW
494
            FROM grid.egon_etrago_link
495
            WHERE carrier= 'central_resistive_heater'
496
            AND scn_name IN ('eGon2035')
497
            GROUP BY (carrier);
498
        """,
499
        warning=False,
500
    )["central_resistive_heater_mw"].values[0]
501
502
    e_resistive_heater = (
503
        round(
504
            (resistive_heater_output - resistive_heater_input)
505
            / resistive_heater_input,
506
            2,
507
        )
508
        * 100
509
    )
510
511
    logger.info(f"'resistive heater': {e_resistive_heater} %")
512
513
    # Comparison for solar thermal collectors
514
515
    input_solar_thermal = db.select_dataframe(
516
        """SELECT carrier, SUM(capacity::numeric) as solar_thermal_collector_mw
517
            FROM supply.egon_scenario_capacities
518
            WHERE carrier= 'urban_central_solar_thermal_collector'
519
            AND scenario_name IN ('eGon2035')
520
            GROUP BY (carrier);
521
        """,
522
        warning=False,
523
    )["solar_thermal_collector_mw"].values[0]
524
525
    output_solar_thermal = db.select_dataframe(
526
        """SELECT carrier, SUM(p_nom::numeric) as solar_thermal_collector_mw
527
            FROM grid.egon_etrago_generator
528
            WHERE carrier= 'solar_thermal_collector'
529
            AND scn_name IN ('eGon2035')
530
            GROUP BY (carrier);
531
        """,
532
        warning=False,
533
    )["solar_thermal_collector_mw"].values[0]
534
535
    e_solar_thermal = (
536
        round(
537
            (output_solar_thermal - input_solar_thermal) / input_solar_thermal,
538
            2,
539
        )
540
        * 100
541
    )
542
    logger.info(f"'solar thermal collector': {e_solar_thermal} %")
543
544
    # Comparison for geothermal
545
546
    input_geo_thermal = db.select_dataframe(
547
        """SELECT carrier,
548
         SUM(capacity::numeric) as Urban_central_geo_thermal_MW
549
            FROM supply.egon_scenario_capacities
550
            WHERE carrier= 'urban_central_geo_thermal'
551
            AND scenario_name IN ('eGon2035')
552
            GROUP BY (carrier);
553
        """,
554
        warning=False,
555
    )["urban_central_geo_thermal_mw"].values[0]
556
557
    output_geo_thermal = db.select_dataframe(
558
        """SELECT carrier, SUM(p_nom::numeric) as geo_thermal_MW
559
            FROM grid.egon_etrago_generator
560
            WHERE carrier= 'geo_thermal'
561
            AND scn_name IN ('eGon2035')
562
            GROUP BY (carrier);
563
    """,
564
        warning=False,
565
    )["geo_thermal_mw"].values[0]
566
567
    e_geo_thermal = (
568
        round((output_geo_thermal - input_geo_thermal) / input_geo_thermal, 2)
569
        * 100
570
    )
571
    logger.info(f"'geothermal': {e_geo_thermal} %")
572
573
574
def residential_electricity_annual_sum(rtol=1e-5):
575
    """Sanity check for dataset electricity_demand_timeseries :
576
    Demand_Building_Assignment
577
578
    Aggregate the annual demand of all census cells at NUTS3 to compare
579
    with initial scaling parameters from DemandRegio.
580
    """
581
582
    df_nuts3_annual_sum = db.select_dataframe(
583
        sql="""
584
        SELECT dr.nuts3, dr.scenario, dr.demand_regio_sum, profiles.profile_sum
585
        FROM (
586
            SELECT scenario, SUM(demand) AS profile_sum, vg250_nuts3
587
            FROM demand.egon_demandregio_zensus_electricity AS egon,
588
             boundaries.egon_map_zensus_vg250 AS boundaries
589
            Where egon.zensus_population_id = boundaries.zensus_population_id
590
            AND sector = 'residential'
591
            GROUP BY vg250_nuts3, scenario
592
            ) AS profiles
593
        JOIN (
594
            SELECT nuts3, scenario, sum(demand) AS demand_regio_sum
595
            FROM demand.egon_demandregio_hh
596
            GROUP BY year, scenario, nuts3
597
              ) AS dr
598
        ON profiles.vg250_nuts3 = dr.nuts3 and profiles.scenario  = dr.scenario
599
        """
600
    )
601
602
    np.testing.assert_allclose(
603
        actual=df_nuts3_annual_sum["profile_sum"],
604
        desired=df_nuts3_annual_sum["demand_regio_sum"],
605
        rtol=rtol,
606
        verbose=False,
607
    )
608
609
    logger.info(
610
        "Aggregated annual residential electricity demand"
611
        " matches with DemandRegio at NUTS-3."
612
    )
613
614
615
def residential_electricity_hh_refinement(rtol=1e-5):
616
    """Sanity check for dataset electricity_demand_timeseries :
617
    Household Demands
618
619
    Check sum of aggregated household types after refinement method
620
    was applied and compare it to the original census values."""
621
622
    df_refinement = db.select_dataframe(
623
        sql="""
624
        SELECT refined.nuts3, refined.characteristics_code,
625
                refined.sum_refined::int, census.sum_census::int
626
        FROM(
627
            SELECT nuts3, characteristics_code, SUM(hh_10types) as sum_refined
628
            FROM society.egon_destatis_zensus_household_per_ha_refined
629
            GROUP BY nuts3, characteristics_code)
630
            AS refined
631
        JOIN(
632
            SELECT t.nuts3, t.characteristics_code, sum(orig) as sum_census
633
            FROM(
634
                SELECT nuts3, cell_id, characteristics_code,
635
                        sum(DISTINCT(hh_5types))as orig
636
                FROM society.egon_destatis_zensus_household_per_ha_refined
637
                GROUP BY cell_id, characteristics_code, nuts3) AS t
638
            GROUP BY t.nuts3, t.characteristics_code    ) AS census
639
        ON refined.nuts3 = census.nuts3
640
        AND refined.characteristics_code = census.characteristics_code
641
    """
642
    )
643
644
    np.testing.assert_allclose(
645
        actual=df_refinement["sum_refined"],
646
        desired=df_refinement["sum_census"],
647
        rtol=rtol,
648
        verbose=False,
649
    )
650
651
    logger.info("All Aggregated household types match at NUTS-3.")
652
653
654
def cts_electricity_demand_share(rtol=1e-5):
655
    """Sanity check for dataset electricity_demand_timeseries :
656
    CtsBuildings
657
658
    Check sum of aggregated cts electricity demand share which equals to one
659
    for every substation as the substation profile is linearly disaggregated
660
    to all buildings."""
661
662
    with db.session_scope() as session:
663
        cells_query = session.query(EgonCtsElectricityDemandBuildingShare)
664
665
    df_demand_share = pd.read_sql(
666
        cells_query.statement, cells_query.session.bind, index_col=None
667
    )
668
669
    np.testing.assert_allclose(
670
        actual=df_demand_share.groupby(["bus_id", "scenario"])[
671
            "profile_share"
672
        ].sum(),
673
        desired=1,
674
        rtol=rtol,
675
        verbose=False,
676
    )
677
678
    logger.info("The aggregated demand shares equal to one!.")
679
680
681
def cts_heat_demand_share(rtol=1e-5):
682
    """Sanity check for dataset electricity_demand_timeseries
683
    : CtsBuildings
684
685
    Check sum of aggregated cts heat demand share which equals to one
686
    for every substation as the substation profile is linearly disaggregated
687
    to all buildings."""
688
689
    with db.session_scope() as session:
690
        cells_query = session.query(EgonCtsHeatDemandBuildingShare)
691
692
    df_demand_share = pd.read_sql(
693
        cells_query.statement, cells_query.session.bind, index_col=None
694
    )
695
696
    np.testing.assert_allclose(
697
        actual=df_demand_share.groupby(["bus_id", "scenario"])[
698
            "profile_share"
699
        ].sum(),
700
        desired=1,
701
        rtol=rtol,
702
        verbose=False,
703
    )
704
705
    logger.info("The aggregated demand shares equal to one!.")
706
707
708
def sanitycheck_pv_rooftop_buildings():
709
    def egon_power_plants_pv_roof_building():
710
        sql = """
711
        SELECT *
712
        FROM supply.egon_power_plants_pv_roof_building
713
        """
714
715
        return db.select_dataframe(sql, index_col="index")
716
717
    pv_roof_df = egon_power_plants_pv_roof_building()
718
719
    valid_buildings_gdf = load_building_data()
720
721
    valid_buildings_gdf = valid_buildings_gdf.assign(
722
        bus_id=valid_buildings_gdf.bus_id.astype(int),
723
        overlay_id=valid_buildings_gdf.overlay_id.astype(int),
724
        max_cap=valid_buildings_gdf.building_area.multiply(
725
            ROOF_FACTOR * PV_CAP_PER_SQ_M
726
        ),
727
    )
728
729
    merge_df = pv_roof_df.merge(
730
        valid_buildings_gdf[["building_area"]],
731
        how="left",
732
        left_on="building_id",
733
        right_index=True,
734
    )
735
736
    assert (
737
        len(merge_df.loc[merge_df.building_area.isna()]) == 0
738
    ), f"{len(merge_df.loc[merge_df.building_area.isna()])} != 0"
739
740
    scenarios = ["status_quo", "eGon2035"]
741
742
    base_path = Path(egon.data.__path__[0]).resolve()
743
744
    res_dir = base_path / "sanity_checks"
745
746
    res_dir.mkdir(parents=True, exist_ok=True)
747
748
    for scenario in scenarios:
749
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 8))
750
751
        scenario_df = merge_df.loc[merge_df.scenario == scenario]
752
753
        logger.info(
754
            scenario + " Capacity:\n" + str(scenario_df.capacity.describe())
755
        )
756
757
        small_gens_df = scenario_df.loc[scenario_df.capacity < 100]
758
759
        sns.histplot(data=small_gens_df, x="capacity", ax=ax1).set_title(
760
            scenario
761
        )
762
763
        sns.scatterplot(
764
            data=small_gens_df, x="capacity", y="building_area", ax=ax2
765
        ).set_title(scenario)
766
767
        plt.tight_layout()
768
769
        plt.savefig(
770
            res_dir / f"{scenario}_pv_rooftop_distribution.png",
771
            bbox_inches="tight",
772
        )
773
774
    for scenario in SCENARIOS:
775
        if scenario == "eGon2035":
776
            assert isclose(
777
                scenario_data(scenario=scenario).capacity.sum(),
778
                merge_df.loc[merge_df.scenario == scenario].capacity.sum(),
779
                rel_tol=1e-02,
780
            ), (
781
                f"{scenario_data(scenario=scenario).capacity.sum()} != "
782
                f"{merge_df.loc[merge_df.scenario == scenario].capacity.sum()}"
783
            )
784
        elif scenario == "eGon100RE":
785
            sources = config.datasets()["solar_rooftop"]["sources"]
786
787
            target = db.select_dataframe(
788
                f"""
789
                SELECT capacity
790
                FROM {sources['scenario_capacities']['schema']}.
791
                {sources['scenario_capacities']['table']} a
792
                WHERE carrier = 'solar_rooftop'
793
                AND scenario_name = '{scenario}'
794
                """
795
            ).capacity[0]
796
797
            dataset = config.settings()["egon-data"]["--dataset-boundary"]
798
799 View Code Duplication
            if dataset == "Schleswig-Holstein":
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
800
                sources = config.datasets()["scenario_input"]["sources"]
801
802
                path = Path(
803
                    f"./data_bundle_egon_data/nep2035_version2021/"
804
                    f"{sources['eGon2035']['capacities']}"
805
                ).resolve()
806
807
                total_2035 = (
808
                    pd.read_excel(
809
                        path,
810
                        sheet_name="1.Entwurf_NEP2035_V2021",
811
                        index_col="Unnamed: 0",
812
                    ).at["PV (Aufdach)", "Summe"]
813
                    * 1000
814
                )
815
                sh_2035 = scenario_data(scenario="eGon2035").capacity.sum()
816
817
                share = sh_2035 / total_2035
818
819
                target *= share
820
821
            assert isclose(
822
                target,
823
                merge_df.loc[merge_df.scenario == scenario].capacity.sum(),
824
                rel_tol=1e-02,
825
            ), (
826
                f"{target} != "
827
                f"{merge_df.loc[merge_df.scenario == scenario].capacity.sum()}"
828
            )
829
        else:
830
            raise ValueError(f"Scenario {scenario} is not valid.")
831
832
833
def sanitycheck_emobility_mit():
834
    """Execute sanity checks for eMobility: motorized individual travel
835
836
    Checks data integrity for eGon2035, eGon2035_lowflex and eGon100RE scenario
837
    using assertions:
838
      1. Allocated EV numbers and EVs allocated to grid districts
839
      2. Trip data (original inout data from simBEV)
840
      3. Model data in eTraGo PF tables (grid.egon_etrago_*)
841
842
    Parameters
843
    ----------
844
    None
845
846
    Returns
847
    -------
848
    None
849
    """
850
851
    def check_ev_allocation():
852
        # Get target number for scenario
853
        ev_count_target = scenario_variation_parameters["ev_count"]
854
        print(f"  Target count: {str(ev_count_target)}")
855
856
        # Get allocated numbers
857
        ev_counts_dict = {}
858
        with db.session_scope() as session:
859
            for table, level in zip(
860
                [
861
                    EgonEvCountMvGridDistrict,
862
                    EgonEvCountMunicipality,
863
                    EgonEvCountRegistrationDistrict,
864
                ],
865
                ["Grid District", "Municipality", "Registration District"],
866
            ):
867
                query = session.query(
868
                    func.sum(
869
                        table.bev_mini
870
                        + table.bev_medium
871
                        + table.bev_luxury
872
                        + table.phev_mini
873
                        + table.phev_medium
874
                        + table.phev_luxury
875
                    ).label("ev_count")
876
                ).filter(
877
                    table.scenario == scenario_name,
878
                    table.scenario_variation == scenario_var_name,
879
                )
880
881
                ev_counts = pd.read_sql(
882
                    query.statement, query.session.bind, index_col=None
883
                )
884
                ev_counts_dict[level] = ev_counts.iloc[0].ev_count
885
                print(
886
                    f"    Count table: Total count for level {level} "
887
                    f"(table: {table.__table__}): "
888
                    f"{str(ev_counts_dict[level])}"
889
                )
890
891
        # Compare with scenario target (only if not in testmode)
892
        if TESTMODE_OFF:
893
            for level, count in ev_counts_dict.items():
894
                np.testing.assert_allclose(
895
                    count,
896
                    ev_count_target,
897
                    rtol=0.0001,
898
                    err_msg=f"EV numbers in {level} seems to be flawed.",
899
                )
900
        else:
901
            print("    Testmode is on, skipping sanity check...")
902
903
        # Get allocated EVs in grid districts
904
        with db.session_scope() as session:
905
            query = session.query(
906
                func.count(EgonEvMvGridDistrict.egon_ev_pool_ev_id).label(
907
                    "ev_count"
908
                ),
909
            ).filter(
910
                EgonEvMvGridDistrict.scenario == scenario_name,
911
                EgonEvMvGridDistrict.scenario_variation == scenario_var_name,
912
            )
913
        ev_count_alloc = (
914
            pd.read_sql(query.statement, query.session.bind, index_col=None)
915
            .iloc[0]
916
            .ev_count
917
        )
918
        print(
919
            f"    EVs allocated to Grid Districts "
920
            f"(table: {EgonEvMvGridDistrict.__table__}) total count: "
921
            f"{str(ev_count_alloc)}"
922
        )
923
924
        # Compare with scenario target (only if not in testmode)
925
        if TESTMODE_OFF:
926
            np.testing.assert_allclose(
927
                ev_count_alloc,
928
                ev_count_target,
929
                rtol=0.0001,
930
                err_msg=(
931
                    "EV numbers allocated to Grid Districts seems to be "
932
                    "flawed."
933
                ),
934
            )
935
        else:
936
            print("    Testmode is on, skipping sanity check...")
937
938
        return ev_count_alloc
939
940
    def check_trip_data():
941
        # Check if trips start at timestep 0 and have a max. of 35040 steps
942
        # (8760h in 15min steps)
943
        print("  Checking timeranges...")
944
        with db.session_scope() as session:
945
            query = session.query(
946
                func.count(EgonEvTrip.event_id).label("cnt")
947
            ).filter(
948
                or_(
949
                    and_(
950
                        EgonEvTrip.park_start > 0,
951
                        EgonEvTrip.simbev_event_id == 0,
952
                    ),
953
                    EgonEvTrip.park_end
954
                    > (60 / int(meta_run_config.stepsize)) * 8760,
955
                ),
956
                EgonEvTrip.scenario == scenario_name,
957
            )
958
        invalid_trips = pd.read_sql(
959
            query.statement, query.session.bind, index_col=None
960
        )
961
        np.testing.assert_equal(
962
            invalid_trips.iloc[0].cnt,
963
            0,
964
            err_msg=(
965
                f"{str(invalid_trips.iloc[0].cnt)} trips in table "
966
                f"{EgonEvTrip.__table__} have invalid timesteps."
967
            ),
968
        )
969
970
        # Check if charging demand can be covered by available charging energy
971
        # while parking
972
        print("  Compare charging demand with available power...")
973
        with db.session_scope() as session:
974
            query = session.query(
975
                func.count(EgonEvTrip.event_id).label("cnt")
976
            ).filter(
977
                func.round(
978
                    cast(
979
                        (EgonEvTrip.park_end - EgonEvTrip.park_start + 1)
980
                        * EgonEvTrip.charging_capacity_nominal
981
                        * (int(meta_run_config.stepsize) / 60),
982
                        Numeric,
983
                    ),
984
                    3,
985
                )
986
                < cast(EgonEvTrip.charging_demand, Numeric),
987
                EgonEvTrip.scenario == scenario_name,
988
            )
989
        invalid_trips = pd.read_sql(
990
            query.statement, query.session.bind, index_col=None
991
        )
992
        np.testing.assert_equal(
993
            invalid_trips.iloc[0].cnt,
994
            0,
995
            err_msg=(
996
                f"In {str(invalid_trips.iloc[0].cnt)} trips (table: "
997
                f"{EgonEvTrip.__table__}) the charging demand cannot be "
998
                f"covered by available charging power."
999
            ),
1000
        )
1001
1002
    def check_model_data():
1003
        # Check if model components were fully created
1004
        print("  Check if all model components were created...")
1005
        # Get MVGDs which got EV allocated
1006
        with db.session_scope() as session:
1007
            query = (
1008
                session.query(
1009
                    EgonEvMvGridDistrict.bus_id,
1010
                )
1011
                .filter(
1012
                    EgonEvMvGridDistrict.scenario == scenario_name,
1013
                    EgonEvMvGridDistrict.scenario_variation
1014
                    == scenario_var_name,
1015
                )
1016
                .group_by(EgonEvMvGridDistrict.bus_id)
1017
            )
1018
        mvgds_with_ev = (
1019
            pd.read_sql(query.statement, query.session.bind, index_col=None)
1020
            .bus_id.sort_values()
1021
            .to_list()
1022
        )
1023
1024
        # Load model components
1025
        with db.session_scope() as session:
1026
            query = (
1027
                session.query(
1028
                    EgonPfHvLink.bus0.label("mvgd_bus_id"),
1029
                    EgonPfHvLoad.bus.label("emob_bus_id"),
1030
                    EgonPfHvLoad.load_id.label("load_id"),
1031
                    EgonPfHvStore.store_id.label("store_id"),
1032
                )
1033
                .select_from(EgonPfHvLoad, EgonPfHvStore)
1034
                .join(
1035
                    EgonPfHvLoadTimeseries,
1036
                    EgonPfHvLoadTimeseries.load_id == EgonPfHvLoad.load_id,
1037
                )
1038
                .join(
1039
                    EgonPfHvStoreTimeseries,
1040
                    EgonPfHvStoreTimeseries.store_id == EgonPfHvStore.store_id,
1041
                )
1042
                .filter(
1043
                    EgonPfHvLoad.carrier == "land_transport_EV",
1044
                    EgonPfHvLoad.scn_name == scenario_name,
1045
                    EgonPfHvLoadTimeseries.scn_name == scenario_name,
1046
                    EgonPfHvStore.carrier == "battery_storage",
1047
                    EgonPfHvStore.scn_name == scenario_name,
1048
                    EgonPfHvStoreTimeseries.scn_name == scenario_name,
1049
                    EgonPfHvLink.scn_name == scenario_name,
1050
                    EgonPfHvLink.bus1 == EgonPfHvLoad.bus,
1051
                    EgonPfHvLink.bus1 == EgonPfHvStore.bus,
1052
                )
1053
            )
1054
        model_components = pd.read_sql(
1055
            query.statement, query.session.bind, index_col=None
1056
        )
1057
1058
        # Check number of buses with model components connected
1059
        mvgd_buses_with_ev = model_components.loc[
1060
            model_components.mvgd_bus_id.isin(mvgds_with_ev)
1061
        ]
1062
        np.testing.assert_equal(
1063
            len(mvgds_with_ev),
1064
            len(mvgd_buses_with_ev),
1065
            err_msg=(
1066
                f"Number of Grid Districts with connected model components "
1067
                f"({str(len(mvgd_buses_with_ev))} in tables egon_etrago_*) "
1068
                f"differ from number of Grid Districts that got EVs "
1069
                f"allocated ({len(mvgds_with_ev)} in table "
1070
                f"{EgonEvMvGridDistrict.__table__})."
1071
            ),
1072
        )
1073
1074
        # Check if all required components exist (if no id is NaN)
1075
        np.testing.assert_equal(
1076
            model_components.drop_duplicates().isna().any().any(),
1077
            False,
1078
            err_msg=(
1079
                f"Some components are missing (see True values): "
1080
                f"{model_components.drop_duplicates().isna().any()}"
1081
            ),
1082
        )
1083
1084
        # Get all model timeseries
1085
        print("  Loading model timeseries...")
1086
        # Get all model timeseries
1087
        model_ts_dict = {
1088
            "Load": {
1089
                "carrier": "land_transport_EV",
1090
                "table": EgonPfHvLoad,
1091
                "table_ts": EgonPfHvLoadTimeseries,
1092
                "column_id": "load_id",
1093
                "columns_ts": ["p_set"],
1094
                "ts": None,
1095
            },
1096
            "Link": {
1097
                "carrier": "BEV_charger",
1098
                "table": EgonPfHvLink,
1099
                "table_ts": EgonPfHvLinkTimeseries,
1100
                "column_id": "link_id",
1101
                "columns_ts": ["p_max_pu"],
1102
                "ts": None,
1103
            },
1104
            "Store": {
1105
                "carrier": "battery_storage",
1106
                "table": EgonPfHvStore,
1107
                "table_ts": EgonPfHvStoreTimeseries,
1108
                "column_id": "store_id",
1109
                "columns_ts": ["e_min_pu", "e_max_pu"],
1110
                "ts": None,
1111
            },
1112
        }
1113
1114
        with db.session_scope() as session:
1115
            for node, attrs in model_ts_dict.items():
1116
                print(f"    Loading {node} timeseries...")
1117
                subquery = (
1118
                    session.query(getattr(attrs["table"], attrs["column_id"]))
1119
                    .filter(attrs["table"].carrier == attrs["carrier"])
1120
                    .filter(attrs["table"].scn_name == scenario_name)
1121
                    .subquery()
1122
                )
1123
1124
                cols = [
1125
                    getattr(attrs["table_ts"], c) for c in attrs["columns_ts"]
1126
                ]
1127
                query = session.query(
1128
                    getattr(attrs["table_ts"], attrs["column_id"]), *cols
1129
                ).filter(
1130
                    getattr(attrs["table_ts"], attrs["column_id"]).in_(
1131
                        subquery
1132
                    ),
1133
                    attrs["table_ts"].scn_name == scenario_name,
1134
                )
1135
                attrs["ts"] = pd.read_sql(
1136
                    query.statement,
1137
                    query.session.bind,
1138
                    index_col=attrs["column_id"],
1139
                )
1140
1141
        # Check if all timeseries have 8760 steps
1142
        print("    Checking timeranges...")
1143
        for node, attrs in model_ts_dict.items():
1144
            for col in attrs["columns_ts"]:
1145
                ts = attrs["ts"]
1146
                invalid_ts = ts.loc[ts[col].apply(lambda _: len(_)) != 8760][
1147
                    col
1148
                ].apply(len)
1149
                np.testing.assert_equal(
1150
                    len(invalid_ts),
1151
                    0,
1152
                    err_msg=(
1153
                        f"{str(len(invalid_ts))} rows in timeseries do not "
1154
                        f"have 8760 timesteps. Table: "
1155
                        f"{attrs['table_ts'].__table__}, Column: {col}, IDs: "
1156
                        f"{str(list(invalid_ts.index))}"
1157
                    ),
1158
                )
1159
1160
        # Compare total energy demand in model with some approximate values
1161
        # (per EV: 14,000 km/a, 0.17 kWh/km)
1162
        print("  Checking energy demand in model...")
1163
        total_energy_model = (
1164
            model_ts_dict["Load"]["ts"].p_set.apply(lambda _: sum(_)).sum()
1165
            / 1e6
1166
        )
1167
        print(f"    Total energy amount in model: {total_energy_model} TWh")
1168
        total_energy_scenario_approx = ev_count_alloc * 14000 * 0.17 / 1e9
1169
        print(
1170
            f"    Total approximated energy amount in scenario: "
1171
            f"{total_energy_scenario_approx} TWh"
1172
        )
1173
        np.testing.assert_allclose(
1174
            total_energy_model,
1175
            total_energy_scenario_approx,
1176
            rtol=0.1,
1177
            err_msg=(
1178
                "The total energy amount in the model deviates heavily "
1179
                "from the approximated value for current scenario."
1180
            ),
1181
        )
1182
1183
        # Compare total storage capacity
1184
        print("  Checking storage capacity...")
1185
        # Load storage capacities from model
1186
        with db.session_scope() as session:
1187
            query = session.query(
1188
                func.sum(EgonPfHvStore.e_nom).label("e_nom")
1189
            ).filter(
1190
                EgonPfHvStore.scn_name == scenario_name,
1191
                EgonPfHvStore.carrier == "battery_storage",
1192
            )
1193
        storage_capacity_model = (
1194
            pd.read_sql(
1195
                query.statement, query.session.bind, index_col=None
1196
            ).e_nom.sum()
1197
            / 1e3
1198
        )
1199
        print(
1200
            f"    Total storage capacity ({EgonPfHvStore.__table__}): "
1201
            f"{round(storage_capacity_model, 1)} GWh"
1202
        )
1203
1204
        # Load occurences of each EV
1205
        with db.session_scope() as session:
1206
            query = (
1207
                session.query(
1208
                    EgonEvMvGridDistrict.bus_id,
1209
                    EgonEvPool.type,
1210
                    func.count(EgonEvMvGridDistrict.egon_ev_pool_ev_id).label(
1211
                        "count"
1212
                    ),
1213
                )
1214
                .join(
1215
                    EgonEvPool,
1216
                    EgonEvPool.ev_id
1217
                    == EgonEvMvGridDistrict.egon_ev_pool_ev_id,
1218
                )
1219
                .filter(
1220
                    EgonEvMvGridDistrict.scenario == scenario_name,
1221
                    EgonEvMvGridDistrict.scenario_variation
1222
                    == scenario_var_name,
1223
                    EgonEvPool.scenario == scenario_name,
1224
                )
1225
                .group_by(EgonEvMvGridDistrict.bus_id, EgonEvPool.type)
1226
            )
1227
        count_per_ev_all = pd.read_sql(
1228
            query.statement, query.session.bind, index_col="bus_id"
1229
        )
1230
        count_per_ev_all["bat_cap"] = count_per_ev_all.type.map(
1231
            meta_tech_data.battery_capacity
1232
        )
1233
        count_per_ev_all["bat_cap_total_MWh"] = (
1234
            count_per_ev_all["count"] * count_per_ev_all.bat_cap / 1e3
1235
        )
1236
        storage_capacity_simbev = count_per_ev_all.bat_cap_total_MWh.div(
1237
            1e3
1238
        ).sum()
1239
        print(
1240
            f"    Total storage capacity (simBEV): "
1241
            f"{round(storage_capacity_simbev, 1)} GWh"
1242
        )
1243
1244
        np.testing.assert_allclose(
1245
            storage_capacity_model,
1246
            storage_capacity_simbev,
1247
            rtol=0.01,
1248
            err_msg=(
1249
                "The total storage capacity in the model deviates heavily "
1250
                "from the input data provided by simBEV for current scenario."
1251
            ),
1252
        )
1253
1254
        # Check SoC storage constraint: e_min_pu < e_max_pu for all timesteps
1255
        print("  Validating SoC constraints...")
1256
        stores_with_invalid_soc = []
1257
        for idx, row in model_ts_dict["Store"]["ts"].iterrows():
1258
            ts = row[["e_min_pu", "e_max_pu"]]
1259
            x = np.array(ts.e_min_pu) > np.array(ts.e_max_pu)
1260
            if x.any():
1261
                stores_with_invalid_soc.append(idx)
1262
1263
        np.testing.assert_equal(
1264
            len(stores_with_invalid_soc),
1265
            0,
1266
            err_msg=(
1267
                f"The store constraint e_min_pu < e_max_pu does not apply "
1268
                f"for some storages in {EgonPfHvStoreTimeseries.__table__}. "
1269
                f"Invalid store_ids: {stores_with_invalid_soc}"
1270
            ),
1271
        )
1272
1273
    def check_model_data_lowflex_eGon2035():
1274
        # TODO: Add eGon100RE_lowflex
1275
        print("")
1276
        print("SCENARIO: eGon2035_lowflex")
1277
1278
        # Compare driving load and charging load
1279
        print("  Loading eGon2035 model timeseries: driving load...")
1280
        with db.session_scope() as session:
1281
            query = (
1282
                session.query(
1283
                    EgonPfHvLoad.load_id,
1284
                    EgonPfHvLoadTimeseries.p_set,
1285
                )
1286
                .join(
1287
                    EgonPfHvLoadTimeseries,
1288
                    EgonPfHvLoadTimeseries.load_id == EgonPfHvLoad.load_id,
1289
                )
1290
                .filter(
1291
                    EgonPfHvLoad.carrier == "land_transport_EV",
1292
                    EgonPfHvLoad.scn_name == "eGon2035",
1293
                    EgonPfHvLoadTimeseries.scn_name == "eGon2035",
1294
                )
1295
            )
1296
        model_driving_load = pd.read_sql(
1297
            query.statement, query.session.bind, index_col=None
1298
        )
1299
        driving_load = np.array(model_driving_load.p_set.to_list()).sum(axis=0)
1300
1301
        print(
1302
            "  Loading eGon2035_lowflex model timeseries: dumb charging "
1303
            "load..."
1304
        )
1305
        with db.session_scope() as session:
1306
            query = (
1307
                session.query(
1308
                    EgonPfHvLoad.load_id,
1309
                    EgonPfHvLoadTimeseries.p_set,
1310
                )
1311
                .join(
1312
                    EgonPfHvLoadTimeseries,
1313
                    EgonPfHvLoadTimeseries.load_id == EgonPfHvLoad.load_id,
1314
                )
1315
                .filter(
1316
                    EgonPfHvLoad.carrier == "land_transport_EV",
1317
                    EgonPfHvLoad.scn_name == "eGon2035_lowflex",
1318
                    EgonPfHvLoadTimeseries.scn_name == "eGon2035_lowflex",
1319
                )
1320
            )
1321
        model_charging_load_lowflex = pd.read_sql(
1322
            query.statement, query.session.bind, index_col=None
1323
        )
1324
        charging_load = np.array(
1325
            model_charging_load_lowflex.p_set.to_list()
1326
        ).sum(axis=0)
1327
1328
        # Ratio of driving and charging load should be 0.9 due to charging
1329
        # efficiency
1330
        print("  Compare cumulative loads...")
1331
        print(f"    Driving load (eGon2035): {driving_load.sum() / 1e6} TWh")
1332
        print(
1333
            f"    Dumb charging load (eGon2035_lowflex): "
1334
            f"{charging_load.sum() / 1e6} TWh"
1335
        )
1336
        driving_load_theoretical = (
1337
            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 1355 is not entered. Are you sure this can never be the case?
Loading history...
1338
        )
1339
        np.testing.assert_allclose(
1340
            driving_load.sum(),
1341
            driving_load_theoretical,
1342
            rtol=0.01,
1343
            err_msg=(
1344
                f"The driving load (eGon2035) deviates by more than 1% "
1345
                f"from the theoretical driving load calculated from charging "
1346
                f"load (eGon2035_lowflex) with an efficiency of "
1347
                f"{float(meta_run_config.eta_cp)}."
1348
            ),
1349
        )
1350
1351
    print("=====================================================")
1352
    print("=== SANITY CHECKS FOR MOTORIZED INDIVIDUAL TRAVEL ===")
1353
    print("=====================================================")
1354
1355
    for scenario_name in ["eGon2035", "eGon100RE"]:
1356
        scenario_var_name = DATASET_CFG["scenario"]["variation"][scenario_name]
1357
1358
        print("")
1359
        print(f"SCENARIO: {scenario_name}, VARIATION: {scenario_var_name}")
1360
1361
        # Load scenario params for scenario and scenario variation
1362
        scenario_variation_parameters = get_sector_parameters(
1363
            "mobility", scenario=scenario_name
1364
        )["motorized_individual_travel"][scenario_var_name]
1365
1366
        # Load simBEV run config and tech data
1367
        meta_run_config = read_simbev_metadata_file(
1368
            scenario_name, "config"
1369
        ).loc["basic"]
1370
        meta_tech_data = read_simbev_metadata_file(scenario_name, "tech_data")
1371
1372
        print("")
1373
        print("Checking EV counts...")
1374
        ev_count_alloc = check_ev_allocation()
1375
1376
        print("")
1377
        print("Checking trip data...")
1378
        check_trip_data()
1379
1380
        print("")
1381
        print("Checking model data...")
1382
        check_model_data()
1383
1384
    print("")
1385
    check_model_data_lowflex_eGon2035()
1386
1387
    print("=====================================================")
1388
1389
1390
def sanitycheck_home_batteries():
1391
    # get constants
1392
    constants = config.datasets()["home_batteries"]["constants"]
1393
    scenarios = constants["scenarios"]
1394
    cbat_pbat_ratio = get_cbat_pbat_ratio()
1395
1396
    sources = config.datasets()["home_batteries"]["sources"]
1397
    targets = config.datasets()["home_batteries"]["targets"]
1398
1399
    for scenario in scenarios:
1400
        # get home battery capacity per mv grid id
1401
        sql = f"""
1402
        SELECT el_capacity as p_nom, bus_id FROM
1403
        {sources["storage"]["schema"]}
1404
        .{sources["storage"]["table"]}
1405
        WHERE carrier = 'home_battery'
1406
        AND scenario = '{scenario}'
1407
        """
1408
1409
        home_batteries_df = db.select_dataframe(sql, index_col="bus_id")
1410
1411
        home_batteries_df = home_batteries_df.assign(
1412
            capacity=home_batteries_df.p_nom * cbat_pbat_ratio
1413
        )
1414
1415
        sql = f"""
1416
        SELECT * FROM
1417
        {targets["home_batteries"]["schema"]}
1418
        .{targets["home_batteries"]["table"]}
1419
        WHERE scenario = '{scenario}'
1420
        """
1421
1422
        home_batteries_buildings_df = db.select_dataframe(
1423
            sql, index_col="index"
1424
        )
1425
1426
        df = (
1427
            home_batteries_buildings_df[["bus_id", "p_nom", "capacity"]]
1428
            .groupby("bus_id")
1429
            .sum()
1430
        )
1431
1432
        assert (home_batteries_df.round(6) == df.round(6)).all().all()
1433
1434
1435
def sanity_check_gas_buses(scn):
1436
    """Execute sanity checks for the gas buses in Germany
1437
1438
    Returns print statements as sanity checks for the CH4, H2_grid and
1439
    H2_saltcavern buses.
1440
      * For all of them, it is checked if they are not isolated.
1441
      * For the grid buses, the deviation is calculated between the
1442
        number of gas grid buses in the database and the original
1443
        Scigrid_gas number of gas buses in Germany.
1444
1445
    Parameters
1446
    ----------
1447
    scn_name : str
1448
        Name of the scenario
1449
1450
    """
1451
    logger.info("BUSES")
1452
1453
    # Are gas buses isolated?
1454
    corresponding_carriers = {
1455
        "eGon2035": {
1456
            "CH4": "CH4",
1457
            "H2_grid": "H2_feedin",
1458
            "H2_saltcavern": "power_to_H2",
1459
        },
1460
        # "eGon100RE": {
1461
        #     "CH4": "CH4",
1462
        #     "H2_grid": "H2_retrofit",
1463
        #     "H2_saltcavern": "H2_extension",
1464
        # }
1465
    }
1466
    for key in corresponding_carriers[scn]:
1467
        isolated_gas_buses = db.select_dataframe(
1468
            f"""
1469
            SELECT bus_id, carrier, country
1470
            FROM grid.egon_etrago_bus
1471
            WHERE scn_name = '{scn}'
1472
            AND carrier = '{key}'
1473
            AND country = 'DE'
1474
            AND bus_id NOT IN
1475
                (SELECT bus0
1476
                FROM grid.egon_etrago_link
1477
                WHERE scn_name = '{scn}'
1478
                AND carrier = '{corresponding_carriers[scn][key]}')
1479
            AND bus_id NOT IN
1480
                (SELECT bus1
1481
                FROM grid.egon_etrago_link
1482
                WHERE scn_name = '{scn}'
1483
                AND carrier = '{corresponding_carriers[scn][key]}')
1484
            ;
1485
            """,
1486
            warning=False,
1487
        )
1488
        if not isolated_gas_buses.empty:
1489
            logger.info(f"Isolated {key} buses:")
1490
            logger.info(isolated_gas_buses)
1491
1492
    # Deviation of the gas grid buses number
1493
    target_file = (
1494
        Path(".") / "datasets" / "gas_data" / "data" / "IGGIELGN_Nodes.csv"
1495
    )
1496
1497
    Grid_buses_list = pd.read_csv(
1498
        target_file,
1499
        delimiter=";",
1500
        decimal=".",
1501
        usecols=["country_code"],
1502
    )
1503
1504
    Grid_buses_list = Grid_buses_list[
1505
        Grid_buses_list["country_code"].str.match("DE")
1506
    ]
1507
    input_grid_buses = len(Grid_buses_list.index)
1508
1509
    for carrier in ["CH4", "H2_grid"]:
1510
        output_grid_buses_df = db.select_dataframe(
1511
            f"""
1512
            SELECT bus_id
1513
            FROM grid.egon_etrago_bus
1514
            WHERE scn_name = '{scn}'
1515
            AND country = 'DE'
1516
            AND carrier = '{carrier}';
1517
            """,
1518
            warning=False,
1519
        )
1520
        output_grid_buses = len(output_grid_buses_df.index)
1521
1522
        e_grid_buses = (
1523
            round(
1524
                (output_grid_buses - input_grid_buses) / input_grid_buses,
1525
                2,
1526
            )
1527
            * 100
1528
        )
1529
        logger.info(f"Deviation {carrier} buses: {e_grid_buses} %")
1530
1531
1532
def sanity_check_CH4_stores(scn):
1533
    """Execute sanity checks for the CH4 stores in Germany
1534
1535
    Returns print statements as sanity checks for the CH4 stores
1536
    capacity in Germany. The deviation is calculated between:
1537
      * the sum of the capacities of the stores with carrier 'CH4'
1538
        in the database (for one scenario) and
1539
      * the sum of:
1540
          * the capacity the gas grid allocated to CH4 (total capacity
1541
            in eGon2035 and capacity reduced the share of the grid
1542
            allocated to H2 in eGon100RE)
1543
          * the total capacity of the CH4 stores in Germany (source: GIE)
1544
1545
    Parameters
1546
    ----------
1547
    scn_name : str
1548
        Name of the scenario
1549
1550
    """
1551
    output_CH4_stores = db.select_dataframe(
1552
        f"""SELECT SUM(e_nom::numeric) as e_nom_germany
1553
                FROM grid.egon_etrago_store
1554
                WHERE scn_name = '{scn}'
1555
                AND carrier = 'CH4'
1556
                AND bus IN
1557
                    (SELECT bus_id
1558
                    FROM grid.egon_etrago_bus
1559
                    WHERE scn_name = '{scn}'
1560
                    AND country = 'DE'
1561
                    AND carrier = 'CH4');
1562
                """,
1563
        warning=False,
1564
    )["e_nom_germany"].values[0]
1565
1566
    if scn == "eGon2035":
1567
        grid_cap = 130000
1568
    elif scn == "eGon100RE":
1569
        grid_cap = 13000 * (
1570
            1
1571
            - get_sector_parameters("gas", "eGon100RE")[
1572
                "retrofitted_CH4pipeline-to-H2pipeline_share"
1573
            ]
1574
        )
1575
1576
    stores_cap_D = 266424202 # MWh GIE https://www.gie.eu/transparency/databases/storage-database/
1577
1578
    input_CH4_stores = stores_cap_D + grid_cap
0 ignored issues
show
introduced by
The variable grid_cap does not seem to be defined for all execution paths.
Loading history...
1579
1580
    e_CH4_stores = (
1581
        round(
1582
            (output_CH4_stores - input_CH4_stores) / input_CH4_stores,
1583
            2,
1584
        )
1585
        * 100
1586
    )
1587
    logger.info(f"Deviation CH4 stores: {e_CH4_stores} %")
1588
1589
1590
def sanity_check_H2_saltcavern_stores(scn):
1591
    """Execute sanity checks for the H2 saltcavern stores in Germany
1592
1593
    Returns print as sanity checks for the H2 saltcavern potential
1594
    storage capacity in Germany. The deviation is calculated between:
1595
      * the sum of the of the H2 saltcavern potential storage capacity
1596
        (e_nom_max) in the database and
1597
      * the sum of the H2 saltcavern potential storage capacity
1598
        assumed to be the ratio of the areas of 500 m radius around
1599
        substations in each german federal state and the estimated
1600
        total hydrogen storage potential of the corresponding federal
1601
        state (data from InSpEE-DS report).
1602
    This test works also in test mode.
1603
1604
    Parameters
1605
    ----------
1606
    scn_name : str
1607
        Name of the scenario
1608
1609
    """
1610
    output_H2_stores = db.select_dataframe(
1611
        f"""SELECT SUM(e_nom_max::numeric) as e_nom_max_germany
1612
                FROM grid.egon_etrago_store
1613
                WHERE scn_name = '{scn}'
1614
                AND carrier = 'H2_underground'
1615
                AND bus IN
1616
                    (SELECT bus_id
1617
                    FROM grid.egon_etrago_bus
1618
                    WHERE scn_name = '{scn}'
1619
                    AND country = 'DE'
1620
                    AND carrier = 'H2_saltcavern');
1621
                """,
1622
        warning=False,
1623
    )["e_nom_max_germany"].values[0]
1624
1625
    storage_potentials = calculate_and_map_saltcavern_storage_potential()
1626
    storage_potentials["storage_potential"] = (
1627
        storage_potentials["area_fraction"] * storage_potentials["potential"]
1628
    )
1629
    input_H2_stores = sum(storage_potentials["storage_potential"].to_list())
1630
1631
    e_H2_stores = (
1632
        round(
1633
            (output_H2_stores - input_H2_stores) / input_H2_stores,
1634
            2,
1635
        )
1636
        * 100
1637
    )
1638
    logger.info(f"Deviation H2 saltcavern stores: {e_H2_stores} %")
1639
1640
1641
def sanity_check_gas_one_port(scn):
1642
    """Check connections of gas one-port components
1643
1644
    Verify that gas one-port component (loads, generators, stores) are
1645
    all connected to a bus (of the right carrier) present in the data
1646
    base. Return print statements if this is not the case.
1647
    These sanity checks are not specific to Germany, they also include
1648
    the neighbouring countries.
1649
1650
    Parameters
1651
    ----------
1652
    scn_name : str
1653
        Name of the scenario
1654
1655
    """
1656
    if scn == "eGon2035":
1657
        # Loads
1658
        ## CH4_for_industry Germany
1659
        isolated_one_port_c = db.select_dataframe(
1660
            f"""
1661
            SELECT load_id, bus, carrier, scn_name
1662
                FROM grid.egon_etrago_load
1663
                WHERE scn_name = '{scn}'
1664
                AND carrier = 'CH4_for_industry'
1665
                AND bus NOT IN
1666
                    (SELECT bus_id
1667
                    FROM grid.egon_etrago_bus
1668
                    WHERE scn_name = '{scn}'
1669
                    AND country = 'DE'
1670
                    AND carrier = 'CH4')
1671
            ;
1672
            """,
1673
            warning=False,
1674
        )
1675
        if not isolated_one_port_c.empty:
1676
            logger.info("Isolated loads:")
1677
            logger.info(isolated_one_port_c)
1678
1679
        ## CH4_for_industry abroad
1680
        isolated_one_port_c = db.select_dataframe(
1681
            f"""
1682
            SELECT load_id, bus, carrier, scn_name
1683
                FROM grid.egon_etrago_load
1684
                WHERE scn_name = '{scn}'
1685
                AND carrier = 'CH4'
1686
                AND bus NOT IN
1687
                    (SELECT bus_id
1688
                    FROM grid.egon_etrago_bus
1689
                    WHERE scn_name = '{scn}'
1690
                    AND country != 'DE'
1691
                    AND carrier = 'CH4')
1692
            ;
1693
            """,
1694
            warning=False,
1695
        )
1696
        if not isolated_one_port_c.empty:
1697
            logger.info("Isolated loads:")
1698
            logger.info(isolated_one_port_c)
1699
1700
        ## H2_for_industry
1701
        isolated_one_port_c = db.select_dataframe(
1702
            f"""
1703
            SELECT load_id, bus, carrier, scn_name
1704
                FROM grid.egon_etrago_load
1705
                WHERE scn_name = '{scn}'
1706
                AND carrier = 'H2_for_industry'
1707
                AND (bus NOT IN
1708
                    (SELECT bus_id
1709
                    FROM grid.egon_etrago_bus
1710
                    WHERE scn_name = '{scn}'
1711
                    AND country = 'DE'
1712
                    AND carrier = 'H2_grid')
1713
                AND bus NOT IN
1714
                    (SELECT bus_id
1715
                    FROM grid.egon_etrago_bus
1716
                    WHERE scn_name = '{scn}'
1717
                    AND country != 'DE'
1718
                    AND carrier = 'AC'))
1719
            ;
1720
            """,
1721
            warning=False,
1722
        )
1723
        if not isolated_one_port_c.empty:
1724
            logger.info("Isolated loads:")
1725
            logger.info(isolated_one_port_c)
1726
1727
        # Genrators
1728
        isolated_one_port_c = db.select_dataframe(
1729
            f"""
1730
            SELECT generator_id, bus, carrier, scn_name
1731
                FROM grid.egon_etrago_generator
1732
                WHERE scn_name = '{scn}'
1733
                AND carrier = 'CH4'
1734
                AND bus NOT IN
1735
                    (SELECT bus_id
1736
                    FROM grid.egon_etrago_bus
1737
                    WHERE scn_name = '{scn}'
1738
                    AND carrier = 'CH4');
1739
            ;
1740
            """,
1741
            warning=False,
1742
        )
1743
        if not isolated_one_port_c.empty:
1744
            logger.info("Isolated generators:")
1745
            logger.info(isolated_one_port_c)
1746
1747
        # Stores
1748
        ## CH4 and H2_underground
1749
        corresponding_carriers = {
1750
            "CH4": "CH4",
1751
            "H2_saltcavern": "H2_underground",
1752
        }
1753
        for key in corresponding_carriers:
1754
            isolated_one_port_c = db.select_dataframe(
1755
                f"""
1756
                SELECT store_id, bus, carrier, scn_name
1757
                    FROM grid.egon_etrago_store
1758
                    WHERE scn_name = '{scn}'
1759
                    AND carrier = '{corresponding_carriers[key]}'
1760
                    AND bus NOT IN
1761
                        (SELECT bus_id
1762
                        FROM grid.egon_etrago_bus
1763
                        WHERE scn_name = '{scn}'
1764
                        AND carrier = '{key}')
1765
                ;
1766
                """,
1767
                warning=False,
1768
            )
1769
            if not isolated_one_port_c.empty:
1770
                logger.info("Isolated stores:")
1771
                logger.info(isolated_one_port_c)
1772
1773
        ## H2_overground
1774
        isolated_one_port_c = db.select_dataframe(
1775
            f"""
1776
            SELECT store_id, bus, carrier, scn_name
1777
                FROM grid.egon_etrago_store
1778
                WHERE scn_name = '{scn}'
1779
                AND carrier = 'H2_overground'
1780
                AND bus NOT IN
1781
                    (SELECT bus_id
1782
                    FROM grid.egon_etrago_bus
1783
                    WHERE scn_name = '{scn}'
1784
                    AND country = 'DE'
1785
                    AND carrier = 'H2_saltcavern')
1786
                AND bus NOT IN
1787
                    (SELECT bus_id
1788
                    FROM grid.egon_etrago_bus
1789
                    WHERE scn_name = '{scn}'
1790
                    AND country = 'DE'
1791
                    AND carrier = 'H2_grid')
1792
            ;
1793
            """,
1794
            warning=False,
1795
        )
1796
        if not isolated_one_port_c.empty:
1797
            logger.info("Isolated stores:")
1798
            logger.info(isolated_one_port_c)
1799
1800
    # elif scn == "eGon2035":
1801
1802
1803
def sanity_check_CH4_grid(scn):
1804
    """Execute sanity checks for the gas grid capacity in Germany
1805
1806
    Returns print statements as sanity checks for the CH4 links
1807
    (pipelines) in Germany. The deviation is calculated between
1808
    the sum of the power (p_nom) of all the CH4 pipelines in Germany
1809
    for one scenario in the database and the sum of the powers of the
1810
    imported pipelines.
1811
    In eGon100RE, the sum is reduced by the share of the grid that is
1812
    allocated to hydrogen (share calculated by PyPSA-eur-sec).
1813
    This test works also in test mode.
1814
1815
    Parameters
1816
    ----------
1817
    scn_name : str
1818
        Name of the scenario
1819
1820
    Returns
1821
    -------
1822
    scn_name : float
1823
        Sum of the power (p_nom) of all the pipelines in Germany
1824
1825
    """
1826
    grid_carrier = "CH4"
1827
    output_gas_grid = db.select_dataframe(
1828
        f"""SELECT SUM(p_nom::numeric) as p_nom_germany
1829
            FROM grid.egon_etrago_link
1830
            WHERE scn_name = '{scn}'
1831
            AND carrier = '{grid_carrier}'
1832
            AND bus0 IN
1833
                (SELECT bus_id
1834
                FROM grid.egon_etrago_bus
1835
                WHERE scn_name = '{scn}'
1836
                AND country = 'DE'
1837
                AND carrier = '{grid_carrier}')
1838
            AND bus1 IN
1839
                (SELECT bus_id
1840
                FROM grid.egon_etrago_bus
1841
                WHERE scn_name = '{scn}'
1842
                AND country = 'DE'
1843
                AND carrier = '{grid_carrier}')
1844
                ;
1845
            """,
1846
        warning=False,
1847
    )["p_nom_germany"].values[0]
1848
1849
    gas_nodes_list = define_gas_nodes_list()
1850
    abroad_gas_nodes_list = define_gas_buses_abroad()
1851
    gas_grid = define_gas_pipeline_list(gas_nodes_list, abroad_gas_nodes_list)
1852
    gas_grid_germany = gas_grid[
1853
        (gas_grid["country_0"] == "DE") & (gas_grid["country_1"] == "DE")
1854
    ]
1855
    p_nom_total = sum(gas_grid_germany["p_nom"].to_list())
1856
1857
    if scn == "eGon2035":
1858
        input_gas_grid = p_nom_total
1859
    if scn == "eGon100RE":
1860
        input_gas_grid = p_nom_total * (
1861
            1
1862
            - get_sector_parameters("gas", "eGon100RE")[
1863
                "retrofitted_CH4pipeline-to-H2pipeline_share"
1864
            ]
1865
        )
1866
1867
    e_gas_grid = (
1868
        round(
1869
            (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 1857 is False. Are you sure this can never be the case?
Loading history...
1870
            2,
1871
        )
1872
        * 100
1873
    )
1874
    logger.info(f"Deviation of the capacity of the CH4 grid: {e_gas_grid} %")
1875
1876
    return p_nom_total
1877
1878
1879
def sanity_check_gas_links(scn):
1880
    """Check connections of gas links
1881
1882
    Verify that gas links are all connected to buses present in the data
1883
    base. Return print statements if this is not the case.
1884
    This sanity check is not specific to Germany, it also includes
1885
    the neighbouring countries.
1886
1887
    Parameters
1888
    ----------
1889
    scn_name : str
1890
        Name of the scenario
1891
1892
    """
1893
    carriers = [
1894
        "CH4",
1895
        "H2_feedin",
1896
        "H2_to_CH4",
1897
        "CH4_to_H2",
1898
        "H2_to_power",
1899
        "power_to_H2",
1900
        "OCGT",
1901
        "central_gas_boiler",
1902
        "central_gas_CHP",
1903
        "central_gas_CHP_heat",
1904
        "industrial_gas_CHP",
1905
    ]
1906
    for c in carriers:
1907
        link_with_missing_bus = db.select_dataframe(
1908
            f"""
1909
            SELECT link_id, bus0, bus1, carrier, scn_name
1910
                FROM grid.egon_etrago_link
1911
                WHERE scn_name = '{scn}'
1912
                AND carrier = '{c}'
1913
                AND (bus0 NOT IN
1914
                    (SELECT bus_id
1915
                    FROM grid.egon_etrago_bus
1916
                    WHERE scn_name = '{scn}')
1917
                OR bus1 NOT IN
1918
                    (SELECT bus_id
1919
                    FROM grid.egon_etrago_bus
1920
                    WHERE scn_name = '{scn}'))
1921
            ;
1922
            """,
1923
            warning=False,
1924
        )
1925
        if not link_with_missing_bus.empty:
1926
            logger.info("Links with missing bus:")
1927
            logger.info(link_with_missing_bus)
1928
1929
1930
def etrago_eGon2035_gas_DE():
1931
    """Execute basic sanity checks for the gas sector in eGon2035
1932
1933
    Returns print statements as sanity checks for the gas sector in
1934
    the eGon2035 scenario for the following components in Germany:
1935
      * Buses: with the function :py:func:`sanity_check_gas_buses`
1936
      * Loads: for the carriers 'CH4_for_industry' and 'H2_for_industry'
1937
        the deviation is calculated between the sum of the loads in the
1938
        database and the sum the loads in the sources document
1939
        (opendata.ffe database)
1940
      * Generators: the deviation is calculated between the sums of the
1941
        nominal powers of the gas generators in the database and of
1942
        the ones in the sources document (Biogaspartner Einspeiseatlas
1943
        Deutschland from the dena and Productions from the SciGRID_gas
1944
        data)
1945
      * Stores: deviations for stores with following carriers are
1946
        calculated:
1947
          * 'CH4': with the function :py:func:`sanity_check_CH4_stores`
1948
          * 'H2_underground': with the function :py:func:`sanity_check_H2_saltcavern_stores`
1949
      * One-port components (loads, generators, stores): verification
1950
        that they are all connected to a bus present in the data base
1951
        with the function :py:func:`sanity_check_gas_one_port`
1952
      * Links: verification:
1953
          * that the gas links are all connected to buses present in
1954
            the data base with the function :py:func:`sanity_check_gas_links`
1955
          * of the capacity of the gas grid with the function
1956
            :py:func:`sanity_check_CH4_grid`
1957
1958
    """
1959
    scn = "eGon2035"
1960
1961
    if TESTMODE_OFF:
1962
        logger.info(f"Gas sanity checks for scenario {scn}")
1963
1964
        # Buses
1965
        sanity_check_gas_buses(scn)
1966
1967
        # Loads
1968
        logger.info("LOADS")
1969
1970
        path = Path(".") / "datasets" / "gas_data" / "demand"
1971
        corr_file = path / "region_corr.json"
1972
        df_corr = pd.read_json(corr_file)
1973
        df_corr = df_corr.loc[:, ["id_region", "name_short"]]
1974
        df_corr.set_index("id_region", inplace=True)
1975
1976
        for carrier in ["CH4_for_industry", "H2_for_industry"]:
1977
1978
            output_gas_demand = db.select_dataframe(
1979
                f"""SELECT (SUM(
1980
                    (SELECT SUM(p)
1981
                    FROM UNNEST(b.p_set) p))/1000000)::numeric as load_twh
1982
                    FROM grid.egon_etrago_load a
1983
                    JOIN grid.egon_etrago_load_timeseries b
1984
                    ON (a.load_id = b.load_id)
1985
                    JOIN grid.egon_etrago_bus c
1986
                    ON (a.bus=c.bus_id)
1987
                    AND b.scn_name = '{scn}'
1988
                    AND a.scn_name = '{scn}'
1989
                    AND c.scn_name = '{scn}'
1990
                    AND c.country = 'DE'
1991
                    AND a.carrier = '{carrier}';
1992
                """,
1993
                warning=False,
1994
            )["load_twh"].values[0]
1995
1996
            input_gas_demand = pd.read_json(
1997
                path / (carrier + "_eGon2035.json")
1998
            )
1999
            input_gas_demand = input_gas_demand.loc[:, ["id_region", "value"]]
2000
            input_gas_demand.set_index("id_region", inplace=True)
2001
            input_gas_demand = pd.concat(
2002
                [input_gas_demand, df_corr], axis=1, join="inner"
2003
            )
2004
            input_gas_demand["NUTS0"] = (input_gas_demand["name_short"].str)[
2005
                0:2
2006
            ]
2007
            input_gas_demand = input_gas_demand[
2008
                input_gas_demand["NUTS0"].str.match("DE")
2009
            ]
2010
            input_gas_demand = sum(input_gas_demand.value.to_list()) / 1000000
2011
2012
            e_demand = (
2013
                round(
2014
                    (output_gas_demand - input_gas_demand) / input_gas_demand,
2015
                    2,
2016
                )
2017
                * 100
2018
            )
2019
            logger.info(f"Deviation {carrier}: {e_demand} %")
2020
2021
        # Generators
2022
        logger.info("GENERATORS")
2023
        carrier_generator = "CH4"
2024
2025
        output_gas_generation = db.select_dataframe(
2026
            f"""SELECT SUM(p_nom::numeric) as p_nom_germany
2027
                    FROM grid.egon_etrago_generator
2028
                    WHERE scn_name = '{scn}'
2029
                    AND carrier = '{carrier_generator}'
2030
                    AND bus IN
2031
                        (SELECT bus_id
2032
                        FROM grid.egon_etrago_bus
2033
                        WHERE scn_name = '{scn}'
2034
                        AND country = 'DE'
2035
                        AND carrier = '{carrier_generator}');
2036
                    """,
2037
            warning=False,
2038
        )["p_nom_germany"].values[0]
2039
2040
        target_file = (
2041
            Path(".")
2042
            / "datasets"
2043
            / "gas_data"
2044
            / "data"
2045
            / "IGGIELGN_Productions.csv"
2046
        )
2047
2048
        NG_generators_list = pd.read_csv(
2049
            target_file,
2050
            delimiter=";",
2051
            decimal=".",
2052
            usecols=["country_code", "param"],
2053
        )
2054
2055
        NG_generators_list = NG_generators_list[
2056
            NG_generators_list["country_code"].str.match("DE")
2057
        ]
2058
2059
        p_NG = 0
2060
        for index, row in NG_generators_list.iterrows():
2061
            param = ast.literal_eval(row["param"])
2062
            p_NG = p_NG + param["max_supply_M_m3_per_d"]
2063
        conversion_factor = 437.5  # MCM/day to MWh/h
2064
        p_NG = p_NG * conversion_factor
2065
2066
        basename = "Biogaspartner_Einspeiseatlas_Deutschland_2021.xlsx"
2067
        target_file = Path(".") / "datasets" / "gas_data" / basename
2068
2069
        conversion_factor_b = 0.01083  # m^3/h to MWh/h
2070
        p_biogas = (
2071
            pd.read_excel(
2072
                target_file,
2073
                usecols=["Einspeisung Biomethan [(N*m^3)/h)]"],
2074
            )["Einspeisung Biomethan [(N*m^3)/h)]"].sum()
2075
            * conversion_factor_b
2076
        )
2077
2078
        input_gas_generation = p_NG + p_biogas
2079
        e_generation = (
2080
            round(
2081
                (output_gas_generation - input_gas_generation)
2082
                / input_gas_generation,
2083
                2,
2084
            )
2085
            * 100
2086
        )
2087
        logger.info(
2088
            f"Deviation {carrier_generator} generation: {e_generation} %"
2089
        )
2090
2091
        # Stores
2092
        logger.info("STORES")
2093
        sanity_check_CH4_stores(scn)
2094
        sanity_check_H2_saltcavern_stores(scn)
2095
2096
        # One-port components
2097
        sanity_check_gas_one_port(scn)
2098
2099
        # Links
2100
        logger.info("LINKS")
2101
        sanity_check_CH4_grid(scn)
2102
        sanity_check_gas_links(scn)
2103
2104
    else:
2105
        print("Testmode is on, skipping sanity check.")
2106
2107
2108
def etrago_eGon2035_gas_abroad():
2109
    """Execute basic sanity checks for the gas sector in eGon2035 abroad
2110
2111
    Returns print statements as sanity checks for the gas sector in
2112
    the eGon2035 scenario for the following components in Germany:
2113
      * Buses
2114
      * Loads: for the carriers 'CH4' and 'H2_for_industry'
2115
        the deviation is calculated between the sum of the loads in the
2116
        database and the sum in the sources document (TYNDP)
2117
      * Generators: the deviation is calculated between the sums of the
2118
        nominal powers of the methane generators abroad in the database
2119
        and of the ones in the sources document (TYNDP)
2120
      * Stores: the deviation for methane stores abroad is calculated
2121
        between the sum of the capacities in the data base and the one
2122
        of the source document (SciGRID_gas data)
2123
      * Links: verification of the capacity of the crossbordering gas
2124
        grid pipelines.
2125
2126
    """
2127
    scn = "eGon2035"
2128
2129
    if TESTMODE_OFF:
2130
        logger.info(f"Gas sanity checks abroad for scenario {scn}")
2131
2132
        # Buses
2133
        logger.info("BUSES")
2134
2135
        # Are gas buses isolated?
2136
        corresponding_carriers = {
2137
            "eGon2035": {
2138
                "CH4": "CH4",
2139
            },
2140
            # "eGon100RE": {
2141
            #     "CH4": "CH4",
2142
            #     "H2_grid": "H2_retrofit",
2143
            # }
2144
        }
2145
        for key in corresponding_carriers[scn]:
2146
            isolated_gas_buses_abroad = db.select_dataframe(
2147
                f"""
2148
                SELECT bus_id, carrier, country
2149
                FROM grid.egon_etrago_bus
2150
                WHERE scn_name = '{scn}'
2151
                AND carrier = '{key}'
2152
                AND country != 'DE'
2153
                AND bus_id NOT IN
2154
                    (SELECT bus0
2155
                    FROM grid.egon_etrago_link
2156
                    WHERE scn_name = '{scn}'
2157
                    AND carrier = '{corresponding_carriers[scn][key]}')
2158
                AND bus_id NOT IN
2159
                    (SELECT bus1
2160
                    FROM grid.egon_etrago_link
2161
                    WHERE scn_name = '{scn}'
2162
                    AND carrier = '{corresponding_carriers[scn][key]}')
2163
                ;
2164
                """,
2165
                warning=False,
2166
            )
2167
            if not isolated_gas_buses_abroad.empty:
2168
                logger.info(f"Isolated {key} buses abroad:")
2169
                logger.info(isolated_gas_buses_abroad)
2170
2171
        # Loads
2172
        logger.info("LOADS")
2173
2174
        (
2175
            Norway_global_demand_1y,
2176
            normalized_ch4_demandTS,
2177
        ) = import_ch4_demandTS()
2178
        input_CH4_demand_abroad = calc_global_ch4_demand(
2179
            Norway_global_demand_1y
2180
        )
2181
        input_CH4_demand = input_CH4_demand_abroad["GlobD_2035"].sum()
2182
2183
        ## CH4
2184
        output_CH4_demand = db.select_dataframe(
2185
            f"""SELECT (SUM(
2186
                (SELECT SUM(p)
2187
                FROM UNNEST(b.p_set) p)))::numeric as load_mwh
2188
                FROM grid.egon_etrago_load a
2189
                JOIN grid.egon_etrago_load_timeseries b
2190
                ON (a.load_id = b.load_id)
2191
                JOIN grid.egon_etrago_bus c
2192
                ON (a.bus=c.bus_id)
2193
                AND b.scn_name = '{scn}'
2194
                AND a.scn_name = '{scn}'
2195
                AND c.scn_name = '{scn}'
2196
                AND c.country != 'DE'
2197
                AND a.carrier = 'CH4';
2198
            """,
2199
            warning=False,
2200
        )["load_mwh"].values[0]
2201
2202
        e_demand_CH4 = (
2203
            round(
2204
                (output_CH4_demand - input_CH4_demand) / input_CH4_demand,
2205
                2,
2206
            )
2207
            * 100
2208
        )
2209
        logger.info(f"Deviation CH4 load: {e_demand_CH4} %")
2210
2211
        ## H2_for_industry
2212
        input_power_to_h2_demand_abroad = calc_global_power_to_h2_demand()
2213
        input_H2_demand = input_power_to_h2_demand_abroad["GlobD_2035"].sum()
2214
2215
        output_H2_demand = db.select_dataframe(
2216
            f"""SELECT SUM(p_set::numeric) as p_set_abroad
2217
                    FROM grid.egon_etrago_load
2218
                    WHERE scn_name = '{scn}'
2219
                    AND carrier = 'H2_for_industry'
2220
                    AND bus IN
2221
                        (SELECT bus_id
2222
                        FROM grid.egon_etrago_bus
2223
                        WHERE scn_name = '{scn}'
2224
                        AND country != 'DE'
2225
                        AND carrier = 'AC');
2226
                    """,
2227
            warning=False,
2228
        )["p_set_abroad"].values[0]
2229
2230
        e_demand_H2 = (
2231
            round(
2232
                (output_H2_demand - input_H2_demand) / input_H2_demand,
2233
                2,
2234
            )
2235
            * 100
2236
        )
2237
        logger.info(f"Deviation H2_for_industry load: {e_demand_H2} %")
2238
2239
        # Generators
2240
        logger.info("GENERATORS ")
2241
        CH4_gen = calc_capacities()
2242
        input_CH4_gen = CH4_gen["cap_2035"].sum()
2243
2244
        output_CH4_gen = db.select_dataframe(
2245
            f"""SELECT SUM(p_nom::numeric) as p_nom_abroad
2246
                    FROM grid.egon_etrago_generator
2247
                    WHERE scn_name = '{scn}'
2248
                    AND carrier = 'CH4'
2249
                    AND bus IN
2250
                        (SELECT bus_id
2251
                        FROM grid.egon_etrago_bus
2252
                        WHERE scn_name = '{scn}'
2253
                        AND country != 'DE'
2254
                        AND carrier = 'CH4');
2255
                    """,
2256
            warning=False,
2257
        )["p_nom_abroad"].values[0]
2258
2259
        e_gen = (
2260
            round(
2261
                (output_CH4_gen - input_CH4_gen) / input_CH4_gen,
2262
                2,
2263
            )
2264
            * 100
2265
        )
2266
        logger.info(f"Deviation CH4 generators: {e_gen} %")
2267
2268
        # Stores
2269
        logger.info("STORES")
2270
        ch4_input_capacities = calc_ch4_storage_capacities()
2271
        input_CH4_stores = ch4_input_capacities["e_nom"].sum()
2272
2273
        output_CH4_stores = db.select_dataframe(
2274
            f"""SELECT SUM(e_nom::numeric) as e_nom_abroad
2275
                    FROM grid.egon_etrago_store
2276
                    WHERE scn_name = '{scn}'
2277
                    AND carrier = 'CH4'
2278
                    AND bus IN
2279
                        (SELECT bus_id
2280
                        FROM grid.egon_etrago_bus
2281
                        WHERE scn_name = '{scn}'
2282
                        AND country != 'DE'
2283
                        AND carrier = 'CH4');
2284
                    """,
2285
            warning=False,
2286
        )["e_nom_abroad"].values[0]
2287
2288
        e_stores = (
2289
            round(
2290
                (output_CH4_stores - input_CH4_stores) / input_CH4_stores,
2291
                2,
2292
            )
2293
            * 100
2294
        )
2295
        logger.info(f"Deviation CH4 stores: {e_stores} %")
2296
2297
        # Links
2298
        logger.info("LINKS")
2299
        ch4_grid_input_capacities = calculate_ch4_grid_capacities()
2300
        input_CH4_grid = ch4_grid_input_capacities["p_nom"].sum()
2301
2302
        grid_carrier = "CH4"
2303
        output_gas_grid = db.select_dataframe(
2304
            f"""SELECT SUM(p_nom::numeric) as p_nom
2305
            FROM grid.egon_etrago_link
2306
            WHERE scn_name = '{scn}'
2307
            AND carrier = '{grid_carrier}'
2308
            AND (bus0 IN
2309
                (SELECT bus_id
2310
                FROM grid.egon_etrago_bus
2311
                WHERE scn_name = '{scn}'
2312
                AND country != 'DE'
2313
                AND carrier = '{grid_carrier}')
2314
            OR bus1 IN
2315
                (SELECT bus_id
2316
                FROM grid.egon_etrago_bus
2317
                WHERE scn_name = '{scn}'
2318
                AND country != 'DE'
2319
                AND carrier = '{grid_carrier}'))
2320
                ;
2321
            """,
2322
            warning=False,
2323
        )["p_nom"].values[0]
2324
2325
        e_gas_grid = (
2326
            round(
2327
                (output_gas_grid - input_CH4_grid) / input_CH4_grid,
2328
                2,
2329
            )
2330
            * 100
2331
        )
2332
        logger.info(
2333
            f"Deviation of the capacity of the crossbordering CH4 grid: {e_gas_grid} %"
2334
        )
2335
2336
    else:
2337
        print("Testmode is on, skipping sanity check.")
2338
2339
2340
def sanitycheck_dsm():
2341
    def df_from_series(s: pd.Series):
2342
        return pd.DataFrame.from_dict(dict(zip(s.index, s.values)))
2343
2344
    for scenario in ["eGon2035", "eGon100RE"]:
2345
        # p_min and p_max
2346
        sql = f"""
2347
        SELECT link_id, bus0 as bus, p_nom FROM grid.egon_etrago_link
2348
        WHERE carrier = 'dsm'
2349
        AND scn_name = '{scenario}'
2350
        ORDER BY link_id
2351
        """
2352
2353
        meta_df = db.select_dataframe(sql, index_col="link_id")
2354
        link_ids = str(meta_df.index.tolist())[1:-1]
2355
2356
        sql = f"""
2357
        SELECT link_id, p_min_pu, p_max_pu
2358
        FROM grid.egon_etrago_link_timeseries
2359
        WHERE scn_name = '{scenario}'
2360
        AND link_id IN ({link_ids})
2361
        ORDER BY link_id
2362
        """
2363
2364
        ts_df = db.select_dataframe(sql, index_col="link_id")
2365
2366
        p_max_df = df_from_series(ts_df.p_max_pu).mul(meta_df.p_nom)
2367
        p_min_df = df_from_series(ts_df.p_min_pu).mul(meta_df.p_nom)
2368
2369
        p_max_df.columns = meta_df.bus.tolist()
2370
        p_min_df.columns = meta_df.bus.tolist()
2371
2372
        targets = config.datasets()["DSM_CTS_industry"]["targets"]
2373
2374
        tables = [
2375
            "cts_loadcurves_dsm",
2376
            "ind_osm_loadcurves_individual_dsm",
2377
            "demandregio_ind_sites_dsm",
2378
            "ind_sites_loadcurves_individual",
2379
        ]
2380
2381
        df_list = []
2382
2383
        for table in tables:
2384
            target = targets[table]
2385
            sql = f"""
2386
            SELECT bus, p_min, p_max, e_max, e_min
2387
            FROM {target["schema"]}.{target["table"]}
2388
            WHERE scn_name = '{scenario}'
2389
            ORDER BY bus
2390
            """
2391
2392
            df_list.append(db.select_dataframe(sql))
2393
2394
        individual_ts_df = pd.concat(df_list, ignore_index=True)
2395
2396
        groups = individual_ts_df[["bus"]].reset_index().groupby("bus").groups
2397
2398
        individual_p_max_df = df_from_series(individual_ts_df.p_max)
2399
2400
        individual_p_max_df = pd.DataFrame(
2401
            [
2402
                individual_p_max_df[idxs].sum(axis=1)
2403
                for idxs in groups.values()
2404
            ],
2405
            index=groups.keys(),
2406
        ).T
2407
2408
        individual_p_min_df = df_from_series(individual_ts_df.p_min)
2409
2410
        individual_p_min_df = pd.DataFrame(
2411
            [
2412
                individual_p_min_df[idxs].sum(axis=1)
2413
                for idxs in groups.values()
2414
            ],
2415
            index=groups.keys(),
2416
        ).T
2417
2418
        # due to the fact that time series are clipped at zero (either
2419
        # direction) there is a little difference between the sum of the
2420
        # individual time series and the aggregated time series as the second
2421
        # is generated independent of the others. This makes atol=1e-01
2422
        # necessary.
2423
        atol = 1e-01
2424
        assert np.allclose(p_max_df, individual_p_max_df, atol=atol)
2425
        assert np.allclose(p_min_df, individual_p_min_df, atol=atol)
2426
2427
        # e_min and e_max
2428
        sql = f"""
2429
        SELECT store_id, bus, e_nom FROM grid.egon_etrago_store
2430
        WHERE carrier = 'dsm'
2431
        AND scn_name = '{scenario}'
2432
        ORDER BY store_id
2433
        """
2434
2435
        meta_df = db.select_dataframe(sql, index_col="store_id")
2436
        store_ids = str(meta_df.index.tolist())[1:-1]
2437
2438
        sql = f"""
2439
        SELECT store_id, e_min_pu, e_max_pu
2440
        FROM grid.egon_etrago_store_timeseries
2441
        WHERE scn_name = '{scenario}'
2442
        AND store_id IN ({store_ids})
2443
        ORDER BY store_id
2444
        """
2445
2446
        ts_df = db.select_dataframe(sql, index_col="store_id")
2447
2448
        e_max_df = df_from_series(ts_df.e_max_pu).mul(meta_df.e_nom)
2449
        e_min_df = df_from_series(ts_df.e_min_pu).mul(meta_df.e_nom)
2450
2451
        e_max_df.columns = meta_df.bus.tolist()
2452
        e_min_df.columns = meta_df.bus.tolist()
2453
2454
        individual_e_max_df = df_from_series(individual_ts_df.e_max)
2455
2456
        individual_e_max_df = pd.DataFrame(
2457
            [
2458
                individual_e_max_df[idxs].sum(axis=1)
2459
                for idxs in groups.values()
2460
            ],
2461
            index=groups.keys(),
2462
        ).T
2463
        individual_e_min_df = df_from_series(individual_ts_df.e_min)
2464
2465
        individual_e_min_df = pd.DataFrame(
2466
            [
2467
                individual_e_min_df[idxs].sum(axis=1)
2468
                for idxs in groups.values()
2469
            ],
2470
            index=groups.keys(),
2471
        ).T
2472
2473
        assert np.allclose(e_max_df, individual_e_max_df)
2474
        assert np.allclose(e_min_df, individual_e_min_df)
2475