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
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
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, {}))
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 }))
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
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
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