# -*- coding: utf-8 -*-
"""
Copyright Sylvain Bouveret, Yann Chevaleyre and François Durand
sylvain.bouveret@imag.fr, yann.chevaleyre@dauphine.fr, fradurand@gmail.com
This file is part of Whalrus.
Whalrus is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Whalrus 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with Whalrus. If not, see <http://www.gnu.org/licenses/>.
"""
import logging
from whalrus.utils.utils import DeleteCacheMixin, cached_property, NiceSet
from whalrus.priorities.priority import Priority
from whalrus.converters_ballot.converter_ballot_general import ConverterBallotGeneral
from whalrus.profiles.profile import Profile
from whalrus.converters_ballot.converter_ballot import ConverterBallot
from typing import Union
[docs]class Rule(DeleteCacheMixin):
"""
A voting rule.
A :class:`Rule` object is a callable whose inputs are ballots and optionally weights, voters and candidates.
When the rule is called, it loads the profile. The output of the call is the rule itself. But
after the call, you can access to the computed variables (ending with an underscore), such as
:attr:`cowinners_`.
At the initialization of a :class:`Rule` object, some options can be given, such as a tie-break rule or a
converter. In some subclasses, there can also be an option about the way to count abstentions, etc.
Parameters
----------
args
If present, these parameters will be passed to ``__call__`` immediately after initialization.
tie_break : Priority
A tie-break rule.
converter : ConverterBallot
The converter that is used to convert input ballots in order to compute :attr:`profile_converted_`.
Default: :class:`ConverterBallotGeneral`.
kwargs
If present, these parameters will be passed to ``__call__`` immediately after initialization.
Attributes
----------
profile_original_ : Profile
The profile as it is entered by the user. Since it uses the constructor of :class:`Profile`, it indirectly uses
:class:`ConverterBallotGeneral` to ensure, for example, that strings like ``'a > b > c'`` are converted to
:class:`Ballot` objects.
profile_converted_ : Profile
The profile, with ballots that are adapted to the voting rule. For example, in :class:`RulePlurality`, it will
be :class:`BallotPlurality` objects, even if the original ballots are :class:`BallotOrder` objects. This uses
the parameter ``converter`` of the rule.
candidates_ : NiceSet
The candidates of the election, as entered in the ``__call__``.
Examples
--------
Cf. :class:`RulePlurality` for some examples.
"""
def __init__(self, *args, tie_break: Priority = Priority.UNAMBIGUOUS, converter: ConverterBallot = None, **kwargs):
"""
Remark: this `__init__` must always be called at the end of the subclasses' `__init__`.
"""
if converter is None:
converter = ConverterBallotGeneral()
# Parameters
self.tie_break = tie_break
self.converter = converter
# Computed variables
self.profile_original_ = None
self.profile_converted_ = None
self.candidates_ = None
# Optional: load a profile at initialization
if args or kwargs:
self(*args, **kwargs)
def __call__(self, ballots: Union[list, Profile] = None, weights: list = None, voters: list = None,
candidates: set = None):
self.profile_original_ = Profile(ballots, weights=weights, voters=voters)
self.profile_converted_ = Profile([self.converter(b, candidates) for b in self.profile_original_],
weights=self.profile_original_.weights, voters=self.profile_original_.voters)
if candidates is None:
candidates = set().union(*[b.candidates for b in self.profile_converted_])
self.candidates_ = NiceSet(candidates)
self._check_profile(candidates)
self.delete_cache()
return self
def _check_profile(self, candidates: set) -> None:
if any([b.candidates != candidates for b in self.profile_converted_]):
logging.warning('Some ballots do not have the same set of candidates as the whole election.')
@cached_property
def n_candidates_(self) -> int:
"""int: Number of candidates.
"""
return len(self.candidates_)
@cached_property
def cowinners_(self) -> NiceSet:
"""NiceSet: Cowinners of the election, i.e. the candidates that fare best in the election.. This is the first
equivalence class in :attr:`order_`. For example, in :class:`RuleScoreNum`, it is the candidates that are tied
for the best score.
"""
# N.B.: it is recommended to override this method when it is possible to make computation cheaper.
return self.order_[0]
@cached_property
def winner_(self) -> object:
"""object: The winner of the election. This is the first candidate in :attr:`strict_order_` and also the
choice of the tie-breaking rule in :attr:`cowinners_`.
"""
return self.tie_break.choice(self.cowinners_)
@cached_property
def cotrailers_(self) -> NiceSet:
"""NiceSet: "Cotrailers" of the election, i.e. the candidates that fare worst in the election. This is the last
equivalence class in :attr:`order_`. For example, in :class:`RuleScoreNum`, it is the candidates that are tied
for the worst score.
"""
# N.B.: it is recommended to override this method when it is possible to make computation cheaper.
return self.order_[-1]
@cached_property
def trailer_(self) -> object:
"""object: The "trailer" of the election. This is the last candidate in :attr:`strict_order_` and also the
unfavorable choice of the tie-breaking rule in :attr:`cotrailers_`.
"""
if len(self.cotrailers_) == self.n_candidates_:
# Caution, the winner is in ``self.cotrailers_``...
if self.n_candidates_ == 1:
# If there is only one candidate, you have no choice.
return list(self.candidates_)[0]
else:
# In other cases, you must be careful not to output the winner (especially for random tie-breaking).
return self.tie_break.choice(
[candidate for candidate in self.cotrailers_ if candidate != self.winner_], reverse=True)
return self.tie_break.choice(self.cotrailers_, reverse=True)
@cached_property
def order_(self) -> list:
"""list: Result of the election as a (weak) order over the candidates. This is a list of :class:`NiceSet`. The
first set contains the candidates that are tied for victory, etc.
"""
raise NotImplementedError
@cached_property
def strict_order_(self) -> list:
"""list: Result of the election as a strict order over the candidates. The first element is the winner, etc.
This may use the tie-breaking rule.
"""
strict_order = [candidate for tie_class in self.order_ for candidate in self.tie_break.sort(tie_class)]
# Check if this is consistent with ``self.winner_`` and ``self.trailer_`` (especially for random tie-breaking).
if strict_order[0] != self.winner_:
strict_order.remove(self.winner_)
strict_order.insert(0, self.winner_)
if strict_order[-1] != self.trailer_:
strict_order.remove(self.trailer_)
strict_order.append(self.trailer_)
return strict_order