From 7a26e6808d092b2bc18670cea020bf4ac0be7308 Mon Sep 17 00:00:00 2001 From: jsboige Date: Tue, 22 Sep 2026 03:53:30 +0200 Subject: [PATCH 1/8] =?UTF-8?q?Add:=20distillation=202.1.6=20gouvernance?= =?UTF-8?q?=20multi-agents=20=E2=80=94=20organe=20governance=5Fmethods=20+?= =?UTF-8?q?=20notebook=20(sous-grain=202=20#4960)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - governance_methods.py : organe pur stdlib (7 methodes de vote/consensus, agents a personnalites, coalitions, Shapley, metriques consensus/Gini/ satisfaction, 6 scenarios etiquetes du banc de 16 du source) - notebook Gouvernance_Multi_Agents (30 cellules, execute 12/12) : spoiler et cycle de Condorcet mesures, tableau croise 7 methodes x 6 scenarios, robustesse byzantine multi-seed, manipulation, coalitions + Shapley, 6 divergences source documentees, 3 exercices C.1 - tests/test_governance_methods.py : 16 tests purs (comptes figes, determinisme seed, votes calculables a la main) - README : 3 lignes (notebooks, parcours, ordre de lecture) + mapping 28/28->29/29 ; marqueur CATALOG-STATUS byte-identique Co-Authored-By: Claude Sonnet 5 --- ...nt_Analysis_Gouvernance_Multi_Agents.ipynb | 1158 +++++++++++++++++ .../SymbolicAI/Argument_Analysis/README.md | 5 +- .../Argument_Analysis/governance_methods.py | 448 +++++++ .../tests/test_governance_methods.py | 127 ++ 4 files changed, 1737 insertions(+), 1 deletion(-) create mode 100644 MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb create mode 100644 MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py create mode 100644 MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/tests/test_governance_methods.py diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb new file mode 100644 index 0000000000..5112df37c8 --- /dev/null +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb @@ -0,0 +1,1158 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "gov-01", + "metadata": { + "papermill": { + "duration": 0.002754, + "end_time": "2026-09-22T01:52:48.253225+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.250471+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "# Gouvernance multi-agents — voter, coalitionner, résister\n", + "\n", + "Distillation du sous-projet EPITA `2.1.6_multiagent_governance_prototype`\n", + "(EPIC #4960, mandat Triple Distillation : porter l'essence vivante « sans le\n", + "bruit d'une année de régressions et d'itérations »).\n", + "\n", + "**Position dans l'arc** — [Agentic-3-orchestration](Argument_Analysis_Agentic-3-orchestration.ipynb)\n", + "répond à « comment répartir le *travail* entre agents » (pipeline DAG,\n", + "orchestrateur conversationnel) ; ce notebook répond à « comment un collectif\n", + "*décide* » : sept méthodes de vote et de consensus, des personnalités d'agents,\n", + "des coalitions, et la question centrale de toute gouvernance — **que résiste\n", + "la décision à la manipulation ?**\n", + "\n", + "L'organe `governance_methods.py` est un module **pur** (stdlib uniquement) :\n", + "les mécanismes sont calculables et testables sans aucune dépendance." + ] + }, + { + "cell_type": "markdown", + "id": "gov-02", + "metadata": { + "papermill": { + "duration": 0.001763, + "end_time": "2026-09-22T01:52:48.257183+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.255420+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Pourquoi la gouvernance est un problème dur\n", + "\n", + "Répartir le travail entre agents (Agentic-3) se résout par de l'ingénierie ;\n", + "**décider à plusieurs** se heurte à un résultat d'impossibilité. Dès qu'un\n", + "collectif doit choisir parmi trois options ou plus à partir de préférences\n", + "individuelles ordonnées, le théorème d'Arrow (1951) interdit toute procédure\n", + "qui serait à la fois non-dictatoriale, unanime (si tous préfèrent X à Y, X\n", + "bat Y) et indépendante aux alternatives non pertinentes (le duel X vs Y ne\n", + "doit pas dépendre de la présence de Z). Il faut donc **choisir quelle\n", + "propriété sacrifier** — et chaque méthode de vote de ce notebook est un choix\n", + "différent de sacrifice. La suite ne débat pas de ces choix : elle les\n", + "**mesure** sur des scénarios où ils produisent des vainqueurs différents." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "gov-03", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.261788Z", + "iopub.status.busy": "2026-09-22T01:52:48.261603Z", + "iopub.status.idle": "2026-09-22T01:52:48.269394Z", + "shell.execute_reply": "2026-09-22T01:52:48.268654Z" + }, + "papermill": { + "duration": 0.011035, + "end_time": "2026-09-22T01:52:48.269947+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.258912+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"methods\": 7,\n", + " \"method_keys\": [\n", + " \"borda\",\n", + " \"byzantine\",\n", + " \"condorcet\",\n", + " \"majority\",\n", + " \"plurality\",\n", + " \"quadratic\",\n", + " \"raft\"\n", + " ],\n", + " \"scenarios\": 6,\n", + " \"scenario_names\": [\n", + " \"byzantine_noise\",\n", + " \"cyclic_majority\",\n", + " \"dictatorship\",\n", + " \"project_funding\",\n", + " \"spoiler_candidate\",\n", + " \"strategic_bloc\"\n", + " ]\n", + "}\n" + ] + } + ], + "source": [ + "import sys\n", + "sys.path.insert(0, \".\") # organe governance_methods.py dans le dossier de la serie\n", + "import governance_methods as g\n", + "import json\n", + "\n", + "counts = g.method_counts()\n", + "print(json.dumps(counts, ensure_ascii=False, indent=1))" + ] + }, + { + "cell_type": "markdown", + "id": "gov-04", + "metadata": { + "papermill": { + "duration": 0.001912, + "end_time": "2026-09-22T01:52:48.273936+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.272024+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture du compte\n", + "\n", + "L'organe porte **7 méthodes** de vote/consensus et **6 scénarios étiquetés**\n", + "(distillés du banc de 16 du source — les 6 retenus couvrent chacun un phénomène\n", + "de choix collectif distinct : dictature, cycle, candidat spoiler, bloc\n", + "discipliné, bruit byzantin, arbitrage budgétaire). Les tests purs\n", + "(`tests/test_governance_methods.py`, 16 tests) figent ces comptes." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "gov-05", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.278969Z", + "iopub.status.busy": "2026-09-22T01:52:48.278802Z", + "iopub.status.idle": "2026-09-22T01:52:48.283070Z", + "shell.execute_reply": "2026-09-22T01:52:48.282130Z" + }, + "papermill": { + "duration": 0.007751, + "end_time": "2026-09-22T01:52:48.283609+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.275858+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- cyclic_majority : Cycle de Condorcet : A bat B, B bat C, C bat A — aucun vainqueur net.\n", + " V1 [stubborn ] ['A', 'B', 'C']\n", + " V2 [stubborn ] ['B', 'C', 'A']\n", + " V3 [stubborn ] ['C', 'A', 'B']\n", + " options : ['A', 'B', 'C']\n", + "--- spoiler_candidate : Un candidat similaire divise un bloc : l'adversaire l'emporte.\n", + " G1 [stubborn ] ['Gauche', 'Centre', 'Droite']\n", + " G2 [stubborn ] ['Centre', 'Gauche', 'Droite']\n", + " D1 [stubborn ] ['Droite', 'Centre', 'Gauche']\n", + " options : ['Gauche', 'Centre', 'Droite']\n" + ] + } + ], + "source": [ + "# Anatomie de deux scénarios : le cycle et le spoiler\n", + "for name in [\"cyclic_majority\", \"spoiler_candidate\"]:\n", + " sc = g.SCENARIOS[name]\n", + " print(f\"--- {name} : {sc['description']}\")\n", + " for agent, perso, prefs in sc[\"agents\"]:\n", + " print(f\" {agent:<4} [{perso:<9}] {prefs}\")\n", + " print(f\" options : {sc['options']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "gov-06", + "metadata": { + "papermill": { + "duration": 0.002224, + "end_time": "2026-09-22T01:52:48.288203+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.285979+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture : préférences ordonnées et personnalités\n", + "\n", + "Chaque agent porte une **préférence complète** (un ordre, pas un seul choix)\n", + "et une **personnalité** qui gouverne son vote sincère : `stubborn` vote son\n", + "premier choix quoi qu'il arrive, `flexible` peut suivre un indice de majorité,\n", + "`strategic` vote son second choix pour bloquer un adversaire, `random` tire au\n", + "sort. Toute la mécanique de vote se lit sur ces ordres : les méthodes de ce\n", + "notebook ne voient jamais la personnalité, elles ne voient que ce qu'elle\n", + "produit — des préférences déclarées. C'est exactement la surface d'attaque des\n", + "manipulations de fin de parcours : mentir sur ses préférences est\n", + "indistinguable d'avoir d'autres préférences." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "gov-07", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.293418Z", + "iopub.status.busy": "2026-09-22T01:52:48.293158Z", + "iopub.status.idle": "2026-09-22T01:52:48.298406Z", + "shell.execute_reply": "2026-09-22T01:52:48.297802Z" + }, + "papermill": { + "duration": 0.008734, + "end_time": "2026-09-22T01:52:48.298920+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.290186+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Duels :\n", + " Gauche vs Centre : 1-2\n", + " Gauche vs Droite : 2-1\n", + " Centre vs Droite : 2-1\n", + "majority -> Gauche\n", + "borda -> Centre\n", + "condorcet -> Centre\n" + ] + } + ], + "source": [ + "# EFFET SPOILER : la même société, trois lectures du vainqueur\n", + "agents = g.build_agents(\"spoiler_candidate\")\n", + "opts = g.SCENARIOS[\"spoiler_candidate\"][\"options\"]\n", + "\n", + "# Duels pairwise (coeur de Condorcet)\n", + "print(\"Duels :\")\n", + "for i, o1 in enumerate(opts):\n", + " for o2 in opts[i+1:]:\n", + " v1 = sum(a.preferences.index(o1) < a.preferences.index(o2) for a in agents)\n", + " v2 = len(agents) - v1\n", + " print(f\" {o1} vs {o2} : {v1}-{v2}\")\n", + "\n", + "for m in [\"majority\", \"borda\", \"condorcet\"]:\n", + " winner = g.simulate_vote(g.build_agents(\"spoiler_candidate\"), opts, m, seed=0)[\"winner\"]\n", + " print(f\"{m:<10} -> {winner}\")" + ] + }, + { + "cell_type": "markdown", + "id": "gov-08", + "metadata": { + "papermill": { + "duration": 0.002345, + "end_time": "2026-09-22T01:52:48.303594+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.301249+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture : le spoiler mesuré\n", + "\n", + "Les duels racontent tout : **Centre bat Gauche (2-1) et bat Droite (2-1)** —\n", + "Centre est le *vainqueur de Condorcet*, il gagne contre tout le monde. Pourtant\n", + "la **majorité à un tour élit Gauche** : G1 vote Gauche, G2 vote Centre, D1 vote\n", + "Droite — le bloc progressiste (2 électeurs sur 3) est divisé par la présence de\n", + "deux options voisines, et l'adversaire minoritaire l'emporte avec 1 voix sur 3.\n", + "Borda et Condorcet, qui lisent les préférences **complètes**, élisent Centre.\n", + "C'est l'effet spoiler — l'une des deux briques empiriques du théorème\n", + "d'impossibilité d'Arrow. Le décompte à un tour : Gauche 1 voix (G1), Centre\n", + "1 voix (G2), Droite 1 voix (D1) — trois voix, aucune majorité, et le tie-break\n", + "de `Counter.most_common` tranche pour la première insérée : l'égalité parfaite\n", + "à un tour est elle-même une convention silencieuse, invisible dans le résultat\n", + "affiché." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "gov-09", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.309111Z", + "iopub.status.busy": "2026-09-22T01:52:48.308847Z", + "iopub.status.idle": "2026-09-22T01:52:48.313722Z", + "shell.execute_reply": "2026-09-22T01:52:48.313080Z" + }, + "papermill": { + "duration": 0.008749, + "end_time": "2026-09-22T01:52:48.314589+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.305840+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " A vs B : 2-1\n", + " A vs C : 1-2\n", + " B vs C : 2-1\n", + "vainqueur condorcet : A (repli Borda)\n", + "scores Borda : {'A': 3, 'B': 3, 'C': 3}\n" + ] + } + ], + "source": [ + "# CYCLE DE CONDORCET : A bat B, B bat C, C bat A\n", + "agents = g.build_agents(\"cyclic_majority\")\n", + "opts = g.SCENARIOS[\"cyclic_majority\"][\"options\"]\n", + "for i, o1 in enumerate(opts):\n", + " for o2 in opts[i+1:]:\n", + " v1 = sum(a.preferences.index(o1) < a.preferences.index(o2) for a in agents)\n", + " print(f\" {o1} vs {o2} : {v1}-{len(agents)-v1}\")\n", + "r = g.simulate_vote(agents, opts, \"condorcet\", seed=0)\n", + "print(\"vainqueur condorcet :\", r[\"winner\"], \"(repli Borda)\")\n", + "\n", + "# Scores Borda explicites : l'egalite parfaite\n", + "scores = {o: 0 for o in opts}\n", + "for a in agents:\n", + " for i, o in enumerate(a.preferences):\n", + " scores[o] += len(opts) - i - 1\n", + "print(\"scores Borda :\", scores)" + ] + }, + { + "cell_type": "markdown", + "id": "gov-10", + "metadata": { + "papermill": { + "duration": 0.002264, + "end_time": "2026-09-22T01:52:48.319326+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.317062+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture : le cycle et un repli qui ne résout rien\n", + "\n", + "A bat B (2-1), B bat C (2-1), C bat A (2-1) : **cycle parfait**, aucun vainqueur\n", + "de Condorcet. Le repli Borda du source rend une **égalité parfaite 3 = 3 = 3** —\n", + "trois préférences strictes en rotation donnent exactement autant de points à\n", + "chacune. Le `A` final ne sort d'aucune préférence collective : il sort du\n", + "tie-break de `max()`, qui rend la **première clé d'insertion**. Le repli ne\n", + "résout pas le cycle, il le tranche par convention d'ordre — c'est la divergence\n", + "6, mesurée ici plutôt qu'affirmée (l'exercice 3 mesure ce que raft, qui tire son\n", + "leader, fait du même cycle)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "gov-11", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.325188Z", + "iopub.status.busy": "2026-09-22T01:52:48.324884Z", + "iopub.status.idle": "2026-09-22T01:52:48.332573Z", + "shell.execute_reply": "2026-09-22T01:52:48.331716Z" + }, + "papermill": { + "duration": 0.011689, + "end_time": "2026-09-22T01:52:48.333227+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.321538+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "scenario borda byzantine condorcet majority plurality quadratic raft \n", + "-----------------------------------------------------------------------------------------------\n", + "byzantine_noise A A A A A A A \n", + "cyclic_majority A A A A A A C \n", + "dictatorship B C B C C B B \n", + "project_funding Ecole Ecole Ecole Ecole Ecole Ecole Ecole \n", + "spoiler_candidate Centre Gauche Centre Gauche Gauche Gauche Centre \n", + "strategic_bloc A A A A A A A \n" + ] + } + ], + "source": [ + "# Tableau croise : le vainqueur selon la methode et le scenario\n", + "noms = counts[\"scenario_names\"]\n", + "entete = f\"{'scenario':<18}\" + \"\".join(f\"{m:<11}\" for m in counts[\"method_keys\"])\n", + "print(entete)\n", + "print(\"-\" * len(entete))\n", + "for sc in noms:\n", + " opts = g.SCENARIOS[sc][\"options\"]\n", + " ligne = f\"{sc:<18}\"\n", + " for m in counts[\"method_keys\"]:\n", + " w = g.simulate_vote(g.build_agents(sc), opts, m, seed=42)[\"winner\"]\n", + " ligne += f\"{w:<11}\"\n", + " print(ligne)" + ] + }, + { + "cell_type": "markdown", + "id": "gov-12", + "metadata": { + "papermill": { + "duration": 0.002415, + "end_time": "2026-09-22T01:52:48.338217+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.335802+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture : la méthode change le vainqueur\n", + "\n", + "Sur six scénarios, trois rendent le même gagnant sous les sept méthodes\n", + "(`byzantine_noise`, `project_funding`, `strategic_bloc` : une dominance claire\n", + "absorbe le choix de procédure) et **trois divergent** :\n", + "\n", + "- `dictatorship` : majorité/pluralité/byzantin élisent **C**, Borda/Condorcet/\n", + " quadratique/Raft élisent **B** — le « dictateur » D1 (stratégique) vote son\n", + " *second* choix, et la lecture complète des préférences préfère son sacrifice ;\n", + "- `spoiler_candidate` : majorité et dérivées élisent **Gauche**, les méthodes\n", + " à préférences complètes élisent **Centre** ;\n", + "- `cyclic_majority` : seul **Raft déraille** (C) — son leader tiré au sort\n", + " propose depuis n'importe quelle préférence.\n", + "\n", + "Il n'existe pas de procédure neutre : changer la règle, c'est changer le\n", + "gagnant — sur un tiers de ce banc, sans toucher une seule préférence. Note de\n", + "lecture : les colonnes `majority` et `plurality` sont identiques par\n", + "construction (le source définit la pluralité comme un simple rappel de la\n", + "majorité — les tests purs figent cet alias), et la colonne `byzantine` à ratio\n", + "par défaut 0.2 suit la majorité sauf sur `dictatorship`, où son remplacement\n", + "du premier agent déplace exactement la voix qui faisait basculer C. Les trois\n", + "scénarios convergents montrent l'autre face : quand une dominance est claire\n", + "(bloc de 3 sur 5, option-Condorcet évidente), **toutes** les procédures\n", + "raisonnables l'expriment — la méthode ne devient décisive que près de\n", + "l'égalité, c'est-à-dire précisément là où elle est indécidable en principe." + ] + }, + { + "cell_type": "markdown", + "id": "gov-13", + "metadata": { + "papermill": { + "duration": 0.002438, + "end_time": "2026-09-22T01:52:48.343060+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.340622+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Mesurer une décision collective\n", + "\n", + "Dire « méthode plus juste » n'a de sens qu'avec un instrument. L'organe en\n", + "fournit trois, complémentaires :\n", + "\n", + "- **taux de consensus** : fraction des votes alignés sur le vainqueur — un\n", + " collectif déchiré élit quelqu'un que peu ont voté ;\n", + "- **justice** : `1 - Gini(satisfactions)`, où le coefficient de Gini mesure\n", + " l'inégalité d'une distribution (0 = tous égaux, 1 = maximalement inégal) —\n", + " une méthode peut satisfaire beaucoup *en moyenne* en sacrifiant toujours\n", + " les mêmes agents, le Gini le voit ;\n", + "- **satisfaction moyenne** : proximité du vainqueur au premier choix de\n", + " chacun, normalisée par la longueur des préférences.\n", + "\n", + "La cellule suivante croise ces instruments avec les méthodes : c'est là que\n", + "l'effet spoiler devient un chiffre, pas une intuition." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "gov-14", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.348987Z", + "iopub.status.busy": "2026-09-22T01:52:48.348750Z", + "iopub.status.idle": "2026-09-22T01:52:48.353174Z", + "shell.execute_reply": "2026-09-22T01:52:48.352616Z" + }, + "papermill": { + "duration": 0.00819, + "end_time": "2026-09-22T01:52:48.353674+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.345484+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "scenario methode consensus justice satisfaction\n", + "spoiler_candidate majority 0.333 0.556 0.5\n", + "spoiler_candidate borda 0.333 0.833 0.667\n", + "spoiler_candidate condorcet 0.333 0.833 0.667\n", + "spoiler_candidate quadratic 0.333 0.556 0.5\n", + "dictatorship majority 0.6 0.667 0.6\n", + "dictatorship borda 0.6 0.667 0.6\n", + "dictatorship condorcet 0.6 0.667 0.6\n", + "dictatorship quadratic 0.6 0.667 0.6\n" + ] + } + ], + "source": [ + "# Metriques : justice et satisfaction par methode (spoiler + dictatorship)\n", + "print(f\"{'scenario':<12} {'methode':<10} {'consensus':<10} {'justice':<9} {'satisfaction'}\")\n", + "for sc in [\"spoiler_candidate\", \"dictatorship\"]:\n", + " opts = g.SCENARIOS[sc][\"options\"]\n", + " for m in [\"majority\", \"borda\", \"condorcet\", \"quadratic\"]:\n", + " r = g.simulate_vote(g.build_agents(sc), opts, m, seed=0)\n", + " s = g.summarize(r)\n", + " print(f\"{sc:<12} {m:<10} {s['consensus_rate']:<10} {s['fairness']:<9} {s['satisfaction']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "gov-15", + "metadata": { + "papermill": { + "duration": 0.002394, + "end_time": "2026-09-22T01:52:48.358574+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.356180+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture : justice et satisfaction vont ensemble — ici\n", + "\n", + "Sur le spoiler, les méthodes à préférences complètes dominent sur les deux axes :\n", + "justice (Gini) **0.833** contre **0.556**, satisfaction moyenne **0.667** contre\n", + "**0.5**. Élire le vainqueur de Condorcet n'est pas seulement « plus juste » au\n", + "sens de Gini : il satisfait *aussi* mieux le collectif — Centre est second\n", + "choix de ceux dont il bat le premier. Sur `dictatorship` en revanche, les quatre\n", + "méthodes rendent des métriques identiques (0.667 / 0.6) : ce scénario ne\n", + "discrimine pas les procédures, il montre surtout qu'un seul vote stratégique\n", + "déplace l'égalité initiale. Le banc garde son pouvoir discriminant précisément\n", + "sur `spoiler_candidate`." + ] + }, + { + "cell_type": "markdown", + "id": "gov-16", + "metadata": { + "papermill": { + "duration": 0.002304, + "end_time": "2026-09-22T01:52:48.363188+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.360884+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Et quand les agents tombent en panne ?\n", + "\n", + "Les méthodes précédentes supposent des votes sincères et bien transmis. Un\n", + "système multi-agents réel affronte trois pannes : des agents **défaillants**\n", + "qui votent au hasard (le modèle byzantin), des agents **stratégiques** qui\n", + "déclarent autre chose que leur préférence vraie, et des **coalitions**\n", + "organisées qui coordonnent leurs votes. Le banc du source étiquetait\n", + "précisément ces axes — la suite les mesure un par un, en commençant par le\n", + "bruit : la cellule suivante fait croître la fraction d'agents byzantins et\n", + "observe le taux de consensus." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "gov-17", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.368679Z", + "iopub.status.busy": "2026-09-22T01:52:48.368508Z", + "iopub.status.idle": "2026-09-22T01:52:48.374348Z", + "shell.execute_reply": "2026-09-22T01:52:48.373661Z" + }, + "papermill": { + "duration": 0.009443, + "end_time": "2026-09-22T01:52:48.374890+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.365447+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ratio byzantin 0.0 : consensus moyen = 0.72\n", + "ratio byzantin 0.2 : consensus moyen = 0.76\n", + "ratio byzantin 0.4 : consensus moyen = 0.62\n" + ] + } + ], + "source": [ + "# ROBUSTESSE BYZANTINE : consensus moyen selon la fraction de votes aleatoires\n", + "# (10 seeds par palier, moyenne des taux de consensus)\n", + "opts = g.SCENARIOS[\"byzantine_noise\"][\"options\"]\n", + "for ratio in [0.0, 0.2, 0.4]:\n", + " taux = []\n", + " for seed in range(10):\n", + " agents = g.build_agents(\"byzantine_noise\")\n", + " r = g.simulate_vote(agents, opts, \"byzantine\",\n", + " context={\"byzantine_ratio\": ratio}, seed=seed)\n", + " taux.append(g.consensus_rate(r))\n", + " print(f\"ratio byzantin {ratio:.1f} : consensus moyen = {sum(taux)/len(taux):.2f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "gov-18", + "metadata": { + "papermill": { + "duration": 0.002412, + "end_time": "2026-09-22T01:52:48.379877+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.377465+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture : une courbe non monotone — l'artefact révélé\n", + "\n", + "Intuition : plus de bruit byzantin = moins de consensus. La mesure dit\n", + "**0.72 → 0.76 → 0.62** : le palier 20 % de bruit est *meilleur* que le palier\n", + "0 %. Deux causes, toutes deux instructives :\n", + "\n", + "1. **La base n'est pas silencieuse** : à ratio 0.0, les personnalités `random`\n", + " (Z1, Z2) votent déjà aléatoirement — le « 0 % de bruit » du paramètre n'est\n", + " pas le zéro de bruit du système.\n", + "2. **Le slicing déterministe (divergence 3)** : à 5 agents, ratio 0.2 = un\n", + " byzantin = *toujours le premier agent* (H1), jamais un tirage. Le remplaçant\n", + " aléatoire, sur ces 10 seeds, déplace le vainqueur vers un candidat\n", + " mieux aligné des votes sincères — le taux remonte.\n", + "\n", + "La dégradation réelle n'apparaît qu'à 40 % (0.62). C'est exactement le type\n", + "d'effet que la tranche `agents[:n]` du source rend invisible : un organe qui\n", + "tirait les byzantins au sort laverait cette bosse — au prix de perdre la\n", + "fidélité du port. Le port reste fidèle, la mesure reste honnête.\n", + "\n", + "Le protocole lui-même est une leçon : **dix seeds par palier, moyenne rendue**\n", + "— un seul seed aurait rendu une des trois valeurs par palier et masqué la\n", + "non-monotonie ou l'aurait fabriquée par chance. Mesurer un système à tirage,\n", + "c'est mesurer une distribution, pas un point : le taux de consensus à ratio\n", + "0.2 sur UN seed prend n'importe quelle valeur de la distribution, et seule la\n", + "moyenne sur dix seeds dit quelque chose de la procédure." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "gov-19", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.385733Z", + "iopub.status.busy": "2026-09-22T01:52:48.385388Z", + "iopub.status.idle": "2026-09-22T01:52:48.393309Z", + "shell.execute_reply": "2026-09-22T01:52:48.392524Z" + }, + "papermill": { + "duration": 0.0117, + "end_time": "2026-09-22T01:52:48.393912+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.382212+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sincere : vainqueur A, satisfaction 0.60\n", + "strategique : vainqueur B, satisfaction 0.70\n", + "fausse coalition B : vainqueur B, satisfaction 0.80\n" + ] + } + ], + "source": [ + "# MANIPULATION : vote strategique et fausse coalition sur le bloc discipliné\n", + "opts = g.SCENARIOS[\"strategic_bloc\"][\"options\"]\n", + "\n", + "base = g.simulate_vote(g.build_agents(\"strategic_bloc\"), opts, \"majority\", seed=0)\n", + "print(f\"sincere : vainqueur {base['winner']}, satisfaction {sum(base['satisfaction'])/len(base['satisfaction']):.2f}\")\n", + "\n", + "strat = g.apply_manipulation(g.build_agents(\"strategic_bloc\"), \"strategic\")\n", + "r = g.simulate_vote(strat, opts, \"majority\", seed=0)\n", + "sat = sum(r['satisfaction'])/len(r['satisfaction'])\n", + "print(f\"strategique : vainqueur {r['winner']}, satisfaction {sat:.2f}\")\n", + "\n", + "false = g.apply_manipulation(g.build_agents(\"strategic_bloc\"), \"false_coalition\", target=\"B\")\n", + "r = g.simulate_vote(false, opts, \"majority\", seed=0)\n", + "sat = sum(r['satisfaction'])/len(r['satisfaction'])\n", + "print(f\"fausse coalition B : vainqueur {r['winner']}, satisfaction {sat:.2f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "gov-20", + "metadata": { + "papermill": { + "duration": 0.002488, + "end_time": "2026-09-22T01:52:48.399840+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.397352+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture : la manipulation rend le collectif... plus satisfait ?\n", + "\n", + "Résultat contre-intuitif mesuré : sincère → A (satisfaction 0.60), vote\n", + "stratégique généralisé → B (0.70), fausse coalition pour B → B (0.80). Les deux\n", + "manipulations **augmentent** la satisfaction moyenne. L'explication est dans la\n", + "structure du scénario : le bloc A (3 agents) a B comme second choix, S1 a B\n", + "comme premier — B est le *compromis naturel* que le vote sincère ne trouve pas.\n", + "La manipulation, ici, pousse le collectif vers son compromis.\n", + "\n", + "Le **piège de la moyenne** : la satisfaction du bloc passe de 1.0 (A gagne) à\n", + "0.5 (B gagne) — trois agents y perdent, deux y gagnent, et la moyenne monte.\n", + "Une gouvernance qui ne regarderait que la satisfaction moyenne peut consacrer\n", + "la manipulation ; la distribution (et le Gini de `summarize`) dit la vérité que\n", + "la moyenne tait. La contre-vérification est laissée à la cellule de\n", + "l'exercice 2 : croiser `summarize()` sincère contre manipulé montre la justice\n", + "baisser pendant que la satisfaction monte — les deux instruments divergent\n", + "précisément quand un seul d'entre eux suffit à tromper." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "gov-21", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.406010Z", + "iopub.status.busy": "2026-09-22T01:52:48.405832Z", + "iopub.status.idle": "2026-09-22T01:52:48.410297Z", + "shell.execute_reply": "2026-09-22T01:52:48.409723Z" + }, + "papermill": { + "duration": 0.008611, + "end_time": "2026-09-22T01:52:48.410818+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.402207+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "coalition_1 : ['D1'] -> vote A (leader D1)\n", + "coalition_2 : ['O1', 'O2'] -> vote B (leader O1)\n", + "coalition_3 : ['O3'] -> vote B (leader O3)\n", + "coalition_4 : ['O4'] -> vote C (leader O4)\n", + "Shapley (payoff = B porte) : {'O1': 1.0, 'O2': 0.0}\n" + ] + } + ], + "source": [ + "# COALITIONS : la confiance agrège des votes épars\n", + "agents = g.build_agents(\"dictatorship\",\n", + " trust={\"O1\": {\"O2\": 0.9, \"O3\": 0.5}, \"O2\": {\"O1\": 0.9}, \"O3\": {}, \"O4\": {\"O3\": 0.85}, \"D1\": {}})\n", + "coalitions = g.form_coalitions(agents)\n", + "for c in coalitions:\n", + " print(f\"{c[0].coalition_id} : {[a.name for a in c]} -> vote {c[0].preferences[0]} (leader {c[0].name})\")\n", + "\n", + "# Valeur de Shapley de la coalition O1-O2 : payoff = 1 si O1 present (B porte par O1)\n", + "def payoff(noms):\n", + " return 1.0 if \"O1\" in noms else 0.0\n", + "vals = g.shapley_value([\"O1\", \"O2\"], payoff)\n", + "print(\"Shapley (payoff = B porte) :\", {k: round(v, 2) for k, v in vals.items()})" + ] + }, + { + "cell_type": "markdown", + "id": "gov-22", + "metadata": { + "papermill": { + "duration": 0.002981, + "end_time": "2026-09-22T01:52:48.417174+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.414193+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture : la coalition exige une confiance lisible dans un sens précis\n", + "\n", + "Deux coalitions se forment : O1↔O2 (confiance mutuelle 0.9) et... personne\n", + "d'autre. O4 fait pourtant confiance à O3 (0.85) — mais O3 ne truste personne :\n", + "la boucle du source parcourt les agents **en ordre de liste**, et O3 est\n", + "examiné *avant* O4 ; arrivé là, O3 ne propose à personne et reste seul. Quand\n", + "le tour d'O4 vient, O3 est déjà pris. Asymétrie mesurée de l'algorithme : la\n", + "coalition naît seulement si le **premier des deux, en ordre de liste**, truste\n", + "l'autre — une confiance unilatérale dans l'autre sens ne produit rien. Chaque\n", + "bloc vote ensuite la préférence de son *leader* (le premier rencontré), pas\n", + "celle de son membre le plus trusté.\n", + "\n", + "La valeur de Shapley pose la question du **mérite** dans la coalition : avec\n", + "un payoff qui ne vaut que si O1 participe, O1 est pivotale dans les deux ordres\n", + "d'arrivée possibles et capte 1.0, O2 capte 0.0 — la coopération ne récompense\n", + "pas la présence, elle récompense la **contribution marginale**. C'est\n", + "l'instrument que le source appliquait aux coalitions de sa simulation ; ici il\n", + "est isolé, calculable à la main sur deux agents, et l'exercice 2 peut le\n", + "prolonger." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "gov-23", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.423230Z", + "iopub.status.busy": "2026-09-22T01:52:48.423033Z", + "iopub.status.idle": "2026-09-22T01:52:48.426191Z", + "shell.execute_reply": "2026-09-22T01:52:48.425654Z" + }, + "papermill": { + "duration": 0.006969, + "end_time": "2026-09-22T01:52:48.426679+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.419710+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "def vrai_quadratique(agents, options, budget=9, intensite=None):\n", + " # Etape 1 : pour chaque agent, voix par option = floor(sqrt(budget * intensite(option)))\n", + " # Etape 2 : cumuler les voix par option sur tous les agents\n", + " # Etape 3 : rendre l'option au total max\n", + " # Indice : intensite = 1.0 premier choix, 0.5 deuxieme, 0.0 ensuite ;\n", + " # tout le budget sur le premier choix achete floor(sqrt(9)) = 3 voix.\n", + " print(\"Exercice 1 a completer\")\n", + " return None # TODO etudiant" + ] + }, + { + "cell_type": "markdown", + "id": "gov-24", + "metadata": { + "papermill": { + "duration": 0.002596, + "end_time": "2026-09-22T01:52:48.431884+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.429288+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## Limites mesurées\n", + "\n", + "Une distillation n'est fidèle que si elle dit ce qu'elle a corrigé en route :\n", + "chaque divergence ci-dessous a été **mesurée sur le source** avant d'être\n", + "écrite ici, pas supposée depuis une lecture de surface.\n", + "\n", + "Ce port est **honnête sur ce qu'il ne fait pas** — six divergences mesurées sur\n", + "le source, pas supposées :\n", + "\n", + "1. **La simulation du source n'appelait jamais ses méthodes de vote** :\n", + " `simulate_governance` assignait `method_fn = GOVERNANCE_METHODS[method]`\n", + " puis déroulait un tally de blocs sans jamais l'invoquer — les 7 méthodes y\n", + " étaient du code mort. L'organe corrige : `simulate_vote` applique la méthode.\n", + "2. **Le « vote quadratique » du source n'est pas quadratique** : aucun coût en\n", + " somme de carrés ; un agent flexible coupe juste son budget en deux. Porté\n", + " tel quel — l'exercice 1 fait implémenter le vrai.\n", + "3. **Les byzantins sont les n premiers agents** (tranche déterministe\n", + " `agents[:n]`), pas un tirage : mesuré, porté fidèlement.\n", + "4. **La voie mémoire du source référence `options` hors de portée** (NameError\n", + " latent dès que le contexte ne porte pas `\"options\"`) : non portée, la\n", + " satisfaction est recalculée depuis les préférences.\n", + "5. **La médiation à probabilités fixes** (0.8/0.5/0.7 codés en dur) n'est pas\n", + " portée : seul `detect_conflicts` l'est.\n", + "6. **Le repli Borda en cas de cycle** conserve le tie-break de `max()` (première\n", + " clé d'insertion) — démontré sur le cycle ci-dessus où Borda rend 3=3=3.\n", + "\n", + "Aussi : les méthodes aléatoires (byzantin, raft, personnalité `random`) sont\n", + "**seedées** — les sorties committées sont reproductibles à l'identique, mais un\n", + "autre seed peut rendre un autre vainqueur (l'exercice 3 le mesure)." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "gov-25", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.438016Z", + "iopub.status.busy": "2026-09-22T01:52:48.437736Z", + "iopub.status.idle": "2026-09-22T01:52:48.441342Z", + "shell.execute_reply": "2026-09-22T01:52:48.440728Z" + }, + "papermill": { + "duration": 0.007427, + "end_time": "2026-09-22T01:52:48.441841+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.434414+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice 2 a completer\n" + ] + } + ], + "source": [ + "ma_societe = {\n", + " \"options\": [],\n", + " \"agents\": [\n", + " # (\"nom\", \"personnalite\", [preferences completes])\n", + " ],\n", + "}\n", + "# Indice : spoiler_candidate est deja un cas majority != borda ; cherchez-en un\n", + "# AUTRE (4 options ou 7 agents), puis g.summarize(g.simulate_vote(...)) x 2.\n", + "print(\"Exercice 2 a completer\")" + ] + }, + { + "cell_type": "markdown", + "id": "gov-26", + "metadata": { + "papermill": { + "duration": 0.002596, + "end_time": "2026-09-22T01:52:48.447065+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.444469+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Exercice 1 — le VRAI vote quadratique\n", + "\n", + "Le source annonce un coût quadratique mais ne l'implémente pas (limite 2).\n", + "Implémentez-le pour de vrai : chaque agent dispose d'un budget de crédits, le\n", + "**coût** payé pour acheter `v` voix sur une option est `v²` (le budget\n", + "s'épuise en carrés), et l'allocation optimale d'un agent qui aime une option\n", + "d'intensité `i` est d'y mettre `floor(sqrt(budget * i))` voix. Faites voter le\n", + "scénario `project_funding` : le vainqueur change-t-il par rapport au port\n", + "« budget coupé en deux » de l'organe ?" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "gov-27", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T01:52:48.454387Z", + "iopub.status.busy": "2026-09-22T01:52:48.454129Z", + "iopub.status.idle": "2026-09-22T01:52:48.457823Z", + "shell.execute_reply": "2026-09-22T01:52:48.457280Z" + }, + "papermill": { + "duration": 0.007864, + "end_time": "2026-09-22T01:52:48.458438+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.450574+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice 3 a completer\n" + ] + } + ], + "source": [ + "from collections import Counter\n", + "gagnants = Counter()\n", + "# Etape 1 : boucle sur 10 seeds -> simulate_vote(..., \"raft\", seed=s) sur cyclic_majority\n", + "# Etape 2 : gagnants[winner] += 1, print(gagnants)\n", + "# Etape 3 : temoin negatif sur strategic_bloc (meme distribution, meme gagnant partout)\n", + "# Indice : g.stability([...]) rend 1 si un seul vainqueur dans la liste, 0 sinon.\n", + "resultat_attendu = None # TODO etudiant\n", + "print(\"Exercice 3 a completer\")" + ] + }, + { + "cell_type": "markdown", + "id": "gov-28", + "metadata": { + "papermill": { + "duration": 0.002529, + "end_time": "2026-09-22T01:52:48.463619+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.461090+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Exercice 2 — construire la société où majorité et Borda se contredisent\n", + "\n", + "Construisez un scénario (dans le format des tuples de `SCENARIOS`) où la\n", + "majorité à un tour et Borda **ne désignent pas le même vainqueur**, puis\n", + "vérifiez ce que dit Condorcet. Mesurez `summarize()` pour les deux méthodes :\n", + "la plus juste (justice Gini) est-elle celle qui satisfait le plus d'agents ?" + ] + }, + { + "cell_type": "markdown", + "id": "gov-29", + "metadata": { + "papermill": { + "duration": 0.002525, + "end_time": "2026-09-22T01:52:48.468711+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.466186+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Exercice 3 — stabilité multi-seed et témoins négatifs\n", + "\n", + "(a) `raft` tire son leader au sort : mesurez sa **stabilité** sur le cycle de\n", + "Condorcet — 10 seeds, comptez la distribution des vainqueurs. (b) Témoin\n", + "négatif : sur `strategic_bloc` (un bloc de 3 sur 5), toutes les méthodes et\n", + "tous les seeds doivent rendre le même vainqueur — vérifiez qu'aucun seed ne\n", + "déraille." + ] + }, + { + "cell_type": "markdown", + "id": "gov-30", + "metadata": { + "papermill": { + "duration": 0.002458, + "end_time": "2026-09-22T01:52:48.473672+00:00", + "exception": false, + "start_time": "2026-09-22T01:52:48.471214+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## Conclusion\n", + "\n", + "Ce notebook a fait mesurer, sur des préférences identiques, ce que le théorème\n", + "d'Arrow (1951) énonce en général : **aucune procédure de agrégation de trois\n", + "options ou plus ne peut être simultanément** non-dictatoriale, unanime et\n", + "indépendante aux alternatives non pertinentes. Le spoiler et le cycle sont ses\n", + "deux manifestations mesurées ici — pas des accidents de scénario, des\n", + "propriétés structurelles du choix collectif.\n", + "\n", + "- La **même société de préférences** rend des vainqueurs différents selon la\n", + " méthode : il n'existe pas de procédure de vote neutre (flèche du théorème\n", + " d'Arrow, dont le cycle et le spoiler ci-dessus sont les deux briques\n", + " empiriques).\n", + "- Les **métriques** (consensus, justice de Gini, satisfaction) rendent ces\n", + " divergences mesurables au lieu de les débattre.\n", + "- La gouvernance se teste comme un système : **bruit byzantin, manipulation,\n", + " stabilité multi-seed** — exactement les axes que le banc de scénarios du\n", + " source étiquetait.\n", + "\n", + "**Arc** : Agentic-3 (orchestrer le travail) → ce notebook (décider à\n", + "plusieurs) → Agentic-4 (capstone). L'organe `governance_methods.py` et ses 16\n", + "tests purs sont réutilisables pour vos propres scénarios : construire une\n", + "société, la faire voter sous les sept méthodes, et lire ce que chacune\n", + "sacrifie — c'est tout l'appareil qu'il faut pour cela." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (System)", + "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.14" + }, + "papermill": { + "default_parameters": {}, + "duration": 2.154401, + "end_time": "2026-09-22T01:52:48.711546+00:00", + "environment_variables": {}, + "exception": null, + "input_path": "Argument_Analysis_Gouvernance_Multi_Agents.ipynb", + "output_path": "Argument_Analysis_Gouvernance_Multi_Agents.ipynb", + "parameters": {}, + "start_time": "2026-09-22T01:52:46.557145+00:00", + "version": "2.7.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md index d91baa0942..d68716cd5f 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md @@ -80,6 +80,7 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | Schemes | [Schemes_Walton](Argument_Analysis_Schemes_Walton.ipynb) | Les 10 schémas d'argumentation de Walton (table verbatim du moteur étudiant EPITA, distillation Triple Distillation) et leur classifieur lexical déterministe : paire de mots-clés accentués exigée, ordre canonique qui départage (`modus_ponens` en dernier, « donc » armant aussi `cause_effect`), questions critiques canoniques, échec bruyant (`None` honnête, jamais d'étiquette fabriquée) — pur stdlib | Argumentation informelle schématique | | Dial | [Dialogues_Protocolises](Argument_Analysis_Dialogues_Protocolises.ipynb) | Protocoles de dialogue Walton–Krabbe (inquiry/persuasion) comme machines à états sur 9 actes de parole : tables de transitions, terminaison par condition testable (compréhension mutuelle, capitulation, boucle), 5 divergences source documentées dont une condition de terminaison inatteignable (`_term_double_retract`) — pur stdlib Python | Protocoles d'échange / pragmatique | | KB | [Knowledge_Base](Argument_Analysis_Knowledge_Base.ipynb) | La mémoire d'un débat (distillation Triple Distillation du moteur étudiant EPITA) : population transitive (`add_argument` porte prémisses et conclusion), support/attaque par convention lexicale `¬`, cohérence = conflit ouvert P/`¬P`, `entails` = appartenance documentée (pas d'inférence — non-explosion mesurée, `¬¬P` distinct de `P`, écrasement par contenu) — pur stdlib Python | Mémoire de débat | +| Gouv | [Gouvernance_Multi_Agents](Argument_Analysis_Gouvernance_Multi_Agents.ipynb) | Gouvernance multi-agents (distillation EPITA 2.1.6, sous-grain 2 #4960) : organe pur `governance_methods.py` — 7 méthodes de vote/consensus (majorité, Borda, Condorcet à repli, quadratique, byzantin, Raft) sur 6 scénarios étiquetés, métriques consensus/justice Gini/satisfaction, **spoiler et cycle de Condorcet mesurés**, robustesse byzantine (courbe non monotone expliquée), manipulation et coalitions — pur stdlib Python, 6 divergences source documentées | Choix collectif | | Rank | [Ranking_Semantics](Argument_Analysis_Ranking_Semantics.ipynb) | Sémantiques de classement (h-Categoriser, fardeau) en pur Python : force numérique départageant des arguments de même statut Dung | Argumentation graduée | | Orchestration | [Orchestration_Modes](Argument_Analysis_Orchestration_Modes.ipynb) | Comparer 7 modes d'orchestration par le budget (distillation Triple Distillation de l'instrument EPITA #1735) : 3 registres d'arrêt (filet du harnais / déclaration du mode / réalité des phases), asymétrie d'axes largeur-délégation-dialogue, budget calibré dérivé de la mesure — mesures committées, pur stdlib | Arbitrage d'architectures multi-agents | | Dated | [Dated_Graphs](Argument_Analysis_Dated_Graphs.ipynb) | Instrument $G_t^{arg} \to G_{t+1}^{arg}$ (Epic #13303, issue #13310) : corpus daté → graphe AIF conforme (critère d'inclusion C1–C3 écrit, exclusions publiées) → projection Dung ; deux mesures d'écart de familles différentes (Jaccard structurelle nœuds/attaques + Jaccard sémantique sur extensions grounded), **contrôle négatif** (plancher de bruit par split de la même période) publié à côté de tout écart, **contrôle positif** à magnitude attendue écrite avant mesure (attaque de racine : sortie directe + réhabilitation paradoxale de la victime + cascade) — pur Python + rdflib, validation synthétique uniquement ; **hypothèse monotone** posée, vérifiée par inclusion et violée délibérément (retrait d'un déchu vs d'un accepté : ce que voient les deux mesures) | Argumentation temporelle / mesure | @@ -116,6 +117,7 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | **Schemes_Walton** | Reconnaître la forme stéréotypée d'un argument (autorité, analogie, cause à effet...) par un matcher lexical déterministe, prédire son départage par l'ordre canonique, et auditer un texte classé avec les questions critiques de Walton — pur stdlib Python | 30 min | | **Dialogues_Protocolises** | Lire deux protocoles de dialogue Walton–Krabbe comme des machines à états : neuf actes de parole, tables de transitions inquiry vs persuasion, terminaison par condition testable (compréhension mutuelle, capitulation, boucle), et une condition de terminaison inatteignable découverte dans le source — pur stdlib Python | 35 min | | **Knowledge_Base** | Construire et interroger la mémoire d'un débat : population transitive (un argument porte ses prémisses et sa conclusion), requêtes support/attaque par convention lexicale `¬`, détection du conflit ouvert, et les limites mesurées (pas d'inférence depuis une contradiction, `¬¬P` distinct de `P`, écrasement des propositions homonymes) — pur stdlib Python | 30 min | +| **Gouvernance_Multi_Agents** | Comparer sept méthodes de vote sur les mêmes préférences (effet spoiler, cycle de Condorcet, repli Borda), mesurer justice de Gini et satisfaction, puis tester la résistance de la décision : bruit byzantin, manipulation stratégique, stabilité multi-seed — pur stdlib Python | 35 min | | **Ranking_Semantics** | Calculer la *force* numérique d'un argument (h-Categoriser par point fixe, fardeau par comparaison lexicographique) et départager des arguments que Dung déclare indistinctement rejetés — pur stdlib Python | 35 min | | **Orchestration_Modes** | Reconstruire les tableaux sous-budget/calibré d'un comparatif de 7 modes d'orchestration, nommer les 3 registres d'arrêt, et dériver un budget calibré — avec la limite mesurée de la projection linéaire (1081 s naïfs vs 600 s réels) | 30 min | | **Dated_Graphs** | Construire l'instrument de comparaison temporelle des graphes d'argumentation : critère d'inclusion écrit d'un énoncé, graphe AIF conforme sérialisé en RDF, projection Dung, deux mesures d'écart de familles différentes, plancher de bruit (contrôle négatif) publié avec chaque mesure, et contrôle positif dont la magnitude attendue est dérivée à la main avant l'exécution | 45 min | @@ -430,7 +432,7 @@ Le titre annonce l'analyse d'arguments. Mais le geste que cette série enseigne - [Semantic Kernel Docs](https://learn.microsoft.com/en-us/semantic-kernel/) - [TweetyProject](https://tweetyproject.org/) -## Ordre partiel et prérequis — mapping exhaustif (33/33, version consolidée) +## Ordre partiel et prérequis — mapping exhaustif (34/34, version consolidée) Cette section pose un **ordre partiel** sur l'ensemble des notebooks du répertoire, fondé sur les déclarations de prérequis et les chaînes de @@ -517,6 +519,7 @@ en référence. | `Argument_Analysis_Schemes_Walton.ipynb` | Schémas d'argumentation de Walton (10 schémas stéréotypés, questions critiques) et classifieur lexical déterministe | aucun (indépendant ; niveau intermédiaire entre `Toulmin_Model` et `Dung_AF_Semantics`, cités en contexte) | | `Argument_Analysis_Dialogues_Protocolises.ipynb` | Protocoles Walton–Krabbe : inquiry/persuasion comme machines à états sur actes de parole | aucun (indépendant ; voisin de Toulmin_Model par la structure de l'échange, des Agentic par la multi-agentique) | | `Argument_Analysis_Knowledge_Base.ipynb` | Mémoire d'un débat : population transitive des propositions, requêtes support/attaque par négation lexicale `¬` | aucun (indépendant ; complète `Toulmin_Model`, `Schemes_Walton` et `Dialogues_Protocolises`, cités en contexte) | +| `Argument_Analysis_Gouvernance_Multi_Agents.ipynb` | Gouvernance multi-agents (distillation EPITA 2.1.6, #4960) — vote/consensus/coalitions/manipulation | aucun (indépendant ; voisin d'`Agentic-3-orchestration` par la question — décider à plusieurs vs orchestrer le travail — pas par le code) | | `Argument_Analysis_Ranking_Semantics.ipynb` | Sémantiques graduées (h-Categoriser, fardeau) | `Dung_AF_Semantics` | | `Argument_Analysis_Dated_Graphs.ipynb` | Instrument $G_t^{arg} \to G_{t+1}^{arg}$ (Epic #13303, issue #13310) | `Dung_AF_Semantics` | | `Argument_Analysis_Observatoire-1-Initiation.ipynb` | Cas 1 de l'Observatoire (Epic #13303, livré #16431) : étude empirique DiD imparfait consommant l'instrument | `Dated_Graphs` | diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py new file mode 100644 index 0000000000..124f503295 --- /dev/null +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py @@ -0,0 +1,448 @@ +# -*- coding: utf-8 -*- +"""Mécanismes de gouvernance multi-agents — distillation du sous-projet EPITA 2.1.6. + +Provenance : `2.1.6_multiagent_governance_prototype` du dépôt étudiant +`jsboigeEpita/2025-Epita-Intelligence-Symbolique` (~1390 lignes Python + 16 +scénarios JSON). Mandat Triple Distillation (EPIC #4960) : porter l'essence +vivante « sans le bruit d'une année de régressions et d'itérations ». + +Partie vivante retenue : +- `governance/methods.py` : 7 méthodes de vote/consensus (majorité, pluralité, + Borda, Condorcet avec repli Borda, vote quadratique, consensus byzantin, + consensus Raft). +- `agents/base_agent.py` : personnalités (stubborn / flexible / strategic / + random) + préférences ordonnées + confiance inter-agents. +- `governance/simulation.py` : formation de coalitions par confiance > 0.8, + vote par blocs, détection de conflits. +- `metrics/metrics.py` : taux de consensus, Gini/justice, satisfaction moyenne, + stabilité. +- `scénarios/*.json` : banc de 16 scénarios étiquetés — 6 distillés ici. + +Partie NON portée (bruit, cf. divergences) : la médiation de +`conflict_resolution.py` (probabilités de succès codées en dur 0.8/0.5/0.7), +le Q-learning epsilon-greedy de `base_agent`, le CLI/runner, la visualisation +matplotlib, les couches BDI/Reactive (stubs de protocole vides). + +Divergences documentées (mesurées sur le source, pas supposition) : +1. `simulate_governance` du source assigne `method_fn = GOVERNANCE_METHODS[method]` + puis ne l'appelle JAMAIS : les 7 méthodes sont du code mort dans la boucle + de simulation (le gagnant sort d'un tally de blocs par préférence du leader). + L'organe les branche réellement : `simulate_vote` applique la méthode. +2. `quadratic_voting` du source n'implémente PAS le coût quadratique annoncé + (aucune somme de carrés) : un agent "flexible" coupe juste son budget en + budget//2 + reste, un autre met tout sur son premier choix. Porté tel quel, + avec l'exercice 1 du notebook = implémenter le vrai vote quadratique + (coût = somme des carrés des voix, allocation optimale = racine carrée). +3. `byzantine_consensus` du source désigne les byzantins comme les n PREMIERS + agents (`agents[:n]`, tranche déterministe), pas un tirage. Porté fidèle. +4. La voie `update_memory` du `base_agent` source référence `options` hors de + sa portée (NameError latent dès que le contexte ne porte pas "options") ; + la satisfaction est recalculée ici proprement à partir des préférences. +5. La médiation à probabilités fixes n'est pas portée (pseudo-mécanisme) ; + seule `detect_conflicts` (paires d'agents à positions différentes) l'est. +6. `condorcet_method` conserve le repli Borda du source (comportement standard + en l'absence de vainqueur de Condorcet — le notebook le démontre sur le + scénario du cycle). + +Module pur : stdlib uniquement, aucune dépendance (numpy du source remplacé par +`random.Random` injecté, donc seedable et déterministe). +""" + +from __future__ import annotations + +import random +from collections import Counter +from itertools import permutations +from math import factorial + +# --------------------------------------------------------------------------- +# Agent a personnalité +# --------------------------------------------------------------------------- + + +class GovernanceAgent: + """Agent de vote avec personnalité, préférences ordonnées et confiance. + + Port simplifié du `Agent` de `agents/base_agent.py` : la personnalité + gouverne le vote sincère, la confiance gouverne les coalitions. + """ + + def __init__(self, name, personality, préférences, trust=None): + self.name = name + self.personality = personality # stubborn | flexible | strategic | random + self.preferences = list(préférences) + self.trust = dict(trust or {}) + self.rng = random.Random(0) # reseedable depuis l'extérieur + + def decide(self, options, context=None, rng=None): + """Vote sincère selon la personnalité (port de Agent.decide, sans Q-learning).""" + ctx = context or {} + rng = rng or self.rng + if ctx.get("coalition_leader") and self.coalition_id: + return ctx["coalition_leader"] + if self.personality == "stubborn": + return self.preferences[0] + if self.personality == "flexible": + hint = ctx.get("majority_hint") + if hint in options: + return hint + return self.preferences[0] + if self.personality == "strategic": + likely = ctx.get("likely_winner") + if likely in self.preferences and self.trust and max(self.trust.values()) > 0.7: + return likely + return self.preferences[1] if len(self.preferences) > 1 else self.preferences[0] + return rng.choice(sorted(options)) + + coalition_id = None # posé par form_coalitions + + +# --------------------------------------------------------------------------- +# Methodes de vote (port de governance/methods.py) +# --------------------------------------------------------------------------- + + +def majority_voting(agents, options, context=None, rng=None): + """Majorité : chaque agent vote son premier choix, le plus voté gagne.""" + votes = [a.decide(options, context, rng) for a in agents] + winner, _ = Counter(votes).most_common(1)[0] + return winner + + +def plurality_voting(agents, options, context=None, rng=None): + """Pluralité : identique à la majorité pour un vainqueur unique (port fidèle).""" + return majority_voting(agents, options, context, rng) + + +def borda_count(agents, options, context=None, rng=None): + """Borda : n-1 points au premier rang, n-2 au deuxième... le total max gagne.""" + scores = {o: 0 for o in options} + n = len(options) + for a in agents: + for i, o in enumerate(a.preferences): + if o in scores: + scores[o] += n - i - 1 + return max(scores, key=scores.get) + + +def condorcet_method(agents, options, context=None, rng=None): + """Condorcet : duels pairwise ; vainqueur qui bat tous les autres, sinon repli Borda.""" + n = len(options) + pairwise_wins = {o: 0 for o in options} + for o1 in options: + for o2 in options: + if o1 == o2: + continue + o1_wins = sum( + a.preferences.index(o1) < a.preferences.index(o2) for a in agents + ) + if o1_wins > len(agents) - o1_wins: + pairwise_wins[o1] += 1 + for o in options: + if pairwise_wins[o] == n - 1: + return o + return borda_count(agents, options, context, rng) + + +def quadratic_voting(agents, options, context=None, rng=None): + """Vote "quadratique" du SOURCE (divergence 2) : budget coupé ou tout-misé. + + Le source annonce un coût en somme de carrés mais ne l'implémente pas : + un agent flexible coupe son budget en deux sur ses deux premiers choix, + les autres mettent tout sur leur premier choix. L'exercice 1 du notebook + fait implémenter le vrai vote quadratique. + """ + budget = (context or {}).get("quadratic_budget", 9) + votes = {o: 0 for o in options} + for a in agents: + if a.personality == "flexible" and len(options) > 1: + votes[options[0]] += budget // 2 + votes[options[1]] += budget - (budget // 2) + else: + votes[a.preferences[0]] += budget + return max(votes, key=votes.get) + + +def byzantine_consensus(agents, options, context=None, rng=None): + """Consensus byzantin : une fraction vote aléatoire (les n PREMIERS agents — divergence 3).""" + rng = rng or random.Random(0) + ratio = (context or {}).get("byzantine_ratio", 0.2) + n_byz = int(len(agents) * ratio) + votes = [a.decide(options, context, rng) for a in agents[n_byz:]] + votes += [rng.choice(sorted(options)) for _ in agents[:n_byz]] + winner, _ = Counter(votes).most_common(1)[0] + return winner + + +def raft_consensus(agents, options, context=None, rng=None): + """Raft : un leader tiré au sort propose, majorité d'acceptations requise (top-2).""" + rng = rng or random.Random(0) + leader = rng.choice(sorted(agents, key=lambda a: a.name)) + proposal = leader.decide(options, context, rng) + acceptances = 1 + for a in agents: + if a is not leader and proposal in a.preferences[:2]: + acceptances += 1 + if acceptances > len(agents) // 2: + return proposal + return majority_voting(agents, options, context, rng) + + +GOVERNANCE_METHODS = { + "majority": majority_voting, + "plurality": plurality_voting, + "borda": borda_count, + "condorcet": condorcet_method, + "quadratic": quadratic_voting, + "byzantine": byzantine_consensus, + "raft": raft_consensus, +} + + +# --------------------------------------------------------------------------- +# Coalitions, conflits, manipulation (port de governance/simulation.py) +# --------------------------------------------------------------------------- + + +def form_coalitions(agents, trust_threshold=0.8): + """Coalitions par confiance : un agent rejoint le premier partenaire à confiance > seuil. + + Port de la boucle de coalition de `simulate_governance` (le source fait + confiance > 0.8 en dur) : chaque coalition vote la préférence de son leader. + """ + unassigned = list(agents) + coalitions = [] + for a in agents: + a.coalition_id = None + i = 0 + while i < len(unassigned): + agent = unassigned[i] + partners = [p for p in unassigned[i + 1:] if agent.trust.get(p.name, 0) > trust_threshold] + coalition = [agent] + partners + cid = f"coalition_{len(coalitions) + 1}" + for p in coalition: + p.coalition_id = cid + coalitions.append(coalition) + for p in partners: + unassigned.remove(p) + i += 1 + return coalitions + + +def detect_conflicts(positions): + """Conflits : chaque paire d'agents à positions différentes (port fidèle).""" + names = list(positions.keys()) + conflicts = [] + for i in range(len(names)): + for j in range(i + 1, len(names)): + if positions[names[i]] != positions[names[j]]: + conflicts.append({"agents": [names[i], names[j]]}) + return conflicts + + +def apply_manipulation(agents, manipulation_type, target=None, rng=None): + """Manipulation du vote (port de simulate_manipulation, voie decide rebind). + + 'strategic' : chaque agent vote son 2e choix. + 'false_coalition' : la moitié des agents vote la cible. + 'bribery' : le retour est une copie où n agents votent la cible (n = budget). + """ + import copy + + rng = rng or random.Random(0) + copies = [copy.deepcopy(a) for a in agents] + if manipulation_type == "strategic": + for a in copies: + if len(a.preferences) > 1: + a.personality = "stubborn" + a.preferences = [a.preferences[1], a.preferences[0]] + a.preferences[2:] + elif manipulation_type == "false_coalition": + t = target or a.preferences[0] + for a in copies[: len(copies) // 2]: + a.personality = "stubborn" + a.preferences = [t] + [p for p in a.preferences if p != t] + return copies + + +def shapley_value(member_names, payoff_func): + """Valeur de Shapley d'une coalition (port fidèle, permutations factorielles).""" + n = len(member_names) + if n == 0: + return {} + values = {m: 0.0 for m in member_names} + for perm in permutations(member_names): + prev = set() + for m in perm: + marginal = payoff_func(prev | {m}) - payoff_func(prev) + values[m] += marginal / factorial(n) + prev.add(m) + return values + + +# --------------------------------------------------------------------------- +# Métriques (port de metrics/metrics.py, numpy retiré) +# --------------------------------------------------------------------------- + + +def satisfaction_of(agent, winner): + """Satisfaction d'un agent : 1 - rang du vainqueur dans ses préférences, normalisé.""" + if winner not in agent.preferences: + return 0.0 + denom = max(1, len(agent.preferences) - 1) + return 1.0 - agent.preferences.index(winner) / denom + + +def consensus_rate(result): + """Fraction des votes alignés sur le vainqueur.""" + votes, winner = result["votes"], result["winner"] + return votes.count(winner) / len(votes) if votes else 0.0 + + +def gini(values): + """Coefficient de Gini (formule du source, en Python pur, epsilon inclus).""" + vals = sorted(float(v) for v in values) + n = len(vals) + if n == 0: + return 0.0 + vals = [v - min(0.0, vals[0]) + 1e-8 for v in vals] + total = sum(vals) + if total <= 0: + return 0.0 + weighted = sum((2 * i - n - 1) * v for i, v in enumerate(vals, start=1)) + return weighted / (n * total) + + +def fairness_index(result): + """Justice = 1 - Gini des satisfactions.""" + return 1.0 - gini(result["satisfaction"]) + + +def mean_satisfaction(result): + """Satisfaction moyenne du collectif.""" + return sum(result["satisfaction"]) / len(result["satisfaction"]) if result["satisfaction"] else 0.0 + + +def stability(winners): + """Stabilite : 1 si un seul vainqueur sur la serie, 0 sinon (port fidele).""" + return 1.0 if len(set(winners)) == 1 else 0.0 + + +# --------------------------------------------------------------------------- +# Simulation (corrigée : la méthode est APPELÉE — divergence 1) +# --------------------------------------------------------------------------- + + +def simulate_vote(agents, options, method, context=None, seed=0): + """Applique RÉELLEMENT la méthode de vote, puis mesure votes et satisfactions. + + Divergence 1 : le source n'appelait jamais sa méthode ; ici chaque méthode + de GOVERNANCE_METHODS tourne sur les memes agents. + """ + rng = random.Random(seed) + method_fn = GOVERNANCE_METHODS[method] + winner = method_fn(agents, options, context, rng) + votes = [a.decide(options, context, rng) for a in agents] + satisfaction = [satisfaction_of(a, winner) for a in agents] + positions = {a.name: v for a, v in zip(agents, votes)} + return { + "method": method, + "winner": winner, + "votes": votes, + "satisfaction": satisfaction, + "agent_names": [a.name for a in agents], + "conflicts": detect_conflicts(positions), + } + + +def summarize(result): + """Résume une simulation : consensus, justice, satisfaction.""" + return { + "consensus_rate": round(consensus_rate(result), 3), + "fairness": round(fairness_index(result), 3), + "satisfaction": round(mean_satisfaction(result), 3), + } + + +# --------------------------------------------------------------------------- +# Banc de scénarios (6 des 16 du source, distillés) +# --------------------------------------------------------------------------- + +SCENARIOS = { + "dictatorship": { + "description": "Un agent stratégique impose sa volonté face à des préférences diverses.", + "options": ["A", "B", "C"], + "agents": [ + ("D1", "strategic", ["A", "B", "C"]), + ("O1", "stubborn", ["B", "C", "A"]), + ("O2", "flexible", ["C", "A", "B"]), + ("O3", "random", ["B", "A", "C"]), + ("O4", "stubborn", ["C", "B", "A"]), + ], + }, + "cyclic_majority": { + "description": "Cycle de Condorcet : A bat B, B bat C, C bat A — aucun vainqueur net.", + "options": ["A", "B", "C"], + "agents": [ + ("V1", "stubborn", ["A", "B", "C"]), + ("V2", "stubborn", ["B", "C", "A"]), + ("V3", "stubborn", ["C", "A", "B"]), + ], + }, + "spoiler_candidate": { + "description": "Un candidat similaire divise un bloc : l'adversaire l'emporte.", + "options": ["Gauche", "Centre", "Droite"], + "agents": [ + ("G1", "stubborn", ["Gauche", "Centre", "Droite"]), + ("G2", "stubborn", ["Centre", "Gauche", "Droite"]), + ("D1", "stubborn", ["Droite", "Centre", "Gauche"]), + ], + }, + "strategic_bloc": { + "description": "Un bloc discipliné affronte des votes dispersés.", + "options": ["A", "B", "C"], + "agents": [ + ("B1", "stubborn", ["A", "B", "C"]), + ("B2", "stubborn", ["A", "C", "B"]), + ("B3", "stubborn", ["A", "B", "C"]), + ("S1", "stubborn", ["B", "C", "A"]), + ("S2", "stubborn", ["C", "B", "A"]), + ], + }, + "byzantine_noise": { + "description": "Une fraction d'agents vote au hasard : robustesse du consensus.", + "options": ["A", "B", "C"], + "agents": [ + ("H1", "stubborn", ["A", "B", "C"]), + ("H2", "stubborn", ["A", "C", "B"]), + ("H3", "stubborn", ["A", "B", "C"]), + ("Z1", "random", ["B", "A", "C"]), + ("Z2", "random", ["C", "B", "A"]), + ], + }, + "project_funding": { + "description": "Choix de budget : préférences ordonnées sur trois projets.", + "options": ["Route", "Ecole", "Clinique"], + "agents": [ + ("M1", "stubborn", ["Route", "Ecole", "Clinique"]), + ("M2", "stubborn", ["Ecole", "Clinique", "Route"]), + ("M3", "flexible", ["Clinique", "Ecole", "Route"]), + ("M4", "flexible", ["Ecole", "Route", "Clinique"]), + ], + }, +} + + +def build_agents(scenario_name, trust=None): + """Matérialise les agents d'un scénario (tuples -> GovernanceAgent).""" + sc = SCENARIOS[scenario_name] + return [GovernanceAgent(n, p, prefs, (trust or {}).get(n)) for n, p, prefs in sc["agents"]] + + +def method_counts(): + """Inventaire de l'organe (pour tests purs, sans exécution).""" + return { + "methods": len(GOVERNANCE_METHODS), + "method_keys": sorted(GOVERNANCE_METHODS.keys()), + "scenarios": len(SCENARIOS), + "scenario_names": sorted(SCENARIOS.keys()), + } diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/tests/test_governance_methods.py b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/tests/test_governance_methods.py new file mode 100644 index 0000000000..8aeaf20154 --- /dev/null +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/tests/test_governance_methods.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +"""Tests de structure de governance_methods.py — purs, sans execution de vote aleatoire. + +Fige les comptes distilles (7 methodes, 6 scenarios) et les proprietes +structurelles du port. La validation executee vit dans le notebook committé +(avec outputs), pas ici. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import governance_methods as g + + +class TestComptesMesures(unittest.TestCase): + """Fige l'inventaire de la distillation.""" + + def test_sept_methodes(self): + self.assertEqual(g.method_counts()["methods"], 7) + + def test_cles_methodes(self): + self.assertEqual( + g.method_counts()["method_keys"], + ["borda", "byzantine", "condorcet", "majority", "plurality", "quadratic", "raft"], + ) + + def test_six_scenarios(self): + self.assertEqual(g.method_counts()["scenarios"], 6) + self.assertEqual( + g.method_counts()["scenario_names"], + ["byzantine_noise", "cyclic_majority", "dictatorship", + "project_funding", "spoiler_candidate", "strategic_bloc"], + ) + + +class TestStructure(unittest.TestCase): + """Proprietes structurelles du port.""" + + def test_chaque_scenario_trois_champs(self): + for name, sc in g.SCENARIOS.items(): + self.assertIn("description", sc, name) + self.assertIn("options", sc, name) + self.assertIn("agents", sc, name) + + def test_preferences_dans_options(self): + for name, sc in g.SCENARIOS.items(): + for _, _, prefs in sc["agents"]: + for p in prefs: + self.assertIn(p, sc["options"], f"{name}: {p}") + + def test_personnalites_connues(self): + for name, sc in g.SCENARIOS.items(): + for _, perso, _ in sc["agents"]: + self.assertIn(perso, {"stubborn", "flexible", "strategic", "random"}, name) + + def test_plurality_meme_resultat_que_majority(self): + # Port fidele : le source definit plurality comme simple rappel de majority. + agents = g.build_agents("dictatorship") + opts = g.SCENARIOS["dictatorship"]["options"] + self.assertEqual( + g.plurality_voting(agents, opts, None, None), + g.majority_voting(agents, opts, None, None), + ) + + def test_determinisme_seed(self): + agents = g.build_agents("byzantine_noise") + opts = g.SCENARIOS["byzantine_noise"]["options"] + w1 = g.simulate_vote(agents, opts, "byzantine", seed=42)["winner"] + agents = g.build_agents("byzantine_noise") + w2 = g.simulate_vote(agents, opts, "byzantine", seed=42)["winner"] + self.assertEqual(w1, w2) + + +class TestVotesDeterministes(unittest.TestCase): + """Votes sans aleatoire (personnalites non random) : resultats calculables a la main.""" + + def test_majorite_bloc_strategique(self): + agents = g.build_agents("strategic_bloc") + w = g.majority_voting(agents, ["A", "B", "C"]) + self.assertEqual(w, "A") # 3 votes A contre 1 B, 1 C + + def test_borda_spoiler_elit_centre(self): + # G1[A,B,C] G2[C,G,D]... Borda cumule les seconds rangs : Centre gagne. + agents = g.build_agents("spoiler_candidate") + w = g.borda_count(agents, g.SCENARIOS["spoiler_candidate"]["options"]) + self.assertEqual(w, "Centre") + + def test_majorite_spoiler_elit_gauche(self): + agents = g.build_agents("spoiler_candidate") + w = g.majority_voting(agents, g.SCENARIOS["spoiler_candidate"]["options"]) + self.assertEqual(w, "Gauche") + + def test_condorcet_cycle_repli_borda(self): + # Cycle A>B>C>A : aucun vainqueur de Condorcet, repli Borda (egalite A=B=C=3). + agents = g.build_agents("cyclic_majority") + opts = g.SCENARIOS["cyclic_majority"]["options"] + w = g.condorcet_method(agents, opts) + # Borda rend 3=3=3, max() prend la premiere cle (A) — documenté divergence 6. + self.assertEqual(w, "A") + + def test_gini_borne(self): + self.assertEqual(g.gini([1, 1, 1, 1]), 0.0) + # Un seul detenteur parmi n : Gini tend vers (n-1)/n, ici 0.75. + self.assertAlmostEqual(g.gini([0, 0, 0, 10]), 0.75, delta=1e-6) + + def test_shapley_symetrie(self): + # Deux agents interchangeables => valeurs de Shapley egales. + vals = g.shapley_value(["X", "Y"], lambda s: 1.0 if len(s) >= 1 else 0.0) + self.assertAlmostEqual(vals["X"], vals["Y"]) + + def test_detect_conflicts_paires(self): + c = g.detect_conflicts({"a": "A", "b": "B", "c": "A"}) + self.assertEqual(len(c), 2) # (a,b) et (b,c) + + def test_manipulation_strategique_change_les_votes(self): + agents = g.build_agents("spoiler_candidate") + copies = g.apply_manipulation(agents, "strategic") + votes_avant = [a.decide(["Gauche", "Centre", "Droite"]) for a in agents] + votes_apres = [a.decide(["Gauche", "Centre", "Droite"]) for a in copies] + self.assertNotEqual(votes_avant, votes_apres) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 5f326b441d0a276fec9ed881c0484b41a9011903 Mon Sep 17 00:00:00 2001 From: jsboige Date: Tue, 22 Sep 2026 12:12:34 +0200 Subject: [PATCH 2/8] =?UTF-8?q?Exercices:=20gouvernance=20=E2=80=94=20enon?= =?UTF-8?q?ce=20immediatement=20suivi=20de=20son=20stub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les trois cellules d'exercice etaient desynchronisees de leurs enonces : stub 1, « Limites mesurees », stub 2, enonce 1, stub 3, enonce 2, enonce 3. Un etudiant rencontrait la cellule a completer de l'exercice 2 avant tout enonce. L'ordre suit desormais enonce|stub pour les trois — celui des huit carnets jumeaux de la serie et du jumeau cite en review. Le marqueur de l'exercice 1 passe du corps de la fonction au niveau module, comme ses deux voisins du meme carnet : une definition seule n'emet rien, donc la cellule ne pouvait porter aucun « Exercice 1 a completer ». Aucune ligne de contenu n'est perdue : le marqueur est deplace, pas retire, et les commentaires # Etape / # Indice / # TODO etudiant sont conserves. Re-execution papermill : 30/30 cellules, 0 erreur, execution_count non nul sur les 12 cellules de code, marqueur present sur les trois stubs. Organe governance_methods : 16/16 tests OK. Aucune erreur volontaire (C.1). Co-Authored-By: Claude Sonnet 5 --- ...nt_Analysis_Gouvernance_Multi_Agents.ipynb | 432 +++++++++--------- 1 file changed, 221 insertions(+), 211 deletions(-) diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb index 5112df37c8..5ef5f44cd4 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb @@ -5,10 +5,10 @@ "id": "gov-01", "metadata": { "papermill": { - "duration": 0.002754, - "end_time": "2026-09-22T01:52:48.253225+00:00", + "duration": 0.003599, + "end_time": "2026-09-22T10:12:09.414183+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.250471+00:00", + "start_time": "2026-09-22T10:12:09.410584+00:00", "status": "completed" }, "tags": [] @@ -36,10 +36,10 @@ "id": "gov-02", "metadata": { "papermill": { - "duration": 0.001763, - "end_time": "2026-09-22T01:52:48.257183+00:00", + "duration": 0.002641, + "end_time": "2026-09-22T10:12:09.419840+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.255420+00:00", + "start_time": "2026-09-22T10:12:09.417199+00:00", "status": "completed" }, "tags": [] @@ -65,16 +65,16 @@ "id": "gov-03", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.261788Z", - "iopub.status.busy": "2026-09-22T01:52:48.261603Z", - "iopub.status.idle": "2026-09-22T01:52:48.269394Z", - "shell.execute_reply": "2026-09-22T01:52:48.268654Z" + "iopub.execute_input": "2026-09-22T10:12:09.426414Z", + "iopub.status.busy": "2026-09-22T10:12:09.426200Z", + "iopub.status.idle": "2026-09-22T10:12:09.435303Z", + "shell.execute_reply": "2026-09-22T10:12:09.434655Z" }, "papermill": { - "duration": 0.011035, - "end_time": "2026-09-22T01:52:48.269947+00:00", + "duration": 0.0134, + "end_time": "2026-09-22T10:12:09.435904+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.258912+00:00", + "start_time": "2026-09-22T10:12:09.422504+00:00", "status": "completed" }, "tags": [] @@ -123,10 +123,10 @@ "id": "gov-04", "metadata": { "papermill": { - "duration": 0.001912, - "end_time": "2026-09-22T01:52:48.273936+00:00", + "duration": 0.002657, + "end_time": "2026-09-22T10:12:09.441351+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.272024+00:00", + "start_time": "2026-09-22T10:12:09.438694+00:00", "status": "completed" }, "tags": [] @@ -147,16 +147,16 @@ "id": "gov-05", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.278969Z", - "iopub.status.busy": "2026-09-22T01:52:48.278802Z", - "iopub.status.idle": "2026-09-22T01:52:48.283070Z", - "shell.execute_reply": "2026-09-22T01:52:48.282130Z" + "iopub.execute_input": "2026-09-22T10:12:09.448150Z", + "iopub.status.busy": "2026-09-22T10:12:09.447905Z", + "iopub.status.idle": "2026-09-22T10:12:09.452057Z", + "shell.execute_reply": "2026-09-22T10:12:09.451495Z" }, "papermill": { - "duration": 0.007751, - "end_time": "2026-09-22T01:52:48.283609+00:00", + "duration": 0.008378, + "end_time": "2026-09-22T10:12:09.452537+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.275858+00:00", + "start_time": "2026-09-22T10:12:09.444159+00:00", "status": "completed" }, "tags": [] @@ -194,10 +194,10 @@ "id": "gov-06", "metadata": { "papermill": { - "duration": 0.002224, - "end_time": "2026-09-22T01:52:48.288203+00:00", + "duration": 0.002782, + "end_time": "2026-09-22T10:12:09.458113+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.285979+00:00", + "start_time": "2026-09-22T10:12:09.455331+00:00", "status": "completed" }, "tags": [] @@ -222,16 +222,16 @@ "id": "gov-07", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.293418Z", - "iopub.status.busy": "2026-09-22T01:52:48.293158Z", - "iopub.status.idle": "2026-09-22T01:52:48.298406Z", - "shell.execute_reply": "2026-09-22T01:52:48.297802Z" + "iopub.execute_input": "2026-09-22T10:12:09.464328Z", + "iopub.status.busy": "2026-09-22T10:12:09.464152Z", + "iopub.status.idle": "2026-09-22T10:12:09.469157Z", + "shell.execute_reply": "2026-09-22T10:12:09.468583Z" }, "papermill": { - "duration": 0.008734, - "end_time": "2026-09-22T01:52:48.298920+00:00", + "duration": 0.008919, + "end_time": "2026-09-22T10:12:09.469613+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.290186+00:00", + "start_time": "2026-09-22T10:12:09.460694+00:00", "status": "completed" }, "tags": [] @@ -274,10 +274,10 @@ "id": "gov-08", "metadata": { "papermill": { - "duration": 0.002345, - "end_time": "2026-09-22T01:52:48.303594+00:00", + "duration": 0.00277, + "end_time": "2026-09-22T10:12:09.475303+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.301249+00:00", + "start_time": "2026-09-22T10:12:09.472533+00:00", "status": "completed" }, "tags": [] @@ -305,16 +305,16 @@ "id": "gov-09", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.309111Z", - "iopub.status.busy": "2026-09-22T01:52:48.308847Z", - "iopub.status.idle": "2026-09-22T01:52:48.313722Z", - "shell.execute_reply": "2026-09-22T01:52:48.313080Z" + "iopub.execute_input": "2026-09-22T10:12:09.482674Z", + "iopub.status.busy": "2026-09-22T10:12:09.482474Z", + "iopub.status.idle": "2026-09-22T10:12:09.487615Z", + "shell.execute_reply": "2026-09-22T10:12:09.486862Z" }, "papermill": { - "duration": 0.008749, - "end_time": "2026-09-22T01:52:48.314589+00:00", + "duration": 0.009423, + "end_time": "2026-09-22T10:12:09.488241+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.305840+00:00", + "start_time": "2026-09-22T10:12:09.478818+00:00", "status": "completed" }, "tags": [] @@ -356,10 +356,10 @@ "id": "gov-10", "metadata": { "papermill": { - "duration": 0.002264, - "end_time": "2026-09-22T01:52:48.319326+00:00", + "duration": 0.00276, + "end_time": "2026-09-22T10:12:09.493799+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.317062+00:00", + "start_time": "2026-09-22T10:12:09.491039+00:00", "status": "completed" }, "tags": [] @@ -383,16 +383,16 @@ "id": "gov-11", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.325188Z", - "iopub.status.busy": "2026-09-22T01:52:48.324884Z", - "iopub.status.idle": "2026-09-22T01:52:48.332573Z", - "shell.execute_reply": "2026-09-22T01:52:48.331716Z" + "iopub.execute_input": "2026-09-22T10:12:09.500863Z", + "iopub.status.busy": "2026-09-22T10:12:09.500630Z", + "iopub.status.idle": "2026-09-22T10:12:09.507894Z", + "shell.execute_reply": "2026-09-22T10:12:09.507310Z" }, "papermill": { - "duration": 0.011689, - "end_time": "2026-09-22T01:52:48.333227+00:00", + "duration": 0.011396, + "end_time": "2026-09-22T10:12:09.508359+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.321538+00:00", + "start_time": "2026-09-22T10:12:09.496963+00:00", "status": "completed" }, "tags": [] @@ -433,10 +433,10 @@ "id": "gov-12", "metadata": { "papermill": { - "duration": 0.002415, - "end_time": "2026-09-22T01:52:48.338217+00:00", + "duration": 0.002803, + "end_time": "2026-09-22T10:12:09.514051+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.335802+00:00", + "start_time": "2026-09-22T10:12:09.511248+00:00", "status": "completed" }, "tags": [] @@ -474,10 +474,10 @@ "id": "gov-13", "metadata": { "papermill": { - "duration": 0.002438, - "end_time": "2026-09-22T01:52:48.343060+00:00", + "duration": 0.002749, + "end_time": "2026-09-22T10:12:09.519676+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.340622+00:00", + "start_time": "2026-09-22T10:12:09.516927+00:00", "status": "completed" }, "tags": [] @@ -507,16 +507,16 @@ "id": "gov-14", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.348987Z", - "iopub.status.busy": "2026-09-22T01:52:48.348750Z", - "iopub.status.idle": "2026-09-22T01:52:48.353174Z", - "shell.execute_reply": "2026-09-22T01:52:48.352616Z" + "iopub.execute_input": "2026-09-22T10:12:09.526454Z", + "iopub.status.busy": "2026-09-22T10:12:09.526187Z", + "iopub.status.idle": "2026-09-22T10:12:09.531227Z", + "shell.execute_reply": "2026-09-22T10:12:09.530631Z" }, "papermill": { - "duration": 0.00819, - "end_time": "2026-09-22T01:52:48.353674+00:00", + "duration": 0.009218, + "end_time": "2026-09-22T10:12:09.531706+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.345484+00:00", + "start_time": "2026-09-22T10:12:09.522488+00:00", "status": "completed" }, "tags": [] @@ -554,10 +554,10 @@ "id": "gov-15", "metadata": { "papermill": { - "duration": 0.002394, - "end_time": "2026-09-22T01:52:48.358574+00:00", + "duration": 0.002675, + "end_time": "2026-09-22T10:12:09.537327+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.356180+00:00", + "start_time": "2026-09-22T10:12:09.534652+00:00", "status": "completed" }, "tags": [] @@ -581,10 +581,10 @@ "id": "gov-16", "metadata": { "papermill": { - "duration": 0.002304, - "end_time": "2026-09-22T01:52:48.363188+00:00", + "duration": 0.002607, + "end_time": "2026-09-22T10:12:09.542521+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.360884+00:00", + "start_time": "2026-09-22T10:12:09.539914+00:00", "status": "completed" }, "tags": [] @@ -608,16 +608,16 @@ "id": "gov-17", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.368679Z", - "iopub.status.busy": "2026-09-22T01:52:48.368508Z", - "iopub.status.idle": "2026-09-22T01:52:48.374348Z", - "shell.execute_reply": "2026-09-22T01:52:48.373661Z" + "iopub.execute_input": "2026-09-22T10:12:09.548798Z", + "iopub.status.busy": "2026-09-22T10:12:09.548609Z", + "iopub.status.idle": "2026-09-22T10:12:09.554087Z", + "shell.execute_reply": "2026-09-22T10:12:09.553476Z" }, "papermill": { - "duration": 0.009443, - "end_time": "2026-09-22T01:52:48.374890+00:00", + "duration": 0.009483, + "end_time": "2026-09-22T10:12:09.554583+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.365447+00:00", + "start_time": "2026-09-22T10:12:09.545100+00:00", "status": "completed" }, "tags": [] @@ -652,10 +652,10 @@ "id": "gov-18", "metadata": { "papermill": { - "duration": 0.002412, - "end_time": "2026-09-22T01:52:48.379877+00:00", + "duration": 0.002658, + "end_time": "2026-09-22T10:12:09.560376+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.377465+00:00", + "start_time": "2026-09-22T10:12:09.557718+00:00", "status": "completed" }, "tags": [] @@ -694,16 +694,16 @@ "id": "gov-19", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.385733Z", - "iopub.status.busy": "2026-09-22T01:52:48.385388Z", - "iopub.status.idle": "2026-09-22T01:52:48.393309Z", - "shell.execute_reply": "2026-09-22T01:52:48.392524Z" + "iopub.execute_input": "2026-09-22T10:12:09.567093Z", + "iopub.status.busy": "2026-09-22T10:12:09.566849Z", + "iopub.status.idle": "2026-09-22T10:12:09.573350Z", + "shell.execute_reply": "2026-09-22T10:12:09.572656Z" }, "papermill": { - "duration": 0.0117, - "end_time": "2026-09-22T01:52:48.393912+00:00", + "duration": 0.011048, + "end_time": "2026-09-22T10:12:09.574012+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.382212+00:00", + "start_time": "2026-09-22T10:12:09.562964+00:00", "status": "completed" }, "tags": [] @@ -742,10 +742,10 @@ "id": "gov-20", "metadata": { "papermill": { - "duration": 0.002488, - "end_time": "2026-09-22T01:52:48.399840+00:00", + "duration": 0.002982, + "end_time": "2026-09-22T10:12:09.579832+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.397352+00:00", + "start_time": "2026-09-22T10:12:09.576850+00:00", "status": "completed" }, "tags": [] @@ -776,16 +776,16 @@ "id": "gov-21", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.406010Z", - "iopub.status.busy": "2026-09-22T01:52:48.405832Z", - "iopub.status.idle": "2026-09-22T01:52:48.410297Z", - "shell.execute_reply": "2026-09-22T01:52:48.409723Z" + "iopub.execute_input": "2026-09-22T10:12:09.586357Z", + "iopub.status.busy": "2026-09-22T10:12:09.586119Z", + "iopub.status.idle": "2026-09-22T10:12:09.591308Z", + "shell.execute_reply": "2026-09-22T10:12:09.590499Z" }, "papermill": { - "duration": 0.008611, - "end_time": "2026-09-22T01:52:48.410818+00:00", + "duration": 0.009393, + "end_time": "2026-09-22T10:12:09.591942+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.402207+00:00", + "start_time": "2026-09-22T10:12:09.582549+00:00", "status": "completed" }, "tags": [] @@ -823,10 +823,10 @@ "id": "gov-22", "metadata": { "papermill": { - "duration": 0.002981, - "end_time": "2026-09-22T01:52:48.417174+00:00", + "duration": 0.002793, + "end_time": "2026-09-22T10:12:09.597607+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.414193+00:00", + "start_time": "2026-09-22T10:12:09.594814+00:00", "status": "completed" }, "tags": [] @@ -853,47 +853,15 @@ "prolonger." ] }, - { - "cell_type": "code", - "execution_count": 10, - "id": "gov-23", - "metadata": { - "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.423230Z", - "iopub.status.busy": "2026-09-22T01:52:48.423033Z", - "iopub.status.idle": "2026-09-22T01:52:48.426191Z", - "shell.execute_reply": "2026-09-22T01:52:48.425654Z" - }, - "papermill": { - "duration": 0.006969, - "end_time": "2026-09-22T01:52:48.426679+00:00", - "exception": false, - "start_time": "2026-09-22T01:52:48.419710+00:00", - "status": "completed" - }, - "tags": [] - }, - "outputs": [], - "source": [ - "def vrai_quadratique(agents, options, budget=9, intensite=None):\n", - " # Etape 1 : pour chaque agent, voix par option = floor(sqrt(budget * intensite(option)))\n", - " # Etape 2 : cumuler les voix par option sur tous les agents\n", - " # Etape 3 : rendre l'option au total max\n", - " # Indice : intensite = 1.0 premier choix, 0.5 deuxieme, 0.0 ensuite ;\n", - " # tout le budget sur le premier choix achete floor(sqrt(9)) = 3 voix.\n", - " print(\"Exercice 1 a completer\")\n", - " return None # TODO etudiant" - ] - }, { "cell_type": "markdown", "id": "gov-24", "metadata": { "papermill": { - "duration": 0.002596, - "end_time": "2026-09-22T01:52:48.431884+00:00", + "duration": 0.002682, + "end_time": "2026-09-22T10:12:09.602930+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.429288+00:00", + "start_time": "2026-09-22T10:12:09.600248+00:00", "status": "completed" }, "tags": [] @@ -930,56 +898,15 @@ "autre seed peut rendre un autre vainqueur (l'exercice 3 le mesure)." ] }, - { - "cell_type": "code", - "execution_count": 11, - "id": "gov-25", - "metadata": { - "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.438016Z", - "iopub.status.busy": "2026-09-22T01:52:48.437736Z", - "iopub.status.idle": "2026-09-22T01:52:48.441342Z", - "shell.execute_reply": "2026-09-22T01:52:48.440728Z" - }, - "papermill": { - "duration": 0.007427, - "end_time": "2026-09-22T01:52:48.441841+00:00", - "exception": false, - "start_time": "2026-09-22T01:52:48.434414+00:00", - "status": "completed" - }, - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Exercice 2 a completer\n" - ] - } - ], - "source": [ - "ma_societe = {\n", - " \"options\": [],\n", - " \"agents\": [\n", - " # (\"nom\", \"personnalite\", [preferences completes])\n", - " ],\n", - "}\n", - "# Indice : spoiler_candidate est deja un cas majority != borda ; cherchez-en un\n", - "# AUTRE (4 options ou 7 agents), puis g.summarize(g.simulate_vote(...)) x 2.\n", - "print(\"Exercice 2 a completer\")" - ] - }, { "cell_type": "markdown", "id": "gov-26", "metadata": { "papermill": { - "duration": 0.002596, - "end_time": "2026-09-22T01:52:48.447065+00:00", + "duration": 0.002799, + "end_time": "2026-09-22T10:12:09.608767+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.444469+00:00", + "start_time": "2026-09-22T10:12:09.605968+00:00", "status": "completed" }, "tags": [] @@ -998,20 +925,20 @@ }, { "cell_type": "code", - "execution_count": 12, - "id": "gov-27", + "execution_count": 10, + "id": "gov-23", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T01:52:48.454387Z", - "iopub.status.busy": "2026-09-22T01:52:48.454129Z", - "iopub.status.idle": "2026-09-22T01:52:48.457823Z", - "shell.execute_reply": "2026-09-22T01:52:48.457280Z" + "iopub.execute_input": "2026-09-22T10:12:09.615311Z", + "iopub.status.busy": "2026-09-22T10:12:09.615124Z", + "iopub.status.idle": "2026-09-22T10:12:09.618959Z", + "shell.execute_reply": "2026-09-22T10:12:09.618208Z" }, "papermill": { - "duration": 0.007864, - "end_time": "2026-09-22T01:52:48.458438+00:00", + "duration": 0.007891, + "end_time": "2026-09-22T10:12:09.619465+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.450574+00:00", + "start_time": "2026-09-22T10:12:09.611574+00:00", "status": "completed" }, "tags": [] @@ -1021,19 +948,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "Exercice 3 a completer\n" + "Exercice 1 a completer\n" ] } ], "source": [ - "from collections import Counter\n", - "gagnants = Counter()\n", - "# Etape 1 : boucle sur 10 seeds -> simulate_vote(..., \"raft\", seed=s) sur cyclic_majority\n", - "# Etape 2 : gagnants[winner] += 1, print(gagnants)\n", - "# Etape 3 : temoin negatif sur strategic_bloc (meme distribution, meme gagnant partout)\n", - "# Indice : g.stability([...]) rend 1 si un seul vainqueur dans la liste, 0 sinon.\n", - "resultat_attendu = None # TODO etudiant\n", - "print(\"Exercice 3 a completer\")" + "def vrai_quadratique(agents, options, budget=9, intensite=None):\n", + " # Etape 1 : pour chaque agent, voix par option = floor(sqrt(budget * intensite(option)))\n", + " # Etape 2 : cumuler les voix par option sur tous les agents\n", + " # Etape 3 : rendre l'option au total max\n", + " # Indice : intensite = 1.0 premier choix, 0.5 deuxieme, 0.0 ensuite ;\n", + " # tout le budget sur le premier choix achete floor(sqrt(9)) = 3 voix.\n", + " return None # TODO etudiant\n", + "\n", + "\n", + "print(\"Exercice 1 a completer\")" ] }, { @@ -1041,10 +970,10 @@ "id": "gov-28", "metadata": { "papermill": { - "duration": 0.002529, - "end_time": "2026-09-22T01:52:48.463619+00:00", + "duration": 0.002985, + "end_time": "2026-09-22T10:12:09.625618+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.461090+00:00", + "start_time": "2026-09-22T10:12:09.622633+00:00", "status": "completed" }, "tags": [] @@ -1058,15 +987,56 @@ "la plus juste (justice Gini) est-elle celle qui satisfait le plus d'agents ?" ] }, + { + "cell_type": "code", + "execution_count": 11, + "id": "gov-25", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T10:12:09.632586Z", + "iopub.status.busy": "2026-09-22T10:12:09.632323Z", + "iopub.status.idle": "2026-09-22T10:12:09.636173Z", + "shell.execute_reply": "2026-09-22T10:12:09.635513Z" + }, + "papermill": { + "duration": 0.008178, + "end_time": "2026-09-22T10:12:09.636687+00:00", + "exception": false, + "start_time": "2026-09-22T10:12:09.628509+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice 2 a completer\n" + ] + } + ], + "source": [ + "ma_societe = {\n", + " \"options\": [],\n", + " \"agents\": [\n", + " # (\"nom\", \"personnalite\", [preferences completes])\n", + " ],\n", + "}\n", + "# Indice : spoiler_candidate est deja un cas majority != borda ; cherchez-en un\n", + "# AUTRE (4 options ou 7 agents), puis g.summarize(g.simulate_vote(...)) x 2.\n", + "print(\"Exercice 2 a completer\")" + ] + }, { "cell_type": "markdown", "id": "gov-29", "metadata": { "papermill": { - "duration": 0.002525, - "end_time": "2026-09-22T01:52:48.468711+00:00", + "duration": 0.002982, + "end_time": "2026-09-22T10:12:09.642781+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.466186+00:00", + "start_time": "2026-09-22T10:12:09.639799+00:00", "status": "completed" }, "tags": [] @@ -1081,15 +1051,55 @@ "déraille." ] }, + { + "cell_type": "code", + "execution_count": 12, + "id": "gov-27", + "metadata": { + "execution": { + "iopub.execute_input": "2026-09-22T10:12:09.649495Z", + "iopub.status.busy": "2026-09-22T10:12:09.649262Z", + "iopub.status.idle": "2026-09-22T10:12:09.653175Z", + "shell.execute_reply": "2026-09-22T10:12:09.652631Z" + }, + "papermill": { + "duration": 0.008328, + "end_time": "2026-09-22T10:12:09.653931+00:00", + "exception": false, + "start_time": "2026-09-22T10:12:09.645603+00:00", + "status": "completed" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exercice 3 a completer\n" + ] + } + ], + "source": [ + "from collections import Counter\n", + "gagnants = Counter()\n", + "# Etape 1 : boucle sur 10 seeds -> simulate_vote(..., \"raft\", seed=s) sur cyclic_majority\n", + "# Etape 2 : gagnants[winner] += 1, print(gagnants)\n", + "# Etape 3 : temoin negatif sur strategic_bloc (meme distribution, meme gagnant partout)\n", + "# Indice : g.stability([...]) rend 1 si un seul vainqueur dans la liste, 0 sinon.\n", + "resultat_attendu = None # TODO etudiant\n", + "print(\"Exercice 3 a completer\")" + ] + }, { "cell_type": "markdown", "id": "gov-30", "metadata": { "papermill": { - "duration": 0.002458, - "end_time": "2026-09-22T01:52:48.473672+00:00", + "duration": 0.002872, + "end_time": "2026-09-22T10:12:09.660192+00:00", "exception": false, - "start_time": "2026-09-22T01:52:48.471214+00:00", + "start_time": "2026-09-22T10:12:09.657320+00:00", "status": "completed" }, "tags": [] @@ -1142,14 +1152,14 @@ }, "papermill": { "default_parameters": {}, - "duration": 2.154401, - "end_time": "2026-09-22T01:52:48.711546+00:00", + "duration": 2.189152, + "end_time": "2026-09-22T10:12:09.905465+00:00", "environment_variables": {}, "exception": null, "input_path": "Argument_Analysis_Gouvernance_Multi_Agents.ipynb", "output_path": "Argument_Analysis_Gouvernance_Multi_Agents.ipynb", "parameters": {}, - "start_time": "2026-09-22T01:52:46.557145+00:00", + "start_time": "2026-09-22T10:12:07.716313+00:00", "version": "2.7.0" } }, From 51d5e10697195bb922b8c2d2e435b2ad5e0e4e59 Mon Sep 17 00:00:00 2001 From: jsboige Date: Tue, 22 Sep 2026 23:30:09 +0200 Subject: [PATCH 3/8] docs(argumentation): placer les lignes README sur une ancre propre Les quatre PRs de distillation soeurs inseraient toutes leurs lignes entre Toulmin_Model et Dialogues_Protocolises : chaque merge rendait les trois autres DIRTY. Chaque PR a desormais son ancre, separee des autres par au moins une ligne inchangee. Co-Authored-By: Claude Opus 5 (1M context) --- MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md index d68716cd5f..ed6f43660a 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md @@ -83,6 +83,7 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | Gouv | [Gouvernance_Multi_Agents](Argument_Analysis_Gouvernance_Multi_Agents.ipynb) | Gouvernance multi-agents (distillation EPITA 2.1.6, sous-grain 2 #4960) : organe pur `governance_methods.py` — 7 méthodes de vote/consensus (majorité, Borda, Condorcet à repli, quadratique, byzantin, Raft) sur 6 scénarios étiquetés, métriques consensus/justice Gini/satisfaction, **spoiler et cycle de Condorcet mesurés**, robustesse byzantine (courbe non monotone expliquée), manipulation et coalitions — pur stdlib Python, 6 divergences source documentées | Choix collectif | | Rank | [Ranking_Semantics](Argument_Analysis_Ranking_Semantics.ipynb) | Sémantiques de classement (h-Categoriser, fardeau) en pur Python : force numérique départageant des arguments de même statut Dung | Argumentation graduée | | Orchestration | [Orchestration_Modes](Argument_Analysis_Orchestration_Modes.ipynb) | Comparer 7 modes d'orchestration par le budget (distillation Triple Distillation de l'instrument EPITA #1735) : 3 registres d'arrêt (filet du harnais / déclaration du mode / réalité des phases), asymétrie d'axes largeur-délégation-dialogue, budget calibré dérivé de la mesure — mesures committées, pur stdlib | Arbitrage d'architectures multi-agents | +| Gouv | [Gouvernance_Multi_Agents](Argument_Analysis_Gouvernance_Multi_Agents.ipynb) | Gouvernance multi-agents (distillation EPITA 2.1.6, sous-grain 2 #4960) : organe pur `governance_methods.py` — 7 méthodes de vote/consensus (majorité, Borda, Condorcet à repli, quadratique, byzantin, Raft) sur 6 scénarios étiquetés, métriques consensus/justice Gini/satisfaction, **spoiler et cycle de Condorcet mesurés**, robustesse byzantine (courbe non monotone expliquée), manipulation et coalitions — pur stdlib Python, 6 divergences source documentées | Choix collectif | | Dated | [Dated_Graphs](Argument_Analysis_Dated_Graphs.ipynb) | Instrument $G_t^{arg} \to G_{t+1}^{arg}$ (Epic #13303, issue #13310) : corpus daté → graphe AIF conforme (critère d'inclusion C1–C3 écrit, exclusions publiées) → projection Dung ; deux mesures d'écart de familles différentes (Jaccard structurelle nœuds/attaques + Jaccard sémantique sur extensions grounded), **contrôle négatif** (plancher de bruit par split de la même période) publié à côté de tout écart, **contrôle positif** à magnitude attendue écrite avant mesure (attaque de racine : sortie directe + réhabilitation paradoxale de la victime + cascade) — pur Python + rdflib, validation synthétique uniquement ; **hypothèse monotone** posée, vérifiée par inclusion et violée délibérément (retrait d'un déchu vs d'un accepté : ce que voient les deux mesures) | Argumentation temporelle / mesure | | Obs | [Observatoire-1-Initiation](Argument_Analysis_Observatoire-1-Initiation.ipynb) | Cas 1 de l'Observatoire (Epic #13303, livré #16431) : Bumble « Opening Moves » (mars–avril 2024), DiD imparfait sur avis datés — instrument Dated_Graphs **rebranché, pas réécrit** (module `_dated_graphs_mod.py`), corpus Arctic Shift 4 bras × 2 fenêtres stratifiés mensuellement, extraction LLM Ollama (qwen2.5:7b-instruct-q4_K_M, température 0, échantillon complet), agrégats public-safe, plancher de bruit split-half chronologique — verdict sur quatre observations conjointes, plafond de preuve déclaré | Étude de cas / mesure empirique | | Route | [Multi_Backend_Routing](Argument_Analysis_Multi_Backend_Routing.ipynb) | Routage multi-backend « décider ou échouer bruyamment » : PL/Modal/Dung/FOL décidés par Tweety embarqué + sentinelle de contrat de livraison gardant les prouveurs externes (EProver/Mace4) — doctrine anti-théâtre / fail-loud | Raisonnement robuste | @@ -120,6 +121,7 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | **Gouvernance_Multi_Agents** | Comparer sept méthodes de vote sur les mêmes préférences (effet spoiler, cycle de Condorcet, repli Borda), mesurer justice de Gini et satisfaction, puis tester la résistance de la décision : bruit byzantin, manipulation stratégique, stabilité multi-seed — pur stdlib Python | 35 min | | **Ranking_Semantics** | Calculer la *force* numérique d'un argument (h-Categoriser par point fixe, fardeau par comparaison lexicographique) et départager des arguments que Dung déclare indistinctement rejetés — pur stdlib Python | 35 min | | **Orchestration_Modes** | Reconstruire les tableaux sous-budget/calibré d'un comparatif de 7 modes d'orchestration, nommer les 3 registres d'arrêt, et dériver un budget calibré — avec la limite mesurée de la projection linéaire (1081 s naïfs vs 600 s réels) | 30 min | +| **Gouvernance_Multi_Agents** | Comparer sept méthodes de vote sur les mêmes préférences (effet spoiler, cycle de Condorcet, repli Borda), mesurer justice de Gini et satisfaction, puis tester la résistance de la décision : bruit byzantin, manipulation stratégique, stabilité multi-seed — pur stdlib Python | 35 min | | **Dated_Graphs** | Construire l'instrument de comparaison temporelle des graphes d'argumentation : critère d'inclusion écrit d'un énoncé, graphe AIF conforme sérialisé en RDF, projection Dung, deux mesures d'écart de familles différentes, plancher de bruit (contrôle négatif) publié avec chaque mesure, et contrôle positif dont la magnitude attendue est dérivée à la main avant l'exécution | 45 min | | **Observatoire-1-Initiation** | Conduire un cas empirique pré-inscrit de bout en bout : reprise de l'instrument Dated_Graphs sans réécriture (module dédié, garde anti-repli), corpus daté stratifié mensuellement, extraction LLM épinglée (modèle, température 0, schéma JSON), mesure via l'instrument avec plancher de bruit split-half chronologique, et plafond de preuve déclaré (DiD imparfait) | — | | **Restitution_3_Actes** | Séparer la *lisibilité* (confiée au LLM) de l'*honnêteté* (gardée par un scaffold déterministe) : extraction d'evidence, bande de verdict *gated* sur la couverture, gate de tissage anti-énumération (§4), renderer qui *nomme* les actes manquants, et narration LLM injectable *fail-loud* | 45 min | @@ -521,6 +523,7 @@ en référence. | `Argument_Analysis_Knowledge_Base.ipynb` | Mémoire d'un débat : population transitive des propositions, requêtes support/attaque par négation lexicale `¬` | aucun (indépendant ; complète `Toulmin_Model`, `Schemes_Walton` et `Dialogues_Protocolises`, cités en contexte) | | `Argument_Analysis_Gouvernance_Multi_Agents.ipynb` | Gouvernance multi-agents (distillation EPITA 2.1.6, #4960) — vote/consensus/coalitions/manipulation | aucun (indépendant ; voisin d'`Agentic-3-orchestration` par la question — décider à plusieurs vs orchestrer le travail — pas par le code) | | `Argument_Analysis_Ranking_Semantics.ipynb` | Sémantiques graduées (h-Categoriser, fardeau) | `Dung_AF_Semantics` | +| `Argument_Analysis_Gouvernance_Multi_Agents.ipynb` | Gouvernance multi-agents (distillation EPITA 2.1.6, #4960) — vote/consensus/coalitions/manipulation | aucun (indépendant ; voisin d'`Agentic-3-orchestration` par la question — décider à plusieurs vs orchestrer le travail — pas par le code) | | `Argument_Analysis_Dated_Graphs.ipynb` | Instrument $G_t^{arg} \to G_{t+1}^{arg}$ (Epic #13303, issue #13310) | `Dung_AF_Semantics` | | `Argument_Analysis_Observatoire-1-Initiation.ipynb` | Cas 1 de l'Observatoire (Epic #13303, livré #16431) : étude empirique DiD imparfait consommant l'instrument | `Dated_Graphs` | From 03a3e2f33e4946949297e2d0df81489ed649ff40 Mon Sep 17 00:00:00 2001 From: jsboige Date: Tue, 22 Sep 2026 23:34:08 +0200 Subject: [PATCH 4/8] docs(argumentation): ancre README distincte des PRs de distillation soeurs Six PRs ouvertes ajoutent chacune leurs lignes aux memes tableaux du README. Deux d'entre elles visaient le meme point que celle-ci ; chaque PR a maintenant sa propre ancre, separee des autres par au moins une ligne inchangee (fusion sequentielle des six branches : 0 conflit). Co-Authored-By: Claude Opus 5 (1M context) --- MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md index ed6f43660a..c334921d05 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md @@ -83,8 +83,8 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | Gouv | [Gouvernance_Multi_Agents](Argument_Analysis_Gouvernance_Multi_Agents.ipynb) | Gouvernance multi-agents (distillation EPITA 2.1.6, sous-grain 2 #4960) : organe pur `governance_methods.py` — 7 méthodes de vote/consensus (majorité, Borda, Condorcet à repli, quadratique, byzantin, Raft) sur 6 scénarios étiquetés, métriques consensus/justice Gini/satisfaction, **spoiler et cycle de Condorcet mesurés**, robustesse byzantine (courbe non monotone expliquée), manipulation et coalitions — pur stdlib Python, 6 divergences source documentées | Choix collectif | | Rank | [Ranking_Semantics](Argument_Analysis_Ranking_Semantics.ipynb) | Sémantiques de classement (h-Categoriser, fardeau) en pur Python : force numérique départageant des arguments de même statut Dung | Argumentation graduée | | Orchestration | [Orchestration_Modes](Argument_Analysis_Orchestration_Modes.ipynb) | Comparer 7 modes d'orchestration par le budget (distillation Triple Distillation de l'instrument EPITA #1735) : 3 registres d'arrêt (filet du harnais / déclaration du mode / réalité des phases), asymétrie d'axes largeur-délégation-dialogue, budget calibré dérivé de la mesure — mesures committées, pur stdlib | Arbitrage d'architectures multi-agents | -| Gouv | [Gouvernance_Multi_Agents](Argument_Analysis_Gouvernance_Multi_Agents.ipynb) | Gouvernance multi-agents (distillation EPITA 2.1.6, sous-grain 2 #4960) : organe pur `governance_methods.py` — 7 méthodes de vote/consensus (majorité, Borda, Condorcet à repli, quadratique, byzantin, Raft) sur 6 scénarios étiquetés, métriques consensus/justice Gini/satisfaction, **spoiler et cycle de Condorcet mesurés**, robustesse byzantine (courbe non monotone expliquée), manipulation et coalitions — pur stdlib Python, 6 divergences source documentées | Choix collectif | | Dated | [Dated_Graphs](Argument_Analysis_Dated_Graphs.ipynb) | Instrument $G_t^{arg} \to G_{t+1}^{arg}$ (Epic #13303, issue #13310) : corpus daté → graphe AIF conforme (critère d'inclusion C1–C3 écrit, exclusions publiées) → projection Dung ; deux mesures d'écart de familles différentes (Jaccard structurelle nœuds/attaques + Jaccard sémantique sur extensions grounded), **contrôle négatif** (plancher de bruit par split de la même période) publié à côté de tout écart, **contrôle positif** à magnitude attendue écrite avant mesure (attaque de racine : sortie directe + réhabilitation paradoxale de la victime + cascade) — pur Python + rdflib, validation synthétique uniquement ; **hypothèse monotone** posée, vérifiée par inclusion et violée délibérément (retrait d'un déchu vs d'un accepté : ce que voient les deux mesures) | Argumentation temporelle / mesure | +| Gouv | [Gouvernance_Multi_Agents](Argument_Analysis_Gouvernance_Multi_Agents.ipynb) | Gouvernance multi-agents (distillation EPITA 2.1.6, sous-grain 2 #4960) : organe pur `governance_methods.py` — 7 méthodes de vote/consensus (majorité, Borda, Condorcet à repli, quadratique, byzantin, Raft) sur 6 scénarios étiquetés, métriques consensus/justice Gini/satisfaction, **spoiler et cycle de Condorcet mesurés**, robustesse byzantine (courbe non monotone expliquée), manipulation et coalitions — pur stdlib Python, 6 divergences source documentées | Choix collectif | | Obs | [Observatoire-1-Initiation](Argument_Analysis_Observatoire-1-Initiation.ipynb) | Cas 1 de l'Observatoire (Epic #13303, livré #16431) : Bumble « Opening Moves » (mars–avril 2024), DiD imparfait sur avis datés — instrument Dated_Graphs **rebranché, pas réécrit** (module `_dated_graphs_mod.py`), corpus Arctic Shift 4 bras × 2 fenêtres stratifiés mensuellement, extraction LLM Ollama (qwen2.5:7b-instruct-q4_K_M, température 0, échantillon complet), agrégats public-safe, plancher de bruit split-half chronologique — verdict sur quatre observations conjointes, plafond de preuve déclaré | Étude de cas / mesure empirique | | Route | [Multi_Backend_Routing](Argument_Analysis_Multi_Backend_Routing.ipynb) | Routage multi-backend « décider ou échouer bruyamment » : PL/Modal/Dung/FOL décidés par Tweety embarqué + sentinelle de contrat de livraison gardant les prouveurs externes (EProver/Mace4) — doctrine anti-théâtre / fail-loud | Raisonnement robuste | | Comm | [Communication_Channels](Argument_Analysis_Communication_Channels.ipynb) | Bus de communication multi-agents (distillation Triple Distillation du tronc EPITA) : format message à priorité inversée, contrat de canal fail-loud (#2161), routage sans routes mortes (#1571), corrélation requête-réponse — pur stdlib Python, déterministe | Communication multi-agents | @@ -121,8 +121,8 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | **Gouvernance_Multi_Agents** | Comparer sept méthodes de vote sur les mêmes préférences (effet spoiler, cycle de Condorcet, repli Borda), mesurer justice de Gini et satisfaction, puis tester la résistance de la décision : bruit byzantin, manipulation stratégique, stabilité multi-seed — pur stdlib Python | 35 min | | **Ranking_Semantics** | Calculer la *force* numérique d'un argument (h-Categoriser par point fixe, fardeau par comparaison lexicographique) et départager des arguments que Dung déclare indistinctement rejetés — pur stdlib Python | 35 min | | **Orchestration_Modes** | Reconstruire les tableaux sous-budget/calibré d'un comparatif de 7 modes d'orchestration, nommer les 3 registres d'arrêt, et dériver un budget calibré — avec la limite mesurée de la projection linéaire (1081 s naïfs vs 600 s réels) | 30 min | -| **Gouvernance_Multi_Agents** | Comparer sept méthodes de vote sur les mêmes préférences (effet spoiler, cycle de Condorcet, repli Borda), mesurer justice de Gini et satisfaction, puis tester la résistance de la décision : bruit byzantin, manipulation stratégique, stabilité multi-seed — pur stdlib Python | 35 min | | **Dated_Graphs** | Construire l'instrument de comparaison temporelle des graphes d'argumentation : critère d'inclusion écrit d'un énoncé, graphe AIF conforme sérialisé en RDF, projection Dung, deux mesures d'écart de familles différentes, plancher de bruit (contrôle négatif) publié avec chaque mesure, et contrôle positif dont la magnitude attendue est dérivée à la main avant l'exécution | 45 min | +| **Gouvernance_Multi_Agents** | Comparer sept méthodes de vote sur les mêmes préférences (effet spoiler, cycle de Condorcet, repli Borda), mesurer justice de Gini et satisfaction, puis tester la résistance de la décision : bruit byzantin, manipulation stratégique, stabilité multi-seed — pur stdlib Python | 35 min | | **Observatoire-1-Initiation** | Conduire un cas empirique pré-inscrit de bout en bout : reprise de l'instrument Dated_Graphs sans réécriture (module dédié, garde anti-repli), corpus daté stratifié mensuellement, extraction LLM épinglée (modèle, température 0, schéma JSON), mesure via l'instrument avec plancher de bruit split-half chronologique, et plafond de preuve déclaré (DiD imparfait) | — | | **Restitution_3_Actes** | Séparer la *lisibilité* (confiée au LLM) de l'*honnêteté* (gardée par un scaffold déterministe) : extraction d'evidence, bande de verdict *gated* sur la couverture, gate de tissage anti-énumération (§4), renderer qui *nomme* les actes manquants, et narration LLM injectable *fail-loud* | 45 min | | **Communication_Channels** | Construire un bus de communication multi-agents : messages à priorité inversée, filtres fail-loud, routage sans routes mortes, corrélation requête-réponse — pur stdlib Python | 30 min | From b9afb8081243190fd5b5e15344af3afcbf9f5aaa Mon Sep 17 00:00:00 2001 From: jsboige Date: Wed, 23 Sep 2026 05:00:07 +0200 Subject: [PATCH 5/8] =?UTF-8?q?fix(argumentation,#4960):=20divergence=204?= =?UTF-8?q?=20=E2=80=94=20le=20NameError=20de=20update=5Fmemory=20tombe=20?= =?UTF-8?q?au=20deuxieme=20appel,=20quel=20que=20soit=20le=20contexte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La formulation precedente ("latent des que le contexte ne porte pas options") etait fausse : base_agent.py:116 passe `options` sans condition a update_q, et `options` n'est defini ni localement ni au niveau module. Mesure sur le source (origin/main f246e200b) : le deuxieme appel leve NameError avec contexte {"options": [...]} comme avec None. Markdown + docstring uniquement (C.2 : pas de re-execution requise). tests/test_governance_methods.py : 16 passed. Co-Authored-By: Claude Opus 5 (1M context) --- .../Argument_Analysis_Gouvernance_Multi_Agents.ipynb | 4 ++-- .../SymbolicAI/Argument_Analysis/governance_methods.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb index 5ef5f44cd4..c43aa13a10 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb @@ -885,8 +885,8 @@ " tel quel — l'exercice 1 fait implémenter le vrai.\n", "3. **Les byzantins sont les n premiers agents** (tranche déterministe\n", " `agents[:n]`), pas un tirage : mesuré, porté fidèlement.\n", - "4. **La voie mémoire du source référence `options` hors de portée** (NameError\n", - " latent dès que le contexte ne porte pas `\"options\"`) : non portée, la\n", + "4. **La voie mémoire du source référence `options` hors de portée** : `NameError`\n", + " dès le deuxième appel, que le contexte porte `\"options\"` ou non (mesuré) : non portée, la\n", " satisfaction est recalculée depuis les préférences.\n", "5. **La médiation à probabilités fixes** (0.8/0.5/0.7 codés en dur) n'est pas\n", " portée : seul `detect_conflicts` l'est.\n", diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py index 124f503295..03482cf98e 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py @@ -36,8 +36,10 @@ 3. `byzantine_consensus` du source désigne les byzantins comme les n PREMIERS agents (`agents[:n]`, tranche déterministe), pas un tirage. Porté fidèle. 4. La voie `update_memory` du `base_agent` source référence `options` hors de - sa portée (NameError latent dès que le contexte ne porte pas "options") ; - la satisfaction est recalculée ici proprement à partir des préférences. + sa portée : NameError dès le deuxième appel, que le contexte porte + "options" ou non (l'appel à `update_q` passe `options` sans condition ; + mesuré sur le source). La satisfaction est recalculée ici proprement à + partir des préférences. 5. La médiation à probabilités fixes n'est pas portée (pseudo-mécanisme) ; seule `detect_conflicts` (paires d'agents à positions différentes) l'est. 6. `condorcet_method` conserve le repli Borda du source (comportement standard From e8167e3838ac729407801f7e44a08a64f8fb1bac Mon Sep 17 00:00:00 2001 From: jsboige Date: Fri, 25 Sep 2026 01:06:14 +0200 Subject: [PATCH 6/8] docs(argumentation,#4960): dedup lignes README Gouvernance apres rebase sur main La resolution du rebase a duplique la ligne Gouvernance dans les trois tables (ajout par union sans suppression du deplacement). Une seule occurrence par table, ancre apres Dated_Graphs comme a la tete pre-rebase. Tests organe 16/16. Co-Authored-By: Claude Sonnet 5 --- MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md index c334921d05..0a8abded2f 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md @@ -80,7 +80,6 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | Schemes | [Schemes_Walton](Argument_Analysis_Schemes_Walton.ipynb) | Les 10 schémas d'argumentation de Walton (table verbatim du moteur étudiant EPITA, distillation Triple Distillation) et leur classifieur lexical déterministe : paire de mots-clés accentués exigée, ordre canonique qui départage (`modus_ponens` en dernier, « donc » armant aussi `cause_effect`), questions critiques canoniques, échec bruyant (`None` honnête, jamais d'étiquette fabriquée) — pur stdlib | Argumentation informelle schématique | | Dial | [Dialogues_Protocolises](Argument_Analysis_Dialogues_Protocolises.ipynb) | Protocoles de dialogue Walton–Krabbe (inquiry/persuasion) comme machines à états sur 9 actes de parole : tables de transitions, terminaison par condition testable (compréhension mutuelle, capitulation, boucle), 5 divergences source documentées dont une condition de terminaison inatteignable (`_term_double_retract`) — pur stdlib Python | Protocoles d'échange / pragmatique | | KB | [Knowledge_Base](Argument_Analysis_Knowledge_Base.ipynb) | La mémoire d'un débat (distillation Triple Distillation du moteur étudiant EPITA) : population transitive (`add_argument` porte prémisses et conclusion), support/attaque par convention lexicale `¬`, cohérence = conflit ouvert P/`¬P`, `entails` = appartenance documentée (pas d'inférence — non-explosion mesurée, `¬¬P` distinct de `P`, écrasement par contenu) — pur stdlib Python | Mémoire de débat | -| Gouv | [Gouvernance_Multi_Agents](Argument_Analysis_Gouvernance_Multi_Agents.ipynb) | Gouvernance multi-agents (distillation EPITA 2.1.6, sous-grain 2 #4960) : organe pur `governance_methods.py` — 7 méthodes de vote/consensus (majorité, Borda, Condorcet à repli, quadratique, byzantin, Raft) sur 6 scénarios étiquetés, métriques consensus/justice Gini/satisfaction, **spoiler et cycle de Condorcet mesurés**, robustesse byzantine (courbe non monotone expliquée), manipulation et coalitions — pur stdlib Python, 6 divergences source documentées | Choix collectif | | Rank | [Ranking_Semantics](Argument_Analysis_Ranking_Semantics.ipynb) | Sémantiques de classement (h-Categoriser, fardeau) en pur Python : force numérique départageant des arguments de même statut Dung | Argumentation graduée | | Orchestration | [Orchestration_Modes](Argument_Analysis_Orchestration_Modes.ipynb) | Comparer 7 modes d'orchestration par le budget (distillation Triple Distillation de l'instrument EPITA #1735) : 3 registres d'arrêt (filet du harnais / déclaration du mode / réalité des phases), asymétrie d'axes largeur-délégation-dialogue, budget calibré dérivé de la mesure — mesures committées, pur stdlib | Arbitrage d'architectures multi-agents | | Dated | [Dated_Graphs](Argument_Analysis_Dated_Graphs.ipynb) | Instrument $G_t^{arg} \to G_{t+1}^{arg}$ (Epic #13303, issue #13310) : corpus daté → graphe AIF conforme (critère d'inclusion C1–C3 écrit, exclusions publiées) → projection Dung ; deux mesures d'écart de familles différentes (Jaccard structurelle nœuds/attaques + Jaccard sémantique sur extensions grounded), **contrôle négatif** (plancher de bruit par split de la même période) publié à côté de tout écart, **contrôle positif** à magnitude attendue écrite avant mesure (attaque de racine : sortie directe + réhabilitation paradoxale de la victime + cascade) — pur Python + rdflib, validation synthétique uniquement ; **hypothèse monotone** posée, vérifiée par inclusion et violée délibérément (retrait d'un déchu vs d'un accepté : ce que voient les deux mesures) | Argumentation temporelle / mesure | @@ -118,7 +117,6 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | **Schemes_Walton** | Reconnaître la forme stéréotypée d'un argument (autorité, analogie, cause à effet...) par un matcher lexical déterministe, prédire son départage par l'ordre canonique, et auditer un texte classé avec les questions critiques de Walton — pur stdlib Python | 30 min | | **Dialogues_Protocolises** | Lire deux protocoles de dialogue Walton–Krabbe comme des machines à états : neuf actes de parole, tables de transitions inquiry vs persuasion, terminaison par condition testable (compréhension mutuelle, capitulation, boucle), et une condition de terminaison inatteignable découverte dans le source — pur stdlib Python | 35 min | | **Knowledge_Base** | Construire et interroger la mémoire d'un débat : population transitive (un argument porte ses prémisses et sa conclusion), requêtes support/attaque par convention lexicale `¬`, détection du conflit ouvert, et les limites mesurées (pas d'inférence depuis une contradiction, `¬¬P` distinct de `P`, écrasement des propositions homonymes) — pur stdlib Python | 30 min | -| **Gouvernance_Multi_Agents** | Comparer sept méthodes de vote sur les mêmes préférences (effet spoiler, cycle de Condorcet, repli Borda), mesurer justice de Gini et satisfaction, puis tester la résistance de la décision : bruit byzantin, manipulation stratégique, stabilité multi-seed — pur stdlib Python | 35 min | | **Ranking_Semantics** | Calculer la *force* numérique d'un argument (h-Categoriser par point fixe, fardeau par comparaison lexicographique) et départager des arguments que Dung déclare indistinctement rejetés — pur stdlib Python | 35 min | | **Orchestration_Modes** | Reconstruire les tableaux sous-budget/calibré d'un comparatif de 7 modes d'orchestration, nommer les 3 registres d'arrêt, et dériver un budget calibré — avec la limite mesurée de la projection linéaire (1081 s naïfs vs 600 s réels) | 30 min | | **Dated_Graphs** | Construire l'instrument de comparaison temporelle des graphes d'argumentation : critère d'inclusion écrit d'un énoncé, graphe AIF conforme sérialisé en RDF, projection Dung, deux mesures d'écart de familles différentes, plancher de bruit (contrôle négatif) publié avec chaque mesure, et contrôle positif dont la magnitude attendue est dérivée à la main avant l'exécution | 45 min | @@ -521,7 +519,6 @@ en référence. | `Argument_Analysis_Schemes_Walton.ipynb` | Schémas d'argumentation de Walton (10 schémas stéréotypés, questions critiques) et classifieur lexical déterministe | aucun (indépendant ; niveau intermédiaire entre `Toulmin_Model` et `Dung_AF_Semantics`, cités en contexte) | | `Argument_Analysis_Dialogues_Protocolises.ipynb` | Protocoles Walton–Krabbe : inquiry/persuasion comme machines à états sur actes de parole | aucun (indépendant ; voisin de Toulmin_Model par la structure de l'échange, des Agentic par la multi-agentique) | | `Argument_Analysis_Knowledge_Base.ipynb` | Mémoire d'un débat : population transitive des propositions, requêtes support/attaque par négation lexicale `¬` | aucun (indépendant ; complète `Toulmin_Model`, `Schemes_Walton` et `Dialogues_Protocolises`, cités en contexte) | -| `Argument_Analysis_Gouvernance_Multi_Agents.ipynb` | Gouvernance multi-agents (distillation EPITA 2.1.6, #4960) — vote/consensus/coalitions/manipulation | aucun (indépendant ; voisin d'`Agentic-3-orchestration` par la question — décider à plusieurs vs orchestrer le travail — pas par le code) | | `Argument_Analysis_Ranking_Semantics.ipynb` | Sémantiques graduées (h-Categoriser, fardeau) | `Dung_AF_Semantics` | | `Argument_Analysis_Gouvernance_Multi_Agents.ipynb` | Gouvernance multi-agents (distillation EPITA 2.1.6, #4960) — vote/consensus/coalitions/manipulation | aucun (indépendant ; voisin d'`Agentic-3-orchestration` par la question — décider à plusieurs vs orchestrer le travail — pas par le code) | | `Argument_Analysis_Dated_Graphs.ipynb` | Instrument $G_t^{arg} \to G_{t+1}^{arg}$ (Epic #13303, issue #13310) | `Dung_AF_Semantics` | From e21befa333d217ce5cead1c62d85456c74f85828 Mon Sep 17 00:00:00 2001 From: jsboige Date: Fri, 25 Sep 2026 10:37:23 +0200 Subject: [PATCH 7/8] =?UTF-8?q?refactor(4960,#17353):=20re-fonder=20gouver?= =?UTF-8?q?nance=20sur=20le=20coeur=20EPITA=20=E2=80=94=20correction=20#19?= =?UTF-8?q?81,=2015=20algorithmes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-fondation (option 1 ai-01) : provenance tronc argumentation_analysis/agents/core/governance/ + sas docs/coursia_contrib/governance_voting_methods.ipynb, pas le projet etudiant fige 2.1.6. - governance_methods.py re-ecrit : 5 scrutins + 2 protocoles + 8 fonctions de choix social (approval/STV/Copeland/Kemeny-Young+safe/Schulze/condorcet_winner/pairwise_matrix) = 15 algorithmes ; correction de l'erreur de categorie #1981 (« 7 methodes de vote » est faux — byzantin/raft sont des protocoles de tolerance aux pannes, pas des scrutins) ; Agent avec personalites/confiance/Q-learning + BDI/Reactive du tronc ; deltas tronc<->prototype documentes (#2576 metriques honnetes, #971 Kemeny safe, quadratic defaut conserve) - tests/test_governance_methods.py : 24 tests (categories 5+2+8=15, scrutins, protocoles, choix social, Agent) — 24/24 passent - notebook re-ecrit : 34 cellules, 12 code, structure sas (scrutins/duels/choix social/protocoles/ metriques/manipulation/limites/exercices) ; exec reelle papermill python3, 12/12, 0 erreur - README : « 7 methodes » purge (3 lignes), doctrine 15 algorithmes + #1981 Mesure firsthand : Pizza gagne la majorite mais perd tous ses duels (matrice pairwise) ; manipulation Borda se retourne (Burger -> Sushi) ; consensus byzantin tue le vainqueur majoritaire (Pizza 10/10 -> 1/10 a ratio 0.45), pas le vainqueur de compromis. Co-Authored-By: Claude Sonnet 5 --- ...nt_Analysis_Gouvernance_Multi_Agents.ipynb | 1160 ++++++++--------- .../SymbolicAI/Argument_Analysis/README.md | 6 +- .../Argument_Analysis/governance_methods.py | 806 ++++++------ .../tests/test_governance_methods.py | 277 ++-- 4 files changed, 1135 insertions(+), 1114 deletions(-) diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb index c43aa13a10..ca82221d0d 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argument_Analysis_Gouvernance_Multi_Agents.ipynb @@ -2,79 +2,72 @@ "cells": [ { "cell_type": "markdown", - "id": "gov-01", + "id": "g17353-01", "metadata": { "papermill": { - "duration": 0.003599, - "end_time": "2026-09-22T10:12:09.414183+00:00", + "duration": 0.006524, + "end_time": "2026-09-25T08:36:39.769205+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.410584+00:00", + "start_time": "2026-09-25T08:36:39.762681+00:00", "status": "completed" }, "tags": [] }, "source": [ - "# Gouvernance multi-agents — voter, coalitionner, résister\n", + "# Gouvernance multi-agents : scrutins, protocoles, choix social\n", "\n", - "Distillation du sous-projet EPITA `2.1.6_multiagent_governance_prototype`\n", - "(EPIC #4960, mandat Triple Distillation : porter l'essence vivante « sans le\n", - "bruit d'une année de régressions et d'itérations »).\n", + "> **[DISTILLATION / PÉDAGOGIE]** Comment un collectif d'agents décide-t-il ? Ce notebook distille la **consolidation du cœur EPITA** (tronc `argumentation_analysis/agents/core/governance/` + carnet sas `docs/coursia_contrib/governance_voting_methods.ipynb`) — *pas* le projet étudiant figé de juin 2025, dont le cœur a corrigé la doctrine.\n", "\n", - "**Position dans l'arc** — [Agentic-3-orchestration](Argument_Analysis_Agentic-3-orchestration.ipynb)\n", - "répond à « comment répartir le *travail* entre agents » (pipeline DAG,\n", - "orchestrateur conversationnel) ; ce notebook répond à « comment un collectif\n", - "*décide* » : sept méthodes de vote et de consensus, des personnalités d'agents,\n", - "des coalitions, et la question centrale de toute gouvernance — **que résiste\n", - "la décision à la manipulation ?**\n", + "Ce notebook est la re-fondation de [PR #17353](https://github.com/jsboige/CoursIA/pull/17353) (sous-grain 2 de l'EPIC #4960) sur le périmètre fixé le 22/09 : on distille les consolidations faites dans le cœur, pas les projets étudiants. Généalogie : `2.1.6_multiagent_governance_prototype` (le tronc en est « Adapted from » et l'a corrigé — deltas documentés dans l'organe).\n", "\n", - "L'organe `governance_methods.py` est un module **pur** (stdlib uniquement) :\n", - "les mécanismes sont calculables et testables sans aucune dépendance." + "## Une erreur de catégorie à défaire d'entrée : « 7 méthodes de vote » (#1981)\n", + "\n", + "Le tronc l'interdit nommément : la surface consolidée n'est **pas** « 7 méthodes de vote ». Elle vaut **15 algorithmes** en trois familles qui ne se comparent pas entre elles :\n", + "\n", + "| Famille | Contenu | Question posée |\n", + "|---|---|---|\n", + "| **5 scrutins** | majority, plurality, borda, condorcet, quadratic | « quelle option agrège le mieux des préférences sincères ? » |\n", + "| **2 protocoles** | byzantine, raft | « comment décider quand une fraction des agents est défaillante ? » |\n", + "| **8 fonctions de choix social** | approval, stv, copeland, kemeny_young(+safe), schulze, condorcet_winner, pairwise_matrix | « quel *classement collectif* minimise le désaccord sur un profil ? » |\n", + "\n", + "Un byzantin ou un Raft **ne se comparent pas à Borda** : ce sont des protocoles de tolérance aux pannes, pas des règles d'agrégation. Les confondre est l'erreur #1981, que le projet étudiant commettait et que le cœur a corrigée." ] }, { "cell_type": "markdown", - "id": "gov-02", + "id": "g17353-02", "metadata": { "papermill": { - "duration": 0.002641, - "end_time": "2026-09-22T10:12:09.419840+00:00", + "duration": 0.004078, + "end_time": "2026-09-25T08:36:39.777715+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.417199+00:00", + "start_time": "2026-09-25T08:36:39.773637+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Pourquoi la gouvernance est un problème dur\n", + "## §1 — L'organe et le scénario du club\n", "\n", - "Répartir le travail entre agents (Agentic-3) se résout par de l'ingénierie ;\n", - "**décider à plusieurs** se heurte à un résultat d'impossibilité. Dès qu'un\n", - "collectif doit choisir parmi trois options ou plus à partir de préférences\n", - "individuelles ordonnées, le théorème d'Arrow (1951) interdit toute procédure\n", - "qui serait à la fois non-dictatoriale, unanime (si tous préfèrent X à Y, X\n", - "bat Y) et indépendante aux alternatives non pertinentes (le duel X vs Y ne\n", - "doit pas dépendre de la présence de Z). Il faut donc **choisir quelle\n", - "propriété sacrifier** — et chaque méthode de vote de ce notebook est un choix\n", - "différent de sacrifice. La suite ne débat pas de ces choix : elle les\n", - "**mesure** sur des scénarios où ils produisent des vainqueurs différents." + "L'organe `governance_methods.py` (stdlib pure, déterministe) porte les 15 algorithmes + la classe `Agent` (personnalités, confiance, Q-learning) du tronc. Chargeons-le et vérifions la catégorisation." ] }, { "cell_type": "code", "execution_count": 1, - "id": "gov-03", + "id": "g17353-03", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.426414Z", - "iopub.status.busy": "2026-09-22T10:12:09.426200Z", - "iopub.status.idle": "2026-09-22T10:12:09.435303Z", - "shell.execute_reply": "2026-09-22T10:12:09.434655Z" + "iopub.execute_input": "2026-09-25T08:36:39.793646Z", + "iopub.status.busy": "2026-09-25T08:36:39.792011Z", + "iopub.status.idle": "2026-09-25T08:36:39.838959Z", + "shell.execute_reply": "2026-09-25T08:36:39.837777Z" }, "papermill": { - "duration": 0.0134, - "end_time": "2026-09-22T10:12:09.435904+00:00", + "duration": 0.058167, + "end_time": "2026-09-25T08:36:39.840148+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.422504+00:00", + "start_time": "2026-09-25T08:36:39.781981+00:00", "status": "completed" }, "tags": [] @@ -84,79 +77,99 @@ "name": "stdout", "output_type": "stream", "text": [ - "{\n", - " \"methods\": 7,\n", - " \"method_keys\": [\n", - " \"borda\",\n", - " \"byzantine\",\n", - " \"condorcet\",\n", - " \"majority\",\n", - " \"plurality\",\n", - " \"quadratic\",\n", - " \"raft\"\n", - " ],\n", - " \"scenarios\": 6,\n", - " \"scenario_names\": [\n", - " \"byzantine_noise\",\n", - " \"cyclic_majority\",\n", - " \"dictatorship\",\n", - " \"project_funding\",\n", - " \"spoiler_candidate\",\n", - " \"strategic_bloc\"\n", - " ]\n", - "}\n" + "SCRUTINS (5) : ['borda', 'condorcet', 'majority', 'plurality', 'quadratic']\n", + "PROTOCOLES (2) : ['byzantine', 'raft']\n", + "CHOIX SOCIAL (8): ['approval', 'condorcet_winner', 'copeland', 'kemeny_young', 'kemeny_young_safe', 'pairwise_matrix', 'schulze', 'stv']\n", + "\n", + "Total = 15 algorithmes (pas « 7 methodes » — #1981 corrige)\n", + "\n", + "Scenario : 7 membres, 4 destinations\n" ] } ], "source": [ - "import sys\n", + "import sys, random\n", "sys.path.insert(0, \".\") # organe governance_methods.py dans le dossier de la serie\n", "import governance_methods as g\n", - "import json\n", "\n", - "counts = g.method_counts()\n", - "print(json.dumps(counts, ensure_ascii=False, indent=1))" + "print(\"SCRUTINS (5) :\", sorted(g.SCRUTINS))\n", + "print(\"PROTOCOLES (2) :\", sorted(g.PROTOCOLES))\n", + "print(\"CHOIX SOCIAL (8):\", sorted(g.SOCIAL_CHOICE))\n", + "total = len(g.SCRUTINS) + len(g.PROTOCOLES) + len(g.SOCIAL_CHOICE)\n", + "print(f\"\\nTotal = {total} algorithmes (pas « 7 methodes » — #1981 corrige)\")\n", + "\n", + "# Le scenario du carnet sas : le repas de fin d'annee du club de robotique.\n", + "# Sept membres, une destination a choisir, des preferences ordonnees.\n", + "OPTIONS = [\"Pizza\", \"Burger\", \"Sushi\", \"Raclette\"]\n", + "PROFIL_PREFS = [\n", + " (\"Alice\", [\"Pizza\", \"Sushi\", \"Burger\", \"Raclette\"]),\n", + " (\"Bruno\", [\"Pizza\", \"Raclette\", \"Sushi\", \"Burger\"]),\n", + " (\"Chloe\", [\"Pizza\", \"Burger\", \"Sushi\", \"Raclette\"]),\n", + " (\"Diego\", [\"Burger\", \"Raclette\", \"Sushi\", \"Pizza\"]),\n", + " (\"Emma\", [\"Burger\", \"Sushi\", \"Raclette\", \"Pizza\"]),\n", + " (\"Farid\", [\"Sushi\", \"Burger\", \"Raclette\", \"Pizza\"]),\n", + " (\"Gwen\", [\"Raclette\", \"Burger\", \"Sushi\", \"Pizza\"]),\n", + "]\n", + "def build_agents(personality=\"stubborn\", seed=0):\n", + " return [g.Agent(n, personality, p, rng=random.Random(seed + i))\n", + " for i, (n, p) in enumerate(PROFIL_PREFS)]\n", + "print(f\"\\nScenario : {len(PROFIL_PREFS)} membres, {len(OPTIONS)} destinations\")" ] }, { "cell_type": "markdown", - "id": "gov-04", + "id": "g17353-04", "metadata": { "papermill": { - "duration": 0.002657, - "end_time": "2026-09-22T10:12:09.441351+00:00", + "duration": 0.003544, + "end_time": "2026-09-25T08:36:39.847471+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.438694+00:00", + "start_time": "2026-09-25T08:36:39.843927+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Lecture du compte\n", + "### Lecture du compte : 15, pas 7\n", "\n", - "L'organe porte **7 méthodes** de vote/consensus et **6 scénarios étiquetés**\n", - "(distillés du banc de 16 du source — les 6 retenus couvrent chacun un phénomène\n", - "de choix collectif distinct : dictature, cycle, candidat spoiler, bloc\n", - "discipliné, bruit byzantin, arbitrage budgétaire). Les tests purs\n", - "(`tests/test_governance_methods.py`, 16 tests) figent ces comptes." + "L'organe affiche ses trois familles séparément — la catégorisation est structurelle (trois dictionnaires distincts), pas un commentaire. Le scénario est celui du carnet sas : 7 membres aux préférences hétérogènes. La Pizza a 3 premières places sur 7 — elle gagne à la majorité simple. Mais est-ce le « bon » choix collectif ?" + ] + }, + { + "cell_type": "markdown", + "id": "g17353-05", + "metadata": { + "papermill": { + "duration": 0.003471, + "end_time": "2026-09-25T08:36:39.854503+00:00", + "exception": false, + "start_time": "2026-09-25T08:36:39.851032+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## §2 — Les 5 scrutins : une même société, cinq vainqueurs possibles\n", + "\n", + "Exécutons les 5 scrutins sur le même profil. C'est la première leçon du carnet sas." ] }, { "cell_type": "code", "execution_count": 2, - "id": "gov-05", + "id": "g17353-06", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.448150Z", - "iopub.status.busy": "2026-09-22T10:12:09.447905Z", - "iopub.status.idle": "2026-09-22T10:12:09.452057Z", - "shell.execute_reply": "2026-09-22T10:12:09.451495Z" + "iopub.execute_input": "2026-09-25T08:36:39.867442Z", + "iopub.status.busy": "2026-09-25T08:36:39.866843Z", + "iopub.status.idle": "2026-09-25T08:36:39.876573Z", + "shell.execute_reply": "2026-09-25T08:36:39.876005Z" }, "papermill": { - "duration": 0.008378, - "end_time": "2026-09-22T10:12:09.452537+00:00", + "duration": 0.0183, + "end_time": "2026-09-25T08:36:39.877768+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.444159+00:00", + "start_time": "2026-09-25T08:36:39.859468+00:00", "status": "completed" }, "tags": [] @@ -166,72 +179,72 @@ "name": "stdout", "output_type": "stream", "text": [ - "--- cyclic_majority : Cycle de Condorcet : A bat B, B bat C, C bat A — aucun vainqueur net.\n", - " V1 [stubborn ] ['A', 'B', 'C']\n", - " V2 [stubborn ] ['B', 'C', 'A']\n", - " V3 [stubborn ] ['C', 'A', 'B']\n", - " options : ['A', 'B', 'C']\n", - "--- spoiler_candidate : Un candidat similaire divise un bloc : l'adversaire l'emporte.\n", - " G1 [stubborn ] ['Gauche', 'Centre', 'Droite']\n", - " G2 [stubborn ] ['Centre', 'Gauche', 'Droite']\n", - " D1 [stubborn ] ['Droite', 'Centre', 'Gauche']\n", - " options : ['Gauche', 'Centre', 'Droite']\n" + "Scrutin Vainqueur \n", + "------------------------\n", + "majority Pizza \n", + "plurality Pizza \n", + "borda Burger \n", + "condorcet Burger \n", + "quadratic Pizza \n", + "\n", + "2 vainqueur(s) distinct(s) selon le scrutin : ['Burger', 'Pizza']\n", + "\n", + "Scores de Borda : {'Burger': 13, 'Sushi': 11, 'Pizza': 9, 'Raclette': 9}\n" ] } ], "source": [ - "# Anatomie de deux scénarios : le cycle et le spoiler\n", - "for name in [\"cyclic_majority\", \"spoiler_candidate\"]:\n", - " sc = g.SCENARIOS[name]\n", - " print(f\"--- {name} : {sc['description']}\")\n", - " for agent, perso, prefs in sc[\"agents\"]:\n", - " print(f\" {agent:<4} [{perso:<9}] {prefs}\")\n", - " print(f\" options : {sc['options']}\")" + "resultats = {nom: fn(build_agents(), OPTIONS) for nom, fn in g.SCRUTINS.items()}\n", + "print(f\"{'Scrutin':<12} {'Vainqueur':<10}\")\n", + "print(\"-\" * 24)\n", + "for nom, gagnant in resultats.items():\n", + " print(f\"{nom:<12} {gagnant:<10}\")\n", + "print(f\"\\n{len(set(resultats.values()))} vainqueur(s) distinct(s) selon le scrutin : \"\n", + " f\"{sorted(set(resultats.values()))}\")\n", + "\n", + "# Detail Borda : le score de chaque destination.\n", + "scores = {o: 0 for o in OPTIONS}\n", + "for _, p in PROFIL_PREFS:\n", + " for i, o in enumerate(p):\n", + " scores[o] += len(OPTIONS) - i - 1\n", + "print(\"\\nScores de Borda :\", dict(sorted(scores.items(), key=lambda kv: -kv[1])))" ] }, { "cell_type": "markdown", - "id": "gov-06", + "id": "g17353-07", "metadata": { "papermill": { - "duration": 0.002782, - "end_time": "2026-09-22T10:12:09.458113+00:00", + "duration": 0.004335, + "end_time": "2026-09-25T08:36:39.886610+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.455331+00:00", + "start_time": "2026-09-25T08:36:39.882275+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Lecture : préférences ordonnées et personnalités\n", + "### Lecture : la majorité et Borda ne disent pas la même chose\n", "\n", - "Chaque agent porte une **préférence complète** (un ordre, pas un seul choix)\n", - "et une **personnalité** qui gouverne son vote sincère : `stubborn` vote son\n", - "premier choix quoi qu'il arrive, `flexible` peut suivre un indice de majorité,\n", - "`strategic` vote son second choix pour bloquer un adversaire, `random` tire au\n", - "sort. Toute la mécanique de vote se lit sur ces ordres : les méthodes de ce\n", - "notebook ne voient jamais la personnalité, elles ne voient que ce qu'elle\n", - "produit — des préférences déclarées. C'est exactement la surface d'attaque des\n", - "manipulations de fin de parcours : mentir sur ses préférences est\n", - "indistinguable d'avoir d'autres préférences." + "La Pizza gagne à la **majorité** (3 premières places), mais le score de Borda raconte une autre histoire : les votes « en profondeur » (2e, 3e places) favorisent une option de compromis. Le scrutin choisit le vainqueur autant que les électeurs — c'est le théorème d'Arrow rendu visible sur 7 personnes." ] }, { "cell_type": "code", "execution_count": 3, - "id": "gov-07", + "id": "g17353-08", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.464328Z", - "iopub.status.busy": "2026-09-22T10:12:09.464152Z", - "iopub.status.idle": "2026-09-22T10:12:09.469157Z", - "shell.execute_reply": "2026-09-22T10:12:09.468583Z" + "iopub.execute_input": "2026-09-25T08:36:39.897705Z", + "iopub.status.busy": "2026-09-25T08:36:39.897406Z", + "iopub.status.idle": "2026-09-25T08:36:39.903981Z", + "shell.execute_reply": "2026-09-25T08:36:39.901793Z" }, "papermill": { - "duration": 0.008919, - "end_time": "2026-09-22T10:12:09.469613+00:00", + "duration": 0.014429, + "end_time": "2026-09-25T08:36:39.904896+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.460694+00:00", + "start_time": "2026-09-25T08:36:39.890467+00:00", "status": "completed" }, "tags": [] @@ -241,80 +254,91 @@ "name": "stdout", "output_type": "stream", "text": [ - "Duels :\n", - " Gauche vs Centre : 1-2\n", - " Gauche vs Droite : 2-1\n", - " Centre vs Droite : 2-1\n", - "majority -> Gauche\n", - "borda -> Centre\n", - "condorcet -> Centre\n" + "Duels pairwise (ligne bat colonne) :\n", + " Pizza Burger Sushi Raclet\n", + " Pizza -- 3 3 3\n", + " Burger 4 -- 4 5\n", + " Sushi 4 3 -- 4\n", + " Raclette 4 2 3 --\n", + "\n", + "Vainqueur de Condorcet sur ce profil : Burger\n" ] } ], "source": [ - "# EFFET SPOILER : la même société, trois lectures du vainqueur\n", - "agents = g.build_agents(\"spoiler_candidate\")\n", - "opts = g.SCENARIOS[\"spoiler_candidate\"][\"options\"]\n", + "# Duels pairwise : la Pizza gagne la majorite mais perd TOUS ses duels ?\n", + "print(\"Duels pairwise (ligne bat colonne) :\")\n", + "print(f\"{'':>10}\", \" \".join(f\"{o[:6]:>7}\" for o in OPTIONS))\n", + "for a in OPTIONS:\n", + " row = []\n", + " for b in OPTIONS:\n", + " if a == b:\n", + " row.append(\" --\")\n", + " else:\n", + " w = sum(p.index(a) < p.index(b) for _, p in PROFIL_PREFS)\n", + " row.append(f\"{w:>7}\")\n", + " print(f\"{a:>10}\", \" \".join(row))\n", "\n", - "# Duels pairwise (coeur de Condorcet)\n", - "print(\"Duels :\")\n", - "for i, o1 in enumerate(opts):\n", - " for o2 in opts[i+1:]:\n", - " v1 = sum(a.preferences.index(o1) < a.preferences.index(o2) for a in agents)\n", - " v2 = len(agents) - v1\n", - " print(f\" {o1} vs {o2} : {v1}-{v2}\")\n", + "# Vainqueur de Condorcet (gagne tous ses duels) sur ce profil de bulletins.\n", + "ballots = [p for _, p in PROFIL_PREFS]\n", + "cw = g.condorcet_winner(ballots, OPTIONS)\n", + "print(f\"\\nVainqueur de Condorcet sur ce profil : {cw}\")" + ] + }, + { + "cell_type": "markdown", + "id": "g17353-09", + "metadata": { + "papermill": { + "duration": 0.004386, + "end_time": "2026-09-25T08:36:39.913623+00:00", + "exception": false, + "start_time": "2026-09-25T08:36:39.909237+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "### Lecture : le paradoxe du compromis\n", "\n", - "for m in [\"majority\", \"borda\", \"condorcet\"]:\n", - " winner = g.simulate_vote(g.build_agents(\"spoiler_candidate\"), opts, m, seed=0)[\"winner\"]\n", - " print(f\"{m:<10} -> {winner}\")" + "La Pizza gagne la majorité mais **perd tous ses duels pairwise** : une majorité de membres préfère chacune des trois autres destinations à la Pizza. Le vainqueur de Condorcet (celui qui gagne tous ses duels) existe-t-il ici ? La matrice le dit directement. C'est la leçon n° 1 du carnet sas : le scrutin majoritaire peut élire une option qu'une majorité de membres classe derrière chaque concurrente." ] }, { "cell_type": "markdown", - "id": "gov-08", + "id": "g17353-10", "metadata": { "papermill": { - "duration": 0.00277, - "end_time": "2026-09-22T10:12:09.475303+00:00", + "duration": 0.004104, + "end_time": "2026-09-25T08:36:39.921995+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.472533+00:00", + "start_time": "2026-09-25T08:36:39.917891+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Lecture : le spoiler mesuré\n", + "## §3 — Le choix social formel : au-delà du vainqueur, le classement\n", "\n", - "Les duels racontent tout : **Centre bat Gauche (2-1) et bat Droite (2-1)** —\n", - "Centre est le *vainqueur de Condorcet*, il gagne contre tout le monde. Pourtant\n", - "la **majorité à un tour élit Gauche** : G1 vote Gauche, G2 vote Centre, D1 vote\n", - "Droite — le bloc progressiste (2 électeurs sur 3) est divisé par la présence de\n", - "deux options voisines, et l'adversaire minoritaire l'emporte avec 1 voix sur 3.\n", - "Borda et Condorcet, qui lisent les préférences **complètes**, élisent Centre.\n", - "C'est l'effet spoiler — l'une des deux briques empiriques du théorème\n", - "d'impossibilité d'Arrow. Le décompte à un tour : Gauche 1 voix (G1), Centre\n", - "1 voix (G2), Droite 1 voix (D1) — trois voix, aucune majorité, et le tie-break\n", - "de `Counter.most_common` tranche pour la première insérée : l'égalité parfaite\n", - "à un tour est elle-même une convention silencieuse, invisible dans le résultat\n", - "affiché." + "Les scrutins donnent un gagnant. Le choix social formel (couche absente du projet étudiant — delta n° 4) donne un **classement collectif** ou une mesure de désaccord. Approval, STV, Copeland, Kemeny-Young, Schulze." ] }, { "cell_type": "code", "execution_count": 4, - "id": "gov-09", + "id": "g17353-11", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.482674Z", - "iopub.status.busy": "2026-09-22T10:12:09.482474Z", - "iopub.status.idle": "2026-09-22T10:12:09.487615Z", - "shell.execute_reply": "2026-09-22T10:12:09.486862Z" + "iopub.execute_input": "2026-09-25T08:36:39.934391Z", + "iopub.status.busy": "2026-09-25T08:36:39.933999Z", + "iopub.status.idle": "2026-09-25T08:36:39.939109Z", + "shell.execute_reply": "2026-09-25T08:36:39.938562Z" }, "papermill": { - "duration": 0.009423, - "end_time": "2026-09-22T10:12:09.488241+00:00", + "duration": 0.013754, + "end_time": "2026-09-25T08:36:39.940210+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.478818+00:00", + "start_time": "2026-09-25T08:36:39.926456+00:00", "status": "completed" }, "tags": [] @@ -324,75 +348,64 @@ "name": "stdout", "output_type": "stream", "text": [ - " A vs B : 2-1\n", - " A vs C : 1-2\n", - " B vs C : 2-1\n", - "vainqueur condorcet : A (repli Borda)\n", - "scores Borda : {'A': 3, 'B': 3, 'C': 3}\n" + "Approval (top-2 approuves) : Burger — comptes {'Pizza': 3, 'Burger': 5, 'Sushi': 3, 'Raclette': 3}\n", + "STV (1 siege, quota Droop) : ['Burger'] — 3 tour(s)\n", + "Copeland (duels +-) : Burger — scores {'Pizza': -3, 'Burger': 3, 'Sushi': 1, 'Raclette': -1}\n", + "Schulze (beatpath) : Burger\n" ] } ], "source": [ - "# CYCLE DE CONDORCET : A bat B, B bat C, C bat A\n", - "agents = g.build_agents(\"cyclic_majority\")\n", - "opts = g.SCENARIOS[\"cyclic_majority\"][\"options\"]\n", - "for i, o1 in enumerate(opts):\n", - " for o2 in opts[i+1:]:\n", - " v1 = sum(a.preferences.index(o1) < a.preferences.index(o2) for a in agents)\n", - " print(f\" {o1} vs {o2} : {v1}-{len(agents)-v1}\")\n", - "r = g.simulate_vote(agents, opts, \"condorcet\", seed=0)\n", - "print(\"vainqueur condorcet :\", r[\"winner\"], \"(repli Borda)\")\n", + "ballots = [p for _, p in PROFIL_PREFS]\n", + "\n", + "w_app, counts_app = g.approval_voting(ballots, OPTIONS, approval_threshold=2)\n", + "print(f\"Approval (top-2 approuves) : {w_app} — comptes {counts_app}\")\n", "\n", - "# Scores Borda explicites : l'egalite parfaite\n", - "scores = {o: 0 for o in opts}\n", - "for a in agents:\n", - " for i, o in enumerate(a.preferences):\n", - " scores[o] += len(opts) - i - 1\n", - "print(\"scores Borda :\", scores)" + "winners_stv, rounds_stv = g.stv(ballots, OPTIONS, seats=1)\n", + "print(f\"STV (1 siege, quota Droop) : {winners_stv} — {len(rounds_stv)} tour(s)\")\n", + "\n", + "w_cop, scores_cop = g.copeland(ballots, OPTIONS)\n", + "print(f\"Copeland (duels +-) : {w_cop} — scores {scores_cop}\")\n", + "\n", + "w_sch, paths_sch = g.schulze(ballots, OPTIONS)\n", + "print(f\"Schulze (beatpath) : {w_sch}\")" ] }, { "cell_type": "markdown", - "id": "gov-10", + "id": "g17353-12", "metadata": { "papermill": { - "duration": 0.00276, - "end_time": "2026-09-22T10:12:09.493799+00:00", + "duration": 0.003945, + "end_time": "2026-09-25T08:36:39.948417+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.491039+00:00", + "start_time": "2026-09-25T08:36:39.944472+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Lecture : le cycle et un repli qui ne résout rien\n", + "### Lecture : quatre méthodes formelles, et le consensus émerge\n", "\n", - "A bat B (2-1), B bat C (2-1), C bat A (2-1) : **cycle parfait**, aucun vainqueur\n", - "de Condorcet. Le repli Borda du source rend une **égalité parfaite 3 = 3 = 3** —\n", - "trois préférences strictes en rotation donnent exactement autant de points à\n", - "chacune. Le `A` final ne sort d'aucune préférence collective : il sort du\n", - "tie-break de `max()`, qui rend la **première clé d'insertion**. Le repli ne\n", - "résout pas le cycle, il le tranche par convention d'ordre — c'est la divergence\n", - "6, mesurée ici plutôt qu'affirmée (l'exercice 3 mesure ce que raft, qui tire son\n", - "leader, fait du même cycle)." + "Sur ce profil, les méthodes de choix social convergent vers l'option de compromis — celle que la majorité simple avait éliminée. STV transfère les voix des éliminés ; Copeland compte les duels nets ; Schulze suit les plus forts chemins. Le vote quadratique et ces méthodes mesurent l'**intensité** ou la **structure** des préférences, pas seulement le premier choix." ] }, { "cell_type": "code", "execution_count": 5, - "id": "gov-11", + "id": "g17353-13", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.500863Z", - "iopub.status.busy": "2026-09-22T10:12:09.500630Z", - "iopub.status.idle": "2026-09-22T10:12:09.507894Z", - "shell.execute_reply": "2026-09-22T10:12:09.507310Z" + "iopub.execute_input": "2026-09-25T08:36:39.960812Z", + "iopub.status.busy": "2026-09-25T08:36:39.960145Z", + "iopub.status.idle": "2026-09-25T08:36:39.965724Z", + "shell.execute_reply": "2026-09-25T08:36:39.965075Z" }, "papermill": { - "duration": 0.011396, - "end_time": "2026-09-22T10:12:09.508359+00:00", + "duration": 0.01322, + "end_time": "2026-09-25T08:36:39.966411+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.496963+00:00", + "start_time": "2026-09-25T08:36:39.953191+00:00", "status": "completed" }, "tags": [] @@ -402,121 +415,84 @@ "name": "stdout", "output_type": "stream", "text": [ - "scenario borda byzantine condorcet majority plurality quadratic raft \n", - "-----------------------------------------------------------------------------------------------\n", - "byzantine_noise A A A A A A A \n", - "cyclic_majority A A A A A A C \n", - "dictatorship B C B C C B B \n", - "project_funding Ecole Ecole Ecole Ecole Ecole Ecole Ecole \n", - "spoiler_candidate Centre Gauche Centre Gauche Gauche Gauche Centre \n", - "strategic_bloc A A A A A A A \n" + "Classement Kemeny-Young optimal : Burger > Sushi > Raclette > Pizza\n", + "Score (somme des preferences pairwise respectees) : 25\n", + "\n", + "Sur 4 options : approx = False (exact ici, <= 8)\n", + "Sur 10 options : ValueError levee -> Kemeny-Young impraticable pour 10 candidats (max 8) — utilis...\n" ] } ], "source": [ - "# Tableau croise : le vainqueur selon la methode et le scenario\n", - "noms = counts[\"scenario_names\"]\n", - "entete = f\"{'scenario':<18}\" + \"\".join(f\"{m:<11}\" for m in counts[\"method_keys\"])\n", - "print(entete)\n", - "print(\"-\" * len(entete))\n", - "for sc in noms:\n", - " opts = g.SCENARIOS[sc][\"options\"]\n", - " ligne = f\"{sc:<18}\"\n", - " for m in counts[\"method_keys\"]:\n", - " w = g.simulate_vote(g.build_agents(sc), opts, m, seed=42)[\"winner\"]\n", - " ligne += f\"{w:<11}\"\n", - " print(ligne)" + "# Kemeny-Young : le classement qui minimise le desaccord total (O(n!), <= 8 options).\n", + "ranking, score = g.kemeny_young(ballots, OPTIONS)\n", + "print(f\"Classement Kemeny-Young optimal : {' > '.join(ranking)}\")\n", + "print(f\"Score (somme des preferences pairwise respectees) : {score}\")\n", + "\n", + "# Au-dela de 8 options : le wrapper safe rebascule sur Copeland (#971).\n", + "many = [f\"opt{i}\" for i in range(10)]\n", + "rank_safe, score_safe, approx = g.kemeny_young_safe(ballots, OPTIONS)\n", + "print(f\"\\nSur {len(OPTIONS)} options : approx = {approx} (exact ici, <= 8)\")\n", + "try:\n", + " g.kemeny_young(ballots, many)\n", + "except ValueError as e:\n", + " print(f\"Sur 10 options : ValueError levee -> {str(e)[:60]}...\")" ] }, { "cell_type": "markdown", - "id": "gov-12", + "id": "g17353-14", "metadata": { "papermill": { - "duration": 0.002803, - "end_time": "2026-09-22T10:12:09.514051+00:00", + "duration": 0.004099, + "end_time": "2026-09-25T08:36:39.974960+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.511248+00:00", + "start_time": "2026-09-25T08:36:39.970861+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Lecture : la méthode change le vainqueur\n", + "### Lecture : le classement optimal a un coût combinatoire\n", "\n", - "Sur six scénarios, trois rendent le même gagnant sous les sept méthodes\n", - "(`byzantine_noise`, `project_funding`, `strategic_bloc` : une dominance claire\n", - "absorbe le choix de procédure) et **trois divergent** :\n", - "\n", - "- `dictatorship` : majorité/pluralité/byzantin élisent **C**, Borda/Condorcet/\n", - " quadratique/Raft élisent **B** — le « dictateur » D1 (stratégique) vote son\n", - " *second* choix, et la lecture complète des préférences préfère son sacrifice ;\n", - "- `spoiler_candidate` : majorité et dérivées élisent **Gauche**, les méthodes\n", - " à préférences complètes élisent **Centre** ;\n", - "- `cyclic_majority` : seul **Raft déraille** (C) — son leader tiré au sort\n", - " propose depuis n'importe quelle préférence.\n", - "\n", - "Il n'existe pas de procédure neutre : changer la règle, c'est changer le\n", - "gagnant — sur un tiers de ce banc, sans toucher une seule préférence. Note de\n", - "lecture : les colonnes `majority` et `plurality` sont identiques par\n", - "construction (le source définit la pluralité comme un simple rappel de la\n", - "majorité — les tests purs figent cet alias), et la colonne `byzantine` à ratio\n", - "par défaut 0.2 suit la majorité sauf sur `dictatorship`, où son remplacement\n", - "du premier agent déplace exactement la voix qui faisait basculer C. Les trois\n", - "scénarios convergents montrent l'autre face : quand une dominance est claire\n", - "(bloc de 3 sur 5, option-Condorcet évidente), **toutes** les procédures\n", - "raisonnables l'expriment — la méthode ne devient décisive que près de\n", - "l'égalité, c'est-à-dire précisément là où elle est indécidable en principe." + "Kemeny-Young donne le classement qui maximise le nombre de préférences pairwise respectées — mais en O(n!), impraticable au-delà de 8 options. Le wrapper `safe` le nomme honnêtement : au-delà, il rend une approximation Copeland et le dit (`approx=True`). C'est le fix d'honnêteté du cœur (#971) : une méthode qui ne peut pas calculer exact doit le dire, pas rendre un chiffre silencieux." ] }, { "cell_type": "markdown", - "id": "gov-13", + "id": "g17353-15", "metadata": { "papermill": { - "duration": 0.002749, - "end_time": "2026-09-22T10:12:09.519676+00:00", + "duration": 0.004653, + "end_time": "2026-09-25T08:36:39.983536+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.516927+00:00", + "start_time": "2026-09-25T08:36:39.978883+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Mesurer une décision collective\n", - "\n", - "Dire « méthode plus juste » n'a de sens qu'avec un instrument. L'organe en\n", - "fournit trois, complémentaires :\n", + "## §4 — Quand les agents sont défaillants : les 2 protocoles\n", "\n", - "- **taux de consensus** : fraction des votes alignés sur le vainqueur — un\n", - " collectif déchiré élit quelqu'un que peu ont voté ;\n", - "- **justice** : `1 - Gini(satisfactions)`, où le coefficient de Gini mesure\n", - " l'inégalité d'une distribution (0 = tous égaux, 1 = maximalement inégal) —\n", - " une méthode peut satisfaire beaucoup *en moyenne* en sacrifiant toujours\n", - " les mêmes agents, le Gini le voit ;\n", - "- **satisfaction moyenne** : proximité du vainqueur au premier choix de\n", - " chacun, normalisée par la longueur des préférences.\n", - "\n", - "La cellule suivante croise ces instruments avec les méthodes : c'est là que\n", - "l'effet spoiler devient un chiffre, pas une intuition." + "Les scrutins supposent des votes sincères. Les protocoles de tolérance aux pannes supposent des **agents défaillants** : une fraction vote au hasard (byzantin), ou un leader peut disparaître (Raft). Ce n'est **pas** comparable à Borda (#1981) — c'est une autre question." ] }, { "cell_type": "code", "execution_count": 6, - "id": "gov-14", + "id": "g17353-16", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.526454Z", - "iopub.status.busy": "2026-09-22T10:12:09.526187Z", - "iopub.status.idle": "2026-09-22T10:12:09.531227Z", - "shell.execute_reply": "2026-09-22T10:12:09.530631Z" + "iopub.execute_input": "2026-09-25T08:36:39.995415Z", + "iopub.status.busy": "2026-09-25T08:36:39.994742Z", + "iopub.status.idle": "2026-09-25T08:36:40.058536Z", + "shell.execute_reply": "2026-09-25T08:36:40.057773Z" }, "papermill": { - "duration": 0.009218, - "end_time": "2026-09-22T10:12:09.531706+00:00", + "duration": 0.071632, + "end_time": "2026-09-25T08:36:40.059531+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.522488+00:00", + "start_time": "2026-09-25T08:36:39.987899+00:00", "status": "completed" }, "tags": [] @@ -526,98 +502,68 @@ "name": "stdout", "output_type": "stream", "text": [ - "scenario methode consensus justice satisfaction\n", - "spoiler_candidate majority 0.333 0.556 0.5\n", - "spoiler_candidate borda 0.333 0.833 0.667\n", - "spoiler_candidate condorcet 0.333 0.833 0.667\n", - "spoiler_candidate quadratic 0.333 0.556 0.5\n", - "dictatorship majority 0.6 0.667 0.6\n", - "dictatorship borda 0.6 0.667 0.6\n", - "dictatorship condorcet 0.6 0.667 0.6\n", - "dictatorship quadratic 0.6 0.667 0.6\n" + "Consensus byzantin selon la fraction de votes aleatoires (10 seeds chacune) :\n", + " ratio vainqueurs observes majoritaire survit ?\n", + " 0.00 {'Pizza': 10} oui\n", + " 0.15 {'Pizza': 6, 'Burger': 4} 6/10\n", + " 0.30 {'Raclette': 1, 'Burger': 6, 'Pizza': 2, 'Sushi': 1} 2/10\n", + " 0.45 {'Raclette': 1, 'Burger': 6, 'Pizza': 1, 'Sushi': 2} 1/10\n" ] } ], "source": [ - "# Metriques : justice et satisfaction par methode (spoiler + dictatorship)\n", - "print(f\"{'scenario':<12} {'methode':<10} {'consensus':<10} {'justice':<9} {'satisfaction'}\")\n", - "for sc in [\"spoiler_candidate\", \"dictatorship\"]:\n", - " opts = g.SCENARIOS[sc][\"options\"]\n", - " for m in [\"majority\", \"borda\", \"condorcet\", \"quadratic\"]:\n", - " r = g.simulate_vote(g.build_agents(sc), opts, m, seed=0)\n", - " s = g.summarize(r)\n", - " print(f\"{sc:<12} {m:<10} {s['consensus_rate']:<10} {s['fairness']:<9} {s['satisfaction']}\")" - ] - }, - { - "cell_type": "markdown", - "id": "gov-15", - "metadata": { - "papermill": { - "duration": 0.002675, - "end_time": "2026-09-22T10:12:09.537327+00:00", - "exception": false, - "start_time": "2026-09-22T10:12:09.534652+00:00", - "status": "completed" - }, - "tags": [] - }, - "source": [ - "### Lecture : justice et satisfaction vont ensemble — ici\n", + "import numpy as np\n", "\n", - "Sur le spoiler, les méthodes à préférences complètes dominent sur les deux axes :\n", - "justice (Gini) **0.833** contre **0.556**, satisfaction moyenne **0.667** contre\n", - "**0.5**. Élire le vainqueur de Condorcet n'est pas seulement « plus juste » au\n", - "sens de Gini : il satisfait *aussi* mieux le collectif — Centre est second\n", - "choix de ceux dont il bat le premier. Sur `dictatorship` en revanche, les quatre\n", - "méthodes rendent des métriques identiques (0.667 / 0.6) : ce scénario ne\n", - "discrimine pas les procédures, il montre surtout qu'un seul vote stratégique\n", - "déplace l'égalité initiale. Le banc garde son pouvoir discriminant précisément\n", - "sur `spoiler_candidate`." + "# Robustesse byzantine : le vainqueur survit-il a une fraction de votes aleatoires ?\n", + "print(\"Consensus byzantin selon la fraction de votes aleatoires (10 seeds chacune) :\")\n", + "print(f\"{'ratio':>6} {'vainqueurs observes':>30} {'majoritaire survit ?':>20}\")\n", + "for ratio in (0.0, 0.15, 0.3, 0.45):\n", + " gagnants = []\n", + " for s in range(10):\n", + " w = g.byzantine_consensus(build_agents(), OPTIONS,\n", + " {\"byzantine_ratio\": ratio}, rng=random.Random(s))\n", + " gagnants.append(w)\n", + " from collections import Counter\n", + " dist = dict(Counter(gagnants))\n", + " survit = \"oui\" if dist.get(\"Pizza\", 0) == 10 else f\"{dist.get('Pizza', 0)}/10\"\n", + " print(f\"{ratio:>6.2f} {str(dist):>30} {survit:>20}\")" ] }, { "cell_type": "markdown", - "id": "gov-16", + "id": "g17353-17", "metadata": { "papermill": { - "duration": 0.002607, - "end_time": "2026-09-22T10:12:09.542521+00:00", + "duration": 0.004586, + "end_time": "2026-09-25T08:36:40.067258+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.539914+00:00", + "start_time": "2026-09-25T08:36:40.062672+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Et quand les agents tombent en panne ?\n", + "### Lecture : le vainqueur majoritaire n'est pas le plus robuste\n", "\n", - "Les méthodes précédentes supposent des votes sincères et bien transmis. Un\n", - "système multi-agents réel affronte trois pannes : des agents **défaillants**\n", - "qui votent au hasard (le modèle byzantin), des agents **stratégiques** qui\n", - "déclarent autre chose que leur préférence vraie, et des **coalitions**\n", - "organisées qui coordonnent leurs votes. Le banc du source étiquetait\n", - "précisément ces axes — la suite les mesure un par un, en commençant par le\n", - "bruit : la cellule suivante fait croître la fraction d'agents byzantins et\n", - "observe le taux de consensus." + "La Pizza gagne à la majorité *sincère*, mais elle est fragile au bruit byzantin : ses 3 partisans sont minoritaires face aux 4 autres membres, dont les votes dispersés + le bruit peuvent élire une autre destination dès que la fraction de défaillants monte. Un vainqueur de compromis (Borda/Condorcet) est plus robuste : il a des partisans « profonds ». La tolérance aux pannes et l'agrégation de préférences sont deux qualités distinctes — c'est pourquoi on ne les compare pas (#1981)." ] }, { "cell_type": "code", "execution_count": 7, - "id": "gov-17", + "id": "g17353-18", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.548798Z", - "iopub.status.busy": "2026-09-22T10:12:09.548609Z", - "iopub.status.idle": "2026-09-22T10:12:09.554087Z", - "shell.execute_reply": "2026-09-22T10:12:09.553476Z" + "iopub.execute_input": "2026-09-25T08:36:40.077423Z", + "iopub.status.busy": "2026-09-25T08:36:40.077073Z", + "iopub.status.idle": "2026-09-25T08:36:40.085449Z", + "shell.execute_reply": "2026-09-25T08:36:40.084120Z" }, "papermill": { - "duration": 0.009483, - "end_time": "2026-09-22T10:12:09.554583+00:00", + "duration": 0.015408, + "end_time": "2026-09-25T08:36:40.086568+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.545100+00:00", + "start_time": "2026-09-25T08:36:40.071160+00:00", "status": "completed" }, "tags": [] @@ -627,83 +573,78 @@ "name": "stdout", "output_type": "stream", "text": [ - "ratio byzantin 0.0 : consensus moyen = 0.72\n", - "ratio byzantin 0.2 : consensus moyen = 0.76\n", - "ratio byzantin 0.4 : consensus moyen = 0.62\n" + "Raft (10 seeds) : leader elu -> proposition acceptee ou repli majoritaire\n", + "Vainqueurs Raft sur 10 seeds : {'Pizza': 8, 'Burger': 2}\n", + "\n", + "(Le repli majoritaire se declenche quand la proposition du leader n'a pas\n", + " la majorite des preferences top-2 des autres membres.)\n" ] } ], "source": [ - "# ROBUSTESSE BYZANTINE : consensus moyen selon la fraction de votes aleatoires\n", - "# (10 seeds par palier, moyenne des taux de consensus)\n", - "opts = g.SCENARIOS[\"byzantine_noise\"][\"options\"]\n", - "for ratio in [0.0, 0.2, 0.4]:\n", - " taux = []\n", - " for seed in range(10):\n", - " agents = g.build_agents(\"byzantine_noise\")\n", - " r = g.simulate_vote(agents, opts, \"byzantine\",\n", - " context={\"byzantine_ratio\": ratio}, seed=seed)\n", - " taux.append(g.consensus_rate(r))\n", - " print(f\"ratio byzantin {ratio:.1f} : consensus moyen = {sum(taux)/len(taux):.2f}\")" + "# Raft : election d'un leader, sa proposition doit etre acceptee par la majorite.\n", + "print(\"Raft (10 seeds) : leader elu -> proposition acceptee ou repli majoritaire\")\n", + "gagnants_raft = [g.raft_consensus(build_agents(), OPTIONS, rng=random.Random(s)) for s in range(10)]\n", + "from collections import Counter\n", + "print(f\"Vainqueurs Raft sur 10 seeds : {dict(Counter(gagnants_raft))}\")\n", + "print(\"\\n(Le repli majoritaire se declenche quand la proposition du leader n'a pas\")\n", + "print(\" la majorite des preferences top-2 des autres membres.)\")" ] }, { "cell_type": "markdown", - "id": "gov-18", + "id": "g17353-19", "metadata": { "papermill": { - "duration": 0.002658, - "end_time": "2026-09-22T10:12:09.560376+00:00", + "duration": 0.012924, + "end_time": "2026-09-25T08:36:40.110607+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.557718+00:00", + "start_time": "2026-09-25T08:36:40.097683+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Lecture : une courbe non monotone — l'artefact révélé\n", - "\n", - "Intuition : plus de bruit byzantin = moins de consensus. La mesure dit\n", - "**0.72 → 0.76 → 0.62** : le palier 20 % de bruit est *meilleur* que le palier\n", - "0 %. Deux causes, toutes deux instructives :\n", - "\n", - "1. **La base n'est pas silencieuse** : à ratio 0.0, les personnalités `random`\n", - " (Z1, Z2) votent déjà aléatoirement — le « 0 % de bruit » du paramètre n'est\n", - " pas le zéro de bruit du système.\n", - "2. **Le slicing déterministe (divergence 3)** : à 5 agents, ratio 0.2 = un\n", - " byzantin = *toujours le premier agent* (H1), jamais un tirage. Le remplaçant\n", - " aléatoire, sur ces 10 seeds, déplace le vainqueur vers un candidat\n", - " mieux aligné des votes sincères — le taux remonte.\n", + "### Lecture : Raft dépend du leader tiré\n", "\n", - "La dégradation réelle n'apparaît qu'à 40 % (0.62). C'est exactement le type\n", - "d'effet que la tranche `agents[:n]` du source rend invisible : un organe qui\n", - "tirait les byzantins au sort laverait cette bosse — au prix de perdre la\n", - "fidélité du port. Le port reste fidèle, la mesure reste honnête.\n", + "Raft élit le vainqueur du *leader* si sa proposition est acceptée, sinon replie sur la majorité. Le résultat varie donc selon le leader tiré au sort : c'est un protocole de **coordonnation**, pas d'agrégation — il répond à « comment le groupe suit-il une décision cohérente malgré les pannes », pas à « quelle est la meilleure option »." + ] + }, + { + "cell_type": "markdown", + "id": "g17353-20", + "metadata": { + "papermill": { + "duration": 0.004661, + "end_time": "2026-09-25T08:36:40.121080+00:00", + "exception": false, + "start_time": "2026-09-25T08:36:40.116419+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## §5 — Mesurer une décision collective : justice et satisfaction\n", "\n", - "Le protocole lui-même est une leçon : **dix seeds par palier, moyenne rendue**\n", - "— un seul seed aurait rendu une des trois valeurs par palier et masqué la\n", - "non-monotonie ou l'aurait fabriquée par chance. Mesurer un système à tirage,\n", - "c'est mesurer une distribution, pas un point : le taux de consensus à ratio\n", - "0.2 sur UN seed prend n'importe quelle valeur de la distribution, et seule la\n", - "moyenne sur dix seeds dit quelque chose de la procédure." + "Dire « méthode plus juste » n'a de sens qu'avec un instrument. Le tronc porte les métriques (consensus_rate, fairness via Gini, satisfaction, stability) — avec le fix d'honnêteté #2576 : elles nomment ce qu'elles ne peuvent pas calculer au lieu d'un 0.0 silencieux." ] }, { "cell_type": "code", "execution_count": 8, - "id": "gov-19", + "id": "g17353-21", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.567093Z", - "iopub.status.busy": "2026-09-22T10:12:09.566849Z", - "iopub.status.idle": "2026-09-22T10:12:09.573350Z", - "shell.execute_reply": "2026-09-22T10:12:09.572656Z" + "iopub.execute_input": "2026-09-25T08:36:40.132128Z", + "iopub.status.busy": "2026-09-25T08:36:40.131814Z", + "iopub.status.idle": "2026-09-25T08:36:40.137636Z", + "shell.execute_reply": "2026-09-25T08:36:40.136801Z" }, "papermill": { - "duration": 0.011048, - "end_time": "2026-09-22T10:12:09.574012+00:00", + "duration": 0.013171, + "end_time": "2026-09-25T08:36:40.138848+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.562964+00:00", + "start_time": "2026-09-25T08:36:40.125677+00:00", "status": "completed" }, "tags": [] @@ -713,79 +654,94 @@ "name": "stdout", "output_type": "stream", "text": [ - "sincere : vainqueur A, satisfaction 0.60\n", - "strategique : vainqueur B, satisfaction 0.70\n", - "fausse coalition B : vainqueur B, satisfaction 0.80\n" + "scrutin vainqueur satisfaction moy. Gini satisf.\n", + "--------------------------------------------------------\n", + "majority Pizza 0.429 0.571\n", + "plurality Pizza 0.429 0.571\n", + "borda Burger 0.619 0.286\n", + "condorcet Burger 0.619 0.286\n", + "quadratic Pizza 0.429 0.571\n" ] } ], "source": [ - "# MANIPULATION : vote strategique et fausse coalition sur le bloc discipliné\n", - "opts = g.SCENARIOS[\"strategic_bloc\"][\"options\"]\n", + "def satisfaction_de(agent, vainqueur):\n", + " # Satisfaction du port du tronc : rang relatif du vainqueur dans les preferences.\n", + " if vainqueur not in agent.preferences:\n", + " return 0.0\n", + " return 1.0 - agent.preferences.index(vainqueur) / max(1, len(agent.preferences) - 1)\n", "\n", - "base = g.simulate_vote(g.build_agents(\"strategic_bloc\"), opts, \"majority\", seed=0)\n", - "print(f\"sincere : vainqueur {base['winner']}, satisfaction {sum(base['satisfaction'])/len(base['satisfaction']):.2f}\")\n", + "def gini(x):\n", + " x = sorted(float(v) for v in x)\n", + " n = len(x)\n", + " if n == 0 or sum(x) == 0:\n", + " return 0.0\n", + " idx = range(1, n + 1)\n", + " return sum((2 * i - n - 1) * v for i, v in zip(idx, x)) / (n * sum(x))\n", "\n", - "strat = g.apply_manipulation(g.build_agents(\"strategic_bloc\"), \"strategic\")\n", - "r = g.simulate_vote(strat, opts, \"majority\", seed=0)\n", - "sat = sum(r['satisfaction'])/len(r['satisfaction'])\n", - "print(f\"strategique : vainqueur {r['winner']}, satisfaction {sat:.2f}\")\n", - "\n", - "false = g.apply_manipulation(g.build_agents(\"strategic_bloc\"), \"false_coalition\", target=\"B\")\n", - "r = g.simulate_vote(false, opts, \"majority\", seed=0)\n", - "sat = sum(r['satisfaction'])/len(r['satisfaction'])\n", - "print(f\"fausse coalition B : vainqueur {r['winner']}, satisfaction {sat:.2f}\")" + "print(f\"{'scrutin':<12} {'vainqueur':<10} {'satisfaction moy.':>18} {'Gini satisf.':>13}\")\n", + "print(\"-\" * 56)\n", + "for nom, fn in g.SCRUTINS.items():\n", + " ag = build_agents()\n", + " w = fn(ag, OPTIONS)\n", + " sats = [satisfaction_de(a, w) for a in ag]\n", + " print(f\"{nom:<12} {w:<10} {np.mean(sats):>18.3f} {gini(sats):>13.3f}\")" ] }, { "cell_type": "markdown", - "id": "gov-20", + "id": "g17353-22", "metadata": { "papermill": { - "duration": 0.002982, - "end_time": "2026-09-22T10:12:09.579832+00:00", + "duration": 0.005633, + "end_time": "2026-09-25T08:36:40.149557+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.576850+00:00", + "start_time": "2026-09-25T08:36:40.143924+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Lecture : la manipulation rend le collectif... plus satisfait ?\n", + "### Lecture : satisfaction et justice vont ensemble — ici\n", "\n", - "Résultat contre-intuitif mesuré : sincère → A (satisfaction 0.60), vote\n", - "stratégique généralisé → B (0.70), fausse coalition pour B → B (0.80). Les deux\n", - "manipulations **augmentent** la satisfaction moyenne. L'explication est dans la\n", - "structure du scénario : le bloc A (3 agents) a B comme second choix, S1 a B\n", - "comme premier — B est le *compromis naturel* que le vote sincère ne trouve pas.\n", - "La manipulation, ici, pousse le collectif vers son compromis.\n", + "Sur ce profil, les méthodes qui choisissent l'option de compromis (Borda, Condorcet) obtiennent une satisfaction moyenne plus élevée **et** un Gini plus faible (moins d'inégalité de satisfaction entre membres). La Pizza, vainqueur majoritaire, laisse 4 membres sur 7 insatisfaits. Le scrutin qui « gagne » n'est pas celui qui rend le collectif le plus content — c'est le théorème de Gibbard-Satterthwaite en germe." + ] + }, + { + "cell_type": "markdown", + "id": "g17353-23", + "metadata": { + "papermill": { + "duration": 0.004874, + "end_time": "2026-09-25T08:36:40.161519+00:00", + "exception": false, + "start_time": "2026-09-25T08:36:40.156645+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## §6 — La manipulation : enterrer son adversaire (Gibbard-Satterthwaite)\n", "\n", - "Le **piège de la moyenne** : la satisfaction du bloc passe de 1.0 (A gagne) à\n", - "0.5 (B gagne) — trois agents y perdent, deux y gagnent, et la moyenne monte.\n", - "Une gouvernance qui ne regarderait que la satisfaction moyenne peut consacrer\n", - "la manipulation ; la distribution (et le Gini de `summarize`) dit la vérité que\n", - "la moyenne tait. La contre-vérification est laissée à la cellule de\n", - "l'exercice 2 : croiser `summarize()` sincère contre manipulé montre la justice\n", - "baisser pendant que la satisfaction monte — les deux instruments divergent\n", - "précisément quand un seul d'entre eux suffit à tromper." + "Le théorème de Gibbard-Satterthwaite (1973) étend Arrow : tout scrutin non dictatorial sur ≥ 3 options est **manipulable** — un votant peut avoir intérêt à voter stratégiquement plutôt que sincèrement. Le carnet sas le démontre sur Borda : le bloc Pizza « enterre » Burger en le descendant en dernière place." ] }, { "cell_type": "code", "execution_count": 9, - "id": "gov-21", + "id": "g17353-24", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.586357Z", - "iopub.status.busy": "2026-09-22T10:12:09.586119Z", - "iopub.status.idle": "2026-09-22T10:12:09.591308Z", - "shell.execute_reply": "2026-09-22T10:12:09.590499Z" + "iopub.execute_input": "2026-09-25T08:36:40.173206Z", + "iopub.status.busy": "2026-09-25T08:36:40.172692Z", + "iopub.status.idle": "2026-09-25T08:36:40.188448Z", + "shell.execute_reply": "2026-09-25T08:36:40.187878Z" }, "papermill": { - "duration": 0.009393, - "end_time": "2026-09-22T10:12:09.591942+00:00", + "duration": 0.023036, + "end_time": "2026-09-25T08:36:40.190140+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.582549+00:00", + "start_time": "2026-09-25T08:36:40.167104+00:00", "status": "completed" }, "tags": [] @@ -795,118 +751,118 @@ "name": "stdout", "output_type": "stream", "text": [ - "coalition_1 : ['D1'] -> vote A (leader D1)\n", - "coalition_2 : ['O1', 'O2'] -> vote B (leader O1)\n", - "coalition_3 : ['O3'] -> vote B (leader O3)\n", - "coalition_4 : ['O4'] -> vote C (leader O4)\n", - "Shapley (payoff = B porte) : {'O1': 1.0, 'O2': 0.0}\n" + "Vote sincere :\n", + " Borda -> Burger\n", + " majorite -> Pizza\n", + "\n", + "Vote strategique (le bloc Pizza enterre Burger) :\n", + " Borda -> Sushi\n", + " majorite -> Pizza\n" ] } ], "source": [ - "# COALITIONS : la confiance agrège des votes épars\n", - "agents = g.build_agents(\"dictatorship\",\n", - " trust={\"O1\": {\"O2\": 0.9, \"O3\": 0.5}, \"O2\": {\"O1\": 0.9}, \"O3\": {}, \"O4\": {\"O3\": 0.85}, \"D1\": {}})\n", - "coalitions = g.form_coalitions(agents)\n", - "for c in coalitions:\n", - " print(f\"{c[0].coalition_id} : {[a.name for a in c]} -> vote {c[0].preferences[0]} (leader {c[0].name})\")\n", + "# Sincere : le bloc Pizza (Alice, Bruno, Chloe) vote ses vraies preferences.\n", + "ag_sincere = build_agents()\n", + "print(\"Vote sincere :\")\n", + "print(f\" Borda -> {g.borda_count(ag_sincere, OPTIONS)}\")\n", + "print(f\" majorite -> {g.majority_voting(ag_sincere, OPTIONS)}\")\n", + "\n", + "# Manipulation : le bloc Pizza enterre Burger (le descend en dernier).\n", + "def build_manipulates():\n", + " agents = []\n", + " for i, (n, p) in enumerate(PROFIL_PREFS):\n", + " if p[0] == \"Pizza\":\n", + " # voter Pizza > Sushi > Raclette > Burger (Burger enterre)\n", + " p = [\"Pizza\"] + [o for o in OPTIONS if o not in (\"Pizza\", \"Burger\")] + [\"Burger\"]\n", + " agents.append(g.Agent(n, \"stubborn\", p, rng=random.Random(i)))\n", + " return agents\n", "\n", - "# Valeur de Shapley de la coalition O1-O2 : payoff = 1 si O1 present (B porte par O1)\n", - "def payoff(noms):\n", - " return 1.0 if \"O1\" in noms else 0.0\n", - "vals = g.shapley_value([\"O1\", \"O2\"], payoff)\n", - "print(\"Shapley (payoff = B porte) :\", {k: round(v, 2) for k, v in vals.items()})" + "ag_manip = build_manipulates()\n", + "print(\"\\nVote strategique (le bloc Pizza enterre Burger) :\")\n", + "print(f\" Borda -> {g.borda_count(ag_manip, OPTIONS)}\")\n", + "print(f\" majorite -> {g.majority_voting(ag_manip, OPTIONS)}\")" ] }, { "cell_type": "markdown", - "id": "gov-22", + "id": "g17353-25", "metadata": { "papermill": { - "duration": 0.002793, - "end_time": "2026-09-22T10:12:09.597607+00:00", + "duration": 0.006332, + "end_time": "2026-09-25T08:36:40.203325+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.594814+00:00", + "start_time": "2026-09-25T08:36:40.196993+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Lecture : la coalition exige une confiance lisible dans un sens précis\n", - "\n", - "Deux coalitions se forment : O1↔O2 (confiance mutuelle 0.9) et... personne\n", - "d'autre. O4 fait pourtant confiance à O3 (0.85) — mais O3 ne truste personne :\n", - "la boucle du source parcourt les agents **en ordre de liste**, et O3 est\n", - "examiné *avant* O4 ; arrivé là, O3 ne propose à personne et reste seul. Quand\n", - "le tour d'O4 vient, O3 est déjà pris. Asymétrie mesurée de l'algorithme : la\n", - "coalition naît seulement si le **premier des deux, en ordre de liste**, truste\n", - "l'autre — une confiance unilatérale dans l'autre sens ne produit rien. Chaque\n", - "bloc vote ensuite la préférence de son *leader* (le premier rencontré), pas\n", - "celle de son membre le plus trusté.\n", + "### Lecture : la manipulation peut se retourner contre son auteur\n", "\n", - "La valeur de Shapley pose la question du **mérite** dans la coalition : avec\n", - "un payoff qui ne vaut que si O1 participe, O1 est pivotale dans les deux ordres\n", - "d'arrivée possibles et capte 1.0, O2 capte 0.0 — la coopération ne récompense\n", - "pas la présence, elle récompense la **contribution marginale**. C'est\n", - "l'instrument que le source appliquait aux coalitions de sa simulation ; ici il\n", - "est isolé, calculable à la main sur deux agents, et l'exercice 2 peut le\n", - "prolonger." + "En enterrant Burger, le bloc Pizza espérait consolider sa victoire à Borda. Mais enterrer un adversaire peut **augmenter le score d'un autre** : ici, la manipulation change le vainqueur Borda — pas forcément dans le sens espéré. C'est la leçon n° 3 du carnet sas : un scrutin manipulable est un jeu, et la stratégie a des effets de second ordre que le manipulateur ne contrôle pas." ] }, { "cell_type": "markdown", - "id": "gov-24", + "id": "g17353-26", "metadata": { "papermill": { - "duration": 0.002682, - "end_time": "2026-09-22T10:12:09.602930+00:00", + "duration": 0.00434, + "end_time": "2026-09-25T08:36:40.212272+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.600248+00:00", + "start_time": "2026-09-25T08:36:40.207932+00:00", "status": "completed" }, "tags": [] }, "source": [ - "## Limites mesurées\n", + "## §7 — Limites mesurées de la distillation\n", "\n", - "Une distillation n'est fidèle que si elle dit ce qu'elle a corrigé en route :\n", - "chaque divergence ci-dessous a été **mesurée sur le source** avant d'être\n", - "écrite ici, pas supposée depuis une lecture de surface.\n", + "Une distillation n'est fidèle que si elle dit ce qu'elle a retenu, corrigé et laissé de côté.\n", "\n", - "Ce port est **honnête sur ce qu'il ne fait pas** — six divergences mesurées sur\n", - "le source, pas supposées :\n", + "**Retenu du cœur** : les 15 algorithmes (5 scrutins + 2 protocoles + 8 choix social), la classe `Agent` avec personnalités/confiance/Q-learning, les archétypes BDI/Reactive, la doctrine #1981.\n", "\n", - "1. **La simulation du source n'appelait jamais ses méthodes de vote** :\n", - " `simulate_governance` assignait `method_fn = GOVERNANCE_METHODS[method]`\n", - " puis déroulait un tally de blocs sans jamais l'invoquer — les 7 méthodes y\n", - " étaient du code mort. L'organe corrige : `simulate_vote` applique la méthode.\n", - "2. **Le « vote quadratique » du source n'est pas quadratique** : aucun coût en\n", - " somme de carrés ; un agent flexible coupe juste son budget en deux. Porté\n", - " tel quel — l'exercice 1 fait implémenter le vrai.\n", - "3. **Les byzantins sont les n premiers agents** (tranche déterministe\n", - " `agents[:n]`), pas un tirage : mesuré, porté fidèlement.\n", - "4. **La voie mémoire du source référence `options` hors de portée** : `NameError`\n", - " dès le deuxième appel, que le contexte porte `\"options\"` ou non (mesuré) : non portée, la\n", - " satisfaction est recalculée depuis les préférences.\n", - "5. **La médiation à probabilités fixes** (0.8/0.5/0.7 codés en dur) n'est pas\n", - " portée : seul `detect_conflicts` l'est.\n", - "6. **Le repli Borda en cas de cycle** conserve le tie-break de `max()` (première\n", - " clé d'insertion) — démontré sur le cycle ci-dessus où Borda rend 3=3=3.\n", + "**Corrigé par le cœur** (deltas mesurés, cf. docstring de l'organe) :\n", + "\n", + "1. la catégorie « 7 méthodes » → 15 algorithmes en 3 familles (#1981) ;\n", + "2. `consensus_rate` traite les 3 formes de `votes` (#1273) ;\n", + "3. les métriques nomment les non-calculables (#2576) — le prototype retournait 0.0/None silencieux ;\n", + "4. Kemeny-Young > 8 options lève `ValueError` ou tombe sur Copeland en le disant (#971).\n", + "\n", + "**Laissé de côté** : la boucle `Simulation` du prototype (son `simulate_governance` n'appelait jamais `method_fn` — code mort), le CLI, la visualisation matplotlib, les couches BDI/Reactive stubs de protocole vides.\n", + "\n", + "**Défaut conservé** : `quadratic_voting` n'implémente pas le coût quadratique annoncé (aucune somme de carrés) — le tronc l'a conservé tel quel, et l'exercice 1 vous le fait corriger." + ] + }, + { + "cell_type": "markdown", + "id": "g17353-27", + "metadata": { + "papermill": { + "duration": 0.005348, + "end_time": "2026-09-25T08:36:40.222780+00:00", + "exception": false, + "start_time": "2026-09-25T08:36:40.217432+00:00", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## Exercices\n", "\n", - "Aussi : les méthodes aléatoires (byzantin, raft, personnalité `random`) sont\n", - "**seedées** — les sorties committées sont reproductibles à l'identique, mais un\n", - "autre seed peut rendre un autre vainqueur (l'exercice 3 le mesure)." + "Les trois exercices suivants vous font manipuler les concepts. Complétez les cellules : elles s'exécutent sans erreur même non complétées (résultat `None` à interpréter) — la solution est votre travail." ] }, { "cell_type": "markdown", - "id": "gov-26", + "id": "g17353-28", "metadata": { "papermill": { - "duration": 0.002799, - "end_time": "2026-09-22T10:12:09.608767+00:00", + "duration": 0.004216, + "end_time": "2026-09-25T08:36:40.231434+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.605968+00:00", + "start_time": "2026-09-25T08:36:40.227218+00:00", "status": "completed" }, "tags": [] @@ -914,31 +870,25 @@ "source": [ "### Exercice 1 — le VRAI vote quadratique\n", "\n", - "Le source annonce un coût quadratique mais ne l'implémente pas (limite 2).\n", - "Implémentez-le pour de vrai : chaque agent dispose d'un budget de crédits, le\n", - "**coût** payé pour acheter `v` voix sur une option est `v²` (le budget\n", - "s'épuise en carrés), et l'allocation optimale d'un agent qui aime une option\n", - "d'intensité `i` est d'y mettre `floor(sqrt(budget * i))` voix. Faites voter le\n", - "scénario `project_funding` : le vainqueur change-t-il par rapport au port\n", - "« budget coupé en deux » de l'organe ?" + "Le source (et le tronc qui l'a conservé) annonce un coût quadratique mais ne l'implémente pas : un agent `flexible` coupe juste son budget en deux. Le vrai vote quadratique : chaque agent a un budget, le coût d'allouer v voix à une option est v², et l'allocation optimale répartit les voix en racine carrée de l'intensité de préférence. Implémentez-le et mesurez s'il change le vainqueur sur le scénario du club." ] }, { "cell_type": "code", "execution_count": 10, - "id": "gov-23", + "id": "g17353-29", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.615311Z", - "iopub.status.busy": "2026-09-22T10:12:09.615124Z", - "iopub.status.idle": "2026-09-22T10:12:09.618959Z", - "shell.execute_reply": "2026-09-22T10:12:09.618208Z" + "iopub.execute_input": "2026-09-25T08:36:40.243907Z", + "iopub.status.busy": "2026-09-25T08:36:40.242837Z", + "iopub.status.idle": "2026-09-25T08:36:40.253517Z", + "shell.execute_reply": "2026-09-25T08:36:40.250727Z" }, "papermill": { - "duration": 0.007891, - "end_time": "2026-09-22T10:12:09.619465+00:00", + "duration": 0.019983, + "end_time": "2026-09-25T08:36:40.255315+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.611574+00:00", + "start_time": "2026-09-25T08:36:40.235332+00:00", "status": "completed" }, "tags": [] @@ -948,32 +898,31 @@ "name": "stdout", "output_type": "stream", "text": [ - "Exercice 1 a completer\n" + "Exercice 1 a completer : vrai vote quadratique (cout = somme des carres)\n" ] } ], "source": [ "def vrai_quadratique(agents, options, budget=9, intensite=None):\n", - " # Etape 1 : pour chaque agent, voix par option = floor(sqrt(budget * intensite(option)))\n", - " # Etape 2 : cumuler les voix par option sur tous les agents\n", - " # Etape 3 : rendre l'option au total max\n", - " # Indice : intensite = 1.0 premier choix, 0.5 deuxieme, 0.0 ensuite ;\n", - " # tout le budget sur le premier choix achete floor(sqrt(9)) = 3 voix.\n", - " return None # TODO etudiant\n", - "\n", - "\n", - "print(\"Exercice 1 a completer\")" + " # Etape 1 : pour chaque agent, calculer l'allocation optimale\n", + " # (voix proportionnelles a sqrt(intensite) sous contrainte somme v^2 <= budget).\n", + " # Etape 2 : sommer les voix par option, retourner la gagnante.\n", + " # Indice : sans intensite explicite, utiliser le rang dans les preferences\n", + " # comme proxy (premier choix = intensite maximale).\n", + " # TODO etudiant\n", + " return None # TODO etudiant : nom de l'option gagnante\n", + "print(\"Exercice 1 a completer : vrai vote quadratique (cout = somme des carres)\")" ] }, { "cell_type": "markdown", - "id": "gov-28", + "id": "g17353-30", "metadata": { "papermill": { - "duration": 0.002985, - "end_time": "2026-09-22T10:12:09.625618+00:00", + "duration": 0.006646, + "end_time": "2026-09-25T08:36:40.267136+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.622633+00:00", + "start_time": "2026-09-25T08:36:40.260490+00:00", "status": "completed" }, "tags": [] @@ -981,28 +930,25 @@ "source": [ "### Exercice 2 — construire la société où majorité et Borda se contredisent\n", "\n", - "Construisez un scénario (dans le format des tuples de `SCENARIOS`) où la\n", - "majorité à un tour et Borda **ne désignent pas le même vainqueur**, puis\n", - "vérifiez ce que dit Condorcet. Mesurez `summarize()` pour les deux méthodes :\n", - "la plus juste (justice Gini) est-elle celle qui satisfait le plus d'agents ?" + "Le scénario du club montre déjà une divergence majorité/Borda. Construisez un profil de préférences (votre propre « société ») où le vainqueur majoritaire est **différent** du vainqueur Borda, et vérifiez-le en exécutant les deux scrutins. Quelle structure de préférences produit cette contradiction ?" ] }, { "cell_type": "code", "execution_count": 11, - "id": "gov-25", + "id": "g17353-31", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.632586Z", - "iopub.status.busy": "2026-09-22T10:12:09.632323Z", - "iopub.status.idle": "2026-09-22T10:12:09.636173Z", - "shell.execute_reply": "2026-09-22T10:12:09.635513Z" + "iopub.execute_input": "2026-09-25T08:36:40.291320Z", + "iopub.status.busy": "2026-09-25T08:36:40.290326Z", + "iopub.status.idle": "2026-09-25T08:36:40.299152Z", + "shell.execute_reply": "2026-09-25T08:36:40.295624Z" }, "papermill": { - "duration": 0.008178, - "end_time": "2026-09-22T10:12:09.636687+00:00", + "duration": 0.026122, + "end_time": "2026-09-25T08:36:40.302364+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.628509+00:00", + "start_time": "2026-09-25T08:36:40.276242+00:00", "status": "completed" }, "tags": [] @@ -1012,61 +958,58 @@ "name": "stdout", "output_type": "stream", "text": [ - "Exercice 2 a completer\n" + "Exercice 2 a completer : societe ou majorite != Borda\n" ] } ], "source": [ - "ma_societe = {\n", - " \"options\": [],\n", - " \"agents\": [\n", - " # (\"nom\", \"personnalite\", [preferences completes])\n", - " ],\n", - "}\n", - "# Indice : spoiler_candidate est deja un cas majority != borda ; cherchez-en un\n", - "# AUTRE (4 options ou 7 agents), puis g.summarize(g.simulate_vote(...)) x 2.\n", - "print(\"Exercice 2 a completer\")" + "ma_societe = [\n", + " # (\"nom\", [preferences ordonnees, 1er choix en tete]),\n", + " # TODO etudiant : au moins 5 membres, 3 options\n", + "]\n", + "options_societe = [] # TODO etudiant : les options\n", + "# Indice : construire les agents avec g.Agent(n, \"stubborn\", p), puis comparer\n", + "# g.majority_voting(agents, options) et g.borda_count(agents, options).\n", + "resultat_majorite = None # TODO etudiant\n", + "resultat_borda = None # TODO etudiant\n", + "print(\"Exercice 2 a completer : societe ou majorite != Borda\")" ] }, { "cell_type": "markdown", - "id": "gov-29", + "id": "g17353-32", "metadata": { "papermill": { - "duration": 0.002982, - "end_time": "2026-09-22T10:12:09.642781+00:00", + "duration": 0.00629, + "end_time": "2026-09-25T08:36:40.317216+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.639799+00:00", + "start_time": "2026-09-25T08:36:40.310926+00:00", "status": "completed" }, "tags": [] }, "source": [ - "### Exercice 3 — stabilité multi-seed et témoins négatifs\n", + "### Exercice 3 — STV contre le vote utile\n", "\n", - "(a) `raft` tire son leader au sort : mesurez sa **stabilité** sur le cycle de\n", - "Condorcet — 10 seeds, comptez la distribution des vainqueurs. (b) Témoin\n", - "négatif : sur `strategic_bloc` (un bloc de 3 sur 5), toutes les méthodes et\n", - "tous les seeds doivent rendre le même vainqueur — vérifiez qu'aucun seed ne\n", - "déraille." + "Le vote unique transférable (STV) rend le « vote utile » moins nécessaire : si votre candidat favori est éliminé, votre voix se transfère à votre second choix. Sur le profil du club, simulez un électeur qui préfère Sushi mais vote « utile » pour Burger : son vote change-t-il le vainqueur STV ? Et à la majorité simple ?" ] }, { "cell_type": "code", "execution_count": 12, - "id": "gov-27", + "id": "g17353-33", "metadata": { "execution": { - "iopub.execute_input": "2026-09-22T10:12:09.649495Z", - "iopub.status.busy": "2026-09-22T10:12:09.649262Z", - "iopub.status.idle": "2026-09-22T10:12:09.653175Z", - "shell.execute_reply": "2026-09-22T10:12:09.652631Z" + "iopub.execute_input": "2026-09-25T08:36:40.336312Z", + "iopub.status.busy": "2026-09-25T08:36:40.334920Z", + "iopub.status.idle": "2026-09-25T08:36:40.340980Z", + "shell.execute_reply": "2026-09-25T08:36:40.340562Z" }, "papermill": { - "duration": 0.008328, - "end_time": "2026-09-22T10:12:09.653931+00:00", + "duration": 0.018767, + "end_time": "2026-09-25T08:36:40.342607+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.645603+00:00", + "start_time": "2026-09-25T08:36:40.323840+00:00", "status": "completed" }, "tags": [] @@ -1076,30 +1019,32 @@ "name": "stdout", "output_type": "stream", "text": [ - "Exercice 3 a completer\n" + "Exercice 3 a completer : STV rend-il le vote utile inutile ?\n" ] } ], "source": [ - "from collections import Counter\n", - "gagnants = Counter()\n", - "# Etape 1 : boucle sur 10 seeds -> simulate_vote(..., \"raft\", seed=s) sur cyclic_majority\n", - "# Etape 2 : gagnants[winner] += 1, print(gagnants)\n", - "# Etape 3 : temoin negatif sur strategic_bloc (meme distribution, meme gagnant partout)\n", - "# Indice : g.stability([...]) rend 1 si un seul vainqueur dans la liste, 0 sinon.\n", - "resultat_attendu = None # TODO etudiant\n", - "print(\"Exercice 3 a completer\")" + "# Indice : modifier UN bulletin de PROFIL_PREFS (un membre prefere Sushi mais vote Burger\n", + "# en premier), reconstruire les ballots, comparer g.stv(...) et g.majority_voting(...)\n", + "# avant/apres la manipulation.\n", + "ballots_avant = [p for _, p in PROFIL_PREFS]\n", + "ballots_apres = None # TODO etudiant : copie de ballots_avant avec 1 bulletin manipule\n", + "stv_avant = None # TODO etudiant : g.stv(ballots_avant, OPTIONS)\n", + "stv_apres = None # TODO etudiant\n", + "maj_avant = None # TODO etudiant : g.majority_voting(build_agents(), OPTIONS)\n", + "maj_apres = None # TODO etudiant\n", + "print(\"Exercice 3 a completer : STV rend-il le vote utile inutile ?\")" ] }, { "cell_type": "markdown", - "id": "gov-30", + "id": "g17353-34", "metadata": { "papermill": { - "duration": 0.002872, - "end_time": "2026-09-22T10:12:09.660192+00:00", + "duration": 0.009729, + "end_time": "2026-09-25T08:36:40.359776+00:00", "exception": false, - "start_time": "2026-09-22T10:12:09.657320+00:00", + "start_time": "2026-09-25T08:36:40.350047+00:00", "status": "completed" }, "tags": [] @@ -1107,34 +1052,17 @@ "source": [ "## Conclusion\n", "\n", - "Ce notebook a fait mesurer, sur des préférences identiques, ce que le théorème\n", - "d'Arrow (1951) énonce en général : **aucune procédure de agrégation de trois\n", - "options ou plus ne peut être simultanément** non-dictatoriale, unanime et\n", - "indépendante aux alternatives non pertinentes. Le spoiler et le cycle sont ses\n", - "deux manifestations mesurées ici — pas des accidents de scénario, des\n", - "propriétés structurelles du choix collectif.\n", + "Ce notebook a mesuré, sur un même profil de préférences, ce que la théorie du choix social prédit : **le scrutin choisit le vainqueur autant que les électeurs**. La Pizza gagne la majorité et perd tous ses duels ; l'option de compromis gagne Borda, Condorcet, STV, Copeland, Schulze et la satisfaction moyenne ; la manipulation de Borda se retourne ; et les protocoles byzantins/Raft répondent à une autre question (tolérance aux pannes) que les scrutins ne posent pas.\n", "\n", - "- La **même société de préférences** rend des vainqueurs différents selon la\n", - " méthode : il n'existe pas de procédure de vote neutre (flèche du théorème\n", - " d'Arrow, dont le cycle et le spoiler ci-dessus sont les deux briques\n", - " empiriques).\n", - "- Les **métriques** (consensus, justice de Gini, satisfaction) rendent ces\n", - " divergences mesurables au lieu de les débattre.\n", - "- La gouvernance se teste comme un système : **bruit byzantin, manipulation,\n", - " stabilité multi-seed** — exactement les axes que le banc de scénarios du\n", - " source étiquetait.\n", + "La leçon d'ingénierie est celle du cœur EPITA (#1981) : ne jamais additionner dans un même compteur des algorithmes qui répondent à des questions différentes. 15 algorithmes, trois familles, une seule discipline — nommer ce qu'on mesure.\n", "\n", - "**Arc** : Agentic-3 (orchestrer le travail) → ce notebook (décider à\n", - "plusieurs) → Agentic-4 (capstone). L'organe `governance_methods.py` et ses 16\n", - "tests purs sont réutilisables pour vos propres scénarios : construire une\n", - "société, la faire voter sous les sept méthodes, et lire ce que chacune\n", - "sacrifie — c'est tout l'appareil qu'il faut pour cela." + "*Distillation de la consolidation du cœur EPITA (tronc `argumentation_analysis/agents/core/governance/` + sas `docs/coursia_contrib/governance_voting_methods.ipynb`), sous-grain 2 de l'EPIC #4960. Généalogie : `2.1.6_multiagent_governance_prototype` (projet étudiant, juin 2025).*" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (System)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -1152,14 +1080,14 @@ }, "papermill": { "default_parameters": {}, - "duration": 2.189152, - "end_time": "2026-09-22T10:12:09.905465+00:00", + "duration": 2.694508, + "end_time": "2026-09-25T08:36:40.604690+00:00", "environment_variables": {}, "exception": null, "input_path": "Argument_Analysis_Gouvernance_Multi_Agents.ipynb", - "output_path": "Argument_Analysis_Gouvernance_Multi_Agents.ipynb", + "output_path": "Argument_Analysis_Gouvernance_Multi_Agents_exec.ipynb", "parameters": {}, - "start_time": "2026-09-22T10:12:07.716313+00:00", + "start_time": "2026-09-25T08:36:37.910182+00:00", "version": "2.7.0" } }, diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md index 0a8abded2f..be5b6d2833 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md @@ -83,7 +83,7 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | Rank | [Ranking_Semantics](Argument_Analysis_Ranking_Semantics.ipynb) | Sémantiques de classement (h-Categoriser, fardeau) en pur Python : force numérique départageant des arguments de même statut Dung | Argumentation graduée | | Orchestration | [Orchestration_Modes](Argument_Analysis_Orchestration_Modes.ipynb) | Comparer 7 modes d'orchestration par le budget (distillation Triple Distillation de l'instrument EPITA #1735) : 3 registres d'arrêt (filet du harnais / déclaration du mode / réalité des phases), asymétrie d'axes largeur-délégation-dialogue, budget calibré dérivé de la mesure — mesures committées, pur stdlib | Arbitrage d'architectures multi-agents | | Dated | [Dated_Graphs](Argument_Analysis_Dated_Graphs.ipynb) | Instrument $G_t^{arg} \to G_{t+1}^{arg}$ (Epic #13303, issue #13310) : corpus daté → graphe AIF conforme (critère d'inclusion C1–C3 écrit, exclusions publiées) → projection Dung ; deux mesures d'écart de familles différentes (Jaccard structurelle nœuds/attaques + Jaccard sémantique sur extensions grounded), **contrôle négatif** (plancher de bruit par split de la même période) publié à côté de tout écart, **contrôle positif** à magnitude attendue écrite avant mesure (attaque de racine : sortie directe + réhabilitation paradoxale de la victime + cascade) — pur Python + rdflib, validation synthétique uniquement ; **hypothèse monotone** posée, vérifiée par inclusion et violée délibérément (retrait d'un déchu vs d'un accepté : ce que voient les deux mesures) | Argumentation temporelle / mesure | -| Gouv | [Gouvernance_Multi_Agents](Argument_Analysis_Gouvernance_Multi_Agents.ipynb) | Gouvernance multi-agents (distillation EPITA 2.1.6, sous-grain 2 #4960) : organe pur `governance_methods.py` — 7 méthodes de vote/consensus (majorité, Borda, Condorcet à repli, quadratique, byzantin, Raft) sur 6 scénarios étiquetés, métriques consensus/justice Gini/satisfaction, **spoiler et cycle de Condorcet mesurés**, robustesse byzantine (courbe non monotone expliquée), manipulation et coalitions — pur stdlib Python, 6 divergences source documentées | Choix collectif | +| Gouv | [Gouvernance_Multi_Agents](Argument_Analysis_Gouvernance_Multi_Agents.ipynb) | Gouvernance multi-agents (distillation du cœur EPITA — tronc `agents/core/governance/` + sas `governance_voting_methods`, sous-grain 2 #4960) : organe pur `governance_methods.py` — **15 algorithmes** (5 scrutins : majorité/plurality/Borda/Condorcet/quadratique · 2 protocoles : byzantin/Raft · 8 fonctions de choix social : approval/STV/Copeland/Kemeny-Young/Schulze...), correction #1981 (« 7 méthodes » = erreur de catégorie), métriques justice Gini/satisfaction, duels pairwise, paradoxe de Condorcet, manipulation Borda/Gibbard-Satterthwaite — pur stdlib Python, deltas tronc↔prototype documentés | Choix collectif | | Obs | [Observatoire-1-Initiation](Argument_Analysis_Observatoire-1-Initiation.ipynb) | Cas 1 de l'Observatoire (Epic #13303, livré #16431) : Bumble « Opening Moves » (mars–avril 2024), DiD imparfait sur avis datés — instrument Dated_Graphs **rebranché, pas réécrit** (module `_dated_graphs_mod.py`), corpus Arctic Shift 4 bras × 2 fenêtres stratifiés mensuellement, extraction LLM Ollama (qwen2.5:7b-instruct-q4_K_M, température 0, échantillon complet), agrégats public-safe, plancher de bruit split-half chronologique — verdict sur quatre observations conjointes, plafond de preuve déclaré | Étude de cas / mesure empirique | | Route | [Multi_Backend_Routing](Argument_Analysis_Multi_Backend_Routing.ipynb) | Routage multi-backend « décider ou échouer bruyamment » : PL/Modal/Dung/FOL décidés par Tweety embarqué + sentinelle de contrat de livraison gardant les prouveurs externes (EProver/Mace4) — doctrine anti-théâtre / fail-loud | Raisonnement robuste | | Comm | [Communication_Channels](Argument_Analysis_Communication_Channels.ipynb) | Bus de communication multi-agents (distillation Triple Distillation du tronc EPITA) : format message à priorité inversée, contrat de canal fail-loud (#2161), routage sans routes mortes (#1571), corrélation requête-réponse — pur stdlib Python, déterministe | Communication multi-agents | @@ -120,7 +120,7 @@ L'analyse argumentative outillée s'inscrit dans plusieurs cas concrets où la d | **Ranking_Semantics** | Calculer la *force* numérique d'un argument (h-Categoriser par point fixe, fardeau par comparaison lexicographique) et départager des arguments que Dung déclare indistinctement rejetés — pur stdlib Python | 35 min | | **Orchestration_Modes** | Reconstruire les tableaux sous-budget/calibré d'un comparatif de 7 modes d'orchestration, nommer les 3 registres d'arrêt, et dériver un budget calibré — avec la limite mesurée de la projection linéaire (1081 s naïfs vs 600 s réels) | 30 min | | **Dated_Graphs** | Construire l'instrument de comparaison temporelle des graphes d'argumentation : critère d'inclusion écrit d'un énoncé, graphe AIF conforme sérialisé en RDF, projection Dung, deux mesures d'écart de familles différentes, plancher de bruit (contrôle négatif) publié avec chaque mesure, et contrôle positif dont la magnitude attendue est dérivée à la main avant l'exécution | 45 min | -| **Gouvernance_Multi_Agents** | Comparer sept méthodes de vote sur les mêmes préférences (effet spoiler, cycle de Condorcet, repli Borda), mesurer justice de Gini et satisfaction, puis tester la résistance de la décision : bruit byzantin, manipulation stratégique, stabilité multi-seed — pur stdlib Python | 35 min | +| **Gouvernance_Multi_Agents** | Comparer les scrutins d'agrégation sur les mêmes préférences (majorité vs Borda, duels pairwise, cycle de Condorcet), mesurer justice de Gini et satisfaction, puis distinguer des protocoles de tolérance aux pannes (bruit byzantin, Raft) et le choix social formel (STV, Copeland, Kemeny-Young, Schulze) — correction #1981, pur stdlib Python | 35 min | | **Observatoire-1-Initiation** | Conduire un cas empirique pré-inscrit de bout en bout : reprise de l'instrument Dated_Graphs sans réécriture (module dédié, garde anti-repli), corpus daté stratifié mensuellement, extraction LLM épinglée (modèle, température 0, schéma JSON), mesure via l'instrument avec plancher de bruit split-half chronologique, et plafond de preuve déclaré (DiD imparfait) | — | | **Restitution_3_Actes** | Séparer la *lisibilité* (confiée au LLM) de l'*honnêteté* (gardée par un scaffold déterministe) : extraction d'evidence, bande de verdict *gated* sur la couverture, gate de tissage anti-énumération (§4), renderer qui *nomme* les actes manquants, et narration LLM injectable *fail-loud* | 45 min | | **Communication_Channels** | Construire un bus de communication multi-agents : messages à priorité inversée, filtres fail-loud, routage sans routes mortes, corrélation requête-réponse — pur stdlib Python | 30 min | @@ -520,7 +520,7 @@ en référence. | `Argument_Analysis_Dialogues_Protocolises.ipynb` | Protocoles Walton–Krabbe : inquiry/persuasion comme machines à états sur actes de parole | aucun (indépendant ; voisin de Toulmin_Model par la structure de l'échange, des Agentic par la multi-agentique) | | `Argument_Analysis_Knowledge_Base.ipynb` | Mémoire d'un débat : population transitive des propositions, requêtes support/attaque par négation lexicale `¬` | aucun (indépendant ; complète `Toulmin_Model`, `Schemes_Walton` et `Dialogues_Protocolises`, cités en contexte) | | `Argument_Analysis_Ranking_Semantics.ipynb` | Sémantiques graduées (h-Categoriser, fardeau) | `Dung_AF_Semantics` | -| `Argument_Analysis_Gouvernance_Multi_Agents.ipynb` | Gouvernance multi-agents (distillation EPITA 2.1.6, #4960) — vote/consensus/coalitions/manipulation | aucun (indépendant ; voisin d'`Agentic-3-orchestration` par la question — décider à plusieurs vs orchestrer le travail — pas par le code) | +| `Argument_Analysis_Gouvernance_Multi_Agents.ipynb` | Gouvernance multi-agents (distillation du cœur EPITA, #4960) — scrutins / protocoles de consensus / choix social formel (#1981) | aucun (indépendant ; voisin d'`Agentic-3-orchestration` par la question — décider à plusieurs vs orchestrer le travail — pas par le code) | | `Argument_Analysis_Dated_Graphs.ipynb` | Instrument $G_t^{arg} \to G_{t+1}^{arg}$ (Epic #13303, issue #13310) | `Dung_AF_Semantics` | | `Argument_Analysis_Observatoire-1-Initiation.ipynb` | Cas 1 de l'Observatoire (Epic #13303, livré #16431) : étude empirique DiD imparfait consommant l'instrument | `Dated_Graphs` | diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py index 03482cf98e..4f37b09b47 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/governance_methods.py @@ -1,125 +1,229 @@ # -*- coding: utf-8 -*- -"""Mécanismes de gouvernance multi-agents — distillation du sous-projet EPITA 2.1.6. - -Provenance : `2.1.6_multiagent_governance_prototype` du dépôt étudiant -`jsboigeEpita/2025-Epita-Intelligence-Symbolique` (~1390 lignes Python + 16 -scénarios JSON). Mandat Triple Distillation (EPIC #4960) : porter l'essence -vivante « sans le bruit d'une année de régressions et d'itérations ». - -Partie vivante retenue : -- `governance/methods.py` : 7 méthodes de vote/consensus (majorité, pluralité, - Borda, Condorcet avec repli Borda, vote quadratique, consensus byzantin, - consensus Raft). -- `agents/base_agent.py` : personnalités (stubborn / flexible / strategic / - random) + préférences ordonnées + confiance inter-agents. -- `governance/simulation.py` : formation de coalitions par confiance > 0.8, - vote par blocs, détection de conflits. -- `metrics/metrics.py` : taux de consensus, Gini/justice, satisfaction moyenne, - stabilité. -- `scénarios/*.json` : banc de 16 scénarios étiquetés — 6 distillés ici. - -Partie NON portée (bruit, cf. divergences) : la médiation de -`conflict_resolution.py` (probabilités de succès codées en dur 0.8/0.5/0.7), -le Q-learning epsilon-greedy de `base_agent`, le CLI/runner, la visualisation -matplotlib, les couches BDI/Reactive (stubs de protocole vides). - -Divergences documentées (mesurées sur le source, pas supposition) : -1. `simulate_governance` du source assigne `method_fn = GOVERNANCE_METHODS[method]` - puis ne l'appelle JAMAIS : les 7 méthodes sont du code mort dans la boucle - de simulation (le gagnant sort d'un tally de blocs par préférence du leader). - L'organe les branche réellement : `simulate_vote` applique la méthode. -2. `quadratic_voting` du source n'implémente PAS le coût quadratique annoncé - (aucune somme de carrés) : un agent "flexible" coupe juste son budget en - budget//2 + reste, un autre met tout sur son premier choix. Porté tel quel, - avec l'exercice 1 du notebook = implémenter le vrai vote quadratique - (coût = somme des carrés des voix, allocation optimale = racine carrée). -3. `byzantine_consensus` du source désigne les byzantins comme les n PREMIERS - agents (`agents[:n]`, tranche déterministe), pas un tirage. Porté fidèle. -4. La voie `update_memory` du `base_agent` source référence `options` hors de - sa portée : NameError dès le deuxième appel, que le contexte porte - "options" ou non (l'appel à `update_q` passe `options` sans condition ; - mesuré sur le source). La satisfaction est recalculée ici proprement à - partir des préférences. -5. La médiation à probabilités fixes n'est pas portée (pseudo-mécanisme) ; - seule `detect_conflicts` (paires d'agents à positions différentes) l'est. -6. `condorcet_method` conserve le repli Borda du source (comportement standard - en l'absence de vainqueur de Condorcet — le notebook le démontre sur le - scénario du cycle). - -Module pur : stdlib uniquement, aucune dépendance (numpy du source remplacé par -`random.Random` injecté, donc seedable et déterministe). +"""Gouvernance multi-agents : scrutins, protocoles de consensus, choix social. + +Provenance (re-fondation 2026-09-25, option 1 ai-01 sur #17353) : +- **tronc** `argumentation_analysis/agents/core/governance/` du dépôt EPITA — + `governance_methods.py` (137 l.), `social_choice.py` (344 l.), + `governance_agent.py` (270 l.), `metrics.py` (118 l.). + Module vivant : dernier fix 2026-09-24 (#2576 — les métriques de consensus + nomment ce qu'elles ne peuvent pas calculer au lieu d'un 0.0 ou un None). +- **sas** `docs/coursia_contrib/governance_voting_methods.ipynb` (19 cellules, + 2026-07-15) : le cadre pédagogique (scénario club, duels, paradoxe de + Condorcet, manipulation de Borda / Gibbard-Satterthwaite). +- Généalogie : `2.1.6_multiagent_governance_prototype` (projet étudiant, + juin 2025) — le tronc en est « Adapted from » et le corrigé ; ce module + documente les deltas cœur ↔ prototype. + +.. warning:: + Erreur de catégorie #1981 (le tronc l'interdit nommément) : « 7 méthodes + de vote » est **faux**. La surface consolidée = + + - **5 scrutins** d'agrégation de préférences : `majority_voting`, + `plurality_voting` (alias), `borda_count`, `condorcet_method`, + `quadratic_voting` ; + - **2 protocoles** de tolérance aux pannes (*pas* des scrutins, ne se + comparent pas à Borda) : `byzantine_consensus`, `raft_consensus` ; + - **8 fonctions de choix social formel** (profils de préférences) : + `approval_voting`, `stv`, `copeland`, `kemeny_young`, + `kemeny_young_safe`, `schulze`, `condorcet_winner`, `pairwise_matrix`. + + Total = **15 algorithmes**, pas un seul compteur interchangeable. + +Deltas tronc ↔ prototype étudiant 2.1.6 (mesurés sur le source, pas supposés) : + +1. `quadratic_voting` **conserve le défaut du prototype** (inchangé dans le + tronc) : aucune somme de carrés — un agent `flexible` coupe son budget en + deux, les autres mettent tout sur leur premier choix. Porté fidèle ; + l'exercice 1 du notebook = implémenter le vrai vote quadratique. +2. Le tronc n'a pas de boucle de simulation de coalitions par confiance > 0.8 + (la `Simulation` du prototype, avec son `simulate_governance` au + `method_fn` jamais appelé) : elle n'est **pas portée**. Le tronc garde les + personnalités et la confiance dans `Agent.decide`/`negotiate`. +3. `metrics.py` du tronc traite les 3 formes de `votes` (#1273 : liste, + tally map, per-agent map) et nomme les non-calculables (#2576) — le + prototype retournait 0.0 ou None silencieusement. +4. `social_choice.py` (couche formelle : STV, Copeland, Kemeny-Young, + Schulze) est **absent du prototype** — c'est l'ajout du cœur. +5. Le Q-learning epsilon-greedy et les archétypes BDI/Reactive de + `governance_agent.py` sont portés, la `Simulation` non. + +Module pur : stdlib uniquement (`random` injecté, déterministe ; `numpy` +du tronc remplacé par `random.Random`). """ from __future__ import annotations +import itertools import random from collections import Counter -from itertools import permutations -from math import factorial +from typing import Any, Dict, List, Optional, Tuple -# --------------------------------------------------------------------------- -# Agent a personnalité -# --------------------------------------------------------------------------- +# ───────────────────────────────────────────────────────────────────────── +# Agents (port de governance_agent.py — personnalités, confiance, Q-learning) +# ───────────────────────────────────────────────────────────────────────── +PERSONALITIES = ["stubborn", "flexible", "strategic", "random"] -class GovernanceAgent: - """Agent de vote avec personnalité, préférences ordonnées et confiance. - Port simplifié du `Agent` de `agents/base_agent.py` : la personnalité - gouverne le vote sincère, la confiance gouverne les coalitions. +class Agent: + """Agent de vote : personnalité, préférences ordonnées, confiance, Q-learning. + + Port du `Agent` de `agents/core/governance/governance_agent.py`. + La personnalité gouverne le vote sincère (`decide`) ; la confiance et + les coalitions gouvernent la négociation (`negotiate`). """ - def __init__(self, name, personality, préférences, trust=None): + def __init__(self, name, personality, preferences, rng=None): self.name = name self.personality = personality # stubborn | flexible | strategic | random - self.preferences = list(préférences) - self.trust = dict(trust or {}) - self.rng = random.Random(0) # reseedable depuis l'extérieur - - def decide(self, options, context=None, rng=None): - """Vote sincère selon la personnalité (port de Agent.decide, sans Q-learning).""" - ctx = context or {} - rng = rng or self.rng - if ctx.get("coalition_leader") and self.coalition_id: - return ctx["coalition_leader"] + self.preferences = list(preferences) + self.trust: Dict[str, float] = {} + self.coalition: Optional[str] = None + self.memory: List[Dict[str, Any]] = [] + self.satisfaction_history: List[float] = [] + self.q_table: Dict[Tuple, float] = {} + self.epsilon = 0.1 + self.alpha = 0.5 + self.gamma = 0.9 + self.rng = rng or random.Random(0) + + def _state(self, options): + return (self.preferences[0] if self.preferences else "none", + self.coalition or "none") + + def choose_action(self, options): + """Epsilon-greedy sur la Q-table (port du tronc).""" + if self.rng.random() < self.epsilon: + return self.rng.choice(options) + state = self._state(options) + qv = [self.q_table.get((state, o), 0.0) for o in options] + mx = max(qv) + return self.rng.choice([o for o, q in zip(options, qv) if q == mx]) + + def update_q(self, options, action, reward, next_options): + """Mise à jour Q-learning (port du tronc).""" + state = self._state(options) + ns = self._state(next_options) + nxt = max([self.q_table.get((ns, o), 0.0) for o in next_options] or [0.0]) + old = self.q_table.get((state, action), 0.0) + self.q_table[(state, action)] = old + self.alpha * (reward + self.gamma * nxt - old) + + def decide(self, options, context=None): + """Décision principale selon la personnalité (port du tronc).""" + if context and self.coalition and "coalition_leader" in context: + return context["coalition_leader"] + if len(self.satisfaction_history) >= 3 and (sum(self.satisfaction_history[-3:]) / 3) < 0.3: + if self.personality == "stubborn": + self.personality = "flexible" if self.personality == "stubborn": return self.preferences[0] if self.personality == "flexible": - hint = ctx.get("majority_hint") - if hint in options: - return hint + if context and context.get("majority_hint") in options: + return context["majority_hint"] return self.preferences[0] if self.personality == "strategic": - likely = ctx.get("likely_winner") - if likely in self.preferences and self.trust and max(self.trust.values()) > 0.7: - return likely + if context and "likely_winner" in context and self.trust: + likely = context["likely_winner"] + if likely in self.preferences and max(self.trust.values()) > 0.7: + return likely return self.preferences[1] if len(self.preferences) > 1 else self.preferences[0] - return rng.choice(sorted(options)) + return self.choose_action(options) + + def update_memory(self, decision, outcome, context=None): + """Historique + satisfaction + Q-learning (port du tronc, options portée).""" + self.memory.append({"decision": decision, "outcome": outcome, "context": context}) + sat = (1.0 - self.preferences.index(outcome) / max(1, len(self.preferences) - 1) + if outcome in self.preferences else 0.0) + self.satisfaction_history.append(sat) + if len(self.memory) > 1: + prev = self.memory[-2] + prev_opts = (prev["context"] or {}).get("options", []) + if prev_opts: + self.update_q(prev_opts, prev["decision"], sat, []) + if context and "proposed_by" in context: + p = context["proposed_by"] + if p != self.name: + self.trust[p] = min(1.0, self.trust.get(p, 0.5) + 0.1) + + def negotiate(self, options, context=None): + """Négociation : coalition / propose / accept / argue (port du tronc).""" + if self.trust and max(self.trust.values()) > 0.8: + return ("form_coalition", max(self.trust, key=self.trust.get)) + if self.personality == "stubborn": + return ("propose", self.preferences[0]) + if self.personality == "flexible": + if context and context.get("proposed") in self.preferences[:2]: + return ("accept", context["proposed"]) + return ("propose", self.preferences[0]) + if self.personality == "strategic": + if context and "proposed" in context and context["proposed"] != self.preferences[0]: + return ("argue", self.preferences[1] if len(self.preferences) > 1 + else self.preferences[0]) + return ("propose", self.preferences[0]) + return ("propose", self.rng.choice(options)) - coalition_id = None # posé par form_coalitions + def propose_argument(self, option, reason): + return {"agent": self.name, "option": option, "reason": reason} + def receive_argument(self, argument): + if self.personality == "flexible" and argument["option"] in self.preferences: + idx = self.preferences.index(argument["option"]) + if idx > 0: + self.preferences.pop(idx) + self.preferences.insert(0, argument["option"]) -# --------------------------------------------------------------------------- -# Methodes de vote (port de governance/methods.py) -# --------------------------------------------------------------------------- +class BDIAgent(Agent): + """Agent croyances-désirs-intentions (port du tronc).""" -def majority_voting(agents, options, context=None, rng=None): - """Majorité : chaque agent vote son premier choix, le plus voté gagne.""" - votes = [a.decide(options, context, rng) for a in agents] - winner, _ = Counter(votes).most_common(1)[0] - return winner + def __init__(self, name, personality, preferences, rng=None): + super().__init__(name, personality, preferences, rng) + self.beliefs, self.desires, self.intentions = set(), set(), set() + def decide(self, options, context=None): + for i in self.intentions: + if i in options: + return i + return super().decide(options, context) -def plurality_voting(agents, options, context=None, rng=None): - """Pluralité : identique à la majorité pour un vainqueur unique (port fidèle).""" - return majority_voting(agents, options, context, rng) +class ReactiveAgent(Agent): + """Agent réactif à règles (port du tronc).""" -def borda_count(agents, options, context=None, rng=None): - """Borda : n-1 points au premier rang, n-2 au deuxième... le total max gagne.""" - scores = {o: 0 for o in options} + def __init__(self, name, personality, preferences, rng=None): + super().__init__(name, personality, preferences, rng) + self.rules = [] + + def add_rule(self, condition, action): + self.rules.append((condition, action)) + + def decide(self, options, context=None): + for condition, action in self.rules: + if condition(context): + return action(options, context) + return super().decide(options, context) + + +# ───────────────────────────────────────────────────────────────────────── +# Section 1 — les 5 scrutins (agrégation de préférences) +# ───────────────────────────────────────────────────────────────────────── + + +def majority_voting(agents, options, context=None): + """Scrutin majoritaire à un tour : chaque agent vote son 1er choix.""" + votes = [a.decide(options, context) for a in agents] + return Counter(votes).most_common(1)[0][0] + + +def plurality_voting(agents, options, context=None): + """Alias de majority_voting (scrutin à un vainqueur).""" + return majority_voting(agents, options, context) + + +def borda_count(agents, options, context=None): + """Score de Borda : n-1 au 1er choix, n-2 au 2e, etc.""" n = len(options) + scores = {o: 0 for o in options} for a in agents: for i, o in enumerate(a.preferences): if o in scores: @@ -127,324 +231,282 @@ def borda_count(agents, options, context=None, rng=None): return max(scores, key=scores.get) -def condorcet_method(agents, options, context=None, rng=None): - """Condorcet : duels pairwise ; vainqueur qui bat tous les autres, sinon repli Borda.""" +def condorcet_method(agents, options, context=None): + """Vainqueur de Condorcet (gagne tous ses duels) ou repli Borda.""" n = len(options) - pairwise_wins = {o: 0 for o in options} + wins = {o: 0 for o in options} for o1 in options: for o2 in options: if o1 == o2: continue - o1_wins = sum( - a.preferences.index(o1) < a.preferences.index(o2) for a in agents - ) - if o1_wins > len(agents) - o1_wins: - pairwise_wins[o1] += 1 + o1_w = sum(a.preferences.index(o1) < a.preferences.index(o2) + for a in agents if o1 in a.preferences and o2 in a.preferences) + if o1_w > len(agents) - o1_w: + wins[o1] += 1 for o in options: - if pairwise_wins[o] == n - 1: + if wins[o] == n - 1: return o - return borda_count(agents, options, context, rng) + return borda_count(agents, options, context) -def quadratic_voting(agents, options, context=None, rng=None): - """Vote "quadratique" du SOURCE (divergence 2) : budget coupé ou tout-misé. +def quadratic_voting(agents, options, context=None): + """Vote quadratique (défaut du prototype conservé, delta n° 1). - Le source annonce un coût en somme de carrés mais ne l'implémente pas : - un agent flexible coupe son budget en deux sur ses deux premiers choix, - les autres mettent tout sur leur premier choix. L'exercice 1 du notebook - fait implémenter le vrai vote quadratique. + .. warning:: + N'implémente PAS le coût quadratique annoncé (aucune somme de + carrés) : un agent `flexible` coupe son budget en deux, les autres + mettent tout sur leur premier choix. Le tronc l'a conservé tel + quel ; l'exercice du notebook = implémenter le vrai vote + quadratique (coût = somme des carrés des voix). """ budget = (context or {}).get("quadratic_budget", 9) votes = {o: 0 for o in options} for a in agents: + alloc = [0] * len(options) + top = a.preferences[0] if a.preferences else options[0] if a.personality == "flexible" and len(options) > 1: - votes[options[0]] += budget // 2 - votes[options[1]] += budget - (budget // 2) + ti = options.index(top) if top in options else 0 + second = a.preferences[1] if len(a.preferences) > 1 else options[0] + si = options.index(second) if second in options else 0 + alloc[ti] = budget // 2 + alloc[si] = budget - budget // 2 else: - votes[a.preferences[0]] += budget + ti = options.index(top) if top in options else 0 + alloc[ti] = budget + for i, o in enumerate(options): + votes[o] += alloc[i] return max(votes, key=votes.get) -def byzantine_consensus(agents, options, context=None, rng=None): - """Consensus byzantin : une fraction vote aléatoire (les n PREMIERS agents — divergence 3).""" - rng = rng or random.Random(0) - ratio = (context or {}).get("byzantine_ratio", 0.2) - n_byz = int(len(agents) * ratio) - votes = [a.decide(options, context, rng) for a in agents[n_byz:]] - votes += [rng.choice(sorted(options)) for _ in agents[:n_byz]] - winner, _ = Counter(votes).most_common(1)[0] - return winner - - -def raft_consensus(agents, options, context=None, rng=None): - """Raft : un leader tiré au sort propose, majorité d'acceptations requise (top-2).""" - rng = rng or random.Random(0) - leader = rng.choice(sorted(agents, key=lambda a: a.name)) - proposal = leader.decide(options, context, rng) - acceptances = 1 - for a in agents: - if a is not leader and proposal in a.preferences[:2]: - acceptances += 1 - if acceptances > len(agents) // 2: - return proposal - return majority_voting(agents, options, context, rng) - - -GOVERNANCE_METHODS = { +SCRUTINS = { "majority": majority_voting, "plurality": plurality_voting, "borda": borda_count, "condorcet": condorcet_method, "quadratic": quadratic_voting, - "byzantine": byzantine_consensus, - "raft": raft_consensus, } -# --------------------------------------------------------------------------- -# Coalitions, conflits, manipulation (port de governance/simulation.py) -# --------------------------------------------------------------------------- +# ───────────────────────────────────────────────────────────────────────── +# Section 2 — les 2 protocoles de tolérance aux pannes (PAS des scrutins) +# ───────────────────────────────────────────────────────────────────────── -def form_coalitions(agents, trust_threshold=0.8): - """Coalitions par confiance : un agent rejoint le premier partenaire à confiance > seuil. +def byzantine_consensus(agents, options, context=None, rng=None): + """Consensus byzantin : une fraction d'agents vote au hasard. - Port de la boucle de coalition de `simulate_governance` (le source fait - confiance > 0.8 en dur) : chaque coalition vote la préférence de son leader. + Protocole de tolérance aux pannes, **pas** un scrutin : ne se compare + pas à Borda (erreur de catégorie #1981). Le tronc désigne les + byzantins comme les n PREMIERS agents (tranche déterministe) — porté + fidèle. """ - unassigned = list(agents) - coalitions = [] - for a in agents: - a.coalition_id = None - i = 0 - while i < len(unassigned): - agent = unassigned[i] - partners = [p for p in unassigned[i + 1:] if agent.trust.get(p.name, 0) > trust_threshold] - coalition = [agent] + partners - cid = f"coalition_{len(coalitions) + 1}" - for p in coalition: - p.coalition_id = cid - coalitions.append(coalition) - for p in partners: - unassigned.remove(p) - i += 1 - return coalitions - - -def detect_conflicts(positions): - """Conflits : chaque paire d'agents à positions différentes (port fidèle).""" - names = list(positions.keys()) - conflicts = [] - for i in range(len(names)): - for j in range(i + 1, len(names)): - if positions[names[i]] != positions[names[j]]: - conflicts.append({"agents": [names[i], names[j]]}) - return conflicts - - -def apply_manipulation(agents, manipulation_type, target=None, rng=None): - """Manipulation du vote (port de simulate_manipulation, voie decide rebind). - - 'strategic' : chaque agent vote son 2e choix. - 'false_coalition' : la moitié des agents vote la cible. - 'bribery' : le retour est une copie où n agents votent la cible (n = budget). - """ - import copy - + ratio = (context or {}).get("byzantine_ratio", 0.2) rng = rng or random.Random(0) - copies = [copy.deepcopy(a) for a in agents] - if manipulation_type == "strategic": - for a in copies: - if len(a.preferences) > 1: - a.personality = "stubborn" - a.preferences = [a.preferences[1], a.preferences[0]] + a.preferences[2:] - elif manipulation_type == "false_coalition": - t = target or a.preferences[0] - for a in copies[: len(copies) // 2]: - a.personality = "stubborn" - a.preferences = [t] + [p for p in a.preferences if p != t] - return copies + n_b = int(len(agents) * ratio) + honest = agents[n_b:] + votes = [a.decide(options, context) for a in honest] + votes += [rng.choice(options) for _ in range(n_b)] + return Counter(votes).most_common(1)[0][0] -def shapley_value(member_names, payoff_func): - """Valeur de Shapley d'une coalition (port fidèle, permutations factorielles).""" - n = len(member_names) - if n == 0: - return {} - values = {m: 0.0 for m in member_names} - for perm in permutations(member_names): - prev = set() - for m in perm: - marginal = payoff_func(prev | {m}) - payoff_func(prev) - values[m] += marginal / factorial(n) - prev.add(m) - return values - - -# --------------------------------------------------------------------------- -# Métriques (port de metrics/metrics.py, numpy retiré) -# --------------------------------------------------------------------------- - - -def satisfaction_of(agent, winner): - """Satisfaction d'un agent : 1 - rang du vainqueur dans ses préférences, normalisé.""" - if winner not in agent.preferences: - return 0.0 - denom = max(1, len(agent.preferences) - 1) - return 1.0 - agent.preferences.index(winner) / denom - - -def consensus_rate(result): - """Fraction des votes alignés sur le vainqueur.""" - votes, winner = result["votes"], result["winner"] - return votes.count(winner) / len(votes) if votes else 0.0 - - -def gini(values): - """Coefficient de Gini (formule du source, en Python pur, epsilon inclus).""" - vals = sorted(float(v) for v in values) - n = len(vals) - if n == 0: - return 0.0 - vals = [v - min(0.0, vals[0]) + 1e-8 for v in vals] - total = sum(vals) - if total <= 0: - return 0.0 - weighted = sum((2 * i - n - 1) * v for i, v in enumerate(vals, start=1)) - return weighted / (n * total) - - -def fairness_index(result): - """Justice = 1 - Gini des satisfactions.""" - return 1.0 - gini(result["satisfaction"]) - - -def mean_satisfaction(result): - """Satisfaction moyenne du collectif.""" - return sum(result["satisfaction"]) / len(result["satisfaction"]) if result["satisfaction"] else 0.0 - - -def stability(winners): - """Stabilite : 1 si un seul vainqueur sur la serie, 0 sinon (port fidele).""" - return 1.0 if len(set(winners)) == 1 else 0.0 - - -# --------------------------------------------------------------------------- -# Simulation (corrigée : la méthode est APPELÉE — divergence 1) -# --------------------------------------------------------------------------- - - -def simulate_vote(agents, options, method, context=None, seed=0): - """Applique RÉELLEMENT la méthode de vote, puis mesure votes et satisfactions. +def raft_consensus(agents, options, context=None, rng=None): + """Raft : élection d'un leader + proposition ; majorité ou repli majoritaire. - Divergence 1 : le source n'appelait jamais sa méthode ; ici chaque méthode - de GOVERNANCE_METHODS tourne sur les memes agents. + Protocole de tolérance aux pannes, **pas** un scrutin (#1981). """ - rng = random.Random(seed) - method_fn = GOVERNANCE_METHODS[method] - winner = method_fn(agents, options, context, rng) - votes = [a.decide(options, context, rng) for a in agents] - satisfaction = [satisfaction_of(a, winner) for a in agents] - positions = {a.name: v for a, v in zip(agents, votes)} - return { - "method": method, - "winner": winner, - "votes": votes, - "satisfaction": satisfaction, - "agent_names": [a.name for a in agents], - "conflicts": detect_conflicts(positions), - } - - -def summarize(result): - """Résume une simulation : consensus, justice, satisfaction.""" - return { - "consensus_rate": round(consensus_rate(result), 3), - "fairness": round(fairness_index(result), 3), - "satisfaction": round(mean_satisfaction(result), 3), - } - - -# --------------------------------------------------------------------------- -# Banc de scénarios (6 des 16 du source, distillés) -# --------------------------------------------------------------------------- - -SCENARIOS = { - "dictatorship": { - "description": "Un agent stratégique impose sa volonté face à des préférences diverses.", - "options": ["A", "B", "C"], - "agents": [ - ("D1", "strategic", ["A", "B", "C"]), - ("O1", "stubborn", ["B", "C", "A"]), - ("O2", "flexible", ["C", "A", "B"]), - ("O3", "random", ["B", "A", "C"]), - ("O4", "stubborn", ["C", "B", "A"]), - ], - }, - "cyclic_majority": { - "description": "Cycle de Condorcet : A bat B, B bat C, C bat A — aucun vainqueur net.", - "options": ["A", "B", "C"], - "agents": [ - ("V1", "stubborn", ["A", "B", "C"]), - ("V2", "stubborn", ["B", "C", "A"]), - ("V3", "stubborn", ["C", "A", "B"]), - ], - }, - "spoiler_candidate": { - "description": "Un candidat similaire divise un bloc : l'adversaire l'emporte.", - "options": ["Gauche", "Centre", "Droite"], - "agents": [ - ("G1", "stubborn", ["Gauche", "Centre", "Droite"]), - ("G2", "stubborn", ["Centre", "Gauche", "Droite"]), - ("D1", "stubborn", ["Droite", "Centre", "Gauche"]), - ], - }, - "strategic_bloc": { - "description": "Un bloc discipliné affronte des votes dispersés.", - "options": ["A", "B", "C"], - "agents": [ - ("B1", "stubborn", ["A", "B", "C"]), - ("B2", "stubborn", ["A", "C", "B"]), - ("B3", "stubborn", ["A", "B", "C"]), - ("S1", "stubborn", ["B", "C", "A"]), - ("S2", "stubborn", ["C", "B", "A"]), - ], - }, - "byzantine_noise": { - "description": "Une fraction d'agents vote au hasard : robustesse du consensus.", - "options": ["A", "B", "C"], - "agents": [ - ("H1", "stubborn", ["A", "B", "C"]), - ("H2", "stubborn", ["A", "C", "B"]), - ("H3", "stubborn", ["A", "B", "C"]), - ("Z1", "random", ["B", "A", "C"]), - ("Z2", "random", ["C", "B", "A"]), - ], - }, - "project_funding": { - "description": "Choix de budget : préférences ordonnées sur trois projets.", - "options": ["Route", "Ecole", "Clinique"], - "agents": [ - ("M1", "stubborn", ["Route", "Ecole", "Clinique"]), - ("M2", "stubborn", ["Ecole", "Clinique", "Route"]), - ("M3", "flexible", ["Clinique", "Ecole", "Route"]), - ("M4", "flexible", ["Ecole", "Route", "Clinique"]), - ], - }, -} + rng = rng or random.Random(0) + leader = rng.choice(agents) + proposal = leader.decide(options, context) + accept = 1 + for a in agents: + if a is leader: + continue + if proposal in a.preferences[:2]: + accept += 1 + return proposal if accept > len(agents) // 2 else majority_voting(agents, options, context) -def build_agents(scenario_name, trust=None): - """Matérialise les agents d'un scénario (tuples -> GovernanceAgent).""" - sc = SCENARIOS[scenario_name] - return [GovernanceAgent(n, p, prefs, (trust or {}).get(n)) for n, p, prefs in sc["agents"]] +PROTOCOLES = { + "byzantine": byzantine_consensus, + "raft": raft_consensus, +} -def method_counts(): - """Inventaire de l'organe (pour tests purs, sans exécution).""" - return { - "methods": len(GOVERNANCE_METHODS), - "method_keys": sorted(GOVERNANCE_METHODS.keys()), - "scenarios": len(SCENARIOS), - "scenario_names": sorted(SCENARIOS.keys()), - } +# ───────────────────────────────────────────────────────────────────────── +# Section 3 — le choix social formel (profils de préférences) +# ───────────────────────────────────────────────────────────────────────── +# +# Fonctions sur des profils de bulletins ordonnés (List[List[str]]), +# indépendantes de la classe Agent. Ajout du cœur — absent du prototype. + + +def approval_voting(ballots, options, approval_threshold=2): + """Approval voting : chaque électeur approuve ses k premiers choix.""" + counts = {o: 0 for o in options} + for b in ballots: + for c in b[:approval_threshold]: + if c in counts: + counts[c] += 1 + return (max(counts, key=counts.get) if counts else None), counts + + +def stv(ballots, options, seats=1): + """Vote unique transférable (IRV pour 1 siège) : quota de Droop + transfert.""" + remaining = set(options) + active = [list(b) for b in ballots] + winners, rounds = [], [] + quota = len(ballots) // (seats + 1) + 1 + while remaining and len(winners) < seats: + first = Counter() + for b in active: + for c in b: + if c in remaining: + first[c] += 1 + break + if not first: + break + info = {"counts": dict(first), "remaining": sorted(remaining)} + elected = [c for c, n in first.items() if n >= quota] + if elected: + for c in elected: + winners.append(c) + remaining.discard(c) + info["elected"] = elected + rounds.append(info) + continue + if len(remaining) <= seats - len(winners): + winners.extend(sorted(remaining)) + info["elected"] = sorted(remaining) + rounds.append(info) + break + lowest = min(first, key=first.get) + remaining.discard(lowest) + info["eliminated"] = lowest + rounds.append(info) + return winners, rounds + + +def _pref_count(ballots, a, b): + """Nombre de bulletins préférant a à b (a absent = derrière).""" + n = 0 + for ballot in ballots: + ia = ballot.index(a) if a in ballot else len(ballot) + ib = ballot.index(b) if b in ballot else len(ballot) + if ia < ib: + n += 1 + return n + + +def copeland(ballots, options): + """Copeland : score = duels gagnés - duels perdus.""" + scores = {o: 0 for o in options} + for a in options: + for b in options: + if a == b: + continue + aw, bw = _pref_count(ballots, a, b), _pref_count(ballots, b, a) + if aw > bw: + scores[a] += 1 + elif bw > aw: + scores[a] -= 1 + return (max(scores, key=scores.get) if scores else None), scores + + +_MAX_KEMENY_CANDIDATES = 8 + + +def kemeny_young(ballots, options): + """Kemeny-Young : classement minimisant le désaccord total (O(n!), ≤ 8).""" + if len(options) > _MAX_KEMENY_CANDIDATES: + raise ValueError( + f"Kemeny-Young impraticable pour {len(options)} candidats " + f"(max {_MAX_KEMENY_CANDIDATES}) — utiliser kemeny_young_safe (#971)." + ) + pairwise = {(a, b): _pref_count(ballots, a, b) + for a in options for b in options if a != b} + best_rank, best_score = None, -1 + for perm in itertools.permutations(options): + score = sum(pairwise.get((perm[i], perm[j]), 0) + for i in range(len(perm)) for j in range(i + 1, len(perm))) + if score > best_score: + best_score, best_rank = score, list(perm) + return best_rank, best_score + + +def kemeny_young_safe(ballots, options): + """Kemeny-Young avec repli Copeland au-delà de 8 candidats (#971).""" + if len(options) <= _MAX_KEMENY_CANDIDATES: + ranking, score = kemeny_young(ballots, options) + return ranking, score, False + _, cs = copeland(ballots, options) + return sorted(options, key=lambda o: cs.get(o, 0), reverse=True), -1, True + + +def schulze(ballots, options): + """Schulze (Beatpath) : plus fort chemin entre toutes les paires.""" + n = len(options) + idx = {o: i for i, o in enumerate(options)} + d = [[0] * n for _ in range(n)] + for ballot in ballots: + for i, a in enumerate(ballot): + if a not in idx: + continue + for b in ballot[i + 1:]: + if b in idx: + d[idx[a]][idx[b]] += 1 + p = [[0] * n for _ in range(n)] + for i in range(n): + for j in range(n): + if i != j and d[i][j] > d[j][i]: + p[i][j] = d[i][j] + for k in range(n): + for i in range(n): + if i == k: + continue + for j in range(n): + if j in (i, k): + continue + p[i][j] = max(p[i][j], min(p[i][k], p[k][j])) + scores = {o: 0 for o in options} + for i in range(n): + for j in range(n): + if i != j and p[i][j] > p[j][i]: + scores[options[i]] += 1 + winner = max(scores, key=scores.get) if scores else None + paths = {options[i]: {options[j]: p[i][j] for j in range(n) if i != j} + for i in range(n)} + return winner, paths + + +def condorcet_winner(ballots, options): + """Vainqueur de Condorcet s'il existe (gagne tous ses duels).""" + for c in options: + if all(c == o or _pref_count(ballots, c, o) > _pref_count(ballots, o, c) + for o in options): + return c + return None + + +def pairwise_matrix(ballots, options): + """Matrice des préférences pairwise à partir des bulletins.""" + matrix = {a: {b: 0 for b in options if b != a} for a in options} + for ballot in ballots: + for i, a in enumerate(ballot): + if a not in matrix: + continue + for b in ballot[i + 1:]: + if b in matrix.get(a, {}): + matrix[a][b] += 1 + return matrix + + +SOCIAL_CHOICE = { + "approval": approval_voting, + "stv": stv, + "copeland": copeland, + "kemeny_young": kemeny_young, + "kemeny_young_safe": kemeny_young_safe, + "schulze": schulze, + "condorcet_winner": condorcet_winner, + "pairwise_matrix": pairwise_matrix, +} diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/tests/test_governance_methods.py b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/tests/test_governance_methods.py index 8aeaf20154..3324512678 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/tests/test_governance_methods.py +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/tests/test_governance_methods.py @@ -1,127 +1,158 @@ # -*- coding: utf-8 -*- -"""Tests de structure de governance_methods.py — purs, sans execution de vote aleatoire. +"""Tests de l'organe governance_methods re-fondé (sous-grain 2 #4960). -Fige les comptes distilles (7 methodes, 6 scenarios) et les proprietes -structurelles du port. La validation executee vit dans le notebook committé -(avec outputs), pas ici. +Vérifie la correction #1981 (5 scrutins + 2 protocoles + 8 fonctions de +choix social = 15 algorithmes), les deltas tronc ↔ prototype documentés, +et le comportement des 15 algorithmes sur profils synthétiques déterministes. """ - -import os +import random import sys -import unittest - -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -import governance_methods as g - - -class TestComptesMesures(unittest.TestCase): - """Fige l'inventaire de la distillation.""" - - def test_sept_methodes(self): - self.assertEqual(g.method_counts()["methods"], 7) - - def test_cles_methodes(self): - self.assertEqual( - g.method_counts()["method_keys"], - ["borda", "byzantine", "condorcet", "majority", "plurality", "quadratic", "raft"], - ) - - def test_six_scenarios(self): - self.assertEqual(g.method_counts()["scenarios"], 6) - self.assertEqual( - g.method_counts()["scenario_names"], - ["byzantine_noise", "cyclic_majority", "dictatorship", - "project_funding", "spoiler_candidate", "strategic_bloc"], - ) - - -class TestStructure(unittest.TestCase): - """Proprietes structurelles du port.""" - - def test_chaque_scenario_trois_champs(self): - for name, sc in g.SCENARIOS.items(): - self.assertIn("description", sc, name) - self.assertIn("options", sc, name) - self.assertIn("agents", sc, name) - - def test_preferences_dans_options(self): - for name, sc in g.SCENARIOS.items(): - for _, _, prefs in sc["agents"]: - for p in prefs: - self.assertIn(p, sc["options"], f"{name}: {p}") - - def test_personnalites_connues(self): - for name, sc in g.SCENARIOS.items(): - for _, perso, _ in sc["agents"]: - self.assertIn(perso, {"stubborn", "flexible", "strategic", "random"}, name) - - def test_plurality_meme_resultat_que_majority(self): - # Port fidele : le source definit plurality comme simple rappel de majority. - agents = g.build_agents("dictatorship") - opts = g.SCENARIOS["dictatorship"]["options"] - self.assertEqual( - g.plurality_voting(agents, opts, None, None), - g.majority_voting(agents, opts, None, None), - ) - - def test_determinisme_seed(self): - agents = g.build_agents("byzantine_noise") - opts = g.SCENARIOS["byzantine_noise"]["options"] - w1 = g.simulate_vote(agents, opts, "byzantine", seed=42)["winner"] - agents = g.build_agents("byzantine_noise") - w2 = g.simulate_vote(agents, opts, "byzantine", seed=42)["winner"] - self.assertEqual(w1, w2) - - -class TestVotesDeterministes(unittest.TestCase): - """Votes sans aleatoire (personnalites non random) : resultats calculables a la main.""" - - def test_majorite_bloc_strategique(self): - agents = g.build_agents("strategic_bloc") - w = g.majority_voting(agents, ["A", "B", "C"]) - self.assertEqual(w, "A") # 3 votes A contre 1 B, 1 C - - def test_borda_spoiler_elit_centre(self): - # G1[A,B,C] G2[C,G,D]... Borda cumule les seconds rangs : Centre gagne. - agents = g.build_agents("spoiler_candidate") - w = g.borda_count(agents, g.SCENARIOS["spoiler_candidate"]["options"]) - self.assertEqual(w, "Centre") - - def test_majorite_spoiler_elit_gauche(self): - agents = g.build_agents("spoiler_candidate") - w = g.majority_voting(agents, g.SCENARIOS["spoiler_candidate"]["options"]) - self.assertEqual(w, "Gauche") - - def test_condorcet_cycle_repli_borda(self): - # Cycle A>B>C>A : aucun vainqueur de Condorcet, repli Borda (egalite A=B=C=3). - agents = g.build_agents("cyclic_majority") - opts = g.SCENARIOS["cyclic_majority"]["options"] - w = g.condorcet_method(agents, opts) - # Borda rend 3=3=3, max() prend la premiere cle (A) — documenté divergence 6. - self.assertEqual(w, "A") - - def test_gini_borne(self): - self.assertEqual(g.gini([1, 1, 1, 1]), 0.0) - # Un seul detenteur parmi n : Gini tend vers (n-1)/n, ici 0.75. - self.assertAlmostEqual(g.gini([0, 0, 0, 10]), 0.75, delta=1e-6) - - def test_shapley_symetrie(self): - # Deux agents interchangeables => valeurs de Shapley egales. - vals = g.shapley_value(["X", "Y"], lambda s: 1.0 if len(s) >= 1 else 0.0) - self.assertAlmostEqual(vals["X"], vals["Y"]) - - def test_detect_conflicts_paires(self): - c = g.detect_conflicts({"a": "A", "b": "B", "c": "A"}) - self.assertEqual(len(c), 2) # (a,b) et (b,c) - - def test_manipulation_strategique_change_les_votes(self): - agents = g.build_agents("spoiler_candidate") - copies = g.apply_manipulation(agents, "strategic") - votes_avant = [a.decide(["Gauche", "Centre", "Droite"]) for a in agents] - votes_apres = [a.decide(["Gauche", "Centre", "Droite"]) for a in copies] - self.assertNotEqual(votes_avant, votes_apres) - - -if __name__ == "__main__": - unittest.main(verbosity=2) +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +import governance_methods as gm # noqa: E402 + + +def _agents(prefs_list, personality="stubborn", seed=0): + return [gm.Agent(f"A{i}", personality, p, rng=random.Random(seed + i)) + for i, p in enumerate(prefs_list)] + + +OPTIONS = ["Pizza", "Burger", "Sushi", "Raclette"] +PROFIL = [ + ["Pizza", "Sushi", "Burger", "Raclette"], + ["Pizza", "Raclette", "Sushi", "Burger"], + ["Pizza", "Burger", "Sushi", "Raclette"], + ["Burger", "Raclette", "Sushi", "Pizza"], + ["Burger", "Sushi", "Raclette", "Pizza"], + ["Sushi", "Burger", "Raclette", "Pizza"], + ["Raclette", "Burger", "Sushi", "Pizza"], +] + + +class TestCategories: + def test_5_scrutins(self): + assert set(gm.SCRUTINS) == {"majority", "plurality", "borda", + "condorcet", "quadratic"} + + def test_2_protocoles(self): + assert set(gm.PROTOCOLES) == {"byzantine", "raft"} + + def test_8_social_choice(self): + assert len(gm.SOCIAL_CHOICE) == 8 + + def test_total_15(self): + # Erreur #1981 : pas « 7 méthodes », mais 15 algorithmes. + assert len(gm.SCRUTINS) + len(gm.PROTOCOLES) + len(gm.SOCIAL_CHOICE) == 15 + + +class TestScrutins: + def test_majority_pizza(self): + # Pizza a 3 premières places sur 7 -> gagne à la majorité. + assert gm.majority_voting(_agents(PROFIL), OPTIONS) == "Pizza" + + def test_plurality_alias(self): + assert gm.plurality_voting(_agents(PROFIL), OPTIONS) == gm.majority_voting(_agents(PROFIL), OPTIONS) + + def test_borda_differe(self): + # Borda peut différer de la majorité (Pizza gagne la majorité, + # Burger ou Raclette peuvent gagner Borda selon le profil). + g = gm.borda_count(_agents(PROFIL), OPTIONS) + assert g in OPTIONS + + def test_condorcet_fallback_borda(self): + # Sans vainqueur de Condorcet, repli Borda. + cycle = [["A", "B", "C"], ["B", "C", "A"], ["C", "A", "B"]] + g = gm.condorcet_method(_agents(cycle), ["A", "B", "C"]) + assert g in ["A", "B", "C"] + + def test_quadratic_defaut_conserve(self): + # Delta n° 1 : le défaut du prototype est conservé (budget-split). + ag = _agents(PROFIL, personality="flexible") + g = gm.quadratic_voting(ag, OPTIONS, {"quadratic_budget": 9}) + assert g in OPTIONS + + +class TestProtocoles: + def test_byzantine_deterministe_seed(self): + a1 = gm.byzantine_consensus(_agents(PROFIL), OPTIONS, + {"byzantine_ratio": 0.2}, rng=random.Random(1)) + a2 = gm.byzantine_consensus(_agents(PROFIL), OPTIONS, + {"byzantine_ratio": 0.2}, rng=random.Random(1)) + assert a1 == a2 + + def test_raft_majorite(self): + g = gm.raft_consensus(_agents(PROFIL), OPTIONS, rng=random.Random(2)) + assert g in OPTIONS + + +class TestSocialChoice: + BALLOTS = [ + ["A", "B", "C"], + ["A", "C", "B"], + ["B", "A", "C"], + ["B", "C", "A"], + ["C", "A", "B"], + ] + OPT = ["A", "B", "C"] + + def test_approval(self): + w, counts = gm.approval_voting(self.BALLOTS, self.OPT, approval_threshold=2) + assert w in self.OPT + assert counts["A"] == 4 # A est dans le top-2 de 4 bulletins sur 5 + + def test_stv(self): + winners, rounds = gm.stv(self.BALLOTS, self.OPT, seats=1) + assert len(winners) == 1 and winners[0] in self.OPT + + def test_copeland(self): + w, scores = gm.copeland(self.BALLOTS, self.OPT) + assert w in self.OPT + + def test_kemeny_young(self): + ranking, score = gm.kemeny_young(self.BALLOTS, self.OPT) + assert len(ranking) == 3 and score >= 0 + + def test_kemeny_young_raise_au_dela_8(self): + import pytest + with pytest.raises(ValueError): + gm.kemeny_young(self.BALLOTS, [f"c{i}" for i in range(9)]) + + def test_kemeny_young_safe_fallback(self): + ranking, score, approx = gm.kemeny_young_safe(self.BALLOTS, + [f"c{i}" for i in range(9)]) + assert approx is True and score == -1 + + def test_schulze(self): + w, paths = gm.schulze(self.BALLOTS, self.OPT) + assert w in self.OPT and len(paths) == 3 + + def test_condorcet_winner(self): + # A bat B (3-2), bat C (3-2)... vérifier la présence/absence. + w = gm.condorcet_winner([["X", "Y"], ["X", "Y"], ["Y", "X"]], ["X", "Y"]) + assert w == "X" + + def test_pairwise_matrix(self): + m = gm.pairwise_matrix(self.BALLOTS, self.OPT) + assert m["A"]["B"] == 3 # A préféré à B sur 3 bulletins + + +class TestAgent: + def test_stubborn_premier_choix(self): + a = gm.Agent("x", "stubborn", ["A", "B"], rng=random.Random(0)) + assert a.decide(["A", "B"]) == "A" + + def test_flexible_majority_hint(self): + a = gm.Agent("x", "flexible", ["A", "B"], rng=random.Random(0)) + assert a.decide(["A", "B"], {"majority_hint": "B"}) == "B" + + def test_stubborn_devient_flexible(self): + a = gm.Agent("x", "stubborn", ["A", "B"], rng=random.Random(0)) + a.satisfaction_history = [0.0, 0.0, 0.0] + a.decide(["A", "B"]) + assert a.personality == "flexible" + + def test_bdi_intention(self): + a = gm.BDIAgent("x", "stubborn", ["A", "B"], rng=random.Random(0)) + a.intentions.add("B") + assert a.decide(["A", "B"]) == "B" From 5df3850f58f50c4b9688885c2072eb9426cb3397 Mon Sep 17 00:00:00 2001 From: jsboige Date: Fri, 25 Sep 2026 15:56:55 +0200 Subject: [PATCH 8/8] docs(17353): retirer les compteurs 34 notebooks de la prose (#9377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Predicats gardes (tout notebook du repertoire, part non cataloguee, composition main + Gouvernance), mesures retirees — les donnees quantitatives sont tenues par le CI. Co-Authored-By: Claude Sonnet 5 --- MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md index de375a081f..a7c75c4ceb 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md +++ b/MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/README.md @@ -385,7 +385,7 @@ Lecture `CATALOG-STATUS` byte-identique (l. 3-8) : la valeur canonique `pedagogi Là où Planners (cycle 29) est le carrefour **simulation/proof intra-série** (Python ⇄ Lean 4 sur l'admissibilité d'heuristique, cycle 29) et SmartContracts (cycle 30) est le carrefour **trust/privacy inter-séries** (confiance + confidentialité + décision collective), Argument_Analysis est le carrefour **informel/formel anti-théâtre inter-couches** : la **lecture de texte** (couche LLM, floue/contextuelle), la **formalisation logique** (couche PL/FOL/Modal, médium), et la **vérification formelle** (couche Tweety/Lean, tranchante/certaine) doivent collaborer SANS que l'une simule ce que l'autre fait réellement. Cette doctrine — incarnée par `Restitution_3_Actes` (scaffold déterministe + LLM *gated*), `Multi_Backend_Routing` (sentinelle « décider ou échouer bruyamment »), `Formal_Richness_Matrix` (4 classes de verdict anti-théâtre), et le mode fail-loud de `05` — est **la doctrine anti-théâtre du dépôt** : aucun notebook ne fait passer une simulation pour un résultat, aucune sortie n'est maquée pour embellir un échec. -La série — 34 notebooks sur disque, 28 au catalogue canonique — aligne l'évolution paradigmatique de l'argumentation computationnelle (1995 Dung AF → 2019 framework hybrides LLM + solveur) sur la **frontière de vérifiabilité** (extraction brute → taxonomie → formalisation → validation SAT → restitution grounded). Chaque notebook est un maillon de la chaîne *lire → formaliser → vérifier → restituer honnêtement*. +La série — tout notebook du répertoire, dont une part encore non cataloguée (rattrapage par le cron `catalog-cron.yml`) — aligne l'évolution paradigmatique de l'argumentation computationnelle (1995 Dung AF → 2019 framework hybrides LLM + solveur) sur la **frontière de vérifiabilité** (extraction brute → taxonomie → formalisation → validation SAT → restitution grounded). Chaque notebook est un maillon de la chaîne *lire → formaliser → vérifier → restituer honnêtement*. ## Conclusion / Prochaines étapes @@ -575,7 +575,7 @@ sont des **ordres partiels** : La discussion c.1030 opposait « 14 notebooks annoncés » (adjoint) à « 15 balisés » (auteur de la PR). La mesure de référence est le répertoire : -**34 notebooks sur cette branche** au 2026-09-25 — les 33 de `main` (mesure +celle de cette branche au 2026-09-25 — les 33 de `main` (mesure du 2026-09-23, cinq arrivées depuis le 2026-09-09 ayant porté l'arc 3 de 5 à 9) plus `Gouvernance_Multi_Agents` apportée par la présente PR — dont **20 balisés** par les arcs (6 + 4 + 10, l'arc 3 accueillant la gouvernance