load_evs_trips()   B
last analyzed

Complexity

Conditions 4

Size

Total Lines 83
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 49
dl 0
loc 83
rs 8.669
c 0
b 0
f 0
cc 4
nop 4

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