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)
Esempio n. 5
0
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()))
Esempio n. 6
0
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)