AlgoRep/code/metrics.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

202 lines
6.7 KiB
Python

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