Skip to content

Commit

Permalink
algorithm examples
Browse files Browse the repository at this point in the history
  • Loading branch information
erelsgl committed Mar 19, 2024
1 parent ed5548d commit 77ba8cd
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 31 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ or run the tests:

## Usage

To activate a fair division algorithm, first construct a `fairpyx.instance`:
To activate a fair division algorithm, first construct a `fairpyx.instance`, for example:

import fairpyx
valuations = {"Alice": {"w":11,"x":22,"y":44,"z":0}, "George": {"w":22,"x":11,"y":66,"z":33}}
instance = fairpyx.Instance(valuations=valuations)

An instance can have other fields, such as: agent capacities, item capacities, agent conflicts and item conflicts. These fields are used by some of the algorithms. See [instances.py](fairpyx/instances.py) for details.
An instance can have other fields, such as: `agent_capacities`, `item_capacities`, `agent_conflicts` and `item_conflicts`. These fields are used by some of the algorithms. See [instances.py](fairpyx/instances.py) for details.

Then, use the function `fairpyx.divide` to run an algorithm on the instance. For example:

Expand All @@ -53,6 +53,8 @@ Then, use the function `fairpyx.divide` to run an algorithm on the instance. For

## Contributing new algorithms

You are welcome to add fair allocation algorithms, including your published algorithms, to `fairpyx`. Please use the following steps to contribute:

1. Fork `fairpyx` and install your fork locally as follows:

```
Expand All @@ -61,7 +63,9 @@ Then, use the function `fairpyx.divide` to run an algorithm on the instance. For
pip install -e .
```
2. Write a function that accepts a parameter of type `AllocationBuilder`, as well as any custom parameters your algorithm needs. The `AllocationBuilder` argument sent to your function is already initialized with an empty allocation. Your function has to modify this argument using the method `give`, which gives an item to an agent and updates the capacities. Your function need not return any value; the allocation is read from the modified parameter. See:
2. Read the code of some existing algorithms, to see how their implemented
Write a function that accepts a parameter of type `AllocationBuilder`, as well as any custom parameters your algorithm needs. The `AllocationBuilder` argument sent to your function is already initialized with an empty allocation. Your function has to modify this argument using the method `give`, which gives an item to an agent and updates the capacities. Your function need not return any value; the allocation is read from the modified parameter. See:
* [picking_sequence.py](fairpyx/algorithms/picking_sequence.py) and [iterated_maximum_matching.py](fairpyx/algorithms/iterated_maximum_matching.py) for examples of algorithms;
* [allocations.py](fairpyx/allocations.py) for more details on the `AllocationBuilder` object.
Expand Down
11 changes: 8 additions & 3 deletions examples/input_formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,17 @@ You can call the same algorithm with only the values, or only the value matrix:
```python
print(divide(fairpyx.algorithms.round_robin, valuations={"Ami": [8,7,6,5], "Tami": [12,8,4,2]}))
print(divide(fairpyx.algorithms.round_robin, valuations=[[8,7,6,5], [12,8,4,2]]))
```

```
{'Ami': [0, 2], 'Tami': [1, 3]}
{0: [0, 2], 1: [1, 3]}
```

# #' For experiments, you can use a numpy random matrix:

For experiments, you can use a numpy random matrix:

```python
import numpy as np
valuations = np.random.randint(1,100,[2,4])
print(valuations)
Expand All @@ -55,8 +62,6 @@ print(allocation)
```

```
{'Ami': [0, 2], 'Tami': [1, 3]}
{0: [0, 2], 1: [1, 3]}
[[44 1 43 50]
[83 90 44 20]]
{0: [0, 3], 1: [1, 2]}
Expand Down
2 changes: 1 addition & 1 deletion examples/input_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
print(divide(fairpyx.algorithms.round_robin, valuations=[[8,7,6,5], [12,8,4,2]]))


# #' For experiments, you can use a numpy random matrix:
#' For experiments, you can use a numpy random matrix:

import numpy as np
valuations = np.random.randint(1,100,[2,4])
Expand Down
79 changes: 79 additions & 0 deletions fairpyx/algorithms/algorithm_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
This is a dummy algorithm, that serves as an example how to implement an algorithm.
Programmer: Erel Segal-Halevi
Since: 2023-06
"""

# The end-users of the algorithm feed the input into an "Instance" variable, which tracks the original input (agents, items and their capacities).
# But the algorithm implementation uses an "AllocationBuilder" variable, which tracks both the ongoing allocation and the remaining input (the remaining capacities of agents and items).
# The function `divide` is an adaptor - it converts an Instance to an AllocationBuilder with an empty allocation.
from fairpyx import Instance, AllocationBuilder, divide



# The `logging` facility is used both for debugging and for illustrating the steps of the algorithm.
# It can be used to automatically generate running examples or explanations.
import logging
logger = logging.getLogger(__name__)

# This example instance is used in doctests throughout this file:
example_instance = Instance(
valuations = {"Alice": {"c1": 10, "c2": 8, "c3": 6}, "Bob": {"c1": 10, "c2": 8, "c3": 6}, "Chana": {"c1": 6, "c2": 8, "c3": 10}, "Dana": {"c1": 6, "c2": 8, "c3": 10}},
agent_capacities = {"Alice": 2, "Bob": 3, "Chana": 2, "Dana": 3},
item_capacities = {"c1": 2, "c2": 3, "c3": 4},
)

def algorithm1(alloc: AllocationBuilder):
"""
This dummy algorithm gives one item to the first agent, and all items to the second agent.
>>> divide(algorithm1, example_instance)
{'Alice': ['c1'], 'Bob': ['c1', 'c2', 'c3'], 'Chana': [], 'Dana': []}
"""
logger.info("\nAlgorithm 1 starts. items %s , agents %s", alloc.remaining_item_capacities, alloc.remaining_agent_capacities)
remaining_agents = list(alloc.remaining_agents()) # `remaining_agents` returns the list of agents with remaining capacities.
remaining_items = list(alloc.remaining_items())
alloc.give(remaining_agents[0], remaining_items[0]) # `give` gives the specified agent the specified item, and updates the capacities.
alloc.give_bundle(remaining_agents[1], remaining_items) # `give_bundle` gives the specified agent the specified set of items, and updates the capacities.
# No need to return a value. The `divide` function returns the output.

def algorithm2(alloc: AllocationBuilder):
"""
This is a serial dictatorship algorithm: it lets each agent in turn pick all remaining items.
>>> divide(algorithm2, example_instance)
{'Alice': ['c1', 'c2'], 'Bob': ['c1', 'c2', 'c3'], 'Chana': ['c2', 'c3'], 'Dana': ['c3']}
"""
logger.info("\nAlgorithm 2 starts. items %s , agents %s", alloc.remaining_item_capacities, alloc.remaining_agent_capacities)
picking_order = list(alloc.remaining_agents())
for agent in picking_order:
bundle = list(alloc.remaining_items())
agent_capacity = alloc.remaining_agent_capacities[agent]
if agent_capacity >= len(bundle):
alloc.give_bundle(agent, bundle)
else:
for i in range(agent_capacity):
alloc.give(agent, bundle[i])
logger.info("%s picks %s", agent, bundle)



### MAIN PROGRAM

if __name__ == "__main__":
# 1. Run the doctests:
import doctest, sys
print("\n",doctest.testmod(), "\n")


# 2. Run the algorithm on random instances, with logging:
logger.addHandler(logging.StreamHandler(sys.stdout))
logger.setLevel(logging.INFO)

from fairpyx.adaptors import divide_random_instance

divide_random_instance(algorithm=algorithm2,
num_of_agents=30, num_of_items=10, agent_capacity_bounds=[2,5], item_capacity_bounds=[3,12],
item_base_value_bounds=[1,100], item_subjective_ratio_bounds=[0.5,1.5], normalized_sum_of_values=100,
random_seed=1)
2 changes: 1 addition & 1 deletion fairpyx/algorithms/iterated_maximum_matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def _(code:str): return TEXTS[code][explanation_logger.language]
agents_with_empty_bundles = [agent for agent,bundle in map_agent_to_bundle.items() if len(bundle)==0]
for agent in agents_with_empty_bundles:
explanation_logger.info(_("you_did_not_get_any"), agents=agent)
alloc.remove_agent(agent)
alloc.remove_agent_from_loop(agent)
del map_agent_to_bundle[agent]

map_agent_to_item = {agent: bundle[0] for agent,bundle in map_agent_to_bundle.items()}
Expand Down
32 changes: 15 additions & 17 deletions fairpyx/algorithms/picking_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@
logger = logging.getLogger(__name__)




def picking_sequence(alloc: AllocationBuilder, agent_order:list):
"""
Allocate the given items to the given agents using the given picking sequence.
:param alloc: an allocation builder, which tracks the allocation and the remaining capacity for items and agents. of the fair course allocation problem.
:param alloc: an allocation builder, which tracks the allocation and the remaining capacity for items and agents.
:param agent_order: a list of indices of agents, representing the picking sequence. The agents will pick items in this order.
>>> from fairpyx.adaptors import divide
Expand All @@ -32,14 +30,14 @@ def picking_sequence(alloc: AllocationBuilder, agent_order:list):
"""
logger.info("\nPicking-sequence with items %s , agents %s, and agent-order %s", alloc.remaining_item_capacities, alloc.remaining_agent_capacities, agent_order)
for agent in cycle(agent_order):
if len(alloc.remaining_agent_capacities)==0 or len(alloc.remaining_item_capacities)==0:
if len(alloc.remaining_agents())==0 or len(alloc.remaining_items())==0:
break
if not agent in alloc.remaining_agent_capacities:
continue
potential_items_for_agent = set(alloc.remaining_items()).difference(alloc.bundles[agent])
if len(potential_items_for_agent)==0:
logger.info("Agent %s cannot pick any more items: remaining=%s, bundle=%s", agent, alloc.remaining_item_capacities, alloc.bundles[agent])
alloc.remove_agent(agent)
alloc.remove_agent_from_loop(agent)
continue
best_item_for_agent = max(potential_items_for_agent, key=lambda item: alloc.effective_value(agent,item))
alloc.give(agent, best_item_for_agent, logger)
Expand Down Expand Up @@ -125,15 +123,15 @@ def bidirectional_round_robin(alloc: AllocationBuilder, agent_order:list=None):
# logger.addHandler(logging.StreamHandler(sys.stdout))
# logger.setLevel(logging.INFO)

from fairpyx.adaptors import divide_random_instance

print("\n\nRound robin:")
divide_random_instance(algorithm=round_robin,
num_of_agents=30, num_of_items=10, agent_capacity_bounds=[2,5], item_capacity_bounds=[3,12],
item_base_value_bounds=[1,100], item_subjective_ratio_bounds=[0.5,1.5], normalized_sum_of_values=100,
random_seed=1)
print("\n\nBidirectional round robin:")
divide_random_instance(algorithm=bidirectional_round_robin,
num_of_agents=30, num_of_items=10, agent_capacity_bounds=[2,5], item_capacity_bounds=[3,12],
item_base_value_bounds=[1,100], item_subjective_ratio_bounds=[0.5,1.5], normalized_sum_of_values=100,
random_seed=1)
# from fairpyx.adaptors import divide_random_instance

# print("\n\nRound robin:")
# divide_random_instance(algorithm=round_robin,
# num_of_agents=30, num_of_items=10, agent_capacity_bounds=[2,5], item_capacity_bounds=[3,12],
# item_base_value_bounds=[1,100], item_subjective_ratio_bounds=[0.5,1.5], normalized_sum_of_values=100,
# random_seed=1)
# print("\n\nBidirectional round robin:")
# divide_random_instance(algorithm=bidirectional_round_robin,
# num_of_agents=30, num_of_items=10, agent_capacity_bounds=[2,5], item_capacity_bounds=[3,12],
# item_base_value_bounds=[1,100], item_subjective_ratio_bounds=[0.5,1.5], normalized_sum_of_values=100,
# random_seed=1)
12 changes: 6 additions & 6 deletions fairpyx/allocations.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,10 @@ def remaining_instance(self)->Instance:
item_conflicts=self.instance.item_conflicts, # item conflicts are the same as in the original instance
items=self.remaining_items()) # item list may be smaller than in the original instance

def remove_item(self, item:any):
def remove_item_from_loop(self, item:any):
del self.remaining_item_capacities[item]

def remove_agent(self, agent:any):
def remove_agent_from_loop(self, agent:any):
del self.remaining_agent_capacities[agent]

def update_conflicts(self, agent:any, item:any):
Expand All @@ -193,10 +193,10 @@ def give(self, agent:any, item:any, logger=None):
# Update capacities:
self.remaining_agent_capacities[agent] -= 1
if self.remaining_agent_capacities[agent] <= 0:
self.remove_agent(agent)
self.remove_agent_from_loop(agent)
self.remaining_item_capacities[item] -= 1
if self.remaining_item_capacities[item] <= 0:
self.remove_item(item)
self.remove_item_from_loop(item)
self.update_conflicts(agent,item)


Expand All @@ -221,15 +221,15 @@ def give_bundles(self, new_bundles:dict, logger=None):
raise ValueError(f"Agent {agent} has no remaining capacity for {num_of_items} new items")
self.remaining_agent_capacities[agent] -= num_of_items
if self.remaining_agent_capacities[agent] <= 0:
self.remove_agent(agent)
self.remove_agent_from_loop(agent)

for item,num_of_owners in map_item_to_num_of_owners.items():
if num_of_owners==0: continue
if item not in self.remaining_item_capacities or self.remaining_item_capacities[item]<num_of_owners:
raise ValueError(f"Item {item} has no remaining capacity for {num_of_owners} new agents")
self.remaining_item_capacities[item] -= num_of_owners
if self.remaining_item_capacities[item] <= 0:
self.remove_item(item)
self.remove_item_from_loop(item)

for agent,bundle in new_bundles.items():
self.bundles[agent].update(bundle)
Expand Down

0 comments on commit 77ba8cd

Please sign in to comment.