eflips-eval 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- LICENSE.md +660 -0
- eflips/eval/input/__init__.py +0 -0
- eflips/eval/input/prepare.py +86 -0
- eflips/eval/input/visualize.py +39 -0
- eflips/eval/output/__init__.py +0 -0
- eflips/eval/output/prepare.py +388 -0
- eflips/eval/output/visualize.py +284 -0
- eflips_eval-0.1.2.dist-info/LICENSE.md +660 -0
- eflips_eval-0.1.2.dist-info/METADATA +109 -0
- eflips_eval-0.1.2.dist-info/RECORD +11 -0
- eflips_eval-0.1.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
import sqlalchemy
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from eflips.model import Rotation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def rotation_name_for_sorting(rotation_name: str) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Takes a rotation name, which in the BVG is a string of two numbers separated by a '/' character, and returns a string
|
|
13
|
+
that can be used for sorting. The first part of the string is returned for BVG rotations.
|
|
14
|
+
|
|
15
|
+
Other rotation names are not supported and will return the rotation name itself.
|
|
16
|
+
|
|
17
|
+
:param rotation_name: The rotation name
|
|
18
|
+
:return: a sortable string
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
if rotation_name is not None and "/" in rotation_name:
|
|
22
|
+
return rotation_name.split("/")[0]
|
|
23
|
+
else:
|
|
24
|
+
return rotation_name
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def rotation_info(
|
|
28
|
+
scenario_id: int,
|
|
29
|
+
session: sqlalchemy.orm.session.Session,
|
|
30
|
+
rotation_ids: None | int | List[int] = None,
|
|
31
|
+
) -> pd.DataFrame:
|
|
32
|
+
"""
|
|
33
|
+
This function provides information about the rotations in a scenario. This information can be provided even before
|
|
34
|
+
the simulation has been run. It creates a dataframe with the following columns:
|
|
35
|
+
|
|
36
|
+
- rotation_id: the id of the rotation
|
|
37
|
+
- rotation_name: the name of the rotation
|
|
38
|
+
- vehicle_type_id: the id of the vehicle type
|
|
39
|
+
- vehicle_type_name: the name of the vehicle type
|
|
40
|
+
- total_distance: the total distance of the rotation
|
|
41
|
+
- time_start: the departure of the first trip
|
|
42
|
+
- time_end: the arrival of the last trip
|
|
43
|
+
- line_name: the name of the line, which is the first part of the rotation name. Used for sorting
|
|
44
|
+
|
|
45
|
+
:param scenario_id: The scenario id for which to create the dataframe
|
|
46
|
+
:param session: An sqlalchemy session to an eflips-model database
|
|
47
|
+
:param rotation_ids: A list of rotation ids to filter for. If None, all rotations are included
|
|
48
|
+
:return: a pandas DataFrame
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
result: List[Dict[str, int | float | str | datetime]] = []
|
|
52
|
+
|
|
53
|
+
rotations = session.query(Rotation).filter(Rotation.scenario_id == scenario_id)
|
|
54
|
+
|
|
55
|
+
if rotation_ids is not None:
|
|
56
|
+
if isinstance(rotation_ids, int):
|
|
57
|
+
rotation_ids = [rotation_ids]
|
|
58
|
+
rotations = rotations.filter(Rotation.id.in_(rotation_ids))
|
|
59
|
+
|
|
60
|
+
for rotation in rotations:
|
|
61
|
+
# The rotation distance comes form the routes of the trips
|
|
62
|
+
distance = 0.0
|
|
63
|
+
for trip in rotation.trips:
|
|
64
|
+
distance += trip.route.distance / 1000
|
|
65
|
+
|
|
66
|
+
result.append(
|
|
67
|
+
{
|
|
68
|
+
"rotation_id": rotation.id,
|
|
69
|
+
"rotation_name": rotation.name,
|
|
70
|
+
"vehicle_type_id": rotation.vehicle_type_id,
|
|
71
|
+
"vehicle_type_name": rotation.vehicle_type.name,
|
|
72
|
+
"total_distance": distance,
|
|
73
|
+
"time_start": rotation.trips[0].departure_time,
|
|
74
|
+
"time_end": rotation.trips[-1].arrival_time,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# We want to properly sort by rotation name, which is a bit intricate, as it's a string of two numbers divided by a
|
|
79
|
+
# '/' character. We can't just sort by the string, as "10/11" would come after "10/1". We need to split the string
|
|
80
|
+
# into its components and sort by them.
|
|
81
|
+
df = pd.DataFrame(result)
|
|
82
|
+
df["line_name"] = df["rotation_name"].apply(rotation_name_for_sorting)
|
|
83
|
+
|
|
84
|
+
df.sort_values(by=["line_name", "time_start"], inplace=True)
|
|
85
|
+
|
|
86
|
+
return df
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import plotly.express as px # type: ignore
|
|
3
|
+
import plotly.graph_objs as go # type: ignore
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def rotation_info(prepared_data: pd.DataFrame) -> go.Figure:
|
|
7
|
+
"""
|
|
8
|
+
This function visualizes the rotation information using plotly
|
|
9
|
+
|
|
10
|
+
:param prepared_data: The result of the rotation_info function, a dataframe with the following columns:
|
|
11
|
+
|
|
12
|
+
- rotation_id: the id of the rotation
|
|
13
|
+
- rotation_name: the name of the rotation
|
|
14
|
+
- vehicle_type_id: the id of the vehicle type
|
|
15
|
+
- vehicle_type_name: the name of the vehicle type
|
|
16
|
+
- total_distance: the total distance of the rotation
|
|
17
|
+
- time_start: the departure of the first trip
|
|
18
|
+
- time_end: the arrival of the last trip
|
|
19
|
+
- line_name: the name of the line, which is the first part of the rotation name. Used for sorting
|
|
20
|
+
|
|
21
|
+
:return: A plotly figure object
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
fig = px.timeline(
|
|
25
|
+
prepared_data,
|
|
26
|
+
x_start="time_start",
|
|
27
|
+
x_end="time_end",
|
|
28
|
+
y="rotation_name",
|
|
29
|
+
color="total_distance",
|
|
30
|
+
labels={
|
|
31
|
+
"rotation_name": "Rotation Name",
|
|
32
|
+
"total_distance": "Total Distance (km)",
|
|
33
|
+
"line_name": "Line Name",
|
|
34
|
+
},
|
|
35
|
+
hover_data=["vehicle_type_name"],
|
|
36
|
+
pattern_shape="line_name",
|
|
37
|
+
)
|
|
38
|
+
fig.update_layout(legend_orientation="h")
|
|
39
|
+
return fig
|
|
File without changes
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Dict, List, Iterable, Tuple
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import numpy.typing as npt
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import sqlalchemy
|
|
8
|
+
from eflips.model import Event, Rotation, Vehicle, Trip, EventType
|
|
9
|
+
from sqlalchemy.orm import Session
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def departure_arrival_soc(
|
|
13
|
+
scenario_id: int, session: sqlalchemy.orm.session.Session
|
|
14
|
+
) -> pd.DataFrame:
|
|
15
|
+
"""
|
|
16
|
+
This function creates a dataframe with the SoC at departure and arrival for each trip.
|
|
17
|
+
The columns are
|
|
18
|
+
- event_id: the associated event id
|
|
19
|
+
- rotation_id: the associated rotation id (for the trip the vehicle is going on or returning from)
|
|
20
|
+
- rotation_name: the name of the rotation
|
|
21
|
+
- vehicle_type_id: the vehicle type id
|
|
22
|
+
- vehicle_type_name: the name of the vehicle type
|
|
23
|
+
- vehicle_id: the vehicle id
|
|
24
|
+
- vehicle_name: the name of the vehicle
|
|
25
|
+
- time: the time at which this SoC was recorded (for departure, this is the departure time from the depot, for
|
|
26
|
+
arrival, this is the arrival time at the depot)
|
|
27
|
+
- soc: the state of charge at the given time
|
|
28
|
+
- event_type: the type of event, either "Departure" or "Arrival"
|
|
29
|
+
|
|
30
|
+
:param scenario_id: The scenario id for which to create the dataframe
|
|
31
|
+
:param session: An sqlalchemy session to an eflips-model database
|
|
32
|
+
|
|
33
|
+
:return: A pandas DataFrame
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
vehicles = session.query(Vehicle).filter(Vehicle.scenario_id == scenario_id)
|
|
37
|
+
result: List[Dict[str, str | int | float | datetime]] = []
|
|
38
|
+
|
|
39
|
+
for vehicle in vehicles:
|
|
40
|
+
events = (
|
|
41
|
+
session.query(Event)
|
|
42
|
+
.filter(Event.vehicle_id == vehicle.id)
|
|
43
|
+
.order_by(Event.time_start)
|
|
44
|
+
.all()
|
|
45
|
+
)
|
|
46
|
+
for i in range(len(events)):
|
|
47
|
+
# The interesting events for us are the ones immediately before or after an event at a depot.
|
|
48
|
+
# That means the previous or next event has an area_id, the event does not
|
|
49
|
+
|
|
50
|
+
if events[i].area_id is None:
|
|
51
|
+
if i > 0 and events[i - 1].area_id is not None:
|
|
52
|
+
# This is a departure event
|
|
53
|
+
result.append(
|
|
54
|
+
{
|
|
55
|
+
"rotation_id": events[i].trip.rotation_id,
|
|
56
|
+
"rotation_name": events[i].trip.rotation.name,
|
|
57
|
+
"vehicle_type_id": vehicle.vehicle_type_id,
|
|
58
|
+
"vehicle_type_name": vehicle.vehicle_type.name,
|
|
59
|
+
"vehicle_id": vehicle.id,
|
|
60
|
+
"vehicle_name": vehicle.name,
|
|
61
|
+
"time": events[i].time_start,
|
|
62
|
+
"soc": events[i].soc_start * 100,
|
|
63
|
+
"event_type": "Departure",
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
elif i < len(events) - 1 and events[i + 1].area_id is not None:
|
|
67
|
+
# This is an arrival event
|
|
68
|
+
result.append(
|
|
69
|
+
{
|
|
70
|
+
"rotation_id": events[i].trip.rotation_id,
|
|
71
|
+
"rotation_name": events[i].trip.rotation.name,
|
|
72
|
+
"vehicle_type_id": vehicle.vehicle_type_id,
|
|
73
|
+
"vehicle_type_name": vehicle.vehicle_type.name,
|
|
74
|
+
"vehicle_id": vehicle.id,
|
|
75
|
+
"vehicle_name": vehicle.name,
|
|
76
|
+
"time": events[i].time_end,
|
|
77
|
+
"soc": events[i].soc_end * 100,
|
|
78
|
+
"event_type": "Arrival",
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return pd.DataFrame(result)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def depot_event(
|
|
86
|
+
scenario_id: int, session: Session, vehicle_ids: None | int | List[int] = None
|
|
87
|
+
) -> pd.DataFrame:
|
|
88
|
+
"""
|
|
89
|
+
This function creates a dataframe with all the events at the depot for a given scenario.
|
|
90
|
+
The columns are
|
|
91
|
+
- time_start: the start time of the event in datetime format
|
|
92
|
+
- time_end: the end time of the event in datetime format
|
|
93
|
+
- vehicle_id: the unique vehicle identifier which could be used for querying the vehicle in the database
|
|
94
|
+
- event_type: the type of event specified in the eflips model. See :class:`eflips.model.EventType` for more information
|
|
95
|
+
- area_id: the unique area identifier which could be used for querying the area in the database
|
|
96
|
+
- trip_id: the unique trip identifier which could be used for querying the trip in the database
|
|
97
|
+
- station_id: the unique station identifier which could be used for querying the station in the database
|
|
98
|
+
- location: the location of the event. This could be "depot", "trip" or "station"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
:param scenario_id: The unique identifier of the scenario
|
|
102
|
+
:param session: A :class:`sqlalchemy.orm.session.Session` object to an eflips-model database
|
|
103
|
+
:param vehicle_ids: A list of vehicle ids to filter for. If None, all vehicles are included
|
|
104
|
+
:return: A pandas DataFrame
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
event_list_for_plot: List[Dict[str, int | float | str | datetime | None]] = []
|
|
108
|
+
events_from_db_q = (
|
|
109
|
+
session.query(Event)
|
|
110
|
+
.filter(Event.scenario_id == scenario_id)
|
|
111
|
+
.order_by(Event.vehicle_id, Event.time_start)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if vehicle_ids is not None:
|
|
115
|
+
if isinstance(vehicle_ids, int):
|
|
116
|
+
vehicle_ids = [vehicle_ids]
|
|
117
|
+
events_from_db_q = events_from_db_q.filter(Event.vehicle_id.in_(vehicle_ids))
|
|
118
|
+
|
|
119
|
+
events_from_db = events_from_db_q.all()
|
|
120
|
+
|
|
121
|
+
for event in events_from_db:
|
|
122
|
+
location = None
|
|
123
|
+
if event.area_id is not None:
|
|
124
|
+
location = "Depot"
|
|
125
|
+
elif event.trip_id is not None:
|
|
126
|
+
location = "Trip"
|
|
127
|
+
elif event.station_id is not None:
|
|
128
|
+
location = "Station"
|
|
129
|
+
|
|
130
|
+
event_list_for_plot.append(
|
|
131
|
+
{
|
|
132
|
+
"time_start": event.time_start,
|
|
133
|
+
"time_end": event.time_end,
|
|
134
|
+
"soc_start": event.soc_start,
|
|
135
|
+
"soc_end": event.soc_end,
|
|
136
|
+
"vehicle_id": str(event.vehicle_id),
|
|
137
|
+
"vehicle_type_id": event.vehicle.vehicle_type_id,
|
|
138
|
+
"vehicle_type_name": event.vehicle.vehicle_type.name,
|
|
139
|
+
"event_type": event.event_type.name.replace("_", " ").title(),
|
|
140
|
+
"area_id": event.area_id,
|
|
141
|
+
"trip_id": event.trip_id,
|
|
142
|
+
"station_id": event.station_id,
|
|
143
|
+
"location": location,
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
df = pd.DataFrame(event_list_for_plot)
|
|
148
|
+
df.sort_values(by=["vehicle_type_name", "time_start"], inplace=True)
|
|
149
|
+
|
|
150
|
+
return df
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def power_and_occupancy(
|
|
154
|
+
aread_id: int | Iterable[int],
|
|
155
|
+
session: sqlalchemy.orm.session.Session,
|
|
156
|
+
temporal_resolution: int = 60,
|
|
157
|
+
) -> pd.DataFrame:
|
|
158
|
+
"""
|
|
159
|
+
This function creates a dataframe containing a timeseries of the power and occupancy of the given area(s).
|
|
160
|
+
The columns are:
|
|
161
|
+
- time: the time at which the data was recorded
|
|
162
|
+
- power: the summed power consumption of the area(s) at the given time
|
|
163
|
+
- occupancy: the summed occupancy of the area(s) at the given time
|
|
164
|
+
|
|
165
|
+
:param aread_id: The id of the area for which to create the dataframe
|
|
166
|
+
:param session: An sqlalchemy session to an eflips-model database
|
|
167
|
+
:param temporal_resolution: The temporal resolution of the timeseries in seconds. Default is 60 seconds.
|
|
168
|
+
:return: A pandas DataFrame
|
|
169
|
+
"""
|
|
170
|
+
if isinstance(aread_id, int):
|
|
171
|
+
aread_id = [aread_id]
|
|
172
|
+
events = session.query(Event).filter(Event.area_id.in_(aread_id))
|
|
173
|
+
|
|
174
|
+
start_time_row = (
|
|
175
|
+
session.query(Event.time_start)
|
|
176
|
+
.filter(Event.area_id.in_(aread_id))
|
|
177
|
+
.order_by(Event.time_start)
|
|
178
|
+
.first()
|
|
179
|
+
)
|
|
180
|
+
if start_time_row is None:
|
|
181
|
+
raise ValueError("No events found for the given area_id")
|
|
182
|
+
start_time = start_time_row[0] # Oh, if we had nullability operators in Python…
|
|
183
|
+
end_time_row = (
|
|
184
|
+
session.query(Event.time_end)
|
|
185
|
+
.filter(Event.area_id.in_(aread_id))
|
|
186
|
+
.order_by(Event.time_end.desc())
|
|
187
|
+
.first()
|
|
188
|
+
)
|
|
189
|
+
if end_time_row is None:
|
|
190
|
+
raise ValueError("No events found for the given area_id")
|
|
191
|
+
end_time = end_time_row[0] # Oh, if we had nullability operators in Python…
|
|
192
|
+
|
|
193
|
+
# Round the start and end times to the nearest temporal_resolution
|
|
194
|
+
start_time = start_time - timedelta(seconds=start_time.second % temporal_resolution)
|
|
195
|
+
end_time = end_time + timedelta(
|
|
196
|
+
seconds=temporal_resolution - end_time.second % temporal_resolution
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Create a 1-second interval time series
|
|
200
|
+
time: npt.NDArray[np.datetime64] = np.arange(
|
|
201
|
+
start_time, end_time, timedelta(seconds=temporal_resolution)
|
|
202
|
+
)
|
|
203
|
+
time_as_unix = np.arange(
|
|
204
|
+
start_time.timestamp(), end_time.timestamp(), temporal_resolution
|
|
205
|
+
)
|
|
206
|
+
energy = np.zeros(time.shape[0])
|
|
207
|
+
occupancy = np.zeros(time.shape[0])
|
|
208
|
+
|
|
209
|
+
# For each event:
|
|
210
|
+
# Convert SoC to energy
|
|
211
|
+
# Resample to 1-second intervals
|
|
212
|
+
# Add the energy to the energy series
|
|
213
|
+
for event in events:
|
|
214
|
+
if event.timeseries is not None:
|
|
215
|
+
this_event_times: List[datetime] = [datetime.fromisoformat(t) for t in event.timeseries["time"]] # type: ignore
|
|
216
|
+
this_event_socs: List[float] = event.timeseries["soc"] # type: ignore
|
|
217
|
+
else:
|
|
218
|
+
this_event_times = []
|
|
219
|
+
this_event_socs = []
|
|
220
|
+
# Attach the event's time_start and soc to the timeseries at the beginning
|
|
221
|
+
this_event_times.insert(0, event.time_start)
|
|
222
|
+
this_event_socs.insert(0, event.soc_start)
|
|
223
|
+
# Attach the event's time_end and soc to the timeseries at the end
|
|
224
|
+
this_event_times.append(event.time_end)
|
|
225
|
+
this_event_socs.append(event.soc_end)
|
|
226
|
+
this_event_unix_times = np.array([t.timestamp() for t in this_event_times])
|
|
227
|
+
|
|
228
|
+
# Validation: the timeseries should be sorted and the socs should be in the range [0, 1] and monotonically increasing
|
|
229
|
+
assert all(
|
|
230
|
+
this_event_times[i] <= this_event_times[i + 1]
|
|
231
|
+
for i in range(len(this_event_times) - 1)
|
|
232
|
+
)
|
|
233
|
+
assert all(0 <= this_event_socs[i] <= 1 for i in range(len(this_event_socs)))
|
|
234
|
+
assert all(
|
|
235
|
+
this_event_socs[i] <= this_event_socs[i + 1]
|
|
236
|
+
for i in range(len(this_event_socs) - 1)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Convert from SoC to enerhgy using the vehicle types battery capacity
|
|
240
|
+
this_event_energy = (
|
|
241
|
+
np.array(this_event_socs) * event.vehicle.vehicle_type.battery_capacity
|
|
242
|
+
) # kWh
|
|
243
|
+
|
|
244
|
+
# Resample the energy to 1-second intervals
|
|
245
|
+
this_event_energy = np.interp(
|
|
246
|
+
time_as_unix, this_event_unix_times, this_event_energy
|
|
247
|
+
)
|
|
248
|
+
energy += this_event_energy
|
|
249
|
+
|
|
250
|
+
# For occupancy, we we create an entry at the beginning and end of the event, then resample to 1-second intervals
|
|
251
|
+
# with left=0 and right=0
|
|
252
|
+
this_event_occupancy = np.interp(
|
|
253
|
+
time_as_unix,
|
|
254
|
+
[event.time_start.timestamp(), event.time_end.timestamp()],
|
|
255
|
+
[1, 1],
|
|
256
|
+
left=0,
|
|
257
|
+
right=0,
|
|
258
|
+
)
|
|
259
|
+
occupancy += this_event_occupancy
|
|
260
|
+
|
|
261
|
+
# Calculate the power from the energy
|
|
262
|
+
power = (np.diff(energy) / np.diff(time_as_unix).astype(float)) * 3600 # kW
|
|
263
|
+
|
|
264
|
+
# Create the dataframe
|
|
265
|
+
result = pd.DataFrame(
|
|
266
|
+
{
|
|
267
|
+
"time": time[1:],
|
|
268
|
+
"power": power,
|
|
269
|
+
"occupancy": occupancy[1:],
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def specific_energy_consumption(scenario_id: int, session: Session) -> pd.DataFrame:
|
|
276
|
+
"""
|
|
277
|
+
Creates a dataframe of all the trip energy consumptions and distances for the given scenario.
|
|
278
|
+
The dataframe contains the following columns:
|
|
279
|
+
- trip_id: the unique identifier of the trip
|
|
280
|
+
- route_id: the unique identifier of the route
|
|
281
|
+
- route_name: the name of the route
|
|
282
|
+
- distance: the distance of the route in km
|
|
283
|
+
- energy_consumption: the energy consumption of the trip in kWh
|
|
284
|
+
- vehicle_type_id: the unique identifier of the vehicle type
|
|
285
|
+
- vehicle_type_name: the name of the vehicle type
|
|
286
|
+
|
|
287
|
+
:param scenario_id: The unique identifier of the scenario
|
|
288
|
+
:param session: An sqlalchemy session to an eflips-model database
|
|
289
|
+
:return: A pandas DataFrame
|
|
290
|
+
"""
|
|
291
|
+
events = (
|
|
292
|
+
session.query(Event)
|
|
293
|
+
.filter(Event.scenario_id == scenario_id)
|
|
294
|
+
.filter(Event.event_type == EventType.DRIVING)
|
|
295
|
+
.options(sqlalchemy.orm.joinedload(Event.trip).joinedload(Trip.route))
|
|
296
|
+
)
|
|
297
|
+
result: List[Dict[str, int | float | str]] = []
|
|
298
|
+
for event in events:
|
|
299
|
+
trip = event.trip
|
|
300
|
+
delta_soc = event.soc_end - event.soc_start
|
|
301
|
+
delta_energy = delta_soc * trip.rotation.vehicle_type.battery_capacity * -1
|
|
302
|
+
result.append(
|
|
303
|
+
{
|
|
304
|
+
"trip_id": trip.id,
|
|
305
|
+
"route_id": trip.route_id,
|
|
306
|
+
"route_name": trip.route.name,
|
|
307
|
+
"distance": trip.route.distance / 1000,
|
|
308
|
+
"energy_consumption": delta_energy,
|
|
309
|
+
"vehicle_type_id": trip.rotation.vehicle_type_id,
|
|
310
|
+
"vehicle_type_name": trip.rotation.vehicle_type.name,
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
return pd.DataFrame(result)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def vehicle_soc(
|
|
317
|
+
vehicle_id: int, session: Session
|
|
318
|
+
) -> Tuple[pd.DataFrame, Dict[str, List[Tuple[str, datetime, datetime]]]]:
|
|
319
|
+
"""
|
|
320
|
+
This function takes in a vehicle id and returns a description what happened to the vehicle over time.
|
|
321
|
+
The dataframe contains the following columns:
|
|
322
|
+
- time: the time at which the SoC was recorded
|
|
323
|
+
- soc: the state of charge at the given time
|
|
324
|
+
|
|
325
|
+
Additionally, a dictionary for the different kinds of events is returned. For each kind of event, a list of Tuples
|
|
326
|
+
with a description of the event, the start time and the end time is returned.
|
|
327
|
+
|
|
328
|
+
The kinds of events are:
|
|
329
|
+
- "rotation": A list of rotation names and the time the rotation started and ended
|
|
330
|
+
- "charging": A list of the location of the charging and the time the charging started and ended
|
|
331
|
+
|
|
332
|
+
:param vehicle_id: the unique identifier of the vehicle
|
|
333
|
+
:param session: A :class:`sqlalchemy.orm.session.Session` object to an eflips-model database
|
|
334
|
+
:return: A pandas DataFrame
|
|
335
|
+
"""
|
|
336
|
+
events_from_db = (
|
|
337
|
+
session.query(Event)
|
|
338
|
+
.filter(Event.vehicle_id == vehicle_id)
|
|
339
|
+
.order_by(Event.time_start)
|
|
340
|
+
.all()
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
descriptions: Dict[str, List[Tuple[str, datetime, datetime]]] = {
|
|
344
|
+
"rotation": [],
|
|
345
|
+
"charging": [],
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# Go through all events and connect the soc_start and soc_end and time_start and time_end
|
|
349
|
+
all_times = []
|
|
350
|
+
all_soc = []
|
|
351
|
+
for event in events_from_db:
|
|
352
|
+
all_times.append(event.time_start)
|
|
353
|
+
all_times.append(event.time_end)
|
|
354
|
+
|
|
355
|
+
if event.timeseries is not None:
|
|
356
|
+
this_event_times: List[datetime] = [
|
|
357
|
+
datetime.fromisoformat(t) for t in event.timeseries["time"] # type: ignore
|
|
358
|
+
]
|
|
359
|
+
this_event_socs: List[float] = event.timeseries["soc"] # type: ignore
|
|
360
|
+
all_times.extend(this_event_times)
|
|
361
|
+
all_soc.extend(this_event_socs)
|
|
362
|
+
|
|
363
|
+
all_soc.append(event.soc_start)
|
|
364
|
+
all_soc.append(event.soc_end)
|
|
365
|
+
|
|
366
|
+
if (
|
|
367
|
+
event.event_type == EventType.CHARGING_DEPOT
|
|
368
|
+
or event.event_type == EventType.CHARGING_OPPORTUNITY
|
|
369
|
+
):
|
|
370
|
+
if event.area is not None:
|
|
371
|
+
name = event.area.name + " in " + event.area.depot.name
|
|
372
|
+
else:
|
|
373
|
+
name = event.station.name
|
|
374
|
+
|
|
375
|
+
descriptions["charging"].append((name, event.time_start, event.time_end))
|
|
376
|
+
|
|
377
|
+
for rotation in (
|
|
378
|
+
session.query(Rotation).filter(Rotation.vehicle_id == vehicle_id).all()
|
|
379
|
+
):
|
|
380
|
+
descriptions["rotation"].append(
|
|
381
|
+
(
|
|
382
|
+
rotation.name,
|
|
383
|
+
rotation.trips[0].departure_time,
|
|
384
|
+
rotation.trips[-1].arrival_time,
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return (pd.DataFrame({"time": all_times, "soc": all_soc}), descriptions)
|