diff --git a/.gitignore b/.gitignore index f9d594a..e6d6b19 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ venv/ .pytest_cache/ .pylint.d/ .DS_Store -results/*.png + results/*.csv results/*.json !results/.gitkeep diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..da2640d --- /dev/null +++ b/REPORT.md @@ -0,0 +1,95 @@ +# 无线通信技术实验报告:信道编码与信道均衡 + +## 1. 实验目的 + +1. 理解信道编码原理,包括 Hamming(7,4) 分组码的编码、伴随式计算和单比特纠错机制,体会编码增益的概念。 +2. 掌握信道均衡技术,理解码间干扰的产生机理,学习迫零均衡器和 LMS 自适应均衡器的设计方法。 +3. 熟悉 GitHub 自动评分流程,包括 Fork 仓库、本地开发、pytest 测试和自动评分的完整工作流。 +4. 实践 Python 科学计算,运用 NumPy 进行矩阵运算和信号处理,使用 Matplotlib 可视化实验结果。 + +## 2. 实验原理 + +### 2.1 信道编码 + +1. Hamming(7,4) 码是一种线性分组码,将 4 位信息比特编码为 7 位码字,引入 3 位监督比特。其生成矩阵 G 为 4 乘 7 矩阵,满足 c 等于 m 点乘 G 后对 2 取模。 +2. 伴随式的定义为:设接收码字为 r,校验矩阵为 H,则伴随式 s 等于 r 点乘 H 的转置后对 2 取模。若 s 等于 0,认为无错;若 s 不等于 0,则 s 对应 H 的某一列,指示错误位置。 +3. 单比特纠错的原理:Hamming(7,4) 码的最小汉明距离 d_min 等于 3,根据编码理论,可纠正的错误位数 t 等于 d_min 减 1 后除以 2 再向下取整,结果为 1,因此能纠正任意单比特错误。 +4. 编码增益是指通过引入冗余比特降低误码率,在相同信噪比下获得性能提升,代价是码率从 1 降至 4 除以 7。 + +### 2.2 信道均衡 + +1. 码间干扰的产生原因是多径信道导致信号经过不同路径到达接收端,产生时延扩展,造成前后符号相互干扰。 +2. 迫零均衡器的设计思路是通过设计 FIR 滤波器 w,使得 h 与 w 的卷积近似等于冲激函数 delta[n],强制消除码间干扰。具体通过求解最小二乘问题得到,其中 A 为卷积矩阵,d 为中心为 1 的目标向量。 +3. LMS 自适应均衡利用训练序列迭代更新抽头系数,更新公式为 w[n+1] 等于 w[n] 加上步长 mu 乘以误差 e[n] 再乘以输入向量 x[n]。其中 e[n] 等于期望输出 d[n] 减去实际输出 y[n]。 + +## 3. 实验环境 + +1. Python 版本为 Python 3.x。 +2. 主要依赖包括 NumPy 用于矩阵运算,Matplotlib 用于绘图,pytest 用于单元测试。 +3. AI 助手使用情况为使用 AI 辅助理解实验要求、调试代码逻辑、解释通信原理概念。 + +## 4. 实验方法与步骤 + +### 4.1 Part 1:信道编码 + +1. 编码,实现 hamming74_encode 函数,将输入比特按每 4 位分组,通过生成矩阵 G 在 GF(2) 上相乘得到 7 位码字。 +2. 加噪与翻转,模拟 BSC 信道,以概率 p 随机翻转码字比特;或模拟 AWGN 信道加高斯噪声后硬判决。 +3. 译码,实现 hamming74_syndrome 函数计算伴随式定位错误,hamming74_decode 函数翻转错误位并提取前 4 位信息比特。 +4. 误码率对比,对比编码前后的误码率,绘制误码率与信噪比关系曲线。 + +### 4.2 Part 2:信道均衡 + +1. 多径信道建模,给定信道冲激响应如 h 等于 [0.9, 0.3, -0.2],产生码间干扰。 +2. 迫零均衡器设计,构造卷积矩阵 A,求解最小二乘问题得到均衡器抽头。 +3. LMS 自适应均衡,使用训练序列,按 LMS 算法迭代更新抽头系数,记录误差收敛曲线。 +4. 效果评估,对比均衡前后的眼图、星座图,计算均方误差评估均衡性能。 + +## 5. 实验结果 + +![编码BER曲线](image-2.png) +![均衡眼图对比](image-3.png) +![LMS误差曲线](image-4.png) + + +## 6. 结果分析与讨论 + +回答: + +1. Hamming(7,4) 为什么能纠正单比特错误? + +Hamming(7,4) 码的最小汉明距离 d_min 等于 3。根据线性分组码的纠错能力公式,可纠正错误位数 t 等于 d_min 减 1 后除以 2 再向下取整,结果为 1。这意味着任意两个合法码字至少相差 3 位,因此当发生单比特错误时,接收序列距离原码字最近,距离为 1,而距离其他合法码字至少为 2,译码器能唯一确定错误位置并纠正。 + +2. 为什么信道编码会引入冗余并降低码率? + +信道编码通过在信息比特中添加监督比特来实现检错纠错功能。Hamming(7,4) 将 4 位信息扩展为 7 位,码率 R 等于 4 除以 7,约等于 0.57,小于 1。冗余比特不携带原始信息,但提供了纠错所需的空间,使得接收端能够区分合法码字和错误图案。 + +3. ZF 均衡为什么可能放大噪声? + +迫零均衡器的设计目标是完全消除码间干扰,即 H(f) 乘以 W(f) 等于 1。在信道频率响应 H(f) 存在深衰落的频率点,幅值很小,此时均衡器增益 W(f) 等于 1 除以 H(f) 会非常大,导致该频段的噪声被大幅放大。因此迫零均衡在消除码间干扰的同时会恶化信噪比,特别是在频率选择性深衰落信道下。 + +4. LMS 的步长过大或过小会出现什么问题? + +当步长过大时,算法会发散,误差剧烈震荡不收敛。原因是更新步长超过最优值范围,在最优解附近来回跳跃甚至远离。 +当步长过小时,收敛极慢,需要大量迭代才能接近最优。原因是每次更新幅度太小,难以快速跟踪信道变化。 +当步长适中时,算法平稳收敛到最优解,满足 0 小于 mu 小于 2 除以最大特征值这一稳定条件。 + +5. 均衡前后 ISI 有什么变化? + +均衡前眼图张开度较小,甚至闭合;均衡后眼图明显增大,清晰张开。 +均衡前码间干扰严重,多径导致符号重叠;均衡后码间干扰大幅抑制,冲激响应接近理想。 +均衡前误码率较高;均衡后误码率显著降低。 +均衡前噪声为原始噪声水平;均衡后若使用迫零均衡,可能因噪声放大而略有增加。 + + +## 7. 实验心得 + +通过本次实验,我对信道编码和信道均衡有了更深入的理解。 +1. 关于信道编码,Hamming(7,4) 码虽然简单,但完整展示了冗余换可靠性的编码思想。伴随式译码的逻辑非常巧妙,将代数结构与物理错误建立了直接对应关系。实现过程中对 GF(2) 上的矩阵运算有了更直观的认识。 +2. 关于信道均衡,迫零和 LMS 代表了两种截然不同的均衡思路。迫零是开环的基于信道先验知识的设计,LMS 是闭环的自适应学习。实际系统中往往需要结合两者优势,或采用更稳健的 MMSE 准则。 +3. 关于自动评分与 GitHub 工作流,pytest 驱动的自动测试让代码质量有了客观标准,也培养了我先写测试、再实现功能的测试驱动开发思维。Fork 到 Clone 到 Commit 到 Push 的流程是现代软件工程的基础技能。 +4. 关于 AI 编程辅助,AI 在解释通信原理、提供代码框架和调试思路上效率很高,但核心算法逻辑如 LMS 更新公式的推导、伴随式的物理意义仍需自己理解,否则难以应对测试中的边界情况。 + +## 8. 参考资料 + +- 课程课件:第6章 信道编码 +- 课程课件:第7章 均衡 diff --git a/REPORT_TEMPLATE.md b/REPORT_TEMPLATE.md deleted file mode 100644 index 5b435ee..0000000 --- a/REPORT_TEMPLATE.md +++ /dev/null @@ -1,60 +0,0 @@ -# 无线通信技术实验报告:信道编码与信道均衡 - -## 1. 实验目的 - -说明本实验希望掌握的信道编码、信道均衡和 GitHub 自动评分技能。 - -## 2. 实验原理 - -### 2.1 信道编码 - -说明 Hamming(7,4)、伴随式、单比特纠错、编码增益等概念。 - -### 2.2 信道均衡 - -说明 ISI、多径信道、ZF、LMS 自适应均衡等概念。 - -## 3. 实验环境 - -- Python 版本: -- 主要依赖:NumPy、Matplotlib、pytest -- AI 助手使用情况: - -## 4. 实验方法与步骤 - -### 4.1 Part 1:信道编码 - -描述编码、加噪/翻转、译码、BER 对比流程。 - -### 4.2 Part 2:信道均衡 - -描述多径信道、ZF/LMS 均衡器设计、均衡效果评估流程。 - -## 5. 实验结果 - -插入结果图: - -```markdown -![编码BER曲线](results/coding_ber_curve.png) -![均衡眼图对比](results/equalization_eye_comparison.png) -![LMS误差曲线](results/equalization_mse_curve.png) -``` - -## 6. 结果分析与讨论 - -回答: - -1. Hamming(7,4) 为什么能纠正单比特错误? -2. 为什么信道编码会引入冗余并降低码率? -3. ZF 均衡为什么可能放大噪声? -4. LMS 的步长过大或过小会出现什么问题? -5. 均衡前后 ISI 有什么变化? - -## 7. 实验心得 - -说明你对信道编码、信道均衡、自动评分和 AI 编程辅助的理解。 - -## 8. 参考资料 - -- 课程课件:第6章 信道编码 -- 课程课件:第7章 均衡 diff --git a/results/coding_ber_curve.png b/results/coding_ber_curve.png new file mode 100644 index 0000000..49fc4ea 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..cb23fde 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..6e36d0d 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..a2bbb36 100644 --- a/src/part1_channel_coding.py +++ b/src/part1_channel_coding.py @@ -48,8 +48,12 @@ 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) 编码') + # 将 bits reshape 为 (-1, 4),再与 HAMMING_G 相乘并对 2 取模 + blocks = bits.reshape(-1, 4) + encoded = (blocks @ HAMMING_G) % 2 + return encoded.flatten() + + # raise NotImplementedError('请实现 Hamming(7,4) 编码') def hamming74_syndrome(codewords): @@ -70,8 +74,10 @@ def hamming74_syndrome(codewords): if codewords.shape[1] != 7: raise ValueError('每个 Hamming(7,4) 码字长度必须为 7') - # TODO: 计算 s = r H^T mod 2。 - raise NotImplementedError('请实现伴随式计算') + # 计算 s = r H^T mod 2 + syndromes = (codewords @ HAMMING_H.T) % 2 + return syndromes + # raise NotImplementedError('请实现伴随式计算') def hamming74_decode(received): @@ -94,8 +100,28 @@ 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) 译码') + # 将 received reshape 为 (-1, 7),复制一份避免直接修改输入 + codewords = received.reshape(-1, 7).copy() + + # 调用 hamming74_syndrome 计算每个码字的伴随式 + syndromes = hamming74_syndrome(codewords) + + # 对每个非零伴随式,与 HAMMING_H 的 7 列逐列比较,定位并翻转错误位 + for i in range(codewords.shape[0]): + syndrome = syndromes[i] + # 若伴随式非零,说明有错误 + if not np.all(syndrome == 0): + # 与 HAMMING_H 的每一列比较,找到匹配列 + for col_idx in range(HAMMING_H.shape[1]): + if np.array_equal(HAMMING_H[:, col_idx], syndrome): + # 翻转对应码字位置 + codewords[i, col_idx] ^= 1 + break + + # 系统码的信息位为前 4 位,取每个码字前 4 位并 flatten 返回 + decoded_bits = codewords[:, :4].flatten() + return decoded_bits + # raise NotImplementedError('请实现 Hamming(7,4) 译码') def convolutional_encode(bits): @@ -108,8 +134,32 @@ def convolutional_encode(bits): if not np.all((bits == 0) | (bits == 1)): raise ValueError('bits 只能包含 0 或 1') - # TODO: 选做任务,可参考课件第6章卷积码部分。 - raise NotImplementedError('选做:请实现卷积码编码') + # 生成多项式 g1=111, g2=101 + # g1 对应输出1: 当前位 + 前一位 + 前两位 + # g2 对应输出2: 当前位 + 前两位 + + # 在末尾添加 2 个尾比特 0 + bits_padded = np.concatenate([bits, [0, 0]]) + + encoded = [] + # 移位寄存器初始状态为 [0, 0](存储前两位) + state = [0, 0] + + for bit in bits_padded: + # 当前输入为 bit,寄存器状态为 [state[0], state[1]] = [前一位, 前两位] + # 输出1 = bit XOR state[0] XOR state[1] (g1=111) + out1 = (bit ^ state[0] ^ state[1]) & 1 + # 输出2 = bit XOR state[1] (g2=101) + out2 = (bit ^ state[1]) & 1 + + encoded.append(out1) + encoded.append(out2) + + # 更新移位寄存器:新状态 = [当前bit, 前一位] + state = [bit, state[0]] + + return np.array(encoded, dtype=int) + # raise NotImplementedError('选做:请实现卷积码编码') def viterbi_decode_hard(received_bits): @@ -120,8 +170,75 @@ def viterbi_decode_hard(received_bits): if len(received_bits) % 2 != 0: raise ValueError('卷积码接收序列长度必须是 2 的倍数') - # TODO: 选做任务,可使用汉明距离作为路径度量。 - raise NotImplementedError('选做:请实现 Viterbi 硬判决译码') + # (2,1,3) 卷积码,约束长度 K=3,状态数为 2^(K-1) = 4 + # 状态用 2 位表示:[s1, s0],s1 为较新的位,s0 为较旧的位 + # 状态转移:输入 0 -> 新状态 = [0, s1],输入 1 -> 新状态 = [1, s1] + + num_states = 4 # 2^(3-1) = 4 + # 将接收序列按每 2 位分组 + symbols = received_bits.reshape(-1, 2) + num_steps = len(symbols) + + # 初始化路径度量,状态 0 为 0,其他为无穷大 + INF = float('inf') + path_metrics = [INF] * num_states + path_metrics[0] = 0 + + # 保存每个状态在每个时刻的前驱状态和输入比特 + # trellis[state][step] = (prev_state, input_bit) + trellis = [[None for _ in range(num_steps)] for _ in range(num_states)] + + # 状态转移表和输出表 + # 状态 s = (s1 << 1) | s0,即 s1 是高位,s0 是低位 + # 对于状态 [s1, s0],输入 u 时: + # 输出1 = u XOR s1 XOR s0 + # 输出2 = u XOR s0 + # 下一状态 = [u, s1] + + for step in range(num_steps): + new_metrics = [INF] * num_states + for state in range(num_states): + if path_metrics[state] == INF: + continue + s1 = (state >> 1) & 1 # 高位 + s0 = state & 1 # 低位 + + # 尝试输入 0 和 1 + for input_bit in [0, 1]: + # 计算输出 + out1 = (input_bit ^ s1 ^ s0) & 1 + out2 = (input_bit ^ s0) & 1 + output = [out1, out2] + + # 计算汉明距离 + hamming_dist = np.sum(symbols[step] != output) + + # 下一状态 + next_state = (input_bit << 1) | s1 + + # 更新路径度量 + new_metric = path_metrics[state] + hamming_dist + if new_metric < new_metrics[next_state]: + new_metrics[next_state] = new_metric + trellis[next_state][step] = (state, input_bit) + + path_metrics = new_metrics + + # 回溯,从最后的状态 0 开始(因为有尾比特保证回到全零) + decoded = [] + current_state = 0 # 尾比特使最终状态为 0 + + for step in range(num_steps - 1, -1, -1): + prev_state, input_bit = trellis[current_state][step] + decoded.append(input_bit) + current_state = prev_state + + # 反转得到正确顺序 + decoded = decoded[::-1] + + # 去掉尾比特对应的 2 个译码输出(最后 2 位是尾比特 0 产生的) + return np.array(decoded[:-2], dtype=int) + # raise NotImplementedError('选做:请实现 Viterbi 硬判决译码') def run_coding_demo(): diff --git a/src/part2_equalization.py b/src/part2_equalization.py index 8cbf1d8..a77ae77 100644 --- a/src/part2_equalization.py +++ b/src/part2_equalization.py @@ -38,8 +38,26 @@ def estimate_zf_equalizer(channel, num_taps): if num_taps < 1: raise ValueError('num_taps 必须为正整数') - # TODO: 构造卷积矩阵并求解 ZF 均衡器抽头。 - raise NotImplementedError('请实现 ZF 均衡器估计') + L = len(channel) + + # 构造卷积矩阵 A,使 A @ taps 表示 channel 与 taps 的卷积结果 + # 卷积结果长度为 L + num_taps - 1 + conv_len = L + num_taps - 1 + A = np.zeros((conv_len, num_taps)) + for i in range(num_taps): + A[i:i+L, i] = channel + + # 构造目标冲激响应 d,在中心位置放置 1,其余位置为 0 + d = np.zeros(conv_len) + center = conv_len // 2 + d[center] = 1.0 + + # 使用 np.linalg.lstsq 求最小二乘解 + taps, _, _, _ = np.linalg.lstsq(A, d, rcond=None) + + return taps + + # raise NotImplementedError('请实现 ZF 均衡器估计') def apply_fir_filter(signal, taps): @@ -58,8 +76,14 @@ 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 滤波') + # 使用 np.convolve 进行完整卷积 + conv_result = np.convolve(signal, taps, mode='full') + + # 截取前 len(signal) 个样本,使输出与输入等长 + filtered = conv_result[:len(signal)] + + return filtered + # raise NotImplementedError('请实现 FIR 滤波') def lms_equalizer(rx_train, tx_train, num_taps, step_size=0.01): @@ -89,8 +113,35 @@ 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,中心抽头为 1,其余为 0 + taps = np.zeros(num_taps) + center = num_taps // 2 + taps[center] = 1.0 + + # 误差序列 + errors = [] + + # 从第 num_taps - 1 个样本开始迭代 + for n in range(num_taps - 1, len(rx_train)): + # 构造当前输入向量 x,取 rx_train 中最近的 num_taps 个样本 + # x[n] = [x[n], x[n-1], ..., x[n-num_taps+1]],注意顺序 + x = np.flip(rx_train[n - num_taps + 1:n + 1]) + + # 计算输出 y = taps @ x + y = taps @ x + + # 计算误差 e = d - y + d = tx_train[n] + e = d - y + + # 根据 LMS 公式更新:taps = taps + step_size * e * x + taps = taps + step_size * e * x + + # 保存每次迭代的误差 + errors.append(e) + + return taps, np.array(errors) + # raise NotImplementedError('请实现 LMS 均衡器') def run_equalization_demo():