15 from collections
import defaultdict
16 from concurrent.futures
import ProcessPoolExecutor
18 from functools
import partial
20 from typing
import Any, cast, Dict, List, Tuple, TypedDict
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
27 from rtree
import index
31 grid_resolution: float
33 stopping_threshold: int
36 trajectory_distinctness_ratio: float
41 Handles all the logic for computing the minimal control set.
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.
50 """An Enum used for determining the motion model to use."""
57 """An Enum used for determining how a trajectory should be flipped."""
64 """Init the lattice generator from the user supplied config."""
78 def _get_wave_front_points(self, pos: int) -> FloatNDArray:
80 Calculate the end points that lie on the wave front.
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.
88 The number of discrete intervals of grid resolution
89 away from the origin to generate the wave points at
94 An array of coordinates
105 positions.append((max_point_coord, varying_point_coord))
108 positions.append((varying_point_coord, max_point_coord))
111 positions.append((max_point_coord, max_point_coord))
113 return np.array(positions)
115 def _get_heading_discretization(self, number_of_headings: int) -> List[float]:
117 Calculate the heading discretization based on the number of headings.
119 Does not uniformly generate headings but instead generates a set of
120 discrete headings that is better suited for straight line trajectories.
123 number_of_headings: int
124 The number of headings to discretize a 360 degree turn into
129 A list of headings in radians
132 max_val = int(number_of_headings / 8)
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])
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])
147 return sorted([np.arctan2(j, i)
for i, j
in zip(outer_edge_x, outer_edge_y)])
149 def _point_to_line_distance(self, p1: FloatNDArray, p2: FloatNDArray,
150 q: FloatNDArray) -> AnyFloat:
152 Return the shortest distance from a point to a line segment.
156 Start point of line segment
158 End point of line segment
160 Point to get distance away from line of
165 The shortest distance between q and line segment p1p2
169 l2 = np.inner(p1 - p2, p1 - p2)
172 return np.linalg.norm(p1 - q)
175 t = max(0, min(1, np.dot(q - p1, p2 - p1) / l2))
176 projected_point = p1 + t * (p2 - p1)
178 return np.linalg.norm(q - projected_point)
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,
186 Determine whether a trajectory is a minimal trajectory.
188 Uses an RTree for speedup.
191 trajectory: Trajectory
192 The trajectory to check
193 prior_end_poses: RTree
194 An RTree holding the current minimal set of trajectories
199 True if the trajectory is a minimal trajectory otherwise false
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],
211 p1 = np.array([x1, y1])
212 p2 = np.array([x2, y2])
225 for prior_end_pose
in prior_end_poses.intersection(
226 (left_bb, bottom_bb, right_bb, top_bb), objects=
'raw'
228 pose = cast(FloatNDArray, prior_end_pose)
232 and angle_difference(yaw, pose[-1])
237 current_length = float(trajectory.parameters.total_length)
239 for prev_pos
in trajectories_by_heading[target_heading]:
240 dist = np.linalg.norm(target_point - prev_pos)
246 def _compute_min_trajectory_length(self) -> float:
248 Compute the minimum trajectory length for the given parameters.
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.
258 The minimal length of a trajectory
265 for i
in range(len(self.
headingsheadings) - 1)
270 def _generate_minimal_spanning_set(self) -> Dict[float, List[Trajectory]]:
272 Generate the minimal spanning set.
274 Iteratves over all possible trajectories and keeps only those that
275 are part of the minimal set.
280 A dictionary where the key is the start_angle and the value is
281 a list of trajectories that begin at that angle
284 quadrant1_end_poses: Dict[float, List[Tuple[Any, float]]] = defaultdict(list)
288 initial_headings = sorted(
289 filter(
lambda x: 0 <= x
and x <= np.pi / 2, self.
headingsheadings)
292 num_cpus = os.cpu_count()
or 1
293 max_workers = min(len(initial_headings), max(1, num_cpus - 1))
296 with ProcessPoolExecutor(max_workers=max_workers)
as executor:
298 results = list(executor.map(compute_func, initial_headings))
299 for heading, trajectories
in results:
300 quadrant1_end_poses[heading] = trajectories
306 def _compute_for_single_heading(self, start_heading: float
307 ) -> Tuple[float, List[Tuple[FloatNDArray, float]]]:
309 Compute the minimal trajectory set for a specific starting heading.
311 This function runs in a separate process.
316 The initial heading angle in radians
320 Dict[float, List[Tuple[FloatNDArray, float]]]
321 The start_heading and the list of discovered minimal trajectories
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()
329 wave_front_start_pos = int(
332 wave_front_cur_pos = wave_front_start_pos
333 iterations_without_trajectory = 0
337 target_headings = sorted(
338 self.
headingsheadings, key=
lambda x: (abs(x - start_heading), -x)
340 target_headings = list(
341 filter(
lambda x: abs(start_heading - x) <= np.pi / 2, target_headings)
345 iterations_without_trajectory += 1
350 for target_point
in positions:
351 for target_heading
in target_headings:
362 if trajectory
is not None:
366 trajectories_by_heading, target_point,
368 trajectories_by_heading[target_heading].append((target_point))
370 new_end_pose = np.array(
371 [target_point[0], target_point[1], target_heading]
374 local_trajectories.append(
375 (target_point, target_heading)
385 prior_end_poses.insert(
387 (left_bb, bottom_bb, right_bb, top_bb),
391 iterations_without_trajectory = 0
393 wave_front_cur_pos += 1
395 return start_heading, local_trajectories
397 def _flip_angle(self, angle: float, flip_type: Flip) -> float:
399 Return the the appropriate flip of the angle in self.headings.
405 Whether to flip acrpss X axis, Y axis, or both
410 The angle in self.heading that is the appropriate flip
413 angle_idx = self.
headingsheadings.index(angle)
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:
419 elif flip_type == self.
FlipFlip.BOTH:
424 raise Exception(f
'Unsupported flip type: {flip_type}')
426 return self.
headingsheadings[int(heading_idx)]
428 def _create_complete_minimal_spanning_set(
429 self, single_quadrant_minimal_set: Dict[float, List[Tuple[Any, float]]]
430 ) -> Dict[float, List[Trajectory]]:
432 Create the full minimal spanning set from a single quadrant set.
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.
439 single_quadrant_minimal_set: dict
440 The minimal set for quadrant 1 (positive x and positive y)
445 The complete minimal spanning set containing the trajectories
449 all_trajectories: Dict[float, List[Trajectory]] = defaultdict(list)
451 for start_angle
in single_quadrant_minimal_set.keys():
453 for end_point, end_angle
in single_quadrant_minimal_set[start_angle]:
459 if start_angle == 0
and end_angle == 0:
460 unflipped_start_angle = 0.0
461 flipped_x_start_angle = np.pi
463 unflipped_end_angle = 0.0
464 flipped_x_end_angle = np.pi
467 unflipped_trajectory = (
470 unflipped_start_angle,
475 flipped_x_trajectory = (
478 flipped_x_start_angle,
484 if unflipped_trajectory
is None or flipped_x_trajectory
is None:
485 raise ValueError(
'No trajectory was found')
488 unflipped_trajectory.parameters.start_angle
489 ].append(unflipped_trajectory)
492 flipped_x_trajectory.parameters.start_angle
493 ].append(flipped_x_trajectory)
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
499 unflipped_end_angle = np.pi / 2
500 flipped_y_end_angle = -np.pi / 2
503 unflipped_trajectory = (
506 unflipped_start_angle,
512 flipped_y_trajectory = (
515 flipped_y_start_angle,
521 if unflipped_trajectory
is None or flipped_y_trajectory
is None:
522 raise ValueError(
'No trajectory was found')
525 unflipped_trajectory.parameters.start_angle
526 ].append(unflipped_trajectory)
528 flipped_y_trajectory.parameters.start_angle
529 ].append(flipped_y_trajectory)
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
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)
545 unflipped_trajectory = (
548 unflipped_start_angle,
553 flipped_x_trajectory = (
556 flipped_x_start_angle,
561 flipped_y_trajectory = (
564 flipped_y_start_angle,
569 flipped_xy_trajectory = (
572 flipped_xy_start_angle,
573 flipped_xy_end_angle,
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')
583 unflipped_trajectory.parameters.start_angle
584 ].append(unflipped_trajectory)
586 flipped_x_trajectory.parameters.start_angle
587 ].append(flipped_x_trajectory)
589 flipped_y_trajectory.parameters.start_angle
590 ].append(flipped_y_trajectory)
592 flipped_xy_trajectory.parameters.start_angle
593 ].append(flipped_xy_trajectory)
595 return all_trajectories
597 def _handle_motion_model(self, spanning_set: Dict[float, List[Trajectory]]
598 ) -> Dict[float, List[Trajectory]]:
600 Add the appropriate motions for the user supplied motion model.
602 Ackerman: No additional trajectories
604 Diff: In place turns to the right and left
606 Omni: Diff + Sliding movements to right and left
610 The minimal spanning set
615 The minimal spanning set with additional trajectories based
624 return diff_spanning_set
629 return omni_spanning_set
632 print(
'No handling implemented for Motion Model: ' + f
'{self.motion_model}')
633 raise NotImplementedError
635 def _add_in_place_turns(self, spanning_set: Dict[float, List[Trajectory]]
636 ) -> Dict[float, List[Trajectory]]:
638 Add in place turns to the spanning set.
640 In place turns are trajectories with only a rotational component and
641 only shift a single angular heading step
645 The minimal spanning set
650 The minimal spanning set containing additional in place turns
654 all_angles = sorted(spanning_set.keys())
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
660 prev_angle = all_angles[prev_angle_idx]
661 next_angle = all_angles[next_angle_idx]
663 left_turn_params = TrajectoryParameters.no_arc(
664 end_point=np.array([0, 0]),
665 start_angle=start_angle,
666 end_angle=next_angle,
668 right_turn_params = TrajectoryParameters.no_arc(
669 end_point=np.array([0, 0]),
670 start_angle=start_angle,
671 end_angle=prev_angle,
676 angle_dif = angle_difference(start_angle, next_angle)
677 steps = int(round(angle_dif / np.deg2rad(10))) + 1
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)
683 left_turn_path = Path(xs=position, ys=position, yaws=left_yaws)
684 right_turn_path = Path(xs=position, ys=position, yaws=right_yaws)
686 left_turn = Trajectory(parameters=left_turn_params, path=left_turn_path)
687 right_turn = Trajectory(parameters=right_turn_params, path=right_turn_path)
689 spanning_set[start_angle].append(left_turn)
690 spanning_set[start_angle].append(right_turn)
694 def _add_horizontal_motions(self, spanning_set: Dict[float, List[Trajectory]]
695 ) -> Dict[float, List[Trajectory]]:
697 Add horizontal sliding motions to the spanning set.
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.
705 The minimal spanning set
710 The minimal spanning set containing additional sliding motions
718 for idx, angle
in enumerate(self.
headingsheadings):
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
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
739 len(left_straight_trajectory.path.xs), angle, dtype=np.float64
744 parmas_l = left_straight_trajectory.parameters
745 left_motion_parameters = TrajectoryParameters(
746 parmas_l.turning_radius,
753 parmas_l.arc_start_point,
754 parmas_l.arc_end_point,
759 params_r = right_straight_trajectory.parameters
760 right_motion_parameters = TrajectoryParameters(
761 params_r.turning_radius,
768 parmas_l.arc_start_point,
769 parmas_l.arc_end_point,
772 left_motion = Trajectory(
773 parameters=left_motion_parameters,
775 xs=left_straight_trajectory.path.xs,
776 ys=left_straight_trajectory.path.ys,
781 right_motion = Trajectory(
782 parameters=right_motion_parameters,
784 xs=right_straight_trajectory.path.xs,
785 ys=right_straight_trajectory.path.ys,
790 spanning_set[angle].append(left_motion)
791 spanning_set[angle].append(right_motion)
795 def run(self) -> Dict[float, List[Trajectory]]:
797 Run the lattice generator.
802 The minimal spanning set including additional motions for the
803 specified motion model
List[float] _get_heading_discretization(self, int number_of_headings)
Dict[float, List[Trajectory]] _add_horizontal_motions(self, Dict[float, List[Trajectory]] spanning_set)
trajectory_distinctness_ratio
FloatNDArray _get_wave_front_points(self, int pos)
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)
float _compute_min_trajectory_length(self)
Dict[float, List[Trajectory]] _generate_minimal_spanning_set(self)
def __init__(self, ConfigDict config)
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)
Dict[float, List[Trajectory]] run(self)