New to this model are the following elements:
Conviction Voting is an approach to organizing a communities preferences into discrete decisions in the management of that communities resources. Strictly speaking conviction voting is less like voting and more like signal processing. 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 Zargham's PhD Thesis.
The work proceeded in collaboration with the Commons Stack, including expanding on the pythin implementation to makeup 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 around allocations their community currency, Honey.
Suppose a group of people want to coordinate to make a collective decision. Social dynamics such as discussions, signaling, and even changing ones mind based on feedback from others input play an important role in these processes. While the actual decision making process involves a lot of informal processes, in order to be fair the ultimate decision making process still requires a set of formal rules that the community collecively agrees to, which serves to functionally channel a plurality of preferences into a discrete outcomes. In our case we are interested in a procedure which supports asynchronous interactions, an provides visibility into likely outcomes prior to their resolution to serve as a driver of good faith, debate and healthy forms of coalition building. Furthermore, participations should be able to show support for multiple initiatives, and to vary the level of support shown. Participants a quantity of signaling power which may be fixed or variable, homogenous or heterogenous. For the purpose of this document, we'll focus on the case where the discrete decisions to be made are decisions to allocate funds from a shared funding pool towards projects of interest to the community.
Let's start taking these words and constructing a mathematical representation that supports a design that meets the description above. To start we need to define participants.
Let A be the set of participants. Consider a participant a∈A. Any participant a has some capacity to participate in the voting process h[a]. In a fixed quantity, homogenous system h[a]=h for all a∈A where h is a constant. The access control process managing how one becomes a participant determines the total supply of "votes" S=∑a∈A=n⋅h where the number of participants is n=|A|. In a smart contract setting, the set A is a set of addresses, and h[a] is a quantity of tokens held by each address a∈A.
Next, we introduce the idea of proposals. Consider a proposal i∈C. Any proposal i is associated with a request for resources r[i]. Those requested resources would be allocated from a constrained pool of communal resources currently totaling R. The pool of resources may become depleted because when a proposal i passes R+=R−r[i]. Therefore it makes sense for us to consider what fraction of the shared resources are being request μi=r[i]R, which means that thre resource depletion from passing proposals can be bounded by requiring μi<μ where μ is a constant representing the maximum fraction of the shared resources which can be dispersed by any one proposal. In order for the system to be sustainable a source of new resources is required. In the case where R is funding, new funding can come from revenues, donations, or in some DAO use cases minting tokens.
Most of the interesting information in this system is distributed amongst the participants and it manifests as preferences over the proposals. This can be thought of as a matrix W∈Rn×m.
These private hidden signals drive discussions and voting actions. Each participant individually decides how to allocate their votes across the available proposals. Participant a supports proposal i by setting x[a,i]>0 but they are limited by their capacity ∑k∈Cx[a,k]≤h[a]. Assuming each participant chooses a subset of the proposals to support, a support graph is formed.
In order to break out of the synchronous voting model, a dynamical systems model of this system is introduced.
Conviction as kinetic energy and Trigger function as required activation energy.
https://www.desmos.com/calculator/yxklrjs5m3
Below we show a sweep of the trigger function threshold:
from model.model.conviction_helper_functions import *
import warnings
warnings.filterwarnings("ignore")
beta = .2 #later we should set this to be param so we can sweep it
# tuning param for the trigger function
rho = .001
#alpha = 1 - 0.9999599 #native timescale for app as in contract code
alpha = 1-.5**3 #timescale set in days with 3 day halflife (from comments in contract comments)
supply= 21706
mcv = supply/(1-alpha)
print('for reference: max conviction = '+str(np.log10(mcv))+'in log10 units')
for reference: max conviction = 5.239669785665982in log10 units
/home/aclarkdata/anaconda3/lib/python3.7/site-packages/statsmodels/tools/_testing.py:19: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead. import pandas.util.testing as tm
supply_sweep = trigger_sweep('effective_supply',trigger_threshold,beta,rho,alpha, supply)
alpha_sweep = trigger_sweep('alpha',trigger_threshold,beta,rho,alpha, supply)
trigger_grid(supply_sweep, alpha_sweep)
Subjective, exploratory modeling of the social system interacting through the conviction voting algorithm.
Global Sentiment -- the outside world appreciating the output of the community Local Sentiment -- agents within the system feeling good about the community
Preferences as mixing process (social influence)
Some proposals are synergistic (passing one makes the other more desireable) Some proposals are (parially) substitutable (passing one makes the other less desirable)
In the cadCAD simulation methodology, we operate on four layers: Policies, Mechanisms, States, and Metrics. Information flows do not have explicit feedback loop unless noted. Policies determine the inputs into the system dynamics, and can come from user input, observations from the exogenous environment, or algorithms. Mechanisms are functions that take the policy decisions and update the States to reflect the policy level changes. States are variables that represent the system quantities at the given point in time, and Metrics are computed from state variables to assess the health of the system. Metrics can often be thought of as KPIs, or Key Performance Indicators.
At a more granular level, to setup a model, there are system conventions and configurations that must be followed.
The way to think of cadCAD modeling is analogous to machine learning pipelines which normally consist of multiple steps when training and running a deployed model. There is preprocessing, which includes segregating features between continuous and categorical, transforming or imputing data, and then instantiating, training, and running a machine learning model with specified hyperparameters. cadCAD modeling can be thought of in the same way as states, roughly translating into features, are fed into pipelines that have built-in logic to direct traffic between different mechanisms, such as scaling and imputation. Accuracy scores, ROC, etc. are analogous to the metrics that can be configured on a cadCAD model, specifying how well a given model is doing in meeting its objectives. The parameter sweeping capability of cadCAD can be thought of as a grid search, or way to find the optimal hyperparameters for a system by running through alternative scenarios. A/B style testing that cadCAD enables is used in the same way machine learning models are A/B tested, except out of the box, in providing a side by side comparison of muliple different models to compare and contrast performance. Utilizing the field of Systems Identification, dynamical systems models can be used to "online learn" by providing a feedback loop to generative system mechanisms.
The model consists of a temporal in memory graph database called network containing nodes of type Participant and type Proposal. Participants will have holdings and sentiment and Proposals will have funds_required, status(candidate or active), conviction Tthe model as three kinds of edges:
Edges in the network go from nodes of type Participant to nodes of type Proposal with the edges having the key type, of which all will be set to support. Edges from participant i to proposal j will have the following additional characteristics:
Sum_j = network.edges[(i,j)]['tokens'] = network.nodes[i]['holdings']
. This value of tokens for participants on proposals must be less than or equal to the total number of tokens held by the participant.network.nodes[j]['conviction'] = Sum_i network.edges[(i,j)]['conviction']
.The other state variable in the model is funds, which is a numpy floating point.
The system consists of 100 time steps without a parameter sweep or monte carlo.
Each partial state update block is kind of a like a phase in a phased based board game. Everyone decides what to do and it reconciles all decisions. One timestep is a full turn, with each block being a phase of a timestep or turn. We will walk through the individaul Partial State update blocks one by one below.
{
# system.py:
'policies': {
'random': driving_process
},
'variables': {
'network': update_network,
'funds':increment_funds,
}
To simulate the arrival of participants and proposal into the system, we have a driving process to represent the arrival of individual agents. We use a random uniform distribution generator, over [0, 1), to calculate the number of new participants. We then use an exponential distribution to calculate the particpant's tokens by using a loc of 0.0 and a scale of expected holdings, which is calculated by .1*supply/number of existing participants. We calculate the number of new proposals by
proposal_rate = 1/median_affinity * (1+total_funds_requested/funds)
rv2 = np.random.rand()
new_proposal = bool(rv2<1/proposal_rate)
The network state variable is updated to include the new participants and proposals, while the funds state variable is updated for the increase in system funds. To see the partial state update code, click here
{
# participants.py
'policies': {
'completion': check_progress
},
'variables': {
'sentiment': update_sentiment_on_completion, #not completing projects decays sentiment, completing bumps it
'network': complete_proposal
}
},
In the next phase of the turn, to see the logic code, click here, the check_progress behavior checks for the completion of previously funded proposals. The code calculates the completion and failure rates as follows:
likelihood = 1.0/(base_completion_rate+np.log(grant_size))
failure_rate = 1.0/(base_failure_rate+np.log(grant_size))
if np.random.rand() < likelihood:
completed.append(j)
elif np.random.rand() < failure_rate:
failed.append(j)
With the base_completion_rate being 100 and the base_failure_rate as 200.
The mechanism then updates the respective network nodes and updates the sentiment variable on proposal completion.
# proposals.py
'policies': {
'release': trigger_function
},
'variables': {
'funds': decrement_funds,
'sentiment': update_sentiment_on_release, #releasing funds can bump sentiment
'network': update_proposals
}
},
The trigger release function checks to see if each proposal passes or not. If a proposal passes, funds are decremented by the amount of the proposal, while the proposal's status is changed in the network object.
{
# participants.py
'policies': {
'participants_act': participants_decisions
},
'variables': {
'network': update_tokens
}
}
The Participants decide based on their affinity if which proposals they would like to support,to see the logic code, click here. Proposals that participants have high affinity for receive more support and pledged tokens than proposals with lower affinity and sentiment. We then update everyone's holdings and their conviction for each proposal.
The the model described above is the second iteration model that covers the core mechanisms of the Aragon Conviction Voting model. Below are next additional dynamics we can attend to enrich the model, and provide workstreams for subsequent iterations of this lab notebook.
Let's factor out into its own notebook where we review the config object and its partial state update blocks.
from model import economyconfig
from model.model.conviction_helper_functions import *
# pull out configurations to illustrate
sim_config,genesis_states,seeds,partial_state_update_blocks = economyconfig.get_configs()
sim_config
{'N': 1, 'T': range(0, 100), 'M': [{}], 'subset_id': 0, 'subset_window': deque([0, None]), 'simulation_id': 0, 'run_id': 0}
partial_state_update_blocks
[{'policies': {'random': <function model.model.system.driving_process(params, step, sL, s)>}, 'variables': {'network': <function model.model.system.update_network(params, step, sL, s, _input)>, 'funds': <function model.model.system.increment_funds(params, step, sL, s, _input)>}}, {'policies': {'completion': <function model.model.participants.check_progress(params, step, sL, s)>}, 'variables': {'sentiment': <function model.model.participants.update_sentiment_on_completion(params, step, sL, s, _input)>, 'network': <function model.model.participants.complete_proposal(params, step, sL, s, _input)>}}, {'policies': {'release': <function model.model.proposals.trigger_function(params, step, sL, s)>}, 'variables': {'funds': <function model.model.proposals.decrement_funds(params, step, sL, s, _input)>, 'sentiment': <function model.model.proposals.update_sentiment_on_release(params, step, sL, s, _input)>, 'network': <function model.model.proposals.update_proposals(params, step, sL, s, _input)>}}, {'policies': {'participants_act': <function model.model.participants.participants_decisions(params, step, sL, s)>}, 'variables': {'network': <function model.model.participants.update_tokens(params, step, sL, s, _input)>}}]
To create the genesis_states, we create our in-memory graph database within networkx.
genesis_states
{'network': <networkx.classes.digraph.DiGraph at 0x7fed5b139390>, 'funds': 48000, 'sentiment': 0.6, 'supply': 21706}
A graph is a type of temporal data structure that evolves over time. A graph G(V,E) consists of vertices or nodes, V={1...V} and is connected by edges E⊆V×V.
See Schema of the states above for more details
Let's explore!
network = genesis_states['network']
# To explore our model prior to the simulation, we extract key components from our networkX object into lists.
proposals = get_nodes_by_type(network, 'proposal')
participants = get_nodes_by_type(network, 'participant')
supporters = get_edges_by_type(network, 'support')
influencers = get_edges_by_type(network, 'influence')
competitors = get_edges_by_type(network, 'conflict')
#sample a participant
network.nodes[participants[0]]
{'type': 'participant', 'holdings': 50.79176616446257, 'sentiment': 0.010951469255264912}
# Let's look at the distribution of participant holdings at the start of the sim
plt.hist([ 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(network, nodelist = participants, edgelist=influencers)
plt.title('Participants Social Network')
Text(0.5, 1.0, 'Participants Social Network')
#lets look at proposals
network.nodes[proposals[0]]
{'type': 'proposal', 'conviction': 0, 'status': 'candidate', 'age': 0, 'funds_requested': 1618.7215158100328, 'trigger': 19367.633382772743}
Proposals initially start without any conviction, and with the status of a candidate. If the proposal's amount of conviction is greater than it's trigger, then the proposal moves to active and it's funds requested are granted.
All initial proposal start with 0 conviction and state 'candidate'we can simply examine the amounts of funds requested
funds_array = np.array([ network.nodes[i]['funds_requested'] for i in proposals])
conviction_required = np.array([trigger_threshold(r, genesis_states['funds'], genesis_states['supply'], beta, rho, alpha) for r in funds_array])
plt.bar( proposals, funds_array/genesis_states['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)')
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.
m = len(proposals)
n = len(participants)
affinities = np.empty((n,m))
for i_ind in range(n):
for j_ind in range(m):
i = participants[i_ind]
j = proposals[j_ind]
affinities[i_ind][j_ind] = network.edges[(i,j)]['affinity']
dims = (20, 5)
fig, ax = plt.subplots(figsize=dims)
sns.heatmap(affinities.T,
xticklabels=participants,
yticklabels=proposals,
square=True,
cbar=True,
cmap = plt.cm.RdYlGn,
ax=ax)
plt.title('affinities between participants and proposals')
plt.ylabel('proposal_id')
plt.xlabel('participant_id')
Text(0.5, 55.73999999999998, 'participant_id')
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.model.conviction_helper_functions import *
from model import run
from cadCAD import configs
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, 1, 1, 4) Execution Method: local_simulations SimIDs : [0] SubsetIDs: [0] Ns : [0] ExpIDs : [0] Execution Mode: single_threaded Total execution time: 158.17s
After the simulation has run successfully, 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 | supply | simulation | subset | run | substep | timestep | conviction | ... | funds_requested | share_of_funds_requested | share_of_funds_requested_all | triggers | conviction_share_of_trigger | age | age_all | conviction_all | triggers_all | conviction_share_of_trigger_all | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
4 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | 48001.94 | 0.60 | 21706 | 0 | 0 | 1 | 4 | 1 | [194.13295659188222, 1138.1090114416809, 113.1... | ... | [1618.7215158100328, 976.2298772276204, 955.77... | [0.020337301242219573, 0.01991126341581559, 0.... | [0.033722003251714774, 0.020337301242219573, 0... | [13449.245661578903, 13385.684039562457, 12468... | [0.014434486623028311, 0.08502434452194665, 0.... | [1, 1, 1, 1, 1, 1, 1] | [1, 1, 1, 1, 1, 1, 1, 1, 1] | [0.0, 194.13295659188222, 1138.1090114416809, ... | [15701.724028376366, 13449.245661578903, 13385... | [0.0, 0.014434486623028311, 0.0850243445219466... |
8 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | 48003.84 | 0.60 | 21706 | 0 | 0 | 1 | 4 | 2 | [363.99929360977916, 2134.079726810394, 212.10... | ... | [1618.7215158100328, 976.2298772276204, 955.77... | [0.02033649690352059, 0.019910475926879855, 0.... | [0.0337206695490814, 0.02033649690352059, 0.01... | [13449.122714571258, 13385.564520413129, 12468... | [0.027064909833517198, 0.15943143253733524, 0.... | [2, 2, 2, 2, 2, 2, 2, 1] | [2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1] | [0.0, 363.99929360977916, 2134.079726810394, 0... | [nan, 13449.122714571258, 13385.564520413129, ... | [nan, 0.027064909833517198, 0.1594314325373352... |
12 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | 48004.13 | 0.60 | 21706 | 0 | 0 | 1 | 4 | 3 | [667.1586969068549, 3013.794084409652, 298.718... | ... | [1618.7215158100328, 976.2298772276204, 955.77... | [0.020336372627242947, 0.019910354254015248, 0... | [0.03372046348216087, 0.020336372627242947, 0.... | [13449.002293612462, 13385.447456847483, 12468... | [0.04960655685393991, 0.22515452652036, 0.0239... | [3, 3, 3, 3, 3, 3, 3, 2, 1, 1] | [3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 1, 1] | [0.0, 667.1586969068549, 3013.794084409652, 0.... | [nan, 13449.002293612462, 13385.447456847483, ... | [nan, 0.04960655685393991, 0.22515452652036, n... |
16 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | 48005.05 | 0.60 | 21706 | 0 | 0 | 1 | 4 | 4 | [932.4231747917961, 3361.442465663199, 374.503... | ... | [1618.7215158100328, 976.2298772276204, 955.77... | [0.020335981618139105, 0.019909971436001594, 0... | [0.03371981513604482, 0.020335981618139105, 0.... | [13448.983687827978, 13385.429369797788, 12468... | [0.06933038186637754, 0.2511269808981839, 0.03... | [4, 4, 4, 4, 4, 4, 4, 3, 2, 2] | [4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 2, 2, 1, 1] | [0.0, 932.4231747917961, 3361.442465663199, 0.... | [nan, 13448.983687827978, 13385.429369797788, ... | [nan, 0.06933038186637754, 0.2511269808981839,... |
20 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | 48006.02 | 0.60 | 21706 | 0 | 0 | 1 | 4 | 5 | [1611.162845844111, 3665.614959197917, 441.593... | ... | [1618.7215158100328, 976.2298772276204, 955.77... | [0.020335573633065672, 0.019909571997641016, 0... | [0.033719138641469214, 0.020335573633065672, 0... | [13448.925148901995, 13385.37246294872, 12468.... | [0.11979863282796621, 0.27385229431190616, 0.0... | [5, 5, 5, 5, 5, 5, 5, 4, 3, 3] | [5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 3, 3, 2, 2, ... | [0.0, 1611.162845844111, 3665.614959197917, 0.... | [nan, 13448.925148901995, 13385.37246294872, n... | [nan, 0.11979863282796621, 0.27385229431190616... |
5 rows × 31 columns
df.plot('timestep','sentiment')
<matplotlib.axes._subplots.AxesSubplot at 0x7fed524551d0>
df.plot('timestep',['funds', 'candidate_funds'])
<matplotlib.axes._subplots.AxesSubplot at 0x7fed36d9f290>
affinities_plot(df)
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 0x7fed36b99d50>
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 0x7fed37f13a90>
nets = df.network.values
K = 55
N = 56
snap_plot(nets[K:N], size_scale = 1/10,savefigs=True)
We have created a simplified conviction voting model that illustrates the state objects, and provides descriptions of how the model fits together. In subsequent notebooks, we will expand the model to introduce additional complexity to more fit real world implementations.