TFD算法详解:STN(Simple Task Network)规划的核心方法

2026-08-28 Updated 2026-08-28 34 min read 1 views

系列第四篇,深入解析全序向前分解算法。如果你还没读过前面的文章,建议先从[任务规划概述]开始。


一、开篇:从STN到TFD

在上一篇文章中,我们讨论了STN与HTN的核心区别:STN仅支持顺序约束和变量绑定约束,而HTN额外支持状态维持约束。这个差异直接导致了两者在算法层面的不同——STN使用TFD算法,而HTN需要更复杂的偏序规划(POP)

那么,TFD算法到底是什么?为什么它能成为航天任务规划的主流方法?本文将从算法思想到代码实现,全面解析TFD。


二、TFD是什么?

2.1 名称解析

TFD 全称 Total-order Forward Decomposition,翻译为"全序向前分解"。这三个词分别对应算法的三个核心特征:

术语 含义
Total-order(全序) 任务网络中的所有任务始终保持完全确定的执行顺序
Forward(向前) 按照执行顺序,从第一个任务开始依次处理
Decomposition(分解) 用方法(Method)将复合任务展开为子任务

2.2 核心思想

TFD的核心理念可以用一句话概括:

在分解任务的每一步,我们都确切知道"世界当前是什么状态"。

这句话听起来简单,但它带来的工程价值是巨大的:

  • 数值计算可行:知道当前状态,就能精确计算"剩余电量""剩余时间"
  • 约束传播简单:不需要复杂的约束满足(CSP)技术
  • 搜索空间可控:全序使得搜索树相对紧凑

2.3 与偏序规划(POP)的对比

为了更好理解TFD的特点,我们对比一下偏序规划:

特性 TFD(全序) POP(偏序)
任务顺序 完全确定 部分确定(有并行)
当前状态 已知 未知(需区间估计)
约束处理 简单 复杂(CSP)
搜索效率 较高 较低
表达能力 受限 更强

简单来说:TFD用表达能力的限制换取了计算效率的提升。这在工程实践中往往是值得的。


三、TFD算法核心流程

3.1 算法输入

TFD算法需要四个输入:

输入 符号 说明
初始状态 $s_0$ 世界状态的完整描述
目标任务网络 $tn$ 待分解的任务集合及其顺序关系
方法集合 $M$ 复合任务的分解规则
操作集合 $O$ 原子任务的具体执行方式

3.2 算法伪代码

下面是TFD的核心伪代码(参考Ghallab等人的《Automated Planning》):

function TFD(s, tn, M, O):
    输入: 当前状态 s, 任务网络 tn, 方法集合 M, 操作集合 O
    输出: 操作序列(解)或 fail

    1. if tn 为空:
    2.     return []  # 空解,成功

    3. # 选择第一个待处理任务(全序的关键!)
    4. t = tn[0]  # 第一个任务
    5. tn' = tn[1:]  # 剩余任务(列表切片)

    6. if t 是原子任务:
    7.     # 尝试所有可能的操作
    8.     for o in O:
    9.         if o.name == t.name:
   10.             if preconditions(o) 满足于 s:
   11.                 # 执行操作,更新状态
   12.                 s_new = apply(o, s)
   13.                 # 递归处理剩余任务
   14.                 result = TFD(s_new, tn', M, O)
   15.                 if result ≠ fail:
   16.                     return [o] + result
   17.     return fail  # 所有操作都不可行

   18. else:  # t 是复合任务
   19.     # 尝试所有可能的方法
   20.     for m in M:
   21.         if m.name == t.name:
   22.             if preconditions(m) 满足于 s:
   23.                 # 分解任务:用方法的子任务替换当前任务
   24.                 subtasks = substitute(m.subtasks, t.variables)
   25.                 # 将子任务插入到任务网络前端
   26.                 tn_new = subtasks + tn'
   27.                 # 递归处理(注意:状态不变!)
   28.                 result = TFD(s, tn_new, M, O)
   29.                 if result ≠ fail:
   30.                     return result
   31.     return fail  # 所有方法都不可行

3.3 关键步骤解读

第4行:选择第一个任务

这是TFD"全序"的核心体现。我们始终选择任务网络中的第一个任务进行处理,这意味着任务的执行顺序在分解过程中就已经确定了。

第6-17行:原子任务处理

当遇到原子任务时,我们需要找到一个可执行的操作:
- 检查操作名称是否匹配
- 检查前提条件是否满足
- 执行操作,更新状态
- 递归处理剩余任务

第18-31行:复合任务处理

当遇到复合任务时,我们需要找到一个可行的方法:
- 检查方法名称是否匹配
- 检查前提条件是否满足
- 用方法的子任务替换当前任务
- 注意:状态不变! 分解只是"展开",还没执行

第14行 vs 第28行:状态变化的时机

这是一个重要的区别:
- 原子任务执行后,状态会改变
- 复合任务分解后,状态不变(只是任务网络变了)


四、一个完整示例

4.1 问题设定

考虑一个简化的卫星对地观测任务:

初始状态
- 卫星姿态:指向东
- 电量:100%

目标:拍摄地面目标A

4.2 领域定义

# 操作(Operators)
Operators:
  - Slew(target):
      名称: Slew
      参数: target(目标指向)
      前提: power > 10
      效果: pointing = target, power -= 10  # 消耗10单位电量

  - TakePhoto(target):
      名称: TakePhoto  
      参数: target(拍摄目标)
      前提: pointing == target, power > 5
      效果: has_image(target) = true, power -= 5  # 消耗5单位电量

# 方法(Methods)
Methods:
  - Observe(target):
      名称: Observe
      参数: target(观测目标)
      前提: true(无条件)
      子任务: [Slew(target), TakePhoto(target)]

4.3 TFD执行过程

让我们一步步追踪TFD算法的执行:

========================================
初始状态:
  s = {pointing=east, power=100}
  tn = [Observe(A)]

Step 1: 处理第一个任务 Observe(A)
----------------------------------------
  任务类型: 复合任务
  匹配方法: Observe(target)
    - 前提检查: true ✓
    - 变量绑定: target = A
    - 子任务: [Slew(A), TakePhoto(A)]
  分解结果: tn = [Slew(A), TakePhoto(A)]
  状态不变: s = {pointing=east, power=100}

Step 2: 处理第一个任务 Slew(A)
----------------------------------------
  任务类型: 原子任务
  匹配操作: Slew(target)
    - 前提检查: power=100 > 10 ✓
    - 执行效果: pointing=A, power=90
  状态更新: s = {pointing=A, power=90}
  剩余任务: tn = [TakePhoto(A)]

Step 3: 处理第一个任务 TakePhoto(A)
----------------------------------------
  任务类型: 原子任务
  匹配操作: TakePhoto(target)
    - 前提检查: 
      - pointing=A == target=A ✓
      - power=90 > 5 ✓
    - 执行效果: has_image(A)=true, power=85
  状态更新: s = {pointing=A, power=85, has_image(A)=true}
  剩余任务: tn = []

Step 4: 任务网络为空,成功!
----------------------------------------
  返回解: [Slew(A), TakePhoto(A)]
========================================

4.4 执行过程图解

                    初始
                      │
                      ▼
              [Observe(A)]
                      │
              方法分解 (状态不变)
                      │
                      ▼
        [Slew(A)] → [TakePhoto(A)]
              │             │
        执行操作         执行操作
        (状态改变)       (状态改变)
              │             │
              ▼             ▼
          pointing=A    has_image(A)

          解: [Slew(A), TakePhoto(A)]

五、TFD的搜索树与回溯

前面的例子只有一条路径成功,但实际情况往往更复杂。当某条路径失败时,TFD会回溯尝试其他选项。

5.1 回溯场景示例

假设我们有两个拍摄目标A和B,但电量有限:

初始状态: power=15
目标任务: [Observe(A), Observe(B)]

搜索过程:
  ├─ Observe(A) → [Slew(A), TakePhoto(A)]
  │    ├─ Slew(A) 执行成功, power=5
  │    ├─ TakePhoto(A) 前提失败 (power=5 不满足 >5)
  │    └─ 回溯:尝试其他方法?无其他方法
  │    └─ Observe(A) 失败
  │
  └─ 无其他选项,规划失败

这个例子说明了TFD的一个重要特点:它是一种搜索算法,会尝试所有可能的分解路径

5.2 搜索树结构

                    根节点
                   [Observe(A)]
                        │
            ┌───────────┴───────────┐
            │                       │
        方法1: Observe          方法2: ...(无)
            │
            ▼
    [Slew(A), TakePhoto(A)]
            │
      ┌─────┴─────┐
      │           │
   成功路径     失败路径
  (前提满足)  (前提不满足)

六、TFD的优缺点分析

6.1 优点

1. 状态确定性

这是TFD最大的工程优势。在分解的每一步,我们都精确知道当前状态:

# TFD可以精确计算
current_power = 100
after_slew = current_power - 10  # 确定的
after_photo = after_slew - 5     # 确定的

# 对比:偏序规划中状态是区间
current_power = [50, 100]  # 不确定范围
after_slew = [40, 90]       # 只能估计区间

2. 数值计算友好

航天任务规划中大量涉及数值计算:
- 轨道动力学
- 能源预算
- 数据存储容量
- 通信窗口

TFD的状态确定性使得这些计算可以直接嵌入规划过程。

3. 搜索效率较高

全序约束使得搜索空间相对紧凑,不需要处理复杂的偏序关系。

4. 工程可维护

算法逻辑清晰,代码实现简单,便于调试和验证。

6.2 缺点

1. 表达能力受限

无法表达"状态维持约束",例如:
- "在整个数据传输期间,天线必须始终指向地面站"
- "任务执行过程中,电量必须保持在20%以上"

2. 并行性受限

全序约束限制了任务的并行执行:

# HTN可以表达
任务A || 任务B  # A和B可以并行

# TFD只能表达
任务A → 任务B   # 或者
任务B → 任务A   # 必须有顺序

3. 完备性问题

某些问题在TFD下可能找不到解,但在偏序规划下有解。

6.3 工程中的折中

实际工程中,常用以下方法弥补TFD的短板:

问题 工程解决方案
状态维持约束 资源锁定机制
并行性受限 预定义的任务模板
数值约束 外部约束求解器

七、TFD的工程实现

7.1 SHOP/SHOP2

SHOP(Simple Hierarchical Ordered Planner)是最早实现TFD的规划器,由UMCP团队开发。

SHOP2是其后继版本,在TFD基础上增加了:
- 更灵活的变量绑定
- 外部函数调用
- 数值计算支持

;; SHOP2 领域定义示例
(defdomain satellite (
  ;; 操作
  (:operator (!slew ?target)
    :precondition (power ?p) (call > ?p 10)
    :effect (pointing ?target)
            (power (call - ?p 10)))

  ;; 方法
  (:method (observe ?target)
    :precondition ()
    :tasks (!slew ?target) (!take-photo ?target))
))

7.2 PyHOP:Python实现

PyHOP是一个简洁的Python HTN规划器,非常适合学习和实验:

# PyHOP 领域定义示例
def slew(state, target):
    """俯仰机动操作"""
    if state.power > 10:
        state.pointing = target
        state.power -= 10
        return state
    return False

def take_photo(state, target):
    """拍照操作"""
    if state.pointing == target and state.power > 5:
        state.has_image[target] = True
        state.power -= 5
        return state
    return False

def observe(state, target):
    """观测方法"""
    return [('slew', target), ('take_photo', target)]

# 注册
pyhop.declare_operators(slew, take_photo)
pyhop.declare_methods('observe', observe)

# 规划
state = State()
state.pointing = 'east'
state.power = 100
state.has_image = {}

plan = pyhop.plan(state, [('observe', 'A')])
# 输出: [('slew', 'A'), ('take_photo', 'A')]

7.3 实现要点

如果你要自己实现TFD,注意以下几点:

1. 任务表示

class Task:
    name: str          # 任务名称
    args: list         # 参数列表
    is_primitive: bool # 是否为原子任务

2. 状态表示

class State:
    fluents: dict      # 状态变量字典
    resources: dict    # 数值资源字典

3. 方法/操作表示

class Method:
    name: str
    preconditions: Callable  # 前提条件函数
    subtasks: list           # 子任务列表

class Operator:
    name: str
    preconditions: Callable  # 前提条件函数
    effects: Callable        # 效果函数

八、总结

8.1 核心要点

概念 要点
TFD本质 全序 + 向前 + 分解
状态确定性 分解过程中始终知道当前状态
算法流程 选任务 → 尝试操作/方法 → 递归
工程价值 数值计算友好、搜索高效、易于实现
表达限制 无状态维持约束、并行性受限

8.2 什么时候用TFD?

推荐使用TFD
- 任务之间有自然的顺序关系
- 需要精确的数值计算
- 对规划效率有较高要求
- 工程实现需要简洁可控

考虑其他方法
- 任务之间有复杂的并行关系
- 需要表达状态维持约束
- 对理论完备性有更高要求

8.3 与前面文章的关系

[任务规划概述] → [HTN形式化定义] → [STN与HTN区别] → [本文: TFD算法]
                                              ↓
                                        下一篇: HTN不可判定性

参考文献

  1. Ghallab, M., Nau, D., & Traverso, P. (2004). Automated Planning: Theory and Practice. Morgan Kaufmann. (Chapter 11)
  2. Nau, D. S., et al. (2003). SHOP2: An HTN planning system. Journal of Artificial Intelligence Research, 20, 379-404.
  3. Erol, K., Hendler, J., & Nau, D. S. (1996). Complexity results for HTN planning. Annals of Mathematics and Artificial Intelligence, 18(1), 69-93.