Nav2 Navigation Stack - jazzy  jazzy
ROS 2 Navigation Stack
trajectory_generator.py
1 # Copyright (c) 2021, Matthew Booker
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License. Reserved.
14 
15 import logging
16 from typing import Tuple, Union
17 
18 import numpy as np
19 
20 from trajectory import Path, Trajectory, TrajectoryParameters
21 
22 logger = logging.getLogger(__name__)
23 
24 
26  """Handles all the logic for generating trajectories."""
27 
28  def __init__(self, config: dict):
29  """Init TrajectoryGenerator using the user supplied config."""
30  self.turning_radiusturning_radius = config['turning_radius']
31 
32  def _get_arc_point(
33  self, trajectory_params: TrajectoryParameters, t: float
34  ) -> Tuple[float, float, float]:
35  """
36  Get point on the arc trajectory using the following parameterization.
37 
38  r(t) = <R * cos(t - pi/2) + a, R * sin(t - pi/2) + b>
39 
40  R = radius
41  a = x offset
42  b = y offset
43 
44  Args
45  ----
46  trajectory_params: TrajectoryParameters
47  The parameters that describe the arc to create
48  t: float
49  A value between 0 - 1 that denotes where along the arc
50  to calculate the point
51 
52  Returns
53  -------
54  x: float
55  x coordinate of generated point
56  y: float
57  y coordinate of generated point
58  yaw: float
59  angle of tangent line to arc at point (x,y)
60 
61  """
62  start_angle = trajectory_params.start_angle
63 
64  arc_dist = t * trajectory_params.arc_length
65  angle_step = arc_dist / trajectory_params.turning_radius
66 
67  if trajectory_params.left_turn:
68  # Calculate points using
69  # r(t) = <R * cos(t - pi/2) + a, R * sin(t - pi/2) + b>
70  t = start_angle + angle_step
71  x = (
72  trajectory_params.turning_radius * np.cos(t - np.pi / 2)
73  + trajectory_params.x_offset
74  )
75  y = (
76  trajectory_params.turning_radius * np.sin(t - np.pi / 2)
77  + trajectory_params.y_offset
78  )
79 
80  yaw = t
81 
82  else:
83  # Right turns go the opposite way across the arc, so we
84  # need to invert the angles and adjust the parametrization
85  start_angle = -start_angle
86 
87  # Calculate points using
88  # r(t) = <R * -cos(t + pi/2) + a, R * sin(t + pi/2) + b>
89  t = start_angle + angle_step
90  x = (
91  trajectory_params.turning_radius * -np.cos(t + np.pi / 2)
92  + trajectory_params.x_offset
93  )
94  y = (
95  trajectory_params.turning_radius * np.sin(t + np.pi / 2)
96  + trajectory_params.y_offset
97  )
98 
99  yaw = -t
100 
101  return x, y, yaw
102 
103  def _get_line_point(
104  self, start_point: np.array, end_point: np.array, t: float
105  ) -> Tuple[float, float]:
106  """
107  Get point on a line segment using the following parameterization.
108 
109  r(t) = p + t * (q - p)
110 
111  p = start point
112  q = end point
113 
114  Args
115  ----
116  start_point: np.array(2,)
117  Starting point of the line segment
118  end_point: np.array(2,)
119  End point of the line segment
120  t: float
121  A value between 0 - 1 that denotes where along the segment
122  to calculate the point
123 
124  Returns
125  -------
126  x: float
127  x coordinate of generated point
128  y: float
129  y coordinate of generated point
130 
131  """
132  return start_point + t * (end_point - start_point)
133 
134  def _create_path(
135  self, trajectory_params: TrajectoryParameters, primitive_resolution: float
136  ) -> Path:
137  """
138  Create the full trajectory path from the given trajectory parameters.
139 
140  Args
141  ----
142  trajectory_params: TrajectoryParameters
143  The parameters that describe the trajectory to create
144  primitive_resolution: float
145  The desired distance between sampled points along the line.
146  This value is not strictly adhered to as the path may not
147  be neatly divisible, however the spacing will be as close
148  as possible.
149 
150  Returns
151  -------
152  TrajectoryPath
153  The trajectory path described by the trajectory parameters
154 
155  """
156  number_of_steps = np.round(
157  trajectory_params.total_length / primitive_resolution
158  ).astype(int)
159  t_step = 1 / number_of_steps
160 
161  start_to_arc_dist = np.linalg.norm(trajectory_params.arc_start_point)
162 
163  transition_points = [
164  start_to_arc_dist / trajectory_params.total_length,
165  (start_to_arc_dist + trajectory_params.arc_length)
166  / trajectory_params.total_length,
167  ]
168 
169  cur_t = t_step
170 
171  xs = []
172  ys = []
173  yaws = []
174 
175  for i in range(1, number_of_steps + 1):
176 
177  # This prevents cur_t going over 1 due to rounding issues in t_step
178  cur_t = min(cur_t, 1)
179 
180  # Handle the initial straight line segment
181  if cur_t <= transition_points[0]:
182  line_t = cur_t / transition_points[0]
183  x, y = self._get_line_point_get_line_point(
184  np.array([0, 0]), trajectory_params.arc_start_point, line_t
185  )
186  yaw = trajectory_params.start_angle
187 
188  # Handle the arc
189  elif cur_t <= transition_points[1]:
190  arc_t = (cur_t - transition_points[0]) / (
191  transition_points[1] - transition_points[0]
192  )
193  x, y, yaw = self._get_arc_point_get_arc_point(trajectory_params, arc_t)
194 
195  # Handle the end straight line segment
196  else:
197  line_t = (cur_t - transition_points[1]) / (1 - transition_points[1])
198  x, y = self._get_line_point_get_line_point(
199  trajectory_params.arc_end_point, trajectory_params.end_point, line_t
200  )
201  yaw = trajectory_params.end_angle
202 
203  xs.append(x)
204  ys.append(y)
205  yaws.append(yaw)
206 
207  cur_t += t_step
208 
209  # Convert to numpy arrays
210  xs = np.array(xs)
211  ys = np.array(ys)
212  yaws = np.array(yaws)
213 
214  # The last point may be slightly off due to rounding issues
215  # so we correct the last point to be exactly the end point
216  xs[-1], ys[-1] = trajectory_params.end_point
217  yaws[-1] = trajectory_params.end_angle
218 
219  return Path(xs, ys, yaws)
220 
221  def _get_intersection_point(
222  self, m1: float, c1: float, m2: float, c2: float
223  ) -> np.array:
224  """
225  Get the intersection point of two lines.
226 
227  The two lines are described by m1 * x + c1 and m2 * x + c2.
228 
229  Args
230  ----
231  m1: float
232  Gradient of line 1
233  c1: float
234  y-intercept of line 1
235  m2: float
236  Gradient of line 2
237  c2: float
238  y-intercept of line2
239 
240  Returns
241  -------
242  np.array (2,)
243  The intersection point of line 1 and 2
244 
245  """
246 
247  def line1(x):
248  return m1 * x + c1
249 
250  x_point = (c2 - c1) / (m1 - m2)
251 
252  return np.array([x_point, line1(x_point)])
253 
254  def _is_left_turn(self, intersection_point: np.array, end_point: np.array) -> bool:
255  """
256  Determine if a trajectory will be a left turn.
257 
258  Uses the determinant to determine whether the arc formed by the
259  intersection and end point turns left or right.
260 
261  Args
262  ----
263  intersection_point: np.array(2,)
264  The intersection point of the lines formed from the start
265  and end angles
266  end_point: np.array(2,)
267  The chosen end point of the trajectory
268 
269  Returns
270  -------
271  bool
272  True if curve turns left, false otherwise
273 
274  """
275  matrix = np.vstack([intersection_point, end_point])
276  det = np.linalg.det(matrix)
277 
278  return det >= 0
279 
280  def _is_dir_vec_correct(
281  self, point1: np.array, point2: np.array, line_angle: float
282  ) -> bool:
283  """
284  Check that the direction vector agrees with the line angle.
285 
286  The direction vector is defined as the vector from point 1 to
287  point 2.
288 
289  Args
290  ----
291  point1: np.array(2,)
292  The start point of the vector
293  point2: np.array(2,)
294  The end point of the vector
295  line_angle: float
296  The angle of a line to compare against the vector
297 
298  Returns
299  -------
300  bool
301  True if both line and vector point in same direction
302 
303  """
304  # Need to round to prevent very small values for 0
305  m = abs(np.tan(line_angle).round(5))
306 
307  if line_angle < 0:
308  m *= -1
309 
310  direction_vec_from_points = point2 - point1
311 
312  direction_vec_from_gradient = np.array([1, m])
313 
314  # Handle when line angle is in quadrant 2 or 3 and when angle is 90
315  if abs(line_angle) > np.pi / 2:
316  direction_vec_from_gradient = np.array([-1, m])
317  elif abs(line_angle) == np.pi / 2:
318  direction_vec_from_gradient = np.array([0, m])
319 
320  direction_vec_from_gradient = direction_vec_from_gradient.round(5)
321  direction_vec_from_points = direction_vec_from_points.round(5)
322 
323  if np.all(
324  np.sign(direction_vec_from_points) == np.sign(direction_vec_from_gradient)
325  ):
326  return True
327  else:
328  return False
329 
330  def _calculate_trajectory_params(
331  self, end_point: np.array, start_angle: float, end_angle: float
332  ) -> Union[TrajectoryParameters, None]:
333  """
334  Calculate the parameters for a trajectory with the desired constraints.
335 
336  The trajectory may consist of an arc and at most two line segments.
337  A straight trajectory will consist of a single line segment. Similarly,
338  a purely curving trajectory will only consist of an arc.
339 
340  Idea:
341  1. Extend a line from (0,0) with angle of start_angle
342  2. Extend a line from end_point with angle of end_angle
343  3. Compute their intersection point, I
344  4. Check that the intersection point leads to a
345  valid trajectory
346  - If I is too close to (0,0) or the end point then
347  no arc greater than the turning radius will reach
348  from (0,0) to end point
349 
350  If two segments from the same exterior point are tangent to
351  a circle then they are congruent
352 
353  Args
354  ----
355  end_point: np.array(2,)
356  The desired end point of the trajectory
357  start_angle: float
358  The start angle of the trajectory in radians
359  end_angle: float
360  The end angle of the trajectory in radians
361 
362  Returns
363  -------
364  TrajectoryParameters or None
365  If a valid trajectory exists then the Trajectory parameters
366  are returned, otherwise None
367 
368  """
369  x2, y2 = end_point
370  arc_start_point = np.array([0, 0])
371  arc_end_point = end_point
372 
373  # Find gradient of line 1 passing through (0,0) that makes an angle
374  # of start_angle with x_axis
375  m1 = np.tan(start_angle).round(5)
376 
377  # Find gradient of line 2 passing through end point that makes an angle
378  # of end_angle with x-axis
379  m2 = np.tan(end_angle).round(5)
380 
381  # Deal with lines that are parallel
382  if m1 == m2:
383  # If they are coincident (i.e. y-intercept is same) then simply
384  # return a circle with infinite radius
385  if round(-m2 * x2 + y2, 5) == 0:
386  return TrajectoryParameters.no_arc(
387  end_point=end_point, start_angle=start_angle, end_angle=end_angle
388  )
389 
390  # Deal with edge case of 90
391  elif (
392  abs(start_angle) == np.pi / 2 and arc_end_point[0] == arc_start_point[0]
393  ):
394  return TrajectoryParameters.no_arc(
395  end_point=end_point,
396  start_angle=start_angle,
397  end_angle=end_angle,
398  )
399 
400  else:
401  logger.debug(
402  'No trajectory possible for equivalent start and '
403  + f'end angles that also passes through p = {x2, y2}'
404  )
405  return None
406 
407  # Find intersection point of lines 1 and 2
408  intersection_point = self._get_intersection_point_get_intersection_point(m1, 0, m2, -m2 * x2 + y2)
409 
410  # Check that the vector from (0,0) to intersection point agrees
411  # with the angle of line 1
412  if not self._is_dir_vec_correct_is_dir_vec_correct(
413  arc_start_point, intersection_point, start_angle
414  ):
415  logger.debug(
416  'No trajectory possible since intersection point occurs '
417  + 'before start point on line 1'
418  )
419  return None
420 
421  # Check that the vector from intersection point to arc start point agrees with
422  # the angle of line 2
423  if not self._is_dir_vec_correct_is_dir_vec_correct(intersection_point, arc_end_point, end_angle):
424  logger.debug(
425  'No trajectory possible since intersection point occurs '
426  + 'after end point on line 2'
427  )
428  return None
429 
430  # Calculate distance between arc start point and intersection point
431  dist_a = round(np.linalg.norm(arc_start_point - intersection_point), 5)
432 
433  # Calculate distance between arc end point and intersection point
434  dist_b = round(np.linalg.norm(arc_end_point - intersection_point), 5)
435 
436  # Calculate the angle between start angle and end angle lines
437  angle_between_lines = np.pi - abs(end_angle - start_angle)
438 
439  # The closer the arc start and end points are to the intersection point the
440  # smaller the turning radius will be. However, we have a constraint on how
441  # small this turning radius can get. To calculate the minimum allowed
442  # distance we draw a right triangle with height equal to the constrained
443  # radius and angle opposite the height leg as half the angle between the
444  # start and end angle lines. The minimum valid distance is then the
445  # base of this triangle.
446  min_valid_distance = round(
447  self.turning_radiusturning_radius / np.tan(angle_between_lines / 2), 5
448  )
449 
450  # Both the distance of p along line 2 and intersection point along
451  # line 1 must be greater than the minimum valid distance
452  if dist_a < min_valid_distance or dist_b < min_valid_distance:
453  logger.debug(
454  'No trajectory possible where radius is larger than '
455  + 'minimum turning radius'
456  )
457  return None
458 
459  if dist_a < dist_b:
460  # Find new point on line 2 that is equidistant away from
461  # intersection point as arc start point is on line 1
462  vec_line2 = arc_end_point - intersection_point
463  vec_line2 /= np.linalg.norm(vec_line2)
464  arc_end_point = intersection_point + dist_a * vec_line2
465 
466  elif dist_a > dist_b:
467  # Find new point on line 1 that is equidistant away from
468  # intersection point as arc end point is on line 2
469  vec_line1 = arc_start_point - intersection_point
470  vec_line1 /= np.linalg.norm(vec_line1)
471 
472  arc_start_point = intersection_point + dist_b * vec_line1
473 
474  x1, y1 = arc_start_point
475  x2, y2 = arc_end_point
476 
477  # Find intersection point of the perpindicular lines of line 1 and 2
478  # that pass through arc start and arc end point respectively
479  if m1 == 0:
480  # If line 1 has gradient 0 then it is the x-axis.
481 
482  def perp_line2(x):
483  return -1 / m2 * (x - x2) + y2
484 
485  circle_center = np.array([x1, perp_line2(x1)])
486  elif m2 == 0:
487 
488  def perp_line1(x):
489  return -1 / m1 * (x - x1) + y1
490 
491  circle_center = np.array([x2, perp_line1(x2)])
492  else:
493  perp_m1 = -1 / m1 if m1 != 0 else 0
494  perp_m2 = -1 / m2 if m2 != 0 else 0
495 
496  circle_center = self._get_intersection_point_get_intersection_point(
497  perp_m1, -perp_m1 * x1 + y1, perp_m2, -perp_m2 * x2 + y2
498  )
499 
500  # The circles radius is the length from the center to arc start/end point
501  # (both distances are the same)
502  radius = np.linalg.norm(circle_center - arc_end_point).round(5)
503  x_offset = circle_center[0].round(5)
504  y_offset = circle_center[1].round(5)
505 
506  if radius < self.turning_radiusturning_radius:
507  logger.debug(
508  'Calculated circle radius is smaller than allowed turning '
509  + f'radius: r = {radius}, min_radius = {self.turning_radius}'
510  )
511  return None
512 
513  left_turn = self._is_left_turn_is_left_turn(intersection_point, end_point)
514 
515  return TrajectoryParameters(
516  radius,
517  x_offset,
518  y_offset,
519  end_point,
520  start_angle,
521  end_angle,
522  left_turn,
523  arc_start_point,
524  arc_end_point,
525  )
526 
528  self,
529  end_point: np.array,
530  start_angle: float,
531  end_angle: float,
532  primitive_resolution: float,
533  ) -> Union[Trajectory, None]:
534  """
535  Create a trajectory from (0,0, start_angle) to (end_point, end_angle).
536 
537  The trajectory will consist of a path that contains discrete points
538  that are spaced primitive_resolution apart.
539 
540  Args
541  ----
542  end_point: np.array(2,)
543  The desired end point of the trajectory
544  start_angle: float
545  The start angle of the trajectory in radians
546  end_angle: float
547  The end angle of the trajectory in radians
548  primitive_resolution: float
549  The spacing between points along the trajectory
550 
551  Returns
552  -------
553  Trajectory or None
554  If a valid trajectory exists then the Trajectory is returned,
555  otherwise None
556 
557  """
558  trajectory_params = self._calculate_trajectory_params_calculate_trajectory_params(
559  end_point, start_angle, end_angle
560  )
561 
562  if trajectory_params is None:
563  return None
564 
565  logger.debug('Trajectory found')
566 
567  trajectory_path = self._create_path_create_path(trajectory_params, primitive_resolution)
568 
569  return Trajectory(trajectory_path, trajectory_params)
bool _is_dir_vec_correct(self, np.array point1, np.array point2, float line_angle)
Union[TrajectoryParameters, None] _calculate_trajectory_params(self, np.array end_point, float start_angle, float end_angle)
Tuple[float, float] _get_line_point(self, np.array start_point, np.array end_point, float t)
bool _is_left_turn(self, np.array intersection_point, np.array end_point)
Union[Trajectory, None] generate_trajectory(self, np.array end_point, float start_angle, float end_angle, float primitive_resolution)
np.array _get_intersection_point(self, float m1, float c1, float m2, float c2)
Tuple[float, float, float] _get_arc_point(self, TrajectoryParameters trajectory_params, float t)
Path _create_path(self, TrajectoryParameters trajectory_params, float primitive_resolution)