- 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__
309 lines
12 KiB
Python
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
|
|
}
|