diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..87bc0db --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(PYTHONUTF8=1 python convert_md2docx.py)", + "Bash(PYTHONUTF8=1 python grading/calculate_grade.py)" + ] + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4b5a294 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:conda", + "python-envs.defaultPackageManager": "ms-python.python:conda" +} \ No newline at end of file diff --git a/REPORT.docx b/REPORT.docx new file mode 100644 index 0000000..ae92eae Binary files /dev/null and b/REPORT.docx differ diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..7d596c2 --- /dev/null +++ b/REPORT.md @@ -0,0 +1,201 @@ +# 无线通信技术实验报告:信道编码与信道均衡 + +## 1. 实验目的 + +本实验旨在通过 Python 仿真掌握无线通信中两项关键技术: + +1. **信道编码**:理解 Hamming(7,4) 线性分组码的编码、伴随式计算与单比特纠错译码流程,观察编码增益对误比特率(BER)的改善效果。 +2. **信道均衡**:理解码间干扰(ISI)的产生机理,掌握迫零(ZF)均衡器和最小均方(LMS)自适应均衡器的设计与实现,评估均衡对通信质量的提升。 +3. **工程实践**:熟悉使用 Git 进行版本控制、GitHub Classroom 自动评分的工作流程,以及 AI 辅助编程在通信仿真中的应用。 + +## 2. 实验原理 + +### 2.1 信道编码 + +**Hamming(7,4) 编码**是一种线性分组码,将 4 个信息比特编码为 7 个编码比特,码率为 $R = 4/7 \approx 0.571$。其生成矩阵为: + +$$ +G = \begin{bmatrix} +1 & 0 & 0 & 0 & 1 & 1 & 0 \\ +0 & 1 & 0 & 0 & 1 & 0 & 1 \\ +0 & 0 & 1 & 0 & 0 & 1 & 1 \\ +0 & 0 & 0 & 1 & 1 & 1 & 1 +\end{bmatrix} +$$ + +编码计算为 $\mathbf{c} = \mathbf{u} \cdot G$(GF(2) 运算),生成系统码(前 4 位为原始信息位,后 3 位为校验位)。 + +**校验矩阵与伴随式**:校验矩阵 $H$ 满足 $G H^T = 0$。接收码字 $\mathbf{r} = \mathbf{c} \oplus \mathbf{e}$($\mathbf{e}$ 为错误图样),伴随式定义为: + +$$ +\mathbf{s} = \mathbf{r} \cdot H^T = \mathbf{e} \cdot H^T +$$ + +本实验使用的校验矩阵为: + +$$ +H = \begin{bmatrix} +1 & 1 & 0 & 1 & 1 & 0 & 0 \\ +1 & 0 & 1 & 1 & 0 & 1 & 0 \\ +0 & 1 & 1 & 1 & 0 & 0 & 1 +\end{bmatrix} +$$ + +**单比特纠错原理**:当发生单比特错误时,伴随式 $\mathbf{s}$ 非零,且等于 $H$ 的第 $i$ 列(错误位置对应的列)。通过对比 $\mathbf{s}$ 与 $H$ 的各列即可定位错误比特并翻转纠正。因为 Hamming(7,4) 的最小汉明距离 $d_{min}=3$,可以纠正 1 个错误或检测 2 个错误。 + +**编码增益**:虽然编码引入了冗余(7/4 倍带宽开销),但在相同信噪比下可获得更低的 BER。编码增益定义为在相同 BER 下编码与未编码所需信噪比的差值。 + +**卷积码与 Viterbi 译码(选做)**:(2,1,3) 卷积码是一种非分组码,编码器由 2 级移位寄存器构成(约束长度 $K=3$),生成多项式为 $g_1 = (111)_2$、$g_2 = (101)_2$。每输入 1 个信息比特产生 2 个编码输出,码率 $R = 1/2$。 + +编码器结构:移位寄存器维持 2 个延迟状态 $(d_1, d_2)$,输出为: +$$ +\begin{aligned} +c_1[n] &= b[n] \oplus d_1[n] \oplus d_2[n] \\ +c_2[n] &= b[n] \oplus d_2[n] +\end{aligned} +$$ +编码结束后追加 2 个零尾比特使状态归零。 + +**Viterbi 硬判决译码**基于最大似然准则,在网格图(4 状态 00/01/10/11)上搜索与接收序列汉明距离最小的路径。每步执行加-比-选(ACS)操作:计算分支度量(汉明距离),累加路径度量,选出幸存路径。处理完全部接收符号后,从最小度量状态回溯,去掉尾比特即得译码结果。 + +### 2.2 信道均衡 + +**码间干扰(ISI)**:在多径传播环境中,信号经不同路径到达接收端,产生时延扩展。时域上表现为当前符号受到前后符号的干扰,即 ISI。ISI 会严重恶化接收性能,需要在接收端通过均衡器予以消除。 + +**多径信道模型**:本实验采用三径 FIR 信道 $h = [0.9, \; 0.35, \; -0.25]$,接收信号为发送符号与信道冲激响应的卷积叠加噪声: + +$$ +r[n] = s[n] * h[n] + w[n] +$$ + +**迫零(ZF)均衡**:通过设计 FIR 均衡器 $\mathbf{w}$,使信道与均衡器的级联响应逼近理想冲激 $\mathbf{d}$(仅中心位置为 1)。求解线性方程组 $A \mathbf{w} \approx \mathbf{d}$(使用最小二乘法)。ZF 均衡器完全消除 ISI,但可能放大噪声,尤其在信道频率响应有深衰落时。 + +**LMS 自适应均衡**:LMS 算法是一种基于梯度下降的自适应滤波方法,无需矩阵求逆。迭代更新公式为: + +$$ +\begin{aligned} +y[n] &= \mathbf{w}^T[n] \cdot \mathbf{x}[n] \\ +e[n] &= d[n] - y[n] \\ +\mathbf{w}[n+1] &= \mathbf{w}[n] + \mu \cdot e[n] \cdot \mathbf{x}[n] +\end{aligned} +$$ + +其中 $\mu$ 为步长参数,控制收敛速度与稳态误差的权衡。 + +## 3. 实验环境 + +- **Python 版本**:3.10+ +- **主要依赖**: + - NumPy(矩阵运算、线性代数) + - Matplotlib(BER 曲线、眼图/波形图、误差曲线绘制) + - SciPy(信号处理辅助) + - pytest(单元测试框架,用于 GitHub Classroom 自动评分) +- **开发工具**:Git(版本控制)、GitHub Classroom(自动评分) +- **AI 助手使用情况**:本实验使用 Claude Code 作为 AI 编程助手,辅助完成代码编写、调试、报告撰写等工作(详见第 8 节)。 + +## 4. 实验方法与步骤 + +### 4.1 Part 1:信道编码 + +1. **生成测试数据**:使用 `generate_bits` 生成 4000 个随机信息比特(seed=2026),并截断为 4 的倍数以适应 Hamming(7,4) 的分组大小。 +2. **编码**:调用 `hamming74_encode`,将信息比特按 4 位分组,通过生成矩阵 $G$ 在 GF(2) 上编码为 7 位码字。 +3. **加噪/翻转**:在误差概率集合 $\{0.001, 0.003, 0.01, 0.03, 0.06, 0.1\}$ 上,通过二元对称信道(BSC)分别对未编码比特和编码比特进行随机翻转。 +4. **译码**:调用 `hamming74_decode`,对接收码字计算伴随式,若伴随式非零则定位并纠正错误位,提取前 4 位信息比特。 +5. **BER 对比**:计算每个误差概率下未编码和 Hamming(7,4) 编码的 BER,绘制双对数坐标 BER 曲线。 +6. **选做——卷积码**:实现 `convolutional_encode`((2,1,3) 卷积码,生成多项式 g1=111, g2=101)和 `viterbi_decode_hard`(硬判决 Viterbi 译码),通过网格图 ACS 操作和回溯完成最大似然译码。 + +### 4.2 Part 2:信道均衡 + +1. **信号生成**:生成 2000 个随机比特(seed=2027),经 BPSK 调制映射为 $\pm 1$ 符号。 +2. **多径传输**:符号通过三径信道 $[0.9, 0.35, -0.25]$,叠加标准差 $\sigma=0.12$ 的高斯噪声,模拟多径与加性噪声的联合影响。 +3. **ZF 均衡器设计**:调用 `estimate_zf_equalizer`,构造卷积矩阵 $A$,以理想冲激为期望响应 $\mathbf{d}$,通过 `np.linalg.lstsq` 求解 7 抽头 FIR 均衡器系数。 +4. **LMS 自适应均衡**:使用前 800 个符号作为训练序列,调用 `lms_equalizer`($\mu=0.01$,7 抽头),迭代更新抽头系数,记录每次迭代的瞬时误差。 +5. **均衡效果评估**: + - 对比均衡前后 BER:对均衡输出进行 BPSK 硬判决解调,计算 BER。 + - 绘制均衡前后波形对比图。 + - 绘制 LMS 训练误差收敛曲线。 + +## 5. 实验结果 + +### 5.1 Part 1:编码 BER 曲线 + +![编码BER曲线](results/coding_ber_curve.png) + +**说明**:图中横轴为信道误码概率(BSC 翻转概率),纵轴为 BER(对数坐标)。蓝色曲线为未编码 BPSK 的 BER,橙色曲线为经 Hamming(7,4) 编码后的 BER。可以看到,在低误码概率下,编码有效降低了 BER;当信道误码概率较小时($\leq 0.03$),单比特纠错能力足以纠正大部分错误,编码增益显著。当信道误码概率过高时($> 0.03$),多个比特同时错误的概率增加,超出 Hamming(7,4) 的单比特纠错能力,编码增益下降。 + +### 5.1 附:卷积码 BER 对比 + +![卷积码BER曲线](results/coding_ber_curve_conv.png) + +**说明**:在 Hamming(7,4) 基础上增加卷积码 (2,1,3) + Viterbi 硬判决译码的 BER 曲线(绿色)。无误传输下译码 BER = 0(验证了编码-译码的正确性)。在低误码概率下卷积码的纠错能力接近 Hamming(7,4);卷积码码率 $R=1/2$ 虽然更低,但约束长度仅 $K=3$ 限制了编码增益,增大约束长度和采用软判决可进一步提升性能。 + +### 5.2 Part 2:均衡波形对比 + +![均衡眼图对比](results/equalization_eye_comparison.png) + +**说明**:图中展示了前 120 个符号的波形。蓝色为原始发送的 BPSK 符号($\pm 1$),橙色为经多径信道后的接收信号(ISI 导致幅度弥散),绿色为 LMS 均衡后的输出。可以观察到,均衡后信号幅度明显向 $\pm 1$ 靠拢,ISI 得到有效抑制,为正确的 BPSK 硬判决创造了条件。 + +### 5.3 LMS 误差收敛曲线 + +![LMS误差曲线](results/equalization_mse_curve.png) + +**说明**:图中横轴为 LMS 迭代次数,纵轴为瞬时平方误差(对数坐标)。MSE 从较高水平开始(均衡器未训练),经过约 200 次迭代后快速下降,随后趋于稳态(接近噪声基底)。曲线表明 $\mu=0.01$ 的步长设置使算法在收敛速度和稳态误差之间取得了较好的平衡。 + +## 6. 结果分析与讨论 + +### 6.1 Hamming(7,4) 为什么能纠正单比特错误? + +Hamming(7,4) 的最小汉明距离 $d_{min}=3$,意味着任意两个合法码字之间至少有 3 个位置不同。当发生单比特错误时,接收码字与原始码字的距离为 1,与其他任意合法码字的距离至少为 2。因此,将接收码字译码为与之距离最近的合法码字,即可纠正该单比特错误。伴随式机制提供了一种高效的实现方式:伴随式 $\mathbf{s} = \mathbf{e} \cdot H^T$ 仅取决于错误图样,且 3 位伴随式可表示 $2^3-1=7$ 种单比特错误位置(全零表示无错),实现了单比特错误的无歧义定位。 + +### 6.2 为什么信道编码会引入冗余并降低码率? + +信道编码通过在信息比特之外增加校验比特,引入受控冗余来换取纠错能力。Hamming(7,4) 将 4 个信息位扩展为 7 个编码位,码率 $R = 4/7 \approx 57\%$。这 3 个校验位是信息位的线性组合,用于接收端检错和纠错。冗余本质上是以带宽或数据速率为代价,换取功率效率(更低的 BER 或更低的所需 SNR)。 + +### 6.3 ZF 均衡为什么可能放大噪声? + +ZF 均衡的目标是使信道与均衡器的级联为理想冲激响应,即 $H(z) \cdot W(z) = 1$。从频域看,$W(f) = 1 / H(f)$。当信道在某些频率上存在深衰落($|H(f)|$ 很小)时,$|W(f)|$ 会很大,导致这些频率处的噪声被显著放大。这就是 ZF 均衡器的噪声增强效应。相比之下,LMS/MMSE 均衡器在消除 ISI 和抑制噪声之间进行折中,通常能获得更好的性能。 + +### 6.4 LMS 的步长过大或过小会出现什么问题? + +- **步长过大**:算法收敛速度快,但稳态误差大,抽头系数在最优值附近剧烈波动,甚至可能导致算法发散(不收敛)。 +- **步长过小**:稳态误差小,抽头系数接近最优解(Wiener 解),但收敛速度慢,需要更长的训练序列才能达到稳定。 + +实际应用中,步长的选取需要在收敛速度和稳态精度之间权衡。一个常见的经验规则是:$\mu < 1 / \lambda_{max}$,其中 $\lambda_{max}$ 是输入自相关矩阵的最大特征值。 + +### 6.5 均衡前后 ISI 有什么变化? + +均衡前,由于多径信道的时延扩展,BPSK 符号 $\pm 1$ 在接收端呈现为幅度在较大范围内弥散分布,±1 的判决边界模糊,导致硬判决后 BER 较高。从波形对比图可以清晰看到,均衡后信号幅度显著向 $\pm 1$ 聚集,ISI 得到有效抑制。定量地,本实验中均衡前 BER 显著高于均衡后 LMS 输出的 BER,验证了均衡器的有效性。 + +## 7. 实验心得 + +通过本次实验,我对以下概念有了更深入的理解: + +1. **信道编码**:Hamming(7,4) 虽然结构简单,但完整展示了线性分组码的编码、伴随式译码和纠错的完整流程。编码增益的直观感受——用 75% 的带宽开销换取显著的 BER 改善——体现了信息论中可靠性与有效性的基本权衡。 + +2. **卷积码与 Viterbi 译码**:与分组码不同,卷积码引入了记忆(移位寄存器),将编码从无记忆映射扩展到有限状态机。Viterbi 译码的网格图搜索是动态规划在通信中的经典应用——每步保留 4 条幸存路径,避免了指数量级的全搜索。汉明距离作为硬判决度量简化了实现,但软判决(欧氏距离)可获得额外编码增益。 + +3. **信道均衡**:ZF 和 LMS 代表了确定性(基于信道估计)和自适应(基于梯度下降)两种均衡思路。LMS 的简洁性和鲁棒性使其在实践中广泛应用,但对步长选择的敏感性也提醒了算法调参的重要性。 + +4. **自动评分与 AI 编程辅助**:GitHub Classroom 的自动化测试为代码质量提供了即时反馈。AI 助手在理解算法原理、实现代码、调试错误和撰写报告等方面提供了高效支持,但需要使用者对 AI 的输出保持批判性审视,确保对底层原理有真正的理解。 + +## 8. AI 助手使用情况说明 + +本实验使用 Claude Code(基于 Claude 4.X 模型)作为 AI 编程助手,具体使用情况如下: + +| 使用环节 | 具体用途 | AI 贡献度 | +|---------|---------|----------| +| 代码编写 | 完成 `hamming74_encode`、`hamming74_syndrome`、`hamming74_decode`、`estimate_zf_equalizer`、`apply_fir_filter`、`lms_equalizer` 等核心函数 | 约 90% | +| 选做实现 | 完成 `convolutional_encode`((2,1,3) 卷积码)和 `viterbi_decode_hard`(硬判决 Viterbi 译码) | 约 95% | +| 调试纠错 | 定位数组维度不匹配、GF(2) 运算错误、LMS 迭代索引偏移、pylint 评分优化等问题 | 约 85% | +| 报告撰写 | 辅助撰写实验报告(原理阐述、结果分析、讨论问题回答) | 约 80% | +| 测试验证 | 协助运行 pytest 测试套件,根据 GitHub Classroom 评分反馈修改代码,最终取得 100/100 | 约 70% | + +**使用体会**:AI 助手显著提高了编码和调试效率,尤其在矩阵运算细节、算法参数调优方面表现出色。但 AI 生成的代码和解释并非总是正确,需要结合课程知识进行验证和修正。建议在使用 AI 辅助的同时,务必独立理解每个算法步骤的物理意义和数学推导。 + +## 9. 参考资料 + +- 课程课件:第 6 章 信道编码 +- 课程课件:第 7 章 均衡 +- S. Lin and D. J. Costello, *Error Control Coding*, 2nd ed. Pearson, 2004. +- S. Haykin, *Adaptive Filter Theory*, 5th ed. Pearson, 2014. +- J. G. Proakis and M. Salehi, *Digital Communications*, 5th ed. McGraw-Hill, 2008. diff --git a/convert_md2docx.py b/convert_md2docx.py new file mode 100644 index 0000000..8526ee0 --- /dev/null +++ b/convert_md2docx.py @@ -0,0 +1,210 @@ +"""将 REPORT.md 转换为 REPORT.docx。""" +import re +import os +from docx import Document +from docx.shared import Pt, Inches, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.oxml.ns import qn + +SRC = 'REPORT.md' +DST = 'REPORT.docx' + + +def add_image_if_exists(doc, alt_text, img_path): + """如果图片文件存在则插入,否则插入占位文本。""" + if os.path.exists(img_path): + try: + doc.add_picture(img_path, width=Inches(5.0)) + last_paragraph = doc.paragraphs[-1] if doc.paragraphs else doc.add_paragraph() + last_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER + caption = doc.add_paragraph(f'图:{alt_text}') + caption.alignment = WD_ALIGN_PARAGRAPH.CENTER + caption.runs[0].font.size = Pt(10) + caption.runs[0].font.color.rgb = RGBColor(100, 100, 100) + except Exception: + doc.add_paragraph(f'[图片:{alt_text}] ({img_path})') + else: + doc.add_paragraph(f'[图片缺失:{alt_text}] ({img_path})') + + +def set_cell_font(cell, text, bold=False, size=10): + """设置表格单元格字体。""" + cell.text = '' + paragraph = cell.paragraphs[0] + run = paragraph.add_run(text) + run.font.size = Pt(size) + run.font.name = 'Microsoft YaHei' + run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei') + run.bold = bold + + +def convert(): + with open(SRC, 'r', encoding='utf-8') as f: + lines = f.readlines() + + doc = Document() + + # 设置默认字体 + style = doc.styles['Normal'] + font = style.font + font.name = 'Microsoft YaHei' + font.size = Pt(11) + style.element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei') + + in_code_block = False + code_lines = [] + in_table = False + table_rows = [] + list_buffer = [] + + def flush_code_block(): + nonlocal code_lines + if code_lines: + code_text = ''.join(code_lines).rstrip() + p = doc.add_paragraph() + p.paragraph_format.left_indent = Inches(0.3) + run = p.add_run(code_text) + run.font.name = 'Consolas' + run.font.size = Pt(9) + run.font.color.rgb = RGBColor(60, 60, 60) + code_lines = [] + + def flush_table(): + if table_rows: + rows = [] + for tr in table_rows: + cells = [c.strip() for c in tr.strip('|').split('|')] + rows.append(cells) + col_count = max(len(r) for r in rows) if rows else 1 + table = doc.add_table(rows=len(rows), cols=col_count, style='Light Grid Accent 1') + for i, row_data in enumerate(rows): + for j, cell_text in enumerate(row_data): + if j < col_count: + set_cell_font(table.cell(i, j), cell_text, bold=(i == 0), size=10) + doc.add_paragraph() + table_rows.clear() + + i = 0 + while i < len(lines): + line = lines[i] + + # 代码块 + if line.startswith('```'): + if in_code_block: + flush_code_block() + in_code_block = False + else: + flush_table() + in_code_block = True + i += 1 + continue + if in_code_block: + code_lines.append(line) + i += 1 + continue + + # 空行 + if line.strip() == '': + i += 1 + continue + + # 表格行 + if line.strip().startswith('|') and line.strip().endswith('|'): + if any(c.isalpha() for c in line): # 跳过纯分隔行如 |---|---| + in_table = True + table_rows.append(line.strip()) + i += 1 + continue + else: + if in_table: + flush_table() + in_table = False + + # 标题 + heading_match = re.match(r'^(#{1,6})\s+(.*)', line) + if heading_match: + level = len(heading_match.group(1)) + text = re.sub(r'<.*?>', '', heading_match.group(2).strip()) + doc.add_heading(text, level=min(level, 3)) + i += 1 + continue + + # 图片 + img_match = re.match(r'^!\[(.*?)\]\((.*?)\)', line) + if img_match: + alt = img_match.group(1) + path = img_match.group(2) + add_image_if_exists(doc, alt, path) + i += 1 + continue + + # 无序列表 + list_match = re.match(r'^(\s*)[-*]\s+(.*)', line) + ordered_match = re.match(r'^(\s*)\d+[\.)]\s+(.*)', line) + if list_match: + text = list_match.group(2) + text = re.sub(r'\*\*(.*?)\*\*', r'\1', text) + p = doc.add_paragraph(text, style='List Bullet') + i += 1 + continue + if ordered_match: + text = ordered_match.group(2) + text = re.sub(r'\*\*(.*?)\*\*', r'\1', text) + p = doc.add_paragraph(text, style='List Number') + i += 1 + continue + + # 普通段落:处理行内格式 + text = line.strip() + text = re.sub(r'', '', text) + text = text.replace(' ', ' ') + + # 处理 $$ 块级公式 -> 简化为居中文本 + if text == '$$': + formula_lines = [] + i += 1 + while i < len(lines) and not lines[i].strip().startswith('$$'): + formula_lines.append(lines[i].strip()) + i += 1 + i += 1 + if formula_lines: + p = doc.add_paragraph() + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + run = p.add_run(' '.join(formula_lines)) + run.font.name = 'Consolas' + run.font.size = Pt(10) + run.font.color.rgb = RGBColor(80, 80, 80) + continue + + # 行内公式 $...$ 和 $$...$$,保留原样 + p = doc.add_paragraph() + + # 简化处理:处理行内 **粗体** 和普通文本 + parts = re.split(r'(\*\*.*?\*\*|`.*?`|\$.*?\$)', text) + for part in parts: + if part.startswith('**') and part.endswith('**'): + run = p.add_run(part[2:-2]) + run.bold = True + elif part.startswith('`') and part.endswith('`'): + run = p.add_run(part[1:-1]) + run.font.name = 'Consolas' + run.font.size = Pt(10) + elif part.startswith('$') and part.endswith('$'): + run = p.add_run(part[1:-1]) + run.font.name = 'Consolas' + run.font.size = Pt(10) + run.font.italic = True + elif part.strip(): + run = p.add_run(part) + + i += 1 + + flush_code_block() + flush_table() + + doc.save(DST) + print(f'已生成 {DST}') + + +if __name__ == '__main__': + convert() diff --git a/src/part1_channel_coding.py b/src/part1_channel_coding.py index 1ecf55e..950a0e6 100644 --- a/src/part1_channel_coding.py +++ b/src/part1_channel_coding.py @@ -6,6 +6,7 @@ """ import numpy as np +# pylint: disable=import-error from utils import ( binary_symmetric_channel, calculate_ber, @@ -48,8 +49,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.flatten() def hamming74_syndrome(codewords): @@ -70,8 +72,7 @@ def hamming74_syndrome(codewords): if codewords.shape[1] != 7: raise ValueError('每个 Hamming(7,4) 码字长度必须为 7') - # TODO: 计算 s = r H^T mod 2。 - raise NotImplementedError('请实现伴随式计算') + return (codewords @ HAMMING_H.T) % 2 def hamming74_decode(received): @@ -94,8 +95,13 @@ 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 i, s in enumerate(syndromes): + if s.any(): + error_pos = np.argwhere((HAMMING_H.T == s).all(axis=1)).item() + codewords[i, error_pos] ^= 1 + return codewords[:, :4].flatten() def convolutional_encode(bits): @@ -108,8 +114,16 @@ def convolutional_encode(bits): if not np.all((bits == 0) | (bits == 1)): raise ValueError('bits 只能包含 0 或 1') - # TODO: 选做任务,可参考课件第6章卷积码部分。 - raise NotImplementedError('选做:请实现卷积码编码') + bits_with_tail = np.append(bits, [0, 0]) + delayed_1, delayed_2 = 0, 0 + encoded = [] + for bit in bits_with_tail: + # g1=111: bit ⊕ d1 ⊕ d2 + # g2=101: bit ⊕ d2 + encoded.extend([bit ^ delayed_1 ^ delayed_2, bit ^ delayed_2]) + delayed_2 = delayed_1 + delayed_1 = bit + return np.array(encoded, dtype=int) def viterbi_decode_hard(received_bits): @@ -120,8 +134,45 @@ def viterbi_decode_hard(received_bits): if len(received_bits) % 2 != 0: raise ValueError('卷积码接收序列长度必须是 2 的倍数') - # TODO: 选做任务,可使用汉明距离作为路径度量。 - raise NotImplementedError('选做:请实现 Viterbi 硬判决译码') + # Trellis: next_state[curr][input] = (next, (out1, out2)) + # States: 0=00, 1=01, 2=10, 3=11 + trellis = { + 0: {0: (0, (0, 0)), 1: (2, (1, 1))}, + 1: {0: (0, (1, 1)), 1: (2, (0, 0))}, + 2: {0: (1, (1, 0)), 1: (3, (0, 1))}, + 3: {0: (1, (0, 1)), 1: (3, (1, 0))}, + } + + num_steps = len(received_bits) // 2 + num_states = 4 + + path_metrics = np.full(num_states, np.inf) + path_metrics[0] = 0.0 + survivors = [[None] * num_states for _ in range(num_steps)] + + for step in range(num_steps): + received_pair = received_bits[2 * step : 2 * step + 2] + new_metrics = np.full(num_states, np.inf) + for curr_state in range(num_states): + if np.isinf(path_metrics[curr_state]): + continue + for input_bit in (0, 1): + next_state, expected = trellis[curr_state][input_bit] + hamming = np.sum(received_pair != np.array(expected)) + candidate = path_metrics[curr_state] + hamming + if candidate < new_metrics[next_state]: + new_metrics[next_state] = candidate + survivors[step][next_state] = (curr_state, input_bit) + path_metrics = new_metrics + + best_state = int(np.argmin(path_metrics)) + decoded = [] + for step in range(num_steps - 1, -1, -1): + prev_state, input_bit = survivors[step][best_state] + decoded.append(input_bit) + best_state = prev_state + decoded.reverse() + return np.array(decoded[:-2], dtype=int) def run_coding_demo(): @@ -131,6 +182,9 @@ def run_coding_demo(): print('=' * 60) error_probabilities = np.array([0.001, 0.003, 0.01, 0.03, 0.06, 0.1]) + + # ===== Hamming(7,4) 演示 ===== + print('\n--- Hamming(7,4) 编码 ---') uncoded_ber = [] coded_ber = [] @@ -158,6 +212,36 @@ def run_coding_demo(): except Exception as error: print(f'❌ Part 1 运行失败:{error}') + # ===== 选做:卷积码演示 ===== + print('\n--- 选做:(2,1,3) 卷积码 + Viterbi 译码 ---') + try: + conv_bits = generate_bits(2000, seed=2026) + conv_encoded = convolutional_encode(conv_bits) + + # 验证无误传输下译码正确性 + decoded_no_error = viterbi_decode_hard(conv_encoded) + ber_no_error = calculate_ber(conv_bits, decoded_no_error) + print(f' 无误传输 BER: {ber_no_error:.4f} (应为 0.0000)') + + # 不同误码率下 BSC 传输 + Viterbi 译码 + conv_ber = [] + for index, probability in enumerate(error_probabilities): + conv_rx = binary_symmetric_channel(conv_encoded, probability, seed=300 + index) + conv_decoded = viterbi_decode_hard(conv_rx) + conv_ber.append(calculate_ber(conv_bits, conv_decoded)) + + plot_ber_curve( + error_probabilities, + {'未编码': uncoded_ber, 'Hamming(7,4)': coded_ber, '卷积码(2,1,3)-Viterbi': conv_ber}, + '信道编码 BER 对比 (含卷积码)', + 'coding_ber_curve_conv.png', + ) + print('✅ 已生成 results/coding_ber_curve_conv.png') + except NotImplementedError as error: + print(f'⏸️ 选做尚未完成:{error}') + except Exception as error: + print(f'❌ 卷积码演示运行失败:{error}') + if __name__ == '__main__': run_coding_demo() diff --git a/src/part2_equalization.py b/src/part2_equalization.py index 8cbf1d8..5c22f64 100644 --- a/src/part2_equalization.py +++ b/src/part2_equalization.py @@ -5,6 +5,7 @@ """ import numpy as np +# pylint: disable=import-error from utils import ( bpsk_demodulate, bpsk_modulate, @@ -38,8 +39,16 @@ def estimate_zf_equalizer(channel, num_taps): if num_taps < 1: raise ValueError('num_taps 必须为正整数') - # TODO: 构造卷积矩阵并求解 ZF 均衡器抽头。 - raise NotImplementedError('请实现 ZF 均衡器估计') + n_rows = len(channel) + num_taps - 1 + A = np.zeros((n_rows, num_taps)) + for j in range(num_taps): + for i in range(len(channel)): + if i + j < n_rows: + A[i + j, j] = channel[i] + d = np.zeros(n_rows) + d[n_rows // 2] = 1.0 + taps, *_ = np.linalg.lstsq(A, d, rcond=None) # pylint: disable=no-member + return taps def apply_fir_filter(signal, taps): @@ -58,8 +67,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 +97,16 @@ 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) + taps[num_taps // 2] = 1.0 + errors = [] + for i in range(num_taps - 1, len(rx_train)): + x = rx_train[i - num_taps + 1 : i + 1][::-1] + y = taps @ x + e = tx_train[i] - y + taps = taps + step_size * e * x + errors.append(e) + return taps, np.array(errors) def run_equalization_demo(): @@ -112,8 +128,10 @@ def run_equalization_demo(): lms_output = apply_fir_filter(rx, lms_taps) raw_bits = bpsk_demodulate(rx[: len(bits)]) + zf_bits = bpsk_demodulate(zf_output[: len(bits)]) eq_bits = bpsk_demodulate(lms_output[: len(bits)]) print(f'均衡前 BER: {calculate_ber(bits, raw_bits):.4f}') + print(f'ZF 均衡后 BER: {calculate_ber(bits, zf_bits):.4f}') print(f'LMS 均衡后 BER: {calculate_ber(bits, eq_bits):.4f}') plot_equalization_results(symbols, rx, lms_output, 'equalization_eye_comparison.png')