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
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)