SocialChoice 04 - Agrégation Computationnelle : SAT et Z3

Navigation : << 03-Voting-Methods.ipynb | Index

Autres notebooks : 01-Arrow-Impossibility-Theorem.ipynb | 01b-Lean-SocialChoice-Formal.ipynb

Kernel : Python 3


Introduction

Ce notebook utilise les solveurs SAT (satisfiabilite propositionnelle) pour verifier mecaniquement les théorèmes d’impossibilité du choix social :

  • Théorème d’Arrow (1951) : aucune fonction de bien-être social avec 3+ alternatives ne peut satisfaire simultanément Pareto, IIA et non-dictature
  • Théorème de Sen (1970) : liberté minimale + Pareto + transitivité sont incompatibles
  • Théorème de Muller-Satterthwaite (1977) : monotonicite + unanimite + non-dictature

L’approche SAT traduit chaque condition en clauses CNF (forme normale conjonctive) puis demande au solveur s’il existe une affectation satisfaisant toutes les contraintes. Si le solveur retourne UNSAT, le théorème est vérifié : aucune fonction d’agrégation ne peut satisfaire toutes les conditions.

Prerequis

  • Notebook SC-03 Méthodes de vote (concepts du choix social)
  • Notions de logique propositionnelle (variables booléennes, clauses, CNF)

Durée estimee : 45 minutes

Ancres savantes – Arrow, K.J. (1951), Social Choice and Individual Values, Cowles Commission Monograph 12, Wiley (théorème d’impossibilité d’Arrow, encode ici en clauses CNF et vérifié UNSAT par le solveur SAT) ; Sen, A.K. (1970), The Impossibility of a Paretian Liberal, Journal of Political Economy 78(1):152-157 (théorème de Sen : liberté minimale + Pareto + transitivité incompatibles, encode en SMT) ; Muller, E. & Satterthwaite, M.A. (1977), The Equivalence of Strong Positive Association and Strategy-proofness, Journal of Economic Theory 14(2):412-418 (théorème de Muller-Satterthwaite : monotonicite + unaninite + non-dictature incompatibles) ; Tseitin, G.S. (1968), On the Complexity of Derivations in the Propositional Calculus, in Slisenko (ed.), Structures in Constructive Mathematics and Mathematical Logic, Part II, pp. 115-125, Nauka, Leningrad (transformation de Tseitin : conversion equivalente en forme normale conjonctive, fondement de l’encodage SAT des axiomes) ; de Moura, L. & Bjorner, N. (2008), Z3: An Efficient SMT Solver, in TACAS 2008, Lecture Notes in Computer Science 4963:337-340 (solveur SMT Z3 utilise pour l’encodage compact a variables entières) ; Ignatiev, A., Morgado, A. & Marques-Silva, J. (2018), PySAT: A Python Toolkit for Prototyping with SAT Oracles, in SAT 2018, Lecture Notes in Computer Science 10929:428-437 (boite a outils PySAT pour l’encodage booléen des axiomes d’Arrow).


1. Rappels : Logique Propositionnelle et SAT

Un problème SAT demande s’il existe une affectation de variables booléennes qui rend vraie une formule en Forme Normale Conjonctive (CNF) :

\[\text{CNF} = C_1 \wedge C_2 \wedge \cdots \wedge C_m\]

ou chaque clause \(C_j = \ell_1 \vee \ell_2 \vee \cdots \vee \ell_k\) est une disjonction de litteraux (variable ou sa negation).

Convention PySAT

  • Variable \(x_i\) representee par l’entier \(i\) (positif)
  • Negation \(\neg x_i\) representee par l’entier \(-i\)
  • Clause : liste d’entiers, ex. [1, -3, 5] signifie \(x_1 \vee \neg x_3 \vee x_5\)
  • Formule CNF : liste de clauses
# Configuration et imports
from pysat.solvers import Glucose3, Minisat22, Cadical103
from pysat.formula import CNF
from itertools import permutations, combinations, product
from collections import defaultdict

print("Configuration OK : SocialChoice 04 - SAT/Z3")
print("Pysat installe avec succes")
Configuration OK : SocialChoice 04 - SAT/Z3
Pysat installe avec succes

Configuration de l’environnement PySAT

Ce notebook utilise trois solveurs SAT modernes implements en C++ avec des bindings Python : - Glucose3 : solveur CDCL (Conflict-Driven Clause Learning) avec apprentissage agressif de clauses - MiniSat22 : un des solveurs CDCL les plus simples et efficaces, souvent utilise comme référence - CaDiCaL103 : solveur recent optimise pour les très grandes instances

La bibliotheque pysat.formula permet de construire des formules CNF (Forme Normale Conjonctive) qui seront transmises aux solveurs. Chaque clause est une liste d’entiers representant des litteraux (positifs ou negatifs).

1.1 Premier exemple : vérification SAT simple

Resolvons une formule CNF simple pour illustrer le fonctionnement.

Formule : \((x_1 \vee x_2) \wedge (\neg x_1 \vee x_3) \wedge (\neg x_2 \vee \neg x_3)\)

# Exemple simple : verification SAT
cnf_example = CNF()
cnf_example.append([1, 2])       # x1 OR x2
cnf_example.append([-1, 3])      # NOT x1 OR x3
cnf_example.append([-2, -3])     # NOT x2 OR NOT x3

with Glucose3() as solver:
    solver.append_formula(cnf_example.clauses)
    sat = solver.solve()
    model = solver.get_model() if sat else None

print(f"Formule : (x1 v x2) ^ (~x1 v x3) ^ (~x2 v ~x3)")
print(f"Resultat : {'SAT' if sat else 'UNSAT'}")
if model:
    print(f"Modele : {model}")
    print(f"  x1 = {model[0] > 0}, x2 = {model[1] > 0}, x3 = {model[2] > 0}")

# Verification manuelle
print("\nVerification des modeles possibles :")
for x1, x2, x3 in product([False, True], repeat=3):
    vals = {1: x1, 2: x2, 3: x3}
    c1 = x1 or x2
    c2 = (not x1) or x3
    c3 = (not x2) or (not x3)
    if c1 and c2 and c3:
        print(f"  x1={x1}, x2={x2}, x3={x3} : SATISFAIT")
Formule : (x1 v x2) ^ (~x1 v x3) ^ (~x2 v ~x3)
Resultat : SAT
Modele : [-1, 2, -3]
  x1 = False, x2 = True, x3 = False

Verification des modeles possibles :
  x1=False, x2=True, x3=False : SATISFAIT
  x1=True, x2=False, x3=True : SATISFAIT

Interprétation de l’exemple SAT

Le solveur trouve deux modèles satisfaisants : - Modèle 1 : x1=False, x2=True, x3=False (le solveur retourne [-1, 2, -3]) - Modèle 2 : x1=True, x2=False, x3=True

Vérification manuelle : - Clause 1 (x1 ∨ x2) : satisfaite dans les deux modèles (au moins une variable vraie) - Clause 2 (¬x1 ∨ x3) : Modèle 1 (True ∨ False = True), Modèle 2 (False ∨ True = True) - Clause 3 (¬x2 ∨ ¬x3) : Modèle 1 (False ∨ True = True), Modèle 2 (True ∨ False = True)

Cet exemple illustre le fonctionnement d’un solveur SAT : il cherche une affectation de variables qui satisfasse toutes les clauses. Pour les théorèmes d’impossibilité, nous chercherons au contraire UNSAT (aucun modèle n’existe).

En complément — pourquoi UNSAT prouve l’impossibilité : l’absence de modèle satisfaisant toutes les clauses prouve qu’aucune fonction d’agrégation ne peut satisfaire toutes les contraintes simultanément — c’est le mécanisme exact par lequel les théorèmes d’impossibilité (Arrow, Sen) sont vérifiés computationnellement.

Pourquoi verifier les modèles a la main ? Le solveur retourne UN modèle ([-1, 2, -3], soit x1=False, x2=True, x3=False), mais la boucle de vérification enumere deux affectations satisfaisantes. Cette distinction distingue satisfiable de tautologie : la formule admet plusieurs modèles, elle n’est donc pas valide pour toute affectation. C’est exactement le type de raisonnement qu’on automatisera pour Arrow — chercher un modèle de la negation du théorème, et constater qu’il n’y en a aucun (UNSAT).


2. Encodage SAT du Théorème d’Arrow

Le théorème d’Arrow (1951) dit qu’avec \(|A| \geq 3\) alternatives et \(n \geq 2\) électeurs, toute fonction de bien-être social (SWF) satisfaisant :

  1. Domaine universel : accepte tout profil de préférences individuelles
  2. Pareto : si tous preferent \(x\) a \(y\), la societe aussi
  3. IIA (Indépendance des Alternatives Irrelevantes) : le classement social entre \(x\) et \(y\) depend uniquement des préférences individuelles sur \(\{x, y\}\)
  4. Non-dictature : aucun individu ne décide seul le classement social

…doit être dictatoriale. C’est une impossibilité.

2.1 Variables booléennes

Nous encodons une SWF comme un ensemble de variables booléennes :

  • \(R(x, y)\) : la societe prefere strictement \(x\) a \(y\) (ou egalite)
  • Pour chaque profil \(\pi\) (ordre des préférences individuelles), les variables \(R_\pi(x, y)\) indiquent le classement social.

2.2 Encodage des contraintes

Chaque condition du théorème est traduite en clauses CNF :

class ArrowSATEncoder:
    """Encode le theoreme d'Arrow comme un probleme SAT.

    Variables booleennes :
      r[pi][x,y] = True signifie : pour le profil pi, la societe prefere x a y.

    L'encodage explore TOUS les profils de preferences possibles
    (domaine universel) et verifie les 3 conditions :
    Pareto, IIA, Non-dictature.
    """

    def __init__(self, alternatives, n_voters):
        self.alternatives = alternatives
        self.n_alt = len(alternatives)
        self.n_voters = n_voters
        self.alt_indices = {a: i for i, a in enumerate(alternatives)}

        # Generer tous les profils possibles (tous les ordres pour chaque electeur)
        all_orders = list(permutations(alternatives))
        self.profiles = list(product(*([all_orders] * n_voters)))

        # Index des variables : (profil_index, x, y) -> variable_id
        self.var_counter = 0
        self.var_map = {}
        self._init_variables()

    def _init_variables(self):
        """Cree les variables booleennes pour chaque (profil, paire d'alternatives)."""
        for pi_idx in range(len(self.profiles)):
            for x, y in permutations(self.alternatives, 2):
                self.var_counter += 1
                self.var_map[(pi_idx, x, y)] = self.var_counter

    def get_var(self, pi_idx, x, y):
        return self.var_map[(pi_idx, x, y)]

    def encode_completeness(self):
        """Pour chaque (profil, paire) : x > y OU y > x (totalite)."""
        clauses = []
        for pi_idx in range(len(self.profiles)):
            for x, y in combinations(self.alternatives, 2):
                v_xy = self.get_var(pi_idx, x, y)
                v_yx = self.get_var(pi_idx, y, x)
                clauses.append([v_xy, v_yx])  # x > y OR y > x
        return clauses
    def encode_asymmetry(self):
        """Pour chaque (profil, paire) : non (x > y ET y > x)."""
        clauses = []
        for pi_idx in range(len(self.profiles)):
            for x, y in combinations(self.alternatives, 2):
                v_xy = self.get_var(pi_idx, x, y)
                v_yx = self.get_var(pi_idx, y, x)
                clauses.append([-v_xy, -v_yx])  # NOT(x>y) OR NOT(y>x)
        return clauses
    def encode_transitivity(self):
        """Si x > y et y > z, alors x > z (pour chaque profil)."""
        clauses = []
        for pi_idx in range(len(self.profiles)):
            for x, y, z in permutations(self.alternatives, 3):
                v_xy = self.get_var(pi_idx, x, y)
                v_yz = self.get_var(pi_idx, y, z)
                v_xz = self.get_var(pi_idx, x, z)
                # NOT(x>y) OR NOT(y>z) OR (x>z)
                clauses.append([-v_xy, -v_yz, v_xz])
        return clauses
    def encode_pareto(self):
        """Si tous les electeurs preferent x a y, la societe prefere x a y."""
        clauses = []
        for pi_idx, profile in enumerate(self.profiles):
            for x, y in permutations(self.alternatives, 2):
                all_prefer = all(
                    voter_pref.index(x) < voter_pref.index(y)
                    for voter_pref in profile
                )
                if all_prefer:
                    v_xy = self.get_var(pi_idx, x, y)
                    clauses.append([v_xy])  # Force x > y
        return clauses
    def encode_iia(self):
        """IIA : si les prefs relatives entre x et y sont identiques dans 2 profils,
        le classement social entre x et y doit etre identique."""
        clauses = []
        for pi1_idx in range(len(self.profiles)):
            for pi2_idx in range(pi1_idx + 1, len(self.profiles)):
                prof1 = self.profiles[pi1_idx]
                prof2 = self.profiles[pi2_idx]
                for x, y in permutations(self.alternatives, 2):
                    same_xy = all(
                        (v1.index(x) < v1.index(y)) == (v2.index(x) < v2.index(y))
                        for v1, v2 in zip(prof1, prof2)
                    )
                    if same_xy:
                        v1_xy = self.get_var(pi1_idx, x, y)
                        v2_xy = self.get_var(pi2_idx, x, y)
                        clauses.append([-v1_xy, v2_xy])   # R1(x,y) => R2(x,y)
                        clauses.append([v1_xy, -v2_xy])   # R2(x,y) => R1(x,y)
        return clauses
    def encode_non_dictatorship(self):
        """Non-dictature (Arrow) : pour chaque electeur i, il existe au moins
        un couple (profil, paire) ou le classement social contredit i.

        Un electeur i est un dictateur (au sens d'Arrow) s'il determine le
        classement social dans TOUS les profils. On nie cela par UNE seule
        clause disjonctive par electeur : il existe au moins un profil pi et une
        paire (x, y) ou i prefere x a y mais la societe ne suit pas.

        Une clause par electeur (et non par paire) : sinon on exige que i soit
        contredit sur chaque paire, ce qui sur-contraint l'encodage et declare
        faussement UNSAT le cas |A| = 2 -- alors qu'une SWF non dictatoriale
        (regle majoritaire) y satisfait Pareto + IIA + non-dictature."""
        clauses = []
        for voter in range(self.n_voters):
            witness = []
            for pi_idx, profile in enumerate(self.profiles):
                for x, y in permutations(self.alternatives, 2):
                    if profile[voter].index(x) < profile[voter].index(y):
                        # i prefere x a y sur ce profil ; temoin = social ne prefere PAS x a y
                        witness.append(-self.get_var(pi_idx, x, y))
            if witness:
                clauses.append(witness)  # au moins une contradiction pour cet electeur
        return clauses
    def encode_all(self):
        """Retourne toutes les clauses CNF pour le theoreme d'Arrow."""
        clauses = []
        clauses.extend(self.encode_completeness())
        clauses.extend(self.encode_asymmetry())
        clauses.extend(self.encode_transitivity())
        clauses.extend(self.encode_pareto())
        clauses.extend(self.encode_iia())
        clauses.extend(self.encode_non_dictatorship())
        return clauses
    def stats(self):
        """Retourne les statistiques de l'encodage."""
        clauses = self.encode_all()
        return {
            'alternatives': self.n_alt,
            'voters': self.n_voters,
            'profiles': len(self.profiles),
            'variables': self.var_counter,
            'clauses': len(clauses),
        }


# Test : encodage pour 3 alternatives, 2 electeurs
enc = ArrowSATEncoder(['A', 'B', 'C'], n_voters=2)
stats = enc.stats()
print(f"Encodage Arrow (3 alt, 2 voters)")
print(f"  Profils : {stats['profiles']}")
print(f"  Variables : {stats['variables']}")
print(f"  Clauses : {stats['clauses']}")
Encodage Arrow (3 alt, 2 voters)
  Profils : 36
  Variables : 216
  Clauses : 2216

Interprétation de la structure de l’encodeur

La classe ArrowSATEncoder genere un problème SAT avec 216 variables et 2216 clauses pour 3 alternatives et 2 électeurs. Chaque variable represente une decision sociale (x > y) pour un profil spécifique.

Decomposition des clauses : - Totalite et asymetrie : assurent que le classement social est une relation d’ordre stricte (antisymetrique et totale) - Transitivité : garantit la cohérence logique du classement (si x > y et y > z, alors x > z) - Pareto : 36 clauses forcent le consensus social quand tous les électeurs sont d’accord - IIA : plus de 1000 clauses assurent la cohérence locale des decisions sociales - Non-dictature : clauses excluant les fonctions dictatoriales (un electeur décide toujours seul)

L’explosion du nombre de clauses s’explique par IIA et la non-dictature qui doivent verifier des conditions globales sur tous les profils.

En complément — lecture du résultat : si le solveur retourne UNSAT, cela prouve qu’aucune affectation ne satisfait toutes les contraintes — le théorème d’Arrow est alors vérifié computationnellement.

Concretement, chaque variable booleenne encode une decision locale de la SWF candidate (« pour le profil pi, la social préférence place a avant b »), et chaque clause interdit une violation locale d’un axiome. La transitivity, Pareto et IIA ne sont pas des formules separees : elles sont compilees dans la même conjonction de clauses, de sorte qu’un modèle de la formule complete serait une SWF contredisant le théorème d’Arrow. C’est cette compilation qui rend la vérification mécanique possible.


3. Vérification UNSAT : Le Théorème d’Arrow Prouve par SAT

Maintenant que l’encodage SAT est construit, nous allons utiliser les solveurs pour verifier le théorème d’Arrow. L’idee est de demander au solveur s’il existe une fonction de bien-être social satisfaisant toutes les contraintes simultanément.

Nous allons tester avec trois solveurs différents pour confirmer que le résultat est robuste et ne depend pas de l’implementation spécifique du solveur.

def verify_arrow_with_sat(alternatives, n_voters, solver_class=Glucose3):
    """Verifie le theoreme d'Arrow par SAT solving.

    Retourne (sat_result, stats_dict).
    Si UNSAT, le theoreme est verifie.
    """
    enc = ArrowSATEncoder(alternatives, n_voters)
    clauses = enc.encode_all()
    stats = enc.stats()

    with solver_class() as solver:
        solver.append_formula(clauses)
        sat = solver.solve()

    return sat, stats


# Verification avec 3 solveurs
alternatives_3 = ['A', 'B', 'C']
solvers = [('Glucose3', Glucose3), ('MiniSat22', Minisat22), ('CaDiCaL103', Cadical103)]

print("VERIFICATION DU THEOREME D'ARROW PAR SAT")
print("=" * 50)
print(f"Configuration : {len(alternatives_3)} alternatives, 2 electeurs")
print()

for name, SolverClass in solvers:
    sat, stats = verify_arrow_with_sat(alternatives_3, 2, SolverClass)
    result = 'SAT (contredit Arrow!)' if sat else 'UNSAT (Arrow verifie)'
    print(f"  {name:12s} : {result}")
    print(f"    {stats['variables']} variables, {stats['clauses']} clauses, {stats['profiles']} profils")

print()
print("Resultat : UNSAT pour tous les solveurs")
print("=> Aucune SWF ne peut satisfaire Pareto + IIA + non-dictature")
print("=> Le theoreme d'Arrow est mecaniquement verifie")

# Pousser jusqu'a 4 alternatives (576 profils, ~1 M clauses) : le solveur tient-il ?
# L'extrapolation naive de la taille suggere l'abandon ; on mesure au lieu d'estimer.
import time, signal
from ctypes import ArgumentError

class Timeout(Exception):
    pass

def _alarm(sig, frame):
    raise Timeout()

signal.signal(signal.SIGALRM, _alarm)

print()
print("CONFIGURATION 4 ALTERNATIVES, 2 ELECTEURS (576 profils)")
print("=" * 50)
alternatives_4 = ['A', 'B', 'C', 'D']
budget_s = 30
for name, SolverClass in [('Glucose3', Glucose3), ('CaDiCaL103', Cadical103)]:
    signal.alarm(budget_s)
    t0 = time.time()
    try:
        sat4, stats4 = verify_arrow_with_sat(alternatives_4, 2, SolverClass)
        signal.alarm(0)
        elapsed4 = (time.time() - t0) * 1000
        result4 = 'SAT (contredit Arrow!)' if sat4 else 'UNSAT (Arrow verifie)'
        print(f"  {name:12s} : {result4}")
        print(f"    {stats4['variables']:,} variables, {stats4['clauses']:,} clauses, {stats4['profiles']} profils, {elapsed4:.0f} ms")
    except Timeout:
        signal.alarm(0)
        print(f"  {name:12s} : ABANDON apres {time.time()-t0:.0f} s (budget {budget_s} s epuise)")
    except ArgumentError:
        # alarme tombee pendant un appel C (pysat/ctypes) : le Timeout du handler
        # est re-emballe en ArgumentError par ctypes -- meme signification
        signal.alarm(0)
        print(f"  {name:12s} : ABANDON apres {time.time()-t0:.0f} s (budget {budget_s} s epuise)")
VERIFICATION DU THEOREME D'ARROW PAR SAT
==================================================
Configuration : 3 alternatives, 2 electeurs

  Glucose3     : UNSAT (Arrow verifie)
    216 variables, 2216 clauses, 36 profils
  MiniSat22    : UNSAT (Arrow verifie)
    216 variables, 2216 clauses, 36 profils
  CaDiCaL103   : UNSAT (Arrow verifie)
    216 variables, 2216 clauses, 36 profils

Resultat : UNSAT pour tous les solveurs
=> Aucune SWF ne peut satisfaire Pareto + IIA + non-dictature
=> Le theoreme d'Arrow est mecaniquement verifie

CONFIGURATION 4 ALTERNATIVES, 2 ELECTEURS (576 profils)
==================================================
  Glucose3     : UNSAT (Arrow verifie)
    6,912 variables, 1,010,882 clauses, 576 profils, 3208 ms
  CaDiCaL103   : UNSAT (Arrow verifie)
    6,912 variables, 1,010,882 clauses, 576 profils, 2835 ms

Vérification mécanique du théorème d’Arrow

Les trois solveurs (Glucose3, MiniSat22, CaDiCaL103) retournent tous UNSAT pour la configuration avec 3 alternatives et 2 électeurs. Ce résultat signifie qu’il n’existe aucune affectation des variables booléennes qui satisfasse simultanément :

  • Totalite : pour chaque paire (x, y), soit x > y, soit y > x
  • Asymetrie : pas a la fois x > y et y > x
  • Transitivité : si x > y et y > z, alors x > z
  • Pareto : si tous preferent x a y, alors x > y socialement
  • IIA : le classement social entre x et y depend seulement des préférences individuelles sur x et y
  • Non-dictature : aucun electeur ne décide toujours seul

C’est une preuve par l’absurde mécanique : si le solveur ne trouve aucun modèle satisfaisant toutes les contraintes, alors aucune fonction de bien-être social n’existe, ce qui prouve le théorème d’Arrow.

La même cellule pousse la vérification jusqu’à 4 alternatives (2 électeurs, 576 profils) : Glucose3 et CaDiCaL103 y rendent aussi UNSAT — la preuve mécanique déborde le cas 3×2.

Interprétation

Les trois solveurs confirment UNSAT : il n’existe aucune fonction de bien-être social qui satisfasse simultanément Pareto, IIA et non-dictature avec 3 alternatives et 2 électeurs.

C’est une preuve mécanique du théorème d’Arrow pour cette configuration spécifique. La preuve est exhaustive car l’encodage couvre tous les profils possibles.

Triple corroboration. Les trois solveurs — Glucose3, MiniSat22, CaDiCaL103 — rendent le même verdict UNSAT sur le même encodage (216 variables, 2216 clauses, 36 profils). Ces moteurs partagent l’algorithme CDCL mais divergent par leurs heuristiques de branchement et d’apprentissage de clauses ; un faux UNSAT de l’un serait extremement improbable chez les trois. Côté exhaustivite : l’encodage couvre les 36 profils possibles (3! x 3! ordres individuels pour 2 électeurs) — le SAT check a donc exploré tout l’espace qu’une enumeration manuelle aurait du couvrir.

Frontière poussée. L’instance 4 alternatives × 2 électeurs (6 912 variables, 1 010 882 clauses, 576 profils) est tranchée elle aussi : Glucose3 et CaDiCaL103 rendent tous deux UNSAT. Les durées observées restent dans la sortie de la cellule précédente, où elles peuvent être remesurées sans figer une valeur machine-dépendante dans la prose. Ce n’est pas la largeur qui referme l’approche mécanique mais la profondeur du profil : à 3 électeurs, la construction de l’encodage elle-même dépasse le budget configuré (section 4).


4. Passage a l’echelle : Impact du nombre d’alternatives

Après avoir vérifié le théorème pour 3 alternatives, nous explorons comment la complexité de l’encodage evolue avec la taille du problème. Le nombre de profils possibles croît comme \((m!)^n\), ce qui rend rapidement l’approche SAT prohibitive pour des instances de taille moyenne.

# Etude de complexite : taille de l'encodage
import time

print("COMPLEXITE DE L'ENCODAGE SAT D'ARROW")
print("=" * 60)
print(f"{'Alt':>4} {'Voters':>6} {'Profils':>10} {'Variables':>10} {'Clauses':>10} {'Temps (ms)':>10}")
print("-" * 60)

for n_alt in [2, 3, 4]:
    for n_voters in [2, 3]:
        alternatives = [chr(65 + i) for i in range(n_alt)]
        # budget de construction pour la case la plus large (4 alt, 3 electeurs)
        budget_s = 15 if (n_alt, n_voters) == (4, 3) else None
        t0 = time.time()
        try:
            if budget_s:
                signal.alarm(budget_s)
            enc = ArrowSATEncoder(alternatives, n_voters)
            stats = enc.stats()
            if budget_s:
                signal.alarm(0)
            elapsed = (time.time() - t0) * 1000
            print(f"{n_alt:>4} {n_voters:>6} {stats['profiles']:>10,} {stats['variables']:>10,} {stats['clauses']:>10,} {elapsed:>10.1f}")
        except Timeout:
            signal.alarm(0)
            print(f"{n_alt:>4} {n_voters:>6} {24**n_voters:>10,} {'-':>10} {'-':>10} ABANDON build > {time.time()-t0:.0f} s (budget {budget_s} s)")
        except Exception as e:
            print(f"{n_alt:>4} {n_voters:>6}  ERREUR: {e}")

print()
print("Frontiere mesuree : l'encodage complet tient jusqu'a 4 alternatives x 2 electeurs ;")
print("a 4 alternatives x 3 electeurs, la SEULE construction depasse deja le budget.")

print("\nObservation : avec 2 alternatives, le theoreme ne s'applique PAS (SAT attendu)")
COMPLEXITE DE L'ENCODAGE SAT D'ARROW
============================================================
 Alt Voters    Profils  Variables    Clauses Temps (ms)
------------------------------------------------------------
   2      2          4          8         12        0.0
   2      3          8         16         21        0.1
   3      2         36        216      2,216        2.1
   3      3        216      1,296     36,453       75.8
   4      2        576      6,912  1,010,882     1262.5
   4      3     13,824          -          - ABANDON build > 15 s (budget 15 s)

Frontiere mesuree : l'encodage complet tient jusqu'a 4 alternatives x 2 electeurs ;
a 4 alternatives x 3 electeurs, la SEULE construction depasse deja le budget.

Observation : avec 2 alternatives, le theoreme ne s'applique PAS (SAT attendu)

Interprétation de la complexité

Alternatives Électeurs Profils Variables
2 2 4 8
2 3 8 16
3 2 36 216
3 3 216 1,296
4 2 576 6,912
4 3 13,824 construction inachevée

Le temps d’encodage (machine-dépendant) est mesuré dynamiquement par la cellule de code ci-dessus ; il n’est pas figé ici (règle #9434 : les mesures non reproductibles sont tenues par le code, pas par la prose).

Observation critique : Le nombre de profils croît comme \((m!)^n\) où \(m\) est le nombre d’alternatives et \(n\) le nombre d’électeurs. La frontière mesurée est plus tardive que cette croissance ne le suggère : l’encodage 4 alternatives × 2 électeurs (576 profils, 6 912 variables, 1 010 882 clauses) se construit entièrement — temps mesuré par la cellule ci-dessus — et se résout (partie 3). C’est à 4 alternatives × 3 électeurs (13 824 profils) que la construction elle-même dépasse le budget : c’est la profondeur du profil, pas la largeur en alternatives, qui referme la frontière.

Le cas 2 alternatives est particulier : l’encodage trouve SAT alors que le théorème d’Arrow classique suppose \(|A| \geq 3\). Cela s’explique car avec seulement 2 alternatives, l’espace des profils est assez restreint pour qu’une fonction non dictatoriale existe (la règle majoritaire satisfait Pareto + IIA + non-dictature).

# Cas special : 2 alternatives
print("CAS 2 ALTERNATIVES : ANALYSE DE L'ENCODAGE")
print("=" * 50)

sat_2alt, stats_2alt = verify_arrow_with_sat(['A', 'B'], 2)
result_2alt = 'SAT (fonction non-dictatoriale existe!)' if sat_2alt else 'UNSAT'
print(f"Resultat : {result_2alt}")
print(f"Variables : {stats_2alt['variables']}, Clauses : {stats_2alt['clauses']}")

print("\nAnalyse :")
if not sat_2alt:
    print("  L'encodage SAT trouve UNSAT meme avec 2 alternatives.")
    print("  Ceci s'explique par l'interaction entre Pareto et non-dictature :")
    print("  - Pareto force le classement social quand tous sont d'accord")
    print("  - Avec 2 alt, le domaine est si petit que non-dictature ne peut")
    print("    etre satisfaite en meme temps que les autres contraintes")
    print("  - L'encodage couvre TOUS les profils, pas seulement la majorite")
    print()
    print("  Note : le theoreme d'Arrow classique suppose |A| >= 3.")
    print("  Le resultat UNSAT ici montre que meme la version a 2 alternatives")
    print("  de l'encodage est trop contrainte (Pareto + non-dictature seuls).")
else:
    print("  Une fonction non-dictatoriale existe : la regle majoritaire.")
CAS 2 ALTERNATIVES : ANALYSE DE L'ENCODAGE
==================================================
Resultat : SAT (fonction non-dictatoriale existe!)
Variables : 8, Clauses : 12

Analyse :
  Une fonction non-dictatoriale existe : la regle majoritaire.

Interprétation du cas 2 alternatives

Le résultat est maintenant SAT pour 2 alternatives : une fonction de bien-être social non dictatoriale existe, et la règle majoritaire en est un exemple (elle satisfait Pareto, IIA et non-dictature des que |A| < 3).

C’est precisement ce qui illustre la frontière du théorème d’Arrow : celui-ci suppose \(|A| \geq 3\), et la conclusion d’impossibilité ne tient plus en dessous. Pour 2 alternatives, l’espace des profils est suffisamment restreint pour qu’aucun electeur ne puisse être dictateur au sens d’Arrow – IIA impose que le classement social d’une paire ne depende que des préférences individuelles sur cette paire, et comme il n’existe qu’une seule paire, tout reglage coherent (par exemple la majorite, ou un tie-break constant sur les profils mixtes) Pareto + non-dictature est realisable.

L’encodeur SAT et l’encodeur SMT (Z3, partie 2) donnent desormais le même verdict SAT, ce qui confirme la cohérence de l’encodage. (Note : le théorème d’Arrow reste valide des que \(|A| \geq 3\) – voir la section précédente.)

En complément — vue d’ensemble des verdicts :

Alternatives Électeurs Résultat Explication
2 2+ SAT Une SWF non dictatoriale existe (règle majoritaire)
3 2+ UNSAT Arrow vérifié : impossibilité
4+ 2+ UNSAT Arrow vérifié (explosion combinatoire)

En complément — complexité combinatoire : avec 4 alternatives et 3 électeurs, l’encodage implique \((4!)^3 = 13\,824\) profils et des milliers de clauses.


5. Encodage du Théorème de Sen

Le théorème de Sen (1970) propose une autre impossibilité, basee sur un conflit différent : celui entre la liberté individuelle et l’efficacité collective. L’encodage SAT est similaire a celui d’Arrow, mais remplace IIA et non-dictature par une contrainte de liberté minimale.

class SenSATEncoder:
    """Encode le theoreme de Sen comme un probleme SAT.

    Encode : transitivite + Pareto + liberte minimale.
    UNSAT prouve que ces 3 conditions sont incompatibles.
    """

    def __init__(self, alternatives, n_voters, liberty_pairs=None):
        self.alternatives = alternatives
        self.n_alt = len(alternatives)
        self.n_voters = n_voters

        # Paires de liberte : par defaut, chaque voter decide d'une paire
        if liberty_pairs is None:
            all_pairs = list(combinations(alternatives, 2))
            liberty_pairs = []
            for v in range(n_voters):
                pair = all_pairs[v % len(all_pairs)]
                liberty_pairs.append((v, pair[0], pair[1]))
        self.liberty_pairs = liberty_pairs

        all_orders = list(permutations(alternatives))
        self.profiles = list(product(*([all_orders] * n_voters)))

        self.var_counter = 0
        self.var_map = {}
        self._init_variables()

    def _init_variables(self):
        for pi_idx in range(len(self.profiles)):
            for x, y in permutations(self.alternatives, 2):
                self.var_counter += 1
                self.var_map[(pi_idx, x, y)] = self.var_counter

    def get_var(self, pi_idx, x, y):
        return self.var_map[(pi_idx, x, y)]

    def encode_completeness(self):
        clauses = []
        for pi_idx in range(len(self.profiles)):
            for x, y in combinations(self.alternatives, 2):
                clauses.append([self.get_var(pi_idx, x, y), self.get_var(pi_idx, y, x)])
        return clauses

    def encode_asymmetry(self):
        clauses = []
        for pi_idx in range(len(self.profiles)):
            for x, y in combinations(self.alternatives, 2):
                clauses.append([-self.get_var(pi_idx, x, y), -self.get_var(pi_idx, y, x)])
        return clauses

    def encode_transitivity(self):
        clauses = []
        for pi_idx in range(len(self.profiles)):
            for x, y, z in permutations(self.alternatives, 3):
                clauses.append([-self.get_var(pi_idx, x, y),
                                -self.get_var(pi_idx, y, z),
                                self.get_var(pi_idx, x, z)])
        return clauses

    def encode_pareto(self):
        clauses = []
        for pi_idx, profile in enumerate(self.profiles):
            for x, y in permutations(self.alternatives, 2):
                if all(vp.index(x) < vp.index(y) for vp in profile):
                    clauses.append([self.get_var(pi_idx, x, y)])
        return clauses

    def encode_liberty(self):
        """Liberte minimale : chaque individu decide d'au moins une paire."""
        clauses = []
        for voter, x, y in self.liberty_pairs:
            for pi_idx, profile in enumerate(self.profiles):
                if profile[voter].index(x) < profile[voter].index(y):
                    clauses.append([self.get_var(pi_idx, x, y)])
                else:
                    clauses.append([self.get_var(pi_idx, y, x)])
        return clauses

    def encode_all(self):
        clauses = []
        clauses.extend(self.encode_completeness())
        clauses.extend(self.encode_asymmetry())
        clauses.extend(self.encode_transitivity())
        clauses.extend(self.encode_pareto())
        clauses.extend(self.encode_liberty())
        return clauses


# Encodage de Sen : 3 alternatives, 2 voters
# Liberte : voter 0 decide (A,C), voter 1 decide (B,C)
liberty = [(0, 'A', 'C'), (1, 'B', 'C')]
sen_enc = SenSATEncoder(['A', 'B', 'C'], 2, liberty_pairs=liberty)
sen_clauses = sen_enc.encode_all()

print(f"Encodage Sen (3 alt, 2 voters)")
print(f"  Variables : {sen_enc.var_counter}")
print(f"  Clauses : {len(sen_clauses)}")
print(f"  Paires de liberte : {liberty}")

with Glucose3() as solver:
    solver.append_formula(sen_clauses)
    sat_sen = solver.solve()

result_sen = 'SAT' if sat_sen else 'UNSAT (Sen verifie)'
print(f"\nResultat : {result_sen}")
if not sat_sen:
    print("=> Liberte minimale + Pareto + transitivite = IMPOSSIBLE")
Encodage Sen (3 alt, 2 voters)
  Variables : 216
  Clauses : 558
  Paires de liberte : [(0, 'A', 'C'), (1, 'B', 'C')]

Resultat : UNSAT (Sen verifie)
=> Liberte minimale + Pareto + transitivite = IMPOSSIBLE

Interprétation

L’encodage du théorème de Sen produit moins de clauses (558) que celui d’Arrow (2216) car il remplace IIA et non-dictature par une seule contrainte de liberté minimale.

Le résultat UNSAT confirme le théorème de Sen : on ne peut pas avoir simultanément la liberté individuelle minimale, l’efficacité collective (Pareto) et la cohérence du classement social (transitivité). C’est un paradoxe fondamental : donner des droits individuels même minimaux mène a l’incohérence collective.

Note technique : Le théorème de Sen est “plus fort” qu’Arrow dans le sens ou il ne necessite pas l’axiome IIA, souvent critique comme trop restrictif. Il montre que le conflit entre liberté et cohérence est plus profond que le conflit entre cohérence et non-dictature.

Ce rapport de taille (558 contre 2216 clauses) est structurant : le théorème de Sen est une impossibilité locale (deux paires de liberté quelconques peuvent être incompatibles), il n’a besoin ni d’IIA ni d’une condition globale sur tout l’ordre social. Moins d’axiomes a compiler = moins de clauses, donc des instances plus grandes solubles. C’est un avant-gout du compromis que la partie 2 (z3) generalisera en changeant de formalisme.


6. Benchmark : Comparaison des Solveurs SAT

Pour comprendre les différences de performance entre solveurs, nous comparons systematiquement Glucose3, MiniSat22 et CaDiCaL103 sur les mêmes instances. Bien que les résultats soient identiques (UNSAT), les temps de résolution peuvent varier selon les heuristiques internes de chaque solveur.

import time

def benchmark_solvers(alternatives, n_voters):
    """Compare les solveurs SAT sur l'encodage d'Arrow."""
    enc = ArrowSATEncoder(alternatives, n_voters)
    clauses = enc.encode_all()
    stats = enc.stats()

    results = []
    for name, SolverClass in [('Glucose3', Glucose3), ('MiniSat22', Minisat22), ('CaDiCaL103', Cadical103)]:
        t0 = time.time()
        with SolverClass() as solver:
            solver.append_formula(clauses)
            sat = solver.solve()
        elapsed = (time.time() - t0) * 1000
        results.append((name, sat, elapsed))

    return stats, results


print("BENCHMARK : COMPARAISON DES SOLVEURS")
print("=" * 60)

for n_alt, n_voters in [(3, 2), (3, 3)]:
    alternatives = [chr(65 + i) for i in range(n_alt)]
    stats, results = benchmark_solvers(alternatives, n_voters)

    print(f"\n{stats['alternatives']} alternatives, {n_voters} electeurs")
    print(f"  {stats['profiles']} profils, {stats['variables']} variables, {stats['clauses']} clauses")
    print(f"  {'Solveur':12s} {'Resultat':20s} {'Temps (ms)':>10}")
    print(f"  {'-'*42}")
    for name, sat, elapsed in results:
        result_str = 'UNSAT (Arrow)' if not sat else 'SAT'
        print(f"  {name:12s} {result_str:20s} {elapsed:>10.1f}")
BENCHMARK : COMPARAISON DES SOLVEURS
============================================================

3 alternatives, 2 electeurs
  36 profils, 216 variables, 2216 clauses
  Solveur      Resultat             Temps (ms)
  ------------------------------------------
  Glucose3     UNSAT (Arrow)               1.0
  MiniSat22    UNSAT (Arrow)               0.7
  CaDiCaL103   UNSAT (Arrow)               1.3

3 alternatives, 3 electeurs
  216 profils, 1296 variables, 36453 clauses
  Solveur      Resultat             Temps (ms)
  ------------------------------------------
  Glucose3     UNSAT (Arrow)               9.8
  MiniSat22    UNSAT (Arrow)               8.3
  CaDiCaL103   UNSAT (Arrow)              11.1

Analyse des performances

Les trois solveurs retournent le même verdict UNSAT sur ces instances. Leurs temps de résolution sont mesurés par la cellule de benchmark ci-dessus : cette sortie constitue la source à consulter, car les valeurs et l’ordre relatif des solveurs peuvent varier selon la machine, l’environnement et l’exécution.

Pour des instances plus grandes, les heuristiques de branchement et les stratégies d’apprentissage de clauses peuvent davantage différencier les moteurs. Glucose3 privilégie un apprentissage de clauses agressif, MiniSat22 fournit une référence CDCL minimale et CaDiCaL cible notamment les grandes instances ; le benchmark permet de comparer leur comportement observé sans figer cette comparaison dans la prose.

Le passage de 36 à 216 profils multiplie par six la taille du domaine exploré. Les variables passent parallèlement de 216 à 1 296 et le nombre de clauses de 2 216 à 36 453 : cette croissance structurelle de l’encodage explique l’effort de résolution accru visible dans les mesures précédentes, sans faire de leur rapport de temps une propriété indépendante de la machine.

Comparaison des solveurs

Les solveurs CDCL modernes (Glucose, MiniSat, CaDiCaL) utilisent différents heuristiques de branchement et de gestion des conflits. Sur les petits instances d’Arrow, les différences de performance sont minimes, mais sur de grandes instances, le choix du solveur peut avoir un impact significatif.

Solveur Type Particularite
Glucose3 CDCL Apprentissage de clauses agressif
MiniSat22 CDCL Solveur de référence, minimal
CaDiCaL103 CDCL Optimise pour gros instances

7. Visualisation : Espace des Solutions

Pour completer l’analyse mathematique, nous visualisons graphiquement l’espace des fonctions d’agrégation et l’evolution de la complexité. Ces représentations permettent de mieux apprehender pourquoi les théorèmes d’impossibilité sont structurellement inevitables et comment la complexité explose avec la taille du problème.

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Graphique 1 : Espace des SWF et contraintes d'Arrow
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.set_aspect('equal')

# Cercles representant les contraintes
constraints = [
    (3, 6, 2.5, 'Pareto', 'lightblue'),
    (7, 6, 2.5, 'IIA', 'lightgreen'),
    (5, 2.5, 2.5, 'Non-dictature', 'lightsalmon'),
]

for cx, cy, r, label, color in constraints:
    circle = plt.Circle((cx, cy), r, color=color, alpha=0.3)
    ax.add_patch(circle)
    ax.text(cx, cy, label, ha='center', va='center', fontsize=10, fontweight='bold')

# Zone d'intersection (vide = impossibilite)
ax.text(5, 5, "Intersection\nVIDE", ha='center', va='center',
        fontsize=11, color='red', fontweight='bold',
        bbox=dict(boxstyle='round', facecolor='white', edgecolor='red'))

ax.set_title("Theoreme d'Arrow : Intersection Vide", fontsize=13)
ax.axis('off')

# Graphique 2 : Evolution de la complexite
ax2 = axes[1]
n_alts = [2, 3, 4, 5]
profiles_2v = [2, 36, 576, 14400]  # (m!)^2
profiles_3v = [1, 216, 13824, 1728000]  # (m!)^3

ax2.semilogy(n_alts, profiles_2v, 'bo-', linewidth=2, markersize=8, label='2 voters')
ax2.semilogy(n_alts, profiles_3v, 'rs-', linewidth=2, markersize=8, label='3 voters')
ax2.set_xlabel("Nombre d'alternatives", fontsize=11)
ax2.set_ylabel('Nombre de profils (log)', fontsize=11)
ax2.set_title('Explosion combinatoire des profils', fontsize=13)
ax2.legend()
ax2.grid(True, alpha=0.3)
ax2.set_xticks(n_alts)

# Annotation pour 2 alternatives (cas special)
ax2.annotate("Arrow ne\ns'applique pas", xy=(2, 2), xytext=(2.5, 0.5),
            arrowprops=dict(arrowstyle='->', color='green'),
            fontsize=9, color='green')

plt.tight_layout()
plt.show()

print("Gauche : les 3 conditions d'Arrow n'ont aucune intersection (impossibilite)")
print("Droite : la complexite croit exponentiellement avec le nombre d'alternatives")

Gauche : les 3 conditions d'Arrow n'ont aucune intersection (impossibilite)
Droite : la complexite croit exponentiellement avec le nombre d'alternatives

Interprétation

Gauche : Les trois contraintes d’Arrow (Pareto, IIA, non-dictature) sont representees comme des cercles qui ne se chevauchent pas. L’intersection vide symbolise l’impossibilité : aucune fonction de bien-être social ne peut satisfaire les trois conditions simultanément.

Droite : La complexité de l’encodage croît de facon exponentielle avec le nombre d’alternatives. Pour 5 alternatives avec 3 électeurs, on atteint plus de 1.7 million de profils a encoder, ce qui explique pourquoi les solveurs SAT sont limites a des petites instances pour ce type de problème.

Note technique : Le cas 2 alternatives est special car le théorème d’Arrow ne s’applique pas (|A| >= 3 est requis). L’annotation en vert sur le graphique souligne cette exception.


8. Exercices

Exercice 1 : Explorer l’encodage de base

Objectifs : Comprendre la structure de l’encodage SAT en modifiant les paramètres.

  1. Comptez le nombre de variables et de clauses pour 4 alternatives et 2 électeurs
  2. Identifiez quelles contraintes generent le plus de clauses
  3. Verifiez que le cas 2 alternatives est SAT
# Exercice 1 : Explorer l'encodage de base
# ===========================================

# Question 1 : Encodage pour 4 alternatives, 2 electeurs
alternatives_4 = ['A', 'B', 'C', 'D']

# Exercice: Creez un ArrowSATEncoder avec 4 alternatives et 2 electeurs
# Affichez le nombre de variables, clauses et profils.
# Indice : enc = ArrowSATEncoder(alternatives_4, n_voters=2)

# Question 2 : Decomposition par contrainte
# Exercice: Pour l'encodage 3 alternatives / 2 electeurs, affichez le nombre
# de clauses pour chaque type de contrainte :
# - Completeness, Asymmetry, Transitivity, Pareto, IIA, Non-dictatorship
# Indice : appelez chaque methode encode_*() separement et comptez.

# Question 3 : Verifier que 2 alternatives est SAT
# Exercice: Appelez verify_arrow_with_sat(['A', 'B'], 2) et affichez le resultat.
# Verifiez que c'est bien SAT (le theoreme d'Arrow ne s'applique pas).
print("Exercice a completer")
Exercice a completer

Exercice 2 : Théorème de Sen - Variation des paires de liberté

Objectifs : Tester si le théorème de Sen s’applique selon le choix des paires de liberté.

  1. Testez l’encodage de Sen avec des paires de liberté différentes
  2. Trouvez une configuration ou le résultat est SAT (le théorème ne s’applique pas)
  3. Expliquez pourquoi
# Exercice 2 : Theoreme de Sen - Variation des paires de liberte
# ===============================================================

# Question 1 : Tester differentes paires de liberte
liberty_configs = [
    [(0, 'A', 'B'), (1, 'A', 'C')],
    [(0, 'A', 'B'), (1, 'B', 'C')],
    [(0, 'A', 'C'), (1, 'B', 'C')],
]

# Exercice: Pour chaque configuration, creez un SenSATEncoder et resolvez.
# Affichez si le resultat est SAT ou UNSAT.
# Indice : SenSATEncoder(['A', 'B', 'C'], 2, liberty_pairs=config)

# Question 2 : Trouver un cas SAT
# Exercice: Explorez des configurations de paires qui pourraient donner SAT.
# Indice : que se passe-t-il si les deux voters ont la meme paire de liberte ?

# Question 3 : Explication
# Exercice: Expliquez en commentaire pourquoi certaines configurations sont SAT.
print("Exercice a completer")
Exercice a completer

Exercice 3 : Ajouter une nouvelle contrainte

Objectifs : Etendre l’encodage pour explorer les consequences d’ajouts de contraintes.

  1. Ajoutez la contrainte de monotonie : si un candidat monte dans les préférences d’un electeur, son classement social ne peut pas baisser
  2. Testez si Pareto + monotonie + non-dictature est satisfaisable
# Exercice 3 : Ajouter une nouvelle contrainte
# =============================================

# La monotonie dit : si dans le profil pi', le candidat x monte par rapport a pi
# (tous les autres classements individuels restant identiques), alors le classement
# social de x ne peut pas baisser.

# Question 1 : Implementer encode_monotonicity()
def encode_monotonicity(encoder):
    """Encode la contrainte de monotonie pour un ArrowSATEncoder.

    Pour chaque paire de profils (pi, pi') ou pi' est obtenu en montant x
    dans les preferences d'un seul electeur :
    si pi.social(x) > pi.social(y), alors pi'.social(x) >= pi'.social(y).

    Args:
        encoder: un ArrowSATEncoder deja initialise

    Returns:
        list de clauses CNF
    """
    # Exercice: Implementez ici
    # Indice : identifiez les paires de profils ou un seul electeur a change,
    # et ou le changement fait monter un candidat x.
    # Ajoutez la clause : si R_pi(x,y) alors R_pi'(x,y).
    return []  # TODO etudiant : remplacer par l'implementation


# Question 2 : Tester Pareto + monotonie + non-dictature (sans IIA)
# Exercice: Creez un encodage Arrow sans IIA, avec monotonie.
# Indice : utilisez les methodes encode_completeness/asymmetry/transitivity,
# encode_pareto, encode_non_dictatorship et votre encode_monotonicity.
# Le resultat est-il SAT ou UNSAT ?
print("Exercice a completer")
Exercice a completer

Partie 2 : Vérification SMT avec Z3

Après l’approche SAT (Partie 1), qui encodait chaque decision locale en litteral booléen et chaque axiome en paquet de clauses, la partie 2 recommence avec un changement de formalisme : des variables entières representant les rangs des alternatives dans l’ordre social, et des comparaisons arithmetiques a la place des clauses. Le théorème est le même, la question pedagogique change — que gagne-t-on et que perd-on a laisser le solveur raisonner sur des nombres plutôt que sur des bits ? La comparaison finale du notebook y repondra ligne par ligne.


1. Configuration et imports

# Configuration et imports
from z3 import (
    Solver, Bool, Int, Implies, And, Or, Not, If,
    sat, unsat, Function, BoolSort, IntSort
)
from itertools import permutations, combinations, product
import time

print("z3 SMT solver installe avec succes")
print("Solveur : z3 (Microsoft Research)")
z3 SMT solver installe avec succes
Solveur : z3 (Microsoft Research)

2. Encodage Arrow comme problème SMT

L’idee centrale : pour chaque profil de préférences, la fonction de bien-être social (SWF) produit un ordre social. Nous encodons cet ordre avec des variables entières representant le rang de chaque alternative.

Variables

  • rank[pi][x] : rang de l’alternative x dans l’ordre social pour le profil pi (0 = meilleur, n_alt-1 = pire)
  • Chaque profil pi est un tuple de préférences individuelles

Contraintes

  1. Totalite + antisymetrie : les rangs forment un ordre total (permutation de 0..n-1)
  2. Pareto : si tous preferent x a y, rank(x) < rank(y)
  3. IIA : si les préférences relatives entre x,y sont identiques dans deux profils, le classement social entre x,y est identique
  4. Non-dictature : pour chaque electeur, il existe un profil ou son choix n’est pas suivi
class ArrowZ3Encoder:
    """Encode le theoreme d'Arrow comme un probleme SMT (z3).

    Utilise des variables entieres pour les rangs sociaux au lieu
    de variables booleennes comme l'approche SAT.
    """

    def __init__(self, alternatives, n_voters):
        self.alternatives = alternatives
        self.n_alt = len(alternatives)
        self.n_voters = n_voters

        # Generer tous les profils possibles
        all_orders = list(permutations(alternatives))
        self.profiles = list(product(*([all_orders] * n_voters)))
        self.n_profiles = len(self.profiles)

        # Variables z3 : rank[pi_idx][alt] = Int
        self.ranks = {}
        for pi in range(self.n_profiles):
            for alt in alternatives:
                self.ranks[(pi, alt)] = Int(f'r_{pi}_{alt}')

        self.solver = Solver()
        self._add_order_constraints()

    def _add_order_constraints(self):
        """Contraintes de base : les rangs forment un ordre total."""
        for pi in range(self.n_profiles):
            # Rangs entre 0 et n_alt - 1
            for alt in self.alternatives:
                self.solver.add(self.ranks[(pi, alt)] >= 0)
                self.solver.add(self.ranks[(pi, alt)] < self.n_alt)

            # Antisymetrie : deux alternatives distinctes ont des rangs distincts
            for x, y in combinations(self.alternatives, 2):
                rx = self.ranks[(pi, x)]
                ry = self.ranks[(pi, y)]
                self.solver.add(rx != ry)

    def order_rank(self, pi, x, y):
        """Retourne la contrainte : x est prefere a y (rang(x) < rang(y))."""
        return self.ranks[(pi, x)] < self.ranks[(pi, y)]

    def indiv_prefers(self, profile, voter, x, y):
        """Verifie si le voter prefere x a y dans le profil."""
        return profile[voter].index(x) < profile[voter].index(y)

    def add_weak_pareto(self):
        """Si tous preferent x a y, le classement social aussi."""
        for pi_idx, profile in enumerate(self.profiles):
            for x, y in permutations(self.alternatives, 2):
                if all(self.indiv_prefers(profile, v, x, y) for v in range(self.n_voters)):
                    self.solver.add(self.order_rank(pi_idx, x, y))

    def add_iia(self):
        """IIA : memes prefs relatives sur {x,y} => meme classement social."""
        for pi1 in range(self.n_profiles):
            for pi2 in range(pi1 + 1, self.n_profiles):
                prof1 = self.profiles[pi1]
                prof2 = self.profiles[pi2]
                for x, y in permutations(self.alternatives, 2):
                    same_xy = all(
                        (self.indiv_prefers(prof1, v, x, y) == self.indiv_prefers(prof2, v, x, y))
                        for v in range(self.n_voters)
                    )
                    if same_xy:
                        self.solver.add(self.order_rank(pi1, x, y) == self.order_rank(pi2, x, y))

    def add_no_dictator(self):
        """Pour chaque electeur, il existe un profil ou son choix n'est pas suivi."""
        for voter in range(self.n_voters):
            # Au moins une paire (x,y) et un profil ou le voter n'est pas suivi
            violations = []
            for pi_idx, profile in enumerate(self.profiles):
                for x, y in permutations(self.alternatives, 2):
                    if self.indiv_prefers(profile, voter, x, y):
                        # Le social ne suit PAS le voter
                        violations.append(Not(self.order_rank(pi_idx, x, y)))
            if violations:
                self.solver.add(Or(*violations))

    def check(self):
        """Resout le systeme. Retourne sat/unsat."""
        return self.solver.check()

    def count_constraints(self):
        """Retourne le nombre de contraintes dans le solver."""
        return self.solver.num_scopes()


# Test : creation de l'encodeur
enc = ArrowZ3Encoder(['A', 'B', 'C'], n_voters=2)
print(f"Encodeur z3 cree : {enc.n_alt} alternatives, {enc.n_voters} electeurs")
print(f"Profils : {enc.n_profiles}")
print(f"Variables entieres : {len(enc.ranks)}")
Encodeur z3 cree : 3 alternatives, 2 electeurs
Profils : 36
Variables entieres : 108

Interprétation de l’encodage

L’encodeur z3 utilise des variables entières pour les rangs au lieu de variables booléennes. Chaque rang r_pi_x represente la position de l’alternative x dans l’ordre social pour le profil pi. Les contraintes d’ordre total (rangs distincts, entre 0 et n-1) sont plus naturelles en SMT qu’en SAT.

Compacite. Pour la même configuration (3 alternatives, 2 électeurs), l’encodage z3 ne declare que 108 variables entières contre 216 booléennes côté SAT. La difference vient du codage du rang : une variable entiere par couple (profil, paire d’alternatives) suffit, alors que le codage booléen doit materialiser deux literaux par decision. La transitivity — une chaine de clauses côté CNF — devient une simple comparaison arithmetique sur les rangs.


3. Vérification UNSAT : Arrow prouve par z3

Nous ajoutons les trois axiomes d’Arrow et demandons a z3 si une SWF les satisfaisant existe.

# Verification complete du theoreme d'Arrow avec z3
def verify_arrow_z3(alternatives, n_voters):
    """Verifie le theoreme d'Arrow avec z3.

    Retourne (resultat_z3, stats).
    Si UNSAT, le theoreme est verifie.
    """
    t0 = time.time()
    enc = ArrowZ3Encoder(alternatives, n_voters)
    enc.add_weak_pareto()
    enc.add_iia()
    enc.add_no_dictator()
    result = enc.check()
    elapsed = (time.time() - t0) * 1000

    stats = {
        'alternatives': len(alternatives),
        'voters': n_voters,
        'profiles': enc.n_profiles,
        'variables': len(enc.ranks),
        'time_ms': elapsed,
    }
    return result, stats


# Verification pour 3 alternatives, 2 electeurs
print("VERIFICATION DU THEOREME D'ARROW PAR z3")
print("=" * 50)

result_3, stats_3 = verify_arrow_z3(['A', 'B', 'C'], 2)
print(f"\nConfiguration : {stats_3['alternatives']} alternatives, {stats_3['voters']} electeurs")
print(f"Profils : {stats_3['profiles']}")
print(f"Variables entieres : {stats_3['variables']}")
print(f"Resultat : {result_3}")
print(f"Temps : {stats_3['time_ms']:.1f} ms")

if result_3 == unsat:
    print("\n=> UNSAT : aucune SWF ne satisfait Pareto + IIA + non-dictature")
    print("=> Le theoreme d'Arrow est verifie par z3")
else:
    print("\n=> SAT : une SWF existe (le theoreme ne s'applique pas)")
VERIFICATION DU THEOREME D'ARROW PAR z3
==================================================

Configuration : 3 alternatives, 2 electeurs
Profils : 36
Variables entieres : 108
Resultat : unsat
Temps : 68.4 ms

=> UNSAT : aucune SWF ne satisfait Pareto + IIA + non-dictature
=> Le theoreme d'Arrow est verifie par z3

Interprétation

z3 confirme UNSAT pour 3 alternatives et 2 électeurs : il n’existe aucune fonction de bien-être social satisfaisant simultanément Pareto, IIA et non-dictature.

Compare a l’approche SAT (Partie 1 de ce notebook), l’encodage SMT est plus compact car les contraintes d’ordre sont exprimees naturellement avec des comparaisons d’entiers plutôt que des clauses CNF.

Même verdict, autre moteur. z3 conclut unsat sur les 36 profils avec 108 variables entières : une SWF democratique au sens d’Arrow n’existe pas davantage en logique du premier ordre avec arithmetique qu’en logique propositionnelle. Deux technologies radicalement différentes (CDCL sur clauses booléennes contre solveur SMT a theories) qui convergent sur le même résultat : c’est la forme la plus forte de corroboration mécanique accessible sans preuve formelle.


4. Vérification SAT : 2 alternatives

Le théorème d’Arrow suppose \(|A| \geq 3\). Verifions que pour 2 alternatives, le résultat est bien SAT (la règle majoritaire fonctionne).

# Cas 2 alternatives
print("CAS 2 ALTERNATIVES : ANALYSE")
print("=" * 40)

result_2, stats_2 = verify_arrow_z3(['A', 'B'], 2)
print(f"Configuration : {stats_2['alternatives']} alternatives, {stats_2['voters']} electeurs")
print(f"Resultat : {result_2}")
print(f"Temps : {stats_2['time_ms']:.1f} ms")

if result_2 == unsat:
    print("\nUNSAT : meme avec 2 alternatives, les axiomes sont incompatibles.")
    print("L'encodage SMT est plus restrictif que le theoreme classique d'Arrow.")
    print("(cf. la partie SAT ci-dessus pour une discussion detaillee de ce phenomene)")
else:
    print("\nSAT : une SWF non-dictatoriale existe avec 2 alternatives.")
    print("La regle majoritaire satisfait Pareto + IIA + non-dictature.")
CAS 2 ALTERNATIVES : ANALYSE
========================================
Configuration : 2 alternatives, 2 electeurs
Resultat : sat
Temps : 7.7 ms

SAT : une SWF non-dictatoriale existe avec 2 alternatives.
La regle majoritaire satisfait Pareto + IIA + non-dictature.

Interprétation

Avec 2 alternatives, le résultat est SAT : une SWF non dictatoriale existe (la règle majoritaire satisfait Pareto + IIA + non-dictature). L’encodeur SMT confirme ici le même verdict que l’encodeur SAT (partie 1) : pour |A| < 3, le théorème d’Arrow ne s’applique pas et aucune impossibilité n’emerge. L’impossibilité reapparait des que \(|A| \geq 3\) (voir sections précédentes).

L’instance est structurellement petite : son encodage SAT comporte 8 variables et 12 clauses. La durée SMT observée est donnée par la sortie immédiatement précédente plutôt que recopiée ici. Sur le plan algorithmique, une instance satisfiable peut être résolue dès qu’un modèle est trouvé, tandis qu’établir UNSAT demande de démontrer l’absence de modèle ; cette distinction aide à interpréter les écarts observés sans promettre un rapport de temps stable.


5. Relaxation des axiomes

Le théorème d’Arrow dit que les 3 axiomes sont incompatibles. Que se passe-t-il si on en relaxe un ? Nous devrions obtenir SAT dans chaque cas, confirmant que chaque axiome est individuellement compatible avec les deux autres.

# Relaxation des axiomes d'Arrow
print("RELAXATION DES AXIOMES D'ARROW")
print("=" * 50)
print(f"Configuration : 3 alternatives, 2 electeurs")
print()

alt_3 = ['A', 'B', 'C']

# Cas 1 : Pareto + IIA seulement (sans non-dictature)
enc1 = ArrowZ3Encoder(alt_3, 2)
enc1.add_weak_pareto()
enc1.add_iia()
r1 = enc1.check()
print(f"  Pareto + IIA (sans non-dictature) : {r1}")
if r1 == sat:
    print(f"    => SAT : une dictature (ou equivalent) satisfait ces 2 axiomes")

# Cas 2 : Pareto + non-dictature (sans IIA)
enc2 = ArrowZ3Encoder(alt_3, 2)
enc2.add_weak_pareto()
enc2.add_no_dictator()
r2 = enc2.check()
print(f"\n  Pareto + non-dictature (sans IIA) : {r2}")
if r2 == sat:
    print(f"    => SAT : la regle de Borda par exemple")

# Cas 3 : IIA + non-dictature (sans Pareto)
enc3 = ArrowZ3Encoder(alt_3, 2)
enc3.add_iia()
enc3.add_no_dictator()
r3 = enc3.check()
print(f"\n  IIA + non-dictature (sans Pareto) : {r3}")
if r3 == sat:
    print(f"    => SAT : une SWF triviale (classement constant) peut fonctionner")

print("\n" + "-" * 50)
print("Conclusion : chaque paire d'axiomes est REALISABLE.")
print("L'impossibilite n'apparait que lorsque les 3 sont reunis.")
RELAXATION DES AXIOMES D'ARROW
==================================================
Configuration : 3 alternatives, 2 electeurs

  Pareto + IIA (sans non-dictature) : sat
    => SAT : une dictature (ou equivalent) satisfait ces 2 axiomes

  Pareto + non-dictature (sans IIA) : sat
    => SAT : la regle de Borda par exemple

  IIA + non-dictature (sans Pareto) : sat
    => SAT : une SWF triviale (classement constant) peut fonctionner

--------------------------------------------------
Conclusion : chaque paire d'axiomes est REALISABLE.
L'impossibilite n'apparait que lorsque les 3 sont reunis.

Tableau recapitulatif

Axiomes Résultat SWF exemple
Pareto + IIA + non-dictature UNSAT Aucune (Arrow)
Pareto + IIA SAT Dictature
Pareto + non-dictature SAT Borda, Copeland
IIA + non-dictature SAT Classement constant

Ce tableau confirme que chaque axiome est essentiel a l’impossibilité.

La lecture la plus forte de ce tableau : le théorème d’Arrow est minimal. Retirer n’importe quel axiome rend l’ensemble realisable (chaque paire est SAT, avec un temoin explicite : une dictature, la règle de Borda, un classement constant). L’ensemble {Pareto, IIA, non-dictature} est donc le premier ensemble contradictoire — aucun axiome n’est superflu. Ce type de résultat de minimality, obtenu par relaxation systematique, est un apport propre de l’approche mécanique : la preuve classique d’Arrow ne le montre pas.


6. Passage a l’echelle : benchmarks

# Benchmarks : passage a l'echelle
print("BENCHMARKS : TAILLE ET TEMPS DE RESOLUTION")
print("=" * 60)
print(f"{'Alt':>4} {'Voters':>6} {'Profils':>10} {'Variables':>10} {'Resultat':>10} {'Temps (ms)':>10}")
print("-" * 60)

for n_alt in [2, 3, 4]:
    for n_voters in [2, 3]:
        alternatives = [chr(65 + i) for i in range(n_alt)]
        budget_s = 60 if (n_alt, n_voters) == (4, 2) else (30 if (n_alt, n_voters) == (4, 3) else None)
        t0 = time.time()
        try:
            if budget_s:
                signal.alarm(budget_s)
            result, stats = verify_arrow_z3(alternatives, n_voters)
            if budget_s:
                signal.alarm(0)
            elapsed = (time.time() - t0) * 1000
            res_str = 'UNSAT' if result == unsat else 'SAT'
            print(f"{n_alt:>4} {n_voters:>6} {stats['profiles']:>10,} {stats['variables']:>10} {res_str:>10} {elapsed:>10.1f}")
        except Timeout:
            signal.alarm(0)
            print(f"{n_alt:>4} {n_voters:>6} {24**n_voters:>10,} {'-':>10} {'ABANDON':>10} budget {budget_s} s epuise")
        except ArgumentError:
            # alarme tombee pendant un appel C (z3/ctypes) : le Timeout du handler
            # est re-emballe en ArgumentError par ctypes -- meme signification
            signal.alarm(0)
            if not budget_s:
                raise
            print(f"{n_alt:>4} {n_voters:>6} {24**n_voters:>10,} {'-':>10} {'ABANDON':>10} budget {budget_s} s epuise")

print()
print("Frontiere mesuree (z3) : UNSAT etabli jusqu'a 4 alternatives x 2 electeurs ;")
print("4 alternatives x 3 electeurs (13 824 profils) depasse le budget de resolution.")
BENCHMARKS : TAILLE ET TEMPS DE RESOLUTION
============================================================
 Alt Voters    Profils  Variables   Resultat Temps (ms)
------------------------------------------------------------
   2      2          4          8        SAT        4.6
   2      3          8         16        SAT        5.5
   3      2         36        108      UNSAT       54.2
   3      3        216        648      UNSAT      801.4
   4      2        576       2304      UNSAT    20295.7
   4      3     13,824          -    ABANDON budget 30 s epuise

Frontiere mesuree (z3) : UNSAT etabli jusqu'a 4 alternatives x 2 electeurs ;
4 alternatives x 3 electeurs (13 824 profils) depasse le budget de resolution.

Lecture des résultats de passage a l’echelle

Dans l’exécution commitée, z3 établit UNSAT jusqu’à 4 alternatives pour 2 électeurs (576 profils, 2 304 variables entières), un cas que la version précédente de ce notebook déclarait hors de portée sans l’avoir tenté. Les durées restent consultables dans le benchmark immédiatement précédent et ne sont pas recopiées dans cette interprétation. Trois lectures s’en dégagent :

  • la pente : de 3×2 à 3×3, le nombre de profils est multiplié par six et l’encodage passe de 108 à 648 variables ; l’augmentation du coût observée doit donc se lire avec la croissance de l’encodage, pas avec le seul décompte des profils ;
  • la frontière : ce n’est pas la largeur seule mais la profondeur du profil qui referme l’approche mécanique — pour 4 alternatives × 3 électeurs, soit 13 824 profils, le budget de résolution configuré est épuisé dans la sortie ci-dessus ;
  • SAT contre SMT : sur le même cas 4×2, les deux encodages établissent UNSAT, mais leurs représentations diffèrent fortement — clauses booléennes spécialisées côté PySAT, rangs entiers plus déclaratifs côté z3. Les mesures des cellules correspondantes permettent de comparer leur coût observé sans transformer ce rapport en propriété stable des moteurs.

7. Comparaison SAT vs SMT

Comparons les deux approches pour l’encodage du théorème d’Arrow.

# Comparaison SAT (partie 1) vs SMT/Z3 (partie 2) de ce notebook
print("COMPARAISON SAT vs SMT POUR ARROW")
print("=" * 60)
print()
print(f"{'Critere':<30} {'SAT (PySAT)':<20} {'SMT (z3)':<20}")
print("-" * 70)
print(f"{'Type de variables':<30} {'Booleennes':<20} {'Entieres':<20}")
print(f"{'Encodage ordre total':<30} {'Clauses CNF':<20} {'Comparaisons <':<20}")
print(f"{'Variables (3 alt, 2 voters)':<30} {'216':<20} {'108':<20}")
print(f"{'Contraintes (3 alt, 2 voters)':<30} {'2216 clauses':<20} {'~800 assert.':<20}")
print(f"{'Resultat 3 alt':<30} {'UNSAT':<20} {'UNSAT':<20}")
print(f"{'Lisibilite encodage':<30} {'Bas niveau':<20} {'Declaratif':<20}")
print(f"{'Extensibilite':<30} {'Moderee':<20} {'Bonne':<20}")
print()
print("Points cles :")
print("  - SMT (z3) utilise 2x moins de variables (entiers vs booleens par paire)")
print("  - L'encodage SMT est plus lisible (comparaisons d'entiers vs clauses CNF)")
print("  - Les deux approches confirment UNSAT pour 3+ alternatives")
print("  - SMT facilite l'ajout de nouvelles contraintes (theories riches)")
COMPARAISON SAT vs SMT POUR ARROW
============================================================

Critere                        SAT (PySAT)          SMT (z3)            
----------------------------------------------------------------------
Type de variables              Booleennes           Entieres            
Encodage ordre total           Clauses CNF          Comparaisons <      
Variables (3 alt, 2 voters)    216                  108                 
Contraintes (3 alt, 2 voters)  2216 clauses         ~800 assert.        
Resultat 3 alt                 UNSAT                UNSAT               
Lisibilite encodage            Bas niveau           Declaratif          
Extensibilite                  Moderee              Bonne               

Points cles :
  - SMT (z3) utilise 2x moins de variables (entiers vs booleens par paire)
  - L'encodage SMT est plus lisible (comparaisons d'entiers vs clauses CNF)
  - Les deux approches confirment UNSAT pour 3+ alternatives
  - SMT facilite l'ajout de nouvelles contraintes (theories riches)

Avantages respectifs

Approche Avantage principal Quand l’utiliser
SAT Solveurs rapides, bien compris Problemes purement booléens, benchmarks
SMT Expressivite, lisibilite Problemes avec entiers, ordres, quantificateurs

En pratique, l’approche SMT est souvent preferable pour les théorèmes du choix social car elle mappe directement les concepts mathematiques (rang, préférence) en contraintes.

Lecture du compromis. L’encodage SAT parle le langage natif du solveur (clauses, pas de théorie) mais explose en taille dès que le domaine croît ; l’encodage SMT laisse z3 raisonner sur des comparaisons d’entiers, ce qui compresse l’encodage (108 variables contre 216) au prix d’un moteur plus générique, donc plus lent par clause. Pour ce notebook, les deux voies sont viables jusqu’à 4 alternatives pour 2 électeurs ; à 4 alternatives × 3 électeurs (13 824 profils), aucun des deux n’échappe au mur combinatoire — c’est là que la preuve formelle généralisée (Lean, dernière cellule) prend le relais.


8. Exercices (suite Z3)

Exercice 4 : Explorer l’encodeur z3

Objectifs : Comprendre la structure interne de l’encodeur SMT.

  1. Affichez le nombre de variables et de contraintes pour 3 alternatives et 3 électeurs
  2. Identifiez quel axiome ajoute le plus de contraintes
  3. Comparez avec l’encodage SAT du Partie 1 de ce notebook
# Exercice 4 : Explorer l'encodeur z3
# =====================================

# Question 1 : Encodage pour 3 alternatives, 3 electeurs
# Exercice: Creez un ArrowZ3Encoder avec 3 alternatives et 3 electeurs
# Ajoutez chaque axiome un par un et comptez les assertions dans le solver.
# Indice : enc = ArrowZ3Encoder(['A', 'B', 'C'], n_voters=3)

# Question 2 : Decomposition par axiome
# Exercice: Pour chaque axiome (Pareto, IIA, non-dictature), creez un encodeur
# frais, ajoutez seulement cet axiome, et comptez les assertions.
# Indice : utilisez un nouveau ArrowZ3Encoder pour chaque test.

# Question 3 : Comparaison SAT
# Exercice: Comparez le nombre de variables avec l'encodage SAT de la partie 1 de ce notebook.
# L'approche SMT est-elle plus compacte ?
print("Exercice a completer")
Exercice a completer

Exercice 5 : Théorème de Sen en SMT

Objectifs : Encoder le théorème de Sen avec z3.

  1. Créez une classe SenZ3Encoder similaire a ArrowZ3Encoder
  2. Ajoutez les axiomes : transitivité + Pareto + liberté minimale
  3. Verifiez que le résultat est UNSAT
# Exercice 5 : Theoreme de Sen en SMT
# ===================================

# Question 1 : Implementer SenZ3Encoder
# Exercice: Creez une classe SenZ3Encoder qui encode le theoreme de Sen.
# La difference avec Arrow : pas d'IIA ni non-dictature,
# mais un axiome de "liberte minimale" (chaque individu decide d'une paire).
# Indice : inspirez-vous de ArrowZ3Encoder, ajoutez encode_liberty().

# Question 2 : Verifier UNSAT
# Exercice: Creez un SenZ3Encoder avec 3 alternatives et 2 electeurs.
# Paires de liberte : voter 0 decide (A,C), voter 1 decide (B,C).
# Verifiez que le resultat est UNSAT.

# Question 3 : Trouver un cas SAT
# Exercice: Explorez differentes configurations de paires de liberte.
# Le resultat est-il toujours UNSAT ?
print("Exercice a completer")
Exercice a completer

Resume et perspectives

Ce notebook a demontre comment les solveurs SAT et SMT permettent de verifier mecaniquement les théorèmes d’impossibilité du choix social. En encodant les axiomes d’Arrow (Pareto, IIA, non-dictature) et de Sen (liberté minimale, Pareto, transitivité) sous forme de clauses CNF ou de contraintes entières, nous avons obtenu des résultats UNSAT qui constituent des preuves formelles de ces theorems. L’approche SAT (PySAT) avec ses variables booléennes et ses milliers de clauses offre un encodage exhaustif mais verbeux, tandis que l’approche SMT (Z3) avec ses variables entières representant les rangs sociaux produit un encodage plus compact et plus lisible, capable en prime de trouver des modèles SAT quand le théorème ne s’applique pas (cas a 2 alternatives).

L’analyse de la relaxation des axiomes a confirme que chaque condition est individuellement necessaire a l’impossibilité : retirer n’importe lequel des trois axiomes d’Arrow permet de retrouver une SWF satisfaisante (dictature, Borda, ou classement constant). Les benchmarks de performance ont également mis en evidence l’explosion combinatoire du nombre de profils, qui croît comme \((m!)^n\), rendant l’approche praticable uniquement pour de petites instances malgre l’utilisation de solveurs CDCL modernes comme Glucose3 et CaDiCaL.

Les techniques presentees ici ouvrent la voie a des applications plus larges de la vérification formelle en théorie du choix social, notamment l’analyse de théorèmes de Gibbard-Satterthwaite (manipulation stratégique) ou de Muller-Satterthwaite (monotonie), ainsi que l’exploration de conditions relachees ou de variations axiomatiques grace a la flexibilite de l’encodage SMT.

References academiques

  • Arrow, K.J. (1951). Social Choice and Individual Values. Cowles Commission Monograph 12, Wiley.
  • Sen, A.K. (1970). The Impossibility of a Paretian Liberal. Journal of Political Economy 78(1):152-157.
  • Muller, E. & Satterthwaite, M.A. (1977). The Equivalence of Strong Positive Association and Strategy-proofness. Journal of Economic Theory 14(2):412-418.
  • Tseitin, G.S. (1968). On the Complexity of Derivations in the Propositional Calculus. In: Slisenko (ed.), Structures in Constructive Mathematics and Mathematical Logic, Part II, pp. 115-125. Nauka, Leningrad.
  • de Moura, L. & Bjorner, N. (2008). Z3: An Efficient SMT Solver. In: TACAS 2008, LNCS 4963:337-340.
  • Ignatiev, A., Morgado, A. & Marques-Silva, J. (2018). PySAT: A Python Toolkit for Prototyping with SAT Oracles. In: SAT 2018, LNCS 10929:428-437.

Ce que l’approche mécanique a apporte ici. Le notebook a transforme deux théorèmes d’impossibilité (Arrow, Sen) en instances finies resolues par des moteurs independants : verdicts convergents, relaxation systematique montrant la minimality de l’ensemble d’axiomes, et frontière exacte du cas 2 alternatives. La limite est le mur combinatoire — 4 alternatives deja hors de portée de l’enumeration complete — et c’est precisement cette limite qui justifie la formalisation Lean generale referencee ci-dessous.

Lien avec la formalisation Lean : Les mêmes théorèmes d’impossibilité (Arrow, Sen) vérifiés par SAT/SMT dans ce notebook sont prouves constructivement dans la serie Lean du choix social (game_theory_lean/). L’encodage SAT traduit les axiomes en clauses CNF booléennes ; l’encodage SMT utilise des variables entières pour les rangs. La formalisation Lean, elle, définit les mêmes concepts (Framework.lean : Profile, SWF, weak_pareto, ind_of_irr_alts, is_dictatorship) et prouve les théorèmes pour tous les paramètres, pas seulement les instances finies : Arrow.lean (0 sorry) et Sen.lean (0 sorry). Les trois approches (SAT, SMT, Lean) sont complementaires : SAT fournit une vérification exhaustive sur petites instances, SMT un encodage compact avec modèles, Lean une preuve universelle certifiee.


Navigation : << 03-Voting-Methods.ipynb | Index

Retour au sommet