{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "993c4ff4",
   "metadata": {},
   "source": [
    "# 15 — Agent-based consensus dynamics\n",
    "\n",
    "Doc page: [agent_based.rst](../../docs/source/algorithms/agent_based.rst).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "139a21f8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:56.273834Z",
     "iopub.status.busy": "2026-05-12T14:05:56.273478Z",
     "iopub.status.idle": "2026-05-12T14:05:56.865083Z",
     "shell.execute_reply": "2026-05-12T14:05:56.863813Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from optimizr import _core as opt\n",
    "plt.rcParams['figure.figsize'] = (8.5, 4.5)\n",
    "plt.rcParams['figure.dpi'] = 110\n",
    "plt.rcParams['axes.grid'] = True\n",
    "plt.rcParams['grid.alpha'] = 0.3\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a77569c5",
   "metadata": {},
   "source": [
    "## Cellule 1 — Convergence vers le consensus\n",
    "\n",
    "**Théorème (consensus DeGroot, version $\\alpha$-pondérée).** Pour la\n",
    "dynamique discrète\n",
    "$$X^i_{k+1} = (1 - \\alpha)\\,X^i_k + \\alpha\\,\\bar X_k + \\sigma\\,\\varepsilon_k^i,$$\n",
    "la moyenne $\\bar X_k$ est conservée en espérance et la variance\n",
    "décroît à taux $(1 - \\alpha)^2$ par pas (pour $\\sigma = 0$).\n",
    "\n",
    "**Équation pivot.**\n",
    "$$\\mathbb{E}[\\text{Var}(X_k)] = (1 - \\alpha)^{2k}\\,\\text{Var}(X_0).$$\n",
    "\n",
    "**Démonstration.** Récurrence linéaire : la composante hors moyenne\n",
    "est multipliée par $(1-\\alpha)$ à chaque pas.  $\\square$\n",
    "\n",
    "**Ce que la cellule vérifie.** `consensus_dynamics(initial, alpha,\n",
    "noise_sigma, n_steps, seed)` reproduit la décroissance géométrique.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "aff03dd6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:56.868502Z",
     "iopub.status.busy": "2026-05-12T14:05:56.868174Z",
     "iopub.status.idle": "2026-05-12T14:05:57.662174Z",
     "shell.execute_reply": "2026-05-12T14:05:57.660763Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Moyenne initiale : 0.079\n",
      "Moyenne finale   : 0.079\n",
      "Var init / final : 1.230 / 3.174e-19\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 97068 characters]",
      "text/plain": [
       "<Figure size 1320x440 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "N, alpha, sigma, n_steps = 50, 0.3, 0.0, 60\n",
    "rng = np.random.default_rng(1)\n",
    "x0 = rng.uniform(-2, 2, N).tolist()\n",
    "\n",
    "res = opt.consensus_dynamics(x0, alpha=alpha, noise_sigma=sigma,\n",
    "                              n_steps=n_steps, seed=0)\n",
    "states = np.array(res['states_flat']).reshape(n_steps + 1, N)\n",
    "mean_traj = np.array(res['mean_trajectory'])\n",
    "\n",
    "variances = states.var(axis=1)\n",
    "print(f\"Moyenne initiale : {np.mean(x0):.3f}\")\n",
    "print(f\"Moyenne finale   : {mean_traj[-1]:.3f}\")\n",
    "print(f\"Var init / final : {variances[0]:.3f} / {variances[-1]:.3e}\")\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n",
    "for i in range(N):\n",
    "    axes[0].plot(states[:, i], lw=0.8, alpha=0.5)\n",
    "axes[0].plot(mean_traj, 'k-', lw=2, label='moyenne')\n",
    "axes[0].set_xlabel('itération k'); axes[0].set_ylabel(r'$X_k^i$')\n",
    "axes[0].set_title(f\"Consensus (α = {alpha}, σ = {sigma})\")\n",
    "axes[0].legend()\n",
    "axes[1].semilogy(variances, lw=2, label='variance empirique')\n",
    "axes[1].semilogy(variances[0] * (1 - alpha) ** (2 * np.arange(n_steps + 1)),\n",
    "                 ':', label=r'$(1-\\alpha)^{2k}$ Var$(X_0)$')\n",
    "axes[1].set_xlabel('itération k'); axes[1].set_ylabel(r'Var($X_k$)')\n",
    "axes[1].set_title(\"Décroissance géométrique\")\n",
    "axes[1].legend()\n",
    "fig.tight_layout(); plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fb8eccea",
   "metadata": {},
   "source": [
    "**Résultat attendu.** Variance qui décroît exponentiellement.\n",
    "\n",
    "**Lecture du graphique.** Faisceau qui se concentre vers la moyenne\n",
    "(ligne noire) ; pente log-linéaire conforme à la prédiction.\n",
    "\n",
    "**Conclusion.** Le primitive est validé sur la convergence\n",
    "géométrique.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5a91c57b",
   "metadata": {},
   "source": [
    "## Cellule 2 — Effet du bruit\n",
    "\n",
    "**Théorème.** Lorsque $\\sigma > 0$, la variance ne tombe pas à zéro\n",
    "mais se stabilise à un niveau d'équilibre proportionnel à $\\sigma^2$.\n",
    "\n",
    "**Équation pivot.** Variance asymptotique croissante en $\\sigma^2$.\n",
    "\n",
    "**Ce que la cellule vérifie.** Sweep sur $\\sigma \\in \\{0.05, 0.1,\n",
    "0.2\\}$ pour $\\alpha = 0.3$.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "dc879366",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:57.665702Z",
     "iopub.status.busy": "2026-05-12T14:05:57.665355Z",
     "iopub.status.idle": "2026-05-12T14:05:58.012408Z",
     "shell.execute_reply": "2026-05-12T14:05:58.011304Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "σ = 0.05 : Var(K) numérique = 0.0048\n",
      "σ = 0.10 : Var(K) numérique = 0.0191\n",
      "σ = 0.20 : Var(K) numérique = 0.0762\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 65588 characters]",
      "text/plain": [
       "<Figure size 935x495 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "N, alpha, n_steps = 80, 0.3, 200\n",
    "rng = np.random.default_rng(11)\n",
    "x0 = rng.uniform(-2, 2, N).tolist()\n",
    "\n",
    "fig, ax = plt.subplots()\n",
    "for sigma in [0.05, 0.1, 0.2]:\n",
    "    r = opt.consensus_dynamics(x0, alpha, sigma, n_steps, seed=42)\n",
    "    states = np.array(r['states_flat']).reshape(n_steps + 1, N)\n",
    "    var = states.var(axis=1)\n",
    "    ax.plot(var, label=fr'$\\sigma = {sigma}$')\n",
    "    print(f\"σ = {sigma:.2f} : Var(K) numérique = {var[-1]:.4f}\")\n",
    "ax.set_xlabel('itération k'); ax.set_ylabel(r'Var($X_k$)')\n",
    "ax.set_title(\"Variance asymptotique vs bruit\")\n",
    "ax.legend()\n",
    "fig.tight_layout(); plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "54036a62",
   "metadata": {},
   "source": [
    "**Résultat attendu.** Variance qui plateauise à un niveau\n",
    "croissant avec $\\sigma^2$.\n",
    "\n",
    "**Lecture du graphique.** Trois courbes décroissantes qui se\n",
    "stabilisent à des paliers proportionnels à $\\sigma^2$.\n",
    "\n",
    "**Conclusion.** Le compromis bruit / convergence est gouverné par\n",
    "$\\alpha$ et $\\sigma$.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "15bd197c",
   "metadata": {},
   "source": [
    "## Cellule 3 — Exemple concret : émergence d'une décision collective\n",
    "\n",
    "**Modèle.** $N = 100$ agents au sein d'un comité décident d'un\n",
    "nombre (par exemple un score entre $-1$ et $+1$).  Chaque agent\n",
    "ajuste sa position vers la moyenne avec un peu de bruit.\n",
    "\n",
    "**Équation pivot.**\n",
    "$$X^i_{k+1} = (1 - \\alpha) X^i_k + \\alpha \\bar X_k + \\sigma \\varepsilon^i_k.$$\n",
    "\n",
    "**Ce que la cellule vérifie.** Visualisation : densité des\n",
    "opinions au cours du temps converge vers une distribution unimodale.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "11ad58dc",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:05:58.015710Z",
     "iopub.status.busy": "2026-05-12T14:05:58.015433Z",
     "iopub.status.idle": "2026-05-12T14:05:58.679005Z",
     "shell.execute_reply": "2026-05-12T14:05:58.677754Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Moyenne initiale : -0.061\n",
      "Moyenne finale   : -0.075\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 117164 characters]",
      "text/plain": [
       "<Figure size 1320x462 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "N = 100\n",
    "n_steps = 80\n",
    "alpha, sigma = 0.2, 0.04\n",
    "rng = np.random.default_rng(7)\n",
    "# distribution initiale : trois clusters\n",
    "x0 = np.concatenate([\n",
    "    rng.normal(-0.7, 0.15, 35),\n",
    "    rng.normal(0.0, 0.10, 35),\n",
    "    rng.normal(+0.7, 0.15, 30),\n",
    "]).tolist()\n",
    "\n",
    "r = opt.consensus_dynamics(x0, alpha, sigma, n_steps, seed=0)\n",
    "states = np.array(r['states_flat']).reshape(n_steps + 1, N)\n",
    "mean_traj = np.array(r['mean_trajectory'])\n",
    "\n",
    "print(f\"Moyenne initiale : {np.mean(x0):.3f}\")\n",
    "print(f\"Moyenne finale   : {mean_traj[-1]:.3f}\")\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(12, 4.2))\n",
    "im = axes[0].imshow(states.T, aspect='auto', cmap='coolwarm',\n",
    "                    interpolation='nearest', origin='lower',\n",
    "                    extent=[0, n_steps, 0, N])\n",
    "axes[0].set_xlabel('itération k')\n",
    "axes[0].set_ylabel('agent i')\n",
    "axes[0].set_title(\"Évolution des opinions individuelles\")\n",
    "plt.colorbar(im, ax=axes[0], label='X_k^i')\n",
    "for k, color in zip([0, 20, n_steps], ['C0', 'C2', 'C3']):\n",
    "    axes[1].hist(states[k], bins=20, alpha=0.55, color=color,\n",
    "                 density=True, label=f'k={k}', edgecolor='white')\n",
    "axes[1].set_xlabel('opinion'); axes[1].set_ylabel('densité')\n",
    "axes[1].set_title(\"Distribution à différents instants\")\n",
    "axes[1].legend()\n",
    "fig.tight_layout(); plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0540fb0c",
   "metadata": {},
   "source": [
    "**Résultat attendu.** Les trois clusters initiaux fusionnent en\n",
    "distribution unimodale concentrée près de $\\bar X_0$.\n",
    "\n",
    "**Lecture du graphique.** Gauche : la heatmap montre l'horizon de\n",
    "fusion.  Droite : trois distributions superposées (initiale tri-modale,\n",
    "intermédiaire, finale unimodale).\n",
    "\n",
    "**Conclusion.** Le primitive `consensus_dynamics` modélise toute\n",
    "dynamique de moyennage par interaction (vote pondéré, opinion\n",
    "publique, scoring collectif).\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "rhftlab",
   "language": "python",
   "name": "rhftlab"
  },
  "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.11.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
