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']}")