Kernel : .NET (C#) — pont PythonNet vers mealpy dans la même exécution
Introduction
MGS-27 a refermé la question FBI sur la paire qui prétendait le moins à la différence : le port C# dérivé battait sa référence mealpy des deux mains. La paire 7 descend d’un cran encore dans la prétention — et change de nature : le Bare Bones PSO de Kennedy (2003) est le PSO dépouillé de toute sa mécanique, ni inertie w, ni coefficients c1/c2 : chaque gène de la nouvelle particule est un simple tirage gaussien autour du milieu entre le meilleur personnel et le meilleur global. Deux conséquences structurelles gouvernent ce protocole :
Aucun PSO de mealpy 3.0.2 n’est bare-bones — les sept variantes du module (OriginalPSO, P_PSO, C_PSO, CL_PSO, AIW_PSO, HPSO_TVAC, LDW_PSO) conservent toutes une vitesse explicite et un couple de coefficients de vitesse — constantes w/c1/c2 (ou leurs variantes w_min/w_max/ci/cf/c_local) pour six d’entre elles, coefficients sin/cos dynamiques pour P_PSO ; en prendre une pour jumeau serait mesurer un PSO classique déguisé en bare-bones. Le jumeau mealpy est donc un subclass de Optimizer — le point d’extension natif de la bibliothèque — qui pinne la formule de Kennedy dans le harnais mealpy (graine, comptage d’évaluations, correction de bornes).
Le portage MGS est ancré sur la position courante, pas sur le pbest — le framework géométrique n’expose pas de mémoire pbest_i par particule, et l’écart est documenté in-source dans BareBonesParticleSwarm.cs. Pour distinguer l’écart noyau (moteur contre moteur) de l’écart d’ancrage (sémantique du portage), le bench croise donc trois bras : le composé MGS, la variante mealpy alignée sur sa sémantique, et le Kennedy 2003 littéral.
La grille, la représentation R1, le budget (population 50, 160 générations) et les graines {0, 1, 7, 42} sont ceux des paires précédentes : les médianes se lisent dans la même colonne du tableau croisé de l’Epic.
// === MGS-28 : socle commun — DLLs MGS, grille de référence, fonction de coût ===// Même socle que MGS-22/23 : 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/25/27.publicstaticstring PuzzleLine28 ="902005403100063025508407060026309001057010290090670530240530600705200304080041950";publicstaticint[,]ParsePuzzle28(){var g =newint[9,9];for(int i =0; i <81; i++) g[i /9, i %9]= PuzzleLine28[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.publicstaticintCountConflicts28(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;}publicstaticintCountEmpty28(int[,] p){int n =0;foreach(var v in p)if(v ==0) n++;return n;}publicstatic List<(int r,int c)>EmptyCells28(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_28(double[] genes){var Puzzle =ParsePuzzle28();var empties =EmptyCells28(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 Puzzle28 =ParsePuzzle28();Console.WriteLine($"Grille de référence : {CountEmpty28(Puzzle28)} cellules vides, "+ $"{81 - CountEmpty28(Puzzle28)} indices fixes, {EmptyCells28(Puzzle28).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-23 au nom près — c’est voulu : la comparabilité des paires de l’Epic repose sur un protocole strictement apparié (même grille Easy[0], même représentation R1 continue à 36 gènes, même fonction de coût comptée en conflits, renvoyant 0 ssi la grille est résolue). Le pont PythonNet qui suivra réimplémente ce comptage à l’identique et le vérifie sur des vecteurs témoins déterministes : la seule différence mesurable entre les bras sera donc le moteur, pas l’échelle de la fitness.
// === Moteur MGS : chromosome R1, fitness instrumentée, composé BareBonesParticleSwarm ===// Kennedy (2003) "Bare Bones" : chaque gène est échantillonné N((ancre + gbest)/2, |gbest - ancre|).// Ni inertie w ni c1/c2 — les constantes de Clerc (w=0,7298 ; c1=c2=1,49618) NE S'APPLIQUENT PAS// à ce composé : un BBPSO qui les porterait serait un PSO classique déguisé.publicclass SudokuR1Chromosome28 : ChromosomeBase{privateconstdouble LO =1.0, HI =10.0;publicSudokuR1Chromosome28():base(EmptyCells28(ParsePuzzle28()).Count){CreateGenes();}publicoverride Gene GenerateGene(int index)=>newGene(RandomizationProvider.Current.GetDouble(LO, HI));publicoverride IChromosome CreateNew()=>newSudokuR1Chromosome28();publicdouble[]ToGenes(){var v =newdouble[Length];for(int i =0; i < Length; i++) v[i]=(double)GetGene(i).Value;return v;}publicint[,]ToGrid()=>DecodeR1_28(ToGenes());}// Fitness instrumentée : chaque évaluation est comptée — le budget se mesure, il ne se suppose pas.publicclass SudokuR1Fitness28 : IFitness{publicstaticint Evals;publicdoubleEvaluate(IChromosome chromosome){ Evals++;return-CountConflicts28(((SudokuR1Chromosome28)chromosome).ToGrid());}}publicstaticclass Mgs28Host{// Trajectoire best-so-far : conflits du meilleur APRÈS chaque génération (event GenerationRan).publicstatic List<int>RunTrajectory(int seed,int popSize,int maxGens,outint conflicts,outint evals,outdouble ms,outdouble[] genes){// 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("BareBonesParticleSwarm", maxGens, popSize);var adam =newSudokuR1Chromosome28();var pop =newMetaPopulation(popSize, popSize, adam);var ga =newMetaGeneticAlgorithm( pop,newSudokuR1Fitness28(),newEliteSelection(),newUniformCrossover(0.5f),newUniformMutation(true), compound); ga.Termination=newGenerationNumberTermination(maxGens);var traj =new List<int>(maxGens +1); ga.GenerationRan+=(s, e)=> traj.Add(CountConflicts28(((SudokuR1Chromosome28)ga.BestChromosome).ToGrid())); SudokuR1Fitness28.Evals=0;var sw = Stopwatch.StartNew(); ga.Start(); sw.Stop();var best =(SudokuR1Chromosome28)ga.BestChromosome; conflicts =CountConflicts28(best.ToGrid()); evals = SudokuR1Fitness28.Evals; ms = sw.Elapsed.TotalMilliseconds; genes = best.ToGenes();return traj;}// Checkpoints 25/50/75/100 % du budget : trajectoire indexée après la génération correspondante.publicstatic(int conflicts,int evals,double ms,double[] genes,int[] cp)RunBbpso(int seed,int popSize,int maxGens){var traj =RunTrajectory(seed, popSize, maxGens,outvar c,outvar e,outvar t,outvar g);var q =new[]{ traj[maxGens /4-1], traj[maxGens /2-1], traj[3* maxGens /4-1], traj[maxGens -1]};return(c, e, t, g, q);}}// Échauffement JIT (course jetée), puis course témoin graine 7.var warmupMgs = Mgs28Host.RunBbpso(123,50,10);var demoMgs = Mgs28Host.RunBbpso(7,50,160);Console.WriteLine($"MGS BBPSO (graine 7, témoin) : {demoMgs.Item1} conflits, {demoMgs.Item2} évaluations, "+ $"{demoMgs.Item3:F0} ms, checkpoints 25/50/75/100 % = {string.Join("/", demoMgs.Item5)}.");
Lecture. Le composé BareBonesParticleSwarm est la transcription directe du Bare Bones Particle Swarms de Kennedy (SIS 2003) dans la grammaire géométrique MGS : l’opérateur crossver géométrique à deux parents lit [position courante, gbest] et tire chaque gène dans N((x_i + gbest)/2, |gbest - x_i|) — la formule est pinnée in-source (DefaultSampleOperator, Box-Muller sur l’IRandomization). Trois propriétés à lire dans le portage : (1) pas de mémoire pbest — l’ancre personnelle est la position courante (variante « gbest-ancrée », écart documenté dans le fichier source, la cellule Formules le croise) ; (2) gel au meilleur global — quand une particule est le gbest, l’écart-type s’annule et son tirage retombe sur elle-même : l’élite ne bouge pas, par construction ; (3) sélection élitiste héritée (FitnessBasedElitistReinsertion, best-N parents+enfants) — le garde-fou qui empêche un mauvais tirage gaussien de détruire l’incumbent. La trajectoire best-so-far est enregistrée à chaque génération via l’événement GenerationRan : les checkpoints 25/50/75/100 % du budget en sont extraits pour la classification de la forme d’écart.
// === 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).#r "nuget: pythonnet,3.1.0"using Python.Runtime;staticstringResolvePythonDll28(){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];}}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=ResolvePythonDll28();PythonEngine.Initialize();// Le problème Python : decode + coût réimplémentés à l'identique, compteur d'évals,// et le JUMEAU BARE-BONES : subclass Optimizer (point d'extension natif mealpy) pinnant// la formule de Kennedy 2003 — aucune des 7 variantes PSO de mealpy 3.0.2 n'est bare-bones.publicstatic PyModule S28;using(Py.GIL()){ S28 = Py.CreateScope(); S28.Set("puzzle_line28", PuzzleLine28); S28.Exec(@"import sysimport numpy as npimport mealpyfrom mealpy import Problem, FloatVar, Optimizerfrom mealpy.swarm_based.PSO import OriginalPSO, P_PSO, C_PSO, CL_PSO, AIW_PSO, HPSO_TVAC, LDW_PSOimport json as _jsonpuzzle =[int(ch)for ch in puzzle_line28]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)))classOriginalBBPSO(Optimizer): # Bare Bones PSO(Kennedy 2003) — jumeau mealpy du composé MGS BareBonesParticleSwarm. # Formule pinnée : x_new ~N((ancre + gbest)/2,|gbest - ancre|) — aucune constante de Clerc. # variant='kennedy' : ancre =pbest_i(local_solution), remplacement INCONDITIONNEL — papier littéral. # variant='mgs' : ancre = position courante x_i, sélection gloutonne per-particle — # sémantique alignée sur le composé MGS(ancre Current + réinsertion élitiste). def __init__(self, epoch=10000, pop_size=100, variant='kennedy',**kwargs):super().__init__(**kwargs) self.epoch= self.validator.check_int('epoch', epoch,[1,100000]) self.pop_size= self.validator.check_int('pop_size', pop_size,[2,10000]) self.variant= variant self.checkpoints={} def generate_agent(self, solution=None): agent = self.generate_empty_agent(solution) agent.target= self.get_target(agent.solution) agent.local_solution= agent.solution.copy() agent.local_target= agent.target.copy()return agent def evolve(self, epoch): # Checkpoint APRÈS l'epoch m : enregistré à l'entrée de l'epoch m+1(g_best à jour).if epoch -1in(self.epoch// 4, self.epoch // 2, 3 * self.epoch // 4): self.checkpoints[epoch -1]=float(self.g_best.target.fitness)for idx inrange(self.pop_size): anchor = self.pop[idx].local_solutionif self.variant== 'kennedy' else self.pop[idx].solution g = self.g_best.solution mean =0.5*(anchor + g) std = np.abs(g - anchor) pos_new = self.correct_solution(self.generator.normal(mean, std)) target = self.get_target(pos_new)if self.variant== 'kennedy': self.pop[idx].update(solution=pos_new.copy(), target=target.copy()) elif self.compare_target(target, self.pop[idx].target, self.problem.minmax): self.pop[idx].update(solution=pos_new.copy(), target=target.copy())if self.compare_target(target, self.pop[idx].local_target, self.problem.minmax): self.pop[idx].update(local_solution=pos_new.copy(), local_target=target.copy())def run_mealpy_bbpso(seed, pop_size, epoch, variant='kennedy'): 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 =OriginalBBPSO(epoch=epoch, pop_size=pop_size, variant=variant) t0 = time.perf_counter() g_best = model.solve(prob, seed=seed) dt =(time.perf_counter()- t0)*1000.0 cps =[int(model.checkpoints[epoch // 4]), int(model.checkpoints[epoch // 2]),int(model.checkpoints[3* epoch // 4]), int(g_best.target.fitness)] sol = _json.dumps([float(v)for v in g_best.solution])returncost(decode(g_best.solution)), PY_EVALS[0], dt, sol, cpsdef bench_mealpy_bbpso(seeds_json, pop_size, epoch, reps=3, variant='kennedy'):out=[]for sd in _json.loads(seeds_json): runs =[run_mealpy_bbpso(sd, pop_size, epoch, variant)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, 'cp': runs[0][4], '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 : {S28.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[]LcgVector28(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[]{LcgVector28(1,51),LcgVector28(2,51),LcgVector28(3,51)};using(Py.GIL()){ S28.Set("__witness_json__", System.Text.Json.JsonSerializer.Serialize(witnessVectors.Select(v => v.ToList()).ToList())); S28.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>>(S28.Get<string>("__py_costs__"));var csCosts = witnessVectors.Select(v =>CountConflicts28(DecodeR1_28(v))).ToList();bool identical = pyCosts.SequenceEqual(csCosts); Console.WriteLine($"Sanity check cout : C# {string.Join(",", csCosts)} | Python {string.Join(",", pyCosts)} "+ $"-> {(identical ? "IDENTIQUE" : "DIFFERENT")}");}
Installing Packages
pythonnet
Pont PythonNet actif : mealpy 3.0.2 sur Python 3.13.12
Sanity check cout : C# 67,71,60 | Python 67,71,60 -> IDENTIQUE
Lecture. Le pont est actif et la sanity check porte tout le bench : la fonction de coût Python redonne exactement le comptage C# sur les vecteurs témoins déterministes. Le point structurant est le subclass OriginalBBPSO : mealpy n’offre aucun PSO bare-bones (six des sept variantes du module exposent explicitement w/c1/c2 — ou leurs variantes w_min/w_max/ci/cf/c_local — et la septième, P_PSO, porte des coefficients sin/cos dynamiques sur la même mise à jour de vitesse), et le point d’extension officiel de la bibliothèque est précisément la classe Optimizer — le jumeau hérite donc du harnais mealpy complet (générateur seedé par solve(prob, seed=...), comptage d’évaluations par Problem, correction de bornes correct_solution, suivi g_best) en pinnant la formule de Kennedy. Les deux variantes du jumeau (kennedy, mgs) ne diffèrent que par l’ancre et la règle de remplacement — tout le reste (harnais, budget, graine) est identique par construction.
// === CELLULE FORMULES : les deux jumeaux AVANT toute mesure (exigence du cadrage paire 7) ===// Objectif : détecter un PSO classique déguisé AVANT de mesurer, puis trancher les divergences// d'ancre et de sélection pour que l'écart mesuré s'interprète (noyau vs portage), pas le subir.using(Py.GIL()){// 1) La détection Clerc : aucun PSO mealpy 3.0.2 n'est bare-bones — six portent w/c1/c2// (ou leurs variantes w_min/w_max/ci/cf/c_local) ; P_PSO porte des coefficients// sin/cos dynamiques sur la même mise à jour de vitesse. S28.Exec(@"__pso_catalog__ = ''for _cls in(OriginalPSO, P_PSO, C_PSO, CL_PSO, AIW_PSO, HPSO_TVAC, LDW_PSO): _m =_cls(epoch=10, pop_size=5) _params ={k: v for k, v in _m.__dict__.items()if k in('w', 'c1', 'c2', 'w_min', 'w_max', 'ci', 'cf', 'c_local')} __pso_catalog__ += f'{_cls.__name__:10s} ' +str(sorted(_params.items()))+chr(10)"); Console.WriteLine("Catalogue PSO mealpy 3.0.2 (aucun bare-bones natif — d'ou le subclass) :"); Console.WriteLine(S28.Get<string>("__pso_catalog__"));// 2) La propriété de gel du bare-bones : std = 0 => tirage déterministe = la moyenne. S28.Exec(@"_rng = np.random.default_rng(0)_loc = np.array([3.5,7.25]); _std0 = np.zeros(2)_draws =[_rng.normal(_loc, _std0)for _ inrange(4)]__freeze__ = 'normal(loc, std=0)-> ' +str([list(map(float, d))for d in _draws])+ ' (deterministe, egale la moyenne)'"); Console.WriteLine($"Propriete de gel (numpy) : {S28.Get<string>("__freeze__")}");}Console.WriteLine(@"Formules pinnées des jumeaux : MGS BareBonesParticleSwarm(in-source, DefaultSampleOperator, Box-Muller): x_new ~N((x_i + gbest)/2,|gbest - x_i|)[ancre = position COURANTE x_i] selection : FitnessBasedElitistReinsertion — best-N parents+enfants(glouton pool) mealpy OriginalBBPSO(variant='kennedy') — Kennedy 2003 litteral : x_new ~N((pbest_i + gbest)/2,|gbest - pbest_i|)[ancre = pbest_i] selection : remplacement INCONDITIONNEL(la memoire vit dans pbest/gbest) mealpy OriginalBBPSO(variant='mgs') — aligne sur la semantique du compose MGS : x_new ~N((x_i + gbest)/2,|gbest - x_i|)[ancre = position courante] selection : glouton per-particle(garde le parent s'il reste meilleur)TRANCHE : deux divergences documentees — (1) l'ANCRE(courante vs pbest),(2) la SELECTION(best-N pool vs per-particle vs inconditionnel). L'ecart MGS<->mealpy brut les melange :le bench croise donc TROIS bras -> bras 1 MGS vs bras 2 mealpy-mgs : memes ancre+glouton => ECART NOYAU bras 2 mealpy-mgs vs bras 3 mealpy-kennedy : meme harnais/kernel => ECART D'ANCRAGE bras 1 MGS vs bras 3 mealpy-kennedy : l'ecart brut des paires precedentes.");
Catalogue PSO mealpy 3.0.2 (aucun bare-bones natif — d'ou le subclass) :
OriginalPSO [('c1', 2.05), ('c2', 2.05), ('w', 0.4)]
P_PSO []
C_PSO [('c1', 2.05), ('c2', 2.05), ('w_max', 0.9), ('w_min', 0.4)]
CL_PSO [('c_local', 1.2), ('w_max', 0.9), ('w_min', 0.4)]
AIW_PSO [('c1', 2.05), ('c2', 2.05)]
HPSO_TVAC [('cf', 0.1), ('ci', 0.5)]
LDW_PSO [('c1', 2.05), ('c2', 2.05), ('w_max', 0.9), ('w_min', 0.4)]
Propriete de gel (numpy) : normal(loc, std=0) -> [[3.5, 7.25], [3.5, 7.25], [3.5, 7.25], [3.5, 7.25]] (deterministe, egale la moyenne)
Formules pinnées des jumeaux :
MGS BareBonesParticleSwarm (in-source, DefaultSampleOperator, Box-Muller) :
x_new ~ N((x_i + gbest)/2, |gbest - x_i|) [ancre = position COURANTE x_i]
selection : FitnessBasedElitistReinsertion — best-N parents+enfants (glouton pool)
mealpy OriginalBBPSO(variant='kennedy') — Kennedy 2003 litteral :
x_new ~ N((pbest_i + gbest)/2, |gbest - pbest_i|) [ancre = pbest_i]
selection : remplacement INCONDITIONNEL (la memoire vit dans pbest/gbest)
mealpy OriginalBBPSO(variant='mgs') — aligne sur la semantique du compose MGS :
x_new ~ N((x_i + gbest)/2, |gbest - x_i|) [ancre = position courante]
selection : glouton per-particle (garde le parent s'il reste meilleur)
TRANCHE : deux divergences documentees — (1) l'ANCRE (courante vs pbest), (2) la SELECTION
(best-N pool vs per-particle vs inconditionnel). L'ecart MGS<->mealpy brut les melange :
le bench croise donc TROIS bras ->
bras 1 MGS vs bras 2 mealpy-mgs : memes ancre+glouton => ECART NOYAU
bras 2 mealpy-mgs vs bras 3 mealpy-kennedy : meme harnais/kernel => ECART D'ANCRAGE
bras 1 MGS vs bras 3 mealpy-kennedy : l'ecart brut des paires precedentes.
Lecture. La tranche est posée avant la mesure, comme l’exige le cadrage : sans elle, un écart MGS↔︎mealpy sur cette paire serait ininterprétable — il mêlerait le moteur (C# GéneticSharp + composé géométrique contre NumPy + harnais mealpy) et la sémantique du portage (ancre courante contre pbest, glouton pool contre per-particle contre remplacement inconditionnel). Le catalogue de la cellule établit la détection Clerc : six des sept PSO mealpy exposent explicitement les constantes de vitesse (w/c1/c2 ou leurs variantes), et le septième, P_PSO, porte des coefficients sin/cos dynamiques sur la même mise à jour de vitesse — aucun ne prétend au bare-bones (la sortie le montre : la ligne P_PSO est la seule à ne lister aucun paramètre), et le subclass est la seule voie honnête vers un jumeau. La propriété de gel (écart-type nul ⇒ tirage déterministe) est le comportement littéral de Kennedy : l’élite ne bouge pas tant qu’elle est le gbest, et c’est une propriété, pas un bug à corriger — l’Exercice 2 en mesurera le prix.
// === 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 K28 =500;var benchVecs =new List<double[]>();for(int i =0; i < K28; i++) benchVecs.Add(LcgVector28(42+ i,51));var csTimes =new List<double>();for(int rep =0; rep <5; rep++){var swRep = Stopwatch.StartNew();foreach(var v in benchVecs)CountConflicts28(DecodeR1_28(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()){ S28.Set("__vecs_json__", System.Text.Json.JsonSerializer.Serialize(benchVecs.Select(v => v.ToList()).ToList())); S28.Exec(@"__py_ms__ = time_python_evals(__vecs_json__)"); pyMs = S28.Get<double>("__py_ms__");}Console.WriteLine($"Fitness seule, {K28} vecteurs identiques (médiane de 5 répétitions par côté) :");Console.WriteLine($" C# : {csMs:F1} ms total -> {csMs / K28:F3} ms/éval");Console.WriteLine($" Python : {pyMs:F1} ms total -> {pyMs / K28: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,1 ms total -> 0,006 ms/éval
Python : 19,3 ms total -> 0,039 ms/éval
rapport Python/C# : 6,30x
Lecture du croisement.
Classement : les deux jumeaux mealpy ex æquo en tête (médiane 25,5 conflits chacun), MGS troisième (30,5). L’écart brut de 5 conflits est donc imputable au moteur, pas au portage.
Écart d’ancrage nul — la paire la plus propre de l’Epic : les deux jumeaux mealpy (ancre local_solution = pbest contre ancre solution = position courante, remplacement inconditionnel contre glouton) produisent des trajectoires identiques graine par graine — mêmes conflits finaux et mêmes checkpoints sur les 4 graines (écarts [0,0,0,0] partout). La divergence tranchée dans la cellule formules ne change rien ici : sur ce problème, la mise à jour locale gloutonne confond pbest et position courante dès qu’une particule s’améliore.
Écart noyau réel, 5 conflits pour mealpy : à sémantique strictement alignée (le bras mealpy-mgs porte exactement la formule MGS dans le harnais mealpy), mealpy atteint 25,5 contre 30,5 — la différence vient du moteur (harnais mealpy : évaluation initiale de la population, update_global_best post-epoch), pas des équations. La forme le confirme, mais pas uniformément selon la graine : au premier quart MGS mène sur deux graines (7 : 29 contre 32 ; 42 : 30 contre 32) et fait jeu égal sur la graine 1 (31 contre 31), mais il est déjà derrière sur la graine 0 (33 contre 32) — le profil de cette graine est persistant (delta [1,9,11,12], l’écart s’installe et grandit), là où les graines 1/7/42 sont classées mixtes ([0,5,6,6], [-3,3,3,3], [-2,0,2,3] : MGS repasse derrière mealpy au plus tard au troisième quart, après avoir été devant ou à égalité au premier). Concrètement, mealpy continue d’exploiter en fin de course (20→19 et 28→27 entre 75 % et 100 %) quand MGS est figé après le premier quart — cp25 = cp100 sur 3 graines sur 4, la graine 0 plaçant sa seule amélioration entre 25 % et 50 % — la propriété de gel de la cellule formules, visible dans la sortie.
Vitesse : MGS reste environ deux fois moins cher par évaluation — le rapport ms/éval de ce passage kernel (0,38×) est lu dans la cellule de bench, et la fitness C# isolée reste plus rapide d’un facteur du même ordre (rapport Python/C# : 6,30× dans la cellule de coût par évaluation). Ce sont des mesures machine : les valeurs absolues ne sont donc pas recopiées ici, seuls les rapports le sont — et ces rapports eux-mêmes varient d’un passage kernel à l’autre ; le verdict ne repose pas sur eux — mais le budget est apparié (8 000/8 050 évaluations), donc mealpy gagne à budget égal. C’est le profil inversé du FBI (MGS-27) : là-bas le portage dérivait et gagnait des deux mains ; ici le portage est fidèle, ne dérive pas — et c’est le harnais adverse qui paie la qualité.
Fil rouge PSO de l’Epic : MGS-22 (PSO classique à coefficients) donnait mealpy devant en qualité à ex æquo en vitesse ; MGS-28 (BBPSO, zéro coefficient) resserre la démonstration — dépouiller la particule de w/c1/c2 ne change pas le verdict moteur, il l’isole : tout ce qui reste de l’écart est noyau.
Exercice 1 : budget ×4 — l’écart se referme-t-il ou s’installe-t-il ?
Le croisement s’est joué à budget 8 000 évaluations. Refaites-le à budget ×4 (population 50, 640 générations/epochs, 4 graines, les trois bras) et comparez : l’écart de médiane se referme, se stabilise, ou s’agrandit ? Relevez aussi les checkpoints 25/50/75/100 % — la forme de l’écart à budget long est-elle la même qu’à court ?
Indice : côté MGS, Mgs28Host.RunBbpso(sd, 50, 640) renvoie déjà les checkpoints ; côté mealpy, bench_mealpy_bbpso(seeds, 50, 640, variant=...) fait de même.
// EXERCICE 1 : budget x4 (pop 50, 640 générations/epochs), 4 graines, les trois bras.// Décommentez et exécutez :// foreach (var sd in new[] {0, 1, 7, 42})// {// var r = Mgs28Host.RunBbpso(sd, 50, 640);// Console.WriteLine($"MGS BBPSO x4 (graine {sd}) : {r.Item1} conflits, {r.Item2} évaluations, " +// $"{r.Item3:F0} ms, cp = {string.Join("/", r.Item5)}.");// }// using (Py.GIL())// {// S28.Set("__seeds_json__", System.Text.Json.JsonSerializer.Serialize(new[] {0, 1, 7, 42}.ToList()));// S28.Exec(@"__bx4_mgs__ = bench_mealpy_bbpso(__seeds_json__, 50, 640, variant='mgs')// __bx4_ken__ = bench_mealpy_bbpso(__seeds_json__, 50, 640, variant='kennedy')");// Console.WriteLine(S28.Get<string>("__bx4_mgs__"));// Console.WriteLine(S28.Get<string>("__bx4_ken__"));// }Console.WriteLine("Exercice a completer (decommentez le bloc ci-dessus).");
Exercice a completer (decommentez le bloc ci-dessus).
Exercice 2 : lever le gel — un plancher d’écart-type change-t-il la donne ?
Le bare-bones gèle au meilleur global : quand ancre == gbest, l’écart-type s’annule et la particule-élite ne peut plus bouger. Ajoutez au subclass un plancher d’écart-type std_floor (std = np.maximum(np.abs(g - anchor), std_floor)) et mesurez les trois bras à std_floor ∈ {0 (statu quo), 0,25, 0,5} : le gel est-il une propriété qui porte la convergence, ou un frein que l’on lève gratuitement ?
Indice : la modification tient dans evolve — pensez à documenter le changement de sémantique dans la docstring du subclass, comme le fait le portage MGS pour son ancre courante.
// EXERCICE 2 : plancher d'ecart-type dans le subclass mealpy (std_floor).// Décommentez et exécutez (esquisse — le subclass OriginalBBPSO est déjà dans le scope S28) :// using (Py.GIL())// {// S28.Exec(@"class BBPSOFloor(OriginalBBPSO):// def __init__(self, epoch=10000, pop_size=100, variant='kennedy', std_floor=0.25, **kwargs):// super().__init__(epoch, pop_size, variant, **kwargs)// self.std_floor = std_floor// def evolve(self, epoch):// if epoch - 1 in (self.epoch // 4, self.epoch // 2, 3 * self.epoch // 4):// self.checkpoints[epoch - 1] = float(self.g_best.target.fitness)// for idx in range(self.pop_size):// anchor = self.pop[idx].local_solution if self.variant == 'kennedy' else self.pop[idx].solution// g = self.g_best.solution// mean = 0.5 * (anchor + g)// std = np.maximum(np.abs(g - anchor), self.std_floor)// pos_new = self.correct_solution(self.generator.normal(mean, std))// target = self.get_target(pos_new)// if self.variant == 'kennedy' or self.compare_target(target, self.pop[idx].target, self.problem.minmax):// self.pop[idx].update(solution=pos_new.copy(), target=target.copy())// if self.compare_target(target, self.pop[idx].local_target, self.problem.minmax):// self.pop[idx].update(local_solution=pos_new.copy(), local_target=target.copy())// def run_floor(seed, floor):// PY_EVALS[0] = 0// prob = SudokuProblem(bounds=FloatVar(lb=(1.0,) * len(empties), ub=(10.0,) * len(empties), name='genes'), minmax='min')// model = BBPSOFloor(epoch=160, pop_size=50, variant='kennedy', std_floor=floor)// g_best = model.solve(prob, seed=seed)// return cost(decode(g_best.solution))// __floor_demo__ = str({f: [run_floor(sd, f) for sd in (0, 1, 7, 42)] for f in (0.0, 0.25, 0.5)})");// Console.WriteLine(S28.Get<string>("__floor_demo__"));// }Console.WriteLine("Exercice a completer (decommentez le bloc ci-dessus).");
Exercice a completer (decommentez le bloc ci-dessus).
Exercice 3 : profiler la fitness — où va la milliseconde ?
Le rapport ms/éval mêle le tirage gaussien, la correction de bornes et le coût Sudoku. Comme dans les paires précédentes, séparez le décodage du comptage de conflits sur les 500 vecteurs témoins, des deux côtés du pont : la fitness C# reste-t-elle plusieurs fois plus rapide, et la part decode/coût change-t-elle de côté ?
// EXERCICE 3 : profil decode vs cost, 500 vecteurs, deux côtés.// Décommentez et exécutez (adapté de MGS-22/23 exercice 3) :// var decSw = Stopwatch.StartNew();// foreach (var v in benchVecs) DecodeR1_28(v);// decSw.Stop();// Console.WriteLine($"C# decode seul : {decSw.Elapsed.TotalMilliseconds / K28:F3} ms/vec " +// $"(reste = coût : {(csMs - decSw.Elapsed.TotalMilliseconds) / K28:F3} ms/vec)");// using (Py.GIL())// {// S28.Set("__vecs_json__", System.Text.Json.JsonSerializer.Serialize(benchVecs.Select(v => v.ToList()).ToList()));// S28.Exec(@"import time// _vecs = _json.loads(__vecs_json__)// _t0 = time.perf_counter()// _grids = [decode(v) for v in _vecs]// _t1 = time.perf_counter()// _csts = [cost(g) for g in _grids]// _t2 = time.perf_counter()// __py_profil__ = f'Python decode seul {(_t1 - _t0) * 1000.0 / len(_vecs):.3f} ms/vec, coût {(_t2 - _t1) * 1000.0 / len(_vecs):.3f} ms/vec'");// Console.WriteLine(S28.Get<string>("__py_profil__"));// }Console.WriteLine("Exercice a completer (decommentez le bloc ci-dessus).");
Exercice a completer (decommentez le bloc ci-dessus).
Diagnostic dérive
Cause (a) — environnement / résolution de chemin (#13959). Les trois directives #r de la cellule socle pointaient ../MetaGeneticSharp/..., une profondeur de répertoire calculée quand ce notebook vivait à la racine de Part4-Metaheuristics/. L’isolation des face-à-face MGS ↔︎ mealpy dans la sous-série MGS-vs-mealpy/ (#13959, déplacement R100) a ajouté un niveau d’arborescence : ../MetaGeneticSharp ne résolvait plus rien, la cellule socle ne compilait plus, et les sorties — transportées telles quelles par le déplacement, jamais re-confirmées par un run — se sont figées.
Correctif. Les trois directives passent à ../../MetaGeneticSharp/... (mêmes DLLs net9.0 Debug, même gitlink 78ce550) et le notebook est ré-exécuté de bout en bout au kernel .net-csharp réel, pont PythonNet vers mealpy 3.0.2. Aucune sortie n’est éditée à la main.
Contradictions prose ↔︎ sorties, pré-existantes, corrigées dans le même passage. Elles ont survécu au gel des sorties précisément parce qu’aucun run ne les confrontait plus :
la lecture du socle annonçait une représentation R1 à 51 gènes alors que la sortie du socle en compte 36 (36 cellules vides, 45 indices fixes, 36 gènes) ;
la lecture du croisement annonçait les graines 1/7/42 en croisement alors que le classifieur imprimé les donne mixtes (et la graine 0 persistante), surestimait la position de MGS au premier quart — MGS y est déjà derrière sur la graine 0 (33 conflits contre 32) — et recopiait des temps muraux absolus ;
l’introduction et la lecture de la cellule formules affirmaient que les sept PSO mealpy portent les constantes w/c1/c2, ce que le catalogue imprimé contredit pour P_PSO (aucun paramètre listé). Vérifié in-source : P_PSO porte des coefficients sin/cos dynamiques, pas les constantes de Clerc. La conclusion (« aucun n’est bare-bones ») tient ; la raison invoquée est corrigée.
Verdict C.4 : CAUSE_FIXED. Protocole inchangé — graines {0, 1, 7, 42}, population 50, 160 générations/epochs, 8 000 évaluations (MGS) / 8 050 (mealpy), représentation R1, fonction de coût en conflits, checkpoints 25/50/75/100 %. Sur sept exécutions complètes — trois par le moteur d’exécution (dotnet_executor.py), quatre par le wrapper canonique de la famille (exec_dotnet_persist.py), la dernière fournissant les sorties committées — les grandeurs déterministes — conflits finaux par graine, checkpoints, budget d’évaluations, drapeau 12/12 bras-graines stables — sont exactes et identiques entre elles et à celles déjà committées : la non-régression est mesurée, pas supposée. Seuls les temps muraux varient d’un passage kernel à l’autre ; ils sont conservés en rapport, jamais en valeur absolue.
Verdict de performance : NO IMPROVEMENT. Sur la taxonomie fermée IMPROVES / NO IMPROVEMENT / TRADE-OFF / INCONCLUSIVE, ce grain vaut NO IMPROVEMENT : l’axe performance est une invariance mesurée contre le moteur mis à jour — protocole, graines, budget d’évaluations et fonction de coût sont identiques d’un bout à l’autre, mais les sorties committées sortent d’un moteur reconstruit (net9.0 Debug) et non de celui qui avait produit les sorties gelées : l’égalité est constatée run après run, jamais déduite du seul protocole. Seul le chemin de résolution des directives #r est corrigé. Le livrable n’est donc pas un gain de performance mais l’exécutabilité restaurée — cellule socle de nouveau compilable, notebook entier relançable au kernel .net-csharp réel — assortie d’une non-régression mesurée. Les deux verdicts coexistent sans se contredire : CAUSE_FIXED qualifie la cause de la dérive (fixée), NO IMPROVEMENT qualifie l’axe performance (inchangé).