Passed
Pull Request — dev (#1138)
by
unknown
02:16
created

motorized_individual_travel.ev_allocation   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 638
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 34
eloc 351
dl 0
loc 638
rs 9.68
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
B data.datasets.emobility.motorized_individual_travel.ev_allocation.calc_evs_per_municipality() 0 75 4
C data.datasets.emobility.motorized_individual_travel.ev_allocation.fix_missing_ags_municipality_regiostar() 0 77 11
A data.datasets.emobility.motorized_individual_travel.ev_allocation.calc_evs_per_reg_district() 0 66 2
B data.datasets.emobility.motorized_individual_travel.ev_allocation.allocate_evs_numbers() 0 116 6
C data.datasets.emobility.motorized_individual_travel.ev_allocation.allocate_evs_to_grid_districts() 0 117 7
B data.datasets.emobility.motorized_individual_travel.ev_allocation.calc_evs_per_grid_district() 0 130 4
1
"""
2
* Calculate number of electric vehicles and allocate on different spatial
3
levels: :func:`allocate_evs_numbers`
4
* Allocate specific EVs to MV grid districts:
5
:func:`allocate_evs_to_grid_districts`
6
"""
7
8
from itertools import permutations
9
10
from sqlalchemy.sql import func
11
import numpy as np
12
import pandas as pd
13
14
from egon.data import db
15
from egon.data.datasets.emobility.motorized_individual_travel.db_classes import (
16
    EgonEvCountMunicipality,
17
    EgonEvCountMvGridDistrict,
18
    EgonEvCountRegistrationDistrict,
19
    EgonEvMvGridDistrict,
20
    EgonEvPool,
21
)
22
from egon.data.datasets.emobility.motorized_individual_travel.helpers import (
23
    COLUMNS_KBA,
24
    CONFIG_EV,
25
    TESTMODE_OFF,
26
    read_kba_data,
27
    read_rs7_data,
28
)
29
from egon.data.datasets.emobility.motorized_individual_travel.tests import (
30
    validate_electric_vehicles_numbers,
31
)
32
from egon.data.datasets.mv_grid_districts import MvGridDistricts
33
from egon.data.datasets.scenario_parameters import get_sector_parameters
34
from egon.data.datasets.zensus_mv_grid_districts import MapZensusGridDistricts
35
from egon.data.datasets.zensus_vg250 import (
36
    DestatisZensusPopulationPerHaInsideGermany,
37
    MapZensusVg250,
38
    Vg250Gem,
39
    Vg250GemPopulation,
40
)
41
import egon.data.config
42
43
RANDOM_SEED = egon.data.config.settings()["egon-data"]["--random-seed"]
44
45
46
def fix_missing_ags_municipality_regiostar(muns, rs7_data):
47
    """Check if all AGS of municipality dataset are included in RegioStaR7
48
    dataset and vice versa.
49
50
    As of Dec 2021, some municipalities are not included int he RegioStaR7
51
    dataset. This is mostly caused by incorporations of a municipality by
52
    another municipality. This is fixed by assining a RS7 id from another
53
    municipality with similar AGS (most likely a neighboured one).
54
55
    Missing entries in the municipality dataset is printed but not fixed
56
    as it doesn't result in bad data. Nevertheless, consider to update the
57
    municipality/VG250 dataset.
58
59
    Parameters
60
    ----------
61
    muns : pandas.DataFrame
62
        Municipality data
63
    rs7_data : pandas.DataFrame
64
        RegioStaR7 data
65
66
    Returns
67
    -------
68
    pandas.DataFrame
69
        Fixed RegioStaR7 data
70
    """
71
72
    if len(muns.ags.unique()) != len(rs7_data.ags):
73
        print(
74
            "==========> Number of AGS differ between VG250 and RS7, "
75
            "trying to fix this..."
76
        )
77
78
        # Get AGS differences
79
        ags_datasets = {"RS7": rs7_data.ags, "VG250": muns.ags}
80
        ags_datasets_missing = {k: [] for k in ags_datasets.keys()}
81
        perm = permutations(ags_datasets.items())
82
        for (name1, ags1), (name2, ags2) in perm:
83
            print(f"  Checking if all AGS of {name1} are in {name2}...")
84
            missing = [_ for _ in ags1 if _ not in ags2.to_list()]
85
            if len(missing) > 0:
86
                ags_datasets_missing[name2] = missing
87
                print(f"    AGS in {name1} but not in {name2}: ", missing)
88
            else:
89
                print("    OK")
90
91
        print("")
92
93
        # Try to fix
94
        for name, missing in ags_datasets_missing.items():
95
            if len(missing) > 0:
96
                # RS7 entries missing: use RS7 number from mun with similar AGS
97
                if name == "RS7":
98
                    for ags in missing:
99
                        similar_entry = rs7_data[
100
                            round((rs7_data.ags.div(10))) == round(ags / 10)
101
                        ].iloc[0]
102
                        if len(similar_entry) > 0:
103
                            print(
104
                                f"Adding dataset from VG250 to RS7 "
105
                                f"based upon AGS {ags}."
106
                            )
107
                            similar_entry.ags = ags
108
                            rs7_data = rs7_data.append(similar_entry)
109
                        print("Consider to update RS7.")
110
                # VG250 entries missing:
111
                elif name == "VG250":
112
                    print(
113
                        "Cannot guess VG250 entries. This error does not "
114
                        "result in bad data but consider to update VG250."
115
                    )
116
117
        if len(muns.ags.unique()) != len(rs7_data.ags):
118
            print("==========> AGS could not be fixed!")
119
        else:
120
            print("==========> AGS were fixed!")
121
122
    return rs7_data
123
124
125
def calc_evs_per_reg_district(scenario_variation_parameters, kba_data):
126
    """Calculate EVs per registration district
127
128
    Parameters
129
    ----------
130
    scenario_variation_parameters : dict
131
        Parameters of scenario variation
132
    kba_data : pandas.DataFrame
133
        Vehicle registration data for registration district
134
135
    Returns
136
    -------
137
    pandas.DataFrame
138
        EVs per registration district
139
    """
140
141
    scenario_variation_parameters["mini_share"] = (
142
        scenario_variation_parameters["bev_mini_share"]
143
        + scenario_variation_parameters["phev_mini_share"]
144
    )
145
    scenario_variation_parameters["medium_share"] = (
146
        scenario_variation_parameters["bev_medium_share"]
147
        + scenario_variation_parameters["phev_medium_share"]
148
    )
149
    scenario_variation_parameters["luxury_share"] = (
150
        scenario_variation_parameters["bev_luxury_share"]
151
        + scenario_variation_parameters["phev_luxury_share"]
152
    )
153
154
    factor_dict = dict()
155
    factor_dict["mini_factor"] = (
156
        scenario_variation_parameters["mini_share"]
157
        * scenario_variation_parameters["ev_count"]
158
        / kba_data.mini.sum()
159
    )
160
    factor_dict["medium_factor"] = (
161
        scenario_variation_parameters["medium_share"]
162
        * scenario_variation_parameters["ev_count"]
163
        / kba_data.medium.sum()
164
    )
165
    factor_dict["luxury_factor"] = (
166
        scenario_variation_parameters["luxury_share"]
167
        * scenario_variation_parameters["ev_count"]
168
        / kba_data.luxury.sum()
169
    )
170
171
    # Define shares and factors
172
    ev_data = kba_data.copy()
173
174
    for tech, params in CONFIG_EV.items():
175
        ev_data[tech] = (
176
            (
177
                kba_data[params["column"]]
178
                * factor_dict[params["factor"]]
179
                * scenario_variation_parameters[params["tech_share"]]
180
                / scenario_variation_parameters[params["share"]]
181
            )
182
            .round()
183
            .astype("int")
184
        )
185
186
    ev_data.drop(
187
        columns=[_ for _ in COLUMNS_KBA if _ != "reg_district"], inplace=True
188
    )
189
190
    return ev_data
191
192
193
def calc_evs_per_municipality(ev_data, rs7_data):
194
    """Calculate EVs per municipality
195
196
    Parameters
197
    ----------
198
    ev_data : pandas.DataFrame
199
        EVs per regstration district
200
    rs7_data : pandas.DataFrame
201
        RegioStaR7 data
202
    """
203
    with db.session_scope() as session:
204
        query = session.query(
205
            Vg250GemPopulation.ags_0.label("ags"),
206
            Vg250GemPopulation.gen,
207
            Vg250GemPopulation.population_total.label("pop"),
208
        )
209
210
    muns = pd.read_sql(
211
        query.statement, query.session.bind, index_col=None
212
    ).astype({"ags": "int64"})
213
214
    muns["ags_district"] = (
215
        muns.ags.multiply(1 / 1000).apply(np.floor).astype("int")
216
    )
217
218
    # Manual fix of Trier-Saarburg: Introduce new `ags_reg_district`
219
    # for correct allocation of mun to registration district
220
    # (Zulassungsbezirk), see above for background.
221
    muns["ags_reg_district"] = muns["ags_district"]
222
    muns.loc[muns["ags_reg_district"] == 7235, "ags_reg_district"] = 7211
223
224
    # Remove multiple municipality entries (due to 'gf' in VG250)
225
    # by summing up population
226
    muns = (
227
        muns[["ags", "gen", "ags_reg_district", "pop"]]
228
        .groupby(["ags", "gen", "ags_reg_district"])
229
        .sum()
230
        .reset_index()
231
    )
232
233
    # Add population of registration district
234
    pop_per_reg_district = (
235
        muns[["ags_reg_district", "pop"]]
236
        .groupby("ags_reg_district")
237
        .sum()
238
        .rename(columns={"pop": "pop_district"})
239
        .reset_index()
240
    )
241
242
    # Fix missing ags in mun data if not in testmode
243
    if TESTMODE_OFF:
244
        rs7_data = fix_missing_ags_municipality_regiostar(muns, rs7_data)
245
246
    # Merge municipality, EV data and pop per district
247
    ev_data_muns = muns.merge(ev_data, on="ags_reg_district").merge(
248
        pop_per_reg_district, on="ags_reg_district"
249
    )
250
251
    # Disaggregate EV numbers to municipality
252
    for tech in ev_data[CONFIG_EV.keys()]:
253
        ev_data_muns[tech] = round(
254
            ev_data_muns[tech]
255
            * ev_data_muns["pop"]
256
            / ev_data_muns["pop_district"]
257
        ).astype("int")
258
259
    # Filter columns
260
    cols = ["ags"]
261
    cols.extend(CONFIG_EV.keys())
262
    ev_data_muns = ev_data_muns[cols]
263
264
    # Merge RS7 data
265
    ev_data_muns = ev_data_muns.merge(rs7_data[["ags", "rs7_id"]], on="ags")
266
267
    return ev_data_muns
268
269
270
def calc_evs_per_grid_district(ev_data_muns):
271
    """Calculate EVs per grid district by using population weighting
272
273
    Parameters
274
    ----------
275
    ev_data_muns : pandas.DataFrame
276
        EV data for municipalities
277
278
    Returns
279
    -------
280
    pandas.DataFrame
281
        EV data for grid districts
282
    """
283
284
    # Read MVGDs with intersecting muns and aggregate pop for each
285
    # municipality part
286
    with db.session_scope() as session:
287
        query_pop_per_mvgd = (
288
            session.query(
289
                MvGridDistricts.bus_id,
290
                Vg250Gem.ags,
291
                func.sum(
292
                    DestatisZensusPopulationPerHaInsideGermany.population
293
                ).label("pop"),
294
            )
295
            .select_from(MapZensusGridDistricts)
296
            .join(
297
                MvGridDistricts,
298
                MapZensusGridDistricts.bus_id == MvGridDistricts.bus_id,
299
            )
300
            .join(
301
                DestatisZensusPopulationPerHaInsideGermany,
302
                MapZensusGridDistricts.zensus_population_id
303
                == DestatisZensusPopulationPerHaInsideGermany.id,
304
            )
305
            .join(
306
                MapZensusVg250,
307
                MapZensusGridDistricts.zensus_population_id
308
                == MapZensusVg250.zensus_population_id,
309
            )
310
            .join(
311
                Vg250Gem, MapZensusVg250.vg250_municipality_id == Vg250Gem.id
312
            )
313
            .group_by(MvGridDistricts.bus_id, Vg250Gem.ags)
314
            .order_by(Vg250Gem.ags)
315
        )
316
317
    mvgd_pop_per_mun = pd.read_sql(
318
        query_pop_per_mvgd.statement,
319
        query_pop_per_mvgd.session.bind,
320
        index_col=None,
321
    ).astype({"bus_id": "int64", "pop": "int64", "ags": "int64"})
322
323
    # Calc population share of each municipality in MVGD
324
    mvgd_pop_per_mun_in_mvgd = mvgd_pop_per_mun.groupby(["bus_id", "ags"]).agg(
325
        {"pop": "sum"}
326
    )
327
328
    # Calc relative and absolute population shares:
329
    # * pop_mun_in_mvgd: pop share of mun which intersects with MVGD
330
    # * pop_share_mun_in_mvgd: relative pop share of mun which
331
    #   intersects with MVGD
332
    # * pop_mun_total: total pop of mun
333
    # * pop_mun_in_mvgd_of_mun_total: relative pop share of mun which
334
    #   intersects with MVGD in relation to total pop of mun
335
    mvgd_pop_per_mun_in_mvgd = (
336
        mvgd_pop_per_mun_in_mvgd.groupby(level=0)
337
        .apply(lambda x: x / float(x.sum()))
338
        .reset_index()
339
        .rename(columns={"pop": "pop_share_mun_in_mvgd"})
340
        .merge(
341
            mvgd_pop_per_mun_in_mvgd.reset_index(),
342
            on=["bus_id", "ags"],
343
            how="left",
344
        )
345
        .rename(columns={"pop": "pop_mun_in_mvgd"})
346
        .merge(
347
            mvgd_pop_per_mun[["ags", "pop"]]
348
            .groupby("ags")
349
            .agg({"pop": "sum"}),
350
            on="ags",
351
            how="left",
352
        )
353
        .rename(columns={"pop": "pop_mun_total"})
354
    )
355
    mvgd_pop_per_mun_in_mvgd["pop_mun_in_mvgd_of_mun_total"] = (
356
        mvgd_pop_per_mun_in_mvgd["pop_mun_in_mvgd"]
357
        / mvgd_pop_per_mun_in_mvgd["pop_mun_total"]
358
    )
359
360
    # Merge EV data
361
    ev_data_mvgds = mvgd_pop_per_mun_in_mvgd.merge(
362
        ev_data_muns, on="ags", how="left"
363
    ).sort_values(["bus_id", "ags"])
364
365
    # Calc EVs per MVGD by using EV from mun and share of mun's pop
366
    # that is located within MVGD
367
    for tech in ev_data_mvgds[CONFIG_EV.keys()]:
368
        ev_data_mvgds[tech] = (
369
            round(
370
                ev_data_mvgds[tech]
371
                * ev_data_mvgds["pop_mun_in_mvgd_of_mun_total"]
372
            )
373
            .fillna(0)
374
            .astype("int")
375
        )
376
377
    # Set RS7 id for MVGD by using the RS7 id from the mun with the
378
    # highest share in population
379
    rs7_data_mvgds = (
380
        ev_data_mvgds[["bus_id", "pop_mun_in_mvgd", "rs7_id"]]
381
        .groupby(["bus_id", "rs7_id"])
382
        .sum()
383
        .sort_values(
384
            ["bus_id", "pop_mun_in_mvgd"], ascending=False, na_position="last"
385
        )
386
        .reset_index()
387
        .drop_duplicates("bus_id", keep="first")[["bus_id", "rs7_id"]]
388
    )
389
390
    # Join RS7 id and select columns
391
    columns = ["bus_id"] + [_ for _ in CONFIG_EV.keys()]
392
    ev_data_mvgds = (
393
        ev_data_mvgds[columns]
394
        .groupby("bus_id")
395
        .agg("sum")
396
        .merge(rs7_data_mvgds, on="bus_id", how="left")
397
    )
398
399
    return ev_data_mvgds
400
401
402
def allocate_evs_numbers():
403
    """Allocate electric vehicles to different spatial levels.
404
405
    Accocation uses today's vehicles registration data per registration
406
    district from KBA and scales scenario's EV targets (BEV and PHEV)
407
    linearly using population. Furthermore, a RegioStaR7 code (BMVI) is
408
    assigned.
409
410
    Levels:
411
    * districts of registration
412
    * municipalities
413
    * grid districts
414
415
    Parameters
416
    ----------
417
418
    Returns
419
    -------
420
421
    """
422
    # Import
423
    kba_data = read_kba_data()
424
    rs7_data = read_rs7_data()
425
426
    for scenario_name in ["eGon2035", "eGon100RE"]:
427
        # Load scenario params
428
        scenario_parameters = get_sector_parameters(
429
            "mobility", scenario=scenario_name
430
        )["motorized_individual_travel"]
431
432
        print(f"SCENARIO: {scenario_name}")
433
434
        # Go through scenario variations
435
        for (
436
            scenario_variation_name,
437
            scenario_variation_parameters,
438
        ) in scenario_parameters.items():
439
440
            print(f"  SCENARIO VARIATION: {scenario_variation_name}")
441
442
            # Get EV target
443
            ev_target = scenario_variation_parameters["ev_count"]
444
445
            #####################################
446
            # EV data per registration district #
447
            #####################################
448
            print("Calculate EV numbers for registration districts...")
449
            ev_data = calc_evs_per_reg_district(
450
                scenario_variation_parameters, kba_data
451
            )
452
            # Check EV results if not in testmode
453
            if TESTMODE_OFF:
454
                validate_electric_vehicles_numbers(
455
                    "EV count in registration districts", ev_data, ev_target
456
                )
457
            # Add scenario columns and write to DB
458
            ev_data["scenario"] = scenario_name
459
            ev_data["scenario_variation"] = scenario_variation_name
460
            ev_data.sort_values(
461
                ["scenario", "scenario_variation", "ags_reg_district"],
462
                inplace=True,
463
            )
464
            ev_data.to_sql(
465
                name=EgonEvCountRegistrationDistrict.__table__.name,
466
                schema=EgonEvCountRegistrationDistrict.__table__.schema,
467
                con=db.engine(),
468
                if_exists="append",
469
                index=False,
470
            )
471
472
            #####################################
473
            #     EV data per municipality      #
474
            #####################################
475
            print("Calculate EV numbers for municipalities...")
476
            ev_data_muns = calc_evs_per_municipality(ev_data, rs7_data)
477
            # Check EV results if not in testmode
478
            if TESTMODE_OFF:
479
                validate_electric_vehicles_numbers(
480
                    "EV count in municipalities", ev_data_muns, ev_target
481
                )
482
            # Add scenario columns and write to DB
483
            ev_data_muns["scenario"] = scenario_name
484
            ev_data_muns["scenario_variation"] = scenario_variation_name
485
            ev_data_muns.sort_values(
486
                ["scenario", "scenario_variation", "ags"], inplace=True
487
            )
488
            ev_data_muns.to_sql(
489
                name=EgonEvCountMunicipality.__table__.name,
490
                schema=EgonEvCountMunicipality.__table__.schema,
491
                con=db.engine(),
492
                if_exists="append",
493
                index=False,
494
            )
495
496
            #####################################
497
            #     EV data per grid district     #
498
            #####################################
499
            print("Calculate EV numbers for grid districts...")
500
            ev_data_mvgds = calc_evs_per_grid_district(ev_data_muns)
501
            # Check EV results if not in testmode
502
            if TESTMODE_OFF:
503
                validate_electric_vehicles_numbers(
504
                    "EV count in grid districts", ev_data_mvgds, ev_target
505
                )
506
            # Add scenario columns and write to DB
507
            ev_data_mvgds["scenario"] = scenario_name
508
            ev_data_mvgds["scenario_variation"] = scenario_variation_name
509
            ev_data_mvgds.sort_values(
510
                ["scenario", "scenario_variation", "bus_id"], inplace=True
511
            )
512
            ev_data_mvgds.to_sql(
513
                name=EgonEvCountMvGridDistrict.__table__.name,
514
                schema=EgonEvCountMvGridDistrict.__table__.schema,
515
                con=db.engine(),
516
                if_exists="append",
517
                index=False,
518
            )
519
520
521
def allocate_evs_to_grid_districts():
522
    """Allocate EVs to MV grid districts for all scenarios and scenario
523
    variations.
524
525
    Each grid district in
526
    :class:`egon.data.datasets.mv_grid_districts.MvGridDistricts`
527
    is assigned a list of electric vehicles from the EV pool in
528
    :class:`EgonEvPool` based on the RegioStar7 region and the
529
    counts per EV type in :class:`EgonEvCountMvGridDistrict`.
530
    Results are written to :class:`EgonEvMvGridDistrict`.
531
    """
532
533
    def get_random_evs(row):
534
        """Get random EV sample for EV type and RS7 region"""
535
        return (
536
            ev_pool.loc[
0 ignored issues
show
introduced by
The variable ev_pool does not seem to be defined in case the for loop on line 544 is not entered. Are you sure this can never be the case?
Loading history...
537
                (ev_pool.rs7_id == row.rs7_id)
538
                & (ev_pool["type"] == row["type"])
539
            ]
540
            .sample(row["count"], replace=True)
541
            .ev_id.to_list()
542
        )
543
544
    for scenario_name in ["eGon2035", "eGon100RE"]:
545
        print(f"SCENARIO: {scenario_name}")
546
547
        # Load EVs per grid district
548
        print("Loading EV counts for grid districts...")
549
        with db.session_scope() as session:
550
            query = session.query(EgonEvCountMvGridDistrict).filter(
551
                EgonEvCountMvGridDistrict.scenario == scenario_name
552
            )
553
        ev_per_mvgd = pd.read_sql(
554
            query.statement, query.session.bind, index_col=None
555
        )
556
557
        # Convert EV types' wide to long format
558
        ev_per_mvgd = pd.melt(
559
            ev_per_mvgd,
560
            id_vars=["scenario", "scenario_variation", "bus_id", "rs7_id"],
561
            value_vars=CONFIG_EV.keys(),
562
            var_name="type",
563
            value_name="count",
564
        )
565
566
        # Load EV pool
567
        print("  Loading EV pool...")
568
        with db.session_scope() as session:
569
            query = session.query(EgonEvPool).filter(
570
                EgonEvPool.scenario == scenario_name
571
            )
572
        ev_pool = pd.read_sql(
573
            query.statement,
574
            query.session.bind,
575
            index_col=None,
576
        )
577
578
        # Draw EVs randomly for each grid district from pool
579
        print("  Draw EVs from pool for grid districts...")
580
        np.random.seed(RANDOM_SEED)
581
        ev_per_mvgd["egon_ev_pool_ev_id"] = ev_per_mvgd.apply(
582
            get_random_evs, axis=1
583
        )
584
        ev_per_mvgd.drop(columns=["rs7_id", "type", "count"], inplace=True)
585
586
        # EV lists to rows
587
        ev_per_mvgd = ev_per_mvgd.explode("egon_ev_pool_ev_id")
588
589
        # Check for empty entries
590
        empty_ev_entries = ev_per_mvgd.egon_ev_pool_ev_id.isna().sum()
591
        if empty_ev_entries > 0:
592
            print("====================================================")
593
            print(
594
                f"WARNING: Found {empty_ev_entries} empty entries "
595
                f"and will remove it:"
596
            )
597
            print(ev_per_mvgd[ev_per_mvgd.egon_ev_pool_ev_id.isna()])
598
            ev_per_mvgd = ev_per_mvgd[~ev_per_mvgd.egon_ev_pool_ev_id.isna()]
599
            print("====================================================")
600
601
        # Write trips to DB
602
        print("  Writing allocated data to DB...")
603
        ev_per_mvgd.to_sql(
604
            name=EgonEvMvGridDistrict.__table__.name,
605
            schema=EgonEvMvGridDistrict.__table__.schema,
606
            con=db.engine(),
607
            if_exists="append",
608
            index=False,
609
            method="multi",
610
            chunksize=10000,
611
        )
612
613
        # Check EV result sums for all scenario variations if not in testmode
614
        if TESTMODE_OFF:
615
            print("  Validating results...")
616
            ev_per_mvgd_counts_per_scn = (
617
                ev_per_mvgd.drop(columns=["bus_id"])
618
                .groupby(["scenario", "scenario_variation"])
619
                .count()
620
            )
621
622
            for (
623
                scn,
624
                scn_var,
625
            ), ev_actual in ev_per_mvgd_counts_per_scn.iterrows():
626
                scenario_parameters = get_sector_parameters(
627
                    "mobility", scenario=scn
628
                )["motorized_individual_travel"]
629
630
                # Get EV target
631
                ev_target = scenario_parameters[scn_var]["ev_count"]
632
633
                np.testing.assert_allclose(
634
                    int(ev_actual),
635
                    ev_target,
636
                    rtol=0.0001,
637
                    err_msg=f"Dataset on EV numbers allocated to MVGDs "
638
                    f"seems to be flawed. "
639
                    f"Scenario: [{scn}], "
640
                    f"Scenario variation: [{scn_var}].",
641
                )
642