Пример #1
0
    def _elect_candidate(self, candidate, regular):
        """
        Elects a single candidate as a regular representative

        :param candidate: Candidate-object
        :type candidate: Candidate

        :param regular: True if we are electing a regular candidate
        :type regular: bool
        """
        # regulars
        if (regular and len(self._elected) >=
                self._counter_obj.election.num_choosable):
            raise RequiredCandidatesElected
        # substitutes
        if (not regular and len(self._elected) >=
            (self._counter_obj.election.num_choosable +
             self._counter_obj.election.num_substitutes)):
            raise RequiredCandidatesElected
        if candidate in self._elected:
            # should not happen
            logger.info("Candidate %s is already elected", candidate)
            return
        if self._quotas_disabled:
            self._elected.append(candidate)
            self._state.all_elected_candidates = self._elected
            if not regular:
                self._elected_substitutes.append(candidate)
                self._state.all_elected_substitutes = self._elected_substitutes
            logger.info("Candidate %s is elected", candidate)
            self._state.add_event(
                count.CountingEvent(count.CountingEventType.CANDIDATE_ELECTED,
                                    {'candidate': str(candidate.id)}))
            return
        # quota rules have to be enforced
        if self._is_max_quota_full(candidate, regular):
            logger.info(
                "Candidate %s is a member of a quota-group "
                "that reached its max. value", candidate)
            self._state.add_event(
                count.CountingEvent(
                    count.CountingEventType.MAX_QUOTA_VALUE_EXCLUDED,
                    {'candidate': str(candidate.id)}))
            return
        self._elected.append(candidate)
        self._state.all_elected_candidates = self._elected
        if not regular:
            self._elected_substitutes.append(candidate)
            self._state.all_elected_substitutes = self._elected_substitutes
        logger.info("Candidate %s is elected", candidate)
        self._state.add_event(
            count.CountingEvent(count.CountingEventType.CANDIDATE_ELECTED,
                                {'candidate': str(candidate.id)}))
        return
Пример #2
0
    def _update_quota_status(self):
        """
        Updates the quota status for candidates.

        Re-checks and if necessary disables quota-checks.
        """
        # this method *MUST* be rewritten if more than gender-quota
        # should be handled
        if self._quotas_disabled:
            return None
        empty_quota_group = False
        no_min_value = False
        for quota_group in self._counter_obj.quotas:
            if not quota_group.members:
                empty_quota_group = True
            if not quota_group.min_value:
                no_min_value = True
        # this is for event (protocol) purposes only
        if empty_quota_group:
            logger.info("At least one quota group is empty. "
                        "Removing quota-rules.")
            self._state.add_event(
                count.CountingEvent(count.CountingEventType.QUOTA_GROUP_EMPTY,
                                    {}))
            self._quotas_disabled = True
            # life is too short. we do not wait for other reasons.
            return None
        if no_min_value:
            logger.info("At least one quota-group has min_value == 0. "
                        "Removing quota-rules.")
            self._state.add_event(
                count.CountingEvent(
                    count.CountingEventType.QUOTA_MIN_VALUE_ZERO, {}))
            self._quotas_disabled = True
            return None
        if (len(self._counter_obj.candidates) <=
                self._counter_obj.election.num_choosable):
            logger.info("Candidates <= regular cendidates to "
                        "elect. Removing quota-rules.")
            self._state.add_event(
                count.CountingEvent(
                    count.CountingEventType.QUOTA_NOT_ENOUGH_CANDIDATES, {}))
            self._quotas_disabled = True
        return None  # please pylint
Пример #3
0
    def _elect_regulars(self, count_results):
        """
        Elects regular representatives from `count_results`

        :param count_results: The results of the count .most_common
        :type count_results: list
        """
        self._state.add_event(
            count.CountingEvent(count.CountingEventType.NEW_REGULAR_ROUND, {}))
        for vcount in count_results:
            if len(self._elected) >= len(self._counter_obj.candidates):
                logger.info("No more candidates left to elect")
                break
            candidate, _ = vcount
            try:
                self._elect_candidate(candidate, regular=True)
            except RequiredCandidatesElected:
                logger.info("All required regular candidates are elected")
                break
        self._state.add_event(
            count.CountingEvent(
                count.CountingEventType.TERMINATE_REGULAR_COUNT, {}))
Пример #4
0
    def _perform_count(self, count_results, count_result_stats):
        """
        Performs the logging / debugging part of the count

        :param count_results: The results of the count .most_common
        :type count_results: list

        :param count_result_stats: The result stats generated by the caller
        :type count_result_stats: dict
        """
        for vcount in count_results:
            candidate, candidate_count = vcount
            logger.info("Candidate %s: %s -> %s", candidate,
                        str(count_result_stats[candidate].items()),
                        candidate_count)
        self._state.add_event(
            count.CountingEvent(
                count.CountingEventType.NEW_COUNT, {
                    'count_results': count_results,
                    'count_result_stats_ntnu': count_result_stats
                }))
Пример #5
0
    def count(self):
        """
        Performs the actual count.

        This method will either return a final state or call itself on a newly
        instanciated object. (recurse until final state is returned)

        :return: A state (result) for this count
        :rtype: RoundState
        """
        logger.info("Starting the NTNU-CV count")
        count_result_stats = {}
        results = collections.Counter()

        # set initial results
        divident = decimal.Decimal(1)
        factors = tuple([
            decimal.Decimal(f)
            for f in range(1,
                           len(self._counter_obj.candidates) * 2, 2)
        ])
        for candidate in self._counter_obj.candidates:
            results[candidate] = decimal.Decimal(0)
            count_result_stats[candidate] = {int(f): 0 for f in factors}
        for ballot in self._counter_obj.ballots:
            if not ballot.candidates:
                # blank ballot
                continue
            for idx, candidate in enumerate(ballot.candidates):
                results[candidate] += divident / factors[idx]
                count_result_stats[candidate][int(factors[idx])] += 1
        count_results = results.most_common()
        self._perform_count(count_results, count_result_stats)
        # now see if two or more candidates have the same score
        scores = [r[1] for r in count_results]
        if len(set(scores)) < len(count_results):
            # at least one duplicate
            logger.info('At least two candidates have the same score. '
                        'Drawing lots')
            self._state.add_event(
                count.CountingEvent(count.CountingEventType.SAME_SCORE, {}))
            unpacked_results = {}
            # this works only with Python >= 3.6
            for vcount in count_results:
                candidate, candidate_count = vcount
                if candidate_count not in unpacked_results:
                    unpacked_results[candidate_count] = [candidate]
                else:
                    unpacked_results[candidate_count].append(candidate)
            count_results = []  # overwrite
            for count_result, candidates in unpacked_results.items():
                if len(candidates) > 1:
                    self._counter_obj.shuffle_candidates(candidates)
                for candidate in candidates:
                    count_results.append((candidate, count_result))
            # new event, debug...
            self._perform_count(count_results, count_result_stats)
        self._elect_regulars(count_results)
        # reset self._quotas_disabled
        self._quotas_disabled = not bool(self._counter_obj.quotas)
        self._update_quota_values()
        self._elect_substitutes(count_results)
        self._state.final = True
        return self._state
Пример #6
0
    def _update_quota_values(self):
        """
        Updates the quota values for substitute candidates.

        Re-checks and if necessary updates `min_value_substitutes` and
        `max_value_substitutes` for the defined quota-groups (if any).

        This method should only be called before the start of the
        substitute count.
        """
        # this method *MUST* be rewritten if more than gender-quota
        # should be handled
        if self._quotas_disabled:
            return None
        # adjust the min_value_substitutes for quotas, in case not enough
        # candidates
        quota_unelected = {}
        empty_quota_group = False
        for quota_group in self._counter_obj.quotas:
            quota_unelected[quota_group] = [
                str(cand.id) for cand in tuple(
                    set(quota_group.members).difference(set(self._elected)))
            ]
            unelected = len(quota_unelected[quota_group])
            if not unelected:
                # reevaluate this statement in the future if more than 2
                # groups can be used! (not only gender - quota)
                empty_quota_group = True
            if (quota_group.min_value_substitutes
                    and unelected < quota_group.min_value_substitutes):
                logger.info(
                    "Amount unelected members (%d) in "
                    "quota-group %s < than current "
                    "min_value_substitutes %d. Adjusting.", unelected,
                    quota_group.name, quota_group.min_value_substitutes)
                self._state.add_event(
                    count.CountingEvent(
                        count.CountingEventType.QUOTA_MIN_VALUE_SUB_ADJUSTED, {
                            'quota_group_name': quota_group.name,
                            'current_value': quota_group.min_value_substitutes,
                            'new_value': unelected
                        }))
                quota_group.min_value_substitutes = unelected
        # now handle the special case of 1 regular and 1 substitute to elect.
        # this is handled differently here than in uiostv
        # USN election rules: §17.3 - B
        if (self._counter_obj.election.num_choosable == 1
                and self._counter_obj.election.num_substitutes == 1
                and len(self._elected) == 1  # paranoia
            ):
            regular_representative = self._elected[0]
            represented_group = self._get_candidate_quota_groups(
                regular_representative)[0]
            the_other_group = tuple(
                set(self._counter_obj.quotas).difference(
                    set((represented_group, ))))[0]
            if not quota_unelected[the_other_group]:
                empty_quota_group = True
            elif the_other_group.min_value_substitutes != 1:
                self._state.add_event(
                    count.CountingEvent(
                        count.CountingEventType.QUOTA_MIN_VALUE_SUB_ADJUSTED, {
                            'quota_group_name': the_other_group.name,
                            'current_value':
                            the_other_group.min_value_substitutes,
                            'new_value': 1
                        }))
                the_other_group.min_value_substitutes = 1
            if represented_group.min_value_substitutes:
                self._state.add_event(
                    count.CountingEvent(
                        count.CountingEventType.QUOTA_MIN_VALUE_SUB_ADJUSTED, {
                            'quota_group_name': represented_group.name,
                            'current_value':
                            represented_group.min_value_substitutes,
                            'new_value': 0
                        }))
                represented_group.min_value_substitutes = 0
        # once min-values are correct, fetch the max-values and create an event
        quotas = []
        unelected_candidates = 0
        for quota_group, unelected_members in quota_unelected.items():
            max_val = self._counter_obj.max_substitutes(quota_group)
            len_unelected_members = len(unelected_members)
            logger.info(
                "Quota-group %s: min_value_substitutes: %d, "
                "max_value_substitutes: %d, %d unelected members",
                quota_group.name, quota_group.min_value_substitutes, max_val,
                len_unelected_members)
            quotas.append({
                'name': quota_group.name,
                'min_value_substitutes': quota_group.min_value_substitutes,
                'max_value_substitutes': max_val,
                'unelected_members': unelected_members
            })
            unelected_candidates += len_unelected_members
        self._state.add_event(
            count.CountingEvent(count.CountingEventType.QUOTA_SUB_UPDATED,
                                {'quotas': quotas}))
        if empty_quota_group:
            logger.info("At least one quota group is now empty. "
                        "Removing quota-rules.")
            self._state.add_event(
                count.CountingEvent(
                    count.CountingEventType.QUOTA_SUB_GROUP_EMPTY, {}))
            self._quotas_disabled = True
            return None
        if (unelected_candidates <=
                self._counter_obj.election.num_substitutes):
            logger.info("Unelected candidates <= substitute cendidates to "
                        "elect. Removing quota-rules.")
            self._state.add_event(
                count.CountingEvent(
                    count.CountingEventType.QUOTA_SUB_NOT_ENOUGH_CANDIDATES,
                    {}))
            self._quotas_disabled = True
        return None  # please pylint
Пример #7
0
    def count(self):
        """
        Performs the actual count.

        This method will either return a final state or call itself on a newly
        instanciated object. (recurse until final state is returned)

        :return: A state (result) for this count
        :rtype: RoundState
        """
        logger.info("Starting the MV count")
        ballot_weights = collections.Counter()  # ballot: weight - dict
        candidate_ballots = {}
        count_result_stats = {}
        results = collections.Counter()
        total_score = decimal.Decimal(0)
        elected_candidate = None

        # generate per pollbook stats
        for pollbook in self._counter_obj.election.pollbooks:
            count_result_stats[pollbook] = {}
            count_result_stats[pollbook]['total'] = decimal.Decimal(0)
            for candidate in self._counter_obj.candidates:
                candidate_ballots[candidate] = list()
                count_result_stats[pollbook][candidate] = {}
                count_result_stats[pollbook][candidate]['total'] = (
                    decimal.Decimal(0))
                count_result_stats[pollbook][candidate]['amount'] = 0
        for ballot in self._counter_obj.ballots:
            if not ballot.candidates:
                # blank ballot
                continue
            candidate_ballots[ballot.candidates[0]].append(ballot)
            ballot_weights[ballot] = ballot.pollbook.weight_per_pollbook
            total_score += ballot.pollbook.weight_per_pollbook
        for candidate, ballots in candidate_ballots.items():
            results[candidate] = decimal.Decimal(0)
            for ballot in ballots:
                results[candidate] += ballot_weights[ballot]
                count_result_stats[ballot.pollbook][candidate][
                    'total'] += ballot_weights[ballot]
                count_result_stats[ballot.pollbook][candidate]['amount'] += 1
                count_result_stats[
                    ballot.pollbook]['total'] += ballot_weights[ballot]
        # set % of total pollbook score - stats
        for pollbook in self._counter_obj.election.pollbooks:
            logger.info("Pollbook %s has a total score: %s", pollbook.name,
                        count_result_stats[pollbook]['total'])
            for candidate in count_result_stats[pollbook]:
                if candidate == 'total':
                    continue
                if not count_result_stats[pollbook][candidate]['total']:
                    # avoid division by 0 and optimize...
                    # here the divisor can not be 0 if divident is not zero
                    count_result_stats[pollbook][candidate][
                        'percent_pollbook'] = decimal.Decimal(0)
                    logger.info(
                        "Candidate %s has a score of 0 in that pollbook",
                        candidate)
                    continue
                count_result_stats[pollbook][candidate]['percent_pollbook'] = (
                    (decimal.Decimal(100) *
                     count_result_stats[pollbook][candidate]['total']) /
                    count_result_stats[pollbook]['total']).quantize(
                        decimal.Decimal('1.00'), decimal.ROUND_HALF_EVEN)
                logger.info(
                    "Candidate %s has a score of %s (%s%%) in that pollbook",
                    candidate,
                    count_result_stats[pollbook][candidate]['total'],
                    count_result_stats[pollbook][candidate]
                    ['percent_pollbook'])
        count_results = results.most_common()
        total_stats = {}
        for vcount in count_results:
            # debugging mostly
            candidate, candidate_count = vcount
            total_stats[str(candidate.id)] = {}
            if total_score:
                total_stats[str(candidate.id)]['percent_score'] = str(
                    (decimal.Decimal(100) * candidate_count /
                     total_score).quantize(decimal.Decimal('1.00'),
                                           decimal.ROUND_HALF_EVEN))
            else:
                total_stats[str(candidate.id)]['percent_score'] = '0'
            if candidate_count > total_score / decimal.Decimal(2):
                logger.info(
                    "Candidate %s: %s (has more than 1/2 of the total "
                    "score and will be elected)", candidate, candidate_count)
                elected_candidate = candidate
                continue
            logger.info("Candidate %s: %s", candidate, candidate_count)
        logger.info("Total score: %s", total_score)
        logger.info("Half score: %s", total_score / decimal.Decimal(2))
        self._state.add_event(
            count.CountingEvent(
                count.CountingEventType.NEW_COUNT, {
                    'count_results': count_results,
                    'count_result_stats': count_result_stats,
                    'total_stats': total_stats,
                    'half_score': str(total_score / decimal.Decimal(2)),
                    'total_score': str(total_score)
                }))
        if elected_candidate is None:
            # drawing
            logger.info(
                "None of the above candidates reached 1/2 of the total score. "
                "Drawing candidate to elect")
            elected_candidate = self._counter_obj.draw_candidate(
                [count_results[0][0], count_results[1][0]])
            self._state.add_event(
                count.CountingEvent(count.CountingEventType.DRAW_SELECT,
                                    {'candidate': str(elected_candidate.id)}))
        self._elected.append(elected_candidate)
        self._state.add_elected_candidate(elected_candidate)
        self._state.all_elected_candidates = self._elected
        logger.info("Candidate %s is elected", elected_candidate)
        self._state.add_event(
            count.CountingEvent(count.CountingEventType.CANDIDATE_ELECTED,
                                {'candidate': str(elected_candidate.id)}))
        self._state.final = True
        return self._state