第1章:Python基础回顾——数组操作、复数运算、矩阵运算

各位同学,欢迎来到《雷达信号处理中的FFT算法实战》。

我是你们的讲师,一个在雷达信号处理领域摸爬滚打十几年的老工程师。今天咱们先不急着讲FFT,先把Python的底子打牢。你想想看,雷达信号处理说白了就是跟数组、复数、矩阵打交道。这三样东西玩不转,后面FFT再花哨也是空中楼阁。

1.1 数组操作——雷达数据的“容器”

雷达回波信号,本质上就是一个时间序列的采样点。我习惯用NumPy的ndarray来装这些数据。为什么不用Python自带的list?因为慢,而且不方便做数学运算。

核心要点:雷达信号处理中,90%的数据都是数组形式。一维数组存时域信号,二维数组存多通道或脉冲矩阵。

先看个最简单的例子。假设我们采集了一段雷达回波,采样率10MHz,采集了1024个点:

import numpy as np

# 模拟一段雷达回波信号
fs = 10e6  # 采样率 10 MHz
N = 1024   # 采样点数
t = np.arange(N) / fs  # 时间轴

# 生成一个简单的正弦波模拟回波
f0 = 1e6  # 信号频率 1 MHz
signal = np.sin(2 * np.pi * f0 * t)

print(f"信号长度: {len(signal)}")
print(f"信号类型: {type(signal)}")
print(f"前5个采样点: {signal[:5]}")

嗯,这里要注意:np.arange(N)生成的是0到N-1的整数序列,除以fs就得到了时间轴。我在项目中遇到过不少新手直接用range(N),结果后面做数学运算时各种报错——因为range对象不是数组。

1.2 数组切片与索引——提取你想要的信号片段

雷达信号处理中,经常需要截取一段信号来分析。比如我只想看第200到第500个采样点之间的信号:

# 切片操作
segment = signal[200:500]  # 左闭右开
print(f"截取信号长度: {len(segment)}")

# 步进采样——有时候为了降采样
downsampled = signal[::2]  # 每隔一个点取一个
print(f"降采样后长度: {len(downsampled)}")

我个人习惯用start:stop:step这种切片方式,非常直观。但曾经有个坑——有一次我在处理脉冲多普勒雷达数据时,不小心把步进写成了负数,结果信号时间轴反了,折腾了半天才发现。所以,切片时务必确认步进方向

避坑指南:我曾经在项目里用signal[0:100:2]做降采样,结果忘了原始信号长度是奇数,最后数据长度对不上。建议先检查数组长度,再做切片。

1.3 复数运算——雷达信号的“灵魂”

雷达信号处理离不开复数。为什么?因为雷达发射的是实信号,但经过正交解调后,我们得到的是I/Q两路信号,合在一起就是复数形式。说白了,复数能同时表示信号的幅度和相位,这对雷达测距、测速至关重要。

Python里创建复数很简单:

# 创建复数
z1 = 3 + 4j  # 实部3,虚部4
z2 = complex(1, 2)  # 另一种写法

print(f"z1 = {z1}")
print(f"z1的实部: {z1.real}")
print(f"z1的虚部: {z1.imag}")
print(f"z1的模: {abs(z1)}")
print(f"z1的相位(弧度): {np.angle(z1)}")
print(f"z1的相位(度): {np.degrees(np.angle(z1))}")

你想想看,在雷达信号处理中,我们经常要计算信号的相位变化。比如动目标检测(MTI)中,就是通过比较相邻脉冲的相位差来判断目标是否移动。

小技巧:我习惯用np.angle()提取相位,但要注意它返回的是主值范围在[-π, π]之间。如果需要连续相位,要用np.unwrap()解包裹。这个坑我踩过——有一次做相位测距,结果相位跳变导致距离计算全错了。

1.4 复数数组的操作

实际雷达数据都是复数数组。比如一个脉冲的I/Q数据:

# 模拟I/Q数据
N = 256
I = np.random.randn(N)  # 同相分量
Q = np.random.randn(N)  # 正交分量

# 合成复数信号
complex_signal = I + 1j * Q

# 计算幅度和相位
amplitude = np.abs(complex_signal)
phase = np.angle(complex_signal)

print(f"复数信号前5个值: {complex_signal[:5]}")
print(f"幅度前5个值: {amplitude[:5]:.3f}")
print(f"相位前5个值: {phase[:5]:.3f}")

这里有个细节:1j是Python的虚数单位,不能写成ji。我记得刚开始用Python时,总习惯用j,结果报错——因为j是变量名。后来就养成了用1j的习惯。

1.5 矩阵运算——多通道雷达数据的“骨架”

现代雷达往往有多个通道(比如相控阵雷达的阵元),数据自然就是二维矩阵了。矩阵运算在波束形成、空间谱估计中无处不在。

先看矩阵的创建和基本运算:

# 创建一个3x3的矩阵
A = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]])

B = np.array([[9, 8, 7],
              [6, 5, 4],
              [3, 2, 1]])

# 矩阵加法
C = A + B
print(f"矩阵加法结果:\n{C}")

# 矩阵乘法(注意:不是对应元素相乘)
D = np.dot(A, B)  # 或者 A @ B
print(f"矩阵乘法结果:\n{D}")

# 对应元素相乘(Hadamard积)
E = A * B
print(f"对应元素相乘:\n{E}")

嗯,这里要特别强调:A * B是对应元素相乘,A @ B才是真正的矩阵乘法。我在项目中见过有人用*做矩阵乘法,结果波束方向图完全不对——因为矩阵乘法顺序搞反了。

实战经验:在雷达信号处理中,矩阵乘法常用于计算协方差矩阵。比如X @ X.conj().T就是计算数据矩阵X的协方差矩阵,这在自适应波束形成(如MVDR)中是核心步骤。

1.6 矩阵转置与共轭转置

复数矩阵的转置和共轭转置是雷达信号处理的“家常便饭”。比如在计算阵列流型矩阵时:

# 复数矩阵
X = np.array([[1+2j, 3+4j],
              [5+6j, 7+8j]])

# 普通转置
X_T = X.T
print(f"转置:\n{X_T}")

# 共轭转置(Hermitian转置)
X_H = X.conj().T  # 或者用 X.T.conj()
print(f"共轭转置:\n{X_H}")

我个人习惯用X.conj().T,因为这样写逻辑清晰——先取共轭,再转置。但要注意,NumPy的.T只做转置,不做共轭。如果你需要共轭转置,千万别直接用.T,否则相位信息就丢了。

避坑指南:我曾经在写自适应滤波算法时,直接用X.T代替共轭转置,结果算法收敛到错误的方向。后来排查了整整一天才发现是共轭转置的问题。所以,处理复数矩阵时,务必确认是转置还是共轭转置

1.7 矩阵求逆与特征分解

在雷达信号处理的高级算法中(比如MUSIC、ESPRIT),矩阵求逆和特征分解是家常便饭。看个简单的例子:

# 创建一个对称正定矩阵
R = np.array([[4, 1],
              [1, 3]])

# 矩阵求逆
R_inv = np.linalg.inv(R)
print(f"逆矩阵:\n{R_inv}")

# 验证:R @ R_inv 应该接近单位矩阵
print(f"验证:\n{R @ R_inv}")

# 特征分解
eigenvalues, eigenvectors = np.linalg.eig(R)
print(f"特征值: {eigenvalues}")
print(f"特征向量:\n{eigenvectors}")

这里要注意:np.linalg.eig()返回的特征向量是按列排列的。也就是说,eigenvectors[:, 0]对应第一个特征值。我刚开始用的时候总搞混,后来就养成了打印形状检查的习惯。

小技巧:如果矩阵是Hermitian矩阵(共轭对称),用np.linalg.eigh()更高效,而且保证返回实特征值。我在处理协方差矩阵时都用这个函数,比eig()快不少。

1.8 广播机制——让数组运算更简洁

NumPy的广播机制在雷达信号处理中非常实用。比如我们要给每个通道的信号加上一个固定的直流偏置:

# 多通道数据:4个通道,每个通道256个采样点
data = np.random.randn(4, 256)

# 每个通道的直流偏置
dc_offset = np.array([0.1, -0.2, 0.05, 0.3])

# 广播:自动将dc_offset扩展到每一行
data_corrected = data + dc_offset[:, np.newaxis]

print(f"原始数据形状: {data.shape}")
print(f"修正后数据形状: {data_corrected.shape}")

你想想看,如果没有广播机制,你得写个循环,每个通道单独加偏置,代码又长又容易出错。广播机制让代码更简洁,也更容易维护。

好了,这一章的内容就到这里。数组操作、复数运算、矩阵运算,这三样东西是雷达信号处理的“三驾马车”。下一章我们开始正式进入FFT的世界,到时候你会发现,今天打的基础全都能用上。

记住:基础不牢,地动山摇。把这些练熟了,后面的路就好走了。