write_model_data_to_db()   F
last analyzed

Complexity

Conditions 15

Size

Total Lines 348
Code Lines 225

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 225
dl 0
loc 348
rs 2.0999
c 0
b 0
f 0
cc 15
nop 6

How to fix   Long Method    Complexity   

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:

Complexity

Complex classes like data.datasets.emobility.motorized_individual_travel.model_timeseries.write_model_data_to_db() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
        )
477
478
    trip_data = pd.read_sql(
479
        query.statement, query.session.bind, index_col=None
480
    ).astype(
481
        {
482
            "ev_id": "int",
483
            "park_start": "int",
484
            "park_end": "int",
485
            "drive_start": "int",
486
            "drive_end": "int",
487
        }
488
    )
489
490
    if flex_only_at_charging_events is True:
491
        # ASSUMPTION: set charging cap 0 where there's no demand
492
        # (discard other plugged-in times)
493
        mask = trip_data.charging_demand == 0
494
        trip_data.loc[mask, "charging_capacity_nominal"] = 0
495
        trip_data.loc[mask, "charging_capacity_grid"] = 0
496
        trip_data.loc[mask, "charging_capacity_battery"] = 0
497
498
    return trip_data
499
500
501
def write_model_data_to_db(
502
    static_params_dict: dict,
503
    load_time_series_df: pd.DataFrame,
504
    bus_id: int,
505
    scenario_name: str,
506
    run_config: pd.DataFrame,
507
    bat_cap: pd.DataFrame,
508
) -> None:
509
    """Write all results for grid district to database
510
511
    Parameters
512
    ----------
513
    static_params_dict : dict
514
        Static model params
515
    load_time_series_df : pd.DataFrame
516
        Load time series for grid district
517
    bus_id : int
518
        ID of grid district
519
    scenario_name : str
520
        Scenario name
521
    run_config : pd.DataFrame
522
        simBEV metadata: run config
523
    bat_cap : pd.DataFrame
524
        Battery capacities per EV type
525
526
    Returns
527
    -------
528
    None
529
    """
530
531
    def calc_initial_ev_soc(bus_id: int, scenario_name: str) -> pd.DataFrame:
532
        """Calculate an average initial state of charge for EVs in MV grid
533
        district.
534
535
        This is done by weighting the initial SoCs at timestep=0 with EV count
536
        and battery capacity for each EV type.
537
        """
538
        with db.session_scope() as session:
539
            query_ev_soc = (
540
                session.query(
541
                    EgonEvPool.type,
542
                    func.count(EgonEvTrip.egon_ev_pool_ev_id).label(
543
                        "ev_count"
544
                    ),
545
                    func.avg(EgonEvTrip.soc_start).label("ev_soc_start"),
546
                )
547
                .select_from(EgonEvTrip)
548
                .join(
549
                    EgonEvPool,
550
                    EgonEvPool.ev_id == EgonEvTrip.egon_ev_pool_ev_id,
551
                )
552
                .join(
553
                    EgonEvMvGridDistrict,
554
                    EgonEvMvGridDistrict.egon_ev_pool_ev_id
555
                    == EgonEvTrip.egon_ev_pool_ev_id,
556
                )
557
                .filter(
558
                    EgonEvTrip.scenario == scenario_name,
559
                    EgonEvPool.scenario == scenario_name,
560
                    EgonEvMvGridDistrict.scenario == scenario_name,
561
                    EgonEvMvGridDistrict.bus_id == bus_id,
562
                    EgonEvTrip.simbev_event_id == 0,
563
                )
564
                .group_by(EgonEvPool.type)
565
            )
566
567
        initial_soc_per_ev_type = pd.read_sql(
568
            query_ev_soc.statement, query_ev_soc.session.bind, index_col="type"
569
        )
570
571
        initial_soc_per_ev_type[
572
            "battery_capacity_sum"
573
        ] = initial_soc_per_ev_type.ev_count.multiply(bat_cap)
574
        initial_soc_per_ev_type[
575
            "ev_soc_start_abs"
576
        ] = initial_soc_per_ev_type.battery_capacity_sum.multiply(
577
            initial_soc_per_ev_type.ev_soc_start
578
        )
579
580
        return (
581
            initial_soc_per_ev_type.ev_soc_start_abs.sum()
582
            / initial_soc_per_ev_type.battery_capacity_sum.sum()
583
        )
584
585
    def write_to_db(write_lowflex_model: bool) -> None:
586
        """Write model data to eTraGo tables"""
587
588
        @db.check_db_unique_violation
589
        def write_bus(scenario_name: str) -> int:
590
            # eMob MIT bus
591
            emob_bus_id = db.next_etrago_id("bus")
592
            with db.session_scope() as session:
593
                session.add(
594
                    EgonPfHvBus(
595
                        scn_name=scenario_name,
596
                        bus_id=emob_bus_id,
597
                        v_nom=1,
598
                        carrier="Li_ion",
599
                        x=etrago_bus.x,
600
                        y=etrago_bus.y,
601
                        geom=etrago_bus.geom,
602
                    )
603
                )
604
            return emob_bus_id
605
606
        @db.check_db_unique_violation
607
        def write_link(scenario_name: str) -> None:
608
            # eMob MIT link [bus_el] -> [bus_ev]
609
            emob_link_id = db.next_etrago_id("link")
610
            with db.session_scope() as session:
611
                session.add(
612
                    EgonPfHvLink(
613
                        scn_name=scenario_name,
614
                        link_id=emob_link_id,
615
                        bus0=etrago_bus.bus_id,
616
                        bus1=emob_bus_id,
617
                        carrier="BEV_charger",
618
                        efficiency=float(run_config.eta_cp),
619
                        p_nom=(
620
                            load_time_series_df.simultaneous_plugged_in_charging_capacity.max()  # noqa: E501
621
                        ),
622
                        p_nom_extendable=False,
623
                        p_nom_min=0,
624
                        p_nom_max=np.Inf,
625
                        p_min_pu=0,
626
                        p_max_pu=1,
627
                        # p_set_fixed=0,
628
                        capital_cost=0,
629
                        marginal_cost=0,
630
                        length=0,
631
                        terrain_factor=1,
632
                    )
633
                )
634
            with db.session_scope() as session:
635
                session.add(
636
                    EgonPfHvLinkTimeseries(
637
                        scn_name=scenario_name,
638
                        link_id=emob_link_id,
639
                        temp_id=1,
640
                        p_min_pu=None,
641
                        p_max_pu=(
642
                            hourly_load_time_series_df.ev_availability.to_list()  # noqa: E501
643
                        ),
644
                    )
645
                )
646
647
        @db.check_db_unique_violation
648
        def write_store(scenario_name: str) -> None:
649
            # eMob MIT store
650
            emob_store_id = db.next_etrago_id("store")
651
            with db.session_scope() as session:
652
                session.add(
653
                    EgonPfHvStore(
654
                        scn_name=scenario_name,
655
                        store_id=emob_store_id,
656
                        bus=emob_bus_id,
657
                        carrier="battery_storage",
658
                        e_nom=static_params_dict["store_ev_battery.e_nom_MWh"],
659
                        e_nom_extendable=False,
660
                        e_nom_min=0,
661
                        e_nom_max=np.Inf,
662
                        e_min_pu=0,
663
                        e_max_pu=1,
664
                        e_initial=(
665
                            initial_soc_mean
666
                            * static_params_dict["store_ev_battery.e_nom_MWh"]
667
                        ),
668
                        e_cyclic=False,
669
                        sign=1,
670
                        standing_loss=0,
671
                    )
672
                )
673
            with db.session_scope() as session:
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
            with db.session_scope() as session:
701
                session.add(
702
                    EgonPfHvLoadTimeseries(
703
                        scn_name=scenario_name,
704
                        load_id=emob_load_id,
705
                        temp_id=1,
706
                        p_set=load_ts,
707
                    )
708
                )
709
710
        # Get eTraGo substation bus
711
        with db.session_scope() as session:
712
            query = session.query(
713
                EgonPfHvBus.scn_name,
714
                EgonPfHvBus.bus_id,
715
                EgonPfHvBus.x,
716
                EgonPfHvBus.y,
717
                EgonPfHvBus.geom,
718
            ).filter(
719
                EgonPfHvBus.scn_name == scenario_name,
720
                EgonPfHvBus.bus_id == bus_id,
721
                EgonPfHvBus.carrier == "AC",
722
            )
723
            etrago_bus = query.first()
724
            if etrago_bus is None:
725
                # TODO: raise exception here!
726
                print(
727
                    f"No AC bus found for scenario {scenario_name} "
728
                    f"with bus_id {bus_id} in table egon_etrago_bus!"
729
                )
730
731
        # Call DB writing functions for regular or lowflex scenario
732
        # * use corresponding scenario name as defined in datasets.yml
733
        # * no storage for lowflex scenario
734
        # * load timeseries:
735
        #   * regular (flex): use driving load
736
        #   * lowflex: use dumb charging load
737
        if write_lowflex_model is False:
738
            emob_bus_id = write_bus(scenario_name=scenario_name)
739
            write_link(scenario_name=scenario_name)
740
            write_store(scenario_name=scenario_name)
741
            write_load(
742
                scenario_name=scenario_name,
743
                connection_bus_id=emob_bus_id,
744
                load_ts=(
745
                    hourly_load_time_series_df.driving_load_time_series.to_list()  # noqa: E501
746
                ),
747
            )
748
        else:
749
            # Get lowflex scenario name
750
            lowflex_scenario_name = DATASET_CFG["scenario"]["lowflex"][
751
                "names"
752
            ][scenario_name]
753
            write_load(
754
                scenario_name=lowflex_scenario_name,
755
                connection_bus_id=etrago_bus.bus_id,
756
                load_ts=hourly_load_time_series_df.load_time_series.to_list(),
757
            )
758
759
    def write_to_file():
760
        """Write model data to file (for debugging purposes)"""
761
        results_dir = WORKING_DIR / Path("results", scenario_name, str(bus_id))
762
        results_dir.mkdir(exist_ok=True, parents=True)
763
764
        hourly_load_time_series_df[["load_time_series"]].to_csv(
765
            results_dir / "ev_load_time_series.csv"
766
        )
767
        hourly_load_time_series_df[["ev_availability"]].to_csv(
768
            results_dir / "ev_availability.csv"
769
        )
770
        hourly_load_time_series_df[["soc_min", "soc_max"]].to_csv(
771
            results_dir / "ev_dsm_profile.csv"
772
        )
773
774
        static_params_dict[
775
            "load_land_transport_ev.p_set_MW"
776
        ] = "ev_load_time_series.csv"
777
        static_params_dict["link_bev_charger.p_max_pu"] = "ev_availability.csv"
778
        static_params_dict["store_ev_battery.e_min_pu"] = "ev_dsm_profile.csv"
779
        static_params_dict["store_ev_battery.e_max_pu"] = "ev_dsm_profile.csv"
780
781
        file = results_dir / "ev_static_params.json"
782
783
        with open(file, "w") as f:
784
            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...
785
786
    print("  Writing model timeseries...")
787
    load_time_series_df = load_time_series_df.assign(
788
        ev_availability=(
789
            load_time_series_df.simultaneous_plugged_in_charging_capacity
790
            / static_params_dict["link_bev_charger.p_nom_MW"]
791
        )
792
    )
793
794
    # Resample to 1h
795
    hourly_load_time_series_df = load_time_series_df.resample("1H").agg(
796
        {
797
            "load_time_series": np.mean,
798
            "flex_time_series": np.mean,
799
            "simultaneous_plugged_in_charging_capacity": np.mean,
800
            "simultaneous_plugged_in_charging_capacity_flex": np.mean,
801
            "soc_min_absolute": np.min,
802
            "soc_max_absolute": np.max,
803
            "ev_availability": np.mean,
804
            "driving_load_time_series": np.sum,
805
        }
806
    )
807
808
    # Create relative SoC timeseries
809
    hourly_load_time_series_df = hourly_load_time_series_df.assign(
810
        soc_min=hourly_load_time_series_df.soc_min_absolute.div(
811
            static_params_dict["store_ev_battery.e_nom_MWh"]
812
        ),
813
        soc_max=hourly_load_time_series_df.soc_max_absolute.div(
814
            static_params_dict["store_ev_battery.e_nom_MWh"]
815
        ),
816
    )
817
    hourly_load_time_series_df = hourly_load_time_series_df.assign(
818
        soc_delta_absolute=(
819
            hourly_load_time_series_df.soc_max_absolute
820
            - hourly_load_time_series_df.soc_min_absolute
821
        ),
822
        soc_delta=(
823
            hourly_load_time_series_df.soc_max
824
            - hourly_load_time_series_df.soc_min
825
        ),
826
    )
827
828
    # Crop hourly TS if needed
829
    hourly_load_time_series_df = hourly_load_time_series_df[:8760]
830
831
    # Create lowflex scenario?
832
    write_lowflex_model = DATASET_CFG["scenario"]["lowflex"][
833
        "create_lowflex_scenario"
834
    ]
835
836
    # Get initial average storage SoC
837
    initial_soc_mean = calc_initial_ev_soc(bus_id, scenario_name)
838
839
    # Write to database: regular and lowflex scenario
840
    write_to_db(write_lowflex_model=False)
841
    print("    Writing flex scenario...")
842
    if write_lowflex_model is True:
843
        print("    Writing lowflex scenario...")
844
        write_to_db(write_lowflex_model=True)
845
846
    # Export to working dir if requested
847
    if DATASET_CFG["model_timeseries"]["export_results_to_csv"]:
848
        write_to_file()
849
850
851
def delete_model_data_from_db():
852
    """Delete all eMob MIT data from eTraGo PF tables"""
853
    with db.session_scope() as session:
854
        # Buses
855
        session.query(EgonPfHvBus).filter(
856
            EgonPfHvBus.carrier == "Li_ion"
857
        ).delete(synchronize_session=False)
858
859
        # Link TS
860
        subquery = (
861
            session.query(EgonPfHvLink.link_id)
862
            .filter(EgonPfHvLink.carrier == "BEV_charger")
863
            .subquery()
864
        )
865
866
        session.query(EgonPfHvLinkTimeseries).filter(
867
            EgonPfHvLinkTimeseries.link_id.in_(subquery)
868
        ).delete(synchronize_session=False)
869
        # Links
870
        session.query(EgonPfHvLink).filter(
871
            EgonPfHvLink.carrier == "BEV_charger"
872
        ).delete(synchronize_session=False)
873
874
        # Store TS
875
        subquery = (
876
            session.query(EgonPfHvStore.store_id)
877
            .filter(EgonPfHvStore.carrier == "battery_storage")
878
            .subquery()
879
        )
880
881
        session.query(EgonPfHvStoreTimeseries).filter(
882
            EgonPfHvStoreTimeseries.store_id.in_(subquery)
883
        ).delete(synchronize_session=False)
884
        # Stores
885
        session.query(EgonPfHvStore).filter(
886
            EgonPfHvStore.carrier == "battery_storage"
887
        ).delete(synchronize_session=False)
888
889
        # Load TS
890
        subquery = (
891
            session.query(EgonPfHvLoad.load_id)
892
            .filter(EgonPfHvLoad.carrier == "land_transport_EV")
893
            .subquery()
894
        )
895
896
        session.query(EgonPfHvLoadTimeseries).filter(
897
            EgonPfHvLoadTimeseries.load_id.in_(subquery)
898
        ).delete(synchronize_session=False)
899
        # Loads
900
        session.query(EgonPfHvLoad).filter(
901
            EgonPfHvLoad.carrier == "land_transport_EV"
902
        ).delete(synchronize_session=False)
903
904
905
def load_grid_district_ids() -> pd.Series:
906
    """Load bus IDs of all grid districts"""
907
    with db.session_scope() as session:
908
        query_mvgd = session.query(MvGridDistricts.bus_id)
909
    return pd.read_sql(
910
        query_mvgd.statement, query_mvgd.session.bind, index_col=None
911
    ).bus_id.sort_values()
912
913
914
def generate_model_data_grid_district(
915
    scenario_name: str,
916
    evs_grid_district: pd.DataFrame,
917
    bat_cap_dict: dict,
918
    run_config: pd.DataFrame,
919
) -> tuple:
920
    """Generates timeseries from simBEV trip data for MV grid district
921
922
    Parameters
923
    ----------
924
    scenario_name : str
925
        Scenario name
926
    evs_grid_district : pd.DataFrame
927
        EV data for grid district
928
    bat_cap_dict : dict
929
        Battery capacity per EV type
930
    run_config : pd.DataFrame
931
        simBEV metadata: run config
932
933
    Returns
934
    -------
935
    pd.DataFrame
936
        Model data for grid district
937
    """
938
939
    # Load trip data
940
    print("  Loading trips...")
941
    trip_data = load_evs_trips(
942
        scenario_name=scenario_name,
943
        evs_ids=evs_grid_district.ev_id.unique(),
944
        charging_events_only=False,
945
        flex_only_at_charging_events=True,
946
    )
947
948
    print("  Preprocessing data...")
949
    # Assign battery capacity to trip data
950
    trip_data["bat_cap"] = trip_data.type.apply(lambda _: bat_cap_dict[_])
951
    trip_data.drop(columns=["type"], inplace=True)
952
953
    # Preprocess trip data
954
    trip_data = data_preprocessing(evs_grid_district, trip_data)
955
956
    # Generate load timeseries
957
    print("  Generating load timeseries...")
958
    load_ts = generate_load_time_series(
959
        ev_data_df=trip_data,
960
        run_config=run_config,
961
        scenario_data=evs_grid_district,
962
    )
963
964
    # Generate static params
965
    static_params = generate_static_params(
966
        trip_data, load_ts, evs_grid_district
967
    )
968
969
    return static_params, load_ts
970
971
972
def generate_model_data_bunch(scenario_name: str, bunch: range) -> None:
973
    """Generates timeseries from simBEV trip data for a bunch of MV grid
974
    districts.
975
976
    Parameters
977
    ----------
978
    scenario_name : str
979
        Scenario name
980
    bunch : list
981
        Bunch of grid districts to generate data for, e.g. [1,2,..,100].
982
        Note: `bunch` is NOT a list of grid districts but is used for slicing
983
        the ordered list (by bus_id) of grid districts! This is used for
984
        parallelization. See
985
        :meth:`egon.data.datasets.emobility.motorized_individual_travel.MotorizedIndividualTravel.generate_model_data_tasks`
986
    """
987
    # Get list of grid districts / substations for this bunch
988
    mvgd_bus_ids = load_grid_district_ids().iloc[bunch]
989
990
    # Get scenario variation name
991
    scenario_var_name = DATASET_CFG["scenario"]["variation"][scenario_name]
992
993
    print(
994
        f"SCENARIO: {scenario_name}, "
995
        f"SCENARIO VARIATION: {scenario_var_name}, "
996
        f"BUNCH: {bunch[0]}-{bunch[-1]}"
997
    )
998
999
    # Load scenario params for scenario and scenario variation
1000
    # scenario_variation_parameters = get_sector_parameters(
1001
    #    "mobility", scenario=scenario_name
1002
    # )["motorized_individual_travel"][scenario_var_name]
1003
1004
    # Get substations
1005
    with db.session_scope() as session:
1006
        query = (
1007
            session.query(
1008
                EgonEvMvGridDistrict.bus_id,
1009
                EgonEvMvGridDistrict.egon_ev_pool_ev_id.label("ev_id"),
1010
            )
1011
            .filter(EgonEvMvGridDistrict.scenario == scenario_name)
1012
            .filter(
1013
                EgonEvMvGridDistrict.scenario_variation == scenario_var_name
1014
            )
1015
            .filter(EgonEvMvGridDistrict.bus_id.in_(mvgd_bus_ids))
1016
            .filter(EgonEvMvGridDistrict.egon_ev_pool_ev_id.isnot(None))
1017
        )
1018
    evs_grid_district = pd.read_sql(
1019
        query.statement, query.session.bind, index_col=None
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