Nav2 Navigation Stack - humble  humble
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  def line1(x):
247  return m1 * x + c1
248 
249  x_point = (c2 - c1) / (m1 - m2)
250 
251  return np.array([x_point, line1(x_point)])
252 
253  def _is_left_turn(self, intersection_point: np.array, end_point: np.array) -> bool:
254  """
255  Determine if a trajectory will be a left turn.
256 
257  Uses the determinant to determine whether the arc formed by the
258  intersection and end point turns left or right.
259 
260  Args
261  ----
262  intersection_point: np.array(2,)
263  The intersection point of the lines formed from the start
264  and end angles
265  end_point: np.array(2,)
266  The chosen end point of the trajectory
267 
268  Returns
269  -------
270  bool
271  True if curve turns left, false otherwise
272 
273  """
274  matrix = np.vstack([intersection_point, end_point])
275  det = np.linalg.det(matrix)
276 
277  return det >= 0
278 
279  def _is_dir_vec_correct(
280  self, point1: np.array, point2: np.array, line_angle: float
281  ) -> bool:
282  """
283  Check that the direction vector agrees with the line angle.
284 
285  The direction vector is defined as the vector from point 1 to
286  point 2.
287 
288  Args
289  ----
290  point1: np.array(2,)
291  The start point of the vector
292  point2: np.array(2,)
293  The end point of the vector
294  line_angle: float
295  The angle of a line to compare against the vector
296 
297  Returns
298  -------
299  bool
300  True if both line and vector point in same direction
301 
302  """
303  # Need to round to prevent very small values for 0
304  m = abs(np.tan(line_angle).round(5))
305 
306  if line_angle < 0:
307  m *= -1
308 
309  direction_vec_from_points = point2 - point1
310 
311  direction_vec_from_gradient = np.array([1, m])
312 
313  # Handle when line angle is in quadrant 2 or 3 and when angle is 90
314  if abs(line_angle) > np.pi / 2:
315  direction_vec_from_gradient = np.array([-1, m])
316  elif abs(line_angle) == np.pi / 2:
317  direction_vec_from_gradient = np.array([0, m])
318 
319  direction_vec_from_gradient = direction_vec_from_gradient.round(5)
320  direction_vec_from_points = direction_vec_from_points.round(5)
321 
322  if np.all(
323  np.sign(direction_vec_from_points) == np.sign(direction_vec_from_gradient)
324  ):
325  return True
326  else:
327  return False
328 
329  def _calculate_trajectory_params(
330  self, end_point: np.array, start_angle: float, end_angle: float
331  ) -> Union[TrajectoryParameters, None]:
332  """
333  Calculate the parameters for a trajectory with the desired constraints.
334 
335  The trajectory may consist of an arc and at most two line segments.
336  A straight trajectory will consist of a single line segment. Similarly,
337  a purely curving trajectory will only consist of an arc.
338 
339  Idea:
340  1. Extend a line from (0,0) with angle of start_angle
341  2. Extend a line from end_point with angle of end_angle
342  3. Compute their intersection point, I
343  4. Check that the intersection point leads to a
344  valid trajectory
345  - If I is too close to (0,0) or the end point then
346  no arc greater than the turning radius will reach
347  from (0,0) to end point
348 
349  If two segments from the same exterior point are tangent to
350  a circle then they are congruent
351 
352  Args
353  ----
354  end_point: np.array(2,)
355  The desired end point of the trajectory
356  start_angle: float
357  The start angle of the trajectory in radians
358  end_angle: float
359  The end angle of the trajectory in radians
360 
361  Returns
362  -------
363  TrajectoryParameters or None
364  If a valid trajectory exists then the Trajectory parameters
365  are returned, otherwise None
366 
367  """
368  x2, y2 = end_point
369  arc_start_point = np.array([0, 0])
370  arc_end_point = end_point
371 
372  # Find gradient of line 1 passing through (0,0) that makes an angle
373  # of start_angle with x_axis
374  m1 = np.tan(start_angle).round(5)
375 
376  # Find gradient of line 2 passing through end point that makes an angle
377  # of end_angle with x-axis
378  m2 = np.tan(end_angle).round(5)
379 
380  # Deal with lines that are parallel
381  if m1 == m2:
382  # If they are coincident (i.e. y-intercept is same) then simply
383  # return a circle with infinite radius
384  if round(-m2 * x2 + y2, 5) == 0:
385  return TrajectoryParameters.no_arc(
386  end_point=end_point, start_angle=start_angle, end_angle=end_angle
387  )
388 
389  # Deal with edge case of 90
390  elif (
391  abs(start_angle) == np.pi / 2 and arc_end_point[0] == arc_start_point[0]
392  ):
393  return TrajectoryParameters.no_arc(
394  end_point=end_point,
395  start_angle=start_angle,
396  end_angle=end_angle,
397  )
398 
399  else:
400  logger.debug(
401  'No trajectory possible for equivalent start and '
402  + f'end angles that also passes through p = {x2, y2}'
403  )
404  return None
405 
406  # Find intersection point of lines 1 and 2
407  intersection_point = self._get_intersection_point_get_intersection_point(m1, 0, m2, -m2 * x2 + y2)
408 
409  # Check that the vector from (0,0) to intersection point agrees
410  # with the angle of line 1
411  if not self._is_dir_vec_correct_is_dir_vec_correct(
412  arc_start_point, intersection_point, start_angle
413  ):
414  logger.debug(
415  'No trajectory possible since intersection point occurs '
416  + 'before start point on line 1'
417  )
418  return None
419 
420  # Check that the vector from intersection point to arc start point agrees with
421  # the angle of line 2
422  if not self._is_dir_vec_correct_is_dir_vec_correct(intersection_point, arc_end_point, end_angle):
423  logger.debug(
424  'No trajectory possible since intersection point occurs '
425  + 'after end point on line 2'
426  )
427  return None
428 
429  # Calculate distance between arc start point and intersection point
430  dist_a = round(np.linalg.norm(arc_start_point - intersection_point), 5)
431 
432  # Calculate distance between arc end point and intersection point
433  dist_b = round(np.linalg.norm(arc_end_point - intersection_point), 5)
434 
435  # Calculate the angle between start angle and end angle lines
436  angle_between_lines = np.pi - abs(end_angle - start_angle)
437 
438  # The closer the arc start and end points are to the intersection point the
439  # smaller the turning radius will be. However, we have a constraint on how
440  # small this turning radius can get. To calculate the minimum allowed
441  # distance we draw a right triangle with height equal to the constrained
442  # radius and angle opposite the height leg as half the angle between the
443  # start and end angle lines. The minimum valid distance is then the
444  # base of this triangle.
445  min_valid_distance = round(
446  self.turning_radiusturning_radius / np.tan(angle_between_lines / 2), 5
447  )
448 
449  # Both the distance of p along line 2 and intersection point along
450  # line 1 must be greater than the minimum valid distance
451  if dist_a < min_valid_distance or dist_b < min_valid_distance:
452  logger.debug(
453  'No trajectory possible where radius is larger than '
454  + 'minimum turning radius'
455  )
456  return None
457 
458  if dist_a < dist_b:
459  # Find new point on line 2 that is equidistant away from
460  # intersection point as arc start point is on line 1
461  vec_line2 = arc_end_point - intersection_point
462  vec_line2 /= np.linalg.norm(vec_line2)
463  arc_end_point = intersection_point + dist_a * vec_line2
464 
465  elif dist_a > dist_b:
466  # Find new point on line 1 that is equidistant away from
467  # intersection point as arc end point is on line 2
468  vec_line1 = arc_start_point - intersection_point
469  vec_line1 /= np.linalg.norm(vec_line1)
470 
471  arc_start_point = intersection_point + dist_b * vec_line1
472 
473  x1, y1 = arc_start_point
474  x2, y2 = arc_end_point
475 
476  # Find intersection point of the perpindicular lines of line 1 and 2
477  # that pass through arc start and arc end point respectively
478  if m1 == 0:
479  # If line 1 has gradient 0 then it is the x-axis.
480 
481  def perp_line2(x):
482  return -1 / m2 * (x - x2) + y2
483 
484  circle_center = np.array([x1, perp_line2(x1)])
485  elif m2 == 0:
486 
487  def perp_line1(x):
488  return -1 / m1 * (x - x1) + y1
489 
490  circle_center = np.array([x2, perp_line1(x2)])
491  else:
492  perp_m1 = -1 / m1 if m1 != 0 else 0
493  perp_m2 = -1 / m2 if m2 != 0 else 0
494 
495  circle_center = self._get_intersection_point_get_intersection_point(
496  perp_m1, -perp_m1 * x1 + y1, perp_m2, -perp_m2 * x2 + y2
497  )
498 
499  # The circles radius is the length from the center to arc start/end point
500  # (both distances are the same)
501  radius = np.linalg.norm(circle_center - arc_end_point).round(5)
502  x_offset = circle_center[0].round(5)
503  y_offset = circle_center[1].round(5)
504 
505  if radius < self.turning_radiusturning_radius:
506  logger.debug(
507  'Calculated circle radius is smaller than allowed turning '
508  + f'radius: r = {radius}, min_radius = {self.turning_radius}'
509  )
510  return None
511 
512  left_turn = self._is_left_turn_is_left_turn(intersection_point, end_point)
513 
514  return TrajectoryParameters(
515  radius,
516  x_offset,
517  y_offset,
518  end_point,
519  start_angle,
520  end_angle,
521  left_turn,
522  arc_start_point,
523  arc_end_point,
524  )
525 
527  self,
528  end_point: np.array,
529  start_angle: float,
530  end_angle: float,
531  primitive_resolution: float,
532  ) -> Union[Trajectory, None]:
533  """
534  Create a trajectory from (0,0, start_angle) to (end_point, end_angle).
535 
536  The trajectory will consist of a path that contains discrete points
537  that are spaced primitive_resolution apart.
538 
539  Args
540  ----
541  end_point: np.array(2,)
542  The desired end point of the trajectory
543  start_angle: float
544  The start angle of the trajectory in radians
545  end_angle: float
546  The end angle of the trajectory in radians
547  primitive_resolution: float
548  The spacing between points along the trajectory
549 
550  Returns
551  -------
552  Trajectory or None
553  If a valid trajectory exists then the Trajectory is returned,
554  otherwise None
555 
556  """
557  trajectory_params = self._calculate_trajectory_params_calculate_trajectory_params(
558  end_point, start_angle, end_angle
559  )
560 
561  if trajectory_params is None:
562  return None
563 
564  logger.debug('Trajectory found')
565 
566  trajectory_path = self._create_path_create_path(trajectory_params, primitive_resolution)
567 
568  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)