blob: 14417932cdfd7191454bbc651e6efb2a6bf05079 [file] [log] [blame]
John Park91e69732019-03-03 13:12:43 -08001from constants import *
2import numpy as np
Ravago Jonesb170ed32022-06-01 21:16:15 -07003import scipy.optimize
John Park91e69732019-03-03 13:12:43 -08004from libspline import Spline, DistanceSpline, Trajectory
Ravago Jones56941a52021-07-31 14:42:38 -07005import copy
Ravago Jonesfa8da562022-07-02 18:10:22 -07006from dataclasses import dataclass
John Park91e69732019-03-03 13:12:43 -08007
Ravago Jones8da89c42022-07-17 19:34:06 -07008
Ravago Jonesfa8da562022-07-02 18:10:22 -07009@dataclass
10class ControlPointIndex:
11 """Class specifying the index of a control point"""
12
13 # The index of the multispline in the list of multisplines
14 multispline_index: int
15
16 # The index of the spline in the multispline
17 spline_index: int
18
19 # The index of the control point in the spline [0-5]
20 control_point_index: int
21
22
23class Multispline():
John Park91e69732019-03-03 13:12:43 -080024 def __init__(self):
Ravago Jonesfa8da562022-07-02 18:10:22 -070025 self.staged_points = [] # Holds all points not yet in spline
John Park91e69732019-03-03 13:12:43 -080026 self.libsplines = [] # Formatted for libspline library usage
27 self.splines = [] # Formatted for drawing
Ravago Jones3b92afa2021-02-05 14:27:32 -080028 self.constraints = [ # default constraints
29 {
30 "constraint_type": "LONGITUDINAL_ACCELERATION",
31 "value": 3
32 }, {
33 "constraint_type": "LATERAL_ACCELERATION",
34 "value": 2
35 }, {
36 "constraint_type": "VOLTAGE",
37 "value": 10
38 }
39 ]
John Park91e69732019-03-03 13:12:43 -080040
Ravago Jones56941a52021-07-31 14:42:38 -070041 def __deepcopy__(self, memo):
Ravago Jonesfa8da562022-07-02 18:10:22 -070042 new_copy = Multispline()
43 new_copy.staged_points = copy.deepcopy(self.staged_points, memo)
Ravago Jones56941a52021-07-31 14:42:38 -070044 new_copy.splines = copy.deepcopy(self.splines, memo)
45 new_copy.constraints = copy.deepcopy(self.constraints, memo)
46 new_copy.update_lib_spline()
47 return new_copy
48
John Park91e69732019-03-03 13:12:43 -080049 def getLibsplines(self):
50 return self.libsplines
51
Ravago Jones3b92afa2021-02-05 14:27:32 -080052 def setConstraint(self, id, value):
53 for constraint in self.constraints:
54 if constraint["constraint_type"] == id:
55 constraint["value"] = value
56 break
57
kyle96e46e12021-02-10 21:47:55 -080058 def getConstraint(self, id):
59 for constraint in self.constraints:
60 if constraint["constraint_type"] == id:
61 return constraint["value"]
62
Ravago Jones3b92afa2021-02-05 14:27:32 -080063 def addConstraintsToTrajectory(self, trajectory):
64 for constraint in self.constraints:
65 if constraint["constraint_type"] == "VOLTAGE":
66 trajectory.SetVoltageLimit(constraint["value"])
67 elif constraint["constraint_type"] == "LATERAL_ACCELERATION":
68 trajectory.SetLateralAcceleration(constraint["value"])
69 elif constraint["constraint_type"] == "LONGITUDINAL_ACCELERATION":
70 trajectory.SetLongitudinalAcceleration(constraint["value"])
71
Ravago Jones359bbd22022-07-07 01:07:02 -070072 @staticmethod
73 def splineExtrapolate(multisplines, index, *, snap=True):
74 multispline = multisplines[index.multispline_index]
75 spline = multispline.splines[index.spline_index]
John Park91e69732019-03-03 13:12:43 -080076
Ravago Jones359bbd22022-07-07 01:07:02 -070077 # find the adjacent splines that will be affected by this spline
John Park91e69732019-03-03 13:12:43 -080078
Ravago Jones359bbd22022-07-07 01:07:02 -070079 prev_multispline = None
80 prev_spline = None
81 if index.spline_index != 0:
82 prev_spline = multispline.splines[index.spline_index - 1]
83 elif index.multispline_index != 0:
84 prev_multispline = multisplines[index.multispline_index - 1]
85 prev_spline = prev_multispline.splines[-1]
John Park91e69732019-03-03 13:12:43 -080086
Ravago Jones359bbd22022-07-07 01:07:02 -070087 next_multispline = None
88 next_spline = None
89 if spline is not multispline.splines[-1]:
90 next_spline = multispline.splines[index.spline_index + 1]
91 elif multispline is not multisplines[-1]:
92 next_multispline = multisplines[index.multispline_index + 1]
93 if next_multispline.splines:
94 next_spline = next_multispline.splines[0]
John Park91e69732019-03-03 13:12:43 -080095
Ravago Jones359bbd22022-07-07 01:07:02 -070096 # adjust the adjacent splines according to our smoothness constraints
97 # the three points on either side are entirely constrained by this spline
John Park91e69732019-03-03 13:12:43 -080098
Ravago Jones359bbd22022-07-07 01:07:02 -070099 if next_spline is not None:
100 f = spline[5] # the end of the spline
101 e = spline[4] # determines the heading
102 d = spline[3]
103 if next_multispline is None:
104 next_spline[0] = f
105 next_spline[1] = f * 2 + e * -1
106 next_spline[2] = d + f * 4 + e * -4
107 else:
108 if snap:
109 Multispline.snapSplines(spline, next_spline, match_first_to_second=False)
John Park91e69732019-03-03 13:12:43 -0800110
Ravago Jones359bbd22022-07-07 01:07:02 -0700111 next_spline[0] = f
112 next_multispline.update_lib_spline()
John Park91e69732019-03-03 13:12:43 -0800113
Ravago Jones359bbd22022-07-07 01:07:02 -0700114 if prev_spline is not None:
115 a = spline[0]
116 b = spline[1]
117 c = spline[2]
118 if prev_multispline is None:
119 prev_spline[5] = a
120 prev_spline[4] = a * 2 + b * -1
121 prev_spline[3] = c + a * 4 + b * -4
122 else:
123 if snap:
124 Multispline.snapSplines(prev_spline, spline, match_first_to_second=True)
125
126 prev_spline[5] = a
127 prev_multispline.update_lib_spline()
128
129 def snapSplines(first_spline, second_spline, *, match_first_to_second):
130 """Snaps two adjacent splines together, preserving the heading from one to another.
131
132 The end of `first_spline` connects to the beginning of `second_spline`
133 The user will have their mouse one one of these splines, so we
134 only want to snap the spline that they're not holding.
135
136 They can have the same heading, or they can have opposite headings
137 which represents the robot driving that spline backwards.
138 """
139
140 # Represent the position of the second control point (controls the heading)
141 # as a vector from the end of the spline to the control point
142 first_vector = first_spline[-2] - first_spline[-1]
143 second_vector = second_spline[1] - second_spline[0]
144
145 # we want to preserve the distance
146 first_magnitude = np.linalg.norm(first_vector, ord=2)
147 second_magnitude = np.linalg.norm(second_vector, ord=2)
148
149 normalized_first = first_vector / first_magnitude
150 normalized_second = second_vector / second_magnitude
151
152 # the proposed new vector if we were to make them point the same direction
153 swapped_second = normalized_first * second_magnitude
154 swapped_first = normalized_second * first_magnitude
155
156 # they were pointing in opposite directions
157 if np.dot(first_vector, second_vector) < 0:
158
159 # rotate the new vectors 180 degrees
160 # to keep them pointing in opposite directions
161 swapped_first = -swapped_first
162 swapped_second = -swapped_second
163
164 # Calculate how far we ended up moving the second control point
165 # so we can move the third control point with it
166 change_in_first = swapped_first - first_vector
167 change_in_second = swapped_second - second_vector
168
169 # apply the changes and discard the other proposed snap
170 if match_first_to_second:
171 first_spline[-2] = swapped_first + first_spline[-1]
172 first_spline[-3] += change_in_first
173 # swapped_second doesn't get used
174 else:
175 second_spline[1] = swapped_second + second_spline[0]
176 second_spline[2] += change_in_second
177 # swapped_first doesn't get used
178
179 def updates_for_mouse_move(self, multisplines, index, mouse):
180 """Moves the control point and adjacent points to follow the mouse"""
181 if index == None: return
182 spline_edit = index.spline_index
183 index_of_edit = index.control_point_index
184
185 spline = self.splines[spline_edit]
186
187 # we want to move it to be on the mouse
188 diffs = mouse - spline[index_of_edit]
189
190 spline[index_of_edit] = mouse
191
192
193 # all three points move together with the endpoint
194 if index_of_edit == 5:
195 spline[3] += diffs
196 spline[4] += diffs
197
198 # check if the next multispline exists and has a spline
199 if index.spline_index == len(
200 self.splines) - 1 and not index.multispline_index == len(
201 multisplines) - 1 and len(
202 multisplines[index.multispline_index +
203 1].splines) > 0:
204 # move the points that lay across the multispline boundary
205 other_spline = multisplines[index.multispline_index +
206 1].splines[0]
207
208 other_spline[1] += diffs
209 other_spline[2] += diffs
210
211 if index_of_edit == 0:
212 spline[2] += diffs
213 spline[1] += diffs
214
215 # check if previous multispline exists
216 if index.spline_index == 0 and not index.multispline_index == 0:
217 other_spline = multisplines[index.multispline_index -
218 1].splines[-1]
219
220 other_spline[3] += diffs
221 other_spline[4] += diffs
222
223 # the third point moves with the second point
224 if index_of_edit == 4:
225 spline[3] += diffs
226
227 if index_of_edit == 1:
228 spline[2] += diffs
229
230 Multispline.splineExtrapolate(multisplines, index, snap=False)
John Park91e69732019-03-03 13:12:43 -0800231
232 def update_lib_spline(self):
233 self.libsplines = []
234 array = np.zeros(shape=(6, 2), dtype=float)
235 for points in self.splines:
236 for j, point in enumerate(points):
237 array[j, 0] = point[0]
238 array[j, 1] = point[1]
239 spline = Spline(np.ascontiguousarray(np.transpose(array)))
240 self.libsplines.append(spline)
241
Ravago Jonesfa8da562022-07-02 18:10:22 -0700242 @staticmethod
243 def nearest_distance(multisplines, point):
244 """Finds the spot along the multisplines that is closest to a
245 given point on the field
246
247 Returns the closest multispline and the distance along that multispline
248 """
Ravago Jonesb170ed32022-06-01 21:16:15 -0700249 def distance(t, distance_spline, point):
250 return np.sum((distance_spline.XY(t) - point)**2)
251
252 # We know the derivative of the function,
253 # so scipy doesn't need to compute it every time
254 def ddistance(t, distance_spline, point):
255 return np.sum(2 * (distance_spline.XY(t) - point) *
256 distance_spline.DXY(t))
257
Ravago Jonesb170ed32022-06-01 21:16:15 -0700258 best_result = None
Ravago Jonesfa8da562022-07-02 18:10:22 -0700259 best_multispline = None
Ravago Jonesb170ed32022-06-01 21:16:15 -0700260
Ravago Jonesfa8da562022-07-02 18:10:22 -0700261 for multispline_index, multispline in enumerate(multisplines):
262 distance_spline = DistanceSpline(multispline.getLibsplines())
Ravago Jonesb170ed32022-06-01 21:16:15 -0700263
Ravago Jonesfa8da562022-07-02 18:10:22 -0700264 # The optimizer finds local minima that often aren't what we want,
265 # so try from multiple locations to find a better minimum.
266 guess_points = np.linspace(0, distance_spline.Length(), num=5)
Ravago Jonesb170ed32022-06-01 21:16:15 -0700267
Ravago Jonesfa8da562022-07-02 18:10:22 -0700268 for guess in guess_points:
269 result = scipy.optimize.minimize(
270 distance,
271 guess,
272 args=(distance_spline, point),
273 bounds=((0, distance_spline.Length()), ),
274 jac=ddistance,
275 )
Ravago Jonesb170ed32022-06-01 21:16:15 -0700276
Ravago Jonesfa8da562022-07-02 18:10:22 -0700277 if result.success and (best_result == None
278 or result.fun < best_result.fun):
279 best_result = result
280 best_multispline = multispline
Ravago Jonesb170ed32022-06-01 21:16:15 -0700281
Ravago Jonesfa8da562022-07-02 18:10:22 -0700282 return (best_multispline, best_result)
283
284 def toJsonObject(self):
Ravago Jones3b92afa2021-02-05 14:27:32 -0800285 multi_spline = {
286 "spline_count": 0,
287 "spline_x": [],
288 "spline_y": [],
289 "constraints": self.constraints,
290 }
291 for points in self.splines:
292 multi_spline["spline_count"] += 1
293 for j, point in enumerate(points):
294 if j == 0 and multi_spline["spline_count"] > 1:
295 continue # skip overlapping points
296 multi_spline["spline_x"].append(point[0])
297 multi_spline["spline_y"].append(point[1])
298 return multi_spline
299
Ravago Jonesfa8da562022-07-02 18:10:22 -0700300 @staticmethod
301 def fromJsonObject(multi_spline):
302 multispline = Multispline()
303 multispline.constraints = multi_spline["constraints"]
304 multispline.splines = []
305 multispline.staged_points = []
Ravago Jones3b92afa2021-02-05 14:27:32 -0800306
307 i = 0
308 for j in range(multi_spline["spline_count"]):
309 # get the last point of the last spline
310 # and read in another 6 points
311 for i in range(i, i + 6):
Ravago Jonesfa8da562022-07-02 18:10:22 -0700312 multispline.staged_points.append(
Ravago Jones3b92afa2021-02-05 14:27:32 -0800313 [multi_spline["spline_x"][i], multi_spline["spline_y"][i]])
Ravago Jonesfa8da562022-07-02 18:10:22 -0700314 multispline.splines.append(np.array(multispline.staged_points))
315 multispline.staged_points = []
316 multispline.update_lib_spline()
317
318 return multispline
Ravago Jones3b92afa2021-02-05 14:27:32 -0800319
John Park91e69732019-03-03 13:12:43 -0800320 def getSplines(self):
321 return self.splines
322
Ravago Jonesfa8da562022-07-02 18:10:22 -0700323 def setControlPoint(self, index, x, y):
324 self.splines[index.spline_index][index.control_point_index] = [x, y]
John Park91e69732019-03-03 13:12:43 -0800325
Ravago Jonesfa8da562022-07-02 18:10:22 -0700326 def addPoint(self, x, y):
327 if (len(self.staged_points) < 6):
328 self.staged_points.append([x, y])
329 if (len(self.staged_points) == 6):
330 self.splines.append(np.array(self.staged_points))
331 self.staged_points = []
John Park91e69732019-03-03 13:12:43 -0800332 self.update_lib_spline()
333 return True
334
Ravago Jones359bbd22022-07-07 01:07:02 -0700335 def extrapolate(self, spline):
Ravago Jonesfa8da562022-07-02 18:10:22 -0700336 """Stages 3 points extrapolated from the end of the multispline"""
Ravago Jonesfa8da562022-07-02 18:10:22 -0700337
338 self.staged_points = []
339
Ravago Jonesfa8da562022-07-02 18:10:22 -0700340 point1 = spline[5]
341 point2 = spline[4]
342 point3 = spline[3]
343
344 self.staged_points.append(point1)
345 self.staged_points.append(point1 * 2 - point2)
346 self.staged_points.append(point3 + point1 * 4 - point2 * 4)