pref_voting 1.16.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pref_voting/__init__.py +1 -0
- pref_voting/analysis.py +496 -0
- pref_voting/axiom.py +38 -0
- pref_voting/axiom_helpers.py +129 -0
- pref_voting/axioms.py +10 -0
- pref_voting/c1_methods.py +963 -0
- pref_voting/combined_methods.py +514 -0
- pref_voting/create_methods.py +128 -0
- pref_voting/data/examples/condorcet_winner/minimal_Anti-Plurality.soc +16 -0
- pref_voting/data/examples/condorcet_winner/minimal_Borda.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Bracket_Voting.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Bucklin.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_Coombs.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Coombs_PUT.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Coombs_TB.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_Dowdall.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_TB.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Iterated_Removal_Condorcet_Loser.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Pareto.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Plurality.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_PluralityWRunoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Positive-Negative_Voting.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_Simplified_Bucklin.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_Superior_Voting.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_Weighted_Bucklin.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Anti-Plurality.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Borda.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Bracket_Voting.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Bucklin.soc +19 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs.soc +21 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_PUT.soc +21 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_TB.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Dowdall.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_TB.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Plurality.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_PluralityWRunoff_PUT.soc +18 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Positive-Negative_Voting.soc +17 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Simplified_Bucklin.soc +20 -0
- pref_voting/data/examples/condorcet_winner/minimal_resolute_Weighted_Bucklin.soc +19 -0
- pref_voting/data/voting_methods_properties.json +414 -0
- pref_voting/data/voting_methods_properties.json.lock +0 -0
- pref_voting/dominance_axioms.py +387 -0
- pref_voting/generate_profiles.py +801 -0
- pref_voting/generate_spatial_profiles.py +198 -0
- pref_voting/generate_utility_profiles.py +160 -0
- pref_voting/generate_weighted_majority_graphs.py +506 -0
- pref_voting/grade_methods.py +184 -0
- pref_voting/grade_profiles.py +357 -0
- pref_voting/helper.py +370 -0
- pref_voting/invariance_axioms.py +671 -0
- pref_voting/io/__init__.py +0 -0
- pref_voting/io/readers.py +432 -0
- pref_voting/io/writers.py +256 -0
- pref_voting/iterative_methods.py +2425 -0
- pref_voting/maj_graph_ex1.png +0 -0
- pref_voting/mappings.py +577 -0
- pref_voting/margin_based_methods.py +2345 -0
- pref_voting/monotonicity_axioms.py +872 -0
- pref_voting/num_evaluation_method.py +77 -0
- pref_voting/other_axioms.py +161 -0
- pref_voting/other_methods.py +939 -0
- pref_voting/pairwise_profiles.py +547 -0
- pref_voting/prob_voting_method.py +105 -0
- pref_voting/probabilistic_methods.py +287 -0
- pref_voting/profiles.py +856 -0
- pref_voting/profiles_with_ties.py +1069 -0
- pref_voting/rankings.py +466 -0
- pref_voting/scoring_methods.py +481 -0
- pref_voting/social_welfare_function.py +59 -0
- pref_voting/social_welfare_functions.py +7 -0
- pref_voting/spatial_profiles.py +448 -0
- pref_voting/stochastic_methods.py +99 -0
- pref_voting/strategic_axioms.py +1394 -0
- pref_voting/swf_axioms.py +173 -0
- pref_voting/utility_functions.py +102 -0
- pref_voting/utility_methods.py +178 -0
- pref_voting/utility_profiles.py +333 -0
- pref_voting/variable_candidate_axioms.py +640 -0
- pref_voting/variable_voter_axioms.py +3747 -0
- pref_voting/voting_method.py +355 -0
- pref_voting/voting_method_properties.py +92 -0
- pref_voting/voting_methods.py +8 -0
- pref_voting/voting_methods_registry.py +136 -0
- pref_voting/weighted_majority_graphs.py +1539 -0
- pref_voting-1.16.31.dist-info/METADATA +208 -0
- pref_voting-1.16.31.dist-info/RECORD +92 -0
- pref_voting-1.16.31.dist-info/WHEEL +4 -0
- pref_voting-1.16.31.dist-info/licenses/LICENSE.txt +21 -0
@@ -0,0 +1,355 @@
|
|
1
|
+
'''
|
2
|
+
File: voting_methods.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: November 6, 2021
|
5
|
+
Update: January 15, 2023
|
6
|
+
|
7
|
+
The VotingMethod class and helper functions for voting methods
|
8
|
+
'''
|
9
|
+
|
10
|
+
import functools
|
11
|
+
import inspect
|
12
|
+
import numpy as np
|
13
|
+
from numba import jit # Remove until numba supports python 3.11
|
14
|
+
import random
|
15
|
+
import json
|
16
|
+
from pref_voting.voting_method_properties import VotingMethodProperties
|
17
|
+
from filelock import FileLock, Timeout
|
18
|
+
import importlib.resources
|
19
|
+
|
20
|
+
import glob
|
21
|
+
import os
|
22
|
+
|
23
|
+
class VotingMethod(object):
|
24
|
+
"""
|
25
|
+
A class to add functionality to voting methods.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
vm (function): An implementation of a voting method. The function should accept a Profile, ProfileWithTies, MajorityGraph, and/or MarginGraph, and a keyword parameter ``curr_cands`` to find the winner after restricting to ``curr_cands``.
|
29
|
+
name (string): The Human-readable name of the voting method.
|
30
|
+
properties (VotingMethodProperties): The properties of the voting method.
|
31
|
+
input_types (list): The types of input that the voting method can accept.
|
32
|
+
|
33
|
+
"""
|
34
|
+
def __init__(self,
|
35
|
+
vm,
|
36
|
+
name=None,
|
37
|
+
input_types=None,
|
38
|
+
skip_registration=False, properties_file=None):
|
39
|
+
|
40
|
+
self.vm = vm
|
41
|
+
self.name = name
|
42
|
+
|
43
|
+
# Determine the path to the properties file
|
44
|
+
if properties_file is None:
|
45
|
+
|
46
|
+
properties_file = importlib.resources.files('pref_voting') / 'data' / 'voting_methods_properties.json'
|
47
|
+
|
48
|
+
|
49
|
+
# Get the properties of the voting method
|
50
|
+
try:
|
51
|
+
with open(properties_file, "r") as file:
|
52
|
+
vm_props = json.load(file)
|
53
|
+
except FileNotFoundError:
|
54
|
+
vm_props = {}
|
55
|
+
except Exception as e:
|
56
|
+
print(f"An error occurred while opening the properties file: {e}")
|
57
|
+
vm_props = {}
|
58
|
+
|
59
|
+
if name in vm_props:
|
60
|
+
properties = VotingMethodProperties(**vm_props[name])
|
61
|
+
else:
|
62
|
+
properties = VotingMethodProperties()
|
63
|
+
|
64
|
+
self.properties = properties
|
65
|
+
self.input_types = input_types
|
66
|
+
self.skip_registration = skip_registration
|
67
|
+
self.algorithm = None
|
68
|
+
|
69
|
+
functools.update_wrapper(self, vm)
|
70
|
+
|
71
|
+
def __call__(self, edata, curr_cands = None, **kwargs):
|
72
|
+
|
73
|
+
if (curr_cands is not None and len(curr_cands) == 0) or len(edata.candidates) == 0:
|
74
|
+
return []
|
75
|
+
|
76
|
+
# Set the algorithm from self.algorithm if it's not already provided in kwargs
|
77
|
+
if 'algorithm' not in kwargs and self.algorithm is not None:
|
78
|
+
params = inspect.signature(self.vm).parameters
|
79
|
+
if 'algorithm' in params and params['algorithm'].kind in [inspect.Parameter.KEYWORD_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD]:
|
80
|
+
kwargs['algorithm'] = self.algorithm
|
81
|
+
|
82
|
+
return self.vm(edata, curr_cands=curr_cands, **kwargs)
|
83
|
+
|
84
|
+
def set_algorithm(self, algorithm):
|
85
|
+
"""
|
86
|
+
Set the algorithm for the voting method if 'algorithm' is an accepted keyword parameter.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
algorithm: The algorithm to set for the voting method.
|
90
|
+
"""
|
91
|
+
params = inspect.signature(self.vm).parameters
|
92
|
+
if 'algorithm' in params and params['algorithm'].kind in [inspect.Parameter.KEYWORD_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD]:
|
93
|
+
self.algorithm = algorithm
|
94
|
+
else:
|
95
|
+
raise ValueError(f"The method {self.name} does not accept 'algorithm' as a parameter.")
|
96
|
+
|
97
|
+
def choose(self, edata, curr_cands = None):
|
98
|
+
"""
|
99
|
+
Return a randomly chosen element from the winning set.
|
100
|
+
"""
|
101
|
+
|
102
|
+
ws = self.__call__(edata, curr_cands = curr_cands)
|
103
|
+
return random.choice(ws)
|
104
|
+
|
105
|
+
def prob(self, edata, curr_cands = None):
|
106
|
+
"""
|
107
|
+
Return a dictionary representing the even-chance tiebreaking for the voting method.
|
108
|
+
"""
|
109
|
+
|
110
|
+
ws = self.__call__(edata, curr_cands = curr_cands)
|
111
|
+
return {c: 1.0 / len(ws) if c in ws else 0.0 for c in edata.candidates}
|
112
|
+
|
113
|
+
def display(self, edata, curr_cands = None, cmap = None, **kwargs):
|
114
|
+
"""
|
115
|
+
Display the winning set of candidates.
|
116
|
+
"""
|
117
|
+
|
118
|
+
cmap = cmap if cmap is not None else edata.cmap
|
119
|
+
|
120
|
+
ws = self.__call__(edata, curr_cands = curr_cands, **kwargs)
|
121
|
+
|
122
|
+
if ws is None: # some voting methods, such as ``ranked_pairs_with_test``, may return None if it is taking long to compute the winner.
|
123
|
+
print(f"{self.name} winning set is not available")
|
124
|
+
else:
|
125
|
+
w_str = f"{self.name} winner is " if len(ws) == 1 else f"{self.name} winners are "
|
126
|
+
print(w_str + "{" + ", ".join([str(cmap[c]) for c in ws]) + "}")
|
127
|
+
|
128
|
+
def set_name(self, new_name):
|
129
|
+
"""Set the name of the voting method."""
|
130
|
+
|
131
|
+
self.name = new_name
|
132
|
+
|
133
|
+
def add_property(self, prop, value):
|
134
|
+
"""Add a property to the voting method."""
|
135
|
+
|
136
|
+
setattr(self.properties, prop, value)
|
137
|
+
|
138
|
+
def remove_property(self, prop):
|
139
|
+
"""Remove a property from the voting method."""
|
140
|
+
|
141
|
+
delattr(self.properties, prop)
|
142
|
+
|
143
|
+
def load_properties(self, filename=None):
|
144
|
+
"""Load the properties of the voting method from a JSON file."""
|
145
|
+
|
146
|
+
# Determine the path to the properties file
|
147
|
+
if filename is None:
|
148
|
+
filename = importlib.resources.files('pref_voting') / 'data' / 'voting_methods_properties.json'
|
149
|
+
lock = FileLock(f"{filename}.lock")
|
150
|
+
with lock:
|
151
|
+
try:
|
152
|
+
with open(filename, 'r') as file:
|
153
|
+
vm_props = json.load(file)
|
154
|
+
except FileNotFoundError:
|
155
|
+
vm_props = {}
|
156
|
+
|
157
|
+
if self.name in vm_props:
|
158
|
+
self.properties = VotingMethodProperties(**vm_props[self.name])
|
159
|
+
else:
|
160
|
+
self.properties = VotingMethodProperties()
|
161
|
+
|
162
|
+
def has_property(self, prop):
|
163
|
+
"""Check if the voting method has a property."""
|
164
|
+
|
165
|
+
return self.properties[prop]
|
166
|
+
|
167
|
+
def get_properties(self):
|
168
|
+
"""Return the properties of the voting method."""
|
169
|
+
|
170
|
+
return {
|
171
|
+
"satisfied": [prop
|
172
|
+
for prop, val in self.properties.items()
|
173
|
+
if val is True],
|
174
|
+
"violated": [prop
|
175
|
+
for prop, val in self.properties.items()
|
176
|
+
if val is False],
|
177
|
+
"na": [prop
|
178
|
+
for prop, val in self.properties.items()
|
179
|
+
if val is None]
|
180
|
+
}
|
181
|
+
|
182
|
+
def save_properties(self, filename=None, timeout=10):
|
183
|
+
"""Save the properties of the voting method to a JSON file."""
|
184
|
+
|
185
|
+
# Determine the path to the properties file
|
186
|
+
if filename is None:
|
187
|
+
filename = importlib.resources.files('pref_voting') / 'data' / 'voting_methods_properties.json'
|
188
|
+
|
189
|
+
|
190
|
+
lock = FileLock(f"{filename}.lock", timeout=timeout)
|
191
|
+
try:
|
192
|
+
with lock:
|
193
|
+
try:
|
194
|
+
with open(filename, 'r') as file:
|
195
|
+
vm_props = json.load(file)
|
196
|
+
except FileNotFoundError:
|
197
|
+
vm_props = {}
|
198
|
+
|
199
|
+
vm_props[self.name] = self.properties.__dict__
|
200
|
+
|
201
|
+
with open(filename, 'w') as file:
|
202
|
+
json.dump(vm_props, file, indent=4, sort_keys=True)
|
203
|
+
except Timeout:
|
204
|
+
print(f"Could not acquire the lock within {timeout} seconds.")
|
205
|
+
|
206
|
+
def get_violation_witness(self, prop, minimal_resolute=False, minimal=False):
|
207
|
+
"""Return the election that witnesses a violation of prop."""
|
208
|
+
|
209
|
+
from pref_voting.profiles import Profile
|
210
|
+
|
211
|
+
elections = {
|
212
|
+
"minimal resolute": None,
|
213
|
+
"minimal": None,
|
214
|
+
"any": None
|
215
|
+
}
|
216
|
+
if self.properties[prop]:
|
217
|
+
print(f"{self.name} satisfies {prop}, no election returned.")
|
218
|
+
return elections
|
219
|
+
elif self.properties[prop] is None:
|
220
|
+
print(f"{self.name} does not have a value for {prop}, no election returned.")
|
221
|
+
return elections
|
222
|
+
else:
|
223
|
+
dir = importlib.resources.files('pref_voting') / 'data' / 'examples' / prop
|
224
|
+
|
225
|
+
for f in glob.glob(f"{dir}*"):
|
226
|
+
fname = os.path.basename(f)
|
227
|
+
is_min = fname.startswith("minimal_")
|
228
|
+
is_min_resolute = fname.startswith("minimal_resolute")
|
229
|
+
found_it = False
|
230
|
+
if is_min_resolute and fname.startswith(f"minimal_resolute_{self.name.replace(' ', '_')}"):
|
231
|
+
print(f"Minimal resolute election for a violation of {prop} found.")
|
232
|
+
elections["minimal resolute"] = Profile.from_preflib(f)
|
233
|
+
if is_min and not is_min_resolute and fname.startswith(f"minimal_{self.name.replace(' ', '_')}"):
|
234
|
+
print(f"Minimal election for a violation of {prop} found.")
|
235
|
+
elections["minimal"] = Profile.from_preflib(f)
|
236
|
+
|
237
|
+
elif not is_min and not is_min_resolute and fname.startswith(f"{self.name.replace(' ', '_')}"):
|
238
|
+
elections["any"] = Profile.from_preflib(f)
|
239
|
+
if all([v is None for v in elections.values()]):
|
240
|
+
print(f"No election found illustrating the violation of {prop}.")
|
241
|
+
return elections
|
242
|
+
|
243
|
+
def check_property(self, prop, include_counterexample=True):
|
244
|
+
"""Check if the voting method satisfies a property."""
|
245
|
+
from pref_voting.axioms import axioms_dict
|
246
|
+
|
247
|
+
if not self.properties[prop]:
|
248
|
+
print(f"{self.name} does not satisfy {prop}")
|
249
|
+
if include_counterexample:
|
250
|
+
if prop in axioms_dict:
|
251
|
+
#prof = prof
|
252
|
+
axioms_dict[prop].counterexample(self)
|
253
|
+
|
254
|
+
elif self.properties[prop] is None:
|
255
|
+
print(f"{self.name} does not have a value for {prop}")
|
256
|
+
|
257
|
+
else:
|
258
|
+
print(f"{self.name} satisfies {prop}")
|
259
|
+
|
260
|
+
def __str__(self):
|
261
|
+
return f"{self.name}"
|
262
|
+
|
263
|
+
def vm(name=None,
|
264
|
+
input_types=None,
|
265
|
+
skip_registration=False):
|
266
|
+
"""
|
267
|
+
A decorator used when creating a voting method.
|
268
|
+
"""
|
269
|
+
def wrapper(f):
|
270
|
+
return VotingMethod(f,
|
271
|
+
name=name,
|
272
|
+
input_types=input_types,
|
273
|
+
skip_registration=skip_registration)
|
274
|
+
return wrapper
|
275
|
+
|
276
|
+
@jit(nopython=True, fastmath=True)
|
277
|
+
def isin(arr, val):
|
278
|
+
"""compiled function testing if the value val is in the array arr
|
279
|
+
"""
|
280
|
+
|
281
|
+
for i in range(arr.shape[0]):
|
282
|
+
if (arr[i]==val):
|
283
|
+
return True
|
284
|
+
return False
|
285
|
+
|
286
|
+
@jit(nopython=True, fastmath=True)
|
287
|
+
def _num_rank_first(rankings, rcounts, cands_to_ignore, cand):
|
288
|
+
"""The number of voters that rank candidate cand first after ignoring the candidates in
|
289
|
+
cands_to_ignore
|
290
|
+
|
291
|
+
Parameters
|
292
|
+
----------
|
293
|
+
rankings: 2d numpy array
|
294
|
+
list of linear orderings of the candidates
|
295
|
+
rcounts: 1d numpy array
|
296
|
+
list of numbers of voters with the rankings
|
297
|
+
cands_to_ignore: 1d numpy array
|
298
|
+
list of the candidates to ignore
|
299
|
+
cand: int
|
300
|
+
a candidate
|
301
|
+
|
302
|
+
Key assumptions:
|
303
|
+
* the candidates are named 0...num_cands - 1, and c1 and c2 are
|
304
|
+
numbers between 0 and num_cands - 1
|
305
|
+
* voters submit linear orders over the candidate
|
306
|
+
"""
|
307
|
+
|
308
|
+
num_voters = len(rankings)
|
309
|
+
|
310
|
+
top_cands_indices = np.zeros(num_voters, dtype=np.int32)
|
311
|
+
|
312
|
+
for vidx in range(num_voters):
|
313
|
+
for level in range(0, len(rankings[vidx])):
|
314
|
+
if not isin(cands_to_ignore, rankings[vidx][level]):
|
315
|
+
top_cands_indices[vidx] = level
|
316
|
+
break
|
317
|
+
top_cands = np.array([rankings[vidx][top_cands_indices[vidx]] for vidx in range(num_voters)])
|
318
|
+
is_cand = top_cands == cand # set to 0 each candidate not equal to cand
|
319
|
+
return np.sum(is_cand * rcounts)
|
320
|
+
|
321
|
+
|
322
|
+
@jit(nopython=True, fastmath=True)
|
323
|
+
def _num_rank_last(rankings, rcounts, cands_to_ignore, cand):
|
324
|
+
"""The number of voters that rank candidate cand last after ignoring the candidates in
|
325
|
+
cands_to_ignore
|
326
|
+
|
327
|
+
Parameters
|
328
|
+
----------
|
329
|
+
rankings: 2d numpy array
|
330
|
+
list of linear orderings of the candidates
|
331
|
+
rcounts: 1d numpy array
|
332
|
+
list of numbers of voters with the rankings
|
333
|
+
cands_to_ignore: 1d numpy array
|
334
|
+
list of the candidates to ignore
|
335
|
+
cand: int
|
336
|
+
a candidate
|
337
|
+
|
338
|
+
Key assumptions:
|
339
|
+
* the candidates are named 0...num_cands - 1, and c1 and c2 are
|
340
|
+
numbers between 0 and num_cands - 1
|
341
|
+
* voters submit linear orders over the candidate
|
342
|
+
"""
|
343
|
+
|
344
|
+
num_voters = len(rankings)
|
345
|
+
|
346
|
+
last_cands_indices = np.zeros(num_voters, dtype=np.int32)
|
347
|
+
|
348
|
+
for vidx in range(num_voters):
|
349
|
+
for level in range(len(rankings[vidx]) - 1,-1,-1):
|
350
|
+
if not isin(cands_to_ignore, rankings[vidx][level]):
|
351
|
+
last_cands_indices[vidx] = level
|
352
|
+
break
|
353
|
+
bottom_cands = np.array([rankings[vidx][last_cands_indices[vidx]] for vidx in range(num_voters)])
|
354
|
+
is_cand = bottom_cands == cand
|
355
|
+
return np.sum(is_cand * rcounts)
|
@@ -0,0 +1,92 @@
|
|
1
|
+
'''
|
2
|
+
File: voting_methods.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: November 6, 2021
|
5
|
+
Update: January 15, 2023
|
6
|
+
|
7
|
+
The VotingMethodProperties class to encapsulate properties of voting methods.
|
8
|
+
'''
|
9
|
+
from enum import Enum
|
10
|
+
|
11
|
+
class ElectionTypes(Enum):
|
12
|
+
PROFILE = "Profile"
|
13
|
+
PROFILE_WITH_TIES = "ProfileWithTies"
|
14
|
+
TRUNCATED_LINEAR_PROFILE = "TruncatedLinearProfile"
|
15
|
+
MAJORITY_GRAPH = "MajorityGraph"
|
16
|
+
MARGIN_GRAPH = "MarginGraph"
|
17
|
+
|
18
|
+
class VotingMethodProperties:
|
19
|
+
"""
|
20
|
+
Class to represent the properties of a voting method.
|
21
|
+
"""
|
22
|
+
def __init__(self, **kwargs):
|
23
|
+
"""
|
24
|
+
Initialize the properties of the voting method.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
**kwargs: Arbitrary keyword arguments representing properties and their boolean values.
|
28
|
+
"""
|
29
|
+
for key, value in kwargs.items():
|
30
|
+
setattr(self, key, value)
|
31
|
+
|
32
|
+
def items(self):
|
33
|
+
"""
|
34
|
+
Return all properties as a list of items.
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
List of all properties and their boolean values.
|
38
|
+
"""
|
39
|
+
return self.__dict__.items()
|
40
|
+
|
41
|
+
def satisfied(self):
|
42
|
+
"""
|
43
|
+
List all properties that are satisfied (i.e., True).
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
List of property names that are satisfied.
|
47
|
+
"""
|
48
|
+
return [key for key, value in self.items() if value is True]
|
49
|
+
|
50
|
+
def violated(self):
|
51
|
+
"""
|
52
|
+
List all properties that are violated (i.e., False).
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
List of property names that are violated.
|
56
|
+
"""
|
57
|
+
return [key for key, value in self.items() if value is False]
|
58
|
+
|
59
|
+
def not_available(self):
|
60
|
+
"""
|
61
|
+
List all properties that are not available (i.e., None).
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
List of property names that are None (currently not available).
|
65
|
+
"""
|
66
|
+
return [key for key, value in self.items() if value is None]
|
67
|
+
|
68
|
+
|
69
|
+
def __getitem__(self, prop):
|
70
|
+
"""
|
71
|
+
Get the value of a property.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
prop: The name of the property.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
The value of the property, or None if the property does not exist.
|
78
|
+
"""
|
79
|
+
return self.__dict__.get(prop, None)
|
80
|
+
|
81
|
+
def __str__(self) -> str:
|
82
|
+
"""
|
83
|
+
Return a string representation of all properties.
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
A string representation of all properties with their status (Satisfied, Violated, or N/A).
|
87
|
+
"""
|
88
|
+
properties = [
|
89
|
+
f"{key}: {'Satisfied' if value is True else 'Violated' if value is False else 'N/A'}"
|
90
|
+
for key, value in self.__dict__.items()
|
91
|
+
]
|
92
|
+
return "\n".join(properties)
|
@@ -0,0 +1,8 @@
|
|
1
|
+
from pref_voting.scoring_methods import *
|
2
|
+
from pref_voting.iterative_methods import *
|
3
|
+
from pref_voting.c1_methods import *
|
4
|
+
from pref_voting.margin_based_methods import *
|
5
|
+
from pref_voting.combined_methods import *
|
6
|
+
from pref_voting.other_methods import *
|
7
|
+
from pref_voting.stochastic_methods import *
|
8
|
+
|
@@ -0,0 +1,136 @@
|
|
1
|
+
'''
|
2
|
+
File: voting_methods_registry.py
|
3
|
+
Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
|
4
|
+
Date: April 29, 2024
|
5
|
+
|
6
|
+
'''
|
7
|
+
|
8
|
+
from pref_voting.voting_method import VotingMethod
|
9
|
+
import inspect
|
10
|
+
|
11
|
+
class VotingMethodRegistry:
|
12
|
+
def __init__(self):
|
13
|
+
self.methods = {}
|
14
|
+
|
15
|
+
def register(self,
|
16
|
+
method,
|
17
|
+
name,
|
18
|
+
method_type,
|
19
|
+
file_location,
|
20
|
+
election_types,
|
21
|
+
properties):
|
22
|
+
self.methods[name] = {
|
23
|
+
"method": method,
|
24
|
+
"method_type": method_type,
|
25
|
+
"file_location": file_location,
|
26
|
+
"election_types": election_types,
|
27
|
+
"properties": properties
|
28
|
+
}
|
29
|
+
|
30
|
+
def discover_methods(self, module):
|
31
|
+
"""Discovers and registers all VotingMethod instances in the given module."""
|
32
|
+
|
33
|
+
fname_to_method_type = {
|
34
|
+
"scoring_methods.py": "Scoring Rule",
|
35
|
+
"iterative_methods.py": "Iterative Method",
|
36
|
+
"c1_methods.py": "C1 Method",
|
37
|
+
"margin_based_methods.py": "Margin Based Method",
|
38
|
+
"other_methods.py": "Other Method",
|
39
|
+
"combined_methods.py": "Combined Method",
|
40
|
+
}
|
41
|
+
for name, obj in inspect.getmembers(module, lambda member: isinstance(member, VotingMethod)):
|
42
|
+
if hasattr(obj, 'name') and hasattr(obj, 'properties'):
|
43
|
+
|
44
|
+
if getattr(obj, 'skip_registration', False):
|
45
|
+
continue
|
46
|
+
|
47
|
+
self.register(obj,
|
48
|
+
obj.name,
|
49
|
+
fname_to_method_type[module.__file__.split("/")[-1]],
|
50
|
+
module.__file__.split("/")[-1],
|
51
|
+
getattr(obj, 'input_types', []),
|
52
|
+
getattr(obj, 'properties', {}))
|
53
|
+
|
54
|
+
def list_methods(self):
|
55
|
+
return list(self.methods.keys())
|
56
|
+
|
57
|
+
def display_methods(self):
|
58
|
+
for name, details in self.methods.items():
|
59
|
+
print(f"{name} ({details['method_type']})")
|
60
|
+
print(f"Satisfied properties: {details['properties'].satisfied() if details['properties'] is not None else 'N/A'}")
|
61
|
+
print(f"Violated properties: {details['properties'].violated() if details['properties'] is not None else 'N/A'}")
|
62
|
+
print()
|
63
|
+
|
64
|
+
def get_methods(self, method_type):
|
65
|
+
return [self.methods[vm]['method'] for vm in self.methods if self.methods[vm]['method_type'] == method_type]
|
66
|
+
|
67
|
+
def filter(self,
|
68
|
+
satisfies=None,
|
69
|
+
violates=None,
|
70
|
+
unknown=None,
|
71
|
+
election_types=None):
|
72
|
+
|
73
|
+
if satisfies is None:
|
74
|
+
satisfies = []
|
75
|
+
if violates is None:
|
76
|
+
violates = []
|
77
|
+
if unknown is None:
|
78
|
+
unknown = []
|
79
|
+
if election_types is None:
|
80
|
+
election_types = []
|
81
|
+
|
82
|
+
found_methods = []
|
83
|
+
for name, method_info in self.methods.items():
|
84
|
+
properties = method_info['properties']
|
85
|
+
method_election_types = method_info['election_types']
|
86
|
+
|
87
|
+
# Check if the method satisfies the required properties
|
88
|
+
if not all(getattr(properties, prop, None) is True for prop in satisfies):
|
89
|
+
continue
|
90
|
+
|
91
|
+
# Check if the method violates the required properties
|
92
|
+
if not all(getattr(properties, prop, None) is False for prop in violates):
|
93
|
+
continue
|
94
|
+
|
95
|
+
# Check for unknown properties
|
96
|
+
if not all(hasattr(properties, prop) and getattr(properties, prop, None) is None for prop in unknown):
|
97
|
+
continue
|
98
|
+
|
99
|
+
# Check if the method supports all required election types
|
100
|
+
if not all(et in method_election_types for et in election_types):
|
101
|
+
continue
|
102
|
+
|
103
|
+
# If all conditions are met, add to results
|
104
|
+
found_methods.append(method_info['method'])
|
105
|
+
|
106
|
+
return found_methods
|
107
|
+
|
108
|
+
def method_type(self, method_name):
|
109
|
+
return self.methods[method_name]['method_type']
|
110
|
+
def file_location(self, method_name):
|
111
|
+
return self.methods[method_name]['file_location']
|
112
|
+
|
113
|
+
def __len__(self):
|
114
|
+
return len(self.methods)
|
115
|
+
|
116
|
+
def __iter__(self):
|
117
|
+
self._iter = iter(method_details['method'] for method_details in self.methods.values())
|
118
|
+
return self
|
119
|
+
|
120
|
+
def __next__(self):
|
121
|
+
return next(self._iter)
|
122
|
+
|
123
|
+
voting_methods = VotingMethodRegistry()
|
124
|
+
|
125
|
+
import pref_voting.scoring_methods
|
126
|
+
voting_methods.discover_methods(pref_voting.scoring_methods)
|
127
|
+
import pref_voting.iterative_methods
|
128
|
+
voting_methods.discover_methods(pref_voting.iterative_methods)
|
129
|
+
import pref_voting.c1_methods
|
130
|
+
voting_methods.discover_methods(pref_voting.c1_methods)
|
131
|
+
import pref_voting.margin_based_methods
|
132
|
+
voting_methods.discover_methods(pref_voting.margin_based_methods)
|
133
|
+
import pref_voting.combined_methods
|
134
|
+
voting_methods.discover_methods(pref_voting.combined_methods)
|
135
|
+
import pref_voting.other_methods
|
136
|
+
voting_methods.discover_methods(pref_voting.other_methods)
|