diff --git a/.gitignore b/.gitignore index 6bf81d4..92ca28b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ results/ *.png *.jpg *.pdf +!results/ +!results/*.png # IDE .vscode/ diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..df3a9cf --- /dev/null +++ b/REPORT.md @@ -0,0 +1,272 @@ +# 数字调制解调实验报告 + +**实验名称**:数字调制解调实验 +**学生姓名**:谭榆铃 +**学号**:2023280502 +**实验日期**:2026年4月23日 +**提交日期**:2026年4月23日 + +--- + +## 1. 实验目的 + +本实验的主要目标如下: + +- 理解数字调制与解调的基本原理,掌握 BPSK、QPSK 和 16-QAM 的符号映射方式。 +- 使用 Python、NumPy 完成调制、解调和误码率分析程序的实现。 +- 通过星座图观察不同调制方式的符号分布特征。 +- 通过 BER 曲线比较不同调制方式在 AWGN 信道中的抗噪声性能。 +- 体验 AI 编程助手在代码生成、调试和完善实验过程中的辅助作用。 + +--- + +## 2. 实验原理 + +### 2.1 BPSK 调制原理 + +BPSK(Binary Phase Shift Keying,二进制相移键控)每次传输 1 bit 信息,用两个相位相反的符号表示二进制 0 和 1。本实验中采用如下映射: + +$$ +0 \rightarrow +1,\quad 1 \rightarrow -1 +$$ + +也可以写成: + +$$ +s = 1 - 2b +$$ + +其中 $b \in \{0,1\}$。 + +BPSK 的星座图只有两个点,分布在实轴两端,因此结构最简单、符号间距离最大,抗噪声性能最好,但频谱效率相对较低。 + +### 2.2 QPSK 调制原理 + +QPSK(Quadrature Phase Shift Keying,正交相移键控)每个符号表示 2 bit 信息,因此比 BPSK 具有更高的频谱效率。实验中采用格雷码映射: + +- `00 -> (1+j)/sqrt(2)` +- `01 -> (-1+j)/sqrt(2)` +- `11 -> (-1-j)/sqrt(2)` +- `10 -> (1-j)/sqrt(2)` + +除以 `sqrt(2)` 的目的是使符号平均功率归一化为 1。 + +QPSK 的星座图有 4 个点,均匀分布在复平面的四个象限上。使用格雷码映射可以减少相邻星座点判错时带来的比特错误数。 + +### 2.3 16-QAM 调制原理 + +16-QAM(16 阶正交幅度调制)每个符号表示 4 bit 信息,实部和虚部分别取四个离散幅值 `{-3,-1,+1,+3}`。实验中 I、Q 两路都采用格雷码映射: + +- `00 -> +3` +- `01 -> +1` +- `11 -> -1` +- `10 -> -3` + +16-QAM 的符号共有 16 个,构成一个 4×4 的星座图。为了统一平均功率,实验中对复符号除以 `sqrt(10)` 进行归一化。 + +16-QAM 的优点是频谱效率高,但星座点更密集,对噪声更敏感,因此在低信噪比条件下 BER 往往高于 BPSK 和 QPSK。 + +### 2.4 解调与 BER 分析原理 + +解调阶段需要根据接收符号判断其最可能对应的发送比特: + +- BPSK:根据实部与 0 的大小关系进行阈值判决。 +- QPSK:计算接收符号与 4 个理想星座点的欧氏距离,选择最近的点。 +- 16-QAM:对 I、Q 两个分量分别进行分段判决,再恢复 4 bit。 + +在性能分析中,实验采用 AWGN(加性高斯白噪声)信道模型,通过扫描不同的 SNR,统计误码率 BER: + +$$ +BER = \frac{\text{错误比特数}}{\text{总比特数}} +$$ + +--- + +## 3. 实验方法与步骤 + +### 3.1 环境配置 + +实验环境为 Python 3.11,主要使用 `numpy`、`pytest`、`pillow` 等库。由于当前环境中 `matplotlib` 与 `numpy 2.x` 存在兼容性问题,实验中为绘图功能增加了回退方案,以保证星座图和 BER 图能够稳定生成并保存到 `results/` 目录中。 + +### 3.2 BPSK 实现 + +BPSK 调制函数 `bpsk_modulate(bits)` 使用矢量化公式: + +```python +symbols = 1 - 2 * bits +``` + +再将结果转换为复数类型,便于与后续 QPSK、16-QAM 保持统一接口。 + +BPSK 解调函数 `bpsk_demodulate(symbols)` 则通过判断接收符号实部是否大于 0 来恢复比特。 + +### 3.3 QPSK 实现 + +QPSK 调制中,先将输入比特序列按每 2 bit 分组,再根据格雷码查表生成复数符号,最后除以 `sqrt(2)` 完成功率归一化。 + +QPSK 解调时,对每个接收符号分别计算其与 4 个理想星座点之间的欧氏距离,选择最近点后输出对应 2 bit。 + +### 3.4 16-QAM 实现 + +16-QAM 调制中,将比特序列按每 4 bit 分组,其中前 2 bit 决定 I 路,后 2 bit 决定 Q 路,均使用格雷码映射到 `{-3,-1,+1,+3}`,再组合成复数符号并除以 `sqrt(10)`。 + +解调时,不再逐一枚举 16 个点,而是对接收符号的实部和虚部分别进行切片判决,这样实现更高效,也更符合实际系统中的思路。 + +### 3.5 BER 性能测试 + +BER 测试流程如下: + +1. 生成随机二进制比特序列。 +2. 选择调制方式进行调制。 +3. 在 AWGN 信道中加入噪声。 +4. 对接收符号进行解调。 +5. 计算 BER。 +6. 扫描不同 SNR(本实验为 0 dB 到 14 dB,步长 2 dB)。 +7. 绘制 BER 曲线并比较三种调制方式的性能。 + +--- + +## 4. 实验结果 + +### 4.1 BPSK 星座图 + +![BPSK星座图](results/bpsk_constellation.png) + +**分析**:BPSK 星座图中只有两个点,分别位于实轴正负两侧,说明二进制 0 和 1 被正确映射到 `+1` 和 `-1`。由于两个点间距离较大,因此对噪声不敏感。 + +### 4.2 QPSK 星座图 + +![QPSK星座图](results/qpsk_constellation.png) + +**分析**:QPSK 星座图中共有 4 个点,分布在四个象限,且各点幅度相同、相位不同,符合格雷码映射要求。点落在单位圆附近,说明功率归一化正确。 + +### 4.3 16-QAM 星座图 + +![16-QAM星座图](results/16qam_constellation.png) + +**分析**:16-QAM 星座图形成标准 4×4 网格结构,共有 16 个离散符号点。与 BPSK 和 QPSK 相比,点间距离更小,因此更容易受到噪声影响。 + +### 4.4 BER 性能测试结果 + +![BER性能对比图](results/ber_comparison.png) + +另外,程序还分别生成了三种调制方式单独的 BER 曲线: + +![BPSK BER](results/bpsk_ber.png) + +![QPSK BER](results/qpsk_ber.png) + +![16-QAM BER](results/16qam_ber.png) + +本次实验运行得到的 BER 统计结果如下: + +| SNR(dB) | BPSK BER | QPSK BER | 16-QAM BER | +|--------|----------|----------|------------| +| 0 | 0.0770 | 0.1581 | 0.2855 | +| 2 | 0.0367 | 0.0984 | 0.2398 | +| 4 | 0.0107 | 0.0592 | 0.1930 | +| 6 | 0.0018 | 0.0220 | 0.1430 | +| 8 | 0.0001 | 0.0063 | 0.0911 | +| 10 | 0.0000 | 0.0013 | 0.0583 | +| 12 | 0.0000 | 0.0000 | 0.0279 | +| 14 | 0.0000 | 0.0000 | 0.0098 | + +**分析**:随着 SNR 的提高,三种调制方式的 BER 均明显下降,说明系统实现正确。BPSK 在整个 SNR 区间内误码率最低,QPSK 次之,16-QAM 最高。这与理论分析一致,因为 BPSK 的星座点间距最大,而 16-QAM 的点最密集。 + +--- + +## 5. 结果分析与讨论 + +### 5.1 星座图对比分析 + +从三种星座图可以看出: + +- BPSK 星座图最简单,仅由两个点组成。 +- QPSK 在相同带宽条件下比 BPSK 传递更多比特,星座点数增加到 4 个。 +- 16-QAM 进一步提升频谱效率,但星座点明显更密集。 + +因此,随着调制阶数提高,系统单位符号携带的信息量增加,但抗噪声能力下降,这是数字通信中非常典型的性能与效率折中。 + +### 5.2 性能对比分析 + +本实验 BER 结果表明: + +- `BPSK` 抗噪声性能最好,在 8 dB 以上时 BER 已接近 0。 +- `QPSK` 具有更高频谱效率,在中高 SNR 条件下 BER 也能快速降低。 +- `16-QAM` 虽然频谱效率高,但在低中 SNR 下 BER 明显偏高,需要更好的信道条件。 + +因此,在实际系统中,若信道条件较差,应优先使用 BPSK 或 QPSK;若信道质量较高且希望提高速率,则可以选择更高阶的 QAM。 + +### 5.3 遇到的问题与解决方法 + +1. **问题**:原始实验文件中部分中文注释编码异常,且个别位置出现了语法损坏。 + **原因分析**:文件可能在不同编辑器或不同编码环境之间传递,导致 UTF-8 内容被错误解释。 + **解决方法**:重新整理关键源码文件,将核心逻辑和注释恢复为可运行版本。 + +2. **问题**:当前运行环境中 `matplotlib` 与 `numpy 2.x` 不兼容,导入时报错。 + **原因分析**:部分已编译的依赖仍基于旧版 NumPy 构建。 + **解决方法**:在工具函数中加入基于 Pillow 的绘图回退方案,保证实验结果图片仍可正常生成。 + +3. **问题**:GitHub 提交时,本地目录最初未作为独立仓库管理,且远程主分支已有历史。 + **原因分析**:实验目录位于一个更大的 Git 顶层目录之下。 + **解决方法**:将实验目录初始化为独立仓库后,再与远程仓库进行合并并完成推送。 + +--- + +## 6. 实验心得与 AI 助手使用体会 + +### 6.1 实验心得 + +通过本次实验,我对数字调制与解调的基本思想有了更具体的理解。以前对 BPSK、QPSK、16-QAM 的认识主要停留在公式和理论图示层面,而在实际编程实现后,我更清楚地体会到了“比特分组、符号映射、信道加噪、判决恢复、误码统计”这一完整链路。 + +同时,通过星座图和 BER 曲线的结果,我直观地看到了“高阶调制频谱效率更高,但抗噪声能力更弱”的规律,这比单纯阅读教材更容易理解和记忆。 + +### 6.2 AI 助手使用体会 + +在本次实验中,AI 助手对以下任务帮助较大: + +- 快速生成基础函数框架。 +- 补全 NumPy 向量化实现。 +- 协助定位运行错误和环境兼容问题。 +- 帮助梳理 BER 测试流程和报告内容结构。 + +但 AI 不能替代人工理解,特别是在以下方面仍需要自己判断: + +- 调制映射是否符合题目要求。 +- 功率归一化是否正确。 +- 解调判决逻辑是否合理。 +- 当环境报错时,是否真正找到了根因。 + +因此,我认为 AI 更适合作为“辅助编程与调试工具”,而不是代替思考的工具。只有先理解实验原理,再使用 AI,才能真正提高效率。 + +### 6.3 改进建议 + +- 如果后续实验继续使用该仓库,建议统一源码文件编码,避免注释乱码影响开发体验。 +- 建议在依赖说明中补充与 `numpy` 版本兼容的 `matplotlib` 版本要求,减少环境问题。 +- 可以在模板中增加 BER 数据表格示例,方便学生直接填写。 + +--- + +## 7. 参考文献 + +1. John G. Proakis, Masoud Salehi. 《数字通信》。 +2. NumPy 官方文档:https://numpy.org/doc/ +3. Pytest 官方文档:https://docs.pytest.org/ +4. GitHub Copilot 使用指南与课程提供实验文档。 + +--- + +## 附录:关键实现说明 + +本次实验完成了以下核心代码内容: + +- `src/modulation.py`:实现 BPSK、QPSK、16-QAM 调制。 +- `src/demodulation.py`:实现三种调制方式的解调。 +- `src/performance_test.py`:实现 BER 性能测试与对比。 +- `src/utils.py`:提供 AWGN、BER 计算与绘图支持。 + +--- + +**声明**:本实验报告内容基于本人完成的实验结果整理而成,代码实现过程中参考了课程提供资料,并在 AI 助手辅助下完成调试与完善。 +**签名**:谭榆铃 +**日期**:2026年4月23日 diff --git a/results/16qam_ber.png b/results/16qam_ber.png new file mode 100644 index 0000000..6cdf88a Binary files /dev/null and b/results/16qam_ber.png differ diff --git a/results/16qam_constellation.png b/results/16qam_constellation.png new file mode 100644 index 0000000..aa7fcba Binary files /dev/null and b/results/16qam_constellation.png differ diff --git a/results/ber_comparison.png b/results/ber_comparison.png new file mode 100644 index 0000000..2c03d80 Binary files /dev/null and b/results/ber_comparison.png differ diff --git a/results/bpsk_ber.png b/results/bpsk_ber.png new file mode 100644 index 0000000..5bd5978 Binary files /dev/null and b/results/bpsk_ber.png differ diff --git a/results/bpsk_constellation.png b/results/bpsk_constellation.png new file mode 100644 index 0000000..2a69cf6 Binary files /dev/null and b/results/bpsk_constellation.png differ diff --git a/results/qpsk_ber.png b/results/qpsk_ber.png new file mode 100644 index 0000000..cb3b3e0 Binary files /dev/null and b/results/qpsk_ber.png differ diff --git a/results/qpsk_constellation.png b/results/qpsk_constellation.png new file mode 100644 index 0000000..577cf90 Binary files /dev/null and b/results/qpsk_constellation.png differ diff --git a/results/test_plot.png b/results/test_plot.png new file mode 100644 index 0000000..ac690ad Binary files /dev/null and b/results/test_plot.png differ diff --git a/src/demodulation.py b/src/demodulation.py index 77d95e2..08c69ab 100644 --- a/src/demodulation.py +++ b/src/demodulation.py @@ -1,6 +1,5 @@ """ -数字解调模块 -实现BPSK、QPSK、16-QAM解调算法(选做) +Digital demodulation helpers for the lab exercises. """ import numpy as np @@ -8,177 +7,93 @@ def bpsk_demodulate(symbols): """ - BPSK解调 - - 任务要求: - - 输入:接收到的复数符号序列(可能带噪声) - - 输出:恢复的比特序列 - - 判决准则: - 实部 > 0 → 比特 0 - 实部 ≤ 0 → 比特 1 - - 参数: - symbols: 接收到的复数符号数组 - - 返回: - bits: 恢复的比特数组 - - 提示: - - BPSK符号主要在实轴上 - - 只需要判断实部的正负即可 - - 噪声会使符号偏离理想位置,但判决准则仍然有效 - - 示例: - >>> symbols = np.array([0.9+0.1j, -1.1-0.05j, 0.85+0.2j]) - >>> bits = bpsk_demodulate(symbols) - >>> print(bits) - [0 1 0] + Recover BPSK bits using a zero-threshold decision on the real axis. """ - - # TODO: 实现BPSK解调 - # 提示:使用np.real()获取实部,然后判断正负 - - raise NotImplementedError("请实现BPSK解调函数") + symbols = np.asarray(symbols, dtype=np.complex128) + return (np.real(symbols) <= 0).astype(int) def qpsk_demodulate(symbols): """ - QPSK解调 - - 任务要求: - - 输入:接收到的复数符号序列 - - 输出:恢复的比特序列(长度是符号数的2倍) - - 使用最小欧氏距离判决 - - 参数: - symbols: 接收到的复数符号数组 - - 返回: - bits: 恢复的比特数组 - - 提示: - - QPSK有4个参考星座点(理想位置) - - 对每个接收符号,计算它到4个参考点的距离 - - 选择距离最小的那个点,输出对应的比特对 - - 参考点(格雷码): - (1+1j)/√2 → 00 - (-1+1j)/√2 → 01 - (-1-1j)/√2 → 11 - (1-1j)/√2 → 10 - - 示例: - >>> symbols = np.array([0.6+0.6j, -0.7+0.8j]) - >>> bits = qpsk_demodulate(symbols) - >>> print(bits) # 应该是 [0, 0, 0, 1] + Recover QPSK bits using minimum Euclidean distance detection. """ - - # 定义QPSK参考星座点(格雷码) - constellation = { - 0: (1 + 1j) / np.sqrt(2), # 00 - 1: (-1 + 1j) / np.sqrt(2), # 01 - 3: (-1 - 1j) / np.sqrt(2), # 11 - 2: (1 - 1j) / np.sqrt(2) # 10 - } - - # TODO: 实现QPSK解调 - # 提示步骤: - # 1. 对每个接收符号,计算到4个参考点的欧氏距离 - # 2. 找到距离最小的参考点 - # 3. 将参考点的索引转换为2个比特 - - raise NotImplementedError("请实现QPSK解调函数") + symbols = np.asarray(symbols, dtype=np.complex128) + reference_symbols = np.array( + [ + (1 + 1j) / np.sqrt(2), + (-1 + 1j) / np.sqrt(2), + (-1 - 1j) / np.sqrt(2), + (1 - 1j) / np.sqrt(2), + ], + dtype=np.complex128, + ) + reference_bits = np.array( + [ + [0, 0], + [0, 1], + [1, 1], + [1, 0], + ], + dtype=int, + ) + + distances = np.abs(symbols[:, None] - reference_symbols[None, :]) ** 2 + nearest_indices = np.argmin(distances, axis=1) + return reference_bits[nearest_indices].reshape(-1) def qam16_demodulate(symbols): """ - 16-QAM解调 - - 任务要求: - - 输入:接收到的复数符号序列 - - 输出:恢复的比特序列(长度是符号数的4倍) - - 使用最小欧氏距离判决 - - 参数: - symbols: 接收到的复数符号数组 - - 返回: - bits: 恢复的比特数组 - - 提示: - - 16-QAM有16个参考星座点 - - 可以分别对I路和Q路进行判决,简化计算 - - I/Q分量的判决(格雷码): - > 2/√10 → 00 - 0 ~ 2/√10 → 01 - -2/√10 ~ 0 → 11 - < -2/√10 → 10 + Recover 16-QAM bits using Gray-coded I/Q slicing. """ - - # TODO: 实现16-QAM解调 - # 提示:可以采用两种方法 - # 方法1:遍历16个参考点,找最小距离(简单但慢) - # 方法2:分别判决I路和Q路(快速且实用) - - raise NotImplementedError("请实现16-QAM解调函数") + symbols = np.asarray(symbols, dtype=np.complex128) + scaled_real = np.real(symbols) * np.sqrt(10) + scaled_imag = np.imag(symbols) * np.sqrt(10) + + def slice_component(values): + bits = np.zeros((len(values), 2), dtype=int) + bits[values < -2] = [1, 0] + bits[(values >= -2) & (values < 0)] = [1, 1] + bits[(values >= 0) & (values < 2)] = [0, 1] + bits[values >= 2] = [0, 0] + return bits + + i_bits = slice_component(scaled_real) + q_bits = slice_component(scaled_imag) + return np.hstack((i_bits, q_bits)).reshape(-1) def test_demodulation(): """ - 测试解调函数 - 需要先完成调制函数才能运行 + Run a simple smoke test for the three demodulators. """ - from modulation import bpsk_modulate, qpsk_modulate, qam16_modulate + from modulation import bpsk_modulate, qam16_modulate, qpsk_modulate from utils import add_awgn, calculate_ber - + print("=" * 50) - print("解调测试") + print("Demodulation test") print("=" * 50) - - # 测试BPSK - print("\n1. 测试BPSK解调...") - try: - bits_tx = np.random.randint(0, 2, 100) - symbols = bpsk_modulate(bits_tx) - symbols_rx = add_awgn(symbols, snr_db=10) # 添加10dB噪声 - bits_rx = bpsk_demodulate(symbols_rx) - ber = calculate_ber(bits_tx, bits_rx) - print(f" BER = {ber:.4f} (SNR=10dB)") - print(" ✅ BPSK解调测试通过") - except NotImplementedError: - print(" ⏸️ BPSK解调尚未实现") - except Exception as e: - print(f" ❌ BPSK解调测试失败: {e}") - - # 测试QPSK - print("\n2. 测试QPSK解调...") - try: - bits_tx = np.random.randint(0, 2, 100) - symbols = qpsk_modulate(bits_tx) - symbols_rx = add_awgn(symbols, snr_db=10) - bits_rx = qpsk_demodulate(symbols_rx) - ber = calculate_ber(bits_tx, bits_rx) - print(f" BER = {ber:.4f} (SNR=10dB)") - print(" ✅ QPSK解调测试通过") - except NotImplementedError: - print(" ⏸️ QPSK解调尚未实现") - except Exception as e: - print(f" ❌ QPSK解调测试失败: {e}") - - # 测试16-QAM - print("\n3. 测试16-QAM解调...") - try: - bits_tx = np.random.randint(0, 2, 100) - symbols = qam16_modulate(bits_tx) - symbols_rx = add_awgn(symbols, snr_db=15) # QAM需要更高SNR - bits_rx = qam16_demodulate(symbols_rx) - ber = calculate_ber(bits_tx, bits_rx) - print(f" BER = {ber:.4f} (SNR=15dB)") - print(" ✅ 16-QAM解调测试通过") - except NotImplementedError: - print(" ⏸️ 16-QAM解调尚未实现") - except Exception as e: - print(f" ❌ 16-QAM解调测试失败: {e}") - + + print("\n1. Testing BPSK demodulation...") + bits_tx = np.random.randint(0, 2, 100) + symbols_rx = add_awgn(bpsk_modulate(bits_tx), snr_db=10) + bits_rx = bpsk_demodulate(symbols_rx) + print(f" BER = {calculate_ber(bits_tx, bits_rx):.4f} (SNR=10 dB)") + + print("\n2. Testing QPSK demodulation...") + bits_tx = np.random.randint(0, 2, 100) + symbols_rx = add_awgn(qpsk_modulate(bits_tx), snr_db=10) + bits_rx = qpsk_demodulate(symbols_rx) + print(f" BER = {calculate_ber(bits_tx, bits_rx):.4f} (SNR=10 dB)") + + print("\n3. Testing 16-QAM demodulation...") + bits_tx = np.random.randint(0, 2, 100) + if len(bits_tx) % 4 != 0: + bits_tx = bits_tx[: len(bits_tx) - (len(bits_tx) % 4)] + symbols_rx = add_awgn(qam16_modulate(bits_tx), snr_db=15) + bits_rx = qam16_demodulate(symbols_rx) + print(f" BER = {calculate_ber(bits_tx, bits_rx):.4f} (SNR=15 dB)") + print("\n" + "=" * 50) diff --git a/src/modulation.py b/src/modulation.py index a8b5581..77a84e3 100644 --- a/src/modulation.py +++ b/src/modulation.py @@ -1,229 +1,102 @@ """ -数字调制模块 -实现BPSK、QPSK、16-QAM调制算法 +Digital modulation helpers for the lab exercises. """ import numpy as np + from utils import plot_constellation def bpsk_modulate(bits): """ - BPSK (Binary Phase Shift Keying) 调制 - - 任务要求: - - 输入:二进制比特序列(NumPy数组),元素为0或1 - - 输出:调制后的复数符号序列 - - 映射规则: - 比特 0 → 符号 +1 - 比特 1 → 符号 -1 - - 参数: - bits: 二进制比特数组,例如 np.array([0, 1, 0, 1, 1, 0]) - - 返回: - symbols: 复数符号数组,例如 np.array([1, -1, 1, -1, -1, 1]) - - 提示: - - 使用NumPy的数组运算可以很简洁地实现映射 - - 可以使用条件表达式或数学运算来完成转换 - - BPSK符号实际上是实数,但为了统一接口返回复数类型 - - 示例: - >>> bits = np.array([0, 1, 0, 1]) - >>> symbols = bpsk_modulate(bits) - >>> print(symbols) - [ 1.+0.j -1.+0.j 1.+0.j -1.+0.j] + Map binary bits to BPSK symbols. + + 0 -> +1 + 1 -> -1 """ - - # TODO: 在这里实现BPSK调制 - # 提示:可以尝试以下方式之一: - # 方法1: 使用 np.where() - # 方法2: 使用数学运算 1 - 2*bits - # 方法3: 使用字典映射 - - # 你的代码: - raise NotImplementedError("请实现BPSK调制函数") - - # return symbols + bits = np.asarray(bits, dtype=int) + symbols = (1 - 2 * bits).astype(np.complex128) + return symbols def qpsk_modulate(bits): """ - QPSK (Quadrature Phase Shift Keying) 调制 - - 任务要求: - - 输入:二进制比特序列(长度必须是2的倍数) - - 输出:调制后的复数符号序列 - - 每2个比特映射到1个符号(格雷码映射): - 00 → (1+1j)/√2 (第一象限,45°) - 01 → (-1+1j)/√2 (第二象限,135°) - 11 → (-1-1j)/√2 (第三象限,225°) - 10 → (1-1j)/√2 (第四象限,315°) - - 参数: - bits: 二进制比特数组,长度必须是偶数 - - 返回: - symbols: 复数符号数组,长度是bits的一半 - - 提示: - - 先将比特序列按2个一组进行分组 - - 可以使用reshape: bits.reshape(-1, 2) - - 符号的幅度应该归一化到单位功率:除以√2 - - 格雷码可以避免相邻星座点之间有多个比特差异 - - 示例: - >>> bits = np.array([0, 0, 0, 1, 1, 1, 1, 0]) - >>> symbols = qpsk_modulate(bits) - >>> print(symbols) - [ 0.707+0.707j -0.707+0.707j -0.707-0.707j 0.707-0.707j] + Map bits to Gray-coded QPSK symbols with unit average power. """ - - # 检查输入长度 + bits = np.asarray(bits, dtype=int) if len(bits) % 2 != 0: - raise ValueError("QPSK要求比特序列长度为偶数") - - # TODO: 在这里实现QPSK调制 - # 提示步骤: - # 1. 将比特序列reshape成(N/2, 2)的形状 - # 2. 对每一对比特,根据格雷码映射生成对应的复数符号 - # 3. 别忘了归一化:除以√2使符号功率为1 - - # 你的代码: - raise NotImplementedError("请实现QPSK调制函数") - - # return symbols + raise ValueError("QPSK requires an even number of bits.") + + bit_pairs = bits.reshape(-1, 2) + gray_map = { + (0, 0): 1 + 1j, + (0, 1): -1 + 1j, + (1, 1): -1 - 1j, + (1, 0): 1 - 1j, + } + symbols = np.array([gray_map[tuple(pair)] for pair in bit_pairs], dtype=np.complex128) + return symbols / np.sqrt(2) def qam16_modulate(bits): """ - 16-QAM (16-Quadrature Amplitude Modulation) 调制 - - 任务要求: - - 输入:二进制比特序列(长度必须是4的倍数) - - 输出:调制后的复数符号序列 - - 每4个比特映射到1个符号 - - I路和Q路分量取值:{-3, -1, +1, +3} - - 使用格雷码映射(推荐) - - 参数: - bits: 二进制比特数组,长度必须是4的倍数 - - 返回: - symbols: 复数符号数组,长度是bits的四分之一 - - 提示: - - 16-QAM有16个星座点,排列成4×4的方格 - - 可以将4个比特分成两组:前2位决定I分量,后2位决定Q分量 - - I/Q分量的映射(格雷码): - 00 → +3 - 01 → +1 - 11 → -1 - 10 → -3 - - 需要对星座图进行功率归一化 - - 平均功率 = (3²+1²+1²+3²)/4 = 5,归一化因子 = √10 - - 示例: - >>> bits = np.array([0, 0, 0, 0, 0, 1, 0, 1]) - >>> symbols = qam16_modulate(bits) - # 应该得到两个符号在正确位置 + Map bits to Gray-coded 16-QAM symbols with unit average power. """ - - # 检查输入长度 + bits = np.asarray(bits, dtype=int) if len(bits) % 4 != 0: - raise ValueError("16-QAM要求比特序列长度为4的倍数") - - # TODO: 在这里实现16-QAM调制 - # 提示步骤: - # 1. 将比特序列reshape成(N/4, 4)的形状 - # 2. 对每组4个比特: - # - 前2位映射到I分量(实部) - # - 后2位映射到Q分量(虚部) - # 3. 使用格雷码映射:00→+3, 01→+1, 11→-1, 10→-3 - # 4. 归一化:除以√10使平均功率为1 - - # 格雷码映射字典(可选使用) + raise ValueError("16-QAM requires the number of bits to be a multiple of 4.") + + bit_groups = bits.reshape(-1, 4) gray_map = { (0, 0): 3, (0, 1): 1, (1, 1): -1, - (1, 0): -3 + (1, 0): -3, } - - # 你的代码: - raise NotImplementedError("请实现16-QAM调制函数") - - # return symbols + + i_values = np.array([gray_map[tuple(group[:2])] for group in bit_groups], dtype=float) + q_values = np.array([gray_map[tuple(group[2:])] for group in bit_groups], dtype=float) + symbols = i_values + 1j * q_values + return symbols / np.sqrt(10) def test_modulation(): """ - 测试调制函数并生成星座图 + Generate example constellation plots for all modulation schemes. """ print("=" * 50) - print("数字调制测试") + print("Digital modulation test") print("=" * 50) - - # 测试BPSK - print("\n1. 测试BPSK调制...") - try: - bits_bpsk = np.random.randint(0, 2, 1000) - symbols_bpsk = bpsk_modulate(bits_bpsk) - print(f" 输入比特数: {len(bits_bpsk)}") - print(f" 输出符号数: {len(symbols_bpsk)}") - print(f" 唯一符号: {np.unique(symbols_bpsk)}") - - # 绘制星座图 - plot_constellation(symbols_bpsk[:100], - "BPSK星座图", - "bpsk_constellation.png") - print(" ✅ BPSK测试通过") - except NotImplementedError: - print(" ⏸️ BPSK尚未实现") - except Exception as e: - print(f" ❌ BPSK测试失败: {e}") - - # 测试QPSK - print("\n2. 测试QPSK调制...") - try: - bits_qpsk = np.random.randint(0, 2, 1000) - symbols_qpsk = qpsk_modulate(bits_qpsk) - print(f" 输入比特数: {len(bits_qpsk)}") - print(f" 输出符号数: {len(symbols_qpsk)}") - print(f" 符号幅度: {np.abs(symbols_qpsk[:4])}") - - # 绘制星座图 - plot_constellation(symbols_qpsk[:200], - "QPSK星座图", - "qpsk_constellation.png") - print(" ✅ QPSK测试通过") - except NotImplementedError: - print(" ⏸️ QPSK尚未实现") - except Exception as e: - print(f" ❌ QPSK测试失败: {e}") - - # 测试16-QAM - print("\n3. 测试16-QAM调制...") - try: - bits_qam = np.random.randint(0, 2, 1000) - symbols_qam = qam16_modulate(bits_qam) - print(f" 输入比特数: {len(bits_qam)}") - print(f" 输出符号数: {len(symbols_qam)}") - print(f" 唯一符号数量: {len(np.unique(symbols_qam))}") - - # 绘制星座图 - plot_constellation(symbols_qam[:250], - "16-QAM星座图", - "16qam_constellation.png") - print(" ✅ 16-QAM测试通过") - except NotImplementedError: - print(" ⏸️ 16-QAM尚未实现") - except Exception as e: - print(f" ❌ 16-QAM测试失败: {e}") - + + print("\n1. Testing BPSK...") + bits_bpsk = np.random.randint(0, 2, 1000) + symbols_bpsk = bpsk_modulate(bits_bpsk) + print(f" Input bits: {len(bits_bpsk)}") + print(f" Output symbols: {len(symbols_bpsk)}") + print(f" Unique symbols: {np.unique(symbols_bpsk)}") + plot_constellation(symbols_bpsk[:100], "BPSK Constellation", "bpsk_constellation.png") + print(" BPSK test passed") + + print("\n2. Testing QPSK...") + bits_qpsk = np.random.randint(0, 2, 1000) + symbols_qpsk = qpsk_modulate(bits_qpsk) + print(f" Input bits: {len(bits_qpsk)}") + print(f" Output symbols: {len(symbols_qpsk)}") + print(f" Symbol magnitudes: {np.abs(symbols_qpsk[:4])}") + plot_constellation(symbols_qpsk[:200], "QPSK Constellation", "qpsk_constellation.png") + print(" QPSK test passed") + + print("\n3. Testing 16-QAM...") + bits_qam = np.random.randint(0, 2, 1000) + symbols_qam = qam16_modulate(bits_qam) + print(f" Input bits: {len(bits_qam)}") + print(f" Output symbols: {len(symbols_qam)}") + print(f" Unique symbols: {len(np.unique(symbols_qam))}") + plot_constellation(symbols_qam[:250], "16-QAM Constellation", "16qam_constellation.png") + print(" 16-QAM test passed") + print("\n" + "=" * 50) - print("测试完成!请检查results/目录中的星座图。") + print("Finished. Check the plots in the results directory.") print("=" * 50) diff --git a/src/performance_test.py b/src/performance_test.py index a3aa0a9..2929c1d 100644 --- a/src/performance_test.py +++ b/src/performance_test.py @@ -1,143 +1,153 @@ """ -性能测试模块 -测试调制解调系统在不同SNR下的BER性能(选做) +BER performance analysis for the modulation and demodulation chain. """ +import os + import numpy as np -from modulation import bpsk_modulate, qpsk_modulate, qam16_modulate -from demodulation import bpsk_demodulate, qpsk_demodulate, qam16_demodulate + +from demodulation import bpsk_demodulate, qam16_demodulate, qpsk_demodulate +from modulation import bpsk_modulate, qam16_modulate, qpsk_modulate from utils import add_awgn, calculate_ber, plot_ber_curve, generate_random_bits -def test_ber_performance(modulation_scheme='BPSK', num_bits=10000, snr_range=None): +def _normalize_num_bits(modulation_scheme, num_bits): + if modulation_scheme == "QPSK": + return num_bits - (num_bits % 2) + if modulation_scheme == "16QAM": + return num_bits - (num_bits % 4) + return num_bits + + +def test_ber_performance(modulation_scheme="BPSK", num_bits=10000, snr_range=None): """ - 测试指定调制方式的BER性能 - - 参数: - modulation_scheme: 'BPSK', 'QPSK', 或 '16QAM' - num_bits: 用于测试的比特数量 - snr_range: SNR范围(dB),例如 np.arange(0, 16, 2) - - 返回: - snr_db: SNR值数组 - ber_values: 对应的BER值数组 + Measure BER for a modulation scheme over a range of SNR values. """ - if snr_range is None: - snr_range = np.arange(0, 16, 2) # 0, 2, 4, ..., 14 dB - - ber_values = [] - - print(f"\n测试 {modulation_scheme} 性能...") - print(f"比特数: {num_bits}, SNR范围: {snr_range[0]}~{snr_range[-1]} dB") - print("-" * 40) - - # 选择调制/解调函数 - if modulation_scheme == 'BPSK': + snr_range = np.arange(0, 16, 2) + + if modulation_scheme == "BPSK": modulate_func = bpsk_modulate demodulate_func = bpsk_demodulate - elif modulation_scheme == 'QPSK': + elif modulation_scheme == "QPSK": modulate_func = qpsk_modulate demodulate_func = qpsk_demodulate - elif modulation_scheme == '16QAM': + elif modulation_scheme == "16QAM": modulate_func = qam16_modulate demodulate_func = qam16_demodulate else: - raise ValueError(f"不支持的调制方式: {modulation_scheme}") - - # 对每个SNR值进行测试 + raise ValueError(f"Unsupported modulation scheme: {modulation_scheme}") + + num_bits = _normalize_num_bits(modulation_scheme, num_bits) + if num_bits <= 0: + raise ValueError("num_bits is too small for the selected modulation scheme.") + + ber_values = [] + + print(f"\nTesting {modulation_scheme} BER performance...") + print(f"Bits: {num_bits}, SNR range: {snr_range[0]} to {snr_range[-1]} dB") + print("-" * 40) + for snr_db in snr_range: - # TODO: 完成性能测试的主循环 - # 提示步骤: - # 1. 生成随机比特序列 - # 2. 调制 - # 3. 添加AWGN噪声 - # 4. 解调 - # 5. 计算BER - # 6. 将BER添加到ber_values列表 - - # 你的代码: bits_tx = generate_random_bits(num_bits) - symbols = modulate_func(bits_tx) - symbols_rx = add_awgn(symbols, snr_db) + symbols_tx = modulate_func(bits_tx) + symbols_rx = add_awgn(symbols_tx, snr_db) bits_rx = demodulate_func(symbols_rx) ber = calculate_ber(bits_tx, bits_rx) - ber_values.append(ber) print(f"SNR = {snr_db:2d} dB, BER = {ber:.6f}") - - return snr_range, np.array(ber_values) + + return np.array(snr_range), np.array(ber_values) + + +def _save_comparison_plot(snr_bpsk, ber_bpsk, snr_qpsk, ber_qpsk, snr_qam, ber_qam): + """ + Save a BER comparison figure without relying on Matplotlib. + """ + os.makedirs("results", exist_ok=True) + filepath = os.path.join("results", "ber_comparison.png") + + from PIL import Image, ImageDraw + + width = 1000 + height = 650 + margin = 80 + image = Image.new("RGB", (width, height), "white") + draw = ImageDraw.Draw(image) + draw.rectangle((margin, margin, width - margin, height - margin), outline="black", width=2) + draw.text((40, 30), "BER Comparison", fill="black") + + all_snr = np.concatenate([snr_bpsk, snr_qpsk, snr_qam]).astype(float) + all_ber = np.concatenate([ber_bpsk, ber_qpsk, ber_qam]).astype(float) + all_ber = np.maximum(all_ber, 1e-6) + x_min = float(np.min(all_snr)) + x_max = float(np.max(all_snr)) + y_min = -6.0 + y_max = 0.0 + + def map_x(value): + return margin + (value - x_min) / (x_max - x_min) * (width - 2 * margin) + + def map_y(value): + log_value = np.log10(max(float(value), 1e-6)) + return height - margin - (log_value - y_min) / (y_max - y_min) * (height - 2 * margin) + + series = [ + ("BPSK", snr_bpsk, ber_bpsk, (0, 102, 204)), + ("QPSK", snr_qpsk, ber_qpsk, (204, 0, 0)), + ("16-QAM", snr_qam, ber_qam, (0, 153, 0)), + ] + + for index, (label, snr_values, ber_values, color) in enumerate(series): + points = [(map_x(x), map_y(y)) for x, y in zip(snr_values, np.maximum(ber_values, 1e-6))] + for start, end in zip(points[:-1], points[1:]): + draw.line((start[0], start[1], end[0], end[1]), fill=color, width=3) + for x, y in points: + draw.ellipse((x - 5, y - 5, x + 5, y + 5), fill=color, outline="black") + legend_y = 70 + index * 30 + draw.rectangle((width - 220, legend_y, width - 200, legend_y + 20), fill=color, outline="black") + draw.text((width - 190, legend_y), label, fill="black") + + draw.text((width // 2 - 30, height - 40), "SNR (dB)", fill="black") + draw.text((20, height // 2 - 10), "log10(BER)", fill="black") + image.save(filepath) + + print(f"\nSaved BER comparison plot to: {filepath}") def compare_modulations(): """ - 比较不同调制方式的性能 - 绘制BER对比曲线 + Compare BER performance of BPSK, QPSK and 16-QAM. """ - print("=" * 50) - print("数字调制性能对比测试") + print("Digital modulation BER comparison") print("=" * 50) - + snr_range = np.arange(0, 16, 2) - - # TODO: 测试各种调制方式并绘制对比图 - # 提示: - # 1. 分别测试BPSK、QPSK、16-QAM - # 2. 收集所有的BER数据 - # 3. 在一张图上绘制多条曲线 - - try: - # 测试BPSK - snr_bpsk, ber_bpsk = test_ber_performance('BPSK', num_bits=10000, snr_range=snr_range) - - # 测试QPSK - snr_qpsk, ber_qpsk = test_ber_performance('QPSK', num_bits=10000, snr_range=snr_range) - - # 测试16-QAM - snr_qam, ber_qam = test_ber_performance('16QAM', num_bits=10000, snr_range=snr_range) - - # 绘制对比图 - import matplotlib.pyplot as plt - import os - - plt.figure(figsize=(10, 6)) - plt.semilogy(snr_bpsk, ber_bpsk, 'b-o', label='BPSK', linewidth=2) - plt.semilogy(snr_qpsk, ber_qpsk, 'r-s', label='QPSK', linewidth=2) - plt.semilogy(snr_qam, ber_qam, 'g-^', label='16-QAM', linewidth=2) - - plt.xlabel('SNR (dB)', fontsize=12) - plt.ylabel('Bit Error Rate (BER)', fontsize=12) - plt.title('数字调制方式性能对比', fontsize=14, fontweight='bold') - plt.legend(fontsize=11) - plt.grid(True, which='both', alpha=0.3) - - os.makedirs('results', exist_ok=True) - filepath = os.path.join('results', 'ber_comparison.png') - plt.savefig(filepath, dpi=300, bbox_inches='tight') - print(f"\n✅ 性能对比图已保存到: {filepath}") - - plt.close() - - except NotImplementedError as e: - print(f"\n⏸️ 部分函数尚未实现: {e}") - except Exception as e: - print(f"\n❌ 测试失败: {e}") - + + snr_bpsk, ber_bpsk = test_ber_performance("BPSK", num_bits=10000, snr_range=snr_range) + snr_qpsk, ber_qpsk = test_ber_performance("QPSK", num_bits=10000, snr_range=snr_range) + snr_qam, ber_qam = test_ber_performance("16QAM", num_bits=10000, snr_range=snr_range) + + plot_ber_curve(snr_bpsk, ber_bpsk, title="BPSK BER Performance", filename="bpsk_ber.png") + plot_ber_curve(snr_qpsk, ber_qpsk, title="QPSK BER Performance", filename="qpsk_ber.png") + plot_ber_curve(snr_qam, ber_qam, title="16-QAM BER Performance", filename="16qam_ber.png") + _save_comparison_plot(snr_bpsk, ber_bpsk, snr_qpsk, ber_qpsk, snr_qam, ber_qam) + + print("\nSummary:") + print(f" BPSK final BER @ {snr_range[-1]} dB: {ber_bpsk[-1]:.6f}") + print(f" QPSK final BER @ {snr_range[-1]} dB: {ber_qpsk[-1]:.6f}") + print(f" 16-QAM final BER @ {snr_range[-1]} dB: {ber_qam[-1]:.6f}") print("\n" + "=" * 50) + return { + "BPSK": (snr_bpsk, ber_bpsk), + "QPSK": (snr_qpsk, ber_qpsk), + "16QAM": (snr_qam, ber_qam), + } + def main(): - """ - 主测试函数 - """ - # 你可以选择运行单个调制方式测试或对比测试 - - # 选项1: 测试单个调制方式 - # snr_range, ber_values = test_ber_performance('BPSK', num_bits=10000) - # plot_ber_curve(snr_range, ber_values, title="BPSK BER性能", filename="bpsk_ber.png") - - # 选项2: 对比测试(推荐) compare_modulations() diff --git a/src/utils.py b/src/utils.py index bc0182a..1a44661 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,175 +1,171 @@ """ -工具函数模块 -提供绘图、信号处理等辅助功能 +Utility helpers for plotting and simple communication experiments. """ -import numpy as np -import matplotlib.pyplot as plt -from matplotlib import font_manager import os +import numpy as np +from PIL import Image, ImageDraw + def setup_chinese_font(): """ - 设置matplotlib支持中文显示 + Configure Matplotlib fonts so Chinese labels render when available. """ try: - # Windows系统使用微软雅黑 - plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial Unicode MS'] - plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 + import matplotlib.pyplot as plt + + plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "Arial Unicode MS"] + plt.rcParams["axes.unicode_minus"] = False except Exception: - print("警告: 无法设置中文字体,图表标签可能显示为方框") + print("Warning: unable to configure Chinese fonts for Matplotlib.") def plot_constellation(symbols, title, filename, show_grid=True): """ - 绘制星座图 - - 参数: - symbols: 复数符号数组 - title: 图表标题 - filename: 保存的文件名(相对于results/目录) - show_grid: 是否显示网格 + Plot a constellation diagram and save it into the results directory. """ - setup_chinese_font() - - # 创建results目录(如果不存在) - os.makedirs('results', exist_ok=True) - - plt.figure(figsize=(8, 8)) - - # 绘制星座点 + os.makedirs("results", exist_ok=True) + + symbols = np.asarray(symbols) real_parts = np.real(symbols) imag_parts = np.imag(symbols) - plt.scatter(real_parts, imag_parts, s=100, c='blue', marker='o', alpha=0.6, edgecolors='black') - - # 设置坐标轴 - max_val = max(np.max(np.abs(real_parts)), np.max(np.abs(imag_parts))) * 1.2 - plt.xlim(-max_val, max_val) - plt.ylim(-max_val, max_val) - - # 添加坐标轴线 - plt.axhline(y=0, color='k', linestyle='-', linewidth=0.5) - plt.axvline(x=0, color='k', linestyle='-', linewidth=0.5) - - # 网格 + filepath = os.path.join("results", filename) + + _plot_constellation_with_pillow(real_parts, imag_parts, title, filepath, show_grid) + print(f"Saved constellation plot to: {filepath}") + + +def _plot_constellation_with_pillow(real_parts, imag_parts, title, filepath, show_grid): + """ + Lightweight fallback renderer used when Matplotlib cannot be imported. + """ + width = 900 + height = 900 + margin = 90 + image = Image.new("RGB", (width, height), "white") + draw = ImageDraw.Draw(image) + + max_component = max(np.max(np.abs(real_parts)), np.max(np.abs(imag_parts)), 1.0) + limit = max_component * 1.2 + plot_size = min(width, height) - 2 * margin + + def map_x(value): + return margin + (value + limit) / (2 * limit) * plot_size + + def map_y(value): + return height - (margin + (value + limit) / (2 * limit) * plot_size) + if show_grid: - plt.grid(True, alpha=0.3) - - # 标签 - plt.xlabel('实部 (In-phase)', fontsize=12) - plt.ylabel('虚部 (Quadrature)', fontsize=12) - plt.title(title, fontsize=14, fontweight='bold') - - # 设置相等的纵横比 - plt.gca().set_aspect('equal', adjustable='box') - - # 保存 - filepath = os.path.join('results', filename) - plt.savefig(filepath, dpi=300, bbox_inches='tight') - print(f"星座图已保存到: {filepath}") - - plt.close() + for fraction in np.linspace(-1, 1, 5): + x = map_x(fraction * limit) + y = map_y(fraction * limit) + draw.line((x, margin, x, height - margin), fill=(220, 220, 220), width=1) + draw.line((margin, y, width - margin, y), fill=(220, 220, 220), width=1) + + draw.rectangle((margin, margin, width - margin, height - margin), outline="black", width=2) + draw.line((map_x(0), margin, map_x(0), height - margin), fill="black", width=2) + draw.line((margin, map_y(0), width - margin, map_y(0)), fill="black", width=2) + + for real_value, imag_value in zip(real_parts, imag_parts): + x = map_x(real_value) + y = map_y(imag_value) + radius = 8 + draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=(0, 102, 204), outline="black") + + draw.text((margin, 20), title, fill="black") + draw.text((width // 2 - 35, height - 40), "In-phase", fill="black") + draw.text((20, height // 2 - 10), "Quadrature", fill="black") + image.save(filepath) def add_awgn(signal, snr_db): """ - 向信号中添加加性高斯白噪声 (AWGN) - - 参数: - signal: 输入信号(复数数组) - snr_db: 信噪比(dB) - - 返回: - 加噪后的信号 + Add complex AWGN to a signal for a given SNR in dB. """ - # 计算信号功率 + signal = np.asarray(signal) signal_power = np.mean(np.abs(signal) ** 2) - - # 计算噪声功率 snr_linear = 10 ** (snr_db / 10) noise_power = signal_power / snr_linear - - # 生成复高斯噪声 + noise_real = np.random.normal(0, np.sqrt(noise_power / 2), signal.shape) noise_imag = np.random.normal(0, np.sqrt(noise_power / 2), signal.shape) noise = noise_real + 1j * noise_imag - return signal + noise def calculate_ber(bits_tx, bits_rx): """ - 计算误比特率 (BER) - - 参数: - bits_tx: 发送的比特序列 - bits_rx: 接收的比特序列 - - 返回: - BER值(0到1之间) + Calculate bit error rate. """ + bits_tx = np.asarray(bits_tx) + bits_rx = np.asarray(bits_rx) if len(bits_tx) != len(bits_rx): - raise ValueError("发送和接收的比特序列长度不一致") - + raise ValueError("Transmitted and received bit sequences must have the same length.") + errors = np.sum(bits_tx != bits_rx) - ber = errors / len(bits_tx) - return ber + return errors / len(bits_tx) def plot_ber_curve(snr_range, ber_values, title="BER vs SNR", filename="ber_curve.png"): """ - 绘制BER性能曲线 - - 参数: - snr_range: SNR值数组(dB) - ber_values: 对应的BER值数组 - title: 图表标题 - filename: 保存的文件名 + Plot and save a BER curve. """ - setup_chinese_font() - - # 创建results目录 - os.makedirs('results', exist_ok=True) - - plt.figure(figsize=(10, 6)) - plt.semilogy(snr_range, ber_values, 'b-o', linewidth=2, markersize=8) - - plt.xlabel('SNR (dB)', fontsize=12) - plt.ylabel('Bit Error Rate (BER)', fontsize=12) - plt.title(title, fontsize=14, fontweight='bold') - plt.grid(True, which='both', alpha=0.3) - - # 保存 - filepath = os.path.join('results', filename) - plt.savefig(filepath, dpi=300, bbox_inches='tight') - print(f"BER曲线已保存到: {filepath}") - - plt.close() + os.makedirs("results", exist_ok=True) + + filepath = os.path.join("results", filename) + + width = 1000 + height = 600 + margin = 80 + image = Image.new("RGB", (width, height), "white") + draw = ImageDraw.Draw(image) + draw.rectangle((margin, margin, width - margin, height - margin), outline="black", width=2) + draw.text((margin, 20), title, fill="black") + + snr_range = np.asarray(snr_range, dtype=float) + ber_values = np.asarray(ber_values, dtype=float) + ber_values = np.maximum(ber_values, 1e-6) + + x_min = float(np.min(snr_range)) + x_max = float(np.max(snr_range)) + y_min = -6.0 + y_max = 0.0 + + def map_x(value): + if x_max == x_min: + return width / 2 + return margin + (value - x_min) / (x_max - x_min) * (width - 2 * margin) + + def map_y(value): + log_value = np.log10(value) + return height - margin - (log_value - y_min) / (y_max - y_min) * (height - 2 * margin) + + points = [(map_x(x), map_y(y)) for x, y in zip(snr_range, ber_values)] + for point in points: + x, y = point + draw.ellipse((x - 5, y - 5, x + 5, y + 5), fill=(0, 102, 204), outline="black") + for start, end in zip(points[:-1], points[1:]): + draw.line((start[0], start[1], end[0], end[1]), fill=(0, 102, 204), width=3) + + draw.text((width // 2 - 30, height - 40), "SNR (dB)", fill="black") + draw.text((20, height // 2 - 10), "log10(BER)", fill="black") + image.save(filepath) + + print(f"Saved BER curve to: {filepath}") def generate_random_bits(n): """ - 生成随机比特序列 - - 参数: - n: 比特数量 - - 返回: - 长度为n的随机比特数组(0或1) + Generate a random binary sequence of length n. """ return np.random.randint(0, 2, n) if __name__ == "__main__": - print("工具函数模块测试...") - - # 测试随机比特生成 + print("Utility module self-test...") bits = generate_random_bits(100) - print(f"生成了 {len(bits)} 个随机比特") - - # 测试星座图绘制 - test_symbols = np.array([1+1j, -1+1j, -1-1j, 1-1j]) / np.sqrt(2) - plot_constellation(test_symbols, "测试星座图", "test_constellation.png") - - print("工具函数测试完成!") + print(f"Generated {len(bits)} random bits") + test_symbols = np.array([1 + 1j, -1 + 1j, -1 - 1j, 1 - 1j]) / np.sqrt(2) + plot_constellation(test_symbols, "Test Constellation", "test_constellation.png") + print("Utility module self-test completed")