ICT-15c — Méta-proxy d’obstruction : structure des désaccords entre proxys

Issue : #7395
Epic : #4588 (IIT -> ICT)

Cadrage

L’intuition directrice (lecture cohomologie-obstruction, cf. doc Grothendieck mergée le 19/07/2026) est que l’obstruction elle-même est informative : quand plusieurs proxys de complexité/intégration (spectral, sensitivity, free-energy) ne « recollent » pas en une mesure globale unique, le motif de leur désaccord porte de l’information sur le substrat.

Acceptance (issue #7395, falsifiable)

  • Positif : motif de désaccord stable cross-substrat (montrer qu’il n’est pas arbitraire — même structure de désaccord sur ≥ 2 substrats).
  • Négatif honnête : motif = bruit (pas de structure stable) → verdict négatif documenté, la jambe meurt proprement.
  • Premier livrable concret : cas où la lecture-obstruction change une décision expérimentale ou une visualisation (pas un habillage rétrospectif).

Discipline (HARD)

Ne pas nommer l’objet tant qu’il n’est pas stabilisé. Le nom (grothendieckien, « structure » et non « mesure ») vient après la stabilisation empirique, pas avant. Pas de lettre grecque décorée sur un objet non encore constaté.

Substrats pilotes (3 instrumentés)

  1. Gray-Scott (réaction-diffusion, Turing patterns) — ict.reaction_diffusion.GrayScott
  2. Axelrod (morphodynamique stratégique, fin de cycle réplicateur) — ict.strategic_morphodynamics
  3. Grokking (compression crossover) — simulé ici via marche aléatoire biaisée sur alphabet à 4 états (proxy minimal).

Livrable de cette notebook

Application du module ict.meta_proxy sur 3 substrats synthétiques, avec verdict falsifiable STABLE / NOISE / INCONCLUSIVE à la fin.

Statut épistémique — Spéculatif : Le motif de désaccord entre proxys est stable cross-substrat = obstruction informative (≠ dispersion d’échelle). Portée et détail dans la matrice de dissociations.

# Imports et substrats pilotes
import sys
from pathlib import Path

ICT_ROOT = Path('.').resolve()
sys.path.insert(0, str(ICT_ROOT))

import numpy as np
import matplotlib
matplotlib.use('Agg')  # rendu PNG en batch
import matplotlib.pyplot as plt

from ict import spectral as SP
from ict import sensitivity as SE
from ict import reaction_diffusion as RD
from ict import strategic_morphodynamics as SM
from ict import bistable as BS  # May (ICT-8)
from ict.meta_proxy import (
    proxy_signature,
    obstruction_vector,
    cross_substrat_obstruction,
)

np.random.seed(20260720)  # determinisme cross-execution
print("meta_proxy loaded. Substrats pilotes : Gray-Scott, Axelrod, Grokking, May (ICT-8).")
meta_proxy loaded. Substrats pilotes : Gray-Scott, Axelrod, Grokking, May (ICT-8).
# --- Substrat 1 : Gray-Scott Turing patterns ---
# API reelle de ict.reaction_diffusion.GrayScott :
#   - constructeur prend F, k, Du, Dv, dt (PAS de `size`)
#   - seed(n=64, rng=...) -> (U, V) initiaux
#   - run(U, V, steps) -> (U, V, snapshots)
# On integre 800 pas avec F/k dans la fenetre Turing pattern (F=0.035, k=0.065).
gs = RD.GrayScott(F=0.035, k=0.065, Du=0.16, Dv=0.08, dt=1.0)
seed_rng = np.random.default_rng(20260720)
U_init, V_init = gs.seed(n=64, rng=seed_rng)
U_final, V_final, _ = gs.run(U_init, V_init, steps=800)

# Observable derivee du substrat : on discretise U (et non V, qui collapse a 0
# avec ces parametres dans la fenetre 800 steps) en 4 quantiles. La trajectoire
# discrete porte alors l'information spatiale reelle du motif Gray-Scott.
u = U_final
u_q = np.quantile(u.flatten(), np.linspace(0, 1, 5)[1:-1])
gray_scott_states = np.digitize(u.flatten(), u_q).tolist()
n_symbols_gs = 4  # déclaré = 4 bins ; effectif = 1 (U constant, tous les pixels dans le même bin ; cf. range [1.0000, 1.0000])
print(f"Gray-Scott : U range=[{u.min():.4f}, {u.max():.4f}], "
      f"n_symbols={n_symbols_gs}, len(states)={len(gray_scott_states)}")
# --- Substrat 1 : Gray-Scott Turing patterns ---
# API reelle de ict.reaction_diffusion.GrayScott :
#   - constructeur prend F, k, Du, Dv, dt (PAS de `size`)
#   - seed(n=64, rng=...) -> (U, V) initiaux
#   - run(U, V, steps) -> (U, V, snapshots)
# On integre 800 pas avec F/k dans la fenetre Turing pattern (F=0.035, k=0.065).
gs = RD.GrayScott(F=0.035, k=0.065, Du=0.16, Dv=0.08, dt=1.0)
seed_rng = np.random.default_rng(20260720)
U_init, V_init = gs.seed(n=64, rng=seed_rng)
U_final, V_final, _ = gs.run(U_init, V_init, steps=800)

# Binarisation : pixels V > 0.5 en 'pattern' (1), sinon 'background' (0)
v = V_final
binary_grid = (v > 0.5).astype(int)
n_pattern = int(binary_grid.sum())
n_bg = int((1 - binary_grid).sum())
ratio = n_pattern / (n_pattern + n_bg)
print(f"Gray-Scott : pattern={n_pattern}, background={n_bg}, ratio={ratio:.3f}")

# Trajectoire discrete : on scan les pixels ligne par ligne -> sequence de 0/1
gray_scott_states = binary_grid.flatten().tolist()
n_symbols_gs = 2
print(f"Longueur trajectoire Gray-Scott : {len(gray_scott_states)} pixels")
Gray-Scott : U range=[1.0000, 1.0000], n_symbols=4, len(states)=4096
Gray-Scott : pattern=0, background=4096, ratio=0.000
Longueur trajectoire Gray-Scott : 4096 pixels

Lecture 1 – Gray-Scott par défaut : le substrat plat comme contrôle

La cellule ci-dessus contient deux essais. Le premier (quantiles de U) est disqualifié par sa propre sortie : U range=[1.0000, 1.0000] — le champ U est constant à 4 décimales après 800 pas, les quatre quantiles coïncident et la « trajectoire » discrète est un symbole unique répété 4096 fois. Le second (binarisation de V) est plus honnête : pattern=0, background=4096, ratio=0.000 — aucun pixel ne dépasse V > 0.5, la grille 64×64 reste uniforme (le commentaire de la cellule le prédisait : V « collapse à 0 avec ces paramètres » dans la fenêtre 800 pas, avec F = 0.035 / k = 0.065).

Conséquence pour la suite : la signature Gray-Scott (spectral_gap = 0.5000, sensibilité 0.0000/0.0000) est celle d’une séquence constante, pas d’un motif de Turing. Le banc le garde néanmoins — comme contrôle trivial : un substrat dont l’observable ne varie pas fixe l’extrémité basse de l’échelle des proxys, et tout vecteur d’obstruction qui l’implique doit être lu avec cette réserve. C’est aussi un rappel expérimental : la fenêtre d’intégration et les paramètres (F, k) font partie du substrat testé.

# --- Substrat 2 : Axelrod end-of-cycle replicator ---
# API reelle de ict.strategic_morphodynamics :
#   - make_strategies(rng) -> Dict[str, Strategy]
#   - payoff_matrix(strategies, n_rounds, n_reps, rng) -> A  (matrice de gain)
#   - replicator_trajectory(A, x0, n_steps) -> traj (n_steps+1, n_strategies)
# On calibre la matrice de gain puis on integre la dynamique.
rng = np.random.default_rng(20260720)
strategies = SM.make_strategies(rng)
A = SM.payoff_matrix(strategies, n_rounds=200, n_reps=3, rng=rng)
n_strat = A.shape[0]
# Frequences initiales uniformes (chaque strategie a 1/n_strat).
x0 = np.full(n_strat, 1.0 / n_strat)
traj = SM.replicator_trajectory(A, x0, n_steps=400)

# Trajectoire discrete : index de la strategie dominante a chaque pas
# (observable derivee du substrat, alphabet = n_strat). L'evolution du
# dominant index porte la dynamique de selection, pas seulement sa stabilite.
axelrod_states = np.argmax(traj, axis=1).tolist()
n_symbols_ax = n_strat
print(f"Axelrod : trajectoire de {len(axelrod_states)} pas "
      f"(dominant strategy index, alphabet={n_symbols_ax}).")
# --- Substrat 2 : Axelrod end-of-cycle replicator ---
# API reelle de ict.strategic_morphodynamics :
#   - make_strategies(rng) -> Dict[str, Strategy]
#   - payoff_matrix(strategies, n_rounds, n_reps, rng) -> A  (matrice de gain)
#   - replicator_trajectory(A, x0, n_steps) -> traj (n_steps+1, n_strategies)
# On calibre la matrice de gain puis on integre la dynamique.
rng = np.random.default_rng(20260720)
strategies = SM.make_strategies(rng)
A = SM.payoff_matrix(strategies, n_rounds=200, n_reps=3, rng=rng)
n_strat = A.shape[0]
# Frequences initiales uniformes (chaque strategie a 1/n_strat).
x0 = np.full(n_strat, 1.0 / n_strat)
traj = SM.replicator_trajectory(A, x0, n_steps=400)

# Trajectoire discrete : on binarise la stabilite de la dominance.
# Pour chaque pas, on prend l'index de la strategie dominante, puis on
# enregistre 1 si elle reste dominante au pas suivant, 0 sinon.
dom_idx = np.argmax(traj, axis=1)
axelrod_states = [int(dom_idx[i] == dom_idx[i - 1]) for i in range(1, len(dom_idx))]
n_symbols_ax = 2
print(f"Axelrod : trajectoire de {len(axelrod_states)} pas (stabilité de la dominance).")
Axelrod : trajectoire de 401 pas (dominant strategy index, alphabet=6).
Axelrod : trajectoire de 400 pas (stabilité de la dominance).
# --- Substrat 3 : Grokking compression crossover (proxy minimal) ---
# Marche aléatoire biaisée : phase 1 = haute entropie (alphabet 4 etats),
# phase 2 = compression vers un etat attracteur (un seul etat visité).
# Le crossover est placé au pas 200 (milieu de trajectoire).
n_steps_g = 400
states_g = []
for t in range(n_steps_g):
    if t < 200:
        # Phase haute entropie : equiprobable parmi 4 etats
        s = int(rng.integers(0, 4))
    else:
        # Phase compression : 90% du temps sur l'etat 0, 10% sur les autres
        if rng.random() < 0.90:
            s = 0
        else:
            s = int(rng.integers(1, 4))
    states_g.append(s)
grokking_states = states_g
n_symbols_gk = 4
print(f"Grokking : trajectoire de {len(grokking_states)} pas, crossover au pas 200.")
Grokking : trajectoire de 400 pas, crossover au pas 200.
# --- Substrat 4 : May (ICT-8, modele de paturage bistable) ---
# Equation : dx/dt = r*x*(1 - x/K) - c*x^2/(x^2 + h^2).
# On simule en regime bistable (c=1.5, sous le pli c_fold ~ 2.4) avec un
# bruit additif (SDE Euler-Maruyama), puis on discretise la biomasse en
# 16 quantiles. Observable derivee du substrat : la biomasse de vegetation
# elle-meme, mesuree sur T=2000 pas a dt=0.01.
gm = BS.GrazingModel(r=1.0, K=10.0, h=1.0)
xs_may = gm.simulate_sde(c=1.5, x0=8.0, sigma=0.05, dt=0.01,
                         T=2000, seed=20260720)
may_q = np.quantile(xs_may, np.linspace(0, 1, 17)[1:-1])
may_states = np.digitize(xs_may, may_q).tolist()
n_symbols_may = 16
print(f"May : x range=[{xs_may.min():.4f}, {xs_may.max():.4f}], "
      f"n_symbols={n_symbols_may}, len(states)={len(may_states)}")
May : x range=[7.9923, 8.3613], n_symbols=16, len(states)=2000

Lecture 2 – Le banc des quatre régimes, lu à partir des trajectoires

Avant les proxys, lire les trajectoires. La cellule ci-dessus fait pour Axelrod deux essais ; la version retenue (dernière affectation, celle que les signatures utilisent) est la stabilité de la dominance : 400 pas, alphabet binaire stable/bascule — une trajectoire d’alternance, pas de niveaux. May (SDE de pâturage bistable, c = 1.5) reste dans un canal étroit : x range=[7.9923, 8.3613] sur 2000 pas — amplitude 0.369 — tout en discrétisant 16 quantiles : la biomasse fluctue autour d’un seul attracteur, elle ne traverse pas le pli dans cette fenêtre. Grokking est une marche biaisée de 400 pas avec crossover au pas 200 : phase 1 haute entropie (équiprobable sur 4 états), phase 2 compression (90 % sur l’état 0). Gray-Scott est plat (Lecture 1).

Le contraste est volontaire (cellule « Le banc de quatre substrats ») : un canal quasi-constant, une alternance binaire, une transition de phase, une constante. Si un motif de désaccord entre proxys se retrouve transversalement à ces quatre régimes — de l’alphabet à 2 symboles au 16-quantiles, du spatial au temporel —, il ne peut pas être un artefact de domaine : c’est précisément la question falsifiable du méta-proxy.

Le banc de quatre substrats : diversifier les régimes pour tester la cohérence

Les quatre substrats ci-dessus ne sont pas quatre instances d’une même dynamique : ce sont quatre régimes hétérogènes, chacun converti en une trajectoire discrète sur un alphabet propre.

Substrat Régime Origine de la trajectoire Alphabet
Gray-Scott motif spatial émergent réaction-diffusion 2D, motif de Turing binarisé 2 (pattern / fond)
Axelrod dynamique stratégique replicateur end-of-cycle, stabilité du dominant 2 (stable / bascule)
Grokking transition de phase marche biaisée, crossover haute-entropie vers compression 4
May (ICT-8) pâturage bistable SDE bistable, biomasse quantifiée 16

Cette diversité est volontaire : c’est le banc de test du méta-proxy. Si la structure des désaccords entre proxys se révèle cohérente d’un régime à l’autre — de la maille binaire au 16-quantiles, du motif spatial à la transition d’apprentissage — c’est la signature d’un invariant portable : l’obstruction mesure une propriété réelle du substrat, pas un artefact de tel ou tel domaine. À l’inverse, si chaque régime produit ses propres désaccords idiosyncratiques, le méta-proxy n’est pas robuste. C’est la question falsifiable que tranche le verdict ci-dessous.

L’étape suivante projette chaque substrat sur une signature à trois proxys : le spectral gap (structure globale du graphe de transition) et la sensibilité moyenne et maximale (réponse locale à la perturbation d’un symbole).

# --- Calcul des signatures multi-proxys ---
# On enrobe les fonctions ict.spectral / ict.sensitivity pour matcher
# le prototype (states, n_symbols) -> float attendu par meta_proxy.
def spec_gap_wrap(states, n_symbols):
    return float(SP.spectral_summary(states, n_symbols)['spectral_gap'])

def sens_mean_wrap(states, n_symbols):
    # f(x) = x : identite sur le vocabulaire (l'observable derivee du
    # substrat est portee par le label ordinal du noeud, pas par une
    # fonction-test). C'est la cle de la desaturation : avec f(x)=x%2,
    # l'instrument saturait a 1.0 sur alphabet binaire et 2.0 sur alphabet
    # quaternaire (deux valeurs distinctes seulement, mean==max partout).
    f = lambda x: x
    return float(SE.sensitivity_distribution(states, n_symbols, f)['mean'])

def sens_max_wrap(states, n_symbols):
    f = lambda x: x
    return float(SE.sensitivity_distribution(states, n_symbols, f)['max'])

sig_gs = proxy_signature(
    gray_scott_states, n_symbols_gs,
    spectral_fn=spec_gap_wrap,
    sensitivity_mean_fn=sens_mean_wrap,
    sensitivity_max_fn=sens_max_wrap,
)
sig_ax = proxy_signature(
    axelrod_states, n_symbols_ax,
    spectral_fn=spec_gap_wrap,
    sensitivity_mean_fn=sens_mean_wrap,
    sensitivity_max_fn=sens_max_wrap,
)
sig_gk = proxy_signature(
    grokking_states, n_symbols_gk,
    spectral_fn=spec_gap_wrap,
    sensitivity_mean_fn=sens_mean_wrap,
    sensitivity_max_fn=sens_max_wrap,
)
sig_may = proxy_signature(
    may_states, n_symbols_may,
    spectral_fn=spec_gap_wrap,
    sensitivity_mean_fn=sens_mean_wrap,
    sensitivity_max_fn=sens_max_wrap,
)

print("=== Signatures multi-proxys (4 substrats, state_function=identite) ===")
for nom, sig in [("gray_scott", sig_gs), ("axelrod", sig_ax),
                  ("grokking", sig_gk), ("may", sig_may)]:
    print(f"{nom:>12}: spectral_gap={sig['spectral_gap']:.4f}, "
          f"sens_mean={sig['sensitivity_mean']:.4f}, "
          f"sens_max={sig['sensitivity_max']:.4f}")
=== Signatures multi-proxys (4 substrats, state_function=identite) ===
  gray_scott: spectral_gap=0.5000, sens_mean=0.0000, sens_max=0.0000
     axelrod: spectral_gap=1.0284, sens_mean=1.0000, sens_max=1.0000
    grokking: spectral_gap=0.7766, sens_mean=3.0000, sens_max=3.0000
         may: spectral_gap=0.0067, sens_mean=2.6250, sens_max=4.0000

Lecture 3 – Désaccord des proxys : deux classements, presque inverses

Le tableau des signatures (fonction d’état identité — l’observable dérivée du substrat, pas une fonction-test bivalente) montre un désaccord de classement entre les proxys. Par gap spectral décroissant : axelrod (1.0284) > grokking (0.7766) > gray_scott (0.5000) > may (0.0067). Par sensibilité maximale décroissante : may (4.0000) > grokking (3.0000) > axelrod (1.0000) > gray_scott (0.0000). Deux proxys, deux ordres presque inverses : le substrat le plus « régulier » spectralement (axelrod, alternance binaire périodique) est le plus pauvre en sensibilité, et le moins régulier spectralement (may, canal étroit quasi-constant) est le plus sensible.

Le contraste interne le plus parlant est May : spectral_gap = 0.0067 — de loin le plus petit du banc, ce qu’un observateur spectral lirait comme une chaîne très lente à mélanger — contre sens_max = 4.0000 : la même trajectoire est « presque sans structure » pour un proxy et « extrêmement sensible » pour l’autre. Le vecteur d’obstruction formalise ce désaccord ; il ne le résout pas.

# --- Vecteurs d'obstruction pairwise + aggregation cross-substrat ---
substrats = {"gray_scott": sig_gs, "axelrod": sig_ax,
                "grokking": sig_gk, "may": sig_may}
verdict = cross_substrat_obstruction(substrats)

print("=== Vecteurs d'obstruction (a - b normalise) ===")
noms = list(substrats.keys())
for i in range(len(noms)):
    for j in range(i + 1, len(noms)):
        vec = obstruction_vector(substrats[noms[i]], substrats[noms[j]])
        print(f"  {noms[i]:>10} vs {noms[j]:<10}: "
              f"spectral={vec['spectral_gap']:+.3f}, "
              f"sens_mean={vec['sensitivity_mean']:+.3f}, "
              f"sens_max={vec['sensitivity_max']:+.3f}, "
              f"||v||₂={vec['norm_l2']:.3f}")

print(f"\n=== Verdict cross-substrat ===")
print(f"  n_substrats = {verdict['n_substrats']}")
print(f"  pairwise_norms = {[round(x, 4) for x in verdict['pairwise_norms']]}")
print(f"  mean_norm_l2  = {verdict['mean_norm_l2']:.4f}")
print(f"  max_norm_l2   = {verdict['max_norm_l2']:.4f}")
print(f"  STABLE_threshold = {verdict['stable_threshold']}, "
      f"NOISE_threshold = {verdict['noise_threshold']}")
print(f"  VERDICT = {verdict['verdict']}")
=== Vecteurs d'obstruction (a - b normalise) ===
  gray_scott vs axelrod   : spectral=-0.346, sens_mean=-1.000, sens_max=-1.000, ||v||₂=1.456
  gray_scott vs grokking  : spectral=-0.217, sens_mean=-1.000, sens_max=-1.000, ||v||₂=1.431
  gray_scott vs may       : spectral=+0.974, sens_mean=-1.000, sens_max=-1.000, ||v||₂=1.717
     axelrod vs grokking  : spectral=+0.140, sens_mean=-0.500, sens_max=-0.500, ||v||₂=0.721
     axelrod vs may       : spectral=+0.987, sens_mean=-0.448, sens_max=-0.600, ||v||₂=1.239
    grokking vs may       : spectral=+0.983, sens_mean=+0.067, sens_max=-0.143, ||v||₂=0.996

=== Verdict cross-substrat ===
  n_substrats = 4
  pairwise_norms = [1.4559, 1.4307, 1.717, 0.7207, 1.2391, 0.9955]
  mean_norm_l2  = 1.2598
  max_norm_l2   = 1.7170
  STABLE_threshold = 0.05, NOISE_threshold = 0.3
  VERDICT = NOISE

Lecture 4 – Le verdict : NOISE, un motif de désaccord qui ne tient pas

Les seuils de la décision sont affichés avec le résultat : STABLE_threshold = 0.05, NOISE_threshold = 0.3, VERDICT = NOISE. La moyenne des normes pairwise vaut 1.2598 — 4,2 fois le seuil NOISE, 25 fois le seuil STABLE — et la paire la plus proche (axelrod vs grokking, 0.7207) reste 2,4 fois au-dessus du seuil NOISE. Verdict négatif honnête : les six vecteurs d’obstruction sont grands, aucun ne « recolle » les substrats, et le motif n’est pas assez fin pour distinguer une structure de désaccord d’un artefact d’échantillonnage. La jambe meurt proprement — c’est la discipline de nommage de l’ouverture qui s’applique : pas d’objet nommé derrière ce motif, tant qu’il n’est pas stable.

Le contenu des vecteurs suggère où le désaccord se concentre. Trois paires sur six ont une composante sens_mean à −1.000 — exactement les trois paires qui impliquent gray_scott, le substrat à sensibilité nulle. La composante spectrale est la plus petite en valeur absolue sur toutes les paires (de 0.140 à 0.987) mais elle porte le plus grand écart : gray_scott vs may, +0.974, la paire la plus éloignée (||v||₂ = 1.717, le max du banc). Deux sources de désaccord, une triviale (le substrat plat contre tous les autres) et une structurelle (l’opposition spectrale maximale entre le plat et le canal étroit) — la moyenne les confond, et c’est bien ce que le verdict NOISE enregistre.

Exercice 1 — Quatrième substrat : impact sur le verdict cross-substrat

Le notebook agrège trois substrats (Gray-Scott, Axelrod, Grokking) via cross_substrat_obstruction et obtient un verdict (STABLE / NOISE / INCONCLUSIVE) fondé sur la norme L2 moyenne des vecteurs d’obstruction.

Objectif. Construire une quatrième trajectoire très désordonnée — une marche aléatoire pure sur 4 symboles, sans crossover ni motif — puis l’ajouter au panel et relire le verdict. Un substrat dominé par le bruit doit-il pousser le verdict vers NOISE, ou le motif cross-substrat reste-t-il STABLE malgré lui ?

Indices. - Réutilisez les wrappers spec_gap_wrap, sens_mean_wrap, sens_max_wrap définis dans la cellule « Signatures multi-proxys » (proxys comparables). - proxy_signature(states, n_symbols, spectral_fn=..., sensitivity_mean_fn=..., sensitivity_max_fn=...) renvoie le dict signature. - Reconstruisez substrats_4 = {**substrats, "bruit": sig_bruit} puis appelez cross_substrat_obstruction(substrats_4). - Comparez verdict_4["verdict"] et verdict_4["mean_norm_l2"] au verdict committé sur 3 substrats (verdict).

# Exercice 1 — Quatrième substrat (bruit pur) et impact sur le verdict.
# TODO étudiant : générer ~400 pas d'une marche aléatoire uniforme sur 4 symboles
# (np.random.default_rng(...).integers(0, 4, size=400)).
bruit_states = None
n_symbols_bruit = 4

# TODO étudiant : calculer la signature proxy du substrat bruit avec les
# MÊMES wrappers que les trois autres (spec_gap_wrap, sens_mean_wrap, sens_max_wrap).
sig_bruit = None

# TODO étudiant : agréger les 4 substrats et lire le nouveau verdict.
# substrats_4 = {**substrats, "bruit": sig_bruit}
# verdict_4 = cross_substrat_obstruction(substrats_4)
verdict_4 = None

print("Exercice 1 à compléter — verdict 4 substrats attendu ici.")
Exercice 1 à compléter — verdict 4 substrats attendu ici.
# --- Visualisation : heatmap des pairwise norms ---
n = len(noms)
M = np.zeros((n, n))
k = 0
for i in range(n):
    for j in range(i + 1, n):
        M[i, j] = verdict['pairwise_norms'][k]
        M[j, i] = M[i, j]
        k += 1

fig, ax = plt.subplots(figsize=(6, 5))
im = ax.imshow(M, cmap='viridis', vmin=0, vmax=max(verdict['noise_threshold'], 0.5))
ax.set_xticks(range(n))
ax.set_yticks(range(n))
ax.set_xticklabels(noms, rotation=20, ha='right')
ax.set_yticklabels(noms)
for i in range(n):
    for j in range(n):
        if i != j:
            ax.text(j, i, f"{M[i, j]:.3f}", ha='center', va='center',
                    color='white' if M[i, j] < 0.3 else 'black', fontsize=9)
ax.set_title(f"Obstruction pairwise ||v||₂ — verdict : {verdict['verdict']}")
plt.colorbar(im, ax=ax, label='||v||₂ norm')
plt.tight_layout()
plt.savefig('ICT-15c-obstruction-heatmap.png', dpi=110, bbox_inches='tight')
plt.close()
print("Heatmap sauvegardee : ICT-15c-obstruction-heatmap.png")
Heatmap sauvegardee : ICT-15c-obstruction-heatmap.png

Lecture 5 – La heatmap : un banc entier au-dessus de la palette

La heatmap donne à lire une conclusion que le tableau ne montre pas : la palette est bornée par max(noise_threshold, 0.5) (voir le code de la cellule), donc à 0.5 ici — et les six normes pairwise (de 0.7207 à 1.7170) dépassent toutes cette borne. Toute la carte est saturée dans la couleur maximale : aucune paire n’apparaît « proche », aucune ne se distingue — la visualisation dit dès le premier regard que le banc entier est en désaccord.

Pour discriminer, il faudrait une palette portée à la gamme réelle des normes (par exemple un vmax autour de 1.717, le max du banc) ; à palette fixée sur le seuil, le « triangle chaud » des trois paires impliquant gray_scott (1.456, 1.431, 1.717) est indiscernable du reste. C’est une direction d’amélioration de la visualisation à noter pour les prochains substrats — pas une correction du verdict : le NOISE de la Lecture 4 ne dépend pas de la palette.

Exercice 2 — Sensibilité du verdict aux seuils stable / noise

Le verdict est une décision à deux seuils : STABLE si mean_norm_l2 <= stable_threshold (défaut 0.05), NOISE si mean_norm_l2 >= noise_threshold (défaut 0.30), INCONCLUSIVE entre les deux. La valeur committée verdict["mean_norm_l2"] est fixée par les trois substrats — mais la classification dépend du choix des seuils.

Objectif. Pour le mean_norm_l2 observé, déterminer la plage de stable_threshold qui ferait basculer le verdict vers STABLE, et la plage de noise_threshold qui le ferait basculer vers NOISE. Cela caractérise la robustesse de la conclusion : un verdict qui change pour un seuil à ±0.01 est fragile ; un verdict invariant sur [0.01, 0.20] est solide.

Indices. - Le verdict se calcule par cross_substrat_obstruction(substrats, stable_threshold=s, noise_threshold=n) — les seuils sont des arguments. - Balayez par ex. stable_threshold dans np.linspace(0.01, 0.50, 25) avec noise_threshold fixé haut (0.99) pour isoler le basculement STABLE. - Notez le plus petit stable_threshold pour lequel verdict == "STABLE".

# Exercice 2 — Robustesse du verdict au choix des seuils.
mean_norm_committed = verdict["mean_norm_l2"]  # observable fixée par les 3 substrats
print(f"mean_norm_l2 committé = {mean_norm_committed:.4f}")

# TODO étudiant : balayer stable_threshold (noise_threshold fixé à 0.99 pour
# isoler le basculement vers STABLE) et identifier le seuil critique.
bascule_stable = None  # plus petit stable_threshold donnant verdict == "STABLE"

# TODO étudiant (extension) : balayer de même noise_threshold (stable fixé à 0.0)
# pour le basculement vers NOISE.
bascule_noise = None

print("Exercice 2 à compléter — seuils de basculement attendus ici.")
mean_norm_l2 committé = 1.2598
Exercice 2 à compléter — seuils de basculement attendus ici.

Exercice 3 — Vérifier l’insensibilité à l’échelle de obstruction_vector

La docstring de obstruction_vector revendique une normalisation par |sig_a[k]| + |sig_b[k]| qui rend le vecteur insensible à l’échelle globale : deux substrats dont les proxys diffèrent d’un facteur 10 doivent donner le même vecteur d’obstruction qu’à l’échelle 1. C’est une propriété falsifiable — vérifions-la.

Objectif. (a) Construire sig_gs_x10 = sig_gs avec chaque proxy multiplié par 10. (b) Vérifier que obstruction_vector(sig_gs, sig_gs_x10) est quasi nul (deux signatures proportionnelles → obstruction nulle). (c) Vérifier que obstruction_vector(sig_gs, sig_ax) ≈ obstruction_vector(sig_gs_x10, sig_ax) (le vecteur cross-substrat est inchangé par la remise à l’échelle d’un seul côté).

Indices. - sig_gs est un dict plat {spectral_gap, sensitivity_mean, sensitivity_max, ...}. Construisez sig_gs_x10 = {k: v*10 for k, v in sig_gs.items() if isinstance(v, (int, float))}. - obstruction_vector(a, b) renvoie un dict dont la clé norm_l2 résume l’amplitude ; comparez les norm_l2.

# Exercice 3 — Scale-invariance du vecteur d'obstruction (propriété falsifiable).
# TODO étudiant : construire sig_gs_x10 (chaque proxy x10).
sig_gs_x10 = None

# TODO étudiant : (b) obstruction entre sig_gs et sa version x10 — attendu ~0.
vec_proportionnel = None

# TODO étudiant : (c) comparer obstruction(sig_gs, sig_ax) vs obstruction(sig_gs_x10, sig_ax).
vec_echelle1 = None
vec_echelle10 = None

print("Exercice 3 à compléter — vérification de l'insensibilité à l'échelle.")
Exercice 3 à compléter — vérification de l'insensibilité à l'échelle.

Interprétation

Substrats testés : Gray-Scott (U quantifiée en 4 niveaux sur grille 64x64 intégrée 800 pas), Axelrod (index de la stratégie dominante, alphabet dynamique = nombre de stratégies), Grokking (marche aléatoire biaisée, compression crossover à mi-parcours), May (biomasse du modèle de pâturage bistable discrétisée en 16 quantiles, régime sous le pli).

Proxys calculés : spectral_gap (gap spectral du Laplacien du graphe de transition), sensitivity_mean et sensitivity_max (sur fonction identité f(x) = x - l’observable dérivée du substrat, pas une fonction-test bivalente). Avec f(x) = x%2 (livraison #7578), l’instrument saturait à mean=max=1.0 sur alphabet binaire et mean=max=2.0 sur alphabet quaternaire : la discrimination cross-substrat était artefactuelle. L’identité restaure un panel de valeurs non-plafonné.

Vecteur d’obstruction : (a - b) / (|a| + |b| + eps) — invariant à l’échelle globale, antisymétrique (obstruction(a, b) = -obstruction(b, a)).

Verdict falsifiable : - STABLE : motif de désaccord cohérent (mean_norm_l₂ ≤ 0.05) sur ≥ 2 substrats — preuve empirique de l’objet. - NOISE : motif aléatoire (mean_norm_l₂ ≥ 0.30) — verdict négatif honnête, la jambe meurt proprement. - INCONCLUSIVE : entre les deux seuils — recalibrer ou augmenter l’échantillon.

Suite logique (issue #7395)

Premier livrable concret = au moins un cas où la lecture-obstruction change une décision expérimentale ou une visualisation (issue #7395, acceptance positive). Si le verdict est STABLE ici : à documenter ; si NOISE : à documenter aussi (verdict honnête). Si INCONCLUSIVE : à recalibrer les seuils ou augmenter la taille d’échantillon.

Discipline de nommage (HARD)

L’objet n’est pas encore nommé. Le vocabulaire utilisé reste descriptif (obstruction vector, pairwise norm, STABLE / NOISE). Aucune lettre grecque décorée sur un objet non encore constaté — la stabilisation empirique précède le baptême.

Retour au sommet