AlgoRep/code/simulator_simpy.py
paul.roost ab902bad5f SimPy implementation with 3000 rounds - complete refactor
- Migrate from simple loop to SimPy discrete event simulator
- Implement node_mobility_process() as parallel SimPy processes
- LEACH: distributed CH election with probability p
- LEACH-C: centralized BS-optimized CH selection
- 6 test scenarios with comprehensive results
- 3000 rounds per scenario for long-term viability testing
- All metrics calculated: FDN, FMR, DLBI, RSPI
- 5 PNG graphs generated with analysis
- Full rapport updated with 3000-round results
- Code cleaned: no .log or .md files, no __pycache__
2025-11-03 11:09:08 +01:00

309 lines
12 KiB
Python

"""
Simulateur avec SimPy - Simulation à événements discrets
Architecture basée sur SimPy pour gestion des processus parallèles et du temps
"""
import simpy
import random
import json
import math
from datetime import datetime
from node import Node
from metrics import Metrics
from config import (
FIELD_WIDTH, FIELD_HEIGHT, INITIAL_ENERGY, BS_POSITION,
SCENARIOS, get_num_rounds_for_scenario, DEBUG, MAX_DISPLACEMENT_PER_ROUND
)
class SimulatorSimPy:
"""
Simulateur basé sur SimPy pour la simulation à événements discrets.
Gère les processus parallèles (nœuds, communication, mobilité).
"""
def __init__(self, scenario, protocol_name="LEACH"):
"""
Initialise le simulateur SimPy.
Args:
scenario (dict): Configuration du scénario
protocol_name (str): "LEACH" ou "LEACH-C"
"""
self.env = simpy.Environment()
self.scenario = scenario
self.protocol_name = protocol_name
self.packet_size = scenario["l"]
self.probability_ch = scenario["p"]
self.num_nodes = scenario["n"]
self.scenario_name = scenario["name"]
self.max_rounds = get_num_rounds_for_scenario(self.num_nodes)
self.nodes = []
self.metrics = Metrics()
self.round_num = 0
self.cluster_heads = []
self.clusters = {} # {cluster_id: [node_ids]}
# Statistiques globales
self.total_packets_to_ch = 0
self.total_packets_to_bs = 0
self.muted_rounds = []
def initialize_network(self):
"""Crée les nœuds et initialise le réseau."""
self.nodes = []
for i in range(self.num_nodes):
x = random.uniform(0, FIELD_WIDTH)
y = random.uniform(0, FIELD_HEIGHT)
node = Node(i, x, y, INITIAL_ENERGY)
self.nodes.append(node)
if DEBUG:
print(f"[SimPy Init] {self.num_nodes} nœuds créés pour {self.protocol_name}")
def node_mobility_process(self, node):
"""
Processus SimPy pour la mobilité d'un nœud.
Met à jour la position du nœud à chaque round.
Args:
node (Node): Le nœud à déplacer
"""
while node.is_alive and self.round_num < self.max_rounds:
yield self.env.timeout(1) # Attendre 1 unité de temps (1 round)
if node.is_alive:
node.move()
def elect_cluster_heads_leach(self):
"""
Élection distribuée des cluster heads (LEACH).
Chaque nœud vivant a probabilité p de devenir CH.
"""
self.cluster_heads = []
self.clusters = {}
for node in self.nodes:
if node.is_alive and random.random() < self.probability_ch:
node.is_cluster_head = True
self.cluster_heads.append(node.node_id)
self.clusters[node.node_id] = [node.node_id]
node.cluster_id = node.node_id
# Nœuds non-CH rejoignent le CH le plus proche
for node in self.nodes:
if node.is_alive and not node.is_cluster_head:
closest_ch = self._find_closest_cluster_head(node)
if closest_ch is not None:
node.cluster_id = closest_ch
if closest_ch not in self.clusters:
self.clusters[closest_ch] = []
self.clusters[closest_ch].append(node.node_id)
else:
# Pas de CH - muted round
pass
def elect_cluster_heads_leachc(self):
"""
Élection centralisée des cluster heads (LEACH-C).
La BS sélectionne les nœuds avec le plus d'énergie comme CHs.
"""
self.cluster_heads = []
self.clusters = {}
# BS collecte info de tous les nœuds (coûteux en énergie)
alive_nodes = [n for n in self.nodes if n.is_alive]
if not alive_nodes:
return
# Consommer énergie pour collecter info à la BS
for node in alive_nodes:
# Coût d'envoi de son status (position + énergie) = ~32 bits
distance_to_bs = node.distance_to(*BS_POSITION)
node.transmit(32, distance_to_bs)
# BS sélectionne CHs : les 10% nœuds avec le plus d'énergie (approximation)
num_expected_ch = max(1, int(len(alive_nodes) * 0.1))
sorted_nodes = sorted(alive_nodes, key=lambda n: n.energy, reverse=True)
selected_ch = sorted_nodes[:num_expected_ch]
for node in selected_ch:
node.is_cluster_head = True
self.cluster_heads.append(node.node_id)
self.clusters[node.node_id] = [node.node_id]
node.cluster_id = node.node_id
# BS envoie la liste des CHs à tous les nœuds
for node in alive_nodes:
if not node.is_cluster_head:
distance_to_bs = node.distance_to(*BS_POSITION)
node.receive(len(self.cluster_heads) * 8) # Reçoit liste des CHs
# Nœuds non-CH rejoignent le CH le plus proche
for node in alive_nodes:
if not node.is_cluster_head:
closest_ch = self._find_closest_cluster_head(node)
if closest_ch is not None:
node.cluster_id = closest_ch
if closest_ch not in self.clusters:
self.clusters[closest_ch] = []
self.clusters[closest_ch].append(node.node_id)
def _find_closest_cluster_head(self, node):
"""Trouve le CH le plus proche d'un nœud."""
if not self.cluster_heads:
return None
closest_ch = None
min_distance = float('inf')
for ch_id in self.cluster_heads:
ch_node = self.nodes[ch_id]
distance = node.distance_to(ch_node.x, ch_node.y)
if distance < min_distance:
min_distance = distance
closest_ch = ch_id
return closest_ch
def communication_phase(self):
"""
Phase de communication : transmission de données dans les clusters.
"""
if not self.cluster_heads:
# Muted round - pas de CH
self.muted_rounds.append(self.round_num)
return
# Nœuds non-CH envoient au CH
for node in self.nodes:
if node.is_alive and not node.is_cluster_head:
# Décider si ce nœud a des données à envoyer
if random.random() < self.probability_ch: # Probabilité d'activité
ch_node = self.nodes[node.cluster_id] if node.cluster_id else None
if ch_node and ch_node.is_alive:
distance = node.distance_to(ch_node.x, ch_node.y)
node.transmit(self.packet_size, distance)
ch_node.receive(self.packet_size)
self.total_packets_to_ch += 1
# CHs agrègent et envoient à la BS
for ch_id in self.cluster_heads:
ch_node = self.nodes[ch_id]
if ch_node.is_alive:
# Nombre de paquets reçus = nombre de nœuds dans le cluster - 1
num_packets = len(self.clusters.get(ch_id, [1])) - 1
if num_packets > 0:
# Agrégation
aggregated_data = self.packet_size # Simplifié
ch_node.aggregate(aggregated_data)
# Transmission vers BS
distance_to_bs = ch_node.distance_to(*BS_POSITION)
ch_node.transmit(aggregated_data, distance_to_bs)
self.total_packets_to_bs += 1
def round_process(self):
"""
Processus principal SimPy pour gérer les rounds de simulation.
"""
while self.round_num < self.max_rounds:
yield self.env.timeout(1) # Avancer le temps d'1 round
# Reset nœuds pour cette ronde
for node in self.nodes:
node.reset_for_round()
# Élection des CHs
if self.protocol_name == "LEACH":
self.elect_cluster_heads_leach()
elif self.protocol_name == "LEACH-C":
self.elect_cluster_heads_leachc()
# Phase de communication
self.communication_phase()
# Mobilité : les nœuds se déplacent
for node in self.nodes:
if node.is_alive:
node.move()
# Enregistrer les métriques pour ce round
self.metrics.record_round(
round_num=self.round_num,
nodes=self.nodes,
ch_nodes=[self.nodes[ch_id] for ch_id in self.cluster_heads],
packets_to_ch=self.total_packets_to_ch,
packets_to_bs=self.total_packets_to_bs,
muted=(len(self.cluster_heads) == 0)
)
if DEBUG and self.round_num % 100 == 0:
alive_count = sum(1 for n in self.nodes if n.is_alive)
print(f" Round {self.round_num}: {alive_count} alive, {len(self.cluster_heads)} CHs")
self.round_num += 1
# Vérifier si tous les nœuds sont morts
if all(not n.is_alive for n in self.nodes):
if DEBUG:
print(f" Tous les nœuds sont morts au round {self.round_num}")
break
def run(self):
"""Exécute la simulation complète."""
print(f"\n{'='*60}")
print(f"Simulation : {self.scenario_name}")
print(f"Protocole : {self.protocol_name}")
print(f"Nœuds : {self.num_nodes}, Taille packet : {self.packet_size}, p={self.probability_ch}")
print(f"{'='*60}")
self.initialize_network()
# Démarrer les processus de mobilité pour tous les nœuds
for node in self.nodes:
self.env.process(self.node_mobility_process(node))
# Démarrer le processus principal de simulation
self.env.process(self.round_process())
# Exécuter la simulation
self.env.run()
# Finalicer les métriques
fdn = self.metrics.calculate_fdn()
fmr = self.metrics.calculate_fmr()
rspi = self.metrics.calculate_rspi(self.max_rounds)
dlbi = self.metrics.calculate_dlbi()
print(f"\nRésultats {self.protocol_name}:")
print(f" FDN (First Dead Node): {fdn}")
print(f" FMR (First Muted Round): {fmr}")
print(f" DLBI: {dlbi:.4f}")
print(f" RSPI: {rspi:.4f}")
return {
"fdn": fdn,
"fmr": fmr,
"dlbi": dlbi,
"rspi": rspi,
"metrics": self.metrics,
"rounds_data": self.metrics.rounds_data
}
def get_results(self):
"""Retourne les résultats de la simulation."""
return {
"fdn": self.metrics.calculate_fdn(),
"fmr": self.metrics.calculate_fmr(),
"dlbi": self.metrics.calculate_dlbi(),
"rspi": self.metrics.calculate_rspi(self.max_rounds),
"metrics": self.metrics,
"rounds_data": self.metrics.rounds_data,
"num_nodes": self.num_nodes,
"num_rounds": self.round_num
}