blob: 830cfdee190cf3cf50a4e2150617b0c58b1cf84c [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():
Ravago Jones5127ccc2022-07-31 16:32:45 -070024
John Park91e69732019-03-03 13:12:43 -080025 def __init__(self):
Ravago Jonesfa8da562022-07-02 18:10:22 -070026 self.staged_points = [] # Holds all points not yet in spline
John Park91e69732019-03-03 13:12:43 -080027 self.libsplines = [] # Formatted for libspline library usage
28 self.splines = [] # Formatted for drawing
Ravago Jones3b92afa2021-02-05 14:27:32 -080029 self.constraints = [ # default constraints
30 {
31 "constraint_type": "LONGITUDINAL_ACCELERATION",
32 "value": 3
33 }, {
34 "constraint_type": "LATERAL_ACCELERATION",
35 "value": 2
36 }, {
37 "constraint_type": "VOLTAGE",
38 "value": 10
39 }
40 ]
John Park91e69732019-03-03 13:12:43 -080041
Ravago Jones56941a52021-07-31 14:42:38 -070042 def __deepcopy__(self, memo):
Ravago Jonesfa8da562022-07-02 18:10:22 -070043 new_copy = Multispline()
44 new_copy.staged_points = copy.deepcopy(self.staged_points, memo)
Ravago Jones56941a52021-07-31 14:42:38 -070045 new_copy.splines = copy.deepcopy(self.splines, memo)
46 new_copy.constraints = copy.deepcopy(self.constraints, memo)
47 new_copy.update_lib_spline()
48 return new_copy
49
John Park91e69732019-03-03 13:12:43 -080050 def getLibsplines(self):
51 return self.libsplines
52
Ravago Jones3b92afa2021-02-05 14:27:32 -080053 def setConstraint(self, id, value):
54 for constraint in self.constraints:
55 if constraint["constraint_type"] == id:
56 constraint["value"] = value
57 break
58
kyle96e46e12021-02-10 21:47:55 -080059 def getConstraint(self, id):
60 for constraint in self.constraints:
61 if constraint["constraint_type"] == id:
62 return constraint["value"]
63
Ravago Jones3b92afa2021-02-05 14:27:32 -080064 def addConstraintsToTrajectory(self, trajectory):
65 for constraint in self.constraints:
66 if constraint["constraint_type"] == "VOLTAGE":
67 trajectory.SetVoltageLimit(constraint["value"])
68 elif constraint["constraint_type"] == "LATERAL_ACCELERATION":
69 trajectory.SetLateralAcceleration(constraint["value"])
70 elif constraint["constraint_type"] == "LONGITUDINAL_ACCELERATION":
71 trajectory.SetLongitudinalAcceleration(constraint["value"])
72
Ravago Jones359bbd22022-07-07 01:07:02 -070073 @staticmethod
74 def splineExtrapolate(multisplines, index, *, snap=True):
75 multispline = multisplines[index.multispline_index]
76 spline = multispline.splines[index.spline_index]
John Park91e69732019-03-03 13:12:43 -080077
Ravago Jones359bbd22022-07-07 01:07:02 -070078 # find the adjacent splines that will be affected by this spline
John Park91e69732019-03-03 13:12:43 -080079
Ravago Jones359bbd22022-07-07 01:07:02 -070080 prev_multispline = None
81 prev_spline = None
82 if index.spline_index != 0:
83 prev_spline = multispline.splines[index.spline_index - 1]
84 elif index.multispline_index != 0:
85 prev_multispline = multisplines[index.multispline_index - 1]
86 prev_spline = prev_multispline.splines[-1]
John Park91e69732019-03-03 13:12:43 -080087
Ravago Jones359bbd22022-07-07 01:07:02 -070088 next_multispline = None
89 next_spline = None
90 if spline is not multispline.splines[-1]:
91 next_spline = multispline.splines[index.spline_index + 1]
92 elif multispline is not multisplines[-1]:
93 next_multispline = multisplines[index.multispline_index + 1]
94 if next_multispline.splines:
95 next_spline = next_multispline.splines[0]
John Park91e69732019-03-03 13:12:43 -080096
Ravago Jones359bbd22022-07-07 01:07:02 -070097 # adjust the adjacent splines according to our smoothness constraints
98 # the three points on either side are entirely constrained by this spline
John Park91e69732019-03-03 13:12:43 -080099
Ravago Jones359bbd22022-07-07 01:07:02 -0700100 if next_spline is not None:
Ravago Jones5127ccc2022-07-31 16:32:45 -0700101 f = spline[5] # the end of the spline
102 e = spline[4] # determines the heading
Ravago Jones359bbd22022-07-07 01:07:02 -0700103 d = spline[3]
104 if next_multispline is None:
105 next_spline[0] = f
106 next_spline[1] = f * 2 + e * -1
107 next_spline[2] = d + f * 4 + e * -4
108 else:
109 if snap:
Ravago Jones5127ccc2022-07-31 16:32:45 -0700110 Multispline.snapSplines(spline,
111 next_spline,
112 match_first_to_second=False)
John Park91e69732019-03-03 13:12:43 -0800113
Ravago Jones359bbd22022-07-07 01:07:02 -0700114 next_spline[0] = f
115 next_multispline.update_lib_spline()
John Park91e69732019-03-03 13:12:43 -0800116
Ravago Jones359bbd22022-07-07 01:07:02 -0700117 if prev_spline is not None:
118 a = spline[0]
119 b = spline[1]
120 c = spline[2]
121 if prev_multispline is None:
122 prev_spline[5] = a
123 prev_spline[4] = a * 2 + b * -1
124 prev_spline[3] = c + a * 4 + b * -4
125 else:
126 if snap:
Ravago Jones5127ccc2022-07-31 16:32:45 -0700127 Multispline.snapSplines(prev_spline,
128 spline,
129 match_first_to_second=True)
Ravago Jones359bbd22022-07-07 01:07:02 -0700130
131 prev_spline[5] = a
132 prev_multispline.update_lib_spline()
133
134 def snapSplines(first_spline, second_spline, *, match_first_to_second):
135 """Snaps two adjacent splines together, preserving the heading from one to another.
136
137 The end of `first_spline` connects to the beginning of `second_spline`
138 The user will have their mouse one one of these splines, so we
139 only want to snap the spline that they're not holding.
140
141 They can have the same heading, or they can have opposite headings
142 which represents the robot driving that spline backwards.
143 """
144
145 # Represent the position of the second control point (controls the heading)
146 # as a vector from the end of the spline to the control point
147 first_vector = first_spline[-2] - first_spline[-1]
148 second_vector = second_spline[1] - second_spline[0]
149
150 # we want to preserve the distance
151 first_magnitude = np.linalg.norm(first_vector, ord=2)
152 second_magnitude = np.linalg.norm(second_vector, ord=2)
153
154 normalized_first = first_vector / first_magnitude
155 normalized_second = second_vector / second_magnitude
156
157 # the proposed new vector if we were to make them point the same direction
158 swapped_second = normalized_first * second_magnitude
159 swapped_first = normalized_second * first_magnitude
160
161 # they were pointing in opposite directions
162 if np.dot(first_vector, second_vector) < 0:
163
164 # rotate the new vectors 180 degrees
165 # to keep them pointing in opposite directions
166 swapped_first = -swapped_first
167 swapped_second = -swapped_second
168
169 # Calculate how far we ended up moving the second control point
170 # so we can move the third control point with it
171 change_in_first = swapped_first - first_vector
172 change_in_second = swapped_second - second_vector
173
174 # apply the changes and discard the other proposed snap
175 if match_first_to_second:
176 first_spline[-2] = swapped_first + first_spline[-1]
177 first_spline[-3] += change_in_first
178 # swapped_second doesn't get used
179 else:
180 second_spline[1] = swapped_second + second_spline[0]
181 second_spline[2] += change_in_second
182 # swapped_first doesn't get used
183
184 def updates_for_mouse_move(self, multisplines, index, mouse):
185 """Moves the control point and adjacent points to follow the mouse"""
186 if index == None: return
187 spline_edit = index.spline_index
188 index_of_edit = index.control_point_index
189
190 spline = self.splines[spline_edit]
191
192 # we want to move it to be on the mouse
193 diffs = mouse - spline[index_of_edit]
194
195 spline[index_of_edit] = mouse
196
Ravago Jones359bbd22022-07-07 01:07:02 -0700197 # all three points move together with the endpoint
198 if index_of_edit == 5:
199 spline[3] += diffs
200 spline[4] += diffs
201
202 # check if the next multispline exists and has a spline
203 if index.spline_index == len(
204 self.splines) - 1 and not index.multispline_index == len(
205 multisplines) - 1 and len(
206 multisplines[index.multispline_index +
207 1].splines) > 0:
208 # move the points that lay across the multispline boundary
209 other_spline = multisplines[index.multispline_index +
210 1].splines[0]
211
212 other_spline[1] += diffs
213 other_spline[2] += diffs
214
215 if index_of_edit == 0:
216 spline[2] += diffs
217 spline[1] += diffs
218
219 # check if previous multispline exists
220 if index.spline_index == 0 and not index.multispline_index == 0:
221 other_spline = multisplines[index.multispline_index -
222 1].splines[-1]
223
224 other_spline[3] += diffs
225 other_spline[4] += diffs
226
227 # the third point moves with the second point
228 if index_of_edit == 4:
229 spline[3] += diffs
230
231 if index_of_edit == 1:
232 spline[2] += diffs
233
234 Multispline.splineExtrapolate(multisplines, index, snap=False)
John Park91e69732019-03-03 13:12:43 -0800235
236 def update_lib_spline(self):
237 self.libsplines = []
238 array = np.zeros(shape=(6, 2), dtype=float)
239 for points in self.splines:
240 for j, point in enumerate(points):
241 array[j, 0] = point[0]
242 array[j, 1] = point[1]
243 spline = Spline(np.ascontiguousarray(np.transpose(array)))
244 self.libsplines.append(spline)
245
Ravago Jonesfa8da562022-07-02 18:10:22 -0700246 @staticmethod
247 def nearest_distance(multisplines, point):
248 """Finds the spot along the multisplines that is closest to a
249 given point on the field
250
251 Returns the closest multispline and the distance along that multispline
252 """
Ravago Jones5127ccc2022-07-31 16:32:45 -0700253
Ravago Jonesb170ed32022-06-01 21:16:15 -0700254 def distance(t, distance_spline, point):
255 return np.sum((distance_spline.XY(t) - point)**2)
256
257 # We know the derivative of the function,
258 # so scipy doesn't need to compute it every time
259 def ddistance(t, distance_spline, point):
260 return np.sum(2 * (distance_spline.XY(t) - point) *
261 distance_spline.DXY(t))
262
Ravago Jonesb170ed32022-06-01 21:16:15 -0700263 best_result = None
Ravago Jonesfa8da562022-07-02 18:10:22 -0700264 best_multispline = None
Ravago Jonesb170ed32022-06-01 21:16:15 -0700265
Ravago Jonesfa8da562022-07-02 18:10:22 -0700266 for multispline_index, multispline in enumerate(multisplines):
Maxwell Henderson3e9d8992023-01-20 18:00:21 -0800267 if not multispline.getLibsplines():
268 continue
Ravago Jonesfa8da562022-07-02 18:10:22 -0700269 distance_spline = DistanceSpline(multispline.getLibsplines())
Ravago Jonesb170ed32022-06-01 21:16:15 -0700270
Ravago Jonesfa8da562022-07-02 18:10:22 -0700271 # The optimizer finds local minima that often aren't what we want,
272 # so try from multiple locations to find a better minimum.
273 guess_points = np.linspace(0, distance_spline.Length(), num=5)
Ravago Jonesb170ed32022-06-01 21:16:15 -0700274
Ravago Jonesfa8da562022-07-02 18:10:22 -0700275 for guess in guess_points:
276 result = scipy.optimize.minimize(
277 distance,
278 guess,
279 args=(distance_spline, point),
280 bounds=((0, distance_spline.Length()), ),
281 jac=ddistance,
282 )
Ravago Jonesb170ed32022-06-01 21:16:15 -0700283
Ravago Jonesfa8da562022-07-02 18:10:22 -0700284 if result.success and (best_result == None
285 or result.fun < best_result.fun):
286 best_result = result
287 best_multispline = multispline
Ravago Jonesb170ed32022-06-01 21:16:15 -0700288
Ravago Jonesfa8da562022-07-02 18:10:22 -0700289 return (best_multispline, best_result)
290
291 def toJsonObject(self):
Ravago Jones3b92afa2021-02-05 14:27:32 -0800292 multi_spline = {
293 "spline_count": 0,
294 "spline_x": [],
295 "spline_y": [],
296 "constraints": self.constraints,
297 }
298 for points in self.splines:
299 multi_spline["spline_count"] += 1
300 for j, point in enumerate(points):
301 if j == 0 and multi_spline["spline_count"] > 1:
302 continue # skip overlapping points
303 multi_spline["spline_x"].append(point[0])
304 multi_spline["spline_y"].append(point[1])
305 return multi_spline
306
Ravago Jonesfa8da562022-07-02 18:10:22 -0700307 @staticmethod
308 def fromJsonObject(multi_spline):
309 multispline = Multispline()
310 multispline.constraints = multi_spline["constraints"]
311 multispline.splines = []
312 multispline.staged_points = []
Ravago Jones3b92afa2021-02-05 14:27:32 -0800313
314 i = 0
315 for j in range(multi_spline["spline_count"]):
316 # get the last point of the last spline
317 # and read in another 6 points
318 for i in range(i, i + 6):
Ravago Jonesfa8da562022-07-02 18:10:22 -0700319 multispline.staged_points.append(
Ravago Jones3b92afa2021-02-05 14:27:32 -0800320 [multi_spline["spline_x"][i], multi_spline["spline_y"][i]])
Ravago Jonesfa8da562022-07-02 18:10:22 -0700321 multispline.splines.append(np.array(multispline.staged_points))
322 multispline.staged_points = []
323 multispline.update_lib_spline()
324
325 return multispline
Ravago Jones3b92afa2021-02-05 14:27:32 -0800326
John Park91e69732019-03-03 13:12:43 -0800327 def getSplines(self):
328 return self.splines
329
Ravago Jonesfa8da562022-07-02 18:10:22 -0700330 def setControlPoint(self, index, x, y):
331 self.splines[index.spline_index][index.control_point_index] = [x, y]
John Park91e69732019-03-03 13:12:43 -0800332
Ravago Jonesfa8da562022-07-02 18:10:22 -0700333 def addPoint(self, x, y):
334 if (len(self.staged_points) < 6):
335 self.staged_points.append([x, y])
336 if (len(self.staged_points) == 6):
337 self.splines.append(np.array(self.staged_points))
338 self.staged_points = []
John Park91e69732019-03-03 13:12:43 -0800339 self.update_lib_spline()
340 return True
341
Ravago Jones359bbd22022-07-07 01:07:02 -0700342 def extrapolate(self, spline):
Ravago Jonesfa8da562022-07-02 18:10:22 -0700343 """Stages 3 points extrapolated from the end of the multispline"""
Ravago Jonesfa8da562022-07-02 18:10:22 -0700344
345 self.staged_points = []
346
Ravago Jonesfa8da562022-07-02 18:10:22 -0700347 point1 = spline[5]
348 point2 = spline[4]
349 point3 = spline[3]
350
351 self.staged_points.append(point1)
352 self.staged_points.append(point1 * 2 - point2)
353 self.staged_points.append(point3 + point1 * 4 - point2 * 4)