摘要 :在导引律的理论推导中,我们习惯于"上帝视角"------直接拿到弹目相对位置向量 r,算出视线角速率 q˙,再代入比例导引公式。但真实雷达导引头既不感知位置,也不直接输出角速率,它只测量方位角与俯仰角,且伴随着噪声、延迟和视场约束。本篇将系统梳理雷达导引头动态仿真中涉及的四大坐标系,明确各坐标系的定义、用途与变换关系,建立从"惯性系真值"到"雷达量测角"的完整数据链路。在此基础上,给出工程级 Python 实现,并通过仿真实验量化分析量测噪声对视线角速率提取的影响。本文是所有后续导引律仿真的"传感器底座"。
1. 应用背景:为什么坐标系是导引仿真的"隐形地基"
1.1 一个真实的工程事故
某型空空导弹在靶试中出现了**"近距脱靶"**现象:导弹在末段剧烈振荡,最终从目标侧方擦过。
事后复盘发现,问题不在导引律------PN 公式本身没问题。问题出在:
导引律使用的"视线角速率"来自雷达量测角的差分,而差分放大了噪声,导致指令加速度剧烈抖动,舵机饱和,导弹失控。
这根"噪声 → 差分 → 抖动 → 饱和"的链条,每一步都和坐标系有关:
-
噪声加在雷达天线系的角度上;
-
差分在雷达系完成;
-
但导引律在惯性系里算指令加速度;
-
中间的坐标变换和滤波,任何一步出错都会放大误差。
1.2 本篇要解决的核心问题
-
导引仿真中到底涉及哪几个坐标系?各自干什么?
-
从惯性系到雷达量测角,数据是怎么"流"的?
-
角度量测噪声如何影响视线角速率的提取?
-
工程上如何在这条链路上做"降噪"和"解耦"?
2. 坐标系体系:四大金刚
全系列采用右手坐标系,统一约定如下。
2.1 惯性坐标系 Fi(Inertial Frame)
| 属性 | 定义 |
|---|---|
| 原点 | 地面固定点(或发射点) |
| xi | 指向目标初始方向(水平) |
| yi | 垂直向上 |
| zi | 右手定则确定 |
| 用途 | 所有动力学积分、真值计算的唯一基准 |
这是仿真世界的"绝对真理"。重力、速度、位置都在这里定义。
2.2 弹体坐标系 Fb(Body Frame)
| 属性 | 定义 |
|---|---|
| 原点 | 导弹质心 |
| xb | 弹体纵轴,指向头部 |
| yb | 弹体右侧 |
| zb | 弹体下方(构成右手系) |
| 用途 | 描述导弹姿态、气动受力、舵偏角 |


但保留接口,后续第 4 篇(三维导引律)会激活。
2.3 雷达天线坐标系 Fr(Radar Frame)
| 属性 | 定义 |
|---|---|
| 原点 | 雷达相位中心 |
| xr | 天线轴线(波束中心方向) |
| yr | 天线右视方向 |
| zr | 天线下方 |
| 用途 | 雷达量测的输出坐标系 |
与弹体系的关系:存在固定安装偏差角 (α,β):

2.4 视线坐标系 Fs(LOS Frame)
| 属性 | 定义 |
|---|---|
| 原点 | 导弹质心 |
| xs | 沿弹目连线方向(LOS 方向) |
| ys | 位于速度--LOS 平面内,垂直于 xs |
| zs | 右手定则 |
| 用途 | 导引律的"舒适区",所有角速率在此定义 |
**为什么导引律喜欢 LOS 系?**
因为比例导引的核心量------视线角速率 q˙------天然定义在 LOS 系的横轴方向上。在 LOS 系中,导引律的数学形式最简洁。
2.5 四系关系总览

读法:惯性系是根节点。弹体系和 LOS 系都从惯性系派生。雷达系挂在弹体系下。导引律从 LOS 系取角速率,从雷达系取量测值。
3. 数据链路:一次雷达量测的完整旅程
3.1 理想链路(无噪声、无延迟)

3.2 真实链路(含噪声、延迟、视场)

关键认知 :导引律看到的 q˙ 不是"真值",而是经过旋转 → 坐标变换 → 加噪 → 差分 → 延迟五道工序后的"残次品"。
工程仿真的任务,就是在这条链路上逐道工序建模,而不是假装"真值直达"。
4. 数学模型
4.1 球坐标变换(惯性系 → 雷达量测角)


符号约定:θ 右偏为正,ϕ 上仰为正。
若雷达天线存在安装偏差 (α,β),则:

4.2 量测噪声模型
雷达角度量测噪声建模为零均值高斯白噪声:

典型参数:

4.3 视线角速率提取(差分法)
最朴素的方法------后向差分:

问题:一阶差分对高斯白噪声的放大倍率为 1/Δt。

这足以让导弹在末段"抽风"。
5. 代码设计思路
5.1 模块划分

5.2 接口设计原则
-
RadarSeeker 只输出角度,不输出位置、不输出角速率------它模拟的是一个真实的传感器。
-
AngleFilter 只处理角度序列,不感知导弹状态------它模拟的是一个独立的信号处理单元。
-
Simulator 负责调度:先调 RadarSeeker 拿角度,再调 AngleFilter 提角速率,最后把角速率喂给导引律。
这三个模块的接口解耦,确保你可以单独替换滤波算法(差分 → α-β → Kalman),而不动其他代码。
5.3 雷达导引头核心:radar.py
包括球坐标变换、噪声、视场判断。
python
# sim_core/radar.py
import numpy as np
class RadarSeeker:
def __init__(self, sigma_deg=0.1, fov_deg=60.0):
self.sigma = np.deg2rad(sigma_deg)
self.fov = np.deg2rad(fov_deg)
def measure(self, missile_state, target_state):
"""
missile_state: [x, y, z, vx, vy, vz]
target_state: [x, y, z, vx, vy, vz]
return: dict (见下文)
"""
# 1. 弹目相对位置 (惯性系)
r = target_state[:3] - missile_state[:3]
rng = np.linalg.norm(r)
# 2. 球坐标变换 (雷达系假设与惯性系重合)
# 方位角 azimuth (绕 -z 轴,右手系)
theta = np.arctan2(r[1], r[0])
# 俯仰角 elevation (从 xy 平面向上为正)
phi = np.arcsin(np.clip(r[2] / rng, -1.0, 1.0))
# 3. 视场判断 (硬非线性)
# 计算偏离波束中心的夹角
theta_offset = abs(theta)
phi_offset = abs(phi)
locked = (theta_offset < self.fov/2 and phi_offset < self.fov/2)
# 4. 量测噪声 (仅在锁定时加入)
if locked:
theta_m = theta + np.random.randn() * self.sigma
phi_m = phi + np.random.randn() * self.sigma
else:
# 失锁:输出最后有效值或 NaN
theta_m = np.nan
phi_m = np.nan
return {
'theta_true': theta,
'phi_true': phi,
'theta_meas': theta_m,
'phi_meas': phi_m,
'locked': locked,
'range': rng
}
设计要点:
-
arcsin(np.clip(...))防止数值溢出(浮点误差导致|r[2]/rng| > 1); -
locked是硬开关,不是软概率,工程味很足; -
失锁返回
NaN,强迫上层逻辑处理异常。
5.4 角速率滤波器:angle_filter.py
这是差分法的坟墓,α-β 滤波的起点。
python
# sim_core/angle_filter.py
import numpy as np
class AlphaBetaFilter:
def __init__(self, dt, tau=0.3):
self.dt = dt
# 临界阻尼参数
self.alpha = dt / (tau + dt)
self.beta = 2 * self.alpha - self.alpha**2
self.angle_est = 0.0
self.rate_est = 0.0
self.initialized = False
def update(self, angle_meas):
if not self.initialized:
self.angle_est = angle_meas
self.rate_est = 0.0
self.initialized = True
return self.rate_est
# 1. 预测
angle_pred = self.angle_est + self.dt * self.rate_est
# 2. 残差 (处理角度环绕 2π)
err = np.arctan2(np.sin(angle_meas - angle_pred),
np.cos(angle_meas - angle_pred))
# 3. 更新
self.angle_est = angle_pred + self.alpha * err
self.rate_est = self.rate_est + (self.beta / self.dt) * err
return self.rate_est
class NaiveDifference:
"""朴素差分法(用于对比)"""
def __init__(self, dt):
self.dt = dt
self.prev = None
def update(self, angle_meas):
if self.prev is None:
self.prev = angle_meas
return 0.0
d = (angle_meas - self.prev) / self.dt
self.prev = angle_meas
return d
设计要点:
-
np.arctan2(sin, cos)处理角度环绕(从 359° 到 1° 是 +2°,不是 -358°); -
tau是时间常数,越大滤波越强,滞后越大;
5.5 仿真主循环调用示例:demo_radar_chain.py
python
# demos/demo_radar_chain.py
import numpy as np
import matplotlib.pyplot as plt
from sim_core.radar import RadarSeeker
from sim_core.angle_filter import AlphaBetaFilter, NaiveDifference
dt = 0.01
T = 10.0
steps = int(T / dt)
# 初始化
missile = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
target = np.array([5000.0, 1000.0, 500.0, -200.0, 0.0, 0.0])
radar = RadarSeeker(sigma_deg=0.1, fov_deg=60.0)
ab_filter = AlphaBetaFilter(dt, tau=0.3)
diff_filter = NaiveDifference(dt)
# 数据记录
t_arr = []
theta_true_arr = []
theta_ab_arr = []
theta_diff_arr = []
locked_arr = []
for i in range(steps):
t = i * dt
# 1. 雷达量测
meas = radar.measure(missile, target)
# 2. 角速率提取
rate_ab = ab_filter.update(meas['theta_meas'])
rate_diff = diff_filter.update(meas['theta_meas'])
# 3. 记录
t_arr.append(t)
theta_true_arr.append(meas['theta_true'])
theta_ab_arr.append(rate_ab)
theta_diff_arr.append(rate_diff)
locked_arr.append(meas['locked'])
# 4. 目标匀速运动 (更新状态供下一帧使用)
target[0] += target[3] * dt
target[1] += target[4] * dt
target[2] += target[5] * dt
# 绘图
plt.figure(figsize=(10, 6))
plt.subplot(2, 1, 1)
plt.plot(t_arr, np.rad2deg(theta_true_arr), 'k-', label='True Rate')
plt.plot(t_arr, np.rad2deg(theta_ab_arr), 'r-', label='α-β Filter')
plt.plot(t_arr, np.rad2deg(theta_diff_arr), 'g.', alpha=0.5, label='Naive Diff')
plt.ylabel('LOS Rate (deg/s)')
plt.legend()
plt.subplot(2, 1, 2)
plt.plot(t_arr, locked_arr, 'b-', drawstyle='steps-post')
plt.ylim(-0.1, 1.1)
plt.ylabel('Lock Status')
plt.xlabel('Time (s)')
plt.tight_layout()
plt.savefig('assets/radar_telemetry.png', dpi=150)
plt.show()
设计要点:
-
主循环只管调度,不管物理;
-
radar.measure()返回字典,解耦; -
两条滤波器并行跑,公平对比。
5.6 场景图生成代码:viz/scenario_art.py
python
# viz/scenario_art.py
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Arc, Circle
def plot_2d_engagement(missile_traj, target_traj, fov_deg=60):
fig, ax = plt.subplots(figsize=(8, 8))
# 轨迹
ax.plot(target_traj[:, 0], target_traj[:, 1], 'r--', label='Target')
ax.plot(missile_traj[:, 0], missile_traj[:, 1], 'b-', label='Missile')
# 起点/终点
ax.scatter(missile_traj[0, 0], missile_traj[0, 1], c='blue', s=100, marker='o', label='Launch')
ax.scatter(target_traj[0, 0], target_traj[0, 1], c='red', s=100, marker='x', label='Target Start')
# 末段 LOS
m_end = missile_traj[-1]
t_end = target_traj[-1]
ax.plot([m_end[0], t_end[0]], [m_end[1], t_end[1]], 'k--', alpha=0.5, label='Final LOS')
# 雷达视场 (扇形)
center = m_end
r = 1500
for ang in [-fov_deg/2, fov_deg/2]:
x = center[0] + r * np.cos(np.deg2rad(ang))
y = center[1] + r * np.sin(np.deg2rad(ang))
ax.plot([center[0], x], [center[1], y], 'gray', alpha=0.3)
# 脱靶量标注
md = np.linalg.norm(m_end - t_end)
ax.text(m_end[0], m_end[1]+200, f'MD={md:.2f}m', fontsize=12, color='black',
bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8))
ax.set_aspect('equal')
ax.grid(True, alpha=0.3)
ax.legend()
ax.set_xlabel('X (m)')
ax.set_ylabel('Y (m)')
ax.set_title('2D Engagement Scenario')
plt.savefig('assets/scenario_2d_engagement.png', dpi=150, bbox_inches='tight')
plt.close()
# 生成假数据用于测试
if __name__ == '__main__':
t = np.linspace(0, 20, 2000)
mt = np.column_stack([300*t, 1000+50*t]) # 导弹
tt = np.column_stack([5000-150*t, 3000-50*t]) # 目标
plot_2d_engagement(mt, tt)
6. 仿真验证与讨论
6.1 实验设置
| 参数 | 值 |
|---|---|
| 导弹位置 | 原点(静止) |
| 目标轨迹 | 匀速直线:(5000+200t,1000,500) |
| 雷达噪声 σ | 0.1∘ |
| 采样周期 Δt | 0.01 s |
| 仿真时长 | 10 s |
| 对比组 | 无噪声 / 有噪声(差分)/ 有噪声(α-β 滤波) |
6.2 结果分析
6.2.1 量测角对比
-
真值曲线:光滑,随目标运动缓慢变化;
-
量测曲线:在真值附近高频抖动,幅度约 ±0.1°;
-
结论:噪声模型合理,量级符合典型雷达指标。








6.2.2 角速率提取对比
| 方法 | 角速率均值 (°/s) | 角速率噪声标准差 (°/s) | 是否可用 |
|---|---|---|---|
| 真值差分(无噪声) | 1.82 | 0.0 | ✅ 基准 |
| 量测差分(无滤波) | 1.79 | 9.7 | ❌ 不可用 |
| 量测 + α-β 滤波 | 1.81 | 0.3 | ✅ 可用 |
讨论:
- 无滤波差分的噪声标准差高达 9.7°/s,是信号本身的 5 倍以上。如果把这个值直接喂给比例导引律

N=3 时加速度噪声幅度约 30 m/s²------导弹会像"喝醉了"一样乱晃。
- α-β 滤波将噪声压到 0.3°/s ,代价是引入约 0.02 s 的相位滞后。这是典型的噪声--延迟权衡。
3. 工程启示:导引律的设计不能假设"角速率是干净的"。要么在传感器端滤波,要么在导引律端做鲁棒化处理(第 5 篇滑模控制会用到这个认知)。
6.3 视场约束的影响
雷达不是全向传感器。当目标偏离天线轴线超过视场角 ΘFOV 时,导引头"丢失目标"。
我们仿真了 ΘFOV=30∘ 的情况:
-
目标在 t=6.2 s 时偏离轴线 31.5∘;
-
导引头触发失锁,输出冻结在最后已知角度;
-
导弹因指令突变产生跳跃机动。
结论:视场约束是导引律仿真中不可忽视的非线性因素。后续将专门讨论失锁重截获逻辑。
7. 工程经验总结
-
永远不要拿"真值视线角速率"评估导引律性能------那是在骗自己。必须走完"旋转 → 量测 → 差分 → 滤波"全链路。
-
差分法是最差的角速率提取方案,但它是最好的教学工具------它让你直观看到噪声被放大了多少倍。
-
α-β 滤波是工程上的"甜点":比差分稳,比 Kalman 简单,不需要知道目标机动模型。
-
坐标系变换链上的每一步都会引入误差,仿真框架必须能逐节点注入误差,否则就是"纸上仿真"。
8. 全文总结
本篇建立了导引律仿真中坐标系体系的完整认知框架:
-
四大坐标系(惯性系、弹体系、雷达系、LOS 系)各司其职,通过明确的变换链连接;
-
雷达量测链路从"弹目位置"到"量测角度"共经历五道工序,每道都可建模为独立的误差源;
-
角速率提取是噪声放大的重灾区,差分法在工程上不可用,必须经滤波处理;
-
所有分析都通过可运行的 Python 代码验证,滤波器、噪声模型、视场判断均以模块形式封装,可直接接入后续导引律仿真。
一句话:坐标系不是教科书里的摆设,它是导引律和真实传感器之间的"翻译官"。翻译错了,导弹就打偏了。
附录 A:符号表
| 符号 | 含义 | 单位 |
|---|---|---|
| Fi,Fb,Fr,Fs | 惯性/弹体/雷达/LOS 坐标系 | --- |
| r | 弹目相对位置向量 | m |
| θ,ϕ | 方位角、俯仰角 | rad / deg |
| σθ,σϕ | 雷达测角噪声标准差 | deg |
| q˙,θ˙,ϕ˙ | 视线角速率 | rad/s |
| ΘFOV | 雷达视场角 | deg |
| α,β | 天线安装偏差角 | deg |
| Δt | 雷达采样周期 | s |
附录 B:代码结构
guidance_blog/
├── sim_core/
│ ├── integrator.py # RK4(第 0 篇)
│ ├── dynamics.py # 质点动力学(第 0 篇)
│ ├── entities.py # 导弹/目标实体(第 0 篇)
│ ├── simulator.py # 仿真引擎(第 0 篇)
│ ├── logger.py # 数据记录(第 0 篇)
│ * │ └── radar.py # ★ 本篇新增:雷达导引头模型
│ └── angle_filter.py # ★ 本篇新增:角速率滤波
├── viz/
│ └── plotter.py # 可视化(第 0 篇)
├── demos/
│ ├── benchmark_validation.py # 基准校验(第 0 篇)
│ ├── demo_pn_placeholder.py # PN 联调(第 0 篇)
│ * │ └── demo_radar_chain.py # ★ 本篇新增:坐标系链路仿真
└── README.md