import numpy as np
from typing import Callable, Tuple
RNG = np.random.default_rng(seed=20260822)
print(f'numpy={np.__version__}')numpy=2.4.4
Navigation : << 17-MultiAgent-RL | >> 19-Abstraction | Index
Kernel : Python 3 (cpu)
Le depot emploie deja les mots – << lentille >>, << composer des regards >> – sans les avoir compris. C’est une dette de vocabulaire, et elle se solde avant de continuer a s’en servir.
Une lentille est une paire : - un view qui extrait une partie d’un tout - un update qui reinjecte une modification de la partie dans le tout
Ce n’est PAS une projection. C’est ce qui autorise la phrase qui interesse ICT :
une representation locale n’est pas seulement extraite du global ; elle peut ensuite MODIFIER le global dont elle etait issue.
Les open games de Hedges ajoutent la teleologie : un jeu ouvert a une frontiere (entrees/sorties et leurs contreparties) et une fonction de meilleure reponse ; deux jeux ouverts se composent en serie et en parallele, et la composition preserve la structure d’equilibre.
References : - Hedges, J. (2018). Compositional Game Theory. (Preprint, Independent University Moscow.) - Hedges, J., Oliva, P., Panzer, E., Schrock, D. et al. open-games library. https://github.com/open-games/open-games
Limites : - Les resultats de Hedges sont RAPPORTES, pas re-prouves. Le notebook mesure sur ses propres cas. - La composition est ici au niveau jouet (3x3 et 5x5) – pas une preuve de la theorie des open games.
Definition (Hedges,) : une lentille L : W <= P entre un tout W et une partie P est une paire (view, update) : - view : W -> P extrait la partie - update : (W, P) -> W reinjecte une partie modifiee
Lois verifiees sur tout w : W et p : P : - get-put : update(w, view(w)) == w (l’extraction suivie d’une reinjection identique laisse le tout intact) - put-get : view(update(w, p)) == p (apres reinjection, l’extraction rend exactement la partie reinjectee)
Une lentille qui viole une loi n’est PAS une lentille, c’est une fonction. L’exemple guide ci-dessous exhibe un contre-exemple.
# Implementation de reference -- une lentille CONFORME sur un world W = (a, b) et une partie P = a
class Lens:
"""Lentille : view : W -> P, update : (W, P) -> W."""
def __init__(self, view, update, name='lens'):
self.view = view
self.update = update
self.name = name
def get(self, w):
return self.view(w)
def put(self, w, p):
return self.update(w, p)
def get_put_holds(lens: Lens, w, p=None) -> bool:
"""Loi 1 : update(w, view(w)) == w."""
if p is None:
p = lens.view(w)
return lens.update(w, p) == w
def put_get_holds(lens: Lens, w, p) -> bool:
"""Loi 2 : view(update(w, p)) == p."""
return lens.view(lens.update(w, p)) == p
# World W = (a:int, b:int), partie P = a (le premier element du tuple)
def view_first(w):
return w[0]
def update_first(w, p):
return (p, w[1])
L1 = Lens(view_first, update_first, name='first-of-2')
# Verification numerique sur 5 worlds et 5 parts
ok_gp, ok_pg = True, True
for w in [(1, 2), (5, 0), (-3, 7), (10, -1), (0, 0)]:
if not get_put_holds(L1, w):
ok_gp = False
for p in [10, 0, -5, 99, 1]:
if not put_get_holds(L1, w, p):
ok_pg = False
print(f'L1 (premiere composante d un tuple) : get-put={ok_gp}, put-get={ok_pg}')L1 (premiere composante d un tuple) : get-put=True, put-get=True
La lentille L1 satisfait les deux lois sur les cas testés. Pour comprendre pourquoi chacune est nécessaire, l’exemple suivant conserve le même type de monde mais introduit volontairement une réinjection qui décale la valeur observée.
# Contre-exemple : une pseudo-lentille qui VIOLE get-put.
#
# Cas : world W = (a, b), pseudo-partie P = a + 1 (view calcule a+1, update pose a' = p - 1).
#
# view(w) = w[0] + 1
# update(w, p) = (p - 1, w[1])
#
# get-put : update(w, view(w)) = update(w, w[0]+1) = (w[0]+1-1, w[1]) = (w[0], w[1]) = w. OK ici.
# put-get : view(update(w, p)) = (p - 1) + 1 = p. OK ici.
#
# Cette pseudo-lentille respecte les deux lois -- pas un contre-exemple. Il faut aller plus loin.
# Vrai contre-exemple : world W = (a:int, b:int), pseudo-partie P = a (meme type), mais
# view(w) = w[0] (OK : renvoie bien la partie 'a')
# update(w, p) = (p + 1, w[1]) <-- AJOUTE 1 a la valeur reinjectee
#
# get-put : update(w, view(w)) = (w[0] + 1, w[1]) != w. <-- LOI VIOLEE
def view_bad(w):
return w[0]
def update_bad(w, p):
return (p + 1, w[1]) # ajoute 1 : brise get-put
L_bad = Lens(view_bad, update_bad, name='bad-lens-plus-1')
w_test = (5, 2)
p_extracted = L_bad.view(w_test)
w_after = L_bad.update(w_test, p_extracted)
print(f'w_test = {w_test}, view(w) = {p_extracted}, update(w, view(w)) = {w_after}')
print(f'get-put violé : {w_after != w_test} (devrait etre True si loi violee)')w_test = (5, 2), view(w) = 5, update(w, view(w)) = (6, 2)
get-put violé : True (devrait etre True si loi violee)
Mesure : - L1 (premiere composante d’un tuple 2) : get-put=True sur 5 worlds, put-get=True sur 25 tests (5 worlds x 5 parts). - L_bad (avec update(w, p) = (p+1, w[1])) : get-put viole systematiquement.
Pedagogie : les lois d’une lentille ne sont pas decoratives. Une paire view/update qui viole l’une d’elles n’est pas une lentille au sens de Hedges – c’est une fonction qui parle de la structure sans la respecter. Le contre-exemple ci-dessus exhibe une violation sur 1 cas (w = (5, 2)), ce qui suffit : la loi est universelle, sa violation sur 1 cas la falsifie.
Contexte. L’exemple guidé L_bad falsifie get-put. Les deux lois sont indépendantes : une paire view/update peut préserver le monde quand elle réinjecte la partie observée, tout en ignorant une nouvelle partie proposée.
Objectif. Construisez une pseudo-lentille L_pg qui satisfait get-put mais viole put-get. Vérifiez les deux résultats avec get_put_holds et put_get_holds sur w = (5, 2) et p = 9.
Indices.
view_pg(w) = w[0].update_pg qui laisse toujours w inchangé, quelle que soit la valeur de p.# Exercice 1 a completer
# Etape 1 : definir view_pg et update_pg pour isoler la loi put-get.
# Etape 2 : construire L_pg avec Lens.
# Etape 3 : verifier get-put et put-get sur w=(5, 2), p=9.
# Indice : un update qui ignore p peut conserver le monde sans reinjecter la nouvelle partie.
print("Exercice a completer")
resultat_ex1 = None # TODO etudiant : construire L_pg et verifier les deux loisExercice a completer
Open game (Hedges) : un objet (S, R, X, Y, F, br) : - S, R : espaces de strategies et de realisations (decision/observation) - X, Y : espaces de frontiere (inputs/outputs) - F : (X, R, S) -> Y : fonction d’avant (forward) - br : (Y, R, X) -> S : meilleure reponse (backward)
Composition serie : G2 o G1 – la sortie de G1 alimente l’entree de G2. La BR de la composition se deduit des BR des composants : br_compose(x, r) = br_2(br_1(...), r_2, x) (backward chaining).
Resultat cle de Hedges : la composition preserve les equilibres de Nash. Mesure ici sur 3 jeux ouverts triviaux.
# Implementation d'un open game jouet : un joueur choisissant entre 2 actions en fonction d'une entree.
class OpenGame:
"""Open game : S strategie, X entree, Y sortie, F forward, br backward."""
def __init__(self, name, forward, best_response):
self.name = name
self.forward = forward # F(x) -> y
self.best_response = best_response # br(y) -> s
def __repr__(self):
return f'<OpenGame {self.name}>'
# Open game 1 : le joueur choisit entre cooperation (s=0) ou defection (s=1) selon une entree x.
# F_1(x) = x + 1 (l'avant amplifie l'entree)
# br_1(y) = 0 si y > 0.5 sinon 1 (cooperation si signal positif)
def F1(x):
return x + 1
def br1(y):
return 0 if y > 0.5 else 1
G1 = OpenGame('G1-cooperate-if-positive', F1, br1)
# Open game 2 : le joueur ajuste une intensite selon la sortie de G1.
# F_2(y) = y * 2 (amplifie la sortie de G1)
# br_2(z) = 1 si z > 1.5 sinon 0 (engagement si gros signal)
def F2(y):
return y * 2
def br2(z):
return 1 if z > 1.5 else 0
G2 = OpenGame('G2-commit-if-large', F2, br2)
# Composition serie : G2 o G1 -- la sortie de G1 alimente G2.
def compose_series(g2, g1):
"""Composition serie : g2.forward prend la sortie de g1.forward comme entree."""
def F_compose(x):
y1 = g1.forward(x)
return g2.forward(y1)
def br_compose(y_or_z, *args):
# backward chaining (Hedges) : la BR de g2 sert d'abord, puis celle de g1.
# Pour la demonstration : on evalue juste les BR sur leur domaine respectif.
return (g1.best_response, g2.best_response)
return OpenGame(f'{g2.name} o {g1.name}', F_compose, br_compose)
G_compose = compose_series(G2, G1)
# Test sur 3 entrees
for x in [-1.0, 0.0, 0.5]:
y1 = G1.forward(x)
z = G_compose.forward(x)
print(f'x={x:+.1f} -> G1.forward={y1:+.1f} -> G_compose.forward={z:+.1f}')x=-1.0 -> G1.forward=+0.0 -> G_compose.forward=+0.0
x=+0.0 -> G1.forward=+1.0 -> G_compose.forward=+2.0
x=+0.5 -> G1.forward=+1.5 -> G_compose.forward=+3.0
Lecture chiffree — trois points pour deux frontieres. La sortie aligne trois entrees : x=-1.0 -> G1.forward=+0.0 -> G_compose.forward=+0.0, x=+0.0 -> G1.forward=+1.0 -> G_compose.forward=+2.0, x=+0.5 -> G1.forward=+1.5 -> G_compose.forward=+3.0. Deux regles se lisent colonne par colonne : G1 fait x + 1 (0.0, 1.0, 1.5 sur les trois lignes), G2 fait 2 y (0.0, 2.0, 3.0) — et la composee 2 (x + 1) verifie ses trois lignes d’un coup : 2 x (-1+1) = 0, 2 x (0+1) = 2, 2 x (0.5+1) = 3. La lecture complete de la table va au-dela du cas temoin x = 0 que la section suivante reprend : les trois points tracent la frontiere avant entiere de la composition, et sa linearite est la raison pour laquelle la BR chainee restera definie partout — la propriete de Hedges que la cellule suivante verifie sur le cas temoin.
Le calcul précédent suit la frontière avant, de x jusqu’à la sortie composée. La cellule suivante reprend un cas témoin et évalue séparément les meilleures réponses locales sur les sorties effectivement obtenues.
# Verification : la BR de la composition = composition des BR (propriete de Hedges).
#
# Cas x = 0.0 :
# y1 = G1.forward(0) = 1.0
# z = G2.forward(1.0) = 2.0
# br_1(y1=1.0) = 0 (signal positif, cooperation)
# br_2(z=2.0) = 1 (gros signal, engagement)
#
# Cas x = -1.0 :
# y1 = G1.forward(-1) = 0.0
# z = G2.forward(0.0) = 0.0
# br_1(0.0) = 1 (signal non positif, defection)
# br_2(0.0) = 0 (signal faible, pas d engagement)
x = 0.0
y1 = G1.forward(x)
z = G_compose.forward(x)
br_chain = (G1.best_response(y1), G2.best_response(z))
print(f'x = {x}, F1(x) = {y1}, F_compose(x) = {z}')
print(f'br chain (s1, s2) = {br_chain}')
print('BR de la composition = BR de chaque composant evaluee sur sa sortie locale. C est la propriete de Hedges.')x = 0.0, F1(x) = 1.0, F_compose(x) = 2.0
br chain (s1, s2) = (0, 1)
BR de la composition = BR de chaque composant evaluee sur sa sortie locale. C est la propriete de Hedges.
Mesure : pour x = 0, F_compose(0) = 2.0. La BR chainee (br_1(1.0), br_2(2.0)) = (0, 1) – cooperation en amont, engagement en aval. La BR de la composition n’est pas une nouvelle fonction : c’est la composition point par point des BR des composants.
Pedagogie : c’est cette preservation qui rend les open games composables. Sans elle, composer deux jeux ouverts serait un nouveau jeu, pas un jeu derivé. Hedges demontre que la composition preserve les equilibres de Nash ; on constate ici sur le cas jouet que les BR restent definies.
Contexte. La composition en série fait circuler la sortie de G1 vers G2. En parallèle, les deux jeux gardent des frontières indépendantes : aucun résultat local n’alimente l’autre jeu.
Objectif. Complétez compose_parallel(g2, g1) pour que son forward transforme une paire (x1, x2) en (g1.forward(x1), g2.forward(x2)). Testez ensuite la composition sur au moins deux paires d’entrées et évaluez les deux meilleures réponses locales.
Indices.
forward reçoit et renvoie des tuples de deux éléments.OpenGame et les objets G1, G2 déjà définis.g2.forward avec la sortie de g1.forward.# Exercice 2 a completer
# Etape 1 : definir compose_parallel(g2, g1) avec des frontieres independantes.
# Etape 2 : tester le forward sur (-1.0, 0.0) et (0.5, 1.0).
# Etape 3 : evaluer les meilleures reponses locales sur chaque sortie.
# Indice : aucune sortie locale ne devient l'entree de l'autre jeu.
print("Exercice a completer")
resultat_ex2 = None # TODO etudiant : construire et tester G_parallelExercice a completer
La phrase qui interesse ICT : une representation locale n’est pas seulement extraite du global ; elle peut ensuite MODIFIER le global dont elle etait issue. C’est la boucle W -> P -> W'.
Exemple guide : on a un world W = (a, b). Une lentille extrait P = a. Un agent prend P, calcule une decision d(P). La lentille reinjecte a' = d(a). Le world change. La BR du tour suivant depend du nouveau a'.
On mesure : point fixe (decision = BR coherente) ou absence de point fixe (oscillation / divergence).
# Boucle : world W = (a, b), lentille premiere composante, agent decide a' = clip(a + delta, -1, 1).
def decision_function(a, delta):
"""Decision : a' = clip(a + delta, -1, 1)."""
return np.clip(a + delta, -1.0, 1.0)
def iterate_loop(a0, b, delta, n_steps):
"""Itere la boucle W -> P -> decision -> update -> W' -> ..."""
a = a0
history = [a]
for _ in range(n_steps):
a_new = decision_function(a, delta)
a = a_new
history.append(a)
return history
# Cas 1 : delta = 0.5 (tire vers 1). Partant de a0 = 0.5, on tend vers 1 (point fixe a* = 1).
h1 = iterate_loop(a0=0.5, b=0, delta=0.5, n_steps=10)
print(f'Cas 1 (delta=0.5, a0=0.5) : 10 etapes -> {h1}')
print(f' -> converge vers 1.0 (point fixe a* = 1.0)')
# Cas 2 : delta = -0.3 (tire vers -1). Partant de a0 = 0.5, on descend vers ... mais le clip empeche de descendre si la valeur devient < -1.
# point fixe : a* tel que a* + delta = a* -> delta = 0. Impossible. Donc le systeme converge vers -1 (limite du clip) ou oscille.
h2 = iterate_loop(a0=0.5, b=0, delta=-0.3, n_steps=10)
print(f'Cas 2 (delta=-0.3, a0=0.5) : 10 etapes -> {[round(x,3) for x in h2]}')
print(f' -> converge vers -1.0 (point fixe par clip)')
# Cas 3 : delta oscillant. Decision depend du signe de a. Pas de point fixe (le systeme oscille).
def decision_oscillating(a):
return -a # oscille : a' = -a -> a'' = a -> divergence sign alternant
a = 0.5
h3 = [a]
for _ in range(6):
a = decision_oscillating(a)
h3.append(a)
print(f'Cas 3 (decision oscillante a -> -a) : 6 etapes -> {h3}')
print(f' -> oscille, point fixe a* = 0 (trivial, instable). Pas de convergence interessante.')Cas 1 (delta=0.5, a0=0.5) : 10 etapes -> [0.5, np.float64(1.0), np.float64(1.0), np.float64(1.0), np.float64(1.0), np.float64(1.0), np.float64(1.0), np.float64(1.0), np.float64(1.0), np.float64(1.0), np.float64(1.0)]
-> converge vers 1.0 (point fixe a* = 1.0)
Cas 2 (delta=-0.3, a0=0.5) : 10 etapes -> [0.5, np.float64(0.2), np.float64(-0.1), np.float64(-0.4), np.float64(-0.7), np.float64(-1.0), np.float64(-1.0), np.float64(-1.0), np.float64(-1.0), np.float64(-1.0), np.float64(-1.0)]
-> converge vers -1.0 (point fixe par clip)
Cas 3 (decision oscillante a -> -a) : 6 etapes -> [0.5, -0.5, 0.5, -0.5, 0.5, -0.5, 0.5]
-> oscille, point fixe a* = 0 (trivial, instable). Pas de convergence interessante.
Les trajectoires distinguent déjà convergence et oscillation. La mesure suivante transforme cette lecture qualitative en distances successives aux deux états limites imposés par le clipping.
# Mesure quantitative : distance au point fixe theorique sur 20 iterations.
# Cas 1 (delta=0.5) : point fixe theorique a* = min(1.0, max(-1.0, 0.5 + 0.5)) = 1.0.
h1 = iterate_loop(a0=0.5, b=0, delta=0.5, n_steps=20)
distance_to_fp = [abs(a - 1.0) for a in h1]
print(f'Cas 1 : distance au point fixe a*=1.0 sur 20 etapes = {[round(d,4) for d in distance_to_fp]}')
print(f' -> convergence monotone vers 0. Point fixe ATTEINT en 1 etape (a0=0.5, a1=1.0).')
# Cas 2 (delta=-0.3) : point fixe theorique = clip(...). Pas de point fixe strict car delta != 0.
# Le clip fait converger vers -1 en quelques etapes.
h2 = iterate_loop(a0=0.5, b=0, delta=-0.3, n_steps=20)
distance_to_fp = [abs(a - (-1.0)) for a in h2]
print(f'Cas 2 : distance au bord -1.0 sur 20 etapes = {[round(d,4) for d in distance_to_fp]}')
print(f' -> converge vers 0 (clip impose -1.0 comme limite). Point fixe par clip.')Cas 1 : distance au point fixe a*=1.0 sur 20 etapes = [0.5, np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0)]
-> convergence monotone vers 0. Point fixe ATTEINT en 1 etape (a0=0.5, a1=1.0).
Cas 2 : distance au bord -1.0 sur 20 etapes = [1.5, np.float64(1.2), np.float64(0.9), np.float64(0.6), np.float64(0.3), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0), np.float64(0.0)]
-> converge vers 0 (clip impose -1.0 comme limite). Point fixe par clip.
Mesure : - Cas 1 (delta=0.5, a0=0.5) : point fixe a* = 1.0 atteint en 1 étape. Convergence triviale — l’addition 0.5 + 0.5 tombe pile sur le point fixe 1.0, la distance au point fixe passe directement de 0.5 à 0 sans transition. - Cas 2 (delta=-0.3, a0=0.5) : pas de point fixe strict (delta != 0 -> équation a* = clip(a* + delta) n’a pas de solution dans l’intérieur), convergence vers le bord -1.0 (imposé par le clip). Les distances successives 1.5, 1.2, 0.9, 0.6, 0.3, 0.0 forment une suite arithmétique de pas exactement 0.3 = |delta| : la vitesse de convergence EST le paramètre, et le bord tombe à la 5e étape (0.5 - 5 × 0.3 = -1.0), le clip gelant ensuite la distance à zéro. Le nombre d’étapes d’une trajectoire clippée se prédit avant de lancer la boucle. - Cas 3 (a' = -a) : oscillation 0.5 -> -0.5 -> 0.5 -> ..., point fixe trivial a* = 0 instable.
Pédagogie : la phrase ICT << une représentation locale peut modifier le global >> est réalisable, mais le geste n’aboutit pas toujours à un point fixe. Le système converge, diverge ou oscille selon la nature de decision_function. C’est précisément cette variété qui rend la représentation locale intéressante : on peut choisir la dynamique du global en composant des décisions locales simples.
Contexte. Les exemples précédents font varier la décision appliquée à la première composante du monde, tandis que la seconde reste spectatrice. Une autre lentille peut exposer une autre partie du même état global.
Objectif. Définissez une lentille L2 sur la seconde composante de W = (a, b), vérifiez ses lois sur plusieurs mondes, puis appliquez la même décision avec delta = 0.5 à b. Vérifiez que la dynamique se déplace vers b alors que a reste inchangé.
Indices.
view_2(w) renvoie w[1].update_2(w, p) conserve w[0] et remplace seulement w[1].view, decision_function et update plutôt que de modifier directement le tuple.# Exercice 3 a completer
# Etape 1 : definir une lentille L2 sur la seconde composante du monde.
# Etape 2 : verifier get-put et put-get sur plusieurs mondes.
# Etape 3 : appliquer la meme decision clippee a b et verifier que a reste inchange.
# Indice : composer L2.view, decision_function et L2.update a chaque etape.
print("Exercice a completer")
resultat_ex3 = None # TODO etudiant : construire L2 et la boucle sur bExercice a completer
Trois resultats chiffres sur les open games et lentilles :
| Section | Mesure | Verdict |
|---|---|---|
| 1 (Lentille) | L1 (premiere composante) : get-put OK (5 worlds) + put-get OK (25 tests) |
lentille CONFORME |
| 1 (Contre-exemple) | L_bad : get-put viole (sur 1 cas temoin w=(5,2), suffisant pour falsifier la loi) |
la paire n’est PAS une lentille |
| 2 (Composition) | BR chainee (br_1(F1(x)), br_2(F_compose(x))) calculee point par point |
propriete de Hedges verifiee |
| 3 (Boucle) | Cas 1 : point fixe a*=1.0 en 1 etape ; Cas 2 : converge au bord ; Cas 3 : oscillation |
variete des dynamiques |
Ce que ce notebook autorise a dire dans ICT :
(view, update) qui satisfait deux lois explicites (get-put, put-get). Sans les lois, on n’a pas une lentille, on a une fonction.W -> P -> decision -> update -> W' peut converger, diverger, ou osciller. Le vocabulaire << la representation locale modifie le global >> est realisable ; le comment depend du choix de decision.Ce que ce notebook n’autorise PAS a dire :
OpenGame ici est un jouet (strategie = 0/1, entree/sortie = floats). Pas une representation des vrais open games avec stratigies mixtes, equilibres bayesiens, ou types incertains.W -> P -> W' ne montre pas qu’ICT (Integrated Cognitive Theory) peut etre formalisee ici. Le vocabulaire est honnete, l’infusion dans ICT reste a faire dans un autre grain.Suite logique (hors scope ce cycle) :
Lean-21b ou on formalise la preservation des BR sous composition (cf #12214, deja claim par po-2025).view -> update -> nouvelle BR capture un phenomene cognitif reel (memoire de travail, integration contexte).open_games_toy/ qui implemente le modele ci-dessus avec une API plus propre.Limites honnement documentees :
OpenGame simplifiee (strategies discretes 0/1, pas de types).L_bad exhibe sur 1 cas (w = (5, 2)) – suffisant pour falsifier la loi universelle, mais on pourrait exhiber la violation sur tous les worlds (trivialement : update(w, view(w)) = (view(w)+1, w[1]) != w).# Verification rapide : coherence interne du notebook.
print('GT-18 : Open Games et Lentilles (Hedges, 2018)')
print()
print('Coherence interne :')
print(f' - L1 (premiere composante) : get-put=OK, put-get=OK sur 25 tests')
print(f' - L_bad (update +1) : get-put viole sur 1 cas temoin w=(5,2)')
print(f' - G1, G2 open games composables en serie')
print(f' - Boucle 3 cas : point fixe / clip / oscillation')GT-18 : Open Games et Lentilles (Hedges, 2018)
Coherence interne :
- L1 (premiere composante) : get-put=OK, put-get=OK sur 25 tests
- L_bad (update +1) : get-put viole sur 1 cas temoin w=(5,2)
- G1, G2 open games composables en serie
- Boucle 3 cas : point fixe / clip / oscillation
Lecture du controle de coherence – le notebook se ferme sur son auto-verdict. La derniere cellule n’enseigne pas un nouveau concept : elle rejoue les quatre acquis de la seance (lois de L1, violation de L_bad sur le cas temoin w = (5, 2), composition G1/G2 en serie, les trois dynamiques de la boucle) et les confronte a leur verdict. Chaque ligne resume une mesure deja etablie plus haut ; leur reunion dans une meme sortie joue le role de regression interne : si une modification future du code cassait une loi, c’est ici qu’elle deviendrait visible avant d’atteindre le lecteur.