1
1
from fractions import Fraction
2
2
import itertools as it
3
3
import numpy as np
4
- from typing import Callable , Optional
4
+ from typing import Callable , Optional , Union
5
5
6
6
from ..models import Election
7
7
from ..election_state import ElectionState
@@ -92,82 +92,103 @@ def next_round(self) -> bool:
92
92
cands_elected += len (s )
93
93
return cands_elected < self .seats
94
94
95
- def run_step (self ) -> ElectionState :
95
+ def run_step (self , step : Optional [ Union [ int , None ]] = None ) -> ElectionState :
96
96
"""
97
- Simulates one round an STV election
97
+ Simulates one round of an STV election.
98
+
99
+ Args:
100
+ step: If an integer is given, run that round. Round numbers must be non-negative.
101
+ If None, run the next round given the value of self.state.curr_round.
102
+ Defaults to None.
98
103
99
104
Returns:
100
- An ElectionState object for a given round
105
+ An ElectionState object for a given round.
101
106
"""
102
- if not self .next_round ():
103
- raise ValueError (
104
- f"Length of elected set equal to number of seats ({ self .seats } )"
105
- )
107
+ # run given step
108
+ if isinstance (step , int ):
109
+ if step < 0 :
110
+ raise ValueError ("Step must be a non-negative integer." )
111
+
112
+ elif step >= 0 :
113
+ while self .state .curr_round < step :
114
+ self .run_step ()
106
115
107
- remaining = self .state .profile .get_candidates ()
108
- ballots = self .state .profile .get_ballots ()
109
- round_votes = compute_votes (remaining , ballots )
110
- elected = []
111
- eliminated = []
112
-
113
- # if number of remaining candidates equals number of remaining seats,
114
- # everyone is elected
115
- if len (remaining ) == self .seats - len (self .state .get_all_winners ()):
116
- elected = [{cand } for cand , votes in round_votes ]
117
- remaining = []
118
- ballots = []
119
-
120
- # elect all candidates who crossed threshold
121
- elif round_votes [0 ].votes >= self .threshold :
122
- for candidate , votes in round_votes :
123
- if votes >= self .threshold :
124
- elected .append ({candidate })
125
- remaining .remove (candidate )
126
- ballots = self .transfer (
127
- candidate ,
128
- ballots ,
129
- {cand : votes for cand , votes in round_votes },
130
- self .threshold ,
131
- )
132
- # since no one has crossed threshold, eliminate one of the people
133
- # with least first place votes
134
- elif self .next_round ():
135
- lp_candidates = [
136
- candidate
137
- for candidate , votes in round_votes
138
- if votes == round_votes [- 1 ].votes
139
- ]
140
-
141
- lp_cand = tie_broken_ranking (
142
- ranking = [set (lp_candidates )],
143
- profile = self .state .profile ,
144
- tiebreak = self .tiebreak ,
145
- )[- 1 ]
146
- eliminated .append (lp_cand )
147
- ballots = remove_cand (lp_cand , ballots )
148
- remaining .remove (next (iter (lp_cand )))
149
-
150
- if len (elected ) >= 1 :
151
- elected = scores_into_set_list (
152
- first_place_votes (self .state .profile ), [c for s in elected for c in s ]
153
- )
116
+ while self .state .curr_round > step :
117
+ self .state = self .state .previous
154
118
155
- # Make sure list-of-sets have non-empty elements
156
- elected = [s for s in elected if s != set ()]
157
- eliminated = [s for s in eliminated if s != set ()]
119
+ return (self .state )
158
120
159
- remaining = [set (remaining )]
160
- remaining = [s for s in remaining if s != set ()]
121
+ # run next step
122
+ else :
123
+ if not self .next_round ():
124
+ raise ValueError (
125
+ f"Length of elected set equal to number of seats ({ self .seats } )"
126
+ )
127
+
128
+ remaining = self .state .profile .get_candidates ()
129
+ ballots = self .state .profile .get_ballots ()
130
+ round_votes = compute_votes (remaining , ballots )
131
+ elected = []
132
+ eliminated = []
133
+
134
+ # if number of remaining candidates equals number of remaining seats,
135
+ # everyone is elected
136
+ if len (remaining ) == self .seats - len (self .state .get_all_winners ()):
137
+ elected = [{cand } for cand , votes in round_votes ]
138
+ remaining = []
139
+ ballots = []
140
+
141
+ # elect all candidates who crossed threshold
142
+ elif round_votes [0 ].votes >= self .threshold :
143
+ for candidate , votes in round_votes :
144
+ if votes >= self .threshold :
145
+ elected .append ({candidate })
146
+ remaining .remove (candidate )
147
+ ballots = self .transfer (
148
+ candidate ,
149
+ ballots ,
150
+ {cand : votes for cand , votes in round_votes },
151
+ self .threshold ,
152
+ )
153
+ # since no one has crossed threshold, eliminate one of the people
154
+ # with least first place votes
155
+ elif self .next_round ():
156
+ lp_candidates = [
157
+ candidate
158
+ for candidate , votes in round_votes
159
+ if votes == round_votes [- 1 ].votes
160
+ ]
161
+
162
+ lp_cand = tie_broken_ranking (
163
+ ranking = [set (lp_candidates )],
164
+ profile = self .state .profile ,
165
+ tiebreak = self .tiebreak ,
166
+ )[- 1 ]
167
+ eliminated .append (lp_cand )
168
+ ballots = remove_cand (lp_cand , ballots )
169
+ remaining .remove (next (iter (lp_cand )))
170
+
171
+ if len (elected ) >= 1 :
172
+ elected = scores_into_set_list (
173
+ first_place_votes (self .state .profile ), [c for s in elected for c in s ]
174
+ )
161
175
162
- self .state = ElectionState (
163
- curr_round = self .state .curr_round + 1 ,
164
- elected = elected ,
165
- eliminated = eliminated ,
166
- remaining = remaining ,
167
- profile = PreferenceProfile (ballots = ballots ),
168
- previous = self .state ,
169
- )
170
- return self .state
176
+ # Make sure list-of-sets have non-empty elements
177
+ elected = [s for s in elected if s != set ()]
178
+ eliminated = [s for s in eliminated if s != set ()]
179
+
180
+ remaining = [set (remaining )]
181
+ remaining = [s for s in remaining if s != set ()]
182
+
183
+ self .state = ElectionState (
184
+ curr_round = self .state .curr_round + 1 ,
185
+ elected = elected ,
186
+ eliminated = eliminated ,
187
+ remaining = remaining ,
188
+ profile = PreferenceProfile (ballots = ballots ),
189
+ previous = self .state ,
190
+ )
191
+ return self .state
171
192
172
193
def run_election (self ) -> ElectionState :
173
194
"""
@@ -176,10 +197,8 @@ def run_election(self) -> ElectionState:
176
197
Returns:
177
198
An ElectionState object with results for a complete election
178
199
"""
179
- if not self .next_round ():
180
- raise ValueError (
181
- f"Length of elected set equal to number of seats ({ self .seats } )"
182
- )
200
+ # always start election from round 0
201
+ self .reset ()
183
202
184
203
while self .next_round ():
185
204
self .run_step ()
0 commit comments