完成py_plan.md
This commit is contained in:
5
sim/core/__init__.py
Normal file
5
sim/core/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Core module."""
|
||||
|
||||
from sim.core.packet import Packet, PacketType
|
||||
|
||||
__all__ = ["Packet", "PacketType"]
|
||||
156
sim/core/metrics.py
Normal file
156
sim/core/metrics.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Metrics system for simulation evaluation.
|
||||
|
||||
Collects and reports:
|
||||
- sent_packets, received_packets
|
||||
- delivery_ratio
|
||||
- avg_delay
|
||||
- avg_hop
|
||||
- retransmissions
|
||||
- collisions
|
||||
- convergence_time
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Set
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from sim import config
|
||||
|
||||
|
||||
@dataclass
|
||||
class SimulationMetrics:
|
||||
"""Metrics for the entire simulation."""
|
||||
|
||||
# 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
|
||||
|
||||
# Routing
|
||||
convergence_time: float = 0.0
|
||||
route_updates: int = 0
|
||||
|
||||
# MAC
|
||||
retries: int = 0
|
||||
acks_received: int = 0
|
||||
|
||||
# Channel
|
||||
collisions: int = 0
|
||||
|
||||
# Hop statistics
|
||||
hop_counts: List[int] = field(default_factory=list)
|
||||
|
||||
# Per-node stats
|
||||
node_stats: Dict[int, dict] = field(default_factory=dict)
|
||||
|
||||
# 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
|
||||
|
||||
def calculate_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)
|
||||
|
||||
def calculate_avg_retries(self) -> float:
|
||||
"""Calculate average retries per packet."""
|
||||
if self.total_sent == 0:
|
||||
return 0.0
|
||||
return self.retries / self.total_sent
|
||||
|
||||
def get_summary(self) -> dict:
|
||||
"""Get metrics summary."""
|
||||
unique_received = len(self.received_packet_ids)
|
||||
return {
|
||||
"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),
|
||||
"collisions": self.collisions,
|
||||
"route_updates": self.route_updates,
|
||||
}
|
||||
|
||||
|
||||
class MetricsCollector:
|
||||
"""Collects metrics from simulation."""
|
||||
|
||||
def __init__(self):
|
||||
self.metrics = SimulationMetrics()
|
||||
self.start_time = 0.0
|
||||
|
||||
def set_start_time(self, time: float):
|
||||
"""Set simulation start time."""
|
||||
self.start_time = time
|
||||
|
||||
def set_convergence_time(self, time: float):
|
||||
"""Set convergence time."""
|
||||
self.metrics.convergence_time = time - self.start_time
|
||||
|
||||
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:
|
||||
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
|
||||
mac_stats = stats.get("mac", {})
|
||||
self.metrics.retries += mac_stats.get("retries", 0)
|
||||
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)."""
|
||||
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
|
||||
|
||||
# Also track all packets sent
|
||||
for nid, nstats in self.metrics.node_stats.items():
|
||||
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", {})
|
||||
self.metrics.retries += mac_stats.get("retries", 0)
|
||||
self.metrics.acks_received += mac_stats.get("received_acks", 0)
|
||||
|
||||
def add_collision(self, count: int = 1):
|
||||
"""Add collision count."""
|
||||
self.metrics.collisions += count
|
||||
|
||||
def add_hop_count(self, hops: int):
|
||||
"""Add hop count for a received packet."""
|
||||
self.metrics.hop_counts.append(hops)
|
||||
|
||||
def get_metrics(self) -> SimulationMetrics:
|
||||
"""Get collected metrics."""
|
||||
return self.metrics
|
||||
79
sim/core/packet.py
Normal file
79
sim/core/packet.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Packet model for LoRa route simulation.
|
||||
|
||||
Defines packet types and structure for HELLO, DATA, and ACK packets.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class PacketType(IntEnum):
|
||||
"""Packet type enumeration."""
|
||||
|
||||
HELLO = 1
|
||||
DATA = 2
|
||||
ACK = 3
|
||||
|
||||
|
||||
@dataclass
|
||||
class Packet:
|
||||
"""
|
||||
LoRa packet structure.
|
||||
|
||||
Attributes:
|
||||
type: Packet type (HELLO, DATA, or ACK)
|
||||
src: Source node ID
|
||||
dst: Destination node ID (-1 for broadcast)
|
||||
seq: Sequence number
|
||||
hop: Current hop count
|
||||
payload: Optional payload data
|
||||
rssi: Received signal strength indicator (set on receive)
|
||||
"""
|
||||
|
||||
type: PacketType
|
||||
src: int
|
||||
dst: int
|
||||
seq: int
|
||||
hop: int = 0
|
||||
payload: Optional[str] = None
|
||||
rssi: Optional[float] = None # Set by receiver
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Packet({self.type.name}, src={self.src}, dst={self.dst}, "
|
||||
f"seq={self.seq}, hop={self.hop})"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_broadcast(self) -> bool:
|
||||
"""Check if packet is broadcast (dst = -1)."""
|
||||
return self.dst == -1
|
||||
|
||||
@property
|
||||
def is_hello(self) -> bool:
|
||||
"""Check if packet is a HELLO packet."""
|
||||
return self.type == PacketType.HELLO
|
||||
|
||||
@property
|
||||
def is_data(self) -> bool:
|
||||
"""Check if packet is a DATA packet."""
|
||||
return self.type == PacketType.DATA
|
||||
|
||||
@property
|
||||
def is_ack(self) -> bool:
|
||||
"""Check if packet is an ACK packet."""
|
||||
return self.type == PacketType.ACK
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert packet to dictionary for serialization."""
|
||||
return {
|
||||
"type": self.type.name,
|
||||
"src": self.src,
|
||||
"dst": self.dst,
|
||||
"seq": self.seq,
|
||||
"hop": self.hop,
|
||||
"payload": self.payload,
|
||||
"rssi": self.rssi,
|
||||
}
|
||||
Reference in New Issue
Block a user