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)