GameTheory-14 : Jeux Differentiels et Equilibres de Stackelberg (C#)

Navigation : << 13-ImperfectInfo-CFR | Index | 15-CooperativeGames >>

Decisions Dynamiques et Leadership Stratégique (jumeau .NET)

Ce notebook est le jumeau C# (.NET Interactive) de GameTheory-14-DifferentialGames-Python.ipynb. Il introduit les jeux differentiels (decisions continues dans le temps) et les equilibres de Stackelberg (modèles leader-follower).

Value-add du jumeau C# (Prong B, EPIC #3801)

Le twin Python s’appuie sur numpy (vectorisation) et scipy (odeint, solve_ivp, minimize_scalar). Ce jumeau reimplementation tout from-scratch en C# / BCL .NET 9 :

Concept Python (lib externe) C# (from-scratch)
Cournot / Stackelberg formules analytiques mêmes formules closed-form (0 optimisation numérique)
Integration ODE scipy.solve_ivp / odeint (Rk45/Radau) integrateur RK4 maison (Runge-Kutta ordre 4)
Equilibre LQ feedback boucle numpy + Riccati Riccati couplee backward en double[] pur
Pursuit-evasion exercice laisse vide modelise en RK4 (pure pursuit radial + perpendiculaire)

Objectifs d’apprentissage

  • Comprendre la différence entre boucle ouverte (open-loop) et boucle fermee (feedback)
  • Maitriser les equilibres de Stackelberg (leader-follower)
  • Analyser les jeux lineaires-quadratiques (LQ) via equations de Riccati
  • Appliquer a l’economie industrielle (oligopoles dynamiques) et a la poursuite-evasion

Duree estimee : 60 minutes

1. Introduction aux jeux dynamiques

1.1 Jeux statiques vs dynamiques

Aspect Jeux statiques Jeux dynamiques
Temps Une seule periode Plusieurs periodes ou continu
Actions Simultanees Séquentielles ou continues
Etat Fixe Evolue selon les actions
Information Connue a l’avance Revelee progressivement

1.2 Types de stratégies

  • Boucle ouverte (open-loop) : le joueur s’engage sur une trajectoire complete \(u(t)\) des le debut
  • Boucle fermee (closed-loop/feedback) : le joueur ajuste \(u(x(t), t)\) selon l’etat courant

1.3 Historique

Les jeux differentiels ont ete formalises par Rufus Isaacs (1965) dans le contexte de problemes de poursuite-evasion militaires. La théorie economique (Stackelberg 1934, oligopoles) a fourni le cadre statique que le differentiel etend au temps continu.

2. Brique de base : integrateur RK4 from-scratch

Le twin Python delegue l’integration des EDO a scipy.integrate.solve_ivp. Ici nous implementons un Runge-Kutta d’ordre 4 (RK4) en C# pur, brique reutilisee pour la simulation des jeux differentiels.

L’erreur locale de RK4 est en \(O(\Delta t^5)\), soit ~\(10^5\) fois plus precise que la méthode d’Euler pour le même pas. Nous le validons sur \(\dot{x} = -x,\ x(0)=1\) dont la solution exacte est \(x(t) = e^{-t}\) : a \(t=1\), \(x^* = e^{-1} \approx 0{,}367879\).

// Prong B (#3801): 0 NuGet, 0 numpy, 0 scipy. BCL .NET 9 uniquement.
using System;
using System.Text;
using System.Collections.Generic;

// --- Integrateur RK4 from-scratch (remplace scipy.integrate.solve_ivp) ---
// dx/dt = f(t, x), x vecteur. Un pas : x_{n+1} = x_n + (k1+2k2+2k3+k4)/6 * dt
public static double[] Rk4Step(Func<double, double[], double[]> f, double t, double[] x, double dt)
{
    int n = x.Length;
    double[] Add(double[] a, double[] b, double s)
    {
        double[] r = new double[n];
        for (int i = 0; i < n; i++) r[i] = a[i] + s * b[i];
        return r;
    }
    double[] k1 = f(t, x);
    double[] k2 = f(t + dt / 2, Add(x, k1, dt / 2));
    double[] k3 = f(t + dt / 2, Add(x, k2, dt / 2));
    double[] k4 = f(t + dt, Add(x, k3, dt));
    double[] xn = new double[n];
    for (int i = 0; i < n; i++) xn[i] = x[i] + (dt / 6.0) * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]);
    return xn;
}

// Integration complete sur [0, T], renvoie trajectoire (lignes = instants)
public static double[][] IntegrateRk4(Func<double, double[], double[]> f, double[] x0, double T, double dt)
{
    var traj = new List<double[]>();
    int nSteps = (int)Math.Round(T / dt);
    double[] x = (double[])x0.Clone();
    traj.Add((double[])x.Clone());
    double t = 0.0;
    for (int i = 0; i < nSteps; i++)
    {
        x = Rk4Step(f, t, x, dt);
        t += dt;
        traj.Add((double[])x.Clone());
    }
    return traj.ToArray();
}

// Helper d'affichage (headless-robust : .Display() -> display_data text/plain)
public static void Show(string s) => s.Display();

// --- Validation RK4 : dx/dt = -x, x(0)=1 -> x(1) = e^{-1} ---
double[] IdentityDecay(double t, double[] x) => new double[] { -x[0] };
var traj = IntegrateRk4(IdentityDecay, new double[] { 1.0 }, T: 1.0, dt: 0.001);
double xNum = traj[^1][0];
double xExact = Math.Exp(-1.0);
var sb0 = new StringBuilder();
sb0.AppendLine("Validation RK4 : dx/dt = -x, x(0) = 1, t = 1");
sb0.AppendLine($"  numerique (RK4, dt=1e-3) : {xNum:F9}");
sb0.AppendLine($"  exact (e^{{-1}})            : {xExact:F9}");
sb0.AppendLine($"  erreur absolue            : {Math.Abs(xNum - xExact):E2}");
sb0.AppendLine("  -> RK4 precise a ~1e-9 pres : integrateur from-scratch valide.");
Show(sb0.ToString());
Validation RK4 : dx/dt = -x, x(0) = 1, t = 1
  numerique (RK4, dt=1e-3) : 0,367879441
  exact (e^{-1})            : 0,367879441
  erreur absolue            : 4,00E-015
  -> RK4 precise a ~1e-9 pres : integrateur from-scratch valide.

2.1 Pourquoi RK4 et pas Euler ?

Le twin Python original utilise une integration d’Euler explicite dans sa classe DifferentialGame.simulate (x[i+1] = x[i] + dx*dt). Euler est d’ordre 1 (erreur \(O(\Delta t^2)\) locale) : pour une même precision il faut un pas 100x plus petit qu’en RK4, ou bien une erreur non negligeable s’accumule sur les horizons longs.

Notre implementation C# choisit RK4 (ordre 4) pour que la simulation des jeux LQ et de la poursuite-evasion soit fiable a pas modere (\(\Delta t = 0{,}01\)), sans dépendance a une librairie externe.

3. Equilibres de Stackelberg et Cournot (formes analytiques)

3.1 Duopole a demande lineaire

Soit \(P(Q) = a - bQ\) la demande inverse, deux firmes de cout marginal constant \(c\).

Cournot (simultane) : chaque firme maximise son profit en prenant la quantite de l’autre comme donnee. Les fonctions de reaction se croisent en

\[q_1^* = \frac{a - 2c_1 + c_2}{3b}, \quad q_2^* = \frac{a - 2c_2 + c_1}{3b}.\]

Stackelberg (séquentiel) : le leader \(L\) anticipe la reaction du follower \(q_F(q_L) = \frac{a - c_F - b q_L}{2b}\) et maximise. On obtient la forme fermee

\[q_L^* = \frac{a - 2c_L + c_F}{2b}, \quad q_F^* = \frac{a - c_F - b q_L^*}{2b}.\]

Avantage du twin C# : ces solutions etant analytiques, aucune optimisation numérique (scipy.optimize.minimize_scalar cote Python) n’est necessaire – on calcule directement la closed-form.

// --- Duopole lineaire : Cournot et Stackelberg en closed-form ---
public sealed class LinearDemand
{
    public readonly double A, B;  // P(Q) = A - B*Q
    public LinearDemand(double a, double b) { A = a; B = b; }
    public double Price(double Q) => Math.Max(0.0, A - B * Q);
}

public sealed class Firm
{
    public readonly string Name;
    public readonly double MarginalCost;
    public Firm(string name, double marginalCost) { Name = name; MarginalCost = marginalCost; }
    public double Profit(double q, double price) => (price - MarginalCost) * q;
}

public static (double q1, double q2) CournotEquilibrium(LinearDemand d, Firm f1, Firm f2)
{
    double q1 = (d.A - 2 * f1.MarginalCost + f2.MarginalCost) / (3 * d.B);
    double q2 = (d.A - 2 * f2.MarginalCost + f1.MarginalCost) / (3 * d.B);
    return (Math.Max(0, q1), Math.Max(0, q2));
}

public static (double qL, double qF) StackelbergEquilibrium(LinearDemand d, Firm leader, Firm follower)
{
    double qL = (d.A - 2 * leader.MarginalCost + follower.MarginalCost) / (2 * d.B);
    double qF = (d.A - follower.MarginalCost - d.B * qL) / (2 * d.B);
    return (Math.Max(0, qL), Math.Max(0, qF));
}

// --- Exemple numerique (a=100, b=1, c_A=c_B=10) ---
var demand = new LinearDemand(a: 100, b: 1);
var firmA = new Firm("A", marginalCost: 10);
var firmB = new Firm("B", marginalCost: 10);

var (q1c, q2c) = CournotEquilibrium(demand, firmA, firmB);
double Qc = q1c + q2c, Pc = demand.Price(Qc);
double piAc = firmA.Profit(q1c, Pc), piBc = firmB.Profit(q2c, Pc);

var (qL, qF) = StackelbergEquilibrium(demand, firmA, firmB);
double Qs = qL + qF, Ps = demand.Price(Qs);
double piL = firmA.Profit(qL, Ps), piF = firmB.Profit(qF, Ps);

var sb = new StringBuilder();
sb.AppendLine("Duopole P(Q) = 100 - Q, couts marginaux c = 10");
sb.AppendLine(new string('=', 60));
sb.AppendLine();
sb.AppendLine("-- Equilibre de Cournot (simultane) --");
sb.AppendLine($"  q_A = {q1c:F2}, q_B = {q2c:F2}, Q = {Qc:F2}, P = {Pc:F2}");
sb.AppendLine($"  pi_A = {piAc:F2}, pi_B = {piBc:F2}");
sb.AppendLine();
sb.AppendLine("-- Equilibre de Stackelberg (A leader) --");
sb.AppendLine($"  q_A (leader) = {qL:F2}, q_B (follower) = {qF:F2}, Q = {Qs:F2}, P = {Ps:F2}");
sb.AppendLine($"  pi_A = {piL:F2}, pi_B = {piF:F2}");
sb.AppendLine();
sb.AppendLine($"Avantage du leader    : {piL - piAc:F2} (vs Cournot)");
sb.AppendLine($"Desavantage du follower: {piF - piBc:F2} (vs Cournot)");
Show(sb.ToString());
Duopole P(Q) = 100 - Q, couts marginaux c = 10
============================================================

-- Equilibre de Cournot (simultane) --
  q_A = 30,00, q_B = 30,00, Q = 60,00, P = 40,00
  pi_A = 900,00, pi_B = 900,00

-- Equilibre de Stackelberg (A leader) --
  q_A (leader) = 45,00, q_B (follower) = 22,50, Q = 67,50, P = 32,50
  pi_A = 1012,50, pi_B = 506,25

Avantage du leader    : 112,50 (vs Cournot)
Desavantage du follower: -393,75 (vs Cournot)

3.2 Interpretation : avantage du premier joueur (first-mover)

Equilibre q_A q_B Q total Prix Profit A Profit B
Cournot 30 30 60 40 900 900
Stackelberg 45 22.5 67.5 32.5 1012.50 506.25
  1. Le leader produit 50% de plus (45 vs 30) : il s’engage sur une quantite elevee, forçant le follower a reduire la sienne.
  2. Le follower subit une perte de 44% (506.25 vs 900) malgre une stratégie optimale de sa part.
  3. Les consommateurs beneficient : Q monte (67.5 vs 60), le prix baisse (32.5 vs 40).
  4. L’industrie perd globalement : profit total 1518.75 < 1800 (prix plus bas = marges reduites).

Paradoxe de Stackelberg : le leadership confere un avantage individuel, mais la competition plus intense reduit le profit collectif.

4. Jeux lineaires-quadratiques (LQ) et equations de Riccati

4.1 Structure

  • Dynamique lineaire : \(\dot{x} = a x + b_1 u_1 + b_2 u_2\)
  • Cout quadratique : \(J_i = \int_0^T (q_i x^2 + r_i u_i^2)\,dt + s_i x(T)^2\)

4.2 Equilibre de Nash en feedback

Pour un jeu LQ, la stratégie optimale est lineaire dans l’etat : \(u_i^*(x,t) = -K_i(t)\,x\).

Les gains \(K_i(t)\) derivent d’une equation de Riccati couplee sur un coefficient scalaire \(P_i(t)\), integree backward depuis la condition terminale \(P_i(T) = s_i\) :

\[K_i = \frac{b_i P_i}{r_i}, \qquad \dot{P_i} = -2 a P_i - q_i + \frac{b_i^2}{r_i} P_i^2 + 2 \frac{b_j^2}{r_j} P_j P_i.\]

Forme couplee (Engwerda 2005) : la derivee de \(P_i\) ne depend que du gain du joueur adverse \(K_j = b_j P_j/r_j\), pas du sien \(K_i\) – sinon le terme quadratique propre \((b_i^2/r_i) P_i^2\) est compte plusieurs fois. Mirror C# de la correction du twin Python [#7864].

4.3 Verification analytique du regime stationnaire (Math Verification)

Avec \(a=-0{,}1\), \(b_1=1\), \(b_2=-1\), \(q_i=r_i=1\), \(s_i=0\), par symetrie \(P_1=P_2=P\). Le point fixe \(\dot P=0\) donne :

\[0{,}2 P - 1 + P^2 + 2 P^2 = 0 \;\Longrightarrow\; 3P^2 + 0{,}2 P - 1 = 0,\]

soit \(P = \frac{-0{,}2 + \sqrt{12{,}04}}{6} \approx 0{,}5450 = K_1(0)\). L’integration numérique backward doit converger vers cette valeur.

// --- Jeu LQ differentiel + Riccati couplee backward (from-scratch, 0 numpy) ---
public sealed class LQGame
{
    public readonly double T, Dt, A, B1, B2, Q1, R1, S1, Q2, R2, S2;
    public readonly double[] Times;
    public LQGame(double T, double dt, double a, double b1, double b2,
                  double q1, double r1, double s1, double q2, double r2, double s2)
    {
        this.T=T; Dt=dt; A=a; B1=b1; B2=b2; Q1=q1; R1=r1; S1=s1; Q2=q2; R2=r2; S2=s2;
        var ts = new List<double>();
        for (double t = 0; t <= T + 1e-9; t += dt) ts.Add(t);
        Times = ts.ToArray();
    }

    // Resout les gains feedback K1(t), K2(t) par Riccati backward.
    public (double[] K1, double[] K2) SolveFeedbackNash()
    {
        int n = Times.Length;
        double[] P1 = new double[n], P2 = new double[n];
        double[] K1 = new double[n], K2 = new double[n];
        P1[n - 1] = S1; P2[n - 1] = S2;
        for (int i = n - 2; i >= 0; i--)
        {
            K1[i + 1] = B1 * P1[i + 1] / R1;
            K2[i + 1] = B2 * P2[i + 1] / R2;
            // Equations de Riccati couplees (correction mirror #7864, cf derive HJB).
            // HJB joueur i : P_i_dot = -2 a P_i - q_i + (b_i^2/r_i) P_i^2
            //                                + 2 (b_j^2/r_j) P_j P_i,  avec K_j = b_j P_j / r_j.
            // La derivee de P_i ne depend que du gain adverse K_j, PAS du sien K_i :
            // sinon le terme quadratique propre (b_i^2/r_i) P_i^2 est compte plusieurs fois
            // (l'ancienne forme via A_cl confondait dynamique fermee et cout).
            double dP1 = -2 * A * P1[i + 1] - Q1
                       + (B1 * B1 / R1) * P1[i + 1] * P1[i + 1]
                       + 2 * (B2 * B2 / R2) * P2[i + 1] * P1[i + 1];
            double dP2 = -2 * A * P2[i + 1] - Q2
                       + (B2 * B2 / R2) * P2[i + 1] * P2[i + 1]
                       + 2 * (B1 * B1 / R1) * P1[i + 1] * P2[i + 1];
            P1[i] = P1[i + 1] - dP1 * Dt;
            P2[i] = P2[i + 1] - dP2 * Dt;
        }
        for (int i = 0; i < n; i++) { K1[i] = B1 * P1[i] / R1; K2[i] = B2 * P2[i] / R2; }
        return (K1, K2);
    }
}

// dx/dt = a*x + b1*u1 + b2*u2 ; feedback ui = -Ki[idx]*x (idx = round(t/dt))
var lq = new LQGame(T: 10.0, dt: 0.05, a: -0.1, b1: 1.0, b2: -1.0,
                    q1: 1.0, r1: 1.0, s1: 0.0, q2: 1.0, r2: 1.0, s2: 0.0);
var (K1, K2) = lq.SolveFeedbackNash();

// Verif analytique du regime stationnaire : 3P^2 + 0.2P - 1 = 0 -> P = (-0.2+sqrt(12.04))/6
double disc = 0.2 * 0.2 + 4 * 3 * 1;          // 12.04
double Pstat = (-0.2 + Math.Sqrt(disc)) / (2 * 3);

var sb4 = new StringBuilder();
sb4.AppendLine("Jeu LQ : Course a l'investissement publicitaire");
sb4.AppendLine(new string('=', 60));
sb4.AppendLine("Modele : dx/dt = -0.1 x + u1 - u2, cout integral (x^2 + u_i^2)");
sb4.AppendLine();
sb4.AppendLine("-- Gains feedback initiaux (Riccati backward, from-scratch) --");
sb4.AppendLine($"  K1(0) = {K1[0]:F4}   (theorie stationnaire : {Pstat:F4})");
sb4.AppendLine($"  K2(0) = {K2[0]:F4}   (symetrie attendue : {-Pstat:F4})");
sb4.AppendLine($"  ecart numerique vs stationnaire : {Math.Abs(K1[0] - Pstat):E2}");
sb4.AppendLine();
sb4.AppendLine("  -> K1(0)=0.5450, K2(0)=-0.5450 confirment la solution de Riccati.");
Show(sb4.ToString());
Jeu LQ : Course a l'investissement publicitaire
============================================================
Modele : dx/dt = -0.1 x + u1 - u2, cout integral (x^2 + u_i^2)

-- Gains feedback initiaux (Riccati backward, from-scratch) --
  K1(0) = 0,5450   (theorie stationnaire : 0,5450)
  K2(0) = -0,5450   (symetrie attendue : -0,5450)
  ecart numerique vs stationnaire : 1,11E-016

  -> K1(0)=0.5450, K2(0)=-0.5450 confirment la solution de Riccati.

4.4 Simulation feedback Nash : convergence vers l’equilibre

Avec \(u_i^*(x,t) = -K_i(t)\,x\), on simule la trajectoire d’etat par RK4 pour différentes conditions initiales \(x_0\). Si \(x>0\) la firme 1 est en avance ; le feedback la pousse a reduire son effort (\(u_1 = -K_1 x < 0\)) tandis que la firme 2 intensifie le sien (\(u_2 = -K_2 x > 0\)). Toutes les trajectoires convergent vers \(x=0\).

// --- Simulation du jeu LQ en feedback Nash via RK4 ---
// Dynamique fermee : dx/dt = (a - b1*K1 - b2*K2) x, avec K interpole par pas.
double[] FeedbackDynamics(double t, double[] x)
{
    int idx = Math.Min((int)Math.Round(t / lq.Dt), K1.Length - 1);
    double Acl = lq.A - lq.B1 * K1[idx] - lq.B2 * K2[idx];
    return new double[] { Acl * x[0] };
}

double[] x0Values = { -2.0, -1.0, 0.0, 1.0, 2.0 };
var sb5 = new StringBuilder();
sb5.AppendLine("Trajectoires d'etat x(T=10) selon x0 (feedback Nash) :");
sb5.AppendLine(new string('-', 48));
sb5.AppendLine($"{"x0",8} {"x(0)",10} {"x(T)",10} {"converge?",10}");
sb5.AppendLine(new string('-', 48));
foreach (double x0 in x0Values)
{
    var tr = IntegrateRk4(FeedbackDynamics, new double[] { x0 }, T: lq.T, dt: 0.01);
    double xEnd = tr[^1][0];
    string conv = (Math.Abs(xEnd) < 0.05) ? "oui" : "non";
    sb5.AppendLine($"{x0,8:F1} {x0,10:F2} {xEnd,10:F4} {conv,10}");
}
sb5.AppendLine();
sb5.AppendLine("Toutes les trajectoires convergent vers x = 0 (etat d'equilibre).");
sb5.AppendLine("Symetrie K1 = -K2 => comportement antisymetrique des efforts u1, u2.");
Show(sb5.ToString());
Trajectoires d'etat x(T=10) selon x0 (feedback Nash) :
------------------------------------------------
      x0       x(0)       x(T)  converge?
------------------------------------------------
    -2,0      -2,00    -0,0000        oui
    -1,0      -1,00    -0,0000        oui
     0,0       0,00     0,0000        oui
     1,0       1,00     0,0000        oui
     2,0       2,00     0,0000        oui

Toutes les trajectoires convergent vers x = 0 (etat d'equilibre).
Symetrie K1 = -K2 => comportement antisymetrique des efforts u1, u2.

4.5 Interpretation LQ

  • Symetrie des gains : \(K_1 = -K_2\) reflete l’antisymetrie du jeu (effets opposes \(b_1=1\), \(b_2=-1\), couts identiques).
  • Interpretation economique : l’effort est proportionnel au desavantage. Plus une firme est en retard, plus elle investit ; a l’equilibre (\(x=0\)) aucun gaspillage.
  • Stabilite : la dynamique fermee \(A_{cl} = -0{,}1 - 2K_1 < 0\) garantit la convergence asymptotique vers \(x=0\).

5. Stackelberg dynamique : valeur de l’engagement

Le leader peut s’engager partiellement : un niveau d’engagement \(\alpha \in [0,1]\) interpole sa quantite entre Cournot (\(\alpha=0\)) et Stackelberg complet (\(\alpha=1\)). Le follower joue toujours sa meilleure reponse \(q_F(q_L)\).

Ceci remplace l’appel a scipy.optimize.minimize_scalar du twin Python par un balayage analytique : pour chaque \(\alpha\), \(q_L(\alpha) = \alpha q_L^{Stack} + (1-\alpha) q^{Cournot}\) puis \(q_F\) par reaction.

// --- Stackelberg dynamique : balayage du niveau d'engagement (0 scipy) ---
public sealed class StackelbergDuopoly
{
    public readonly LinearDemand D;
    public readonly Firm Leader, Follower;
    public StackelbergDuopoly(LinearDemand d, Firm leader, Firm follower) { D=d; Leader=leader; Follower=follower; }
    public double FollowerReaction(double qL) => Math.Max(0, (D.A - Follower.MarginalCost - D.B * qL) / (2 * D.B));
    public double LeaderProfit(double qL, double qF) => (D.Price(qL + qF) - Leader.MarginalCost) * qL;
    public double FollowerProfit(double qL, double qF) => (D.Price(qL + qF) - Follower.MarginalCost) * qF;
}

var duo = new StackelbergDuopoly(demand, firmA, firmB);
double qCournot = (demand.A - firmA.MarginalCost) / (3 * demand.B);   // symetrique = 30
double qLstack = (demand.A - 2 * firmA.MarginalCost + firmB.MarginalCost) / (2 * demand.B); // 45

var sb6 = new StringBuilder();
sb6.AppendLine("Stackelberg dynamique : impact du niveau d'engagement");
sb6.AppendLine(new string('=', 60));
sb6.AppendLine();
sb6.AppendLine($"{"alpha",8} {"q_L",10} {"q_F",10} {"pi_L",12} {"pi_F",12}");
sb6.AppendLine(new string('-', 54));
double[] commitments = { 0.0, 0.25, 0.5, 0.75, 1.0 };
foreach (double alpha in commitments)
{
    double qL = alpha * qLstack + (1 - alpha) * qCournot;
    double qF = duo.FollowerReaction(qL);
    double piL = duo.LeaderProfit(qL, qF);
    double piF = duo.FollowerProfit(qL, qF);
    sb6.AppendLine($"{alpha,8:F2} {qL,10:F2} {qF,10:F2} {piL,12:F2} {piF,12:F2}");
}
sb6.AppendLine();
sb6.AppendLine("alpha=0 : Cournot (simultane)   |   alpha=1 : Stackelberg (sequentiel)");
Show(sb6.ToString());
Stackelberg dynamique : impact du niveau d'engagement
============================================================

   alpha        q_L        q_F         pi_L         pi_F
------------------------------------------------------
    0,00      30,00      30,00       900,00       900,00
    0,25      33,75      28,12       949,22       791,02
    0,50      37,50      26,25       984,38       689,06
    0,75      41,25      24,38      1005,47       594,14
    1,00      45,00      22,50      1012,50       506,25

alpha=0 : Cournot (simultane)   |   alpha=1 : Stackelberg (sequentiel)

5.1 Interpretation : paradoxe du bien-etre

  • Profit leader : 900 (Cournot) -> 1012.50 (Stackelberg), +12.5%. Rendements decroissants.
  • Profit follower : 900 -> 506.25, -44%.
  • Profit total industrie : 1800 -> 1518.75. L’industrie perd a l’engagement.
  • Quantite totale : 60 -> 67.5. Les consommateurs gagnent (prix plus bas).

Implication (Dixit 1980) : si le leader peut s’engager de maniere credible, il a toujours intérêt a le faire. Mais du point de vue collectif, Cournot Pareto-domine Stackelberg.

6. Poursuite-evasion : jeu differentiel a somme nulle (Isaacs)

Le twin Python laisse la poursuite-evasion en exercice non resolu. Ce jumeau la modelise et la simule en RK4 – c’est l’illustration canonique (Isaacs 1965) d’un jeu differentiel a somme nulle.

6.1 Cadre

  • Poursuivant \(P\) en \(p(t) \in \mathbb{R}^2\), vitesse \(v_p\) ; evasif \(E\) en \(e(t)\), vitesse \(v_e < v_p\).
  • Capture quand \(\|p - e\| < \varepsilon\).
  • Objectif de \(P\) : minimiser le temps de capture ; objectif de \(E\) : le maximiser.

6.2 Cas radial (verifiable analytiquement) — meilleur cas pour l’evasif

Si \(E\) fuit droit devant le long de l’axe \(P \to E\), toute sa vitesse \(v_e\) s’oppose au rapprochement : la distance se ferme a la vitesse \(v_p - v_e\). Temps de capture \(t^* = d_0 / (v_p - v_e)\). Pour \(d_0 = 10\), \(v_p = 2\), \(v_e = 1\) : \(t^* = 10\). C’est le meilleur cas pour \(E\) (pire pour \(P\)) en pure pursuit.

6.3 Evasion perpendiculaire — pire cas pour l’evasif

\(P\) vise a chaque instant la position de \(E\) (pure pursuit) ; \(E\) fuit perpendiculairement a la ligne de visee. La vitesse de \(E\) est alors orthogonale a la ligne \(P \to E\) : sa composante radiale est nulle, donc \(E\) ne contribue pas a etirer la distance. Celle-ci se ferme a la vitesse \(v_p\) seule : \(t^* = d_0 / v_p = 5 < 10\). \(E\) gaspille toute sa vitesse en pure perte laterale.

Lecon (Isaacs) : contre un poursuivant en pure pursuit, la meilleure evasion est de fuir droit devant (maximiser la composante radiale anti-\(P\)), jamais de fuir perpendiculairement. Cela semble contre-intuitif (on croit souvent qu’esquiver lateralement aide) mais la geometrie est sans appel : seule la composante de vitesse le long de la ligne de visee compte pour le temps de capture.

6.4 Evasion vers un refuge — cas non resoluble analytiquement

Les cas radial et perpendiculaire sont verifiables analytiquement (formules \(d_0/(v_p-v_e)\) et \(d_0/v_p\)) : le simulateur RK4 s’y contente de confirmer une formule connue. Pour exercer reellement l’integrateur sur un cas ou aucune formule close n’existe, considerons une evasion vers un refuge : \(E\) ne fuit pas \(P\) directement, mais file vers une zone sure \(R\) situee hors de l’axe \(P \to E\) (ex. \(R = (10, 8)\), au-dessus de l’axe).

La trajectoire de \(E\) n’est alors ni radiale ni perpendiculaire (composantes melangees), et celle de \(P\) (pure pursuit) devient courbe. Le temps de capture \(t^*\) n’admet plus de formule analytique simple : seule l’integration numérique (RK4) le donne. Ce scénario est central en jeux differentiels (Isaacs : escape to a target set).

// --- Poursuite-evasion 2D en RK4 (from-scratch) ---
// Etat x = [px, py, ex, ey]. Strategie de P = pure pursuit (vers E).
// Strategie de E : (a) radiale : fuit droit devant P ; (b) perpendiculaire.
public static double Distance(double[] p, double[] e)
{
    double dx = p[0] - e[0], dy = p[1] - e[1];
    return Math.Sqrt(dx * dx + dy * dy);
}

// Mode d'evasion de E : "radial" (fuit sur la ligne P->E) ou "perpendiculaire".
public static double SimulatePursuit(double vp, double ve, double[] startP, double[] startE,
                                     string mode, double dt, double eps, double tMax,
                                     out double captureTime)
{
    double[] x = { startP[0], startP[1], startE[0], startE[1] };
    double t = 0.0;
    double dInit = Distance(startP, startE);
    // Dynamique : P -> E a vitesse vp ; E dans sa direction d'evasion a vitesse ve.
    double[] Dyn(double tt, double[] s)
    {
        double dx = s[2] - s[0], dy = s[3] - s[1];
        double d = Math.Sqrt(dx * dx + dy * dy);
        if (d < 1e-12) return new double[4];
        double ux = dx / d, uy = dy / d;   // unite P -> E
        double vx, vy;                      // direction de fuite de E
        if (mode == "radial") { vx = ux; vy = uy; }             // fuit droit devant
        else { vx = -uy; vy = ux; }                             // perpendiculaire (norme a gauche)
        return new double[] { vp * ux, vp * uy, ve * vx, ve * vy };
    }
    while (t < tMax)
    {
        x = Rk4Step(Dyn, t, x, dt);
        t += dt;
        if (Distance(new double[] { x[0], x[1] }, new double[] { x[2], x[3] }) < eps) break;
    }
    captureTime = t;
    return dInit;
}

double vp = 2.0, ve = 1.0;
var p0 = new double[] { 0.0, 0.0 };
var e0 = new double[] { 10.0, 0.0 };

SimulatePursuit(vp, ve, p0, e0, "radial", dt: 0.001, eps: 0.05, tMax: 50.0, out double tRad);
SimulatePursuit(vp, ve, p0, e0, "perpendiculaire", dt: 0.001, eps: 0.05, tMax: 200.0, out double tPerp);

// --- Cas 3 : evasion vers un refuge (zone sure) ---
// E ne fuie pas P directement : il file vers un point de refuge R hors de l'axe P->E.
// Sa trajectoire n'est alors NI radiale NI perpendiculaire (composantes melangees),
// et celle de P (pure pursuit) devient courbe. Le temps de capture/echappement n'admet
// PLUS de formule analytique simple (d0/(vp-ve) ou d0/vp) : seule l'integration RK4 le donne.
double[] refuge = new double[] { 10.0, 8.0 };  // au-dessus de l'axe P->E
public static double SimulatePursuitRefuge(double vp, double ve, double[] startP, double[] startE,
                                           double[] refuge, double dt, double eps, double tMax,
                                           out bool escaped)
{
    double[] x = { startP[0], startP[1], startE[0], startE[1] };
    double t = 0.0;
    double[] Dyn(double tt, double[] s)
    {
        double dpx = s[2] - s[0], dpy = s[3] - s[1];   // P -> E
        double d = Math.Sqrt(dpx * dpx + dpy * dpy);
        if (d < 1e-12) return new double[4];
        double ux = dpx / d, uy = dpy / d;              // P : pure pursuit vers E
        double drx = refuge[0] - s[2], dry = refuge[1] - s[3];  // E -> refuge
        double dr = Math.Sqrt(drx * drx + dry * dry);
        if (dr < 1e-12) return new double[] { vp * ux, vp * uy, 0, 0 };
        double wx = drx / dr, wy = dry / dr;            // E : file vers le refuge
        return new double[] { vp * ux, vp * uy, ve * wx, ve * wy };
    }
    while (t < tMax)
    {
        x = Rk4Step(Dyn, t, x, dt);
        t += dt;
        double dPE = Distance(new double[] { x[0], x[1] }, new double[] { x[2], x[3] });
        double dER = Distance(new double[] { x[2], x[3] }, refuge);
        if (dPE < eps) { escaped = false; return t; }      // P capture E
        if (dER < eps) { escaped = true;  return t; }      // E atteint le refuge (echappe)
    }
    escaped = true;
    return tMax;
}

double tRefuge = SimulatePursuitRefuge(vp, ve, p0, e0, refuge, dt: 0.001, eps: 0.05, tMax: 200.0, out bool eRefugeEscaped);
double dRefuge = Distance(e0, refuge);  // distance E0 -> refuge (borne inferieure du temps d'E : dRefuge/ve)
double tRadTheory = 10.0 / (vp - ve);  // d0 / (vp - ve) = 10

var sb7 = new StringBuilder();
sb7.AppendLine("Poursuite-evasion (vp=2, ve=1, d0=10, eps=0.05) en RK4");
sb7.AppendLine(new string('=', 60));
sb7.AppendLine($"  evasion radiale        : t* = {tRad:F3}  (theorie d0/(vp-ve) = {tRadTheory:F3})  <- meilleur cas pour E");
sb7.AppendLine($"  evasion perpendiculaire: t* = {tPerp:F3}  (theorie d0/vp      = {10.0/vp:F3})  <- pire cas pour E");
sb7.AppendLine($"  evasion vers refuge   : t* = {tRefuge:F3}  (E atteint refuge a {dRefuge:F1}, borne dRefuge/ve = {dRefuge/ve:F3})  {(eRefugeEscaped ? "<- E ECHAPPE (atteint le refuge)" : "<- P capture E en chemin")}");
sb7.AppendLine($"  (refuge hors axe P->E : trajectoire courbe, PAS de formule analytique -> RK4 necessaire)");
sb7.AppendLine();
sb7.AppendLine($"  ecart radial vs theorie : {Math.Abs(tRad - tRadTheory):F4}");
sb7.AppendLine("  -> vp > ve garantit la capture en temps fini (Isaacs 1965).");
sb7.AppendLine("  -> radial maximise t* (E oppose toute sa vitesse) ; perpendiculaire le minimise (E gaspille sa vitesse).");
Show(sb7.ToString());
Poursuite-evasion (vp=2, ve=1, d0=10, eps=0.05) en RK4
============================================================
  evasion radiale        : t* = 9,951  (theorie d0/(vp-ve) = 10,000)  <- meilleur cas pour E
  evasion perpendiculaire: t* = 4,975  (theorie d0/vp      = 5,000)  <- pire cas pour E
  evasion vers refuge   : t* = 6,617  (E atteint refuge a 8,0, borne dRefuge/ve = 8,000)  <- P capture E en chemin
  (refuge hors axe P->E : trajectoire courbe, PAS de formule analytique -> RK4 necessaire)

  ecart radial vs theorie : 0,0490
  -> vp > ve garantit la capture en temps fini (Isaacs 1965).
  -> radial maximise t* (E oppose toute sa vitesse) ; perpendiculaire le minimise (E gaspille sa vitesse).

6.5 Interpretation : theoreme d’Isaacs

  1. Capture garantie : des que \(v_p > v_e\), le poursuivant capture l’evasif en temps fini, quelle que soit la stratégie d’evasion. C’est le résultat fondamental d’Isaacs (1965).
  2. Evasion radiale = meilleur cas pour E : \(E\) oppose toute sa vitesse, la distance se ferme a \((v_p - v_e)\), \(t^* = d_0/(v_p-v_e)\) maximal.
  3. Evasion perpendiculaire = pire cas pour E : la composante radiale de \(E\) est nulle, fermeture a \(v_p\) seul, \(t^* = d_0/v_p\) minimal. \(E\) gaspille sa vitesse lateralement.
  4. Lien avec les jeux LQ : la poursuite-evasion est un cas particulier de jeu differentiel a somme nulle ; la resolution passe aussi par une equation HJB/Riccati, mais l’approche geometrique directe (pure pursuit) est ici suffisante et lisible.
  5. Evasion vers refuge (cas non analytique) : avec \(R=(10,8)\) hors de l’axe, le simulateur donne \(t^* \approx 6{,}6\), intermediaire entre radial (\(\approx 10\)) et perpendiculaire (\(\approx 5\)), et surtout \(P\) capture \(E\) avant qu’il n’atteigne le refuge (la borne \(d_{refuge}/v_e = 8\) n’est pas atteinte). Ce résultat — ni l’un ni l’autre cas limite — n’est derivable d’aucune formule close : c’est précisément la que l’integrateur RK4 justifie son existence, la ou les cas radial et perpendiculaire ne faisaient que verifier des formules connues.

Prong B : ce twin C# transforme un exercice vide du Python en exemple pleinement resolu, illustrant le moteur differentiel la ou le Python se contentait d’une esquisse.

Synthese et transition vers les jeux cooperatifs

Ce que nous avons appris

  1. Jeux dynamiques : decisions dans le temps continu, etat evolutif.
  2. Open-loop vs feedback : engagement total vs adaptation a l’etat courant.
  3. Stackelberg : avantage du premier joueur par engagement stratégique.
  4. Jeux LQ : solution analytique lineaire \(u_i = -K_i x\) via Riccati couplee.
  5. Pursuit-evasion : jeu a somme nulle, capture garantie si \(v_p > v_e\) (Isaacs).

Formules cles

Concept Formule
Reaction follower (duopole lineaire) \(q_F(q_L) = (a - c_F - b q_L)/(2b)\)
Stackelberg (leader) \(q_L^* = (a - 2c_L + c_F)/(2b)\)
Feedback LQ \(u^*(x,t) = -K(t)\,x\), \(K = bP/r\) (Riccati)
Valeur de l’engagement \(\Delta\pi = \pi_{Stack} - \pi_{Cournot}\)
Capture (pursuit radial) \(t^* = d_0/(v_p - v_e)\)

Comparaison Cournot vs Stackelberg

Aspect Cournot Stackelberg
Timing Simultane Séquentiel
Output total 60 (bas) 67.5 (haut)
Prix 40 (haut) 32.5 (bas)
Profit leader 900 1012.50 (avantage)
Bien-etre industrie 1800 1518.75 (moins)

Du Stackelberg a Shapley

Non-cooperatif Cooperatif (notebook 15)
Avantage du leader Contribution marginale (Shapley)
Equilibre de Nash Core (stabilite)
Best response Fonction caractéristique
Engagement Formation de coalition

Notebook suivant : GameTheory-15-CooperativeGames-Python - Jeux cooperatifs, valeur de Shapley et formation de coalitions

7. Tranche 2 : verification croisee avec MathNet.Numerics (moteur SOTA .NET)

La tranche 1 a reimplemente tout from-scratch (BCL pur). Cette tranche 2 branche le moteur SOTA .NET MathNet.Numerics (deja en service dans Infer-8-TrueSkill) pour verifier que nos integrations et optimisations from-scratch coincident avec une bibliotheque de production.

Deux experiences concretes du jumeau Python sont reprises :

  1. Integration ODE : le jumeau Python resout la dynamique fermee du jeu LQ avec scipy.integrate.solve_ivp (cellule 37). Ici, MathNet.Numerics.OdeSolvers.RungeKutta .FourthOrder integre la meme dynamique feedback Nash que notre RK4 from-scratch (cellule 11), et on compare les vecteurs de trajectoire (distance L-infini) sous tolerance declaree.
  2. Optimisation du leader : le jumeau Python resout l’equilibre de Stackelberg statique avec scipy.optimize.minimize_scalar (cellule 20). Ici, NelderMeadSimplex maximise le profit du leader sur la fonction de reaction du follower, et on compare l’optimum vecteur-a-vecteur a la forme close (q_L* = 45).

La comparaison vecteur-a-vecteur (pas des constantes attendues) est le gabarit exige pour tout nouveau pont SOTA (#11058) : elle reste valide quand le jeu n’a pas de formule close.

// --- Tranche 2 : integration ODE avec MathNet.Numerics (moteur SOTA) ---
// Experience 1 : la dynamique fermee du jeu LQ en feedback Nash (cellule 11),
// integree par MathNet RungeKutta.FourthOrder et comparee a notre RK4 from-scratch.
#r "nuget: MathNet.Numerics, 5.0.0"
using MathNet.Numerics.LinearAlgebra;
using MathNet.Numerics.OdeSolvers;

// Dynamique fermee (identique a la cellule 11) : dx/dt = (a - b1*K1 - b2*K2) x
double AclAt(double t)
{
    int idx = Math.Min((int)Math.Round(t / lq.Dt), K1.Length - 1);
    return lq.A - lq.B1 * K1[idx] - lq.B2 * K2[idx];
}
double[] DynClosedLoop(double t, double[] x) => new double[] { AclAt(t) * x[0] };

// --- Trajectoire from-scratch (cellule 11, meme pas que MathNet) ---
double[] x0v = { 1.0 };
double Tsim = lq.T;
double dtSim = lq.Dt;   // pas = grille de gains K (concordance aux bornes)
var trajScratch = IntegrateRk4(DynClosedLoop, x0v, T: Tsim, dt: dtSim);

// --- Meme trajectoire via MathNet RungeKutta.FourthOrder (meme pas) ---
int nSteps = (int)Math.Round(Tsim / dtSim);
Vector<double>[] trajMathNet = RungeKutta.FourthOrder(
    Vector<double>.Build.DenseOfArray(x0v), 0.0, Tsim, nSteps + 1,  // N = nb de points de sortie (MathNet integre N-1 pas)
    (t, x) => Vector<double>.Build.DenseOfArray(new double[] { AclAt(t) * x[0] }));

// --- Concordance vecteur-a-vecteur : distance L-infini ---
int nPts = Math.Min(trajScratch.Length, trajMathNet.Length);
double lInf = 0.0;
for (int i = 0; i < nPts; i++) lInf = Math.Max(lInf, Math.Abs(trajScratch[i][0] - trajMathNet[i][0]));
double tol = 1e-6;
var sbT2 = new StringBuilder();
sbT2.AppendLine("Tranche 2 -- Concordance ODE MathNet vs from-scratch (LQ feedback Nash, x0=1)");
sbT2.AppendLine($"  x(T) from-scratch    : {trajScratch[^1][0]:F9}");
sbT2.AppendLine($"  x(T) MathNet RK4     : {trajMathNet[^1][0]:F9}");
sbT2.AppendLine($"  distance L-infini    : {lInf:E3} (tolerance {tol:E1})");
sbT2.AppendLine($"  verdict              : {(lInf <= tol ? "CONCORDANT" : "ECART")}");
Show(sbT2.ToString());
Installed Packages
  • MathNet.Numerics, 5.0.0
Tranche 2 -- Concordance ODE MathNet vs from-scratch (LQ feedback Nash, x0=1)
  x(T) from-scratch    : 0,000010279
  x(T) MathNet RK4     : 0,000010279
  distance L-infini    : 0,000E+000 (tolerance 1,0E-006)
  verdict              : CONCORDANT
// --- Tranche 2 : optimisation du leader avec MathNet NelderMead (moteur SOTA) ---
// Experience 2 : l'equilibre de Stackelberg statique que le jumeau Python resout avec
// scipy.optimize.minimize_scalar (bounded) -- retrouve ici par NelderMeadSimplex.
using MathNet.Numerics.Optimization;

// Profit du leader avec reaction du follower (cf. Python solve_static_stackelberg).
double LeaderProfitNet(double qL)
{
    double qF = duo.FollowerReaction(qL);
    return duo.LeaderProfit(qL, qF);
}

// Maximiser le profit = minimiser -profit (NelderMead est un minimiseur).
var obj = ObjectiveFunction.Value(x => -LeaderProfitNet(x[0]));
var opt = NelderMeadSimplex.Minimum(obj,
    Vector<double>.Build.DenseOfArray(new double[] { 30.0 }),
    Vector<double>.Build.DenseOfArray(new double[] { 5.0 }));
double qLStar = opt.MinimizingPoint[0];
double qFStar = duo.FollowerReaction(qLStar);

// Forme close (cellule 6) et comparaison vecteur-a-vecteur.
var (qLClosed, qFClosed) = StackelbergEquilibrium(demand, firmA, firmB);
double lInfOpt = Math.Max(Math.Abs(qLStar - qLClosed), Math.Abs(qFStar - qFClosed));
double tolOpt = 1e-4;
var sbT2b = new StringBuilder();
sbT2b.AppendLine("Tranche 2 -- Optimum Stackelberg statique : MathNet NelderMead vs forme close");
sbT2b.AppendLine($"  qL* MathNet : {qLStar:F6}   |  forme close : {qLClosed:F6}");
sbT2b.AppendLine($"  qF* MathNet : {qFStar:F6}   |  forme close : {qFClosed:F6}");
sbT2b.AppendLine($"  ecart L-infini : {lInfOpt:E3} (tolerance {tolOpt:E1})");
sbT2b.AppendLine($"  verdict        : {(lInfOpt <= tolOpt ? "CONCORDANT" : "ECART")}");
sbT2b.AppendLine($"  (Python scipy  : minimize_scalar -> qL* = 45, meme optimum)");
Show(sbT2b.ToString());
Tranche 2 -- Optimum Stackelberg statique : MathNet NelderMead vs forme close
  qL* MathNet : 45,000000   |  forme close : 45,000000
  qF* MathNet : 22,500000   |  forme close : 22,500000
  ecart L-infini : 0,000E+000 (tolerance 1,0E-004)
  verdict        : CONCORDANT
  (Python scipy  : minimize_scalar -> qL* = 45, meme optimum)

8. Exercices

Exercice 1 : Stackelberg asymetrique (\(c_L \neq c_F\))

Reprenez le duopole avec un leader plus efficace (\(c_L = 5\)) qu’un follower (\(c_F = 15\)). Calculez les quantites et profits d’equilibre. Le leader beneficie-t-il deux fois (avantage du 1er joueur ET avantage de cout) ?

Exercice 2 : Valeur de l’engagement

Implementez ValeurEngagement(demand, firmL, firmF) qui retourne \(\pi^{Stack}_L - \pi^{Cournot}_L\) pour le leader. Evaluez-la pour \(c \in \{5, 10, 20, 40\}\) : l’avantage d’engagement croit-il ou decroit-il avec le cout ?

Indice : utilisez CournotEquilibrium et StackelbergEquilibrium déjà définies.

Exercice 3 : Pursuit-evasion avec evasion intelligente

Actuellement l’evasif fuit perpendiculairement (stratégie fixe). Implementez une evasion opportuniste : a chaque pas, E choisit la direction (parmi radial, perpendiculaire-gauche, perpendiculaire-droite) qui maximise la distance a P au pas suivant. Comparez \(t^*\) aux deux stratégies de reference.

Exercice 4 : Stackelberg a 3 joueurs (un leader, deux followers)

Un leader \(L\) choisit \(q_L\) ; deux followers \(F_1, F_2\) jouent ensuite un sous-jeu de Cournot entre eux (connaissant \(q_L\)). Donnez l’expression de l’equilibre et simulez-le pour \(a=100, b=1, c=10\).

Indice : les followers jouent Cournot sur le marche residuel \(Q_R = (a - b q_L) - b(q_{F1}+q_{F2})\).

// Exercice 1 : Stackelberg asymetrique (c_L != c_F)
// TODO etudiant : calculer l'equilibre avec c_L = 5, c_F = 15
// Etape 1 : definir firmL = new Firm("L", 5), firmF = new Firm("F", 15)
// Etape 2 : appeler StackelbergEquilibrium(demand, firmL, firmF)
// Etape 3 : comparer au cas symetrique (45, 22.5)
var resExo1 = (qL: 0.0, qF: 0.0);  // TODO etudiant
Show("Exercice a completer : Stackelberg asymetrique (c_L=5, c_F=15)");
Exercice a completer : Stackelberg asymetrique (c_L=5, c_F=15)
// Exercice 2 : Valeur de l'engagement
// TODO etudiant : pi_Stackelberg(leader) - pi_Cournot(leader)
// Etape 1 : boucler sur c in {5, 10, 20, 40}
// Etape 2 : pour chaque c, calculer pi_Cournot et pi_Stackelberg du leader
// Etape 3 : afficher la difference et commenter la tendance
double valeurEngagement = 0.0;  // TODO etudiant
Show("Exercice a completer : Valeur de l'engagement vs cout marginal");
Exercice a completer : Valeur de l'engagement vs cout marginal
// Exercice 3 : Pursuit-evasion a evasion opportuniste
// TODO etudiant : a chaque pas, E choisit la direction maximisant la distance a P
// Etape 1 : pour chaque candidat (radial, perp-gauche, perp-droite), simuler 1 pas
// Etape 2 : garder la direction qui eloigne le plus E de P
// Etape 3 : comparer t* a "radial" et "perpendiculaire" fixes
double tCaptureOpportuniste = 0.0;  // TODO etudiant
Show("Exercice a completer : Evasion opportuniste (max distance a P)");
Exercice a completer : Evasion opportuniste (max distance a P)
// Exercice 4 : Stackelberg a 3 joueurs (1 leader + 2 followers en Cournot)
// TODO etudiant : expression de l'equilibre sur le marche residuel
// Etape 1 : followers jouent Cournot sur le marche residuel apres q_L
// Etape 2 : leader maximise son profit en anticipant q_F1*(q_L), q_F2*(q_L)
// Etape 3 : simuler pour a=100, b=1, c=10
var resExo4 = (qL: 0.0, qF1: 0.0, qF2: 0.0);  // TODO etudiant
Show("Exercice a completer : Stackelberg a 3 joueurs (marche residuel)");
Exercice a completer : Stackelberg a 3 joueurs (marche residuel)

Conclusion

Ce jumeau C# a presente les jeux differentiels et les equilibres de Stackelberg, reimplementation from-scratch (tranche 1) plus une verification croisee avec le moteur SOTA .NET MathNet.Numerics (tranche 2).

Résultats cles reproduits (Math Verification Standard)

Modèle Leader Follower Industrie Source
Cournot (statique) q=30, pi=900 q=30, pi=900 1800 closed-form
Stackelberg (dynamique) q=45, pi=1012.50 q=22.5, pi=506.25 1518.75 closed-form
LQ feedback K1(0)=0.5450 K2(0)=-0.5450 (symetrie) Riccati = eq. 3P²+0.2P-1=0
Pursuit radial t*=10 (capture) – = d0/(vp-ve) verifie

Value-add du jumeau (Prong B, #3801)

  • Tranche 1 : RK4 from-scratch remplace scipy.integrate.solve_ivp (validation e^{-1} a 1e-9), Riccati couplee backward en double[] pur, pursuit-evasion pleinement modelisee. 0 NuGet, 0 numpy, 0 scipy : BCL .NET 9 uniquement.
  • Tranche 2 : branchement de MathNet.Numerics (moteur SOTA .NET, deja en service dans Infer-8) – concordance vecteur-a-vecteur de l’ODE feedback Nash (RungeKutta.FourthOrder vs from-scratch, L-infini < 1e-6) et de l’optimum Stackelberg statique (NelderMeadSimplex vs forme close, L-infini < 1e-4). Gabarit de pont exige pour tout nouveau pont SOTA (#11058).

Suite : GT-15 - Jeux cooperatifs | Retour au sommaire

Retour au sommet