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,1539 @@
1
+ """
2
+ File: weighted_majority_graphs.py
3
+ Author: Wes Holliday (wesholliday@berkeley.edu) and Eric Pacuit (epacuit@umd.edu)
4
+ Date: January 5, 2022
5
+ Updated: July 12, 2022
6
+ Updated: December 19, 2022
7
+
8
+ Majority Graphs, Margin Graphs and Support Graphs
9
+ """
10
+
11
+ import networkx as nx
12
+ from tabulate import tabulate
13
+ import matplotlib.pyplot as plt
14
+ import string
15
+ from itertools import combinations, permutations
16
+ from ortools.linear_solver import pywraplp
17
+
18
+ import numpy as np
19
+
20
+ class MajorityGraph(object):
21
+ """A majority graph is an asymmetric directed graph. The nodes are the candidates and an edge from candidate :math:`c` to :math:`d` means that :math:`c` is majority preferred to :math:`d`.
22
+
23
+ :param candidates: List of the candidates. To be used as nodes in the majority graph.
24
+ :type candidates: list[int] or list[str]
25
+ :param edges: List of the pairs of candidates describing the edges in the majority graph. If :math:`(c,d)` is in the list of edges, then there is an edge from :math:`c` to :math:`d`.
26
+ :type edges: list
27
+ :param cmap: Dictionary mapping candidates to candidate names (strings). If not provided, each candidate name is mapped to itself.
28
+ :type cmap: dict[int: str], optional
29
+
30
+ :Example:
31
+
32
+ The following code creates a majority graph in which 0 is majority preferred to 1, 1 is majority preferred to 2, and 2 is majority preferred to 0:
33
+
34
+ .. code-block:: python
35
+
36
+ mg = MajorityGraph([0, 1, 2], [(0,1), (1,2), (2,0)])
37
+
38
+ .. warning:: Currently, there is no check that the edge relation is asymmetric. It is assumed that the user provides an appropriate set of edges.
39
+ """
40
+
41
+ def __init__(self, candidates, edges, cmap=None):
42
+ """constructer method"""
43
+
44
+ mg = nx.DiGraph()
45
+ mg.add_nodes_from(candidates)
46
+ mg.add_edges_from(edges)
47
+ self.mg = mg
48
+ """A networkx DiGraph object representing the majority graph."""
49
+
50
+ self.cmap = cmap if cmap is not None else {c: str(c) for c in candidates}
51
+
52
+ self.candidates = list(candidates)
53
+ """The list of candidates."""
54
+
55
+ self.num_cands = len(self.candidates)
56
+ """The number of candidates."""
57
+
58
+ self.cindices = list(range(self.num_cands))
59
+ self._cand_to_cindex = {c: i for i, c in enumerate(self.candidates)}
60
+ self.cand_to_cindex = lambda c: self._cand_to_cindex[c]
61
+ self._cindex_to_cand = {i: c for i, c in enumerate(self.candidates)}
62
+ self.cindex_to_cand = lambda i: self._cindex_to_cand[i]
63
+ """A dictionary mapping each candidate to its index in the list of candidates and vice versa."""
64
+
65
+ self.maj_matrix = [[False for c2 in self.cindices] for c1 in self.cindices]
66
+ """A matrix of Boolean values representing the majority graph."""
67
+
68
+ for c1_idx in self.cindices:
69
+ for c2_idx in self.cindices:
70
+ if mg.has_edge(self.cindex_to_cand(c1_idx), self.cindex_to_cand(c2_idx)):
71
+ self.maj_matrix[c1_idx][c2_idx] = True
72
+ self.maj_matrix[c2_idx][c1_idx] = False
73
+ elif mg.has_edge(self.cindex_to_cand(c2_idx), self.cindex_to_cand(c1_idx)):
74
+ self.maj_matrix[c2_idx][c1_idx] = True
75
+ self.maj_matrix[c1_idx][c2_idx] = False
76
+
77
+ def margin(self, c1, c2):
78
+ raise Exception("margin is not implemented for majority graphs.")
79
+
80
+ def support(self, c1, c2):
81
+ raise Exception("support is not implemented for majority graphs.")
82
+
83
+ def ratio(self, c1, c2):
84
+ raise Exception("ratio is not implemented for majority graphs.")
85
+
86
+ @property
87
+ def edges(self):
88
+ """Returns a list of the edges in the majority graph."""
89
+
90
+ return list(self.mg.edges)
91
+
92
+ @property
93
+ def is_tournament(self):
94
+ """Returns True if the majority graph is a **tournament** (there is an edge between any two distinct nodes)."""
95
+
96
+ return all([
97
+ self.mg.has_edge(c1, c2) or self.mg.has_edge(c2, c1)
98
+ for c1 in self.candidates
99
+ for c2 in self.candidates
100
+ if c1 != c2
101
+ ])
102
+
103
+ def majority_prefers(self, c1, c2):
104
+ """Returns true if there is an edge from `c1` to `c2`."""
105
+ return self.mg.has_edge(c1, c2)
106
+
107
+ def is_tied(self, c1, c2):
108
+ """Returns true if there is no edge from `c1` to `c2` or from `c2` to `c1`."""
109
+ return not self.mg.has_edge(c1, c2) and not self.mg.has_edge(c2, c1)
110
+
111
+ def copeland_scores(self, curr_cands=None, scores=(1, 0, -1)):
112
+ """The Copeland scores in the profile restricted to the candidates in ``curr_cands``.
113
+
114
+ The **Copeland score** for candidate :math:`c` is calculated as follows: :math:`c` receives ``scores[0]`` points for every candidate that :math:`c` is majority preferred to, ``scores[1]`` points for every candidate that is tied with :math:`c`, and ``scores[2]`` points for every candidate that is majority preferred to :math:`c`. The default ``scores`` is ``(1, 0, -1)``.
115
+
116
+ :param curr_cands: restrict attention to candidates in this list. Defaults to all candidates in the profile if not provided.
117
+ :type curr_cands: list[int], optional
118
+ :param scores: the scores used to calculate the Copeland score of a candidate :math:`c`: ``scores[0]`` is for the candidates that :math:`c` is majority preferred to; ``scores[1]`` is for the candidates tied with :math:`c`; and ``scores[2]`` is for the candidates majority preferred to :math:`c`. The default value is ``scores = (1, 0, -1)``
119
+ :type scores: tuple[int], optional
120
+ :returns: a dictionary associating each candidate in ``curr_cands`` with its Copeland score.
121
+
122
+ """
123
+
124
+ wscore, tscore, lscore = scores
125
+ candidates = self.candidates if curr_cands is None else curr_cands
126
+ c_scores = {c: 0.0 for c in candidates}
127
+ for c1 in candidates:
128
+ for c2 in candidates:
129
+ if self.majority_prefers(c1, c2):
130
+ c_scores[c1] += wscore
131
+ elif self.majority_prefers(c2, c1):
132
+ c_scores[c1] += lscore
133
+ elif c1 != c2:
134
+ c_scores[c1] += tscore
135
+ return c_scores
136
+
137
+ def dominators(self, cand, curr_cands=None):
138
+ """Returns the list of candidates that are majority preferred to ``cand`` in the majority graph restricted to ``curr_cands``."""
139
+ candidates = self.candidates if curr_cands is None else curr_cands
140
+
141
+ return [c for c in candidates if self.majority_prefers(c, cand)]
142
+
143
+ def dominates(self, cand, curr_cands=None):
144
+ """Returns the list of candidates that ``cand`` is majority preferred to in the majority graph restricted to ``curr_cands``."""
145
+ candidates = self.candidates if curr_cands is None else curr_cands
146
+
147
+ return [c for c in candidates if self.majority_prefers(cand, c)]
148
+
149
+ def condorcet_winner(self, curr_cands=None):
150
+ """Returns the Condorcet winner in the profile restricted to ``curr_cands`` if one exists, otherwise returns None.
151
+
152
+ The **Condorcet winner** is the candidate that is majority preferred to every other candidate.
153
+ """
154
+
155
+ curr_cands = curr_cands if curr_cands is not None else self.candidates
156
+
157
+ cw = None
158
+ for c1 in curr_cands:
159
+ if all([self.majority_prefers(c1, c2) for c2 in curr_cands if c1 != c2]):
160
+ cw = c1
161
+ break # if a Condorcet winner exists, then it is unique
162
+ return cw
163
+
164
+ def weak_condorcet_winner(self, curr_cands=None):
165
+ """Returns a list of the weak Condorcet winners in the profile restricted to ``curr_cands`` (which may be empty).
166
+
167
+ A candidate :math:`c` is a **weak Condorcet winner** if there is no other candidate that is majority preferred to :math:`c`.
168
+
169
+ .. note:: While the Condorcet winner is unique if it exists, there may be multiple weak Condorcet winners.
170
+ """
171
+
172
+ curr_cands = curr_cands if curr_cands is not None else self.candidates
173
+
174
+ weak_cw = list()
175
+ for c1 in curr_cands:
176
+ if not any(
177
+ [self.majority_prefers(c2, c1) for c2 in curr_cands if c1 != c2]
178
+ ):
179
+ weak_cw.append(c1)
180
+ return sorted(weak_cw) if len(weak_cw) > 0 else None
181
+
182
+ def condorcet_loser(self, curr_cands=None):
183
+ """Returns the Condorcet loser in the profile restricted to ``curr_cands`` if one exists, otherwise returns None.
184
+
185
+ A candidate :math:`c` is a **Condorcet loser** if every other candidate is majority preferred to :math:`c`.
186
+ """
187
+
188
+ curr_cands = curr_cands if curr_cands is not None else self.candidates
189
+
190
+ cl = None
191
+ for c1 in curr_cands:
192
+ if all([self.majority_prefers(c2, c1) for c2 in curr_cands if c1 != c2]):
193
+ cl = c1
194
+ break # if a Condorcet loser exists, then it is unique
195
+ return cl
196
+
197
+ def cycles(self, curr_cands = None):
198
+ """Returns True if the margin graph has a cycle.
199
+
200
+ This uses the networkx method ``networkx.find_cycle`` to find the cycles in ``self.mg``.
201
+
202
+ :Example:
203
+
204
+ .. exec_code::
205
+
206
+ from pref_voting.weighted_majority_graphs import MajorityGraph
207
+ mg = MajorityGraph([0,1,2], [(0,1), (1,2), (0,2)])
208
+ print(f"The cycles in the majority graph are {mg.cycles()}")
209
+ mg = MajorityGraph([0,1,2], [(0,1), (1,2), (2,0)])
210
+ print(f"The cycles in the majority graph are {mg.cycles()}")
211
+ mg = MajorityGraph([0,1,2,3], [(0,1), (3,0), (1,2), (3,1), (2,0), (3,2)])
212
+ print(f"The cycles in the majority graph are {mg.cycles()}")
213
+
214
+ """
215
+
216
+ if curr_cands is None:
217
+ return list(nx.simple_cycles(self.mg))
218
+ else:
219
+ mg = nx.DiGraph()
220
+ subgraph = mg.subgraph(curr_cands).copy()
221
+ return list(nx.simple_cycles(subgraph))
222
+
223
+
224
+ def has_cycle(self, curr_cands = None):
225
+ """Returns True if there is a cycle in the majority graph."""
226
+ if curr_cands is None:
227
+ try:
228
+ cycle = nx.find_cycle(self.mg)
229
+ except:
230
+ cycle = list()
231
+ else:
232
+ mg = nx.DiGraph()
233
+ subgraph = mg.subgraph(curr_cands).copy()
234
+ try:
235
+ cycle = nx.find_cycle(subgraph)
236
+ except:
237
+ cycle = list()
238
+
239
+ return len(cycle) != 0
240
+
241
+ def remove_candidates(self, cands_to_ignore):
242
+ """Remove all candidates from ``cands_to_ignore`` from the Majority Graph.
243
+
244
+ :param cands_to_ignore: list of candidates to remove from the profile
245
+ :type cands_to_ignore: list[int]
246
+ :returns: a majority graph with candidates from ``cands_to_ignore`` removed and a dictionary mapping the candidates from the new profile to the original candidate names.
247
+
248
+ :Example:
249
+
250
+ .. exec_code::
251
+
252
+ from pref_voting.weighted_majority_graphs import MajorityGraph
253
+ mg = MajorityGraph([0, 1, 2], [(0, 1), (1, 2), (2, 0)])
254
+ print(f"Candidates: {mg.candidates}")
255
+ print(f"Edges: {mg.edges}")
256
+ mg_new = mg.remove_candidates([1])
257
+ print(f"Candidates: {mg_new.candidates}")
258
+ print(f"Edges: {mg_new.edges}")
259
+ """
260
+
261
+ new_cands = [c for c in self.candidates if c not in cands_to_ignore]
262
+
263
+ new_edges = [e for e in self.edges if e[0] in new_cands and e[1] in new_cands]
264
+
265
+ new_cmap = {c: cname for c, cname in self.cmap.items() if c in new_cands}
266
+
267
+ return MajorityGraph(new_cands, new_edges, cmap=new_cmap)
268
+
269
+ def to_networkx(self):
270
+ """
271
+ Return a networkx weighted DiGraph representing the margin graph.
272
+ """
273
+
274
+ return self.mg
275
+
276
+ def description(self):
277
+ """
278
+ Returns a string describing the Majority Graph.
279
+ """
280
+ return f"MajorityGraph({self.candidates}, {self.edges}, cmap={self.cmap})"
281
+
282
+ def display(self, cmap=None, curr_cands=None):
283
+ """Display a majority graph (restricted to ``curr_cands``) using networkx.draw.
284
+
285
+ :param cmap: the candidate map to use (overrides the cmap associated with this majority graph)
286
+ :type cmap: dict[int,str], optional
287
+ :param curr_cands: list of candidates
288
+ :type curr_cands: list[int], optional
289
+ :rtype: None
290
+
291
+ :Example:
292
+
293
+ .. code::
294
+
295
+ from pref_voting.weighted_majority_graphs import MajorityGraph
296
+ mg = MajorityGraph([0,1,2], [(0,1), (1,2), (2,0)])
297
+ mg.display()
298
+
299
+ .. image:: ./maj_graph_ex1.png
300
+ :width: 400
301
+ :alt: Alternative text
302
+
303
+ """
304
+
305
+ cmap = cmap if cmap is not None else self.cmap
306
+ curr_cands = self.candidates if curr_cands is None else curr_cands
307
+
308
+ mg = nx.DiGraph()
309
+ mg.add_nodes_from([cmap[c] for c in curr_cands])
310
+ mg.add_edges_from(
311
+ [
312
+ (cmap[c1], cmap[c2])
313
+ for c1, c2 in self.mg.edges
314
+ if c1 in curr_cands and c2 in curr_cands
315
+ ]
316
+ )
317
+
318
+ pos = nx.circular_layout(mg)
319
+
320
+ nx.draw(
321
+ mg,
322
+ pos,
323
+ font_size=20,
324
+ font_color="white",
325
+ node_size=700,
326
+ width=1.5,
327
+ with_labels=True,
328
+ )
329
+ plt.show()
330
+
331
+ def display_cycles(self, cmap=None):
332
+ """
333
+ Display the cycles in the margin graph.
334
+
335
+ Args:
336
+ cmap (dict, optional): The cmap used to map candidates to candidate names
337
+ """
338
+
339
+ cycles = self.cycles()
340
+
341
+ print(f"There {'are' if len(cycles) != 1 else 'is'} {len(cycles)} {'cycle' if len(cycles) == 1 else 'cycles'}{':' if len(cycles) > 0 else '.'} \n")
342
+ for cycle in cycles:
343
+ cmap = cmap if cmap is not None else self.cmap
344
+ cmap_inverse = {cname: c for c, cname in cmap.items()}
345
+ mg_with_cycle = nx.DiGraph()
346
+
347
+ mg_with_cycle.add_nodes_from([cmap[c] for c in self.candidates])
348
+ mg_with_cycle.add_edges_from([(cmap[e[0]], cmap[e[1]]) for e in self.edges])
349
+
350
+ cands = self.candidates
351
+ mg_edges = list(self.edges)
352
+
353
+ cycle_edges = [(cmap[c], cmap[cycle[cidx + 1]]) for cidx,c in enumerate(cycle[0:-1])] + [(cmap[cycle[-1]], cmap[cycle[0]])]
354
+
355
+ cands_in_cycle = [cmap[c1] for c1 in cands if c1 in cycle]
356
+
357
+ node_colors = ["blue" if n in cands_in_cycle else "lightgray" for n in mg_with_cycle.nodes ]
358
+
359
+ pos = nx.circular_layout(mg_with_cycle)
360
+ nx.draw(mg_with_cycle, pos, width=1.5, edge_color="white")
361
+ nx.draw_networkx_nodes(
362
+ mg_with_cycle, pos, node_color=node_colors, node_size=700
363
+ )
364
+ nx.draw_networkx_labels(mg_with_cycle, pos, font_size=20, font_color="white")
365
+ nx.draw_networkx_edges(
366
+ mg_with_cycle,
367
+ pos,
368
+ edgelist=cycle_edges,
369
+ width=10,
370
+ alpha=1.0,
371
+ edge_color="b",
372
+ arrowsize=25,
373
+ min_target_margin=15,
374
+ node_size=700,
375
+ )
376
+
377
+ nx.draw_networkx_edges(
378
+ mg_with_cycle,
379
+ pos,
380
+ edgelist=[(cmap[e[0]], cmap[e[1]]) for e in mg_edges if (cmap[e[0]], cmap[e[1]]) not in cycle_edges],
381
+ width=1.5,
382
+ edge_color="lightgray",
383
+ arrowsize=15,
384
+ min_target_margin=15,
385
+ )
386
+
387
+ ax = plt.gca()
388
+ ax.set_frame_on(False)
389
+ plt.show()
390
+
391
+ def to_latex(self, cmap=None, new_cand=None):
392
+ """Outputs TikZ code for displaying the majority graph.
393
+
394
+ :param cmap: the candidate map to use (overrides the cmap associated with this majority graph)
395
+ :type cmap: dict[int,str], optional
396
+ :param new_cand: the candidate that is displayed on the far right, *only used for displaying 5 candidates*.
397
+ :type new_cand: int
398
+ :rtype: str
399
+
400
+ .. warning:: This works best for 3, 4 or 5 candidates. It will produce the code for more than 5 outputs, but the positioning of the nodes may need to be modified.
401
+
402
+ :Example:
403
+
404
+ .. exec_code::
405
+
406
+ from pref_voting.weighted_majority_graphs import MajorityGraph
407
+ mg = MajorityGraph([0,1,2], [(0,1), (1,2), (2,0)])
408
+ print(mg.to_latex())
409
+ print(mg.to_latex(cmap = {0:"a", 1:"b", 2:"c"}))
410
+
411
+ """
412
+ if len(self.candidates) == 3:
413
+ return three_cand_tikz_str(self, cmap=cmap)
414
+ elif len(self.candidates) == 4:
415
+ return four_cand_tikz_str(self, cmap=cmap)
416
+ elif len(self.candidates) == 5:
417
+ return five_cand_tikz_str(self, cmap=cmap, new_cand=new_cand)
418
+ else:
419
+ pos = nx.circular_layout(self.mg)
420
+ return to_tikz_str(self, pos, cmap=cmap)
421
+
422
+ @classmethod
423
+ def from_profile(cls, profile, cmap=None):
424
+ """Generates a majority graph from a :class:`Profile`.
425
+
426
+ :param profile: the profile
427
+ :type profile: Profile
428
+ :param cmap: the candidate map to use (overrides the cmap associated with this majority graph)
429
+ :type cmap: dict[int,str], optional
430
+ :rtype: str
431
+
432
+ :Example:
433
+
434
+ .. exec_code::
435
+
436
+ from pref_voting.profiles import Profile
437
+ from pref_voting.weighted_majority_graphs import MajorityGraph
438
+ prof = Profile([[0,1,2], [1,2,0], [2,0,1]])
439
+ mg = MajorityGraph.from_profile(prof)
440
+ print(mg.edges)
441
+
442
+ # it is better to use the Profile method
443
+ mg = prof.majority_graph()
444
+ print(mg.edges)
445
+
446
+ """
447
+ cmap = profile.cmap if cmap is None else cmap
448
+ return cls(
449
+ profile.candidates,
450
+ [
451
+ (c1, c2)
452
+ for c1 in profile.candidates
453
+ for c2 in profile.candidates
454
+ if profile.majority_prefers(c1, c2)
455
+ ],
456
+ cmap=cmap,
457
+ )
458
+
459
+ def __add__(self, other_mg):
460
+ """
461
+ Add to majority graphs together. The result is a majority graph with the union of the candidates and edges of the two majority graphs. If there is an edge from :math:`c` to :math:`d` in both majority graphs, then the edge is included in the resulting majority graph. If there is an edge from :math:`c` to :math:`d` in neither majority graph, then there is no edge from :math:`c` to :math:`d` in the resulting majority graph.
462
+ """
463
+ new_cands = list(set(self.candidates + other_mg.candidates))
464
+
465
+ _new_edges = list(set(self.edges + other_mg.edges))
466
+
467
+ new_edges = list()
468
+ for c1, c2 in _new_edges:
469
+ if (self.majority_prefers(c1, c2) and other_mg.majority_prefers(c2, c1)) or (self.majority_prefers(c2, c1) and other_mg.majority_prefers(c1, c2)):
470
+ continue
471
+ else:
472
+ new_edges.append((c1, c2))
473
+ return MajorityGraph(new_cands, new_edges)
474
+
475
+ def __eq__(self, other_mg):
476
+ """
477
+ Check if two majority graphs are equal.
478
+ """
479
+ return self.candidates == other_mg.candidates and sorted(self.edges) == sorted(other_mg.edges)
480
+
481
+ class MarginGraph(MajorityGraph):
482
+ """A margin graph is a weighted asymmetric directed graph. The nodes are the candidates and an edge from candidate :math:`c` to :math:`d` with weight :math:`w` means that :math:`c` is majority preferred to :math:`d` by a **margin** of :math:`w`.
483
+
484
+ :param candidates: List of the candidates. To be used as nodes in the majority graph.
485
+ :type candidates: list[int] or list[str]
486
+ :param w_edges: List of the pairs of candidates describing the edges in the majority graph. If :math:`(c,d,w)` is in the list of edges, then there is an edge from :math:`c` to :math:`d` with weight :math:`w`.
487
+ :type w_edges: list
488
+ :param cmap: Dictionary mapping candidates to candidate names (strings). If not provided, each candidate name is mapped to itself.
489
+ :type cmap: dict[int: str], optional
490
+
491
+ :Example:
492
+
493
+ The following code creates a margin graph in which 0 is majority preferred to 1 by a margin of 1, 1 is majority preferred to 2 by a margin of 3, and 2 is majority preferred to 0 by a margin of 5:
494
+
495
+ .. code-block:: python
496
+
497
+ mg = MarginGraph([0, 1, 2], [(0,1,1), (1,2,3), (2,0,5)])
498
+
499
+ .. warning:: Currently, there is no check that the edge relation is asymmetric or that weights of edges are positive. It is assumed that the user provides an appropriate set of edges with weights.
500
+ """
501
+
502
+ def __init__(self, candidates, w_edges, cmap=None):
503
+ """constructor method"""
504
+
505
+ super().__init__(candidates, [(e[0], e[1]) for e in w_edges], cmap=cmap)
506
+
507
+ self.margin_matrix = [[0 for c2 in self.cindices] for c1 in self.cindices]
508
+ """The margin matrix, where the :math:`(i, j)`-entry is the number of voters who rank candidate with index :math:`i` above the candidate with index :math:`j` minus the number of voters who rank candidate with index :math:`j` above the candidate with index :math:`i`. """
509
+
510
+ for c1, c2, margin in w_edges:
511
+ self.margin_matrix[self.cand_to_cindex(c1)][self.cand_to_cindex(c2)] = margin
512
+ self.margin_matrix[self.cand_to_cindex(c2)][self.cand_to_cindex(c1)] = -1 * margin
513
+
514
+ def margin(self, c1, c2):
515
+ """Returns the margin of ``c1`` over ``c2``."""
516
+ return self.margin_matrix[self.cand_to_cindex(c1)][self.cand_to_cindex(c2)]
517
+
518
+ def strength_matrix(self, curr_cands = None, strength_function = None):
519
+ """
520
+ Return the strength matrix of the profile. The strength matrix is a matrix where the entry in row :math:`i` and column :math:`j` is the number of voters that rank the candidate with index :math:`i` over the candidate with index :math:`j`. If ``curr_cands`` is provided, then the strength matrix is restricted to the candidates in ``curr_cands``. If ``strength_function`` is provided, then the strength matrix is computed using the strength function."""
521
+
522
+ if curr_cands is not None:
523
+ cindices = [cidx for cidx, _ in enumerate(curr_cands)]
524
+ cindex_to_cand = lambda cidx: curr_cands[cidx]
525
+ cand_to_cindex = lambda c: cindices[curr_cands.index(c)]
526
+ strength_function = self.margin if strength_function is None else strength_function
527
+ strength_matrix = np.array([[strength_function(cindex_to_cand(a_idx), cindex_to_cand(b_idx)) for b_idx in cindices] for a_idx in cindices])
528
+ else:
529
+ cindices = self.cindices
530
+ cindex_to_cand = self.cindex_to_cand
531
+ cand_to_cindex = self.cand_to_cindex
532
+ strength_matrix = np.array(self.margin_matrix) if strength_function is None else np.array([[strength_function(cindex_to_cand(a_idx), cindex_to_cand(b_idx)) for b_idx in cindices] for a_idx in cindices])
533
+
534
+ return strength_matrix, cand_to_cindex
535
+
536
+ @property
537
+ def edges(self):
538
+ """Returns a list of the weighted edges in the margin graph."""
539
+
540
+ return [(c1, c2, self.margin(c1, c2)) for c1, c2 in self.mg.edges]
541
+
542
+ def remove_candidates(self, cands_to_ignore):
543
+ """Remove all candidates from ``cands_to_ignore`` from the Majority Graph.
544
+
545
+ :param cands_to_ignore: list of candidates to remove from the profile
546
+ :type cands_to_ignore: list[int]
547
+ :returns: a majority graph with candidates from ``cands_to_ignore`` removed and a dictionary mapping the candidates from the new profile to the original candidate names.
548
+
549
+ :Example:
550
+
551
+ .. exec_code::
552
+
553
+ from pref_voting.weighted_majority_graphs import MarginGraph
554
+ mg = MarginGraph([0, 1, 2], [(0, 1, 11), (1, 2, 13), (2, 0, 5)])
555
+ print(f"Candidates: {mg.candidates}")
556
+ print(f"Edges: {mg.edges}")
557
+ mg_new = mg.remove_candidates([1])
558
+ print(f"Candidates: {mg_new.candidates}")
559
+ print(f"Edges: {mg_new.edges}")
560
+ """
561
+
562
+ new_cands = [c for c in self.candidates if c not in cands_to_ignore]
563
+
564
+ new_edges = [e for e in self.edges if e[0] in new_cands and e[1] in new_cands]
565
+
566
+ new_cmap = {c: cname for c, cname in self.cmap.items() if c in new_cands}
567
+
568
+ return MarginGraph(new_cands, new_edges, cmap=new_cmap)
569
+
570
+ def majority_prefers(self, c1, c2):
571
+ """Returns True if the margin of ``c1`` over ``c2`` is positive."""
572
+ return self.margin_matrix[self.cand_to_cindex(c1)][self.cand_to_cindex(c2)] > 0
573
+
574
+ def is_tied(self, c1, c2):
575
+ """Returns True if the margin ``c1`` over ``c2`` is zero."""
576
+ return self.margin_matrix[self.cand_to_cindex(c1)][self.cand_to_cindex(c2)] == 0
577
+
578
+ def is_uniquely_weighted(self):
579
+ """Returns True if all the margins between distinct candidates are unique and there is no 0 margin between distinct candidates."""
580
+ has_zero_margins = any(
581
+ [
582
+ self.margin(c1, c2) == 0
583
+ for c1 in self.candidates
584
+ for c2 in self.candidates
585
+ if c1 != c2
586
+ ]
587
+ )
588
+ return not has_zero_margins and len(
589
+ list(set([self.margin(e[0], e[1]) for e in self.mg.edges]))
590
+ ) == len(self.mg.edges)
591
+
592
+
593
+ def to_networkx(self):
594
+ """
595
+ Return a networkx weighted DiGraph representing the margin graph.
596
+ """
597
+
598
+ g = nx.DiGraph()
599
+ g.add_nodes_from(self.candidates)
600
+ g.add_weighted_edges_from(self.edges)
601
+
602
+ return g
603
+
604
+ def debord_profile(self):
605
+ """
606
+ Find a profile that generates the margin graph using the algorithm from Debord's (1987) proof.
607
+ """
608
+
609
+ from pref_voting.profiles import Profile
610
+
611
+ candidates = self.candidates
612
+
613
+ ranks = list()
614
+ rcounts = list()
615
+
616
+ if all([w % 2 == 0 for _,_,w in self.edges]):
617
+ for c1, c2, w in self.edges:
618
+ other_cands = [c for c in candidates if c != c1 and c != c2]
619
+
620
+ lin_ord1 = sorted(other_cands)
621
+ lin_ord2 = sorted(other_cands, reverse=True)
622
+
623
+ ranks.append([c1, c2] + lin_ord1)
624
+ rcounts.append(w//2)
625
+ ranks.append(lin_ord2 + [c1, c2])
626
+ rcounts.append(w//2)
627
+ return Profile(ranks, rcounts, cmap=self.cmap)
628
+
629
+ elif all([w % 2 == 1 for _,_,w in self.edges]): # all weights are odd
630
+ single_prof = Profile([sorted(candidates)], [1])
631
+
632
+ for c1, c2, w in self.edges:
633
+ other_cands = [c for c in candidates if c != c1 and c != c2]
634
+ lin_ord1 = sorted(other_cands)
635
+ lin_ord2 = sorted(other_cands, reverse=True)
636
+ if w-single_prof.margin(c1, c2) > 0:
637
+ ranks.append([c1, c2] + lin_ord1)
638
+ rcounts.append((w-single_prof.margin(c1, c2))//2)
639
+ ranks.append(lin_ord2 + [c1, c2])
640
+ rcounts.append((w-single_prof.margin(c1, c2))//2)
641
+ ranks.append(sorted(candidates))
642
+ rcounts.append(1)
643
+ return Profile(ranks, rcounts, cmap=self.cmap)
644
+ else:
645
+ print("Cannot find a Profile since the weights do not all have the same parity.")
646
+ return None
647
+
648
+ def minimal_profile(self):
649
+ """
650
+ Use an integer linear program to find a minimal profile generating the margin graph.
651
+ """
652
+ from pref_voting.profiles import Profile
653
+
654
+ solver = pywraplp.Solver.CreateSolver("SAT")
655
+
656
+ num_cands = len(self.candidates)
657
+ rankings = list(permutations(range(num_cands)))
658
+
659
+ ranking_to_var = dict()
660
+ infinity = solver.infinity()
661
+ for ridx, r in enumerate(rankings):
662
+ _v = solver.IntVar(0.0, infinity, f"x{ridx}")
663
+ ranking_to_var[r] = _v
664
+
665
+ nv = solver.IntVar(0.0, infinity, "nv")
666
+ equations = list()
667
+ for c1 in self.candidates:
668
+ for c2 in self.candidates:
669
+ if c1 != c2:
670
+ margin = self.margin(c1, c2)
671
+ if margin >= 0:
672
+ rankings_c1_over_c2 = [ranking_to_var[r] for r in rankings if r.index(c1) < r.index(c2)]
673
+ rankings_c2_over_c1 = [ranking_to_var[r] for r in rankings if r.index(c2) < r.index(c1)]
674
+ equations.append(sum(rankings_c1_over_c2) == margin + sum(rankings_c2_over_c1))
675
+
676
+ equations.append(nv == sum(list(ranking_to_var.values())))
677
+
678
+ for eq in equations:
679
+ solver.Add(eq)
680
+
681
+ solver.Minimize(nv)
682
+
683
+ status = solver.Solve()
684
+
685
+ if status == pywraplp.Solver.INFEASIBLE:
686
+ print("Error: Did not find a solution.")
687
+ return None
688
+
689
+ if status != pywraplp.Solver.OPTIMAL:
690
+ print("Warning: Did not find an optimal solution.")
691
+
692
+ _ranks = list()
693
+ _rcounts = list()
694
+
695
+ for r,v in ranking_to_var.items():
696
+
697
+ if v.solution_value() > 0:
698
+ _ranks.append(r)
699
+ _rcounts.append(int(v.solution_value()))
700
+ if not v.solution_value().is_integer():
701
+ print("ERROR: Found non integer, ", v.solution_value())
702
+ return None
703
+ return Profile(_ranks, rcounts = _rcounts)
704
+
705
+ def normalize_ordered_weights(self):
706
+ """
707
+ Returns a MarginGraph with the same order of the edges, except that the weights are 2, 4, 6,...
708
+
709
+ .. important::
710
+
711
+ This function returns a margin graph that has the same ordering of the edges, but the edges may have different weights. Qualitative margin graph invariant
712
+ voting methods will identify the same winning sets for both graphs.
713
+
714
+ """
715
+
716
+ sorted_edges = sorted(self.edges, key=lambda e: e[2])
717
+ sorted_margins = sorted(
718
+ [
719
+ self.margin(c1, c2)
720
+ for c1 in self.candidates
721
+ for c2 in self.candidates
722
+ if self.margin(c1, c2) > 0
723
+ ]
724
+ )
725
+ curr_margin = sorted_margins[0]
726
+ new_margin = 2
727
+ new_edges = list()
728
+ for e in sorted_edges:
729
+ if e[2] > curr_margin:
730
+ curr_margin = e[2]
731
+ new_margin += 2
732
+ new_edges.append((e[0], e[1], new_margin))
733
+
734
+ else:
735
+ new_edges.append((e[0], e[1], new_margin))
736
+
737
+ return MarginGraph(self.candidates, new_edges, cmap=self.cmap)
738
+
739
+ def description(self):
740
+ """
741
+ Returns a string describing the Margin Graph.
742
+ """
743
+ return f"MarginGraph({self.candidates}, {self.edges}, cmap={self.cmap})"
744
+
745
+ def display(self, curr_cands=None, cmap=None):
746
+ """Display a margin graph (restricted to ``curr_cands``) using networkx.draw.
747
+
748
+ :param cmap: the candidate map to use (overrides the cmap associated with this majority graph)
749
+ :type cmap: dict[int,str], optional
750
+ :param curr_cands: list of candidates
751
+ :type curr_cands: list[int], optional
752
+ :rtype: None
753
+
754
+ :Example:
755
+
756
+ .. code::
757
+
758
+ from pref_voting.weighted_majority_graphs import MarginGraph
759
+ mg = MarginGraph([0,1,2], [(0,1,3), (1,2,1), (2,0,5)])
760
+ mg.display()
761
+
762
+ .. image:: ./margin_graph_ex1.png
763
+ :width: 400
764
+ :alt: Alternative text
765
+
766
+ """
767
+
768
+ cmap = cmap if cmap is not None else self.cmap
769
+ curr_cands = self.candidates if curr_cands is None else curr_cands
770
+
771
+ mg = nx.DiGraph()
772
+ mg.add_nodes_from([cmap[c] for c in curr_cands])
773
+ mg.add_weighted_edges_from(
774
+ [
775
+ (cmap[c1], cmap[c2], self.margin(c1, c2))
776
+ for c1, c2 in self.mg.edges
777
+ if c1 in curr_cands and c2 in curr_cands
778
+ ]
779
+ )
780
+
781
+ pos = nx.circular_layout(mg)
782
+
783
+ nx.draw(
784
+ mg,
785
+ pos,
786
+ font_size=20,
787
+ font_color="white",
788
+ node_size=700,
789
+ width=1.5,
790
+ with_labels=True,
791
+ )
792
+ labels = nx.get_edge_attributes(mg, "weight")
793
+ nx.draw_networkx_edge_labels(
794
+ mg, pos, edge_labels=labels, font_size=14, label_pos=0.3
795
+ )
796
+
797
+ plt.show()
798
+
799
+ def display_with_defeat(
800
+ self, defeat, curr_cands=None, show_undefeated=True, cmap=None
801
+ ):
802
+ """
803
+ Display the margin graph with any edges that are ``defeat`` edges highlighted in blue.
804
+
805
+ Args:
806
+ defeat (networkx.DiGraph): The defeat relation represented as a networkx object.
807
+ curr_cands (List[int], optional): If set, then use the defeat relation for the profile restricted to the candidates in ``curr_cands``
808
+ show_undefeated (bool, optional): If true, color the undefeated candidates blue and the other candidates red.
809
+ cmap (dict, optional): The cmap used to map candidates to candidate names
810
+
811
+
812
+ """
813
+
814
+ cmap = cmap if cmap is not None else self.cmap
815
+ cmap_inverse = {cname: c for c, cname in cmap.items()}
816
+ mg_with_defeat = nx.DiGraph()
817
+
818
+ mg = nx.DiGraph()
819
+ mg.add_nodes_from([cmap[c] for c in self.mg.nodes])
820
+ mg.add_edges_from([(cmap[e[0]], cmap[e[1]]) for e in self.mg.edges])
821
+
822
+ cands = self.candidates if curr_cands is None else curr_cands
823
+
824
+ mg_edges = list(mg.edges())
825
+ defeat_edges = list(defeat.edges())
826
+ edges = mg_edges + [
827
+ (cmap[e[0]], cmap[e[1]])
828
+ for e in defeat_edges
829
+ if (cmap[e[0]], cmap[e[1]]) not in mg_edges
830
+ ]
831
+
832
+ mg_with_defeat.add_nodes_from([cmap[c] for c in cands])
833
+ mg_with_defeat.add_edges_from(edges)
834
+
835
+ if show_undefeated:
836
+
837
+ undefeated_cands = [
838
+ cmap[c1]
839
+ for c1 in cands
840
+ if not any([defeat.has_edge(c2, c1) for c2 in cands])
841
+ ]
842
+ node_colors = [
843
+ "blue" if n in undefeated_cands else "red" for n in mg_with_defeat.nodes
844
+ ]
845
+
846
+ else:
847
+ node_colors = ["#1f78b4" for n in mg_with_defeat.nodes]
848
+
849
+ pos = nx.circular_layout(mg_with_defeat)
850
+ nx.draw(mg, pos, width=1.5, edge_color="white")
851
+ nx.draw_networkx_nodes(
852
+ mg_with_defeat, pos, node_color=node_colors, node_size=700
853
+ )
854
+ nx.draw_networkx_labels(mg_with_defeat, pos, font_size=20, font_color="white")
855
+ nx.draw_networkx_edges(
856
+ mg_with_defeat,
857
+ pos,
858
+ edgelist=[
859
+ (cmap[e[0]], cmap[e[1]])
860
+ for e in defeat_edges
861
+ if (cmap[e[0]], cmap[e[1]]) in mg_edges
862
+ ],
863
+ width=10,
864
+ alpha=0.5,
865
+ edge_color="b",
866
+ arrowsize=25,
867
+ min_target_margin=15,
868
+ node_size=700,
869
+ )
870
+ collection = nx.draw_networkx_edges(
871
+ mg_with_defeat,
872
+ pos,
873
+ edgelist=[
874
+ (cmap[e[0]], cmap[e[1]])
875
+ for e in defeat_edges
876
+ if (cmap[e[0]], cmap[e[1]]) not in mg_edges
877
+ ],
878
+ width=10,
879
+ alpha=0.5,
880
+ edge_color="b",
881
+ arrowsize=25,
882
+ min_target_margin=15,
883
+ )
884
+ if collection is not None:
885
+ for patch in collection:
886
+ patch.set_linestyle("dashed")
887
+
888
+ nx.draw_networkx_edges(
889
+ mg_with_defeat,
890
+ pos,
891
+ edgelist=mg_edges,
892
+ width=1.5,
893
+ arrowsize=15,
894
+ min_target_margin=15,
895
+ )
896
+ labels = {
897
+ e: self.margin(cmap_inverse[e[0]], cmap_inverse[e[1]]) for e in mg_edges
898
+ }
899
+
900
+ nx.draw_networkx_edge_labels(
901
+ mg_with_defeat, pos, edge_labels=labels, font_size=14, label_pos=0.3
902
+ )
903
+ ax = plt.gca()
904
+ ax.set_frame_on(False)
905
+ plt.show()
906
+
907
+ def display_cycles(self, cmap=None):
908
+ """
909
+ Display the cycles in the margin graph.
910
+
911
+ Args:
912
+ cmap (dict, optional): The cmap used to map candidates to candidate names.
913
+
914
+ """
915
+
916
+ cycles = self.cycles()
917
+
918
+ print(f"There {'are' if len(cycles) != 1 else 'is'} {len(cycles)} {'cycle' if len(cycles) == 1 else 'cycles'}{':' if len(cycles) > 0 else '.'} \n")
919
+ for cycle in cycles:
920
+ cmap = cmap if cmap is not None else self.cmap
921
+ mg_with_cycle = nx.DiGraph()
922
+
923
+ mg_with_cycle.add_nodes_from([cmap[c] for c in self.candidates])
924
+ mg_with_cycle.add_edges_from([(cmap[e[0]], cmap[e[1]]) for e in self.edges])
925
+
926
+ cands = self.candidates
927
+ mg_edges = list(self.edges)
928
+
929
+ cycle_edges = [(cmap[c], cmap[cycle[cidx + 1]]) for cidx,c in enumerate(cycle[0:-1])] + [(cmap[cycle[-1]], cmap[cycle[0]])]
930
+
931
+ cands_in_cycle = [cmap[c1] for c1 in cands if c1 in cycle]
932
+
933
+ node_colors = ["blue" if n in cands_in_cycle else "lightgray" for n in mg_with_cycle.nodes ]
934
+
935
+ pos = nx.circular_layout(mg_with_cycle)
936
+ nx.draw(mg_with_cycle, pos, width=1.5, edge_color="white")
937
+ nx.draw_networkx_nodes(
938
+ mg_with_cycle, pos, node_color=node_colors, node_size=700
939
+ )
940
+ nx.draw_networkx_labels(mg_with_cycle, pos, font_size=20, font_color="white")
941
+ nx.draw_networkx_edges(
942
+ mg_with_cycle,
943
+ pos,
944
+ edgelist=cycle_edges,
945
+ width=10,
946
+ alpha=1.0,
947
+ edge_color="b",
948
+ arrowsize=25,
949
+ min_target_margin=15,
950
+ node_size=700,
951
+ )
952
+
953
+ nx.draw_networkx_edges(
954
+ mg_with_cycle,
955
+ pos,
956
+ edgelist=[(cmap[e[0]], cmap[e[1]]) for e in mg_edges if (cmap[e[0]], cmap[e[1]]) not in cycle_edges],
957
+ width=1.5,
958
+ edge_color="lightgray",
959
+ arrowsize=15,
960
+ min_target_margin=15,
961
+ )
962
+ labels = {
963
+ (cmap[e[0]], cmap[e[1]]): self.margin(e[0], e[1]) for e in mg_edges
964
+ }
965
+
966
+ nx.draw_networkx_edge_labels(
967
+ mg_with_cycle, pos, edge_labels=labels, font_size=14, label_pos=0.3
968
+ )
969
+ ax = plt.gca()
970
+ ax.set_frame_on(False)
971
+ plt.show()
972
+
973
+ @classmethod
974
+ def from_profile(cls, profile, cmap=None):
975
+ """Generates a majority graph from a :class:`Profile`.
976
+
977
+ :param profile: the profile
978
+ :type profile: Profile
979
+ :param cmap: the candidate map to use (overrides the cmap associated with this majority graph)
980
+ :type cmap: dict[int,str], optional
981
+ :rtype: str
982
+
983
+ :Example:
984
+
985
+ .. exec_code::
986
+
987
+ from pref_voting.profiles import Profile
988
+ from pref_voting.weighted_majority_graphs import MarginGraph
989
+ prof = Profile([[0,1,2], [1,2,0], [2,0,1]], [2, 1, 2])
990
+ mg = MarginGraph.from_profile(prof)
991
+ print(mg.edges)
992
+ print(mg.margin_matrix)
993
+
994
+ # it is better to use the Profile method
995
+ mg = prof.margin_graph()
996
+ print(mg.edges)
997
+ print(mg.margin_matrix)
998
+
999
+ """
1000
+
1001
+ cmap = profile.cmap if cmap is None else cmap
1002
+ return cls(
1003
+ profile.candidates,
1004
+ [
1005
+ (c1, c2, profile.margin(c1, c2))
1006
+ for c1 in profile.candidates
1007
+ for c2 in profile.candidates
1008
+ if profile.majority_prefers(c1, c2)
1009
+ ],
1010
+ cmap=cmap,
1011
+ )
1012
+
1013
+ def __add__(self, edata):
1014
+ """
1015
+ Return a MarginGraph in which the new margin of candidate :math:`a` over :math:`b` is the sum of the
1016
+ existing margin of :math:`a` over :math:`b` with with the margin :math:`a` over :math:`b` in ``edata``.
1017
+ """
1018
+ candidates = self.candidates
1019
+ new_edges = list()
1020
+ for c1, c2 in combinations(candidates, 2):
1021
+
1022
+ new_margin = self.margin(c1, c2) + edata.margin(c1, c2)
1023
+
1024
+ if new_margin > 0:
1025
+ new_edges.append((c1, c2, new_margin))
1026
+ elif new_margin < 0:
1027
+
1028
+ new_edges.append((c2, c1, -1 * new_margin))
1029
+
1030
+ return MarginGraph(candidates, new_edges, cmap=self.cmap)
1031
+
1032
+ def __eq__(self, other_mg):
1033
+ """
1034
+ Return True if the margin graphs are equal (the candidates, and all edges and weights are the same); otherwise, return False
1035
+ """
1036
+
1037
+ return self.candidates == other_mg.candidates and sorted(self.edges) == sorted(other_mg.edges)
1038
+
1039
+ class SupportGraph(MajorityGraph):
1040
+ """A support graph is a weighted asymmetric directed graph. The nodes are the candidates and an edge from candidate :math:`c` to :math:`d` with weight :math:`w` means that the **support** of :math:`c` over :math:`d` is :math:`w`.
1041
+
1042
+ :param candidates: List of the candidates. To be used as nodes in the majority graph.
1043
+ :type candidates: list[int] or list[str]
1044
+ :param w_edges: List representing the edges in the majority graph with supports. If :math:`(c,d,(n,m))` is in the list of edges, then there is an edge from :math:`c` to :math:`d`, the support for :math:`c` over :math:`d` is :math:`n`, and the support for :math:`d` over :math:`c` is :math:`m`.
1045
+ :type w_edges: list
1046
+ :param cmap: Dictionary mapping candidates to candidate names (strings). If not provided, each candidate name is mapped to itself.
1047
+ :type cmap: dict[int: str], optional
1048
+
1049
+ :Example:
1050
+
1051
+ The following code creates a support graph in which:
1052
+
1053
+ - 0 is majority preferred to 1, the number of voters who rank 0 over 1 is 4, and the number of voters who rank 1 over 0 is 3;
1054
+ - 1 is majority preferred to 2, the number of voters who rank 1 over 2 is 5, and the number of voters who rank 2 over 1 is 2; and
1055
+ - 2 is majority preferred to 0, the number of voters who rank 2 over 0 is 6, and the number of voters who rank 0 over 2 is 1.
1056
+
1057
+ .. code-block:: python
1058
+
1059
+ sg = SupportGraph([0, 1, 2], [(0, 1, (4, 3)), (1, 2, (5, 2)), (2, 0, (6, 1))])
1060
+
1061
+ .. warning:: Currently, there is no check to that the edge relation is asymmetric. It is assumed that the user provides an appropriate set of edges with weights.
1062
+ """
1063
+
1064
+ def __init__(self, candidates, w_edges, cmap=None):
1065
+ """constructor method"""
1066
+
1067
+ super().__init__(
1068
+ candidates,
1069
+ [
1070
+ (e[0], e[1]) if e[2][0] > e[2][1] else (e[1], e[0])
1071
+ for e in w_edges
1072
+ if e[2][0] != e[2][1]
1073
+ ],
1074
+ cmap=cmap,
1075
+ )
1076
+
1077
+ self.s_matrix = [[0 for c2 in self.cindices] for c1 in self.cindices]
1078
+ """The support matrix, where the :math:`(i, j)`-entry is the number of voters who rank candidate with index :math:`i` above the candidate with index :math:`j`. """
1079
+
1080
+ for c1, c2, support in w_edges:
1081
+ self.s_matrix[self.cand_to_cindex(c1)][self.cand_to_cindex(c2)] = support[0]
1082
+ self.s_matrix[self.cand_to_cindex(c2)][self.cand_to_cindex(c1)] = support[1]
1083
+
1084
+ @property
1085
+ def edges(self):
1086
+ """Returns a list of the weighted edges in the margin graph."""
1087
+
1088
+ return [
1089
+ (c1, c2, (self.support(c1, c2), self.support(c2, c1)))
1090
+ for c1, c2 in self.mg.edges
1091
+ ]
1092
+
1093
+ def margin(self, c1, c2):
1094
+ """Returns the margin of ``c1`` over ``c2``."""
1095
+
1096
+ return (
1097
+ self.s_matrix[self.cand_to_cindex(c1)][self.cand_to_cindex(c2)]
1098
+ - self.s_matrix[self.cand_to_cindex(c2)][self.cand_to_cindex(c1)]
1099
+ )
1100
+
1101
+ def support(self, c1, c2):
1102
+ """Returns the support of ``c1`` over ``c2``."""
1103
+
1104
+ return self.s_matrix[self.cand_to_cindex(c1)][self.cand_to_cindex(c2)]
1105
+
1106
+ def majority_prefers(self, c1, c2):
1107
+ """Returns True if ``c1`` is majority preferred to ``c2``."""
1108
+
1109
+ return (
1110
+ self.s_matrix[self.cand_to_cindex(c1)][self.cand_to_cindex(c2)]
1111
+ > self.s_matrix[self.cand_to_cindex(c2)][self.cand_to_cindex(c1)]
1112
+ )
1113
+
1114
+ def is_tied(self, c1, c2):
1115
+ """Returns True if ``c1`` is tied with ``c2``."""
1116
+
1117
+ return (
1118
+ self.s_matrix[self.cand_to_cindex(c1)][self.cand_to_cindex(c2)]
1119
+ == self.s_matrix[self.cand_to_cindex(c2)][self.cand_to_cindex(c1)]
1120
+ )
1121
+
1122
+ def strength_matrix(self, curr_cands = None, strength_function = None):
1123
+ """
1124
+ Return the strength matrix of the profile. The strength matrix is a matrix where the entry in row :math:`i` and column :math:`j` is the number of voters that rank the candidate with index :math:`i` over the candidate with index :math:`j`. If ``curr_cands`` is provided, then the strength matrix is restricted to the candidates in ``curr_cands``. If ``strength_function`` is provided, then the strength matrix is computed using the strength function."""
1125
+
1126
+ if curr_cands is not None:
1127
+ cindices = [cidx for cidx, _ in enumerate(curr_cands)]
1128
+ cindex_to_cand = lambda cidx: curr_cands[cidx]
1129
+ cand_to_cindex = lambda c: cindices[curr_cands.index(c)]
1130
+ strength_function = self.support if strength_function is None else strength_function
1131
+ strength_matrix = np.array([[strength_function(cindex_to_cand(a_idx), cindex_to_cand(b_idx)) for b_idx in cindices] for a_idx in cindices])
1132
+ else:
1133
+ cindices = self.cindices
1134
+ cindex_to_cand = self.cindex_to_cand
1135
+ cand_to_cindex = self.cand_to_cindex
1136
+ strength_matrix = np.array(self.s_matrix) if strength_function is None else np.array([[strength_function(cindex_to_cand(a_idx), cindex_to_cand(b_idx)) for b_idx in cindices] for a_idx in cindices])
1137
+
1138
+ return strength_matrix, cand_to_cindex
1139
+
1140
+ def remove_candidates(self, cands_to_ignore):
1141
+ """Remove all candidates from ``cands_to_ignore`` from the Majority Graph.
1142
+
1143
+ :param cands_to_ignore: list of candidates to remove from the profile
1144
+ :type cands_to_ignore: list[int]
1145
+ :returns: a majority graph with candidates from ``cands_to_ignore`` removed and a dictionary mapping the candidates from the new profile to the original candidate names.
1146
+
1147
+ :Example:
1148
+
1149
+ .. exec_code::
1150
+
1151
+ from pref_voting.weighted_majority_graphs import SupportGraph
1152
+ sg = SupportGraph([0, 1, 2], [(0, 1, (11, 1)), (1, 2, (5, 13)), (2, 0, (5, 10))])
1153
+ print(f"Candidates: {sg.candidates}")
1154
+ print(f"Edges: {sg.edges}")
1155
+ sg_new = sg.remove_candidates([1])
1156
+ print(f"Candidates: {sg_new.candidates}")
1157
+ print(f"Edges: {sg_new.edges}")
1158
+ """
1159
+
1160
+ new_cands = [c for c in self.candidates if c not in cands_to_ignore]
1161
+
1162
+ new_edges = [e for e in self.edges if e[0] in new_cands and e[1] in new_cands]
1163
+
1164
+ new_cmap = {c: cname for c, cname in self.cmap.items() if c in new_cands}
1165
+
1166
+ return SupportGraph(new_cands, new_edges, cmap=new_cmap)
1167
+
1168
+
1169
+ def display(self, curr_cands=None, cmap=None):
1170
+ """Display a support graph (restricted to ``curr_cands``) using networkx.draw.
1171
+
1172
+ :param cmap: the candidate map to use (overrides the cmap associated with this majority graph)
1173
+ :type cmap: dict[int,str], optional
1174
+ :param curr_cands: list of candidates
1175
+ :type curr_cands: list[int], optional
1176
+ :rtype: None
1177
+
1178
+ """
1179
+
1180
+ cmap = cmap if cmap is not None else self.cmap
1181
+ curr_cands = self.candidates if curr_cands is None else curr_cands
1182
+
1183
+ mg = nx.DiGraph()
1184
+ mg.add_nodes_from([cmap[c] for c in curr_cands])
1185
+ mg.add_weighted_edges_from(
1186
+ [
1187
+ (cmap[c1], cmap[c2], self.support(c1, c2))
1188
+ for c1, c2 in self.mg.edges
1189
+ if c1 in curr_cands and c2 in curr_cands
1190
+ ]
1191
+ )
1192
+
1193
+ pos = nx.circular_layout(mg)
1194
+
1195
+ nx.draw(
1196
+ mg,
1197
+ pos,
1198
+ font_size=20,
1199
+ font_color="white",
1200
+ node_size=700,
1201
+ width=1.5,
1202
+ with_labels=True,
1203
+ )
1204
+ labels = nx.get_edge_attributes(mg, "weight")
1205
+ nx.draw_networkx_edge_labels(
1206
+ mg, pos, edge_labels=labels, font_size=14, label_pos=0.3
1207
+ )
1208
+
1209
+ plt.show()
1210
+
1211
+ @classmethod
1212
+ def from_profile(cls, profile, cmap=None):
1213
+ """Generates a support graph from a :class:`Profile`.
1214
+
1215
+ :param profile: the profile
1216
+ :type profile: Profile
1217
+ :param cmap: the candidate map to use (overrides the cmap associated with this majority graph)
1218
+ :type cmap: dict[int,str], optional
1219
+ :rtype: str
1220
+
1221
+ :Example:
1222
+
1223
+ .. exec_code::
1224
+
1225
+ from pref_voting.profiles import Profile
1226
+ from pref_voting.weighted_majority_graphs import SupportGraph
1227
+ prof = Profile([[0,1,2], [1,2,0], [2,0,1]], [2, 1, 2])
1228
+ sg = SupportGraph.from_profile(prof)
1229
+ print(sg.edges)
1230
+ print(sg.s_matrix)
1231
+
1232
+ # it is better to use the Profile method
1233
+ sg = prof.support_graph()
1234
+ print(sg.edges)
1235
+ print(sg.s_matrix)
1236
+
1237
+ """
1238
+
1239
+ cmap = profile.cmap if cmap is None else cmap
1240
+ return cls(
1241
+ profile.candidates,
1242
+ [
1243
+ (c1, c2, (profile.support(c1, c2), profile.support(c2, c1)))
1244
+ for c1 in profile.candidates
1245
+ for c2 in profile.candidates
1246
+ ],
1247
+ cmap=cmap,
1248
+ )
1249
+
1250
+
1251
+ ###
1252
+ # functions to display graphs in tikz
1253
+ ##
1254
+ def three_cand_tikz_str(g, cmap=None):
1255
+ """Returns the TikZ code to display the graph ``g`` based on 3 candidates (may be a MajorityGraph, MarginGraph or a SupportGraph)."""
1256
+
1257
+ a = g.candidates[0]
1258
+ b = g.candidates[1]
1259
+ c = g.candidates[2]
1260
+
1261
+ if type(g) == MarginGraph:
1262
+ w = lambda c, d: f"node[fill=white] {{${g.margin(c,d)}$}}"
1263
+ elif type(g) == SupportGraph:
1264
+ w = lambda c, d: f"node[fill=white] {{${g.support(c,d)}$}}"
1265
+ else:
1266
+ w = lambda c, d: ""
1267
+
1268
+ cmap = g.cmap if cmap is None else cmap
1269
+
1270
+ nodes = f"""
1271
+ \\begin{{tikzpicture}}
1272
+ \\node[circle,draw,minimum width=0.25in] at (0,0) (a) {{${cmap[a]}$}};
1273
+ \\node[circle,draw,minimum width=0.25in] at (3,0) (c) {{${cmap[c]}$}};
1274
+ \\node[circle,draw,minimum width=0.25in] at (1.5,1.5) (b) {{${cmap[b]}$}};\n"""
1275
+
1276
+ if g.majority_prefers(a, b):
1277
+ ab_edge = f"\\path[->,draw,thick] (a) to {w(a,b)} (b);\n"
1278
+ elif g.majority_prefers(b, a):
1279
+ ab_edge = f"\\path[->,draw,thick] (b) to {w(b,a)} (a);\n"
1280
+ else:
1281
+ ab_edge = ""
1282
+
1283
+ if g.majority_prefers(b, c):
1284
+ bc_edge = f"\\path[->,draw,thick] (b) to {w(b,c)} (c);\n"
1285
+ elif g.majority_prefers(c, b):
1286
+ bc_edge = f"\\path[->,draw,thick] (c) to {w(c,b)} (b);\n"
1287
+ else:
1288
+ bc_edge = ""
1289
+
1290
+ if g.majority_prefers(a, c):
1291
+ ac_edge = f"\\path[->,draw,thick] (a) to {w(a,c)} (c);\n"
1292
+ elif g.majority_prefers(c, a):
1293
+ ac_edge = f"\\path[->,draw,thick] (c) to {w(c,a)} (a);\n"
1294
+ else:
1295
+ ac_edge = ""
1296
+
1297
+ return nodes + ab_edge + bc_edge + ac_edge + "\\end{tikzpicture}"
1298
+
1299
+
1300
+ def four_cand_tikz_str(g, cmap=None):
1301
+ """Returns the TikZ code to display the graph ``g`` based on 4 candidates (may be a MajorityGraph, MarginGraph or a SupportGraph)."""
1302
+
1303
+ a = g.candidates[0]
1304
+ b = g.candidates[1]
1305
+ c = g.candidates[2]
1306
+ d = g.candidates[3]
1307
+
1308
+ if type(g) == MarginGraph:
1309
+ w = lambda c, d: f"node[fill=white] {{${g.margin(c,d)}$}}"
1310
+ elif type(g) == SupportGraph:
1311
+ w = lambda c, d: f"node[fill=white] {{${g.support(c,d)}$}}"
1312
+ else:
1313
+ w = lambda c, d: ""
1314
+
1315
+ cmap = g.cmap if cmap is None else cmap
1316
+
1317
+ nodes = f"""
1318
+ \\begin{{tikzpicture}}
1319
+ \\node[circle,draw,minimum width=0.25in] at (0,0) (a) {{${cmap[a]}$}};
1320
+ \\node[circle,draw,minimum width=0.25in] at (3,0) (b) {{${cmap[b]}$}};
1321
+ \\node[circle,draw,minimum width=0.25in] at (1.5,1.5) (c) {{${cmap[c]}$}};
1322
+ \\node[circle,draw,minimum width=0.25in] at (1.5,-1.5) (d) {{${cmap[d]}$}};\n"""
1323
+
1324
+ if g.majority_prefers(a, b):
1325
+ ab_edge = f"\\path[->,draw,thick] (a) to[pos=.7] {w(a,b)} (b);\n"
1326
+ elif g.majority_prefers(b, a):
1327
+ ab_edge = f"\\path[->,draw,thick] (b) to[pos=.7] {w(b,a)} (a);\n"
1328
+ else:
1329
+ ab_edge = ""
1330
+
1331
+ if g.majority_prefers(a, c):
1332
+ ac_edge = f"\\path[->,draw,thick] (a) to {w(a,c)} (c);\n"
1333
+ elif g.majority_prefers(c, a):
1334
+ ac_edge = f"\\path[->,draw,thick] (c) to {w(c,a)} (a);\n"
1335
+ else:
1336
+ ac_edge = ""
1337
+
1338
+ if g.majority_prefers(a, d):
1339
+ ad_edge = f"\\path[->,draw,thick] (a) to {w(a,d)} (d);\n"
1340
+ elif g.majority_prefers(d, a):
1341
+ ad_edge = f"\\path[->,draw,thick] (d) to {w(d,a)} (a);\n"
1342
+ else:
1343
+ ad_edge = ""
1344
+
1345
+ if g.majority_prefers(b, c):
1346
+ bc_edge = f"\\path[->,draw,thick] (b) to {w(b,c)} (c);\n"
1347
+ elif g.majority_prefers(c, b):
1348
+ bc_edge = f"\\path[->,draw,thick] (c) to {w(c,b)} (b);\n"
1349
+ else:
1350
+ bc_edge = ""
1351
+
1352
+ if g.majority_prefers(b, d):
1353
+ bd_edge = f"\\path[->,draw,thick] (b) to {w(b,d)} (d);\n"
1354
+ elif g.majority_prefers(d, b):
1355
+ bd_edge = f"\\path[->,draw,thick] (d) to {w(d,b)} (b);\n"
1356
+ else:
1357
+ bd_edge = ""
1358
+
1359
+ if g.majority_prefers(c, d):
1360
+ cd_edge = f"\\path[->,draw,thick] (c) to[pos=.7] {w(c,d)} (d);\n"
1361
+ elif g.majority_prefers(d, c):
1362
+ cd_edge = f"\\path[->,draw,thick] (d) to[pos=.7] {w(d,c)} (c);\n"
1363
+ else:
1364
+ cd_edge = ""
1365
+
1366
+ return (
1367
+ nodes
1368
+ + ab_edge
1369
+ + ac_edge
1370
+ + ad_edge
1371
+ + bc_edge
1372
+ + bd_edge
1373
+ + cd_edge
1374
+ + "\\end{tikzpicture}"
1375
+ )
1376
+
1377
+
1378
+ def five_cand_tikz_str(g, cmap=None, new_cand=None):
1379
+
1380
+ candidates = list(g.candidates)
1381
+
1382
+ if new_cand is not None:
1383
+
1384
+ e = new_cand
1385
+
1386
+ cands_minus = [_c for _c in candidates if _c != new_cand]
1387
+
1388
+ a = cands_minus[0]
1389
+ b = cands_minus[1]
1390
+ c = cands_minus[2]
1391
+ d = cands_minus[3]
1392
+ else:
1393
+ a = candidates[0]
1394
+ b = candidates[1]
1395
+ c = candidates[2]
1396
+ d = candidates[3]
1397
+ e = candidates[4]
1398
+
1399
+ # new_cand = candidates[4]
1400
+
1401
+ node_id = {a: "a", b: "b", c: "c", d: "d", e: "e"}
1402
+ print(node_id)
1403
+ if type(g) == MarginGraph:
1404
+ w = lambda c, d: f"node[fill=white] {{${g.margin(c,d)}$}}"
1405
+ elif type(g) == SupportGraph:
1406
+ w = lambda c, d: f"node[fill=white] {{${g.support(c,d)}$}}"
1407
+ else:
1408
+ w = lambda c, d: ""
1409
+
1410
+ cmap = g.cmap if cmap is None else cmap
1411
+
1412
+ nodes = f"""
1413
+ \\begin{{tikzpicture}}
1414
+ \\node[circle,draw,minimum width=0.25in] at (2,1.5) (a) {{${cmap[a]}$}};
1415
+ \\node[circle,draw,minimum width=0.25in] at (0,1.5) (b) {{${cmap[b]}$}};
1416
+ \\node[circle,draw,minimum width=0.25in] at (0,-1.5) (c) {{${cmap[c]}$}};
1417
+ \\node[circle,draw,minimum width=0.25in] at (2,-1.5) (d) {{${cmap[d]}$}};
1418
+ \\node[circle,draw,minimum width=0.25in] at (3.5,0) (e) {{${cmap[e]}$}};\n"""
1419
+ edges = [(a, b), (a, d), (a, e), (b, c), (c, d), (d, e)]
1420
+ edges_with_pos = [(a, c), (b, d), (b, e), (c, e)]
1421
+
1422
+ edge_tikz_str = list()
1423
+ for c1, c2 in edges:
1424
+ if g.majority_prefers(c1, c2):
1425
+ edge_tikz_str.append(
1426
+ f"\\path[->,draw,thick] ({node_id[c1]}) to {w(c1,c2)} ({node_id[c2]});\n"
1427
+ )
1428
+ elif g.majority_prefers(c2, c1):
1429
+ edge_tikz_str.append(
1430
+ f"\\path[->,draw,thick] ({node_id[c2]}) to {w(c2,c1)} ({node_id[c1]});\n"
1431
+ )
1432
+ else:
1433
+ edge_tikz_str.append("")
1434
+ for c1, c2 in edges_with_pos:
1435
+ if g.majority_prefers(c1, c2):
1436
+ edge_tikz_str.append(
1437
+ f"\\path[->,draw,thick] ({node_id[c1]}) to[pos=.7] {w(c1,c2)} ({node_id[c2]});\n"
1438
+ )
1439
+ elif g.majority_prefers(c2, c1):
1440
+ edge_tikz_str.append(
1441
+ f"\\path[->,draw,thick] ({node_id[c2]}) to[pos=.7] {w(c2,c1)} ({node_id[c1]});\n"
1442
+ )
1443
+ else:
1444
+ edge_tikz_str.append("")
1445
+
1446
+ return nodes + "".join(edge_tikz_str) + "\\end{tikzpicture}"
1447
+
1448
+
1449
+ def to_tikz_str(g, pos, cmap=None):
1450
+
1451
+ node_id = {c: string.ascii_lowercase[cidx] for cidx, c in enumerate(g.candidates)}
1452
+
1453
+ if type(g) == MarginGraph:
1454
+ w = lambda c, d: f"node[fill=white] {{${g.margin(c,d)}$}}"
1455
+ elif type(g) == SupportGraph:
1456
+ w = lambda c, d: f"node[fill=white] {{${g.support(c,d)}$}}"
1457
+ else:
1458
+ w = lambda c, d: ""
1459
+
1460
+ cmap = g.cmap if cmap is None else cmap
1461
+
1462
+ node_tikz_str = list()
1463
+
1464
+ for c in g.candidates:
1465
+ node_tikz_str.append(
1466
+ f"\\node[circle,draw,minimum width=0.25in] at ({float(2.4*list(pos[c])[0])},{float(2.6*list(pos[c])[1])}) ({node_id[c]}) {{${cmap[c]}$}};\n"
1467
+ )
1468
+
1469
+ edges_tikz_str = list()
1470
+ for c1 in g.candidates:
1471
+ for c2 in g.candidates:
1472
+ if g.majority_prefers(c1, c2):
1473
+ edges_tikz_str.append(
1474
+ f"\\path[->,draw,thick] ({node_id[c1]}) to[pos=.7] {w(c1,c2)} ({node_id[c2]});\n"
1475
+ )
1476
+ elif g.majority_prefers(c2, c1):
1477
+ edges_tikz_str.append(
1478
+ f"\\path[->,draw,thick] ({node_id[c2]}) to[pos=.7] {w(c2,c1)} ({node_id[c1]});\n"
1479
+ )
1480
+ else:
1481
+ edges_tikz_str.append("")
1482
+
1483
+ return (
1484
+ "\\begin{tikzpicture}\n"
1485
+ + "".join(node_tikz_str)
1486
+ + "".join(edges_tikz_str)
1487
+ + "\\end{tikzpicture}"
1488
+ )
1489
+
1490
+
1491
+ def maximal_elements(g):
1492
+ """return the nodes in g with no incoming arrows."""
1493
+ return [n for n in g.nodes if g.in_degree(n) == 0]
1494
+
1495
+
1496
+ def display_mg_with_sc(edata, curr_cands=None, cmap=None):
1497
+ """
1498
+ Display the margin graph with the Split Cycle defeat relation highlighted.
1499
+ """
1500
+ from pref_voting.margin_based_methods import split_cycle_defeat
1501
+
1502
+ if type(edata) == MarginGraph:
1503
+ edata.display_with_defeat(
1504
+ split_cycle_defeat(edata, curr_cands=curr_cands, cmap=cmap)
1505
+ )
1506
+ else:
1507
+ edata.display_margin_graph_with_defeat(
1508
+ split_cycle_defeat(edata, curr_cands=curr_cands, cmap=cmap)
1509
+ )
1510
+
1511
+
1512
+ def display_graph(g, curr_cands=None, cmap=None):
1513
+ """Helper function to display a weighted directed graph."""
1514
+
1515
+ cmap = cmap if cmap is not None else {n: str(n) for n in g.nodes}
1516
+
1517
+ candidates = g.nodes if curr_cands is None else curr_cands
1518
+
1519
+ displayed_g = nx.DiGraph()
1520
+ displayed_g.add_nodes_from([cmap[c] for c in candidates])
1521
+ displayed_g.add_weighted_edges_from(
1522
+ [(cmap[e[0]], cmap[e[1]], e[2]["weight"]) for e in g.edges(data=True)]
1523
+ )
1524
+
1525
+ pos = nx.circular_layout(displayed_g)
1526
+ nx.draw(
1527
+ displayed_g,
1528
+ pos,
1529
+ font_size=20,
1530
+ node_color="blue",
1531
+ font_color="white",
1532
+ node_size=700,
1533
+ with_labels=True,
1534
+ )
1535
+ labels = nx.get_edge_attributes(displayed_g, "weight")
1536
+ nx.draw_networkx_edge_labels(
1537
+ displayed_g, pos, edge_labels=labels, font_size=14, label_pos=0.3
1538
+ )
1539
+ plt.show()