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.
OptiLine/utils.py ADDED
@@ -0,0 +1,1656 @@
1
+ import math
2
+
3
+ import matplotlib.pyplot as plt
4
+ import numpy as np
5
+ import quadprog
6
+ from scipy import interpolate
7
+ from scipy import optimize
8
+ from scipy import spatial
9
+ from typing import Union
10
+
11
+ def calc_spline_lengths(coeffs_x: np.ndarray,
12
+ coeffs_y: np.ndarray,
13
+ quickndirty: bool = False,
14
+ no_interp_points: int = 15) -> np.ndarray:
15
+ """
16
+ Calculate spline lengths for third order splines defining x- and y-coordinates using intermediate steps.
17
+
18
+ Parameters
19
+ ----------
20
+ coeffs_x : np.ndarray
21
+ Coefficient matrix of the x splines with shape (n_splines, 4).
22
+ coeffs_y : np.ndarray
23
+ Coefficient matrix of the y splines with shape (n_splines, 4).
24
+ quickndirty : bool, optional
25
+ If True, returns approximate lengths using Euclidean distance between start and end points.
26
+ no_interp_points : int, optional
27
+ Number of interpolation steps for length calculation (default is 15).
28
+
29
+ Returns
30
+ -------
31
+ np.ndarray
32
+ Array of spline segment lengths.
33
+
34
+ Notes
35
+ -----
36
+ Assumes cubic splines with coefficients in order: [a0, a1, a2, a3].
37
+ """
38
+ if coeffs_x.shape[0] != coeffs_y.shape[0]:
39
+ raise ValueError("Coefficient matrices must have the same number of splines!")
40
+
41
+ if coeffs_x.ndim == 1:
42
+ coeffs_x = np.expand_dims(coeffs_x, 0)
43
+ coeffs_y = np.expand_dims(coeffs_y, 0)
44
+
45
+ # Calculating the lengths
46
+ if quickndirty:
47
+ end_x = np.sum(coeffs_x, axis=1)
48
+ end_y = np.sum(coeffs_y, axis=1)
49
+ start_x = coeffs_x[:, 0]
50
+ start_y = coeffs_y[:, 0]
51
+ return np.hypot(end_x - start_x, end_y - start_y)
52
+
53
+ t_steps = np.linspace(0.0, 1.0, no_interp_points)
54
+ vander_t = np.vander(t_steps, N=4, increasing=True)
55
+
56
+ x_vals_all = vander_t @ coeffs_x.T # shape: (no_interp_points, no_splines)
57
+ y_vals_all = vander_t @ coeffs_y.T
58
+ dx = np.diff(x_vals_all, axis=0)
59
+ dy = np.diff(y_vals_all, axis=0)
60
+ spline_lengths = np.sum(np.sqrt(dx**2 + dy**2), axis=0)
61
+
62
+ return spline_lengths
63
+
64
+ import numpy as np
65
+ import math
66
+
67
+ def interp_splines(coeffs_x, coeffs_y, no_interp_points=None, stepnum_fixed=None,
68
+ spline_lengths=None, stepsize_approx=None, incl_last_point=False):
69
+ """
70
+ Interpolates 2D spline segments with given polynomial coefficients.
71
+
72
+ Parameters
73
+ ----------
74
+ coeffs_x : ndarray
75
+ Shape (n, 4), where each row is [a0, a1, a2, a3] for a cubic x spline segment.
76
+ coeffs_y : ndarray
77
+ Same as coeffs_x, but for y coordinates.
78
+ no_interp_points : int, optional
79
+ Number of interpolated points. Only needed if `stepnum_fixed` is None.
80
+ stepnum_fixed : list or ndarray, optional
81
+ Number of interpolation points per segment (can vary per segment).
82
+ spline_lengths : list or ndarray, optional
83
+ Lengths of spline segments (used with stepsize_approx).
84
+ stepsize_approx : float, optional
85
+ Approximate step size for interpolation.
86
+ incl_last_point : bool, optional
87
+ If True, include final point at t = 1 of last segment.
88
+
89
+ Returns
90
+ -------
91
+ path_interp : ndarray
92
+ Interpolated 2D path of shape (no_interp_points, 2).
93
+ spline_inds : ndarray
94
+ Indices of spline segment for each interpolated point.
95
+ t_values : ndarray
96
+ Normalized t value [0, 1] of point within its segment.
97
+ dists_interp : ndarray or None
98
+ Distance along the path for each point (only if stepsize_approx is used).
99
+ """
100
+ # check sizes
101
+ if coeffs_x.shape[0] != coeffs_y.shape[0]:
102
+ raise RuntimeError("Coefficient matrices must have the same length!")
103
+
104
+ if spline_lengths is not None and coeffs_x.shape[0] != spline_lengths.size:
105
+ raise RuntimeError("coeffs_x/y and spline_lengths must have the same length!")
106
+
107
+ # check if coeffs_x and coeffs_y have exactly two dimensions and raise error otherwise
108
+ if not (coeffs_x.ndim == 2 and coeffs_y.ndim == 2):
109
+ raise RuntimeError("Coefficient matrices do not have two dimensions!")
110
+
111
+ # check if step size specification is valid
112
+ if (stepsize_approx is None and stepnum_fixed is None) \
113
+ or (stepsize_approx is not None and stepnum_fixed is not None):
114
+ raise RuntimeError("Provide one of 'stepsize_approx' and 'stepnum_fixed' and set the other to 'None'!")
115
+
116
+ if stepnum_fixed is not None and len(stepnum_fixed) != coeffs_x.shape[0]:
117
+ raise RuntimeError("The provided list 'stepnum_fixed' must hold an entry for every spline!")
118
+
119
+
120
+ coeffs_x = np.array(coeffs_x)
121
+ coeffs_y = np.array(coeffs_y)
122
+ dists_interp = None
123
+
124
+ if stepsize_approx is not None:
125
+ spline_lengths = np.asarray(spline_lengths).flatten()
126
+ dists_cum = np.cumsum(spline_lengths)
127
+
128
+ no_interp_points = math.ceil(dists_cum[-1] / stepsize_approx) + 1
129
+ dists_interp = np.linspace(0.0, dists_cum[-1], no_interp_points)
130
+
131
+ path_interp = np.zeros((no_interp_points, 2))
132
+ spline_inds = np.zeros(no_interp_points, dtype=int)
133
+ t_values = np.zeros(no_interp_points)
134
+
135
+ js = np.searchsorted(dists_cum, dists_interp[:-1])
136
+ spline_inds[:-1] = js
137
+ d0s = np.where(js > 0, dists_cum[js - 1], 0.0)
138
+ t_vals = (dists_interp[:-1] - d0s) / spline_lengths[js]
139
+ t_values[:-1] = t_vals
140
+ t2 = t_vals ** 2
141
+ t3 = t_vals ** 3
142
+ path_interp[:-1, 0] = coeffs_x[js, 0] + coeffs_x[js, 1] * t_vals + coeffs_x[js, 2] * t2 + coeffs_x[js, 3] * t3
143
+ path_interp[:-1, 1] = coeffs_y[js, 0] + coeffs_y[js, 1] * t_vals + coeffs_y[js, 2] * t2 + coeffs_y[js, 3] * t3
144
+
145
+ elif stepnum_fixed is not None:
146
+ no_interp_points = np.sum(stepnum_fixed)
147
+ path_interp = np.zeros((no_interp_points, 2))
148
+ spline_inds = np.zeros(no_interp_points, dtype=int)
149
+ t_values = np.zeros(no_interp_points)
150
+
151
+ j = 0
152
+ for i, steps in enumerate(stepnum_fixed):
153
+ if i < len(stepnum_fixed) - 1:
154
+ t_values[j:j+steps-1] = np.linspace(0, 1, steps)[:-1]
155
+ spline_inds[j:j+steps-1] = i
156
+ j += steps - 1
157
+ else:
158
+ t_values[j:j+steps] = np.linspace(0, 1, steps)
159
+ spline_inds[j:j+steps] = i
160
+ j += steps
161
+
162
+ t_set = np.column_stack((np.ones(no_interp_points), t_values, t_values**2, t_values**3))
163
+
164
+ # Repeat coefficients according to stepnum_fixed (excluding last point per segment)
165
+ coeffs_x_rep = np.vstack([
166
+ np.tile(row, (n - 1 if i < len(stepnum_fixed) - 1 else n, 1))
167
+ for i, (row, n) in enumerate(zip(coeffs_x, stepnum_fixed))
168
+ ])
169
+ coeffs_y_rep = np.vstack([
170
+ np.tile(row, (n - 1 if i < len(stepnum_fixed) - 1 else n, 1))
171
+ for i, (row, n) in enumerate(zip(coeffs_y, stepnum_fixed))
172
+ ])
173
+
174
+ path_interp[:, 0] = np.sum(coeffs_x_rep * t_set, axis=1)
175
+ path_interp[:, 1] = np.sum(coeffs_y_rep * t_set, axis=1)
176
+
177
+ else:
178
+ raise ValueError("Either stepsize_approx or stepnum_fixed must be provided.")
179
+
180
+ # Include the final point at t=1 of the last segment if requested
181
+ if incl_last_point:
182
+ path_interp[-1, 0] = np.dot(coeffs_x[-1], [1, 1, 1, 1])
183
+ path_interp[-1, 1] = np.dot(coeffs_y[-1], [1, 1, 1, 1])
184
+ spline_inds[-1] = coeffs_x.shape[0] - 1
185
+ t_values[-1] = 1.0
186
+ else:
187
+ path_interp = path_interp[:-1]
188
+ spline_inds = spline_inds[:-1]
189
+ t_values = t_values[:-1]
190
+ if dists_interp is not None:
191
+ dists_interp = dists_interp[:-1]
192
+
193
+ return path_interp, spline_inds, t_values, dists_interp
194
+
195
+
196
+ def calc_splines(path: np.ndarray,
197
+ el_lengths: np.ndarray = None,
198
+ psi_s: float = None,
199
+ psi_e: float = None,
200
+ use_dist_scaling: bool = True) -> tuple:
201
+ """
202
+
203
+ .. description::
204
+ Solve for curvature continuous cubic splines (spline parameter t) between given points i (splines evaluated at
205
+ t = 0 and t = 1). The splines must be set up separately for x- and y-coordinate.
206
+
207
+ Spline equations:
208
+ P_{x,y}(t) = a_3 * t³ + a_2 * t² + a_1 * t + a_0
209
+ P_{x,y}'(t) = 3a_3 * t² + 2a_2 * t + a_1
210
+ P_{x,y}''(t) = 6a_3 * t + 2a_2
211
+
212
+ a * {x; y} = {b_x; b_y}
213
+
214
+ .. inputs::
215
+ :param path: x and y coordinates as the basis for the spline construction (closed or unclosed). If
216
+ path is provided unclosed, headings psi_s and psi_e are required!
217
+ :type path: np.ndarray
218
+ :param el_lengths: distances between path points (closed or unclosed). The input is optional. The distances
219
+ are required for the scaling of heading and curvature values. They are calculated using
220
+ euclidian distances if required but not supplied.
221
+ :type el_lengths: np.ndarray
222
+ :param psi_s: orientation of the {start, end} point.
223
+ :type psi_s: float
224
+ :param psi_e: orientation of the {start, end} point.
225
+ :type psi_e: float
226
+ :param use_dist_scaling: bool flag to indicate if heading and curvature scaling should be performed. This should
227
+ be done if the distances between the points in the path are not equal.
228
+ :type use_dist_scaling: bool
229
+
230
+ .. outputs::
231
+ :return x_coeff: spline coefficients of the x-component.
232
+ :rtype x_coeff: np.ndarray
233
+ :return y_coeff: spline coefficients of the y-component.
234
+ :rtype y_coeff: np.ndarray
235
+ :return M: LES coefficients.
236
+ :rtype M: np.ndarray
237
+ :return normvec_normalized: normalized normal vectors [x, y].
238
+ :rtype normvec_normalized: np.ndarray
239
+
240
+ .. notes::
241
+ Outputs are always unclosed!
242
+
243
+ path and el_lengths inputs can either be closed or unclosed, but must be consistent! The function detects
244
+ automatically if the path was inserted closed.
245
+
246
+ Coefficient matrices have the form a_0i, a_1i * t, a_2i * t^2, a_3i * t^3.
247
+ """
248
+
249
+ # check if path is closed
250
+ if np.all(np.isclose(path[0], path[-1])) and psi_s is None:
251
+ closed = True
252
+ else:
253
+ closed = False
254
+
255
+ # check inputs
256
+ if not closed and (psi_s is None or psi_e is None):
257
+ raise RuntimeError("Headings must be provided for unclosed spline calculation!")
258
+
259
+ if el_lengths is not None and path.shape[0] != el_lengths.size + 1:
260
+ raise RuntimeError("el_lengths input must be one element smaller than path input!")
261
+
262
+ # if distances between path coordinates are not provided but required, calculate euclidean distances as el_lengths
263
+ if use_dist_scaling and el_lengths is None:
264
+ el_lengths = np.sqrt(np.sum(np.power(np.diff(path, axis=0), 2), axis=1))
265
+ elif el_lengths is not None:
266
+ el_lengths = np.copy(el_lengths)
267
+
268
+ # if closed and use_dist_scaling active append element length in order to obtain overlapping elements for proper
269
+ # scaling of the last element afterwards
270
+ if use_dist_scaling and closed:
271
+ el_lengths = np.append(el_lengths, el_lengths[0])
272
+
273
+ # get number of splines
274
+ no_splines = path.shape[0] - 1
275
+
276
+ # calculate scaling factors between every pair of splines
277
+ if use_dist_scaling:
278
+ scaling = el_lengths[:-1] / el_lengths[1:]
279
+ else:
280
+ scaling = np.ones(no_splines - 1)
281
+
282
+ # ------------------------------------------------------------------------------------------------------------------
283
+ # DEFINE LINEAR EQUATION SYSTEM ------------------------------------------------------------------------------------
284
+ # ------------------------------------------------------------------------------------------------------------------
285
+
286
+ # M_{x,y} * a_{x,y} = b_{x,y}) with a_{x,y} being the desired spline param
287
+ # *4 because of 4 parameters in cubic spline
288
+ M = np.zeros((no_splines * 4, no_splines * 4))
289
+ b_x = np.zeros((no_splines * 4, 1))
290
+ b_y = np.zeros((no_splines * 4, 1))
291
+
292
+ # create template for M array entries
293
+ # row 1: beginning of current spline should be placed on current point (t = 0)
294
+ # row 2: end of current spline should be placed on next point (t = 1)
295
+ # row 3: heading at end of current spline should be equal to heading at beginning of next spline (t = 1 and t = 0)
296
+ # row 4: curvature at end of current spline should be equal to curvature at beginning of next spline (t = 1 and
297
+ # t = 0)
298
+ template_M = np.array( # current point | next point | bounds
299
+ [[1, 0, 0, 0, 0, 0, 0, 0], # a_0i = {x,y}_i
300
+ [1, 1, 1, 1, 0, 0, 0, 0], # a_0i + a_1i + a_2i + a_3i = {x,y}_i+1
301
+ [0, 1, 2, 3, 0, -1, 0, 0], # _ a_1i + 2a_2i + 3a_3i - a_1i+1 = 0
302
+ [0, 0, 2, 6, 0, 0, -2, 0]]) # _ 2a_2i + 6a_3i - 2a_2i+1 = 0
303
+
304
+ for i in range(no_splines):
305
+ j = i * 4
306
+
307
+ if i < no_splines - 1:
308
+ M[j: j + 4, j: j + 8] = template_M
309
+
310
+ M[j + 2, j + 5] *= scaling[i]
311
+ M[j + 3, j + 6] *= math.pow(scaling[i], 2)
312
+
313
+ else:
314
+ # no curvature and heading bounds on last element (handled afterwards)
315
+ M[j: j + 2, j: j + 4] = [[1, 0, 0, 0],
316
+ [1, 1, 1, 1]]
317
+
318
+ b_x[j: j + 2] = [[path[i, 0]],
319
+ [path[i + 1, 0]]]
320
+ b_y[j: j + 2] = [[path[i, 1]],
321
+ [path[i + 1, 1]]]
322
+
323
+ # ------------------------------------------------------------------------------------------------------------------
324
+ # SET BOUNDARY CONDITIONS FOR LAST AND FIRST POINT -----------------------------------------------------------------
325
+ # ------------------------------------------------------------------------------------------------------------------
326
+
327
+ if not closed:
328
+ # if the path is unclosed we want to fix heading at the start and end point of the path (curvature cannot be
329
+ # determined in this case) -> set heading boundary conditions
330
+
331
+ # heading start point
332
+ M[-2, 1] = 1 # heading start point (evaluated at t = 0)
333
+
334
+ if el_lengths is None:
335
+ el_length_s = 1.0
336
+ else:
337
+ el_length_s = el_lengths[0]
338
+
339
+ b_x[-2] = math.cos(psi_s + math.pi / 2) * el_length_s
340
+ b_y[-2] = math.sin(psi_s + math.pi / 2) * el_length_s
341
+
342
+ # heading end point
343
+ M[-1, -4:] = [0, 1, 2, 3] # heading end point (evaluated at t = 1)
344
+
345
+ if el_lengths is None:
346
+ el_length_e = 1.0
347
+ else:
348
+ el_length_e = el_lengths[-1]
349
+
350
+ b_x[-1] = math.cos(psi_e + math.pi / 2) * el_length_e
351
+ b_y[-1] = math.sin(psi_e + math.pi / 2) * el_length_e
352
+
353
+ else:
354
+ # heading boundary condition (for a closed spline)
355
+ M[-2, 1] = scaling[-1]
356
+ M[-2, -3:] = [-1, -2, -3]
357
+ # b_x[-2] = 0
358
+ # b_y[-2] = 0
359
+
360
+ # curvature boundary condition (for a closed spline)
361
+ M[-1, 2] = 2 * math.pow(scaling[-1], 2)
362
+ M[-1, -2:] = [-2, -6]
363
+ # b_x[-1] = 0
364
+ # b_y[-1] = 0
365
+
366
+ # ------------------------------------------------------------------------------------------------------------------
367
+ # SOLVE ------------------------------------------------------------------------------------------------------------
368
+ # ------------------------------------------------------------------------------------------------------------------
369
+
370
+ x_les = np.squeeze(np.linalg.solve(M, b_x)) # squeeze removes single-dimensional entries
371
+ y_les = np.squeeze(np.linalg.solve(M, b_y))
372
+
373
+ # get coefficients of every piece into one row -> reshape
374
+ coeffs_x = np.reshape(x_les, (no_splines, 4))
375
+ coeffs_y = np.reshape(y_les, (no_splines, 4))
376
+
377
+ # get normal vector (behind used here instead of ahead for consistency with other functions) (second coefficient of
378
+ # cubic splines is relevant for the heading)
379
+ normvec = np.stack((coeffs_y[:, 1], -coeffs_x[:, 1]), axis=1)
380
+
381
+ # normalize normal vectors
382
+ norm_factors = 1.0 / np.sqrt(np.sum(np.power(normvec, 2), axis=1))
383
+ normvec_normalized = np.expand_dims(norm_factors, axis=1) * normvec
384
+
385
+ return coeffs_x, coeffs_y, M, normvec_normalized
386
+
387
+
388
+ def create_raceline(refline: np.ndarray,
389
+ normvectors: np.ndarray,
390
+ alpha: np.ndarray,
391
+ stepsize_interp: float,
392
+ el_lengths: np.ndarray = None) -> tuple:
393
+ """
394
+
395
+ .. description::
396
+ This function includes the algorithm part connected to the interpolation of the raceline after the optimization.
397
+
398
+ .. inputs::
399
+ :param refline: array containing the track reference line [x, y] (unit is meter, must be unclosed!)
400
+ :type refline: np.ndarray
401
+ :param normvectors: normalized normal vectors for every point of the reference line [x_component, y_component]
402
+ (unit is meter, must be unclosed!)
403
+ :type normvectors: np.ndarray
404
+ :param alpha: solution vector of the optimization problem containing the lateral shift in m for every point.
405
+ :type alpha: np.ndarray
406
+ :param stepsize_interp: stepsize in meters which is used for the interpolation after the raceline creation.
407
+ :type stepsize_interp: float
408
+
409
+ .. outputs::
410
+ :return raceline_interp: interpolated raceline [x, y] in m.
411
+ :rtype raceline_interp: np.ndarray
412
+ :return A_raceline: linear equation system matrix of the splines on the raceline.
413
+ :rtype A_raceline: np.ndarray
414
+ :return coeffs_x_raceline: spline coefficients of the x-component.
415
+ :rtype coeffs_x_raceline: np.ndarray
416
+ :return coeffs_y_raceline: spline coefficients of the y-component.
417
+ :rtype coeffs_y_raceline: np.ndarray
418
+ :return spline_inds_raceline_interp: contains the indices of the splines that hold the interpolated points.
419
+ :rtype spline_inds_raceline_interp: np.ndarray
420
+ :return t_values_raceline_interp: containts the relative spline coordinate values (t) of every point on the
421
+ splines.
422
+ :rtype t_values_raceline_interp: np.ndarray
423
+ :return s_raceline_interp: total distance in m (i.e. s coordinate) up to every interpolation point.
424
+ :rtype s_raceline_interp: np.ndarray
425
+ :return spline_lengths_raceline: lengths of the splines on the raceline in m.
426
+ :rtype spline_lengths_raceline: np.ndarray
427
+ :return el_lengths_raceline_interp_cl: distance between every two points on interpolated raceline in m (closed!).
428
+ :rtype el_lengths_raceline_interp_cl: np.ndarray
429
+ """
430
+
431
+ # calculate raceline on the basis of the optimized alpha values
432
+ raceline = refline + np.expand_dims(alpha, 1) * normvectors
433
+
434
+ # calculate new splines on the basis of the raceline
435
+ raceline_cl = np.vstack((raceline, raceline[0]))
436
+ if el_lengths is None:
437
+ coeffs_x_raceline, coeffs_y_raceline, A_raceline, normvectors_raceline =calc_splines(path=raceline_cl,use_dist_scaling=False)
438
+ else:
439
+ coeffs_x_raceline, coeffs_y_raceline, A_raceline, normvectors_raceline =calc_splines(path=raceline_cl,el_lengths=el_lengths)
440
+
441
+ # calculate new spline lengths
442
+ spline_lengths_raceline =calc_spline_lengths(coeffs_x=coeffs_x_raceline,
443
+ coeffs_y=coeffs_y_raceline)
444
+
445
+ # interpolate splines for evenly spaced raceline points
446
+ raceline_interp, spline_inds_raceline_interp, t_values_raceline_interp, s_raceline_interp = interp_splines(spline_lengths=spline_lengths_raceline,
447
+ coeffs_x=coeffs_x_raceline,
448
+ coeffs_y=coeffs_y_raceline,
449
+ incl_last_point=False,
450
+ stepsize_approx=stepsize_interp)
451
+
452
+ # calculate element lengths
453
+ s_tot_raceline = float(np.sum(spline_lengths_raceline))
454
+ el_lengths_raceline_interp = np.diff(s_raceline_interp)
455
+ el_lengths_raceline_interp_cl = np.append(el_lengths_raceline_interp, s_tot_raceline - s_raceline_interp[-1])
456
+
457
+ return raceline_interp, A_raceline, coeffs_x_raceline, coeffs_y_raceline, spline_inds_raceline_interp, \
458
+ t_values_raceline_interp, s_raceline_interp, spline_lengths_raceline, el_lengths_raceline_interp_cl
459
+
460
+ def conv_filt(signal: np.ndarray,
461
+ filt_window: int,
462
+ closed: bool) -> np.ndarray:
463
+ """
464
+
465
+ .. description::
466
+ Filter a given temporal signal using a convolution (moving average) filter.
467
+
468
+ .. inputs::
469
+ :param signal: temporal signal that should be filtered (always unclosed).
470
+ :type signal: np.ndarray
471
+ :param filt_window: filter window size for moving average filter (must be odd).
472
+ :type filt_window: int
473
+ :param closed: flag showing if the signal can be considered as closable, e.g. for velocity profiles.
474
+ :type closed: bool
475
+
476
+ .. outputs::
477
+ :return signal_filt: filtered input signal (always unclosed).
478
+ :rtype signal_filt: np.ndarray
479
+
480
+ .. notes::
481
+ signal input is always unclosed!
482
+
483
+ len(signal) = len(signal_filt)
484
+ """
485
+
486
+ # check if window width is odd
487
+ if not filt_window % 2 == 1:
488
+ raise RuntimeError("Window width of moving average filter must be odd!")
489
+
490
+ # calculate half window width - 1
491
+ w_window_half = int((filt_window - 1) / 2)
492
+
493
+ # apply filter
494
+ if closed:
495
+ # temporarily add points in front of and behind signal
496
+ signal_tmp = np.concatenate((signal[-w_window_half:], signal, signal[:w_window_half]), axis=0)
497
+
498
+ # apply convolution filter used as a moving average filter and remove temporary points
499
+ signal_filt = np.convolve(signal_tmp,
500
+ np.ones(filt_window) / float(filt_window),
501
+ mode="same")[w_window_half:-w_window_half]
502
+
503
+ else:
504
+ # implementation 1: include boundaries during filtering
505
+ # no_points = signal.size
506
+ # signal_filt = np.zeros(no_points)
507
+ #
508
+ # for i in range(no_points):
509
+ # if i < w_window_half:
510
+ # signal_filt[i] = np.average(signal[:i + w_window_half + 1])
511
+ #
512
+ # elif i < no_points - w_window_half:
513
+ # signal_filt[i] = np.average(signal[i - w_window_half:i + w_window_half + 1])
514
+ #
515
+ # else:
516
+ # signal_filt[i] = np.average(signal[i - w_window_half:])
517
+
518
+ # implementation 2: start filtering at w_window_half and stop at -w_window_half
519
+ signal_filt = np.copy(signal)
520
+ signal_filt[w_window_half:-w_window_half] = np.convolve(signal,
521
+ np.ones(filt_window) / float(filt_window),
522
+ mode="same")[w_window_half:-w_window_half]
523
+
524
+ return signal_filt
525
+
526
+ def import_veh_dyn_info(ggv_import_path: str = None,
527
+ ax_max_machines_import_path: str = None) -> tuple:
528
+ """
529
+ .. description::
530
+ This function imports the required vehicle dynamics information from several files: The vehicle ggv diagram
531
+ ([vx, ax_max, ay_max], velocity in m/s, accelerations in m/s2) and the ax_max_machines array containing the
532
+ longitudinal acceleration limits by the electrical motors ([vx, ax_max_machines], velocity in m/s, acceleration in
533
+ m/s2).
534
+
535
+ .. inputs::
536
+ :param ggv_import_path: Path to the ggv csv file.
537
+ :type ggv_import_path: str
538
+ :param ax_max_machines_import_path: Path to the ax_max_machines csv file.
539
+ :type ax_max_machines_import_path: str
540
+
541
+ .. outputs::
542
+ :return ggv: ggv diagram
543
+ :rtype ggv: np.ndarray
544
+ :return ax_max_machines: ax_max_machines array
545
+ :rtype ax_max_machines: np.ndarray
546
+ """
547
+
548
+ # GGV --------------------------------------------------------------------------------------------------------------
549
+ if ggv_import_path is not None:
550
+
551
+ # load csv
552
+ with open(ggv_import_path, "rb") as fh:
553
+ ggv = np.loadtxt(fh, comments='#', delimiter=",")
554
+
555
+ # expand dimension in case of a single row
556
+ if ggv.ndim == 1:
557
+ ggv = np.expand_dims(ggv, 0)
558
+
559
+ # check columns
560
+ if ggv.shape[1] != 3:
561
+ raise RuntimeError("ggv diagram must consist of the three columns [vx, ax_max, ay_max]!")
562
+
563
+ # check values
564
+ invalid_1 = ggv[:, 0] < 0.0 # assure velocities > 0.0
565
+ invalid_2 = ggv[:, 1:] > 50.0 # assure valid maximum accelerations
566
+ invalid_3 = ggv[:, 1] < 0.0 # assure positive accelerations
567
+ invalid_4 = ggv[:, 2] < 0.0 # assure positive accelerations
568
+
569
+ if np.any(invalid_1) or np.any(invalid_2) or np.any(invalid_3) or np.any(invalid_4):
570
+ raise RuntimeError("ggv seems unreasonable!")
571
+
572
+ else:
573
+ ggv = None
574
+
575
+ # AX_MAX_MACHINES --------------------------------------------------------------------------------------------------
576
+ if ax_max_machines_import_path is not None:
577
+
578
+ # load csv
579
+ with open(ax_max_machines_import_path, "rb") as fh:
580
+ ax_max_machines = np.loadtxt(fh, comments='#', delimiter=",")
581
+
582
+ # expand dimension in case of a single row
583
+ if ax_max_machines.ndim == 1:
584
+ ax_max_machines = np.expand_dims(ax_max_machines, 0)
585
+
586
+ # check columns
587
+ if ax_max_machines.shape[1] != 2:
588
+ raise RuntimeError("ax_max_machines must consist of the two columns [vx, ax_max_machines]!")
589
+
590
+ # check values
591
+ invalid_1 = ax_max_machines[:, 0] < 0.0 # assure velocities > 0.0
592
+ invalid_2 = ax_max_machines[:, 1] > 20.0 # assure valid maximum accelerations
593
+ invalid_3 = ax_max_machines[:, 1] < 0.0 # assure positive accelerations
594
+
595
+ if np.any(invalid_1) or np.any(invalid_2) or np.any(invalid_3):
596
+ raise RuntimeError("ax_max_machines seems unreasonable!")
597
+
598
+ else:
599
+ ax_max_machines = None
600
+
601
+ return ggv, ax_max_machines
602
+
603
+
604
+ def normalize_psi(psi: Union[np.ndarray, float]) -> np.ndarray:
605
+ psi_out = np.sign(psi) * np.mod(np.abs(psi), 2 * math.pi)
606
+
607
+ # restrict psi to [-pi,pi[
608
+ if type(psi_out) is np.ndarray:
609
+ psi_out[psi_out >= math.pi] -= 2 * math.pi
610
+ psi_out[psi_out < -math.pi] += 2 * math.pi
611
+
612
+ else:
613
+ if psi_out >= math.pi:
614
+ psi_out -= 2 * math.pi
615
+ elif psi_out < -math.pi:
616
+ psi_out += 2 * math.pi
617
+
618
+ return psi_out
619
+
620
+ def calc_head_curv_an(coeffs_x: np.ndarray,
621
+ coeffs_y: np.ndarray,
622
+ ind_spls: np.ndarray,
623
+ t_spls: np.ndarray,
624
+ calc_curv: bool = True,
625
+ calc_dcurv: bool = False) -> tuple:
626
+ """
627
+
628
+ .. description::
629
+ Analytical calculation of heading psi, curvature kappa, and first derivative of the curvature dkappa
630
+ on the basis of third order splines for x- and y-coordinate.
631
+
632
+ .. inputs::
633
+ :param coeffs_x: coefficient matrix of the x splines with size (no_splines x 4).
634
+ :type coeffs_x: np.ndarray
635
+ :param coeffs_y: coefficient matrix of the y splines with size (no_splines x 4).
636
+ :type coeffs_y: np.ndarray
637
+ :param ind_spls: contains the indices of the splines that hold the points for which we want to calculate heading/curv.
638
+ :type ind_spls: np.ndarray
639
+ :param t_spls: containts the relative spline coordinate values (t) of every point on the splines.
640
+ :type t_spls: np.ndarray
641
+ :param calc_curv: bool flag to show if curvature should be calculated as well (kappa is set 0.0 otherwise).
642
+ :type calc_curv: bool
643
+ :param calc_dcurv: bool flag to show if first derivative of curvature should be calculated as well.
644
+ :type calc_dcurv: bool
645
+
646
+ .. outputs::
647
+ :return psi: heading at every point.
648
+ :rtype psi: float
649
+ :return kappa: curvature at every point.
650
+ :rtype kappa: float
651
+ :return dkappa: first derivative of curvature at every point (if calc_dcurv bool flag is True).
652
+ :rtype dkappa: float
653
+
654
+ .. notes::
655
+ len(ind_spls) = len(t_spls) = len(psi) = len(kappa) = len(dkappa)
656
+ """
657
+
658
+ # check inputs
659
+ if coeffs_x.shape[0] != coeffs_y.shape[0]:
660
+ raise ValueError("Coefficient matrices must have the same length!")
661
+
662
+ if ind_spls.size != t_spls.size:
663
+ raise ValueError("ind_spls and t_spls must have the same length!")
664
+
665
+ if not calc_curv and calc_dcurv:
666
+ raise ValueError("dkappa cannot be calculated without kappa!")
667
+
668
+ # ------------------------------------------------------------------------------------------------------------------
669
+ # CALCULATE HEADING ------------------------------------------------------------------------------------------------
670
+ # ------------------------------------------------------------------------------------------------------------------
671
+
672
+ # calculate required derivatives
673
+ x_d = coeffs_x[ind_spls, 1] \
674
+ + 2 * coeffs_x[ind_spls, 2] * t_spls \
675
+ + 3 * coeffs_x[ind_spls, 3] * np.power(t_spls, 2)
676
+
677
+ y_d = coeffs_y[ind_spls, 1] \
678
+ + 2 * coeffs_y[ind_spls, 2] * t_spls \
679
+ + 3 * coeffs_y[ind_spls, 3] * np.power(t_spls, 2)
680
+
681
+ # calculate heading psi (pi/2 must be substracted due to our convention that psi = 0 is north)
682
+ psi = np.arctan2(y_d, x_d) - math.pi / 2
683
+ psi = normalize_psi(psi)
684
+
685
+ # ------------------------------------------------------------------------------------------------------------------
686
+ # CALCULATE CURVATURE ----------------------------------------------------------------------------------------------
687
+ # ------------------------------------------------------------------------------------------------------------------
688
+
689
+ if calc_curv:
690
+ # calculate required derivatives
691
+ x_dd = 2 * coeffs_x[ind_spls, 2] \
692
+ + 6 * coeffs_x[ind_spls, 3] * t_spls
693
+
694
+ y_dd = 2 * coeffs_y[ind_spls, 2] \
695
+ + 6 * coeffs_y[ind_spls, 3] * t_spls
696
+
697
+ # calculate curvature kappa
698
+ kappa = (x_d * y_dd - y_d * x_dd) / np.power(np.power(x_d, 2) + np.power(y_d, 2), 1.5)
699
+
700
+ else:
701
+ kappa = 0.0
702
+
703
+ # ------------------------------------------------------------------------------------------------------------------
704
+ # CALCULATE FIRST DERIVATIVE OF CURVATURE --------------------------------------------------------------------------
705
+ # ------------------------------------------------------------------------------------------------------------------
706
+
707
+ if calc_dcurv:
708
+ # calculate required derivatives
709
+ x_ddd = 6 * coeffs_x[ind_spls, 3]
710
+
711
+ y_ddd = 6 * coeffs_y[ind_spls, 3]
712
+
713
+ # calculate first derivative of curvature dkappa
714
+ dkappa = ((np.power(x_d, 2) + np.power(y_d, 2)) * (x_d * y_ddd - y_d * x_ddd) -
715
+ 3 * (x_d * y_dd - y_d * x_dd) * (x_d * x_dd + y_d * y_dd)) / \
716
+ np.power(np.power(x_d, 2) + np.power(y_d, 2), 3)
717
+
718
+ return psi, kappa, dkappa
719
+
720
+ else:
721
+
722
+ return psi, kappa
723
+
724
+
725
+
726
+ def H_f(reftrack: np.ndarray,
727
+ normvectors: np.ndarray,
728
+ A: np.ndarray,
729
+ kappa_bound: float,
730
+ w_veh: float,
731
+ print_debug: bool = False,
732
+ plot_debug: bool = False,
733
+ closed: bool = True,
734
+ psi_s: float = None,
735
+ psi_e: float = None,
736
+ fix_s: bool = False,
737
+ fix_e: bool = False) -> tuple:
738
+
739
+ """
740
+ .. description::
741
+ This function uses outputs the data neede to solve the min curvature problem
742
+
743
+ Please refer to the paper for further information:
744
+ Heilmeier, Wischnewski, Hermansdorfer, Betz, Lienkamp, Lohmann
745
+ Minimum Curvature Trajectory Planning and Control for an Autonomous Racecar
746
+ DOI: 10.1080/00423114.2019.1631455
747
+
748
+ .. inputs::
749
+ :param reftrack: array containing the reference track, i.e. a reference line and the according track widths to
750
+ the right and to the left [x, y, w_tr_right, w_tr_left] (unit is meter, must be unclosed!)
751
+ :type reftrack: np.ndarray
752
+ :param normvectors: normalized normal vectors for every point of the reference track [x_component, y_component]
753
+ (unit is meter, must be unclosed!)
754
+ :type normvectors: np.ndarray
755
+ :param A: linear equation system matrix for splines (applicable for both, x and y direction)
756
+ -> System matrices have the form a_i, b_i * t, c_i * t^2, d_i * t^3
757
+ -> see calc_splines.py for further information or to obtain this matrix
758
+ :type A: np.ndarray
759
+ :param kappa_bound: curvature boundary to consider during optimization.
760
+ :type kappa_bound: float
761
+ :param w_veh: vehicle width in m. It is considered during the calculation of the allowed deviations from the
762
+ reference line.
763
+ :type w_veh: float
764
+ :param print_debug: bool flag to print debug messages.
765
+ :type print_debug: bool
766
+ :param plot_debug: bool flag to plot the curvatures that are calculated based on the original linearization and on
767
+ a linearization around the solution.
768
+ :type plot_debug: bool
769
+ :param closed: bool flag specifying whether a closed or unclosed track should be assumed
770
+ :type closed: bool
771
+ :param psi_s: heading to be enforced at the first point for unclosed tracks
772
+ :type psi_s: float
773
+ :param psi_e: heading to be enforced at the last point for unclosed tracks
774
+ :type psi_e: float
775
+ :param fix_s: determines if start point is fixed to reference line for unclosed tracks
776
+ :type fix_s: bool
777
+ :param fix_e: determines if last point is fixed to reference line for unclosed tracks
778
+ :type fix_e: bool
779
+
780
+ .. outputs::
781
+ :return H: Matrix H defined in the mentioned paper
782
+ :rtype alpha_mincurv: np.ndarray
783
+ :return f: Matrix f defined in the mentioned paper
784
+ :rtype curv_error_max: np.ndarray
785
+ :return G: Constrait matrix
786
+ :rtype alpha_mincurv: np.ndarray
787
+ :return h: Constraint on norm of alpha defined in thementioned paper
788
+ :rtype curv_error_max: np.ndarray
789
+ """
790
+
791
+ no_points = reftrack.shape[0]
792
+
793
+ no_splines = no_points
794
+ if not closed:
795
+ no_splines -= 1
796
+
797
+ # check inputs
798
+ if no_points != normvectors.shape[0]:
799
+ raise RuntimeError("Array size of reftrack should be the same as normvectors!")
800
+
801
+ if (no_points * 4 != A.shape[0] and closed) or (no_splines * 4 != A.shape[0] and not closed)\
802
+ or A.shape[0] != A.shape[1]:
803
+ raise RuntimeError("Spline equation system matrix A has wrong dimensions!")
804
+
805
+ # create extraction matrix -> only b_i coefficients of the solved linear equation system are needed for gradient
806
+ # information
807
+ A_ex_b = np.zeros((no_points, no_splines * 4), dtype=int)
808
+
809
+ for i in range(no_splines):
810
+ A_ex_b[i, i * 4 + 1] = 1 # 1 * b_ix = E_x * x
811
+
812
+ # coefficients for end of spline (t = 1)
813
+ if not closed:
814
+ A_ex_b[-1, -4:] = np.array([0, 1, 2, 3])
815
+
816
+ # create extraction matrix -> only c_i coefficients of the solved linear equation system are needed for curvature
817
+ # information
818
+ A_ex_c = np.zeros((no_points, no_splines * 4), dtype=int)
819
+
820
+ for i in range(no_splines):
821
+ A_ex_c[i, i * 4 + 2] = 2 # 2 * c_ix = D_x * x
822
+
823
+ # coefficients for end of spline (t = 1)
824
+ if not closed:
825
+ A_ex_c[-1, -4:] = np.array([0, 0, 2, 6])
826
+
827
+ # invert matrix A resulting from the spline setup linear equation system and apply extraction matrix
828
+ A_inv = np.linalg.inv(A)
829
+ T_c = np.matmul(A_ex_c, A_inv)
830
+
831
+ # set up M_x and M_y matrices including the gradient information, i.e. bring normal vectors into matrix form
832
+ M_x = np.zeros((no_splines * 4, no_points))
833
+ M_y = np.zeros((no_splines * 4, no_points))
834
+
835
+ for i in range(no_splines):
836
+ j = i * 4
837
+
838
+ if i < no_points - 1:
839
+ M_x[j, i] = normvectors[i, 0]
840
+ M_x[j + 1, i + 1] = normvectors[i + 1, 0]
841
+
842
+ M_y[j, i] = normvectors[i, 1]
843
+ M_y[j + 1, i + 1] = normvectors[i + 1, 1]
844
+ else:
845
+ M_x[j, i] = normvectors[i, 0]
846
+ M_x[j + 1, 0] = normvectors[0, 0] # close spline
847
+
848
+ M_y[j, i] = normvectors[i, 1]
849
+ M_y[j + 1, 0] = normvectors[0, 1]
850
+
851
+ # set up q_x and q_y matrices including the point coordinate information
852
+ q_x = np.zeros((no_splines * 4, 1))
853
+ q_y = np.zeros((no_splines * 4, 1))
854
+
855
+ for i in range(no_splines):
856
+ j = i * 4
857
+
858
+ if i < no_points - 1:
859
+ q_x[j, 0] = reftrack[i, 0]
860
+ q_x[j + 1, 0] = reftrack[i + 1, 0]
861
+
862
+ q_y[j, 0] = reftrack[i, 1]
863
+ q_y[j + 1, 0] = reftrack[i + 1, 1]
864
+ else:
865
+ q_x[j, 0] = reftrack[i, 0]
866
+ q_x[j + 1, 0] = reftrack[0, 0]
867
+
868
+ q_y[j, 0] = reftrack[i, 1]
869
+ q_y[j + 1, 0] = reftrack[0, 1]
870
+
871
+ # for unclosed tracks, specify start- and end-heading constraints
872
+ if not closed:
873
+ q_x[-2, 0] = math.cos(psi_s + math.pi / 2)
874
+ q_y[-2, 0] = math.sin(psi_s + math.pi / 2)
875
+
876
+ q_x[-1, 0] = math.cos(psi_e + math.pi / 2)
877
+ q_y[-1, 0] = math.sin(psi_e + math.pi / 2)
878
+
879
+ # set up P_xx, P_xy, P_yy matrices
880
+ x_prime = np.eye(no_points, no_points) * np.matmul(np.matmul(A_ex_b, A_inv), q_x)
881
+ y_prime = np.eye(no_points, no_points) * np.matmul(np.matmul(A_ex_b, A_inv), q_y)
882
+
883
+ x_prime_sq = np.power(x_prime, 2)
884
+ y_prime_sq = np.power(y_prime, 2)
885
+ x_prime_y_prime = -2 * np.matmul(x_prime, y_prime)
886
+
887
+ curv_den = np.power(x_prime_sq + y_prime_sq, 1.5) # calculate curvature denominator
888
+ curv_part = np.divide(1, curv_den, out=np.zeros_like(curv_den),
889
+ where=curv_den != 0) # divide where not zero (diag elements)
890
+ curv_part_sq = np.power(curv_part, 2)
891
+
892
+ P_xx = np.matmul(curv_part_sq, y_prime_sq)
893
+ P_yy = np.matmul(curv_part_sq, x_prime_sq)
894
+ P_xy = np.matmul(curv_part_sq, x_prime_y_prime)
895
+
896
+ # ------------------------------------------------------------------------------------------------------------------
897
+ # SET UP FINAL MATRICES FOR SOLVER ---------------------------------------------------------------------------------
898
+ # ------------------------------------------------------------------------------------------------------------------
899
+
900
+ T_nx = np.matmul(T_c, M_x)
901
+ T_ny = np.matmul(T_c, M_y)
902
+
903
+ H_x = np.matmul(T_nx.T, np.matmul(P_xx, T_nx))
904
+ H_xy = np.matmul(T_ny.T, np.matmul(P_xy, T_nx))
905
+ H_y = np.matmul(T_ny.T, np.matmul(P_yy, T_ny))
906
+ H = H_x + H_xy + H_y
907
+ H = (H + H.T) / 2 # make H symmetric
908
+
909
+ f_x = 2 * np.matmul(np.matmul(q_x.T, T_c.T), np.matmul(P_xx, T_nx))
910
+ f_xy = np.matmul(np.matmul(q_x.T, T_c.T), np.matmul(P_xy, T_ny)) \
911
+ + np.matmul(np.matmul(q_y.T, T_c.T), np.matmul(P_xy, T_nx))
912
+ f_y = 2 * np.matmul(np.matmul(q_y.T, T_c.T), np.matmul(P_yy, T_ny))
913
+ f = f_x + f_xy + f_y
914
+ f = np.squeeze(f) # remove non-singleton dimensions
915
+
916
+ Q_x = np.matmul(curv_part, y_prime)
917
+ Q_y = np.matmul(curv_part, x_prime)
918
+
919
+ # this part is multiplied by alpha within the optimization (variable part)
920
+ E_kappa = np.matmul(Q_y, T_ny) - np.matmul(Q_x, T_nx)
921
+
922
+ # original curvature part (static part)
923
+ k_kappa_ref = np.matmul(Q_y, np.matmul(T_c, q_y)) - np.matmul(Q_x, np.matmul(T_c, q_x))
924
+
925
+ con_ge = np.ones((no_points, 1)) * kappa_bound - k_kappa_ref
926
+ con_le = -(np.ones((no_points, 1)) * -kappa_bound - k_kappa_ref) # multiplied by -1 as only LE conditions are poss.
927
+ con_stack = np.append(con_ge, con_le)
928
+
929
+ # calculate allowed deviation from refline
930
+ dev_max_right = reftrack[:, 2] - w_veh / 2
931
+ dev_max_left = reftrack[:, 3] - w_veh / 2
932
+
933
+ # check that there is space remaining between left and right maximum deviation (both can be negative as well!)
934
+ if np.any(-dev_max_right > dev_max_left) or np.any(-dev_max_left > dev_max_right):
935
+ raise RuntimeError("Problem not solvable, track might be too small to run with current safety distance!")
936
+
937
+ # consider value boundaries (dev_max_left <= alpha <= dev_max_right)
938
+ G = np.vstack((np.eye(no_points), -np.eye(no_points), E_kappa, -E_kappa))
939
+ h = np.append(dev_max_right, dev_max_left)
940
+ h = np.append(h, con_stack)
941
+
942
+ # G = np.vstack((np.eye(no_points), -np.eye(no_points)))
943
+ # h = np.append(dev_max_right, dev_max_left)
944
+
945
+ return H , f, G ,h
946
+
947
+ def interp_track_widths(w_track: np.ndarray,
948
+ spline_inds: np.ndarray,
949
+ t_values: np.ndarray,
950
+ incl_last_point: bool = False) -> np.ndarray:
951
+ """
952
+ .. description::
953
+ The function (linearly) interpolates the track widths in the same steps as the splines were interpolated before.
954
+
955
+ Keep attention that the (multiple) interpolation of track widths can lead to unwanted effects, e.g. that peaks
956
+ in the track widths can disappear if the stepsize is too large (kind of an aliasing effect).
957
+
958
+ .. inputs::
959
+ :param w_track: array containing the track widths in meters [w_track_right, w_track_left] to interpolate,
960
+ optionally with banking angle in rad: [w_track_right, w_track_left, banking]
961
+ :type w_track: np.ndarray
962
+ :param spline_inds: indices that show which spline (and here w_track element) shall be interpolated.
963
+ :type spline_inds: np.ndarray
964
+ :param t_values: relative spline coordinate values (t) of every point on the splines specified by spline_inds
965
+ :type t_values: np.ndarray
966
+ :param incl_last_point: bool flag to show if last point should be included or not.
967
+ :type incl_last_point: bool
968
+
969
+ .. outputs::
970
+ :return w_track_interp: array with interpolated track widths (and optionally banking angle).
971
+ :rtype w_track_interp: np.ndarray
972
+
973
+ .. notes::
974
+ All inputs are unclosed.
975
+ """
976
+
977
+ # ------------------------------------------------------------------------------------------------------------------
978
+ # CALCULATE INTERMEDIATE STEPS -------------------------------------------------------------------------------------
979
+ # ------------------------------------------------------------------------------------------------------------------
980
+
981
+ w_track_cl = np.vstack((w_track, w_track[0]))
982
+ no_interp_points = t_values.size # unclosed
983
+
984
+ if incl_last_point:
985
+ w_track_interp = np.zeros((no_interp_points + 1, w_track.shape[1]))
986
+ w_track_interp[-1] = w_track_cl[-1]
987
+ else:
988
+ w_track_interp = np.zeros((no_interp_points, w_track.shape[1]))
989
+
990
+ # vectorized linear interpolation: w0 + t*(w1-w0)
991
+ w0 = w_track_cl[spline_inds]
992
+ w1 = w_track_cl[spline_inds + 1]
993
+ w_track_interp[:no_interp_points] = w0 + t_values[:, np.newaxis] * (w1 - w0)
994
+
995
+ return w_track_interp
996
+
997
+ import quadprog
998
+ def New_reftrack(reftrack: np.ndarray,
999
+ ds: np.ndarray,
1000
+ interp_step: float,
1001
+ kappa_bound:float,
1002
+ wveh: float) -> np.ndarray:
1003
+ """
1004
+
1005
+ .. description::
1006
+ Modify the reftrack for reoptimisation
1007
+
1008
+ .. inputs::
1009
+ :param signal: temporal signal that should be filtered (always unclosed).
1010
+ :type signal: np.ndarray
1011
+
1012
+
1013
+ .. outputs::
1014
+ :return signal_filt: filtered input signal (always unclosed).
1015
+ :rtype signal_filt: np.ndarray
1016
+
1017
+ """
1018
+ kapb = kappa_bound
1019
+ sfty = wveh
1020
+ si = interp_step
1021
+ reftrack_tmp = reftrack
1022
+ coeffs_x, coeffs_y, M, normvec_norm = calc_splines(path=np.vstack((reftrack[:, 0:2],reftrack[0, 0:2])),el_lengths=ds)
1023
+ H, f, G , h = H_f(reftrack=reftrack,
1024
+ normvectors=normvec_norm,
1025
+ A=M,
1026
+ kappa_bound=kapb,
1027
+ w_veh=sfty,
1028
+ closed=True)
1029
+
1030
+
1031
+ alpha = quadprog.solve_qp(H, -f, -G.T,-h,0)[0]
1032
+ raceline_interp, a_opt, coeffs_x_opt, coeffs_y_opt, spline_inds_opt_interp, t_vals_opt_interp, s_points_opt_interp,\
1033
+ spline_lengths_opt, el_lengths_opt_interp = create_raceline(refline=reftrack[:, :2],
1034
+ normvectors=normvec_norm,
1035
+ alpha=alpha,
1036
+ stepsize_interp=si)
1037
+
1038
+ reftrack_tmp[:, 2] -= alpha
1039
+ reftrack_tmp[:, 3] += alpha
1040
+
1041
+ ws_track_tmp = interp_track_widths(w_track=reftrack_tmp[:, 2:],
1042
+ spline_inds=spline_inds_opt_interp,
1043
+ t_values=t_vals_opt_interp,
1044
+ incl_last_point=False)
1045
+
1046
+ # create new reftrack
1047
+ reftrack_tmp = np.column_stack((raceline_interp, ws_track_tmp))
1048
+
1049
+
1050
+ return reftrack_tmp
1051
+
1052
+ def nonreg_sampling(track: np.ndarray,
1053
+ eps_kappa: float = 1e-3,
1054
+ step_non_reg: int = 0) -> tuple:
1055
+ """
1056
+ .. description::
1057
+ The non-regular sampling function runs through the curvature profile and determines straight and corner sections.
1058
+ During straight sections it reduces the amount of points by skipping them depending on the step_non_reg parameter.
1059
+
1060
+ .. inputs::
1061
+ :param track: [x, y, w_tr_right, w_tr_left] (always unclosed).
1062
+ :type track: np.ndarray
1063
+ :param eps_kappa: identify straights using this threshold in curvature in rad/m, i.e. straight if
1064
+ kappa < eps_kappa
1065
+ :type eps_kappa: float
1066
+ :param step_non_reg: determines how many points are skipped in straight sections, e.g. step_non_reg = 3 means
1067
+ every fourth point is used while three points are skipped
1068
+ :type step_non_reg: int
1069
+
1070
+ .. outputs::
1071
+ :return track_sampled: [x, y, w_tr_right, w_tr_left] sampled track (always unclosed).
1072
+ :rtype track_sampled: np.ndarray
1073
+ :return sample_idxs: indices of points that are kept
1074
+ :rtype sample_idxs: np.ndarray
1075
+ """
1076
+
1077
+ # if stepsize is equal to zero simply return the input
1078
+ if step_non_reg == 0:
1079
+ return track, np.arange(0, track.shape[0])
1080
+
1081
+ # calculate curvature (required to be able to differentiate straight and corner sections)
1082
+ path_cl = np.vstack((track[:, :2], track[0, :2]))
1083
+ coeffs_x, coeffs_y = calc_splines(path=path_cl)[:2]
1084
+ kappa_path = calc_head_curv_an(coeffs_x=coeffs_x,
1085
+ coeffs_y=coeffs_y,
1086
+ ind_spls=np.arange(0, coeffs_x.shape[0]),
1087
+ t_spls=np.zeros(coeffs_x.shape[0]))[1]
1088
+
1089
+ # run through the profile to determine the indices of the points that are kept
1090
+ idx_latest = step_non_reg + 1
1091
+ sample_idxs = [0]
1092
+
1093
+ for idx in range(1, len(kappa_path)):
1094
+ if np.abs(kappa_path[idx]) >= eps_kappa or idx >= idx_latest:
1095
+ # keep this point
1096
+ sample_idxs.append(idx)
1097
+ idx_latest = idx + step_non_reg + 1
1098
+
1099
+ return track[sample_idxs], np.array(sample_idxs)
1100
+
1101
+
1102
+ def calc_head_curv_num(path: np.ndarray,
1103
+ el_lengths: np.ndarray,
1104
+ is_closed: bool,
1105
+ stepsize_psi_preview: float = 1.0,
1106
+ stepsize_psi_review: float = 1.0,
1107
+ stepsize_curv_preview: float = 2.0,
1108
+ stepsize_curv_review: float = 2.0,
1109
+ calc_curv: bool = True) -> tuple:
1110
+ """
1111
+ .. description::
1112
+ Numerical calculation of heading psi and curvature kappa on the basis of a given path.
1113
+
1114
+ .. inputs::
1115
+ :param path: array of points [x, y] (always unclosed).
1116
+ :type path: np.ndarray
1117
+ :param el_lengths: array containing the element lengths.
1118
+ :type el_lengths: np.ndarray
1119
+ :param is_closed: close path for heading and curvature calculation.
1120
+ :type is_closed: bool
1121
+ :param stepsize_psi_preview: preview/review distances used for numerical heading/curvature calculation.
1122
+ :type stepsize_psi_preview: float
1123
+ :param stepsize_psi_review: preview/review distances used for numerical heading/curvature calculation.
1124
+ :type stepsize_psi_review: float
1125
+ :param stepsize_curv_preview: preview/review distances used for numerical heading/curvature calculation.
1126
+ :type stepsize_curv_preview: float
1127
+ :param stepsize_curv_review: preview/review distances used for numerical heading/curvature calculation.
1128
+ :type stepsize_curv_review: float
1129
+ :param calc_curv: bool flag to show if curvature should be calculated (kappa is set 0.0 otherwise).
1130
+ :type calc_curv: bool
1131
+
1132
+ .. outputs::
1133
+ :return psi: heading at every point (always unclosed).
1134
+ :rtype psi: float
1135
+ :return kappa: curvature at every point (always unclosed).
1136
+ :rtype kappa: float
1137
+
1138
+ .. notes::
1139
+ path must be inserted unclosed, i.e. path[-1] != path[0], even if is_closed is set True! (el_lengths is kind
1140
+ of closed if is_closed is True of course!)
1141
+
1142
+ case is_closed is True:
1143
+ len(path) = len(el_lengths) = len(psi) = len(kappa)
1144
+
1145
+ case is_closed is False:
1146
+ len(path) = len(el_lengths) + 1 = len(psi) = len(kappa)
1147
+ """
1148
+
1149
+ # check inputs
1150
+ if is_closed and path.shape[0] != el_lengths.size:
1151
+ raise RuntimeError("path and el_lenghts must have the same length!")
1152
+
1153
+ elif not is_closed and path.shape[0] != el_lengths.size + 1:
1154
+ raise RuntimeError("path must have the length of el_lengths + 1!")
1155
+
1156
+ # get number if points
1157
+ no_points = path.shape[0]
1158
+
1159
+ # ------------------------------------------------------------------------------------------------------------------
1160
+ # CASE: CLOSED PATH ------------------------------------------------------------------------------------------------
1161
+ # ------------------------------------------------------------------------------------------------------------------
1162
+
1163
+ if is_closed:
1164
+
1165
+ # --------------------------------------------------------------------------------------------------------------
1166
+ # PREVIEW/REVIEW DISTANCES -------------------------------------------------------------------------------------
1167
+ # --------------------------------------------------------------------------------------------------------------
1168
+
1169
+ # calculate how many points we look to the front and rear of the current position for the head/curv calculations
1170
+ ind_step_preview_psi = round(stepsize_psi_preview / float(np.average(el_lengths)))
1171
+ ind_step_review_psi = round(stepsize_psi_review / float(np.average(el_lengths)))
1172
+ ind_step_preview_curv = round(stepsize_curv_preview / float(np.average(el_lengths)))
1173
+ ind_step_review_curv = round(stepsize_curv_review / float(np.average(el_lengths)))
1174
+
1175
+ ind_step_preview_psi = max(ind_step_preview_psi, 1)
1176
+ ind_step_review_psi = max(ind_step_review_psi, 1)
1177
+ ind_step_preview_curv = max(ind_step_preview_curv, 1)
1178
+ ind_step_review_curv = max(ind_step_review_curv, 1)
1179
+
1180
+ steps_tot_psi = ind_step_preview_psi + ind_step_review_psi
1181
+ steps_tot_curv = ind_step_preview_curv + ind_step_review_curv
1182
+
1183
+ # --------------------------------------------------------------------------------------------------------------
1184
+ # HEADING ------------------------------------------------------------------------------------------------------
1185
+ # --------------------------------------------------------------------------------------------------------------
1186
+
1187
+ # calculate tangent vectors for every point
1188
+ path_temp = np.vstack((path[-ind_step_review_psi:], path, path[:ind_step_preview_psi]))
1189
+ tangvecs = np.stack((path_temp[steps_tot_psi:, 0] - path_temp[:-steps_tot_psi, 0],
1190
+ path_temp[steps_tot_psi:, 1] - path_temp[:-steps_tot_psi, 1]), axis=1)
1191
+
1192
+ # calculate psi of tangent vectors (pi/2 must be substracted due to our convention that psi = 0 is north)
1193
+ psi = np.arctan2(tangvecs[:, 1], tangvecs[:, 0]) - math.pi / 2
1194
+ psi = normalize_psi(psi)
1195
+
1196
+ # --------------------------------------------------------------------------------------------------------------
1197
+ # CURVATURE ----------------------------------------------------------------------------------------------------
1198
+ # --------------------------------------------------------------------------------------------------------------
1199
+
1200
+ if calc_curv:
1201
+ psi_temp = np.insert(psi, 0, psi[-ind_step_review_curv:])
1202
+ psi_temp = np.append(psi_temp, psi[:ind_step_preview_curv])
1203
+
1204
+ # calculate delta psi
1205
+ delta_psi = normalize_psi(psi_temp[steps_tot_curv:steps_tot_curv + no_points] - psi_temp[:no_points])
1206
+
1207
+ # calculate kappa
1208
+ s_points_cl = np.cumsum(el_lengths)
1209
+ s_points_cl = np.insert(s_points_cl, 0, 0.0)
1210
+ s_points = s_points_cl[:-1]
1211
+ s_points_cl_reverse = np.flipud(-np.cumsum(np.flipud(el_lengths))) # should not include 0.0 as last value
1212
+
1213
+ s_points_temp = np.insert(s_points, 0, s_points_cl_reverse[-ind_step_review_curv:])
1214
+ s_points_temp = np.append(s_points_temp, s_points_cl[-1] + s_points[:ind_step_preview_curv])
1215
+
1216
+ kappa = delta_psi / (s_points_temp[steps_tot_curv:] - s_points_temp[:-steps_tot_curv])
1217
+
1218
+ else:
1219
+ kappa = 0.0
1220
+
1221
+ # ------------------------------------------------------------------------------------------------------------------
1222
+ # CASE: UNCLOSED PATH ----------------------------------------------------------------------------------------------
1223
+ # ------------------------------------------------------------------------------------------------------------------
1224
+
1225
+ else:
1226
+
1227
+ # --------------------------------------------------------------------------------------------------------------
1228
+ # HEADING ------------------------------------------------------------------------------------------------------
1229
+ # --------------------------------------------------------------------------------------------------------------
1230
+
1231
+ # calculate tangent vectors for every point
1232
+ tangvecs = np.zeros((no_points, 2))
1233
+
1234
+ tangvecs[0, 0] = path[1, 0] - path[0, 0] # i == 0
1235
+ tangvecs[0, 1] = path[1, 1] - path[0, 1]
1236
+
1237
+ tangvecs[1:-1, 0] = path[2:, 0] - path[:-2, 0] # 0 < i < no_points - 1
1238
+ tangvecs[1:-1, 1] = path[2:, 1] - path[:-2, 1]
1239
+
1240
+ tangvecs[-1, 0] = path[-1, 0] - path[-2, 0] # i == -1
1241
+ tangvecs[-1, 1] = path[-1, 1] - path[-2, 1]
1242
+
1243
+ # calculate psi of tangent vectors (pi/2 must be substracted due to our convention that psi = 0 is north)
1244
+ psi = np.arctan2(tangvecs[:, 1], tangvecs[:, 0]) - math.pi / 2
1245
+ psi = normalize_psi(psi)
1246
+
1247
+ # --------------------------------------------------------------------------------------------------------------
1248
+ # CURVATURE ----------------------------------------------------------------------------------------------------
1249
+ # --------------------------------------------------------------------------------------------------------------
1250
+
1251
+ if calc_curv:
1252
+ # calculate delta psi
1253
+ delta_psi = np.zeros(no_points)
1254
+
1255
+ delta_psi[0] = psi[1] - psi[0] # i == 0
1256
+ delta_psi[1:-1] = psi[2:] - psi[:-2] # 0 < i < no_points - 1
1257
+ delta_psi[-1] = psi[-1] - psi[-2] # i == -1
1258
+
1259
+ # normalize delta_psi
1260
+ delta_psi = normalize_psi(delta_psi)
1261
+
1262
+ # calculate kappa
1263
+ kappa = np.zeros(no_points)
1264
+
1265
+ kappa[0] = delta_psi[0] / el_lengths[0] # i == 0
1266
+ kappa[1:-1] = delta_psi[1:-1] / (el_lengths[1:] + el_lengths[:-1]) # 0 < i < no_points - 1
1267
+ kappa[-1] = delta_psi[-1] / el_lengths[-1] # i == -1
1268
+
1269
+ else:
1270
+ kappa = 0.0
1271
+
1272
+ return psi, kappa
1273
+
1274
+
1275
+ def check_normals_crossing(track: np.ndarray,
1276
+ normvec_normalized: np.ndarray,
1277
+ horizon: int = 3) -> bool:
1278
+ """
1279
+ .. description::
1280
+ This function checks spline normals for crossings. Returns True if a crossing was found, otherwise False.
1281
+
1282
+ .. inputs::
1283
+ :param track: array containing the track [x, y, w_tr_right, w_tr_left] to check
1284
+ :type track: np.ndarray
1285
+ :param normvec_normalized: array containing normalized normal vectors for every track point
1286
+ [x_component, y_component]
1287
+ :type normvec_normalized: np.ndarray
1288
+ :param horizon: determines the number of normals in forward and backward direction that are checked
1289
+ against each normal on the line
1290
+ :type horizon: int
1291
+
1292
+ .. outputs::
1293
+ :return found_crossing: bool value indicating if a crossing was found or not
1294
+ :rtype found_crossing: bool
1295
+
1296
+ .. notes::
1297
+ The checks can take a while if full check is performed. Inputs are unclosed.
1298
+ """
1299
+
1300
+ # check input
1301
+ no_points = track.shape[0]
1302
+
1303
+ if horizon >= no_points:
1304
+ raise RuntimeError("Horizon of %i points is too large for a track with %i points, reduce horizon!"
1305
+ % (horizon, no_points))
1306
+
1307
+ elif horizon >= no_points / 2:
1308
+ print("WARNING: Horizon of %i points makes no sense for a track with %i points, reduce horizon!"
1309
+ % (horizon, no_points))
1310
+
1311
+ # initialization
1312
+ les_mat = np.zeros((2, 2))
1313
+ idx_list = list(range(0, no_points))
1314
+ idx_list = idx_list[-horizon:] + idx_list + idx_list[:horizon]
1315
+
1316
+ # loop through all points of the track to check for crossings in their neighbourhoods
1317
+ for idx in range(no_points):
1318
+
1319
+ # determine indices of points in the neighbourhood of the current index
1320
+ idx_neighbours = idx_list[idx:idx + 2 * horizon + 1]
1321
+ del idx_neighbours[horizon]
1322
+ idx_neighbours = np.array(idx_neighbours)
1323
+
1324
+ # remove indices of normal vectors that are collinear to the current index
1325
+ is_collinear_b = np.isclose(np.cross(normvec_normalized[idx], normvec_normalized[idx_neighbours]), 0.0)
1326
+ idx_neighbours_rel = idx_neighbours[np.nonzero(np.invert(is_collinear_b))[0]]
1327
+
1328
+ # check crossings solving an LES
1329
+ for idx_comp in list(idx_neighbours_rel):
1330
+
1331
+ # LES: x_1 + lambda_1 * nx_1 = x_2 + lambda_2 * nx_2; y_1 + lambda_1 * ny_1 = y_2 + lambda_2 * ny_2;
1332
+ const = track[idx_comp, :2] - track[idx, :2]
1333
+ les_mat[:, 0] = normvec_normalized[idx]
1334
+ les_mat[:, 1] = -normvec_normalized[idx_comp]
1335
+
1336
+ # solve LES
1337
+ lambdas = np.linalg.solve(les_mat, const)
1338
+
1339
+ # we have a crossing within the relevant part if both lambdas lie between -w_tr_left and w_tr_right
1340
+ if -track[idx, 3] <= lambdas[0] <= track[idx, 2] \
1341
+ and -track[idx_comp, 3] <= lambdas[1] <= track[idx_comp, 2]:
1342
+ return True # found crossing
1343
+
1344
+ return False
1345
+
1346
+ def interp_track(track: np.ndarray,
1347
+ stepsize: float) -> np.ndarray:
1348
+ """
1349
+
1350
+ .. description::
1351
+ Interpolate track points linearly to a new stepsize.
1352
+
1353
+ .. inputs::
1354
+ :param track: track in the format [x, y, w_tr_right, w_tr_left, (banking)].
1355
+ :type track: np.ndarray
1356
+ :param stepsize: desired stepsize after interpolation in m.
1357
+ :type stepsize: float
1358
+
1359
+ .. outputs::
1360
+ :return track_interp: interpolated track [x, y, w_tr_right, w_tr_left, (banking)].
1361
+ :rtype track_interp: np.ndarray
1362
+
1363
+ .. notes::
1364
+ Track input and output are unclosed! track input must however be closable in the current form!
1365
+ The banking angle is optional and must not be provided!
1366
+ """
1367
+
1368
+ # ------------------------------------------------------------------------------------------------------------------
1369
+ # LINEAR INTERPOLATION OF TRACK ------------------------------------------------------------------------------------
1370
+ # ------------------------------------------------------------------------------------------------------------------
1371
+
1372
+ # create closed track
1373
+ track_cl = np.vstack((track, track[0]))
1374
+
1375
+ # calculate element lengths (euclidian distance)
1376
+ el_lengths_cl = np.sqrt(np.sum(np.power(np.diff(track_cl[:, :2], axis=0), 2), axis=1))
1377
+
1378
+ # sum up total distance (from start) to every element
1379
+ dists_cum_cl = np.cumsum(el_lengths_cl)
1380
+ dists_cum_cl = np.insert(dists_cum_cl, 0, 0.0)
1381
+
1382
+ # calculate desired lenghts depending on specified stepsize (+1 because last element is included)
1383
+ no_points_interp_cl = math.ceil(dists_cum_cl[-1] / stepsize) + 1
1384
+ dists_interp_cl = np.linspace(0.0, dists_cum_cl[-1], no_points_interp_cl)
1385
+
1386
+ # interpolate closed track points
1387
+ track_interp_cl = np.zeros((no_points_interp_cl, track_cl.shape[1]))
1388
+
1389
+ track_interp_cl[:, 0] = np.interp(dists_interp_cl, dists_cum_cl, track_cl[:, 0])
1390
+ track_interp_cl[:, 1] = np.interp(dists_interp_cl, dists_cum_cl, track_cl[:, 1])
1391
+ track_interp_cl[:, 2] = np.interp(dists_interp_cl, dists_cum_cl, track_cl[:, 2])
1392
+ track_interp_cl[:, 3] = np.interp(dists_interp_cl, dists_cum_cl, track_cl[:, 3])
1393
+
1394
+ if track_cl.shape[1] == 5:
1395
+ track_interp_cl[:, 4] = np.interp(dists_interp_cl, dists_cum_cl, track_cl[:, 4])
1396
+
1397
+ return track_interp_cl[:-1]
1398
+
1399
+
1400
+ def side_of_line(a: Union[tuple, np.ndarray],
1401
+ b: Union[tuple, np.ndarray],
1402
+ z: Union[tuple, np.ndarray]) -> float:
1403
+ """
1404
+ .. description::
1405
+ Function determines if a point z is on the left or right side of a line from a to b. It is based on the z component
1406
+ orientation of the cross product, see question on
1407
+ https://stackoverflow.com/questions/1560492/how-to-tell-whether-a-point-is-to-the-right-or-left-side-of-a-line
1408
+
1409
+ .. inputs::
1410
+ :param a: point coordinates [x, y]
1411
+ :type a: Union[tuple, np.ndarray]
1412
+ :param b: point coordinates [x, y]
1413
+ :type b: Union[tuple, np.ndarray]
1414
+ :param z: point coordinates [x, y]
1415
+ :type z: Union[tuple, np.ndarray]
1416
+
1417
+ .. outputs::
1418
+ :return side: 0.0 = on line, 1.0 = left side, -1.0 = right side.
1419
+ :rtype side: float
1420
+ """
1421
+
1422
+ # calculate side
1423
+ side = np.sign((b[0] - a[0]) * (z[1] - a[1]) - (b[1] - a[1]) * (z[0] - a[0]))
1424
+
1425
+ return side
1426
+
1427
+
1428
+
1429
+
1430
+ # ----------------------------------------------------------------------------------------------------------------------
1431
+ # DISTANCE CALCULATION FOR OPTIMIZATION --------------------------------------------------------------------------------
1432
+ # ----------------------------------------------------------------------------------------------------------------------
1433
+
1434
+ # return distance from point p to a point on the spline at spline parameter t_glob
1435
+ def dist_to_p(t_glob: np.ndarray, path: list, p: np.ndarray):
1436
+ s_vals = interpolate.splev(t_glob, path)
1437
+ s = np.array([s_vals[0].item(), s_vals[1].item()])
1438
+ assert p.ndim == 1, f"Expected 1D p, got shape {p.shape}"
1439
+ assert s.ndim == 1, f"Expected 1D s, got shape {s.shape}"
1440
+ return spatial.distance.euclidean(p, s)
1441
+
1442
+
1443
+ def spline_approximation(track: np.ndarray,
1444
+ k_reg: int = 3,
1445
+ s_reg: int = 10,
1446
+ stepsize_prep: float = 1.0,
1447
+ stepsize_reg: float = 3.0,
1448
+ debug: bool = False) -> np.ndarray:
1449
+ """
1450
+
1451
+ .. description::
1452
+ Smooth spline approximation for a track (e.g. centerline, reference line).
1453
+
1454
+ .. inputs::
1455
+ :param track: [x, y, w_tr_right, w_tr_left, (banking)] (always unclosed).
1456
+ :type track: np.ndarray
1457
+ :param k_reg: order of B splines.
1458
+ :type k_reg: int
1459
+ :param s_reg: smoothing factor (usually between 5 and 100).
1460
+ :type s_reg: int
1461
+ :param stepsize_prep: stepsize used for linear track interpolation before spline approximation.
1462
+ :type stepsize_prep: float
1463
+ :param stepsize_reg: stepsize after smoothing.
1464
+ :type stepsize_reg: float
1465
+ :param debug: flag for printing debug messages
1466
+ :type debug: bool
1467
+
1468
+ .. outputs::
1469
+ :return track_reg: [x, y, w_tr_right, w_tr_left, (banking)] (always unclosed).
1470
+ :rtype track_reg: np.ndarray
1471
+
1472
+ .. notes::
1473
+ The function can only be used for closable tracks, i.e. track is closed at the beginning!
1474
+ The banking angle is optional and must not be provided!
1475
+ """
1476
+
1477
+ # ------------------------------------------------------------------------------------------------------------------
1478
+ # LINEAR INTERPOLATION BEFORE SMOOTHING ----------------------------------------------------------------------------
1479
+ # ------------------------------------------------------------------------------------------------------------------
1480
+
1481
+ track_interp =interp_track(track=track,
1482
+ stepsize=stepsize_prep)
1483
+ track_interp_cl = np.vstack((track_interp, track_interp[0]))
1484
+
1485
+ # ------------------------------------------------------------------------------------------------------------------
1486
+ # SPLINE APPROXIMATION / PATH SMOOTHING ----------------------------------------------------------------------------
1487
+ # ------------------------------------------------------------------------------------------------------------------
1488
+
1489
+ # create closed track (original track)
1490
+ track_cl = np.vstack((track, track[0]))
1491
+ no_points_track_cl = track_cl.shape[0]
1492
+ el_lengths_cl = np.sqrt(np.sum(np.power(np.diff(track_cl[:, :2], axis=0), 2), axis=1))
1493
+ dists_cum_cl = np.cumsum(el_lengths_cl)
1494
+ dists_cum_cl = np.insert(dists_cum_cl, 0, 0.0)
1495
+
1496
+ # find B spline representation of the inserted path and smooth it in this process
1497
+ # (tck_cl: tuple (vector of knots, the B-spline coefficients, and the degree of the spline))
1498
+ tck_cl, t_glob_cl = interpolate.splprep([track_interp_cl[:, 0], track_interp_cl[:, 1]],
1499
+ k=k_reg,
1500
+ s=s_reg,
1501
+ per=1)[:2]
1502
+
1503
+ # calculate total length of smooth approximating spline based on euclidian distance with points at every 0.25m
1504
+ no_points_lencalc_cl = math.ceil(dists_cum_cl[-1]) * 4
1505
+ path_smoothed_tmp = np.array(interpolate.splev(np.linspace(0.0, 1.0, no_points_lencalc_cl), tck_cl)).T
1506
+ len_path_smoothed_tmp = np.sum(np.sqrt(np.sum(np.power(np.diff(path_smoothed_tmp, axis=0), 2), axis=1)))
1507
+
1508
+ # get smoothed path
1509
+ no_points_reg_cl = math.ceil(len_path_smoothed_tmp / stepsize_reg) + 1
1510
+ path_smoothed = np.array(interpolate.splev(np.linspace(0.0, 1.0, no_points_reg_cl), tck_cl)).T[:-1]
1511
+
1512
+ # ------------------------------------------------------------------------------------------------------------------
1513
+ # PROCESS TRACK WIDTHS (AND BANKING ANGLE IF GIVEN) ----------------------------------------------------------------
1514
+ # ------------------------------------------------------------------------------------------------------------------
1515
+
1516
+ # find the closest points on the B spline to input points
1517
+ dists_cl = np.zeros(no_points_track_cl) # contains (min) distances between input points and spline
1518
+ closest_point_cl = np.zeros((no_points_track_cl, 2)) # contains the closest points on the spline
1519
+ closest_t_glob_cl = np.zeros(no_points_track_cl) # containts the t_glob values for closest points
1520
+ t_glob_guess_cl = dists_cum_cl / dists_cum_cl[-1] # start guess for the minimization
1521
+
1522
+ for i in range(no_points_track_cl):
1523
+ # get t_glob value for the point on the B spline with a minimum distance to the input points
1524
+ closest_t_glob_cl[i] = optimize.fmin(dist_to_p,
1525
+ x0=t_glob_guess_cl[i],
1526
+ args=(tck_cl, track_cl[i, :2]),
1527
+ disp=False)
1528
+
1529
+ # evaluate B spline on the basis of t_glob to obtain the closest point
1530
+ closest_point_cl[i] = interpolate.splev(closest_t_glob_cl[i], tck_cl)
1531
+
1532
+ # save distance from closest point to input point
1533
+ dists_cl[i] = math.sqrt(math.pow(closest_point_cl[i, 0] - track_cl[i, 0], 2)
1534
+ + math.pow(closest_point_cl[i, 1] - track_cl[i, 1], 2))
1535
+
1536
+ if debug:
1537
+ print("Spline approximation: mean deviation %.2fm, maximum deviation %.2fm"
1538
+ % (float(np.mean(dists_cl)), float(np.amax(np.abs(dists_cl)))))
1539
+
1540
+ # get side of smoothed track compared to the inserted track
1541
+ sides = np.zeros(no_points_track_cl - 1)
1542
+
1543
+ for i in range(no_points_track_cl - 1):
1544
+ sides[i] = side_of_line(a=track_cl[i, :2], b=track_cl[i+1, :2],z=closest_point_cl[i])
1545
+
1546
+ sides_cl = np.hstack((sides, sides[0]))
1547
+
1548
+ # calculate new track widths on the basis of the new reference line, but not interpolated to new stepsize yet
1549
+ w_tr_right_new_cl = track_cl[:, 2] + sides_cl * dists_cl
1550
+ w_tr_left_new_cl = track_cl[:, 3] - sides_cl * dists_cl
1551
+
1552
+ # interpolate track widths after smoothing (linear)
1553
+ w_tr_right_smoothed_cl = np.interp(np.linspace(0.0, 1.0, no_points_reg_cl), closest_t_glob_cl, w_tr_right_new_cl)
1554
+ w_tr_left_smoothed_cl = np.interp(np.linspace(0.0, 1.0, no_points_reg_cl), closest_t_glob_cl, w_tr_left_new_cl)
1555
+
1556
+ track_reg = np.column_stack((path_smoothed, w_tr_right_smoothed_cl[:-1], w_tr_left_smoothed_cl[:-1]))
1557
+
1558
+ # interpolate banking if given (linear)
1559
+ if track_cl.shape[1] == 5:
1560
+ banking_smoothed_cl = np.interp(np.linspace(0.0, 1.0, no_points_reg_cl), closest_t_glob_cl, track_cl[:, 4])
1561
+ track_reg = np.column_stack((track_reg, banking_smoothed_cl[:-1]))
1562
+
1563
+ return track_reg
1564
+
1565
+ import sys
1566
+ def prep_track(reftrack_imp: np.ndarray,
1567
+ reg_smooth_opts: dict,
1568
+ stepsize_opts: dict,
1569
+ debug: bool = False,
1570
+ min_width: float = None) -> tuple:
1571
+ """
1572
+ Documentation:
1573
+ This function prepares the inserted reference track for optimization.
1574
+
1575
+ Inputs:
1576
+ reftrack_imp: imported track [x_m, y_m, w_tr_right_m, w_tr_left_m]
1577
+ reg_smooth_opts: parameters for the spline approximation
1578
+ stepsize_opts: dict containing the stepsizes before spline approximation and after spline interpolation
1579
+ debug: boolean showing if debug messages should be printed
1580
+ min_width: [m] minimum enforced track width (None to deactivate)
1581
+
1582
+ Outputs:
1583
+ reftrack_interp: track after smoothing and interpolation [x_m, y_m, w_tr_right_m, w_tr_left_m]
1584
+ normvec_normalized_interp: normalized normal vectors on the reference line [x_m, y_m]
1585
+ a_interp: LES coefficients when calculating the splines
1586
+ coeffs_x_interp: spline coefficients of the x-component
1587
+ coeffs_y_interp: spline coefficients of the y-component
1588
+ """
1589
+
1590
+ # ------------------------------------------------------------------------------------------------------------------
1591
+ # INTERPOLATE REFTRACK AND CALCULATE INITIAL SPLINES ---------------------------------------------------------------
1592
+ # ------------------------------------------------------------------------------------------------------------------
1593
+
1594
+ # smoothing and interpolating reference track
1595
+ reftrack_interp = spline_approximation(track=reftrack_imp,
1596
+ k_reg=reg_smooth_opts["k_reg"],
1597
+ s_reg=reg_smooth_opts["s_reg"],
1598
+ stepsize_prep=stepsize_opts["stepsize_prep"],
1599
+ stepsize_reg=stepsize_opts["stepsize_reg"],
1600
+ debug=debug)
1601
+
1602
+ # calculate splines
1603
+ refpath_interp_cl = np.vstack((reftrack_interp[:, :2], reftrack_interp[0, :2]))
1604
+
1605
+ coeffs_x_interp, coeffs_y_interp, a_interp, normvec_normalized_interp = calc_splines(path=refpath_interp_cl)
1606
+
1607
+ # ------------------------------------------------------------------------------------------------------------------
1608
+ # CHECK SPLINE NORMALS FOR CROSSING POINTS -------------------------------------------------------------------------
1609
+ # ------------------------------------------------------------------------------------------------------------------
1610
+
1611
+ normals_crossing = check_normals_crossing(track=reftrack_interp,normvec_normalized=normvec_normalized_interp,horizon=3)
1612
+
1613
+ if normals_crossing:
1614
+ bound_1_tmp = reftrack_interp[:, :2] + normvec_normalized_interp * np.expand_dims(reftrack_interp[:, 2], axis=1)
1615
+ bound_2_tmp = reftrack_interp[:, :2] - normvec_normalized_interp * np.expand_dims(reftrack_interp[:, 3], axis=1)
1616
+
1617
+ plt.figure()
1618
+
1619
+ plt.plot(reftrack_interp[:, 0], reftrack_interp[:, 1], 'k-')
1620
+ for i in range(bound_1_tmp.shape[0]):
1621
+ temp = np.vstack((bound_1_tmp[i], bound_2_tmp[i]))
1622
+ plt.plot(temp[:, 0], temp[:, 1], "r-", linewidth=0.7)
1623
+
1624
+ plt.grid()
1625
+ ax = plt.gca()
1626
+ ax.set_aspect("equal", "datalim")
1627
+ plt.xlabel("east in m")
1628
+ plt.ylabel("north in m")
1629
+ plt.title("Error: at least one pair of normals is crossed!")
1630
+
1631
+ plt.show()
1632
+
1633
+ raise IOError("At least two spline normals are crossed, check input or increase smoothing factor!")
1634
+
1635
+ # ------------------------------------------------------------------------------------------------------------------
1636
+ # ENFORCE MINIMUM TRACK WIDTH (INFLATE TIGHTER SECTIONS UNTIL REACHED) ---------------------------------------------
1637
+ # ------------------------------------------------------------------------------------------------------------------
1638
+
1639
+ manipulated_track_width = False
1640
+
1641
+ if min_width is not None:
1642
+ for i in range(reftrack_interp.shape[0]):
1643
+ cur_width = reftrack_interp[i, 2] + reftrack_interp[i, 3]
1644
+
1645
+ if cur_width < min_width:
1646
+ manipulated_track_width = True
1647
+
1648
+ # inflate to both sides equally
1649
+ reftrack_interp[i, 2] += (min_width - cur_width) / 2
1650
+ reftrack_interp[i, 3] += (min_width - cur_width) / 2
1651
+
1652
+ if manipulated_track_width:
1653
+ print("WARNING: Track region was smaller than requested minimum track width -> Applied artificial inflation in"
1654
+ " order to match the requirements!", file=sys.stderr)
1655
+
1656
+ return reftrack_interp, normvec_normalized_interp, a_interp, coeffs_x_interp, coeffs_y_interp