New to this version 3 model are the following elements:
Conviction Voting is a real time vote streaming tool, and a novel approach to continuously organizing a communities preferences into discrete decisions in the management of that communities resources.
Suppose a group of people want to coordinate to make a collective decision. Social dynamics such as discussions, signaling, and even changing one's mind based on feedback from other's input play an important role in these processes. Conviction Voting can augment these social dynamics by making group preferences more transparent, and channels a plurality of preferences into discrete outcomes. In our case we are interested in a procedure which supports asynchronous interactions, and provides visibility into likely outcomes prior to their resolution to serve as a driver of debate and healthy forms of coalition building. Furthermore, participants should be able to show support for multiple initiatives, and to vary the level of support shown. Participants have a quantity of signaling power which may be fixed or variable, homogenous (e.g. democratic) or heterogenous (e.g. plutocratic or meritocratic).
Strictly speaking conviction voting is less like voting and more like a signal processing tool. Framing the approach and the initial algorithm design was done by Michael Zargham and published in a short research proposal Social Sensor Fusion. This work is based on a dynamic resource allocation algorithm presented in Dr. Zargham's PhD Thesis.
The work proceeded in collaboration with the Commons Stack, including expanding on the python implementation to make up part of the Commons Simulator game. An implemention of Conviction Voting as a smart contract within the Aragon Framework was developed by 1Hive.org and is currently being used for community decision making for allocations of their community currency, Honey.
For a more in-depth look at the mathematical derivation of Conviction Voting, and to get a better understanding of its various parameters and how they are defined, be sure to read more in the algorithm_overview.ipynb.
Understanding the derivations of the Conviction Voting parameters can give you insight into how changing them may impact your system. The $\alpha$ parameter regulates the half life decay rate of conviction. See the Deriving_Alpha notebook for a full derivation and more details about alpha.
Conviction can be considered like a fluctuating kinetic energy, with the Trigger function acting as a required activation energy for proposals to pass. This is the mechanism by which a continuous community preference turns into a discrete action event: passing a proposal. See Trigger Function Explanation for more details around the trigger function and how it works.
System has the right to do direct mints: $$F^+ = F + minted tokens$$ $$S^+ = S + minted tokens$$
The system may also see the arrival of new funds which come from outside supply and are donated to the funding pool: $$L^+ = L - donated tokens$$ $$F^+ = F + donated tokens$$
When tokens are added to a liquidity pool or cold wallet and removed from staking on proposals: $$L^+ = L + tokens$$ $$E^+ = E - tokens$$
When tokens are removed from a liquidity pool or cold wallet and staked towards proposals: $$L^+ = L - tokens$$ $$E^+ = E + tokens$$
Tokens in $L$ or $E$ are defined at the level of the account holding them.
If you are looking to build upon this model or others like it, you may want to read more about the structure of this cadCAD model in the model_structure.ipynb file.
from model import config
from model.parts.sys_params import initial_values
from model.parts.utils import *
Params (config.py) : {'beta': 0.2, 'rho': 0.0025, 'alpha': 0.7937005259840998, 'gamma': 0.001, 'sensitivity': 0.75, 'tmin': 1, 'min_supp': 1, 'base_completion_rate': 45, 'base_failure_rate': 180, 'base_engagement_rate': 0.3, 'lowest_affinity_to_support': 0.3}
We are loading the state_variables and configuration from the state_variables and the config python files. If you would like to change these initial values, please modify prior to running this notebook or restart the kernal and run all to see your changes.
from copy import deepcopy
from cadCAD import configs
# pull out configurations to illustrate
sim_config,state_variables,partial_state_update_blocks = config.get_configs()
state_variables['network'] = config_initialization(configs,initial_values)
Params (config.py) : {'beta': 0.2, 'rho': 0.0025, 'alpha': 0.7937005259840998, 'gamma': 0.001, 'sensitivity': 0.75, 'tmin': 1, 'min_supp': 1, 'base_completion_rate': 45, 'base_failure_rate': 180, 'base_engagement_rate': 0.3, 'lowest_affinity_to_support': 0.3}
partial_state_update_blocks
[{'policies': {'random': <function model.parts.system.driving_process(params, step, sL, s)>}, 'variables': {'network': <function model.parts.system.update_network(params, step, sL, s, _input)>, 'effective_supply': <function model.parts.system.increment_supply(params, step, sL, s, _input)>}}, {'policies': {'random': <function model.parts.system.minting_rule(params, step, sL, s)>}, 'variables': {'total_supply': <function model.parts.system.mint_to_supply(params, step, sL, s, _input)>, 'funds': <function model.parts.system.mint_to_funds(params, step, sL, s, _input)>}}, {'policies': {'completion': <function model.parts.participants.check_progress(params, step, sL, s)>}, 'variables': {'sentiment': <function model.parts.participants.update_sentiment_on_completion(params, step, sL, s, _input)>, 'network': <function model.parts.participants.complete_proposal(params, step, sL, s, _input)>}}, {'policies': {'release': <function model.parts.proposals.trigger_function(params, step, sL, s)>}, 'variables': {'funds': <function model.parts.proposals.decrement_funds(params, step, sL, s, _input)>, 'sentiment': <function model.parts.proposals.update_sentiment_on_release(params, step, sL, s, _input)>, 'network': <function model.parts.proposals.update_proposals(params, step, sL, s, _input)>}}, {'policies': {'participants_act': <function model.parts.participants.participants_decisions(params, step, sL, s)>}, 'variables': {'network': <function model.parts.participants.update_tokens(params, step, sL, s, _input)>}}, {'policies': {'calculations': <function model.parts.metrics.kpi_calculations(params, step, sL, s)>}, 'variables': {'fractionOfSupplyForVoting': <function model.parts.metrics.kpi_fractionOfSupplyForVoting(params, step, sL, s, _input)>, 'fractionOfSupplyInPool': <function model.parts.metrics.kpi_fractionOfSupplyInPool(params, step, sL, s, _input)>, 'fractionOfProposalStages': <function model.parts.metrics.kpi_proposal_stages(params, step, sL, s, _input)>, 'fractionOfFundStages': <function model.parts.metrics.kpi_fractionOfFundStages(params, step, sL, s, _input)>}}]
Initial values are the starting values for the simulation.
initial_values
{'sentiment': 0.6, 'n': 30, 'm': 7, 'funds': 4867.21, 'supply': 22392.22}
$n$ is initial participants, whereas $m$ is initial proposals.
Sim_config holds the global hyperparameters for the simulations
sim_config[0]['M']
{'beta': 0.2, 'rho': 0.0025, 'alpha': 0.7937005259840998, 'gamma': 0.001, 'sensitivity': 0.75, 'tmin': 1, 'min_supp': 1, 'base_completion_rate': 45, 'base_failure_rate': 180, 'base_engagement_rate': 0.3, 'lowest_affinity_to_support': 0.3}
Initial state variable values
state_variables
{'network': <networkx.classes.digraph.DiGraph at 0x7fc1d74c2ca0>, 'funds': 4867.21, 'sentiment': 0.6, 'effective_supply': 14020.008000000002, 'total_supply': 22392.22, 'fractionOfSupplyForVoting': 0, 'fractionOfSupplyInPool': 0, 'fractionOfProposalStages': 0, 'fractionOfFundStages': 0}
A graph is a type of temporal data structure that evolves over time. A graph $\mathcal{G}(\mathcal{V},\mathcal{E})$ consists of vertices or nodes, $\mathcal{V} = \{1...\mathcal{V}\}$ and is connected by edges $\mathcal{E} \subseteq \mathcal{V} \times \mathcal{V}$.
See Schema of the states above for more details
Let's explore!
# To explore our model prior to the simulation, we extract key components from our networkX object into lists.
proposals = get_nodes_by_type(state_variables['network'], 'proposal')
participants = get_nodes_by_type(state_variables['network'], 'participant')
supporters = get_edges_by_type(state_variables['network'], 'support')
influencers = get_edges_by_type(state_variables['network'], 'influence')
competitors = get_edges_by_type(state_variables['network'], 'conflict')
#sample a participant
state_variables['network'].nodes[participants[0]]
{'type': 'participant', 'holdings': 102.82251653764003, 'sentiment': 0.4549373377681203}
# Let's look at the distribution of participant holdings at the start of the sim
plt.hist([ state_variables['network'].nodes[i]['holdings'] for i in participants])
plt.title('Histogram of Participants Token Holdings')
plt.xlabel('Amount of Honey')
plt.ylabel('Count of Participants')
Text(0, 0.5, 'Count of Participants')
nx.draw_spring(state_variables['network'], nodelist = participants, edgelist=influencers)
plt.title('Participants Social Network')
Text(0.5, 1.0, 'Participants Social Network')
#lets look at proposals
state_variables['network'].nodes[proposals[0]]
{'type': 'proposal', 'conviction': 0, 'status': 'candidate', 'age': 0, 'funds_requested': 165.22075939857623, 'trigger': 7961.202738430356}
Proposals initially start without any conviction, and with the status of a candidate. If the proposal's amount of conviction is greater than its trigger, then the proposal moves to active and its funds requested are granted.
All initial proposals start with 0 conviction and state 'candidate' so we can simply examine the amounts of funds requested
funds_array = np.array([state_variables['network'].nodes[i]['funds_requested'] for i in proposals])
conviction_required = np.array([trigger_threshold(r, initial_values['funds'], initial_values['supply'], sim_config[0]['M']['alpha'],sim_config[0]['M']) for r in funds_array])
plt.bar( proposals, funds_array/initial_values['funds'])
plt.title('Bar chart of Proposals Funds Requested')
plt.xlabel('Proposal identifier')
plt.ylabel('Amount of Honey requested(as a Fraction of Funds available)')
Text(0, 0.5, 'Amount of Honey requested(as a Fraction of Funds available)')
Small proposals will all be similar and big proposals will go up a lot. See the Alpha explainer file for more explanation. As you can see below, the largest proposal is 0.175 of the max conviction allowed, which will let them pass relatively quickly.
plt.bar(proposals, conviction_required / (state_variables['effective_supply']/(1-sim_config[0]['M']['alpha'])))
plt.title('Bar chart of Proposals as Percentage of Max Conviction Conviction')
plt.xlabel('Proposal identifier')
plt.ylabel('Percentage of Max Conviction')
Text(0, 0.5, 'Percentage of Max Conviction')
plt.bar(proposals, conviction_required)
plt.title('Bar chart of Proposals Conviction Required')
plt.xlabel('Proposal identifier')
plt.ylabel('Amount of Conviction')
Text(0, 0.5, 'Amount of Conviction')
Conviction is a concept that arises in the edges between participants and proposals. In the initial conditions there are no votes yet so we can look at that later however, the voting choices are driven by underlying affinities which we can see now.
affinities_plot(state_variables['network'], dims = (20, 5))
Now we will create the final system configuration, append the genesis states we created, and run our simulation.
import numpy as np
import pandas as pd
from model import run
pd.options.display.float_format = '{:.2f}'.format
%matplotlib inline
rdf = run.run()
___________ ____ ________ __ ___/ / ____/ | / __ \ / ___/ __` / __ / / / /| | / / / / / /__/ /_/ / /_/ / /___/ ___ |/ /_/ / \___/\__,_/\__,_/\____/_/ |_/_____/ by cadCAD Execution Mode: local_proc Configuration Count: 1 Dimensions of the first simulation: (Timesteps, Params, Runs, Vars) = (100, 11, 1, 9) Execution Method: local_simulations SimIDs : [0] SubsetIDs: [0] Ns : [0] ExpIDs : [0] Execution Mode: single_threaded Total execution time: 66.83s
from model.state_schema import state_schema
schema_check(rdf,state_schema)
{'network': True, 'funds': True, 'sentiment': True, 'effective_supply': True, 'total_supply': True, 'fractionOfSupplyForVoting': True, 'fractionOfSupplyInPool': True, 'fractionOfProposalStages': True, 'fractionOfFundStages': True}
The check returns the same datatypes as expected, which passes one of our controls that the simulation performed as anticipated
After the simulation has run successfully, and we have checked the schema, we perform some postprocessing to extract node and edge values from the network object and add as columns to the pandas dataframe. For the rdf, we take only the values at the last substep of each timestep in the simulation.
df= run.postprocessing(rdf,0)
df.head(5)
network | funds | sentiment | effective_supply | total_supply | fractionOfSupplyForVoting | fractionOfSupplyInPool | fractionOfProposalStages | fractionOfFundStages | simulation | ... | age_all | conviction_all | triggers_all | conviction_share_of_trigger_all | percentageOfActiveProposals | percentageOfCompletedProposals | percentageOfKilledProposals | percentageOfActiveFundsRequested | percentageOfCompletedFundsRequested | percentageOfKilledFundsRequested | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
6 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | 4889.60 | 0.60 | 14020.01 | 22414.61 | 0.63 | 4.58 | {'percentageOfActive': 0.0, 'percentageOfCompl... | {'percentageOfActiveFundsRequested': 0.0, 'per... | 0 | ... | [1, 1, 1, 1, 1, 1, 1, 1] | [315.0028534719921, 0.0, 0.0, 413.515643822682... | [6161.5474755421565, 4606.307168380783, 5006.2... | [0.0511239838242543, 0.0, 0.0, 0.0534461876603... | 0.00 | 0.00 | 0.38 | 0.00 | 0.00 | 0.22 |
12 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | 4912.02 | 0.60 | 14109.59 | 22437.03 | 0.63 | 4.57 | {'percentageOfActive': 0.0, 'percentageOfCompl... | {'percentageOfActiveFundsRequested': 0.0, 'per... | 0 | ... | [2, 2, 2, 2, 2, 2, 2, 2, 1] | [689.069255985934, 0.0, 0.0, 741.7232278274, 1... | [6150.02705747431, nan, nan, 7712.327894898151... | [0.11204328851667208, nan, nan, 0.096173715373... | 0.00 | 0.00 | 0.33 | 0.00 | 0.00 | 0.22 |
18 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | 4934.45 | 0.60 | 14109.59 | 22459.46 | 0.63 | 4.55 | {'percentageOfActive': 0.0, 'percentageOfCompl... | {'percentageOfActiveFundsRequested': 0.0, 'per... | 0 | ... | [3, 3, 3, 3, 3, 3, 3, 3, 2, 1] | [985.9659564142296, 0.0, 0.0, 1002.22175988391... | [6177.856274860535, nan, nan, 7737.05064270970... | [0.1595967779999038, nan, nan, 0.1295353754506... | 0.00 | 0.00 | 0.30 | 0.00 | 0.00 | 0.19 |
24 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | 4956.91 | 0.60 | 14109.59 | 22481.92 | 0.63 | 4.54 | {'percentageOfActive': 0.0, 'percentageOfCompl... | {'percentageOfActiveFundsRequested': 0.0, 'per... | 0 | ... | [4, 4, 4, 4, 4, 4, 4, 4, 3, 2, 1] | [1300.3581675780288, 0.0, 0.0, 1208.9795817952... | [6166.512935081749, nan, nan, 7712.8076823156,... | [0.2108741490154338, nan, nan, 0.1567496081313... | 0.00 | 0.00 | 0.36 | 0.00 | 0.00 | 0.28 |
30 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | 4979.40 | 0.60 | 14109.59 | 22504.41 | 0.63 | 4.52 | {'percentageOfActive': 0.0, 'percentageOfCompl... | {'percentageOfActiveFundsRequested': 0.0, 'per... | 0 | ... | [5, 5, 5, 5, 5, 5, 5, 5, 4, 3, 2, 1] | [1683.9038085993063, 0.0, 0.0, 1373.0833737976... | [6155.29193202844, nan, nan, 7688.872669166836... | [0.27357009662487053, nan, nan, 0.178580584290... | 0.00 | 0.00 | 0.33 | 0.00 | 0.00 | 0.26 |
5 rows × 43 columns
df.plot('timestep','sentiment',title='Sentiment over time')
<matplotlib.axes._subplots.AxesSubplot at 0x7fc1d403d520>
The above plot demonstrates system sentiment changing over time as proposals pass or fail.
df.plot('timestep',['funds', 'candidate_funds'],title ='Funds and candidate funds')
<matplotlib.axes._subplots.AxesSubplot at 0x7fc1d5196a60>
In the above graph, funds represent the total available funds, whereas candidate funds represent total funds requested by candidate proposals.
affinities_plot(df.network.values[-1],dims = (20,20))
The above matrix represents participant affinities towards proposals, ranging from -1 to +1.
df.plot(x='timestep',y=['candidate_count','active_count','completed_count', 'killed_count', 'failed_count'],
kind='area')
plt.title('Proposal Status')
plt.ylabel('count of proposals')
plt.legend(ncol = 3,loc='upper center', bbox_to_anchor=(0.5, -0.15))
<matplotlib.legend.Legend at 0x7fc1aab59eb0>
The above graph shows the number of various types of proposals at a range of timesteps. Ecosystems with more completed proposals will have higher overall agent sentiment than systems with more failed and killed proposals.
df.plot(x='timestep',y=['candidate_funds','active_funds','completed_funds', 'killed_funds', 'failed_funds'], kind='area')
plt.title('Proposal Status weighted by funds requested')
plt.ylabel('Funds worth of proposals')
plt.legend(ncol = 3,loc='upper center', bbox_to_anchor=(0.5, -0.15))
<matplotlib.legend.Legend at 0x7fc1aa830070>
The above graph shows the amount of funds requested by various types of proposals at a range of timesteps.
nets = df.network.values
K = 0
N = 1
snap_plot(nets[K:N], size_scale = 1/10,dims = (10,10),savefigs=True)
/home/aclarkdata/repos/Aragon_Conviction_Voting/models/v3/model/parts/utils.py:699: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect. plt.tight_layout()
On the left side are participants, with the right side of the graph being the proposals. With this graph, we can see the links between the participants and the proposals that they support. The percentage on the right hand are the the amount of the required amount to pass that has been fulfilled.
You can move the K and N to different points within the 100 timesteps, 0 indexed, to see how the model evolves over time.
As you can see with the plot above at the start of the simulation, no proposals have been formally supported yet. Below we can see many interactions between agents and proposals.
snap_plot(nets[50:51], size_scale = 1/10,dims = (10,10),savefigs=True)
df.plot('timestep',['total_supply','funds'],title='Total Supply and Funds')
<matplotlib.axes._subplots.AxesSubplot at 0x7fc1aab5afa0>
df.plot('timestep',['effective_supply'],title='Effective Supply')
<matplotlib.axes._subplots.AxesSubplot at 0x7fc1ab4ed460>
As expected effective_supply is increasing with the arrival of new participants.
df.plot('timestep',['participant_count'],title='Count of Participants')
<matplotlib.axes._subplots.AxesSubplot at 0x7fc1aabd3400>
Below we will analyze system health metrics such as fraction of supply used for voting (which is effective supply over total supply), and percentage of proposals and requested funds in different stages.
df.plot('timestep',['fractionOfSupplyForVoting'],title='Metric: Fraction of Supply For Voting')
<matplotlib.axes._subplots.AxesSubplot at 0x7fc1ab514820>
df.plot('timestep',['fractionOfSupplyInPool'],title='Metric: Fraction of Supply in Pool')
<matplotlib.axes._subplots.AxesSubplot at 0x7fc1ab41b160>
df.plot('timestep',['percentageOfActiveProposals','percentageOfCompletedProposals',
'percentageOfKilledProposals'],
title='Metric: Percentage of Proposal in Stage')
<matplotlib.axes._subplots.AxesSubplot at 0x7fc1ab380be0>
df.plot('timestep',['percentageOfActiveFundsRequested','percentageOfCompletedFundsRequested',
'percentageOfKilledFundsRequested'],
title='Metric: Percentage of Funds in Stage')
<matplotlib.axes._subplots.AxesSubplot at 0x7fc1ab33f580>
We have created a conviction voting model that closely adheres to the 1Hive implementation. This notebook describes the use case, how the model works, and provides descriptions of how it fits together. We have utilized all four levels of a cadCAD simulation: Behaviors, Mechanisms, States, and Metrics. To continue to expand the model, we can take our designed flight simulator and begin to stress test and make it antifragile through parameter sweeps, A/B testing, and monte carlo runs. Additionally, we could add the following enhancements:
to prevent small proposal spamming from draining the communal funding pool, all proposals should have some minimum conviction required to pass
to avoid slow conviction aggregation due to "inactive" tokens (e.g. locked up in cold storage or liquidity pool, without active participation in governance), effective supply is the portion of tokens that are active in community governance
the proposal process could make use of additional mechanisms like fund escrow, proposal backlog processes, reviews/validation & disputability/contestation to ensure that the incentive to game the system is kept to a minimum through responsible community oversight