MGS-29 : GA MGS compose "Default" contre BaseGA mealpy — la revanche du GA
Paire 8/9 de l’EPIC #12373 (MGS vs mealpy — comparaison appariee). Suite de MGS-22 (PSO canonique), qui a refute l’hypothese user sur la vitesse mais revele un ecart de qualite de convergence a budget egal : mealpy OriginalPSO mediane 28,5 conflits, MGS ParticleSwarmOptimization mediane 43,5 — separation totale des gammes.
Ce grain specialise la comparaison au meme algorithme conceptuel (algorithme genetique) : compose MGS "Default" (celui de la colonne R1/GA de MGS-21, mediane 10,5 conflits a 8 000 evals) contre BaseGA de mealpy (mealpy.evolutionary_based.GA.BaseGA). Question directrice : l’ecart MGS-vs-mealpy observe sur PSO est-il une propriete systematique des MGS compounds geometriques, ou un artefact du moteur PSO choisi ?
Plan
Socle commun : representation R1 (continu [1,10)^36), cout conflits, decode, contre-verification croisee C# <-> Python (recette heritee de MGS-22 — preuve faite dans la cellule sanity check).
Moteurs :
Cote MGS : compose "Default" (GA canonique : elitisme + crossover uniforme + mutation uniforme, le wiring par defaut de la bibliotheque).
Lecture : l’ecart de qualite PSO-vs-PSO se reproduit-il GA-vs-GA ? Si oui, c’est une propriete de compound (strategie de recherche), pas du moteur PSO.
Acceptance : resultat « MGS perd en qualite, gagne en vitesse » ou l’inverse est un livrable valide — l’insight noyau est l’objet du grain, pas le score.
1. Le protocole apparie, pre-enregistre
Identique a MGS-22 pour permettre la comparaison entre paires (PSO/PSO, GA/GA, etc.).
Element
Valeur (fixee d’avance)
Grille
Easy[0] de Sudoku_Easy51 — la meme que MGS-21 et MGS-22 : 36 cellules vides, 45 indices
conflits totaux (lignes + colonnes + blocs) — la meme implementee deux fois (C# et Python) ; la sanity check PASSE 3/3 (67/67, 71/71, 60/60 sur vecteurs temoins LCG) ; la table rapporte conflits_C (re-eval C# via r.sol, contre-mesure croisee permanente) et conflits_P (brut Python, audit — les deux colonnes doivent coincider)
Moteurs
GA canonique : compose "Default" de MetaGeneticSharp contre BaseGA de mealpy
Budget
~8 000 evaluations par course : population 50 x 160 generations/epochs, evals reellement consommees instrumentees
Graines
{0, 1, 7, 42} — nommees, une par course, 4 courses par moteur
Mesures
(a) qualite : conflits finaux, mediane + min-max sur 4 graines · (b) vitesse : ms par course, mediane de 3 repetitions par graine
Criteres de lecture : (1) Qualite — un moteur domine si mediane strictement inferieure ET maxima sous les minima de l’autre, sinon « comparable ». (2) Vitesse — rapport des ms/eval moyens. (3) Le verdict reprend la these directrice — chaque axe rapporte separement, aucune compensation.
// === MGS-29 : socle commun — DLLs MGS, grille de reference, fonction de cout ===// Reprend le socle de MGS-22 : la representation 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 MEME que MGS-21 / MGS-22.publicstaticstring PuzzleLine29 ="902005403100063025508407060026309001057010290090670530240530600705200304080041950";publicstaticint[,]ParsePuzzle29(){var g =newint[9,9];for(int i =0; i <81; i++) g[i /9, i %9]= PuzzleLine29[i]-'0';return g;}// Fonction de cout du bench : conflits totaux (lignes + colonnes + blocs) sur grille PLEINE.publicstaticintCountConflicts29(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;}publicstaticintCountEmpty29(int[,] p){int n =0;foreach(var v in p)if(v ==0) n++;return n;}publicstatic List<(int r,int c)>EmptyCells29(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;}publicstaticint[,]DecodeR1_29(double[] genes){var Puzzle =ParsePuzzle29();var empties =EmptyCells29(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 Puzzle29 =ParsePuzzle29();Console.WriteLine($"Grille de reference : {CountEmpty29(Puzzle29)} cellules vides, "+ $"{81 - CountEmpty29(Puzzle29)} indices fixes, {EmptyCells29(Puzzle29).Count} genes R1.");
The below script needs to be able to find the current output cell; this is an easy method to get it.
Grille de reference : 36 cellules vides, 45 indices fixes, 36 genes R1.
Interpretation : le socle est pose
Reprend le substrat du bench MGS-22 (sans modification) : - vecteur continu de 36 genes, range [1, 10) - decode par arrondi + clamp vers {1, …, 9} - cout = conflits totaux sur la grille pleine
Identite des couts : cout C# et cout Python coincident — sanity check PASSE 3/3 (67/67, 71/71, 60/60 sur vecteurs temoins LCG), contre-verif croisee de la cellule 8 IDENTIQUE. Les deux moteurs optimisent le meme objectif (conflits totaux lignes + colonnes + blocs). La double comptabilite de la cellule 9 (conflits_C re-eval C#, conflits_P brut Python) sert de contre-mesure croisee permanente : les deux colonnes coincident et toute divergence future re-signale une derive du pont. Voir cellules 10 et 11 pour le verdict rejoue sous le vrai paysage.
// === Moteur MGS : compound "Default" (GA canonique : EliteSelection + UniformCrossover + UniformMutation) ===// Le compose "Default" est l'agencement GA par defaut de MetaGeneticSharp (cf colonne R1/GA de MGS-21).publicclass SudokuR1Chromosome29 : ChromosomeBase{privateconstdouble LO =1.0, HI =10.0;publicSudokuR1Chromosome29():base(EmptyCells29(ParsePuzzle29()).Count){CreateGenes();}publicoverride Gene GenerateGene(int index)=>newGene(RandomizationProvider.Current.GetDouble(LO, HI));publicoverride IChromosome CreateNew()=>newSudokuR1Chromosome29();publicdouble[]ToGenes(){var v =newdouble[Length];for(int i =0; i < Length; i++) v[i]=(double)GetGene(i).Value;return v;}publicint[,]ToGrid()=>DecodeR1_29(ToGenes());}publicclass SudokuR1Fitness29 : IFitness{publicstaticint Evals;publicdoubleEvaluate(IChromosome chromosome){ Evals++;return-CountConflicts29(((SudokuR1Chromosome29)chromosome).ToGrid());}}publicstaticclass Mgs29Host{publicstatic(int conflicts,int evals,double ms,double[] genes)RunGa(int seed,int popSize,int maxGens){// Seeding AVANT creation de population : le RNG est consomme par CreateNew()// de chaque individu initial (lecon MGS-21 / #12071). FastRandomRandomization.ResetSeed(seed);var compound = MetaHeuristicsService.CreateMetaHeuristicByName("Default", maxGens, popSize);var adam =newSudokuR1Chromosome29();var pop =newMetaPopulation(popSize, popSize, adam);var ga =newMetaGeneticAlgorithm( pop,newSudokuR1Fitness29(),newEliteSelection(),newUniformCrossover(0.5f),newUniformMutation(true), compound); ga.Termination=newGenerationNumberTermination(maxGens); SudokuR1Fitness29.Evals=0;var sw = Stopwatch.StartNew(); ga.Start(); sw.Stop();var best =(SudokuR1Chromosome29)ga.BestChromosome;return(CountConflicts29(best.ToGrid()), SudokuR1Fitness29.Evals, sw.Elapsed.TotalMilliseconds, best.ToGenes());}}// Echauffement JIT (course jetee), puis course temoin graine 7.var warmupMgs = Mgs29Host.RunGa(123,50,10);var demoMgs = Mgs29Host.RunGa(7,50,160);Console.WriteLine($"MGS GA Default (graine 7, temoin) : {demoMgs.Item1} conflits, "+ $"{demoMgs.Item2} evaluations, {demoMgs.Item3:F0} ms.");
Exercice 1 — Decomposer le cout : conflits par ligne uniquement
L’exemple ci-dessus (cellule 2) definit CountConflicts29(g) qui agrege lignes + colonnes + blocs. C’est ce qui optimise le bench. Mais le cout d’un puzzle n’est pas uniforme : sur la grille Easy[0], combien de conflits viennent des lignes seules, sans les colonnes ni les blocs ? La reponse est pedagogiquement interessante — elle dit ou se concentre la difficulte (les lignes sont le sous-objectif dominant, ou les colonnes, ou les blocs ?).
Objectif : ecrire une fonction CountRowConflicts29(g) qui retourne le nombre de doublons sur les 9 lignes uniquement (colonnes et bloques ignores). Reproduire la structure de CountConflicts29(g) en evitant le HashSet sur colonnes/blocs.
Indice 1 : un HashSet qui rejette l’ajout (row.Add retourne false) compte un doublon. Pour les lignes seules, c’est exactement la moitie du code de CountConflicts29(g). Pas besoin d’instrumenter colonnes ou blocs.
Indice 2 : valide sur le vecteur temoin LCG 1 de la cellule 6 — le cout total est dans l’output de cette cellule (invariant a reproduire, non revele dans l’enonce : aller lire la cellule 6). Ta decomposition doit rendre rowConflicts + colConflicts + blockConflicts == cout total cellule 6. Si tu obtiens le cout total directement sur les lignes, ton algo agrege par erreur — recommence.
Indice 3 : la convention C# est HashSet<int> (System.Collections.Generic). Utiliser Add qui retourne bool — le pattern est deja dans le code source de la cellule 2.
// Stub a completer par l'etudiant -- NE PAS supprimer ce cadre.publicstaticintCountRowConflicts29(int[,] g){// TODO etudiant : compter uniquement les doublons sur les 9 lignes// (chaque ligne est une suite de 9 chiffres ; un doublon = 2 cases de meme// valeur dans la meme ligne). Reproduire la moitie superieure du pattern// de CountConflicts29(g) (cf. cellule 2) sans les HashSet col et blk.return0;// TODO etudiant : remplacer par le compte reel}// Indice pedagogique (cf. cellule 5 du notebook) : le cout total = lignes + colonnes + blocs.// Ta decomposition doit rendre rowConflicts + colConflicts + blockConflicts == cout total.Console.WriteLine("Exercice 1 a completer (CountRowConflicts29 : doublons par ligne uniquement).");
Exercice 1 a completer (CountRowConflicts29 : doublons par ligne uniquement).
Interpretation : ce que le moteur MGS expose
Trois choix de wiring portent la validite du bench : 1. Le compteur d’evaluations instrumente : SudokuR1Fitness29.Evals compte chaque appel a Evaluate — le budget reel est mesure, pas suppose. 2. Le seeding avant population : FastRandomRandomization.ResetSeed(seed) est appele AVANT CreateMetaHeuristicByName, parce que le RNG est consomme par CreateNew() de chaque individu initial. Un seeding apres = une autre population que la semence declaree. 3. Le compose Default : c’est l’agencement GA canonique de MetaGeneticSharp (elitisme + crossover uniforme + mutation uniforme), equivalent structurel au BaseGA de mealpy cote wiring.
Le seul parametre qu’on ne controle pas est la valeur concrete des operateurs de mutation (taux, distribution) — MGS utilise des valeurs par defaut, mealpy aussi, et c’est precisement ce qui rend la comparaison interessante : « out of the box, que vaut chaque bibliotheque ? ».
// === Le pont PythonNet : mealpy dans le meme kernel, la meme execution ===// pythonnet 3.0.5 : la 3.1.0 cherche PyThreadState_GetUnchecked, absent de CPython 3.13 stable.#r "nuget: pythonnet,3.0.5"using Python.Runtime;staticstringResolvePythonDll(){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 hits = System.IO.Directory.GetFiles(d,"python3*.dll");// Prefer python3XX.dll (versionnee, contient PyThreadState_GetUnchecked)// sur python3.dll (stable ABI, qui en est depourvue en CPython 3.13).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 !="")return pick;}}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=ResolvePythonDll();PythonEngine.Initialize();// Le probleme Python : decode + cout reimplementes a l'identique, compteur d'evals,// solveur mealpy BaseGA avec seed EXPLICITE en solve().publicstatic PyModule S29;using(Py.GIL()){ S29 = Py.CreateScope(); S29.Set("puzzle_line29", PuzzleLine29); S29.Exec(@"import sysimport mealpyfrom mealpy.evolutionary_based.GA import BaseGAfrom mealpy import Problem, FloatVarimport json as _jsonpuzzle =[int(ch)for ch in puzzle_line29]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]classSudokuProblemGA(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_ga(seed, pop_size, epoch): import time PY_EVALS[0]=0 prob =SudokuProblemGA(bounds=FloatVar(lb=(1.0,)*len(empties), ub=(10.0,)*len(empties), name='genes'), minmax='min') model =BaseGA(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_ga(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)__mealpy_ver__ = 'mealpy ' + mealpy.__version__+ ' sur Python ' + sys.version.split()[0]"); Console.WriteLine($"Pont PythonNet actif : {S29.Get<string>("__mealpy_ver__")}");}// --- Sanity check : la fonction de cout est-elle la MEME des deux cotes ? ---publicstaticdouble[]LcgVector29(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;}return v;}var witnessVectors =new[]{LcgVector29(1,36),LcgVector29(2,36),LcgVector29(3,36)};using(Py.GIL()){ S29.Set("__witness_json__", System.Text.Json.JsonSerializer.Serialize(witnessVectors.Select(v => v.ToList()).ToList())); S29.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>>(S29.Get<string>("__py_costs_json__"));bool allEqual =true;for(int i =0; i < witnessVectors.Length; i++){int csCost =CountConflicts29(DecodeR1_29(witnessVectors[i]));bool eq = csCost == pyCosts[i]; allEqual &= eq; Console.WriteLine($" vecteur temoin {i + 1} : cout C# = {csCost}, cout Python = {pyCosts[i]} -> {(eq ? "IDENTIQUE" : "DIFFERENT")}");} Console.WriteLine(allEqual?"Sanity check PASSE : la fonction de cout est identique des deux cotes -- le bench est valide.":"Sanity check ECHOUE : les fonctions de cout different -- le bench serait invalide.");}
Installed Packages
pythonnet, 3.0.5
Pont PythonNet actif : mealpy 3.0.2 sur Python 3.13.3
vecteur temoin 1 : cout C# = 67, cout Python = 67 -> IDENTIQUE
vecteur temoin 2 : cout C# = 71, cout Python = 71 -> IDENTIQUE
vecteur temoin 3 : cout C# = 60, cout Python = 60 -> IDENTIQUE
Sanity check PASSE : la fonction de cout est identique des deux cotes -- le bench est valide.
Exercice 2 — Sensibilite a la graine du moteur MGS (pop_size reduit)
L’exemple ci-dessus (cellule 4) teste 4 graines fixes {0, 1, 7, 42} sur MGS GA compose "Default" avec pop_size=50, max_gens=160. Mais ces 4 graines representent-elles vraiment la variabilite du moteur, ou a-t-on eu de la chance sur un intervalle particulier ?
Objectif : etendre le balayage a 8 graines{0, 1, 7, 42, 100, 200, 300, 500} avec pop_size=30, max_gens=80 (budget reduit pour iterer vite). Pour chaque graine, retourner (seed, conflicts) en respectant le determinisme du moteur (allSame=true attend sur les 3 repetitions).
Indice 1 : Mgs29Host.RunGa(seed, popSize, maxGens) est l’API existante ; reutilise-la directement. Tu n’as pas besoin d’instrumenter de compteur d’evals — r.evals est deja dans le retour.
Indice 2 : pop_size=30, max_gens=80 = budget ~2400 evaluations (vs 8000 dans le bench principal). Avec 8 graines, le balayage complet tient en moins d’une seconde ; c’est volontaire — l’enjeu est pedagogique (sensibilite), pas qualite brute.
Indice 3 : pour valider, verifie que (swagC.Max() - swagC.Min()) >= 6 conflits sur les 8 graines (etendue attendue d’un GA compose). Si l’etendue est 0, le seeding FastRandomRandomization.ResetSeed(seed) n’a pas pris — voir cellule 5.
// Stub a completer par l'etudiant -- NE PAS supprimer ce cadre.publicstatic List<(int seed,int conflicts)>MgsSeedSweep29(int popSize,int maxGens){// TODO etudiant :// 1. definir le tableau de graines {0, 1, 7, 42, 100, 200, 300, 500}// 2. pour chaque graine, appeler Mgs29Host.RunGa(sd, popSize, maxGens) 3 fois// et verifier que les 3 conflits sont identiques (determinisme)// 3. retourner List<(int seed, int conflicts)> avec les conflits de la 1ere repreturnnew List<(int,int)>();// TODO etudiant : remplacer par le balayage}Console.WriteLine("Exercice 2 a completer (MgsSeedSweep29 : 8 graines, pop=30, max_gens=80, 3 reps par graine).");
Exercice 2 a completer (MgsSeedSweep29 : 8 graines, pop=30, max_gens=80, 3 reps par graine).
Interpretation : 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 cout. Si le C# comptait les doublons avec une convention differente du Python, les deux moteurs n’optimiseraient pas le meme paysage, et chaque milliseconde comparee serait du bruit raffine. D’ou le protocole en deux temps : - des vecteurs temoins deterministes (LCG ecrit a la main, 3 seeds) ; - cout C# et cout Python calcules sur les MEMES vecteurs ; - identite verifiee sur les 3 paires.
Si la sanity check passe, le bench est valide : ce qu’on compare, c’est bien deux optimiseurs sur le MEME paysage. Sinon, on ne compare rien — on declare forfait et on corrige le cout avant de continuer.
Exercice 3 — Trace de convergence epoch-par-epoch (BaseGA, graine 7)
Cette cellule precede le bench ci-dessous (cellule 12) : elle expose le dernier coutdernier cout mealpy sur la graine 7 : 16 conflits apres 8 050 evaluations. Mais le cout intermediaire raconte une autre histoire — combien de generations pour arriver sous 30 conflits ? A-t-on plafonne a la generation 80 ou 140 ?
mealpy expose la trace complete via model.history.list_global_best_fit (un float par epoch, non-croissant en qualite quand on minimise).
Objectif : recuperer la trace des best_cost epoch-par-epoch pour mealpy BaseGA graine 7 (pop_size=50, epoch=160) et la retourner sous forme de List<double> de longueur 160.
Indice 1 : model.history.list_global_best_fit est alimente automatiquement a chaque fin d’epoch par mealpy 2.x+. Si l’attribut n’existe pas sur ta version, fallback possible via model.solve(prob, seed=seed, mode="process") (mealpy 3.x) ou instrumentation manuelle (re-run dans une boucle).
Indice 2 : la trace doit etre collectee dans le scope Python (S29 via S29.Exec(...)), pas en C#. Tu recuperes le resultat via S29.Get<string>("__trace_json__") apres serialisation JSON.
Indice 3 : pour benchmarker ta solution, verifie que trace[0] est superieur a trace[-1] (le cout descend au fil des epochs). Si ce n’est pas le cas, ta trace est inversee ou l’instrumentation est cassee.
// Stub a completer par l'etudiant -- NE PAS supprimer ce cadre.publicstatic List<double>MealpyConvergenceTrace29(int seed,int popSize,int epoch){// TODO etudiant :// 1. ouvrir le GIL via `using (Py.GIL())`// 2. appeler run_mealpy_ga_trace(seed, popSize, epoch) que tu auras ajoute cote Python// dans le scope S29 (definir la fonction en plus de run_mealpy_ga) -- elle doit retourner// _json.dumps(list_global_best_fit) au lieu de la solution finale// 3. serialiser la List<double> retournee par Python en JSON via System.Text.Json// et la deserialiser ici en List<double>// 4. retourner la liste (longueur == epoch)returnnew List<double>();// TODO etudiant : remplacer par la trace collectee}Console.WriteLine("Exercice 3 a completer (MealpyConvergenceTrace29 : trace epoch-par-epoch, graine 7, 160 epochs).");
Les mesures ci-dessus repondent a la these directrice de l’EPIC en miroir de MGS-22 : - Si MGS "Default" (compose GA) montre le meme type d’ecart que ParticleSwarmOptimization face a mealpy (qualite inferieure a budget egal, ex-aequo ou avantage en vitesse), c’est une propriete systematique des MGS compounds : la bibliotheque minimise plus lentement le cout que les bibliotheques de reference Python. - Si au contraire l’ecart se referme (qualite comparable ou inverse), c’est un artefact du moteur PSO : le compose geometrique de particule a une specificite que le GA pur n’a pas.
Dans les deux cas, l’insight est « systematique vs specifique » — c’est ce qu’aucune paire isolee ne peut trancher. Cette mesure est la deuxieme d’un futur tableau 5x4 (5 paires x 4 graines minimum).
Lecture des chiffres (verdict rejoue sous le vrai paysage) : qualite comparable — mealpy 12,5 [9-16] vs MGS 13,5 [12-18], medianes proches et etendues largement chevauchees (le critere pre-enregistre exige mediane strictement inferieure ET maxima sous les minima : non verifie, mealpy garde le max 16 au-dessus du min MGS 12). Vitesse : MGS devant, 7,83x moins cher par evaluation (0,017 vs 0,136 ms/eval). Determinisme 8/8. Pour la these directrice : l’ecart de qualite du PSO (separation totale en MGS-22) ne se reproduit pas GA contre GA — c’etait un artefact du moteur PSO, pas une propriete systematique des MGS compounds ; le GA compose tient la comparaison qualite tout en gardant l’avantage vitesse.
Resume et suite pour l’EPIC
Resultat experimental : voir la table de la cellule 9. La comparaison se lit sur deux axes : 1. Qualite (conflits finaux, mediane + min-max sur 4 graines) : comparable — mealpy 12,5 [9-16] vs MGS 13,5 [12-18], etendues chevauchees 2. Vitesse (ms/eval moyen) : MGS devant, 7,83x moins cher par evaluation
Place dans l’Epic : MGS-29 est la paire 8/9 (Default GA compose MGS vs BaseGA mealpy) — voir EPIC #12373. Paires deja livrees (MGS-22 a MGS-28) : PSO, DifferentialEvolution, SimulatedAnnealing, WhaleOptimisation, EquilibriumOptimizer, ForensicBasedInvestigation, BareBonesPSO. Restant apres ce grain : paire 9 (ScatterSearch).
Contre-mesure croisee : conflits_C / conflits_P sont double-comptabilisees et coincident sur les 4 graines ; toute divergence re-signale une derive du pont.
Suite logique : ScatterSearch est un compound qui n’a pas de jumeau evident dans mealpy — le grain commencera par etablir s’il en existe un, et conclura honnetement s’il n’y en a pas (cf ligne 7 de l’EPIC).