diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..1db6f57 --- /dev/null +++ b/REPORT.md @@ -0,0 +1,90 @@ +# 无线通信技术实验报告:信道编码与信道均衡 + +- 姓名:徐雷 +- 学号:2022090123 +- 实验名称:信道编码与信道均衡综合实验 + +## 1. 实验目的 + +本实验通过补全 Python 仿真代码,理解无线通信系统中信道编码和信道均衡的基本作用。Part 1 使用 Hamming(7,4) 线性分组码完成编码、伴随式检测和单比特纠错,观察冗余校验位对误比特率 BER 的改善。Part 2 面向多径信道造成的符号间干扰,实现 ZF 迫零均衡器、FIR 滤波和 LMS 自适应均衡器,观察均衡前后波形、误差曲线和 BER 变化。同时,本实验还练习了 pytest 本地测试、结果图生成和 GitHub 自动评分流程。 + +## 2. 实验原理 + +### 2.1 信道编码 + +Hamming(7,4) 码每 4 个信息比特生成 7 个码字比特,其中前 4 位为信息位,后 3 位为校验位。本实验使用系统码生成矩阵 `HAMMING_G`,编码过程是在 GF(2) 上计算 `c = uG mod 2`。接收端使用校验矩阵 `HAMMING_H` 计算伴随式 `s = rH^T mod 2`。 + +如果伴随式为全零,说明没有检测到单比特错误;如果伴随式非零,则将它与 `HAMMING_H` 的各列比较。由于 Hamming(7,4) 的每一列对应一个码字位置,匹配到的列号就是错误比特位置,翻转该位置即可纠正单比特错误。Hamming(7,4) 通过增加 3 个冗余位提升可靠性,但码率从 1 降为 4/7,因此会牺牲一部分传输效率。 + +### 2.2 信道均衡 + +多径信道可表示为 `r[n] = sum h[k]s[n-k] + w[n]`。当多个延迟路径叠加时,当前接收符号会包含相邻符号的影响,形成符号间干扰 ISI。均衡器的目标是设计 FIR 滤波器,使信道和均衡器的整体卷积响应尽量接近单位冲激。 + +ZF 均衡器通过求解 `A @ taps ≈ d` 得到抽头,其中 `A` 是由信道冲激响应构造的卷积矩阵,`d` 是中心位置为 1 的目标冲激响应。ZF 能压低 ISI,但如果信道频谱存在深衰落,可能把噪声一并放大。 + +LMS 均衡器是一种自适应方法。每次迭代取当前接收样本和过去样本组成输入向量 `x[n]`,计算输出 `y[n] = w^T x[n]`,再用训练符号得到误差 `e[n] = d[n] - y[n]`,最后按 `w = w + μ e[n] x[n]` 更新抽头。步长 `μ` 过小会导致收敛慢,过大则可能振荡甚至发散。 + +## 3. 实验环境 + +- Python 版本:3.13.5 +- 主要依赖:NumPy 2.1.3、SciPy 1.15.3、Matplotlib 3.10.0、pytest 8.3.4 +- 操作系统环境:Windows PowerShell +- AI 助手使用情况:解释 Hamming(7,4)、ZF 和 LMS 的实现思路,并在本地运行 pytest 与评分脚本验证结果。最终提交内容经过本地测试和结果检查。 + +## 4. 实验方法与步骤 + +### 4.1 Part 1:信道编码 + +首先将输入比特序列转换为一维整数数组,并检查长度是否为 4 的倍数。然后把比特按每 4 位分组,计算 `(blocks @ HAMMING_G) % 2` 得到编码码字。接收端先将长度为 7 的倍数的接收序列 reshape 成码字矩阵,再计算 `(codewords @ HAMMING_H.T) % 2` 得到伴随式。译码时,对于每个非零伴随式,逐列匹配 `HAMMING_H`,找到错误位置并翻转,最后取每个码字的前 4 位作为译码结果。 + +实验脚本中对原始比特和 Hamming 编码比特分别通过二元对称信道,比较未编码 BER 和编码后译码 BER,并生成 BER 曲线。 + +### 4.2 Part 2:信道均衡 + +ZF 均衡部分先根据信道响应构造卷积矩阵,使矩阵乘法结果等价于 `np.convolve(channel, taps)`,再以中心冲激为目标用最小二乘法求解均衡器抽头。FIR 滤波部分使用 `np.convolve(signal, taps, mode='full')`,并截取前 `len(signal)` 个样本,使输出长度和输入一致。 + +LMS 均衡部分将抽头初始化为中心抽头为 1,其余为 0。从第 `num_taps - 1` 个样本开始,每次取当前及过去接收样本组成输入窗口,计算输出、误差并更新抽头。训练完成后,将学到的抽头应用到完整接收序列,比较均衡前后的 BER,并绘制波形对比和误差曲线。 + +## 5. 实验结果 + +![编码BER曲线](results/coding_ber_curve.png) + +![均衡眼图对比](results/equalization_eye_comparison.png) + +![LMS误差曲线](results/equalization_mse_curve.png) + +Part 1 的 BER 统计结果如下: + +| 信道翻转概率 | 未编码 BER | Hamming(7,4) BER | +| --- | ---: | ---: | +| 0.001 | 0.00075 | 0.00000 | +| 0.003 | 0.00300 | 0.00000 | +| 0.010 | 0.01125 | 0.00000 | +| 0.030 | 0.02925 | 0.00700 | +| 0.060 | 0.05775 | 0.01950 | +| 0.100 | 0.10100 | 0.06800 | + +Part 2 中,均衡前 BER 为 0.00100,LMS 均衡后 BER 为 0.00000。LMS 训练误差前 100 次迭代均方误差约为 1.05550,最后 100 次约为 0.03828。ZF 等效响应主峰约为 0.94440,旁瓣能量约为 0.05251。 + +## 6. 结果分析与讨论 + +Hamming(7,4) 能纠正单比特错误,是因为任意一个码字位置翻转都会产生唯一的非零伴随式,该伴随式正好等于校验矩阵的对应列。因此译码器可以根据 syndrome 直接定位错误位置。在较低信道翻转概率下,一个 7 位码字中出现两位及以上错误的概率较低,单比特纠错能力能够显著降低 BER。随着翻转概率升高,多比特错误增多,Hamming(7,4) 不能全部纠正,因此编码后 BER 仍会上升。 + +信道编码的代价是引入冗余。本实验中每 4 个信息比特需要发送 7 个码字比特,码率为 4/7。冗余位本身不携带新的用户信息,但提供了检错和纠错能力,这体现了可靠性和传输效率之间的折中。 + +均衡实验中,多径信道使接收波形相对发送符号出现拖尾和叠加。ZF 均衡通过最小二乘方式让等效冲激响应接近中心单峰,测试中主峰明显高于旁瓣,说明 ISI 被压制。但 ZF 只从消除 ISI 的角度设计,如果某些频率分量被信道严重衰减,逆滤波会放大这些频率上的噪声。 + +LMS 的误差曲线整体下降,说明抽头通过训练序列逐步逼近可用的均衡器。步长太小时,更新幅度小,训练需要更多符号才能收敛;步长太大时,抽头可能在最优值附近振荡,甚至使误差发散。本实验步长 0.01 在当前信道和训练长度下能稳定收敛,均衡后 BER 降到 0。 + +## 7. 实验心得 + +本实验把课件中的矩阵编码、伴随式译码和自适应滤波公式落实到可运行代码中。Hamming(7,4) 的实现重点在于所有矩阵运算都必须在 GF(2) 中进行,译码时 syndrome 与校验矩阵列的对应关系非常关键。均衡部分的重点在于时序对齐:FIR 输出、ZF 卷积矩阵和 LMS 输入窗口必须采用一致的因果卷积约定,否则即使公式正确,误差也可能无法下降。 + +通过 pytest 和本地评分脚本,可以及时发现函数签名、返回形状和边界输入问题。AI 工具适合用于解释算法、检查实现思路和辅助调试,但实验结果必须通过本地运行、图像生成和评分脚本验证,不能只依赖生成代码本身。 + +## 8. 参考资料 + +- 课程课件:第 6 章 信道编码 +- 课程课件:第 7 章 均衡 +- 仓库文档:`docs/theory_channel_coding.md` +- 仓库文档:`docs/theory_equalization.md` diff --git a/results/coding_ber_curve.png b/results/coding_ber_curve.png new file mode 100644 index 0000000..455949d Binary files /dev/null and b/results/coding_ber_curve.png differ diff --git a/results/equalization_eye_comparison.png b/results/equalization_eye_comparison.png new file mode 100644 index 0000000..006f80c Binary files /dev/null and b/results/equalization_eye_comparison.png differ diff --git a/results/equalization_mse_curve.png b/results/equalization_mse_curve.png new file mode 100644 index 0000000..4a3d5e5 Binary files /dev/null and b/results/equalization_mse_curve.png differ diff --git a/src/part1_channel_coding.py b/src/part1_channel_coding.py index 1ecf55e..b640a51 100644 --- a/src/part1_channel_coding.py +++ b/src/part1_channel_coding.py @@ -48,8 +48,9 @@ def hamming74_encode(bits): if not np.all((bits == 0) | (bits == 1)): raise ValueError('bits 只能包含 0 或 1') - # TODO: 将 bits reshape 为 (-1, 4),再与 HAMMING_G 相乘并对 2 取模。 - raise NotImplementedError('请实现 Hamming(7,4) 编码') + blocks = bits.reshape(-1, 4) + encoded = (blocks @ HAMMING_G) % 2 + return encoded.reshape(-1) def hamming74_syndrome(codewords): @@ -70,8 +71,12 @@ def hamming74_syndrome(codewords): if codewords.shape[1] != 7: raise ValueError('每个 Hamming(7,4) 码字长度必须为 7') - # TODO: 计算 s = r H^T mod 2。 - raise NotImplementedError('请实现伴随式计算') + if codewords.ndim != 2: + raise ValueError('codewords 必须是一维或二维数组') + if not np.all((codewords == 0) | (codewords == 1)): + raise ValueError('codewords 只能包含 0 或 1') + + return (codewords @ HAMMING_H.T) % 2 def hamming74_decode(received): @@ -94,8 +99,22 @@ def hamming74_decode(received): if received.ndim != 1 or len(received) % 7 != 0: raise ValueError('received 必须是一维数组,长度为 7 的倍数') - # TODO: 使用 hamming74_syndrome 完成单比特纠错,并返回前 4 个信息位。 - raise NotImplementedError('请实现 Hamming(7,4) 译码') + if not np.all((received == 0) | (received == 1)): + raise ValueError('received 只能包含 0 或 1') + + codewords = received.reshape(-1, 7).copy() + syndromes = hamming74_syndrome(codewords) + syndrome_table = HAMMING_H.T + + for row_index, syndrome in enumerate(syndromes): + if np.all(syndrome == 0): + continue + matches = np.where(np.all(syndrome_table == syndrome, axis=1))[0] + if len(matches) == 1: + error_position = matches[0] + codewords[row_index, error_position] ^= 1 + + return codewords[:, :4].reshape(-1) def convolutional_encode(bits): @@ -152,11 +171,11 @@ def run_coding_demo(): 'Hamming(7,4) 编码前后 BER 对比', 'coding_ber_curve.png', ) - print('✅ 已生成 results/coding_ber_curve.png') + print('[OK] 已生成 results/coding_ber_curve.png') except NotImplementedError as error: - print(f'⏸️ 尚未完成核心函数:{error}') + print(f'[TODO] 尚未完成核心函数:{error}') except Exception as error: - print(f'❌ Part 1 运行失败:{error}') + print(f'[ERROR] Part 1 运行失败:{error}') if __name__ == '__main__': diff --git a/src/part2_equalization.py b/src/part2_equalization.py index 8cbf1d8..09c91a1 100644 --- a/src/part2_equalization.py +++ b/src/part2_equalization.py @@ -38,8 +38,16 @@ def estimate_zf_equalizer(channel, num_taps): if num_taps < 1: raise ValueError('num_taps 必须为正整数') - # TODO: 构造卷积矩阵并求解 ZF 均衡器抽头。 - raise NotImplementedError('请实现 ZF 均衡器估计') + output_length = len(channel) + num_taps - 1 + matrix = np.zeros((output_length, num_taps), dtype=float) + for tap_index in range(num_taps): + matrix[tap_index:tap_index + len(channel), tap_index] = channel + + desired = np.zeros(output_length, dtype=float) + desired[output_length // 2] = 1.0 + + taps, *_ = np.linalg.lstsq(matrix, desired, rcond=None) + return taps def apply_fir_filter(signal, taps): @@ -58,8 +66,7 @@ def apply_fir_filter(signal, taps): if signal.ndim != 1 or taps.ndim != 1: raise ValueError('signal 和 taps 必须是一维数组') - # TODO: 使用 np.convolve,并截取与 signal 等长的输出。 - raise NotImplementedError('请实现 FIR 滤波') + return np.convolve(signal, taps, mode='full')[: len(signal)] def lms_equalizer(rx_train, tx_train, num_taps, step_size=0.01): @@ -89,8 +96,23 @@ def lms_equalizer(rx_train, tx_train, num_taps, step_size=0.01): if num_taps < 1: raise ValueError('num_taps 必须为正整数') - # TODO: 实现 LMS 自适应均衡训练。 - raise NotImplementedError('请实现 LMS 均衡器') + if rx_train.ndim != 1 or tx_train.ndim != 1: + raise ValueError('rx_train 和 tx_train 必须是一维数组') + if len(rx_train) < num_taps: + raise ValueError('训练序列长度必须不小于 num_taps') + + taps = np.zeros(num_taps, dtype=float) + taps[num_taps // 2] = 1.0 + errors = [] + + for sample_index in range(num_taps - 1, len(rx_train)): + window = rx_train[sample_index - num_taps + 1:sample_index + 1][::-1] + output = float(taps @ window) + error = tx_train[sample_index] - output + taps = taps + step_size * error * window + errors.append(error) + + return taps, np.asarray(errors, dtype=float) def run_equalization_demo(): @@ -106,7 +128,7 @@ def run_equalization_demo(): rx = multipath_channel(symbols, channel, noise_std=0.12, seed=7) zf_taps = estimate_zf_equalizer(channel, num_taps=7) - zf_output = apply_fir_filter(rx, zf_taps) + apply_fir_filter(rx, zf_taps) lms_taps, errors = lms_equalizer(rx[:800], symbols[:800], num_taps=7, step_size=0.01) lms_output = apply_fir_filter(rx, lms_taps) @@ -118,11 +140,11 @@ def run_equalization_demo(): plot_equalization_results(symbols, rx, lms_output, 'equalization_eye_comparison.png') plot_mse_curve(errors, 'equalization_mse_curve.png') - print('✅ 已生成均衡结果图') + print('[OK] 已生成均衡结果图') except NotImplementedError as error: - print(f'⏸️ 尚未完成核心函数:{error}') + print(f'[TODO] 尚未完成核心函数:{error}') except Exception as error: - print(f'❌ Part 2 运行失败:{error}') + print(f'[ERROR] Part 2 运行失败:{error}') if __name__ == '__main__':