464 lines
17 KiB
Python
464 lines
17 KiB
Python
from scipy.stats import expon, gamma
|
|
import numpy as np
|
|
import networkx as nx
|
|
from hatch import TokenBatch
|
|
from convictionvoting import trigger_threshold
|
|
from IPython.core.debugger import set_trace
|
|
from functools import wraps
|
|
|
|
def dump_output(f):
|
|
@wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
result = f(*args, **kwargs)
|
|
print(result)
|
|
return result
|
|
return wrapper
|
|
|
|
def get_nodes_by_type(g, node_type_selection):
|
|
return [node for node in g.nodes if g.nodes[node]['type']== node_type_selection ]
|
|
|
|
def get_edges_by_type(g, edge_type_selection):
|
|
return [edge for edge in g.edges if g.edges[edge]['type']== edge_type_selection ]
|
|
|
|
def get_proposals(network):
|
|
return get_nodes_by_type(network, "proposal")
|
|
|
|
def get_participants(network):
|
|
return get_nodes_by_type(network, "participant")
|
|
|
|
def initial_social_network(network: nx.DiGraph, scale = 1, sigmas=3) -> nx.DiGraph:
|
|
participants = get_participants(network)
|
|
|
|
for i in participants:
|
|
for j in participants:
|
|
if not(j==i):
|
|
influence_rv = expon.rvs(loc=0.0, scale=scale)
|
|
if influence_rv > scale+sigmas*scale**2:
|
|
network.add_edge(i,j)
|
|
network.edges[(i,j)]['influence'] = influence_rv
|
|
network.edges[(i,j)]['type'] = 'influence'
|
|
return network
|
|
|
|
def initial_conflict_network(network: nx.DiGraph, rate = .25) -> nx.DiGraph:
|
|
proposals = get_proposals(network)
|
|
|
|
for i in proposals:
|
|
for j in proposals:
|
|
if not(j==i):
|
|
conflict_rv = np.random.rand()
|
|
if conflict_rv < rate :
|
|
network.add_edge(i,j)
|
|
network.edges[(i,j)]['conflict'] = 1-conflict_rv
|
|
network.edges[(i,j)]['type'] = 'conflict'
|
|
return network
|
|
|
|
def add_proposals_and_relationships_to_network(n: nx.DiGraph, proposals: int, funding_pool: float, token_supply: float) -> nx.DiGraph:
|
|
participant_count = len(n)
|
|
for i in range(proposals):
|
|
j = participant_count + i
|
|
n.add_node(j, type="proposal", conviction=0, status="candidate", age=0)
|
|
|
|
r_rv = gamma.rvs(3,loc=0.001, scale=10000)
|
|
n.nodes[j]['funds_requested'] = r_rv
|
|
n.nodes[j]['trigger']= trigger_threshold(r_rv, funding_pool, token_supply)
|
|
|
|
for i in range(participant_count):
|
|
n.add_edge(i, j)
|
|
rv = np.random.rand()
|
|
a_rv = 1-4*(1-rv)*rv #polarized distribution
|
|
n.edges[(i, j)]['affinity'] = a_rv
|
|
n.edges[(i, j)]['tokens'] = 0
|
|
n.edges[(i, j)]['conviction'] = 0
|
|
n.edges[(i, j)]['type'] = 'support'
|
|
|
|
n = initial_conflict_network(n, rate = .25)
|
|
n = initial_social_network(n, scale = 1)
|
|
return n
|
|
|
|
def update_collateral_pool(params, step, sL, s, _input):
|
|
commons = s["commons"]
|
|
s["collateral_pool"] = commons._collateral_pool
|
|
return "collateral_pool", commons._collateral_pool
|
|
|
|
def update_token_supply(params, step, sL, s, _input):
|
|
commons = s["commons"]
|
|
s["token_supply"] = commons._token_supply
|
|
return "token_supply", commons._token_supply
|
|
|
|
def update_funding_pool(params, step, sL, s, _input):
|
|
commons = s["commons"]
|
|
s["funding_pool"] = commons._funding_pool
|
|
return "funding_pool", commons._funding_pool
|
|
# =========================================================================================================
|
|
def gen_new_participant(network, new_participant_tokens):
|
|
i = len([node for node in network.nodes])
|
|
|
|
network.add_node(i)
|
|
network.nodes[i]['type']="participant"
|
|
|
|
s_rv = np.random.rand()
|
|
network.nodes[i]['sentiment'] = s_rv
|
|
network.nodes[i]['holdings_vesting']=None
|
|
network.nodes[i]['holdings_nonvesting']=TokenBatch(new_participant_tokens, 5, 5)
|
|
|
|
# Connect this new participant to existing proposals.
|
|
for j in get_nodes_by_type(network, 'proposal'):
|
|
network.add_edge(i, j)
|
|
|
|
rv = np.random.rand()
|
|
a_rv = 1-4*(1-rv)*rv #polarized distribution
|
|
network.edges[(i, j)]['affinity'] = a_rv
|
|
network.edges[(i,j)]['tokens'] = a_rv*network.nodes[i]['holdings_nonvesting'].value
|
|
network.edges[(i, j)]['conviction'] = 0
|
|
network.edges[(i,j)]['type'] = 'support'
|
|
|
|
return network
|
|
|
|
def gen_new_proposal(network, funds, supply, trigger_func, scale_factor = 1.0/100):
|
|
j = len([node for node in network.nodes])
|
|
network.add_node(j)
|
|
network.nodes[j]['type']="proposal"
|
|
|
|
network.nodes[j]['conviction']=0
|
|
network.nodes[j]['status']='candidate'
|
|
network.nodes[j]['age']=0
|
|
|
|
rescale = funds*scale_factor
|
|
r_rv = gamma.rvs(3,loc=0.001, scale=rescale)
|
|
network.nodes[j]['funds_requested'] = r_rv
|
|
|
|
network.nodes[j]['trigger']= trigger_func(r_rv, funds, supply)
|
|
|
|
participants = get_nodes_by_type(network, 'participant')
|
|
proposing_participant = np.random.choice(participants)
|
|
|
|
for i in participants:
|
|
network.add_edge(i, j)
|
|
if i==proposing_participant:
|
|
network.edges[(i, j)]['affinity']=1
|
|
else:
|
|
rv = np.random.rand()
|
|
a_rv = 1-4*(1-rv)*rv #polarized distribution
|
|
network.edges[(i, j)]['affinity'] = a_rv
|
|
|
|
network.edges[(i, j)]['conviction'] = 0
|
|
network.edges[(i,j)]['tokens'] = 0
|
|
network.edges[(i,j)]['type'] = 'support'
|
|
|
|
return network
|
|
|
|
def calc_total_funds_requested(network):
|
|
proposals = get_proposals(network)
|
|
fund_requests = [network.nodes[j]['funds_requested'] for j in proposals if network.nodes[j]['status']=='candidate' ]
|
|
total_funds_requested = np.sum(fund_requests)
|
|
return total_funds_requested
|
|
|
|
def calc_median_affinity(network):
|
|
supporters = get_edges_by_type(network, 'support')
|
|
affinities = [network.edges[e]['affinity'] for e in supporters ]
|
|
median_affinity = np.median(affinities)
|
|
return median_affinity
|
|
|
|
@dump_output
|
|
def gen_new_participants_proposals_funding_randomly(params, step, sL, s):
|
|
network = s['network']
|
|
commons = s['commons']
|
|
funds = s['funding_pool']
|
|
sentiment = s['sentiment']
|
|
|
|
def randomly_gen_new_participant(participant_count, sentiment, current_token_supply, commons):
|
|
arrival_rate = 10/(1+sentiment)
|
|
rv1 = np.random.rand()
|
|
new_participant = bool(rv1<1/arrival_rate)
|
|
|
|
if new_participant:
|
|
# Below line is quite different from Zargham's original, which gave
|
|
# tokens instead. Here we randomly generate each participant's
|
|
# post-Hatch investment, in DAI/USD. Here the settings for
|
|
# expon.rvs() should generate investments of ~0-500 DAI.
|
|
new_participant_investment = expon.rvs(loc=0.0, scale=100)
|
|
new_participant_tokens = commons.dai_to_tokens(new_participant_investment)
|
|
return new_participant, new_participant_investment, new_participant_tokens
|
|
else:
|
|
return new_participant, 0, 0
|
|
|
|
def randomly_gen_new_proposal(total_funds_requested, median_affinity, funding_pool):
|
|
proposal_rate = 1/median_affinity * (1+total_funds_requested/funding_pool)
|
|
rv2 = np.random.rand()
|
|
new_proposal = bool(rv2<1/proposal_rate)
|
|
return new_proposal
|
|
|
|
def randomly_gen_new_funding(funds, sentiment):
|
|
"""
|
|
Each step, more funding comes to the Commons through the exit tribute,
|
|
because after the hatching phase, all incoming money goes to the
|
|
collateral reserve, not to the funding pool.
|
|
"""
|
|
scale_factor = funds*sentiment**2/10000
|
|
if scale_factor <1:
|
|
scale_factor = 1
|
|
|
|
#this shouldn't happen but expon is throwing domain errors
|
|
if sentiment>.4:
|
|
funds_arrival = expon.rvs(loc = 0, scale = scale_factor )
|
|
else:
|
|
funds_arrival = 0
|
|
return funds_arrival
|
|
|
|
new_participant, new_participant_investment, new_participant_tokens = randomly_gen_new_participant(len(get_participants(network)), sentiment, s['token_supply'], commons)
|
|
|
|
new_proposal = randomly_gen_new_proposal(calc_total_funds_requested(network), calc_median_affinity(network), funds)
|
|
|
|
funds_arrival = randomly_gen_new_funding(funds, sentiment)
|
|
|
|
return({'new_participant':new_participant,
|
|
'new_participant_investment':new_participant_investment,
|
|
'new_participant_tokens': new_participant_tokens,
|
|
'new_proposal':new_proposal,
|
|
'funds_arrival':funds_arrival})
|
|
|
|
def add_participants_proposals_to_network(params, step, sL, s, _input):
|
|
network = s['network']
|
|
funds = s['funding_pool']
|
|
supply = s['token_supply']
|
|
|
|
trigger_func = params[0]["trigger_threshold"]
|
|
|
|
new_participant = _input['new_participant'] #T/F
|
|
new_proposal = _input['new_proposal'] #T/F
|
|
|
|
if new_participant:
|
|
network = gen_new_participant(network, _input['new_participant_tokens'])
|
|
|
|
if new_proposal:
|
|
network= gen_new_proposal(network,funds,supply,trigger_func )
|
|
|
|
#update age of the existing proposals
|
|
proposals = get_nodes_by_type(network, 'proposal')
|
|
|
|
for j in proposals:
|
|
network.nodes[j]['age'] = network.nodes[j]['age']+1
|
|
if network.nodes[j]['status'] == 'candidate':
|
|
requested = network.nodes[j]['funds_requested']
|
|
network.nodes[j]['trigger'] = trigger_func(requested, funds, supply)
|
|
else:
|
|
network.nodes[j]['trigger'] = np.nan
|
|
|
|
key = 'network'
|
|
value = network
|
|
|
|
return (key, value)
|
|
|
|
def new_participants_and_new_funds_commons(params, step, sL, s, _input):
|
|
commons = s["commons"]
|
|
if _input['new_participant']:
|
|
tokens, realized_price = commons.deposit(_input['new_participant_investment'])
|
|
# print(tokens, realized_price, _input['new_participant_tokens'])
|
|
if _input['funds_arrival']:
|
|
commons._funding_pool += _input['funds_arrival']
|
|
return "commons", commons
|
|
# =========================================================================================================
|
|
@dump_output
|
|
def make_active_proposals_complete_or_fail_randomly(params, step, sL, s):
|
|
network = s['network']
|
|
proposals = get_proposals(network)
|
|
|
|
completed = []
|
|
failed = []
|
|
for j in proposals:
|
|
if network.nodes[j]['status'] == 'active':
|
|
grant_size = network.nodes[j]['funds_requested']
|
|
base_completion_rate=params[0]['base_completion_rate']
|
|
likelihood = 1.0/(base_completion_rate+np.log(grant_size))
|
|
|
|
base_failure_rate = params[0]['base_failure_rate']
|
|
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)
|
|
return({'completed':completed, 'failed':failed})
|
|
|
|
def get_sentimental(sentiment, force, decay=0):
|
|
mu = decay
|
|
sentiment = sentiment*(1-mu) + force
|
|
if sentiment > 1:
|
|
sentiment = 1
|
|
return sentiment
|
|
|
|
def sentiment_decays_wo_completed_proposals(params, step, sL, s, _input):
|
|
network = s['network']
|
|
proposals = get_proposals(network)
|
|
completed = _input['completed']
|
|
failed = _input['failed']
|
|
|
|
grants_outstanding = np.sum([network.nodes[j]['funds_requested'] for j in proposals if network.nodes[j]['status']=='active'])
|
|
grants_completed = np.sum([network.nodes[j]['funds_requested'] for j in completed])
|
|
grants_failed = np.sum([network.nodes[j]['funds_requested'] for j in failed])
|
|
|
|
sentiment = s['sentiment']
|
|
|
|
if grants_outstanding>0:
|
|
force = (grants_completed-grants_failed)/grants_outstanding
|
|
else:
|
|
force=1
|
|
|
|
mu = params[0]['sentiment_decay']
|
|
if (force >=0) and (force <=1):
|
|
sentiment = get_sentimental(sentiment, force, mu)
|
|
else:
|
|
sentiment = get_sentimental(sentiment, 0, mu)
|
|
|
|
key = 'sentiment'
|
|
value = sentiment
|
|
|
|
return (key, value)
|
|
|
|
def update_network_w_proposal_status(params, step, sL, s, _input):
|
|
network = s['network']
|
|
participants = get_participants(network)
|
|
proposals = get_proposals(network)
|
|
competitors = get_edges_by_type(network, 'conflict')
|
|
completed = _input['completed']
|
|
for j in completed:
|
|
network.nodes[j]['status']='completed'
|
|
|
|
for c in proposals:
|
|
if (j,c) in competitors:
|
|
conflict = network.edges[(j,c)]['conflict']
|
|
for i in participants:
|
|
network.edges[(i,c)]['affinity'] = network.edges[(i,c)]['affinity'] *(1-conflict)
|
|
|
|
for i in participants:
|
|
force = network.edges[(i,j)]['affinity']
|
|
sentiment = network.nodes[i]['sentiment']
|
|
network.nodes[i]['sentiment'] = get_sentimental(sentiment, force, decay=0)
|
|
|
|
failed = _input['failed']
|
|
for j in failed:
|
|
network.nodes[j]['status']='failed'
|
|
for i in participants:
|
|
force = -network.edges[(i,j)]['affinity']
|
|
sentiment = network.nodes[i]['sentiment']
|
|
network.nodes[i]['sentiment'] = get_sentimental(sentiment, force, decay=0)
|
|
|
|
key = 'network'
|
|
value = network
|
|
return (key, value)
|
|
|
|
# =========================================================================================================
|
|
@dump_output
|
|
def conviction_gathering(params, step, sL, s):
|
|
def sort_proposals_by_conviction(network, proposals):
|
|
ordered = sorted(proposals, key=lambda j:network.nodes[j]['conviction'] , reverse=True)
|
|
return ordered
|
|
network = s['network']
|
|
funding_pool = s['funding_pool']
|
|
token_supply = s['token_supply']
|
|
proposals = get_proposals(network)
|
|
min_proposal_age = params[0]['min_proposal_age_days']
|
|
trigger_func = params[0]['trigger_threshold']
|
|
|
|
accepted = []
|
|
triggers = {}
|
|
funds_to_be_released = 0
|
|
for j in proposals:
|
|
if network.nodes[j]['status'] == 'candidate':
|
|
requested = network.nodes[j]['funds_requested']
|
|
age = network.nodes[j]['age']
|
|
|
|
threshold = trigger_func(requested, funding_pool, token_supply)
|
|
if age > min_proposal_age:
|
|
conviction = network.nodes[j]['conviction']
|
|
if conviction >threshold:
|
|
accepted.append(j)
|
|
funds_to_be_released = funds_to_be_released + requested
|
|
else:
|
|
threshold = np.nan
|
|
|
|
triggers[j] = threshold
|
|
|
|
#catch over release and keep the highest conviction results
|
|
if funds_to_be_released > funding_pool:
|
|
#print('funds ='+str(funds))
|
|
#print(accepted)
|
|
ordered = sort_proposals_by_conviction(network, accepted)
|
|
#print(ordered)
|
|
accepted = []
|
|
release = 0
|
|
ind = 0
|
|
while release + network.nodes[ordered[ind]]['funds_requested'] < funding_pool:
|
|
accepted.append(ordered[ind])
|
|
release= network.nodes[ordered[ind]]['funds_requested']
|
|
ind=ind+1
|
|
|
|
return({'accepted':accepted, 'triggers':triggers})
|
|
|
|
def decrement_commons_funding_pool(params, step, sL, s, _input):
|
|
commons = s['commons']
|
|
network = s['network']
|
|
accepted = _input['accepted']
|
|
|
|
for j in accepted:
|
|
commons.spend(network.nodes[j]['funds_requested'])
|
|
|
|
key = 'commons'
|
|
value = commons
|
|
return (key, value)
|
|
|
|
def update_sentiment_on_release(params, step, sL, s, _input):
|
|
network = s['network']
|
|
proposals = get_proposals(network)
|
|
accepted = _input['accepted']
|
|
|
|
proposals_outstanding = np.sum([network.nodes[j]['funds_requested'] for j in proposals if network.nodes[j]['status']=='candidate'])
|
|
proposals_accepted = np.sum([network.nodes[j]['funds_requested'] for j in accepted])
|
|
|
|
sentiment = s['sentiment']
|
|
force = proposals_accepted/proposals_outstanding
|
|
if (force >=0) and (force <=1):
|
|
sentiment = get_sentimental(sentiment, force, False)
|
|
else:
|
|
sentiment = get_sentimental(sentiment, 0, False)
|
|
|
|
key = 'sentiment'
|
|
value = sentiment
|
|
return (key, value)
|
|
|
|
def update_proposals(params, step, sL, s, _input):
|
|
network = s['network']
|
|
accepted = _input['accepted']
|
|
triggers = _input['triggers']
|
|
participants = get_participants(network)
|
|
proposals = get_proposals(network)
|
|
sensitivity = params[0]['sensitivity']
|
|
|
|
for j in proposals:
|
|
network.nodes[j]['trigger'] = triggers[j]
|
|
|
|
#bookkeeping conviction and participant sentiment
|
|
for j in accepted:
|
|
network.nodes[j]['status']='active'
|
|
network.nodes[j]['conviction']=np.nan
|
|
#change status to active
|
|
for i in participants:
|
|
#operating on edge = (i,j)
|
|
#reset tokens assigned to other candidates
|
|
network.edges[(i,j)]['tokens']=0
|
|
network.edges[(i,j)]['conviction'] = np.nan
|
|
|
|
#update participants sentiments (positive or negative)
|
|
affinities = [network.edges[(i,p)]['affinity'] for p in proposals if not(p in accepted)]
|
|
if len(affinities)>1:
|
|
max_affinity = np.max(affinities)
|
|
force = network.edges[(i,j)]['affinity']-sensitivity*max_affinity
|
|
else:
|
|
force = 0
|
|
|
|
#based on what their affinities to the accepted proposals
|
|
network.nodes[i]['sentiment'] = get_sentimental(network.nodes[i]['sentiment'], force, False)
|
|
|
|
key = 'network'
|
|
value = network
|
|
return (key, value)
|