|
| 1 | +""" |
| 2 | +Module: audit_tie_breaker |
| 3 | +Author: Berj K. Chilingirian |
| 4 | +Date: 10 August 2016 |
| 5 | +
|
| 6 | +Description: |
| 7 | +
|
| 8 | + The Australian Senate Election (ASE) may require an Australian Election |
| 9 | + Official (AEO) to break a tie. There are three cases in which ties must |
| 10 | + be broken manually by an AEO: |
| 11 | +
|
| 12 | + Case 1: Election Order Tie |
| 13 | +
|
| 14 | + If multiple candidates hold the same number of votes and that |
| 15 | + number is greater than the quota, then a previous round in which |
| 16 | + those candidates held a differing number of votes is used to |
| 17 | + determine the election order. If no such round exists, the AEO |
| 18 | + determines a permutation of candidate IDs, specifying the order |
| 19 | + in which those candidates are elected. |
| 20 | +
|
| 21 | + Case 2: Election Tie |
| 22 | +
|
| 23 | + If there are two final continuing candidates, with one remaining |
| 24 | + vacancy, and both candidates hold the same number of votes, the |
| 25 | + AEO decides which candidate is elected. |
| 26 | +
|
| 27 | + Case 3: Exclusion Tie |
| 28 | +
|
| 29 | + If a candidate must be excluded, then the lowest number of votes |
| 30 | + held by any candidate is found. If multiple candidates hold that |
| 31 | + number of votes, the same tie breaker system as in (1) is used. |
| 32 | + If that fails, the AEO decides what candidate is excluded. |
| 33 | +
|
| 34 | + We want our auditing procedures to be as consistent as possible with the |
| 35 | + real ASE. However, the Commonwealth Electoral Act of 1981 does not specify |
| 36 | + the tie-breaking procedure of an AEO. Thus, we use tie-breaking information |
| 37 | + from the real election to resolve ties encountered during our audit. |
| 38 | +
|
| 39 | + To do this, we: |
| 40 | +
|
| 41 | + (1) Ingest tie-breaking information from the real ASE. |
| 42 | +
|
| 43 | + (2) Construct a directed, acyclic graph where a directed edge from A to B |
| 44 | + represents situations where A is elected over B (cases 1, 2) or B is |
| 45 | + excluded (case 3). Note that more than one edge may be created by a |
| 46 | + given tie-breaking event. |
| 47 | +
|
| 48 | + (3) Sort the vertices into a linear order using a random topological sort. |
| 49 | +
|
| 50 | + (4) Use the linear ordering discovered in Step 3 to break all ties |
| 51 | + encountered during the audit. In other words, given candidate IDs A |
| 52 | + and B, prefer electing/not-excluding the candidate earlier in the |
| 53 | + linear order. |
| 54 | +
|
| 55 | +Usage: |
| 56 | +
|
| 57 | + .. code-block:: python3 |
| 58 | +
|
| 59 | + >>> import audit_tie_breaker |
| 60 | + >>> atb = AuditTieBreaker(['A', 'B', 'C', 'D', 'E', 'F', 'G'], verbose=True) |
| 61 | + >>> atb.load_election( |
| 62 | + [ # Election Order Tie Events |
| 63 | + [ |
| 64 | + [ |
| 65 | + ['A', 'B', 'C'], |
| 66 | + ['A', 'C', 'B'], |
| 67 | + ['B', 'A', 'C'], |
| 68 | + ['B', 'C', 'A'], |
| 69 | + ['C', 'A', 'B'], |
| 70 | + ['C', 'B', 'A'], |
| 71 | + ], |
| 72 | + ['B', 'A', 'C'], |
| 73 | + ], |
| 74 | + ], |
| 75 | + [ # Election Tie Events |
| 76 | + [ |
| 77 | + ['D', 'G'], |
| 78 | + 'G', |
| 79 | + ], |
| 80 | + ], |
| 81 | + [ # Exclusion Tie Events |
| 82 | + [ |
| 83 | + ['D', 'E', 'F'], |
| 84 | + 'E', |
| 85 | + ], |
| 86 | + ], |
| 87 | + ) |
| 88 | + = Building Audit Tie-Breaking Graph... |
| 89 | + - Added edge B -> C because B preferred to C in resolution for election order tie. |
| 90 | + - Added edge B -> A because B preferred to A in resolution for election order tie. |
| 91 | + - Added edge C -> A because C preferred to A in resolution for election order tie. |
| 92 | + - Added edge G -> D because G elected over D. |
| 93 | + - Added edge D -> E because E excluded over D. |
| 94 | + - Added edge F -> E because E excluded over F. |
| 95 | + --> Linear order determined as B, C, A, F, G, D, E. |
| 96 | +
|
| 97 | + = Verifying linear order is consisent with real election's tie-breaking events... |
| 98 | + - Election order tie between ['A', 'B', 'C'] broken with permutation ['B', 'C', 'A']. |
| 99 | + - Election tie between ['D', 'G'] broken by electing G. |
| 100 | + - Exclusion tie between ['D', 'E', 'F'] broken by excluding E. |
| 101 | + --> Linear order is consistent with real election's tie-breaking events. |
| 102 | +
|
| 103 | + >>> atb.break_tie(['A', 'B', 'C'], 1) |
| 104 | + - Election order tie between ['A', 'B', 'C'] broken with permutation ['B', 'C', 'A']. |
| 105 | +""" |
| 106 | + |
| 107 | +import itertools |
| 108 | +import random |
| 109 | +import sys |
| 110 | + |
| 111 | + |
| 112 | +class AuditTieBreaker(object): |
| 113 | + """ Implements a class for breaking ties encountered during an audit. |
| 114 | +
|
| 115 | + :ivar _vertices: A mapping from a vertex in the audit tie breaking graph to its |
| 116 | + neighbors. |
| 117 | + :vartype _vertices: dict |
| 118 | + :ivar _print_fn: A function which takes a string as input and writes it to the |
| 119 | + appropriate file. |
| 120 | + :vartype _print_fn: function |
| 121 | + :ivar _linear_order: A linear ordering of the candidate IDs of all candidates in |
| 122 | + the contest being audited. The linear ordering is represented as a mapping |
| 123 | + from a candidate ID to its position in the linear order. |
| 124 | + :vartype _linear_order: dict |
| 125 | + """ |
| 126 | + WRITE_OPT = 'w' |
| 127 | + |
| 128 | + COMMA_DELIM = ', ' |
| 129 | + |
| 130 | + # Tie-Breaking Event IDs. |
| 131 | + ELECTION_ORDER_TIE_ID = 1 |
| 132 | + ELECTION_TIE_ID = 2 |
| 133 | + EXCLUSION_TIE_ID = 3 |
| 134 | + |
| 135 | + # Messages to be printed during verbose mode. |
| 136 | + BUILDING_GRAPH_MSG = '= Building Audit Tie-Breaking Graph...' |
| 137 | + VERIFY_LINEAR_ORDER_MSG = '\n= Verifying linear order is consisent with real election\'s tie-breaking events...' |
| 138 | + IS_CONSISTENT_MSG = ' --> Linear order is consistent with real election\'s tie-breaking events.\n' |
| 139 | + |
| 140 | + # Messages to be printed during verbose mode - require formatting. |
| 141 | + ADDED_ELECTION_ORDER_TIE_EDGE = ' - Added edge {0} -> {1} because {0} preferred to {1} in resolution for election order tie.' |
| 142 | + ADDED_ELECTION_TIE_EDGE = ' - Added edge {0} -> {1} because {0} elected over {1}.' |
| 143 | + ADDED_EXCLUSION_TIE_EDGE = ' - Added edge {0} -> {1} because {1} excluded over {0}.' |
| 144 | + LINEAR_ORDER_MSG = ' --> Linear order determined as {0}.' |
| 145 | + ELECTION_ORDER_TIE_BREAK = ' - Election order tie between {0} broken with permutation {1}.' |
| 146 | + ELECTION_TIE_BREAK = ' - Election tie between {0} broken by electing {1}.' |
| 147 | + EXCLUSION_TIE_BREAK = ' - Exclusion tie between {0} broken by excluding {1}.' |
| 148 | + |
| 149 | + def __init__(self, candidate_ids, seed=1, verbose=False, out_f=None): |
| 150 | + """ Initializes the `AuditTieBreaker` object. |
| 151 | +
|
| 152 | + :param candidate_ids: A list of the candidate IDs of all candidates in the |
| 153 | + election contest. |
| 154 | + :type candidate_ids: list |
| 155 | + :param verbose: A flag indicating whether or not the `AuditTieBreaker` |
| 156 | + object should be verbose when loading data/breaking ties. |
| 157 | + :type verbose: bool |
| 158 | + :param seed: An integer specifying the random seed to use when determining |
| 159 | + the random topological sort of the candidate IDs of all candidates in |
| 160 | + the election (default: 1). |
| 161 | + :type seed: int |
| 162 | + :param out_f: A string representing the name of a file to write all debug |
| 163 | + information to (default: stdout). Only used when `verbose` is true. |
| 164 | + :type out_f: str |
| 165 | + """ |
| 166 | + random.seed(seed) |
| 167 | + self._vertices = {candidate_id : [] for candidate_id in candidate_ids} |
| 168 | + self._print_fn = AuditTieBreaker._setup_print_fn(out_f) if verbose else lambda x : None |
| 169 | + self._linear_order = {} |
| 170 | + |
| 171 | + @staticmethod |
| 172 | + def _setup_print_fn(out_f): |
| 173 | + """ Returns a function to be used for writing verbose information. |
| 174 | +
|
| 175 | + :param out_f: A string representing the name of a file to write all debug |
| 176 | + information to. |
| 177 | + :type out_f: str |
| 178 | +
|
| 179 | + :return: A function which takes a string as input and writes it to the |
| 180 | + appropriate file. |
| 181 | + :rtype: function |
| 182 | + """ |
| 183 | + if out_f is not None: |
| 184 | + sys.stdout = open(out_f, AuditTieBreaker.WRITE_OPT) |
| 185 | + return lambda x : print(x) |
| 186 | + |
| 187 | + def _visit(self, v, linear_order): |
| 188 | + """ Explores the neighbors of the given node recursively and then adds the |
| 189 | + explored node to the head of the linear order. |
| 190 | +
|
| 191 | + :param v: A candidate ID. |
| 192 | + :type v: int |
| 193 | + :param linear_order: The linear order of candidate IDs so far. |
| 194 | + :type linear_order: list |
| 195 | + """ |
| 196 | + if v in linear_order: |
| 197 | + # Do not explore a node twice. |
| 198 | + return |
| 199 | + random.shuffle(self._vertices[v]) |
| 200 | + for u in self._vertices[v]: |
| 201 | + self._visit(u, linear_order) |
| 202 | + linear_order.insert(0, v) |
| 203 | + |
| 204 | + def load_events(self, election_order_ties, election_ties, exclusion_ties): |
| 205 | + """ Loads all tie-breaking events specified in `events_f`. |
| 206 | +
|
| 207 | + :param election_order_ties: A list of 2-tuples representing election order |
| 208 | + tie events where the first entry is the permutations of election orders |
| 209 | + and the second entry is the permutation that resovled the tie. |
| 210 | + :type election_order_ties: list |
| 211 | + :param election_ties: A list of 2-tuples representing election tie events |
| 212 | + where the first entry is the IDs of the candidates tied for election |
| 213 | + and the second entry is the candidate ID that resovled the tie. |
| 214 | + :type election_ties: list |
| 215 | + :param exclusion_ties: A list of 2-tuples representing exclusion tie events |
| 216 | + where the first entry is the IDs of the candidates tied for exclusion |
| 217 | + and the second entry is the candidate ID that resovled the tie. |
| 218 | + :type exclusion_ties: list |
| 219 | + """ |
| 220 | + # Construct audit tie-breaking graph. |
| 221 | + self._print_fn(AuditTieBreaker.BUILDING_GRAPH_MSG) |
| 222 | + |
| 223 | + # Process Election Order Tie Events. |
| 224 | + for _, resolution in election_order_ties: |
| 225 | + for src_cid, dest_cid in itertools.combinations(resolution, 2): |
| 226 | + self._vertices[src_cid].append(dest_cid) |
| 227 | + self._print_fn(AuditTieBreaker.ADDED_ELECTION_ORDER_TIE_EDGE.format( |
| 228 | + src_cid, |
| 229 | + dest_cid, |
| 230 | + )) |
| 231 | + |
| 232 | + # Process Election Tie Events. |
| 233 | + for candidate_ids, resolution in election_ties: |
| 234 | + for cid in candidate_ids: |
| 235 | + if cid != resolution: |
| 236 | + self._vertices[resolution].append(cid) |
| 237 | + self._print_fn(AuditTieBreaker.ADDED_ELECTION_TIE_EDGE.format( |
| 238 | + resolution, |
| 239 | + cid, |
| 240 | + )) |
| 241 | + |
| 242 | + # Process Exclusion Tie Events. |
| 243 | + for candidate_ids, resolution in exclusion_ties: |
| 244 | + for cid in candidate_ids: |
| 245 | + if cid != resolution: |
| 246 | + self._vertices[cid].append(resolution) |
| 247 | + self._print_fn(AuditTieBreaker.ADDED_EXCLUSION_TIE_EDGE.format( |
| 248 | + cid, |
| 249 | + resolution, |
| 250 | + )) |
| 251 | + |
| 252 | + # Determine a random topological sorting of the vertices in the audit tie-breaking graph. |
| 253 | + vertices = sorted(self._vertices.keys()) |
| 254 | + random.shuffle(vertices) |
| 255 | + linear_order = [] |
| 256 | + for v in vertices: |
| 257 | + self._visit(v, linear_order) |
| 258 | + self._linear_order = {linear_order[i] : i for i in range(len(linear_order))} |
| 259 | + self._print_fn(AuditTieBreaker.LINEAR_ORDER_MSG.format( |
| 260 | + AuditTieBreaker.COMMA_DELIM.join([str(x) for x in linear_order])) |
| 261 | + ) |
| 262 | + |
| 263 | + # Verify linear order is consistent with the real election's tie-breakin events. |
| 264 | + self._print_fn(AuditTieBreaker.VERIFY_LINEAR_ORDER_MSG) |
| 265 | + for permutations, resolution in election_order_ties: |
| 266 | + assert self.break_election_order_tie(permutations) == permutations.index(resolution) |
| 267 | + for candidate_ids, resolution in election_ties: |
| 268 | + assert self.break_election_tie(candidate_ids) == candidate_ids.index(resolution) |
| 269 | + for candidate_ids, resolution in exclusion_ties: |
| 270 | + assert self.break_exclusion_tie(candidate_ids) == candidate_ids.index(resolution) |
| 271 | + self._print_fn(AuditTieBreaker.IS_CONSISTENT_MSG) |
| 272 | + |
| 273 | + def break_tie(self, candidate_ids, case_num): |
| 274 | + """ Returns the resolution for the given candidate IDs and case number. |
| 275 | +
|
| 276 | + :param candidate_ids: A list of candidate IDs. |
| 277 | + :type candidate_ids: list |
| 278 | + :param case_num: A integer identifying the tie-breaking case. |
| 279 | + :type case_num: int |
| 280 | +
|
| 281 | + :return: The resolution for the given candidate IDs and case number. |
| 282 | + :rtype: A single candidate ID (cases 2,3) or a permutation of the given cadndidate IDs |
| 283 | + (case 1). |
| 284 | + """ |
| 285 | + cids_to_order = {cid : self._linear_order[cid] for cid in candidate_ids} |
| 286 | + resolution = sorted(cids_to_order, key=cids_to_order.__getitem__) |
| 287 | + |
| 288 | + if case_num == AuditTieBreaker.ELECTION_ORDER_TIE_ID: |
| 289 | + result = resolution |
| 290 | + self._print_fn(AuditTieBreaker.ELECTION_ORDER_TIE_BREAK.format(candidate_ids, result)) |
| 291 | + elif case_num == AuditTieBreaker.ELECTION_TIE_ID: |
| 292 | + result = resolution[0] |
| 293 | + self._print_fn(AuditTieBreaker.ELECTION_TIE_BREAK.format(candidate_ids, result)) |
| 294 | + else: |
| 295 | + result = resolution[-1] |
| 296 | + self._print_fn(AuditTieBreaker.EXCLUSION_TIE_BREAK.format(candidate_ids, result)) |
| 297 | + return result |
| 298 | + |
| 299 | + def break_election_order_tie(self, permutations): |
| 300 | + """ Convenience wrapper for breaking election order ties. """ |
| 301 | + return permutations.index(tuple(self.break_tie(permutations[0], AuditTieBreaker.ELECTION_ORDER_TIE_ID))) |
| 302 | + |
| 303 | + def break_election_tie(self, candidate_ids): |
| 304 | + """ Convenience wrapper for breaking election ties. """ |
| 305 | + return candidate_ids.index(self.break_tie(candidate_ids, AuditTieBreaker.ELECTION_TIE_ID)) |
| 306 | + |
| 307 | + def break_exclusion_tie(self, candidate_ids): |
| 308 | + """ Convenience wrapper for breaking exclusion ties. """ |
| 309 | + return candidate_ids.index(self.break_tie(candidate_ids, AuditTieBreaker.EXCLUSION_TIE_ID)) |
| 310 | + |
| 311 | + |
| 312 | +def test_audit_tie_breaker(): |
| 313 | + """ Tests the `AuditTieBreaker` implementation. """ |
| 314 | + # Test `AuditTieBreaker` implementation. |
| 315 | + audit_tb = AuditTieBreaker(['A', 'B', 'C', 'D', 'E', 'F', 'G'], verbose=True) |
| 316 | + audit_tb.load_events( |
| 317 | + [ # Election Order Tie Events |
| 318 | + [ |
| 319 | + [ |
| 320 | + ['A', 'B', 'C'], |
| 321 | + ['A', 'C', 'B'], |
| 322 | + ['B', 'A', 'C'], |
| 323 | + ['B', 'C', 'A'], |
| 324 | + ['C', 'A', 'B'], |
| 325 | + ['C', 'B', 'A'], |
| 326 | + ], |
| 327 | + ['B', 'A', 'C'], |
| 328 | + ], |
| 329 | + ], |
| 330 | + [ # Election Tie Events |
| 331 | + [ |
| 332 | + ['D', 'G'], |
| 333 | + 'G', |
| 334 | + ], |
| 335 | + ], |
| 336 | + [ # Exclusion Tie Events |
| 337 | + [ |
| 338 | + ['D', 'E', 'F'], |
| 339 | + 'E', |
| 340 | + ], |
| 341 | + ], |
| 342 | + ) |
| 343 | + audit_tb._print_fn('= Running AuditTieBreaker tests...') |
| 344 | + assert audit_tb.break_tie(['A', 'B', 'C'], 1) == ['B', 'A', 'C'] |
| 345 | + assert audit_tb.break_tie(['D', 'E', 'F'], 3) == 'E' |
| 346 | + assert audit_tb.break_tie(['D', 'G'], 2) == 'G' |
| 347 | + assert audit_tb.break_tie(['B', 'F'], 2) == 'B' # Test depends on random.seed of 1. |
| 348 | + assert audit_tb.break_tie(['B', 'F'], 3) == 'F' # Test depends on random.seed of 1. |
| 349 | + audit_tb._print_fn(' --> Tests PASSED!') |
| 350 | + |
| 351 | + |
| 352 | +if __name__ == '__main__': |
| 353 | + # Runs AuditTieBreaker Tests. |
| 354 | + test_audit_tie_breaker() |
0 commit comments