def budget_balanced_ascending_auction( market: Market, ps_recipe_struct: List[Any], agent_counts: List[int] = None) -> TradeWithMultipleRecipes: """ Calculate the trade and prices using generalized-ascending-auction. Allows multiple recipes, but they must be represented by a *recipe tree*. :param market: contains a list of k categories, each containing several agents. :param ps_recipe_struct: a nested list of integers. Each integer represents a category-index. The nested list represents a tree, where each path from root to leaf represents a recipe. For example: [0, [1, None]] is a single recipe with categories {0,1}. [0, [1, None, 2, None]] is two recipes with categories {0,1} and {0,2}. :return: Trade object, representing the trade and prices. >>> logger.setLevel(logging.DEBUG) >>> # ONE BUYER, ONE SELLER >>> recipe_11 = [0, [1, None]] >>> agent_counts = [1, 2, 1, 2] >>> recipe_1100_1011 = [0, [1, None, 2, [3, None]]] >>> >>> market = Market([AgentCategory("buyer", [101,101,101,101,101]), AgentCategory("seller1", [-1,-90]), AgentCategory("seller2", [-1,-90]), AgentCategory("seller", [-50, -50, -50, -50, -50, -50])]) >>> RecipeTree(market.categories, [0, [1, None, 2, None, 3, None]], [1,2,2,2]).optimal_trade_with_counters() ([([101, -1, -90], {'counter': 1, 'index': 1}), ([101, -1, -90], {'counter': 1, 'index': 2}), ([101, -50, -50], {'counter': 3, 'index': 3}), ([101, -50, -50], {'counter': 3, 'index': 3}), ([101, -50, -50], {'counter': 3, 'index': 3})], 5, 23, 1, 3, {'1': 1, '2': 1, '3': 3}) >>> print(market); print(budget_balanced_ascending_auction(market, [0, [1, None, 2, None, 3, None]], [1,2,2,2])) Traders: [buyer: [101, 101, 101, 101, 101], seller1: [-1, -90], seller2: [-1, -90], seller: [-50, -50, -50, -50, -50, -50]] seller1: 0 potential deals, price=-50.5 seller2: 0 potential deals, price=-50.5 seller: 3 potential deals, price=-50.5 buyer: all 3 remaining traders selected, price=101.0 (seller1 + seller2 + seller): all 3 remaining deals selected 3 deals overall >>> market = Market([AgentCategory("buyer", [101,101,101]), AgentCategory("seller1", [-1,-90]), AgentCategory("seller2", [-1,-90]), AgentCategory("seller", [-50, -50, -50, -50, -50, -50])]) >>> RecipeTree(market.categories, [0, [1, None, 2, None, 3, None]], [1,2,2,2]).optimal_trade_with_counters() ([([101, -1, -90], {'counter': 1, 'index': 1}), ([101, -1, -90], {'counter': 1, 'index': 2}), ([101, -50, -50], {'counter': 1, 'index': 3})], 3, 21, 1, 1, {'1': 1, '2': 1, '3': 1}) >>> print(market); print(budget_balanced_ascending_auction(market, [0, [1, None, 2, None, 3, None]], [1,2,2,2])) Traders: [buyer: [101, 101, 101], seller1: [-1, -90], seller2: [-1, -90], seller: [-50, -50, -50, -50, -50, -50]] seller1: 0 potential deals, price=-50.0 seller2: 0 potential deals, price=-50.0 seller: 2 potential deals, price=-50.0 buyer: 2 out of 3 remaining traders selected, price=100.0 (seller1 + seller2 + seller): all 2 remaining deals selected 2 deals overall >>> market = Market([AgentCategory("buyer", [27.,21., 17.,11., 3.]), AgentCategory("seller", [-4.0, -5.0, -11.0]), AgentCategory("A", [-2.0, -3.0, -11.0]), AgentCategory("B", [-1.0, -2.0, -8.0])]) >>> RecipeTree(market.categories, recipe_1100_1011, agent_counts).optimal_trade_with_counters() ([([27.0, -2.0, -1.0, -2.0], {'counter': 1, 'index': 3}), ([21.0, -4.0, -5.0], {'counter': 1, 'index': 1})], 2, 34.0, 1, 1, {'3': 1, '1': 1}) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_1100_1011, agent_counts)) Traders: [buyer: [27.0, 21.0, 17.0, 11.0, 3.0], seller: [-4.0, -5.0, -11.0], A: [-2.0, -3.0, -11.0], B: [-1.0, -2.0, -8.0]] seller: 1 potential deals, price=-8.5 B: 1 potential deals, price=-7.0 A: all 1 remaining traders selected, price=-3.0 (B): all 1 remaining deals selected buyer: all 2 remaining traders selected, price=17.0 (seller + A): all 2 remaining deals selected 2 deals overall >>> market = Market([AgentCategory("buyer", [17.,11.]), AgentCategory("seller", [-5.0]), AgentCategory("A", [-3.0]), AgentCategory("B", [-2.0])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_1100_1011)) Traders: [buyer: [17.0, 11.0], seller: [-5.0], A: [-3.0], B: [-2.0]] No trade >>> market = Market([AgentCategory("buyer", [9.]), AgentCategory("seller", [-4.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0], seller: [-4.0]] No trade >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0, 8.0], seller: [-4.0]] seller: 1 potential deals, price=-8.0 buyer: all 1 remaining traders selected, price=8.0 (seller): all 1 remaining deals selected 1 deals overall >>> logger.setLevel(logging.WARNING) >>> market = Market([AgentCategory("buyer", [9.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0], seller: [-3.0, -4.0]] No trade >>> logger.setLevel(logging.WARNING) >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0, 8.0], seller: [-3.0, -4.0]] seller: 1 potential deals, price=-4.0 buyer: 1 out of 2 remaining traders selected, price=4.0 (seller): all 1 remaining deals selected 1 deals overall """ logger.info("\n#### Multi-Recipe Budget-Balanced Ascending Auction\n") logger.info(market) logger.info("Procurement-set recipe struct: {}".format(ps_recipe_struct)) logger.info("Procurement-set recipe agent counts: {}".format(agent_counts)) remaining_market = market.clone() recipe_tree = RecipeTree(remaining_market.categories, ps_recipe_struct, agent_counts) logger.info("Tree of recipes: {}".format(recipe_tree.paths_to_leaf())) ps_recipes = recipe_tree.recipes() logger.info("Procurement-set recipes: {}".format(ps_recipes)) optimal_trade, optimal_count, optimal_GFT = recipe_tree.optimal_trade() logger.info("For comparison, the optimal trade has k=%d, GFT=%f: %s\n", optimal_count, optimal_GFT, optimal_trade) # optimal_trade = market.optimal_trade(ps_recipe)[0] #### STOPPED HERE prices = SimultaneousAscendingPriceVectors(ps_recipes, -MAX_VALUE, agent_counts) while True: largest_category_size, combined_category_size, indices_of_prices_to_increase = recipe_tree.largest_categories( indices=True) logger.info("\n") logger.info(remaining_market) logger.info( "Largest category indices are %s. Largest category size = %d, combined category size = %d", indices_of_prices_to_increase, largest_category_size, combined_category_size) if combined_category_size == 0: logger.info("\nCombined category size is 0 - no trade!") logger.info(" Final price-per-unit vector: %s", prices) logger.info(remaining_market) return TradeWithMultipleRecipes( remaining_market.categories, recipe_tree, prices.map_category_index_to_price(), agent_counts) increases = [] for category_index in indices_of_prices_to_increase: category = remaining_market.categories[category_index] target_price = category.lowest_agent_value( ) if category.size() > 0 else MAX_VALUE increases.append((category_index, target_price, category.name)) logger.info("Planned price-increases: %s", increases) prices.increase_prices(increases) map_category_index_to_price = prices.map_category_index_to_price() if prices.status == PriceStatus.STOPPED_AT_ZERO_SUM: logger.info("\nPrice crossed zero.") logger.info(" Final price-per-unit vector: %s", map_category_index_to_price) logger.info(remaining_market) return TradeWithMultipleRecipes(remaining_market.categories, recipe_tree, map_category_index_to_price, agent_counts) remove_lowest_agent(market, remaining_market, map_category_index_to_price, indices_of_prices_to_increase)
def budget_balanced_ascending_auction( market: Market, ps_recipes: List[List]) -> TradeWithMultipleRecipes: """ Calculate the trade and prices using generalized-ascending-auction. Allows multiple recipes, but they must all be binary, and must all start with 1. E.g.: [ [1,1,0,0], [1,0,1,1] ] I.e., there is 1 buyer category and n-1 seller categories. Each buyer may wish to buy a different combination of products. :param market: contains a list of k categories, each containing several agents. :param ps_recipes: a list of lists of integers, one integer per category. Each integer i represents the number of agents of category i that should be in each procurement-set. :return: Trade object, representing the trade and prices. >>> # ONE BUYER, ONE SELLER >>> recipe_11 = [[1,1]] >>> >>> market = Market([AgentCategory("buyer", [9.]), AgentCategory("seller", [-4.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0], seller: [-4.0]] No trade >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0, 8.0], seller: [-4.0]] No trade >>> logger.setLevel(logging.WARNING) >>> market = Market([AgentCategory("buyer", [9.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0], seller: [-3.0, -4.0]] seller: [-3.0]: all 1 agents trade and pay -4.0 buyer: [9.0]: all 1 agents trade and pay 4.0 >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0, 8.0], seller: [-3.0, -4.0]] seller: [-3.0, -4.0]: random 1 out of 2 agents trade and pay -8.0 buyer: [9.0]: all 1 agents trade and pay 8.0 """ num_recipes = len(ps_recipes) if num_recipes < 1: raise ValueError("Empty list of recipes") num_categories = market.num_categories for i, ps_recipe in enumerate(ps_recipes): if len(ps_recipe) != num_categories: raise ValueError( "There are {} categories but {} elements in PS recipe #{}". format(num_categories, len(ps_recipe), i)) if any((r != 1 and r != 0) for r in ps_recipe): raise ValueError( "Currently, the multi-recipe protocol supports only recipes of zeros and ones; {} was given" .format(ps_recipe)) logger.info("\n#### Multi-Recipe Budget-Balanced Ascending Auction\n") logger.info(market) logger.info("Procurement-set recipes: {}".format(ps_recipes)) map_category_index_to_recipe_indices, common_category_indices, map_recipe_index_to_unique_category_indices = \ _analyze_recipes(num_categories, ps_recipes) # Calculating the optimal trade with multiple recipes is left for future work. # optimal_trade = market.optimal_trade(ps_recipe)[0] # logger.info("For comparison, the optimal trade is: %s\n", optimal_trade) remaining_market = market.clone() prices = SimultaneousAscendingPriceVectors(ps_recipes, -MAX_VALUE) # Functions for calculating the number of potential PS that can be supported by a category: # Currently we assume a recipe of ones, so the number of potential PS is simply the category size: fractional_potential_ps = lambda category_index: remaining_market.categories[ category_index].size() integral_potential_ps = lambda category_index: remaining_market.categories[ category_index].size() while True: # find a category with a largest number of potential PS, and increase its price largest_common_category_index = max(common_category_indices, key=fractional_potential_ps) \ if len(common_category_indices)>0 \ else None largest_common_category_size = fractional_potential_ps(largest_common_category_index) \ if len(common_category_indices) > 0 \ else 0 logger.info("Largest common category is %d and its size is %d", largest_common_category_index, largest_common_category_size) map_recipe_index_to_largest_unique_category_index = [ max(unique_category_indices, key=fractional_potential_ps) for unique_category_indices in map_recipe_index_to_unique_category_indices ] if len(map_recipe_index_to_largest_unique_category_index) == 0: raise ValueError("No unique categories") unique_categories_size = sum([ fractional_potential_ps(largest_unique_category_index) for largest_unique_category_index in map_recipe_index_to_largest_unique_category_index ]) logger.info( "Largest unique categories are %s and their total size is %d", map_recipe_index_to_largest_unique_category_index, unique_categories_size) if unique_categories_size == 0: logger.info("\nThe unique categories %s became empty - no trade!", map_recipe_index_to_largest_unique_category_index) logger.info(" Final price-per-unit vector: %s", prices) logger.info(remaining_market) return TradeWithMultipleRecipes( remaining_market.categories, ps_recipes, prices.map_category_index_to_price()) if largest_common_category_size >= unique_categories_size: logger.info( "Raising price of the largest common category (%d) in all recipes", largest_common_category_index) main_category_index = largest_common_category_index main_category = remaining_market.categories[main_category_index] logger.info("%s before: %d agents remain", main_category.name, main_category.size()) increases = [ (main_category_index, main_category.lowest_agent_value(), main_category.name) ] * num_recipes else: # largest_common_category_size < unique_categories_size logger.info( "Raising price of the largest unique categories in each recipe: %s", map_recipe_index_to_largest_unique_category_index) increases = [] for recipe_index, main_category_index in enumerate( map_recipe_index_to_largest_unique_category_index): main_category = remaining_market.categories[ main_category_index] logger.info("%s before: %d agents remain", main_category.name, main_category.size()) if main_category.size() == 0: logger.info( "\nThe %s category became empty - no trade in recipe %d", main_category.name, recipe_index) del ps_recipes[recipe_index] if len(ps_recipes) > 0: return budget_balanced_ascending_auction( market, ps_recipes) else: logger.info("\nNo recipes left - no trade!") logger.info(" Final price-per-unit vector: %s", prices) logger.info(remaining_market) return TradeWithMultipleRecipes( remaining_market.categories, ps_recipes, map_category_index_to_price) increases.append( (main_category_index, main_category.lowest_agent_value(), main_category.name)) if len(increases) == 0: raise ValueError("No increases!") logger.info("Planned increases: %s", increases) prices.increase_prices(increases) map_category_index_to_price = prices.map_category_index_to_price() if prices.status == PriceStatus.STOPPED_AT_ZERO_SUM: logger.info("\nPrice crossed zero.") logger.info(" Final price-per-unit vector: %s", map_category_index_to_price) logger.info(remaining_market) return TradeWithMultipleRecipes(remaining_market.categories, ps_recipes, map_category_index_to_price) for category_index in range(num_categories): category = remaining_market.categories[category_index] if map_category_index_to_price[category_index] is not None \ and category.size()>0 \ and category.lowest_agent_value() <= map_category_index_to_price[category_index]: category.remove_lowest_agent() logger.info( "{} after: {} agents remain, {} PS supported".format( category.name, category.size(), integral_potential_ps(category_index)))
def budget_balanced_ascending_auction( market: Market, ps_recipe_struct: List[Any]) -> TradeWithMultipleRecipes: """ Calculate the trade and prices using generalized-ascending-auction. Allows multiple recipes, but they must be represented by a *recipe tree*. :param market: contains a list of k categories, each containing several agents. :param ps_recipe_struct: a nested list of integers. Each integer represents a category-index. The nested list represents a tree, where each path from root to leaf represents a recipe. For example: [0, [1, None]] is a single recipe with categories {0,1}. [0, [1, None, 2, None]] is two recipes with categories {0,1} and {0,2}. :return: Trade object, representing the trade and prices. >>> logger.setLevel(logging.WARNING) >>> # ONE BUYER, ONE SELLER >>> recipe_11 = [0, [1, None]] >>> >>> market = Market([AgentCategory("buyer", [9.]), AgentCategory("seller", [-4.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0], seller: [-4.0]] No trade >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0, 8.0], seller: [-4.0]] seller: 1 potential deals, price=-8.0 buyer: all 1 traders selected, price=8.0 seller: all 1 traders selected 1 deals overall >>> logger.setLevel(logging.WARNING) >>> market = Market([AgentCategory("buyer", [9.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0], seller: [-3.0, -4.0]] No trade >>> logger.setLevel(logging.WARNING) >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction(market, recipe_11)) Traders: [buyer: [9.0, 8.0], seller: [-3.0, -4.0]] seller: 1 potential deals, price=-4.0 buyer: 1 out of 2 traders selected, price=4.0 seller: all 1 traders selected 1 deals overall """ logger.info("\n#### Multi-Recipe Budget-Balanced Ascending Auction\n") logger.info(market) logger.info("Procurement-set recipe struct: {}".format(ps_recipe_struct)) remaining_market = market.clone() recipe_tree = RecipeTree(remaining_market.categories, ps_recipe_struct) logger.info("Tree of recipes: {}".format(recipe_tree.paths_to_leaf())) ps_recipes = recipe_tree.recipes() logger.info("Procurement-set recipes: {}".format(ps_recipes)) optimal_trade, optimal_count, optimal_GFT = recipe_tree.optimal_trade() logger.info("For comparison, the optimal trade has k=%d, GFT=%f: %s\n", optimal_count, optimal_GFT, optimal_trade) # optimal_trade = market.optimal_trade(ps_recipe)[0] #### STOPPED HERE prices = SimultaneousAscendingPriceVectors(ps_recipes, -MAX_VALUE) while True: largest_category_size, combined_category_size, indices_of_prices_to_increase = recipe_tree.largest_categories( indices=True) logger.info("\n") logger.info(remaining_market) logger.info( "Largest category indices are %s. Largest category size = %d, combined category size = %d", indices_of_prices_to_increase, largest_category_size, combined_category_size) if combined_category_size == 0: logger.info("\nCombined category size is 0 - no trade!") logger.info(" Final price-per-unit vector: %s", prices) logger.info(remaining_market) return TradeWithMultipleRecipes( remaining_market.categories, recipe_tree, prices.map_category_index_to_price()) increases = [] for category_index in indices_of_prices_to_increase: category = remaining_market.categories[category_index] target_price = category.lowest_agent_value( ) if category.size() > 0 else MAX_VALUE increases.append((category_index, target_price, category.name)) logger.info("Planned price-increases: %s", increases) prices.increase_prices(increases) map_category_index_to_price = prices.map_category_index_to_price() if prices.status == PriceStatus.STOPPED_AT_ZERO_SUM: logger.info("\nPrice crossed zero.") logger.info(" Final price-per-unit vector: %s", map_category_index_to_price) logger.info(remaining_market) return TradeWithMultipleRecipes(remaining_market.categories, recipe_tree, map_category_index_to_price) for category_index in range(market.num_categories): category = remaining_market.categories[category_index] if map_category_index_to_price[category_index] is not None \ and category.size()>0 \ and category.lowest_agent_value() <= map_category_index_to_price[category_index]: category.remove_lowest_agent() logger.info("{} after: {} agents remain".format( category.name, category.size()))
def budget_balanced_ascending_auction( market:Market, ps_recipes: list)->TradeWithMultipleRecipes: """ Calculate the trade and prices using generalized-ascending-auction. Allows multiple recipes, but only of the following kind: [ [1,0,0,x], [0,1,0,y], [0,0,1,z] ] (i.e., there are n-1 buyer categories and 1 seller category. One agent of category 1 buys x units; of category 2 buys y units; of category 3 buys z units; etc.) :param market: contains a list of k categories, each containing several agents. :param ps_recipes: a list of lists of integers, one integer per category. Each integer i represents the number of agents of category i that should be in each procurement-set. :return: Trade object, representing the trade and prices. >>> # ONE BUYER, ONE SELLER >>> market = Market([AgentCategory("buyer", [9.]), AgentCategory("seller", [-4.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [[1,1]])) Traders: [buyer: [9.0], seller: [-4.0]] No trade >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [[1,1]])) Traders: [buyer: [9.0, 8.0], seller: [-4.0]] No trade >>> market = Market([AgentCategory("buyer", [9.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [[1,1]])) Traders: [buyer: [9.0], seller: [-3.0, -4.0]] seller: [-3.0]: all 1 agents trade and pay -4.0 buyer: [9.0]: all 1 agents trade and pay 4.0 >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [[1,1]])) Traders: [buyer: [9.0, 8.0], seller: [-3.0, -4.0]] seller: [-3.0, -4.0]: random 1 out of 2 agents trade and pay -8.0 buyer: [9.0]: all 1 agents trade and pay 8.0 >>> # ONE BUYER, TWO SELLERS >>> market = Market([AgentCategory("buyer", [9.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [[1,2]])) Traders: [buyer: [9.0], seller: [-3.0, -4.0]] No trade >>> market = Market([AgentCategory("buyer", [9., 8., 7., 6.]), AgentCategory("seller", [-6., -5., -4.,-3.,-2.,-1.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [[1,2]])) Traders: [buyer: [9.0, 8.0, 7.0, 6.0], seller: [-1.0, -2.0, -3.0, -4.0, -5.0, -6.0]] seller: [-1.0, -2.0, -3.0, -4.0]: random 2 out of 4 agents trade and pay -4.0 buyer: [9.0]: all 1 agents trade and pay 8.0 """ logger.info("\n#### Budget-Balanced Ascending Auction with Multiple Recipes - n-1 buyer categories\n") logger.info(market) logger.info("Procurement-set recipes: %s", ps_recipes) map_buyer_category_to_seller_count = _convert_recipes_to_seller_counts(ps_recipes, market.num_categories) logger.info("Map buyer category index to seller count: %s", map_buyer_category_to_seller_count) # NOTE: Calculating the optimal trade cannot be done greedily - # it requires solving a restricted instance of Knapsack. # optimal_trade = market.optimal_trade(ps_recipe, max_iterations=max_iterations)[0] # logger.info("For comparison, the optimal trade is: %s\n", optimal_trade) remaining_market = market.clone() buyer_categories = remaining_market.categories[:-1] num_buyer_categories = market.num_categories-1 seller_category = remaining_market.categories[-1] prices = AscendingPriceVector([1, 1], -MAX_VALUE) buyer_price_index = 0 seller_price_index = 1 # prices[0] represents the price for all buyer-categories per single unit. # prices[1] represents the price for all sellers. try: num_units_offered = len(seller_category) num_units_demanded = sum([len(buyer_categories[i])*map_buyer_category_to_seller_count[i] for i in range(num_buyer_categories)]) target_unit_count = min(num_units_demanded, num_units_offered) logger.info("%d units demanded by buyers, %d units offered by sellers, minimum is %d", num_units_demanded, num_units_offered, target_unit_count) while True: logger.info("Prices: %s, Target unit count: %d", prices, target_unit_count) price_index = buyer_price_index while True: num_units_demanded = sum([len(buyer_categories[i]) * map_buyer_category_to_seller_count[i] for i in range(num_buyer_categories)]) logger.info(" Buyers demand %d units", num_units_demanded) if num_units_demanded == 0: raise EmptyCategoryException() if num_units_demanded <= target_unit_count: break map_buyer_category_to_lowest_value = [category.lowest_agent_value() for category in buyer_categories] logger.debug(" map_buyer_category_to_lowest_value=%s", map_buyer_category_to_lowest_value) map_buyer_category_to_lowest_value_per_unit = [value / count for value,count in zip(map_buyer_category_to_lowest_value,map_buyer_category_to_seller_count)] logger.debug(" map_buyer_category_to_lowest_value_per_unit=%s", map_buyer_category_to_lowest_value_per_unit) category_index_with_lowest_value_per_unit = min(range(num_buyer_categories), key=lambda i:map_buyer_category_to_lowest_value_per_unit[i]) category_with_lowest_value_per_unit = buyer_categories[category_index_with_lowest_value_per_unit] lowest_value_per_unit = map_buyer_category_to_lowest_value_per_unit[category_index_with_lowest_value_per_unit] logger.info(" lowest value per unit is %f, of category %d (%s)", lowest_value_per_unit, category_index_with_lowest_value_per_unit, category_with_lowest_value_per_unit.name) prices.increase_price_up_to_balance(price_index, category_with_lowest_value_per_unit.lowest_agent_value()/map_buyer_category_to_seller_count[category_index_with_lowest_value_per_unit], category_with_lowest_value_per_unit.name) category_with_lowest_value_per_unit.remove_lowest_agent() category = seller_category # logger.info("\n### Step 1a: balancing the sellers (%s)", category.name) price_index = seller_price_index while True: num_units_offered = len(category) logger.info(" Sellers offer %d units", num_units_offered) if num_units_offered == 0: raise EmptyCategoryException() if num_units_offered <= target_unit_count: break prices.increase_price_up_to_balance(price_index, category.lowest_agent_value(), category.name) category.remove_lowest_agent() target_unit_count -= 1 except EmptyCategoryException: logger.info("\nOne of the categories became empty. No trade!") logger.info(" Final price-per-unit vector: %s", prices) # Construct the final price-vector: buyer_price_per_unit = prices[buyer_price_index] seller_price_per_unit = prices[seller_price_index] final_prices = \ [buyer_price_per_unit * unit_count for unit_count in map_buyer_category_to_seller_count] + \ [seller_price_per_unit] logger.info(" %s", remaining_market) return TradeWithMultipleRecipes(remaining_market.categories, map_buyer_category_to_seller_count, final_prices)
def budget_balanced_ascending_auction_twolevels( market: Market, ps_recipe_counts: List[Any]) -> TradeWithMultipleRecipes: """ Calculate the trade and prices using generalized-ascending-auction. Allows multiple non-binary recipes, but they must be represented by a *recipe tree* with two levels. :param market: contains a list of k categories, each containing several agents. category 0 is the root; the others are its children. :param ps_recipe_counts: Required number r_g for each category g. :return: Trade object, representing the trade and prices. >>> logger.setLevel(logging.INFO) >>> # ONE BUYER, ONE SELLER >>> recipe_11 = [1, 1] >>> >>> market = Market([AgentCategory("buyer", [9.]), AgentCategory("seller", [-4.])]) >>> print(market); print(budget_balanced_ascending_auction_twolevels(market, recipe_11)) Traders: [buyer: [9.0], seller: [-4.0]] No trade >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.])]) >>> print(market); print(budget_balanced_ascending_auction_twolevels(market, recipe_11)) Traders: [buyer: [9.0, 8.0], seller: [-4.0]] seller: 1 potential deals, price=-8.0 buyer: all 1 traders selected, price=8.0 seller: all 1 traders selected 1 deals overall >>> logger.setLevel(logging.WARNING) >>> market = Market([AgentCategory("buyer", [9.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction_twolevels(market, recipe_11)) Traders: [buyer: [9.0], seller: [-3.0, -4.0]] No trade >>> logger.setLevel(logging.WARNING) >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction_twolevels(market, recipe_11)) Traders: [buyer: [9.0, 8.0], seller: [-3.0, -4.0]] seller: 1 potential deals, price=-4.0 buyer: 1 out of 2 traders selected, price=4.0 seller: all 1 traders selected 1 deals overall """ logger.info("\n#### Multi-Recipe Budget-Balanced Ascending Auction\n") logger.info(market) root_count = ps_recipe_counts[0] logger.info("Root category required count: %d", root_count) child_counts = ps_recipe_counts[1:] num_recipes = len(child_counts) logger.info("Child category required counts: %s", child_counts) ps_recipes = [[root_count] + recipe_index * [0] + [child_counts[recipe_index]] + (num_recipes - recipe_index - 1) * [0] for recipe_index in range(num_recipes)] logger.info("PS recipes: %s", ps_recipes) prices = SimultaneousAscendingPriceVectors(ps_recipes, -MAX_VALUE) remaining_market = market.clone() while True: root_category = remaining_market.categories[0] root_category_size = len(root_category) normalized_root_category_size = root_category_size / root_count child_category_sizes = [ len(category) for category in remaining_market.categories[1:] ] normalized_child_category_size = sum([ floor(child_category_size / child_count) for child_category_size, child_count in zip( child_category_sizes, child_counts) ]) logger.info("Root category size: %g, normalized: %g", root_category_size, normalized_root_category_size) logger.info("Child category sizes: %s, normalized: %g", child_category_sizes, normalized_child_category_size) if normalized_root_category_size > normalized_child_category_size: # increase only the root price prices_to_increase = [0] else: # increase the children prices prices_to_increase = range(1, market.num_categories) increases = [] for category_index in prices_to_increase: category = remaining_market.categories[category_index] target_price = category.lowest_agent_value( ) if category.size() > 0 else MAX_VALUE increases.append((category_index, target_price, category.name)) logger.info("\n") # logger.info(remaining_market) # logger.info("Largest category indices are %s. Largest category size = %d, combined category size = %d", indices_of_prices_to_increase, largest_category_size, combined_category_size) # if combined_category_size == 0: # logger.info("\nCombined category size is 0 - no trade!") # logger.info(" Final price-per-unit vector: %s", prices) # logger.info(remaining_market) # return TradeWithMultipleRecipes(remaining_market.categories, recipe_tree, prices.map_category_index_to_price()) logger.info("Planned price-increases: %s", increases) prices.increase_prices(increases) map_category_index_to_price = prices.map_category_index_to_price() if prices.status == PriceStatus.STOPPED_AT_ZERO_SUM: logger.info("\nPrice crossed zero.") logger.info(" Final price-per-unit vector: %s", map_category_index_to_price) logger.info(remaining_market) return TradeWithMultipleRecipes(remaining_market.categories, ps_recipe_counts, map_category_index_to_price) else: # Remove agents who do not want to trade in the new prices: for category_index in prices_to_increase: category = remaining_market.categories[category_index] if map_category_index_to_price[category_index] is not None \ and category.size()>0 \ and category.lowest_agent_value() <= map_category_index_to_price[category_index]: category.remove_lowest_agent() logger.info("{} after: {} agents remain".format( category.name, category.size()))
def budget_balanced_ascending_auction( market: Market, ps_recipe: list, max_iterations=999999999) -> TradeWithSinglePrice: """ Calculate the trade and prices using generalized-ascending-auction. :param market: contains a list of k categories, each containing several agents. :param ps_recipe: a list of integers, one integer per category. Each integer i represents the number of agents of category i that should be in each procurement-set. :return: Trade object, representing the trade and prices. >>> # ONE BUYER, ONE SELLER >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [1,1])) Traders: [buyer: [9.0, 8.0], seller: [-4.0]] No trade >>> market = Market([AgentCategory("seller", [-4.]), AgentCategory("buyer", [9.,8.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [1,1])) Traders: [seller: [-4.0], buyer: [9.0, 8.0]] seller: [-4.0]: all 1 agents trade and pay -8.0 buyer: [9.0]: all 1 agents trade and pay 8.0 >>> market = Market([AgentCategory("buyer", [9.,8.]), AgentCategory("seller", [-4.,-3.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [1,1])) Traders: [buyer: [9.0, 8.0], seller: [-3.0, -4.0]] buyer: [9.0]: all 1 agents trade and pay 8.0 seller: [-3.0, -4.0]: random 1 out of 2 agents trade and pay -8.0 >>> # ONE BUYER, ONE SELLER, ONE MEDIATOR >>> market = Market([AgentCategory("seller", [-4.,-3.]), AgentCategory("buyer", [9.,8.]), AgentCategory("mediator", [-1.,-2.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [1,1,1])) Traders: [seller: [-3.0, -4.0], buyer: [9.0, 8.0], mediator: [-1.0, -2.0]] seller: [-3.0]: all 1 agents trade and pay -4.0 buyer: [9.0]: all 1 agents trade and pay 8.0 mediator: [-1.0, -2.0]: random 1 out of 2 agents trade and pay -4.0 >>> market = Market([AgentCategory("buyer", [9.,8.,7.]), AgentCategory("mediator", [-1.,-2.,-3.]), AgentCategory("seller", [-4.,-3.,-2.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [1,1,1])) Traders: [buyer: [9.0, 8.0, 7.0], mediator: [-1.0, -2.0, -3.0], seller: [-2.0, -3.0, -4.0]] buyer: [9.0, 8.0]: all 2 agents trade and pay 7.0 mediator: [-1.0, -2.0]: all 2 agents trade and pay -3.0 seller: [-2.0, -3.0, -4.0]: random 2 out of 3 agents trade and pay -4.0 >>> # ONE BUYER, TWO SELLERS >>> market = Market([AgentCategory("buyer", [9., 8., 7., 6.]), AgentCategory("seller", [-6., -5., -4.,-3.,-2.,-1.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [1,2])) Traders: [buyer: [9.0, 8.0, 7.0, 6.0], seller: [-1.0, -2.0, -3.0, -4.0, -5.0, -6.0]] buyer: [9.0]: all 1 agents trade and pay 8.0 seller: [-1.0, -2.0, -3.0, -4.0]: random 2 out of 4 agents trade and pay -4.0 >>> market = Market([AgentCategory("seller", [-4.,-3.,-2.,-1.]), AgentCategory("buyer", [9.,8.])]) >>> print(market); print(budget_balanced_ascending_auction(market, [2,1])) Traders: [seller: [-1.0, -2.0, -3.0, -4.0], buyer: [9.0, 8.0]] seller: [-1.0, -2.0, -3.0]: random 2 out of 3 agents trade and pay -4.0 buyer: [9.0, 8.0]: random 1 out of 2 agents trade and pay 8.0 >>> # ONE SELLER, ONE BUYER, ZERO MEDIATORS >>> market = Market([AgentCategory("seller", [-4.]), AgentCategory("buyer", [9.,8.]), AgentCategory("mediator", [-5, -7])]) >>> print(market); print(budget_balanced_ascending_auction(market, [1,1,0])) Traders: [seller: [-4.0], buyer: [9.0, 8.0], mediator: [-5, -7]] seller: [-4.0]: all 1 agents trade and pay -8.0 buyer: [9.0]: all 1 agents trade and pay 8.0 """ num_categories = market.num_categories if len(ps_recipe) != num_categories: raise ValueError( "There are {} categories but {} elements in the PS recipe".format( num_categories, len(ps_recipe))) relevant_category_indices = [ i for i in range(num_categories) if ps_recipe[i] > 0 ] logger.info("\n#### Budget-Balanced Ascending Auction\n") logger.info(market) logger.info("Procurement-set recipe: {}".format(ps_recipe)) optimal_trade = market.optimal_trade(ps_recipe, max_iterations=max_iterations)[0] logger.info("For comparison, the optimal trade is: %s\n", optimal_trade) remaining_market = market.clone() prices = AscendingPriceVector(ps_recipe, -MAX_VALUE) # Functions for calculating the number of potential PS that can be supported by a category: fractional_potential_ps = lambda category_index: remaining_market.categories[ category_index].size() / ps_recipe[category_index] integral_potential_ps = lambda category_index: math.floor( remaining_market.categories[category_index].size() / ps_recipe[ category_index]) while True: # find a category with a largest number of potential PS, and increase its price main_category_index = max(relevant_category_indices, key=fractional_potential_ps) main_category = remaining_market.categories[main_category_index] logger.info("Chosen category: {} with {} agents and ratio {}".format( main_category.name, main_category.size(), fractional_potential_ps(main_category_index))) if main_category.size() == 0: logger.info("\nThe %s category became empty - no trade!", main_category.name) logger.info(" Final price-per-unit vector: %s", prices) break prices.increase_price_up_to_balance(main_category_index, main_category.lowest_agent_value(), main_category.name) if prices.status == PriceStatus.STOPPED_AT_ZERO_SUM: logger.info("\nPrice crossed zero.") logger.info(" Final price-per-unit vector: %s", prices) break main_category.remove_lowest_agent() logger.info( " {} price increases to {}: {} agents and ratio {}".format( main_category.name, prices[main_category_index], main_category.size(), fractional_potential_ps(main_category_index))) logger.info(remaining_market) return TradeWithSinglePrice(remaining_market.categories, ps_recipe, prices.prices)