blob: 5551587c2d6c8f11cecc8896a4ba5523a62851a8 [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):
267 distance_spline = DistanceSpline(multispline.getLibsplines())
Ravago Jonesb170ed32022-06-01 21:16:15 -0700268
Ravago Jonesfa8da562022-07-02 18:10:22 -0700269 # The optimizer finds local minima that often aren't what we want,
270 # so try from multiple locations to find a better minimum.
271 guess_points = np.linspace(0, distance_spline.Length(), num=5)
Ravago Jonesb170ed32022-06-01 21:16:15 -0700272
Ravago Jonesfa8da562022-07-02 18:10:22 -0700273 for guess in guess_points:
274 result = scipy.optimize.minimize(
275 distance,
276 guess,
277 args=(distance_spline, point),
278 bounds=((0, distance_spline.Length()), ),
279 jac=ddistance,
280 )
Ravago Jonesb170ed32022-06-01 21:16:15 -0700281
Ravago Jonesfa8da562022-07-02 18:10:22 -0700282 if result.success and (best_result == None
283 or result.fun < best_result.fun):
284 best_result = result
285 best_multispline = multispline
Ravago Jonesb170ed32022-06-01 21:16:15 -0700286
Ravago Jonesfa8da562022-07-02 18:10:22 -0700287 return (best_multispline, best_result)
288
289 def toJsonObject(self):
Ravago Jones3b92afa2021-02-05 14:27:32 -0800290 multi_spline = {
291 "spline_count": 0,
292 "spline_x": [],
293 "spline_y": [],
294 "constraints": self.constraints,
295 }
296 for points in self.splines:
297 multi_spline["spline_count"] += 1
298 for j, point in enumerate(points):
299 if j == 0 and multi_spline["spline_count"] > 1:
300 continue # skip overlapping points
301 multi_spline["spline_x"].append(point[0])
302 multi_spline["spline_y"].append(point[1])
303 return multi_spline
304
Ravago Jonesfa8da562022-07-02 18:10:22 -0700305 @staticmethod
306 def fromJsonObject(multi_spline):
307 multispline = Multispline()
308 multispline.constraints = multi_spline["constraints"]
309 multispline.splines = []
310 multispline.staged_points = []
Ravago Jones3b92afa2021-02-05 14:27:32 -0800311
312 i = 0
313 for j in range(multi_spline["spline_count"]):
314 # get the last point of the last spline
315 # and read in another 6 points
316 for i in range(i, i + 6):
Ravago Jonesfa8da562022-07-02 18:10:22 -0700317 multispline.staged_points.append(
Ravago Jones3b92afa2021-02-05 14:27:32 -0800318 [multi_spline["spline_x"][i], multi_spline["spline_y"][i]])
Ravago Jonesfa8da562022-07-02 18:10:22 -0700319 multispline.splines.append(np.array(multispline.staged_points))
320 multispline.staged_points = []
321 multispline.update_lib_spline()
322
323 return multispline
Ravago Jones3b92afa2021-02-05 14:27:32 -0800324
John Park91e69732019-03-03 13:12:43 -0800325 def getSplines(self):
326 return self.splines
327
Ravago Jonesfa8da562022-07-02 18:10:22 -0700328 def setControlPoint(self, index, x, y):
329 self.splines[index.spline_index][index.control_point_index] = [x, y]
John Park91e69732019-03-03 13:12:43 -0800330
Ravago Jonesfa8da562022-07-02 18:10:22 -0700331 def addPoint(self, x, y):
332 if (len(self.staged_points) < 6):
333 self.staged_points.append([x, y])
334 if (len(self.staged_points) == 6):
335 self.splines.append(np.array(self.staged_points))
336 self.staged_points = []
John Park91e69732019-03-03 13:12:43 -0800337 self.update_lib_spline()
338 return True
339
Ravago Jones359bbd22022-07-07 01:07:02 -0700340 def extrapolate(self, spline):
Ravago Jonesfa8da562022-07-02 18:10:22 -0700341 """Stages 3 points extrapolated from the end of the multispline"""
Ravago Jonesfa8da562022-07-02 18:10:22 -0700342
343 self.staged_points = []
344
Ravago Jonesfa8da562022-07-02 18:10:22 -0700345 point1 = spline[5]
346 point2 = spline[4]
347 point3 = spline[3]
348
349 self.staged_points.append(point1)
350 self.staged_points.append(point1 * 2 - point2)
351 self.staged_points.append(point3 + point1 * 4 - point2 * 4)