Jusqu’ici (MGS-1 à MGS-4) nous avons vu le moteur MetaGeneticAlgorithm, la grammaire fluente de primitives (Match, IfElse, Container, Scoped…), et deux composés structurants (Eukaryote, Islands). Ce notebook répond à la question qui reste : d’où viennent les algorithmes « publiés » — Whale Optimization Algorithm (WOA), Equilibrium Optimizer (EO), Forensic-Based Investigation (FBI) — et comment les utiliser sans retomber dans le piège de la boîte noire.
L’argument de MetaGeneticSharp (cf. MGS-6 Benchmarks) est que reconstruire ces algorithmes à partir de primitives composables démontre leur mécanisme au lieu de le cacher derrière une métaphore animale (critique de Sorensen, 2015 : les métaheuristiques bio-inspirées seraient des « metaphors exposed »). Ce notebook montre, pour chaque métaheuristique composée du catalogue, le même parcours en trois temps :
La description — la métaphore et la mécanique, dans l’esprit des fiches de mealpy (la bibliothèque Python qui sert de repère, et dont le fork s’inspire explicitement).
La reconstruction depuis les primitives — le code réel de la lib qui assemble l’algorithme à partir de briques (IfElse, Match, GenerationMetaHeuristic, croisements géométriques…). Ce n’est pas une paraphrase : c’est le corps de la méthode BuildMainHeuristic() de la classe, lu transparentement.
Le raccourci factory — le one-liner MetaHeuristicsService.CreateMetaHeuristicByName("...") qui produit exactement cette reconstruction, et la preuve (le type racine renvoyé) que raccourci et reconstruction sont identiques.
Rappel C.2 : kernel .net-csharp. Les DLLs MetaGeneticSharp + GeneticSharp sont chargées depuis le build du fork, compilé en net9.0 pour correspondre au kernel (.NET 8 via dotnet-interactive). Voir la cellule de câblage ci-dessous.
// Câblage : chargement des DLLs MetaGeneticSharp + GeneticSharp (build net9.0 du fork).// Prérequis de build (depuis la racine du fork) :// dotnet build src/MetaGeneticSharp.Domain/MetaGeneticSharp.Domain.csproj -c Debug// (net9.0 pour matcher le target net9.0 du csproj ; les DLLs core + dépendances System.Drawing// sont co-localisées dans Domain/bin/Debug/net9.0/.)#r "../MetaGeneticSharp/src/MetaGeneticSharp.Domain/bin/Debug/net9.0/GeneticSharp.Infrastructure.Framework.dll"#r "../MetaGeneticSharp/src/MetaGeneticSharp.Domain/bin/Debug/net9.0/GeneticSharp.Domain.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"#r "../MetaGeneticSharp/src/MetaGeneticSharp.Extensions/bin/Debug/net9.0/MetaGeneticSharp.Extensions.dll"using MetaGeneticSharp;using GeneticSharp;using System;using System.Collections.Generic;using System.Linq;Console.WriteLine("MetaGeneticSharp + GeneticSharp chargés (build net9.0 du fork).");Console.WriteLine(" MetaHeuristicsService : "+typeof(MetaHeuristicsService).Name);Console.WriteLine(" WhaleOptimisationAlgorithm : "+typeof(WhaleOptimisationAlgorithm).Name);Console.WriteLine(" EquilibriumOptimizer : "+typeof(EquilibriumOptimizer).Name);Console.WriteLine(" ForensicBasedInvestigation : "+typeof(ForensicBasedInvestigation).Name);
The below script needs to be able to find the current output cell; this is an easy method to get it.
Le point d’entrée de la factory est une simple énumération : KnownCompoundMetaheuristics. Chaque valeur nomme une métaheuristique composée que MetaHeuristicsService sait construire. C’est l’équivalent du registre de mealpy — une liste close d’algorithmes prêts à l’emploi.
Archipel mêlant WOA + EO + GA (composition de composés).
Listons-le dynamiquement pour confirmer ce que la lib expose réellement (plutôt que de faire confiance à une liste écrite à la main).
// Le catalogue vu par la lib (Reflection sur l'enum KnownCompoundMetaheuristics).var names = MetaHeuristicsService.GetMetaHeuristicNames();Console.WriteLine("Catalogue KnownCompoundMetaheuristics ("+ names.Count+" entrées) :");foreach(var n in names) Console.WriteLine(" - "+ n);// La factory : un seul appel construit n'importe quel composé par son nom.// Signature : CreateMetaHeuristicByName(name, maxGenerations=1000, populationSize=100,// geometricConverter=null, noMutation=true)IMetaHeuristic demo = MetaHeuristicsService.CreateMetaHeuristicByName("Default");Console.WriteLine();Console.WriteLine("CreateMetaHeuristicByName(Default) -> "+ demo.GetType().Name);
Le terrain commun : chromosome continu et banc d’essai
Les composés géométriques (WOA, EO, FBI) opèrent sur des gènes continus lus comme des double via un GeometricConverter. Le chromosome DoubleArrayChromosome ci-dessous (réplique minimale de l’assistant de test du fork) stocke des double nus : la conversion gene <-> double est donc l’identité. C’est la représentation commune sur laquelle nous lancerons chaque composé.
Les fonctions test (Sphere, Rastrigin, Rosenbrock) sont définies inline plus bas pour garder ce notebook autonome (trois surfaces canoniques d’optimisation continue). Rappel : GeneticSharp maximise, les fonctions test minimisent -> la fitness est l’objectif négativé (plus proche de 0 = meilleur). Le banc d’essai RunBenchmark câble un composé dans le moteur MetaGeneticAlgorithm (sélection élitiste, croisement uniforme, mutation uniforme) et renvoie la meilleure fitness. BuildViaFactory(name) est le raccourci paramétré par nom que nous réutiliserons pour chaque composé.
// DoubleArrayChromosome : chromosome minimal stockant des gènes double nus.// (Inspiré de l'assistant de test MetaGeneticSharp.Domain.Tests.Geometric.DoubleArrayChromosome,// étendu avec des bornes par gène pour que CreateNew() randomise la population initiale.)publicclass DoubleArrayChromosome : ChromosomeBase{privatereadonlydouble _min;privatereadonlydouble _max;publicDoubleArrayChromosome(double[] values,double min,double max):base(values.Length){ _min = min; _max = max;for(int i =0; i < values.Length; i++)ReplaceGene(i,newGene(values[i]));}publicoverride IChromosome CreateNew(){var rand = RandomizationProvider.Current;int n = Length;var vals =newdouble[n];for(int i =0; i < n; i++) vals[i]= rand.GetDouble(_min, _max);returnnewDoubleArrayChromosome(vals, _min, _max);}publicoverride Gene GenerateGene(int geneIndex)=>newGene(RandomizationProvider.Current.GetDouble(_min, _max));publicdouble[]GetDoubleValues()=>GetGenes().Select(g =>(double)g.Value).ToArray();}// --- Fonctions test inline (objectif à minimiser -> fitness = -objectif). ---// Sphere : f(x) = Σ x_i² minimum global 0 en 0.// Rastrigin : f(x) = Σ (x_i² - 10 cos(2π x_i)) + 10·n minimum global 0 en 0.// Rosenbrock: f(x) = Σ [100(x_{i+1}-x_i²)² + (1-x_i)²] minimum global 0 en (1,...,1).publicclass SphereFitness : IFitness{publicdoubleEvaluate(IChromosome c){var x =((DoubleArrayChromosome)c).GetDoubleValues();double s =0;for(int i =0; i < x.Length; i++) s += x[i]* x[i];return-s;}}publicclass RastriginFitness : IFitness{publicdoubleEvaluate(IChromosome c){var x =((DoubleArrayChromosome)c).GetDoubleValues();double s =0;for(int i =0; i < x.Length; i++) s += x[i]* x[i]-10* Math.Cos(2* Math.PI* x[i]);return-(s +10* x.Length);}}publicclass RosenbrockFitness : IFitness{publicdoubleEvaluate(IChromosome c){var x =((DoubleArrayChromosome)c).GetDoubleValues();double s =0;for(int i =0; i < x.Length-1; i++){double a =100*(x[i +1]- x[i]* x[i]);double b =1- x[i]; s += a * a + b * b;}return-s;}}// Bornes usuelles par fonction test (intervalle de tirage de la population initiale).static(double min,double max)Bounds(Type t)=> t ==typeof(RosenbrockFitness)?(-2.048,2.048):(-5.12,5.12);constint Dim =5;constint PopSize =60;constint Generations =80;// Reproductibilite du banc : le moteur tire tout son alea de RandomizationProvider.Current,// un FastRandomRandomization NON seede par defaut. Sans cet appel, chaque execution produit// d'autres valeurs, et les cellules d'interpretation citent alors des nombres qu'aucun// lecteur ne peut reproduire.// (BasicRandomization.ResetSeed serait un no-op ici : le moteur ne lit pas son ThreadLocal// tant que Current reste FastRandom -- cf #12071, dont le perimetre n'avait pas couvert MGS-5.)FastRandomRandomization.ResetSeed(42);// Banc d'essai : lance une métaheuristique sur une fonction test, renvoie la meilleure fitness.doubleRunBenchmark(IFitness fitness, Type fitnessType, IMetaHeuristic mh,int dim = Dim){var(min, max)=Bounds(fitnessType);double mid =0.5*(min + max);var adam =newDoubleArrayChromosome(Enumerable.Repeat(mid, dim).ToArray(), min, max);var pop =newMetaPopulation(PopSize, PopSize, adam);var ga =newMetaGeneticAlgorithm( pop, fitness,newEliteSelection(),newUniformCrossover(0.5f),newUniformMutation(true), mh); ga.Termination=newGenerationNumberTermination(Generations); ga.Start();return ga.BestChromosome.Fitness.HasValue? ga.BestChromosome.Fitness.Value:double.NaN;}// Raccourci factory paramétré par nom (câble automatiquement un convertisseur identité pour les géométriques).IMetaHeuristic BuildViaFactory(string name)=> MetaHeuristicsService.CreateMetaHeuristicByName(name, maxGenerations: Generations, populationSize: PopSize);var demoFns =new(string Name, Type Type)[]{("Sphere",typeof(SphereFitness)),("Rastrigin",typeof(RastriginFitness)),("Rosenbrock",typeof(RosenbrockFitness)),};Console.WriteLine("Harness + fonctions test inline + factory BuildViaFactory(name) prêts.");
Harness + fonctions test inline + factory BuildViaFactory(name) prêts.
Le parcours en trois temps
Pour chaque composé géométrique (WOA, EO, FBI), la lib définit une classe qui hérite de GeometricMetaHeuristicBase. Cette classe implémente BuildMainHeuristic() : c’est la reconstruction depuis les primitives — la méthode qui assemble l’algorithme avec la grammaire fluente vue en MGS-2. Appeler .Build() (ou la factory) exécute cette reconstruction et renvoie l’arbre de primitives prêt à l’emploi.
Nous allons donc, pour chaque composé :
lire sa fiche (métaphore + équations),
lire sa reconstructionBuildMainHeuristic() — c’est l’implémentation, montrée transparentement,
appeler la factory et prouver qu’elle renvoie le même type racine que la reconstruction (donc : même arbre).
Commençons par le plus célèbre : WOA.
1. Whale Optimization Algorithm (WOA)
Description
WOA (Mirjalili & Lewis, 2016) mime la chasse en bubble-net des baleines à bosse. La mécanique se décompose en trois comportements sélectionnés stochastiquement à chaque génération :
Encerclement de la proie : chaque baleine se rapproche de la meilleure solution connue (ou d’une solution aléatoire en phase d’exploration) par un déplacement linéaire D = |C·x_best - x| puis x <- x_best - A·D.
Spirale bubble-net (exploitation) : la baleine décrit une hélice autour de la meilleure proie : x <- D'·exp(b·l)·cos(2π·l) + x_best, avec b l’amplitude hélicoïdale et l un angle aléatoire.
Recherche aléatoire : quand |A| > 1, on se déplace vers un individu aléatoire (exploration globale).
Deux paramètres clef : a décroit linéairement de 2 à 0 (il pilote la bascule exploration -> exploitation via A = 2a·r - a), et b fixe la spirale. C’est exactement la fiche mealpy (mealpy.evolutionary_based.WOA), dont le fork reproduit les équations.
Reconstruction depuis les primitives
La classe WhaleOptimisationAlgorithm.BuildMainHeuristic() construit l’algorithme ainsi (code réel de la lib, annoté) :
Chaque ligne est une primitive : IfElseMetaHeuristic (dispatch booléen), MatchMetaHeuristic (choix des parents par MatchingKind.Current/Random/Best), CrossoverMetaHeuristic + GeometricCrossover (l’opérateur de déplacement continu), WithParam (paramètres dépendants du contexte : génération, individu). Aucune boîte noire — tout est lisible et modifiable. C’est la réponse de MetaGeneticSharp à Sorensen : on ne nie pas que WOA est une composition d’opérateurs standards, on l’expose comme telle.
// --- WOA : raccourci factory + preuve d'équivalence avec la reconstruction ---IMetaHeuristic woaViaFactory =BuildViaFactory("WhaleOptimisation");Type woaRootViaFactory = MetaHeuristicsService.GetMetaHeuristicTypeByName("WhaleOptimisation");// La reconstruction lit le type racine produit par BuildMainHeuristic() : IfElseMetaHeuristic.Console.WriteLine("WOA via factory : "+ woaViaFactory.GetType().Name);Console.WriteLine("Type racine (GetMetaHeuristicTypeByName) : "+ woaRootViaFactory.Name);Console.WriteLine(" == typeof(IfElseMetaHeuristic) ? "+(woaRootViaFactory ==typeof(IfElseMetaHeuristic)));Console.WriteLine(" -> la factory renvoie bien l'arbre IfElse de la reconstruction ci-dessus.");Console.WriteLine();// Exécution : WOA sur Sphere (unimodale lisse, terrain favorable au bubble-net).double woaSphere =RunBenchmark(newSphereFitness(),typeof(SphereFitness), woaViaFactory);Console.WriteLine("WOA sur Sphere (5D, "+ Generations +" gen) -> fitness = "+ woaSphere.ToString("G5")+" (proche de 0 = bon)");
WOA via factory : IfElseMetaHeuristic
Type racine (GetMetaHeuristicTypeByName) : IfElseMetaHeuristic
== typeof(IfElseMetaHeuristic) ? True
-> la factory renvoie bien l'arbre IfElse de la reconstruction ci-dessus.
WOA sur Sphere (5D, 80 gen) -> fitness = -2,9974E-10 (proche de 0 = bon)
2. Equilibrium Optimizer (EO)
Description
EO (Faramarzi et al., 2019) est inspiré du bilan de masse en volume de contrôle : on estime l’état d’équilibre d’un système dynamique. Le fork le porte directement depuis mealpy.physics_based.EO.
La mécanique : on garde un pool d’équilibre des 4 meilleures solutions + leur centroïde. Chaque particule se déplace vers ce point d’équilibre pondéré, avec un terme de décroissance exponentielle du premier ordre qui ralentit le déplacement au fil des générations (taux f = a1·sign(r-0.5)·(exp(-lambda·t) - 1)). Un generation control probabilitygcp active ou non la mise à jour, ce qui mélange exploitation (vers l’équilibre) et diversité.
Reconstruction depuis les primitives
EquilibriumOptimizer.BuildMainHeuristic() :
var eo =newMatchMetaHeuristic()// composé = un Match principal.WithMatches(Current, Custom)// parent + cible "custom" (le centroïde).WithCustomMatchStep(Best, AdditionalPicks=3)// les 4 meilleures solutions (1+3).WithChildMetaHeuristic(centroidHeuristic)// GeometricCrossover 4-parents = moyenne (centroïde).WithCrossoverMetaHeuristic(generationHeuristic)// opérateur EO : éq. bilan de masse + decay exp.WithParam("t",(h,ctx)=>GetTime(...))// temps sans dimension Eq. 9.WithParam("lambda",(h,ctx,n)=> rnd x n genes)// par gène.WithParam("f",(h,ctx,t,l,r)=> ExponentialRate)// décroissance Eq. 11.WithParam("gcp",(h,ctx,n,r1,r2)=> GetGCP);// generation control Eq. 15
L’idée géométrique est limpide : MatchMetaHeuristic choisit les parents (les 4 meilleures + le centroïde calculé par un croisement 4-parents), puis le GeometricCrossover applique l’équation du bilan de masse. Encore une fois, l’algorithme « physique » se révèle être une composition d’opérateurs standards sur un croisement géométrique.
// --- EO : raccourci factory + exécution ---IMetaHeuristic eoViaFactory =BuildViaFactory("EquilibriumOptimizer");Type eoRoot = MetaHeuristicsService.GetMetaHeuristicTypeByName("EquilibriumOptimizer");Console.WriteLine("EO via factory : "+ eoViaFactory.GetType().Name);Console.WriteLine("Type racine EO : "+ eoRoot.Name+" (== MatchMetaHeuristic ? "+(eoRoot ==typeof(MatchMetaHeuristic))+")");double eoSphere =RunBenchmark(newSphereFitness(),typeof(SphereFitness), eoViaFactory);Console.WriteLine("EO sur Sphere -> fitness = "+ eoSphere.ToString("G5"));Console.WriteLine();
EO via factory : MatchMetaHeuristic
Type racine EO : MatchMetaHeuristic (== MatchMetaHeuristic ? True)
EO sur Sphere -> fitness = -1,7122E-27
3. Forensic-Based Investigation (FBI)
Description
FBI (Chou & Nguyen, 2020) mime le processus d’enquête policière : localisation du suspect, enquête, localisation de la poursuite. Le fork le porte depuis mealpy.human_based.FBIO. C’est un composé multi-phase : 4 étapes (A1, A2, B1, B2) cyclées à chaque génération.
A1 (localisation) : on perturbe un gène aléatoire d’un individu avec un bruit gaussien combiné à deux témoins aléatoires.
A2 (enquête) : selon une probabilité liée au rang de fitness de l’individu, on croise avec le meilleur + 3 témoins (5-parent), sinon on ne fait rien (NoOp).
B1 : combinaison linéaire entre courant et meilleur.
B2 : combinaison courant + aléatoire + meilleur, avec une branche dépendant d’un drapeau randomBetter.
Reconstruction depuis les primitives
ForensicBasedInvestigation.BuildMainHeuristic() — c’est l’exemple le plus parlant de « composition de phases » :
La primitive clef ici est GenerationMetaHeuristic : elle cycle ses sous-heuristiques par numéro de génération (cf. MGS-2). Un algorithme « humain » à 4 phases devient l’enchaînement de 4 MatchMetaHeuristic/IfElseMetaHeuristic standards. Changer une phase (par ex. remplacer le bruit gaussien de A1) est une édition locale — impossible dans une mealpy.FBIO() monolithique.
FBI via factory : GenerationMetaHeuristic
Type racine FBI : GenerationMetaHeuristic (== GenerationMetaHeuristic ? True)
FBI sur Sphere -> fitness = -1,7838E-26
4. Islands : la composition de composés
Description
Le modèle insulaire (déjà détaillé en MGS-4) partitionne la population en sous-populations (« îles ») évoluées indépendamment, avec une migration périodique qui échange des individus. Le catalogue en expose deux variantes riches :
Islands5Default : 5 îles homogènes, toutes en DefaultMetaHeuristic, migration « ring » à 2 % (MediumMigrationRate). Une variante NoMigration désactive les échanges (îles isolées).
Islands5BestMixture : 5 îles hétérogènes — 2 îles WOA, 2 îles EO, 1 île GA — migration plus fine (0.5 %). C’est le cas d’école de la composition de composés : on mélange des algorithmes complets dans un même archipel.
Reconstruction
Aucune magie : la factory instancie un IslandCompoundMetaheuristic (lui-même un IslandMetaHeuristic) avec les composés voulus :
On voit ici tout l’intérêt de l’approche primitives-based : un hybride « îles WOA+EO » qui n’existe dans aucune bibliothèque monolithique s’énonce en quelques lignes, parce que WOA et EO sont eux-mêmes des composés réutilisables. C’est exactement le prolongement annoncé en conclusion de MGS-6.
Islands5Default via factory : IslandMetaHeuristic
Type racine Islands : IslandMetaHeuristic (== IslandMetaHeuristic ? True)
Islands5Default sur Sphere -> fitness = -0,011947
Islands5BestMixture sur Sphere -> fitness = -1,38E-17
Vue d’ensemble : les quatre familles sur le banc
Pour comparer les composés géométriques entre eux (et avec les îles), nous lançons chacun sur les mêmes fonctions test. Rappel : fitness = objectif négativé, donc la valeur la moins négative (la plus proche de 0) est la meilleure. L’objectif n’est pas de désigner un « gagnant » (No Free Lunch : il n’y en a pas), mais de vérifier que chaque composé reconstruit depuis des primitives est un solveur fonctionnel et compétitif — la preuve que la reconstruction préserve le pouvoir d’optimisation.
// Grille de comparaison : Default + 4 familles de composés x 3 fonctions test.var configs =new(string Name, Func<IMetaHeuristic> Build)[]{("Default (GA)",()=>BuildViaFactory("Default")),("WOA",()=>BuildViaFactory("WhaleOptimisation")),("EO",()=>BuildViaFactory("EquilibriumOptimizer")),("FBI",()=>BuildViaFactory("ForensicBasedInvestigation")),("Islands5BestMix",()=>BuildViaFactory("Islands5BestMixture")),};string header ="Fonction "+string.Join("", configs.Select(c => c.Name.PadLeft(16)));Console.WriteLine(header);Console.WriteLine(newstring('-',12+16* configs.Length));foreach(var(fnName, fnType)in demoFns){var fitness =(IFitness)Activator.CreateInstance(fnType);string row = fnName.PadRight(12);foreach(var(_, build)in configs){double f =RunBenchmark(fitness, fnType,build()); row += f.ToString("G5").PadLeft(16);} Console.WriteLine(row);}Console.WriteLine();Console.WriteLine("Chaque colonne est le même type d'objet (IMetaHeuristic) construit en un appel de factory.");
Fonction Default (GA) WOA EO FBI Islands5BestMix
--------------------------------------------------------------------------------------------
Sphere -0,0030369 -3,458E-10 -2,2683E-25 -5,2763E-25 -1,4157E-20
Rastrigin -0,44365 -9,5195E-08 -2,9849 -0 -0,01979
Rosenbrock -7,6115 -3,8221 -3,3167 -3,7862 -3,7513
Chaque colonne est le même type d'objet (IMetaHeuristic) construit en un appel de factory.
Les nouveaux arrivants, sur le banc dé-biaisé
La grille ci-dessus compare les quatre familles historiques du catalogue. Mais le catalogue (KnownCompoundMetaheuristics) contient aussi trois composés plus récents que cette grille ne montre pas : DifferentialEvolution (Storn & Price, 1997), BareBonesParticleSwarm (Kennedy, 2003) et SimulatedAnnealing (Kirkpatrick et al., 1983). La factory les construit exactement comme les autres – un appel par nom.
Pour les démontrer, on ne se contente pas du banc centré : on les confronte au banc dé-biaisé (cf. MGS-6 pour l’analyse complète du protocole). Rappel du problème : le harnais seede le chromosome adam au milieu des bornes – sur l’optimum de Sphere et Rastrigin (fonctions à optimum en zéro, bornes symétriques). Le protocole CEC déplace l’optimum hors du centre (f_debias(x) = f(Q·x − offset), Q orthogonale seedée, offset par dimension seedé) via les décorateurs du fork :
var offset = ShiftVectors.Seeded(Dim,1.5, seed);// ~30 % de la demi-étendue [-5.12, 5.12]var Q = RotationMatrices.Seeded(Dim, seed);var cec =newRotatedFitness(newShiftedFitness(inner, offset), Q);
L’expérience est appariée : chaque configuration (Default en référence + les trois nouveaux) tourne sur la version centrée ET la version dé-biaisée de Sphere et Rastrigin, 3 runs indépendants par cellule (moyenne – le GA est stochastique). Question : les nouveaux arrivants résistent-ils au retrait du cadeau du centre ?
// Nouveaux composés (DE, BareBonesPSO, PSO canonique à vélocité, SimulatedAnnealing) via la factory,// confrontes au banc de-biase (shifted+rotated CEC). Comparaison appariee centered-vs-debiased,// 3 runs par cellule (moyenne), Default en reference.int ndSeed =42;int ndRuns =3;var newConfigs =new(string Name, Func<IMetaHeuristic> Build)[]{("Default (GA)",()=>BuildViaFactory("Default")),("DifferentialEvol",()=>BuildViaFactory("DifferentialEvolution")),("BareBonesPSO",()=>BuildViaFactory("BareBonesParticleSwarm")),// PSO canonique a velocite (#11977 axe 3) : recurrence Shi & Eberhart// w*v + c1*r1*(pbest-x) + c2*r2*(gbest-x), constantes Clerc. Ce n'est PAS// le bare-bones de Kennedy ci-dessus : les deux coexistent au catalogue// parce qu'elles incarnent deux lectures de "essaim" -- memorie par// particule (velocite + pbest) vs tirage gaussien sans etat.("CanonicalPSO",()=>BuildViaFactory("ParticleSwarmOptimization")),("SimulatedAnneal",()=>BuildViaFactory("SimulatedAnnealing")),};var zeroOptFns =new(string Name, Type Type)[]{("Sphere",typeof(SphereFitness)),("Rastrigin",typeof(RastriginFitness)),};var Qnd = RotationMatrices.Seeded(Dim, ndSeed);Console.WriteLine($"seed={ndSeed} Q orthogonal: {RotationMatrices.IsOrthogonal(Qnd)} runs per cell={ndRuns}");Console.WriteLine("(fitness = objectif negativé, plus proche de 0 = meilleur; moyenne sur les runs)\n");doubleMeanRuns(IFitness f, Type t, Func<IMetaHeuristic> build)=> Enumerable.Range(0, ndRuns).Select(_ =>RunBenchmark(f, t,build())).Average();foreach(var(fnName, fnType)in zeroOptFns){var inner =(IFitness)Activator.CreateInstance(fnType);var offset = ShiftVectors.Seeded(Dim,1.5, ndSeed);var cec =newRotatedFitness(newShiftedFitness(inner, offset), Qnd); Console.WriteLine($"{fnName,-10} |offset|_inf={offset.Select(Math.Abs).Max(),7:G4} (optimum a Q^T*offset, hors centre)");string rowC =" centered :", rowD =" shifted :";foreach(var(cfgName, build)in newConfigs){double c =MeanRuns(inner, fnType, build);double d =MeanRuns(cec, fnType, build); rowC += $" {cfgName,-18} {c,12:G5}"; rowD += $" {cfgName,-18} {d,12:G5}";} Console.WriteLine(rowC); Console.WriteLine(rowD);}Console.WriteLine("\n--- Done: 5 configs x 2 fonctions a optimum-centre, centered vs shifted+rotated, 3 runs/cellule ---");
Lecture : les nouveaux arrivants passent le test – mais le peloton se tasse
Rappel : fitness = objectif négativé, plus proche de 0 = meilleur ; chaque cellule = moyenne sur 3 runs ; les deux lignes par fonction sont appariées (même harnais, seul l’optimum bouge : centre vs Q^T·offset, Q orthogonal: True en sortie). Le banc est seedé (FastRandomRandomization.ResetSeed(42), cellule du harnais) : les valeurs ci-dessous se reproduisent à l’identique d’une exécution à l’autre.
Sphere : aucun des nouveaux n’avait besoin du cadeau du centre. DE et BBPSO convergent à la précision machine des deux côtés – DE à -2,5559E-11 (centré) et -9,1368E-11 (dé-biaisé), BBPSO à -1,3298E-20 et -1,8979E-21. À ce niveau de précision, l’écart centered/shifted n’est pas mesurable : ces deux composés ne doivent rien à la position de l’optimum sur une unimodale. C’est le comportement qu’on attend d’un vrai optimiseur – et le contraste avec WOA dans MGS-6, dont la marge s’évaporait de neuf ordres de grandeur sur la même fonction.
Rastrigin : le GA nu tient le leadership des deux côtés. Sur cette exécution, Default devance DE partout – -1,366 centré et -10,462 dé-biaisé, contre -2,3218 / -6,4986 pour le différentiel. La marge se resserre en passant au banc dé-biaisé (×1,70 au centre, ×1,61 une fois l’optimum déplacé) mais l’ordre ne s’inverse pas. C’est l’inverse de ce que la réputation du différentiel sur les paysages multimodaux laisserait attendre ; une lecture possible est que la recombinaison d’un GA élitiste explore plusieurs bassins simultanément là où une population différentielle resserrée autour d’un bassin local n’a plus de mécanisme de redémarrage – hypothèse que ce banc (3 runs, une graine) ne teste pas.
La queue ne se redistribue pas. SA devance BBPSO dans les deux régimes : -3,8682 contre -12,281 au centre (×3,17), -5,4753 contre -14,784 dé-biaisé (×2,70). Le recuit simulé, qui accepte des pas dégradants calibrés sur la géométrie du paysage, conserve donc son avantage quand la rotation désaxe les vallées ; le bare-bones, qui resample autour des meilleures solutions sans mémoire de vitesse, reste en retrait sur une surface aux vallées étroites. La paire réellement serrée est ailleurs : dé-biaisé, Default (-10,462) et le PSO canonique (-11,703) ne sont séparés que de ×1,12 – un écart que 3 runs ne tranchent pas.
Verdict honnête : les nouveaux composés sortent du banc dé-biaisé sans faux champion – BBPSO montre une robustesse de précision machine sur Sphere, le PSO canonique domine le bare-bones sur Rastrigin, et aucun classement ne reposait sur le seeding au centre. La signature de biais de Kudela, qui renversait 3 des 4 fonctions dans MGS-6 pour les familles historiques, ne renverse ici ni le vainqueur de Sphere ni celui de Rastrigin ; elle resserre les marges et laisse la queue inchangée. Réserve de méthode identique à MGS-6 : 3 runs départagent les écarts nets, pas les écarts serrés (Default vs PSO canonique dé-biaisé : ×1,12 sur une moyenne de 3).
Et toujours le même argument d’architecture : cette section n’a aucun nouvel algorithme à écrire – trois noms de factory en plus, deux décorateurs composés, zéro modification du moteur.
Le PSO canonique à vélocité entre au banc (#11977 axe 3). La ligne CanonicalPSO est la récurrence de Shi & Eberhart — inertie w, termes cognitif c1 et social c2, mémoire de vitesse et de pbest par particule — livrée dans la lib par #12030. La comparer au BareBonesPSO de la même table n’est pas une coquetterie de catalogue : le bare-bones de Kennedy supprime précisément la récurrence vitesse/position au profit d’un tirage gaussien centré sur (pbest+gbest)/2. Les deux lignes mesurent donc ce que cette mémoire apporte (ou coûte) sur le même banc dé-biaisé — la lecture se fait sur les chiffres ci-dessus, pas sur la métaphore. Ce que cette exécution donne à lire : sur Sphere, les deux PSO convergent vers l’optimum (BBPSO -1,3E-20, PSO canonique -2,5E-16 : quatre ordres de grandeur les séparent, mais les deux sont à la précision machine) ; sur Rastrigin centrée, la surface multimodale les sépare radicalement — le PSO canonique atteint -7,2E-07, c’est-à-dire l’optimum à la précision machine, contre -12,281 pour le bare-bones, et il reste devant sous rotation (-11,703 contre -14,784). La mémoire de vitesse et de pbest paie donc là où la géométrie du paysage, pas seulement la proximité de l’optimum, décide. Pour la confrontation lib-vs-lib à moteur égal (MGS vs mealpy, même grille de sudoku), voir MGS-22.
Deuxième vague : les quatre briques de la dernière fusion
Le fork MetaGeneticSharp vient d’absorber quatre nouvelles briques (PRs fork #45 à #48). La répartition est le point pédagogique : une seule entre au catalogue KnownCompoundMetaheuristics et se construit par la factory ; les trois autres se construisent directement – et c’est la thèse compositionnelle du cours en acte.
Brique
Nature
Référence
ScatterSearch
composé du catalogue (nouvelle entrée enum + factory)
Glover, A Template for Scatter Search and Path Relinking, 1998 ; Laguna & Marti, 2003
MemeticAlgorithm
wrapper : emballe n’importe quel ICompoundMetaheuristic et y greffe une couche d’amélioration locale
Moscato, On Evolution, Search, Optimization, Genetic Algorithms and Martial Arts, 1989
recherche tabou : Glover, 1986 ; découplage du monolithe DiscreteTS de ygmh (Hernandez & Gonzalez, Metaheuristics in C#)
DiscreteSwapPSO
PSO où la vélocité est une distribution sur des transpositions (algèbre Minus / Move / Times)
port du DiscretePSO de ygmh (Hernandez & Gonzalez)
MemeticAlgorithm n’est pas un « nouvel algorithme » : c’est un emballage. Sa documentation le dit explicitement – ImproveOperator == null désactive la couche (pass-through pur), le wrapper se contente alors d’enchaîner le composé interne. Armé d’un opérateur, il applique une amélioration locale au meilleur chromosome à chaque génération (ImprovementCount chromosome(s) améliorés par génération).
L’expérience monte l’échelle complète. D’abord ScatterSearch et DiscreteSwapPSO sur le banc apparié Rastrigin (centré vs dé-biaisé, même protocole que la première vague). Puis Memetic(ScatterSearch)armé d’un hill-climb tabou construit sur les primitives – trois briques sur quatre dans une seule construction.
// Deuxieme vague (1/2) : ScatterSearch entre au catalogue (factory par nom, comme les// autres) ; DiscreteSwapPSO se construit directement (hors catalogue). Banc apparie// Rastrigin centre vs de-biase, 3 runs (moyenne), Default en reference -- meme protocole// que la premiere vague (meme seed => meme decorateur debiase que la cellule precedente).var namesWave2 = MetaHeuristicsService.GetMetaHeuristicNames();bool scatterInCatalog = namesWave2.Contains("ScatterSearch");Console.WriteLine($"ScatterSearch au catalogue : {scatterInCatalog} (sur {namesWave2.Count} entrees)");Console.WriteLine("fitness = objectif negativé, plus proche de 0 = meilleur ; moyenne sur les runs");Console.WriteLine();var wave2Configs =new(string Name, Func<IMetaHeuristic> Build)[]{("Default (GA)",()=>BuildViaFactory("Default")),("ScatterSearch",()=>BuildViaFactory("ScatterSearch")),("DiscreteSwapPSO",()=>new DiscreteSwapPSO { MaxGenerations = Generations, NoMutation =true}.Build()),};var rastriginType =typeof(RastriginFitness);var rastriginInner =(IFitness)Activator.CreateInstance(rastriginType);var offsetWave2 = ShiftVectors.Seeded(Dim,1.5, ndSeed);var cecWave2 =newRotatedFitness(newShiftedFitness(rastriginInner, offsetWave2), Qnd);Console.WriteLine("{0,-17} {1,16} {2,16}","config","centré","dé-biaisé");foreach(var(cfgName, build)in wave2Configs){double centered = Enumerable.Range(0, ndRuns).Select(_ =>RunBenchmark(rastriginInner, rastriginType,build())).Average();double debiased = Enumerable.Range(0, ndRuns).Select(_ =>RunBenchmark(cecWave2, rastriginType,build())).Average(); Console.WriteLine("{0,-17} {1,16:G6} {2,16:G6}", cfgName, centered, debiased);}
ScatterSearch au catalogue : True (sur 16 entrees)
fitness = objectif negativé, plus proche de 0 = meilleur ; moyenne sur les runs
config centré dé-biaisé
Default (GA) -0,7193 -6,12603
ScatterSearch -6,94097 -5,28436
DiscreteSwapPSO -43,3697 -47,8313
Lecture : ScatterSearch premier au dé-biaisé – mais sous le bruit de tirage, DiscreteSwapPSO paye l’inadéquation du domaine
Rappel : fitness = objectif négativé, plus proche de 0 = meilleur ; moyenne sur 3 runs ; le décorateur dé-biaisé est le même que celui de la première vague (même seed – même offset, même rotation Qnd), donc la colonne « dé-biaisé » est comparable d’une vague à l’autre. Le banc est seedé (FastRandomRandomization.ResetSeed(42)) : ces valeurs se reproduisent à l’identique, mais les deux cellules consomment le RNG à des offsets différents, ce qui fournit une mesure directe de la sensibilité au tirage.
ScatterSearch devance Default au dé-biaisé – l’écart reste sous le bruit de tirage. Dé-biaisé : -5,28436 contre -6,12603 pour Default sur la même exécution, soit ×1,16. Or le même Default, entre la colonne dé-biaisée de la première vague (-10,462) et celle-ci (-6,12603), varie de ×1,71 : l’écart de tirage est plus grand que l’avantage mesuré. Autrement dit, sur cette exécution ScatterSearch est premier, mais ce classement n’est pas établi par ce banc – il faudrait plusieurs graines pour le trancher. Le centré, lui, reste nettement à l’avantage de Default (-0,7193 contre -6,94097, ×9,7). La raison mécanique de l’avantage dé-biaisé tient au reference-set, qui conserve une fraction de membres choisis par diversité max-min et pas seulement par qualité : la recherche ne s’engouffre pas dans le bassin d’attraction du point de départ, précisément là où le décentrage de l’optimum piège les méthodes gourmandes en exploitation. Sur cette exécution, ScatterSearch est le meilleur dé-biaisé des deux vagues confondues (première vague : SA -5,4753, DE -6,4986, Default -10,462, CanonicalPSO -11,703, BBPSO -14,784) – mais SA le talonne à ×1,04, donc la première place n’est pas davantage un résultat robuste.
DiscreteSwapPSO : le contre-exemple instructif.-43,3697 / -47,8313, loin derrière tout le plateau. Ce n’est pas un défaut d’implémentation mais une inadéquation de domaine : la vélocité de ce PSO est une distribution sur des transpositions – elle réarrange des valeurs existantes, elle n’en fabrique pas. Sur des gènes continus, aucun réarrangement ne peut raffiner vers l’optimum. L’opérateur est fait pour les espaces de permutations, où « déplacer un occupant vers sa position cible » est précisément le bon geste – ce sera l’arme naturelle sur le TSP de MGS-7. Le catalogue ne promet pas l’universalité : chaque brique a son domaine.
// Deuxieme vague (2/2) : l'echelle memetique sur ScatterSearch.// La factory materialise un convertisseur identite quand on n'en fournit pas// (cf. MetaHeuristicsService.CreateMetaHeuristicByName) ; en construction directe,// on le fait nous-memes -- c'est exactement ce que la factory conditionne.var convNb =newTypedGeometricConverter();convNb.SetTypedConverter(new GeometricConverter<double>{ IsOrdered =false, DoubleToGeneConverter =(geneIndex, geomValue)=> geomValue, GeneToDoubleConverter =(genIndex, geneValue)=> geneValue});ScatterSearch NewScatterSearch()=>new ScatterSearch{ MaxGenerations = Generations, NoMutation =true, GeometricConverter = convNb};// Barreau 1 : SS nu (factory). Barreau 2 : Memetic(SS) inerte -- ImproveOperator null,// la couche est un pass-through documente. Barreau 3 : Memetic(SS) arme d'un hill-climb// tabou (Stepped(0.25), memoire recency tenure 32, aspiration-sur-meilleur, 4 moves max).IMetaHeuristic BuildMemeticTabu(IFitness fitness)=>newMemeticAlgorithm(NewScatterSearch()){ ImprovementCount =1, ImproveOperator = TabuHillClimb.Improvement(newIdentityConverterNb(), c => fitness.Evaluate(c), TabuHillClimb.Stepped(0.25),newSolutionHashProjection(),()=>newRecencyTabuMemory(32),newAspirationOnBestFilter(), maxMoves:4)}.Build();var ladderConfigs =new(string Name, Func<IFitness, IMetaHeuristic> Build)[]{("SS nu", f =>BuildViaFactory("ScatterSearch")),("Memetic(SS) inerte", f =>newMemeticAlgorithm(NewScatterSearch()).Build()),("Memetic(SS)+tabou", f =>BuildMemeticTabu(f)),};var ladderInner =(IFitness)Activator.CreateInstance(rastriginType);Console.WriteLine("{0,-20} {1,16} {2,16}","config","centré","dé-biaisé");foreach(var(name, build)in ladderConfigs){double c = Enumerable.Range(0, ndRuns).Select(_ =>RunBenchmark(ladderInner, rastriginType,build(ladderInner))).Average();double d = Enumerable.Range(0, ndRuns).Select(_ =>RunBenchmark(cecWave2, rastriginType,build(cecWave2))).Average(); Console.WriteLine("{0,-20} {1,16:G6} {2,16:G6}", name, c, d);}// Convertisseur identite double<->double pour le hill-climb tabou, meme recette que les// tests du fork (pas d'embedding : la marche opere directement sur les genes).sealedclass IdentityConverterNb : IGeometricConverter{publicbool IsOrdered =>false;publicdoubleGeneToDouble(int geneIndex,object geneValue)=>(double)geneValue;publicobjectDoubleToGene(int geneIndex,double metricValue)=> metricValue;public IGeometryEmbedding<object>GetEmbedding()=>null!;}
config centré dé-biaisé
SS nu -6,20815 -3,94196
Memetic(SS) inerte -5,59382 -3,37504
Memetic(SS)+tabou -5,3761 -5,3287
Lecture : l’échelle prouve la composition, pas un classement
Trois barreaux, une seule bande. De -3,4 à -6,2 selon la colonne : à ce budget (80 générations, 60 individus, 3 runs), la variance inter-trajectoires domine les différences entre barreaux. Détail révélateur : le barreau inerte (-5,59382 centré) fait mieux que SS nu (-6,20815) alors que sa couche d’amélioration est désactivée. La neutralité du wrapper est structurelle – ImproveOperator == null, aucune évaluation ajoutée, aucun individu modifié – mais pas trajectorielle : l’arbre d’opérateurs compte un conteneur de plus, le flux RNG est consommé différemment, et sur un paysage multimodal comme Rastrigin deux trajectoires distinctes à budget égal peuvent atterrir dans des bassins différents. Un benchmark à 3 runs sépare le mécanisme du bruit – le protocole complet (multi-runs, fonctions multiples) est l’objet de MGS-6.
Ce que l’échelle prouve est mécanique. Les briques s’emboîtent sans toucher au composé interne : ScatterSearch – reconstruit ici à la main avec le convertisseur identité que la factory matérialise d’ordinaire pour nous – entre dans MemeticAlgorithm (wrapper), qui reçoit un opérateur TabuHillClimb.Improvement câblé sur trois pièces détachées : projection (SolutionHashProjection : l’attribut interdit est la solution entière), mémoire (RecencyTabuMemory(32) : file FIFO des 32 derniers attributs), filtre (AspirationOnBestFilter : l’interdit se lève si le candidat bat le meilleur jamais vu). Aucune de ces classes n’en connaît une autre ; toutes trois sont injectées.
L’armement tabou : un signal, pas un verdict. Centré, -5,3761 contre -5,59382 pour l’inerte – dans le bon sens. Dé-biaisé, c’est l’inverse : -5,3287 contre -3,37504, la couche tabou passe derrière le barreau inerte. Le signe de l’écart dépend donc de la colonne, ce qui est la signature d’une différence noyée dans le bruit à 3 runs. Le hill-climb explore v ± 0,25 par gène (4 moves maximum par passe sur le meilleur) : un pas grossier pour Rastrigin à 5 dimensions, calibré ici pour montrer le câblage, pas pour gagner le benchmark.
Conclusion : la factory ne cache rien, elle conditionne
Le parcours en trois temps montre la thèse de MetaGeneticSharp en acte :
Description (métaphore) -> reconstruction (BuildMainHeuristic, code réel lu) -> factory (CreateMetaHeuristicByName). Les trois désignent le même objet : la factory renvoie l’arbre de primitives que la reconstruction assemble (prouvé par GetMetaHeuristicTypeByName, qui retourne le type racine IfElse/Match/Generation/Island).
La factory n’est donc pas une boîte noire qui remplacerait l’algorithme par une magie cachée : c’est un raccourci vers la reconstruction elle-même. Si vous voulez comprendre WOA, vous lisez WhaleOptimisationAlgorithm.BuildMainHeuristic() — il est là, en plein jour. Si vous voulez l’utiliser, vous appelez la factory. Et si vous voulez le modifier (changer l’opérateur bubble-net, ajouter une borne, hybrider avec EO sur des îles), vous éditez une primitive — pas un monolithe.
C’est la différence avec une mealpy.WOA() : mealpy offre le raccourci (et c’est précisément le modèle de fiche descriptive que nous avons repris en étape 1), mais n’offre pas la transparence. MetaGeneticSharp offre les deux, parce que le raccourci est la reconstruction, exposée.
Le notebook suivant, MGS-6 Benchmarks, pousse l’argument jusqu’au bout : mesurer que ces composés reconstruits sont compétitifs face au GA nu et au modèle insulaire, sur 10 fonctions canoniques.
Exercices
Convention : cellules à compléter. Squelette fourni, à vous d’implémenter. Ne pas lever d’erreur — utiliser // TODO et un Console.WriteLine informatif.
Exercice 1 : WhaleOptimisationNaive — la variante simplifiée
Le catalogue contient une seconde WOA, WhaleOptimisationNaive. La factory l’obtient en remplaçant l’opérateur bubble-net hélicoïdal par une simple combinaison convexe (GetSimpleBubbleNetOperator(mixCoef: 0.5), cf. la classe WhaleOptimisationAlgorithm). Construisez-la via la factory, lancez-la sur Sphere ET Rastrigin, et comparez à la WOA complète. Question : sur quelle famille de surfaces le bubble-net hélicoïdal apporte-t-il un gain par rapport à la combinaison convexe naïve ?
// Exercice 1 : comparer WOA complète vs WhaleOptimisationNaive sur Sphere + Rastrigin.// TODO étudiant : construire les deux composés via BuildViaFactory, lancer RunBenchmark sur// Sphere et Rastrigin, et afficher un tableau (complète, naïve) x (Sphere, Rastrigin).Console.WriteLine("Exercice à compléter : WOA complète vs WhaleOptimisationNaive.");
Exercice à compléter : WOA complète vs WhaleOptimisationNaive.
Exercice 2 : composer un hybride qui n’existe dans aucune bibliothèque
Toute la valeur de l’approche primitives-based est de rendre les hybrides triviaux à énoncer. En vous inspirant de la reconstruction FBI (qui cycle 4 phases avec GenerationMetaHeuristic), composez une métaheuristique qui applique EO pendant la première moitié des générations puis WOA pendant la seconde moitié (un raffinement « exploration équilibre -> exploitation baleine »). Étape 1 : construire eoMh et woaMh via la factory. Étape 2 : les envelopper dans un new GenerationMetaHeuristic(Generations / 2, eoMh, woaMh). Étape 3 : la lancer via RunBenchmark sur Rosenbrock et comparer aux composés seuls.
// Exercice 2 : hybride EO-then-WOA via GenerationMetaHeuristic.// TODO étudiant :// IMetaHeuristic eoMh = ...; // BuildViaFactory("EquilibriumOptimizer")// IMetaHeuristic woaMh = ...; // BuildViaFactory("WhaleOptimisation")// var hybrid = new GenerationMetaHeuristic(Generations / 2, eoMh, woaMh);// double f = RunBenchmark(new RosenbrockFitness(), typeof(RosenbrockFitness), hybrid);Console.WriteLine("Exercice à compléter : hybride EO-then-WOA (aucune lib ne fournit cela).");
Exercice à compléter : hybride EO-then-WOA (aucune lib ne fournit cela).
Exercice 3 : preuve d’équivalence factory <-> classe
Pour un composé géométrique au choix (EO ou FBI), vérifiez explicitement que MetaHeuristicsService.CreateMetaHeuristicByName(name) et l’appel direct new XClass().Build() renvoient le même type racine (via GetMetaHeuristicTypeByName). C’est la preuve que la factory ne fait qu’envelopper la reconstruction de la classe — pas de magie cachée. Étape 1 : lire le type racine via GetMetaHeuristicTypeByName. Étape 2 : construire l’instance via la classe (par ex. new EquilibriumOptimizer { MaxGenerations = Generations }.Build() après avoir câblé un convertisseur). Étape 3 : comparer les deux Type.
// Exercice 3 : factory == classe (même type racine).// TODO étudiant : pour EO (ou FBI), comparer :// - MetaHeuristicsService.GetMetaHeuristicTypeByName("EquilibriumOptimizer")// - le type produit par new EquilibriumOptimizer { MaxGenerations = Generations }.Build()// (pensez à câbler un GeometricConverter<double> identité, comme la factory le fait en interne).Console.WriteLine("Exercice à compléter : équivalence factory <-> classe (même type racine).");
Exercice à compléter : équivalence factory <-> classe (même type racine).