""" This file contains general functions useful in trading goods. """ from registry.registry import get_env import random import copy DEBUG = True ACCEPT = 1 INADEQ = 0 REJECT = -1 AMT_AVAIL = "amt_available" GOODS = "goods" answer_dict = { 1: "I accept", 0: "I'm indifferent about", -1: "I reject" } COMPLEMENTS = "complementaries" DEF_MAX_UTIL = 20 # this should be set by the models that use this module DIM_UTIL_BASE = 1.1 # we should experiment with this! max_util = DEF_MAX_UTIL """ All utility functions must be registered here! """ UTIL_FUNC = "util_func" GEN_UTIL_FUNC = "gen_util_func" STEEP_GRADIENT = 20 def gen_util_func(qty): return max_util * (DIM_UTIL_BASE ** (-qty)) def penguin_util_func(qty): return 25 * (1 ** (-qty)) def cat_util_func(qty): return 10 * (1 ** (-qty)) def bear_util_func(qty): return 15 * (1 ** (-qty)) def steep_util_func(qty): return 20 * (2 ** (-qty)) util_funcs = { GEN_UTIL_FUNC: gen_util_func, "penguin_util_func": penguin_util_func, "cat_util_func": cat_util_func, "bear_util_func": bear_util_func, "steep_util_func": steep_util_func } def get_util_func(fname): return util_funcs[fname] """ We expect goods dictionaries to look like: goods = { "houses": { AMT_AVAIL: int, "maybe more fields": vals ... }, "trucks": { AMT_AVAIL: int, "maybe more fields": vals ... }, "etc.": { AMT_AVAIL: int, "maybe more fields": vals ... }, } A trader is an object that can be indexed to yield a goods dictionary. """ def is_complement(trader, good, comp): """ see if 'comp' is complement of 'good' """ if comp in trader[GOODS][good][COMPLEMENTS]: return True else: return False def check_complement(trader): """ see if COMPLEMENT is an attribute in trader """ if COMPLEMENTS in trader[GOODS]: return True else: return False def is_depleted(goods_dict): """ See if `goods_dict` has any non-zero amount of goods in it. """ for good in goods_dict: if goods_dict[good][AMT_AVAIL] > 0: return False # if all goods are 0 (or less) dict is empty: return True def transfer(to_goods, from_goods, good_nm, amt=None, comp=None): """ Transfer goods between two goods dicts. Use `amt` if it is not None. """ nature = copy.deepcopy(from_goods) if not amt: amt = from_goods[good_nm][AMT_AVAIL] for good in from_goods: if good in to_goods: amt_before_add = to_goods[good][AMT_AVAIL] else: amt_before_add = 0 to_goods[good] = nature[good] if good != good_nm: to_goods[good][AMT_AVAIL] = amt_before_add else: from_goods[good][AMT_AVAIL] -= amt to_goods[good][AMT_AVAIL] = amt_before_add + amt if comp: for g in to_goods: if to_goods[g][AMT_AVAIL] > 0: to_goods[g]['incr'] += amt * STEEP_GRADIENT comp_list = to_goods[g][COMPLEMENTS] for comp in comp_list: to_goods[comp]['incr'] += STEEP_GRADIENT * amt def get_rand_good(goods_dict, nonzero=False): """ What should this do with empty dict? """ # print("Calling get_rand_good()") if goods_dict is None or not len(goods_dict): return None else: if nonzero and is_depleted(goods_dict): # we can't allocate what we don't have! print("Goods are depleted!") return None goods_list = list(goods_dict.keys()) good = random.choice(goods_list) if nonzero: # pick again if the goods is endowed (amt is 0) # if we get big goods dicts, this could be slow: while goods_dict[good][AMT_AVAIL] == 0: good = random.choice(goods_list) return good def incr_util(good_dict, good, amt=None): if amt: good_dict[good]["incr"] += amt else: good_dict[good]["incr"] += 1 def amt_adjust(trader, good): """ This function will check if divisibility is an attribute of the goods. If so, amt traded will depend on divisibility; otherwise, amt will be 1. """ item = list(trader["goods"])[0] if "divisibility" in trader["goods"][item]: return trader["goods"][good]["divisibility"] else: return 1 def endow(trader, avail_goods, equal=False, rand=False, comp=False): """ This function is going to pick a good at random, and give the trader all of it, by default. We will write partial distributions later. """ if equal: # each trader get equal amount of good equal_dist(comp=comp) elif rand: # each trader get random amt of good rand_dist(trader[GOODS], avail_goods, comp=comp) else: # pick an item at random # stick all of it in trader's goods dictionary good2acquire = get_rand_good(avail_goods, nonzero=True) if good2acquire is not None: # get some of the good transfer(trader[GOODS], avail_goods, good2acquire, comp=comp) def equal_dist(num_trader, to_goods, from_goods, comp=None): """ each trader get equal amount of goods to_goods = trader[GOODS], from_goods = avail_goods """ for good in from_goods: amt = from_goods[good][AMT_AVAIL] / num_trader transfer(to_goods, from_goods, good, amt, comp=comp) def rand_dist(to_goods, from_goods, comp=None): """ select random good by random amount and transfer to trader """ selected_good = get_rand_good(from_goods, nonzero=True) amt = random.randrange(0, from_goods[selected_good][AMT_AVAIL], 1) transfer(to_goods, from_goods, selected_good, amt, comp=comp) def goods_to_str(goods): """ take a goods dict to string """ string = ', '.join([str(goods[k][AMT_AVAIL]) + " " + str(k) for k in goods.keys()]) return string def answer_to_str(ans): """ convert integer value of ans to string """ return answer_dict[ans] def negotiate(trader1, trader2, comp=False, amt=1): # this_good is a dict for this_good in trader1["goods"]: amt = amt_adjust(trader1, this_good) while trader1["goods"][this_good][AMT_AVAIL] >= amt: # we want to offer "divisibility" amount extra each loop ans = rec_offer(trader2, this_good, amt, trader1, comp=comp) # Besides acceptance or rejection, the offer can be inadequate! if ans == ACCEPT or ans == REJECT: break amt += amt def seek_a_trade(agent, comp=False): nearby_agent = get_env().get_closest_agent(agent) if nearby_agent is not None: negotiate(agent, nearby_agent, comp) if DEBUG: print("I'm", agent.name, "I have", goods_to_str(agent[GOODS])) # return False means to move return False def seek_a_trade_w_comp(agent): return seek_a_trade(agent, comp=True) def rec_offer(agent, their_good, their_amt, counterparty, comp=False): """ Receive an offer: we don't need to ever change my_amt in this function, because if the counter-party can't bid enough for a single unit, no trade is possible. """ my_amt = 1 gain = utility_delta(agent, their_good, their_amt) if comp: gain += agent[GOODS][their_good]["incr"] print(their_good, agent[GOODS][their_good]['incr']) for my_good in agent["goods"]: # adjust my_amt if "divisibility" is one of the attributes my_amt = amt_adjust(agent, my_good) if my_good != their_good and agent["goods"][my_good][AMT_AVAIL] > 0: loss = -utility_delta(agent, my_good, -my_amt) if comp: loss += agent[GOODS][my_good]["incr"] print("my good: " + my_good + "; his good: " + their_good + ", I gain: " + str(gain) + " and lose: " + str(loss)) if gain > loss: if rec_reply(counterparty, their_good, their_amt, my_good, my_amt, comp=comp): trade(agent, my_good, my_amt, counterparty, their_good, their_amt, comp=comp) return ACCEPT else: return INADEQ return REJECT def rec_reply(agent, my_good, my_amt, their_good, their_amt, comp=None): gain = utility_delta(agent, their_good, their_amt) loss = utility_delta(agent, my_good, -my_amt) if comp: gain += agent[GOODS][their_good]["incr"] loss -= agent[GOODS][my_good]["incr"] if gain > abs(loss): return ACCEPT else: return INADEQ def trade(agent, my_good, my_amt, counterparty, their_good, their_amt, comp=None): adj_add_good(agent, my_good, -my_amt, comp=comp) adj_add_good(agent, their_good, their_amt, comp=comp) adj_add_good(counterparty, their_good, -their_amt, comp=comp) adj_add_good(counterparty, my_good, my_amt, comp=comp) def utility_delta(agent, good, change): """ We are going to determine the utility of goods gained (amt is positive) or lost (amt is negative). `change` will be fractional if good divisibility < 1 """ curr_good = agent["goods"][good] ufunc_name = curr_good[UTIL_FUNC] curr_amt = curr_good[AMT_AVAIL] curr_util = get_util_func(ufunc_name)(curr_amt) new_util = get_util_func(ufunc_name)(curr_amt + change) return ((new_util + curr_util) / 2) * change def adj_add_good(agent, good, amt, comp=None): agent["util"] += utility_delta(agent, good, amt) old_amt = agent["goods"][good][AMT_AVAIL] agent["goods"][good][AMT_AVAIL] += amt if comp: adj_add_good_w_comp(agent, good, amt, old_amt) def new_good(old_amt, amt): return old_amt == 0 and amt > 0 def is_compl_good(agent, good): ''' check if this good is a comp of other goods that the agent have ''' return agent[GOODS][good]['incr'] != 0 def good_not_have(agent, g): ''' check if this agent no longer have this good ''' return agent[GOODS][g][AMT_AVAIL] == 0 def compl_lst(agent, good): ''' return the complimentary list of this good ''' return agent[GOODS][good][COMPLEMENTS] def adj_add_good_w_comp(agent, good, amt, old_amt): if new_good(old_amt, amt): if is_compl_good(agent, good): incr_util(agent[GOODS], good, amt=amt * STEEP_GRADIENT) # now increase utility of this good's complements: for comp in compl_lst(agent, good): incr_util(agent[GOODS], comp, amt=amt * STEEP_GRADIENT) print(agent[GOODS]) if good_not_have(agent, good): for comp in compl_lst(agent, good): agent[GOODS][comp]['incr'] = 0