Skip to content

Commit 74d758e

Browse files
author
Berj Chilingirian
committed
Add code; almost stable -- need to configure config.json for dividebatur
1 parent d65c426 commit 74d758e

13 files changed

+1855
-0
lines changed

aus_senate_audit/__init__.py

Whitespace-only changes.
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
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()

aus_senate_audit/audits/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)