A la fin de ce notebook, vous saurez : 1. Comprendre les principes de l’optimisation par essaim de particules (PSO) 2. Adapter le PSO a la resolution de Sudoku 3. Implementer un solveur d’essaim discret (Workers/Explorers) adapte au Sudoku 4. Comparer les performances avec les autres métaheuristiques
Ce notebook implemente un solveur de Sudoku utilisant le Particle Swarm Optimization (PSO) en Python. C’est l’equivalent Python du notebook C# Sudoku-05-PSO-CSharp.ipynb.
Introduction
Le Particle Swarm Optimization (PSO) est une métaheuristique inspiree du comportement social des oiseaux et des poissons. Developpe par Kennedy et Eberhart en 1995, l’algorithme simule un essaim de particules qui explorent l’espace de recherche.
Principes fondamentaux (PSO canonique)
Particules : Chaque particule represente une solution candidate
Position : La position de la particule = configuration de la grille
Vitesse : Direction et vitesse de deplacement dans l’espace de recherche
pBest : Meilleure position personnelle de la particule
gBest : Meilleure position globale de l’essaim
Equation de mise a jour (PSO canonique, espace continu)
Ou : - \(w\) : poids d’inertie (impact de la vitesse précédente) - \(c_1\), \(c_2\) : coefficients d’apprentissage (cognitif et social) - \(r_1\), \(r_2\) : nombres aleatoires [0, 1]
Adaptation au Sudoku : une variante discrete inspiree de l’essaim
L’equation de vitesse ci-dessus est définie sur un espace continu : additionner une vitesse a une position suppose des coordonnees reelles. Le Sudoku est un problème combinatoire discret (permutations de chiffres), ou cette addition n’a pas de sens direct. Ce notebook n’implemente donc pas l’equation de vitesse canonique ; il utilise une heuristique d’essaim discrete (classe PSOSudokuSolver) inspiree du PSO et des colonies d’organismes :
Workers (worker_ratio = 0.90) : exploitent leur voisinage par echange de deux cases dans un même bloc (neighbor_matrix, recherche locale), en acceptant un voisin s’il reduit l’erreur (ou rarement, via un faible mutation_rate). Un worker qui stagne trop longtemps (age > max_age) est reinitialise.
Explorers (10%) : repartent d’une grille aleatoire complete (random_matrix) a chaque itération, pour diversifier la recherche.
Fusion : a chaque epoque, le pire worker est remplace par un croisement par blocs (merge_matrices) du meilleur worker et du meilleur explorer.
Meilleure globale : l’essaim conserve la meilleure grille rencontree (best_error / best_solution), qui est la solution retournee.
A retenir : il n’y a ici ni champ de vitesse, ni memoire pBest par particule, ni coefficients \(w\)/\(c_1\)/\(c_2\). Les workers ne sont pas attires par gBest au sens de l’equation canonique : chaque worker se compare a sa propre erreur courante. C’est un exemple typique de la difficulte a transposer une métaheuristique continue (PSO) vers un espace discret : on garde l’idee d’essaim (exploration parallele + memoire de la meilleure solution), mais le mécanisme de deplacement est entierement repense (echanges locaux + fusion + redemarrages).
Installation
pip install mealpy numpy matplotlib
# Importsimport numpy as npimport timeimport randomfrom typing import List, Tuple, Optionalimport matplotlib.pyplot as plt# mealpy : moteur de metaheuristiques de production (requirements.txt de la serie Sudoku).# Le corps du notebook reste en PSO manuel (pedagogie) ; la tranche finale (#10382)# invoque mealpy comme moteur de reference. NB : mealpy 3.x a deplace Problem# (mealpy.problem -> mealpy.utils.problem) -- l'ancien chemin echouait silencieusement# dans le try/except, d'ou l'ancien output "mealpy non installe".try:from mealpy import PSOfrom mealpy.utils.problem import Problemfrom mealpy.utils.space import FloatVar MEALPY_AVAILABLE =Trueprint("mealpy importe avec succes")exceptImportError: MEALPY_AVAILABLE =Falseprint("mealpy non installe - environnement a reparer (pip install mealpy==3.0.2, cf requirements.txt)")
mealpy importe avec succes
Interpretation : Imports et dependances
Sortie obtenue : mealpy importe avec succes (3.0.2)
Bibliotheques importees :
Bibliotheque
Usage
Alternatives
numpy
Manipulation de matrices 9x9
-
time
Mesure des performances
-
random
Generation aleatoire
np.random
matplotlib
Visualisation (optionnel)
-
mealpy
Moteur de metaheuristiques de production
Implementation manuelle (corps du notebook)
Points cles : 1. Indépendance : Le corps du notebook implemente PSO manuellement ; mealpy est invoque en fin de parcours comme moteur de production 2. NumPy : Essentiel pour la representation matricielle 3. Typing : Annotations de type pour la clarte du code
Note technique : L’implementation manuelle de PSO permet de comprendre l’algorithme en detail. La bibliotheque mealpy fournit une implementation generique mais moins adaptee aux specifics du Sudoku (encodage par blocs).
Configuration du chemin vers les fichiers de puzzles.
# Configuration du chemin vers les puzzlesimport osfrom pathlib import Path# Resolution robuste du repertoire du notebook (CWD peut differer sous Papermill)_here = Path().resolve()for _candidate in [_here, _here /"MyIA.AI.Notebooks"/"Sudoku", _here.parent /"Sudoku"]:if (_candidate /"Puzzles").exists(): _here = _candidatebreakNOTEBOOK_DIR = _herePUZZLES_DIR = NOTEBOOK_DIR /"Puzzles"# Suffixe repo-relatif dans les outputs : jamais de chemin machine commite (leak #3436, triage C)if PUZZLES_DIR.exists():print(f"Dossier Puzzles: {'/'.join(PUZZLES_DIR.parts[-3:])}")else:print("ATTENTION: Dossier Puzzles non trouve") PUZZLES_DIR = Path(os.getcwd()) /"Puzzles"
Dossier Puzzles: MyIA.AI.Notebooks/Sudoku/Puzzles
Interpretation : Configuration de l’environnement
Sortie obtenue : Dossier Puzzles trouve et chemin affiche
Structure des fichiers :
Élément
Chemin
Rôle
NOTEBOOK_DIR
D:\Dev\CoursIA\MyIA.AI.Notebooks\Sudoku
Racine de la serie
PUZZLES_DIR
.../Sudoku/Puzzles
Conteneur des fichiers de puzzles
Points cles : 1. Pathlib : Utilisation de Path pour la portabilite (Windows/Linux) 2. Verification : Test d’existence avant d’acceder aux fichiers 3. Fallback : Si dossier absent, utilise le repertoire courant
Note technique : L’utilisation de pathlib.Path est recommandee en Python 3 pour manipuler les chemins de fichiers de maniere orientee objet. Elle gere automatiquement les separateurs (/ ou ) selon l’OS.
1. Classe SudokuGrid
Representation d’une grille de Sudoku avec méthodes pour calculer le nombre d’erreurs.
class SudokuGrid:"""Representation d'une grille de Sudoku 9x9."""def__init__(self, grid: Optional[np.ndarray] =None):if grid isNone:self.cells = np.zeros((9, 9), dtype=int)else:self.cells = grid.copy()@classmethoddef from_string(cls, s: str) ->'SudokuGrid': s = s.replace('.', '0').replace(' ', '').replace('\n', '')iflen(s) !=81:raiseValueError(f"La chaine doit avoir 81 caracteres") grid = cls() grid.cells = np.array([int(c) for c in s], dtype=int).reshape(9, 9)return griddef clone(self) ->'SudokuGrid':return SudokuGrid(self.cells.copy())def count_errors(self) ->int:"""Compte le nombre total d'erreurs (doublons).""" errors =0# Erreurs par lignefor i inrange(9): row =self.cells[i, :] row_nonzero = row[row >0] errors +=len(row_nonzero) -len(np.unique(row_nonzero))# Erreurs par colonnefor j inrange(9): col =self.cells[:, j] col_nonzero = col[col >0] errors +=len(col_nonzero) -len(np.unique(col_nonzero))# Erreurs par bloc 3x3for box_row inrange(3):for box_col inrange(3): block =self.cells[box_row*3:(box_row+1)*3, box_col*3:(box_col+1)*3].flatten() block_nonzero = block[block >0] errors +=len(block_nonzero) -len(np.unique(block_nonzero))return errorsdef is_solved(self) ->bool:"""Verifie si la grille est resolue."""if np.any(self.cells ==0):returnFalsereturnself.count_errors() ==0def__str__(self) ->str: lines = []for r inrange(9):if r >0and r %3==0: lines.append('-'*21) row_str =''for c inrange(9):if c >0and c %3==0: row_str +='| ' val =self.cells[r, c] row_str += (str(val) if val !=0else'.') +' ' lines.append(row_str)return'\n'.join(lines)def load_puzzles(filepath: str, max_puzzles: int=None) -> List[str]: puzzles = []withopen(filepath, 'r') as f:for line in f: line = line.strip()iflen(line) >=81: puzzles.append(line[:81])if max_puzzles andlen(puzzles) >= max_puzzles:breakreturn puzzles# Charger les puzzleseasy_puzzles = load_puzzles(str(PUZZLES_DIR /'Sudoku_Easy51.txt'), max_puzzles=5)print(f"Puzzles charges: {len(easy_puzzles)}")test_grid = SudokuGrid.from_string(easy_puzzles[0])print("\nGrille de test:")print(test_grid)
Sortie obtenue : 5 puzzles charges, grille de test affichée
Composants de la classe SudokuGrid :
Méthode
Rôle
Complexite
from_string()
Parse une chaîne de 81 caractères
O(1)
count_errors()
Compte doublons lignes/colonnes/blocs
O(9*9) = O(1)
is_solved()
Verifie si grille valide
O(1)
clone()
Copie profonde
O(1)
Grille de test : - Format : 9x9 avec points pour les cases vides - Separateurs visuels : lignes tous les 3 blocs - Cases vides : 36 (sur 81)
Points cles : 1. Representation compacte : Chaîne de 81 caractères pour le stockage 2. Validation efficace : Utilisation de np.unique pour detecter les doublons 3. Affichage lisible : Separateurs visuels pour les blocs 3x3
Note technique : La méthode count_errors() compte les doublons dans chaque ligne, colonne et bloc. Une grille resolue a 0 erreurs. Cette fonction sera la base de notre fonction de fitness pour le PSO.
2. Approches PSO pour Sudoku
Il existe plusieurs facons d’encoder un Sudoku pour PSO. Nous allons explorer deux approches :
Approche 1 : Encodage direct par cellules
Chaque particule contient 81 valeurs (1-9) pour chaque cellule.
Approche 2 : Encodage par permutations de blocs
Chaque bloc 3x3 est initialise avec les valeurs correctes, puis PSO melange les permutations.
3. Implementation manuelle du PSO
Nous implementons un PSO spécifique au Sudoku, sans utiliser mealpy, pour mieux comprendre l’algorithme.
class MatrixHelperPSO:"""Helper pour la manipulation des matrices Sudoku.""" SIZE =9 BLOCK_SIZE =3@staticmethoddef corner(block: int) -> Tuple[int, int]:"""Retourne le coin superieur gauche d'un bloc.""" r = (block //3) *3 c = (block %3) *3return (r, c)@staticmethoddef random_matrix(rng: random.Random, problem: np.ndarray) -> np.ndarray:"""Cree une matrice aleatoire valide par bloc.""" result = problem.copy()for block inrange(9): corner_r, corner_c = MatrixHelperPSO.corner(block) values =list(range(1, 10))# Retirer les valeurs deja presentesfor r inrange(corner_r, corner_r +3):for c inrange(corner_c, corner_c +3): val = problem[r, c]if val !=0and val in values: values.remove(val)# Melanger rng.shuffle(values)# Remplir les cellules vides pointer =0for r inrange(corner_r, corner_r +3):for c inrange(corner_c, corner_c +3):if result[r, c] ==0: result[r, c] = values[pointer] pointer +=1return result@staticmethoddef neighbor_matrix(rng: random.Random, problem: np.ndarray, matrix: np.ndarray) -> np.ndarray:"""Genere un voisin par echange dans un bloc.""" result = matrix.copy() block = rng.randint(0, 8) corner_r, corner_c = MatrixHelperPSO.corner(block) cells = []for r inrange(corner_r, corner_r +3):for c inrange(corner_c, corner_c +3):if problem[r, c] ==0: cells.append((r, c))iflen(cells) <2:return result k1 = rng.randint(0, len(cells) -1) k2 = (k1 + rng.randint(1, len(cells) -1)) %len(cells) r1, c1 = cells[k1] r2, c2 = cells[k2] result[r1, c1], result[r2, c2] = result[r2, c2], result[r1, c1]return result@staticmethoddef merge_matrices(rng: random.Random, m1: np.ndarray, m2: np.ndarray) -> np.ndarray:"""Fusionne deux matrices par blocs.""" result = m1.copy()for block inrange(9):if rng.random() <0.50: corner_r, corner_c = MatrixHelperPSO.corner(block)for r inrange(corner_r, corner_r +3):for c inrange(corner_c, corner_c +3): result[r, c] = m2[r, c]return resultprint("MatrixHelperPSO defini.")
MatrixHelperPSO defini.
Interpretation : Helpers matriciels pour PSO
Sortie obtenue : Classe MatrixHelperPSO définie
Opérations cles :
Méthode
Rôle
Stratégie
random_matrix
Initialisation
Remplit chaque bloc 3x3 avec 1-9
neighbor_matrix
Voisinage
Echange 2 cases dans un bloc
merge_matrices
Crossover
Fusionne 2 matrices par blocs (50%)
Points cles : 1. Encodage par blocs : Chaque bloc 3x3 contient toujours 1-9 (pas d’erreur de bloc) 2. Respect des contraintes : Les cases fixes du puzzle ne sont jamais modifiees 3. Efficacite : Opérations locales (un bloc) pour limiter les changements
Note technique : L’approche par blocs reduit drastiquement l’espace de recherche. Au lieu de 9^81 possibilites, on a (9!)^9 configurations, et chaque bloc est déjà valide localement.
Classe PSO principale utilisant les helpers matriciels.
class SudokuPSO:"""Representation d'une grille Sudoku avec calcul d'erreur."""def__init__(self, cell_values: np.ndarray):self.cell_values = cell_values.copy()@staticmethoddef new(cell_values: np.ndarray) ->'SudokuPSO':return SudokuPSO(cell_values.copy())@propertydef error(self) ->int:"""Compte les erreurs (valeurs manquantes dans lignes/colonnes/blocs)."""returnself._count_errors(True) +self._count_errors(False)def _count_errors(self, count_by_row: bool) ->int: errors =0for i inrange(9): counts = np.zeros(9, dtype=int)for j inrange(9): cell_value =self.cell_values[i, j] if count_by_row elseself.cell_values[j, i]if cell_value >0: counts[cell_value -1] +=1for k inrange(9):if counts[k] ==0: errors +=1return errorsprint("SudokuPSO defini.")
SudokuPSO defini.
Interpretation : Fonction d’evaluation PSO
Sortie obtenue : Classe SudokuPSO définie
Mécanisme de calcul d’erreur :
Type de verification
Méthode
Erreur comptee
Lignes
Valeurs manquantes (1-9)
+1 par valeur absente
Colonnes
Valeurs manquantes (1-9)
+1 par valeur absente
Total
Somme lignes + colonnes
0 a 162
Points cles : 1. Optimisation : On ne verifie PAS les blocs 3x3 (déjà valides par construction) 2. Fitness inverse : Erreur = 0 signifie solution valide 3. Efficacite : Comptage par tableaux d’occurrences (O(1) par ligne/colonne)
Note technique : Cette fonction d’evaluation est spécifique a l’encodage par blocs. Puisque chaque bloc contient exactement les chiffres 1-9, il ne peut pas y avoir d’erreur de bloc. Les seules erreurs possibles sont les conflits entre lignes et colonnes.
Implementation de l’algorithme PSO avec ses variantes de particules.
from enum import Enumclass OrganismType(Enum): WORKER =1 EXPLORER =2class Organism:"""Representation d'une particule (organisme)."""def__init__(self, org_type: OrganismType, matrix: np.ndarray, error: int, age: int=0):self.type= org_typeself.matrix = matrix.copy()self.error = errorself.age = ageprint("Classes OrganismType et Organism definies.")
Classes OrganismType et Organism definies.
Interpretation : Modelisation des particules
Sortie obtenue : Classes OrganismType et Organism définies
Structure d’une particule :
Attribut
Type
Rôle
type
OrganismType
WORKER ou EXPLORER
matrix
np.ndarray
Grille 9x9 representant la position
error
int
Nombre d’erreurs (fitness inverse)
age
int
Epochs sans amelioration
Points cles : 1. Dualite des rôles : Workers exploitent localement, explorers cherchent globalement 2. Age comme indicateur : Permet de detecter la stagnation 3. Fitness inverse : On minimise l’erreur (0 = solution valide)
Note technique : Le typage Python avec Enum permet une distinction claire des comportements. Les workers utilisent neighbor_matrix (echange dans un bloc), les explorers utilisent random_matrix (reinitialisation complete).
Points cles : 1. Double stratégie : Workers exploitent, explorers decouvrent 2. Fusion intelligente : Le pire worker est remplace par la fusion des meilleurs 3. Redemarrages : Plusieurs tentatives pour eviter les optima locaux
Note technique : La fusion par blocs (50% de probabilite) permet de combiner les bonnes parties de deux solutions sans detruire les blocs corrects. C’est une forme de crossover preserve les blocs 3x3 valides.
5. Test sur un Puzzle Facile
print("=== Test : Puzzle Facile ===")puzzle = SudokuGrid.from_string(easy_puzzles[0])print("Puzzle original:")print(puzzle)solver = PSOSudokuSolver( num_organisms=200, max_epochs=3000, max_restarts=10)start = time.time()result, solved = solver.solve(puzzle)elapsed = time.time() - startprint(f"\nSolution trouvee en {elapsed:.2f}s:")print(result)print(f"\nSolution valide: {solved}")
Le PSO peut trouver la solution pour les puzzles faciles
Temps
Variable selon la configuration
Workers vs Explorers
Les workers exploitent, les explorers decouvrent
Point cle : Le PSO combine l’exploitation (convergence vers gBest) avec l’exploration (recherche aleatoire).
Exercice : Decomposition des erreurs par type
Contexte
Le PSO minimise une fonction d’erreur globale. Comprendre quels types de contraintes sont violes aide a diagnostiquer les difficultes de resolution.
Objectif
Implementez la fonction qui decompose le nombre de violations en erreurs de lignes, colonnes et blocs.
Ce que la fonction doit retourner
Un dictionnaire avec 3 cles : , , , chacune contenant le nombre de doublons trouves.
Indices : - Pour chaque ligne : comptez les valeurs en double avec - Un doublon = pour chaque valeur - Les blocs 3x3 sont indexes par
def count_errors_by_type(grid: np.ndarray) ->dict:"""Decompose les erreurs d'une grille par type de contrainte. Args: grid: Grille 9x9 en array numpy Returns: Dictionnaire {"rows": int, "cols": int, "blocks": int} """# Etape 1 : Compter les doublons par ligne# Etape 2 : Compter les doublons par colonne# Etape 3 : Compter les doublons par bloc 3x3return {"rows": 0, "cols": 0, "blocks": 0} # TODO etudiant# Testez votre fonctionprint("Exercice count_errors_by_type a completer")
Interpretation : Benchmark PSO sur plusieurs puzzles
Sortie obtenue : Résultats sur 2 puzzles de difficulte croissante. Les temps de résolution sont mesurés en direct par la cellule 28 (benchmark_pso, un chrono par puzzle) — re-exécutez-la pour les valeurs courantes (PSO stochastique : grande variance selon l’initialisation aléatoire).
Puzzle
Cases vides
Statut
Redemarrages
1
36
OK
1/5
2
49
OK
1/5
Bilan global : - Taux de succes : 2/2 (100%)
Points cles : 1. Correlation difficulte/temps : Plus de cases vides = temps de résolution plus long (mesuré en direct par la cellule 28) 2. Ecart de difficulte : le puzzle 2 (49 vides) est nettement plus lent que le puzzle 1 (36 vides) — mesurez le rapport en direct via la cellule 28 3. Convergence progressive : On observe l’erreur descendre (29 -> 4 -> 0)
Note technique : Les messages “Epoch X, Best error: Y” montrent que l’algorithme converge souvent vers quelques erreurs (2 a 4) avant d’atteindre 0. Cela correspond a une configuration presque valide ou quelques blocs doivent etre reorganises.
Exercice : Detection de convergence
Contexte
Le PSO s’arrete après un nombre fixe d’epochs. Mais si la solution stagne, continuer est inutile. Un critere de convergence permet d’arreter plus tot.
Objectif
Implementez la fonction qui detecte si le PSO a converge, en verifiant que l’erreur n’a pas baisse depuis un nombre donne d’epochs.
Ce que la fonction doit verifier
La liste contient au moins éléments
La différence entre l’erreur la plus recente et celle d’il y a epochs est inferieure a
Indices : - Comparez avec - Si la liste a moins de éléments, retournez (pas assez de données)
def has_converged(error_history: list, patience: int=50, threshold: float=0.01) ->bool:"""Detecte si le PSO a converge. Args: error_history: Liste des erreurs par epoch patience: Nombre d'epochs sans amelioration avant convergence threshold: Seuil de variation considere comme stagnation Returns: True si le PSO a converge (stagnation detectee) """# Etape 1 : Verifier que la liste a assez d'elements# Etape 2 : Comparer erreur recente vs erreur d'il y a patience epochs# Etape 3 : Retourner True si la variation est inferieure au seuilreturnFalse# TODO etudiant : implementez la detection# Testez votre fonctionprint("Exercice has_converged a completer")
=== Test avec differents parametres ===
Tentative 1/5
Epoch 0, Best error: 23
Conservateur: 3.03s - Succes
Tentative 1/10
Epoch 0, Best error: 23
Standard: 3.74s - Succes
Tentative 1/20
Epoch 0, Best error: 21
Agressif: 6.63s - Succes
Interpretation : Impact des paramètres PSO
Sortie obtenue : Temps de resolution selon trois configurations. Les temps sont mesurés en direct par la cellule 33 (un chrono par configuration) — re-exécutez-la pour les valeurs courantes (PSO stochastique).
Configuration
Organismes
Epochs
Restarts
Statut
Conservateur
100
2000
5
Succes
Standard
200
3000
10
Succes
Agressif
300
5000
20
Succes
Points cles : 1. Configuration conservateur : Plus rapide pour ce puzzle facile 2. Toutes les configurations reussissent sur les puzzles faciles 3. Le temps de resolution ne croit pas lineairement avec les paramètres
Note technique : Pour les puzzles faciles, une configuration legere suffit. Les paramètres agressifs sont utiles pour les puzzles difficiles ou l’essaim peut stagner dans des optima locaux.
Exercice : Ratio workers/explorers adaptatif
Contexte
Dans le solveur PSO, le paramètre worker_ratio fixe la proportion de workers (exploitation) par rapport aux explorers (exploration). Une valeur statique de 0.90 signifie 90% de workers et 10% d’explorers, independamment de la progression de la recherche.
Or, les besoins en exploration vs exploitation changent au cours de la resolution : - Debut : Il faut beaucoup d’exploration pour couvrir l’espace de recherche - Milieu : Un equilibre entre exploration et exploitation - Fin : L’exploitation domine pour affiner la meilleure solution
Objectif
Implementez la fonction adaptive_worker_ratio qui calcule dynamiquement le ratio workers/explorers en fonction de la progression de l’erreur.
Résultats attendus
Situation
current_error
initial_error
worker_ratio attendu
Debut (0% progres)
20
20
0.700
Mi-parcours (50%)
10
20
0.825
Pres du but (90%)
2
20
0.925
Erreur nulle (100%)
0
20
0.950
Indice : La formule est un interpolation lineaire : plus l’erreur baisse (bonne progression), plus on augmente le ratio de workers. Utilisez max() et min() pour borner le résultat.
def adaptive_worker_ratio(current_error: int, initial_error: int, min_ratio: float=0.70, max_ratio: float=0.95) ->float:"""Calcule un worker_ratio adaptatif en fonction de la progression. Quand l'erreur baisse (bonne exploitation), on augmente le ratio de workers. Quand l'erreur stagne ou remonte, on augmente les explorers. Args: current_error: Erreur actuelle de la meilleure solution initial_error: Erreur a l'initialisation de l'essaim min_ratio: Ratio minimum d'explorers (exploration renforcee) max_ratio: Ratio maximum de workers (exploitation renforcee) Returns: Worker_ratio adapte entre min_ratio et max_ratio """# Etape 1 : Calculer le taux de progression (0.0 a 1.0)# Indice : progression = 1.0 - (current_error / initial_error)# Attention au cas ou initial_error == 0# Etape 2 : Calculer le ratio lineairement entre min_ratio et max_ratio# Indice : ratio = min_ratio + progression * (max_ratio - min_ratio)# Etape 3 : Retourner le ratio clamp entre min_ratio et max_ratioreturn0.90# TODO etudiant : remplacer par le calcul adaptatif# Tests pour verifier votre implementation# Test 1 : Erreur initiale -> ratio par defaut (peu de progression)ratio_start = adaptive_worker_ratio(current_error=20, initial_error=20)print(f"Debut (erreur 20/20) : worker_ratio = {ratio_start:.3f}")# Test 2 : Mi-parcours -> ratio intermediaireratio_mid = adaptive_worker_ratio(current_error=10, initial_error=20)print(f"Mi-parcours (erreur 10/20) : worker_ratio = {ratio_mid:.3f}")# Test 3 : Pres de la solution -> beaucoup de workers (exploitation)ratio_end = adaptive_worker_ratio(current_error=2, initial_error=20)print(f"Pres de la solution (erreur 2/20) : worker_ratio = {ratio_end:.3f}")print("Exercice adaptive_worker_ratio a completer")
Debut (erreur 20/20) : worker_ratio = 0.900
Mi-parcours (erreur 10/20) : worker_ratio = 0.900
Pres de la solution (erreur 2/20) : worker_ratio = 0.900
Exercice adaptive_worker_ratio a completer
8. Comparaison avec GA et SA
print("=== Comparaison des metaheuristiques ===")print("\n| Aspect | PSO | Algorithme Genetique | Recuit Simule |")print("|--------|-----|---------------------|---------------|")print("| Inspiration | Oiseaux/poissons | Evolution biologique | Thermodynamique |")print("| Population | Multiple | Multiple | Unique |")print("| Exploration | Workers + Explorers | Mutation | Temperature |")print("| Exploitation | Convergence vers gBest | Crossover | Refroidissement |")print("| Performance Sudoku | ~1-5s | ~1-10s | ~2-5s |")print("\n**Note**: Ces valeurs sont approximatives et dependent des parametres.")
=== Comparaison des metaheuristiques ===
| Aspect | PSO | Algorithme Genetique | Recuit Simule |
|--------|-----|---------------------|---------------|
| Inspiration | Oiseaux/poissons | Evolution biologique | Thermodynamique |
| Population | Multiple | Multiple | Unique |
| Exploration | Workers + Explorers | Mutation | Temperature |
| Exploitation | Convergence vers gBest | Crossover | Refroidissement |
| Performance Sudoku | ~1-5s | ~1-10s | ~2-5s |
**Note**: Ces valeurs sont approximatives et dependent des parametres.
Interpretation : Comparaison des métaheuristiques
Sortie obtenue : Tableau comparatif des caractéristiques de PSO, GA et SA
Aspect
PSO
Algorithme Génétique
Recuit Simule
Inspiration
Oiseaux/poissons
Evolution biologique
Thermodynamique
Population
Multiple (essaim)
Multiple (generation)
Unique (solution courante)
Exploration
Workers + Explorers
Mutation
Temperature
Exploitation
Convergence vers gBest
Crossover
Refroidissement
Points cles : 1. Performances variables : les trois métaheuristiques (PSO, GA, SA) ont des temps de résolution fortement variables selon l’initialisation — comparez les moyennes en ré-exécutant les cellules benchmark de chaque notebook (Sudoku-03 GA, Sudoku-04 SA, Sudoku-05 PSO) 2. L’architecture a double rôle (workers/explorers) offre un bon equilibre 3. Le choix de la méthode depend du type de puzzle et des contraintes de temps
Note technique : PSO est particulierement efficace pour les problemes ou l’espace de recherche peut etre partitionne (ici, par blocs 3x3).
Exercice : Analyse du PSO
Exemple 1 : Analyse de convergence
Modifiez le solveur pour enregistrer l’evolution de best_error au fil des epochs et affichez un graphique de convergence.
Exemple 2 : Adaptation dynamique
Implementez une adaptation dynamique de worker_ratio qui augmente l’exploration quand la solution stagne.
Exemple 3 : Hybride PSO-SA
Combinez PSO avec le recuit simule : utilisez SA pour affiner les solutions trouvees par PSO.
Exercice : PSO Hybride avec Recherche Locale
Enonce
Implementez un solveur PSO hybride qui, lorsque le meilleur organisme stagne pendant plus de stagnation_limit epochs, applique une recherche locale intensive sur sa solution.
local_search_improve(matrix, problem, max_attempts) : Pour chaque bloc, essaye tous les echanges de paires de cases libres et garde le meilleur (descente de gradient)
HybridPSOSudokuSolver : Etend PSOSudokuSolver en detectant la stagnation et en appelant local_search_improve
Résultats attendus
Le PSO hybride devrait avoir moins de redemarrages et etre plus fiable sur les puzzles difficiles.
Indice :
La stagnation se detecte en comptant les epochs sans amelioration de best_error. Lorsque ce compteur depasse stagnation_limit, appliquer la recherche locale et reinitialiser le compteur.
def local_search_improve(matrix: np.ndarray, problem: np.ndarray, max_attempts: int=100) -> np.ndarray:""" Recherche locale intensive : pour chaque bloc, essaye tous les echanges de paires de cases libres et garde le meilleur. Args: matrix: Grille courante problem: Grille initiale (definit les cases fixes) max_attempts: Nombre de passes maximum Returns: Grille amelioree (ou identique si aucune amelioration) """# TODO : Implementer la recherche locale# 1. Calculer l'erreur courante# 2. Pour chaque passe (jusqu'a max_attempts) :# improved = False# Pour chaque bloc (0-8) :# Lister les cases libres du bloc (cells ou problem[r,c] == 0)# Pour chaque paire (i, j) de cases libres :# Echanger les valeurs temporairement# Calculer la nouvelle erreur# Si meilleure : garder l'echange, improved = True# Sinon : annuler l'echange# Si not improved : break (optimum local atteint)# 3. Retourner la grille amelioreepassclass HybridPSOSudokuSolver(PSOSudokuSolver):""" PSO hybride avec recherche locale lors de la stagnation. Quand best_error ne change pas pendant stagnation_limit epochs, applique local_search_improve sur la meilleure solution. """def__init__(self, stagnation_limit: int=500, **kwargs):super().__init__(**kwargs)self.stagnation_limit = stagnation_limitdef _solve_internal(self, sudoku: SudokuPSO, rng: random.Random) -> SudokuPSO:"""Surcharge avec detection de stagnation + recherche locale."""# TODO : Reprendre la logique de PSOSudokuSolver._solve_internal()# et ajouter :# - epochs_without_improvement = 0# - Apres chaque mise a jour : si best_error n'a pas change, incrementer# - Si epochs_without_improvement >= stagnation_limit :# * Appliquer local_search_improve sur best_solution.cell_values# * Si amelioration : reinitialiser epochs_without_improvementpass# Test (decommenter une fois implemente)# hybrid = HybridPSOSudokuSolver(stagnation_limit=300, num_organisms=200, max_epochs=3000, max_restarts=5)# test_puzzle = SudokuGrid.from_string(easy_puzzles[0])# result, solved = hybrid.solve(test_puzzle)# print(f"Hybride: {solved}")print("Exercice PSO hybride a implementer !")
Exercice PSO hybride a implementer !
Lecture du résultat : le moteur atteint, l’encodage reste le goulot
Ce que cette section établit : le notebook invoque désormais mealpy (OriginalPSO, 3 seeds seedés) comme moteur de production, sur la même instance que le jumeau C# (la chaîne 81 caractères imprimée des deux côtés en fait la preuve). Les DEUX jumeaux de la paire atteignent le même moteur — chacun par sa voie (natif Python ici, pont PythonNet côté C#).
Ce qu’elle ne dit pas : que mealpy « résout » le Sudoku de façon fiable. Sur ce run, les résidus vont de 0 (seed 99 — l’instance facile est résolue) à 14 conflits (seed 7) : un zéro atteint une fois sur trois n’est pas une convergence systématique, et le solveur manuel du corps (fusion bloc-par-bloc + redémarrages) résout le même puzzle à chaque exécution. Ce n’est pas un défaut du moteur : c’est la leçon d’encodage déjà énoncée par le jumeau C# (le chromosome permutation-de-ligne de GeneticSharp converge là où l’encodage naïf stagne). Un moteur de production ne rattrape pas un encodage qui ne préserve pas la structure — l’infrastructure (population, vitesse, pbest/gbest, bornes) est le livrable du moteur, pas la solution garantie.
# === Tranche (#10382) : mealpy OriginalPSO sur le meme puzzle facile (easy_puzzles[0]) ===# Instance partagee avec le jumeau C# (Tranche 3) : la chaine 81 ci-dessous est imprimee par# les DEUX jumeaux -- l'egalite des chaines dans les outputs committees fait la preuve# cross-twin que le meme probleme est attaque par le meme moteur des deux cotes.import mealpyPUZZLE_81 = easy_puzzles[0].replace(' ', '').replace('\n', '')assertlen(PUZZLE_81) ==81print(f"Instance (81 chiffres, 0 = case vide) : {PUZZLE_81}")print(f"mealpy {mealpy.__version__} | numpy {np.__version__}")class SudokuMealpyProblem(Problem):"""Decodage rang par ligne : cellules vides d'une ligne triees par x croissant recoivent les chiffres manquants de la ligne, tries par ordre croissant."""def__init__(self, puzzle_str):self.grid0 = np.array([int(c) for c in puzzle_str.replace('.', '0')], dtype=int).reshape(9, 9)self.empty = [(r, c) for r inrange(9) for c inrange(9) ifself.grid0[r, c] ==0]self.missing = {}for r inrange(9): present =set(self.grid0[r, :]) - {0}self.missing[r] =sorted(set(range(1, 10)) - present) n =len(self.empty)super().__init__(bounds=FloatVar(lb=[0.0] * n, ub=[1.0] * n), minmax="min", log_to=None) # log_to=None : silence le logger epoch par epoch de mealpydef decode(self, x): g =self.grid0.copy() by_row = {}for k, (r, c) inenumerate(self.empty): by_row.setdefault(r, []).append((x[k], c))for r, items in by_row.items(): order = [c for _, c insorted(items)]for digit, c inzip(self.missing[r], order): g[r, c] = digitreturn gdef obj_func(self, solution): g =self.decode(np.asarray(solution)) errors =0for j inrange(9): errors +=9-len(np.unique(g[:, j]))for br inrange(3):for bc inrange(3): blk = g[br *3:(br +1) *3, bc *3:(bc +1) *3].flatten() errors +=9-len(np.unique(blk))returnfloat(errors)probleme_mealpy = SudokuMealpyProblem(PUZZLE_81)print(f"Dimensions continues : {len(probleme_mealpy.empty)} (une par cellule vide)")print()print("mealpy OriginalPSO, 3 seeds (meme budget que le jumeau C# : epoch=300, pop=60)")for seed in (42, 7, 99): t0 = time.perf_counter() modele = PSO.OriginalPSO(epoch=300, pop_size=60, seed=seed) meilleur = modele.solve(probleme_mealpy) dt = time.perf_counter() - t0print(f" seed={seed:2d} : erreur residuelle = {meilleur.target.fitness:.0f} conflits [{dt:.1f} s]")
Le corps de ce notebook implement PSO manuellement (numpy) : c’est le moteur pédagogique. Le principe de parité lib-vs-lib — chaque jumeau atteint un moteur de production de son écosystème — demande ici un moteur dédié. Pour cette paire, le moteur est mealpy (PyPI, MIT, 233+ métaheuristiques) — invoqué ici nativement, et par le jumeau C# via son pont PythonNet.
Encodage miroir du chromosome GeneticSharp du jumeau C# : une coordonnée continue par cellule vide ; dans chaque ligne, les cellules vides reçoivent les chiffres manquants (triés) ordonnés par le tri croissant des coordonnées — lignes valides par construction, la fitness n’optimise que les conflits colonnes + blocs.
Resume et perspectives
Ce notebook a implemente le Particle Swarm Optimization (PSO) pour la resolution de Sudoku, avec un encodage par blocs 3x3 qui garantit la validite locale de chaque bloc. L’architecture a double rôle (Workers pour l’exploitation locale, Explorers pour l’exploration globale) offre un equilibre entre convergence et diversification. Les benchmarks ont montre une grande variabilite : de quelques secondes pour les puzzles faciles a plusieurs dizaines de secondes pour les grilles plus contraintes (mesurer en direct via la cellule benchmark 28, PSO stochastique), avec un taux de succes de 2/2 sur le benchmark execute ici. La stagnation frequente a quelques erreurs (2 a 4) illustre la difficulte des métaheuristiques a echapper aux optima locaux sur les problemes fortement contraints.
L’analyse des paramètres (taille de l’essaim, nombre d’epochs, redemarrages) a montre que la configuration “Conservatrice” suffit souvent pour les puzzles faciles, tandis que les puzzles difficiles necessitent une configuration “Agressive” avec un cout temporel significatif. L’exercice propose sur le PSO hybride avec recherche locale vise précisément a resoudre le problème de stagnation en combinant l’exploration globale de l’essaim avec une descente de gradient locale.
Le notebook suivant, Sudoku-06-AIMA-CSP-Python, aborde une approche déterministe : la programmation par contraintes (CSP) selon le cadre academique AIMA, avec les algorithmes AC-3, Forward Checking et MAC qui garantissent la resolution complete.