Coverage for src/pyroboplan/planning/path_shortcutting.py: 93%
39 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-02 22:03 -0500
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-02 22:03 -0500
1"""Capabilities to shorten paths produced by motion planners."""
3import numpy as np
5from pyroboplan.core.utils import configuration_distance, get_path_length
6from pyroboplan.planning.utils import (
7 check_collisions_along_path,
8 discretize_joint_space_path,
9)
12def get_normalized_path_scaling(q_path):
13 """
14 Returns a list of length-normalized scaling values along a joint configuration path,
15 from 0.0 at the start to 1.0 at the end.
17 Parameters
18 ----------
19 q_path : list[array-like]
20 The list of joint configurations describing the path.
22 Return
23 ------
24 list[float]
25 The list of scaling values, between 0.0 and 1.0, at each of the path waypoints.
26 """
27 if len(q_path) == 0:
28 return []
29 if len(q_path) == 1:
30 return [1.0]
32 path_length = 0.0
33 path_length_list = [path_length]
34 for idx in range(len(q_path) - 1):
35 path_length += configuration_distance(q_path[idx], q_path[idx + 1])
36 path_length_list.append(path_length)
37 return np.array(path_length_list) / path_length
40def get_configuration_from_normalized_path_scaling(q_path, path_scalings, value):
41 """
42 Given a path and its corresponding length-normalized scalings between 0.0 and 1.0,
43 get the joint configuration at a specific scale value.
45 For example:
46 * 0.0 will return the start configuration of the path
47 * 1.0 will return the end configuration of the path
48 * 0.5 will return the configuration halfway along the length of the path
50 Parameters
51 ----------
52 q_path : list[array-like]
53 The list of joint configurations describing the path.
54 path_scalings : list[float]
55 The list of scaling values, between 0.0 and 1.0, at each of the path waypoints.
56 value : float
57 The value, between 0.0 and 1.0, at which to get the joint configuration along the path.
59 Return
60 ------
61 tuple (array-like, int)
62 A tuple containing the joint configuration at the specified scaling value along the path,
63 as well as the index corresponding to the next point along the path.
64 """
65 if value < 0 or value > 1:
66 raise ValueError("Scaling value must be in the range [0, 1]")
68 for idx in range(len(path_scalings)): 68 ↛ exitline 68 didn't return from function 'get_configuration_from_normalized_path_scaling' because the loop on line 68 didn't complete
69 if value > path_scalings[idx]:
70 continue
72 delta_scale = (value - path_scalings[idx - 1]) / (
73 path_scalings[idx] - path_scalings[idx - 1]
74 )
75 return q_path[idx - 1] + delta_scale * (q_path[idx] - q_path[idx - 1]), idx
78def shortcut_path(model, collision_model, q_path, max_iters=100, max_step_size=0.05):
79 """
80 Performs path shortcutting by sampling two random points along the path and verifying
81 whether those two points can be connected directly without collisions.
83 This implementation is based on section 3.5.3 of:
84 https://motion.cs.illinois.edu/RoboticSystems/MotionPlanningHigherDimensions.html
86 Parameters
87 ----------
88 model : `pinocchio.Model`
89 The model of the robot.
90 collision_model : `pinocchio.Model`.
91 The model to use for collision checking.
92 q_path : list[array-like]
93 The list of joint configurations describing the path.
94 max_iters : int
95 The maximum iterations for randomly sampling shortcut points.
96 max_step_size : float
97 Maximum joint configuration step size for collision checking along path segments.
99 Return
100 ------
101 list[array-like]
102 The shortest length path over all the shortcutting attempts.
103 """
104 # If the original path has 2 points or less, this is already a shortest path.
105 if len(q_path) < 3:
106 return q_path
108 q_shortened = q_path
109 for _ in range(max_iters):
110 # If the path has been shortened to 2 points or less, this is already a shortest path.
111 if len(q_shortened) < 3: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 break
114 # Sample two points along the path length
115 path_scalings = get_normalized_path_scaling(q_shortened)
117 low_point, high_point = sorted(np.random.random(2))
118 q_low, idx_low = get_configuration_from_normalized_path_scaling(
119 q_shortened, path_scalings, low_point
120 )
121 q_high, idx_high = get_configuration_from_normalized_path_scaling(
122 q_shortened, path_scalings, high_point
123 )
124 # there is nothing to shorten on a linear path segment
125 if idx_low == idx_high:
126 continue
128 # Check if the sampled segment is collision free. If it is, shortcut the path.
129 path_to_goal = discretize_joint_space_path([q_low, q_high], max_step_size)
130 if not check_collisions_along_path(model, collision_model, path_to_goal): 130 ↛ 109line 130 didn't jump to line 109 because the condition on line 130 was always true
131 q_shortened = (
132 q_shortened[:idx_low] + [q_low, q_high] + q_shortened[idx_high:]
133 )
134 return q_shortened