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 小结
直接打靶法就像学游泳时的浮板——它帮你建立信心,让你理解水感。但你不能永远抱着浮板。当你掌握了基本原理,就要去挑战更高级的方法。
我个人建议,每个做轨迹优化的工程师都应该亲手实现一次直接打靶法。哪怕只是解决一个简单的二维火箭着陆问题。这个过程会让你深刻理解:什么是离散化?什么是数值积分?优化器到底在干什么?这些基础打牢了,后面学什么都快。
好,这一章就到这里。记住,直接打靶法不是万能的,但它是你进入轨迹优化世界的一把钥匙。