跳转至

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. 本章目标

学完本章后,你应当能够:

  1. 解释 TAMP_T1 §7 当黑盒用的 PDDLStream 内部如何工作:Stream 是什么、它的过程性组件(条件生成器)与声明性组件(认证事实)各自的角色。
  2. 编写一个 Stream 声明:定义它的输入、输出、所需的领域事实(domain facts)和认证的事实(certified facts),并实现对应的条件生成器(如 IK 采样器、放置位姿采样器)。
  3. 描述 PDDLStream 如何把一个含连续参数的规划问题,归约为"一系列有限 PDDL 问题"——理解 optimistic 对象、certified facts、level 这三个核心概念。
  4. 对比 PDDLStream 的三种算法——Incremental、Focused、Adaptive——各自如何平衡"采样"与"搜索",以及它们的适用场景。
  5. 诊断 Stream 设计中的常见问题:采样器效率、optimistic 计划无法落地、紧约束下的组合爆炸。
  6. PDDLStream 接到 TAMP_T1 的 Mini-TAMP 累积项目上,替换原来朴素的 Plan-then-Check 协调器,并理解它相对 LGP(下一章)的取舍。
  7. 判断何时该用 PDDLStream(vs 纯 PDDL、纯运动规划),并说清 PDDLStream 的理论保证(概率完备性)依赖什么、为什么 TAMP 的解常落在零测度子流形上。
  8. 应用 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,让符号规划器从中选。

朴素离散化:
  预生成 100 个候选抓取位姿 g_1...g_100
  预生成 100 个候选放置位置 p_1...p_100
  把它们当 PDDL 对象, 让 FF 从 100×100 组合里选

为什么不行?

  • 粒度两难:离散化粗了(如 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 已经点明方向:让几何采样器成为符号规划过程的一部分,在规划过程中按需生成参数,并把"这个参数满足某约束"作为信息反馈给符号搜索。

具体地说,我们需要一种机制,它能:

  1. 按需生成连续参数——不是预先盲目生成(朴素解一),也不是事后盲目填(朴素解二),而是在符号搜索需要时,针对性地生成。
  2. 把几何过程当黑盒——IK 求解器、碰撞检测、运动规划器已经是成熟工具,机制应该直接调用它们,而不是重新发明。
  3. 把生成的结果翻译成符号事实——生成了一个可达的抓取位姿后,要以符号规划器能理解的形式告诉它"位姿 \(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 机制)。

练习

  1. (⭐⭐) 对 §2.1 的 pick-and-place,估算朴素离散化的组合规模:若抓取位姿、放置位姿各离散化为 \(N\) 个候选,IK 各有若干解,符号规划器面对的"对象 + 组合"规模大致是多少?说明为什么 \(N\) 增大时这不可行。
  2. (⭐⭐⭐) 用自己的话解释:为什么 §2.1 的连续参数是"耦合"的?举一个具体的耦合链——某个抓取位姿的选择如何导致后续放置无解。
  3. (⭐⭐) §2.4 列出了"让采样器成为规划一部分"需要的三点能力。对照 TAMP_T2 §9.4 的策略二,说明 PDDLStream 相比"在分配代价里嵌入一次规划器"更进了一步在哪里。
  4. (⭐⭐⭐,判断) 用 §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),由求解器控制取多少。

练习

  1. (⭐⭐) 为"倒水"任务设计一个新 Stream:机器人要把杯子里的水倒进碗里,需要一个"倒水位姿"采样器(杯子相对碗的倾倒位姿)。写出这个 Stream 的输入、输出、认证的事实。
  2. (⭐⭐⭐) §3.4 的 Stream 依赖链是 抓取 → IK → 运动。如果再加一个"放置 → IK → 运动"的链(放置也要 IK 和运动),画出完整的 Stream 依赖图,标出哪些 Stream 的输出是哪些 Stream 的输入。
  3. (⭐⭐) 区分静态事实与流式事实:对 §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 认证的静态事实与动作的流式状态。 - 错误描述:把几何关系(KinGrasp)当成会被动作改变的状态,或反之。 - 现象/后果:domain 建模错误——把静态事实放进动作效果去 add/delete,或把动态状态当成 Stream 认证。 - 根本原因:静态事实(几何关系,永真)和流式状态(随动作变)是两类不同的命题。 - 正确做法:Stream 只认证静态几何关系((Kin p g q) 永远成立);动作只增删流式状态(HandEmptyAtConf)。建模时先分清每个谓词属于哪类。

练习

  1. (⭐⭐,声明) 为 §3 练习 1 的"倒水"Stream 写出完整的 PDDLStream 声明(:inputs/:domain/:outputs/:certified),并说明它依赖哪些前序 Stream 的输出。
  2. (⭐⭐⭐,实现) 实现 §4.4 的 test-cfree 碰撞测试 Stream 的生成器:输入构型 q 和位姿 p,无碰撞则 yield (),碰撞则不产出。用一个简化的碰撞检测(如球体距离)。
  3. (⭐⭐⭐,调试) 给定一个 IK 生成器,它偶尔 yield 出超出关节限位的构型。指出这违反了哪个认证事实,会导致什么后果,如何修复(对照 §4.3 Step 3)。
  4. (⭐⭐⭐⭐,设计) 判断下列几何能力各应写成哪类 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(乐观引导,只采需要的)。

练习

  1. (⭐⭐⭐) 用自己的话解释"归约为一系列有限 PDDL 问题":为什么任一时刻的已采样值是有限的?为什么解出的有限问题(且值真实)就是可行计划?
  2. (⭐⭐⭐) 对 §2.1 的 pick-and-place,列出各 Stream 的乐观对象及其 level(参照 §5.3 的分层)。为什么 level 必须至少到运动 Stream 的层级,乐观搜索才能找到完整计划骨架?
  3. (⭐⭐⭐⭐) 对比 Incremental 和 Focused 在"桌上 10 个物体、只有 1 种可行抓放组合"这个紧约束问题上的行为:各自会采样多少、为什么 Focused 快得多?
  4. (⭐⭐⭐⭐,跨节综合) 把 §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 能否对那个高架子产出构型),再怀疑算法。

练习

  1. (⭐⭐⭐) 用自己的话解释"完备性完全依赖于条件采样器":举一个 Stream 设计不当导致 PDDLStream 找不到本存在的解的例子。
  2. (⭐⭐⭐⭐) 为什么 TAMP 的可行解常落在零测度子流形上?以 Kin(p, g, q) 为例说明,并解释为什么这使得"在构型空间随机采样再检查"几乎必然失败、而 IK 采样器能解决。
  3. (⭐⭐⭐,跨章) 对照 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 实战建议)。

练习

  1. (⭐⭐⭐) 解释外部成本函数如何让"几何代价进入符号搜索":以 move 的路径长度为例,说明没有它时符号搜索为什么无法偏好短路径计划。
  2. (⭐⭐⭐,跨章) 把 §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 的调用顺序和耦合方式不同。

练习

  1. (⭐⭐⭐) 对照 §5.7 的两段伪代码,指出 Incremental 和 Optimistic 各自的 SEARCH 调用扮演什么角色(终止检查 vs 需求生成器)。为什么这个角色差异导致 Optimistic 在昂贵 stream 上更省?
  2. (⭐⭐⭐⭐) §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.09.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-basesample-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 动作时,只写流式状态前提(AtPoseHandEmpty),漏掉几何谓词(KinGrasp)。 - 现象/后果:符号规划器会认为"任何时候都能 pick",规划出几何上不可行的计划(抓一个够不到的位姿),且 Stream 根本不被触发(没有谓词要求它产值)。 - 根本原因:动作前提是符号层"看见几何约束"的唯一通道(§4.2)。漏写几何谓词,等于切断了符号-几何的联系,退回纯符号规划。 - 正确做法:每个有几何约束的动作,前提里必须包含对应的 Stream 认证谓词(pick 要有 KinGrasp)——对照 §6.2 的 domain 检查。

陷阱 6-2(概念误区):以为目标必须指定所有连续参数的具体值。 - 错误描述:写 goal 时纠结"杯子到底放架子哪个位置",试图填一个具体坐标。 - 现象/后果:写死的位置可能不可达/会碰撞,导致无解;或限制了求解器本可找到的更优放置。 - 根本原因:连续参数的具体值应由 Stream 采样供给,目标只需表达"要达到什么效果"(§6.4 的存在量词目标)。 - 正确做法:目标用存在量词(exists ?p, AtPose(cup,?p) ∧ Supported(cup,?p,shelf)),把"具体放哪"留给 sample-place——能不指定的连续值就不指定(§6.4 本质洞察)。

练习

  1. (⭐⭐) 运行思路推演:对 §6.4 的问题,若 shelf 太高、所有 IK 采样都无解,PDDLStream 会怎么表现?它会无限采样还是会终止?(提示:结合 §5 的乐观计划落空与算法行为。)
  2. (⭐⭐⭐) 给 §6.2 的 domain 加一个新约束:放置时杯子不能碰到架子上已有的物体。需要新增哪个 Stream(test 型还是 generator 型)、在 place 前提里加什么谓词?
  3. (⭐⭐⭐) §6.4 的 goal 用了存在量词 exists ?p。解释为什么放置位姿 ?p 用存在量词而非具体值——这与 Stream 的"按需采样"如何配合?
  4. (⭐⭐⭐⭐) 把 §6 的 pick-and-place 扩展为"把 cup 和 book 都收拾好"(两个物体)。讨论对象数翻倍后,Incremental 与 Adaptive 算法的表现差异(结合 §5.4)。
  5. (⭐⭐⭐,追踪) 对照 §6.6 的运行轨迹,若在"迭代 3-细化"阶段 sample-place 连续 5 次都配不出可行 IK,Adaptive 算法在什么时候应该从"利用"(换放置)转为"探索"(换骨架)?这个权衡由什么决定(§5.4)?
  6. (⭐⭐⭐⭐,对照追踪) 仿照 §6.6 的 Adaptive 运行轨迹,为 Incremental 算法写一条同问题的运行轨迹(提示:Incremental 不用乐观对象,每轮盲目地为所有 Stream 各采一批值再搜索)。对比两条轨迹,具体指出 Incremental 在哪些步骤上做了"白采"(采了最终没用上的值),从而直观理解为什么紧约束问题上 Adaptive 比 Incremental 快。
  7. (⭐⭐⭐,动手) 运行 §6.7 的 80 行最小实现,然后做三个修改实验:(a) 把 REACH 改成 5.0,观察计划是否变短(单站位能否够更远);(b) 把中转位姿 [2.5, 5.0, 7.5] 删到只剩 [5.0],看是否仍可解;(c) 把目标改成 12.0(超出所有站位范围),观察算法如何"采到无新事实可采"而终止——这正是 §5.5 说的"采样器覆盖不全则找不到解"。
  8. (⭐⭐⭐⭐,改造) 把 §6.7 的最小实现从 Incremental 改成"惰性"版本:不再每轮把所有 stream 全采,而是先只采 sample-posesample-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 采得准)。 - 正确做法:先让采样器"采得准"(偏向可行区域、粗筛),再调算法。地基不牢,算法调度无用。

练习

  1. (⭐⭐) 给定一个把"采抓取 + 解IK"合在一起的 Stream,把它按因子结构拆成两个 Stream,写出拆分后的声明,并说明拆分带来的三个好处(命中率/失败局部化/复用)。
  2. (⭐⭐⭐) 对"放置时不能碰到架子上已有物体"这个约束,判断该写成 generator 内置还是 test stream,说明理由(结合该约束是否依赖运行时场景状态)。
  3. (⭐⭐⭐) 为 §7.3 的 sample_place_smart 设计一个"粗略可达性预筛"rough_reachable:用什么便宜的检查(不解完整 IK)来快速排除明显够不到的放置点?讨论预筛的"宁可放过不可错杀"原则(不能筛掉可行点)。
  4. (⭐⭐⭐⭐,综合) 结合 §5.5 和 §7.1-§7.3:解释"完备性依赖采样器"如何同时要求采样器既完整(不漏可行值,否则破坏完备性)又高效(偏向可行区域,否则慢)。这两个要求会冲突吗?如何平衡?
  5. (⭐⭐⭐⭐,工作流实践) 用 §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 解决的是组合复杂、几何紧约束的问题;简单问题用什么都行。

练习

  1. (⭐⭐) 把 §7.2 的 PDDLStreamCoordinator 接入 TAMP_T1 §11 的 Mini-TAMP 环境,跑通单物体 pick-and-place,对比它与原 Plan-then-Check 协调器的输出。
  2. (⭐⭐⭐) 构造一个"紧约束"场景(物体被部分遮挡、放置区域狭小),对比 PDDLStream 与 Plan-then-Check 的求解时间,验证 §7.2 末尾的论断。
  3. (⭐⭐⭐) 把 §7.2 的 algorithm 在 incremental/focused/adaptive 间切换,在同一问题上对比求解时间与采样次数,印证 §5.4 的算法差异。
  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 到产品的关键认知。

练习

  1. (⭐⭐) §9.1 的切黄瓜任务把"切片"外包给 RL 而非用 PDDLStream 直接规划。结合 §10(流式 vs 优化式),解释为什么切片不适合 PDDLStream 的采样范式。
  2. (⭐⭐⭐) 下载或浏览 Kitchen Worlds 的一个 domain/stream 范例,对照 §7 的设计模式,找出它用了哪些模式(因子切分、test stream、采样器偏置)。
  3. (⭐⭐⭐) 画出一个"用语音命令让机器人做沙拉"的完整系统数据流,标出 PDDLStream 在其中的位置,以及它上下游分别接什么(呼应 §9.4 与 TAMP_T0 §3.8)。
  4. (⭐⭐⭐,真机) §9.5 列了从仿真到真机的三道鸿沟。对"机械臂按规划的抓取位姿去抓杯子,但杯子实际位置有 2cm 感知误差"这个情形,分别说明三种缓解手段(抓取留容差/执行闭环修正/不确定性 TAMP)各如何应对,以及它们分别接本线哪一章。
  5. (⭐⭐⭐⭐,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 显示更差) 失去可验证性与完备性

练习

  1. (⭐⭐) 对下列任务各判断该用 PDDLStream 还是 LGP,说明理由:(a) 把散落的 8 个积木摆到指定货架;(b) 把一杯水倒进窄口瓶;(c) 把插头插进插座。
  2. (⭐⭐⭐) §10.2 的三代演进中,PDDLStream 相比 STRIPStream 的主要改进是"遵循 PDDL 约定"。为什么这个看似工程性的改进很重要?(提示:能复用 FastDownward 等成熟 PDDL 规划器,§5.1。)
  3. (⭐⭐⭐) §10.3 局限一说乐观对象"广度优先盲目展开"。结合 §5.2-§5.3,解释为什么物体多、计划长时这会爆炸,以及"最佳优先 + GNN 排序"如何缓解。
  4. (⭐⭐⭐⭐,跨章综合) 把本节"采样 vs 优化"的分野,与 MPPI 线"采样式 MPC vs 梯度式 MPC"对照——两组对比反映的是否是同一种更深的权衡?用 3-4 句话阐述。
  5. (⭐⭐⭐,前沿) §10.5 把 LLM 与 PDDLStream 的结合分三层(外层/中层/内核)。为什么"内核替代"目前最不成熟?结合 §5.5 的概率完备性说明 LLM 缺什么保证。
  6. (⭐⭐⭐⭐,定量分析) §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 调优型约束)正确选型。