Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
lattice_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 from collections import defaultdict
16 from concurrent.futures import ProcessPoolExecutor
17 from enum import Enum
18 from functools import partial
19 import os
20 from typing import Any, cast, Dict, List, Tuple, TypedDict
21 
22 from nav2_smac_planner.lattice_primitives.helper import angle_difference, interpolate_yaws
23 from nav2_smac_planner.lattice_primitives.trajectory import (AnyFloat, FloatNDArray, Path,
24  Trajectory, TrajectoryParameters)
25 from nav2_smac_planner.lattice_primitives.trajectory_generator import TrajectoryGenerator
26 import numpy as np
27 from rtree import index
28 
29 
30 class ConfigDict(TypedDict):
31  grid_resolution: float
32  turning_radius: float
33  stopping_threshold: int
34  num_of_headings: int
35  motion_model: str
36  trajectory_distinctness_ratio: float
37 
38 
40  """
41  Handles all the logic for computing the minimal control set.
42 
43  Computes the minimal control set for a vehicle given its parameters.
44  Includes handling the propagating and searching along wavefronts as
45  well as determining if a trajectory is part of the minimal set based
46  on previously added trajectories.
47  """
48 
49  class MotionModel(Enum):
50  """An Enum used for determining the motion model to use."""
51 
52  ACKERMANN = 1
53  DIFF = 2
54  OMNI = 3
55 
56  class Flip(Enum):
57  """An Enum used for determining how a trajectory should be flipped."""
58 
59  X = 1
60  Y = 2
61  BOTH = 3
62 
63  def __init__(self, config: ConfigDict):
64  """Init the lattice generator from the user supplied config."""
65  self.trajectory_generatortrajectory_generator = TrajectoryGenerator(config)
66  self.grid_resolutiongrid_resolution = config['grid_resolution']
67  self.turning_radiusturning_radius = config['turning_radius']
68  self.stopping_thresholdstopping_threshold = config['stopping_threshold']
69  self.num_of_headingsnum_of_headings = config['num_of_headings']
70  self.trajectory_distinctness_ratiotrajectory_distinctness_ratio = config['trajectory_distinctness_ratio']
71  self.headingsheadings = self._get_heading_discretization_get_heading_discretization(config['num_of_headings'])
72 
73  self.motion_modelmotion_model = self.MotionModelMotionModel[config['motion_model'].upper()]
74 
75  self.DISTANCE_THRESHOLDDISTANCE_THRESHOLD = 0.5 * self.grid_resolutiongrid_resolution
76  self.ROTATION_THRESHOLDROTATION_THRESHOLD = 0.5 * (2 * np.pi / self.num_of_headingsnum_of_headings)
77 
78  def _get_wave_front_points(self, pos: int) -> FloatNDArray:
79  """
80  Calculate the end points that lie on the wave front.
81 
82  Uses the user supplied grid resolution to calculate the
83  valid end points that lie on a wave front at a discrete
84  interval away from the origin.
85 
86  Args:
87  pos: int
88  The number of discrete intervals of grid resolution
89  away from the origin to generate the wave points at
90 
91  Returns
92  -------
93  np.array
94  An array of coordinates
95 
96  """
97  positions = []
98 
99  max_point_coord = self.grid_resolutiongrid_resolution * pos
100 
101  for i in range(pos):
102  varying_point_coord = self.grid_resolutiongrid_resolution * i
103 
104  # Change the y and keep x at max
105  positions.append((max_point_coord, varying_point_coord))
106 
107  # Change the x and keep y at max
108  positions.append((varying_point_coord, max_point_coord))
109 
110  # Append the corner
111  positions.append((max_point_coord, max_point_coord))
112 
113  return np.array(positions)
114 
115  def _get_heading_discretization(self, number_of_headings: int) -> List[float]:
116  """
117  Calculate the heading discretization based on the number of headings.
118 
119  Does not uniformly generate headings but instead generates a set of
120  discrete headings that is better suited for straight line trajectories.
121 
122  Args:
123  number_of_headings: int
124  The number of headings to discretize a 360 degree turn into
125 
126  Returns
127  -------
128  list
129  A list of headings in radians
130 
131  """
132  max_val = int(number_of_headings / 8)
133 
134  outer_edge_x = []
135  outer_edge_y = []
136 
137  # Generate points that lie on the perimeter of the surface
138  # of a square with sides of length max_val
139  for i in range(-max_val, max_val + 1):
140  outer_edge_x.extend([i, i])
141  outer_edge_y.extend([-max_val, max_val])
142 
143  if i != max_val and i != -max_val:
144  outer_edge_x.extend([-max_val, max_val])
145  outer_edge_y.extend([i, i])
146 
147  return sorted([np.arctan2(j, i) for i, j in zip(outer_edge_x, outer_edge_y)])
148 
149  def _point_to_line_distance(self, p1: FloatNDArray, p2: FloatNDArray,
150  q: FloatNDArray) -> AnyFloat:
151  """
152  Return the shortest distance from a point to a line segment.
153 
154  Args:
155  p1: np.array(2,)
156  Start point of line segment
157  p2: np.array(2,)
158  End point of line segment
159  q: np.array(2,)
160  Point to get distance away from line of
161 
162  Returns
163  -------
164  float
165  The shortest distance between q and line segment p1p2
166 
167  """
168  # Get back the l2-norm without the square root
169  l2 = np.inner(p1 - p2, p1 - p2)
170 
171  if l2 == 0:
172  return np.linalg.norm(p1 - q)
173 
174  # Ensure t lies in [0, 1]
175  t = max(0, min(1, np.dot(q - p1, p2 - p1) / l2))
176  projected_point = p1 + t * (p2 - p1)
177 
178  return np.linalg.norm(q - projected_point)
179 
180  def _is_minimal_trajectory(
181  self, trajectory: Trajectory, prior_end_poses: index.Rtree,
182  trajectories_by_heading: Dict[float, List[Tuple[Any, float]]],
183  target_point: FloatNDArray, target_heading: float,
184  ) -> bool:
185  """
186  Determine whether a trajectory is a minimal trajectory.
187 
188  Uses an RTree for speedup.
189 
190  Args:
191  trajectory: Trajectory
192  The trajectory to check
193  prior_end_poses: RTree
194  An RTree holding the current minimal set of trajectories
195 
196  Returns
197  -------
198  bool
199  True if the trajectory is a minimal trajectory otherwise false
200 
201  """
202  # Iterate over line segments in the trajectory
203  for x1, y1, x2, y2, yaw in zip(
204  trajectory.path.xs[:-1],
205  trajectory.path.ys[:-1],
206  trajectory.path.xs[1:],
207  trajectory.path.ys[1:],
208  trajectory.path.yaws[:-1],
209  ):
210 
211  p1 = np.array([x1, y1])
212  p2 = np.array([x2, y2])
213 
214  # Create a bounding box search region
215  # around the line segment
216  left_bb = min(x1, x2) - self.DISTANCE_THRESHOLDDISTANCE_THRESHOLD
217  right_bb = max(x1, x2) + self.DISTANCE_THRESHOLDDISTANCE_THRESHOLD
218  top_bb = max(y1, y2) + self.DISTANCE_THRESHOLDDISTANCE_THRESHOLD
219  bottom_bb = min(y1, y2) - self.DISTANCE_THRESHOLDDISTANCE_THRESHOLD
220 
221  # For any previous end points in the search region we
222  # check the distance to that point and the angle
223  # difference. If they are within threshold then this
224  # trajectory can be composed from a previous trajectory
225  for prior_end_pose in prior_end_poses.intersection(
226  (left_bb, bottom_bb, right_bb, top_bb), objects='raw'
227  ):
228  pose = cast(FloatNDArray, prior_end_pose)
229  if (
230  self._point_to_line_distance_point_to_line_distance(p1, p2, pose[:-1])
231  < self.DISTANCE_THRESHOLDDISTANCE_THRESHOLD
232  and angle_difference(yaw, pose[-1])
233  < self.ROTATION_THRESHOLDROTATION_THRESHOLD
234  ):
235  return False
236 
237  current_length = float(trajectory.parameters.total_length)
238  # Iterate through the local trajectories with the same target heading
239  for prev_pos in trajectories_by_heading[target_heading]:
240  dist = np.linalg.norm(target_point - prev_pos)
241  if dist < self.trajectory_distinctness_ratiotrajectory_distinctness_ratio * float(current_length):
242  return False
243 
244  return True
245 
246  def _compute_min_trajectory_length(self) -> float:
247  """
248  Compute the minimum trajectory length for the given parameters.
249 
250  The minimum trajectory length is defined as the length needed
251  for the sharpest possible turn to move from 0 degrees to the next
252  discrete heading. Since the distance between headings is not uniform
253  we take the smallest possible difference.
254 
255  Returns
256  -------
257  float
258  The minimal length of a trajectory
259 
260  """
261  # Compute arc length for a turn that moves from 0 degrees to
262  # the minimum heading difference
263  heading_diff = [
264  abs(self.headingsheadings[i + 1] - self.headingsheadings[i])
265  for i in range(len(self.headingsheadings) - 1)
266  ]
267 
268  return self.turning_radiusturning_radius * min(heading_diff)
269 
270  def _generate_minimal_spanning_set(self) -> Dict[float, List[Trajectory]]:
271  """
272  Generate the minimal spanning set.
273 
274  Iteratves over all possible trajectories and keeps only those that
275  are part of the minimal set.
276 
277  Returns
278  -------
279  dict
280  A dictionary where the key is the start_angle and the value is
281  a list of trajectories that begin at that angle
282 
283  """
284  quadrant1_end_poses: Dict[float, List[Tuple[Any, float]]] = defaultdict(list)
285 
286  # Since we only compute for quadrant 1 we only need headings between
287  # 0 and 90 degrees
288  initial_headings = sorted(
289  filter(lambda x: 0 <= x and x <= np.pi / 2, self.headingsheadings)
290  )
291 
292  num_cpus = os.cpu_count() or 1
293  max_workers = min(len(initial_headings), max(1, num_cpus - 1))
294 
295  compute_func = partial(self._compute_for_single_heading_compute_for_single_heading)
296  with ProcessPoolExecutor(max_workers=max_workers) as executor:
297  # compute results in parallel
298  results = list(executor.map(compute_func, initial_headings))
299  for heading, trajectories in results:
300  quadrant1_end_poses[heading] = trajectories
301 
302  # Once we have found the minimal trajectory set for quadrant 1
303  # we can leverage symmetry to create the complete minimal set
304  return self._create_complete_minimal_spanning_set_create_complete_minimal_spanning_set(quadrant1_end_poses)
305 
306  def _compute_for_single_heading(self, start_heading: float
307  ) -> Tuple[float, List[Tuple[FloatNDArray, float]]]:
308  """
309  Compute the minimal trajectory set for a specific starting heading.
310 
311  This function runs in a separate process.
312 
313  Args
314  ----
315  start_heading: float
316  The initial heading angle in radians
317 
318  Returns
319  -------
320  Dict[float, List[Tuple[FloatNDArray, float]]]
321  The start_heading and the list of discovered minimal trajectories
322 
323  """
324  trajectories_by_heading: Dict[float, List[Tuple[FloatNDArray, float]]] = defaultdict(list)
325  local_trajectories: List[Tuple[FloatNDArray, float]] = []
326  prior_end_poses = index.Index()
327  # Use the minimum trajectory length to find the starting wave front
328  min_trajectory_length = self._compute_min_trajectory_length_compute_min_trajectory_length()
329  wave_front_start_pos = int(
330  np.round(min_trajectory_length / self.grid_resolutiongrid_resolution)
331  )
332  wave_front_cur_pos = wave_front_start_pos
333  iterations_without_trajectory = 0
334 
335  # To get target headings: sort headings radially and remove those
336  # that are more than 90 degrees away
337  target_headings = sorted(
338  self.headingsheadings, key=lambda x: (abs(x - start_heading), -x)
339  )
340  target_headings = list(
341  filter(lambda x: abs(start_heading - x) <= np.pi / 2, target_headings)
342  )
343 
344  while iterations_without_trajectory < self.stopping_thresholdstopping_threshold:
345  iterations_without_trajectory += 1
346 
347  # Generate x,y coordinates for current wave front
348  positions = self._get_wave_front_points_get_wave_front_points(wave_front_cur_pos)
349 
350  for target_point in positions:
351  for target_heading in target_headings:
352  # Use 10% of grid separation for finer granularity
353  # when checking if trajectory overlaps another already
354  # seen trajectory
355  trajectory = self.trajectory_generatortrajectory_generator.generate_trajectory(
356  target_point,
357  start_heading,
358  target_heading,
359  0.1 * self.grid_resolutiongrid_resolution,
360  )
361 
362  if trajectory is not None:
363  # Check if path overlaps something in minimal
364  # spanning set
365  if self._is_minimal_trajectory_is_minimal_trajectory(trajectory, prior_end_poses,
366  trajectories_by_heading, target_point,
367  target_heading):
368  trajectories_by_heading[target_heading].append((target_point))
369  # Add end pose to minimal set
370  new_end_pose = np.array(
371  [target_point[0], target_point[1], target_heading]
372  )
373 
374  local_trajectories.append(
375  (target_point, target_heading)
376  )
377 
378  # Create a new bounding box in the RTree
379  # for this trajectory
380  left_bb = target_point[0] - self.DISTANCE_THRESHOLDDISTANCE_THRESHOLD
381  right_bb = target_point[0] + self.DISTANCE_THRESHOLDDISTANCE_THRESHOLD
382  bottom_bb = target_point[1] - self.DISTANCE_THRESHOLDDISTANCE_THRESHOLD
383  top_bb = target_point[1] + self.DISTANCE_THRESHOLDDISTANCE_THRESHOLD
384 
385  prior_end_poses.insert(
386  0,
387  (left_bb, bottom_bb, right_bb, top_bb),
388  new_end_pose,
389  )
390 
391  iterations_without_trajectory = 0
392 
393  wave_front_cur_pos += 1
394 
395  return start_heading, local_trajectories
396 
397  def _flip_angle(self, angle: float, flip_type: Flip) -> float:
398  """
399  Return the the appropriate flip of the angle in self.headings.
400 
401  Args:
402  angle: float
403  The angle to flip
404  flip_type: Flip
405  Whether to flip acrpss X axis, Y axis, or both
406 
407  Returns
408  -------
409  float
410  The angle in self.heading that is the appropriate flip
411 
412  """
413  angle_idx = self.headingsheadings.index(angle)
414 
415  if flip_type == self.FlipFlip.X:
416  heading_idx = (self.num_of_headingsnum_of_headings / 2 - 1) - angle_idx - 1
417  elif flip_type == self.FlipFlip.Y:
418  heading_idx = self.num_of_headingsnum_of_headings - angle_idx - 2
419  elif flip_type == self.FlipFlip.BOTH:
420  heading_idx = (
421  angle_idx - (self.num_of_headingsnum_of_headings / 2)
422  ) % self.num_of_headingsnum_of_headings
423  else:
424  raise Exception(f'Unsupported flip type: {flip_type}')
425 
426  return self.headingsheadings[int(heading_idx)]
427 
428  def _create_complete_minimal_spanning_set(
429  self, single_quadrant_minimal_set: Dict[float, List[Tuple[Any, float]]]
430  ) -> Dict[float, List[Trajectory]]:
431  """
432  Create the full minimal spanning set from a single quadrant set.
433 
434  Exploits the symmetry between the quadrants to create the full set.
435  This is done by flipping every trajectory in the first quadrant across
436  either the X-axis, Y-axis, or both axes.
437 
438  Args:
439  single_quadrant_minimal_set: dict
440  The minimal set for quadrant 1 (positive x and positive y)
441 
442  Returns
443  -------
444  dict
445  The complete minimal spanning set containing the trajectories
446  in all quadrants
447 
448  """
449  all_trajectories: Dict[float, List[Trajectory]] = defaultdict(list)
450 
451  for start_angle in single_quadrant_minimal_set.keys():
452 
453  for end_point, end_angle in single_quadrant_minimal_set[start_angle]:
454 
455  x, y = end_point
456 
457  # Prevent double adding trajectories that lie on axes
458  # (i.e. start and end angle are either both 0 or both pi/2)
459  if start_angle == 0 and end_angle == 0:
460  unflipped_start_angle = 0.0
461  flipped_x_start_angle = np.pi
462 
463  unflipped_end_angle = 0.0
464  flipped_x_end_angle = np.pi
465 
466  # Generate trajectories from calculated parameters
467  unflipped_trajectory = (
468  self.trajectory_generatortrajectory_generator.generate_trajectory(
469  np.array([x, y]),
470  unflipped_start_angle,
471  unflipped_end_angle,
472  self.grid_resolutiongrid_resolution,
473  )
474  )
475  flipped_x_trajectory = (
476  self.trajectory_generatortrajectory_generator.generate_trajectory(
477  np.array([-x, -y]),
478  flipped_x_start_angle,
479  flipped_x_end_angle,
480  self.grid_resolutiongrid_resolution,
481  )
482  )
483 
484  if unflipped_trajectory is None or flipped_x_trajectory is None:
485  raise ValueError('No trajectory was found')
486 
487  all_trajectories[
488  unflipped_trajectory.parameters.start_angle
489  ].append(unflipped_trajectory)
490 
491  all_trajectories[
492  flipped_x_trajectory.parameters.start_angle
493  ].append(flipped_x_trajectory)
494 
495  elif abs(start_angle) == np.pi / 2 and abs(end_angle) == np.pi / 2:
496  unflipped_start_angle = np.pi / 2
497  flipped_y_start_angle = -np.pi / 2
498 
499  unflipped_end_angle = np.pi / 2
500  flipped_y_end_angle = -np.pi / 2
501 
502  # Generate trajectories from calculated parameters
503  unflipped_trajectory = (
504  self.trajectory_generatortrajectory_generator.generate_trajectory(
505  np.array([-x, y]),
506  unflipped_start_angle,
507  unflipped_end_angle,
508  self.grid_resolutiongrid_resolution,
509  )
510  )
511 
512  flipped_y_trajectory = (
513  self.trajectory_generatortrajectory_generator.generate_trajectory(
514  np.array([x, -y]),
515  flipped_y_start_angle,
516  flipped_y_end_angle,
517  self.grid_resolutiongrid_resolution,
518  )
519  )
520 
521  if unflipped_trajectory is None or flipped_y_trajectory is None:
522  raise ValueError('No trajectory was found')
523 
524  all_trajectories[
525  unflipped_trajectory.parameters.start_angle
526  ].append(unflipped_trajectory)
527  all_trajectories[
528  flipped_y_trajectory.parameters.start_angle
529  ].append(flipped_y_trajectory)
530 
531  else:
532  unflipped_start_angle = start_angle
533  flipped_x_start_angle = self._flip_angle_flip_angle(start_angle, self.FlipFlip.X)
534  flipped_y_start_angle = self._flip_angle_flip_angle(start_angle, self.FlipFlip.Y)
535  flipped_xy_start_angle = self._flip_angle_flip_angle(
536  start_angle, self.FlipFlip.BOTH
537  )
538 
539  unflipped_end_angle = end_angle
540  flipped_x_end_angle = self._flip_angle_flip_angle(end_angle, self.FlipFlip.X)
541  flipped_y_end_angle = self._flip_angle_flip_angle(end_angle, self.FlipFlip.Y)
542  flipped_xy_end_angle = self._flip_angle_flip_angle(end_angle, self.FlipFlip.BOTH)
543 
544  # Generate trajectories from calculated parameters
545  unflipped_trajectory = (
546  self.trajectory_generatortrajectory_generator.generate_trajectory(
547  np.array([x, y]),
548  unflipped_start_angle,
549  unflipped_end_angle,
550  self.grid_resolutiongrid_resolution,
551  )
552  )
553  flipped_x_trajectory = (
554  self.trajectory_generatortrajectory_generator.generate_trajectory(
555  np.array([-x, y]),
556  flipped_x_start_angle,
557  flipped_x_end_angle,
558  self.grid_resolutiongrid_resolution,
559  )
560  )
561  flipped_y_trajectory = (
562  self.trajectory_generatortrajectory_generator.generate_trajectory(
563  np.array([x, -y]),
564  flipped_y_start_angle,
565  flipped_y_end_angle,
566  self.grid_resolutiongrid_resolution,
567  )
568  )
569  flipped_xy_trajectory = (
570  self.trajectory_generatortrajectory_generator.generate_trajectory(
571  np.array([-x, -y]),
572  flipped_xy_start_angle,
573  flipped_xy_end_angle,
574  self.grid_resolutiongrid_resolution,
575  )
576  )
577 
578  if (unflipped_trajectory is None or flipped_y_trajectory is None or
579  flipped_x_trajectory is None or flipped_xy_trajectory is None):
580  raise ValueError('No trajectory was found')
581 
582  all_trajectories[
583  unflipped_trajectory.parameters.start_angle
584  ].append(unflipped_trajectory)
585  all_trajectories[
586  flipped_x_trajectory.parameters.start_angle
587  ].append(flipped_x_trajectory)
588  all_trajectories[
589  flipped_y_trajectory.parameters.start_angle
590  ].append(flipped_y_trajectory)
591  all_trajectories[
592  flipped_xy_trajectory.parameters.start_angle
593  ].append(flipped_xy_trajectory)
594 
595  return all_trajectories
596 
597  def _handle_motion_model(self, spanning_set: Dict[float, List[Trajectory]]
598  ) -> Dict[float, List[Trajectory]]:
599  """
600  Add the appropriate motions for the user supplied motion model.
601 
602  Ackerman: No additional trajectories
603 
604  Diff: In place turns to the right and left
605 
606  Omni: Diff + Sliding movements to right and left
607 
608  Args:
609  spanning set: dict
610  The minimal spanning set
611 
612  Returns
613  -------
614  dict
615  The minimal spanning set with additional trajectories based
616  on the motion model
617 
618  """
619  if self.motion_modelmotion_model == self.MotionModelMotionModel.ACKERMANN:
620  return spanning_set
621 
622  elif self.motion_modelmotion_model == self.MotionModelMotionModel.DIFF:
623  diff_spanning_set = self._add_in_place_turns_add_in_place_turns(spanning_set)
624  return diff_spanning_set
625 
626  elif self.motion_modelmotion_model == self.MotionModelMotionModel.OMNI:
627  omni_spanning_set = self._add_in_place_turns_add_in_place_turns(spanning_set)
628  omni_spanning_set = self._add_horizontal_motions_add_horizontal_motions(omni_spanning_set)
629  return omni_spanning_set
630 
631  else:
632  print('No handling implemented for Motion Model: ' + f'{self.motion_model}')
633  raise NotImplementedError
634 
635  def _add_in_place_turns(self, spanning_set: Dict[float, List[Trajectory]]
636  ) -> Dict[float, List[Trajectory]]:
637  """
638  Add in place turns to the spanning set.
639 
640  In place turns are trajectories with only a rotational component and
641  only shift a single angular heading step
642 
643  Args:
644  spanning_set: dict
645  The minimal spanning set
646 
647  Returns
648  -------
649  dict
650  The minimal spanning set containing additional in place turns
651  for each start angle
652 
653  """
654  all_angles = sorted(spanning_set.keys())
655 
656  for idx, start_angle in enumerate(all_angles):
657  prev_angle_idx = idx - 1 if idx - 1 >= 0 else len(all_angles) - 1
658  next_angle_idx = idx + 1 if idx + 1 < len(all_angles) else 0
659 
660  prev_angle = all_angles[prev_angle_idx]
661  next_angle = all_angles[next_angle_idx]
662 
663  left_turn_params = TrajectoryParameters.no_arc(
664  end_point=np.array([0, 0]),
665  start_angle=start_angle,
666  end_angle=next_angle,
667  )
668  right_turn_params = TrajectoryParameters.no_arc(
669  end_point=np.array([0, 0]),
670  start_angle=start_angle,
671  end_angle=prev_angle,
672  )
673 
674  # Calculate number of steps needed to rotate by roughly 10 degrees
675  # for each pose
676  angle_dif = angle_difference(start_angle, next_angle)
677  steps = int(round(angle_dif / np.deg2rad(10))) + 1
678 
679  position = np.full(steps, 0)
680  left_yaws = interpolate_yaws(start_angle, next_angle, True, steps)
681  right_yaws = interpolate_yaws(start_angle, prev_angle, False, steps)
682 
683  left_turn_path = Path(xs=position, ys=position, yaws=left_yaws)
684  right_turn_path = Path(xs=position, ys=position, yaws=right_yaws)
685 
686  left_turn = Trajectory(parameters=left_turn_params, path=left_turn_path)
687  right_turn = Trajectory(parameters=right_turn_params, path=right_turn_path)
688 
689  spanning_set[start_angle].append(left_turn)
690  spanning_set[start_angle].append(right_turn)
691 
692  return spanning_set
693 
694  def _add_horizontal_motions(self, spanning_set: Dict[float, List[Trajectory]]
695  ) -> Dict[float, List[Trajectory]]:
696  """
697  Add horizontal sliding motions to the spanning set.
698 
699  The horizontal sliding motions are simply straight line trajectories
700  at 90 degrees to every start angle in the spanning set. The yaw of these
701  trajectories is the same as the start angle for which it is generated.
702 
703  Args:
704  spanning_set: dict
705  The minimal spanning set
706 
707  Returns
708  -------
709  dict
710  The minimal spanning set containing additional sliding motions
711  for each start angle
712 
713  """
714  # Calculate the offset in the headings list that represents an
715  # angle change of 90 degrees
716  idx_offset = int(self.num_of_headingsnum_of_headings / 4)
717 
718  for idx, angle in enumerate(self.headingsheadings):
719 
720  # Copy the straight line trajectory for the start angle that
721  # is 90 degrees to the left
722  left_angle_idx = int((idx + idx_offset) % self.num_of_headingsnum_of_headings)
723  left_angle = self.headingsheadings[left_angle_idx]
724  left_trajectories = spanning_set[left_angle]
725  left_straight_trajectory = next(
726  t for t in left_trajectories if t.parameters.end_angle == left_angle
727  )
728 
729  # Copy the straight line trajectory for the start angle that
730  # is 90 degrees to the right
731  right_angle_idx = int((idx - idx_offset) % self.num_of_headingsnum_of_headings)
732  right_angle = self.headingsheadings[right_angle_idx]
733  right_trajectories = spanning_set[right_angle]
734  right_straight_trajectory = next(
735  t for t in right_trajectories if t.parameters.end_angle == right_angle
736  )
737 
738  yaws = np.full(
739  len(left_straight_trajectory.path.xs), angle, dtype=np.float64
740  )
741 
742  # Create a new set of parameters that represents
743  # the left sliding motion
744  parmas_l = left_straight_trajectory.parameters
745  left_motion_parameters = TrajectoryParameters(
746  parmas_l.turning_radius,
747  parmas_l.x_offset,
748  parmas_l.y_offset,
749  parmas_l.end_point,
750  angle,
751  angle,
752  parmas_l.left_turn,
753  parmas_l.arc_start_point,
754  parmas_l.arc_end_point,
755  )
756 
757  # Create a new set of parameters that represents
758  # the right sliding motion
759  params_r = right_straight_trajectory.parameters
760  right_motion_parameters = TrajectoryParameters(
761  params_r.turning_radius,
762  params_r.x_offset,
763  params_r.y_offset,
764  params_r.end_point,
765  angle,
766  angle,
767  params_r.left_turn,
768  parmas_l.arc_start_point,
769  parmas_l.arc_end_point,
770  )
771 
772  left_motion = Trajectory(
773  parameters=left_motion_parameters,
774  path=Path(
775  xs=left_straight_trajectory.path.xs,
776  ys=left_straight_trajectory.path.ys,
777  yaws=yaws,
778  ),
779  )
780 
781  right_motion = Trajectory(
782  parameters=right_motion_parameters,
783  path=Path(
784  xs=right_straight_trajectory.path.xs,
785  ys=right_straight_trajectory.path.ys,
786  yaws=yaws,
787  ),
788  )
789 
790  spanning_set[angle].append(left_motion)
791  spanning_set[angle].append(right_motion)
792 
793  return spanning_set
794 
795  def run(self) -> Dict[float, List[Trajectory]]:
796  """
797  Run the lattice generator.
798 
799  Returns
800  -------
801  dict
802  The minimal spanning set including additional motions for the
803  specified motion model
804 
805  """
806  complete_spanning_set = self._generate_minimal_spanning_set_generate_minimal_spanning_set()
807 
808  return self._handle_motion_model_handle_motion_model(complete_spanning_set)
List[float] _get_heading_discretization(self, int number_of_headings)
Dict[float, List[Trajectory]] _add_horizontal_motions(self, Dict[float, List[Trajectory]] spanning_set)
Tuple[float, List[Tuple[FloatNDArray, float]]] _compute_for_single_heading(self, float start_heading)
bool _is_minimal_trajectory(self, Trajectory trajectory, index.Rtree prior_end_poses, Dict[float, List[Tuple[Any, float]]] trajectories_by_heading, FloatNDArray target_point, float target_heading)
Dict[float, List[Trajectory]] _create_complete_minimal_spanning_set(self, Dict[float, List[Tuple[Any, float]]] single_quadrant_minimal_set)
Dict[float, List[Trajectory]] _add_in_place_turns(self, Dict[float, List[Trajectory]] spanning_set)
float _flip_angle(self, float angle, Flip flip_type)
Dict[float, List[Trajectory]] _generate_minimal_spanning_set(self)
AnyFloat _point_to_line_distance(self, FloatNDArray p1, FloatNDArray p2, FloatNDArray q)
Dict[float, List[Trajectory]] _handle_motion_model(self, Dict[float, List[Trajectory]] spanning_set)