Kernel : .NET (C#) — pont PythonNet vers mealpy dans la même exécution
Introduction
Quatre paires mesurées, quatre verdicts différents : mealpy devant sur PSO (MGS-22), écart resserré et vitesse inversée sur DE (MGS-23), premier doublé MGS sur le recuit (MGS-24), ex æquo MGS-MGS et mealpy derrière sur WOA (MGS-25). La paire FBI — Forensic-Based Investigation est d’une autre nature que les quatre précédentes : c’est la seule de l’Epic dont le port C# revendique être traduit directement depuis le fichier mealpy de référence (docstring du composé, PR giacomelli/GeneticSharp#87 : « implemented directly from the mealpy version »). En théorie, la paire miroir absolue : mêmes équations, zéro reconstruction indépendante.
MGS ForensicBasedInvestigation : composé géométrique population-based à quatre phases — A1 (investigation : un gène perturbé par un bruit normal), A2 (location : combinaison conditionnée par le rang de fitness), B1/B2 (pursuit : attraction vers le meilleur) — avec une réinsertion pairwise (FitnessBasedPairwiseReinsertion) ;
mealpy OriginalFBIO (human_based) : les quatre mêmes phases A1/A2/B1/B2 de Chou & Nguyen (2020), itérées par epoch sur une population.
Deux dérives séparent pourtant le miroir, toutes deux déclarées avant la mesure : le bruit de la phase A1 est N(0,1) côté MGS (transformée de Box-Muller — l’IRandomization upstream n’expose pas GetNormal) mais uniforme dans (−1, 1) côté mealpy 3.0.2 ; et les équations A2/B2 diffèrent dans le détail de leurs appariements d’individus. La comparaison reste loyale (même substrat, budget d’évaluations égalisé, protocole hérité) : c’est précisément la question de cette paire — deux implémentations qui se réclament de la même source ont-elles convergé ?
Enfant de l’Epic #12373 (comparaison appariée MGS ↔︎ mealpy) : une paire, un notebook, une PR.
1. Le protocole apparié, pré-enregistré — hérité de MGS-22, non renégocié
Le protocole est celui de MGS-22 (#12302), repris intégralement pour que les paires de l’Epic soient comparables entre elles :
même substrat : grille Easy[0] de Sudoku_Easy51.txt, représentation R1 (continu + arrondi), fonction de coût = conflits totaux d’une grille pleine ;
budget d’évaluations égalisé — mesuré, pas supposé : les compteurs des deux moteurs sont rapportés tels quels. Les deux moteurs étant population-based, la structure est symétrique : MGS consomme ~pop 50 × 200 générations ≈ 8 100 évaluations (la réinsertion pairwise n’évalue que ~40 enfants par génération de 50 — le compteur tranche) ; mealpy FBIO déroule ses quatre phases par epoch (4 × 50 évaluations par epoch après la population initiale) → 40 epochs ≈ 8 050 évaluations ;
4 graines nommées {0, 1, 7, 42}, médiane + min/max, jamais un run isolé ;
contre-vérification croisée du coût : le vainqueur mealpy est décodé et coûté côté C# — sans elle on compare deux fonctions de coût, pas deux moteurs ;
ms/éval séparé du temps total ;
graines passées explicitement aux deux moteurs : solve(prob, seed=N) côté mealpy (le paramètre du constructeur est silencieusement ignoré en 3.x), ResetSeed(N) côté MGS ;
déterminisme vérifié (répétition, conflits identiques exigés) avant de publier le moindre chiffre.
Paramètres par défaut de chaque bibliothèque, mesurés et déclarés (c’est le protocole MGS-22 : on compare les bibliothèques telles que leurs auteurs les livrent) :
moteur
architecture
phases
paramètres scalaires
MGS ForensicBasedInvestigation
population 50, composé géométrique, réinsertion pairwise
A1/A2/B1/B2 par génération
aucun au-delà de la population
mealpy OriginalFBIO
population, epochs
A1/A2/B1/B2 par epoch
aucun au-delà de epoch/pop_size
C’est la première paire de l’Epic sans aucun confond paramétrique : les deux constructeurs n’exposent pas le moindre réglage transportable. Ce qui subsiste est une divergence d’équations — le bruit A1 (N(0,1) contre uniforme (−1,1)) et les appariements d’individus des phases A2/B2 — déclarée d’entrée, mesurée au croisement, et qu’aucun paramètre ne peut annuler : contrairement au temp_init du recuit (MGS-24), elle ne se neutralise pas, elle se lit.
// === MGS-27 : socle commun — DLLs MGS, grille de référence, fonction de coût ===// Même socle que MGS-22/23/24/25 : 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/22/23/24/25.publicstaticstring PuzzleLine27 ="902005403100063025508407060026309001057010290090670530240530600705200304080041950";publicstaticint[,]ParsePuzzle27(){var g =newint[9,9];for(int i =0; i <81; i++) g[i /9, i %9]= PuzzleLine27[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.publicstaticintCountConflicts27(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;}publicstaticintCountEmpty27(int[,] p){int n =0;foreach(var v in p)if(v ==0) n++;return n;}publicstatic List<(int r,int c)>EmptyCells27(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_27(double[] genes){var Puzzle =ParsePuzzle27();var empties =EmptyCells27(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 Puzzle27 =ParsePuzzle27();Console.WriteLine($"Grille de référence : {CountEmpty27(Puzzle27)} cellules vides, "+ $"{81 - CountEmpty27(Puzzle27)} indices fixes, {EmptyCells27(Puzzle27).Count} gènes R1.");
The below script needs to be able to find the current output cell; this is an easy method to get it.
Grille de référence : 36 cellules vides, 45 indices fixes, 36 gènes R1.
Lecture. Le socle est posé, identique à MGS-22/23/24/25 au nom près — c’est voulu : la comparabilité de l’Epic #12373 tient à ce que chaque paire courre sur exactement le même substrat. 36 cellules vides = 36 gènes continus dans [1, 10), la fonction de coût compte les doublons ligne/colonne/bloc d’une grille pleine et vaut 0 ssi résolue.
// === Moteur MGS : chromosome R1, fitness instrumentée, composé ForensicBasedInvestigation ===// FBI GÉOMÉTRIQUE MGS : composé population-based à quatre phases A1/A2/B1/B2// (investigation-location-pursuit), réinsertion FitnessBasedPairwiseReinsertion par défaut.publicclass SudokuR1Chromosome27 : ChromosomeBase{privateconstdouble LO =1.0, HI =10.0;publicSudokuR1Chromosome27():base(EmptyCells27(ParsePuzzle27()).Count){CreateGenes();}publicoverride Gene GenerateGene(int index)=>newGene(RandomizationProvider.Current.GetDouble(LO, HI));publicoverride IChromosome CreateNew()=>newSudokuR1Chromosome27();publicdouble[]ToGenes(){var v =newdouble[Length];for(int i =0; i < Length; i++) v[i]=(double)GetGene(i).Value;return v;}publicint[,]ToGrid()=>DecodeR1_27(ToGenes());}// Fitness instrumentée : chaque évaluation est comptée — le budget se mesure, il ne se suppose pas.publicclass SudokuR1Fitness27 : IFitness{publicstaticint Evals;publicdoubleEvaluate(IChromosome chromosome){ Evals++;return-CountConflicts27(((SudokuR1Chromosome27)chromosome).ToGrid());}}publicstaticclass Mgs27Host{publicstatic(int conflicts,int evals,double ms,double[] genes)RunFbi(int seed,int popSize,int maxGens){// Seeding AVANT création de population : le RNG est consommé par CreateNew()// de chaque individu initial (leçon #12071 / MGS-21). FastRandomRandomization.ResetSeed(seed);var compound = MetaHeuristicsService.CreateMetaHeuristicByName("ForensicBasedInvestigation", maxGens, popSize);var adam =newSudokuR1Chromosome27();var pop =newMetaPopulation(popSize, popSize, adam);var ga =newMetaGeneticAlgorithm( pop,newSudokuR1Fitness27(),newEliteSelection(),newUniformCrossover(0.5f),newUniformMutation(true), compound); ga.Termination=newGenerationNumberTermination(maxGens); SudokuR1Fitness27.Evals=0;var sw = Stopwatch.StartNew(); ga.Start(); sw.Stop();var best =(SudokuR1Chromosome27)ga.BestChromosome;return(CountConflicts27(best.ToGrid()), SudokuR1Fitness27.Evals, sw.Elapsed.TotalMilliseconds, best.ToGenes());}}// Échauffement JIT (course jetée), puis course témoin graine 7.var warmupMgs = Mgs27Host.RunFbi(123,50,10);var demoMgs = Mgs27Host.RunFbi(7,50,200);Console.WriteLine($"MGS FBI (graine 7, témoin) : {demoMgs.Item1} conflits, "+ $"{demoMgs.Item2} évaluations, {demoMgs.Item3:F0} ms.");
Lecture. Le composé MGS ForensicBasedInvestigation est branché sur le même harnais que les paires précédentes : chromosome R1, fitness comptée, seeding avant création de population. La course témoin graine 7 donne le premier chiffre — 39 conflits pour 7 962 évaluations en 458 ms (run original : 322 ms) — et surtout la mesure structurelle du budget : ~40 évaluations par génération de 50 (la réinsertion pairwise de FBI ne soumet au duel que les enfants effectivement candidats), donc 200 générations ≈ 8 000 évaluations pour répondre aux 8 050 de mealpy (50 individus initiaux + 40 epochs × 4 phases × 50). L’échauffement JIT la précède pour que la course mesurée ne paie pas la compilation.
// === Le pont PythonNet : mealpy dans le même kernel, la même exécution ===// Recette validée MGS-22 (#12356) : pythonnet 3.1.0, DLL résolue par probe// (PYTHONNET_PYDLL d'abord, sinon installs standards par OS — aucun chemin machine en dur).// NB : pythonnet 3.1.0 exige CPython >= 3.13 (symbole PyThreadState_GetUnchecked absent// de python311.dll) — le probe résout donc la version la plus HAUTE disponible.#r "nuget: pythonnet,3.1.0"using Python.Runtime;staticstringResolvePythonDll27(){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)){var dirs = System.IO.Directory.GetDirectories(pyDir,"Python3*").OrderByDescending(d => System.IO.Path.GetFileName(d).Replace("Python","")).ToList();foreach(var d in dirs){var hit = System.IO.Directory.GetFiles(d,"python3*.dll");if(hit.Length==0)continue;// python3.dll (stub ABI stable, 10 chars) ne forwarde PAS// PyThreadState_GetUnchecked : preferer la DLL versionnee la// plus longue (ex. python313.dll), comme la branche miniconda.return hit.OrderByDescending(h => System.IO.Path.GetFileName(h).Length).First();}}}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 !=""){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=ResolvePythonDll27();PythonEngine.Initialize();// Le problème Python : decode + coût réimplémentés à l'identique, compteur d'évals,// solveur mealpy OriginalFBIO avec seed EXPLICITE en solve() (API 3.x — le seed du// constructeur est ignoré) et journal muet (log_to='nothing').publicstatic PyModule S27;using(Py.GIL()){ S27 = Py.CreateScope(); S27.Set("puzzle_line27", PuzzleLine27); S27.Exec(@"import sysimport mealpyfrom mealpy.human_based.FBIO import OriginalFBIOfrom mealpy import Problem, FloatVarimport json as _jsonimport inspect as _inspectpuzzle =[int(ch)for ch in puzzle_line27]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_fbio(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 =OriginalFBIO(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_fbio(seeds_json, pop_size, epoch, reps=3):out=[]for sd in _json.loads(seeds_json): runs =[run_mealpy_fbio(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]# Defaults mealpy OriginalFBIO(3.x): mesures, pas doc. FBI n'expose AUCUN parametre# scalaire au-dela d'epoch/pop_size — la signature fait foi._sig =str(_inspect.signature(OriginalFBIO.__init__))__defaults__ = 'mealpy OriginalFBIO defaults: signature ' + _sig + ' (aucun parametre scalaire transportable)'__mealpy_ver__ = 'mealpy ' + mealpy.__version__+ ' sur Python ' + sys.version.split()[0]"); Console.WriteLine($"Pont PythonNet actif : {S27.Get<string>("__mealpy_ver__")}"); Console.WriteLine($"Parametres defaut mealpy : {S27.Get<string>("__defaults__")}");}// --- 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[]LcgVector27(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[]{LcgVector27(1,36),LcgVector27(2,36),LcgVector27(3,36)};using(Py.GIL()){ S27.Set("__witness_json__", System.Text.Json.JsonSerializer.Serialize(witnessVectors.Select(v => v.ToList()).ToList())); S27.Exec(@"__py_costs__ = _json.dumps([cost_of_vector(v) for v in _json.loads(__witness_json__)])");var pyCosts = System.Text.Json.JsonSerializer.Deserialize<List<int>>(S27.Get<string>("__py_costs__"));var csCosts = witnessVectors.Select(v =>CountConflicts27(DecodeR1_27(v))).ToList();bool identical = pyCosts.SequenceEqual(csCosts); Console.WriteLine($"Sanity check cout : C# {string.Join(",", csCosts)} | Python {string.Join(",", pyCosts)} "+ $"-> {(identical ? "IDENTIQUE" : "DIFFERENT")}");}
Lecture. Le pont PythonNet est actif et la sanity check porte tout le bench : les trois vecteurs témoins LCG, décodés et coûtés indépendamment des deux côtés, doivent donner exactement les mêmes conflits. Sans cette égalité prouvée, une différence mesurée entre moteurs pourrait n’être qu’une différence entre les deux fonctions de coût. La signature mealpy OriginalFBIO est mesurée sur l’instance : FBI n’expose aucun paramètre au-delà de epoch/pop_size — c’est la ligne « Parametres defaut mealpy » ci-dessus qui fait foi pour le tableau du §1 : la première paire de l’Epic sans confond paramétrique.
2. Le croisement — 2 moteurs × 4 graines à budget égal
Population 50 × 200 générations côté MGS (~40 évaluations par génération), 40 epochs × population 50 côté mealpy (quatre phases par epoch) : les deux budgets visent ~8 000 évaluations et les compteurs rendent leur mesure. Graines {0, 1, 7, 42}, trois répétitions par graine des deux côtés pour la médiane de temps (amendement anti-pic GC), déterminisme exigé partout.
// === LE BENCH : 2 moteurs x 4 graines {0,1,7,42} — MGS pop 50 x 200 générations,// mealpy FBI pop 50 x 40 epochs : budgets mesurés par les compteurs, pas supposés. ===publicclass BenchRow27{publicint seed {get;set;}publicint conflicts {get;set;}publicbool all_same {get;set;}publicint evals {get;set;}publicdouble ms {get;set;}publicstring sol {get;set;}}int[] Seeds27 ={0,1,7,42};// --- Côté MGS (C#) : 3 répétitions par graine, ms = médiane (amendement §2) ---var mgsRows =new List<(int seed,int conflicts,int evals,double ms,bool allSame)>();foreach(var sd in Seeds27){var runs3 =new List<(int c,int e,double t)>();for(int rep =0; rep <3; rep++){var r = Mgs27Host.RunFbi(sd,50,200); runs3.Add((r.Item1, r.Item2, r.Item3));}var times = runs3.Select(x => x.t).OrderBy(t => t).ToList();double med = times[1]; mgsRows.Add((sd, runs3[0].c, runs3[0].e, med, runs3.All(x => x.c== runs3[0].c)));}// --- Côté mealpy (Python, boucle unique dans le scope) ---string mealpyJson;using(Py.GIL()){ S27.Set("__seeds_json__", System.Text.Json.JsonSerializer.Serialize(Seeds27.ToList())); S27.Exec(@"__bench_json__ = bench_mealpy_fbio(__seeds_json__, 50, 40)"); mealpyJson = S27.Get<string>("__bench_json__");}var mealpyRows = System.Text.Json.JsonSerializer.Deserialize<List<BenchRow27>>(mealpyJson);// --- Table ---staticdoubleMedian27(List<int> xs){var s = xs.OrderBy(x => x).ToList();return(s.Count%2==1)? s[s.Count/2]:(s[s.Count/2-1]+ s[s.Count/2])/2.0;}Console.WriteLine($"{"moteur",-9} {"graine",6} {"conflits",9} {"evals",7} {"ms",8} {"ms/eval",8}");foreach(var r in mgsRows) Console.WriteLine($"{"MGS",-9} {r.seed,6} {r.conflicts,9} {r.evals,7} {r.ms,8:F0} {r.ms / r.evals,8:F3}");foreach(var r in mealpyRows) Console.WriteLine($"{"mealpy",-9} {r.seed,6} {r.conflicts,9} {r.evals,7} {r.ms,8:F0} {r.ms / r.evals,8:F3}");var mgsC = mgsRows.Select(r => r.conflicts).ToList();var mpC = mealpyRows.Select(r => r.conflicts).ToList();double mgsMsEval = mgsRows.Average(r => r.ms/ r.evals);double mpMsEval = mealpyRows.Average(r => r.ms/ r.evals);Console.WriteLine();Console.WriteLine($"MGS : médiane conflits {Median27(mgsC):F1} (min {mgsC.Min()}, max {mgsC.Max()}), ms/éval moyen {mgsMsEval:F3}");Console.WriteLine($"mealpy : médiane conflits {Median27(mpC):F1} (min {mpC.Min()}, max {mpC.Max()}), ms/éval moyen {mpMsEval:F3}");Console.WriteLine($"Rapport ms/éval mealpy/MGS : {mpMsEval / mgsMsEval:F2}x");int detMgs = mgsRows.Count(r => r.allSame)+ mealpyRows.Count(r => r.all_same);Console.WriteLine($"Déterminisme : conflits identiques sur les 3 répétitions pour {detMgs}/8 paires graine-moteur.");
// === 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 K27 =500;var benchVecs =new List<double[]>();for(int i =0; i < K27; i++) benchVecs.Add(LcgVector27(42+ i,36));var csTimes =new List<double>();for(int rep =0; rep <5; rep++){var swRep = Stopwatch.StartNew();foreach(var v in benchVecs)CountConflicts27(DecodeR1_27(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()){ S27.Set("__vecs_json__", System.Text.Json.JsonSerializer.Serialize(benchVecs.Select(v => v.ToList()).ToList())); S27.Exec(@"__py_ms__ = time_python_evals(__vecs_json__)"); pyMs = S27.Get<double>("__py_ms__");}Console.WriteLine($"Fitness seule, {K27} vecteurs identiques (médiane de 5 répétitions par côté) :");Console.WriteLine($" C# : {csMs:F1} ms total -> {csMs / K27:F3} ms/éval");Console.WriteLine($" Python : {pyMs:F1} ms total -> {pyMs / K27: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# : 3,0 ms total -> 0,006 ms/éval
Python : 15,6 ms total -> 0,031 ms/éval
rapport Python/C# : 5,13x
Lecture du croisement. MGS passe devant — et pour la deuxième fois de l’Epic après le recuit (MGS-24), sur les deux axes à la fois, cette fois avec séparation totale :
qualité : médiane 37,0 contre 43,5 conflits, étendues [25-39] contre [43-45] — le meilleur mealpy (43) reste au-dessus du pire MGS (39). Aux graines : MGS gagne 4/4 (37 contre 43, 37 contre 44, 39 contre 45, 25 contre 43), la graine 42 offrant le meilleur run de la paire ;
vitesse : l’évaluation MGS est 5,63× moins chère (0,031 contre 0,174 ms/éval ; run original : 4,11×) — le plus grand écart moteur de l’Epic, devant le recuit (SA 3,26×, DE 2,37×, WOA 1,78×, EO 1,41×, PSO 0,8×-1,3× coude-à-coude — re-exécutions #13407 ; l’amplitude du ratio est sensible à la machine, mais l’ordre FBI devant SA tient dans les deux campagnes) ;
le protocole est tenu : 7 962 à 8 172 évaluations MGS contre 8 050 mealpy (±1,5 %), déterminisme 8/8, contre-vérification croisée du coût IDENTIQUE (45 = 45, cellule témoin ci-dessus).
Deux signatures à lire ensemble : mealpy est régulier mais médiocre (43-45 sur quatre graines), MGS dispersé mais gagnant partout (25-39). C’est l’inverse exact du WOA (MGS-25), où les deux colonnes MGS étaient ex æquo et mealpy derrière — ici l’écart entre les deux implémentations dépasse l’écart typique entre algorithmes différents de l’Epic.
Lecture du coût par évaluation. La fitness C# isolée reste 5,13× plus rapide (0,006 contre 0,031 ms/éval ; run original : 6,36× — l’amplitude du ratio fitness est sensible à la machine, l’ordre ne l’est pas ; même ordre que les 5,13× du PSO, 5,33× du SA, 5,56× de l’EO, 3,71× du DE — re-exécutions #13407). Cette fois l’écart moteur (5,63×) dépasse l’écart fitness (5,13×), à la différence de DE et SA : le composé MGS paie sa mécanique géométrique par évaluation, mais l’OriginalFBIO de mealpy (quatre phases NumPy par epoch, génération d’agents intermédiaires) paie encore plus cher.
Verdict de la paire. Deuxième doublé de l’Epic — le plus net : séparation totale de qualité et le plus grand écart de vitesse de l’Epic (5,63×, devant les 3,26× du recuit re-exécuté), sur la paire qui prétendait être un miroir. La divergence déclarée au §1 en est la lecture la plus économique : le bruit A1 du port MGS, N(0,1) par Box-Muller, a des queues plus lourdes que l’uniforme (−1, 1) de mealpy 3.0.2 — une perturbation du gène investigé parfois bien plus ample, cohérente avec une médiane plus basse ET une variance plus grande côté MGS. C’est une hypothèse déclarée, pas une preuve (aucun paramètre ne permet de l’isoler d’une exécution à l’autre) ; mais la paire démontre au moins ceci : deux implémentations qui se réclament de la même source ont ici plus divergé que des paires conçues différentes.
Exercice 1 : budget ×4 — l’écart de qualité se referme-t-il ?
MGS-22/23/24 posaient la même question pour PSO, DE et le recuit. Un écart qui se referme à budget accru dit « le moteur distillé converge plus lentement mais atteint le même plateau » ; un écart stable dit « plateau différent ». Côté MGS le budget ×4 se règle en générations (800) ; côté mealpy en epochs (160).
À compléter (décommentez dans la cellule suivante) :
1. Relancez Mgs27Host.RunFbi sur les 4 graines à pop 50, 640 générations.
2. Relancez bench_mealpy_fbio à pop 50, 160 epochs.
3. Verdict : les médianes se rapprochent-elles, ou l'écart du croisement est-il un plateau ?
// EXERCICE 1 : budget x4 — MGS pop 50, 800 générations ; mealpy pop 50, 160 epochs. 4 graines.// Décommentez et exécutez :// foreach (var sd in new[] {0, 1, 7, 42})// {// var r = Mgs27Host.RunFbi(sd, 50, 800);// Console.WriteLine($"MGS FBI x4 (graine {sd}) : {r.Item1} conflits, {r.Item2} évaluations, {r.Item3:F0} ms.");// }// using (Py.GIL())// {// S27.Set("__seeds_json__", System.Text.Json.JsonSerializer.Serialize(new[] {0, 1, 7, 42}.ToList()));// S27.Exec(@"__bench_x4_json__ = bench_mealpy_fbio(__seeds_json__, 50, 160)");// Console.WriteLine(S27.Get<string>("__bench_x4_json__"));// }Console.WriteLine("Exercice a completer (decommentez le bloc ci-dessus).");
Exercice a completer (decommentez le bloc ci-dessus).
Exercice 2 : population ×2 à budget égal — la diversité paie-t-elle ?
FBI est un algorithme de population : ses phases A2/B2 tirent des partenaires aléatoires, et leur efficacité dépend du réservoir de diversité disponible. Doubler la population (100) en dépensant le même budget d’évaluations divise les générations/epochs par deux : si l’écart du croisement tient à la diversité, il doit bouger ; s’il tient au moteur, il doit rester.
À compléter (décommentez dans la cellule suivante) :
1. Relancez Mgs27Host.RunFbi sur les 4 graines à pop 100, 80 générations.
2. Relancez bench_mealpy_fbio à pop 100, 20 epochs (100 individus initiaux + 20 × 400).
3. Verdict : à budget égal, plus de diversité change-t-elle le classement ?
// EXERCICE 2 : population x2 a budget egal — MGS pop 100, 100 générations ; mealpy pop 100,// 20 epochs. 4 graines.// Décommentez et exécutez :// foreach (var sd in new[] {0, 1, 7, 42})// {// var r = Mgs27Host.RunFbi(sd, 100, 100);// Console.WriteLine($"MGS FBI pop100 (graine {sd}) : {r.Item1} conflits, {r.Item2} évaluations, {r.Item3:F0} ms.");// }// using (Py.GIL())// {// S27.Set("__seeds_json__", System.Text.Json.JsonSerializer.Serialize(new[] {0, 1, 7, 42}.ToList()));// S27.Exec(@"__bench_p100_json__ = bench_mealpy_fbio(__seeds_json__, 100, 20)");// Console.WriteLine(S27.Get<string>("__bench_p100_json__"));// }Console.WriteLine("Exercice a completer (decommentez le bloc ci-dessus).");
Exercice a completer (decommentez le bloc ci-dessus).
Exercice 3 : budget réduit ×¼ — qui tient le petit budget ?
L’inverse de l’exercice 1 : 2 000 évaluations (MGS pop 50 × 50 générations ≈ 2 020 ; mealpy pop 50 × 10 epochs = 2 050). Un moteur qui garde une partie de son avance à petit budget a une convergence précoce plus efficace ; un moteur qui s’effondre vit de la longue course. Combiné à l’exercice 1, ce bracket encadre la réponse du budget sur les deux moteurs.
À compléter (décommentez dans la cellule suivante) :
1. Relancez Mgs27Host.RunFbi sur les 4 graines à pop 50, 40 générations.
2. Relancez bench_mealpy_fbio à pop 50, 10 epochs.
3. Verdict : le classement du croisement survit-il au petit budget ?
// EXERCICE 3 : budget reduit x1/4 — MGS pop 50, 50 générations ; mealpy pop 50, 10 epochs.// 4 graines.// Décommentez et exécutez :// foreach (var sd in new[] {0, 1, 7, 42})// {// var r = Mgs27Host.RunFbi(sd, 50, 50);// Console.WriteLine($"MGS FBI x1/4 (graine {sd}) : {r.Item1} conflits, {r.Item2} évaluations, {r.Item3:F0} ms.");// }// using (Py.GIL())// {// S27.Set("__seeds_json__", System.Text.Json.JsonSerializer.Serialize(new[] {0, 1, 7, 42}.ToList()));// S27.Exec(@"__bench_q_json__ = bench_mealpy_fbio(__seeds_json__, 50, 10)");// Console.WriteLine(S27.Get<string>("__bench_q_json__"));// }Console.WriteLine("Exercice a completer (decommentez le bloc ci-dessus).");
Exercice a completer (decommentez le bloc ci-dessus).