""" 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 }