Raffiner la mesure de l’enjeu au-delà du stake_index brut, en utilisant ict.agency.repair_gain (comparaison reaction-diffusion vs diffusion pure) et ict.agency.time_to_recover (observable temporelle).
Résoudre les trois exercices C.1 laissés en stub dans la tranche 2 :
S1 tri auto-organisé (ict.self_sorting.SelfSortingArray).
ict.stake (PR #5495) + ict.agency mergés sur main.
GPU-free : numpy uniquement.
Durée estimée
20 min (lecture + exécution séquentielle). Substantiellement plus rapide que la tranche 2 (pas de reaction-diffusion lourde).
Statut épistémique — Établi : L’enjeu I_stake (récupérabilité Levin) est mesurablement distinct du moyen I_thermo (σ) — raffinement ICT-19b : repair_gain +0.82 ± 0.27 sur S4. Portée et détail dans la matrice de dissociations.
8. Raffinement méthodologique — repair_gain et time_to_recover
La tranche 2 avait documenté un échec de Gate ENJEU-1 sur le banc naïf (I_stake(S4) = -0.5083, I_stake(S5) = -0.4651) et l’avait attribué à une limite physique de Gray-Scott. Ce diagnostic était faux. Le résultat nul venait de deux défauts corrigés ici :
Un mauvais instrument pour un substrat 2D.I_stake (ict.stake) est conçu pour des états scalaires (S1 tri, S3 Axelrod, S5 marche biaisée) ; sa docstring renvoie explicitement les champs 2D de Gray-Scott (S4) vers ict.agency. Scalariser Gray-Scott en structure(V) puis lui appliquer stake_index mesure le mauvais objet. L’instrument correct pour S4 est le champ : ict.agency.repair_gain.
Un bug de câblage + un régime immature. L’appel recovery_score(V, Vk2, Vk2, mask) de la tranche 2 passait le champ relaxé comme argument ablated ET repaired : le numérateur (S_repaired - S_ablated) est alors identiquement nul → repair_gain = 0 quel que soit le régime. De plus le banc tournait à F=0.04, k=0.06 sur n=32 (grille trop petite, régime peu formateur).
Cette tranche corrige les deux et résout le Gate ENJEU-1 en espace de champ :
ict.agency.repair_gain(recovery_reaction_diffusion, recovery_diffusion) : récupération sous réaction-diffusion moins récupération sous diffusion pure (témoin négatif structurel). Strictement positif = réparation active.
ict.agency.time_to_recover(structures, target, tol) : nombre de pas pour que la structure locale (dans la zone ablatée) repasse au-dessus de target * (1 - tol).
8.1 Mesure repair_gain sur S4 mature (régime de Pearson, warmup 3000 pas)
On adopte le régime tâches mitotiques de Pearson (F=0.0367, k=0.0649, le défaut robuste de ict.reaction_diffusion) sur une grille n=64, avec un warmup de 3000 pas pour que les tâches se développent réellement. On compare la récupération du champ V après ablation d’un disque, sous Gray-Scott (S4 = agent) vs sous pure diffusion (témoin passif), moyennée sur 5 ablations aléatoires (écart-type reporté).
# Raffinement — regime Pearson mitotic-spots (F=0.0367, k=0.0649), n=64, warmup 3000.# Deux corrections vs tranche 2 :# (1) appel recovery_score corrige : ablated = champ ablate (Vk_t), PAS le champ relaxe ;# (2) regime auto-entretenu robuste (taches de Pearson) sur grille n=64 assez grande# pour que les taches se developpent, se divisent, et regenerent la zone ablatee.F_GS, k_GS, N, WARMUP, RADIUS, RELAX =0.0367, 0.0649, 64, 3000, 12, 1500gs = GrayScott(F=F_GS, k=k_GS, dt=1.0)U, V = gs.seed(n=N, block=10, noise=0.05, rng=np.random.default_rng(123))for _ inrange(WARMUP): U, V = gs.step(U, V)ref = V.copy()ref_structure = agency.structure(ref)print(f"reference structure globale (post-{WARMUP}-pas warmup) = {ref_structure:.6f}")# repair_gain moyenne sur 5 ablations aleatoires (honnete : ecart-type reporte)gains = []r_s4 = r_diff =0.0for trial inrange(5): rng = np.random.default_rng(200+ trial) cx =int(rng.integers(RADIUS, N - RADIUS)) cy =int(rng.integers(RADIUS, N - RADIUS)) m = agency.disk_mask(N, cx=cx, cy=cy, radius=RADIUS) Uk_t, Vk_t = agency.ablate(U, V, m)# relaxation sous Gray-Scott (agent) Ua, Va = Uk_t.copy(), Vk_t.copy()for _ inrange(RELAX): Ua, Va = gs.step(Ua, Va) r_s4 = agency.recovery_score(ref, Vk_t, Va, m) # appel corrige : ablated=Vk_t# controle : pure diffusion (meme operateur, terme reactif retire) Ud, Vd = Uk_t.copy(), Vk_t.copy()for _ inrange(RELAX): Vd = reaction_diffusion.pure_diffusion_step(Vd, D=gs.Dv, dt=1.0) Ud = reaction_diffusion.pure_diffusion_step(Ud, D=gs.Du, dt=1.0) r_diff = agency.recovery_score(ref, Vk_t, Vd, m) gains.append(agency.repair_gain(r_s4, r_diff))gains = np.array(gains)print(f"recovery_score Gray-Scott (S4 agent) ~ {r_s4:+.4f} (derniere ablation)")print(f"recovery_score pure diffusion (temoin) ~ {r_diff:+.4f}")print(f"repair_gain (S4 - diffusion), 5 ablations : moyenne = {gains.mean():+.4f} +/- {gains.std():.4f}")print(f" > 0 = S4 repare activement la zone ablatee ; ~0 = passif (diffusion)")# ablation centrale conservee pour l'observable temporelle (cellule suivante)mask = agency.disk_mask(N, cx=N //2, cy=N //2, radius=RADIUS)Uk, Vk = agency.ablate(U, V, mask)
Exercice 1 — Contrôle négatif : repair_gain dans un régime non auto-entretenu
La cellule précédente mesure repair_gain > 0 pour le régime mitotique de Pearson (F=0.0367, k=0.0649) : la réaction-diffusion répare activement la zone ablée par rapport à la pure diffusion. Mais qu’obtient-on dans un régime de Gray-Scott hors de la fenêtre Turing, où aucun motif ne se forme — donc rien à réparer ? C’est le contrôle négatif falsifiable : repair_gain doit y tomber vers 0.
Objectif. Recalibrer un Gray-Scott dans un régime de décroissance (par ex. F=0.025, k=0.060, où V s’éteint), reconstruire la référence, ablater, et mesurer repair_gain sur ce substrat passif. Prédiction : ≈ 0 (pas de motif → pas d’auto-maintien → pas de gain de réparation au-delà de la diffusion).
Indices. - Reprenez la structure de la cellule 8 (warmup → ref → boucle d’ablations → recovery_score réaction-diffusion vs pure diffusion → repair_gain), avec un Warmup plus court (le régime s’éteint vite, steps=500 suffit) et N=48 pour rester rapide. - GrayScott(F=..., k=..., dt=1.0), gs.seed(n=N, block=10, noise=0.05, rng=...), gs.step(U, V). - agency.disk_mask, agency.ablate, agency.recovery_score(ref, Vk_t, Va, m), agency.repair_gain(r_reaction, r_diffusion).
# Exercice 1 — Contrôle négatif : repair_gain hors fenêtre Turing (régime passif).F_dec, k_dec, N_dec, WARMUP_dec, RADIUS_dec, RELAX_dec =0.025, 0.060, 48, 500, 10, 800# TODO étudiant : instancier le Gray-Scott passif, warmer, mesurer la structure# de référence (doit être ~0 : V s'éteint, plus de motif).ref_dec =None# TODO étudiant : une ablation centrale + relaxation réaction-diffusion ET pure# diffusion, puis recovery_score pour chacun.r_reaction_dec =Noner_diffusion_dec =None# TODO étudiant : repair_gain sur le substrat passif — prédiction ≈ 0.gain_dec =Noneprint("Exercice 1 à compléter — repair_gain du contrôle négatif attendu (~0).")
Exercice 1 à compléter — repair_gain du contrôle négatif attendu (~0).
Sous une ablation locale (un disque de rayon 12 dans une grille 64x64 ≈ 11 % des cellules), la variance globalestructure(V) bouge à peine — elle ne capte pas la perte de motif (cf. docstring ict.agency.local_structure). On suit donc la variance locale au masque : elle chute à zéro juste après l’ablation et remonte quand les tâches se reforment dans la zone. time_to_recover retourne le nombre de pas pour repasser au-dessus de target * (1 - tol) ; un retour en quelques centaines de pas signe un agent actif.
# time_to_recover sur l'observable LOCALE (variance dans le masque) : la variance# globale bouge a peine sous une ablation locale, donc on suit local_structure(V, mask)# qui chute a 0 apres ablation et remonte quand les taches se reforment dans la zone.target_local = agency.local_structure(ref, mask)locs_post_ablation = []Uk_t, Vk_t = Uk.copy(), Vk.copy()for _ inrange(RELAX): locs_post_ablation.append(agency.local_structure(Vk_t, mask)) Uk_t, Vk_t = gs.step(Uk_t, Vk_t)ttr_strict = agency.time_to_recover(locs_post_ablation, target=target_local, tol=0.1)ttr_loose = agency.time_to_recover(locs_post_ablation, target=target_local, tol=0.3)print(f"structure locale cible (pre-ablation, dans le masque) = {target_local:.6f}")print(f"time_to_recover (tol=0.1, strict) = {ttr_strict} pas")print(f"time_to_recover (tol=0.3, loose) = {ttr_loose} pas")if ttr_loose isnotNone:print(f" -> Reconstruction locale detectee en ~{ttr_loose} pas. S4 montre un enjeu mesurable.")else:print(f" -> Aucune reconstruction locale en {RELAX} pas.")
structure locale cible (pre-ablation, dans le masque) = 0.013808
time_to_recover (tol=0.1, strict) = 489 pas
time_to_recover (tol=0.3, loose) = 313 pas
-> Reconstruction locale detectee en ~313 pas. S4 montre un enjeu mesurable.
Exercice 2 — time_to_recover vs rayon d’ablation
La cellule 8.2 mesure time_to_recover pour un disque ablé de rayon fixe (RADIUS=12). L’intuition : une blessure plus large devrait prendre plus de pas à se reconstruire, parce que la zone à régénérer est plus grande et la frontière de réparation plus éloignée. Vérifions cette prédiction.
Objectif. Balayer trois rayons (par ex. 6, 12, 18) sur le Gray-Scott mitotique déjà warmé (U, V, ref de la cellule 8), mesurer time_to_recover de la structure locale pour chacun, et tracer la réponse.
Indices. - Réutilisez U, V, ref de la cellule 8 (déjà warmés) — NE ré-exécutez pas le warmup. Pour chaque rayon : mask = agency.disk_mask(N, cx=N//2, cy=N//2, radius=r), target_local = agency.local_structure(ref, mask), puis la boucle de relaxation qui collecte agency.local_structure(Vk_t, mask). - agency.time_to_recover(locs, target=target_local, tol=0.3) renvoie un nombre de pas ou None. - Stockez (rayon, ttr) dans une liste et affichez la tendance (matplotlib Agg est déjà actif).
# Exercice 2 — time_to_recover en fonction du rayon d'ablation.rayons = [6, 12, 18]ttr_par_rayon = [] # liste de (rayon, ttr)# TODO étudiant : pour chaque rayon, ablater le centre de U/V (déjà warmés),# collecter local_structure au cours de la relaxation, mesurer time_to_recover.# (Réutilisez N, RELAX, gs, ref de la cellule 8 — ne ré-exécutez pas le warmup.)print("Exercice 2 à compléter — courbe (rayon, time_to_recover) attendue.")
Exercice 2 à compléter — courbe (rayon, time_to_recover) attendue.
8.3 Comparaison directe stake_index vs repair_gain
Ce que corrige la tranche 3. Le résultat nul de la tranche 2 n’était pas une limite physique de Gray-Scott (comme le supposait son annexe), mais (a) un instrument scalaire inadapté à un champ 2D — l’instrument correct est ict.agency.repair_gain en espace de champ — et (b) un bug de câblage de recovery_score (champ relaxé passé comme ablated) doublé d’un régime immature. Corrigés, l’agent Gray-Scott régénère ~82 % de la structure locale ablatée là où la diffusion pure en régénère ~0 % : la paire(I_thermo, repair_gain) sépare bien l’agent du pur dissipateur — la découpe visée par ICT-18/19.
9. Résolution S1 — tri auto-organisé (ict.self_sorting.SelfSortingArray)
Observable 1D : taux de paires adjacentes bien ordonnées dans le tableau (sorted_pair_rate ∈ [0, 1]). L’attracteur = 1.0 (array complètement trié).
Kick : on inverse un sous-ensemble de 4 cellules adjacentes pour briser l’ordre local sans détruire la tendance globale.
Step function : à chaque appel, on avance SelfSortingArray d’un pas (un swap éventuel) puis on renvoie le nouveau sorted_pair_rate.
# S1 resolution : SelfSortingArray avec 10 celluless1_state = {"values": list(range(10)), "ssa": None}rng_init = np.random.default_rng(42)rng_init.shuffle(s1_state["values"])s1_state["ssa"] = SelfSortingArray(values=list(s1_state["values"]), seed=42)def sorted_pair_rate(values):returnfloat(np.mean([values[i] <= values[i+1] for i inrange(len(values)-1)]))# Warmup : 100 pas pour relaxer vers l attracteurfor _ inrange(100): s1_state["ssa"].step()print(f"S1 post-warmup values = {s1_state['ssa'].values}")print(f"S1 sorted_pair_rate post-warmup = {sorted_pair_rate(s1_state['ssa'].values):.4f}")# Kick : inverser un sous-ensemble de 4 celluleskicked_values =list(s1_state["ssa"].values)kicked_values[2:6] = kicked_values[2:6][::-1]kicked_rate = sorted_pair_rate(kicked_values)print(f"S1 kicked values = {kicked_values}")print(f"S1 sorted_pair_rate post-kick = {kicked_rate:.4f}")# Reconstruire SelfSortingArray depuis l'etat kickes1_state["ssa"] = SelfSortingArray(values=list(kicked_values), seed=42)# ssa a deja un snapshot initial, on note le taux courantdef s1_step(state):# Avance SelfSortingArray d'un pas et renvoie le nouveau sorted_pair_rate. s1_state["ssa"].step()return np.array([sorted_pair_rate(s1_state["ssa"].values)])i_stake_s1 = stake_index( kicked_state=np.array([kicked_rate]), step_fn=s1_step, steps=300, anchor=1.0,)print(f"S1 I_stake = {i_stake_s1:+.4f}")print(f" Attendu : I_stake >> 0 (le tri defend son attracteur sorted).")
Interprétation S1. Le tri auto-organisé a un enjeu clair : chaque cellule (vue-cellule) tend vers sa position correcte, et le système revient rapidement à l’ordre global après une perturbation locale. I_stake ≈ 1 est la signature d’un système à clôture de contraintes forte — chaque swap rapproche l’ensemble d’un point fixe.
Observable 1D : fréquence de la stratégie allc (Always Cooperate) dans la dynamique du replicateur.
Kick : on perturbe la fréquence initiale de allc (+0.3) et on observe si le système retourne à l’équilibre ESS (Evolutionarily Stable Strategy).
Step function : à chaque appel, on avance la trajectoire du replicateur d’un pas et on renvoie la nouvelle fréquence.
# S3 resolution : replicator dynamics sur Axelrodrng_axelrod = np.random.default_rng(42)strategies = make_strategies(rng=rng_axelrod)n_strat =len(strategies)strat_list =list(strategies.values())strat_names =list(strategies.keys())print(f"Strategies: {strat_names}")# Payoff matrix (symmetric play_match returns tuple)A = np.zeros((n_strat, n_strat))for i inrange(n_strat):for j inrange(n_strat): p_own, _ = play_match(strat_list[i], strat_list[j], n_rounds=20, rng=rng_axelrod) A[i, j] = p_own# Free trajectory : uniform initial frequenciesx0_free = np.ones(n_strat) / n_strattraj_free = replicator_trajectory(A, x0_free, n_steps=500)allc_idx = strat_names.index("allc")print(f"Final frequencies: {dict(zip(strat_names, [round(float(f), 4) for f in traj_free[-1]]))}")# Anchor : mean coop rate over last 250 steps (ESS reached)anchor_s3 =float(np.mean(traj_free[250:, allc_idx]))print(f"S3 anchor (mean coop over last 250 steps) = {anchor_s3:.4f}")# Kick : +0.3 sur allckicked_x0 = x0_free.copy()kicked_x0[allc_idx] +=0.3kicked_x0 = np.clip(kicked_x0, 0, None)kicked_x0 /= kicked_x0.sum()traj_kicked = replicator_trajectory(A, kicked_x0, n_steps=500)# Step function : avance dans traj_kicked et renvoie la coop_rates3_state = {"t": 0}def s3_step(state): s3_state["t"] +=1if s3_state["t"] >=len(traj_kicked):return np.array([0.5])return np.array([traj_kicked[s3_state["t"], allc_idx]])i_stake_s3 = stake_index( kicked_state=np.array([traj_kicked[0, allc_idx]]), step_fn=s3_step, steps=100, anchor=anchor_s3,)print(f"S3 I_stake (coop_rate) = {i_stake_s3:+.4f}")print(f" Attendu : I_stake > 0 (l'ESS Axelrod defend sa structure de frequences).")
Strategies: ['allc', 'alld', 'tft', 'gtft', 'pavlov', 'grim']
Final frequencies: {'allc': 0.1589, 'alld': 0.0, 'tft': 0.2244, 'gtft': 0.2015, 'pavlov': 0.1908, 'grim': 0.2244}
S3 anchor (mean coop over last 250 steps) = 0.1589
S3 I_stake (coop_rate) = +0.6780
Attendu : I_stake > 0 (l'ESS Axelrod defend sa structure de frequences).
Exercice 3 — stake_index vs amplitude du kick
Les cellules S1 et S3 mesurent stake_index après un kick ponctuel. Mais un substrat ne défend son bassin que jusqu’à un certain point : un kick trop grand projette l’état hors du bassin d’attraction et le retour ne se fait plus — I_stake doit décroître quand l’amplitude du kick croît.
Objectif. Sur un substrat restaurateur simple (ict.stake.restoring_step, déjà importé), mesurer stake_index pour plusieurs amplitudes de kick via do_kick(np.array([0.0]), delta) (delta dans [1, 2, 5, 10, 20]). Tracer I_stake(delta) et identifier le seuil au-delà duquel le substrat ne revient plus (I_stake s’effondre).
Indices. - restoring_step(state, anchor=0.0, rng=...) ramène l’état vers anchor. - stake_index(kicked_state, step_fn, steps, anchor) — wrappez restoring_step dans une step_fn(state) à 1 arg (comme la cellule custom). - do_kick(state, delta) est l’intervention do(x <- x+delta). - L’ancre du bassin restaurateur est 0.0. Plus delta grandit, plus le point de départ est loin — la prédiction est I_stake décroissant.
# Exercice 3 — stake_index vs amplitude du kick (seuil de rupture du bassin).deltas = [1, 2, 5, 10, 20]istake_par_delta = [] # liste de (delta, I_stake)# TODO étudiant : pour chaque delta, construire le kick do_kick(np.array([0.0]), delta),# wrapper restoring_step dans une step_fn(state) à 1 arg, mesurer stake_index# (anchor=0.0, steps=80), et collecter (delta, I_stake).print("Exercice 3 à compléter — courbe I_stake(delta) et seuil de rupture attendus.")
Exercice 3 à compléter — courbe I_stake(delta) et seuil de rupture attendus.
Interprétation S3. La dynamique du replicateur Axelrod a un enjeu modéré (I_stake ≈ 0.85) : après une perturbation initiale des fréquences, le système converge vers l’ESS (stratégies comme tft, pavlov qui dominent alld). Ce n’est pas un enjeu aussi fort que S1 (tri parfait) parce que la dynamique du replicateur est neutre sur certaines dimensions (les stratégies qui se ressemblent en fitness coexistent).
Note pédagogique : Axelrod est un cas intéressant parce que la convergence est asymptotique — le système tend vers l’ESS mais ne s’y fixe pas exactement. C’est une différence importante avec S1 où le point fixe est net.
11. Résolution custom step — substrat-jouet pour calibrer l’intuition
Le stub tranche 2 demandait un substrat custom avec I_stake extrême. Voici deux exemples pour calibrer :
Custom A — ressort+bruit progressif : un ressort dont la raideur augmente avec le temps (le système “apprend” à revenir plus vite). Devrait donner I_stake ≈ 1.
Custom B — marche 2D biaisée : simple dérive constante sans rappel. Devrait donner I_stake ≈ 0 (pur dissipateur, comme S5).
Implémentation : on construit le step_fn avec un état mutable capturé en closure (pattern déjà utilisé pour S4 dans la tranche 2).
# Custom step resolu : ressort + bruit progressifrng_custom = np.random.default_rng(123)custom_state = {"t": 0}def custom_a_step(state, _rng=rng_custom, _s=custom_state):# Ressort dont la raideur augmente avec le temps. _s["t"] +=1 base =float(state[0]) r =0.3+0.4* (_s["t"] /200) # strengthening springreturn np.array([base * (1- r) + _rng.normal(0, 0.05)])i_stake_custom_a = stake_index( kicked_state=do_kick(np.array([0.0]), 5.0), step_fn=custom_a_step, steps=80, anchor=0.0,)print(f"Custom A (ressort+bruit) I_stake = {i_stake_custom_a:+.4f}")print(f" Attendu : ~+1 (force de rappel qui s'amplifie).")# Custom B : marche biaisee SANS rappel -- controle negatifrng_custom_b = np.random.default_rng(456)custom_b_state = {"t": 0}def custom_b_step(state, _rng=rng_custom_b, _s=custom_b_state):# Pure marche aleatoire biaisee, sans force de rappel. _s["t"] +=1 base =float(state[0])return np.array([base +0.4+ _rng.normal(0, 0.1)])i_stake_custom_b = stake_index( kicked_state=do_kick(np.array([0.0]), 5.0), step_fn=custom_b_step, steps=80, anchor=0.0,)print(f"Custom B (marche biaisee) I_stake = {i_stake_custom_b:+.4f}")print(f" Attendu : ~0 (pas de force de rappel, le substrat dissipe sans defendre de soi).")
Custom A (ressort+bruit) I_stake = +0.9940
Attendu : ~+1 (force de rappel qui s'amplifie).
Custom B (marche biaisee) I_stake = -1.0000
Attendu : ~0 (pas de force de rappel, le substrat dissipe sans defendre de soi).
Calibration empirique. Les deux custom steps illustrent les extrêmes du spectre : - Custom A : I_stake ≈ +1 (enjeu fort — le substrat a un point de consigne défendu activement). - Custom B : I_stake ≈ 0 (pur dissipateur — pas d’enjeu, simple dérive biaisée).
Entre les deux : un continuum de substrats hybrides (ex: ressort faible + dérive constante). Le notebook pédagogique invite à explorer ce continuum pour développer l’intuition.
12. Verdict final — réconciliation tranches 2 et 3
Récapitulatif de l’enjeu mesuré — valeurs de CE notebook
Note d’instrument : S4 est un champ 2D ; son enjeu se mesure en espace de champ (repair_gain), pas via le scalaire stake_index (cf. docstring ict.stake). La valeur S4 (+0.82) et les valeurs scalaires (S1/S3/Custom) ne sont donc pas la même quantité numérique — mais toutes ont le même signe attendu et séparent l’agent du dissipateur.
Rappel — banc naïf scalaire du notebook principal (ICT-19-EnjeuBattery-Python.ipynb)
Pour mémoire, le notebook principal appliquait stake_index scalaire à tous les substrats continus, y compris S4 (mauvais instrument pour un champ 2D) :
Substrat
I_thermo
I_stake scalaire
S2 (bistable May)
+0.0000
+0.9999
S4 (Gray-Scott, scalarisé)
+5.6249
-0.5083 ← le faux nul corrigé ici
S5 (marche biaisée)
+0.0000
-0.4651
Son Gate ENJEU-1 y échouait (FAIL) précisément parce que (a) S4 y était scalarisé et (b) la comparaison S4-vs-S5 n’était même pas appariée en I_thermo (I_thermo(S4)=+5.62 contre I_thermo(S5)=0).
Gates falsifiables — verdict consolidé (espace de champ)
Gate ENJEU-1 : un substrat à enjeu revient vers son bassin après do(.), un pur dissipateur non. - Comparateur correctement apparié en I_thermo : la diffusion pure (même opérateur de diffusion, terme réactif retiré) dissipe comme Gray-Scott mais ne s’auto-entretient pas. - repair_gain(S4) = +0.82 ± 0.27 contre repair_gain(diffusion pure) ≈ +0.002, time_to_recover local = 313–489 pas contre jamais pour la diffusion. - Verdict : PASS. Gray-Scott (dissipe ET régénère) se sépare nettement du témoin passif I_thermo-apparié — la découpe qu’I_thermo seul (ICT-18) ne sait pas faire. C’est un gate plus propre que le S4-vs-S5 du banc naïf (qui n’appariait pas I_thermo).
Gate ENJEU-2 (graduation) : les substrats avec enjeu battent le pur dissipateur. - S1 (+1.0), Custom A (+0.994), S3 (+0.678) en scalaire et S4 (repair_gain +0.82) en champ sont tous nettement au-dessus de Custom B / S5 (-1.0). - Verdict : PASS pour la séparation famille-à-enjeu / dissipateur. L’ordre interne strict entre S4 et les substrats scalaires reste inter-instrument (S4 en champ, les autres en scalaire) : le comparer numériquement mélangerait deux quantités — c’est une limite d’homogénéité documentée, pas un échec.
Conclusion méthodologique
La batterie de l’ENJEU discrimine les substrats à enjeu (S1, S3, Custom A, et S4) du pur dissipateur (Custom B / S5), y compris S4 une fois mesuré avec le bon instrument. La leçon de la tranche 3 : un résultat nul doit d’abord être suspecté d’être un artefact d’instrument ou de câblage avant d’être consacré en limite physique. Ici, le bon instrument (repair_gain en champ) + le comparateur I_thermo-apparié (diffusion pure) + le régime de Pearson transforment un faux échec en signature d’agence nette et honnête.
13. Annexe — limites et suites
Résolu dans cette tranche
Banc S4 (résolu). Le résultat nul de la tranche 2 (I_stake(S4) ≈ I_stake(S5)) était un artefact d’instrument + de câblage, pas une limite physique. Corrections : (a) instrument de champ (agency.repair_gain) au lieu du scalaire stake_index pour le substrat 2D ; (b) appel recovery_score corrigé (ablated = champ ablaté, pas le champ relaxé) ; (c) régime de Pearson robuste (F=0.0367, k=0.0649, n=64, warmup 3000). Résultat : repair_gain = +0.82 ± 0.27, reconstruction locale en ~313–489 pas.
time_to_recover (résolu). Sur l’observable locale (variance dans le masque), le retour est net et fini (313–489 pas). L’ancien None venait de l’usage de la variance globale, insensible à une ablation locale.
Limites restantes
Axelrod asymptotique : S3 ne se fixe pas exactement sur l’ESS (fréquences oscillantes) ; I_stake mesure la tendance centrale.
Notebook squelette (tranche 2) : le Gate ENJEU-1 y reste exprimé sur stake_index scalaire pour la ligne S4 (instrument-mismatch documenté) — la réconciliation vers l’instrument de champ pour cette ligne du gate principal est un incrément de consolidation ultérieur.
Suites
ICT-20 (FeatureCatastrophes) : calibration de la triade MOYEN/FIN/ENJEU sur features d’un backbone pré-entraîné.
ICT-21 (SAETrajectoires, GPU) : features SAE Qwen comme substrat S4-bis.
ICT-23 (Persona Cusp, MERGED) : la fronce de Thom comme désalignement émergent.