第 4 章 运动规划¶
规划器的算法机制(树怎么长、图怎么连、代价怎么估)属于 robotics-note 第 9 章;这一章只解决工程侧的三个问题:请求里哪些字段真正改变结果、时间参数化把解变成什么样的轨迹、失败时怎么在错误码与配置之间建立因果。读数均来自运行环境 ROS 2 Humble + MoveIt 2 2.5.10 + OMPL 1.7.0,样本臂 panda,场景是第 3 章那种手工方块环境。算法层面的取舍不在此处展开。
请求进管线到轨迹出管线的四段:
flowchart LR
req["MotionPlanRequest"] --> adapt["适配器链<br/>起点 / 目标容差"]
adapt --> planner["规划插件<br/>OMPL / Pilz"]
planner --> resp["轨迹 + 错误码"]
resp --> tparam["时间参数化"]
1. 管线结构与请求字段¶
一次规划请求由管线(planning pipeline)消化:管线 = 一个规划插件 + 一串请求适配器(request adapter),插件负责搜索、适配器负责后置处理。panda 样本配置里 OMPL 管线的定义就是这样:
## ompl_planning.yaml(节选):插件 + 适配器链 + 起点容差
planning_plugin: ompl_interface/OMPLPlanner
request_adapters: >-
default_planner_request_adapters/AddTimeOptimalParameterization
default_planner_request_adapters/ResolveConstraintFrames
default_planner_request_adapters/FixWorkspaceBounds
default_planner_request_adapters/FixStartStateBounds
default_planner_request_adapters/FixStartStateCollision
default_planner_request_adapters/FixStartStatePathConstraints
start_state_max_bounds_error: 0.1 # 起点越出限位 0.1 rad 以内可被修正,超出即判非法
这些适配器各自带默认参数,运行时从进程输出里可直接读到:path_tolerance 0.1、resample_dt 0.1 s、default_workspace_bounds 10.0、start_state_max_bounds_error 0.1、start_state_max_dt 0.5、jiggle_fraction 0.02、max_sampling_attempts 100。
链条的顺序有意义:先 FixWorkspaceBounds / FixStartStateBounds / FixStartStateCollision 把起点收拾合法,再交给 OMPLPlanner 搜索,最后由 AddTimeOptimalParameterization 做时间参数化。所以“时间参数化失败”和“搜索失败”在同一根管线上是两个环节,返回的码也不同——这是下一节归因表的基础。
请求侧真正改变结果的字段:
| 字段 | 作用 | 边界与坑 |
|---|---|---|
group_name |
选组,决定决策变量维度 | 名字写错返回 FAILURE,不报“组不存在” |
pipeline_id |
选管线 | 只能选配置里声明过的管线,demo 装了 ompl / chomp / pilz_industrial_motion_planner 三条 |
planner_id |
选管线内的规划器配置名 | 要写 planner_configs 下的完整键名(如 RRTConnectkConfigDefault);只写 RRTConnect 会走“找不到配置、改用默认”的路径,表面上仍出解,参数却不是预期的那套 |
allowed_planning_time |
单次尝试的时间预算(s) | 预算不足直接失败,与场景难度无关 |
num_planning_attempts |
尝试次数 | 采样规划器的成功率靠它累加 |
max_velocity_scaling_factor / max_acceleration_scaling_factor |
相对上限的缩放(0–1] | 缺省值是 0,不同管线处理不同 |
goal_constraints |
目标:关节约束或位姿约束 | 位姿约束是“区域”而非点,容差决定可行采样 |
最后一条要展开:位姿目标在消息里是 PositionConstraint(一个包围体区域,通常是半径几毫米的球)加 OrientationConstraint(逐轴角容差)。把容差收紧会让可行采样变少、规划变慢;容差过宽则解出来的末端位置可能偏出工艺要求。抓取侧的“到点精度”通常由这一对容差决定,而不是由控制器的定位精度决定。
2. 缩放因子缺省值的管线差异¶
max_velocity_scaling_factor 在消息里的缺省值是 0.0,语义上应当是“不缩放”。两条管线对它的处理不一致:同一份 Pilz 请求(缺省缩放因子)返回 INVALID_MOTION_PLAN,而把字段显式写成 1.0 后同一请求正常出解;OMPL 管线对 0 值不报错。
## 同一请求(Pilz 管线,PTP → 关节目标 extended),仅缩放因子不同
scaling 未显式给出: code=-2 INVALID_MOTION_PLAN,0 个航点
scaling = 1.0: code= 1 航点 24,时长 2.242 s
排查这类问题的顺序应当是:先在请求里把所有缩放因子显式写出,再调规划器与预算。跨发行版这一处理还会变化(新版本把请求侧的缺省值统一交给管线适配器处理),因此不把它当作通用结论,只作为“缺省值不可依赖”的实例。
3. 采样规划器的适用分界与成功率¶
用一根立柱挡住起点与目标之间的直线路径(柱 0.10×0.25×0.30 m,中心 (0.45, −0.10, 0.30);目标 (0.45, −0.30, 0.30)),每个规划器在每个预算下跑 20 次(每次 1 尝试),给出成功率与 Wilson 95% 区间:
| 规划器 | 0.05 s 预算 | 0.20 s 预算 | 出解时轨迹时长 |
|---|---|---|---|
| RRTConnect | 0/20 = 0.00 [0.00, 0.16] | 2/20 = 0.10 [0.03, 0.30] | 2.95 s |
| PRM | 3/20 = 0.15 [0.05, 0.36] | 2/20 = 0.10 [0.03, 0.30] | 2.98 s |
| BiTRRT | 1/20 = 0.05 [0.01, 0.24] | 3/20 = 0.15 [0.05, 0.36] | 2.47 s |
| EST | 0/20 = 0.00 [0.00, 0.16] | 7/20 = 0.35 [0.18, 0.57] | 2.69 s |
| LBKPIECE | 0/20 = 0.00 [0.00, 0.16] | 2/20 = 0.10 [0.03, 0.30] | 2.75 s |
这张表能读出三件事,第一件是否定性的:五个规划器的区间大量重叠,换一批 20 次(同一场景、同一预算,先跑过一轮)排名就变了——前一轮里 RRTConnect 在 0.05 s 拿到 2/10 而 BiTRRT 0/10,这一轮 0.05 s 下 PRM 反而最高。所以小样本不足以支撑“这台机器该用哪个规划器”的结论,任何排序类断言都要先把样本量提到 30 次以上并给区间。
第二件是预算的主导作用:0.05 s 下四个规划器全部 0/20;把预算给到 2 s、单次尝试,RRTConnect / BiTRRT / LBKPIECE 都能出解。这一类失败的第一位原因是算力配额,而不是算法选择。
第三件是出解质量的差异被掩盖:成功样本的轨迹时长在 2.0–3.0 s、航点 22–32 之间波动,同一条请求两次结果不同——这是采样规划器的本性(概率完备但不保证最优)。要可复现的路径形状,应当使用第 5 节的笛卡尔通路或固定起点目标的对照实验;要更短的解,用 simplify solutions 类适配器或增加尝试次数取最优,而不是换规划器。
OMPL 侧还有两类参数值得单独调:longest_valid_segment_fraction(两点间插值检查的步长,过大就漏检碰撞)与 range / goal_bias(决定探索与向目标收敛的平衡)。它们的语义与算法效果归 robotics-note 第 9 章,此处只列“调什么”。
GPU 并行规划(cuRobo 一类)把搜索与碰撞检查批量搬到 GPU 上展开。它的形态是按机器人配置初始化一个批量规划器,再对多组起/终状态一次求:
from curobo.wrap.reacher.motion_gen import MotionGen, MotionGenConfig, MotionGenPlanConfig
mg = MotionGen(MotionGenConfig.load_from_robot_config(robot_cfg, world_cfg))
mg.warmup() # 首次编译/预热
result = mg.plan_single(start_state, goal_pose, MotionGenPlanConfig())
plan = result.get_interpolated_plan() # 插值后的轨迹
适用面是多目标/多初值的批量查询(抓取候选筛选、轨迹库生成);单臂单次请求的收益取决于数据搬运与预处理开销,不属于“换个库就快”的场景。API 随版本演进,以仓库示例为准。
4. 时间参数化与缩放因子¶
采样规划器给出的是无时间的路径点序列(几何折线)。时间参数化(time parameterization)按关节速度与加速度上限把它重定时,得到可执行的轨迹;MoveIt 默认的适配器是 TOTG(time-optimal trajectory generation,时间最优轨迹生成)。同一几何路径(起点 ready → 组态 extended)在不同缩放因子下的轨迹:
| 速度缩放 | 加速度缩放 | 轨迹时长 | 航点数 | 峰值关节速度 |
|---|---|---|---|---|
| 1.00 | 1.00 | 1.781 s | 19 | 2.175 rad/s |
| 0.50 | 1.00 | 2.514 s | 27 | 1.088 rad/s |
| 1.00 | 0.20 | 3.878 s | 40 | 1.195 rad/s |
| 0.25 | 0.25 | 5.017 s | 52 | 0.544 rad/s |
三行事实值得记住。第一行的峰值 2.175 rad/s 恰好等于第 1 章 joint_limits.yaml 里的速度上限——重定时把上限吃满了,说明规划侧用的是覆写值而非 URDF 值。第二行速度缩放到 0.5,峰值精确减半(1.088),时长却只增加 41%(不是 100%),因为这条路径里有一段本来就没吃满速度上限。第三行只压加速度(0.2)时,峰值速度也被拖低到 1.195,时长增加 118%,且航点从 19 增到 52——加速度约束改变的是速度剖面与采样密度,不只是时长。
航点数由重定时决定,不由几何路径决定,这也是为什么同一目标在不同缩放下航点数差别这么大;执行侧(第 7 章)看到的插值密度因此直接受这里影响。
5. 笛卡尔通路与三种指令¶
抓取与放置大量要求末端走直线(贴近工件、插入、抽出)。MoveIt 提供两种笛卡尔通路,二者在“绕障”上的行为差别很大。
/compute_cartesian_path(moveit_msgs/srv/GetCartesianPath):把目标位姿按 max_step 离散成一串中间位姿,逐个求逆解并检查碰撞,串起来即路径。它做的是“沿直线走”,不会绕障——遇到障碍就在某一步停下并报告完成度:
无障碍的直线下降 0.1 m : 完成度=1.00,18 航点,直线度最大偏差 0.014 mm
同一路径,直线穿过立柱 : 完成度=0.39,12 航点(走到柱面即停)
同一路径,关闭碰撞检查 : 完成度=1.00,21 航点(直接穿柱)
完成度(fraction)是“走完了多大比例的航点”,不是“成功率”;返回码仍是成功。这决定了用法:先按完成度判断是否真的走到了,再决定是否下发执行;avoid_collisions=False 时它照样出解,只是解会穿模。直线度偏差 0.014 mm 说明这条通路的几何精度足够,风险全在完成度这一侧。
Pilz 工业规划器提供三种工业指令,PTP(point-to-point)、LIN(直线)、CIRC(圆弧):
PTP → 关节目标 extended: 24 航点,2.242 s,峰值关节速度 2.039 rad/s
各关节首次到达终值的时间比例 = [0.00, 0.94, 0.00, 0.98, 0.00, 0.00, 0.00]
LIN → 直线下降 0.1 m: 26 航点,2.445 s,直线度最大偏差 0.002 mm
笛卡尔速度 min/中位/max = 2.6 / 50.1 / 50.1 mm/s
中段(25%–75%)速度极差 = 0.00 mm/s
同一目标 vs=0.25: 时长 8.115 s
PTP 的比例里那些 0.00 是因为对应关节本来就在目标值上(ready 与 extended 只有 joint2、joint4 不同);真正运动的两个关节在 0.94 与 0.98 处到位,即同时到达——这是 PTP 的定义性特征,代价是路径形状不可控(不是直线)。LIN 则相反:直线度 0.002 mm、中段速度极差 0.00 mm/s,即匀速直线段,起止处是加减速斜坡(min 2.6 mm/s 就是起点段)。这三条结果正是“工业控制器行为”的刻画,也是选 Pilz 而非 OMPL 的唯一理由:需要直线 + 匀速时。
CIRC 没有可用的消息级通路:Humble 里只有 moveit_msgs/srv/GetCartesianPath(请求是位姿序列,不含圆心/途经点),三点圆弧的指令只在 C++ 侧的接口上提供。因此本章不写 CIRC 的行为。
三种指令都不做绕障规划:需要绕开障碍又必须保持直线时,正确做法是把任务拆成“采样规划到直线起点 → LIN 走完直线”两段(第 5 章的 stage 编排正是为此)。
6. 失败归因清单¶
同一套接口上,不同成因落在不同错误码上,且耗时形态也不同(码值取自 moveit_msgs/MoveItErrorCodes):
| 现象 | 错误码 | 名目 | 耗时形态 |
|---|---|---|---|
| 目标位姿不可达(x=2.0 m) | −27 | GOAL_STATE_INVALID |
烧满预算(5 s 请求 → 5051 ms) |
| 目标位形与障碍相交 | −27 | GOAL_STATE_INVALID |
烧满预算(5064 ms) |
目标取 SRDF 组态 transport |
−27 | GOAL_STATE_INVALID |
立即失败(41 ms) |
| 起点越出限位(joint4=1.0,上限 0.0873) | −26 | START_STATE_INVALID |
立即失败(32 ms) |
| 组名写错 | 99999 | FAILURE |
立即失败(27 ms) |
| 预算过小而场景需绕行 | −27 | GOAL_STATE_INVALID |
223 ms |
| Pilz 请求缺省缩放因子 | −2 | INVALID_MOTION_PLAN |
立即失败 |
三条使用要点。
目标非法有两种表现,区别在“能不能立刻判定”。transport 组态的失败只用 41 ms:因为它是一组确定的关节值,合法性可以一步查出——该组态本身就让 panda_link7 与 panda_link5 相交 4.4 mm(用第 3 章的校验服务单独确认过,无需世界物体即非法)。而位姿目标要经过逆解与采样,不可达时会反复尝试直到烧满预算。判断“某组态是否合法”应直接用校验服务,不要用规划请求去试,后者会把 5 s 预算全花掉。
耗时形态本身就是线索。返回失败且几乎立即返回 → 起点/目标/配置层的确定性问题(越限、非法、名字错);返回失败且耗时接近预算 → 搜索层问题(不可达、需绕行、预算不足)。这一条比错误码更快指向原因。
同一现象在不同接口上的码可能不同。transport 组态作为目标,在第 1 章的进程内接口上返回 FAILURE 与 INVALID_MOTION_PLAN,在本章的动作接口上返回 GOAL_STATE_INVALID。写排障清单时要按接口层分别记录,不能混成一条“规划失败”。
7. 要点¶
- 管线 = 一个插件 + 一串请求适配器;起点修正适配器在搜索之前,时间参数化在搜索之后,所以同一管线的失败环节不同、错误码也不同。
- 真正改变结果的请求字段是组名、管线、规划器配置名、时间预算、尝试次数、两个缩放因子与目标约束的容差;缩放因子缺省值 0 在 OMPL 与 Pilz 上处理不一致,显式写出是唯一稳妥做法。
- 五个采样规划器在同一绕障场景下的成功率区间大量重叠,且换一批样本排名就变:小样本不足以排序,预算与尝试次数才是第一位原因(2 s 预算下多数规划器都能出解)。
- 时间参数化吃满覆写后的速度上限(峰值恰为 2.175 rad/s);压速度上限不会等比例拉长时长,压加速度会同时改变速度剖面与航点密度。
- 笛卡尔通路不做绕障:
/compute_cartesian_path用fraction报告走到哪儿(障碍下 0.39),关闭碰撞检查照样 1.00 但穿模;Pilz 的 LIN 给出 0.002 mm 直线度与中段匀速,PTP 给出同时到位——需要“绕行 + 直线”时必须拆成两段。 - 失败归因优先看耗时形态:立即失败指向起点/目标/配置层,烧满预算指向搜索层;判断组态合法性用校验服务而不是发规划请求。
- SRDF 的具名组态不保证合法:panda 样本的
transport就让 link7 与 link5 相交 4.4 mm,作为目标会直接判非法。
参考文献与延伸阅读¶
- Şucan, I. A., Moll, M., & Kavraki, L. E. The Open Motion Planning Library. IEEE Robotics & Automation Magazine, 2012. DOI
- LaValle, S. M. Planning Algorithms. Cambridge University Press, 2006. 书籍主页
- Karaman, S., & Frazzoli, E. Sampling-Based Algorithms for Optimal Motion Planning. The International Journal of Robotics Research, 2011. DOI