Passed
Pull Request — dev (#1034)
by Stephan
02:13
created

load_evs_trips()   B

Complexity

Conditions 4

Size

Total Lines 84
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

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