generate_model_data_bunch()   B
last analyzed

Complexity

Conditions 3

Size

Total Lines 90
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 46
dl 0
loc 90
rs 8.7672
c 0
b 0
f 0
cc 3
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
"""
2
Generate timeseries for eTraGo and pypsa-eur-sec
3
4
Call order
5
* generate_model_data_eGon2035() / generate_model_data_eGon100RE()
6
7
  * generate_model_data()
8
9
    * generate_model_data_grid_district()
10
11
      * load_evs_trips()
12
      * data_preprocessing()
13
      * generate_load_time_series()
14
      * write_model_data_to_db()
15
16
Notes
17
-----
18
# TODO REWORK
19
Share of EV with access to private charging infrastructure (`flex_share`) for
20
use cases work and home are not supported by simBEV v0.1.2 and are applied here
21
(after simulation). Applying those fixed shares post-simulation introduces
22
small errors compared to application during simBEV's trip generation.
23
24
Values (cf. `flex_share` in scenario parameters
25
:func:`egon.data.datasets.scenario_parameters.parameters.mobility`) were
26
linearly extrapolated based upon
27
https://nationale-leitstelle.de/wp-content/pdf/broschuere-lis-2025-2030-final.pdf
28
(p.92):
29
30
* eGon2035: home=0.8, work=1.0
31
* eGon100RE: home=1.0, work=1.0
32
"""
33
34
from collections import Counter
35
from pathlib import Path
36
import datetime as dt
37
import json
38
39
from sqlalchemy.sql import func
40
import numpy as np
41
import pandas as pd
42
43
from egon.data import db
44
from egon.data.datasets.emobility.motorized_individual_travel.db_classes import (  # noqa: E501
45
    EgonEvMvGridDistrict,
46
    EgonEvPool,
47
    EgonEvTrip,
48
)
49
from egon.data.datasets.emobility.motorized_individual_travel.helpers import (
50
    DATASET_CFG,
51
    MVGD_MIN_COUNT,
52
    WORKING_DIR,
53
    read_simbev_metadata_file,
54
    reduce_mem_usage,
55
)
56
from egon.data.datasets.etrago_setup import (
57
    EgonPfHvBus,
58
    EgonPfHvLink,
59
    EgonPfHvLinkTimeseries,
60
    EgonPfHvLoad,
61
    EgonPfHvLoadTimeseries,
62
    EgonPfHvStore,
63
    EgonPfHvStoreTimeseries,
64
)
65
from egon.data.datasets.mv_grid_districts import MvGridDistricts
66
67
68
def data_preprocessing(
69
    scenario_data: pd.DataFrame, ev_data_df: pd.DataFrame
70
) -> pd.DataFrame:
71
    """Filter SimBEV data to match region requirements. Duplicates profiles
72
    if necessary. Pre-calculates necessary parameters for the load time series.
73
74
    Parameters
75
    ----------
76
    scenario_data : pd.Dataframe
77
        EV per grid district
78
    ev_data_df : pd.Dataframe
79
        Trip data
80
81
    Returns
82
    -------
83
    pd.Dataframe
84
        Trip data
85
    """
86
    # get ev data for given profiles
87
    ev_data_df = ev_data_df.loc[
88
        ev_data_df.ev_id.isin(scenario_data.ev_id.unique())
89
    ]
90
91
    # drop faulty data
92
    ev_data_df = ev_data_df.loc[ev_data_df.park_start <= ev_data_df.park_end]
93
94
    # calculate time necessary to fulfill the charging demand and brutto
95
    # charging capacity in MVA
96
    ev_data_df = ev_data_df.assign(
97
        charging_capacity_grid_MW=(ev_data_df.charging_capacity_grid / 10**3),
98
        minimum_charging_time=(
99
            ev_data_df.charging_demand
100
            / ev_data_df.charging_capacity_nominal
101
            * 4
102
        ),
103
        location=ev_data_df.location.str.replace("/", "_"),
104
    )
105
106
    # fix driving events
107
    ev_data_df.minimum_charging_time.fillna(0, inplace=True)
108
109
    # calculate charging capacity for last timestep
110
    (
111
        full_timesteps,
112
        last_timestep_share,
113
    ) = ev_data_df.minimum_charging_time.divmod(1)
114
115
    full_timesteps = full_timesteps.astype(int)
116
117
    ev_data_df = ev_data_df.assign(
118
        full_timesteps=full_timesteps,
119
        last_timestep_share=last_timestep_share,
120
        last_timestep_charging_capacity_grid_MW=(
121
            last_timestep_share * ev_data_df.charging_capacity_grid_MW
122
        ),
123
        charge_end=ev_data_df.park_start + full_timesteps,
124
        last_timestep=ev_data_df.park_start + full_timesteps,
125
    )
126
127
    # Calculate flexible charging capacity:
128
    # only for private charging facilities at home and work
129
    mask_work = (ev_data_df.location == "0_work") & (
130
        ev_data_df.use_case == "work"
131
    )
132
    mask_home = (ev_data_df.location == "6_home") & (
133
        ev_data_df.use_case == "home"
134
    )
135
136
    ev_data_df["flex_charging_capacity_grid_MW"] = 0
137
    ev_data_df.loc[mask_work | mask_home, "flex_charging_capacity_grid_MW"] = (
138
        ev_data_df.loc[mask_work | mask_home, "charging_capacity_grid_MW"]
139
    )
140
141
    ev_data_df["flex_last_timestep_charging_capacity_grid_MW"] = 0
142
    ev_data_df.loc[
143
        mask_work | mask_home, "flex_last_timestep_charging_capacity_grid_MW"
144
    ] = ev_data_df.loc[
145
        mask_work | mask_home, "last_timestep_charging_capacity_grid_MW"
146
    ]
147
148
    # Check length of timeseries
149
    if len(ev_data_df.loc[ev_data_df.last_timestep > 35040]) > 0:
150
        print("    Warning: Trip data exceeds 1 year and is cropped.")
151
        # Correct last TS
152
        ev_data_df.loc[ev_data_df.last_timestep > 35040, "last_timestep"] = (
153
            35040
154
        )
155
156
    if DATASET_CFG["model_timeseries"]["reduce_memory"]:
157
        return reduce_mem_usage(ev_data_df)
158
159
    return ev_data_df
160
161
162
def generate_load_time_series(
163
    ev_data_df: pd.DataFrame,
164
    run_config: pd.DataFrame,
165
    scenario_data: pd.DataFrame,
166
) -> pd.DataFrame:
167
    """Calculate the load time series from the given trip data. A dumb
168
    charging strategy is assumed where each EV starts charging immediately
169
    after plugging it in. Simultaneously the flexible charging capacity is
170
    calculated.
171
172
    Parameters
173
    ----------
174
    ev_data_df : pd.DataFrame
175
        Full trip data
176
    run_config : pd.DataFrame
177
        simBEV metadata: run config
178
    scenario_data : pd.Dataframe
179
        EV per grid district
180
181
    Returns
182
    -------
183
    pd.DataFrame
184
        time series of the load and the flex potential
185
    """
186
    # Get duplicates dict
187
    profile_counter = Counter(scenario_data.ev_id)
188
189
    # instantiate timeindex
190
    timeindex = pd.date_range(
191
        start=dt.datetime.fromisoformat(f"{run_config.start_date} 00:00:00"),
192
        end=dt.datetime.fromisoformat(f"{run_config.end_date} 23:45:00")
193
        + dt.timedelta(minutes=int(run_config.stepsize)),
194
        freq=f"{int(run_config.stepsize)}Min",
195
    )
196
197
    load_time_series_df = pd.DataFrame(
198
        data=0.0,
199
        index=timeindex,
200
        columns=["load_time_series", "flex_time_series"],
201
    )
202
203
    load_time_series_array = np.zeros(len(load_time_series_df))
204
    flex_time_series_array = load_time_series_array.copy()
205
    simultaneous_plugged_in_charging_capacity = load_time_series_array.copy()
206
    simultaneous_plugged_in_charging_capacity_flex = (
207
        load_time_series_array.copy()
208
    )
209
    soc_min_absolute = load_time_series_array.copy()
210
    soc_max_absolute = load_time_series_array.copy()
211
    driving_load_time_series_array = load_time_series_array.copy()
212
213
    columns = [
214
        "ev_id",
215
        "drive_start",
216
        "drive_end",
217
        "park_start",
218
        "park_end",
219
        "charge_end",
220
        "charging_capacity_grid_MW",
221
        "last_timestep",
222
        "last_timestep_charging_capacity_grid_MW",
223
        "flex_charging_capacity_grid_MW",
224
        "flex_last_timestep_charging_capacity_grid_MW",
225
        "soc_start",
226
        "soc_end",
227
        "bat_cap",
228
        "location",
229
        "consumption",
230
    ]
231
232
    # iterate over charging events
233
    for (
234
        _,
235
        ev_id,
236
        drive_start,
237
        drive_end,
238
        start,
239
        park_end,
240
        end,
241
        cap,
242
        last_ts,
243
        last_ts_cap,
244
        flex_cap,
245
        flex_last_ts_cap,
246
        soc_start,
247
        soc_end,
248
        bat_cap,
249
        location,
250
        consumption,
251
    ) in ev_data_df[columns].itertuples():
252
        ev_count = profile_counter[ev_id]
253
254
        load_time_series_array[start:end] += cap * ev_count
255
        load_time_series_array[last_ts] += last_ts_cap * ev_count
256
257
        flex_time_series_array[start:end] += flex_cap * ev_count
258
        flex_time_series_array[last_ts] += flex_last_ts_cap * ev_count
259
260
        simultaneous_plugged_in_charging_capacity[start : park_end + 1] += (
261
            cap * ev_count
262
        )
263
        simultaneous_plugged_in_charging_capacity_flex[
264
            start : park_end + 1
265
        ] += (flex_cap * ev_count)
266
267
        # ====================================================
268
        # min and max SoC constraints of aggregated EV battery
269
        # ====================================================
270
        # (I) Preserve SoC while driving
271
        if location == "driving":
272
            # Full band while driving
273
            # soc_min_absolute[drive_start:drive_end+1] +=
274
            # soc_end * bat_cap * ev_count
275
            #
276
            # soc_max_absolute[drive_start:drive_end+1] +=
277
            # soc_start * bat_cap * ev_count
278
279
            # Real band (decrease SoC while driving)
280
            soc_min_absolute[drive_start : drive_end + 1] += (
281
                np.linspace(soc_start, soc_end, drive_end - drive_start + 2)[
282
                    1:
283
                ]
284
                * bat_cap
285
                * ev_count
286
            )
287
            soc_max_absolute[drive_start : drive_end + 1] += (
288
                np.linspace(soc_start, soc_end, drive_end - drive_start + 2)[
289
                    1:
290
                ]
291
                * bat_cap
292
                * ev_count
293
            )
294
295
            # Equal distribution of driving load
296
            if soc_start > soc_end:  # reqd. for PHEV
297
                driving_load_time_series_array[
298
                    drive_start : drive_end + 1
299
                ] += (consumption * ev_count) / (drive_end - drive_start + 1)
300
301
        # (II) Fix SoC bounds while parking w/o charging
302
        elif soc_start == soc_end:
303
            soc_min_absolute[start : park_end + 1] += (
304
                soc_start * bat_cap * ev_count
305
            )
306
            soc_max_absolute[start : park_end + 1] += (
307
                soc_end * bat_cap * ev_count
308
            )
309
310
        # (III) Set SoC bounds at start and end of parking while charging
311
        # for flexible and non-flexible events
312
        elif soc_start < soc_end:
313
            if flex_cap > 0:
314
                # * "flex" (private charging only, band: SoC_min..SoC_max)
315
                soc_min_absolute[start : park_end + 1] += (
316
                    soc_start * bat_cap * ev_count
317
                )
318
                soc_max_absolute[start : park_end + 1] += (
319
                    soc_end * bat_cap * ev_count
320
                )
321
322
                # * "flex+" (private charging only, band: 0..1)
323
                #   (IF USED: add elif with flex scenario)
324
                # soc_min_absolute[start] += soc_start * bat_cap * ev_count
325
                # soc_max_absolute[start] += soc_start * bat_cap * ev_count
326
                # soc_min_absolute[park_end] += soc_end * bat_cap * ev_count
327
                # soc_max_absolute[park_end] += soc_end * bat_cap * ev_count
328
329
            # * Set SoC bounds for non-flexible charging (increase SoC while
330
            #   charging)
331
            # (SKIP THIS PART for "flex++" (private+public charging))
332
            elif flex_cap == 0:
333
                soc_min_absolute[start : park_end + 1] += (
334
                    np.linspace(soc_start, soc_end, park_end - start + 1)
335
                    * bat_cap
336
                    * ev_count
337
                )
338
                soc_max_absolute[start : park_end + 1] += (
339
                    np.linspace(soc_start, soc_end, park_end - start + 1)
340
                    * bat_cap
341
                    * ev_count
342
                )
343
344
    # Build timeseries
345
    load_time_series_df = load_time_series_df.assign(
346
        load_time_series=load_time_series_array,
347
        flex_time_series=flex_time_series_array,
348
        simultaneous_plugged_in_charging_capacity=(
349
            simultaneous_plugged_in_charging_capacity
350
        ),
351
        simultaneous_plugged_in_charging_capacity_flex=(
352
            simultaneous_plugged_in_charging_capacity_flex
353
        ),
354
        soc_min_absolute=(soc_min_absolute / 1e3),
355
        soc_max_absolute=(soc_max_absolute / 1e3),
356
        driving_load_time_series=driving_load_time_series_array / 1e3,
357
    )
358
359
    # validate load timeseries
360
    np.testing.assert_almost_equal(
361
        load_time_series_df.load_time_series.sum() / 4,
362
        (
363
            ev_data_df.ev_id.apply(lambda _: profile_counter[_])
364
            * ev_data_df.charging_demand
365
        ).sum()
366
        / 1000
367
        / float(run_config.eta_cp),
368
        decimal=-1,
369
    )
370
371
    if DATASET_CFG["model_timeseries"]["reduce_memory"]:
372
        return reduce_mem_usage(load_time_series_df)
373
    return load_time_series_df
374
375
376
def generate_static_params(
377
    ev_data_df: pd.DataFrame,
378
    load_time_series_df: pd.DataFrame,
379
    evs_grid_district_df: pd.DataFrame,
380
) -> dict:
381
    """Calculate static parameters from trip data.
382
383
    * cumulative initial SoC
384
    * cumulative battery capacity
385
    * simultaneous plugged in charging capacity
386
387
    Parameters
388
    ----------
389
    ev_data_df : pd.DataFrame
390
        Fill trip data
391
392
    Returns
393
    -------
394
    dict
395
        Static parameters
396
    """
397
    max_df = (
398
        ev_data_df[["ev_id", "bat_cap", "charging_capacity_grid_MW"]]
399
        .groupby("ev_id")
400
        .max()
401
    )
402
403
    # Get EV duplicates dict and weight battery capacity
404
    max_df["bat_cap"] = max_df.bat_cap.mul(
405
        pd.Series(Counter(evs_grid_district_df.ev_id))
406
    )
407
408
    static_params_dict = {
409
        "store_ev_battery.e_nom_MWh": float(max_df.bat_cap.sum() / 1e3),
410
        "link_bev_charger.p_nom_MW": float(
411
            load_time_series_df.simultaneous_plugged_in_charging_capacity.max()
412
        ),
413
    }
414
415
    return static_params_dict
416
417
418
def load_evs_trips(
419
    scenario_name: str,
420
    evs_ids: list,
421
    charging_events_only: bool = False,
422
    flex_only_at_charging_events: bool = True,
423
) -> pd.DataFrame:
424
    """Load trips for EVs
425
426
    Parameters
427
    ----------
428
    scenario_name : str
429
        Scenario name
430
    evs_ids : list of int
431
        IDs of EV to load the trips for
432
    charging_events_only : bool
433
        Load only events where charging takes place
434
    flex_only_at_charging_events : bool
435
        Flexibility only at charging events. If False, flexibility is provided
436
        by plugged-in EVs even if no charging takes place.
437
438
    Returns
439
    -------
440
    pd.DataFrame
441
        Trip data
442
    """
443
    # Select only charigung events
444
    if charging_events_only is True:
445
        charging_condition = EgonEvTrip.charging_demand > 0
446
    else:
447
        charging_condition = EgonEvTrip.charging_demand >= 0
448
449
    with db.session_scope() as session:
450
        query = (
451
            session.query(
452
                EgonEvTrip.egon_ev_pool_ev_id.label("ev_id"),
453
                EgonEvTrip.location,
454
                EgonEvTrip.use_case,
455
                EgonEvTrip.charging_capacity_nominal,
456
                EgonEvTrip.charging_capacity_grid,
457
                EgonEvTrip.charging_capacity_battery,
458
                EgonEvTrip.soc_start,
459
                EgonEvTrip.soc_end,
460
                EgonEvTrip.charging_demand,
461
                EgonEvTrip.park_start,
462
                EgonEvTrip.park_end,
463
                EgonEvTrip.drive_start,
464
                EgonEvTrip.drive_end,
465
                EgonEvTrip.consumption,
466
                EgonEvPool.type,
467
            )
468
            .join(
469
                EgonEvPool, EgonEvPool.ev_id == EgonEvTrip.egon_ev_pool_ev_id
470
            )
471
            .filter(EgonEvTrip.egon_ev_pool_ev_id.in_(evs_ids))
472
            .filter(EgonEvTrip.scenario == scenario_name)
473
            .filter(EgonEvPool.scenario == scenario_name)
474
            .filter(charging_condition)
475
            .order_by(
476
                EgonEvTrip.egon_ev_pool_ev_id, EgonEvTrip.simbev_event_id
477
            )
478
        )
479
480
    trip_data = pd.read_sql(
481
        query.statement, query.session.bind, index_col=None
482
    ).astype(
483
        {
484
            "ev_id": "int",
485
            "park_start": "int",
486
            "park_end": "int",
487
            "drive_start": "int",
488
            "drive_end": "int",
489
        }
490
    )
491
492
    if flex_only_at_charging_events is True:
493
        # ASSUMPTION: set charging cap 0 where there's no demand
494
        # (discard other plugged-in times)
495
        mask = trip_data.charging_demand == 0
496
        trip_data.loc[mask, "charging_capacity_nominal"] = 0
497
        trip_data.loc[mask, "charging_capacity_grid"] = 0
498
        trip_data.loc[mask, "charging_capacity_battery"] = 0
499
500
    return trip_data
501
502
503
def write_model_data_to_db(
504
    static_params_dict: dict,
505
    load_time_series_df: pd.DataFrame,
506
    bus_id: int,
507
    scenario_name: str,
508
    run_config: pd.DataFrame,
509
    bat_cap: pd.DataFrame,
510
) -> None:
511
    """Write all results for grid district to database
512
513
    Parameters
514
    ----------
515
    static_params_dict : dict
516
        Static model params
517
    load_time_series_df : pd.DataFrame
518
        Load time series for grid district
519
    bus_id : int
520
        ID of grid district
521
    scenario_name : str
522
        Scenario name
523
    run_config : pd.DataFrame
524
        simBEV metadata: run config
525
    bat_cap : pd.DataFrame
526
        Battery capacities per EV type
527
528
    Returns
529
    -------
530
    None
531
    """
532
533
    def calc_initial_ev_soc(bus_id: int, scenario_name: str) -> pd.DataFrame:
534
        """Calculate an average initial state of charge for EVs in MV grid
535
        district.
536
537
        This is done by weighting the initial SoCs at timestep=0 with EV count
538
        and battery capacity for each EV type.
539
        """
540
        with db.session_scope() as session:
541
            query_ev_soc = (
542
                session.query(
543
                    EgonEvPool.type,
544
                    func.count(EgonEvTrip.egon_ev_pool_ev_id).label(
545
                        "ev_count"
546
                    ),
547
                    func.avg(EgonEvTrip.soc_start).label("ev_soc_start"),
548
                )
549
                .select_from(EgonEvTrip)
550
                .join(
551
                    EgonEvPool,
552
                    EgonEvPool.ev_id == EgonEvTrip.egon_ev_pool_ev_id,
553
                )
554
                .join(
555
                    EgonEvMvGridDistrict,
556
                    EgonEvMvGridDistrict.egon_ev_pool_ev_id
557
                    == EgonEvTrip.egon_ev_pool_ev_id,
558
                )
559
                .filter(
560
                    EgonEvTrip.scenario == scenario_name,
561
                    EgonEvPool.scenario == scenario_name,
562
                    EgonEvMvGridDistrict.scenario == scenario_name,
563
                    EgonEvMvGridDistrict.bus_id == bus_id,
564
                    EgonEvTrip.simbev_event_id == 0,
565
                )
566
                .group_by(EgonEvPool.type)
567
            )
568
569
        initial_soc_per_ev_type = pd.read_sql(
570
            query_ev_soc.statement, query_ev_soc.session.bind, index_col="type"
571
        )
572
573
        initial_soc_per_ev_type["battery_capacity_sum"] = (
574
            initial_soc_per_ev_type.ev_count.multiply(bat_cap)
575
        )
576
        initial_soc_per_ev_type["ev_soc_start_abs"] = (
577
            initial_soc_per_ev_type.battery_capacity_sum.multiply(
578
                initial_soc_per_ev_type.ev_soc_start
579
            )
580
        )
581
582
        return (
583
            initial_soc_per_ev_type.ev_soc_start_abs.sum()
584
            / initial_soc_per_ev_type.battery_capacity_sum.sum()
585
        )
586
587
    def write_to_db(write_lowflex_model: bool) -> None:
588
        """Write model data to eTraGo tables"""
589
590
        @db.check_db_unique_violation
591
        def write_bus(scenario_name: str) -> int:
592
            # eMob MIT bus
593
            emob_bus_id = db.next_etrago_id("bus")
594
            with db.session_scope() as session:
595
                session.add(
596
                    EgonPfHvBus(
597
                        scn_name=scenario_name,
598
                        bus_id=emob_bus_id,
599
                        v_nom=1,
600
                        carrier="Li_ion",
601
                        x=etrago_bus.x,
602
                        y=etrago_bus.y,
603
                        geom=etrago_bus.geom,
604
                    )
605
                )
606
            return emob_bus_id
607
608
        @db.check_db_unique_violation
609
        def write_link(scenario_name: str) -> None:
610
            # eMob MIT link [bus_el] -> [bus_ev]
611
            emob_link_id = db.next_etrago_id("link")
612
            with db.session_scope() as session:
613
                session.add(
614
                    EgonPfHvLink(
615
                        scn_name=scenario_name,
616
                        link_id=emob_link_id,
617
                        bus0=etrago_bus.bus_id,
618
                        bus1=emob_bus_id,
619
                        carrier="BEV_charger",
620
                        efficiency=float(run_config.eta_cp),
621
                        p_nom=(
622
                            load_time_series_df.simultaneous_plugged_in_charging_capacity.max()  # noqa: E501
623
                        ),
624
                        p_nom_extendable=False,
625
                        p_nom_min=0,
626
                        p_nom_max=np.Inf,
627
                        p_min_pu=0,
628
                        p_max_pu=1,
629
                        # p_set_fixed=0,
630
                        capital_cost=0,
631
                        marginal_cost=0,
632
                        length=0,
633
                        terrain_factor=1,
634
                    )
635
                )
636
            with db.session_scope() as session:
637
                session.add(
638
                    EgonPfHvLinkTimeseries(
639
                        scn_name=scenario_name,
640
                        link_id=emob_link_id,
641
                        temp_id=1,
642
                        p_min_pu=None,
643
                        p_max_pu=(
644
                            hourly_load_time_series_df.ev_availability.to_list()  # noqa: E501
645
                        ),
646
                    )
647
                )
648
649
        @db.check_db_unique_violation
650
        def write_store(scenario_name: str) -> None:
651
            # eMob MIT store
652
            emob_store_id = db.next_etrago_id("store")
653
            with db.session_scope() as session:
654
                session.add(
655
                    EgonPfHvStore(
656
                        scn_name=scenario_name,
657
                        store_id=emob_store_id,
658
                        bus=emob_bus_id,
659
                        carrier="battery_storage",
660
                        e_nom=static_params_dict["store_ev_battery.e_nom_MWh"],
661
                        e_nom_extendable=False,
662
                        e_nom_min=0,
663
                        e_nom_max=np.Inf,
664
                        e_min_pu=0,
665
                        e_max_pu=1,
666
                        e_initial=(
667
                            initial_soc_mean
668
                            * static_params_dict["store_ev_battery.e_nom_MWh"]
669
                        ),
670
                        e_cyclic=False,
671
                        sign=1,
672
                        standing_loss=0,
673
                    )
674
                )
675
            with db.session_scope() as session:
676
                session.add(
677
                    EgonPfHvStoreTimeseries(
678
                        scn_name=scenario_name,
679
                        store_id=emob_store_id,
680
                        temp_id=1,
681
                        e_min_pu=hourly_load_time_series_df.soc_min.to_list(),
682
                        e_max_pu=hourly_load_time_series_df.soc_max.to_list(),
683
                    )
684
                )
685
686
        @db.check_db_unique_violation
687
        def write_load(
688
            scenario_name: str, connection_bus_id: int, load_ts: list
689
        ) -> None:
690
            # eMob MIT load
691
            emob_load_id = db.next_etrago_id("load")
692
            with db.session_scope() as session:
693
                session.add(
694
                    EgonPfHvLoad(
695
                        scn_name=scenario_name,
696
                        load_id=emob_load_id,
697
                        bus=connection_bus_id,
698
                        carrier="land_transport_EV",
699
                        sign=-1,
700
                    )
701
                )
702
            with db.session_scope() as session:
703
                session.add(
704
                    EgonPfHvLoadTimeseries(
705
                        scn_name=scenario_name,
706
                        load_id=emob_load_id,
707
                        temp_id=1,
708
                        p_set=load_ts,
709
                    )
710
                )
711
712
        # Get eTraGo substation bus
713
        with db.session_scope() as session:
714
            query = session.query(
715
                EgonPfHvBus.scn_name,
716
                EgonPfHvBus.bus_id,
717
                EgonPfHvBus.x,
718
                EgonPfHvBus.y,
719
                EgonPfHvBus.geom,
720
            ).filter(
721
                EgonPfHvBus.scn_name == scenario_name,
722
                EgonPfHvBus.bus_id == bus_id,
723
                EgonPfHvBus.carrier == "AC",
724
            )
725
            etrago_bus = query.first()
726
            if etrago_bus is None:
727
                # TODO: raise exception here!
728
                print(
729
                    f"No AC bus found for scenario {scenario_name} "
730
                    f"with bus_id {bus_id} in table egon_etrago_bus!"
731
                )
732
733
        # Call DB writing functions for regular or lowflex scenario
734
        # * use corresponding scenario name as defined in datasets.yml
735
        # * no storage for lowflex scenario
736
        # * load timeseries:
737
        #   * regular (flex): use driving load
738
        #   * lowflex: use dumb charging load
739
        #   * status2019: also dumb charging
740
        #   * status2023: also dumb charging
741
742
        if scenario_name in ["status2019", "status2023"]:
743
            write_load(
744
                scenario_name=scenario_name,
745
                connection_bus_id=etrago_bus.bus_id,
746
                load_ts=hourly_load_time_series_df.load_time_series.to_list(),
747
            )
748
        else:
749
            if write_lowflex_model is False:
750
                emob_bus_id = write_bus(scenario_name=scenario_name)
751
                write_link(scenario_name=scenario_name)
752
                write_store(scenario_name=scenario_name)
753
                write_load(
754
                    scenario_name=scenario_name,
755
                    connection_bus_id=emob_bus_id,
756
                    load_ts=(
757
                        hourly_load_time_series_df.driving_load_time_series.to_list()  # noqa: E501
758
                    ),
759
                )
760
761
            else:
762
                # Get lowflex scenario name
763
                lowflex_scenario_name = DATASET_CFG["scenario"]["lowflex"][
764
                    "names"
765
                ][scenario_name]
766
                write_load(
767
                    scenario_name=lowflex_scenario_name,
768
                    connection_bus_id=etrago_bus.bus_id,
769
                    load_ts=hourly_load_time_series_df.load_time_series.to_list(),
770
                )
771
772
    def write_to_file():
773
        """Write model data to file (for debugging purposes)"""
774
        results_dir = WORKING_DIR / Path("results", scenario_name, str(bus_id))
775
        results_dir.mkdir(exist_ok=True, parents=True)
776
777
        hourly_load_time_series_df[["load_time_series"]].to_csv(
778
            results_dir / "ev_load_time_series.csv"
779
        )
780
        hourly_load_time_series_df[["ev_availability"]].to_csv(
781
            results_dir / "ev_availability.csv"
782
        )
783
        hourly_load_time_series_df[["soc_min", "soc_max"]].to_csv(
784
            results_dir / "ev_dsm_profile.csv"
785
        )
786
787
        static_params_dict["load_land_transport_ev.p_set_MW"] = (
788
            "ev_load_time_series.csv"
789
        )
790
        static_params_dict["link_bev_charger.p_max_pu"] = "ev_availability.csv"
791
        static_params_dict["store_ev_battery.e_min_pu"] = "ev_dsm_profile.csv"
792
        static_params_dict["store_ev_battery.e_max_pu"] = "ev_dsm_profile.csv"
793
794
        file = results_dir / "ev_static_params.json"
795
796
        with open(file, "w") as f:
797
            json.dump(static_params_dict, f, indent=4)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable static_params_dict does not seem to be defined.
Loading history...
798
799
    print("  Writing model timeseries...")
800
    load_time_series_df = load_time_series_df.assign(
801
        ev_availability=(
802
            load_time_series_df.simultaneous_plugged_in_charging_capacity
803
            / static_params_dict["link_bev_charger.p_nom_MW"]
804
        )
805
    )
806
807
    # Resample to 1h
808
    hourly_load_time_series_df = load_time_series_df.resample("1H").agg(
809
        {
810
            "load_time_series": np.mean,
811
            "flex_time_series": np.mean,
812
            "simultaneous_plugged_in_charging_capacity": np.mean,
813
            "simultaneous_plugged_in_charging_capacity_flex": np.mean,
814
            "soc_min_absolute": np.min,
815
            "soc_max_absolute": np.max,
816
            "ev_availability": np.mean,
817
            "driving_load_time_series": np.sum,
818
        }
819
    )
820
821
    # Create relative SoC timeseries
822
    hourly_load_time_series_df = hourly_load_time_series_df.assign(
823
        soc_min=hourly_load_time_series_df.soc_min_absolute.div(
824
            static_params_dict["store_ev_battery.e_nom_MWh"]
825
        ),
826
        soc_max=hourly_load_time_series_df.soc_max_absolute.div(
827
            static_params_dict["store_ev_battery.e_nom_MWh"]
828
        ),
829
    )
830
    hourly_load_time_series_df = hourly_load_time_series_df.assign(
831
        soc_delta_absolute=(
832
            hourly_load_time_series_df.soc_max_absolute
833
            - hourly_load_time_series_df.soc_min_absolute
834
        ),
835
        soc_delta=(
836
            hourly_load_time_series_df.soc_max
837
            - hourly_load_time_series_df.soc_min
838
        ),
839
    )
840
841
    # Crop hourly TS if needed
842
    hourly_load_time_series_df = hourly_load_time_series_df[:8760]
843
844
    # Create lowflex scenario?
845
    write_lowflex_model = DATASET_CFG["scenario"]["lowflex"][
846
        "create_lowflex_scenario"
847
    ]
848
849
    # Get initial average storage SoC
850
    initial_soc_mean = calc_initial_ev_soc(bus_id, scenario_name)
851
852
    # Write to database: regular and lowflex scenario
853
    write_to_db(write_lowflex_model=False)
854
    print("    Writing flex scenario...")
855
    if write_lowflex_model is True:
856
        print("    Writing lowflex scenario...")
857
        write_to_db(write_lowflex_model=True)
858
859
    # Export to working dir if requested
860
    if DATASET_CFG["model_timeseries"]["export_results_to_csv"]:
861
        write_to_file()
862
863
864
def delete_model_data_from_db():
865
    """Delete all eMob MIT data from eTraGo PF tables"""
866
    with db.session_scope() as session:
867
        subquery_bus_de = (
868
            session.query(EgonPfHvBus.bus_id)
869
            .filter(EgonPfHvBus.country == "DE")
870
            .subquery()
871
        )
872
873
        # Link TS
874
        subquery = (
875
            session.query(EgonPfHvLink.link_id)
876
            .filter(
877
                EgonPfHvLink.carrier == "BEV_charger",
878
                EgonPfHvLink.bus0.in_(subquery_bus_de),
879
                EgonPfHvLink.bus1.in_(subquery_bus_de),
880
            )
881
            .subquery()
882
        )
883
884
        session.query(EgonPfHvLinkTimeseries).filter(
885
            EgonPfHvLinkTimeseries.link_id.in_(subquery)
886
        ).delete(synchronize_session=False)
887
888
        # Links
889
        session.query(EgonPfHvLink).filter(
890
            EgonPfHvLink.carrier == "BEV_charger",
891
            EgonPfHvLink.bus0.in_(subquery_bus_de),
892
            EgonPfHvLink.bus1.in_(subquery_bus_de),
893
        ).delete(synchronize_session=False)
894
895
        # Store TS
896
        subquery = (
897
            session.query(EgonPfHvStore.store_id)
898
            .filter(
899
                EgonPfHvStore.carrier == "battery_storage",
900
                EgonPfHvStore.bus.in_(subquery_bus_de),
901
            )
902
            .subquery()
903
        )
904
905
        session.query(EgonPfHvStoreTimeseries).filter(
906
            EgonPfHvStoreTimeseries.store_id.in_(subquery)
907
        ).delete(synchronize_session=False)
908
909
        # Stores
910
        session.query(EgonPfHvStore).filter(
911
            EgonPfHvStore.carrier == "battery_storage",
912
            EgonPfHvStore.bus.in_(subquery_bus_de),
913
        ).delete(synchronize_session=False)
914
915
        # Load TS
916
        subquery = (
917
            session.query(EgonPfHvLoad.load_id)
918
            .filter(
919
                EgonPfHvLoad.carrier == "land_transport_EV",
920
                EgonPfHvLoad.bus.in_(subquery_bus_de),
921
            )
922
            .subquery()
923
        )
924
925
        session.query(EgonPfHvLoadTimeseries).filter(
926
            EgonPfHvLoadTimeseries.load_id.in_(subquery)
927
        ).delete(synchronize_session=False)
928
929
        # Loads
930
        session.query(EgonPfHvLoad).filter(
931
            EgonPfHvLoad.carrier == "land_transport_EV",
932
            EgonPfHvLoad.bus.in_(subquery_bus_de),
933
        ).delete(synchronize_session=False)
934
935
        # Buses
936
        session.query(EgonPfHvBus).filter(
937
            EgonPfHvBus.carrier == "Li_ion",
938
            EgonPfHvBus.country == "DE",
939
        ).delete(synchronize_session=False)
940
941
942
def load_grid_district_ids() -> pd.Series:
943
    """Load bus IDs of all grid districts"""
944
    with db.session_scope() as session:
945
        query_mvgd = session.query(MvGridDistricts.bus_id)
946
    return pd.read_sql(
947
        query_mvgd.statement, query_mvgd.session.bind, index_col=None
948
    ).bus_id.sort_values()
949
950
951
def generate_model_data_grid_district(
952
    scenario_name: str,
953
    evs_grid_district: pd.DataFrame,
954
    bat_cap_dict: dict,
955
    run_config: pd.DataFrame,
956
) -> tuple:
957
    """Generates timeseries from simBEV trip data for MV grid district
958
959
    Parameters
960
    ----------
961
    scenario_name : str
962
        Scenario name
963
    evs_grid_district : pd.DataFrame
964
        EV data for grid district
965
    bat_cap_dict : dict
966
        Battery capacity per EV type
967
    run_config : pd.DataFrame
968
        simBEV metadata: run config
969
970
    Returns
971
    -------
972
    pd.DataFrame
973
        Model data for grid district
974
    """
975
976
    # Load trip data
977
    print("  Loading trips...")
978
    trip_data = load_evs_trips(
979
        scenario_name=scenario_name,
980
        evs_ids=evs_grid_district.ev_id.unique(),
981
        charging_events_only=False,
982
        flex_only_at_charging_events=True,
983
    )
984
985
    print("  Preprocessing data...")
986
    # Assign battery capacity to trip data
987
    trip_data["bat_cap"] = trip_data.type.apply(lambda _: bat_cap_dict[_])
988
    trip_data.drop(columns=["type"], inplace=True)
989
990
    # Preprocess trip data
991
    trip_data = data_preprocessing(evs_grid_district, trip_data)
992
993
    # Generate load timeseries
994
    print("  Generating load timeseries...")
995
    load_ts = generate_load_time_series(
996
        ev_data_df=trip_data,
997
        run_config=run_config,
998
        scenario_data=evs_grid_district,
999
    )
1000
1001
    # Generate static params
1002
    static_params = generate_static_params(
1003
        trip_data, load_ts, evs_grid_district
1004
    )
1005
1006
    return static_params, load_ts
1007
1008
1009
def generate_model_data_bunch(scenario_name: str, bunch: range) -> None:
1010
    """Generates timeseries from simBEV trip data for a bunch of MV grid
1011
    districts.
1012
1013
    Parameters
1014
    ----------
1015
    scenario_name : str
1016
        Scenario name
1017
    bunch : list
1018
        Bunch of grid districts to generate data for, e.g. [1,2,..,100].
1019
        Note: `bunch` is NOT a list of grid districts but is used for slicing
1020
        the ordered list (by bus_id) of grid districts! This is used for
1021
        parallelization. See
1022
        :meth:`egon.data.datasets.emobility.motorized_individual_travel.MotorizedIndividualTravel.generate_model_data_tasks`
1023
    """
1024
    # Get list of grid districts / substations for this bunch
1025
    mvgd_bus_ids = load_grid_district_ids().iloc[bunch]
1026
1027
    # Get scenario variation name
1028
    scenario_var_name = DATASET_CFG["scenario"]["variation"][scenario_name]
1029
1030
    print(
1031
        f"SCENARIO: {scenario_name}, "
1032
        f"SCENARIO VARIATION: {scenario_var_name}, "
1033
        f"BUNCH: {bunch[0]}-{bunch[-1]}"
1034
    )
1035
1036
    # Load scenario params for scenario and scenario variation
1037
    # scenario_variation_parameters = get_sector_parameters(
1038
    #    "mobility", scenario=scenario_name
1039
    # )["motorized_individual_travel"][scenario_var_name]
1040
1041
    # Get substations
1042
    with db.session_scope() as session:
1043
        query = (
1044
            session.query(
1045
                EgonEvMvGridDistrict.bus_id,
1046
                EgonEvMvGridDistrict.egon_ev_pool_ev_id.label("ev_id"),
1047
            )
1048
            .filter(EgonEvMvGridDistrict.scenario == scenario_name)
1049
            .filter(
1050
                EgonEvMvGridDistrict.scenario_variation == scenario_var_name
1051
            )
1052
            .filter(EgonEvMvGridDistrict.bus_id.in_(mvgd_bus_ids))
1053
            .filter(EgonEvMvGridDistrict.egon_ev_pool_ev_id.isnot(None))
1054
        )
1055
    evs_grid_district = pd.read_sql(
1056
        query.statement, query.session.bind, index_col=None
1057
    ).astype({"ev_id": "int"})
1058
1059
    mvgd_bus_ids = evs_grid_district.bus_id.unique()
1060
    print(
1061
        f"{len(evs_grid_district)} EV loaded "
1062
        f"({len(evs_grid_district.ev_id.unique())} unique) in "
1063
        f"{len(mvgd_bus_ids)} grid districts."
1064
    )
1065
1066
    # Get run metadata
1067
    meta_tech_data = read_simbev_metadata_file(scenario_name, "tech_data")
1068
    meta_run_config = read_simbev_metadata_file(scenario_name, "config").loc[
1069
        "basic"
1070
    ]
1071
1072
    # Generate timeseries for each MVGD
1073
    print("GENERATE MODEL DATA...")
1074
    ctr = 0
1075
    for bus_id in mvgd_bus_ids:
1076
        ctr += 1
1077
        print(
1078
            f"Processing grid district: bus {bus_id}... "
1079
            f"({ctr}/{len(mvgd_bus_ids)})"
1080
        )
1081
        (
1082
            static_params,
1083
            load_ts,
1084
        ) = generate_model_data_grid_district(
1085
            scenario_name=scenario_name,
1086
            evs_grid_district=evs_grid_district[
1087
                evs_grid_district.bus_id == bus_id
1088
            ],
1089
            bat_cap_dict=meta_tech_data.battery_capacity.to_dict(),
1090
            run_config=meta_run_config,
1091
        )
1092
        write_model_data_to_db(
1093
            static_params_dict=static_params,
1094
            load_time_series_df=load_ts,
1095
            bus_id=bus_id,
1096
            scenario_name=scenario_name,
1097
            run_config=meta_run_config,
1098
            bat_cap=meta_tech_data.battery_capacity,
1099
        )
1100
1101
1102
def generate_model_data_status2019_remaining():
1103
    """Generates timeseries for status2019 scenario for grid districts which
1104
    has not been processed in the parallel tasks before.
1105
    """
1106
    generate_model_data_bunch(
1107
        scenario_name="status2019",
1108
        bunch=range(MVGD_MIN_COUNT, len(load_grid_district_ids())),
1109
    )
1110
1111
1112
def generate_model_data_status2023_remaining():
1113
    """Generates timeseries for status2023 scenario for grid districts which
1114
    has not been processed in the parallel tasks before.
1115
    """
1116
    generate_model_data_bunch(
1117
        scenario_name="status2023",
1118
        bunch=range(MVGD_MIN_COUNT, len(load_grid_district_ids())),
1119
    )
1120
1121
1122
def generate_model_data_eGon2035_remaining():
1123
    """Generates timeseries for eGon2035 scenario for grid districts which
1124
    has not been processed in the parallel tasks before.
1125
    """
1126
    generate_model_data_bunch(
1127
        scenario_name="eGon2035",
1128
        bunch=range(MVGD_MIN_COUNT, len(load_grid_district_ids())),
1129
    )
1130
1131
1132
def generate_model_data_eGon100RE_remaining():
1133
    """Generates timeseries for eGon100RE scenario for grid districts which
1134
    has not been processed in the parallel tasks before.
1135
    """
1136
    generate_model_data_bunch(
1137
        scenario_name="eGon100RE",
1138
        bunch=range(MVGD_MIN_COUNT, len(load_grid_district_ids())),
1139
    )
1140