-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjourney.py
284 lines (237 loc) · 12.9 KB
/
journey.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
from datetime import datetime, timedelta
from typing import List, Union, Optional, Tuple, Dict
from connections import Footpath, TripSegment
from distribution import Distribution
class Journey(object):
"""
A journey composed of Footpaths and TripSegments from a source to a destination.
Immutable. It's parameters should never be modified as some of them are computed at initialization. To create a new
Journey from this one, use the add_segment_to_journey(*) method, which efficiently does so.
Attributes:
- :class:`List[Union[Footpath, TripSegment]]` journey_segments --> The consecutive journey segments leading from the
source to the destination
- :class:`int` departure_stop --> The index of the train stop from which the journey starts
- :class:`int` arrival_stop --> The index of the train stop where the journey will eventually end
- :class:`int` current_arrival_stop --> The index of the train stop where the journey currently ends
- :class:`bool` reached_destination --> Whether the arrival was reached (arrival_stop == current_arrival_stop)
- :class:`bool` reached_destination --> Whether the arrival was reached (arrival_stop == current_arrival_stop)
- :class:`datetime.datetime` target_arr_time --> The latest time at which the passenger wanted to get to the end
- :class:`int` min_connection_time --> The minimum amount of time, in minutes, to "change tracks" at a stop
- :class:`Dict[int, Distribution]` delay_distributions --> Distribution ids of trips to their delay distributions
"""
def __init__(self,
departure_stop: int,
arrival_stop: int,
journey_segments: List[Union[Footpath, TripSegment]],
target_arrival_time: datetime,
min_connection_time: timedelta,
delay_distributions: Dict[int, Distribution],
arrival_time_at_last_stop: Optional[float],
success_probability: Optional[float]):
self.journey_segments = journey_segments
self.departure_stop = departure_stop
self.arrival_stop = arrival_stop
if len(self.journey_segments) == 0:
self.current_arrival_stop = departure_stop
else:
if isinstance(self.journey_segments[-1], Footpath):
self.current_arrival_stop = self.journey_segments[-1].arr_stop
else:
self.current_arrival_stop = self.journey_segments[-1].exit_connection.arr_stop
self.reached_destination = (self.current_arrival_stop == self.arrival_stop)
self.min_connection_time = min_connection_time
# The segment changes in the path
self.precomputed_changes = None
# The success probability of the path
if success_probability is None:
self.chance_of_success = 1.0
else:
self.chance_of_success = success_probability
# The delay distributions for different types of trips
self.delay_distributions = delay_distributions
# Private variables
self._departure_time = False, None
if arrival_time_at_last_stop is not None:
self._arrival_time = True, arrival_time_at_last_stop
else:
self._arrival_time = False, None
self._target_arr_time = target_arrival_time
self._walk_time = False, None
def __len__(self):
return len(self.journey_segments)
def __repr__(self):
return f'<Journey of {len(self)} segments>'
def __str__(self):
s = f'Journey of {len(self)} segments, departs={self.departure_time()}, arrives={self.current_arrival_time()}'
for p in self.journey_segments:
s += f'\n {str(p)}'
return s
def departure_time(self) -> Optional[datetime]:
"""
:return: the time at which the passenger needs to leave the starting point. None if unknown.
"""
if self._departure_time[0]:
return self._departure_time[1]
if len(self.journey_segments) == 0:
self._departure_time = (True, None)
return None
if isinstance(self.journey_segments[0], Footpath):
if len(self.journey_segments) == 1:
self._departure_time = (True, None)
return self.target_arrival_time() - self.journey_segments[0].walk_time
else:
if not isinstance(self.journey_segments[1], TripSegment):
raise ValueError(f'Two Footpaths in a row in a Journey: {self.journey_segments}')
dep_time = self.journey_segments[1].departure_time - self.journey_segments[0].walk_time
self._departure_time = (True, dep_time)
return dep_time
else:
self._departure_time = (True, self.journey_segments[0].departure_time)
return self._departure_time[1]
def current_arrival_time(self) -> Optional[datetime]:
"""
:return: The time at which the passenger arrives at the current last stop. None if unknown.
"""
if self._arrival_time[0]:
return self._arrival_time[1]
if len(self.journey_segments) == 0:
self._arrival_time = (True, None)
return None
if isinstance(self.journey_segments[-1], Footpath):
if len(self.journey_segments) == 1:
if self.reached_destination:
self._arrival_time = (True, self.target_arrival_time())
return self.target_arrival_time()
else:
self._arrival_time = (True, None)
return None
else:
if not isinstance(self.journey_segments[-2], TripSegment):
raise ValueError(f'Two Footpaths in a row in a Journey: {self.journey_segments}')
arr_time = self.journey_segments[-2].arrival_time + self.journey_segments[-1].walk_time
self._arrival_time = (True, arr_time)
return self._arrival_time[1]
else:
arr_time = self.journey_segments[-1].arrival_time
self._arrival_time = (True, arr_time)
return self._arrival_time[1]
def target_arrival_time(self) -> datetime:
"""
:return: the time at which the passenger wants to arrive at the destination
"""
return self._target_arr_time
def duration(self) -> int:
"""
:return: The current journey duration, in minutes
"""
time_diff = (self.current_arrival_time() - self.departure_time()).seconds
return (time_diff // 60) + int(time_diff % 60 > 0)
def walk_time(self) -> int:
"""
:return: The amount of time that needs to be spent walking during the journey
"""
if self._walk_time[0]:
return self._walk_time[1]
time: int = 0
for segment in self.journey_segments:
if isinstance(segment, Footpath):
time += (segment.walk_time.seconds // 60)
self._walk_time = True, time
return time
def success_probability(self) -> float:
"""
:return: the success probability of this Journey, based on delays
"""
return self.chance_of_success
def changes(self) -> List[Tuple[TripSegment, int]]:
"""
:return: an iterable outputting trip segments and the maximum delay that can occur during the journey for the
passenger not to miss the next trip.
"""
if self.precomputed_changes is not None:
return self.precomputed_changes
changes = []
for i, segment in enumerate(self.journey_segments):
if isinstance(segment, TripSegment):
# If this segment is the last one before arriving at the destination, the amount of delay that can occur
# is the amount of time between the arrival and the time the person needs to be at the destination
if i == len(self.journey_segments) - 1:
max_delay = (self.target_arrival_time() - segment.exit_connection.arr_time).seconds // 60
changes.append((segment, max_delay))
# Same if it is the segment before last but we need to walk
elif i == len(self.journey_segments) - 2 and isinstance(self.journey_segments[-1], Footpath):
arr_time_plus_walk_time = segment.exit_connection.arr_time + self.journey_segments[-1].walk_time
max_delay = (self.target_arrival_time() - arr_time_plus_walk_time).seconds // 60
changes.append((segment, max_delay))
# Otherwise, it's the difference between the arrival time of this connection and the departure time of
# the next, minus the walking time
else:
next_stop_arr_time = segment.exit_connection.arr_time
next_connection_index = i + 1
if isinstance(self.journey_segments[i + 1], Footpath):
next_stop_arr_time += self.journey_segments[i + 1].walk_time
next_connection_index += 1
next_connection_dep = self.journey_segments[next_connection_index].enter_connection.dep_time
max_delay = (next_connection_dep - next_stop_arr_time).seconds//60
changes.append((segment, max_delay))
self.precomputed_changes = changes
return changes
def add_segment_to_journey(j: Journey, new_segment: Union[Footpath, TripSegment]) -> Journey:
"""
Copies a Journey and appends a segment to it (the segment should have as a departure stop the arrival stop of the
current Journey).
:param j: the Journey to which we want to add a segment
:param new_segment: the segment to add to the Journey
:return: A copy of the journey with the added segment
"""
new_journey_segments = j.journey_segments.copy()
new_journey_segments.append(new_segment)
new_success_probability = j.success_probability()
new_arrival_time_at_last_stop = None
if isinstance(new_segment, Footpath):
# If we haven't arrived, we don't know at what time the next connection is yet.
# Otherwise we know at what time we needed to be there
if new_segment.arr_stop == j.arrival_stop:
# If the journey didn't have an arrival time, then the chance of making this trip is 1.
# Otherwise we need to compute the probability to arrive at the destination in time
if j.current_arrival_time() is not None:
time_to_arrive = j.current_arrival_time() + new_segment.walk_time
max_delay = (j.target_arrival_time() - time_to_arrive).seconds // 60
# The probability to arrive in time is based on the probability distribution of the last trip segment
previous_trip = j.journey_segments[-1]
last_trip_distribution = j.delay_distributions.get(previous_trip.delay_distribution_id())
new_success_probability = new_success_probability * last_trip_distribution.cdf(max_delay)
new_arrival_time_at_last_stop = previous_trip.arrival_time + new_segment.walk_time
else:
# If we didn't have a minimum arrival time, than we can arrive at the last stop at the target
new_arrival_time_at_last_stop = j.target_arrival_time()
else:
# Compute the probability of arriving at the stop before the connection leaves.
# 1 if there is no current arrival time for the journey
if j.current_arrival_time() is not None:
if isinstance(j.journey_segments[-1], TripSegment):
previous_trip = j.journey_segments[-1]
arrival_time_at_new_connection = previous_trip.arrival_time
else:
previous_trip = j.journey_segments[-2]
arrival_time_at_new_connection = previous_trip.arrival_time + j.journey_segments[-1].walk_time
last_trip_distribution = j.delay_distributions.get(previous_trip.delay_distribution_id())
max_delay = (new_segment.departure_time - arrival_time_at_new_connection).seconds // 60
new_success_probability *= last_trip_distribution.cdf(max_delay)
# If this is the last connection, compute the probability of arriving there in time
if new_segment.exit_connection.arr_stop == j.arrival_stop:
trip_dist = j.delay_distributions.get(new_segment.delay_distribution_id())
max_delay = (j.target_arrival_time() - new_segment.arrival_time).seconds // 60
new_success_probability *= trip_dist.cdf(max_delay)
new_arrival_time_at_last_stop = new_segment.arrival_time
extended_journey = Journey(
j.departure_stop,
j.arrival_stop,
new_journey_segments,
j.target_arrival_time(),
j.min_connection_time,
j.delay_distributions,
new_arrival_time_at_last_stop,
new_success_probability,
)
return extended_journey