From 475ac0353c609fa9c616885d7d7e76bae6c206a3 Mon Sep 17 00:00:00 2001 From: Anna Bruce <44057980+the-Bruce@users.noreply.github.com> Date: Sat, 9 Mar 2024 23:36:44 +0000 Subject: [PATCH 1/4] Correct STV algorithm Add tests election data from https://github.com/jklundell/droop, and reference answers. Change stv calculator to use floats (as it gets far too slow on the test sets when using rational maths) Fix calculator logic when flowing results past elected candidates --- votes/stv.py | 204 ++++++---- votes/tests/conftest.py | 10 + votes/tests/data/42.blt | 8 + votes/tests/data/513.blt | 9 + votes/tests/data/M135.blt | 712 ++++++++++++++++++++++++++++++++++ votes/tests/data/SC-Vm-12.blt | 20 + votes/tests/data/SC.blt | 110 ++++++ votes/tests/data/SCw.blt | 111 ++++++ votes/tests/test_stv.py | 77 +++- 9 files changed, 1177 insertions(+), 84 deletions(-) create mode 100644 votes/tests/conftest.py create mode 100644 votes/tests/data/42.blt create mode 100644 votes/tests/data/513.blt create mode 100644 votes/tests/data/M135.blt create mode 100644 votes/tests/data/SC-Vm-12.blt create mode 100644 votes/tests/data/SC.blt create mode 100644 votes/tests/data/SCw.blt diff --git a/votes/stv.py b/votes/stv.py index e3874a8..d847777 100644 --- a/votes/stv.py +++ b/votes/stv.py @@ -1,6 +1,6 @@ import secrets from enum import Enum -from fractions import Fraction +from decimal import Decimal, Context, localcontext, ROUND_DOWN, ROUND_UP from operator import attrgetter, itemgetter from typing import Dict, List, Set, Tuple @@ -35,7 +35,7 @@ class Candidate: def __init__(self, id_: int): self.id = id_ self.status = States.HOPEFUL - self.keep_factor: Fraction = Fraction(1) + self.keep_factor: float = 1.0 def __str__(self): return f"{self.id}: {self.status} ({str(self.keep_factor)})" @@ -45,10 +45,10 @@ def __repr__(self): class Vote: - def __init__(self, candidates: Dict[int, Candidate], prefs: Tuple[int]): + def __init__(self, candidates: Dict[int, Candidate], prefs: Tuple[int, ...]): self.prefs = tuple(map(candidates.get, prefs)) - def check(self, candidates: Set[int]): + def check(self, candidates: Set[Candidate]): if len(self.prefs) != len(set(self.prefs)): raise ElectionError(f"Double Vote [{self.prefs}]") for i in self.prefs: @@ -63,7 +63,7 @@ def __repr__(self): class Election: - def __init__(self, candidates: Set[int], votes: List[Tuple[int]], seats: int): + def __init__(self, candidates: Set[int], votes: List[Tuple[int, ...]], seats: int): self.candidatedict = {i: Candidate(i) for i in candidates} self.candidates = set(self.candidatedict.values()) self.votes = [Vote(self.candidatedict, i) for i in votes] @@ -72,11 +72,7 @@ def __init__(self, candidates: Set[int], votes: List[Tuple[int]], seats: int): self.fulllog = [] self.actlog = [] print(candidates, votes, seats) - # Huge initial value - # (surplus should never be this high in our situation (its more votes than there are people in the world)) - # If this code is still used when population is this high, - # why the fuck haven't you moved this to a faster language?????? - self.previous_surplus = Fraction(10000000000000000000000000, 1) + self.omega = 0.000001 for i in self.votes: i.check(self.candidates) @@ -84,83 +80,100 @@ def withdraw(self, candidates: Set[int]): candidates = [self.candidatedict[cand] for cand in candidates] for i in candidates: i.status = States.WITHDRAWN - i.keep_factor = Fraction(0) + i.keep_factor = 0.0 def round(self): self.rounds += 1 # B1 shortcircuit = False electable = [] + elected = [] for candidate in self.candidates: if candidate.status == States.ELECTED or candidate.status == States.HOPEFUL: electable.append(candidate) + if candidate.status == States.ELECTED: + elected.append(candidate) if len(electable) <= self.seats: for i in electable: i.status = States.ELECTED shortcircuit = True + elif len(elected) == self.seats: + for candidate in self.candidates: + if candidate.status == States.HOPEFUL: + candidate.status = States.DEFEATED + shortcircuit = True - # B2a - wastage = Fraction(0) - scores = {k: Fraction(0) for k in self.candidates} - for vote in self.votes: - weight: Fraction = Fraction(1) - for candidate in vote.prefs: - delta: Fraction = weight * candidate.keep_factor - scores[candidate] += delta - weight -= delta - wastage += weight - - # Check all votes accounted for - assert wastage + sum(scores.values()) == len(self.votes) - - # B2b - quota = Fraction(sum(scores.values()), self.seats + 1) - - if shortcircuit: - # Defer shortcircuit until after scores calculated to log one extra line - self._log(scores, wastage, quota) - self._report() - raise StopIteration("Election Finished") - - # B2c - elected = False - for candidate in self.candidates: - if candidate.status == States.HOPEFUL and scores[candidate] > quota: - candidate.status = States.ELECTED - elected = True + converged = False + wastage = 0.0 + scores = {k: 0.0 for k in self.candidates} + quota = None + + previous_surplus = float("+Infinity") + while not converged: + # B2a + wastage = 0.0 + scores = {k: 0.0 for k in self.candidates} + for vote in self.votes: + weight: float = 1.0 + for candidate in vote.prefs: + delta: float = weight * candidate.keep_factor + scores[candidate] += delta + weight -= delta + if weight == 0: + continue + wastage += weight + + # Check all votes accounted for + assert len(self.votes) - self.omega <= wastage + sum(scores.values()) <= len(self.votes) + self.omega + + # B2b + quota = sum(scores.values()) / (self.seats + 1) + 0.000000001 + + if shortcircuit: + # Defer shortcircuit until after scores calculated to log one extra line + self._log(scores, wastage, quota) + self._report() + raise StopIteration("Election Finished") + + # B2c + elected = False + for candidate in self.candidates: + if candidate.status == States.HOPEFUL and scores[candidate] > quota: + candidate.status = States.ELECTED + elected = True - # B2d - surplus = Fraction(0) - for candidate in self.candidates: - if candidate.status == States.ELECTED: - surplus += scores[candidate] - quota - - # B2e - if elected: - self.previous_surplus = surplus - self._log(scores, wastage, quota) - return - - if surplus == 0 or surplus >= self.previous_surplus: - # B3 - sorted_results = sorted( - filter(lambda x: x[0].status == States.HOPEFUL, scores.items()), - key=itemgetter(1), - ) - min_score = sorted_results[0][1] - eliminated_candidate: Candidate = self._choose( - list(filter(lambda x: x[1] == min_score, sorted_results)) - ) - eliminated_candidate.status = States.DEFEATED - eliminated_candidate.keep_factor = Fraction(0) - else: - # B2f + # B2d + surplus = 0.0 for candidate in self.candidates: if candidate.status == States.ELECTED: - candidate.keep_factor = Fraction( - candidate.keep_factor * quota, scores[candidate] - ) - self.previous_surplus = surplus + surplus += scores[candidate] - quota + + # B2e + if elected: + self._log(scores, wastage, quota) + return + + if surplus < self.omega or surplus >= previous_surplus: + converged = True + else: + # B2f + for candidate in self.candidates: + if candidate.status == States.ELECTED: + candidate.keep_factor = (candidate.keep_factor * quota) / scores[candidate] + previous_surplus = surplus + + # B3 + sorted_results = sorted( + filter(lambda x: x[0].status == States.HOPEFUL, scores.items()), + key=itemgetter(1), + ) + min_score = sorted_results[0][1] + eliminated_candidate: Candidate = self._choose( + list(filter(lambda x: x[1] <= min_score + self.omega, sorted_results)) + ) + eliminated_candidate.status = States.DEFEATED + eliminated_candidate.keep_factor = 0.0 + self._log(scores, wastage, quota) def _choose(self, candidates): @@ -193,19 +206,19 @@ def _log(self, scores, wastage, quota): self._addlog(self.rounds) self._addlog("======") candstates = {} - for i in self.candidates: + for i in sorted(self.candidates, key=attrgetter('id')): assert isinstance(i, Candidate) - self._addlog("Candidate:", i.id, i.keep_factor.limit_denominator(1000)) + self._addlog("Candidate:", i.id, i.keep_factor) self._addlog("Status:", str(i.status)) - self._addlog("Votes:", str(scores[i].limit_denominator(1000))) + self._addlog("Votes:", str(scores[i])) self._addlog() candstates[str(i.id)] = { - "keep_factor": float(i.keep_factor.limit_denominator(1000)), + "keep_factor": float(i.keep_factor), "status": str(i.status), - "votes": float(scores[i].limit_denominator(1000)), + "votes": float(scores[i]), } - self._addlog("Wastage:", str(wastage.limit_denominator(1000))) - self._addlog("Threshold:", str(quota.limit_denominator(1000))) + self._addlog("Wastage:", str(wastage)) + self._addlog("Threshold:", str(quota)) self._addlog() self._addaction( @@ -213,8 +226,8 @@ def _log(self, scores, wastage, quota): { "round": self.rounds, "candidates": candstates, - "wastage": float(wastage.limit_denominator(1000)), - "threshold": float(quota.limit_denominator(1000)), + "wastage": float(wastage), + "threshold": float(quota), }, ) @@ -239,14 +252,14 @@ def _report(self): def full_election(self): # Log initial state - scores = {k: Fraction(0) for k in self.candidates} - wastage = Fraction(0) + scores = {k: Decimal(0) for k in self.candidates} + wastage = Decimal(0) for vote in self.votes: if len(vote.prefs) > 0: scores[vote.prefs[0]] += 1 else: wastage += 1 - quota = Fraction(sum(scores.values()), self.seats + 1) + quota = Decimal(sum(scores.values())) / Decimal(self.seats + 1) self._log(scores, wastage, quota) try: @@ -262,3 +275,30 @@ def winners(self): filter(lambda x: x.status == States.ELECTED, self.candidates), ) ) + + +class DeterministicElection(Election): + def __init__(self, *args, random_picks=None, **kwargs): + super().__init__(*args, **kwargs) + self.random_picks = random_picks + + def _choose(self, candidates): + if len(candidates) > 1: + # Fix random choice to known behaviour. Only useful for tests + i = self.random_picks[self.rounds] + a = [candidate[0] for candidate in candidates if candidate[0].id == i][0] + self._addlog("-Tiebreak-") + self._addlog("! DETERMINISTIC PICK ! DEBUG ONLY ! DETERMINISTIC PICK !") + self._addlog(a) + self._addlog() + self._addaction( + "tiebreak", + { + "round": self.rounds, + "candidates": [str(candidate[0].id) for candidate in candidates], + "choice": str(a.id), + }, + ) + else: + a = candidates[0][0] + return a diff --git a/votes/tests/conftest.py b/votes/tests/conftest.py new file mode 100644 index 0000000..addfd78 --- /dev/null +++ b/votes/tests/conftest.py @@ -0,0 +1,10 @@ +from pathlib import Path + +import pytest + + +@pytest.fixture +def data(): + test_dir = Path(__file__).absolute().parent + + return test_dir / "data" diff --git a/votes/tests/data/42.blt b/votes/tests/data/42.blt new file mode 100644 index 0000000..3db434a --- /dev/null +++ b/votes/tests/data/42.blt @@ -0,0 +1,8 @@ +3 2 +4 1 2 0 +2 3 0 +0 +"Castor" +"Pollux" +"Helen" +"Pollux and Helen should tie" diff --git a/votes/tests/data/513.blt b/votes/tests/data/513.blt new file mode 100644 index 0000000..ff153be --- /dev/null +++ b/votes/tests/data/513.blt @@ -0,0 +1,9 @@ +3 2 +5 1 2 0 +1 2 0 +3 3 0 +0 +"Castor" +"Pollux" +"Helen" +"Pollux and Helen should tie" diff --git a/votes/tests/data/M135.blt b/votes/tests/data/M135.blt new file mode 100644 index 0000000..4945c5d --- /dev/null +++ b/votes/tests/data/M135.blt @@ -0,0 +1,712 @@ +25 7 +1 21 17 9 20 6 7 15 5 22 13 8 0 +1 1 7 0 +1 19 16 15 4 17 7 8 9 20 18 11 12 2 14 22 3 13 1 0 +1 20 13 6 18 15 23 0 +1 7 22 15 3 4 5 13 20 16 17 11 12 23 9 19 0 +1 6 22 0 +1 1 2 3 0 +1 1 2 12 0 +1 1 2 23 17 6 16 10 11 22 5 19 14 4 0 +1 8 23 24 12 19 2 11 22 17 18 1 6 0 +1 14 6 21 17 18 19 20 4 15 16 2 12 11 7 3 24 9 13 0 +1 18 19 24 4 20 16 15 7 13 14 9 0 +1 4 22 7 21 11 20 1 18 17 16 6 2 3 14 23 0 +1 3 10 14 9 6 13 11 12 20 17 18 7 22 0 +1 12 18 0 +1 1 21 17 13 5 16 12 11 7 3 18 9 24 14 2 19 10 15 8 0 +1 2 6 15 9 7 8 16 10 24 12 23 22 18 3 13 1 4 5 20 17 14 0 +1 6 21 2 10 11 12 15 16 24 3 13 18 9 0 +1 1 2 13 17 24 7 10 6 3 4 18 19 0 +1 2 20 19 0 +1 14 13 0 +1 5 8 14 9 16 12 2 3 22 18 11 20 4 10 17 0 +1 1 0 +1 18 20 3 15 17 22 10 0 +1 1 18 9 16 14 15 6 17 22 0 +1 19 22 24 12 23 0 +1 16 19 22 9 0 +1 2 13 18 10 7 15 16 8 5 1 12 23 22 9 20 4 11 19 6 17 24 14 0 +1 10 14 23 17 5 19 4 16 6 3 13 8 2 12 9 24 21 18 22 0 +1 21 10 16 0 +1 3 13 11 12 10 14 15 19 0 +1 1 20 7 16 17 18 13 14 22 19 8 15 11 21 9 10 2 3 4 0 +1 10 2 23 5 4 11 18 12 15 14 9 0 +1 11 5 3 1 7 4 15 16 0 +1 7 21 10 11 20 13 17 24 19 0 +1 1 2 13 23 16 0 +1 10 1 19 12 8 21 20 13 23 24 9 6 11 0 +1 2 18 24 6 7 8 16 3 0 +1 10 14 9 13 8 3 15 17 24 6 0 +1 3 4 0 +1 1 9 10 4 7 17 0 +1 1 2 23 5 24 9 10 13 14 8 22 21 19 15 16 12 6 7 4 0 +1 24 17 0 +1 1 23 20 10 8 14 13 4 18 11 2 0 +1 1 6 3 2 18 10 14 19 11 12 13 0 +1 6 7 0 +1 6 22 20 5 1 12 13 23 9 24 14 7 3 15 16 11 18 4 2 0 +1 15 21 0 +1 24 18 7 21 13 23 5 0 +1 12 4 1 24 6 9 7 0 +1 11 12 15 3 23 21 10 18 9 2 13 5 6 16 17 22 0 +1 19 14 0 +1 18 24 14 6 22 17 21 4 23 7 8 20 13 5 16 11 0 +1 13 19 16 6 12 10 11 14 8 3 9 0 +1 5 1 2 6 12 18 23 19 15 3 13 9 24 10 0 +1 3 4 23 17 5 22 18 11 2 20 15 21 9 8 19 16 10 0 +1 19 14 1 11 17 18 9 10 2 23 5 13 15 0 +1 10 18 9 20 4 12 23 15 16 2 14 6 7 11 19 13 3 5 17 22 8 1 24 0 +1 3 13 19 10 20 23 9 21 16 6 7 8 17 11 0 +1 6 7 8 9 14 22 18 23 5 13 11 12 0 +1 6 7 8 4 0 +1 7 0 +1 2 1 13 17 21 6 7 5 12 14 20 4 8 9 11 16 22 3 23 15 0 +1 14 15 16 24 8 20 18 19 13 12 2 23 5 1 10 11 22 4 0 +1 15 14 1 16 6 19 18 13 5 7 22 4 3 0 +1 1 9 11 19 13 18 22 5 3 8 0 +1 11 4 1 2 18 0 +1 6 8 9 24 3 23 10 20 18 4 19 11 17 13 21 0 +1 4 22 18 10 5 7 1 12 0 +1 1 21 6 20 7 3 5 15 24 14 10 11 12 8 9 2 16 17 13 23 22 4 0 +1 12 6 0 +1 6 14 15 16 21 2 12 13 5 23 10 19 11 17 18 9 20 3 4 8 24 7 0 +1 1 9 10 15 23 3 4 20 14 22 13 5 11 12 8 17 16 0 +1 21 6 24 0 +1 15 0 +1 23 5 4 2 12 19 22 0 +1 1 2 3 16 13 14 18 23 17 0 +1 4 3 13 1 11 0 +1 8 17 22 10 21 5 11 14 7 0 +1 3 7 15 11 12 6 21 2 0 +1 10 11 24 7 16 22 9 6 20 17 1 2 3 23 8 12 0 +1 12 8 13 5 18 6 24 16 7 11 0 +1 8 9 10 11 4 23 20 17 18 19 13 3 5 1 2 7 0 +1 15 0 +1 1 9 20 3 2 12 0 +1 5 20 11 12 2 9 16 0 +1 18 4 20 15 21 14 24 10 23 8 11 22 13 9 16 19 7 0 +1 3 10 2 15 21 5 19 7 4 20 12 17 1 9 16 0 +1 2 15 1 13 18 19 24 17 5 11 12 4 20 23 3 9 8 6 14 16 7 22 21 0 +1 13 9 10 20 16 12 6 3 11 5 2 0 +1 12 23 2 20 3 9 10 11 17 18 19 0 +1 11 19 2 3 23 6 16 17 4 5 1 20 15 8 14 10 21 9 13 24 7 0 +1 13 9 23 8 20 15 6 16 0 +1 9 6 7 21 10 13 17 0 +1 7 8 23 3 5 10 11 12 14 15 16 24 1 22 2 19 18 20 21 0 +1 12 15 3 0 +1 5 13 14 0 +1 12 19 20 23 10 21 4 5 7 0 +1 15 7 4 10 11 22 21 6 17 24 12 0 +1 19 13 11 24 2 23 14 0 +1 7 20 12 8 2 3 18 4 11 22 6 24 10 16 13 14 23 9 0 +1 7 16 6 3 4 17 11 22 9 12 20 18 0 +1 16 18 10 1 8 9 12 24 21 20 6 7 15 5 4 2 23 3 11 22 0 +1 24 23 8 16 17 22 20 13 19 6 1 2 18 10 11 0 +1 20 11 10 6 19 13 21 22 16 0 +1 20 8 0 +1 5 13 14 8 20 23 3 10 15 17 11 12 6 24 9 0 +1 5 1 2 3 21 16 23 10 9 0 +1 1 2 7 8 16 13 12 23 15 22 18 17 0 +1 16 9 14 2 6 21 5 0 +1 12 1 7 8 13 16 10 5 19 18 3 9 6 24 14 0 +1 4 0 +1 3 23 7 8 16 5 12 0 +1 4 8 15 11 9 21 0 +1 16 6 1 9 13 17 21 11 12 18 19 0 +1 1 12 17 0 +1 22 9 16 0 +1 1 2 7 6 19 12 14 13 17 21 9 10 11 20 4 18 24 23 16 5 22 15 3 0 +1 9 10 6 17 18 11 19 0 +1 21 16 0 +1 1 10 18 23 12 17 22 3 4 5 15 24 13 11 16 19 21 0 +1 9 5 1 13 12 11 15 16 0 +1 1 3 23 4 19 0 +1 2 3 15 0 +1 4 15 0 +1 1 5 18 6 4 22 12 2 15 16 7 8 9 10 21 24 13 19 0 +1 3 23 19 20 12 15 21 2 6 0 +1 3 15 19 0 +1 9 12 2 0 +1 1 23 5 4 9 10 11 14 17 20 16 12 13 3 6 15 0 +1 8 23 15 16 18 21 2 6 3 4 0 +1 19 10 6 18 15 21 1 17 22 20 0 +1 14 15 4 12 2 8 10 0 +1 11 3 4 22 6 12 8 20 23 0 +1 11 2 6 21 24 18 19 0 +1 13 16 24 6 7 12 11 0 +1 4 5 17 20 8 9 10 15 23 16 2 3 6 11 18 1 13 19 24 14 0 +1 4 21 18 17 0 +1 3 16 12 5 1 9 4 20 7 8 22 2 0 +1 4 5 0 +1 8 2 6 7 21 24 5 10 19 13 0 +1 9 24 1 19 15 10 14 16 23 8 6 3 11 0 +1 14 19 17 11 18 1 16 13 15 8 12 3 4 5 0 +1 12 19 18 14 10 1 2 15 11 22 20 4 5 17 0 +1 11 21 15 16 0 +1 7 21 20 19 5 12 14 16 15 0 +1 23 0 +1 6 9 18 13 14 24 17 4 3 7 19 5 2 11 0 +1 13 3 11 17 20 15 8 2 23 21 10 22 7 6 18 4 24 9 14 0 +1 21 0 +1 8 12 6 18 10 13 14 23 24 7 1 22 4 5 20 16 17 0 +1 10 2 5 3 24 8 14 0 +1 10 13 6 7 18 20 23 5 2 3 15 12 1 4 8 0 +1 8 17 21 10 12 14 9 2 24 13 3 6 16 23 11 20 4 5 18 1 15 0 +1 4 1 15 12 6 7 24 18 19 13 16 5 3 20 17 9 14 0 +1 1 18 4 10 11 12 6 22 3 15 0 +1 1 23 0 +1 1 2 14 13 4 8 9 23 21 22 3 5 12 18 19 16 6 7 15 0 +1 1 2 6 18 19 17 24 3 4 15 5 7 16 8 9 0 +1 4 17 0 +1 14 6 12 19 17 20 15 8 0 +1 2 0 +1 5 9 11 12 15 19 8 2 6 24 18 14 23 7 0 +1 11 19 4 22 24 6 23 21 16 0 +1 22 19 2 15 7 16 0 +1 19 2 3 9 10 11 17 0 +1 1 4 5 9 15 17 14 18 23 6 7 13 19 0 +1 1 5 22 15 14 20 12 11 16 21 9 10 0 +1 12 23 21 2 3 8 20 16 19 15 17 0 +1 15 16 8 13 14 24 9 12 0 +1 8 2 3 7 23 10 11 19 24 12 20 9 6 17 14 15 0 +1 7 15 1 23 19 20 12 2 3 11 24 14 0 +1 9 10 23 5 17 21 11 14 6 3 4 7 8 16 12 19 18 20 13 1 2 15 0 +1 14 9 20 6 24 17 23 21 15 0 +1 17 23 8 9 10 15 14 5 19 1 4 0 +1 2 3 15 21 22 12 8 16 10 20 14 5 23 13 18 19 4 1 7 0 +1 19 10 5 12 20 8 24 18 7 0 +1 4 21 19 8 22 1 9 11 7 0 +1 20 10 11 19 14 3 4 0 +1 1 19 7 8 9 22 12 6 15 14 23 17 10 21 2 3 11 24 5 13 0 +1 1 18 8 9 0 +1 10 11 0 +1 13 20 21 24 0 +1 1 12 2 13 11 17 22 3 15 4 5 21 10 18 9 16 23 8 14 24 6 7 20 19 0 +1 17 11 14 8 23 16 21 24 7 13 1 2 22 9 10 19 4 5 15 12 6 0 +1 1 22 18 7 8 10 21 6 24 0 +1 8 9 2 16 21 23 3 4 13 6 12 0 +1 7 22 13 19 15 23 9 17 18 16 12 8 4 5 20 6 24 10 11 14 2 3 21 0 +1 10 0 +1 6 7 21 13 24 10 14 23 5 11 18 3 4 2 12 0 +1 6 7 9 23 13 3 14 15 8 22 18 19 4 2 20 1 21 10 17 11 12 5 0 +1 9 6 24 4 14 15 16 18 13 11 20 3 19 22 2 12 5 1 8 0 +1 15 1 2 7 6 21 11 20 14 16 13 18 23 5 0 +1 1 2 18 22 19 24 21 5 23 11 12 17 9 20 8 16 0 +1 15 11 20 23 0 +1 5 3 4 0 +1 5 19 22 2 20 4 7 15 21 10 9 11 8 6 24 3 0 +1 1 7 8 22 21 15 6 24 14 4 5 13 20 19 0 +1 13 0 +1 15 11 19 24 17 3 22 12 14 6 9 23 13 2 1 4 7 8 10 0 +1 14 15 16 13 3 11 9 10 2 0 +1 13 11 17 1 19 8 15 4 3 0 +1 9 22 18 21 0 +1 6 15 23 18 19 4 5 17 1 2 7 8 9 11 14 22 3 21 16 0 +1 6 13 16 12 15 11 0 +1 20 10 11 0 +1 10 20 13 6 18 14 15 12 4 19 22 0 +1 14 17 24 15 5 1 20 9 21 6 8 16 0 +1 2 0 +1 1 20 21 24 11 12 4 14 2 7 15 19 10 6 9 8 16 23 3 22 5 13 0 +1 11 12 8 24 9 15 0 +1 4 20 12 24 10 21 17 5 16 13 14 18 0 +1 1 6 7 8 9 20 23 14 3 17 22 19 12 15 0 +1 15 23 3 13 1 6 9 10 21 5 18 16 8 24 0 +1 10 15 9 7 8 13 17 20 12 0 +1 8 23 9 10 11 14 15 1 22 4 19 2 3 21 0 +1 6 18 7 8 12 21 10 20 4 17 15 16 0 +1 15 14 22 24 3 4 11 12 13 19 0 +1 18 5 23 8 22 15 16 10 1 2 6 17 12 11 20 3 21 4 0 +1 1 6 17 9 18 0 +1 1 6 7 12 20 4 2 3 8 16 19 13 15 18 9 0 +1 24 14 6 21 10 11 19 16 15 20 23 22 0 +1 24 20 16 8 2 0 +1 19 16 17 9 10 0 +1 1 2 0 +1 23 16 11 10 24 12 0 +1 20 1 2 17 0 +1 3 11 17 21 22 1 8 16 0 +1 9 0 +1 17 18 1 0 +1 17 10 11 12 24 0 +1 2 0 +1 12 21 22 18 1 24 9 8 15 0 +1 11 19 2 7 15 5 16 0 +1 15 7 20 18 9 23 21 0 +1 5 21 4 1 23 9 8 22 10 11 12 0 +1 11 22 20 12 4 1 13 14 24 21 10 2 0 +1 23 9 10 15 17 18 6 14 7 16 8 2 3 5 19 20 21 13 11 12 1 22 4 0 +1 10 3 23 9 12 8 15 16 17 20 11 22 4 1 2 6 13 14 0 +1 9 10 11 21 2 13 1 16 0 +1 10 6 3 23 4 12 17 1 11 24 18 16 2 7 9 8 22 21 5 19 0 +1 18 19 16 4 14 23 17 24 10 11 22 21 0 +1 20 12 14 16 19 11 22 15 0 +1 1 10 9 6 8 7 0 +1 11 0 +1 9 11 0 +1 1 2 15 14 5 19 13 6 0 +1 3 21 5 20 17 16 11 0 +1 17 18 5 11 10 22 19 12 0 +1 8 0 +1 1 23 12 13 2 7 15 3 4 0 +1 5 2 6 24 13 21 12 1 22 20 18 19 7 15 17 10 11 9 16 23 14 8 4 3 0 +1 9 10 11 2 18 6 7 17 14 13 15 16 1 23 3 4 12 0 +1 2 3 21 18 19 13 1 11 24 15 9 12 0 +1 16 24 6 3 21 11 12 4 1 2 23 13 18 15 0 +1 18 19 23 9 11 0 +1 1 23 5 20 10 9 17 3 4 12 21 0 +1 4 7 22 15 23 11 16 6 9 2 1 14 20 19 13 5 18 8 24 3 12 0 +1 6 20 3 5 7 21 16 24 23 19 22 2 13 14 0 +1 3 2 14 6 21 11 12 1 0 +1 7 24 16 17 18 3 12 13 19 23 8 22 6 15 0 +1 7 4 15 11 24 13 23 3 8 12 1 10 16 17 18 21 2 14 19 0 +1 10 12 3 8 23 17 22 5 16 4 19 20 18 9 15 0 +1 1 23 10 12 18 8 13 14 20 4 5 24 9 21 3 16 17 0 +1 11 22 4 0 +1 7 4 2 16 14 11 5 19 15 12 6 3 23 18 1 20 0 +1 1 12 3 19 13 18 22 20 16 17 10 15 21 11 7 8 9 0 +1 4 11 22 13 3 23 21 24 19 5 1 10 6 20 12 14 15 16 8 7 0 +1 1 23 0 +1 5 3 7 20 11 16 18 24 14 21 9 6 12 2 13 1 4 19 17 10 0 +1 1 4 8 9 2 7 21 24 17 6 3 15 0 +1 7 15 11 12 5 17 0 +1 1 8 24 18 5 20 17 11 12 15 16 10 0 +1 5 2 18 1 4 22 3 19 10 15 14 11 6 24 16 23 13 20 8 9 0 +1 11 16 21 17 12 13 18 19 15 0 +1 20 4 17 24 21 9 19 10 11 15 3 12 23 0 +1 1 13 5 24 11 16 2 12 6 3 0 +1 2 3 10 18 16 5 1 8 7 15 22 13 19 23 21 0 +1 10 12 6 9 13 23 4 17 0 +1 2 0 +1 2 3 11 24 14 20 0 +1 7 17 10 20 19 13 23 0 +1 24 15 10 11 22 21 5 1 6 17 0 +1 10 6 16 15 5 19 18 4 12 11 17 3 9 0 +1 16 6 13 20 21 17 9 24 10 12 0 +1 7 0 +1 22 18 10 12 6 7 14 0 +1 3 22 10 12 15 16 17 11 20 13 5 1 6 9 24 23 21 4 2 18 19 7 8 0 +1 7 16 13 19 21 22 17 18 9 4 12 20 3 24 8 2 5 23 14 0 +1 20 17 18 1 22 14 3 13 12 6 7 0 +1 6 5 7 8 13 20 23 16 12 14 0 +1 10 15 16 13 23 11 20 4 12 6 5 24 9 2 1 22 14 0 +1 14 16 19 6 0 +1 13 14 6 21 0 +1 15 16 11 9 18 3 21 22 6 13 5 12 19 2 14 24 8 7 0 +1 10 11 22 17 21 9 0 +1 19 14 7 9 4 5 0 +1 1 0 +1 8 9 6 3 21 17 7 15 0 +1 6 11 4 9 10 19 20 7 23 5 16 21 13 14 3 0 +1 7 8 5 1 2 3 4 11 21 0 +1 23 11 5 15 1 12 17 20 8 7 0 +1 21 12 10 19 22 5 13 23 3 17 16 2 14 20 1 6 18 9 24 0 +1 16 11 1 12 22 3 8 2 20 21 24 18 23 4 5 6 19 13 14 15 7 0 +1 12 6 9 10 13 19 14 15 16 0 +1 3 19 20 5 22 10 2 13 4 0 +1 20 8 9 11 12 6 13 14 15 16 2 18 22 3 10 0 +1 13 17 12 6 19 18 5 1 2 3 23 0 +1 11 5 15 23 16 13 20 21 22 3 0 +1 12 13 1 6 16 18 5 20 22 7 3 4 2 8 11 14 23 21 24 0 +1 5 2 11 16 10 18 21 23 17 20 4 0 +1 14 24 16 13 23 10 4 0 +1 15 24 6 7 8 20 16 11 5 23 21 10 9 3 12 14 18 17 2 22 0 +1 3 4 21 16 11 20 17 18 1 19 14 2 0 +1 2 6 23 14 0 +1 8 15 1 13 5 3 6 12 23 17 0 +1 3 11 10 4 16 24 17 18 0 +1 9 16 13 11 17 22 12 0 +1 4 8 12 14 15 0 +1 1 2 3 18 19 20 5 13 15 14 9 0 +1 1 2 7 0 +1 20 19 4 7 15 6 11 18 1 2 12 8 16 5 22 17 13 9 23 0 +1 1 4 9 14 24 7 5 13 6 20 12 19 8 17 18 23 11 21 16 10 15 0 +1 15 21 6 3 9 16 7 1 2 23 0 +1 13 5 8 16 15 0 +1 6 12 2 8 9 16 24 5 20 1 19 10 11 13 22 3 21 0 +1 11 15 21 5 7 8 23 24 17 22 20 18 1 2 10 14 0 +1 18 19 6 7 5 24 2 3 23 17 21 0 +1 1 6 7 8 22 21 2 12 0 +1 1 22 20 10 13 15 8 9 16 2 3 23 12 0 +1 12 0 +1 8 9 10 6 23 13 15 14 20 1 18 7 12 11 0 +1 6 12 17 24 16 9 0 +1 1 15 16 9 6 19 17 3 12 0 +1 6 3 12 24 18 19 14 15 16 17 23 8 1 13 20 21 2 7 22 9 10 0 +1 1 2 11 12 14 10 20 0 +1 2 3 23 5 17 16 4 13 14 15 8 0 +1 1 2 18 19 22 21 16 8 0 +1 1 16 0 +1 14 10 11 12 23 17 16 18 19 0 +1 7 12 2 19 16 5 13 14 0 +1 17 22 2 14 3 19 18 24 11 15 7 8 16 12 0 +1 10 20 1 8 24 23 0 +1 11 12 3 13 14 17 18 16 6 21 1 24 2 4 5 8 9 23 0 +1 11 0 +1 1 18 11 7 20 19 0 +1 9 14 24 21 13 8 6 16 7 22 18 10 5 1 2 20 3 4 19 0 +1 3 13 21 0 +1 20 19 5 2 0 +1 11 22 14 15 16 8 9 21 4 1 2 12 0 +1 3 1 5 8 9 2 17 4 0 +1 3 24 12 1 6 13 14 15 23 19 20 22 4 18 2 21 5 7 16 11 0 +1 21 10 19 11 0 +1 23 15 2 18 14 8 9 10 13 17 4 22 12 6 7 3 5 19 0 +1 12 1 7 6 18 15 23 4 11 9 10 13 5 8 24 17 21 2 3 19 0 +1 5 4 8 2 12 0 +1 14 15 12 4 16 6 24 3 5 18 21 22 11 17 9 2 8 23 0 +1 14 16 12 24 9 8 23 3 18 4 0 +1 1 2 3 17 21 6 12 11 14 22 4 0 +1 22 4 5 13 11 0 +1 14 15 21 6 17 20 12 18 19 13 9 0 +1 23 3 13 15 2 17 1 9 14 18 16 24 12 0 +1 7 15 23 11 24 17 20 3 14 16 8 9 6 12 10 2 18 22 0 +1 3 2 24 17 18 23 9 11 12 6 21 0 +1 10 11 17 9 20 19 0 +1 1 9 23 10 0 +1 10 0 +1 2 15 21 24 3 7 16 17 18 1 6 0 +1 16 17 18 3 23 0 +1 7 22 13 11 12 6 17 18 19 0 +1 9 0 +1 6 19 10 11 5 3 4 24 15 20 12 1 23 2 21 16 0 +1 13 0 +1 23 7 22 11 24 0 +1 12 23 7 2 0 +1 18 1 9 14 17 20 21 16 24 15 23 13 3 7 8 5 4 19 2 11 22 0 +1 5 7 8 2 11 1 23 24 6 21 9 10 13 3 16 19 17 0 +1 1 7 0 +1 24 9 15 0 +1 12 23 4 13 18 15 16 17 3 8 22 2 1 7 21 20 10 11 0 +1 5 23 3 22 18 16 17 13 0 +1 1 8 18 19 22 13 24 17 16 0 +1 9 22 1 14 24 5 12 3 23 10 15 6 19 13 16 0 +1 1 2 5 22 12 18 10 11 21 9 8 14 20 19 0 +1 13 3 6 9 23 11 12 19 1 20 22 8 16 15 0 +1 22 13 0 +1 4 10 1 18 0 +1 1 2 6 9 0 +1 23 16 22 24 18 19 13 14 15 2 4 5 6 9 8 21 0 +1 4 10 11 1 2 6 7 8 15 12 5 13 17 0 +1 10 20 23 0 +1 7 8 0 +1 2 11 12 19 23 8 6 22 16 21 5 24 17 10 1 15 20 9 7 14 3 4 0 +1 10 4 11 14 19 2 13 3 21 17 0 +1 9 24 3 21 4 8 12 6 16 7 1 10 23 0 +1 19 16 21 13 14 15 11 0 +1 6 12 24 14 10 2 11 22 5 8 9 16 18 19 21 0 +1 16 11 5 2 3 23 19 12 18 7 8 13 14 10 6 17 4 21 0 +1 19 20 13 12 0 +1 15 11 1 10 0 +1 6 21 10 9 12 8 15 7 20 19 2 18 13 14 0 +1 16 17 18 11 21 6 1 9 10 22 5 24 8 14 3 23 0 +1 4 22 21 2 3 15 23 5 24 6 7 8 12 17 10 14 16 0 +1 1 23 8 15 14 11 18 9 10 2 13 24 17 20 0 +1 2 12 7 19 10 3 13 11 17 14 15 16 23 18 4 1 8 9 6 24 0 +1 4 9 23 13 14 19 18 8 2 3 21 20 17 10 6 0 +1 10 11 19 15 18 3 21 17 0 +1 15 16 6 3 7 5 11 17 0 +1 8 9 2 3 11 0 +1 18 2 3 4 11 20 1 9 8 15 13 0 +1 15 14 23 12 6 22 18 0 +1 7 8 10 22 9 20 15 12 0 +1 4 5 21 18 11 12 6 1 2 3 13 22 9 20 23 7 15 0 +1 1 2 23 16 5 21 24 10 6 7 3 8 9 20 15 12 11 18 19 0 +1 12 16 11 7 8 24 5 20 19 0 +1 7 6 20 23 19 17 0 +1 1 12 11 0 +1 1 12 6 21 17 22 19 24 9 23 8 20 5 7 15 16 2 14 0 +1 5 23 8 12 3 2 7 21 11 17 18 22 20 19 4 16 13 0 +1 16 7 21 13 18 1 9 15 23 4 5 19 11 12 20 14 8 10 6 22 0 +1 17 18 23 8 13 14 5 12 11 21 10 9 4 24 2 7 6 3 19 20 16 22 0 +1 20 8 9 11 24 0 +1 1 9 22 3 8 2 14 18 19 0 +1 1 2 10 22 19 13 3 4 5 23 17 12 15 7 8 9 24 11 20 16 0 +1 10 2 0 +1 3 10 18 22 5 1 7 20 23 2 24 9 6 19 13 0 +1 5 13 3 7 17 16 24 14 20 6 21 10 4 15 22 2 12 1 19 0 +1 3 24 11 12 15 0 +1 10 14 4 5 1 12 6 11 16 13 19 20 18 3 23 24 0 +1 1 4 5 20 11 0 +1 7 0 +1 4 24 5 1 20 23 2 15 7 8 16 17 18 21 10 6 14 0 +1 12 3 23 20 22 19 2 1 7 0 +1 17 18 10 6 5 0 +1 8 9 15 22 20 7 1 11 16 17 21 5 19 12 6 0 +1 4 1 9 17 0 +1 11 21 20 13 23 2 18 6 22 5 4 9 16 17 0 +1 17 11 24 4 3 12 18 9 0 +1 1 10 19 18 9 0 +1 17 18 10 20 19 24 5 23 9 15 22 4 11 0 +1 21 4 5 20 3 12 2 18 7 0 +1 4 19 8 20 23 12 6 15 14 0 +1 1 23 7 10 20 13 6 17 18 11 21 2 12 15 14 16 24 8 4 22 5 3 19 0 +1 4 18 7 21 24 5 20 3 2 9 6 15 0 +1 18 11 15 16 17 0 +1 5 15 24 8 14 3 13 11 0 +1 19 0 +1 1 8 14 0 +1 11 12 16 19 13 15 23 21 2 22 20 1 8 24 9 18 10 7 6 0 +1 6 18 19 20 4 2 11 0 +1 23 15 14 19 5 1 7 18 0 +1 11 12 4 23 0 +1 15 3 21 17 11 5 1 10 2 12 18 14 13 24 20 23 4 8 9 16 0 +1 13 23 10 17 12 4 0 +1 3 5 4 17 18 14 20 0 +1 20 24 12 11 14 0 +1 14 4 17 13 11 22 19 8 9 1 6 0 +1 23 19 5 4 22 3 8 16 17 7 0 +1 8 16 21 22 4 2 0 +1 6 17 5 24 7 8 23 12 3 10 0 +1 12 11 20 4 21 9 15 16 19 7 8 24 6 0 +1 1 8 0 +1 15 5 7 0 +1 2 7 15 0 +1 14 16 24 7 0 +1 7 8 18 20 6 22 2 3 12 11 15 17 21 14 16 24 5 1 10 13 4 9 23 0 +1 12 8 17 18 22 6 20 1 9 13 24 23 3 5 2 11 0 +1 13 15 2 3 4 6 24 22 12 18 20 5 16 14 23 17 11 1 7 8 0 +1 1 9 10 13 6 12 19 5 22 2 7 14 0 +1 1 7 12 18 13 0 +1 18 10 21 11 19 15 16 17 22 24 14 8 12 9 23 4 20 7 1 2 0 +1 17 5 13 14 4 11 22 19 15 3 6 21 18 7 8 2 16 23 0 +1 13 5 9 20 21 0 +1 3 20 14 15 16 0 +1 14 13 5 12 1 2 3 22 9 10 19 24 11 17 7 15 8 6 20 16 0 +1 6 10 0 +1 9 8 23 21 22 12 6 14 13 24 20 4 5 18 10 11 7 0 +1 1 0 +1 3 24 19 14 0 +1 3 4 14 15 16 21 9 13 17 22 19 0 +1 7 14 23 8 2 6 24 17 20 10 11 12 1 16 5 4 9 0 +1 10 19 14 17 18 0 +1 20 16 0 +1 11 21 0 +1 7 11 19 2 15 17 8 23 16 10 13 3 4 21 22 5 9 12 6 14 18 0 +1 10 15 1 7 19 5 21 12 17 24 18 23 9 8 0 +1 1 5 17 19 22 6 7 20 23 21 2 13 9 11 8 4 12 3 16 0 +1 11 19 24 17 13 8 16 7 3 6 2 14 22 15 0 +1 16 11 19 6 20 7 21 0 +1 1 22 3 8 11 14 15 13 0 +1 16 12 6 7 21 4 5 22 18 19 17 13 20 8 10 11 0 +1 13 11 20 10 15 2 3 23 0 +1 16 17 24 10 22 0 +1 4 2 12 19 23 9 10 5 1 24 6 17 7 3 20 11 0 +1 7 15 16 23 17 21 24 18 3 4 5 20 1 10 13 11 0 +1 13 23 9 21 24 6 0 +1 1 12 19 4 17 11 22 9 8 16 24 7 6 5 2 14 10 21 18 13 0 +1 6 7 8 24 9 15 23 10 14 3 19 4 17 20 18 1 22 13 0 +1 15 1 5 13 6 7 2 3 8 16 17 11 14 0 +1 11 12 6 3 24 8 22 4 2 10 21 18 14 15 20 19 17 0 +1 23 3 13 0 +1 8 2 17 18 10 12 6 16 13 4 7 9 19 0 +1 3 6 7 12 8 16 22 19 5 1 2 4 21 9 11 24 17 18 13 10 20 23 15 14 0 +1 1 7 3 10 11 12 6 5 20 17 0 +1 4 10 12 22 7 20 8 2 0 +1 20 8 4 9 10 11 16 19 7 14 15 0 +1 4 1 6 7 8 9 20 21 17 5 19 0 +1 5 10 18 13 24 15 21 17 20 23 19 22 3 4 8 12 9 1 0 +1 1 6 7 15 13 18 20 17 10 21 9 16 24 0 +1 20 18 10 17 7 23 2 0 +1 13 9 16 11 22 7 5 8 24 17 18 19 0 +1 10 9 8 23 15 0 +1 19 18 23 3 12 4 5 13 21 0 +1 3 4 17 5 22 0 +1 6 3 4 12 1 13 18 19 7 24 9 10 2 14 15 23 21 5 16 17 0 +1 4 5 8 17 20 23 15 16 11 19 24 14 9 10 2 1 22 6 18 7 12 13 3 0 +1 4 5 7 8 0 +1 4 9 13 11 0 +1 6 9 0 +1 5 22 6 24 3 10 13 11 20 18 12 0 +1 15 7 21 5 4 11 2 20 18 9 0 +1 7 1 8 14 10 22 15 3 11 19 13 21 2 24 0 +1 3 13 10 1 7 15 8 0 +1 3 2 11 0 +1 1 13 4 22 15 20 5 6 19 0 +1 18 19 0 +1 2 7 16 14 15 3 12 23 8 9 10 17 0 +1 11 12 15 16 0 +1 22 4 1 12 2 3 8 23 21 17 24 18 9 10 20 13 14 6 7 0 +1 1 7 9 11 21 13 5 12 8 16 17 19 18 24 6 0 +1 7 8 0 +1 1 21 6 9 12 8 14 0 +1 1 12 8 24 9 10 11 15 21 18 0 +1 2 12 23 16 22 0 +1 3 15 1 2 23 8 0 +1 13 8 9 5 1 2 3 20 23 21 24 7 4 17 18 11 12 6 22 15 16 10 19 0 +1 12 23 3 11 5 8 14 9 0 +1 6 7 0 +1 1 9 16 6 19 0 +1 1 22 3 4 0 +1 13 4 7 8 17 18 16 9 24 2 20 23 21 22 14 15 6 0 +1 7 0 +1 23 21 22 18 19 20 15 16 17 3 4 5 13 6 14 11 12 24 9 10 2 7 1 8 0 +1 0 +1 1 6 3 13 0 +1 21 1 3 6 7 8 2 17 13 24 12 11 16 4 22 0 +1 6 0 +1 5 1 16 0 +1 1 20 8 14 18 23 21 2 12 6 7 10 13 19 24 9 16 17 11 3 0 +1 17 21 6 20 10 22 12 0 +1 1 4 0 +1 4 5 12 14 24 10 18 3 13 20 8 22 9 21 0 +1 17 20 23 16 19 22 6 7 0 +1 17 16 13 20 9 8 22 0 +1 5 17 18 0 +1 0 +1 6 0 +1 6 9 19 13 11 22 10 20 1 16 7 4 18 0 +1 2 3 24 21 17 0 +1 5 2 15 0 +1 7 5 20 12 2 3 4 21 18 1 22 17 0 +1 2 14 3 19 13 23 0 +1 20 21 12 14 18 22 4 17 1 24 5 13 2 3 10 11 19 0 +1 16 24 14 23 10 20 5 1 18 19 13 17 12 0 +1 2 7 24 14 16 0 +1 2 14 13 21 10 9 0 +1 4 7 15 0 +1 1 8 9 19 20 16 24 17 12 6 13 11 0 +1 21 2 6 18 23 11 0 +1 10 18 3 22 4 5 19 20 0 +1 5 0 +1 11 3 6 22 12 15 16 0 +1 9 10 22 2 3 4 11 16 21 0 +1 14 10 11 18 22 24 15 19 8 23 5 21 13 1 2 3 16 7 20 17 4 12 9 6 0 +1 23 14 24 15 6 8 16 7 0 +1 4 3 0 +1 13 14 16 11 0 +1 12 5 1 10 20 21 24 3 16 7 13 15 22 17 9 0 +1 21 18 11 14 15 20 23 4 5 1 2 12 3 13 24 10 6 22 0 +1 6 0 +1 2 7 20 19 15 21 13 5 10 17 18 4 22 6 8 9 12 11 16 3 24 23 0 +1 9 2 8 22 6 3 12 13 14 0 +1 1 6 9 20 4 22 17 10 11 16 15 0 +1 17 8 9 16 6 23 3 13 19 22 0 +1 11 0 +1 13 14 23 11 21 2 6 7 15 16 19 18 22 1 12 17 20 24 9 0 +1 1 2 20 21 18 8 6 24 17 9 0 +1 20 3 12 11 0 +1 11 5 13 8 6 7 12 21 22 15 3 24 17 18 10 14 23 16 19 20 4 0 +1 24 0 +1 21 15 23 4 0 +1 8 21 20 23 5 16 2 3 12 0 +1 1 2 9 8 20 21 10 6 4 19 7 18 24 22 0 +1 9 10 18 7 8 24 12 6 14 16 3 15 19 0 +1 1 18 2 0 +1 1 2 14 11 0 +1 3 18 19 11 16 10 17 5 6 7 0 +1 3 22 11 5 2 1 23 10 16 17 6 14 9 24 18 19 0 +1 11 5 3 7 21 24 4 1 2 0 +1 9 10 17 15 1 19 13 11 20 8 23 5 16 24 21 0 +1 13 17 22 5 15 16 20 18 11 24 12 3 21 10 0 +1 8 23 16 13 1 0 +1 2 6 21 9 10 11 24 14 0 +1 14 6 19 16 10 20 4 9 8 12 0 +1 1 11 0 +1 1 2 12 11 5 7 15 16 19 17 18 0 +1 2 6 18 11 24 12 21 7 0 +1 13 9 23 6 24 15 19 8 12 1 2 0 +1 1 23 0 +1 15 0 +1 3 21 16 0 +1 4 23 7 18 13 5 19 14 8 22 2 11 12 15 16 0 +1 1 18 13 5 6 21 9 10 11 20 16 19 2 14 3 15 0 +1 20 13 19 11 4 9 10 18 23 8 24 21 0 +1 12 9 5 15 1 22 6 8 23 4 0 +1 1 2 3 13 14 15 0 +1 5 8 13 14 17 20 23 12 6 7 24 15 0 +1 5 10 11 19 12 1 16 7 14 15 8 22 3 0 +1 9 6 13 19 2 3 12 1 7 11 22 18 21 23 0 +1 10 11 24 8 21 0 +1 16 21 1 22 5 20 13 14 15 24 18 9 10 12 11 0 +1 9 0 +1 20 19 18 0 +1 10 11 12 8 15 24 19 20 18 14 3 13 23 21 2 16 7 1 0 +1 15 16 0 +1 4 8 0 +1 3 21 5 1 6 7 0 +1 7 8 14 18 23 3 5 4 24 2 6 9 10 11 19 15 1 0 +1 1 23 5 6 24 15 2 14 11 12 0 +1 1 2 24 14 6 7 8 19 16 15 21 0 +1 6 9 10 19 20 12 4 8 23 5 22 11 21 16 13 14 15 18 0 +1 10 15 11 2 13 6 17 0 +1 19 8 20 0 +1 8 10 11 21 6 0 +1 2 20 21 6 7 22 9 24 17 18 14 0 +1 12 23 19 13 22 20 14 8 2 3 18 5 1 16 17 7 10 11 0 +1 4 10 19 2 12 11 18 22 20 23 5 15 1 21 9 24 8 7 16 17 14 3 13 0 +1 4 0 +1 3 24 17 21 10 8 13 19 2 0 +1 1 7 6 3 15 0 +1 3 11 16 0 +1 2 9 23 16 22 24 5 0 +1 4 9 20 3 5 15 14 13 19 7 6 21 17 24 8 22 0 +1 9 11 16 21 20 1 17 18 7 8 15 14 2 3 4 5 12 6 24 13 19 10 0 +1 1 20 24 2 11 18 22 3 19 14 0 +1 1 2 14 10 11 18 4 5 6 7 0 +1 4 14 16 6 7 3 21 0 +1 4 19 18 10 17 24 3 23 13 0 +1 1 8 10 22 0 +1 1 2 15 12 6 7 16 11 23 21 0 +1 8 2 23 3 4 18 19 13 20 15 14 9 10 11 24 6 17 22 0 +1 10 1 9 2 20 3 13 21 0 +1 7 19 0 +1 15 16 21 17 18 24 5 1 9 10 20 19 0 +1 12 13 15 16 11 17 18 5 4 22 24 7 0 +1 16 6 15 17 24 9 14 2 10 0 +1 16 3 24 23 5 15 8 12 18 13 7 22 2 19 10 1 20 11 21 17 6 4 14 9 0 +1 11 6 15 2 0 +1 15 8 12 5 4 7 17 11 23 3 16 2 14 0 +1 9 13 3 7 12 6 18 19 20 23 5 22 0 +1 8 13 2 14 15 20 4 0 +1 5 2 1 20 8 23 15 0 +1 4 19 20 17 0 +1 3 8 15 16 9 12 13 4 7 23 11 0 +1 12 5 6 22 14 2 11 0 +1 4 5 17 11 12 0 +1 9 12 8 13 24 21 10 11 5 23 1 17 0 +1 3 11 16 24 21 22 19 0 +1 1 2 13 3 15 0 +1 12 16 6 2 7 23 9 10 11 4 14 15 17 21 19 13 0 +1 15 0 +1 8 2 11 17 5 19 13 14 15 16 18 1 22 20 21 6 7 9 10 23 24 12 0 +1 21 17 18 7 12 15 2 3 22 13 19 14 5 1 4 0 +1 1 2 8 18 23 22 16 11 5 17 3 21 0 +1 6 15 1 2 3 23 4 5 20 14 11 12 17 0 +1 3 12 6 0 +1 4 1 2 9 10 6 20 22 12 5 15 19 18 23 14 7 8 17 21 16 0 +1 1 2 3 9 13 10 20 6 14 16 11 0 +1 3 6 10 15 14 4 0 +0 +"C1" +"C2" +"C3" +"C4" +"C5" +"C6" +"C7" +"C8" +"C9" +"C10" +"C11" +"C12" +"C13" +"C14" +"C15" +"C16" +"C17" +"C18" +"C19" +"C20" +"C21" +"C22" +"C23" +"C24" +"C25" +"M135: Gen v1.1 1 3 45" +"Brian Wichmann, email: brian.wichmann@bcs.org.uk" +"The validation of the STV programs for ERBS, 1st September 1999." +First Prefs + 0119 32 43 44 29 36 34 24 27 35 31 28 25 20 27 18 15 13 15 21 13 6 14 8 0 2 +Quota 84.88 diff --git a/votes/tests/data/SC-Vm-12.blt b/votes/tests/data/SC-Vm-12.blt new file mode 100644 index 0000000..acf9af0 --- /dev/null +++ b/votes/tests/data/SC-Vm-12.blt @@ -0,0 +1,20 @@ +5 3 +333 1 3 0 +333 1 4 0 +333 1 5 0 +333 2 3 0 +333 2 4 0 +333 2 5 0 +667 3 0 +667 4 0 +667 5 0 +1 1 2 3 0 +1 1 2 4 0 +2 2 1 3 0 +0 +"A" +"B" +"X" +"Y" +"Z" +"Voting matters example (Issue 12, 'The computational accuracy using the Meek algorithm')" diff --git a/votes/tests/data/SC.blt b/votes/tests/data/SC.blt new file mode 100644 index 0000000..6381cec --- /dev/null +++ b/votes/tests/data/SC.blt @@ -0,0 +1,110 @@ +13 4 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 13 7 5 4 2 1 0 +1 5 13 2 7 4 1 0 +1 13 4 1 2 0 +1 4 2 13 1 3 0 +1 4 13 2 7 0 +1 7 8 12 1 13 9 2 4 11 5 0 +1 1 13 3 4 7 5 2 0 +1 4 1 13 7 3 5 2 0 +1 4 1 13 7 3 5 2 0 +1 1 4 13 2 7 5 3 9 8 12 11 0 +1 7 1 8 9 13 3 2 4 12 5 11 0 +1 7 8 9 11 12 0 +1 7 1 8 9 13 3 2 4 12 5 11 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 5 1 13 3 2 4 7 0 +1 7 8 9 3 12 2 13 5 1 4 0 +1 3 13 1 2 0 +1 3 13 1 2 0 +1 4 7 13 5 0 +1 4 5 13 3 2 1 0 +1 7 8 9 11 12 0 +1 7 9 8 12 11 2 13 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 4 3 13 1 0 +1 4 3 13 1 0 +1 13 3 4 1 7 2 5 0 +1 3 13 4 1 7 2 5 0 +1 13 3 4 1 2 5 7 0 +1 13 1 4 2 5 3 7 0 +1 13 4 3 1 2 5 0 +1 3 13 1 4 2 7 5 0 +1 4 3 13 8 0 +1 4 3 13 1 0 +1 1 4 13 7 0 +1 1 4 13 7 0 +1 7 3 4 13 1 5 0 +1 9 11 8 7 0 +1 7 8 9 11 12 0 +1 13 3 5 2 0 +1 4 5 2 3 13 0 +1 13 5 2 4 1 3 7 0 +1 13 5 2 1 7 3 8 0 +1 7 8 9 12 11 0 +1 7 8 4 9 12 0 +1 13 4 7 2 8 0 +1 4 13 2 7 1 8 0 +1 7 8 11 9 12 0 +1 8 4 7 11 13 9 0 +1 7 4 2 13 0 +1 13 1 7 4 0 +1 4 1 2 13 9 5 0 +1 1 4 13 5 3 2 0 +1 7 5 4 8 11 2 13 1 3 9 12 0 +1 4 8 7 5 13 2 1 3 9 0 +1 3 4 7 2 12 13 8 1 5 0 +1 7 8 9 11 12 0 +1 7 8 11 9 0 +1 7 8 11 9 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 13 2 4 7 5 1 0 +1 7 8 9 11 12 0 +1 7 8 9 11 0 +1 7 8 9 11 12 0 +1 2 7 8 12 13 11 1 5 9 0 +1 4 7 8 2 0 +1 4 13 5 2 1 3 0 +1 13 4 7 3 1 5 8 2 9 0 +1 4 3 13 1 7 0 +1 13 4 1 3 0 +1 4 1 13 3 2 7 8 0 +1 2 4 13 5 0 +1 2 3 4 1 13 7 0 +1 3 1 4 13 0 +1 4 3 1 2 11 0 +1 3 1 4 13 2 7 0 +1 4 1 2 8 3 0 +1 4 1 5 13 2 3 0 +1 4 1 5 13 2 3 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 13 7 3 2 5 4 1 0 +1 13 5 7 2 4 1 0 +1 7 8 9 11 12 0 +1 1 13 3 7 2 4 5 0 +1 13 1 2 7 4 5 3 0 +1 13 1 2 7 4 5 3 0 +1 3 1 4 2 13 7 9 5 8 0 +0 +"Budd Dickinson" +"Kirit Mookerjee" +"Tom Sevigny" +"Pat LaMarche" +"Michael Piacsek" +"Write-In 2" +"Rebecca Rotzler" +"Kristen Olsen" +"Jason Jones" +"Write-In 1" +"Steve Greenfield" +"Jimmy Leas" +"Steve Kramer" +"GPUS Steering Committee Election 2005-07-24" diff --git a/votes/tests/data/SCw.blt b/votes/tests/data/SCw.blt new file mode 100644 index 0000000..7ec1508 --- /dev/null +++ b/votes/tests/data/SCw.blt @@ -0,0 +1,111 @@ +13 4 +-4 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 13 7 5 4 2 1 0 +1 5 13 2 7 4 1 0 +1 13 4 1 2 0 +1 4 2 13 1 3 0 +1 4 13 2 7 0 +1 7 8 12 1 13 9 2 4 11 5 0 +1 1 13 3 4 7 5 2 0 +1 4 1 13 7 3 5 2 0 +1 4 1 13 7 3 5 2 0 +1 1 4 13 2 7 5 3 9 8 12 11 0 +1 7 1 8 9 13 3 2 4 12 5 11 0 +1 7 8 9 11 12 0 +1 7 1 8 9 13 3 2 4 12 5 11 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 5 1 13 3 2 4 7 0 +1 7 8 9 3 12 2 13 5 1 4 0 +1 3 13 1 2 0 +1 3 13 1 2 0 +1 4 7 13 5 0 +1 4 5 13 3 2 1 0 +1 7 8 9 11 12 0 +1 7 9 8 12 11 2 13 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 4 3 13 1 0 +1 4 3 13 1 0 +1 13 3 4 1 7 2 5 0 +1 3 13 4 1 7 2 5 0 +1 13 3 4 1 2 5 7 0 +1 13 1 4 2 5 3 7 0 +1 13 4 3 1 2 5 0 +1 3 13 1 4 2 7 5 0 +1 4 3 13 8 0 +1 4 3 13 1 0 +1 1 4 13 7 0 +1 1 4 13 7 0 +1 7 3 4 13 1 5 0 +1 9 11 8 7 0 +1 7 8 9 11 12 0 +1 13 3 5 2 0 +1 4 5 2 3 13 0 +1 13 5 2 4 1 3 7 0 +1 13 5 2 1 7 3 8 0 +1 7 8 9 12 11 0 +1 7 8 4 9 12 0 +1 13 4 7 2 8 0 +1 4 13 2 7 1 8 0 +1 7 8 11 9 12 0 +1 8 4 7 11 13 9 0 +1 7 4 2 13 0 +1 13 1 7 4 0 +1 4 1 2 13 9 5 0 +1 1 4 13 5 3 2 0 +1 7 5 4 8 11 2 13 1 3 9 12 0 +1 4 8 7 5 13 2 1 3 9 0 +1 3 4 7 2 12 13 8 1 5 0 +1 7 8 9 11 12 0 +1 7 8 11 9 0 +1 7 8 11 9 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 13 2 4 7 5 1 0 +1 7 8 9 11 12 0 +1 7 8 9 11 0 +1 7 8 9 11 12 0 +1 2 7 8 12 13 11 1 5 9 0 +1 4 7 8 2 0 +1 4 13 5 2 1 3 0 +1 13 4 7 3 1 5 8 2 9 0 +1 4 3 13 1 7 0 +1 13 4 1 3 0 +1 4 1 13 3 2 7 8 0 +1 2 4 13 5 0 +1 2 3 4 1 13 7 0 +1 3 1 4 13 0 +1 4 3 1 2 11 0 +1 3 1 4 13 2 7 0 +1 4 1 2 8 3 0 +1 4 1 5 13 2 3 0 +1 4 1 5 13 2 3 0 +1 7 8 9 11 12 0 +1 7 8 9 11 12 0 +1 13 7 3 2 5 4 1 0 +1 13 5 7 2 4 1 0 +1 7 8 9 11 12 0 +1 1 13 3 7 2 4 5 0 +1 13 1 2 7 4 5 3 0 +1 13 1 2 7 4 5 3 0 +1 3 1 4 2 13 7 9 5 8 0 +0 +"Budd Dickinson" +"Kirit Mookerjee" +"Tom Sevigny" +"Pat LaMarche" +"Michael Piacsek" +"Write-In 2" +"Rebecca Rotzler" +"Kristen Olsen" +"Jason Jones" +"Write-In 1" +"Steve Greenfield" +"Jimmy Leas" +"Steve Kramer" +"GPUS Steering Committee Election 2005-07-24" diff --git a/votes/tests/test_stv.py b/votes/tests/test_stv.py index 3633ceb..77bbcc6 100644 --- a/votes/tests/test_stv.py +++ b/votes/tests/test_stv.py @@ -1,6 +1,79 @@ +from pathlib import Path + import pytest -from votes.stv import Election, ElectionError +from votes.stv import Election, ElectionError, DeterministicElection + + +def read_blt(file_path: Path, det_picks: dict[int,int]): + """ + A blt file looks a bit like this + 4 2 + -2 + 3 1 3 4 0 + 4 1 3 2 0 + 2 4 1 3 0 + 1 2 0 + 2 2 4 3 1 0 + 1 3 4 2 0 + 0 + "Adam" + "Basil" + "Charlotte" + "Donald" + "Title" + """ + + def split_vote(line) -> list[tuple[int, ...]] | None: + vals = [int(x) for x in line.split(" ")] + c = vals.pop(0) + if c == 0: + return None + assert vals.pop() == 0 + return [tuple(vals)] * c + + with file_path.open() as fp: + count, seats = map(int, fp.readline().strip().split(" ")) + maybe_withdrawn = fp.readline().strip() + votes = [] + if "-" in maybe_withdrawn: + withdrawn = [-int(x) for x in maybe_withdrawn.split(" ")] + line = fp.readline().strip() + else: + withdrawn = [] + line = maybe_withdrawn + while True: + v = split_vote(line) + if v is None: + break + votes.extend(v) + line = fp.readline().strip() + e = DeterministicElection(set(range(1, count + 1)), votes, seats, random_picks=det_picks) + if withdrawn: + e.withdraw(set(withdrawn)) + return e + + +@pytest.mark.parametrize("file,picks,winners,tiebreak", [ + ("42.blt", {2:3}, [1, 2], {2: [2, 3]}), + ("513.blt", {2:3}, [1, 2], {2: [2, 3]}), + ("M135.blt", {12:13}, [1, 2, 3, 4, 6, 9, 10], {12: [13, 16]}), + ("SC.blt", {2:6, 3:10, 4:11}, [4, 7, 8, 13], {2: [6, 10, 11, 12], 3: [10, 11, 12], 4:[11,12]}), + ("SCw.blt", {2:6, 3:10, 4:11}, [1, 7, 8, 13], {2: [6, 10, 11, 12], 3: [10, 11, 12], 4:[11,12]}), + ("SC-Vm-12.blt", {2: 3}, [1, 2, 4], {2: [3, 4, 5]}), +]) +def test_blts(data, file, winners, tiebreak, picks): + e = read_blt(data / file, picks) + e.full_election() + assert sorted(e.winners()) == winners + count = 0 + for i in e.actlog: + if i['type'] == "tiebreak": + count += 1 + d = i['details'] + assert d['round'] in tiebreak.keys() + assert sorted([int(c) for c in d['candidates']]) == sorted(tiebreak[d['round']]) + assert count == len(tiebreak) def test_fptp_equivalent(): @@ -72,7 +145,7 @@ def test_corner_case(): # Bug caused the second round to erroneously tiebreak between all people after electing 3. # Check this isn't reintroduced assert e.actlog[2]["type"] != "tiebreak" - assert sorted(e.winners()) == [3, 5] + assert sorted(e.winners()) == [3, 4] def test_two_available_four(): From b8c0b11908cfdfdf8ebfdea776b914b6e7aced61 Mon Sep 17 00:00:00 2001 From: Anna Bruce <44057980+the-Bruce@users.noreply.github.com> Date: Sat, 9 Mar 2024 23:37:56 +0000 Subject: [PATCH 2/4] Maybe add CI tests? --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f2da98..058858b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,8 @@ jobs: run: | pipenv run black --check . pipenv run isort --check-only --profile black . + - name: Run STV tests + run: pipenv run pytest ./votes/tests build-and-push-image: name: Build and Push Docker Image @@ -64,4 +66,4 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha - cache-to: type=gha,mode=max \ No newline at end of file + cache-to: type=gha,mode=max From e4afd87e04d8b6023ef85a2569c999583d73a8b2 Mon Sep 17 00:00:00 2001 From: Anna Bruce <44057980+the-Bruce@users.noreply.github.com> Date: Sat, 9 Mar 2024 23:38:24 +0000 Subject: [PATCH 3/4] black / isort --- votes/stv.py | 14 ++++++++---- votes/tests/test_stv.py | 47 ++++++++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/votes/stv.py b/votes/stv.py index d847777..b47e91b 100644 --- a/votes/stv.py +++ b/votes/stv.py @@ -1,6 +1,6 @@ import secrets +from decimal import ROUND_DOWN, ROUND_UP, Context, Decimal, localcontext from enum import Enum -from decimal import Decimal, Context, localcontext, ROUND_DOWN, ROUND_UP from operator import attrgetter, itemgetter from typing import Dict, List, Set, Tuple @@ -124,7 +124,11 @@ def round(self): wastage += weight # Check all votes accounted for - assert len(self.votes) - self.omega <= wastage + sum(scores.values()) <= len(self.votes) + self.omega + assert ( + len(self.votes) - self.omega + <= wastage + sum(scores.values()) + <= len(self.votes) + self.omega + ) # B2b quota = sum(scores.values()) / (self.seats + 1) + 0.000000001 @@ -159,7 +163,9 @@ def round(self): # B2f for candidate in self.candidates: if candidate.status == States.ELECTED: - candidate.keep_factor = (candidate.keep_factor * quota) / scores[candidate] + candidate.keep_factor = ( + candidate.keep_factor * quota + ) / scores[candidate] previous_surplus = surplus # B3 @@ -206,7 +212,7 @@ def _log(self, scores, wastage, quota): self._addlog(self.rounds) self._addlog("======") candstates = {} - for i in sorted(self.candidates, key=attrgetter('id')): + for i in sorted(self.candidates, key=attrgetter("id")): assert isinstance(i, Candidate) self._addlog("Candidate:", i.id, i.keep_factor) self._addlog("Status:", str(i.status)) diff --git a/votes/tests/test_stv.py b/votes/tests/test_stv.py index 77bbcc6..e37c7fe 100644 --- a/votes/tests/test_stv.py +++ b/votes/tests/test_stv.py @@ -2,10 +2,10 @@ import pytest -from votes.stv import Election, ElectionError, DeterministicElection +from votes.stv import DeterministicElection, Election, ElectionError -def read_blt(file_path: Path, det_picks: dict[int,int]): +def read_blt(file_path: Path, det_picks: dict[int, int]): """ A blt file looks a bit like this 4 2 @@ -48,31 +48,48 @@ def split_vote(line) -> list[tuple[int, ...]] | None: break votes.extend(v) line = fp.readline().strip() - e = DeterministicElection(set(range(1, count + 1)), votes, seats, random_picks=det_picks) + e = DeterministicElection( + set(range(1, count + 1)), votes, seats, random_picks=det_picks + ) if withdrawn: e.withdraw(set(withdrawn)) return e -@pytest.mark.parametrize("file,picks,winners,tiebreak", [ - ("42.blt", {2:3}, [1, 2], {2: [2, 3]}), - ("513.blt", {2:3}, [1, 2], {2: [2, 3]}), - ("M135.blt", {12:13}, [1, 2, 3, 4, 6, 9, 10], {12: [13, 16]}), - ("SC.blt", {2:6, 3:10, 4:11}, [4, 7, 8, 13], {2: [6, 10, 11, 12], 3: [10, 11, 12], 4:[11,12]}), - ("SCw.blt", {2:6, 3:10, 4:11}, [1, 7, 8, 13], {2: [6, 10, 11, 12], 3: [10, 11, 12], 4:[11,12]}), - ("SC-Vm-12.blt", {2: 3}, [1, 2, 4], {2: [3, 4, 5]}), -]) +@pytest.mark.parametrize( + "file,picks,winners,tiebreak", + [ + ("42.blt", {2: 3}, [1, 2], {2: [2, 3]}), + ("513.blt", {2: 3}, [1, 2], {2: [2, 3]}), + ("M135.blt", {12: 13}, [1, 2, 3, 4, 6, 9, 10], {12: [13, 16]}), + ( + "SC.blt", + {2: 6, 3: 10, 4: 11}, + [4, 7, 8, 13], + {2: [6, 10, 11, 12], 3: [10, 11, 12], 4: [11, 12]}, + ), + ( + "SCw.blt", + {2: 6, 3: 10, 4: 11}, + [1, 7, 8, 13], + {2: [6, 10, 11, 12], 3: [10, 11, 12], 4: [11, 12]}, + ), + ("SC-Vm-12.blt", {2: 3}, [1, 2, 4], {2: [3, 4, 5]}), + ], +) def test_blts(data, file, winners, tiebreak, picks): e = read_blt(data / file, picks) e.full_election() assert sorted(e.winners()) == winners count = 0 for i in e.actlog: - if i['type'] == "tiebreak": + if i["type"] == "tiebreak": count += 1 - d = i['details'] - assert d['round'] in tiebreak.keys() - assert sorted([int(c) for c in d['candidates']]) == sorted(tiebreak[d['round']]) + d = i["details"] + assert d["round"] in tiebreak.keys() + assert sorted([int(c) for c in d["candidates"]]) == sorted( + tiebreak[d["round"]] + ) assert count == len(tiebreak) From b2b4391f818a3b08eaf54e7b2f2a82de403025e0 Mon Sep 17 00:00:00 2001 From: Anna Bruce <44057980+the-Bruce@users.noreply.github.com> Date: Sun, 10 Mar 2024 00:04:14 +0000 Subject: [PATCH 4/4] Fix comment --- votes/stv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/votes/stv.py b/votes/stv.py index b47e91b..d7b389c 100644 --- a/votes/stv.py +++ b/votes/stv.py @@ -8,7 +8,7 @@ STV calculator Based on procedure as defined in https://prfound.org/resources/reference/reference-meek-rule/ -Uses exact ratio arithmetic to prevent need to use epsilon float comparisons. +Uses float arithmetic as exact arithmetic became too expensive. Uses a secure random generator to split ties randomly. Unfortunately this is more likely to trigger than I'd prefer due to the small populations and single seats. """