From e082b83bb65124904374b1151ebf40ae815afb7a Mon Sep 17 00:00:00 2001 From: tanyuling226 <1375639893@qq.com> Date: Thu, 23 Apr 2026 20:41:00 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=AE=8C=E6=88=90=E8=B0=83=E5=88=B6?= =?UTF-8?q?=E3=80=81=E8=A7=A3=E8=B0=83=E4=B8=8EBER=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=AE=9E=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/grading.yml | 164 ++++++++++++++ .gitignore | 49 +++++ DEPLOYMENT_CHECKLIST.md | 372 +++++++++++++++++++++++++++++++ PROJECT_README.md | 315 ++++++++++++++++++++++++++ README.md | 285 ++++++++++++++++++++++++ REPORT_TEMPLATE.md | 177 +++++++++++++++ TEACHER_GUIDE.md | 322 +++++++++++++++++++++++++++ docs/copilot_guide.md | 379 ++++++++++++++++++++++++++++++++ docs/git_quickstart.md | 372 +++++++++++++++++++++++++++++++ docs/theory_bpsk.md | 115 ++++++++++ docs/theory_qam.md | 220 ++++++++++++++++++ docs/theory_qpsk.md | 169 ++++++++++++++ examples/generate_examples.py | 142 ++++++++++++ grading/calculate_grade.py | 252 +++++++++++++++++++++ grading/check_report.py | 115 ++++++++++ grading/test_bpsk.py | 91 ++++++++ grading/test_qam16.py | 160 ++++++++++++++ grading/test_qpsk.py | 121 ++++++++++ requirements.txt | 7 + results/16qam_ber.png | Bin 0 -> 8106 bytes results/16qam_constellation.png | Bin 0 -> 8174 bytes results/ber_comparison.png | Bin 0 -> 13440 bytes results/bpsk_ber.png | Bin 0 -> 8913 bytes results/bpsk_constellation.png | Bin 0 -> 7508 bytes results/qpsk_ber.png | Bin 0 -> 8708 bytes results/qpsk_constellation.png | Bin 0 -> 7657 bytes results/test_plot.png | Bin 0 -> 14218 bytes src/demodulation.py | 101 +++++++++ src/modulation.py | 104 +++++++++ src/performance_test.py | 155 +++++++++++++ src/test_environment.py | 146 ++++++++++++ src/utils.py | 171 ++++++++++++++ 32 files changed, 4504 insertions(+) create mode 100644 .github/workflows/grading.yml create mode 100644 .gitignore create mode 100644 DEPLOYMENT_CHECKLIST.md create mode 100644 PROJECT_README.md create mode 100644 README.md create mode 100644 REPORT_TEMPLATE.md create mode 100644 TEACHER_GUIDE.md create mode 100644 docs/copilot_guide.md create mode 100644 docs/git_quickstart.md create mode 100644 docs/theory_bpsk.md create mode 100644 docs/theory_qam.md create mode 100644 docs/theory_qpsk.md create mode 100644 examples/generate_examples.py create mode 100644 grading/calculate_grade.py create mode 100644 grading/check_report.py create mode 100644 grading/test_bpsk.py create mode 100644 grading/test_qam16.py create mode 100644 grading/test_qpsk.py create mode 100644 requirements.txt create mode 100644 results/16qam_ber.png create mode 100644 results/16qam_constellation.png create mode 100644 results/ber_comparison.png create mode 100644 results/bpsk_ber.png create mode 100644 results/bpsk_constellation.png create mode 100644 results/qpsk_ber.png create mode 100644 results/qpsk_constellation.png create mode 100644 results/test_plot.png create mode 100644 src/demodulation.py create mode 100644 src/modulation.py create mode 100644 src/performance_test.py create mode 100644 src/test_environment.py create mode 100644 src/utils.py diff --git a/.github/workflows/grading.yml b/.github/workflows/grading.yml new file mode 100644 index 0000000..2a546b7 --- /dev/null +++ b/.github/workflows/grading.yml @@ -0,0 +1,164 @@ +name: 自动评分系统 + +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - main + +jobs: + grading: + runs-on: ubuntu-latest + + steps: + - name: 检出代码 + uses: actions/checkout@v3 + + - name: 设置Python环境 + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: 安装依赖 + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: 运行环境测试 + id: env_test + continue-on-error: true + run: | + python src/test_environment.py + + - name: 运行BPSK测试 + id: bpsk_test + continue-on-error: true + run: | + python -m pytest grading/test_bpsk.py -v --tb=short > bpsk_result.txt 2>&1 + cat bpsk_result.txt + + - name: 运行QPSK测试 + id: qpsk_test + continue-on-error: true + run: | + python -m pytest grading/test_qpsk.py -v --tb=short > qpsk_result.txt 2>&1 + cat qpsk_result.txt + + - name: 运行16-QAM测试 + id: qam_test + continue-on-error: true + run: | + python -m pytest grading/test_qam16.py -v --tb=short > qam_result.txt 2>&1 + cat qam_result.txt + + - name: 检查实验报告 + id: report_check + continue-on-error: true + run: | + python grading/check_report.py > report_result.txt 2>&1 + cat report_result.txt + + - name: 代码质量检查 + id: pylint_check + continue-on-error: true + run: | + python -m pylint src/modulation.py --score=y > pylint_result.txt 2>&1 || true + cat pylint_result.txt + + - name: 计算总评分 + id: calculate_grade + run: | + python grading/calculate_grade.py > grade_result.txt 2>&1 + cat grade_result.txt + + - name: 上传评分报告 + uses: actions/upload-artifact@v3 + with: + name: grading-report + path: | + grade_report.json + grade_result.txt + bpsk_result.txt + qpsk_result.txt + qam_result.txt + report_result.txt + pylint_result.txt + + - name: 评论PR + if: github.event_name == 'pull_request' + uses: actions/github-script@v6 + with: + script: | + const fs = require('fs'); + + // 读取评分结果 + let gradeText = ''; + try { + gradeText = fs.readFileSync('grade_result.txt', 'utf8'); + } catch (error) { + gradeText = '评分系统运行失败,请检查代码。'; + } + + // 构造评论内容 + const comment = `## 🤖 自动评分结果 + + ${gradeText} + + --- + + ### 📊 详细测试结果 + +
+ 点击查看BPSK测试详情 + + \`\`\` + ${fs.readFileSync('bpsk_result.txt', 'utf8').substring(0, 2000)} + \`\`\` + +
+ +
+ 点击查看QPSK测试详情 + + \`\`\` + ${fs.readFileSync('qpsk_result.txt', 'utf8').substring(0, 2000)} + \`\`\` + +
+ +
+ 点击查看16-QAM测试详情 + + \`\`\` + ${fs.readFileSync('qam_result.txt', 'utf8').substring(0, 2000)} + \`\`\` + +
+ +
+ 点击查看报告检查详情 + + \`\`\` + ${fs.readFileSync('report_result.txt', 'utf8').substring(0, 1000)} + \`\`\` + +
+ + --- + + 💡 **提示**: + - 如果测试未通过,请查看上方的详细错误信息 + - 修改代码后重新提交会触发自动评分 + - 完整的评分报告可在 Actions 的 Artifacts 中下载 + + ⏰ **评分时间**: ${new Date().toLocaleString('zh-CN', {timeZone: 'Asia/Shanghai'})} + `; + + // 发布评论 + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92ca28b --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 实验结果 +results/ +*.png +*.jpg +*.pdf +!results/ +!results/*.png + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# 测试 +.pytest_cache/ +.coverage +htmlcov/ +.tox/ diff --git a/DEPLOYMENT_CHECKLIST.md b/DEPLOYMENT_CHECKLIST.md new file mode 100644 index 0000000..351f118 --- /dev/null +++ b/DEPLOYMENT_CHECKLIST.md @@ -0,0 +1,372 @@ +# 🎉 数字调制解调实验平台 - 部署完成 + +## ✅ 已完成的工作 + +### 📁 文件结构(共32个文件) + +``` +wireless-modulation-experiment/ +├── 📄 README.md # 学生实验指南(主文档) +├── 📄 PROJECT_README.md # 项目总览 +├── 📄 TEACHER_GUIDE.md # 教师使用说明 +├── 📄 REPORT_TEMPLATE.md # 实验报告模板 +├── 📄 requirements.txt # Python依赖清单 +├── 📄 .gitignore # Git忽略规则 +│ +├── 📁 .github/workflows/ +│ └── 📄 grading.yml # GitHub Actions自动评分工作流 +│ +├── 📁 src/ (学生代码区) +│ ├── 📄 modulation.py # 调制函数(含TODO模板) +│ ├── 📄 demodulation.py # 解调函数(选做) +│ ├── 📄 performance_test.py # 性能测试(选做) +│ ├── 📄 utils.py # 工具函数(已完整实现) +│ └── 📄 test_environment.py # 环境测试脚本 +│ +├── 📁 grading/ (自动评分系统) +│ ├── 📄 test_bpsk.py # BPSK单元测试(6个测试用例) +│ ├── 📄 test_qpsk.py # QPSK单元测试(9个测试用例) +│ ├── 📄 test_qam16.py # 16-QAM单元测试(9个测试用例) +│ ├── 📄 check_report.py # 实验报告自动检查 +│ └── 📄 calculate_grade.py # 总评分计算脚本 +│ +├── 📁 docs/ (实验文档) +│ ├── 📄 theory_bpsk.md # BPSK原理详解 +│ ├── 📄 theory_qpsk.md # QPSK原理详解 +│ ├── 📄 theory_qam.md # 16-QAM原理详解 +│ ├── 📄 copilot_guide.md # GitHub Copilot使用指南 +│ └── 📄 git_quickstart.md # Git快速入门教程 +│ +├── 📁 examples/ (示例输出) +│ ├── 🖼️ bpsk_constellation.png # BPSK星座图示例 +│ ├── 🖼️ qpsk_constellation.png # QPSK星座图示例 +│ ├── 🖼️ 16qam_constellation.png # 16-QAM星座图示例 +│ ├── 🖼️ ber_curve_example.png # BER性能曲线示例 +│ └── 📄 generate_examples.py # 示例生成脚本 +│ +└── 📁 results/ (学生输出目录) + └── .gitkeep +``` + +--- + +## 🎯 核心功能 + +### 1. 学生实验任务(6个) + +#### 必做任务(75分) +- ✅ **任务0**: 环境配置(5分) + - 使用Copilot Agent或手动安装 + - 环境测试脚本验证 + +- ✅ **任务1**: BPSK调制(25分) + - 实现二进制相移键控 + - 生成并保存星座图 + +- ✅ **任务2**: QPSK调制(25分) + - 实现正交相移键控 + - 格雷码映射 + +- ✅ **任务3**: 16-QAM调制(20分) + - 实现正交幅度调制 + - 功率归一化 + +#### 选做任务(20分加分) +- ✅ **任务4**: 解调实现(10分) + - 最小欧氏距离判决 + +- ✅ **任务5**: BER性能分析(10分) + - 生成BER vs SNR曲线 + +#### 实验报告(15分) +- ✅ **任务6**: 完整实验报告 + - 7个章节模板 + - 自动完整性检查 + +--- + +### 2. 自动评分系统 + +#### 测试覆盖(24个测试用例) + +**BPSK测试** (6个): +- ✅ 基本映射规则 +- ✅ 全0/全1边界测试 +- ✅ 符号取值验证 +- ✅ 随机序列测试 +- ✅ 大规模序列测试 +- ✅ 星座图文件检查 + +**QPSK测试** (9个): +- ✅ 输入长度验证 +- ✅ 输出长度检查 +- ✅ 格雷码映射验证 +- ✅ 单位能量归一化 +- ✅ 4个星座点数量 +- ✅ 相位分布检查 +- ✅ 平均功率验证 +- ✅ 连续比特对映射 +- ✅ 星座图文件检查 + +**16-QAM测试** (9个): +- ✅ 输入长度验证 +- ✅ 输出长度检查 +- ✅ 16个星座点验证 +- ✅ I/Q分量取值检查 +- ✅ 功率归一化验证 +- ✅ 格雷码映射测试 +- ✅ 符号分布均匀性 +- ✅ 角点功率验证 +- ✅ 星座图文件检查 + +#### 评分规则 +``` +总分 = 环境(5) + BPSK(25) + QPSK(25) + 16-QAM(20) + 报告(15) + + 代码质量(-10~+5) + 解调(+10) + BER(+10) +``` + +#### GitHub Actions工作流 +- ✅ 自动触发(PR提交时) +- ✅ 环境配置(Python 3.11 + 依赖) +- ✅ 运行所有测试 +- ✅ 生成评分报告 +- ✅ PR评论反馈 +- ✅ 上传详细日志 + +--- + +### 3. 配套文档(5份理论+2份工具) + +#### 理论文档 +- ✅ **BPSK原理**:映射规则、星座图、优缺点、应用场景 +- ✅ **QPSK原理**:格雷码、I/Q调制、相位分布、性能分析 +- ✅ **QAM原理**:多级调制、功率归一化、自适应调制 + +#### 工具指南 +- ✅ **Copilot使用指南**: + - 基本用法和高级技巧 + - 实验中的具体应用 + - 11个常见问题解答 + - 提示词示例 + +- ✅ **Git快速入门**: + - 工作流程图解 + - 常用命令速查 + - 11个场景化教程 + - 图形工具推荐 + +--- + +### 4. 辅助工具 + +#### 工具函数库(utils.py) +```python +✅ setup_chinese_font() # 中文字体配置 +✅ plot_constellation() # 星座图绘制 +✅ add_awgn() # AWGN噪声生成 +✅ calculate_ber() # 误比特率计算 +✅ plot_ber_curve() # BER曲线绘制 +✅ generate_random_bits() # 随机比特生成 +``` + +#### 示例文件(4张高清图片) +- 🖼️ BPSK星座图(带标注) +- 🖼️ QPSK星座图(格雷码标注) +- 🖼️ 16-QAM星座图(16点方阵) +- 🖼️ BER性能曲线(理论值) + +--- + +## 📊 统计数据 + +### 代码量 +- **总文件数**: 32个文件 +- **代码总行数**: ~3200行 +- **Python代码**: ~2500行 +- **Markdown文档**: ~700行 +- **YAML配置**: 100行 + +### 文档量 +- **学生指南**: ~500行 +- **理论文档**: ~600行 +- **工具指南**: ~800行 +- **报告模板**: ~200行 + +### 测试覆盖 +- **单元测试**: 24个 +- **代码质量**: pylint检查 +- **环境测试**: 4个检查项 +- **报告检查**: 5个评分维度 + +--- + +## 🚀 下一步部署 + +### 教师操作清单 + +#### 1. 推送到GitHub(5分钟) +```bash +cd wireless-modulation-experiment +git init +git add . +git commit -m "初始化数字调制解调实验平台 v1.0" +git branch -M main +git remote add origin https://github.com/你的用户名/wireless-modulation-experiment.git +git push -u origin main +``` + +#### 2. 配置仓库设置(2分钟) +- [ ] 设置为模板仓库(Settings → Template repository) +- [ ] 配置Actions权限(Settings → Actions → General) + - ✅ Read and write permissions + - ✅ Allow GitHub Actions to create and approve pull requests + +#### 3. 测试评分系统(10分钟) +- [ ] 自己创建一个测试PR +- [ ] 验证GitHub Actions是否正常运行 +- [ ] 检查评分结果是否正确显示 + +#### 4. 发布实验通知(5分钟) +```markdown +📢 数字调制解调实验通知 + +实验时间:2026年4月24日 14:00-16:00 +实验地点:[实验室名称] + +模板仓库:https://github.com/你的用户名/wireless-modulation-experiment +提交截止:2026年5月1日 23:59 + +请提前准备: +1. Python 3.8+ 环境 +2. GitHub账号 +3. VS Code + GitHub Copilot(可选) +``` + +--- + +## 🎓 课堂准备 + +### 第一小时(讲解与演示) + +#### 1. 开场介绍(5分钟) +- 实验目标和评分标准 +- AI辅助编程的理念 + +#### 2. 环境配置演示(10分钟) +- 使用Copilot Agent自动配置 +- 运行test_environment.py验证 + +#### 3. 调制原理讲解(20分钟) +- BPSK: 最简单的调制 +- QPSK: 频谱效率翻倍 +- 16-QAM: 更高的数据速率 + +#### 4. BPSK实现演示(15分钟) +- 打开modulation.py +- 使用Copilot生成代码 +- 运行并查看星座图 + +#### 5. Git流程演示(10分钟) +- Fork → Clone → Commit → Push → PR + +### 第二小时(学生实验) + +#### 学生任务 +- 完成QPSK和16-QAM实现 +- 生成星座图 +- 提交PR + +#### 教师巡回 +- 解答技术问题 +- 引导使用Copilot +- 检查进度 + +--- + +## 📈 预期成果 + +### 学生学习成果 +- ✅ 理解数字调制原理 +- ✅ 掌握NumPy/Matplotlib +- ✅ 学会使用AI编程助手 +- ✅ 熟悉GitHub协作流程 + +### 教学效果提升 +- ⏱️ 减少环境配置时间(从30分钟→10分钟) +- 🤖 AI辅助提高编程效率(提升50%+) +- 📊 自动评分节省人力(每个学生节省15分钟) +- 🎯 即时反馈促进学习(3分钟内获得评分) + +--- + +## 🔧 维护与扩展 + +### 短期维护 +- [ ] 收集学生反馈 +- [ ] 优化提示词模板 +- [ ] 调整评分权重 + +### 中期扩展 +- [ ] 添加更多调制方式(64-QAM、OFDM) +- [ ] 集成软件无线电硬件(USRP) +- [ ] 开发Web可视化界面 + +### 长期规划 +- [ ] 构建完整的通信实验平台 +- [ ] 发布开源教学资源 +- [ ] 推广到其他高校 + +--- + +## 🎉 总结 + +### ✨ 创新点 + +1. **AI原生设计** + - 从零设计支持AI辅助编程 + - 提供详细的Copilot使用指南 + - 平衡AI辅助与独立思考 + +2. **全自动评分** + - GitHub Actions无缝集成 + - 3分钟内反馈结果 + - 可重复评分 + +3. **完整的教学闭环** + - 理论文档 + 代码模板 + 自动评分 + 即时反馈 + - 学生自主学习 + 教师精准指导 + +4. **开源可扩展** + - 模块化设计 + - 易于定制 + - 便于分享 + +### 🏆 价值 + +**对学生**: +- 提高学习效率 +- 掌握前沿工具 +- 培养工程能力 + +**对教师**: +- 减少重复劳动 +- 提升教学质量 +- 聚焦核心指导 + +**对课程**: +- 与时俱进 +- 吸引学生兴趣 +- 提升教学效果 + +--- + +## 📞 支持 + +如有问题,请联系: +- 📧 Email: [你的邮箱] +- 💬 GitHub Issues +- 📝 课程群 + +--- + +**实验平台已就绪,祝教学圆满成功!** 🚀🎓✨ diff --git a/PROJECT_README.md b/PROJECT_README.md new file mode 100644 index 0000000..ce06bf4 --- /dev/null +++ b/PROJECT_README.md @@ -0,0 +1,315 @@ +# 数字调制解调实验平台 - 项目说明 + +## 📋 项目概述 + +这是一个基于 GitHub + AI 辅助编程的数字调制解调实验平台,用于《通信原理》或《数字通信》课程的实验教学。 + +**实验时长**:2小时(120分钟) +**目标学生**:本科生/研究生 +**技术栈**:Python + NumPy + Matplotlib + GitHub Actions + GitHub Copilot + +--- + +## 🎯 教学目标 + +1. 理解 BPSK、QPSK、16-QAM 调制原理 +2. 掌握 Python 科学计算工具(NumPy、Matplotlib) +3. 学习使用 AI 编程助手(GitHub Copilot) +4. 熟悉 GitHub 协作流程和自动评分系统 + +--- + +## 📦 仓库结构 + +``` +wireless-modulation-experiment/ +├── README.md # 学生实验指南 +├── TEACHER_GUIDE.md # 教师使用说明 +├── REPORT_TEMPLATE.md # 实验报告模板 +├── requirements.txt # Python依赖 +├── .gitignore # Git忽略文件 +│ +├── .github/workflows/ +│ └── grading.yml # GitHub Actions自动评分 +│ +├── src/ # 学生代码区 +│ ├── modulation.py # 调制函数(待实现) +│ ├── demodulation.py # 解调函数(选做) +│ ├── performance_test.py # 性能测试(选做) +│ ├── utils.py # 工具函数(已实现) +│ └── test_environment.py # 环境测试脚本 +│ +├── grading/ # 自动评分脚本 +│ ├── test_bpsk.py # BPSK单元测试 +│ ├── test_qpsk.py # QPSK单元测试 +│ ├── test_qam16.py # 16-QAM单元测试 +│ ├── check_report.py # 报告检查 +│ └── calculate_grade.py # 总评分计算 +│ +├── docs/ # 实验文档 +│ ├── theory_bpsk.md # BPSK原理 +│ ├── theory_qpsk.md # QPSK原理 +│ ├── theory_qam.md # QAM原理 +│ ├── copilot_guide.md # Copilot使用指南 +│ └── git_quickstart.md # Git快速入门 +│ +├── examples/ # 示例输出 +│ ├── bpsk_constellation.png # BPSK示例图 +│ ├── qpsk_constellation.png # QPSK示例图 +│ ├── 16qam_constellation.png # 16-QAM示例图 +│ ├── ber_curve_example.png # BER曲线示例 +│ └── generate_examples.py # 生成示例脚本 +│ +└── results/ # 学生结果(自动创建) + └── .gitkeep +``` + +--- + +## ✅ 实验任务清单 + +### 必做任务(75分) + +- [ ] **任务0**: 环境配置(5分) + - 使用 Copilot Agent 或手动安装 Python 环境 + - 运行 `test_environment.py` 验证 + +- [ ] **任务1**: BPSK 调制(25分) + - 实现 `bpsk_modulate()` 函数 + - 生成星座图并保存 + +- [ ] **任务2**: QPSK 调制(25分) + - 实现 `qpsk_modulate()` 函数(格雷码) + - 生成星座图并保存 + +- [ ] **任务3**: 16-QAM 调制(20分) + - 实现 `qam16_modulate()` 函数 + - 生成星座图并保存 + +- [ ] **任务6**: 实验报告(15分) + - 填写 `REPORT.md` + - 包含原理、方法、结果、分析、心得 + +### 选做任务(20分加分) + +- [ ] **任务4**: 解调实现(10分) + - `bpsk_demodulate()` + - `qpsk_demodulate()` + - `qam16_demodulate()` + +- [ ] **任务5**: BER 性能分析(10分) + - 生成 BER vs SNR 曲线 + - 对比不同调制方式 + +### 代码质量(-10~+5分) + +- pylint 评分 ≥ 8.0: +5分 +- pylint 评分 5.0~8.0: 0分 +- pylint 评分 < 5.0: -10分 + +--- + +## 🚀 快速开始 + +### 教师部署 + +1. 克隆本仓库到你的 GitHub 账号 +2. 设置为模板仓库(Template repository) +3. 配置 GitHub Actions 权限 +4. 发布实验通知给学生 + +### 学生使用 + +1. Fork 模板仓库到个人账号 +2. Clone 到本地 +3. 安装依赖:`pip install -r requirements.txt` +4. 完成实验任务 +5. Commit & Push +6. 创建 Pull Request +7. 查看自动评分结果 + +--- + +## 📊 评分系统 + +### 自动评分流程 + +```mermaid +graph LR + A[学生提交PR] --> B[GitHub Actions触发] + B --> C[环境测试] + B --> D[BPSK测试] + B --> E[QPSK测试] + B --> F[16-QAM测试] + B --> G[报告检查] + B --> H[代码质量检查] + C --> I[计算总分] + D --> I + E --> I + F --> I + G --> I + H --> I + I --> J[PR评论显示结果] +``` + +### 评分标准 + +| 评分项 | 满分 | 评分细则 | +|--------|------|----------| +| 环境配置 | 5 | 成功运行环境测试脚本 | +| BPSK调制 | 25 | 映射正确(15) + 星座图(10) | +| QPSK调制 | 25 | 映射正确(15) + 星座图(10) | +| 16-QAM调制 | 20 | 映射正确(12) + 星座图(8) | +| 实验报告 | 15 | 完整性(8) + 分析深度(7) | +| 代码质量 | -10~+5 | 基于 pylint 评分 | +| 解调实现 | +10 | 选做加分 | +| BER性能 | +10 | 选做加分 | + +--- + +## 🛠️ 技术特性 + +### GitHub Actions 自动评分 + +- **自动触发**:学生提交 PR 后 3-5 分钟内完成评分 +- **详细反馈**:测试失败时显示具体错误信息 +- **可重复评分**:修改代码后重新提交即可重新评分 + +### AI 辅助编程 + +- **GitHub Copilot**:学生可使用 AI 助手编写代码 +- **教学引导**:提供详细的提示词示例和使用指南 +- **平衡学习**:强调理解原理,避免盲目依赖 AI + +### 自动化测试 + +- **单元测试**:pytest 覆盖所有核心功能 +- **代码质量**:pylint 检查代码规范 +- **报告检查**:自动验证实验报告完整性 + +--- + +## 📚 配套文档 + +### 理论文档 + +- [BPSK原理](docs/theory_bpsk.md):二进制相移键控 +- [QPSK原理](docs/theory_qpsk.md):正交相移键控 +- [QAM原理](docs/theory_qam.md):正交幅度调制 + +### 工具指南 + +- [Copilot使用指南](docs/copilot_guide.md):如何有效使用 AI 助手 +- [Git快速入门](docs/git_quickstart.md):Git 基本操作 + +### 模板文件 + +- [实验报告模板](REPORT_TEMPLATE.md):标准报告格式 + +--- + +## 🎓 教学建议 + +### 第一小时(课堂讲解) + +1. **介绍实验目标**(5分钟) +2. **演示环境配置**(10分钟) + - 使用 Copilot Agent 自动安装依赖 +3. **讲解调制原理**(20分钟) + - BPSK、QPSK、16-QAM 原理 + - 星座图概念 +4. **演示 BPSK 实现**(15分钟) + - 使用 Copilot 辅助编写代码 + - 生成星座图 +5. **讲解 Git 和 PR 流程**(10分钟) + +### 第二小时(学生实验) + +1. 学生独立完成 QPSK 和 16-QAM +2. 教师和助教巡回答疑 +3. 鼓励学生尝试选做任务 +4. 最后 10 分钟:提交代码并创建 PR + +--- + +## 🔧 维护与更新 + +### 更新实验内容 + +修改模板仓库的文件后: + +```bash +git add . +git commit -m "更新实验要求" +git push origin main +``` + +学生重新 Fork 或 Pull 即可获取最新版本。 + +### 调整评分权重 + +修改 `grading/calculate_grade.py`: + +```python +# 调整分值 +bpsk_score = 30 # 从25提高到30 +qpsk_score = 30 # 从25提高到30 +``` + +### 添加新任务 + +1. 在 `src/` 中添加新的代码模板 +2. 在 `grading/` 中添加对应的测试脚本 +3. 更新 `README.md` 和评分系统 + +--- + +## 📞 支持与反馈 + +### 问题报告 + +如果发现 Bug 或有改进建议: + +- 在 GitHub 上提 Issue +- 发送邮件至:[你的邮箱] + +### 贡献代码 + +欢迎提交 Pull Request 改进本平台! + +--- + +## 📄 许可证 + +本项目采用 MIT 许可证。 + +--- + +## 🙏 致谢 + +感谢以下开源项目: + +- [NumPy](https://numpy.org/) +- [Matplotlib](https://matplotlib.org/) +- [pytest](https://pytest.org/) +- [GitHub Actions](https://github.com/features/actions) +- [GitHub Copilot](https://github.com/features/copilot) + +--- + +**版本**:v1.0.0 +**最后更新**:2026年4月21日 +**维护者**:[你的名字] + +--- + +## 📈 统计信息 + +- **总代码行数**:~3000行 +- **文档页数**:~50页 +- **测试用例数**:30+个 +- **平均完成时间**:90-120分钟 + +--- + +**祝实验教学圆满成功!** 🎉 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b6b0e9 --- /dev/null +++ b/README.md @@ -0,0 +1,285 @@ +# 数字调制解调实验 + +## 📚 实验概述 + +本实验要求学生实现常见的数字调制方式(BPSK、QPSK、16-QAM),生成星座图,并选做性能分析。实验使用 Python + NumPy + Matplotlib,鼓励使用 AI 编程助手(GitHub Copilot / Claude Code)。 + +**实验时长**:2小时(120分钟) +- 第一小时:教师讲解和演示 +- 第二小时:学生动手实验 + +**提交截止**:实验课后7天(下周四 23:59) + +--- + +## 🎯 实验目标 + +1. 理解数字调制的基本原理(BPSK、QPSK、16-QAM) +2. 实现调制算法并可视化星座图 +3. 学习使用 AI 编程助手辅助开发 +4. 熟悉 GitHub 协作流程和自动评分系统 + +--- + +## 📝 实验任务 + +### 任务0:环境准备(5分) +- 使用 Copilot Agent 配置 Python 环境 +- Fork 本仓库到个人账号 +- Clone 到本地并安装依赖 + +### 任务1:BPSK 调制(25分)✅ 必做 +在 `src/modulation.py` 中实现 `bpsk_modulate()` 函数: +- 输入:比特序列 `[0, 1, 0, 1, ...]` +- 输出:符号序列 `[+1, -1, +1, -1, ...]` +- 映射关系:`0 → +1`, `1 → -1` +- 生成星座图并保存到 `results/bpsk_constellation.png` + +### 任务2:QPSK 调制(25分)✅ 必做 +在 `src/modulation.py` 中实现 `qpsk_modulate()` 函数: +- 每2比特一组 +- 格雷码映射: + - `00 → (1+1j)/√2` (45°) + - `01 → (-1+1j)/√2` (135°) + - `11 → (-1-1j)/√2` (225°) + - `10 → (1-1j)/√2` (315°) +- 生成星座图并保存到 `results/qpsk_constellation.png` + +### 任务3:16-QAM 调制(20分)✅ 必做 +在 `src/modulation.py` 中实现 `qam16_modulate()` 函数: +- 每4比特一组 +- I/Q 分量取值:`-3, -1, +1, +3` +- 生成16个符号的星座图 +- 保存到 `results/16qam_constellation.png` + +### 任务4:解调实现(10分)⭐ 选做 +在 `src/demodulation.py` 中实现解调函数: +- `bpsk_demodulate()`:判决准则 +- `qpsk_demodulate()`:最小欧氏距离判决 +- `qam16_demodulate()`:最小欧氏距离判决 + +### 任务5:BER 性能分析(10分)⭐ 选做 +在 `src/performance_test.py` 中完成性能测试: +- 生成随机比特序列 +- 调制 → 添加 AWGN 噪声 → 解调 +- 扫描不同 SNR(0~15 dB) +- 绘制 BER vs SNR 曲线(对数坐标) + +### 任务6:实验报告(15分)📄 可课后完成 +在根目录创建 `REPORT.md`,包含: +1. 实验目的 +2. 实验原理(简述BPSK/QPSK/QAM) +3. 实验方法与步骤 +4. 实验结果(插入星座图) +5. 结果分析与讨论 +6. 实验心得与 Copilot 使用体会 +7. 参考文献 + +⚠️ **重要**:实验结果(代码+星座图)必须提交,实验报告可以后续完善。 + +--- + +## 🚀 快速开始 + +### 1. 环境配置(第一小时) + +**使用 Copilot Agent 自动配置**(推荐): +``` +打开 VSCode Copilot Chat,输入: +"请帮我配置 Python 开发环境,需要安装 numpy、scipy、matplotlib" +``` + +**手动安装**(备选): +```bash +# 1. 安装 Python 3.8+ +# 2. 安装依赖 +pip install -r requirements.txt +``` + +### 2. 获取实验代码 + +```bash +# Fork 本仓库到你的 GitHub 账号 +# 然后 Clone 到本地 +git clone https://github.com/你的用户名/wireless-modulation-experiment.git +cd wireless-modulation-experiment + +# 测试环境 +python src/test_environment.py +``` + +### 3. 完成实验 + +打开 `src/modulation.py`,使用 GitHub Copilot 辅助完成代码: + +**提示词示例**: +``` +"请实现 BPSK 调制函数,输入是比特序列,输出是符号序列, +其中比特0映射到+1,比特1映射到-1" +``` + +运行测试: +```bash +python src/modulation.py +``` + +检查结果: +- 查看 `results/` 目录是否生成了星座图 +- 确认图片清晰、坐标轴标注正确 + +### 4. 提交到 GitHub + +```bash +git add . +git commit -m "完成 BPSK 和 QPSK 调制" +git push origin main +``` + +在 GitHub 网页上创建 Pull Request: +1. 访问你的仓库 +2. 点击 "Pull requests" → "New pull request" +3. 填写 PR 描述(说明完成了哪些任务) +4. 点击 "Create pull request" + +### 5. 查看自动评分 + +等待 3-5 分钟,GitHub Actions 会自动运行评分脚本。 +评分结果会以 Comment 形式显示在你的 PR 下方。 + +--- + +## 💡 使用 AI 助手的提示 + +### 推荐的提问模板 + +**调制实现**: +``` +"请用 Python 实现 BPSK 调制,输入二进制序列, +输出复数符号序列,0映射到+1,1映射到-1" +``` + +**星座图绘制**: +``` +"请用 matplotlib 画出 QPSK 的星座图, +四个点在单位圆上均匀分布" +``` + +**代码调试**: +``` +"我的代码报错了:[粘贴错误信息],请帮我找出问题" +``` + +**代码解释**: +``` +"请解释这段代码的含义:[粘贴代码]" +``` + +### AI 助手使用建议 + +✅ 先理解原理,再使用 AI 生成代码 +✅ 生成的代码要仔细阅读理解 +✅ 可以让 AI 解释代码的实现逻辑 +✅ 遇到错误时,将完整错误信息粘贴给 AI +✅ 尝试修改参数观察结果变化 + +❌ 不要完全依赖 AI,要培养独立思考能力 +❌ 不要直接提交 AI 生成的代码而不理解其含义 + +--- + +## 📊 评分标准 + +| 评分项 | 分值 | 评分细则 | +|--------|------|----------| +| 环境配置 | 5分 | 成功运行环境测试脚本 | +| BPSK调制 | 25分 | 映射正确(15分) + 星座图(10分) | +| QPSK调制 | 25分 | 映射正确(15分) + 星座图(10分) | +| 16-QAM调制 | 20分 | 映射正确(12分) + 星座图(8分) | +| 解调实现 | 10分 | 正确实现解调算法(选做加分) | +| BER性能 | 10分 | 生成BER曲线并分析(选做加分) | +| 实验报告 | 15分 | 完整性(8分) + 分析深度(7分) | +| 代码质量 | -10~+5分 | pylint评分 > 8.0加分,< 5.0扣分 | + +**总分**:基础任务(0-3+报告)满分75分,选做任务可额外加分20分。 + +--- + +## 📁 仓库结构 + +``` +wireless-modulation-experiment/ +├── README.md # 本文件 +├── REQUIREMENTS.md # 详细任务要求 +├── .github/workflows/ +│ └── grading.yml # 自动评分工作流 +├── docs/ +│ ├── theory_bpsk.md # BPSK原理 +│ ├── theory_qpsk.md # QPSK原理 +│ ├── theory_qam.md # QAM原理 +│ └── copilot_guide.md # Copilot使用指南 +├── src/ +│ ├── modulation.py # 调制函数(学生填充) +│ ├── demodulation.py # 解调函数(学生填充) +│ ├── performance_test.py # 性能测试(学生填充) +│ ├── utils.py # 工具函数(已实现) +│ └── test_environment.py # 环境测试脚本 +├── grading/ # 评分脚本(学生不可见) +│ ├── test_bpsk.py +│ ├── test_qpsk.py +│ ├── test_qam16.py +│ ├── check_report.py +│ └── calculate_grade.py +├── examples/ # 示例输出 +│ ├── bpsk_constellation.png +│ ├── qpsk_constellation.png +│ └── ber_curve_example.png +├── results/ # 学生结果(自动创建) +├── requirements.txt # Python依赖 +├── .gitignore +└── REPORT_TEMPLATE.md # 报告模板 +``` + +--- + +## ❓ 常见问题 + +**Q1: 我不会用 Git,怎么办?** +A: 课上会有 10 分钟的 Git 快速演示,也可以查看 [Git 快速入门](docs/git_quickstart.md)。 + +**Q2: GitHub Copilot 需要付费吗?** +A: 学生可以申请免费使用,访问 https://education.github.com/ 申请。 + +**Q3: 我的代码运行报错,怎么办?** +A: 将完整错误信息粘贴给 Copilot 求助,或向教师/助教求助。 + +**Q4: 星座图应该是什么样的?** +A: 查看 `examples/` 目录中的示例图片。 + +**Q5: 我可以多次提交吗?** +A: 可以!在截止时间前可以随意修改和提交,系统会取最后一次评分。 + +**Q6: 选做任务必须做吗?** +A: 不是必须的。基础任务满分75分即可及格,选做任务是加分项。 + +--- + +## 📖 参考资料 + +- [BPSK原理详解](docs/theory_bpsk.md) +- [QPSK原理详解](docs/theory_qpsk.md) +- [QAM原理详解](docs/theory_qam.md) +- [GitHub Copilot 使用指南](docs/copilot_guide.md) +- [Git 快速入门](docs/git_quickstart.md) + +--- + +## 📧 联系方式 + +如有问题,请通过以下方式联系: +- 课程群:[课程群号] +- 邮箱:[教师邮箱] +- Office Hours:[时间地点] + +--- + +**祝实验顺利!🎉** diff --git a/REPORT_TEMPLATE.md b/REPORT_TEMPLATE.md new file mode 100644 index 0000000..9cbc306 --- /dev/null +++ b/REPORT_TEMPLATE.md @@ -0,0 +1,177 @@ +# 实验报告模板 + +**实验名称**:数字调制解调实验 +**学生姓名**:[你的姓名] +**学号**:[你的学号] +**实验日期**:2026年4月24日 +**提交日期**:[提交日期] + +--- + +## 1. 实验目的 + +请在此描述实验的目的,例如: +- 理解数字调制的基本原理 +- 掌握BPSK、QPSK、16-QAM调制算法的实现 +- 学习使用Python和NumPy进行信号处理 +- 体验AI编程助手在开发中的应用 + +--- + +## 2. 实验原理 + +### 2.1 BPSK调制原理 + +请简述BPSK的调制原理,包括: +- 基本映射关系 +- 星座图特征 +- 优缺点 + +可以插入公式(使用LaTeX语法): + +比特 $b$ 映射到符号 $s$: +$$ +s = +\begin{cases} ++1, & \text{if } b = 0 \\ +-1, & \text{if } b = 1 +\end{cases} +$$ + +### 2.2 QPSK调制原理 + +请简述QPSK的调制原理... + +### 2.3 16-QAM调制原理 + +请简述16-QAM的调制原理... + +--- + +## 3. 实验方法与步骤 + +### 3.1 环境配置 + +描述你如何配置Python环境,是否使用了Copilot Agent辅助? + +### 3.2 BPSK实现 + +描述BPSK的实现步骤和关键代码: + +```python +def bpsk_modulate(bits): + # 你的代码实现 + pass +``` + +### 3.3 QPSK实现 + +描述QPSK的实现... + +### 3.4 16-QAM实现 + +描述16-QAM的实现... + +--- + +## 4. 实验结果 + +### 4.1 BPSK星座图 + +![BPSK星座图](results/bpsk_constellation.png) + +**分析**:从图中可以看出BPSK有两个星座点,分别位于... + +### 4.2 QPSK星座图 + +![QPSK星座图](results/qpsk_constellation.png) + +**分析**:... + +### 4.3 16-QAM星座图 + +![16-QAM星座图](results/16qam_constellation.png) + +**分析**:... + +### 4.4 性能测试结果(选做) + +如果完成了BER性能测试,请在此展示结果: + +![BER性能曲线](results/ber_comparison.png) + +**分析**:从曲线可以看出... + +--- + +## 5. 结果分析与讨论 + +### 5.1 星座图对比分析 + +对比三种调制方式的星座图,分析它们的特点... + +### 5.2 性能对比分析 + +对比三种调制方式的性能,包括: +- 频谱效率 +- 抗噪声性能 +- 实现复杂度 + +### 5.3 遇到的问题与解决方法 + +描述实验过程中遇到的问题及解决方法: + +1. **问题**:初始实现的BPSK星座图显示异常 + - **原因分析**:... + - **解决方法**:... + +2. **问题**:... + +--- + +## 6. 实验心得与Copilot使用体会 + +### 6.1 实验心得 + +描述通过本实验的收获,例如: +- 对数字调制的理解更加深入 +- 熟悉了Python科学计算工具的使用 +- 学会了如何调试信号处理代码 + +### 6.2 AI助手使用体会 + +描述使用GitHub Copilot或其他AI助手的体会: +- 哪些任务AI助手帮助很大? +- 哪些地方还需要人工思考? +- 对AI辅助编程的看法 + +### 6.3 改进建议 + +对本实验或实验平台的改进建议... + +--- + +## 7. 参考文献 + +1. John G. Proakis, Masoud Salehi. 《数字通信(第五版)》. 电子工业出版社, 2011. +2. [维基百科 - 相移键控](https://zh.wikipedia.org/wiki/%E7%9B%B8%E7%A7%BB%E9%94%AE%E6%8E%A7) +3. [NumPy官方文档](https://numpy.org/doc/) +4. 其他参考资料... + +--- + +## 附录:完整代码 + +如果需要,可以在此附上完整的代码实现。 + +```python +# modulation.py 完整代码 +... +``` + +--- + +**声明**:本实验报告内容真实,所有代码均为本人编写(或在AI助手辅助下完成),未抄袭他人成果。 + +**签名**:________ +**日期**:________ diff --git a/TEACHER_GUIDE.md b/TEACHER_GUIDE.md new file mode 100644 index 0000000..71736e0 --- /dev/null +++ b/TEACHER_GUIDE.md @@ -0,0 +1,322 @@ +# 教师使用说明 + +本文档面向教师,说明如何部署和管理数字调制解调实验平台。 + +--- + +## 1. 平台部署 + +### 1.1 创建GitHub模板仓库 + +1. 在你的 GitHub 组织或个人账号下创建新仓库: + - 仓库名:`wireless-modulation-experiment` + - 可见性:`Public`(便于学生访问) + - 勾选 `Template repository`(作为模板) + +2. 将本地文件推送到远程仓库: + +```bash +cd wireless-modulation-experiment +git init +git add . +git commit -m "初始化数字调制解调实验平台" +git branch -M main +git remote add origin https://github.com/jwentong/wireless-modulation-experiment.git +git push -u origin main +``` + +### 1.2 配置GitHub Actions权限 + +1. 进入仓库 `Settings` → `Actions` → `General` +2. **Workflow permissions** 设置为: + - ✅ `Read and write permissions` + - ✅ `Allow GitHub Actions to create and approve pull requests` + +### 1.3 保护评分脚本 + +将 `grading/` 目录设置为学生不可见(可选): + +**方法1**:使用私有子模块 +```bash +# 将grading移到私有仓库 +git submodule add https://github.com/jwentong/wireless-modulation-grading.git grading +``` + +**方法2**:在学生 fork 后删除 grading/ +- 学生 fork 后,GitHub Actions 会从教师仓库拉取评分脚本 +- 在 workflow 中添加: + +```yaml +- name: 拉取评分脚本 + run: | + git clone https://github.com/jwentong/wireless-modulation-grading.git grading +``` + +--- + +## 2. 学生使用流程 + +### 2.1 发布实验通知 + +在课程群或公告中发布: + +``` +📢 数字调制解调实验通知 + +实验时间:2026年4月24日(本周四)14:00-16:00 +实验地点:[地点] + +准备工作: +1. 确保已安装 Python 3.8+ 和 VS Code +2. 注册 GitHub 账号并申请 GitHub Student Pack(免费使用Copilot) +3. 预习实验指导:[链接] + +实验模板仓库: +https://github.com/jwentong/wireless-modulation-experiment + +提交截止时间:2026年5月1日 23:59 + +如有问题,请在群里提问或发邮件。 +``` + +### 2.2 指导学生Fork仓库 + +在实验课上演示: + +1. 访问模板仓库 +2. 点击 `Use this template` 或 `Fork` +3. Clone 到本地 +4. 安装依赖并测试环境 + +### 2.3 学生提交流程 + +学生完成实验后: + +1. Commit 并 Push 到自己的仓库 +2. 创建 Pull Request(Base: 教师仓库,Head: 学生仓库) +3. GitHub Actions 自动评分 +4. 学生查看评分结果,可选修改后重新提交 + +--- + +## 3. 评分管理 + +### 3.1 自动评分规则 + +评分系统自动执行以下检查: + +| 项目 | 分值 | 检查方式 | +|------|------|----------| +| 环境配置 | 5分 | 运行 `test_environment.py` | +| BPSK调制 | 25分 | pytest测试映射正确性、星座图 | +| QPSK调制 | 25分 | pytest测试格雷码、归一化 | +| 16-QAM调制 | 20分 | pytest测试I/Q映射、功率 | +| 实验报告 | 15分 | 检查章节完整性、字数、图片 | +| 代码质量 | -10~+5分 | pylint评分 | +| 选做加分 | +20分 | 解调函数(+10)、BER分析(+10) | + +总分:75分(基础)+ 20分(选做)+ 5分(代码质量)= 100分 + +### 3.2 查看所有学生评分 + +**方法1**:导出 Pull Requests + +使用 GitHub API 或第三方工具导出所有 PR 的评分结果。 + +**方法2**:使用脚本批量检查 + +创建脚本遍历所有学生的 fork: + +```python +import requests + +students = [ + "student1", + "student2", + # ... +] + +for student in students: + url = f"https://api.github.com/repos/{student}/wireless-modulation-experiment/actions/runs" + # 获取最新评分结果 + # ... +``` + +### 3.3 手动复核 + +自动评分后,教师应复核: + +1. **代码逻辑**:是否真正理解原理,还是盲目使用AI +2. **实验报告**:分析是否深入,心得是否真实 +3. **星座图质量**:是否清晰、标注是否规范 + +调整最终分数(±5分)。 + +--- + +## 4. 常见问题处理 + +### 问题1:学生 GitHub Actions 失败 + +**原因**: +- 依赖安装失败 +- 代码有语法错误 +- 测试超时 + +**解决**: +1. 查看 Actions 日志,找到具体错误 +2. 指导学生修改代码 +3. 重新 Push 触发评分 + +### 问题2:学生报告评分过低 + +**原因**: +- 缺少必要章节 +- 字数不足 +- 没有插入图片 + +**解决**: +- 提供 `REPORT_TEMPLATE.md` 模板 +- 说明评分标准 +- 允许修改后重新提交 + +### 问题3:学生代码雷同 + +**检测方法**: +1. 使用代码相似度检测工具(如 MOSS) +2. 检查 Git 提交历史(是否一次性提交大量代码) +3. 现场抽查,要求学生解释代码 + +**处理**: +- 警告并要求重新完成 +- 严重者按学术不诚信处理 + +### 问题4:AI生成代码质量差 + +**指导**: +- 强调理解原理比完成代码更重要 +- 要求学生在报告中说明使用AI的方式 +- 课堂上演示正确的 Copilot 使用方法 + +--- + +## 5. 评分调整 + +在 `grading/calculate_grade.py` 中可以调整各项分值: + +```python +# 修改分值权重 +env_score = 5 # 环境测试 +bpsk_score = 25 # BPSK +qpsk_score = 25 # QPSK +qam_score = 20 # 16-QAM +report_score = 15 # 报告 +``` + +也可以添加新的评分项: + +```python +# 添加代码注释检查 +comment_score = 5 +if check_comments(code): + comment_score = 5 +else: + comment_score = 0 +``` + +--- + +## 6. 实验改进建议 + +### 6.1 难度调整 + +**降低难度**: +- 提供更多代码框架 +- 减少必做任务数量 +- 延长实验时间 + +**提高难度**: +- 要求实现更高阶调制(64-QAM、256-QAM) +- 要求实现信道编码(卷积码、Turbo码) +- 要求实现实时信号处理 + +### 6.2 扩展实验 + +可以基于此平台扩展更多实验: + +- **实验2**:信道编码与译码 +- **实验3**:OFDM调制 +- **实验4**:MIMO系统仿真 +- **实验5**:软件无线电实现(使用 USRP) + +### 6.3 加入实时竞赛 + +设置排行榜,比较学生的 BER 性能或代码效率。 + +```python +# 在 GitHub Actions 中记录性能指标 +with open('leaderboard.json', 'r+') as f: + data = json.load(f) + data[student_name] = { + 'ber': ber_value, + 'runtime': runtime_ms, + 'score': total_score + } + # 排序并更新 +``` + +--- + +## 7. 技术支持 + +### 7.1 常用命令 + +```bash +# 更新模板仓库 +git add . +git commit -m "更新实验要求" +git push origin main + +# 查看所有fork +gh api repos/你的用户名/wireless-modulation-experiment/forks + +# 批量运行测试 +for repo in student_repos: + git clone $repo + cd repo + pytest grading/ + cd .. +``` + +### 7.2 监控GitHub Actions配额 + +免费账号有限制: +- Public 仓库:无限 +- Private 仓库:每月 2000 分钟 + +查看用量:`Settings` → `Billing` → `Actions` + +--- + +## 8. 联系与反馈 + +如果在使用过程中遇到问题,或有改进建议,请: + +- 📧 发送邮件至:[你的邮箱] +- 💬 在仓库中提 Issue +- 📝 提交 Pull Request 改进文档 + +--- + +## 9. 许可与致谢 + +本实验平台基于以下技术: +- Python + NumPy + Matplotlib +- GitHub + GitHub Actions +- pytest + pylint + +感谢开源社区的贡献! + +--- + +**祝教学顺利!** 🎓 diff --git a/docs/copilot_guide.md b/docs/copilot_guide.md new file mode 100644 index 0000000..847cebd --- /dev/null +++ b/docs/copilot_guide.md @@ -0,0 +1,379 @@ +# GitHub Copilot 使用指南 + +本指南帮助你在数字调制解调实验中有效使用 GitHub Copilot 和其他 AI 编程助手。 + +--- + +## 1. 什么是 GitHub Copilot? + +GitHub Copilot 是一个 AI 编程助手,可以: +- 根据注释自动生成代码 +- 补全函数和代码块 +- 提供多个实现方案供选择 +- 解释现有代码 +- 帮助调试错误 + +--- + +## 2. 如何获取 GitHub Copilot? + +### 学生免费使用 + +1. 访问 [GitHub Education](https://education.github.com/) +2. 使用学校邮箱申请 GitHub Student Developer Pack +3. 审核通过后,Copilot 将自动激活 + +### 安装 VS Code 扩展 + +1. 打开 VS Code +2. 在扩展市场搜索 "GitHub Copilot" +3. 安装并登录你的 GitHub 账号 + +--- + +## 3. 基本使用技巧 + +### 3.1 通过注释引导生成代码 + +**✅ 好的提示(详细且明确)**: + +```python +def bpsk_modulate(bits): + """ + BPSK调制函数 + 将二进制比特序列映射到符号序列 + 0 -> +1, 1 -> -1 + 返回复数数组 + """ + # [Copilot会在这里生成代码] +``` + +**❌ 不好的提示(过于简略)**: + +```python +def bpsk_modulate(bits): + # 实现BPSK +``` + +### 3.2 分步引导 + +将复杂任务分解成小步骤: + +```python +def qpsk_modulate(bits): + # 步骤1: 检查输入长度是否为偶数 + + # 步骤2: 将比特序列reshape成(N/2, 2) + + # 步骤3: 对每对比特应用格雷码映射 + + # 步骤4: 归一化到单位功率 + + return symbols +``` + +Copilot 会逐步生成每个部分的代码。 + +### 3.3 使用示例数据 + +提供示例输入输出: + +```python +def qpsk_modulate(bits): + """ + 示例: + 输入: [0, 0, 0, 1, 1, 1, 1, 0] + 输出: [(1+1j)/√2, (-1+1j)/√2, (-1-1j)/√2, (1-1j)/√2] + """ + # Copilot会根据示例生成代码 +``` + +--- + +## 4. 实验中的具体应用 + +### 任务1: 实现BPSK调制 + +**推荐提示词**: + +```python +def bpsk_modulate(bits): + """ + 实现BPSK调制 + + 参数: + bits: numpy数组,元素为0或1 + + 返回: + symbols: numpy复数数组 + - 比特0映射到+1 + - 比特1映射到-1 + + 实现提示: + 使用numpy的向量化操作,避免循环 + """ +``` + +### 任务2: 绘制星座图 + +**推荐提示词**: + +```python +def plot_constellation(symbols, title): + """ + 绘制星座图 + + 参数: + symbols: 复数符号数组 + title: 图表标题 + + 要求: + - 使用matplotlib绘制散点图 + - 显示实部和虚部坐标轴 + - 添加网格 + - 保存到results/目录 + """ +``` + +### 任务3: 添加AWGN噪声 + +**推荐提示词**: + +```python +def add_awgn(signal, snr_db): + """ + 向信号添加加性高斯白噪声 + + 参数: + signal: 复数信号数组 + snr_db: 信噪比(分贝) + + 返回: + noisy_signal: 加噪后的信号 + + 实现步骤: + 1. 计算信号功率 + 2. 根据SNR计算噪声功率 + 3. 生成复高斯噪声(实部和虚部独立) + 4. 将噪声加到信号上 + """ +``` + +--- + +## 5. 调试技巧 + +### 5.1 让 Copilot 解释错误 + +当代码报错时,选中错误信息和相关代码,然后: + +1. 打开 Copilot Chat (Ctrl+I 或 Cmd+I) +2. 输入: + +``` +我的代码报错了: +[粘贴错误信息] + +请帮我找出问题并建议修改方案。 +``` + +### 5.2 验证生成的代码 + +Copilot 生成代码后,务必: + +1. **阅读并理解**:确保代码逻辑正确 +2. **运行测试**:验证输出是否符合预期 +3. **检查边界条件**:输入异常值测试 + +### 5.3 对比多个方案 + +在函数内按 `Alt+]` (或 `Cmd+]`) 查看 Copilot 提供的其他实现方案,选择最优的。 + +--- + +## 6. 高级技巧 + +### 6.1 使用 Copilot Chat + +打开 Chat 面板进行对话式编程: + +``` +提示: 请帮我实现QPSK调制,要求使用格雷码映射, +并将符号归一化到单位能量。 + +Copilot: [生成代码并解释] + +你: 能否添加输入验证,检查比特数组长度是否为偶数? + +Copilot: [更新代码] +``` + +### 6.2 代码重构 + +选中一段代码,在 Chat 中输入: + +``` +请优化这段代码,提高可读性和性能。 +``` + +### 6.3 生成测试用例 + +```python +# 输入:请为bpsk_modulate函数生成完整的pytest测试用例 + +# Copilot会生成: +def test_bpsk_modulate(): + # 测试基本映射 + bits = np.array([0, 1, 0, 1]) + symbols = bpsk_modulate(bits) + expected = np.array([1, -1, 1, -1]) + assert np.allclose(symbols, expected) + + # 测试全0 + ... +``` + +--- + +## 7. 注意事项 + +### ⚠️ 不要完全依赖 AI + +- **理解原理**:先学习调制原理,再使用 Copilot +- **独立思考**:AI 生成的代码可能有错,需要人工审查 +- **学习为主**:Copilot 是辅助工具,不是替代思考 + +### ⚠️ 避免学术不诚信 + +- 使用 Copilot 辅助编程是**允许的** +- 但必须**理解并能解释**所有提交的代码 +- 在报告中**注明**使用了 AI 助手 + +### ⚠️ 代码质量检查 + +Copilot 生成的代码可能: +- 缺少错误处理 +- 效率不是最优 +- 不符合代码规范 + +提交前务必: +- 运行 `pylint` 检查代码质量 +- 运行所有测试用例 +- 手动 review 关键逻辑 + +--- + +## 8. 常见问题 + +### Q1: Copilot 生成的代码不正确怎么办? + +A: +1. 检查提示词是否足够详细 +2. 尝试分步引导 +3. 查看其他建议(Alt+]) +4. 手动修改并学习正确实现 + +### Q2: Copilot 没有反应? + +A: +1. 确认已登录 GitHub 账号 +2. 检查网络连接 +3. 重启 VS Code +4. 查看扩展是否已启用 + +### Q3: 如何提高 Copilot 的准确率? + +A: +- 编写详细的函数文档 +- 提供示例输入输出 +- 使用清晰的变量命名 +- 保持代码上下文简洁 + +--- + +## 9. 替代方案 + +如果无法使用 GitHub Copilot,可以尝试: + +### 9.1 Claude / ChatGPT + +在网页中与 AI 对话: + +``` +你: 请帮我用Python实现BPSK调制函数, +输入是numpy数组[0,1,0,1],输出应该是[1,-1,1,-1] + +AI: [生成代码] + +你: 如何绘制星座图? + +AI: [提供matplotlib代码] +``` + +### 9.2 本地 Copilot Agent + +在 VS Code 中使用 Copilot Agent(需要付费): + +``` +@workspace 帮我配置Python环境并安装numpy、matplotlib +``` + +### 9.3 其他 AI 编码助手 + +- **Cursor**:AI-first 编辑器 +- **Tabnine**:代码补全工具 +- **Amazon CodeWhisperer**:AWS 的 AI 编程助手 + +--- + +## 10. 实验报告中如何写使用体会? + +在报告的"实验心得"部分,可以这样写: + +**示例1**(简洁版): + +> 在本实验中,我使用了 GitHub Copilot 辅助编程。Copilot 在实现基础映射逻辑时提供了很大帮助,但在归一化和格雷码映射部分,我需要根据理论知识手动调整。总体而言,AI 助手可以提高编程效率,但理解原理仍然是最重要的。 + +**示例2**(详细版): + +> **AI 助手使用情况** +> +> 本实验中使用了 GitHub Copilot 进行辅助编程,主要体会如下: +> +> 1. **有帮助的场景**: +> - 生成NumPy数组操作的样板代码 +> - 快速实现matplotlib绘图框架 +> - 自动补全函数参数和类型提示 +> +> 2. **需要人工修正的场景**: +> - QPSK格雷码映射的具体实现 +> - 16-QAM归一化因子的计算 +> - 边界条件的错误处理 +> +> 3. **学习收获**: +> - 学会了如何有效地与AI协作 +> - 理解了提示工程(Prompt Engineering)的重要性 +> - 体会到AI是工具而非替代,扎实的理论基础仍然必不可少 + +--- + +## 11. 总结 + +GitHub Copilot 是强大的辅助工具,但: + +✅ **应该**: +- 用于加速编程流程 +- 生成样板代码 +- 探索不同实现方案 +- 学习新的API用法 + +❌ **不应该**: +- 完全依赖而不理解原理 +- 不加审查地提交生成的代码 +- 把AI当作"作业代写工具" + +记住:**AI是助手,你才是工程师!** 🚀 + +--- + +**祝实验顺利!如有问题,请随时向教师或助教求助。** diff --git a/docs/git_quickstart.md b/docs/git_quickstart.md new file mode 100644 index 0000000..0a60c29 --- /dev/null +++ b/docs/git_quickstart.md @@ -0,0 +1,372 @@ +# Git 快速入门 + +本指南帮助你快速掌握实验所需的 Git 基本操作。 + +--- + +## 什么是 Git? + +Git 是一个版本控制系统,用于: +- 跟踪代码的修改历史 +- 多人协作开发 +- 备份和恢复代码 + +GitHub 是基于 Git 的代码托管平台。 + +--- + +## 实验工作流程 + +``` +1. Fork模板仓库到你的GitHub账号 + ↓ +2. Clone到本地 + ↓ +3. 编写代码 + ↓ +4. Commit提交修改 + ↓ +5. Push到GitHub + ↓ +6. 创建Pull Request + ↓ +7. 自动评分 +``` + +--- + +## 1. Fork 仓库 + +在 GitHub 网页上: + +1. 访问教师提供的模板仓库 +2. 点击右上角的 **Fork** 按钮 +3. 仓库会复制到你的账号下 + +--- + +## 2. Clone 到本地 + +打开终端(或 Git Bash),运行: + +```bash +git clone https://github.com/你的用户名/wireless-modulation-experiment.git +cd wireless-modulation-experiment +``` + +--- + +## 3. 基本 Git 命令 + +### 查看状态 + +```bash +git status +``` + +显示哪些文件被修改、新增或删除。 + +### 添加文件到暂存区 + +```bash +# 添加单个文件 +git add src/modulation.py + +# 添加所有修改 +git add . + +# 添加results目录下的所有文件 +git add results/ +``` + +### 提交修改 + +```bash +git commit -m "完成BPSK调制实现" +``` + +**提交信息规范**: +- 简洁明了,说明做了什么 +- 使用中文或英文均可 +- 示例: + - ✅ "实现BPSK和QPSK调制" + - ✅ "修复星座图显示问题" + - ✅ "添加实验报告" + - ❌ "update" + - ❌ "修改" + +### 推送到 GitHub + +```bash +git push origin main +``` + +如果是第一次推送,可能需要: + +```bash +git push -u origin main +``` + +--- + +## 4. 常用场景 + +### 场景1: 第一次提交 + +```bash +# 1. 修改了文件后,查看状态 +git status + +# 2. 添加所有修改 +git add . + +# 3. 提交 +git commit -m "完成BPSK和QPSK实现" + +# 4. 推送 +git push origin main +``` + +### 场景2: 增量提交 + +```bash +# 修改代码... +git add src/modulation.py results/ +git commit -m "完成16-QAM调制" +git push origin main +``` + +### 场景3: 查看修改历史 + +```bash +# 查看提交日志 +git log + +# 简洁版 +git log --oneline + +# 查看最近3条 +git log -n 3 +``` + +### 场景4: 查看文件差异 + +```bash +# 查看未暂存的修改 +git diff + +# 查看已暂存的修改 +git diff --staged + +# 查看特定文件的修改 +git diff src/modulation.py +``` + +--- + +## 5. 创建 Pull Request + +### 步骤1: 确保代码已推送 + +```bash +git push origin main +``` + +### 步骤2: 在 GitHub 网页上创建 PR + +1. 访问你的仓库(`https://github.com/你的用户名/wireless-modulation-experiment`) +2. 点击 **Pull requests** 标签 +3. 点击 **New pull request** +4. 选择: + - Base repository: `教师的仓库` + - Base branch: `main` + - Head repository: `你的仓库` + - Compare branch: `main` +5. 填写 PR 标题和描述: + +**标题示例**: +``` +[学号] 姓名 - 数字调制解调实验提交 +``` + +**描述示例**: +``` +## 完成情况 + +- [x] 任务0: 环境配置 +- [x] 任务1: BPSK调制 +- [x] 任务2: QPSK调制 +- [x] 任务3: 16-QAM调制 +- [ ] 任务4: 解调实现(选做) +- [ ] 任务5: BER性能分析(选做) +- [x] 任务6: 实验报告 + +## 说明 + +所有必做任务已完成,星座图正确生成。 +使用GitHub Copilot辅助编程。 +``` + +6. 点击 **Create pull request** + +--- + +## 6. 自动评分流程 + +创建 PR 后: + +1. **GitHub Actions 自动运行**(3-5分钟) +2. **评分结果显示在 PR 评论中** +3. **查看详细日志**: + - 点击 PR 页面的 **Checks** 标签 + - 点击 **自动评分系统** + - 展开各个步骤查看详情 + +如果测试未通过: +- 根据错误信息修改代码 +- 重新 commit 并 push +- GitHub Actions 会自动重新评分 + +--- + +## 7. 常见问题 + +### Q1: 忘记添加文件就 commit 了? + +```bash +# 添加遗漏的文件 +git add 遗漏的文件 + +# 修正上一次提交(不产生新的commit) +git commit --amend --no-edit +``` + +### Q2: 提交信息写错了? + +```bash +# 修改最后一次提交的信息 +git commit --amend -m "正确的提交信息" +``` + +### Q3: 想撤销某个文件的修改? + +```bash +# 撤销对文件的修改(恢复到上次commit的状态) +git checkout -- src/modulation.py + +# 或者使用新语法 +git restore src/modulation.py +``` + +### Q4: 想撤销已经 add 的文件? + +```bash +# 从暂存区移除,但保留修改 +git reset HEAD src/modulation.py + +# 或者使用新语法 +git restore --staged src/modulation.py +``` + +### Q5: 推送失败,提示 "rejected"? + +可能是远程仓库有更新,需要先拉取: + +```bash +# 拉取远程更新 +git pull origin main + +# 如果有冲突,解决后再提交 +git add . +git commit -m "解决冲突" +git push origin main +``` + +### Q6: 如何查看远程仓库地址? + +```bash +git remote -v +``` + +--- + +## 8. .gitignore 文件 + +`.gitignore` 用于忽略不需要提交的文件,例如: + +``` +# Python +__pycache__/ +*.pyc + +# 结果文件(可选:如果想提交results,删除这一行) +results/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +``` + +本实验已配置好 `.gitignore`,无需修改。 + +--- + +## 9. Git 图形化工具 + +如果不喜欢命令行,可以使用图形化工具: + +### VS Code 内置 Git + +1. 点击左侧的 **Source Control** 图标 +2. 查看修改的文件 +3. 点击 `+` 添加到暂存区 +4. 输入提交信息并点击 ✓ 提交 +5. 点击 `...` → `Push` 推送 + +### GitHub Desktop + +下载:https://desktop.github.com/ + +提供友好的图形界面,适合初学者。 + +--- + +## 10. 参考资料 + +- [Git 官方文档](https://git-scm.com/doc) +- [GitHub 官方教程](https://docs.github.com/cn/get-started) +- [Git 速查表](https://education.github.com/git-cheat-sheet-education.pdf) + +--- + +## 11. 实验专用速查表 + +```bash +# 克隆仓库 +git clone https://github.com/你的用户名/wireless-modulation-experiment.git + +# 查看状态 +git status + +# 添加所有修改 +git add . + +# 提交 +git commit -m "完成BPSK和QPSK" + +# 推送 +git push origin main + +# 查看提交历史 +git log --oneline + +# 查看修改 +git diff +``` + +--- + +**如有疑问,请向教师或助教求助!** 🚀 diff --git a/docs/theory_bpsk.md b/docs/theory_bpsk.md new file mode 100644 index 0000000..6187dec --- /dev/null +++ b/docs/theory_bpsk.md @@ -0,0 +1,115 @@ +# BPSK调制原理 + +## 什么是BPSK? + +BPSK(Binary Phase Shift Keying,二进制相移键控)是最简单的数字调制方式之一。它通过改变载波的相位来传输数字信息。 + +## 调制原理 + +### 基本映射 + +BPSK使用两个相位状态来表示二进制数据: + +- **比特 0** → 相位 **0°** → 符号 **+1** +- **比特 1** → 相位 **180°** → 符号 **-1** + +数学表达式: + +$$ +s(t) = +\begin{cases} +A \cos(2\pi f_c t), & \text{比特为 0} \\ +-A \cos(2\pi f_c t), & \text{比特为 1} +\end{cases} +$$ + +其中: +- $A$ 是信号幅度 +- $f_c$ 是载波频率 +- $t$ 是时间 + +### 星座图 + +BPSK的星座图非常简单,只有两个点: + +``` + 虚部(Q) + | + | + -1 | +1 实部(I) +-------+------- + | + | +``` + +两个符号在实轴上,相位相差180°。 + +## 优点 + +1. **实现简单**:只需要判断相位是0°还是180° +2. **抗噪声性能好**:两个符号点距离最远,不容易混淆 +3. **解调容易**:只需要判断实部的正负 + +## 缺点 + +1. **频谱效率低**:每个符号只传输1比特信息 +2. **数据速率低**:在相同带宽下,传输速率是QPSK的一半 + +## 应用场景 + +BPSK常用于: +- 深空通信(信噪比极低的环境) +- RFID标签 +- 卫星通信 +- 低速数据传输 + +## Python实现要点 + +实现BPSK调制的关键步骤: + +1. **映射比特到符号** + ```python + symbols = np.where(bits == 0, 1, -1) + ``` + +2. **或者使用数学运算** + ```python + symbols = 1 - 2 * bits + ``` + +3. **转换为复数(保持接口一致性)** + ```python + symbols = symbols.astype(complex) + ``` + +## 解调原理 + +BPSK解调非常简单: + +$$ +\hat{b} = +\begin{cases} +0, & \text{if } \text{Re}(r) > 0 \\ +1, & \text{if } \text{Re}(r) \leq 0 +\end{cases} +$$ + +其中 $r$ 是接收到的符号(可能含噪声)。 + +## 理论性能 + +在AWGN(加性高斯白噪声)信道下,BPSK的误比特率(BER)为: + +$$ +P_e = Q\left(\sqrt{\frac{2E_b}{N_0}}\right) +$$ + +其中: +- $E_b$ 是每比特能量 +- $N_0$ 是噪声功率谱密度 +- $Q(x)$ 是Q函数 + +## 参考资料 + +1. Proakis, J. G., & Salehi, M. (2008). *Digital Communications* (5th ed.). McGraw-Hill. +2. [维基百科 - 相移键控](https://zh.wikipedia.org/wiki/%E7%9B%B8%E7%A7%BB%E9%94%AE%E6%8E%A7) diff --git a/docs/theory_qam.md b/docs/theory_qam.md new file mode 100644 index 0000000..5fafb50 --- /dev/null +++ b/docs/theory_qam.md @@ -0,0 +1,220 @@ +# 16-QAM调制原理 + +## 什么是QAM? + +QAM(Quadrature Amplitude Modulation,正交幅度调制)同时改变载波的**幅度**和**相位**来传输信息。16-QAM使用16个不同的符号,每个符号传输4比特。 + +## 调制原理 + +### 基本映射 + +16-QAM将**每4个比特**映射到**一个符号**,使用16个不同的幅度-相位组合: + +| I/Q分量 | 比特 | 符号值(归一化前) | +|---------|------|-------------------| +| I: 00 | 00xx | +3 | +| I: 01 | 01xx | +1 | +| I: 11 | 11xx | -1 | +| I: 10 | 10xx | -3 | + +同样的规则适用于Q分量(后2位)。 + +### 星座图 + +16-QAM的星座图是一个4×4的方阵: + +``` + 虚部(Q) + | + +3 • • • • + +1 • • • • 实部(I) + -1 • • • • + -3 • • • • + | +``` + +16个符号均匀分布在方格点上。 + +### 归一化 + +为了使平均功率为1,需要进行归一化: + +**未归一化的平均功率**: +$$ +P_{avg} = \frac{1}{16} \sum_{i,q} (I^2 + Q^2) = \frac{2 \times (3^2 + 1^2 + 1^2 + 3^2)}{16} = \frac{2 \times 20}{16} = 2.5 +$$ + +等等,让我重新计算... + +每个I/Q分量的平均功率: +$$ +P_I = \frac{3^2 + 1^2 + 1^2 + 3^2}{4} = \frac{20}{4} = 5 +$$ + +总平均功率:$P_{total} = 2 \times 5 = 10$ + +**归一化因子**:$\sqrt{10}$ + +**归一化后的符号**: +$$ +s = \frac{I + jQ}{\sqrt{10}} +$$ + +## 格雷码映射 + +16-QAM也使用格雷码映射,确保相邻符号只有1位差异: + +| 比特(I路) | I分量 | +|-------------|-------| +| 00 | +3 | +| 01 | +1 | +| 11 | -1 | +| 10 | -3 | + +同样的映射用于Q路(后2位)。 + +## 优点 + +1. **频谱效率高**:每个符号传输4比特,是QPSK的两倍 +2. **数据速率高**:在相同带宽下传输更多数据 +3. **广泛应用**:现代通信系统的标配 + +## 缺点 + +1. **抗噪声性能差**:符号点之间距离较近 +2. **需要更高的SNR**:相比BPSK和QPSK +3. **实现复杂**:需要更精确的幅度控制 + +## Python实现要点 + +### 方法1:分离I/Q路处理 + +```python +# 将比特序列reshape成(N/4, 4) +bit_groups = bits.reshape(-1, 4) + +# 前2位映射到I分量 +i_bits = bit_groups[:, :2] +# 后2位映射到Q分量 +q_bits = bit_groups[:, 2:] + +# 格雷码映射函数 +def gray_to_level(b0, b1): + """格雷码映射:00→+3, 01→+1, 11→-1, 10→-3""" + if b0 == 0 and b1 == 0: + return 3 + elif b0 == 0 and b1 == 1: + return 1 + elif b0 == 1 and b1 == 1: + return -1 + else: # b0 == 1 and b1 == 0 + return -3 + +# 对每组比特应用映射 +I = np.array([gray_to_level(i[0], i[1]) for i in i_bits]) +Q = np.array([gray_to_level(q[0], q[1]) for q in q_bits]) + +# 组合并归一化 +symbols = (I + 1j * Q) / np.sqrt(10) +``` + +### 方法2:使用字典映射 + +```python +# 定义完整的格雷码映射 +gray_map = { + (0, 0): 3, + (0, 1): 1, + (1, 1): -1, + (1, 0): -3 +} + +# 映射I和Q分量 +bit_groups = bits.reshape(-1, 4) +I = np.array([gray_map[(b[0], b[1])] for b in bit_groups]) +Q = np.array([gray_map[(b[2], b[3])] for b in bit_groups]) + +symbols = (I + 1j * Q) / np.sqrt(10) +``` + +## 解调原理 + +16-QAM解调也使用最小欧氏距离判决,但可以简化: + +**方法1:穷举搜索** +- 计算到所有16个参考点的距离 +- 选择最小距离对应的符号 + +**方法2:分离判决**(更快) +- 分别对I路和Q路进行判决 +- I路:判断实部属于{-3, -1, +1, +3}中的哪一个 +- Q路:判断虚部属于{-3, -1, +1, +3}中的哪一个 + +```python +def demodulate_component(value): + """判决单个分量""" + value_scaled = value * np.sqrt(10) # 反归一化 + + if value_scaled > 2: + return (0, 0) # +3 → 00 + elif value_scaled > 0: + return (0, 1) # +1 → 01 + elif value_scaled > -2: + return (1, 1) # -1 → 11 + else: + return (1, 0) # -3 → 10 +``` + +## 性能特性 + +### 符号功率分布 + +16-QAM的符号功率不均匀: + +- **角点**(4个):功率最大 = $(3^2 + 3^2) / 10 = 1.8$ +- **边点**(8个):功率中等 = $(3^2 + 1^2) / 10 = 1.0$ +- **内点**(4个):功率最小 = $(1^2 + 1^2) / 10 = 0.2$ + +平均功率 = 1.0(已归一化) + +### 误比特率 + +在AWGN信道下,16-QAM的近似误比特率: + +$$ +P_b \approx \frac{3}{8}Q\left(\sqrt{\frac{4E_s}{10N_0}}\right) +$$ + +## 更高阶的QAM + +- **64-QAM**:8×8方阵,每符号6比特 +- **256-QAM**:16×16方阵,每符号8比特 +- **1024-QAM**:32×32方阵,每符号10比特 + +阶数越高,频谱效率越高,但对SNR要求也越高。 + +## 应用场景 + +16-QAM及更高阶QAM应用于: +- **WiFi**:802.11n/ac/ax +- **LTE/5G**:根据信道质量自适应选择调制方式 +- **数字电视**:DVB-C/DVB-T +- **有线调制解调器**:DOCSIS + +## 自适应调制 + +现代通信系统会根据信道质量动态调整调制方式: + +| SNR条件 | 调制方式 | 数据速率 | +|---------|----------|----------| +| 很差 | BPSK | 1x | +| 较差 | QPSK | 2x | +| 一般 | 16-QAM | 4x | +| 良好 | 64-QAM | 6x | +| 很好 | 256-QAM | 8x | + +## 参考资料 + +1. Proakis, J. G. (2008). *Digital Communications* (5th ed.). McGraw-Hill. +2. [Wikipedia - QAM](https://en.wikipedia.org/wiki/Quadrature_amplitude_modulation) +3. 3GPP TS 36.211 - Physical channels and modulation (LTE) diff --git a/docs/theory_qpsk.md b/docs/theory_qpsk.md new file mode 100644 index 0000000..29b8f3c --- /dev/null +++ b/docs/theory_qpsk.md @@ -0,0 +1,169 @@ +# QPSK调制原理 + +## 什么是QPSK? + +QPSK(Quadrature Phase Shift Keying,正交相移键控)是BPSK的扩展,使用四个相位状态来传输数字信息,频谱效率是BPSK的两倍。 + +## 调制原理 + +### 基本映射 + +QPSK将**每2个比特**映射到**一个符号**,使用四个相位状态: + +**格雷码映射**(推荐,相邻符号只有1位差异): + +| 比特对 | 相位 | 符号(归一化) | +|--------|------|----------------| +| 00 | 45° | $(1+j)/\sqrt{2}$ | +| 01 | 135° | $(-1+j)/\sqrt{2}$ | +| 11 | 225° | $(-1-j)/\sqrt{2}$ | +| 10 | 315° | $(1-j)/\sqrt{2}$ | + +数学表达式: + +$$ +s(t) = A \cos\left(2\pi f_c t + \phi_i\right) +$$ + +其中 $\phi_i \in \{45°, 135°, 225°, 315°\}$ + +### 星座图 + +QPSK的星座图有四个点,均匀分布在单位圆上: + +``` + 虚部(Q) + | + 01 | 00 + | +-------+------- 实部(I) + | + 11 | 10 + | +``` + +四个符号相位相差90°。 + +## 复数表示 + +使用复数表示QPSK符号更加简洁: + +$$ +s = I + jQ +$$ + +其中: +- $I$ 是同相分量(In-phase) +- $Q$ 是正交分量(Quadrature) + +归一化后,每个符号的能量为1:$|s|^2 = 1$ + +## 为什么使用格雷码? + +格雷码(Gray Code)的特点是**相邻符号只有1位不同**。 + +**好处**:当噪声导致符号判决错误时,通常是判决到相邻的符号点,使用格雷码可以确保只有1个比特错误,而不是2个。 + +**示例**: +- 自然码:00 → 01 → 10 → 11(相邻差2位) +- 格雷码:00 → 01 → 11 → 10(相邻差1位) + +## 优点 + +1. **频谱效率高**:每个符号传输2比特信息 +2. **性能较好**:在相同误符号率下,误比特率比自然码低 +3. **应用广泛**:WiFi、LTE等系统的基础调制方式 + +## 缺点 + +1. **实现稍复杂**:需要I/Q两路处理 +2. **抗噪声性能低于BPSK**:符号点距离较近 + +## Python实现要点 + +实现QPSK调制的关键步骤: + +### 方法1:查表法 + +```python +# 定义格雷码映射字典 +gray_map = { + (0, 0): (1 + 1j) / np.sqrt(2), + (0, 1): (-1 + 1j) / np.sqrt(2), + (1, 1): (-1 - 1j) / np.sqrt(2), + (1, 0): (1 - 1j) / np.sqrt(2) +} + +# 每2比特查表 +symbols = [] +for i in range(0, len(bits), 2): + bit_pair = (bits[i], bits[i+1]) + symbols.append(gray_map[bit_pair]) +``` + +### 方法2:数学计算 + +```python +# 将比特序列reshape成(N/2, 2) +bit_pairs = bits.reshape(-1, 2) + +# 分别计算I和Q分量 +I = np.where(bit_pairs[:, 0] == 0, 1, -1) +Q = np.where(bit_pairs[:, 1] == 0, 1, -1) + +# 格雷码映射(交换Q的符号) +Q = np.where(bit_pairs[:, 0] == 1, -Q, Q) + +# 组合并归一化 +symbols = (I + 1j * Q) / np.sqrt(2) +``` + +## 解调原理 + +QPSK解调使用**最小欧氏距离判决**: + +1. 计算接收符号到所有参考点的距离 +2. 选择距离最小的参考点 +3. 输出该参考点对应的比特对 + +$$ +\hat{s} = \arg\min_{s_i} |r - s_i|^2 +$$ + +## 理论性能 + +在AWGN信道下,QPSK的误符号率(SER)为: + +$$ +P_s \approx 2Q\left(\sqrt{\frac{E_s}{N_0}}\right) +$$ + +由于使用格雷码,误比特率约为: + +$$ +P_b \approx \frac{1}{2}P_s +$$ + +## I/Q调制解释 + +QPSK可以看作两路独立的BPSK: + +$$ +s(t) = I \cos(2\pi f_c t) - Q \sin(2\pi f_c t) +$$ + +- **I路**:同相载波,$\cos(2\pi f_c t)$ +- **Q路**:正交载波,$\sin(2\pi f_c t)$(相位差90°) + +## 应用场景 + +QPSK广泛应用于: +- 卫星通信 +- WiFi(IEEE 802.11) +- LTE/5G移动通信 +- 数字电视广播(DVB) + +## 参考资料 + +1. Proakis, J. G. (2008). *Digital Communications* (5th ed.). McGraw-Hill. +2. [Wikipedia - QPSK](https://en.wikipedia.org/wiki/Phase-shift_keying#Quadrature_phase-shift_keying_(QPSK)) diff --git a/examples/generate_examples.py b/examples/generate_examples.py new file mode 100644 index 0000000..c4f5a8e --- /dev/null +++ b/examples/generate_examples.py @@ -0,0 +1,142 @@ +""" +生成示例星座图供学生参考 +""" + +import numpy as np +import matplotlib.pyplot as plt +import os +import sys + +# 添加src目录到路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +# 设置中文字体 +plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial Unicode MS'] +plt.rcParams['axes.unicode_minus'] = False + +def create_example_constellations(): + """创建示例星座图""" + + # 创建examples目录 + os.makedirs('../examples', exist_ok=True) + + # BPSK示例 + fig, ax = plt.subplots(1, 1, figsize=(8, 8)) + bpsk_symbols = np.array([-1, 1]) + ax.scatter(np.real(bpsk_symbols), np.imag(bpsk_symbols), + s=200, c='blue', marker='o', alpha=0.7, edgecolors='black', linewidths=2) + ax.axhline(y=0, color='k', linestyle='-', linewidth=0.5) + ax.axvline(x=0, color='k', linestyle='-', linewidth=0.5) + ax.grid(True, alpha=0.3) + ax.set_xlim(-1.5, 1.5) + ax.set_ylim(-1.5, 1.5) + ax.set_xlabel('实部 (In-phase)', fontsize=12) + ax.set_ylabel('虚部 (Quadrature)', fontsize=12) + ax.set_title('BPSK星座图(示例)', fontsize=14, fontweight='bold') + ax.set_aspect('equal') + + # 标注符号 + ax.text(-1, -0.2, '比特1', ha='center', fontsize=10) + ax.text(1, -0.2, '比特0', ha='center', fontsize=10) + + plt.savefig('../examples/bpsk_constellation.png', dpi=300, bbox_inches='tight') + plt.close() + print("✅ BPSK示例已生成") + + # QPSK示例 + fig, ax = plt.subplots(1, 1, figsize=(8, 8)) + qpsk_symbols = np.array([ + (1 + 1j) / np.sqrt(2), + (-1 + 1j) / np.sqrt(2), + (-1 - 1j) / np.sqrt(2), + (1 - 1j) / np.sqrt(2) + ]) + ax.scatter(np.real(qpsk_symbols), np.imag(qpsk_symbols), + s=200, c='red', marker='s', alpha=0.7, edgecolors='black', linewidths=2) + ax.axhline(y=0, color='k', linestyle='-', linewidth=0.5) + ax.axvline(x=0, color='k', linestyle='-', linewidth=0.5) + ax.grid(True, alpha=0.3) + ax.set_xlim(-1.2, 1.2) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('实部 (In-phase)', fontsize=12) + ax.set_ylabel('虚部 (Quadrature)', fontsize=12) + ax.set_title('QPSK星座图(示例)', fontsize=14, fontweight='bold') + ax.set_aspect('equal') + + # 标注符号 + labels = ['00', '01', '11', '10'] + for sym, label in zip(qpsk_symbols, labels): + offset = 0.15 + ax.text(np.real(sym) + offset * np.sign(np.real(sym)), + np.imag(sym) + offset * np.sign(np.imag(sym)), + label, ha='center', fontsize=10, + bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5)) + + plt.savefig('../examples/qpsk_constellation.png', dpi=300, bbox_inches='tight') + plt.close() + print("✅ QPSK示例已生成") + + # 16-QAM示例 + fig, ax = plt.subplots(1, 1, figsize=(8, 8)) + + # 生成16-QAM符号 + levels = np.array([-3, -1, 1, 3]) + qam16_symbols = [] + for i in levels: + for q in levels: + qam16_symbols.append((i + 1j * q) / np.sqrt(10)) + qam16_symbols = np.array(qam16_symbols) + + ax.scatter(np.real(qam16_symbols), np.imag(qam16_symbols), + s=150, c='green', marker='o', alpha=0.7, edgecolors='black', linewidths=2) + ax.axhline(y=0, color='k', linestyle='-', linewidth=0.5) + ax.axvline(x=0, color='k', linestyle='-', linewidth=0.5) + ax.grid(True, alpha=0.3) + ax.set_xlim(-1.2, 1.2) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('实部 (In-phase)', fontsize=12) + ax.set_ylabel('虚部 (Quadrature)', fontsize=12) + ax.set_title('16-QAM星座图(示例)', fontsize=14, fontweight='bold') + ax.set_aspect('equal') + + plt.savefig('../examples/16qam_constellation.png', dpi=300, bbox_inches='tight') + plt.close() + print("✅ 16-QAM示例已生成") + + # BER性能曲线示例 + fig, ax = plt.subplots(1, 1, figsize=(10, 6)) + + snr_db = np.arange(0, 16, 2) + + # 理论BER曲线(近似) + from scipy.special import erfc + + # BPSK + ber_bpsk = 0.5 * erfc(np.sqrt(10**(snr_db / 10))) + + # QPSK (与BPSK相同) + ber_qpsk = 0.5 * erfc(np.sqrt(10**(snr_db / 10))) + + # 16-QAM (近似) + ber_qam16 = 0.75 * erfc(np.sqrt(0.4 * 10**(snr_db / 10))) + + ax.semilogy(snr_db, ber_bpsk, 'b-o', linewidth=2, markersize=8, label='BPSK') + ax.semilogy(snr_db, ber_qpsk, 'r-s', linewidth=2, markersize=8, label='QPSK') + ax.semilogy(snr_db, ber_qam16, 'g-^', linewidth=2, markersize=8, label='16-QAM') + + ax.set_xlabel('SNR (dB)', fontsize=12) + ax.set_ylabel('Bit Error Rate (BER)', fontsize=12) + ax.set_title('数字调制方式BER性能对比(理论曲线)', fontsize=14, fontweight='bold') + ax.legend(fontsize=11) + ax.grid(True, which='both', alpha=0.3) + ax.set_ylim(1e-6, 1) + + plt.savefig('../examples/ber_curve_example.png', dpi=300, bbox_inches='tight') + plt.close() + print("✅ BER曲线示例已生成") + + print("\n所有示例图片已保存到 examples/ 目录") + + +if __name__ == "__main__": + create_example_constellations() diff --git a/grading/calculate_grade.py b/grading/calculate_grade.py new file mode 100644 index 0000000..abc9792 --- /dev/null +++ b/grading/calculate_grade.py @@ -0,0 +1,252 @@ +""" +总评分计算脚本 +整合所有测试结果并生成最终评分 +""" + +import subprocess +import sys +import os +import json + + +def run_pytest(test_file, test_name): + """运行pytest测试并返回结果""" + try: + result = subprocess.run( + [sys.executable, '-m', 'pytest', test_file, '-v', '--tb=short', '--json-report', '--json-report-file=temp_report.json'], + capture_output=True, + text=True, + timeout=60 + ) + + # 尝试解析JSON报告 + if os.path.exists('temp_report.json'): + with open('temp_report.json', 'r') as f: + report = json.load(f) + os.remove('temp_report.json') + + total = report.get('summary', {}).get('total', 0) + passed = report.get('summary', {}).get('passed', 0) + + return passed, total, result.returncode == 0 + else: + # 回退方案:解析输出文本 + if 'passed' in result.stdout: + # 尝试从输出中提取通过的测试数 + import re + match = re.search(r'(\d+) passed', result.stdout) + if match: + passed = int(match.group(1)) + return passed, passed, True + + return 0, 1, False + + except subprocess.TimeoutExpired: + print(f" ⏱️ {test_name}超时") + return 0, 1, False + except Exception as e: + print(f" ❌ {test_name}运行失败: {e}") + return 0, 1, False + + +def calculate_grade(): + """计算总评分""" + print("=" * 60) + print("数字调制解调实验 - 自动评分系统") + print("=" * 60) + print() + + total_score = 0 + max_score = 100 + + # 环境测试 (5分) + print("1️⃣ 环境配置测试 (5分)") + try: + result = subprocess.run( + [sys.executable, 'src/test_environment.py'], + capture_output=True, + text=True, + timeout=30 + ) + if result.returncode == 0: + env_score = 5 + print(" ✅ 环境测试通过: +5分") + else: + env_score = 0 + print(" ❌ 环境测试失败: 0分") + except: + env_score = 0 + print(" ❌ 环境测试失败: 0分") + + total_score += env_score + print() + + # BPSK测试 (25分) + print("2️⃣ BPSK调制测试 (25分)") + passed, total, success = run_pytest('grading/test_bpsk.py', 'BPSK') + if total > 0: + bpsk_score = int(25 * passed / total) + print(f" 通过测试: {passed}/{total}") + print(f" 得分: {bpsk_score}/25") + else: + bpsk_score = 0 + print(" ❌ 测试未运行: 0分") + + total_score += bpsk_score + print() + + # QPSK测试 (25分) + print("3️⃣ QPSK调制测试 (25分)") + passed, total, success = run_pytest('grading/test_qpsk.py', 'QPSK') + if total > 0: + qpsk_score = int(25 * passed / total) + print(f" 通过测试: {passed}/{total}") + print(f" 得分: {qpsk_score}/25") + else: + qpsk_score = 0 + print(" ❌ 测试未运行: 0分") + + total_score += qpsk_score + print() + + # 16-QAM测试 (20分) + print("4️⃣ 16-QAM调制测试 (20分)") + passed, total, success = run_pytest('grading/test_qam16.py', '16-QAM') + if total > 0: + qam_score = int(20 * passed / total) + print(f" 通过测试: {passed}/{total}") + print(f" 得分: {qam_score}/20") + else: + qam_score = 0 + print(" ❌ 测试未运行: 0分") + + total_score += qam_score + print() + + # 实验报告 (15分) + print("5️⃣ 实验报告检查 (15分)") + try: + result = subprocess.run( + [sys.executable, 'grading/check_report.py'], + capture_output=True, + text=True, + timeout=10 + ) + # 从输出中提取分数 + import re + match = re.search(r'最终报告得分:\s*(\d+)', result.stdout) + if match: + report_score = int(match.group(1)) + else: + report_score = 0 + print(f" 报告得分: {report_score}/15") + except: + report_score = 0 + print(" ❌ 报告检查失败: 0分") + + total_score += report_score + print() + + # 代码质量检查 (pylint) (-10~+5分) + print("6️⃣ 代码质量检查 (pylint)") + try: + result = subprocess.run( + [sys.executable, '-m', 'pylint', 'src/modulation.py', '--score=y'], + capture_output=True, + text=True, + timeout=30 + ) + + # 提取pylint分数 + import re + match = re.search(r'Your code has been rated at ([\d.]+)/10', result.stdout) + if match: + pylint_score_raw = float(match.group(1)) + + if pylint_score_raw >= 8.0: + pylint_bonus = 5 + print(f" ✅ 代码质量优秀 ({pylint_score_raw}/10): +5分") + elif pylint_score_raw >= 5.0: + pylint_bonus = 0 + print(f" ⚠️ 代码质量一般 ({pylint_score_raw}/10): 0分") + else: + pylint_bonus = -10 + print(f" ❌ 代码质量较差 ({pylint_score_raw}/10): -10分") + else: + pylint_bonus = 0 + print(" ℹ️ 无法获取pylint分数: 0分") + except: + pylint_bonus = 0 + print(" ℹ️ pylint检查跳过: 0分") + + total_score += pylint_bonus + print() + + # 选做加分项 + print("7️⃣ 选做任务加分") + bonus_score = 0 + + # 检查解调函数 + if os.path.exists('src/demodulation.py'): + with open('src/demodulation.py', 'r', encoding='utf-8') as f: + content = f.read() + if 'raise NotImplementedError' not in content: + bonus_score += 10 + print(" ✅ 解调功能已实现: +10分") + + # 检查性能测试 + if os.path.exists('results/ber_comparison.png') or os.path.exists('results/ber_curve.png'): + bonus_score += 10 + print(" ✅ BER性能分析完成: +10分") + + if bonus_score == 0: + print(" ℹ️ 未完成选做任务: 0分") + + total_score += bonus_score + print() + + # 最终评分 + print("=" * 60) + print(f"总分: {total_score}/{max_score}") + + if total_score >= 90: + grade = "A (优秀)" + elif total_score >= 80: + grade = "B (良好)" + elif total_score >= 70: + grade = "C (中等)" + elif total_score >= 60: + grade = "D (及格)" + else: + grade = "F (不及格)" + + print(f"等级: {grade}") + print("=" * 60) + + # 生成详细报告 + report = { + 'total_score': total_score, + 'max_score': max_score, + 'grade': grade, + 'breakdown': { + 'environment': env_score, + 'bpsk': bpsk_score, + 'qpsk': qpsk_score, + 'qam16': qam_score, + 'report': report_score, + 'code_quality': pylint_bonus, + 'bonus': bonus_score + } + } + + # 保存到文件 + with open('grade_report.json', 'w', encoding='utf-8') as f: + json.dump(report, f, indent=2, ensure_ascii=False) + + print("\n详细评分报告已保存到: grade_report.json") + + return total_score + + +if __name__ == "__main__": + calculate_grade() diff --git a/grading/check_report.py b/grading/check_report.py new file mode 100644 index 0000000..800dfc3 --- /dev/null +++ b/grading/check_report.py @@ -0,0 +1,115 @@ +""" +实验报告检查脚本 +""" + +import os +import re + + +def check_report_exists(): + """检查报告文件是否存在""" + report_files = ['REPORT.md', 'report.md', 'Report.md'] + + for filename in report_files: + if os.path.exists(filename): + return filename + return None + + +def check_report_content(filepath): + """检查报告内容完整性""" + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # 必需的章节 + required_sections = [ + '实验目的', + '实验原理', + '实验方法', + '实验结果', + '结果分析', + '实验心得' + ] + + score = 0 + feedback = [] + + # 检查字数(至少1000字) + char_count = len(content) + if char_count >= 1000: + score += 3 + feedback.append(f"✅ 字数达标 ({char_count}字)") + else: + feedback.append(f"⚠️ 字数不足 ({char_count}字,建议至少1000字)") + + # 检查章节完整性 + sections_found = 0 + for section in required_sections: + if section in content or section.lower() in content.lower(): + sections_found += 1 + + section_score = sections_found * 2 + score += section_score + feedback.append(f"📋 章节完整性: {sections_found}/{len(required_sections)} ({section_score}分)") + + # 检查是否包含图片引用 + image_refs = re.findall(r'!\[.*?\]\(.*?\)', content) + if len(image_refs) >= 3: + score += 2 + feedback.append(f"✅ 包含图片引用 ({len(image_refs)}张)") + else: + feedback.append(f"⚠️ 图片引用不足 ({len(image_refs)}张,建议至少3张)") + + # 检查是否包含代码块 + code_blocks = re.findall(r'```[\s\S]*?```', content) + if len(code_blocks) >= 1: + score += 1 + feedback.append(f"✅ 包含代码示例 ({len(code_blocks)}处)") + + # 检查参考文献 + if '参考文献' in content or '参考资料' in content: + score += 1 + feedback.append("✅ 包含参考文献") + + # 最大15分 + score = min(score, 15) + + return score, feedback + + +def generate_report_score(): + """生成报告评分""" + print("=" * 50) + print("实验报告检查") + print("=" * 50) + + # 检查文件是否存在 + report_file = check_report_exists() + + if report_file is None: + print("❌ 未找到实验报告文件 (REPORT.md)") + print("扣分: -15分") + return 0 + + print(f"✅ 找到报告文件: {report_file}") + + # 检查内容 + try: + score, feedback = check_report_content(report_file) + + print("\n评分详情:") + for item in feedback: + print(f" {item}") + + print(f"\n报告得分: {score}/15") + + return score + + except Exception as e: + print(f"❌ 报告检查失败: {e}") + return 0 + + +if __name__ == "__main__": + score = generate_report_score() + print(f"\n最终报告得分: {score}") diff --git a/grading/test_bpsk.py b/grading/test_bpsk.py new file mode 100644 index 0000000..4d191f9 --- /dev/null +++ b/grading/test_bpsk.py @@ -0,0 +1,91 @@ +""" +BPSK调制自动评分测试 +""" + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +import numpy as np +import pytest +from modulation import bpsk_modulate + + +class TestBPSK: + """BPSK调制测试类""" + + def test_basic_mapping(self): + """测试基本映射规则""" + bits = np.array([0, 1, 0, 1]) + symbols = bpsk_modulate(bits) + + # 检查长度 + assert len(symbols) == len(bits), "输出符号数量应等于输入比特数量" + + # 检查映射:0→+1, 1→-1 + expected = np.array([1, -1, 1, -1]) + np.testing.assert_array_almost_equal(np.real(symbols), expected, decimal=5, + err_msg="BPSK映射错误:0应映射到+1,1应映射到-1") + + def test_all_zeros(self): + """测试全0输入""" + bits = np.zeros(10, dtype=int) + symbols = bpsk_modulate(bits) + assert np.allclose(np.real(symbols), 1), "全0比特应映射到全+1符号" + + def test_all_ones(self): + """测试全1输入""" + bits = np.ones(10, dtype=int) + symbols = bpsk_modulate(bits) + assert np.allclose(np.real(symbols), -1), "全1比特应映射到全-1符号" + + def test_symbol_values(self): + """测试符号取值正确性""" + bits = np.random.randint(0, 2, 100) + symbols = bpsk_modulate(bits) + + unique_real = np.unique(np.round(np.real(symbols), 5)) + assert len(unique_real) <= 2, "BPSK符号实部应只有两个取值" + assert set(unique_real).issubset({-1.0, 1.0}), "BPSK符号应为+1或-1" + + def test_random_sequence(self): + """测试随机比特序列""" + np.random.seed(42) + bits = np.random.randint(0, 2, 1000) + symbols = bpsk_modulate(bits) + + # 验证每个符号 + for i, bit in enumerate(bits): + expected = 1 if bit == 0 else -1 + actual = np.real(symbols[i]) + assert np.isclose(actual, expected, atol=1e-5), \ + f"第{i}个比特{bit}映射错误,期望{expected},得到{actual}" + + def test_large_sequence(self): + """测试大规模比特序列""" + bits = np.random.randint(0, 2, 10000) + symbols = bpsk_modulate(bits) + + assert len(symbols) == 10000, "大规模测试失败:输出长度不正确" + + # 统计符号分布(应该接近50%:50%) + num_positive = np.sum(np.real(symbols) > 0) + ratio = num_positive / len(symbols) + assert 0.45 < ratio < 0.55, f"符号分布异常:正符号比例={ratio:.2%}(应接近50%)" + + +def test_constellation_file_exists(): + """测试是否生成了星座图文件""" + constellation_file = os.path.join('results', 'bpsk_constellation.png') + + # 如果文件不存在,尝试运行modulation.py生成 + if not os.path.exists(constellation_file): + pytest.skip("BPSK星座图文件不存在,请运行modulation.py生成") + + # 检查文件大小(至少应该有几KB) + file_size = os.path.getsize(constellation_file) + assert file_size > 1000, "BPSK星座图文件过小,可能未正确生成" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/grading/test_qam16.py b/grading/test_qam16.py new file mode 100644 index 0000000..bc8d4e4 --- /dev/null +++ b/grading/test_qam16.py @@ -0,0 +1,160 @@ +""" +16-QAM调制自动评分测试 +""" + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +import numpy as np +import pytest +from modulation import qam16_modulate + + +class TestQAM16: + """16-QAM调制测试类""" + + def test_input_length_validation(self): + """测试输入长度验证""" + # 非4的倍数应该抛出异常 + for length in [1, 2, 3, 5, 6, 7]: + bits = np.random.randint(0, 2, length) + with pytest.raises(ValueError): + qam16_modulate(bits) + + def test_output_length(self): + """测试输出长度""" + for length in [4, 8, 12, 16, 100]: + bits = np.random.randint(0, 2, length) + symbols = qam16_modulate(bits) + assert len(symbols) == length // 4, \ + f"16-QAM输出符号数应为输入比特数的1/4,输入{length}比特应输出{length//4}符号" + + def test_sixteen_constellation_points(self): + """测试星座点数量""" + # 确保有足够的比特生成所有16个可能的符号 + bits = np.random.randint(0, 2, 1000) + symbols = qam16_modulate(bits) + + # 对符号进行四舍五入并去重 + symbols_rounded = np.round(symbols, decimals=4) + unique_symbols = np.unique(symbols_rounded) + + # 可能不是所有16个点都出现,但至少应该有10个以上 + assert len(unique_symbols) >= 10, \ + f"16-QAM星座点数量不足,期望至少10个,实际{len(unique_symbols)}个" + + def test_iq_component_values(self): + """测试I/Q分量取值""" + bits = np.random.randint(0, 2, 400) + symbols = qam16_modulate(bits) + + # 归一化前的I/Q分量应该是{-3, -1, +1, +3} + # 归一化后应该是这些值除以√10 + norm_factor = np.sqrt(10) + expected_values = np.array([-3, -1, 1, 3]) / norm_factor + + real_parts = np.real(symbols) + imag_parts = np.imag(symbols) + + # 检查实部 + for val in np.unique(np.round(real_parts, 4)): + min_diff = min(abs(val - exp) for exp in expected_values) + assert min_diff < 0.01, f"实部值{val:.4f}不在期望范围内" + + # 检查虚部 + for val in np.unique(np.round(imag_parts, 4)): + min_diff = min(abs(val - exp) for exp in expected_values) + assert min_diff < 0.01, f"虚部值{val:.4f}不在期望范围内" + + def test_power_normalization(self): + """测试功率归一化""" + bits = np.random.randint(0, 2, 10000) + symbols = qam16_modulate(bits) + + # 计算平均功率 + avg_power = np.mean(np.abs(symbols) ** 2) + + # 平均功率应接近1 + assert np.isclose(avg_power, 1.0, atol=0.05), \ + f"16-QAM平均功率应为1,实际为{avg_power:.4f}" + + def test_gray_code_mapping(self): + """测试格雷码映射(部分测试用例)""" + # 测试几个特定的比特组合 + test_cases = [ + [0, 0, 0, 0], # I=+3, Q=+3 + [0, 1, 0, 1], # I=+1, Q=+1 + [1, 1, 1, 1], # I=-1, Q=-1 + [1, 0, 1, 0], # I=-3, Q=-3 + ] + + norm = np.sqrt(10) + expected_iq = [ + (3 + 3j) / norm, + (1 + 1j) / norm, + (-1 - 1j) / norm, + (-3 - 3j) / norm + ] + + for bits, expected in zip(test_cases, expected_iq): + bits_array = np.array(bits) + symbols = qam16_modulate(bits_array) + np.testing.assert_almost_equal(symbols[0], expected, decimal=4, + err_msg=f"格雷码映射错误:{bits}") + + def test_symbol_distribution(self): + """测试符号分布均匀性""" + # 大量随机比特应该产生相对均匀的符号分布 + np.random.seed(42) + bits = np.random.randint(0, 2, 10000) + symbols = qam16_modulate(bits) + + # 统计每个符号出现的次数 + symbols_rounded = np.round(symbols, decimals=3) + unique, counts = np.unique(symbols_rounded, return_counts=True) + + # 每个符号期望出现次数约为 total/16 + expected_count = len(symbols) / 16 + + # 允许一定的偏差(例如±40%) + for count in counts: + ratio = count / expected_count + assert 0.3 < ratio < 1.7, \ + f"符号分布不均匀:某符号出现{count}次,期望约{expected_count:.0f}次" + + def test_corner_points(self): + """测试四个角点的功率""" + # 四个角点功率最大 + bits_corners = np.array([ + [0, 0, 0, 0], # 右上角 + [1, 0, 0, 0], # 左上角 + [1, 0, 1, 0], # 左下角 + [0, 0, 1, 0], # 右下角 + ]).flatten() + + symbols = qam16_modulate(bits_corners) + powers = np.abs(symbols) ** 2 + + # 角点功率应该相等且最大 + assert np.allclose(powers, powers[0], atol=0.01), "四个角点功率应该相等" + + # 角点功率 = (3^2 + 3^2) / 10 = 1.8 + expected_corner_power = 18 / 10 + assert np.isclose(powers[0], expected_corner_power, atol=0.01), \ + f"角点功率应为{expected_corner_power:.2f},实际为{powers[0]:.2f}" + + +def test_constellation_file_exists(): + """测试是否生成了星座图文件""" + constellation_file = os.path.join('results', '16qam_constellation.png') + + if not os.path.exists(constellation_file): + pytest.skip("16-QAM星座图文件不存在,请运行modulation.py生成") + + file_size = os.path.getsize(constellation_file) + assert file_size > 1000, "16-QAM星座图文件过小,可能未正确生成" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/grading/test_qpsk.py b/grading/test_qpsk.py new file mode 100644 index 0000000..6d2334d --- /dev/null +++ b/grading/test_qpsk.py @@ -0,0 +1,121 @@ +""" +QPSK调制自动评分测试 +""" + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +import numpy as np +import pytest +from modulation import qpsk_modulate + + +class TestQPSK: + """QPSK调制测试类""" + + def test_input_length_validation(self): + """测试输入长度验证""" + # 奇数长度应该抛出异常 + bits_odd = np.array([0, 1, 0]) + with pytest.raises(ValueError): + qpsk_modulate(bits_odd) + + def test_output_length(self): + """测试输出长度""" + bits = np.array([0, 0, 0, 1, 1, 1, 1, 0]) + symbols = qpsk_modulate(bits) + assert len(symbols) == len(bits) // 2, "QPSK输出符号数应为输入比特数的一半" + + def test_gray_code_mapping(self): + """测试格雷码映射""" + # 测试所有4种可能的比特对 + test_cases = [ + ([0, 0], (1 + 1j) / np.sqrt(2)), # 00 → 45° + ([0, 1], (-1 + 1j) / np.sqrt(2)), # 01 → 135° + ([1, 1], (-1 - 1j) / np.sqrt(2)), # 11 → 225° + ([1, 0], (1 - 1j) / np.sqrt(2)) # 10 → 315° + ] + + for bits, expected in test_cases: + bits_array = np.array(bits) + symbols = qpsk_modulate(bits_array) + np.testing.assert_almost_equal(symbols[0], expected, decimal=5, + err_msg=f"格雷码映射错误:{bits} → {symbols[0]} (期望 {expected})") + + def test_unit_energy(self): + """测试符号能量归一化""" + bits = np.random.randint(0, 2, 100) + symbols = qpsk_modulate(bits) + + # 所有符号的幅度应该接近1 + magnitudes = np.abs(symbols) + np.testing.assert_array_almost_equal(magnitudes, np.ones(len(symbols)), decimal=4, + err_msg="QPSK符号幅度应为1(单位能量)") + + def test_four_constellation_points(self): + """测试星座点数量""" + bits = np.random.randint(0, 2, 1000) + symbols = qpsk_modulate(bits) + + # 对符号进行四舍五入并去重 + symbols_rounded = np.round(symbols, decimals=5) + unique_symbols = np.unique(symbols_rounded) + + assert len(unique_symbols) == 4, f"QPSK应有4个不同的星座点,实际有{len(unique_symbols)}个" + + def test_phase_distribution(self): + """测试相位分布""" + bits = np.random.randint(0, 2, 1000) + symbols = qpsk_modulate(bits) + + # 计算相位(弧度) + phases = np.angle(symbols) + + # 期望的相位(45°, 135°, -135°, -45°) + expected_phases = [np.pi/4, 3*np.pi/4, -3*np.pi/4, -np.pi/4] + + # 检查每个符号的相位是否接近期望值之一 + for phase in phases: + min_diff = min(abs(phase - exp) for exp in expected_phases) + assert min_diff < 0.01, f"相位{np.degrees(phase):.1f}°不在期望范围内" + + def test_average_power(self): + """测试平均功率""" + bits = np.random.randint(0, 2, 10000) + symbols = qpsk_modulate(bits) + + avg_power = np.mean(np.abs(symbols) ** 2) + assert np.isclose(avg_power, 1.0, atol=0.01), \ + f"QPSK平均功率应为1,实际为{avg_power:.4f}" + + def test_consecutive_pairs(self): + """测试连续比特对的正确映射""" + bits = np.array([0, 0, 0, 1, 1, 1, 1, 0]) + symbols = qpsk_modulate(bits) + + # 手动计算期望值 + expected = np.array([ + (1 + 1j) / np.sqrt(2), # 00 + (-1 + 1j) / np.sqrt(2), # 01 + (-1 - 1j) / np.sqrt(2), # 11 + (1 - 1j) / np.sqrt(2) # 10 + ]) + + np.testing.assert_array_almost_equal(symbols, expected, decimal=5, + err_msg="连续比特对映射错误") + + +def test_constellation_file_exists(): + """测试是否生成了星座图文件""" + constellation_file = os.path.join('results', 'qpsk_constellation.png') + + if not os.path.exists(constellation_file): + pytest.skip("QPSK星座图文件不存在,请运行modulation.py生成") + + file_size = os.path.getsize(constellation_file) + assert file_size > 1000, "QPSK星座图文件过小,可能未正确生成" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..48749dc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +# 实验所需的Python依赖包 +numpy>=1.21.0 +scipy>=1.7.0 +matplotlib>=3.4.0 +pytest>=7.0.0 +pytest-cov>=3.0.0 +pylint>=2.12.0 diff --git a/results/16qam_ber.png b/results/16qam_ber.png new file mode 100644 index 0000000000000000000000000000000000000000..6cdf88a7bce913f32ab514e43b0456665b2b398e GIT binary patch literal 8106 zcmeHMXIPVImwq#-j16QE8B`DzK|zU%h=3G7M5IP3MyV3aq@9J$CEwVp6E^byyE2Ql)t{V>#$z(CDWK_hv;)p@Aa7V=AUJ6u=_B)Kn;_@sRv>Ck7)vKR7J0tsueFXB#+_2ts>=`r7nf?+z zIgNN(&&kQjOM&}c7e6b>&Aw+jp?5mlw8^@p>DfVrIG3T{&Q=BP6`w2^4%ExD>=KRt zdMqWTg@BuxpPw)Bd<3p>ZqZB)-K}P2MS_*l1Y%aeJ_$i(`{4kI3C08q zcu(hG0UaG3zXM(?^P_d{ixXFc!+hBf&}&69?*zry@EueR<~DS>MeKZM(Vk}3l|_?9 zmn^na{KjeIF;_{-$HYoGMUcGSqgaa<8l_3JEELil_T=2KSCb66KJrxJ=vXYSKdO?f zEEIdjvh!`byCE(0_3PJZai$H=o!WG;N0jI3)ES$utWoL!gANvVW-WkMvR-=8SI1d* z?#)S@k-gWPV9@x8eg8RIQg?2T4WqlGbeS@hSq2-12&zjBKIFXg^w)Qe-&%+nuyl5l zES356TXOVSn|^&PmfUTT+1Ak!^y{e?rKMhdMNaADn$kEar%7)Vi*@Phm~x09uU!R4 zhoQSwuz<+5v~$x1!&}()NA}e(7caJ`Dh2MBbeaA7Xgjx{a+Vj3qJ@#k&B&Op;KX=L zrRU2ocp%6<)1()#)6(ioCwlT{#5?rG>_@7>>m_u`R61F5Ia7%622$iIR3chE=#Z7{itK{f&^L1!+n2MNbV==kkU5~GSPk!&n=a%kKxr z@7%d_V6_n^yU;ltB>5;SFc=_yG%RMZiFO%14ol3`dx+-9I}dWR@&<-}2+VCLC5uE}7|b>M%&sG@I@%g!dWs3n(PPlglnuEqX}^QpI7sQHGS$ zI$c8U-)B=imx-rMVZ7!;QsUeunfc;69?J{@m?t7V0x>?f*sLI7*?H2WHgr55Uy98% z1Z+jg;pvg5j3I*8G7UZFYlP$gr$Ue(KAX2w$TI_ON7NvZeA#%rzkJED?8^F_sD#$d zIocE4C+#p+|NQxLOH~Jlh1xI?Y})nDnMP&pRd*G=XiFU(?d@#8p5!N30C+^O8N|-r-r%f2;P-H*?00*ZzEGI4dT z>OdL9l7*G6m+m2jJBdQ(o2c9&EbQlPq0T4=Kiwx`OI$uhl0iZ4ELNIgVg73|4oodNfo zn2egnu_FlS&0~w+o?m6$7K4wd1`FH(Gyxv<2At+9xs4n@D|R_?Nm%!;3}a+yi>)UJ zZSC!%x;d{*7&e|$dJ%?h-?j?HybZCz8W{AQmMb}I{OM^|&FwkkAepmz7WCP2_NEv~ z2PHzlUUBmeKAV+;k6(YAkdYzUB?A?v7wVzh+H84paVNT)|zZ!SIIRxItqX>qfPF|ugyU!lm*GrSNLSyisx%Zl!Ma@ zODF*n{csw`JOw~O>DnmT6lY>%165B|FJnt6v{QzZHU0t1vi>qfw48Q~C_Su&2g!Eb%ZyG3eU6_smXH#DkSoD%VQkJHSyjDm1 z;!Cz5-phx+Uy&An(?$9BT)%(Es#oH6t36LP@C2rl6Qr4{9TU;=lu0436!4ILqT6pl zp2?ZbgFm?>X7!PL+b zL(y72suVn;h!0I#L*ye={kfeKZwPCRoCjeTqjXWd0TT&S!rQB+pLp#Z4>npalXoH$=TdE(N`4AFb=C3S z6z+{U1$jFI+s5+X9>3%iB^;YW{e19*VF6*kz(_S)crEd>KHYjcZ3dMP5@qLiQ)=<` zX;P`a(f~{Ig_fa)g6+7+DRpQ<7f0P3!E|VRURW7feYTCY|r`-l{tLV!J z@uF_=E3siV#}ey;i&d-!C`X44D@HD*s1H&TM7*cbCk($t*|E+im&?&=eR#aB`MIVG zvF}f9krJIFjJunoo+NlPPm(~Bn^0+Ham>ka8JuQ`M3(JS>oo+4ILC+t)|;8I!o6|! zqHce#b3S}GF-eutcHGlQtRpY(0(*0y=3t!r=KaPA(fH=D1lG=I{Q1_8%%gi-!-_=W zfg0_CaRXGS#kq_qcx9K_+>1s?JcB)p)cFfo`434SWysskeM0)gCUDnDJR~;a+JX2STjp`R zr+-;wC={?rGn53mrd^o_uD;NWYo^}5U$7n*q-cU6FKr~8?Y>E7_ch&OTlCB%C-e!& z!@6n&Xd2Tki}H<-(Ft|LVI7mSqWgzbzvBWmp~4HtR%b7aSPuw`qeJX!O{x!{{o!?2 zdt(?u6ZR6jRD^pSGQEv=$Vccu>n9lhp^#MRFC{jOyv=8DiYv1l2yL7`VZo_9d8VHH zi@eZf0Po#i6hGs_zy<8gebLa3Ew0Qa4^&N!Dv&pvBp>&{%a56q55qRzc!qWNtc+&- z0{>a}qx>Ha`b|W(3jf%o+WDRKU1_Pfqs(M*#l|B@bGqqN(s@VD@LEK^kwn)qaW)MI z2uz7y$3GacOp%%`;~376$>PC+l1;RxWWB(vyu1SI(L*|i0|#VBdV*MQwQ;v$1qfBT4b_Vyd#ikel)T=1SXpdxllS ziL>IE<6%H*S#shcx#x(-Ap8jFW&}2@>$Cmd&1Gb?i)I=R4w}$r4TIdC4m<8S(t^3r zN0|;LtmRX>S}__rv(y#Mh$p+5z(9&NbCj8)|8!}^i?6sT9hL3hQG*nrIm*hhKj4no z%YVqf`kq-IkX=t7V;;J5?4FGfL-;eF=+ACsl04t;6GK|-*^4Ax*{1pD1MAUqe?IVD zIyKPgncT|K9}w{TGuuDy0Y`~oYz+7A`F~wVGQ&8<5tr}fC`?2$#G4#UOG_rZue2Xet zJ?7lpkUyHHCRa%Ydkkx zBNTrg8ti^(I4$ErgvgbB`R?7R9{c7Cths46H*(E&Kv?KGzXDy8GqmcYwM~0Y=$>&~ zoG53P^JsgmLz)NY@%yQout|>*uP=@xD z8*LbqZj_mxZdH<`SQmZ+`rBxU8Ss00Muv5SDU^FjePJ>4FRwOl-y@)$b@PKyQBl!g zx!(YuL4(#&zra>$(9p;T4?Vn)vORoubaa^R^V_*^8MK%DuI!>BxA)#CX}SB*_Z~MW za;$v9Rqngf;`_>ly21+qHXg&!H^-iLozF3A1+^}VL8mswN|V9QG3Qf)+Ls}!Vk9We17uQ$9GkH)CLRI*x>J?LcYd(YhK$}-{BxBUPe zxbwn$&D1kyEiYXc#!MpY2M$AqYNcqD(3X2}rFzWkpC8ZU7Jd(@pg9E{piL;1xRd*f zoThBN7NYO7unP8F`j2n9GM{EA=_f{8(;Keyam1t;8bJx_VWbn^z>rqYFC9a{qV%0^$oO$1{g zS0GL>DIiq~ZIV~wm_`h~rRVokK;aD$;E{GF!jvGwYHhU%PtT60St|MMSReSDkXUC< zS*hfA##Qa@?HW<1O{j@mcJ9?SG?ayz65OYOuEL1pai*zEs@L7!4fn3h(ig4V+}y5D zpi~2$0o58mcF=#%iCX9CG~L|Scm`cLSl?yvk!g(Grx1*+q;+orG2(tZpF8m#aMNip z%Lr(dlI%f5N<~*VOp5pc)tL*27{}l#tuPJ2)gDXH2MHEbTM+T}i(TfJ500pY-;u~} zn^f>)v$D1}EVLhe!X*M^AcEJ>z@X$-xvzjSn>6t)*2q&_&rEBbZiOep^dkI(-dW&l zivD|YiR|*n0nbJCz>jG_fcM+JM===#@ME`y28sM$9W1sZQ7Nd3&poZdd?_W~ivr{| zPQu2pZW#uRz=(vDvw&Gy)`1+YD`=9HNMFt`qbFnW@0c zShnoA{_b`q@K-6~R#fx-TIqT4Q4~<&e*3x;V3r%qKVhQydCU!c_|nq}fQgotR&-+E z`T1&YBM&>^l_nDh!W;#6(~gol)-=yKrw};V7J{G+cHbO43PEeGN&Y_i09*-!ojC_VS`~H&_d8!q=L|-lyEE1( zo)vVpd>>S4ci(RB5yIn3B87p4BanVZI~Bn@T_Vvitg}ZuI_9Ut`1CC)q|@ z+qUV_4&H_t#U0w9y(Llm8thXz+Gn@tF514$YX^T6#J-G5>mPB`^cLMF^iGx#y6c!$ z%muUF#c3?=p)1}`27*35@I|C71R1V{_TALZfuN=vP7vhVv;$o7RfaCBN^T^m%0gRh z5%cfYYGXVi?aymjdte9-YXGvmeMXb)ofD47l z@}!6a%@JynrX|02ve0`80=Qxw<^si;Bkbo~Y=#p19Dl*V^es#6&JS6B|a| z$?Y#OqrZ6m{5&%w+|`aZ9HWJsX_N;+@G>LV`O}H5Ng9GYgeZSU9GDVrRE@#9WJ(c^ z8DJ=)CHC=&=1nMz^RL^)LMCFMGO+ITP5b`-e*K!)Hv;h8T>2Gaunxj0^(A5uZhcxt+iTdYt>2iHqbi*%2oq9Rhzk;UeUUaz{K!CD?8TKwxV2TPWmbPC!! zHj2gOokuKYo9^hj;r(jPk3F5IA}FwDY-kw7+oxxP5Y9Zlb;K+m_Ka9UuC1t8EG+Ld z8@qjsUWh#A*)QNy5dP19E`u3Vzxuwjvy)&L@${>_g+$sX+BVMUX%I@&jZgfL#(Vb$ zGRP!>yNmffD{MqX{PZM9UUZE}yaLv(Hk>;|E~>e}?EKOm|MKE!qE347*ChiU*pMBO z@{q}S!hP|2lyGEIos3FlIk)2M)o>OfN}eCJd>Jf78+3E*`8s}8&AVbv)Q_FBM(|NiDVOQ{k&`O z38ZrK(>r=pOWw=mXqU|Otzf-DMF3^Q0_3R2Ja>r`-f7BoE}4iLPep$QJ09GnR)G`o zHyfRv0^6zIjYkc`%%n<|RCh~qXY+cms==@Ua0^q%scdO!0lB0wtL`64om(2fIHc~* z&B_{H>_r*G`Oc4bM2iIsDwtHj)zM@GZh2<7r>dZ8Uq>c_VpiYZeaFpfrHUd7d|4f$ zlNmKPGyIra$0f4|oyyTvvUpYCU^0A-vAR@+EjfPS!n|LKXV_97nmbU%#d`$h_lNgC z^atyF&j{T)(j2d*rk0}b=^iMoD8U9celLpc^g6z9&+Ib~|WCGm#NmXKUEY(IovIZ6wBy^_*9E$;93sQ6pocIaeK?;58f zS9_E!MwWUyD-5wEH(_p|Fx&* z-?}94MgaVqDfqW@?%`aBJrM-hv*!kow){qgHF3EGRq~M#G{Wk(wnxg(!C=PB zS}?L#QF$YXcCtkVM`ElE&+i&m^3DG`;j_int!~+>rgwvHlsVH_`l>IVmRZa&n2}hy z)zxnnk4gh=k_4)%AuJfN`0?jVi;a?ZuKy5U5V756@;bi>#Pb+J=NC8(HGKH4MAyGr z^S4go&ZH-g4nR{l(+Xq@1e^%#jf}<8s*oMu0^hTagVV7EGphZ zuewsMcJcU)CFx44-`uoO^T(-Qmd`(@6p0D?7>8 zzDwzN0XtQ-=xPj4$7tcQ2u%;zYbmWyFngDb`Fph5sEgrg<>cNAUr64Og+LZ4<%9}n zidoVUaP4?O@F}qL+iYzn1(=Swt2vUI!3xLg>b@1XHO?3-hh}jN!&b9y2D0cyZYBKmm9R~A+6WnDoe_r6JB@ApzKu-Bwl`15LH5LU})ug z=}H4N0fBDJxr%a0m%q7i3;q4?iYv)6QxcErE11jT6Sce0pN75nyX%j;>#fOsJ4>H5 zw$X~3X$%={Ied4Qk8pWb&$ZAb!TyZD|4Mt{G=SU3wP6snen=Gy#A3=$k0s$el_H#b zCK15d*75Wr4>#c^Ao4EnpSa<>P%J7qkhXK@&hVj2XqQAw#V}VC2I$J?Kii#bNkCQy z`#32Bx}JXM-@V;|U~!gSNbv9i5XyUb!J#A(Lt3PJ)`fXq9KJ43dQCw5D+%6AlY{m} z??`UA@riaAcW4lMo;N+1mx+r&;yXGz2GOo4R3xVY=>15si05Fr_-h#q-I>19rk#A- zuO^_U0O-|#XITegioHm++!jlNQtp>LVdMWrwV06jB%zg0X~cX7yxZ-H%+vbDJVT84 za}(u6+p4qSoc^B4b^xg=)**nCt(IP#l#B$FaeVgix-iZfMGHseW}vR=U0sfWeO+C_ zjk22-2A8{?Q(n%?XM$yfGos4P=0X2qnU#}VYoHEYZd}?U`VF42=O6#DA;%_>of>N% zHnq&7h&`2?RoqTEDK`TQK%8o8jV$aetemnc?t1a!IJT;{#d1-)SODL8h;AjGbxQ!E zk5`^c{ZtlGZ8wbQJ`5iOG?$T);i9oh#aTP;fx|&g0K|h1z^?%n**oYZCxY-8Z0A0ofA9Uyj ztTM)_Vi0!weS6@*eNK5@SOoE4cIbSnS7BXQc`CitTOgf#Q@B#QFSn?kf4$z|DqJQ{Ab-CHvwzmDG zJ!llwuk~PjL%W*#7q;YxS<1xQKa+RZnR;KW{?lT$rKUXkl?nk6Hk7WM>0NRKBZzQByRHz#yuX4)8y zEnyy%yyHN0ne*p1lgq=NH0iPU(F5ng8241K@{MH7g!V#_L z+oGGZF|~^~XRY{}QVeK6YHj_dbr~Naj_>yQ-(y%Q$C`v_yCXe2vtcfKYS+nCmBAE+ zycMs^D-^_0lCw0>5D=)2jXb5mL~+}M4)sGRo?OW-AjHxlvaFR>+qrQz;f=2V8D0@vF(!>>c(w&8ankNZCkV8O^~z0!d1agE zb*dCkek-m9zv@WzzZ{VD*g3iFf029hw_7ih+b2ForE{pYkD;#nZ&~09sXI6nq+fdLW%jkL=HT#e-FS7!;>wr6V;_V;vzvey4{GX4z%>Wy=N;i z466&{?fJ*@Dk~~Td^Rz5Kt6aq@s3|ASpgEO04)lh+YvXiJen+SHozHThUz=g;F*!r zH6B$#oSME;pU1$(zyUMEYOYP&0T?zw3lG$KYzp*mQc@BiBPQtVBLG_AX0B?bhtiSY-5JJ#{Vj=VA~P9<3{_dV=mZ4~ zv+sb3)}yySEiKL5+#Ew*8Q+A8nzcpnX}~|k31?fmFCL5nKAU8-`A^-*Wx!-34~QpG z;)tmStwT}s9fIjP46sVdPj|4VrqY2sCA;ZFaKC?EqPl5W|EM`|Gg2r*U@v@*?UqT} z?5nIQG>jOD)tP=-Q{J08XvM3chK7Y1@7V*OAOmWVNM7Q64fTj|FrSJzk#Q`B&FlDA6BNZ>Cq6x@S@5bnr`_Ovh#0fn} z$rua-EToa*y6FIae`MvkzWr6HV46*_8$H0!-Temzjfg(Klnj7ef+bGf7~X>{;$W!2 zT$~+gPE1U+PtoV_h>F4I=J!}Sa`{#viu)+Ddn|z$og|TPqY^Rru5Ic6@N@iWj>-Re6zwbZO<5}+e+OO-ru6Iv0)GqCzKSYlp$R3r;=dU9O z?HGb=3*5C0?(heW=pu;pC6)7MwO!(-@a~}z^PY<{qclT#>3M8#{NJ5F+$OQ}j1y%dO9DD%+pRUJSzR zKI#&UEqfW{;Pps2LEPO$Vl-})sz~VgD{^AzuEU(6E)vl6f>?U zBUh|1*shcAKAwwJAw=_Etm?#|t+_Bf_3ZpE_mJQ*=@# zY&5~$rmMixwtQZ9IPw$4wcn53{QIk!mDy%t6>{P_HBrylb6%NQLHce>(j^|(p{{#5 zh9!=cx`{gwq2pbt=3V=+A`IlM zO%c|twX|9v?`G|F84PaE)Z=v>dQly~Z0j13-*o1$Kc|hz`Wcll-qo>7hcAV2$yl|8 z9l6@IYHR4+X$C@)pZriwEG1POQo4m8=Y`o>DetaEd9BRU3dh-<6K17+3t}fYG>TDr z9;k;2E>9H_fbLVbKKn#c;!c{>W}y52#z|N5(2=W=FKv%j&QE0K@W|TyLyZI>m#>f;Gxc(K&N$u$ zu7Lko+lo%np31p*66I@4e`$9z{>d|I1VNYC;7k1d{h1XPlWYkUv~)}>i-XUX=f<_D z_pr*!)iuGK%M?jetAS^+f{J%4c!66=T3uzPKx*XT4SV` z`$T%G*XmzmrPaCC2!jULQ?GdVsc=Hk+^?&n&56cdwMwh4N}day7LAqS5mzh{T+ONM z*6S-3p36cil6HNc9UkhU7Pqj$GB3LB)J5JGa zY2-(2qqnv}>?!M*)Aj>J_JhQt!4P<{Xv#ZNXP6ejI!z56O_urS^!NA3$cO{EDOxJk z;VLz2uqL>4X(IC&PH`wyIRT_N)m=y#ZykN9rRF*up?C2)&+0b`FMCosrDCRrGhC4R zh1u(b`@*E$gAtE@2IE(L^_!Y(soL;CiIm7W6+LNT>yyJc95cFVuQy z5WeF=Lvff~XwshAnA0c5S$@*RFaGxVilDWWLmrOMRFQ2jb(F z(;zJ^4MAnE!KeHBxp%$|)rUtN*S}uzPFr*jkNhIU*wbT4kHQ40c#>Bkgs;Cjm1}Rq z)k+2ekAXZF2;S=w-qc10c5%la0)k*AlHK;+kU?T%Vj#WTfc%8V5j??oYo7BOugwEf zC6Fx8d-xKkb&?bW%7jKX&P~=OP3u0~uI}nhNErvGZVad|TVGqIpfymv2daXIqB@9 zrG+VsZEsmaEa;#TO-^lE>Z|QJUNR8um%RE+kCx~}8&%LBLRj`ne17Tv?$!0*8H@LSNtame6ny-q$e-SV+WIOi1xfJjmZu_Hn4@oMG8%%g$>7mI! z&TxVTaTL!G5Zt9}hB{RhHw{dsHjs zydpaa76U~Zlf3vPeiCaUkfZzpTq7w9={^~^X zX3)MGHsr@{^h)O5Z&=*?N6dWRBW7YD!Ha?Ic+uiO5D7K!#1F2sBU1U0Ay(t7$^l$kvTgJr`6UBMo}G2#U43oKg~dfiD4 z++h7>(NY#6+?d}HL4l~vmbi_-i@F%XrB9to(|n_wV}vI&0l5m`X%L+!GBUyV%$pNd zA)U#}$V3p_s`tn;a-E)@%x`w?ykj!JgoM7ko<7$fz$>JZw;6-4r%Ss`_Zip}A5jT| z&DMsE*M_V77$nN3tHRaJj0Yae{sWmhShjZ&vVNybQ0P;LFovGrE`6J3AnTdb~(DpgWPV#5W;y9-GmJ;kN5 zH)PwVflzv;WnKlV4!bJeSzxIZCdeskvr^hzP;L;D(weF+!7DVowoKx+`%E)zNBDO~ z2s^eJI-H!^G%u}=$X3ctq`d)?HwYo2MZ^>yd?Ba!!Dxa9xgJWC3D~*Jbyn22%eHbg z8yp4I5?Z17(9ljS_lHLpXp0`1DW|`AKT!s0F|>?X7}@cvgohKdPH&Jz#h;#@p4Ro7 zZ+|VmRs;KoqI6F6ka1Br*v|h$@qB(iaoj?*vT3QB2Jx{Oi~4%|QhKwc=T&Xt1zH3z zQ=@z_VJ;}Y3-{^`p2mlXWBP@xr$E*M+mzAhb2FFOPU313w$t-Jc4dN{f9`6;GVdRZSiJZCj8- zrb!D|`-H{dY@C#s{+un+5{_?k#$8$#<@IT??kSq70s{kYIc44TQm}qusv9*oNJW#M zx@~4ho0S&(9vSpf;g98+=iWV)-s@1^89M&FAZ7ktgy7XER=Cn`yxI;StsKfwK@~}; z&so(@_gg3}>r=DPYWm?0hBb=!x@=JM^B5MQ_PqXfg)=%S?r%IJjJ>!XdN%DuRKnuy zs&<2c85h~zrv5Af+l0_eNlkE@052{DZ(d&|te2BT#Ei-oe;e4Mg+CM(P^UeIw6aPe zcvn5%tr8|b{h8K#wZnULx|%uB>9@X07~{bceUUMG)A{R@y&H)lk?W&C&r$NYg5Ud^HN zrjYI*c$#x39miTCD-H9_e|USCH!Dz9`kB4|kgvffzaZTotK^0Q#YB5%WB4LC+(M`_ zlk4mc*+Lt97pOu|B}8uI6~xNtki2IGzSVL)_zqdku(tVW3@cGvzi8^<`C**1QH3+z zuHV1244{Pm45hRf910dDJ0V)FHsLB}fra%Sgq3TV?jRt@+b2}QmPwWCofe7UZ0_F6 z-2`#t3Xg`DM^(bOJm-GO!Z8R}xr|5CJsi)P$MTbPA7dUHQjz_i6_+O;3_KMUQgMLa z1gH;G!qE6SEQ@z+y$0dwkdl`N^+P?#@PDdTWw= zL>kGta+0Sl2!^oSOMwG}tVNR==EWs$r#4%!+1NKBXENY~GAtZh0S6e{3hD?HDUGJoc7G;&EU9) z(VJ3k7lp)9&Tj0HTU=bs=I!#tQZF!l?9OhA^ff)AN8_uyY-zurgR^AJveY*PY)2)* z{1EXg7jbpq{eqy1u9vgBHhb)yv~R9dLymX2lfNxu=v?>Z;kMK7Y zQRqJ+L!ovA&Cw~{(wG4G%m+QR*pa0RWa$d$#KfC&d-L%1B{2alIwy(Uo;h*ykJx-J z?^o)$17_w_X0R z5flD^`W>?)!K59p1n4Pej7?;i?JLe%<8Zdy6|l{3tIDVI%4@86wq3{TrO71FrDbau zu^Tbv*cV#$=Iz-(9-$RHN)x!>>7%)hP->g}*Y7Sow>k)-TKz_j$;Kv)r&z=WU3~;D z7&5;qV)u;+c*W67Yk$Shz5m;T;QS2gP11H+!zHDk{im=$-~Bvq==aq9A1lLI1()Pw zNeRzJeXB0Rs~22?|LV?KXO^p$DmR%aCQu}tu+J-5i@7)# z6}cqx5%)=TbhN$zqfbg3$?C-3U;98$i1*|ibWQsF`bo#9_BFZ(wO^;l*$0hJl_c7_HcCknR5AeJA#$HiZi(v4>&i@*o2)wSh(S-YJc@Vx_u; zrtU?-GxDM8Rq>ba^;(_^<^9O>%86pf**%L@+)WWwphP!RYsN^OX>B|kZ&mTP+54k3 z^A2-)=EoO`Naw#^s*2guvUkvxuP|_Zs4Y8Dv^B4*Tk_fS@Yef2YNva4O1`Y247G_= zm9ILJ=Hxl{s$YsTr=461P{XNZ&i!*^EE=rjhOL@d%gm7V2~va)|LN%Gqn?DMTi5%I zT*@nJ1QNdAt0>LUU>MT(teI_Bkl-cr9U%JY&EI=Jm_8eGt=meXm8c=Or8@p*^fQ@q z`0OV02qt4ea*D~X2ez&e@4?|8uM^ShP?&2PW%nh~s65CgbpW3B{Lh8oL z5oOx)nhY*@WD+J^+*V#|B6tONo?Y6dsFdNP zO(}wWmQ1g+n^b$GTC6H;4yw27)XdWJj791yyKyrY-x)}d@SmAn3NqD8ZE!5mK0LsG z(@BsFla?E#*#h3EX->TJIRE*g%i z_At~jDE&07nZ~wz?bOdmHbU3VH8iPqDY@{4R%h*`=|S}$Z(m%92bVf8j~5v5&1jv` zo^3pOfOvtuUxxT3BsB;&3g=b!pP3_H6$uufm@Jy@?D$gL?6~^Q=pL@;AXRw4_t4K7 z$K`uMn;&Ic)RJ6WnmeiQd`CiN?y`qHq_e7(`e*!Chsq(tw&)_Qk56nDcR%)oTgv&{Qty>YV?P#jh39H3)7*$ib~9Fu zPY`W&<~RCc@?vLQ;JcV@mj?|Z_Cj6s*HUk^sBqascZYhk$r0aeT%07oU4ManKag2= zzPMNQ^=ZedS$$t@zzoJ~S5qf7<$=znthX~DXN~huz6X_|hgQgGB zHPG)dj8?#eS`Tn*1;&a!1dLZF&9DPEz#x%C6S|Pf%yw!5-mr24d$~pLg-T%O$ zlDgb5tYUTfxi5NCC)Eeg_*nI(Z(IPxop44;C*_Usr@%!cg}$eD5(T?*>^1e1lbLpq z{Iq>+#v9~_^c6QZLuB7k7pwgrwch}%%|~gX;)8r8n6-f z2Ruv-+(Gz+#!n3f`U+v@o1#5I;#K#!_^(dSNe@fLnN>mpvW{p&rQ8xK?I_RELLUby*IWe_EO zY>KgWpM0q)ycfIef37EXBRLt|J5;ux&Et?U=u?@r#-6ubouZ#i;~-M>FQU_#cc?QT z{7fsi|H_xtjcv7;y+yB1J0?D)a}XJ={kfImOfP(CHPpT9LjwuC6 zr`t)C?aDbRm)ti_b4(g*V`elB4G4ula(TlXC0o^oCE=((hr>+FE|Q&h%DLkjWq!J60h)A|CL_&m9d5w>lc! zC8x4mu%#Eh*TUalpN=rr?%f`mb>&Ur@x_K5jKt#HEXh)Hxb!LJ@mp{QNmksMzWs-i zE~}egYn6`9pH*lIevQA@Lvx4gbLu^1Ej3v;~CMK@*a0^Cz z&X1#?segQkixR1RD}v-0f5e4vm!&W(uHQ&rh`3*QG$&4!Fb>gRNME!k{M9a;z|V=v z%dNPQM*2OW%IS9McLaK#2707Y_RF(eEKM|+-hsmqS$0s>NFY`6;ox~LoVuFdjXGQg z$fa)mL3!YnBISREFzv7VeDA-TiJuYc;lCIXpM3Lu3!<;GY3<2}$?>Asr?K(xHmv5W zT7?#BA4^+h)a|I_72(!jYN>)!Nl_cV&b_$O?b~r*Am~60TJo;^KKWu*aHrdr()?{+ zklOP5z?Czta(alIGA+d$x~A(|{(WmnZHl4dja5#WOh#fY7WZzm>S{3|$H!Qe3buyX z(K%BU1fL!USM#+|C0u=Q(Y-9KOP#NuX^!ZA8N6An>K>yGKOy}L+#$PP-lq@#K#QkF z-dOV`ijnG=lBuLGK~acHK6ZR$e5;U+FtDBs_d zWZJ6iq1q;mOA8i!`?maU=&?s?!VqS^(FVnBl-Q7QjFTWC`SZV4;HRo7)Z;|nZb~(8 zdFRj{j)}_^%3rn|JoEuM-pyf=asS1QXhB=dM@Sc@GVKVZr&K}AR~9*5q2&!5{+jU`!!Q^mKTcSt6$|i$+j9^P^T#B@= zkz0$usN48janqJqc&KVZlg-%dEz&eDvk~e8Jp$q`Z^(!57CHWX9j@-5t;PL>H=Z(X zeyqW=`SkqOQ_UA7({KG!8Wx*$?+-PxsDrgiw#=CSCOMw5Gtu|vmhAh6FdShd4a}BU zZ_UKtug9hNZA$G9KcPO-BVYnORY0NP8##pOgqOuOm;QRWB3ulVM%uDZ?4^yzEu34A zv%^g+8k7#Kk+M#%LY4#unYS3kHj_PT3xgn_Ii?nycusxFUGN4hB0AM;h1f1J&DdoyV%-T`-3dGN z7pSwkSyka;%|;*s;Z%B@4^xHGCtQeoqd@^CI-K5xUnd6o~V(w_Vv%igvuSaA6k@#YlWn`kH})rOC5I|Lx)%xsoR1 z1g`Lua>A^qYFNSY_0L`LN&k{(@Le346fHfKh86<-S2m2nr`MD7BU!EDqhXk(N)c0d5w|G?12?SoX6=jP@H zQP76-_h>0PW}bi6rc$oOszEE^@N;?cS61~N=!;To_HdqBnM+l|TNmQP1TCQlU}KX2 zLpEsG@VfN*Hi(&Z7~%{Ym!BPynWz^|EQ7JF?EOD2jm^7~=n621E5Y7&qpNMAN9=5C zllEMiYm}%g^H{$sUTg}}TEf!r!nJwh^?B&!tVVfLqc#UAkC@Jk6|}0|qY5j*w750p zn)~0Z!mhPK(L(6hF+e{KomUdPmZ#8`7ip3;#Fm>rjZCDfr)x90E)`pwA8M>Za3!&!82X>5&MN#L@JlV2_2 z35~#G%VIE}m+SMPy}8=|9Gcbh9mWJI-3Qb9Fz5xL0*A-x4Hny5;Cw9SrrzwCV%?$~ zA93ZKZcbcRy`~iQg`Qc(|1o*)NmWNd1y0ZogaJ)`-vpp$uptiwzf|lWFKn9wX#Imo&=Q z%XNOD9lDCvRN;d4-L;e)QLGRbs|rg_i?)}c&Mr;xXdR1z-U)tmc0^_GF0>_${p^9J zA&iaJg}uqb>$6JhXlwDMEf4=Sqx!HSNvv@e>UUoE$ z{i&mgN~U$886Fpra_tzYt{!fmo~a`B80m57HeCxFf?sPJ8y+RE6<{P$upVFp9hybz z@n`0%kIa>QA8vpJ^y-Z~@AYm?0tYx6mYt|QU;7R_s8Z=P37`$>F{m9?E6`GHQ(!vxU* z^ZaiE0<^zAWlF+)`poQiDfBO}y}vLVli+17zWejF@ftZw?;raMW;!YAiQL{Fi$-GZ z)jZ>9b>rIK4@(w9-d!J`YgJEF^7f*NP;5FsS$DtJ&qs&lO(eE{ znVsa%4H&t%_}w=_TkV29C}%Lshc@?FgF>6tpU=IdJr<`0WufhZRfB+Zv52(*vO}gy z(Ww68j)Qf~Q)<0;aa9oz0NY(1L=gq|l8o9tKH`%0bZN5SoO+D--P%?IOxUcCp8Ps* zaENpLS8hHj-QN1$%D8hcV~S`sWOaLK#?Gj#?WhZLrb`NXjKS7Gu;pB(`npBLbMOUB z7SRqpd}Cf0DT8|$-eph6#~KpDZ{SZ1^@fm31s&zx8B@CfXI3Q7Y&ib?!4l;rpae_2 zi$l6#bQ`LbkbOny_;;4THW)a{^^a#D1*QW`p*Xl5&r!nqxa}*qPgyRR$wxSK+vBsP zTIwNDz8(&c6b=?(utTu>WYtX5EmQ@D)LZ zut8G_IV)vP_XG$TJEDvu^U2uLB~8CCqkUWlE8KaDr+Ij|heqHXe*EO(saHPRd6KesgQ2PT^GAN00$y8_maZGKxjv*o~k#EcqNhn&eIIsJst^7kcO0?9| zVQ}bJ1^UnhefSW65|LFV>PpMKUIE=|@XsGNvsA zJ}qC<2cx=q!ZB45C1_0i7^%Q?j7eqyl%nnHOqV7D^lOs>0#h@|atu;!H?08q;PxH( zv8n}r4y`!OIO;Is({sVF5VvNaW5Y9KlJWLsjM`ClbV<6v&4Etf<`b6;t6XiP#$l9* zH1SJ65N$u8z-b^Y`Yv$7fO69IBm(EtI}Jm4NgIlzMK&F_bm^nF6w-FlvS}<5BZaxL z8W$l(3QhT$WzW_Njnp!+S=AWbrYGA=8iAb25h4^+84KJ^`j#+Z&~?-b-j!I3gsNLx zOktuiHI#{mGy8Qij%`*Wn{)ECfCgtY2I`iDiG1gunU{IRCSBgK7Ae~b z_>+FPb*y_a5D+j2k;C@p(HI1})eBYIS4e4)5c;1Ey;Fz97G0%Aa__Y>Y_( zEUQjpib(_gHtJX0PciRIJVSj8w;PQ5Ln+R5@DB^Nl(U3AEE(e%1p=UHA;R$9CoXT2 zsRhnHe3i#b*<-Ca#@N6vvh|(mL|DOdzUH}q0nhIMlp(pPiV<*ze2i@7^xs>!OX@%h z(>GqxpwNk3k6QT|(3|IA7sf^+_B-&zpF3%!?N9u06?XA~1qXn%UC+!YuO%G=nqa{y zlTPj@p|IlR$PG%*z;lsp@NqvpJZE-0B@~2nc;n+&H0Z}7Obk+3U-Zin$GU-BG1{qp z5FwiLFf6n{^WGcI5(kxWcdvrmVk7as1AB?c{>NsNhbJ8aVAO&CYc_?&{*RJa1Zz3)((r? zJ4G15k_GPE7W&7Z$Oi|MGn?|cD$7K3lR0SD$kwf zU&NvDFNKk+Nj$#NLhvx7}vR7nXcQncftsW2`M$dEwsy@xIY$_T!8KB4!})rpp{D*N3ROU0t?_%4%SZM zJYG7Qn2fn_1Q=k4o360-t&ZcR0pD8U&w(IXj=(+;WRbDmjYK(%F|`?x#F!|s>JMx= zJ~D%L7wlY8o98Zazft1^T)TbC!)WlLH-A*AiZJ4j?t{%0fV4D_FrstowooD5WJGTc zIbKdmfOY2L%qV;ZOSBmO>*pEudG3n98477}YlJoF;3DI5p8Fc4GdS}j?O;J^N8v)| zcBeuBWZ)QJHzRkFsVxL9tnwxk9{3XJK@7&C+jH?oWH%CBh<-%~m z1F{TH#?XLGX$XVZdONjeJe0*3NTZ|@uv04CgRa+HKC!#)mQs~B7M!rD}D z9$`lO212OV96So5PAEn#;5*bY4i{h_9+2Qo;4oAx5II7v_p-+eZ7zI{M))u3Y?Na01)e%A!*NpiumqF;`z~!A6j&BZ@AH2*e$@CV*=fI?~PZ!Rlu(X3!C{w zb4$D%kf5Q;W}b8l=-VIA%KsN`mY~`diNq(Tp*JB+osohJieAT=;Y4-?Sd5UIg;^u# zFKIDPwixZ7uLL{CH5X2IT#QMvS8USs2osA~S7Yn8*n|Dbi#sj=q0pQOlQUrLGwJ#XU};NoVhP#ovNwb`>qS(C1L z5wow{9Nr0O;jw|ySuDv)l- zu>esZG$?)lL2%FiPxL8E8H!7QmP2V7{B#2z?nQ~mc|<$Wj;`)khE8S(oM}}6&k!wX zHr4eJ&Y6H&2jE-;1^_3K-alJfT6%iq?FYVY!Rs9Y{!9De=31^b;oeZ)iy$_!lFdGY zMvgj@j}=&;ehi@F0A=Mh6J(5nH34j3A0`Oc>&2mPb--``o=8|-rWk*^-$@>B1RT}^ zgLqMr!!ioi1=x$z+^Amfy|oo8VP!-%RW1GufERDVK&B0bB6Yt250?+%Izbg^k1YJK z^~(1c+yF^aN(obAhW^aF6#(pRuw@s!DGdOB@|Y)p`vLz6hp_w0Mu#sx8-vCJoFd`5 zcC7jipDvVK0E_-_s5#p>c~^%p!OF#N5?!8ADA1gw@d?0wv`nY@RKfrVwg{+XYR>vn z4y(F+?n;zzBo81|P;}^LTIPT9&98|m@gr78#3HW9ELYR~`AAqL;s7OD1_%!IGvjZM zWf#59&9#REDS%>@YrR)$0dD_(rJMX|Y40Vj+m}`I}q0qam zPwj~=15W{X=Lph{=ni1yAOkWUu)qKdB#S{;AcxVcfi#ceMVC4%y_UvP0enhe=8$|F zk|==aK)om#{$IhW!@*I3A*b-Es?lUP*y?#<0Eq;;hyP_UGgu3xud0OgKwrgrUB;;{ zl)u`4kHO}V{@%iF8V%)DD{263OEetD{~f&AJy905n^kCzMDPY_W%d3=u}IKxD6}Q? zrZ$K4H(I8~e{z`_0C5@uSRKy#BhS5824PF)gC~+aMzahGPsz$gk>o6z;sE!23(oS{ z^w=kpaD2?t7L5nC0B&aM^wQk`oFo=3mmAlknZvwi!;}H>4TqWkINbL}AW&Ib_3j<+ XuKmrqa}fR`52A8G?R@sRTMz#St&A9) literal 0 HcmV?d00001 diff --git a/results/bpsk_ber.png b/results/bpsk_ber.png new file mode 100644 index 0000000000000000000000000000000000000000..5bd5978b1249676a63939ae5366f683ce83c1fbc GIT binary patch literal 8913 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 literal 0 HcmV?d00001 diff --git a/results/bpsk_constellation.png b/results/bpsk_constellation.png new file mode 100644 index 0000000000000000000000000000000000000000..2a69cf6aea8af2d1a925ff66870632aed306e927 GIT binary patch literal 7508 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 aQ3BGdRL3q=sY+E^=m^rgLPSA9 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 literal 0 HcmV?d00001 diff --git a/src/demodulation.py b/src/demodulation.py new file mode 100644 index 0000000..08c69ab --- /dev/null +++ b/src/demodulation.py @@ -0,0 +1,101 @@ +""" +Digital demodulation helpers for the lab exercises. +""" + +import numpy as np + + +def bpsk_demodulate(symbols): + """ + Recover BPSK bits using a zero-threshold decision on the real axis. + """ + symbols = np.asarray(symbols, dtype=np.complex128) + return (np.real(symbols) <= 0).astype(int) + + +def qpsk_demodulate(symbols): + """ + Recover QPSK bits using minimum Euclidean distance detection. + """ + 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): + """ + Recover 16-QAM bits using Gray-coded I/Q slicing. + """ + 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, qam16_modulate, qpsk_modulate + from utils import add_awgn, calculate_ber + + print("=" * 50) + print("Demodulation test") + print("=" * 50) + + 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) + + +if __name__ == "__main__": + test_demodulation() diff --git a/src/modulation.py b/src/modulation.py new file mode 100644 index 0000000..77a84e3 --- /dev/null +++ b/src/modulation.py @@ -0,0 +1,104 @@ +""" +Digital modulation helpers for the lab exercises. +""" + +import numpy as np + +from utils import plot_constellation + + +def bpsk_modulate(bits): + """ + Map binary bits to BPSK symbols. + + 0 -> +1 + 1 -> -1 + """ + bits = np.asarray(bits, dtype=int) + symbols = (1 - 2 * bits).astype(np.complex128) + return symbols + + +def qpsk_modulate(bits): + """ + 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 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): + """ + 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 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, + } + + 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("Digital modulation test") + print("=" * 50) + + 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("Finished. Check the plots in the results directory.") + print("=" * 50) + + +if __name__ == "__main__": + test_modulation() diff --git a/src/performance_test.py b/src/performance_test.py new file mode 100644 index 0000000..2929c1d --- /dev/null +++ b/src/performance_test.py @@ -0,0 +1,155 @@ +""" +BER performance analysis for the modulation and demodulation chain. +""" + +import os + +import numpy as np + +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 _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): + """ + Measure BER for a modulation scheme over a range of SNR values. + """ + if snr_range is None: + snr_range = np.arange(0, 16, 2) + + if modulation_scheme == "BPSK": + modulate_func = bpsk_modulate + demodulate_func = bpsk_demodulate + elif modulation_scheme == "QPSK": + modulate_func = qpsk_modulate + demodulate_func = qpsk_demodulate + elif modulation_scheme == "16QAM": + modulate_func = qam16_modulate + demodulate_func = qam16_demodulate + else: + 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: + bits_tx = generate_random_bits(num_bits) + 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 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(): + """ + Compare BER performance of BPSK, QPSK and 16-QAM. + """ + print("=" * 50) + print("Digital modulation BER comparison") + print("=" * 50) + + snr_range = np.arange(0, 16, 2) + + 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(): + compare_modulations() + + +if __name__ == "__main__": + main() diff --git a/src/test_environment.py b/src/test_environment.py new file mode 100644 index 0000000..8e3892d --- /dev/null +++ b/src/test_environment.py @@ -0,0 +1,146 @@ +""" +环境测试脚本 +验证Python环境和依赖包是否正确安装 +""" + +import sys + + +def test_python_version(): + """测试Python版本""" + version = sys.version_info + print(f"Python版本: {version.major}.{version.minor}.{version.micro}") + + if version.major < 3 or (version.major == 3 and version.minor < 8): + print("❌ Python版本过低,需要3.8或更高版本") + return False + else: + print("✅ Python版本符合要求") + return True + + +def test_packages(): + """测试必需的包""" + required_packages = { + 'numpy': '1.21.0', + 'scipy': '1.7.0', + 'matplotlib': '3.4.0', + 'pytest': '7.0.0' + } + + all_ok = True + + for package, min_version in required_packages.items(): + try: + if package == 'numpy': + import numpy as np + version = np.__version__ + print(f"✅ NumPy {version} 已安装") + + elif package == 'scipy': + import scipy + version = scipy.__version__ + print(f"✅ SciPy {version} 已安装") + + elif package == 'matplotlib': + import matplotlib + version = matplotlib.__version__ + print(f"✅ Matplotlib {version} 已安装") + + elif package == 'pytest': + import pytest + version = pytest.__version__ + print(f"✅ Pytest {version} 已安装") + + except ImportError: + print(f"❌ {package} 未安装,请运行: pip install {package}") + all_ok = False + + return all_ok + + +def test_numpy_operations(): + """测试NumPy基本操作""" + try: + import numpy as np + + # 测试数组创建 + arr = np.array([1, 2, 3, 4]) + + # 测试复数运算 + complex_arr = np.array([1+1j, -1+1j, -1-1j, 1-1j]) + + # 测试数学运算 + result = np.abs(complex_arr) + + print("✅ NumPy基本操作测试通过") + return True + + except Exception as e: + print(f"❌ NumPy操作测试失败: {e}") + return False + + +def test_matplotlib(): + """测试Matplotlib绘图功能""" + try: + import matplotlib.pyplot as plt + import numpy as np + import os + + # 创建测试目录 + os.makedirs('results', exist_ok=True) + + # 创建简单图表 + plt.figure(figsize=(6, 6)) + x = np.array([1, -1, -1, 1, 1]) + y = np.array([1, 1, -1, -1, 1]) + plt.plot(x, y, 'b-') + plt.grid(True) + plt.savefig('results/test_plot.png') + plt.close() + + print("✅ Matplotlib绘图测试通过") + return True + + except Exception as e: + print(f"❌ Matplotlib测试失败: {e}") + return False + + +def main(): + """主测试函数""" + print("=" * 50) + print("数字调制解调实验 - 环境测试") + print("=" * 50) + print() + + results = [] + + print("1. 检查Python版本...") + results.append(test_python_version()) + print() + + print("2. 检查依赖包...") + results.append(test_packages()) + print() + + print("3. 测试NumPy操作...") + results.append(test_numpy_operations()) + print() + + print("4. 测试Matplotlib绘图...") + results.append(test_matplotlib()) + print() + + print("=" * 50) + if all(results): + print("🎉 所有测试通过!环境配置正确。") + print("你可以开始实验了!") + else: + print("⚠️ 部分测试未通过,请检查并修复环境问题。") + print("=" * 50) + + +if __name__ == "__main__": + main() diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..1a44661 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,171 @@ +""" +Utility helpers for plotting and simple communication experiments. +""" + +import os + +import numpy as np +from PIL import Image, ImageDraw + + +def setup_chinese_font(): + """ + Configure Matplotlib fonts so Chinese labels render when available. + """ + try: + 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("Warning: unable to configure Chinese fonts for Matplotlib.") + + +def plot_constellation(symbols, title, filename, show_grid=True): + """ + Plot a constellation diagram and save it into the results directory. + """ + os.makedirs("results", exist_ok=True) + + symbols = np.asarray(symbols) + real_parts = np.real(symbols) + imag_parts = np.imag(symbols) + 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: + 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): + """ + 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): + """ + 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("Transmitted and received bit sequences must have the same length.") + + errors = np.sum(bits_tx != bits_rx) + return errors / len(bits_tx) + + +def plot_ber_curve(snr_range, ber_values, title="BER vs SNR", filename="ber_curve.png"): + """ + Plot and save a BER curve. + """ + 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): + """ + Generate a random binary sequence of length n. + """ + return np.random.randint(0, 2, n) + + +if __name__ == "__main__": + print("Utility module self-test...") + bits = generate_random_bits(100) + 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") From 037748a7397a3f47b1d2a6c822d29229528aa09d Mon Sep 17 00:00:00 2001 From: tanyuling226 <1375639893@qq.com> Date: Thu, 23 Apr 2026 21:35:24 +0800 Subject: [PATCH 2/2] =?UTF-8?q?2023280502=E8=B0=AD=E6=A6=86=E9=93=83=20?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E5=AE=9E=E9=AA=8C=E6=8A=A5=E5=91=8A=E4=B8=8E?= =?UTF-8?q?=E5=AE=9E=E9=AA=8C=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- REPORT.md | 272 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 REPORT.md 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日