跳转至

第 5 章 任务级编排

一次规划请求解决的是“从这个位形到那个位姿”,而操作任务天然是分段的:接近、闭合、抬起、搬运、放下、松开。把这些段落串起来的机制就是任务级编排。本章讲 MoveIt Task Constructor(MTC,任务构造器)的 stage 模型——分解方式、解如何在 stage 之间传播、失败以什么形态暴露——并说明这套接口的形态边界:Python 绑定与 C++ 各自能承担到哪一步。以 ROS 2 Humble + MoveIt 2 2.5.10 + MTC 0.1.3、样本臂 panda 为例。

MTC 的 stage 组合与解传播方向:

flowchart LR
    task["Task"] --> stages["Stage 组合<br/>Generator / Propagator / Connector"]
    stages --> sol["Solutions<br/>解沿 stage 逐级传播"]
    sol --> diag["失败计数与诊断"]

1. 任务级分解的动机

单请求规划的边界在第 4 章已经清楚:目标是一组约束,起点是一个位形,中间由采样规划器搜索。操作任务里至少有三个东西塞不进这个模型:

  • 目标本身是集合:抓取位姿由工件的对称性与夹爪方向决定,候选不止一个;选定哪个要等到知道“能不能到达”之后。
  • 中间状态是任务语义的一部分:抓取后物体要从世界物体变成机器人的一部分(attach,第 3 章),此后所有碰撞查询都带着它。这不是一个位姿约束能表达的。
  • 失败要在段落粒度上报:整条轨迹规划失败与“接近段可达但抬起段撞桌”是不同的处置——前者换预算重试,后者换抓取位姿。

MTC 的答案是把任务显式建成stage 图:每个 stage 只负责一小段语义,stage 之间通过解的接口(interface states)传递候选位形,规划发生在相邻 stage 的候选之间。抓取 = approach + grasp + lift + place 的分解正是这套模型的典型用法。

2. stage 的类型谱系与属性接口

MTC 的 stage 分三类:生成器(generator,产出候选状态而不规划)、连接器(connect,在两个接口状态之间规划轨迹)、容器(container,把若干 stage 组合成一个语义单元)。Python 绑定里可直接枚举(from moveit.task_constructor import core, stages):

CurrentState  FixedState  GeneratePose  GenerateGraspPose  GeneratePlacePose
GenerateRandomPose  ComputeIK  Connect  MoveTo  MoveRelative  ModifyPlanningScene
Pick  Place  SimpleGrasp  SimpleUnGrasp  FixCollisionObjects        ← stage 类
Merger  Alternatives  Fallbacks  SerialContainer  ParallelContainerBase  ← 容器类
PipelinePlanner  JointInterpolationPlanner  CartesianPath  MultiPlanner  ← 规划器类
PathLength  DistanceToReference  Clearance  LinkMotion  TrajectoryDuration  ← 代价项

四类值得单独认:Pick / Place / SimpleGrasp 是把“接近 + 闭合 + attach”这种成段的语义打包好的容器;ModifyPlanningScene 提供 addObject / removeObject / attachObject / detachObject / allowCollisions,即在任务图内部修改场景(第 3 章的场景编辑在这里成为 stage 的一部分);FixCollisionObjects 专门处理“物体与机器人初始就相交”的情形;末尾一列是代价项(cost term),挂在 stage 上参与解的排序。

每个 stage 的行为由属性表驱动,Python 侧属性名目直接可见:

ik = stages.ComputeIK("compute IK", generator)   # 包裹一个位姿生成器
ik.group = "panda_arm"          # 用组配置的 IK 求解器
ik.ik_frame = pose_in_hand      # 目标位姿的度量点(末端上的偏置帧)
ik.max_ik_solutions = 4         # 限制每个位姿产出的关节解数量
ik.ignore_collisions = False    # 候选解是否要过碰撞检查
props = ik.properties
props.configureInitFrom(core.Stage.PropertyInitializerSource.INTERFACE, ["target_pose"])

最后两行是 MTC 的核心机制:属性可以从父 stage(PARENT)或接口状态(INTERFACE)继承,而不是硬写。target_pose 从接口继承意味着这个 stage 不必知道自己的目标位姿——它由上游生成器产出的每个候选决定。这一层设计正是“解的传播”的实现方式:生成器产出候选,下游 stage 对每个候选分别计算,连接器负责把两侧候选两两连起来。

3. 解的传播与失败计数

stage 图的输出不是一条轨迹,而是每个 stage 的候选集与统计量。C++ demo(moveit_task_constructor_demo 的 pick & place,经 run.launch.py 提供参数)跑起来后,/statistics 话题(moveit_task_constructor_msgs/msg/TaskStatistics)给出的形态是:

每 stage 的 (solved 数, failed 数, 累计计算时间):
  id=2   (1, 0,  0.014 s)      id=6 (74,  0, 5.418 s)
  id=3   (1, 0,  0.014 s)      id=7 (75, 22, 1.402 s)
  id=4   (1, 0,  0.019 s)     id=24 (402, 209,  9.835 s)
  id=5   (0, 73, 3.353 s)     id=25 (0, 531, 19.993 s)

三个可判读的点。其一,生成器与连接器的计数含义不同:id=2/3/4 各产出 1 个解(当前状态、场景修改一类),而 id=6/7 的几十个解是候选位姿与候选关节解;id=5 有 73 次失败 0 个解,说明它是尝试把两侧连起来却连不上的连接器。其二,失败计数集中在连接器(id=25 失败 531 次、累计 20 s),这是任务级规划的主要算力去向,也是排查的第一目标。其三,候选多不等于有解:这一轮里两侧各自有大量候选,但 /solution 上没有产出完整解。

完整解缺席的原因要从 move_group 那一侧读:连接阶段返回“计算出的路径含非法状态(37 个点中的第 8 个)”,并列出接触对 cube(物体)× panda_link5(机器人 link)。即生成器给出的抓取候选本身让手臂与工件相交,连接器在两侧候选之间找不到合法路径。处置顺序由此确定:先查候选(生成器的抓取位姿与 IK 帧偏置),再查连接(预算与路径约束),而不是一上来加大规划时间。

这套统计是 MTC 最有价值的可观测面:StageStatistics.solved / failed / total_compute_time 三个量把“任务在哪一步卡住、算力花在哪”变成可数的指标。第 10 章的端到端统计要按段拆解,用的就是它。

4. 接口形态的实际边界

MTC 的 Python 绑定(随 moveit-task-constructor-core 提供的 moveit.task_constructor)可以导入、枚举类型、构造 stage 对象并设置属性,但跑不完整条规划链路:task.loadRobotModel(node) 会失败,任务拿不到机器人模型,plan() 无从谈起。

原因在依赖侧而非 MTC 侧:这些绑定需要一个真实的 rclcpp 节点来订阅 /robot_description 与规划管线,而 Humble 的 rclcpp Python 扩展(rclcpp.cpython-310-*.so)只暴露 init / shutdown 与 Node,且 Node 除 name 之外没有可用成员。因此第 4 章与第 6 章的规划链路一律走 rclpy + 动作接口,MTC 的链路走 C++;Python 教程脚本(pickplace.py 一类)还额外依赖 moveit_commander,Humble 的二进制包里没有提供。

由此界定本章的取材范围:stage 谱系与属性名目取自 Python 绑定的直接枚举,可靠;stage 的组装顺序与容器嵌套语义取自 C++ demo 与其 Python 教程脚本的源码结构,行为数字以 C++ 侧统计为准;Pick / Place 容器的内部动作序列(接近距离、抬起方向、抓取后 attach 的挂点)在 C++ 侧由 setApproachMotion / setLiftMotion / setIKFrame 配置,Python 教程里同一组调用的签名一致,其具体执行结果属外部实现、不在本地断言。

5. 段落级的组合、回退与代价

stage 图不止是顺序串联。四类组合方式对应四种工程需求:

组合 语义 用途
Alternatives 并列的多个候选,各产出解并参与排序 多个抓取位姿、多条入口路径
Fallbacks 按顺序尝试,前面的失败才走后面的 先试笛卡尔直线抽出,失败退回采样规划
Merger 两臂分别规划后再合并(MergeMode 控制合并策略) 双臂或“先摆臂再动手”的分治
SerialContainer / ParallelContainerBase 自定义段落结构 把带场景修改的子序列当作一个 stage

代价项(PathLength、DistanceToReference、Clearance、LinkMotion、TrajectoryDuration)挂在 stage 上,通过 setCostTerm 生效。选择代价项就是在定义“什么算好解”:抓取侧常用 Clearance(离障碍越远越好)与 PathLength(省节拍),恢复侧常用 DistanceToReference(回到参考位形的偏差)。

回退与重规划在任务层也有分工。MTC 内部的重规划靠 Fallbacks 与生成器的候选顺序;跨任务的重规划(场景变了、物体掉了)则由外部决定——这也是与行为树的边界:MTC 负责“一个任务内部的分解与候选搜索”,行为树负责“跨任务的状态机与故障处置策略”(导航侧的行为树实践归 ROS2-note 第 10 章)。把 attach/detach 的时序写进行为树、把抓取位姿搜索写进 MTC,是常见的正确切分。

6. 编排与生成—测试式规划的关系

MTC 的结构是“先生成候选集合,再检验可达性”,属于生成—测试(generate-and-test)式规划:生成器廉价地产出大量候选,连接器用昂贵的规划逐个检验。这决定了两个实践结论。

第一,候选的数量与质量决定搜索代价。max_ik_solutions、angle_delta(抓取位姿的角度步长)这类参数直接决定候选规模;第 3 节里 id=25 的 531 次失败正是候选规模乘上连接代价的结果。收紧生成器(只产出符合夹爪几何与 clearance 的候选)通常比放宽规划器预算更有效。

第二,被检验的是“可达性”而不是“可抓取性”。连接器判断的只有碰撞与限位;抓取是否真的成立(力闭合、接触摩擦、指尖包络)不属于规划器,它的判据在第 6 章给出。把这两件事混在一起,会得到“规划成功但抓取失败”却无从归因的局面——这也是第 10 章端到端统计必须分段的原因。

算法层面的完备性、概率完备性等概念归 robotics-note 第 8、9 章;此处只需要工程结论:任务级编排改变的是候选的组织方式与失败的定位粒度,不改变单段规划的算法性质。

7. 要点

  • 任务级编排解决的是单请求模型装不下的三件事:目标是候选集合、中间状态带任务语义(attach)、失败要在段落粒度定位。
  • stage 分生成器 / 连接器 / 容器三类;属性可从父 stage 或接口状态继承(PARENT / INTERFACE),继承机制就是解传播的实现方式。
  • 统计量 StageStatistics 的 solved / failed / total_compute_time 是首要可观测面:C++ demo 一轮里连接器失败 531 次、累计 20 s,而完整解为零——瓶颈在连接而不在生成。
  • 无完整解时先读连接器的非法状态报告(本例为物体与 panda_link5 接触),据此收紧抓取候选,而不是加大规划预算。
  • 接口形态边界:MTC 的 Python 绑定可导入、可枚举、可构造 stage,但 loadRobotModel 依赖的 rclcpp Python 节点只是 stub,完整链路需走 C++;行为数字以 C++ 侧为准。
  • 组合方式对应四种需求:Alternatives(并列候选)、Fallbacks(顺序回退)、Merger(分治合并)、容器(自定义段落);代价项定义“什么算好解”。
  • 生成—测试结构决定了:收紧候选比放宽预算有效;规划器只检验可达性,可抓取性另有色(第 6 章)。

参考文献与延伸阅读

  • Colledanchise, M., & Ögren, P. Behavior Trees in Robotics and AI: An Introduction. CRC Press, 2018. DOI
  • MoveIt. 项目门户与文档入口. 文档
  • Siciliano, B., & Khatib, O. (Eds.). Springer Handbook of Robotics (2nd ed.). Springer, 2016. DOI