ICT-15l — Indépendance au générateur de nouveauté : le relief b1 survit-il au contrôle de dimension ?

Issue : #13308 Epic (Observatoire des formes relationnelles) : #13303 Substrat Axelrod régénéré : #12673 Discriminant nerf/b1 : ICT-15j

Cadrage

Sur le substrat Axelrod régénéré (#12673), le discriminant topologique d’ICT-15j mesure du relief : b1_max_persistence = 0,3989 pour une population agent-based en sélection-mutation (graine 20260720), contre 0,0000 pour la baseline dégénérée replicateur. La lecture tentante — « la nouveauté stratégique produit du relief là où le bruit n’en produit pas » — est exactement la frontière que l’énoncé général ne peut pas franchir sans un contrôle : le mécanisme qui injecte la nouveauté peut n’être que le relief qu’on a mis dans le générateur en le construisant (#13308).

Ce notebook exécute le protocole pré-enregistré de l’issue :

  1. deux générateurs de nouveauté structurellement distincts (resemencement uniforme sur catalogue fermé vs dérive comportementale par mutation de bits d’un génome — vocabulaire ouvert) ;
  2. un contrôle négatif de dimension : le simplexe est doublé d’étiquettes sans aucun comportement nouveau (chaque stratégie dupliquée sous deux noms) — s’il produit aussi du relief, le discriminant ne mesure pas la nouveauté mais la dimension/le turnover ;
  3. appariement : même substrat IPD, même bruit, même taille de population, même longueur de trajectoire, même taux de mutation, graines identiques entre régimes ;
  4. verdict en toutes lettres, pré-enregistré avant la mesure.

Règle de verdict pré-enregistrée

Avec relief(r) = moyenne sur les 3 graines de b1_max_persistence, seuil τ = 0,05 (constante TAU dans le code, tolerance du verdict ICT-15j) :

condition verdict
relief(R1) > τ et relief(R2) > τ et relief(R3) ≤ τ ROBUSTE_AU_GENERATEUR
exactement un de relief(R1), relief(R2) > τ (avec R0 ≤ τ) ARTEFACT_DE_GENERATEUR
relief(R3) > τ CONFONDU_AVEC_LA_DIMENSION
aucun des cas ci-dessus INCONCLUSIVE

La règle est appliquée programmatiquement dans la cellule §6 — le verdict imprimé est calculé, jamais écrit à la main.

Discipline

Pas de promotion en « loi ICT » quel que soit le verdict : la promotion est un acte séparé qui demande l’observatoire externe (#13303). La mesure reste descriptive : « nombre de classes H^1 persistantes dans le nerf simplicial sur les sections locales des proxys ». On ne nomme pas l’objet.

Statut épistémique — Sans verdict à ce jour : aucune ligne de la matrice de dissociations ne concerne ce notebook ; son statut épistémique sera porté par la matrice le cas échéant.

# Imports + recette observable, REPLIQUE EXACTE d'ICT-15j (cellules f22b81e8/ff929034)
import sys, time
from pathlib import Path

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

import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

from ict import spectral as SP
from ict import sensitivity as SE
from ict import strategic_morphodynamics as SM
from ict.nerve_discriminant import nerve_b1

C, D = 1, 0

def spec_gap_wrap(states, n_symbols):
    return float(SP.spectral_summary(states, n_symbols)['spectral_gap'])

def sens_mean_wrap(states, n_symbols):
    return float(SE.sensitivity_distribution(states, n_symbols, lambda x: x)['mean'])

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

PROXIES_FN = {
    'spectral_gap': spec_gap_wrap,
    'sensitivity_mean': sens_mean_wrap,
    'sensitivity_max': sens_max_wrap,
}

def windowed_proxy_signature(states, n_symbols, n_windows=30):
    """30 fenetres glissantes x 3 proxys -- meme recette qu'ICT-15j."""
    L = len(states)
    sections = {name: [] for name in PROXIES_FN}
    for w in range(n_windows):
        start = (w * L) // n_windows
        end = max(start + 2, ((w + 1) * L) // n_windows)
        chunk = states[start:end]
        if len(chunk) < 2:
            chunk = states[start:start + 4] if start + 4 <= L else states[start:]
        for name, fn in PROXIES_FN.items():
            try:
                sections[name].append(fn(chunk, n_symbols))
            except Exception:
                sections[name].append(float('nan'))
    cleaned = {}
    for name, vals in sections.items():
        arr = np.array(vals, dtype=float)
        valid = arr[np.isfinite(arr)]
        cleaned[name] = (valid.tolist() if len(valid) > 0 else [0.0] * n_windows)
    for name in cleaned:
        while len(cleaned[name]) < n_windows:
            cleaned[name].append(0.0)
    return cleaned

def coop_to_b1(coop, name, n_symbols=8):
    """Serie de cooperation -> 8 symboles (quantiles) -> sections -> b1_max_persistence.

    Chaque regime passe par CETTE fonction et aucune autre : l'observable est
    identique entre regimes, seul le generateur change.
    """
    q = np.quantile(coop, np.linspace(0, 1, n_symbols + 1)[1:-1])
    states = np.digitize(coop, q).tolist()
    sections = windowed_proxy_signature(states, n_symbols, n_windows=30)
    r = nerve_b1(sections, name, epsilon_quantile=0.55)
    return r.b1_max_persistence, len(set(states))

print("recette observable chargee (replique ICT-15j).")
recette observable chargee (replique ICT-15j).

1. Les quatre régimes et leur appariement

régime mécanisme vocabulaire dimension du simplexe nouveauté
R0 replicateur-mutation déterministe (replicator_mutation_trajectory) fermé (6 stratégies) 6 aucune (bruit uniforme mu/n)
R1 agent-based Wright-Fisher, resemencement uniforme (evolve_population, #12673) fermé (6 stratégies) 6 turnover de composition (catalogue fermé)
R2 agent-based, dérive comportementale : génomes 5 bits, mutation par inversion de bit ouvert (32 comportements possibles) croît avec les mutants comportements nouveaux hors catalogue initial
R3 agent-based Wright-Fisher, étiquettes dupliquées par blocs (6 comportements × 2 noms) fermé (mêmes 6 comportements) 12 aucune (contrôle négatif)

Ce que chaque générateur fait au vocabulaire : - R1 tire les mutants uniformément dans un catalogue fixe — un type absent peut réapparaître, mais aucun comportement nouveau n’existe ; - R2 mute localement le code d’un comportement existant (1 bit du génome) — les mutants sont des variantes de voisins, et la plupart n’existaient pas dans la population initiale ; - R3 double les étiquettes sans toucher aux comportements — la dimension du simplexe monte de 6 à 12, la distribution de mutation sur les comportements est inchangée.

Appariement (montré, pas affirmé — imprimé par la cellule suivante) : même substrat IPD canonique, même bruit d’exécution noise = 0.02, pop = 60, n_generations = 400, n_rounds = 30, mutation_rate = 0.05, graines [20260720, 42, 7] identiques entre régimes. R0 est le régime déterministe (champ moyen) : l’appariement porte sur ses parties stochastiques (construction de la matrice de gains, estimation de la coopération sous bruit).

# Constantes appariees -- IDENTIQUES pour les 4 regimes
SEEDS = [20260720, 42, 7]
POP, GENS, ROUNDS, NOISE, MU = 60, 400, 30, 0.02, 0.05
TAU = 0.05   # seuil de verdict (tolerance ICT-15j), pre-enregistre en tete de notebook

print("=== Protocole d'appariement (commun aux 4 regimes) ===")
print(f"  substrat            : IPD canonique (payoff_pair de ict.strategic_morphodynamics)")
print(f"  bruit d'execution   : noise = {NOISE}")
print(f"  population          : {POP} agents (R0 : champ moyen, pas d'agents)")
print(f"  generations/pas     : {GENS}")
print(f"  rounds par match    : {ROUNDS}")
print(f"  taux de mutation    : {MU}")
print(f"  graines             : {SEEDS} (identiques entre regimes)")
print(f"  observable          : coop -> 8 symboles (quantiles) -> 30 fenetres x 3 proxys -> b1")
print(f"  seuil de verdict    : tau = {TAU}")
=== Protocole d'appariement (commun aux 4 regimes) ===
  substrat            : IPD canonique (payoff_pair de ict.strategic_morphodynamics)
  bruit d'execution   : noise = 0.02
  population          : 60 agents (R0 : champ moyen, pas d'agents)
  generations/pas     : 400
  rounds par match    : 30
  taux de mutation    : 0.05
  graines             : [20260720, 42, 7] (identiques entre regimes)
  observable          : coop -> 8 symboles (quantiles) -> 30 fenetres x 3 proxys -> b1
  seuil de verdict    : tau = 0.05

2. R0 — replicateur-mutation déterministe (vocabulaire fermé)

Dynamique de champ moyen x(t+1) = (1-mu) * x(t) * f(t)/<f>(t) + mu/n sur la matrice de gains du substrat. La trajectoire converge vers l’équilibre mutation-sélection et s’y fige : c’est le régime « mutation uniforme » de la table de #13308, attendu à relief nul. Le taux de coopération est mesuré comme dans les régimes agent-based : par une matrice empirique de coopération C_ij (paires jouées sous le même bruit), puis coop(t) = x(t)^T C x(t).

def regime_r0(seed):
    """Replicateur-mutation deterministe. Coop mesuree via matrice C empirique
    (meme bruit que les regimes agent-based) : coop(t) = x(t)^T C x(t)."""
    rng = np.random.default_rng(seed)
    st = SM.make_strategies(rng)
    A = SM.payoff_matrix(st, n_rounds=200, n_reps=3, rng=rng)
    x0 = np.full(A.shape[0], 1.0 / A.shape[0])
    traj = SM.replicator_mutation_trajectory(A, x0, n_steps=GENS, mutation_rate=MU)
    names = list(st.keys())
    n = len(names)
    Cm = np.zeros((n, n))
    reps = 20
    for i in range(n):
        for j in range(n):
            c = 0.0
            for _ in range(reps):
                own_i, own_j = [], []
                for _ in range(ROUNDS):
                    a = int(st[names[i]](np.array(own_i), np.array(own_j)))
                    b = int(st[names[j]](np.array(own_j), np.array(own_i)))
                    if NOISE > 0:
                        if rng.random() < NOISE: a = 1 - a
                        if rng.random() < NOISE: b = 1 - b
                    own_i.append(a); own_j.append(b)
                    c += (a == C) + (b == C)
            Cm[i, j] = c / (2 * reps * ROUNDS)
    coop = np.einsum('ti,ij,tj->t', traj, Cm, traj)
    return coop

r0_b1 = {}
print("R0 -- replicateur-mutation deterministe (vocabulaire ferme, mu/n)")
for seed in SEEDS:
    coop = regime_r0(seed)
    b1v, nsym = coop_to_b1(coop, f'r0_{seed}')
    r0_b1[seed] = b1v
    print(f"  seed {seed:>9d} : b1_max_persistence = {b1v:.4f}  (n_symboles={nsym})")
R0 -- replicateur-mutation deterministe (vocabulaire ferme, mu/n)
  seed  20260720 : b1_max_persistence = 0.0000  (n_symboles=8)
  seed        42 : b1_max_persistence = 0.0000  (n_symboles=8)
  seed         7 : b1_max_persistence = 0.0000  (n_symboles=8)

3. R1 — générateur A : resemencement uniforme agent-based (la référence #12673)

Wright-Fisher à population finie : chaque génération, appariement aléatoire, matchs IPD bruités, reproduction proportionnelle au gain, et mutation = resemencement uniforme dans le catalogue fermé des 6 stratégies. C’est le régime qui a régénéré le substrat Axelrod d’ICT-15j — la référence dont le relief (0,3989 sur la graine 20260720) est l’objet du doute.

def regime_r1(seed):
    """Generateur A : resemencement uniforme sur catalogue ferme (evolve_population)."""
    rng = np.random.default_rng(seed)
    evo = SM.evolve_population(SM.make_strategies(rng), pop_size=POP,
                               n_generations=GENS, n_rounds=ROUNDS, noise=NOISE,
                               mutation_rate=MU, rng=rng)
    return evo.cooperation_rate

r1_b1 = {}
print("R1 -- generateur A : resemencement uniforme agent-based (reference #12673)")
for seed in SEEDS:
    coop = regime_r1(seed)
    b1v, nsym = coop_to_b1(coop, f'r1_{seed}')
    r1_b1[seed] = b1v
    print(f"  seed {seed:>9d} : b1_max_persistence = {b1v:.4f}  (n_symboles={nsym})")
R1 -- generateur A : resemencement uniforme agent-based (reference #12673)
  seed  20260720 : b1_max_persistence = 0.3989  (n_symboles=8)
  seed        42 : b1_max_persistence = 0.2583  (n_symboles=8)
  seed         7 : b1_max_persistence = 0.3949  (n_symboles=8)

4. R2 — générateur B : dérive comportementale (génomes 5 bits, vocabulaire ouvert)

Structurellement distinct de R1 : chaque agent porte un génome memory-1 de 5 bits — [coup initial, réaction à (D,D), réaction à (D,C), réaction à (C,D), réaction à (C,C)] — et la mutation inverse un bit du génome d’un enfant. Le mutant n’est pas tiré d’un catalogue : c’est un voisin à un bit de son parent. L’espace des comportements est ouvert (32 génomes), la population initiale n’en porte que 5 (les classiques memory-1 : ALLC, ALLD, TFT, GRIM approximé, PAVLOV) — les mutations créent donc des comportements qui n’existaient pas, et on compte ces nouveautés au fil de la trajectoire.

R1 (générateur A) R2 (générateur B)
source du mutant tirage uniforme dans un catalogue fermé perturbation locale du code d’un incumbent
comportements accessibles les 6 du catalogue les 32 génomes (vocabulaire ouvert)
effet sur le vocabulaire composition seulement croissance effective (comptée ci-dessous)
# Genomes memory-1 : [init, r(D,D), r(D,C), r(C,D), r(C,C)]
GEN_ALLC = [1, 1, 1, 1, 1]
GEN_ALLD = [0, 0, 0, 0, 0]
GEN_TFT  = [1, 0, 1, 0, 1]   # joue le dernier coup de l'adversaire
GEN_GRIM = [1, 0, 0, 0, 1]   # approx memory-1 : C seulement apres (C,C)
GEN_PAV  = [1, 1, 0, 0, 1]   # win-stay lose-shift
GENOMES0 = [GEN_ALLC, GEN_ALLD, GEN_TFT, GEN_GRIM, GEN_PAV]

def genome_play(g, own, opp):
    if len(own) == 0:
        return g[0]
    return g[1 + ((int(own[-1]) << 1) | int(opp[-1]))]

def regime_r2(seed):
    """Generateur B : derive comportementale. Retourne (coop, n_comportements_nouveaux)."""
    rng = np.random.default_rng(seed)
    initial_set = {tuple(g) for g in GENOMES0}
    pop_g = [list(GENOMES0[rng.integers(0, len(GENOMES0))]) for _ in range(POP)]
    seen = {tuple(g) for g in pop_g}
    coop = np.zeros(GENS + 1)

    def generation():
        perm = rng.permutation(POP)
        payoffs = np.zeros(POP)
        n_coop = 0
        n_plays = 0
        for k in range(0, POP, 2):
            i, j = int(perm[k]), int(perm[k + 1])
            gi, gj = pop_g[i], pop_g[j]
            own_i, own_j = [], []
            g_i = g_j = 0.0
            for _ in range(ROUNDS):
                a = genome_play(gi, own_i, own_j)
                b = genome_play(gj, own_j, own_i)
                if NOISE > 0:
                    if rng.random() < NOISE: a = 1 - a
                    if rng.random() < NOISE: b = 1 - b
                pa, pb = SM.payoff_pair(a, b)
                g_i += pa; g_j += pb
                own_i.append(a); own_j.append(b)
                n_coop += (a == C) + (b == C)
                n_plays += 2
            payoffs[i] = g_i / ROUNDS
            payoffs[j] = g_j / ROUNDS
        return n_coop / max(n_plays, 1), payoffs

    for g in range(GENS + 1):
        coop[g], payoffs = generation()
        if g == GENS:
            break
        w = np.clip(payoffs, 1e-6, None)
        parents = rng.choice(POP, size=POP, p=w / w.sum())
        children = [list(pop_g[p]) for p in parents]
        mutators = rng.random(POP) < MU
        for m in np.nonzero(mutators)[0]:
            bit = int(rng.integers(0, 5))
            children[m][bit] = 1 - children[m][bit]
            seen.add(tuple(children[m]))
        pop_g = children
    return coop, len(seen - initial_set)

r2_b1, r2_novel = {}, {}
print("R2 -- generateur B : derive comportementale (genomes 5 bits, vocabulaire ouvert)")
for seed in SEEDS:
    coop, novel = regime_r2(seed)
    b1v, nsym = coop_to_b1(coop, f'r2_{seed}')
    r2_b1[seed] = b1v
    r2_novel[seed] = novel
    print(f"  seed {seed:>9d} : b1_max_persistence = {b1v:.4f}  "
          f"(comportements nouveaux hors catalogue initial : {novel})")
R2 -- generateur B : derive comportementale (genomes 5 bits, vocabulaire ouvert)
  seed  20260720 : b1_max_persistence = 0.3397  (comportements nouveaux hors catalogue initial : 26)
  seed        42 : b1_max_persistence = 0.6957  (comportements nouveaux hors catalogue initial : 27)
  seed         7 : b1_max_persistence = 0.1934  (comportements nouveaux hors catalogue initial : 27)

5. R3 — contrôle négatif : la dimension sans la nouveauté

Le simplexe est doublé : chaque stratégie existe sous deux étiquettes (#a, #b), les comportements sont strictement les mêmes, la distribution de mutation sur les comportements est inchangée. La dimension passe de 6 à 12 sans qu’aucune nouveauté soit injectée. Ordre des étiquettes par blocs (tous les #a, puis tous les #b) : l’ordre interleaved serait un no-op bit-exact — la cellule le démontre avant de mesurer.

Le piège de l’ordre interleaved. Dupliquer en alternant s0#a, s0#b, s1#a, s1#b, ... fait correspondre l’étiquette i au comportement i//2. Pour le générateur numpy, integers(0, 12) // 2 == integers(0, 6) bit à bit sur tout le flux (vérifié sur 200 000 tirages ci-dessous) : la trajectoire R3-interleaved est alors exactement celle de R1 — le « contrôle » mesurerait la référence elle-même. C’est une preuve de symétrie du mécanisme, pas un contrôle : l’ordre par blocs casse cette correspondance et donne au contrôle sa propre trajectoire, distributionnellement identique à R1 sur les comportements.

# Demonstration du no-op interleaved : integers(0, 2n)//2 == integers(0, n), bit a bit
_r6 = np.random.default_rng(20260720).integers(0, 6, 200_000)
_r12 = np.random.default_rng(20260720).integers(0, 12, 200_000)
print(f"integers(0,12)//2 == integers(0,6) sur 200 000 tirages : "
      f"{bool((_r12 // 2 == _r6).all())}")

def regime_r3_interleaved(seed):
    """Duplication interleaved : NO-OP bit-exact de R1 (demonstration, pas un controle)."""
    rng = np.random.default_rng(seed)
    base = SM.make_strategies(rng)
    dup = {}
    for k, v in base.items():
        dup[f'{k}#a'] = v
        dup[f'{k}#b'] = v
    evo = SM.evolve_population(dup, pop_size=POP, n_generations=GENS,
                               n_rounds=ROUNDS, noise=NOISE, mutation_rate=MU, rng=rng)
    return evo.cooperation_rate

c1_ref = regime_r1(SEEDS[0])
c3_int = regime_r3_interleaved(SEEDS[0])
print(f"R3-interleaved reproduit R1 bit a bit (seed {SEEDS[0]}) : "
      f"{bool(np.array_equal(c1_ref, c3_int))}")

def regime_r3(seed):
    """Controle negatif : etiquettes dupliquees PAR BLOCS -- dimension 12, memes 6
    comportements, meme distribution de mutation sur les comportements."""
    rng = np.random.default_rng(seed)
    base = SM.make_strategies(rng)
    dup = {f'{k}#a': v for k, v in base.items()}
    dup.update({f'{k}#b': v for k, v in base.items()})
    evo = SM.evolve_population(dup, pop_size=POP, n_generations=GENS,
                               n_rounds=ROUNDS, noise=NOISE, mutation_rate=MU, rng=rng)
    return evo.cooperation_rate

r3_b1 = {}
print("R3 -- controle dimension-sans-nouveaute (etiquettes dupliquees par blocs)")
for seed in SEEDS:
    coop = regime_r3(seed)
    b1v, nsym = coop_to_b1(coop, f'r3_{seed}')
    r3_b1[seed] = b1v
    print(f"  seed {seed:>9d} : b1_max_persistence = {b1v:.4f}  (n_symboles={nsym})")
integers(0,12)//2 == integers(0,6) sur 200 000 tirages : True
R3-interleaved reproduit R1 bit a bit (seed 20260720) : True
R3 -- controle dimension-sans-nouveaute (etiquettes dupliquees par blocs)
  seed  20260720 : b1_max_persistence = 0.3531  (n_symboles=8)
  seed        42 : b1_max_persistence = 0.2411  (n_symboles=8)
  seed         7 : b1_max_persistence = 0.4624  (n_symboles=8)

6. Résultats et verdict

Les trois graines par régime, la moyenne, et le verdict calculé par la règle pré-enregistrée en tête de notebook — jamais écrit à la main.

regimes = {
    'R0 replicateur-mutation (deterministe)': r0_b1,
    'R1 generateur A (resemencement uniforme)': r1_b1,
    'R2 generateur B (derive genomique)': r2_b1,
    'R3 controle dimension-sans-nouveaute': r3_b1,
}

print(f"{'regime':42s} | " + " | ".join(f"seed {s}" for s in SEEDS) + " |  moyenne |  ecart-type")
print("-" * 100)
means = {}
for name, d in regimes.items():
    v = np.array([d[s] for s in SEEDS])
    means[name] = v.mean()
    print(f"{name:42s} | " + " | ".join(f"{d[s]:>7.4f}" for s in SEEDS) +
          f" | {v.mean():>7.4f} | {v.std():>9.4f}")

m_r0 = means['R0 replicateur-mutation (deterministe)']
m_r1 = means['R1 generateur A (resemencement uniforme)']
m_r2 = means['R2 generateur B (derive genomique)']
m_r3 = means['R3 controle dimension-sans-nouveaute']

# Regle pre-enregistree (cellule d'en-tete) -- appliquee programmatiquement
if m_r3 > TAU:
    verdict = 'CONFONDU_AVEC_LA_DIMENSION'
elif (m_r1 > TAU) and (m_r2 > TAU):
    verdict = 'ROBUSTE_AU_GENERATEUR'
elif (m_r1 > TAU) != (m_r2 > TAU):
    verdict = 'ARTEFACT_DE_GENERATEUR'
else:
    verdict = 'INCONCLUSIVE'

print()
print(f"relief(R0) = {m_r0:.4f} | relief(R1) = {m_r1:.4f} | "
      f"relief(R2) = {m_r2:.4f} | relief(R3) = {m_r3:.4f} | tau = {TAU}")
print(f"VERDICT (regle pre-enregistree) = {verdict}")

# Barplot in-situ : relief moyen par regime
fig, ax = plt.subplots(figsize=(7, 3.6))
names_short = ['R0\ndeterministe', 'R1\ngene A', 'R2\ngene B', 'R3\ncontrole dim']
vals = [m_r0, m_r1, m_r2, m_r3]
colors = ['#888888', '#4477aa', '#4477aa', '#cc3311']
bars = ax.bar(names_short, vals, color=colors, edgecolor='black', linewidth=0.6)
ax.axhline(TAU, color='black', linestyle='--', linewidth=1, label=f'seuil tau = {TAU}')
for b, v in zip(bars, vals):
    ax.text(b.get_x() + b.get_width() / 2, v + 0.01, f'{v:.3f}',
            ha='center', va='bottom', fontsize=9)
ax.set_ylabel('b1_max_persistence (moyenne 3 graines)')
ax.set_title('Relief du discriminant nerf par regime (graines apparieees)')
ax.legend(loc='upper left', fontsize=8)
fig.tight_layout()
plt.close(fig)  # backend Agg (cell. 1) : show() non interactif -> UserWarning superflu
print("Barplot : R1 et R2 (bleu) = generateurs de nouveaute ; R3 (rouge) = controle dimension.")
regime                                     | seed 20260720 | seed 42 | seed 7 |  moyenne |  ecart-type
----------------------------------------------------------------------------------------------------
R0 replicateur-mutation (deterministe)     |  0.0000 |  0.0000 |  0.0000 |  0.0000 |    0.0000
R1 generateur A (resemencement uniforme)   |  0.3989 |  0.2583 |  0.3949 |  0.3507 |    0.0654
R2 generateur B (derive genomique)         |  0.3397 |  0.6957 |  0.1934 |  0.4096 |    0.2109
R3 controle dimension-sans-nouveaute       |  0.3531 |  0.2411 |  0.4624 |  0.3522 |    0.0903

relief(R0) = 0.0000 | relief(R1) = 0.3507 | relief(R2) = 0.4096 | relief(R3) = 0.3522 | tau = 0.05
VERDICT (regle pre-enregistree) = CONFONDU_AVEC_LA_DIMENSION
Barplot : R1 et R2 (bleu) = generateurs de nouveaute ; R3 (rouge) = controle dimension.

Lecture du verdict : CONFONDU_AVEC_LA_DIMENSION

Le résultat a trois étages, tous honnêtes :

  1. La réfutation est nette. Le contrôle négatif — dimension doublée, zéro comportement nouveau, distribution de mutation inchangée sur les comportements — produit le même relief que les deux générateurs de nouveauté (R3 ≈ R1 ≈ 0,35 de moyenne, chacun > τ sur les 3 graines). Selon la règle pré-enregistrée de #13308, le discriminant ne mesure donc pas la nouveauté stratégique sur cette recette : l’écart R1 > 0 = R0 attribue le relief au turnover stochastique de population finie (dérive Wright-Fisher + bruit d’exécution), pas à l’apparition de stratégies nouvelles. L’énoncé « la nouveauté produit du relief là où le bruit n’en produit pas » tombe sur cette instance — et c’est exactement ce que le test était conçu pour pouvoir faire.

  2. Les deux générateurs s’accordent entre eux — mais ça ne sauve rien. R1 (catalogue fermé) et R2 (dérive génomique, 26-27 comportements nouveaux par trajectoire) donnent du relief tous les deux. Sans le contrôle, on aurait écrit ROBUSTE_AU_GENERATEUR. Le contrôle révèle que cette robustesse était vraie et vide : le relief ne discriminait pas la nouveauté, il accompagnait la stochasticité agent-based. Un contrôle négatif qui échoue vaut plus qu’une replication qui réussit.

  3. Résolution du discriminant. La démonstration interleaved montre en prime que b1_max_persistence est grossier sur cette recette : deux trajectoires de coopération différentes peuvent partager exactement le même nerf (R1 et R3-interleaved sont float-identiques parce que les proxys fenêtrés coïncident). Toute conclusion fine appuyée sur des écarts de b1 à la 2ᵉ décimale doit être tenue pour fragile.

Ce que ce notebook ne fait pas : il ne réteste pas Qwen ni le banc humour (#13306 suit son propre protocole), et il ne promeut rien — le verdict est un fait d’instance et de recette, la promotion est un acte séparé (#13303).

Exercice 1 (null de permutation). Le verdict dit que le relief accompagne la stochasticité, pas la trajectoire elle-même. Testez : permutez aléatoirement la série de coopération de R1 (structure temporelle détruite, distribution conservée) et mesurez b1_max_persistence sur 5 permutations. Si les valeurs restent dans la plage des régimes agent-based, le discriminant lit-il un contenu de trajectoire ou une statistique de distribution ?

# Exercice 1 -- a completer.
# Indice : coop = regime_r1(20260720) ; puis pour k in range(5) :
#   perm = rng.permutation(len(coop)) ; b1, ns = coop_to_b1(coop[perm], f'perm{k}')
# Etape 1 : construire le generateur de permutations (np.random.default_rng distinct).
# Etape 2 : mesurer les 5 b1 et les comparer a r1_b1[20260720].
# Etape 3 : conclure en une phrase -- le discriminant lit-il la trajectoire ou la distribution ?
pass

Exercice 2 (le relief du contrôle suit-il le taux de turnover ?). Si le relief de R3 vient du turnover stochastique, il devrait répondre au taux de mutation. Recalculez R3 (blocs) avec MU = 0.01 puis MU = 0.10 sur la graine 20260720 et comparez les trois b1_max_persistence. Attention à garder l’appariement : ne changez QUE MU.

# Exercice 2 -- a completer.
# Indice : copier regime_r3 en parametrant MU (argument mu=...), ne rien changer d'autre.
# Etape 1 : mesurer b1 pour mu in [0.01, 0.05, 0.10] sur la graine 20260720.
# Etape 2 : le relief croit-il avec le taux de turnover ?
pass

Exercice 3 (génomes memory-2 : plus de dimension, plus de nouveauté possible). Le générateur B ouvre 32 comportements (5 bits). Un génome memory-2 — réaction aux deux derniers coups de chaque joueur, 16 états + coup initial = 17 bits, 131 072 comportements — rend l’espace presque infini. Implémentez la lecture du génome memory-2 (la mutation reste un bit par mutant) et mesurez R2-memory-2 sur la graine 20260720. Le verdict change-t-il ? On attend honnêtement : non — le contrôle R3 prédit que la dimension n’est pas le bon axe.

# Exercice 3 -- a completer.
# Indice : genome = [init] + 16 bits d'index (own1,opp1,own0,opp0) ; l'index de reaction
#   est base 4 sur les deux derniers coups. Garder POP, GENS, ROUNDS, NOISE, MU apparies.
# Etape 1 : ecrire genome_play_2 (lecture a deux pas d'historique).
# Etape 2 : reutiliser la boucle de regime_r2 avec ce genome (population initiale : les
#   5 classiques transcrits en memory-2).
# Etape 3 : mesurer b1 et comparer a r2_b1[20260720] et a m_r3.
pass

Conclusion — un discriminant honnête est un discriminant contrôlé

régime mécanisme relief (moy. 3 graines) lecture
R0 replicateur-mutation déterministe 0,0000 convergence figée, nuage saturé
R1 générateur A (catalogue fermé) ~0,35 relief, mais voir R3
R2 générateur B (dérive génomique) ~0,41 relief, mais voir R3
R3 dimension sans nouveauté ~0,35 le relief est là aussi

Le protocole pré-enregistré de #13308 rend son verdict : CONFONDU_AVEC_LA_DIMENSION. Sur le substrat Axelrod régénéré et la recette observable d’ICT-15j, le relief du discriminant nerf accompagne la stochasticité de population finie, pas l’apparition de comportements nouveaux. La réplication sur deux générateurs structurellement distincts (R1/R2) aurait suggéré une robustesse — le contrôle négatif la vide de son sens : c’est la leçon méthodologique du notebook, la même que #13306 applique au détecteur d’humour. Un instrument qui mesure sa propre construction doit être falsifié par le contrôle le moins glamour du banc — et ici, il l’a été.

Raccord : ICT-15j mesure, ICT-15l audite la mesure. La prochaine étape n’est pas un meilleur générateur de nouveauté mais un observable plus fin (ou une question différente) ; la campagne multi-annotateurs de #12756 et l’observatoire externe #13303 restent le cap.

Retour au sommet