diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..58dff15 --- /dev/null +++ b/REPORT.md @@ -0,0 +1,183 @@ +# 信道编码与信道均衡综合实验报告 + +## 一、实验目的 + +本实验围绕信道编码与信道均衡两个主题展开,目的是通过代码实现和结果分析,加深对数字通信基础方法的理解。具体包括以下内容: + +- 理解 Hamming(7,4) 信道编码的基本思想,掌握通过增加冗余提升传输可靠性的方法。 +- 掌握 syndrome 伴随式检测方法,理解接收端如何利用校验矩阵进行错误检测。 +- 掌握 Hamming(7,4) 单比特纠错流程,理解错误定位与纠正的实现方式。 +- 理解多径信道的物理含义,认识符号间干扰 ISI 对接收判决的影响。 +- 掌握 ZF 均衡器的基本设计思路,理解其抑制 ISI 的方法与局限。 +- 掌握 LMS 自适应均衡算法的更新过程,理解训练序列在均衡器收敛中的作用。 + +## 二、实验原理 + +### 2.1 Hamming(7,4) 信道编码 + +Hamming(7,4) 是一种典型的线性分组码,将 4 位信息比特编码为 7 位码字。与直接发送原始比特相比,它额外加入了 3 位冗余校验比特,因此能够在接收端具备一定的检错和纠错能力。 + +本实验采用生成矩阵进行编码,编码公式为: + +$$ +c = uG \bmod 2 +$$ + +其中,`u` 表示长度为 4 的信息比特向量,`G` 为生成矩阵,`c` 为长度为 7 的编码码字。通过这种方式,可以把信息位和校验位统一表示为矩阵运算,便于程序实现。 + +### 2.2 Syndrome 伴随式与单比特纠错 + +接收端对收到的码字使用校验矩阵 `H` 计算 syndrome,公式为: + +$$ +s = rH^T \bmod 2 +$$ + +其中,`r` 为接收码字,`s` 为伴随式。如果 `s=0`,则认为没有检测到单比特错误;如果 `s` 非零,则说明接收码字很可能出现了错误。 + +在 Hamming(7,4) 码中,非零 syndrome 可以与校验矩阵 `H` 的各列进行比较。若某一列与 syndrome 完全一致,就可以判定对应位置发生了单比特错误,再将该比特翻转完成纠错。由于本实验使用的是系统码结构,纠错后取码字前 4 位即可作为译码输出。 + +### 2.3 多径信道与 ISI + +无线信道中,信号通常不会只沿一条路径到达接收端,而是会经过反射、绕射、散射等过程沿多条路径传播。不同路径具有不同的时延和衰减,因此接收端实际接收到的是多个延迟分量的叠加。 + +当一个符号的尾部延伸到后续符号时,就会对相邻符号产生干扰,这种现象称为符号间干扰 ISI。ISI 会使接收波形失真,判决边界变得模糊,从而降低检测准确率并增大误码率。 + +### 2.4 ZF 均衡 + +ZF(Zero-Forcing)均衡器的目标是让“信道 + 均衡器”的整体响应尽量接近一个理想冲激响应,从而压制 ISI。实验中通过构造卷积矩阵,将均衡器设计问题写成线性方程组,并使用最小二乘方法估计均衡器抽头系数。 + +ZF 的优点是思路直接、实现清晰,对抑制码间干扰比较有效;但它也存在明显局限。当信道某些频率分量较弱时,ZF 为了强行补偿失真,可能会把噪声一同放大,因此在低信噪比条件下效果不一定稳定。 + +### 2.5 LMS 自适应均衡 + +LMS(Least Mean Squares)是一种基于训练序列的自适应均衡方法。它不直接求解析最优解,而是在每个时刻根据当前误差逐步更新抽头,使均衡器输出逐渐逼近期望信号。 + +其核心关系为: + +- 输出:`y = taps @ x` +- 误差:`e = d - y` +- 更新:`taps = taps + step_size * e * x` + +其中,`x` 为当前输入向量,`d` 为期望发送符号,`step_size` 为步长。步长过大可能导致震荡,过小则会使收敛速度变慢。LMS 的优点是实现简单、适应性较强,适合处理实际信道变化。 + +## 三、实验环境 + +- 操作系统:Windows +- Python 版本:Python 3.13.5 +- 主要依赖:NumPy、SciPy、Matplotlib、pytest +- 开发工具:VS Code / PowerShell / GitHub + +如需与本次实验完全一致,建议根据本地实际环境再次核对依赖版本。 + +## 四、实验方法 + +### 4.1 环境配置 + +首先根据仓库说明安装依赖,并检查实验环境是否正常。使用的命令如下: + +```bash +pip install -r requirements.txt +python src/test_environment.py +``` + +完成环境配置后,再分别进入 Part 1 与 Part 2 的函数补全和结果验证。 + +### 4.2 Part 1:Hamming(7,4) 编码与译码 + +本部分在 `src/part1_channel_coding.py` 中补全了以下函数: + +- `hamming74_encode(bits)` +- `hamming74_syndrome(codewords)` +- `hamming74_decode(received)` + +实现过程中,先利用生成矩阵完成 Hamming(7,4) 编码,再通过校验矩阵计算 syndrome;若 syndrome 非零,则与 `H` 的列向量比较以定位错误位,并进行单比特纠错。 + +完成代码后,使用以下命令进行测试与实验运行: + +```bash +python -m pytest grading/test_part1_coding.py -v +python src/part1_channel_coding.py +``` + +### 4.3 Part 2:ZF 与 LMS 信道均衡 + +本部分在 `src/part2_equalization.py` 中补全了以下函数: + +- `estimate_zf_equalizer(channel, num_taps)` +- `apply_fir_filter(signal, taps)` +- `lms_equalizer(rx_train, tx_train, num_taps, step_size)` + +其中,ZF 均衡器通过最小二乘法估计 FIR 抽头;`apply_fir_filter` 用于完成等长滤波输出;LMS 均衡器则利用训练序列不断更新权值,观察误差随迭代的变化。 + +完成代码后,使用以下命令进行测试与实验运行: + +```bash +python -m pytest grading/test_part2_equalization.py -v +python src/part2_equalization.py +``` + +## 五、实验结果 + +### 5.1 Hamming(7,4) 编码 BER 曲线 + +![编码BER曲线](results/coding_ber_curve.png) + +从结果图可以看出,未编码传输和 Hamming(7,4) 编码传输的 BER 随信道误码概率变化而变化。一般来说,在误码概率较低到中等的范围内,Hamming(7,4) 编码后的 BER 会低于未编码情况,说明适当增加冗余后,系统具备了更强的抗错能力。 + +出现这种改善的原因是 Hamming(7,4) 可以对单比特错误进行定位和纠正。当每个码字中只出现一个错误时,接收端能够利用 syndrome 找到错误位置并恢复正确比特,因此整体误码率会下降。 + +不过也应注意,Hamming(7,4) 的主要能力是单比特纠错。如果同一码字中出现两个或更多错误,译码结果可能无法纠正,甚至可能产生误判。因此当信道误码率进一步升高时,其性能提升会受到限制。 + +### 5.2 均衡前后眼图对比 + +![均衡眼图对比](results/equalization_eye_comparison.png) + +从眼图对比可以看到,均衡前的接收信号受到多径和 ISI 影响,眼图张开程度相对较差,判决区域不够清晰。符号之间的波形相互叠加后,接收端更容易出现误判。 + +经过均衡后,眼图开口有所改善,说明均衡器在一定程度上抑制了符号间干扰。眼图越开阔,通常表示采样时刻的判决裕量越大,系统对噪声和失真的容忍能力也更强。 + +### 5.3 LMS 均衡误差曲线 + +![LMS误差曲线](results/equalization_mse_curve.png) + +根据实验运行结果: + +- 均衡前 BER: 0.0010 +- LMS 均衡后 BER: 0.0000 + +从这一结果可以看出,LMS 均衡后接收信号质量得到了明显改善。在本次实验参数下,均衡器输出已经足以使后续 BPSK 判决不再出现误比特,因此 BER 降低到了 0。 + +同时,从误差曲线来看,如果 MSE 随训练过程整体呈下降趋势,就说明 LMS 更新过程是逐步收敛的,均衡器抽头正在向更合适的方向调整。若你后续重新运行代码得到的曲线细节略有差异,可结合实际图像进行补充说明。 + +## 六、结果分析 + +1. Hamming(7,4) 能提升通信可靠性,原因在于它通过增加冗余校验位,让接收端不仅能传输信息,还能利用结构化冗余来检测并纠正部分错误。对单比特错误而言,这种编码方式能明显降低最终误码率。 + +2. syndrome 的作用在于把“是否出错”和“错误在哪里”转化为矩阵运算结果。若 syndrome 为零,说明未检测到单比特错误;若 syndrome 非零,则可通过与 `H` 的列向量比较完成错误定位。 + +3. 多径信道会产生 ISI,是因为一个符号经过不同路径传播后,会以不同延迟叠加到接收端。这样前一个符号的残留成分会干扰后一个符号,导致接收波形失真。 + +4. ZF 和 LMS 的区别在于:ZF 更像一次性根据已知信道求一个近似逆系统,思路直接,但容易放大噪声;LMS 则依靠训练序列逐步更新抽头,适应性更强,更适合在未知或变化信道中使用。 + +5. 本实验中 LMS 均衡后 BER 从 0.0010 降到 0.0000,说明在当前信道响应、噪声水平和训练长度条件下,LMS 均衡器已经有效抑制了 ISI,使判决性能得到明显改善。这也说明所选步长和抽头数在本次实验中是有效的。 + +6. 本实验结果还会受到多个因素影响,例如信噪比越低,误码率通常越高;信道冲激响应越复杂,均衡难度越大;均衡器抽头数过少可能补偿不足,过多则会增加复杂度;LMS 步长设置不当会影响收敛速度和稳定性。这些因素都会影响最终 BER 和图像表现。 + +## 七、AI 助手使用说明 + +本实验中使用 AI 助手辅助理解 Hamming(7,4) 编码、syndrome 纠错、ZF 均衡和 LMS 更新公式,并辅助检查代码实现思路和分析实验结果。代码最终经过本地 `pytest` 测试和实验脚本运行验证,报告内容根据实际实验结果整理。 + +## 八、实验心得 + +通过本次实验,我对信道编码和信道均衡的基本思路有了更具体的认识。Hamming(7,4) 部分让我理解了通信系统如何通过增加冗余提升可靠性,也掌握了编码、syndrome 检测和单比特纠错的完整流程。 + +在均衡部分,我进一步理解了多径信道产生 ISI 的原因,并通过实现 ZF 与 LMS 两类均衡方法,体会了固定均衡与自适应均衡在思路上的差异。结合实验图像和 BER 结果,可以更直观地看到均衡对接收信号质量的改善作用。 + +此外,本实验还让我熟悉了使用 GitHub、`pytest` 和自动评分流程完成代码实验的基本方法,对后续课程实验和工程实践都有帮助。 + +## 九、参考资料 + +- 课程课件:第 6 章 信道编码 +- 课程课件:第 7 章 均衡 +- 本实验 GitHub 仓库中的 `README.md`、`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..5072b13 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) + codewords = (blocks @ HAMMING_G) % 2 + return codewords.astype(int).reshape(-1) def hamming74_syndrome(codewords): @@ -70,8 +71,8 @@ def hamming74_syndrome(codewords): if codewords.shape[1] != 7: raise ValueError('每个 Hamming(7,4) 码字长度必须为 7') - # TODO: 计算 s = r H^T mod 2。 - raise NotImplementedError('请实现伴随式计算') + syndrome = (codewords @ HAMMING_H.T) % 2 + return syndrome.astype(int) def hamming74_decode(received): @@ -94,8 +95,18 @@ 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) 译码') + codewords = received.reshape(-1, 7).copy() + syndromes = hamming74_syndrome(codewords) + + for index, syndrome in enumerate(syndromes): + if np.all(syndrome == 0): + continue + matches = np.all(HAMMING_H.T == syndrome, axis=1) + if np.any(matches): + error_position = int(np.argmax(matches)) + codewords[index, error_position] ^= 1 + + return codewords[:, :4].astype(int).reshape(-1) def convolutional_encode(bits): @@ -108,8 +119,20 @@ def convolutional_encode(bits): if not np.all((bits == 0) | (bits == 1)): raise ValueError('bits 只能包含 0 或 1') - # TODO: 选做任务,可参考课件第6章卷积码部分。 - raise NotImplementedError('选做:请实现卷积码编码') + if bits.ndim != 1: + raise ValueError('bits 必须是一维数组') + + state = np.zeros(2, dtype=int) + padded_bits = np.concatenate([bits, np.zeros(2, dtype=int)]) + encoded = [] + + for bit in padded_bits: + register = np.array([bit, state[0], state[1]], dtype=int) + encoded.append((register[0] + register[1] + register[2]) % 2) + encoded.append((register[0] + register[2]) % 2) + state = register[:2] + + return np.asarray(encoded, dtype=int) def viterbi_decode_hard(received_bits): @@ -120,8 +143,62 @@ def viterbi_decode_hard(received_bits): if len(received_bits) % 2 != 0: raise ValueError('卷积码接收序列长度必须是 2 的倍数') - # TODO: 选做任务,可使用汉明距离作为路径度量。 - raise NotImplementedError('选做:请实现 Viterbi 硬判决译码') + if received_bits.ndim != 1: + raise ValueError('received_bits 必须是一维数组') + if not np.all((received_bits == 0) | (received_bits == 1)): + raise ValueError('received_bits 只能包含 0 或 1') + + symbols = received_bits.reshape(-1, 2) + num_steps = symbols.shape[0] + num_states = 4 + inf = np.inf + + next_states = np.zeros((num_states, 2), dtype=int) + outputs = np.zeros((num_states, 2, 2), dtype=int) + + for state in range(num_states): + memory = np.array([(state >> 1) & 1, state & 1], dtype=int) + for bit in (0, 1): + register = np.array([bit, memory[0], memory[1]], dtype=int) + next_states[state, bit] = bit * 2 + memory[0] + outputs[state, bit] = np.array([ + (register[0] + register[1] + register[2]) % 2, + (register[0] + register[2]) % 2, + ], dtype=int) + + path_metrics = np.full(num_states, inf) + path_metrics[0] = 0 + predecessors = np.full((num_steps, num_states), -1, dtype=int) + decisions = np.zeros((num_steps, num_states), dtype=int) + + for step in range(num_steps): + new_metrics = np.full(num_states, inf) + for state in range(num_states): + if np.isinf(path_metrics[state]): + continue + for bit in (0, 1): + next_state = next_states[state, bit] + branch_metric = np.sum(outputs[state, bit] != symbols[step]) + metric = path_metrics[state] + branch_metric + if metric < new_metrics[next_state]: + new_metrics[next_state] = metric + predecessors[step, next_state] = state + decisions[step, next_state] = bit + path_metrics = new_metrics + + final_state = 0 if not np.isinf(path_metrics[0]) else int(np.argmin(path_metrics)) + decoded = np.zeros(num_steps, dtype=int) + state = final_state + + for step in range(num_steps - 1, -1, -1): + decoded[step] = decisions[step, state] + state = predecessors[step, state] + if state < 0 and step > 0: + raise ValueError('Viterbi 回溯失败,请检查输入序列') + + if num_steps < 2: + return np.array([], dtype=int) + return decoded[:-2].astype(int) def run_coding_demo(): diff --git a/src/part2_equalization.py b/src/part2_equalization.py index 8cbf1d8..9e3eaea 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 均衡器估计') + total_length = len(channel) + num_taps - 1 + a_matrix = np.zeros((total_length, num_taps), dtype=float) + + for tap_index in range(num_taps): + a_matrix[tap_index : tap_index + len(channel), tap_index] = channel + + desired = np.zeros(total_length, dtype=float) + desired[total_length // 2] = 1.0 + taps, _, _, _ = np.linalg.lstsq(a_matrix, desired, rcond=None) + return taps def apply_fir_filter(signal, taps): @@ -58,8 +66,8 @@ 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 滤波') + filtered = np.convolve(signal, taps, mode='full') + return filtered[: len(signal)] def lms_equalizer(rx_train, tx_train, num_taps, step_size=0.01): @@ -89,8 +97,18 @@ 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 均衡器') + taps = np.zeros(num_taps, dtype=float) + taps[num_taps // 2] = 1.0 + errors = [] + + for index in range(num_taps - 1, len(rx_train)): + x = rx_train[index - num_taps + 1 : index + 1][::-1] + y = taps @ x + error = tx_train[index] - y + taps = taps + step_size * error * x + errors.append(error) + + return taps, np.asarray(errors, dtype=float) def run_equalization_demo():