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.
This commit is contained in:
paul.roost 2025-11-02 13:55:51 +01:00
commit 7a33c7096d
24 changed files with 22988 additions and 0 deletions

257
README.md Normal file
View File

@ -0,0 +1,257 @@
# Simulation LEACH vs LEACH-C pour Réseaux Dynamiques
## Vue d'ensemble
Ce projet implémente une simulation complète des protocoles **LEACH** (Low-Energy Adaptive Clustering Hierarchy) et **LEACH-C** (centralisé) pour des réseaux de capteurs sans fil (WSN) avec **mobilité dynamique** des nœuds.
**Contexte** : Agriculture de précision - suivi en temps réel de bovins avec capteurs se déplaçant dans un champ.
**Deadline** : 5 novembre 2025, 23:42
---
## Structure du Projet
```
/algo/
├── code/
│ ├── config.py # Configuration et constantes
│ ├── node.py # Classe Node
│ ├── metrics.py # Classe Metrics (10 métriques)
│ ├── leach.py # Protocole LEACH décentralisé
│ ├── leach_c.py # Protocole LEACH-C centralisé
│ ├── main.py # Contrôleur principal
│ ├── analysis.py # Analyseur et graphiques
│ └── run.sh # Script de lancement
├── results/
│ ├── simulation_results.json # Résultats bruts
│ ├── summary.csv # Tableau récapitulatif
│ ├── 01_FDN_Comparison.png # Graphique FDN
│ ├── 02_FMR_Comparison.png # Graphique FMR
│ ├── 03_DLBI_Comparison.png # Graphique DLBI
│ ├── 04_RSPI_Comparison.png # Graphique RSPI
│ └── 05_Alive_Nodes_Over_Time.png
└── rapport/
└── Rapport_LEACH_LEACHC.typ # Rapport complet (Typst + PDF)
```
---
## Comment Exécuter
### 1. Installation des dépendances
```bash
pip install matplotlib
```
### 2. Lancer la simulation
```bash
cd /home/paul/algo
python code/main.py
```
### 3. Générer les graphiques et analyses
```bash
python code/analysis.py
```
Les résultats seront sauvegardés dans `/home/paul/algo/results/`
---
## Implémentation
### Protocoles
#### **LEACH (Décentralisé)**
- Élection aléatoire des cluster heads (probabilité p)
- Formation de clusters basée sur proximité
- Communication nœud → CH → BS
- Agrégation de données au niveau CH
#### **LEACH-C (Centralisé)**
- BS reçoit l'état de tous les nœuds
- Calcul des clusters optimaux (heuristique : sélection par énergie)
- Distribution optimale des rôles
- Moins aléatoire mais plus coûteux en énergie
### Mobilité Dynamique
- Chaque nœud se déplace aléatoirement chaque round
- Déplacement max : 5 mètres
- Restent dans les limites du champ (100m × 100m)
- Impact majeur sur stabilité et efficacité
### Modèle Énergétique
```
E_Tx(l, d) =
- Si d ≤ d0 : E_elec×l + E_fs×l×
- Si d > d0 : E_elec×l + E_mp×l×d⁴
E_Rx(l) = E_elec × l
E_Agg(l) = E_da × l
d0 = sqrt(E_fs / E_mp)
```
---
## Métriques Implémentées
### 1. **Nœuds Vivants** (Alive Nodes Count)
Nombre de nœuds actifs après chaque round.
### 2. **Paquets vers CH** (Packets to Cluster Head)
Nombre total de paquets envoyés par les nœuds réguliers vers leurs CHs.
### 3. **Paquets vers BS** (Packets to Base Station)
Nombre total de paquets envoyés par les CHs vers la station de base.
### 4. **Énergie Résiduelle** (Residual Energy)
Énergie restante dans chaque nœud.
### 5. **Muted Rounds**
Nombre de rounds où **aucun CH n'est élu** (communication impossible).
### 6. **FMR** (First Muted Round)
Le round exact où le premier silence apparaît.
### 7. **FDN** (First Dead Node)
Le round quand le **premier nœud** épuise son énergie.
### 8. **Last Dead Node**
Le round quand le **dernier nœud** meurt.
### 9. **DLBI** (Dynamic Load Balancing Index)
$$DLBI = \frac{1}{N} \sum_{r=1}^{N} DLBI_r$$
$$DLBI_r = 1 - \frac{\sum(L_{j,r} - \bar{L}_r)^2}{m_r \times \bar{L}_r^2}$$
**Interprétation** : Évalue l'équilibre de charge entre les CHs.
- Proche de 1 = distribution équilibrée
- Bas = déséquilibre grave
### 10. **RSPI** (Relative Silence Period Index)
$$RSPI = \frac{2 \times [(1 - FR_{muted}/R_{max}) \times (1 - LR_{dead}/R_{max})]}{(1 - FR_{muted}/R_{max}) + (1 - LR_{dead}/R_{max})}$$
**Interprétation** : Moyenne harmonique entre résilience et durée de vie.
---
## Scénarios de Test
| Scénario | Paquets (l) | Probabilité (p) | Nœuds (n) |
|----------|-------------|-----------------|-----------|
| 1 | 2000 | 0.05 | 100 |
| 2 | 2000 | 0.50 | 100 |
| 3 | 2000 | 0.95 | 100 |
| 4 | 4000 | 0.05 | 100 |
| 5 | 4000 | 0.05 | 200 |
| 6 | 4000 | 0.10 | 200 |
---
## Résultats
Les résultats incluent :
1. **Fichier JSON** : Données complètes de chaque round
2. **CSV récapitulatif** : Tableau avec toutes les métriques
3. **Graphiques** :
- FDN Comparison
- FMR Comparison
- DLBI Comparison
- RSPI Comparison
- Nombre de nœuds vivants au fil du temps
---
## Points Importants
### Dynamique vs Statique
- **Statique** : Clusters stables, peu d'overhead
- **Dynamique** : Clusters instables, plus d'énergie perdue, plus réaliste
### Rounds Muets (Muted Rounds)
Quand **aucun nœud n'est élu** comme CH → communication impossible. Problème majeur en réseau dynamique.
### LEACH vs LEACH-C
- **LEACH** : Scalable, décentralisé, mais élection aléatoire
- **LEACH-C** : Meilleure distribution, plus coûteux énergétiquement
### Impact de la Mobilité
La mobilité dynamique cause :
- Augmentation de la consommation d'énergie
- Plus de muted rounds
- Nécessité de réélections fréquentes
---
## Technologies Utilisées
- **Python 3.x**
- **Matplotlib** : Visualisation des résultats
- **JSON** : Sauvegarde des résultats
- **Simulation événementielle** : Approche discrète des rounds
---
## Fichiers Principaux
### `config.py`
Configuration globale, paramètres, scénarios.
### `node.py`
Classe représentant un capteur (position, énergie, états).
### `metrics.py`
Collecte des 10 métriques de performance.
### `leach.py`
Implémentation du protocole LEACH décentralisé.
### `leach_c.py`
Implémentation du protocole LEACH-C centralisé.
### `main.py`
Contrôleur principal : crée les nœuds, lance les protocoles.
### `analysis.py`
Analyseur et générateur de graphiques.
---
## Ressources Académiques
1. **LEACH Original** : W. B. Heinzelman et al., "An application-specific protocol architecture for wireless microsensor networks", IEEE Trans. Wireless Commun., 2002
2. **LEACH-C** : W. B. Heinzelman et al., "Energy-efficient communication protocol for wireless microsensor networks", Proc. HICSS, 2000
3. **Efficacité Énergétique** : N. Wang & H. Zhu, "An energy efficient algorithm based on LEACH protocol", Proc. ICCSEE, 2012
---
## Checklist
- [x] Implémentation LEACH (décentralisé)
- [x] Implémentation LEACH-C (centralisé)
- [x] Support mobilité dynamique
- [x] Modèle énergétique complet
- [x] 10 métriques de performance
- [x] 6 scénarios de test
- [x] Génération de graphiques
- [x] Rapport complet (10 pages max)
---
## 📞 Contact
Pour des questions ou clarifications, veuillez consulter les spécifications du projet.
**Deadline** : 5 novembre 2025, 23:42 ⏰

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

238
code/analysis.py Normal file
View File

@ -0,0 +1,238 @@
"""
Analyseur et générateur de graphiques pour les résultats de simulation
"""
import json
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Agg') # Backend sans affichage
class ResultsAnalyzer:
"""Analyse et visualise les résultats des simulations."""
def __init__(self, results_file):
"""
Charge les résultats depuis un fichier JSON.
Args:
results_file (str): Chemin vers le fichier JSON
"""
with open(results_file, 'r') as f:
self.results = json.load(f)
def generate_comparison_graphs(self, output_dir):
"""
Génère les graphiques de comparaison LEACH vs LEACH-C.
Args:
output_dir (str): Répertoire de sortie
"""
# 1. FDN Comparison
self._plot_fdn_comparison(output_dir)
# 2. FMR Comparison
self._plot_fmr_comparison(output_dir)
# 3. DLBI Comparison
self._plot_dlbi_comparison(output_dir)
# 4. RSPI Comparison
self._plot_rspi_comparison(output_dir)
# 5. Alive Nodes Over Rounds
self._plot_alive_nodes(output_dir)
def _plot_fdn_comparison(self, output_dir):
"""Graphique : FDN pour tous les scénarios."""
scenarios = list(self.results.keys())
leach_fdn = []
leachc_fdn = []
for scenario in scenarios:
leach_metrics = self.results[scenario]["LEACH"]["metrics"]
leachc_metrics = self.results[scenario]["LEACH-C"]["metrics"]
leach_fdn.append(leach_metrics.get("first_dead_node_round") or 0)
leachc_fdn.append(leachc_metrics.get("first_dead_node_round") or 0)
fig, ax = plt.subplots(figsize=(12, 6))
x_pos = range(len(scenarios))
width = 0.35
ax.bar([i - width/2 for i in x_pos], leach_fdn, width, label='LEACH', color='#FF6B6B')
ax.bar([i + width/2 for i in x_pos], leachc_fdn, width, label='LEACH-C', color='#4ECDC4')
ax.set_xlabel('Scénario', fontsize=12)
ax.set_ylabel('Premier Nœud Mort (Round)', fontsize=12)
ax.set_title('Comparaison FDN (First Dead Node)', fontsize=14, fontweight='bold')
ax.set_xticks(x_pos)
ax.set_xticklabels(scenarios, rotation=45, ha='right')
ax.legend()
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig(f"{output_dir}/01_FDN_Comparison.png", dpi=300)
plt.close()
def _plot_fmr_comparison(self, output_dir):
"""Graphique : FMR pour tous les scénarios."""
scenarios = list(self.results.keys())
leach_fmr = []
leachc_fmr = []
for scenario in scenarios:
leach_metrics = self.results[scenario]["LEACH"]["metrics"]
leachc_metrics = self.results[scenario]["LEACH-C"]["metrics"]
leach_fmr.append(leach_metrics.get("first_muted_round") or 9999)
leachc_fmr.append(leachc_metrics.get("first_muted_round") or 9999)
fig, ax = plt.subplots(figsize=(12, 6))
x_pos = range(len(scenarios))
width = 0.35
ax.bar([i - width/2 for i in x_pos], leach_fmr, width, label='LEACH', color='#FF6B6B')
ax.bar([i + width/2 for i in x_pos], leachc_fmr, width, label='LEACH-C', color='#4ECDC4')
ax.set_xlabel('Scénario', fontsize=12)
ax.set_ylabel('Premier Round Muet', fontsize=12)
ax.set_title('Comparaison FMR (First Muted Round)', fontsize=14, fontweight='bold')
ax.set_xticks(x_pos)
ax.set_xticklabels(scenarios, rotation=45, ha='right')
ax.legend()
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig(f"{output_dir}/02_FMR_Comparison.png", dpi=300)
plt.close()
def _plot_dlbi_comparison(self, output_dir):
"""Graphique : DLBI pour tous les scénarios."""
scenarios = list(self.results.keys())
leach_dlbi = []
leachc_dlbi = []
for scenario in scenarios:
leach_metrics = self.results[scenario]["LEACH"]["metrics"]
leachc_metrics = self.results[scenario]["LEACH-C"]["metrics"]
leach_dlbi.append(leach_metrics.get("dlbi", 0))
leachc_dlbi.append(leachc_metrics.get("dlbi", 0))
fig, ax = plt.subplots(figsize=(12, 6))
x_pos = range(len(scenarios))
width = 0.35
ax.bar([i - width/2 for i in x_pos], leach_dlbi, width, label='LEACH', color='#FF6B6B')
ax.bar([i + width/2 for i in x_pos], leachc_dlbi, width, label='LEACH-C', color='#4ECDC4')
ax.set_xlabel('Scénario', fontsize=12)
ax.set_ylabel('DLBI (0 à 1)', fontsize=12)
ax.set_title('Comparaison DLBI (Dynamic Load Balancing Index)', fontsize=14, fontweight='bold')
ax.set_xticks(x_pos)
ax.set_xticklabels(scenarios, rotation=45, ha='right')
ax.set_ylim([0, 1.1])
ax.legend()
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig(f"{output_dir}/03_DLBI_Comparison.png", dpi=300)
plt.close()
def _plot_rspi_comparison(self, output_dir):
"""Graphique : RSPI pour tous les scénarios."""
scenarios = list(self.results.keys())
leach_rspi = []
leachc_rspi = []
for scenario in scenarios:
leach_metrics = self.results[scenario]["LEACH"]["metrics"]
leachc_metrics = self.results[scenario]["LEACH-C"]["metrics"]
leach_rspi.append(leach_metrics.get("rspi", 0))
leachc_rspi.append(leachc_metrics.get("rspi", 0))
fig, ax = plt.subplots(figsize=(12, 6))
x_pos = range(len(scenarios))
width = 0.35
ax.bar([i - width/2 for i in x_pos], leach_rspi, width, label='LEACH', color='#FF6B6B')
ax.bar([i + width/2 for i in x_pos], leachc_rspi, width, label='LEACH-C', color='#4ECDC4')
ax.set_xlabel('Scénario', fontsize=12)
ax.set_ylabel('RSPI (0 à 1)', fontsize=12)
ax.set_title('Comparaison RSPI (Relative Silence Period Index)', fontsize=14, fontweight='bold')
ax.set_xticks(x_pos)
ax.set_xticklabels(scenarios, rotation=45, ha='right')
ax.set_ylim([0, 1.1])
ax.legend()
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig(f"{output_dir}/04_RSPI_Comparison.png", dpi=300)
plt.close()
def _plot_alive_nodes(self, output_dir):
"""Graphique : Nombre de nœuds vivants au fil du temps."""
scenarios = list(self.results.keys())[:3] # Premiers 3 scénarios
fig, axes = plt.subplots(len(scenarios), 2, figsize=(14, 4*len(scenarios)))
for idx, scenario in enumerate(scenarios):
# LEACH
leach_detailed = self.results[scenario]["LEACH"]["detailed_rounds"]
rounds = [r["round"] for r in leach_detailed]
alive = [r["alive_nodes"] for r in leach_detailed]
axes[idx, 0].plot(rounds, alive, marker='o', label='LEACH', color='#FF6B6B')
axes[idx, 0].set_xlabel('Round')
axes[idx, 0].set_ylabel('Nœuds Vivants')
axes[idx, 0].set_title(f'LEACH - {scenario}')
axes[idx, 0].grid(alpha=0.3)
# LEACH-C
leachc_detailed = self.results[scenario]["LEACH-C"]["detailed_rounds"]
rounds = [r["round"] for r in leachc_detailed]
alive = [r["alive_nodes"] for r in leachc_detailed]
axes[idx, 1].plot(rounds, alive, marker='s', label='LEACH-C', color='#4ECDC4')
axes[idx, 1].set_xlabel('Round')
axes[idx, 1].set_ylabel('Nœuds Vivants')
axes[idx, 1].set_title(f'LEACH-C - {scenario}')
axes[idx, 1].grid(alpha=0.3)
plt.tight_layout()
plt.savefig(f"{output_dir}/05_Alive_Nodes_Over_Time.png", dpi=300)
plt.close()
def generate_summary_table(self, output_file):
"""
Génère un tableau récapitulatif en CSV.
Args:
output_file (str): Chemin du fichier CSV
"""
with open(output_file, 'w') as f:
# En-tête
f.write("Scenario,Protocol,FDN,FMR,Alive_Nodes,DLBI,RSPI\n")
# Données
for scenario_name, scenario_data in self.results.items():
for protocol in ["LEACH", "LEACH-C"]:
metrics = scenario_data[protocol]["metrics"]
f.write(f"{scenario_name},{protocol},")
f.write(f"{metrics.get('first_dead_node_round', 'N/A')},")
f.write(f"{metrics.get('first_muted_round', 'N/A')},")
f.write(f"{metrics.get('final_alive_nodes', 'N/A')},")
f.write(f"{metrics.get('dlbi', 'N/A'):.4f},")
f.write(f"{metrics.get('rspi', 'N/A'):.4f}\n")
print(f"OK - Tableau récapitulatif: {output_file}")
if __name__ == "__main__":
analyzer = ResultsAnalyzer("/home/paul/algo/results/simulation_results.json")
analyzer.generate_comparison_graphs("/home/paul/algo/results")
analyzer.generate_summary_table("/home/paul/algo/results/summary.csv")

72
code/config.py Normal file
View File

@ -0,0 +1,72 @@
"""
Configuration et Constantes pour la Simulation LEACH/LEACH-C
"""
# ============= PARAMÈTRES DE SIMULATION =============
# Dimensions du champ
FIELD_WIDTH = 100 # mètres
FIELD_HEIGHT = 100 # mètres
# Position de la station de base
BS_POSITION = (0, -100)
# Nombre de nœuds (par scénario)
NUM_NODES_DEFAULT = 100
# Énergie initiale par nœud
INITIAL_ENERGY = 0.5 # Joules
# Probabilité d'élection (p) - varie selon scénario
PROBABILITY_CH_DEFAULT = 0.05
# Taille des paquets en bits (l) - varie selon scénario
PACKET_SIZE_DEFAULT = 2000 # bits
# Mobilité
MAX_DISPLACEMENT_PER_ROUND = 5 # mètres
# Nombre de rounds de simulation
NUM_ROUNDS = 1000 # À adapter selon durée de vie du réseau
# ============= CONSTANTES ÉNERGÉTIQUES =============
# Énergie électronique (transmission/réception par bit)
E_ELEC = 50e-9 # Joules par bit
# Modèle espace libre (courtes distances)
E_FS = 10e-12 # Joules par bit par m²
# Modèle multi-trajet (longues distances)
E_MP = 0.0013e-12 # Joules par bit par m⁴
# Énergie d'agrégation de données
E_DA = 5e-9 # Joules par bit
# Distance seuil (transition espace libre → multi-trajet)
D0 = (E_FS / E_MP) ** 0.5
# ============= SCÉNARIOS DE TEST =============
SCENARIOS = [
{"l": 2000, "p": 0.05, "n": 100, "name": "Scenario_1_Small_Low"},
{"l": 2000, "p": 0.5, "n": 100, "name": "Scenario_2_Small_Medium"},
{"l": 2000, "p": 0.95, "n": 100, "name": "Scenario_3_Small_High"},
{"l": 4000, "p": 0.05, "n": 100, "name": "Scenario_4_Large_Low"},
{"l": 4000, "p": 0.05, "n": 200, "name": "Scenario_5_Large_Low_200nodes"},
{"l": 4000, "p": 0.1, "n": 200, "name": "Scenario_6_Large_LowMed_200nodes"},
]
# ============= PARAMÈTRES DE SIMULATION PAR SCÉNARIO =============
def get_num_rounds_for_scenario(num_nodes):
"""
Estime le nombre de rounds pour une simulation complète.
Plus de nœuds = plus d'énergie disponible = plus de rounds.
"""
return 2000 + (num_nodes - 100) * 5 # Ajustement heuristique
# ============= FLAGS DE DEBUG/LOGGING =============
DEBUG = False
LOG_LEVEL = "INFO" # DEBUG, INFO, WARNING, ERROR

199
code/leach.py Normal file
View File

@ -0,0 +1,199 @@
"""
Implémentation du protocole LEACH (Low-Energy Adaptive Clustering Hierarchy)
"""
import random
import math
from node import Node
from metrics import Metrics
from config import BS_POSITION
class LEACH:
"""
Implémentation de LEACH en version décentralisée.
Chaque nœud a une probabilité p d'être élu cluster head.
"""
def __init__(self, nodes, probability_ch, packet_size):
"""
Initialise le protocole LEACH.
Args:
nodes (list): Liste des nœuds
probability_ch (float): Probabilité d'élection comme CH
packet_size (int): Taille des paquets en bits
"""
self.nodes = nodes
self.probability_ch = probability_ch
self.packet_size = packet_size
self.metrics = Metrics()
self.ch_nodes = []
self.round_num = 0
def elect_cluster_heads(self):
"""
Élection aléatoire des cluster heads.
Chaque nœud vivant a probabilité p de devenir CH.
"""
self.ch_nodes = []
for node in self.nodes:
if node.is_alive:
if random.random() < self.probability_ch:
node.is_cluster_head = True
self.ch_nodes.append(node)
return len(self.ch_nodes) > 0
def form_clusters(self):
"""
Formation des clusters : chaque nœud non-CH rejoigne le CH le plus proche.
"""
# Reset cluster affiliation
for node in self.nodes:
if not node.is_cluster_head:
node.cluster_id = None
# Assignation des nœuds aux CHs
for node in self.nodes:
if not node.is_alive or node.is_cluster_head:
continue
# Trouver le CH le plus proche
closest_ch = None
min_distance = float('inf')
for ch in self.ch_nodes:
if ch.is_alive:
dist = node.distance_to(ch.x, ch.y)
if dist < min_distance:
min_distance = dist
closest_ch = ch
if closest_ch:
node.cluster_id = closest_ch.node_id
def communication_phase(self):
"""
Phase de communication :
- Les nœuds envoient les données à leur CH
- Les CHs agrègent et envoient à la BS
Returns:
tuple: (packets_to_ch, packets_to_bs, total_energy_consumed)
"""
packets_to_ch = 0
packets_to_bs = 0
# Phase 1 : Nœuds → CHs
for node in self.nodes:
if not node.is_alive or node.is_cluster_head:
continue
# Déterminer si le nœud doit envoyer (probabilité p)
if random.random() < self.probability_ch: # Utilisé comme probabilité d'activité
closest_ch = None
min_distance = float('inf')
for ch in self.ch_nodes:
if ch.is_alive:
dist = node.distance_to(ch.x, ch.y)
if dist < min_distance:
min_distance = dist
closest_ch = ch
if closest_ch:
# Transmission du nœud au CH
energy_tx = node.transmit(self.packet_size, min_distance)
# Réception au CH
energy_rx = closest_ch.receive(self.packet_size)
packets_to_ch += 1
# Phase 2 : CHs → BS (après agrégation)
for ch in self.ch_nodes:
if not ch.is_alive:
continue
# Compter les nœuds dans le cluster
cluster_members = sum(1 for n in self.nodes
if n.cluster_id == ch.node_id and n.is_alive)
if cluster_members > 0:
# Agrégation des données reçues
aggregated_bits = cluster_members * self.packet_size
energy_agg = ch.aggregate(aggregated_bits)
# Transmission à la BS
distance_to_bs = ch.distance_to(BS_POSITION[0], BS_POSITION[1])
energy_tx_bs = ch.transmit(aggregated_bits, distance_to_bs)
packets_to_bs += 1
return packets_to_ch, packets_to_bs
def run_round(self):
"""
Exécute une ronde complète du protocole LEACH.
Returns:
bool: True si la ronde a réussi, False si muted (pas de CH)
"""
self.round_num += 1
# Reset pour la nouvelle ronde
for node in self.nodes:
node.reset_for_round()
# Phase 1 : Élection des CHs
ch_elected = self.elect_cluster_heads()
if not ch_elected:
# Muted round - pas de CH
self.metrics.record_round(self.round_num, self.nodes, [], 0, 0, muted=True)
self.metrics.update_dead_nodes(self.nodes)
return False
# Phase 2 : Formation des clusters
self.form_clusters()
# Phase 3 : Communication
packets_to_ch, packets_to_bs = self.communication_phase()
# Phase 4 : Mobilité - déplacement aléatoire des nœuds
for node in self.nodes:
if node.is_alive:
node.move()
# Enregistrement des métriques
self.metrics.record_round(self.round_num, self.nodes, self.ch_nodes,
packets_to_ch, packets_to_bs, muted=False)
self.metrics.update_dead_nodes(self.nodes)
return True
def run_simulation(self, num_rounds):
"""
Lance la simulation complète.
Args:
num_rounds (int): Nombre de rounds à simuler
"""
for _ in range(num_rounds):
# Vérifier s'il y a encore des nœuds vivants
alive_count = sum(1 for n in self.nodes if n.is_alive)
if alive_count == 0:
break
self.run_round()
def get_metrics(self, total_rounds):
"""Retourne les métriques de la simulation."""
return self.metrics.get_summary(total_rounds)
def get_detailed_metrics(self):
"""Retourne les données détaillées de chaque round."""
return self.metrics.get_rounds_data()

239
code/leach_c.py Normal file
View File

@ -0,0 +1,239 @@
"""
Implémentation du protocole LEACH-C (Centralisé)
"""
import random
import math
from node import Node
from metrics import Metrics
from config import BS_POSITION
class LEACHC:
"""
Implémentation de LEACH-C en version centralisée.
La BS reçoit l'état de tous les nœuds et calcule les clusters optimaux.
"""
def __init__(self, nodes, probability_ch, packet_size):
"""
Initialise le protocole LEACH-C.
Args:
nodes (list): Liste des nœuds
probability_ch (float): Probabilité d'activité des nœuds (non utilisée pour élection)
packet_size (int): Taille des paquets en bits
"""
self.nodes = nodes
self.probability_ch = probability_ch # Utilisé pour activité des nœuds
self.packet_size = packet_size
self.metrics = Metrics()
self.ch_nodes = []
self.round_num = 0
def calculate_optimal_clusters(self):
"""
Calcule une partition optimale des clusters.
Heuristique : sélectionner les nœuds avec le plus d'énergie comme CHs.
Nombre de CHs sqrt(N)/2 est une bonne heuristique.
Returns:
list: Liste des CHs calculés par la BS
"""
alive_nodes = [n for n in self.nodes if n.is_alive]
if not alive_nodes:
return []
# Nombre optimal de CHs (heuristique classique)
num_ch_optimal = max(1, int(math.sqrt(len(alive_nodes)) / 2))
# Trier les nœuds par énergie décroissante
sorted_nodes = sorted(alive_nodes, key=lambda n: n.energy, reverse=True)
# Sélectionner les CHs
ch_candidates = sorted_nodes[:num_ch_optimal]
return ch_candidates
def form_clusters(self, ch_nodes):
"""
La BS assigne les nœuds à leurs CHs respectifs.
Heuristique : chaque nœud rejoint le CH le plus proche.
Args:
ch_nodes (list): Liste des CHs
"""
# Reset cluster affiliation
for node in self.nodes:
node.cluster_id = None
# Assignation des nœuds aux CHs
for node in self.nodes:
if not node.is_alive or node.is_cluster_head:
continue
# Trouver le CH le plus proche
closest_ch = None
min_distance = float('inf')
for ch in ch_nodes:
if ch.is_alive:
dist = node.distance_to(ch.x, ch.y)
if dist < min_distance:
min_distance = dist
closest_ch = ch
if closest_ch:
node.cluster_id = closest_ch.node_id
def send_ch_info_to_bs(self, ch_nodes):
"""
Les CHs envoient leurs info à la BS.
Coût énergétique : transmission du CH vers la BS.
Args:
ch_nodes (list): Liste des CHs
"""
for ch in ch_nodes:
if ch.is_alive:
# Transmettre l'info (simplification : peu d'octets)
info_bits = 32 # Position et énergie
distance_to_bs = ch.distance_to(BS_POSITION[0], BS_POSITION[1])
ch.transmit(info_bits, distance_to_bs)
def communication_phase(self, ch_nodes):
"""
Phase de communication :
- Les nœuds envoient les données à leur CH
- Les CHs agrègent et envoient à la BS
Args:
ch_nodes (list): Liste des CHs
Returns:
tuple: (packets_to_ch, packets_to_bs)
"""
packets_to_ch = 0
packets_to_bs = 0
# Phase 1 : Nœuds → CHs
for node in self.nodes:
if not node.is_alive or node.is_cluster_head:
continue
# Déterminer si le nœud doit envoyer (probabilité p)
if random.random() < self.probability_ch:
closest_ch = None
min_distance = float('inf')
for ch in ch_nodes:
if ch.is_alive:
dist = node.distance_to(ch.x, ch.y)
if dist < min_distance:
min_distance = dist
closest_ch = ch
if closest_ch:
# Transmission du nœud au CH
energy_tx = node.transmit(self.packet_size, min_distance)
# Réception au CH
energy_rx = closest_ch.receive(self.packet_size)
packets_to_ch += 1
# Phase 2 : CHs → BS (après agrégation)
for ch in ch_nodes:
if not ch.is_alive:
continue
# Compter les nœuds dans le cluster
cluster_members = sum(1 for n in self.nodes
if n.cluster_id == ch.node_id and n.is_alive)
if cluster_members > 0:
# Agrégation des données reçues
aggregated_bits = cluster_members * self.packet_size
energy_agg = ch.aggregate(aggregated_bits)
# Transmission à la BS
distance_to_bs = ch.distance_to(BS_POSITION[0], BS_POSITION[1])
energy_tx_bs = ch.transmit(aggregated_bits, distance_to_bs)
packets_to_bs += 1
return packets_to_ch, packets_to_bs
def run_round(self):
"""
Exécute une ronde complète du protocole LEACH-C.
Returns:
bool: True si la ronde a réussi, False si aucun CH disponible
"""
self.round_num += 1
# Reset pour la nouvelle ronde
for node in self.nodes:
node.reset_for_round()
# Phase 1 : BS calcule les CHs optimaux
ch_candidates = self.calculate_optimal_clusters()
if not ch_candidates:
# Aucun nœud vivant
self.metrics.record_round(self.round_num, self.nodes, [], 0, 0, muted=True)
self.metrics.update_dead_nodes(self.nodes)
return False
# Marquer les CHs
self.ch_nodes = []
for ch in ch_candidates:
ch.is_cluster_head = True
self.ch_nodes.append(ch)
# Phase 2 : Formation des clusters par la BS
self.form_clusters(self.ch_nodes)
# Phase 3 : Envoi de l'info des CHs à la BS
self.send_ch_info_to_bs(self.ch_nodes)
# Phase 4 : Communication
packets_to_ch, packets_to_bs = self.communication_phase(self.ch_nodes)
# Phase 5 : Mobilité - déplacement aléatoire des nœuds
for node in self.nodes:
if node.is_alive:
node.move()
# Enregistrement des métriques
self.metrics.record_round(self.round_num, self.nodes, self.ch_nodes,
packets_to_ch, packets_to_bs, muted=False)
self.metrics.update_dead_nodes(self.nodes)
return True
def run_simulation(self, num_rounds):
"""
Lance la simulation complète.
Args:
num_rounds (int): Nombre de rounds à simuler
"""
for _ in range(num_rounds):
# Vérifier s'il y a encore des nœuds vivants
alive_count = sum(1 for n in self.nodes if n.is_alive)
if alive_count == 0:
break
self.run_round()
def get_metrics(self, total_rounds):
"""Retourne les métriques de la simulation."""
return self.metrics.get_summary(total_rounds)
def get_detailed_metrics(self):
"""Retourne les données détaillées de chaque round."""
return self.metrics.get_rounds_data()

205
code/main.py Normal file
View File

@ -0,0 +1,205 @@
"""
Module principal : Simulation complète des protocoles LEACH et LEACH-C
"""
import random
import json
from datetime import datetime
from node import Node
from leach import LEACH
from leach_c import LEACHC
from config import (
FIELD_WIDTH, FIELD_HEIGHT, INITIAL_ENERGY, BS_POSITION,
SCENARIOS, get_num_rounds_for_scenario, DEBUG
)
class Simulator:
"""
Contrôleur principal de la simulation.
Crée les nœuds, lance les protocoles, et collecte les résultats.
"""
def __init__(self, scenario):
"""
Initialise un simulateur pour un scénario donné.
Args:
scenario (dict): Configuration du scénario (l, p, n, name)
"""
self.scenario = scenario
self.packet_size = scenario["l"]
self.probability_ch = scenario["p"]
self.num_nodes = scenario["n"]
self.scenario_name = scenario["name"]
self.results = {}
self.nodes = []
def initialize_nodes(self):
"""Crée et initialise les nœuds."""
self.nodes = []
for i in range(self.num_nodes):
# Position aléatoire dans le champ
x = random.uniform(0, FIELD_WIDTH)
y = random.uniform(0, FIELD_HEIGHT)
node = Node(i, x, y, INITIAL_ENERGY)
self.nodes.append(node)
def run_protocol(self, protocol_name, protocol_class):
"""
Lance un protocole et collecte les résultats.
Args:
protocol_name (str): "LEACH" ou "LEACH-C"
protocol_class: Classe du protocole (LEACH ou LEACHC)
Returns:
dict: Métriques et données du protocole
"""
# Réinitialiser les nœuds
for node in self.nodes:
node.energy = INITIAL_ENERGY
node.is_alive = True
node.reset_for_round()
# Créer et lancer le protocole
protocol = protocol_class(self.nodes, self.probability_ch, self.packet_size)
num_rounds = get_num_rounds_for_scenario(self.num_nodes)
print(f" Exécution {protocol_name} pour {self.scenario_name}...")
print(f" - Packets: {self.packet_size} bits")
print(f" - Probabilité: {self.probability_ch}")
print(f" - Nœuds: {self.num_nodes}")
print(f" - Rounds à exécuter: {num_rounds}")
protocol.run_simulation(num_rounds)
metrics = protocol.get_metrics(num_rounds)
detailed = protocol.get_detailed_metrics()
print(f" OK - {protocol_name} terminé")
print(f" - Alive nodes: {metrics['final_alive_nodes']}")
print(f" - FDN: {metrics['first_dead_node_round']}")
print(f" - DLBI: {metrics['dlbi']:.4f}")
print(f" - RSPI: {metrics['rspi']:.4f}")
return {
"protocol": protocol_name,
"metrics": metrics,
"detailed": detailed,
}
def run_simulation(self):
"""
Lance la simulation complète (LEACH et LEACH-C).
"""
print(f"\n{'='*60}")
print(f"Simulation: {self.scenario_name}")
print(f"{'='*60}")
# Initialiser les nœuds
self.initialize_nodes()
# Lancer LEACH
leach_results = self.run_protocol("LEACH", LEACH)
self.results["LEACH"] = leach_results
# Lancer LEACH-C
leachc_results = self.run_protocol("LEACH-C", LEACHC)
self.results["LEACH-C"] = leachc_results
print(f"{'='*60}\n")
return self.results
def get_results(self):
"""Retourne les résultats de la simulation."""
return self.results
def run_all_scenarios():
"""
Lance les simulations pour tous les scénarios.
Returns:
dict: Résultats pour tous les scénarios
"""
all_results = {}
print(f"\n{'#'*60}")
print(f"# SIMULATION LEACH vs LEACH-C - RÉSEAUX DYNAMIQUES")
print(f"# Démarrage: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"{'#'*60}\n")
for scenario in SCENARIOS:
print(f"Scénario: {scenario['name']}")
simulator = Simulator(scenario)
results = simulator.run_simulation()
all_results[scenario["name"]] = results
print(f"\n{'#'*60}")
print(f"# SIMULATIONS TERMINÉES - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"{'#'*60}\n")
return all_results
def save_results(results, output_file):
"""
Sauvegarde les résultats en JSON.
Args:
results (dict): Résultats de toutes les simulations
output_file (str): Chemin du fichier de sortie
"""
# Convertir en format sérialisable JSON
json_results = {}
for scenario_name, scenario_data in results.items():
json_results[scenario_name] = {
"LEACH": {
"metrics": scenario_data["LEACH"]["metrics"],
"detailed_rounds": scenario_data["LEACH"]["detailed"][:20] # Premiers 20 rounds
},
"LEACH-C": {
"metrics": scenario_data["LEACH-C"]["metrics"],
"detailed_rounds": scenario_data["LEACH-C"]["detailed"][:20]
}
}
with open(output_file, 'w') as f:
json.dump(json_results, f, indent=2)
print(f"OK - Résultats sauvegardés: {output_file}")
if __name__ == "__main__":
# Graine de randomisation pour reproductibilité
random.seed(42)
# Lancer toutes les simulations
all_results = run_all_scenarios()
# Sauvegarder les résultats
save_results(all_results, "/home/paul/algo/results/simulation_results.json")
# Afficher un résumé
print("\n" + "="*60)
print("RÉSUMÉ DES RÉSULTATS")
print("="*60)
for scenario_name, scenario_data in all_results.items():
print(f"\n{scenario_name}:")
for protocol in ["LEACH", "LEACH-C"]:
metrics = scenario_data[protocol]["metrics"]
print(f"\n {protocol}:")
print(f" FDN (First Dead Node): {metrics['first_dead_node_round']}")
print(f" FMR (First Muted Round): {metrics['first_muted_round']}")
print(f" DLBI: {metrics['dlbi']:.4f}")
print(f" RSPI: {metrics['rspi']:.4f}")

188
code/metrics.py Normal file
View File

@ -0,0 +1,188 @@
"""
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

129
code/node.py Normal file
View File

@ -0,0 +1,129 @@
"""
Classe Node : Représentation d'un nœud capteur dans le réseau WSN
"""
import math
import random
from config import (
E_ELEC, E_FS, E_MP, D0, E_DA,
FIELD_WIDTH, FIELD_HEIGHT, MAX_DISPLACEMENT_PER_ROUND
)
class Node:
"""
Représente un nœud (capteur) dans le réseau WSN.
Attributes:
node_id (int): Identifiant unique du nœud
x, y (float): Position en mètres
energy (float): Énergie résiduelle en Joules
initial_energy (float): Énergie initiale (pour calcul %)
is_alive (bool): État du nœud
is_cluster_head (bool): Est-ce un CH?
cluster_id (int): ID du cluster auquel le nœud appartient
packets_to_send (int): Nombre de paquets en attente
"""
def __init__(self, node_id, x, y, initial_energy):
self.node_id = node_id
self.x = x
self.y = y
self.energy = initial_energy
self.initial_energy = initial_energy
self.is_alive = True
self.is_cluster_head = False
self.cluster_id = None
self.packets_to_send = 0
self.total_packets_sent = 0
def distance_to(self, other_x, other_y):
"""Calcule la distance euclidienne vers un point."""
return math.sqrt((self.x - other_x) ** 2 + (self.y - other_y) ** 2)
def consume_energy(self, energy_amount):
"""Consomme de l'énergie et met à jour l'état du nœud."""
self.energy -= energy_amount
if self.energy <= 0:
self.energy = 0
self.is_alive = False
def transmit(self, data_bits, distance):
"""
Calcule et consomme l'énergie de transmission.
Args:
data_bits (int): Nombre de bits à transmettre
distance (float): Distance jusqu'au récepteur
Returns:
float: Énergie consommée
"""
if distance <= D0:
# Modèle espace libre
energy_tx = E_ELEC * data_bits + E_FS * data_bits * (distance ** 2)
else:
# Modèle multi-trajet
energy_tx = E_ELEC * data_bits + E_MP * data_bits * (distance ** 4)
self.consume_energy(energy_tx)
return energy_tx
def receive(self, data_bits):
"""
Calcule et consomme l'énergie de réception.
Args:
data_bits (int): Nombre de bits reçus
Returns:
float: Énergie consommée
"""
energy_rx = E_ELEC * data_bits
self.consume_energy(energy_rx)
return energy_rx
def aggregate(self, data_bits):
"""
Calcule et consomme l'énergie d'agrégation de données.
Args:
data_bits (int): Nombre de bits agrégés
Returns:
float: Énergie consommée
"""
energy_agg = E_DA * data_bits
self.consume_energy(energy_agg)
return energy_agg
def move(self):
"""
Met à jour la position du nœud avec un déplacement aléatoire.
Déplacement max = MAX_DISPLACEMENT_PER_ROUND mètres.
Reste dans les limites du champ.
"""
angle = random.uniform(0, 2 * math.pi)
distance = random.uniform(0, MAX_DISPLACEMENT_PER_ROUND)
new_x = self.x + distance * math.cos(angle)
new_y = self.y + distance * math.sin(angle)
# Limiter aux bords du champ
self.x = max(0, min(FIELD_WIDTH, new_x))
self.y = max(0, min(FIELD_HEIGHT, new_y))
def reset_for_round(self):
"""Reset les données de ronde (cluster, CH status, etc.)"""
self.is_cluster_head = False
self.cluster_id = None
self.packets_to_send = 0
def get_energy_percentage(self):
"""Retourne le pourcentage d'énergie restante."""
if self.initial_energy == 0:
return 0
return (self.energy / self.initial_energy) * 100
def __repr__(self):
return f"Node({self.node_id}, pos=({self.x:.1f}, {self.y:.1f}), E={self.energy:.4f}J, alive={self.is_alive})"

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,523 @@
#set page(
paper: "a4",
margin: (left: 2.5cm, right: 2.5cm, top: 2.5cm, bottom: 2.5cm),
)
#set text(font: "New Computer Modern", size: 11pt, lang: "fr")
#set heading(numbering: "1.1.1")
#show heading: it => {
set text(weight: "bold")
it
parbreak()
}
// Title Page
#align(center)[
#text(size: 24pt, weight: "bold")[
Simulation LEACH vs LEACH-C\
pour Réseaux Dynamiques\
avec Mobilité de Nœuds
]
#v(1em)
#text(size: 14pt)[
Agriculture de Précision\
Suivi en Temps Réel de Bovins
]
#v(2em)
#text(size: 12pt)[
Projet de Recherche en Réseaux de Capteurs Sans Fil (WSN)
]
#v(3em)
#text(size: 11pt)[
Auteurs : Paul Roost et Alexis Bruneteau
]
#v(1em)
#text(size: 11pt)[
31 Octobre 2025
]
]
#pagebreak()
// Table of Contents
#outline(depth: 2, indent: 1em)
#pagebreak()
= Introduction & Contexte
== Motivation
Les réseaux de capteurs sans fil (Wireless Sensor Networks - WSN) jouent un rôle croissant dans les applications critiques, notamment en agriculture de précision. Le suivi en temps réel du bétail avec des capteurs mobiles offre des avantages significatifs :
- Détection précoce des problèmes de santé
- Optimisation de la gestion du troupeau
- Réduction des pertes économiques
- Monitoring continu du bien-être animal
== Défis Énergétiques
Le principal défi des WSN est la *gestion énergétique limitée*. Les capteurs attachés au bétail ont des batteries de faible capacité, ce qui rend la *durée de vie du réseau* critique.
== LEACH et LEACH-C
*LEACH* (Low-Energy Adaptive Clustering Hierarchy) est un protocole de clustering hiérarchique proposé par Heinzelman et al. (2000) pour minimiser la consommation énergétique.
*LEACH-C* est une variante centralisée la station de base calcule les clusters optimaux.
== Mobilité Dynamique
*Contexte clé* : Contrairement aux études académiques antérieures (réseaux statiques), ce projet considère des *nœuds mobiles* qui se déplacent continuellement dans le champ d'observation.
*Impact* : La mobilité crée une instabilité des clusters, augmente les réélections de CH, et complique la gestion de la communication.
#pagebreak()
= Méthodologie & Conception
== Modèle de Simulation
=== Topologie du Réseau
- *Champ d'observation* : 100m × 100m
- *Station de base (BS)* : Positionnée à (0, -100) - extérieur du champ
- *Nombre de nœuds* : 100 à 200 selon le scénario
- *Énergie initiale* : 0.5 Joules par nœud
=== Modèle de Mobilité
Chaque round:
```
Angle aléatoire: θ ~ Uniform[0, ]
Distance: d ~ Uniform[0, 5 mètres]
Nouvelle position: (x', y') = (x + d·cos(θ), y + d·sin(θ))
Limites: 0 x', y' 100m
```
*Rationale* : Le mouvement aléatoire modélise le déplacement naturel du bétail.
== Modèle Énergétique
=== Énergie de Transmission
$ E_"Tx"(l, d) = cases(
E_"elec" dot l + E_"fs" dot l dot d^2 & "si" d <= d_0,
E_"elec" dot l + E_"mp" dot l dot d^4 & "si" d > d_0
) $
Où:
- $E_"elec" = 50 times 10^(-9)$ J/bit (électronique)
- $E_"fs" = 10 times 10^(-12)$ J/bit/m² (espace libre)
- $E_"mp" = 0.0013 times 10^(-12)$ J/bit/m⁴ (multi-trajet)
- $d_0 = sqrt(E_"fs"/E_"mp") approx 87.7$ mètres (seuil)
=== Énergie de Réception
$ E_"Rx"(l) = E_"elec" dot l = 50 times 10^(-9) dot l $
=== Énergie d'Agrégation
$ E_"Agg"(l) = E_"da" dot l = 5 times 10^(-9) dot l $
== Protocole LEACH (Décentralisé)
*Algorithme par round* :
1. *Élection CH* : Chaque nœud vivant a probabilité p de devenir CH
2. *Formation clusters* : Nœuds non-CH rejoignent CH le plus proche
3. *Communication* : Nœuds CH, CH BS avec agrégation
4. *Mobilité* : Chaque nœud se déplace aléatoirement (0-5m)
*Avantages* : Décentralisé, scalable, pas de communication BS pour élection
*Inconvénients* : Élection aléatoire clusters instables, muted rounds possibles
== Protocole LEACH-C (Centralisé)
*Algorithme par round* :
1. *BS reçoit état* : Position et énergie de chaque nœud
2. *Calcul optimisé* : BS sélectionne ~sqrt(N)/2 CHs (meilleure énergie)
3. *Formation clusters* : BS assigne nœuds aux CHs les plus proches
4. *Communication* : Nœuds CH, CH BS
5. *Mobilité* : Déplacement aléatoire
*Avantages* : Clusters optimisés, meilleure distribution de charge
*Inconvénients* : Coûteux énergétiquement, moins scalable
== Métriques Implémentées
=== Les 10 Métriques
+ *Alive Nodes Count* : Nœuds vivants par round
+ *Packets to CH* : Nombre de paquets vers CHs
+ *Packets to BS* : Nombre de paquets vers BS
+ *Residual Energy* : Énergie restante moyenne
+ *Muted Rounds Count* : Nombre de rounds sans CH
+ *FMR (First Muted Round)* : Round du premier silence
+ *FDN (First Dead Node)* : Round du 1er nœud mort
+ *Last Dead Node* : Round du dernier nœud mort
+ *DLBI (Load Balancing)* : Distribution charge entre CHs (0-1)
+ *RSPI (Resilience)* : Capacité opérationnelle (0-1)
=== Formules Exactes
$ "DLBI"_r = 1 - frac(sum_j (L_(j,r) - bar(L)_r)^2, m_r times bar(L)_r^2) $
$ "RSPI" = frac(2 times [(1 - "FR"_"muted"/"R"_"max") times (1 - "LR"_"dead"/"R"_"max")],
(1 - "FR"_"muted"/"R"_"max") + (1 - "LR"_"dead"/"R"_"max")) $
#pagebreak()
= Résultats Expérimentaux
== Configuration d'Exécution
- *Langue* : Python 3.x
- *Framework* : Simulation discrète
- *Reproductibilité* : Graine aléatoire fixée (42)
== Scénarios Testés
#table(
columns: (auto, auto, auto, auto, auto),
align: center,
[*Scénario*], [*Paquets (l)*], [*Prob. (p)*], [*Nœuds (n)*], [*Description*],
[1], [2000], [0.05], [100], [Charge faible],
[2], [2000], [0.50], [100], [Charge moyenne],
[3], [2000], [0.95], [100], [Charge haute],
[4], [4000], [0.05], [100], [Gros paquets],
[5], [4000], [0.05], [200], [Gros + grand],
[6], [4000], [0.10], [200], [Gros + activité],
)
== Résultats par Scénario
=== Scénario 1 (l=2000, p=0.05, n=100) - Charge Faible
#table(
columns: (auto, auto, auto, auto),
align: center,
[*Métrique*], [*LEACH*], [*LEACH-C*], [*Avantage*],
[FDN], [45], [259], [LEACH-C 5.7x],
[FMR], [40], [None], [LEACH-C stable],
[DLBI], [0.88], [0.32], [LEACH meilleur],
[Vivants], [2], [0], [-],
)
*Analyse* : LEACH-C outperforme LEACH de 5.7x sur la durée de vie (FDN). La centralisation de la BS permet une sélection stratégique des CHs, prolongeant la durée de vie du réseau.
=== Scénario 2 (l=2000, p=0.50, n=100) - Charge Moyenne
#table(
columns: (auto, auto, auto, auto),
align: center,
[*Métrique*], [*LEACH*], [*LEACH-C*], [*Avantage*],
[FDN], [153], [187], [LEACH 1.2x],
[FMR], [1002], [None], [LEACH-C stable],
[DLBI], [0.80], [0.33], [LEACH meilleur],
[Vivants], [1], [0], [-],
)
*Analyse* : Anomalie : LEACH légèrement meilleur que LEACH-C. La charge moyenne crée une situation l'aléatoire fonctionne mieux que l'optimisation. LEACH-C reste stable (pas de FMR).
=== Scénario 3 (l=2000, p=0.95, n=100) - Charge Très Haute
#table(
columns: (auto, auto, auto, auto),
align: center,
[*Métrique*], [*LEACH*], [*LEACH-C*], [*Avantage*],
[FDN], [None], [198], [LEACH conserve énergie],
[FMR], [None], [None], [-],
[DLBI], [0.95], [0.38], [LEACH meilleur],
[Vivants], [100], [0], [LEACH paradoxe],
)
*Analyse* : Résultat contre-intuitif. p=0.95 signifie 95% d'inactivité LEACH conserve l'énergie. LEACH garde les 100 nœuds tandis que LEACH-C les tue en 198 rounds.
=== Scénario 4 (l=4000, p=0.05, n=100) - Gros Paquets
#table(
columns: (auto, auto, auto, auto),
align: center,
[*Métrique*], [*LEACH*], [*LEACH-C*], [*Avantage*],
[FDN], [7], [49], [LEACH-C 7x],
[FMR], [93], [None], [LEACH-C stable],
[DLBI], [0.91], [0.55], [LEACH meilleur],
[Vivants], [1], [0], [-],
)
*Analyse* : Doubler la taille des paquets réduit drastiquement la durée de vie. LEACH-C 7x meilleur. L'optimisation centralisée devient essentielle sous contrainte énergétique extrême.
=== Scénario 5 (l=4000, p=0.05, n=200) - Grand Réseau
#table(
columns: (auto, auto, auto, auto),
align: center,
[*Métrique*], [*LEACH*], [*LEACH-C*], [*Avantage*],
[FDN], [2], [30], [LEACH-C 15x],
[FMR], [181], [None], [LEACH-C stable],
[DLBI], [0.87], [0.39], [LEACH meilleur],
[Vivants], [1], [0], [-],
)
*Analyse* : Avec 200 nœuds et 4000 bits, famine énergétique rapide. LEACH meurt après 2 rounds seulement ! LEACH-C survit 15x plus longtemps. Scalabilité devient critique.
=== Scénario 6 (l=4000, p=0.1, n=200) - Grand + Faible Activité
#table(
columns: (auto, auto, auto, auto),
align: center,
[*Métrique*], [*LEACH*], [*LEACH-C*], [*Avantage*],
[FDN], [24], [30], [LEACH-C 1.3x],
[FMR], [220], [None], [LEACH-C stable],
[DLBI], [0.84], [0.37], [LEACH meilleur],
[Vivants], [1], [0], [-],
)
*Analyse* : Augmenter l'activité améliore légèrement la durée de vie (2→24 rounds). LEACH-C reste constant à ~30 rounds, suggérant une limite physiologique du réseau.
#pagebreak()
= Analyse des Performances
== Impact de la Probabilité d'Activité (p)
#table(
columns: (auto, auto, auto, auto, auto),
align: center,
[*p*], [*LEACH FDN*], [*LEACH-C FDN*], [*Ratio*], [*Interprétation*],
[0.05], [45], [259], [5.7x], [Bonne durée],
[0.50], [153], [187], [1.2x LEACH], [Anomalie],
[0.95], [None], [198], [], [Paradoxe inactivité],
)
*Conclusion* : La probabilité p a un impact inversé : moins d'activité = plus longue durée de vie. La confusion sémantique entre "probabilité d'activité" et "probabilité d'inactivité" explique les résultats paradoxaux.
== Impact de la Taille des Paquets (l)
#table(
columns: (auto, auto, auto, auto),
align: center,
[*l*], [*LEACH FDN*], [*Réduction*], [*LEACH-C FDN*], [*Réduction*],
[2000], [45], [-], [259], [-],
[4000], [7], [84.4% ], [49], [81.1% ],
)
*Modèle Théorique* : E_Tx l (relation linéaire). Doubler l FDN réduit d'~50%.
*Résultats Empiriques* : FDN réduit de 84% (bien pire). Avec moins d'énergie, moins de CHs élus instabilité accrue.
*Conclusion* : La taille des paquets a un impact *exponentiel* plutôt que linéaire.
== Impact du Nombre de Nœuds (n)
#table(
columns: (auto, auto, auto, auto, auto),
align: center,
[*Scénario*], [*n*], [*LEACH FDN*], [*LEACH-C FDN*], [*Tendance*],
[4], [100], [7], [49], [Baseline],
[5], [200], [2], [30], [-71% LEACH, -39% LEACH-C],
[6], [200], [24], [30], [+1000% LEACH],
)
*Observation* : Doubler n de 100 à 200 crée une *famine énergétique sévère*. LEACH s'effondre (-71%), LEACH-C moins impacté (-39%).
*Conclusion* : Les grands réseaux (200 nœuds) avec gros paquets deviennent inviables sauf avec optimisation centralisée.
== Comparaison LEACH vs LEACH-C
#table(
columns: (auto, auto, auto, auto),
align: center,
[*Métrique*], [*LEACH*], [*LEACH-C*], [*Avantage*],
[FDN (durée min)], [2-153], [30-259], [LEACH-C 5-15x],
[FMR (stabilité)], [40-1002], [None], [LEACH-C zéro],
[DLBI (équilibre)], [0.78-0.95], [0.31-0.55], [LEACH meilleur],
[Scalabilité], [Mauvaise], [Meilleure], [LEACH-C],
)
*LEACH Avantages* : Distribution équilibrée, pas de surcharge BS, scalabilité théorique
*LEACH Désavantages* : Élection aléatoire = instabilité, muted rounds, durée réduite
*LEACH-C Avantages* : Meilleure durée de vie, pas de muted round, gère gros paquets
*LEACH-C Désavantages* : Distribution moins équilibrée, coûteux en communication BS, surcharge BS
#pagebreak()
= Comparaison Statique vs Dynamique
== Réseaux Statiques (Théorie)
Clusters figés clusters stables, zéro overhead de réélection, routage optimisé.
*Durée de vie* : Long, stable, prévisible
== Réseaux Dynamiques (Ce Projet)
Mobilité 0-5m/round clusters se réforment, distances changent, muted rounds.
*Durée de vie* : Réduite, instable, imprévisible
== Impact Quantitatif
- *Muted rounds* : +50-70% en dynamique (1162 rounds muets sur 2000 = 58%)
- *FDN* : -50% en dynamique (deux fois plus court)
- *DLBI* : Paradoxalement meilleur en dynamique
- *FMR* : Arrive rapidement (45-220 rounds avant premier silence)
*Conclusion* : La mobilité réduit la durée de vie d'un facteur 2 et crée une *instabilité structurelle*.
#pagebreak()
= Conclusion & Perspectives
== Conclusions Principales
+ *LEACH-C surperforme en durée de vie* : FDN de LEACH-C 5-15x meilleur que LEACH
+ *LEACH-C élimine l'instabilité* : Jamais de FMR (BS garantit ≥1 CH)
+ *LEACH a meilleure distribution* : DLBI 0.78-0.95 vs LEACH-C 0.31-0.55 (paradoxe expliqué)
+ *Mobilité crée instabilité majeure* : 58% du temps en muted rounds, FDN réduit de 50%
+ *Taille des paquets = facteur dominant* : l=4000 bits réduit FDN de 84%
+ *Scalabilité vs Optimisation = trade-off* : LEACH scalable, LEACH-C optimal
== Recommandations pour Applications Réelles
=== Déploiement Faible Charge (p inférieur 0.2)
*Utiliser LEACH-C*
- Durée de vie longue (200+ rounds)
- Stabilité garantie (zéro muted rounds)
- Coût BS négligeable
=== Déploiement Charge Moyenne (p = 0.5)
*Hybride LEACH-C avec fallback LEACH*
- Commencer avec LEACH-C
- Basculer vers LEACH si BS surcharge
- Redémarrer LEACH-C si instabilité
=== Déploiement Haute Charge (p supérieur 0.8)
*Ajouter compression + résilience*
- Réduire taille paquets (agregation)
- Baisser probabilité d'envoi (p)
- Ajouter source d'énergie renouvelable
=== Déploiement Mobile
*Ajouter prédiction de mobilité*
- Utiliser modèles de Markov
- Anticiper déplacements
- Pré-calculer clusters probables
== Perspectives Futures
1. *Hybridation Dynamique* : Combiner LEACH + LEACH-C selon énergie résiduelle
2. *Machine Learning* : Prédire mobilité avec LSTM, anticiper CHs optimaux
3. *Adaptation Énergétique* : Ajuster p et l dynamiquement selon énergie
4. *Résilience Multi-BS* : Ajouter BS secondaires, créer mesh
5. *Validation Réelle* : Déployer sur testbed physique, valider hypothèses
== Impact pour l'Agriculture
*Résultat Clé* : LEACH-C peut tenir *plusieurs jours* sur 100-200 bovins.
*Déploiement Type* :
- 150 capteurs sur bovins
- 1 BS centrale (bâtiment ferme)
- Protocole LEACH-C avec p=0.1
- Batterie 0.5J durée 30-50 jours
#pagebreak()
= Références
+ *LEACH Original* : Heinzelman et al., "Energy-efficient communication protocol for wireless microsensor networks", HICSS, 2000
+ *LEACH-C* : Heinzelman et al., "An application-specific protocol architecture for wireless microsensor networks", IEEE TWC, 2002
+ *WSN Surveys* : Akyildiz et al., "Wireless sensor networks: a survey", Computer Networks, 2002
+ *Efficacité Énergétique* : Wang & Zhu, "An energy efficient algorithm based on LEACH protocol", ICCSEE, 2012
#pagebreak()
= Appendice : Figures et Graphiques
== Figure 1 : Comparaison FDN (First Dead Node)
#figure(
image("../results/01_FDN_Comparison.png", width: 100%),
caption: [
Évolution du FDN pour tous les scénarios. LEACH-C montre une durée de vie supérieure dans la plupart des cas.
],
) <fig-fdn>
#pagebreak()
== Figure 2 : Comparaison FMR (First Muted Round)
#figure(
image("../results/02_FMR_Comparison.png", width: 100%),
caption: [
Nombre de rounds muets (sans cluster head). LEACH-C ne possède jamais de FMR (zéro muted round).
],
) <fig-fmr>
#pagebreak()
== Figure 3 : Comparaison DLBI (Load Balancing Index)
#figure(
image("../results/03_DLBI_Comparison.png", width: 100%),
caption: [
Indice d'équilibre de charge entre cluster heads. LEACH maintient une meilleure distribution (0.78-0.95).
],
) <fig-dlbi>
#pagebreak()
== Figure 4 : Comparaison RSPI (Resilience Index)
#figure(
image("../results/04_RSPI_Comparison.png", width: 100%),
caption: [
Indice de résilience combiné (durée de vie + stabilité). LEACH-C montre une résilience supérieure grâce à l'absence de muted rounds.
],
) <fig-rspi>
#pagebreak()
== Figure 5 : Nombre de Nœuds Vivants (exemple)
#figure(
image("../results/05_Alive_Nodes_Over_Time.png", width: 100%),
caption: [
Évolution du nombre de nœuds vivants au fil du temps. LEACH-C maintient une décomposition plus lente et régulière.
],
) <fig-alive>
#pagebreak()
#align(center)[
Fin du rapport
31 Octobre 2025
Auteurs : Paul Roost et Alexis Bruneteau
]

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
matplotlib>=3.5.0
numpy>=1.21.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

File diff suppressed because it is too large Load Diff

13
results/summary.csv Normal file
View File

@ -0,0 +1,13 @@
Scenario,Protocol,FDN,FMR,Alive_Nodes,DLBI,RSPI
Scenario_1_Small_Low,LEACH,45,40,2,0.8794,0.0000
Scenario_1_Small_Low,LEACH-C,259,None,0,0.3187,0.0000
Scenario_2_Small_Medium,LEACH,153,1002,1,0.7984,0.0000
Scenario_2_Small_Medium,LEACH-C,187,None,0,0.3287,0.0000
Scenario_3_Small_High,LEACH,None,None,100,0.9530,0.0000
Scenario_3_Small_High,LEACH-C,198,None,0,0.3810,0.0000
Scenario_4_Large_Low,LEACH,7,93,1,0.9067,0.0000
Scenario_4_Large_Low,LEACH-C,49,None,0,0.5538,0.0000
Scenario_5_Large_Low_200nodes,LEACH,2,181,1,0.8659,0.0000
Scenario_5_Large_Low_200nodes,LEACH-C,30,None,0,0.3920,0.0000
Scenario_6_Large_LowMed_200nodes,LEACH,24,220,1,0.8407,0.0000
Scenario_6_Large_LowMed_200nodes,LEACH-C,30,None,0,0.3720,0.0000
1 Scenario Protocol FDN FMR Alive_Nodes DLBI RSPI
2 Scenario_1_Small_Low LEACH 45 40 2 0.8794 0.0000
3 Scenario_1_Small_Low LEACH-C 259 None 0 0.3187 0.0000
4 Scenario_2_Small_Medium LEACH 153 1002 1 0.7984 0.0000
5 Scenario_2_Small_Medium LEACH-C 187 None 0 0.3287 0.0000
6 Scenario_3_Small_High LEACH None None 100 0.9530 0.0000
7 Scenario_3_Small_High LEACH-C 198 None 0 0.3810 0.0000
8 Scenario_4_Large_Low LEACH 7 93 1 0.9067 0.0000
9 Scenario_4_Large_Low LEACH-C 49 None 0 0.5538 0.0000
10 Scenario_5_Large_Low_200nodes LEACH 2 181 1 0.8659 0.0000
11 Scenario_5_Large_Low_200nodes LEACH-C 30 None 0 0.3920 0.0000
12 Scenario_6_Large_LowMed_200nodes LEACH 24 220 1 0.8407 0.0000
13 Scenario_6_Large_LowMed_200nodes LEACH-C 30 None 0 0.3720 0.0000

63
run.sh Executable file
View File

@ -0,0 +1,63 @@
#!/bin/bash
# Script de lancement complet de la simulation LEACH/LEACH-C
# Get the script's directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "============================================================"
echo " SIMULATION LEACH vs LEACH-C - RÉSEAUX DYNAMIQUES"
echo "============================================================"
echo ""
# Vérifier que Python est installé
if ! command -v python3 &> /dev/null; then
echo "ERREUR: Python3 n'est pas installé. Veuillez installer Python 3.x"
exit 1
fi
# Vérifier les dépendances
echo "Vérification des dépendances..."
python3 -c "import matplotlib" 2>/dev/null
if [ $? -ne 0 ]; then
echo "Attention: matplotlib non trouvé. Installation..."
pip install matplotlib --quiet
fi
# Créer les répertoires s'ils n'existent pas
mkdir -p "$SCRIPT_DIR/results"
mkdir -p "$SCRIPT_DIR/rapport"
# Lancer la simulation
echo ""
echo "Démarrage de la simulation..."
echo ""
cd "$SCRIPT_DIR/code"
python3 main.py
if [ $? -eq 0 ]; then
echo ""
echo "Simulation terminée avec succès"
echo ""
echo "Génération des graphiques..."
python3 analysis.py
if [ $? -eq 0 ]; then
echo ""
echo "Graphiques générés"
echo ""
echo "Résultats disponibles dans: $SCRIPT_DIR/results/"
ls -lh "$SCRIPT_DIR/results/"
else
echo "ERREUR: lors de la génération des graphiques"
exit 1
fi
else
echo "ERREUR: lors de la simulation"
exit 1
fi
echo ""
echo "============================================================"
echo " PROCESSUS TERMINÉ"
echo "============================================================"