Implement backwards tie breaking

This commit is contained in:
RunasSudo 2020-12-24 01:36:39 +11:00
parent 06ab133615
commit 503fed26d1
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
6 changed files with 73 additions and 10 deletions

View File

@ -105,7 +105,7 @@
<label> <label>
Ties: Ties:
<select id="selTies"> <select id="selTies">
<option value="backwards_random" selected>Backwards then random (NYI)</option> <option value="backwards_random" selected>Backwards then random</option>
<option value="random">Random</option> <option value="random">Random</option>
</select> </select>
</label> </label>

View File

@ -50,7 +50,7 @@ onmessage = function(evt) {
} }
if (evt.data.options['ties'] === 'backwards_random') { 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') { } else if (evt.data.options['ties'] === 'random') {
counter.options['ties'] = [py.pyRCV2.ties.TiesRandom(evt.data.seed)]; counter.options['ties'] = [py.pyRCV2.ties.TiesRandom(evt.data.seed)];
} }

View File

@ -91,7 +91,7 @@ def main(args):
counter.options['ties'] = [] counter.options['ties'] = []
for t in args.ties: for t in args.ties:
if t == 'backwards': if t == 'backwards':
counter.options['ties'].append(TiesBackwards()) counter.options['ties'].append(TiesBackwards(counter))
elif t == 'prompt': elif t == 'prompt':
counter.options['ties'].append(TiesPrompt()) counter.options['ties'].append(TiesPrompt())
elif t == 'random': elif t == 'random':

View File

@ -62,7 +62,8 @@ class BaseSTVCounter:
def reset(self): def reset(self):
""" """
Public function: 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 # Distribute first preferences
@ -83,7 +84,7 @@ class BaseSTVCounter:
self.elect_meeting_quota() self.elect_meeting_quota()
__pragma__('opov') __pragma__('opov')
return CountStepResult( result = CountStepResult(
'First preferences', 'First preferences',
self.candidates, self.candidates,
self.exhausted, self.exhausted,
@ -92,6 +93,9 @@ class BaseSTVCounter:
self.quota self.quota
) )
__pragma__('noopov') __pragma__('noopov')
self.step_results = [result]
return result
def step(self): def step(self):
""" """
@ -151,7 +155,7 @@ class BaseSTVCounter:
self.num_elected += 1 self.num_elected += 1
__pragma__('opov') __pragma__('opov')
return CountStepResult( result = CountStepResult(
'Bulk election', 'Bulk election',
self.candidates, self.candidates,
self.exhausted, self.exhausted,
@ -160,6 +164,9 @@ class BaseSTVCounter:
self.quota self.quota
) )
__pragma__('noopov') __pragma__('noopov')
self.step_results.append(result)
return result
def distribute_surpluses(self): def distribute_surpluses(self):
""" """
@ -200,7 +207,7 @@ class BaseSTVCounter:
self.elect_meeting_quota() self.elect_meeting_quota()
__pragma__('opov') __pragma__('opov')
return CountStepResult( result = CountStepResult(
'Surplus of ' + candidate_surplus.name, 'Surplus of ' + candidate_surplus.name,
self.candidates, self.candidates,
self.exhausted, self.exhausted,
@ -209,6 +216,9 @@ class BaseSTVCounter:
self.quota self.quota
) )
__pragma__('noopov') __pragma__('noopov')
self.step_results.append(result)
return result
def do_surplus(self, candidate_surplus, count_card, surplus): def do_surplus(self, candidate_surplus, count_card, surplus):
""" """
@ -237,7 +247,7 @@ class BaseSTVCounter:
self.elect_meeting_quota() self.elect_meeting_quota()
__pragma__('opov') __pragma__('opov')
return CountStepResult( result = CountStepResult(
'Exclusion of ' + candidate_excluded.name, 'Exclusion of ' + candidate_excluded.name,
self.candidates, self.candidates,
self.exhausted, self.exhausted,
@ -246,6 +256,9 @@ class BaseSTVCounter:
self.quota self.quota
) )
__pragma__('noopov') __pragma__('noopov')
self.step_results.append(result)
return result
def candidate_to_exclude(self): def candidate_to_exclude(self):
""" """

View File

@ -39,6 +39,9 @@ class Ballot:
def __init__(self, value, preferences): def __init__(self, value, preferences):
self.value = value self.value = value
self.preferences = preferences self.preferences = preferences
def clone(self):
return Ballot(self.value, self.preferences)
class Election: class Election:
""" """
@ -73,6 +76,16 @@ class CountCard:
"""Roll over previous round transfers in preparation for next round""" """Roll over previous round transfers in preparation for next round"""
self.orig_votes = self.votes self.orig_votes = self.votes
self.transfers = Num('0') 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: class CountCompleted:
pass pass
@ -87,3 +100,14 @@ class CountStepResult:
self.total = total self.total = total
self.quota = quota 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)

View File

@ -65,14 +65,40 @@ class TiesPrompt:
def choose_highest(self, l): def choose_highest(self, l):
return self.choose_lowest(l) return self.choose_lowest(l)
# FIXME: This is untested!
class TiesBackwards: 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): 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): 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: class TiesRandom:
"""Break ties randomly, using the SHARandom deterministic RNG"""
def __init__(self, seed): def __init__(self, seed):
self.random = SHARandom(seed) self.random = SHARandom(seed)