import random
import itertools
import statistics
random.seed(20260916)
print("kernel python3, stdlib only")kernel python3, stdlib only
Série : Search / Part1-Foundations — variante -d de Search-11 (métaheuristiques MEALPy) et Search-11c (sélection empirique) Opération attestée : op 11 « Descendre sous budget » — EPIC #12204 (table des opérations), 2ᵉ attestation après mimo_lean/Descent.lean (attestation 1, Lean-formel) Source primaire (lue firsthand) : MyIA.AI.Notebooks/SymbolicAI/Lean/mimo_lean/Descent.lean — descent_flips_le_barrier (confinement + plafond) et descent_target_before_ceiling (terminaison dans la cible avant épuisement du budget), thèse op 11 explicite l.148
Ce que ce notebook fait que Search-11 ne fait pas : Search-11 utilise des métaheuristiques qui améliorent iterativement sous un budget d’évaluations ; il n’énonce ni ne mesure jamais la loi qui garantit qu’elles terminent. Ici, la loi est un objet : énoncée, exercée par des assertions runtime sur des centaines d’exécutions, et surtout cassée où elle doit casser — sa troisième hypothèse est fausse en paysage générique, et le notebook l’exhibe.
Statuts de preuve : [exercée ici] = mesuré/asserted dans ce notebook · [Lean-formel] = prouvé dans Descent.lean · [rapporté] = affirmé sans re-vérification.
Exécution : stdlib uniquement (random, itertools), CPU, ~20 min de lecture, ~5 s d’exécution.
Descent.lean formalise une descente comme une suite d’états où seuls les flips acceptés comptent, sous trois hypothèses :
| Hypothèse | Énoncé | Rôle |
|---|---|---|
hstrict |
chaque mouvement accepté décroît strictement le coût | interdit les cycles |
hbarrier |
le coût reste confiné sous une borne B sur tout le run |
majore le nombre de pas |
hnostall |
hors de la cible, un mouvement accepté existe toujours | bloque uniquement sur la cible |
Sous ces trois hypothèses, descent_target_before_ceiling démontre : le dernier état est dans la cible, et le run a consommé strictement moins que le plafond M_N dès que B < M_N. C’est la garantie de complexité : terminaison dans la cible avant épuisement du budget.
La question de ce notebook : que reste-t-il de cette loi quand on quitte le cadre Lean — c’est-à-dire sur un paysage combinatoire générique, avec un budget d’évaluations explicite ? Réponse en trois temps : la loi de terminaison tient (§2, exercée), la troisième hypothèse tombe (§3, exhibée), et le budget achète de la qualité à rendement décroissant (§4, mesuré).
Le substrat : sac à dos 0/1 (instances synthétiques seedées). Le potentiel Φ = valeur négative de la sélection (décroître Φ = augmenter la valeur). Le voisinage : flips de bit uniques. La barrière de confinement : la relaxation LP du sac à dos (majorant de toute sélection admissible — Φ ne peut pas descendre en dessous).
Chaque exécution est instrumentée : trajectoire de Φ, mouvements acceptés, raison d’arrêt. Les assertions hstrict/hbarrier sont vérifiées au runtime, à chaque mouvement accepté — la loi n’est pas décrite, elle est exercée.
def instance_knapsack(n, seed, ratio_poids=0.5):
"""Instance 0/1 synthetique : n objets, capacite = ratio * somme poids."""
rng = random.Random(seed)
poids = [rng.randint(10, 100) for _ in range(n)]
valeurs = [poids[i] + rng.randint(-5, 20) for i in range(n)] # correlation valeur/poids
capacite = int(ratio_poids * sum(poids))
return poids, valeurs, capacite
def borne_lp(poids, valeurs, capacite):
"""Relaxation fractionnaire (greedy densite) : majorant de l'optimum 0/1."""
ordre = sorted(range(len(poids)), key=lambda i: valeurs[i] / poids[i], reverse=True)
reste, total = capacite, 0
for i in ordre:
pris = min(poids[i], reste)
total += valeurs[i] * pris / poids[i]
reste -= pris
if reste == 0:
break
return total
def dp_exact(poids, valeurs, capacite):
"""Optimum 0/1 exact par programmation dynamique (temoin de verite)."""
best = [0] * (capacite + 1)
for p, v in zip(poids, valeurs):
for c in range(capacite, p - 1, -1):
if best[c - p] + v > best[c]:
best[c] = best[c - p] + v
return best[capacite]
def admissible(x, poids, capacite):
return sum(p for p, xi in zip(poids, x) if xi) <= capacite
def valeur(x, valeurs):
return sum(v for v, xi in zip(valeurs, x) if xi)
def descente_sous_budget(x0, poids, valeurs, capacite, budget, collect=True):
"""Hill-climbing a stricte amelioration, plafonne a `budget` evaluations.
Retourne un dict : etat final, trajectoire du potentiel, nombre de pas,
raison d'arret ('budget' | 'blocage'), et compteurs de violation des
hypotheses hstrict/hbarrier (assertions runtime de la loi, section 2).
"""
lp = borne_lp(poids, valeurs, capacite)
x = list(x0)
phi = -valeur(x, valeurs) # potentiel : decroitre = s'ameliorer
trajectoire = [phi] if collect else []
hstrict_viol, hbarrier_viol = 0, 0
evaluations, pas = 0, 0
ameliorant_final = None
while evaluations < budget:
best_delta, best_i = 0, None
for i in range(len(x)):
if evaluations >= budget:
break
y = list(x); y[i] = 1 - y[i]
if not admissible(y, poids, capacite):
evaluations += 1
continue
evaluations += 1
d = valeur(y, valeurs) - valeur(x, valeurs)
if d > best_delta:
best_delta, best_i = d, i
if best_i is None:
ameliorant_final = False # aucun voisin ameliorant : blocage
break
y = list(x); y[best_i] = 1 - y[best_i]
phi_new = -valeur(y, valeurs)
if phi_new >= phi: # hstrict : mouvement accepté doit decroitre strictement
hstrict_viol += 1
if -phi_new > lp + 1e-9: # hbarrier : confinement sous le majorant LP
hbarrier_viol += 1
x, phi = y, phi_new
pas += 1
if collect:
trajectoire.append(phi)
else:
ameliorant_final = None # budget epuise avant balayage complet du voisinage
if ameliorant_final is None:
# le dernier balayage incomplet peut cacher un ameliorant : le noter honnetement
raison = 'budget'
else:
raison = 'blocage'
return {'x': x, 'valeur': -phi, 'trajectoire': trajectoire, 'pas': pas,
'evaluations': evaluations, 'raison': raison,
'hstrict_viol': hstrict_viol, 'hbarrier_viol': hbarrier_viol,
'lp': lp}raison distingue budget atteint (le plafond d’évaluations a interrompu la recherche alors qu’un améliorant pouvait exister) de blocage (un balayage complet du voisinage n’a trouvé aucun mouvement améliorant) — la dissociation que Descent.lean érige en objet.hstrict_viol / hbarrier_viol comptent les violations des deux premières hypothèses sur les mouvements réellement acceptés : ils doivent rester à zéro, et la cellule suivante le vérifie en série.lp est le majorant LP : la barrière de confinement. Aucun état admissible ne peut dépasser cette valeur — Φ reste confiné au-dessus de −lp.# LA LOI, EXERCICEE : hstrict + hbarrier + plafond, sur 60 executions
random.seed(20260916)
runs = []
for n in (16, 24, 32):
for seed in range(20):
poids, valeurs, capacite = instance_knapsack(n, seed)
x0 = [0] * n
budget = 8 * n * n # generosite : budget >> longueur typique d'un run
r = descente_sous_budget(x0, poids, valeurs, capacite, budget)
runs.append((n, seed, r))
v_strict = sum(r['hstrict_viol'] for _, _, r in runs)
v_barrier = sum(r['hbarrier_viol'] for _, _, r in runs)
depassements = sum(1 for (n, _, r) in runs if r['evaluations'] > 8 * n * n)
print(f"executions : {len(runs)}")
print(f"violations hstrict : {v_strict} (mouvements acceptes sans decroissance stricte)")
print(f"violations hbarrier : {v_barrier} (etat au-dela du majorant LP)")
print(f"depassements de plafond : {depassements} (evaluations > budget)")
assert v_strict == 0 and v_barrier == 0 and depassements == 0
print("=> *[exercee ici]* la loi de terminaison tient : strictement decroissant, confine, plafonne.")executions : 60
violations hstrict : 0 (mouvements acceptes sans decroissance stricte)
violations hbarrier : 0 (etat au-dela du majorant LP)
depassements de plafond : 0 (evaluations > budget)
=> *[exercee ici]* la loi de terminaison tient : strictement decroissant, confine, plafonne.
Sur 60 exécutions (3 tailles × 20 graines), zéro violation : chaque mouvement accepté décroît strictement Φ, aucun état ne franchit la barrière LP, aucune exécution ne dépasse son plafond d’évaluations. La partie inconditionnelle de la loi de Descent.lean — descent_flips_le_barrier — se transporte intacte du cadre Lean au paysage combinatoire : la descente sous budget termine toujours. Ce n’est pas une propriété de MEALPy ou d’un algorithme particulier : c’est la conséquence arithmétique de « strictement décroissant + confiné ».
hnostall ne survit pas au paysage génériqueLe théorème Lean conclut « le dernier état est dans la cible » — mais seulement sous hnostall : hors de la cible, un mouvement accepté existe toujours. Cette hypothèse est gratuite en paysage générique. Ce qui suit l’exhibe : on classe chaque arrêt en budget atteint vs blocage, puis on demande aux blocages s’ils sont sur la cible (optimum global, connu par DP exacte) ou hors cible — des optima locaux, la négation exacte de hnostall.
# LA DISSOCIATION, EXERCICEE : budget-atteint vs blocage, et blocage-hors-cible
random.seed(20260916)
budgets_serre = 4 * 24 * 24 # budget volontairement serre pour rendre 'budget' observable
stats = {'budget': 0, 'blocage_cible': 0, 'blocage_hors_cible': 0}
gaps_hors_cible = []
n = 24
for seed in range(60):
poids, valeurs, capacite = instance_knapsack(n, seed)
opt = dp_exact(poids, valeurs, capacite)
x0 = [random.Random(seed).randint(0, 1) for _ in range(n)]
if not admissible(x0, poids, capacite):
x0 = [0] * n
r = descente_sous_budget(x0, poids, valeurs, capacite, budgets_serre, collect=False)
if r['raison'] == 'budget':
stats['budget'] += 1
gap = 1 - r['valeur'] / opt
gaps_hors_cible.append(gap) # budget epuise : gap quelconque
else:
if r['valeur'] == opt:
stats['blocage_cible'] += 1
else:
stats['blocage_hors_cible'] += 1
gaps_hors_cible.append(1 - r['valeur'] / opt)
total = sum(stats.values())
print(f"runs : {total}")
print(f"budget atteint : {stats['budget']:3d} ({stats['budget']/total:.0%})")
print(f"blocage SUR la cible : {stats['blocage_cible']:3d} ({stats['blocage_cible']/total:.0%})")
print(f"blocage HORS cible (optimum local) : {stats['blocage_hors_cible']:3d} ({stats['blocage_hors_cible']/total:.0%})")
print(f"gap final median : {statistics.median(gaps_hors_cible):.1%}")
print(f"gap final max : {max(gaps_hors_cible):.1%}")
print()
print("=> *[exercee ici]* hnostall est fausse :", end=" ")
if stats['blocage_hors_cible'] > 0:
print(f"{stats['blocage_hors_cible']} runs se bloquent sur un optimum local :")
print(" le theoreme 'dernier etat dans la cible' ne se transporte PAS ;")
print(" sa conclusion devient : dernier etat = minimum LOCAL, atteint sous budget.")
else:
print("aucun optimum local observe sur cet echantillon (a confronter a un voisinage plus pauvre).")
# Regime budget SERRE (~2 balayages) : la cause d'arret 'budget' existe aussi
stats_tight = {'budget': 0, 'blocage': 0}
for seed in range(30):
poids, valeurs, capacite = instance_knapsack(n, seed)
x0 = [random.Random(seed).randint(0, 1) for _ in range(n)]
if not admissible(x0, poids, capacite):
x0 = [0] * n
r = descente_sous_budget(x0, poids, valeurs, capacite, 48, collect=False)
stats_tight[r['raison']] += 1
print(f"a budget serre (48 evals ~ 2 balayages) : budget {stats_tight['budget']}/30, blocage {stats_tight['blocage']}/30")runs : 60
budget atteint : 0 (0%)
blocage SUR la cible : 0 (0%)
blocage HORS cible (optimum local) : 60 (100%)
gap final median : 10.4%
gap final max : 16.9%
=> *[exercee ici]* hnostall est fausse : 60 runs se bloquent sur un optimum local :
le theoreme 'dernier etat dans la cible' ne se transporte PAS ;
sa conclusion devient : dernier etat = minimum LOCAL, atteint sous budget.
a budget serre (48 evals ~ 2 balayages) : budget 30/30, blocage 0/30
Les deux causes d’arrêt existent, chacune dans son régime de budget : à budget serré (~2 balayages), l’arrêt est « budget atteint » — on ne sait pas, un améliorant pouvait exister au-delà du plafond ; à budget généreux (96 balayages), l’arrêt est « blocage » — on sait, aucun voisin n’améliore. Et sur ce régime généreux, tous les blocages observés sont hors cible : hnostall y est littéralement fausse — un état non-cible sans mouvement accepté, gap médian ~10 %. C’est la dette que Descent.lean consigne dans son propre fichier (« un échec de décroissance n’est pas un échec d’expérience à écarter, c’est une dissociation à exhiber ») : transportée hors du cadre Lean, la garantie de cible dégénère en garantie de minimum local sous budget. La loi forte (terminaison) tient ; la loi faible (qualité de l’atteint) ne tient que hypothèse par hypothèse.
La descente est un algorithme anytime : à chaque plafond correspond une qualité atteignable. La mesure : gap final vs optimum DP exact, à budget croissant, 30 graines par point — la courbe de rendement du budget.
# COURBE QUALITE-BUDGET (anytime) : gap median vs budget, multi-seed
random.seed(20260916)
n = 24
echantillons = []
for B in (20, 50, 100, 200, 400, 800):
gaps = []
for seed in range(30):
poids, valeurs, capacite = instance_knapsack(n, 1000 + seed)
opt = dp_exact(poids, valeurs, capacite)
rng = random.Random(2000 + seed)
x0 = [rng.randint(0, 1) for _ in range(n)]
if not admissible(x0, poids, capacite):
x0 = [0] * n
r = descente_sous_budget(x0, poids, valeurs, capacite, B, collect=False)
gaps.append(1 - r['valeur'] / opt)
echantillons.append((B, gaps))
q = sorted(gaps)
print(f"budget {B:4d} evaluations | gap median {statistics.median(gaps):6.1%} | "
f"IQR [{q[7]:.1%}, {q[22]:.1%}] | pire {max(gaps):.1%}")
g_med = [statistics.median(g) for _, g in echantillons]
ameliorations = [g_med[i] - g_med[i + 1] for i in range(len(g_med) - 1)]
print()
print("rendement marginal (gain de gap par doubling de budget) :", )
for i, a in enumerate(ameliorations):
print(f" {echantillons[i][0]:4d} -> {echantillons[i+1][0]:4d} : {a:+.1%}")budget 20 evaluations | gap median 86.3% | IQR [18.4%, 87.1%] | pire 89.0%
budget 50 evaluations | gap median 63.8% | IQR [12.2%, 66.4%] | pire 72.6%
budget 100 evaluations | gap median 39.3% | IQR [12.2%, 42.8%] | pire 48.0%
budget 200 evaluations | gap median 10.8% | IQR [7.9%, 12.7%] | pire 17.0%
budget 400 evaluations | gap median 8.9% | IQR [7.0%, 12.2%] | pire 17.0%
budget 800 evaluations | gap median 8.9% | IQR [7.0%, 12.2%] | pire 17.0%
rendement marginal (gain de gap par doubling de budget) :
20 -> 50 : +22.4%
50 -> 100 : +24.5%
100 -> 200 : +28.5%
200 -> 400 : +1.9%
400 -> 800 : +0.0%
Le gap médian s’effondre aux petits budgets puis se plateaute : doubler le budget rapporte de moins en moins, et le plateau restant est exactement la part des optima locaux que §3 a exhibée — le budget n’achète plus rien quand tous les runs restants sont bloqués, pas interrompus. C’est la frontière opérationnelle de l’op 11 : au-delà du budget de saturation, la seule monnaie est le changement de voisinage ou les restarts (exercice 1), pas plus d’évaluations.
Expérience ICT candidate : le protocole qualité-budget de §4, appliqué à une descente multi-regards (op 12) — budget partagé entre deux potentiels différents, courbe comparée au budget intégrairement alloué au regard le plus performant en isolation. La dissociation §3 prédit que le partage ne peut pas battre le meilleur regard seul sur les instances où il se bloque ; la mesurer dirait si le plateau de saturation est un invariant ou un artefact du voisinage.
Limites (bornes explicites) : (1) substrat sac à dos 0/1 seul — voisinage bit-flip unimodal par objet ; (2) hill-climbing déterministe — pas de recuit, pas de restarts (c’est l’exercice 1, pas le corps) ; (3) la comparaison MEALPy reste le territoire de Search-11 — ce notebook énonce la loi, il ne refait pas le tour d’horizon.
Exercices — les stubs s’exécutent sans erreur (politique du dépôt : compléter, pas débloquer).
# Exercice 1 — restarts strategiques : mesurer l'effet sur les blocages hors cible
# Indice : repartir d'un x0 aleatoire a chaque blocage, budget global constant a repartir.
# Etape 1 : encapsuler la boucle (descente + tirage) dans budget_restarts().
# Etape 2 : recompter stats['blocage_hors_cible'] a budget global egal.
def descente_restart(x0_gen, poids, valeurs, capacite, budget_global, k_restarts):
# TODO etudiant : k_restarts descentes, budget_global // k_restarts chacune, garder la meilleure
return None
# Exercice 2 — barriere : LP fractionnaire vs borne triviale 0
# Indice : hbarrier est une HYPOTHESE ; une barriere plus serree raccourcit le plafond theorique.
# Etape 1 : recalculer le majorant de flips de descent_flips_le_barrier avec lp vs valeur initiale.
# Etape 2 : comparer au pas reellement observe.
def plafond_theorique(x0, lp):
# TODO etudiant : majorant du nombre de pas acceptes selon la barriere choisie
return None
# Exercice 3 — voisinage 2-flip : le plateau de saturation recule-t-il ?
# Indice : le voisinage 2-flip contient le 1-flip ; un optimum 1-flip n'est pas optimum 2-flip.
# Etape 1 : etendre le balayage aux paires (i, j). Etape 2 : retracer la courbe de la section 4.
def descente_2flip(x0, poids, valeurs, capacite, budget):
# TODO etudiant : meme schema, voisins = paires de bits
return None
print("Exercices a completer : 1 (restarts), 2 (barriere), 3 (voisinage 2-flip).")Exercices a completer : 1 (restarts), 2 (barriere), 3 (voisinage 2-flip).
| Exigence de distillation | Où elle vit |
|---|---|
| Source primaire firsthand | mimo_lean/Descent.lean — descent_flips_le_barrier l.110, descent_target_before_ceiling l.144, thèse op 11 l.148 |
| Objet formel | descente_sous_budget : potentiel strictement décroissant, barrière LP, plafond d’évaluations |
| Claim exact, exercé | §2 : 60 exécutions, 0 violation hstrict, 0 violation hbarrier, 0 dépassement de plafond — [exercée ici] |
| Contre-claim, exhibé | §3 : hnostall fausse — 100 % des blocages hors cible à budget généreux (gap médian 10,4 %), arrêts « budget » exhibés à budget serré |
| Expérience ICT candidate | §5 : qualité-budget d’une descente multi-regards (op 12) |
Attestation : 2ᵉ attestation de l’opération 11 « Descendre sous budget » (EPIC #12204), sur substrat indépendant de l’attestation Lean — empirique-notebook, stdout réels committés. La promotion en TABLE appartient à la relecture froide (A7).