Example #1
0
    def EFAllocateRec(a: float, b: float) -> List[List[Tuple[float, float]]]:
        """
        exactly the same as EFAllocate, but creates the envy free allocation
        within a given interval.

        :param a: starting point of the interval to allocate.
        :param b: end point of the interval to allocate.
        :return: a list of lists of intervals mathching the sandwich allocation.
        """

        if round(a, roundAcc) == round(b, roundAcc):
            return Allocation(agents, len(agents) * [[]])

        #1
        numAgents = len(agents)
        ret = Allocation(agents, len(agents) * [[]])
        cover = Cover(a, b, agents, roundAcc=roundAcc)

        #2
        for inter in cover:
            pieces = sandwichAllocation(a, b, inter[0], inter[1], numAgents)
            for permuted_pieces in permutations(pieces):
                if is_envyfree(agents, permuted_pieces, roundAcc):
                    logger.info(
                        "allocation from %f to %f completed with sandwich allocation.",
                        a, b)
                    return permuted_pieces

        #3
        logger.info("no valid allocation without recursion from %f to %f.", a,
                    b)
        logger.info(
            "splitting to sub-allocations using the cover of the whole interval."
        )
        points = [a]
        for interval in cover:
            points.append(interval[1])
        points.sort()
        ret = num_of_agents * [[]]
        for i in range(1, len(points)):
            logger.info("creating allocation from %f to %f:", points[i - 1],
                        points[i])
            alloc = EFAllocateRec(points[i - 1], points[i])
            # merge the existing allocation with the new allocation:
            merged_alloc = [
                ret[i_agent] + alloc[i_agent]
                for i_agent in range(num_of_agents)
            ]
            ret = merged_alloc
        logger.info("covered allocation from %f to %f using merging.", a, b)
        return ret
Example #2
0
def one_directional_bag_filling(values, thresholds:List[float]):
	"""
	The simplest bag-filling procedure: fills a bag in the given order of objects.
	
	:param valuations: a valuation matrix (a row for each agent, a column for each object).
	:param thresholds: determines, for each agent, the minimum value that should be in a bag before the agent accepts it.

	>>> one_directional_bag_filling(values=[[11,33],[44,22]], thresholds=[30,30])
	Agent #0 gets {1} with value 33.
	Agent #1 gets {0} with value 44.
	<BLANKLINE>
	>>> one_directional_bag_filling(values=[[11,33],[44,22]], thresholds=[10,10])
	Agent #0 gets {0} with value 11.
	Agent #1 gets {1} with value 22.
	<BLANKLINE>
	>>> one_directional_bag_filling(values=[[11,33],[44,22]], thresholds=[40,30])
	Agent #0 gets {} with value 0.
	Agent #1 gets {0} with value 44.
	<BLANKLINE>
	"""
	values = ValuationMatrix(values)
	if len(thresholds) != values.num_of_agents:
		raise ValueError(f"Number of valuations {values.num_of_agents} differs from number of thresholds {len(thresholds)}")

	allocation = SequentialAllocation(values.agents(), values.objects(), logger)
	bag = Bag(values, thresholds)
	while True:
		(willing_agent, allocated_objects) = bag.fill(allocation.remaining_objects, allocation.remaining_agents)
		if willing_agent is None:  break
		allocation.let_agent_get_objects(willing_agent, allocated_objects)
		bag.reset()
	return Allocation(values, allocation.bundles)
Example #3
0
def last_diminisher(agents: List[Agent]) -> Allocation:
    """
    :param agents: a list of Agent objects.
    :return: a proportional cake-allocation.

    >>> from fairpy.agents import PiecewiseConstantAgent
    >>> from fairpy.cake.pieces import round_allocation
    >>> Alice = PiecewiseConstantAgent([33,33], "Alice")
    >>> last_diminisher([Alice])
    Alice gets {(0, 2)} with value 66.
    <BLANKLINE>
    >>> George = PiecewiseConstantAgent([11,55], "George")
    >>> last_diminisher([Alice, George])
    Alice gets {(0, 1.0)} with value 33.
    George gets {(1.0, 2)} with value 55.
    <BLANKLINE>
    >>> last_diminisher([George, Alice])
    George gets {(1.0, 2)} with value 55.
    Alice gets {(0, 1.0)} with value 33.
    <BLANKLINE>
    >>> Abraham = PiecewiseConstantAgent([4,1,1], "Abraham")
    >>> round_allocation(last_diminisher([Abraham, George, Alice]), 3)
    Abraham gets {(0, 0.5)} with value 2.
    George gets {(1.167, 2)} with value 45.8.
    Alice gets {(0.5, 1.167)} with value 22.
    <BLANKLINE>
    """
    num_of_agents = len(agents)
    if num_of_agents == 0:
        raise ValueError("There must be at least one agent")
    pieces = num_of_agents * [None]
    start = 0
    active_agents = list(range(num_of_agents))
    last_diminisher_recursive(start, agents, active_agents, pieces)
    return Allocation(agents, pieces)
Example #4
0
def round_allocation(allocation: Allocation, digits: int = 3) -> Allocation:
    """
    Rounds all the pieces in the given allocaton. 
    For presentation purposes.
    """
    rounded_bundles = [
        round_bundle(bundle, digits) for bundle in allocation.bundles
    ]
    return Allocation(allocation.agents, rounded_bundles)
Example #5
0
def symmetric_protocol(agents: List[Agent]) -> Allocation:
    """
    Symmetric cut-and-choose protocol: both agents cut, the manager chooses who gets what.

    :param agents: a list that must contain exactly 2 Agent objects.
    :return: a proportional and envy-free allocation.

    >>> Alice = PiecewiseConstantAgent([33,33], "Alice")
    >>> George = PiecewiseConstantAgent([11,55], "George")
    >>> symmetric_protocol([Alice, George])
    Alice gets {(0, 1.2)} with value 39.6.
    George gets {(1.2, 2)} with value 44.
    <BLANKLINE>
    >>> symmetric_protocol([George, Alice])
    George gets {(1.2, 2)} with value 44.
    Alice gets {(0, 1.2)} with value 39.6.
    <BLANKLINE>

    >>> Alice = PiecewiseConstantAgent([33,33,33], "Alice")
    >>> symmetric_protocol([Alice, George])
    Alice gets {(1.45, 3)} with value 51.2.
    George gets {(0, 1.45)} with value 35.8.
    <BLANKLINE>
    >>> symmetric_protocol([George, Alice])
    George gets {(0, 1.45)} with value 35.8.
    Alice gets {(1.45, 3)} with value 51.2.
    <BLANKLINE>
    """

    num_of_agents = len(agents)
    if num_of_agents != 2:
        raise ValueError("Cut and choose works only for two agents")

    pieces = num_of_agents * [None]

    marks = [agent.mark(0, agent.total_value() / 2) for agent in agents]
    logger.info("The agents mark at %f, %f", marks[0], marks[1])
    cut = sum(marks) / 2
    logger.info("The cake is cut at %f.", cut)

    if marks[0] < marks[1]:
        logger.info("%s's mark is to the left of %s's mark.", agents[0].name(),
                    agents[1].name())
        pieces[0] = [(0, cut)]
        pieces[1] = [(cut, agents[1].cake_length())]
    else:
        logger.info("%s's mark is to the left of %s's mark.", agents[1].name(),
                    agents[0].name())
        pieces[1] = [(0, cut)]
        pieces[0] = [(cut, agents[0].cake_length())]

    return Allocation(agents, pieces)
Example #6
0
def asymmetric_protocol(agents: List[Agent]) -> Allocation:
    """
    Asymmetric cut-and-choose protocol: one cuts and the other chooses.

    :param agents: a list that must contain exactly 2 Agent objects.
    :return: a proportional and envy-free allocation.

    >>> Alice = PiecewiseConstantAgent([33,33], "Alice")
    >>> George = PiecewiseConstantAgent([11,55], "George")
    >>> asymmetric_protocol([Alice, George])
    Alice gets {(0, 1.0)} with value 33.
    George gets {(1.0, 2)} with value 55.
    <BLANKLINE>
    >>> asymmetric_protocol([George, Alice])
    George gets {(1.4, 2)} with value 33.
    Alice gets {(0, 1.4)} with value 46.2.
    <BLANKLINE>

    >>> Alice = PiecewiseConstantAgent([33,33,33], "Alice")
    >>> asymmetric_protocol([Alice, George])
    Alice gets {(1.5, 3)} with value 49.5.
    George gets {(0, 1.5)} with value 38.5.
    <BLANKLINE>
    >>> asymmetric_protocol([George, Alice])
    George gets {(0, 1.4)} with value 33.
    Alice gets {(1.4, 3)} with value 52.8.
    <BLANKLINE>
    """

    num_of_agents = len(agents)
    if num_of_agents != 2:
        raise ValueError("Cut and choose works only for two agents")

    pieces = num_of_agents * [None]

    (cutter,
     chooser) = agents  # equivalent to: cutter=agents[0]; chooser=agents[1]
    cut = cutter.mark(0, cutter.total_value() / 2)
    logger.info("The cutter (%s) cuts at %.2f.", cutter.name(), cut)

    if chooser.eval(0, cut) > chooser.total_value() / 2:
        logger.info("The chooser (%s) chooses the leftmost piece.",
                    chooser.name())
        pieces[1] = [(0, cut)]
        pieces[0] = [(cut, cutter.cake_length())]
    else:
        logger.info("The chooser (%s) chooses the rightmost piece.",
                    chooser.name())
        pieces[1] = [(cut, chooser.cake_length())]
        pieces[0] = [(0, cut)]
    return Allocation(agents, pieces)
def allocationToOnePiece(alloction:List[List[tuple]],agents:List[Agent])->Allocation:
    """
    Get a fully allocation of (0,1) that every agent have a list of adjacent pieces and return allocation
        of the union of each list
    :param alloction: An fully allocation of (0,1)
    :param agents: A list of agents
    :return: New alloction of (0,1)
    """
    new_pieces = len(agents)*[None]
    for pieces,i in  zip(alloction,range(len(alloction))):
        if (pieces == None):
            continue
        new_pieces[i] = [intervalUnionFromList(pieces)]
    return Allocation(agents, new_pieces)
def divide(agents: List[Agent], epsilon: float) -> Allocation:
    """
    this function gets a list of agents and epsilon and returns an approximation of the division
    :param agents: the players
    :param epsilon: a float
    :return: starting points and end points of the cuts

    >>> from fairpy.agents import PiecewiseConstantAgent
    >>> a = PiecewiseConstantAgent([0.25, 0.5, 0.25], name="Alice")
    >>> b = PiecewiseConstantAgent([0.23, 0.7, 0.07], name="Bob")
    >>> round_allocation(divide([a,b], 0.2))
    Alice gets {(0, 0.8)} with value 0.2.
    Bob gets {(0.8, 1.791)} with value 0.6.
    <BLANKLINE>
    """
    logger.info(
        "\nStep 1: Discretizing the cake to parts with value at most epsilon=%f",
        epsilon)
    items = discretization_procedure(agents, epsilon)
    logger.info("  Discretized cake: ")
    logger.info(items)

    logger.info("\nStep 2: Evaluation")
    matrix = get_players_valuation(agents, items)
    logger.info("  Agents' values: ")
    for line in matrix:
        logger.info(line)

    logger.info("\nStep 3: Discrete allocation")
    result = discrete_utilitarian_welfare_approximation(matrix, items)

    num_of_players = len(result[0])
    pieces = num_of_players * [None]
    for j in range(num_of_players):
        pieces[j] = [(items[result[0][j]], items[result[1][j] + 1])]
    return Allocation(agents, pieces)
Example #9
0
def improve_ef4_protocol(agents: List[Agent]) -> Allocation:
    """
    Runs the "An Improved Envy-Free Cake Cutting Protocol for Four Agents" to allocate
    a cake to 4 agents.

    In actuality, this is a proxy function for `improve_ef4_algo.improve_ef4_impl.Algorithm`
    class and its `main` function, which provide the actual algorithm implementation.

    :param agents: list of agents to run the algorithm on
    :return: an 'Allocation' object, containing allocation of cake slices to the given agents
    :throws ValueError: if the agents list given does not contain 4 agents

    >>> from fairpy.cake.pieces import round_allocation 
    >>> from fairpy.agents import PiecewiseConstantAgent 
    >>> agents = [PiecewiseConstantAgent([3, 6, 3], "agent1"), PiecewiseConstantAgent([0, 2, 4], "agent2"), PiecewiseConstantAgent([6, 4, 2], "agent3"), PiecewiseConstantAgent([3, 3, 3], "agent4")]
    >>> allocation = improve_ef4_protocol(agents)
    >>> round_allocation(allocation)
    agent1 gets {(0.667, 0.833),(1.5, 2.0)} with value 3.5.
    agent2 gets {(0.333, 0.5),(2.0, 3)} with value 4.
    agent3 gets {(0.833, 1.0),(1.0, 1.5)} with value 3.
    agent4 gets {(0, 0.333),(0.5, 0.667)} with value 1.5.
    <BLANKLINE>
    """
    if len(agents) != 4:
        raise ValueError("expected 4 agents")

    algorithm = impl.Algorithm(agents, logger)
    result = algorithm.main()

    pieces = len(agents)*[None]
    for i in range(len(agents)):
        agent = agents[i]
        allocated_slices = result.get_allocation_for_agent(agent)
        pieces[i] = [(s.start, s.end) for s in allocated_slices]

    return Allocation(agents, pieces)
Example #10
0
def opt_piecewise_constant(agents: List[Agent]) -> Allocation:
    """
    algorithm for finding an optimal EF allocation when agents have piecewise constant valuations.
    :param agents: a list of PiecewiseConstantAgent agents
    :return: an optimal envy-free allocation

    >>> alice = PiecewiseConstantAgent([15,15,0,30,30], name='alice')
    >>> bob = PiecewiseConstantAgent([0,30,30,30,0], name='bob')
    >>> gin = PiecewiseConstantAgent([10,0,30,0,60], name='gin')
    >>> print(str(opt_piecewise_constant([alice,gin])))
    alice gets {(0.0, 1.0),(1.0, 2.0),(3.0, 4.0)} with value 60.
    gin gets {(2.0, 3.0),(4.0, 5.0)} with value 90.
    <BLANKLINE>
    >>> alice = PiecewiseConstantAgent([5], name='alice')
    >>> bob = PiecewiseConstantAgent([5], name='bob')
    >>> print(str(opt_piecewise_constant([alice,bob])))
    alice gets {(0.0, 0.5)} with value 2.5.
    bob gets {(0.5, 1.0)} with value 2.5.
    <BLANKLINE>
    >>> alice = PiecewiseConstantAgent([3], name='alice')
    >>> bob = PiecewiseConstantAgent([5], name='bob')
    >>> print(str(opt_piecewise_constant([alice,bob])))
    alice gets {(0.0, 0.5)} with value 1.5.
    bob gets {(0.5, 1.0)} with value 2.5.
    <BLANKLINE>
    >>> alice = PiecewiseConstantAgent([0,1,0,2,0,3], name='alice')
    >>> bob = PiecewiseConstantAgent([1,0,2,0,3,0], name='bob')
    >>> print(str(opt_piecewise_constant([alice,bob])))
    alice gets {(1.0, 2.0),(3.0, 4.0),(5.0, 6.0)} with value 6.
    bob gets {(0.0, 1.0),(2.0, 3.0),(4.0, 5.0)} with value 6.
    <BLANKLINE>
   """

    value_matrix = [list(agent.valuation.values) for agent in agents]
    num_of_agents = len(value_matrix)
    num_of_pieces = len(value_matrix[0])

    # Check for correct number of agents
    if num_of_agents < 2:
        raise ValueError(
            f'Optimal EF Cake Cutting works only for two agents or more')
    logger.info(f'Received {num_of_agents} agents')

    if not all(
        [agent.cake_length() == agents[0].cake_length() for agent in agents]):
        raise ValueError(f'Agents cake lengths are not equal')
    logger.info(f'Each agent cake length is {agents[0].cake_length()}')

    # XiI[i][I] represents the fraction of interval I given to agent i. Should be in {0,1}.
    XiI = [[
        cvxpy.Variable(
            name=
            f'{agents[agent_index].name()} interval {piece_index+1} fraction',
            integer=False) for piece_index in range(num_of_pieces)
    ] for agent_index in range(num_of_agents)]
    logger.info(
        f'Fraction matrix has {len(XiI)} rows (agents) and {len(XiI[0])} columns (intervals)'
    )

    constraints = feasibility_constraints(XiI)

    agents_w = []
    for i in range(num_of_agents):
        value_of_i = sum(
            [XiI[i][g] * value_matrix[i][g] for g in range(num_of_pieces)])
        agents_w.append(value_of_i)
        for j in range(num_of_agents):
            if j != i:
                value_of_j = sum([
                    XiI[j][g] * value_matrix[i][g]
                    for g in range(num_of_pieces)
                ])
                logger.info(
                    f'Adding Envy-Free constraint for agent: {agents[i].name()},\n{value_of_j} <= {value_of_i}'
                )
                constraints.append(value_of_j <= value_of_i)

    objective = sum(agents_w)
    logger.info(f'Objective function to maximize is {objective}')

    prob = cvxpy.Problem(cvxpy.Maximize(objective), constraints)
    prob.solve()

    logger.info(f'Problem status: {prob.status}')

    pieces_allocation = get_pieces_allocations(num_of_pieces, XiI)
    return Allocation(agents, pieces_allocation)
Example #11
0
def opt_piecewise_linear(agents: List[Agent]) -> Allocation:
    """
     algorithm for finding an optimal EF allocation when agents have piecewise linear valuations.
    :param agents: a list of agents
    :return: an optimal envy-free allocation
    >>> alice = PiecewiseLinearAgent([11,22,33,44],[1,0,3,-2],name="alice")
    >>> bob = PiecewiseLinearAgent([11,22,33,44],[-1,0,-3,2],name="bob")
    >>> print(str(opt_piecewise_linear([alice,bob])))
    alice gets {(0.5, 1),(1, 1.466),(2.5, 3),(3, 3.5)} with value 55.
    bob gets {(0, 0.5),(1.466, 2),(2, 2.5),(3.5, 4)} with value 55.
    <BLANKLINE>
    >>> alice = PiecewiseLinearAgent([5], [0], name='alice')
    >>> bob = PiecewiseLinearAgent([5], [0], name='bob')
    >>> print(str(opt_piecewise_linear([alice,bob])))
    alice gets {(0, 0.5)} with value 2.5.
    bob gets {(0.5, 1)} with value 2.5.
    <BLANKLINE>
    >>> alice = PiecewiseLinearAgent([5], [-1], name='alice')
    >>> bob = PiecewiseLinearAgent([5], [0], name='bob')
    >>> print(str(opt_piecewise_linear([alice,bob])))
    alice gets {(0, 0.5)} with value 2.62.
    bob gets {(0.5, 1)} with value 2.5.
    <BLANKLINE>
    >>> alice = PiecewiseLinearAgent([5], [-1], name='alice')
    >>> bob = PiecewiseLinearAgent([5], [-1], name='bob')
    >>> print(str(opt_piecewise_linear([alice,bob])))
    alice gets {(0, 0.475)} with value 2.5.
    bob gets {(0.475, 1)} with value 2.25.
    <BLANKLINE>
    >>> alice = PiecewiseLinearAgent([0,1,0,2,0,3], [0,0,0,0,0,0], name='alice')
    >>> bob = PiecewiseLinearAgent([1,0,2,0,3,0], [0,0,0,0,0,0],name='bob')
    >>> print(str(opt_piecewise_linear([alice,bob])))
    alice gets {(1, 2),(3, 4),(5, 6)} with value 6.
    bob gets {(0, 1),(2, 3),(4, 5)} with value 6.
    <BLANKLINE>
    """
    def Y(i, op, j, intervals) -> list:
        """
        returns all pieces that  s.t Y(i op j) = {x ∈ [0, 1] : vi(x) op vj (x)}
        :param i: agent index
        :param op: operator to apply
        :param j: ajent index
        :param intervals: (x's) to apply function and from pieces will be returned
        :return: list of intervals
        """
        return [(start, end) for start, end in intervals
                if op(agents[i].eval(start, end), agents[j].eval(start, end))]

    def isIntersect(poly_1: np.poly1d, poly_2: np.poly1d) -> float:
        """
        checks for polynomials intersection
        :param poly_1: np.poly1d
        :param poly_2: np.poly1d
        :return: corresponding x or 0 if none exist
        """
        logger.debug(f'isIntersect: {poly_1}=poly_1,{poly_2}=poly_2')
        m_1, c_1 = poly_1.c if len(poly_1.c) > 1 else [0, poly_1.c[0]]
        m_2, c_2 = poly_2.c if len(poly_2.c) > 1 else [0, poly_2.c[0]]
        logger.debug(f'isIntersect: m_1={m_1} c_1={c_1}, m_2={m_2} c_2={c_2}')
        return ((c_2 - c_1) / (m_1 - m_2)) if (m_1 - m_2) != 0 else 0.0

    def R(x: tuple) -> float:
        """
        R(x) = v1(x)/v2(x)
        :param x: interval
        :return: ratio
        """
        if agents[1].eval(x[0], x[1]) > 0:
            return agents[0].eval(x[0], x[1]) / agents[1].eval(x[0], x[1])
        return 0

    def V_l(agent_index, inter_list):
        """
        sums agent's value along intervals from inter_list
        :param agent_index: agent index 0 or 1
        :param inter_list: list of intervals
        :return: sum of intervals for agent
        """
        logger.info(
            f'V_list(agent_index={agent_index}, inter_list={inter_list})')
        return sum([V(agent_index, start, end) for start, end in inter_list])

    def V(agent_index: int, start: float, end: float):
        """
        agent value of interval from start to end
        :param agent_index: agent index 0 or 1
        :param start: interval starting point
        :param end: interval ending point
        :return: value of interval for agent
        """
        logger.info(f'V(agent_index={agent_index},start={start},end={end})')
        return agents[agent_index].eval(start, end)

    def get_optimal_allocation():
        """
        Creates maximum total value allocation
        :return: optimal allocation for 2 agents list[list[tuple], list[tuple]] and list of new intervals
        """
        logger.debug(f'length: {agents[0].cake_length()}')
        intervals = [(start, start + 1)
                     for start in range(agents[0].cake_length())]
        logger.info(
            f'getting optimal allocation for initial intervals: {intervals}')
        new_intervals = []
        allocs = [[], []]
        for piece, (start, end) in enumerate(intervals):
            logger.debug(
                f'get_optimal_allocation: piece={piece}, start={start}, end={end}'
            )
            mid = isIntersect(agents[0].valuation.piece_poly[piece],
                              agents[1].valuation.piece_poly[piece])
            if 0 < mid < 1:
                logger.debug(f'mid={mid}')
                new_intervals.append((start, start + mid))
                if V(0, start, start + mid) > V(1, start, start + mid):
                    allocs[0].append((start, start + mid))
                else:
                    allocs[1].append((start, start + mid))
                start += mid
            if V(0, start, end) > V(1, start, end):
                allocs[0].append((start, end))
            else:
                allocs[1].append((start, end))
            new_intervals.append((start, end))
        return allocs, new_intervals

    def Y_op_r(intervals, op, r):
        """
        Y op r = {x : (v1(x) < v2(x)) ∧ (R(x) op r)}
        :param intervals: intervals to test condition
        :param op: operator.le, operator.lt, operator.gt, operator.ge etc.
        :param r: ratio
        :return: list of valid intervals
        """
        result = []
        for start, end in intervals:
            if agents[0].eval(start, end) < agents[1].eval(start, end) and op(
                    R((start, end)), r):
                result.append((start, end))
        return result

    allocs, new_intervals = get_optimal_allocation()
    logger.info(
        f'get_optimal_allocation returned:\nallocation: {allocs}\npieces: {new_intervals}'
    )

    y_0_gt_1 = Y(0, operator.gt, 1, new_intervals)
    y_1_gt_0 = Y(1, operator.gt, 0, new_intervals)
    y_0_eq_1 = Y(0, operator.eq, 1, new_intervals)
    y_0_ge_1 = Y(0, operator.ge, 1, new_intervals)
    y_1_ge_0 = Y(1, operator.ge, 0, new_intervals)
    y_0_lt_1 = Y(0, operator.lt, 1, new_intervals)
    logger.debug(f'y_0_gt_1 {y_0_gt_1}')
    logger.debug(f'y_1_gt_0 {y_1_gt_0}')
    logger.debug(f'y_0_eq_1 {y_0_eq_1}')
    logger.debug(f'y_0_ge_1 {y_0_ge_1}')
    logger.debug(f'y_1_ge_0 {y_1_ge_0}')

    if (V_l(0, y_0_ge_1) >= (agents[0].total_value() / 2)
            and V_l(1, y_1_ge_0) >= (agents[1].total_value() / 2)):
        if V_l(0, y_0_gt_1) >= (agents[0].total_value() / 2):
            allocs = [y_0_gt_1, y_1_ge_0]
        else:
            missing_value = (agents[0].total_value() / 2) - V_l(0, y_0_gt_1)
            interval_options = []
            for start, end in y_0_eq_1:
                mid = agents[0].mark(start, missing_value)
                logger.debug(
                    f'start {start}, end {end}, mid {mid}, missing value {missing_value}'
                )
                if mid:
                    interval_options.append([(start, mid), (mid, end)])
            logger.debug(f'int_opt {interval_options}')
            agent_0_inter, agent_1_inter = interval_options.pop()
            y_0_gt_1.append(agent_0_inter)
            y_1_gt_0.append(agent_1_inter)
            logger.info(f'agent 0 pieces {y_0_gt_1}')
            logger.info(f'agent 1 pieces {y_1_gt_0}')
            allocs = [y_0_gt_1, y_1_gt_0]
        return Allocation(agents, allocs)

    if V_l(0, y_0_ge_1) < (agents[0].total_value() / 2):
        # Create V1(Y(1≥2) ∪ Y(≥r)) ≥ 1/2
        ratio_dict = {x: R(x) for x in y_0_lt_1}
        y_le_r_dict = {
            r: Y_op_r(y_0_lt_1, operator.ge, r)
            for inter, r in ratio_dict.items()
        }
        valid_r_dict = {}
        r_star = {0: None}
        for r, val in y_le_r_dict.items():
            if V_l(0, y_0_gt_1 + val) >= (agents[0].cake_length() / 2):
                highest_value, interval_dict = r_star.popitem()
                temp_dict = {r: val}

                if V_l(0, y_0_gt_1 + val) > highest_value:
                    highest_value = V_l(0, y_0_gt_1 + val)
                    interval_dict = temp_dict

                valid_r_dict[r] = val
                r_star[highest_value] = interval_dict

        logger.info(f'Valid Y(≥r) s.t. V1(Y(1≥2 U Y(≥r))) is {valid_r_dict}')
        logger.info(f'Y(≥r*) is {r_star}')

        # Give Y>r∗ to agent 1
        _, r_max_dict = r_star.popitem()

        if not r_max_dict:
            logger.info(f'Y > r* returned empty, returning')
            return Allocation(agents, allocs)

        r_max, inter_r_max = r_max_dict.popitem()
        agent_0_allocation = y_0_gt_1 + Y_op_r(inter_r_max, operator.gt, r_max)
        agent_1_allocation = y_0_lt_1

        # divide Y=r∗ so that agent 1 receives exactly value 1
        missing_value = (agents[0].total_value() / 2) - V_l(
            0, agent_0_allocation)
        y_eq_r = Y_op_r(inter_r_max, operator.eq, r_max)
        logger.info(f'Y(=r*) is {y_eq_r}')
        for start, end in y_eq_r:
            agent_1_allocation.remove((start, end))
            mid = agents[0].mark(start, missing_value)
            logger.debug(
                f'start {start}, end {end}, mid {mid}, missing value {missing_value}'
            )
            if mid <= end:
                agent_0_allocation.append((start, mid))
                agent_1_allocation.append((mid, end))
            else:
                agent_1_allocation.append((start, end))

        logger.info(f'agent 0 pieces {agent_0_allocation}')
        logger.info(f'agent 1 pieces {agent_1_allocation}')
        allocs = [agent_0_allocation, agent_1_allocation]
        logger.info(
            f'Is allocation {agent_0_allocation, agent_1_allocation}, Envy Free ? {a.isEnvyFree(3)}'
        )

    return Allocation(agents, allocs)
def equally_sized_pieces(agents: List[Agent], piece_size: float) -> Allocation:
    """
    Algorithm 1.
    Approximation algorithm of the optimal auction for uniform-size pieces.

    Complexity and approximation:
    - Requires only 2 / l values from each agent.
    - Runs in time polynomial in n + 1 / l.
    - Approximates the optimal welfare by a factor of 2.

    :param agents: A list of Agent objects.
    :param piece_size: Size of an equally sized piece (in the paper: l).
    :return: A cake-allocation, not necessarily all the cake will be allocated.

    The doctest will work when the set of edges will return according lexicographic order
    >>> Alice = PiecewiseConstantAgent([100, 1], "Alice")
    >>> Bob = PiecewiseConstantAgent([2, 90], "Bob")
    >>> equally_sized_pieces([Alice, Bob], 0.5)
    Alice gets {(0, 1)} with value 100.
    Bob gets {(1, 2)} with value 90.
    <BLANKLINE>

    The doctest will work when the set of edges will return according lexicographic order
    >>> Alice = PiecewiseConstantAgent([1, 1, 1, 1, 1], "Alice")
    >>> Bob = PiecewiseConstantAgent([3, 3, 3, 1, 1], "Bob")
    >>> equally_sized_pieces([Alice, Bob], 3 / 5)
    Alice gets {(2, 5)} with value 3.
    Bob gets {(0, 3)} with value 9.
    <BLANKLINE>
    """
    # > Bob gets {(0, 3)} with value 9.00

    # Initializing variables and asserting conditions
    num_of_agents = len(agents)
    if num_of_agents == 0:
        raise ValueError("There must be at least one agent")
    if not 0 < piece_size <= 1:
        raise ValueError("Piece size must be between 0 and 1")

    logger.info("Piece size (l) = %f", piece_size)
    delta = 1 - int(1 / piece_size) * piece_size
    logger.info("Delta := 1 - floor(1 / l) * l = %f", delta)

    logger.info("Create the partitions P_0_l and P_d_l")
    # Creating the partition of the pieces that start from 0
    partition_0_l = create_partition(piece_size)
    logger.info("  The partition P_0_l (l-sized pieces starting at 0) = %s",
                partition_0_l)
    # Creating the partition of the pieces that start from delta
    partition_delta_l = create_partition(piece_size, start=delta)
    logger.info(
        "  The partition P_d_l (l-sized pieces starting at delta) = %s",
        partition_delta_l)
    # Merging the partitions to one partition
    all_partitions = partition_0_l + partition_delta_l

    length = max([a.cake_length() for a in agents])
    # Normalizing the partitions to match the form of the pieces allocation of the Agents
    normalize_partitions = [(int(p[0] * length), int(p[1] * length))
                            for p in all_partitions]
    normalize_partitions_0_l = [(int(p[0] * length), int(p[1] * length))
                                for p in partition_0_l]
    normalize_partitions_delta_l = [(int(p[0] * length), int(p[1] * length))
                                    for p in partition_delta_l]

    # Evaluating the pieces of the partition for every agent there is
    logger.info(
        "For each piece (in both partitions) and agent: compute the agent's value of the piece."
    )
    evaluations = {}
    # Get evaluation for every piece
    for piece in normalize_partitions:
        # For every piece get evaluation for every agent
        for agent in agents:
            evaluations[(agent, piece)] = agent.eval(start=piece[0],
                                                     end=piece[1])
    # Create the matching graph
    # One side is the agents, the other side is the partitions and the weights are the evaluations
    logger.info("Create the partition graphs G_0_l and G_d_l")
    g_0_l = create_matching_graph(agents, normalize_partitions_0_l,
                                  evaluations)
    logger.info("  The graph G_0_l = %s", stringify_agent_piece_graph(g_0_l))
    g_delta_l = create_matching_graph(agents, normalize_partitions_delta_l,
                                      evaluations)
    logger.info("  The graph G_d_l = %s",
                stringify_agent_piece_graph(g_delta_l))

    # Set the edges to be in order, (Agent, partition)
    logger.info("Compute maximum weight matchings for each graph respectively")
    edges_set_0_l = fix_edges(max_weight_matching(g_0_l))
    logger.info("  The edges in G_0_l = %s", stringify_edge_set(edges_set_0_l))
    edges_set_delta_l = fix_edges(max_weight_matching(g_delta_l))
    logger.info("  The edges in G_d_l = %s",
                stringify_edge_set(edges_set_delta_l))

    logger.info("Choose the heavier among the matchings")
    # Check which matching is heavier and choose it
    if calculate_weight(g_delta_l, edges_set_delta_l) > calculate_weight(
            g_0_l, edges_set_0_l):
        edges_set = edges_set_delta_l
    else:
        edges_set = edges_set_0_l

    # Find the agents that are in the allocation that was chosen
    chosen_agents = [agent for (agent, piece) in edges_set]
    chosen_agents.sort(key=lambda agent: agent.name())
    # Create allocation

    pieces = len(chosen_agents) * [None]
    # Add the edges to the allocation
    for edge in edges_set:
        pieces[chosen_agents.index(edge[0])] = [edge[1]]
    return Allocation(chosen_agents, pieces)
Example #13
0
def EFAllocate(agents: List[Agent], roundAcc=2) -> Allocation:
    """
    Envy Free cake cutting protocol for piecewise agents that runs
    in a polynomial time complexity.

    :param agents: a list of agents.
    :param roundAcc: the rounding accuracy of the algorithm in decimal digits.
    :return: an envy-free allocation.

    >>> from fairpy.agents import PiecewiseUniformAgent
    >>> Alice = PiecewiseUniformAgent([(5,7)], "Alice")
    >>> George = PiecewiseUniformAgent([(4,9)], "George")
    >>> print(str(EFAllocate([Alice,George])))
    Alice gets {(0, 6.0)} with value 1.
    George gets {(6.0, 7.5),(7.5, 9.0)} with value 3.
    <BLANKLINE>
    >>> Alice = PiecewiseUniformAgent([(2,3), (9,10)], "Alice")
    >>> George = PiecewiseUniformAgent([(1,2), (6,7)], "George")
    >>> print(str(EFAllocate([Alice,George])))
    Alice gets {(2.0, 6.0),(6.0, 10.0)} with value 2.
    George gets {(0, 2.0)} with value 1.
    <BLANKLINE>
    """
    num_of_agents = len(agents)

    def sandwichAllocation(a, b, alpha, beta,
                           n) -> List[List[Tuple[float, float]]]:
        """
        creates a sandwich allocation using the specified paramaters.
        :param a: starting point of the section to split.
        :param b: end point of the section to split.
        :param alpha: starting point of the internal interval.
        :param beta: end point of the internal interval.
        :param n: the amount of agents in the allocation.
        :return: a list of lists of intervals mathching the sandwich allocation.

        >>>sandwichAllocation(0,1,0.4,0.6,2)
        [[(0.4, 0.6)], [(0,0.2), (0.2, 0.4), (0.6, 0.8), (0.8, 1)]]
        """
        gamma = round((alpha - a) / (2 * (n - 1)), roundAcc)
        delta = round((b - beta) / (2 * (n - 1)), roundAcc)
        tmp = [[(alpha, beta)]]
        for j in range(1, n):
            toAdd = []
            toAdd.append((round(a + (j - 1) * gamma,
                                roundAcc), round(a + j * gamma, roundAcc)))
            toAdd.append(
                (round(alpha - (j) * gamma,
                       roundAcc), round(alpha - (j - 1) * gamma, roundAcc)))
            toAdd.append((round(beta + (j - 1) * delta,
                                roundAcc), round(beta + j * delta, roundAcc)))
            toAdd.append((round(b - (j) * delta,
                                roundAcc), round(b - (j - 1) * delta,
                                                 roundAcc)))
            tmp.append(toAdd)

        #clear useless points where the start equals to the end
        ret = []
        for piece in tmp:
            ret.append([])
            for inter in piece:
                if round(inter[1] - inter[0], roundAcc) > 0.0:
                    ret[-1].append(inter)
        return ret

    def EFAllocateRec(a: float, b: float) -> List[List[Tuple[float, float]]]:
        """
        exactly the same as EFAllocate, but creates the envy free allocation
        within a given interval.

        :param a: starting point of the interval to allocate.
        :param b: end point of the interval to allocate.
        :return: a list of lists of intervals mathching the sandwich allocation.
        """

        if round(a, roundAcc) == round(b, roundAcc):
            return Allocation(agents, len(agents) * [[]])

        #1
        numAgents = len(agents)
        ret = Allocation(agents, len(agents) * [[]])
        cover = Cover(a, b, agents, roundAcc=roundAcc)

        #2
        for inter in cover:
            pieces = sandwichAllocation(a, b, inter[0], inter[1], numAgents)
            for permuted_pieces in permutations(pieces):
                if is_envyfree(agents, permuted_pieces, roundAcc):
                    logger.info(
                        "allocation from %f to %f completed with sandwich allocation.",
                        a, b)
                    return permuted_pieces

        #3
        logger.info("no valid allocation without recursion from %f to %f.", a,
                    b)
        logger.info(
            "splitting to sub-allocations using the cover of the whole interval."
        )
        points = [a]
        for interval in cover:
            points.append(interval[1])
        points.sort()
        ret = num_of_agents * [[]]
        for i in range(1, len(points)):
            logger.info("creating allocation from %f to %f:", points[i - 1],
                        points[i])
            alloc = EFAllocateRec(points[i - 1], points[i])
            # merge the existing allocation with the new allocation:
            merged_alloc = [
                ret[i_agent] + alloc[i_agent]
                for i_agent in range(num_of_agents)
            ]
            ret = merged_alloc
        logger.info("covered allocation from %f to %f using merging.", a, b)
        return ret

    #run the bounded function over the whole area - from 0 to 1.
    alloc = EFAllocateRec(0, max([agent.cake_length() for agent in agents]))
    return Allocation(agents, alloc)
def algor1(AgentList) -> Allocation:
    """
    Answer a Mark query: return "end" such that the value of the interval [start,end] is target_value  {[(NORMALIZED)]}.

    :param start: Location on cake where the calculation starts.
    :param targetValue: required value for the piece [start,end]
    :return: the end of an interval with a value of target_value.
    If the value is too high - returns None.

    >>> aa = PiecewiseConstantAgentNormalized([2, 8, 2], name="aa")
    >>> bb = PiecewiseConstantAgentNormalized([4, 2, 6], name="bb")
    >>> lstba = [aa,bb]
    >>> round_allocation(algor1(lstba))
    aa gets {(0.333, 1.0)} with value 0.833.
    bb gets {(0.0, 0.333)} with value 0.333.
    <BLANKLINE>
    >>> a0 = PiecewiseConstantAgentNormalized([4, 10, 20], name="a0")
    >>> a1 = PiecewiseConstantAgentNormalized([14, 42, 9, 17], name="a1")
    >>> a2 = PiecewiseConstantAgentNormalized([30, 1, 12], name="a2")
    >>> lstaaa = [a0,a1,a2]
    >>> round_allocation(algor1(lstaaa))
    a0 gets {(0.382, 1.0)} with value 0.839.
    a1 gets {(0.159, 0.382)} with value 0.333.
    a2 gets {(0.0, 0.159)} with value 0.333.
    <BLANKLINE>
    """
    l = 0.00
    lenAgents = len(AgentList)
    # N = list(range(0, lenAgents))
    N = [i for i in range(lenAgents)]

    # Mlist is the list who save the M Parts of the cake
    pieces = lenAgents * [None]
    #####Mlist = [None] * lenAgents

    # the leftest point of each of the agent to fulfill the condition of the 1/3
    rList = [None] * lenAgents

    # for saving who was the last agent to remove
    lastAgentRemove = -1

    # the main loop
    while hasBiggerThanThird(l, N, AgentList):
        for i in N:
            if AgentList[i].eval(l, 1.0) >= (1 / 3):
                #where it equal 1/3
                rList[i] = (AgentList[i].mark(l, (1 / 3)))
            else:
                rList[i] = 1
        # j - agent with smallest rList value (r[i])
        j = N[0]
        # r - smallest rList value (r[i])
        r = rList[N[0]]
        # find j and r (finds minimum)
        for k in N:
            if rList[k] < r:
                j = k
                r = rList[k]
        logger.info(
            "From The agents who remained The agent (%s) is with the leftest point - which is %s.",
            AgentList[j].name(), str(r))
        # gives agent j this segment , moves l to the right (r)
        pieces[j] = [(l, r)]
        #####Mlist[j] = [l, r]
        l = r

        # remove j
        N.remove(j)

        lastAgentRemove = j
        if len(N) == 0:
            break
    # if N is not empty
    if N:
        # arbitrary agent in N

        j = N[0]
        logger.info(" (%s) is getting the rest. %s till 1.0",
                    AgentList[j].name(), str(l))
        pieces[j] = [(l, 1.0)]
        #####Mlist[j] = [l, 1.0]
    else:
        logger.info("There is no agents that remained")
        j = lastAgentRemove
        logger.info(" we are adding to (%s) the rest. %s till 1.0",
                    AgentList[j].name(), str(l))
        # [a, l] unite with [l, 1] => [a, 1]
        tupe1 = pieces[j]
        pieces[j] = [(tupe1[0][0], 1.0)]
        #####Mlist[j][1] = 1.0
    logger.info("")

    # for printing the results

    return Allocation(AgentList, pieces)
Example #15
0
def elaborate_simplex_solution(agents: List[Agent], epsilon) -> Allocation:
    """
    according to the algorithm in theirs essay, the algorithm will create class that solves the problem with simplex
    and return allocation.

    :param agents: a list that must contain exactly 3 Agent objects.
    :param epsilon: the approximation parameter
    :return: a proportional and envy-free-approximation allocation.

    >>> George = PiecewiseConstantAgent([4, 6], name="George")
    >>> Abraham = PiecewiseConstantAgent([6, 4], name="Abraham")
    >>> Hanna = PiecewiseConstantAgent([3, 3], name="Hanna")
    >>> agents = [George, Abraham, Hanna]
    >>> elaborate_simplex_solution(agents, 1/2)
    George gets {(0, 1.0)} with value 4.
    Abraham gets {(1.0, 1.5)} with value 2.
    Hanna gets {(1.5, 2)} with value 1.5.
    <BLANKLINE>
    """
    # checking parameters validity
    num_of_agents = len(agents)
    if num_of_agents != 3 or epsilon == 0:
        raise ValueError(
            "This simplex solution works only for 3 agents, with approximation epsilon greater than 0"
        )

    pieces = num_of_agents * [None]
    n = max([agent.cake_length() for agent in agents])

    # init the solver with simplex, with the approximation value, cake length and agents's list
    solver = SimplexSolver(epsilon, n, agents)

    # solver returns a vertex, which represent a proper partition of the segment
    triplet = solver.recursive_algorithm1(0, solver.N, 0, solver.N)
    # reversing the indices to a proper partition
    logger.info(
        "we found a triplet of indices that represent a proper envy-free-approximation partition"
    )
    or_indices = []
    counter = 0
    for i in range(len(triplet)):
        or_indices.append(solver.epsilon * (triplet[i] + counter))
        counter += triplet[i]
    first_index = solver.label(triplet)
    second_index = (first_index + 1) % 3
    third_index = (first_index + 2) % 3
    first_color_index = solver.color(first_index, triplet)

    if first_color_index == 0:
        # then allocate to him his choice
        pieces[first_index] = [(0, or_indices[0])]
        logger.info("%s gets the the piece [%f,%f].",
                    solver.agents[first_index].name(), 0, or_indices[0])
        # find which of the next two has more envious between the leftovers pieces, and let him be second
        options = [(or_indices[0], or_indices[1]), (or_indices[1], n)]
        sec_dif = agents[second_index].eval(
            or_indices[0], or_indices[1]) - agents[second_index].eval(
                or_indices[1], n)
        thr_dif = agents[third_index].eval(
            or_indices[0], or_indices[1]) - agents[third_index].eval(
                or_indices[1], n)
        second = second_index if abs(sec_dif) >= abs(thr_dif) else third_index
        third = second_index if abs(sec_dif) < abs(thr_dif) else third_index

        # define which option goes to the second as first priority
        max_option = options[np.argmax(agents[second].eval(start, end)
                                       for (start, end) in options)]
        options.remove(max_option)
        min_option = options[np.argmin(agents[second].eval(start, end)
                                       for (start, end) in options)]

        # allocate both players

        pieces[second] = [max_option]
        logger.info("%s gets the the piece [%f,%f].",
                    solver.agents[second].name(), max_option[0], max_option[1])
        pieces[third] = [min_option]
        logger.info("%s gets the the piece [%f,%f].",
                    solver.agents[third].name(), min_option[0], min_option[1])

    elif first_color_index == 1:
        # same things happens, just for another option
        pieces[first_index] = [(or_indices[0], or_indices[1])]
        logger.info("%s gets the the piece [%f,%f].",
                    solver.agents[first_index].name(), or_indices[0],
                    or_indices[1])

        # find which of the next two has more envious between the leftovers pieces, and let him be second
        options = [(0, or_indices[0]), (or_indices[1], n)]
        sec_dif = agents[second_index].eval(
            0, or_indices[0]) - agents[second_index].eval(or_indices[1], n)
        thr_dif = agents[third_index].eval(
            0, or_indices[0]) - agents[third_index].eval(or_indices[1], n)
        second = second_index if abs(sec_dif) >= abs(thr_dif) else third_index
        third = second_index if abs(sec_dif) <= abs(thr_dif) else third_index

        # define which option goes to the second as first priority
        max_option = options[np.argmax(agents[second].eval(start, end)
                                       for (start, end) in options)]
        options.remove(max_option)
        min_option = options[np.argmin(agents[second].eval(start, end)
                                       for (start, end) in options)]

        # allocate both players

        pieces[second] = [max_option]
        logger.info("%s gets the the piece [%f,%f].",
                    solver.agents[second].name(), max_option[0], max_option[1])
        pieces[third] = [min_option]
        logger.info("%s gets the the piece [%f,%f].",
                    solver.agents[third].name(), min_option[0], min_option[1])
    else:
        # same things happens, just for another option
        pieces[first_index] = [(or_indices[1], n)]
        logger.info("%s gets the the piece [%f,%f].",
                    solver.agents[first_index].name(), or_indices[1], n)

        # find which of the next two has more envious between the leftovers pieces, and let him be second
        options = [(0, or_indices[0]), (or_indices[0], or_indices[1])]
        sec_dif = agents[second_index].eval(
            0, or_indices[0]) - agents[second_index].eval(
                or_indices[0], or_indices[1])
        thr_dif = agents[third_index].eval(
            0, or_indices[0]) - agents[third_index].eval(
                or_indices[0], or_indices[1])
        second = second_index if abs(sec_dif) >= abs(thr_dif) else third_index
        third = second_index if abs(sec_dif) <= abs(thr_dif) else third_index

        # define which option goes to the second as first priority
        max_option = options[np.argmax(agents[second].eval(start, end)
                                       for (start, end) in options)]
        options.remove(max_option)
        min_option = options[np.argmin(agents[second].eval(start, end)
                                       for (start, end) in options)]

        # allocate both players
        pieces[second] = [max_option]
        logger.info("%s gets the the piece [%f,%f].",
                    solver.agents[second].name(), max_option[0], max_option[1])
        pieces[third] = [min_option]
        logger.info("%s gets the the piece [%f,%f].",
                    solver.agents[third].name(), min_option[0], min_option[1])

    return Allocation(agents, pieces)
Example #16
0
    def adapted_algorithm(input, *args, **kwargs):

        # Step 1. Adapt the input:
        valuation_matrix = list_of_valuations = object_names = agent_names = None
        if isinstance(
                input,
                ValuationMatrix):  # instance is already a valuation matrix
            valuation_matrix = input
        elif isinstance(input,
                        np.ndarray):  # instance is a numpy valuation matrix
            valuation_matrix = ValuationMatrix(input)
        elif isinstance(input, list) and isinstance(input[0],
                                                    list):  # list of lists
            list_of_valuations = input
            valuation_matrix = ValuationMatrix(list_of_valuations)
        elif isinstance(input, dict):
            agent_names = list(input.keys())
            list_of_valuations = list(input.values())
            if isinstance(list_of_valuations[0],
                          dict):  # maps agent names to dicts of valuations
                object_names = list(list_of_valuations[0].keys())
                list_of_valuations = [[
                    valuation[object] for object in object_names
                ] for valuation in list_of_valuations]
            valuation_matrix = ValuationMatrix(list_of_valuations)
        else:
            raise TypeError(f"Unsupported input type: {type(input)}")

        # Step 2. Run the algorithm:
        output = algorithm(valuation_matrix, *args, **kwargs)
        # return output if isinstance(output,Allocation) else Allocation(valuation_matrix, output)

        # Step 3. Adapt the output:
        if isinstance(output, Allocation):
            return output

        if agent_names is None:
            agent_names = [f"Agent #{i}" for i in valuation_matrix.agents()]

        # print("agent_names", agent_names, "object_names", object_names,"output", output)

        if isinstance(output, np.ndarray) or isinstance(
                output, AllocationMatrix):  # allocation matrix
            allocation_matrix = AllocationMatrix(output)
            if isinstance(input, dict):
                list_of_bundles = [
                    FractionalBundle(allocation_matrix[i], object_names)
                    for i in allocation_matrix.agents()
                ]
                dict_of_bundles = dict(zip(agent_names, list_of_bundles))
                return Allocation(input,
                                  dict_of_bundles,
                                  matrix=allocation_matrix)
            else:
                return Allocation(valuation_matrix, allocation_matrix)
        elif isinstance(output, list):
            if object_names is None:
                list_of_bundles = output
            else:
                list_of_bundles = [[
                    object_names[object_index] for object_index in bundle
                ] for bundle in output]
            dict_of_bundles = dict(zip(agent_names, list_of_bundles))
            return Allocation(
                input if isinstance(input, dict) else valuation_matrix,
                dict_of_bundles)
        else:
            raise TypeError(f"Unsupported output type: {type(output)}")
def discrete_setting(agents: List[Agent],
                     pieces: List[Tuple[float, float]]) -> Allocation:
    """
    Algorithm 2.
    Approximation algorithm of the optimal auction for a discrete cake with known piece sizes.

    Complexity and approximation:
    - Requires at most 2m values from each agent.
    - Runs in time polynomial in n + log m.
    - Approximates the optimal welfare by a factor of log m + 1.

    :param agents: A list of Agent objects.
    :param pieces: List of sized pieces.
    :return: A cake-allocation.

    The doctest will work when the set of edges will return according lexicographic order
    >>> Alice = PiecewiseConstantAgent([100, 1], "Alice")
    >>> Bob = PiecewiseConstantAgent([2, 90], "Bob")
    >>> discrete_setting([Alice, Bob], [(0, 1), (1, 2)])
    Alice gets {(0, 1)} with value 100.
    Bob gets {(1, 2)} with value 90.
    <BLANKLINE>

    """
    # Set m to be the number of pieces in the given partition
    m = len(pieces)
    # Set r to be log of the number of pieces
    r = int(log(m, 2))

    max_weight = 0
    max_match = None

    logger.info(
        "For every t = 0,...,r create the 2 ^ t-partition, partition sequence of 2 ^ t items."
    )
    logger.info("Denote the t-th partition by Pt.")
    # Go over the partition by powers of 2
    for t in range(0, r + 1):
        logger.info("Iteration t = %d", t)
        # Change the partition to be a partition with 2^t size of every piece
        partition_i = change_partition(pieces, t)

        logger.info(
            "For each piece and agent: compute the agent's value of the piece."
        )
        # Evaluate every piece in the new partition
        evaluations = {}
        # Go over every piece in the partition
        for piece in partition_i:
            # Go over each Agent
            for agent in agents:
                # Evaluate the piece according to the Agent
                evaluations[(agent, piece)] = agent.eval(start=piece[0],
                                                         end=piece[1])

        logger.info("create the partition graph G - Pt=%d", t)
        # Create the matching graph according to the new partition
        g_i = create_matching_graph(agents, partition_i, evaluations)
        logger.info("Compute a maximum weight matching Mt in the graph GPt")
        # Find the max weight matching of the graph and get the set of edges of the matching
        edges_set = max_weight_matching(g_i)
        # Set the edges to be in order, (Agent, partition)
        edges_set = fix_edges(edges_set)
        # Calculate the sum of the weights in the edges set
        weight = calculate_weight(g_i, edges_set)
        # Check for the max weight
        if weight > max_weight:
            max_weight = weight
            # Keep the edges set of the max weight
            max_match = edges_set

    # Get the agents that are part of the edges of the max weight
    chosen_agents = [edge[0] for edge in max_match]
    chosen_agents.sort(key=lambda agent: agent.name())
    # Create the allocation
    pieces = len(chosen_agents) * [None]
    # Add the edges to the allocation
    for edge in max_match:
        pieces[chosen_agents.index(edge[0])] = [edge[1]]
    return Allocation(chosen_agents, pieces)