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