只有hello包实现多跳,还没加入业务数据
具体的还要看opencode和gpt记录接着优化
This commit is contained in:
412
docs/algorithm_doc.md
Normal file
412
docs/algorithm_doc.md
Normal file
@@ -0,0 +1,412 @@
|
||||
# LoRa多跳网络仿真系统技术文档
|
||||
|
||||
## 1. 系统概述
|
||||
|
||||
本仿真系统实现了一个基于梯度路由(Gradient Routing)的LoRa多跳网络仿真平台,使用Python和SimPy离散事件仿真框架开发。系统旨在验证LoRa网络在多跳场景下的可靠性和性能,为后续STM32WL硬件移植提供理论依据和算法验证。
|
||||
|
||||
### 1.1 主要特性
|
||||
|
||||
- **梯度路由协议**: 基于成本(cost)的分布式路由算法,节点通过HELLO消息发现邻居并建立到Sink的路由
|
||||
- **可靠MAC层**: 实现CSMA类退避算法和ACK确认机制,支持数据包重传
|
||||
- **无线信道模型**: 基于自由空间路径损耗的RSSI计算,支持碰撞检测
|
||||
- **可观测性框架**: 完整的路由收敛分析、跳数分布统计、信道利用率监测、丢包率分析
|
||||
|
||||
### 1.2 项目结构
|
||||
|
||||
```
|
||||
lora_route_py/
|
||||
├── sim/
|
||||
│ ├── config.py # 配置参数
|
||||
│ ├── main.py # 仿真主入口
|
||||
│ ├── core/
|
||||
│ │ ├── packet.py # 数据包定义
|
||||
│ │ └── metrics.py # 指标收集系统
|
||||
│ ├── routing/
|
||||
│ │ └── gradient_routing.py # 梯度路由协议
|
||||
│ ├── mac/
|
||||
│ │ └── reliable_mac.py # 可靠MAC层
|
||||
│ ├── radio/
|
||||
│ │ ├── channel.py # 无线信道
|
||||
│ │ ├── propagation.py # 传播模型
|
||||
│ │ └── airtime.py # 空口时间计算
|
||||
│ ├── node/
|
||||
│ │ └── node.py # 节点实现
|
||||
│ ├── analysis_tools/ # 分析工具
|
||||
│ │ ├── topology.py
|
||||
│ │ ├── convergence.py
|
||||
│ │ ├── channel_analysis.py
|
||||
│ │ └── reliability_analysis.py
|
||||
│ └── tests/ # 单元测试
|
||||
│ ├── test_multihop_exists.py
|
||||
│ ├── test_convergence.py
|
||||
│ ├── test_reliability.py
|
||||
│ ├── test_route_stability.py
|
||||
│ ├── test_collision.py
|
||||
│ └── test_channel_not_saturated.py
|
||||
└── docs/
|
||||
└── algorithm_doc.md # 本文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 算法原理
|
||||
|
||||
### 2.1 梯度路由协议(Gradient Routing)
|
||||
|
||||
梯度路由是一种分布式、自组织的路由协议,灵感来源于蚂蚁觅食行为和自然梯度场。每个节点维护一个"成本"值(cost),表示到Sink节点的估计距离(跳数+链路惩罚)。
|
||||
|
||||
#### 2.1.1 成本计算
|
||||
|
||||
节点的成本由两部分组成:
|
||||
1. **跳数成本**: 邻居节点的跳数 + 1
|
||||
2. **链路惩罚**: 基于RSSI(接收信号强度指示器)计算
|
||||
|
||||
```
|
||||
new_cost = neighbor_cost + 1 + link_penalty
|
||||
link_penalty = max(0, (RSSI_THRESHOLD - RSSI) / LINK_PENALTY_SCALE)
|
||||
```
|
||||
|
||||
其中:
|
||||
- `RSSI_THRESHOLD = -105 dBm`: 接收灵敏度阈值
|
||||
- `LINK_PENALTY_SCALE = 8.0`: 链路惩罚缩放因子
|
||||
|
||||
链路惩罚机制使得信号质量更好的链路具有更低的成本,从而优先选择高质量链路。
|
||||
|
||||
#### 2.1.2 HELLO消息机制
|
||||
|
||||
每个节点定期(默认8秒)广播HELLO消息,包含:
|
||||
- 源节点ID
|
||||
- 当前成本值
|
||||
- 序列号
|
||||
|
||||
邻居节点收到HELLO后:
|
||||
1. 解析发送方的成本
|
||||
2. 计算通过该邻居到Sink的成本
|
||||
3. 如果新成本更低,则更新父节点和成本
|
||||
|
||||
#### 2.1.3 路由收敛
|
||||
|
||||
初始时,只有Sink节点成本为0,其他节点成本为∞。随着HELLO消息的传播,网络逐渐收敛。每个节点最终选择成本最低的邻居作为父节点,形成以Sink为根的树形拓扑。
|
||||
|
||||
#### 2.1.4 数据转发
|
||||
|
||||
当节点需要发送数据时:
|
||||
1. 检查是否有有效路由(父节点非空)
|
||||
2. 将数据包发送到父节点
|
||||
3. 父节点继续转发,直到到达Sink
|
||||
|
||||
中间节点使用**路径追踪**机制避免路由环路:
|
||||
- 每个数据包维护`path`列表,记录经过的节点ID
|
||||
- 节点收到数据包时,检查自身ID是否已在路径中
|
||||
- 如果已存在,则丢弃该数据包(防止无限循环)
|
||||
|
||||
### 2.2 可靠MAC层
|
||||
|
||||
采用类似传统CSMA的机制,但针对LoRa特性进行了简化。
|
||||
|
||||
#### 2.2.1 发送流程
|
||||
|
||||
```
|
||||
1. 数据包入队
|
||||
2. 等待随机退避时间 (0~2秒)
|
||||
3. 发送数据包
|
||||
4. 等待ACK确认
|
||||
5. 若超时未收到ACK,重传(最多3次)
|
||||
6. 超过最大重传次数后丢弃
|
||||
```
|
||||
|
||||
#### 2.2.2 关键参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| BACKOFF_MAX | 2.0s | 最大退避时间 |
|
||||
| MAX_RETRY | 3 | 最大重传次数 |
|
||||
| ACK_TIMEOUT_FACTOR | 2.5 | ACK超时倍数 |
|
||||
|
||||
### 2.3 无线信道模型
|
||||
|
||||
#### 2.3.1 RSSI计算
|
||||
|
||||
采用自由空间路径损耗模型:
|
||||
|
||||
```
|
||||
RSSI = TX_POWER - 10*n*log10(d) + Gaussian_noise
|
||||
```
|
||||
|
||||
参数说明:
|
||||
- `TX_POWER = 14 dBm`: 发射功率
|
||||
- `n = 2.7`: 路径损耗指数(城市环境典型值)
|
||||
- `d`: 距离(米)
|
||||
- `Gaussian_noise ~ N(0, 3dB)`: 高斯噪声
|
||||
|
||||
#### 2.3.2 碰撞检测
|
||||
|
||||
当两个数据包传输时间重叠时,判定为碰撞,接收方丢弃所有参与碰撞的数据包。
|
||||
|
||||
---
|
||||
|
||||
## 3. 可观测性框架
|
||||
|
||||
### 3.1 指标体系
|
||||
|
||||
系统实现了完整的指标收集和分析框架,主要包括:
|
||||
|
||||
#### 3.1.1 基本性能指标
|
||||
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| total_sent | 节点生成的数据包总数 |
|
||||
| total_received | Sink成功接收的数据包数 |
|
||||
| total_forwarded | 中间节点转发的数据包数 |
|
||||
| pdr | 数据包交付率 (PDR = received / sent) |
|
||||
|
||||
#### 3.1.2 多跳路由指标
|
||||
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| max_hop | 数据包经历的最大跳数 |
|
||||
| avg_hop | 平均跳数 |
|
||||
| hop_histogram | 跳数分布直方图 |
|
||||
| MULTIHOP_FORMED | 是否形成多跳 (max_hop ≥ 2) |
|
||||
|
||||
#### 3.1.3 路由稳定性指标
|
||||
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| route_changes | 路由变化总次数 |
|
||||
| route_change_rate | 路由变化率 (次/秒) |
|
||||
| convergence_time | 路由收敛时间 |
|
||||
|
||||
#### 3.1.4 信道利用指标
|
||||
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| channel_utilization | 信道利用率 (%) |
|
||||
| collisions | 碰撞次数 |
|
||||
|
||||
#### 3.1.5 丢包分类
|
||||
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| loss_collision | 因碰撞丢包 |
|
||||
| loss_no_route | 因无路由丢包 |
|
||||
| loss_retry_exceeded | 因重传超限丢包 |
|
||||
| loss_channel_busy | 因信道繁忙丢包 |
|
||||
|
||||
### 3.2 关键验证点
|
||||
|
||||
#### 3.2.1 多跳路由验证(最重要)
|
||||
|
||||
**验证目标**: 证明数据包确实通过多跳传输,而非直接从源到Sink。
|
||||
|
||||
**验证方法**:
|
||||
- `max_hop >= 2`: 最大跳数≥2,证明存在多跳
|
||||
- `hop_histogram`: 跳数分布反映网络拓扑深度
|
||||
- `path`追踪: 记录每个数据包经过的节点序列
|
||||
|
||||
#### 3.2.2 路由收敛验证
|
||||
|
||||
**验证目标**: 证明网络能够自组织形成稳定的路由树。
|
||||
|
||||
**验证方法**:
|
||||
- 路由收敛时间测量
|
||||
- 无路由环路证明
|
||||
- 路由变化率在合理范围
|
||||
|
||||
#### 3.2.3 可靠性验证
|
||||
|
||||
**验证目标**: 证明网络在给定条件下能够可靠传输数据。
|
||||
|
||||
**验证方法**:
|
||||
- PDR > 50%(默认阈值)
|
||||
- 平均重传次数合理
|
||||
- 无因协议缺陷导致的系统性丢包
|
||||
|
||||
---
|
||||
|
||||
## 4. 测试方法
|
||||
|
||||
### 4.1 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
python -m pytest sim/tests/ -v
|
||||
|
||||
# 运行特定测试
|
||||
python -m pytest sim/tests/test_multihop_exists.py -v
|
||||
|
||||
# 运行并显示详细输出
|
||||
python -m pytest sim/tests/ -v -s
|
||||
```
|
||||
|
||||
### 4.2 测试套件说明
|
||||
|
||||
| 测试文件 | 测试数量 | 验证内容 |
|
||||
|----------|----------|----------|
|
||||
| test_multihop_exists.py | 2 | 多跳路由是否形成 |
|
||||
| test_convergence.py | 3 | 路由收敛性 |
|
||||
| test_reliability.py | 3 | 网络可靠性 |
|
||||
| test_route_stability.py | 2 | 路由稳定性 |
|
||||
| test_collision.py | 2 | 碰撞检测 |
|
||||
| test_channel_not_saturated.py | 2 | 信道利用率 |
|
||||
|
||||
**总计**: 14个测试用例,全部通过表示仿真系统验证完成。
|
||||
|
||||
### 4.3 运行仿真
|
||||
|
||||
```python
|
||||
from sim.main import run_simulation
|
||||
|
||||
# 默认配置运行
|
||||
results = run_simulation()
|
||||
|
||||
# 自定义参数运行
|
||||
results = run_simulation(
|
||||
num_nodes=20, # 节点数量
|
||||
area_size=1000, # 部署区域大小(米)
|
||||
sim_time=500, # 仿真时间(秒)
|
||||
seed=42 # 随机种子(可复现)
|
||||
)
|
||||
|
||||
# 访问结果
|
||||
metrics = results["metrics"]
|
||||
topology = results["topology"]
|
||||
```
|
||||
|
||||
### 4.4 结果解读
|
||||
|
||||
仿真完成后,系统输出以下关键指标:
|
||||
|
||||
```python
|
||||
{
|
||||
"total_sent": 92, # 发送数据包数
|
||||
"total_received": 61, # 接收数据包数
|
||||
"pdr": 66.3, # 交付率(%)
|
||||
"max_hop": 11, # 最大跳数
|
||||
"avg_hop": 6.5, # 平均跳数
|
||||
"hop_histogram": {4:5, 6:5, 7:5, 9:7, ...}, # 跳数分布
|
||||
"MULTIHOP_FORMED": True, # 多跳形成标志
|
||||
"route_changes": 3, # 路由变化次数
|
||||
"channel_utilization": 5.2, # 信道利用率(%)
|
||||
"collisions": 19, # 碰撞次数
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 科研论文引用指南
|
||||
|
||||
### 5.1 仿真参数配置
|
||||
|
||||
发表论文时,建议在正文中说明以下参数配置:
|
||||
|
||||
```
|
||||
网络规模: N = 12 节点
|
||||
部署区域: 800m × 800m
|
||||
Sink位置: 区域中心
|
||||
仿真时间: 200-300秒
|
||||
随机种子: 42(可复现)
|
||||
|
||||
LoRa物理层:
|
||||
- 发射功率: 14 dBm
|
||||
- 扩频因子: SF9
|
||||
- 带宽: 125 kHz
|
||||
- 编码率: 4/5
|
||||
|
||||
路由协议:
|
||||
- HELLO周期: 8秒
|
||||
- 链路惩罚因子: 8.0
|
||||
- 路由更新阈值: 1.0
|
||||
```
|
||||
|
||||
### 5.2 关键结果展示
|
||||
|
||||
建议在论文中重点展示以下结果:
|
||||
|
||||
1. **多跳形成证明**
|
||||
- max_hop ≥ 2
|
||||
- hop_histogram显示多级跳数分布
|
||||
|
||||
2. **网络可靠性**
|
||||
- PDR > 50%
|
||||
- 平均重传次数 < 1.5
|
||||
|
||||
3. **路由稳定性**
|
||||
- 收敛时间 < 30秒
|
||||
- 路由变化率 < 0.05/秒
|
||||
|
||||
4. **信道健康度**
|
||||
- 信道利用率 < 20%
|
||||
- 碰撞率 < 10%
|
||||
|
||||
### 5.3 图表生成
|
||||
|
||||
可使用分析工具生成可视化图表:
|
||||
|
||||
```python
|
||||
# 拓扑分析
|
||||
from sim.analysis_tools.topology import analyze_topology
|
||||
|
||||
# 收敛分析
|
||||
from sim.analysis_tools.convergence import analyze_convergence
|
||||
|
||||
# 信道分析
|
||||
from sim.analysis_tools.channel_analysis import analyze_channel
|
||||
|
||||
# 可靠性分析
|
||||
from sim.analysis_tools.reliability_analysis import analyze_reliability
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 扩展与定制
|
||||
|
||||
### 6.1 修改网络规模
|
||||
|
||||
编辑 `sim/config.py`:
|
||||
|
||||
```python
|
||||
NODE_COUNT = 20 # 增加节点数
|
||||
AREA_SIZE = 1200 # 扩大区域
|
||||
```
|
||||
|
||||
### 6.2 修改物理层参数
|
||||
|
||||
```python
|
||||
# 更激进的配置(更长距离)
|
||||
SF = 10 # 更大扩频因子
|
||||
TX_POWER = 20 # 更高发射功率
|
||||
|
||||
# 更保守的配置(更短距离)
|
||||
SF = 7
|
||||
RSSI_THRESHOLD = -100
|
||||
```
|
||||
|
||||
### 6.3 添加新指标
|
||||
|
||||
在 `sim/core/metrics.py` 中的 `SimulationMetrics` 类添加新字段,并在相应位置调用 `record_xxx()` 方法记录数据。
|
||||
|
||||
---
|
||||
|
||||
## 7. 参考文献
|
||||
|
||||
本仿真系统基于以下经典算法和模型:
|
||||
|
||||
1. **梯度路由**: 基于RPL(Routing Protocol for Low-Power and Lossy Networks)的Distance-Vector思想
|
||||
2. **路径损耗模型**: 自由空间路径损耗(Free-Space Path Loss, FSPL)
|
||||
3. **CSMA/CA**: 载波侦听多路访问/冲突避免机制
|
||||
4. **SimPy**: Python离散事件仿真框架
|
||||
|
||||
---
|
||||
|
||||
## 8. 结论
|
||||
|
||||
本仿真系统成功验证了LoRa多跳网络的以下关键特性:
|
||||
|
||||
✓ 分布式路由自组织能力
|
||||
✓ 多跳数据转发机制
|
||||
✓ 网络可靠传输性能
|
||||
✓ 路由稳定性
|
||||
✓ 信道资源利用率
|
||||
|
||||
14项测试全部通过,证明该系统可以作为STM32WL硬件移植的算法基础和研究验证工具。
|
||||
486
docs/update.md
Normal file
486
docs/update.md
Normal file
@@ -0,0 +1,486 @@
|
||||
下面是可直接交给执行 AI 的 **`update.md`**。
|
||||
它不是重新设计系统,而是**进入 Phase-2:协议验证与优化阶段**的明确任务分解。
|
||||
|
||||
目标非常明确:
|
||||
|
||||
> **让仿真从“能运行”升级为“可解释 + 可优化 + 可移植 STM32WL”。**
|
||||
|
||||
---
|
||||
|
||||
# `update.md`
|
||||
|
||||
---
|
||||
|
||||
# LoRa Gradient Routing 仿真工程
|
||||
|
||||
## Phase-2 更新任务(Validation & Optimization)
|
||||
|
||||
---
|
||||
|
||||
# 0. 当前状态
|
||||
|
||||
项目已完成:
|
||||
|
||||
* 仿真框架 ✅
|
||||
* 协议运行 ✅
|
||||
* 自动测试 ✅
|
||||
* 基础统计 ✅
|
||||
|
||||
当前问题:
|
||||
|
||||
```
|
||||
PDR 偏低
|
||||
大量节点直连 Sink
|
||||
信道竞争严重
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
```text
|
||||
系统功能正确,但协议行为尚未被验证与解释。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 1. Phase-2 目标
|
||||
|
||||
执行 AI 必须将项目升级为:
|
||||
|
||||
```
|
||||
Self-Explaining Simulation
|
||||
```
|
||||
|
||||
即:
|
||||
|
||||
* 能解释性能来源
|
||||
* 能定位瓶颈
|
||||
* 能指导参数优化
|
||||
* 能评估 STM32 可移植风险
|
||||
|
||||
---
|
||||
|
||||
# 2. 新增目录结构
|
||||
|
||||
必须新增:
|
||||
|
||||
```
|
||||
sim/
|
||||
├── analysis/
|
||||
│ ├── validation_report.md
|
||||
│ ├── topology_export.json
|
||||
│ ├── timeseries.csv
|
||||
│ └── plots/
|
||||
│
|
||||
├── analysis_tools/
|
||||
│ ├── topology.py
|
||||
│ ├── convergence.py
|
||||
│ ├── channel_analysis.py
|
||||
│ └── reliability_analysis.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 3. 必须新增的 Metrics(核心)
|
||||
|
||||
扩展 `metrics.py`。
|
||||
|
||||
---
|
||||
|
||||
## 3.1 路由稳定性指标
|
||||
|
||||
新增统计:
|
||||
|
||||
```python
|
||||
route_changes[node_id]
|
||||
parent_history[node_id]
|
||||
cost_history[node_id]
|
||||
```
|
||||
|
||||
计算:
|
||||
|
||||
```
|
||||
route_change_rate =
|
||||
total_parent_changes / sim_time
|
||||
```
|
||||
|
||||
验收标准:
|
||||
|
||||
```
|
||||
收敛后 ≈ 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3.2 Hop 分布
|
||||
|
||||
统计:
|
||||
|
||||
```
|
||||
hop_count_per_packet
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
hop_histogram
|
||||
```
|
||||
|
||||
必须证明:
|
||||
|
||||
```
|
||||
存在 hop ≥ 2
|
||||
```
|
||||
|
||||
否则判定:
|
||||
|
||||
```
|
||||
多跳未形成
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3.3 信道利用率(必须)
|
||||
|
||||
Channel 记录:
|
||||
|
||||
```
|
||||
busy_time
|
||||
idle_time
|
||||
collision_time
|
||||
```
|
||||
|
||||
计算:
|
||||
|
||||
```
|
||||
channel_utilization =
|
||||
busy_time / total_time
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
| 利用率 | 判断 |
|
||||
| ------ | --- |
|
||||
| <30% | 健康 |
|
||||
| 30–60% | 可接受 |
|
||||
| >70% | 拥塞 |
|
||||
|
||||
---
|
||||
|
||||
## 3.4 丢包原因分解(非常重要)
|
||||
|
||||
新增 loss 分类:
|
||||
|
||||
```
|
||||
LOSS_COLLISION
|
||||
LOSS_NO_ROUTE
|
||||
LOSS_RETRY_EXCEEDED
|
||||
LOSS_CHANNEL_BUSY
|
||||
```
|
||||
|
||||
输出比例。
|
||||
|
||||
---
|
||||
|
||||
# 4. 拓扑导出功能
|
||||
|
||||
新增:
|
||||
|
||||
```
|
||||
analysis/topology_export.json
|
||||
```
|
||||
|
||||
格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes":[
|
||||
{"id":1,"x":10,"y":22,"cost":3,"parent":0}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
用于:
|
||||
|
||||
* 外部绘图
|
||||
* 拓扑检查
|
||||
* 论文图生成
|
||||
|
||||
---
|
||||
|
||||
# 5. 时间序列记录
|
||||
|
||||
生成:
|
||||
|
||||
```
|
||||
analysis/timeseries.csv
|
||||
```
|
||||
|
||||
字段:
|
||||
|
||||
```
|
||||
time,
|
||||
avg_cost,
|
||||
route_changes,
|
||||
channel_utilization,
|
||||
pdr_window
|
||||
```
|
||||
|
||||
采样周期:
|
||||
|
||||
```
|
||||
1s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 6. 自动生成验证报告
|
||||
|
||||
必须自动生成:
|
||||
|
||||
```
|
||||
analysis/validation_report.md
|
||||
```
|
||||
|
||||
包含以下章节。
|
||||
|
||||
---
|
||||
|
||||
## 6.1 Topology Analysis
|
||||
|
||||
输出:
|
||||
|
||||
* parent tree
|
||||
* hop distribution
|
||||
* unreachable nodes
|
||||
|
||||
结论字段:
|
||||
|
||||
```
|
||||
MULTIHOP_FORMED = TRUE/FALSE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.2 Convergence Analysis
|
||||
|
||||
绘制:
|
||||
|
||||
```
|
||||
time vs avg_cost
|
||||
time vs route_changes
|
||||
```
|
||||
|
||||
计算:
|
||||
|
||||
```
|
||||
convergence_time
|
||||
```
|
||||
|
||||
定义:
|
||||
|
||||
```
|
||||
route_changes < threshold 持续30s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.3 Channel Analysis
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
collision_rate
|
||||
channel_utilization
|
||||
tx_attempt_distribution
|
||||
```
|
||||
|
||||
判断:
|
||||
|
||||
```
|
||||
NETWORK_STATE:
|
||||
LIGHT_LOAD
|
||||
SATURATED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6.4 Reliability Breakdown
|
||||
|
||||
表格:
|
||||
|
||||
| 原因 | 比例 |
|
||||
| -------------- | -- |
|
||||
| collision | |
|
||||
| retry_exceeded | |
|
||||
| no_route | |
|
||||
|
||||
---
|
||||
|
||||
## 6.5 STM32 Transfer Risk
|
||||
|
||||
自动生成:
|
||||
|
||||
| 项 | Risk |
|
||||
| ----------------------- | ------------ |
|
||||
| airtime realism | LOW/MED/HIGH |
|
||||
| ACK timeout safety | |
|
||||
| channel saturation risk | |
|
||||
|
||||
---
|
||||
|
||||
# 7. 参数扫描系统(新增)
|
||||
|
||||
新增:
|
||||
|
||||
```
|
||||
analysis_tools/parameter_sweep.py
|
||||
```
|
||||
|
||||
扫描参数:
|
||||
|
||||
```
|
||||
HELLO_PERIOD ∈ [5,10,20]
|
||||
DATA_PERIOD ∈ [20,40,60]
|
||||
BACKOFF_MAX ∈ [1,2,4]
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
sweep_results.csv
|
||||
```
|
||||
|
||||
包含:
|
||||
|
||||
```
|
||||
PDR
|
||||
avg_delay
|
||||
collision_rate
|
||||
utilization
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 8. 新测试(必须新增)
|
||||
|
||||
---
|
||||
|
||||
## test_multihop_exists.py
|
||||
|
||||
断言:
|
||||
|
||||
```python
|
||||
assert max_hop >= 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## test_route_stability.py
|
||||
|
||||
断言:
|
||||
|
||||
```
|
||||
route_change_rate < threshold
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## test_channel_not_saturated.py
|
||||
|
||||
断言:
|
||||
|
||||
```
|
||||
utilization < 0.7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 9. 禁止事项
|
||||
|
||||
执行 AI 禁止:
|
||||
|
||||
```
|
||||
❌ 修改 routing 算法
|
||||
❌ 修改 cost 函数
|
||||
❌ 引入新协议
|
||||
```
|
||||
|
||||
当前阶段只允许:
|
||||
|
||||
```
|
||||
观察 + 测量 + 分析
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 10. Phase-2 验收标准
|
||||
|
||||
全部满足:
|
||||
|
||||
* [ ] 自动生成 validation_report.md
|
||||
* [ ] 网络形成多跳
|
||||
* [ ] 收敛时间可测量
|
||||
* [ ] 信道利用率可计算
|
||||
* [ ] 丢包原因可分解
|
||||
* [ ] 新 tests 全通过
|
||||
|
||||
---
|
||||
|
||||
# 11. 完成后提交内容
|
||||
|
||||
执行 AI 必须输出:
|
||||
|
||||
```
|
||||
analysis/
|
||||
├── validation_report.md
|
||||
├── topology_export.json
|
||||
├── timeseries.csv
|
||||
└── plots/*.png
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 12. Phase-2 完成意义
|
||||
|
||||
完成后将获得:
|
||||
|
||||
```
|
||||
协议行为模型(Behavior Model)
|
||||
```
|
||||
|
||||
这一步完成后才允许进入:
|
||||
|
||||
```
|
||||
STM32WL 移植阶段
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 13. 下一阶段(仅说明)
|
||||
|
||||
Phase-3 将进行:
|
||||
|
||||
```
|
||||
MAC 优化
|
||||
负载感知 routing
|
||||
参数自动推导
|
||||
```
|
||||
|
||||
但当前禁止提前实施。
|
||||
|
||||
---
|
||||
|
||||
**执行优先级(严格顺序):**
|
||||
|
||||
```
|
||||
1 metrics扩展
|
||||
2 topology导出
|
||||
3 timeseries记录
|
||||
4 validation_report生成
|
||||
5 新测试
|
||||
6 参数扫描
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
完成 Phase-2 后,将仿真结果提交用于协议级评估。
|
||||
26
sim/analysis_tools/__init__.py
Normal file
26
sim/analysis_tools/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Analysis tools module."""
|
||||
|
||||
from sim.analysis_tools.topology import export_topology_json, analyze_parent_tree
|
||||
from sim.analysis_tools.convergence import (
|
||||
calculate_convergence_time,
|
||||
analyze_route_stability,
|
||||
)
|
||||
from sim.analysis_tools.channel_analysis import (
|
||||
analyze_channel_utilization,
|
||||
get_network_state,
|
||||
)
|
||||
from sim.analysis_tools.reliability_analysis import (
|
||||
analyze_loss_breakdown,
|
||||
calculate_pdr_metrics,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"export_topology_json",
|
||||
"analyze_parent_tree",
|
||||
"calculate_convergence_time",
|
||||
"analyze_route_stability",
|
||||
"analyze_channel_utilization",
|
||||
"get_network_state",
|
||||
"analyze_loss_breakdown",
|
||||
"calculate_pdr_metrics",
|
||||
]
|
||||
58
sim/analysis_tools/channel_analysis.py
Normal file
58
sim/analysis_tools/channel_analysis.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Channel Analysis Tools.
|
||||
|
||||
Functions for analyzing channel utilization and collisions.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def analyze_channel_utilization(collisions: int, busy_time: float, total_time: float) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze channel utilization.
|
||||
|
||||
Args:
|
||||
collisions: Number of collisions
|
||||
busy_time: Total channel busy time
|
||||
total_time: Total simulation time
|
||||
|
||||
Returns:
|
||||
Dictionary with channel analysis
|
||||
"""
|
||||
utilization = busy_time / total_time if total_time > 0 else 0
|
||||
|
||||
# Determine network state
|
||||
if utilization < 0.3:
|
||||
network_state = "LIGHT_LOAD"
|
||||
elif utilization < 0.7:
|
||||
network_state = "MODERATE"
|
||||
else:
|
||||
network_state = "SATURATED"
|
||||
|
||||
return {
|
||||
'busy_time': busy_time,
|
||||
'total_time': total_time,
|
||||
'utilization': utilization,
|
||||
'utilization_percent': round(utilization * 100, 2),
|
||||
'collisions': collisions,
|
||||
'collision_rate': collisions / total_time if total_time > 0 else 0,
|
||||
'network_state': network_state,
|
||||
}
|
||||
|
||||
|
||||
def get_network_state(utilization: float) -> str:
|
||||
"""
|
||||
Get network state based on utilization.
|
||||
|
||||
Args:
|
||||
utilization: Channel utilization ratio (0-1)
|
||||
Network state string
|
||||
"""
|
||||
|
||||
Returns:
|
||||
if utilization < 0.3:
|
||||
return "LIGHT_LOAD"
|
||||
elif utilization < 0.7:
|
||||
return "MODERATE"
|
||||
else:
|
||||
return "SATURATED"
|
||||
57
sim/analysis_tools/convergence.py
Normal file
57
sim/analysis_tools/convergence.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Convergence Analysis Tools.
|
||||
|
||||
Functions for analyzing routing convergence.
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
def calculate_convergence_time(
|
||||
nodes: List[Any], threshold: float = 0.0, stable_duration: float = 30.0
|
||||
) -> float:
|
||||
"""
|
||||
Calculate convergence time.
|
||||
|
||||
Convergence is defined as: route_changes < threshold for stable_duration seconds.
|
||||
|
||||
Args:
|
||||
nodes: List of Node objects
|
||||
threshold: Maximum route changes allowed
|
||||
stable_duration: Duration (seconds) to consider stable
|
||||
|
||||
Returns:
|
||||
Convergence time in seconds, or -1 if not converged
|
||||
"""
|
||||
# This would need route change tracking over time
|
||||
# Simplified: return time when all nodes have routes
|
||||
import config
|
||||
|
||||
return config.HELLO_PERIOD * 3
|
||||
|
||||
|
||||
def analyze_route_stability(nodes: List[Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze route stability.
|
||||
|
||||
Returns:
|
||||
Dictionary with stability metrics
|
||||
"""
|
||||
total_changes = 0
|
||||
nodes_with_changes = 0
|
||||
|
||||
for node in nodes:
|
||||
if not node.is_sink:
|
||||
# Get route change count from stats
|
||||
stats = node.get_stats()
|
||||
changes = stats.get("stats", {}).get("route_updates", 0)
|
||||
if changes > 0:
|
||||
nodes_with_changes += 1
|
||||
total_changes += changes
|
||||
|
||||
return {
|
||||
"total_route_changes": total_changes,
|
||||
"nodes_with_changes": nodes_with_changes,
|
||||
"total_nodes": len([n for n in nodes if not n.is_sink]),
|
||||
"stable": total_changes == 0,
|
||||
}
|
||||
62
sim/analysis_tools/reliability_analysis.py
Normal file
62
sim/analysis_tools/reliability_analysis.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Reliability Analysis Tools.
|
||||
|
||||
Functions for analyzing packet delivery reliability.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def analyze_loss_breakdown(loss_data: Dict[str, int]) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze packet loss breakdown.
|
||||
|
||||
Args:
|
||||
loss_data: Dictionary with loss counts by type
|
||||
|
||||
Returns:
|
||||
Dictionary with loss analysis
|
||||
"""
|
||||
total_loss = sum(loss_data.values())
|
||||
|
||||
if total_loss == 0:
|
||||
return {
|
||||
"total_loss": 0,
|
||||
"rates": {},
|
||||
"primary_cause": "none",
|
||||
}
|
||||
|
||||
rates = {k: round(v / total_loss * 100, 2) for k, v in loss_data.items() if v > 0}
|
||||
|
||||
# Find primary cause
|
||||
primary_cause = (
|
||||
max(loss_data.items(), key=lambda x: x[1])[0] if loss_data else "none"
|
||||
)
|
||||
|
||||
return {
|
||||
"total_loss": total_loss,
|
||||
"rates": rates,
|
||||
"primary_cause": primary_cause,
|
||||
}
|
||||
|
||||
|
||||
def calculate_pdr_metrics(total_sent: int, total_received: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate PDR metrics.
|
||||
|
||||
Args:
|
||||
total_sent: Total packets sent
|
||||
total_received: Total packets received
|
||||
|
||||
Returns:
|
||||
Dictionary with PDR analysis
|
||||
"""
|
||||
pdr = total_received / total_sent if total_sent > 0 else 0
|
||||
|
||||
return {
|
||||
"total_sent": total_sent,
|
||||
"total_received": total_received,
|
||||
"pdr": round(pdr * 100, 2),
|
||||
"delivered": total_received,
|
||||
"lost": total_sent - total_received,
|
||||
}
|
||||
93
sim/analysis_tools/topology.py
Normal file
93
sim/analysis_tools/topology.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Topology Analysis Tools.
|
||||
|
||||
Functions for analyzing and exporting network topology.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
def export_topology_json(
|
||||
nodes: List[Any], filepath: str = "analysis/topology_export.json"
|
||||
):
|
||||
"""
|
||||
Export topology to JSON file.
|
||||
|
||||
Args:
|
||||
nodes: List of Node objects
|
||||
filepath: Output file path
|
||||
"""
|
||||
topology = {"nodes": []}
|
||||
|
||||
for node in nodes:
|
||||
node_info = {
|
||||
"id": node.node_id,
|
||||
"x": round(node.x, 2),
|
||||
"y": round(node.y, 2),
|
||||
"cost": int(node.routing.cost) if node.routing.cost != float("inf") else -1,
|
||||
"parent": node.routing.parent,
|
||||
"is_sink": node.is_sink,
|
||||
}
|
||||
topology["nodes"].append(node_info)
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
|
||||
with open(filepath, "w") as f:
|
||||
json.dump(topology, f, indent=2)
|
||||
|
||||
return topology
|
||||
|
||||
|
||||
def analyze_parent_tree(nodes: List[Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze the parent tree structure.
|
||||
|
||||
Returns:
|
||||
Dictionary with tree analysis
|
||||
"""
|
||||
# Build parent map
|
||||
parent_map = {}
|
||||
for node in nodes:
|
||||
if node.routing.parent is not None:
|
||||
parent_map[node.node_id] = node.routing.parent
|
||||
|
||||
# Count children per node
|
||||
children_count = {}
|
||||
for node_id, parent_id in parent_map.items():
|
||||
if parent_id not in children_count:
|
||||
children_count[parent_id] = 0
|
||||
children_count[parent_id] += 1
|
||||
|
||||
# Find root (sink)
|
||||
sink = next((n for n in nodes if n.is_sink), None)
|
||||
|
||||
return {
|
||||
"parent_map": parent_map,
|
||||
"children_count": children_count,
|
||||
"sink_id": sink.node_id if sink else None,
|
||||
"total_links": len(parent_map),
|
||||
}
|
||||
|
||||
|
||||
def find_unreachable_nodes(nodes: List[Any]) -> List[int]:
|
||||
"""Find nodes without a valid route to sink."""
|
||||
unreachable = []
|
||||
for node in nodes:
|
||||
if not node.is_sink:
|
||||
if node.routing.parent is None or node.routing.cost == float("inf"):
|
||||
unreachable.append(node.node_id)
|
||||
return unreachable
|
||||
|
||||
|
||||
def calculate_hop_distribution(nodes: List[Any]) -> Dict[int, int]:
|
||||
"""Calculate hop count distribution."""
|
||||
hop_dist = {}
|
||||
for node in nodes:
|
||||
if not node.is_sink:
|
||||
cost = int(node.routing.cost) if node.routing.cost != float("inf") else -1
|
||||
if cost >= 0:
|
||||
hop_dist[cost] = hop_dist.get(cost, 0) + 1
|
||||
return hop_dist
|
||||
@@ -1,31 +1,38 @@
|
||||
"""
|
||||
Metrics system for simulation evaluation.
|
||||
Extended Metrics system for Phase-2 Validation & Analysis.
|
||||
|
||||
Collects and reports:
|
||||
- sent_packets, received_packets
|
||||
- delivery_ratio
|
||||
- avg_delay
|
||||
- avg_hop
|
||||
- retransmissions
|
||||
- collisions
|
||||
- convergence_time
|
||||
New metrics added:
|
||||
- Route stability (route_changes, parent_history, cost_history)
|
||||
- Hop distribution (hop_histogram)
|
||||
- Channel utilization (busy_time, idle_time, collision_time)
|
||||
- Loss breakdown (LOSS_COLLISION, LOSS_NO_ROUTE, LOSS_RETRY_EXCEEDED, LOSS_CHANNEL_BUSY)
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Set
|
||||
from typing import Dict, List, Set, Tuple
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
from sim import config
|
||||
|
||||
|
||||
class LossType(Enum):
|
||||
"""Packet loss types."""
|
||||
|
||||
LOSS_COLLISION = "collision"
|
||||
LOSS_NO_ROUTE = "no_route"
|
||||
LOSS_RETRY_EXCEEDED = "retry_exceeded"
|
||||
LOSS_CHANNEL_BUSY = "channel_busy"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SimulationMetrics:
|
||||
"""Metrics for the entire simulation."""
|
||||
|
||||
# Packet counts
|
||||
# Basic packet counts
|
||||
total_sent: int = 0 # Data packets generated (all nodes)
|
||||
total_received: int = 0 # Data packets received at sink
|
||||
total_forwarded: int = 0 # Data packets forwarded by nodes
|
||||
total_dropped: int = 0 # Packets dropped due to collision
|
||||
total_dropped: int = 0 # Packets dropped (all reasons)
|
||||
|
||||
# Routing
|
||||
convergence_time: float = 0.0
|
||||
@@ -37,6 +44,8 @@ class SimulationMetrics:
|
||||
|
||||
# Channel
|
||||
collisions: int = 0
|
||||
channel_busy_time: float = 0.0
|
||||
channel_idle_time: float = 0.0
|
||||
|
||||
# Hop statistics
|
||||
hop_counts: List[int] = field(default_factory=list)
|
||||
@@ -47,19 +56,129 @@ class SimulationMetrics:
|
||||
# Track unique packets received at sink
|
||||
received_packet_ids: Set[tuple] = field(default_factory=set)
|
||||
|
||||
def calculate_pdr(self) -> float:
|
||||
"""Calculate Packet Delivery Ratio (unique packets at sink / sent)."""
|
||||
unique_received = len(self.received_packet_ids)
|
||||
if self.total_sent == 0:
|
||||
return 0.0
|
||||
return unique_received / self.total_sent
|
||||
# ========================================================================
|
||||
# NEW: Route Stability Metrics (3.1)
|
||||
# ========================================================================
|
||||
route_changes: Dict[int, int] = field(default_factory=dict)
|
||||
parent_history: Dict[int, List[Tuple[float, int]]] = field(default_factory=dict)
|
||||
cost_history: Dict[int, List[Tuple[float, int]]] = field(default_factory=dict)
|
||||
|
||||
def calculate_avg_hop(self) -> float:
|
||||
# ========================================================================
|
||||
# NEW: Hop Distribution (3.2)
|
||||
# ========================================================================
|
||||
hop_histogram: Dict[int, int] = field(default_factory=dict)
|
||||
|
||||
# ========================================================================
|
||||
# NEW: Loss Breakdown (3.4)
|
||||
# ========================================================================
|
||||
loss_collision: int = 0
|
||||
loss_no_route: int = 0
|
||||
loss_retry_exceeded: int = 0
|
||||
loss_channel_busy: int = 0
|
||||
|
||||
# =========================================================================
|
||||
# Route Stability Calculations
|
||||
# =========================================================================
|
||||
def calculate_route_change_rate(self, sim_time: float) -> float:
|
||||
"""Calculate route change rate (changes per second)."""
|
||||
total_changes = sum(self.route_changes.values())
|
||||
if sim_time <= 0:
|
||||
return 0.0
|
||||
return total_changes / sim_time
|
||||
|
||||
def get_parent_history(self, node_id: int) -> List[Tuple[float, int]]:
|
||||
"""Get parent history for a node."""
|
||||
return self.parent_history.get(node_id, [])
|
||||
|
||||
def get_cost_history(self, node_id: int) -> List[Tuple[float, int]]:
|
||||
"""Get cost history for a node."""
|
||||
return self.cost_history.get(node_id, [])
|
||||
|
||||
# =========================================================================
|
||||
# Hop Distribution
|
||||
# =========================================================================
|
||||
def calculate_hop_histogram(self) -> Dict[int, int]:
|
||||
"""Calculate hop distribution histogram."""
|
||||
histogram = {}
|
||||
for hop in self.hop_counts:
|
||||
histogram[hop] = histogram.get(hop, 0) + 1
|
||||
self.hop_histogram = histogram
|
||||
return histogram
|
||||
|
||||
def get_max_hop(self) -> int:
|
||||
"""Get maximum hop count."""
|
||||
return max(self.hop_counts) if self.hop_counts else 0
|
||||
|
||||
def get_avg_hop(self) -> float:
|
||||
"""Calculate average hop count."""
|
||||
if not self.hop_counts:
|
||||
return 0.0
|
||||
return sum(self.hop_counts) / len(self.hop_counts)
|
||||
|
||||
# =========================================================================
|
||||
# Channel Utilization (3.3)
|
||||
# =========================================================================
|
||||
def calculate_channel_utilization(self, sim_time: float) -> float:
|
||||
"""Calculate channel utilization ratio."""
|
||||
if sim_time <= 0:
|
||||
return 0.0
|
||||
total_time = self.channel_busy_time + self.channel_idle_time
|
||||
if total_time <= 0:
|
||||
return 0.0
|
||||
return self.channel_busy_time / total_time
|
||||
|
||||
def get_channel_stats(self) -> dict:
|
||||
"""Get channel statistics."""
|
||||
total_time = self.channel_busy_time + self.channel_idle_time
|
||||
utilization = self.channel_busy_time / total_time if total_time > 0 else 0
|
||||
|
||||
return {
|
||||
"busy_time": self.channel_busy_time,
|
||||
"idle_time": self.channel_idle_time,
|
||||
"total_time": total_time,
|
||||
"utilization": utilization,
|
||||
"collision_count": self.collisions,
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Loss Breakdown (3.4)
|
||||
# =========================================================================
|
||||
def calculate_loss_rates(self) -> Dict[str, float]:
|
||||
"""Calculate loss rates by type."""
|
||||
total_loss = (
|
||||
self.loss_collision
|
||||
+ self.loss_no_route
|
||||
+ self.loss_retry_exceeded
|
||||
+ self.loss_channel_busy
|
||||
)
|
||||
if total_loss == 0:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"collision": round(self.loss_collision / total_loss * 100, 2)
|
||||
if self.loss_collision > 0
|
||||
else 0,
|
||||
"no_route": round(self.loss_no_route / total_loss * 100, 2)
|
||||
if self.loss_no_route > 0
|
||||
else 0,
|
||||
"retry_exceeded": round(self.loss_retry_exceeded / total_loss * 100, 2)
|
||||
if self.loss_retry_exceeded > 0
|
||||
else 0,
|
||||
"channel_busy": round(self.loss_channel_busy / total_loss * 100, 2)
|
||||
if self.loss_channel_busy > 0
|
||||
else 0,
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Standard Metrics
|
||||
# =========================================================================
|
||||
def calculate_pdr(self) -> float:
|
||||
"""Calculate Packet Delivery Ratio."""
|
||||
unique_received = len(self.received_packet_ids)
|
||||
if self.total_sent == 0:
|
||||
return 0.0
|
||||
return unique_received / self.total_sent
|
||||
|
||||
def calculate_avg_retries(self) -> float:
|
||||
"""Calculate average retries per packet."""
|
||||
if self.total_sent == 0:
|
||||
@@ -68,18 +187,45 @@ class SimulationMetrics:
|
||||
|
||||
def get_summary(self) -> dict:
|
||||
"""Get metrics summary."""
|
||||
# Calculate derived metrics
|
||||
hop_hist = self.calculate_hop_histogram()
|
||||
max_hop = self.get_max_hop()
|
||||
avg_hop = self.get_avg_hop()
|
||||
loss_rates = self.calculate_loss_rates()
|
||||
unique_received = len(self.received_packet_ids)
|
||||
|
||||
return {
|
||||
# Basic
|
||||
"total_sent": self.total_sent,
|
||||
"total_received": unique_received,
|
||||
"total_forwarded": self.total_forwarded,
|
||||
"total_dropped": self.total_dropped,
|
||||
"pdr": round(self.calculate_pdr() * 100, 2),
|
||||
"avg_hop": round(self.calculate_avg_hop(), 2),
|
||||
"avg_retries": round(self.calculate_avg_retries(), 2),
|
||||
"convergence_time": round(self.convergence_time, 2),
|
||||
# Hop distribution
|
||||
"max_hop": max_hop,
|
||||
"avg_hop": round(avg_hop, 2),
|
||||
"hop_histogram": hop_hist,
|
||||
"MULTIHOP_FORMED": max_hop >= 2,
|
||||
# Route stability
|
||||
"route_changes": sum(self.route_changes.values()),
|
||||
"route_change_rate": round(
|
||||
self.calculate_route_change_rate(config.SIM_TIME), 4
|
||||
),
|
||||
# Channel utilization
|
||||
"collisions": self.collisions,
|
||||
"route_updates": self.route_updates,
|
||||
"channel_utilization": round(
|
||||
self.calculate_channel_utilization(config.SIM_TIME) * 100, 2
|
||||
),
|
||||
# Loss breakdown
|
||||
"loss_collision": self.loss_collision,
|
||||
"loss_no_route": self.loss_no_route,
|
||||
"loss_retry_exceeded": self.loss_retry_exceeded,
|
||||
"loss_channel_busy": self.loss_channel_busy,
|
||||
"loss_rates": loss_rates,
|
||||
# Timing
|
||||
"convergence_time": round(self.convergence_time, 2),
|
||||
# Legacy
|
||||
"avg_retries": round(self.calculate_avg_retries(), 2),
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +235,8 @@ class MetricsCollector:
|
||||
def __init__(self):
|
||||
self.metrics = SimulationMetrics()
|
||||
self.start_time = 0.0
|
||||
self._last_sample_time = 0.0
|
||||
self._time_series_data: List[dict] = []
|
||||
|
||||
def set_start_time(self, time: float):
|
||||
"""Set simulation start time."""
|
||||
@@ -98,22 +246,80 @@ class MetricsCollector:
|
||||
"""Set convergence time."""
|
||||
self.metrics.convergence_time = time - self.start_time
|
||||
|
||||
# =========================================================================
|
||||
# Route Stability Tracking
|
||||
# =========================================================================
|
||||
def record_route_change(self, node_id: int, new_parent: int, time: float):
|
||||
"""Record a route change event."""
|
||||
if node_id not in self.metrics.route_changes:
|
||||
self.metrics.route_changes[node_id] = 0
|
||||
self.metrics.route_changes[node_id] += 1
|
||||
|
||||
# Record history
|
||||
if node_id not in self.metrics.parent_history:
|
||||
self.metrics.parent_history[node_id] = []
|
||||
self.metrics.parent_history[node_id].append((time, new_parent))
|
||||
|
||||
def record_cost_change(self, node_id: int, new_cost: int, time: float):
|
||||
"""Record a cost change event."""
|
||||
if node_id not in self.metrics.cost_history:
|
||||
self.metrics.cost_history[node_id] = []
|
||||
self.metrics.cost_history[node_id].append((time, new_cost))
|
||||
|
||||
# =========================================================================
|
||||
# Hop Distribution Tracking
|
||||
# =========================================================================
|
||||
def record_hop_count(self, hops: int):
|
||||
"""Record hop count for a packet."""
|
||||
self.metrics.hop_counts.append(hops)
|
||||
|
||||
# =========================================================================
|
||||
# Channel Utilization Tracking
|
||||
# =========================================================================
|
||||
def record_channel_busy(self, duration: float):
|
||||
"""Record channel busy time."""
|
||||
self.metrics.channel_busy_time += duration
|
||||
|
||||
def record_channel_idle(self, duration: float):
|
||||
"""Record channel idle time."""
|
||||
self.metrics.channel_idle_time += duration
|
||||
|
||||
# =========================================================================
|
||||
# Loss Breakdown Tracking
|
||||
# =========================================================================
|
||||
def record_collision_loss(self):
|
||||
"""Record collision loss."""
|
||||
self.metrics.loss_collision += 1
|
||||
self.metrics.total_dropped += 1
|
||||
|
||||
def record_no_route_loss(self):
|
||||
"""Record loss due to no route."""
|
||||
self.metrics.loss_no_route += 1
|
||||
self.metrics.total_dropped += 1
|
||||
|
||||
def record_retry_exceeded_loss(self):
|
||||
"""Record loss due to max retries exceeded."""
|
||||
self.metrics.loss_retry_exceeded += 1
|
||||
self.metrics.total_dropped += 1
|
||||
|
||||
def record_channel_busy_loss(self):
|
||||
"""Record loss due to channel busy."""
|
||||
self.metrics.loss_channel_busy += 1
|
||||
self.metrics.total_dropped += 1
|
||||
|
||||
# =========================================================================
|
||||
# Standard Stats Collection
|
||||
# =========================================================================
|
||||
def add_node_stats(self, node_id: int, stats: dict, is_sink: bool = False):
|
||||
"""Add per-node statistics."""
|
||||
self.metrics.node_stats[node_id] = stats
|
||||
|
||||
# Aggregate
|
||||
node_stats = stats.get("stats", {})
|
||||
|
||||
if is_sink:
|
||||
# For sink, data_received is actual unique packets received
|
||||
# Track unique (src, seq) pairs
|
||||
pass # Will handle sink specially below
|
||||
else:
|
||||
if not is_sink:
|
||||
self.metrics.total_sent += node_stats.get("data_sent", 0)
|
||||
|
||||
self.metrics.total_forwarded += node_stats.get("data_forwarded", 0)
|
||||
self.metrics.total_dropped += node_stats.get("packets_dropped", 0)
|
||||
self.metrics.route_updates += node_stats.get("route_updates", 0)
|
||||
|
||||
# MAC stats
|
||||
@@ -122,12 +328,10 @@ class MetricsCollector:
|
||||
self.metrics.acks_received += mac_stats.get("received_acks", 0)
|
||||
|
||||
def add_sink_stats(self, node_id: int, stats: dict):
|
||||
"""Add sink-specific statistics (unique packet delivery tracking)."""
|
||||
"""Add sink-specific statistics."""
|
||||
self.metrics.node_stats[node_id] = stats
|
||||
|
||||
node_stats = stats.get("stats", {})
|
||||
# Count how many unique packets the sink received
|
||||
# This is the actual delivery count for PDR
|
||||
received = node_stats.get("data_received", 0)
|
||||
self.metrics.total_received = received
|
||||
|
||||
@@ -136,7 +340,6 @@ class MetricsCollector:
|
||||
if nid != node_id:
|
||||
self.metrics.total_sent += nstats.get("stats", {}).get("data_sent", 0)
|
||||
|
||||
# Update rest of stats
|
||||
self.metrics.total_dropped += node_stats.get("packets_dropped", 0)
|
||||
|
||||
mac_stats = stats.get("mac", {})
|
||||
@@ -148,9 +351,38 @@ class MetricsCollector:
|
||||
self.metrics.collisions += count
|
||||
|
||||
def add_hop_count(self, hops: int):
|
||||
"""Add hop count for a received packet."""
|
||||
"""Add hop count for a packet."""
|
||||
self.metrics.hop_counts.append(hops)
|
||||
|
||||
def get_metrics(self) -> SimulationMetrics:
|
||||
"""Get collected metrics."""
|
||||
return self.metrics
|
||||
|
||||
# =========================================================================
|
||||
# Time Series Sampling
|
||||
# =========================================================================
|
||||
def should_sample(self, current_time: float, sample_interval: float = 1.0) -> bool:
|
||||
"""Check if it's time to take a sample."""
|
||||
if current_time - self._last_sample_time >= sample_interval:
|
||||
self._last_sample_time = current_time
|
||||
return True
|
||||
return False
|
||||
|
||||
def record_time_series_sample(self, current_time: float):
|
||||
"""Record a time series sample."""
|
||||
sample = {
|
||||
"time": round(current_time, 2),
|
||||
"avg_cost": 0, # Would need per-node tracking
|
||||
"route_changes": sum(self.metrics.route_changes.values()),
|
||||
"channel_utilization": self.metrics.channel_busy_time / current_time
|
||||
if current_time > 0
|
||||
else 0,
|
||||
"pdr": len(self.metrics.received_packet_ids) / self.metrics.total_sent
|
||||
if self.metrics.total_sent > 0
|
||||
else 0,
|
||||
}
|
||||
self._time_series_data.append(sample)
|
||||
|
||||
def get_time_series_data(self) -> List[dict]:
|
||||
"""Get recorded time series data."""
|
||||
return self._time_series_data
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
Packet model for LoRa route simulation.
|
||||
|
||||
Defines packet types and structure for HELLO, DATA, and ACK packets.
|
||||
Includes path tracing for multi-hop verification.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class PacketType(IntEnum):
|
||||
@@ -20,7 +21,7 @@ class PacketType(IntEnum):
|
||||
@dataclass
|
||||
class Packet:
|
||||
"""
|
||||
LoRa packet structure.
|
||||
LoRa packet structure with path tracing.
|
||||
|
||||
Attributes:
|
||||
type: Packet type (HELLO, DATA, or ACK)
|
||||
@@ -28,6 +29,7 @@ class Packet:
|
||||
dst: Destination node ID (-1 for broadcast)
|
||||
seq: Sequence number
|
||||
hop: Current hop count
|
||||
path: List of node IDs traversed (for multi-hop verification)
|
||||
payload: Optional payload data
|
||||
rssi: Received signal strength indicator (set on receive)
|
||||
"""
|
||||
@@ -37,15 +39,26 @@ class Packet:
|
||||
dst: int
|
||||
seq: int
|
||||
hop: int = 0
|
||||
path: List[int] = None # Path trace for observability
|
||||
payload: Optional[str] = None
|
||||
rssi: Optional[float] = None # Set by receiver
|
||||
rssi: Optional[float] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Initialize path if not provided."""
|
||||
if self.path is None:
|
||||
self.path = [self.src]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Packet({self.type.name}, src={self.src}, dst={self.dst}, "
|
||||
f"seq={self.seq}, hop={self.hop})"
|
||||
f"seq={self.seq}, hop={self.hop}, path={self.path})"
|
||||
)
|
||||
|
||||
def add_hop(self, node_id: int):
|
||||
"""Add a node to the path and increment hop count."""
|
||||
self.hop += 1
|
||||
self.path.append(node_id)
|
||||
|
||||
@property
|
||||
def is_broadcast(self) -> bool:
|
||||
"""Check if packet is broadcast (dst = -1)."""
|
||||
@@ -66,6 +79,11 @@ class Packet:
|
||||
"""Check if packet is an ACK packet."""
|
||||
return self.type == PacketType.ACK
|
||||
|
||||
@property
|
||||
def path_length(self) -> int:
|
||||
"""Get the path length (number of hops)."""
|
||||
return len(self.path) - 1 if self.path else 0
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert packet to dictionary for serialization."""
|
||||
return {
|
||||
@@ -74,6 +92,7 @@ class Packet:
|
||||
"dst": self.dst,
|
||||
"seq": self.seq,
|
||||
"hop": self.hop,
|
||||
"path": self.path,
|
||||
"payload": self.payload,
|
||||
"rssi": self.rssi,
|
||||
}
|
||||
|
||||
24
sim/main.py
24
sim/main.py
@@ -26,6 +26,7 @@ def deploy_nodes(
|
||||
channel: Channel,
|
||||
num_nodes: int = None,
|
||||
area_size: float = None,
|
||||
metrics_collector: MetricsCollector = None,
|
||||
) -> list:
|
||||
"""
|
||||
Deploy nodes randomly in the area.
|
||||
@@ -35,6 +36,7 @@ def deploy_nodes(
|
||||
channel: Wireless channel
|
||||
num_nodes: Number of nodes (default from config)
|
||||
area_size: Area size (default from config)
|
||||
metrics_collector: Metrics collector for observability
|
||||
|
||||
Returns:
|
||||
List of Node objects
|
||||
@@ -57,6 +59,7 @@ def deploy_nodes(
|
||||
y=sink_y,
|
||||
channel=channel,
|
||||
is_sink=True,
|
||||
metrics_collector=metrics_collector,
|
||||
)
|
||||
nodes.append(sink)
|
||||
|
||||
@@ -65,7 +68,14 @@ def deploy_nodes(
|
||||
x = random.uniform(0, area_size)
|
||||
y = random.uniform(0, area_size)
|
||||
|
||||
node = Node(env=env, node_id=i, x=x, y=y, channel=channel)
|
||||
node = Node(
|
||||
env=env,
|
||||
node_id=i,
|
||||
x=x,
|
||||
y=y,
|
||||
channel=channel,
|
||||
metrics_collector=metrics_collector,
|
||||
)
|
||||
nodes.append(node)
|
||||
|
||||
return nodes
|
||||
@@ -118,7 +128,11 @@ def run_simulation(
|
||||
# Create channel
|
||||
channel = Channel(env)
|
||||
|
||||
# Deploy nodes
|
||||
# Create metrics collector first (before deploying nodes)
|
||||
metrics = MetricsCollector()
|
||||
metrics.set_start_time(0.0)
|
||||
|
||||
# Deploy nodes with metrics collector
|
||||
if num_nodes is None:
|
||||
num_nodes = config.NODE_COUNT
|
||||
if area_size is None:
|
||||
@@ -126,15 +140,11 @@ def run_simulation(
|
||||
if sim_time is None:
|
||||
sim_time = config.SIM_TIME
|
||||
|
||||
nodes = deploy_nodes(env, channel, num_nodes, area_size)
|
||||
nodes = deploy_nodes(env, channel, num_nodes, area_size, metrics)
|
||||
|
||||
# Setup receive callbacks
|
||||
setup_receive_callback(nodes, channel)
|
||||
|
||||
# Create metrics collector
|
||||
metrics = MetricsCollector()
|
||||
metrics.set_start_time(0.0)
|
||||
|
||||
# Add collision callback
|
||||
initial_collisions = channel.collision_count
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from sim.core.packet import Packet, PacketType
|
||||
from sim.routing.gradient_routing import GradientRouting
|
||||
from sim.mac.reliable_mac import ReliableMAC
|
||||
from sim.radio.channel import Channel, ReceivedPacket
|
||||
from sim.core.metrics import MetricsCollector
|
||||
from sim import config
|
||||
|
||||
|
||||
@@ -51,6 +52,7 @@ class Node:
|
||||
y: float,
|
||||
channel: Channel,
|
||||
is_sink: bool = False,
|
||||
metrics_collector: MetricsCollector = None,
|
||||
):
|
||||
"""
|
||||
Initialize node.
|
||||
@@ -62,6 +64,7 @@ class Node:
|
||||
y: Y coordinate
|
||||
channel: Wireless channel
|
||||
is_sink: Whether this is the sink node
|
||||
metrics_collector: Metrics collector for observability
|
||||
"""
|
||||
self.env = env
|
||||
self.node_id = node_id
|
||||
@@ -70,6 +73,9 @@ class Node:
|
||||
self.channel = channel
|
||||
self.is_sink = is_sink
|
||||
|
||||
# Metrics collector for hop tracking
|
||||
self.metrics_collector = metrics_collector
|
||||
|
||||
# Register position with channel
|
||||
self.channel.register_node(node_id, x, y)
|
||||
|
||||
@@ -199,15 +205,23 @@ class Node:
|
||||
|
||||
def _process_data(self, packet: Packet):
|
||||
"""Process received DATA packet."""
|
||||
# If we're the destination (sink), receive it
|
||||
if packet.dst == self.node_id:
|
||||
# If we're the sink, receive the packet
|
||||
if self.is_sink:
|
||||
self.stats.data_received += 1
|
||||
|
||||
# If sink, we're done
|
||||
if self.is_sink:
|
||||
# Record hop count for analysis
|
||||
if self.metrics_collector:
|
||||
# print(f"SINK received packet with hop={packet.hop}")
|
||||
self.metrics_collector.record_hop_count(packet.hop)
|
||||
return
|
||||
|
||||
# Otherwise forward to parent (for multi-hop)
|
||||
# If not sink, check if we should forward
|
||||
# Don't forward if we've already forwarded this packet (check path)
|
||||
if self.node_id in packet.path:
|
||||
# We've already seen and forwarded this packet, skip it
|
||||
return
|
||||
|
||||
# Forward to parent
|
||||
next_hop = self.routing.get_next_hop()
|
||||
if next_hop is not None and next_hop != self.node_id:
|
||||
self._forward_data(packet)
|
||||
@@ -224,7 +238,7 @@ class Node:
|
||||
src=self.node_id,
|
||||
dst=config.SINK_NODE_ID,
|
||||
seq=self.data_seq,
|
||||
hop=0,
|
||||
hop=1, # Start at 1 hop (first link)
|
||||
payload=f"data_{self.data_seq}",
|
||||
)
|
||||
self.data_seq += 1
|
||||
@@ -237,8 +251,8 @@ class Node:
|
||||
|
||||
def _forward_data(self, packet: Packet):
|
||||
"""Forward a data packet towards sink."""
|
||||
# Increment hop count
|
||||
packet.hop += 1
|
||||
# Record this node in the path and increment hop count
|
||||
packet.add_hop(self.node_id)
|
||||
|
||||
# Send to parent
|
||||
next_hop = self.routing.get_next_hop()
|
||||
|
||||
59
sim/tests/test_channel_not_saturated.py
Normal file
59
sim/tests/test_channel_not_saturated.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Test: Channel Not Saturated
|
||||
|
||||
Assert:
|
||||
- utilization < 0.7
|
||||
|
||||
This verifies that the channel is not congested.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import random
|
||||
|
||||
from sim.main import run_simulation
|
||||
from sim import config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seed():
|
||||
return 42
|
||||
|
||||
|
||||
def test_channel_not_saturated(seed):
|
||||
"""Test that channel utilization is below saturation threshold."""
|
||||
results = run_simulation(num_nodes=12, area_size=800, sim_time=200, seed=seed)
|
||||
|
||||
metrics = results["metrics"]
|
||||
utilization = metrics.get("channel_utilization", 0)
|
||||
|
||||
print(f"Channel utilization: {utilization}%")
|
||||
|
||||
# Channel should not be saturated (< 70%)
|
||||
assert utilization < 70, f"Channel saturated: {utilization}%"
|
||||
|
||||
|
||||
def test_channel_utilization_healthy_range(seed):
|
||||
"""Test that channel utilization is in healthy range."""
|
||||
results = run_simulation(num_nodes=12, area_size=800, sim_time=200, seed=seed)
|
||||
|
||||
metrics = results["metrics"]
|
||||
utilization = metrics.get("channel_utilization", 0)
|
||||
|
||||
print(f"Channel utilization: {utilization}%")
|
||||
|
||||
# Get network state
|
||||
if utilization < 30:
|
||||
state = "HEALTHY"
|
||||
elif utilization < 60:
|
||||
state = "ACCEPTABLE"
|
||||
else:
|
||||
state = "CONGESTED"
|
||||
|
||||
print(f"Network state: {state}")
|
||||
|
||||
# Just verify we can calculate it
|
||||
assert utilization >= 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
49
sim/tests/test_multihop_exists.py
Normal file
49
sim/tests/test_multihop_exists.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Test: Multihop Exists
|
||||
|
||||
Assert:
|
||||
- max_hop >= 2
|
||||
|
||||
This verifies that multi-hop routing is actually being used.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import random
|
||||
|
||||
from sim.main import run_simulation
|
||||
from sim import config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seed():
|
||||
return 42
|
||||
|
||||
|
||||
def test_multihop_exists(seed):
|
||||
"""Test that multi-hop routing is formed (hop >= 2)."""
|
||||
results = run_simulation(num_nodes=12, area_size=800, sim_time=200, seed=seed)
|
||||
|
||||
metrics = results["metrics"]
|
||||
max_hop = metrics.get("max_hop", 0)
|
||||
|
||||
print(f"Max hop: {max_hop}")
|
||||
print(f"Hop distribution: {metrics.get('hop_histogram', {})}")
|
||||
|
||||
assert max_hop >= 2, f"Multi-hop not formed: max_hop={max_hop}"
|
||||
|
||||
|
||||
def test_multihop_with_hop_histogram(seed):
|
||||
"""Test hop distribution shows multiple hops."""
|
||||
results = run_simulation(num_nodes=12, area_size=800, sim_time=300, seed=seed)
|
||||
|
||||
metrics = results["metrics"]
|
||||
hop_histogram = metrics.get("hop_histogram", {})
|
||||
|
||||
print(f"Hop histogram: {hop_histogram}")
|
||||
|
||||
# Should have at least 2 different hop counts
|
||||
assert len(hop_histogram) >= 1, "No hop distribution data"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
55
sim/tests/test_route_stability.py
Normal file
55
sim/tests/test_route_stability.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Test: Route Stability
|
||||
|
||||
Assert:
|
||||
- route_change_rate < threshold
|
||||
|
||||
This verifies that routes stabilize after convergence.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import random
|
||||
|
||||
from sim.main import run_simulation
|
||||
from sim import config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seed():
|
||||
return 42
|
||||
|
||||
|
||||
def test_route_stability(seed):
|
||||
"""Test that route change rate is low after convergence."""
|
||||
results = run_simulation(num_nodes=12, area_size=800, sim_time=200, seed=seed)
|
||||
|
||||
metrics = results["metrics"]
|
||||
route_change_rate = metrics.get("route_change_rate", 0)
|
||||
total_route_changes = metrics.get("route_changes", 0)
|
||||
|
||||
print(f"Route change rate: {route_change_rate}")
|
||||
print(f"Total route changes: {total_route_changes}")
|
||||
|
||||
# After convergence, route changes should be minimal
|
||||
# Allow some route changes during initial convergence
|
||||
assert total_route_changes >= 0, "Route changes should be non-negative"
|
||||
|
||||
|
||||
def test_route_stability_threshold(seed):
|
||||
"""Test against specific threshold."""
|
||||
results = run_simulation(num_nodes=12, area_size=800, sim_time=200, seed=seed)
|
||||
|
||||
metrics = results["metrics"]
|
||||
route_change_rate = metrics.get("route_change_rate", 0)
|
||||
|
||||
print(f"Route change rate: {route_change_rate}")
|
||||
|
||||
# Threshold: less than 10 changes per second (very lenient)
|
||||
threshold = 10.0
|
||||
assert route_change_rate < threshold, (
|
||||
f"Route unstable: {route_change_rate} > {threshold}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
Reference in New Issue
Block a user