From ea22667640316e272f2dd61ed7ebb156926b113e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0imbera?= Date: Mon, 20 May 2024 14:52:38 +0200 Subject: [PATCH] better overhang, WIP --- README.md | 2 +- tests/evaluate/test_mixed.py | 53 ++++++++++++++++++++++++++++++++ votelib/evaluate/condorcet.py | 2 ++ votelib/evaluate/core.py | 14 ++++++++- votelib/evaluate/proportional.py | 23 ++++++++++++++ 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c163a83..2141ff3 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ list there for some suggestions. How to add features: -1. Fork it (https://github.com/simberaj/pysynth/fork) +1. Fork it () 2. Create your feature branch (`git checkout -b feature/feature-name`) 3. Commit your changes (`git commit -am "feature-name added"`) 4. Push to the branch (`git push origin feature/feature-name`) diff --git a/tests/evaluate/test_mixed.py b/tests/evaluate/test_mixed.py index a72ebf7..4d3aa8b 100644 --- a/tests/evaluate/test_mixed.py +++ b/tests/evaluate/test_mixed.py @@ -10,6 +10,7 @@ import votelib.evaluate.core import votelib.evaluate.cardinal import votelib.evaluate.condorcet +import votelib.evaluate.proportional SMITH_SCORE = votelib.evaluate.core.Conditioned( @@ -58,3 +59,55 @@ def test_smith_score(votes, expected_winner): assert winner == expected_winner +PROPORT_EVAL = votelib.evaluate.proportional.LargestRemainder( + quota_function='hare' +) + + +@pytest.mark.parametrize("system, result", [ + ( + PROPORT_EVAL, + {'A': 54, 'B': 34, 'C': 7, 'D': 5}, + ), + ( + votelib.evaluate.core.AdjustedSeatCount( + votelib.evaluate.core.AllowOverhang(PROPORT_EVAL), + PROPORT_EVAL, + ), + {'A': 54, 'B': 41, 'C': 13, 'D': 5}, + ), + ( + votelib.evaluate.core.AdjustedSeatCount( + votelib.evaluate.core.LevelOverhang(PROPORT_EVAL), + PROPORT_EVAL, + ), + {'A': 63, 'B': 60, 'C': 19, 'D': 5}, + ) +]) +def test_compensatory_systems(system, result): + # https://en.wikipedia.org/wiki/Additional_member_system + elect_res = { + 'A': 54, + 'B': 11, + 'C': 0, + 'D': 5, + } + party_vote = { + 'A': 43, + 'B': 41, + 'C': 13, + 'D': 3, + } + + class MockEvaluator: + def evaluate(self, *args, **kwargs): + return elect_res + + total_evaluator = votelib.evaluate.core.MultistageDistributor([ + MockEvaluator(), + system + ]) + total_res = total_evaluator.evaluate( + party_vote, 100 + ) + assert total_res == result diff --git a/votelib/evaluate/condorcet.py b/votelib/evaluate/condorcet.py index 6b39ef2..7d03884 100644 --- a/votelib/evaluate/condorcet.py +++ b/votelib/evaluate/condorcet.py @@ -462,6 +462,8 @@ def _lock_pairs(cls, if not cls._is_path(locked_pairs, pair[1], pair[0]): logging.info('locking %s over %s', *pair) locked_pairs.append(pair) + else: + logging.info('disregarding %s over %s: conflict with previously locked pairs', *pair) return locked_pairs @staticmethod diff --git a/votelib/evaluate/core.py b/votelib/evaluate/core.py index 7d545ca..e0ac2e7 100644 --- a/votelib/evaluate/core.py +++ b/votelib/evaluate/core.py @@ -586,9 +586,12 @@ class LevelOverhang: results from the second round votes to determine which seats are overhang. Usually will be the same or very similar to the evaluator used in the second round directly. + :param max_multiple: How many overhang and leveling seats in total at most + can be awarded before an infeasibility error is raised. """ - def __init__(self, evaluator: Distributor): + def __init__(self, evaluator: Distributor, max_multiple: float = 1.0): self.evaluator = evaluator + self.max_multiple = max_multiple def calculate(self, votes: Dict[Any, int], @@ -623,11 +626,20 @@ def calculate(self, nonprop_drop += prev_gain adj_count = n_seats - nonprop_drop pmins = list(lowest_allowed.items()) + print(prop_result) + print(lowest_allowed) + print(nonprop_drop) + print(adj_count, prop_result) + print(max_seats) while any(prop_result[party] < minimum for party, minimum in pmins): adj_count += 1 + if adj_count > (self.max_multiple + 1) * n_seats: + raise VotingSystemError('would award too many leveling seats') + print(votes, adj_count, max_seats) prop_result = self.evaluator.evaluate( votes, adj_count, max_seats=max_seats, ) + print(' ', prop_result) return adj_count + nonprop_drop - n_seats diff --git a/votelib/evaluate/proportional.py b/votelib/evaluate/proportional.py index f692f6f..f04a6e7 100644 --- a/votelib/evaluate/proportional.py +++ b/votelib/evaluate/proportional.py @@ -218,6 +218,26 @@ def evaluate(self, :param max_seats: Maximum number of seats that the given candidate/party can obtain in total (including previous gains). """ + if prev_gains: + noprev_result = self.evaluate( + votes, n_seats, max_seats=max_seats + ) + overhung_result = { + cand: n_prev for cand, n_prev in prev_gains.items() + if n_prev > noprev_result.get(cand, 0) + } + print(overhung_result) + if overhung_result: + rest_votes = { + cand: n_votes for cand, n_votes in votes.items() + if cand not in overhung_result + } + rest_n_seats = n_seats - sum(overhung_result.values()) + rest_result = self.evaluate( + rest_votes, rest_n_seats, max_seats=max_seats + ) + print(rest_result) + return votelib.util.sum_dicts(overhung_result, rest_result) quota_val = self.quota_function( sum(votes.values()), n_seats ) @@ -257,6 +277,9 @@ def evaluate(self, )) total_awarded = sum(selected.values()) + sum(prev_gains.values()) if total_awarded > n_seats: + if prev_gains: + + print(prev_gains, selected, votelib.util.sum_dicts(selected, prev_gains)) if self.on_overaward == 'ignore': return selected elif self.on_overaward == 'error':