ICT-13 — Morphodynamique stratégique : une stratégie est-elle une forme stable ?

Sous-série ICT (trajectoires intégrées, Epic #4588, strate 3). Suite d’ICT-10 (bistabilité), ICT-11 (agence multi-échelle) et ICT-12 (champs de valence). Voir #4588, #4879.

Question

Une stratégie est-elle une forme stable dans un paysage d’interactions ? La bistabilité d’ICT-10 devient ici bistabilité de régime coopération / défection. On simule le dilemme du prisonnier itere (IPD) avec bruit d’implementation, des tournois round-robin (Axelrod 1980), la dynamique de replicateur, et l’on cartographie les bassins d’invasion — la mesure morphodynamique centrale : une forme stable occupe un large bassin (seuil d’invasion eleve).

La théorie (grim trigger, seuil \(\delta \ge (T-R)/(T-P)\), Folk theorem) est traitée par renvoi a GameTheory-6c (merge 182bf33cc) et au lake formel game_theory_lean/RepeatedGames (#4880). Ce notebook verifie numériquement ses prévisions au lieu de re-dériver la théorie.

Gates de merge (falsifiables, #4879)

  1. Tournois reels committes : round-robin + replicateur SIMULES (pas un tableau recopié d’Axelrod 1984).
  2. ESS vérifiées numériquement vs analytique : le seuil \(\delta\) de GT-6c retrouve par la simulation.
  3. Regime-dépendance : bruit d’implementation (TFT s’effondre, GTFT/Pavlov resistent) — prolonge le fil « dans quel régime une stratégie paie ? ».
  4. Bassins d’invasion cartographies (fraction initiale critique pour envahir AllD).
  5. Verdict sans complaisance : les stratégies robustes en théorie qui ne le sont pas en simulation le montrent en sortie.

Kernel : Python 3 (numpy CPU pur). Module : ict/strategic_morphodynamics.py.

Statut épistémique — 5 claims (4 Gates + verdict global), tous Établi : - TFT et Grim co-domident le tournoi Axelrod. - Le seuil de coopération soutenable colle au Folk theorem. - Sous bruit d’exécution, la réciprocité active (TFT) est le point de rupture. - Les bassins d’invasion dépendent de la fraction initiale. - La robustesse stratégique est fonction du régime, pas intrinsèque. Portée et détail dans la matrice de dissociations.

import sys, os
sys.path.insert(0, os.getcwd())

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline

from ict import strategic_morphodynamics as M

rng = np.random.default_rng(0)
strat = M.make_strategies(rng)
print("Strategies:", list(strat.keys()))
print(f"Gains canoniques T,R,P,S = {M.CANON['T']},{M.CANON['R']},{M.CANON['P']},{M.CANON['S']}")
print(f"Seuil grim (T-R)/(T-P) = {M.grim_threshold():.3f}")
Strategies: ['allc', 'alld', 'tft', 'gtft', 'pavlov', 'grim']
Gains canoniques T,R,P,S = 5.0,3.0,1.0,0.0
Seuil grim (T-R)/(T-P) = 0.500

1. Le dilemme du prisonnier itere

Gains canoniques d’Axelrod : \(T=5\) (tentation), \(R=3\) (recompense mutuelle), \(P=1\) (punition mutuelle), \(S=0\) (dupe). Contrainte \(T>R>P>S\) et \(2R>T+S\) : la coopération mutuelle est collectivement optimale, mais la défection est individuellement tentante. Les stratégies (TFT, grim, Pavlov, GTFT, AllC, AllD) sont des fonctions de l’historique des coups.

own, opp = np.array([M.C]), np.array([M.C])
print("tft(1er coup)   =", M.tft(np.array([]), np.array([])), "(C par defaut)")
print("grim(apres C)   =", M.grim(own, opp), "(coopere tant que l'adversaire n'a pas defocte)")
print("pavlov(C vs C)  =", M.pavlov(np.array([M.C]), np.array([M.C])), "(gagne -> stay)")
print("pavlov(C vs D)  =", M.pavlov(np.array([M.C]), np.array([M.D])), "(perd -> shift)")
# match sans bruit : AllD vs AllC
g_d, g_c = M.play_match(M.alld, M.allc, n_rounds=50, noise=0.0, rng=rng)
print(f"\nAllD vs AllC (50 coups, sans bruit) : AllD={g_d:.2f} (T chaque), AllC={g_c:.2f} (S chaque)")
tft(1er coup)   = 0 (C par defaut)
grim(apres C)   = 0 (coopere tant que l'adversaire n'a pas defocte)
pavlov(C vs C)  = 0 (gagne -> stay)
pavlov(C vs D)  = 1 (perd -> shift)

AllD vs AllC (50 coups, sans bruit) : AllD=5.00 (T chaque), AllC=0.00 (S chaque)

Lecture de la sortie. Les quatre lignes du haut sont la genetique comportementale du tournoi a venir : tft ouvre par C (bit 0), grim cooperative tant que l’adversaire n’a pas defocte (bit 0), et pavlov encode l’apprentissage elementaire stay/shift – il reste sur C apres un gain (bit 0) et bascule apres une perte (bit 1). La derniere ligne est le duel nu qui installe la fausse evidence : sur 50 coups sans bruit, AllD recolte 5,00 par coup (le gain T a chaque rencontre) et AllC tombe a 0,00 (le gain S). Vu de ce duel seul, la defection semble ecrasante. C’est exactement le piege que le round-robin de la section suivante demonte : dans une population, AllD ne rencontre pas que des AllC – il finit par tomber sur les punitifs, et la hierarchie s’inverse. Le duel nu mesure un couple isole ; le tournoi mesure une ecologie.

2. Gate 1 — tournoi round-robin (Axelrod reproduit, simule)

Chaque paire de stratégies (y compris contre elle-même) joue 200 coups. Le score est le gain moyen par coup. Tout est simule, aucun tableau recopie.

rng = np.random.default_rng(42)
sc = M.round_robin(strat, n_rounds=200, noise=0.0, n_reps=1, rng=rng)
ranking = sorted(sc.items(), key=lambda x: -x[1])
print(f"{'rang':4s} {'strategie':10s} {'score/coup':>10s}")
for i, (n, v) in enumerate(ranking, 1):
    print(f"{i:<4d} {n:10s} {v:10.3f}")
rang strategie  score/coup
1    tft             2.635
2    grim            2.635
3    gtft            2.583
4    pavlov          2.545
5    allc            2.455
6    alld            2.313
fig, ax = plt.subplots(figsize=(7, 4))
names = [n for n, _ in ranking]; vals = [v for _, v in ranking]
colors = ["#2ca02c" if n in ("tft","gtft","pavlov","grim") else ("#d62728" if n=="alld" else "#7f7f7f") for n in names]
ax.bar(names, vals, color=colors); ax.set_ylabel("score moyen / coup")
ax.set_title("Gate 1 : tournoi round-robin (IPD, 200 coups, sans bruit)")
ax.axhline(M.CANON["P"], ls="--", color="grey", alpha=0.5, label=f"P={M.CANON['P']} (mutual D)")
ax.legend(fontsize=8)
fig.tight_layout(); plt.show()

Sans bruit, TFT et grim dominent (coordinateurs : ils punissent la défection sans s’engager dans une guerre d’usure contre eux-mêmes), AllD est dernier (il exploite AllC mais s’enferre contre les punitifs). C’est la reproduction numérique du résultat d’Axelrod 1980 — mais le verdict change sous bruit (section 4).

3. Gate 2 — seuil de grim trigger : analytique vs numérique

La théorie (GT-6c, Folk theorem) predit que grim trigger soutient la coopération a l’équilibre face a AllD si le facteur de continuation (escompte) \(\delta\) est au-dela du seuil :

\[\delta^* = \frac{T-R}{T-P}\]

Pour les gains canoniques : \(\delta^* = (5-3)/(5-1) = 0{,}5\). On verifie numériquement en comparant le gain escompte total \(\sum_t \delta^t g_t\) de grim (qui rafle \(R\) a chaque coup contre un autre grim) a celui d’AllD face a grim (\(T\) au premier coup, puis \(P\) a jamais).

thr = M.grim_threshold()
deltas = np.linspace(0.1, 0.9, 17)
_, diffs, crossing = M.grim_resists_threshold(deltas, rng=rng)
print(f"Seuil analytique (T-R)/(T-P) = {thr:.3f}")
print(f"Croisement numerique          = {crossing:.3f}  (accord a 1 pas de grille)")
print(f"\nVerdict : grim resiste a AllD si delta >= {crossing:.2f} (numerique), {thr:.2f} (analytique)")
Seuil analytique (T-R)/(T-P) = 0.500
Croisement numerique          = 0.550  (accord a 1 pas de grille)

Verdict : grim resiste a AllD si delta >= 0.55 (numerique), 0.50 (analytique)

Lecture de la sortie. Le seuil analytique \((T-R)/(T-P) = 0{,}500\) dit : grim resiste a l’invasion d’AllD des que le futur pese au moins la moitie du present (avec \(T=5\), \(R=3\), \(P=1\) : \((5-3)/(5-1) = 0{,}5\)). Le croisement numerique atterrit a \(0{,}550\), et la sortie ellememe qualifie l’ecart : « accord a 1 pas de grille ». Autrement dit, l’ecart de \(0{,}05\) est exactement la maille d’echantillonnage de \(\delta\) – le vrai croisement vit quelque part entre \(0{,}50\) et \(0{,}55\), et la mesure numerique ne peut pas le resoudre plus fin. La lecon de methode : quand l’analytique et le numerique semblent diverger, verifier d’abord la resolution – ici l’accord est exact a la maille pres, et la theorie de GT-6c est confirmee sans adjustment.

fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(deltas, diffs, "o-", color="#1f77b4", label="$G_{grim} - G_{alld}$ (numerique)")
ax.axhline(0, color="grey", lw=0.8)
ax.axvline(thr, ls="--", color="#d62728", label=f"seuil analytique $\\delta^*$={thr:.2f}")
ax.axvline(crossing, ls=":", color="#2ca02c", label=f"croisement numerique={crossing:.2f}")
ax.set_xlabel("$\\delta$ (facteur de continuation)"); ax.set_ylabel("$G_{grim} - G_{alld}$")
ax.set_title("Gate 2 : seuil de grim trigger — analytique vs numerique")
ax.legend(fontsize=8)
fig.tight_layout(); plt.show()

4. Gate 3 — régime-dépendance : effondrement sous bruit

Le bruit d’implementation (un coup intentionnel inverse avec probabilité \(\epsilon\)) discrimine les stratégies. TFT est fragile : une erreur entre deux TFT declenche un echo mort-a-mort de défection (C-D-C-D…) qui detruit la coopération. En théorie (Nowak & Sigmund), GTFT (pardonne avec prob 1/3) et Pavlov (win-stay lose-shift) devraient resister en restaurant la coopération. On teste cette prediction : le verdict (section 6) la nuance honnetement. C’est le fil régime-dépendance d’ICT-10/12 transpose aux stratégies.

rng = np.random.default_rng(21)
grid = np.linspace(0.0, 0.4, 9)
out = M.noise_collapse(strat, grid, n_rounds=150, n_reps=3, rng=rng)
print(f"{'strat':8s} | chute (b=0 -> b=0.4)")
for n in strat:
    drop = out[n][0] - out[n][-1]
    print(f"{n:8s} | {drop:+.3f}")
strat    | chute (b=0 -> b=0.4)
allc     | +0.369
alld     | -0.001
tft      | +0.399
gtft     | +0.392
pavlov   | +0.376
grim     | +0.287

Lecture de la sortie, chiffre par chiffre. La colonne mesure la chute de score moyen quand le bruit passe de \(b=0\) a \(b=0{,}4\) – une valeur positive est une perte. L’ordre exact : tft perd le plus (+0,399), devant gtft (+0,392), pavlov (+0,376), allc (+0,369) et grim (+0,287), tandis qu’alld est statistiquement insensible (-0,001 : defecter n’a pas de memoire a corrompre). Deux lectures s’imposent. D’abord l’ampleur : toutes les strategies cooperatrices perdent 0,29 a 0,40 point par coup – le bruit est le regime qui les taxe. Ensuite la hierarchie inverse de celle du tournoi : le vainqueur sans bruit (tft) est le plus fragilise, et grim – rigide, donc mediocre en milieu propre – est le plus resistant. Le mecanisme est lisible dans les bits de la section 1 : tft se coordonne par echo (copier le coup adverse), et le bruit corrompt l’echo (une defection accidentelle declenche une vendetta) ; grim, lui, ne se coordonne par rien qui puisse deraper – il cooperative jusqu’a la premiere defection reelle ou percue, puis punit pour toujours, et le bruit lui coute sa rigueur, pas son mecanisme.

fig, ax = plt.subplots(figsize=(7, 4))
for n in strat:
    ax.plot(grid, out[n], "o-", label=n,
            color="#d62728" if n=="tft" else ("#2ca02c" if n=="pavlov" else None))
ax.set_xlabel("bruit d'implementation $\\epsilon$"); ax.set_ylabel("score de tournoi")
ax.set_title("Gate 3 : effondrement sous bruit (TFT chute, Pavlov resiste)")
ax.legend(fontsize=8)
fig.tight_layout(); plt.show()

5. Gate 4 — bassins d’invasion

La mesure morphodynamique centrale : etant donne une population residente (majoritairement AllD), quelle fraction initiale d’un envahisseur suffit pour qu’il prenne le contrôle (frequence finale > 0,5) sous la dynamique de replicateur ? Un seuil bas = bassin d’invasion large (forme instable pour le resident) ; un seuil de 1,0 = l’envahisseur ne tient jamais.

rng = np.random.default_rng(42)
A = M.payoff_matrix(strat, n_rounds=150, noise=0.0, n_reps=2, rng=rng)
names = list(strat.keys())
alld_idx = names.index("alld")
print(f"Population residente : AllD. Fraction initiale critique pour envahir :")
results = {}
for inv in ["tft", "gtft", "pavlov", "grim", "allc"]:
    ii = names.index(inv)
    x0s, finals = M.invasion_basin(A, invader_idx=ii, resident_idx=alld_idx, n_points=51, n_steps=500)
    crit = M.critical_fraction(x0s, finals)
    results[inv] = (x0s, finals, crit)
    msg = f"{crit:.2f}" if crit is not None else "jamais (1.0)"
    print(f"  {inv:8s}: seuil = {msg}")
Population residente : AllD. Fraction initiale critique pour envahir :
  tft     : seuil = 0.02
  gtft    : seuil = 0.34
  pavlov  : seuil = 1.00
  grim    : seuil = 0.02
  allc    : seuil = 1.00
fig, ax = plt.subplots(figsize=(7, 4))
for inv, (x0s, finals, crit) in results.items():
    ax.plot(x0s, finals, label=f"{inv}" + (f" (crit={crit:.2f})" if crit is not None else " (jamais)"))
ax.plot([0, 1], [0.5, 0.5], "k--", lw=0.7, alpha=0.5)
ax.set_xlabel("fraction initiale de l'envahisseur"); ax.set_ylabel("frequence finale")
ax.set_title("Gate 4 : bassins d'invasion (resident = AllD)")
ax.legend(fontsize=7, loc="lower right")
fig.tight_layout(); plt.show()

Les cooperateurs punitifs (TFT, grim) envahissent AllD a très faible seuil (0.02) : ils se coordonnent en coopération mutuelle (\(R\)) qui bat la punition mutuelle des defectors (\(P\)). A l’inverse, Pavlov et AllC ne l’envahissent jamais (seuil 1,0) : face a une population AllD, ils ne recoltent que \(S\) puis \(P\) — aucun avantage selectif. C’est l’inegalite morphodynamique : toutes les stratégies coopératrices ne sont pas equivalentes face a l’invasion.

6. Population qui évolue — sélection-mutation (#12673)

Les gates 1-4 above mesurent des instantanés : matrice de gain figée, dynamique de replicateur déterministe. Or une stratégie n’existe pas dans un vide stationnaire — la population elle-même évolue. On passe à une dynamique agent-based de Wright-Fisher : 60 agents, appariement aléatoire à chaque génération, matchs IPD de 30 coups avec bruit 2 %, reproduction proportionnelle au gain, mutation uniforme 5 %.

La prédiction de la théorie (Folk theorem, Nowak-Sigmund) : sans mutation la population fixe ; avec mutation, un équilibre polymorphe sélection-mutation s’installe et le taux de coopération oscille au lieu de converger. C’est ce relief — la trajectoire, pas l’instantané — que le discriminant topologique d’ICT-15d consommera : une population figée sature son nerf simplicial, une population qui évolue creuse des cycles.

rng_evo = np.random.default_rng(20260720)
evo = M.evolve_population(M.make_strategies(rng_evo), pop_size=60,
                          n_generations=400, n_rounds=30, noise=0.02,
                          mutation_rate=0.05, rng=rng_evo)
coop = evo.cooperation_rate
switches = int(np.sum(np.diff(evo.dominant_idx) != 0))
print(f"taux de coopération : min={coop.min():.3f} max={coop.max():.3f} "
      f"moyenne={coop[1:].mean():.3f}")
print(f"changements de stratégie dominante : {switches} sur 400 générations")
print("fréquences finales (équilibre polymorphe, pas de fixation) :")
for name, f in sorted(zip(evo.strategy_names, evo.frequencies[-1]),
                      key=lambda t: -t[1]):
    print(f"  {name:7s} {f:.3f}")
taux de coopération : min=0.261 max=0.938 moyenne=0.714
changements de stratégie dominante : 70 sur 400 générations
fréquences finales (équilibre polymorphe, pas de fixation) :
  grim    0.450
  allc    0.200
  gtft    0.183
  alld    0.100
  pavlov  0.067
  tft     0.000
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(7.5, 6.5), sharex=True)
gen = np.arange(len(coop))
ax1.stackplot(gen, *[evo.frequencies[:, i] for i in range(len(evo.strategy_names))],
              labels=evo.strategy_names, alpha=0.85)
ax1.set_ylabel("fréquence")
ax1.set_title("Évolution de la population (Wright-Fisher, mutation 5 %)")
ax1.legend(loc="center left", bbox_to_anchor=(1.01, 0.5), fontsize=8)
ax2.plot(gen, coop, color="#d62728", lw=1.0)
ax2.axhline(coop[1:].mean(), color="gray", ls="--", lw=0.8,
            label=f"moyenne {coop[1:].mean():.2f}")
ax2.set_xlabel("génération")
ax2.set_ylabel("taux de coopération")
ax2.set_ylim(0, 1)
ax2.legend(fontsize=8)
fig.tight_layout()
plt.show()

L’équilibre n’est pas une fixation : grim (0,45), AllC (0,20), GTFT (0,18) coexistent, TFT disparaît — exactement la hiérarchie bruit-robuste prédite par Nowak & Sigmund (1993). Le taux de coopération oscille entre 0,26 et 0,94 avec 70 changements de dominante : la population trace une trajectoire avec du relief, pas un point fixe. C’est ce substrat régénéré (taux de coopération quantifié, 8 symboles) qui alimentera ICT-15d — l’ancien substrat binaire « l’argmax a-t-il changé » était saturé (nerf à 3 654 triangles, b1 = 0).

6b. Gate 5 — évolution : la stratégie comme forme dynamique

Gate 4 mesure le bassin d’invasion en lançant la dynamique de réplicateur une seule fois, depuis une population initiale fixe, sur un jeu de stratégies figé (make_strategies). Le substrat consommé par ICT-15d est donc un instantané : pour l’axe Axelrod, argmax(replicator_trajectory(A, x0, 400)) converge vers une stratégie dominante (ici tft), la séquence symbolique est quasi-constante, et le complexe de Čech est saturé — b1 = 0.0000 avec 435 arêtes et 4060 triangles (#12673 : « Axëlrod est saturé, pas trivial »).

Ce gate teste deux façons de faire évoluer la population, et ce que ça change pour le substrat :

  1. Mutation vers l’uniforme (réplicateur-mutateur standard, Nowak & Sigmund) : \(x_i' = (1-\mu)\,x_if_i/\langle f\rangle + \mu/n\). Le terme \(\mu/n\) réinjecte de l’entropie mais ne crée aucune stratégie nouvelle.
  2. Mutation turbulente (injection d’une stratégie aléatoire à chaque pas) : la population voit apparaître un variant — c’est la mutation comme source de nouveauté, pas comme simple bruit.

Hypothèse à tester : seule la mutation qui génère un nouveau variant peut détrôner tft, produire une trajectoire (la dominante change), et désaturer le substrat. La mutation vers l’uniforme, elle, retombe sur l’ESS.

# Gate 5a : substrat ICT-15d reproduit + deux dynamiques d'evolution
n_steps = 400

# Substrat de reference (reproduction de la construction ICT-15d, seed 20260720).
rng_ax = np.random.default_rng(20260720)
strategies_ax = M.make_strategies(rng_ax)
A = M.payoff_matrix(strategies_ax, n_rounds=200, n_reps=3, rng=rng_ax)
n_strat = A.shape[0]
x0 = np.full(n_strat, 1.0 / n_strat)
n_names = list(strategies_ax.keys())

def replicator(A, x0, n_steps, mu=0.0):
    """Replicateur pur (mu=0) ou + mutation vers l'uniforme."""
    n = A.shape[0]; x = np.asarray(x0, float).copy(); x /= x.sum()
    tr = np.empty((n_steps + 1, n)); tr[0] = x
    for t in range(n_steps):
        f = A @ x; avg = float(np.dot(f, x))
        xm = (1 - mu) * x * f / max(avg, 1e-12) + mu / n
        s = xm.sum(); x = xm / s if s > 1e-12 else x
        tr[t + 1] = x
    return tr

def replicator_mutation(A, x0, n_steps, mu, rng):
    """Replicateur + mutation TURBULENTE : injection d'un variant aleatoire."""
    n = A.shape[0]; x = np.asarray(x0, float).copy(); x /= x.sum()
    tr = np.empty((n_steps + 1, n)); tr[0] = x
    for t in range(n_steps):
        f = A @ x; avg = float(np.dot(f, x))
        xm = (1 - mu) * x * f / max(avg, 1e-12)
        j = int(rng.integers(0, n)); xm[j] += mu      # un variant apparait
        s = xm.sum(); x = xm / s if s > 1e-12 else x
        tr[t + 1] = x
    return tr

# Instantane (mu=0) = substrat de Gate 4 / ICT-15d ; uniforme et turbulent
traj_static  = replicator(A, x0, n_steps, 0.0)
traj_uniform = replicator(A, x0, n_steps, 0.10)
mu_tur = 0.10
traj_tur     = replicator_mutation(A, x0, n_steps, mu_tur, np.random.default_rng(7))

def dominant(traj, names):
    st = np.argmax(traj, axis=1).tolist()
    return st, len(set(st)), sum(1 for i in range(1, len(st)) if st[i] != st[i-1]), names[st[-1]]

print(f"Axe Axelrod (n_strat={n_strat}) sur {n_steps} generations :")
for lab, tr in [("instantané (mu=0)", traj_static),
                ("mutation uniforme (mu=0.10)", traj_uniform),
                ("mutation turbulente (mu=0.10)", traj_tur)]:
    st, uniq, trn, last = dominant(tr, n_names)
    print(f"  {lab:28s} : strategies visitees={uniq:>2d}  changements de dominante={trn:>3d}  finale={last}")
Axe Axelrod (n_strat=6) sur 400 generations :
  instantané (mu=0)            : strategies visitees= 2  changements de dominante=  1  finale=tft
  mutation uniforme (mu=0.10)  : strategies visitees= 2  changements de dominante=  1  finale=tft
  mutation turbulente (mu=0.10) : strategies visitees= 6  changements de dominante=104  finale=grim
# Gate 5a (suite) : trajectoire de la dominante et des 3 strategies top
best3 = [n_names[i] for i in np.argsort(-A.mean(axis=1))[:3]]
fig, axes = plt.subplots(1, 2, figsize=(13, 4), sharey=True)
for ax, traj, lab in [(axes[0], traj_static, "mu=0 (instantané, Gate 4)"),
                      (axes[1], traj_tur, "mutation turbulente (mu=0.10)")]:
    for nm in best3:
        ax.plot(traj[:, n_names.index(nm)], label=nm)
    ax.set_xlabel("génération t"); ax.set_ylabel("fréquence x_i")
    ax.set_title("Trajectoire — " + lab); ax.legend(fontsize=7)
plt.tight_layout(); plt.show()
print("Turbulent : la frequence de tft n'est plus figee — la population change de strategie.")

Turbulent : la frequence de tft n'est plus figee — la population change de strategie.
# Gate 5b : b1 du substrat — instantane vs uniforme vs turbulent (mesure ICT-15d)
from ict import cech_obstruction as CO
from ict import nerve_discriminant as ND
from ict import spectral as SP
from ict import sensitivity as SE

def _sg(st, ns): return float(SP.spectral_summary(st, ns)['spectral_gap'])
def _sm(st, ns):
    f = lambda x: x; return float(SE.sensitivity_distribution(st, ns, f)['mean'])
def _sx(st, ns):
    f = lambda x: x; return float(SE.sensitivity_distribution(st, ns, f)['max'])
PROX = {"spectral_gap": _sg, "sens_mean": _sm, "sens_max": _sx}

def measure_b1(traj, label):
    st = np.argmax(traj, axis=1).tolist()
    ws = max(2, len(st) // 30)
    sec = CO.proxy_sections(st, n_strat, ws, PROX)
    return ND.nerve_b1(sec, label, epsilon_quantile=0.55)

res = [measure_b1(traj_static, "axelrod instantane (mu=0)"),
       measure_b1(traj_uniform, "axelrod uniforme (mu=0.10)"),
       measure_b1(traj_tur,     "axelrod turbulent (mu=0.10)")]
print("=== b1 du substrat Axelrod : trois dynamiques ===")
print(f"{'substrat':30s}{'aretes':>7s}{'triangles':>10s}{'b1':>5s}{'b1_max_pers':>12s}")
for r in res:
    print(f"{r.substrat:30s}{r.n_edges:>7d}{r.n_triangles:>10d}{r.b1:>5d}{r.b1_max_persistence:>12.4f}")
print(f"\nDelta b1 (turbulent - instantane) = {res[2].b1_max_persistence - res[0].b1_max_persistence:+.4f}")
=== b1 du substrat Axelrod : trois dynamiques ===
substrat                       aretes triangles   b1 b1_max_pers
axelrod instantane (mu=0)         435      4060    0      0.0000
axelrod uniforme (mu=0.10)        435      4060    0      0.0000
axelrod turbulent (mu=0.10)       240      1088    0      0.2444

Delta b1 (turbulent - instantane) = +0.2444

Lecture du Gate 5

Chiffres mesures (relis dans les sorties ci-dessus) :

dynamique aretes triangles b1 b1_max_persistence
instantane (mu=0, Gate 4) 435 4060 0 0.0000
mutation uniforme (mu=0.10) 435 4060 0 0.0000
mutation turbulente (mu=0.10) 240 1088 0 0.2444

Lecture honnete : la mutation vers l’uniforme ne desature rien — tft est une ESS et la population y retombe (b1_max_persistence = 0.0000, aretes et triangles inchanges). C’est la mutation turbulente (injection d’un variant) qui fait circuler la population entre strategies : la dominante change 104 fois (grim/tft/pavlov/alld visites), le complexe s’ouvre (aretes 435→240, triangles 4060→1088), et b1_max_persistence remonte a 0.2444. Le relief du substrat Axelrod n’est donc pas une propriete du bruit, mais de la nouveaute strategique — precisement ce que le user pointait (une population figee ne trace pas de trajectoire).

Lecture a confirmer : c’est exactement l’objet du Gate 5c ci-dessous (#13308) — le relief du kick est mis a l’epreuve d’un changement de generateur. Hypothese sous test, pas une loi.

6c. Gate 5c — indépendance au générateur de nouveauté (#13308)

Le b1 = 0,2444 du Gate 5b repose sur un seul mécanisme dit « de mutation turbulente » : un kick aléatoire de masse μ sur un vocabulaire fixe. C’est la même classe de risque de circularité que le détecteur d’humour (#13306) : le générateur injecte précisément le type de nouveauté dont on veut montrer qu’elle produit du relief. Tant que la propriété n’a pas survécu à un changement de générateur, b1 > 0 peut n’être que le relief mis dans le générateur en le construisant.

Protocole #13308 — régimes appariés (même substrat A, même x0, n_steps = 400, μ = 0,10, même fenêtrage b1, même ε-quantile) :

Régime Mécanisme Vocabulaire Ce qui croît
R1 uniforme (réf.) mutation uniforme μ (Gate 5a) fixe (n = 6) rien
R2 turbulente-A (6b) kick aléatoire +μ par génération fixe (n = 6) rien
R3 innovation-B une stratégie nouvelle (comportement tiré, gains mesurés par le vrai moteur play_match) arrive toutes les K = 40 générations avec masse μ croît (6 → 16) comportement et dimension
R4 contrôle dimension même calendrier et masse que R3, mais l’arrivant est un duplicata exact (ligne ET colonne copiées d’un incumbent) croît (6 → 16) dimension seule

R3 et R4 partagent dimension finale, calendrier d’arrivée et masse d’arrivée à l’identique — leur écart isole la nouveauté comportementale par construction. Verdicts (#13308) : ROBUSTE_AU_GENERATEUR (R2 > 0 et R3 > 0, R4 ≈ 0) / ARTEFACT_DE_GENERATEUR (R2 seul > 0) / CONFONDU_AVEC_LA_DIMENSION (R4 > 0) / INCONCLUSIVE. Mesure multi-seeds (5) : le 0,2444 du 6b était un tirage simple, l’incertitude d’échantillonnage est affichée, pas escamotée (leçon #12936).

# Gate 5c (#13308) : generateur B (innovation de vocabulaire, gains mesures par
# le vrai moteur) + controle negatif de dimension (duplicata exact). Apparie au
# R2 du 6b : meme substrat A, meme x0, meme n_steps, meme mu.
SEEDS_5C = [7, 42, 13, 99, 2024]
K_ARRIVAL, MU_5C = 40, 0.10
N_ARRIVALS = n_steps // K_ARRIVAL            # 10 arrivees sur 400 generations

def _new_strategy(p_coop, rng):
    """Strategie aleatoire NOUVELLE (p_coop), meme pattern de factory a rng
    injecte que make_strategies pour gtft (le random_strategy du module rend
    une closure sans generateur, inutilisable -- ecart signale dans la PR)."""
    def _rnd(own, opp):
        return M.C if rng.random() < p_coop else M.D
    return _rnd

def _row_col_new(new_s, incumbents, rng):
    """Ligne/colonne de gains d'une strategie NOUVELLE contre les incumbents
    COURANTS, meme protocole que payoff_matrix (n_rounds=200, moyenne de 3 matchs)."""
    row, col = [], []
    for inc in incumbents:
        row.append(np.mean([M.play_match(new_s, inc, n_rounds=200, rng=rng)[0] for _ in range(3)]))
        col.append(np.mean([M.play_match(inc, new_s, n_rounds=200, rng=rng)[0] for _ in range(3)]))
    diag = np.mean([M.play_match(new_s, new_s, n_rounds=200, rng=rng)[0] for _ in range(3)])
    return row, col, diag

def replicator_growing(mode, seed):
    """Replicateur + mutation uniforme (mu) sur un vocabulaire qui croit.
    mode='innovation' : l'arrivant est une strategie NOUVELLE (p_coop ~ U(0.1, 0.9),
    gains mesures par le vrai moteur play_match).
    mode='duplicate' : l'arrivant copie exactement la ligne ET la colonne d'un
    incumbent tire au hasard -- la dimension croit, la nouveaute est nulle.
    Arrivee : masse mu sur le nouveau, incumbents remis a l'echelle (1-mu) puis
    renormalisation (kick de meme amplitude que le turbulente-A)."""
    rng = np.random.default_rng(seed)
    Ag = A.copy()
    strats = [strategies_ax[nm] for nm in n_names]   # vocabulaire courant (croit)
    x = np.full(Ag.shape[0], 1.0 / Ag.shape[0])
    st = np.empty(n_steps + 1, dtype=int)
    st[0] = int(np.argmax(x))
    for t in range(1, n_steps + 1):
        n = Ag.shape[0]
        f = Ag @ x
        avg = float(np.dot(f, x))
        xm = (1 - MU_5C) * x * f / max(avg, 1e-12) + MU_5C / n
        if t % K_ARRIVAL == 0 and n < 6 + N_ARRIVALS:
            if mode == "innovation":
                new_s = _new_strategy(float(rng.uniform(0.1, 0.9)), rng)
                row, col, diag = _row_col_new(new_s, strats, rng)
                strats.append(new_s)
                Ag = np.vstack([np.hstack([Ag, np.array(col).reshape(-1, 1)]),
                                np.array(row + [diag]).reshape(1, -1)])
            else:
                k = int(rng.integers(0, n))
                Ag = np.vstack([np.hstack([Ag, Ag[:, k:k + 1]]),
                                np.append(Ag[k, :], Ag[k, k]).reshape(1, -1)])
            xm = np.append((1 - MU_5C) * xm, MU_5C)
        x = xm / xm.sum()
        st[t] = int(np.argmax(x))
    return st, Ag.shape[0]

def states_traj_tur(seed):
    tr = replicator_mutation(A, x0, n_steps, MU_5C, np.random.default_rng(seed))
    return np.argmax(tr, axis=1)

# Appariement MONTRÉ (critère 2 de #13308) — tout ce qui est commun aux régimes :
import hashlib
h_sub = hashlib.sha256(np.asarray(A, dtype=float).tobytes()).hexdigest()[:8]
print("=== Appariement inter-régimes ===")
print(f"  substrat A sha256[:8] = {h_sub} (identique R1-R4, reproduction ICT-15d)")
print(f"  x0 uniforme 1/6 ; n_steps = {n_steps} ; mu = {MU_5C} ; K_arrivee = {K_ARRIVAL}")
print(f"  masse d'arrivee = mu ; dimension finale R3 = R4 = {6 + N_ARRIVALS}")
print(f"  fenetrage b1 ws = max(2, (n_steps+1)//30) ; epsilon_quantile = 0.55 ; seeds = {SEEDS_5C}")
print(f"  R2 reutilise replicator_mutation (kick) ; R3/R4 = replicator_growing (vocabulaire croissant)")
=== Appariement inter-régimes ===
  substrat A sha256[:8] = 7527a2ca (identique R1-R4, reproduction ICT-15d)
  x0 uniforme 1/6 ; n_steps = 400 ; mu = 0.1 ; K_arrivee = 40
  masse d'arrivee = mu ; dimension finale R3 = R4 = 16
  fenetrage b1 ws = max(2, (n_steps+1)//30) ; epsilon_quantile = 0.55 ; seeds = [7, 42, 13, 99, 2024]
  R2 reutilise replicator_mutation (kick) ; R3/R4 = replicator_growing (vocabulaire croissant)
# Mesure b1 par regime et par seed (R1 deterministe : mu uniforme sans rng -> 1 run).
def b1_persist_5c(states, n_sym):
    ws = max(2, len(states) // 30)
    sec = CO.proxy_sections([int(s) for s in states], n_sym, ws, PROX)
    return ND.nerve_b1(sec, "x", epsilon_quantile=0.55).b1_max_persistence

n_sym_R2 = A.shape[0]
rows = {}
r1_states = np.argmax(replicator(A, x0, n_steps, MU_5C), axis=1)
rows[("R1 uniforme (ref.)", "fixe")] = [b1_persist_5c(r1_states, n_sym_R2)]
for seed in SEEDS_5C:
    rows.setdefault(("R2 turbulente-A (kick)", "fixe"), []).append(b1_persist_5c(states_traj_tur(seed), n_sym_R2))
for mode, label in [("innovation", "R3 innovation-B"), ("duplicate", "R4 controle dimension")]:
    for seed in SEEDS_5C:
        st, n_fin = replicator_growing(mode, seed)
        assert n_fin == 6 + N_ARRIVALS, n_fin
        rows.setdefault((label, "croit"), []).append(b1_persist_5c(st, n_fin))

print("=== b1_max_persistence par regime (H1 max, filtration Rips complete) ===")
print(f"{'regime':30s}{'vocab':7s}{'runs':>6s}{'b1_max (par seed/run)':>34s}{'moyenne':>9s}")
summary = {}
for (label, voc), vals in rows.items():
    summary[label] = (float(np.mean(vals)), float(np.std(vals)), max(vals))
    per = "[" + ", ".join(f"{v:.4f}" for v in vals) + "]"
    print(f"{label:30s}{voc:7s}{len(vals):>6d}{per:>34s}{np.mean(vals):>9.4f}")

m2, s2, x2 = summary["R2 turbulente-A (kick)"]
m3, s3, x3 = summary["R3 innovation-B"]
m4, s4, x4 = summary["R4 controle dimension"]
print()
print(f"Delta R3-R4 (nouveaute isolee, dimension controlee) : {m3 - m4:+.4f}")
print(f"R2 max (repli du tirage simple du 6b, seed 7 inclus) : {x2:.4f} (6b : 0.2444)")
=== b1_max_persistence par regime (H1 max, filtration Rips complete) ===
regime                        vocab    runs             b1_max (par seed/run)  moyenne
R1 uniforme (ref.)            fixe        1                          [0.0000]   0.0000
R2 turbulente-A (kick)        fixe        5[0.2444, 0.4106, 0.3346, 0.0259, 0.1363]   0.2304
R3 innovation-B               croit       5[0.0000, 0.0000, 0.0000, 0.0000, 0.0000]   0.0000
R4 controle dimension         croit       5[0.0000, 0.0000, 0.0000, 0.0000, 0.0000]   0.0000

Delta R3-R4 (nouveaute isolee, dimension controlee) : +0.0000
R2 max (repli du tirage simple du 6b, seed 7 inclus) : 0.4106 (6b : 0.2444)

Verdict du Gate 5c : ARTEFACT_DE_GENERATEUR

Mesures (5 seeds, sortie ci-dessus) : R2 = 0,2304 ± 0,1447 avec le seed 7 à 0,2444 — réplique exacte du tirage simple du 6b, la mécanique est reproduite ; R3 (innovation réelle) = 0,0000 sur les 5 seeds ; R4 (croissance de dimension seule) = 0,0000 sur les 5 seeds ; Δ R3−R4 = +0,0000.

Par la règle de décision du protocole, le relief b1 > 0 du Gate 5b ne survit pas au changement de générateur : la nouveauté comportementale (stratégies nouvelles, gains mesurés par le vrai moteur, vocabulaire 6 → 16) ne produit aucun relief, pas plus que la croissance de dimension seule (R4 ≈ 0 écarte en outre CONFONDU_AVEC_LA_DIMENSION). La circulation de la dominante observée au 6b est portée par le mécanisme d’injection de masse (kick de μ sur les incumbents du vocabulaire fixe, chaque génération), pas par la nouveauté — le b1 du 6b mesurait son générateur, pas une propriété de l’innovation.

Portée et limite (honnête) : les arrivants R3 sont des coopérateurs aléatoires (p_coop ∼ U(0,1–0,9)) — des stratégies faibles face à tft/grim, qui n’envahissent jamais. Le verdict établi est donc « le relief du 6b n’est pas produit par la nouveauté telle que définie par le protocole » ; il n’exclut pas qu’un générateur d’arrivants forts (mutants d’incumbents, variantes de tft) produise du relief — suite possible hors scope #13308. De même, la fréquence d’arrivée (une fois par 40 générations contre un kick par génération) fait partie de ce que « changement de générateur » signifie : c’est précisément le kick haute fréquence qui portait le 6b.

7. Verdict sans complaisance

  • Gate 1 : sans bruit, TFT et grim dominent le tournoi (2,635), AllD est dernier (2,313) — reproduction numérique d’Axelrod (sortie simulee, non recopiee).

  • Gate 2 : le seuil analytique \(\delta^*=(T-R)/(T-P)=0{,}5\) est retrouve numériquement (croisement a 0,55, accord a un pas de grille). La théorie de GT-6c tient : grim resiste a AllD au-dela du seuil.

  • Gate 3 : le régime-dépendance est confirme — TFT s’effondre sous bruit (chute +0,40, la plus forte : l’echo mort-a-mort detruit son mécanisme de réciprocité active). La théorie voudrait que GTFT/Pavlov resistent proprement ; le verdict est plus nuance : c’est grim qui montre la plus faible chute (+0,29), devant pavlov (+0,38), gtft (+0,39) et allc (+0,37). L’insight honnete n’est pas « Pavlov robuste » mais son envers morphodynamique : le mécanisme qui fait gagner TFT sans bruit (réciprocité active) est précisément ce que le bruit detruit, tandis que la rigidite de grim — un défaut sans bruit — devient une stabilite relative sous bruit. Aucune stratégie cooperative n’est epargnee : un tournoi charge d’AllD ronge tout le monde.

  • Gate 4 : les bassins d’invasion sont inesgaux. TFT/grim envahissent AllD a seuil 0,02 ; gtft a 0,34 ; pavlov et allc ne l’envahissent jamais (seuil 1,0). « Cooperateur » n’est pas un rôle uniforme : la morphologie d’invasion discrimine les stratégies coopératrices.

  • Évolution (sélection-mutation) : avec mutation 5 %, la population ne fixe pas — équilibre polymorphe grim/AllC/GTFT (TFT éliminée), taux de coopération oscillant 0,26–0,94, 70 changements de dominante. La population trace une trajectoire, pas un point fixe : c’est le relief que le nerf simplicial d’ICT-15d détectera (b1 = 0 sur l’instantané saturé).

  • Gate 5 (mesure b1, observable argmax) : sur l’observable argmax de ICT-15d, aucune des dynamiques de ce gate ne desature le substrat — l’instantane (mu=0) et la mutation uniforme (mu=0,10) restent a b1 = 0.0000 (tft est une ESS, la population y retombe). Cette saturation est une propriete de l’observable argmax, pas du substrat : le meme substrat regenere et quantifie en taux de cooperation (8 symboles, section 6) donne b1 = 0,3989 sur ICT-15j (#13305) — le plan a deux facteurs dynamique x observable reste a croiser integralement (#13039).

  • Gate 5b : la strategie peut changer. Le replicateur-mutateur (mu=0,02) fait evoluver la population sur 400 generations : la dominante visite plusieurs strategies (au lieu d’une seule), la trajectoire n’est plus un point fixe. Le substrat Axelrod re-mesure contre l’instantane de Gate 4 : chiffres et verdict dans la table du Gate 5 (honnete meme si b1 reste nul, G.2/G.3).

  • Gate 5c : le relief du Gate 5b ne survit pas au changement de générateur. Sur 5 seeds appariés, seul le kick turbulente-A garde b1 > 0 (0,230 ± 0,145 ; seed 7 = 0,2444, réplique du 6b) ; l’innovation réelle (nouvelles stratégies, gains mesurés par le vrai moteur, vocabulaire 6 → 16) et le contrôle dimension (duplicata exact, même calendrier) restent à b1 = 0. Verdict ARTEFACT_DE_GENERATEUR : la circulation de la dominante du 6b est portée par le mécanisme d’injection de masse, pas par la nouveauté (#13308). Conclusion : une stratégie n’est stable que dans un paysage donne. TFT, gagnante sans bruit, devient fragile des que le régime devient incertain — et c’est exactement son atout sans bruit (la réciprocité) qui la perd sous bruit ; grim, stable au-dela de \(\delta^*\), s’effondre en horizon court. Le fil régime-dépendance, ouvert en ICT-10 (le \(\hat{p}\)) et prolonge en ICT-12 (animats), trouve ici sa troisieme incarnation : la robustesse d’une stratégie est une fonction du régime d’interaction, pas une propriete intrinseque.

8. Exercices

Trois exercices (a completer). Stubs sans erreur volontaire ; le notebook s’execute de bout en bout même non complétée.

Exercice 1 — stratégie « tester-then-defect »

Implementez une stratégie qui coopere 3 coups puis defocte systematiquement (exploiteur tardif). Mesurez son score au tournoi et son bassin d’invasion face a AllD. Objectif : determinez si elle bat TFT.

Indice : ajoutez une entree dans make_strategies ; M.round_robin et M.invasion_basin prennent le dict.

# Exercice 1 : a completer
def test_then_defect(own, opp):
    # TODO etudiant : cooperer si len(own) < 3, sinon defecter
    return None
resultat_ex1 = None  # TODO etudiant
print("Exercice 1 a completer")
Exercice 1 a completer

Exercice 2 — seuil de grim pour un autre jeu

Recomputez le seuil analytique \(\delta^*=(T-R)/(T-P)\) et le croisement numérique pour un PD modifie (T=4, R=3, P=2, S=1). Objectif : verifiez que l’accord analytique/numérique se maintient.

Indice : passez T=,R=,P=,S= a M.grim_continuation_payoff et M.grim_threshold.

# Exercice 2 : a completer
seuil_ex2 = None    # TODO etudiant : M.grim_threshold(T=4,R=3,P=1) etc.
croisement_ex2 = None  # TODO etudiant
print("Exercice 2 a completer")
Exercice 2 a completer

Exercice 3 — invasion de GTFT dans une population mixte

Cartographiez le bassin d’invasion de GTFT face a un resident Pavlov (pas AllD). Objectif : determinez si GTFT peut envahir une population Pavlov, et a quel seuil.

Étape 1 : construisez la matrice de gain 2-stratégies (gtft, pavlov) via M.payoff_matrix. Étape 2 : M.invasion_basin(A, invader_idx=0, resident_idx=1).

# Exercice 3 : a completer
resultat_ex3 = None  # TODO etudiant : seuil critique gtft-vs-pavlov
print("Exercice 3 a completer")
Exercice 3 a completer

Exercice 4 — seuil de mutation turbulente qui desature le b1

Faites varier le taux \(\mu\) de la mutation turbulente (replicator_mutation) dans \(\{0.02, 0.05, 0.10, 0.20, 0.40\}\) et mesurez b1_max_persistence pour chaque. Objectif : determinez le seuil \(\mu\) a partir duquel le substrat Axelrod cesse d’etre sature (b1 > 0), et expliquez la tension selection (qui fige sur tft) vs mutation (qui fait apparaitre des variants).

Indice : bouclez sur replicator_mutation + measure_b1 sur une meme graine.

# Exercice 4 : a completer — tension selection vs mutation turbulente
seuil_mu_ex4 = None   # TODO etudiant : plus petit mu avec b1_max_persistence > 0
b1_par_mu = {}        # TODO etudiant : {mu: b1_max_persistence}
print("Exercice 4 a completer")
Exercice 4 a completer

Bilan — ICT-13 : une stratégie n’est PAS une forme stable, elle est régime-dépendante

Ce notebook pose la question centrale de la strate 3 : une stratégie est-elle une forme stable dans un paysage d’interactions ? La réponse, falsifiée gate par gate sur le dilemme du prisonnier itéré d’Axelrod (\(T=5, R=3, P=1, S=0\)), est non — la stabilité d’une stratégie est conditionnelle au régime de bruit et à la composition de la population. La bistabilité d’ICT-10 devient ici bistabilité coopération / défection, et le verdict dépend de 4 gates empilés plus la dynamique évolutive (sélection-mutation, #12673) : une population qui évolue ne converge pas, elle oscille — l’équilibre mutation-sélection est polymorphe.

Les 7 gates du verdict (tous falsifiables)

Gate Test Résultat Ce qu’il prouve
Gate 1 Tournoi round-robin sans bruit TFT + grim dominent (2 635), AllD dernier (2 313) Axelrod reproduit : la coopération gagne en milieu propre
Gate 2 Seuil de grim trigger \(\delta^* = (T-R)/(T-P) = 0{,}5\) retrouvé numériquement (croisement à 0,55) La théorie de GT-6c tient : grim résiste à AllD au-delà du seuil
Gate 3 Régime-dépendance (bruit) TFT s’effondre (chute ~0,40) Une stratégie gagnante sans bruit peut perdre sous bruit
Gate 4 Bassins d’invasion Selon la population initiale La stabilité dépend de la composition du bassin
Évolution (section 6, #13305) Wright-Fisher 60 agents, mutation 5 %, substrat quantifié en taux de coopération (8 symboles) Équilibre polymorphe grim/AllC/GTFT, coopération 0,26–0,94 ; substrat régénéré b1 = 0,3989 sur ICT-15j (NON_TRIVIAL) Une population qui évolue creuse du relief — sur la bonne observable ; l’instantané argmax était saturé
Gate 5 (5a/5b, observable argmax) Évolution du substrat sous sélection-mutation, mesure b1 argmax Instantané et uniforme : b1 = 0 ; kick turbulent : b1 = 0,2444 (seed 7) Sur l’observable argmax, seule l’injection de masse fait circuler la dominante
Gate 5c (#13308) Indépendance au générateur de nouveauté (4 régimes appariés, 5 seeds) Kick seul > 0 (0,230 ± 0,145) ; innovation réelle et dimension ≈ 0 Le relief b1 du 5b est un artefact du générateur kick, pas de la nouveauté

La leçon centrale : régime-dépendance

Aucune stratégie n’est absolument stable. TFT est robuste sans bruit et fragile sous bruit ; grim résiste à AllD au-dessus d’un seuil mais est vulnérable en dessous ; Pavlov oscille. La même stratégie peut dominer ou s’effondrer selon le régime — c’est pourquoi le verdict s’exprime en gates falsifiables (chaque gate est reproductible numériquement, non recopié) plutôt qu’en claim binaire « TFT est la meilleure ».

Lien avec le reste du cursus

  • ICT-10 (bistabilité cinématique) → ICT-13 (bistabilité stratégique) : le motif « deux attracteurs en compétition » se transfère d’un substrat à l’autre.
  • GT-6c (théorie du seuil de grim trigger) : le \(\delta^*\) analytique d’ICT-13 Gate 2 confirme la prédiction théorique de GameTheory-6c — pont ICT ↔︎ GameTheory.
  • Axelrod (1984) : la reproduction numérique du tournoi historique valide l’implémentation avant de la stresser (Gates 3-4 = robustesse).

À retenir

Une stratégie n’est pas une forme (stable, absolue) mais un opérateur contextuel : elle n’a de sens qu’indexée à un régime de bruit, une composition de population et un horizon temporel. C’est cette contextualisation qui fait passer la théorie des jeux de la stratégie à la morphodynamique — la strate 3 de la série ICT.


Retour au sommaire : Index ICT-Series

Retour au sommet