Passed
Pull Request — dev (#1068)
by
unknown
02:05
created

cts_heat_demand_share()   A

Complexity

Conditions 2

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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