MGS-22 : MGS contre mealpy — le bench croisé lib-vs-lib sur le Sudoku
Jusqu’ici, les bancs de la série comparaient des composés entre eux — WOA contre EO contre Islands, tous construits dans le même moteur. Et quand Sudoku-05 confrontait le solveur maison à GeneticSharp puis à MetaGeneticSharp (Tranches 2-3), chaque moteur tournait dans son langage, avec sa propre fonction de coût et son propre budget — la comparaison était honnête en qualité, approximative en méthode.
Ce notebook fait le banc que l’axe 4 de l’issue #11977 demande : la même expérience, deux bibliothèques, deux langages, dans une seule exécution — MetaGeneticSharp (C#/.NET 9, DLLs du sous-module) contre mealpy 3.0.2 (Python/NumPy, la référence du catalogue open-source des métaheuristiques) — reliés par le pont PythonNet dans le kernel .NET. L’hypothèse de départ est celle du user, citée telle quelle : « possible que les perfs ne suivent pas mealpy sous NumPy optimisé, mais la comparaison sera intéressante ». Un résultat « MGS plus lent » est donc un livrable valide — à condition qu’il soit mesuré avec la méthode : mêmes grilles, mêmes budgets d’évaluations, graines nommées.
1. Le protocole apparié, pré-enregistré
Avant d’exécuter quoi que ce soit, le protocole — parce qu’un bench dont les règles sont écrites après les résultats n’est pas un bench.
Élément
Valeur (fixée d’avance)
Grille
Easy[0] de Sudoku_Easy51 — la même que MGS-21 et les Tranches 1-4 de Sudoku-05 : 36 cellules vides, 45 indices
Représentation
R1 : vecteur continu \([1,10)^{36}\), décodage par arrondi + clamp — le substrat continu, identique des deux côtés
Fonction de coût
conflits totaux (lignes + colonnes + blocs) — la même implémentée deux fois (C# et Python), égalité vérifiée par sanity check
Moteurs
PSO canonique à vélocité : composé ParticleSwarmOptimization de MetaGeneticSharp contre OriginalPSO de mealpy
Budget
~8 000 évaluations par course : population 50 × 160 générations/epochs, évals réellement consommées instrumentées des deux côtés
Graines
{0, 1, 7, 42} — nommées, une par course, 4 courses par moteur
Mesures
(a) qualité : conflits finaux, médiane + min-max sur 4 graines · (b) vitesse : ms par course, médiane de 3 répétitions par graine (amendement post-run-pilote, motivé en §2) · (c) coût par évaluation : ms/éval sur 500 évaluations de fitness isolées, médiane de 5 répétitions
Critères de lecture, pré-enregistrés. (1) Qualité : un moteur domine si sa médiane de conflits est strictement inférieure ET que ses maxima restent sous les minima de l’autre ; sinon « comparable ». (2) Vitesse : rapport des ms/éval moyens. (3) Le verdict global reprend l’hypothèse user — chaque axe est rapporté séparément, aucune compensation entre axes (un moteur plus lent ET de même qualité se déclare comme tel).
Pourquoi ce design est le seul honnête : comparer « MGS 37 s contre mealpy 3 s » pris sur deux notebooks, deux fonctions de coût voisines et deux budgets différents ne prouve rien — c’est la comparaison que la Tranche 3 de Sudoku-05 subissait (37 033 ms pour 200 000 évaluations, budget et implémentation non appariés). Ici chaque facteur autre que la bibliothèque (grille, coût, budget, graine) est cloué.
// === MGS-22 : socle commun — DLLs MGS, grille de référence, fonction de coût ===// Même socle que MGS-21 : la représentation R1 (continu + arrondi) est le substrat du bench.#r "../../MetaGeneticSharp/src/MetaGeneticSharp.Domain/bin/Debug/net9.0/GeneticSharp.Infrastructure.Framework.dll"#r "../../MetaGeneticSharp/src/MetaGeneticSharp.Domain/bin/Debug/net9.0/MetaGeneticSharp.Infrastructure.dll"#r "../../MetaGeneticSharp/src/MetaGeneticSharp.Domain/bin/Debug/net9.0/MetaGeneticSharp.Domain.dll"using MetaGeneticSharp;using GeneticSharp;using System.Diagnostics;// Grille facile Easy[0] de Sudoku_Easy51.txt — la MÊME que MGS-21 et Sudoku-5 Tranches 1-4.publicstaticstring PuzzleLine22 ="902005403100063025508407060026309001057010290090670530240530600705200304080041950";publicstaticint[,]ParsePuzzle22(){var g =newint[9,9];for(int i =0; i <81; i++) g[i /9, i %9]= PuzzleLine22[i]-'0';return g;}// Fonction de coût du bench : conflits totaux (lignes + colonnes + blocs) sur grille PLEINE.// Renvoie 0 ssi résolu. Le côté Python réimplémente exactement ce comptage.publicstaticintCountConflicts22(int[,] g){int conflicts =0;for(int i =0; i <9; i++){var row =new HashSet<int>();var col =new HashSet<int>();var blk =new HashSet<int>();for(int j =0; j <9; j++){if(!row.Add(g[i, j])) conflicts++;if(!col.Add(g[j, i])) conflicts++;int br =3*(i /3)+ j /3, bc =3*(i %3)+ j %3;if(!blk.Add(g[br, bc])) conflicts++;}}return conflicts;}publicstaticintCountEmpty22(int[,] p){int n =0;foreach(var v in p)if(v ==0) n++;return n;}publicstatic List<(int r,int c)>EmptyCells22(int[,] p){var l =new List<(int,int)>();for(int r =0; r <9; r++)for(int c =0; c <9; c++)if(p[r, c]==0) l.Add((r, c));return l;}// Décodage R1 : arrondi + clamp vers 1..9 sur les cellules vides, ordre de lecture.publicstaticint[,]DecodeR1_22(double[] genes){var Puzzle =ParsePuzzle22();var empties =EmptyCells22(Puzzle);var g =(int[,])Puzzle.Clone();for(int k =0; k < empties.Count; k++) g[empties[k].r, empties[k].c]= Math.Max(1, Math.Min(9,(int)Math.Round(genes[k])));return g;}var Puzzle22 =ParsePuzzle22();Console.WriteLine($"Grille de référence : {CountEmpty22(Puzzle22)} cellules vides, "+ $"{81 - CountEmpty22(Puzzle22)} indices fixes, {EmptyCells22(Puzzle22).Count} gènes R1.");
Grille de référence : 36 cellules vides, 45 indices fixes, 36 gènes R1.
Interprétation : le socle est posé
Rien de neuf côté C# — c’est voulu. Le chromosome R1 à 36 gènes continus et le compte de conflits totaux sont exactement ceux de MGS-21 : le présent bench hérite du substrat validé plutôt que d’en réintroduire un. La seule pièce nouvelle est DecodeR1_22 découplé du chromosome : le décodage vit comme une fonction pure sur un vecteur, parce que le côté Python devra appliquer exactement la même transformation aux solutions mealpy.
Un détail d’implémentation à ne pas laisser passer : (int)Math.Round en C# applique l’arrondi bancaire (les demi-entiers vont au pair), et int(round(...)) en Python aussi — mais ce n’est pas une raison pour croire les deux paysages identiques : sur des gènes tirés uniformément dans \([1,10)\), la probabilité de tomber exactement sur un demi-entier est nulle en théorie et négligeable en pratique. L’argument, pourtant, ne sera pas invoqué : la sanity check de la section suivante tranche sur des données réelles — trois vecteurs témoins coûtés indépendamment des deux côtés.
// === Moteur MGS : chromosome R1, fitness instrumentée, PSO canonique composé ===// Le PSO canonique à vélocité (axe 1 de #11977) : récurrence w/c1/c2 de Shi & Eberhart.publicclass SudokuR1Chromosome22 : ChromosomeBase{privateconstdouble LO =1.0, HI =10.0;publicSudokuR1Chromosome22():base(EmptyCells22(ParsePuzzle22()).Count){CreateGenes();}publicoverride Gene GenerateGene(int index)=>newGene(RandomizationProvider.Current.GetDouble(LO, HI));publicoverride IChromosome CreateNew()=>newSudokuR1Chromosome22();publicdouble[]ToGenes(){var v =newdouble[Length];for(int i =0; i < Length; i++) v[i]=(double)GetGene(i).Value;return v;}publicint[,]ToGrid()=>DecodeR1_22(ToGenes());}// Fitness instrumentée : budget réel + trajectoire best-so-far aux quarts du budget.publicclass SudokuR1Fitness22 : IFitness{publicstaticint Evals;publicstaticint Best;publicstaticint[] Checkpoints =newint[4];publicstaticint[] Thresholds =newint[4];publicstaticvoidReset(int expectedEvals){ Evals =0; Best =int.MaxValue; Checkpoints =newint[4]; Thresholds =new[]{1,2,3,4}.Select(q =>(int)Math.Ceiling(expectedEvals * q /4.0)).ToArray();}publicdoubleEvaluate(IChromosome chromosome){int conflicts =CountConflicts22(((SudokuR1Chromosome22)chromosome).ToGrid()); Evals++; Best = Math.Min(Best, conflicts);for(int i =0; i < Thresholds.Length; i++)if(Checkpoints[i]==0&& Evals >= Thresholds[i]) Checkpoints[i]= Best;return-conflicts;}}publicstaticclass Mgs22Host{publicstatic(int conflicts,int evals,double ms,double[] genes,int[] cp)RunPso(int seed,int popSize,int maxGens){ FastRandomRandomization.ResetSeed(seed);var compound = MetaHeuristicsService.CreateMetaHeuristicByName("ParticleSwarmOptimization", maxGens, popSize);var adam =newSudokuR1Chromosome22();var pop =newMetaPopulation(popSize, popSize, adam);var ga =newMetaGeneticAlgorithm( pop,newSudokuR1Fitness22(),newEliteSelection(),newUniformCrossover(0.5f),newUniformMutation(true), compound); ga.Termination=newGenerationNumberTermination(maxGens); SudokuR1Fitness22.Reset(popSize * maxGens);var sw = Stopwatch.StartNew(); ga.Start(); sw.Stop();var best =(SudokuR1Chromosome22)ga.BestChromosome;return(CountConflicts22(best.ToGrid()), SudokuR1Fitness22.Evals, sw.Elapsed.TotalMilliseconds, best.ToGenes(), SudokuR1Fitness22.Checkpoints.ToArray());}}var warmupMgs = Mgs22Host.RunPso(123,50,10);var demoMgs = Mgs22Host.RunPso(7,50,160);Console.WriteLine($"MGS PSO (graine 7, témoin) : {demoMgs.conflicts} conflits, "+ $"{demoMgs.evals} évaluations, {demoMgs.ms:F0} ms, "+ $"cp25/50/75/100={string.Join("/", demoMgs.cp)}.");
Trois choix de wiring portent la validité du bench :
Le compteur d’évaluations vit dans la fitness, pas dans un estimateur.SudokuR1Fitness22.Evals s’incrémente à chaque Evaluate — le « budget ~8 000 » du protocole se vérifie course par course au lieu d’être supposé depuis population × générations. C’est la même instrumentation que MGS-21, et elle rend la comparaison des budgets lisible dans le tableau final plutôt qu’arguée dans la prose.
Le seeding précède la création de population.FastRandomRandomization.ResetSeed(seed) avant new SudokuR1Chromosome22() — le RNG est consommé par CreateNew() de chaque individu initial ; re-seeder après ne fixe rien (leçon #12071). Les graines {0, 1, 7, 42} produisent des courses reproductibles : ré-exécuter ce notebook redonne les mêmes conflits.
L’échauffement est jeté, pas compté. La première course (graine 123, 10 générations) paie la compilation JIT et l’amorçage des DLLs ; la course témoin et le bench mesurent du code chaud. Le côté mealpy aura son échauffement symétrique — sinon la comparaison des temps mesurerait l’amorçage d’un côté et le régime permanent de l’autre.
La course témoin (graine 7) donne le premier point de données. Regardez ses conflits : de l’ordre de la colonne R1/PSO de MGS-21 (médiane 45,0 à budget égal), pas de la résolution. C’est attendu — le PSO continu plafonne sur le Sudoku discret, et il plafonnera des deux côtés de la comparaison à venir : c’est précisément pourquoi le protocole compare à budget égal plutôt qu’à résolution.
// === Le pont PythonNet : mealpy dans le même kernel, la même exécution ===// Recette validée (probe SC-4) : pythonnet 3.1.0 + DLL autonome du Python python.org.// Le Python 3.13 du système porte mealpy 3.0.2 (version affichée ci-dessous = preuve).#r "nuget: pythonnet,3.1.0"using Python.Runtime;// Resolution de la DLL Python : PYTHONNET_PYDLL (pattern Planners-9-HTN) d'abord,// sinon probe par OS des installs standards (Windows : python.org user-install,// Python3xx/python3xx.dll, puis racines conda miniconda3/anaconda3 ; Unix :// libpython3.x du systeme, .so Linux / .dylib macOS).// Aucun chemin machine en dur : le notebook s'execute partout ou un Python 3.10+// avec mealpy est present (#10643).staticstringResolvePythonDll(){// Types System.IO qualifies : .NET Interactive n'inclut pas System.IO dans// ses usings par defaut (CS0103 constate au premier passage).var env = Environment.GetEnvironmentVariable("PYTHONNET_PYDLL");if(!string.IsNullOrEmpty(env)&& System.IO.File.Exists(env))return env;if(OperatingSystem.IsWindows()){// Installs CPython.org standards d'abord (les plus récentes portent les packages récents// comme mealpy), ensuite le scan LOCALAPPDATA — un Python périmé qui n'a pas mealpy// ne doit pas masquer une install plus récente (pb 2026-08-25 : Python310 2023 devant Python313).foreach(var c innew[]{ @"C:\Python313\python313.dll", @"C:\Python312\python312.dll"})if(System.IO.File.Exists(c))return c;var local = Environment.GetEnvironmentVariable("LOCALAPPDATA");if(!string.IsNullOrEmpty(local)){var pyDir = System.IO.Path.Combine(local,"Programs","Python");if(System.IO.Directory.Exists(pyDir))foreach(var d in System.IO.Directory.GetDirectories(pyDir,"Python3*")){var hit = System.IO.Directory.GetFiles(d,"python3*.dll");if(hit.Length>0)return hit[0];}}// Racines conda (user puis systeme) : python313.dll versionne prefere au// python3.dll stable-ABI (recommandation pythonnet).var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);foreach(var root innew[]{ System.IO.Path.Combine(home,"miniconda3"), System.IO.Path.Combine(home,"anaconda3"), @"C:\ProgramData\miniconda3", @"C:\ProgramData\anaconda3"}){if(!System.IO.Directory.Exists(root))continue;var hits = System.IO.Directory.GetFiles(root,"python3*.dll");var pick ="";foreach(var h in hits)if(System.IO.Path.GetFileName(h).Length>11) pick = h;if(pick ==""&& hits.Length>0) pick = hits[0];if(pick !=""){// python3XX.dll de conda depend de DLLs de <root>\Library\bin// (ssl, ffi, zlib...) : le loader natif ne cherche PAS le dossier// de la DLL chargee, d'ou Win32 error 126. Equivalent de// `conda activate` : repertoire conda en tete du PATH du processus// AVANT le chargement (sert aussi aux .pyd de numpy/scipy).var dirs =new[]{ root, System.IO.Path.Combine(root,"Library","mingw-w64","bin"), System.IO.Path.Combine(root,"Library","bin"), System.IO.Path.Combine(root,"Scripts")};var path = Environment.GetEnvironmentVariable("PATH")??"";var toAdd ="";foreach(var d in dirs)if(System.IO.Directory.Exists(d)&&!path.Contains(d +";")) toAdd += d +";";if(toAdd !="") Environment.SetEnvironmentVariable("PATH", toAdd + path);return pick;}}}else{var libs =new[]{"/usr/lib/x86_64-linux-gnu","/usr/lib","/usr/local/lib","/opt/homebrew/lib"};foreach(var dir in libs)if(System.IO.Directory.Exists(dir)){var hit = System.IO.Directory.GetFiles(dir,"libpython3.*");foreach(var h in hit)if(h.EndsWith(".so")|| h.EndsWith(".dylib"))return h;}}thrownew System.IO.FileNotFoundException("DLL Python introuvable : definir PYTHONNET_PYDLL ou installer Python 3.10+ (mealpy requis).");}Runtime.PythonDLL=ResolvePythonDll();PythonEngine.Initialize();// Le problème Python : decode + coût réimplémentés à l'identique, compteur d'évals,// solveur mealpy avec seed EXPLICITE en solve() (API 3.x — le seed du constructeur est// ignoré, vérifié en amont) et journal muet (log_to='nothing').publicstatic PyModule S22;using(Py.GIL()){ S22 = Py.CreateScope(); S22.Set("puzzle_line22", PuzzleLine22); S22.Exec(@"import sysimport mealpyfrom mealpy.swarm_based.PSO import OriginalPSOfrom mealpy import Problem, FloatVarimport json as _jsonpuzzle =[int(ch)for ch in puzzle_line22]empties =[(i // 9, i % 9) for i in range(81) if puzzle[i] == 0]def decode(vec): g =[puzzle[r *9:(r +1)*9]for r inrange(9)]for k inrange(len(empties)): r, c = empties[k] v =int(round(float(vec[k]))) g[r][c]=max(1,min(9, v))return gdef cost(g): conflicts =0for i inrange(9): units =([g[i][j]for j inrange(9)],[g[j][i]for j inrange(9)],[g[3*(i // 3) + j // 3][3 * (i % 3) + j % 3] for j in range(9)])for unit in units: seen =set()for v in unit:if v in seen: conflicts +=1 seen.add(v)return conflictsdef cost_of_vector(vec):returncost(decode(vec))PY_EVALS =[0]classSudokuProblem(Problem): def __init__(self, bounds=None, minmax='min',**kwargs):super().__init__(bounds, minmax, log_to='nothing',**kwargs) def obj_func(self, x): PY_EVALS[0]+=1returnfloat(cost(decode(x)))def run_mealpy_pso(seed, pop_size, epoch): import time PY_EVALS[0]=0 prob =SudokuProblem(bounds=FloatVar(lb=(1.0,)*len(empties), ub=(10.0,)*len(empties), name='genes'), minmax='min') model =OriginalPSO(epoch=epoch, pop_size=pop_size) t0 = time.perf_counter() g_best = model.solve(prob, seed=seed) dt =(time.perf_counter()- t0)*1000.0 sol = _json.dumps([float(v)for v in g_best.solution])returncost(decode(g_best.solution)), PY_EVALS[0], dt, soldef bench_mealpy(seeds_json, pop_size, epoch, reps=3):out=[]for sd in _json.loads(seeds_json): runs =[run_mealpy_pso(sd, pop_size, epoch)for _ inrange(reps)] cs =[r[0]for r in runs] es =[r[1]for r in runs] ts =sorted(r[2]for r in runs) med = ts[len(ts)// 2] if len(ts) % 2 == 1 else (ts[len(ts) // 2 - 1] + ts[len(ts) // 2]) / 2.0out.append({'seed': sd, 'conflicts': cs[0], 'all_same':len(set(cs))==1, 'evals': es[0], 'ms': med, 'sol': runs[0][3]})return _json.dumps(out)def time_python_evals(vecs_json, reps=5): import time vecs = _json.loads(vecs_json) ts =[]for _ inrange(reps): t0 = time.perf_counter()for v in vecs:cost_of_vector(v) ts.append((time.perf_counter()- t0)*1000.0) ts.sort()return ts[len(ts)// 2]__mealpy_ver__ = 'mealpy ' + mealpy.__version__+ ' sur Python ' + sys.version.split()[0]"); Console.WriteLine($"Pont PythonNet actif : {S22.Get<string>("__mealpy_ver__")}");}// --- Sanity check : la fonction de coût est-elle la MÊME des deux côtés ? ---// 3 vecteurs témoins DÉTERMINISTES (LCG écrit à la main), décodés et costés des deux côtés.publicstaticdouble[]LcgVector22(int seed,int n){uint state =(uint)seed;var v =newdouble[n];for(int i =0; i < n; i++){ state = state *1664525u+1013904223u; v[i]=1.0+(state /4294967296.0)*9.0;// uniforme dans [1, 10)}return v;}var witnessVectors =new[]{LcgVector22(1,36),LcgVector22(2,36),LcgVector22(3,36)};using(Py.GIL()){ S22.Set("__witness_json__", System.Text.Json.JsonSerializer.Serialize(witnessVectors.Select(v => v.ToList()).ToList())); S22.Exec(@"__py_costs_json__ = _json.dumps([cost_of_vector(v) for v in _json.loads(__witness_json__)])");var pyCosts = System.Text.Json.JsonSerializer.Deserialize<List<int>>(S22.Get<string>("__py_costs_json__"));bool allEqual =true;for(int i =0; i < witnessVectors.Length; i++){int csCost =CountConflicts22(DecodeR1_22(witnessVectors[i]));bool eq = csCost == pyCosts[i]; allEqual &= eq; Console.WriteLine($" vecteur témoin {i + 1} : coût C# = {csCost}, coût Python = {pyCosts[i]} -> {(eq ? "IDENTIQUE" : "DIFFERENT")}");} Console.WriteLine(allEqual?"Sanity check PASSE : la fonction de coût est identique des deux côtés -- le bench est valide.":"Sanity check ECHOUE : les fonctions de coût diffèrent -- le bench serait invalide.");}
Installing Packages
pythonnet
Pont PythonNet actif : mealpy 3.0.2 sur Python 3.13.12
vecteur témoin 1 : coût C# = 67, coût Python = 67 -> IDENTIQUE
vecteur témoin 2 : coût C# = 71, coût Python = 71 -> IDENTIQUE
vecteur témoin 3 : coût C# = 60, coût Python = 60 -> IDENTIQUE
Sanity check PASSE : la fonction de coût est identique des deux côtés -- le bench est valide.
Interprétation : pourquoi la sanity check porte tout le bench
Le point fragile d’une comparaison cross-langage n’est pas le solveur — c’est la fonction de coût. Si le C# comptait les doublons avec une convention différente du Python, les deux moteurs n’optimiseraient pas le même paysage, et chaque milliseconde comparée serait du bruit raffiné. D’où le protocole en deux temps :
des vecteurs témoins déterministes — générés par un LCG écrit à la main (\(state \leftarrow state \times 1664525 + 1013904223\), la paire classique de Numerical Recipes), parce que System.Random et le RNG de NumPy ne produisent pas les mêmes suites : le LCG garantit que les deux côtés coûtent exactement les mêmes points de l’espace ;
le coût calculé indépendamment de chaque côté, puis confronté — trois vecteurs, trois paires.
Si les trois paires coïncident, l’égalité des fonctions de coût est établie sur données — et l’argument théorique sur les arrondis de demi-entiers devient superflu.
Le pont lui-même suit la recette du probe : Runtime.PythonDLL pointé sur la DLL autonome du Python python.org 3.13, code Python injecté par scope.Exec en chaîne verbatim, données échangées en JSON (Set pour descendre, Get pour remonter) — jamais par lecture directe de variables Python complexes, qui ne traversent pas proprement la frontière. Le scope S22 est gardé en champ statique : les cellules suivantes (course témoin mealpy, bench complet, profil de coût) réutilisent le même espace de noms Python et les mêmes définitions. Notez aussi les graines des deux moteurs passées explicitement : solve(prob, seed=N) côté mealpy (le paramètre du constructeur est silencieusement ignoré par l’API 3.x — un piège documenté dans le code), ResetSeed(N) côté MGS.
// === Moteur mealpy : trajectoire instrumentée, course témoin, contre-vérification croisée ===using(Py.GIL()){// Le budget mealpy inclut l'évaluation initiale : pop × (epoch + 1) = 8 050.// Les seuils propres au budget évitent de comparer des numéros d'évaluation absolus différents. S22.Exec(@"PY_BEST = [10**9]PY_CP =[[0,0,0,0]]PY_THRESHOLDS =[[0,0,0,0]]classTrackedSudokuProblem(Problem): def __init__(self, bounds=None, minmax='min',**kwargs):super().__init__(bounds, minmax, log_to='nothing',**kwargs) def obj_func(self, x): PY_EVALS[0]+=1 value =cost(decode(x)) PY_BEST[0]=min(PY_BEST[0], value)for i, threshold inenumerate(PY_THRESHOLDS[0]):if PY_CP[0][i]==0 and PY_EVALS[0]>= threshold: PY_CP[0][i]= PY_BEST[0]returnfloat(value)def run_mealpy_pso(seed, pop_size, epoch): import time expected = pop_size *(epoch +1) PY_EVALS[0]=0 PY_BEST[0]=10**9 PY_CP[0]=[0,0,0,0] PY_THRESHOLDS[0]=[(expected * q +3)// 4 for q in (1, 2, 3, 4)] prob =TrackedSudokuProblem( bounds=FloatVar(lb=(1.0,)*len(empties), ub=(10.0,)*len(empties), name='genes'), minmax='min') model =OriginalPSO(epoch=epoch, pop_size=pop_size) t0 = time.perf_counter() g_best = model.solve(prob, seed=seed) dt =(time.perf_counter()- t0)*1000.0 sol = _json.dumps([float(v)for v in g_best.solution])returncost(decode(g_best.solution)), PY_EVALS[0], dt, sol,list(PY_CP[0])def bench_mealpy(seeds_json, pop_size, epoch, reps=3):out=[]for sd in _json.loads(seeds_json): runs =[run_mealpy_pso(sd, pop_size, epoch)for _ inrange(reps)] cs =[r[0]for r in runs] es =[r[1]for r in runs] ts =sorted(r[2]for r in runs) med = ts[len(ts)// 2] if len(ts) % 2 == 1 else (ts[len(ts) // 2 - 1] + ts[len(ts) // 2]) / 2.0out.append({'seed': sd, 'conflicts': cs[0], 'all_same':len(set(cs))==1 and all(r[4]== runs[0][4]for r in runs), 'evals': es[0], 'ms': med, 'cp': runs[0][4], 'sol': runs[0][3]})return _json.dumps(out)_wu_c, _wu_e, _wu_t, _wu_sol, _wu_cp =run_mealpy_pso(123,50,10)__d_c__, __d_e__, __d_t__, __d_sol__, __d_cp__ =run_mealpy_pso(7,50,160)");int dConflicts = S22.Get<int>("__d_c__");int dEvals = S22.Get<int>("__d_e__");double dMs = S22.Get<double>("__d_t__");var dCp = System.Text.Json.JsonSerializer.Deserialize<int[]>( S22.Eval("_json.dumps(__d_cp__)").ToString()); Console.WriteLine($"mealpy OriginalPSO (graine 7, témoin) : {dConflicts} conflits, "+ $"{dEvals} évaluations, {dMs:F0} ms, "+ $"cp25/50/75/100={string.Join("/", dCp)}.");var solJson = S22.Get<string>("__d_sol__");var genes = System.Text.Json.JsonSerializer.Deserialize<double[]>(solJson);int csRecheck =CountConflicts22(DecodeR1_22(genes)); Console.WriteLine($"Contre-vérif croisée : coût C# du meilleur mealpy = {csRecheck} "+ $"(Python rapporte {dConflicts}) -> {(csRecheck == dConflicts ? "IDENTIQUE" : "DIFFERENT")}");}
2. Le croisement — 2 moteurs × 4 graines à budget égal
Le plan est maintenant entièrement câblé : même grille, même coût (prouvé sur données), même représentation. Reste à exécuter le plan de la section 1 — population 50, 160 générations/epochs, graines {0, 1, 7, 42} — et à rapporter les quatre colonnes : conflits finaux, évaluations réellement consommées, temps, et le ratio ms/éval. Les courses mealpy tournent en une seule entrée dans le scope Python (la boucle vit côté Python, les résultats reviennent en JSON) ; les courses MGS tournent côté C#. Médianes et min-max se calculent sur les 4 graines de chaque moteur.
Amendement de protocole, motivé par le run pilote. La première exécution complète a montré le défaut du timing single-run : les temps MGS varient de ±30 % d’une exécution à l’autre (paliers de garbage collection .NET — un run a mesuré 1 132 ms là où un autre donne 660 ms pour la même course seedée), soit plus que l’écart entre moteurs. Un protocole vitesse qui bouge plus que la grandeur mesurée ne mesure rien. Amendement, appliqué symétriquement : 3 répétitions par graine et par moteur, la colonne ms rapporte la médiane des 3 ; les conflits seedés doivent être identiques sur les 3 répétitions (et le notebook l’affiche — preuve de déterminisme en direct, 12 courses par moteur). La fitness isolée (ci-dessous) suit la même règle à 5 répétitions. La qualité, elle, ne bouge pas : elle est seedée, le pilote l’a montré chiffre par chiffre.
Rappel des critères pré-enregistrés : qualité — médiane strictement inférieure ET max sous min de l’autre, sinon « comparable » ; vitesse — rapport des ms/éval moyens ; aucune compensation entre les axes. Le verdict sur l’hypothèse user (« possible que les perfs ne suivent pas mealpy ») se lira sur l’axe vitesse ; l’axe qualité dira si les deux implémentations du PSO canonique convergent pareil à budget égal.
Lecture du croisement : MGS domine dès le premier quart
Les critères pré-enregistrés et les trajectoires best-so-far donnent une lecture plus précise que les seuls résultats finaux :
Qualité finale : MGS atteint une médiane de 17,0 conflits contre 28,5 pour mealpy, avec séparation totale des gammes : 12-22 contre 26-31. Le pire résultat MGS (22) reste meilleur que le meilleur résultat mealpy (26) ; la domination ne dépend pas d’un chevauchement interprété favorablement.
Trajectoire médiane : MGS part de 32,0 au checkpoint 25 % et continue de descendre — 22,0, puis 19,0 et 17,0 — tandis que mealpy passe de 42,0 à 33,5, puis 30,0 et 28,5. L’écart médian MGS − mealpy se maintient entre −10,0 et −11,5 conflits d’un bout à l’autre de la course. Le classifieur dérivé des quatre checkpoints conclut précipitation précoce : l’avantage MGS est acquis dès le premier quart, sans que MGS cesse pour autant d’améliorer son best-so-far jusqu’à la fin.
Déterminisme : conflits finaux et quatre checkpoints sont identiques sur les trois répétitions pour les huit couples graine-moteur. La forme observée n’est donc pas un artefact d’une seule trajectoire, et la séparation finale n’est pas du bruit entre répétitions.
Coût par étape : la cellule calcule dynamiquement le rapport ms/éval du run courant (mealpy y est devant). Les temps absolus ne sont pas recopiés ici, car ils dépendent du matériel ; cet axe ne fonde pas le verdict de qualité.
L’hypothèse de départ — « possible que les perfs ne suivent pas mealpy sous NumPy optimisé » — se sépare toujours en deux axes, mais le classement qualité a basculé : MGS domine nettement la qualité à budget égal, mealpy garde l’avantage sur le coût par étape. Les deux PSO ne partagent pas tous leurs paramètres par défaut : ce banc mesure donc les bibliothèques telles qu’un utilisateur les lance, tandis que les checkpoints localisent quand leur comportement diverge.
Avertissement de lecture. Ce croisement tourne sous le défaut corrigé du composé PSO MGS (mutation de l’hôte préservée, MetaGeneticSharp#50). Les outputs historiques committés au gitlink précédent montraient la situation inverse — médiane MGS 43,5 [39-48], best-so-far gelé dès le checkpoint 25 % — que la section 3 rejoue ci-dessous comme témoin (noMutation: true) et attribue à la mutation désactivée. Le classement ci-dessus est donc la conséquence directe du correctif, pas une propriété du noyau PSO lui-même.
// === Coût par évaluation : la fitness seule, hors moteur, 500 vecteurs identiques ===// Les vecteurs sont générés côté C# (LCG, graines 42..541) et passés en JSON au Python :// les DEUX côtés chronomètrent decode+coût sur exactement les mêmes 500 points.int K22 =500;var benchVecs =new List<double[]>();for(int i =0; i < K22; i++) benchVecs.Add(LcgVector22(42+ i,36));var csTimes =new List<double>();for(int rep =0; rep <5; rep++){var swRep = Stopwatch.StartNew();foreach(var v in benchVecs)CountConflicts22(DecodeR1_22(v)); swRep.Stop(); csTimes.Add(swRep.Elapsed.TotalMilliseconds);}csTimes.Sort();double csMs = csTimes[2];// médiane de 5 (amendement §2)double pyMs;using(Py.GIL()){ S22.Set("__vecs_json__", System.Text.Json.JsonSerializer.Serialize(benchVecs.Select(v => v.ToList()).ToList())); S22.Exec(@"__py_ms__ = time_python_evals(__vecs_json__)"); pyMs = S22.Get<double>("__py_ms__");}Console.WriteLine($"Fitness seule, {K22} vecteurs identiques (médiane de 5 répétitions par côté) :");Console.WriteLine($" C# : {csMs:F1} ms total -> {csMs / K22:F3} ms/éval");Console.WriteLine($" Python : {pyMs:F1} ms total -> {pyMs / K22:F3} ms/éval");Console.WriteLine($" rapport Python/C# : {pyMs / csMs:F2}x");
Fitness seule, 500 vecteurs identiques (médiane de 5 répétitions par côté) :
C# : 6,9 ms total -> 0,014 ms/éval
Python : 38,2 ms total -> 0,076 ms/éval
rapport Python/C# : 5,56x
Lecture du coût par évaluation : isoler la fitness du moteur
La cellule mesure decode + cost sur les 500 mêmes vecteurs, cinq fois par langage, puis affiche la médiane. Le rapport Python/C# reste très supérieur à un : la fitness C# est nettement moins coûteuse isolément. Ce résultat préserve l’observation antérieure d’un écart d’environ 5-6× selon la machine, sans figer en prose un temps absolu qui dériverait au prochain passage kernel.
Ce résultat ne se transpose pas directement au bench complet. Le temps par évaluation y inclut aussi la mise à jour des particules, la gestion de la population et les structures propres à chaque bibliothèque. La comparaison utile est donc la décomposition calculée depuis les deux cellules de mesure :
fitness isolée : avantage C# net ;
moteur complet : coût par étape du même ordre de grandeur ;
mécanique résiduelle : la différence temps total par évaluation − fitness isolée absorbe l’essentiel du coût côté MGS ;
conséquence : accélérer encore la fitness C# ne suffit ni à expliquer ni à corriger une dynamique de convergence — la stagnation tardive des outputs historiques a depuis été attribuée au câblage de la mutation (section 3), pas au coût de la fitness.
Les anciennes mesures illustraient déjà ce paradoxe : un avantage massif de la fitness C# devenait presque invisible dans le temps total, parce que sélection, structures de population et mises à jour du moteur dominaient le budget par étape. Le présent run confirme la structure du diagnostic sans transformer des chiffres machine-dépendants en constantes pédagogiques.
Trois moralités en découlent : le coût par évaluation d’un moteur n’est pas le coût de sa fitness ; un avantage fitness spectaculaire peut être absorbé par la mécanique de génération ; et un timing single-run peut inverser le signe du classement. Les temps absolus restent donc dans l’output frais. Les rapports mesurés dans une même cellule et la décomposition fitness/moteur conservent, eux, la comparaison pédagogique.
3. Correctif MGS #50 : la mutation PSO préservée sur les plateaux quantifiés
Le profil stagnation tardive MGS mesuré à la section 2 — outputs committés au gitlink précédant le correctif : médiane 43,5 [39-48], best-so-far gelé dès le checkpoint 25 % sur les quatre graines — remonte à un mécanisme vérifiable du composé PSO : le clamp de vélocité se recalibre sur max(|pbest − x|, |gbest − x|). Sur un objectif quantifié comme le nôtre — les gènes R1 sont arrondis à l’entier avant évaluation — la convergence x = pbest = gbest donne un span nul, donc v = 0 : l’état est absorbant, la nuée ne peut plus produire un nouvel arrondi, et la mutation de l’hôte qui aurait pu la relancer était désactivée dans le composé (NoMutation = true hérité par tous les composés géométriques).
Le correctif atomique MetaGeneticSharp#50 ne touche ni la récurrence, ni le clamp, ni le budget : il change un défaut de câblage — pour le PSO canonique seul, l’omission du paramètre préserve désormais la mutation de l’hôte (noMutation omis → mutation active), l’opt-out explicite noMutation: true conservant exactement le comportement historique. L’expérience appariée isole cette seule bascule, les deux jambes exécutées dans le même kernel sur le même câblage MetaGeneticAlgorithm :
défaut corrigé : noMutation omis — la UniformMutation(true) de l’hôte reste active dans le composé.
Les invariants sont inchangés : graines {0, 1, 7, 42}, population 50, 160 générations (8 000 évaluations), grille, fonction de coût, représentation R1, checkpoints 25/50/75/100 %, sélection/croisement externes et paramètres PSO (w/c1/c2, ratio de clamp) identiques. La section 2 ci-dessus, re-exécutée sous le défaut corrigé, mesure donc déjà la jambe « après » ; la jambe témoin est rejouée ici pour que la paire vive dans une seule exécution appariée.
Le verdict est pré-enregistré avant la mesure :
INCONCLUSIVE si le témoin noMutation: true ne reproduit pas la baseline committée au gitlink précédent — la paire ne mesurerait plus le correctif seul ;
NO IMPROVEMENT si la médiane corrigée ne baisse pas celle du témoin ;
TRADE-OFF si la médiane baisse mais qu’au moins une graine se dégrade de plus de 2 conflits ;
IMPROVES si la médiane baisse sans dégradation par graine au-delà de 2 conflits.
// === EXPÉRIENCE RÉSOLUE : correctif #50 — mutation PSO préservée contre opt-out historique ===// Une seule variable change entre les deux jambes : noMutation=true (historique) vs omis (défaut corrigé).publicstaticclass Mgs22P1Host{// Même câblage que Mgs22Host.RunPso, seul noMutation est explicite :// true = comportement d'avant #50, null (omis) = défaut corrigé qui préserve la mutation PSO.publicstatic(int conflicts,int evals,double ms,int[] cp)RunPsoMutation(int seed,int popSize,int maxGens,bool? noMutation){ FastRandomRandomization.ResetSeed(seed);var compound = MetaHeuristicsService.CreateMetaHeuristicByName("ParticleSwarmOptimization", maxGens, popSize,null, noMutation);var adam =newSudokuR1Chromosome22();var pop =newMetaPopulation(popSize, popSize, adam);var ga =newMetaGeneticAlgorithm( pop,newSudokuR1Fitness22(),newEliteSelection(),newUniformCrossover(0.5f),newUniformMutation(true), compound); ga.Termination=newGenerationNumberTermination(maxGens); SudokuR1Fitness22.Reset(popSize * maxGens);var sw = Stopwatch.StartNew(); ga.Start(); sw.Stop();var best =(SudokuR1Chromosome22)ga.BestChromosome;return(CountConflicts22(best.ToGrid()), SudokuR1Fitness22.Evals, sw.Elapsed.TotalMilliseconds, SudokuR1Fitness22.Checkpoints.ToArray());}}// Témoin de validité : la baseline committée au gitlink précédant #50 (section 2 avant bump).var p1CommittedBaseline =new Dictionary<int,int>{[0]=39,[1]=41,[7]=46,[42]=48};List<(int seed,int conflicts,int evals,double ms,bool allSame,int[] cp)>RunP1Leg(bool? noMutation){var rows =new List<(int,int,int,double,bool,int[])>();foreach(var sd in Seeds22){var runs3 =new List<(int c,int e,double t,int[] q)>();for(int rep =0; rep <3; rep++){var r = Mgs22P1Host.RunPsoMutation(sd,50,160, noMutation); runs3.Add((r.conflicts, r.evals, r.ms, r.cp));}var times = runs3.Select(x => x.t).OrderBy(t => t).ToList(); rows.Add((sd, runs3[0].c, runs3[0].e, times[1], runs3.All(x => x.c== runs3[0].c&& x.q.SequenceEqual(runs3[0].q)), runs3[0].q));}return rows;}var p1ControlRows =RunP1Leg(true);// jambe historique : opt-out noMutation=truevar p1FixedRows =RunP1Leg(null);// jambe corrigée : noMutation omis (défaut #50)var p1ControlC = p1ControlRows.Select(r => r.conflicts).ToList();var p1FixedC = p1FixedRows.Select(r => r.conflicts).ToList();double p1ControlMedian =Median22(p1ControlC);double p1FixedMedian =Median22(p1FixedC);bool p1ControlFlat = p1ControlRows.All(r => r.cp.All(v => v == r.cp[0]));var p1Deltas = p1FixedRows.Join(p1ControlRows, f => f.seed, c => c.seed,(f, c)=>(seed: f.seed, delta: f.conflicts- c.conflicts)).ToList();int p1WorstDegradation = p1Deltas.Max(x => x.delta);bool p1BaselineParity = p1ControlRows.All(r => r.conflicts== p1CommittedBaseline[r.seed]);// Cohérence interne : la jambe corrigée doit coïncider avec la section 2 re-exécutée (même construction).bool p1CorrectedMatchesBench = p1FixedRows.All(r =>{var b = mgsRows.Single(x => x.seed== r.seed);return r.conflicts== b.conflicts&& r.cp.SequenceEqual(b.cp);});string p1Verdict =!p1BaselineParity?"INCONCLUSIVE": p1FixedMedian >= p1ControlMedian?"NO IMPROVEMENT":(p1WorstDegradation >2?"TRADE-OFF":"IMPROVES");Console.WriteLine($"{"jambe",-20} {"graine",6} {"conflits",9} {"evals",7} {"ms/eval",8} {"cp25/50/75/100",-18}");foreach(var r in p1ControlRows) Console.WriteLine($"{"témoin noMut=true",-20} {r.seed,6} {r.conflicts,9} {r.evals,7} {r.ms / r.evals,8:F3} {string.Join("/", r.cp),-18}");foreach(var r in p1FixedRows) Console.WriteLine($"{"défaut corrigé #50",-20} {r.seed,6} {r.conflicts,9} {r.evals,7} {r.ms / r.evals,8:F3} {string.Join("/", r.cp),-18}");Console.WriteLine();Console.WriteLine($"Témoin historique : médiane finale {p1ControlMedian:F1} [{p1ControlC.Min()}-{p1ControlC.Max()}], "+ $"trajectoires plates sur les 4 graines : {(p1ControlFlat ? "oui" : "non")}");Console.WriteLine($"Défaut corrigé : médiane finale {p1FixedMedian:F1} [{p1FixedC.Min()}-{p1FixedC.Max()}]");var p1ControlCpMedian = Enumerable.Range(0,4).Select(k =>Median22(p1ControlRows.Select(r => r.cp[k]))).ToArray();var p1FixedCpMedian = Enumerable.Range(0,4).Select(k =>Median22(p1FixedRows.Select(r => r.cp[k]))).ToArray();Console.WriteLine("Trajectoires médianes par checkpoint :");for(int k =0; k <4; k++) Console.WriteLine($" cp{25 * (k + 1),3}% : témoin {p1ControlCpMedian[k]:F1} -> corrigé {p1FixedCpMedian[k]:F1} "+ $"(delta {p1FixedCpMedian[k] - p1ControlCpMedian[k]:+0.0;-0.0;0.0})");Console.WriteLine("Écarts finaux corrigé - témoin par graine : "+string.Join(", ", p1Deltas.Select(x => $"{x.seed}:{x.delta:+#;-#;0}")));Console.WriteLine($"Pire dégradation par graine : {p1WorstDegradation:+#;-#;0} conflit(s)");Console.WriteLine($"Parité témoin vs baseline committée (gitlink pré-#50) : {(p1BaselineParity ? "4/4" : "ÉCHEC")}");Console.WriteLine($"Cohérence jambe corrigée vs section 2 re-exécutée : {(p1CorrectedMatchesBench ? "4/4" : "ÉCHEC")}");Console.WriteLine($"Verdict du correctif pré-enregistré : {p1Verdict}");Console.WriteLine($"Déterminisme : témoin {p1ControlRows.Count(r => r.allSame)}/4, corrigé {p1FixedRows.Count(r => r.allSame)}/4 "+"graines avec conflits et checkpoints identiques sur les 3 répétitions.");
Lecture du correctif #50 : le plateau n’est plus absorbant
Le verdict pré-enregistré est IMPROVES. La paire isole exactement la bascule de mutation : le témoin noMutation: true reproduit à l’identique la baseline committée au gitlink précédent — médiane 43,5 [39-48], parité 4/4, trajectoires plates sur les quatre graines — tandis que le défaut corrigé atteint une médiane de 17,0 conflits [12-22]. Les quatre graines gagnent entre 24 et 30 conflits (0:-27, 1:-25, 7:-24, 42:-30) : le gain n’est pas un déplacement de performance entre graines, il est uniforme.
Les trajectoires médianes confirment le mécanisme plutôt qu’un meilleur tirage : le témoin est à 43,5 dès le checkpoint 25 % et n’avance plus, le corrigé passe de 32,0 à 22,0, puis 19,0 et 17,0 — la nuée continue d’améliorer son best-so-far jusqu’à la fin du budget, ce que le clamp de vélocité à span nul interdisait. Le déterminisme est total sur les deux jambes (4/4 graines sur trois répétitions), et la jambe corrigée coïncide avec la section 2 re-exécutée (4/4) : la paire mesure bien le correctif seul, pas une différence de câblage entre cellules.
Deux conséquences. D’abord le coût : les timings des deux jambes restent dans l’output ; grandeurs machine-dépendantes, elles ne fondent pas le verdict. Ensuite la hiérarchie : sous le défaut corrigé, la section 2 ci-dessus mesure MGS à 17,0 [12-22] contre mealpy à 28,5 [26-31], avec séparation totale des gammes. Le profil « stagnation tardive MGS » constaté sur les outputs historiques était donc bien un défaut de câblage du composé — la mutation désactivée — et non une limite de la récurrence PSO elle-même ; la boucle corrective de #13778 le clôt sur cette paire.
Exercice 1 : la revanche du GA — BaseGA mealpy contre le composé Default MGS
Le croisement ci-dessus compare deux implémentations du même algorithme (PSO canonique). L’exercice symétrique naturel est le GA : mealpy.evolutionary_based.GA.BaseGA contre le composé "Default" de MetaGeneticSharp (celui de la colonne R1/GA de MGS-21, médiane 10,5 à 8 000 évaluations). Consignez pour chaque moteur et chaque graine de {0, 1, 7, 42} : conflits finaux, évaluations consommées, temps — puis répondez : est-ce que l’ordre observé PSO-contre-PSO se retrouve GA-contre-GA, ou est-ce une propriété du couple algorithme × bibliothèque ?
Plan suggéré : réutiliser run_mealpy_pso comme gabarit pour écrire run_mealpy_ga (seule la classe du modèle change), et Mgs22Host.RunPso pour un RunGa (le composé "Default" se crée par le même CreateMetaHeuristicByName).
// EXERCICE 1 : bench croisé GA mealpy (BaseGA) contre GA MGS (composé "Default")// Graine par graine, budget ~8000 évals, mêmes colonnes que le croisement PSO.// Indice : côté Python -> from mealpy.evolutionary_based.GA import BaseGA puis// un run_mealpy_ga sur le même SudokuProblem ; côté C# -> composé "Default".// Etape 1 : implémenter run_mealpy_ga dans le scope S22.// Etape 2 : implémenter Mgs22Host.RunGa (copie de RunPso, composé "Default").// Etape 3 : boucler sur les 4 graines des deux côtés, imprimer la table, médianes.var resultExo1 =nullas List<string>;Console.WriteLine("Exercice a completer : bench croise GA mealpy vs GA MGS (4 graines, budget egal).");
Exercice a completer : bench croise GA mealpy vs GA MGS (4 graines, budget egal).
Exercice 2 : budget ×4 — l’écart de qualité se referme-t-il ?
Le croisement se conclut (voir la Lecture) à ~8 000 évaluations, budget où la représentation R1 plafonne loin de la résolution. Multipliez le budget par 4 (population 50 × 640 générations) des deux côtés, graines {0, 1, 7, 42}, et mesurez : les médianes de conflits descendent-elles de concert ? Le rapport ms/éval bouge-t-il, ou est-il une propriété de la fitness, indépendante du budget ? Un moteur profite-t-il du budget long plus vite que l’autre — et si oui, laquelle des deux implémentations (paramètres par défaut différents : coefficients de Clerc contre Shi & Eberhart, gestion du voisinage) est la cause plausible ?
// EXERCICE 2 : budget x4 (pop 50, 640 générations/epochs), 4 graines, deux moteurs.// Indice : un seul changement de paramètre par côté -- 160 -> 640.// Etape 1 : relancer le plan du croisement avec gens/epoch = 640.// Etape 2 : comparer médianes/min-max à 8000 vs 32000 évals par moteur.// Etape 3 : rapporter si le rapport ms/éval reste stable (fitness-déterminé) ou dérive.var resultExo2 =nullas List<string>;Console.WriteLine("Exercice a completer : budget x4 des deux cotes, comparaison des medianes et du rapport ms/eval.");
Exercice a completer : budget x4 des deux cotes, comparaison des medianes et du rapport ms/eval.
Exercice 3 : profiler la fitness — où va la milliseconde ?
La cellule du coût par évaluation mesure decode + cost en bloc. Décomposez : chronométrez séparément (a) le décodage (construction de la grille depuis le vecteur), (b) le compte de conflits (parcours des 27 unités), sur les 500 mêmes vecteurs, des deux côtés. Le rapport Python/C# est-il homogène entre (a) et (b), ou une des deux étapes porte-t-elle l’écart ? Puis la question qui ferme la boucle : combien de la ms/éval du bench complet est de la fitness, combien du moteur ? (différence bench-complet moins fitness-isolée, par côté).
// EXERCICE 3 : profil decode vs cost, 500 vecteurs, deux côtés, puis séparation// fitness/moteur dans la ms/éval du bench complet.// Indice : côté C# deux Stopwatches (DecodeR1_22 seul, CountConflicts22 seul) ;// côté Python deux time.perf_counter autour de decode(v) et cost(g).// Etape 1 : mesurer (a) decode seul, (b) cost seul, chaque côté.// Etape 2 : répartir la ms/éval du bench entre fitness et moteur.var resultExo3 =nullas List<string>;Console.WriteLine("Exercice a completer : profil decode/cost par cote, repartition fitness vs moteur.");
Exercice a completer : profil decode/cost par cote, repartition fitness vs moteur.
Résumé et perspectives
Réponse à l’hypothèse de départ. Sur ce plan apparié — PSO canonique contre OriginalPSO, même grille et même coût, budgets mesurés, graines {0, 1, 7, 42} — MGS domine la qualité finale : médiane 17,0 contre 28,5 conflits, avec séparation des gammes 12-22 contre 26-31. mealpy garde l’avantage sur le coût par étape ; son ampleur dépend du matériel et se lit dans l’output, pas dans une valeur figée en prose. L’hypothèse « les perfs ne suivront pas mealpy » est donc réfutée sur l’axe qualité à budget égal ; sur l’axe vitesse brute, mealpy reste devant.
Le mécanisme, localisé et tranché. Les outputs historiques d’avant le correctif montraient la situation inverse : médiane MGS 43,5, atteinte dès 25 % du budget puis figée — le profil « stagnation tardive ». La section 3 rejoue ce cas en témoin (noMutation: true) et tranche l’attribution : le plateau quantifié était un état absorbant parce que le câblage historique désactivait la mutation de l’hôte ; le défaut corrigé fait passer la médiane de 43,5 à 17,0 sur le même plan, verdict IMPROVES. La cible d’analyse n’était donc ni la fitness, ni la dynamique de population : un défaut de câblage du composé, isolé et mesuré par la paire.
La méthode, réutilisable. Sanity check cross-langage sur vecteurs témoins LCG ; graines passées explicitement à solve() et ResetSeed() ; budgets comptés dans les fitness ; trajectoires best-so-far aux quarts du budget propre à chaque moteur ; médiane et min-max sur quatre graines ; temps mesurés en médiane de répétitions. Le notebook vérifie le déterminisme des conflits et des checkpoints pour les huit couples graine-moteur. Cette séparation protège deux leçons des runs précédents : les grandeurs seedées sont reproductibles, tandis que les rapports de timing restent sensibles au matériel.
Ce que le benchmark ne prouve toujours pas. Les paramètres par défaut des deux PSO diffèrent : le banc mesure les bibliothèques telles qu’un utilisateur les lance, et l’écart restant MGS-mealpy sous défauts corrigés reste un fait de réglage et d’implémentation, pas une hiérarchie intrinsèque des noyaux PSO. Toute nouvelle amélioration doit conserver fonction de coût, graines, budgets et checkpoints, puis comparer avant/après sur le même plan — exactement le protocole appliqué en section 3.
Perspectives. (1) L’Exercice 1 teste si la forme d’écart se reproduit GA contre GA, afin de distinguer effet bibliothèque et effet algorithme. (2) L’Exercice 2 prolonge le budget pour distinguer convergence durable et plafond rattrapable. (3) L’Exercice 3 sépare décodage et compte de conflits afin de localiser le coût de la fitness sans le confondre avec la dynamique du solveur. La suite corrective appelée par les runs précédents a été exécutée en section 3 : le changement atomique — mutation préservée — y rejoue ce benchmark à l’identique et conclut IMPROVES.