diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/NPoS/.gitignore b/NPoS/.gitignore new file mode 100644 index 0000000..9f94e5d --- /dev/null +++ b/NPoS/.gitignore @@ -0,0 +1,2 @@ +.vscode +__pycache__ diff --git a/NPoS/ComplicatedPhragmén.py b/NPoS/ComplicatedPhragmén.py index 561634d..f61efea 100644 --- a/NPoS/ComplicatedPhragmén.py +++ b/NPoS/ComplicatedPhragmén.py @@ -7,7 +7,7 @@ class edge: #self.index #self.voterindex #self.canindex - + class voter: def __init__(self,votetuple): @@ -83,7 +83,7 @@ class assignment: def unelect(self,candidate): self.canelected[candidate.index]=False self.electedcandidates.remove(candidate) - + def setuplists(votelist): #Instead of Python's dict here, you can use anything with O(log n) addition and lookup. #We can also use a hashmap, by generating a random constant r and useing H(canid+r) @@ -94,7 +94,7 @@ def setuplists(votelist): numcandidates=0 numvoters=0 numedges=0 - + #Get an array of candidates that we can reference these by index for nom in voterlist: nom.index=numvoters @@ -115,13 +115,13 @@ def setuplists(votelist): edge.canindex=numcandidates numcandidates += 1 return(voterlist,candidatearray) - + def seqPhragmén(votelist,numtoelect): nomlist,candidates=setuplists(votelist) #creating an assignment now also computes the total possible stake for each candidate a=assignment(nomlist,candidates) - + for round in range(numtoelect): for canindex in range(len(candidates)): if not a.canelected[canindex]: @@ -172,8 +172,8 @@ def calculateScores(a,cutoff): #if not a.canelected[canindex]: #print(a.candidates[canindex].canid," has score ", a.canscore[canindex]," with cutoff ",cutoff) #print("Approval stake: ", a.canapproval[canindex]," support: ",a.cansupport[canindex]," denominator: ",a.canscoredenominator[canindex], " numerator: ",a.canscorenumerator[canindex]) - - + + def calculateMaxScore(a): supportList=[a.cansupport[can.index] for can in a.electedcandidates] supportList.append(0.0) @@ -249,7 +249,7 @@ def equalise(a, nom, tolerance): # Attempts to redistribute the nominators budget between elected validators # Assumes that all elected validators have backedstake set correctly # returns the max difference in stakes between sup - + electededges=[edge for edge in nom.edges if a.canelected[edge.canindex]] if len(electededges)==0: return 0.0 @@ -310,7 +310,7 @@ def seqPhragménwithpostprocessing(votelist,numtoelect, ratio=1): def factor3point15(votelist, numtoelect,tolerance=0.1): nomlist,candidates=setuplists(votelist) a=assignment(nomlist,candidates) - + for round in range(numtoelect): bestcandidate,score=calculateMaxScore(a) insertWithScore(a,bestcandidate, score) @@ -325,7 +325,7 @@ def maybecandidate(a,newcandidate,shouldremoveworst, tolerance): if shouldremoveworst: worstcanidate =min(electedcandidates, key = lambda x: b.cansupport[x.index]) b.unelect(worstcandidate) - b.elect(newcandidate) + b.elect(newcandidate) equaliseall(b,100000000,tolerance) newvalue=min([b.cansupport[candidate.index] for candidate in b.electedcandidates]) return b, newvalue @@ -395,7 +395,7 @@ def binarysearchfeasible(votelist,numtoelect,tolerance=0.1): else: targetvalue=currentvalue #print(targetvalue,lastgoodindex, maxvalue,bestknownvalue,currentvalue) - + for round in range(lastgoodindex+1,numtoelect): # First try maxscore candidate, which will help with PJR @@ -460,22 +460,22 @@ def doall(votelist, numtoelect, listvoters=True, listcans=True): if listvoters: print("Votes ",votelist) alglist=[(approvalvoting,"Approval voting"), (seqPhragmén, "Sequential Phragmén"), - (seqPhragménwithpostprocessing, "Sequential Phragmén with post processing"), + (seqPhragménwithpostprocessing, "Sequential Phragmén with post processing"), (factor3point15, "The factor 3.15 thing"), (binarysearchfeasible,"Factor 2 by binary search"), (SFFB18, "SFFB18")] for alg,name in alglist: - st=time.perf_counter() + st=time.perf_counter() a = alg(votelist,numtoelect) - et=time.perf_counter() + et=time.perf_counter() print(name, " gives") printresult(a,listvoters,listcans) print(" in ",et-st," seconds.") print() - + def example1(): votelist=[("A",10.0,["X","Y"]),("B",20.0,["X","Z"]),("C",30.0,["Y","Z"])] doall(votelist,2) - + def example2(): # Approval voting does not do so well for this kind of thing. @@ -491,7 +491,7 @@ def example3(): bluevoters = [("BlueV"+str(i),20.0,blueparty) for i in range(20)] votelist= redvoters+bluevoters doall(votelist, 20, False) - + def example4(): #Now we want an example where seq Phragmén is not so good. @@ -518,7 +518,7 @@ def example6(): ("M",50.0, ["M"])] print("Votes ",votelist) doall(votelist,5) - + def exampleLine(): votelist = [ ("a", 2000, ["A"]), @@ -530,7 +530,7 @@ def exampleLine(): ("g", 1000, ["F","G"]) ] doall(votelist,7) - + def ri(vals=20,noms=2000, votesize=10): #Let's try a random instance candidates=["Val"+str(i) for i in range(vals)] @@ -560,28 +560,28 @@ def riparty(vals=200,noms=2000, votesize=10,seed=1): - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/NPoS/npos.py b/NPoS/npos.py new file mode 100644 index 0000000..eec386a --- /dev/null +++ b/NPoS/npos.py @@ -0,0 +1,428 @@ +import unittest + + +def print_list(ll): + for item in ll: + print(item) + + +class edge: + def __init__(self, nominator_id, validator_id): + self.nominator_id = nominator_id + self.validator_id = validator_id + self.load = 0 + self.weight = 0 + self.candidate = None + + def __str__(self): + return "Edge({}, weight = {:,})".format( + self.validator_id, + self.weight, + ) + + +class nominator: + def __init__(self, nominator_id, budget, targets): + self.nominator_id = nominator_id + self.budget = budget + self.edges = [edge(self.nominator_id, validator_id) for validator_id in targets] + self.load = 0 + + def __str__(self): + return "Nominator({}, budget = {:,}, load = {}, edges = {})".format( + self.nominator_id, + self.budget, + self.load, + [str(e) for e in self.edges] + ) + + +class candidate: + def __init__(self, validator_id, index): + self.validator_id = validator_id + self.valindex = index + self.approval_stake = 0 + self.backed_stake = 0 + self.elected = False + self.score = 0 + self.scoredenom = 0 + + def __str__(self): + return "Candidate({}, approval = {:,}, backed_stake = {:,})".format( + self.validator_id, + self.approval_stake, + int(self.backed_stake), + ) + + +def seq_phragmen(votelist, num_to_elect): + nomlist, candidates = setuplists(votelist) + calculate_approval(nomlist) + + elected_candidates = list() + for round in range(num_to_elect): + for candidate in candidates: + if not candidate.elected: + candidate.score = 1/candidate.approval_stake + for nom in nomlist: + for edge in nom.edges: + if not edge.candidate.elected: + edge.candidate.score += nom.budget * nom.load / edge.candidate.approval_stake + best_candidate = 0 + best_score = 1000 # should be infinite but I'm lazy + for candidate in candidates: + if not candidate.elected and candidate.score < best_score: + best_score = candidate.score + best_candidate = candidate.valindex + elected_candidate = candidates[best_candidate] + elected_candidate.elected = True + elected_candidate.electedpos = round + elected_candidates.append(elected_candidate) + for nom in nomlist: + for edge in nom.edges: + if edge.candidate.valindex == best_candidate: + edge.load = elected_candidate.score - nom.load + nom.load = elected_candidate.score + + for candidate in elected_candidates: + candidate.backed_stake = 0 + + for nom in nomlist: + for edge in nom.edges: + if nom.load > 0.0: + edge.weight = nom.budget * edge.load/nom.load + edge.candidate.backed_stake += edge.weight + else: + edge.weight = 0 + return (nomlist, elected_candidates) + + +def equalise(nom, tolerance): + # Attempts to redistribute the nominators budget between elected validators. Assumes that all + # elected validators have backed_stake set correctly. Returns the max difference in stakes + # between sup. + + elected_edges = [edge for edge in nom.edges if edge.candidate.elected] + + if len(elected_edges) < 2: + return 0.0 + + stake_used = sum([edge.weight for edge in elected_edges]) + backed_stakes = [edge.candidate.backed_stake for edge in elected_edges] + backingbacked_stakes = [ + edge.candidate.backed_stake for edge in elected_edges if edge.weight > 0.0 + ] + + if len(backingbacked_stakes) > 0: + difference = max(backingbacked_stakes)-min(backed_stakes) + difference += nom.budget - stake_used + if difference < tolerance: + return difference + else: + difference = nom.budget + + # remove all backing + for edge in nom.edges: + edge.candidate.backed_stake -= edge.weight + edge.weight = 0 + + elected_edges.sort(key=lambda x: x.candidate.backed_stake) + cumulative_backed_stake = 0 + last_index = len(elected_edges) - 1 + + for i in range(len(elected_edges)): + backed_stake = elected_edges[i].candidate.backed_stake + if backed_stake * i - cumulative_backed_stake > nom.budget: + last_index = i-1 + break + cumulative_backed_stake += backed_stake + + last_stake = elected_edges[last_index].candidate.backed_stake + ways_to_split = last_index+1 + excess = nom.budget + cumulative_backed_stake - last_stake*ways_to_split + + for edge in elected_edges[0:ways_to_split]: + edge.weight = excess / ways_to_split + last_stake - edge.candidate.backed_stake + edge.candidate.backed_stake += edge.weight + + return difference + + +def equalise_all(nomlist, maxiterations, tolerance): + for i in range(maxiterations): + # for j in range(len(nomlist)): + # nom = random.choice(nomlist) + # equalise(nom, tolerance) + maxdifference = 0 + for nom in nomlist: + difference = equalise(nom, tolerance) + maxdifference = max(difference, maxdifference) + if maxdifference < tolerance: + return + + +def seq_phragmen_with_equalise(votelist, num_to_elect): + nomlist, elected_candidates = seq_phragmen(votelist, num_to_elect) + equalise_all(nomlist, 2, 0) + return nomlist, elected_candidates + + +def calculateMaxScoreNoCutoff(nomlist, candidates): + # First we compute the denominator of the score + for candidate in candidates: + if not candidate.elected: + candidate.scoredenom = 1.0 + + for nom in nomlist: + denominator_contrib = 0 + + for edge in nom.edges: + if edge.candidate.elected: + denominator_contrib += edge.weight/edge.candidate.backed_stake + + for edge in nom.edges: + if not edge.candidate.elected: + edge.candidate.scoredenom += denominator_contrib + + # Then we divide. Not that score here is comparable to the recipricol of the score in + # seq-phragmen. In particular there low scores are good whereas here high scores are good. + best_candidate = 0 + best_score = 0.0 + for candidate in candidates: + if candidate.approval_stake > 0.0: + candidate.score = candidate.approval_stake / candidate.scoredenom + if not candidate.elected and candidate.score > best_score: + best_score = candidate.score + best_candidate = candidate + else: + candidate.score = 0.0 + + return (best_candidate, best_score) + + +def electWithScore(nomlist, elected_candidate, cutoff): + for nom in nomlist: + for new_edge in nom.edges: + if new_edge.validator_id == elected_candidate.validator_id: + used_budget = sum([edge.weight for edge in nom.edges]) + + new_edge.weight = nom.budget - used_budget + elected_candidate.backed_stake += nom.budget - used_budget + + for edge in nom.edges: + if edge.validator_id != elected_candidate.validator_id and edge.weight > 0.0: + if edge.candidate.backed_stake > cutoff: + stake_to_take = edge.weight * cutoff / edge.candidate.backed_stake + + new_edge.weight += stake_to_take + elected_candidate.backed_stake += stake_to_take + + edge.weight -= stake_to_take + edge.candidate.backed_stake -= stake_to_take + + +def phragmms(votelist, num_to_elect, tolerance=0.1): + nomlist, candidates = setuplists(votelist) + calculate_approval(nomlist) + + elected_candidates = list() + for round in range(num_to_elect): + (elected_candidate, score) = calculateMaxScoreNoCutoff(nomlist, candidates) + electWithScore(nomlist, elected_candidate, score) + + elected_candidate.elected = True + elected_candidates.append(elected_candidate) + elected_candidate.electedpos = round + + equalise_all(nomlist, 10, tolerance) + + return nomlist, elected_candidates + + +def approval_voting(votelist, num_to_elect): + nomlist, candidates = setuplists(votelist) + # Compute the total possible stake for each candidate + for nom in nomlist: + for edge in nom.edges: + edge.candidate.approval_stake += nom.budget + edge.weight = nom.budget/min(len(nom.edges), num_to_elect) + edge.candidate.backed_stake += edge.weight + candidates.sort(key=lambda x: x.approval_stake, reverse=True) + elected_candidates = candidates[0:num_to_elect] + return nomlist, elected_candidates + + +def calculate_approval(nomlist): + for nom in nomlist: + for edge in nom.edges: + edge.candidate.approval_stake += nom.budget + + +def setuplists(votelist): + ''' + Basically populates edge.candidate, and returns nomlist and candidate array. The former is a + flat list of nominators and the latter is a flat list of validator candidates. + + Instead of Python's dict here, you can use anything with O(log n) addition and lookup. We can + also use a hashmap like dict, by generating a random constant r and useing H(canid+r) since the + naive thing is obviously attackable. + ''' + nomlist = [nominator(votetuple[0], votetuple[1], votetuple[2]) for votetuple in votelist] + # Basically used as a cache. + candidate_dict = dict() + candidate_array = list() + num_candidates = 0 + # Get an array of candidates.# We could reference these by index rather than pointer + for nom in nomlist: + for edge in nom.edges: + validator_id = edge.validator_id + if validator_id in candidate_dict: + index = candidate_dict[validator_id] + edge.candidate = candidate_array[index] + else: + candidate_dict[validator_id] = num_candidates + newcandidate = candidate(validator_id, num_candidates) + candidate_array.append(newcandidate) + + edge.candidate = newcandidate + num_candidates += 1 + return nomlist, candidate_array + + +def run_and_print_all(votelist, to_elect): + print("######\nVotes ", votelist) + + print("\nSequential Phragmén gives") + nomlist, elected_candidates = seq_phragmen(votelist, to_elect) + printresult(nomlist, elected_candidates) + + print("\nApproval voting gives") + nomlist, elected_candidates = approval_voting(votelist, to_elect) + printresult(nomlist, elected_candidates) + + print("\nSequential Phragmén with post processing gives") + nomlist, elected_candidates = seq_phragmen_with_equalise(votelist, to_elect) + printresult(nomlist, elected_candidates) + + print("\nBalanced Heuristic (3.15 factor) gives") + nomlist, elected_candidates = phragmms(votelist, to_elect) + printresult(nomlist, elected_candidates) + + +def printresult(nomlist, elected_candidates, verbose=True): + for candidate in elected_candidates: + print(candidate.validator_id, " is elected with stake ", + candidate.backed_stake, "and score ", candidate.score) + if verbose: + for nom in nomlist: + print(nom.nominator_id, " has load ", nom.load, "and supported ") + for edge in nom.edges: + print(edge.validator_id, " with stake ", edge.weight, end=", ") + print() + print() + + +def example1(): + votelist = [ + ("A", 10.0, ["X", "Y"]), + ("B", 20.0, ["X", "Z"]), + ("C", 30.0, ["Y", "Z"]), + ] + run_and_print_all(votelist, 2) + + +def example2(): + votelist = [ + ("10", 1000, ["10"]), + ("20", 1000, ["20"]), + ("30", 1000, ["30"]), + ("40", 1000, ["40"]), + ('2', 500, ['10', '20', '30']), + ('4', 500, ['10', '20', '40']) + ] + run_and_print_all(votelist, 2) + + +class MaxScoreTest(unittest.TestCase): + def test_max_score_1(self): + votelist = [ + (10, 10.0, [1, 2]), + (20, 20.0, [1, 3]), + (30, 30.0, [2, 3]), + ] + nomlist, candidates = setuplists(votelist) + calculate_approval(nomlist) + + best, score = calculateMaxScoreNoCutoff(nomlist, candidates) + self.assertEqual(best.validator_id, 3) + self.assertEqual(score, 50) + + def test_balance_heuristic_example_1(self): + votelist = [ + (10, 10.0, [1, 2]), + (20, 20.0, [1, 3]), + (30, 30.0, [2, 3]), + ] + nomlist, winners = phragmms(votelist, 2, 0) + self.assertEqual(winners[0].validator_id, 3) + self.assertEqual(winners[1].validator_id, 2) + + self.assertEqual(winners[0].backed_stake, 30) + self.assertEqual(winners[1].backed_stake, 30) + + def test_balance_heuristic_example_linear(self): + votelist = [ + (2, 2000, [11]), + (4, 1000, [11, 21]), + (6, 1000, [21, 31]), + (8, 1000, [31, 41]), + (110, 1000, [41, 51]), + (120, 1000, [51, 61]), + (130, 1000, [61, 71]), + ] + + nomlist, winners = phragmms(votelist, 4, 0) + self.assertEqual(winners[0].validator_id, 11) + self.assertEqual(winners[0].backed_stake, 3000) + + self.assertEqual(winners[1].validator_id, 31) + self.assertEqual(winners[1].backed_stake, 2000) + + self.assertEqual(winners[2].validator_id, 51) + self.assertEqual(winners[2].backed_stake, 1500) + + self.assertEqual(winners[3].validator_id, 61) + self.assertEqual(winners[3].backed_stake, 1500) + + + +class ElectionTest(unittest.TestCase): + def test_phragmen(self): + votelist = [ + ("A", 10.0, ["X", "Y"]), + ("B", 20.0, ["X", "Z"]), + ("C", 30.0, ["Y", "Z"]), + ] + nomlist, elected_candidates = seq_phragmen(votelist, 2) + self.assertEqual(elected_candidates[0].validator_id, "Z") + self.assertAlmostEqual(elected_candidates[0].score, 0.02) + self.assertEqual(elected_candidates[1].validator_id, "Y") + self.assertAlmostEqual(elected_candidates[1].score, 0.04) + + def test_approval(self): + votelist = [ + ("A", 10.0, ["X", "Y"]), + ("B", 20.0, ["X", "Z"]), + ("C", 30.0, ["Y", "Z"]), + ] + nomlist, elected_candidates = approval_voting(votelist, 2) + self.assertEqual(elected_candidates[0].validator_id, "Z") + self.assertAlmostEqual(elected_candidates[0].approval_stake, 50.0) + self.assertEqual(elected_candidates[1].validator_id, "Y") + self.assertAlmostEqual(elected_candidates[1].approval_stake, 40.0) + + +def main(): + # example1() + example2() + # example3() diff --git a/NPoS/remote_test.py b/NPoS/remote_test.py new file mode 100644 index 0000000..94a773f --- /dev/null +++ b/NPoS/remote_test.py @@ -0,0 +1,92 @@ +import npos +import pprint +from substrateinterface import SubstrateInterface +from substrateinterface.utils.ss58 import ss58_decode, ss58_encode + + +pp = pprint.PrettyPrinter(indent=4) + +substrate = SubstrateInterface( + url="ws://localhost:9944", + address_type=0, + type_registry={'types': { + "StakingLedger": { + "type": "struct", + "type_mapping": [ + ["stash", "AccountId"], + ["total", "Compact"], + ["active", "Compact"], + ["unlocking", "Vec>"], + ["claimedReward", "Vec"] + ] + }, + } + }, + type_registry_preset='polkadot', +) + +head = substrate.get_chain_finalised_head() + + +def get_candidates(): + prefix = substrate.generate_storage_hash("Staking", "Validators") + pairs = substrate.rpc_request(method="state_getPairs", params=[prefix, head])['result'] + last_32_bytes = list(map(lambda p: "0x" + p[0][-64:], pairs)) + return list(map(lambda k: ss58_encode(k), last_32_bytes)) + + +def get_nominators(): + prefix = substrate.generate_storage_hash("Staking", "Nominators") + pairs = substrate.rpc_request(method="state_getPairs", params=[prefix, head])['result'] + + nominators = list( + map(lambda p: ("0x" + p[0][-64:], substrate.decode_scale("Nominations", p[1])['targets']), pairs) + ) + + nominators = list(map(lambda x: ( + ss58_encode(x[0], substrate.address_type), + x[1], + ), nominators)) + + return list(map(lambda x: ( + x[0], + get_backing_stake_of(x[0]), + [ss58_encode(acc, substrate.address_type) for acc in x[1]], + ), nominators)) + + +def get_backing_stake_of(who): + ctrl = substrate.get_runtime_state( + module="Staking", + storage_function="Bonded", + params=[who], + block_hash=head, + )['result'] + + ctrl = ss58_encode(ctrl, substrate.address_type) + + ledger = substrate.get_runtime_state( + module="Staking", + storage_function="Ledger", + params=[ctrl], + block_hash=head, + )['result'] + + return ledger['active'] + + +def validator_count(): + return substrate.get_runtime_state("Staking", "ValidatorCount", [], head)['result'] + + +candidates = get_candidates() +nominators = get_nominators() +to_elect = validator_count() + +print("{} validators, {} nominators, electing {}".format(len(candidates), len(nominators), to_elect)) +distribution, winners = npos.phragmms(nominators, to_elect) +npos.print_list(winners) +print(sum([c.backed_stake for c in winners])) +print(min([c.backed_stake for c in winners])) +score = [min([c.backed_stake for c in winners]), sum([c.backed_stake for c in winners])] +print("score = [{:,}, {:,}]".format(int(score[0]), int(score[1]))) diff --git a/NPoS/setup.cfg b/NPoS/setup.cfg new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/NPoS/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/NPoS/simplePhragmén.py b/NPoS/simplePhragmén.py deleted file mode 100644 index 60be278..0000000 --- a/NPoS/simplePhragmén.py +++ /dev/null @@ -1,329 +0,0 @@ -#from itertools import count -class edge: - def __init__(self,nomid,valiid): - self.nomid=nomid - self.valiid=valiid - #self.validator - self.load=0 - self.weight=0 - -class nominator: - def __init__(self,votetuple): - self.nomid=votetuple[0] - self.budget=votetuple[1] - self.edges=[edge(self.nomid,valiid) for valiid in votetuple[2]] - self.load=0 - -class candidate: - def __init__(self,valiid,valindex): - self.valiid = valiid - self.valindex=valindex - self.approvalstake=0 - self.elected=False - self.backedstake=0 - self.score=0 - self.scoredenom=0 - -def setuplists(votelist): - #Instead of Python's dict here, you can use anything with O(log n) addition and lookup. - #We can also use a hashmap like dict, by generating a random constant r and useing H(canid+r) - #since the naive thing is obviously attackable. - nomlist = [nominator(votetuple) for votetuple in votelist] - candidatedict=dict() - candidatearray=list() - numcandidates=0 - #Get an array of candidates. ]#We could reference these by index - #rather than pointer - for nom in nomlist: - for edge in nom.edges: - valiid = edge.valiid - if valiid in candidatedict: - edge.candidate=candidatearray[candidatedict[valiid]] - else: - candidatedict[valiid]=numcandidates - newcandidate=candidate(valiid,numcandidates) - candidatearray.append(newcandidate) - edge.candidate=newcandidate - numcandidates += 1 - return(nomlist,candidatearray) - - -def seqPhragmén(votelist,numtoelect): - nomlist,candidates=setuplists(votelist) - #Compute the total possible stake for each candidate - for nom in nomlist: - for edge in nom.edges: - edge.candidate.approvalstake += nom.budget - - electedcandidates=list() - for round in range(numtoelect): - for candidate in candidates: - if not candidate.elected: - candidate.score=1/candidate.approvalstake - for nom in nomlist: - for edge in nom.edges: - if not edge.candidate.elected: - edge.candidate.score +=nom.budget * nom.load / edge.candidate.approvalstake - bestcandidate=0 - bestscore = 1000 #should be infinite but I'm lazy - for candidate in candidates: - if not candidate.elected and candidate.score < bestscore: - bestscore=candidate.score - bestcandidate=candidate.valindex - electedcandidate=candidates[bestcandidate] - electedcandidate.elected=True - electedcandidate.electedpos=round - electedcandidates.append(electedcandidate) - for nom in nomlist: - for edge in nom.edges: - if edge.candidate.valindex == bestcandidate: - edge.load=electedcandidate.score - nom.load - nom.load=electedcandidate.score - - for candidate in electedcandidates: - candidate.backedstake=0 - - for nom in nomlist: - for edge in nom.edges: - if nom.load > 0.0: - edge.weight = nom.budget * edge.load/nom.load - edge.candidate.backedstake += edge.weight - else: - edge.weight = 0 - return (nomlist,electedcandidates) - -def calculateMaxScoreNoCutoff(nomlist,candidates): - # First we compute the denominator of the score - for candidate in candidates: - if not candidate.elected: - candidate.scoredenom=1.0 - for nom in nomlist: - denominatorcontrib = 0 - for edge in nom.edges: - if edge.candidate.elected: - denominatorcontrib += edge.weight/edge.candidate.backedstake - # print(nom.nomid, denominatorcontrib) - for edge in nom.edges: - if not edge.candidate.elected: - edge.candidate.scoredenom += denominatorcontrib - # print(edge.candidate.valiid, nom.nomid, denominatorcontrib, edge.candidate.scoredenom) - # Then we divide. Not that score here is comparable to the recipricol of the score in seqPhragmen. - # In particular there low scores are good whereas here high scores are good. - bestcandidate=0 - bestscore = 0.0 - for candidate in candidates: - # print(candidate.valiid, candidate.approvalstake, candidate.scoredenom) - if candidate.approvalstake > 0.0: - candidate.score = candidate.approvalstake/candidate.scoredenom - if not candidate.elected and candidate.score > bestscore: - bestscore=candidate.score - bestcandidate=candidate - else: - candidate.score=0.0 - # print(len(candidates), bestcandidate, bestscore) - return (bestcandidate,bestscore) - -def electWithScore(nomlist, electedcandidate, cutoff): - for nom in nomlist: - for newedge in nom.edges: - if newedge.valiid == electedcandidate.valiid: - usedbudget = sum([edge.weight for edge in nom.edges]) - newedge.weight = nom.budget-usedbudget - electedcandidate.backedstake += nom.budget-usedbudget - for edge in nom.edges: - if edge.valiid != electedcandidate.valiid and edge.weight > 0.0: - if edge.candidate.backedstake > cutoff: - staketotake = edge.weight * cutoff / edge.candidate.backedstake - newedge.weight += staketotake - edge.weight -= staketotake - edge.candidate.backedstake -= staketotake - electedcandidate.backedstake += staketotake - - -def approvalvoting(votelist,numtoelect): - nomlist,candidates=setuplists(votelist) - #Compute the total possible stake for each candidate - for nom in nomlist: - for edge in nom.edges: - edge.candidate.approvalstake += nom.budget - edge.weight = nom.budget/min(len(nom.edges),numtoelect) - edge.candidate.backedstake += edge.weight - candidates.sort( key = lambda x : x.approvalstake, reverse=True) - electedcandidates=candidates[0:numtoelect] - return nomlist,electedcandidates - -def printresult(nomlist,electedcandidates): - for candidate in electedcandidates: - print(candidate.valiid," is elected with stake ",candidate.backedstake, "and score ",candidate.score) - print() - for nom in nomlist: - print(nom.nomid," has load ",nom.load, "and supported ") - for edge in nom.edges: - print(edge.valiid," with stake ",edge.weight, end=" ") - print() - -def equalise(nom, tolerance): - # Attempts to redistribute the nominators budget between elected validators - # Assumes that all elected validators have backedstake set correctly - # returns the max difference in stakes between sup - - electededges=[edge for edge in nom.edges if edge.candidate.elected] - if len(electededges)==0: - return 0.0 - stakeused = sum([edge.weight for edge in electededges]) - backedstakes=[edge.candidate.backedstake for edge in electededges] - backingbackedstakes=[edge.candidate.backedstake for edge in electededges if edge.weight > 0.0] - if len(backingbackedstakes) > 0: - difference = max(backingbackedstakes)-min(backedstakes) - difference += nom.budget-stakeused - if difference < tolerance: - return difference - else: - difference = nom.budget - #remove all backing - for edge in nom.edges: - edge.candidate.backedstake -= edge.weight - edge.weight=0 - electededges.sort(key=lambda x: x.candidate.backedstake) - cumulativebackedstake=0 - lastcandidateindex=len(electededges)-1 - for i in range(len(electededges)): - backedstake=electededges[i].candidate.backedstake - #print(nom.nomid,electededges[i].valiid,backedstake,cumulativebackedstake,i) - if backedstake * i - cumulativebackedstake > nom.budget: - lastcandidateindex=i-1 - break - cumulativebackedstake +=backedstake - laststake=electededges[lastcandidateindex].candidate.backedstake - waystosplit=lastcandidateindex+1 - excess = nom.budget + cumulativebackedstake - laststake*waystosplit - for edge in electededges[0:waystosplit]: - edge.weight = excess / waystosplit + laststake - edge.candidate.backedstake - edge.candidate.backedstake += edge.weight - return difference - -import random -def equaliseall(nomlist,maxiterations,tolerance): - for i in range(maxiterations): - for j in range(len(nomlist)): - nom=random.choice(nomlist) - equalise(nom,tolerance/10) - maxdifference=0 - for nom in nomlist: - difference=equalise(nom,tolerance/10) - maxdifference=max(difference,maxdifference) - if maxdifference < tolerance: - return - -def seqPhragménwithpostprocessing(votelist,numtoelect): - nomlist,electedcandidates = seqPhragmén(votelist,numtoelect) - equaliseall(nomlist,2,0.1) - return nomlist,electedcandidates - -def factor3point15(votelist, numtoelect,tolerance=0.1): - nomlist,candidates=setuplists(votelist) - # Compute the total possible stake for each candidate - for nom in nomlist: - for edge in nom.edges: - edge.candidate.approvalstake += nom.budget - - electedcandidates=list() - for round in range(numtoelect): - electedcandidate,score=calculateMaxScoreNoCutoff(nomlist,candidates) - electWithScore(nomlist, electedcandidate, score) - electedcandidate.elected=True - electedcandidates.append(electedcandidate) - electedcandidate.electedpos=round - equaliseall(nomlist,100,tolerance) - return nomlist,electedcandidates - -def example1(): - votelist=[("A",10.0,["X","Y"]),("B",20.0,["X","Z"]),("C",30.0,["Y","Z"])] - print("Votes ",votelist) - nomlist, electedcandidates = seqPhragmén(votelist,2) - print("Sequential Phragmén gives") - printresult(nomlist, electedcandidates) - nomlist, electedcandidates = approvalvoting(votelist,2) - print() - print("Approval voting gives") - printresult(nomlist, electedcandidates) - nomlist, electedcandidates = seqPhragménwithpostprocessing(votelist,2) - print("Sequential Phragmén with post processing gives") - printresult(nomlist, electedcandidates) - nomlist, electedcandidates = factor3point15(votelist,2) - print("Factor 3.15 thing gives") - printresult(nomlist, electedcandidates) - -def example2(): - votelist = [ - ("10", 1000, ["10"]), - ("20", 1000, ["20"]), - ("30", 1000, ["30"]), - ("40", 1000, ["40"]), - ('2', 500, ['10', '20', '30']), - ('4', 500, ['10', '20', '40']) - ] - print("Votes ",votelist) - nomlist, electedcandidates = seqPhragmén(votelist,2) - print("Sequential Phragmén gives") - printresult(nomlist, electedcandidates) - nomlist, electedcandidates = approvalvoting(votelist,2) - print() - print("Approval voting gives") - printresult(nomlist, electedcandidates) - nomlist, electedcandidates = seqPhragménwithpostprocessing(votelist,2) - print("Sequential Phragmén with post processing gives") - printresult(nomlist, electedcandidates) - nomlist, electedcandidates = factor3point15(votelist,2) - print("Factor 3.15 thing gives") - printresult(nomlist, electedcandidates) - -import unittest -class electiontests(unittest.TestCase): - def testexample1Phragmén(self): - votelist=[("A",10.0,["X","Y"]),("B",20.0,["X","Z"]),("C",30.0,["Y","Z"])] - nomlist, electedcandidates = seqPhragmén(votelist,2) - self.assertEqual(electedcandidates[0].valiid,"Z") - self.assertAlmostEqual(electedcandidates[0].score,0.02) - self.assertEqual(electedcandidates[1].valiid,"Y") - self.assertAlmostEqual(electedcandidates[1].score,0.04) - def testexample1approval(self): - votelist=[("A",10.0,["X","Y"]),("B",20.0,["X","Z"]),("C",30.0,["Y","Z"])] - nomlist, electedcandidates = approvalvoting(votelist,2) - self.assertEqual(electedcandidates[0].valiid,"Z") - self.assertAlmostEqual(electedcandidates[0].approvalstake,50.0) - self.assertEqual(electedcandidates[1].valiid,"Y") - self.assertAlmostEqual(electedcandidates[1].approvalstake,40.0) -def dotests(): - unittest.main() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -