Apport verifie : protocole commun applique a trois terrains structurants (Sudoku, Puissance 4, Wordle), avec metriques uniformes, timeouts, execution parallele, checkpoints CSV et visualisations.
Reproduction WSL/Linux du 2026-08-24 : uv run pytest -q -> 20 tests passes en 21,11 s.
Limite assumee : sous Windows natif, l’orchestrateur emploie les API Unix resource et SIGALRM ; la collecte echoue. Cette limite reste visible et motive la section Linux/WSL obligatoire.
Ce que ce notebook n’est pas : un copier-coller du depot etudiant. Le geste conserve est le protocole commun et la triangulation cross-terrains ; le depot de Theodore contient les details d’implementation et la documentation utilisateur (voir sa PR).
Pareto frontieres et cas discriminant (le gagnant depend de l’instance)
tranche 3/N
4
Ponts series : Search, Sudoku, GameTheory, App-7-Wordle, App-14b-ConnectFour
tranche 4/N
Les tranches 2 a 4 dependent du terrain de Theodore et arriveront dans des PR suivantes de la lane myia-po-2024:CoursIA-2. Cette premiere tranche etablit le cadre et l’attribution.
Cadre theorique : la selection empirique d’algorithmes
Trois pierres angulaires a poser avant d’attaquer la selection :
1. Le probleme de Rice (1976)
John R. Rice, The Algorithm Selection Problem, Advances in Computers vol. 15 (1976). La formalisation : on se donne un ensemble \(I\) d’instances, un ensemble \(A\) d’algorithmes, une fonction de performance \(p : I \times A \to \mathbb{R}\) (a minimiser). On cherche une fonction de selection \(s : I \to A\) qui minimise \(\sum_{i \in I} p(i, s(i))\).
La difficulte : \(p\) n’est pas connue analytiquement. On observe \(p\) sur un echantillon d’instances et on apprend \(s\). Trois ingredients :
instance features : un vecteur \(f(i) \in \mathbb{R}^k\) qui caracterise l’instance ;
algorithme portfolio : l’ensemble \(A\) des candidats ;
cout de selection : le cout (en temps, en memoire ou en evaluations) pour choisir \(s\).
La selection n’est utile que si \(s\) est beaucoup moins cher que d’executer tous les algorithmes. Si tous les \(a \in A\) sont triviaux, selectionner ne sert a rien.
2. No Free Lunch (Wolpert 1996)
David H. Wolpert, The Lack of A Priori Distinctions Between Learning Algorithms, Neural Computation vol. 8 no. 7 (1996). Le resultat central : sur l’ensemble de toutes les distributions possibles, la performance moyenne de tout algorithme est la meme que celle d’un randomiseur uniforme. Donc aucune superiority intrinseque d’un algorithme sur un autre.
Ce que cela dit vraiment : si l’on restreint la distribution (par exemple, les instances de Sudoku, ou les grilles Wordle de 5 lettres), les differences emergent. La selection n’est donc legitime que sur une distribution d’instances bien definie. Sortir de la distribution = sortir du domaine ou la selection a ete calibree.
3. Empirical algorithmics (Smith-Miles 2009)
Kate A. Smith-Miles, Cross-Disciplinary Perspectives on Meta-Learning for Algorithm Selection, ACM Computing Surveys vol. 41 no. 1 (2009). Le cadre met en lumiere trois pieges recurrents :
biais de representation : mesurer uniquement sur des instances faciles ou uniquement sur des instances dures ;
biais de metrique : confondre temps CPU, temps wallclock, nombre de noeuds explores, qualite de solution ; la Pareto frontiere expose la structure ;
biais de budget : dire “mieux” sans fixer le budget ; avec un budget infini, beaucoup d’algorithmes triviaux deviennent gagnants.
Le geste empirique qui repond a ces pieges : une seule table de sortie, plusieurs metriques, memes budgets, meme horizon, meme grain de randomisation, meme representation du substrat.
Ce que la suite couvrira
Les sections 2 a 4 (tranches suivantes) appliqueront ce cadre au protocole de Theodore sur les trois terrains. Elles montreront notamment :
une meme instance de Sudoku traitee par 6 solveurs (DLX, CP-SAT, SMT, GA, MCTS, recuit simule) avec un meme budget et une meme metrique ;
une meme position de Puissance 4 traitee par 4 solveurs avec alpha-beta, MCTS et negamax ;
une meme grille Wordle traitee par 3 solveurs informationnels.
Le resultat attendu n’est pas un classement unique mais une carte (probleme, representation, algorithme) -> (temps, memoire, qualite) ou chaque algorithme est gagnant dans une zone.
# Setup env : verifier la disponibilite des briques utilisees dans les tranches suivantes.# Cette cellule execute sans dependre du depot etudiant de Theodore : elle valide simplement# que l'ecosysteme Python du notebook (kernel `coursia-ml-training`) tient les invariants# promis par les Sections A et B du depot source.import sysimport platformimport importlibprint(f"Python : {sys.version}")print(f"Plateforme : {platform.system()}{platform.release()} ({platform.machine()})")# Inventaire des bibliotheques qui seront sollicitees dans les tranches 2 a 4.expected_modules = {'numpy': 'tableaux numeriques','pandas': 'tables de resultats / CSV','matplotlib': 'visualisations','scipy': 'solveurs numeriques (recuit simule, etc.)','sklearn': 'selection algo / portfolio / features','ortools': 'CP-SAT (Sudoku, Puissance 4)','pysat': 'SAT/SMT (Sudoku encode SAT)','z3': 'SMT (Sudoku encode SMT)',}present = {}missing = []for mod, role in expected_modules.items():try: m = importlib.import_module(mod) ver =getattr(m, '__version__', '?') present[mod] = verexceptExceptionas e: missing.append((mod, role, type(e).__name__))print("\n--- Modules presents ---")for mod, ver in present.items():print(f" {mod:12s}{ver:>12s} ({expected_modules[mod]})")if missing:print("\n--- Modules manquants ---")for mod, role, exc in missing:print(f" {mod:12s} INDISPONIBLE ({role}) -- {exc}")else:print("\n--- Aucun module manquant ---")
temps CPU / wallclock : budgets homogenes par solveur et par difficulte.
memoire : pic resident set size.
noeuds explores : comparables entre paradigmes uniquement si le paradigme expose cette metrique (les algorithmes genetiques et les solveurs CP-SAT l’exposent ; MCTS aussi ; recuit simule et Wordle informationnels non).
qualite de solution : profondeur de recherche, optimalite, validite.
Les resultats sont consignes en CSV unifie par Theodore dans son depot ; cette tranche reproduit le cadre Python (imports, structure de donnees, helpers) sans executer les benchmarks reels ceux-ci relevant de la tranche 3 (Pareto + cas discriminant) avec acces au depot de Theodore.
# Imports du protocole commun.import csvimport osimport subprocessimport timefrom collections import defaultdictfrom dataclasses import dataclass, fieldfrom pathlib import Pathfrom typing import Callable, Iterableimport numpy as npimport pandas as pd# Verifier la disponibilite des solveurs (les tranches 3 executeront les benchmarks reels ;# cette cellule valide que l'ecosysteme Python tient les briques promises).solver_libs = {'ortools': 'CP-SAT (Sudoku encode CP)','pysat': 'SAT (Sudoku encode SAT)','z3': 'SMT (Sudoku encode SMT)',}present = []missing = []for mod, role in solver_libs.items():try:__import__(mod) present.append((mod, role))exceptExceptionas e: missing.append((mod, role, type(e).__name__))print('Solveurs CP/SAT/SMT disponibles :')for mod, role in present:print(f' [OK] {mod:8s} -- {role}')for mod, role, exc in missing:print(f' [FAIL] {mod:8s} -- {role} ({exc})')print()print('Note : Sudoku/Puissance 4/Wordle terrain specifiques (DLX, MCTS, recuit simule) non')print('importables en Python natif -- les tranches 3 appellent les solveurs via subprocess sur le')print('depot de Theodore (fork jsboigeEpita/2026-Epita-Intelligence-Symbolique PR#42).')
Solveurs CP/SAT/SMT disponibles :
[OK] ortools -- CP-SAT (Sudoku encode CP)
[OK] pysat -- SAT (Sudoku encode SAT)
[OK] z3 -- SMT (Sudoku encode SMT)
Note : Sudoku/Puissance 4/Wordle terrain specifiques (DLX, MCTS, recuit simule) non
importables en Python natif -- les tranches 3 appellent les solveurs via subprocess sur le
depot de Theodore (fork jsboigeEpita/2026-Epita-Intelligence-Symbolique PR#42).
# Structure de donnees unifiee pour les benchmarks.@dataclassclass TerrainResult:"""Une observation (solveur x instance x graine) selon le protocole de Theodore.""" terrain: str solveur: str instance_id: str difficulte: str graine: int temps_cpu_s: float temps_wallclock_s: float memoire_rss_mb: float noeuds_explores: int|None qualite: str profondeur: int|None timestamp: str= field(default_factory=lambda: time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()))@dataclassclass TerrainConfig:"""Configuration d'un terrain (un triplet (terrain, difficulte, solveur)).""" terrain: str difficulte: str solveur: str timeout_s: int=30 budget_evaluations: int|None=None# Registre canonique des triplets (terrain, solveur) du protocole Theodore.PROTOCOL = {'Sudoku': [ ('DLX', 'Exact, dancing links, pas de budget.'), ('CP-SAT or-tools', 'Exact, modele CP-SAT, budget implicite = duree.'), ('SMT Z3', 'Exact, modele SMT, budget implicite = duree.'), ('GA deap', 'Approche, population 60, crossover swap, mutation cellule.'), ('MCTS', 'Approche, simulations aleatoires bornees.'), ('Recuit simule', 'Approche, temperature exponentielle, voisin = swap.'), ],'Puissance4': [ ('Alpha-beta', 'Exact, profondeur iterative deepening.'), ('Negamax', 'Exact, symetrie miroir.'), ('MCTS UCT', 'Approche, UCB1 selection.'), ('Recherche exo.', 'Exact bornee par timeout.'), ],'Wordle': [ ('Entropy max', 'Informationnel, maximise le gain d information par essai.'), ('Expectation max', 'Informationnel, maximise l esperance de reduction du candidat.'), ('Baseline freq.', 'Informationnel, propose les lettres par frequence anglaise.'), ],}print('Protocole canonique (3 terrains) :')for terrain, solveurs in PROTOCOL.items():print(f' {terrain} ({len(solveurs)} solveurs)')for solveur, descr in solveurs:print(f' - {solveur:18s}{descr}')total =sum(len(s) for s in PROTOCOL.values())print(f'Total : {total} solveurs sur 3 terrains (cible Theodore).')
Protocole canonique (3 terrains) :
Sudoku (6 solveurs)
- DLX Exact, dancing links, pas de budget.
- CP-SAT or-tools Exact, modele CP-SAT, budget implicite = duree.
- SMT Z3 Exact, modele SMT, budget implicite = duree.
- GA deap Approche, population 60, crossover swap, mutation cellule.
- MCTS Approche, simulations aleatoires bornees.
- Recuit simule Approche, temperature exponentielle, voisin = swap.
Puissance4 (4 solveurs)
- Alpha-beta Exact, profondeur iterative deepening.
- Negamax Exact, symetrie miroir.
- MCTS UCT Approche, UCB1 selection.
- Recherche exo. Exact bornee par timeout.
Wordle (3 solveurs)
- Entropy max Informationnel, maximise le gain d information par essai.
- Expectation max Informationnel, maximise l esperance de reduction du candidat.
- Baseline freq. Informationnel, propose les lettres par frequence anglaise.
Total : 13 solveurs sur 3 terrains (cible Theodore).
# Helpers du protocole commun (timer, chargeur CSV, metriques homogenes).class ProtocolTimer:"""Contexte-mesure : CPU + wallclock + RSS a la sortie."""def__init__(self):self.cpu_s =0.0self.wallclock_s =0.0self.rss_mb =0.0def__enter__(self):self._t_cpu_start = time.process_time()self._t_wall_start = time.perf_counter()returnselfdef__exit__(self, *exc):self.cpu_s = time.process_time() -self._t_cpu_startself.wallclock_s = time.perf_counter() -self._t_wall_starttry:import resourceself.rss_mb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss /1024.0exceptImportError:self.rss_mb =float('nan')returnFalsedef write_csv_checkpoint(path, rows): rows =list(rows)ifnot rows:return fieldnames =list(rows[0].keys()) file_exists = Path(path).exists()withopen(path, 'a', newline='', encoding='utf-8') as f: w = csv.DictWriter(f, fieldnames=fieldnames)ifnot file_exists: w.writeheader() w.writerows(rows)def charger_csv(path):return pd.read_csv(path, encoding='utf-8')def resume_5graines(observations): by_key = defaultdict(list)for obs in observations: by_key[(obs.terrain, obs.solveur, obs.difficulte)].append(obs) out = {}for k, obs_list in by_key.items(): cpu = np.array([o.temps_cpu_s for o in obs_list]) wall = np.array([o.temps_wallclock_s for o in obs_list]) rss = np.array([o.memoire_rss_mb for o in obs_list]) out[k] = {'n_graines': len(obs_list),'cpu_moy_s': float(np.mean(cpu)),'cpu_et_s': float(np.std(cpu, ddof=1)) iflen(cpu) >1else0.0,'wall_moy_s': float(np.mean(wall)),'wall_et_s': float(np.std(wall, ddof=1)) iflen(wall) >1else0.0,'rss_moy_mb': float(np.nanmean(rss)) if np.any(~np.isnan(rss)) elsefloat('nan'),'succes_rate': float(np.mean([1.0if o.qualite =='OPTIMAL'else0.0for o in obs_list])), }return out# Demonstration sur donnees factices (5 graines, 1 solveur, 1 terrain) pour valider les helpers.demo = []for graine in [7, 42, 99, 123, 777]:with ProtocolTimer() as t: time.sleep(0.01) demo.append(TerrainResult( terrain='Sudoku', solveur='CP-SAT or-tools', instance_id='easy-001', difficulte='Easy', graine=graine, temps_cpu_s=t.cpu_s, temps_wallclock_s=t.wallclock_s, memoire_rss_mb=t.rss_mb, noeuds_explores=None, qualite='OPTIMAL', profondeur=None, ))resume = resume_5graines(demo)for (terrain, solveur, difficulte), stats in resume.items(): cpu_moy = stats['cpu_moy_s'] cpu_et = stats['cpu_et_s'] wall_moy = stats['wall_moy_s'] wall_et = stats['wall_et_s'] rss = stats['rss_moy_mb'] succes = stats['succes_rate'] *100 rss_str =f'{rss:.2f}'ifnot np.isnan(rss) else'n/a (Windows)'print(f'{terrain:10s}{solveur:18s}{difficulte:8s} cpu={cpu_moy:.4f} +/- {cpu_et:.4f} s, wall={wall_moy:.4f} +/- {wall_et:.4f} s, rss={rss_str} MB, succes={succes:.0f}%')
Écrivez une fonction qui groupe des TerrainConfig par (terrain, difficulté) et signale les groupes où les solveurs ne partagent pas le même timeout_s ou le même budget d’évaluations.
Étape 1 : construisez les groupes de configurations comparables.
Étape 2 : retournez, pour chaque groupe inéquitable, les budgets observés.
Indice : traitez explicitement budget_evaluations=None, qui désigne un budget non renseigné et non un budget nul.
# TODO étudiant : valider l'équité d'un protocole multi-solveurs.def verifier_equite(configurations):# Étape 1 : comparer les budgets par terrain et difficulté.# Étape 2 : signaler les groupes dont les solveurs n'ont pas le même timeout.# Indice : regroupez les configurations par (terrain, difficulte).returnNonerapport_equite = verifier_equite([])print("Exercice à compléter : audit d'équité", rapport_equite)
Exercice à compléter : audit d'équité None
3. Frontiere de Pareto multi-critere
Un podium unique (le solveur le plus rapide) cache presque toujours un compromis interessant. La lecture par frontiere de Pareto montre, pour les 13 solveurs du PROTOCOL, le compromis entre deux axes complementaires :
axe 1 : wall_moy_s (wallclock moyen sur 5 graines, log-echelle).
axe 2 : succes_rate (proportion d instances resolues optimalement).
Un solveur p est domine par un autre solveur q si q est au moins aussi bon sur les deux axes (wall_moy_s(q) <= wall_moy_s(p) ET succes_rate(q) >= succes_rate(p)) et strictement meilleur sur au moins un des deux. La frontiere de Pareto est l ensemble des solveurs non domines : il n y a pas un meilleur, il y a un front qui dit au lecteur selon la priorite (rapide vs fiable), choisissez votre point. (Definition standard de dominance faible.)
Important : la frontiere est calculee sur les mesures de la section 2, pas sur des benchmarks frais. La tranche 4 executera les benchmarks reels (subprocess sur le depot de Theodore) ; cette tranche montre la methode sur 13 points synthetiques representant le profil du PROTOCOL.
# Calcul de la frontiere de Pareto sur le profil synthetique des 13 solveurs du PROTOCOL.import numpy as np# Profil synthetique : chaque solveur a un (wall_moy_s, succes_rate) tire de la litterature# (Theodore source) + estimations pour les solveurs non chiffres (MCTS, recuit simule,# informationnels Wordle). Wall en log-echelle, succes en [0, 1].PROFIL_SYNTHETIQUE = [# (terrain, solveur, wall_moy_s, succes_rate) ("Sudoku", "DLX", 0.10, 1.00), ("Sudoku", "CP-SAT or-tools", 0.25, 1.00), ("Sudoku", "SMT Z3", 0.40, 1.00), ("Sudoku", "GA deap", 8.00, 0.92), ("Sudoku", "MCTS", 6.00, 0.85), ("Sudoku", "Recuit simule", 10.00, 0.78), ("Puissance4", "Alpha-beta", 0.50, 1.00), ("Puissance4", "Negamax", 0.40, 1.00), ("Puissance4", "MCTS UCT", 3.00, 0.96), ("Puissance4", "Recherche exo.", 15.00, 1.00), ("Wordle", "Entropy max", 0.30, 0.95), ("Wordle", "Expectation max", 0.35, 0.93), ("Wordle", "Baseline freq.", 0.05, 0.65),]print(f"Profil synthetique : {len(PROFIL_SYNTHETIQUE)} solveurs charges.")def frontiere_pareto(points):'''Renvoie l ensemble des indices non-domines. Un point p domine q si p.wall <= q.wall ET p.succes >= q.succes, avec au moins une inegalite stricte. ''' n =len(points) non_domines = []for i inrange(n): _, _, wi, si = points[i] domine =Falsefor j inrange(n):if i == j:continue _, _, wj, sj = points[j]if (wj <= wi and sj >= si) and (wj < wi or sj > si): domine =Truebreakifnot domine: non_domines.append(i)return non_dominesfront = frontiere_pareto(PROFIL_SYNTHETIQUE)print(f"Frontiere de Pareto : {len(front)}/{len(PROFIL_SYNTHETIQUE)} solveurs non domines.")print()for idx in front: terrain, solveur, wall, succes = PROFIL_SYNTHETIQUE[idx]print(f" [PARETO] {terrain:10s}{solveur:18s} wall={wall:6.2f}s succes={succes*100:5.1f}%")domines_idx = [i for i inrange(len(PROFIL_SYNTHETIQUE)) if i notin front]if domines_idx:print()print(f"Solveurs domines (a ne PAS choisir en lecture Pareto) :")for idx in domines_idx: terrain, solveur, wall, succes = PROFIL_SYNTHETIQUE[idx]print(f" [DOMINE] {terrain:10s}{solveur:18s} wall={wall:6.2f}s succes={succes*100:5.1f}%")
Exercice 2 : étendre la frontière de Pareto à la mémoire
Le calcul précédent utilise le temps et le taux de succès. Ajoutez un troisième axe memoire_rss_mb à minimiser, puis identifiez les solveurs non dominés selon les trois critères.
Étape 1 : définissez la règle de dominance en trois dimensions.
Étape 2 : vérifiez qu’une égalité sur deux axes n’empêche pas une domination stricte sur le troisième.
Indice : commencez par un petit profil synthétique de quatre solveurs dont vous connaissez la frontière attendue.
# TODO étudiant : généraliser la dominance à trois critères.def frontiere_pareto_3d(points):# Étape 1 : ajouter la mémoire comme troisième objectif à minimiser.# Étape 2 : conserver uniquement les points non dominés.# Indice : une domination exige au moins une inégalité stricte.returnNonefront_3d = frontiere_pareto_3d([])print("Exercice à compléter : frontière Pareto 3D", front_3d)
Exercice à compléter : frontière Pareto 3D None
3.1 Cas discriminant : le gagnant change selon la famille d instances
Un cas discriminant empirique est un cas ou la solution optimale change selon la lecture :
Budget serre ET exigence de fiabilite maximale (>= 99%) : Negamax (Puissance4) et CP-SAT (Sudoku) sont les seuls a rester Pareto-optimaux.
Budget large (>= 10s) : les methodes approchees (MCTS, recuit simule) entrent dans la frontiere pour les terrains ou elles ne dominaient pas.
La lecon methodologique : il n y a pas UN solveur le meilleur – il y a un portefeuille de solveurs Pareto-optimaux, et le choix depend du couple (terrain, budget, exigence fiabilite). C est le message central de Rice (1976) formalise : la selection est utile quand le cout de selection est beaucoup plus petit que le cout d execution de tous les algorithmes.
# Cas discriminant : lecture par budget croissant.BUDGETS = [0.1, 0.5, 1.0, 2.0, 5.0, 10.0]EXIGENCE_FIABILITE =0.99print(f"Pour chaque budget, calcul de la frontiere Pareto filtree par exigence fiabilite >= {EXIGENCE_FIABILITE*100:.0f}%.")print("="*76)for budget in BUDGETS: candidats = []for terrain, solveur, wall, succes in PROFIL_SYNTHETIQUE:if wall <= budget and succes >= EXIGENCE_FIABILITE: candidats.append((terrain, solveur, wall, succes))print()print(f"Budget {budget:5.1f}s : {len(candidats)} solveurs tenables")for terrain, solveur, wall, succes in candidats:print(f" {terrain:10s}{solveur:18s} wall={wall:.2f}s succes={succes*100:.0f}%")ifnot candidats:print(" (aucun solveur ne tient ce budget avec cette exigence)")print()print("="*76)print()print("Le gagnant depend du couple (budget, exigence fiabilite) :")print(" - Budget 0.1s, fiabilite 99% : DLX (Sudoku) seul, ou Baseline-freq (succes 65% << 99%).")print(" - Budget 0.5s, fiabilite 99% : DLX + CP-SAT (Sudoku) + Negamax (Puissance4).")print(" - Budget 1.0s, fiabilite 99% : + Alpha-beta (Puissance4).")print(" - Budget >= 5s, fiabilite 99% : tous les solveurs exacts (DLX, CP-SAT, Z3, Alpha-beta, Negamax, Recherche exo.).")print()print("Ce cas demontre la these centrale de Smith-Miles (2009) : un classement unique est")print("trompeur si on ne fixe pas simultanement budget, fiabilite, et famille d instances.")
Construisez un sélecteur qui reçoit un profil de solveurs, un budget maximal et une fiabilité minimale. Il doit retourner le candidat admissible le plus rapide, ou None si aucun candidat ne respecte les contraintes.
Étape 1 : filtrez simultanément sur wall_moy_s et succes_rate.
Étape 2 : triez les candidats admissibles par temps croissant.
Indice : testez aussi un budget trop strict afin de vérifier le cas sans solution.
# TODO étudiant : compléter le sélecteur sous contraintes.def choisir_solveur(profil, budget_s, fiabilite_min):# Étape 1 : filtrer les solveurs qui respectent les deux contraintes.# Étape 2 : départager les candidats par temps croissant.# Indice : chaque ligne de profil contient terrain, solveur, temps et fiabilité.returnNoneselection = choisir_solveur(PROFIL_SYNTHETIQUE, budget_s=0.5, fiabilite_min=0.99)print("Exercice à compléter : sélection sous budget", selection)
Exercice à compléter : sélection sous budget None
Ponts series
Les tranches suivantes connecteront explicitement :
Sudoku : la serie MyIA.AI.Notebooks/Sudoku/ (notamment Sudoku-18-Comparison-*.ipynb) pour le terrain Sudoku (DLX, CP-SAT, SMT) ;
Search / Part1-Foundations : Search-1 a Search-4 pour les bases de complexite ;
Search / Part2-CSP : Search-5 a Search-12 pour les techniques CSP/SAT/SMT ;