从连续波形到星座坐标:信号空间几何表示与仿真避坑
信号处理这条线上有个挺有意思的现象很多人能把 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_{i1}^{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) 维欧氏空间里做判决。压缩是有损的损失的部分无害。理解了这一层“信号在几何空间中的表示”就从数学游戏变成了工程合理性。顺便提一句这套推理对任何完备正交基都成立包括后面要讲的匹配滤波器输出采样——匹配滤波器在 (tT) 时刻的输出值恰好就是 (\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-SchmidtMGS在浮点运算下精度明显更好。做仿真时用 MGS 写法几乎是零成本没有理由不用。2.2 手算一组三脉冲信号的完整过程拿一个具体的例子走一遍比看十遍公式有用。设 (T1)三个信号定义在 ([0,T]) 上(s_1(t) 1)(0\le tT/2)其余为 0(s_2(t) 1)(T/2\le tT)其余为 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/201/2)(01/21/2)(1/21/21)和波形域直接积分的结果完全一致。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))输出在 (tT) 时刻的值是 (\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_{n0}^{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, endpointFalse) 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_{i1}^{N}|s_i|^2 ]这就是帕塞瓦尔等式在信号空间里的表现。它的实用价值在于你可以只在坐标域算能量不用回头去积分波形。而且反过来还提供了一条核验路径——如果两个域算出来的能量对不上说明基不正交、或者基不完备漏掉了有能量的方向或者系数算错了。我处理过一个典型的“基不完备”案例一组信号里有两个是不同时移的升余弦脉冲做 Gram-Schmidt 只得到了一个基函数能量核对时发现波形域能量加和远大于坐标域。原因不是 bug而是那组信号的时移量恰好让两个脉冲完全正交、且都属于同一个二维子空间但我的采样窗口截断导致正交性被破坏实际需要两个基。把采样窗口从一个符号周期扩到三个符号周期问题消失。这件事说明能量核对表不是走过场它能暴露采样窗口、截断这类结构性错误。两信号之间的距离平方同理[ d^2(s_1,s_2) \int_0^T |s_1(t)-s_2(t)|^2dt \sum_{i1}^{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^2Q^218)平均能量只有 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_s1.6E_b)3内部 4、边上 3、角上 2均值 3M-PSK(4E_s\sin^2(\pi/M))2圆上相邻两点16-QAM 那行的平均邻居数可以自己算16 个点里 4 个内部点有 4 个邻居8 个边点有 3 个4 个角点有 2 个((4\times48\times34\times2)/1648/163)。这个数字不是随便写的直接决定近似的系数。用这些量还能算功率效率。以 (E_b/N_010) dB 为例16-QAM 的 (E_s4E_b)(d_{\min}^21.6E_b)于是 (d_{\min}^2/(2N_0)0.8E_b/N_08)(\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, endpointFalse) # endpointFalse 很关键 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_tol1e-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, axis1) 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], dtypefloat) pts np.array([[i, q] for q in levels for i in levels]) Es_raw np.mean(np.sum(pts**2, axis1)) # 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参数再强调一次默认endpointTrue时,采样点包含终点 (T)实际间隔变成 (T/(N-1))和 (T_s T/N) 的假设不符。这个差异只有千分之二但会通过 (T_s) 一路传导到能量和噪声方差上在低信噪比仿真里足以让曲线偏移零点几个 dB。写信号仿真代码时endpointFalse应该当成肌肉记忆。同样值得养成习惯的是重复正交化那一轮循环。修正 Gram-Schmidt 已经比经典版稳定但对于条件数很差的信号集比如几个几乎线性相关的扩频码做两轮正交化几乎不增加成本却能把基函数的正交误差压到机器精度。这段代码里我把for _ in range(2)写死了实际用的时候可以根据 Gram 矩阵的非对角元大小决定要不要加第三轮。整条链路走下来最核心的认知其实只有一句几何表示之所以成立靠的是内积保距和噪声在正交补上无害这两条性质。基函数怎么选、坐标怎么算、距离怎么比都是这两条性质的推论。把这层逻辑理顺了公式就不再需要死记遇到新的调制方式或者新的波形集照着同样的流程走一遍就能得到正确结果。