1717from schulze_method import SchulzeMethod
1818from pygraph .classes .digraph import digraph
1919from pygraph .algorithms .minmax import maximum_flow
20+ #from multiprocessing import Pool
2021import itertools
22+ from common_functions import unique_permutations
2123
22- class SchulzeSTV (SchulzeMethod ):
23-
24- compute_vote_management_with_pygraph = False
24+ PREFERRED_LESS = 1
25+ PREFERRED_SAME = 2
26+ PREFERRED_MORE = 3
27+ STRENGTH_TOLERANCE = 0.0000000001
28+ STRENGTH_THRESHOLD = 0.1
2529
30+ class SchulzeSTV (SchulzeMethod ):
31+
2632 def __init__ (self , ballots , required_winners , notation = None ):
2733 self .required_winners = required_winners
2834 SchulzeMethod .__init__ (self , ballots , notation )
29-
35+
3036 def calculate_results (self ):
31-
37+
3238 # Generate the list of patterns we need to complete
33- self .__generate_completion_patterns__ ()
3439 self .__generate_completed_patterns__ ()
3540 self .__generate_vote_management_graph__ ()
36-
41+
3742 # Build the graph of possible winners
3843 self .graph = digraph ()
3944 for candidate_set in itertools .combinations (self .candidates , self .required_winners ):
4045 self .graph .add_nodes ([tuple (sorted (list (candidate_set )))])
41-
46+
4247 # Generate the edges between nodes
4348 for candidate_set in itertools .combinations (self .candidates , self .required_winners + 1 ):
4449 for candidate in candidate_set :
45- other_candidates = sorted (list ( set (candidate_set ) - set ([candidate ]) ))
50+ other_candidates = sorted (set (candidate_set ) - set ([candidate ]))
4651 completed = self .__proportional_completion__ (candidate , other_candidates )
4752 weight = self .__strength_of_vote_management__ (completed )
4853 if weight > 0 :
4954 for subset in itertools .combinations (other_candidates , len (other_candidates ) - 1 ):
5055 self .graph .add_edge ((tuple (other_candidates ), tuple (sorted (list (subset ) + [candidate ]))), weight )
51-
56+
5257 # Determine the winner through the Schwartz set heuristic
5358 self .schwartz_set_heuristic ()
54-
59+
5560 # Split the "winner" into its candidate components
5661 self .winners = set ([item for innerlist in self .winners for item in innerlist ])
57-
62+
5863 def results (self ):
5964 results = SchulzeMethod .results (self )
6065 return results
61-
66+
6267 def __generate_vote_management_graph__ (self ):
63- if self .compute_vote_management_with_pygraph == False : return
6468 self .vote_management_graph = digraph ()
6569 self .vote_management_graph .add_nodes (self .completed_patterns )
66- self .vote_management_graph .del_node (tuple ([3 ]* self .required_winners ))
70+ self .vote_management_graph .del_node (tuple ([PREFERRED_MORE ]* self .required_winners ))
6771 self .pattern_nodes = self .vote_management_graph .nodes ()
6872 self .vote_management_graph .add_nodes (["source" ,"sink" ])
6973 for pattern_node in self .pattern_nodes :
@@ -76,86 +80,70 @@ def __generate_vote_management_graph__(self):
7680 self .vote_management_graph .add_edge ((pattern_node , i ))
7781 for i in range (self .required_winners ):
7882 self .vote_management_graph .add_edge ((i , "sink" ))
79-
80- def __generate_completion_patterns__ (self ):
81- self .completion_patterns = []
82- for i in range (0 ,self .required_winners ):
83- for j in range (0 , i + 1 ):
84- for pattern in self .__unique_permutations__ ([2 ]* (self .required_winners - i )+ [1 ]* (j )+ [3 ]* (i - j )):
85- self .completion_patterns .append (tuple (pattern ))
86-
83+
84+ # Generates a list of all patterns that do not contain indifference
8785 def __generate_completed_patterns__ (self ):
8886 self .completed_patterns = []
89- for i in range (0 ,self .required_winners + 1 ):
90- for pattern in self .__unique_permutations__ ([1 ]* (self .required_winners - i )+ [3 ]* (i )):
87+ for i in range (0 , self .required_winners + 1 ):
88+ for pattern in unique_permutations (
89+ [PREFERRED_LESS ]* (self .required_winners - i )
90+ + [PREFERRED_MORE ]* (i )
91+ ):
9192 self .completed_patterns .append (tuple (pattern ))
92-
93- def __unique_permutations__ (self , xs ):
94- if len (xs )< 2 :
95- yield xs
96- else :
97- h = []
98- for x in xs :
99- h .append (x )
100- if x in h [:- 1 ]:
101- continue
102- ts = xs [:]; ts .remove (x )
103- for ps in self .__unique_permutations__ (ts ):
104- yield [x ]+ ps
105-
93+
10694 def __proportional_completion__ (self , candidate , other_candidates ):
10795 profile = dict (zip (self .completed_patterns , [0 ]* len (self .completed_patterns )))
108-
96+
10997 # Obtain an initial tally from the ballots
11098 for ballot in self .ballots :
11199 pattern = []
112100 for other_candidate in other_candidates :
113101 if ballot ["ballot" ][candidate ] < ballot ["ballot" ][other_candidate ]:
114- pattern .append (1 )
102+ pattern .append (PREFERRED_LESS )
115103 elif ballot ["ballot" ][candidate ] == ballot ["ballot" ][other_candidate ]:
116- pattern .append (2 )
104+ pattern .append (PREFERRED_SAME )
117105 else :
118- pattern .append (3 )
106+ pattern .append (PREFERRED_MORE )
119107 pattern = tuple (pattern )
120108 if pattern not in profile :
121109 profile [pattern ] = 0.0
122110 profile [pattern ] += ballot ["count" ]
123-
124- # Complete each pattern in order
125- for pattern in self . completion_patterns :
126- if pattern in profile :
127- profile = self .__proportional_completion_round__ (pattern , profile )
128-
111+
112+ # Peel off patterns with indifference (from the most to the least) and apply proportional completion to them
113+ for pattern in sorted ( profile . keys (), key = lambda pattern : pattern . count ( PREFERRED_SAME ), reverse = True ) :
114+ if pattern . count ( PREFERRED_SAME ) == 0 : break
115+ self .__proportional_completion_round__ (pattern , profile )
116+
129117 return profile
130-
118+
131119 def __proportional_completion_round__ (self , completion_pattern , profile ):
132-
120+
133121 # Remove pattern that contains indifference
134122 completion_pattern_weight = profile [completion_pattern ]
135123 del profile [completion_pattern ]
136-
124+
137125 patterns_to_consider = {}
138126 for pattern in profile .keys ():
139127 append = False
140128 append_target = []
141129 for i in range (len (completion_pattern )):
142- if completion_pattern [i ] != 2 :
130+ if completion_pattern [i ] != PREFERRED_SAME :
143131 append_target .append (completion_pattern [i ])
144132 else :
145133 append_target .append (pattern [i ])
146- if completion_pattern [i ] == 2 and pattern [i ] != 2 :
134+ if completion_pattern [i ] == PREFERRED_SAME and pattern [i ] != PREFERRED_SAME :
147135 append = True
148- append_target = tuple (append_target )
149136 if append == True :
137+ append_target = tuple (append_target )
150138 if append_target not in patterns_to_consider :
151139 patterns_to_consider [append_target ] = set ()
152140 patterns_to_consider [append_target ].add (pattern )
153-
141+
154142 denominator = 0
155143 for (append_target , patterns ) in patterns_to_consider .items ():
156144 for pattern in patterns :
157145 denominator += profile [pattern ]
158-
146+
159147 # Reweight the remaining items
160148 for pattern in patterns_to_consider .keys ():
161149 if denominator == 0 :
@@ -164,117 +152,35 @@ def __proportional_completion_round__(self, completion_pattern, profile):
164152 if pattern not in profile :
165153 profile [pattern ] = 0
166154 profile [pattern ] += sum (profile [considered_pattern ] for considered_pattern in patterns_to_consider [pattern ]) * completion_pattern_weight / denominator
167-
155+
168156 return profile
169-
157+
170158 # This method converts the voter profile into a capacity graph and iterates
171159 # on the maximum flow using the Edmonds Karp algorithm. The end result is
172160 # the limit of the strength of the voter management as per Markus Schulze's
173161 # Calcul02.pdf (draft, 28 March 2008, abstract: "In this paper we illustrate
174162 # the calculation of the strengths of the vote managements.").
175163 def __strength_of_vote_management__ (self , voter_profile ):
176-
177- # This implementation uses Python Graph Core and requires less code, but
178- # runs roughly 2-3x slower than our internal implementation
179- if self .compute_vote_management_with_pygraph :
180-
181- # Initialize the graph weights
182- for pattern in self .pattern_nodes :
183- self .vote_management_graph .set_edge_weight (("source" , pattern ), voter_profile [pattern ])
184- for i in range (self .required_winners ):
185- if pattern [i ] == 1 :
186- self .vote_management_graph .set_edge_weight ((pattern , i ), voter_profile [pattern ])
187-
188- # Iterate towards the limit
189- r = [(float (sum (voter_profile .values ())) - voter_profile [tuple ([3 ]* self .required_winners )]) / self .required_winners ]
190- while len (r ) < 2 or r [- 2 ] - r [- 1 ] > 0.0000000001 :
191- for i in range (self .required_winners ):
192- self .vote_management_graph .set_edge_weight ((i , "sink" ), r [- 1 ])
193- max_flow = maximum_flow (self .vote_management_graph , "source" , "sink" )
194- sink_sum = sum (v for k ,v in max_flow [0 ].iteritems () if k [1 ] == "sink" )
195- r .append (sink_sum / self .required_winners )
196-
197- # Return the final max flow
198- return round (r [- 1 ],9 )
199-
200- # This implementation generates a capacity matrix and iterates on it
201- # using a custom-written Edmonds-Karp implementation. It'd be nice to
202- # speed up the Python Graph Core implementation and scrap this section,
203- # along with the two accompanying methods.
204- else :
205- number_of_candidates = len (voter_profile .keys ()[0 ])
206- number_of_patterns = len (voter_profile ) - 1
207- number_of_nodes = 1 + number_of_patterns + number_of_candidates + 1
208- ordered_patterns = sorted (voter_profile .keys ())
209- ordered_patterns .remove (tuple ([3 ]* number_of_candidates ))
210-
211- r = [(float (sum (voter_profile .values ())) - voter_profile [tuple ([3 ]* number_of_candidates )]) / number_of_candidates ]
212-
213- # Generate a number_of_nodes x number_of_nodes matrix of zeroes
214- C = []
215- for i in range (number_of_nodes ):
216- C .append ([0 ] * number_of_nodes )
217-
218- # Source to voters
219- vertex = 0
220- for pattern in ordered_patterns :
221- C [0 ][vertex + 1 ] = voter_profile [pattern ]
222- vertex += 1
223-
224- # Voters to candidates
225- vertex = 0
226- for pattern in ordered_patterns :
227- for i in range (1 ,number_of_candidates + 1 ):
228- if pattern [i - 1 ] == 1 :
229- C [vertex + 1 ][1 + number_of_patterns + i - 1 ] = voter_profile [pattern ]
230- vertex += 1
231-
232- # Iterate towards the limit
233- while len (r ) < 2 or r [- 2 ] - r [- 1 ] > 0.0000000001 :
234- for i in range (number_of_candidates ):
235- C [1 + number_of_patterns + i ][number_of_nodes - 1 ] = r [- 1 ]
236- r .append (SchulzeSTV .__edmonds_karp__ (C ,0 ,number_of_nodes - 1 )/ number_of_candidates )
237- return round (r [- 1 ],9 )
238-
239- # The Edmonds-Karp algorithm is an implementation of the Ford-Fulkerson
240- # method for computing the maximum flow in a flow network in O(VE^2).
241- #
242- # Sourced from http://semanticweb.org/wiki/Python_implementation_of_Edmonds-Karp_algorithm
243- @staticmethod
244- def __edmonds_karp__ (C , source , sink ):
245- n = len (C ) # C is the capacity matrix
246- F = [[0 ] * n for i in xrange (n )]
247- # residual capacity from u to v is C[u][v] - F[u][v]
248-
249- while True :
250- path = SchulzeSTV .__bfs__ (C , F , source , sink )
251- if not path :
252- break
253- # traverse path to find smallest capacity
254- flow = min (C [u ][v ] - F [u ][v ] for u ,v in path )
255- # traverse path to update flow
256- for u ,v in path :
257- F [u ][v ] += flow
258- F [v ][u ] -= flow
259-
260- return sum (F [source ][i ] for i in xrange (n ))
261-
262- # In graph theory, breadth-first search (BFS) is a graph search algorithm
263- # that begins at the root node and explores all the neighboring nodes. Then
264- # for each of those nearest nodes, it explores their unexplored neighbor
265- # nodes, and so on, until it finds the goal.
266- #
267- # Sourced from http://semanticweb.org/wiki/Python_implementation_of_Edmonds-Karp_algorithm
268- @staticmethod
269- def __bfs__ (C , F , source , sink ):
270- queue = [source ]
271- paths = {source : []}
272- while queue :
273- u = queue .pop (0 )
274- for v in xrange (len (C )):
275- if C [u ][v ] - F [u ][v ] > 0 and v not in paths :
276- paths [v ] = paths [u ] + [(u ,v )]
277- if v == sink :
278- return paths [v ]
279- queue .append (v )
280- return None
164+
165+ # Initialize the graph weights
166+ for pattern in self .pattern_nodes :
167+ self .vote_management_graph .set_edge_weight (("source" , pattern ), voter_profile [pattern ])
168+ for i in range (self .required_winners ):
169+ if pattern [i ] == 1 :
170+ self .vote_management_graph .set_edge_weight ((pattern , i ), voter_profile [pattern ])
171+
172+ # Iterate towards the limit
173+ r = [(float (sum (voter_profile .values ())) - voter_profile [tuple ([PREFERRED_MORE ]* self .required_winners )]) / self .required_winners ]
174+ while len (r ) < 2 or r [- 2 ] - r [- 1 ] > STRENGTH_TOLERANCE :
175+ for i in range (self .required_winners ):
176+ self .vote_management_graph .set_edge_weight ((i , "sink" ), r [- 1 ])
177+ max_flow = maximum_flow (self .vote_management_graph , "source" , "sink" )
178+ sink_sum = sum (v for k ,v in max_flow [0 ].iteritems () if k [1 ] == "sink" )
179+ r .append (sink_sum / self .required_winners )
180+
181+ # We expect strengths to be above a specified threshold
182+ if sink_sum < STRENGTH_THRESHOLD :
183+ return 0
184+
185+ # Return the final max flow
186+ return round (r [- 1 ],9 )
0 commit comments