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.
Files changed (92) hide show
  1. pref_voting/__init__.py +1 -0
  2. pref_voting/analysis.py +496 -0
  3. pref_voting/axiom.py +38 -0
  4. pref_voting/axiom_helpers.py +129 -0
  5. pref_voting/axioms.py +10 -0
  6. pref_voting/c1_methods.py +963 -0
  7. pref_voting/combined_methods.py +514 -0
  8. pref_voting/create_methods.py +128 -0
  9. pref_voting/data/examples/condorcet_winner/minimal_Anti-Plurality.soc +16 -0
  10. pref_voting/data/examples/condorcet_winner/minimal_Borda.soc +17 -0
  11. pref_voting/data/examples/condorcet_winner/minimal_Bracket_Voting.soc +20 -0
  12. pref_voting/data/examples/condorcet_winner/minimal_Bucklin.soc +19 -0
  13. pref_voting/data/examples/condorcet_winner/minimal_Coombs.soc +20 -0
  14. pref_voting/data/examples/condorcet_winner/minimal_Coombs_PUT.soc +20 -0
  15. pref_voting/data/examples/condorcet_winner/minimal_Coombs_TB.soc +20 -0
  16. pref_voting/data/examples/condorcet_winner/minimal_Dowdall.soc +19 -0
  17. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff.soc +18 -0
  18. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_PUT.soc +18 -0
  19. pref_voting/data/examples/condorcet_winner/minimal_Instant_Runoff_TB.soc +18 -0
  20. pref_voting/data/examples/condorcet_winner/minimal_Iterated_Removal_Condorcet_Loser.soc +17 -0
  21. pref_voting/data/examples/condorcet_winner/minimal_Pareto.soc +17 -0
  22. pref_voting/data/examples/condorcet_winner/minimal_Plurality.soc +18 -0
  23. pref_voting/data/examples/condorcet_winner/minimal_PluralityWRunoff_PUT.soc +18 -0
  24. pref_voting/data/examples/condorcet_winner/minimal_Positive-Negative_Voting.soc +17 -0
  25. pref_voting/data/examples/condorcet_winner/minimal_Simplified_Bucklin.soc +18 -0
  26. pref_voting/data/examples/condorcet_winner/minimal_Superior_Voting.soc +19 -0
  27. pref_voting/data/examples/condorcet_winner/minimal_Weighted_Bucklin.soc +19 -0
  28. pref_voting/data/examples/condorcet_winner/minimal_resolute_Anti-Plurality.soc +17 -0
  29. pref_voting/data/examples/condorcet_winner/minimal_resolute_Borda.soc +17 -0
  30. pref_voting/data/examples/condorcet_winner/minimal_resolute_Bracket_Voting.soc +20 -0
  31. pref_voting/data/examples/condorcet_winner/minimal_resolute_Bucklin.soc +19 -0
  32. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs.soc +21 -0
  33. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_PUT.soc +21 -0
  34. pref_voting/data/examples/condorcet_winner/minimal_resolute_Coombs_TB.soc +20 -0
  35. pref_voting/data/examples/condorcet_winner/minimal_resolute_Dowdall.soc +18 -0
  36. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff.soc +18 -0
  37. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_PUT.soc +18 -0
  38. pref_voting/data/examples/condorcet_winner/minimal_resolute_Instant_Runoff_TB.soc +18 -0
  39. pref_voting/data/examples/condorcet_winner/minimal_resolute_Plurality.soc +18 -0
  40. pref_voting/data/examples/condorcet_winner/minimal_resolute_PluralityWRunoff_PUT.soc +18 -0
  41. pref_voting/data/examples/condorcet_winner/minimal_resolute_Positive-Negative_Voting.soc +17 -0
  42. pref_voting/data/examples/condorcet_winner/minimal_resolute_Simplified_Bucklin.soc +20 -0
  43. pref_voting/data/examples/condorcet_winner/minimal_resolute_Weighted_Bucklin.soc +19 -0
  44. pref_voting/data/voting_methods_properties.json +414 -0
  45. pref_voting/data/voting_methods_properties.json.lock +0 -0
  46. pref_voting/dominance_axioms.py +387 -0
  47. pref_voting/generate_profiles.py +801 -0
  48. pref_voting/generate_spatial_profiles.py +198 -0
  49. pref_voting/generate_utility_profiles.py +160 -0
  50. pref_voting/generate_weighted_majority_graphs.py +506 -0
  51. pref_voting/grade_methods.py +184 -0
  52. pref_voting/grade_profiles.py +357 -0
  53. pref_voting/helper.py +370 -0
  54. pref_voting/invariance_axioms.py +671 -0
  55. pref_voting/io/__init__.py +0 -0
  56. pref_voting/io/readers.py +432 -0
  57. pref_voting/io/writers.py +256 -0
  58. pref_voting/iterative_methods.py +2425 -0
  59. pref_voting/maj_graph_ex1.png +0 -0
  60. pref_voting/mappings.py +577 -0
  61. pref_voting/margin_based_methods.py +2345 -0
  62. pref_voting/monotonicity_axioms.py +872 -0
  63. pref_voting/num_evaluation_method.py +77 -0
  64. pref_voting/other_axioms.py +161 -0
  65. pref_voting/other_methods.py +939 -0
  66. pref_voting/pairwise_profiles.py +547 -0
  67. pref_voting/prob_voting_method.py +105 -0
  68. pref_voting/probabilistic_methods.py +287 -0
  69. pref_voting/profiles.py +856 -0
  70. pref_voting/profiles_with_ties.py +1069 -0
  71. pref_voting/rankings.py +466 -0
  72. pref_voting/scoring_methods.py +481 -0
  73. pref_voting/social_welfare_function.py +59 -0
  74. pref_voting/social_welfare_functions.py +7 -0
  75. pref_voting/spatial_profiles.py +448 -0
  76. pref_voting/stochastic_methods.py +99 -0
  77. pref_voting/strategic_axioms.py +1394 -0
  78. pref_voting/swf_axioms.py +173 -0
  79. pref_voting/utility_functions.py +102 -0
  80. pref_voting/utility_methods.py +178 -0
  81. pref_voting/utility_profiles.py +333 -0
  82. pref_voting/variable_candidate_axioms.py +640 -0
  83. pref_voting/variable_voter_axioms.py +3747 -0
  84. pref_voting/voting_method.py +355 -0
  85. pref_voting/voting_method_properties.py +92 -0
  86. pref_voting/voting_methods.py +8 -0
  87. pref_voting/voting_methods_registry.py +136 -0
  88. pref_voting/weighted_majority_graphs.py +1539 -0
  89. pref_voting-1.16.31.dist-info/METADATA +208 -0
  90. pref_voting-1.16.31.dist-info/RECORD +92 -0
  91. pref_voting-1.16.31.dist-info/WHEEL +4 -0
  92. pref_voting-1.16.31.dist-info/licenses/LICENSE.txt +21 -0
@@ -0,0 +1,3747 @@
1
+ """
2
+ File: variable_voter_axioms.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: March 16, 2024
5
+
6
+ Variable voter axioms
7
+ """
8
+
9
+ from pref_voting.axiom import Axiom
10
+ from pref_voting.axiom_helpers import *
11
+ import numpy as np
12
+ from itertools import product, combinations, permutations
13
+ from pref_voting.helper import weak_orders
14
+ from pref_voting.rankings import Ranking
15
+ from pref_voting.generate_profiles import strict_weak_orders
16
+
17
+ def divide_electorate(prof):
18
+ """Given a Profile or ProfileWithTies object, yield all possible ways to divide the electorate into two nonempty electorates."""
19
+
20
+ R, C = prof.rankings_counts
21
+
22
+ ranges = [range(count+1) for count in C]
23
+
24
+ # For each combination of divisions
25
+ for division in product(*ranges):
26
+ C1 = np.array(division)
27
+ C2 = C - C1
28
+
29
+ # We will filter out rankings where the count is zero
30
+ nonzero_indices_C1 = np.nonzero(C1)[0]
31
+ nonzero_indices_C2 = np.nonzero(C2)[0]
32
+
33
+ # Only yield if both electorates have at least one voter
34
+ if nonzero_indices_C1.size > 0 and nonzero_indices_C2.size > 0:
35
+
36
+ if isinstance(prof, Profile):
37
+ rankings1 = R[nonzero_indices_C1].tolist()
38
+ rankings2 = R[nonzero_indices_C2].tolist()
39
+ else: # ProfileWithTies
40
+ rankings1 = [R[i] for i in nonzero_indices_C1]
41
+ rankings2 = [R[i] for i in nonzero_indices_C2]
42
+
43
+ counts1 = C1[nonzero_indices_C1].tolist()
44
+ counts2 = C2[nonzero_indices_C2].tolist()
45
+
46
+ # Convert rankings to comparable format for ordering check
47
+ if isinstance(prof, Profile):
48
+ comparable1 = rankings1
49
+ comparable2 = rankings2
50
+ else: # ProfileWithTies - convert Ranking objects to tuples for comparison
51
+ comparable1 = [tuple(r.rmap) for r in rankings1]
52
+ comparable2 = [tuple(r.rmap) for r in rankings2]
53
+
54
+ if comparable1 <= comparable2: # This prevents yielding both prof1, prof2 and later on prof2, prof1, unless they are equal
55
+
56
+ if isinstance(prof,Profile):
57
+ prof1 = Profile(rankings1, rcounts = counts1)
58
+ prof2 = Profile(rankings2, rcounts = counts2)
59
+
60
+ if isinstance(prof,ProfileWithTies):
61
+ prof1 = ProfileWithTies(rankings1, rcounts = counts1)
62
+ prof2 = ProfileWithTies(rankings2, rcounts = counts2)
63
+
64
+ if prof.using_extended_strict_preference:
65
+ prof1.use_extended_strict_preference()
66
+ prof2.use_extended_strict_preference()
67
+
68
+ yield prof1, prof2
69
+
70
+ def has_reinforcement_violation_with_undergeneration(prof, vm, verbose=False):
71
+ """Returns true if there is some binary partition of the electorate such that some candidate wins in both subprofiles but not in the full profile"""
72
+ ws = vm(prof)
73
+
74
+ for prof1, prof2 in divide_electorate(prof):
75
+ winners_in_both = [c for c in vm(prof1) if c in vm(prof2)]
76
+ if len(winners_in_both) > 0:
77
+ undergenerated = [c for c in winners_in_both if c not in ws]
78
+ if len(undergenerated) > 0:
79
+ if verbose:
80
+ print(f"Candidate {undergenerated[0]} wins in subprofiles 1 and 2 but loses in the full profile:")
81
+ print("")
82
+ print("Subprofile 1")
83
+ prof1.display()
84
+ print(prof1.description())
85
+ vm.display(prof1)
86
+ print("")
87
+ print("Subprofile 2")
88
+ prof2.display()
89
+ print(prof2.description())
90
+ vm.display(prof2)
91
+ print("")
92
+ print("Full profile")
93
+ prof.display()
94
+ print(prof.description())
95
+ vm.display(prof)
96
+ print("")
97
+ return True
98
+
99
+ return False
100
+
101
+ def has_reinforcement_violation_with_overgeneration(prof, vm, verbose=False):
102
+ """Returns true if there is some binary partition of the electorate such that some candidate wins in both subprofiles
103
+ but there is a winner in the full profile who is not among the winners in both subprofiles"""
104
+
105
+ ws = vm(prof)
106
+
107
+ for prof1, prof2 in divide_electorate(prof):
108
+ winners_in_both = [c for c in vm(prof1) if c in vm(prof2)]
109
+ if len(winners_in_both) > 0:
110
+ overgenerated = [c for c in ws if c not in winners_in_both]
111
+ if len(overgenerated) > 0:
112
+ if verbose:
113
+ print(f"Candidate {overgenerated[0]} wins in the full profile but is not among the candidates who win in both subprofiles:")
114
+ print("")
115
+ print("Subprofile 1")
116
+ prof1.display()
117
+ print(prof1.description())
118
+ vm.display(prof1)
119
+ print("")
120
+ print("Subprofile 2")
121
+ prof2.display()
122
+ print(prof2.description())
123
+ vm.display(prof2)
124
+ print("")
125
+ print("Full profile")
126
+ prof.display()
127
+ print(prof.description())
128
+ vm.display(prof)
129
+ print("")
130
+ return True
131
+
132
+ return False
133
+
134
+
135
+ def has_reinforcement_violation(prof, vm, verbose=False):
136
+ """
137
+ Returns True if there is a binary partition of the electorate such that (i) at least one candidate wins in both subelections and either (ii) some candidate who wins in both subelections does not win in the full election or (iii) some candidate who wins in the full election does not win both subelections.
138
+
139
+ Args:
140
+ prof: a Profile or ProfileWithTies object.
141
+ vm (VotingMethod): A voting method to test.
142
+ verbose (bool, default=False): If a violation is found, display the violation.
143
+
144
+ Returns:
145
+ Result of the test (bool): Returns True if there is a violation and False otherwise.
146
+
147
+ """
148
+ if has_reinforcement_violation_with_undergeneration(prof, vm, verbose):
149
+ return True
150
+
151
+ if has_reinforcement_violation_with_overgeneration(prof, vm, verbose):
152
+ return True
153
+
154
+ return False
155
+
156
+ def find_all_reinforcement_violations(prof, vm, verbose=False):
157
+ """
158
+ Returns all violations of reinforcement for a given profile and voting method.
159
+
160
+ Args:
161
+ prof: a Profile or ProfileWithTies object.
162
+ vm (VotingMethod): A voting method to test.
163
+ verbose (bool, default=False): If a violation is found, display the violation.
164
+
165
+ Returns:
166
+ Two list of triples (cand,prof1,prof2) where prof1 and prof2 partition the electorate. In the first list, (cand,prof1,prof2) indicates that cand wins in both prof1 and prof2 but loses in prof. In the second list, (cand,prof1,prof2) indicates that cand wins in prof but not in both prof1 and prof2 (and there are candidates who win in both prof1 and prof2).
167
+
168
+ """
169
+ ws = vm(prof)
170
+
171
+ undergenerations = list()
172
+ overgenerations = list()
173
+
174
+ for prof1, prof2 in divide_electorate(prof):
175
+ winners_in_both = [c for c in vm(prof1) if c in vm(prof2)]
176
+ if len(winners_in_both) > 0:
177
+
178
+ undergenerated = [c for c in winners_in_both if c not in ws]
179
+ if len(undergenerated) > 0:
180
+ for c in undergenerated:
181
+ undergenerations.append((c, prof1, prof2))
182
+ if verbose:
183
+ print(f"Candidate {undergenerated[0]} wins in subprofiles 1 and 2 but loses in the full profile:")
184
+ print("")
185
+ print("Subprofile 1")
186
+ prof1.display()
187
+ print(prof1.description())
188
+ vm.display(prof1)
189
+ print("")
190
+ print("Subprofile 2")
191
+ prof2.display()
192
+ print(prof2.description())
193
+ vm.display(prof2)
194
+ print("")
195
+ print("Full profile")
196
+ prof.display()
197
+ print(prof.description())
198
+ vm.display(prof)
199
+ print("")
200
+
201
+ overgenerated = [c for c in ws if c not in winners_in_both]
202
+ if len(overgenerated) > 0:
203
+ for c in overgenerated:
204
+ overgenerations.append((c, prof1, prof2))
205
+ if verbose:
206
+ print(f"Candidate {overgenerated[0]} wins in the full profile but is not among the candidates who win in both subprofiles:")
207
+ print("")
208
+ print("Subprofile 1")
209
+ prof1.display()
210
+ print(prof1.description())
211
+ vm.display(prof1)
212
+ print("")
213
+ print("Subprofile 2")
214
+ prof2.display()
215
+ print(prof2.description())
216
+ vm.display(prof2)
217
+ print("")
218
+ print("Full profile")
219
+ prof.display()
220
+ print(prof.description())
221
+ vm.display(prof)
222
+ print("")
223
+
224
+ return undergenerations, overgenerations
225
+
226
+ reinforcement = Axiom(
227
+ "Reinforcement",
228
+ has_violation = has_reinforcement_violation,
229
+ find_all_violations = find_all_reinforcement_violations,
230
+ )
231
+
232
+ def _submultisets_of_fixed_cardinality(elements, multiplicities, cardinality):
233
+
234
+ # Yields all sub-multisets of the given multiset with fixed cardinality.
235
+ # For a closed-form expression for the number of sub-multisets of fixed cardinality, see https://arxiv.org/abs/1511.06142
236
+
237
+ def valid_partitions(cardinality, remaining_elements):
238
+ if cardinality == 0:
239
+ yield ()
240
+ return
241
+ if not remaining_elements:
242
+ return
243
+ first, *rest = remaining_elements
244
+ first_idx = elements.index(first)
245
+ max_count = min(cardinality, multiplicities[first_idx])
246
+ for i in range(1, max_count + 1):
247
+ for partition in valid_partitions(cardinality-i, rest):
248
+ yield (i,) + partition
249
+
250
+ for i in range(1, min(len(elements), cardinality) + 1):
251
+ for subset in combinations(elements, i):
252
+ for partition in valid_partitions(cardinality, subset):
253
+ if len(partition) == len(subset):
254
+ yield (subset, partition)
255
+
256
+
257
+ def has_positive_involvement_violation(prof, vm, verbose=False, violation_type="Removal", coalition_size = 1, uniform_coalition = True, require_resoluteness = False, require_uniquely_weighted = False, check_probabilities = False):
258
+ """
259
+ If violation_type = "Removal", returns True if removing some voter (or voters if coalition_size > 1) who ranked a losing candidate A in first place causes A to win, witnessing a violation of positive involvement.
260
+
261
+ If uniform_coalition = True, then only coalitions of voters with the same ranking are considered.
262
+
263
+ If require_resoluteness = True, then only profiles with a unique winner are considered.
264
+
265
+ If require_uniquely_weighted = True, then only uniquely-weighted profiles are considered.
266
+
267
+ If check_probabilities = True, the function also checks whether removing the voters who ranked A in first-place causes A's probability of winning to increase (in the case of a tie broken by even-chance tiebreaking).
268
+
269
+ Args:
270
+ prof: a Profile or ProfileWithTies object.
271
+ vm (VotingMethod): A voting method to test.
272
+ verbose (bool, default=False): If a violation is found, display the violation.
273
+ violation_type: default is "Removal"
274
+
275
+ Returns:
276
+ Result of the test (bool): Returns True if there is a violation and False otherwise."""
277
+
278
+ winners = vm(prof)
279
+ losers = [c for c in prof.candidates if c not in winners]
280
+
281
+ if require_resoluteness and len(winners) > 1:
282
+ return False
283
+
284
+ if require_uniquely_weighted and not prof.is_uniquely_weighted():
285
+ return False
286
+
287
+ if violation_type == "Removal":
288
+ if uniform_coalition:
289
+ for loser in losers:
290
+
291
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == loser and prof.rankings.count(r) >= coalition_size]
292
+
293
+ for r in relevant_ranking_types:
294
+
295
+ rankings = prof.rankings
296
+
297
+ for i in range(coalition_size):
298
+ rankings.remove(r) # remove coalition_size-many tokens of the type of ranking
299
+
300
+ if isinstance(prof,Profile):
301
+ prof2 = Profile(rankings)
302
+
303
+ if isinstance(prof,ProfileWithTies):
304
+ prof2 = ProfileWithTies(rankings, candidates = prof.candidates)
305
+ if prof.using_extended_strict_preference:
306
+ prof2.use_extended_strict_preference()
307
+
308
+ winners2 = vm(prof2)
309
+
310
+ if require_resoluteness and len(winners2) > 1:
311
+ continue
312
+
313
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
314
+ continue
315
+
316
+ if loser in winners2:
317
+
318
+ if verbose:
319
+ prof = prof.anonymize()
320
+ if coalition_size == 1:
321
+ print(f"{loser} loses in the full profile, but {loser} is a winner after removing voter with the ranking {str(r)}:")
322
+ else:
323
+ print(f"{loser} loses in the full profile, but {loser} is a winner after removing {coalition_size} voters with the ranking {str(r)}:")
324
+ print("")
325
+ print("Full profile:")
326
+ prof.display()
327
+ print(prof.description())
328
+ prof.display_margin_graph()
329
+ vm.display(prof)
330
+ print("")
331
+ if coalition_size == 1:
332
+ print(f"Profile with voter removed:")
333
+ else:
334
+ print(f"Profile with {coalition_size} voters removed:")
335
+ prof2 = prof2.anonymize()
336
+ prof2.display()
337
+ print(prof2.description())
338
+ prof2.display_margin_graph()
339
+ vm.display(prof2)
340
+ print("")
341
+ return True
342
+
343
+ if check_probabilities:
344
+ for winner in winners:
345
+
346
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == winner and prof.rankings.count(r) >= coalition_size]
347
+
348
+ for r in relevant_ranking_types:
349
+
350
+ rankings = prof.rankings
351
+
352
+ for i in range(coalition_size):
353
+ rankings.remove(r) # remove coalition_size-many tokens of the type of ranking
354
+
355
+ if isinstance(prof,Profile):
356
+ prof2 = Profile(rankings)
357
+
358
+ if isinstance(prof,ProfileWithTies):
359
+ prof2 = ProfileWithTies(rankings, candidates = prof.candidates)
360
+ if prof.using_extended_strict_preference:
361
+ prof2.use_extended_strict_preference()
362
+
363
+ winners2 = vm(prof2)
364
+
365
+ if require_resoluteness and len(winners2) > 1:
366
+ continue
367
+
368
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
369
+ continue
370
+
371
+ if winner in winners2 and len(winners) > len(winners2):
372
+
373
+ if verbose:
374
+ prof = prof.anonymize()
375
+ if coalition_size == 1:
376
+ print(f"{winner} has a higher probability of winning after removing voter with the ranking {str(r)}:")
377
+ else:
378
+ print(f"{winner} has a higher probability of winning after removing removing {coalition_size} voters with the ranking {str(r)}:")
379
+ print("")
380
+ print("Full profile:")
381
+ prof.display()
382
+ print(prof.description())
383
+ prof.display_margin_graph()
384
+ vm.display(prof)
385
+ print("")
386
+ if coalition_size == 1:
387
+ print(f"Profile with voter removed:")
388
+ else:
389
+ print(f"Profile with {coalition_size} voters removed:")
390
+ prof2 = prof2.anonymize()
391
+ prof2.display()
392
+ print(prof2.description())
393
+ prof2.display_margin_graph()
394
+ vm.display(prof2)
395
+ print("")
396
+ return True
397
+
398
+
399
+ if not uniform_coalition:
400
+ for loser in losers:
401
+
402
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == loser]
403
+ relevant_ranking_types_counts = [prof.rankings.count(r) for r in relevant_ranking_types]
404
+
405
+ for coalition_rankings, coalition_rankings_counts in _submultisets_of_fixed_cardinality(relevant_ranking_types,relevant_ranking_types_counts,coalition_size):
406
+
407
+ rankings = prof.rankings
408
+
409
+ for r_idx, r in enumerate(coalition_rankings):
410
+ for i in range(coalition_rankings_counts[r_idx]):
411
+ rankings.remove(r)
412
+
413
+ if isinstance(prof,Profile):
414
+ prof2 = Profile(rankings)
415
+
416
+ if isinstance(prof,ProfileWithTies):
417
+ prof2 = ProfileWithTies(rankings, candidates = prof.candidates)
418
+ if prof.using_extended_strict_preference:
419
+ prof2.use_extended_strict_preference()
420
+
421
+ winners2 = vm(prof2)
422
+
423
+ if require_resoluteness and len(winners2) > 1:
424
+ continue
425
+
426
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
427
+ continue
428
+
429
+ if loser in winners2:
430
+
431
+ if verbose:
432
+ prof = prof.anonymize()
433
+ print(f"{loser} loses in the full profile, but {loser} is a winner after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
434
+ print("")
435
+ print("Full profile:")
436
+ prof.display()
437
+ print(prof.description())
438
+ prof.display_margin_graph()
439
+ vm.display(prof)
440
+ print("")
441
+ print(f"Profile with coalition removed:")
442
+ prof2 = prof2.anonymize()
443
+ prof2.display()
444
+ print(prof2.description())
445
+ prof2.display_margin_graph()
446
+ vm.display(prof2)
447
+ print("")
448
+ return True
449
+
450
+ if check_probabilities:
451
+ for winner in winners:
452
+
453
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == winner]
454
+ relevant_ranking_types_counts = [prof.rankings.count(r) for r in relevant_ranking_types]
455
+
456
+ for coalition_rankings, coalition_rankings_counts in _submultisets_of_fixed_cardinality(relevant_ranking_types,relevant_ranking_types_counts,coalition_size):
457
+
458
+ rankings = prof.rankings
459
+
460
+ for r_idx, r in enumerate(coalition_rankings):
461
+ for i in range(coalition_rankings_counts[r_idx]):
462
+ rankings.remove(r)
463
+
464
+ if isinstance(prof,Profile):
465
+ prof2 = Profile(rankings)
466
+
467
+ if isinstance(prof,ProfileWithTies):
468
+ prof2 = ProfileWithTies(rankings)
469
+ if prof.using_extended_strict_preference:
470
+ prof2.use_extended_strict_preference()
471
+
472
+ winners2 = vm(prof2)
473
+
474
+ if require_resoluteness and len(winners2) > 1:
475
+ continue
476
+
477
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
478
+ continue
479
+
480
+ if winner in winners2 and len(winners) > len(winners2):
481
+
482
+ if verbose:
483
+ prof = prof.anonymize()
484
+ print(f"{winner} has a higher probability of winning after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
485
+ print("")
486
+ print("Full profile:")
487
+ prof.display()
488
+ print(prof.description())
489
+ prof.display_margin_graph()
490
+ vm.display(prof)
491
+ print("")
492
+ print(f"Profile with coalition removed:")
493
+ prof2 = prof2.anonymize()
494
+ prof2.display()
495
+ print(prof2.description())
496
+ prof2.display_margin_graph()
497
+ vm.display(prof2)
498
+ print("")
499
+ return True
500
+
501
+ return False
502
+
503
+ def find_all_positive_involvement_violations(prof, vm, verbose=False, violation_type="Removal", coalition_size = 1, uniform_coalition = True, require_resoluteness = False, require_uniquely_weighted = False, check_probabilities = False):
504
+ """
505
+ If violation_type = "Removal", returns a list of pairs (loser, rankings, counts) such that removing the indicated rankings with the indicated counts causes the loser to win, witnessing a violation of positive involvement.
506
+
507
+ If uniform_coalition = True, then only coalitions of voters with the same ranking are considered.
508
+
509
+ If require_resoluteness = True, then only profiles with a unique winner are considered.
510
+
511
+ If require_uniquely_weighted = True, then only uniquely-weighted profiles are considered.
512
+
513
+ If check_probabilities = True, the function also checks whether removing the voters who ranked A in first-place causes A's probability of winning to increase (in the case of a tie broken by even-chance tiebreaking).
514
+
515
+ Args:
516
+ prof: a Profile or ProfileWithTies object.
517
+ vm (VotingMethod): A voting method to test.
518
+ verbose (bool, default=False): If a violation is found, display the violation.
519
+ violation_type: default is "Removal"
520
+
521
+ Returns:
522
+ A List of triples (loser,rankings,counts) witnessing violations of positive involvement.
523
+
524
+ .. warning::
525
+ This function is slow when uniform_coalition = False and the numbers of voters and candidates are too large.
526
+ """
527
+
528
+ winners = vm(prof)
529
+ losers = [c for c in prof.candidates if c not in winners]
530
+
531
+ witnesses = list()
532
+
533
+ if require_resoluteness and len(winners) > 1:
534
+ return witnesses
535
+
536
+ if require_uniquely_weighted and not prof.is_uniquely_weighted():
537
+ return witnesses
538
+
539
+ if violation_type == "Removal":
540
+ if uniform_coalition:
541
+ for loser in losers:
542
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == loser and prof.rankings.count(r) >= coalition_size]
543
+
544
+ for r in relevant_ranking_types: # for each type of ranking
545
+
546
+ rankings = prof.rankings # copy the token rankings
547
+
548
+ for i in range(coalition_size):
549
+ rankings.remove(r) # remove coalition_size-many tokens of the type of ranking
550
+
551
+ if isinstance(prof,Profile):
552
+ prof2 = Profile(rankings)
553
+
554
+ if isinstance(prof,ProfileWithTies):
555
+ prof2 = ProfileWithTies(rankings, candidates = prof.candidates)
556
+ if prof.using_extended_strict_preference:
557
+ prof2.use_extended_strict_preference()
558
+
559
+ winners2 = vm(prof2)
560
+
561
+ if require_resoluteness and len(winners2) > 1:
562
+ continue
563
+
564
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
565
+ continue
566
+
567
+ if loser in winners2:
568
+ witnesses.append((loser, [r], [coalition_size]))
569
+ if verbose:
570
+ prof = prof.anonymize()
571
+ if coalition_size == 1:
572
+ print(f"{loser} loses in the full profile, but {loser} is a winner after removing voter with the ranking {str(r)}:")
573
+ else:
574
+ print(f"{loser} loses in the full profile, but {loser} is a winner after removing {coalition_size} voters with the ranking {str(r)}:")
575
+ print("")
576
+ print("Full profile")
577
+ prof.display()
578
+ print(prof.description())
579
+ prof.display_margin_graph()
580
+ vm.display(prof)
581
+ print("")
582
+ if coalition_size == 1:
583
+ print(f"Profile with voter removed:")
584
+ else:
585
+ print(f"Profile with {coalition_size} voters removed:")
586
+ prof2 = prof2.anonymize()
587
+ prof2.display()
588
+ print(prof2.description())
589
+ prof2.display_margin_graph()
590
+ vm.display(prof2)
591
+ print("")
592
+
593
+ if check_probabilities:
594
+ for winner in winners:
595
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == winner and prof.rankings.count(r) >= coalition_size]
596
+
597
+ for r in relevant_ranking_types:
598
+
599
+ rankings = prof.rankings
600
+
601
+ for i in range(coalition_size):
602
+ rankings.remove(r)
603
+
604
+ if isinstance(prof,Profile):
605
+ prof2 = Profile(rankings)
606
+
607
+ if isinstance(prof,ProfileWithTies):
608
+ prof2 = ProfileWithTies(rankings, candidates = prof.candidates)
609
+ if prof.using_extended_strict_preference:
610
+ prof2.use_extended_strict_preference()
611
+
612
+ winners2 = vm(prof2)
613
+
614
+ if require_resoluteness and len(winners2) > 1:
615
+ continue
616
+
617
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
618
+ continue
619
+
620
+ if winner in winners2 and len(winners) > len(winners2):
621
+ witnesses.append((winner, [r], [coalition_size]))
622
+ if verbose:
623
+ prof = prof.anonymize()
624
+ if coalition_size == 1:
625
+ print(f"{winner} has a higher probability of winning after removing voter with the ranking {str(r)}:")
626
+ else:
627
+ print(f"{winner} has a higher probability of winning after removing {coalition_size} voters with the ranking {str(r)}:")
628
+ print("")
629
+ print("Full profile")
630
+ prof.display()
631
+ print(prof.description())
632
+ prof.display_margin_graph()
633
+ vm.display(prof)
634
+ print("")
635
+ if coalition_size == 1:
636
+ print(f"Profile with voter removed:")
637
+ else:
638
+ print(f"Profile with {coalition_size} voters removed:")
639
+ prof2 = prof2.anonymize()
640
+ prof2.display()
641
+ print(prof2.description())
642
+ prof2.display_margin_graph()
643
+ vm.display(prof2)
644
+ print("")
645
+
646
+
647
+ if not uniform_coalition:
648
+ for loser in losers:
649
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == loser]
650
+ relevant_ranking_types_counts = [prof.rankings.count(r) for r in relevant_ranking_types]
651
+
652
+ for coalition_rankings, coalition_rankings_counts in _submultisets_of_fixed_cardinality(relevant_ranking_types,relevant_ranking_types_counts,coalition_size):
653
+
654
+ rankings = prof.rankings
655
+
656
+ for r_idx, r in enumerate(coalition_rankings):
657
+ for i in range(coalition_rankings_counts[r_idx]):
658
+ rankings.remove(r)
659
+
660
+ if isinstance(prof,Profile):
661
+ prof2 = Profile(rankings)
662
+
663
+ if isinstance(prof,ProfileWithTies):
664
+ prof2 = ProfileWithTies(rankings, candidates = prof.candidates)
665
+ if prof.using_extended_strict_preference:
666
+ prof2.use_extended_strict_preference()
667
+
668
+ winners2 = vm(prof2)
669
+
670
+ if require_resoluteness and len(winners2) > 1:
671
+ continue
672
+
673
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
674
+ continue
675
+
676
+ if loser in winners2:
677
+ witnesses.append((loser, coalition_rankings, coalition_rankings_counts))
678
+ if verbose:
679
+ prof = prof.anonymize()
680
+ print(f"{loser} loses in the full profile, but {loser} is a winner after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
681
+ print("")
682
+ print("Full profile")
683
+ prof.display()
684
+ print(prof.description())
685
+ prof.display_margin_graph()
686
+ vm.display(prof)
687
+ print("")
688
+ print(f"Profile with coalition removed:")
689
+ prof2 = prof2.anonymize()
690
+ prof2.display()
691
+ print(prof2.description())
692
+ prof2.display_margin_graph()
693
+ vm.display(prof2)
694
+ print("")
695
+
696
+ if check_probabilities:
697
+ for winner in winners:
698
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == winner]
699
+ relevant_ranking_types_counts = [prof.rankings.count(r) for r in relevant_ranking_types]
700
+
701
+ for coalition_rankings, coalition_rankings_counts in _submultisets_of_fixed_cardinality(relevant_ranking_types,relevant_ranking_types_counts,coalition_size):
702
+
703
+ rankings = prof.rankings
704
+
705
+ for r_idx, r in enumerate(coalition_rankings):
706
+ for i in range(coalition_rankings_counts[r_idx]):
707
+ rankings.remove(r)
708
+
709
+ if isinstance(prof,Profile):
710
+ prof2 = Profile(rankings)
711
+
712
+ if isinstance(prof,ProfileWithTies):
713
+ prof2 = ProfileWithTies(rankings, candidates = prof.candidates)
714
+ if prof.using_extended_strict_preference:
715
+ prof2.use_extended_strict_preference()
716
+
717
+ winners2 = vm(prof2)
718
+
719
+ if require_resoluteness and len(winners2) > 1:
720
+ continue
721
+
722
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
723
+ continue
724
+
725
+ if winner in winners2 and len(winners) > len(winners2):
726
+ witnesses.append((winner, coalition_rankings, coalition_rankings_counts))
727
+ if verbose:
728
+ prof = prof.anonymize()
729
+ print(f"{winner} has a higher probability of winning after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
730
+ print("")
731
+ print("Full profile")
732
+ prof.display()
733
+ print(prof.description())
734
+ prof.display_margin_graph()
735
+ vm.display(prof)
736
+ print("")
737
+ print(f"Profile with coalition removed:")
738
+ prof2 = prof2.anonymize()
739
+ prof2.display()
740
+ print(prof2.description())
741
+ prof2.display_margin_graph()
742
+ vm.display(prof2)
743
+ print("")
744
+
745
+ return witnesses
746
+
747
+ positive_involvement = Axiom(
748
+ "Positive Involvement",
749
+ has_violation = has_positive_involvement_violation,
750
+ find_all_violations = find_all_positive_involvement_violations,
751
+ )
752
+
753
+ def has_negative_involvement_violation(prof, vm, verbose=False, violation_type="Removal", coalition_size=1, uniform_coalition=True, require_resoluteness=False, require_uniquely_weighted=False, check_probabilities=False):
754
+ """
755
+ If violation_type = "Removal", returns True if removing some voter(s) who ranked a winning candidate B in last place causes B to lose, witnessing a violation of negative involvement.
756
+
757
+ If uniform_coalition = True, then only coalitions of voters with the same ranking are considered.
758
+
759
+ If require_resoluteness = True, then only profiles with a unique winner are considered.
760
+
761
+ If require_uniquely_weighted = True, then only uniquely-weighted profiles are considered.
762
+
763
+ If check_probabilities = True, the function also checks whether removing the voters who ranked B in last-place causes B's probability of winning to decrease (in the case of a tie broken by even-chance tiebreaking).
764
+
765
+ Args:
766
+ prof: a Profile or ProfileWithTies object.
767
+ vm (VotingMethod): A voting method to test.
768
+ verbose (bool, default=False): If a violation is found, display the violation.
769
+ violation_type: default is "Removal"
770
+ coalition_size: default is 1
771
+ uniform_coalition: default is True
772
+ require_resoluteness: default is False
773
+ require_uniquely_weighted: default is False
774
+ check_probabilities: default is False
775
+
776
+ Returns:
777
+ Result of the test (bool): Returns True if there is a violation and False otherwise."""
778
+
779
+ winners = vm(prof)
780
+
781
+ if require_resoluteness and len(winners) > 1:
782
+ return False
783
+
784
+ if require_uniquely_weighted and not prof.is_uniquely_weighted():
785
+ return False
786
+
787
+ if violation_type == "Removal":
788
+ if uniform_coalition:
789
+ for winner in winners:
790
+ relevant_ranking_types = [r for r in prof.ranking_types if r[-1] == winner and prof.rankings.count(r) >= coalition_size]
791
+
792
+ for r in relevant_ranking_types:
793
+ rankings = prof.rankings.copy()
794
+
795
+ for i in range(coalition_size):
796
+ rankings.remove(r) # remove coalition_size-many tokens of the type of ranking
797
+
798
+ if isinstance(prof, Profile):
799
+ prof2 = Profile(rankings)
800
+
801
+ if isinstance(prof, ProfileWithTies):
802
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
803
+ if prof.using_extended_strict_preference:
804
+ prof2.use_extended_strict_preference()
805
+
806
+ winners2 = vm(prof2)
807
+
808
+ if require_resoluteness and len(winners2) > 1:
809
+ continue
810
+
811
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
812
+ continue
813
+
814
+ if winner not in winners2:
815
+ if verbose:
816
+ prof = prof.anonymize()
817
+ if coalition_size == 1:
818
+ print(f"{winner} wins in the full profile, but {winner} is a loser after removing a voter with the ranking {str(r)}:")
819
+ else:
820
+ print(f"{winner} wins in the full profile, but {winner} is a loser after removing {coalition_size} voters with the ranking {str(r)}:")
821
+ print("")
822
+ print("Full profile")
823
+ prof.display()
824
+ print(prof.description())
825
+ prof.display_margin_graph()
826
+ vm.display(prof)
827
+ print("")
828
+ if coalition_size == 1:
829
+ print("Profile with voter removed")
830
+ else:
831
+ print(f"Profile with {coalition_size} voters removed")
832
+ prof2 = prof2.anonymize()
833
+ prof2.display()
834
+ print(prof2.description())
835
+ prof2.display_margin_graph()
836
+ vm.display(prof2)
837
+ print("")
838
+ return True
839
+
840
+ # Case: B's probability of winning decreases as winning set expands
841
+ if check_probabilities and winner in winners2 and len(winners) < len(winners2):
842
+ if verbose:
843
+ prof = prof.anonymize()
844
+ if coalition_size == 1:
845
+ print(f"{winner} becomes less likely to win after removing a voter with the ranking {str(r)}:")
846
+ else:
847
+ print(f"{winner} becomes less likely to win after removing {coalition_size} voters with the ranking {str(r)}:")
848
+ print("")
849
+ print("Full profile")
850
+ prof.display()
851
+ print(prof.description())
852
+ prof.display_margin_graph()
853
+ vm.display(prof)
854
+ print("")
855
+ if coalition_size == 1:
856
+ print("Profile with voter removed")
857
+ else:
858
+ print(f"Profile with {coalition_size} voters removed")
859
+ prof2 = prof2.anonymize()
860
+ prof2.display()
861
+ print(prof2.description())
862
+ prof2.display_margin_graph()
863
+ vm.display(prof2)
864
+ print("")
865
+ return True
866
+
867
+ if not uniform_coalition:
868
+ for winner in winners:
869
+ relevant_ranking_types = [r for r in prof.ranking_types if r[-1] == winner]
870
+ relevant_ranking_types_counts = [prof.rankings.count(r) for r in relevant_ranking_types]
871
+
872
+ for coalition_rankings, coalition_rankings_counts in _submultisets_of_fixed_cardinality(relevant_ranking_types, relevant_ranking_types_counts, coalition_size):
873
+
874
+ rankings = prof.rankings.copy()
875
+
876
+ for r_idx, r in enumerate(coalition_rankings):
877
+ for i in range(coalition_rankings_counts[r_idx]):
878
+ rankings.remove(r)
879
+
880
+ if isinstance(prof, Profile):
881
+ prof2 = Profile(rankings)
882
+
883
+ if isinstance(prof, ProfileWithTies):
884
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
885
+ if prof.using_extended_strict_preference:
886
+ prof2.use_extended_strict_preference()
887
+
888
+ winners2 = vm(prof2)
889
+
890
+ if require_resoluteness and len(winners2) > 1:
891
+ continue
892
+
893
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
894
+ continue
895
+
896
+ if winner not in winners2:
897
+ if verbose:
898
+ prof = prof.anonymize()
899
+ print(f"{winner} wins in the full profile, but {winner} is a loser after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
900
+ print("")
901
+ print("Full profile")
902
+ prof.display()
903
+ print(prof.description())
904
+ prof.display_margin_graph()
905
+ vm.display(prof)
906
+ print("")
907
+ print(f"Profile with coalition removed")
908
+ prof2 = prof2.anonymize()
909
+ prof2.display()
910
+ print(prof2.description())
911
+ prof2.display_margin_graph()
912
+ vm.display(prof2)
913
+ print("")
914
+ return True
915
+
916
+ # Case: B's probability of winning decreases as winning set expands
917
+ if check_probabilities and winner in winners2 and len(winners) < len(winners2):
918
+ if verbose:
919
+ prof = prof.anonymize()
920
+ print(f"{winner} becomes less likely to win after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
921
+ print("")
922
+ print("Full profile")
923
+ prof.display()
924
+ print(prof.description())
925
+ prof.display_margin_graph()
926
+ vm.display(prof)
927
+ print("")
928
+ print(f"Profile with coalition removed")
929
+ prof2 = prof2.anonymize()
930
+ prof2.display()
931
+ print(prof2.description())
932
+ prof2.display_margin_graph()
933
+ vm.display(prof2)
934
+ print("")
935
+ return True
936
+
937
+ return False
938
+
939
+ def find_all_negative_involvement_violations(prof, vm, verbose=False, violation_type="Removal", coalition_size=1, uniform_coalition=True, require_resoluteness=False, require_uniquely_weighted=False, check_probabilities=False):
940
+ """
941
+ If violation_type = "Removal", returns a list of tuples (winner, rankings, counts) such that removing the indicated rankings with the indicated counts causes the winner to lose, witnessing a violation of negative involvement.
942
+
943
+ If uniform_coalition = True, then only coalitions of voters with the same ranking are considered.
944
+
945
+ If require_resoluteness = True, then only profiles with a unique winner are considered.
946
+
947
+ If require_uniquely_weighted = True, then only uniquely-weighted profiles are considered.
948
+
949
+ If check_probabilities = True, the function also checks whether removing the voters who ranked B in last-place causes B's probability of winning to decrease (in the case of a tie broken by even-chance tiebreaking).
950
+
951
+ Args:
952
+ prof: a Profile or ProfileWithTies object.
953
+ vm (VotingMethod): A voting method to test.
954
+ verbose (bool, default=False): If a violation is found, display the violation.
955
+ violation_type: default is "Removal"
956
+ coalition_size: default is 1
957
+ uniform_coalition: default is True
958
+ require_resoluteness: default is False
959
+ require_uniquely_weighted: default is False
960
+ check_probabilities: default is False
961
+
962
+ Returns:
963
+ A List of tuples (winner, rankings, counts) witnessing violations of negative involvement.
964
+
965
+ .. warning::
966
+ This function is slow when uniform_coalition = False and the numbers of voters and candidates are too large.
967
+ """
968
+
969
+ winners = vm(prof)
970
+
971
+ witnesses = list()
972
+
973
+ if require_resoluteness and len(winners) > 1:
974
+ return witnesses
975
+
976
+ if require_uniquely_weighted and not prof.is_uniquely_weighted():
977
+ return witnesses
978
+
979
+ if violation_type == "Removal":
980
+ if uniform_coalition:
981
+ for winner in winners:
982
+ relevant_ranking_types = [r for r in prof.ranking_types if r[-1] == winner and prof.rankings.count(r) >= coalition_size]
983
+
984
+ for r in relevant_ranking_types:
985
+ rankings = prof.rankings.copy()
986
+
987
+ for i in range(coalition_size):
988
+ rankings.remove(r) # remove coalition_size-many tokens of the type of ranking
989
+
990
+ if isinstance(prof, Profile):
991
+ prof2 = Profile(rankings)
992
+
993
+ if isinstance(prof, ProfileWithTies):
994
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
995
+ if prof.using_extended_strict_preference:
996
+ prof2.use_extended_strict_preference()
997
+
998
+ winners2 = vm(prof2)
999
+
1000
+ if require_resoluteness and len(winners2) > 1:
1001
+ continue
1002
+
1003
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
1004
+ continue
1005
+
1006
+ if winner not in winners2:
1007
+ witnesses.append((winner, [r], [coalition_size]))
1008
+ if verbose:
1009
+ prof = prof.anonymize()
1010
+ if coalition_size == 1:
1011
+ print(f"{winner} wins in the full profile, but {winner} is a loser after removing a voter with the ranking {str(r)}:")
1012
+ else:
1013
+ print(f"{winner} wins in the full profile, but {winner} is a loser after removing {coalition_size} voters with the ranking {str(r)}:")
1014
+ print("")
1015
+ print("Full profile")
1016
+ prof.display()
1017
+ print(prof.description())
1018
+ prof.display_margin_graph()
1019
+ vm.display(prof)
1020
+ print("")
1021
+ if coalition_size == 1:
1022
+ print("Profile with voter removed")
1023
+ else:
1024
+ print(f"Profile with {coalition_size} voters removed")
1025
+ prof2 = prof2.anonymize()
1026
+ prof2.display()
1027
+ print(prof2.description())
1028
+ prof2.display_margin_graph()
1029
+ vm.display(prof2)
1030
+ print("")
1031
+
1032
+ # Case: B's probability of winning decreases as winning set expands
1033
+ if check_probabilities and winner in winners2 and len(winners) < len(winners2):
1034
+ witnesses.append((winner, [r], [coalition_size]))
1035
+ if verbose:
1036
+ prof = prof.anonymize()
1037
+ if coalition_size == 1:
1038
+ print(f"{winner} becomes less likely to win after removing a voter with the ranking {str(r)}:")
1039
+ else:
1040
+ print(f"{winner} becomes less likely to win after removing {coalition_size} voters with the ranking {str(r)}:")
1041
+ print("")
1042
+ print("Full profile")
1043
+ prof.display()
1044
+ print(prof.description())
1045
+ prof.display_margin_graph()
1046
+ vm.display(prof)
1047
+ print("")
1048
+ if coalition_size == 1:
1049
+ print("Profile with voter removed")
1050
+ else:
1051
+ print(f"Profile with {coalition_size} voters removed")
1052
+ prof2 = prof2.anonymize()
1053
+ prof2.display()
1054
+ print(prof2.description())
1055
+ prof2.display_margin_graph()
1056
+ vm.display(prof2)
1057
+ print("")
1058
+
1059
+ if not uniform_coalition:
1060
+ for winner in winners:
1061
+ relevant_ranking_types = [r for r in prof.ranking_types if r[-1] == winner]
1062
+ relevant_ranking_types_counts = [prof.rankings.count(r) for r in relevant_ranking_types]
1063
+
1064
+ for coalition_rankings, coalition_rankings_counts in _submultisets_of_fixed_cardinality(relevant_ranking_types, relevant_ranking_types_counts, coalition_size):
1065
+
1066
+ rankings = prof.rankings.copy()
1067
+
1068
+ for r_idx, r in enumerate(coalition_rankings):
1069
+ for i in range(coalition_rankings_counts[r_idx]):
1070
+ rankings.remove(r)
1071
+
1072
+ if isinstance(prof, Profile):
1073
+ prof2 = Profile(rankings)
1074
+
1075
+ if isinstance(prof, ProfileWithTies):
1076
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
1077
+ if prof.using_extended_strict_preference:
1078
+ prof2.use_extended_strict_preference()
1079
+
1080
+ winners2 = vm(prof2)
1081
+
1082
+ if require_resoluteness and len(winners2) > 1:
1083
+ continue
1084
+
1085
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
1086
+ continue
1087
+
1088
+ if winner not in winners2:
1089
+ witnesses.append((winner, coalition_rankings, coalition_rankings_counts))
1090
+ if verbose:
1091
+ prof = prof.anonymize()
1092
+ print(f"{winner} wins in the full profile, but {winner} is a loser after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
1093
+ print("")
1094
+ print("Full profile")
1095
+ prof.display()
1096
+ print(prof.description())
1097
+ prof.display_margin_graph()
1098
+ vm.display(prof)
1099
+ print("")
1100
+ print(f"Profile with coalition removed")
1101
+ prof2 = prof2.anonymize()
1102
+ prof2.display()
1103
+ print(prof2.description())
1104
+ prof2.display_margin_graph()
1105
+ vm.display(prof2)
1106
+ print("")
1107
+
1108
+ # Case: B's probability of winning decreases as winning set expands
1109
+ if check_probabilities and winner in winners2 and len(winners) < len(winners2):
1110
+ witnesses.append((winner, coalition_rankings, coalition_rankings_counts))
1111
+ if verbose:
1112
+ prof = prof.anonymize()
1113
+ print(f"{winner} becomes less likely to win after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
1114
+ print("")
1115
+ print("Full profile")
1116
+ prof.display()
1117
+ print(prof.description())
1118
+ prof.display_margin_graph()
1119
+ vm.display(prof)
1120
+ print("")
1121
+ print(f"Profile with coalition removed")
1122
+ prof2 = prof2.anonymize()
1123
+ prof2.display()
1124
+ print(prof2.description())
1125
+ prof2.display_margin_graph()
1126
+ vm.display(prof2)
1127
+ print("")
1128
+
1129
+ return witnesses
1130
+
1131
+ negative_involvement = Axiom(
1132
+ "Negative Involvement",
1133
+ has_violation = has_negative_involvement_violation,
1134
+ find_all_violations = find_all_negative_involvement_violations,
1135
+ )
1136
+
1137
+ def has_positive_negative_involvement_violation(prof, vm, verbose=False, violation_type="Removal", coalition_size=1, uniform_coalition=True, require_resoluteness=False, require_uniquely_weighted=False, check_probabilities=False):
1138
+ """
1139
+ If violation_type = "Removal", returns True if removing some voter(s) who ranked a losing candidate A in first place and a winning candidate B in last place causes A to win and B to lose, witnessing a violation of positive-negative involvement.
1140
+
1141
+ If uniform_coalition = True, then only coalitions of voters with the same ranking are considered.
1142
+
1143
+ If require_resoluteness = True, then only profiles with a unique winner are considered.
1144
+
1145
+ If require_uniquely_weighted = True, then only uniquely-weighted profiles are considered.
1146
+
1147
+ If check_probabilities = True, the function also checks whether removing the voters causes the probability of their favorite winning to increase and the probability of their least favorite winning to decrease.
1148
+
1149
+ Args:
1150
+ prof: a Profile or ProfileWithTies object.
1151
+ vm (VotingMethod): A voting method to test.
1152
+ verbose (bool, default=False): If a violation is found, display the violation.
1153
+ violation_type: default is "Removal"
1154
+ coalition_size: default is 1
1155
+ uniform_coalition: default is True
1156
+ require_resoluteness: default is False
1157
+ require_uniquely_weighted: default is False
1158
+ check_probabilities: default is False
1159
+
1160
+ Returns:
1161
+ Result of the test (bool): Returns True if there is a violation and False otherwise.
1162
+ """
1163
+
1164
+ winners = vm(prof)
1165
+ non_winners = [c for c in prof.candidates if c not in winners]
1166
+
1167
+ if require_resoluteness and len(winners) > 1:
1168
+ return False
1169
+
1170
+ if require_uniquely_weighted and not prof.is_uniquely_weighted():
1171
+ return False
1172
+
1173
+ if violation_type == "Removal":
1174
+ if uniform_coalition:
1175
+ # Check for standard positive-negative involvement violations and Case 1 probability violations
1176
+ for favorite in non_winners:
1177
+ for least_favorite in winners:
1178
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == favorite and r[-1] == least_favorite and prof.rankings.count(r) >= coalition_size]
1179
+
1180
+ for r in relevant_ranking_types:
1181
+ rankings = prof.rankings
1182
+
1183
+ for i in range(coalition_size):
1184
+ rankings.remove(r) # remove coalition_size-many tokens of the type of ranking
1185
+
1186
+ if isinstance(prof, Profile):
1187
+ prof2 = Profile(rankings)
1188
+
1189
+ if isinstance(prof, ProfileWithTies):
1190
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
1191
+ if prof.using_extended_strict_preference:
1192
+ prof2.use_extended_strict_preference()
1193
+
1194
+ winners2 = vm(prof2)
1195
+
1196
+ if require_resoluteness and len(winners2) > 1:
1197
+ continue
1198
+
1199
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
1200
+ continue
1201
+
1202
+ if favorite in winners2 and least_favorite not in winners2:
1203
+ if verbose:
1204
+ prof = prof.anonymize()
1205
+ if coalition_size == 1:
1206
+ print(f"{favorite} loses and {least_favorite} wins in the full profile, but {favorite} is a winner and {least_favorite} is a loser after removing a voter with the ranking {str(r)}:")
1207
+ else:
1208
+ print(f"{favorite} loses and {least_favorite} wins in the full profile, but {favorite} is a winner and {least_favorite} is a loser after removing {coalition_size} voters with the ranking {str(r)}:")
1209
+ print("")
1210
+ print("Full profile")
1211
+ prof.display()
1212
+ print(prof.description())
1213
+ prof.display_margin_graph()
1214
+ vm.display(prof)
1215
+ print("")
1216
+ if coalition_size == 1:
1217
+ print("Profile with voter removed")
1218
+ else:
1219
+ print(f"Profile with {coalition_size} voters removed")
1220
+ prof2 = prof2.anonymize()
1221
+ prof2.display()
1222
+ print(prof2.description())
1223
+ prof2.display_margin_graph()
1224
+ vm.display(prof2)
1225
+ print("")
1226
+ return True
1227
+
1228
+ # Case 1: When removing a ballot, favorite goes from losing to winning, while least_favorite becomes less likely to win, since the winning set expands
1229
+ # Viewed in terms of adding a ballot, favorite goes from winning to losing, while least_favorite becomes more likely to win, since the winning set shrinks
1230
+ if check_probabilities and favorite in winners2 and least_favorite in winners2 and len(winners) < len(winners2):
1231
+ if verbose:
1232
+ prof = prof.anonymize()
1233
+ if coalition_size == 1:
1234
+ print(f"{favorite} becomes more likely to win and {least_favorite} becomes less likely to win after removing a voter with the ranking {str(r)}:")
1235
+ else:
1236
+ print(f"{favorite} becomes more likely to win and {least_favorite} becomes less likely to win after removing {coalition_size} voters with the ranking {str(r)}:")
1237
+ print("")
1238
+ print("Full profile")
1239
+ prof.display()
1240
+ print(prof.description())
1241
+ prof.display_margin_graph()
1242
+ vm.display(prof)
1243
+ print("")
1244
+ if coalition_size == 1:
1245
+ print("Profile with voter removed")
1246
+ else:
1247
+ print(f"Profile with {coalition_size} voters removed")
1248
+ prof2 = prof2.anonymize()
1249
+ prof2.display()
1250
+ print(prof2.description())
1251
+ prof2.display_margin_graph()
1252
+ vm.display(prof2)
1253
+ print("")
1254
+ return True
1255
+
1256
+ # Check for Case 2 probability violations (where favorite is already a winner)
1257
+ if check_probabilities:
1258
+ for favorite in winners:
1259
+ for least_favorite in winners:
1260
+ if favorite == least_favorite:
1261
+ continue
1262
+
1263
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == favorite and r[-1] == least_favorite and prof.rankings.count(r) >= coalition_size]
1264
+
1265
+ for r in relevant_ranking_types:
1266
+ rankings = prof.rankings
1267
+
1268
+ for i in range(coalition_size):
1269
+ rankings.remove(r) # remove coalition_size-many tokens of the type of ranking
1270
+
1271
+ if isinstance(prof, Profile):
1272
+ prof2 = Profile(rankings)
1273
+
1274
+ if isinstance(prof, ProfileWithTies):
1275
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
1276
+ if prof.using_extended_strict_preference:
1277
+ prof2.use_extended_strict_preference()
1278
+
1279
+ winners2 = vm(prof2)
1280
+
1281
+ if require_resoluteness and len(winners2) > 1:
1282
+ continue
1283
+
1284
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
1285
+ continue
1286
+
1287
+ # Case 2: When removing a ballot, least_favorite goes from winning to losing, while favorite remains a winner and becomes more likely to win, since the winning set shrinks
1288
+ # Viewed in terms of adding a ballot, least_favorite goes from losing to winning, while favorite becomes less likely to win, since the winning set expands
1289
+ if check_probabilities and favorite in winners and least_favorite in winners and least_favorite not in winners2 and len(winners) > len(winners2):
1290
+ if verbose:
1291
+ prof = prof.anonymize()
1292
+ if coalition_size == 1:
1293
+ print(f"{least_favorite} becomes less likely to win and {favorite} remains a winner after removing a voter with the ranking {str(r)}:")
1294
+ else:
1295
+ print(f"{least_favorite} becomes less likely to win and {favorite} remains a winner after removing {coalition_size} voters with the ranking {str(r)}:")
1296
+ print("")
1297
+ print("Full profile")
1298
+ prof.display()
1299
+ print(prof.description())
1300
+ prof.display_margin_graph()
1301
+ vm.display(prof)
1302
+ print("")
1303
+ if coalition_size == 1:
1304
+ print("Profile with voter removed")
1305
+ else:
1306
+ print(f"Profile with {coalition_size} voters removed")
1307
+ prof2 = prof2.anonymize()
1308
+ prof2.display()
1309
+ print(prof2.description())
1310
+ prof2.display_margin_graph()
1311
+ vm.display(prof2)
1312
+ print("")
1313
+ return True
1314
+
1315
+ if not uniform_coalition:
1316
+ for favorite in non_winners:
1317
+ for least_favorite in winners:
1318
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == favorite and r[-1] == least_favorite]
1319
+ relevant_ranking_types_counts = [prof.rankings.count(r) for r in relevant_ranking_types]
1320
+
1321
+ for coalition_rankings, coalition_rankings_counts in _submultisets_of_fixed_cardinality(relevant_ranking_types, relevant_ranking_types_counts, coalition_size):
1322
+
1323
+ rankings = prof.rankings
1324
+
1325
+ for r_idx, r in enumerate(coalition_rankings):
1326
+ for i in range(coalition_rankings_counts[r_idx]):
1327
+ rankings.remove(r)
1328
+
1329
+ if isinstance(prof, Profile):
1330
+ prof2 = Profile(rankings)
1331
+
1332
+ if isinstance(prof, ProfileWithTies):
1333
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
1334
+ if prof.using_extended_strict_preference:
1335
+ prof2.use_extended_strict_preference()
1336
+
1337
+ winners2 = vm(prof2)
1338
+
1339
+ if require_resoluteness and len(winners2) > 1:
1340
+ continue
1341
+
1342
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
1343
+ continue
1344
+
1345
+ if favorite in winners2 and least_favorite not in winners2:
1346
+ if verbose:
1347
+ prof = prof.anonymize()
1348
+ print(f"{favorite} loses and {least_favorite} wins in the full profile, but {favorite} is a winner and {least_favorite} is a loser after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
1349
+ print("")
1350
+ print("Full profile")
1351
+ prof.display()
1352
+ print(prof.description())
1353
+ prof.display_margin_graph()
1354
+ vm.display(prof)
1355
+ print("")
1356
+ print(f"Profile with coalition removed")
1357
+ prof2 = prof2.anonymize()
1358
+ prof2.display()
1359
+ print(prof2.description())
1360
+ prof2.display_margin_graph()
1361
+ vm.display(prof2)
1362
+ print("")
1363
+ return True
1364
+
1365
+ # Case 1: When removing a ballot, favorite goes from losing to winning, while least_favorite becomes less likely to win, since the winning set expands
1366
+ # Viewed in terms of adding a ballot, favorite goes from winning to losing, while least_favorite becomes more likely to win, since the winning set shrinks
1367
+ if check_probabilities and favorite in winners2 and least_favorite in winners2 and len(winners) < len(winners2):
1368
+ if verbose:
1369
+ prof = prof.anonymize()
1370
+ print(f"{favorite} becomes more likely to win and {least_favorite} becomes less likely to win after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
1371
+ print("")
1372
+ print("Full profile")
1373
+ prof.display()
1374
+ print(prof.description())
1375
+ prof.display_margin_graph()
1376
+ vm.display(prof)
1377
+ print("")
1378
+ print(f"Profile with coalition removed")
1379
+ prof2 = prof2.anonymize()
1380
+ prof2.display()
1381
+ print(prof2.description())
1382
+ prof2.display_margin_graph()
1383
+ vm.display(prof2)
1384
+ print("")
1385
+ return True
1386
+
1387
+ # Case 2: When removing a ballot, least_favorite goes from winning to losing, while favorite remains a winner and becomes more likely to win, since the winning set shrinks
1388
+ # Viewed in terms of adding a ballot, least_favorite goes from losing to winning, while favorite becomes less likely to win, since the winning set expands
1389
+ if check_probabilities and favorite in winners and least_favorite in winners and least_favorite not in winners2 and len(winners) > len(winners2):
1390
+ if verbose:
1391
+ prof = prof.anonymize()
1392
+ print(f"{least_favorite} becomes less likely to win and {favorite} remains a winner after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
1393
+ print("")
1394
+ print("Full profile")
1395
+ prof.display()
1396
+ print(prof.description())
1397
+ prof.display_margin_graph()
1398
+ vm.display(prof)
1399
+ print("")
1400
+ print(f"Profile with coalition removed")
1401
+ prof2 = prof2.anonymize()
1402
+ prof2.display()
1403
+ print(prof2.description())
1404
+ prof2.display_margin_graph()
1405
+ vm.display(prof2)
1406
+ print("")
1407
+ return True
1408
+
1409
+ return False
1410
+
1411
+ def find_all_positive_negative_involvement_violations(prof, vm, verbose=False, violation_type="Removal", coalition_size=1, uniform_coalition=True, require_resoluteness=False, require_uniquely_weighted=False, check_probabilities=False):
1412
+ """
1413
+ If violation_type = "Removal", returns a list of tuples (favorite, least_favorite, rankings, counts) such that removing the indicated rankings with the indicated counts causes their favorite to win and their least_favorite to lose, witnessing a violation of positive-negative involvement.
1414
+
1415
+ If uniform_coalition = True, then only coalitions of voters with the same ranking are considered.
1416
+
1417
+ If require_resoluteness = True, then only profiles with a unique winner are considered.
1418
+
1419
+ If require_uniquely_weighted = True, then only uniquely-weighted profiles are considered.
1420
+
1421
+ If check_probabilities = True, the function also checks whether removing the voters causes the probability of their favorite winning to increase and the probability of their least favorite winning to decrease.
1422
+
1423
+ Args:
1424
+ prof: a Profile or ProfileWithTies object.
1425
+ vm (VotingMethod): A voting method to test.
1426
+ verbose (bool, default=False): If a violation is found, display the violation.
1427
+ violation_type: default is "Removal"
1428
+ coalition_size: default is 1
1429
+ uniform_coalition: default is True
1430
+ require_resoluteness: default is False
1431
+ require_uniquely_weighted: default is False
1432
+ check_probabilities: default is False
1433
+
1434
+ Returns:
1435
+ A List of tuples (loser, winner, rankings, counts) witnessing violations of positive-negative involvement.
1436
+
1437
+ .. warning::
1438
+ This function is slow when uniform_coalition = False and the numbers of voters and candidates are too large.
1439
+ """
1440
+
1441
+ winners = vm(prof)
1442
+ non_winners = [c for c in prof.candidates if c not in winners]
1443
+
1444
+ witnesses = list()
1445
+
1446
+ if require_resoluteness and len(winners) > 1:
1447
+ return witnesses
1448
+
1449
+ if require_uniquely_weighted and not prof.is_uniquely_weighted():
1450
+ return witnesses
1451
+
1452
+ if violation_type == "Removal":
1453
+ if uniform_coalition:
1454
+ # Check for standard positive-negative involvement violations and Case 1 probability violations
1455
+ for favorite in non_winners:
1456
+ for least_favorite in winners:
1457
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == favorite and r[-1] == least_favorite and prof.rankings.count(r) >= coalition_size]
1458
+
1459
+ for r in relevant_ranking_types:
1460
+ rankings = prof.rankings
1461
+
1462
+ for i in range(coalition_size):
1463
+ rankings.remove(r) # remove coalition_size-many tokens of the type of ranking
1464
+
1465
+ if isinstance(prof, Profile):
1466
+ prof2 = Profile(rankings)
1467
+
1468
+ if isinstance(prof, ProfileWithTies):
1469
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
1470
+ if prof.using_extended_strict_preference:
1471
+ prof2.use_extended_strict_preference()
1472
+
1473
+ winners2 = vm(prof2)
1474
+
1475
+ if require_resoluteness and len(winners2) > 1:
1476
+ continue
1477
+
1478
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
1479
+ continue
1480
+
1481
+ if favorite in winners2 and least_favorite not in winners2:
1482
+ witnesses.append((favorite, least_favorite, [r], [coalition_size]))
1483
+ if verbose:
1484
+ prof = prof.anonymize()
1485
+ if coalition_size == 1:
1486
+ print(f"{favorite} loses and {least_favorite} wins in the full profile, but {favorite} is a winner and {least_favorite} is a loser after removing a voter with the ranking {str(r)}:")
1487
+ else:
1488
+ print(f"{favorite} loses and {least_favorite} wins in the full profile, but {favorite} is a winner and {least_favorite} is a loser after removing {coalition_size} voters with the ranking {str(r)}:")
1489
+ print("")
1490
+ print("Full profile")
1491
+ prof.display()
1492
+ print(prof.description())
1493
+ prof.display_margin_graph()
1494
+ vm.display(prof)
1495
+ print("")
1496
+ if coalition_size == 1:
1497
+ print("Profile with voter removed")
1498
+ else:
1499
+ print(f"Profile with {coalition_size} voters removed")
1500
+ prof2 = prof2.anonymize()
1501
+ prof2.display()
1502
+ print(prof2.description())
1503
+ prof2.display_margin_graph()
1504
+ vm.display(prof2)
1505
+ print("")
1506
+
1507
+ # Case 1: When removing a ballot, favorite goes from losing to winning, while least_favorite becomes less likely to win, since the winning set expands
1508
+ # Viewed in terms of adding a ballot, favorite goes from winning to losing, while least_favorite becomes more likely to win, since the winning set shrinks
1509
+ if check_probabilities and favorite in winners2 and least_favorite in winners2 and len(winners) < len(winners2):
1510
+ witnesses.append((favorite, least_favorite, [r], [coalition_size]))
1511
+ if verbose:
1512
+ prof = prof.anonymize()
1513
+ if coalition_size == 1:
1514
+ print(f"{favorite} becomes more likely to win and {least_favorite} becomes less likely to win after removing a voter with the ranking {str(r)}:")
1515
+ else:
1516
+ print(f"{favorite} becomes more likely to win and {least_favorite} becomes less likely to win after removing {coalition_size} voters with the ranking {str(r)}:")
1517
+ print("")
1518
+ print("Full profile")
1519
+ prof.display()
1520
+ print(prof.description())
1521
+ prof.display_margin_graph()
1522
+ vm.display(prof)
1523
+ print("")
1524
+ if coalition_size == 1:
1525
+ print("Profile with voter removed")
1526
+ else:
1527
+ print(f"Profile with {coalition_size} voters removed")
1528
+ prof2 = prof2.anonymize()
1529
+ prof2.display()
1530
+ print(prof2.description())
1531
+ prof2.display_margin_graph()
1532
+ vm.display(prof2)
1533
+ print("")
1534
+
1535
+ # Check for Case 2 probability violations (where favorite is already a winner)
1536
+ if check_probabilities:
1537
+ for favorite in winners:
1538
+ for least_favorite in winners:
1539
+ if favorite == least_favorite:
1540
+ continue
1541
+
1542
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == favorite and r[-1] == least_favorite and prof.rankings.count(r) >= coalition_size]
1543
+
1544
+ for r in relevant_ranking_types:
1545
+ rankings = prof.rankings
1546
+
1547
+ for i in range(coalition_size):
1548
+ rankings.remove(r) # remove coalition_size-many tokens of the type of ranking
1549
+
1550
+ if isinstance(prof, Profile):
1551
+ prof2 = Profile(rankings)
1552
+
1553
+ if isinstance(prof, ProfileWithTies):
1554
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
1555
+ if prof.using_extended_strict_preference:
1556
+ prof2.use_extended_strict_preference()
1557
+
1558
+ winners2 = vm(prof2)
1559
+
1560
+ if require_resoluteness and len(winners2) > 1:
1561
+ continue
1562
+
1563
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
1564
+ continue
1565
+
1566
+ # Case 2: When removing a ballot, least_favorite goes from winning to losing, while favorite remains a winner and becomes more likely to win, since the winning set shrinks
1567
+ # Viewed in terms of adding a ballot, least_favorite goes from losing to winning, while favorite becomes less likely to win, since the winning set expands
1568
+ if check_probabilities and favorite in winners and least_favorite in winners and least_favorite not in winners2 and len(winners) > len(winners2):
1569
+ witnesses.append((favorite, least_favorite, [r], [coalition_size]))
1570
+ if verbose:
1571
+ prof = prof.anonymize()
1572
+ if coalition_size == 1:
1573
+ print(f"{least_favorite} becomes less likely to win and {favorite} remains a winner after removing a voter with the ranking {str(r)}:")
1574
+ else:
1575
+ print(f"{least_favorite} becomes less likely to win and {favorite} remains a winner after removing {coalition_size} voters with the ranking {str(r)}:")
1576
+ print("")
1577
+ print("Full profile")
1578
+ prof.display()
1579
+ print(prof.description())
1580
+ prof.display_margin_graph()
1581
+ vm.display(prof)
1582
+ print("")
1583
+ if coalition_size == 1:
1584
+ print("Profile with voter removed")
1585
+ else:
1586
+ print(f"Profile with {coalition_size} voters removed")
1587
+ prof2 = prof2.anonymize()
1588
+ prof2.display()
1589
+ print(prof2.description())
1590
+ prof2.display_margin_graph()
1591
+ vm.display(prof2)
1592
+ print("")
1593
+
1594
+ if not uniform_coalition:
1595
+ # Check for standard positive-negative involvement violations and Case 1 probability violations
1596
+ for favorite in non_winners:
1597
+ for least_favorite in winners:
1598
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == favorite and r[-1] == least_favorite]
1599
+ relevant_ranking_types_counts = [prof.rankings.count(r) for r in relevant_ranking_types]
1600
+
1601
+ for coalition_rankings, coalition_rankings_counts in _submultisets_of_fixed_cardinality(relevant_ranking_types, relevant_ranking_types_counts, coalition_size):
1602
+
1603
+ rankings = prof.rankings
1604
+
1605
+ for r_idx, r in enumerate(coalition_rankings):
1606
+ for i in range(coalition_rankings_counts[r_idx]):
1607
+ rankings.remove(r)
1608
+
1609
+ if isinstance(prof, Profile):
1610
+ prof2 = Profile(rankings)
1611
+
1612
+ if isinstance(prof, ProfileWithTies):
1613
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
1614
+ if prof.using_extended_strict_preference:
1615
+ prof2.use_extended_strict_preference()
1616
+
1617
+ winners2 = vm(prof2)
1618
+
1619
+ if require_resoluteness and len(winners2) > 1:
1620
+ continue
1621
+
1622
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
1623
+ continue
1624
+
1625
+ if favorite in winners2 and least_favorite not in winners2:
1626
+ witnesses.append((favorite, least_favorite, coalition_rankings, coalition_rankings_counts))
1627
+ if verbose:
1628
+ prof = prof.anonymize()
1629
+ print(f"{favorite} loses and {least_favorite} wins in the full profile, but {favorite} is a winner and {least_favorite} is a loser after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
1630
+ print("")
1631
+ print("Full profile")
1632
+ prof.display()
1633
+ print(prof.description())
1634
+ prof.display_margin_graph()
1635
+ vm.display(prof)
1636
+ print("")
1637
+ print(f"Profile with coalition removed")
1638
+ prof2 = prof2.anonymize()
1639
+ prof2.display()
1640
+ print(prof2.description())
1641
+ prof2.display_margin_graph()
1642
+ vm.display(prof2)
1643
+ print("")
1644
+
1645
+ # Case 1: When removing a ballot, favorite goes from losing to winning, while least_favorite becomes less likely to win, since the winning set expands
1646
+ # Viewed in terms of adding a ballot, favorite goes from winning to losing, while least_favorite becomes more likely to win, since the winning set shrinks
1647
+ if check_probabilities and favorite in winners2 and least_favorite in winners2 and len(winners) < len(winners2):
1648
+ witnesses.append((favorite, least_favorite, coalition_rankings, coalition_rankings_counts))
1649
+ if verbose:
1650
+ prof = prof.anonymize()
1651
+ print(f"{favorite} becomes more likely to win and {least_favorite} becomes less likely to win after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
1652
+ print("")
1653
+ print("Full profile")
1654
+ prof.display()
1655
+ print(prof.description())
1656
+ prof.display_margin_graph()
1657
+ vm.display(prof)
1658
+ print("")
1659
+ print(f"Profile with coalition removed")
1660
+ prof2 = prof2.anonymize()
1661
+ prof2.display()
1662
+ print(prof2.description())
1663
+ prof2.display_margin_graph()
1664
+ vm.display(prof2)
1665
+ print("")
1666
+
1667
+ # Check for Case 2 probability violations (where favorite is already a winner)
1668
+ if check_probabilities:
1669
+ for favorite in winners:
1670
+ for least_favorite in winners:
1671
+ if favorite == least_favorite:
1672
+ continue
1673
+
1674
+ relevant_ranking_types = [r for r in prof.ranking_types if r[0] == favorite and r[-1] == least_favorite]
1675
+ relevant_ranking_types_counts = [prof.rankings.count(r) for r in relevant_ranking_types]
1676
+
1677
+ for coalition_rankings, coalition_rankings_counts in _submultisets_of_fixed_cardinality(relevant_ranking_types, relevant_ranking_types_counts, coalition_size):
1678
+
1679
+ rankings = prof.rankings
1680
+
1681
+ for r_idx, r in enumerate(coalition_rankings):
1682
+ for i in range(coalition_rankings_counts[r_idx]):
1683
+ rankings.remove(r)
1684
+
1685
+ if isinstance(prof, Profile):
1686
+ prof2 = Profile(rankings)
1687
+
1688
+ if isinstance(prof, ProfileWithTies):
1689
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
1690
+ if prof.using_extended_strict_preference:
1691
+ prof2.use_extended_strict_preference()
1692
+
1693
+ winners2 = vm(prof2)
1694
+
1695
+ if require_resoluteness and len(winners2) > 1:
1696
+ continue
1697
+
1698
+ if require_uniquely_weighted and not prof2.is_uniquely_weighted():
1699
+ continue
1700
+
1701
+ # Case 2: When removing a ballot, least_favorite goes from winning to losing, while favorite remains a winner and becomes more likely to win, since the winning set shrinks
1702
+ # Viewed in terms of adding a ballot, least_favorite goes from losing to winning, while favorite becomes less likely to win, since the winning set expands
1703
+ if check_probabilities and favorite in winners and least_favorite in winners and least_favorite not in winners2 and len(winners) > len(winners2):
1704
+ witnesses.append((favorite, least_favorite, coalition_rankings, coalition_rankings_counts))
1705
+ if verbose:
1706
+ prof = prof.anonymize()
1707
+ print(f"{least_favorite} becomes less likely to win and {favorite} remains a winner after removing a {coalition_size}-voter coalition with the rankings {[str(r) for r in coalition_rankings]} and counts {coalition_rankings_counts}:")
1708
+ print("")
1709
+ print("Full profile")
1710
+ prof.display()
1711
+ print(prof.description())
1712
+ prof.display_margin_graph()
1713
+ vm.display(prof)
1714
+ print("")
1715
+ print(f"Profile with coalition removed")
1716
+ prof2 = prof2.anonymize()
1717
+ prof2.display()
1718
+ print(prof2.description())
1719
+ prof2.display_margin_graph()
1720
+ vm.display(prof2)
1721
+ print("")
1722
+
1723
+ return witnesses
1724
+
1725
+ positive_negative_involvement = Axiom(
1726
+ "Positive-Negative Involvement",
1727
+ has_violation=has_positive_negative_involvement_violation,
1728
+ find_all_violations=find_all_positive_negative_involvement_violations
1729
+ )
1730
+
1731
+ def has_tolerant_positive_involvement_violation(prof, vm, verbose=False, violation_type="Removal"):
1732
+ """
1733
+ If violation_type = "Removal", returns True if it is possible to cause a loser A to win by removing some voter who ranked A above every candidate B such that A is not majority preferred to B, witnessing a violation of tolerant positive involvement.
1734
+
1735
+ ..note:
1736
+ A strengthening of positive involvement, introduced in https://arxiv.org/abs/2210.12503
1737
+
1738
+ Args:
1739
+ prof: a Profile or ProfileWithTies object.
1740
+ vm (VotingMethod): A voting method to test.
1741
+ verbose (bool, default=False): If a violation is found, display the violation.
1742
+ violation_type: default is "Removal"
1743
+
1744
+ Returns:
1745
+ Result of the test (bool): Returns True if there is a violation and False otherwise."""
1746
+
1747
+ if isinstance(prof, ProfileWithTies):
1748
+ prof.use_extended_strict_preference()
1749
+
1750
+ winners = vm(prof)
1751
+ losers = [c for c in prof.candidates if c not in winners]
1752
+
1753
+ if violation_type == "Removal":
1754
+ for loser in losers:
1755
+ for r in prof.ranking_types: # for each type of ranking
1756
+
1757
+ rankings = prof.rankings
1758
+ rankings.remove(r) # remove the first token of the type of ranking
1759
+
1760
+ if isinstance(prof, Profile):
1761
+ prof2 = Profile(rankings)
1762
+
1763
+ if isinstance(prof, ProfileWithTies):
1764
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
1765
+ if prof.using_extended_strict_preference:
1766
+ prof2.use_extended_strict_preference()
1767
+
1768
+ tolerant_ballot = True
1769
+
1770
+ # check whether the loser is ranked above every candiddate c such that the loser is not majority preferred to c
1771
+ for c in prof.candidates:
1772
+ if c != loser and not prof2.majority_prefers(loser, c):
1773
+ # Handle different ranking types
1774
+ if isinstance(r, tuple):
1775
+ # Profile case: r is a tuple, use index comparison
1776
+ if r.index(c) < r.index(loser):
1777
+ tolerant_ballot = False
1778
+ break
1779
+ else:
1780
+ # ProfileWithTies case: r is a Ranking object, use strict_pref method
1781
+ if not r.strict_pref(loser, c):
1782
+ tolerant_ballot = False
1783
+ break
1784
+
1785
+ if tolerant_ballot:
1786
+ if loser in vm(prof2):
1787
+ if verbose:
1788
+ prof = prof.anonymize()
1789
+ ranking_str = str(r) if hasattr(r, '__str__') else str(r)
1790
+ print(f"{loser} loses in the full profile, but {loser} is a winner after removing a voter with the ranking {ranking_str}:")
1791
+ print("")
1792
+ print("Full profile")
1793
+ prof.display()
1794
+ print(prof.description())
1795
+ prof.display_margin_graph()
1796
+ vm.display(prof)
1797
+ print("")
1798
+ print("Profile with voter removed")
1799
+ prof2 = prof2.anonymize()
1800
+ prof2.display()
1801
+ print(prof2.description())
1802
+ prof2.display_margin_graph()
1803
+ vm.display(prof2)
1804
+ print("")
1805
+ return True
1806
+
1807
+ return False
1808
+
1809
+ def find_all_tolerant_positive_involvement_violations(prof, vm, verbose=False, violation_type="Removal"):
1810
+ """
1811
+ If violation_type = "Removal", returns a list of pairs (loser,ranking) such that removing a voter with the given ranking causes the loser to win, witnessing a violation of tolerant positive involvement.
1812
+
1813
+ Args:
1814
+ prof: a Profile or ProfileWithTies object.
1815
+ vm (VotingMethod): A voting method to test.
1816
+ verbose (bool, default=False): If a violation is found, display the violation.
1817
+ violation_type: default is "Removal"
1818
+
1819
+ Returns:
1820
+ A List of pairs (loser,ranking) witnessing violations of positive involvement."""
1821
+
1822
+ if isinstance(prof, ProfileWithTies):
1823
+ prof.use_extended_strict_preference()
1824
+
1825
+ winners = vm(prof)
1826
+ losers = [c for c in prof.candidates if c not in winners]
1827
+
1828
+ witnesses = list()
1829
+
1830
+ if violation_type == "Removal":
1831
+ for loser in losers:
1832
+ for r in prof.ranking_types: # for each type of ranking
1833
+
1834
+ rankings = prof.rankings
1835
+ rankings.remove(r) # remove the first token of the type of ranking
1836
+
1837
+ if isinstance(prof, Profile):
1838
+ prof2 = Profile(rankings)
1839
+
1840
+ if isinstance(prof, ProfileWithTies):
1841
+ prof2 = ProfileWithTies(rankings, candidates=prof.candidates)
1842
+ if prof.using_extended_strict_preference:
1843
+ prof2.use_extended_strict_preference()
1844
+
1845
+ tolerant_ballot = True
1846
+
1847
+ # check whether the loser is ranked above every candiddate c such that the loser is not majority preferred to c
1848
+ for c in prof.candidates:
1849
+ if c != loser and not prof2.majority_prefers(loser, c):
1850
+ # Handle different ranking types
1851
+ if isinstance(r, tuple):
1852
+ # Profile case: r is a tuple, use index comparison
1853
+ if r.index(c) < r.index(loser):
1854
+ tolerant_ballot = False
1855
+ break
1856
+ else:
1857
+ # ProfileWithTies case: r is a Ranking object, use strict_pref method
1858
+ if not r.strict_pref(loser, c):
1859
+ tolerant_ballot = False
1860
+ break
1861
+
1862
+ if tolerant_ballot:
1863
+ if loser in vm(prof2):
1864
+ witnesses.append((loser, r))
1865
+ if verbose:
1866
+ prof = prof.anonymize()
1867
+ ranking_str = str(r) if hasattr(r, '__str__') else str(r)
1868
+ print(f"{loser} loses in the full profile, but {loser} is a winner after removing a voter with the ranking {ranking_str}:")
1869
+ print("")
1870
+ print("Full profile")
1871
+ prof.display()
1872
+ print(prof.description())
1873
+ prof.display_margin_graph()
1874
+ vm.display(prof)
1875
+ print("")
1876
+ print("Profile with voter removed")
1877
+ prof2 = prof2.anonymize()
1878
+ prof2.display()
1879
+ print(prof2.description())
1880
+ prof2.display_margin_graph()
1881
+ vm.display(prof2)
1882
+ print("")
1883
+ return witnesses
1884
+
1885
+ tolerant_positive_involvement = Axiom(
1886
+ "Tolerant Positive Involvement",
1887
+ has_violation = has_tolerant_positive_involvement_violation,
1888
+ find_all_violations = find_all_tolerant_positive_involvement_violations,
1889
+ )
1890
+
1891
+ def has_bullet_vote_positive_involvement_violation(prof, vm, verbose=False, coalition_size = 1, require_resoluteness = False, require_uniquely_weighted = False, check_probabilities = False):
1892
+ """
1893
+ Returns True if it is possible to cause a winner A to lose by adding coalition_size-many new voters who bullet vote for A.
1894
+
1895
+ If require_resoluteness = True, then only profiles with a unique winner are considered.
1896
+
1897
+ If require_uniquely_weighted = True, then only uniquely-weighted profiles are considered.
1898
+
1899
+ If check_probabilities = True, then the function also checks whether adding coalition_size-many new voters who bullet vote for A causes A's probability of winning to decrease (in the case of a tie broken by even-chance tiebreaking).
1900
+
1901
+ Args:
1902
+ prof: a Profile or ProfileWithTies object.
1903
+ vm (VotingMethod): A voting method to test.
1904
+ verbose (bool, default=False): If a violation is found, display the violation.
1905
+ violation_type: default is "Removal"
1906
+
1907
+ Returns:
1908
+ Result of the test (bool): Returns True if there is a violation and False otherwise."""
1909
+
1910
+ if require_uniquely_weighted == True and not prof.is_uniquely_weighted():
1911
+ return False
1912
+
1913
+ ws = vm(prof)
1914
+
1915
+ if require_resoluteness == True and len(ws) > 1:
1916
+ return False
1917
+
1918
+ for w in ws:
1919
+ if isinstance(prof, Profile):
1920
+ new_prof = ProfileWithTies([{c:c_indx+1 for c_indx, c in enumerate(r)} for r in prof.rankings] + [{w:1}] * coalition_size, candidates = prof.candidates)
1921
+ new_prof.use_extended_strict_preference()
1922
+ new_mg = new_prof.margin_graph()
1923
+
1924
+ if isinstance(prof, ProfileWithTies):
1925
+ new_prof = ProfileWithTies(prof.rankings + [{w:1}] * coalition_size, candidates = prof.candidates)
1926
+ new_prof.use_extended_strict_preference()
1927
+ new_mg = new_prof.margin_graph()
1928
+
1929
+ if require_uniquely_weighted == True and not new_mg.is_uniquely_weighted():
1930
+ continue
1931
+
1932
+ new_ws = vm(new_prof)
1933
+
1934
+ if require_resoluteness == True and len(new_ws) > 1:
1935
+ continue
1936
+
1937
+ if w not in new_ws:
1938
+ if verbose:
1939
+ prof = prof.anonymize()
1940
+ new_prof = new_prof.anonymize()
1941
+ print(f"Violation of Bullet Vote Positive Involvement for {vm.name}")
1942
+ print("Original profile:")
1943
+ prof.display()
1944
+ print(prof.description())
1945
+ prof.display_margin_graph()
1946
+ vm.display(prof)
1947
+ print("New profile:")
1948
+ new_prof.display()
1949
+ print(new_prof.description())
1950
+ new_prof.display_margin_graph()
1951
+ vm.display(new_prof)
1952
+ print("")
1953
+
1954
+ return True
1955
+
1956
+ if check_probabilities and len(new_ws) > len(ws):
1957
+
1958
+ if verbose:
1959
+ prof = prof.anonymize()
1960
+ new_prof = new_prof.anonymize()
1961
+ print(f"Violation of Probabilistic Bullet Vote Positive Involvement for {vm.name}")
1962
+ print("Original profile:")
1963
+ prof.display()
1964
+ print(prof.description())
1965
+ prof.display_margin_graph()
1966
+ vm.display(prof)
1967
+ print("New profile:")
1968
+ new_prof.display()
1969
+ print(new_prof.description())
1970
+ new_prof.display_margin_graph()
1971
+ vm.display(new_prof)
1972
+ print("")
1973
+
1974
+ return True
1975
+
1976
+ return False
1977
+
1978
+ def find_all_bullet_vote_positive_involvement_violations(prof, vm, verbose=False, coalition_size = 1, require_resoluteness = False, require_uniquely_weighted = False, check_probabilities = False):
1979
+ """
1980
+ Returns a list of candidates who win in the given profile but lose after adding coalition_size-many new voters who bullet vote for them.
1981
+
1982
+ If require_resoluteness = True, then only profiles with a unique winner are considered.
1983
+
1984
+ If require_uniquely_weighted = True, then only uniquely-weighted profiles are considered.
1985
+
1986
+ If check_probabilities = True, then the function also checks whether adding coalition_size-many new voters who bullet vote for A causes A's probability of winning to decrease (in the case of a tie broken by even-chance tiebreaking).
1987
+
1988
+ Args:
1989
+ prof: a Profile or ProfileWithTies object.
1990
+ vm (VotingMethod): A voting method to test.
1991
+ verbose (bool, default=False): If a violation is found, display the violation.
1992
+
1993
+ Returns:
1994
+ A List of candidates who win in the given profile but lose after adding coalition_size-many new voters who bullet vote for them.
1995
+
1996
+ """
1997
+
1998
+ if require_uniquely_weighted == True and not prof.is_uniquely_weighted():
1999
+ return False
2000
+
2001
+ ws = vm(prof)
2002
+
2003
+ if require_resoluteness == True and len(ws) > 1:
2004
+ return False
2005
+
2006
+ violations = list()
2007
+
2008
+ for w in ws:
2009
+ if isinstance(prof, Profile):
2010
+ new_prof = ProfileWithTies([{c:c_indx+1 for c_indx, c in enumerate(r)} for r in prof.rankings] + [{w:1}] * coalition_size, candidates = prof.candidates)
2011
+ new_prof.use_extended_strict_preference()
2012
+ new_mg = new_prof.margin_graph()
2013
+ if isinstance(prof, ProfileWithTies):
2014
+ new_prof = ProfileWithTies(prof.rankings + [{w:1}] * coalition_size, candidates = prof.candidates)
2015
+ new_prof.use_extended_strict_preference()
2016
+ new_mg = new_prof.margin_graph()
2017
+
2018
+ if require_uniquely_weighted == True and not new_mg.is_uniquely_weighted():
2019
+ continue
2020
+
2021
+ new_ws = vm(new_prof)
2022
+
2023
+ if require_resoluteness == True and len(new_ws) > 1:
2024
+ continue
2025
+
2026
+ if w not in new_ws:
2027
+ if verbose:
2028
+ prof = prof.anonymize()
2029
+ new_prof = new_prof.anonymize()
2030
+ print(f"Violation of Bullet Vote Positive Involvement for {vm.name}")
2031
+ print("Original profile:")
2032
+ prof.display()
2033
+ print(prof.description())
2034
+ prof.display_margin_graph()
2035
+ vm.display(prof)
2036
+ print("New profile:")
2037
+ new_prof.display()
2038
+ print(new_prof.description())
2039
+ new_prof.display_margin_graph()
2040
+ vm.display(new_prof)
2041
+ print("")
2042
+
2043
+ violations.append(w)
2044
+
2045
+ if check_probabilities and len(new_ws) > len(ws):
2046
+
2047
+ if verbose:
2048
+ prof = prof.anonymize()
2049
+ new_prof = new_prof.anonymize()
2050
+ print(f"Violation of Probabilistic Bullet Vote Positive Involvement for {vm.name}")
2051
+ print("Original profile:")
2052
+ prof.display()
2053
+ print(prof.description())
2054
+ prof.display_margin_graph()
2055
+ vm.display(prof)
2056
+ print("New profile:")
2057
+ new_prof.display()
2058
+ print(new_prof.description())
2059
+ new_prof.display_margin_graph()
2060
+ vm.display(new_prof)
2061
+ print("")
2062
+
2063
+ violations.append(w)
2064
+
2065
+ return violations
2066
+
2067
+ bullet_vote_positive_involvement = Axiom(
2068
+ "Bullet Vote Positive Involvement",
2069
+ has_violation = has_bullet_vote_positive_involvement_violation,
2070
+ find_all_violations = find_all_bullet_vote_positive_involvement_violations,
2071
+ )
2072
+
2073
+ def has_semi_positive_involvement_violation(prof, vm, verbose=False):
2074
+ """
2075
+ Semi-Positive Involvement says that if A wins in an initial profile, and we add a voter with a truncated ballot
2076
+ ranking A first, then it cannot happen that all of the winners in the new profile are unranked by the truncated ballot.
2077
+
2078
+ Rather than adding a ballot, this function removes a ballot from the profile and then adds a truncated version of the ballot.
2079
+
2080
+ The function returns True if there is a ballot such that starting from an updated version of prof
2081
+ with the ballot removed, adding a truncated version of the ballot causes the winner(s)
2082
+ to shift from the top-ranked candidate (plus possibly other ranked candidates) to some unranked candidate(s).
2083
+
2084
+ Args:
2085
+ prof: a Profile or ProfileWithTies object.
2086
+ vm (VotingMethod): A voting method to test.
2087
+ verbose (bool, default=False): If a violation is found, display the violation.
2088
+
2089
+ Returns:
2090
+ Result of the test (bool): Returns True if there is a violation and False otherwise.
2091
+ """
2092
+
2093
+ if isinstance(prof, Profile):
2094
+ # For each ranking in the profile
2095
+ for r_idx, r in enumerate(prof.rankings):
2096
+ # Create a profile with this ballot removed
2097
+ removed_ballot_rankings = prof.rankings.copy()
2098
+ removed_ballot_rankings.pop(r_idx)
2099
+ removed_ballot_prof = Profile(removed_ballot_rankings)
2100
+
2101
+ # Get winners when ballot is removed
2102
+ removed_ballot_winners = vm(removed_ballot_prof)
2103
+
2104
+ # For each possible truncation of the ballot
2105
+ for truncation_level in range(1, len(prof.candidates)):
2106
+ # Create a truncated ballot with only the top truncation_level candidates
2107
+ ranked_candidates = r[:truncation_level]
2108
+ unranked_candidates = [c for c in prof.candidates if c not in ranked_candidates]
2109
+
2110
+ # Get the top-ranked candidate
2111
+ top_ranked_candidate = r[0] if len(r) > 0 else None
2112
+
2113
+ # Create a ranking object for the truncated ballot
2114
+ truncated_ballot = {c: i+1 for i, c in enumerate(ranked_candidates)}
2115
+
2116
+ # Create a profile with the truncated ballot
2117
+ # Convert tuples to dictionaries for ProfileWithTies
2118
+ converted_rankings = []
2119
+ for ranking in removed_ballot_rankings:
2120
+ converted_rankings.append({c: i+1 for i, c in enumerate(ranking)})
2121
+
2122
+ # Add the truncated ballot to the converted rankings
2123
+ converted_rankings.append(truncated_ballot)
2124
+ truncated_ballot_prof = ProfileWithTies(converted_rankings, candidates=prof.candidates)
2125
+ truncated_ballot_prof.use_extended_strict_preference()
2126
+
2127
+ # Get winners when truncated ballot is added
2128
+ truncated_ballot_winners = vm(truncated_ballot_prof)
2129
+
2130
+ # Check if winners shifted from top-ranked+other-ranked to unranked candidates
2131
+ # The winners in the profile without the truncated ballot must include the top-ranked candidate
2132
+ winners_include_top_ranked = top_ranked_candidate in removed_ballot_winners
2133
+ # All winners in the profile with the truncated ballot must be among the unranked candidates
2134
+ winners_from_unranked = all(c in unranked_candidates for c in truncated_ballot_winners)
2135
+
2136
+ if winners_include_top_ranked and winners_from_unranked:
2137
+ if verbose:
2138
+ print(f"Semi-Positive Involvement violation found:")
2139
+ print("Original profile:")
2140
+ prof.display()
2141
+ print(prof.description())
2142
+ vm.display(prof)
2143
+ print("\nProfile with ballot removed:")
2144
+ removed_ballot_prof.display()
2145
+ print(removed_ballot_prof.description())
2146
+ vm.display(removed_ballot_prof)
2147
+ print("\nMargin graph of profile with ballot removed:")
2148
+ removed_ballot_prof.display_margin_graph()
2149
+ print(f"Winners: {removed_ballot_winners}")
2150
+ print(f"\nTruncated ballot: {truncated_ballot}")
2151
+ print(f"Top-ranked candidate: {top_ranked_candidate}")
2152
+ print(f"Ranked candidates: {ranked_candidates}")
2153
+ print(f"Unranked candidates: {unranked_candidates}")
2154
+ print("\nProfile with truncated ballot:")
2155
+ truncated_ballot_prof.display()
2156
+ print(truncated_ballot_prof.description())
2157
+ vm.display(truncated_ballot_prof)
2158
+ print("\nMargin graph of profile with truncated ballot:")
2159
+ truncated_ballot_prof.display_margin_graph()
2160
+ print(f"Winners: {truncated_ballot_winners}")
2161
+ print(f"\nWinners shifted from including top-ranked candidate {top_ranked_candidate} to unranked candidates {truncated_ballot_winners}")
2162
+ return True
2163
+
2164
+ elif isinstance(prof, ProfileWithTies):
2165
+ # For each ranking in the profile
2166
+ rankings, rcounts = prof.rankings_counts
2167
+
2168
+ for r_idx, (r, count) in enumerate(zip(rankings, rcounts)):
2169
+ # Skip if this is not a single ballot
2170
+ if count != 1:
2171
+ continue
2172
+
2173
+ # Create a profile with this ballot removed
2174
+ removed_ballot_rankings = rankings.copy()
2175
+ removed_ballot_rcounts = rcounts.copy()
2176
+ removed_ballot_rankings.pop(r_idx)
2177
+ removed_ballot_rcounts.pop(r_idx)
2178
+ removed_ballot_prof = ProfileWithTies(removed_ballot_rankings, rcounts=removed_ballot_rcounts, candidates=prof.candidates)
2179
+ if prof.using_extended_strict_preference:
2180
+ removed_ballot_prof.use_extended_strict_preference()
2181
+
2182
+ # Get winners when ballot is removed
2183
+ removed_ballot_winners = vm(removed_ballot_prof)
2184
+
2185
+ # For each possible truncation of the ballot
2186
+ ranked_candidates = r.cands
2187
+ for truncation_level in range(1, len(ranked_candidates)):
2188
+ # Get the candidates at each rank
2189
+ candidates_by_rank = [r.cands_at_rank(rank) for rank in r.ranks]
2190
+
2191
+ # Take only the first truncation_level ranks
2192
+ truncated_ranked_candidates = [c for rank_candidates in candidates_by_rank[:truncation_level] for c in rank_candidates]
2193
+ unranked_candidates = [c for c in prof.candidates if c not in truncated_ranked_candidates]
2194
+
2195
+ # Get the top-ranked candidates (those at rank 1)
2196
+ top_ranked_candidates = candidates_by_rank[0] if len(candidates_by_rank) > 0 else []
2197
+
2198
+ # Create a ranking object for the truncated ballot
2199
+ truncated_ballot = {c: r.rmap[c] for c in truncated_ranked_candidates}
2200
+
2201
+ # Create a profile with the truncated ballot
2202
+ truncated_ballot_rankings = removed_ballot_rankings + [Ranking(truncated_ballot)]
2203
+ truncated_ballot_rcounts = removed_ballot_rcounts + [1]
2204
+ truncated_ballot_prof = ProfileWithTies(truncated_ballot_rankings, rcounts=truncated_ballot_rcounts, candidates=prof.candidates)
2205
+ if prof.using_extended_strict_preference:
2206
+ truncated_ballot_prof.use_extended_strict_preference()
2207
+
2208
+ # Get winners when truncated ballot is added
2209
+ truncated_ballot_winners = vm(truncated_ballot_prof)
2210
+
2211
+ # Check if winners shifted from top-ranked+other-ranked to unranked candidates
2212
+ # The winners in the profile without the truncated ballot must include at least one top-ranked candidate
2213
+ winners_include_top_ranked = any(c in top_ranked_candidates for c in removed_ballot_winners)
2214
+ # All winners in the profile with the truncated ballot must be among the unranked candidates
2215
+ winners_from_unranked = all(c in unranked_candidates for c in truncated_ballot_winners)
2216
+
2217
+ if winners_include_top_ranked and winners_from_unranked:
2218
+ if verbose:
2219
+ print(f"Semi-Positive Involvement violation found:")
2220
+ print("Original profile:")
2221
+ prof.display()
2222
+ print(prof.description())
2223
+ vm.display(prof)
2224
+ print("\nProfile with ballot removed:")
2225
+ removed_ballot_prof.display()
2226
+ print(removed_ballot_prof.description())
2227
+ vm.display(removed_ballot_prof)
2228
+ print("\nMargin graph of profile with ballot removed:")
2229
+ removed_ballot_prof.display_margin_graph()
2230
+ print(f"Winners: {removed_ballot_winners}")
2231
+ print(f"\nTruncated ballot: {truncated_ballot}")
2232
+ print(f"Top-ranked candidates: {top_ranked_candidates}")
2233
+ print(f"Ranked candidates: {truncated_ranked_candidates}")
2234
+ print(f"Unranked candidates: {unranked_candidates}")
2235
+ print("\nProfile with truncated ballot:")
2236
+ truncated_ballot_prof.display()
2237
+ print(truncated_ballot_prof.description())
2238
+ vm.display(truncated_ballot_prof)
2239
+ print("\nMargin graph of profile with truncated ballot:")
2240
+ truncated_ballot_prof.display_margin_graph()
2241
+ print(f"Winners: {truncated_ballot_winners}")
2242
+ print(f"\nWinners shifted from including top-ranked candidate(s) {[prof.cmap[c] for c in removed_ballot_winners if c in top_ranked_candidates]} to unranked candidates {[prof.cmap[c] for c in truncated_ballot_winners]}")
2243
+ return True
2244
+
2245
+ return False
2246
+
2247
+ def find_all_semi_positive_involvement_violations(prof, vm, verbose=False):
2248
+ """
2249
+ Returns all violations of semi-positive involvement for a given profile and voting method.
2250
+
2251
+ Args:
2252
+ prof: a Profile or ProfileWithTies object.
2253
+ vm (VotingMethod): A voting method to test.
2254
+ verbose (bool, default=False): If a violation is found, display the violation.
2255
+
2256
+ Returns:
2257
+ A list of tuples (ballot, truncated_ballot, removed_ballot_winners, truncated_ballot_winners) where each tuple represents a violation.
2258
+ """
2259
+
2260
+ violations = []
2261
+
2262
+ if isinstance(prof, Profile):
2263
+ # For each ranking in the profile
2264
+ for r_idx, r in enumerate(prof.rankings):
2265
+ # Create a profile with this ballot removed
2266
+ removed_ballot_rankings = prof.rankings.copy()
2267
+ removed_ballot_rankings.pop(r_idx)
2268
+ removed_ballot_prof = Profile(removed_ballot_rankings)
2269
+
2270
+ # Get winners when ballot is removed
2271
+ removed_ballot_winners = vm(removed_ballot_prof)
2272
+
2273
+ # For each possible truncation of the ballot
2274
+ for truncation_level in range(1, len(prof.candidates)):
2275
+ # Create a truncated ballot with only the top truncation_level candidates
2276
+ ranked_candidates = r[:truncation_level]
2277
+ unranked_candidates = [c for c in prof.candidates if c not in ranked_candidates]
2278
+
2279
+ # Get the top-ranked candidate
2280
+ top_ranked_candidate = r[0] if len(r) > 0 else None
2281
+
2282
+ # Create a ranking object for the truncated ballot
2283
+ truncated_ballot = {c: i+1 for i, c in enumerate(ranked_candidates)}
2284
+
2285
+ # Create a profile with the truncated ballot
2286
+ # Convert tuples to dictionaries for ProfileWithTies
2287
+ converted_rankings = []
2288
+ for ranking in removed_ballot_rankings:
2289
+ converted_rankings.append({c: i+1 for i, c in enumerate(ranking)})
2290
+
2291
+ # Add the truncated ballot to the converted rankings
2292
+ converted_rankings.append(truncated_ballot)
2293
+ truncated_ballot_prof = ProfileWithTies(converted_rankings, candidates=prof.candidates)
2294
+ truncated_ballot_prof.use_extended_strict_preference()
2295
+
2296
+ # Get winners when truncated ballot is added
2297
+ truncated_ballot_winners = vm(truncated_ballot_prof)
2298
+
2299
+ # Check if winners shifted from top-ranked+other-ranked to unranked candidates
2300
+ # The winners in the profile without the truncated ballot must include the top-ranked candidate
2301
+ winners_include_top_ranked = top_ranked_candidate in removed_ballot_winners
2302
+ # All winners in the profile with the truncated ballot must be among the unranked candidates
2303
+ winners_from_unranked = all(c in unranked_candidates for c in truncated_ballot_winners)
2304
+
2305
+ if winners_include_top_ranked and winners_from_unranked:
2306
+ violations.append((r, truncated_ballot, removed_ballot_winners, truncated_ballot_winners))
2307
+ if verbose:
2308
+ print(f"Semi-Positive Involvement violation found:")
2309
+ print("")
2310
+ print("Original profile:")
2311
+ prof.display()
2312
+ print(prof.description())
2313
+ vm.display(prof)
2314
+ print("\nProfile with ballot removed:")
2315
+ removed_ballot_prof.display()
2316
+ print(removed_ballot_prof.description())
2317
+ vm.display(removed_ballot_prof)
2318
+ print("\nMargin graph of profile with ballot removed:")
2319
+ removed_ballot_prof.display_margin_graph()
2320
+ print(f"Winners: {removed_ballot_winners}")
2321
+ print(f"\nTruncated ballot: {truncated_ballot}")
2322
+ print(f"Top-ranked candidate: {top_ranked_candidate}")
2323
+ print(f"Ranked candidates: {ranked_candidates}")
2324
+ print(f"Unranked candidates: {unranked_candidates}")
2325
+ print("\nProfile with truncated ballot:")
2326
+ truncated_ballot_prof.display()
2327
+ print(truncated_ballot_prof.description())
2328
+ vm.display(truncated_ballot_prof)
2329
+ print("\nMargin graph of profile with truncated ballot:")
2330
+ truncated_ballot_prof.display_margin_graph()
2331
+ print(f"Winners: {truncated_ballot_winners}")
2332
+ print(f"\nWinners shifted from including top-ranked candidate {top_ranked_candidate} to unranked candidates {truncated_ballot_winners}")
2333
+
2334
+ elif isinstance(prof, ProfileWithTies):
2335
+ # For each ranking in the profile
2336
+ rankings, rcounts = prof.rankings_counts
2337
+
2338
+ for r_idx, (r, count) in enumerate(zip(rankings, rcounts)):
2339
+ # Skip if this is not a single ballot
2340
+ if count != 1:
2341
+ continue
2342
+
2343
+ # Create a profile with this ballot removed
2344
+ removed_ballot_rankings = rankings.copy()
2345
+ removed_ballot_rcounts = rcounts.copy()
2346
+ removed_ballot_rankings.pop(r_idx)
2347
+ removed_ballot_rcounts.pop(r_idx)
2348
+ removed_ballot_prof = ProfileWithTies(removed_ballot_rankings, rcounts=removed_ballot_rcounts, candidates=prof.candidates)
2349
+ if prof.using_extended_strict_preference:
2350
+ removed_ballot_prof.use_extended_strict_preference()
2351
+
2352
+ # Get winners when ballot is removed
2353
+ removed_ballot_winners = vm(removed_ballot_prof)
2354
+
2355
+ # For each possible truncation of the ballot
2356
+ ranked_candidates = r.cands
2357
+ for truncation_level in range(1, len(ranked_candidates)):
2358
+ # Get the candidates at each rank
2359
+ candidates_by_rank = [r.cands_at_rank(rank) for rank in r.ranks]
2360
+
2361
+ # Take only the first truncation_level ranks
2362
+ truncated_ranked_candidates = [c for rank_candidates in candidates_by_rank[:truncation_level] for c in rank_candidates]
2363
+ unranked_candidates = [c for c in prof.candidates if c not in truncated_ranked_candidates]
2364
+
2365
+ # Get the top-ranked candidates (those at rank 1)
2366
+ top_ranked_candidates = candidates_by_rank[0] if len(candidates_by_rank) > 0 else []
2367
+
2368
+ # Create a ranking object for the truncated ballot
2369
+ truncated_ballot = {c: r.rmap[c] for c in truncated_ranked_candidates}
2370
+
2371
+ # Create a profile with the truncated ballot
2372
+ truncated_ballot_rankings = removed_ballot_rankings + [Ranking(truncated_ballot)]
2373
+ truncated_ballot_rcounts = removed_ballot_rcounts + [1]
2374
+ truncated_ballot_prof = ProfileWithTies(truncated_ballot_rankings, rcounts=truncated_ballot_rcounts, candidates=prof.candidates)
2375
+ if prof.using_extended_strict_preference:
2376
+ truncated_ballot_prof.use_extended_strict_preference()
2377
+
2378
+ # Get winners when truncated ballot is added
2379
+ truncated_ballot_winners = vm(truncated_ballot_prof)
2380
+
2381
+ # Check if winners shifted from top-ranked+other-ranked to unranked candidates
2382
+ # The winners in the profile without the truncated ballot must include at least one top-ranked candidate
2383
+ winners_include_top_ranked = any(c in top_ranked_candidates for c in removed_ballot_winners)
2384
+ # All winners in the profile with the truncated ballot must be among the unranked candidates
2385
+ winners_from_unranked = all(c in unranked_candidates for c in truncated_ballot_winners)
2386
+
2387
+ if winners_include_top_ranked and winners_from_unranked:
2388
+ violations.append((r, Ranking(truncated_ballot), removed_ballot_winners, truncated_ballot_winners))
2389
+ if verbose:
2390
+ print(f"Semi-Positive Involvement violation found:")
2391
+ print("")
2392
+ print("Original profile:")
2393
+ prof.display()
2394
+ print(prof.description())
2395
+ vm.display(prof)
2396
+ print("\nProfile with ballot removed:")
2397
+ removed_ballot_prof.display()
2398
+ print(removed_ballot_prof.description())
2399
+ vm.display(removed_ballot_prof)
2400
+ print("\nMargin graph of profile with ballot removed:")
2401
+ removed_ballot_prof.display_margin_graph()
2402
+ print(f"Winners: {removed_ballot_winners}")
2403
+ print(f"\nTruncated ballot: {truncated_ballot}")
2404
+ print(f"Top-ranked candidates: {top_ranked_candidates}")
2405
+ print(f"Ranked candidates: {truncated_ranked_candidates}")
2406
+ print(f"Unranked candidates: {unranked_candidates}")
2407
+ print("\nProfile with truncated ballot:")
2408
+ truncated_ballot_prof.display()
2409
+ print(truncated_ballot_prof.description())
2410
+ vm.display(truncated_ballot_prof)
2411
+ print("\nMargin graph of profile with truncated ballot:")
2412
+ truncated_ballot_prof.display_margin_graph()
2413
+ print(f"Winners: {truncated_ballot_winners}")
2414
+ print(f"\nWinners shifted from including top-ranked candidate(s) {[prof.cmap[c] for c in removed_ballot_winners if c in top_ranked_candidates]} to unranked candidates {[prof.cmap[c] for c in truncated_ballot_winners]}")
2415
+
2416
+ return violations
2417
+
2418
+ semi_positive_involvement = Axiom(
2419
+ "Semi-Positive Involvement",
2420
+ has_violation = has_semi_positive_involvement_violation,
2421
+ find_all_violations = find_all_semi_positive_involvement_violations,
2422
+ )
2423
+
2424
+ def has_truncated_involvement_violation(prof, vm, verbose=False):
2425
+ """
2426
+ Returns True if there is a ballot such that starting from an updated version of prof with the ballot removed, adding a truncated version of the ballot causes the winners to shift from ranked candidates to unranked candidates.
2427
+
2428
+ Args:
2429
+ prof: a Profile or ProfileWithTies object.
2430
+ vm (VotingMethod): A voting method to test.
2431
+ verbose (bool, default=False): If a violation is found, display the violation.
2432
+
2433
+ Returns:
2434
+ Result of the test (bool): Returns True if there is a violation and False otherwise.
2435
+ """
2436
+
2437
+ if isinstance(prof, Profile):
2438
+ # For each ranking in the profile
2439
+ for r_idx, r in enumerate(prof.rankings):
2440
+ # Create a profile with this ballot removed
2441
+ removed_ballot_rankings = prof.rankings.copy()
2442
+ removed_ballot_rankings.pop(r_idx)
2443
+ removed_ballot_prof = Profile(removed_ballot_rankings)
2444
+
2445
+ # Get winners when ballot is removed
2446
+ removed_ballot_winners = vm(removed_ballot_prof)
2447
+
2448
+ # For each possible truncation of the ballot
2449
+ for truncation_level in range(1, len(prof.candidates)):
2450
+ # Create a truncated ballot with only the top truncation_level candidates
2451
+ ranked_candidates = r[:truncation_level]
2452
+ unranked_candidates = [c for c in prof.candidates if c not in ranked_candidates]
2453
+
2454
+ # Create a ranking object for the truncated ballot
2455
+ truncated_ballot = {c: i+1 for i, c in enumerate(ranked_candidates)}
2456
+
2457
+ # Create a profile with the truncated ballot
2458
+ # Convert tuples to dictionaries for ProfileWithTies
2459
+ converted_rankings = []
2460
+ for ranking in removed_ballot_rankings:
2461
+ converted_rankings.append({c: i+1 for i, c in enumerate(ranking)})
2462
+
2463
+ # Add the truncated ballot to the converted rankings
2464
+ converted_rankings.append(truncated_ballot)
2465
+ truncated_ballot_prof = ProfileWithTies(converted_rankings, candidates=prof.candidates)
2466
+ truncated_ballot_prof.use_extended_strict_preference()
2467
+
2468
+ # Get winners when truncated ballot is added
2469
+ truncated_ballot_winners = vm(truncated_ballot_prof)
2470
+
2471
+ # Check if winners shifted from ranked to unranked candidates
2472
+ # All winners in the profile without the truncated ballot must be among the ranked candidates
2473
+ winners_from_ranked = all(c in ranked_candidates for c in removed_ballot_winners)
2474
+ # All winners in the profile with the truncated ballot must be among the unranked candidates
2475
+ winners_from_unranked = all(c in unranked_candidates for c in truncated_ballot_winners)
2476
+
2477
+ if winners_from_ranked and winners_from_unranked:
2478
+ if verbose:
2479
+ print(f"Truncated Involvement violation found:")
2480
+ print("")
2481
+ print("Original profile:")
2482
+ prof.display()
2483
+ print(prof.description())
2484
+ vm.display(prof)
2485
+ print("\nProfile with ballot removed:")
2486
+ removed_ballot_prof.display()
2487
+ print(removed_ballot_prof.description())
2488
+ vm.display(removed_ballot_prof)
2489
+ print("\nMargin graph of profile with ballot removed:")
2490
+ removed_ballot_prof.display_margin_graph()
2491
+ print("\nProfile with truncated ballot:")
2492
+ truncated_ballot_prof.display()
2493
+ print(truncated_ballot_prof.description())
2494
+ vm.display(truncated_ballot_prof)
2495
+ print("\nMargin graph of profile with truncated ballot:")
2496
+ truncated_ballot_prof.display_margin_graph()
2497
+ print(f"\nTruncated ballot: {truncated_ballot}")
2498
+ print(f"Ranked candidates: {ranked_candidates}")
2499
+ print(f"Unranked candidates: {unranked_candidates}")
2500
+ print(f"Winners: {truncated_ballot_winners}")
2501
+ print(f"\nWinners shifted from ranked candidates {[prof.cmap[c] for c in removed_ballot_winners if c in ranked_candidates]} to unranked candidates {[prof.cmap[c] for c in truncated_ballot_winners]}")
2502
+ return True
2503
+
2504
+ elif isinstance(prof, ProfileWithTies):
2505
+ # For each ranking in the profile
2506
+ rankings, rcounts = prof.rankings_counts
2507
+
2508
+ for r_idx, (r, count) in enumerate(zip(rankings, rcounts)):
2509
+ # Skip if this is not a single ballot
2510
+ if count != 1:
2511
+ continue
2512
+
2513
+ # Create a profile with this ballot removed
2514
+ removed_ballot_rankings = rankings.copy()
2515
+ removed_ballot_rcounts = rcounts.copy()
2516
+ removed_ballot_rankings.pop(r_idx)
2517
+ removed_ballot_rcounts.pop(r_idx)
2518
+ removed_ballot_prof = ProfileWithTies(removed_ballot_rankings, rcounts=removed_ballot_rcounts, candidates=prof.candidates)
2519
+ if prof.using_extended_strict_preference:
2520
+ removed_ballot_prof.use_extended_strict_preference()
2521
+
2522
+ # Get winners when ballot is removed
2523
+ removed_ballot_winners = vm(removed_ballot_prof)
2524
+
2525
+ # For each possible truncation of the ballot
2526
+ ranked_candidates = r.cands
2527
+ for truncation_level in range(1, len(ranked_candidates)):
2528
+ # Get the candidates at each rank
2529
+ candidates_by_rank = [r.cands_at_rank(rank) for rank in r.ranks]
2530
+
2531
+ # Take only the first truncation_level ranks
2532
+ truncated_ranked_candidates = [c for rank_candidates in candidates_by_rank[:truncation_level] for c in rank_candidates]
2533
+ unranked_candidates = [c for c in prof.candidates if c not in truncated_ranked_candidates]
2534
+
2535
+ # Create a ranking object for the truncated ballot
2536
+ truncated_ballot = {c: r.rmap[c] for c in truncated_ranked_candidates}
2537
+
2538
+ # Create a profile with the truncated ballot
2539
+ truncated_ballot_rankings = removed_ballot_rankings + [Ranking(truncated_ballot)]
2540
+ truncated_ballot_rcounts = removed_ballot_rcounts + [1]
2541
+ truncated_ballot_prof = ProfileWithTies(truncated_ballot_rankings, rcounts=truncated_ballot_rcounts, candidates=prof.candidates)
2542
+ if prof.using_extended_strict_preference:
2543
+ truncated_ballot_prof.use_extended_strict_preference()
2544
+
2545
+ # Get winners when truncated ballot is added
2546
+ truncated_ballot_winners = vm(truncated_ballot_prof)
2547
+
2548
+ # Check if winners shifted from ranked to unranked candidates
2549
+ winners_from_ranked = any(c in truncated_ranked_candidates for c in removed_ballot_winners)
2550
+ winners_from_unranked = all(c in unranked_candidates for c in truncated_ballot_winners)
2551
+
2552
+ if winners_from_ranked and winners_from_unranked:
2553
+ if verbose:
2554
+ print(f"Truncated Involvement violation found:")
2555
+ print("")
2556
+ print("Original profile:")
2557
+ prof.display()
2558
+ print(prof.description())
2559
+ vm.display(prof)
2560
+ print("\nProfile with ballot removed:")
2561
+ removed_ballot_prof.display()
2562
+ print(removed_ballot_prof.description())
2563
+ vm.display(removed_ballot_prof)
2564
+ print("\nMargin graph of profile with ballot removed:")
2565
+ removed_ballot_prof.display_margin_graph()
2566
+ print("\nProfile with truncated ballot:")
2567
+ truncated_ballot_prof.display()
2568
+ print(truncated_ballot_prof.description())
2569
+ vm.display(truncated_ballot_prof)
2570
+ print("\nMargin graph of profile with truncated ballot:")
2571
+ truncated_ballot_prof.display_margin_graph()
2572
+ print(f"\nTruncated ballot: {truncated_ballot}")
2573
+ print(f"Ranked candidates: {truncated_ranked_candidates}")
2574
+ print(f"Unranked candidates: {unranked_candidates}")
2575
+ print(f"Winners: {truncated_ballot_winners}")
2576
+ print(f"\nWinners shifted from ranked candidates {[prof.cmap[c] for c in removed_ballot_winners if c in truncated_ranked_candidates]} to unranked candidates {[prof.cmap[c] for c in truncated_ballot_winners]}")
2577
+ return True
2578
+
2579
+ return False
2580
+
2581
+ def find_all_truncated_involvement_violations(prof, vm, verbose=False):
2582
+ """
2583
+ Returns all violations of truncated involvement for a given profile and voting method.
2584
+
2585
+ Args:
2586
+ prof: a Profile or ProfileWithTies object.
2587
+ vm (VotingMethod): A voting method to test.
2588
+ verbose (bool, default=False): If a violation is found, display the violation.
2589
+
2590
+ Returns:
2591
+ A list of tuples (ballot, truncated_ballot, removed_ballot_winners, truncated_ballot_winners) where each tuple represents a violation.
2592
+ """
2593
+
2594
+ violations = []
2595
+
2596
+ if isinstance(prof, Profile):
2597
+ # For each ranking in the profile
2598
+ for r_idx, r in enumerate(prof.rankings):
2599
+ # Create a profile with this ballot removed
2600
+ removed_ballot_rankings = prof.rankings.copy()
2601
+ removed_ballot_rankings.pop(r_idx)
2602
+ removed_ballot_prof = Profile(removed_ballot_rankings)
2603
+
2604
+ # Get winners when ballot is removed
2605
+ removed_ballot_winners = vm(removed_ballot_prof)
2606
+
2607
+ # For each possible truncation of the ballot
2608
+ for truncation_level in range(1, len(prof.candidates)):
2609
+ # Create a truncated ballot with only the top truncation_level candidates
2610
+ ranked_candidates = r[:truncation_level]
2611
+ unranked_candidates = [c for c in prof.candidates if c not in ranked_candidates]
2612
+
2613
+ # Create a ranking object for the truncated ballot
2614
+ truncated_ballot = {c: i+1 for i, c in enumerate(ranked_candidates)}
2615
+
2616
+ # Create a profile with the truncated ballot
2617
+ # Convert tuples to dictionaries for ProfileWithTies
2618
+ converted_rankings = []
2619
+ for ranking in removed_ballot_rankings:
2620
+ converted_rankings.append({c: i+1 for i, c in enumerate(ranking)})
2621
+
2622
+ # Add the truncated ballot to the converted rankings
2623
+ converted_rankings.append(truncated_ballot)
2624
+ truncated_ballot_prof = ProfileWithTies(converted_rankings, candidates=prof.candidates)
2625
+ truncated_ballot_prof.use_extended_strict_preference()
2626
+
2627
+ # Get winners when truncated ballot is added
2628
+ truncated_ballot_winners = vm(truncated_ballot_prof)
2629
+
2630
+ # Check if winners shifted from ranked to unranked candidates
2631
+ # All winners in the profile without the truncated ballot must be among the ranked candidates
2632
+ winners_from_ranked = all(c in ranked_candidates for c in removed_ballot_winners)
2633
+ # All winners in the profile with the truncated ballot must be among the unranked candidates
2634
+ winners_from_unranked = all(c in unranked_candidates for c in truncated_ballot_winners)
2635
+
2636
+ if winners_from_ranked and winners_from_unranked:
2637
+ violations.append((r, truncated_ballot, removed_ballot_winners, truncated_ballot_winners))
2638
+ if verbose:
2639
+ print(f"Truncated Involvement violation found:")
2640
+ print("")
2641
+ print("Original profile:")
2642
+ prof.display()
2643
+ print(prof.description())
2644
+ vm.display(prof)
2645
+ print("\nProfile with ballot removed:")
2646
+ removed_ballot_prof.display()
2647
+ print(removed_ballot_prof.description())
2648
+ vm.display(removed_ballot_prof)
2649
+ print("\nMargin graph of profile with ballot removed:")
2650
+ removed_ballot_prof.display_margin_graph()
2651
+ print("\nProfile with truncated ballot:")
2652
+ truncated_ballot_prof.display()
2653
+ print(truncated_ballot_prof.description())
2654
+ vm.display(truncated_ballot_prof)
2655
+ print("\nMargin graph of profile with truncated ballot:")
2656
+ truncated_ballot_prof.display_margin_graph()
2657
+ print(f"\nWinners shifted from ranked candidates {[prof.cmap[c] for c in removed_ballot_winners if c in ranked_candidates]} to unranked candidates {[prof.cmap[c] for c in truncated_ballot_winners]}")
2658
+
2659
+ elif isinstance(prof, ProfileWithTies):
2660
+ # For each ranking in the profile
2661
+ rankings, rcounts = prof.rankings_counts
2662
+
2663
+ for r_idx, (r, count) in enumerate(zip(rankings, rcounts)):
2664
+ # Skip if this is not a single ballot
2665
+ if count != 1:
2666
+ continue
2667
+
2668
+ # Create a profile with this ballot removed
2669
+ removed_ballot_rankings = rankings.copy()
2670
+ removed_ballot_rcounts = rcounts.copy()
2671
+ removed_ballot_rankings.pop(r_idx)
2672
+ removed_ballot_rcounts.pop(r_idx)
2673
+ removed_ballot_prof = ProfileWithTies(removed_ballot_rankings, rcounts=removed_ballot_rcounts, candidates=prof.candidates)
2674
+ if prof.using_extended_strict_preference:
2675
+ removed_ballot_prof.use_extended_strict_preference()
2676
+
2677
+ # Get winners when ballot is removed
2678
+ removed_ballot_winners = vm(removed_ballot_prof)
2679
+
2680
+ # For each possible truncation of the ballot
2681
+ ranked_candidates = r.cands
2682
+ for truncation_level in range(1, len(ranked_candidates)):
2683
+ # Get the candidates at each rank
2684
+ candidates_by_rank = [r.cands_at_rank(rank) for rank in r.ranks]
2685
+
2686
+ # Take only the first truncation_level ranks
2687
+ truncated_ranked_candidates = [c for rank_candidates in candidates_by_rank[:truncation_level] for c in rank_candidates]
2688
+ unranked_candidates = [c for c in prof.candidates if c not in truncated_ranked_candidates]
2689
+
2690
+ # Create a ranking object for the truncated ballot
2691
+ truncated_ballot = {c: r.rmap[c] for c in truncated_ranked_candidates}
2692
+
2693
+ # Create a profile with the truncated ballot
2694
+ truncated_ballot_rankings = removed_ballot_rankings + [Ranking(truncated_ballot)]
2695
+ truncated_ballot_rcounts = removed_ballot_rcounts + [1]
2696
+ truncated_ballot_prof = ProfileWithTies(truncated_ballot_rankings, rcounts=truncated_ballot_rcounts, candidates=prof.candidates)
2697
+ if prof.using_extended_strict_preference:
2698
+ truncated_ballot_prof.use_extended_strict_preference()
2699
+
2700
+ # Get winners when truncated ballot is added
2701
+ truncated_ballot_winners = vm(truncated_ballot_prof)
2702
+
2703
+ # Check if winners shifted from ranked to unranked candidates
2704
+ winners_from_ranked = any(c in truncated_ranked_candidates for c in removed_ballot_winners)
2705
+ winners_from_unranked = all(c in unranked_candidates for c in truncated_ballot_winners)
2706
+
2707
+ if winners_from_ranked and winners_from_unranked:
2708
+ violations.append((r, Ranking(truncated_ballot), removed_ballot_winners, truncated_ballot_winners))
2709
+ if verbose:
2710
+ print(f"Truncated Involvement violation found:")
2711
+ print("")
2712
+ print("Original profile:")
2713
+ prof.display()
2714
+ print(prof.description())
2715
+ vm.display(prof)
2716
+ print("\nProfile with ballot removed:")
2717
+ removed_ballot_prof.display()
2718
+ print(removed_ballot_prof.description())
2719
+ vm.display(removed_ballot_prof)
2720
+ print("\nMargin graph of profile with ballot removed:")
2721
+ removed_ballot_prof.display_margin_graph()
2722
+ print("\nProfile with truncated ballot:")
2723
+ truncated_ballot_prof.display()
2724
+ print(truncated_ballot_prof.description())
2725
+ vm.display(truncated_ballot_prof)
2726
+ print("\nMargin graph of profile with truncated ballot:")
2727
+ truncated_ballot_prof.display_margin_graph()
2728
+ print(f"\nWinners shifted from ranked candidates {[prof.cmap[c] for c in removed_ballot_winners if c in truncated_ranked_candidates]} to unranked candidates {[prof.cmap[c] for c in truncated_ballot_winners]}")
2729
+
2730
+ return violations
2731
+
2732
+ truncated_involvement = Axiom(
2733
+ "Truncated Involvement",
2734
+ has_violation = has_truncated_involvement_violation,
2735
+ find_all_violations = find_all_truncated_involvement_violations,
2736
+ )
2737
+
2738
+ def has_participation_violation(prof, vm, verbose = False, violation_type = "Removal", coalition_size = 1, uniform_coalition = True, set_preference = "single-winner"):
2739
+ """
2740
+ If violation_type = "Removal", returns True if removing some voter(s) from prof changes the vm winning set such that the (each) voter prefers the new winner(s) to the original winner(s), according to the set_preference relation.
2741
+
2742
+ If violation_type = "Addition", returns True if adding some voter(s) from prof changes the vm winning set such that the (each) voter prefers the original winner(s) to the new winner(s), according to the set_preference relation.
2743
+
2744
+ If coalition_size > 1, checks for a violation involving a coalition of voters acting together.
2745
+
2746
+ If uniform_coalition = True, all voters in the coalition must have the same ranking.
2747
+
2748
+ If set_preference = "single-winner", a voter prefers a set A of candidates to a set B of candidates if A and B are singletons and the voter ranks the candidate in A above the candidate in B.
2749
+
2750
+ If set_preference = "weak-dominance", a voter prefers a set A to a set B if in their sincere ranking, all candidates in A are weakly above all candidates in B and some candidate in A is strictly above some candidate in B.
2751
+
2752
+ If set_preference = "optimist", a voter prefers a set A to a set B if in their sincere ranking, their favorite from A is above their favorite from B.
2753
+
2754
+ If set_preference = "pessimist", a voter prefers a set A to a set B if in their sincere ranking, their least favorite from A is above their least favorite from B.
2755
+
2756
+ Args:
2757
+ prof: a Profile or ProfileWithTies object.
2758
+ vm (VotingMethod): A voting method to test.
2759
+ verbose (bool, default=False): If a violation is found, display the violation.
2760
+ violation_type: default is "Removal"
2761
+ coalition_size: default is 1
2762
+ uniform_coalition: default is True
2763
+ set_preference: default is "single-winner". Other options are "weak-dominance", "optimist", and "pessimist".
2764
+
2765
+ Returns:
2766
+ Result of the test (bool): Returns True if there is a violation and False otherwise.
2767
+
2768
+ """
2769
+
2770
+ winners = vm(prof)
2771
+
2772
+ if isinstance(prof,ProfileWithTies):
2773
+ prof.use_extended_strict_preference()
2774
+
2775
+ found_manipulator = False
2776
+
2777
+ ranking_types = prof.ranking_types
2778
+
2779
+ ws = vm(prof)
2780
+
2781
+ if set_preference == "single-winner":
2782
+ if len(ws) > 1:
2783
+ return False
2784
+
2785
+ if uniform_coalition:
2786
+
2787
+ if violation_type == "Removal":
2788
+
2789
+ relevant_ranking_types = [r for r in prof.ranking_types if prof.rankings.count(r) >= coalition_size]
2790
+
2791
+ for r in relevant_ranking_types:
2792
+ if not found_manipulator:
2793
+
2794
+ ranking_tokens = [r for r in prof.rankings]
2795
+
2796
+ for i in range(coalition_size):
2797
+ ranking_tokens.remove(r) # remove coalition_size-many tokens of the type of ranking
2798
+
2799
+ if isinstance(prof,Profile):
2800
+
2801
+ new_prof = Profile(ranking_tokens)
2802
+ new_ws = vm(new_prof)
2803
+
2804
+ old_winner_to_compare = None
2805
+ new_winner_to_compare = None
2806
+
2807
+ if set_preference == "single-winner" and len(new_ws) == 1:
2808
+
2809
+ old_winner_to_compare = ws[0]
2810
+ new_winner_to_compare = new_ws[0]
2811
+
2812
+ elif set_preference == "weak-dominance":
2813
+ r_as_ranking = Ranking({c: i for i, c in enumerate(r)})
2814
+
2815
+ elif set_preference == "optimist":
2816
+
2817
+ old_winner_to_compare = [cand for cand in r if cand in ws][0]
2818
+ new_winner_to_compare = [cand for cand in r if cand in new_ws][0]
2819
+
2820
+ elif set_preference == "pessimist":
2821
+
2822
+ old_winner_to_compare = [cand for cand in r if cand in ws][-1]
2823
+ new_winner_to_compare = [cand for cand in r if cand in new_ws][-1]
2824
+
2825
+ if old_winner_to_compare is not None and r.index(old_winner_to_compare) > r.index(new_winner_to_compare) or (set_preference == "weak-dominance" and r_as_ranking.weak_dom(new_ws,ws)):
2826
+
2827
+ found_manipulator = True
2828
+
2829
+ if verbose:
2830
+ prof = prof.anonymize()
2831
+ new_prof = new_prof.anonymize()
2832
+ print(f"Violation of Participation for {vm.name} under the {set_preference} set preference.")
2833
+ if coalition_size == 1:
2834
+ print(f"A voter with the ranking {r} can benefit by abstaining.")
2835
+ else:
2836
+ print(f"{coalition_size} voters with the ranking {r} can benefit by jointly abstaining.")
2837
+ print("")
2838
+ print("Original Profile:")
2839
+ prof.display()
2840
+ print(prof.description())
2841
+ print("")
2842
+ vm.display(prof)
2843
+ prof.display_margin_graph()
2844
+ print("")
2845
+ if coalition_size == 1:
2846
+ print("Profile if the voter abstains:")
2847
+ else:
2848
+ print("Profile if the voters abstain:")
2849
+ new_prof.display()
2850
+ print(new_prof.description())
2851
+ print("")
2852
+ vm.display(new_prof)
2853
+ new_prof.display_margin_graph()
2854
+
2855
+ if isinstance(prof,ProfileWithTies):
2856
+ r_dict = r.rmap
2857
+
2858
+ new_prof = ProfileWithTies(ranking_tokens, candidates = prof.candidates)
2859
+ new_prof.use_extended_strict_preference()
2860
+ new_ws = vm(new_prof)
2861
+
2862
+ ranked_old_winners = [c for c in ws if c in r_dict.keys()]
2863
+ ranked_new_winners = [c for c in new_ws if c in r_dict.keys()]
2864
+
2865
+ rank_of_old_winner_to_compare = None
2866
+ rank_of_new_winner_to_compare = None
2867
+
2868
+ if set_preference == "single-winner" and len(new_ws) == 1:
2869
+
2870
+ rank_of_old_winner_to_compare = r_dict[ws[0]] if ranked_old_winners else math.inf
2871
+ rank_of_new_winner_to_compare = r_dict[new_ws[0]] if ranked_new_winners else math.inf
2872
+
2873
+ elif set_preference == "optimist":
2874
+
2875
+ rank_of_old_winner_to_compare = min([r_dict[c] for c in ranked_old_winners]) if ranked_old_winners else math.inf
2876
+ rank_of_new_winner_to_compare = min([r_dict[c] for c in ranked_new_winners]) if ranked_new_winners else math.inf
2877
+
2878
+ elif set_preference == "pessimist":
2879
+
2880
+ rank_of_old_winner_to_compare = max([r_dict[c] for c in ranked_old_winners]) if ranked_old_winners == ws else math.inf
2881
+ rank_of_new_winner_to_compare = max([r_dict[c] for c in ranked_new_winners]) if ranked_new_winners == new_ws else math.inf
2882
+
2883
+ if rank_of_old_winner_to_compare is not None and rank_of_old_winner_to_compare > rank_of_new_winner_to_compare or (set_preference == "weak-dominance" and r.weak_dom(new_ws,ws,use_extended_preferences=True)):
2884
+
2885
+ found_manipulator = True
2886
+
2887
+ if verbose:
2888
+ prof = prof.anonymize()
2889
+ new_prof = new_prof.anonymize()
2890
+ print(f"Violation of Participation for {vm.name} under the {set_preference} set preference.")
2891
+ if coalition_size == 1:
2892
+ print(f"A voter with the ranking {r} can benefit by abstaining.")
2893
+ else:
2894
+ print(f"{coalition_size} voters with the ranking {r} can benefit by jointly abstaining.")
2895
+ print("")
2896
+ print("Original Profile:")
2897
+ prof.display()
2898
+ print(prof.description())
2899
+ print("")
2900
+ vm.display(prof)
2901
+ prof.display_margin_graph()
2902
+ print("")
2903
+ if coalition_size == 1:
2904
+ print("Profile if the voter abstains:")
2905
+ else:
2906
+ print("Profile if the voters abstain:")
2907
+ new_prof.display()
2908
+ print(new_prof.description())
2909
+ print("")
2910
+ vm.display(new_prof)
2911
+ new_prof.display_margin_graph()
2912
+
2913
+ if violation_type == "Addition":
2914
+
2915
+ if isinstance(prof,Profile):
2916
+
2917
+ for new_r in permutations(prof.candidates):
2918
+ if not found_manipulator:
2919
+
2920
+ new_ranking_tokens = [r for r in prof.rankings]
2921
+
2922
+ for i in range(coalition_size):
2923
+ new_ranking_tokens.append(new_r)
2924
+
2925
+ new_prof = Profile(new_ranking_tokens)
2926
+ new_ws = vm(new_prof)
2927
+
2928
+ old_winner_to_compare = None
2929
+ new_winner_to_compare = None
2930
+
2931
+ if set_preference == "single-winner" and len(new_ws) == 1:
2932
+
2933
+ old_winner_to_compare = ws[0]
2934
+ new_winner_to_compare = new_ws[0]
2935
+
2936
+ elif set_preference == "weak-dominance":
2937
+ new_r_as_ranking = Ranking({c: i for i, c in enumerate(new_r)})
2938
+
2939
+ elif set_preference == "optimist":
2940
+
2941
+ old_winner_to_compare = [cand for cand in new_r if cand in ws][0]
2942
+ new_winner_to_compare = [cand for cand in new_r if cand in new_ws][0]
2943
+
2944
+ elif set_preference == "pessimist":
2945
+
2946
+ old_winner_to_compare = [cand for cand in new_r if cand in ws][-1]
2947
+ new_winner_to_compare = [cand for cand in new_r if cand in new_ws][-1]
2948
+
2949
+ if old_winner_to_compare is not None and new_r.index(old_winner_to_compare) < new_r.index(new_winner_to_compare) or (set_preference == "weak-dominance" and new_r_as_ranking.weak_dom(ws,new_ws)):
2950
+
2951
+ found_manipulator = True
2952
+
2953
+ if verbose:
2954
+ prof = prof.anonymize()
2955
+ new_prof = new_prof.anonymize()
2956
+ print(f"Violation of Participation for {vm.name} under the {set_preference} set preference.")
2957
+ if coalition_size == 1:
2958
+ print(f"A new voter who joins with the ranking {new_r} will wish they had abstained.")
2959
+ else:
2960
+ print(f"{coalition_size} new voters who join with the ranking {new_r} will wish they had jointly abstained.")
2961
+ print("")
2962
+ print("Original Profile without voter(s):")
2963
+ prof.display()
2964
+ print(prof.description())
2965
+ print("")
2966
+ vm.display(prof)
2967
+ prof.display_margin_graph()
2968
+ print("")
2969
+ print("New Profile with voter(s) added:")
2970
+ new_prof.display()
2971
+ print(new_prof.description())
2972
+ print("")
2973
+ vm.display(new_prof)
2974
+ new_prof.display_margin_graph()
2975
+
2976
+ if isinstance(prof,ProfileWithTies):
2977
+
2978
+ for _new_r in weak_orders(prof.candidates):
2979
+ new_r = Ranking(_new_r)
2980
+ new_r_dict = new_r.rmap
2981
+
2982
+ if not found_manipulator:
2983
+
2984
+ new_ranking_tokens = [r for r in prof.rankings]
2985
+
2986
+ for i in range(coalition_size):
2987
+ new_ranking_tokens.append(new_r)
2988
+
2989
+ new_prof = ProfileWithTies(new_ranking_tokens, candidates = prof.candidates)
2990
+ new_prof.use_extended_strict_preference()
2991
+ new_ws = vm(new_prof)
2992
+
2993
+ ranked_old_winners = [c for c in ws if c in new_r_dict.keys()]
2994
+ ranked_new_winners = [c for c in new_ws if c in new_r_dict.keys()]
2995
+
2996
+ rank_of_old_winner_to_compare = None
2997
+ rank_of_new_winner_to_compare = None
2998
+
2999
+ if set_preference == "single-winner" and len(new_ws) == 1:
3000
+
3001
+ rank_of_old_winner_to_compare = new_r_dict[ws[0]] if ranked_old_winners else math.inf
3002
+ rank_of_new_winner_to_compare = new_r_dict[new_ws[0]] if ranked_new_winners else math.inf
3003
+
3004
+ elif set_preference == "optimist":
3005
+
3006
+ rank_of_old_winner_to_compare = min([new_r_dict[c] for c in ranked_old_winners]) if ranked_old_winners else math.inf
3007
+ rank_of_new_winner_to_compare = min([new_r_dict[c] for c in ranked_new_winners]) if ranked_new_winners else math.inf
3008
+
3009
+ elif set_preference == "pessimist":
3010
+
3011
+ rank_of_old_winner_to_compare = max([new_r_dict[c] for c in ranked_old_winners]) if ranked_old_winners == ws else math.inf
3012
+ rank_of_new_winner_to_compare = max([new_r_dict[c] for c in ranked_new_winners]) if ranked_new_winners == new_ws else math.inf
3013
+
3014
+ if rank_of_old_winner_to_compare is not None and rank_of_old_winner_to_compare < rank_of_new_winner_to_compare or (set_preference == "weak-dominance" and r.weak_dom(ws,new_ws,use_extended_preferences=True)):
3015
+
3016
+ found_manipulator = True
3017
+
3018
+ if verbose:
3019
+ prof = prof.anonymize()
3020
+ new_prof = new_prof.anonymize()
3021
+ print(f"Violation of Participation for {vm.name} under the {set_preference} set preference.")
3022
+ if coalition_size == 1:
3023
+ print(f"A new voter who joins with the ranking {new_r} will wish they had abstained.")
3024
+ else:
3025
+ print(f"{coalition_size} new voters who join with the ranking {new_r} will wish they had jointly abstained.")
3026
+ print("")
3027
+ print("Original Profile without voter(s):")
3028
+ prof.display()
3029
+ print(prof.description())
3030
+ print("")
3031
+ vm.display(prof)
3032
+ prof.display_margin_graph()
3033
+ print("")
3034
+ print("New Profile with voter(s) added:")
3035
+ new_prof.display()
3036
+ print(new_prof.description())
3037
+ print("")
3038
+ vm.display(new_prof)
3039
+ new_prof.display_margin_graph()
3040
+
3041
+ return found_manipulator
3042
+
3043
+ def find_all_participation_violations(prof, vm, verbose = False, violation_type = "Removal", coalition_size = 1, uniform_coalition = True, set_preference = "single-winner"):
3044
+ """
3045
+ Returns a list of tuples (preferred_winners, dispreferred_winners, ranking) witnessing violations of participation.
3046
+
3047
+ If violation_type = "Removal", returns a list of tuples (preferred_winners, dispreferred_winners, ranking) such that removing coalition_size-many voters with the given ranking changes the winning set from the preferred_winners to the dispreferred_winners, according to the set_preference relation.
3048
+
3049
+ If violation_type = "Addition", returns a list of tuples (preferred_winners, dispreferred_winners, ranking) such that adding coalition_size-many voters with the given ranking changes the winning set from the preferred_winners to the dispreferred_winners, according to the set_preference relation.
3050
+
3051
+ If coalition_size > 1, checks for a violation involving a coalition of voters acting together.
3052
+
3053
+ If uniform_coalition = True, all voters in the coalition must have the same ranking.
3054
+
3055
+ If set_preference = "single-winner", a voter prefers a set A of candidates to a set B of candidates if A and B are singletons and the voter ranks the candidate in A above the candidate in B.
3056
+
3057
+ If set_preference = "weak-dominance", a voter prefers a set A to a set B if in their sincere ranking, all candidates in A are weakly above all candidates in B and some candidate in A is strictly above some candidate in B.
3058
+
3059
+ If set_preference = "optimist", a voter prefers a set A to a set B if in their sincere ranking, their favorite from A is above their favorite from B.
3060
+
3061
+ If set_preference = "pessimist", a voter prefers a set A to a set B if in their sincere ranking, their least favorite from A is above their least favorite from B.
3062
+
3063
+ Args:
3064
+ prof: a Profile or ProfileWithTies object.
3065
+ vm (VotingMethod): A voting method to test.
3066
+ verbose (bool, default=False): If a violation is found, display the violation.
3067
+ violation_type: default is "Removal"
3068
+ coalition_size: default is 1
3069
+ uniform_coalition: default is True
3070
+ set_preference: default is "single-winner". Other options are "weak-dominance", "optimist", and "pessimist".
3071
+
3072
+ Returns:
3073
+ A List of tuples (preferred_winners, dispreferred_winners, ranking) witnessing violations of participation.
3074
+ """
3075
+
3076
+ violations = list()
3077
+
3078
+ winners = vm(prof)
3079
+
3080
+ if isinstance(prof,ProfileWithTies):
3081
+ prof.use_extended_strict_preference()
3082
+
3083
+ ranking_types = prof.ranking_types
3084
+
3085
+ ws = vm(prof)
3086
+
3087
+ if set_preference == "single-winner":
3088
+ if len(ws) > 1:
3089
+ return False
3090
+
3091
+ if uniform_coalition:
3092
+
3093
+ if violation_type == "Removal":
3094
+
3095
+ relevant_ranking_types = [r for r in prof.ranking_types if prof.rankings.count(r) >= coalition_size]
3096
+
3097
+ for r in relevant_ranking_types:
3098
+ ranking_tokens = [r for r in prof.rankings]
3099
+
3100
+ for i in range(coalition_size):
3101
+ ranking_tokens.remove(r) # remove coalition_size-many tokens of the type of ranking
3102
+
3103
+ if isinstance(prof,Profile):
3104
+
3105
+ new_prof = Profile(ranking_tokens)
3106
+ new_ws = vm(new_prof)
3107
+
3108
+ old_winner_to_compare = None
3109
+ new_winner_to_compare = None
3110
+
3111
+ if set_preference == "single-winner" and len(new_ws) == 1:
3112
+
3113
+ old_winner_to_compare = ws[0]
3114
+ new_winner_to_compare = new_ws[0]
3115
+
3116
+ elif set_preference == "weak-dominance":
3117
+ r_as_ranking = Ranking({c: i for i, c in enumerate(r)})
3118
+
3119
+ elif set_preference == "optimist":
3120
+
3121
+ old_winner_to_compare = [cand for cand in r if cand in ws][0]
3122
+ new_winner_to_compare = [cand for cand in r if cand in new_ws][0]
3123
+
3124
+ elif set_preference == "pessimist":
3125
+
3126
+ old_winner_to_compare = [cand for cand in r if cand in ws][-1]
3127
+ new_winner_to_compare = [cand for cand in r if cand in new_ws][-1]
3128
+
3129
+ if old_winner_to_compare is not None and r.index(old_winner_to_compare) > r.index(new_winner_to_compare) or (set_preference == "weak-dominance" and r_as_ranking.weak_dom(new_ws,ws)):
3130
+
3131
+ violations.append((ws, new_ws, r))
3132
+
3133
+ if verbose:
3134
+ prof = prof.anonymize()
3135
+ new_prof = new_prof.anonymize()
3136
+ print(f"Violation of Participation for {vm.name} under the {set_preference} set preference.")
3137
+ if coalition_size == 1:
3138
+ print(f"A voter with the ranking {r} can benefit by abstaining.")
3139
+ else:
3140
+ print(f"{coalition_size} voters with the ranking {r} can benefit by jointly abstaining.")
3141
+ print("")
3142
+ print("Original Profile:")
3143
+ prof.display()
3144
+ print(prof.description())
3145
+ print("")
3146
+ vm.display(prof)
3147
+ prof.display_margin_graph()
3148
+ print("")
3149
+ if coalition_size == 1:
3150
+ print("Profile if the voter abstains:")
3151
+
3152
+ else:
3153
+ print("Profile if the voters abstain:")
3154
+ new_prof.display()
3155
+ print(new_prof.description())
3156
+ print("")
3157
+ vm.display(new_prof)
3158
+ new_prof.display_margin_graph()
3159
+
3160
+ if isinstance(prof,ProfileWithTies):
3161
+ r_dict = r.rmap
3162
+
3163
+ new_prof = ProfileWithTies(ranking_tokens, candidates = prof.candidates)
3164
+ new_prof.use_extended_strict_preference()
3165
+ new_ws = vm(new_prof)
3166
+
3167
+ ranked_old_winners = [c for c in ws if c in r_dict.keys()]
3168
+ ranked_new_winners = [c for c in new_ws if c in r_dict.keys()]
3169
+
3170
+ rank_of_old_winner_to_compare = None
3171
+ rank_of_new_winner_to_compare = None
3172
+
3173
+ if set_preference == "single-winner" and len(new_ws) == 1:
3174
+
3175
+ rank_of_old_winner_to_compare = r_dict[ws[0]] if ranked_old_winners else math.inf
3176
+ rank_of_new_winner_to_compare = r_dict[new_ws[0]] if ranked_new_winners else math.inf
3177
+
3178
+ elif set_preference == "optimist":
3179
+
3180
+ rank_of_old_winner_to_compare = min([r_dict[c] for c in ranked_old_winners]) if ranked_old_winners else math.inf
3181
+ rank_of_new_winner_to_compare = min([r_dict[c] for c in ranked_new_winners]) if ranked_new_winners else math.inf
3182
+
3183
+ elif set_preference == "pessimist":
3184
+
3185
+ rank_of_old_winner_to_compare = max([r_dict[c] for c in ranked_old_winners]) if ranked_old_winners == ws else math.inf
3186
+ rank_of_new_winner_to_compare = max([r_dict[c] for c in ranked_new_winners]) if ranked_new_winners == new_ws else math.inf
3187
+
3188
+ if rank_of_old_winner_to_compare is not None and rank_of_old_winner_to_compare > rank_of_new_winner_to_compare or (set_preference == "weak-dominance" and r.weak_dom(new_ws,ws,use_extended_preferences=True)):
3189
+
3190
+ violations.append((ws, new_ws, r_dict))
3191
+
3192
+ if verbose:
3193
+ prof = prof.anonymize()
3194
+ new_prof = new_prof.anonymize()
3195
+ print(f"Violation of Participation for {vm.name} under the {set_preference} set preference.")
3196
+ if coalition_size == 1:
3197
+ print(f"A voter with the ranking {r} can benefit by abstaining.")
3198
+ else:
3199
+ print(f"{coalition_size} voters with the ranking {r} can benefit by jointly abstaining.")
3200
+ print("")
3201
+ print("Original Profile:")
3202
+ prof.display()
3203
+ print(prof.description())
3204
+ print("")
3205
+ vm.display(prof)
3206
+ prof.display_margin_graph()
3207
+ print("")
3208
+ if coalition_size == 1:
3209
+ print("Profile if the voter abstains:")
3210
+ else:
3211
+ print("Profile if the voters abstain:")
3212
+ new_prof.display()
3213
+ print(new_prof.description())
3214
+ print("")
3215
+ vm.display(new_prof)
3216
+ new_prof
3217
+
3218
+ if violation_type == "Addition":
3219
+
3220
+ if isinstance(prof,Profile):
3221
+
3222
+ for new_r in permutations(prof.candidates):
3223
+ new_ranking_tokens = [r for r in prof.rankings]
3224
+
3225
+ for i in range(coalition_size):
3226
+ new_ranking_tokens.append(new_r)
3227
+
3228
+ new_prof = Profile(new_ranking_tokens)
3229
+ new_ws = vm(new_prof)
3230
+
3231
+ old_winner_to_compare = None
3232
+ new_winner_to_compare = None
3233
+
3234
+ if set_preference == "single-winner" and len(new_ws) == 1:
3235
+
3236
+ old_winner_to_compare = ws[0]
3237
+ new_winner_to_compare = new_ws[0]
3238
+
3239
+ elif set_preference == "weak-dominance":
3240
+ new_r_as_ranking = Ranking({c: i for i, c in enumerate(new_r)})
3241
+
3242
+ elif set_preference == "optimist":
3243
+
3244
+ old_winner_to_compare = [cand for cand in new_r if cand in ws][0]
3245
+ new_winner_to_compare = [cand for cand in new_r if cand in new_ws][0]
3246
+
3247
+ elif set_preference == "pessimist":
3248
+
3249
+ old_winner_to_compare = [cand for cand in new_r if cand in ws][-1]
3250
+ new_winner_to_compare = [cand for cand in new_r if cand in new_ws][-1]
3251
+
3252
+ if old_winner_to_compare is not None and new_r.index(old_winner_to_compare) < new_r.index(new_winner_to_compare) or (set_preference == "weak-dominance" and new_r_as_ranking.weak_dom(ws,new_ws)):
3253
+
3254
+ violations.append((ws, new_ws, new_r))
3255
+
3256
+ if verbose:
3257
+ prof = prof.anonymize()
3258
+ new_prof = new_prof.anonymize()
3259
+ print(f"Violation of Participation for {vm.name} under the {set_preference} set preference.")
3260
+ if coalition_size == 1:
3261
+ print(f"A new voter who joins with the ranking {new_r} will wish they had abstained.")
3262
+ else:
3263
+ print(f"{coalition_size} new voters who join with the ranking {new_r} will wish they had jointly abstained.")
3264
+ print("")
3265
+ print("Original Profile without voter(s):")
3266
+ prof.display()
3267
+ print(prof.description())
3268
+ print("")
3269
+ vm.display(prof)
3270
+ prof.display_margin_graph()
3271
+ print("")
3272
+ print("New Profile with voter(s) added:")
3273
+ new_prof.display()
3274
+ print(new_prof.description())
3275
+ print("")
3276
+ vm.display(new_prof)
3277
+ new_prof.display_margin_graph()
3278
+
3279
+ if isinstance(prof,ProfileWithTies):
3280
+
3281
+ for _new_r in weak_orders(prof.candidates):
3282
+ new_r = Ranking(_new_r)
3283
+ new_r_dict = new_r.rmap
3284
+
3285
+ new_ranking_tokens = [r for r in prof.rankings]
3286
+
3287
+ for i in range(coalition_size):
3288
+ new_ranking_tokens.append(new_r)
3289
+
3290
+ new_prof = ProfileWithTies(new_ranking_tokens, candidates = prof.candidates)
3291
+ new_prof.use_extended_strict_preference()
3292
+ new_ws = vm(new_prof)
3293
+
3294
+ ranked_old_winners = [c for c in ws if c in new_r_dict.keys()]
3295
+ ranked_new_winners = [c for c in new_ws if c in new_r_dict.keys()]
3296
+
3297
+ rank_of_old_winner_to_compare = None
3298
+ rank_of_new_winner_to_compare = None
3299
+
3300
+ if set_preference == "single-winner" and len(new_ws) == 1:
3301
+
3302
+ rank_of_old_winner_to_compare = new_r_dict[ws[0]] if ranked_old_winners else math.inf
3303
+ rank_of_new_winner_to_compare = new_r_dict[new_ws[0]] if ranked_new_winners else math.inf
3304
+
3305
+ elif set_preference == "optimist":
3306
+
3307
+ rank_of_old_winner_to_compare = min([new_r_dict[c] for c in ranked_old_winners]) if ranked_old_winners else math.inf
3308
+ rank_of_new_winner_to_compare = min([new_r_dict[c] for c in ranked_new_winners]) if ranked_new_winners else math.inf
3309
+
3310
+ elif set_preference == "pessimist":
3311
+
3312
+ rank_of_old_winner_to_compare = max([new_r_dict[c] for c in ranked_old_winners]) if ranked_old_winners == ws else math.inf
3313
+ rank_of_new_winner_to_compare = max([new_r_dict[c] for c in ranked_new_winners]) if ranked_new_winners == new_ws else math.inf
3314
+
3315
+ if rank_of_old_winner_to_compare is not None and rank_of_old_winner_to_compare < rank_of_new_winner_to_compare or (set_preference == "weak-dominance" and r.weak_dom(ws,new_ws,use_extended_preferences=True)):
3316
+
3317
+ violations.append((ws, new_ws, new_r_dict))
3318
+
3319
+ if verbose:
3320
+ prof = prof.anonymize()
3321
+ new_prof = new_prof.anonymize()
3322
+ print(f"Violation of Participation for {vm.name} under the {set_preference} set preference.")
3323
+ if coalition_size == 1:
3324
+ print(f"A new voter who joins with the ranking {new_r} will wish they had abstained.")
3325
+ else:
3326
+ print(f"{coalition_size} new voters who join with the ranking {new_r} will wish they had jointly abstained.")
3327
+ print("")
3328
+ print(" Original Profile without voter(s):")
3329
+ prof.display()
3330
+ print(prof.description())
3331
+ print("")
3332
+ vm.display(prof)
3333
+ prof.display_margin_graph()
3334
+ print("")
3335
+ print("New Profile with voter(s) added:")
3336
+ new_prof.display()
3337
+ print(new_prof.description())
3338
+ print("")
3339
+ vm.display(new_prof)
3340
+ new_prof.display_margin_graph()
3341
+
3342
+ return violations
3343
+
3344
+ participation = Axiom(
3345
+ "Participation",
3346
+ has_violation = has_participation_violation,
3347
+ find_all_violations = find_all_participation_violations,
3348
+ )
3349
+
3350
+ def has_single_voter_resolvability_violation(prof, vm, verbose=False):
3351
+ """
3352
+ If prof is a Profile, returns True if there are multiple vm winners in prof and for one such winner A, there is no linear ballot that can be added to prof to make A the unique winner.
3353
+
3354
+ If prof is a ProfileWithTies, returns True if there are multiple vm winners in prof and for one such winner A, there is no Ranking (allowing ties) that can be added to prof to make A the unique winner.
3355
+
3356
+ Args:
3357
+ prof: a Profile or ProfileWithTies object.
3358
+ vm (VotingMethod): A voting method to test.
3359
+ verbose (bool, default=False): If a violation is found, display the violation.
3360
+
3361
+ Returns:
3362
+ Result of the test (bool): Returns True if there is a violation and False otherwise.
3363
+ """
3364
+
3365
+ winners = vm(prof)
3366
+
3367
+ if isinstance(prof,ProfileWithTies):
3368
+ prof.use_extended_strict_preference()
3369
+
3370
+ if len(winners) > 1:
3371
+ for winner in winners:
3372
+
3373
+ found_voter_to_add = False
3374
+
3375
+ if isinstance(prof,Profile):
3376
+ for r in permutations(prof.candidates):
3377
+ new_prof = Profile(prof.rankings + [r])
3378
+ if vm(new_prof) == [winner]:
3379
+ found_voter_to_add = True
3380
+ break
3381
+
3382
+ if isinstance(prof,ProfileWithTies):
3383
+ for _r in weak_orders(prof.candidates):
3384
+ r = Ranking(_r)
3385
+ new_prof = ProfileWithTies(prof.rankings + [r], candidates = prof.candidates)
3386
+ new_prof.use_extended_strict_preference()
3387
+ if vm(new_prof) == [winner]:
3388
+ found_voter_to_add = True
3389
+ break
3390
+
3391
+ if not found_voter_to_add:
3392
+
3393
+ if verbose:
3394
+ prof = prof.anonymize()
3395
+ if isinstance(prof,Profile):
3396
+ print(f"Violation of Single-Voter Resolvability for {vm.name}: cannot make {winner} the unique winner by adding a linear ballot.")
3397
+ if isinstance(prof,ProfileWithTies):
3398
+ print(f"Violation of Single-Voter Resolvability for {vm.name}: cannot make {winner} the unique winner by adding a Ranking.")
3399
+ print("")
3400
+ print("Profile:")
3401
+ prof.display()
3402
+ print(prof.description())
3403
+ print("")
3404
+ vm.display(prof)
3405
+ prof.display_margin_graph()
3406
+ print("")
3407
+
3408
+ return True
3409
+
3410
+ return False
3411
+
3412
+ return False
3413
+
3414
+ def find_all_single_voter_resolvability_violations(prof, vm, verbose=False):
3415
+ """
3416
+ If prof is a Profile, returns a list of candidates who win in prof but who cannot be made the unique winner by adding a linear ballot.
3417
+
3418
+ If prof is a ProfileWithTies, returns a list of candidates who win in prof but who cannot be made the unique winner by adding a Ranking (allowing ties).
3419
+
3420
+ Args:
3421
+ prof: a Profile or ProfileWithTies object.
3422
+ vm (VotingMethod): A voting method to test.
3423
+ verbose (bool, default=False): If a violation is found, display the violation.
3424
+
3425
+ Returns:
3426
+ A List of candidates who win in the given profile but who cannot be made the unique winner by adding a ballot.
3427
+ """
3428
+
3429
+ winners = vm(prof)
3430
+
3431
+ if isinstance(prof,ProfileWithTies):
3432
+ prof.use_extended_strict_preference()
3433
+
3434
+ violations = list()
3435
+
3436
+ if len(winners) > 1:
3437
+ for winner in winners:
3438
+
3439
+ found_voter_to_add = False
3440
+
3441
+ if isinstance(prof,Profile):
3442
+ for r in permutations(prof.candidates):
3443
+ new_prof = Profile(prof.rankings + [r])
3444
+ if vm(new_prof) == [winner]:
3445
+ found_voter_to_add = True
3446
+ break
3447
+
3448
+ if isinstance(prof,ProfileWithTies):
3449
+ for _r in weak_orders(prof.candidates):
3450
+ r = Ranking(_r)
3451
+ new_prof = ProfileWithTies(prof.rankings + [r], candidates = prof.candidates)
3452
+ new_prof.use_extended_strict_preference()
3453
+ if vm(new_prof) == [winner]:
3454
+ found_voter_to_add = True
3455
+ break
3456
+
3457
+ if not found_voter_to_add:
3458
+
3459
+ if verbose:
3460
+ prof = prof.anonymize()
3461
+ if isinstance(prof,Profile):
3462
+ print(f"Violation of Single-Voter Resolvability for {vm.name}: cannot make {winner} the unique winner by adding a linear ballot.")
3463
+ if isinstance(prof,ProfileWithTies):
3464
+ print(f"Violation of Single-Voter Resolvability for {vm.name}: cannot make {winner} the unique winner by adding a Ranking.")
3465
+ print("")
3466
+ print("Profile:")
3467
+ prof.display()
3468
+ print(prof.description())
3469
+ print("")
3470
+ vm.display(prof)
3471
+ prof.display_margin_graph()
3472
+ print("")
3473
+
3474
+ violations.append(winner)
3475
+
3476
+ return violations
3477
+
3478
+ single_voter_resolvability = Axiom(
3479
+ "Single-Voter Resolvability",
3480
+ has_violation = has_single_voter_resolvability_violation,
3481
+ find_all_violations = find_all_single_voter_resolvability_violations,
3482
+ )
3483
+
3484
+ def has_neutral_reversal_violation(prof, vm, verbose=False):
3485
+ """Returns True if adding a reversal pair of voters (a voter with ranking L and a voter with the reverse ranking L^{-1}) changes the winners.
3486
+
3487
+ Args:
3488
+ prof: a Profile or ProfileWithTies object
3489
+ vm: a voting method
3490
+ verbose: if True, display the violation when found
3491
+
3492
+ Returns:
3493
+ True if there is a violation, False otherwise
3494
+ """
3495
+
3496
+ winners = vm(prof)
3497
+
3498
+ # Get all possible linear orders of the candidates
3499
+ all_rankings = list(permutations(prof.candidates))
3500
+
3501
+ # For each linear order L, add L and its reverse L^-1 to create new profile
3502
+ for ranking in all_rankings:
3503
+ # Skip if we've already considered this pair (to avoid redundancy)
3504
+ reverse_ranking = tuple(reversed(ranking))
3505
+ if reverse_ranking < ranking:
3506
+ continue
3507
+
3508
+ if isinstance(prof, Profile):
3509
+ # For Profile objects, we need to work with numpy arrays
3510
+ pair_arr = np.array([list(ranking), list(reverse_ranking)])
3511
+ combined_rankings = np.concatenate([prof._rankings, pair_arr], axis=0)
3512
+ combined_rcounts = np.concatenate([prof._rcounts, [1, 1]], axis=0)
3513
+ prof2 = Profile(combined_rankings, rcounts=combined_rcounts)
3514
+ else:
3515
+ # For ProfileWithTies, we work with Ranking objects
3516
+ ranking_obj = Ranking({c: i for i, c in enumerate(ranking)})
3517
+ reverse_ranking_obj = Ranking({c: len(ranking)-1-i for i, c in enumerate(ranking)})
3518
+ new_rankings = prof.rankings + [ranking_obj, reverse_ranking_obj]
3519
+ prof2 = ProfileWithTies(new_rankings, candidates=prof.candidates)
3520
+ if prof.using_extended_strict_preference:
3521
+ prof2.use_extended_strict_preference()
3522
+
3523
+ # Check if winners changed
3524
+ winners2 = vm(prof2)
3525
+ if set(winners) != set(winners2):
3526
+ if verbose:
3527
+ print(f"Adding voters with rankings {ranking} and {reverse_ranking} changes the winners:")
3528
+ print("\nOriginal profile:")
3529
+ prof.display()
3530
+ vm.display(prof)
3531
+ print("\nProfile after adding reversal pair:")
3532
+ prof2.display()
3533
+ vm.display(prof2)
3534
+ return True
3535
+
3536
+ return False
3537
+
3538
+ def find_all_neutral_reversal_violations(prof, vm, verbose=False):
3539
+ """Returns a list of reversal pairs (L, L^-1) that when added to the profile change the winners.
3540
+
3541
+ Args:
3542
+ prof: a Profile or ProfileWithTies object
3543
+ vm: a voting method
3544
+ verbose: if True, display the violations when found
3545
+
3546
+ Returns:
3547
+ A list of pairs (L, L^-1) where L is a linear order and L^-1 is its reverse
3548
+ """
3549
+
3550
+ winners = vm(prof)
3551
+ violations = []
3552
+
3553
+ # Get all possible linear orders of the candidates
3554
+ all_rankings = list(permutations(prof.candidates))
3555
+
3556
+ # For each linear order L, add L and its reverse L^-1 to create new profile
3557
+ for ranking in all_rankings:
3558
+ # Create the reverse ranking
3559
+ reverse_ranking = tuple(reversed(ranking))
3560
+
3561
+ # Skip if we've already considered this pair (to avoid redundancy)
3562
+ if reverse_ranking < ranking:
3563
+ continue
3564
+
3565
+ # Add the ranking and its reverse to create new profile
3566
+ if isinstance(prof, Profile):
3567
+ # For Profile objects, we need to work with numpy arrays
3568
+ pair_arr = np.array([list(ranking), list(reverse_ranking)])
3569
+ combined_rankings = np.concatenate([prof._rankings, pair_arr], axis=0)
3570
+ combined_rcounts = np.concatenate([prof._rcounts, [1, 1]], axis=0)
3571
+ prof2 = Profile(combined_rankings, rcounts=combined_rcounts)
3572
+ else:
3573
+ # For ProfileWithTies, we work with Ranking objects
3574
+ ranking_obj = Ranking({c: i for i, c in enumerate(ranking)})
3575
+ reverse_ranking_obj = Ranking({c: len(ranking)-1-i for i, c in enumerate(ranking)})
3576
+ new_rankings = prof.rankings + [ranking_obj, reverse_ranking_obj]
3577
+ prof2 = ProfileWithTies(new_rankings, candidates=prof.candidates)
3578
+ if prof.using_extended_strict_preference:
3579
+ prof2.use_extended_strict_preference()
3580
+
3581
+ # Check if winners changed
3582
+ winners2 = vm(prof2)
3583
+ if set(winners) != set(winners2):
3584
+ violations.append((ranking, reverse_ranking))
3585
+ if verbose:
3586
+ print(f"Adding voters with rankings {ranking} and {reverse_ranking} changes the winners:")
3587
+ print("\nOriginal profile:")
3588
+ prof.display()
3589
+ vm.display(prof)
3590
+ print("\nProfile after adding reversal pair:")
3591
+ prof2.display()
3592
+ vm.display(prof2)
3593
+
3594
+ return violations
3595
+
3596
+ neutral_reversal = Axiom(
3597
+ "Neutral Reversal",
3598
+ has_violation = has_neutral_reversal_violation,
3599
+ find_all_violations = find_all_neutral_reversal_violations,
3600
+ )
3601
+
3602
+
3603
+ def has_neutral_indifference_violation(prof, vm, verbose=False):
3604
+ """
3605
+ Return True if the profile prof has a neutral indifference violation for the voting method vm. Otherwise, return False. That is, return True if there is a tie ranking that can be added to the profile that changes winning set according to vm. Otherwise, return False.
3606
+ """
3607
+
3608
+ tie_ranking = Ranking({c:0 for c in prof.candidates})
3609
+ if isinstance(prof, ProfileWithTies):
3610
+ new_rankings = prof.rankings + [tie_ranking]
3611
+ new_prof = ProfileWithTies(new_rankings)
3612
+ elif isinstance(prof, Profile):
3613
+ new_rankings = [Ranking.from_linear_order(r) for r in prof.rankings] + [tie_ranking]
3614
+ new_prof = ProfileWithTies(new_rankings)
3615
+ if vm(prof) != vm(new_prof):
3616
+ if verbose:
3617
+ print("The original profile")
3618
+ prof.anonymize().display()
3619
+ print(prof.description())
3620
+ vm.display(prof)
3621
+ print("")
3622
+ print(f"The profile after adding a tie ranking:")
3623
+ new_prof.anonymize().display()
3624
+ print(new_prof.description())
3625
+ vm.display(new_prof)
3626
+ return True
3627
+ return False
3628
+
3629
+ def find_all_neutral_indifference_violations(prof, vm, verbose=False):
3630
+ """
3631
+ Return a list containing the profile with an additional voter that ranks all candidates as a tie if this profile has a different winning set according to vm than the original profile. Otherwise, return the empty list.
3632
+ """
3633
+
3634
+ tie_ranking = Ranking({c:0 for c in prof.candidates})
3635
+ if isinstance(prof, ProfileWithTies):
3636
+ new_rankings = prof.rankings + [tie_ranking]
3637
+ new_prof = ProfileWithTies(new_rankings)
3638
+ elif isinstance(prof, Profile):
3639
+ new_rankings = [Ranking.from_linear_order(r) for r in prof.rankings] + [tie_ranking]
3640
+ new_prof = ProfileWithTies(new_rankings)
3641
+ if vm(prof) != vm(new_prof):
3642
+ if verbose:
3643
+ print("The original profile")
3644
+ prof.anonymize().display()
3645
+ print(prof.description())
3646
+ vm.display(prof)
3647
+ print("")
3648
+ print(f"The profile after adding a tie ranking:")
3649
+ new_prof.anonymize().display()
3650
+ print(new_prof.description())
3651
+ vm.display(new_prof)
3652
+ return [new_prof]
3653
+ return []
3654
+
3655
+ neutral_indifference = Axiom(
3656
+ "Neutral Indifference",
3657
+ has_violation = has_neutral_indifference_violation,
3658
+ find_all_violations = find_all_neutral_indifference_violations,
3659
+ )
3660
+
3661
+ def has_nonlinear_neutral_reversal_violation(prof, vm, verbose=False):
3662
+ """
3663
+ Return True if there is a violation of the nonlinear neutral reversal axiom for the voting method vm. Otherwise, return False. That is, return True if there is a strict weak order and its reverse that can be added to the profile that changes the winning set according to vm.
3664
+ """
3665
+ for swo in strict_weak_orders(prof.candidates):
3666
+
3667
+ ranking = Ranking.from_indiff_list(swo)
3668
+ ranking_reverse = ranking.reverse()
3669
+
3670
+ if isinstance(prof, ProfileWithTies):
3671
+ new_rankings = prof.rankings + [ranking, ranking_reverse]
3672
+ new_prof = ProfileWithTies(new_rankings)
3673
+
3674
+ elif isinstance(prof, Profile):
3675
+ new_rankings = [Ranking.from_linear_order(r) for r in prof.rankings] + [ranking, ranking_reverse]
3676
+ new_prof = ProfileWithTies(new_rankings)
3677
+
3678
+ if vm(prof) != vm(new_prof):
3679
+ if verbose:
3680
+ print("The original profile")
3681
+ prof.anonymize().display()
3682
+ print(prof.description())
3683
+ vm.display(prof)
3684
+ print("")
3685
+ print(f"The profile after adding {ranking} and its reverse {ranking_reverse}:")
3686
+ new_prof.anonymize().display()
3687
+ print(new_prof.description())
3688
+ vm.display(new_prof)
3689
+ return True
3690
+ return False
3691
+
3692
+
3693
+ def find_all_nonlinear_neutral_reversal_violations(prof, vm, verbose=False):
3694
+ """
3695
+ Return the list of strict weak orders and their reverse such that adding them to prof results in a different winning set according to vm. Otherwise, return the empty list.
3696
+ """
3697
+
3698
+ violation_swos = []
3699
+ for swo in strict_weak_orders(prof.candidates):
3700
+
3701
+ ranking = Ranking.from_indiff_list(swo)
3702
+ ranking_reverse = ranking.reverse()
3703
+
3704
+ if isinstance(prof, ProfileWithTies):
3705
+ new_rankings = prof.rankings + [ranking, ranking_reverse]
3706
+ new_prof = ProfileWithTies(new_rankings)
3707
+
3708
+ elif isinstance(prof, Profile):
3709
+ new_rankings = [Ranking.from_linear_order(r) for r in prof.rankings] + [ranking, ranking_reverse]
3710
+ new_prof = ProfileWithTies(new_rankings)
3711
+
3712
+ if vm(prof) != vm(new_prof):
3713
+ if verbose:
3714
+ print("The original profile")
3715
+ prof.anonymize().display()
3716
+ print(prof.description())
3717
+ vm.display(prof)
3718
+ print("")
3719
+ print(f"The profile after adding {ranking} and its reverse {ranking_reverse}:")
3720
+ new_prof.anonymize().display()
3721
+ print(new_prof.description())
3722
+ vm.display(new_prof)
3723
+ violation_swos.append((ranking, ranking_reverse))
3724
+
3725
+ return violation_swos
3726
+
3727
+ nonlinear_neutral_reversal = Axiom(
3728
+ "Nonlinear Neutral Reversal",
3729
+ has_violation = has_nonlinear_neutral_reversal_violation,
3730
+ find_all_violations = find_all_nonlinear_neutral_reversal_violations,
3731
+ )
3732
+
3733
+ variable_voter_axioms = [
3734
+ reinforcement,
3735
+ positive_involvement,
3736
+ negative_involvement,
3737
+ positive_negative_involvement,
3738
+ tolerant_positive_involvement,
3739
+ bullet_vote_positive_involvement,
3740
+ semi_positive_involvement,
3741
+ truncated_involvement,
3742
+ participation,
3743
+ single_voter_resolvability,
3744
+ neutral_reversal,
3745
+ neutral_indifference,
3746
+ nonlinear_neutral_reversal,
3747
+ ]