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/solvers.py
ADDED
|
@@ -0,0 +1,2342 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import math
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import quadprog
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def opt_min_curv(reftrack: np.ndarray,
|
|
9
|
+
normvectors: np.ndarray,
|
|
10
|
+
A: np.ndarray,
|
|
11
|
+
kappa_bound: float,
|
|
12
|
+
w_veh: float,
|
|
13
|
+
print_debug: bool = False,
|
|
14
|
+
plot_debug: bool = False,
|
|
15
|
+
closed: bool = True,
|
|
16
|
+
psi_s: float = None,
|
|
17
|
+
psi_e: float = None,
|
|
18
|
+
fix_s: bool = False,
|
|
19
|
+
fix_e: bool = False) -> tuple:
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
.. description::
|
|
23
|
+
This function uses a QP solver to minimize the summed curvature of a path by moving the path points along their
|
|
24
|
+
normal vectors within the track width. The function can be used for closed and unclosed tracks. For unclosed tracks
|
|
25
|
+
the heading psi_s and psi_e is enforced on the first and last point of the reftrack. Furthermore, in case of an
|
|
26
|
+
unclosed track, the first and last point of the reftrack are not subject to optimization and stay same.
|
|
27
|
+
|
|
28
|
+
Please refer to the paper for further information:
|
|
29
|
+
Heilmeier, Wischnewski, Hermansdorfer, Betz, Lienkamp, Lohmann
|
|
30
|
+
Minimum Curvature Trajectory Planning and Control for an Autonomous Racecar
|
|
31
|
+
DOI: 10.1080/00423114.2019.1631455
|
|
32
|
+
|
|
33
|
+
Hint: CVXOPT can be used as a solver instead of quadprog by uncommenting the import and corresponding code section.
|
|
34
|
+
|
|
35
|
+
.. inputs::
|
|
36
|
+
:param reftrack: array containing the reference track, i.e. a reference line and the according track widths to
|
|
37
|
+
the right and to the left [x, y, w_tr_right, w_tr_left] (unit is meter, must be unclosed!)
|
|
38
|
+
:type reftrack: np.ndarray
|
|
39
|
+
:param normvectors: normalized normal vectors for every point of the reference track [x_component, y_component]
|
|
40
|
+
(unit is meter, must be unclosed!)
|
|
41
|
+
:type normvectors: np.ndarray
|
|
42
|
+
:param A: linear equation system matrix for splines (applicable for both, x and y direction)
|
|
43
|
+
-> System matrices have the form a_i, b_i * t, c_i * t^2, d_i * t^3
|
|
44
|
+
-> see calc_splines.py for further information or to obtain this matrix
|
|
45
|
+
:type A: np.ndarray
|
|
46
|
+
:param kappa_bound: curvature boundary to consider during optimization.
|
|
47
|
+
:type kappa_bound: float
|
|
48
|
+
:param w_veh: vehicle width in m. It is considered during the calculation of the allowed deviations from the
|
|
49
|
+
reference line.
|
|
50
|
+
:type w_veh: float
|
|
51
|
+
:param print_debug: bool flag to print debug messages.
|
|
52
|
+
:type print_debug: bool
|
|
53
|
+
:param plot_debug: bool flag to plot the curvatures that are calculated based on the original linearization and on
|
|
54
|
+
a linearization around the solution.
|
|
55
|
+
:type plot_debug: bool
|
|
56
|
+
:param closed: bool flag specifying whether a closed or unclosed track should be assumed
|
|
57
|
+
:type closed: bool
|
|
58
|
+
:param psi_s: heading to be enforced at the first point for unclosed tracks
|
|
59
|
+
:type psi_s: float
|
|
60
|
+
:param psi_e: heading to be enforced at the last point for unclosed tracks
|
|
61
|
+
:type psi_e: float
|
|
62
|
+
:param fix_s: determines if start point is fixed to reference line for unclosed tracks
|
|
63
|
+
:type fix_s: bool
|
|
64
|
+
:param fix_e: determines if last point is fixed to reference line for unclosed tracks
|
|
65
|
+
:type fix_e: bool
|
|
66
|
+
|
|
67
|
+
.. outputs::
|
|
68
|
+
:return alpha_mincurv: solution vector of the opt. problem containing the lateral shift in m for every point.
|
|
69
|
+
:rtype alpha_mincurv: np.ndarray
|
|
70
|
+
:return curv_error_max: maximum curvature error when comparing the curvature calculated on the basis of the
|
|
71
|
+
linearization around the original refererence track and around the solution.
|
|
72
|
+
:rtype curv_error_max: float
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
no_points = reftrack.shape[0]
|
|
77
|
+
|
|
78
|
+
no_splines = no_points
|
|
79
|
+
if not closed:
|
|
80
|
+
no_splines -= 1
|
|
81
|
+
|
|
82
|
+
# check inputs
|
|
83
|
+
if no_points != normvectors.shape[0]:
|
|
84
|
+
raise RuntimeError("Array size of reftrack should be the same as normvectors!")
|
|
85
|
+
|
|
86
|
+
if (no_points * 4 != A.shape[0] and closed) or (no_splines * 4 != A.shape[0] and not closed)\
|
|
87
|
+
or A.shape[0] != A.shape[1]:
|
|
88
|
+
raise RuntimeError("Spline equation system matrix A has wrong dimensions!")
|
|
89
|
+
|
|
90
|
+
# create extraction matrix -> only b_i coefficients of the solved linear equation system are needed for gradient
|
|
91
|
+
# information
|
|
92
|
+
A_ex_b = np.zeros((no_points, no_splines * 4), dtype=int)
|
|
93
|
+
|
|
94
|
+
A_ex_b[np.arange(no_splines), np.arange(no_splines) * 4 + 1] = 1 # 1 * b_ix = E_x * x
|
|
95
|
+
|
|
96
|
+
# coefficients for end of spline (t = 1)
|
|
97
|
+
if not closed:
|
|
98
|
+
A_ex_b[-1, -4:] = np.array([0, 1, 2, 3])
|
|
99
|
+
|
|
100
|
+
# create extraction matrix -> only c_i coefficients of the solved linear equation system are needed for curvature
|
|
101
|
+
# information
|
|
102
|
+
A_ex_c = np.zeros((no_points, no_splines * 4), dtype=int)
|
|
103
|
+
|
|
104
|
+
A_ex_c[np.arange(no_splines), np.arange(no_splines) * 4 + 2] = 2 # 2 * c_ix = D_x * x
|
|
105
|
+
|
|
106
|
+
# coefficients for end of spline (t = 1)
|
|
107
|
+
if not closed:
|
|
108
|
+
A_ex_c[-1, -4:] = np.array([0, 0, 2, 6])
|
|
109
|
+
|
|
110
|
+
# invert matrix A resulting from the spline setup linear equation system and apply extraction matrix
|
|
111
|
+
A_inv = np.linalg.inv(A)
|
|
112
|
+
T_c = np.matmul(A_ex_c, A_inv)
|
|
113
|
+
|
|
114
|
+
# set up M_x and M_y matrices including the gradient information, i.e. bring normal vectors into matrix form
|
|
115
|
+
M_x = np.zeros((no_splines * 4, no_points))
|
|
116
|
+
M_y = np.zeros((no_splines * 4, no_points))
|
|
117
|
+
|
|
118
|
+
rows_0 = np.arange(no_splines) * 4
|
|
119
|
+
rows_1 = rows_0 + 1
|
|
120
|
+
cols_0 = np.arange(no_splines)
|
|
121
|
+
cols_1 = np.arange(1, no_splines + 1) % no_points # wraps last index to 0 for closed track
|
|
122
|
+
|
|
123
|
+
M_x[rows_0, cols_0] = normvectors[cols_0, 0]
|
|
124
|
+
M_x[rows_1, cols_1] = normvectors[cols_1, 0]
|
|
125
|
+
M_y[rows_0, cols_0] = normvectors[cols_0, 1]
|
|
126
|
+
M_y[rows_1, cols_1] = normvectors[cols_1, 1]
|
|
127
|
+
|
|
128
|
+
# set up q_x and q_y matrices including the point coordinate information
|
|
129
|
+
q_x = np.zeros((no_splines * 4, 1))
|
|
130
|
+
q_y = np.zeros((no_splines * 4, 1))
|
|
131
|
+
|
|
132
|
+
q_x[rows_0, 0] = reftrack[cols_0, 0]
|
|
133
|
+
q_x[rows_1, 0] = reftrack[cols_1, 0]
|
|
134
|
+
q_y[rows_0, 0] = reftrack[cols_0, 1]
|
|
135
|
+
q_y[rows_1, 0] = reftrack[cols_1, 1]
|
|
136
|
+
|
|
137
|
+
# for unclosed tracks, specify start- and end-heading constraints
|
|
138
|
+
if not closed:
|
|
139
|
+
q_x[-2, 0] = math.cos(psi_s + math.pi / 2)
|
|
140
|
+
q_y[-2, 0] = math.sin(psi_s + math.pi / 2)
|
|
141
|
+
|
|
142
|
+
q_x[-1, 0] = math.cos(psi_e + math.pi / 2)
|
|
143
|
+
q_y[-1, 0] = math.sin(psi_e + math.pi / 2)
|
|
144
|
+
|
|
145
|
+
# set up P_xx, P_xy, P_yy matrices
|
|
146
|
+
x_prime = np.eye(no_points, no_points) * np.matmul(np.matmul(A_ex_b, A_inv), q_x)
|
|
147
|
+
y_prime = np.eye(no_points, no_points) * np.matmul(np.matmul(A_ex_b, A_inv), q_y)
|
|
148
|
+
|
|
149
|
+
x_prime_sq = np.power(x_prime, 2)
|
|
150
|
+
y_prime_sq = np.power(y_prime, 2)
|
|
151
|
+
x_prime_y_prime = -2 * np.matmul(x_prime, y_prime)
|
|
152
|
+
|
|
153
|
+
curv_den = np.power(x_prime_sq + y_prime_sq, 1.5) # calculate curvature denominator
|
|
154
|
+
curv_part = np.divide(1, curv_den, out=np.zeros_like(curv_den),
|
|
155
|
+
where=curv_den != 0) # divide where not zero (diag elements)
|
|
156
|
+
curv_part_sq = np.power(curv_part, 2)
|
|
157
|
+
|
|
158
|
+
P_xx = np.matmul(curv_part_sq, y_prime_sq)
|
|
159
|
+
P_yy = np.matmul(curv_part_sq, x_prime_sq)
|
|
160
|
+
P_xy = np.matmul(curv_part_sq, x_prime_y_prime)
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
163
|
+
# SET UP FINAL MATRICES FOR SOLVER ---------------------------------------------------------------------------------
|
|
164
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
T_nx = np.matmul(T_c, M_x)
|
|
167
|
+
T_ny = np.matmul(T_c, M_y)
|
|
168
|
+
|
|
169
|
+
H_x = np.matmul(T_nx.T, np.matmul(P_xx, T_nx))
|
|
170
|
+
H_xy = np.matmul(T_ny.T, np.matmul(P_xy, T_nx))
|
|
171
|
+
H_y = np.matmul(T_ny.T, np.matmul(P_yy, T_ny))
|
|
172
|
+
H = H_x + H_xy + H_y
|
|
173
|
+
H = (H + H.T) / 2 # make H symmetric
|
|
174
|
+
|
|
175
|
+
f_x = 2 * np.matmul(np.matmul(q_x.T, T_c.T), np.matmul(P_xx, T_nx))
|
|
176
|
+
f_xy = np.matmul(np.matmul(q_x.T, T_c.T), np.matmul(P_xy, T_ny)) \
|
|
177
|
+
+ np.matmul(np.matmul(q_y.T, T_c.T), np.matmul(P_xy, T_nx))
|
|
178
|
+
f_y = 2 * np.matmul(np.matmul(q_y.T, T_c.T), np.matmul(P_yy, T_ny))
|
|
179
|
+
f = f_x + f_xy + f_y
|
|
180
|
+
f = np.squeeze(f) # remove non-singleton dimensions
|
|
181
|
+
|
|
182
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
183
|
+
# KAPPA CONSTRAINTS ------------------------------------------------------------------------------------------------
|
|
184
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
Q_x = np.matmul(curv_part, y_prime)
|
|
187
|
+
Q_y = np.matmul(curv_part, x_prime)
|
|
188
|
+
|
|
189
|
+
# this part is multiplied by alpha within the optimization (variable part)
|
|
190
|
+
E_kappa = np.matmul(Q_y, T_ny) - np.matmul(Q_x, T_nx)
|
|
191
|
+
|
|
192
|
+
# original curvature part (static part)
|
|
193
|
+
k_kappa_ref = np.matmul(Q_y, np.matmul(T_c, q_y)) - np.matmul(Q_x, np.matmul(T_c, q_x))
|
|
194
|
+
|
|
195
|
+
con_ge = np.ones((no_points, 1)) * kappa_bound - k_kappa_ref
|
|
196
|
+
con_le = -(np.ones((no_points, 1)) * -kappa_bound - k_kappa_ref) # multiplied by -1 as only LE conditions are poss.
|
|
197
|
+
con_stack = np.append(con_ge, con_le)
|
|
198
|
+
|
|
199
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
200
|
+
# CALL QUADRATIC PROGRAMMING ALGORITHM -----------------------------------------------------------------------------
|
|
201
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
quadprog interface description taken from
|
|
205
|
+
https://github.com/stephane-caron/qpsolvers/blob/master/qpsolvers/quadprog_.py
|
|
206
|
+
|
|
207
|
+
Solve a Quadratic Program defined as:
|
|
208
|
+
|
|
209
|
+
minimize
|
|
210
|
+
(1/2) * alpha.T * H * alpha + f.T * alpha
|
|
211
|
+
|
|
212
|
+
subject to
|
|
213
|
+
G * alpha <= h
|
|
214
|
+
A * alpha == b
|
|
215
|
+
|
|
216
|
+
using quadprog <https://pypi.python.org/pypi/quadprog/>.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
H : numpy.array
|
|
221
|
+
Symmetric quadratic-cost matrix.
|
|
222
|
+
f : numpy.array
|
|
223
|
+
Quadratic-cost vector.
|
|
224
|
+
G : numpy.array
|
|
225
|
+
Linear inequality constraint matrix.
|
|
226
|
+
h : numpy.array
|
|
227
|
+
Linear inequality constraint vector.
|
|
228
|
+
A : numpy.array, optional
|
|
229
|
+
Linear equality constraint matrix.
|
|
230
|
+
b : numpy.array, optional
|
|
231
|
+
Linear equality constraint vector.
|
|
232
|
+
initvals : numpy.array, optional
|
|
233
|
+
Warm-start guess vector (not used).
|
|
234
|
+
|
|
235
|
+
Returns
|
|
236
|
+
-------
|
|
237
|
+
alpha : numpy.array
|
|
238
|
+
Solution to the QP, if found, otherwise ``None``.
|
|
239
|
+
|
|
240
|
+
Note
|
|
241
|
+
----
|
|
242
|
+
The quadprog solver only considers the lower entries of `H`, therefore it
|
|
243
|
+
will use a wrong cost function if a non-symmetric matrix is provided.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
# calculate allowed deviation from refline
|
|
247
|
+
dev_max_right = reftrack[:, 2] - w_veh / 2
|
|
248
|
+
dev_max_left = reftrack[:, 3] - w_veh / 2
|
|
249
|
+
|
|
250
|
+
# constrain resulting path to reference line at start- and end-point for open tracks
|
|
251
|
+
if not closed and fix_s:
|
|
252
|
+
dev_max_left[0] = 0.05
|
|
253
|
+
dev_max_right[0] = 0.05
|
|
254
|
+
|
|
255
|
+
if not closed and fix_e:
|
|
256
|
+
dev_max_left[-1] = 0.05
|
|
257
|
+
dev_max_right[-1] = 0.05
|
|
258
|
+
|
|
259
|
+
# check that there is space remaining between left and right maximum deviation (both can be negative as well!)
|
|
260
|
+
if np.any(-dev_max_right > dev_max_left) or np.any(-dev_max_left > dev_max_right):
|
|
261
|
+
raise RuntimeError("Problem not solvable, track might be too small to run with current safety distance!")
|
|
262
|
+
|
|
263
|
+
# consider value boundaries (-dev_max_left <= alpha <= dev_max_right)
|
|
264
|
+
G = np.vstack((np.eye(no_points), -np.eye(no_points), E_kappa, -E_kappa))
|
|
265
|
+
h = np.append(dev_max_right, dev_max_left)
|
|
266
|
+
h = np.append(h, con_stack)
|
|
267
|
+
|
|
268
|
+
# save start time
|
|
269
|
+
t_start = time.perf_counter()
|
|
270
|
+
|
|
271
|
+
# solve problem (CVXOPT) -------------------------------------------------------------------------------------------
|
|
272
|
+
# args = [cvxopt.matrix(H), cvxopt.matrix(f), cvxopt.matrix(G), cvxopt.matrix(h)]
|
|
273
|
+
# sol = cvxopt.solvers.qp(*args)
|
|
274
|
+
#
|
|
275
|
+
# if 'optimal' not in sol['status']:
|
|
276
|
+
# print("WARNING: Optimal solution not found!")
|
|
277
|
+
#
|
|
278
|
+
# alpha_mincurv = np.array(sol['x']).reshape((H.shape[1],))
|
|
279
|
+
|
|
280
|
+
# solve problem (quadprog) -----------------------------------------------------------------------------------------
|
|
281
|
+
alpha_mincurv = quadprog.solve_qp(H, -f, -G.T, -h, 0)[0]
|
|
282
|
+
|
|
283
|
+
# print runtime into console window
|
|
284
|
+
if print_debug:
|
|
285
|
+
print("Solver runtime opt_min_curv: " + "{:.3f}".format(time.perf_counter() - t_start) + "s")
|
|
286
|
+
|
|
287
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
288
|
+
# CALCULATE CURVATURE ERROR ----------------------------------------------------------------------------------------
|
|
289
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
# calculate curvature once based on original linearization and once based on a new linearization around the solution
|
|
292
|
+
q_x_tmp = q_x + np.matmul(M_x, np.expand_dims(alpha_mincurv, 1))
|
|
293
|
+
q_y_tmp = q_y + np.matmul(M_y, np.expand_dims(alpha_mincurv, 1))
|
|
294
|
+
|
|
295
|
+
x_prime_tmp = np.eye(no_points, no_points) * np.matmul(np.matmul(A_ex_b, A_inv), q_x_tmp)
|
|
296
|
+
y_prime_tmp = np.eye(no_points, no_points) * np.matmul(np.matmul(A_ex_b, A_inv), q_y_tmp)
|
|
297
|
+
|
|
298
|
+
x_prime_prime = np.squeeze(np.matmul(T_c, q_x) + np.matmul(T_nx, np.expand_dims(alpha_mincurv, 1)))
|
|
299
|
+
y_prime_prime = np.squeeze(np.matmul(T_c, q_y) + np.matmul(T_ny, np.expand_dims(alpha_mincurv, 1)))
|
|
300
|
+
|
|
301
|
+
xp_d = np.diag(x_prime)
|
|
302
|
+
yp_d = np.diag(y_prime)
|
|
303
|
+
xp_tmp_d = np.diag(x_prime_tmp)
|
|
304
|
+
yp_tmp_d = np.diag(y_prime_tmp)
|
|
305
|
+
|
|
306
|
+
curv_orig_lin = (xp_d * y_prime_prime - yp_d * x_prime_prime) / np.power(xp_d**2 + yp_d**2, 1.5)
|
|
307
|
+
curv_sol_lin = (xp_tmp_d * y_prime_prime - yp_tmp_d * x_prime_prime) / np.power(xp_tmp_d**2 + yp_tmp_d**2, 1.5)
|
|
308
|
+
|
|
309
|
+
if plot_debug:
|
|
310
|
+
plt.plot(curv_orig_lin)
|
|
311
|
+
plt.plot(curv_sol_lin)
|
|
312
|
+
plt.legend(("original linearization", "solution based linearization"))
|
|
313
|
+
plt.show()
|
|
314
|
+
|
|
315
|
+
# calculate maximum curvature error
|
|
316
|
+
curv_error_max = np.amax(np.abs(curv_sol_lin - curv_orig_lin))
|
|
317
|
+
|
|
318
|
+
return alpha_mincurv, curv_error_max
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class ConstrainedCMAES_t:
|
|
322
|
+
"""
|
|
323
|
+
CMAES optimizer.
|
|
324
|
+
|
|
325
|
+
Parameters:
|
|
326
|
+
- f_t: The objective function to minimize.
|
|
327
|
+
- mean: initial mean.
|
|
328
|
+
- sigma: Initial step size.
|
|
329
|
+
- popsize: Population size.
|
|
330
|
+
- bounds1: Upper bounds.
|
|
331
|
+
- bounds2: Lower bounds.
|
|
332
|
+
"""
|
|
333
|
+
def __init__(self, f_t,mean, sigma, popsize, bounds1=None, bounds2=None):
|
|
334
|
+
self.mean = np.array(mean)
|
|
335
|
+
self.sigma = sigma
|
|
336
|
+
self.popsize = popsize
|
|
337
|
+
self.dim = len(mean)
|
|
338
|
+
self.cov_matrix = np.eye(self.dim)
|
|
339
|
+
self.bounds1 = bounds1
|
|
340
|
+
self.bounds2 = bounds2
|
|
341
|
+
self.ft = f_t
|
|
342
|
+
# CMA-ES parameters
|
|
343
|
+
self.weights = np.log(self.popsize/2 + 1) - np.log(np.arange(1, self.popsize + 1))
|
|
344
|
+
self.weights[self.weights < 0] = 0
|
|
345
|
+
self.weights /= np.sum(self.weights)
|
|
346
|
+
self.mu = int(self.popsize / 2)
|
|
347
|
+
self.mu_w = 1 / np.sum(self.weights[:self.mu]**2)
|
|
348
|
+
|
|
349
|
+
# Learning rates and constants
|
|
350
|
+
self.c_sigma = (self.mu_w + 2) / (self.dim + self.mu_w + 5)
|
|
351
|
+
self.d_sigma = 1 + 2 * max(0, np.sqrt((self.mu_w - 1) / (self.dim + 1)) - 1) + self.c_sigma
|
|
352
|
+
self.c_c = (4 + self.mu_w/self.dim) / (self.dim + 4 + 2*self.mu_w/self.dim)
|
|
353
|
+
self.c1 = 2 / ((self.dim + 1.3)**2 + self.mu_w)
|
|
354
|
+
self.cmu = min(1 - self.c1, 2 * (self.mu_w - 2 + 1/self.mu_w) / ((self.dim + 2)**2 + self.mu_w))
|
|
355
|
+
|
|
356
|
+
# Evolution paths
|
|
357
|
+
self.p_c = np.zeros(self.dim)
|
|
358
|
+
self.p_sigma = np.zeros(self.dim)
|
|
359
|
+
|
|
360
|
+
# Expected length of N(0,I)
|
|
361
|
+
self.chi_n = np.sqrt(self.dim) * (1 - 1/(4*self.dim) + 1/(21*self.dim**2))
|
|
362
|
+
|
|
363
|
+
def sample_population(self):
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
.. description::
|
|
367
|
+
Draws a new population of candidate solutions from the current multivariate Gaussian distribution and
|
|
368
|
+
clips them to the specified bounds if provided.
|
|
369
|
+
|
|
370
|
+
.. outputs::
|
|
371
|
+
:return samples: array of candidate solutions with shape (popsize, dim), clipped to [bounds2, bounds1].
|
|
372
|
+
:rtype samples: np.ndarray
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
samples = np.random.multivariate_normal(self.mean, self.sigma**2 * self.cov_matrix, self.popsize)
|
|
376
|
+
if self.bounds1 is not None:
|
|
377
|
+
samples = np.clip(samples, self.bounds2, self.bounds1)
|
|
378
|
+
return samples
|
|
379
|
+
|
|
380
|
+
def objective_function(self, ds):
|
|
381
|
+
return self.ft(ds)
|
|
382
|
+
|
|
383
|
+
def update(self, solutions, fitness):
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
.. description::
|
|
387
|
+
Updates the CMA-ES internal state (mean, evolution paths, covariance matrix, step size) using the
|
|
388
|
+
current population and their fitness values. Enforces positive semi-definiteness of the covariance matrix.
|
|
389
|
+
|
|
390
|
+
.. inputs::
|
|
391
|
+
:param solutions: array of candidate solutions evaluated in the current generation, shape (popsize, dim).
|
|
392
|
+
:type solutions: np.ndarray
|
|
393
|
+
:param fitness: array of objective function values corresponding to each solution, shape (popsize,).
|
|
394
|
+
:type fitness: np.ndarray
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
# Sort solutions
|
|
398
|
+
sorted_indices = np.argsort(fitness)
|
|
399
|
+
sorted_solutions = solutions[sorted_indices]
|
|
400
|
+
|
|
401
|
+
# Calculate weighted mean of the new population
|
|
402
|
+
old_mean = self.mean.copy()
|
|
403
|
+
self.mean = np.sum((self.weights[:self.mu, np.newaxis] * sorted_solutions[:self.mu]), axis=0)
|
|
404
|
+
|
|
405
|
+
# Update evolution paths
|
|
406
|
+
y = self.mean - old_mean
|
|
407
|
+
try:
|
|
408
|
+
C_2 = np.linalg.cholesky(self.cov_matrix)
|
|
409
|
+
except np.linalg.LinAlgError:
|
|
410
|
+
print("Warning: Cholesky decomposition failed. Using diagonal matrix instead.")
|
|
411
|
+
C_2 = np.diag(np.sqrt(np.abs(np.diag(self.cov_matrix))))
|
|
412
|
+
|
|
413
|
+
z = np.linalg.solve(C_2, y) / self.sigma
|
|
414
|
+
|
|
415
|
+
h_sigma = (np.linalg.norm(self.p_sigma) /
|
|
416
|
+
np.sqrt(1 - (1-self.c_sigma)**(2*(self.iterations+1))) / self.chi_n
|
|
417
|
+
< (1.4 + 2/(self.dim+1)))
|
|
418
|
+
|
|
419
|
+
self.p_sigma = (1 - self.c_sigma) * self.p_sigma + np.sqrt(self.c_sigma * (2 - self.c_sigma) * self.mu_w) * z
|
|
420
|
+
self.p_c = (1 - self.c_c) * self.p_c + h_sigma * np.sqrt(self.c_c * (2 - self.c_c) * self.mu_w) * y / self.sigma
|
|
421
|
+
|
|
422
|
+
# Adapt covariance matrix
|
|
423
|
+
c1a = self.c1 * (1 - (1-h_sigma**2) * self.c_c * (2-self.c_c))
|
|
424
|
+
self.cov_matrix = ((1 - c1a - self.cmu) * self.cov_matrix +
|
|
425
|
+
c1a * np.outer(self.p_c, self.p_c) +
|
|
426
|
+
self.cmu * np.sum(self.weights[:self.mu, np.newaxis, np.newaxis] *
|
|
427
|
+
(sorted_solutions[:self.mu] - old_mean)[:, :, np.newaxis] *
|
|
428
|
+
(sorted_solutions[:self.mu] - old_mean)[:, np.newaxis, :], axis=0) /
|
|
429
|
+
self.sigma**2)
|
|
430
|
+
|
|
431
|
+
# Adapt step size
|
|
432
|
+
self.sigma *= np.exp((self.c_sigma / self.d_sigma) * (np.linalg.norm(self.p_sigma) / self.chi_n - 1))
|
|
433
|
+
|
|
434
|
+
# Ensure positive semidefinite covariance matrix
|
|
435
|
+
epsilon = 1e-8
|
|
436
|
+
self.cov_matrix += epsilon * np.eye(self.dim)
|
|
437
|
+
eigvals, eigvecs = np.linalg.eigh(self.cov_matrix)
|
|
438
|
+
eigvals = np.maximum(eigvals, epsilon)
|
|
439
|
+
self.cov_matrix = np.dot(eigvecs, np.dot(np.diag(eigvals), eigvecs.T))
|
|
440
|
+
|
|
441
|
+
def optimize(self, iterations):
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
.. description::
|
|
445
|
+
Runs the CMA-ES optimization loop for a fixed number of generations. Each generation samples a new
|
|
446
|
+
population, evaluates the objective, and updates the distribution parameters.
|
|
447
|
+
|
|
448
|
+
.. inputs::
|
|
449
|
+
:param iterations: number of generations to run the optimization.
|
|
450
|
+
:type iterations: int
|
|
451
|
+
|
|
452
|
+
.. outputs::
|
|
453
|
+
:return mean: optimized mean vector representing the best solution estimate.
|
|
454
|
+
:rtype mean: np.ndarray
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
self.iterations = 0
|
|
458
|
+
for _ in range(iterations):
|
|
459
|
+
samples = self.sample_population()
|
|
460
|
+
fitness = np.array([self.objective_function(s) for s in samples])
|
|
461
|
+
self.update(samples, fitness)
|
|
462
|
+
self.iterations += 1
|
|
463
|
+
return self.mean
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class ZORM:
|
|
468
|
+
def __init__(self, func, x0, num_iterations, mu=0.05, h=0.001, t=1, grad_type='noth', constraint_type='notc'):
|
|
469
|
+
"""
|
|
470
|
+
ZORM optimizer.
|
|
471
|
+
|
|
472
|
+
Parameters:
|
|
473
|
+
- func: The objective function to minimize.
|
|
474
|
+
- mu: Smoothing parameter for gradient approximation.
|
|
475
|
+
- h: Step size.
|
|
476
|
+
- x0: initial guess
|
|
477
|
+
- num_iterations: number of iterations
|
|
478
|
+
- t: number of sampled directions for gradient estimation (averaged).
|
|
479
|
+
- grad_type: 'noth' for forward gradient, 'h' for symmetric (central difference).
|
|
480
|
+
- constraint_type: 'notc' for unconstrained, 'c' for constrained optimization.
|
|
481
|
+
"""
|
|
482
|
+
self.func = func
|
|
483
|
+
self.mu = mu
|
|
484
|
+
self.h = h
|
|
485
|
+
self.x0=x0
|
|
486
|
+
self.T = num_iterations
|
|
487
|
+
self.t = t
|
|
488
|
+
self.grad_type = grad_type
|
|
489
|
+
self.constraint_type = constraint_type
|
|
490
|
+
|
|
491
|
+
def _sample_gaussian(self, d):
|
|
492
|
+
return np.random.normal(0, 1, size=d)
|
|
493
|
+
|
|
494
|
+
def _feasible_projection(self, x, min, max):
|
|
495
|
+
y = np.clip(x,min,max)
|
|
496
|
+
return y
|
|
497
|
+
|
|
498
|
+
def _grad(self, x):
|
|
499
|
+
grad_sum = np.zeros(len(x))
|
|
500
|
+
fx = self.func(x)
|
|
501
|
+
for _ in range(self.t):
|
|
502
|
+
u = self._sample_gaussian(len(x))
|
|
503
|
+
perturbation = self.mu * u
|
|
504
|
+
if self.grad_type == 'noth':
|
|
505
|
+
g = (self.func(x + perturbation) - fx) / self.mu
|
|
506
|
+
elif self.grad_type == 'h':
|
|
507
|
+
g = (self.func(x + perturbation) - self.func(x - perturbation)) / (2 * self.mu)
|
|
508
|
+
else:
|
|
509
|
+
raise ValueError("Invalid gradient type. Use 'noth' or 'h'.")
|
|
510
|
+
grad_sum += g * u
|
|
511
|
+
return grad_sum / self.t
|
|
512
|
+
|
|
513
|
+
def _step(self, x):
|
|
514
|
+
grad_est = self._grad(x)
|
|
515
|
+
return x - self.h * grad_est
|
|
516
|
+
|
|
517
|
+
def optimize(self, lower_bounds=None, upper_bounds=None):
|
|
518
|
+
"""
|
|
519
|
+
Runs the ZORM optimization.
|
|
520
|
+
|
|
521
|
+
Parameters:
|
|
522
|
+
- x0: Initial guess (1D numpy array).
|
|
523
|
+
- T: Number of iterations.
|
|
524
|
+
- lower_bounds: Lower bounds matrix G for constraints (only needed if constraint_type='c').
|
|
525
|
+
- upper_bounds: Upper bounds vector h for constraints (only needed if constraint_type='c').
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
- x: A (dim, T+1) array of iterates.
|
|
529
|
+
"""
|
|
530
|
+
dim = len(self.x0)
|
|
531
|
+
x = np.zeros((dim, self.T + 1))
|
|
532
|
+
x[:, 0] = self.x0
|
|
533
|
+
if lower_bounds is None or upper_bounds is None:
|
|
534
|
+
raise ValueError("Bounds must be provided when constraint_type is 'c'.")
|
|
535
|
+
for k in range(self.T):
|
|
536
|
+
new_x = self._step(x[:, k])
|
|
537
|
+
if self.constraint_type == 'c':
|
|
538
|
+
new_x = self._feasible_projection(new_x, lower_bounds, upper_bounds)
|
|
539
|
+
x[:, k + 1] = new_x
|
|
540
|
+
|
|
541
|
+
return x
|
|
542
|
+
|
|
543
|
+
from OptiLine.utils import calc_splines,create_raceline, calc_head_curv_an, H_f, import_veh_dyn_info
|
|
544
|
+
from OptiLine.KinematicProfs import calc_vel_profile, calc_ax_profile, calc_t_profile , cumulative_distances
|
|
545
|
+
|
|
546
|
+
class Opt_min_CurvTime:
|
|
547
|
+
def __init__(self, reftrack, center, mu=0.05, h=0.001, kapb=0.7, sfty=1, t=1, si=0.8,
|
|
548
|
+
vm=22.88, m_veh=3, drag_coeff=0.0045, MC=1, min_s=None, max_s=None, sigma=0.001,
|
|
549
|
+
iterations_ZO=300, iterations_CMA=30, popsize=16,
|
|
550
|
+
ggv_import_path="maps/ggv.csv", ax_max_machines_import_path="maps/ax_max_machines.csv",
|
|
551
|
+
fw=3, refine_every=None, refine_subsample=1):
|
|
552
|
+
"""
|
|
553
|
+
Min Curv and Time optimizer.
|
|
554
|
+
|
|
555
|
+
Parameters:
|
|
556
|
+
- reftrack: reference track array containing the reference line and track widths [x, y, w_tr_right, w_tr_left].
|
|
557
|
+
- center: array containing the center line and track widths [x, y, w_tr_right, w_tr_left].
|
|
558
|
+
- mu: Smoothing parameter for gradient approximation for ZO solver
|
|
559
|
+
- h: Step size for ZO solver
|
|
560
|
+
- iterations_ZO: number of iterations for ZO solver
|
|
561
|
+
- sfty: half of the vehicle width
|
|
562
|
+
- t = number of sampled directions for gradient estimation (averaged) for ZO solver
|
|
563
|
+
- kapb = bound on the maximum allowed curvature
|
|
564
|
+
- si: interpolation step size of the race line
|
|
565
|
+
- vm: maximum available velocity
|
|
566
|
+
- MC: Monte Carlo number of repetion for ZO solvers
|
|
567
|
+
- min_s: minimum curv length
|
|
568
|
+
- max_s: maximum curv length
|
|
569
|
+
- sigma: Initial covariance for CMA-es solver
|
|
570
|
+
- popsize: population size for CMA-es solver
|
|
571
|
+
- iterations_CMA: number of iterations for ZORM solver
|
|
572
|
+
- iterations_CMA: number of iterations for CMA-es solver
|
|
573
|
+
- ggv_import_path: Path for importing ggv file
|
|
574
|
+
- ax_max_machines_import_path: path for importing ax_max_machines file.
|
|
575
|
+
- fw: filter window lengths for convolution (moving average) filtering of velocity profile
|
|
576
|
+
- m_veh: vehicle mass
|
|
577
|
+
- drag_coeff: drag coefficient
|
|
578
|
+
- refine_every: int or None. When set to a positive integer q, CurveLenOpt rebuilds the
|
|
579
|
+
reference track from the current optimised raceline every q ZO iterations.
|
|
580
|
+
None (default) disables refinement and preserves the original behaviour.
|
|
581
|
+
Call reset() to restore self.reftrack to the original track at any time.
|
|
582
|
+
- refine_subsample: int. Sub-sampling stride applied to the new raceline before it
|
|
583
|
+
becomes the refined reference track. 1 = no sub-sampling (default).
|
|
584
|
+
"""
|
|
585
|
+
|
|
586
|
+
if min_s is None and max_s is None:
|
|
587
|
+
el_lengths = np.sqrt(np.sum(np.power(np.diff(np.vstack((reftrack[:, 0:2], reftrack[0, 0:2])), axis=0), 2), axis=1))
|
|
588
|
+
min_s = np.min(el_lengths) *1
|
|
589
|
+
max_s = np.max(el_lengths) *1.7
|
|
590
|
+
|
|
591
|
+
self.reftrack = reftrack
|
|
592
|
+
self.mu = mu
|
|
593
|
+
self.h = h
|
|
594
|
+
self.t = t
|
|
595
|
+
self.ggv_import_path = ggv_import_path
|
|
596
|
+
self.ax_max_machines_import_path = ax_max_machines_import_path
|
|
597
|
+
self.sfty = sfty
|
|
598
|
+
self.kapb = kapb
|
|
599
|
+
self.si = si
|
|
600
|
+
self.vm = vm
|
|
601
|
+
self.MC = MC
|
|
602
|
+
self.min_s=min_s
|
|
603
|
+
self.max_s=max_s
|
|
604
|
+
self.iterations_ZO=iterations_ZO
|
|
605
|
+
self.iterations_CMA=iterations_CMA
|
|
606
|
+
self.sigma = sigma
|
|
607
|
+
self.popsize = popsize
|
|
608
|
+
self.fw = fw
|
|
609
|
+
self.m_veh=m_veh
|
|
610
|
+
self.drag_coeff=drag_coeff
|
|
611
|
+
self.center=center
|
|
612
|
+
lengths = np.sqrt(np.sum(np.power(np.diff(self.reftrack[:,0:2], axis=0), 2), axis=1))
|
|
613
|
+
lengths=np.append(lengths, lengths[0])
|
|
614
|
+
self.lengths=lengths
|
|
615
|
+
self.ggv,self.ax_max_machines =import_veh_dyn_info(ggv_import_path=self.ggv_import_path,ax_max_machines_import_path=self.ax_max_machines_import_path)
|
|
616
|
+
# Pre-built p_ggv cache keyed by kappa array size (size is approx constant across f_t calls)
|
|
617
|
+
self._p_ggv_cache = {}
|
|
618
|
+
|
|
619
|
+
# Refinement settings
|
|
620
|
+
self.refine_every = refine_every
|
|
621
|
+
self.refine_subsample = int(refine_subsample)
|
|
622
|
+
# Immutable copy of the original track; used by reset() and _build_refined_reftrack()
|
|
623
|
+
self._base_reftrack = reftrack.copy()
|
|
624
|
+
|
|
625
|
+
coeffs_x, coeffs_y, M, normvec_norm = calc_splines(path=np.vstack((center[:, 0:2], center[0, 0:2])),use_dist_scaling=True)
|
|
626
|
+
|
|
627
|
+
self.bound1 = center[:, 0:2] - normvec_norm * np.expand_dims(center[:, 2], axis=1)
|
|
628
|
+
self.bound2 = center[:, 0:2] + normvec_norm * np.expand_dims(center[:, 3], axis=1)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# ------------------------------------------------------------------
|
|
632
|
+
# Public helper
|
|
633
|
+
# ------------------------------------------------------------------
|
|
634
|
+
|
|
635
|
+
def reset(self):
|
|
636
|
+
"""
|
|
637
|
+
.. description::
|
|
638
|
+
Restore self.reftrack to the original reference track supplied at construction
|
|
639
|
+
time, reverting any in-place refinements made during CurveLenOpt.
|
|
640
|
+
"""
|
|
641
|
+
self._update_reftrack(self._base_reftrack.copy())
|
|
642
|
+
|
|
643
|
+
# ------------------------------------------------------------------
|
|
644
|
+
# Private helpers for path refinement
|
|
645
|
+
# ------------------------------------------------------------------
|
|
646
|
+
|
|
647
|
+
def _update_reftrack(self, reftrack_new: np.ndarray) -> None:
|
|
648
|
+
"""
|
|
649
|
+
.. description::
|
|
650
|
+
Replace self.reftrack with reftrack_new and recompute all derived quantities:
|
|
651
|
+
segment lengths, optimisation bounds (min_s / max_s), and the p_ggv cache.
|
|
652
|
+
|
|
653
|
+
.. inputs::
|
|
654
|
+
:param reftrack_new: new reference track [x, y, w_right, w_left].
|
|
655
|
+
:type reftrack_new: np.ndarray
|
|
656
|
+
"""
|
|
657
|
+
self.reftrack = reftrack_new
|
|
658
|
+
lengths = np.sqrt(np.sum(
|
|
659
|
+
np.power(np.diff(reftrack_new[:, 0:2], axis=0), 2), axis=1))
|
|
660
|
+
lengths = np.append(lengths, lengths[0])
|
|
661
|
+
self.lengths = lengths
|
|
662
|
+
self.min_s = float(np.min(lengths))
|
|
663
|
+
self.max_s = float(np.max(lengths)) * 1.7
|
|
664
|
+
self._p_ggv_cache = {} # kappa size may have changed — invalidate
|
|
665
|
+
|
|
666
|
+
def _build_refined_reftrack(self,
|
|
667
|
+
alpha_m: np.ndarray,
|
|
668
|
+
normvec_norm: np.ndarray) -> np.ndarray:
|
|
669
|
+
"""
|
|
670
|
+
.. description::
|
|
671
|
+
Build a geometrically correct refined reference track from the QP solution on
|
|
672
|
+
the CURRENT self.reftrack.
|
|
673
|
+
|
|
674
|
+
The new centreline is the set of control points shifted laterally by alpha_m
|
|
675
|
+
along their unit normal vectors. The track half-widths are adjusted so that
|
|
676
|
+
the two physical boundary lines stay EXACTLY in place:
|
|
677
|
+
|
|
678
|
+
new_w_col2[i] = w_col2[i] + alpha_m[i]
|
|
679
|
+
new_w_col3[i] = w_col3[i] − alpha_m[i]
|
|
680
|
+
|
|
681
|
+
Derivation (QP constraint boundaries preserved):
|
|
682
|
+
The QP constraint is −(w_col3 − sfty) ≤ α ≤ (w_col2 − sfty),
|
|
683
|
+
so positive α moves the car toward B_A = center + n · w_col2
|
|
684
|
+
and negative α moves it toward B_B = center − n · w_col3.
|
|
685
|
+
|
|
686
|
+
After the shift, the remaining distances to each boundary are:
|
|
687
|
+
room toward B_A: w_col2 − alpha (moved closer → less room)
|
|
688
|
+
room toward B_B: w_col3 + alpha (moved away → more room)
|
|
689
|
+
|
|
690
|
+
From the new center the next-pass optimizer can reach at most:
|
|
691
|
+
new_center + (new_w_col2 − sfty)·n
|
|
692
|
+
= (center + alpha·n) + (w_col2 − alpha − sfty)·n
|
|
693
|
+
= center + (w_col2 − sfty)·n ✓
|
|
694
|
+
new_center − (new_w_col3 − sfty)·n
|
|
695
|
+
= (center + alpha·n) − (w_col3 + alpha − sfty)·n
|
|
696
|
+
= center − (w_col3 − sfty)·n ✓
|
|
697
|
+
i.e. the optimizer is confined to exactly the same physical region
|
|
698
|
+
as in the original pass, regardless of how many refinement steps
|
|
699
|
+
have been taken.
|
|
700
|
+
|
|
701
|
+
A minimum clearance of self.sfty is enforced on both sides so widths can
|
|
702
|
+
never fall below the vehicle half-width. The optional sub-sampling stride
|
|
703
|
+
self.refine_subsample is applied before returning.
|
|
704
|
+
|
|
705
|
+
.. inputs::
|
|
706
|
+
:param alpha_m: lateral shifts in m at every control point, shape (N,).
|
|
707
|
+
:type alpha_m: np.ndarray
|
|
708
|
+
:param normvec_norm: unit normal vectors at every control point, shape (N, 2).
|
|
709
|
+
:type normvec_norm: np.ndarray
|
|
710
|
+
|
|
711
|
+
.. outputs::
|
|
712
|
+
:return reftrack_new: refined reference track [x, y, w_col2, w_col3].
|
|
713
|
+
:rtype reftrack_new: np.ndarray
|
|
714
|
+
"""
|
|
715
|
+
new_xy = self.reftrack[:, :2] + alpha_m[:, np.newaxis] * normvec_norm
|
|
716
|
+
new_w2 = self.reftrack[:, 2] - alpha_m
|
|
717
|
+
new_w3 = self.reftrack[:, 3] + alpha_m
|
|
718
|
+
|
|
719
|
+
reftrack_new = np.column_stack([new_xy, new_w2, new_w3])
|
|
720
|
+
|
|
721
|
+
# step = int(self.refine_subsample)
|
|
722
|
+
# if step > 1:
|
|
723
|
+
# nn = reftrack_new.shape[0]
|
|
724
|
+
# idx = np.arange(0, nn, step)
|
|
725
|
+
# idx = np.unique(np.concatenate([idx, [nn - 1]]))
|
|
726
|
+
# reftrack_new = reftrack_new[idx]
|
|
727
|
+
|
|
728
|
+
return reftrack_new
|
|
729
|
+
|
|
730
|
+
def _solve_alpha(self, ds: np.ndarray):
|
|
731
|
+
"""
|
|
732
|
+
.. description::
|
|
733
|
+
Solve the minimum-curvature QP on self.reftrack for the given segment lengths
|
|
734
|
+
and return the lateral shift vector alpha_m together with the normal vectors.
|
|
735
|
+
Both arrays are at CONTROL-POINT resolution (one entry per track point), which
|
|
736
|
+
is what _build_refined_reftrack needs to compute physically correct widths.
|
|
737
|
+
|
|
738
|
+
.. inputs::
|
|
739
|
+
:param ds: spline segment lengths for the current reference track, shape (N,).
|
|
740
|
+
:type ds: np.ndarray
|
|
741
|
+
|
|
742
|
+
.. outputs::
|
|
743
|
+
:return alpha_m: lateral shift at every control point in m, shape (N,).
|
|
744
|
+
:rtype alpha_m: np.ndarray
|
|
745
|
+
:return normvec_norm: unit normal vectors at every control point, shape (N, 2).
|
|
746
|
+
:rtype normvec_norm: np.ndarray
|
|
747
|
+
"""
|
|
748
|
+
coeffs_x, coeffs_y, M_mat, normvec_norm = calc_splines(
|
|
749
|
+
path=np.vstack((self.reftrack[:, 0:2], self.reftrack[0, 0:2])),
|
|
750
|
+
el_lengths=ds)
|
|
751
|
+
H, f, G, h = H_f(
|
|
752
|
+
reftrack=self.reftrack,
|
|
753
|
+
normvectors=normvec_norm,
|
|
754
|
+
A=M_mat,
|
|
755
|
+
kappa_bound=self.kapb,
|
|
756
|
+
w_veh=self.sfty,
|
|
757
|
+
closed=True)
|
|
758
|
+
alpha_m = quadprog.solve_qp(H, -f, -G.T, -h, 0)[0]
|
|
759
|
+
return alpha_m, normvec_norm
|
|
760
|
+
|
|
761
|
+
def _extract_raceline(self, ds: np.ndarray) -> np.ndarray:
|
|
762
|
+
"""
|
|
763
|
+
.. description::
|
|
764
|
+
Convenience method: solve the QP and return the DENSE interpolated raceline
|
|
765
|
+
(one point every self.si metres). Useful for external inspection or plotting.
|
|
766
|
+
Refinement internally uses _solve_alpha + _build_refined_reftrack instead.
|
|
767
|
+
|
|
768
|
+
.. inputs::
|
|
769
|
+
:param ds: spline segment lengths for the current reference track, shape (N,).
|
|
770
|
+
:type ds: np.ndarray
|
|
771
|
+
|
|
772
|
+
.. outputs::
|
|
773
|
+
:return raceline_interp: interpolated raceline [x, y], shape (M, 2).
|
|
774
|
+
:rtype raceline_interp: np.ndarray
|
|
775
|
+
"""
|
|
776
|
+
alpha_m, normvec_norm = self._solve_alpha(ds)
|
|
777
|
+
raceline_interp, *_ = create_raceline(
|
|
778
|
+
refline=self.reftrack[:, :2],
|
|
779
|
+
normvectors=normvec_norm,
|
|
780
|
+
alpha=alpha_m,
|
|
781
|
+
stepsize_interp=self.si)
|
|
782
|
+
return raceline_interp
|
|
783
|
+
|
|
784
|
+
# ------------------------------------------------------------------
|
|
785
|
+
|
|
786
|
+
def f_t(self,ds):
|
|
787
|
+
"""
|
|
788
|
+
|
|
789
|
+
.. description::
|
|
790
|
+
Objective function for the curve-length optimization. Given a set of spline segment lengths ds, computes
|
|
791
|
+
the minimum-curvature raceline, interpolates the path, calculates the velocity and acceleration profiles,
|
|
792
|
+
and returns the estimated lap time.
|
|
793
|
+
|
|
794
|
+
.. inputs::
|
|
795
|
+
:param ds: array of spline segment lengths used to parameterize the reference track.
|
|
796
|
+
:type ds: np.ndarray
|
|
797
|
+
|
|
798
|
+
.. outputs::
|
|
799
|
+
:return laptime: estimated lap time in seconds for the raceline generated from the given segment lengths.
|
|
800
|
+
:rtype laptime: float
|
|
801
|
+
"""
|
|
802
|
+
|
|
803
|
+
sfty = self.sfty
|
|
804
|
+
kapb = self.kapb
|
|
805
|
+
|
|
806
|
+
coeffs_x, coeffs_y, M, normvec_norm = calc_splines(path=np.vstack((self.reftrack[:, 0:2], self.reftrack[0, 0:2])),el_lengths=ds)
|
|
807
|
+
H, f, G , h = H_f(reftrack=self.reftrack,
|
|
808
|
+
normvectors=normvec_norm,
|
|
809
|
+
A=M,
|
|
810
|
+
kappa_bound=kapb,
|
|
811
|
+
w_veh=sfty,
|
|
812
|
+
closed=True)
|
|
813
|
+
|
|
814
|
+
alpha_m = quadprog.solve_qp(H, -f, -G.T,-h,0)[0]
|
|
815
|
+
si=self.si
|
|
816
|
+
raceline_interp, a_opt, coeffs_x_opt, coeffs_y_opt, spline_inds_opt_interp, t_vals_opt_interp, s_points_opt_interp,\
|
|
817
|
+
spline_lengths_opt, el_lengths_opt_interp = create_raceline(refline=self.reftrack[:, :2],
|
|
818
|
+
normvectors=normvec_norm,
|
|
819
|
+
alpha=alpha_m,
|
|
820
|
+
stepsize_interp=si,)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
psi_vel_opt, kappa_opt =calc_head_curv_an(coeffs_x=coeffs_x_opt,
|
|
824
|
+
coeffs_y=coeffs_y_opt,
|
|
825
|
+
ind_spls=spline_inds_opt_interp,
|
|
826
|
+
t_spls=t_vals_opt_interp)
|
|
827
|
+
|
|
828
|
+
# s_splines = cumulative_distances(el_lengths_opt_interp)
|
|
829
|
+
|
|
830
|
+
vm =self.vm
|
|
831
|
+
fw = 3
|
|
832
|
+
n = kappa_opt.size
|
|
833
|
+
if n not in self._p_ggv_cache:
|
|
834
|
+
self._p_ggv_cache[n] = np.repeat(np.expand_dims(self.ggv, axis=0), n, axis=0)
|
|
835
|
+
vx_profile_opt = calc_vel_profile(ggv=self.ggv,
|
|
836
|
+
ax_max_machines=self.ax_max_machines,
|
|
837
|
+
v_max=vm,
|
|
838
|
+
kappa=kappa_opt,
|
|
839
|
+
el_lengths=el_lengths_opt_interp,
|
|
840
|
+
closed=True,
|
|
841
|
+
filt_window=fw,
|
|
842
|
+
dyn_model_exp=1.0,
|
|
843
|
+
drag_coeff=self.drag_coeff,
|
|
844
|
+
m_veh=self.m_veh,
|
|
845
|
+
v_start = 0.0,
|
|
846
|
+
p_ggv=self._p_ggv_cache[n])
|
|
847
|
+
|
|
848
|
+
# calculate longitudinal acceleration profile
|
|
849
|
+
vx_profile_opt_cl = np.append(vx_profile_opt, vx_profile_opt[0])
|
|
850
|
+
ax_profile_opt = calc_ax_profile(vx_profile=vx_profile_opt_cl,
|
|
851
|
+
el_lengths=el_lengths_opt_interp,
|
|
852
|
+
eq_length_output=False)
|
|
853
|
+
|
|
854
|
+
# calculate laptime
|
|
855
|
+
t_profile_cl = calc_t_profile(vx_profile=vx_profile_opt,
|
|
856
|
+
ax_profile=ax_profile_opt,
|
|
857
|
+
el_lengths=el_lengths_opt_interp)
|
|
858
|
+
|
|
859
|
+
return t_profile_cl[-1]
|
|
860
|
+
|
|
861
|
+
def CurveLenOpt(self, solver='ZO', refine_every=None):
|
|
862
|
+
"""
|
|
863
|
+
|
|
864
|
+
.. description::
|
|
865
|
+
Optimizes the spline segment lengths of the reference track to minimize lap time.
|
|
866
|
+
Supports two solvers: zeroth-order random method (ZO) and CMA-ES (CMA).
|
|
867
|
+
Monte Carlo averaging is applied over self.MC runs when refinement is disabled.
|
|
868
|
+
|
|
869
|
+
When refine_every is set to a positive integer q the ZO loop is split into blocks
|
|
870
|
+
of q iterations. After each block (except the last) the current optimised raceline
|
|
871
|
+
is extracted, the reference track is rebuilt from that raceline via
|
|
872
|
+
_build_refined_reftrack, and optimisation continues on the refined track.
|
|
873
|
+
This updates self.reftrack as a side-effect; call reset() to restore the original.
|
|
874
|
+
|
|
875
|
+
.. inputs::
|
|
876
|
+
:param solver: optimization solver to use. 'ZO' for zeroth-order random
|
|
877
|
+
method, 'CMA' for CMA-ES.
|
|
878
|
+
:type solver: str
|
|
879
|
+
:param refine_every: per-call override for self.refine_every. Positive integer q
|
|
880
|
+
enables refinement every q iterations; 0 or None disables it.
|
|
881
|
+
:type refine_every: int or None
|
|
882
|
+
|
|
883
|
+
.. outputs::
|
|
884
|
+
:return ds_ff: optimized array of spline segment lengths for the current
|
|
885
|
+
self.reftrack (may be a refined track when refinement is used).
|
|
886
|
+
:rtype ds_ff: np.ndarray
|
|
887
|
+
"""
|
|
888
|
+
|
|
889
|
+
# Resolve effective refinement cadence (per-call overrides instance default)
|
|
890
|
+
q = refine_every if refine_every is not None else self.refine_every
|
|
891
|
+
use_refinement = (q is not None and int(q) > 0)
|
|
892
|
+
|
|
893
|
+
if solver == 'ZO':
|
|
894
|
+
ds_0 = self.lengths.copy()
|
|
895
|
+
|
|
896
|
+
if not use_refinement:
|
|
897
|
+
# ----------------------------------------------------------------
|
|
898
|
+
# Original behaviour: MC runs, no path refinement
|
|
899
|
+
# ----------------------------------------------------------------
|
|
900
|
+
ds_ff = np.zeros_like(ds_0)
|
|
901
|
+
for _ in range(self.MC):
|
|
902
|
+
ZOs = ZORM(self.f_t, ds_0, self.iterations_ZO,
|
|
903
|
+
mu=self.mu, h=self.h, t=self.t, constraint_type='c')
|
|
904
|
+
ds = ZOs.optimize(lower_bounds=self.min_s, upper_bounds=self.max_s)
|
|
905
|
+
ds_ff += ds[:, -1]
|
|
906
|
+
return ds_ff / self.MC
|
|
907
|
+
|
|
908
|
+
else:
|
|
909
|
+
# ----------------------------------------------------------------
|
|
910
|
+
# Refinement-aware loop
|
|
911
|
+
# ----------------------------------------------------------------
|
|
912
|
+
if self.MC > 1:
|
|
913
|
+
print("WARNING [CurveLenOpt]: MC > 1 is not supported with path "
|
|
914
|
+
"refinement; running a single pass (MC=1).")
|
|
915
|
+
|
|
916
|
+
q = int(q)
|
|
917
|
+
if q > self.iterations_ZO:
|
|
918
|
+
print(f"WARNING [CurveLenOpt]: refine_every ({q}) > iterations_ZO "
|
|
919
|
+
f"({self.iterations_ZO}). No refinement will occur.")
|
|
920
|
+
|
|
921
|
+
# Partition total iterations into blocks of q (last block may be shorter).
|
|
922
|
+
# Refinement happens BETWEEN blocks, NOT after the final block, so we
|
|
923
|
+
# always end with an optimised ds on the current self.reftrack.
|
|
924
|
+
n_full = self.iterations_ZO // q
|
|
925
|
+
rem = self.iterations_ZO % q
|
|
926
|
+
all_blocks = [q] * n_full
|
|
927
|
+
if rem > 0:
|
|
928
|
+
all_blocks.append(rem)
|
|
929
|
+
n_blocks = len(all_blocks)
|
|
930
|
+
n_refinements = n_blocks - 1 # refine between every pair of blocks
|
|
931
|
+
|
|
932
|
+
ds_current = self.lengths.copy()
|
|
933
|
+
|
|
934
|
+
for blk_idx, blk_iters in enumerate(all_blocks):
|
|
935
|
+
ZOs = ZORM(self.f_t, ds_current, blk_iters,
|
|
936
|
+
mu=self.mu, h=self.h, t=self.t, constraint_type='c')
|
|
937
|
+
ds_opt = ZOs.optimize(
|
|
938
|
+
lower_bounds=self.min_s, upper_bounds=self.max_s)[:, -1]
|
|
939
|
+
|
|
940
|
+
is_last = (blk_idx == n_blocks - 1)
|
|
941
|
+
|
|
942
|
+
if not is_last:
|
|
943
|
+
# Solve QP → exact alpha + normals → geometrically correct reftrack
|
|
944
|
+
alpha_m, normvec = self._solve_alpha(ds_opt)
|
|
945
|
+
reftrack_refined = self._build_refined_reftrack(alpha_m, normvec)
|
|
946
|
+
self._update_reftrack(reftrack_refined)
|
|
947
|
+
ds_current = self.lengths.copy() # new initial ds for next block
|
|
948
|
+
print(f" [path refine {blk_idx + 1}/{n_refinements}] "
|
|
949
|
+
f"{reftrack_refined.shape[0]} ctrl pts | "
|
|
950
|
+
f"laptime ≈ {self.f_t(ds_current):.3f} s")
|
|
951
|
+
else:
|
|
952
|
+
ds_current = ds_opt # final optimised result on current track
|
|
953
|
+
|
|
954
|
+
return ds_current
|
|
955
|
+
|
|
956
|
+
if solver == 'CMA':
|
|
957
|
+
if not use_refinement:
|
|
958
|
+
# ----------------------------------------------------------------
|
|
959
|
+
# Original behaviour: MC runs, no path refinement
|
|
960
|
+
# ----------------------------------------------------------------
|
|
961
|
+
mean = self.lengths.copy()
|
|
962
|
+
s_cmaa = np.zeros_like(mean)
|
|
963
|
+
for _ in range(self.MC):
|
|
964
|
+
cma_es_s = ConstrainedCMAES_t(
|
|
965
|
+
self.f_t, mean, self.sigma, self.popsize,
|
|
966
|
+
bounds1=np.ones_like(self.lengths) * self.max_s,
|
|
967
|
+
bounds2=np.ones_like(self.lengths) * self.min_s)
|
|
968
|
+
s_cmai = cma_es_s.optimize(iterations=self.iterations_CMA)
|
|
969
|
+
s_cmaa += s_cmai
|
|
970
|
+
return s_cmaa / self.MC
|
|
971
|
+
|
|
972
|
+
else:
|
|
973
|
+
# ----------------------------------------------------------------
|
|
974
|
+
# Refinement-aware loop (mirrors the ZO logic)
|
|
975
|
+
# ----------------------------------------------------------------
|
|
976
|
+
if self.MC > 1:
|
|
977
|
+
print("WARNING [CurveLenOpt]: MC > 1 is not supported with path "
|
|
978
|
+
"refinement; running a single pass (MC=1).")
|
|
979
|
+
|
|
980
|
+
q = int(q)
|
|
981
|
+
if q > self.iterations_CMA:
|
|
982
|
+
print(f"WARNING [CurveLenOpt]: refine_every ({q}) > iterations_CMA "
|
|
983
|
+
f"({self.iterations_CMA}). No refinement will occur.")
|
|
984
|
+
|
|
985
|
+
# Partition total CMA iterations into blocks of q.
|
|
986
|
+
# Refinement happens BETWEEN blocks, not after the final block.
|
|
987
|
+
n_full = self.iterations_CMA // q
|
|
988
|
+
rem = self.iterations_CMA % q
|
|
989
|
+
all_blocks = [q] * n_full
|
|
990
|
+
if rem > 0:
|
|
991
|
+
all_blocks.append(rem)
|
|
992
|
+
n_blocks = len(all_blocks)
|
|
993
|
+
n_refinements = n_blocks - 1
|
|
994
|
+
|
|
995
|
+
ds_current = self.lengths.copy()
|
|
996
|
+
|
|
997
|
+
for blk_idx, blk_iters in enumerate(all_blocks):
|
|
998
|
+
cma_es_s = ConstrainedCMAES_t(
|
|
999
|
+
self.f_t, ds_current, self.sigma, self.popsize,
|
|
1000
|
+
bounds1=np.ones_like(ds_current) * self.max_s,
|
|
1001
|
+
bounds2=np.ones_like(ds_current) * self.min_s)
|
|
1002
|
+
ds_opt = cma_es_s.optimize(iterations=blk_iters)
|
|
1003
|
+
|
|
1004
|
+
is_last = (blk_idx == n_blocks - 1)
|
|
1005
|
+
|
|
1006
|
+
if not is_last:
|
|
1007
|
+
# Solve QP → exact alpha + normals → geometrically correct reftrack
|
|
1008
|
+
alpha_m, normvec = self._solve_alpha(ds_opt)
|
|
1009
|
+
reftrack_refined = self._build_refined_reftrack(alpha_m, normvec)
|
|
1010
|
+
self._update_reftrack(reftrack_refined)
|
|
1011
|
+
ds_current = self.lengths.copy() # new initial mean for next block
|
|
1012
|
+
print(f" [path refine {blk_idx + 1}/{n_refinements}] "
|
|
1013
|
+
f"{reftrack_refined.shape[0]} ctrl pts | "
|
|
1014
|
+
f"laptime ≈ {self.f_t(ds_current):.3f} s")
|
|
1015
|
+
else:
|
|
1016
|
+
ds_current = ds_opt # final optimised result on current track
|
|
1017
|
+
|
|
1018
|
+
return ds_current
|
|
1019
|
+
|
|
1020
|
+
def generate_raceline(self, ds=None, solver='ZO', refine_every=None):
|
|
1021
|
+
"""
|
|
1022
|
+
|
|
1023
|
+
.. description::
|
|
1024
|
+
Generates the optimized raceline geometry for a given set of spline segment lengths.
|
|
1025
|
+
If no segment lengths are provided, runs CurveLenOpt first to obtain them.
|
|
1026
|
+
|
|
1027
|
+
When refine_every is set, CurveLenOpt is called with that cadence and self.reftrack
|
|
1028
|
+
may be updated as a side-effect (see CurveLenOpt docs). The returned raceline is
|
|
1029
|
+
computed on whatever self.reftrack is current after that call.
|
|
1030
|
+
|
|
1031
|
+
.. inputs::
|
|
1032
|
+
:param ds: array of spline segment lengths. If None, computed via
|
|
1033
|
+
CurveLenOpt(solver, refine_every).
|
|
1034
|
+
:type ds: np.ndarray
|
|
1035
|
+
:param solver: solver to use if ds is None. 'ZO' or 'CMA'.
|
|
1036
|
+
:type solver: str
|
|
1037
|
+
:param refine_every: per-call refinement cadence passed to CurveLenOpt when
|
|
1038
|
+
ds is None. None uses self.refine_every (default: no refinement).
|
|
1039
|
+
:type refine_every: int or None
|
|
1040
|
+
|
|
1041
|
+
.. outputs::
|
|
1042
|
+
:return raceline_interp: interpolated raceline coordinates [x, y].
|
|
1043
|
+
:rtype raceline_interp: np.ndarray
|
|
1044
|
+
:return ds: spline segment lengths used (either provided or optimized).
|
|
1045
|
+
:rtype ds: np.ndarray
|
|
1046
|
+
"""
|
|
1047
|
+
|
|
1048
|
+
if ds is None:
|
|
1049
|
+
ds = self.CurveLenOpt(solver=solver, refine_every=refine_every)
|
|
1050
|
+
|
|
1051
|
+
coeffs_x, coeffs_y, M, normvec_norm = calc_splines(path=np.vstack((self.reftrack[:, 0:2], self.reftrack[0, 0:2])),el_lengths=ds)
|
|
1052
|
+
H, f, G , h = H_f(reftrack=self.reftrack,
|
|
1053
|
+
normvectors=normvec_norm,
|
|
1054
|
+
A=M,
|
|
1055
|
+
kappa_bound=self.kapb,
|
|
1056
|
+
w_veh=self.sfty,
|
|
1057
|
+
closed=True)
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
alpha_m_s = quadprog.solve_qp(H, -f, -G.T,-h,0)[0]
|
|
1061
|
+
raceline_interp, a_opt, coeffs_x_opt, coeffs_y_opt, spline_inds_opt_interp, t_vals_opt_interp, s_points_opt_interp,\
|
|
1062
|
+
spline_lengths_opt, el_lengths_opt_interp = create_raceline(refline=self.reftrack[:, :2],
|
|
1063
|
+
normvectors=normvec_norm,
|
|
1064
|
+
alpha=alpha_m_s,
|
|
1065
|
+
stepsize_interp=self.si)
|
|
1066
|
+
return raceline_interp,ds
|
|
1067
|
+
|
|
1068
|
+
def generate_kinProfs(self, ds=None, solver='ZO', refine_every=None):
|
|
1069
|
+
"""
|
|
1070
|
+
|
|
1071
|
+
.. description::
|
|
1072
|
+
Generates the full kinematic profiles (velocity, acceleration, curvature, time,
|
|
1073
|
+
raceline) for a given set of spline segment lengths. If no segment lengths are
|
|
1074
|
+
provided, runs CurveLenOpt first.
|
|
1075
|
+
|
|
1076
|
+
When refine_every is set, CurveLenOpt is called with that cadence and self.reftrack
|
|
1077
|
+
may be updated as a side-effect (see CurveLenOpt docs). All profiles are then
|
|
1078
|
+
computed on whatever self.reftrack is current after that call.
|
|
1079
|
+
|
|
1080
|
+
.. inputs::
|
|
1081
|
+
:param ds: array of spline segment lengths. If None, computed via
|
|
1082
|
+
CurveLenOpt(solver, refine_every).
|
|
1083
|
+
:type ds: np.ndarray
|
|
1084
|
+
:param solver: solver to use if ds is None. 'ZO' or 'CMA'.
|
|
1085
|
+
:type solver: str
|
|
1086
|
+
:param refine_every: per-call refinement cadence passed to CurveLenOpt when
|
|
1087
|
+
ds is None. None uses self.refine_every (default: no refinement).
|
|
1088
|
+
:type refine_every: int or None
|
|
1089
|
+
|
|
1090
|
+
.. outputs::
|
|
1091
|
+
:return s_splines: cumulative distance profile along the raceline in m.
|
|
1092
|
+
:rtype s_splines: np.ndarray
|
|
1093
|
+
:return vx_profile_opt: optimized velocity profile in m/s.
|
|
1094
|
+
:rtype vx_profile_opt: np.ndarray
|
|
1095
|
+
:return ax_profile_opt: longitudinal acceleration profile in m/s2.
|
|
1096
|
+
:rtype ax_profile_opt: np.ndarray
|
|
1097
|
+
:return kappa_opt: curvature profile of the raceline in rad/m.
|
|
1098
|
+
:rtype kappa_opt: np.ndarray
|
|
1099
|
+
:return t_profile_cl: lap time profile in seconds (cumulative).
|
|
1100
|
+
:rtype t_profile_cl: np.ndarray
|
|
1101
|
+
:return raceline_interp: interpolated raceline coordinates [x, y].
|
|
1102
|
+
:rtype raceline_interp: np.ndarray
|
|
1103
|
+
"""
|
|
1104
|
+
|
|
1105
|
+
if ds is None:
|
|
1106
|
+
ds = self.CurveLenOpt(solver=solver, refine_every=refine_every)
|
|
1107
|
+
coeffs_x, coeffs_y, M, normvec_norm = calc_splines(path=np.vstack((self.reftrack[:, 0:2], self.reftrack[0, 0:2])),el_lengths=ds)
|
|
1108
|
+
H, f, G , h = H_f(reftrack=self.reftrack,
|
|
1109
|
+
normvectors=normvec_norm,
|
|
1110
|
+
A=M,
|
|
1111
|
+
kappa_bound=self.kapb,
|
|
1112
|
+
w_veh=self.sfty,
|
|
1113
|
+
closed=True)
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
alpha_m_s = quadprog.solve_qp(H, -f, -G.T,-h,0)[0]
|
|
1117
|
+
|
|
1118
|
+
raceline_interp, a_opt, coeffs_x_opt, coeffs_y_opt, spline_inds_opt_interp, t_vals_opt_interp, s_points_opt_interp,\
|
|
1119
|
+
spline_lengths_opt, el_lengths_opt_interp = create_raceline(refline=self.reftrack[:, :2],
|
|
1120
|
+
normvectors=normvec_norm,
|
|
1121
|
+
alpha=alpha_m_s,
|
|
1122
|
+
stepsize_interp=self.si)
|
|
1123
|
+
|
|
1124
|
+
psi_vel_opt, kappa_opt =calc_head_curv_an(coeffs_x=coeffs_x_opt,
|
|
1125
|
+
coeffs_y=coeffs_y_opt,
|
|
1126
|
+
ind_spls=spline_inds_opt_interp,
|
|
1127
|
+
t_spls=t_vals_opt_interp)
|
|
1128
|
+
|
|
1129
|
+
s_splines = cumulative_distances(el_lengths_opt_interp)
|
|
1130
|
+
vx_profile_opt = calc_vel_profile(ggv=self.ggv,
|
|
1131
|
+
ax_max_machines=self.ax_max_machines,
|
|
1132
|
+
v_max=self.vm,
|
|
1133
|
+
kappa=kappa_opt,
|
|
1134
|
+
el_lengths=el_lengths_opt_interp,
|
|
1135
|
+
closed=True,
|
|
1136
|
+
filt_window=self.fw,
|
|
1137
|
+
dyn_model_exp=1.0,
|
|
1138
|
+
drag_coeff=self.drag_coeff,
|
|
1139
|
+
m_veh=self.m_veh,
|
|
1140
|
+
v_start = 0.0)
|
|
1141
|
+
|
|
1142
|
+
# calculate longitudinal acceleration profile
|
|
1143
|
+
vx_profile_opt_cl = np.append(vx_profile_opt, vx_profile_opt[0])
|
|
1144
|
+
ax_profile_opt = calc_ax_profile(vx_profile=vx_profile_opt_cl,
|
|
1145
|
+
el_lengths=el_lengths_opt_interp,
|
|
1146
|
+
eq_length_output=False)
|
|
1147
|
+
|
|
1148
|
+
# calculate laptime
|
|
1149
|
+
t_profile_cl = calc_t_profile(vx_profile=vx_profile_opt,
|
|
1150
|
+
ax_profile=ax_profile_opt,
|
|
1151
|
+
el_lengths=el_lengths_opt_interp)
|
|
1152
|
+
|
|
1153
|
+
return s_splines, vx_profile_opt, ax_profile_opt, kappa_opt, t_profile_cl, raceline_interp
|
|
1154
|
+
|
|
1155
|
+
def Comparison(self, ds_ZO=None, ds_CMA=None, plot='N', output='N', refine_every=None):
|
|
1156
|
+
"""
|
|
1157
|
+
|
|
1158
|
+
.. description::
|
|
1159
|
+
Compares raceline kinematic profiles across four cases: ZO-optimized, CMA-ES-optimized,
|
|
1160
|
+
initial segment lengths, and the centerline. Prints lap times and optionally plots and
|
|
1161
|
+
returns all profiles.
|
|
1162
|
+
|
|
1163
|
+
Each solver is always run from the original base reference track so the comparison is
|
|
1164
|
+
fair regardless of whether path refinement is used for ZO. self.reftrack is reset to
|
|
1165
|
+
the base track when Comparison returns.
|
|
1166
|
+
|
|
1167
|
+
.. inputs::
|
|
1168
|
+
:param ds_ZO: optimized segment lengths from the ZO solver. If None, computed
|
|
1169
|
+
internally via CurveLenOpt('ZO', refine_every).
|
|
1170
|
+
:type ds_ZO: np.ndarray
|
|
1171
|
+
:param ds_CMA: optimized segment lengths from the CMA-ES solver. If None,
|
|
1172
|
+
computed internally via CurveLenOpt('CMA').
|
|
1173
|
+
:type ds_CMA: np.ndarray
|
|
1174
|
+
:param plot: 'Y' to display comparison plots, 'N' to skip.
|
|
1175
|
+
:type plot: str
|
|
1176
|
+
:param output: controls which profiles are returned. 'Y' returns all four,
|
|
1177
|
+
'ZO'/'CMA'/'initial'/'center' returns the corresponding case
|
|
1178
|
+
only. 'N' returns nothing.
|
|
1179
|
+
:type output: str
|
|
1180
|
+
:param refine_every: path-refinement cadence forwarded to CurveLenOpt for the ZO
|
|
1181
|
+
solver only. None (default) uses self.refine_every.
|
|
1182
|
+
:type refine_every: int or None
|
|
1183
|
+
|
|
1184
|
+
.. outputs::
|
|
1185
|
+
:return profiles: kinematic profiles (s_splines, vx, ax, kappa, t_profile, raceline)
|
|
1186
|
+
for the selected output case(s). None if output is 'N'.
|
|
1187
|
+
:rtype profiles: tuple or None
|
|
1188
|
+
"""
|
|
1189
|
+
|
|
1190
|
+
# ---- ZO (run from original base track; refinement optional) ----
|
|
1191
|
+
self._update_reftrack(self._base_reftrack.copy())
|
|
1192
|
+
if ds_ZO is None:
|
|
1193
|
+
ds_ZO = self.CurveLenOpt(solver='ZO', refine_every=refine_every)
|
|
1194
|
+
s_splines, vx_profile_opt, ax_profile_opt, kappa_opt, t_profile_cl, raceline_interp = \
|
|
1195
|
+
self.generate_kinProfs(ds=ds_ZO)
|
|
1196
|
+
|
|
1197
|
+
# ---- CMA (always from original base track, no refinement) ----
|
|
1198
|
+
self._update_reftrack(self._base_reftrack.copy())
|
|
1199
|
+
if ds_CMA is None:
|
|
1200
|
+
ds_CMA = self.CurveLenOpt(solver='CMA')
|
|
1201
|
+
s_splines1, vx_profile_opt1, ax_profile_opt1, kappa_opt1, t_profile_cl1, raceline_interp1 = \
|
|
1202
|
+
self.generate_kinProfs(ds=ds_CMA)
|
|
1203
|
+
|
|
1204
|
+
# ---- initial segment lengths (base track, no optimisation) ----
|
|
1205
|
+
self._update_reftrack(self._base_reftrack.copy())
|
|
1206
|
+
ds0 = self.lengths.copy()
|
|
1207
|
+
s_splines2, vx_profile_opt2, ax_profile_opt2, kappa_opt2, t_profile_cl2, raceline_interp2 = \
|
|
1208
|
+
self.generate_kinProfs(ds=ds0)
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
##Calculate the profiles for centerline
|
|
1212
|
+
lengths1 = np.sqrt(np.sum(np.power(np.diff(self.center[:,0:2], axis=0), 2), axis=1))
|
|
1213
|
+
lengths1=np.append(lengths1, lengths1[0])
|
|
1214
|
+
coeffs_x, coeffs_y, M, normvec_norm = calc_splines(path=np.vstack((self.center[:, 0:2], self.center[0, 0:2])),el_lengths=lengths1)
|
|
1215
|
+
H, f, G , h = H_f(reftrack=self.center,
|
|
1216
|
+
normvectors=normvec_norm,
|
|
1217
|
+
A=M,
|
|
1218
|
+
kappa_bound=self.kapb,
|
|
1219
|
+
w_veh=self.sfty,
|
|
1220
|
+
closed=True)
|
|
1221
|
+
alpha_m_0 = quadprog.solve_qp(H, -f, -G.T,-h,0)[0]
|
|
1222
|
+
|
|
1223
|
+
raceline_interp4, a_opt4, coeffs_x_opt4, coeffs_y_opt4, spline_inds_opt_interp4, t_vals_opt_interp4, s_points_opt_interp4,\
|
|
1224
|
+
spline_lengths_opt4, el_lengths_opt_interp4 = create_raceline(refline=self.center[:, :2],
|
|
1225
|
+
normvectors=normvec_norm,
|
|
1226
|
+
alpha=np.zeros_like(alpha_m_0),
|
|
1227
|
+
stepsize_interp=self.si)
|
|
1228
|
+
|
|
1229
|
+
psi_vel_opt4, kappa_opt4 =calc_head_curv_an(coeffs_x=coeffs_x_opt4,
|
|
1230
|
+
coeffs_y=coeffs_y_opt4,
|
|
1231
|
+
ind_spls=spline_inds_opt_interp4,
|
|
1232
|
+
t_spls=t_vals_opt_interp4)
|
|
1233
|
+
|
|
1234
|
+
s_splines4 = cumulative_distances(el_lengths_opt_interp4)
|
|
1235
|
+
vx_profile_opt4 = calc_vel_profile(ggv=self.ggv,
|
|
1236
|
+
ax_max_machines=self.ax_max_machines,
|
|
1237
|
+
v_max=self.vm,
|
|
1238
|
+
kappa=kappa_opt4,
|
|
1239
|
+
el_lengths=el_lengths_opt_interp4,
|
|
1240
|
+
closed=True,
|
|
1241
|
+
filt_window=self.fw,
|
|
1242
|
+
dyn_model_exp=1.0,
|
|
1243
|
+
drag_coeff=self.drag_coeff,
|
|
1244
|
+
m_veh=self.m_veh,
|
|
1245
|
+
v_start = 0.0)
|
|
1246
|
+
|
|
1247
|
+
# calculate longitudinal acceleration profile
|
|
1248
|
+
vx_profile_opt_cl4 = np.append(vx_profile_opt4, vx_profile_opt4[0])
|
|
1249
|
+
ax_profile_opt4 = calc_ax_profile(vx_profile=vx_profile_opt_cl4,
|
|
1250
|
+
el_lengths=el_lengths_opt_interp4,
|
|
1251
|
+
eq_length_output=False)
|
|
1252
|
+
|
|
1253
|
+
# calculate laptime
|
|
1254
|
+
t_profile_cl4 = calc_t_profile(vx_profile=vx_profile_opt4,
|
|
1255
|
+
ax_profile=ax_profile_opt4,
|
|
1256
|
+
el_lengths=el_lengths_opt_interp4)
|
|
1257
|
+
|
|
1258
|
+
print("INFO: Estimated laptime for ZO: %.2fs" % t_profile_cl[-1])
|
|
1259
|
+
print("INFO: Estimated laptime for CMA-ES: %.2fs" % t_profile_cl1[-1])
|
|
1260
|
+
print("INFO: Estimated laptime for initial: %.2fs" % t_profile_cl2[-1])
|
|
1261
|
+
print("INFO: Estimated laptime for centerline: %.2fs" % t_profile_cl4[-1])
|
|
1262
|
+
|
|
1263
|
+
if plot=='Y':
|
|
1264
|
+
plt.figure(figsize=(12, 6))
|
|
1265
|
+
plt.subplot(1,2,1)
|
|
1266
|
+
plt.plot(self.reftrack[:,0],self.reftrack[:,1],'b--',label='ref_line')
|
|
1267
|
+
plt.plot(self.bound1[:, 0], self.bound1[:, 1], 'k', label=' Track')
|
|
1268
|
+
plt.plot(self.bound2[:, 0], self.bound2[:, 1], 'k')
|
|
1269
|
+
plt.plot(raceline_interp[:,0], raceline_interp[:,1],'r.-' , label='Opt_ZO')
|
|
1270
|
+
plt.plot(raceline_interp2[:,0], raceline_interp2[:,1],'g-' , label='initial')
|
|
1271
|
+
plt.xlabel('X')
|
|
1272
|
+
plt.ylabel('Y')
|
|
1273
|
+
plt.legend()
|
|
1274
|
+
plt.grid(True)
|
|
1275
|
+
plt.subplot(1,2,2)
|
|
1276
|
+
plt.plot(self.reftrack[:,0],self.reftrack[:,1],'b--',label='ref_line')
|
|
1277
|
+
plt.plot(self.bound1[:, 0], self.bound1[:, 1], 'k', label=' Track')
|
|
1278
|
+
plt.plot(self.bound2[:, 0], self.bound2[:, 1], 'k')
|
|
1279
|
+
plt.plot(raceline_interp1[:,0], raceline_interp1[:,1],'r.-' , label='Opt_CMA')
|
|
1280
|
+
plt.plot(raceline_interp2[:,0], raceline_interp2[:,1],'g-' , label='initial')
|
|
1281
|
+
plt.xlabel('X')
|
|
1282
|
+
plt.ylabel('Y')
|
|
1283
|
+
plt.legend()
|
|
1284
|
+
plt.grid(True)
|
|
1285
|
+
plt.show()
|
|
1286
|
+
|
|
1287
|
+
plt.figure(figsize=(15, 15))
|
|
1288
|
+
plt.subplot(3,4,1)
|
|
1289
|
+
plt.plot(s_splines, vx_profile_opt,'b' , label='v_ZO')
|
|
1290
|
+
plt.plot(s_splines4, vx_profile_opt4,'r' , label='v_center')
|
|
1291
|
+
plt.plot(s_splines2, vx_profile_opt2,'g' , label='v_initial')
|
|
1292
|
+
plt.ylabel('v_profile')
|
|
1293
|
+
plt.xlabel('distance')
|
|
1294
|
+
plt.legend()
|
|
1295
|
+
plt.grid(True)
|
|
1296
|
+
plt.subplot(3,4,2)
|
|
1297
|
+
plt.plot(s_splines, ax_profile_opt,'b' , label='a_ZO')
|
|
1298
|
+
plt.plot(s_splines4, ax_profile_opt4,'r' , label='a_center')
|
|
1299
|
+
plt.plot(s_splines2, ax_profile_opt2,'g' , label='a_initial')
|
|
1300
|
+
plt.ylabel('a_profile')
|
|
1301
|
+
plt.xlabel('distance')
|
|
1302
|
+
plt.legend()
|
|
1303
|
+
plt.grid(True)
|
|
1304
|
+
plt.subplot(3,4,3)
|
|
1305
|
+
plt.plot(s_splines, kappa_opt,'b' , label='k_ZO')
|
|
1306
|
+
plt.plot(s_splines4, kappa_opt4,'r' , label='k_center')
|
|
1307
|
+
plt.plot(s_splines2, kappa_opt2,'g' , label='k_initial')
|
|
1308
|
+
plt.ylabel('curv_profile')
|
|
1309
|
+
plt.xlabel('distance')
|
|
1310
|
+
plt.legend()
|
|
1311
|
+
plt.grid(True)
|
|
1312
|
+
plt.subplot(3,4,4)
|
|
1313
|
+
plt.plot(s_splines, t_profile_cl[:-1],'b' , label='t_ZO')
|
|
1314
|
+
plt.plot(s_splines4, t_profile_cl4[:-1],'r' , label='t_center')
|
|
1315
|
+
plt.plot(s_splines2, t_profile_cl2[:-1],'g' , label='t_initial')
|
|
1316
|
+
plt.ylabel('time')
|
|
1317
|
+
plt.xlabel('distance')
|
|
1318
|
+
plt.legend()
|
|
1319
|
+
plt.grid(True)
|
|
1320
|
+
plt.subplot(3,4,5)
|
|
1321
|
+
plt.plot(s_splines1, vx_profile_opt1,'b' , label='v_cma')
|
|
1322
|
+
plt.plot(s_splines4, vx_profile_opt4,'r' , label='v_center')
|
|
1323
|
+
plt.plot(s_splines2, vx_profile_opt2,'g' , label='v_initial')
|
|
1324
|
+
plt.ylabel('v_profile')
|
|
1325
|
+
plt.xlabel('distance')
|
|
1326
|
+
plt.legend()
|
|
1327
|
+
plt.grid(True)
|
|
1328
|
+
plt.subplot(3,4,6)
|
|
1329
|
+
plt.plot(s_splines1, ax_profile_opt1,'b' , label='a_cma')
|
|
1330
|
+
plt.plot(s_splines4, ax_profile_opt4,'r' , label='a_center')
|
|
1331
|
+
plt.plot(s_splines2, ax_profile_opt2,'g' , label='a_initial')
|
|
1332
|
+
plt.ylabel('a_profile')
|
|
1333
|
+
plt.xlabel('distance')
|
|
1334
|
+
plt.legend()
|
|
1335
|
+
plt.grid(True)
|
|
1336
|
+
plt.subplot(3,4,7)
|
|
1337
|
+
plt.plot(s_splines1, kappa_opt1,'b' , label='k_cma')
|
|
1338
|
+
plt.plot(s_splines4, kappa_opt4,'r' , label='k_center')
|
|
1339
|
+
plt.plot(s_splines2, kappa_opt2,'g' , label='k_initial')
|
|
1340
|
+
plt.ylabel('curv_profile')
|
|
1341
|
+
plt.xlabel('distance')
|
|
1342
|
+
plt.legend()
|
|
1343
|
+
plt.grid(True)
|
|
1344
|
+
plt.subplot(3,4,8)
|
|
1345
|
+
plt.plot(s_splines1, t_profile_cl1[:-1],'b' , label='t_cma')
|
|
1346
|
+
plt.plot(s_splines4, t_profile_cl4[:-1],'r' , label='t_center')
|
|
1347
|
+
plt.plot(s_splines2, t_profile_cl2[:-1],'g' , label='t_initial')
|
|
1348
|
+
plt.ylabel('time')
|
|
1349
|
+
plt.xlabel('distance')
|
|
1350
|
+
plt.legend()
|
|
1351
|
+
plt.grid(True)
|
|
1352
|
+
plt.subplot(3,4,9)
|
|
1353
|
+
plt.plot(s_splines2, vx_profile_opt2,'b' , label='v_initial')
|
|
1354
|
+
plt.plot(s_splines4, vx_profile_opt4,'r' , label='v_center')
|
|
1355
|
+
plt.ylabel('v_profile')
|
|
1356
|
+
plt.xlabel('distance')
|
|
1357
|
+
plt.legend()
|
|
1358
|
+
plt.grid(True)
|
|
1359
|
+
plt.subplot(3,4,10)
|
|
1360
|
+
plt.plot(s_splines2, ax_profile_opt2,'b' , label='a_initial')
|
|
1361
|
+
plt.plot(s_splines4, ax_profile_opt4,'r' , label='a_center')
|
|
1362
|
+
plt.ylabel('a_profile')
|
|
1363
|
+
plt.xlabel('distance')
|
|
1364
|
+
plt.legend()
|
|
1365
|
+
plt.grid(True)
|
|
1366
|
+
plt.subplot(3,4,11)
|
|
1367
|
+
plt.plot(s_splines2, kappa_opt2,'b' , label='k_initial')
|
|
1368
|
+
plt.plot(s_splines4, kappa_opt4,'r' , label='k_center')
|
|
1369
|
+
plt.ylabel('curv_profile')
|
|
1370
|
+
plt.xlabel('distance')
|
|
1371
|
+
plt.legend()
|
|
1372
|
+
plt.grid(True)
|
|
1373
|
+
plt.subplot(3,4,12)
|
|
1374
|
+
plt.plot(s_splines2, t_profile_cl2[:-1],'b' , label='t_initial')
|
|
1375
|
+
plt.plot(s_splines4, t_profile_cl4[:-1],'r' , label='t_center')
|
|
1376
|
+
plt.ylabel('time')
|
|
1377
|
+
plt.xlabel('distance')
|
|
1378
|
+
plt.legend()
|
|
1379
|
+
plt.grid(True)
|
|
1380
|
+
plt.show()
|
|
1381
|
+
|
|
1382
|
+
# Always restore original track when Comparison exits
|
|
1383
|
+
self._update_reftrack(self._base_reftrack.copy())
|
|
1384
|
+
|
|
1385
|
+
if output == 'Y':
|
|
1386
|
+
return s_splines, vx_profile_opt, ax_profile_opt, kappa_opt, t_profile_cl, raceline_interp,\
|
|
1387
|
+
s_splines1, vx_profile_opt1, ax_profile_opt1, kappa_opt1, t_profile_cl1, raceline_interp1,\
|
|
1388
|
+
s_splines2, vx_profile_opt2, ax_profile_opt2, kappa_opt2, t_profile_cl2, raceline_interp2,\
|
|
1389
|
+
s_splines4, vx_profile_opt4, ax_profile_opt4, kappa_opt4, t_profile_cl4, raceline_interp4
|
|
1390
|
+
if output == 'ZO':
|
|
1391
|
+
return s_splines, vx_profile_opt, ax_profile_opt, kappa_opt, t_profile_cl, raceline_interp
|
|
1392
|
+
if output == 'CMA':
|
|
1393
|
+
return s_splines1, vx_profile_opt1, ax_profile_opt1, kappa_opt1, t_profile_cl1, raceline_interp1
|
|
1394
|
+
if output == 'initial':
|
|
1395
|
+
return s_splines2, vx_profile_opt2, ax_profile_opt2, kappa_opt2, t_profile_cl2, raceline_interp2
|
|
1396
|
+
if output == 'center':
|
|
1397
|
+
return s_splines4, vx_profile_opt4, ax_profile_opt4, kappa_opt4, t_profile_cl4, raceline_interp4
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
# ===========================================================================
|
|
1401
|
+
# Blackbox_raceline
|
|
1402
|
+
# ===========================================================================
|
|
1403
|
+
|
|
1404
|
+
class Blackbox_raceline:
|
|
1405
|
+
"""
|
|
1406
|
+
Direct zeroth-order (ZO) optimization of the lateral shift vector alpha
|
|
1407
|
+
for minimum lap time (or a user-supplied blackbox cost function).
|
|
1408
|
+
|
|
1409
|
+
Motivation
|
|
1410
|
+
----------
|
|
1411
|
+
The existing ``Opt_min_CurvTime`` class uses a two-stage pipeline:
|
|
1412
|
+
ZO/CMA-ES optimizes spline segment lengths ds → QP solves for alpha that
|
|
1413
|
+
minimizes *curvature* (a proxy for lap time). The curvature proxy is
|
|
1414
|
+
inexact and couples geometry to the inner QP.
|
|
1415
|
+
|
|
1416
|
+
``Blackbox_raceline`` bypasses both stages. Alpha is the optimization
|
|
1417
|
+
variable directly and the true lap time (from ``KinematicProfs``) is the
|
|
1418
|
+
cost function, so there is no proxy and no inner QP.
|
|
1419
|
+
|
|
1420
|
+
Formulation
|
|
1421
|
+
-----------
|
|
1422
|
+
* Variable : ``alpha`` ∈ R^N, one lateral shift per control point
|
|
1423
|
+
* Feasible set: box ``alpha[i] ∈ [-(w_left[i] - sfty), (w_right[i] - sfty)]``
|
|
1424
|
+
* Objective : ``minimize cost(alpha)`` [default: lap time]
|
|
1425
|
+
* Method : projected ZO stochastic gradient descent
|
|
1426
|
+
|
|
1427
|
+
The box constraint is the same as the QP constraint used inside
|
|
1428
|
+
``Opt_min_CurvTime``, so the feasible region is identical.
|
|
1429
|
+
|
|
1430
|
+
Fixed normal vectors
|
|
1431
|
+
--------------------
|
|
1432
|
+
Spline normal vectors are computed once from the Euclidean segment lengths
|
|
1433
|
+
of the initial ``reftrack`` and held fixed throughout the run. Alpha is
|
|
1434
|
+
interpreted as displacement along these fixed normals, which keeps the
|
|
1435
|
+
box-projection exact and avoids recomputing splines at every iteration.
|
|
1436
|
+
|
|
1437
|
+
Gradient estimation
|
|
1438
|
+
-------------------
|
|
1439
|
+
Two oracles are supported for sampling the random direction u:
|
|
1440
|
+
|
|
1441
|
+
* ``'gaussian'`` : u ~ N(0, I_N) — standard Gaussian ZO estimator
|
|
1442
|
+
* ``'sphere'`` : u ~ Uniform(S^{N-1}) — normalized Gaussian; trades
|
|
1443
|
+
higher variance for uniform coverage of the search space
|
|
1444
|
+
|
|
1445
|
+
Two finite-difference schemes are supported:
|
|
1446
|
+
|
|
1447
|
+
* ``'noth'`` (forward) : ``g = (f(alpha + μu) - f(alpha)) / μ * u``
|
|
1448
|
+
— 1 extra function evaluation per direction
|
|
1449
|
+
* ``'h'`` (central) : ``g = (f(alpha + μu) - f(alpha - μu)) / 2μ * u``
|
|
1450
|
+
— 2 extra evaluations per direction, lower bias
|
|
1451
|
+
|
|
1452
|
+
Parameters
|
|
1453
|
+
----------
|
|
1454
|
+
reftrack : np.ndarray, shape (N, 4)
|
|
1455
|
+
Reference track [x, y, w_right, w_left]. Unclosed.
|
|
1456
|
+
ggv : np.ndarray
|
|
1457
|
+
GGV table already loaded, shape (K, 3) → [vx, ax_max, ay_max].
|
|
1458
|
+
ax_max_machines : np.ndarray
|
|
1459
|
+
Machine longitudinal limits, shape (L, 2) → [vx, ax_max].
|
|
1460
|
+
sfty : float
|
|
1461
|
+
Vehicle safety half-width in m (box constraint clearance).
|
|
1462
|
+
v_max : float
|
|
1463
|
+
Maximum velocity [m/s].
|
|
1464
|
+
si : float
|
|
1465
|
+
Raceline interpolation stepsize [m].
|
|
1466
|
+
fw : int or None
|
|
1467
|
+
Velocity profile convolution filter window length. None = no filter.
|
|
1468
|
+
m_veh : float
|
|
1469
|
+
Vehicle mass [kg].
|
|
1470
|
+
drag_coeff : float
|
|
1471
|
+
Drag coefficient: 0.5 * c_w * A_front * rho_air.
|
|
1472
|
+
dyn_model_exp : float
|
|
1473
|
+
Dynamics model exponent in [1.0, 2.0].
|
|
1474
|
+
cost_fn : callable or None
|
|
1475
|
+
User-supplied cost function ``cost_fn(alpha) -> float``.
|
|
1476
|
+
If None the default lap-time oracle is used.
|
|
1477
|
+
init : str
|
|
1478
|
+
Initial alpha strategy.
|
|
1479
|
+
|
|
1480
|
+
``'random'`` — alpha_0 sampled uniformly inside the feasible box.
|
|
1481
|
+
``'mincurv'`` — alpha_0 = QP minimum-curvature solution on reftrack
|
|
1482
|
+
(warm start from the best geometric proxy).
|
|
1483
|
+
oracle : str
|
|
1484
|
+
``'gaussian'`` or ``'sphere'`` (see above).
|
|
1485
|
+
mu : float
|
|
1486
|
+
ZO smoothing / perturbation magnitude μ.
|
|
1487
|
+
h : float
|
|
1488
|
+
Gradient-descent step size.
|
|
1489
|
+
t : int
|
|
1490
|
+
Number of random directions averaged per gradient estimate.
|
|
1491
|
+
grad_type : str
|
|
1492
|
+
``'noth'`` (forward difference) or ``'h'`` (central difference).
|
|
1493
|
+
iterations : int
|
|
1494
|
+
Total number of gradient steps.
|
|
1495
|
+
kappa_bound : float
|
|
1496
|
+
Curvature bound used for the QP when ``init='mincurv'``.
|
|
1497
|
+
seed : int or None
|
|
1498
|
+
Global random seed set at construction time. Reapply with
|
|
1499
|
+
``find_alpha(seed=...)`` for per-run reproducibility.
|
|
1500
|
+
"""
|
|
1501
|
+
|
|
1502
|
+
def __init__(
|
|
1503
|
+
self,
|
|
1504
|
+
reftrack: np.ndarray,
|
|
1505
|
+
ggv: np.ndarray,
|
|
1506
|
+
ax_max_machines: np.ndarray,
|
|
1507
|
+
sfty: float = 1.0,
|
|
1508
|
+
v_max: float = 22.88,
|
|
1509
|
+
si: float = 0.8,
|
|
1510
|
+
fw: int = 3,
|
|
1511
|
+
m_veh: float = 1000.0,
|
|
1512
|
+
drag_coeff: float = 0.0,
|
|
1513
|
+
dyn_model_exp: float = 1.0,
|
|
1514
|
+
cost_fn=None,
|
|
1515
|
+
init: str = 'random',
|
|
1516
|
+
oracle: str = 'gaussian',
|
|
1517
|
+
mu: float = 0.05,
|
|
1518
|
+
h: float = 0.001,
|
|
1519
|
+
t: int = 1,
|
|
1520
|
+
grad_type: str = 'noth',
|
|
1521
|
+
iterations: int = 300,
|
|
1522
|
+
kappa_bound: float = 0.7,
|
|
1523
|
+
seed: int = None,
|
|
1524
|
+
):
|
|
1525
|
+
# ---- store hyper-parameters -------------------------------------------
|
|
1526
|
+
self.reftrack = reftrack.copy()
|
|
1527
|
+
self.N = reftrack.shape[0]
|
|
1528
|
+
self.ggv = ggv
|
|
1529
|
+
self.ax_max_machines = ax_max_machines
|
|
1530
|
+
self.sfty = sfty
|
|
1531
|
+
self.v_max = v_max
|
|
1532
|
+
self.si = si
|
|
1533
|
+
self.fw = fw
|
|
1534
|
+
self.m_veh = m_veh
|
|
1535
|
+
self.drag_coeff = drag_coeff
|
|
1536
|
+
self.dyn_model_exp = dyn_model_exp
|
|
1537
|
+
self.cost_fn = cost_fn
|
|
1538
|
+
self.init = init
|
|
1539
|
+
self.oracle = oracle
|
|
1540
|
+
self.mu = mu
|
|
1541
|
+
self.h = h
|
|
1542
|
+
self.t = t
|
|
1543
|
+
self.grad_type = grad_type
|
|
1544
|
+
self.iterations = iterations
|
|
1545
|
+
self.kappa_bound = kappa_bound
|
|
1546
|
+
|
|
1547
|
+
if seed is not None:
|
|
1548
|
+
np.random.seed(seed)
|
|
1549
|
+
|
|
1550
|
+
# ---- box constraints (same convention as QP inside Opt_min_CurvTime) --
|
|
1551
|
+
# positive alpha → shift toward the right boundary (w_right side)
|
|
1552
|
+
# negative alpha → shift toward the left boundary (w_left side)
|
|
1553
|
+
self.lo = -(reftrack[:, 3] - sfty) # lower bound (max left shift)
|
|
1554
|
+
self.hi = reftrack[:, 2] - sfty # upper bound (max right shift)
|
|
1555
|
+
|
|
1556
|
+
bad = np.where(self.lo > self.hi)[0]
|
|
1557
|
+
if bad.size > 0:
|
|
1558
|
+
raise ValueError(
|
|
1559
|
+
f"Track is narrower than the safety margin at {bad.size} point(s) "
|
|
1560
|
+
f"(indices: {bad[:5]}{'...' if bad.size > 5 else ''}). "
|
|
1561
|
+
"Reduce sfty or check track widths.")
|
|
1562
|
+
|
|
1563
|
+
# ---- fixed normal vectors (computed once from initial Euclidean ds) ----
|
|
1564
|
+
ds_init = np.hypot(
|
|
1565
|
+
np.diff(np.r_[reftrack[:, 0], reftrack[0, 0]]),
|
|
1566
|
+
np.diff(np.r_[reftrack[:, 1], reftrack[0, 1]]))
|
|
1567
|
+
_, _, self._M, self._normvec = calc_splines(
|
|
1568
|
+
path=np.vstack((reftrack[:, :2], reftrack[0, :2])),
|
|
1569
|
+
el_lengths=ds_init,
|
|
1570
|
+
use_dist_scaling=True)
|
|
1571
|
+
|
|
1572
|
+
# ---- p_ggv cache (keyed by interpolated raceline length) ---------------
|
|
1573
|
+
self._p_ggv_cache: dict = {}
|
|
1574
|
+
|
|
1575
|
+
# ---- initial alpha + result placeholders --------------------------------
|
|
1576
|
+
self.alpha_0 = self._init_alpha()
|
|
1577
|
+
self.alpha_opt = self.alpha_0.copy()
|
|
1578
|
+
self.history = None
|
|
1579
|
+
|
|
1580
|
+
# ------------------------------------------------------------------
|
|
1581
|
+
# Private helpers
|
|
1582
|
+
# ------------------------------------------------------------------
|
|
1583
|
+
|
|
1584
|
+
def _init_alpha(self) -> np.ndarray:
|
|
1585
|
+
"""Return the initial alpha vector according to self.init.
|
|
1586
|
+
|
|
1587
|
+
Supported modes
|
|
1588
|
+
---------------
|
|
1589
|
+
'zero'
|
|
1590
|
+
Start with zero shift (alpha = 0), i.e. the original reference track.
|
|
1591
|
+
'random'
|
|
1592
|
+
Uniform sample inside the feasible box [lo, hi].
|
|
1593
|
+
'mincurv'
|
|
1594
|
+
Solve the minimum-curvature QP (H_f) and use the result as the
|
|
1595
|
+
warm-start. This is the best geometric proxy for lap-time and
|
|
1596
|
+
typically the fastest starting point for further ZO/CMA refinement.
|
|
1597
|
+
'shortest' (aliases: 'shortest_path', 'sp')
|
|
1598
|
+
Solve the Optimal Shortest Path (OSP) QP and use that alpha.
|
|
1599
|
+
The shortest path has maximum corner radii (less lateral movement
|
|
1600
|
+
overall) which sometimes beats mincurv as a starting point on
|
|
1601
|
+
tracks where sustained high speed on long straights matters more
|
|
1602
|
+
than corner apex curvature.
|
|
1603
|
+
"""
|
|
1604
|
+
if self.init == 'random':
|
|
1605
|
+
return np.random.uniform(self.lo, self.hi)
|
|
1606
|
+
elif self.init == 'zero':
|
|
1607
|
+
return np.zeros(self.N)
|
|
1608
|
+
|
|
1609
|
+
elif self.init == 'mincurv':
|
|
1610
|
+
# Warm-start from the QP min-curvature solution on the original reftrack.
|
|
1611
|
+
# This is the best geometric proxy available and typically a better starting
|
|
1612
|
+
# point than a random interior point.
|
|
1613
|
+
H, f_vec, G, h_vec = H_f(
|
|
1614
|
+
reftrack=self.reftrack,
|
|
1615
|
+
normvectors=self._normvec,
|
|
1616
|
+
A=self._M,
|
|
1617
|
+
kappa_bound=self.kappa_bound,
|
|
1618
|
+
w_veh=self.sfty,
|
|
1619
|
+
closed=True)
|
|
1620
|
+
alpha_mc = quadprog.solve_qp(H, -f_vec, -G.T, -h_vec, 0)[0]
|
|
1621
|
+
# Clip in case QP and Blackbox box constraints differ by a rounding epsilon
|
|
1622
|
+
return np.clip(alpha_mc, self.lo, self.hi)
|
|
1623
|
+
|
|
1624
|
+
elif self.init in ('shortest', 'shortest_path', 'sp'):
|
|
1625
|
+
# Warm-start from the OSP (Optimal Shortest Path) QP solution.
|
|
1626
|
+
# OSP internally uses w_veh/2 as the per-side clearance; we pass
|
|
1627
|
+
# 2*sfty so that w_veh/2 = sfty, giving the same feasible box as
|
|
1628
|
+
# Blackbox_raceline uses for the gradient descent.
|
|
1629
|
+
H_sp, f_sp, G_sp, h_sp = OSP(
|
|
1630
|
+
reftrack=self.reftrack,
|
|
1631
|
+
normvectors=self._normvec,
|
|
1632
|
+
w_veh=2.0 * self.sfty)
|
|
1633
|
+
alpha_sp = quadprog.solve_qp(H_sp, -f_sp, -G_sp.T, -h_sp, 0)[0]
|
|
1634
|
+
# Clip to Blackbox box (OSP and H_f constraints can differ slightly
|
|
1635
|
+
# due to numerical tolerances or rounding in dev_max).
|
|
1636
|
+
return np.clip(alpha_sp, self.lo, self.hi)
|
|
1637
|
+
|
|
1638
|
+
else:
|
|
1639
|
+
raise ValueError(
|
|
1640
|
+
f"Unknown init mode '{self.init}'. "
|
|
1641
|
+
"Use 'random', 'mincurv', or 'shortest'.")
|
|
1642
|
+
|
|
1643
|
+
def _sample_direction(self) -> np.ndarray:
|
|
1644
|
+
"""Sample a unit-scaled random direction u according to self.oracle."""
|
|
1645
|
+
u = np.random.normal(0.0, 1.0, size=self.N)
|
|
1646
|
+
if self.oracle == 'sphere':
|
|
1647
|
+
norm = np.linalg.norm(u)
|
|
1648
|
+
if norm > 1e-12:
|
|
1649
|
+
u /= norm # project onto N-sphere
|
|
1650
|
+
elif self.oracle != 'gaussian':
|
|
1651
|
+
raise ValueError(
|
|
1652
|
+
f"Unknown oracle '{self.oracle}'. Use 'gaussian' or 'sphere'.")
|
|
1653
|
+
return u
|
|
1654
|
+
|
|
1655
|
+
def _eval_cost(self, alpha: np.ndarray) -> float:
|
|
1656
|
+
"""Evaluate cost at alpha (dispatch to user cost_fn or lap-time oracle)."""
|
|
1657
|
+
if self.cost_fn is not None:
|
|
1658
|
+
return float(self.cost_fn(alpha))
|
|
1659
|
+
return self._laptime_cost(alpha)
|
|
1660
|
+
|
|
1661
|
+
def _laptime_cost(self, alpha: np.ndarray) -> float:
|
|
1662
|
+
"""
|
|
1663
|
+
Default cost oracle: build raceline from alpha and return the lap time
|
|
1664
|
+
computed by the KinematicProfs forward-backward velocity solver.
|
|
1665
|
+
"""
|
|
1666
|
+
_, _, cx, cy, si_idx, tv, _, _, el = create_raceline(
|
|
1667
|
+
refline=self.reftrack[:, :2],
|
|
1668
|
+
normvectors=self._normvec,
|
|
1669
|
+
alpha=alpha,
|
|
1670
|
+
stepsize_interp=self.si)
|
|
1671
|
+
_, kappa = calc_head_curv_an(
|
|
1672
|
+
coeffs_x=cx, coeffs_y=cy, ind_spls=si_idx, t_spls=tv)
|
|
1673
|
+
|
|
1674
|
+
n = kappa.size
|
|
1675
|
+
if n not in self._p_ggv_cache:
|
|
1676
|
+
self._p_ggv_cache[n] = np.repeat(
|
|
1677
|
+
np.expand_dims(self.ggv, axis=0), n, axis=0)
|
|
1678
|
+
|
|
1679
|
+
vx = calc_vel_profile(
|
|
1680
|
+
ggv=self.ggv,
|
|
1681
|
+
ax_max_machines=self.ax_max_machines,
|
|
1682
|
+
v_max=self.v_max,
|
|
1683
|
+
kappa=kappa,
|
|
1684
|
+
el_lengths=el,
|
|
1685
|
+
closed=True,
|
|
1686
|
+
filt_window=self.fw,
|
|
1687
|
+
dyn_model_exp=self.dyn_model_exp,
|
|
1688
|
+
drag_coeff=self.drag_coeff,
|
|
1689
|
+
m_veh=self.m_veh,
|
|
1690
|
+
p_ggv=self._p_ggv_cache[n])
|
|
1691
|
+
|
|
1692
|
+
vx_cl = np.append(vx, vx[0])
|
|
1693
|
+
ax_prof = calc_ax_profile(
|
|
1694
|
+
vx_profile=vx_cl, el_lengths=el, eq_length_output=False)
|
|
1695
|
+
t_prof = calc_t_profile(
|
|
1696
|
+
vx_profile=vx, ax_profile=ax_prof, el_lengths=el)
|
|
1697
|
+
return float(t_prof[-1])
|
|
1698
|
+
|
|
1699
|
+
# ------------------------------------------------------------------
|
|
1700
|
+
# Public API
|
|
1701
|
+
# ------------------------------------------------------------------
|
|
1702
|
+
|
|
1703
|
+
def find_alpha(
|
|
1704
|
+
self,
|
|
1705
|
+
solver: str = 'ZO',
|
|
1706
|
+
n_iter: int = None,
|
|
1707
|
+
# ZO parameters
|
|
1708
|
+
step_size: float = None,
|
|
1709
|
+
mu: float = None,
|
|
1710
|
+
t: int = None,
|
|
1711
|
+
grad_type: str = None,
|
|
1712
|
+
# CMA-ES parameters
|
|
1713
|
+
sigma: float = None,
|
|
1714
|
+
popsize: int = None,
|
|
1715
|
+
# Common
|
|
1716
|
+
seed: int = None,
|
|
1717
|
+
verbose: bool = True,
|
|
1718
|
+
print_every: int = 50,
|
|
1719
|
+
):
|
|
1720
|
+
"""
|
|
1721
|
+
Minimize the cost over alpha using either ZO gradient descent or CMA-ES.
|
|
1722
|
+
|
|
1723
|
+
Both solvers respect the box constraint ``alpha ∈ [lo, hi]`` via
|
|
1724
|
+
projection (ZO) or clipping of the sampled population (CMA-ES).
|
|
1725
|
+
The best alpha seen across all evaluations is tracked and stored in
|
|
1726
|
+
``self.alpha_opt``; ``self.history`` records the best-so-far cost at
|
|
1727
|
+
the end of each iteration/generation.
|
|
1728
|
+
|
|
1729
|
+
Parameters
|
|
1730
|
+
----------
|
|
1731
|
+
solver : str
|
|
1732
|
+
``'ZO'`` — projected zeroth-order stochastic gradient descent.
|
|
1733
|
+
``'CMA'`` — CMA-ES evolutionary strategy.
|
|
1734
|
+
n_iter : int or None
|
|
1735
|
+
For ZO : number of gradient steps (default: ``self.iterations``).
|
|
1736
|
+
For CMA : number of generations (default: ``self.iterations``).
|
|
1737
|
+
One CMA generation = ``popsize`` function evaluations.
|
|
1738
|
+
step_size : float or None [ZO only]
|
|
1739
|
+
Gradient step size h. Defaults to ``self.h``.
|
|
1740
|
+
mu : float or None [ZO only]
|
|
1741
|
+
Perturbation magnitude μ. Defaults to ``self.mu``.
|
|
1742
|
+
t : int or None [ZO only]
|
|
1743
|
+
Directions averaged per gradient estimate. Defaults to ``self.t``.
|
|
1744
|
+
grad_type : str or None [ZO only]
|
|
1745
|
+
``'noth'`` (forward difference) or ``'h'`` (central difference).
|
|
1746
|
+
Defaults to ``self.grad_type``.
|
|
1747
|
+
sigma : float or None [CMA only]
|
|
1748
|
+
Initial step-size (standard deviation) for CMA-ES.
|
|
1749
|
+
A good default is ~(1/3) × (hi - lo) range, but it is
|
|
1750
|
+
problem-specific. Defaults to ``self.h`` (used as a scale proxy).
|
|
1751
|
+
popsize : int or None [CMA only]
|
|
1752
|
+
Population size (number of candidate solutions per generation).
|
|
1753
|
+
Defaults to ``4 + int(3 * np.log(self.N))`` (CMA-ES heuristic).
|
|
1754
|
+
seed : int or None
|
|
1755
|
+
Random seed reset before the loop.
|
|
1756
|
+
verbose : bool
|
|
1757
|
+
Print progress every ``print_every`` iterations/generations.
|
|
1758
|
+
print_every : int
|
|
1759
|
+
Verbosity interval.
|
|
1760
|
+
|
|
1761
|
+
Returns
|
|
1762
|
+
-------
|
|
1763
|
+
best_alpha : np.ndarray, shape (N,)
|
|
1764
|
+
Alpha that achieved the lowest observed cost.
|
|
1765
|
+
history : np.ndarray, shape (n_iter + 1,)
|
|
1766
|
+
``history[0]`` = cost of the initial alpha_0.
|
|
1767
|
+
``history[k]`` = best-so-far cost after iteration/generation k.
|
|
1768
|
+
"""
|
|
1769
|
+
if seed is not None:
|
|
1770
|
+
np.random.seed(seed)
|
|
1771
|
+
|
|
1772
|
+
n_iter = n_iter if n_iter is not None else self.iterations
|
|
1773
|
+
|
|
1774
|
+
# ---- evaluate starting point (common to both solvers) ------------------
|
|
1775
|
+
alpha_start = self.alpha_0.copy()
|
|
1776
|
+
f_start = self._eval_cost(alpha_start)
|
|
1777
|
+
best_alpha = alpha_start.copy()
|
|
1778
|
+
best_cost = f_start
|
|
1779
|
+
history = np.full(n_iter + 1, np.nan)
|
|
1780
|
+
history[0] = f_start
|
|
1781
|
+
|
|
1782
|
+
if verbose:
|
|
1783
|
+
print(f"[Blackbox_raceline/{solver}] init laptime = {f_start:.4f} s"
|
|
1784
|
+
f" (init='{self.init}', N={self.N})")
|
|
1785
|
+
|
|
1786
|
+
# ====================================================================
|
|
1787
|
+
# ZO — projected stochastic gradient descent
|
|
1788
|
+
# ====================================================================
|
|
1789
|
+
if solver == 'ZO':
|
|
1790
|
+
step_size = step_size if step_size is not None else self.h
|
|
1791
|
+
mu = mu if mu is not None else self.mu
|
|
1792
|
+
t_dirs = t if t is not None else self.t
|
|
1793
|
+
grad_type = grad_type if grad_type is not None else self.grad_type
|
|
1794
|
+
|
|
1795
|
+
alpha = alpha_start.copy()
|
|
1796
|
+
f_curr = f_start
|
|
1797
|
+
|
|
1798
|
+
for k in range(n_iter):
|
|
1799
|
+
|
|
1800
|
+
# gradient estimate averaged over t_dirs random directions
|
|
1801
|
+
grad_sum = np.zeros(self.N)
|
|
1802
|
+
for _ in range(t_dirs):
|
|
1803
|
+
u = self._sample_direction()
|
|
1804
|
+
|
|
1805
|
+
alpha_fwd = np.clip(alpha + mu * u, self.lo, self.hi)
|
|
1806
|
+
f_fwd = self._eval_cost(alpha_fwd)
|
|
1807
|
+
|
|
1808
|
+
if grad_type == 'noth':
|
|
1809
|
+
g = (f_fwd - f_curr) / mu * u
|
|
1810
|
+
else: # 'h' = central difference
|
|
1811
|
+
alpha_bwd = np.clip(alpha - mu * u, self.lo, self.hi)
|
|
1812
|
+
f_bwd = self._eval_cost(alpha_bwd)
|
|
1813
|
+
g = (f_fwd - f_bwd) / (2.0 * mu) * u
|
|
1814
|
+
|
|
1815
|
+
grad_sum += g
|
|
1816
|
+
|
|
1817
|
+
grad_est = grad_sum / t_dirs
|
|
1818
|
+
|
|
1819
|
+
# projected gradient step
|
|
1820
|
+
alpha = np.clip(alpha - step_size * grad_est, self.lo, self.hi)
|
|
1821
|
+
f_curr = self._eval_cost(alpha)
|
|
1822
|
+
|
|
1823
|
+
if f_curr < best_cost:
|
|
1824
|
+
best_cost = f_curr
|
|
1825
|
+
best_alpha = alpha.copy()
|
|
1826
|
+
history[k + 1] = best_cost
|
|
1827
|
+
|
|
1828
|
+
if verbose and (k + 1) % print_every == 0:
|
|
1829
|
+
print(f" iter {k + 1:4d}/{n_iter}: laptime = {f_curr:.4f} s"
|
|
1830
|
+
f" best = {best_cost:.4f} s")
|
|
1831
|
+
|
|
1832
|
+
# ====================================================================
|
|
1833
|
+
# CMA-ES — covariance matrix adaptation evolution strategy
|
|
1834
|
+
# ====================================================================
|
|
1835
|
+
elif solver == 'CMA':
|
|
1836
|
+
# Default sigma: 1/6 of the mean box half-width (explores ~1σ
|
|
1837
|
+
# within the feasible range with high probability)
|
|
1838
|
+
if sigma is None:
|
|
1839
|
+
sigma = float(np.mean(self.hi - self.lo)) / 6.0
|
|
1840
|
+
if popsize is None:
|
|
1841
|
+
popsize = 4 + int(3 * np.log(self.N)) # CMA-ES standard heuristic
|
|
1842
|
+
|
|
1843
|
+
# Wrap _eval_cost to track best-ever across all individual evaluations
|
|
1844
|
+
_best = {'alpha': best_alpha.copy(), 'cost': best_cost}
|
|
1845
|
+
|
|
1846
|
+
def _tracked_cost(a: np.ndarray) -> float:
|
|
1847
|
+
c = self._eval_cost(a)
|
|
1848
|
+
if c < _best['cost']:
|
|
1849
|
+
_best['cost'] = c
|
|
1850
|
+
_best['alpha'] = a.copy()
|
|
1851
|
+
return c
|
|
1852
|
+
|
|
1853
|
+
cma = ConstrainedCMAES_t(
|
|
1854
|
+
_tracked_cost,
|
|
1855
|
+
mean=alpha_start.copy(),
|
|
1856
|
+
sigma=sigma,
|
|
1857
|
+
popsize=popsize,
|
|
1858
|
+
bounds1=self.hi,
|
|
1859
|
+
bounds2=self.lo)
|
|
1860
|
+
# ConstrainedCMAES_t.update() references self.iterations (normally
|
|
1861
|
+
# set inside optimize()). We manage the loop ourselves for
|
|
1862
|
+
# best-tracking, so we must initialise it here.
|
|
1863
|
+
cma.iterations = 0
|
|
1864
|
+
|
|
1865
|
+
for gen in range(n_iter):
|
|
1866
|
+
samples = cma.sample_population()
|
|
1867
|
+
fitness = np.array([cma.objective_function(s) for s in samples])
|
|
1868
|
+
cma.update(samples, fitness)
|
|
1869
|
+
cma.iterations += 1
|
|
1870
|
+
history[gen + 1] = _best['cost'] # best-so-far after this generation
|
|
1871
|
+
|
|
1872
|
+
if verbose and (gen + 1) % print_every == 0:
|
|
1873
|
+
print(f" gen {gen + 1:4d}/{n_iter}: gen_best = {min(fitness):.4f} s"
|
|
1874
|
+
f" best = {_best['cost']:.4f} s")
|
|
1875
|
+
|
|
1876
|
+
best_alpha = _best['alpha']
|
|
1877
|
+
best_cost = _best['cost']
|
|
1878
|
+
|
|
1879
|
+
else:
|
|
1880
|
+
raise ValueError(f"Unknown solver '{solver}'. Use 'ZO' or 'CMA'.")
|
|
1881
|
+
|
|
1882
|
+
# ---- finalise ----------------------------------------------------------
|
|
1883
|
+
if verbose:
|
|
1884
|
+
print(f"[Blackbox_raceline/{solver}] done best laptime = {best_cost:.4f} s"
|
|
1885
|
+
f" (improvement = {f_start - best_cost:.4f} s)")
|
|
1886
|
+
|
|
1887
|
+
self.alpha_opt = best_alpha
|
|
1888
|
+
self.history = history
|
|
1889
|
+
return best_alpha, history
|
|
1890
|
+
|
|
1891
|
+
def generate_raceline(self, alpha: np.ndarray = None):
|
|
1892
|
+
"""
|
|
1893
|
+
Build the full raceline and kinematic profiles for a given alpha.
|
|
1894
|
+
|
|
1895
|
+
Parameters
|
|
1896
|
+
----------
|
|
1897
|
+
alpha : np.ndarray or None
|
|
1898
|
+
Lateral shift vector, shape (N,). If None, uses self.alpha_opt
|
|
1899
|
+
(the best alpha from the most recent find_alpha() call).
|
|
1900
|
+
|
|
1901
|
+
Returns
|
|
1902
|
+
-------
|
|
1903
|
+
s : np.ndarray
|
|
1904
|
+
Cumulative distance along the raceline [m], shape (M,).
|
|
1905
|
+
vx : np.ndarray
|
|
1906
|
+
Velocity profile [m/s], shape (M,) (unclosed).
|
|
1907
|
+
ax : np.ndarray
|
|
1908
|
+
Longitudinal acceleration [m/s²], shape (M,) (closed wrap-around).
|
|
1909
|
+
kappa : np.ndarray
|
|
1910
|
+
Curvature [rad/m], shape (M,).
|
|
1911
|
+
t_prof : np.ndarray
|
|
1912
|
+
Cumulative lap time [s], shape (M + 1,). t_prof[-1] = total lap time.
|
|
1913
|
+
raceline_xy : np.ndarray
|
|
1914
|
+
Interpolated raceline coordinates [x, y], shape (M, 2).
|
|
1915
|
+
"""
|
|
1916
|
+
if alpha is None:
|
|
1917
|
+
if self.alpha_opt is None:
|
|
1918
|
+
raise RuntimeError(
|
|
1919
|
+
"No alpha available. Run find_alpha() first or pass alpha explicitly.")
|
|
1920
|
+
alpha = self.alpha_opt
|
|
1921
|
+
|
|
1922
|
+
raceline_xy, _, cx, cy, si_idx, tv, _, _, el = create_raceline(
|
|
1923
|
+
refline=self.reftrack[:, :2],
|
|
1924
|
+
normvectors=self._normvec,
|
|
1925
|
+
alpha=alpha,
|
|
1926
|
+
stepsize_interp=self.si)
|
|
1927
|
+
_, kappa = calc_head_curv_an(
|
|
1928
|
+
coeffs_x=cx, coeffs_y=cy, ind_spls=si_idx, t_spls=tv)
|
|
1929
|
+
|
|
1930
|
+
s = cumulative_distances(el)
|
|
1931
|
+
|
|
1932
|
+
n = kappa.size
|
|
1933
|
+
if n not in self._p_ggv_cache:
|
|
1934
|
+
self._p_ggv_cache[n] = np.repeat(
|
|
1935
|
+
np.expand_dims(self.ggv, axis=0), n, axis=0)
|
|
1936
|
+
|
|
1937
|
+
vx = calc_vel_profile(
|
|
1938
|
+
ggv=self.ggv,
|
|
1939
|
+
ax_max_machines=self.ax_max_machines,
|
|
1940
|
+
v_max=self.v_max,
|
|
1941
|
+
kappa=kappa,
|
|
1942
|
+
el_lengths=el,
|
|
1943
|
+
closed=True,
|
|
1944
|
+
filt_window=self.fw,
|
|
1945
|
+
dyn_model_exp=self.dyn_model_exp,
|
|
1946
|
+
drag_coeff=self.drag_coeff,
|
|
1947
|
+
m_veh=self.m_veh,
|
|
1948
|
+
p_ggv=self._p_ggv_cache[n])
|
|
1949
|
+
|
|
1950
|
+
vx_cl = np.append(vx, vx[0])
|
|
1951
|
+
ax = calc_ax_profile(
|
|
1952
|
+
vx_profile=vx_cl, el_lengths=el, eq_length_output=False)
|
|
1953
|
+
t_prof = calc_t_profile(
|
|
1954
|
+
vx_profile=vx, ax_profile=ax, el_lengths=el)
|
|
1955
|
+
|
|
1956
|
+
return s, vx, ax, kappa, t_prof, raceline_xy
|
|
1957
|
+
|
|
1958
|
+
def reset_alpha(self, new_init: str = None):
|
|
1959
|
+
"""
|
|
1960
|
+
Re-initialize alpha_0 (and reset alpha_opt / history).
|
|
1961
|
+
|
|
1962
|
+
Useful for multi-start experiments: call reset_alpha() then find_alpha()
|
|
1963
|
+
to run a fresh trial from a new starting point.
|
|
1964
|
+
|
|
1965
|
+
Parameters
|
|
1966
|
+
----------
|
|
1967
|
+
new_init : str or None
|
|
1968
|
+
Override init strategy ('random' or 'mincurv'). None keeps current.
|
|
1969
|
+
"""
|
|
1970
|
+
if new_init is not None:
|
|
1971
|
+
self.init = new_init
|
|
1972
|
+
self.alpha_0 = self._init_alpha()
|
|
1973
|
+
self.alpha_opt = self.alpha_0.copy()
|
|
1974
|
+
self.history = None
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
from scipy.integrate import quad
|
|
1978
|
+
from scipy.optimize import fsolve
|
|
1979
|
+
|
|
1980
|
+
class Clothoid_raceline:
|
|
1981
|
+
def __init__(self, k_0, s, x0, y0, th0, nump=10,a_max=5.3,a_min=12,ay_max=12,c0=0.00002,c1=0.0015,v_max=22.88,v0=0,vf=22.88):
|
|
1982
|
+
self.k_0 = k_0
|
|
1983
|
+
self.s = s
|
|
1984
|
+
self.x0 = x0
|
|
1985
|
+
self.y0 = y0
|
|
1986
|
+
self.th0 = th0
|
|
1987
|
+
self.nump = nump
|
|
1988
|
+
self.a_max = a_max
|
|
1989
|
+
self.a_min = a_min
|
|
1990
|
+
self.ay_max = ay_max
|
|
1991
|
+
self.c0 = c0
|
|
1992
|
+
self.c1 = c1
|
|
1993
|
+
self.v_max = v_max
|
|
1994
|
+
self.v0=v0
|
|
1995
|
+
self.vf=vf
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
def X_0(self,a, b, c):
|
|
1999
|
+
"""
|
|
2000
|
+
|
|
2001
|
+
.. description::
|
|
2002
|
+
Computes the normalized x-displacement integral of a clothoid segment by numerically integrating
|
|
2003
|
+
cos(a/2 * tau^2 + b * tau + c) over [0, 1].
|
|
2004
|
+
|
|
2005
|
+
.. inputs::
|
|
2006
|
+
:param a: curvature rate coefficient (k1 * s^2).
|
|
2007
|
+
:type a: float
|
|
2008
|
+
:param b: initial curvature coefficient (k0 * s).
|
|
2009
|
+
:type b: float
|
|
2010
|
+
:param c: initial heading angle theta_0 in radians.
|
|
2011
|
+
:type c: float
|
|
2012
|
+
|
|
2013
|
+
.. outputs::
|
|
2014
|
+
:return result: normalized x-displacement of the clothoid segment.
|
|
2015
|
+
:rtype result: float
|
|
2016
|
+
"""
|
|
2017
|
+
|
|
2018
|
+
integrand = lambda tau: np.cos(a/2 * tau**2 + b * tau + c)
|
|
2019
|
+
result, _ = quad(integrand, 0, 1)
|
|
2020
|
+
return result
|
|
2021
|
+
|
|
2022
|
+
def Y_0(self,a, b, c):
|
|
2023
|
+
"""
|
|
2024
|
+
|
|
2025
|
+
.. description::
|
|
2026
|
+
Computes the normalized y-displacement integral of a clothoid segment by numerically integrating
|
|
2027
|
+
sin(a/2 * tau^2 + b * tau + c) over [0, 1].
|
|
2028
|
+
|
|
2029
|
+
.. inputs::
|
|
2030
|
+
:param a: curvature rate coefficient (k1 * s^2).
|
|
2031
|
+
:type a: float
|
|
2032
|
+
:param b: initial curvature coefficient (k0 * s).
|
|
2033
|
+
:type b: float
|
|
2034
|
+
:param c: initial heading angle theta_0 in radians.
|
|
2035
|
+
:type c: float
|
|
2036
|
+
|
|
2037
|
+
.. outputs::
|
|
2038
|
+
:return result: normalized y-displacement of the clothoid segment.
|
|
2039
|
+
:rtype result: float
|
|
2040
|
+
"""
|
|
2041
|
+
|
|
2042
|
+
integrand = lambda tau: np.sin(a/2 * tau**2 + b * tau + c)
|
|
2043
|
+
result, _ = quad(integrand, 0, 1)
|
|
2044
|
+
return result
|
|
2045
|
+
|
|
2046
|
+
def compute_clothoid_path(self):
|
|
2047
|
+
"""
|
|
2048
|
+
|
|
2049
|
+
.. description::
|
|
2050
|
+
Computes the full x-y path, arc-length stations, and curvature profile of a piecewise clothoid curve
|
|
2051
|
+
defined by the curvature array k_0 and station array s. Each segment uses a linearly varying curvature
|
|
2052
|
+
(clothoid), and the path is reconstructed by numerically integrating X_0 and Y_0.
|
|
2053
|
+
|
|
2054
|
+
.. outputs::
|
|
2055
|
+
:return x_full: x-coordinates of all sampled path points in m.
|
|
2056
|
+
:rtype x_full: np.ndarray
|
|
2057
|
+
:return y_full: y-coordinates of all sampled path points in m.
|
|
2058
|
+
:rtype y_full: np.ndarray
|
|
2059
|
+
:return s_full: arc-length position of each sampled point along the path in m.
|
|
2060
|
+
:rtype s_full: np.ndarray
|
|
2061
|
+
:return curvature_full: curvature value at each sampled point in rad/m.
|
|
2062
|
+
:rtype curvature_full: np.ndarray
|
|
2063
|
+
"""
|
|
2064
|
+
|
|
2065
|
+
x = [self.x0]
|
|
2066
|
+
y = [self.y0]
|
|
2067
|
+
|
|
2068
|
+
theta_0 = self.th0
|
|
2069
|
+
|
|
2070
|
+
x_full = []
|
|
2071
|
+
y_full = []
|
|
2072
|
+
s_full = []
|
|
2073
|
+
curvature_full = []
|
|
2074
|
+
|
|
2075
|
+
for i in range(1, len(self.s)):
|
|
2076
|
+
|
|
2077
|
+
L = self.s[i] - self.s[i - 1]
|
|
2078
|
+
|
|
2079
|
+
if L == 0:
|
|
2080
|
+
s_full.append(np.nan)
|
|
2081
|
+
curvature_full.append(np.nan)
|
|
2082
|
+
continue
|
|
2083
|
+
|
|
2084
|
+
k0 = self.k_0[i - 1]
|
|
2085
|
+
k1 = (self.k_0[i] - self.k_0[i - 1]) / L
|
|
2086
|
+
|
|
2087
|
+
s_values = np.linspace(0, L, self.nump)
|
|
2088
|
+
|
|
2089
|
+
for s_val in s_values:
|
|
2090
|
+
|
|
2091
|
+
X = s_val * self.X_0(k1 * s_val**2, k0 * s_val, theta_0)
|
|
2092
|
+
Y = s_val * self.Y_0(k1 * s_val**2, k0 * s_val, theta_0)
|
|
2093
|
+
|
|
2094
|
+
x_new = x[-1] + X
|
|
2095
|
+
y_new = y[-1] + Y
|
|
2096
|
+
|
|
2097
|
+
x_full.append(x_new)
|
|
2098
|
+
y_full.append(y_new)
|
|
2099
|
+
|
|
2100
|
+
s_full.append(self.s[i - 1] + s_val)
|
|
2101
|
+
curvature_full.append(k0 + k1 * s_val)
|
|
2102
|
+
|
|
2103
|
+
theta_0 = theta_0 + k0 * L + (k1 * L**2) / 2
|
|
2104
|
+
|
|
2105
|
+
x.append(x_full[-1])
|
|
2106
|
+
y.append(y_full[-1])
|
|
2107
|
+
|
|
2108
|
+
x_full = np.array(x_full)
|
|
2109
|
+
y_full = np.array(y_full)
|
|
2110
|
+
s_full = np.array(s_full)
|
|
2111
|
+
curvature_full = np.array(curvature_full)
|
|
2112
|
+
|
|
2113
|
+
return x_full, y_full, s_full, curvature_full
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
|
|
2118
|
+
|
|
2119
|
+
|
|
2120
|
+
def OSP(reftrack: np.ndarray,
|
|
2121
|
+
normvectors: np.ndarray,
|
|
2122
|
+
w_veh: float,
|
|
2123
|
+
print_debug: bool = False) -> np.ndarray:
|
|
2124
|
+
"""
|
|
2125
|
+
|
|
2126
|
+
.. description::
|
|
2127
|
+
Builds the quadratic programming matrices H, f, G, h for the Optimal Shortest Path (OSP) problem.
|
|
2128
|
+
The objective minimizes the total squared lateral deviation between consecutive raceline points,
|
|
2129
|
+
subject to track-width bounds on the lateral shift alpha.
|
|
2130
|
+
|
|
2131
|
+
.. inputs::
|
|
2132
|
+
:param reftrack: reference track array [x, y, w_tr_right, w_tr_left] in m (unclosed).
|
|
2133
|
+
:type reftrack: np.ndarray
|
|
2134
|
+
:param normvectors: normalized normal vectors for every track point [x_component, y_component].
|
|
2135
|
+
:type normvectors: np.ndarray
|
|
2136
|
+
:param w_veh: vehicle width in m used to reduce the allowed lateral deviation.
|
|
2137
|
+
:type w_veh: float
|
|
2138
|
+
:param print_debug: flag to print debug information (currently unused).
|
|
2139
|
+
:type print_debug: bool
|
|
2140
|
+
|
|
2141
|
+
.. outputs::
|
|
2142
|
+
:return H: symmetric quadratic cost matrix for the QP.
|
|
2143
|
+
:rtype H: np.ndarray
|
|
2144
|
+
:return f: linear cost vector for the QP.
|
|
2145
|
+
:rtype f: np.ndarray
|
|
2146
|
+
:return G: inequality constraint matrix (stacked identity matrices).
|
|
2147
|
+
:rtype G: np.ndarray
|
|
2148
|
+
:return h: inequality constraint vector encoding the allowed lateral deviations.
|
|
2149
|
+
:rtype h: np.ndarray
|
|
2150
|
+
"""
|
|
2151
|
+
|
|
2152
|
+
no_points = reftrack.shape[0]
|
|
2153
|
+
|
|
2154
|
+
# check inputs
|
|
2155
|
+
if no_points != normvectors.shape[0]:
|
|
2156
|
+
raise RuntimeError("Array size of reftrack should be the same as normvectors!")
|
|
2157
|
+
|
|
2158
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
2159
|
+
# SET UP FINAL MATRICES FOR SOLVER ---------------------------------------------------------------------------------
|
|
2160
|
+
# ------------------------------------------------------------------------------------------------------------------
|
|
2161
|
+
|
|
2162
|
+
H = np.zeros((no_points, no_points))
|
|
2163
|
+
f = np.zeros(no_points)
|
|
2164
|
+
|
|
2165
|
+
i_idx = np.arange(no_points)
|
|
2166
|
+
next_idx = (i_idx + 1) % no_points
|
|
2167
|
+
prev_idx = (i_idx - 1) % no_points
|
|
2168
|
+
|
|
2169
|
+
np.fill_diagonal(H, 4 * (normvectors[:, 0]**2 + normvectors[:, 1]**2))
|
|
2170
|
+
off_diag = -2 * (normvectors[i_idx, 0] * normvectors[next_idx, 0]
|
|
2171
|
+
+ normvectors[i_idx, 1] * normvectors[next_idx, 1])
|
|
2172
|
+
H[i_idx, next_idx] = off_diag
|
|
2173
|
+
H[next_idx, i_idx] = off_diag
|
|
2174
|
+
|
|
2175
|
+
f = (2 * normvectors[:, 0] * (2 * reftrack[:, 0] - reftrack[prev_idx, 0] - reftrack[next_idx, 0])
|
|
2176
|
+
+ 2 * normvectors[:, 1] * (2 * reftrack[:, 1] - reftrack[prev_idx, 1] - reftrack[next_idx, 1]))
|
|
2177
|
+
|
|
2178
|
+
|
|
2179
|
+
|
|
2180
|
+
# calculate allowed deviation from refline
|
|
2181
|
+
dev_max_right = reftrack[:, 2] - w_veh / 2
|
|
2182
|
+
dev_max_left = reftrack[:, 3] - w_veh / 2
|
|
2183
|
+
|
|
2184
|
+
# set minimum deviation to zero
|
|
2185
|
+
dev_max_right[dev_max_right < 0.001] = 0.001
|
|
2186
|
+
dev_max_left[dev_max_left < 0.001] = 0.001
|
|
2187
|
+
|
|
2188
|
+
# consider value boundaries (-dev_max <= alpha <= dev_max)
|
|
2189
|
+
G = np.vstack((np.eye(no_points), -np.eye(no_points)))
|
|
2190
|
+
h = np.ones(2 * no_points) * np.append(dev_max_right, dev_max_left)
|
|
2191
|
+
|
|
2192
|
+
return H,f,G,h
|
|
2193
|
+
|
|
2194
|
+
def ShortestPath(reftrack: np.ndarray,
|
|
2195
|
+
w_veh: float,
|
|
2196
|
+
stepsize: float,
|
|
2197
|
+
plot: bool,
|
|
2198
|
+
v_max = 22.88,
|
|
2199
|
+
ggv_import_path="maps/ggv.csv",ax_max_machines_import_path="maps/ax_max_machines.csv") -> np.ndarray:
|
|
2200
|
+
"""
|
|
2201
|
+
|
|
2202
|
+
.. description::
|
|
2203
|
+
Computes the shortest feasible path through a closed reference track by solving a QP via the OSP formulation.
|
|
2204
|
+
After obtaining the optimal lateral shift, the function interpolates the raceline, computes kinematic profiles
|
|
2205
|
+
(velocity, acceleration, lap time), and optionally plots the results.
|
|
2206
|
+
|
|
2207
|
+
.. inputs::
|
|
2208
|
+
:param reftrack: reference track array [x, y, w_tr_right, w_tr_left] in m (unclosed).
|
|
2209
|
+
:type reftrack: np.ndarray
|
|
2210
|
+
:param w_veh: vehicle width in m used to reduce the allowed lateral deviation.
|
|
2211
|
+
:type w_veh: float
|
|
2212
|
+
:param stepsize: interpolation step size for the raceline in m (currently unused internally).
|
|
2213
|
+
:type stepsize: float
|
|
2214
|
+
:param v_max: maximum velocity in m/s for the velocity profile calculation.
|
|
2215
|
+
:type v_max: float
|
|
2216
|
+
:param plot: flag to enable plotting of the track and raceline.
|
|
2217
|
+
:type plot: bool
|
|
2218
|
+
:param ggv_import_path: file path to the ggv diagram CSV.
|
|
2219
|
+
:type ggv_import_path: str
|
|
2220
|
+
:param ax_max_machines_import_path: file path to the maximum machine acceleration CSV.
|
|
2221
|
+
:type ax_max_machines_import_path: str
|
|
2222
|
+
|
|
2223
|
+
.. outputs::
|
|
2224
|
+
:return raceline_interp: interpolated shortest-path raceline coordinates [x, y].
|
|
2225
|
+
:rtype raceline_interp: np.ndarray
|
|
2226
|
+
:return alpha_shpath: optimal lateral shift in m for every reference track point.
|
|
2227
|
+
:rtype alpha_shpath: np.ndarray
|
|
2228
|
+
:return s_splines: cumulative arc-length stations along the raceline in m.
|
|
2229
|
+
:rtype s_splines: np.ndarray
|
|
2230
|
+
:return vx_profile_opt_cl: closed velocity profile (last point appended) in m/s.
|
|
2231
|
+
:rtype vx_profile_opt_cl: np.ndarray
|
|
2232
|
+
:return ax_profile_opt: longitudinal acceleration profile in m/s2.
|
|
2233
|
+
:rtype ax_profile_opt: np.ndarray
|
|
2234
|
+
:return kappa_opt: curvature profile of the raceline in rad/m.
|
|
2235
|
+
:rtype kappa_opt: np.ndarray
|
|
2236
|
+
:return t_profile_cl: cumulative lap time profile in seconds.
|
|
2237
|
+
:rtype t_profile_cl: np.ndarray
|
|
2238
|
+
"""
|
|
2239
|
+
|
|
2240
|
+
lengths = np.sqrt(np.sum(np.power(np.diff(reftrack[:,0:2], axis=0), 2), axis=1))
|
|
2241
|
+
lengths=np.append(lengths, lengths[0])
|
|
2242
|
+
coeffs_x, coeffs_y, M, normvec_norm = calc_splines(path=np.vstack((reftrack[:, 0:2], reftrack[0, 0:2])),el_lengths=lengths)
|
|
2243
|
+
H, f, G , h = OSP(reftrack=reftrack,
|
|
2244
|
+
normvectors=normvec_norm,
|
|
2245
|
+
w_veh=w_veh,)
|
|
2246
|
+
alpha_shpath = quadprog.solve_qp(H, -f, -G.T, -h, 0)[0]
|
|
2247
|
+
# sampled_pointss=np.zeros_like(reftrack[:,:2])
|
|
2248
|
+
# for i in range(len(alpha_shpath)):
|
|
2249
|
+
# sampled_pointss[i,:] = reftrack[i,:2]+alpha_shpath[i]*normvec_norm[i,:]
|
|
2250
|
+
|
|
2251
|
+
|
|
2252
|
+
|
|
2253
|
+
|
|
2254
|
+
raceline_interp, a_opt, coeffs_x_opt, coeffs_y_opt, spline_inds_opt_interp, t_vals_opt_interp, s_points_opt_interp,\
|
|
2255
|
+
spline_lengths_opt, el_lengths_opt_interp = create_raceline(refline=reftrack[:, :2],
|
|
2256
|
+
normvectors=normvec_norm,
|
|
2257
|
+
alpha=alpha_shpath,
|
|
2258
|
+
stepsize_interp=2.0,)
|
|
2259
|
+
|
|
2260
|
+
|
|
2261
|
+
psi_vel_opt, kappa_opt =calc_head_curv_an(coeffs_x=coeffs_x_opt,
|
|
2262
|
+
coeffs_y=coeffs_y_opt,
|
|
2263
|
+
ind_spls=spline_inds_opt_interp,
|
|
2264
|
+
t_spls=t_vals_opt_interp)
|
|
2265
|
+
|
|
2266
|
+
# s_splines = cumulative_distances(el_lengths_opt_interp)
|
|
2267
|
+
|
|
2268
|
+
vm = v_max
|
|
2269
|
+
fw = 3
|
|
2270
|
+
|
|
2271
|
+
ggv,ax_max_machines =import_veh_dyn_info(ggv_import_path=ggv_import_path,ax_max_machines_import_path=ax_max_machines_import_path)
|
|
2272
|
+
vx_profile_opt = calc_vel_profile(ggv=ggv,
|
|
2273
|
+
ax_max_machines=ax_max_machines,
|
|
2274
|
+
v_max=vm,
|
|
2275
|
+
kappa=kappa_opt,
|
|
2276
|
+
el_lengths=el_lengths_opt_interp,
|
|
2277
|
+
closed=True,
|
|
2278
|
+
filt_window=fw,
|
|
2279
|
+
dyn_model_exp=1.0,
|
|
2280
|
+
drag_coeff=0.75,
|
|
2281
|
+
m_veh=1000.0,
|
|
2282
|
+
v_start = 0.0)
|
|
2283
|
+
|
|
2284
|
+
# calculate longitudinal acceleration profile
|
|
2285
|
+
s_splines = cumulative_distances(el_lengths_opt_interp)
|
|
2286
|
+
vx_profile_opt_cl = np.append(vx_profile_opt, vx_profile_opt[0])
|
|
2287
|
+
ax_profile_opt = calc_ax_profile(vx_profile=vx_profile_opt_cl,
|
|
2288
|
+
el_lengths=el_lengths_opt_interp,
|
|
2289
|
+
eq_length_output=False)
|
|
2290
|
+
|
|
2291
|
+
# calculate laptime
|
|
2292
|
+
t_profile_cl = calc_t_profile(vx_profile=vx_profile_opt,
|
|
2293
|
+
ax_profile=ax_profile_opt,
|
|
2294
|
+
el_lengths=el_lengths_opt_interp)
|
|
2295
|
+
|
|
2296
|
+
if plot == True:
|
|
2297
|
+
bound1 = reftrack[:, 0:2] - normvec_norm * np.expand_dims(reftrack[:, 2], axis=1)
|
|
2298
|
+
bound2 = reftrack[:, 0:2] + normvec_norm * np.expand_dims(reftrack[:, 3], axis=1)
|
|
2299
|
+
|
|
2300
|
+
plt.figure(figsize=(10, 10))
|
|
2301
|
+
plt.plot(reftrack[:,0],reftrack[:,1],'b--',label='ref_line')
|
|
2302
|
+
plt.plot(bound1[:, 0], bound1[:, 1], 'k', label=' Track')
|
|
2303
|
+
plt.plot(bound2[:, 0], bound2[:, 1], 'k')
|
|
2304
|
+
plt.plot(raceline_interp[:,0], raceline_interp[:,1],'r.-' , label='Shortest Path')
|
|
2305
|
+
plt.xlabel('X')
|
|
2306
|
+
plt.ylabel('Y')
|
|
2307
|
+
plt.legend()
|
|
2308
|
+
plt.grid(True)
|
|
2309
|
+
plt.show()
|
|
2310
|
+
|
|
2311
|
+
|
|
2312
|
+
plt.figure(figsize=(15, 15))
|
|
2313
|
+
plt.subplot(2,2,1)
|
|
2314
|
+
plt.plot(s_splines, vx_profile_opt,'b' , label='v')
|
|
2315
|
+
plt.ylabel('v_profile')
|
|
2316
|
+
plt.xlabel('distance')
|
|
2317
|
+
plt.legend()
|
|
2318
|
+
plt.grid(True)
|
|
2319
|
+
plt.subplot(2,2,2)
|
|
2320
|
+
plt.plot(s_splines, ax_profile_opt,'b' , label='a')
|
|
2321
|
+
plt.ylabel('a_profile')
|
|
2322
|
+
plt.xlabel('distance')
|
|
2323
|
+
plt.legend()
|
|
2324
|
+
plt.grid(True)
|
|
2325
|
+
plt.subplot(2,2,3)
|
|
2326
|
+
plt.plot(s_splines, kappa_opt,'b' , label='curvature')
|
|
2327
|
+
plt.ylabel('curv_profile')
|
|
2328
|
+
plt.xlabel('distance')
|
|
2329
|
+
plt.legend()
|
|
2330
|
+
plt.grid(True)
|
|
2331
|
+
plt.subplot(2,2,4)
|
|
2332
|
+
plt.plot(s_splines, t_profile_cl[:-1],'b' , label='time')
|
|
2333
|
+
plt.ylabel('time')
|
|
2334
|
+
plt.xlabel('distance')
|
|
2335
|
+
plt.legend()
|
|
2336
|
+
plt.grid(True)
|
|
2337
|
+
plt.show()
|
|
2338
|
+
|
|
2339
|
+
print(t_profile_cl[-1])
|
|
2340
|
+
|
|
2341
|
+
|
|
2342
|
+
return raceline_interp,alpha_shpath,s_splines,vx_profile_opt_cl,ax_profile_opt,kappa_opt,t_profile_cl
|