diff --git a/README.rst b/README.rst
index c542a8b..bf830df 100644
--- a/README.rst
+++ b/README.rst
@@ -25,18 +25,25 @@ Methods implemented
* Plurality (aka first-past-the-post or fptp)
* Instant-Runoff Voting (aka IRV)
* Schulze Method (aka Beatpath)
+ * Borda
* Multiple Winner Methods
* Plurality at large (aka block voting)
* Single Transferable Vote (aka STV)
* Schulze STV
+ * Borda at large
* Ordering Methods
* Schulze Proportional Representation
* Schulze Nonproportional Representation
+* Manipulation Algorithms
+
+ * Average Fit (coalition manipulation heurestic for Borda votting rule)
+ * Largest Fit (coalition manipulation heurestic for Borda votting rule)
+
Basic Usage
-----------
diff --git a/py3votecore/borda.py b/py3votecore/borda.py
new file mode 100644
index 0000000..395db34
--- /dev/null
+++ b/py3votecore/borda.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2023, Leora Schmerler
+#
+# This program 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.
+#
+# 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from .abstract_classes import AbstractSingleWinnerVotingSystem
+from .borda_at_large import BordaAtLarge
+
+class Borda(AbstractSingleWinnerVotingSystem):
+
+ def __init__(self, ballots, tie_breaker=None):
+ """
+ The constructer accepts ballots of voters and a tie breaker if given.
+ >>> Borda([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}]) # doctest:+ELLIPSIS
+ <...>
+ >>> Borda([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], tie_breaker=["A", "B", "C", "D"]) # doctest:+ELLIPSIS
+ <...>
+ """
+ super(Borda, self).__init__(ballots, BordaAtLarge, tie_breaker=tie_breaker)
+
+
+if __name__ == '__main__':
+ import doctest
+ (failures, tests) = doctest.testmod(report=True)
+ print("{} failures, {} tests".format(failures, tests))
+
diff --git a/py3votecore/borda_at_large.py b/py3votecore/borda_at_large.py
new file mode 100644
index 0000000..f82d6d3
--- /dev/null
+++ b/py3votecore/borda_at_large.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2023, Leora Schmerler
+#
+# This program 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.
+#
+# 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from .abstract_classes import MultipleWinnerVotingSystem
+from .common_functions import matching_keys, unique_permutations
+import copy
+
+class BordaAtLarge(MultipleWinnerVotingSystem):
+
+ def __init__(self, ballots: list, tie_breaker=None, required_winners: int=1):
+ """
+ The constructer accepts ballots of voters, a tie breaker if given and the number of required winners.
+ >>> BordaAtLarge([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], required_winners = 2) # doctest:+ELLIPSIS
+ <...>
+ >>> BordaAtLarge([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], tie_breaker=["A", "B", "C", "D"] ,required_winners = 2) # doctest:+ELLIPSIS
+ <...>
+ """
+ super(BordaAtLarge, self).__init__(ballots, tie_breaker=tie_breaker, required_winners=required_winners)
+
+ def calculate_results(self):
+ """
+ calculate_results accepts an instance of Borda, and is called from the constructor.
+ >>> BordaAtLarge([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], tie_breaker=["A", "B", "C", "D"] ,required_winners = 2).calculate_results() # doctest:+ELLIPSIS
+ ...
+ >>> BordaAtLarge([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], required_winners = 2).calculate_results() # doctest:+ELLIPSIS
+ ...
+ """
+
+ # Standardize the ballot format and extract the candidates
+ self.candidates = set()
+ for ballot in self.ballots:
+
+ # Convert a single candidate ballots into ballot lists
+ if not isinstance(ballot["ballot"], list):
+ ballot["ballot"] = [ballot["ballot"]]
+
+ # Ensure no ballot has an excess of votes
+ if len(ballot["ballot"]) < self.required_winners:
+ raise Exception("A ballot contained too many candidates")
+
+ # Add all candidates on the ballot to the set
+ self.candidates.update(set(ballot["ballot"]))
+
+ # Sum up all votes for each candidate
+ self.tallies = dict.fromkeys(self.candidates, 0)
+ for ballot in self.ballots:
+ for i in range(len(ballot["ballot"])):
+ self.tallies[ballot["ballot"][i]] += (ballot["count"]*(len(self.candidates)-1-i))
+ tallies = copy.deepcopy(self.tallies)
+
+ # Determine which candidates win
+ winning_candidates = set()
+ while len(winning_candidates) < self.required_winners:
+
+ # Find the remaining candidates with the most votes
+ largest_tally = max(tallies.values())
+ top_candidates = matching_keys(tallies, largest_tally)
+
+ # Reduce the found candidates if there are too many
+ if len(top_candidates | winning_candidates) > self.required_winners:
+ self.tied_winners = top_candidates.copy()
+ while len(top_candidates | winning_candidates) > self.required_winners:
+ top_candidates.remove(self.break_ties(top_candidates, True))
+
+ # Move the top candidates into the winning pile
+ winning_candidates |= top_candidates
+ for candidate in top_candidates:
+ del tallies[candidate]
+
+ self.winners = winning_candidates
+
+ def as_dict(self):
+ """
+ as_dict accepts an instance of Borda, and returns a dict of the BordaAtLarge.
+ >>> BordaAtLarge([{ "count":3, "ballot":["A", "B", "C", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], tie_breaker=["A", "B", "C", "D"] ,required_winners = 2).as_dict() # doctest:+ELLIPSIS
+ {...}
+ >>> BordaAtLarge([{ "count":3, "ballot":["A", "C", "B", "D"]},{ "count":2, "ballot":["D", "B", "A", "C"]},{ "count":2, "ballot":["D", "B", "C", "A"]}], required_winners = 2).as_dict() # doctest:+ELLIPSIS
+ {...}
+ """
+ data = super(BordaAtLarge, self).as_dict()
+ data["tallies"] = self.tallies
+ return data
+
+if __name__ == '__main__':
+ import doctest
+ (failures, tests) = doctest.testmod(report=True)
+ print("{} failures, {} tests".format(failures, tests))
+
diff --git a/py3votecore/borda_manipulation_heurestics_methods.py b/py3votecore/borda_manipulation_heurestics_methods.py
new file mode 100644
index 0000000..be40373
--- /dev/null
+++ b/py3votecore/borda_manipulation_heurestics_methods.py
@@ -0,0 +1,253 @@
+# Copyright (C) 2023, Leora Schmerler
+#
+# This program 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.
+#
+# 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from .borda import Borda
+import networkx as nx
+
+
+def AverageFit(ballots: list, preferred_candidate: str, k: int)->list or bool:
+ """
+ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011),
+ https://ojs.aaai.org/index.php/AAAI/article/view/7873
+
+ AverageFit: accepts ballots of voters, a number of manipulators, k, that try to manipulate their vote in order that thier preferred
+ candidate will be elected by the Borda voting rule, tie-breaks in faver of the preferred_candidate. The algorithm outputs true if it succeeds
+ to find such manipulation, and false otherwise.
+
+ Programmer: Leora Schmerler
+
+ >>> AverageFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2) # doctest:+ELLIPSIS
+ [...]
+ >>> AverageFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE
+ [['d', 'a', 'c', 'b'], ['d', 'a', 'c', 'b']]
+ >>> AverageFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2) # doctest:+NORMALIZE_WHITESPACE
+ [['d', 'c', 'a', 'b'], ['d', 'c', 'a', 'b']]
+
+ >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5) # doctest:+ELLIPSIS
+ [...]
+ >>> AverageFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4)
+ False
+ """
+ map_candidates_times_placed = {c:0 for c in ballots[0]["ballot"]} # Dictionary of candidates and amount of times the manipulators have placed each candidate.
+ m = len(map_candidates_times_placed) # Number of candidates.
+ manipulators = [["" for j in range(m)] for i in range(k)] # Creating the manipulators preference profile.
+ map_scores_to_give = {i:k for i in range(m-1)} # Dictionary of the scores the manipulators can give.
+ current_scores = Borda(ballots).as_dict()["tallies"]
+ current_scores[preferred_candidate] += (k*(m-1))
+ map_candidates_times_placed[preferred_candidate] = k
+ for manipulator in manipulators:
+ manipulator[0] = preferred_candidate
+ average_gap = create_gap_dic(current_scores, preferred_candidate, k) # Dictionary of the average score gap of the candidate that the manipulators want to win to each other candidate.
+ highest_score_to_give = m-2
+ if average_gap == False: # If average_gap is False, then there exists a candidate c, that has a higher score than candidate the manipulators, and therefore, the algorithm returns False.
+ return False
+ for i in range(k*(m - 1)): # Number of scores to give to the candidates.
+ current_c = max(average_gap, key = lambda a: (average_gap[a], map_candidates_times_placed[a]))
+ current_score_to_give = find_possible_score(current_scores, map_scores_to_give, preferred_candidate, current_c, highest_score_to_give)
+ if current_score_to_give == -1: # If there is no available score that is possible to give c, AverageFit returns False.
+ return False
+ current_scores[current_c] += current_score_to_give
+ map_candidates_times_placed[current_c] += 1
+ average_gap[current_c] = (current_scores[preferred_candidate] - current_scores[current_c])/(k - map_candidates_times_placed[current_c]) if map_candidates_times_placed[current_c] != k else 0
+ map_scores_to_give[current_score_to_give] -= 1
+ manipulators[k-1-map_scores_to_give[current_score_to_give]][m-1-current_score_to_give] = current_c
+ highest_score_to_give = update_highest_score_to_give(map_scores_to_give, highest_score_to_give)
+ if map_candidates_times_placed[current_c]==k:
+ del average_gap[current_c]
+ if is_legal(manipulators, map_candidates_times_placed):
+ return manipulators
+ else:
+ return legal_manipulation(manipulators, map_candidates_times_placed.keys())
+
+
+def LargestFit(ballots: list, preferred_candidate: str, k: int)->list or bool:
+ """
+ "Complexity of and Algorithms for Borda Manipulation", by Jessica Davies, George Katsirelos, Nina Narodytska and Toby Walsh(2011),
+ https://ojs.aaai.org/index.php/AAAI/article/view/7873
+
+ LargestFit: accepts ballots of voters, a number of manipulators, k, that try to manipulate their vote in order that thier preferred
+ candidate will be elected by the Borda voting rule, tie-breaks in faver of the preferred_candidate. The algorithm outputs true if it succeeds
+ to find such manipulation, and false otherwise.
+
+ Programmer: Leora Schmerler
+
+ >>> LargestFit([{"count": 1, "ballot": ["c","a","b","d"]}, {"count": 1, "ballot": ["b","c","a","d"]}], "d", 2) # doctest:+ELLIPSIS
+ [...]
+ >>> LargestFit([{"count": 1, "ballot": ["c","b","d","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}], "d", 2) # doctest:+ELLIPSIS
+ [...]
+ >>> LargestFit([{"count": 1, "ballot": ["b","d","c","a"]}, {"count": 1, "ballot": ["b","c","a","d"]}, {"count": 1, "ballot": ["b","a","d","c"]}, {"count": 1, "ballot": ["a","c","d","b"]}], "d", 2) # doctest:+ELLIPSIS
+ [...]
+
+ >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 5) # doctest:+ELLIPSIS
+ [...]
+ >>> LargestFit([{"count": 1, "ballot": ["a","b","c","d","e","f","g","h"]}, {"count": 1, "ballot": ["a","b","c","e","d","f","g","h"]}, {"count": 1, "ballot": ["a","b","e","c","f","g","h","d"]}, {"count": 1, "ballot": ["a","h","c","e","d","g","f","b"]}, {"count": 1, "ballot": ["g","b","c","f","e","h","d","a"]}, {"count": 1, "ballot": ["f","a","c","b","e","h","g","d"]}, {"count": 1, "ballot": ["h","g","a","f","d","e","b","c"]}, {"count": 1, "ballot": ["h","g","b","f","e","a","c","d"]}], "d", 4) # doctest:+ELLIPSIS
+ [...]
+
+ """
+
+ map_candidates_times_placed = {c:0 for c in ballots[0]["ballot"]} # Dictionary of candidates and amount of times the manipulators have placed each candidate.
+ m = len(map_candidates_times_placed) # Number of candidates.
+ manipulators = [["" for j in range(m)] for i in range(k)] # Creating the manipulators preference profile.
+ current_scores = Borda(ballots).as_dict()["tallies"]
+ current_scores[preferred_candidate] += (k*(m-1))
+ map_candidates_times_placed[preferred_candidate] = k
+ for manipulator in manipulators:
+ manipulator[0] = preferred_candidate
+ gap = create_gap_dic(current_scores, preferred_candidate, 1) # Dictionary of the score gap of the candidate that the manipulators want to win to each other candidate.
+ if gap == False: # If gap is False, then there exists a candidate c, that has a higher score than candidate the manipulators, and therefore, the algorithm returns False.
+ return False
+ for i in range(m - 2, -1, -1): # The score given in the current iteration.
+ for j in range(k): # The relaxed manipulator giving that score.
+ current_c = max(gap, key = lambda a: (gap[a], map_candidates_times_placed[a]))
+ if gap[current_c] - i < 0: # If we add to current_c the score of i and it overtakes the score of candidate, LargestFit return False.
+ return False
+ manipulators[j][m-1-i] = current_c # LargestFit places current_c in the manipulators, gives current_c i points, and updates the number of times it was placed, the gap and the score.
+ gap[current_c] -= i
+ current_scores[current_c] += i
+ map_candidates_times_placed[current_c] += 1
+ if map_candidates_times_placed[current_c]==k:
+ del gap[current_c]
+ if not is_legal(manipulators, map_candidates_times_placed):
+ return legal_manipulation(manipulators, map_candidates_times_placed.keys())
+ return manipulators
+
+
+
+def find_possible_score(current_scores: dict, map_scores_to_give: dict, preferred_candidate: str, current_c: str, highest_score_to_give: int)->int:
+ """
+ find_possible_score: accepts a dict of the current scores from ballots of voters and part of the manipulators preference profile, a dict of scores still posiible to give,
+ a string of the candidate the manipulators want her to win, a string c of a candidate the manipulators want to give the next Borda score and an integer of the highest
+ posiible score to give some candidate.
+ The function outputs the score posiible to give to candidate c, such that the score candidate has will be higher or grater than c's score. If no such score exists, returns false.
+ >>> find_possible_score({"C": 11, "Carle": 10, "Barak": 7, "Diana": 5, "Rachel":10}, {0: 4, 1: 4, 2:4, 3: 4, 4: 4}, "C", "Diana", 4)
+ 4
+ >>> find_possible_score({"C": 91, "Carle": 80, "Barak": 92, "Diana": 77, "Rachel":88}, {0: 4, 1: 4, 2: 4, 3: 0, 4: 1}, "Barak", "C", 4)
+ 1
+ >>> find_possible_score({"A": 91, "B": 80, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, {0: 0, 1: 0, 2: 1, 3: 0, 4: 1}, "H", "C", 4)
+ -1
+ >>> find_possible_score({'b': 6, 'c': 6, 'd': 6, 'a': 6}, {0: 2, 1: 0, 2: 0}, 'd', 'b', 0)
+ 0
+ """
+ current_score_to_give = highest_score_to_give
+ if current_scores[preferred_candidate] <= current_score_to_give + current_scores[current_c]:
+ current_score_to_give = current_scores[preferred_candidate] - current_scores[current_c] # The highest score c can get, since it is smaller that the highest avialable one.
+ if map_scores_to_give[current_score_to_give] == 0:
+ for k in range(current_score_to_give, -1, -1):
+ if map_scores_to_give[k] != 0:
+ return k
+ return -1
+ return current_score_to_give
+
+def update_highest_score_to_give(map_scores_to_give :dict, highest_score_to_give: int)->int:
+ """
+ update_highest_score_to_give: accepts a dict of scores still posiible to give and an integer that is the highest possible Borda score to give.
+ The function outputs the next highest score possible to give. If the highest possible score is not possible anymore, that is, it was currently
+ given, the function searches for a lower integer that can be given next, from the next highest to 0, and returns it. If all scores were given
+ or the highest possible Borda score stays the same, then the function returns the highest possible Borda score.
+ >>> update_highest_score_to_give({0: 4, 1: 4, 2:4, 3: 4, 4: 4}, 4)
+ 4
+ >>> update_highest_score_to_give({0: 4, 1: 4, 2: 4, 3: 0, 4: 0}, 4)
+ 2
+ >>> update_highest_score_to_give({0: 0, 1: 0, 2: 0, 3: 0, 4: 0}, 4)
+ 4
+ >>> update_highest_score_to_give({0: 2, 1: 0, 2: 0}, 1)
+ 0
+ """
+ if map_scores_to_give[highest_score_to_give] == 0: # If giving a candidate the highest possible score and therefore that score is not possible anymore.
+ for k in range(highest_score_to_give - 1, -1, -1):
+ if map_scores_to_give[k] != 0:
+ return k
+ return highest_score_to_give
+
+def create_gap_dic(current_scores: dict, preferred_candidate: str, k: int)->dict:
+ """
+ create_gap_dic: accepts a dict of the current scores from ballots of voters and part of the manipulators preference profile and the candidate
+ the manipulators want her to win. The function constructs and outputs a dict of gaps of each candidate c, to the candidate the manipulators
+ want to win. When the algorithm that calls this function is LargestFit, then the gap is the difference between the scores. Otherwise, the gap in AverageFit,
+ is an average gap.
+ >>> create_gap_dic({"A": 91, "B": 80, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "H", 1) #doctest: +NORMALIZE_WHITESPACE
+ {'A': 8.0, 'B': 19.0, 'C': 1.0, 'D': 22.0, 'E': 11.0, 'F': 12.0, 'G': 10.0}
+ >>> create_gap_dic({"A": 100, "B": 80, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "A", 2) #doctest: +NORMALIZE_WHITESPACE
+ {'B': 10.0, 'C': 1.0, 'D': 11.5, 'E': 6.0, 'F': 6.5, 'G': 5.5, 'H': 0.5}
+ >>> create_gap_dic({"A": 91, "B": 102, "C": 98, "D": 77, "E": 88, "F": 87, "G": 89, "H": 99}, "B", 5) #doctest: +NORMALIZE_WHITESPACE
+ {'A': 2.2, 'C': 0.8, 'D': 5.0, 'E': 2.8, 'F': 3.0, 'G': 2.6, 'H': 0.6}
+ """
+ gap = {}
+ for c in current_scores.keys():
+ if c != preferred_candidate:
+ gap[c] = (current_scores[preferred_candidate] - current_scores[c])/ k # Creates the average gap for each candidate c. When LargestFit calls this function, k=1.
+ if gap[c] < 0:
+ return False
+ return gap
+
+def is_legal(manipulators: list, map_candidates_times_placed: dict)->bool:
+ """
+ is_legal: accepts a list of manipulators, and their preference order list and a dict of candidates, where each candidate was placed k times
+ The function checks if this manipulation is legal, that is, if there exists a manipulator that places a candidate more than one (and therefore,
+ doesn't place at least one candidate in her preference order).
+ >>> is_legal([["A","B","C","D"],["A","B","C","D"],["A","B","C","D"]], {"A":3, "B":3, "C":3, "D":3})
+ True
+ >>> is_legal([["C","D","B","A"],["A","D","C","B"],["D","A","C","B"]], {"A":3, "B":3, "C":3, "D":3})
+ True
+ >>> is_legal([["C","D","B","A"],["A","D","C","B"],["D","A","C","B"],["D","A","A","B"],["D","C","C","B"]], {"A":5, "B":5, "C":5, "D":5})
+ False
+ >>> is_legal([['d', 'h', 'g', 'c', 'e', 'f', 'b', 'a'], ['d', 'h', 'g', 'c', 'e', 'f', 'b', 'a'], ['d', 'g', 'f', 'e', 'c', 'h', 'b', 'a'], ['d', 'e', 'h', 'f', 'c', 'g', 'b', 'a'], ['d', 'f', 'e', 'c', 'h', 'g', 'b', 'a']], {"a":5, "b":5, "c":5, "d":5, "e":5, "f":5, "g":5, "h":5})
+ True
+ """
+ for i in range(len(manipulators)-1, -1, -1):
+ for c in manipulators[i]:
+ map_candidates_times_placed[c] = map_candidates_times_placed[c] - 1
+ if map_candidates_times_placed[c] != i:
+ return False
+ return True
+
+
+def legal_manipulation(manipulators: list, candidates: set)->list:
+ """
+ legal_manipulation: accepts a list of manipulators, and their preference order list that is not a legal manipulation and a set of candidates.
+ The function return a leagl manipulation.
+ >>> legal_manipulation([["C","D","B","A"],["A","D","C","B"],["D","A","C","B"],["D","A","A","B"],["D","C","C","B"]], {"A", "B", "C", "D"})# doctest:+ELLIPSIS
+ [...]
+ >>> legal_manipulation([["C","D","B","E","A"],["A","E","D","C","B"],["D","A","E","E","B"],["D","E","A","A","B"],["D","C","C","C","B"]], {"A", "B", "C", "D", "E"})# doctest:+ELLIPSIS
+ [...]
+ """
+ G = nx.Graph()
+ G.add_nodes_from([j for j in range(len(manipulators))], bipartite=0)
+ G.add_nodes_from(candidates, bipartite=1)
+ for i in range(len(manipulators)):
+ for j in range(len(manipulators[i])):
+ # Each edge's whight is for the amount of time a candidate recieves j points.
+ if (j, manipulators[i][j]) in G.edges:
+ G.edges[j, manipulators[i][j]]["weight"] += 1
+ else:
+ G.add_edge(j, manipulators[i][j], weight=1)
+ match = nx.bipartite.maximum_matching(G, top_nodes=candidates) # Each match is for each manipulaotr's placing.
+ for i in range(len(manipulators)):
+ for j in range(len(manipulators[i])):
+ manipulators[i][j] = match[j]
+ G.edges[j, match[j]]["weight"] -= 1 # Removing weight for each match placed.
+ if G.edges[j, match[j]]["weight"] == 0:
+ G.remove_edge(j, match[j])
+ match = nx.bipartite.maximum_matching(G, top_nodes=candidates) # Preparing the next manipulators placing.
+ return manipulators
+
+if __name__ == '__main__':
+ import doctest
+ (failures, tests) = doctest.testmod(report=True)
+ print("{} failures, {} tests".format(failures, tests))
+
+
+
+
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..21a9cd7
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+networkx >= 3.0
+python-graph-core >= 1.8.2
diff --git a/setup.py b/setup.py
index d37c2ed..31e6f1c 100644
--- a/setup.py
+++ b/setup.py
@@ -9,10 +9,11 @@
requires = [
'python-graph-core >= 1.8.0',
+ 'networkx >= 3.0',
]
setup(name='python3-vote-core',
- version='20170329.00',
+ version='20230116.00',
description="An implementation of various election methods, most notably the Schulze Method and Schulze STV. -- Python 3 Only",
long_description=README + '\n\n' + CHANGES + '\n\n' + LICENSE,
classifiers=[
diff --git a/test_functionality/test_borda.py b/test_functionality/test_borda.py
new file mode 100644
index 0000000..2dade03
--- /dev/null
+++ b/test_functionality/test_borda.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2023, Leora Schmerler
+#
+# This program 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.
+#
+# 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from py3votecore.borda import Borda
+import unittest
+
+
+class TestBorda(unittest.TestCase):
+
+ # Borda, no ties
+ def test_no_ties(self):
+
+ # Generate data
+ input = [
+ {"count": 26, "ballot": ["c1","c2","c3"]},
+ {"count": 22, "ballot": ["c2","c1","c3"]},
+ {"count": 23, "ballot": ["c3","c2","c1"]}
+ ]
+ output = Borda(input).as_dict()
+
+ # Run tests
+ self.assertEqual(output, {
+ 'candidates': set(['c1', 'c2', 'c3']),
+ 'tallies': {'c3': 46, 'c2': 93, 'c1': 74},
+ 'winner': 'c2'
+ })
+
+ # Borda, irrelevant ties
+ def test_irrelevant_ties(self):
+
+ # Generate data
+ input = [
+ {"count": 26, "ballot": ["c1", "c2", "c3"]},
+ {"count": 23, "ballot": ["c2", "c1", "c3"]},
+ {"count": 72, "ballot": ["c1", "c3", "c2"]}
+ ]
+ output = Borda(input).as_dict()
+
+ # Run tests
+ self.assertEqual(output, {
+ 'candidates': set(['c1', 'c2', 'c3']),
+ 'tallies': {'c3': 72, 'c2': 72, 'c1': 219},
+ 'winner': 'c1'
+ })
+
+ # Borda, relevant ties
+ def test_relevant_ties(self):
+
+ # Generate data
+ input = [
+ {"count": 49, "ballot": ["c1", "c2", "c3"]},
+ {"count": 26, "ballot": ["c2", "c1", "c3"]},
+ {"count": 23, "ballot": ["c3", "c2", "c1"]}
+ ]
+ output = Borda(input).as_dict()
+
+ # Run tests
+ self.assertEqual(output["tallies"], {'c1': 124, 'c2': 124, 'c3': 46})
+ self.assertEqual(output["tied_winners"], set(['c1', 'c2']))
+ self.assertTrue(output["winner"] in output["tied_winners"])
+ self.assertEqual(len(output["tie_breaker"]), 3)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test_functionality/test_borda_at_large.py b/test_functionality/test_borda_at_large.py
new file mode 100644
index 0000000..7ff83ce
--- /dev/null
+++ b/test_functionality/test_borda_at_large.py
@@ -0,0 +1,102 @@
+# Copyright (C) 2023, Leora Schmerler
+#
+# This program 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.
+#
+# 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from py3votecore.borda_at_large import BordaAtLarge
+import unittest
+
+
+class TestBordaAtLarge(unittest.TestCase):
+
+ # Borda at Large, no ties
+ def test_borda_at_large_no_ties(self):
+
+ # Generate data
+ output = BordaAtLarge([
+ {"count": 26, "ballot": ["c1", "c2", "c3"]},
+ {"count": 22, "ballot": ["c1", "c3", "c2"]},
+ {"count": 23, "ballot": ["c2", "c3", "c1"]}
+ ], required_winners=2).as_dict()
+
+ # Run tests
+ self.assertEqual(output, {
+ 'candidates': set(['c1', 'c2', 'c3']),
+ 'tallies': {'c3': 45, 'c2': 72, 'c1': 96},
+ 'winners': set(['c2', 'c1'])
+ })
+
+ # Borda at Large, irrelevant ties
+ def test_borda_at_large_irrelevant_ties_top(self):
+
+ # Generate data
+ output = BordaAtLarge([
+ {"count": 16, "ballot": ["c1", "c2", "c3", "c4", "c5"]},
+ {"count": 25, "ballot": ["c1", "c3", "c2", "c4", "c5"]},
+ {"count": 22, "ballot": ["c2", "c3", "c1", "c5", "c4"]},
+ {"count": 22, "ballot": ["c4", "c5", "c2", "c1", "c3"]}
+ ], required_winners=2).as_dict()
+
+ # Run tests
+ self.assertEqual(output, {
+ 'candidates': set(['c1', 'c2', 'c3', 'c4', 'c5']),
+ 'tallies': {'c3': 173, 'c2': 230, 'c1': 230, 'c5': 88, 'c4': 129},
+ 'winners': set(['c2', 'c1'])
+ })
+
+
+ # Borda at Large, irrelevant ties
+ def test_borda_at_large_irrelevant_ties_low(self):
+
+ # Generate data
+ output = BordaAtLarge([
+ {"count": 30, "ballot": ["c4", "c1", "c2", "c3"]},
+ {"count": 22, "ballot": ["c3", "c2", "c1", "c4"]},
+ {"count": 22, "ballot": ["c1", "c4", "c3", "c2"]},
+ {"count": 4, "ballot": ["c4", "c2", "c1", "c3"]},
+ {"count": 8, "ballot": ["c2", "c3", "c1", "c4"]},
+ {"count": 2, "ballot": ["c3", "c2", "c4", "c1"]}
+ ], required_winners=2).as_dict()
+
+ print(output)
+ # Run tests
+ self.assertEqual(output["tallies"], {'c3': 110, 'c2': 110, 'c1': 160, 'c4': 148})
+ self.assertEqual(output['winners'], set(['c4', 'c1']))
+ self.assertEqual(len(output), 3)
+
+ # Borda at Large, relevant ties
+ def test_borda_at_large_relevant_ties(self):
+
+
+ # Generate data
+ output = BordaAtLarge([
+ {"count": 30, "ballot": ["c1", "c4", "c2", "c3"]},
+ {"count": 22, "ballot": ["c3", "c2", "c1", "c4"]},
+ {"count": 22, "ballot": ["c1", "c4", "c3", "c2"]},
+ {"count": 4, "ballot": ["c4", "c2", "c1", "c3"]},
+ {"count": 8, "ballot": ["c2", "c3", "c1", "c4"]},
+ {"count": 10, "ballot": ["c1", "c2", "c4", "c3"]}
+ ], required_winners=2).as_dict()
+
+ # Run tests
+ self.assertEqual(output["tallies"], {'c3': 104, 'c2': 126, 'c1': 220, 'c4': 126})
+ self.assertEqual(len(output["tie_breaker"]), 4)
+ self.assertEqual(output["tied_winners"], set(['c2', 'c4']))
+ self.assertTrue("c1" in output["winners"] and ("c2" in output["winners"] or "c4" in output["winners"]))
+ self.assertEqual(len(output), 5)
+
+
+if __name__ == "__main__":
+ unittest.main()
+
+
diff --git a/test_functionality/test_borda_manipulation_heurestics_methods.py b/test_functionality/test_borda_manipulation_heurestics_methods.py
new file mode 100644
index 0000000..5165061
--- /dev/null
+++ b/test_functionality/test_borda_manipulation_heurestics_methods.py
@@ -0,0 +1,194 @@
+# Copyright (C) 2023, Leora Schmerler
+#
+# This program 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.
+#
+# 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import random
+from py3votecore.borda_manipulation_heurestics_methods import AverageFit, LargestFit
+import unittest
+import time
+
+
+class TestAverageFit(unittest.TestCase):
+
+ # Manipulation fails
+ def test_4_candidates_manipulation_fails(self):
+
+ input = [
+ {"count": 26, "ballot": ["a","b","c","d"]},
+ {"count": 22, "ballot": ["a","c","d","b"]},
+ {"count": 23, "ballot": ["a","b","c","d"]}
+ ]
+ output = AverageFit(ballots=input, preferred_candidate="d", k=60)
+
+ # Run test
+ self.assertFalse(output)
+
+ # Manipulation succeeds
+ def test_10_candidates_successful_manipulation(self):
+
+ input = [
+ {"count": 26, "ballot": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]},
+ {"count": 53, "ballot": ["a", "b", "j", "d", "g", "e", "h", "f", "c", "i"]},
+ {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]}
+ ] # "a" = 1143, "b" = 1136, "c" = 811, "d" = 1122, "e" = 702, "f" = 426, "g" = 415, "h" = 499, "i" = 170, "j" = 371
+ output = AverageFit(ballots=input, preferred_candidate="d", k=8)
+ # [["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"]]
+ # "a" = 1143, "b" = 1144, "c" = 827, "d" = 1194, "e" = 726, "f" = 466, "g" = 463, "h" = 531, "i" = 234, "j" = 427
+ # Run test
+ self.assertEqual(len(output), 8)
+ for manip in output:
+ self.assertEqual(len(manip), 10)
+ for i,j in [(0,'d'),(1,'i'),(2,'j'),(3,'g'),(4,'f'),(5,'h'),(6,'e'),(7,'c'),(8,'b'),(9,'a')]:
+ self.assertEqual(manip[i],j)
+
+ # Borda
+ def test_4_candidate_manipulation(self):
+
+ # Generate data
+ input = [
+ {"count": 2, "ballot": ["c","a","b","d"]},
+ {"count": 2, "ballot": ["b","c","a","d"]}]
+ # "a" = 6, "b" = 8, "c" = 10, "d" = 0
+ output = AverageFit(ballots=input, preferred_candidate="d", k=5)
+ # [["d","a","b","c"],["d","a","b","c"],["d","a","b","c"],["d","a","b","c"],["d","b","c","a"]]
+ # "a" = 14, "b" = 13, "c" = 11, "d" = 15
+ # Run tests
+ self.assertEqual(len(output),5)
+ for manip in output:
+ self.assertEqual(len(manip), 4)
+ self.assertEqual(manip[0],'d')
+
+ def test_100_candidate_manipulation(self):
+ ballots = []
+ for i in range(20):
+ ballots.append({"count": 2, "ballot": create_vote(100)})
+ start = time.process_time()
+ print(AverageFit(ballots, "A", 4))
+ end = time.process_time()
+ duration_in_seconds = end-start
+ output = duration_in_seconds < 10
+ self.assertTrue(output)
+
+
+ def test_1000_voters_manipulation(self):
+ ballots = []
+ for i in range(1000):
+ ballots.append({"count": 1, "ballot": create_vote(50)})
+ start = time.process_time()
+ print(AverageFit(ballots, "A", 100))
+ end = time.process_time()
+ duration_in_seconds = end-start
+ output = duration_in_seconds < 10
+ self.assertTrue(output)
+
+def create_alphabet_vote(m):
+ ABC = ['Z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
+ 'V', 'W', 'X', 'Y']
+ preference = []
+ for i in range(1, m + 1):
+ tmp = ''
+ c = i
+ while c >= 0:
+ tmp += ABC[c % 26]
+ c = c - 26
+ if c == 0:
+ c = -1
+ preference.append(tmp)
+ return preference
+def create_vote(m):
+ preference = create_alphabet_vote(m)
+ random.shuffle(preference)
+ return preference
+
+class TestLargestFit(unittest.TestCase):
+
+ # Manipulation fails
+ def test_4_candidates_manipulation_fails(self):
+
+ input = [
+ {"count": 26, "ballot": ["a","b","c","d"]},
+ {"count": 22, "ballot": ["a","c","d","b"]},
+ {"count": 23, "ballot": ["a","b","c","d"]}
+ ]
+ output = LargestFit(ballots=input, preferred_candidate="d", k=60)
+
+ # Run test
+ self.assertFalse(output)
+
+ # Manipulation succeeds
+ def test_10_candidates_successful_manipulation(self):
+
+ input = [
+ {"count": 26, "ballot": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]},
+ {"count": 53, "ballot": ["a", "b", "j", "d", "g", "e", "h", "f", "c", "i"]},
+ {"count": 72, "ballot": ["d", "c", "b", "a", "e", "h", "f", "i", "g", "j"]}
+ ] # "a" = 1143, "b" = 1136, "c" = 811, "d" = 1122, "e" = 702, "f" = 426, "g" = 415, "h" = 499, "i" = 170, "j" = 371
+ output = LargestFit(ballots=input, preferred_candidate="d", k=8)
+ # [["d","i","j","g","f","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","f","g","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","f","g","h","e","c","b","a"],["d","i","j","g","f","h","e","c","b","a"],["d","i","j","f","g","h","e","c","b","a"],["d","i","g","j","f","h","e","c","b","a"]]
+ # "a" = 1143, "b" = 1144, "c" = 827, "d" = 1194, "e" = 726, "f" = 469, "g" = 461, "h" = 531, "i" = 234, "j" = 426
+ # Run test
+ self.assertEqual(len(output),8)
+ for manip in output:
+ self.assertEqual(len(manip), 10)
+ for i,j in [(0,'d'),(1,'i'),(5,'h'),(6,'e'),(7,'c')]:
+ self.assertEqual(manip[i],j)
+
+ # Borda, relevant ties
+ def test_4_candidate_manipulation(self):
+
+ # Generate data
+ input = [
+ {"count": 2, "ballot": ["c","a","b","d"]},
+ {"count": 2, "ballot": ["b","c","a","d"]}]
+ # "a" = 6, "b" = 8, "c" = 10, "d" = 0
+ output = LargestFit(ballots=input, preferred_candidate="d", k=5)
+ # [["d","a","c","b"],["d","a","c","b"],["d","b","a","c"],["d","a","b","c"],["d","b","c","a"]]
+ # Run tests
+ self.assertEqual(len(output),5)
+ for manip in output:
+ self.assertEqual(len(manip), 4)
+ self.assertEqual(manip[0],'d')
+
+ def test_100_candidate_manipulation(self):
+
+ ballots = []
+ for i in range(20):
+ ballots.append({"count": 2, "ballot": create_vote(100)})
+ start = time.process_time()
+ print(LargestFit(ballots, "A", 4))
+ end = time.process_time()
+ duration_in_seconds = end-start
+ output = duration_in_seconds < 10
+ self.assertTrue(output)
+
+
+ def test_1000_voters_manipulation(self):
+ ballots = []
+ for i in range(1000):
+ ballots.append({"count": 1, "ballot": create_vote(50)})
+ start = time.process_time()
+ print(LargestFit(ballots, "A", 100))
+ end = time.process_time()
+ duration_in_seconds = end-start
+ output = duration_in_seconds < 10
+ self.assertTrue(output)
+
+
+
+if __name__ == "__main__":
+ unittest.main()
+
+
+
+
diff --git a/test_functionality/test_schulze_method.py b/test_functionality/test_schulze_method.py
index 9372712..ac16fd7 100644
--- a/test_functionality/test_schulze_method.py
+++ b/test_functionality/test_schulze_method.py
@@ -169,7 +169,10 @@ def test_tuple_ballots(self):
output_list = SchulzeMethod(input_list, ballot_notation=SchulzeMethod.BALLOT_NOTATION_GROUPING).as_dict()
# Run tests
- self.assertEqual(output_tuple, output_list)
+ self.assertEqual(output_tuple['candidates'], output_list['candidates'])
+ self.assertEqual(output_tuple['tied_winners'], output_list['tied_winners'])
+ self.assertEqual(output_tuple['pairs'], output_list['pairs'])
+ self.assertEqual(output_tuple['strong_pairs'], output_list['strong_pairs'])
if __name__ == "__main__":
unittest.main()