TAMP_T3 PDDLStream 与流式集成 (PDDLStream and Stream-based Integration)¶
难度: ⭐⭐⭐ ~ ⭐⭐⭐⭐ (本章是 TAMP 集成范式的纵深,整体偏进阶) 前置知识: TAMP_T1(PDDL/FF、TAMP 核心挑战、PDDLStream 入门)、TAMP_T2(删除松弛启发式、规划-分配耦合)、采样运动规划(RRT/PRM,逆运动学) 核心参考: Garrett, Lozano-Pérez & Kaelbling (2020, ICAPS), "PDDLStream: Integrating Symbolic Planners and Blackbox Samplers via Optimistic Adaptive Planning"; Garrett et al. (2021, Annual Review) TAMP 综述 与既有章节的关系: 本章把 TAMP_T1 §7 当黑盒用的 PDDLStream 彻底打开,是 TAMP_T2 §9"让符号层看见几何可行性"(策略二)那条路的系统化展开。它与下一章 TAMP_T4(LGP)并列为缝合符号-几何鸿沟的两大主流范式。
0. 前置自测¶
开始本章之前,请完成下面四道自测题。它们检验本章依赖的三块基础——符号规划(PDDL/FF)、采样运动规划(IK/RRT)、以及 TAMP 的核心难题(符号-几何鸿沟)。如果两道以上答不出来,先回对应前置章节。
| # | 问题 | 期望掌握程度 | 答不出来回到 |
|---|---|---|---|
| Q1 | TAMP 的核心难题"符号-几何鸿沟"指什么?为什么纯符号规划器(如 FF)无法直接处理"机械臂够不够得到"这类问题? | 能说出离散命题 vs 连续几何,规划器看不到几何 | TAMP_T1 §2.5 |
| Q2 | 逆运动学 (IK) 求解器、碰撞检测器、运动规划器 (RRT),这三类几何过程各自的输入和输出是什么? | 能说出 IK:目标位姿→关节角;碰撞检测:构型→是否碰撞;RRT:起止构型→路径 | TAMP_T1 §4、机器人学基础 |
| Q3 | TAMP_T2 §9 讲的"让符号层看见几何可行性"(策略二)是什么意思?它解决了什么问题? | 能说出把几何可行性/代价信息反馈进符号决策 | TAMP_T2 §9.4 |
| Q4 | 一个 pick 动作的前提里有"机械臂能到达抓取位姿"。这个"抓取位姿"是一个连续值(6 自由度),但 PDDL 的对象是离散符号。如何让符号规划器处理这个连续参数? | 能意识到需要某种机制把连续值"引入"符号世界——这正是本章的主题 | TAMP_T1 §7(入门) |
| Q5 | 下面三个问题各该用纯 PDDL、纯运动规划、还是 PDDLStream?(a) 货架补货的顺序调度(无几何);(b) 机械臂从 A 构型移到 B 构型避障;(c) 整理桌面(够不够得到决定先搬哪个)。 | 能按"离散选择是否与几何耦合"区分三者 | 本章 §2.5(学完即可) |
参考答案 (点击展开)
**Q1**: 符号-几何鸿沟指任务层用**离散命题**描述世界(如 `On(cup, table)`),运动层用**连续向量**描述世界(如 `cup.pose = (0.3, 0.2, 0.1)`)。纯符号规划器(FF)的世界里只有有限个命题,没有几何信息——它知道"架子是空的",但不知道"架子高 1.2 米、机械臂臂展 0.8 米",所以判断不了"够不够得到"。这道鸿沟是 TAMP 全部困难的根源。 **Q2**: **逆运动学 (IK)**:输入末端执行器的目标位姿(6 自由度),输出能达到该位姿的关节角配置(可能多解或无解)。**碰撞检测**:输入一个机器人构型(关节角),输出该构型是否与环境/自身碰撞(布尔值)。**运动规划器 (RRT)**:输入起始构型和目标构型,输出连接两者的无碰撞路径(一串构型)。 **Q3**: 策略二指在分配/规划的代价或可行性判断里**直接调用规划器/几何过程**,把真实的几何信息(这个抓取够不够得到、这条路有多长)反馈进符号决策,而非用粗糙的假设(直线距离)。它解决了"符号层盲目决策、几何层只能事后否决"导致的回溯问题。本章的 PDDLStream 正是把这个思想做成一套完整、通用的框架。 **Q4**: 需要一种机制,能在符号规划过程中**按需生成**满足约束的连续值,并把"这个值满足某个约束"这件事**以符号事实的形式告诉规划器**。例如调用 IK 求解器生成一个可达的抓取位姿,然后告诉规划器"位姿 $q_{17}$ 是 cup 的一个可达抓取"。本章的核心——**Stream**——正是这个机制的形式化。 **Q5**: (a) **纯 PDDL**——补货顺序是纯离散调度,无几何约束,符号规划器直接解。(b) **纯运动规划**——只需 A 到 B 的无碰撞路径,没有"先做什么"的离散选择,RRT/PRM 即可。(c) **PDDLStream**——"够不够得到决定先搬哪个"意味着离散选择(搬的顺序)的可行性依赖几何(够不够得到),二者耦合,这正是 TAMP 问题(§2.5 的判断信号)。1. 本章目标¶
学完本章后,你应当能够:
- 解释 TAMP_T1 §7 当黑盒用的 PDDLStream 内部如何工作:Stream 是什么、它的过程性组件(条件生成器)与声明性组件(认证事实)各自的角色。
- 编写一个 Stream 声明:定义它的输入、输出、所需的领域事实(domain facts)和认证的事实(certified facts),并实现对应的条件生成器(如 IK 采样器、放置位姿采样器)。
- 描述 PDDLStream 如何把一个含连续参数的规划问题,归约为"一系列有限 PDDL 问题"——理解 optimistic 对象、certified facts、level 这三个核心概念。
- 对比 PDDLStream 的三种算法——Incremental、Focused、Adaptive——各自如何平衡"采样"与"搜索",以及它们的适用场景。
- 诊断 Stream 设计中的常见问题:采样器效率、optimistic 计划无法落地、紧约束下的组合爆炸。
- 把 PDDLStream 接到 TAMP_T1 的 Mini-TAMP 累积项目上,替换原来朴素的 Plan-then-Check 协调器,并理解它相对 LGP(下一章)的取舍。
- 判断何时该用 PDDLStream(vs 纯 PDDL、纯运动规划),并说清 PDDLStream 的理论保证(概率完备性)依赖什么、为什么 TAMP 的解常落在零测度子流形上。
- 应用 Stream 设计模式为新领域(如倒水)从零设计一套 Stream,并理解 PDDLStream 在真实系统中作为符号-几何骨架协调中枢的定位、与 LLM 的"辅助而非替代"关系。
本章知识导航¶
本章只回答一个问题,但回答得很深:如何让符号规划器处理连续的几何参数? TAMP_T1 §2.5 揭示了符号-几何鸿沟,TAMP_T2 §9 给出了"让符号层看见几何"的思路,本章把这个思路落实为一套可运行的框架——PDDLStream。整章围绕"Stream"这一核心概念展开:
根问题:符号规划器如何处理连续几何参数?
│
┌───────────────────────┼───────────────────────┐
│ │ │
【是什么】 【怎么运转】 【怎么用】
│ │ │
§2 回顾鸿沟与朴素解的失败 §4 Stream 的形式化 §7 Stream 设计模式
├─ 为何 plan-then-check 不够 ├─ 输入/输出/domain/certified ├─ 因子切分/test/采得准
├─ §2.5 何时该用 PDDLStream ├─ 条件生成器(过程组件) └─ §7.6 新领域设计工作流
└─ 为何要"采样器进规划" └─ 认证事实(声明组件) │
│ │ §8 工程实践
§3 核心思想:Stream 是接口 §5 PDDLStream 怎么求解 ├─ pddlstream 库 + 接 Mini-TAMP
├─ 把 IK/碰撞/RRT 当黑盒 ├─ optimistic 乐观对象 └─ §8.4 调试清单
└─ 采样器 ↔ 符号搜索的桥 ├─ certified facts / level │
│ ├─ 归约为有限 PDDL §9 真实应用
│ ├─ 三算法+§5.5概率完备 ├─ 烹饪/Kitchen Worlds/NAMO
│ └─ §5.6成本 §5.7伪代码 └─ §9.5 仿真到真机鸿沟
│ │ │
│ │ §10 横向对比与局限
└────────────────────────┴── §6 完整案例 ├─ vs LGP / FFRob 演进
§6.6运行轨迹 §6.7纯Python最小实现 └─ §10.5 LLM 辅助而非替代
怎么读这张图:左列建立"为什么需要 Stream"(§2-§3),中列是本章硬核——Stream 的形式化(§4)与 PDDLStream 的求解机制(§5,含 §5.5 概率完备性)。落地部分依次是 §6 完整案例、§7 Stream 设计模式与反模式、§8 接入 Mini-TAMP、§9 真实应用、§10 横向对比与局限。⭐⭐⭐ 的 §3-§4(Stream 概念)和 §5(求解机制)是必须掌握的主干;⭐⭐⭐⭐ 的 §5.4(三种算法)与 §5.5(理论)是进阶。
主干与分支:第一遍务必拿下 §3(Stream 是什么)、§4(Stream 怎么定义)、§5.1-§5.3(optimistic/certified/level 三概念)、§7(设计模式,实战必备)。§5.4-§5.5(算法与理论)和 §10 与 LGP 的对比可第二遍深入。
前置知识桥接¶
本章站在三块基石上,各用几行重新激活。
回顾 TAMP_T1 §2.5:符号-几何鸿沟。 任务层用离散命题(On(cup, table)),运动层用连续向量(cup.pose ∈ ℝ³),两者之间横着一道鸿沟——规划器"看不到"几何。TAMP_T1 §7 给出了 PDDLStream 作为缝合鸿沟的一种范式,但只展示了用法,没讲清它内部如何工作。本章就是打开这个黑盒。
回顾 TAMP_T1 §7:PDDLStream 入门。 TAMP_T1 §7 介绍过:PDDLStream 在 PDDL 之上引入 Stream,让符号规划能调用几何采样器(IK、运动规划);它有 Incremental、Focused 等算法变体;核心是 certified/optimistic 的交替。本章把这些点逐一讲透——TAMP_T1 给了"是什么",本章给"为什么这样设计、内部怎么跑、怎么自己写一个"。
回顾 TAMP_T2 §9:让符号层看见几何(策略二)。 TAMP_T2 §9.4 讲分配-规划耦合时,策略二是"把规划器嵌进代价/可行性计算,让符号层看见真实几何"。当时是在多机分配的语境下讲的。本章把这个思想一般化、系统化:PDDLStream 不只是"嵌入一次规划器",而是建立了一套完整的机制,让符号搜索和几何采样交替进行、互相喂信息,直到找到一个符号上合法、几何上可行的完整计划。
本质洞察:TAMP_T1 给了 PDDLStream 的"用户手册",TAMP_T2 §9 给了它背后的"设计哲学"(让符号看见几何),本章给的是"原理 + 源码级理解"。三章层层递进——这正是 TAMP 线"总论给地图、基础给入门、专章给纵深"的设计。读完本章,你不仅会用 PDDLStream,还能在它跑不出解时诊断原因、能为新领域自己设计 Stream。
如果跳过本章会怎样¶
- 场景一(只学了 TAMP_T1 §7):你会用 PDDLStream 跑官方例子,但稍微改一下领域(加一个新的几何约束,如"倒水时杯口要朝下")就不知道怎么写对应的 Stream,也不知道为什么有时它跑几秒出解、有时跑几分钟不收敛。缺了 §4-§5 的原理,你只能照搬例子,无法迁移。
- 场景二(想做接触丰富的操作):你发现 PDDLStream 在"抓放摆"这类问题上很顺,但在"推、倒、插"这类力/接触密集的操作上费劲。你不知道这是因为 PDDLStream 的 Stream 范式更适合"采样离散选择 + 几何可行性",而接触密集操作更适合 §10 对比的 LGP(优化式)。缺了 §10 的对比,你会用错范式。
- 场景三(仿真跑通了想上真机):你在仿真里用 PDDLStream 解出了漂亮的计划,搬到真机却频频失败——抓取抓偏、规划好的无碰撞路径真的碰了、环境一变计划就过时。你不知道这是 §9.5 的三道"仿真到真机"鸿沟(感知噪声、误差余量、规划-执行节奏),也不知道该用 T6(不确定性)、T5(行为树闭环)来弥合。缺了 §9 的工程视角,你会以为"仿真跑通=真机能用",在部署时反复碰壁。
预计阅读时间¶
| 模式 | 覆盖范围 | 预计时间 |
|---|---|---|
| 精读 | 全部小节 + Stream 实现 + §6 完整案例复现 + §7.6 设计工作流 + 练习 | 16-20 小时 |
| 速读 | §2/§3/§4/§5.1-5.3 主干 + §7 设计模式 + 跳过 §5.4-5.6 与 §10 细节 | 6-7 小时 |
| 实战 | §2.5 判断 + §4 Stream 定义 + §7.6 设计工作流 + §8 接入 + §8.4 调试 | 8-10 小时 |
| 速查 | 知识导航 + §4 Stream 定义模板 + §5 三概念 + §7.5 反模式表 + 各节小结 | 2 小时 |
针对不同目标:想理解原理走精读/速读;想动手为新领域建模走实战路径(重点 §7.6 工作流 + §8.4 调试);想快速查用法走速查。
2. 重访鸿沟:为什么朴素解都不够 ⭐⭐⭐¶
2.1 一个具体的连续参数难题¶
先把抽象的"符号-几何鸿沟"落到一个最小但完整的例子上,整章都会回到它。
任务:机械臂把桌上的杯子 cup 抓起来,放到架子 shelf 上。符号层面,这是 TAMP_T1 §3 写过的 pick-and-place,两个动作:pick(cup) 然后 place(cup, shelf)。但每个动作背后藏着连续参数:
| 符号动作 | 它隐含的连续参数 | 这些参数要满足的几何约束 |
|---|---|---|
pick(cup) |
抓取位姿 \(g\)(手相对杯子怎么抓,6 自由度);抓取时的机械臂构型 \(q_1\) | \(g\) 是 cup 的有效抓取;\(q_1\) 通过 IK 达到 \(g\);\(q_1\) 无碰撞 |
place(cup, shelf) |
放置位姿 \(p\)(杯子放在架子哪个位置);放置时构型 \(q_2\) | \(p\) 在 shelf 上且稳定;\(q_2\) 通过 IK 达到 \(p\);\(q_2\) 无碰撞 |
| 两动作之间 | 运动轨迹 \(\tau\)(从 \(q_1\) 移动到 \(q_2\)) | \(\tau\) 连接 \(q_1, q_2\) 且全程无碰撞 |
问题的核心矛盾:这些参数(\(g, q_1, p, q_2, \tau\))都是连续值,有无穷多种选择,而 PDDL 的对象是有限的离散符号。符号规划器 FF 知道"要 pick 再 place",但它没法凭空"想出"一个能抓得到、放得稳、走得通的具体参数组合——那需要 IK 求解器、碰撞检测、运动规划器这些几何过程。
本质洞察:TAMP 的难,不在于"符号规划难"或"运动规划难"(这两个分别都有成熟解法),而在于这些连续参数之间是耦合的——抓取位姿 \(g\) 决定了构型 \(q_1\),\(q_1\) 又约束了能走到哪些 \(q_2\),\(q_2\) 又限制了放置位姿 \(p\)。你不能孤立地选每个参数,因为前一个选择会让后一个无解。这正是 TAMP_T1 §5.3 讲的"耦合约束",也是为什么不能"符号层选好动作、运动层各自填参数"——填到一半会发现凑不齐一组相容的参数。
面对这个连续参数难题,有两种自然但都行不通的朴素想法——把连续参数预先离散化(§2.2),或先排符号计划再事后填几何(§2.3)。下面逐一看它们为什么失败,失败的原因会直接引出 Stream 的设计动机。
2.2 朴素解一:穷举离散化¶
最直白的想法:把连续参数离散化——预先生成有限个候选抓取位姿、有限个候选放置位置,把它们当成离散对象塞进 PDDL,让符号规划器从中选。
为什么不行?
- 粒度两难:离散化粗了(如 10 个位姿),可能漏掉唯一可行的那个;细了(如 10000 个),PDDL 对象爆炸,符号搜索的分支因子失控。
- 盲目生成:预生成时不知道哪些位姿后续用得上。可能生成的 100 个抓取位姿里,没有一个能配出无碰撞的放置——白生成。
- 耦合没解决:离散化只是把"无穷选择"变成"很多选择",参数间的耦合(\(g\) 决定 \(q_1\) 决定……)依然存在,组合数依然爆炸。
2.3 朴素解二:先符号后几何(plan-then-check 重现)¶
TAMP_T1 §2.4 已经批判过的方案,这里在连续参数语境下再看一遍:先让符号规划器排出 [pick(cup), place(cup, shelf)],再让几何过程逐步填参数、检查可行。
plan-then-check:
1. FF 输出符号计划 [pick(cup), place(cup, shelf)]
2. 为 pick 采样抓取位姿 g, 解 IK 得 q_1, 查碰撞 —— OK
3. 为 place 采样放置位姿 p, 解 IK 得 q_2 —— 失败! q_2 与架子碰撞
4. 回到 2 重采样 g (因为 q_1 限制了能到的 q_2)... 盲目回溯
这就是 TAMP_T1 §2.4 那个指数回溯陷阱,根源同样是信息单向:几何层只回报"失败",不告诉符号层"为什么失败、该怎么调"。符号层不知道是抓取位姿选错了、还是这个放置位置本就够不到,只能盲目重试。
本质洞察:朴素解一(离散化)和朴素解二(先符号后几何)的失败,指向同一个症结——符号决策和几何采样被割裂在两个阶段。离散化把几何采样提前到符号搜索之前(盲目预生成),先符号后几何把它推迟到符号搜索之后(盲目事后填)。两者都没让符号搜索和几何采样交错进行、互相指导。PDDLStream 的全部创新,就是把这两个阶段编织在一起——这是下一节的主题。
2.4 出路:让采样器成为规划的一部分¶
正确的出路,TAMP_T2 §9 已经点明方向:让几何采样器成为符号规划过程的一部分,在规划过程中按需生成参数,并把"这个参数满足某约束"作为信息反馈给符号搜索。
具体地说,我们需要一种机制,它能:
- 按需生成连续参数——不是预先盲目生成(朴素解一),也不是事后盲目填(朴素解二),而是在符号搜索需要时,针对性地生成。
- 把几何过程当黑盒——IK 求解器、碰撞检测、运动规划器已经是成熟工具,机制应该直接调用它们,而不是重新发明。
- 把生成的结果翻译成符号事实——生成了一个可达的抓取位姿后,要以符号规划器能理解的形式告诉它"位姿 \(g\) 是 cup 的一个可达抓取",这样符号搜索才能用上它。
这三点,正是 Stream 这个概念要实现的。Stream 是 PDDLStream 的核心,下一节详解。
2.5 何时该用 PDDLStream:与纯 PDDL、纯运动规划的三方判断¶
在深入 Stream 之前,先建立一个实用判断:什么时候真的需要 PDDLStream,什么时候纯 PDDL 或纯运动规划就够了? 用错工具的代价很大——简单问题上 PDDLStream 是杀鸡用牛刀,复杂问题上纯 PDDL/纯运动规划又根本解不了。
三方对照:
| 你的问题 | 该用什么 | 为什么 |
|---|---|---|
| 纯离散决策,无几何(如积木世界逻辑、调度) | 纯 PDDL(FF/Fast Downward) | 没有连续参数,符号规划器直接解,引入 Stream 是多余开销 |
| 单段无碰撞路径,无符号决策(A 到 B 怎么走) | 纯运动规划(RRT/PRM) | 没有"先做什么"的离散选择,只需几何路径,引入符号层多余 |
| 离散决策 + 连续几何耦合(做什么取决于够不够得到) | PDDLStream / TAMP | 二者纠缠(§2.1),分开做会回溯(§2.3),需要二者协同 |
关键判断信号:离散选择的可行性是否依赖几何? 这是要不要 PDDLStream 的分水岭:
判断流程:
问题有"先做什么、按什么顺序"的离散选择吗?
否 → 纯运动规划(只有"怎么动")
是 ↓
这些离散选择的可行性, 依赖几何吗(够不够得到、放不放得下、走不走得通)?
否 → 纯 PDDL(符号决策与几何无关, 如纯逻辑调度)
是 → PDDLStream / TAMP(离散选择与几何耦合, 本章主题)
本质洞察:PDDLStream 的"适用区",恰好是纯 PDDL 和纯运动规划各自的"盲区"的交集——既有离散决策(纯运动规划处理不了)、又有这些决策与几何的耦合(纯 PDDL 处理不了)。这呼应 TAMP_T1 §2 那句"TAMP 无法被符号规划或运动规划任一单独解决"。判断要不要上 PDDLStream,不看"有没有几何"也不看"有没有离散选择",而看离散选择的可行性是否被几何决定——若是(够不到就不能这么做),就是 TAMP 问题,需要 PDDLStream 这类框架让两层协同。这个判断是 TAMP_T0 §6 选型决策树 Q3("动作可行性是否强烈依赖几何")在本章的具体化。
⚠️ 常见陷阱¶
陷阱 2-1(思维陷阱):以为把连续参数离散化就能用经典规划器。 - 错误描述:预生成有限个候选位姿/位置,当 PDDL 对象,套 FF 求解。 - 现象/后果:粒度粗则漏可行解,粒度细则对象爆炸搜索失控;且盲目预生成大量用不上的候选。 - 根本原因:离散化没解决参数间耦合,只把无穷选择变成大量选择;预生成时不知道哪些候选后续可行。 - 正确做法:用 Stream 按需生成(§3-§4)——在符号搜索需要时针对性采样,而非预先盲目离散化。
陷阱 2-2(思维陷阱):把 plan-then-check 的失败归咎于"重试次数不够"。 - 错误描述:先符号后几何失败时,以为多采样几次、多重试就能解决。 - 现象/后果:增加重试只是延缓爆炸,紧约束问题下指数回溯依旧。 - 根本原因:症结是符号层与几何层信息单向——几何层不告诉符号层"为什么失败",符号层无法针对性修正。 - 正确做法:建立双向信息流——让几何采样的结果(认证事实)反馈进符号搜索(§3 的 Stream 机制)。
练习¶
- (⭐⭐) 对 §2.1 的 pick-and-place,估算朴素离散化的组合规模:若抓取位姿、放置位姿各离散化为 \(N\) 个候选,IK 各有若干解,符号规划器面对的"对象 + 组合"规模大致是多少?说明为什么 \(N\) 增大时这不可行。
- (⭐⭐⭐) 用自己的话解释:为什么 §2.1 的连续参数是"耦合"的?举一个具体的耦合链——某个抓取位姿的选择如何导致后续放置无解。
- (⭐⭐) §2.4 列出了"让采样器成为规划一部分"需要的三点能力。对照 TAMP_T2 §9.4 的策略二,说明 PDDLStream 相比"在分配代价里嵌入一次规划器"更进了一步在哪里。
- (⭐⭐⭐,判断) 用 §2.5 的判断流程,为下列任务各判断该用纯 PDDL、纯运动规划还是 PDDLStream,并说明关键信号(离散选择的可行性是否依赖几何):(a) 电梯调度(多部电梯响应多个楼层请求);(b) 扫地机器人沿固定路线清扫;(c) 在杂物堆里取出底部的书(要先移开压着的物体)。提示:(c) 的"先移开什么"与"能不能取到"耦合,是典型 TAMP。
3. 核心思想:Stream 是采样器与符号搜索之间的接口 ⭐⭐⭐¶
3.1 动机:把"几何能力"封装成符号搜索能调用的东西¶
§2.4 提出了需求,本节给出 PDDLStream 的回答。PDDLStream (Garrett, Lozano-Pérez & Kaelbling, ICAPS 2020) 的核心贡献,是提出 PDDLStream 这一描述语言,引入 stream 作为在 PDDL 中纳入采样过程的接口。
理解 Stream 的最好方式,是先想清楚一个机器人系统里已经有哪些"几何能力":
| 几何能力 | 已有的成熟工具 | 输入 → 输出 |
|---|---|---|
| 求逆运动学 | IK 求解器(如 IKFast、TRAC-IK) | 目标位姿 → 关节角构型 |
| 检测碰撞 | 碰撞检测库(如 FCL) | 构型 → 是否无碰撞 |
| 规划运动 | RRT/PRM(如 OMPL) | 起止构型 → 无碰撞路径 |
| 采样抓取 | 抓取采样器 | 物体 → 候选抓取位姿 |
| 采样放置 | 放置采样器 | 物体 + 区域 → 候选放置位姿 |
这些工具都已存在、都好用。问题只是:符号规划器 FF 不会调用它们,也不理解它们的输出。Stream 就是架在中间的那座桥——它把每个几何能力封装成一个符号搜索能调用、其结果符号搜索能理解的单元。
本质洞察:Stream 的设计哲学是"不重新发明,只做接口"。用于评估和产生这些约束满足值的专用过程——如逆运动学求解器、碰撞检测器、运动规划器——往往是已知的。PDDLStream 不试图把这些几何能力重写进符号规划器,而是把它们当黑盒,只规定一个统一的接口规范,让符号搜索能按需调用、能消费结果。这种"接口而非重写"的思路,是软件工程里"封装"思想在 TAMP 上的体现——也是 PDDLStream 模块化、几何采样器可插拔的根源(呼应 TAMP_T0 §3.4 板块③的"强项")。
3.2 Stream 的两个组件:过程的 + 声明的¶
Stream 的精妙在于它同时有两面。每个 stream 都有过程性和声明性两个组件。
过程组件(procedural):一个条件生成器。
过程组件是一个条件生成器 (conditional generator),一个从输入值元组到输出值元组的有限或无限序列的函数。"条件"二字关键:条件生成器能构造依赖于已有值的新值,例如生成与已有位姿和抓取满足运动学约束的新机器人构型。
用 pick 的 IK Stream 举例:
IK Stream 的过程组件(条件生成器):
输入: 物体位姿 p, 抓取位姿 g (已有的值)
输出: 一串机械臂构型 q_1, q_2, q_3, ... (新生成的值)
含义: 给定要抓的位姿和抓法, 不断吐出能达到该抓取的 IK 解
(依赖输入 p,g —— 这就是"条件")
它是个生成器(generator)——可以一直吐出新解,要几个给几个。这正好对应 §2.4 的"按需生成"。
声明组件(declarative):认证的事实。
光生成值还不够——符号搜索得知道"这个值意味着什么"。声明组件就是规定:当条件生成器吐出一个输出时,它认证 (certify) 了哪些符号事实。
IK Stream 的声明组件(认证事实):
当生成器对输入 (p, g) 吐出构型 q 时,
它认证以下符号事实为真:
(Kin p g q) —— "构型 q 在运动学上达到了对位姿 p 的抓取 g"
于是符号规划器就能在 pick 动作的前提里用 (Kin ?p ?g ?q) 这个谓词
这样,生成器吐出的连续值 \(q\),就通过认证事实 (Kin p g q) 进入了符号世界——符号规划器可以把 q 当成一个对象、把 (Kin p g q) 当成一个为真的命题来用。§2.4 的第三点能力(把结果翻译成符号事实)实现了。
本质洞察:Stream 的两个组件,恰好对应"连续世界"和"离散世界"各一只脚。过程组件(生成器)站在连续世界——它生产连续的几何值(构型、位姿、路径)。声明组件(认证事实)站在离散世界——它把"这个连续值满足某约束"这件事,表达成离散的符号命题。Stream 就是同时踩在鸿沟两岸的桥墩:一端用生成器够到连续几何,一端用认证事实够到符号逻辑。这就是它能缝合 TAMP_T1 §2.5 那道符号-几何鸿沟的根本原因——它不是消除鸿沟,而是在鸿沟上架了可以来回传递信息的桥。
3.3 Stream 如何改变规划的运转方式¶
有了 Stream,规划的运转方式从"两阶段割裂"变成"采样-搜索交织":
有了 Stream 后的运转(对比 §2.3 的 plan-then-check):
符号搜索进行中, 发现需要一个满足 (Kin p g q) 的构型 q
→ 调用 IK Stream 的生成器, 输入 (p,g), 得到 q
→ Stream 认证 (Kin p g q) 为真
→ 这个新事实进入符号世界, 搜索可以继续用它
→ 若后续发现 q 配不出无碰撞放置, 搜索可请求更多 q 或换抓取 g
采样与搜索交替进行, 信息双向流动
对比 §2.3 的 plan-then-check:那里是"符号搜索完全结束,再一次性填几何";这里是"符号搜索过程中按需触发采样,采样结果立即反馈进搜索"。这就是 §2.4 三点能力的合体:按需生成(生成器)+ 黑盒调用(Stream 封装已有工具)+ 翻译成符号事实(认证)。
但这里藏着一个先有鸡还是先有蛋的问题:符号搜索要用 (Kin p g q) 才能继续,但 q 要等生成器算出来才有——而生成器又要等搜索告诉它"需要算 (p,g) 的 IK"才会算。到底先搜索还是先采样? 这个调度问题,正是 PDDLStream 求解算法(§5)要回答的核心。PDDLStream 的巧妙回答是"乐观地假装值已经有了"——这是 §5 的 optimistic 思想,本节先埋下。
把 Stream 带来的改变浓缩成一张"信息流向"对比,能看清它解决了什么:
| 朴素 plan-then-check(§2.3) | 有 Stream 的 PDDLStream | |
|---|---|---|
| 几何信息流向 | 单向:几何层只回报"成功/失败" | 双向:采样结果(认证事实)反馈进搜索 |
| 失败时符号层知道什么 | 只知道"失败了",不知为何 | 知道是哪个 Stream、哪步采样失败(可定位) |
| 采样与搜索的关系 | 割裂(先搜完,再填几何) | 交织(搜索按需触发采样,采样反馈进搜索) |
| 失败后的应对 | 盲目重排整个计划 | 局部重采或换骨架(§6.6 会看到) |
本质洞察:这张表点出了 Stream 的核心价值——它把符号层与几何层之间从"单向汇报"变成"双向对话"。plan-then-check 里几何层像个只会说"行/不行"的哑巴下属,符号层得不到任何可用于改进的信息;有了 Stream,几何层能把"我采到了这个可达构型""这一步 IK 失败了"这类具体信息以符号事实的形式说给符号层听。正是这种双向信息流,让 PDDLStream 避免了 §2.3 的盲目回溯——符号层不再瞎猜,而是基于几何反馈有的放矢地调整。这呼应 TAMP_T2 §9 反复强调的"让上层看见下层的真实信息",是缝合任何分层决策鸿沟的通用良方。
3.4 一组 Stream 描述了什么¶
一个完整的 TAMP 领域,需要一组 Stream 协同。回到 §2.1 的 pick-and-place,需要的 Stream 大致有:
| Stream | 输入 → 输出 | 认证的事实 | 封装的几何能力 |
|---|---|---|---|
| 采样抓取 | 物体 → 抓取位姿 \(g\) | (Grasp obj g) |
抓取采样器 |
| 采样放置 | 物体 + 区域 → 放置位姿 \(p\) | (Place obj region p) |
放置采样器 |
| 逆运动学 | 位姿 + 抓取 → 构型 \(q\) | (Kin p g q) |
IK 求解器 |
| 运动规划 | 起止构型 → 路径 \(\tau\) | (Motion q1 q2 tau) |
RRT/PRM |
| 碰撞检测 | 构型 + 物体位姿 → (测试) | (CFree q p) |
碰撞检测器 |
这组 Stream 加上 PDDL 的 domain(动作的前提/效果,现在前提里可以用 (Grasp ...)、(Kin ...) 这些由 Stream 认证的谓词)和 problem(初始状态、目标),就构成一个完整的 PDDLStream 问题。
本质洞察:注意 Stream 之间是有依赖链的——IK Stream 的输入需要抓取 Stream 的输出(要先有抓取位姿 \(g\) 才能算它的 IK 构型 \(q\)),运动 Stream 的输入需要 IK Stream 的输出(要先有构型才能规划它们之间的路径)。这条依赖链
抓取 → IK → 运动正好对应 §2.1 那条参数耦合链g → q_1 → q_2 → τ。Stream 的依赖结构,把 §2.1 的连续参数耦合显式地编码了出来——这是 PDDLStream 能处理耦合参数的关键:耦合不再是隐藏的陷阱,而是被 Stream 的输入输出关系明确表达,求解器据此有序地采样。
3.5 Stream 与相邻概念的区别¶
为了精确定位 Stream,把它和几个容易混淆的相邻概念辨析一下。这些辨析能帮你避免把 Stream 误用成别的东西。
Stream vs PDDL 动作(action)。 这是最关键的区分(§3 陷阱 3-1 会再强调)。动作改变世界状态(pick 让 HandEmpty 变假),它有 add/delete 效果;Stream 不改变状态,只生成值并认证静态几何事实(Kin(p,g,q) 永真)。一个记法:动作回答"做了什么、状态怎么变",Stream 回答"几何上什么是可能的"。
Stream vs 语义附着(semantic attachment)。 经典规划领域有一个相关概念叫"语义附着"——把某些谓词的求值委托给外部过程。Stream 与它一脉相承但更进一步:语义附着主要做判定(这个谓词在外部过程看来是真是假),而 Stream 不仅能判定(test stream),还能生成满足约束的新值(generator stream 产出 IK 构型)。换句话说,Stream = 语义附着的"判定"能力 + "生成见证者"能力。这个"能生成新对象"的能力,正是 TAMP 需要的——光判定"这个构型可达吗"不够,还得能造出一个可达构型。
Stream vs 普通采样器/生成器。 一个裸的 IK 采样器只是个 Python 函数,产出构型。Stream = 这个采样器(过程组件)+ 一份声明(它要什么输入、产出认证什么事实)。是这份声明让采样器能被符号规划器理解和调度——裸采样器符号规划器不知道何时调用它、它的输出意味着什么。声明是把"几何能力"接入"符号世界"的接口契约。
| 概念 | 改状态? | 能生成新值? | 能判定? | 与符号搜索的关系 |
|---|---|---|---|---|
| PDDL 动作 | 是 | — | — | 搜索的基本步骤 |
| 语义附着 | 否 | 否(仅判定) | 是 | 委托谓词求值 |
| 裸采样器 | 否 | 是 | 可 | 符号搜索不认识它 |
| Stream | 否 | 是(generator) | 是(test) | 声明使其可被调度 |
本质洞察:Stream 在这张表里的独特位置,揭示了它的设计精髓——它集齐了"生成新值"(裸采样器有、语义附着无)和"被符号搜索理解调度"(语义附着有、裸采样器无)两种能力。正是这个组合,让 Stream 成为缝合符号-几何鸿沟的恰当工具:既能像采样器一样在连续空间造出满足约束的值,又能像语义附着一样把结果以符号形式喂给规划器。理解 Stream "是什么"的最精确方式,就是看它如何融合了这两类相邻概念各自缺失的那一半。
⚠️ 常见陷阱¶
陷阱 3-1(概念误区):把 Stream 理解成"另一个动作"。
- 错误描述:以为 Stream 像 PDDL 动作一样,会改变世界状态。
- 现象/后果:试图给 Stream 写"效果"(add/delete),混淆 Stream 与 action。
- 根本原因:Stream 不是动作——它不改变世界状态,只生成值并认证静态事实(如 (Kin p g q) 这种几何关系,永远为真,不会被动作删除)。动作才改变状态(如 pick 让 HandEmpty 变假)。
- 正确做法:明确区分——Stream 认证的是静态事实(几何关系,永真);action 操作的是流式事实/状态(如手是否空,会变)。Stream 负责"几何上什么是可能的",action 负责"做了什么、状态怎么变"。
陷阱 3-2(概念误区):以为 Stream 的生成器必须穷尽所有解。
- 错误描述:认为 IK Stream 要返回所有 IK 解才算正确。
- 现象/后果:试图一次枚举无穷多解,或在多解时纠结返回哪个。
- 根本原因:Stream 是生成器——按需吐出,要几个给几个。求解器需要更多解时会继续请求,不需要时就停。无穷生成器(如不断采样新放置位姿)完全合法。
- 正确做法:把生成器写成"可以一直产出"的形式(Python 的 yield),由求解器控制取多少。
练习¶
- (⭐⭐) 为"倒水"任务设计一个新 Stream:机器人要把杯子里的水倒进碗里,需要一个"倒水位姿"采样器(杯子相对碗的倾倒位姿)。写出这个 Stream 的输入、输出、认证的事实。
- (⭐⭐⭐) §3.4 的 Stream 依赖链是
抓取 → IK → 运动。如果再加一个"放置 → IK → 运动"的链(放置也要 IK 和运动),画出完整的 Stream 依赖图,标出哪些 Stream 的输出是哪些 Stream 的输入。 - (⭐⭐) 区分静态事实与流式事实:对 §3.4 的 pick-and-place,列出哪些谓词是 Stream 认证的静态事实(几何关系)、哪些是动作改变的流式状态(如手的状态)。
4. Stream 的形式化:怎么声明、怎么实现 ⭐⭐⭐¶
§3 讲清了 Stream 是什么(采样器与符号搜索的接口)。本节落到具体:一个 Stream 在 PDDLStream 里怎么声明(声明组件的语法)、怎么实现(过程组件的代码)。这是从"理解概念"到"能自己写"的一跳。
4.1 Stream 声明的四个要素¶
一个 Stream 声明回答四个问题:它要什么输入、产什么输出、输入要满足什么前提、输出认证什么事实。对应四个字段:
| 字段 | 英文 | 含义 | IK Stream 的例子 |
|---|---|---|---|
| 输入参数 | :inputs |
生成器需要哪些输入值 | ?p ?g(位姿、抓取) |
| 输入前提 | :domain |
输入参数必须满足的事实(否则不该调用) | (Pose ?p) (Grasp ?g) |
| 输出参数 | :outputs |
生成器产出哪些新值 | ?q(构型) |
| 认证事实 | :certified |
输出满足什么——产出后认证为真的事实 | (Kin ?p ?g ?q) (Conf ?q) |
为什么需要 :domain(输入前提)? 因为不是任意输入都该喂给生成器。IK Stream 的输入 ?p ?g 必须分别是合法的位姿和抓取——:domain 声明了这个要求。求解器只在输入满足 :domain 时才调用这个 Stream,避免无意义的调用(如对一个不是抓取的值求 IK)。这也建立了 §3.4 的 Stream 依赖链:IK 的 :domain 要求 (Grasp ?g),而 (Grasp ?g) 由抓取 Stream 认证——于是 IK 必然在抓取之后。
4.2 PDDLStream 的 Stream 声明语法¶
PDDLStream 用一种类 PDDL 的语法声明 Stream(与 TAMP_T1 §7.2 见过的形式一致,这里讲清每个字段):
; 逆运动学 Stream 的声明
(:stream sample-ik
:inputs (?p ?g) ; 输入: 位姿、抓取
:domain (and (Pose ?p) (Grasp ?g)) ; 输入前提: ?p 是位姿, ?g 是抓取
:outputs (?q) ; 输出: 构型
:certified (and (Conf ?q) ; 认证: ?q 是合法构型
(Kin ?p ?g ?q))) ; ?q 运动学达到对 ?p 的抓取 ?g
读法:这个名为 sample-ik 的 Stream,在输入 ?p ?g 满足 (Pose ?p) 且 (Grasp ?g) 时可被调用;调用后产出构型 ?q,并认证 (Conf ?q) 和 (Kin ?p ?g ?q) 为真。
再看一个采样放置位姿的 Stream(无 IK 那样的强输入依赖,但依赖物体和区域):
; 采样放置位姿 Stream
(:stream sample-place
:inputs (?obj ?region) ; 输入: 物体、区域
:domain (and (Graspable ?obj) (Region ?region))
:outputs (?p) ; 输出: 放置位姿
:certified (and (Pose ?p) ; 认证: ?p 是位姿
(Supported ?obj ?p ?region))); ?p 让 ?obj 稳定在 ?region
这些认证的谓词如何被动作用上? 在 PDDL domain 里,动作的前提现在可以引用这些由 Stream 认证的谓词。例如 pick 动作:
(:action pick
:parameters (?obj ?p ?g ?q)
:precondition (and (AtPose ?obj ?p) ; 流式: 物体当前在位姿 p (会变)
(Grasp ?g) ; 静态: g 是抓取 (Stream 认证)
(Kin ?p ?g ?q) ; 静态: q 达到对 p 的抓取 g (Stream 认证)
(AtConf ?q) ; 流式: 机械臂当前在构型 q
(HandEmpty)) ; 流式: 手空
:effect (and (Holding ?obj ?g)
(not (HandEmpty))
(not (AtPose ?obj ?p))))
注意前提里 (Grasp ?g)、(Kin ?p ?g ?q) 是 Stream 认证的静态事实(几何关系,永真),而 (AtPose ...)、(HandEmpty)、(AtConf ...) 是动作改变的流式状态(会变)。这正是 §3 陷阱 3-1 强调的区分——Stream 认证静态几何关系,动作操作动态状态。
一个 PDDLStream 问题由哪几部分组织? 初学时容易被多个文件搞晕,这里厘清。一个完整 PDDLStream 问题通常分三个声明文件 + 一份 Python 代码(§6 会完整演示):
| 部分 | 文件/形式 | 内容 | 类比纯 PDDL |
|---|---|---|---|
| domain | domain.pddl |
动作的前提/效果(前提可用 Stream 认证的谓词) | 与纯 PDDL 的 domain 一致 |
| stream 声明 | stream.pddl |
各 Stream 的 :inputs/:domain/:outputs/:certified |
纯 PDDL 没有,这是扩展 |
| problem | 代码或 problem.pddl |
初始状态 init、目标 goal | 与纯 PDDL 的 problem 一致 |
| stream 生成器 | Python 代码 | 各 Stream 的条件生成器实现 + stream_map 注册 | 纯 PDDL 没有,这是扩展 |
本质洞察:这个文件结构清楚地显示了 PDDLStream "在 PDDL 之上扩展"的本质——domain 和 problem 与纯 PDDL 几乎一样(这正是 §10.2 说的"遵循 PDDL 约定"的好处,能复用 FastDownward),新增的只有 stream 声明(声明组件)和 stream 生成器(过程组件)这两块。换句话说,会写纯 PDDL 的人学 PDDLStream,要补的就是"如何声明 Stream + 如何实现生成器"这两件事——其余都是已有知识。这也是为什么本章 §4 把重心放在 Stream 的声明与实现上:那正是 PDDLStream 相对纯 PDDL 的全部增量。
4.3 实现条件生成器(过程组件)¶
声明只说了"这个 Stream 要什么、给什么",真正干活的是过程组件——条件生成器的代码。它是一个普通的 Python 函数,输入参数、yield 输出值。
Step 1:为什么生成器用 yield 而非 return。
为什么用 yield (生成器) 而非 return (一次性返回)?
因为 §3 讲的"按需生成": 求解器可能只需要 1 个 IK 解就够了, 也可能
需要更多(若第一个配不出无碰撞放置)。用 yield, 求解器要一个算一个,
不浪费; 用 return 一次算完所有, 要么算不完(无穷解), 要么浪费。
条件生成器的骨架:
def gen(input1, input2):
while 还能产出:
value = 用几何过程算一个解(input1, input2)
if value 有效:
yield (value,) # 元组形式产出, 对应 :outputs
Step 2:正确写法——IK Stream 的生成器。
import numpy as np
def sample_ik_gen(pose, grasp, robot, max_attempts=20):
"""IK Stream 的条件生成器。
输入: 物体位姿 pose, 抓取 grasp。产出: 能达到该抓取的构型 q。
对应声明的 :inputs (?p ?g) :outputs (?q)。"""
# 计算目标末端执行器位姿 = 物体位姿 ∘ 抓取偏移
target_ee_pose = compose(pose, grasp)
for _ in range(max_attempts):
# 调用 IK 求解器(黑盒) —— 加随机种子构型以获得不同解
seed = random_seed_conf(robot)
q = solve_ik(robot, target_ee_pose, seed=seed) # 已有的 IK 工具
if q is not None and within_joint_limits(robot, q):
yield (q,) # 产出一个构型(元组, 对应单输出 ?q)
# 没解就继续尝试下一个随机种子, 直到 max_attempts
def sample_grasp_gen(obj):
"""抓取 Stream 的生成器: 给定物体, 不断产出候选抓取位姿。
这是个无穷生成器 —— 可以一直采样新抓取。"""
while True:
g = sample_grasp_from_model(obj) # 抓取采样器(黑盒)
if g is not None:
yield (g,)
def plan_motion_gen(q1, q2, obstacles):
"""运动 Stream 的生成器: 给定起止构型, 产出无碰撞路径。
输入依赖 IK Stream 的输出(q1,q2) —— 体现 §3.4 的依赖链。"""
path = birrt(q1, q2, obstacles) # RRT 运动规划器(黑盒)
if path is not None:
yield (path,) # 成功则产出路径; 失败则不产出(空生成器)
Step 3:错误写法并解释为什么错。
# ❌ 错误 1: 生成器用 return 一次返回所有解
def sample_ik_wrong(pose, grasp, robot):
solutions = []
for _ in range(1000): # 强行枚举 1000 个
q = solve_ik(robot, compose(pose, grasp), seed=random_seed_conf(robot))
if q is not None: solutions.append(q)
return solutions
# 问题: (1)若求解器只需要1个解, 白算999个; (2)对无穷解的 Stream(如采样抓取)
# 根本无法 return 完。必须用 yield 让求解器按需取。
# ❌ 错误 2: 生成器不检查输入前提(:domain), 对非法输入硬算
def sample_ik_wrong2(pose, grasp, robot):
# 没确认 pose 是合法位姿、grasp 是合法抓取就直接算
yield (solve_ik(robot, compose(pose, grasp)),) # 可能对垃圾输入算出垃圾
# 问题: :domain 的作用是让求解器只在输入合法时调用。但生成器实现也应稳健 ——
# solve_ik 返回 None 时要处理, 不能把 None 当构型 yield 出去污染符号世界。
# ❌ 错误 3: 认证了生成器并未真正保证的事实
# 声明 :certified (CFree ?q) 但生成器没做碰撞检测就 yield
# 问题: 认证事实是对符号搜索的"承诺"——承诺这个值满足该约束。若没真正检查就认证,
# 符号搜索会基于假事实规划, 最终计划几何上不可行。认证必须名副其实。
Step 4:Stream 声明与生成器的对应关系。
# === 声明(declarative) ←→ 生成器(procedural) 的对应 ===
#
# 声明: 生成器:
# (:stream sample-ik
# :inputs (?p ?g) ←→ def sample_ik_gen(pose, grasp, ...):
# :domain (Pose ?p) # (求解器保证传入的 pose 满足 Pose)
# (Grasp ?g) # (传入的 grasp 满足 Grasp)
# :outputs (?q) ←→ # yield (q,) ← 产出对应 ?q
# :certified (Kin ?p ?g ?q)) # yield 的 q 必须真的满足 Kin(p,g,q)
#
# 关键约定:
# - :inputs 顺序 = 生成器参数顺序
# - :outputs 顺序 = yield 元组的元素顺序
# - :certified 是生成器对每个产出值必须信守的"承诺"(见 Step3 错误3)
4.4 测试 Stream:test stream 与 fluent stream¶
除了"生成值"的 Stream,还有两类特殊 Stream,补全表达力:
test stream(测试型):不产出新值,只测试输入是否满足某约束,认证一个事实或失败。典型是碰撞检测:
(:stream test-cfree ; 碰撞测试, 不产新值
:inputs (?q ?p)
:domain (and (Conf ?q) (Pose ?p))
:outputs () ; 无输出
:certified (CFree ?q ?p)) ; 若 q 与 p 处物体无碰撞, 认证 CFree
它的生成器是:检查 ?q 和 ?p 是否无碰撞,无碰撞就 yield ()(认证 CFree),碰撞就什么都不 yield(不认证)。test stream 让"检查类"几何能力(碰撞、可见性)也能纳入 Stream 框架。
fluent stream(流式条件型):认证的事实依赖于当前状态(而非纯静态几何)。前面 §3 反复强调 Stream 认证的是"永真的静态几何关系",但有些约束并非永真——它依赖动作发生时世界的状态。fluent stream 正是处理这类约束的。
为什么需要它?一个可见性的例子。 设想机器人要"看见"某个物体才能检测它(detect 动作的前提是"目标可见")。但"可见"不是静态的——它取决于当前其他物体在哪:如果另一个物体挡在中间,目标就不可见。这个约束 Visible(camera_pose, target) 依赖场景里其他物体的当前位姿(流式状态),不是永真的几何关系。
fluent stream 处理依赖状态的约束:
(:stream test-visible
:inputs (?cam_q ?target)
:domain (and (Conf ?cam_q) (Movable ?target))
:fluents (AtPose ?obj ?p) ; ← 声明它依赖的流式状态!
:outputs ()
:certified (Visible ?cam_q ?target))
含义: 是否可见, 取决于当前所有物体的 AtPose(流式)
—— 别的物体挡住就不可见, 物体移开就可见
对比普通 test stream: test-cfree 的碰撞(若障碍固定)是静态的;
但与可移动物体的可见性/碰撞是流式的
它与普通 test stream 的关键区别:普通 test stream 认证的事实只依赖输入值(静态);fluent stream 额外依赖当前状态(通过 :fluents 声明),所以同一组输入在不同状态下可能认证不同结果。这呼应 §7.2 模式二——依赖运行时场景的约束(与可移动物体的碰撞、可见性)正是 fluent stream 的用武之地。
本质洞察:generator stream(产值)、test stream(测试)、fluent stream(依赖状态)三类,覆盖了 TAMP 里几何过程的三种形态——生产满足约束的值(采样抓取、IK、运动)、判定值是否满足约束(碰撞、可见性)、判定依赖当前状态的约束。这个分类不是随意的,它对应几何过程在逻辑里扮演的三种角色:存在量词的"见证者"(产值证明"存在一个满足的值")、谓词的"判定器"(测试"这个值满足吗")、状态相关谓词的判定器("在当前状态下这个值满足吗")。认得这三类,你就能为任何新的几何能力判断它该写成哪类 Stream——这是为新领域设计 Stream 的起点(§7.6 工作流第 2 步)。一个判断口诀:要产值用 generator、纯几何判定用 test、判定依赖"其他物体当前在哪"用 fluent。
⚠️ 常见陷阱¶
陷阱 4-1(编程陷阱):生成器用 return 一次返回所有解。
- 错误描述:把条件生成器写成枚举所有解后 return(见 §4.3 Step 3 错误 1)。
- 现象/后果:对无穷解 Stream(采样抓取)无法返回完;对有限解也浪费计算(求解器可能只需一个)。
- 根本原因:Stream 是按需生成器,求解器控制取多少。return 破坏了"按需"。
- 正确做法:用 yield,让求解器要一个算一个。无穷生成器写成 while True: yield ...。
陷阱 4-2(概念误区):认证了生成器并未保证的事实。
- 错误描述::certified 声明 (CFree ?q),但生成器没做碰撞检测就产出(见 §4.3 Step 3 错误 3)。
- 现象/后果:符号搜索基于假事实规划,最终计划几何上不可行,执行时碰撞。
- 根本原因:认证事实是对符号搜索的承诺。承诺不兑现,搜索的正确性就崩塌。
- 正确做法:认证必须名副其实——生成器产出的每个值,必须真正满足所有 :certified 事实(该检查的检查、该约束的约束)。
陷阱 4-3(概念误区):混淆 Stream 认证的静态事实与动作的流式状态。
- 错误描述:把几何关系(Kin、Grasp)当成会被动作改变的状态,或反之。
- 现象/后果:domain 建模错误——把静态事实放进动作效果去 add/delete,或把动态状态当成 Stream 认证。
- 根本原因:静态事实(几何关系,永真)和流式状态(随动作变)是两类不同的命题。
- 正确做法:Stream 只认证静态几何关系((Kin p g q) 永远成立);动作只增删流式状态(HandEmpty、AtConf)。建模时先分清每个谓词属于哪类。
练习¶
- (⭐⭐,声明) 为 §3 练习 1 的"倒水"Stream 写出完整的 PDDLStream 声明(
:inputs/:domain/:outputs/:certified),并说明它依赖哪些前序 Stream 的输出。 - (⭐⭐⭐,实现) 实现 §4.4 的
test-cfree碰撞测试 Stream 的生成器:输入构型q和位姿p,无碰撞则yield (),碰撞则不产出。用一个简化的碰撞检测(如球体距离)。 - (⭐⭐⭐,调试) 给定一个 IK 生成器,它偶尔
yield出超出关节限位的构型。指出这违反了哪个认证事实,会导致什么后果,如何修复(对照 §4.3 Step 3)。 - (⭐⭐⭐⭐,设计) 判断下列几何能力各应写成哪类 Stream(generator/test/fluent)并说明理由:(a) 采样一个可见某物体的相机位姿;(b) 检查两个物体是否堆叠稳定;(c) 判断从当前位置能否看见目标(依赖当前其他物体的遮挡)。
5. PDDLStream 怎么求解:乐观、认证、归约 ⭐⭐⭐⭐¶
§4 讲清了 Stream 怎么定义。但还有 §3.3 末尾埋下的核心难题没回答:符号搜索要用 Stream 认证的事实才能继续,而事实要等生成器算出来才有,生成器又要等搜索说"需要"才算——先有鸡还是先有蛋? 本节讲 PDDLStream 求解算法如何破解这个循环。这是本章最硬核的一节(⭐⭐⭐⭐)。
本节读法导引:§5.1(归约为有限 PDDL)、§5.2(optimistic 乐观对象)、§5.3(certified/level)是必须拿下的三个核心概念,它们环环相扣地破解"鸡生蛋"。§5.4(三种算法)告诉你这三个概念怎么组合成实际可用的求解器。§5.5(概率完备性)和 §5.6(外部成本/懒惰)偏理论与进阶,第一遍可略读、回头优化性能或深究原理时再细读。建议第一遍:§5.1→§5.2→§5.3→§5.4 主干,先建立"采样-搜索如何咬合"的整体图景。
5.1 关键洞察:归约为一系列有限 PDDL 问题¶
PDDLStream 求解的总纲,是 ICAPS 2020 论文的核心贡献之一:提供领域无关的算法,把 PDDLStream 问题归约为一系列有限 PDDL 问题。
这句话是理解一切的钥匙。它的意思是:
- PDDLStream 问题本身是"无限"的——连续参数有无穷多取值,Stream 能产出无穷多个值,无法直接交给只能处理有限对象的经典规划器。
- 但在任一时刻,已经被生成器产出的值是有限的(到目前为止采样了多少个抓取、多少个构型)。把这些有限的值连同它们的认证事实,组成一个有限的 PDDL 问题,就能交给经典规划器(FF/Fast Downward)求解。
- 如果这个有限 PDDL 问题解出来了,且解里用到的所有值都是真实采样出来的,那就是一个真正可行的计划。如果解不出来,就采样更多值,扩大有限问题,再求解。
本质洞察:PDDLStream 把一个"无限"的混合问题,变成"一串逐渐变大的有限问题"。这与 TAMP_T1 §4.2 讲的 PRM 思想异曲同工——PRM 也是不断采样新构型、扩大 roadmap、再在图上搜索,直到找到路径。"Incremental" PDDLStream 算法交替进行采样和搜索,直到找到计划,类似 PRM 可以继续采样额外构型并搜索图的方式。采样扩大问题、搜索尝试求解、失败则采样更多——这个"采样-搜索"循环是 PDDLStream(乃至大量采样式 TAMP)的元结构。认得它,你就抓住了 §5 的灵魂。
剩下的问题是:怎么知道该采样哪些值? 盲目采样所有 Stream 的所有输入组合会爆炸(回到 §2.2 离散化的老问题)。PDDLStream 的高明之处,在于用 optimistic(乐观) 思想来引导采样——只采样"搜索真正需要"的值。
5.2 optimistic 对象:先假装值已经有了¶
破解"鸡生蛋"循环的关键一招:乐观地假装 Stream 想产出的值已经存在,先用这个假想的值去做符号搜索,看搜索需不需要它;如果搜索出的计划用到了这个假想值,再真正去采样它。
具体机制:
optimistic(乐观)思想:
对每个能产出值的 Stream, 引入一个"乐观对象"(optimistic object) ——
一个占位符, 代表"这个 Stream 将来能产出的某个值"。
例如: IK Stream 的乐观对象 q̂, 代表"将来某个能达到抓取的构型"。
用这些乐观对象 + 它们(假想)认证的事实, 组成一个乐观的 PDDL 问题, 交给搜索。
搜索若找到一个用了 q̂ 的计划骨架 —— 这就告诉我们: "需要为这一步真正采样一个 q"。
于是针对性地调用 IK Stream 采样真实的 q, 替换 q̂。
这就是论文标题里 "Optimistic" 的含义。每个乐观输出代表一个唯一的乐观输出值;在某个例子里总共创建了 13 个 stream 实例,位姿和抓取 stream 实例都是 level 1,IK stream 实例是 level 2,运动 stream 实例……——乐观对象按依赖深度分层(level,见 §5.3)。
本质洞察:optimistic 思想破解"鸡生蛋"的方式,是把"采样"和"搜索"的顺序倒过来想。朴素思路是"先采样出值,搜索才能用"(鸡生蛋困境);optimistic 思路是"先让搜索用假想的值规划,规划用到了再去采样"——让搜索来指导采样,而非采样盲目地为搜索备料。这正好解决了 §2.2 离散化"盲目预生成"的病根:不再预先生成一堆可能用不上的值,而是让符号搜索告诉我们"这一步需要一个满足 Kin 的构型",再针对性采样。搜索指导采样、采样服务搜索——双向的、有的放矢的,这是 PDDLStream 高效的根源。
5.3 certified facts 与 level:管理"乐观"的真实性¶
optimistic 带来一个新问题:搜索基于"假想值"找到的计划,是乐观计划 (optimistic plan)——它可能落不了地(假想的构型实际 IK 无解)。所以需要机制管理乐观值的真实性。
certified facts(认证事实)的两种状态。 一个认证事实可能是:
- 真实认证的:由生成器真正产出的值认证的事实(如真采样出构型 \(q\),认证
(Kin p g q))——这是板上钉钉的。 - 乐观认证的:由乐观对象"假想"认证的事实(如乐观对象 \(\hat{q}\) 假想认证
(Kin p g q̂))——这是待验证的承诺。
求解过程就是不断把"乐观认证"变成"真实认证":搜索用乐观事实找到计划骨架 → 针对骨架用到的乐观对象去真正采样 → 采样成功则乐观事实升级为真实事实 → 全部升级成功,计划落地。
level(层级):乐观对象的依赖深度。 §3.4 讲过 Stream 有依赖链(抓取→IK→运动)。乐观对象也继承这个依赖:
level 分层(对应 §3.4 依赖链):
level 1: 抓取乐观对象 ĝ、放置乐观对象 p̂ (不依赖别的乐观对象)
level 2: IK 乐观对象 q̂ (依赖 level 1 的 ĝ/p̂)
level 3: 运动乐观对象 τ̂ (依赖 level 2 的 q̂)
level 控制乐观对象的"展开深度"——只展开到 level 1,IK 和运动的乐观对象还没引入,搜索可能找不到完整计划;展开到更高 level,引入更多乐观对象,搜索能找到更长的计划骨架,但乐观问题也更大。在某个例子里 OPTIMISTIC 在 level ≤ 2 时找不到计划,当 level = 3 时乐观 stream……——这说明 level 要够深,才能让乐观计划覆盖完整的参数依赖链。
本质洞察:level 本质是在控制"乐观"的程度——展开多少层假想值。这是一个探索的深度旋钮:level 太浅,假想值不够,搜索找不到计划(连乐观的都找不到);level 太深,假想值太多,乐观问题膨胀、且很多假想值最终采样不出来(白展开)。PDDLStream 的算法(§5.4)本质就是在调度这个旋钮——以及调度"何时停止加深 level、转而去真正采样验证"。这个"展开假想 vs 落实真实"的张力,是 §5.4 三种算法的分水岭。
5.4 三种算法:Incremental、Focused、Adaptive¶
有了 optimistic/certified/level 三个概念,PDDLStream 的三种算法就是它们的不同调度策略。论文引入一个算法,动态平衡探索新候选计划与利用已有计划,以解决紧约束问题并局部优化产生低代价解。
算法一:Incremental(增量式)——纯采样-搜索循环,不用乐观。
最简单,不引入乐观对象。交替进行采样和搜索直到找到计划:搜索若没找到到目标的动作序列,就通过采样器生成更多认证事实,再次搜索。
Incremental:
while 未找到计划:
1. 用所有 Stream 各采样一批新值, 认证新事实 (盲目地多采一层)
2. 把当前所有真实值 + 事实组成有限 PDDL, 交 FF 求解
3. 解出 → 返回; 解不出 → 回到 1, 再采一批
优点:简单、概率完备(采样够多终能找到)。缺点:盲目采样——不知道哪些值有用,每轮对所有 Stream 都采,在紧约束/多对象问题上慢(呼应 §2.2 离散化的盲目性,只是这里是渐进的)。
算法二:Focused(聚焦式)——用乐观引导,只采样搜索需要的。
引入乐观对象。先用乐观对象搜索出计划骨架,只为骨架用到的乐观对象采样。
Focused:
while 未找到计划:
1. 引入乐观对象(到某 level), 组成乐观 PDDL, 交 FF 求解
2. 得到乐观计划骨架 —— 它指明了"需要哪些值"
3. 只为骨架用到的乐观对象, 调用对应 Stream 真正采样
4. 采样全成功 → 乐观事实升级为真实 → 计划落地, 返回
某步采样失败 → 记录失败, 回到 1 (避开这个走不通的骨架)
优点:有的放矢——只采样搜索真正需要的值,紧约束问题上远快于 Incremental。缺点:乐观计划可能反复落不了地,需要好的失败处理。
算法三:Adaptive(自适应)——动态平衡探索与利用。
这是论文主推的算法。它动态平衡两件事:探索新的乐观计划骨架(exploration)vs 在已有骨架上反复采样尝试落地(exploitation)。这使算法能贪心地搜索参数绑定空间,更快解决紧约束问题,同时局部优化以产生低代价解。
Adaptive(自适应):
动态决定: 是该花精力为当前乐观计划多采样(利用),
还是放弃它、去搜索新的乐观计划(探索)?
通过追踪每个 Stream 的采样成功率/代价, 自适应分配采样预算。
—— 在 Incremental 的盲目和 Focused 的激进之间取动态平衡。
三种算法对比:
| 算法 | 用乐观? | 采样策略 | 强项 | 弱项 |
|---|---|---|---|---|
| Incremental | 否 | 盲目,每轮全采 | 简单、易实现 | 紧约束/多对象慢 |
| Focused | 是 | 聚焦,只采骨架需要的 | 紧约束快 | 乐观计划反复落空 |
| Adaptive | 是 | 动态平衡探索/利用 | 综合最优,论文主推 | 实现最复杂 |
实战中怎么选? 不必纠结理论细节,按下面的简单规则即可:
PDDLStream 算法选择(实战规则):
默认 → Adaptive (综合最优, 大多数情况的首选)
调试/教学/想看清行为 → Incremental (最简单, 行为最易理解)
确认问题紧约束、Adaptive 仍慢 → 检查是不是采样器问题(§7.3, §8.4)
而非继续换算法
注: 三者都是概率完备的(§5.5), 选择影响的是"多快找到"而非"能否找到"
关键认识:三种算法的差别是效率而非正确性——它们都概率完备(§5.5),都不会错过存在的解,区别只在多快找到。所以"换算法"是性能调优手段,不是"解不出来"的救命稻草(解不出来先查采样器,§8.4)。
本质洞察:三种算法是"采样盲目程度"光谱上的三个点。Incremental 最盲目(不用乐观,全采);Focused 最激进(完全跟着乐观骨架走,只采它要的);Adaptive 在两者间动态调节。这个光谱呼应了所有"采样 + 搜索"类算法的共性张力——探索(试新方向)vs 利用(深挖当前方向)。你在强化学习(探索-利用困境)、MPPI 采样(§MPPI 线)、甚至 §10 要对比的 LGP 优化(全局探索 vs 局部下降)里都会反复见到它。PDDLStream 的 Adaptive 算法,就是把这个经典张力用"采样预算的自适应分配"来解决。
⚠️ 常见陷阱¶
陷阱 5-1(概念误区):以为 PDDLStream 直接把无限问题交给规划器。 - 错误描述:认为 PDDLStream 有某种能处理连续参数的"特殊规划器"。 - 现象/后果:找不到这种规划器,误解整个框架。 - 根本原因:PDDLStream 不处理无限——它把问题归约为一系列有限 PDDL 问题(§5.1),每个都交给普通的 FF/Fast Downward。连续性由 Stream 采样消化,符号搜索始终面对有限对象。 - 正确做法:理解"归约为有限问题序列"是核心机制——采样把无限变有限,经典规划器解有限问题。
陷阱 5-2(概念误区):把乐观计划当成最终可执行计划。 - 错误描述:Focused/Adaptive 搜出乐观计划骨架就以为完成了。 - 现象/后果:乐观计划用的是假想值,直接执行会失败(假想构型可能 IK 无解)。 - 根本原因:乐观计划只是"如果这些值存在就可行"的骨架,必须经采样验证、把乐观值替换为真实值才能落地。 - 正确做法:乐观计划只是中间产物——它指明"需要采样哪些值",必须经 §5.3 的"乐观→真实"升级才是可执行计划。
陷阱 5-3(思维陷阱):在所有问题上都用 Incremental(因为它简单)。 - 错误描述:图省事只用 Incremental,不管问题规模。 - 现象/后果:紧约束、多对象问题上 Incremental 盲目采样,慢到不可用。 - 根本原因:Incremental 不用乐观引导,盲目采所有 Stream,组合随对象数爆炸。 - 正确做法:简单/少对象问题 Incremental 够用;紧约束/多对象用 Focused 或 Adaptive(乐观引导,只采需要的)。
练习¶
- (⭐⭐⭐) 用自己的话解释"归约为一系列有限 PDDL 问题":为什么任一时刻的已采样值是有限的?为什么解出的有限问题(且值真实)就是可行计划?
- (⭐⭐⭐) 对 §2.1 的 pick-and-place,列出各 Stream 的乐观对象及其 level(参照 §5.3 的分层)。为什么 level 必须至少到运动 Stream 的层级,乐观搜索才能找到完整计划骨架?
- (⭐⭐⭐⭐) 对比 Incremental 和 Focused 在"桌上 10 个物体、只有 1 种可行抓放组合"这个紧约束问题上的行为:各自会采样多少、为什么 Focused 快得多?
- (⭐⭐⭐⭐,跨节综合) 把 §5.4 的"探索 vs 利用"张力,与 TAMP_T2 §9.4 的"解耦+迭代 vs 联合优化"对照——它们是否反映了相似的权衡?用 2-3 句话说明。
5.5 理论保证:概率完备性从哪来 ⭐⭐⭐⭐¶
§5.1-§5.4 讲清了 PDDLStream 怎么跑,但有一个理论问题还没回答:这套采样-搜索循环,能保证最终找到解吗? 如果解存在,PDDLStream 会不会永远找不到、白白采样下去?这是任何采样式算法都必须回答的——回忆 TAMP_T1 §4 讲 RRT 时也强调过"概率完备性"。本节给出 PDDLStream 的理论性质。这部分偏理论,第一遍可只记住结论。
结论先行:PDDLStream 的两个算法都是概率完备的。 PDDLStream 的理论基础,是 Garrett、Lozano-Pérez、Kaelbling 的姊妹论文 (2018, IJRR, "Sampling-based methods for factored task and motion planning")。这些算法是概率完备的,给定一组充分的条件采样器。"概率完备"的含义与 RRT 一致:如果解存在,那么随着采样数趋于无穷,找到解的概率趋于 1。
完备性依赖什么——这是关键的限定。 注意上面那句话的后半截"给定一组充分的条件采样器"。算法的完备性完全依赖于条件采样器。这句话点破了 PDDLStream 理论的核心结构:
本质洞察:PDDLStream 把"完备性"这个责任,从规划算法转移到了采样器。算法本身(采样-搜索循环)的完备性是有保证的——只要采样器"够好",算法就能找到解。但"够好"的责任落在了你写的 Stream 生成器身上:如果你的 IK 采样器永远采不到那个唯一可行的构型,PDDLStream 再怎么循环也找不到解。这与 §4 陷阱 4-2 "认证必须名副其实"是一体两面——Stream 不仅要认证真实的事实,还要能采样到所有"必要"的值。理解这一点,你就知道当 PDDLStream 找不到解时,该先怀疑的是 Stream 设计(采样器覆盖不全),而非算法本身。
factored transition system:为什么"因子化"是理论根基。 姊妹论文的理论框架,把机器人任务与运动规划问题建模为 factored transition system(因子化转移系统)——含连续和离散状态、控制空间的离散时间规划问题;这个表述能凸显问题中由"只影响少数变量的约束"产生的因子结构。
"因子化"是什么意思、为什么重要?回到 §2.1 的参数耦合:抓取 \(g\) 决定构型 \(q_1\),\(q_1\) 约束 \(q_2\)……乍看所有参数纠缠在一起。但每个约束其实只牵涉少数几个变量——Kin(p, g, q) 只关联位姿、抓取、构型这三个,不关联场景里其他物体的位姿。这种"约束只影响局部变量"的性质就是因子化。直接暴露因子结构,使我们能设计出更高效地采样状态/控制、并搜索所得空间的算法。
因子化的意义(对比"整体"看待):
整体视角: 所有参数 (g, q1, p, q2, τ, 其他物体位姿...) 是一个高维联合空间
—— 维度灾难, 无法采样
因子化视角: 约束 Kin(p,g,q) 只关联 3 个变量 → 只需为这 3 个采样
约束 Motion(q1,q2,τ) 只关联 3 个变量 → 独立采样
Stream 的依赖链正好沿着因子结构组织采样
—— 因子化把"采无穷维联合分布"拆成"沿依赖链采一串低维条件分布"
降维约束与子流形:为什么 TAMP 比一般运动规划更难。 姊妹论文还有一个深刻的理论贡献。论文分析了问题解空间的拓扑,特别是存在降维约束 (dimensionality-reducing constraints) 时的情形,并将这些条件连接到能组合产生该子流形上值的条件采样器。
这点为什么重要?因为 TAMP 的可行解往往落在一个零测度的子流形上。例如"构型 \(q\) 恰好让末端达到抓取位姿 \(g\)"——满足 Kin 的 \(q\) 不是一个区域,而是构型空间里一个低维曲面(IK 解的流形)。在零测度集合上随机采样,命中概率为零——这正是为什么不能盲目在构型空间随机采样然后检查(朴素解会几乎永远采不中)。Stream 的价值在于:IK 采样器直接在那个子流形上采样(解 IK 而非随机撒点),把"命中零测度集"变成"在低维流形上采样"。
本质洞察:这条理论揭示了 TAMP 比一般运动规划更难的根本原因——一般运动规划的可行解是构型空间里的正测度区域(自由空间有体积,随机采样能命中);而 TAMP 的可行解常被"恰好达到""恰好接触""恰好对齐"这类等式约束压到零测度子流形上(没有体积,随机采样命中概率为零)。Stream(尤其是 IK、抓取这类采样器)的本质,是用专用过程直接在子流形上生成样本,绕开"随机撒点命中零测度集"的不可能。这也解释了为什么 §3.1 强调"不重新发明、只做接口"——IK 求解器之所以不可替代,正因为它知道如何在
Kin约束的子流形上采样,这是通用随机采样器做不到的。
与 PRM/FFRob 的形式对应。 §5.1 提过 Incremental 算法类似 PRM。姊妹论文把这个类比形式化了:Incremental 算法可以看作运动规划 PRM 和 TAMP 中迭代 FFRob 算法的推广——二者都在各自的问题类上交替进行穷举采样与搜索阶段。也就是说:
| 算法 | 问题类 | "采样"采什么 | "搜索"在什么上搜 |
|---|---|---|---|
| PRM | 运动规划 | 构型 | roadmap 图 |
| 迭代 FFRob | pick-and-place TAMP | 物体位姿 + 构型 | 含几何约束的状态空间 |
| PDDLStream Incremental | 任意条件采样器的 TAMP | 任意 Stream 的输出值 | 归约的有限 PDDL |
PDDLStream 是这一谱系的最一般者——它把"采样-搜索"从 PRM 的纯几何、FFRob 的固定 pick-place,推广到了任意条件采样器定义的问题。这就是它"通用 TAMP 框架"地位的理论来源。
⚠️ 常见陷阱¶
陷阱 5-4(概念误区):以为概率完备意味着"一定能在合理时间找到解"。 - 错误描述:把"概率完备"理解成"实践中总能快速求解"。 - 现象/后果:在紧约束问题上等很久不出解,误以为算法出错。 - 根本原因:概率完备是渐近性质(采样数→∞ 时找到概率→1),不保证有限时间内找到,更不保证快。紧约束/子流形狭窄时可能需要海量采样。 - 正确做法:理解概率完备是"不会错过解"的保证,不是"快"的保证。要快需靠 Focused/Adaptive 的乐观引导、好的采样器设计。
陷阱 5-5(思维陷阱):把"找不到解"归咎于算法而非 Stream。 - 错误描述:PDDLStream 跑不出解时,怀疑框架/算法有问题。 - 现象/后果:到处调算法参数,却不检查 Stream 采样器。 - 根本原因:§5.5 的核心结论——完备性完全依赖采样器。算法本身完备,找不到解几乎总是因为某个 Stream 采不到必要的值(覆盖不全、子流形上采样失败)。 - 正确做法:先单独测每个 Stream 能否对目标产出必要的值(如 IK 能否对那个高架子产出构型),再怀疑算法。
练习¶
- (⭐⭐⭐) 用自己的话解释"完备性完全依赖于条件采样器":举一个 Stream 设计不当导致 PDDLStream 找不到本存在的解的例子。
- (⭐⭐⭐⭐) 为什么 TAMP 的可行解常落在零测度子流形上?以
Kin(p, g, q)为例说明,并解释为什么这使得"在构型空间随机采样再检查"几乎必然失败、而 IK 采样器能解决。 - (⭐⭐⭐,跨章) 对照 TAMP_T1 §4 的 RRT 概率完备性:RRT 的完备性依赖什么?PDDLStream 的完备性依赖什么?两者在"完备性责任落在哪"上有何异同?
5.6 进阶机制:外部成本、懒惰细化与代价-速度权衡 ⭐⭐⭐⭐¶
§5.1-§5.5 讲清了核心机制。本节补三个实战中重要、但初学常忽略的进阶点,它们解释了 PDDLStream 为什么能既找可行解又控制求解效率。这部分偏进阶,第一遍可跳过,回头优化性能时再读。
进阶一:外部成本函数(让几何代价进入符号搜索)。 §7.4 会讲给动作加 cost,但 cost 从哪来?很多代价是几何的——move 的代价是路径长度,而路径长度要等运动 Stream 算出 τ 才知道。PDDLStream 支持外部成本函数:把"算这个动作代价"也当成一种 Stream(输入动作参数,输出一个数值代价),让几何代价能流进符号搜索的目标函数。
外部成本函数(把几何代价接入符号搜索):
(:function (MoveCost ?q1 ?q2 ?tau) ; 声明一个代价函数
...) ; 由外部过程(算路径长)提供值
动作 move 的 :cost 引用 (MoveCost ?q1 ?q2 ?tau)
→ 符号搜索在比较计划时, 用的是真实几何代价(路径长), 而非固定常数
这与本章主题一脉相承:Stream 让几何可行性进入符号世界(认证事实),外部成本函数让几何代价也进入符号世界(数值)。两者合起来,符号搜索才能既判断"可行不可行"又比较"哪个更省"。
进阶二:懒惰展开(lazy)——能省则省的采样。 §5.4 提过 pddlstream 的搜索算法按"懒惰程度"排序。"懒惰"的核心思想是:尽量推迟昂贵的采样/计算,先用便宜的乐观估计,只在不得不算时才真正算。
懒惰(lazy) vs 不懒惰(eager):
不懒惰: 每遇到一个乐观对象就立刻真正采样验证 —— 准但慢
懒惰: 先用乐观对象搭完整骨架, 只在骨架确定后才采样验证最关键的
—— 省掉了为"最终没被选中的骨架"采样的浪费
pddlstream 的搜索选项从"最不懒(最低代价, 最优但最慢)"
到"最懒(最低运行时, 快但可能次优)"排成一个谱
本质洞察:懒惰展开是 §5.4"探索 vs 利用"张力在计算层的另一种体现——不懒惰是"为每个可能性都精算"(充分但浪费),懒惰是"先粗后精、按需精算"(高效但可能漏)。这与运动规划里的 Lazy-PRM(先建图不查碰撞、找到路径候选后才查关键边的碰撞)是完全相同的思想(TAMP_T1 §4 的 PRM 变体)。"懒惰"作为一种通用的计算节约策略——推迟昂贵操作直到必需——你会在规控的许多角落见到它:Lazy-PRM、惰性碰撞检测、§8.3 拍卖的 lazy auction。认得这个模式,你就能在自己的系统里主动用它换性能。
进阶三:代价-速度权衡的旋钮。 把进阶一、二合起来,PDDLStream 给了你一组调节"解质量 vs 求解速度"的旋钮:
| 旋钮 | 偏解质量(慢) | 偏求解速度(快) | 对应节 |
|---|---|---|---|
| 搜索算法懒惰度 | 不懒惰(Dijkstra/UCS,最优) | 最懒(最快出解,可能次优) | §5.6 进阶二 |
| 是否用外部成本 | 用真实几何代价(解更优) | 单位代价(不比较,更快) | §5.6 进阶一 |
| 算法选择 | — | Incremental→Focused→Adaptive | §5.4 |
| 采样器质量 | 充分采样(不漏) | 偏置采样(采得准,§7.3) | §7.3 |
实战建议:先用"快"的配置(最懒 + 单位代价 + Adaptive)跑通,确认能解;再按需往"质量"端调(加外部成本、降低懒惰度),换取更优的解。这是工程上典型的"先可行、再优化"。
⚠️ 常见陷阱¶
陷阱 5-6(思维陷阱):忽视外部成本,以为 PDDLStream 只能找可行解、不能优化。 - 错误描述:认为 PDDLStream 只管可行性,不能优化路径长度等几何代价。 - 现象/后果:放弃用 PDDLStream 求优质解,或在外部硬编码代价比较。 - 根本原因:不知道外部成本函数能把几何代价接入符号搜索(§5.6 进阶一)。 - 正确做法:用外部成本函数让 move 等动作的代价反映真实几何量,配合不懒惰搜索,PDDLStream 能找低代价解(§5.4 末尾"局部优化")。
陷阱 5-7(编程陷阱):一上来就用最不懒(最优)配置,导致求解极慢。 - 错误描述:默认追求最优,用 Dijkstra/UCS + 全量采样。 - 现象/后果:简单问题也跑很久,误以为 PDDLStream 慢。 - 根本原因:最优配置最慢;多数问题先要的是"能解出来",而非"最优"。 - 正确做法:先用最懒 + Adaptive 快速求可行解,确认可解后再按需调向质量端(§5.6 实战建议)。
练习¶
- (⭐⭐⭐) 解释外部成本函数如何让"几何代价进入符号搜索":以 move 的路径长度为例,说明没有它时符号搜索为什么无法偏好短路径计划。
- (⭐⭐⭐,跨章) 把 §5.6 的"懒惰展开"与 TAMP_T1 §4 的 Lazy-PRM 对照——两者"推迟昂贵操作"的具体对象分别是什么?为什么这个思想能换来性能?
5.7 两个算法的伪代码:Incremental 与 Optimistic 并排¶
把 §5.4 的文字描述落成伪代码,能看清 Incremental 和优化类算法(Focused/Adaptive 共享的 OPTIMISTIC 骨架)的结构差异。这也呼应 §6.7 那个可运行的 Incremental 实现。
Incremental(盲目采样-搜索):
function INCREMENTAL(init, goal, streams):
facts ← init
loop:
plan ← SEARCH(facts, goal) # 在当前有限事实上搜索(§5.1)
if plan ≠ None: return plan # 找到→返回
for s in streams: # 否则: 盲目评估所有 stream(§5.4)
for output in EVALUATE(s, facts): # 每个 stream 产一批
facts ← facts ∪ certified(output) # 认证的事实加入
if 无新事实: return FAILURE # 采尽仍无解→失败
Incremental 算法急切而盲目地评估所有 stream 实例,产生许多与任务无关的事实;当 stream 评估昂贵时(机器人领域 IK、运动规划常如此),开销很大。这正是 §6.7 实现里"每轮把所有 stream 全采"的样子。
OPTIMISTIC(Focused/Adaptive 共享骨架,惰性 + 乐观):
function OPTIMISTIC(init, goal, streams):
facts ← init
loop:
# 1. 用乐观对象规划(假装 stream 输出已存在, §5.2)
opt_facts ← facts ∪ OPTIMISTIC-OBJECTS(streams) # 引入乐观对象
opt_plan ← SEARCH(opt_facts, goal) # 搜出乐观骨架
if opt_plan = None:
if 无更多乐观对象可加: return FAILURE
continue # 加深 level 再试
# 2. 只为骨架用到的乐观对象, 真正采样(§5.3 惰性)
for opt_obj in opt_plan 用到的乐观对象:
real ← EVALUATE(对应 stream) # 按需评估
if real 成功: 乐观→真实(替换)
else: 记录失败, PROCESS-STREAMS 决定下一步 # 各算法分歧点
if 骨架全部落实: return 真实计划
Focused、Binding、Adaptive 用共享伪代码 OPTIMISTIC,由一个元参数过程 PROCESS-STREAMS 实现各算法;核心原则是在检查有效性前惰性地探索候选计划——用代表假想 stream 输出的乐观对象来规划,然后才评估实际 stream 输出。三个优化类算法的区别,只在 PROCESS-STREAMS 这一步——失败后是继续利用当前骨架(Binding)、换骨架(Focused),还是动态权衡(Adaptive)。
本质洞察:并排看两段伪代码,差异一目了然——Incremental 在"采样"阶段盲目(
for s in streams全采),Optimistic 在"采样"前先用乐观对象"问一句搜索需要什么"(只EVALUATE骨架用到的)。这个"先问需求再生产"与"盲目生产"的区别,就是 §5.2 乐观思想的全部价值。更深一层:Incremental 把"搜索"放在循环开头当终止检查,Optimistic 把"搜索"放在循环开头当需求生成器(搜出的骨架告诉你采什么)——同一个 SEARCH 调用,在两个算法里扮演的角色不同。看懂这点,你就理解了为什么 §5.4 说三种算法是"采样盲目程度光谱":本质是 SEARCH 与 EVALUATE 的调用顺序和耦合方式不同。
练习¶
- (⭐⭐⭐) 对照 §5.7 的两段伪代码,指出 Incremental 和 Optimistic 各自的
SEARCH调用扮演什么角色(终止检查 vs 需求生成器)。为什么这个角色差异导致 Optimistic 在昂贵 stream 上更省? - (⭐⭐⭐⭐) §5.7 说三个优化类算法只在
PROCESS-STREAMS一步分歧。结合 §5.4,描述 Focused(失败换骨架)和 Adaptive(动态权衡)在这一步分别怎么做,以及为什么 Adaptive 在"采样路径很多"的操作领域通常更好。
6. 完整案例:pick-and-place 的 PDDLStream 全流程 ⭐⭐⭐¶
前面分别讲了 Stream 的定义(§4)和求解机制(§5)。本节把它们串成一个完整的、可运行的 pick-and-place 例子,让 §2.1 那个连续参数难题真正被解决。这呼应 TAMP_T1 §7.4 的完整示例,但本章逐组件讲清每部分为什么这么写。
6.1 问题回顾与组件清单¶
回到 §2.1:机械臂把 cup 从桌子抓起、放到 shelf。一个完整的 PDDLStream 问题需要三部分拼装:
| 部分 | 内容 | 在本章哪节讲过 |
|---|---|---|
| Domain(PDDL) | 动作 pick/place/move 的前提与效果,前提里用 Stream 认证的谓词 | §4.2 的 pick 动作 |
| Stream 声明 | sample-grasp / sample-place / sample-ik / plan-motion 等 | §4.2 的声明语法 |
| Stream 生成器 | 每个 Stream 对应的 Python 条件生成器 | §4.3 的生成器实现 |
| Problem | 初始状态、目标 | 下面给出 |
6.2 Domain:动作定义¶
(define (domain pick-place-stream)
(:requirements :strips :typing)
(:predicates
; 流式状态(动作改变, 会变)
(AtPose ?obj ?p) ; 物体在位姿 p
(AtConf ?q) ; 机械臂在构型 q
(Holding ?obj ?g) ; 手持 obj 用抓取 g
(HandEmpty)
; 静态事实(Stream 认证, 永真)
(Grasp ?obj ?g) ; g 是 obj 的有效抓取
(Pose ?obj ?p) ; p 是 obj 的合法位姿
(Supported ?obj ?p ?region) ; p 让 obj 稳定在 region
(Kin ?obj ?p ?g ?q) ; 构型 q 达到对 obj@p 的抓取 g
(Motion ?q1 ?q2 ?tau)) ; tau 是 q1 到 q2 的无碰撞路径
(:action pick
:parameters (?obj ?p ?g ?q)
:precondition (and (AtPose ?obj ?p) (HandEmpty) (AtConf ?q)
(Grasp ?obj ?g) (Kin ?obj ?p ?g ?q)) ; ← Stream 认证的谓词
:effect (and (Holding ?obj ?g) (not (HandEmpty)) (not (AtPose ?obj ?p))))
(:action place
:parameters (?obj ?p ?g ?q ?region)
:precondition (and (Holding ?obj ?g) (AtConf ?q)
(Supported ?obj ?p ?region) (Kin ?obj ?p ?g ?q))
:effect (and (AtPose ?obj ?p) (HandEmpty) (not (Holding ?obj ?g))))
(:action move
:parameters (?q1 ?q2 ?tau)
:precondition (and (AtConf ?q1) (Motion ?q1 ?q2 ?tau))
:effect (and (AtConf ?q2) (not (AtConf ?q1)))))
注意 pick/place 的前提里,(Grasp ...)、(Kin ...)、(Supported ...) 都是 Stream 认证的静态事实——符号规划器要靠 Stream 提供这些,才能完成含几何约束的规划。这正是 §4.2 强调的"Stream 认证的谓词进入动作前提"。
6.3 Stream 声明与生成器:拼装¶
把 §4 的声明和生成器组装起来(这里用 Python 字典风格表示生成器注册,对应 pddlstream 库的实际用法):
# ===== Stream 声明(pddlstream 的 .pddl 风格, 这里内联说明) =====
# (:stream sample-grasp :inputs (?obj) :domain (Graspable ?obj)
# :outputs (?g) :certified (Grasp ?obj ?g))
# (:stream sample-place :inputs (?obj ?region) :domain (and (Graspable ?obj)(Region ?region))
# :outputs (?p) :certified (and (Pose ?obj ?p)(Supported ?obj ?p ?region)))
# (:stream sample-ik :inputs (?obj ?p ?g) :domain (and (Pose ?obj ?p)(Grasp ?obj ?g))
# :outputs (?q) :certified (Kin ?obj ?p ?g ?q))
# (:stream plan-motion :inputs (?q1 ?q2) :domain (and (Conf ?q1)(Conf ?q2))
# :outputs (?tau) :certified (Motion ?q1 ?q2 ?tau))
# ===== 生成器实现(过程组件, §4.3 的代码) =====
import numpy as np
def get_grasp_gen(world):
def gen(obj):
while True: # 无穷生成器
g = world.sample_grasp(obj) # 抓取采样器(黑盒)
if g is not None:
yield (g,)
return gen
def get_place_gen(world):
def gen(obj, region):
while True:
p = world.sample_stable_pose(obj, region) # 放置采样器
if p is not None:
yield (p,)
return gen
def get_ik_gen(world):
def gen(obj, pose, grasp):
target = world.compose(pose, grasp) # 目标末端位姿
for _ in range(20): # 有限尝试
q = world.solve_ik(target, seed=world.random_conf())
if q is not None and world.within_limits(q):
yield (q,) # 认证 (Kin obj pose grasp q)
return gen
def get_motion_gen(world):
def gen(q1, q2):
tau = world.birrt(q1, q2) # RRT(黑盒)
if tau is not None:
yield (tau,) # 认证 (Motion q1 q2 tau)
return gen
# ===== 注册(把声明名映射到生成器) =====
def make_stream_map(world):
return {
"sample-grasp": get_grasp_gen(world),
"sample-place": get_place_gen(world),
"sample-ik": get_ik_gen(world),
"plan-motion": get_motion_gen(world),
}
6.4 Problem 与求解¶
# ===== Problem: 初始状态 + 目标 =====
def make_problem(world):
q0 = world.initial_conf() # 机械臂初始构型
cup_pose0 = world.get_pose("cup") # 杯子初始位姿(桌上)
init = [
("Graspable", "cup"), ("Region", "shelf"),
("AtPose", "cup", cup_pose0), ("Pose", "cup", cup_pose0),
("AtConf", q0), ("Conf", q0), ("HandEmpty",),
]
goal = ("exists", ("?p",), # 目标: 杯子被放到架子上
("and", ("AtPose", "cup", "?p"),
("Supported", "cup", "?p", "shelf")))
return init, goal
# ===== 求解(调用 pddlstream 的求解器) =====
def solve_pick_place(world, algorithm="adaptive"):
from pddlstream.algorithms.meta import solve # pddlstream 库
domain_pddl = open("pick-place-stream.pddl").read()
stream_pddl = open("stream.pddl").read()
stream_map = make_stream_map(world)
init, goal = make_problem(world)
problem = (domain_pddl, {}, stream_pddl, stream_map, init, goal)
# algorithm: 'incremental' / 'focused' / 'adaptive' (§5.4)
solution = solve(problem, algorithm=algorithm, unit_costs=True)
plan, cost, certificate = solution
return plan # [('move',q0,q1,tau1), ('pick',cup,p0,g,q1), ('move',...), ('place',...)]
求解返回的 plan 里,每个动作的参数都是真实采样出来的值(具体的构型、位姿、路径)——这就是 §5.3 讲的"乐观事实全部升级为真实事实"后的可执行计划。§2.1 那个连续参数难题,至此被完整解决:抓取 \(g\)、构型 \(q_1/q_2\)、路径 \(\tau\) 都由对应 Stream 采样、由 PDDLStream 调度着相容地凑齐。
为什么 goal 用存在量词 exists ?p? 这是个值得停下来想的细节。我们的目标是"杯子在架子上",但杯子具体放在架子的哪个位置(?p)?写目标时我们不知道、也不该指定——具体放哪由 PDDLStream 通过 sample-place 采样、并确保该位置可达可放。用存在量词 exists ?p, AtPose(cup, ?p) ∧ Supported(cup, ?p, shelf),等于说"存在某个放置位姿,让杯子稳定在架子上",把"具体哪个位姿"留给求解器去采样填充。
存在量词目标与按需采样的配合:
若写死目标 AtPose(cup, p_specific):
→ 强制杯子必须放在 p_specific, 但这个点可能不可达/会碰撞 → 无解
用 exists ?p:
→ "放在架子上任意可行位置即可", 求解器用 sample-place 采可行的 p
→ 与 §3 的"按需采样"完美配合: 目标不指定连续值, 由 Stream 供给
本质洞察:存在量词目标体现了 PDDLStream 处理连续参数的一贯哲学——能不指定的连续值就不指定,留给采样器按需供给。这与 §2.2 批判的"朴素离散化"形成鲜明对比:离散化是"预先把所有可能位置都列出来选",存在量词目标是"只说要达到什么效果,具体值让采样器现采"。前者盲目穷举、后者有的放矢。从目标的写法(用存在量词而非具体值),到求解的机制(乐观对象 + 按需采样),PDDLStream 始终贯彻"连续值延迟到必需时才具体化"——这正是它优雅处理无穷连续空间的根本姿态。
6.5 全流程回顾:一张图串起本章¶
PDDLStream 求解 pick-and-place 全流程(串起 §3-§5):
Problem(目标: cup 在 shelf 上)
│
▼ §5.4 算法(以 Adaptive 为例)
引入乐观对象 ĝ,p̂,q̂,τ̂ (§5.2), 按 level 展开(§5.3)
│
▼ 组成乐观 PDDL(有限), 交 FF 搜索(§5.1)
乐观计划骨架: [move, pick(cup,p̂_cup,ĝ,q̂_1), move, place(cup,p̂_shelf,ĝ,q̂_2)]
│ 骨架指明"需要采样哪些值"
▼ 为骨架用到的乐观对象调用 Stream 采样(§4.3 生成器)
sample-grasp→真实 g; sample-place→真实 p; sample-ik→真实 q; plan-motion→真实 τ
│ 乐观事实升级为真实事实(§5.3)
▼ 某步采样失败? → 回到搜索换骨架; 全成功? → 计划落地
可执行计划: [move(q0,q1,τ1), pick(cup,p0,g,q1), move(q1,q2,τ2), place(cup,p_s,g,q2)]
本质洞察:这张图把本章所有概念收束成一条求解流。Stream(§3-§4)是"零件供应商"——按需供应几何值;optimistic/level(§5.2-§5.3)是"先画蓝图"——用假想零件规划出骨架;归约为有限 PDDL(§5.1)是"用现成的经典规划器画蓝图";三种算法(§5.4)是"调度策略"——决定先画蓝图还是先备料、备多少。整个 PDDLStream,就是让"符号蓝图"和"几何零件"在一个有节奏的采样-搜索循环里彼此咬合,直到拼出一个既符号合法、又几何可行的完整计划——这正是缝合 TAMP_T1 §2.5 符号-几何鸿沟的完整答案。
6.6 求解器运行轨迹追踪:看 Adaptive 算法实际怎么跑¶
§5.4 讲了三种算法的策略,§6.5 给了流程图,但还是抽象。本节模拟一次 Adaptive 算法在 §6 pick-and-place 上的实际运行轨迹——把"采样-搜索交替"这个抽象节奏,变成你能逐步看到的事件序列。这能让 §5 的机制从"知道原理"变成"看见运转"。
下面是一次典型求解的事件日志(简化标注每一步在做什么、对应本章哪节):
=== PDDLStream Adaptive 求解 pick-and-place 的运行轨迹 ===
[迭代 1] 引入 level-1 乐观对象 (§5.2-5.3)
乐观: ĝ(cup的抓取), p̂(cup在shelf的放置)
→ 组成乐观 PDDL, 交 FastDownward 搜索 (§5.1)
→ 搜索失败: 只有抓取/放置, 没有 IK 构型和运动, 凑不出完整计划
→ level 不够深, 加深 (§5.3 level 旋钮)
[迭代 2] 引入 level-2 乐观对象
乐观: + q̂1(pick构型), q̂2(place构型)
→ 搜索: 仍缺 move 的路径 τ, 失败
→ 继续加深
[迭代 3] 引入 level-3 乐观对象
乐观: + τ̂1, τ̂2(运动路径)
→ 搜索成功! 得到乐观骨架: (§5.2 乐观计划)
[move(q0,q̂1,τ̂1), pick(cup,p0,ĝ,q̂1), move(q̂1,q̂2,τ̂2), place(cup,p̂,ĝ,q̂2)]
→ 骨架指明: 需为 ĝ,p̂,q̂1,q̂2,τ̂1,τ̂2 采样真实值
[迭代 3 - 细化] 沿依赖链采样真实值 (§3.4 依赖链 + §4.3 生成器)
sample-grasp(cup) → g ✓ (level-1, 无依赖, 先采)
sample-place(cup,shelf) → p ✓
sample-ik(cup,p0,g) → q1 ✓ (level-2, 依赖 g)
sample-ik(cup,p,g) → q2 ✗ 失败! (该放置 p 处 IK 无解)
→ 乐观计划部分落空 (§5.2 本质洞察)
→ Adaptive 决策: 利用(为这骨架换个 p 重试) vs 探索(换骨架) (§5.4)
→ 选择利用: 再采 sample-place → p' ✓
sample-ik(cup,p',g) → q2' ✓
plan-motion(q0,q1) → τ1 ✓ (level-3, 依赖 q1)
plan-motion(q1,q2') → τ2 ✓
[完成] 所有乐观事实升级为真实 (§5.3 乐观→真实)
可执行计划: [move(q0,q1,τ1), pick(cup,p0,g,q1),
move(q1,q2',τ2), place(cup,p',g,q2')]
→ 返回 (参数全是真实采样值, 可直接执行)
从这条轨迹能看到几个 §5 讲过、但只有看运行才有实感的点:
- level 逐步加深(迭代 1→3):乐观对象不是一次全引入,而是不够就加深,直到乐观骨架能凑出完整计划。这就是 §5.3 的"展开深度旋钮"在实际转动。
- 采样沿依赖链(迭代 3 细化):先采无依赖的抓取/放置(level-1),再采依赖它们的 IK(level-2),最后采依赖构型的运动(level-3)——正是 §3.4 的依赖链顺序。
- 失败局部化 + Adaptive 决策(q2 采样失败处):一个 IK 失败不会让整个计划推倒,而是触发 §5.4 的"利用 vs 探索"权衡——这里选择"利用"(换个放置位姿重试当前骨架),而非"探索"(换整个骨架)。
本质洞察:这条轨迹揭示了 PDDLStream 高效的微观机制——它不是"要么全成功要么从头来",而是"逐步加深、沿链采样、失败局部修复"。对比 §2.3 的 plan-then-check(一处失败就盲目重排整个计划),PDDLStream 的失败是局部的、可定位的(知道是 q2 这一步的 IK 失败),修复也是局部的(只换 p 重采,不动已成功的 g、q1)。这种"局部失败、局部修复"的能力,正是乐观对象 + 依赖链 + Adaptive 调度三者合力的结果——也是它比朴素回溯快的根本原因。看懂这条轨迹,你就真正理解了为什么 §5 的那套机制不是花架子,而是实打实地避免了 §2 的盲目回溯。
6.7 动手:80 行纯 Python 跑通 Incremental 核心机制¶
前面的代码都依赖 pddlstream 库 + FastDownward(本地需编译)。为了让你真正看见采样-搜索循环跑起来,本节给一个不依赖任何库的最小实现——用最简的 BFS 当符号搜索,亲手实现 §5.4 的 Incremental 算法。它故意简化几何(用一维数轴模拟"够不够得到"),但完整保留了 PDDLStream 的核心骨架:Stream 认证事实 → 归约为有限问题 → 搜索失败则采样更多 → 再搜。
问题设定(一维的 pick-and-place 缩影):物体在位置 0.0,要搬到 9.5。机械臂有若干站位,每个站位只能够到 REACH=3.0 范围内的位置。关键:单个站位够不到从 0.0 到 9.5 的全程(距离 9.5 > 2×3),所以必须中转——这正是 §2.1 参数耦合的一维缩影(站位选择依赖几何可达,且前后段耦合)。
from itertools import product
from collections import deque
REACH = 3.0
BASES = [0.0, 2.5, 5.0, 7.5, 10.0] # 候选站位
# ===== 三个 Stream(对应 §4 的声明 + 生成器) =====
def stream_sample_pose(facts):
"""sample-pose: 产出候选中转位姿。certified: (Pos p)。"""
for p in [2.5, 5.0, 7.5]:
yield (("Pos", p),)
def stream_sample_base(facts):
"""sample-base: 产出候选站位。certified: (Base b)。"""
for b in BASES:
yield (("Base", b),)
def stream_reachable(facts):
"""test-reachable: 站位 b 够得到位置 p 吗。domain:(Base b),(Pos p)。certified:(Reachable b p)。
这是 test stream(§4.4)——不产新对象, 只认证几何关系。"""
bases = [f[1] for f in facts if f[0] == "Base"]
poss = [f[1] for f in facts if f[0] == "Pos"]
for b, p in product(bases, poss):
if abs(b - p) <= REACH: # 几何检查: 够得到
yield (("Reachable", b, p),)
STREAMS = [stream_sample_pose, stream_sample_base, stream_reachable]
INIT = [("Pos", 0.0), ("Pos", 9.5), ("AtObj", 0.0)] # 物体在0, 两个已知位姿
GOAL = ("AtObj", 9.5)
# ===== 符号动作: move-and-carry(from_p, to_p, b) =====
# 前提: (AtObj from_p) ∧ (Reachable b from_p) ∧ (Reachable b to_p) ← 用了Stream认证的Reachable
def applicable_actions(state, all_facts):
at = [f[1] for f in state if f[0] == "AtObj"]
reach = {(f[1], f[2]) for f in all_facts if f[0] == "Reachable"}
poss = [f[1] for f in all_facts if f[0] == "Pos"]
acts = []
for fr in at:
for to in poss:
if to == fr: continue
for b in BASES:
if (b, fr) in reach and (b, to) in reach: # 同站位够到两端
acts.append(("move-and-carry", fr, to, b)); break
return acts
def apply_action(state, a):
_, fr, to, b = a
return [f for f in state if not (f[0]=="AtObj" and f[1]==fr)] + [("AtObj", to)]
def bfs_plan(state, all_facts, goal, max_depth=5):
"""符号搜索 = 归约后的有限 PDDL 问题(§5.1), 这里用最简 BFS。"""
start = frozenset(state); q = deque([(start, [])]); seen = {start}
while q:
s, plan = q.popleft()
if goal in s: return plan
if len(plan) >= max_depth: continue
for a in applicable_actions(list(s), all_facts):
ns = frozenset(apply_action(list(s), a))
if ns not in seen: seen.add(ns); q.append((ns, plan+[a]))
return None
# ===== Incremental 算法: 采样-搜索循环(§5.4) =====
def incremental_solve(max_iters=6):
facts = list(INIT)
state = [f for f in INIT if f[0] == "AtObj"]
for it in range(1, max_iters + 1):
plan = bfs_plan(state, facts, GOAL) # 1. 搜索(解有限问题)
if plan is not None:
return plan
new = [] # 2. 失败→采样更多 certified facts
for s in STREAMS:
for out in s(facts):
for fact in out:
if fact not in facts and fact not in new:
new.append(fact)
facts.extend(new) # 扩大有限问题
if not new: break # 无新事实可采→终止
return None
plan = incremental_solve()
for a in plan:
print(f"搬运: {a[1]} → {a[2]} (站位 b={a[3]})")
# 输出:
# 搬运: 0.0 → 5.0 (站位 b=2.5)
# 搬运: 5.0 → 9.5 (站位 b=7.5)
运行结果:算法在迭代 1-2 采样(产出位姿、站位、可达性事实),迭代 3 搜索成功,得到一个 2 步中转计划:先把物体从 0.0 搬到 5.0(用站位 2.5,它够得到 0.0 和 5.0),再从 5.0 搬到 9.5(用站位 7.5)。
这个 80 行实现,把本章的抽象概念全部坐实了:
- certified facts(§3.2):每个 stream
yield的就是认证事实((Pos p)、(Base b)、(Reachable b p))。 - 归约为有限 PDDL(§5.1):每轮
bfs_plan面对的是当前有限事实集组成的有限搜索问题。 - 采样-搜索循环(§5.4 Incremental):
incremental_solve的主循环就是"搜索失败→采样更多→再搜",与 PRM 的"采样-连接-搜索"同构。 - Stream 依赖链(§3.4):
reachable的输入依赖sample-base和sample-pose的输出——必须先有站位和位姿,才能测可达性。 - 参数耦合(§2.1):单站位够不到全程,迫使计划用中转位姿——这正是"前段选择约束后段"的耦合在一维的体现。
本质洞察:这个最小实现刻意只做了 Incremental(最简单、不用乐观对象),所以你能看到它的"盲目"——每轮把所有 stream 的所有可能输出全采一遍(
sample_base把 5 个站位全产出、reachable把所有够得到的组合全认证),其中很多最终没用上。这正是 §5.4 说 Incremental "盲目全采"的真实样子,也是 §10.3 说它在多对象时慢的根源。想象一下:如果位姿和站位各有上千个候选,这种盲目全采就会爆炸——这时才需要 Focused/Adaptive 的乐观对象来"只采搜索需要的"。亲手跑过这个盲目版本,你就能真切体会为什么 PDDLStream 要发明乐观对象(§5.2)——不是为了复杂而复杂,而是 Incremental 的盲目在真实规模下确实扛不住。这就是"先理解朴素方案的痛,再理解高级方案的妙"。
这个最小实现与官方库的关系。 它和真实的 pddlstream 库(§8)有三点关键简化,理解这些简化就理解了真实库多做了什么:(1) 符号搜索——这里用最简 BFS,官方库用 FastDownward(带启发式,§5.1),能处理大得多的符号空间;(2) 算法——这里只有 Incremental,官方库还有 Focused/Adaptive(乐观对象 + 惰性,§5.7 伪代码);(3) 几何——这里用一维数轴模拟"够得到",真实库的 Stream 生成器调用真正的 IK、碰撞检测、运动规划(§3.1 的黑盒)。但核心骨架完全一致:Stream 认证事实、归约为有限问题、采样-搜索循环。所以读懂这 80 行,你就读懂了官方库的"心脏"——剩下的都是把每个部件换成更强的实现。建议接着 §6.7 练习 8 的提示,亲手把它朝 Focused 改造一步,体会乐观惰性带来的差别。
⚠️ 常见陷阱¶
陷阱 6-1(编程陷阱):在 domain 动作前提里漏写 Stream 认证的谓词。
- 错误描述:写 pick/place 动作时,只写流式状态前提(AtPose、HandEmpty),漏掉几何谓词(Kin、Grasp)。
- 现象/后果:符号规划器会认为"任何时候都能 pick",规划出几何上不可行的计划(抓一个够不到的位姿),且 Stream 根本不被触发(没有谓词要求它产值)。
- 根本原因:动作前提是符号层"看见几何约束"的唯一通道(§4.2)。漏写几何谓词,等于切断了符号-几何的联系,退回纯符号规划。
- 正确做法:每个有几何约束的动作,前提里必须包含对应的 Stream 认证谓词(pick 要有 Kin、Grasp)——对照 §6.2 的 domain 检查。
陷阱 6-2(概念误区):以为目标必须指定所有连续参数的具体值。
- 错误描述:写 goal 时纠结"杯子到底放架子哪个位置",试图填一个具体坐标。
- 现象/后果:写死的位置可能不可达/会碰撞,导致无解;或限制了求解器本可找到的更优放置。
- 根本原因:连续参数的具体值应由 Stream 采样供给,目标只需表达"要达到什么效果"(§6.4 的存在量词目标)。
- 正确做法:目标用存在量词(exists ?p, AtPose(cup,?p) ∧ Supported(cup,?p,shelf)),把"具体放哪"留给 sample-place——能不指定的连续值就不指定(§6.4 本质洞察)。
练习¶
- (⭐⭐) 运行思路推演:对 §6.4 的问题,若
shelf太高、所有 IK 采样都无解,PDDLStream 会怎么表现?它会无限采样还是会终止?(提示:结合 §5 的乐观计划落空与算法行为。) - (⭐⭐⭐) 给 §6.2 的 domain 加一个新约束:放置时杯子不能碰到架子上已有的物体。需要新增哪个 Stream(test 型还是 generator 型)、在 place 前提里加什么谓词?
- (⭐⭐⭐) §6.4 的 goal 用了存在量词
exists ?p。解释为什么放置位姿?p用存在量词而非具体值——这与 Stream 的"按需采样"如何配合? - (⭐⭐⭐⭐) 把 §6 的 pick-and-place 扩展为"把 cup 和 book 都收拾好"(两个物体)。讨论对象数翻倍后,Incremental 与 Adaptive 算法的表现差异(结合 §5.4)。
- (⭐⭐⭐,追踪) 对照 §6.6 的运行轨迹,若在"迭代 3-细化"阶段
sample-place连续 5 次都配不出可行 IK,Adaptive 算法在什么时候应该从"利用"(换放置)转为"探索"(换骨架)?这个权衡由什么决定(§5.4)? - (⭐⭐⭐⭐,对照追踪) 仿照 §6.6 的 Adaptive 运行轨迹,为 Incremental 算法写一条同问题的运行轨迹(提示:Incremental 不用乐观对象,每轮盲目地为所有 Stream 各采一批值再搜索)。对比两条轨迹,具体指出 Incremental 在哪些步骤上做了"白采"(采了最终没用上的值),从而直观理解为什么紧约束问题上 Adaptive 比 Incremental 快。
- (⭐⭐⭐,动手) 运行 §6.7 的 80 行最小实现,然后做三个修改实验:(a) 把
REACH改成5.0,观察计划是否变短(单站位能否够更远);(b) 把中转位姿[2.5, 5.0, 7.5]删到只剩[5.0],看是否仍可解;(c) 把目标改成12.0(超出所有站位范围),观察算法如何"采到无新事实可采"而终止——这正是 §5.5 说的"采样器覆盖不全则找不到解"。 - (⭐⭐⭐⭐,改造) 把 §6.7 的最小实现从 Incremental 改成"惰性"版本:不再每轮把所有 stream 全采,而是先只采
sample-pose和sample-base,只在搜索发现"需要某个 Reachable 事实"时才按需调用stream_reachable。讨论这个改造如何朝 Focused 算法(§5.4)靠近,以及它在位姿/站位很多时能省多少采样。
7. Stream 设计模式与反模式 ⭐⭐⭐¶
§4 讲了怎么写单个 Stream,§6 给了一组 Stream 的完整例子。但实战中,为一个新领域设计一整套协同的 Stream,是 PDDLStream 工程里最考验功力、也最容易出错的环节。§5.5 已经点明"完备性完全依赖采样器"——Stream 设计的好坏,直接决定 PDDLStream 能不能用、跑得快不快。本节把实战中反复验证的设计模式与反模式系统化。
7.1 模式一:沿因子结构切分 Stream¶
§5.5 讲的"因子化"不只是理论,它直接指导 Stream 该怎么切。核心原则:一个 Stream 对应一个因子(一组只关联少数变量的约束),不要把多个独立约束塞进一个 Stream。
反模式: 一个"大一统" Stream 同时采抓取+IK+运动
(:stream sample-everything
:inputs (?obj ?region)
:outputs (?g ?q1 ?p ?q2 ?tau) ; 一次产出所有参数!
:certified (and (Grasp ?obj ?g)(Kin ...)(Supported ...)(Motion ...)))
问题: (1)把因子结构抹平了, 退回 §5.5 说的"高维联合空间", 采样命中率极低
(2)任一参数失败, 整组重采, 浪费
(3)无法复用 —— 不同动作需要不同子集时只能再写一个大 Stream
正确模式: 沿因子切成小 Stream, 用依赖链串联
sample-grasp (1个因子) → sample-ik (1个因子) → plan-motion (1个因子)
每个只采少数变量, 高命中; 失败只重采那一个; 可被多个动作复用
本质洞察:Stream 切分的黄金法则,是让每个 Stream 的
:certified对应一个几何约束、:inputs只含该约束真正牵涉的变量。这等价于让 Stream 的粒度匹配问题的因子结构(§5.5)。切得对,采样在低维空间高效进行、失败局部化、组件可复用;切得错(大一统),就抹平了因子结构、退回维度灾难。判断一个 Stream 是否切得太大,看它的:certified是否包含多个本可独立的约束——若是,拆开。
7.2 模式二:test stream 用在"判定"而非"生成"¶
§4.4 介绍了 test stream(不产值,只认证)。实战中一个常见决策是:碰撞这类约束,该写成 test stream(事后判定)还是塞进 generator 的认证里(生成时保证)?
两种处理碰撞的方式:
方式A (生成时保证): sample-ik 的生成器内部就做碰撞检测, 只 yield 无碰撞的 q
+ 产出的 q 一定无碰撞, 下游不用再查
- 若碰撞约束依赖"其他物体的位姿"(运行时才定), 生成时无法预知 → 不适用
方式B (test stream 事后判定): sample-ik 只管 IK, 单独的 test-cfree 查碰撞
(:stream test-cfree :inputs (?q ?obj2 ?p2) :certified (CFree ?q ?obj2 ?p2))
+ 碰撞约束可关联"动作发生时其他物体在哪", 更灵活
+ 符合因子化 —— IK 因子和碰撞因子分开
- 多一层 test, 搜索时要满足 CFree 前提
经验法则:与"其他物体当前位置"无关的约束(如关节限位、自碰撞)→ 放进 generator 生成时保证;依赖动作发生时场景状态的约束(如与可移动物体的碰撞)→ 写成 test stream。后者正是 §4.4 提到 fluent stream 的用武之地——碰撞对象的位姿是流式状态。
7.3 模式三:让采样器"聪明"而非"多产"¶
§5.5 说完备性依赖采样器。但"采样器够好"不等于"采样器吐得多",而是"采样器吐得准"——尽量产出下游用得上的值。
反模式: 采样器盲目均匀撒点
def sample_place_dumb(obj, region):
while True:
yield (uniform_random_point(region),) # 区域内均匀采
问题: 很多采样点虽在区域内, 但 IK 够不到 / 会碰撞 → 下游大量失败, 白采
改进模式: 采样器利用领域知识偏向"可能可行"的区域
def sample_place_smart(obj, region, robot):
while True:
p = sample_point(region)
if rough_reachable(robot, p): # 粗略可达性预筛(便宜的检查)
yield (p,) # 只吐"大概率下游能用"的
收益: 减少下游(IK/运动)的无效尝试, 整体快
本质洞察:这呼应 §5.4 Adaptive 算法的精神,但层次不同——Adaptive 在算法层调度"采哪个 Stream",而这里在采样器层让每个 Stream 自己"采得准"。两者叠加才是高效 PDDLStream 的完整图景:算法层有的放矢地选 Stream,采样器层有的放矢地出值。一个常见误区是只调算法不优化采样器——但若采样器本身盲目,再好的算法调度也是在一堆废样本里打转。采样器的"先验质量"是 PDDLStream 性能的地基,这也是为什么实战中为新领域设计采样器(而非套用通用随机采样)如此关键。
7.4 模式四:用 cost 引导低代价解¶
§5.4 末尾提过 PDDLStream 能"局部优化产生低代价解"。这通过给 Stream 输出和动作附代价实现——让规划器不仅找可行解,还偏好低代价的。
给动作/Stream 加代价, 引导解的质量:
- move 动作代价 = 路径长度 → 规划器偏好短路径计划
- 抓取 Stream 可对不同抓取赋不同代价(稳定抓取代价低) → 偏好稳抓
PDDLStream 的 Adaptive 算法在找到可行解后, 会继续局部优化降低代价
(§5.4: "局部优化以产生低代价解")
注意取舍:加代价让解更优,但也让搜索更慢(要比较代价)。pddlstream 库的搜索选项里,从"最不懒(最低代价)"到"最懒(最低运行时)"排列了一系列搜索算法,Dijkstra/一致代价搜索最优但最慢——这是"解质量 vs 求解速度"在搜索配置层的旋钮。
7.5 反模式总览¶
把前面的反模式连同其他常见错误汇总,作为 Stream 设计的检查清单:
| 反模式 | 症状 | 根因 | 对应模式 |
|---|---|---|---|
| 大一统 Stream | 采样命中率极低、失败全组重采 | 抹平因子结构,退回高维 | §7.1 切小 |
| 碰撞硬塞进 generator | 依赖其他物体位姿的约束无法处理 | 静态生成无法预知运行时场景 | §7.2 用 test stream |
| 采样器盲目撒点 | 下游大量无效尝试 | 采样不偏向可行区域 | §7.3 采得准 |
| 只调算法不优化采样器 | 换 Adaptive 仍慢 | 采样器先验质量差,地基不牢 | §7.3 采样器优先 |
| 无代价、只求可行 | 解可行但绕路/抓得不稳 | 没给搜索质量信号 | §7.4 加 cost |
| 认证未保证的事实 | 计划执行时失败(§4 陷阱4-2) | 采样器没真正满足约束 | §4.3 认证名副其实 |
7.6 完整工作流:为"倒水"任务从零设计一套 Stream¶
把 §7.1-§7.5 的模式串成一个可操作的流程,用一个新任务——机器人把杯子里的水倒进碗里——从零走一遍。这是从"知道模式"到"会用模式"的桥。倒水比 pick-and-place 复杂:它有"对准碗口""倾倒角度"这些 pick-place 没有的几何约束。
第 1 步:识别任务的几何约束(决定需要哪些 Stream)。 先把任务拆成"需要满足哪些几何关系",每个关系将对应一个 Stream(§7.1 因子切分):
倒水任务的几何约束分解(§7.1 沿因子切分):
1. 抓起杯子 → 需要"杯子的抓取位姿" → Grasp(cup, g)
2. 杯子对准碗口 → 需要"倒水位姿"(杯相对碗) → PourPose(cup, bowl, q_pour)
3. 达到倒水位姿 → 需要 IK → Kin(pour_pose, g, q)
4. 移动到各构型 → 需要运动规划 → Motion(q1, q2, τ)
5. 倾倒不洒到外面 → 需要碰撞/容器对齐检查 → AlignedOverBowl(cup_pose, bowl) [test]
第 2 步:为每个约束判断 Stream 类型(§4.4 三类 + §7.2 判定)。
| 约束 | Stream 类型 | 判断理由 |
|---|---|---|
| Grasp | generator | 产出抓取位姿候选 |
| PourPose | generator | 产出倒水位姿候选(杯口在碗上方、倾斜角合适) |
| Kin | generator | 产出 IK 构型 |
| Motion | generator | 产出路径 |
| AlignedOverBowl | test | 只判定"杯口是否对准碗",不产新值(§7.2:依赖杯碗当前相对位姿,但此处碗固定,也可 generator 内置) |
第 3 步:确定依赖链(§3.4),排出 Stream 顺序。
倒水的 Stream 依赖链:
sample-grasp(cup) → g
sample-pour-pose(cup, bowl) → q_pour (杯相对碗的倒水位姿)
sample-ik(q_pour, g) → q (依赖 g 和 q_pour)
plan-motion(q_current, q) → τ (依赖 q)
[test] aligned-over-bowl(q_pour) (验证对准)
第 4 步:写采样器,注意"采得准"(§7.3)。 倒水位姿采样器是关键——盲目采样会产出大量"杯口没对准碗"或"角度不对会洒"的位姿。让它"采得准":
def sample_pour_pose_gen(cup, bowl):
"""倒水位姿采样器: 产出杯相对碗的倾倒位姿。
§7.3 采得准: 偏置到"杯口在碗正上方 + 合理倾角", 而非盲目均匀采。"""
bowl_top = get_bowl_opening_center(bowl) # 碗口中心
while True:
# 偏置采样: 杯口位置在碗口上方小范围, 倾角在能倒出水的范围
offset = sample_small_xy_offset() # 杯口相对碗口的水平偏移(小)
tilt = sample_pour_tilt() # 倾倒角(如 90°~140°, 能倒出且不过度)
q_pour = make_pour_pose(bowl_top, offset, tilt)
if cup_opening_above_bowl(q_pour, bowl): # 粗筛: 杯口确在碗范围上方
yield (q_pour,) # 只吐"大概率不洒"的位姿
对比盲目版(在杯子可能的所有位姿里均匀采),这个采样器利用领域知识(碗口位置、有效倾角范围)大幅提高下游可用率——这正是 §7.3 模式三。
第 5 步:认证名副其实(§4.3 / §7.5 最后一行)。 sample-pour-pose 认证 PourPose(cup, bowl, q_pour),这个认证承诺"在此位姿倒水能进碗"。采样器必须真正保证这点(上面的 cup_opening_above_bowl 检查),否则规划出的倒水计划执行时会洒——这就是 §4 陷阱 4-2 在新领域的具体化。
第 6 步:对照反模式检查(§7.5)。 设计完跑一遍 §7.5 的检查清单:有没有大一统 Stream(把抓取+倒水位姿+IK 塞一起)?碰撞/对齐该不该用 test?采样器是否盲目?有没有给"洒出量"加 cost(§7.4,偏好洒得少的倒法)?
本质洞察:这个工作流揭示了"为新领域设计 Stream"的通用套路——识别几何约束(定 Stream 集合)→ 判类型(generator/test)→ 排依赖链 → 写"采得准"的采样器 → 确保认证名副其实 → 对照反模式检查。六步里,前三步是"结构设计"(沿因子切、定类型、排顺序),后三步是"质量保证"(采得准、认证真、避反模式)。这套流程对任何新 TAMP 领域都适用——把"倒水"换成"插销""开门""叠衣服",步骤不变,只是约束和采样器不同。掌握了这个工作流,你就从"会改官方例子"真正升级到"能为任意新任务设计 PDDLStream 领域"——这是 §5.5"完备性依赖采样器"在工程上的落地能力。
⚠️ 常见陷阱¶
陷阱 7-1(思维陷阱):用一个"大一统" Stream 一次采出所有参数。 - 错误描述:图省事写一个 Stream 同时产出抓取、构型、放置、路径(§7.1 反模式)。 - 现象/后果:采样命中率极低(高维联合空间)、任一参数失败整组重采、无法复用。 - 根本原因:抹平了问题的因子结构,退回 §5.5 说的维度灾难。 - 正确做法:沿因子切成小 Stream(一个 Stream 一个约束、只含相关变量),用依赖链串联。
陷阱 7-2(概念误区):把所有约束都塞进 generator 的认证里。 - 错误描述:把依赖运行时场景的约束(与可移动物体碰撞)放进生成器生成时保证。 - 现象/后果:生成时无法预知动作发生时其他物体在哪,要么漏检要么过度保守。 - 根本原因:静态生成无法处理依赖动态状态的约束。 - 正确做法:与其他物体当前位置无关的约束放 generator;依赖场景状态的写 test/fluent stream(§7.2)。
陷阱 7-3(思维陷阱):只优化算法、不优化采样器。 - 错误描述:PDDLStream 慢就换算法(Incremental→Adaptive),不看采样器质量。 - 现象/后果:换了算法仍慢,因为采样器盲目,算法在废样本里打转。 - 根本原因:采样器的先验质量是 PDDLStream 性能地基(§5.5 完备性依赖采样器、§7.3 采得准)。 - 正确做法:先让采样器"采得准"(偏向可行区域、粗筛),再调算法。地基不牢,算法调度无用。
练习¶
- (⭐⭐) 给定一个把"采抓取 + 解IK"合在一起的 Stream,把它按因子结构拆成两个 Stream,写出拆分后的声明,并说明拆分带来的三个好处(命中率/失败局部化/复用)。
- (⭐⭐⭐) 对"放置时不能碰到架子上已有物体"这个约束,判断该写成 generator 内置还是 test stream,说明理由(结合该约束是否依赖运行时场景状态)。
- (⭐⭐⭐) 为 §7.3 的
sample_place_smart设计一个"粗略可达性预筛"rough_reachable:用什么便宜的检查(不解完整 IK)来快速排除明显够不到的放置点?讨论预筛的"宁可放过不可错杀"原则(不能筛掉可行点)。 - (⭐⭐⭐⭐,综合) 结合 §5.5 和 §7.1-§7.3:解释"完备性依赖采样器"如何同时要求采样器既完整(不漏可行值,否则破坏完备性)又高效(偏向可行区域,否则慢)。这两个要求会冲突吗?如何平衡?
- (⭐⭐⭐⭐,工作流实践) 用 §7.6 的六步工作流,为"机器人开抽屉取出里面的物体"这个任务从零设计一套 Stream:(1) 列出几何约束(抓抽屉把手、拉开的轨迹、取物体);(2) 为每个约束判 generator/test/fluent 类型;(3) 排依赖链;(4) 指出哪个采样器最需要"采得准"、怎么偏置;(5) 说明每个 Stream 的认证事实;(6) 对照反模式自查。这道题综合了本节全部模式,是检验"能否为新领域建模"的试金石。
8. 工程实践:用 pddlstream 库接入 Mini-TAMP ⭐⭐⭐¶
前面几节讲清了 PDDLStream 的原理(§3-§5)、给了完整案例(§6)、梳理了设计模式(§7)。本节落到工程:用官方的 pddlstream 库,把前面学的东西接到 TAMP_T1 的 Mini-TAMP 累积项目上。这是从"理解 + 会设计"到"真正跑在系统里"的一步。本节还给出 §8.4 的调试清单——多组件系统出问题时如何系统定位,这是实战中最需要、却最少被讲清的部分。
8.1 pddlstream 库简介¶
PDDLStream 有官方开源实现,由原作者维护。PDDLStream 是 PDDLStream/STRIPStream 规划框架的"第三版",旨在取代之前的版本;它在表示和算法上做了若干改进,最显著的是尽可能遵循 PDDL 约定和语法,并包含若干新算法。它依赖 FastDownward 作为底层经典规划器——这与本章 §5.1"归约为有限 PDDL 问题"完全一致:PDDLStream 负责采样调度,FastDownward 负责解每个有限 PDDL 问题。
| 工程要点 | 说明 |
|---|---|
| 底层规划器 | FastDownward(需编译,§5.1 解有限 PDDL 问题) |
| 算法选择 | solve(..., algorithm=...):incremental/focused/adaptive(§5.4) |
| Stream 定义 | .pddl 文件写声明 + Python 写生成器,通过 stream_map 注册 |
| 适配新机器人 | 替换生成器里的 IK/碰撞/运动规划为你的机器人工具(§3.1 的黑盒) |
8.2 接入 TAMP_T1 的 Mini-TAMP 累积项目¶
TAMP_T1 §11 的 Mini-TAMP 用的是朴素的 Plan-then-Check 协调器(先 pyperplan 规划、再逐步检查运动,正是 §2.3 批判的方案)。本章的累积项目升级:用 PDDLStream 替换 Plan-then-Check,让系统从"盲目事后检查"变成"采样-搜索交织"。
# ===== Mini-TAMP 的 PDDLStream 升级(替换 T1 §11 的 TAMPCoordinator) =====
class PDDLStreamCoordinator:
"""用 PDDLStream 替换 T1 的 Plan-then-Check 协调器。
把 T1 已有的几何能力(IK/碰撞/运动)封装成 Stream 生成器。"""
def __init__(self, env, algorithm="adaptive"):
self.env = env # 复用 T1 的 PyBullet 环境
self.algorithm = algorithm
def _make_stream_map(self):
"""把 T1 已有的几何工具封装成 Stream 生成器(§4.3)。"""
env = self.env
def grasp_gen(obj):
while True:
g = env.sample_grasp(obj) # T1 已有
if g is not None: yield (g,)
def ik_gen(obj, pose, grasp):
for _ in range(20):
q = env.solve_ik(env.compose(pose, grasp)) # T1 的 MotionPlanner.solve_ik
if q is not None: yield (q,)
def motion_gen(q1, q2):
tau = env.plan_motion(q1, q2) # T1 的 MotionPlanner.plan_motion
if tau is not None: yield (tau,)
return {"sample-grasp": grasp_gen, "sample-ik": ik_gen, "plan-motion": motion_gen}
def solve(self, init, goal):
from pddlstream.algorithms.meta import solve
problem = (self._domain(), {}, self._streams(),
self._make_stream_map(), init, goal)
plan, cost, cert = solve(problem, algorithm=self.algorithm)
return plan # 可执行计划(参数都是真实采样值)
关键点:升级不需要重写几何能力——T1 已经实现了 IK、碰撞、运动规划,本章只是把它们封装成 Stream 生成器(§3.1 的"接口而非重写")。改变的是协调方式:从 Plan-then-Check 的盲目回溯,变成 PDDLStream 的采样-搜索交织。
8.3 累积项目里程碑¶
| 阶段 | 章节 | 累积内容 |
|---|---|---|
| 阶段 0 | TAMP_T1 §11 | 单机 Mini-TAMP(Plan-then-Check 协调器) |
| 阶段 1 | TAMP_T2 §11 | 多机分配层(匈牙利/SSI) |
| 阶段 2(本章) | 本章 §8 | 用 PDDLStream 替换 Plan-then-Check 协调器 |
| 阶段 3 | T5 | 用行为树替换协调器,加执行监控 |
8.4 调试 PDDLStream:当它跑不出解时的排查清单¶
PDDLStream 是个多组件系统(domain + stream 声明 + 生成器 + 求解器),出问题时定位往往困难。§5.5 已给了首要原则——完备性依赖采样器,找不到解先怀疑 Stream。这里给一个系统的分层排查清单,从最可能的原因查起。
PDDLStream 跑不出解的分层排查(从最常见到最少见):
第1层 — 单独测每个 Stream 生成器:
对目标涉及的每个 Stream, 手动喂典型输入, 看它能否 yield 出值
- sample-ik 对那个高架子能产出构型吗? (§5.5: 子流形上能采到吗)
- 若某 Stream 永远不 yield → 它是瓶颈, 检查几何工具/采样范围
第2层 — 检查认证事实是否名副其实:
每个生成器 yield 的值, 真的满足 :certified 吗?
- 加断言: 对 yield 的 q, assert 它真的满足 Kin / 无碰撞
- 若认证了假事实 → 搜索基于假信息, 计划落地失败 (§4 陷阱4-2)
第3层 — 检查 domain 谓词与 stream 认证的衔接:
动作前提用的谓词, 都有 Stream 或 init 提供吗?
- 某前提谓词无人认证 → 依赖链断裂, 乐观搜索永远缺这一环 (§3.4, §5.3)
- 谓词名拼写一致吗? (Kin vs kin)
第4层 — 检查 level / 算法配置:
乐观对象展开够深吗? (§5.3)
- level 不够 → 乐观骨架凑不齐, 试加深或换 Adaptive
- 用了 Incremental 且问题紧约束 → 换 Focused/Adaptive (§5.4)
第5层 — 目标本身可行吗:
确认目标几何上真的可达 (架子不是高到任何 IK 都无解)
- 若目标不可行, PDDLStream 会一直采样 (概率完备的代价, §5.5 陷阱5-4)
本质洞察:这个排查清单的顺序,正是 PDDLStream 出错概率从高到低的顺序——绝大多数"跑不出解"的问题在第 1-2 层(Stream 采不到值、认证了假事实),而非求解器本身。这再次印证 §5.5 的核心:完备性责任在采样器。一个高效的调试习惯是"自底向上、先隔离再集成":先单独确认每个 Stream 能正确产出/认证(第 1-2 层),再确认它们的衔接(第 3 层),最后才调求解器配置(第 4 层)。这与调试任何多组件系统的原则一致——先确认每个零件好用,再怀疑装配。新手常犯的错是一上来就调求解器参数(第 4 层),却跳过了最可能出错的 Stream 本身。
区分两类不同的问题:找不到解 vs 找得慢。 上面的清单针对"找不到解"。但还有一类常见困扰是"能找到解,但太慢"——这要换一套排查思路,核心是性能剖析:
"能解但慢"的剖析(区别于"找不到解"):
统计各 Stream 的调用次数与耗时:
- 哪个 Stream 被调用最多次? (可能是采样器盲目, §7.3)
- 哪个 Stream 单次最慢? (通常是 plan-motion/IK 等昂贵几何, §5.6 懒惰)
对症下药:
- 调用次数多 → 让采样器采得准(§7.3)或换 Adaptive(§5.4)减少无效采样
- 单次慢 → 用惰性策略推迟昂贵 stream(§5.6), 只在必需时调用
- 符号搜索慢 → 检查对象数是否爆炸(§10.3 长链问题)
这呼应 §5.6 的代价-速度旋钮和 §7.3 的采样器质量——"慢"几乎总是"采样器盲目"或"昂贵 stream 调用过多",而非符号搜索本身(符号搜索有 FastDownward 的强启发式撑着)。所以剖析时先看 Stream 的调用统计,这是定位性能瓶颈的最快路径。
⚠️ 常见陷阱¶
陷阱 8-1(编程陷阱):把 T1 的几何工具重写而非封装。 - 错误描述:接入 PDDLStream 时重新实现 IK/碰撞/运动规划。 - 现象/后果:重复劳动,且新实现可能与 T1 的不一致,引入 bug。 - 根本原因:误解 PDDLStream 的"黑盒"定位——它要的是把已有工具封装成生成器,不是重写。 - 正确做法:把 T1 已有的几何方法直接包进 Stream 生成器(§7.2),只改协调方式。
陷阱 8-2(思维陷阱):以为换上 PDDLStream 就一定比 Plan-then-Check 快。 - 错误描述:认为 PDDLStream 在任何问题上都碾压 Plan-then-Check。 - 现象/后果:在极简单问题(单物体、几何宽松)上,PDDLStream 的调度开销可能反而不如直接 Plan-then-Check。 - 根本原因:PDDLStream 的优势在紧约束/多对象(避免盲目回溯);简单问题上回溯本就少,调度开销显现。 - 正确做法:理解适用边界——PDDLStream 解决的是组合复杂、几何紧约束的问题;简单问题用什么都行。
练习¶
- (⭐⭐) 把 §7.2 的
PDDLStreamCoordinator接入 TAMP_T1 §11 的 Mini-TAMP 环境,跑通单物体 pick-and-place,对比它与原 Plan-then-Check 协调器的输出。 - (⭐⭐⭐) 构造一个"紧约束"场景(物体被部分遮挡、放置区域狭小),对比 PDDLStream 与 Plan-then-Check 的求解时间,验证 §7.2 末尾的论断。
- (⭐⭐⭐) 把 §7.2 的
algorithm在 incremental/focused/adaptive 间切换,在同一问题上对比求解时间与采样次数,印证 §5.4 的算法差异。 - (⭐⭐⭐,性能剖析) 按 §8.4 的"能解但慢"剖析思路,给 §7.2 的协调器加一段统计代码:记录每个 Stream(grasp/ik/motion)被调用的次数和总耗时。在一个稍复杂的场景上跑,找出"调用最多次"和"单次最慢"的 Stream,并据此判断该用 §7.3(采得准)还是 §5.6(惰性)来优化。
9. 真实应用:PDDLStream 在机器人上的落地 ⭐⭐⭐¶
前面用 pick-and-place 这个最小例子讲透了原理。本节看 PDDLStream 在真实、复杂机器人任务上的应用,让你对它的能力边界有实感——既看到它能做什么,也为 §10 的局限讨论铺垫。
9.1 长时域厨房任务:切黄瓜沙拉¶
一个有代表性的落地工作,是把 PDDLStream 用于真实烹饪任务。该工作提出一个面向真实烹饪任务的集成 TAMP 框架,用双臂机器人系统,把 PDDLStream 与 MoveIt Task Constructor(一个多阶段操作规划器)结合,以增强相互依赖任务的多步运动规划。它用各种烹饪相关技能(物体固定、基于力的尖端检测、用强化学习做切片)增强框架,以制作简单黄瓜沙拉这一长时域任务为案例(切片并装盘)为目标。
这个案例揭示了几个超出 pick-and-place 的难点:
| 难点 | 为什么 pick-and-place 没有 | PDDLStream 如何应对 |
|---|---|---|
| 双臂相互干扰 | 单臂无协同问题 | 在烹饪场景里,某只手臂的固定抓取可能干扰切片、与另一只手臂碰撞——需 test stream 表达双臂间碰撞 |
| 接触丰富的切片 | 抓放无持续接触 | 切片是力控、接触密集——这里用 RL 技能补充(呼应 §10 会讲的 PDDLStream 接触处理局限) |
| 长时域(14 步级) | pick-place 仅 2-4 步 | 乐观规划的对象展开随步数增长(§5.5 因子化 + §10 长时域局限) |
本质洞察:这个案例印证了 §10(流式 vs 优化式)的判断——PDDLStream 擅长"组合摆放"的骨架(拿刀、固定黄瓜、移动到盘子),但接触密集的切片动作它不直接处理,而是借助外部技能(RL 切片、MoveIt Task Constructor 的多阶段运动)。这正是真实系统的常态:PDDLStream 做高层符号-几何骨架,把接触/力控这类"调优型"子问题外包给专门方法。没有哪个框架包打天下——理解 PDDLStream 的边界,才能知道哪些子问题该交给别人(这是 §10 横向对比的实践意义)。
9.2 Kitchen Worlds:长时域 TAMP 的标准测试场¶
为了系统地研究和评测长时域 TAMP,社区构建了基于 PDDLStream 的开源问题库。Kitchen Worlds 是其中代表——一个厨房和家居场景的长时域 TAMP 问题库,以及求解它们的规划器;它能可视化 LISDF 格式的场景(SDF 的扩展,含 URDF),用 PDDLStream 求解由 scene.lisdf、problem.pddl、domain.pddl、stream.pddl 定义的 TAMP 问题,并能程序化生成含刚体和铰接物体的场景。
这类测试场的价值,对学习者有两层:
- 学习价值:它提供了大量真实复杂度的 domain/stream 范例,是从"会写 pick-place"进阶到"会写复杂领域 Stream"的最佳素材——对照 §7 的设计模式读这些范例,能看到模式在真实领域如何应用。
- 研究价值:它是评测 PDDLStream 改进算法(更好的采样、学习引导)的标准基准,新方法常在它上面与原始 PDDLStream 比较。
9.3 移动操作与可移动障碍导航(NAMO)¶
前两个案例都偏"桌面操作"。但 PDDLStream 的思想在移动操作(mobile manipulation)——机器人既要移动底盘又要操作物体——同样适用,而且这里符号-几何耦合更强:底盘走到哪决定了机械臂能操作什么,操作了什么又改变了能走的路。
一类典型问题是可移动障碍导航(NAMO, Navigation Among Movable Obstacles)。导航的目标不总能直接到达,有时需要操作环境——开门、抓走挡路的物体、推开挡路的家具;这类需要操作环境才能导航的问题称为 NAMO。这与本章的 pick-and-place 有一个关键的相似:"能不能到达目标"依赖"先做哪些操作"(先把挡路的箱子搬开,才能走过去),正是符号决策(搬哪个、按什么顺序)与几何可行性(搬开后路通不通)的耦合——和 §2.1 的参数耦合同源,只是尺度从桌面放大到整个房间。
PDDLStream 同源团队(Kaelbling、Lozano-Pérez 组)把这条线推进到了部分可观测。VANAMO 工作研究"可见性感知的可移动障碍导航"(VANAMO),放宽了"地图完全可见、物体位置完全已知"的假设,提出 LAMB(Look and Manipulate Backchaining)算法,有基于视觉的简单 API,易迁移到真实机器人并能扩展到大型 3D 环境。它把本章确定性、全可观的设定,推向了真机更现实的"边看边操作"——这正是 §9.5 要讲的"从仿真到真机"鸿沟在导航尺度上的体现。
本质洞察:从桌面 pick-and-place 到房间尺度的 NAMO,符号-几何耦合的本质不变、尺度放大——桌面上是"抓取位姿约束放置位姿",房间里是"搬开哪个障碍决定哪条路可通"。这说明本章学的 Stream 思想(用采样器把几何可行性反馈进符号搜索)不是只对抓放摆有效,而是一套处理"离散决策 ↔ 连续几何耦合"的通用方法论。无论问题是桌面整理、厨房烹饪、还是房间导航,只要存在"做什么取决于几何可行性、几何可行性又取决于做什么"的耦合,Stream 的范式就适用。理解了这种尺度无关的通用性,你就不会把 PDDLStream 局限在"机械臂抓放"的小盒子里——它是任务层缝合符号与几何的一把通用钥匙。
9.4 从案例看 PDDLStream 的工程定位¶
综合这些应用,PDDLStream 在真实系统里的定位逐渐清晰:
PDDLStream 在真实机器人系统中的典型角色:
上游: 自然语言/高层目标 (可能由 LLM 转成 PDDL 目标, 接 T7)
│
PDDLStream: 符号-几何骨架规划 ← 本章主体
│ 产出"拿刀→固定黄瓜→切片→装盘"的骨架 + 抓放摆的几何参数
│ 接触密集子动作(切片)外包给↓
外部技能: RL 切片 / MoveIt Task Constructor 多阶段运动 / 力控
│
执行: 行为树监控执行(接 T5)
本质洞察:真实 TAMP 系统几乎从不是"纯 PDDLStream",而是 PDDLStream 作为符号-几何骨架的协调中枢,上接语言理解(LLM)、下接专门技能(RL、力控、多阶段运动)、外加执行监控(行为树)。这呼应 TAMP_T0 §3.8 那张"六大板块协作数据流"——PDDLStream 是板块③(流式集成),它在完整系统里与板块⑤(LLM,T7)、板块④(行为树,T5)、以及②(优化式技能,T4)协同。学 PDDLStream 的终点不是孤立地用它,而是理解它在整个任务层生态里的接口位置——这也是为什么本章反复强调它与 LLM(§9.4 上游)、LGP(§10 对比)、行为树(§8 接 Mini-TAMP)的关系。
9.5 从仿真到真机:PDDLStream 落地的工程鸿沟¶
§6 的案例和 §8 的接入都隐含一个假设——几何过程(IK、碰撞、运动)能给出可靠结果。但从仿真到真机,有几道工程鸿沟必须正视,它们决定 PDDLStream 规划出的计划在真机上能否真正执行。这些是 §9.1 那类真实工作背后的隐性挑战。
鸿沟一:感知给的状态是带噪声的估计。 PDDLStream 的 problem 里,物体初始位姿 (AtPose cup p0) 在仿真里是精确已知的,但真机上 p0 来自感知(相机+位姿估计),有噪声。规划基于不准的 p0,采样的抓取/IK 可能在真机上偏了。
感知噪声对 PDDLStream 的影响:
规划时假设 cup 在 p0, 采样抓取 g、构型 q 都基于 p0
真机上 cup 实际在 p0 + 噪声 → 按 q 去抓可能抓偏/抓空
缓解: (1)抓取采样器留容差(采更鲁棒的抓取)
(2)执行时闭环修正(视觉伺服, 接 T5 行为树的监控)
(3)不确定性下的 TAMP(把噪声建进规划, 接 T6)
这正是为什么本章是确定性 TAMP,而 T6(不确定性 TAMP)要专门处理感知噪声——PDDLStream 的"认证事实"假设几何关系确定,噪声打破了这个假设。
鸿沟二:认证的几何关系在真机上要留余量。 §4 强调"认证名副其实"——但仿真里"恰好无碰撞"的构型,真机上因标定误差、控制误差可能真的碰了。所以真机部署时,碰撞检测、可达性判断都要留安全余量(膨胀障碍、收紧关节限位),让认证的事实在真机的误差范围内仍成立。这是 §4 陷阱 4-2 在真机上的延伸:认证不仅要"仿真里真",还要"真机误差内真"。
鸿沟三:规划时间与执行节奏。 PDDLStream 求解(尤其紧约束、多对象)可能要数秒到数十秒。真机上若环境动态变化(人走过、物体被移动),规划出的计划可能在执行时已过时。缓解靠 §8 的快配置(§5.6 最懒+Adaptive 先出解)+ 执行层监控(T5 行为树,发现环境变了就触发重规划)。
本质洞察:从仿真到真机的三道鸿沟——感知噪声、误差余量、规划-执行节奏——揭示了一个普遍真相:PDDLStream(以及一切规划器)规划的是"模型里的世界",而执行发生在"真实的世界",两者总有差距。弥合这个差距不是 PDDLStream 本身的职责,而要靠它与其他板块协同:感知噪声交给 T6(不确定性 TAMP)、执行偏差交给 T5(行为树闭环监控)、动态变化交给重规划。这再次印证 §9.4——PDDLStream 是协调中枢,真机落地必须配齐上下游。理解这些鸿沟,你才不会天真地以为"仿真里跑通=真机能用",这是从 demo 到产品的关键认知。
练习¶
- (⭐⭐) §9.1 的切黄瓜任务把"切片"外包给 RL 而非用 PDDLStream 直接规划。结合 §10(流式 vs 优化式),解释为什么切片不适合 PDDLStream 的采样范式。
- (⭐⭐⭐) 下载或浏览 Kitchen Worlds 的一个 domain/stream 范例,对照 §7 的设计模式,找出它用了哪些模式(因子切分、test stream、采样器偏置)。
- (⭐⭐⭐) 画出一个"用语音命令让机器人做沙拉"的完整系统数据流,标出 PDDLStream 在其中的位置,以及它上下游分别接什么(呼应 §9.4 与 TAMP_T0 §3.8)。
- (⭐⭐⭐,真机) §9.5 列了从仿真到真机的三道鸿沟。对"机械臂按规划的抓取位姿去抓杯子,但杯子实际位置有 2cm 感知误差"这个情形,分别说明三种缓解手段(抓取留容差/执行闭环修正/不确定性 TAMP)各如何应对,以及它们分别接本线哪一章。
- (⭐⭐⭐⭐,NAMO 建模) 为 §9.3 的"可移动障碍导航"设计 PDDLStream 的核心谓词与动作:机器人要从 A 走到 B,但路上有箱子挡着,搬开箱子才能通过。提示:需要
(BlocksPath box path)、(ClearPath path)等谓词,一个move-obstacle动作和一个navigate动作。说明这里的符号-几何耦合(搬哪个箱子决定哪条路通)如何对应 §2.1 的参数耦合,以及该用什么 Stream 判定"路是否被挡"。
10. PDDLStream 与其他 TAMP 范式:横向对比与局限 ⭐⭐⭐¶
TAMP_T0 §3 把缝合符号-几何鸿沟的方法分为多个板块。本节把 PDDLStream 放进整个 TAMP 范式谱系,横向对比,并诚实地讨论它的局限——避免 §1"如果跳过本章会怎样"场景二那种"用错范式"的错误,也为后续章节(T4 LGP、T7 学习)铺垫。
10.1 与 LGP:采样 vs 优化(接 T4)¶
最重要的对比是与 LGP(逻辑-几何规划,下一章 TAMP_T4),它们是缝合鸿沟的两条主流路。
| 维度 | PDDLStream(流式,本章) | LGP(优化式,T4) |
|---|---|---|
| 核心机制 | 符号搜索调用几何采样器(Stream) | 把符号选择与轨迹优化联立成一个优化问题 |
| 符号与几何的关系 | 分工、交替(搜索↔采样) | 联立、同时求解 |
| 几何处理 | 采样(生成离散候选值) | 连续优化(梯度下降轨迹) |
| 求解主体 | 离散符号搜索 + 黑盒采样 | 混合离散-连续非线性优化 |
| 数学形态 | 归约为一系列有限 PDDL(§5.1) | 逻辑约束的混合优化(KOMO,T4) |
各自擅长什么。 PDDLStream 擅长"组合摆放"——物体多、抓放摆为主、约束是"够不够得到、放不放得稳"这类可采样判定的几何关系,采离散候选很自然(§6 的 pick-and-place 是典型)。LGP 擅长"接触丰富"——推、倒、插、力控这类动力学密集、接触连续的操作。优化式 TAMP 用目标函数定义目标条件,能处理开放式目标、机器人动力学和物理交互,特别适合求解高度复杂、富接触的运动与操作问题。
把常见操作按这个标准归类,作为选型参考:
| 任务 | 几何约束类型 | 推荐范式 |
|---|---|---|
| 把多个物体摆到指定位置 | 选择型(抓哪、放哪) | PDDLStream |
| 整理桌面、货架补货 | 选择型(顺序 + 抓放) | PDDLStream |
| 倒水、舀取 | 调优型(倾角、速度连续) | LGP(或外包 RL,如 §9.1) |
| 推动重物到目标 | 调优型(接触力、推点) | LGP |
| 插销、拧螺丝 | 调优型(对齐、力控) | LGP |
| 开门、拉抽屉(铰接物体) | 混合(选抓取 + 调轨迹) | 二者结合(§10.4) |
本质洞察:PDDLStream 与 LGP 的分野,本质是"采样" vs "优化"两种处理连续量的哲学之争——这是贯穿整个规控领域的一条主线(你在 MPPI 线的采样式 MPC vs 梯度式 MPC 对比里见过同样的分野)。采样(PDDLStream)适合"离散候选中选一个就行"的几何关系(抓哪个位姿、放哪个位置);优化(LGP)适合"连续地调到最好"的几何量(力、角度、平滑度)。判断用哪个范式,先问你的几何约束是"选择型"还是"调优型":选择型(抓放摆)→ PDDLStream;调优型(推倒插、力控)→ LGP。这正是 TAMP_T0 §6 选型决策树里 Q4 那一问的依据。§9.1 切黄瓜把切片外包给 RL,正是因为切片是"调优型"、不适合 PDDLStream 采样。
10.2 与 FFRob、STRIPStream:同一谱系的演进¶
PDDLStream 不是凭空出现的,它是同一研究脉络迭代三代的产物。理解这个演进,能看清 PDDLStream 每个设计为解决什么前代问题。
| 代际 | 框架 | 核心 | 局限(催生下一代) |
|---|---|---|---|
| 第一代 | FFRob (2014/2017) | 用流形采样 + 多查询 roadmap 把 TAMP 问题迭代离散化,让 EAS 规划器算出含几何约束的启发式;概率完备、有限期望运行时 | 针对 pick-place 等固定问题类,不够通用 |
| 第二代 | STRIPStream (2018) | 引入扩展动作规范 (EAS) 作为支持任意谓词作条件的通用规划表示,把 FFRob 推广到任意采样器 | 表示未完全遵循 PDDL 约定 |
| 第三代 | PDDLStream (2020) | PDDLStream/STRIPStream 框架的"第三版",旨在取代之前版本;尽可能遵循 PDDL 约定和语法,并含若干新算法 | (当前主流) |
迭代版 FFRob 是概率完备且指数收敛的;PDDLStream 的 Incremental 算法可看作把这种"迭代采样-搜索"策略从 pick-place 领域推广到任意条件采样器定义的领域。这条演进印证了 §5.5 的理论谱系——PDDLStream 是 PRM→FFRob→STRIPStream→PDDLStream 这条"采样-搜索"主线的最一般者。
了解这条演进对学习者有什么用? 三点实际价值。其一,看清设计的"为什么":PDDLStream 每个看似理所当然的设计(黑盒采样器、遵循 PDDL、optimistic),都是为解决前代的某个具体痛点——FFRob 不够通用催生了 STRIPStream 的"任意谓词",STRIPStream 不遵循 PDDL 催生了 PDDLStream 的"复用 FastDownward"。其二,判断新工作的定位:读到一篇新 TAMP 论文,你能快速判断它在这条谱系的哪个位置、改进了哪一环(采样?搜索?建模?)。其三,预判未来方向:这条线一路在"更通用 + 更高效 + 更易用"上推进,下一步(学习引导、LLM 辅助)也沿此延展。
本质洞察:FFRob→STRIPStream→PDDLStream 的演进,与 TAMP_T0 §5 描绘的整个任务层历史钟摆(STRIPS→PDDL→TAMP→LLM)是同一个动力学在更小尺度上的重演——每一代都在"扩大能处理的问题范围"和"保持可验证性/效率"之间寻找新平衡。FFRob 解决了 pick-place 的几何,STRIPStream 把它推广到任意采样器(扩大范围),PDDLStream 又遵循 PDDL 约定以复用成熟规划器(保持效率与易用)。看懂一个框架的演进史,等于看懂了它所在领域的进化逻辑——这也是为什么本章不只讲 PDDLStream"是什么",还要讲它"从哪来、往哪去"。
10.3 PDDLStream 的局限与改进方向¶
诚实面对局限,是用对工具的前提。PDDLStream 主要有三类局限:
局限一:长时域多对象时乐观对象盲目展开。 这是最被诟病的。§5.2 的乐观对象按 level 展开,但这个集合是以广度优先方式被穷举式扩展的,不管手头问题的逻辑和几何结构,使得多对象长时域推理变得极其耗时。物体一多、计划一长,乐观对象数爆炸。改进方向是学习引导:提出几何信息引导的符号规划器,以最佳优先方式扩展对象与事实集合,由从过往搜索计算中学到的图神经网络排序(接 T7)。
局限二:接触密集操作处理弱。 §10.1 已述——采样范式不擅长力控、推倒插。改进是与 LGP(T4)或 RL 技能(§9.1)结合。
局限三:plan-first 而非 anytime 的某些变体效率问题。 后续工作如 COAST 指出并改进:提出一个概率完备、plan-first 的 TAMP 算法,显著快于 PDDLStream;在任务规划时间上比 PDDLStream 和 IDTMP 快一个数量级。这类工作持续优化 PDDLStream 的搜索策略。
| 局限 | 表现 | 改进方向 | 相关章节 |
|---|---|---|---|
| 长时域多对象慢 | 乐观对象广度优先盲目展开 | 学习引导展开(GNN 排序) | T7 |
| 接触密集弱 | 采样不适合力/接触连续量 | 结合 LGP / RL 技能 | T4, §9.1 |
| 搜索效率 | 某些场景下不够快 | 改进搜索算法(如 COAST) | 前沿 |
一个量化的例子。 局限一(长时域多对象慢)有多严重?COAST 工作给了对照数据:在需要长链 stream 计划(一个 stream 实例的输出频繁作为后续 stream 输入)的厨房任务上,当有 8 个目标时,PDDLStream 总规划时间达 1000 秒,而 COAST 只需 10 秒——差距达两个数量级。原因正是局限一:长链 stream 计划需要 PDDLStream 多轮 stream 生成才能产出高层 stream 实例,许多符号 stream 对象被加入任务状态,拖慢了任务规划。但同一工作也指出 PDDLStream 在另一些领域(如 Rover)表现尚可——因为该领域 PDDLStream 的增量 stream 生成能在动作和规划迭代间复用 rover 位置。这说明局限是领域相关的:长链、对象频繁新增的领域最吃亏,能复用对象的领域影响小。
本质洞察:PDDLStream 的三类局限,恰好指向 TAMP 线后续三章——长时域慢→学习引导(T7)、接触弱→优化式(T4)、效率→更好的搜索。一个框架的局限,往往就是下一个研究方向的起点。这也说明 §1"如果跳过本章场景二"的告诫为何重要:不懂 PDDLStream 的边界,你既不知道何时该换 LGP(接触),也不知道何时该上学习引导(长时域),更无法理解 COAST 这类改进在改进什么。掌握一个方法的边界,和掌握方法本身同等重要。
10.4 不是非此即彼¶
各范式并非互斥——前沿工作常结合:用 PDDLStream 式的符号搜索做高层骨架,用 LGP 式的优化做局部轨迹精化,用学习引导采样/对象展开。§9.1 的切黄瓜(PDDLStream + RL + MoveIt Task Constructor)就是活生生的结合案例。本章和 T4 分别讲透两条范式,是为了让你理解各自的内核;实践中按问题特性选择或混合,是更高阶的能力(T9 综合实战会涉及)。
10.5 LLM 与 PDDLStream:能替代还是只能辅助(接 T7)¶
PDDLStream 有一个被诟病的工程负担:domain 和 Stream 都要人手写。大模型 (LLM) 的兴起带来一个诱人的设想——能不能让 LLM 自动生成 PDDL domain、甚至替代 PDDLStream 的某些组件?这是 §9.4 提到"上游接 LLM"的具体化,也是 TAMP_T7 的核心议题。本节给出当前研究的诚实图景。
方向一:LLM 生成 PDDL(降低建模负担)。 这是相对成熟的方向。有方法利用 LLM 和环境反馈自动生成 PDDL domain 与 problem 描述文件,无需人工干预;通过迭代细化过程生成多个 problem PDDL 候选,并基于与环境交互的反馈逐步细化 domain PDDL。但有个关键限制:LLM 能把自然语言领域描述转成看起来合理的 PDDL 标记,但确保动作在领域内一致仍是难题;自动一致性检查能改善 LLM 生成的 PDDL 质量,却仍不能保证绝对正确。
本质洞察:LLM 生成 PDDL 的价值与边界,恰好印证 TAMP_T0 §5 那个"钟摆"——LLM 扩大了"能从自然语言进入形式化"的范围,但牺牲了可验证性(生成的 PDDL 可能不一致、不正确)。所以主流做法不是"LLM 生成完直接用",而是"LLM 生成 + 形式化检查/修复":LLM 出草稿,PDDL 一致性检查器(或 PDDLStream 求解失败的反馈)把关修正。这与本章 §4 陷阱 4-2"认证必须名副其实"是同一种精神——LLM 的产出和 Stream 的认证一样,都需要被验证,不能盲信。
方向二:LLM 替代 PDDLStream 组件(替代还是辅助?)。 更激进的设想是用 LLM 直接替换 PDDLStream 的求解组件(如让 LLM 直接生成动作序列、或充当采样器)。一项系统评测给出了冷静的结论。Mendez-Mendez (2025) 的工作开发了 16 个用 Gemini 2.5 Flash 替换关键 TAMP 组件的算法,在三个领域 4950 个问题上的零样本实验显示,基于 Gemini 的规划器比工程化的对手成功率更低、规划时间更长。
这个结果很有教育意义——它用大规模实验回答了"LLM 能否替代 PDDLStream 组件":目前不能(至少零样本下不能)。值得注意的是,这项工作也复述了 Stream 的精确定义,与本章 §3-§4 完全一致:stream 给定输入 x 产生输出序列 y(1), y(2), ...,每个输出满足证书谓词 cert(x, y(i));test stream 是不产出值的特例,如 test-cfree 认证 cFree;stream 若无法满足其证书则失败——印证了本章对 Stream(§4.1 四要素)和 test stream(§4.4)的讲法。
本质洞察:LLM 与 PDDLStream 的关系,目前的共识是"辅助而非替代"——LLM 擅长降低建模负担(生成 PDDL 草稿、提供常识先验),但 PDDLStream 的形式化求解(保证几何可行、概率完备)仍不可替代。这与 TAMP_T0 §3.6 对 LLM 的定位完全一致:"有常识但不严谨的任务规划器"。把 LLM 套在 PDDLStream 外层(生成 domain、提供采样先验、解释失败),而非用它替换内核(求解、采样、验证),是当前最稳妥的结合方式。§5.5 的理论保证(概率完备)正是 PDDLStream 不可被 LLM 替代的硬核——LLM 没有这种保证。T7 会深入这条线。
小结:三个层次的结合。
| 结合层次 | LLM 角色 | 成熟度 | 风险 |
|---|---|---|---|
| 外层(生成 domain/stream 草稿) | 降低人工建模负担 | 较成熟,需检查修复 | 生成的 PDDL 不一致 |
| 中层(提供采样先验/启发式) | 引导 Stream 采样、对象展开(呼应 §10.3 GNN) | 研究中 | 先验可能误导 |
| 内核(替代求解/采样组件) | 直接生成计划或采样 | 不成熟(Mendez-Mendez 2025 显示更差) | 失去可验证性与完备性 |
练习¶
- (⭐⭐) 对下列任务各判断该用 PDDLStream 还是 LGP,说明理由:(a) 把散落的 8 个积木摆到指定货架;(b) 把一杯水倒进窄口瓶;(c) 把插头插进插座。
- (⭐⭐⭐) §10.2 的三代演进中,PDDLStream 相比 STRIPStream 的主要改进是"遵循 PDDL 约定"。为什么这个看似工程性的改进很重要?(提示:能复用 FastDownward 等成熟 PDDL 规划器,§5.1。)
- (⭐⭐⭐) §10.3 局限一说乐观对象"广度优先盲目展开"。结合 §5.2-§5.3,解释为什么物体多、计划长时这会爆炸,以及"最佳优先 + GNN 排序"如何缓解。
- (⭐⭐⭐⭐,跨章综合) 把本节"采样 vs 优化"的分野,与 MPPI 线"采样式 MPC vs 梯度式 MPC"对照——两组对比反映的是否是同一种更深的权衡?用 3-4 句话阐述。
- (⭐⭐⭐,前沿) §10.5 把 LLM 与 PDDLStream 的结合分三层(外层/中层/内核)。为什么"内核替代"目前最不成熟?结合 §5.5 的概率完备性说明 LLM 缺什么保证。
- (⭐⭐⭐⭐,定量分析) §10.3 给的数据是:8 目标厨房任务 PDDLStream 1000s、COAST 10s,但 Rover 领域 PDDLStream 表现尚可。结合"长链 stream 计划"与"对象可复用"两个因素,解释为什么同一个 PDDLStream 在两个领域差距如此之大。这对你判断"自己的问题适不适合 PDDLStream"有什么启示?
本章常见误解汇总¶
| 误解 | 正确理解 | 对应节 |
|---|---|---|
| 把连续参数离散化就能用经典规划器 | 离散化没解决参数耦合,粒度两难、盲目预生成;要用 Stream 按需采样 | §2 陷阱 2-1 |
| plan-then-check 失败是重试次数不够 | 症结是符号-几何信息单向;要建立双向信息流(Stream 认证反馈) | §2 陷阱 2-2 |
| Stream 是另一种动作,会改变状态 | Stream 不改状态,只生成值并认证静态几何事实;动作才改流式状态 | §3 陷阱 3-1 |
| Stream 生成器必须穷尽所有解 | Stream 是按需生成器(yield),求解器控制取多少;无穷生成器合法 | §3 陷阱 3-2 |
| 生成器用 return 一次返回所有解 | 用 yield 按需产出;return 对无穷解无法完成、对有限解浪费 | §4 陷阱 4-1 |
| 认证生成器未真正保证的事实 | 认证是对搜索的承诺,必须名副其实,否则计划几何不可行 | §4 陷阱 4-2 |
| 混淆静态事实与流式状态 | Stream 认证静态几何关系(永真);动作增删流式状态(会变) | §4 陷阱 4-3 |
| PDDLStream 直接把无限问题交给规划器 | 它归约为一系列有限 PDDL 问题,每个交给 FF;采样把无限变有限 | §5 陷阱 5-1 |
| 乐观计划就是可执行计划 | 乐观计划是假想值的骨架,必须经采样验证、升级为真实值才能执行 | §5 陷阱 5-2 |
| 所有问题都用 Incremental(因为简单) | Incremental 盲目采样,紧约束/多对象慢;那时用 Focused/Adaptive | §5 陷阱 5-3 |
| 接入 PDDLStream 要重写几何工具 | 把已有 IK/碰撞/运动封装成 Stream 生成器即可(黑盒,不重写) | §8 陷阱 8-1 |
| PDDLStream 在任何问题上都比 plan-then-check 快 | 优势在紧约束/多对象;极简单问题上调度开销可能反而不划算 | §8 陷阱 8-2 |
| PDDLStream 和 LGP 二选一、互斥 | 两条范式,按约束是"选择型/调优型"选;前沿常结合 | §10.1, §10.4 |
| 概率完备意味着实践中一定快速找到解 | 完备是渐近性质(采样→∞),不保证有限时间或快 | §5.5 陷阱 5-4 |
| 找不到解是算法的错 | 完备性完全依赖采样器;先查 Stream 能否采到必要值 | §5.5 陷阱 5-5 |
| PDDLStream 只能找可行解、不能优化 | 外部成本函数让几何代价入符号搜索,可找低代价解 | §5.6 陷阱 5-6 |
| 一上来就用最优配置 | 最优最慢;先用最懒+Adaptive 求可行,再调向质量 | §5.6 陷阱 5-7 |
| 用大一统 Stream 一次采所有参数 | 抹平因子结构、退回维度灾难;沿因子切小 | §7 陷阱 7-1 |
| 所有约束都塞进 generator 认证 | 依赖运行时场景的约束写 test/fluent stream | §7 陷阱 7-2 |
| 慢就只换算法、不优化采样器 | 采样器先验质量是性能地基;先采得准再调算法 | §7 陷阱 7-3 |
| LLM 能直接替代 PDDLStream 内核 | 目前更差(Mendez-Mendez 2025);LLM 辅助生成而非替代求解 | §10.5 |
| 有几何或有离散选择就该用 PDDLStream | 关键是离散选择的可行性是否依赖几何(耦合)才需 TAMP | §2.5 |
| Incremental 的"采样-搜索"很高效 | Incremental 盲目全采(§6.7 可见),多对象时爆炸,故需乐观对象 | §6.7, §5.4 |
| PDDLStream 只适合机械臂抓放摆 | 处理"离散决策↔几何耦合"的通用方法,桌面/厨房/导航(NAMO)皆适用 | §9.3 |
本章小结¶
本章打开了 TAMP_T1 当黑盒用的 PDDLStream,回答了一个问题:符号规划器如何处理连续的几何参数? 回顾全章逻辑链:
- §2 重访鸿沟:朴素离散化(盲目预生成)和 plan-then-check(盲目事后填)都失败,因为它们把符号决策与几何采样割裂在两个阶段。出路是让采样器成为规划的一部分。
- §3 核心思想:Stream 是采样器与符号搜索之间的接口——过程组件(条件生成器)站在连续世界产值,声明组件(认证事实)站在离散世界把值翻译成符号命题;Stream 是同时踩在鸿沟两岸的桥墩。
- §4 形式化:Stream 声明的四要素(inputs/domain/outputs/certified)+ 用 yield 实现条件生成器;generator/test/fluent 三类 Stream 覆盖几何过程的三种角色。
- §5 求解机制:核心是"归约为一系列有限 PDDL 问题";optimistic 乐观对象破解鸡生蛋循环(让搜索指导采样)、certified/level 管理乐观的真实性;Incremental/Focused/Adaptive 三算法是"采样盲目程度"光谱上的三点;§5.5 给出概率完备性(完备性完全依赖采样器、因子化、零测度子流形);§5.6 补外部成本、懒惰细化与代价-速度旋钮。
- §6 完整案例:pick-and-place 全流程,把 Stream 定义与求解机制串成可运行代码;§6.6 用一条运行轨迹展示 Adaptive 的"逐步加深、沿链采样、失败局部修复";§6.7 用 80 行纯 Python 跑通 Incremental,亲眼看见采样-搜索循环与盲目全采。
- §7 设计模式与反模式:沿因子切分、test 用在判定、采样器采得准、cost 引导四模式 + 反模式总览;§7.6 用"倒水"任务走完为新领域设计 Stream 的六步工作流。
- §8 工程实践:pddlstream 库(依赖 FastDownward);累积项目用 PDDLStream 替换 T1 的 Plan-then-Check(封装而非重写几何工具)。
- §9 真实应用:切黄瓜烹饪 TAMP(PDDLStream + MoveIt Task Constructor + RL)、Kitchen Worlds 测试场、NAMO 移动操作(VANAMO);符号-几何耦合从桌面到房间尺度不变,PDDLStream 是协调中枢;§9.5 直面从仿真到真机的三道鸿沟。
- §10 横向对比与局限:与 LGP(采样 vs 优化)、与 FFRob/STRIPStream(三代演进);三类局限(长时域慢、接触弱、效率)及改进;§10.5 LLM 与 PDDLStream 的"辅助而非替代"。
一句话收束本章:PDDLStream 用 Stream 在符号-几何鸿沟上架桥,让符号蓝图与几何零件在采样-搜索循环里彼此咬合,拼出既符号合法又几何可行的完整计划。
术语速查表¶
| 术语 | 一句话定义 |
|---|---|
| Stream | 采样器与符号搜索之间的接口,有过程(生成器)和声明(认证)两组件 |
| 条件生成器 (conditional generator) | Stream 的过程组件,从输入值产出依赖于输入的输出值序列(yield) |
| 认证事实 (certified fact) | Stream 的声明组件,规定产出值满足哪些符号事实(对搜索的承诺) |
| 静态事实 vs 流式状态 | Stream 认证的几何关系(永真)vs 动作改变的状态(会变) |
| generator/test/fluent stream | 产值型 / 测试型 / 依赖状态型,三类 Stream |
| optimistic 对象 | 代表"某 Stream 将来能产出的值"的占位符,破解采样-搜索鸡生蛋 |
| 乐观计划 (optimistic plan) | 用乐观对象搜出的计划骨架,指明需采样哪些值,待验证 |
| level | 乐观对象的依赖深度(抓取 1→IK 2→运动 3),控制展开深度 |
| 归约为有限 PDDL | PDDLStream 把无限问题变成一串逐渐变大的有限 PDDL 问题 |
| Incremental | 不用乐观,盲目采样-搜索循环,简单但紧约束慢 |
| Focused | 用乐观引导,只采骨架需要的值,紧约束快 |
| Adaptive | 动态平衡探索(新骨架)与利用(采样落地),论文主推 |
| 概率完备 | 解存在则采样数→∞ 时找到概率→1;完备性依赖采样器 |
| factored transition system | 因子化转移系统;约束只影响少数变量,使采样可分解 |
| 零测度子流形 | TAMP 可行解常落在的低维曲面(如 IK 解流形),随机采样命中概率为零 |
| 外部成本函数 | 把几何代价(如路径长)接入符号搜索目标的机制 |
| 懒惰展开 (lazy) | 推迟昂贵采样/计算到必需时,换求解速度 |
| Stream 设计六步 | 识别约束→判类型→排依赖链→采得准→认证真→查反模式 |
| NAMO / VANAMO | 可移动障碍导航 / 可见性感知版;搬开障碍才能通行,符号-几何耦合的房间尺度体现 |
知识点总表¶
| # | 知识点 | 核心要点 | 对应节 | 难度 |
|---|---|---|---|---|
| 1 | 连续参数难题 | 参数耦合,不能孤立选,朴素解都失败 | §2.1-2.3 | ⭐⭐⭐ |
| 2 | Stream 是接口 | 封装几何能力,采样器↔符号搜索的桥 | §3.1 | ⭐⭐⭐ |
| 3 | Stream 两组件 | 过程(生成器,连续)+ 声明(认证,离散) | §3.2 | ⭐⭐⭐ |
| 4 | Stream 依赖链 | 抓取→IK→运动,编码参数耦合 | §3.4 | ⭐⭐⭐ |
| 5 | Stream 声明四要素 | inputs/domain/outputs/certified | §4.1-4.2 | ⭐⭐⭐ |
| 6 | 条件生成器实现 | yield 按需产出,认证须名副其实 | §4.3 | ⭐⭐⭐ |
| 7 | 三类 Stream | generator/test/fluent | §4.4 | ⭐⭐⭐ |
| 8 | 归约为有限 PDDL | 采样把无限变有限,类比 PRM | §5.1 | ⭐⭐⭐⭐ |
| 9 | optimistic 对象 | 假装值已有,让搜索指导采样 | §5.2 | ⭐⭐⭐⭐ |
| 10 | certified/level | 管理乐观真实性,控制展开深度 | §5.3 | ⭐⭐⭐⭐ |
| 11 | 三种算法 | Incremental/Focused/Adaptive,采样盲目程度光谱 | §5.4 | ⭐⭐⭐⭐ |
| 12 | 概率完备性 | 完备性完全依赖采样器;因子化、零测度子流形 | §5.5 | ⭐⭐⭐⭐ |
| 13 | 外部成本/懒惰 | 几何代价入符号搜索;推迟昂贵计算;代价-速度旋钮 | §5.6 | ⭐⭐⭐⭐ |
| 14 | 完整求解流 | 乐观骨架→采样验证→落地;运行轨迹局部修复 | §6.5-6.6 | ⭐⭐⭐ |
| 15 | Stream 设计模式 | 因子切分/test判定/采得准/cost引导;六步工作流 | §7 | ⭐⭐⭐ |
| 16 | 最小实现 | 80行纯Python跑通Incremental采样-搜索循环 | §6.7 | ⭐⭐⭐ |
| 17 | 接入 Mini-TAMP | 封装几何工具为 Stream,替换 Plan-then-Check | §8.2 | ⭐⭐⭐ |
| 18 | 真实应用 | 烹饪 TAMP、Kitchen Worlds、NAMO 移动操作 | §9 | ⭐⭐⭐ |
| 19 | 尺度无关通用性 | 桌面/厨房/导航同源耦合,Stream 是通用钥匙 | §9.3 | ⭐⭐⭐ |
| 20 | 流式 vs 优化式 | 采样 vs 优化,组合摆放 vs 接触丰富 | §10.1 | ⭐⭐⭐ |
| 21 | 三代演进与局限 | FFRob→STRIPStream→PDDLStream;长时域/接触/效率 | §10.2-10.3 | ⭐⭐⭐ |
| 22 | LLM 与 PDDLStream | 辅助而非替代;外层生成/中层先验/内核不成熟 | §10.5 | ⭐⭐⭐ |
延伸阅读¶
核心论文(PDDLStream,§3-§6): - Garrett, Lozano-Pérez & Kaelbling (2020), "PDDLStream: Integrating Symbolic Planners and Blackbox Samplers via Optimistic Adaptive Planning," ICAPS(arXiv:1802.08705). ⭐⭐⭐⭐ —— 本章主参考,Stream、optimistic、三算法的原始出处。 - Garrett, Lozano-Pérez & Kaelbling (2018), "Sampling-based Methods for Factored Task and Motion Planning," IJRR(arXiv:1801.00680). ⭐⭐⭐⭐ —— §5.5 理论基础:概率完备性、因子化转移系统、零测度子流形、与 PRM/FFRob 的形式对应。 - Garrett et al. (2021), "Integrated Task and Motion Planning," Annual Review of Control, Robotics, and Autonomous Systems. ⭐⭐⭐ —— TAMP 综述,PDDLStream 在 TAMP 集成范式中的定位。
三代演进(§10.2): - Garrett, Lozano-Pérez & Kaelbling (2018), "STRIPStream," 与 FFRob (2014/2017)。⭐⭐⭐ —— PDDLStream 的前两代,理解演进脉络。
前置与背景: - McDermott et al. (1998), PDDL 原始规范。⭐⭐ —— Stream 声明语法所基于的 PDDL。 - TAMP_T1 §7(PDDLStream 入门)、§4(采样运动规划 RRT/PRM,含 Lazy-PRM)。⭐⭐ —— 本章的直接前置。
前沿延伸(学习/LLM 引导,§10.3、§10.5): - Khodeir, Agro & Shkurti (2021), "Learning to Search in Task and Motion Planning with Streams"(arXiv:2111.13144). ⭐⭐⭐⭐ —— 针对 PDDLStream 乐观规划中对象集合被穷举式广度优先扩展、导致长时域多对象推理耗时的问题,提出用图神经网络从过往搜索中学习、以最佳优先方式扩展对象与事实集合的几何信息符号规划器。接 TAMP_T7。 - Mendez-Mendez (2025), "A Systematic Study of LLMs for Task and Motion Planning With PDDLStream"(arXiv:2510.00182). ⭐⭐⭐ —— §10.5 主参考,用 4950 个问题系统评测 LLM 替代 PDDLStream 组件,结论是目前更差,印证"辅助而非替代"。 - Muguira-Iturralde, Curtis, Du, Kaelbling & Lozano-Pérez, "Visibility-Aware Navigation Among Movable Obstacles (VANAMO)"(arXiv:2212.02671). ⭐⭐⭐ —— §9.3 移动操作/NAMO 的代表,PDDLStream 同源团队把符号-几何耦合推向部分可观测的房间尺度。 - COAST (2024, arXiv:2405.08572). ⭐⭐⭐ —— §10.3 提到的改进工作,plan-first、比 PDDLStream 快一个数量级。
工程实现: - pddlstream 开源库(github.com/caelan/pddlstream)+ FastDownward 文档。⭐⭐⭐ —— §8 的实现基础。 - Kitchen Worlds(§9.2):基于 PDDLStream 的长时域 TAMP 问题库与范例。⭐⭐⭐
本章与后续章节的关系¶
| 后续章节 | 本章哪部分为它铺垫 | 它如何深化/对比本章 |
|---|---|---|
| T4 LGP | §10.1 流式 vs 优化式的对比、§8 的"采样 vs 优化"分野 | 详解优化式范式(KOMO 联立优化),与本章流式范式互补;处理本章弱项的接触密集操作 |
| T5 行为树 | §8 累积项目(替换协调器)、§9.5 鸿沟三(执行闭环) | 用行为树替换协调器,加执行监控与重规划;闭环修正感知/执行偏差 |
| T6 不确定性 TAMP | §5.2 的 optimistic/采样思想、§9.5 鸿沟一(感知噪声) | 信念空间下的采样式 TAMP,把几何不确定性建进规划 |
| T7 大模型规划 | §5.1 归约、§10.5 LLM 辅助、延伸阅读的学习引导搜索 | LLM 生成 PDDL/Stream、学习引导采样与对象展开 |
| T8 多机 TAMP | §8 接 Mini-TAMP、TAMP_T2 分配 | 多机的分配-规划-几何联合 |
| T9 综合实战 | §7 设计工作流、§9 协调中枢定位 | 把 PDDLStream 与 LLM/行为树/优化技能整合成完整系统 |
🔧 故障排查手册¶
| 症状 | 可能原因 | 排查步骤 | 相关节 |
|---|---|---|---|
| PDDLStream 无限采样不终止 | 目标几何上不可行(如架子根本够不到),乐观计划反复落空 | 1. 单独测 IK/运动 Stream 能否对该目标产出 2. 设采样上限 3. 确认目标几何可达 | §5.3, §6 练习1 |
| 求出的计划执行时碰撞/IK 失败 | 某 Stream 认证了未真正保证的事实 | 1. 检查每个生成器是否真正满足 certified(碰撞真检了吗)2. 对照 §4.3 Step3 错误3 | §4 陷阱 4-2 |
| 紧约束/多对象问题慢到不可用 | 用了 Incremental(盲目采样) | 1. 切换到 Focused/Adaptive 2. 检查 Stream 依赖链是否合理 3. 看采样次数统计 | §5.4 陷阱 5-3 |
| 乐观搜索找不到任何计划骨架 | level 展开不够深,乐观对象没覆盖完整参数链 | 1. 确认 level 至少到运动 Stream 层 2. 检查 Stream 依赖链是否断裂(某 domain 事实无 Stream 认证) | §5.3, §6 练习2 |
| Stream 从不被调用 | 输入前提(:domain)从未满足,或谓词名不匹配 | 1. 检查 :domain 谓词是否由前序 Stream/init 提供 2. 检查谓词名拼写一致 3. 确认依赖链 | §4.1, §3.4 |
| 计划里参数是占位符而非真实值 | 乐观计划未经验证就被当结果 | 1. 确认走完"乐观→真实"升级 2. 检查算法是否正常终止于真实计划 | §5.2-5.3 陷阱 5-2 |
| 加新几何约束后建模混乱 | 静态事实与流式状态混淆 | 1. 区分该谓词是几何关系(Stream 静态)还是状态(动作流式)2. 对照 §4.2 | §4 陷阱 4-3 |
| 接入 Mini-TAMP 后行为异常 | 重写了几何工具而非封装,新旧不一致 | 1. 确认生成器直接调 T1 已有方法 2. 不要重新实现 IK/运动 | §8 陷阱 8-1 |
| 求解极慢,简单问题也慢 | 用了最优(最不懒)配置 | 1. 改最懒+Adaptive 先求可行 2. 用单位代价 3. 确认 §5.6 旋钮配置 | §5.6 陷阱 5-7 |
| 找到的计划绕路/抓得不稳 | 没用外部成本,搜索无质量信号 | 1. 给 move 加路径长代价 2. 降低懒惰度比较代价 3. 给抓取赋稳定性代价 | §5.6, §7.4 |
| 采样器盲目、下游大量失败 | 采样未偏向可行区域 | 1. 给采样器加粗筛(如可达性预筛)2. 偏置到合理范围(§7.6 倒水例) | §7.3 |
| 新领域 Stream 设计无从下手 | 没有系统流程 | 1. 按 §7.6 六步:识别约束→判类型→排链→采得准→认证真→查反模式 | §7.6 |
| 长时域多对象时极慢 | 乐观对象广度优先盲目展开 | 1. 减少无关对象 2. 关注学习引导展开(GNN,§10.3)3. 考虑 COAST 等改进 | §10.3 |
API 速查表¶
| API / 概念 | 用法 | 说明 | 对应节 |
|---|---|---|---|
:stream 声明 |
(:stream name :inputs ... :domain ... :outputs ... :certified ...) |
Stream 的声明组件,四要素 | §4.1-4.2 |
| 条件生成器 | def gen(in1, in2): ... yield (out,) |
Stream 的过程组件,yield 按需产出 | §4.3 |
| test stream | :outputs () :certified (Pred ...) + yield () 或不产出 |
测试型 Stream(碰撞、可见性) | §4.4 |
| stream_map | {"stream-name": generator_fn, ...} |
把声明名映射到生成器 | §6.3 |
pddlstream ... solve |
solve(problem, algorithm="adaptive") |
求解;algorithm: incremental/focused/adaptive | §5.4, §6.4 |
| problem 元组 | (domain_pddl, constant_map, stream_pddl, stream_map, init, goal) |
PDDLStream 问题的标准构成 | §6.4 |
| 存在量词目标 | ("exists", ("?p",), ("and", ...)) |
目标含未定连续参数时用 | §6.4 练习3 |
| optimistic 对象 | (框架内部)代表将来可产出的值的占位符 | 破解采样-搜索鸡生蛋 | §5.2 |
| FastDownward | PDDLStream 的底层经典规划器(需编译) | 解每个有限 PDDL 子问题 | §5.1, §8.1 |
:fluents |
:fluents (AtPose ?obj ?p) |
fluent stream 声明依赖的流式状态 | §4.4 |
:function 外部成本 |
(:function (MoveCost ?q1 ?q2 ?tau) ...) |
把几何代价接入符号搜索目标 | §5.6 |
| 搜索懒惰度 | planner= 从最不懒(UCS,最优)到最懒(最快) |
代价-速度权衡旋钮 | §5.6 |
| 单位代价 | solve(..., unit_costs=True) |
不比较代价,求解更快(牺牲最优) | §5.6, §6.4 |
研究实践建议¶
学习 PDDLStream 有一条清晰的能力进阶路线,从"会用"到"会扩展"再到"会研究",每一阶段对应本章不同部分。
第一阶段——理解机制(对应 §3-§6):先把 §4 的 Stream 声明与生成器对应关系吃透,再手推 §6.5 的求解流程图、对照 §6.6 的运行轨迹——能在纸上模拟"乐观骨架→采样→落地"一遍,知道每一步在做什么。然后跑通 pddlstream 官方的 pick-and-place 例子,对照代码看 §6 的各组件。检验标准:能解释 §5.2 的乐观对象如何破解"采样-搜索鸡生蛋"。
第二阶段——自己设计 Stream(对应 §7):为一个新领域(如 §7.6 的倒水,或插销、开门)走一遍 §7.6 的六步工作流——识别约束、判 generator/test/fluent 类型(§4.4)、排依赖链、写"采得准"的采样器、确保认证名副其实、对照反模式检查。这是从"会改官方例子"到"能为新任务建领域"的关键跳。亲手体会 §5.3 的 level 如何影响能否找到计划、§7.3 的采样器质量如何影响速度。检验标准:你设计的领域能让 PDDLStream 求出可执行计划,且你能诊断它跑不出解时的原因(§8.4 排查清单)。
第三阶段——真机落地(对应 §8-§9):把 PDDLStream 接到真实机器人(或高保真仿真),正视 §9.5 的三道鸿沟——感知噪声、误差余量、规划-执行节奏。配齐上下游:执行监控(行为树)、必要时的闭环修正。检验标准:理解"仿真跑通≠真机能用",知道每道鸿沟该靠哪一章的方法弥合。
第四阶段——研究前沿(对应 §10):PDDLStream 的瓶颈在多对象长时域时乐观对象的盲目展开(§10.3、Khodeir 2021)。可关注几个方向:用学习引导采样/对象展开(GNN 排序,接 T7)、与 LGP 优化结合处理接触密集操作(接 T4)、不确定几何下的流式 TAMP(接 T6)、改进搜索策略以加速长链 stream 计划(如 COAST,§10.3 的 1000s→10s)、把符号-几何耦合推向部分可观测的移动操作(VANAMO,§9.3)。§10.5 的 LLM×PDDLStream 也是活跃方向,但记住当前共识是"辅助而非替代"(Mendez-Mendez 2025)。这些都建立在本章对 optimistic 采样机制(§5)和概率完备性(§5.5)的理解之上——不懂"完备性依赖采样器",就无法判断一个改进是在改进采样器、搜索、还是建模。
下一章预告:TAMP_T4《逻辑-几何规划 LGP》将讲缝合符号-几何鸿沟的另一条路——与本章"采样调用"相对的"联立优化"。LGP 把动作骨架选择与轨迹优化写进一个混合优化问题(KOMO),用连续优化处理本章用采样处理的几何,特别擅长本章 §10.1 说的"接触丰富"操作。读完 T4,你就掌握了 TAMP 集成的两大范式,能按问题特性(选择型 vs 调优型约束)正确选型。