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/KinematicProfs.py +970 -0
- OptiLine/__init__.py +1 -0
- OptiLine/map_builder.py +790 -0
- OptiLine/opt_mintime.py +769 -0
- OptiLine/solvers.py +2342 -0
- OptiLine/utils.py +1656 -0
- optiline_py-0.1.7.dist-info/METADATA +35 -0
- optiline_py-0.1.7.dist-info/RECORD +9 -0
- optiline_py-0.1.7.dist-info/WHEEL +4 -0
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
|