diff --git a/html/index.html b/html/index.html index 5894ef6..2e6cb00 100644 --- a/html/index.html +++ b/html/index.html @@ -105,7 +105,7 @@ diff --git a/html/worker.js b/html/worker.js index 0825879..469124a 100644 --- a/html/worker.js +++ b/html/worker.js @@ -50,7 +50,7 @@ onmessage = function(evt) { } if (evt.data.options['ties'] === 'backwards_random') { - counter.options['ties'] = [py.pyRCV2.ties.TiesBackwards(), py.pyRCV2.ties.TiesRandom(evt.data.seed)]; + counter.options['ties'] = [py.pyRCV2.ties.TiesBackwards(counter), py.pyRCV2.ties.TiesRandom(evt.data.seed)]; } else if (evt.data.options['ties'] === 'random') { counter.options['ties'] = [py.pyRCV2.ties.TiesRandom(evt.data.seed)]; } diff --git a/pyRCV2/cli/stv.py b/pyRCV2/cli/stv.py index 2c0bfe2..402748b 100644 --- a/pyRCV2/cli/stv.py +++ b/pyRCV2/cli/stv.py @@ -91,7 +91,7 @@ def main(args): counter.options['ties'] = [] for t in args.ties: if t == 'backwards': - counter.options['ties'].append(TiesBackwards()) + counter.options['ties'].append(TiesBackwards(counter)) elif t == 'prompt': counter.options['ties'].append(TiesPrompt()) elif t == 'random': diff --git a/pyRCV2/method/base_stv.py b/pyRCV2/method/base_stv.py index f33186f..90d0363 100644 --- a/pyRCV2/method/base_stv.py +++ b/pyRCV2/method/base_stv.py @@ -62,7 +62,8 @@ class BaseSTVCounter: def reset(self): """ Public function: - Reset the count and perform the first step + Perform the first step (distribute first preferences) + Does not reset the states of candidates, etc. """ # Distribute first preferences @@ -83,7 +84,7 @@ class BaseSTVCounter: self.elect_meeting_quota() __pragma__('opov') - return CountStepResult( + result = CountStepResult( 'First preferences', self.candidates, self.exhausted, @@ -92,6 +93,9 @@ class BaseSTVCounter: self.quota ) __pragma__('noopov') + + self.step_results = [result] + return result def step(self): """ @@ -151,7 +155,7 @@ class BaseSTVCounter: self.num_elected += 1 __pragma__('opov') - return CountStepResult( + result = CountStepResult( 'Bulk election', self.candidates, self.exhausted, @@ -160,6 +164,9 @@ class BaseSTVCounter: self.quota ) __pragma__('noopov') + + self.step_results.append(result) + return result def distribute_surpluses(self): """ @@ -200,7 +207,7 @@ class BaseSTVCounter: self.elect_meeting_quota() __pragma__('opov') - return CountStepResult( + result = CountStepResult( 'Surplus of ' + candidate_surplus.name, self.candidates, self.exhausted, @@ -209,6 +216,9 @@ class BaseSTVCounter: self.quota ) __pragma__('noopov') + + self.step_results.append(result) + return result def do_surplus(self, candidate_surplus, count_card, surplus): """ @@ -237,7 +247,7 @@ class BaseSTVCounter: self.elect_meeting_quota() __pragma__('opov') - return CountStepResult( + result = CountStepResult( 'Exclusion of ' + candidate_excluded.name, self.candidates, self.exhausted, @@ -246,6 +256,9 @@ class BaseSTVCounter: self.quota ) __pragma__('noopov') + + self.step_results.append(result) + return result def candidate_to_exclude(self): """ diff --git a/pyRCV2/model.py b/pyRCV2/model.py index 04ebcb9..70fb1c3 100644 --- a/pyRCV2/model.py +++ b/pyRCV2/model.py @@ -39,6 +39,9 @@ class Ballot: def __init__(self, value, preferences): self.value = value self.preferences = preferences + + def clone(self): + return Ballot(self.value, self.preferences) class Election: """ @@ -73,6 +76,16 @@ class CountCard: """Roll over previous round transfers in preparation for next round""" self.orig_votes = self.votes self.transfers = Num('0') + + def clone(self): + """Return a clone of this count card (including cloning ballots) as a record of this stage""" + result = CountCard() + result.orig_votes = self.orig_votes + result.transfers = self.transfers + result.ballots = [b.clone() for b in self.ballots] + result.state = self.state + result.order_elected = self.order_elected + return result class CountCompleted: pass @@ -87,3 +100,14 @@ class CountStepResult: self.total = total self.quota = quota + + def clone(self): + """Return a clone of this result as a record of this stage""" + + candidates = SafeDict() + for c, cc in self.candidates.items(): + __pragma__('opov') + candidates[c] = cc.clone() + __pragma__('noopov') + + return CountStepResult(self.comment, candidates, self.exhausted.clone(), self.loss_fraction.clone(), self.total, self.quota) diff --git a/pyRCV2/ties.py b/pyRCV2/ties.py index 4a22e88..c6670bb 100644 --- a/pyRCV2/ties.py +++ b/pyRCV2/ties.py @@ -65,14 +65,40 @@ class TiesPrompt: def choose_highest(self, l): return self.choose_lowest(l) +# FIXME: This is untested! class TiesBackwards: + """ + Break ties based on the candidate who had the highest/lowest total at the end + of the most recent stage where one candidate had a higher/lower total than + all other tied candidates, if such a stage exists + """ + + def __init__(self, counter): + self.counter = counter + def choose_lowest(self, l): - raise Exception('Not yet implemented') + for result in reversed(self.counter.step_results): + __pragma__('opov') + l2 = [(x, result.candidates[x[0]].votes) for x in l] + l2.sort(key=lambda x: x[1]) + if l2[0][1] < l2[1][1]: # Did one candidate have fewer votes than the others? + return l2[0][0] + __pragma__('noopov') + return None def choose_highest(self, l): - raise Exception('Not yet implemented') + for result in reversed(self.counter.step_results): + __pragma__('opov') + l2 = [(x, result.candidates[x[0]].votes) for x in l] + l2.sort(key=lambda x: x[1], reverse=True) + if l2[0][1] > l2[1][1]: # Did one candidate have more votes than the others? + return l2[0][0] + __pragma__('noopov') + return None class TiesRandom: + """Break ties randomly, using the SHARandom deterministic RNG""" + def __init__(self, seed): self.random = SHARandom(seed)