vse-sim 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
compat.py ADDED
@@ -0,0 +1,47 @@
1
+ """Small compatibility helpers shared across the simulation modules."""
2
+
3
+ from numbers import Number
4
+
5
+ from numpy import ceil as numpy_ceil
6
+ from numpy import floor as numpy_floor
7
+ from numpy import mean as numpy_mean
8
+ from numpy import median as numpy_median
9
+ from numpy import sqrt as numpy_sqrt
10
+ from numpy import std as numpy_std
11
+
12
+
13
+ def as_builtin_scalar(value):
14
+ """Convert NumPy scalar values to their Python built-in equivalents."""
15
+ try:
16
+ return value.item()
17
+ except (AttributeError, ValueError):
18
+ return value
19
+
20
+
21
+ def ceil(*args, **kwargs):
22
+ return as_builtin_scalar(numpy_ceil(*args, **kwargs))
23
+
24
+
25
+ def floor(*args, **kwargs):
26
+ return as_builtin_scalar(numpy_floor(*args, **kwargs))
27
+
28
+
29
+ def mean(*args, **kwargs):
30
+ return as_builtin_scalar(numpy_mean(*args, **kwargs))
31
+
32
+
33
+ def median(*args, **kwargs):
34
+ return as_builtin_scalar(numpy_median(*args, **kwargs))
35
+
36
+
37
+ def sqrt(*args, **kwargs):
38
+ return as_builtin_scalar(numpy_sqrt(*args, **kwargs))
39
+
40
+
41
+ def std(*args, **kwargs):
42
+ return as_builtin_scalar(numpy_std(*args, **kwargs))
43
+
44
+
45
+ def isnum(x):
46
+ """Return whether ``x`` is an instance of a numeric type."""
47
+ return isinstance(x, Number)
dataClasses.py ADDED
@@ -0,0 +1,354 @@
1
+ import random
2
+
3
+ from compat import isnum, mean
4
+ from mydecorators import autoassign, decorator
5
+
6
+
7
+ class VseOneRun:
8
+ @autoassign
9
+ def __init__(self, result, tallyItems, strat):
10
+ pass
11
+
12
+
13
+ class VseMethodRun:
14
+ @autoassign
15
+ def __init__(self, method, choosers, results):
16
+ pass
17
+
18
+
19
+ ####data holders for output
20
+ from collections import defaultdict
21
+
22
+
23
+ class SideTally(defaultdict):
24
+ """Used for keeping track of how many voters are being strategic, etc.
25
+
26
+ DO NOT use plain +; for this class, it is equivalent to +=, but less readable.
27
+
28
+ """
29
+
30
+ def __init__(self):
31
+ super().__init__(int)
32
+
33
+ def initKeys(self, chooser):
34
+ try:
35
+ self.keyList = chooser.allTallyKeys()
36
+ except AttributeError:
37
+ try:
38
+ self.keyList = list(chooser)
39
+ except TypeError:
40
+ self.keyList = []
41
+ self.initKeys = staticmethod(lambda x: x) # don't do it again
42
+
43
+ def serialize(self):
44
+ try:
45
+ return [self[key] for key in self.keyList]
46
+ except AttributeError:
47
+ return []
48
+
49
+ def fullSerialize(self):
50
+ if not hasattr(self, "keyList"):
51
+ return [self[key] for key in self.keys()]
52
+ return [self[key] for key in self.keyList]
53
+
54
+ def itemList(self):
55
+ try:
56
+ kl = self.keyList
57
+ return [(k, self[k]) for k in kl] + [(k, self[k]) for k in self.keys() if k not in kl]
58
+ except AttributeError:
59
+ return list(self.items())
60
+
61
+
62
+ class Tallies(list):
63
+ """Used (ONCE) as an enumerator, gives an inexhaustible flow of SideTally objects.
64
+ After that, use as list to see those objects.
65
+
66
+ >>> ts = Tallies()
67
+ >>> for i, j in zip(ts, [5,4,3]):
68
+ ... i[j] += j
69
+ ...
70
+ >>> [t.serialize() for t in ts]
71
+ [[], [], [], []]
72
+ >>> [t.fullSerialize() for t in ts]
73
+ [[5], [4], [3], []]
74
+ >>> [t.initKeys([k]) for (t,k) in zip(ts,[6,4,3])]
75
+ [None, None, None]
76
+ >>> [t.serialize() for t in ts]
77
+ [[0], [4], [3], []]
78
+ """
79
+
80
+ def __iter__(self):
81
+ if getattr(self, "used", False):
82
+ return super().__iter__()
83
+ self.used = True
84
+ return self._generated_tallies()
85
+
86
+ def __eq__(self, other):
87
+ if not isinstance(other, Tallies):
88
+ return super().__eq__(other)
89
+ return super().__eq__(other) and getattr(self, "used", False) == getattr(
90
+ other, "used", False
91
+ )
92
+
93
+ def _generated_tallies(self):
94
+ while True:
95
+ tally = SideTally()
96
+ self.append(tally)
97
+ yield tally
98
+
99
+
100
+ ##Election Methods
101
+ class Method:
102
+ """Base class for election methods. Holds some of the duct tape."""
103
+
104
+ def __str__(self):
105
+ return self.__class__.__name__
106
+
107
+ def results(self, ballots, isHonest=False, **kwargs):
108
+ """Combines ballots into results. Override for comparative
109
+ methods.
110
+
111
+ Ballots is an iterable of list-or-tuple of numbers (utility) higher is better for the choice of that index.
112
+
113
+ Returns a results-array which should be a list of the same length as a ballot with a number (higher is better) for the choice at that index.
114
+
115
+ Test for subclasses, makes no sense to test this method in the abstract base class.
116
+ """
117
+ if type(ballots) is not list:
118
+ ballots = list(ballots)
119
+ return list(map(self.candScore, zip(*ballots)))
120
+
121
+ @staticmethod # cls is provided explicitly, not through binding
122
+ def honBallot(cls, utils):
123
+ """Takes utilities and returns an honest ballot"""
124
+ raise NotImplementedError(f"{cls} needs honBallot")
125
+
126
+ @staticmethod
127
+ def winner(results):
128
+ """Simply find the winner once scores are already calculated. Override for
129
+ ranked methods.
130
+
131
+
132
+ >>> Method().winner([1,2,3,2,-100])
133
+ 2
134
+ >>> 2 < Method().winner([1,2,1,3,3,3,2,1,2]) < 6
135
+ True
136
+ """
137
+ winScore = max(result for result in results if isnum(result))
138
+ winners = [cand for (cand, score) in enumerate(results) if score == winScore]
139
+ return random.choice(winners)
140
+
141
+ def honBallotFor(self, voters):
142
+ """This is where you would do any setup necessary and create an honBallot
143
+ function. But the base version just returns the honBallot function."""
144
+ return self.honBallot
145
+
146
+ def dummyBallotFor(self, polls):
147
+ """Returns a (function which takes utilities and returns a dummy ballot)
148
+ for the given "polling" info."""
149
+ return lambda cls, utilities, stratTally: utilities
150
+
151
+ def resultsFor(self, voters, chooser, tally=None, **kwargs):
152
+ """create ballots and get results.
153
+
154
+ Again, test on subclasses.
155
+ """
156
+ if tally is None:
157
+ tally = SideTally()
158
+ tally.initKeys(chooser)
159
+ return dict(
160
+ results=self.results(
161
+ [chooser(self.__class__, voter, tally) for voter in voters], **kwargs
162
+ ),
163
+ chooser=chooser.__name__,
164
+ tally=tally,
165
+ )
166
+
167
+ def multiResults(self, voters, chooserFuns=(), media=(lambda x, t: x), checkStrat=True):
168
+ """Runs two base elections: first with honest votes, then
169
+ with strategic results based on the first results (filtered by
170
+ the media). Then, runs a series of elections using each chooserFun
171
+ in chooserFuns to select the votes for each voter.
172
+
173
+ Returns a tuple of (honResults, stratResults, ...). The stratresults
174
+ are based on common polling information, which is given by media(honresults).
175
+ """
176
+ from stratFunctions import OssChooser
177
+
178
+ honTally = SideTally()
179
+ self.__class__.extraEvents = {}
180
+ hon = self.resultsFor(voters, self.honBallotFor(voters), honTally, isHonest=True)
181
+
182
+ stratTally = SideTally()
183
+
184
+ polls = media(hon["results"], stratTally)
185
+ winner, _w, target, _t = self.stratTargetFor(sorted(enumerate(polls), key=lambda x: -x[1]))
186
+
187
+ strat = self.resultsFor(voters, self.stratBallotFor(polls), stratTally)
188
+
189
+ ossTally = SideTally()
190
+ oss = self.resultsFor(voters, self.ballotChooserFor(OssChooser()), ossTally)
191
+ ossWinner = oss["results"].index(max(oss["results"]))
192
+ ossTally["worked"] += 1 if ossWinner == target else (0 if ossWinner == winner else -1)
193
+
194
+ smart = dict(
195
+ results=(hon["results"] if ossTally["worked"] == 1 else oss["results"]),
196
+ chooser="smartOss",
197
+ tally=SideTally(),
198
+ )
199
+
200
+ extraTallies = Tallies()
201
+ results = [strat, oss, smart] + [
202
+ self.resultsFor(voters, self.ballotChooserFor(chooserFun), aTally)
203
+ for (chooserFun, aTally) in zip(chooserFuns, extraTallies)
204
+ ]
205
+ return [(hon["results"], hon["chooser"], list(self.__class__.extraEvents.items()))] + [
206
+ (r["results"], r["chooser"], r["tally"].itemList()) for r in results
207
+ ]
208
+
209
+ def vseOn(self, voters, chooserFuns=(), **args):
210
+ """Finds honest and strategic voter satisfaction efficiency (VSE)
211
+ for this method on the given electorate.
212
+ """
213
+ multiResults = self.multiResults(voters, chooserFuns, **args)
214
+ utils = voters.socUtils
215
+ best = max(utils)
216
+ rand = mean(utils)
217
+
218
+ # import pprint
219
+ # pprint.pprint(multiResults)
220
+ vses = VseMethodRun(
221
+ self.__class__,
222
+ chooserFuns,
223
+ [
224
+ VseOneRun(
225
+ [(utils[self.winner(result)] - rand) / (best - rand)],
226
+ tally,
227
+ chooser,
228
+ )
229
+ for (result, chooser, tally) in multiResults[0]
230
+ ],
231
+ )
232
+ vses.extraEvents = multiResults[1]
233
+ return vses
234
+
235
+ def resultsTable(self, eid, emodel, cands, voters, chooserFuns=(), **args):
236
+ multiResults = self.multiResults(voters, chooserFuns, **args)
237
+ utils = voters.socUtils
238
+ best = max(utils)
239
+ rand = mean(utils)
240
+ rows = []
241
+ nvot = len(voters)
242
+ for result, chooser, tallyItems in multiResults:
243
+ row = {
244
+ "eid": eid,
245
+ "emodel": emodel,
246
+ "ncand": cands,
247
+ "nvot": nvot,
248
+ "best": best,
249
+ "rand": rand,
250
+ "method": str(self),
251
+ "chooser": chooser, # .getName(),
252
+ "util": utils[self.winner(result)],
253
+ "vse": (utils[self.winner(result)] - rand) / (best - rand),
254
+ }
255
+ # print(tallyItems)
256
+ for i, (k, v) in enumerate(tallyItems):
257
+ # print("Result: tally ",i,k,v)
258
+ row[f"tallyName{str(i)}"] = str(k)
259
+ row[f"tallyVal{str(i)}"] = str(v)
260
+ rows.append(row)
261
+ return rows
262
+
263
+ @staticmethod
264
+ def ballotChooserFor(chooserFun):
265
+ """Takes a chooserFun; returns a ballot chooser using that chooserFun"""
266
+
267
+ def ballotChooser(cls, voter, tally):
268
+ return getattr(voter, f"{cls.__name__}_{chooserFun(cls, voter, tally)}")
269
+
270
+ ballotChooser.__name__ = chooserFun.getName()
271
+ return ballotChooser
272
+
273
+ def stratTarget2(self, places):
274
+ ((frontId, frontResult), (targId, targResult)) = places[:2]
275
+ return (frontId, frontResult, targId, targResult)
276
+
277
+ def stratTarget3(self, places):
278
+ ((frontId, frontResult), (targId, targResult)) = places[:3:2]
279
+ return (frontId, frontResult, targId, targResult)
280
+
281
+ stratTargetFor = stratTarget2
282
+
283
+ def stratBallotFor(self, polls):
284
+ """Returns a (function which takes utilities and returns a strategic ballot)
285
+ for the given "polling" info."""
286
+
287
+ places = sorted(enumerate(polls), key=lambda x: -x[1]) # from high to low
288
+ # print("places",places)
289
+ (frontId, frontResult, targId, targResult) = self.stratTargetFor(places)
290
+ n = len(polls)
291
+
292
+ @rememberBallots
293
+ def stratBallot(cls, voter):
294
+ stratGap = voter[targId] - voter[frontId]
295
+ ballot = [0] * len(voter)
296
+ isStrat = stratGap > 0
297
+ extras = cls.fillStratBallot(
298
+ voter,
299
+ polls,
300
+ places,
301
+ n,
302
+ stratGap,
303
+ ballot,
304
+ frontId,
305
+ frontResult,
306
+ targId,
307
+ targResult,
308
+ )
309
+ result = dict(strat=ballot, isStrat=isStrat, stratGap=stratGap)
310
+ if extras:
311
+ result.update(extras)
312
+ return result
313
+
314
+ return stratBallot
315
+
316
+
317
+ @decorator
318
+ def rememberBallot(fun):
319
+ """A decorator for a function of the form xxxBallot(cls, voter)
320
+ which memoizes the vote onto the voter in an attribute named <methName>_xxx
321
+ """
322
+
323
+ def getAndRemember(cls, voter, tally=None):
324
+ ballot = fun(cls, voter)
325
+ setattr(voter, f"{cls.__name__}_{fun.__name__[:-6]}", ballot)
326
+ return ballot
327
+
328
+ getAndRemember.__name__ = fun.__name__
329
+ getAndRemember.allTallyKeys = lambda: []
330
+ return getAndRemember
331
+
332
+
333
+ @decorator
334
+ def rememberBallots(fun):
335
+ """A decorator for a function of the form xxxBallot(cls, voter)
336
+ which memoizes the vote onto the voter in an attribute named <methName>_xxx
337
+ """
338
+
339
+ def getAndRemember(cls, voter, tally=None):
340
+ ballots = fun(cls, voter)
341
+ for bType, ballot in ballots.items():
342
+ setattr(voter, f"{cls.__name__}_{bType}", ballot)
343
+
344
+ return ballots[fun.__name__[:-6]] # leave off the "...Ballot"
345
+
346
+ getAndRemember.__name__ = fun.__name__
347
+ getAndRemember.allTallyKeys = lambda: []
348
+ return getAndRemember
349
+
350
+
351
+ class CandidateWithCount:
352
+ def __init__(self, c=[], v=0):
353
+ self.candidate = c
354
+ self.votes = v
debugDump.py ADDED
@@ -0,0 +1,11 @@
1
+ DEBUG = True
2
+
3
+
4
+ def debug(*args):
5
+ if DEBUG:
6
+ print(*args)
7
+
8
+
9
+ def setDebug(state):
10
+ global DEBUG
11
+ DEBUG = state