OptiLine-Py 0.1.7__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.
@@ -0,0 +1,970 @@
1
+ import numpy as np
2
+ import math
3
+ from OptiLine.utils import calc_splines, conv_filt
4
+ from scipy.interpolate import splprep, splev
5
+
6
+ try:
7
+ from numba import njit as _njit
8
+ except ImportError:
9
+ def _njit(fn=None, **_): # transparent no-op fallback
10
+ return fn if fn is not None else lambda f: f
11
+
12
+
13
+ # mode_int encoding: 0 = accel_forw, 1 = decel_forw, 2 = decel_backw
14
+ @_njit(cache=True)
15
+ def _calc_ax_poss_nb(vx_start, radius,
16
+ ggv_vx, ggv_ax, ggv_ay,
17
+ ax_mach_vx, ax_mach_ax,
18
+ mu, dyn_model_exp, drag_coeff, m_veh,
19
+ mode_int):
20
+ ax_max_tires = mu * np.interp(vx_start, ggv_vx, ggv_ax)
21
+ ay_max_tires = mu * np.interp(vx_start, ggv_vx, ggv_ay)
22
+ ay_used = vx_start * vx_start / radius
23
+
24
+ if mode_int != 1 and ax_max_tires < 0.0: # accel_forw or decel_backw
25
+ ax_max_tires = -ax_max_tires
26
+ elif mode_int == 1 and ax_max_tires > 0.0: # decel_forw
27
+ ax_max_tires = -ax_max_tires
28
+
29
+ radicand = 1.0 - (ay_used / ay_max_tires) ** dyn_model_exp
30
+ ax_avail_tires = ax_max_tires * radicand ** (1.0 / dyn_model_exp) if radicand > 0.0 else 0.0
31
+
32
+ if mode_int == 0: # accel_forw — apply motor limits
33
+ ax_mach_tmp = np.interp(vx_start, ax_mach_vx, ax_mach_ax)
34
+ ax_avail = ax_avail_tires if ax_avail_tires < ax_mach_tmp else ax_mach_tmp
35
+ else:
36
+ ax_avail = ax_avail_tires
37
+
38
+ ax_drag = -(vx_start * vx_start) * drag_coeff / m_veh
39
+
40
+ if mode_int != 2: # accel_forw or decel_forw
41
+ return ax_avail + ax_drag
42
+ else: # decel_backw
43
+ return ax_avail - ax_drag
44
+
45
+ def calc_vel_profile(ax_max_machines: np.ndarray,
46
+ kappa: np.ndarray,
47
+ el_lengths: np.ndarray,
48
+ closed: bool,
49
+ drag_coeff: float,
50
+ m_veh: float,
51
+ ggv: np.ndarray = None,
52
+ loc_gg: np.ndarray = None,
53
+ v_max: float = None,
54
+ dyn_model_exp: float = 1.0,
55
+ mu: np.ndarray = None,
56
+ v_start: float = None,
57
+ v_end: float = None,
58
+ filt_window: int = None,
59
+ p_ggv: np.ndarray = None) -> np.ndarray:
60
+ """
61
+
62
+ .. description::
63
+ Calculates a velocity profile using the tire and motor limits as good as possible.
64
+
65
+ .. inputs::
66
+ :param ax_max_machines: longitudinal acceleration limits by the electrical motors: [vx, ax_max_machines]. Velocity
67
+ in m/s, accelerations in m/s2. They should be handed in without considering drag resistance,
68
+ i.e. simply by calculating F_x_drivetrain / m_veh
69
+ :type ax_max_machines: np.ndarray
70
+ :param kappa: curvature profile of given trajectory in rad/m (always unclosed).
71
+ :type kappa: np.ndarray
72
+ :param el_lengths: element lengths (distances between coordinates) of given trajectory.
73
+ :type el_lengths: np.ndarray
74
+ :param closed: flag to set if the velocity profile must be calculated for a closed or unclosed trajectory.
75
+ :type closed: bool
76
+ :param drag_coeff: drag coefficient including all constants: drag_coeff = 0.5 * c_w * A_front * rho_air
77
+ :type drag_coeff: float
78
+ :param m_veh: vehicle mass in kg.
79
+ :type m_veh: float
80
+ :param ggv: ggv-diagram to be applied: [vx, ax_max, ay_max]. Velocity in m/s, accelerations in m/s2.
81
+ ATTENTION: Insert either ggv + mu (optional) or loc_gg!
82
+ :type ggv: np.ndarray
83
+ :param loc_gg: local gg diagrams along the path points: [[ax_max_0, ay_max_0], [ax_max_1, ay_max_1], ...],
84
+ accelerations in m/s2. ATTENTION: Insert either ggv + mu (optional) or loc_gg!
85
+ :type loc_gg: np.ndarray
86
+ :param v_max: Maximum longitudinal speed in m/s (optional if ggv is supplied, taking the minimum of the
87
+ fastest velocities covered by the ggv and ax_max_machines arrays then).
88
+ :type v_max: float
89
+ :param dyn_model_exp: exponent used in the vehicle dynamics model (usual range [1.0,2.0]).
90
+ :type dyn_model_exp: float
91
+ :param mu: friction coefficients (always unclosed).
92
+ :type mu: np.ndarray
93
+ :param v_start: start velocity in m/s (used in unclosed case only).
94
+ :type v_start: float
95
+ :param v_end: end velocity in m/s (used in unclosed case only).
96
+ :type v_end: float
97
+ :param filt_window: filter window size for moving average filter (must be odd).
98
+ :type filt_window: int
99
+
100
+ .. outputs::
101
+ :return vx_profile: calculated velocity profile (always unclosed).
102
+ :rtype vx_profile: np.ndarray
103
+
104
+ .. notes::
105
+ All inputs must be inserted unclosed, i.e. kappa[-1] != kappa[0], even if closed is set True! (el_lengths is kind of
106
+ closed if closed is True of course!)
107
+
108
+ case closed is True:
109
+ len(kappa) = len(el_lengths) = len(mu) = len(vx_profile)
110
+
111
+ case closed is False:
112
+ len(kappa) = len(el_lengths) + 1 = len(mu) = len(vx_profile)
113
+ """
114
+
115
+ # ------------------------------------------------------------------------------------------------------------------
116
+ # INPUT CHECKS -----------------------------------------------------------------------------------------------------
117
+ # ------------------------------------------------------------------------------------------------------------------
118
+
119
+ # check if either ggv (and optionally mu) or loc_gg are handed in
120
+ if (ggv is not None or mu is not None) and loc_gg is not None:
121
+ raise RuntimeError("Either ggv and optionally mu OR loc_gg must be supplied, not both (or all) of them!")
122
+
123
+ if ggv is None and loc_gg is None:
124
+ raise RuntimeError("Either ggv or loc_gg must be supplied!")
125
+
126
+ # check shape of loc_gg
127
+ if loc_gg is not None:
128
+ if loc_gg.ndim != 2:
129
+ raise RuntimeError("loc_gg must have two dimensions!")
130
+
131
+ if loc_gg.shape[0] != kappa.size:
132
+ raise RuntimeError("Length of loc_gg and kappa must be equal!")
133
+
134
+ if loc_gg.shape[1] != 2:
135
+ raise RuntimeError("loc_gg must consist of two columns: [ax_max, ay_max]!")
136
+
137
+ # check shape of ggv
138
+ if ggv is not None and ggv.shape[1] != 3:
139
+ raise RuntimeError("ggv diagram must consist of the three columns [vx, ax_max, ay_max]!")
140
+
141
+ # check size of mu
142
+ if mu is not None and kappa.size != mu.size:
143
+ raise RuntimeError("kappa and mu must have the same length!")
144
+
145
+ # check size of kappa and element lengths
146
+ if closed and kappa.size != el_lengths.size:
147
+ raise RuntimeError("kappa and el_lengths must have the same length if closed!")
148
+
149
+ elif not closed and kappa.size != el_lengths.size + 1:
150
+ raise RuntimeError("kappa must have the length of el_lengths + 1 if unclosed!")
151
+
152
+ # check start and end velocities
153
+ if not closed and v_start is None:
154
+ raise RuntimeError("v_start must be provided for the unclosed case!")
155
+
156
+ if v_start is not None and v_start < 0.0:
157
+ v_start = 0.0
158
+ print('WARNING: Input v_start was < 0.0. Using v_start = 0.0 instead!')
159
+
160
+ if v_end is not None and v_end < 0.0:
161
+ v_end = 0.0
162
+ print('WARNING: Input v_end was < 0.0. Using v_end = 0.0 instead!')
163
+
164
+ # check dyn_model_exp
165
+ if not 1.0 <= dyn_model_exp <= 2.0:
166
+ print('WARNING: Exponent for the vehicle dynamics model should be in the range [1.0, 2.0]!')
167
+
168
+ # check shape of ax_max_machines
169
+ if ax_max_machines.shape[1] != 2:
170
+ raise RuntimeError("ax_max_machines must consist of the two columns [vx, ax_max_machines]!")
171
+
172
+ # check v_max
173
+ if v_max is None:
174
+ if ggv is None:
175
+ raise RuntimeError("v_max must be supplied if ggv is None!")
176
+ else:
177
+ v_max = min(ggv[-1, 0], ax_max_machines[-1, 0])
178
+
179
+ else:
180
+ # check if ggv covers velocity until v_max
181
+ if ggv is not None and ggv[-1, 0] < v_max:
182
+ raise RuntimeError("ggv has to cover the entire velocity range of the car (i.e. >= v_max)!")
183
+
184
+ # check if ax_max_machines covers velocity until v_max
185
+ if ax_max_machines[-1, 0] < v_max:
186
+ raise RuntimeError("ax_max_machines has to cover the entire velocity range of the car (i.e. >= v_max)!")
187
+
188
+ # ------------------------------------------------------------------------------------------------------------------
189
+ # BRINGING GGV OR LOC_GG INTO SHAPE FOR EQUAL HANDLING AFTERWARDS --------------------------------------------------
190
+ # ------------------------------------------------------------------------------------------------------------------
191
+
192
+ """For an equal/easier handling of every case afterwards we bring all cases into a form where the local ggv is made
193
+ available for every waypoint, i.e. [ggv_0, ggv_1, ggv_2, ...] -> we have a three dimensional array p_ggv (path_ggv)
194
+ where the first dimension is the waypoint, the second is the velocity and the third is the two acceleration columns
195
+ -> DIM = NO_WAYPOINTS_CLOSED x NO_VELOCITY ENTRIES x 3"""
196
+
197
+ # CASE 1: ggv supplied -> copy it for every waypoint (skip if pre-built p_ggv provided)
198
+ if ggv is not None:
199
+ if p_ggv is None or p_ggv.shape[0] != kappa.size:
200
+ p_ggv = np.repeat(np.expand_dims(ggv, axis=0), kappa.size, axis=0)
201
+ op_mode = 'ggv'
202
+
203
+ # CASE 2: local gg diagram supplied -> add velocity dimension (artificial velocity of 10.0 m/s)
204
+ else:
205
+ p_ggv = np.expand_dims(np.column_stack((np.ones(loc_gg.shape[0]) * 10.0, loc_gg)), axis=1)
206
+ op_mode = 'loc_gg'
207
+
208
+ # ------------------------------------------------------------------------------------------------------------------
209
+ # SPEED PROFILE CALCULATION (FB) -----------------------------------------------------------------------------------
210
+ # ------------------------------------------------------------------------------------------------------------------
211
+
212
+ # transform curvature kappa into corresponding radii (abs because curvature has a sign in our convention)
213
+ radii = np.abs(np.divide(1.0, kappa, out=np.full(kappa.size, np.inf), where=kappa != 0.0))
214
+
215
+ # call solver
216
+ if not closed:
217
+ vx_profile = __solver_fb_unclosed(p_ggv=p_ggv,
218
+ ax_max_machines=ax_max_machines,
219
+ v_max=v_max,
220
+ radii=radii,
221
+ el_lengths=el_lengths,
222
+ mu=mu,
223
+ v_start=v_start,
224
+ v_end=v_end,
225
+ dyn_model_exp=dyn_model_exp,
226
+ drag_coeff=drag_coeff,
227
+ m_veh=m_veh,
228
+ op_mode=op_mode)
229
+
230
+ else:
231
+ vx_profile = __solver_fb_closed(p_ggv=p_ggv,
232
+ ax_max_machines=ax_max_machines,
233
+ v_max=v_max,
234
+ radii=radii,
235
+ el_lengths=el_lengths,
236
+ mu=mu,
237
+ dyn_model_exp=dyn_model_exp,
238
+ drag_coeff=drag_coeff,
239
+ m_veh=m_veh,
240
+ op_mode=op_mode)
241
+
242
+ # ------------------------------------------------------------------------------------------------------------------
243
+ # POSTPROCESSING ---------------------------------------------------------------------------------------------------
244
+ # ------------------------------------------------------------------------------------------------------------------
245
+
246
+ if filt_window is not None:
247
+ vx_profile = conv_filt(signal=vx_profile,
248
+ filt_window=filt_window,
249
+ closed=closed)
250
+
251
+ return vx_profile
252
+
253
+
254
+ def __solver_fb_unclosed(p_ggv: np.ndarray,
255
+ ax_max_machines: np.ndarray,
256
+ v_max: float,
257
+ radii: np.ndarray,
258
+ el_lengths: np.ndarray,
259
+ v_start: float,
260
+ drag_coeff: float,
261
+ m_veh: float,
262
+ op_mode: str,
263
+ mu: np.ndarray = None,
264
+ v_end: float = None,
265
+ dyn_model_exp: float = 1.0) -> np.ndarray:
266
+ """
267
+
268
+ .. description::
269
+ Forward-backward solver for an unclosed (open) trajectory. Computes the velocity profile by first applying
270
+ a forward acceleration pass from v_start, then a backward deceleration pass toward v_end.
271
+
272
+ .. inputs::
273
+ :param p_ggv: per-waypoint ggv diagram: [vx, ax_max, ay_max] repeated for each point.
274
+ :type p_ggv: np.ndarray
275
+ :param ax_max_machines: longitudinal acceleration limits by the electrical motors: [vx, ax_max_machines].
276
+ :type ax_max_machines: np.ndarray
277
+ :param v_max: maximum longitudinal speed in m/s.
278
+ :type v_max: float
279
+ :param radii: radius profile of the trajectory in m.
280
+ :type radii: np.ndarray
281
+ :param el_lengths: element lengths between consecutive trajectory points in m.
282
+ :type el_lengths: np.ndarray
283
+ :param v_start: start velocity in m/s.
284
+ :type v_start: float
285
+ :param drag_coeff: drag coefficient including all constants: drag_coeff = 0.5 * c_w * A_front * rho_air.
286
+ :type drag_coeff: float
287
+ :param m_veh: vehicle mass in kg.
288
+ :type m_veh: float
289
+ :param op_mode: operation mode, either 'ggv' or 'loc_gg'.
290
+ :type op_mode: str
291
+ :param mu: friction coefficients along the trajectory (optional, defaults to 1.0).
292
+ :type mu: np.ndarray
293
+ :param v_end: end velocity in m/s (optional).
294
+ :type v_end: float
295
+ :param dyn_model_exp: exponent used in the vehicle dynamics model (usual range [1.0, 2.0]).
296
+ :type dyn_model_exp: float
297
+
298
+ .. outputs::
299
+ :return vx_profile: calculated velocity profile (unclosed).
300
+ :rtype vx_profile: np.ndarray
301
+ """
302
+
303
+ # ------------------------------------------------------------------------------------------------------------------
304
+ # FORWARD BACKWARD SOLVER ------------------------------------------------------------------------------------------
305
+ # ------------------------------------------------------------------------------------------------------------------
306
+
307
+ # handle mu
308
+ if mu is None:
309
+ mu = np.ones(radii.size)
310
+ mu_mean = 1.0
311
+ else:
312
+ mu_mean = np.mean(mu)
313
+
314
+ # run through all the points and check for possible lateral acceleration
315
+ if op_mode == 'ggv':
316
+ # in ggv mode all ggvs are equal -> we can use the first one
317
+ ay_max_global = mu_mean * np.amin(p_ggv[0, :, 2]) # get first lateral acceleration estimate
318
+ vx_profile = np.sqrt(ay_max_global * radii) # get first velocity profile estimate
319
+
320
+ ay_max_curr = mu * np.interp(vx_profile, p_ggv[0, :, 0], p_ggv[0, :, 2])
321
+ vx_profile = np.sqrt(np.multiply(ay_max_curr, radii))
322
+
323
+ else:
324
+ # in loc_gg mode all ggvs consist of a single line due to the missing velocity dependency, mu is None in this
325
+ # case
326
+ vx_profile = np.sqrt(p_ggv[:, 0, 2] * radii) # get first velocity profile estimate
327
+
328
+ # cut vx_profile to car's top speed
329
+ vx_profile[vx_profile > v_max] = v_max
330
+
331
+ # consider v_start
332
+ if vx_profile[0] > v_start:
333
+ vx_profile[0] = v_start
334
+
335
+ # calculate acceleration profile
336
+ vx_profile = __solver_fb_acc_profile(p_ggv=p_ggv,
337
+ ax_max_machines=ax_max_machines,
338
+ v_max=v_max,
339
+ radii=radii,
340
+ el_lengths=el_lengths,
341
+ mu=mu,
342
+ vx_profile=vx_profile,
343
+ backwards=False,
344
+ dyn_model_exp=dyn_model_exp,
345
+ drag_coeff=drag_coeff,
346
+ m_veh=m_veh)
347
+
348
+ # consider v_end
349
+ if v_end is not None and vx_profile[-1] > v_end:
350
+ vx_profile[-1] = v_end
351
+
352
+ # calculate deceleration profile
353
+ vx_profile = __solver_fb_acc_profile(p_ggv=p_ggv,
354
+ ax_max_machines=ax_max_machines,
355
+ v_max=v_max,
356
+ radii=radii,
357
+ el_lengths=el_lengths,
358
+ mu=mu,
359
+ vx_profile=vx_profile,
360
+ backwards=True,
361
+ dyn_model_exp=dyn_model_exp,
362
+ drag_coeff=drag_coeff,
363
+ m_veh=m_veh)
364
+
365
+ return vx_profile
366
+
367
+
368
+ def __solver_fb_closed(p_ggv: np.ndarray,
369
+ ax_max_machines: np.ndarray,
370
+ v_max: float,
371
+ radii: np.ndarray,
372
+ el_lengths: np.ndarray,
373
+ drag_coeff: float,
374
+ m_veh: float,
375
+ op_mode: str,
376
+ mu: np.ndarray = None,
377
+ dyn_model_exp: float = 1.0) -> np.ndarray:
378
+ """
379
+
380
+ .. description::
381
+ Forward-backward solver for a closed (circuit) trajectory. Processes two laps to determine a consistent
382
+ velocity profile, applying an acceleration pass followed by a deceleration pass on the doubled arrays.
383
+
384
+ .. inputs::
385
+ :param p_ggv: per-waypoint ggv diagram: [vx, ax_max, ay_max] repeated for each point.
386
+ :type p_ggv: np.ndarray
387
+ :param ax_max_machines: longitudinal acceleration limits by the electrical motors: [vx, ax_max_machines].
388
+ :type ax_max_machines: np.ndarray
389
+ :param v_max: maximum longitudinal speed in m/s.
390
+ :type v_max: float
391
+ :param radii: radius profile of the trajectory in m.
392
+ :type radii: np.ndarray
393
+ :param el_lengths: element lengths between consecutive trajectory points in m.
394
+ :type el_lengths: np.ndarray
395
+ :param drag_coeff: drag coefficient including all constants: drag_coeff = 0.5 * c_w * A_front * rho_air.
396
+ :type drag_coeff: float
397
+ :param m_veh: vehicle mass in kg.
398
+ :type m_veh: float
399
+ :param op_mode: operation mode, either 'ggv' or 'loc_gg'.
400
+ :type op_mode: str
401
+ :param mu: friction coefficients along the trajectory (optional, defaults to 1.0).
402
+ :type mu: np.ndarray
403
+ :param dyn_model_exp: exponent used in the vehicle dynamics model (usual range [1.0, 2.0]).
404
+ :type dyn_model_exp: float
405
+
406
+ .. outputs::
407
+ :return vx_profile: calculated velocity profile (unclosed, second lap extracted).
408
+ :rtype vx_profile: np.ndarray
409
+ """
410
+
411
+ # ------------------------------------------------------------------------------------------------------------------
412
+ # FORWARD BACKWARD SOLVER ------------------------------------------------------------------------------------------
413
+ # ------------------------------------------------------------------------------------------------------------------
414
+
415
+ no_points = radii.size
416
+
417
+ # handle mu
418
+ if mu is None:
419
+ mu = np.ones(no_points)*1
420
+ mu_mean = 1.0*1
421
+ else:
422
+ mu_mean = np.mean(mu)
423
+
424
+ # run through all the points and check for possible lateral acceleration
425
+ if op_mode == 'ggv':
426
+ # in ggv mode all ggvs are equal -> we can use the first one
427
+ ay_max_global = mu_mean * np.amin(p_ggv[0, :, 2]) # get first lateral acceleration estimate
428
+ vx_profile = np.sqrt(ay_max_global * radii) # get first velocity estimate (radii must be positive!)
429
+
430
+ # iterate until the initial velocity profile converges (break after max. 100 iterations)
431
+ converged = False
432
+
433
+ for i in range(10):
434
+ vx_profile_prev_iteration = vx_profile
435
+
436
+ ay_max_curr = mu * np.interp(vx_profile, p_ggv[0, :, 0], p_ggv[0, :, 2])
437
+ vx_profile = np.sqrt(np.multiply(ay_max_curr, radii))
438
+
439
+ # break the loop if the maximum change of the velocity profile was below 0.5%
440
+ if np.max(np.abs(vx_profile / vx_profile_prev_iteration - 1.0)) < 0.005:
441
+ converged = True
442
+ break
443
+
444
+ if not converged:
445
+ print("The initial vx profile did not converge after 100 iterations, please check radii and ggv!")
446
+
447
+ else:
448
+ # in loc_gg mode all ggvs consist of a single line due to the missing velocity dependency, mu is None in this
449
+ # case
450
+ vx_profile = np.sqrt(p_ggv[:, 0, 2] * radii) # get first velocity estimate (radii must be positive!)
451
+
452
+ # cut vx_profile to car's top speed
453
+ vx_profile[vx_profile > v_max] = v_max
454
+
455
+ """We need to calculate the speed profile for three laps and keep the middle lap.
456
+
457
+ Why 3 laps, not 2:
458
+ The backward pass is implemented by *flipping* the array and running the same forward
459
+ algorithm. The forward algorithm only ever writes vx_profile[i+1] — it never touches
460
+ vx_profile[0] (index 0 of the flipped array = vx[-1] of the original).
461
+
462
+ With 2-lap doubling the flipped array has size 2*N. vx[-1] of the second copy sits at
463
+ flipped[0] and is therefore *never* constrained by the backward pass, regardless of
464
+ how many iterations you run. The car's speed at the end of the lap is never forced to
465
+ account for the braking needed to enter the next lap's corner — so vx[-1] stays too
466
+ high and the appended wrap-around acceleration ax = (vx[0]²-vx[-1]²)/(2*ds) comes out
467
+ at -36 m/s².
468
+
469
+ With 3 laps the flipped array has size 3*N. In the flipped representation the corner
470
+ at the start of the third copy sits at flipped[N-1] (= vx[0] of the third copy =
471
+ the slow-corner speed, e.g. 15 m/s). That creates an acceleration phase at index
472
+ N-1 → N, so the forward-on-flipped algorithm constrains flipped[N] = vx[-1] of the
473
+ middle (second) copy to the physically correct braking distance from the corner.
474
+ After flipping back, result[2*N-1] = vx[-1] of the middle lap is correctly limited. ✓
475
+ """
476
+
477
+ # Triple the track arrays (fixed for both passes)
478
+ radii_triple = np.concatenate((radii, radii, radii), axis=0)
479
+ el_lengths_triple = np.concatenate((el_lengths, el_lengths, el_lengths), axis=0)
480
+ mu_triple = np.concatenate((mu, mu, mu), axis=0)
481
+ p_ggv_triple = np.concatenate((p_ggv, p_ggv, p_ggv), axis=0)
482
+
483
+ # --- forward (acceleration) pass on 3 laps; keep middle lap ---
484
+ vx_profile_triple = np.concatenate((vx_profile, vx_profile, vx_profile), axis=0)
485
+ vx_profile_triple = __solver_fb_acc_profile(p_ggv=p_ggv_triple,
486
+ ax_max_machines=ax_max_machines,
487
+ v_max=v_max,
488
+ radii=radii_triple,
489
+ el_lengths=el_lengths_triple,
490
+ mu=mu_triple,
491
+ vx_profile=vx_profile_triple,
492
+ backwards=False,
493
+ dyn_model_exp=dyn_model_exp,
494
+ drag_coeff=drag_coeff,
495
+ m_veh=m_veh)
496
+ vx_profile = vx_profile_triple[no_points:2 * no_points] # middle lap
497
+
498
+ # --- backward (deceleration) pass on 3 laps; keep middle lap ---
499
+ # vx[-1] of the middle lap is now at flipped[N] (not flipped[0]) and IS
500
+ # constrained by the acceleration phase that starts at flipped[N-1] = vx[0] corner.
501
+ vx_profile_triple = np.concatenate((vx_profile, vx_profile, vx_profile), axis=0)
502
+ vx_profile_triple = __solver_fb_acc_profile(p_ggv=p_ggv_triple,
503
+ ax_max_machines=ax_max_machines,
504
+ v_max=v_max,
505
+ radii=radii_triple,
506
+ el_lengths=el_lengths_triple,
507
+ mu=mu_triple,
508
+ vx_profile=vx_profile_triple,
509
+ backwards=True,
510
+ dyn_model_exp=dyn_model_exp,
511
+ drag_coeff=drag_coeff,
512
+ m_veh=m_veh)
513
+ vx_profile = vx_profile_triple[no_points:2 * no_points] # middle lap
514
+
515
+ return vx_profile
516
+
517
+
518
+ def __solver_fb_acc_profile(p_ggv: np.ndarray,
519
+ ax_max_machines: np.ndarray,
520
+ v_max: float,
521
+ radii: np.ndarray,
522
+ el_lengths: np.ndarray,
523
+ mu: np.ndarray,
524
+ vx_profile: np.ndarray,
525
+ drag_coeff: float,
526
+ m_veh: float,
527
+ dyn_model_exp: float = 1.0,
528
+ backwards: bool = False) -> np.ndarray:
529
+ """
530
+
531
+ .. description::
532
+ Applies a single forward or backward acceleration pass over the velocity profile. Iterates through
533
+ acceleration phases and enforces physically possible velocities point by point using calc_ax_poss.
534
+
535
+ .. inputs::
536
+ :param p_ggv: per-waypoint ggv diagram: [vx, ax_max, ay_max].
537
+ :type p_ggv: np.ndarray
538
+ :param ax_max_machines: longitudinal acceleration limits by the electrical motors: [vx, ax_max_machines].
539
+ :type ax_max_machines: np.ndarray
540
+ :param v_max: maximum longitudinal speed in m/s.
541
+ :type v_max: float
542
+ :param radii: radius profile of the trajectory in m.
543
+ :type radii: np.ndarray
544
+ :param el_lengths: element lengths between consecutive trajectory points in m.
545
+ :type el_lengths: np.ndarray
546
+ :param mu: friction coefficients along the trajectory.
547
+ :type mu: np.ndarray
548
+ :param vx_profile: current velocity profile to be updated in-place.
549
+ :type vx_profile: np.ndarray
550
+ :param drag_coeff: drag coefficient including all constants: drag_coeff = 0.5 * c_w * A_front * rho_air.
551
+ :type drag_coeff: float
552
+ :param m_veh: vehicle mass in kg.
553
+ :type m_veh: float
554
+ :param dyn_model_exp: exponent used in the vehicle dynamics model (usual range [1.0, 2.0]).
555
+ :type dyn_model_exp: float
556
+ :param backwards: if True, performs a backward deceleration pass; otherwise forward acceleration.
557
+ :type backwards: bool
558
+
559
+ .. outputs::
560
+ :return vx_profile: updated velocity profile after the acceleration/deceleration pass.
561
+ :rtype vx_profile: np.ndarray
562
+ """
563
+
564
+ # ------------------------------------------------------------------------------------------------------------------
565
+ # PREPARATIONS -----------------------------------------------------------------------------------------------------
566
+ # ------------------------------------------------------------------------------------------------------------------
567
+
568
+ no_points = vx_profile.size
569
+
570
+ # check for reversed direction
571
+ if backwards:
572
+ radii_mod = np.flipud(radii)
573
+ el_lengths_mod = np.flipud(el_lengths)
574
+ mu_mod = np.flipud(mu)
575
+ vx_profile = np.flipud(vx_profile)
576
+ else:
577
+ radii_mod = radii
578
+ el_lengths_mod = el_lengths
579
+ mu_mod = mu
580
+
581
+ # Pre-extract constant arrays and mode integer for the numba kernel
582
+ mode_int = 2 if backwards else 0 # 0 = accel_forw, 2 = decel_backw
583
+ ax_mach_vx = ax_max_machines[:, 0]
584
+ ax_mach_ax = ax_max_machines[:, 1]
585
+
586
+ # ------------------------------------------------------------------------------------------------------------------
587
+ # SEARCH START POINTS FOR ACCELERATION PHASES ----------------------------------------------------------------------
588
+ # ------------------------------------------------------------------------------------------------------------------
589
+
590
+ vx_diffs = np.diff(vx_profile)
591
+ acc_inds = np.where(vx_diffs > 0.0)[0] # indices of points with positive acceleration
592
+ if acc_inds.size != 0:
593
+ # check index diffs -> we only need the first point of every acceleration phase
594
+ acc_inds_diffs = np.diff(acc_inds)
595
+ acc_inds_diffs = np.insert(acc_inds_diffs, 0, 2) # first point is always a starting point
596
+ acc_inds_rel = acc_inds[acc_inds_diffs > 1] # starting point indices for acceleration phases
597
+ else:
598
+ acc_inds_rel = [] # if vmax is low and can be driven all the time
599
+
600
+ # ------------------------------------------------------------------------------------------------------------------
601
+ # CALCULATE VELOCITY PROFILE ---------------------------------------------------------------------------------------
602
+ # ------------------------------------------------------------------------------------------------------------------
603
+
604
+ # cast np.array as a list
605
+ acc_inds_rel = list(acc_inds_rel)
606
+
607
+ # while we have indices remaining in the list
608
+ while acc_inds_rel:
609
+ # set index to first list element
610
+ i = acc_inds_rel.pop(0)
611
+
612
+ # start from current index and run until either the end of the lap or a termination criterion are reached
613
+ while i < no_points - 1:
614
+
615
+ ax_possible_cur = _calc_ax_poss_nb(
616
+ float(vx_profile[i]), float(radii_mod[i]),
617
+ p_ggv[i, :, 0], p_ggv[i, :, 1], p_ggv[i, :, 2],
618
+ ax_mach_vx, ax_mach_ax,
619
+ float(mu_mod[i]), dyn_model_exp, drag_coeff, m_veh, mode_int)
620
+
621
+ vx_possible_next = math.sqrt(math.pow(vx_profile[i], 2) + 2 * ax_possible_cur * el_lengths_mod[i])
622
+
623
+ if backwards:
624
+ """
625
+ We have to loop the calculation if we are in the backwards iteration (currently just once). This is
626
+ because we calculate the possible ax at a point i which does not necessarily fit for point i + 1
627
+ (which is i - 1 in the real direction). At point i + 1 (or i - 1 in real direction) we have a different
628
+ start velocity (vx_possible_next), radius and mu value while the absolute value of ax remains the same
629
+ in both directions.
630
+ """
631
+
632
+ # looping just once at the moment
633
+ for j in range(1):
634
+ ax_possible_next = _calc_ax_poss_nb(
635
+ float(vx_possible_next), float(radii_mod[i + 1]),
636
+ p_ggv[i + 1, :, 0], p_ggv[i + 1, :, 1], p_ggv[i + 1, :, 2],
637
+ ax_mach_vx, ax_mach_ax,
638
+ float(mu_mod[i + 1]), dyn_model_exp, drag_coeff, m_veh, mode_int)
639
+
640
+ vx_tmp = math.sqrt(math.pow(vx_profile[i], 2) + 2 * ax_possible_next * el_lengths_mod[i])
641
+
642
+ if vx_tmp < vx_possible_next:
643
+ vx_possible_next = vx_tmp
644
+ else:
645
+ break
646
+
647
+ # save possible next velocity if it is smaller than the current value
648
+ if vx_possible_next < vx_profile[i + 1]:
649
+ vx_profile[i + 1] = vx_possible_next
650
+
651
+ i += 1
652
+
653
+ # break current acceleration phase if next speed would be higher than the maximum vehicle velocity or if we
654
+ # are at the next acceleration phase start index
655
+ if vx_possible_next > v_max or (acc_inds_rel and i >= acc_inds_rel[0]):
656
+ break
657
+
658
+ # ------------------------------------------------------------------------------------------------------------------
659
+ # POSTPROCESSING ---------------------------------------------------------------------------------------------------
660
+ # ------------------------------------------------------------------------------------------------------------------
661
+
662
+ # flip output vel_profile if necessary
663
+ if backwards:
664
+ vx_profile = np.flipud(vx_profile)
665
+
666
+ return vx_profile
667
+
668
+
669
+ def calc_ax_poss(vx_start: float,
670
+ radius: float,
671
+ ggv: np.ndarray,
672
+ mu: float,
673
+ dyn_model_exp: float,
674
+ drag_coeff: float,
675
+ m_veh: float,
676
+ ax_max_machines: np.ndarray = None,
677
+ mode: str = 'accel_forw') -> float:
678
+ """
679
+ This function returns the possible longitudinal acceleration in the current step/point.
680
+
681
+ .. inputs::
682
+ :param vx_start: [m/s] velocity at current point
683
+ :type vx_start: float
684
+ :param radius: [m] radius on which the car is currently driving
685
+ :type radius: float
686
+ :param ggv: ggv-diagram to be applied: [vx, ax_max, ay_max]. Velocity in m/s, accelerations in m/s2.
687
+ :type ggv: np.ndarray
688
+ :param mu: [-] current friction value
689
+ :type mu: float
690
+ :param dyn_model_exp: [-] exponent used in the vehicle dynamics model (usual range [1.0,2.0]).
691
+ :type dyn_model_exp: float
692
+ :param drag_coeff: [m2*kg/m3] drag coefficient incl. all constants: drag_coeff = 0.5 * c_w * A_front * rho_air
693
+ :type drag_coeff: float
694
+ :param m_veh: [kg] vehicle mass
695
+ :type m_veh: float
696
+ :param ax_max_machines: longitudinal acceleration limits by the electrical motors: [vx, ax_max_machines]. Velocity
697
+ in m/s, accelerations in m/s2. They should be handed in without considering drag resistance.
698
+ Can be set None if using one of the decel modes.
699
+ :type ax_max_machines: np.ndarray
700
+ :param mode: [-] operation mode, can be 'accel_forw', 'decel_forw', 'decel_backw'
701
+ -> determines if machine limitations are considered and if ax should be considered negative
702
+ or positive during deceleration (for possible backwards iteration)
703
+ :type mode: str
704
+
705
+ .. outputs::
706
+ :return ax_final: [m/s2] final acceleration from current point to next one
707
+ :rtype ax_final: float
708
+ """
709
+
710
+ # ------------------------------------------------------------------------------------------------------------------
711
+ # PREPARATIONS -----------------------------------------------------------------------------------------------------
712
+ # ------------------------------------------------------------------------------------------------------------------
713
+
714
+ # check inputs
715
+ if mode not in ['accel_forw', 'decel_forw', 'decel_backw']:
716
+ raise RuntimeError("Unknown operation mode for calc_ax_poss!")
717
+
718
+ if mode == 'accel_forw' and ax_max_machines is None:
719
+ raise RuntimeError("ax_max_machines is required if operation mode is accel_forw!")
720
+
721
+ if ggv.ndim != 2 or ggv.shape[1] != 3:
722
+ raise RuntimeError("ggv must have two dimensions and three columns [vx, ax_max, ay_max]!")
723
+
724
+ # ------------------------------------------------------------------------------------------------------------------
725
+ # CONSIDER TIRE POTENTIAL ------------------------------------------------------------------------------------------
726
+ # ------------------------------------------------------------------------------------------------------------------
727
+
728
+ # calculate possible and used accelerations (considering tires)
729
+ ax_max_tires = mu * np.interp(vx_start, ggv[:, 0], ggv[:, 1])
730
+ ay_max_tires = mu * np.interp(vx_start, ggv[:, 0], ggv[:, 2])
731
+ ay_used = math.pow(vx_start, 2) / radius
732
+
733
+ # during forward acceleration and backward deceleration ax_max_tires must be considered positive, during forward
734
+ # deceleration it must be considered negative
735
+ if mode in ['accel_forw', 'decel_backw'] and ax_max_tires < 0.0:
736
+ print("WARNING: Inverting sign of ax_max_tires because it should be positive but was negative!")
737
+ ax_max_tires *= -1.0
738
+ elif mode == 'decel_forw' and ax_max_tires > 0.0:
739
+ print("WARNING: Inverting sign of ax_max_tires because it should be negative but was positve!")
740
+ ax_max_tires *= -1.0
741
+
742
+ radicand = 1.0 - math.pow(ay_used / ay_max_tires, dyn_model_exp)
743
+
744
+ if radicand > 0.0:
745
+ ax_avail_tires = ax_max_tires * math.pow(radicand, 1.0 / dyn_model_exp)
746
+ else:
747
+ ax_avail_tires = 0.0
748
+
749
+ # ------------------------------------------------------------------------------------------------------------------
750
+ # CONSIDER MACHINE LIMITATIONS -------------------------------------------------------------------------------------
751
+ # ------------------------------------------------------------------------------------------------------------------
752
+
753
+ # consider limitations imposed by electrical machines during forward acceleration
754
+ if mode == 'accel_forw':
755
+ # interpolate machine acceleration to be able to consider varying gear ratios, efficiencies etc.
756
+ ax_max_machines_tmp = np.interp(vx_start, ax_max_machines[:, 0], ax_max_machines[:, 1])
757
+ ax_avail_vehicle = min(ax_avail_tires, ax_max_machines_tmp)
758
+ else:
759
+ ax_avail_vehicle = ax_avail_tires
760
+
761
+ # ------------------------------------------------------------------------------------------------------------------
762
+ # CONSIDER DRAG ----------------------------------------------------------------------------------------------------
763
+ # ------------------------------------------------------------------------------------------------------------------
764
+
765
+ # calculate equivalent longitudinal acceleration of drag force at the current speed
766
+
767
+ ax_drag = -math.pow(vx_start, 2) * drag_coeff / m_veh
768
+ # ax_drag = 0
769
+
770
+
771
+ # drag reduces the possible acceleration in the forward case and increases it in the backward case
772
+ if mode in ['accel_forw', 'decel_forw']:
773
+ ax_final = ax_avail_vehicle + ax_drag
774
+ # attention: this value will now be negative in forward direction if tire is entirely used for cornering
775
+ else:
776
+ ax_final = ax_avail_vehicle - ax_drag
777
+
778
+ return ax_final
779
+
780
+
781
+
782
+ def calc_ax_profile(vx_profile: np.ndarray,
783
+ el_lengths: np.ndarray,
784
+ eq_length_output: bool = False) -> np.ndarray:
785
+ """
786
+ .. description::
787
+ The function calculates the acceleration profile for a given velocity profile.
788
+
789
+ .. inputs::
790
+ :param vx_profile: array containing the velocity profile used as a basis for the acceleration calculations.
791
+ :type vx_profile: np.ndarray
792
+ :param el_lengths: array containing the element lengths between every point of the velocity profile.
793
+ :type el_lengths: np.ndarray
794
+ :param eq_length_output: assumes zero acceleration for the last point of the acceleration profile and therefore
795
+ returns ax_profile with equal length to vx_profile.
796
+ :type eq_length_output: bool
797
+
798
+ .. outputs::
799
+ :return ax_profile: acceleration profile calculated for the inserted vx_profile.
800
+ :rtype ax_profile: np.ndarray
801
+
802
+ .. notes::
803
+ case eq_length_output is True:
804
+ len(vx_profile) = len(el_lengths) + 1 = len(ax_profile)
805
+
806
+ case eq_length_output is False:
807
+ len(vx_profile) = len(el_lengths) + 1 = len(ax_profile) + 1
808
+ """
809
+
810
+ # check inputs
811
+ if vx_profile.size != el_lengths.size + 1:
812
+ raise RuntimeError("Array size of vx_profile should be 1 element bigger than el_lengths!")
813
+
814
+ # calculate longitudinal acceleration profile array numerically: (v_end^2 - v_beg^2) / 2*s
815
+ if eq_length_output:
816
+ ax_profile = np.zeros(vx_profile.size)
817
+ ax_profile[:-1] = (np.power(vx_profile[1:], 2) - np.power(vx_profile[:-1], 2)) / (2 * el_lengths)
818
+ else:
819
+ ax_profile = (np.power(vx_profile[1:], 2) - np.power(vx_profile[:-1], 2)) / (2 * el_lengths)
820
+
821
+ return ax_profile
822
+
823
+
824
+ def calc_t_profile(vx_profile: np.ndarray,
825
+ el_lengths: np.ndarray,
826
+ t_start: float = 0.0,
827
+ ax_profile: np.ndarray = None) -> np.ndarray:
828
+ """
829
+
830
+ .. description::
831
+ Calculate a temporal duration profile for a given trajectory.
832
+
833
+ .. inputs::
834
+ :param vx_profile: array containing the velocity profile.
835
+ :type vx_profile: np.ndarray
836
+ :param el_lengths: array containing the element lengths between every point of the velocity profile.
837
+ :type el_lengths: np.ndarray
838
+ :param t_start: start time in seconds added to first array element.
839
+ :type t_start: float
840
+ :param ax_profile: acceleration profile fitting to the velocity profile.
841
+ :type ax_profile: np.ndarray
842
+
843
+ .. outputs::
844
+ :return t_profile: time profile for the given velocity profile.
845
+ :rtype t_profile: np.ndarray
846
+
847
+ .. notes::
848
+ len(el_lengths) + 1 = len(t_profile)
849
+
850
+ len(vx_profile) and len(ax_profile) must be >= len(el_lengths) as the temporal duration from one point to the next
851
+ is only calculated based on the previous point.
852
+ """
853
+
854
+ # check inputs
855
+ if vx_profile.size < el_lengths.size:
856
+ raise RuntimeError("vx_profile and el_lenghts must have at least the same length!")
857
+
858
+ if ax_profile is not None and ax_profile.size < el_lengths.size:
859
+ raise RuntimeError("ax_profile and el_lenghts must have at least the same length!")
860
+
861
+ # calculate acceleration profile if required
862
+ if ax_profile is None:
863
+ ax_profile = calc_ax_profile(vx_profile=vx_profile,
864
+ el_lengths=el_lengths,
865
+ eq_length_output=False)
866
+
867
+ # calculate temporal duration of every step between two points
868
+ no_points = el_lengths.size
869
+ ax = ax_profile[:no_points]
870
+ vx = vx_profile[:no_points]
871
+ ax_nonzero = ~np.isclose(ax, 0.0)
872
+ discriminant = np.maximum(vx**2 + 2 * ax * el_lengths, 0.0)
873
+ t_steps = np.where(
874
+ ax_nonzero,
875
+ (-vx + np.sqrt(discriminant)) / np.where(ax_nonzero, ax, 1.0),
876
+ el_lengths / vx
877
+ )
878
+
879
+ # calculate temporal duration profile out of steps
880
+ t_profile = np.insert(np.cumsum(t_steps), 0, 0.0) + t_start
881
+
882
+ return t_profile
883
+
884
+
885
+ def curvature_profile(points, s=3):
886
+ """
887
+
888
+ .. description::
889
+ Fits a parametric spline to a set of 2D points and computes the curvature profile at each point.
890
+
891
+ .. inputs::
892
+ :param points: array of 2D coordinates [[x0, y0], [x1, y1], ...].
893
+ :type points: np.ndarray
894
+ :param s: smoothing factor for the spline fit (passed to splprep).
895
+ :type s: float
896
+
897
+ .. outputs::
898
+ :return tck: spline representation (knots, coefficients, degree) as returned by splprep.
899
+ :rtype tck: tuple
900
+ :return curvatures: curvature values at each input point in rad/m.
901
+ :rtype curvatures: np.ndarray
902
+ """
903
+
904
+ points = np.array(points)
905
+ x = points[:, 0]
906
+ y = points[:, 1]
907
+
908
+ # Fit the spline to the points
909
+ tck, u = splprep([x, y], s=s)
910
+
911
+ # Evaluate first and second derivatives of the spline
912
+ dx, dy = splev(u, tck, der=1)
913
+ ddx, ddy = splev(u, tck, der=2)
914
+
915
+ # Compute curvature
916
+ curvatures = (dx * ddy - dy * ddx) / np.power(dx**2 + dy**2, 1.5)
917
+
918
+ return tck, curvatures
919
+
920
+ def curvature_profile2(reftrack):
921
+ """
922
+
923
+ .. description::
924
+ Computes the curvature profile of a closed reference track using cubic spline coefficients. Splines are
925
+ calculated via calc_splines and curvature is derived analytically from the first and second derivatives.
926
+
927
+ .. inputs::
928
+ :param reftrack: reference track array where columns 0 and 1 are the x and y coordinates respectively.
929
+ :type reftrack: np.ndarray
930
+
931
+ .. outputs::
932
+ :return curv: curvature profile of the reference track in rad/m.
933
+ :rtype curv: np.ndarray
934
+ :return coeffs_x: cubic spline coefficients for the x coordinate.
935
+ :rtype coeffs_x: np.ndarray
936
+ :return coeffs_y: cubic spline coefficients for the y coordinate.
937
+ :rtype coeffs_y: np.ndarray
938
+ """
939
+
940
+ coeffs_x, coeffs_y, M, normvec_norm2 = calc_splines(path=np.vstack((reftrack[:, 0:2], reftrack[0, 0:2])))
941
+ # dx[i] = (1/ds_f[i])*coeffs_x[i,1]
942
+ # dy[i] = (1/ds_f[i])*coeffs_y[i,1]
943
+ # ddx[i] = (1/ds_f[i])**2*2*coeffs_x[i,2]
944
+ # ddy[i] = (1/ds_f[i])**2*2*coeffs_y[i,2]
945
+ dx = coeffs_x[:, 1]
946
+ dy = coeffs_y[:, 1]
947
+ ddx = 2 * coeffs_x[:, 2]
948
+ ddy = 2 * coeffs_y[:, 2]
949
+ curv = (dx * ddy - dy * ddx) / np.power(dx**2 + dy**2, 1.5)
950
+
951
+ return curv,coeffs_x,coeffs_y
952
+
953
+ def cumulative_distances(distances):
954
+ """
955
+
956
+ .. description::
957
+ Computes cumulative distances along a path, starting from zero, given segment lengths.
958
+
959
+ .. inputs::
960
+ :param distances: array of segment lengths between consecutive path points.
961
+ :type distances: np.ndarray
962
+
963
+ .. outputs::
964
+ :return cumulative_d: cumulative distance array starting at 0.0, length equal to len(distances) - 1 + 1.
965
+ :rtype cumulative_d: np.ndarray
966
+ """
967
+
968
+ cumulative_d = np.cumsum(distances[:-1])
969
+ cumulative_d = np.insert(cumulative_d, 0, 0)
970
+ return cumulative_d