diff --git a/pyRCV2/__main__.py b/pyRCV2/__main__.py new file mode 100644 index 0000000..5e0e0ff --- /dev/null +++ b/pyRCV2/__main__.py @@ -0,0 +1,29 @@ +# pyRCV2: Preferential vote counting +# Copyright © 2020 Lee Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from .cli import stv + +import argparse + +parser = argparse.ArgumentParser(prog='python -m pyRCV', description='pyRCV2: Preferential vote counting') +subparsers = parser.add_subparsers(title='method', dest='subcommand') + +stv.add_parser(subparsers) + +args = parser.parse_args() + +if args.subcommand == 'stv': + stv.main(args) diff --git a/pyRCV2/cli/stv.py b/pyRCV2/cli/stv.py new file mode 100644 index 0000000..dcf0f6e --- /dev/null +++ b/pyRCV2/cli/stv.py @@ -0,0 +1,95 @@ +# pyRCV2: Preferential vote counting +# Copyright © 2020 Lee Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pyRCV2.blt +import pyRCV2.model +import pyRCV2.numbers + +from pyRCV2.method.base_stv import BaseUIGSTVCounter, BaseWIGSTVCounter +from pyRCV2.method.wright import WrightSTVCounter + +def add_parser(subparsers): + parser = subparsers.add_parser('stv', help='single transferable vote') + parser.add_argument('file', help='path to BLT file') + + parser.add_argument('--quota', '-q', choices=['droop', 'droop_exact', 'hare', 'hare_exact'], default='droop', help='quota calculation (default: droop)') + parser.add_argument('--quota-criterion', '-qc', choices=['geq', 'gt'], default='geq', help='quota criterion (default: geq)') + parser.add_argument('--prog-quota', action='store_true', help='progressively reducing quota') + parser.add_argument('--numbers', '-n', choices=['fixed', 'rational', 'int', 'native'], default='fixed', help='numbers mode (default: fixed)') + parser.add_argument('--decimals', type=int, default=5, help='decimal places if --numbers fixed (default: 5)') + parser.add_argument('--surplus-order', '-s', choices=['size', 'order'], default='size', help='whether to distribute surpluses by size or by order of election (default: size)') + parser.add_argument('--method', '-m', choices=['wig', 'uig', 'wright'], default='wig', help='method of surpluses and exclusions (default: wig - weighted inclusive Gregory)') + +def print_step(result): + print(result.comment) + + for candidate, count_card in result.candidates.items(): + state = None + if count_card.state == pyRCV2.model.CandidateState.ELECTED or count_card.state == pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED: + state = 'ELECTED' + elif count_card.state == pyRCV2.model.CandidateState.EXCLUDED: + state = 'Excluded' + elif count_card.state == pyRCV2.model.CandidateState.WITHDRAWN: + state = 'Withdrawn' + + if state is None: + print('- {}: {} ({})'.format(candidate.name, count_card.votes.pp(2), count_card.transfers.pp(2))) + else: + print('- {}: {} ({}) - {}'.format(candidate.name, count_card.votes.pp(2), count_card.transfers.pp(2), state)) + + print('Exhausted: {} ({})'.format(result.exhausted.votes.pp(2), result.exhausted.transfers.pp(2))) + print('Loss to fraction: {} ({})'.format(result.loss_fraction.votes.pp(2), result.loss_fraction.transfers.pp(2))) + print('Total votes: {}'.format(result.total.pp(2))) + print('Quota: {}'.format(result.quota.pp(2))) + + print() + +def main(args): + # Set settings + if args.numbers == 'native': + pyRCV2.numbers.set_numclass(pyRCV2.numbers.Native) + elif args.numbers == 'int': + pyRCV2.numbers.set_numclass(pyRCV2.numbers.NativeInt) + elif args.numbers == 'rational': + pyRCV2.numbers.set_numclass(pyRCV2.numbers.Rational) + elif args.numbers == 'fixed': + pyRCV2.numbers.set_numclass(pyRCV2.numbers.Fixed) + pyRCV2.numbers.set_dps(args.decimals) + + with open(args.file, 'r') as f: + election = pyRCV2.blt.readBLT(f.read()) + + # Create counter + if args.method == 'uig': + counter = BaseUIGSTVCounter(election, vars(args)) + elif args.method == 'wright': + counter = WrightSTVCounter(election, vars(args)) + else: + counter = BaseWIGSTVCounter(election, vars(args)) + + # Reset + result = counter.reset() + + print_step(result) + + # Step election + while True: + result = counter.step() + + if isinstance(result, pyRCV2.model.CountCompleted): + break + + print_step(result) diff --git a/pyRCV2/method/base_stv.py b/pyRCV2/method/base_stv.py index 009176e..c4ceed0 100644 --- a/pyRCV2/method/base_stv.py +++ b/pyRCV2/method/base_stv.py @@ -173,9 +173,9 @@ class BaseSTVCounter: if len(has_surplus) > 0: # Distribute surpluses in specified order if self.options['surplus_order'] == 'size': - has_surplus.sort(lambda x: x[1].votes, True) + has_surplus.sort(key=lambda x: x[1].votes, reverse=True) elif self.options['surplus_order'] == 'order': - has_surplus.sort(lambda x: x[1].order_elected) + has_surplus.sort(key=lambda x: x[1].order_elected) else: raise STVException('Invalid surplus order option') @@ -251,7 +251,7 @@ class BaseSTVCounter: """ hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL] - hopefuls.sort(lambda x: x[1].votes) + hopefuls.sort(key=lambda x: x[1].votes) # TODO: Handle ties candidate_excluded, count_card = hopefuls[0] diff --git a/pyRCV2/numbers/fixed_py.py b/pyRCV2/numbers/fixed_py.py index 27b4e10..26801ea 100644 --- a/pyRCV2/numbers/fixed_py.py +++ b/pyRCV2/numbers/fixed_py.py @@ -29,7 +29,10 @@ class Fixed: """ def __init__(self, val): - self.impl = Decimal(val).quantize(_quantize_exp) + if isinstance(val, Fixed): + self.impl = val.impl + else: + self.impl = Decimal(val).quantize(_quantize_exp) def pp(self, dp): """Pretty print to specified number of decimal places""" @@ -46,7 +49,7 @@ class Fixed: return Fixed(self.impl - other.impl) def __mul__(self, other): return Fixed(self.impl * other.impl) - def __div__(self, other): + def __truediv__(self, other): return Fixed(self.impl / other.impl) def __gt__(self, other): diff --git a/pyRCV2/numbers/int_py.py b/pyRCV2/numbers/int_py.py index ddd47c0..fe91112 100644 --- a/pyRCV2/numbers/int_py.py +++ b/pyRCV2/numbers/int_py.py @@ -22,7 +22,10 @@ class NativeInt: """ def __init__(self, val): - self.impl = int(val) + if isinstance(val, NativeInt): + self.impl = val.impl + else: + self.impl = int(val) def pp(self, dp): """Pretty print to specified number of decimal places""" @@ -39,7 +42,7 @@ class NativeInt: return NativeInt(self.impl - other.impl) def __mul__(self, other): return NativeInt(self.impl * other.impl) - def __div__(self, other): + def __truediv__(self, other): return NativeInt(self.impl / other.impl) def __gt__(self, other): diff --git a/pyRCV2/numbers/native_py.py b/pyRCV2/numbers/native_py.py index 21ac3fd..b8f7d9e 100644 --- a/pyRCV2/numbers/native_py.py +++ b/pyRCV2/numbers/native_py.py @@ -22,7 +22,10 @@ class Native: """ def __init__(self, val): - self.impl = float(val) + if isinstance(val, Native): + self.impl = val.impl + else: + self.impl = float(val) def pp(self, dp): """Pretty print to specified number of decimal places""" @@ -39,7 +42,7 @@ class Native: return Native(self.impl - other.impl) def __mul__(self, other): return Native(self.impl * other.impl) - def __div__(self, other): + def __truediv__(self, other): return Native(self.impl / other.impl) def __gt__(self, other): diff --git a/pyRCV2/numbers/rational_py.py b/pyRCV2/numbers/rational_py.py index 1e090e0..6b79cbb 100644 --- a/pyRCV2/numbers/rational_py.py +++ b/pyRCV2/numbers/rational_py.py @@ -23,13 +23,17 @@ class Rational: """ def __init__(self, val): - self.impl = Fraction(val) + if isinstance(val, Rational): + self.impl = val.impl + else: + self.impl = Fraction(val) def pp(self, dp): """ Pretty print to specified number of decimal places """ - return format(self.impl, '.{}f'.format(dp)) + # TODO: Work out if there is a better way of doing this + return format(float(self.impl), '.{}f'.format(dp)) def to_rational(self): return self @@ -47,7 +51,7 @@ class Rational: return Rational(self.impl - other.impl) def __mul__(self, other): return Rational(self.impl * other.impl) - def __div__(self, other): + def __truediv__(self, other): return Rational(self.impl / other.impl) def __gt__(self, other):