2020-12-23 21:39:43 +11:00
# pyRCV2: Preferential vote counting
2021-01-03 18:57:56 +11:00
# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
2020-12-23 21:39:43 +11:00
#
# 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 <https://www.gnu.org/licenses/>.
import pyRCV2 . blt
import pyRCV2 . model
import pyRCV2 . numbers
2020-12-29 02:43:37 +11:00
from pyRCV2 . method . base_stv import UIGSTVCounter , WIGSTVCounter , EGSTVCounter
2021-01-05 01:27:30 +11:00
from pyRCV2 . ties import TiesBackwards , TiesForwards , TiesPrompt , TiesRandom
2020-12-23 21:39:43 +11:00
2020-12-24 00:04:30 +11:00
import sys
2020-12-23 21:39:43 +11:00
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) ' )
2020-12-23 22:36:49 +11:00
parser . add_argument ( ' --quota-criterion ' , ' -c ' , choices = [ ' geq ' , ' gt ' ] , default = ' geq ' , help = ' quota criterion (default: geq) ' )
2021-01-01 22:26:57 +11:00
parser . add_argument ( ' --quota-mode ' , choices = [ ' static ' , ' progressive ' , ' ers97 ' ] , default = ' static ' , help = ' whether to apply a form of progressive quota (default: static) ' )
parser . add_argument ( ' --no-bulk-elect ' , action = ' store_true ' , help = ' disable bulk election unless absolutely required ' )
parser . add_argument ( ' --bulk-exclude ' , action = ' store_true ' , help = ' use bulk exclusion ' )
parser . add_argument ( ' --defer-surpluses ' , action = ' store_true ' , help = ' defer surplus transfers if possible ' )
2021-01-03 00:28:35 +11:00
parser . add_argument ( ' --numbers ' , ' -n ' , choices = [ ' fixed ' , ' rational ' , ' native ' ] , default = ' fixed ' , help = ' numbers mode (default: fixed) ' )
2020-12-23 21:39:43 +11:00
parser . add_argument ( ' --decimals ' , type = int , default = 5 , help = ' decimal places if --numbers fixed (default: 5) ' )
2021-01-01 22:26:57 +11:00
parser . add_argument ( ' --no-round-quota ' , action = ' store_true ' , help = ' do not round the quota ' )
parser . add_argument ( ' --round-quota ' , type = int , help = ' round quota to specified decimal places ' )
2021-01-03 00:28:35 +11:00
parser . add_argument ( ' --round-votes ' , type = int , help = ' round votes to specified decimal places ' )
2021-01-01 22:26:57 +11:00
parser . add_argument ( ' --round-tvs ' , type = int , help = ' round transfer values to specified decimal places ' )
2021-01-03 00:28:35 +11:00
parser . add_argument ( ' --round-weights ' , type = int , help = ' round ballot weights to specified decimal places ' )
2020-12-23 21:39:43 +11:00
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) ' )
2021-01-03 18:57:56 +11:00
parser . add_argument ( ' --method ' , ' -m ' , choices = [ ' wig ' , ' uig ' , ' eg ' ] , default = ' wig ' , help = ' method of transferring surpluses (default: wig - weighted inclusive Gregory) ' )
2020-12-29 02:43:37 +11:00
parser . add_argument ( ' --transferable-only ' , action = ' store_true ' , help = ' examine only transferable papers during surplus distributions ' )
2021-01-03 18:57:56 +11:00
parser . add_argument ( ' --exclusion ' , choices = [ ' one_round ' , ' parcels_by_order ' , ' by_value ' , ' wright ' ] , default = ' one_round ' , help = ' how to perform exclusions (default: one_round) ' )
2021-01-05 01:27:30 +11:00
parser . add_argument ( ' --ties ' , ' -t ' , action = ' append ' , choices = [ ' backwards ' , ' forwards ' , ' prompt ' , ' random ' ] , default = None , help = ' how to resolve ties (default: backwards then random) ' )
2020-12-24 00:04:30 +11:00
parser . add_argument ( ' --random-seed ' , default = None , help = ' arbitrary string used to seed the RNG for random tie breaking ' )
2021-01-03 19:16:48 +11:00
parser . add_argument ( ' --hide-excluded ' , action = ' store_true ' , help = ' hide excluded candidates from results report ' )
parser . add_argument ( ' --sort-votes ' , action = ' store_true ' , help = ' sort candidates by votes in results report ' )
2021-01-04 18:10:07 +11:00
parser . add_argument ( ' --pp-decimals ' , type = int , default = 2 , help = ' print votes to specified decimal places in results report (default: 2) ' )
2020-12-23 21:39:43 +11:00
2021-01-03 19:16:48 +11:00
def print_step ( args , stage , result ) :
2021-01-03 01:23:53 +11:00
print ( ' {} . {} ' . format ( stage , result . comment ) )
2020-12-23 21:39:43 +11:00
2021-01-03 19:16:48 +11:00
results = list ( result . candidates . items ( ) )
if args . sort_votes :
results . sort ( key = lambda x : x [ 1 ] . votes , reverse = True )
for candidate , count_card in results :
2020-12-23 21:39:43 +11:00
state = None
2020-12-29 02:43:37 +11:00
if count_card . state == pyRCV2 . model . CandidateState . ELECTED or count_card . state == pyRCV2 . model . CandidateState . PROVISIONALLY_ELECTED or count_card . state == pyRCV2 . model . CandidateState . DISTRIBUTING_SURPLUS :
2021-01-05 03:37:09 +11:00
state = ' ELECTED {} ' . format ( count_card . order_elected )
2020-12-29 02:43:37 +11:00
elif count_card . state == pyRCV2 . model . CandidateState . EXCLUDED or count_card . state == pyRCV2 . model . CandidateState . EXCLUDING :
2021-01-05 03:37:09 +11:00
state = ' Excluded {} ' . format ( - count_card . order_elected )
2020-12-23 21:39:43 +11:00
elif count_card . state == pyRCV2 . model . CandidateState . WITHDRAWN :
state = ' Withdrawn '
2021-01-04 18:10:07 +11:00
ppVotes = count_card . votes . pp ( args . pp_decimals )
ppTransfers = count_card . transfers . pp ( args . pp_decimals )
2021-01-03 19:16:48 +11:00
2020-12-23 21:39:43 +11:00
if state is None :
2021-01-03 19:16:48 +11:00
print ( ' - {} : {} ( {} ) ' . format ( candidate . name , ppVotes , ppTransfers ) )
2020-12-23 21:39:43 +11:00
else :
2021-01-04 18:10:07 +11:00
if not ( args . hide_excluded and state == ' Excluded ' and float ( ppVotes ) == 0 and float ( ppTransfers ) == 0 ) :
2021-01-03 19:16:48 +11:00
print ( ' - {} : {} ( {} ) - {} ' . format ( candidate . name , ppVotes , ppTransfers , state ) )
2020-12-23 21:39:43 +11:00
2021-01-04 18:10:07 +11:00
print ( ' Exhausted: {} ( {} ) ' . format ( result . exhausted . votes . pp ( args . pp_decimals ) , result . exhausted . transfers . pp ( args . pp_decimals ) ) )
print ( ' Loss to fraction: {} ( {} ) ' . format ( result . loss_fraction . votes . pp ( args . pp_decimals ) , result . loss_fraction . transfers . pp ( args . pp_decimals ) ) )
print ( ' Total votes: {} ' . format ( result . total . pp ( args . pp_decimals ) ) )
2021-01-01 22:26:57 +11:00
if args . quota_mode == ' ers97 ' and result . vote_required_election < result . quota :
2021-01-04 18:10:07 +11:00
print ( ' Vote required for election: {} ' . format ( result . vote_required_election . pp ( args . pp_decimals ) ) )
2021-01-01 22:26:57 +11:00
else :
2021-01-04 18:10:07 +11:00
print ( ' Quota: {} ' . format ( result . quota . pp ( args . pp_decimals ) ) )
2020-12-23 21:39:43 +11:00
print ( )
def main ( args ) :
# Set settings
if args . numbers == ' native ' :
pyRCV2 . numbers . set_numclass ( pyRCV2 . numbers . Native )
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 ' :
2020-12-27 18:57:36 +11:00
counter = UIGSTVCounter ( election , vars ( args ) )
2020-12-29 02:43:37 +11:00
elif args . method == ' eg ' :
counter = EGSTVCounter ( election , vars ( args ) )
2020-12-23 21:39:43 +11:00
else :
2020-12-27 18:57:36 +11:00
counter = WIGSTVCounter ( election , vars ( args ) )
2020-12-23 21:39:43 +11:00
2021-01-01 22:26:57 +11:00
if args . no_round_quota :
counter . options [ ' round_quota ' ] = None
2020-12-23 22:36:49 +11:00
if args . ties is None :
2021-01-03 19:16:48 +11:00
args . ties = [ ' prompt ' ]
2020-12-23 22:36:49 +11:00
counter . options [ ' ties ' ] = [ ]
for t in args . ties :
if t == ' backwards ' :
2020-12-24 01:36:39 +11:00
counter . options [ ' ties ' ] . append ( TiesBackwards ( counter ) )
2021-01-05 01:27:30 +11:00
elif t == ' forwards ' :
counter . options [ ' ties ' ] . append ( TiesForwards ( counter ) )
2020-12-23 22:36:49 +11:00
elif t == ' prompt ' :
counter . options [ ' ties ' ] . append ( TiesPrompt ( ) )
elif t == ' random ' :
2020-12-24 00:04:30 +11:00
if args . random_seed is None :
2021-01-01 22:26:57 +11:00
print ( ' A --random-seed is required to use random tie breaking. ' )
2020-12-24 00:04:30 +11:00
sys . exit ( 1 )
counter . options [ ' ties ' ] . append ( TiesRandom ( args . random_seed ) )
2020-12-23 22:36:49 +11:00
2021-01-01 22:26:57 +11:00
counter . options [ ' bulk_elect ' ] = not args . no_bulk_elect
2020-12-29 02:43:37 +11:00
counter . options [ ' papers ' ] = ' transferable ' if args . transferable_only else ' both '
2020-12-27 18:27:41 +11:00
2020-12-23 21:39:43 +11:00
# Reset
2021-01-03 01:23:53 +11:00
stage = 1
2020-12-23 21:39:43 +11:00
result = counter . reset ( )
2021-01-03 19:16:48 +11:00
print_step ( args , stage , result )
2020-12-23 21:39:43 +11:00
# Step election
while True :
2021-01-03 01:23:53 +11:00
stage + = 1
2020-12-23 21:39:43 +11:00
result = counter . step ( )
if isinstance ( result , pyRCV2 . model . CountCompleted ) :
break
2021-01-03 19:16:48 +11:00
print_step ( args , stage , result )