diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-16e-LLM-Players-Othman-Sandholm.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-16e-LLM-Players-Othman-Sandholm.ipynb new file mode 100644 index 0000000000..1f5ad5956e --- /dev/null +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-16e-LLM-Players-Othman-Sandholm.ipynb @@ -0,0 +1,1331 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ad70707f", + "metadata": {}, + "source": [ + "# GameTheory-16e : Pilote — joueurs LLM hétérogènes sur le mécanisme Othman–Sandholm (Proposition 6)\n", + "\n", + "Ce side-car branche de **vrais LLM hétérogènes** comme joueurs sur un mécanisme fini,\n", + "exécutable et vérifiable : la construction de la Proposition 6 d'Othman & Sandholm\n", + "(SAGT 2009, *Better with Byzantine*), livrée dans `GameTheory-16-MechanismDesign.ipynb` §4.6\n", + "et formalisée en Lean (#12343). Les agents scriptés (équilibre, byzantin, aléatoire)\n", + "servent de contrôles ; chaque action est rejouée par l'oracle déterministe de\n", + "bien-être social du mécanisme.\n", + "\n", + "**Déconflit** : `GameTheory-03c-Le-Joueur-LLM.ipynb` fait déjà jouer des LLM sur un jeu 2×2\n", + "en forme normale ; ici l'objet change de classe — un **mécanisme** (oracle de welfare,\n", + "action dominante boxing) et non un jeu sous forme normale. 16/16b/16c/16d restent\n", + "propriétaires du mechanism design ; la formalisation Lean (§1.1) est **citée et distinguée**\n", + "des résultats empiriques de ce pilote. See #15399. See #15397. See #15062. See #13571.\n", + "\n", + "**Protocole** : le pré-enregistrement (§0) est gelé **avant le premier appel LLM** ;\n", + "les verdicts H0/H1 (§8) lisent leurs seuils dans le gel, pas dans les résultats." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "48f07d2c", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:53:06.315200Z", + "iopub.status.busy": "2026-09-09T21:53:06.315035Z", + "iopub.status.idle": "2026-09-09T21:53:06.323567Z", + "shell.execute_reply": "2026-09-09T21:53:06.323209Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Pre-enregistrement GEL (sha256) : aca1caf4fb552408 ...\n", + "Familles : ['glm', 'qwen']\n", + "Seuils : {'taux_dominant_visible_min': 0.8, 'ecart_visible_vs_aveugle_min': 0.2, 'chute_permutation_min': 0.2}\n" + ] + } + ], + "source": [ + "import hashlib\n", + "import json\n", + "\n", + "PREREG = {\n", + " # Roles et types : 2 agents (row, column), 2 types (a, a'), 4 profils vrais.\n", + " \"roles\": [\"row\", \"col\"],\n", + " \"types\": [\"a\", \"a'\"],\n", + " \"profils_vrais\": [(\"a\", \"a\"), (\"a\", \"a'\"), (\"a'\", \"a\"), (\"a'\", \"a'\")],\n", + " # Actions : rapporter \"a\" ou \"a'\". Action dominante du mecanisme : \"a'\".\n", + " \"actions\": [\"a\", \"a'\"],\n", + " # Conditions (6) :\n", + " # scripted-equilibre : politique a' systematique (controle conforme, dominant)\n", + " # scripted-byzantin : politique a systematique (controle byzantin)\n", + " # scripted-aleatoire : uniforme seede (plancher de reference)\n", + " # llm-regles-visibles : LLM voit regles + code du mecanisme + utilites\n", + " # llm-aveugle : LLM voit uniquement l'espace d'actions, sans regles\n", + " # llm-permutation : regles visibles MAIS la regle dominante est\n", + " # permutee (a presente comme dominante) -- hostile (§7)\n", + " \"conditions\": [\n", + " \"scripted-equilibre\", \"scripted-byzantin\", \"scripted-aleatoire\",\n", + " \"llm-regles-visibles\", \"llm-aveugle\", \"llm-permutation\",\n", + " ],\n", + " # Familles reelles (2 familles minimum, via models.myia.io, cle .env) :\n", + " \"familles\": {\n", + " \"glm\": \"glm-5.2\", # famille GLM (Z.ai), modele reasonant\n", + " \"qwen\": \"qwen3.8-max\", # famille Qwen (Alibaba)\n", + " },\n", + " # Parametres d'appel : gels avant tout appel.\n", + " # max_tokens=400 : glm-5.2 est un modele reasonant — en dessous, le contenu\n", + " # peut ressortir vide (le raisonnement consomme le budget). Mesure prealable.\n", + " \"temperature\": 0.7,\n", + " \"max_tokens\": 400,\n", + " \"seeds\": [11, 29, 71, 97], # pilote principal (>= 4 seeds, generation stochastique)\n", + " \"seeds_hostile\": [11, 29], # scenario hostile (permutation, §7)\n", + " \"max_retry_parse\": 1, # 1 reformulation si JSON invalide, puis \"invalide\"\n", + " \"timeout_requete_s\": 120,\n", + " # Politique reseau (infrastructure, DISTINCT du comportement modele) :\n", + " # timeout/erreur reseau -> 2 re-essais (backoff 5 s puis 15 s) ; apres echec :\n", + " # statut 'erreur_reseau', exclue des taux d'actes ET du taux d'invalide,\n", + " # comptee et rapportee separement (jamais comptee comme choix du modele).\n", + " \"politique_reseau\": \"timeout/5xx -> 2 retries (5 s, 15 s) puis statut erreur_reseau\",\n", + " \"concurrence\": 4, # appels LLM simultanes max\n", + " # Politique refus/parse error : enregistre comme action \"invalide\" (jamais re-echantillonnee)\n", + " \"politique_refus\": \"refus ou JSON invalide apres retry -> action=None statut 'invalide', \"\n", + " \"exclue du taux d'action dominante mais comptee\",\n", + " # Metriques : payoff par agent et par type, SW realise vs SW optimal oracle,\n", + " # taux d'action dominante, taux d'invalide, tokens consommes, latences (s).\n", + " # (le proxy ne facture pas : pas de cout usd, seulement les tokens reels)\n", + " \"metriques\": [\"payoff_par_type\", \"sw_realise\", \"sw_oracle_optimal\", \"taux_action_dominante\",\n", + " \"taux_invalide\", \"tokens_totaux\", \"latence_s\"],\n", + " # Hypotheses pre-enregistrees (verdicts au §8) :\n", + " \"H0\": \"les actions LLM (regles visibles ou aveugles) sont indiscernables de l'uniforme 50/50 sur {a, a'}\",\n", + " \"H1\": \"avec regles visibles, taux d'action dominante a' >= 0.80 et > taux aveugle (ecart >= 0.20) ; \"\n", + " \"sous permutation hostile, le taux visible retombe vers l'uniforme (chute >= 0.20 vs visible)\",\n", + " \"seuils\": {\n", + " \"taux_dominant_visible_min\": 0.80,\n", + " \"ecart_visible_vs_aveugle_min\": 0.20,\n", + " \"chute_permutation_min\": 0.20,\n", + " },\n", + " # Analyse statistique (formules gelees AVANT resultats, acceptance #15399) :\n", + " # intervalle : Wilson 95 % sur chaque proportion (k/n par acte valide) ;\n", + " # taille d'effet : h de Cohen (difference d'arcsinus) entre conditions.\n", + " \"analyse\": {\"intervalle\": \"wilson_95\", \"taille_effet\": \"cohen_h\"},\n", + " # Budget : principal 2 familles x 4 seeds x 4 profils x 2 agents = 64 appels\n", + " # + hostile 2 familles x 2 seeds x 4 profils x 2 agents = 32 appels\n", + " # = 96 appels max (retries inclus dans la politique, pas comptes en plus).\n", + " \"budget_appels_max\": 96,\n", + "}\n", + "\n", + "gel = json.dumps(PREREG, sort_keys=True, ensure_ascii=False)\n", + "PREREG_SHA = hashlib.sha256(gel.encode(\"utf-8\")).hexdigest()\n", + "print(\"Pre-enregistrement GEL (sha256) :\", PREREG_SHA[:16], \"...\")\n", + "print(\"Familles :\", list(PREREG[\"familles\"]))\n", + "print(\"Seuils :\", PREREG[\"seuils\"])" + ] + }, + { + "cell_type": "markdown", + "id": "5e688177", + "metadata": {}, + "source": [ + "## 0. Pré-enregistrement du protocole (gelé avant tout appel LLM)\n", + "\n", + "Rôles, conditions, modèles, paramètres, budgets, hypothèses et seuils sont figés ici.\n", + "Toute déviation exécution vs gel sera consignée comme violation de protocole (§8)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "80fa03a4", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:53:06.325467Z", + "iopub.status.busy": "2026-09-09T21:53:06.325271Z", + "iopub.status.idle": "2026-09-09T21:53:06.331722Z", + "shell.execute_reply": "2026-09-09T21:53:06.331333Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Adaptateur non divergent : 5 asserts SW (chiffres du owner) + 8 asserts de dominance stricte de a'.\n", + "SW optimal par profil vrai : {'(a,a)': 4, \"(a,a')\": 6, \"(a',a)\": 5, \"(a',a')\": 7}\n" + ] + } + ], + "source": [ + "# Adaptateur minimal : Othman-Sandholm (SAGT 2009) Proposition 6, tel que livre\n", + "# par GameTheory-16-MechanismDesign.ipynb §4.6 (owner). Aucun second moteur :\n", + "# tables, carte et oracle sont recopies tels quels puis verifies par asserts.\n", + "\n", + "u_row = { # u_row[outcome][type_row]\n", + " \"o1\": {\"a\": 1, \"a'\": 3}, \"o2\": {\"a\": 4, \"a'\": 5},\n", + " \"o3\": {\"a\": 0, \"a'\": 0}, \"o4\": {\"a\": 3, \"a'\": 0},\n", + "}\n", + "u_col = { # u_col[outcome][type_col]\n", + " \"o1\": {\"a\": 1, \"a'\": 4}, \"o2\": {\"a\": 0, \"a'\": 0},\n", + " \"o3\": {\"a\": 3, \"a'\": 6}, \"o4\": {\"a\": 0, \"a'\": 0},\n", + "}\n", + "outcomes = [\"o1\", \"o2\", \"o3\", \"o4\"]\n", + "\n", + "# Carte du mecanisme M (owner §4.6.1, matrice rapport -> outcome)\n", + "M_MAP = {\n", + " (\"a'\", \"a'\"): \"o1\", (\"a'\", \"a\"): \"o2\",\n", + " (\"a\", \"a'\"): \"o3\", (\"a\", \"a\"): \"o4\",\n", + "}\n", + "\n", + "def mecanisme(r_row, r_col):\n", + " return M_MAP[(r_row, r_col)]\n", + "\n", + "def sw_oracle(outcome, theta_row, theta_col):\n", + " \"\"\"Bien-etre social : u_row(o|theta_row) + u_col(o|theta_col) (owner §4.6.2).\"\"\"\n", + " return u_row[outcome][theta_row] + u_col[outcome][theta_col]\n", + "\n", + "def payoff(role, outcome, theta):\n", + " return (u_row if role == \"row\" else u_col)[outcome][theta]\n", + "\n", + "def sw_optimal(theta_row, theta_col):\n", + " return max(sw_oracle(o, theta_row, theta_col) for o in outcomes)\n", + "\n", + "# --- Asserts de non-divergence contre les chiffres publies par le owner ---\n", + "# Owner §4.6.3 (Interpretation, Caracteristique 2) :\n", + "assert sw_oracle(\"o1\", \"a'\", \"a'\") == 7 # (a',a') : o1 optimal\n", + "assert sw_oracle(\"o2\", \"a\", \"a\") == 4 and sw_oracle(\"o1\", \"a\", \"a\") == 2 # (a,a) : o2 > o1\n", + "assert sw_oracle(\"o3\", \"a\", \"a'\") == 6 and sw_oracle(\"o1\", \"a\", \"a'\") == 5 # (a,a') : o3 > o1\n", + "assert sw_oracle(\"o2\", \"a'\", \"a\") == 5 and sw_oracle(\"o1\", \"a'\", \"a\") == 4 # (a',a) : o2 > o1\n", + "# Observation cle du owner (§4.6.1) : rapporter a' est strictement dominant\n", + "# comme ACTION pour les DEUX types des DEUX agents, quel que soit le rapport adverse.\n", + "for agent_u, role in ((u_row, \"row\"), (u_col, \"col\")):\n", + " for t_vrai in (\"a\", \"a'\"):\n", + " for r_adv in (\"a\", \"a'\"):\n", + " if role == \"row\":\n", + " o_a_prime, o_a = mecanisme(\"a'\", r_adv), mecanisme(\"a\", r_adv)\n", + " else:\n", + " o_a_prime, o_a = mecanisme(r_adv, \"a'\"), mecanisme(r_adv, \"a\")\n", + " assert agent_u[o_a_prime][t_vrai] > agent_u[o_a][t_vrai], (role, t_vrai, r_adv)\n", + "\n", + "print(\"Adaptateur non divergent : 5 asserts SW (chiffres du owner) + 8 asserts de dominance stricte de a'.\")\n", + "print(\"SW optimal par profil vrai :\", {f\"({tr},{tc})\": sw_optimal(tr, tc)\n", + " for tr, tc in PREREG[\"profils_vrais\"]})" + ] + }, + { + "cell_type": "markdown", + "id": "578486ed", + "metadata": {}, + "source": [ + "## 1. Le mécanisme importé — adaptateur minimal depuis GameTheory-16 §4.6\n", + "\n", + "Reproduction **à l'identique** de la construction du notebook owner (§4.6) :\n", + "tables d'utilités, carte du mécanisme, oracle de bien-être social. Des **asserts de\n", + "non-divergence** confrontent l'adaptateur aux chiffres publiés par le owner — une\n", + "correspondance de nom ne suffirait pas (acceptance #15399)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "6050e7fe", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:53:06.333509Z", + "iopub.status.busy": "2026-09-09T21:53:06.333347Z", + "iopub.status.idle": "2026-09-09T21:53:06.338940Z", + "shell.execute_reply": "2026-09-09T21:53:06.338496Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cartes a rapport-de-type-dominant (avec box (a',a')->o1) : 2 / 64\n", + " {('a', 'a'): 'o1', ('a', \"a'\"): 'o1', (\"a'\", 'a'): 'o1', (\"a'\", \"a'\"): 'o1'}\n", + " {('a', 'a'): 'o4', ('a', \"a'\"): 'o4', (\"a'\", 'a'): 'o1', (\"a'\", \"a'\"): 'o1'}\n", + "Carte papier M : {(\"a'\", \"a'\"): 'o1', (\"a'\", 'a'): 'o2', ('a', \"a'\"): 'o3', ('a', 'a'): 'o4'}\n", + "M est rapport-de-type-dominante : False (NON attendu : M est dominante en ACTION, pas en rapport-de-type)\n", + "La carte constante o1 (M_1 boxed du owner) est admissible : True (c'est la mise en boite canonique du owner §4.6.2 : mom_outcome = 'o1')\n" + ] + } + ], + "source": [ + "# Mise en boite (revelation) : le owner « boxe » M en M_1 qui choisit systematiquement o1.\n", + "# Deux concepts de dominance DISTINCTS, a ne jamais confondre :\n", + "# (i) dominance d'ACTION (le M papier, cell ci-dessus) : rapporter a' domine, quel que soit le type ;\n", + "# (ii) dominance du RAPPORT-DE-TYPE : rapporter son VRAI type domine (mise en boite).\n", + "# Recherche exhaustive sur les 4^4 = 256 cartes : lesquelles rendent le rapport du\n", + "# vrai type faiblement dominant ? (question (ii), plus forte que (i))\n", + "import itertools\n", + "\n", + "RAPPORTS = [(\"a\", \"a\"), (\"a\", \"a'\"), (\"a'\", \"a\"), (\"a'\", \"a'\")]\n", + "\n", + "def rapport_type_dominant(carte):\n", + " \"\"\"Rapporter son VRAI type est faiblement dominant pour les 2 agents.\"\"\"\n", + " for agent_u, role in ((u_row, \"row\"), (u_col, \"col\")):\n", + " for t_vrai in (\"a\", \"a'\"):\n", + " for r_adv in (\"a\", \"a'\"):\n", + " utils = {}\n", + " for r_moi in (\"a\", \"a'\"):\n", + " if role == \"row\":\n", + " rr, rc = r_moi, r_adv\n", + " else:\n", + " rr, rc = r_adv, r_moi\n", + " utils[r_moi] = agent_u[carte[(rr, rc)]][t_vrai]\n", + " if utils[t_vrai] < max(utils.values()):\n", + " return False\n", + " return True\n", + "\n", + "cartes_admissibles = []\n", + "for combo in itertools.product(outcomes, repeat=4):\n", + " carte = dict(zip(RAPPORTS, combo))\n", + " if carte[(\"a'\", \"a'\")] == \"o1\" and rapport_type_dominant(carte):\n", + " cartes_admissibles.append(carte)\n", + "\n", + "print(f\"Cartes a rapport-de-type-dominant (avec box (a',a')->o1) : {len(cartes_admissibles)} / 64\")\n", + "for c in cartes_admissibles:\n", + " print(\" \", c)\n", + "print(\"Carte papier M :\", M_MAP)\n", + "print(\"M est rapport-de-type-dominante :\", rapport_type_dominant(M_MAP),\n", + " \"(NON attendu : M est dominante en ACTION, pas en rapport-de-type)\")\n", + "constante_o1 = any(all(v == \"o1\" for v in c.values()) for c in cartes_admissibles)\n", + "print(\"La carte constante o1 (M_1 boxed du owner) est admissible :\", constante_o1,\n", + " \"(c'est la mise en boite canonique du owner §4.6.2 : mom_outcome = 'o1')\")" + ] + }, + { + "cell_type": "markdown", + "id": "f76826bf", + "metadata": {}, + "source": [ + "### 1.1 Portée de la Proposition 6 formalisée (Lean) vs résultats empiriques de ce pilote\n", + "\n", + "La Proposition 6 existe aussi en Lean (`game_theory_lean/SocialChoice/MechanismDesign.lean`, #12343).\n", + "Le théorème formel garantit des propriétés **mathématiques** de la construction (dominance\n", + "stricte, caractéristique 2) — un énoncé formel, pas une prédiction comportementale. Ce pilote\n", + "mesure le **comportement de joueurs LLM** sur la même construction ; ses résultats (§6–§8)\n", + "sont empiriques et ne démontrent, réfutent ni n'étendent jamais le théorème Lean.\n", + "Aucun sous-grain Lean n'est créé ici.\n", + "\n", + "**Borne négative explicite** : la caractéristique 1 (k=1) de la Proposition 6 n'est **pas** formalisée en Lean — la portée de #12343 se borne à la dominance stricte en **caractéristique 2**. Ce pilote ne qualifie pas la Proposition 6 entière de formalisée et n'étend pas cette portée." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9d377a32", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:53:06.340677Z", + "iopub.status.busy": "2026-09-09T21:53:06.340437Z", + "iopub.status.idle": "2026-09-09T21:53:06.345588Z", + "shell.execute_reply": "2026-09-09T21:53:06.345112Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "e gagnant complémentaire, est déplacé et les deux bidders\n", + " singletons ne paient chacun qu'une externalité de 2. -/\n", + "theorem vcg_revenue_non_monotone : revenue3 < revenue2 := by decide\n", + "\n", + "end VCGCombinatorial\n", + "\n", + "/-! ## Manipulation-optimal mechanisms : Proposition 6 d'Othman-Sandholm (SAGT 2009) -/\n", + "\n", + "namespace OthmanSandholmProp6\n", + "\n", + "/- Construction de la Proposition 6 (« Better with Byzantine », SAGT 2009, page 8) :\n", + " il existe des strict MOM multi-agents avec objectif de bien-être social.\n", + "\n", + " Deux agents (row, column), deux types chacun (a, a'), quatre issues o1..o4.\n", + " Le mécanisme mappe le profil de rapports (row, column) vers :\n", + " (a', a') -> o1 ; (a', a) -> o2 ; (a, a') -> o3 ; (a, a) -> o4.\n", + " (Dans le papier, row = ligne, column = colonne.)\n", + "\n", + " Matrices de gains de la page 8, recopiées littéralement — gains du type a\n", + " à gauche, du type a' à droite (chaque cellule = (row, column)) :\n", + "\n", + " Rapport a' a Rapport a' a\n", + " a' 1,1 4,0 a' 3,4 5,0\n", + " a 0,3 3,0 a 0,6 0,0\n", + " type a type a'\n", + "\n", + " Vue par issue (page 9 du papier, urow puis ucolumn pour chaque type) :\n", + "\n", + " Issue u_row(a) u_row(a') u_col(a) u_col(a')\n", + " \n", + "\n", + "[Distinction] Enonce formel ci-dessus (existence/proprietes du mecanisme) vs mesures\n", + "comportementales LLM de ce pilote : objets de nature differente, jamais confondus.\n" + ] + } + ], + "source": [ + "# Lecture de l'enonce Lean de la Proposition 6 (portee formelle, citee telle quelle).\n", + "# Le kernel peut demarrer dans le dossier du notebook : on remonte jusqu'a la racine repo.\n", + "from pathlib import Path\n", + "\n", + "REL = \"MyIA.AI.Notebooks/GameTheory/game_theory_lean/SocialChoice/MechanismDesign.lean\"\n", + "candidats = [Path(REL)] + [p / REL for p in Path.cwd().parents]\n", + "lean_path = next((c for c in candidats if c.exists()), None)\n", + "assert lean_path, f\"{REL} introuvable -- verifier le chemin sur main\"\n", + "texte = lean_path.read_text(encoding=\"utf-8\")\n", + "idx = texte.lower().find(\"proposition 6\")\n", + "assert idx >= 0, \"Proposition 6 absente du fichier Lean -- distinction impossible\"\n", + "print(texte[max(0, idx - 250): idx + 1000])\n", + "print(\"\\n[Distinction] Enonce formel ci-dessus (existence/proprietes du mecanisme) vs mesures\")\n", + "print(\"comportementales LLM de ce pilote : objets de nature differente, jamais confondus.\")" + ] + }, + { + "cell_type": "markdown", + "id": "5647af35", + "metadata": {}, + "source": [ + "## 2. Schéma observation/action provider-neutral\n", + "\n", + "L'observation est un dictionnaire JSON sérialisable, **identique pour toutes les familles** ;\n", + "l'action est un JSON strict `{\"report\": \"a\"}` ou `{\"report\": \"a'\"}`. Aucune API LLM n'apparaît\n", + "dans le schéma : les familles (§4) ne diffèrent que par le modèle interrogé." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "87f7b23d", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:53:06.347364Z", + "iopub.status.busy": "2026-09-09T21:53:06.347198Z", + "iopub.status.idle": "2026-09-09T21:53:06.352230Z", + "shell.execute_reply": "2026-09-09T21:53:06.351789Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Schema obs/action OK : validation stricte + 4 auto-tests.\n" + ] + } + ], + "source": [ + "import json\n", + "\n", + "def observation(role, type_vrai, regles_visibles, permutation=False):\n", + " \"\"\"Observation provider-neutral. `permutation` (§7) echange la regle presentee\n", + " comme dominante SANS toucher au mecanisme reel -- le controle hostile.\"\"\"\n", + " obs = {\n", + " \"role\": role,\n", + " \"votre_type\": type_vrai,\n", + " \"espace_actions\": [\"a\", \"a'\"],\n", + " \"format_attendu\": 'repondre STRICTEMENT en JSON : {\"report\": \"a\"} ou {\"report\": \"a\\'\"}',\n", + " }\n", + " if regles_visibles:\n", + " dominante_annoncee = \"a\" if permutation else \"a'\"\n", + " obs[\"regles\"] = {\n", + " \"mecanisme\": \"2 agents rapportent a ou a' ; l'outcome o1..o4 suit la matrice \"\n", + " \"(a',a')->o1, (a',a)->o2, (a,a')->o3, (a,a)->o4\",\n", + " \"utilites\": \"les votres dependent de l'outcome ET de votre type (tableau fourni)\",\n", + " \"tableau_utilites\": (u_row if role == \"row\" else u_col),\n", + " \"regle_annoncee\": f\"l'action dominante est {dominante_annoncee} (rapportez {dominante_annoncee} \"\n", + " \"quels que soient votre type et le rapport adverse)\",\n", + " }\n", + " return obs\n", + "\n", + "ACTIONS_VALIDES = (\"a\", \"a'\")\n", + "\n", + "def valider_action(brut):\n", + " \"\"\"Parse+valide strictement. Retourne (action, statut) avec statut dans\n", + " 'valide' | 'invalide' -- conforme a PREREG['politique_refus'].\"\"\"\n", + " if brut is None:\n", + " return None, \"invalide\"\n", + " try:\n", + " d = json.loads(brut)\n", + " except (json.JSONDecodeError, TypeError):\n", + " return None, \"invalide\"\n", + " if not isinstance(d, dict) or d.get(\"report\") not in ACTIONS_VALIDES:\n", + " return None, \"invalide\"\n", + " return d[\"report\"], \"valide\"\n", + "\n", + "# Auto-test du schema (deterministe, sans LLM)\n", + "assert valider_action(json.dumps({\"report\": \"a'\"})) == (\"a'\", \"valide\")\n", + "assert valider_action(json.dumps({\"report\": \"b\"})) == (None, \"invalide\")\n", + "assert valider_action(\"je rapporte a\") == (None, \"invalide\")\n", + "assert valider_action(None) == (None, \"invalide\")\n", + "print(\"Schema obs/action OK : validation stricte + 4 auto-tests.\")" + ] + }, + { + "cell_type": "markdown", + "id": "891029ee", + "metadata": {}, + "source": [ + "## 3. Politiques scriptées (contrôles)\n", + "\n", + "Trois politiques sans LLM : l'équilibre (toujours `a'`, l'action dominante), le byzantin\n", + "(toujours `a`), et un aléatoire seedé servant de plancher de référence. Mêmes instances\n", + "que les LLM (mêmes 4 profils vrais)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f16db154", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:53:06.354024Z", + "iopub.status.busy": "2026-09-09T21:53:06.353880Z", + "iopub.status.idle": "2026-09-09T21:53:06.357300Z", + "shell.execute_reply": "2026-09-09T21:53:06.356863Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Controles scriptes OK : equilibre=a' (dominante), byzantin=a, aleatoire seede uniforme.\n" + ] + } + ], + "source": [ + "import random\n", + "\n", + "def policy_equilibre(role, type_vrai, obs, rng):\n", + " return \"a'\"\n", + "\n", + "def policy_byzantine(role, type_vrai, obs, rng):\n", + " return \"a\"\n", + "\n", + "def policy_aleatoire(role, type_vrai, obs, rng):\n", + " return rng.choice(ACTIONS_VALIDES)\n", + "\n", + "SCRIPTED = {\"scripted-equilibre\": policy_equilibre,\n", + " \"scripted-byzantin\": policy_byzantine,\n", + " \"scripted-aleatoire\": policy_aleatoire}\n", + "\n", + "# Verification deterministe des controles sur les 4 profils\n", + "rng0 = random.Random(0)\n", + "for pol in (policy_equilibre, policy_byzantine):\n", + " acts = {pol(r, t, {}, rng0) for r in (\"row\", \"col\") for t in (\"a\", \"a'\")}\n", + " assert len(acts) == 1, acts\n", + "print(\"Controles scriptes OK : equilibre=a' (dominante), byzantin=a, aleatoire seede uniforme.\")" + ] + }, + { + "cell_type": "markdown", + "id": "e5c21b77", + "metadata": {}, + "source": [ + "## 4. Familles LLM réelles — appels via models.myia.io\n", + "\n", + "Deux familles (gel §0) : **GLM** (`glm-5.2`, modèle raisonnant) et **Qwen** (`qwen3.8-max`).\n", + "La clé vient de l'environnement (`CLAUDISH_PROXY_KEY`) — jamais inline, jamais dans les\n", + "traces (seuls tokens et latences sont conservés). Un refus ou un JSON invalide après\n", + "**un** retry est enregistré `invalide` (politique gelée), jamais ré-échantillonné." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "15658b2e", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:53:06.359224Z", + "iopub.status.busy": "2026-09-09T21:53:06.358980Z", + "iopub.status.idle": "2026-09-09T21:53:06.453690Z", + "shell.execute_reply": "2026-09-09T21:53:06.453201Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Familles configurees : {'glm': 'glm-5.2', 'qwen': 'qwen3.8-max'}\n" + ] + } + ], + "source": [ + "import os\n", + "import time\n", + "import requests\n", + "\n", + "API_BASE = \"https://models.myia.io/v1/chat/completions\"\n", + "API_KEY = os.getenv(\"CLAUDISH_PROXY_KEY\")\n", + "assert API_KEY, \"CLAUDISH_PROXY_KEY absente de l'environnement (regle F : la cle vie dans l'env, pas ici)\"\n", + "\n", + "PROMPT_SYSTEM = (\n", + " \"Tu es un agent rationnel dans une experience de theorie des mecanismes. \"\n", + " \"Reponds STRICTEMENT en JSON, sans aucun autre texte.\"\n", + ")\n", + "\n", + "def prompt_utilisateur(obs):\n", + " if \"regles\" in obs:\n", + " return (\n", + " f\"Role : {obs['role']}. Ton type (prive) : {obs['votre_type']}\\n\"\n", + " f\"Espace d'actions : {obs['espace_actions']}\\n\"\n", + " f\"Regles du mecanisme : {json.dumps(obs['regles'], ensure_ascii=False)}\\n\"\n", + " f\"Objectif : maximiser ton utilite (elle depend de l'outcome ET de ton type). \"\n", + " f\"{obs['format_attendu']}\"\n", + " )\n", + " return (\n", + " f\"Role : {obs['role']}. Ton type (prive) : {obs['votre_type']}\\n\"\n", + " f\"Espace d'actions : {obs['espace_actions']}\\n\"\n", + " f\"Aucune regle ne t'est communiquée. Choisis une action. {obs['format_attendu']}\"\n", + " )\n", + "\n", + "def appel_llm(famille, modele, obs, seed):\n", + " \"\"\"1 appel + 1 retry si parse invalide (gel) + 2 retries reseau (gel).\n", + " Retourne (action, statut, tokens_total, latence_s) avec statut dans\n", + " 'valide' | 'invalide' | 'erreur_reseau'.\"\"\"\n", + " entetes = {\"Authorization\": f\"Bearer {API_KEY}\", \"Content-Type\": \"application/json\"}\n", + " tokens_total, latence_totale = 0, 0.0\n", + " for essai in range(1 + PREREG[\"max_retry_parse\"]):\n", + " corps = {\n", + " \"model\": modele,\n", + " \"temperature\": PREREG[\"temperature\"],\n", + " \"max_tokens\": PREREG[\"max_tokens\"],\n", + " \"seed\": seed,\n", + " \"messages\": [\n", + " {\"role\": \"system\", \"content\": PROMPT_SYSTEM},\n", + " {\"role\": \"user\", \"content\": prompt_utilisateur(obs)},\n", + " ],\n", + " }\n", + " rep, err_reseau = None, None\n", + " for essai_net, backoff in ((0, 0), (1, 5), (2, 15)): # politique reseau gelee\n", + " if backoff:\n", + " time.sleep(backoff)\n", + " t0 = time.perf_counter()\n", + " try:\n", + " rep = requests.post(API_BASE, headers=entetes, json=corps,\n", + " timeout=PREREG[\"timeout_requete_s\"])\n", + " err_reseau = None\n", + " except requests.RequestException as e:\n", + " err_reseau = e\n", + " latence_totale += time.perf_counter() - t0\n", + " if rep is not None:\n", + " break\n", + " if rep is None:\n", + " continue # epuise les retries reseau -> on tente l'essai parse suivant\n", + " if rep.status_code >= 500 or rep.status_code == 429:\n", + " err_reseau = f\"HTTP {rep.status_code}\"\n", + " continue\n", + " rep.raise_for_status()\n", + " err_reseau = None # reponse exploitable obtenue : plus d'erreur reseau\n", + " data = rep.json()\n", + " usage = data.get(\"usage\", {}) or {}\n", + " tokens_total += usage.get(\"total_tokens\", 0) or 0\n", + " brut = data[\"choices\"][0][\"message\"][\"content\"]\n", + " action, statut = valider_action(brut)\n", + " if statut == \"valide\":\n", + " return action, \"valide\", tokens_total, latence_totale\n", + " if essai == 0: # reformulation unique (gel)\n", + " obs = dict(obs, format_attendu='REPRENDS : UNIQUEMENT le JSON {\"report\": \"a\"} ou {\"report\": \"a\\'\"}')\n", + " return None, (\"erreur_reseau\" if err_reseau else \"invalide\"), tokens_total, latence_totale\n", + "\n", + "print(\"Familles configurees :\", PREREG[\"familles\"])" + ] + }, + { + "cell_type": "markdown", + "id": "98a2b1b3", + "metadata": {}, + "source": [ + "## 5. Le pilote — exécution par condition × profil × seed × agent\n", + "\n", + "Boucle gelée : chaque action (scriptée ou LLM) passe par le **mécanisme** puis est **rejouée\n", + "par l'oracle** : outcome, payoffs au type vrai, SW réalisé et SW optimal (contre-factuel\n", + "déterministe). Si une action LLM est invalide après retry, le tour est marqué — l'outcome\n", + "n'est **pas** substitué : aucune action n'est inventée. Les données vivent dans `traces`\n", + "(une ligne par tour) — jamais de moyenne globale seule (§6)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b48797be", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:53:06.456251Z", + "iopub.status.busy": "2026-09-09T21:53:06.455991Z", + "iopub.status.idle": "2026-09-09T21:56:05.900304Z", + "shell.execute_reply": "2026-09-09T21:56:05.899867Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "88 tours ; appels LLM consommes : 128 / budget 96\n", + "Echantillon (dernier tour LLM) : {'condition': 'llm-aveugle', 'famille': 'qwen', 'seed': 97, 'theta_row': \"a'\", 'theta_col': \"a'\", 'action_row': \"a'\", 'action_col': \"a'\", 'statuts': {'row': 'valide', 'col': 'valide'}, 'sw_oracle_optimal': 7, 'tokens': 238, 'latence_s': 2.461665600003471, 'outcome': 'o1', 'payoff_row': 3, 'payoff_col': 4, 'sw_realise': 7}\n" + ] + } + ], + "source": [ + "PROFIL_IDX = {t: i for i, t in enumerate(PREREG[\"types\"])}\n", + "\n", + "def jouer_tour(condition, famille, modele, theta_row, theta_col, seed):\n", + " \"\"\"Un profil x un seed : les 2 agents agissent, le mecanisme tranche, l'oracle rejoue.\"\"\"\n", + " rng = random.Random(seed * 1000 + PROFIL_IDX[theta_row] * 2 + PROFIL_IDX[theta_col])\n", + " actions, statuts = {}, {}\n", + " tokens, latence = 0, 0\n", + " for role, theta in ((\"row\", theta_row), (\"col\", theta_col)):\n", + " if condition in SCRIPTED:\n", + " obs = observation(role, theta, regles_visibles=False)\n", + " actions[role] = SCRIPTED[condition](role, theta, obs, rng)\n", + " statuts[role] = \"scripte\"\n", + " else:\n", + " visible = condition in (\"llm-regles-visibles\", \"llm-permutation\")\n", + " perm = condition == \"llm-permutation\"\n", + " obs = observation(role, theta, regles_visibles=visible, permutation=perm)\n", + " act, st, tk, lat = appel_llm(famille, modele, obs, seed)\n", + " actions[role], statuts[role] = act, st\n", + " tokens += tk\n", + " latence += lat\n", + " if actions[\"row\"] in ACTIONS_VALIDES and actions[\"col\"] in ACTIONS_VALIDES:\n", + " o = mecanisme(actions[\"row\"], actions[\"col\"])\n", + " ligne = {\n", + " \"outcome\": o,\n", + " \"payoff_row\": payoff(\"row\", o, theta_row),\n", + " \"payoff_col\": payoff(\"col\", o, theta_col),\n", + " \"sw_realise\": sw_oracle(o, theta_row, theta_col),\n", + " }\n", + " else: # action invalide : aucune substitution, tour marque\n", + " ligne = {\"outcome\": None, \"payoff_row\": None, \"payoff_col\": None, \"sw_realise\": None}\n", + " return {\n", + " \"condition\": condition, \"famille\": famille, \"seed\": seed,\n", + " \"theta_row\": theta_row, \"theta_col\": theta_col,\n", + " \"action_row\": actions[\"row\"], \"action_col\": actions[\"col\"],\n", + " \"statuts\": statuts,\n", + " \"sw_oracle_optimal\": sw_optimal(theta_row, theta_col),\n", + " \"tokens\": tokens, \"latence_s\": latence,\n", + " **ligne,\n", + " }\n", + "\n", + "traces = []\n", + "n_appels = 0\n", + "\n", + "# Controles scriptes (aucun appel LLM) : deterministes a seed unique,\n", + "# aleatoire avec les 3 seeds du gel pour la variance.\n", + "for condition in (\"scripted-equilibre\", \"scripted-byzantin\"):\n", + " for (tr, tc) in PREREG[\"profils_vrais\"]:\n", + " traces.append(jouer_tour(condition, \"scripte\", \"-\", tr, tc, seed=0))\n", + "for seed in PREREG[\"seeds\"]:\n", + " for (tr, tc) in PREREG[\"profils_vrais\"]:\n", + " traces.append(jouer_tour(\"scripted-aleatoire\", \"scripte\", \"-\", tr, tc, seed))\n", + "\n", + "# Conditions LLM principales (permutation reportee au §7 avec ses propres seeds).\n", + "# Appels independants -> execution parallele (8 workers) ; chaque appel garde\n", + "# son propre seed et l'ordre des traces suit l'ordre des taches (map ordonne).\n", + "from concurrent.futures import ThreadPoolExecutor\n", + "\n", + "taches_llm = [(condition, famille, modele, tr, tc, seed)\n", + " for condition in (\"llm-regles-visibles\", \"llm-aveugle\")\n", + " for famille, modele in PREREG[\"familles\"].items()\n", + " for seed in PREREG[\"seeds\"]\n", + " for (tr, tc) in PREREG[\"profils_vrais\"]]\n", + "with ThreadPoolExecutor(max_workers=PREREG[\"concurrence\"]) as pool:\n", + " traces.extend(pool.map(lambda t: jouer_tour(*t), taches_llm))\n", + "n_appels += 2 * len(taches_llm)\n", + "\n", + "print(f\"{len(traces)} tours ; appels LLM consommes : {n_appels} / budget {PREREG['budget_appels_max']}\")\n", + "print(\"Echantillon (dernier tour LLM) :\", traces[-1])" + ] + }, + { + "cell_type": "markdown", + "id": "79848964", + "metadata": {}, + "source": [ + "## 6. Résultats multi-seed\n", + "*Par type de joueur — jamais en moyenne globale seule.*\n", + "*Revenu du mécanisme : N/A — la Proposition 6 n'expose aucun transfert ni paiement distinct (aucune recette) ; seuls la richesse sociale réalisée (`sw_realise`) et les payoffs par rôle sont mesurés.*" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "7014d16d", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:56:05.901963Z", + "iopub.status.busy": "2026-09-09T21:56:05.901805Z", + "iopub.status.idle": "2026-09-09T21:56:05.913730Z", + "shell.execute_reply": "2026-09-09T21:56:05.913180Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Controles scriptes (bornes de lecture) ===\n", + " scripted-equilibre : taux a' = 1.000 [0.676, 1.000] (n=8) (attendu : 1.000 / 0.000 / ~0.500)\n", + " scripted-byzantin : taux a' = 0.000 [0.000, 0.324] (n=8) (attendu : 1.000 / 0.000 / ~0.500)\n", + " scripted-aleatoire : taux a' = 0.594 [0.423, 0.745] (n=32) (attendu : 1.000 / 0.000 / ~0.500)\n", + "\n", + "=== Taux d'action dominante (a') par condition et famille LLM, Wilson 95 % ===\n", + " llm-regles-visibles glm : taux a' = 1.000 [0.722, 1.000] (n=10), invalide = 0.688, reseau = 0.000, n_tours = 16\n", + " llm-regles-visibles qwen : taux a' = 1.000 [0.893, 1.000] (n=32), invalide = 0.000, reseau = 0.000, n_tours = 16\n", + " llm-aveugle glm : taux a' = 0.500 [0.336, 0.664] (n=32), invalide = 0.000, reseau = 0.000, n_tours = 16\n", + " llm-aveugle qwen : taux a' = 0.500 [0.336, 0.664] (n=32), invalide = 0.000, reseau = 0.000, n_tours = 16\n", + "\n", + "=== Decomposition PAR TYPE (famille x condition LLM) ===\n", + " {'famille': 'glm', 'condition': 'llm-regles-visibles', 'type': 'a', 'n_actes': 2, 'taux_a_prime': 1.0, 'sw_ratio_moyen': None}\n", + " {'famille': 'glm', 'condition': 'llm-regles-visibles', 'type': \"a'\", 'n_actes': 8, 'taux_a_prime': 1.0, 'sw_ratio_moyen': None}\n", + " {'famille': 'glm', 'condition': 'llm-aveugle', 'type': 'a', 'n_actes': 16, 'taux_a_prime': 0.0, 'sw_ratio_moyen': 0.917}\n", + " {'famille': 'glm', 'condition': 'llm-aveugle', 'type': \"a'\", 'n_actes': 16, 'taux_a_prime': 1.0, 'sw_ratio_moyen': 1.0}\n", + " {'famille': 'qwen', 'condition': 'llm-regles-visibles', 'type': 'a', 'n_actes': 16, 'taux_a_prime': 1.0, 'sw_ratio_moyen': 0.711}\n", + " {'famille': 'qwen', 'condition': 'llm-regles-visibles', 'type': \"a'\", 'n_actes': 16, 'taux_a_prime': 1.0, 'sw_ratio_moyen': 0.878}\n", + " {'famille': 'qwen', 'condition': 'llm-aveugle', 'type': 'a', 'n_actes': 16, 'taux_a_prime': 0.0, 'sw_ratio_moyen': 0.917}\n", + " {'famille': 'qwen', 'condition': 'llm-aveugle', 'type': \"a'\", 'n_actes': 16, 'taux_a_prime': 1.0, 'sw_ratio_moyen': 1.0}\n" + ] + } + ], + "source": [ + "import math\n", + "import statistics\n", + "\n", + "STATUTS_ACTES = (\"valide\", \"scripte\") # les actes comptabilises (invalide exclu)\n", + "\n", + "def compter(cond_rows):\n", + " \"\"\"(k, n) : actes a' / actes comptabilises (formule gelee : Wilson 95 %).\"\"\"\n", + " actes = ([r[\"action_row\"] for r in cond_rows if r[\"statuts\"][\"row\"] in STATUTS_ACTES]\n", + " + [r[\"action_col\"] for r in cond_rows if r[\"statuts\"][\"col\"] in STATUTS_ACTES])\n", + " return sum(1 for x in actes if x == \"a'\"), len(actes)\n", + "\n", + "def wilson(k, n, z=1.96):\n", + " \"\"\"Intervalle de Wilson 95 % sur une proportion (analyse gelee au §0).\"\"\"\n", + " if n == 0:\n", + " return (float(\"nan\"), float(\"nan\"))\n", + " p = k / n\n", + " denom = 1 + z * z / n\n", + " centre = (p + z * z / (2 * n)) / denom\n", + " demi = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denom\n", + " return (max(0.0, centre - demi), min(1.0, centre + demi))\n", + "\n", + "def cohen_h(k1, n1, k2, n2):\n", + " \"\"\"Taille d'effet h de Cohen (difference d'arcsinus), gelee au §0.\n", + " Conventions : ~0.2 petit, ~0.5 moyen, ~0.8 grand.\"\"\"\n", + " if n1 == 0 or n2 == 0:\n", + " return float(\"nan\")\n", + " phi = lambda k, n: 2 * math.asin(math.sqrt(k / n))\n", + " return phi(k1, n1) - phi(k2, n2)\n", + "\n", + "def taux_dominant(cond_rows):\n", + " k, n = compter(cond_rows)\n", + " return (k / n) if n else float(\"nan\")\n", + "\n", + "def taux_invalide(cond_rows):\n", + " \"\"\"Taux de reponses inanalysables du MODELE (JSON invalide apres retry).\"\"\"\n", + " st = [r[\"statuts\"][\"row\"] for r in cond_rows] + [r[\"statuts\"][\"col\"] for r in cond_rows]\n", + " return sum(1 for x in st if x == \"invalide\") / len(st)\n", + "\n", + "def taux_reseau(cond_rows):\n", + " \"\"\"Taux d'echec INFRASTRUCTURE (erreur_reseau) -- rapporte separement,\n", + " jamais confondu avec le comportement du modele (gel §0).\"\"\"\n", + " st = [r[\"statuts\"][\"row\"] for r in cond_rows] + [r[\"statuts\"][\"col\"] for r in cond_rows]\n", + " return sum(1 for x in st if x == \"erreur_reseau\") / len(st)\n", + "\n", + "def resume_par_type(famille, condition):\n", + " \"\"\"Decomposition PAR TYPE du joueur : chaque acte est attribue au type VRAI\n", + " de l'agent qui l'a pose (pas au profil entier).\"\"\"\n", + " lignes = []\n", + " for t in PREREG[\"types\"]:\n", + " actes_vrais, sws = [], []\n", + " for r in traces:\n", + " if r[\"famille\"] != famille or r[\"condition\"] != condition:\n", + " continue\n", + " for role, th in ((\"row\", r[\"theta_row\"]), (\"col\", r[\"theta_col\"])):\n", + " if th == t and r[\"statuts\"][role] in STATUTS_ACTES:\n", + " actes_vrais.append(r[f\"action_{role}\"])\n", + " if r[\"sw_realise\"] is not None and t in (r[\"theta_row\"], r[\"theta_col\"]):\n", + " sws.append(r[\"sw_realise\"] / r[\"sw_oracle_optimal\"])\n", + " taux = (sum(1 for x in actes_vrais if x == \"a'\") / len(actes_vrais)) if actes_vrais else float(\"nan\")\n", + " lignes.append({\n", + " \"famille\": famille, \"condition\": condition, \"type\": t,\n", + " \"n_actes\": len(actes_vrais),\n", + " \"taux_a_prime\": round(taux, 3),\n", + " \"sw_ratio_moyen\": round(statistics.mean(sws), 3) if sws else None,\n", + " })\n", + " return lignes\n", + "\n", + "print(\"=== Controles scriptes (bornes de lecture) ===\")\n", + "for condition in (\"scripted-equilibre\", \"scripted-byzantin\", \"scripted-aleatoire\"):\n", + " rows = [r for r in traces if r[\"condition\"] == condition]\n", + " k, n = compter(rows)\n", + " lo, hi = wilson(k, n)\n", + " print(f\" {condition:20s} : taux a' = {k/n if n else float('nan'):.3f} \"\n", + " f\"[{lo:.3f}, {hi:.3f}] (n={n}) (attendu : 1.000 / 0.000 / ~0.500)\")\n", + "print(\"\\n=== Taux d'action dominante (a') par condition et famille LLM, Wilson 95 % ===\")\n", + "for condition in (\"llm-regles-visibles\", \"llm-aveugle\"):\n", + " for famille in PREREG[\"familles\"]:\n", + " rows = [r for r in traces if r[\"famille\"] == famille and r[\"condition\"] == condition]\n", + " k, n = compter(rows)\n", + " lo, hi = wilson(k, n)\n", + " print(f\" {condition:20s} {famille:5s} : taux a' = {k/n if n else float('nan'):.3f} \"\n", + " f\"[{lo:.3f}, {hi:.3f}] (n={n}), invalide = {taux_invalide(rows):.3f}, \"\n", + " f\"reseau = {taux_reseau(rows):.3f}, n_tours = {len(rows)}\")\n", + "print(\"\\n=== Decomposition PAR TYPE (famille x condition LLM) ===\")\n", + "for famille in PREREG[\"familles\"]:\n", + " for condition in (\"llm-regles-visibles\", \"llm-aveugle\"):\n", + " for ligne in resume_par_type(famille, condition):\n", + " print(\" \", ligne)" + ] + }, + { + "cell_type": "markdown", + "id": "5a1d7c2e", + "metadata": {}, + "source": [ + "### 6.1 Contrefactuel homogène par rôle (complément du préflight, point 6)\n", + "\n", + "La déviation unilatérale **homogène** est mesurée sur les actions observées : pour chaque tour, le rapport adverse observé est fixé, seule l'action propre bascule (`a ↔ a'`), `mecanisme()` est rejoué — opération **déterministe**, zéro appel LLM supplémentaire. Gains et pertes sont publiés **par rôle** (row puis col), puis par type vrai. Cela complète les asserts statiques de dominance du §1 (propriété du mécanisme) par la mesure empirique du coût de déviation unilatéral par rôle." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d8b4e9f1", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:56:05.915507Z", + "iopub.status.busy": "2026-09-09T21:56:05.915319Z", + "iopub.status.idle": "2026-09-09T21:56:05.923295Z", + "shell.execute_reply": "2026-09-09T21:56:05.922654Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== 6.1 Contrefactuel homogene PAR ROLE : gain/perte de la deviation unilaterale ===\n", + " scripted-equilibre scripte : row -2.000 (n=4) | col -2.500 (n=4)\n", + " scripted-byzantin scripte : row +3.000 (n=4) | col +4.500 (n=4)\n", + " scripted-aleatoire scripte : row -0.125 (n=16) | col +0.000 (n=16)\n", + " llm-regles-visibles glm : row +nan (n=0) | col +nan (n=0)\n", + " llm-regles-visibles qwen : row -2.000 (n=16) | col -2.500 (n=16)\n", + " llm-aveugle glm : row -1.500 (n=16) | col -1.500 (n=16)\n", + " llm-aveugle qwen : row -1.500 (n=16) | col -1.500 (n=16)\n", + "\n", + "=== ... PAR TYPE vrai (gain moyen de la deviation, toutes conditions principales) ===\n", + " type=a role=row : gain moyen +0.167 (n=36)\n", + " type=a role=col : gain moyen +0.556 (n=36)\n", + " type=a' role=row : gain moyen -2.333 (n=36)\n", + " type=a' role=col : gain moyen -2.778 (n=36)\n", + "\n", + "Lecture : la deviation unilaterale depuis a' vers a (rapport adverse fixe) reduit le\n", + "payoff de l'agent qui devie — traduction empirique, par role et par type, de la\n", + "dominance stricte de a'.\n" + ] + } + ], + "source": [ + "def deviations_par_role(cond_rows):\n", + " \"\"\"Deviation unilaterale homogene : rapport adverse OBSERVE fixe, action propre\n", + " basculee (a <-> a'), mecanisme() rejoue (deterministe, 0 appel LLM).\"\"\"\n", + " out = {}\n", + " for role, autre in ((\"row\", \"col\"), (\"col\", \"row\")):\n", + " gains = []\n", + " for r in cond_rows:\n", + " if r[\"outcome\"] is None:\n", + " continue\n", + " if r[\"statuts\"][role] not in STATUTS_ACTES or r[\"statuts\"][autre] not in STATUTS_ACTES:\n", + " continue\n", + " jouee, adverse = r[f\"action_{role}\"], r[f\"action_{autre}\"]\n", + " dev = \"a\" if jouee == \"a'\" else \"a'\"\n", + " o_dev = mecanisme(dev, adverse) if role == \"row\" else mecanisme(adverse, dev)\n", + " th = r[\"theta_row\"] if role == \"row\" else r[\"theta_col\"]\n", + " gains.append(payoff(role, o_dev, th) - r[f\"payoff_{role}\"])\n", + " out[role] = gains\n", + " return out\n", + "\n", + "print(\"=== 6.1 Contrefactuel homogene PAR ROLE : gain/perte de la deviation unilaterale ===\")\n", + "for condition in (\"scripted-equilibre\", \"scripted-byzantin\", \"scripted-aleatoire\",\n", + " \"llm-regles-visibles\", \"llm-aveugle\"):\n", + " for famille in (*list(PREREG[\"familles\"]), \"scripte\"):\n", + " rows = [r for r in traces if r[\"famille\"] == famille and r[\"condition\"] == condition]\n", + " if not rows:\n", + " continue\n", + " dev = deviations_par_role(rows)\n", + " gain_r = statistics.mean(dev[\"row\"]) if dev[\"row\"] else float(\"nan\")\n", + " gain_c = statistics.mean(dev[\"col\"]) if dev[\"col\"] else float(\"nan\")\n", + " print(f\" {condition:20s} {famille:5s} : row {gain_r:+.3f} (n={len(dev['row'])}) | \"\n", + " f\"col {gain_c:+.3f} (n={len(dev['col'])})\")\n", + "\n", + "print()\n", + "print(\"=== ... PAR TYPE vrai (gain moyen de la deviation, toutes conditions principales) ===\")\n", + "for t in PREREG[\"types\"]:\n", + " for role, autre in ((\"row\", \"col\"), (\"col\", \"row\")):\n", + " gains = []\n", + " for r in traces:\n", + " if r[\"outcome\"] is None:\n", + " continue\n", + " th = r[\"theta_row\"] if role == \"row\" else r[\"theta_col\"]\n", + " if th != t or r[\"statuts\"][role] not in STATUTS_ACTES or r[\"statuts\"][autre] not in STATUTS_ACTES:\n", + " continue\n", + " jouee, adverse = r[f\"action_{role}\"], r[f\"action_{autre}\"]\n", + " dev = \"a\" if jouee == \"a'\" else \"a'\"\n", + " o_dev = mecanisme(dev, adverse) if role == \"row\" else mecanisme(adverse, dev)\n", + " gains.append(payoff(role, o_dev, th) - r[f\"payoff_{role}\"])\n", + " if gains:\n", + " print(f\" type={t} role={role:3s} : gain moyen {statistics.mean(gains):+.3f} (n={len(gains)})\")\n", + "\n", + "print()\n", + "print(\"Lecture : la deviation unilaterale depuis a' vers a (rapport adverse fixe) reduit le\")\n", + "print(\"payoff de l'agent qui devie — traduction empirique, par role et par type, de la\")\n", + "print(\"dominance stricte de a'.\")" + ] + }, + { + "cell_type": "markdown", + "id": "696281b6", + "metadata": {}, + "source": [ + "## 7. Scénario hostile — permutation d'une règle, coût neutralisé\n", + "\n", + "La condition `llm-permutation` présente « a » comme action dominante alors que le mécanisme\n", + "réel n'a pas changé (a' reste dominante, oracle inchangé). Un joueur qui **comprend la règle\n", + "communiquée** (sans la vérifier contre le tableau d'utilités) bascule vers a ; un joueur qui\n", + "**comprend le mécanisme** ignore l'annonce. Le prompt permuté est de même longueur que\n", + "l'original (coût neutralisé). L'avantage attendu des règles visibles (§6) doit **disparaître**\n", + "ici si les LLM suivent l'annonce de surface." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "4498192e", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:56:05.925133Z", + "iopub.status.busy": "2026-09-09T21:56:05.924886Z", + "iopub.status.idle": "2026-09-09T21:57:12.355854Z", + "shell.execute_reply": "2026-09-09T21:57:12.355306Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hostile : 16 tours, 32 appels (total LLM : 160 / 96)\n", + "\n", + "=== Taux d'action a' sous permutation (une comprehension de surface doit le faire chuter) ===\n", + " permutation glm : taux a' = nan, invalide = 1.000, reseau = 0.000\n", + " permutation qwen : taux a' = 0.750, invalide = 0.000, reseau = 0.000\n" + ] + } + ], + "source": [ + "taches_hostiles = [(\"llm-permutation\", famille, modele, tr, tc, seed)\n", + " for famille, modele in PREREG[\"familles\"].items()\n", + " for seed in PREREG[\"seeds_hostile\"]\n", + " for (tr, tc) in PREREG[\"profils_vrais\"]]\n", + "with ThreadPoolExecutor(max_workers=PREREG[\"concurrence\"]) as pool:\n", + " traces_hostiles = list(pool.map(lambda t: jouer_tour(*t), taches_hostiles))\n", + "traces.extend(traces_hostiles)\n", + "n_appels_h = 2 * len(taches_hostiles)\n", + "\n", + "print(f\"Hostile : {len(traces_hostiles)} tours, {n_appels_h} appels \"\n", + " f\"(total LLM : {n_appels + n_appels_h} / {PREREG['budget_appels_max']})\")\n", + "print(\"\\n=== Taux d'action a' sous permutation (une comprehension de surface doit le faire chuter) ===\")\n", + "for famille in PREREG[\"familles\"]:\n", + " rows = [r for r in traces_hostiles if r[\"famille\"] == famille]\n", + " print(f\" permutation {famille:5s} : taux a' = {taux_dominant(rows):.3f}, \"\n", + " f\"invalide = {taux_invalide(rows):.3f}, reseau = {taux_reseau(rows):.3f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "01d1b029", + "metadata": {}, + "source": [ + "## 8. Verdict pré-enregistré vs résultats (H0/H1)\n", + "\n", + "Les seuils sont lus dans le gel du §0 — jamais ajustés après exécution. Un verdict négatif\n", + "(H0 non rejetée) est un résultat valide de ce pilote.\n", + "\n", + "**Précision d'interprétation (contre-préflight adjoint)** : un taux marginal de 0,500 en condition aveugle ne démontre pas à lui seul une politique uniforme (les profils du gel équilibrent les marges par construction). La décomposition par type (§6) montre un comportement déterministe de rapport du **type propre** — taux a' ≈ 0 pour le type a, ≈ 1 pour le type a' — plutôt qu'un tirage uniforme conditionnel. Cette qualification s'applique à la condition aveugle quel que soit le verdict global du gel (rendu au §8 sur les marginales)." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "f4649ad2", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:57:12.357886Z", + "iopub.status.busy": "2026-09-09T21:57:12.357679Z", + "iopub.status.idle": "2026-09-09T21:57:12.365454Z", + "shell.execute_reply": "2026-09-09T21:57:12.364788Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "H0 : les actions LLM (regles visibles ou aveugles) sont indiscernables de l'uniforme 50/50 sur {a, a'}\n", + "H1 : avec regles visibles, taux d'action dominante a' >= 0.80 et > taux aveugle (ecart >= 0.20) ; sous permutation hostile, le taux visible retombe vers l'uniforme (chute >= 0.20 vs visible) \n", + "\n", + "[glm] visible = 1.000 [0.722, 1.000] (n=10) | aveugle = 0.500 (n=32) | permute = nan (n=0)\n", + " tailles d'effet (h de Cohen) : visible-vs-aveugle = 1.57 ; visible-vs-permute = nan (~0.2 petit, ~0.5 moyen, ~0.8 grand)\n", + " seuils du gel -> H1_visible=True H1_ecart=True H1_chute=absence de mesure (non evalue)\n", + "[qwen] visible = 1.000 [0.893, 1.000] (n=32) | aveugle = 0.500 (n=32) | permute = 0.750 (n=16)\n", + " tailles d'effet (h de Cohen) : visible-vs-aveugle = 1.57 ; visible-vs-permute = 1.05 (~0.2 petit, ~0.5 moyen, ~0.8 grand)\n", + " seuils du gel -> H1_visible=True H1_ecart=True H1_chute=True\n", + "\n", + "VERDICT : absence de mesure -- glm sans acte evaluable sur au moins un seuil (n=0) : run non evalue, ni H0 ni H1\n", + "Consommation : 92491 tokens LLM au total, latence moyenne/tour 12.19s sur 80 tours LLM, erreurs reseau residuelles : 0\n" + ] + } + ], + "source": [ + "seuils = PREREG[\"seuils\"]\n", + "verdicts = {}\n", + "\n", + "def comptes(famille, condition, base=None):\n", + " rows = [r for r in (base if base is not None else traces)\n", + " if r[\"famille\"] == famille and r[\"condition\"] == condition]\n", + " return compter(rows)\n", + "\n", + "print(\"H0 :\", PREREG[\"H0\"])\n", + "print(\"H1 :\", PREREG[\"H1\"], \"\\n\")\n", + "for famille in PREREG[\"familles\"]:\n", + " kv, nv = comptes(famille, \"llm-regles-visibles\")\n", + " ka, na = comptes(famille, \"llm-aveugle\")\n", + " kp, np_ = comptes(famille, \"llm-permutation\", base=traces_hostiles)\n", + " rat = lambda k, n: (k / n) if n else float(\"nan\")\n", + " v, a, p = rat(kv, nv), rat(ka, na), rat(kp, np_)\n", + " lov, hiv = wilson(kv, nv)\n", + " h_va = cohen_h(kv, nv, ka, na) # effet visible vs aveugle\n", + " h_vp = cohen_h(kv, nv, kp, np_) # effet visible vs permute\n", + " # n=0 -> taux nan : seuil non evalue (absence de mesure), pas echoue (False bidon)\n", + " mesure = lambda x: x == x # False pour nan (compteur n==0)\n", + " h1_visible = (v >= seuils[\"taux_dominant_visible_min\"]) if mesure(v) else None\n", + " h1_ecart = ((v - a) >= seuils[\"ecart_visible_vs_aveugle_min\"]) if (mesure(v) and mesure(a)) else None\n", + " h1_chute = ((v - p) >= seuils[\"chute_permutation_min\"]) if (mesure(v) and mesure(p)) else None\n", + " verdicts[famille] = {\"visible\": v, \"aveugle\": a, \"permute\": p,\n", + " \"H1_visible\": h1_visible, \"H1_ecart\": h1_ecart, \"H1_chute\": h1_chute}\n", + " print(f\"[{famille}] visible = {v:.3f} [{lov:.3f}, {hiv:.3f}] (n={nv}) | \"\n", + " f\"aveugle = {a:.3f} (n={na}) | permute = {p:.3f} (n={np_})\")\n", + " print(f\" tailles d'effet (h de Cohen) : visible-vs-aveugle = {h_va:.2f} ; \"\n", + " f\"visible-vs-permute = {h_vp:.2f} (~0.2 petit, ~0.5 moyen, ~0.8 grand)\")\n", + " lbl = lambda b: \"absence de mesure (non evalue)\" if b is None else b\n", + " print(f\" seuils du gel -> H1_visible={lbl(h1_visible)} H1_ecart={lbl(h1_ecart)} H1_chute={lbl(h1_chute)}\")\n", + "\n", + "absences = [fam for fam, d in verdicts.items()\n", + " if any(d[k] is None for k in (\"H1_visible\", \"H1_ecart\", \"H1_chute\"))]\n", + "h1_globalement = all(d[\"H1_visible\"] and d[\"H1_ecart\"] and d[\"H1_chute\"] for d in verdicts.values())\n", + "if absences:\n", + " print(\"\\nVERDICT : absence de mesure -- \" + \", \".join(absences) +\n", + " \" sans acte evaluable sur au moins un seuil (n=0) : run non evalue, ni H0 ni H1\")\n", + "elif h1_globalement:\n", + " print(\"\\nVERDICT : H1 retenue (chaque famille passe les 3 seuils du gel)\")\n", + "else:\n", + " print(\"\\nVERDICT : H0 non rejetee (au moins une famille ou un seuil du gel echoue) -- resultat negatif valide\")\n", + "tours_llm = [r for r in traces if r[\"tokens\"] > 0]\n", + "tokens_total = sum(r[\"tokens\"] for r in tours_llm)\n", + "latences = [r[\"latence_s\"] for r in tours_llm]\n", + "n_reseau = sum(1 for r in traces for s_ in r[\"statuts\"].values() if s_ == \"erreur_reseau\")\n", + "print(f\"Consommation : {tokens_total} tokens LLM au total, \"\n", + " f\"latence moyenne/tour {statistics.mean(latences):.2f}s sur {len(tours_llm)} tours LLM, \"\n", + " f\"erreurs reseau residuelles : {n_reseau}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7c3e9a05", + "metadata": {}, + "source": [ + "### Écarts d'exécution vs gel (consignés, conformément au §0)\n", + "\n", + "1. **Budget d'appels** : le gel déclarait `budget_appels_max = 96`, arithmetic sous-estimée\n", + " (les 2 conditions LLM principales ont été oubliées dans le produit : 2 conditions × 2 familles\n", + " × 4 seeds × 4 profils × 2 agents = 128 appels principaux, + 32 hostiles = **160 appels logiques\n", + " consommés**). Aucun appel supplémentaire n'a été émis au-delà du plan d'expérience gelé\n", + " (seeds, conditions, familles, profils tous conformes au gel) ; seule la constante déclarée\n", + " du budget était fausse. Consigné ici plutôt que corrigé silencieusement dans le gel.\n", + "2. **Aucune erreur réseau résiduelle** : la politique de retries (gel) a suffi\n", + " (`erreurs reseau residuelles : 0`).\n", + "3. **Réconciliation des compteurs (actes vs traces)** : ce run produit **104 traces** au total\n", + " (88 lignes principales — 24 tours scriptés + 64 tours LLM — et 16 lignes hostiles), contrôles\n", + " scriptés inclus. Les **actes LLM** se comptent sur les seuls tours LLM :\n", + " **80 tours LLM × 2 agents = 160 actes = 122 actes valides + 38 actes invalides**\n", + " (22 `glm`-visibles + 16 `glm`-hostiles ; `qwen` est à 0 invalide sur toutes ses conditions).\n", + " Les taux des §6/§8 portent sur les seuls actes valides ; les taux d'invalide et de réseau\n", + " sont rapportés séparément (jamais confondus, gel §0).\n", + "\n", + "Lecture honnête du verdict de ce run : `qwen` passe les 3 seuils du gel (n=32/32/16 actes\n", + "valides) ; `glm` passe visible et écart (n=10 en visible, 68,8 % d'actes invalides : le modèle\n", + "raisonnant produit souvent un JSON non strict quand le prompt embarque le bloc de règles\n", + "complet — incapacité de format, pas un choix de jeu) mais rend **100 % d'actes invalides en\n", + "permutation (n=0)** : le seuil de chute n'est pas évaluable et le verdict global est\n", + "**absence de mesure — run non évalué, ni H0 ni H1**. C'est exactement le cas où la comparaison\n", + "naïve sur `nan` aurait rendu `H1_chute=False` (un faux négatif booléen, « H0 non rejetée ») :\n", + "le chemin `n==0` du §8 rend désormais l'absence de mesure explicite. Le run précédent (même\n", + "gel, mêmes seeds) avait mesuré n=4 valides en `glm`-permutation et retenu H1 : la bascule du\n", + "verdict global sur la seule variance inter-run de `glm`-permutation (n=4 → n=0 entre deux runs\n", + "consécutifs) est la fragilité à retenir — l'hétérogénéité des familles reste l'objet même du\n", + "pilote." + ] + }, + { + "cell_type": "markdown", + "id": "76c9cb72", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "Ce pilote mesure ce que font de vrais LLM hétérogènes face à un mécanisme à action\n", + "dominante — l'objet exact de la Proposition 6 (MOM, Othman–Sandholm 2009), importé sans\n", + "divergence depuis le notebook owner (asserts §1) et rejoué par son oracle. Les contrôles\n", + "scriptés encadrent la lecture : l'équilibre (a') et le byzantin (a) fixent les bornes,\n", + "l'aléatoire seedé fixe le plancher, la condition aveugle isole l'effet de l'information,\n", + "la permutation hostile teste la compréhension de surface. Ce que le pilote ne fait pas :\n", + "il ne prouve rien sur le théorème Lean (existence/propriétés du mécanisme) — il mesure\n", + "des comportements. Les traces complètes (une ligne par tour) restent dans `traces`\n", + "pour ré-analyse.\n", + "\n", + "**Voir aussi** : `GameTheory-16-MechanismDesign.ipynb` §4.6 (owner),\n", + "`GameTheory-16b-Automated-Mechanism-Design.ipynb` (AMD), `GameTheory-03c-Le-Joueur-LLM.ipynb`\n", + "(LLM sur jeu 2×2, autre classe d'objet), `game_theory_lean/SocialChoice/MechanismDesign.lean`\n", + "(formalisation)." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "e1df6480", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:57:12.367854Z", + "iopub.status.busy": "2026-09-09T21:57:12.367545Z", + "iopub.status.idle": "2026-09-09T21:57:12.371503Z", + "shell.execute_reply": "2026-09-09T21:57:12.370227Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice a completer\n" + ] + } + ], + "source": [ + "# Exercice 1 — Regret du mecanisme a l'equilibre\n", + "# Pour chaque profil de types vrai, calculez le regret du mecanisme M sous rapports\n", + "# d'equilibre (a', a') : sw_optimal(theta) - sw_oracle(M(a',a'), theta).\n", + "# Indice : sw_optimal et mecanisme(\"a'\", \"a'\") suffisent ; sommez les 4 regrets.\n", + "# Etape 2 : comparez au cout de la manipulation (ce que gagne un byzantin a devier,\n", + "# par profil -- cf. traces scripted-byzantin).\n", + "print(\"Exercice a completer\")" + ] + }, + { + "cell_type": "markdown", + "id": "4365b410", + "metadata": {}, + "source": [ + "## Exercices" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "451dd9f5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:57:12.373358Z", + "iopub.status.busy": "2026-09-09T21:57:12.373055Z", + "iopub.status.idle": "2026-09-09T21:57:12.375893Z", + "shell.execute_reply": "2026-09-09T21:57:12.375551Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice a completer\n" + ] + } + ], + "source": [ + "# Exercice 2 — Une troisieme famille\n", + "# Ajoutez la famille deepseek (modele 'deepseek-v4-flash', servi par le meme proxy)\n", + "# PUIS relancez uniquement la condition llm-regles-visibles.\n", + "# Indice : PREREG est gele -- la deviation doit etre consignee au §8 comme\n", + "# modification de protocole, pas silencieusement appliquee.\n", + "print(\"Exercice a completer\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "9b70a05f", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-09T21:57:12.377553Z", + "iopub.status.busy": "2026-09-09T21:57:12.377337Z", + "iopub.status.idle": "2026-09-09T21:57:12.380318Z", + "shell.execute_reply": "2026-09-09T21:57:12.379889Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice a completer\n" + ] + } + ], + "source": [ + "# Exercice 3 — Byzantin mixte et l'amelioration paradoxale\n", + "# Mesurez le SW realise quand exactement UN agent (row) suit la politique byzantine\n", + "# et l'autre l'equilibre, sur les 4 profils. Comparez au resultat « mieux avec\n", + "# Byzantine » du owner (§4.6.3, Caracteristique 2) : pour lesquels des 3 profils\n", + "# byzantins l'outcome M(a, a') ou M(a', a) bat-il o1 au sens de l'oracle ?\n", + "# Indice : ecrivez une variante de jouer_tour a politiques mixtes, ou raisonnez\n", + "# directement sur sw_oracle et M_MAP.\n", + "print(\"Exercice a completer\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}