# pyRCV2: Preferential vote counting # Copyright © 2020–2021 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 pytest import pyRCV2.blt import pyRCV2.numbers from pyRCV2.method.base_stv import STVException from pyRCV2.method.meek import MeekSTVCounter from pyRCV2.model import CandidateState, CountCompleted from pyRCV2.ties import TiesBackwards def test_meekm(): """Compare count of meekm.blt with model result produced by Hill et al. reference implementation""" pyRCV2.numbers.set_numclass(pyRCV2.numbers.Fixed) pyRCV2.numbers.set_dps(9) with open('tests/data/meekm.blt', 'r') as f: election = pyRCV2.blt.readBLT(f.read()) assert len(election.candidates) == 4 cand_a = next(c for c in election.candidates if c.name == 'Adam') cand_b = next(c for c in election.candidates if c.name == 'Basil') cand_c = next(c for c in election.candidates if c.name == 'Charlotte') cand_d = next(c for c in election.candidates if c.name == 'Donald') assert election.withdrawn == [cand_b] counter = MeekSTVCounter(election, { 'quota': 'droop_exact', 'quota_criterion': 'gt', 'quota_mode': 'progressive', 'ties': [] }) # Stage 1 result = counter.reset() assert result.comment == 'First preferences' assert result.total == pyRCV2.numbers.Num('13') assert result.quota == pyRCV2.numbers.Num('4') assert len(result.candidates.impl) == 4 assert result.candidates[cand_a].state == CandidateState.ELECTED assert result.candidates[cand_b].state == CandidateState.WITHDRAWN assert result.candidates[cand_c].state == CandidateState.HOPEFUL assert result.candidates[cand_d].state == CandidateState.HOPEFUL assert result.candidates[cand_a].votes == pyRCV2.numbers.Num('7') assert result.candidates[cand_c].votes == pyRCV2.numbers.Num('1') assert result.candidates[cand_d].votes == pyRCV2.numbers.Num('4') assert result.exhausted.votes == pyRCV2.numbers.Num('1') # Stage 2 result = counter.step() assert result.total == pyRCV2.numbers.Num('13') assert result.quota == pyRCV2.numbers.Num('4') assert result.candidates[cand_a].state == CandidateState.ELECTED assert result.candidates[cand_b].state == CandidateState.WITHDRAWN assert result.candidates[cand_c].state == CandidateState.HOPEFUL assert result.candidates[cand_d].state == CandidateState.HOPEFUL assert result.candidates[cand_a].votes == pyRCV2.numbers.Num('4') assert result.candidates[cand_c].votes == pyRCV2.numbers.Num('4') assert result.candidates[cand_d].votes == pyRCV2.numbers.Num('4') assert result.exhausted.votes == pyRCV2.numbers.Num('1') # Stage 3 # FIXME: This fails in Javascript (rounding errors?) with pytest.raises(STVException) as exc_info: result = counter.step() assert exc_info.value.message == 'Unable to resolve tie' # Retry further stages counter.options['ties'] = [TiesBackwards(counter)] result = counter.step() while not isinstance(result, CountCompleted): result = counter.step() assert result.candidates[cand_a].state == CandidateState.ELECTED assert result.candidates[cand_b].state == CandidateState.WITHDRAWN assert result.candidates[cand_c].state == CandidateState.EXCLUDED # Charlotte loses the tie if broken backwards assert result.candidates[cand_d].state == CandidateState.ELECTED