AlgoRep/code/metrics.py
paul.roost 7a33c7096d Add simulation results and launch script for LEACH/LEACH-C
- Created summary.csv to store simulation results for various scenarios, protocols, and metrics.
- Developed run.sh script to automate the simulation process, including dependency checks, simulation execution, and result analysis.
- Ensured proper directory structure for results and reports.
- Added error handling for Python and matplotlib dependencies.
2025-11-02 13:55:51 +01:00

189 lines
6.2 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):
"""Met à jour les rounds de décès des nœuds."""
current_round = len(self.rounds_data)
for node in nodes:
if not node.is_alive:
if self.first_dead_node_round is None:
self.first_dead_node_round = current_round
self.last_dead_node_round = current_round
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
FR_muted = self.first_muted_round if self.first_muted_round else total_rounds
LR_dead = self.last_dead_node_round if self.last_dead_node_round 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
if denominator == 0:
return 0
return numerator / denominator
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