4. 直接法入门:直接打靶法(Direct Shooting)原理与实现,优缺点分析

好,咱们今天聊聊直接打靶法。说实话,这是我最喜欢的入门方法之一。为什么?因为它直观,好理解,而且上手快。你想想看,我们做轨迹优化,本质上就是在找一个控制序列,让飞行器从A点飞到B点,同时满足各种约束。直接打靶法就是最直接的想法——把控制量离散化,然后用数值积分去模拟飞行过程。

4.1 核心思想:把连续问题变成离散问题

直接打靶法的思路其实很简单。我们不去碰那些复杂的变分法、庞特里亚金极值原理,而是直接把时间轴切分成N段。每一段上,控制量用一个参数表示(比如常值、线性插值)。然后,我们用数值积分方法(比如龙格-库塔法)从初始状态开始往前推,一直推到终端。

说白了,就是把一个连续的最优控制问题,变成了一个有限维的参数优化问题。这个参数就是那些控制量离散点。优化器去调整这些参数,让终端状态尽可能接近目标,同时让某个性能指标(比如燃料消耗)最小。

核心公式:

给定初始状态 x₀,控制参数 u₁, u₂, ..., uₙ,通过积分得到终端状态 xₙ = F(x₀, u₁, ..., uₙ)。优化目标就是最小化 J = φ(xₙ) + Σ L(xₖ, uₖ),同时满足 xₙ = x_target 以及路径约束。

我在项目中遇到过不少新手,一上来就想用间接法,结果被协态变量和横截条件搞得晕头转向。我建议,如果你刚开始接触轨迹优化,直接从直接打靶法入手。它虽然效率不是最高的,但能帮你快速建立直觉。

4.2 实现步骤:手把手教你搭一个

好,咱们来看看具体怎么实现。我习惯用Python + SciPy来做演示,因为生态好,调试方便。

4.2.1 时间离散化

首先,把时间轴 [t₀, t_f] 分成 N 段。每段长度 Δt = (t_f - t₀)/N。控制量在每个时间节点上取值,段内可以用零阶保持(常值)或一阶保持(线性插值)。我个人习惯用零阶保持,简单,而且对于大多数工程问题已经够用了。

4.2.2 数值积分

从初始状态 x₀ 开始,用数值积分方法一步步往前推。最常用的是四阶龙格-库塔法(RK4)。

def rk4_step(f, x, u, dt):
    """单步RK4积分"""
    k1 = f(x, u)
    k2 = f(x + 0.5*dt*k1, u)
    k3 = f(x + 0.5*dt*k2, u)
    k4 = f(x + dt*k3, u)
    return x + (dt/6.0)*(k1 + 2*k2 + 2*k3 + k4)

def simulate(x0, u_seq, dt):
    """完整轨迹仿真"""
    x = x0
    traj = [x0]
    for u in u_seq:
        x = rk4_step(dynamics, x, u, dt)
        traj.append(x)
    return traj

4.2.3 构建优化问题

优化变量就是控制序列 u₁, u₂, ..., uₙ。目标函数可以是终端误差 + 控制能耗。约束包括终端状态约束和路径约束。

def objective(u_flat):
    """目标函数:终端误差 + 控制能耗"""
    u_seq = u_flat.reshape((N, m))
    traj = simulate(x0, u_seq, dt)
    xf = traj[-1]
    
    # 终端位置误差
    pos_err = np.linalg.norm(xf[:3] - x_target[:3])
    # 控制能耗
    control_cost = np.sum(np.linalg.norm(u_seq, axis=1)) * dt
    
    return pos_err + 0.1 * control_cost

def constraints(u_flat):
    """约束:终端速度匹配 + 路径约束"""
    u_seq = u_flat.reshape((N, m))
    traj = simulate(x0, u_seq, dt)
    xf = traj[-1]
    
    # 终端速度约束
    vel_err = xf[3:6] - x_target[3:6]
    
    # 路径约束(比如最大加速度)
    path_con = []
    for x in traj:
        path_con.append(x[6] - max_accel)  # 假设第7维是加速度
    
    return np.concatenate([vel_err, path_con])

4.2.4 调用优化器

用SciPy的 minimize 函数,选择 SLSQP 或 trust-constr 算法。

from scipy.optimize import minimize

# 初始猜测:全零控制
u0 = np.zeros(N * m)

# 边界:控制量上下限
bounds = [(-max_u, max_u)] * (N * m)

# 求解
result = minimize(objective, u0, method='SLSQP',
                  bounds=bounds, constraints={'type': 'eq', 'fun': constraints})

# 提取最优控制序列
u_opt = result.x.reshape((N, m))

小技巧:初始猜测很重要。我曾经试过随便给个全零猜测,结果优化器直接发散。建议先用一个简单的制导律(比如比例导引)生成一条参考轨迹,然后用这个轨迹的控制量作为初始猜测。收敛速度快很多。

4.3 优缺点分析:没有银弹

直接打靶法有它的优势,也有明显的短板。咱们客观分析一下。

4.3.1 优点

  • 实现简单:你不需要懂变分法、协态变量这些数学工具。只要有动力学模型和数值积分器,就能搭起来。
  • 变量少:优化变量只有控制量离散点,数量是 N×m(N是时间节点数,m是控制维度)。相比配点法,变量少得多。
  • 直观:每一步的物理意义都很清楚。你知道控制量在做什么,轨迹长什么样。
  • 容易调试:如果结果不对,你可以一步步检查:积分器对不对?约束写没写对?目标函数合理吗?

4.3.2 缺点

  • 对初始猜测敏感:这是最大的问题。如果初始猜测离最优解太远,优化器很容易陷入局部最优或者直接发散。
  • 数值稳定性差:对于长时域、强非线性系统,积分误差会累积。终端状态对控制量的敏感性可能变得非常差,导致梯度信息失真。
  • 难以处理路径约束:路径约束(比如最大过载、动压限制)需要在每个积分步上检查。如果约束很严格,优化器可能很难找到可行解。
  • 效率问题:每次迭代都要重新做一次完整的数值积分。如果N很大,计算量会非常可观。

避坑指南:我曾经在一个再入轨迹优化项目里用了直接打靶法。飞行时间300秒,我分了1000个节点。结果每次迭代都要跑1000步积分,优化器跑了200多次迭代才收敛。整整跑了两个小时。后来我换成多重打靶法,把问题拆成10段,每段单独积分,再加连续性约束。半小时就搞定了。

4.4 什么时候用直接打靶法?

根据我的经验,直接打靶法最适合以下场景:

  • 短时域问题:飞行时间在几秒到几十秒内,积分误差可控。
  • 弱非线性系统:动力学方程比较温和,不会出现混沌或剧烈变化。
  • 快速原型验证:你需要快速验证一个想法,不想花时间搭复杂的框架。
  • 教学演示:给学生讲轨迹优化,直接打靶法是最容易理解的。

但如果你的问题是长时域、强非线性、多约束的,我建议你考虑多重打靶法或者配点法。这些方法虽然实现起来复杂一些,但稳定性和效率都好得多。

4.5 知识体系图

下面这张图展示了直接打靶法在整个轨迹优化方法中的位置,以及它与其他方法的关系。

直接打靶法知识体系 轨迹优化方法 间接法(变分法) 直接法 直接打靶法 多重打靶法 配点法 核心:控制离散化 + 数值积分 优点:简单、直观、变量少 缺点:初值敏感、数值稳定性差 适用场景 • 短时域问题 • 弱非线性系统 • 快速原型验证 • 教学演示

嗯,这张图把关系理得很清楚。直接打靶法在直接法家族里是最基础的一个。它虽然简单,但理解它之后,再去学多重打靶法和配点法就会容易很多。

4.6 小结

直接打靶法就像学游泳时的浮板——它帮你建立信心,让你理解水感。但你不能永远抱着浮板。当你掌握了基本原理,就要去挑战更高级的方法。

我个人建议,每个做轨迹优化的工程师都应该亲手实现一次直接打靶法。哪怕只是解决一个简单的二维火箭着陆问题。这个过程会让你深刻理解:什么是离散化?什么是数值积分?优化器到底在干什么?这些基础打牢了,后面学什么都快。

好,这一章就到这里。记住,直接打靶法不是万能的,但它是你进入轨迹优化世界的一把钥匙。


专注资料整理