MGS-31 : Synthèse croisée MGS contre mealpy — neuf paires, un verdict

Notebook de synthèse de l’EPIC #12373 (tracker #12607, item 5). Il ne mesure rien lui-même : il récolte les sorties committées des neuf notebooks de paires (MGS-22 à MGS-30, toutes re-exécutées post-fix sur le noyau assaini — bumps ad13416104/158fca7af6, correctifs #12082/#12084/#12538, série probe #13407) et répond à la question directrice de l’Epic :

L’écart de convergence MGS vs mealpy est-il systématique (noyau) ou spécifique (portage) ?

Navigation : << MGS-30 · Série Part 4 — Métaheuristiques · MGS-31 (dernier)

*** | Méthode · Tableau croisé · Forme de l’écart · Diagnostic · Épilogue

Méthode

Le protocole apparié est celui hérité de MGS-22 : même fonction de coût (Sudoku 36 gènes R1, 45 indices fixes), même budget d’évaluations, graines {0, 1, 7, 42}, sanity check des vecteurs témoins des deux côtés du pont PythonNet. Chaque notebook de paire committé sa table de mesures ; ce notebook relit ces fichiers à l’exécution et re-dérive chaque chiffre.

Quatre règles, héritées des incidents fondateurs de l’Epic :

  • Prose dérivée uniquement des outputs committés (leçon MGS-5 #12187 : une prose citait des chiffres absents des sorties). Aucun nombre de ce notebook n’est saisi à la main.
  • Classements calculés dynamiquement (leçon MGS-2 #11943 : le « Constat » racontait l’attendu que son propre output contredisait). Les verdicts « qui gagne » sortent d’un tri, pas d’un récit.
  • Cellules d’exercices exclues de la récolte (les exercices pré-résolus sont en cours de dé-leak, #13515) : seules les cellules de banc protocole alimentent le tableau.
  • Bras canoniques explicites : quatre paires sont des décompositions à trois bras (P4 spirale/naïf, P7 kennedy/jumeau-formule-MGS, P9 complet/ablaté). Le tableau croisé retient un bras MGS et un bras mealpy par paire — MGS canonique (le composé complet : spirale, EO, complet) contre mealpy standard (le jumeau à formule de la littérature : BaseGA, OriginalEO, kennedy) — et la section « forme de l’écart » replace les bras témoins à côté.
// === MGS-31 : récolte des outputs committés des 9 paires ===
// Parse les .ipynb voisins (System.Text.Json), extrait les streams des cellules de banc,
// re-dérive médianes / min-max / ms-éval. Accent-insensible (médiane/mediane), virgule
// décimale française parsée en invariant, bras multiples (décompositions 3 bras).
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;

string[] Fichiers = {
    "MGS-22-MGS-vs-Mealpy.ipynb",
    "MGS-23-DifferentialEvolution-vs-Mealpy.ipynb",
    "MGS-24-SimulatedAnnealing-vs-Mealpy.ipynb",
    "MGS-25-WhaleOptimisation-vs-Mealpy.ipynb",
    "MGS-26-EquilibriumOptimizer-vs-Mealpy.ipynb",
    "MGS-27-ForensicBasedInvestigation-vs-Mealpy.ipynb",
    "MGS-28-BareBonesPSO-vs-Mealpy.ipynb",
    "MGS-29-GA-vs-Mealpy.ipynb",
    "MGS-30-ScatterSearch-Decomposition.ipynb",
};
string[] Algorithmes = {
    "PSO canonique (paire fondatrice)", "DifferentialEvolution", "SimulatedAnnealing",
    "WhaleOptimisation", "EquilibriumOptimizer", "ForensicBasedInvestigation",
    "BareBonesPSO", "GA Default/BaseGA", "ScatterSearch",
};

// Normalisation : minuscules + suppression des diacritiques ("médiane" == "mediane").
string Norm(string s)
{
    var sb = new StringBuilder();
    foreach (var ch in s.Normalize(NormalizationForm.FormD))
        if (CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark)
            sb.Append(ch);
    return sb.ToString().ToLowerInvariant();
}
double Num(string s) => double.Parse(s.Replace(',', '.'), CultureInfo.InvariantCulture);

record LigneGraine(string Bras, int Graine, int Conflits, double MsEval, int[] Cps);
record ResumeImprime(string Bras, double Mediane, int Min, int Max, double MsEvalMoyen);
record Paire(int Num, string Fichier, string Algo, List<LigneGraine> Lignes,
             List<ResumeImprime> Resumes, double? RatioImprime, bool Determinisme)
{
    public string BrasMgs => Lignes.Select(l => l.Bras).First(b => b.StartsWith("mgs") && !b.Contains("ablat") && !b.Contains("naive"));
    public string BrasMealpy => Lignes.Select(l => l.Bras).First(b => b.StartsWith("mealpy") && b.IndexOf("mgs", 6) < 0);
}

var reLigne = new Regex(@"^(?<eng>(?:mgs|mealpy)[a-z0-9\- ]*?)\s+(?<g>\d+)\s+(?<tail>[0-9].*)$", RegexOptions.Compiled);
// NB : groupes NOMMES obligatoires — en .NET les groupes non nommes sont numerotes AVANT les nommes,
// Groups[4] etait donc le ms/eval ("0,104") et non le max (exception Int32.Parse).
var reResume = new Regex(@"^(?<eng>[a-z][a-z0-9\- ]*?)\s*:\s*mediane conflits (?<med>[\d.,]+) \(min (?<min>\d+), max (?<max>\d+)\), ms/eval moyen (?<mse>[\d.,]+)", RegexOptions.Compiled);
var reRatio = new Regex(@"rapport ms/eval mealpy/[a-z0-9\- ]+:\s*([\d.,]+)x", RegexOptions.Compiled);
var reDet = new Regex(@"determinisme", RegexOptions.Compiled);

var paires = new List<Paire>();
for (int p = 0; p < Fichiers.Length; p++)
{
    var doc = JsonDocument.Parse(File.ReadAllText(Fichiers[p]));
    var lignes = new List<LigneGraine>();
    var resumes = new List<ResumeImprime>();
    double? ratio = null; bool det = false;
    foreach (var cell in doc.RootElement.GetProperty("cells").EnumerateArray())
    {
        if (cell.GetProperty("cell_type").GetString() != "code") continue;
        if (!cell.TryGetProperty("outputs", out var outs)) continue;
        foreach (var o in outs.EnumerateArray())
        {
            if (o.GetProperty("output_type").GetString() != "stream") continue;
            // "text" peut etre un tableau de chaines OU une chaine unique (nbformat tolere les deux).
            var tel = o.GetProperty("text");
            var texte = tel.ValueKind == JsonValueKind.Array
                ? string.Join("", tel.EnumerateArray().Select(t => t.GetString()))
                : tel.GetString();
            // Cellules d'exercice : exclues (de-leak #13515 en cours sur les paires).
            if (texte.Contains("Exercice")) continue;
            foreach (var brute in texte.Split('\n'))
            {
                var l = Norm(brute.Trim());
                var m = reLigne.Match(l);
                if (m.Success)
                {
                    var jetons = m.Groups["tail"].Value.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
                    if (jetons.Length >= 2 && int.TryParse(jetons[0], out int conflits))
                    {
                        // Colonne optionnelle checkpoints en fin de ligne ("33/31/31/31") :
                        // ms/eval est alors l'avant-dernier jeton, les checkpoints le dernier.
                        var dernier = jetons[^1];
                        int[] cps = null; int idxMsEval = jetons.Length - 1;
                        var mcp = Regex.Match(dernier, @"^(\d+)/(\d+)/(\d+)/(\d+)$");
                        if (mcp.Success)
                        {
                            cps = new[] { int.Parse(mcp.Groups[1].Value), int.Parse(mcp.Groups[2].Value), int.Parse(mcp.Groups[3].Value), int.Parse(mcp.Groups[4].Value) };
                            idxMsEval = jetons.Length - 2;
                        }
                        lignes.Add(new LigneGraine(m.Groups["eng"].Value.TrimEnd(), int.Parse(m.Groups["g"].Value), conflits, Num(jetons[idxMsEval]), cps));
                    }
                    continue;
                }
                var r = reResume.Match(l);
                if (r.Success)
                    resumes.Add(new ResumeImprime(r.Groups["eng"].Value.TrimEnd(), Num(r.Groups["med"].Value),
                        int.Parse(r.Groups["min"].Value), int.Parse(r.Groups["max"].Value), Num(r.Groups["mse"].Value)));
                var q = reRatio.Match(l);
                if (q.Success) ratio = Num(q.Groups[1].Value);
                if (reDet.IsMatch(l)) det = true;
            }
        }
    }
    paires.Add(new Paire(p + 1, Fichiers[p], Algorithmes[p], lignes, resumes, ratio, det));
}

Console.WriteLine($"paire  bras (lignes de graine)                          resumes  ratio impr.  determ.");
foreach (var p in paires)
{
    var bras = string.Join(", ", p.Lignes.GroupBy(l => l.Bras).Select(g => $"{g.Key} x{g.Count()}"));
    Console.WriteLine($"P{p.Num}     {bras,-52} {p.Resumes.Count}        {(p.RatioImprime?.ToString("0.00") ?? "-"),-6}      {(p.Determinisme ? "oui" : "non")}");
}
paire  bras (lignes de graine)                          resumes  ratio impr.  determ.
P1     mgs x4, mealpy x4                                    2        0,81        oui
P2     mgs x4, mealpy x4                                    2        2,37        oui
P3     mgs x4, mealpy x4                                    2        3,26        oui
P4     mgs spirale x4, mgs naive x4, mealpy x4              3        1,78        oui
P5     mgs eo x4, mealpy x4                                 2        1,41        oui
P6     mgs x4, mealpy x4                                    2        3,06        oui
P7     mgs x4, mealpy-mgs x4, mealpy-kennedy x4             3        -           oui
P8     mgs x4, mealpy x4                                    2        7,83        oui
P9     mgs-complet x4, mgs-ablate x4, mealpy-jumeau x4      3        -           oui

Lecture. Les neuf paires exposent le format standard du protocole ; quatre d’entre elles (P4, P7, P9 et la paire fondatrice P1 dans sa version à bras multiples) déclarent des bras de décomposition en plus des bras canoniques. Les comptes ci-dessus sont la matière première ; la cellule suivante confronte chaque chiffre re-dérivé au résumé imprimé par la paire elle-même — un désaccord signifierait que ce notebook parse mal, et serait signalé avant toute interprétation.

// === Contrôle d'intégrité : re-derivation vs resumes imprimés ===
// Médiane = moyenne des deux valeurs centrales (4 graines). Tolérances relatives aux
// arrondis d'impression des paires (2-3 decimales imprimees, moyenne de valeurs arrondies) :
// médiane ±0,05 exacte ; ms/éval ±0,002 ou 2% ; ratio ±0,06 ou 3%.
double Mediane(IEnumerable<int> v) { var s = v.OrderBy(x => x).ToArray(); int n = s.Length; return n % 2 == 1 ? s[n / 2] : (s[n / 2 - 1] + s[n / 2]) / 2.0; }
double Moyenne(IEnumerable<double> v) => v.Any() ? v.Average() : double.NaN;

int passes = 0, echecs = 0;
foreach (var p in paires)
{
    var problems = new List<string>();
    foreach (var bras in p.Lignes.Select(l => l.Bras).Distinct())
    {
        var lg = p.Lignes.Where(l => l.Bras == bras).ToList();
        var imp = p.Resumes.FirstOrDefault(r => r.Bras == bras);
        if (imp is null) { problems.Add($"resume absent ({bras})"); continue; }
        double med = Mediane(lg.Select(l => l.Conflits));
        if (Math.Abs(med - imp.Mediane) > 0.05) problems.Add($"mediane {bras} {med:0.0}!={imp.Mediane:0.0}");
        if (lg.Min(l => l.Conflits) != imp.Min) problems.Add($"min {bras}");
        if (lg.Max(l => l.Conflits) != imp.Max) problems.Add($"max {bras}");
        double mse = Moyenne(lg.Select(l => l.MsEval));
        if (!(Math.Abs(mse - imp.MsEvalMoyen) <= 0.002 || Math.Abs(mse - imp.MsEvalMoyen) <= 0.02 * imp.MsEvalMoyen))
            problems.Add($"ms/eval {bras} {mse:0.000}!={imp.MsEvalMoyen:0.000}");
    }
    if (p.RatioImprime.HasValue)
    {
        var lm = p.Lignes.Where(l => l.Bras == p.BrasMgs).Select(l => l.MsEval).ToList();
        var lp = p.Lignes.Where(l => l.Bras == p.BrasMealpy).Select(l => l.MsEval).ToList();
        double ratioCalc = Moyenne(lp) / Moyenne(lm);
        if (!(Math.Abs(ratioCalc - p.RatioImprime.Value) <= 0.06 || Math.Abs(ratioCalc - p.RatioImprime.Value) <= 0.03 * p.RatioImprime.Value))
            problems.Add($"ratio {ratioCalc:0.00}!={p.RatioImprime:0.00}");
    }
    if (problems.Count == 0) { passes++; Console.WriteLine($"P{p.Num} ({p.Algo}) : PASS — re-derivation conforme aux resumes imprimés"); }
    else { echecs++; Console.WriteLine($"P{p.Num} ({p.Algo}) : FAIL — {string.Join("; ", problems)}"); }
}
Console.WriteLine();
Console.WriteLine($"INTEGRITE : {passes}/{paires.Count} paires conformes, {echecs} écart(s).");
P1 (PSO canonique (paire fondatrice)) : PASS — re-derivation conforme aux resumes imprimés
P2 (DifferentialEvolution) : PASS — re-derivation conforme aux resumes imprimés
P3 (SimulatedAnnealing) : PASS — re-derivation conforme aux resumes imprimés
P4 (WhaleOptimisation) : PASS — re-derivation conforme aux resumes imprimés
P5 (EquilibriumOptimizer) : PASS — re-derivation conforme aux resumes imprimés
P6 (ForensicBasedInvestigation) : PASS — re-derivation conforme aux resumes imprimés
P7 (BareBonesPSO) : PASS — re-derivation conforme aux resumes imprimés
P8 (GA Default/BaseGA) : PASS — re-derivation conforme aux resumes imprimés
P9 (ScatterSearch) : PASS — re-derivation conforme aux resumes imprimés

INTEGRITE : 9/9 paires conformes, 0 écart(s).

Tableau croisé

Chaque ligne est une paire ; les colonnes qualité (conflits médians, étendue) et coût (ms par évaluation, rapport mealpy/MGS) sont calculées depuis les graines des bras canoniques récoltées. Le verdict qualité trie la médiane (moins de conflits = meilleur) ; le verdict coût lit le rapport (rapport > 1 = mealpy plus coûteux par évaluation).

// === Le tableau croisé 9 paires (bras canoniques) + detail des bras temoins ===
double Mediane2(IEnumerable<int> v) { var s = v.OrderBy(x => x).ToArray(); int n = s.Length; return n % 2 == 1 ? s[n / 2] : (s[n / 2 - 1] + s[n / 2]) / 2.0; }
string Etendue(IEnumerable<int> v) { var l = v.ToList(); return $"{l.Min()}-{l.Max()}"; }
string Fr(double v) => v.ToString("0.00", CultureInfo.GetCultureInfo("fr-FR"));

var lignesTab = new List<string>();
lignesTab.Add("| Paire | Algorithme | Méd MGS | Méd mealpy | Δ méd (MGS−mealpy) | Étendue MGS | Étendue mealpy | ms/éval MGS | ms/éval mealpy | Ratio | Qualité |");
lignesTab.Add("|---|---|---|---|---|---|---|---|---|---|---|");
foreach (var p in paires)
{
    var lm = p.Lignes.Where(l => l.Bras == p.BrasMgs).ToList();
    var lp = p.Lignes.Where(l => l.Bras == p.BrasMealpy).ToList();
    double medM = Mediane2(lm.Select(l => l.Conflits)), medP = Mediane2(lp.Select(l => l.Conflits));
    double mseM = lm.Average(l => l.MsEval), mseP = lp.Average(l => l.MsEval);
    double ratio = mseP / mseM;
    double delta = medM - medP;
    string verdict = delta < -0.001 ? "**MGS**" : delta > 0.001 ? "mealpy" : "égal";
    lignesTab.Add($"| P{p.Num} | {p.Algo} | {Fr(medM)} | {Fr(medP)} | {delta:+0.0;-0.0;0} | {Etendue(lm.Select(l => l.Conflits))} | {Etendue(lp.Select(l => l.Conflits))} | {mseM:0.000} | {mseP:0.000} | {Fr(ratio)}x | {verdict} |");
}
foreach (var l in lignesTab) Console.WriteLine(l);

var stats = paires.Select(p => {
    var lm = p.Lignes.Where(l => l.Bras == p.BrasMgs).ToList();
    var lp = p.Lignes.Where(l => l.Bras == p.BrasMealpy).ToList();
    return new {
        Num = p.Num, Algo = p.Algo,
        MedM = Mediane2(lm.Select(l => l.Conflits)), MedP = Mediane2(lp.Select(l => l.Conflits)),
        MseM = lm.Average(l => l.MsEval), MseP = lp.Average(l => l.MsEval),
    };
}).ToList();

Console.WriteLine();
Console.WriteLine("Bras de décomposition (hors canoniques) — médianes pour mémoire :");
foreach (var p in paires)
{
    var temoins = p.Lignes.GroupBy(l => l.Bras)
        .Where(g => g.Key != p.BrasMgs && g.Key != p.BrasMealpy)
        .Select(g => $"{g.Key} {Mediane2(g.Select(l => l.Conflits)):0.0}").ToList();
    if (temoins.Count > 0) Console.WriteLine($"  P{p.Num} : {string.Join(", ", temoins)}");
}
| Paire | Algorithme | Méd MGS | Méd mealpy | Δ méd (MGS−mealpy) | Étendue MGS | Étendue mealpy | ms/éval MGS | ms/éval mealpy | Ratio | Qualité |
|---|---|---|---|---|---|---|---|---|---|---|
| P1 | PSO canonique (paire fondatrice) | 43,50 | 28,50 | +15,0 | 39-48 | 26-31 | 0,104 | 0,085 | 0,82x | mealpy |
| P2 | DifferentialEvolution | 25,50 | 21,50 | +4,0 | 20-29 | 20-27 | 0,042 | 0,100 | 2,38x | mealpy |
| P3 | SimulatedAnnealing | 27,00 | 31,00 | -4,0 | 22-28 | 27-34 | 0,042 | 0,137 | 3,28x | **MGS** |
| P4 | WhaleOptimisation | 49,50 | 51,00 | -1,5 | 46-51 | 50-54 | 0,066 | 0,117 | 1,78x | **MGS** |
| P5 | EquilibriumOptimizer | 42,00 | 25,00 | +17,0 | 40-46 | 21-31 | 0,064 | 0,090 | 1,40x | mealpy |
| P6 | ForensicBasedInvestigation | 37,00 | 43,50 | -6,5 | 25-39 | 43-45 | 0,049 | 0,150 | 3,05x | **MGS** |
| P7 | BareBonesPSO | 30,50 | 25,50 | +5,0 | 29-31 | 19-27 | 0,036 | 0,094 | 2,58x | mealpy |
| P8 | GA Default/BaseGA | 13,50 | 12,50 | +1,0 | 12-18 | 9-16 | 0,017 | 0,136 | 7,86x | mealpy |
| P9 | ScatterSearch | 54,00 | 51,50 | +2,5 | 43-56 | 48-53 | 0,113 | 0,123 | 1,09x | mealpy |

Bras de décomposition (hors canoniques) — médianes pour mémoire :
  P4 : mgs naive 49,5
  P7 : mealpy-mgs 25,5
  P9 : mgs-ablate 52,0

Lecture du tableau. La qualité ne raconte pas une histoire unique : mealpy domine 6 paires sur 9, MGS 3, et l’amplitude va de −6,5 (P6, FBI) à +17 (P5, EO). Les trois paires où MGS gagne — recuit simulé (−4), WOA (−1,5), FBI (−6,5) — sont précisément celles dont le composé MGS embarque une réinsertion ou une stratégie de mouvement absente du jumeau mealpy ; les six autres donnent l’avantage au portage mealpy, jusqu’au triple écart de l’EquilibriumOptimizer original (P5 : médianes 42 contre 25).

Deux lignes sortent du lot. P1 cumule l’écart qualité le plus large après P5 (+15 conflits) et le seul rapport de coût inférieur à 1 (0,82x) : le banc fondateur mesure un PSO MGS instrumenté dont la sonde par évaluation pèse plus lourd que le moteur mealpy — la paire elle-même mesure la fitness seule à 5,13x Python/C#,confirmation que l’instrumentation, pas l’algorithme, inverse ce rapport. P8 est la paire la plus serrée en qualité (13,5 contre 12,5 — un seul conflit de médiane) et la plus coûteuse en relatif (7,86x) : deux moteurs GA convergent pareil, l’un paie chaque évaluation sept fois moins cher. Les étendues médianes (6 conflits MGS, 7 mealpy) sont du même ordre : aucun des deux moteurs n’est plus «loterie» que l’autre à quatre graines.


La forme de l’écart

Le tracker #12607 demande de classer la forme de chaque écart (précipitation précoce / stagnation tardive / coût par step) depuis des trajectoires best-so-far aux checkpoints 25/50/75/100 %. Deux paires seulement journalisent ces checkpoints dans leur banc — P7 (tous les bras, toutes les graines) et P9 (tous les bras, toutes les graines) ; les autres ne commettent que le point final. La colonne « forme » est donc partielle par construction sur 7 paires — ce qui suit l’affirme plutôt que de le maquiller. P9 va plus loin : son propre output classe déjà la forme de son écart axe/noyau par graine ; cette classification est reprise telle quelle.

// === Trajectoires checkpoints journalisées + agrégats ===
Console.WriteLine("Checkpoints best-so-far par graine (quand journalisés au banc) :");
foreach (var p in paires.Where(p => p.Lignes.Any(l => l.Cps != null)))
{
    foreach (var g in p.Lignes.GroupBy(l => l.Bras).OrderBy(g => g.Key))
    {
        if (g.First().Cps == null) continue;
        var m1 = string.Join("  ", g.Select(l => $"g{l.Graine}: {l.Cps[0]}/{l.Cps[1]}/{l.Cps[2]}/{l.Cps[3]}"));
        Console.WriteLine($"  P{p.Num} {g.Key,-15} : {m1}");
    }
}

Console.WriteLine();
Console.WriteLine("Agrégats sur les 9 paires (bras canoniques) :");
double[] ratios = stats.Select(s => s.MseP / s.MseM).ToArray();
int mgsGagne = stats.Count(s => s.MedM < s.MedP), mealpyGagne = stats.Count(s => s.MedM > s.MedP), egal = stats.Count(s => s.MedM == s.MedP);
Console.WriteLine($"  Qualité (médiane conflits) : MGS devant {mgsGagne}/9, mealpy devant {mealpyGagne}/9, égalité {egal}/9.");
Console.WriteLine($"  Coût ms/éval (mealpy/MGS)  : min {Fr(ratios.Min())}x, max {Fr(ratios.Max())}x, médiane {Fr(ratios.OrderBy(r => r).ToArray()[ratios.Length / 2])}x.");
Console.WriteLine($"  Paires à rapport >= 2x    : {ratios.Count(r => r >= 2)}/9 ; à rapport < 1x : {ratios.Count(r => r < 1)}/9.");
Console.WriteLine($"  Dispersion qualité (étendue méd. des étendues) : MGS {Fr(Mediane2(paires.Select(p => { var l = p.Lignes.Where(x => x.Bras == p.BrasMgs).Select(x => x.Conflits).ToList(); return l.Max() - l.Min(); }))):0} conflits, mealpy {Fr(Mediane2(paires.Select(p => { var l = p.Lignes.Where(x => x.Bras == p.BrasMealpy).Select(x => x.Conflits).ToList(); return l.Max() - l.Min(); }))):0} conflits.");
Checkpoints best-so-far par graine (quand journalisés au banc) :
  P7 mealpy-kennedy  : g0: 32/22/20/19  g1: 31/26/25/25  g7: 32/26/26/26  g42: 32/30/28/27
  P7 mealpy-mgs      : g0: 32/22/20/19  g1: 31/26/25/25  g7: 32/26/26/26  g42: 32/30/28/27
  P7 mgs             : g0: 33/31/31/31  g1: 31/31/31/31  g7: 29/29/29/29  g42: 30/30/30/30
  P9 mealpy-jumeau   : g0: 53/53/53/53  g1: 50/50/48/48  g7: 53/53/53/53  g42: 50/50/50/50
  P9 mgs-ablate      : g0: 54/54/54/54  g1: 56/56/56/56  g7: 50/50/50/50  g42: 45/45/45/45
  P9 mgs-complet     : g0: 54/54/54/54  g1: 56/56/56/56  g7: 54/54/54/54  g42: 43/43/43/43

Agrégats sur les 9 paires (bras canoniques) :
  Qualité (médiane conflits) : MGS devant 3/9, mealpy devant 6/9, égalité 0/9.
  Coût ms/éval (mealpy/MGS)  : min 0,82x, max 7,86x, médiane 2,38x.
  Paires à rapport >= 2x    : 5/9 ; à rapport < 1x : 1/9.
  Dispersion qualité (étendue méd. des étendues) : MGS 6,00 conflits, mealpy 7,00 conflits.

Lecture. Sur les deux paires qui journalisent les trajectoires, la forme est lisible et ce n’est pas la même des deux côtés. En P7, le MGS atteint son plateau dès le checkpoint 25 % (g0 : 33/31/31/31 — plus un conflit gagné sur les trois quarts restants du budget) pendant que mealpy continue de descendre (g0 : 32/22/20/19) : stagnation précoce côté MGS, précipitation jamais consolidée côté mealpy. En P9, mealpy-jumeau gagne encore à 75 % du budget sur la graine 1 (50/50/48/48) — un mouvement tardif que le MGS-complet, plat sur toutes ses trajectoires, ne produit jamais.

L’isolateur le plus net de P7 : les deux bras mealpy — formule Kennedy-2003 littérale et formule MGS portée — ont des trajectoires identiques graine par graine (32/22/20/19, 31/26/25/25, …). La formule ne discrimine pas sur ce landscape ; l’écart P7 (30,5 contre 25,5) est donc un écart de moteur, pas de formule. Et le coût par step reste l’axe systématique : médiane 2,38x, cinq paires au-delà de 2x. Les sept autres paires ne commettent que le point final — leur forme ne se déduit pas, elle se mesurerait.

// === Deux vues : médianes par paire, rapport de coût par paire ===
// SVG inline zéro-dépendance (rend GitHub/nbviewer/offline) — canon SvgChartHelper.cs (#6927, #6942).
// Décimales en point invariant (virgule = séparateur de coordonnées SVG -> casse Chromium en fr-FR).
#load "../../Probas/Infer/SvgChartHelper.cs"

var cats = stats.Select(s => $"P{s.Num}").ToArray();
var medsMgs = stats.Select(s => s.MedM).ToArray();
var medsMealpy = stats.Select(s => s.MedP).ToArray();
var ratiosChart = stats.Select(s => s.MseP / s.MseM).ToArray();

display(SvgChartHelper.GroupedBar("Médiane de conflits par paire (4 graines, budget égal, bras canoniques)",
    cats, new[] { medsMgs, medsMealpy }, new[] { "MGS", "mealpy" }, width: 760, height: 380));
display(SvgChartHelper.GroupedBar("Rapport de coût ms/éval mealpy/MGS (log) — >1 = mealpy plus coûteux",
    cats, new[] { ratiosChart }, new[] { "ratio" }, width: 760, height: 340, logY: true));
Médiane de conflits par paire (4 graines, budget égal, bras canoniques)014.5829.1643.7458.32P1P2P3P4P5P6P7P8P9MGSmealpy
Rapport de coût ms/éval mealpy/MGS (log) — >1 = mealpy plus coûteux125P1P2P3P4P5P6P7P8P9ratio

Diagnostic : noyau ou portage ?

Le tableau croisé sépare ce que la question directrice confondait : l’écart de coût est systématique, l’écart de qualité est spécifique.

Coût par évaluation — systématique (noyau). Huit paires sur neuf paient mealpy plus cher par évaluation (médiane 2,38x, jusqu’à 7,86x en P8). C’est la frontière d’exécution Python-contre-C#, mesurée isolément dans chaque paire par le test « fitness seule » (rapport 5,13x à 5,33x selon les paires) : l’ordre de grandeur du rapport de banc s’explique par la runtime, pas par les algorithmes. L’unique inversion (P1, 0,82x) est elle-même un artefact d’instrumentation du banc fondateur, documenté dans la paire.

Qualité — spécifique (portage/algorithme). Aucun moteur ne domine : mealpy devant 6/9, MGS 3/9, avec des amplitudes qui vont dans les deux sens (−6,5 à +17). Les bras de décomposition isolent les causes là où ils existent : en P7, deux formules distinctes sur le même moteur mealpy convergent à l’identique (25,5 partout) alors que le moteur MGS reste à 30,5 — l’écart BBPSO est moteur ; en P9, le bras ablaté (52,0) bat le bras complet (54,0) sur le moteur MGS — la diversité RefSet ne paie pas sur ce paysage, l’écart ScatterSearch est interne à la stratégie, pas au noyau.

Enfin, le contexte d’assainissement ferme la porte aux explications faciles : toutes les mesures récoltées ici sont post-correction (seeding décoratif #12071/#12082/#12084, garde bloquante DE #12464, bornes fantômes #12466/#12538, constantes PSO canoniques byte-exactes #12032) et post-bump du submodule — les écarts mesurés sont ceux du noyau assaini, pas les résidus des défauts corrigés.

Réponse à la question directrice, limites, suite

L’écart de convergence MGS vs mealpy est systématique pour le coût, spécifique pour la qualité. Chaque évaluation mealpy coûte en médiane 2,4x plus cher (frontière de runtime, isolée par les tests fitness-seule des paires) ; la qualité de convergence, elle, se joue algorithme par algorithme — mealpy gagne 6 des 9 paires (jusqu’à −17 conflits de médiane sur l’EO original), MGS les 3 autres (SA, WOA, FBI, jusqu’à −6,5). Les bras de décomposition tranchent deux cas : P7 montre un écart de moteur à formule constante, P9 un écart de stratégie à moteur constant. Le noyau assaini n’a pas de défaut systématique résiduel à corriger : il a un profil — rapide par évaluation, inégal en convergence selon l’algorithme.

Limites, mesurées. Les trajectoires aux checkpoints ne sont journalisées que pour 2 paires sur 9 (la colonne « forme » est partielle par construction) ; un seul paysage (Sudoku 36 gènes R1, 45 indices fixes) ; quatre graines ; un seul budget. Les exercices ×4 budget des paires suggèrent que certains écarts se referment à budget étendu — hors périmètre du tableau croisé, qui mesure à budget égal ; la boucle corrective #13778 l’a depuis mesuré pour la paire SA : l’écart ne se referme pas, il s’inverse (voir l’épilogue).

Suite. Tracker #12607 item 5 livré ; il reste l’item 6 (fermeture de l’Epic #12373) — arbitrage coordinateur.

Épilogue — la boucle corrective #13778 mesure l’exercice ×4 de la paire SA

Cette synthèse refermait la question directrice de l’Epic #12373 ; l’EPIC #13778 (« MGS vs mealpy : convertir les benchmarks en correctifs MGS puis re-mesurer ») a depuis fait passer la paire SA — un des trois gains MGS (−4 à budget égal) — par la boucle corrective complète : baseline figée, diagnostic causal instrumenté, correctif atomique opt-in, re-mesure appariée. Ce que la boucle a mesuré (trace complète dans le commentaire livré sur l’issue ; correctif : MetaGeneticSharp#59) :

  • Instrument. Dispersion inter-individus et meilleur courant aux checkpoints (4 graines, G160/G640). Après la mort de la température (T_k = 0,95^k devient gloutonne vers G150), l’acceptation en marche latérale sur les plateaux entretient une dispersion qui atteint transitoirement 21,7 pour des gênes bornés [1, 10).
  • Hypothèse. Le pas σ = StepFraction·|aléatoire − courant| étant relatif à la population, cette dispersion se rétro-alimente ; coupler le pas au même schedule géométrique que la température (homothétie x’ = courant + α^(kγ)·(proposé − courant)) devrait raffiner la proposition quand l’acceptation amont se fige. Correctif StepCoolingExponent opt-in strict (null par défaut, voie historique inchangée), qui ne tire aucun aléa : chaque graine rejoue la même suite RNG que la baseline.
  • Verdict : NO IMPROVEMENT, monotone en γ. γ = 0,25 / 0,5 / 1 → médiane G160 figée à 32,0 / 36,5 / 38,5 (baseline : 27,0), progression G160→G640 nulle sur les 4 graines, dispersion finale 0,0000 : le couplage collapse la population et reproduit le gel du « walker » mealpy. La prémisse réfutée est la partie informative — la dérive latérale entretenue n’est pas un défaut : c’est la recherche parallèle.
  • L’exercice 2 de MGS-24, mesuré. MGS-SA passe de 27,0 (G160) à 13,0 (G640 ; médiane des graines 0/1/7/42 : 14, 11, 15, 12) quand mealpy passe de 31,0 (8 000 epochs) à 27,0 (32 000) : l’écart de −4 devient −14 à budget ×4. Il ne se referme pas — il s’inverse en faveur de MGS. La réponse causale à la question de l’exercice (« se referme-t-il ? ») est donc mesurée ; l’exercice reste ouvert aux étudiants.

Ce résultat renforce le diagnostic ci-dessus : l’écart SA est spécifique à l’algorithme, et l’hypothèse d’un défaut noyau remédiable par couplage pas-température est falsifiée — sur ce paysage, la dispersion entretenue est le mécanisme qui paie à budget étendu.

Exercice 1 — la médiane à la main

Le tableau ci-dessus donne la médiane des conflits de la paire 8 (GA) pour les deux moteurs. Depuis les quatre graines MGS récoltées dans ce notebook (variable paires), re-dérivez la médiane MGS de P8 sans utiliser Mediane2 : triez les quatre valeurs, moyennez les deux centrales, et comparez à la colonne du tableau.

Indice : une médiane de nombre pair d’observations n’est jamais une observation — c’est leur point milieu.

// EXERCICE 1 : médiane manuelle de la paire 8, côté MGS.
// Etape 1 : isoler les conflits des 4 graines du bras MGS de la paire 8 dans paires.
// Etape 2 : les trier, moyenner les deux centrales.
// Etape 3 : comparer au chiffre du tableau croisé et afficher l'écart.
Console.WriteLine("Exercice a completer");
Exercice a completer

Exercice 2 — l’étendue comme signal de variance

Deux moteurs peuvent partager une médiane proche et raconter des histoires très différentes si l’étendue (min–max) sur 4 graines est large. Repérez dans le tableau la paire dont l’étendue mealpy est la plus large, et celle dont l’étendue MGS est la plus large.

Indice : une étendue large avec une médiane basse signale un moteur qui gagne parfois grand et perd parfois grand — exactement le profil qu’un budget ×4 (exercices des paires) vient tester.

// EXERCICE 2 : étendue maximale de chaque côté.
// Etape 1 : parcourir paires, calculer max-min des conflits des bras canoniques.
// Etape 2 : identifier la paire à l'étendue mealpy max, puis MGS max.
// Etape 3 : afficher les deux paires et leurs étendues.
Console.WriteLine("Exercice a completer");
Exercice a completer

Exercice 3 — classer une forme d’écart

Supposez une dixième paire dont le témoin journalise les checkpoints 25/50/75/100 % = 40/31/31/31 côté MGS et 45/40/40/38 côté mealpy, avec le même budget. Classez son écart : précipitation précoce, stagnation tardive, ou coût par step ? Quel verdict qualité au 100 % ?

Indice : comparez la trajectoire — qui descend tôt, qui stagne — avant de regarder le point final.

// EXERCICE 3 : classification d'une trajectoire fictive.
// Etape 1 : écrire la règle : précoce = écart creusé avant 50% ; tardive = écart figé après 50%.
// Etape 2 : l'appliquer aux checkpoints donnés dans l'énoncé.
// Etape 3 : conclure en une phrase + verdict qualité 100%.
Console.WriteLine("Exercice a completer");
Exercice a completer
Retour au sommet