MGS-23 : DifferentialEvolution MGS contre mealpy — l’écart du PSO se reproduit-il ?

Navigation : << MGS-22 (PSO vs mealpy) | Index

Kernel : .NET (C#) — pont PythonNet vers mealpy dans la même exécution


Introduction

MGS-22 a mesuré, sur le Sudoku en représentation continue R1 et à budget d’évaluations égalisé, un écart net de qualité : mealpy OriginalPSO domine le composé MGS ParticleSwarmOptimization (28,5 contre 43,5 conflits médians), alors que la fitness C# isolée est 5,7× plus rapide et le coût par évaluation identique. L’hypothèse ouverte : cet écart est-il une propriété systématique des composés géométriques MGS, ou un accident du portage PSO ?

Ce notebook confronte la paire la plus propre pour trancher : Differential Evolution. Les deux implémentations suivent la même récurrence de base — mutation v = x_r1 + F·(x_r2 − x_r3), croisement binomial au taux CR, sélection gloutonne — donc un écart de convergence y est plus difficile à attribuer à une divergence de définition. Enfants de l’Epic #12373 (comparaison appariée MGS ↔︎ mealpy) : une paire, un notebook, une PR.


1. Le protocole apparié, pré-enregistré — hérité de MGS-22, non renégocié

Le protocole est celui de MGS-22 (#12302), repris intégralement pour que les paires de l’Epic soient comparables entre elles :

  • même substrat : grille Easy[0] de Sudoku_Easy51.txt, représentation R1 (continu + arrondi), fonction de coût = conflits totaux d’une grille pleine ;
  • budget d’évaluations égalisé — mesuré, pas supposé : les compteurs des deux moteurs sont rapportés tels quels ;
  • 4 graines nommées {0, 1, 7, 42}, médiane + min/max, jamais un run isolé ;
  • contre-vérification croisée du coût : le vainqueur mealpy est décodé et coûté côté C# — sans elle on compare deux fonctions de coût, pas deux moteurs ;
  • ms/éval séparé du temps total ;
  • graines passées explicitement aux deux moteurs : solve(prob, seed=N) côté mealpy (le paramètre du constructeur est silencieusement ignoré en 3.x), ResetSeed(N) côté MGS ;
  • déterminisme vérifié (répétition, conflits identiques exigés) avant de publier le moindre chiffre.

Paramètres par défaut de chaque bibliothèque, mesurés et déclarés (c’est le protocole MGS-22 : on compare les bibliothèques telles que leurs auteurs les livrent) :

moteur F (facteur d’échelle) CR (croisement)
MGS DifferentialEvolution 0,5 (DefaultScaleFactor) 0,9 (DefaultCrossoverRate)
mealpy OriginalDE 0,1 (wf) 0,9 (cr)

Le facteur F diffère par défaut (0,5 contre 0,1) : ce confond est déclaré ici, mesuré dans les sorties, puis neutralisé au §3 en alignant wf. La question « mécanique ou paramètre » reçoit ainsi un verdict causal apparié, séparé du classement aux paramètres par défaut.

// === MGS-23 : socle commun — DLLs MGS, grille de référence, fonction de coût ===
// Même socle que MGS-22 : 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.
public static string PuzzleLine23 = "902005403100063025508407060026309001057010290090670530240530600705200304080041950";

public static int[,] ParsePuzzle23()
{
    var g = new int[9, 9];
    for (int i = 0; i < 81; i++) g[i / 9, i % 9] = PuzzleLine23[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.
public static int CountConflicts23(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;
}

public static int CountEmpty23(int[,] p) { int n = 0; foreach (var v in p) if (v == 0) n++; return n; }

public static List<(int r, int c)> EmptyCells23(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.
public static int[,] DecodeR1_23(double[] genes)
{
    var Puzzle = ParsePuzzle23();
    var empties = EmptyCells23(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 Puzzle23 = ParsePuzzle23();
Console.WriteLine($"Grille de référence : {CountEmpty23(Puzzle23)} cellules vides, " +
                  $"{81 - CountEmpty23(Puzzle23)} indices fixes, {EmptyCells23(Puzzle23).Count} gènes R1.");
Grille de référence : 36 cellules vides, 45 indices fixes, 36 gènes R1.

Lecture. Le socle est posé, identique à MGS-22 au nom près — c’est voulu : la comparabilité de l’Epic #12373 tient à ce que chaque paire courre sur exactement le même substrat. 36 cellules vides = 36 gènes continus dans [1, 10), la fonction de coût compte les doublons ligne/colonne/bloc d’une grille pleine et vaut 0 ssi résolue.

// === Moteur MGS : chromosome R1, fitness instrumentée, composé DifferentialEvolution ===
// DE/rand/1/bin canonique de Storn & Price (1997), composé géométrique MGS :
// mutant v = x_r1 + F*(x_r2 - x_r3), croisement binomial CR, sélection gloutonne héritée.
public class SudokuR1Chromosome23 : ChromosomeBase
{
    private const double LO = 1.0, HI = 10.0;
    public SudokuR1Chromosome23() : base(EmptyCells23(ParsePuzzle23()).Count) { CreateGenes(); }
    public override Gene GenerateGene(int index)
        => new Gene(RandomizationProvider.Current.GetDouble(LO, HI));
    public override IChromosome CreateNew() => new SudokuR1Chromosome23();
    public double[] ToGenes() { var v = new double[Length]; for (int i = 0; i < Length; i++) v[i] = (double)GetGene(i).Value; return v; }
    public int[,] ToGrid() => DecodeR1_23(ToGenes());
}

// Fitness instrumentée : budget réel + trajectoire best-so-far aux quarts du budget.
public class SudokuR1Fitness23 : IFitness
{
    public static int Evals;
    public static int Best;
    public static int[] Checkpoints = new int[4];
    public static int[] Thresholds = new int[4];

    public static void Reset(int expectedEvals)
    {
        Evals = 0;
        Best = int.MaxValue;
        Checkpoints = new int[4];
        Thresholds = new[] { 1, 2, 3, 4 }
            .Select(q => (int)Math.Ceiling(expectedEvals * q / 4.0))
            .ToArray();
    }

    public double Evaluate(IChromosome chromosome)
    {
        int conflicts = CountConflicts23(((SudokuR1Chromosome23)chromosome).ToGrid());
        Evals++;
        Best = Math.Min(Best, conflicts);
        for (int i = 0; i < Thresholds.Length; i++)
            if (Checkpoints[i] == 0 && Evals >= Thresholds[i])
                Checkpoints[i] = Best;
        return -conflicts;
    }
}

public static class Mgs23Host
{
    private static IGeometricConverter DoubleConverter(
        bool clampBounds, Action<double> observeProposal)
    {
        var converter = new GeometricConverter<double>
        {
            IsOrdered = false,
            DoubleToGeneConverter = (geneIndex, value) =>
            {
                observeProposal(value);
                return clampBounds
                    ? Math.Max(1.0, Math.Min(Math.BitDecrement(10.0), value))
                    : value;
            },
            GeneToDoubleConverter = (geneIndex, value) => value
        };
        var typed = new TypedGeometricConverter();
        typed.SetTypedConverter(converter);
        return typed;
    }

    public static (int conflicts, int evals, double ms, double[] genes, int[] cp,
                   int convertedGenes, int outOfBounds) RunDe(
        int seed, int popSize, int maxGens, double? scaleFactor = null,
        bool monitorBounds = false, bool clampBounds = false)
    {
        FastRandomRandomization.ResetSeed(seed);
        int convertedGenes = 0;
        int outOfBounds = 0;
        Action<double> observeProposal = value =>
        {
            convertedGenes++;
            if (value < 1.0 || value >= 10.0) outOfBounds++;
        };
        var compound = scaleFactor.HasValue || monitorBounds || clampBounds
            ? new DifferentialEvolution
              {
                  MaxGenerations = maxGens,
                  GeometricConverter = DoubleConverter(clampBounds, observeProposal),
                  ScaleFactor = scaleFactor ?? DifferentialEvolution.DefaultScaleFactor,
                  CrossoverRate = DifferentialEvolution.DefaultCrossoverRate,
                  NoMutation = true
              }.Build()
            : MetaHeuristicsService.CreateMetaHeuristicByName(
                "DifferentialEvolution", maxGens, popSize);
        var adam = new SudokuR1Chromosome23();
        var pop = new MetaPopulation(popSize, popSize, adam);
        var ga = new MetaGeneticAlgorithm(
            pop, new SudokuR1Fitness23(),
            new EliteSelection(), new UniformCrossover(0.5f), new UniformMutation(true),
            compound);
        ga.Termination = new GenerationNumberTermination(maxGens);
        SudokuR1Fitness23.Reset(popSize * maxGens);
        var sw = Stopwatch.StartNew();
        ga.Start();
        sw.Stop();
        var best = (SudokuR1Chromosome23)ga.BestChromosome;
        return (CountConflicts23(best.ToGrid()), SudokuR1Fitness23.Evals,
                sw.Elapsed.TotalMilliseconds, best.ToGenes(),
                SudokuR1Fitness23.Checkpoints.ToArray(), convertedGenes, outOfBounds);
    }
}

var warmupMgs = Mgs23Host.RunDe(123, 50, 10);
var demoMgs = Mgs23Host.RunDe(7, 50, 160);
Console.WriteLine($"MGS DE (graine 7, témoin) : {demoMgs.conflicts} conflits, " +
                  $"{demoMgs.evals} évaluations, {demoMgs.ms:F0} ms, " +
                  $"cp25/50/75/100={string.Join("/", demoMgs.cp)}.");
MGS DE (graine 7, témoin) : 29 conflits, 8000 évaluations, 706 ms, cp25/50/75/100=42/29/29/29.

Lecture. Le composé MGS DifferentialEvolution (DE/rand/1/bin, F = 0,5, CR = 0,9 par défaut) est branché sur le même harnais que le PSO de MGS-22 : chromosome R1, fitness comptée, seeding avant création de population. La course témoin graine 7 produit 29 conflits pour 8 000 évaluations ; son temps reste dans l’output frais car il dépend de la machine. L’échauffement JIT la précède pour que la course mesurée ne paie pas la compilation.

// === Le pont PythonNet : mealpy dans le même kernel, la même exécution ===
// Recette validée MGS-22 (#12356) : pythonnet 3.1.0, DLL résolue par probe
// (PYTHONNET_PYDLL d'abord, sinon installs standards par OS — aucun chemin machine en dur).
#r "nuget: pythonnet,3.1.0"
using Python.Runtime;
static string ResolvePythonDll23()
{
    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 in new[] { @"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 in new[] {
                     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;
            }
    }
    throw new System.IO.FileNotFoundException(
        "DLL Python introuvable : definir PYTHONNET_PYDLL ou installer Python 3.10+ (mealpy requis).");
}
Runtime.PythonDLL = ResolvePythonDll23();
PythonEngine.Initialize();

// Le problème Python : decode + coût réimplémentés à l'identique, compteur d'évals,
// solveur mealpy OriginalDE avec seed EXPLICITE en solve() (API 3.x — le seed du
// constructeur est ignoré) et journal muet (log_to='nothing').
public static PyModule S23;
using (Py.GIL())
{
    S23 = Py.CreateScope();
    S23.Set("puzzle_line23", PuzzleLine23);
    S23.Exec(@"import sys
import mealpy
from mealpy.evolutionary_based.DE import OriginalDE
from mealpy import Problem, FloatVar
import json as _json

puzzle = [int(ch) for ch in puzzle_line23]
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 in range(9)]
    for k in range(len(empties)):
        r, c = empties[k]
        v = int(round(float(vec[k])))
        g[r][c] = max(1, min(9, v))
    return g

def cost(g):
    conflicts = 0
    for i in range(9):
        units = ([g[i][j] for j in range(9)],
                 [g[j][i] for j in range(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 conflicts

def cost_of_vector(vec):
    return cost(decode(vec))

PY_EVALS = [0]
PY_BEST = [10**9]
PY_CP = [[0, 0, 0, 0]]
PY_THRESHOLDS = [[0, 0, 0, 0]]

class SudokuProblem(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] += 1
        value = cost(decode(x))
        PY_BEST[0] = min(PY_BEST[0], value)
        for i, threshold in enumerate(PY_THRESHOLDS[0]):
            if PY_CP[0][i] == 0 and PY_EVALS[0] >= threshold:
                PY_CP[0][i] = PY_BEST[0]
        return float(value)

def run_mealpy_de(seed, pop_size, epoch, wf=None):
    import time
    expected = pop_size * (epoch + 1)
    PY_EVALS[0] = 0
    PY_BEST[0] = 10**9
    PY_CP[0] = [0, 0, 0, 0]
    PY_THRESHOLDS[0] = [(expected * q + 3) // 4 for q in (1, 2, 3, 4)]
    prob = SudokuProblem(bounds=FloatVar(lb=(1.0,) * len(empties), ub=(10.0,) * len(empties), name='genes'), minmax='min')
    if wf is None:
        model = OriginalDE(epoch=epoch, pop_size=pop_size)
    else:
        model = OriginalDE(epoch=epoch, pop_size=pop_size, wf=wf)
    t0 = time.perf_counter()
    g_best = model.solve(prob, seed=seed)
    dt = (time.perf_counter() - t0) * 1000.0
    sol = _json.dumps([float(v) for v in g_best.solution])
    return cost(decode(g_best.solution)), PY_EVALS[0], dt, sol, list(PY_CP[0])

def bench_mealpy_de(seeds_json, pop_size, epoch, reps=3, wf=None):
    out = []
    for sd in _json.loads(seeds_json):
        runs = [run_mealpy_de(sd, pop_size, epoch, wf) for _ in range(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.0
        out.append({'seed': sd, 'conflicts': cs[0],
                    'all_same': len(set(cs)) == 1 and all(r[4] == runs[0][4] for r in runs),
                    'evals': es[0], 'ms': med, 'sol': runs[0][3], 'cp': runs[0][4]})
    return _json.dumps(out)

def time_python_evals(vecs_json, reps=5):
    import time
    vecs = _json.loads(vecs_json)
    ts = []
    for _ in range(reps):
        t0 = time.perf_counter()
        for v in vecs:
            cost_of_vector(v)
        ts.append((time.perf_counter() - t0) * 1000.0)
    ts.sort()
    return ts[len(ts) // 2]

# Defaults mealpy OriginalDE (3.x) : mesures, pas doc
_m = OriginalDE(epoch=10, pop_size=5)
__defaults__ = f'mealpy OriginalDE defaults: wf={_m.wf}, cr={_m.cr}, strategy={_m.strategy}'
__mealpy_ver__ = 'mealpy ' + mealpy.__version__ + ' sur Python ' + sys.version.split()[0]");
    Console.WriteLine($"Pont PythonNet actif : {S23.Get<string>("__mealpy_ver__")}");
    Console.WriteLine($"Parametres defaut mealpy : {S23.Get<string>("__defaults__")}");
}

// --- Sanity check : la fonction de coût est-elle la MÊME des deux côtés ? ---
// 3 vecteurs témoins DÉTERMINISTES (LCG écrit à la main), décodés et costés des deux côtés.
public static double[] LcgVector23(int seed, int n)
{
    uint state = (uint)seed;
    var v = new double[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[] { LcgVector23(1, 51), LcgVector23(2, 51), LcgVector23(3, 51) };
using (Py.GIL())
{
    S23.Set("__witness_json__",
        System.Text.Json.JsonSerializer.Serialize(witnessVectors.Select(v => v.ToList()).ToList()));
    S23.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>>(S23.Get<string>("__py_costs__"));
    var csCosts = witnessVectors.Select(v => CountConflicts23(DecodeR1_23(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
Parametres defaut mealpy : mealpy OriginalDE defaults: wf=0.1, cr=0.9, strategy=0
Sanity check cout : C# 67,71,60 | Python 67,71,60 -> IDENTIQUE

Lecture. Le pont PythonNet est actif et la sanity check porte tout le bench : les trois vecteurs témoins LCG, décodés et coûtés indépendamment des deux côtés, donnent exactement les mêmes conflits (67, 71, 60 des deux côtés — IDENTIQUE). Sans cette égalité prouvée, une différence mesurée entre moteurs pourrait n’être qu’une différence entre les deux fonctions de coût. Les défauts mealpy (wf, cr, strategy) sont mesurés sur l’instance, pas recopiés de la doc — c’est la ligne « Parametres defaut mealpy » ci-dessus qui fait foi pour le confond F déclaré au §1.

// === Moteur mealpy : trajectoire instrumentée, course témoin, contre-vérification croisée ===
using (Py.GIL())
{
    // Le budget mealpy inclut l'évaluation initiale : pop × (epoch + 1) = 8 050.
    S23.Exec(@"_wu_c, _wu_e, _wu_t, _wu_sol, _wu_cp = run_mealpy_de(123, 50, 10)
__d_c__, __d_e__, __d_t__, __d_sol__, __d_cp__ = run_mealpy_de(7, 50, 160)");
    int dConflicts = S23.Get<int>("__d_c__");
    int dEvals = S23.Get<int>("__d_e__");
    double dMs = S23.Get<double>("__d_t__");
    var dCp = System.Text.Json.JsonSerializer.Deserialize<int[]>(
        S23.Eval("_json.dumps(__d_cp__)").ToString());
    Console.WriteLine($"mealpy OriginalDE (graine 7, témoin) : {dConflicts} conflits, " +
                      $"{dEvals} évaluations, {dMs:F0} ms, " +
                      $"cp25/50/75/100={string.Join("/", dCp)}.");

    var solJson = S23.Get<string>("__d_sol__");
    var genes = System.Text.Json.JsonSerializer.Deserialize<double[]>(solJson);
    int csRecheck = CountConflicts23(DecodeR1_23(genes));
    Console.WriteLine($"Contre-vérif croisée : coût C# du meilleur mealpy = {csRecheck} " +
                      $"(Python rapporte {dConflicts}) -> {(csRecheck == dConflicts ? "IDENTIQUE" : "DIFFERENT")}");
}
mealpy OriginalDE (graine 7, témoin) : 20 conflits, 8050 évaluations, 2605 ms, cp25/50/75/100=30/20/20/20.
Contre-vérif croisée : coût C# du meilleur mealpy = 20 (Python rapporte 20) -> IDENTIQUE

2. Le croisement — 2 moteurs × 4 graines à budget égal

Le plan est maintenant le même que MGS-22 : population 50, 160 générations (MGS) / 160 epochs (mealpy), graines {0, 1, 7, 42}, trois répétitions par graine côté MGS pour la médiane de temps (amendement anti-pic GC), déterminisme exigé partout.

// === LE BENCH : 2 moteurs x 4 graines {0,1,7,42}, population 50, 160 générations ===
public class BenchRow23
{
    public int seed { get; set; }
    public int conflicts { get; set; }
    public bool all_same { get; set; }
    public int evals { get; set; }
    public double ms { get; set; }
    public string sol { get; set; }
    public List<int> cp { get; set; }
}

int[] Seeds23 = { 0, 1, 7, 42 };
var mgsRows = new List<(int seed, int conflicts, int evals, double ms, bool allSame, int[] cp)>();
foreach (var sd in Seeds23)
{
    var runs3 = new List<(int c, int e, double t, int[] q)>();
    for (int rep = 0; rep < 3; rep++)
    {
        var r = Mgs23Host.RunDe(sd, 50, 160);
        runs3.Add((r.conflicts, r.evals, r.ms, r.cp));
    }
    var times = runs3.Select(x => x.t).OrderBy(t => t).ToList();
    mgsRows.Add((sd, runs3[0].c, runs3[0].e, times[1],
                 runs3.All(x => x.c == runs3[0].c && x.q.SequenceEqual(runs3[0].q)),
                 runs3[0].q));
}

string mealpyJson;
using (Py.GIL())
{
    S23.Set("__seeds_json__", System.Text.Json.JsonSerializer.Serialize(Seeds23.ToList()));
    S23.Exec(@"__bench_json__ = bench_mealpy_de(__seeds_json__, 50, 160)");
    mealpyJson = S23.Get<string>("__bench_json__");
}
var mealpyRows = System.Text.Json.JsonSerializer.Deserialize<List<BenchRow23>>(mealpyJson);

static double Median23(IEnumerable<int> xs)
{
    var s = xs.OrderBy(x => x).ToList();
    return (s.Count % 2 == 1) ? s[s.Count / 2] : (s[s.Count / 2 - 1] + s[s.Count / 2]) / 2.0;
}

static string Shape23(double[] mgs, double[] mealpy)
{
    var delta = Enumerable.Range(0, 4).Select(i => mgs[i] - mealpy[i]).ToArray();
    var signs = delta.Where(x => x != 0).Select(Math.Sign).Distinct().ToList();
    if (signs.Count > 1) return "croisement";
    var labels = new List<string>();
    double finalGap = Math.Abs(delta[3]);
    if (finalGap > 0 && Math.Abs(delta[0]) >= 0.75 * finalGap)
        labels.Add("précipitation précoce");
    double lateMgs = mgs[1] - mgs[3];
    double lateMealpy = mealpy[1] - mealpy[3];
    if (lateMealpy - lateMgs >= 2.0)
        labels.Add("stagnation tardive MGS");
    else if (lateMgs - lateMealpy >= 2.0)
        labels.Add("stagnation tardive mealpy");
    return labels.Count > 0 ? string.Join(" + ", labels) : "écart progressif/mixte";
}

Console.WriteLine($"{"moteur",-9} {"graine",6} {"conflits",9} {"evals",7} {"ms",8} {"ms/eval",8} {"cp25/50/75/100",-18}");
foreach (var r in mgsRows)
    Console.WriteLine($"{"MGS",-9} {r.seed,6} {r.conflicts,9} {r.evals,7} {r.ms,8:F0} {r.ms / r.evals,8:F3} {string.Join("/", r.cp),-18}");
foreach (var r in mealpyRows)
    Console.WriteLine($"{"mealpy",-9} {r.seed,6} {r.conflicts,9} {r.evals,7} {r.ms,8:F0} {r.ms / r.evals,8:F3} {string.Join("/", r.cp),-18}");

var mgsC = mgsRows.Select(r => r.conflicts).ToList();
var mpC = mealpyRows.Select(r => r.conflicts).ToList();
double mgsMsEval = mgsRows.Average(r => r.ms / r.evals);
double mpMsEval = mealpyRows.Average(r => r.ms / r.evals);
Console.WriteLine();
Console.WriteLine($"MGS    : médiane conflits {Median23(mgsC):F1} (min {mgsC.Min()}, max {mgsC.Max()}), ms/éval moyen {mgsMsEval:F3}");
Console.WriteLine($"mealpy : médiane conflits {Median23(mpC):F1} (min {mpC.Min()}, max {mpC.Max()}), ms/éval moyen {mpMsEval:F3}");
Console.WriteLine($"Rapport ms/éval mealpy/MGS : {mpMsEval / mgsMsEval:F2}x");

var mgsCpMedian = Enumerable.Range(0, 4).Select(k => Median23(mgsRows.Select(r => r.cp[k]))).ToArray();
var mpCpMedian = Enumerable.Range(0, 4).Select(k => Median23(mealpyRows.Select(r => r.cp[k]))).ToArray();
Console.WriteLine();
Console.WriteLine("Trajectoires best-so-far agrégées (médiane [min-max]) :");
for (int k = 0; k < 4; k++)
{
    var m = mgsRows.Select(r => r.cp[k]).ToList();
    var p = mealpyRows.Select(r => r.cp[k]).ToList();
    Console.WriteLine($"  cp{25 * (k + 1),3}% : MGS {mgsCpMedian[k]:F1} [{m.Min()}-{m.Max()}] | " +
                      $"mealpy {mpCpMedian[k]:F1} [{p.Min()}-{p.Max()}] | " +
                      $"delta MGS-mealpy {mgsCpMedian[k] - mpCpMedian[k]:+0.0;-0.0;0.0}");
}
Console.WriteLine($"Forme de l'écart qualité : {Shape23(mgsCpMedian, mpCpMedian)}.");
Console.WriteLine("Axe coût par step : " +
                  (mpMsEval < mgsMsEval
                      ? $"mealpy devant ({mpMsEval / mgsMsEval:F2}x le coût MGS)"
                      : $"MGS devant ({mgsMsEval / mpMsEval:F2}x le coût mealpy)"));
int deterministic = mgsRows.Count(r => r.allSame) + mealpyRows.Count(r => r.all_same);
Console.WriteLine($"Déterminisme : conflits ET checkpoints identiques sur les 3 répétitions pour {deterministic}/8 paires graine-moteur.");
moteur    graine  conflits   evals       ms  ms/eval cp25/50/75/100    
MGS            0        20    8000      570    0,071 37/20/20/20       
MGS            1        25    8000      625    0,078 41/28/25/25       
MGS            7        29    8000      710    0,089 42/29/29/29       
MGS           42        26    8000      929    0,116 33/26/26/26       
mealpy         0        27    8050     2410    0,299 31/27/27/27       
mealpy         1        23    8050     2739    0,340 31/23/23/23       
mealpy         7        20    8050     2068    0,257 30/20/20/20       
mealpy        42        20    8050     2376    0,295 24/20/20/20       

MGS    : médiane conflits 25,5 (min 20, max 29), ms/éval moyen 0,089
mealpy : médiane conflits 21,5 (min 20, max 27), ms/éval moyen 0,298
Rapport ms/éval mealpy/MGS : 3,36x

Trajectoires best-so-far agrégées (médiane [min-max]) :
  cp 25% : MGS 39,0 [33-42] | mealpy 30,5 [24-31] | delta MGS-mealpy +8,5
  cp 50% : MGS 27,0 [20-29] | mealpy 21,5 [20-27] | delta MGS-mealpy +5,5
  cp 75% : MGS 25,5 [20-29] | mealpy 21,5 [20-27] | delta MGS-mealpy +4,0
  cp100% : MGS 25,5 [20-29] | mealpy 21,5 [20-27] | delta MGS-mealpy +4,0
Forme de l'écart qualité : précipitation précoce.
Axe coût par step : MGS devant (0,30x le coût mealpy)
Déterminisme : conflits ET checkpoints identiques sur les 3 répétitions pour 8/8 paires graine-moteur.

Lecture des trajectoires : l’écart DE se forme tôt puis se réduit

Les quatre checkpoints localisent un mécanisme différent de celui observé sur le PSO de MGS-22 :

  • Qualité finale : mealpy atteint une médiane de 21,5 conflits contre 25,5 pour MGS, avec chevauchement des gammes — [20-27] contre [20-29].
  • Trajectoires médianes : l’écart MGS − mealpy est déjà de +8,5 conflits à 25 % du budget, puis se réduit à +5,5, +4,0 et +4,0. MGS rattrape donc une partie de son retard avant de rejoindre son plateau final.
  • Forme de l’écart : le classifieur dérivé des outputs conclut précipitation précoce. Contrairement à P1, le défaut n’est pas une stagnation MGS dès le premier quart : il est surtout acquis pendant l’initialisation et le début de la dynamique.
  • Déterminisme : conflits finaux et quatre checkpoints sont identiques sur les trois répétitions pour les huit couples graine-moteur.
  • Coût par étape : MGS reste devant sur ce run, mais les temps absolus restent dans l’output frais car ils dépendent du matériel.

Cette paire nuance donc le diagnostic transversal : le noyau MGS ne produit pas une seule forme d’écart. P1 montrait une stagnation tardive ; P2 montre un retard précoce partiellement rattrapé.

// === 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 K23 = 500;
var benchVecs = new List<double[]>();
for (int i = 0; i < K23; i++) benchVecs.Add(LcgVector23(42 + i, 51));

var csTimes = new List<double>();
for (int rep = 0; rep < 5; rep++)
{
    var swRep = Stopwatch.StartNew();
    foreach (var v in benchVecs) CountConflicts23(DecodeR1_23(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())
{
    S23.Set("__vecs_json__", System.Text.Json.JsonSerializer.Serialize(benchVecs.Select(v => v.ToList()).ToList()));
    S23.Exec(@"__py_ms__ = time_python_evals(__vecs_json__)");
    pyMs = S23.Get<double>("__py_ms__");
}

Console.WriteLine($"Fitness seule, {K23} vecteurs identiques (médiane de 5 répétitions par côté) :");
Console.WriteLine($"  C#     : {csMs:F1} ms total -> {csMs / K23:F3} ms/éval");
Console.WriteLine($"  Python : {pyMs:F1} ms total -> {pyMs / K23: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#     : 9,6 ms total -> 0,019 ms/éval
  Python : 63,5 ms total -> 0,127 ms/éval
  rapport Python/C# : 6,61x

Lecture du coût isolé et conclusion de la paire

La fitness isolée C# est nettement moins coûteuse que son équivalent Python sur les 500 mêmes vecteurs. Le bench complet conserve lui aussi un avantage de coût par évaluation pour MGS, mais dans une proportion plus faible : la mécanique du moteur absorbe une partie de l’avantage de la fitness.

Le verdict de P2 croise donc trois axes issus des outputs frais :

  1. qualité finale : mealpy devant en médiane, mais gammes chevauchantes ;
  2. forme temporelle : précipitation précoce, avec un delta qui décroît au fil des checkpoints ;
  3. coût par évaluation : MGS devant sur le run courant, sans figer les millisecondes machine-dépendantes dans la prose.

Le confond du facteur différentiel (F = 0,5 côté MGS contre wf = 0,1 par défaut côté mealpy) est neutralisé par l’expérience résolue du §3. Le benchmark principal conserve le point de vue utilisateur aux paramètres par défaut ; l’expérience suivante isole l’effet du paramétrage.


3. Expérience causale : neutraliser le confond du facteur différentiel

Le benchmark par défaut compare MGS avec F = 0,5 à mealpy avec wf = 0,1. Cette différence empêche d’attribuer le retard précoce au noyau plutôt qu’au paramétrage. L’expérience suivante ne change qu’un facteur : mealpy est relancé avec wf = 0,5, tandis que les graines {0, 1, 7, 42}, la population 50, le budget 160 epochs, la fonction de coût, la représentation R1 et les checkpoints restent strictement identiques.

Le critère est pré-enregistré avant la mesure :

  • CONFOND_SUPPORTED si l’écart final absolu est réduit d’au moins 50 % ;
  • CONFOND_PARTIAL s’il diminue sans atteindre 50 % ;
  • CONFOND_REJECTED s’il reste identique ou s’aggrave.

Ce verdict porte sur la cause de l’écart observé, pas sur un classement général des bibliothèques.

// === EXPÉRIENCE RÉSOLUE : mealpy OriginalDE avec wf=0,5 ===
// Une seule variable change par rapport au benchmark par défaut : wf 0,1 -> 0,5.
string alignedJson;
using (Py.GIL())
{
    S23.Set("__seeds_json__", System.Text.Json.JsonSerializer.Serialize(Seeds23.ToList()));
    S23.Exec(@"__bench_wf_json__ = bench_mealpy_de(__seeds_json__, 50, 160, wf=0.5)");
    alignedJson = S23.Get<string>("__bench_wf_json__");
}
var alignedRows = System.Text.Json.JsonSerializer.Deserialize<List<BenchRow23>>(alignedJson);
var alignedC = alignedRows.Select(r => r.conflicts).ToList();
var alignedCpMedian = Enumerable.Range(0, 4)
    .Select(k => Median23(alignedRows.Select(r => r.cp[k])))
    .ToArray();
double defaultFinalGap = Math.Abs(Median23(mgsC) - Median23(mpC));
double alignedFinalGap = Math.Abs(Median23(mgsC) - Median23(alignedC));
double gapReduction = defaultFinalGap == 0.0
    ? 0.0
    : (defaultFinalGap - alignedFinalGap) / defaultFinalGap;
string causalVerdict = alignedFinalGap < defaultFinalGap
    ? (gapReduction >= 0.5 ? "CONFOND_SUPPORTED" : "CONFOND_PARTIAL")
    : "CONFOND_REJECTED";

Console.WriteLine($"{"mealpy wf=0,5",-15} {"graine",6} {"conflits",9} {"evals",7} {"ms/eval",8} {"cp25/50/75/100",-18}");
foreach (var r in alignedRows)
    Console.WriteLine($"{"aligné",-15} {r.seed,6} {r.conflicts,9} {r.evals,7} {r.ms / r.evals,8:F3} {string.Join("/", r.cp),-18}");

Console.WriteLine();
Console.WriteLine($"mealpy wf=0,5 : médiane conflits {Median23(alignedC):F1} " +
                  $"(min {alignedC.Min()}, max {alignedC.Max()})");
Console.WriteLine("Trajectoire alignée agrégée (médiane [min-max]) :");
for (int k = 0; k < 4; k++)
{
    var values = alignedRows.Select(r => r.cp[k]).ToList();
    Console.WriteLine($"  cp{25 * (k + 1),3}% : {alignedCpMedian[k]:F1} " +
                      $"[{values.Min()}-{values.Max()}]");
}
Console.WriteLine($"Écart final absolu par défaut : {defaultFinalGap:F1}");
Console.WriteLine($"Écart final absolu après alignement : {alignedFinalGap:F1}");
Console.WriteLine($"Réduction relative de l'écart : {100.0 * gapReduction:F1} %");
Console.WriteLine($"Verdict causal pré-enregistré : {causalVerdict}");
Console.WriteLine($"Déterminisme aligné : {alignedRows.Count(r => r.all_same)}/4 graines " +
                  "avec conflits et checkpoints identiques sur les 3 répétitions.");
mealpy wf=0,5   graine  conflits   evals  ms/eval cp25/50/75/100    
aligné               0        43    8050    0,458 45/45/43/43       
aligné               1        42    8050    0,333 47/43/43/42       
aligné               7        43    8050    0,359 46/46/43/43       
aligné              42        33    8050    0,271 49/46/40/33       

mealpy wf=0,5 : médiane conflits 42,5 (min 33, max 43)
Trajectoire alignée agrégée (médiane [min-max]) :
  cp 25% : 46,5 [45-49]
  cp 50% : 45,5 [43-46]
  cp 75% : 43,0 [40-43]
  cp100% : 42,5 [33-43]
Écart final absolu par défaut : 4,0
Écart final absolu après alignement : 17,0
Réduction relative de l'écart : -325,0 %
Verdict causal pré-enregistré : CONFOND_REJECTED
Déterminisme aligné : 4/4 graines avec conflits et checkpoints identiques sur les 3 répétitions.

Lecture de l’expérience alignée : le confond est rejeté

Le verdict pré-enregistré est CONFOND_REJECTED. Avec wf = 0,5, mealpy passe de 21,5 à 42,5 conflits médians : l’écart final absolu avec MGS augmente de 4,0 à 17,0 conflits au lieu de diminuer. La dégradation est présente dès le premier checkpoint — médiane 46,5 conflits — et persiste jusqu’à la fin. Les quatre graines reproduisent exactement leurs conflits et checkpoints sur trois répétitions.

Le défaut mealpy wf = 0,1 n’était donc pas un handicap qui expliquait artificiellement son avance : sur cette représentation R1 quantifiée, il est au contraire une composante importante de sa trajectoire. Pour améliorer MGS, copier la valeur F = 0,5 vers mealpy est une piste réfutée ; la prochaine expérience doit tester une baisse contrôlée du facteur MGS, sans modifier simultanément sa réinsertion ou son croisement.


4. Correction candidate : abaisser le facteur MGS à mécanique constante

L’expérience précédente a réfuté l’alignement de mealpy vers wf = 0,5. Elle suggère le test symétrique et actionnable : abaisser le facteur MGS de F = 0,5 à F = 0,1, valeur performante observée côté mealpy, sans modifier simultanément un autre mécanisme.

Les invariants restent les mêmes : graines {0, 1, 7, 42}, population 50, 160 générations, représentation R1, fonction de coût, checkpoints 25/50/75/100 %, CR = 0,9, mutation externe supprimée et FitnessBasedElitistReinsertion. La construction directe de DifferentialEvolution ne sert qu’à injecter ScaleFactor; le chemin par défaut reste le témoin.

Le verdict correctif est pré-enregistré avant la mesure :

  • IMPROVES si la médiane finale baisse et si aucune graine ne se dégrade de plus de 2 conflits ;
  • NO IMPROVEMENT si la médiane ne baisse pas ;
  • TRADE-OFF si la médiane baisse mais qu’au moins une graine se dégrade de plus de 2 conflits.

Ce critère distingue un gain reproductible d’un simple déplacement de performance entre graines.

// === EXPÉRIENCE RÉSOLUE : MGS DifferentialEvolution avec F=0,1 ===
// Une seule variable change : ScaleFactor 0,5 -> 0,1. CR, mutation et réinsertion restent constants.
var reducedMgsRows = new List<(int seed, int conflicts, int evals, double ms, bool allSame, int[] cp)>();
foreach (var sd in Seeds23)
{
    var runs3 = new List<(int c, int e, double t, int[] q)>();
    for (int rep = 0; rep < 3; rep++)
    {
        var r = Mgs23Host.RunDe(sd, 50, 160, scaleFactor: 0.1);
        runs3.Add((r.conflicts, r.evals, r.ms, r.cp));
    }
    var times = runs3.Select(x => x.t).OrderBy(t => t).ToList();
    reducedMgsRows.Add((sd, runs3[0].c, runs3[0].e, times[1],
        runs3.All(x => x.c == runs3[0].c && x.q.SequenceEqual(runs3[0].q)),
        runs3[0].q));
}

var reducedMgsC = reducedMgsRows.Select(r => r.conflicts).ToList();
var reducedMgsCpMedian = Enumerable.Range(0, 4)
    .Select(k => Median23(reducedMgsRows.Select(r => r.cp[k])))
    .ToArray();
double defaultMgsMedian = Median23(mgsC);
double reducedMgsMedian = Median23(reducedMgsC);
var seedDegradations = reducedMgsRows
    .Join(mgsRows, r => r.seed, d => d.seed,
        (r, d) => (seed: r.seed, delta: r.conflicts - d.conflicts))
    .ToList();
int worstSeedDegradation = seedDegradations.Max(x => x.delta);
string correctionVerdict = reducedMgsMedian >= defaultMgsMedian
    ? "NO IMPROVEMENT"
    : (worstSeedDegradation > 2 ? "TRADE-OFF" : "IMPROVES");

Console.WriteLine($"{"MGS F=0,1",-11} {"graine",6} {"conflits",9} {"evals",7} {"ms/eval",8} {"cp25/50/75/100",-18}");
foreach (var r in reducedMgsRows)
    Console.WriteLine($"{"réduit",-11} {r.seed,6} {r.conflicts,9} {r.evals,7} {r.ms / r.evals,8:F3} {string.Join("/", r.cp),-18}");

Console.WriteLine();
Console.WriteLine($"MGS F=0,5 : médiane finale {defaultMgsMedian:F1} [{mgsC.Min()}-{mgsC.Max()}]");
Console.WriteLine($"MGS F=0,1 : médiane finale {reducedMgsMedian:F1} [{reducedMgsC.Min()}-{reducedMgsC.Max()}]");
Console.WriteLine("Trajectoire MGS F=0,1 agrégée (médiane [min-max]) :");
for (int k = 0; k < 4; k++)
{
    var values = reducedMgsRows.Select(r => r.cp[k]).ToList();
    Console.WriteLine($"  cp{25 * (k + 1),3}% : {reducedMgsCpMedian[k]:F1} [{values.Min()}-{values.Max()}]");
}
Console.WriteLine("Écarts finaux F=0,1 - F=0,5 par graine : " +
                  string.Join(", ", seedDegradations.Select(x => $"{x.seed}:{x.delta:+#;-#;0}")));
Console.WriteLine($"Pire dégradation par graine : {worstSeedDegradation:+#;-#;0} conflit(s)");
Console.WriteLine($"Verdict correctif pré-enregistré : {correctionVerdict}");
Console.WriteLine($"Déterminisme F=0,1 : {reducedMgsRows.Count(r => r.allSame)}/4 graines " +
                  "avec conflits et checkpoints identiques sur les 3 répétitions.");
MGS F=0,1   graine  conflits   evals  ms/eval cp25/50/75/100    
réduit           0        31    8000    0,055 31/31/31/31       
réduit           1        23    8000    0,040 28/23/23/23       
réduit           7        23    8000    0,050 24/23/23/23       
réduit          42        28    8000    0,050 28/28/28/28       

MGS F=0,5 : médiane finale 25,5 [20-29]
MGS F=0,1 : médiane finale 25,5 [23-31]
Trajectoire MGS F=0,1 agrégée (médiane [min-max]) :
  cp 25% : 28,0 [24-31]
  cp 50% : 25,5 [23-31]
  cp 75% : 25,5 [23-31]
  cp100% : 25,5 [23-31]
Écarts finaux F=0,1 - F=0,5 par graine : 0:+11, 1:-2, 7:-6, 42:+2
Pire dégradation par graine : +11 conflit(s)
Verdict correctif pré-enregistré : NO IMPROVEMENT
Déterminisme F=0,1 : 4/4 graines avec conflits et checkpoints identiques sur les 3 répétitions.

Lecture de la correction candidate : départ accéléré, plateau inchangé

Le verdict pré-enregistré est NO IMPROVEMENT. Abaisser MGS de F = 0,5 à F = 0,1 améliore nettement le premier checkpoint médian, de 39,0 à 28,0 conflits, mais ne change pas la médiane finale : 25,5 conflits dans les deux cas. La dynamique atteint son plateau plus tôt sans abaisser ce plateau.

Le résultat par graine interdit aussi de conclure à une correction robuste : les deltas finaux F=0,1 − F=0,5 sont +11, −2, −6, +2. La graine 7 gagne 6 conflits, tandis que la graine 0 en perd 11. Les quatre trajectoires réduites sont déterministes sur trois répétitions ; cette dispersion est donc un effet du paramètre sur le bassin exploré, pas du bruit d’exécution.

Conclusion actionnable pour MGS : remplacer globalement F = 0,5 par F = 0,1 n’est pas une correction acceptable. En revanche, le gain précoce suggère de tester ensuite un facteur adaptatif ou intermédiaire qui conserve l’accélération initiale sans figer prématurément la population, toujours à CR et réinsertion constants.


5. Diagnostic causal : les propositions DE sortent-elles du domaine R1 ?

Le chromosome R1 est initialisé dans l’intervalle semi-ouvert [1, 10), mais le convertisseur identité de MGS ne borne pas les propositions v = x_r1 + F·(x_r2 − x_r3). Le décodeur les ramène ensuite vers les chiffres 1 à 9 par arrondi et clamp. Cette étape peut donc concentrer plusieurs propositions continues différentes sur les mêmes valeurs de bord.

L’expérience mesure d’abord ce mécanisme sans modifier les propositions et exige la parité exacte avec le témoin : mêmes conflits et mêmes checkpoints pour chaque graine. Elle relance ensuite le même moteur avec un unique changement, le clamp du convertisseur vers [1, 10). F = 0,5, CR = 0,9, mutation, réinsertion, graines, population, budget, représentation, fonction de coût et checkpoints restent constants.

Le verdict est pré-enregistré avant la mesure :

  • IMPROVES si la médiane finale baisse, sans dégradation supérieure à 2 conflits sur une graine ;
  • NO IMPROVEMENT si la médiane ne baisse pas ;
  • TRADE-OFF si la médiane baisse mais qu’une graine se dégrade de plus de 2 conflits ;
  • INVALID MONITOR si l’observation identité ne reproduit pas exactement le témoin.

Un taux hors bornes élevé établit le mécanisme, mais seule l’amélioration appariée établit sa causalité sur la qualité.

// === EXPÉRIENCE RÉSOLUE : observation identité puis clamp du domaine [1, 10) ===
var boundsRows = new List<(int seed, int baseline, int monitored, int bounded,
    bool monitorParity, bool boundedSame, int proposals, int outOfBounds,
    int[] boundedCp)>();
foreach (var sd in Seeds23)
{
    var baseline = mgsRows.Single(r => r.seed == sd);
    var monitored = Mgs23Host.RunDe(sd, 50, 160, monitorBounds: true);
    var boundedRuns = new List<(int conflicts, int[] cp)>();
    for (int rep = 0; rep < 3; rep++)
    {
        var bounded = Mgs23Host.RunDe(sd, 50, 160, clampBounds: true);
        boundedRuns.Add((bounded.conflicts, bounded.cp));
    }
    boundsRows.Add((
        sd,
        baseline.conflicts,
        monitored.conflicts,
        boundedRuns[0].conflicts,
        monitored.conflicts == baseline.conflicts && monitored.cp.SequenceEqual(baseline.cp),
        boundedRuns.All(r => r.conflicts == boundedRuns[0].conflicts
            && r.cp.SequenceEqual(boundedRuns[0].cp)),
        monitored.convertedGenes,
        monitored.outOfBounds,
        boundedRuns[0].cp));
}

bool monitorParity = boundsRows.All(r => r.monitorParity);
var boundedC = boundsRows.Select(r => r.bounded).ToList();
double boundedMedian = Median23(boundedC);
var boundedDeltas = boundsRows.Select(r => (r.seed, delta: r.bounded - r.baseline)).ToList();
int boundedWorstDegradation = boundedDeltas.Max(x => x.delta);
long totalProposals = boundsRows.Sum(r => (long)r.proposals);
long totalOutOfBounds = boundsRows.Sum(r => (long)r.outOfBounds);
double outOfBoundsRate = 100.0 * totalOutOfBounds / totalProposals;
string boundsVerdict = !monitorParity
    ? "INVALID MONITOR"
    : boundedMedian >= defaultMgsMedian
        ? "NO IMPROVEMENT"
        : (boundedWorstDegradation > 2 ? "TRADE-OFF" : "IMPROVES");

Console.WriteLine($"{"borne",-10} {"graine",6} {"témoin",7} {"observé",8} {"clamp",6} " +
                  $"{"hors bornes",12} {"taux",8} {"cp clamp 25/50/75/100",-22}");
foreach (var r in boundsRows)
    Console.WriteLine($"{"[1,10)",-10} {r.seed,6} {r.baseline,7} {r.monitored,8} {r.bounded,6} " +
                      $"{r.outOfBounds,12} {100.0 * r.outOfBounds / r.proposals,7:F2}% " +
                      $"{string.Join("/", r.boundedCp),-22}");

Console.WriteLine();
Console.WriteLine($"Parité observation identité : {(monitorParity ? "4/4" : "ÉCHEC")}");
Console.WriteLine($"Propositions observées : {totalProposals}, hors [1,10) : {totalOutOfBounds} ({outOfBoundsRate:F2} %)");
Console.WriteLine($"MGS identité : médiane finale {defaultMgsMedian:F1} [{mgsC.Min()}-{mgsC.Max()}]");
Console.WriteLine($"MGS clampée : médiane finale {boundedMedian:F1} [{boundedC.Min()}-{boundedC.Max()}]");
Console.WriteLine("Écarts finaux clamp - identité par graine : " +
                  string.Join(", ", boundedDeltas.Select(x => $"{x.seed}:{x.delta:+#;-#;0}")));
Console.WriteLine($"Pire dégradation par graine : {boundedWorstDegradation:+#;-#;0} conflit(s)");
Console.WriteLine($"Verdict bornes pré-enregistré : {boundsVerdict}");
Console.WriteLine($"Déterminisme clamp : {boundsRows.Count(r => r.boundedSame)}/4 graines " +
                  "avec conflits et checkpoints identiques sur les 3 répétitions.");
borne      graine  témoin  observé  clamp  hors bornes     taux cp clamp 25/50/75/100 
[1,10)          0      20       20     28        66348   23,18% 38/28/28/28           
[1,10)          1      25       25     24        65975   23,05% 30/24/24/24           
[1,10)          7      29       29     30        77032   26,92% 39/30/30/30           
[1,10)         42      26       26     27        46712   16,32% 32/27/27/27           

Parité observation identité : 4/4
Propositions observées : 1144800, hors [1,10) : 256067 (22,37 %)
MGS identité : médiane finale 25,5 [20-29]
MGS clampée : médiane finale 27,5 [24-30]
Écarts finaux clamp - identité par graine : 0:+8, 1:-1, 7:+1, 42:+1
Pire dégradation par graine : +8 conflit(s)
Verdict bornes pré-enregistré : NO IMPROVEMENT
Déterminisme clamp : 4/4 graines avec conflits et checkpoints identiques sur les 3 répétitions.

Lecture du diagnostic de bornes : mécanisme réel, correction réfutée

L’observation est transparente : elle reproduit exactement les conflits et checkpoints du témoin pour les quatre graines. Elle mesure 256 067 propositions hors [1, 10) sur 1 144 800, soit 22,37 %. Les sorties de domaine sont donc un mécanisme fréquent de la dynamique MGS DE sur R1, pas un cas marginal.

Ce mécanisme n’explique toutefois pas l’écart de qualité par une absence de clamp. Borner les propositions dégrade la médiane finale de 25,5 à 27,5 conflits. Les écarts finaux clamp − identité sont +8, −1, +1, +1 : une seule graine gagne un conflit, tandis que la graine 0 en perd huit. Le verdict pré-enregistré est donc NO IMPROVEMENT, avec déterminisme complet sur trois répétitions.

Conclusion actionnable pour MGS : un clamp global du convertisseur DE ne doit pas être introduit comme correction par défaut. Les propositions hors domaine participent ici à l’exploration utile malgré le clamp tardif du décodeur. La prochaine hypothèse doit agir sur la dynamique sans supprimer brutalement ces excursions — par exemple une stratégie de réparation réfléchie ou un facteur adaptatif — puis être testée avec le même protocole apparié.


6. Correctif MGS #51 : calendrier linéaire du facteur différentiel

Les deux corrections candidates testées plus haut sont réfutées, mais leurs mesures se complètent : F = 0,1 (section 4) accélère l’entrée dans le plateau — checkpoint 25 % médian 39,0 → 28,0 — sans l’abaisser (médiane finale 25,5 dans les deux cas), et le clamp global des bornes (section 5) ne corrige rien. L’hypothèse restante : conserver l’accélération initiale de F = 0,1, puis retrouver l’exploration de F = 0,5. Le noyau ne pouvait pas l’exprimer — DifferentialEvolution.BuildMainHeuristic() capturait un ScaleFactor unique au build — d’où le correctif atomique MetaGeneticSharp#51 : une propriété optionnelle InitialScaleFactor ; quand elle est renseignée, F évolue linéairement de cette valeur vers ScaleFactor selon GenerationsNumber / MaxGenerations ; quand elle est nulle, la voie historique à F fixe est inchangée.

L’expérience appariée isole le calendrier, les deux jambes passant par la même construction directe au convertisseur double-identité :

  • témoin historique : ScaleFactor = 0,5, InitialScaleFactor nul — F fixe, voie de code inchangée ;
  • calendrier corrigé : InitialScaleFactor = 0,1 rejoignant linéairement ScaleFactor = 0,5 sur les 160 générations.

Les invariants sont inchangés : graines {0, 1, 7, 42}, population 50, 160 générations (8 000 évaluations), grille, fonction de coût, représentation R1, CR = 0,9, mutation externe supprimée (NoMutation = true), réinsertion FitnessBasedElitistReinsertion, checkpoints 25/50/75/100 %.

Le verdict est pré-enregistré avant la mesure :

  • INCONCLUSIVE si le témoin à F fixe ne reproduit pas la baseline de la section 2 — la paire ne mesurerait plus le calendrier seul, et la non-régression de la voie nulle du correctif ne serait pas établie ;
  • NO IMPROVEMENT si la médiane du calendrier ne baisse pas celle du témoin ;
  • TRADE-OFF si la médiane baisse mais qu’au moins une graine se dégrade de plus de 2 conflits ;
  • IMPROVES si la médiane baisse sans dégradation par graine au-delà de 2 conflits.
// === EXPÉRIENCE RÉSOLUE : correctif #51 — calendrier linéaire F 0,1 -> 0,5 contre F fixe 0,5 ===
// Une seule variable change entre les deux jambes : InitialScaleFactor nul (F fixe) vs 0,1 (calendrier).
public static class Mgs23P2dHost
{
    // Convertisseur double-identité, même forme que celui que la factory câble quand
    // l'appelant n'en fournit pas : aucune observation, aucun clamp, gènes = doubles nus.
    private static IGeometricConverter IdentityConverter()
    {
        var converter = new GeometricConverter<double>
        {
            IsOrdered = false,
            DoubleToGeneConverter = (geneIndex, value) => value,
            GeneToDoubleConverter = (geneIndex, value) => value
        };
        var typed = new TypedGeometricConverter();
        typed.SetTypedConverter(converter);
        return typed;
    }

    public static (int conflicts, int evals, double ms, int[] cp) RunDeScale(
        int seed, int popSize, int maxGens, double? initialScaleFactor)
    {
        FastRandomRandomization.ResetSeed(seed);
        var de = new DifferentialEvolution
        {
            MaxGenerations = maxGens,
            GeometricConverter = IdentityConverter(),
            ScaleFactor = DifferentialEvolution.DefaultScaleFactor,
            CrossoverRate = DifferentialEvolution.DefaultCrossoverRate,
            NoMutation = true
        };
        if (initialScaleFactor.HasValue) de.InitialScaleFactor = initialScaleFactor.Value;
        var compound = de.Build();
        var adam = new SudokuR1Chromosome23();
        var pop = new MetaPopulation(popSize, popSize, adam);
        var ga = new MetaGeneticAlgorithm(
            pop, new SudokuR1Fitness23(),
            new EliteSelection(), new UniformCrossover(0.5f), new UniformMutation(true),
            compound);
        ga.Termination = new GenerationNumberTermination(maxGens);
        SudokuR1Fitness23.Reset(popSize * maxGens);
        var sw = Stopwatch.StartNew();
        ga.Start();
        sw.Stop();
        var best = (SudokuR1Chromosome23)ga.BestChromosome;
        return (CountConflicts23(best.ToGrid()), SudokuR1Fitness23.Evals,
                sw.Elapsed.TotalMilliseconds, SudokuR1Fitness23.Checkpoints.ToArray());
    }
}

// F effectivement appliqué aux frontières de chaque quart, lu depuis l'API publique du composé.
Console.WriteLine("Calendrier F du correctif #51 (DifferentialEvolution.LinearScaleFactor, 160 générations) : " +
    string.Join(", ", new[] { 0, 40, 80, 120, 160 }.Select(g =>
        $"gén {g,3} : F = {DifferentialEvolution.LinearScaleFactor(0.1, DifferentialEvolution.DefaultScaleFactor, g, 160):F2}")));
Console.WriteLine();

List<(int seed, int conflicts, int evals, double ms, bool allSame, int[] cp)> RunP2dLeg(double? initialScaleFactor)
{
    var rows = new List<(int, int, int, double, bool, int[])>();
    foreach (var sd in Seeds23)
    {
        var runs3 = new List<(int c, int e, double t, int[] q)>();
        for (int rep = 0; rep < 3; rep++)
        {
            var r = Mgs23P2dHost.RunDeScale(sd, 50, 160, initialScaleFactor);
            runs3.Add((r.conflicts, r.evals, r.ms, r.cp));
        }
        var times = runs3.Select(x => x.t).OrderBy(t => t).ToList();
        rows.Add((sd, runs3[0].c, runs3[0].e, times[1],
            runs3.All(x => x.c == runs3[0].c && x.q.SequenceEqual(runs3[0].q)),
            runs3[0].q));
    }
    return rows;
}

var p2dControlRows = RunP2dLeg(null);   // jambe historique : F fixe 0,5 (InitialScaleFactor nul)
var p2dSchedRows = RunP2dLeg(0.1);      // jambe corrigée : F 0,1 -> 0,5 linéaire sur 160 générations

var p2dControlC = p2dControlRows.Select(r => r.conflicts).ToList();
var p2dSchedC = p2dSchedRows.Select(r => r.conflicts).ToList();
double p2dControlMedian = Median23(p2dControlC);
double p2dSchedMedian = Median23(p2dSchedC);
var p2dDeltas = p2dSchedRows
    .Join(p2dControlRows, s => s.seed, c => c.seed, (s, c) => (seed: s.seed, delta: s.conflicts - c.conflicts))
    .ToList();
int p2dWorstDegradation = p2dDeltas.Max(x => x.delta);
// Double contrôle : la jambe F fixe doit reproduire la baseline de la section 2 (parité de construction
// directe contre factory) — c'est aussi la preuve que #51 ne touche pas la voie nulle.
bool p2dBenchParity = p2dControlRows.All(r =>
{
    var b = mgsRows.Single(x => x.seed == r.seed);
    return r.conflicts == b.conflicts && r.cp.SequenceEqual(b.cp);
});
string p2dVerdict = !p2dBenchParity
    ? "INCONCLUSIVE"
    : p2dSchedMedian >= p2dControlMedian
        ? "NO IMPROVEMENT"
        : (p2dWorstDegradation > 2 ? "TRADE-OFF" : "IMPROVES");

Console.WriteLine($"{"jambe",-21} {"graine",6} {"conflits",9} {"evals",7} {"ms/eval",8} {"cp25/50/75/100",-18}");
foreach (var r in p2dControlRows)
    Console.WriteLine($"{"témoin F=0,5 fixe",-21} {r.seed,6} {r.conflicts,9} {r.evals,7} {r.ms / r.evals,8:F3} {string.Join("/", r.cp),-18}");
foreach (var r in p2dSchedRows)
    Console.WriteLine($"{"calendrier 0,1->0,5",-21} {r.seed,6} {r.conflicts,9} {r.evals,7} {r.ms / r.evals,8:F3} {string.Join("/", r.cp),-18}");

Console.WriteLine();
Console.WriteLine($"Témoin F fixe       : médiane finale {p2dControlMedian:F1} [{p2dControlC.Min()}-{p2dControlC.Max()}]");
Console.WriteLine($"Calendrier 0,1->0,5 : médiane finale {p2dSchedMedian:F1} [{p2dSchedC.Min()}-{p2dSchedC.Max()}]");
var p2dControlCpMedian = Enumerable.Range(0, 4).Select(k => Median23(p2dControlRows.Select(r => r.cp[k]))).ToArray();
var p2dSchedCpMedian = Enumerable.Range(0, 4).Select(k => Median23(p2dSchedRows.Select(r => r.cp[k]))).ToArray();
Console.WriteLine("Trajectoires médianes par checkpoint :");
for (int k = 0; k < 4; k++)
    Console.WriteLine($"  cp{25 * (k + 1),3}% : témoin {p2dControlCpMedian[k]:F1} -> calendrier {p2dSchedCpMedian[k]:F1} " +
                      $"(delta {p2dSchedCpMedian[k] - p2dControlCpMedian[k]:+0.0;-0.0;0.0})");
Console.WriteLine("Écarts finaux calendrier - témoin par graine : " +
                  string.Join(", ", p2dDeltas.Select(x => $"{x.seed}:{x.delta:+#;-#;0}")));
Console.WriteLine($"Pire dégradation par graine : {p2dWorstDegradation:+#;-#;0} conflit(s)");
Console.WriteLine($"Parité témoin F fixe vs baseline section 2 : {(p2dBenchParity ? "4/4" : "ÉCHEC")}");
Console.WriteLine($"Verdict du correctif pré-enregistré : {p2dVerdict}");
Console.WriteLine($"Déterminisme : témoin {p2dControlRows.Count(r => r.allSame)}/4, calendrier {p2dSchedRows.Count(r => r.allSame)}/4 " +
                  "graines avec conflits et checkpoints identiques sur les 3 répétitions.");
Calendrier F du correctif #51 (DifferentialEvolution.LinearScaleFactor, 160 générations) : gén   0 : F = 0,10, gén  40 : F = 0,20, gén  80 : F = 0,30, gén 120 : F = 0,40, gén 160 : F = 0,50

jambe                 graine  conflits   evals  ms/eval cp25/50/75/100    
témoin F=0,5 fixe          0        20    8000    0,029 37/20/20/20       
témoin F=0,5 fixe          1        25    8000    0,024 41/28/25/25       
témoin F=0,5 fixe          7        29    8000    0,023 42/29/29/29       
témoin F=0,5 fixe         42        26    8000    0,020 33/26/26/26       
calendrier 0,1->0,5        0        25    8000    0,025 30/25/25/25       
calendrier 0,1->0,5        1        32    8000    0,021 36/32/32/32       
calendrier 0,1->0,5        7        25    8000    0,020 29/25/25/25       
calendrier 0,1->0,5       42        24    8000    0,022 28/24/24/24       

Témoin F fixe       : médiane finale 25,5 [20-29]
Calendrier 0,1->0,5 : médiane finale 25,0 [24-32]
Trajectoires médianes par checkpoint :
  cp 25% : témoin 39,0 -> calendrier 29,5 (delta -9,5)
  cp 50% : témoin 27,0 -> calendrier 25,0 (delta -2,0)
  cp 75% : témoin 25,5 -> calendrier 25,0 (delta -0,5)
  cp100% : témoin 25,5 -> calendrier 25,0 (delta -0,5)
Écarts finaux calendrier - témoin par graine : 0:+5, 1:+7, 7:-4, 42:-2
Pire dégradation par graine : +7 conflit(s)
Parité témoin F fixe vs baseline section 2 : 4/4
Verdict du correctif pré-enregistré : TRADE-OFF
Déterminisme : témoin 4/4, calendrier 4/4 graines avec conflits et checkpoints identiques sur les 3 répétitions.

Lecture du correctif #51 : l’accélération initiale est réelle, la convergence finale ne suit pas

Le verdict pré-enregistré est TRADE-OFF. La jambe témoin (F = 0,5 fixe, le comportement historique quand InitialScaleFactor reste null) reproduit la baseline de la section 2 avec une parité 4/4 — médiane finale 25,5 conflits [20-29], graines 20/25/29/26 — de sorte que la paire mesure bien l’effet du calendrier seul. La jambe corrigée (F = 0,10 à la génération 0, montée linéaire 0,20 / 0,30 / 0,40 jusqu’à 0,50 à la génération 160, vérifiés sur l’API du correctif) atteint une médiane de 25,0 [24-32], avec 25/32/25/24 par graine.

Le calendrier fait ce qu’il promet en début de course : au checkpoint 25 % du budget, la trajectoire médiane passe de 39,0 à 29,5 conflits (delta −9,5) — l’accélération initiale y est mesurée et nette. Mais l’avantage s’évanouit ensuite : delta −2,0 au cp 50 %, −0,5 aux cp 75 % et 100 %. Et la redistribution par graine est trop forte pour être négligée : les écarts finaux sont 0:+5, 1:+7, 7:−4, 42:−2 — deux graines gagnent, deux perdent, avec une pire dégradation de +7 conflits, au-dessus du seuil de 2 du verdict pré-enregistré. Médiane plus basse mais dégradation concentrée : c’est la définition même d’un trade-off, pas d’une amélioration.

Trois contrôles clos l’expérience. Le déterminisme est total sur les deux jambes (4/4 graines, conflits et checkpoints identiques sur les trois répétitions). Les temps par évaluation restent dans l’output ; grandeurs machine-dépendantes, elles ne fondent pas le verdict. Et la null-path du correctif est vérifiée par construction : sans InitialScaleFactor, le F fixe historique reste intact, ce que la parité 4/4 de la jambe témoin confirme. Conclusion pratique : le calendrier linéaire n’est pas retenu comme défaut — un gain médian résiduel de 0,5 conflit au terme du budget ne justifie pas une redistribution inter-graines aussi marquée (écarts jusqu’à +7) ; l’hypothèse P2 est soldée sur cette paire, et le F fixe de 0,5 reste le réglage de référence.

Exercice 1 : cartographier la sensibilité locale à wf

L’expérience résolue n’observe que deux valeurs, 0,1 et 0,5. Complétez une petite courbe de sensibilité avec wf ∈ {0,3 ; 0,5 ; 0,7} en conservant une seule graine exploratoire, la population 50 et le budget 160. Comparez les conflits finaux et le checkpoint à 25 % pour repérer une zone robuste plutôt qu’un optimum ponctuel.

Indice : appelez run_mealpy_de(7, 50, 160, wf) pour chaque valeur, puis stockez les résultats avant de les afficher.

// EXERCICE 1 : sensibilité locale de mealpy OriginalDE à wf.
// TODO étudiant : compléter la liste des valeurs et agréger les résultats.
var wfValues = new[] { 0.3, 0.5, 0.7 };
var sensitivityRows = new List<(double wf, int conflicts, int cp25)>();

// Indice : dans un bloc using (Py.GIL()), appelez
// run_mealpy_de(7, 50, 160, wf) pour chaque valeur de wf,
// puis ajoutez (wf, conflits, checkpoint25) à sensitivityRows.

Console.WriteLine("Exercice a completer : comparer wf=0,3, 0,5 et 0,7.");
Exercice a completer : comparer wf=0,3, 0,5 et 0,7.

Exercice 2 : budget ×4 — l’écart de qualité se referme-t-il ?

MGS-22 posait la même question pour le PSO. Un écart qui se referme à budget accru dit « le moteur distillé converge plus lentement mais atteint le même plateau » ; un écart stable dit « plateau différent ».

// EXERCICE 2 : budget x4 (pop 50, 640 générations/epochs), 4 graines, deux côtés.
// Décommentez et exécutez :
// foreach (var sd in new[] {0, 1, 7, 42})
// {
//     var r = Mgs23Host.RunDe(sd, 50, 640);
//     Console.WriteLine($"MGS DE x4 (graine {sd}) : {r.Item1} conflits, {r.Item2} évaluations, {r.Item3:F0} ms.");
// }
// using (Py.GIL())
// {
//     S23.Set("__seeds_json__", System.Text.Json.JsonSerializer.Serialize(new[] {0, 1, 7, 42}.ToList()));
//     S23.Exec(@"__bench_x4_json__ = bench_mealpy_de(__seeds_json__, 50, 640)");
//     Console.WriteLine(S23.Get<string>("__bench_x4_json__"));
// }

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 ?

La cellule du coût par évaluation compare déjà le total decode+coût. Pour localiser la différence, séparez les deux étapes côté Python (dé coder une fois, coûter N fois) et comparez au profil C# équivalent.

// EXERCICE 3 : profil decode vs cost, 500 vecteurs, deux côtés.
// Décommentez et exécutez (adapté de MGS-22 exercice 3) :
// var decSw = Stopwatch.StartNew();
// foreach (var v in benchVecs) DecodeR1_23(v);
// decSw.Stop();
// Console.WriteLine($"C# decode seul : {decSw.Elapsed.TotalMilliseconds / K23:F3} ms/vec " +
//     $"(reste = coût : {(csMs - decSw.Elapsed.TotalMilliseconds) / K23:F3} ms/vec)");
// using (Py.GIL())
// {
//     S23.Set("__vecs_json__", System.Text.Json.JsonSerializer.Serialize(benchVecs.Select(v => v.ToList()).ToList()));
//     S23.Exec(@"import time
// _vecs = _json.loads(__vecs_json__)
// _t0 = time.perf_counter()
// _grids = [decode(v) for v in _vecs]
// _t1 = time.perf_counter()
// for g in _grids: cost(g)
// _t2 = time.perf_counter()
// print(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("Exercice a completer (decommentez le bloc ci-dessus).");
Exercice a completer (decommentez le bloc ci-dessus).
Retour au sommet