信号处理这条线上有个挺有意思的现象:很多人能把 QAM 星座图画得滚瓜烂熟,也能背出误码率公式,但真要让他把一段具体的波形拆成正交基函数上的坐标,再算出能量和两两之间的距离,手就开始抖了。原因不复杂——教材把“信号在几何空间中的表示”当成一个定理来讲,讲完就直奔调制解调,中间那条从连续波形到离散坐标的链路,被压缩成了几行公式。而这些公式里的每一个系数、每一个归一化因子,恰恰是仿真跑不通、星座图画歪、误码率对不上时的罪魁祸首。
这篇东西想干的事情很具体:把正交基函数怎么挑、展开系数怎么算、能量和距离怎么在两个域之间来回换算、调制方案怎么落到几何平面上这四件事串成一条能走通的路径,中间该有的推导、该核对的量纲、该踩的坑都摊开说。读者对象是正在做通信或信号处理仿真的人,也包括刚学完信号与系统、看公式能懂但一动手就虚的同学。我不打算复述教材,重点放在“为什么这么设计”和“实际做的时候哪里会错”这两件事上。
1. 波形空间里的维数焦虑:为什么非要做几何化
1.1 连续时间信号是个无限维的东西,但我们只关心几个方向
一个定义在 ([0,T]) 上的实信号 (s(t)),严格来说属于平方可积函数空间 (L_2[0,T]),这个空间的维数是无限的——你可以找出无穷多个线性无关的函数。调制解调要做的事情,是在这无穷多个方向里,只挑出真正携带信息的那么几个,剩下的全部扔掉。PSK 只用了二维,16-QAM 也是二维,单极性基带信号只用了一维。
这个“扔掉”的过程不是随意裁剪,它有严格的条件:被扔掉的那部分必须和保留的部分正交。设保留下来的方向由基函数 (\varphi_1(t),\dots,\varphi_N(t)) 张成,那么对任意一个信号 (s(t)),我们做分解
[ s(t) = \sum_{i=1}^{N} s_i\varphi_i(t) + e(t),\qquad \langle e,\varphi_i\rangle = 0 ]
其中 (s_i = \langle s,\varphi_i\rangle) 就是展开系数,(e(t)) 是残差。残差和所有基正交,意味着它在这个子空间里“没有投影”,任何对投影系数的线性运算都不受它影响。
我刚开始学这块的时候有个误解,以为残差是“误差”,越小越好。其实不是。残差可以是很大的能量,只要它落在正交补空间里,对判决就完全没有影响。真正决定成败的,是基函数张成的那个子空间有没有把信号间的差异装进去。
1.2 投影之后判决定居:噪声在正交补上的分量是白扔的
把这件事说到工程上,就绕不开加性高斯白噪声(AWGN)信道下的充分统计量问题。接收信号是 (r(t) = s_m(t) + n(t)),我们对它做同样的投影,得到
[ r_i = \langle r,\varphi_i\rangle = s_{m,i} + n_i,\qquad n_i = \langle n,\varphi_i\rangle ]
关键结论是:如果 (n(t)) 是双边功率谱密度为 (N_0/2) 的白噪声,那么 (n_i) 是零均值高斯随机变量,且
[ E[n_i n_j] = \frac{N_0}{2}\int_0^T \varphi_i(t)\varphi_j(t),dt = \frac{N_0}{2}\delta_{ij} ]
也就是说,各个维度的噪声是独立同分布的,方差都是 (N_0/2)(实信号情形)。更重要的另一半:噪声在正交补空间上的分量 (\langle n, e\rangle) 与 ((n_1,\dots,n_N)) 统计独立,而信号 (s_m) 在正交补上没有分量,所以这部分观测值里纯粹是噪声,对判断发的是哪个 (s_m) 一点用都没有。
这就是为什么我们可以放心地把一条连续波形压缩成 (N) 个数字,然后在一个 (N) 维欧氏空间里做判决。压缩是有损的,损失的部分无害。理解了这一层,“信号在几何空间中的表示”就从数学游戏变成了工程合理性。
顺便提一句,这套推理对任何完备正交基都成立,包括后面要讲的匹配滤波器输出采样——匹配滤波器在 (t=T) 时刻的输出值,恰好就是 (\langle r,\varphi\rangle)。相关器实现和匹配滤波器实现是同一件事的两种画法,很多教材分开讲,容易让人以为是两个不同的东西。
2. 正交基的构造:Gram-Schmidt 的执行顺序比公式更重要
2.1 先减投影再归一化,两步的顺序不能颠
给定一组信号 (s_1(t),\dots,s_M(t)),要构造出张成同一空间的归一化正交基,标准做法是 Gram-Schmidt 正交化。流程本身很短:
- 取第一个信号,直接归一化得到 (\varphi_1 = s_1/|s_1|);
- 取第二个信号,先减掉它在 (\varphi_1) 上的投影:(g_2 = s_2 - \langle s_2,\varphi_1\rangle\varphi_1);
- 归一化:(\varphi_2 = g_2/|g_2|);
- 对后续每个信号,先减掉它在所有已有基函数上的投影,再归一化。
顺序为什么不能反?因为归一化之后再减投影,减掉的是一个已经放缩过的量,减法做完剩下的向量不再和 (\varphi_1) 正交。有人图省事,先把所有信号全部归一化到单位能量,然后再做正交化,结果基函数之间内积不是零——这种错误在小数点后第三位就能露馅,但如果你只用眼睛看星座图,可能永远发现不了。
还有一个更隐蔽的坑:减投影必须用当前最新的基函数。经典 Gram-Schmidt 在数学上是等价的,但如果写成“先把所有投影系数算完,再统一减”,那就是经典 GS,数值稳定性差;每减一次就用最新值,是修正 Gram-Schmidt(MGS),在浮点运算下精度明显更好。做仿真时用 MGS 写法几乎是零成本,没有理由不用。
2.2 手算一组三脉冲信号的完整过程
拿一个具体的例子走一遍,比看十遍公式有用。设 (T=1),三个信号定义在 ([0,T]) 上:
- (s_1(t) = 1),(0\le t<T/2),其余为 0;
- (s_2(t) = 1),(T/2\le t<T),其余为 0;
- (s_3(t) = s_1(t) - s_2(t))。
先算能量。(\langle s_1,s_1\rangle = \int_0^{1/2}1,dt = 1/2),同理 (s_2) 的能量也是 (1/2)。(s_3) 在 ([0,T/2]) 上取 (+1),在 ([T/2,T]) 上取 (-1),能量是 (1/2 + 1/2 = 1)。
第一步,(\varphi_1 = s_1/\sqrt{1/2} = \sqrt{2},s_1),即在 ([0,T/2]) 上取 (\sqrt2),其余为 0。
第二步,算 (\langle s_2,\varphi_1\rangle = \int \sqrt2 s_1 s_2,dt = 0),因为两个脉冲不重叠。所以 (g_2 = s_2),(\varphi_2 = \sqrt2,s_2)。
第三步,(\langle s_3,\varphi_1\rangle = \int \sqrt2 s_1 s_3,dt = \sqrt2 \cdot (1/2) = \sqrt2/2)。同理 (\langle s_3,\varphi_2\rangle = -\sqrt2/2)。减去投影后 (g_3 = s_3 - \frac{\sqrt2}{2}\varphi_1 + \frac{\sqrt2}{2}\varphi_2)。代入 (\varphi_1 = \sqrt2 s_1),(\varphi_2 = \sqrt2 s_2),得到 (g_3 = s_3 - s_1 + s_2 = 0)。
结果很清楚:三个信号只张成了二维空间,(s_3) 是线性组合,不贡献新维度。基函数个数是 2,不是 3。这一点在写代码时必须处理——归一化时 (g_3) 的范数为零,如果不做阈值判断直接除,会得到 NaN 或 Inf,整个矩阵被污染。
对应的坐标是:(s_1 \to (\sqrt{1/2}, 0)),(s_2 \to (0, \sqrt{1/2})),(s_3 \to (\sqrt{1/2}, -\sqrt{1/2}))。拿能量核对一下:(1/2+0=1/2),(0+1/2=1/2),(1/2+1/2=1),和波形域直接积分的结果完全一致。
2.3 基不唯一,但距离和能量跟着一起不变
必须强调一个容易被忽略的事实:正交基不是唯一的。你对已有一组正交基做任意正交变换(旋转、反射),得到的新基照样正交归一,展开系数会跟着变,但信号之间的内积、能量、距离全都不变。这叫等距变换。
带来的直接后果是:星座图在坐标平面上的绝对朝向没有物理意义,有意义的只有点之间的相对位置。有人在仿真里发现自己的 16-QAM 星座和教材画的差了个 45 度旋转,慌得不行,其实完全正常,只是基函数选法不同。真正需要对齐的是归一化标尺——每个维度 1 单位能量对应多少幅度。
反过来说,等距变换也提供了调试手段:只要你算出的距离矩阵和目标距离矩阵对不上,问题一定出在基的构造或系数计算上,和基的“长相”无关。我一般会在代码里直接把 (M\times M) 的距离矩阵打出来,和手算的理论值逐项比对,这一步能挡掉八成低级错误。
3. 展开系数:内积、相关和匹配滤波说的是同一件事
3.1 系数的物理含义是“这段波形里含多少这个方向”
展开系数 (s_i = \langle s,\varphi_i\rangle = \int_0^T s(t)\varphi_i(t),dt) 的几何解释是投影长度,物理解释是相关性,工程解释是相关器输出。三种说法指的是同一个积分,只是站的角度不同。
把它和傅里叶级数对照特别好理解。傅里叶级数就是拿正弦余弦当基函数做展开,系数是 (\frac{2}{T}\int s(t)\cos(2\pi kt/T)dt),前面那个 (2/T) 就是归一化因子在起作用——(\cos) 在 ([0,T]) 上的能量是 (T/2),归一化基函数是 (\sqrt{2/T}\cos(\cdot)),系数自然就带上 (\sqrt{2/T}) 的因子。很多人记傅里叶系数公式总记混那个 (2/T) 和 (1/T),本质原因就是没把它当成正交基展开来理解。
匹配滤波器的推导也是这个积分。滤波器冲激响应取 (h(t) = s(T-t)),输出在 (t=T) 时刻的值是 (\int_0^T r(\tau)s(\tau)d\tau),正是内积。所以“相关接收机”和“匹配滤波器接收机”这两个名字在工程里经常混用,性能完全一样,选哪个取决于你的实现平台——FPGA 上做乘累加更方便,模拟前端用匹配滤波器更省事。
3.2 用采样值近似积分时,那个 (T_s) 因子必须跟着走
真实仿真里没有连续积分,只有离散采样。设采样间隔 (T_s = T/N),采样点 (s[n] = s(nT_s)),那么
[ \langle s_1,s_2\rangle = \int_0^T s_1(t)s_2(t),dt \approx T_s\sum_{n=0}^{N-1}s_1[n]s_2[n] ]
这个 (T_s) 是新手最容易漏的东西。漏了会怎样?所有能量和距离都按 (1/T_s) 倍缩放。如果你只比较相对大小,比如算归一化相关系数或者判决最近邻,缩放不影响结果,所以很多代码在“能跑通”的阶段是对的。但一旦你要把仿真出来的误码率和理论曲线对齐,或者把能量换算成实际功率,立刻就对不上了。
更麻烦的是,漏 (T_s) 的错误往往不是单独出现的。常见组合拳是:漏了 (T_s),同时又多除了一次采样点数 (N),两个错误恰好部分抵消,导致某些配置下看起来正常,换个采样率就崩。我的习惯是定义两个明确的小函数,把所有的内积运算都收口到里面,任何地方都不允许直接写np.dot(a, b):
import numpy as np T = 1.0 N = 512 t = np.linspace(0, T, N, endpoint=False) Ts = T / N def inner(a, b): """连续内积的离散近似,Ts 已经包含在内""" return Ts * np.sum(a * b) def energy(s): return inner(s, s)所有涉及内积、能量、距离的地方一律调这两个函数,从源头杜绝量纲混乱。这个封装看着啰嗦,但它救过我很多次。
3.3 复数信号的共轭:别在实信号上跟着瞎取
实信号的内积是对称的,(\langle s_1,s_2\rangle = \langle s_2,s_1\rangle)。复数基带信号就不一样了,正确形式是
[ \langle s_1,s_2\rangle = \int_0^T s_1(t)s_2^*(t),dt ]
第二个参数取共轭。用错了会导致什么?最典型的症状是展开系数出现不该有的相位翻转,QPSK 星座点全部镜像或者转了方向。因为 (\langle s_1,s_2\rangle) 和 (\langle s_2,s_1\rangle) 互为共轭,只要你保证在所有地方统一用一种顺序,其实也能自洽——但一旦混用,比如 Gram-Schmidt 里用了一种、计算距离时用了另一种,结果就彻底乱了。
我的建议是:实信号就老老实实只写实部代码,别为了“通用”提前引入复数;确实要做复基带仿真时,把所有内积统一写成conj(b)的形式,并且在心理上把第一个参数当“被投影的向量”,第二个当“投影方向”。这个约定固定下来,后面所有公式都顺着它推。
4. 能量与距离:两个域之间的换算表
4.1 帕塞瓦尔等式就是能量守恒的坐标版本
对任意完备正交基,有
[ E_s = \int_0^T |s(t)|^2dt = \sum_{i=1}^{N}|s_i|^2 ]
这就是帕塞瓦尔等式在信号空间里的表现。它的实用价值在于:你可以只在坐标域算能量,不用回头去积分波形。而且反过来还提供了一条核验路径——如果两个域算出来的能量对不上,说明基不正交、或者基不完备(漏掉了有能量的方向),或者系数算错了。
我处理过一个典型的“基不完备”案例:一组信号里有两个是不同时移的升余弦脉冲,做 Gram-Schmidt 只得到了一个基函数,能量核对时发现波形域能量加和远大于坐标域。原因不是 bug,而是那组信号的时移量恰好让两个脉冲完全正交、且都属于同一个二维子空间,但我的采样窗口截断导致正交性被破坏,实际需要两个基。把采样窗口从一个符号周期扩到三个符号周期,问题消失。这件事说明,能量核对表不是走过场,它能暴露采样窗口、截断这类结构性错误。
两信号之间的距离平方同理:
[ d^2(s_1,s_2) = \int_0^T |s_1(t)-s_2(t)|^2dt = \sum_{i=1}^{N}|s_{1,i}-s_{2,i}|^2 ]
左边是波形域,右边是坐标域,两边必须相等。这条等式是后面所有星座距离分析的基础。
4.2 相关系数衡量“像不像”,距离衡量“差多少”
除了距离,还有个常用量是归一化相关系数
[ \rho = \frac{\langle s_1,s_2\rangle}{\sqrt{E_1E_2}} ]
它的取值范围是 ([-1,1]),等于 1 表示两个信号完全一样(只差正比例),等于 0 表示正交,等于 -1 表示反相。和距离的关系是
[ d^2 = E_1 + E_2 - 2\sqrt{E_1E_2},\rho ]
等能量时 (d^2 = 2E(1-\rho))。这个式子在做波形设计时非常好用:想让两个信号容易区分,就压低 (\rho);极端情况下 (\rho=-1)(反相)时距离最大,等于 (2\sqrt{E}),这正是 BPSK 和 QPSK 用反相点对的原因。
值得一提的是,判决边界和相关系数直接挂钩。二选一的等能量情形,判决门限就落在两个星座点的中点,等价于比较 (\langle r,s_1\rangle) 和 (\langle r,s_2\rangle) 哪个大,也就是比较 (r) 和谁更相关。多选一时,最近邻判决划分出的是 Voronoi 区域,区域边界是星座点连线的垂直平分面。
4.3 一张必须逐项核对的对照表
把上面的关系整理成表,做仿真时逐项打钩:
| 物理量 | 波形域表达式 | 坐标域表达式 | 常见错误 |
|---|---|---|---|
| 内积 | (\int s_1 s_2^* dt) | (\sum s_{1,i}s_{2,i}^*) | 漏 (T_s);复信号忘取共轭 |
| 能量 | (\int |s|^2 dt) | (\sum |s_i|^2) | 基不完备时坐标域偏小 |
| 距离平方 | (\int |s_1-s_2|^2 dt) | (\sum |s_{1,i}-s_{2,i}|^2) | 先算坐标差再平方,顺序搞反 |
| 相关系数 | (\langle s_1,s_2\rangle/\sqrt{E_1E_2}) | 同左 | 分母漏开方 |
| 平均符号能量 | (\frac{1}{M}\sum_m E_m) | 同左 | 把峰值能量当平均能量 |
最后一行特别值得说一句。做 QAM 时经常要报平均符号能量 (E_s),但星座图上你能直接看到的是最大幅度对应的峰值能量。16-QAM 里最大幅度点 (I^2+Q^2=18),平均能量只有 10,差了一倍多。把峰值当平均值用,报出来的信噪比会虚高将近 2.6 dB,曲线和理论对不上还找不着原因。
5. 调制方案落到平面上:星座坐标是算出来的,不是画出来的
5.1 PSK 与 QAM 的两个标准基函数
载波调制的信号通常写成 (s(t) = A(t)\cos(2\pi f_ct + \theta(t)))。要把它变成二维坐标,标准做法是取两个正交归一的基函数
[ \varphi_1(t) = \sqrt{\frac{2}{T}}\cos(2\pi f_ct),\qquad \varphi_2(t) = -\sqrt{\frac{2}{T}}\sin(2\pi f_ct) ]
在 ([0,T]) 上积分,且 (f_c) 是 (1/T) 的整数倍(保证整周期积分),这两个函数确实正交归一。取负号是为了让 (\varphi_2) 和 (\varphi_1) 构成右手系,和复指数 (e^{j2\pi f_ct}) 的实部虚部对应关系一致。
把 (s(t) = \sqrt{2E_s/T}\cos(2\pi f_ct + \theta)) 展开:
[ s(t) = \sqrt{E_s}\cos\theta\cdot\varphi_1(t) + \sqrt{E_s}\sin\theta\cdot\varphi_2(t) ]
所以星座坐标就是 ((\sqrt{E_s}\cos\theta, \sqrt{E_s}\sin\theta)),落在一个半径 (\sqrt{E_s}) 的圆上。M-PSK 的全部点都在这个圆上均匀分布,(\theta_k = 2\pi k/M + \theta_0)。
QAM 则是把两路独立的多电平幅度调制合成,坐标是 ((a_i\sqrt{E_0}, a_q\sqrt{E_0})),其中 (a_i,a_q) 取 ({\pm1,\pm3,\dots}),(E_0) 是单维单位幅度对应的能量。注意 16-QAM 不是 16-PSK,两者平均功率相同的情况下,16-QAM 的最小距离要大得多,这是它功率效率更高的根源——圆的周长有限,点挤在圆上间距必然变小,而方形排布把点铺满了平面。
对基带 PAM,基函数退化成 (\varphi(t) = \sqrt{1/T},\mathrm{rect}(t/T)),一维坐标,星座点排成一条直线。单极性 NRZ 的坐标是 ({0, \sqrt{E}}),双极性是 ({-\sqrt{E}, +\sqrt{E}}),距离差一倍——这是双极性比单极性抗噪好 3 dB 的直接几何解释。
5.2 从最小距离反推误码:几何量到性能量的桥
一旦星座点坐标定了,最小欧氏距离 (d_{\min}) 就是决定误码性能的核心几何量。AWGN 下,两个等能量点之间的成对错误概率是
[ P_2 = Q!\left(\frac{d_{\min}}{\sqrt{2N_0}}\right) ]
这里的 (Q(x) = \frac{1}{\sqrt{2\pi}}\int_x^\infty e^{-u^2/2}du),(N_0) 是单边噪声功率谱密度。系数 (\sqrt{2N_0}) 的来源是:判决统计量的噪声分量方差是 (N_0/2),两个假设下的噪声之差方差是 (N_0),门限在中点,于是错误概率是 (Q(d_{\min}/(2\sqrt{N_0/2})))。
符号错误率的常用近似是 (P_e \approx \bar N,Q(d_{\min}/\sqrt{2N_0})),(\bar N) 是最近邻点的平均个数。举几个实际的数:
| 调制 | (d_{\min}^2) | 最近邻数 (\bar N) | 说明 |
|---|---|---|---|
| BPSK | (4E_b) | 1 | 一维,反相两点 |
| QPSK | (2E_s = 4E_b) | 2 | 每维一个邻居 |
| 16-QAM | (6E_s/(M-1)=0.4E_s=1.6E_b) | 3 | 内部 4、边上 3、角上 2,均值 3 |
| M-PSK | (4E_s\sin^2(\pi/M)) | 2 | 圆上相邻两点 |
16-QAM 那行的平均邻居数可以自己算:16 个点里 4 个内部点有 4 个邻居,8 个边点有 3 个,4 个角点有 2 个,((4\times4+8\times3+4\times2)/16=48/16=3)。这个数字不是随便写的,直接决定近似的系数。
用这些量还能算功率效率。以 (E_b/N_0=10) dB 为例,16-QAM 的 (E_s=4E_b),(d_{\min}^2=1.6E_b),于是 (d_{\min}^2/(2N_0)=0.8E_b/N_0=8),(\sqrt 8\approx2.83),(Q(2.83)\approx2.3\times10^{-3}),乘 3 得到约 (7\times10^{-3})。和完整公式 (3Q(\sqrt{0.8E_b/N_0})) 完全一致。这个手算过程建议每个人都做一遍,做到能闭着眼睛报数量级,仿真曲线一出来就知道对不对。
还有一条从几何直觉出发的重要判断:QPSK 和 BPSK 的 (E_b/N_0) 性能完全一样。看起来 QPSK 每符号能量翻倍,但它的最小距离也跟着翻倍,同时每个符号带 2 比特,一除正好抵消。这是“功率效率由最小距离和每维能量共同决定”的经典例证。
5.3 正交性不理想时,星座是被拉歪而不是被平移
理想情况下 (\varphi_1,\varphi_2) 严格正交,星座是规则网格。实际系统里 I/Q 两路存在幅度失配 (\epsilon) 和相位失配 (\Delta\theta),等效于两个基函数的内积不再是 0 而是 (\sin\Delta\theta) 这类小量。
结果不是星座整体平移,而是线性变形:原本正方形网格被拉成平行四边形(相位失配),或者长宽比被改变(幅度失配)。这个变形用矩阵语言描述就是一个非正交的 2×2 变换矩阵作用在原坐标上。因为判决器仍然按理想的欧氏距离算,变形后的星座会导致内圈点误判率飙升。
调试这类问题有个捷径:把接收到的星座点做聚类,算每个簇的协方差矩阵,特征向量的方向就是变形的主轴方向。如果协方差的主轴和坐标轴有夹角,基本可以锁定是相位失配;如果主轴和坐标轴对齐但两个方向的方差差异明显,是幅度失配。这比盯着星座图肉眼找规律靠谱得多。
6. 我在这条链路上踩过的坑
6.1 维数、秩和基的个数不是一回事
新手最常见的混淆是把基函数个数当成信号个数。前面那个三脉冲例子已经说明了,三个信号可能只张成二维。更隐蔽的情况是:信号个数多、秩也不低,但你的采样窗和信号支撑不匹配,导致本该正交的基在数值上不完全正交。
我一般会在 Gram-Schmidt 之后做两件事:一是打印基函数两两内积构成的 Gram 矩阵,理想情况应该接近单位阵,非对角元超过 (10^{-6}) 就该警觉;二是核对秩,用numpy.linalg.matrix_rank对信号矩阵做一次 SVD,看奇异值分布,如果有接近零的奇异值却生成了对应的基函数,说明正交化过程中出了问题。
还有一点:当信号个数大于维数时,Gram-Schmidt 的后半段会不断产出零向量。这时候终止条件(阈值)设多少就很关键。设太大,有效的低能量方向被误删;设太小,浮点噪声被当成新维度,基函数里混进数值垃圾。经验值取信号最大范数的 (10^{-9}) 到 (10^{-10}) 倍比较稳妥,同时一定要用相对阈值,不要用绝对阈值。
6.2 能量归一化链条上的一次连环错误
我遇到过最难查的一次,症状是 16-QAM 的仿真误码率和理论差 1.5 dB,而且只在低信噪比下偏,高信噪比反而对得上。查了半天发现是三处归一化各自都“差不多对”但叠一起就错了:
第一处,基函数归一化时用的采样点数算错,实际用了 (N-1) 而不是 (N),因为linspace默认包含终点,导致采样间隔是 (T/(N-1));第二处,星座点归一化到单位平均能量时,除的是峰值能量而不是平均能量;第三处,噪声方差按单边 (N_0) 加,但信号是按双边谱密度定义的。
三处错误单独看都是几个百分点,叠起来在低信噪比区放大成 dB 级的偏差。最终的解决办法是把整条链路的所有能量量纲写成一个显式的检查脚本,每次跑仿真前先跑一遍,检查项包括:基函数 Gram 矩阵是否为单位阵、每个信号坐标域能量和波形域能量之比是否为 1、星座平均能量是否等于设定值、噪声样本的实测方差是否等于理论值。这四项全过,误码率基本不会错。
6.3 一段能把整条链路跑通的验证代码
把前面所有环节串起来,这段代码可以直接拿去当模板改:
import numpy as np # ---------- 基础工具 ---------- T, N = 1.0, 512 t = np.linspace(0, T, N, endpoint=False) # endpoint=False 很关键 Ts = T / N def inner(a, b): return Ts * np.sum(np.conj(b) * a) def norm(a): return np.sqrt(np.real(inner(a, a))) def gram_schmidt(sigs, rel_tol=1e-10): basis = [] scale = max(norm(s) for s in sigs) for s in sigs: g = s.astype(complex).copy() for _ in range(2): # 重复正交化一轮,提升精度 for phi in basis: g = g - inner(g, phi) * phi n = norm(g) if n < rel_tol * scale: continue basis.append(g / n) return basis # ---------- 造三个基带信号 ---------- s1 = np.where(t < T/2, 1.0, 0.0) + 0j s2 = np.where(t >= T/2, 1.0, 0.0) + 0j s3 = s1 - s2 basis = gram_schmidt([s1, s2, s3]) print("基函数个数:", len(basis)) # 2 G = np.array([[inner(basis[i], basis[j]) for j in range(len(basis))] for i in range(len(basis))]) print("Gram 矩阵:\n", np.round(G, 10)) # 单位阵 # ---------- 坐标与能量核对 ---------- sigs = [s1, s2, s3] coords = np.array([[inner(s, phi) for phi in basis] for s in sigs]) E_wave = np.array([np.real(inner(s, s)) for s in sigs]) E_coord = np.sum(np.abs(coords)**2, axis=1) print("波形域能量:", np.round(E_wave, 6)) # [0.5 0.5 1.0] print("坐标域能量:", np.round(E_coord, 6)) # 应当一致 # ---------- 距离核对 ---------- d_wave = np.sqrt(np.real(inner(s1 - s2, s1 - s2))) d_coord = np.linalg.norm(coords[0] - coords[1]) print("距离(波形域/坐标域):", round(d_wave, 6), round(d_coord, 6)) # ---------- 16-QAM 星座与最小距离 ---------- levels = np.array([-3, -1, 1, 3], dtype=float) pts = np.array([[i, q] for q in levels for i in levels]) Es_raw = np.mean(np.sum(pts**2, axis=1)) # 10 D = np.linalg.norm(pts[:, None, :] - pts[None, :, :], axis=-1) np.fill_diagonal(D, np.inf) dmin_raw = D.min() # 2 print("原始平均能量/最小距离/d²/E:", Es_raw, dmin_raw, dmin_raw**2/Es_raw) # 10 2 0.4 pts_n = pts / np.sqrt(Es_raw) # 归一化到 Es = 1 dmin = dmin_raw / np.sqrt(Es_raw) print("归一化后 dmin²:", round(dmin**2, 6)) # 0.4这段代码的每一个print都是一个检查点,跑一遍就知道链路通没通。我个人习惯是把这些检查点固化成一个self_check()函数,在每次改动参数后自动跑,比事后对着曲线猜原因高效得多。
关于linspace的endpoint参数,再强调一次:默认endpoint=True时,采样点包含终点 (T),实际间隔变成 (T/(N-1)),和 (T_s = T/N) 的假设不符。这个差异只有千分之二,但会通过 (T_s) 一路传导到能量和噪声方差上,在低信噪比仿真里足以让曲线偏移零点几个 dB。写信号仿真代码时,endpoint=False应该当成肌肉记忆。
同样值得养成习惯的是重复正交化那一轮循环。修正 Gram-Schmidt 已经比经典版稳定,但对于条件数很差的信号集(比如几个几乎线性相关的扩频码),做两轮正交化几乎不增加成本,却能把基函数的正交误差压到机器精度。这段代码里我把for _ in range(2)写死了,实际用的时候可以根据 Gram 矩阵的非对角元大小决定要不要加第三轮。
整条链路走下来,最核心的认知其实只有一句:几何表示之所以成立,靠的是内积保距和噪声在正交补上无害这两条性质。基函数怎么选、坐标怎么算、距离怎么比,都是这两条性质的推论。把这层逻辑理顺了,公式就不再需要死记,遇到新的调制方式或者新的波形集,照着同样的流程走一遍就能得到正确结果。