""" Classe Metrics : Collecte et calcul des métriques de performance """ import math from collections import defaultdict class Metrics: """ Collecte les métriques de performance pour chaque round. Métriques suivies: 1. Alive nodes count 2. Packets to cluster head 3. Packets to base station 4. Residual energy 5. Muted rounds (no CH elected) 6. First muted round (FMR) 7. First dead node (FDN) 8. Last dead node 9. Dynamic Load Balancing Index (DLBI) 10. Relative Silence Period Index (RSPI) """ def __init__(self): # Données par round self.rounds_data = [] # Statistiques globales self.first_dead_node_round = None self.last_dead_node_round = None self.first_muted_round = None self.total_muted_rounds = 0 self.muted_rounds_list = [] # Pour calcul DLBI self.ch_loads_per_round = {} # round -> {ch_id -> load} def record_round(self, round_num, nodes, ch_nodes, packets_to_ch, packets_to_bs, muted=False): """ Enregistre les données d'une ronde. Args: round_num (int): Numéro du round nodes (list): Liste de tous les nœuds ch_nodes (list): Liste des cluster heads de ce round packets_to_ch (int): Nombre total de paquets vers CHs packets_to_bs (int): Nombre total de paquets vers BS muted (bool): Si true, aucun CH n'a été élu ce round """ alive_count = sum(1 for n in nodes if n.is_alive) residual_energy = [n.energy for n in nodes if n.is_alive] avg_residual_energy = sum(residual_energy) / len(residual_energy) if residual_energy else 0 round_data = { "round": round_num, "alive_nodes": alive_count, "packets_to_ch": packets_to_ch, "packets_to_bs": packets_to_bs, "avg_residual_energy": avg_residual_energy, "ch_count": len(ch_nodes), "muted": muted, } self.rounds_data.append(round_data) # Suivi des muted rounds if muted: self.muted_rounds_list.append(round_num) self.total_muted_rounds += 1 if self.first_muted_round is None: self.first_muted_round = round_num # Enregistrer les charges des CHs pour DLBI if ch_nodes: self.ch_loads_per_round[round_num] = {} for ch in ch_nodes: # La charge = nombre de nœuds dans le cluster cluster_size = 1 # Le CH lui-même for node in nodes: if node.cluster_id == ch.node_id and not node.is_cluster_head: cluster_size += 1 self.ch_loads_per_round[round_num][ch.node_id] = cluster_size def update_dead_nodes(self, nodes, round_num): """Met à jour les rounds de décès des nœuds.""" for node in nodes: if not node.is_alive: if self.first_dead_node_round is None: self.first_dead_node_round = round_num self.last_dead_node_round = round_num def calculate_dlbi(self): """ Calcule le Dynamic Load Balancing Index. DLBI = (1/N) * Σ(DLBI_r) pour r=1 à N DLBI_r = 1 - [Σ(L_j,r - L̄_r)² / (m_r * L̄_r²)] Returns: float: DLBI (0 à 1, plus élevé = mieux) """ if not self.ch_loads_per_round: return 0 dlbi_values = [] for round_num, loads in self.ch_loads_per_round.items(): if not loads or len(loads) == 0: continue loads_list = list(loads.values()) m_r = len(loads_list) # Nombre de CHs L_bar_r = sum(loads_list) / m_r # Charge moyenne if L_bar_r == 0: dlbi_r = 0 else: variance = sum((load - L_bar_r) ** 2 for load in loads_list) dlbi_r = 1 - (variance / (m_r * (L_bar_r ** 2))) dlbi_values.append(dlbi_r) if dlbi_values: return sum(dlbi_values) / len(dlbi_values) return 0 def calculate_rspi(self, total_rounds): """ Calcule le Relative Silence Period Index. RSPI = 2 * [(1 - FR_muted/R_max) * (1 - LR_dead/R_max)] / [(1 - FR_muted/R_max) + (1 - LR_dead/R_max)] Args: total_rounds (int): Nombre total de rounds Returns: float: RSPI (0 à 1, plus élevé = mieux) """ if not total_rounds: return 0 # Si pas de muted round, utiliser le nombre total de rounds FR_muted = self.first_muted_round if self.first_muted_round is not None else total_rounds # Si pas de dead node, utiliser le nombre total de rounds LR_dead = self.last_dead_node_round if self.last_dead_node_round is not None else total_rounds R_max = total_rounds term1 = 1 - (FR_muted / R_max) term2 = 1 - (LR_dead / R_max) numerator = 2 * term1 * term2 denominator = term1 + term2 # Si dénominateur est zéro ou négatif, retourner 0 if denominator <= 0: return 0 result = numerator / denominator # Clamper entre 0 et 1 (éviter les valeurs négatives) return max(0, min(1, result)) def calculate_fdn(self): """Retourne le First Dead Node round.""" return self.first_dead_node_round def calculate_fmr(self): """Retourne le First Muted Round.""" return self.first_muted_round def get_summary(self, total_rounds): """ Retourne un résumé complet des métriques. Returns: dict: Dictionnaire avec toutes les métriques """ if not self.rounds_data: return {} final_round = self.rounds_data[-1] return { "total_rounds_completed": len(self.rounds_data), "final_alive_nodes": final_round["alive_nodes"], "first_dead_node_round": self.first_dead_node_round, "last_dead_node_round": self.last_dead_node_round, "first_muted_round": self.first_muted_round, "total_muted_rounds": self.total_muted_rounds, "dlbi": self.calculate_dlbi(), "rspi": self.calculate_rspi(total_rounds), } def get_rounds_data(self): """Retourne les données de tous les rounds.""" return self.rounds_data