stv_tool.py was named with an underscore so that it is importable. You shouldn't need to copy/paste all of its code ...
(tho of course, it doesn't live in lib/ ... maybe it should 'svn mv' into lib?) On Thu, Mar 26, 2015 at 2:10 PM, <humbed...@apache.org> wrote: > Author: humbedooh > Date: Thu Mar 26 19:10:31 2015 > New Revision: 1669412 > > URL: http://svn.apache.org/r1669412 > Log: > use stv_tool.py's calculations instead, they actually work. > > Modified: > steve/trunk/pysteve/lib/plugins/stv.py > > Modified: steve/trunk/pysteve/lib/plugins/stv.py > URL: > http://svn.apache.org/viewvc/steve/trunk/pysteve/lib/plugins/stv.py?rev=1669412&r1=1669411&r2=1669412&view=diff > > ============================================================================== > --- steve/trunk/pysteve/lib/plugins/stv.py (original) > +++ steve/trunk/pysteve/lib/plugins/stv.py Thu Mar 26 19:10:31 2015 > @@ -17,8 +17,19 @@ > """ STV Voting Plugin """ > import re, json, random > > +ELECTED = 1 > +HOPEFUL = 2 > +ELIMINATED = 4 > +ALMOST = 8 > + > +ERROR_MARGIN = 0.00001 > +BILLIONTH = 0.000000001 > + > + > from lib import constants > > +debug = [] > + > def validateSTV(vote, issue): > "Tries to validate a vote, returns why if not valid, None otherwise" > letters = [chr(i) for i in range(ord('a'), ord('a') + > len(issue['candidates']))] > @@ -64,99 +75,307 @@ def getproportion(votes, winners, step, > > > > -def tallySTV(votes, issue): > - m = re.match(r"stv(\d+)", issue['type']) > - if not m: > - raise Exception("Not an STV vote!") > - > - numseats = int(m.group(1)) > - candidates = [] > - for c in issue['candidates']: > - candidates.append(c['name']) > - > > - debug = [] > - > - # Set up letters for mangling > - letters = [chr(i) for i in range(ord('a'), ord('a') + > len(candidates))] > - cc = "".join(letters) > - > - # Keep score of votes > - points = {} > - > - # Set all scores to 0 at first > - for c in cc: > - points[c] = 0 > > - # keep score of winners > - winners = [] > - turn = 0 > > - # Find quota to win a seat > - quota = ( len(votes) / (numseats + 1) ) + 1 > - debug.append("Seats available: %u. Votes cast: %u" % (numseats, > len(votes))) > - debug.append("Votes required to win a seat: %u ( (%u/(%u+1))+1 )" % > (quota, len(votes), numseats)) > +def run_vote(names, votes, num_seats): > + candidates = CandidateList(names) > > - > - surplus = 0 > - # While we still have seats to fill > - if not len(candidates) < numseats: > - y = 0 > - while len(winners) < numseats and len(cc) > 0 and turn < 1000: > #Don't run for > 1000 iterations, that's a bug > - turn += 1 > + # name -> Candidate > + remap = dict((c.name, c) for c in candidates.l) > > - s = 0 > + # Turn VOTES into a list of ordered-lists of Candidate objects > + votes = [[remap[n] for n in choices] for choices in votes.values()] > + > + if candidates.count(ELECTED + HOPEFUL) <= num_seats: > + debug.append('All candidates elected') > + candidates.change_state(HOPEFUL, ELECTED) > + return > + if num_seats <= 0: > + candidates.change_state(HOPEFUL, ELIMINATED) > + return > + > + quota = None # not used on first pass > + iteration = 1 > + while candidates.count(ELECTED) < num_seats: > + debug.append('Iteration %d' % iteration) > + iteration += 1 > + quota = iterate_one(quota, votes, candidates, num_seats) > + candidates.reverse_random() > + > + debug.append('All seats full') > + candidates.change_state(HOPEFUL, ELIMINATED) > + > + return candidates.retval() > + > + > +class CandidateList(object): > + def __init__(self, names): > + num_cand = len(names) > + randset = generate_random(num_cand) > + > + self.l = [ ] > + for n, r in zip(names, randset): > + c = Candidate(n, r, num_cand-1) > + self.l.append(c) > + > + def count(self, state): > + count = 0 > + for c in self.l: > + if (c.status & state) != 0: > + count += 1 > + return count > + def retval(self): > + winners = [] > + for c in self.l: > + if c.status == ELECTED: > + winners.append(c.name) > + return winners > > - # Get votes > - xpoints = getproportion(votes, winners, 0, surplus) > - surplus = 0 > - if turn == 1: > - debug.append("Initial tally: %s" % json.dumps(xpoints)) > - else: > - debug.append("Proportional move: %s" % > json.dumps(xpoints)) > - > - for x in xpoints: > - points[x] += xpoints[x] > - mq = 0 > - > - # For each candidate letter, find if someone won a seat > - for c in cc: > - if len(winners) >= numseats: > - break > - if points[c] >= quota and not c in winners: > - winners.append(c) > - debug.append("WINNER: %s got elected in with %u > votes! %u seats remain" % (c, points[c], numseats - len(winners))) > - cc.replace(c, "") > - mq += 1 > - surplus += points[c] - quota > - > - # If we found no winners in this round, eliminate the lowest > scorer and retally > - if mq < 1: > - lowest = 99999999 > - lowestC = None > - for c in cc: > - if points[c] < lowest: > - lowest = points[c] > - lowestC = c > - > - debug.append("DRAW: %s is eliminated" % lowestC) > - if lowestC: > - cc.replace(lowestC, "") > - else: > - debug.append("No more canididates?? buggo?") > - break > - y += 1 > + def change_state(self, from_state, to_state): > + any_changed = False > + for c in self.l: > + if (c.status & from_state) != 0: > + c.status = to_state > + if to_state == ELECTED: > + c.elect() > + elif to_state == ELIMINATED: > + c.eliminate() > + any_changed = True > + return any_changed > + > + def reverse_random(self): > + # Flip the values to create a different ordering among candidates. > Note > + # that this will alternate the domain between [0.0, 1.0) and (0.0, > 1.0] > + for c in self.l: > + c.rand = 1.0 - c.rand > + > + def adjust_weights(self, quota): > + for c in self.l: > + if c.status == ELECTED: > + c.adjust_weight(quota) > + > + def print_results(self): > + for c in self.l: > + print '%-40s%selected' % (c.name, c.status == ELECTED and ' ' or ' > not ') > + > + def dbg_display_tables(self, excess): > + total = excess > + for c in self.l: > + debug.append('%-20s %15.9f %15.9f' % (c.name, c.weight, c.vote)) > + total += c.vote > + debug.append('%-20s %15s %15.9f' %( 'Non-transferable', ' ', excess)) > + debug.append('%-20s %15s %15.9f' % ( 'Total', ' ', total)) > + > + > +class Candidate(object): > + def __init__(self, name, rand, ahead): > + self.name = name > + self.rand = rand > + self.ahead = ahead > + self.status = HOPEFUL > + self.weight = 1.0 > + self.vote = None # calculated later > + > + def elect(self): > + self.status = ELECTED > + debug.append('Elected: %s' % self.name) > + > + def eliminate(self): > + self.status = ELIMINATED > + self.weight = 0.0 > + debug.append('Eliminated: %s' % self.name) > + > + def adjust_weight(self, quota): > + assert quota is not None > + self.weight = (self.weight * quota) / self.vote > + > + def __cmp__(self, other): > + if self.ahead < other.ahead: > + return -1 > + if self.ahead == other.ahead: > + return cmp(self.vote, other.vote) > + return 1 > + > + > +def iterate_one(quota, votes, candidates, num_seats): > + # assume that: count(ELECTED) < num_seats > + if candidates.count(ELECTED + HOPEFUL) <= num_seats: > + debug.append('All remaining candidates elected') > + candidates.change_state(HOPEFUL, ELECTED) > + return None > + > + candidates.adjust_weights(quota) > > - # Everyone's a winner!! > + changed, new_quota, surplus = recalc(votes, candidates, num_seats) > + if not changed and surplus < ERROR_MARGIN: > + debug.append('Remove Lowest (forced)') > + exclude_lowest(candidates.l) > + return new_quota > + > + > +def recalc(votes, candidates, num_seats): > + excess = calc_totals(votes, candidates) > + calc_aheads(candidates) > + candidates.dbg_display_tables(excess) > + quota = calc_quota(len(votes), excess, num_seats) > + any_changed = elect(quota, candidates, num_seats) > + surplus = calc_surplus(quota, candidates) > + any_changed |= try_remove_lowest(surplus, candidates) > + return any_changed, quota, surplus > + > + > +def calc_totals(votes, candidates): > + for c in candidates.l: > + c.vote = 0.0 > + excess = 0.0 > + for choices in votes: > + vote = 1.0 > + for c in choices: > + if c.status == HOPEFUL: > + c.vote += vote > + vote = 0.0 > + break > + if c.status != ELIMINATED: > + wv = c.weight * vote # weighted vote > + c.vote += wv > + vote -= wv > + if vote == 0.0: # nothing left to distribute > + break > + excess += vote > + return excess > + > + > +def calc_aheads(candidates): > + c_sorted = sorted(candidates.l) > + last = 0 > + for i in range(1, len(c_sorted)+1): > + if i == len(c_sorted) or c_sorted[last] != c_sorted[i]: > + for c in c_sorted[last:i]: > + c.ahead = (i - 1) + last > + last = i > + > + > +def calc_quota(num_voters, excess, num_seats): > + if num_seats > 2: > + quota = (float(num_voters) - excess) / (float(num_seats + 1) + > BILLIONTH) > + else: > + quota = (float(num_voters) - excess) / float(num_seats + 1) > + if quota < ERROR_MARGIN: > + raise Exception('Internal Error - very low quota') > + debug.append('Quota = %.9f' % quota) > + return quota > + > + > +def elect(quota, candidates, num_seats): > + for c in candidates.l: > + if c.status == HOPEFUL and c.vote >= quota: > + c.status = ALMOST > + > + any_changed = False > + > + while candidates.count(ELECTED + ALMOST) > num_seats: > + debug.append('Vote tiebreaker! voters: %d seats: %d' %( > candidates.count(ELECTED + ALMOST), num_seats)) > + candidates.change_state(HOPEFUL, ELIMINATED) > + exclude_lowest(candidates.l) > + any_changed = True # we changed the candidates via exclude_lowest() > + > + any_changed |= candidates.change_state(ALMOST, ELECTED) > + return any_changed > + > + > +def calc_surplus(quota, candidates): > + surplus = 0.0 > + for c in candidates.l: > + if c.status == ELECTED: > + surplus += c.vote - quota > + debug.append('Total Surplus = %.9f' % surplus) > + return surplus > + > + > +def try_remove_lowest(surplus, candidates): > + lowest1 = 1e18 > + lowest2 = 1e18 > + which = None > + for c in candidates.l: > + if c.status == HOPEFUL and c.vote < lowest1: > + lowest1 = c.vote > + which = c > + for c in candidates.l: > + if c != which and c.status != ELIMINATED and c.vote < lowest2: > + lowest2 = c.vote > + > + diff = lowest2 - lowest1 > + if diff >= 0.0: > + debug.append('Lowest Difference = %.9f - %.9f = %.9f' % ( lowest2, > lowest1, diff)) > + if diff > surplus: > + debug.append('Remove Lowest (unforced)') > + which.eliminate() > + return True > + return False > + > + > +def exclude_lowest(candidates): > + ### use: ahead = len(candidates) ?? > + ahead = 1000000000. # greater than any possible candidate.ahead > + rand = 1.1 # greater than any possible candidate.rand > + which = None > + used_rand = False > + > + for c in candidates: > + if c.status == HOPEFUL or c.status == ALMOST: > + if c.ahead < ahead: > + ahead = c.ahead > + rand = c.rand > + which = c > + use_rand = False > + elif c.ahead == ahead: > + use_rand = True > + if c.rand < rand: > + ran = c.rand > + which = c > + > + if use_rand: > + debug.append('Random choice used!') > + > + assert which > + which.eliminate() > + > + > +def generate_random(count): > + random.seed(0) ### choose a seed based on input? for now: repeatable. > + while True: > + # Generate COUNT values in [0.0, 1.0) > + values = [random.random() for x in range(count)] > + > + # Ensure there are no duplicates > + for value in values: > + if values.count(value) > 1: > + break > else: > - winners = letters > + # The loop finished without breaking out > + return values > > - # Compile list of winner names > + > +def tallySTV(votes, issue): > + > + m = re.match(r"stv(\d+)", issue['type']) > + if not m: > + raise Exception("Not an STV vote!") > + > + numseats = int(m.group(1)) > + candidates = {} > + z = 0 > + for c in issue['candidates']: > + candidates[chr(ord('a') + z)] = c['name'] > + z += 1 > + > + # run the stv calc > + winners = run_vote(candidates, votes, numseats) > winnernames = [] > - random.shuffle(winners) > + > for c in winners: > - i = ord(c) - ord('a') > - winnernames.append(candidates[i]) > + winnernames.append(candidates[c]) > > # Return the data > return { > > >