From 18d17c29dab4eb7718f1009295daec98ffd352da Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Tue, 21 May 2024 12:29:14 +0300 Subject: [PATCH 001/111] Algorithems headlines --- .../Optimization_Matching/Demote.py | 39 +++++++++++++ .../Optimization_Matching/FaSt-Gen.py | 42 ++++++++++++++ .../algorithms/Optimization_Matching/FaSt.py | 42 ++++++++++++++ .../Optimization_Matching/LookAheadRoutine.py | 58 +++++++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 fairpyx/algorithms/Optimization_Matching/Demote.py create mode 100644 fairpyx/algorithms/Optimization_Matching/FaSt-Gen.py create mode 100644 fairpyx/algorithms/Optimization_Matching/FaSt.py create mode 100644 fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py diff --git a/fairpyx/algorithms/Optimization_Matching/Demote.py b/fairpyx/algorithms/Optimization_Matching/Demote.py new file mode 100644 index 0000000..0ac2bcf --- /dev/null +++ b/fairpyx/algorithms/Optimization_Matching/Demote.py @@ -0,0 +1,39 @@ +""" + "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) + + Programmer: Hadar Bitan, Yuval Ben-Simhon +""" + +from fairpyx import Instance, AllocationBuilder, ExplanationLogger +import logging +logger = logging.getLogger(__name__) + + +def Demote(): + """ + Demote algorithm: Adjust the matching by moving a student to a lower-ranked college + while maintaining the invariant of a complete stable matching. + The Demote algorithm is a helper function used within the FaSt algorithm to adjust the matching while maintaining stability. + + # def Demote(alloc: AllocationBuilder, student_index: int, down_index: int, up_index: int): + :param alloc: an allocation builder, which tracks the allocation and the remaining capacity. + :param student_index: Index of the student to move. + :param down_index: Index of the college to move the student to. + :param up_index: Index of the upper bound college. + + # The test is the same as the running example we gave in Ex2. + + >>> from fairpyx import AllocationBuilder + >>> alloc = AllocationBuilder(agent_capacities={"s1": 1, "s2": 1, "s3": 1, "s4": 1, "s5": 1}, item_capacities={"c1": 1, "c2": 2, "c3": 2}) + >>> alloc.add_allocation(0, 0) # s1 -> c1 + >>> alloc.add_allocation(1, 1) # s2 -> c2 + >>> alloc.add_allocation(2, 1) # s3 -> c2 + >>> alloc.add_allocation(3, 2) # s4 -> c3 + >>> alloc.add_allocation(4, 2) # s5 -> c3 + >>> Demote(alloc, 2, 2, 1) + >>> alloc.get_allocation() + {'s1': ['c1'], 's2': ['c2'], 's3': ['c3'], 's4': ['c3'], 's5': ['c2']} + """ +if __name__ == "__main__": + import doctest, sys + print(doctest.testmod()) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt-Gen.py b/fairpyx/algorithms/Optimization_Matching/FaSt-Gen.py new file mode 100644 index 0000000..1f6757d --- /dev/null +++ b/fairpyx/algorithms/Optimization_Matching/FaSt-Gen.py @@ -0,0 +1,42 @@ +""" + "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) + + Programmer: Hadar Bitan, Yuval Ben-Simhon +""" + +from fairpyx import Instance, AllocationBuilder, ExplanationLogger +import logging +logger = logging.getLogger(__name__) + +def FaStGen(I:tuple)->dict: + """ + Algorithem 3-FaSt-Gen: finding a match for the general ranked valuations setting. + + + :param I: A presentation of the problem, aka a tuple that contain the list of students(S), the list of colleges(C) when the capacity + of each college is n-1 where n is the number of students, student valuation function(U), college valuation function(V). + + >>> from fairpyx.adaptors import divide + >>> S = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} + >>> C = {"c1", "c2", "c3"} + >>> V = { + "c1" : ["s1","s2","s3","s4","s5","s6","s7"], + "c2" : ["s2","s4","s1","s3","s5","s6","s7"], + "c3" : [s3,s5,s6,s1,s2,s4,s7]} #the colleges valuations + >>> U = { + "s1" : ["c1","c3","c2"], + "s2" : ["c2","c1","c3"], + "s3" : ["c1","c3","c2"], + "s4" : ["c3","c2","c1"], + "s5" : ["c2","c3","c1"], + "s6" :["c3","c1","c2"], + "s7" : ["c1","c2","c3"]} #the students valuations + >>> I = (S, C, U ,V) + >>> FaStGen(I) + {'c1' : ['s1','s2'], 'c2' : ['s3','s4','s5']. 'c3' : ['s6','s7']} + """ + + +if __name__ == "__main__": + import doctest, sys + print(doctest.testmod()) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py new file mode 100644 index 0000000..1db298f --- /dev/null +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -0,0 +1,42 @@ +""" +"On Achieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) + +Programmer: Hadar Bitan, Yuval Ben-Simhon +""" + +from fairpyx import Instance, AllocationBuilder, ExplanationLogger +import logging + +logger = logging.getLogger(__name__) + +def FaSt(): + """ + FaSt algorithm: Find a leximin optimal stable matching under ranked isometric valuations. +# def FaSt(instance: Instance, explanation_logger: ExplanationLogger = ExplanationLogger()): + :param instance: Instance of ranked isometric valuations + + # The test is not the same as the running example we gave in Ex2. + # We asked to change it to be with 3 courses and 7 students, like in algorithm 3 (FaSt-Gen algo). + + >>> from fairpyx.adaptors import divide + >>> S = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} #Student set + >>> C = {"c1", "c2", "c3"} #College set + >>> V = { + ... "s1": {"c1": 1, "c3": 2, "c2": 3}, + ... "s2": {"c2": 1, "c1": 2, "c3": 3}, + ... "s3": {"c1": 1, "c3": 2, "c2": 3}, + ... "s4": {"c3": 1, "c2": 2, "c1": 3}, + ... "s5": {"c2": 1, "c3": 2, "c1": 3}, + ... "s6": {"c3": 1, "c1": 2, "c2": 3}, + ... "s7": {"c1": 1, "c2": 2, "c3": 3} + ... } # V[i][j] is the valuation of Si for matching with Cj + >>> instance = Instance(S, C, V) + >>> FaSt(instance) + >>> alloc = AllocationBuilder(agent_capacities={s: 1 for s in S}, item_capacities={c: 1 for c in C}, valuations=V) + >>> alloc.get_allocation() + {'s1': ['c1'], 's2': ['c2'], 's3': ['c1'], 's4': ['c3'], 's5': ['c3'], 's6': ['c3'], 's7': ['c2']} + """ + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py b/fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py new file mode 100644 index 0000000..7089a50 --- /dev/null +++ b/fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py @@ -0,0 +1,58 @@ +""" + "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) + + Programmer: Hadar Bitan, Yuval Ben-Simhon +""" + +from fairpyx import Instance, AllocationBuilder, ExplanationLogger +import logging +logger = logging.getLogger(__name__) + +def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list, SoftFix:list)->(dict,list,list,list): + """ + Algorithem 4-LookAheadRoutine: Designed to handle cases where a decrease in the leximin value + may be balanced by future changes in the pairing, + the goal is to ensure that the sumi pairing will maintain a good leximin value or even improve it. + + + :param I: A presentation of the problem, aka a tuple that contain the list of students(S), the list of colleges(C) when the capacity + of each college is n-1 where n is the number of students, student valuation function(U), college valuation function(V). + :param match: The current match of the students and colleges. + :param down: The lowest ranked unaffixed college + :param LowerFix: The group of colleges whose lower limit is fixed + :param UpperFix: The group of colleges whose upper limit is fixed. + :param SoftFix: A set of temporary upper limits. + + + >>> from fairpyx.adaptors import divide + >>> S = {"s1", "s2", "s3", "s4", "s5"} + >>> C = {"c1", "c2", "c3", "c4"} + >>> V = { + "c1" : ["s1","s2","s3","s4","s5"], + "c2" : ["s2","s1","s3","s4","s5"], + "c3" : ["s3","s2","s1","s4","s5"], + "c4" : ["s4","s3","s2","s1","s5"]} #the colleges valuations + >>> U = { + "s1" : ["c1","c3","c2","c4"], + "s2" : ["c2","c3","c4","c1"], + "s3" : ["c3","c4","c1","c2"], + "s4" : ["c4","c1","c2","c3"], + "s5" : ["c1","c3","c2","c4"]} #the students valuations # 5 seats available + >>> I = (S, C, U ,V) + >>> match = { + "c1" : ["s1","s2"], + "c2" : ["s3","s5"], + "c3" : ["s4"], + "c4" : []} + >>> down = "c4" + >>> LowerFix = [] + >>> UpperFix = [] + >>> SoftFix = [] + >>> LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) + ({'c1': ['s1', 's2'], 'c2': ['s5'], 'c3' : ['s3'], 'c4' : ['s4']}, ['c2'], [], []) + """ + + +if __name__ == "__main__": + import doctest, sys + print(doctest.testmod()) From 3ecff8aa1f72e27567b5755cae5c4cb9f6433db9 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Tue, 21 May 2024 13:54:26 +0300 Subject: [PATCH 002/111] Test #1 try --- .../Optimization_Matching/Demote.py | 4 +- .../algorithms/Optimization_Matching/FaSt.py | 2 +- .../Tests/test_Demote.py | 91 +++++++++++++++++++ .../Optimization_Matching/Tests/test_FaSt.py | 0 .../Tests/test_FaStGen.py | 0 .../Tests/test_look_ahead.py | 0 6 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py create mode 100644 fairpyx/algorithms/Optimization_Matching/Tests/test_FaSt.py create mode 100644 fairpyx/algorithms/Optimization_Matching/Tests/test_FaStGen.py create mode 100644 fairpyx/algorithms/Optimization_Matching/Tests/test_look_ahead.py diff --git a/fairpyx/algorithms/Optimization_Matching/Demote.py b/fairpyx/algorithms/Optimization_Matching/Demote.py index 0ac2bcf..e4c9d0d 100644 --- a/fairpyx/algorithms/Optimization_Matching/Demote.py +++ b/fairpyx/algorithms/Optimization_Matching/Demote.py @@ -1,7 +1,7 @@ """ "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) - Programmer: Hadar Bitan, Yuval Ben-Simhon + Programmers: Hadar Bitan, Yuval Ben-Simhon """ from fairpyx import Instance, AllocationBuilder, ExplanationLogger @@ -22,7 +22,7 @@ def Demote(): :param up_index: Index of the upper bound college. # The test is the same as the running example we gave in Ex2. - + >>> from fairpyx import AllocationBuilder >>> alloc = AllocationBuilder(agent_capacities={"s1": 1, "s2": 1, "s3": 1, "s4": 1, "s5": 1}, item_capacities={"c1": 1, "c2": 2, "c3": 2}) >>> alloc.add_allocation(0, 0) # s1 -> c1 diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 1db298f..1a32120 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -1,7 +1,7 @@ """ "On Achieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) -Programmer: Hadar Bitan, Yuval Ben-Simhon +Programmers: Hadar Bitan, Yuval Ben-Simhon """ from fairpyx import Instance, AllocationBuilder, ExplanationLogger diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py b/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py new file mode 100644 index 0000000..4221792 --- /dev/null +++ b/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py @@ -0,0 +1,91 @@ +""" +Test that Demote algorithm returns a feasible solution. + +Programmers: Hadar Bitan, Yuval Ben-Simhon +""" + +import pytest +from fairpyx import AllocationBuilder +from your_module import Demote + +def test_demote_simple(): + """ + Test the Demote algorithm with a simple scenario. + """ + # Create an AllocationBuilder instance + alloc = AllocationBuilder(agent_capacities={"s1": 1, "s2": 1, "s3": 1, "s4": 1, "s5": 1}, + item_capacities={"c1": 2, "c2": 2, "c3": 2}) + + # Initial matching + alloc.add_allocation(0, 0) # s1 -> c1 + alloc.add_allocation(1, 1) # s2 -> c2 + alloc.add_allocation(2, 1) # s3 -> c2 + alloc.add_allocation(3, 2) # s4 -> c3 + alloc.add_allocation(4, 2) # s5 -> c3 + + # Apply Demote algorithm + Demote(alloc, 2, 2, 1) + + # Expected result after demotion + expected_result = {'s1': ['c1'], 's2': ['c2'], 's3': ['c3'], 's4': ['c3'], 's5': ['c3']} + + # Check if the result matches the expected result + assert alloc.get_allocation() == expected_result, "Demote algorithm test failed" + +def test_demote_edge_cases(): + """ + Test edge cases for the Demote algorithm. + """ + # Case 1: Moving the last student to the first college + alloc1 = AllocationBuilder(agent_capacities={"s1": 1}, item_capacities={"c1": 1}) + alloc1.add_allocation(0, 0) # s1 -> c1 + Demote(alloc1, 0, 1, 0) + assert alloc1.get_allocation() == {'s1': ['c1']}, "Demote algorithm edge case 1 failed" + + # Case 2: Moving the first student to the last college + alloc2 = AllocationBuilder(agent_capacities={"s1": 1}, item_capacities={"c1": 1}) + alloc2.add_allocation(0, 0) # s1 -> c1 + Demote(alloc2, 0, 2, 0) + assert alloc2.get_allocation() == {'s1': ['c3']}, "Demote algorithm edge case 2 failed" + + # Case 3: Moving a student to the same college (no change expected) + alloc3 = AllocationBuilder(agent_capacities={"s1": 1}, item_capacities={"c1": 1}) + alloc3.add_allocation(0, 0) # s1 -> c1 + Demote(alloc3, 0, 0, 0) + assert alloc3.get_allocation() == {'s1': ['c1']}, "Demote algorithm edge case 3 failed" + + # Case 4: Moving a student when all other colleges are full + alloc4 = AllocationBuilder(agent_capacities={"s1": 1}, item_capacities={"c1": 1, "c2": 1}) + alloc4.add_allocation(0, 0) # s1 -> c1 + alloc4.add_allocation(1, 1) # s2 -> c2 + Demote(alloc4, 0, 2, 0) + assert alloc4.get_allocation() == {'s1': ['c1'], 's2': ['c2']}, "Demote algorithm edge case 4 failed" + +def test_demote_large_input(): + """ + Test the Demote algorithm on a large input. + """ + num_students = 1000 + num_colleges = 100 + student_indices = [f"s{i}" for i in range(1, num_students + 1)] + college_indices = [f"c{i}" for i in range(1, num_colleges + 1)] + + agent_capacities = {student: 1 for student in student_indices} + item_capacities = {college: 10 for college in college_indices} + + alloc = AllocationBuilder(agent_capacities=agent_capacities, item_capacities=item_capacities) + + # Initial allocation + for i, student in enumerate(student_indices): + alloc.add_allocation(i, i % num_colleges) + + # Move the last student to the last college + Demote(alloc, num_students - 1, num_colleges - 1, 0) + + allocation = alloc.get_allocation() + assert len(allocation) == num_students, "Demote algorithm large input failed" + assert all(len(courses) <= 10 for courses in allocation.values()), "Demote algorithm large input capacity failed" + + +if __name__ == "__main__": + pytest.main(["-v", __file__]) diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_FaSt.py b/fairpyx/algorithms/Optimization_Matching/Tests/test_FaSt.py new file mode 100644 index 0000000..e69de29 diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_FaStGen.py b/fairpyx/algorithms/Optimization_Matching/Tests/test_FaStGen.py new file mode 100644 index 0000000..e69de29 diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_look_ahead.py b/fairpyx/algorithms/Optimization_Matching/Tests/test_look_ahead.py new file mode 100644 index 0000000..e69de29 From 6b4203320ae6e1c280e7aab57ed5f18d4aea5fc4 Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Tue, 21 May 2024 14:01:07 +0300 Subject: [PATCH 003/111] Update test_Demote.py --- fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py b/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py index 4221792..3982d2e 100644 --- a/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py +++ b/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py @@ -2,6 +2,7 @@ Test that Demote algorithm returns a feasible solution. Programmers: Hadar Bitan, Yuval Ben-Simhon + """ import pytest From 8edde050d3627d3d13a653df9b4e57698b66f090 Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Tue, 21 May 2024 21:25:46 +0300 Subject: [PATCH 004/111] Update test_Demote.py --- fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py b/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py index 3982d2e..65ae1a9 100644 --- a/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py +++ b/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py @@ -7,7 +7,8 @@ import pytest from fairpyx import AllocationBuilder -from your_module import Demote +import Demote + def test_demote_simple(): """ From d8868176abfacd581eb84e766c33bd00c8c9fb0d Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Tue, 21 May 2024 22:32:14 +0300 Subject: [PATCH 005/111] tests finish --- .../Optimization_Matching/Demote.py | 3 + .../algorithms/Optimization_Matching/FaSt.py | 2 + .../{FaSt-Gen.py => FaStGen.py} | 2 + .../Optimization_Matching/LookAheadRoutine.py | 4 +- .../Tests/test_Demote.py | 11 +- .../Optimization_Matching/Tests/test_FaSt.py | 75 ++++++++++ .../Tests/test_FaStGen.py | 92 +++++++++++++ .../Tests/test_look_ahead.py | 129 ++++++++++++++++++ 8 files changed, 316 insertions(+), 2 deletions(-) rename fairpyx/algorithms/Optimization_Matching/{FaSt-Gen.py => FaStGen.py} (97%) diff --git a/fairpyx/algorithms/Optimization_Matching/Demote.py b/fairpyx/algorithms/Optimization_Matching/Demote.py index e4c9d0d..2b0fa23 100644 --- a/fairpyx/algorithms/Optimization_Matching/Demote.py +++ b/fairpyx/algorithms/Optimization_Matching/Demote.py @@ -2,6 +2,7 @@ "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) Programmers: Hadar Bitan, Yuval Ben-Simhon + Date: 19.5.2024 """ from fairpyx import Instance, AllocationBuilder, ExplanationLogger @@ -34,6 +35,8 @@ def Demote(): >>> alloc.get_allocation() {'s1': ['c1'], 's2': ['c2'], 's3': ['c3'], 's4': ['c3'], 's5': ['c2']} """ + return 0 + if __name__ == "__main__": import doctest, sys print(doctest.testmod()) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 1a32120..93bcca6 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -2,6 +2,7 @@ "On Achieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) Programmers: Hadar Bitan, Yuval Ben-Simhon +Date: 19.5.2024 """ from fairpyx import Instance, AllocationBuilder, ExplanationLogger @@ -36,6 +37,7 @@ def FaSt(): >>> alloc.get_allocation() {'s1': ['c1'], 's2': ['c2'], 's3': ['c1'], 's4': ['c3'], 's5': ['c3'], 's6': ['c3'], 's7': ['c2']} """ + return 0 if __name__ == "__main__": import doctest diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt-Gen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py similarity index 97% rename from fairpyx/algorithms/Optimization_Matching/FaSt-Gen.py rename to fairpyx/algorithms/Optimization_Matching/FaStGen.py index 1f6757d..13235ee 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt-Gen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -2,6 +2,7 @@ "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) Programmer: Hadar Bitan, Yuval Ben-Simhon + Date: 19.5.2024 """ from fairpyx import Instance, AllocationBuilder, ExplanationLogger @@ -35,6 +36,7 @@ def FaStGen(I:tuple)->dict: >>> FaStGen(I) {'c1' : ['s1','s2'], 'c2' : ['s3','s4','s5']. 'c3' : ['s6','s7']} """ + return 0 if __name__ == "__main__": diff --git a/fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py b/fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py index 7089a50..98a1ab3 100644 --- a/fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py +++ b/fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py @@ -2,6 +2,7 @@ "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) Programmer: Hadar Bitan, Yuval Ben-Simhon + Date: 19.5.2024 """ from fairpyx import Instance, AllocationBuilder, ExplanationLogger @@ -37,7 +38,7 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list "s2" : ["c2","c3","c4","c1"], "s3" : ["c3","c4","c1","c2"], "s4" : ["c4","c1","c2","c3"], - "s5" : ["c1","c3","c2","c4"]} #the students valuations # 5 seats available + "s5" : ["c1","c3","c2","c4"]} #the students valuations >>> I = (S, C, U ,V) >>> match = { "c1" : ["s1","s2"], @@ -51,6 +52,7 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list >>> LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) ({'c1': ['s1', 's2'], 'c2': ['s5'], 'c3' : ['s3'], 'c4' : ['s4']}, ['c2'], [], []) """ + return 0 if __name__ == "__main__": diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py b/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py index 65ae1a9..998312a 100644 --- a/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py +++ b/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py @@ -2,7 +2,8 @@ Test that Demote algorithm returns a feasible solution. Programmers: Hadar Bitan, Yuval Ben-Simhon - +Date: 19.5.2024 +We used chat-GPT and our friends from the university for ideas of cases. """ import pytest @@ -84,10 +85,18 @@ def test_demote_large_input(): # Move the last student to the last college Demote(alloc, num_students - 1, num_colleges - 1, 0) + # Add assertions allocation = alloc.get_allocation() assert len(allocation) == num_students, "Demote algorithm large input failed" assert all(len(courses) <= 10 for courses in allocation.values()), "Demote algorithm large input capacity failed" +def test_demote_empty_input(): + """ + Test the Demote algorithm with empty input. + """ + alloc_empty = AllocationBuilder(agent_capacities={}, item_capacities={}) + Demote(alloc_empty, 0, 0, 0) + assert alloc_empty.get_allocation() == {}, "Demote algorithm failed on empty input" if __name__ == "__main__": pytest.main(["-v", __file__]) diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_FaSt.py b/fairpyx/algorithms/Optimization_Matching/Tests/test_FaSt.py index e69de29..6650e4f 100644 --- a/fairpyx/algorithms/Optimization_Matching/Tests/test_FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/Tests/test_FaSt.py @@ -0,0 +1,75 @@ +""" +Test the FaSt algorithm for course allocation. + +Programmers: Hadar Bitan, Yuval Ben-Simhon +Date: 19.5.2024 +We used chat-Gpt and our friends from the university for ideas of cases. +""" + +import pytest +from fairpyx import Instance +import FaSt + +def test_FaSt_basic_case(): + """ + Basic test case for the FaSt algorithm. + """ + # Define the instance + S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + C = ["c1", "c2", "c3"] + V = { + "c1": ["s1", "s2", "s3", "s4", "s5", "s6", "s7"], + "c2": ["s2", "s4", "s1", "s3", "s5", "s6", "s7"], + "c3": ["s3", "s5", "s6", "s1", "s2", "s4", "s7"] + } + instance = Instance(agent_capacities={student: 1 for student in S}, item_capacities={course: 1 for course in C}, valuations=V) + + # Run the FaSt algorithm + allocation = FaSt(instance) + + # Define the expected allocation + expected_allocation = {'s1': ['c2'], 's2': ['c1'], 's3': ['c3'], 's4': ['c3'], 's5': ['c3'], 's6': ['c1'], 's7': ['c2']} + + # Assert the result + assert allocation == expected_allocation, "FaSt algorithm basic case failed" + +def test_FaSt_edge_cases(): + """ + Test edge cases for the FaSt algorithm. + """ + # Edge case 1: Empty input + instance_empty = Instance(agent_capacities={}, item_capacities={}, valuations={}) + allocation_empty = FaSt(instance_empty) + assert allocation_empty == {}, "FaSt algorithm failed on empty input" + + # Edge case 2: Single student and single course + instance_single = Instance(agent_capacities={"s1": 1}, item_capacities={"c1": 1}, valuations={"c1": ["s1"]}) + allocation_single = FaSt(instance_single) + assert allocation_single == {"s1": ["c1"]}, "FaSt algorithm failed on single student and single course" + +def test_FaSt_large_input(): + """ + Test the FaSt algorithm on a large input. + """ + # Define the instance with a large number of students and courses + num_students = 100 + num_courses = 50 + students = [f"s{i}" for i in range(1, num_students + 1)] + courses = [f"c{i}" for i in range(1, num_courses + 1)] + valuations = {course: students for course in courses} + + instance_large = Instance(agent_capacities={student: 1 for student in students}, item_capacities={course: 10 for course in courses}, valuations=valuations) + + # Run the FaSt algorithm + allocation_large = FaSt(instance_large) + + # Add assertions + # Ensure that all students are assigned to a course + assert len(allocation_large) == len(students), "Not all students are assigned to a course" + + # Ensure that each course has the correct number of students assigned + for course, assigned_students in allocation_large.items(): + assert len(assigned_students) == 10, f"Incorrect number of students assigned to {course}" + +if __name__ == "__main__": + pytest.main(["-v", __file__]) diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_FaStGen.py b/fairpyx/algorithms/Optimization_Matching/Tests/test_FaStGen.py index e69de29..5f98030 100644 --- a/fairpyx/algorithms/Optimization_Matching/Tests/test_FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/Tests/test_FaStGen.py @@ -0,0 +1,92 @@ +""" +Test the FaStGen algorithm for course allocation. + +Programmers: Hadar Bitan, Yuval Ben-Simhon +Date: 19.5.2024 +We used chat-Gpt and our friends from the university for ideas of cases. +""" + +import pytest +from fairpyx import Instance +import FaStGen + +def test_FaStGen_basic_case(): + """ + Basic test case for the FaStGen algorithm. + """ + # Define the instance + S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + C = ["c1", "c2", "c3"] + V = { + "c1": ["s1", "s2", "s3", "s4", "s5", "s6", "s7"], + "c2": ["s2", "s4", "s1", "s3", "s5", "s6", "s7"], + "c3": ["s3", "s5", "s6", "s1", "s2", "s4", "s7"] + } + U = { + "s1": ["c1", "c3", "c2"], + "s2": ["c2", "c1", "c3"], + "s3": ["c1", "c3", "c2"], + "s4": ["c3", "c2", "c1"], + "s5": ["c2", "c3", "c1"], + "s6": ["c3", "c1", "c2"], + "s7": ["c1", "c2", "c3"] + } + + # Assuming `Instance` can handle student and course preferences directly + instance = Instance(S, C, U, V) + + # Run the FaStGen algorithm + allocation = FaStGen(instance) + + # Define the expected allocation (this is hypothetical; you should set it based on the actual expected output) + expected_allocation = {'s1': 'c1', 's2': 'c2', 's3': 'c3', 's4': 'c1', 's5': 'c2', 's6': 'c3', 's7': 'c1'} + + # Assert the result + assert allocation == expected_allocation, "FaStGen algorithm basic case failed" + +def test_FaStGen_edge_cases(): + """ + Test edge cases for the FaStGen algorithm. + """ + # Edge case 1: Empty input + instance_empty = Instance([], [], {}, {}) + allocation_empty = FaStGen(instance_empty) + assert allocation_empty == {}, "FaStGen algorithm failed on empty input" + + # Edge case 2: Single student and single course + S_single = ["s1"] + C_single = ["c1"] + U_single = {"s1": ["c1"]} + V_single = {"c1": ["s1"]} + instance_single = Instance(S_single, C_single, U_single, V_single) + allocation_single = FaStGen(instance_single) + assert allocation_single == {"s1": "c1"}, "FaStGen algorithm failed on single student and single course" + +def test_FaStGen_large_input(): + """ + Test the FaStGen algorithm on a large input. + """ + # Define the instance with a large number of students and courses + num_students = 100 + num_courses = 50 + students = [f"s{i}" for i in range(1, num_students + 1)] + courses = [f"c{i}" for i in range(1, num_courses + 1)] + valuations = {course: students for course in courses} + preferences = {student: courses for student in students} # Assuming all students prefer all courses equally + + instance_large = Instance(students, courses, preferences, valuations) + + # Run the FaStGen algorithm + allocation_large = FaStGen(instance_large) + + # Add assertions + # Ensure that all students are assigned to a course + assert len(allocation_large) == len(students), "Not all students are assigned to a course" + + # Ensure that each course has students assigned to it + for student, course in allocation_large.items(): + assert student in students, f"Unexpected student {student} in allocation" + assert course in courses, f"Unexpected course {course} in allocation" + +if __name__ == "__main__": + pytest.main(["-v", __file__]) diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_look_ahead.py b/fairpyx/algorithms/Optimization_Matching/Tests/test_look_ahead.py index e69de29..5549cd7 100644 --- a/fairpyx/algorithms/Optimization_Matching/Tests/test_look_ahead.py +++ b/fairpyx/algorithms/Optimization_Matching/Tests/test_look_ahead.py @@ -0,0 +1,129 @@ +""" +Test the Look Ahead Routine algorithm for course allocation. + +Programmers: Hadar Bitan, Yuval Ben-Simhon +Date: 19.5.2024 +We used chat-GPT and our friends from the university for ideas of cases. +""" + +import pytest +from fairpyx import Instance +import LookAheadRoutine + +def test_look_ahead_routine_basic_case(): + """ + Basic test case for the Look Ahead Routine algorithm. + """ + # Define the instance + S = {"s1", "s2", "s3", "s4", "s5"} + C = {"c1", "c2", "c3", "c4"} + U = { + "s1": ["c1", "c3", "c2", "c4"], + "s2": ["c2", "c3", "c4", "c1"], + "s3": ["c3", "c4", "c1", "c2"], + "s4": ["c4", "c1", "c2", "c3"], + "s5": ["c1", "c3", "c2", "c4"] + } + V = { + "c1": ["s1", "s2", "s3", "s4", "s5"], + "c2": ["s2", "s1", "s3", "s4", "s5"], + "c3": ["s3", "s2", "s1", "s4", "s5"], + "c4": ["s4", "s3", "s2", "s1", "s5"] + } + + I = (S, C, U, V) + match = { + "c1": ["s1", "s2"], + "c2": ["s3", "s5"], + "c3": ["s4"], + "c4": [] + } + down = "c4" + LowerFix = [] + UpperFix = [] + SoftFix = [] + + # Run the Look Ahead Routine algorithm + new_match, new_LowerFix, new_UpperFix, new_SoftFix = LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) + + # Define the expected output + expected_new_match = {'c1': ['s1', 's2'], 'c2': ['s5'], 'c3': ['s3'], 'c4': ['s4']} + expected_new_LowerFix = ['c2'] + expected_new_UpperFix = [] + expected_new_SoftFix = [] + + # Assert the result + assert new_match == expected_new_match, "Look Ahead Routine algorithm basic case failed" + assert new_LowerFix == expected_new_LowerFix, "Look Ahead Routine algorithm basic case failed on LowerFix" + assert new_UpperFix == expected_new_UpperFix, "Look Ahead Routine algorithm basic case failed on UpperFix" + assert new_SoftFix == expected_new_SoftFix, "Look Ahead Routine algorithm basic case failed on SoftFix" + +def test_look_ahead_routine_edge_cases(): + """ + Test edge cases for the Look Ahead Routine algorithm. + """ + # Edge case 1: Empty input + I_empty = (set(), set(), {}, {}) + match_empty = {} + down_empty = "" + LowerFix_empty = [] + UpperFix_empty = [] + SoftFix_empty = [] + + new_match_empty, new_LowerFix_empty, new_UpperFix_empty, new_SoftFix_empty = LookAheadRoutine(I_empty, match_empty, down_empty, LowerFix_empty, UpperFix_empty, SoftFix_empty) + assert new_match_empty == {}, "Look Ahead Routine algorithm failed on empty input" + assert new_LowerFix_empty == [], "Look Ahead Routine algorithm failed on empty input (LowerFix)" + assert new_UpperFix_empty == [], "Look Ahead Routine algorithm failed on empty input (UpperFix)" + assert new_SoftFix_empty == [], "Look Ahead Routine algorithm failed on empty input (SoftFix)" + + # Edge case 2: Single student and single course + I_single = ({"s1"}, {"c1"}, {"s1": ["c1"]}, {"c1": ["s1"]}) + match_single = {"c1": ["s1"]} + down_single = "c1" + LowerFix_single = [] + UpperFix_single = [] + SoftFix_single = [] + + new_match_single, new_LowerFix_single, new_UpperFix_single, new_SoftFix_single = LookAheadRoutine(I_single, match_single, down_single, LowerFix_single, UpperFix_single, SoftFix_single) + assert new_match_single == {"c1": ["s1"]}, "Look Ahead Routine algorithm failed on single student and single course" + assert new_LowerFix_single == [], "Look Ahead Routine algorithm failed on single student and single course (LowerFix)" + assert new_UpperFix_single == [], "Look Ahead Routine algorithm failed on single student and single course (UpperFix)" + assert new_SoftFix_single == [], "Look Ahead Routine algorithm failed on single student and single course (SoftFix)" + +def test_look_ahead_routine_large_input(): + """ + Test the Look Ahead Routine algorithm on a large input. + """ + # Define the instance with a large number of students and courses + num_students = 100 + num_courses = 50 + students = {f"s{i}" for i in range(1, num_students + 1)} + courses = {f"c{i}" for i in range(1, num_courses + 1)} + U = {student: list(courses) for student in students} + V = {course: list(students) for course in courses} + + I_large = (students, courses, U, V) + match_large = {course: [] for course in courses} + down_large = "c1" + LowerFix_large = [] + UpperFix_large = [] + SoftFix_large = [] + + # Run the Look Ahead Routine algorithm + new_match_large, new_LowerFix_large, new_UpperFix_large, new_SoftFix_large = LookAheadRoutine(I_large, match_large, down_large, LowerFix_large, UpperFix_large, SoftFix_large) + + # Add assertions + # Ensure that all students are considered in the new matching + assert all(student in {s for lst in new_match_large.values() for s in lst} for student in students), "Not all students are considered in the new matching" + + # Ensure no student is matched to more than one course + all_students = [s for lst in new_match_large.values() for s in lst] + assert len(all_students) == len(set(all_students)), "A student is matched to more than one course" + + # Ensure LowerFix, UpperFix, and SoftFix are updated correctly + assert isinstance(new_LowerFix_large, list), "LowerFix is not a list" + assert isinstance(new_UpperFix_large, list), "UpperFix is not a list" + assert isinstance(new_SoftFix_large, list), "SoftFix is not a list" + +if __name__ == "__main__": + pytest.main(["-v", __file__]) From 097e59ef6b508c03993fccab94e8473289423698 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Tue, 28 May 2024 15:30:31 +0300 Subject: [PATCH 006/111] fixing according to erel --- .../Optimization_Matching/Demote.py | 42 ------------- .../algorithms/Optimization_Matching/FaSt.py | 48 +++++++++++---- .../Optimization_Matching/FaStGen.py | 55 +++++++++++++++-- .../Optimization_Matching/LookAheadRoutine.py | 60 ------------------- .../Tests => tests}/test_Demote.py | 0 .../Tests => tests}/test_FaSt.py | 0 .../Tests => tests}/test_FaStGen.py | 0 .../Tests => tests}/test_look_ahead.py | 0 8 files changed, 86 insertions(+), 119 deletions(-) delete mode 100644 fairpyx/algorithms/Optimization_Matching/Demote.py delete mode 100644 fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py rename {fairpyx/algorithms/Optimization_Matching/Tests => tests}/test_Demote.py (100%) rename {fairpyx/algorithms/Optimization_Matching/Tests => tests}/test_FaSt.py (100%) rename {fairpyx/algorithms/Optimization_Matching/Tests => tests}/test_FaStGen.py (100%) rename {fairpyx/algorithms/Optimization_Matching/Tests => tests}/test_look_ahead.py (100%) diff --git a/fairpyx/algorithms/Optimization_Matching/Demote.py b/fairpyx/algorithms/Optimization_Matching/Demote.py deleted file mode 100644 index 2b0fa23..0000000 --- a/fairpyx/algorithms/Optimization_Matching/Demote.py +++ /dev/null @@ -1,42 +0,0 @@ -""" - "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) - - Programmers: Hadar Bitan, Yuval Ben-Simhon - Date: 19.5.2024 -""" - -from fairpyx import Instance, AllocationBuilder, ExplanationLogger -import logging -logger = logging.getLogger(__name__) - - -def Demote(): - """ - Demote algorithm: Adjust the matching by moving a student to a lower-ranked college - while maintaining the invariant of a complete stable matching. - The Demote algorithm is a helper function used within the FaSt algorithm to adjust the matching while maintaining stability. - - # def Demote(alloc: AllocationBuilder, student_index: int, down_index: int, up_index: int): - :param alloc: an allocation builder, which tracks the allocation and the remaining capacity. - :param student_index: Index of the student to move. - :param down_index: Index of the college to move the student to. - :param up_index: Index of the upper bound college. - - # The test is the same as the running example we gave in Ex2. - - >>> from fairpyx import AllocationBuilder - >>> alloc = AllocationBuilder(agent_capacities={"s1": 1, "s2": 1, "s3": 1, "s4": 1, "s5": 1}, item_capacities={"c1": 1, "c2": 2, "c3": 2}) - >>> alloc.add_allocation(0, 0) # s1 -> c1 - >>> alloc.add_allocation(1, 1) # s2 -> c2 - >>> alloc.add_allocation(2, 1) # s3 -> c2 - >>> alloc.add_allocation(3, 2) # s4 -> c3 - >>> alloc.add_allocation(4, 2) # s5 -> c3 - >>> Demote(alloc, 2, 2, 1) - >>> alloc.get_allocation() - {'s1': ['c1'], 's2': ['c2'], 's3': ['c3'], 's4': ['c3'], 's5': ['c2']} - """ - return 0 - -if __name__ == "__main__": - import doctest, sys - print(doctest.testmod()) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 93bcca6..6347aee 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -10,16 +10,42 @@ logger = logging.getLogger(__name__) -def FaSt(): +def Demote(matching:dict, student_index:int, down:int, up:int): """ - FaSt algorithm: Find a leximin optimal stable matching under ranked isometric valuations. -# def FaSt(instance: Instance, explanation_logger: ExplanationLogger = ExplanationLogger()): - :param instance: Instance of ranked isometric valuations + Demote algorithm: Adjust the matching by moving a student to a lower-ranked college + while maintaining the invariant of a complete stable matching. + The Demote algorithm is a helper function used within the FaSt algorithm to adjust the matching while maintaining stability. + + :param matching: the matchinf of the students with colleges. + :param student_index: Index of the student to move. + :param down_index: Index of the college to move the student to. + :param up_index: Index of the upper bound college. + + # The test is the same as the running example we gave in Ex2. + + >>> from fairpyx import AllocationBuilder + >>> alloc = AllocationBuilder(agent_capacities={"s1": 1, "s2": 1, "s3": 1, "s4": 1, "s5": 1}, item_capacities={"c1": 1, "c2": 2, "c3": 2}) + >>> alloc.add_allocation(0, 0) # s1 -> c1 + >>> alloc.add_allocation(1, 1) # s2 -> c2 + >>> alloc.add_allocation(2, 1) # s3 -> c2 + >>> alloc.add_allocation(3, 2) # s4 -> c3 + >>> alloc.add_allocation(4, 2) # s5 -> c3 + >>> Demote(alloc, 2, 2, 1) + >>> alloc.get_allocation() + {'s1': ['c1'], 's2': ['c2'], 's3': ['c3'], 's4': ['c3'], 's5': ['c2']} + """ + return 0 - # The test is not the same as the running example we gave in Ex2. - # We asked to change it to be with 3 courses and 7 students, like in algorithm 3 (FaSt-Gen algo). - >>> from fairpyx.adaptors import divide +def FaSt(alloc: AllocationBuilder): + """ + FaSt algorithm: Find a leximin optimal stable matching under ranked isometric valuations. + # def FaSt(instance: Instance, explanation_logger: ExplanationLogger = ExplanationLogger()): + :param alloc: an allocation builder, which tracks the allocation and the remaining capacity for items and agents. + # The test is not the same as the running example we gave in Ex2. + # We asked to change it to be with 3 courses and 7 students, like in algorithm 3 (FaSt-Gen algo). + + >>> from fairpyx.adaptors import divide >>> S = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} #Student set >>> C = {"c1", "c2", "c3"} #College set >>> V = { @@ -31,14 +57,14 @@ def FaSt(): ... "s6": {"c3": 1, "c1": 2, "c2": 3}, ... "s7": {"c1": 1, "c2": 2, "c3": 3} ... } # V[i][j] is the valuation of Si for matching with Cj - >>> instance = Instance(S, C, V) - >>> FaSt(instance) - >>> alloc = AllocationBuilder(agent_capacities={s: 1 for s in S}, item_capacities={c: 1 for c in C}, valuations=V) - >>> alloc.get_allocation() + >>> instance = Instance(agents=S, items=C, valuations=V) + >>> divide(FaSt, instance=instance) {'s1': ['c1'], 's2': ['c2'], 's3': ['c1'], 's4': ['c3'], 's5': ['c3'], 's6': ['c3'], 's7': ['c2']} """ return 0 + + if __name__ == "__main__": import doctest doctest.testmod() diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index 13235ee..b5c50c5 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -9,14 +9,12 @@ import logging logger = logging.getLogger(__name__) -def FaStGen(I:tuple)->dict: +def FaStGen(alloc: AllocationBuilder)->dict: """ Algorithem 3-FaSt-Gen: finding a match for the general ranked valuations setting. - :param I: A presentation of the problem, aka a tuple that contain the list of students(S), the list of colleges(C) when the capacity - of each college is n-1 where n is the number of students, student valuation function(U), college valuation function(V). - + :param alloc: an allocation builder, which tracks the allocation and the remaining capacity for items and agents. >>> from fairpyx.adaptors import divide >>> S = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} >>> C = {"c1", "c2", "c3"} @@ -32,13 +30,58 @@ def FaStGen(I:tuple)->dict: "s5" : ["c2","c3","c1"], "s6" :["c3","c1","c2"], "s7" : ["c1","c2","c3"]} #the students valuations - >>> I = (S, C, U ,V) - >>> FaStGen(I) + >>> instance = (S, C, U ,V) + >>> divide(FaStGen, instance=instance) {'c1' : ['s1','s2'], 'c2' : ['s3','s4','s5']. 'c3' : ['s6','s7']} """ return 0 +def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list, SoftFix:list)->(dict,list,list,list): + """ + Algorithem 4-LookAheadRoutine: Designed to handle cases where a decrease in the leximin value + may be balanced by future changes in the pairing, + the goal is to ensure that the sumi pairing will maintain a good leximin value or even improve it. + + + :param I: A presentation of the problem, aka a tuple that contain the list of students(S), the list of colleges(C) when the capacity + of each college is n-1 where n is the number of students, student valuation function(U), college valuation function(V). + :param match: The current match of the students and colleges. + :param down: The lowest ranked unaffixed college + :param LowerFix: The group of colleges whose lower limit is fixed + :param UpperFix: The group of colleges whose upper limit is fixed. + :param SoftFix: A set of temporary upper limits. + + + >>> from fairpyx.adaptors import divide + >>> S = {"s1", "s2", "s3", "s4", "s5"} + >>> C = {"c1", "c2", "c3", "c4"} + >>> V = { + "c1" : ["s1","s2","s3","s4","s5"], + "c2" : ["s2","s1","s3","s4","s5"], + "c3" : ["s3","s2","s1","s4","s5"], + "c4" : ["s4","s3","s2","s1","s5"]} #the colleges valuations + >>> U = { + "s1" : ["c1","c3","c2","c4"], + "s2" : ["c2","c3","c4","c1"], + "s3" : ["c3","c4","c1","c2"], + "s4" : ["c4","c1","c2","c3"], + "s5" : ["c1","c3","c2","c4"]} #the students valuations + >>> I = (S, C, U ,V) + >>> match = { + "c1" : ["s1","s2"], + "c2" : ["s3","s5"], + "c3" : ["s4"], + "c4" : []} + >>> down = "c4" + >>> LowerFix = [] + >>> UpperFix = [] + >>> SoftFix = [] + >>> LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) + ({'c1': ['s1', 's2'], 'c2': ['s5'], 'c3' : ['s3'], 'c4' : ['s4']}, ['c2'], [], []) + """ + return 0 + if __name__ == "__main__": import doctest, sys print(doctest.testmod()) diff --git a/fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py b/fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py deleted file mode 100644 index 98a1ab3..0000000 --- a/fairpyx/algorithms/Optimization_Matching/LookAheadRoutine.py +++ /dev/null @@ -1,60 +0,0 @@ -""" - "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) - - Programmer: Hadar Bitan, Yuval Ben-Simhon - Date: 19.5.2024 -""" - -from fairpyx import Instance, AllocationBuilder, ExplanationLogger -import logging -logger = logging.getLogger(__name__) - -def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list, SoftFix:list)->(dict,list,list,list): - """ - Algorithem 4-LookAheadRoutine: Designed to handle cases where a decrease in the leximin value - may be balanced by future changes in the pairing, - the goal is to ensure that the sumi pairing will maintain a good leximin value or even improve it. - - - :param I: A presentation of the problem, aka a tuple that contain the list of students(S), the list of colleges(C) when the capacity - of each college is n-1 where n is the number of students, student valuation function(U), college valuation function(V). - :param match: The current match of the students and colleges. - :param down: The lowest ranked unaffixed college - :param LowerFix: The group of colleges whose lower limit is fixed - :param UpperFix: The group of colleges whose upper limit is fixed. - :param SoftFix: A set of temporary upper limits. - - - >>> from fairpyx.adaptors import divide - >>> S = {"s1", "s2", "s3", "s4", "s5"} - >>> C = {"c1", "c2", "c3", "c4"} - >>> V = { - "c1" : ["s1","s2","s3","s4","s5"], - "c2" : ["s2","s1","s3","s4","s5"], - "c3" : ["s3","s2","s1","s4","s5"], - "c4" : ["s4","s3","s2","s1","s5"]} #the colleges valuations - >>> U = { - "s1" : ["c1","c3","c2","c4"], - "s2" : ["c2","c3","c4","c1"], - "s3" : ["c3","c4","c1","c2"], - "s4" : ["c4","c1","c2","c3"], - "s5" : ["c1","c3","c2","c4"]} #the students valuations - >>> I = (S, C, U ,V) - >>> match = { - "c1" : ["s1","s2"], - "c2" : ["s3","s5"], - "c3" : ["s4"], - "c4" : []} - >>> down = "c4" - >>> LowerFix = [] - >>> UpperFix = [] - >>> SoftFix = [] - >>> LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) - ({'c1': ['s1', 's2'], 'c2': ['s5'], 'c3' : ['s3'], 'c4' : ['s4']}, ['c2'], [], []) - """ - return 0 - - -if __name__ == "__main__": - import doctest, sys - print(doctest.testmod()) diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py b/tests/test_Demote.py similarity index 100% rename from fairpyx/algorithms/Optimization_Matching/Tests/test_Demote.py rename to tests/test_Demote.py diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_FaSt.py b/tests/test_FaSt.py similarity index 100% rename from fairpyx/algorithms/Optimization_Matching/Tests/test_FaSt.py rename to tests/test_FaSt.py diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_FaStGen.py b/tests/test_FaStGen.py similarity index 100% rename from fairpyx/algorithms/Optimization_Matching/Tests/test_FaStGen.py rename to tests/test_FaStGen.py diff --git a/fairpyx/algorithms/Optimization_Matching/Tests/test_look_ahead.py b/tests/test_look_ahead.py similarity index 100% rename from fairpyx/algorithms/Optimization_Matching/Tests/test_look_ahead.py rename to tests/test_look_ahead.py From e28cc5ff10b34f5f645ac1890aad021112626bb8 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Tue, 28 May 2024 19:17:04 +0300 Subject: [PATCH 007/111] Update FaStGen.py --- fairpyx/algorithms/Optimization_Matching/FaStGen.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index b5c50c5..70b9e1e 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -9,12 +9,15 @@ import logging logger = logging.getLogger(__name__) -def FaStGen(alloc: AllocationBuilder)->dict: +def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:dict)->dict: """ Algorithem 3-FaSt-Gen: finding a match for the general ranked valuations setting. - :param alloc: an allocation builder, which tracks the allocation and the remaining capacity for items and agents. + :param alloc: an allocation builder, which tracks the allocation and the remaining capacity for items and agents. + :param agents_valuations: a dictionary represents how agents valuates the items + :param items_valuations: a dictionary represents how items valuates the agents + >>> from fairpyx.adaptors import divide >>> S = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} >>> C = {"c1", "c2", "c3"} @@ -30,8 +33,8 @@ def FaStGen(alloc: AllocationBuilder)->dict: "s5" : ["c2","c3","c1"], "s6" :["c3","c1","c2"], "s7" : ["c1","c2","c3"]} #the students valuations - >>> instance = (S, C, U ,V) - >>> divide(FaStGen, instance=instance) + >>> instance = Instance(agents=S, items=C) + >>> divide(FaStGen, instance=instance, agents_valuations=U, items_valuations=V) {'c1' : ['s1','s2'], 'c2' : ['s3','s4','s5']. 'c3' : ['s6','s7']} """ return 0 From d0e5e7e85d7e9eab3c5a3167a4c39183a2399493 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Mon, 3 Jun 2024 13:39:30 +0300 Subject: [PATCH 008/111] start implementaion --- .../algorithms/Optimization_Matching/FaSt.py | 145 +++++++++++++++++- .../Optimization_Matching/FaStGen.py | 92 ++++++++++- 2 files changed, 230 insertions(+), 7 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 6347aee..865ed21 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -10,7 +10,9 @@ logger = logging.getLogger(__name__) -def Demote(matching:dict, student_index:int, down:int, up:int): + + +def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dict: """ Demote algorithm: Adjust the matching by moving a student to a lower-ranked college while maintaining the invariant of a complete stable matching. @@ -34,10 +36,25 @@ def Demote(matching:dict, student_index:int, down:int, up:int): >>> alloc.get_allocation() {'s1': ['c1'], 's2': ['c2'], 's3': ['c3'], 's4': ['c3'], 's5': ['c2']} """ - return 0 + # Move student to college 'down' while reducing the number of students in 'up' + # Set t to student_index + t = student_index + # Set p to 'down' + p = down_index + # While p > up + while p > up_index: + # Remove student 't' from college 'cp-1' + matching[p - 1].remove(t) + # Add student 't' to college 'cp' + matching[p].append(t) + # Decrement t and p + t -= 1 + p -= 1 -def FaSt(alloc: AllocationBuilder): + return matching #Return the matching after the change + +def FaSt(alloc: AllocationBuilder)-> dict: """ FaSt algorithm: Find a leximin optimal stable matching under ranked isometric valuations. # def FaSt(instance: Instance, explanation_logger: ExplanationLogger = ExplanationLogger()): @@ -57,12 +74,130 @@ def FaSt(alloc: AllocationBuilder): ... "s6": {"c3": 1, "c1": 2, "c2": 3}, ... "s7": {"c1": 1, "c2": 2, "c3": 3} ... } # V[i][j] is the valuation of Si for matching with Cj - >>> instance = Instance(agents=S, items=C, valuations=V) + >>> instance = Instance(agents=S, items=C, _valuations=V) >>> divide(FaSt, instance=instance) {'s1': ['c1'], 's2': ['c2'], 's3': ['c1'], 's4': ['c3'], 's5': ['c3'], 's6': ['c3'], 's7': ['c2']} """ - return 0 + S = alloc.instance.agents + C = alloc.instance.items + V = alloc.instance._valuations + # Now V look like this: + # "Alice": {"c1":2, "c2": 3, "c3": 4}, + # "Bob": {"c1": 4, "c2": 5, "c3": 6} + + # Initialize a stable matching + matching = initialize_stable_matching(S, C, V) + # Compute the initial leximin value and position array + leximin_value, pos = compute_leximin_value(matching, V) + + # Iterate to find leximin optimal stable matching + for i in range(len(S) - 1, -1, -1): + for j in range(len(C) - 1, 0, -1): + # Check if moving student i to college j improves the leximin value + if can_improve_leximin(S[i], C[j], V, leximin_value): + # If it does improve - perform the demote operation to maintain stability + Demote(matching, S[i], C[j-1], C[j]) + # Recompute the leximin value and position array after the demotion + leximin_value, pos = compute_leximin_value(matching, V) + + # Return the final stable matching + return matching + + +def can_improve_leximin(student, college, V, leximin_value)-> bool: + """ + Check if moving the student to the college improves the leximin value. + + :param student: The student being considered for reassignment + :param college: The college being considered for the student's reassignment + :param V: Valuation matrix where V[i][j] is the valuation of student i for college j + :param leximin_value: The current leximin value + :return: True if the new leximin value is an improvement, otherwise False + """ + # Get the current value of the student for the new college + current_value = V[student - 1][college - 1] # assuming students and colleges are 1-indexed + # Create a copy of the current leximin values + new_values = leximin_value[:] + # Remove the current value of the student in their current college from the leximin values + new_values.remove(current_value) + # Add the current value of the student for the new college to the leximin values + new_values.append(current_value) + # Sort the new leximin values to form the new leximin tuple + new_values.sort() + # Return True if the new leximin tuple is lexicographically greater than the current leximin tuple + return new_values > leximin_value + + +def update_leximin_value(matching, V)-> list: + # Update the leximin value after demotion + values = [] + for college, students in matching.items(): + for student in students: + student_index = student - 1 # assuming students are 1-indexed + college_index = college - 1 # assuming colleges are 1-indexed + values.append(V[student_index][college_index]) + values.sort() + return values + + +def compute_leximin_value(matching, V)-> tuple: + """ + Compute the leximin value of the current matching. + + This function calculates the leximin value of the current matching by evaluating the + valuations of students for their assigned colleges. The leximin value is the sorted + list of these valuations. It also returns the position array that tracks the positions + of the valuations in the sorted list. + + :param matching: A dictionary representing the current matching where each college is a key and the value is a list of assigned students + :param V: Valuation matrix where V[i][j] is the valuation of student i for college j + :return: A tuple (values, pos) where values is the sorted list of valuations (leximin value) and pos is the position array + """ + + values = []# Initialize an empty list to store the valuations + for college, students in matching.items():# Iterate over each college and its assigned students in the matching + for student in students:# Iterate over each student assigned to the current college + student_index = student - 1 # assuming students are 1-indexed + college_index = college - 1 # assuming colleges are 1-indexed + # Append the student's valuation for the current college to the values list + values.append(V[student_index][college_index]) + # Sort the valuations in non-decreasing order to form the leximin tuple + values.sort() + pos = [0] * len(values)# Initialize the position array to track the positions of the valuations + # Populate the position array with the index of each valuation + for idx, value in enumerate(values): + pos[idx] = idx + # Return the sorted leximin values and the position array + return values, pos + + +def initialize_stable_matching(S, C, V)-> dict: + """ + Initialize a student optimal stable matching. + This function creates an initial stable matching by assigning students to colleges based on + their preferences. The first n - m + 1 students are assigned to the highest-ranked college, + and the remaining students are assigned to the other colleges in sequence. + + :param S: List of students + :param C: List of colleges + :param V: Valuation matrix where V[i][j] is the valuation of student i for college j + :return: A dictionary representing the initial stable matching where each college is a key and the value is a list of assigned students + """ + # Get the number of students and colleges + n = len(S) + m = len(C) + # Create an empty matching dictionary where each college has an empty list of assigned students + matching = {c: [] for c in C} + + # Assign the first n - m + 1 students to the highest ranked college (C1) + for i in range(n - m + 1): + matching[C[0]].append(S[i]) + + # Assign the remaining students to the other colleges in sequence + for j in range(1, m): + matching[C[j]].append(S[n - m + j]) + return matching# Return the initialized stable matching if __name__ == "__main__": diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index 70b9e1e..b6225e4 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -6,6 +6,7 @@ """ from fairpyx import Instance, AllocationBuilder, ExplanationLogger +from FaSt import Demote import logging logger = logging.getLogger(__name__) @@ -37,10 +38,51 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d >>> divide(FaStGen, instance=instance, agents_valuations=U, items_valuations=V) {'c1' : ['s1','s2'], 'c2' : ['s3','s4','s5']. 'c3' : ['s6','s7']} """ - return 0 + S = alloc.instance.agents + C = alloc.instance.items + match = [] + UpperFix = [C[1]] + LowerFix = [C[len(C)]] + SoftFix = [] + UnFixed = [item for item in C if item not in UpperFix] + + matching_valuations_sum = { + colleague: sum(items_valuations[colleague][student] for student in students) + for colleague, students in match.items() + } + + while len(LowerFix) + len([item for item in UpperFix if item not in LowerFix]) < len(C) - 1: + up = min([j for j in C if j not in LowerFix]) + down = min(valuations_sum for valuations_sum in matching_valuations_sum.values()) + SoftFix = [pair for pair in SoftFix if not (pair[1] <= up < pair[0])] + + if (len(match[up]) == 1) or (): + LowerFix.append(up) + else: + _match = Demote(_match, up, down) + if calcluate_leximin(_match) >= calcluate_leximin(match): + match = _match + elif sourceDec(_match, match) == up: + LowerFix.append(up) + UpperFix.append(up + 1) + elif sourceDec(_match, match) in alloc.instance.agents: + t = match[sourceDec(_match, match)] + LowerFix.append(t) + UpperFix.append(t+1) + A = [j for j in UnFixed if (j > t + 1)] + SoftFix.extend((j, t+1) for j in A) + else: + match, LowerFix, UpperFix, SoftFix = LookAheadRoutine(match, down, LowerFix, UpperFix, SoftFix) + UnFixed = [ + j for j in alloc.instance.items + if (j not in UpperFix) or + any((j, _j) not in SoftFix for _j in alloc.instance.items if _j > j) + ] + return match -def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list, SoftFix:list)->(dict,list,list,list): + +def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list, SoftFix:list)->tuple: """ Algorithem 4-LookAheadRoutine: Designed to handle cases where a decrease in the leximin value may be balanced by future changes in the pairing, @@ -54,6 +96,7 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list :param LowerFix: The group of colleges whose lower limit is fixed :param UpperFix: The group of colleges whose upper limit is fixed. :param SoftFix: A set of temporary upper limits. + *We will asume that in the colleges list in index 0 there is college 1 in index 1 there is coll >>> from fairpyx.adaptors import divide @@ -83,8 +126,53 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list >>> LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) ({'c1': ['s1', 's2'], 'c2': ['s5'], 'c3' : ['s3'], 'c4' : ['s4']}, ['c2'], [], []) """ + agents, items, agents_valuations, items_valuations = I + LF = LowerFix.copy() + UF = UpperFix.copy() + _match = match.copy() + + while len(LF) + len([item for item in UF if item not in LF]) < len(items) - 1: + up = min([j for j in items if j not in LowerFix]) + if (len(match[up]) == 1) or (): + LF.append(up) + else: + _match = Demote(_match, len(agents) - 1, up, down) + if calcluate_leximin(_match) >= calcluate_leximin(match): + match = _match + LowerFix = LF + UpperFix = UF + break + elif sourceDec(_match, match) == up: + LF.append(up) + UF.append(up + 1) + elif sourceDec(_match, match) in agents: + t = match[sourceDec(_match, match)] + if t == down: + UpperFix.append(down) + else: + SoftFix.append((down, t)) + break + return (match, LowerFix, UpperFix, SoftFix) + +def calcluate_leximin(match:dict)->int: return 0 +def sourceDec(new_match:dict, old_match:dict): + """ + Determines the agent causing the leximin decrease between two matchings mu1 and mu2. + + Parameters: + - new_match: First matching (dict of colleges to students) + - old_match: Second matching (dict of colleges to students) + + Returns: + - The agent (student) causing the leximin decrease. + """ + for agent in new_match: + if new_match[agent] != old_match[agent]: + return agent + return None + if __name__ == "__main__": import doctest, sys print(doctest.testmod()) From e53880b300f33538a4c49da7ef05a9d9d1d83636 Mon Sep 17 00:00:00 2001 From: zachibs Date: Mon, 3 Jun 2024 19:34:51 +0300 Subject: [PATCH 009/111] gale shapley algorithm - signatures and tests --- ...hapley_pareto_dominant_market_mechanism.py | 39 ++++ .../__init__.py | 0 fairpyx/algorithms/__init__.py | 1 + .../test_gale_shapley.py | 179 ++++++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py create mode 100644 fairpyx/algorithms/Course_bidding_at_business_schools/__init__.py create mode 100644 tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py diff --git a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py new file mode 100644 index 0000000..090c4a9 --- /dev/null +++ b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py @@ -0,0 +1,39 @@ +""" +"Course bidding at business schools", by Tayfun Sönmez and M. Utku Ünver (2010) +https://doi.org/10.1111/j.1468-2354.2009.00572.x + +Allocate course seats using Gale-Shapley pareto-dominant market mechanism. + +Programmer: Zachi Ben Shitrit +Since: 2024-05 +""" + +from fairpyx import Instance, AllocationBuilder, ExplanationLogger + +import logging +logger = logging.getLogger(__name__) + +def gale_shapley(alloc: AllocationBuilder, course_order_per_student: dict, tie_braking_lottery: dict) -> dict: + """ + Allocate the given items to the given agents using the gale_shapley protocol. + :param alloc: an allocation builder which tracks agent_capacities, item_capacities, valuations. + :param course_order_per_student: a dictionary that matches each agent to hes course rankings in order to indicate his preferences + :param tie_braking_lottery: a dictionary that matches each agent to hes tie breaking additive points (a sample from unified distribution [0,1]) + + >>> from fairpyx.adaptors import divide + >>> s1 = {"c1": 40, "c2": 60} + >>> s2 = {"c1": 70, "c2": 30} + >>> s3 = {"c1": 70, "c2": 30} + >>> s4 = {"c1": 40, "c2": 60} + >>> s5 = {"c1": 50, "c2": 50} + >>> agent_capacities = {"Alice": 1, "Bob": 1, "Chana": 1, "Dana": 1, "Dor": 1} + >>> course_capacities = {"c1": 3, "c2": 2} + >>> valuations = {"Alice": s1, "Bob": s2, "Chana": s3, "Dana": s4, "Dor": s5} + >>> course_order_per_student = {"Alice": ["c2", "c1"], "Bob": ["c1", "c2"], "Chana": ["c1", "c2"], "Dana": ["c2", "c1"], "Dor": ["c1", "c2"]} + >>> tie_braking_lottery = {"Alice": 0.9, "Bob": 0.1, "Chana": 0.2, "Dana": 0.6, "Dor": 0.4} + >>> instance = Instance(agent_capacities=agent_capacities, item_capacities=course_capacities, valuations=valuations) + >>> divide(gale_shapley, instance=instance, course_order_per_student=course_order_per_student, tie_braking_lottery=tie_braking_lottery) + {'Alice': ['c2'], 'Bob': ['c1'], 'Chana': ['c1'], 'Dana': ['c2'], 'Dor': ['c1']} + """ + + return {} \ No newline at end of file diff --git a/fairpyx/algorithms/Course_bidding_at_business_schools/__init__.py b/fairpyx/algorithms/Course_bidding_at_business_schools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fairpyx/algorithms/__init__.py b/fairpyx/algorithms/__init__.py index 125042d..f1efbbd 100644 --- a/fairpyx/algorithms/__init__.py +++ b/fairpyx/algorithms/__init__.py @@ -2,3 +2,4 @@ from fairpyx.algorithms.iterated_maximum_matching import iterated_maximum_matching, iterated_maximum_matching_adjusted, iterated_maximum_matching_unadjusted from fairpyx.algorithms.picking_sequence import round_robin, bidirectional_round_robin, serial_dictatorship from fairpyx.algorithms.utilitarian_matching import utilitarian_matching +from fairpyx.algorithms.Course_bidding_at_business_schools.Gale_Shapley_pareto_dominant_market_mechanism import gale_shapley \ No newline at end of file diff --git a/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py b/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py new file mode 100644 index 0000000..d13cae5 --- /dev/null +++ b/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py @@ -0,0 +1,179 @@ +""" +Test Allocate course seats using Gale-Shapley pareto-dominant market mechanism. + +Programmer: Zachi Ben Shitrit +Since: 2024-05 +""" + +import pytest +import fairpyx + +def test_regular_case(): + + s1 = {"c1": 40, "c2": 60} + s2 = {"c1": 70, "c2": 30} + s3 = {"c1": 70, "c2": 30} + s4 = {"c1": 40, "c2": 60} + s5 = {"c1": 50, "c2": 50} + agent_capacities = {"Alice": 1, "Bob": 1, "Chana": 1, "Dana": 1, "Dor": 1} + course_capacities = {"c1": 3, "c2": 2} + valuations = {"Alice": s1, "Bob": s2, "Chana": s3, "Dana": s4, "Dor": s5} + course_order_per_student = {"Alice": ["c2", "c1"], "Bob": ["c1", "c2"], "Chana": ["c1", "c2"], "Dana": ["c2", "c1"], "Dor": ["c1", "c2"]} + tie_braking_lottery = {"Alice": 0.9, "Bob": 0.1, "Chana": 0.2, "Dana": 0.6, "Dor": 0.4} + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + allocation = fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + assert allocation == {'Alice': ['c2'], 'Bob': ['c1'], 'Chana': ['c1'], 'Dana': ['c2'], 'Dor': ['c1']}, "failed" + +def test_one_agent(): + s1 = {"c1": 40, "c2": 60} + agent_capacities = {"Alice": 1} + course_capacities = {"c1": 1, "c2": 1} + valuations = {"Alice": s1} + course_order_per_student = {"Alice": ["c2", "c1"]} + tie_braking_lottery = {"Alice": 0.9} + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + allocation = fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + assert allocation == {'Alice': ['c2']}, "failed" + +def test_empty_input(): + agent_capacities = {} + course_capacities = {} + valuations = {} + course_order_per_student = {} + tie_braking_lottery = {} + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + allocation = fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + assert allocation == {}, "failed" + +def test_wrong_input_type_agent_capacities(): + agent_capacities = "not a dict" + course_capacities = {"c1": 3, "c2": 2} + valuations = {"Alice": {"c1": 40, "c2": 60}} + course_order_per_student = {"Alice": ["c2", "c1"]} + tie_braking_lottery = {"Alice": 0.9} + with pytest.raises(TypeError): + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + +def test_wrong_input_type_course_capacities(): + agent_capacities = {"Alice": 1} + course_capacities = "not a dict" + valuations = {"Alice": {"c1": 40, "c2": 60}} + course_order_per_student = {"Alice": ["c2", "c1"]} + tie_braking_lottery = {"Alice": 0.9} + with pytest.raises(TypeError): + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + +def test_wrong_input_type_valuations(): + agent_capacities = {"Alice": 1} + course_capacities = {"c1": 3, "c2": 2} + valuations = "not a dict" + course_order_per_student = {"Alice": ["c2", "c1"]} + tie_braking_lottery = {"Alice": 0.9} + with pytest.raises(TypeError): + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + +def test_wrong_input_type_course_order_per_student(): + agent_capacities = {"Alice": 1} + course_capacities = {"c1": 3, "c2": 2} + valuations = {"Alice": {"c1": 40, "c2": 60}} + course_order_per_student = "not a dict" + tie_braking_lottery = {"Alice": 0.9} + with pytest.raises(TypeError): + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + +def test_wrong_input_type_tie_braking_lottery(): + agent_capacities = {"Alice": 1} + course_capacities = {"c1": 3, "c2": 2} + valuations = {"Alice": {"c1": 40, "c2": 60}} + course_order_per_student = {"Alice": ["c2", "c1"]} + tie_braking_lottery = "not a dict" + with pytest.raises(TypeError): + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + + +def test_large_input(): + num_students = 1000 + num_courses = 50 + agent_capacities = {f"Student_{i}": 1 for i in range(num_students)} + course_capacities = {f"Course_{i}": num_students // num_courses for i in range(num_courses)} + valuations = {f"Student_{i}": {f"Course_{j}": (i+j) % 100 for j in range(num_courses)} for i in range(num_students)} + course_order_per_student = {f"Student_{i}": [f"Course_{j}" for j in range(num_courses)] for i in range(num_students)} + tie_braking_lottery = {f"Student_{i}": i / num_students for i in range(num_students)} + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + allocation = fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + # Validate that the allocation is valid + assert len(allocation) == num_students, "failed" + assert all(len(courses) == 1 for courses in allocation.values()), "failed" + fairpyx.validate_allocation(instance, allocation, title=f"gale_shapley") + + +def test_edge_case_tie(): + s1 = {"c1": 50, "c2": 50} + s2 = {"c1": 50, "c2": 50} + agent_capacities = {"Alice": 1, "Bob": 1} + course_capacities = {"c1": 1, "c2": 1} + valuations = {"Alice": s1, "Bob": s2} + course_order_per_student = {"Alice": ["c1", "c2"], "Bob": ["c1", "c2"]} + tie_braking_lottery = {"Alice": 0.5, "Bob": 0.5} + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + allocation = fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + assert set(allocation.keys()) == {"Alice", "Bob"}, "failed" + assert set(allocation["Alice"] + allocation["Bob"]) == {"c1", "c2"}, "failed" + +if __name__ == "__main__": + pytest.main(["-v",__file__]) \ No newline at end of file From 69e9008cc6b2753eb31b016f3fe8807f3f3dba35 Mon Sep 17 00:00:00 2001 From: zachibs Date: Wed, 12 Jun 2024 19:51:12 +0300 Subject: [PATCH 010/111] feat: gale_shapley algorithm works --- ...hapley_pareto_dominant_market_mechanism.py | 110 +++++++++++++++++- .../test_gale_shapley.py | 16 +-- 2 files changed, 116 insertions(+), 10 deletions(-) diff --git a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py index 090c4a9..fb9db01 100644 --- a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py +++ b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py @@ -9,11 +9,59 @@ """ from fairpyx import Instance, AllocationBuilder, ExplanationLogger +import fairpyx +import numpy as np +from typing import Dict, List import logging logger = logging.getLogger(__name__) -def gale_shapley(alloc: AllocationBuilder, course_order_per_student: dict, tie_braking_lottery: dict) -> dict: +def sort_and_tie_brake(input_dict: Dict[str, float], tie_braking_lottery: Dict[str, float], course_capacity: int) -> List[tuple[str, float]]: + """ + Sorts a dictionary by its values in descending order and adds a number + to the values of keys with the same value. + stops if the count passed the course's capacity and its not on a tie. + + Parameters: + input_dict (Dict[str, float]): A dictionary with string keys and float values. + tie_braking_lottery (Dict[str, float]): A dictionary with string keys and float values. + course_capacity (int): The number of students allowed at the course + + Returns: + dict: A new dictionary with sorted and modified values. + """ + + # Sort the dictionary by values in descending order + sorted_dict = dict(sorted(input_dict.items(), key=lambda item: item[1], reverse=True)) + + # Initialize previous value to track duplicate values + previous_value = None + prev_key = "" + + # Initialize a variable to track count + count: int = 0 + + # Iterate over the sorted dictionary and modify values + for key in sorted_dict: + current_value = sorted_dict[key] + + if current_value == previous_value: + # If current value is the same as previous, add the number to both current and previous values + sorted_dict[key] += tie_braking_lottery[key] + sorted_dict[prev_key] += tie_braking_lottery[prev_key] + elif count >= course_capacity: + break + + # Update previous_value and prev_key to current_value and key for next iteration + previous_value = sorted_dict[key] + prev_key = key + + sorted_dict = (sorted(sorted_dict.items(), key=lambda item: item[1], reverse=True)) + + return sorted_dict + + +def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, List[str]], tie_braking_lottery: Dict[str, float]): """ Allocate the given items to the given agents using the gale_shapley protocol. :param alloc: an allocation builder which tracks agent_capacities, item_capacities, valuations. @@ -36,4 +84,62 @@ def gale_shapley(alloc: AllocationBuilder, course_order_per_student: dict, tie_b {'Alice': ['c2'], 'Bob': ['c1'], 'Chana': ['c1'], 'Dana': ['c2'], 'Dor': ['c1']} """ - return {} \ No newline at end of file + if(not tie_braking_lottery): + tie_braking_lottery = {agent: float(np.random.uniform(0.001, 1, 1)) for agent in alloc.remaining_agents()} + + input_to_check_types = [alloc.remaining_agent_capacities, alloc.remaining_item_capacities, course_order_per_student, tie_braking_lottery] + for input_to_check in input_to_check_types: + if(type(input_to_check) != dict): + raise TypeError; + + if(alloc.remaining_agent_capacities == {} or alloc.remaining_item_capacities == {}): + return {} + was_an_offer_declined: bool = True + course_to_on_hold_students: Dict[str, Dict[str, float]] = {course: {} for course in alloc.remaining_items()} + student_to_rejection_count: Dict[str, int] = {student: alloc.remaining_agent_capacities[student] for student in alloc.remaining_agents()} + + while(was_an_offer_declined): + logger.info("Each student proposes to his top qI courses based on his stated preferences.") + for student in alloc.remaining_agents(): + student_capability: int = student_to_rejection_count[student] + for index in range(student_capability): + wanted_course = course_order_per_student[student].pop(index) + if(wanted_course in course_to_on_hold_students): + if(student in course_to_on_hold_students[wanted_course]): + continue + try: + student_to_course_proposal = alloc.effective_value(student, wanted_course) + course_to_on_hold_students[wanted_course][student] = student_to_course_proposal + except Exception as e: + return {} + + logger.info("Each course c rejects all but the highest bidding qc students among those who have proposed") + student_to_rejection_count = {student: 0 for student in alloc.remaining_agents()} + for course_name in course_to_on_hold_students: + course_capacity = alloc.remaining_item_capacities[course_name] + course_to_offerings = course_to_on_hold_students[course_name] + if(len(course_to_offerings) == 0): + continue + elif(len(course_to_offerings) <= course_capacity): + was_an_offer_declined = False + continue + on_hold_students_sorted_and_tie_braked = sort_and_tie_brake(course_to_offerings, tie_braking_lottery, course_capacity) + course_to_on_hold_students[course_name].clear() + for key, value in on_hold_students_sorted_and_tie_braked[:course_capacity]: + course_to_on_hold_students[course_name][key] = value + + rejected_students = on_hold_students_sorted_and_tie_braked[course_capacity:] + for rejected_student, bid in rejected_students: + student_to_rejection_count[rejected_student] += 1 + was_an_offer_declined = True + + logger.info("The procedure terminates when no proposal is rejected, and at this stage course assignments are finalized.") + final_course_matchings = course_to_on_hold_students.items() + for course_name, matching in final_course_matchings: + for student, bid in matching.items(): + alloc.give(student, course_name, logger) + + +if __name__ == "__main__": + import doctest + print(doctest.testmod()) \ No newline at end of file diff --git a/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py b/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py index d13cae5..a255fb2 100644 --- a/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py +++ b/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py @@ -51,14 +51,14 @@ def test_empty_input(): valuations = {} course_order_per_student = {} tie_braking_lottery = {} - instance = fairpyx.Instance(agent_capacities=agent_capacities, - item_capacities=course_capacities, - valuations=valuations) - allocation = fairpyx.divide(fairpyx.algorithms.gale_shapley, - instance=instance, - course_order_per_student=course_order_per_student, - tie_braking_lottery=tie_braking_lottery) - assert allocation == {}, "failed" + with pytest.raises(StopIteration): + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + allocation = fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) def test_wrong_input_type_agent_capacities(): agent_capacities = "not a dict" From c197ef1548c8af451f989d7bc8263bc94d05a508 Mon Sep 17 00:00:00 2001 From: zachibs Date: Wed, 12 Jun 2024 20:11:34 +0300 Subject: [PATCH 011/111] refactor: in gale_shapley algorithm --- ...hapley_pareto_dominant_market_mechanism.py | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py index fb9db01..b1d7e5c 100644 --- a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py +++ b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py @@ -8,8 +8,7 @@ Since: 2024-05 """ -from fairpyx import Instance, AllocationBuilder, ExplanationLogger -import fairpyx +from fairpyx import AllocationBuilder import numpy as np from typing import Dict, List @@ -18,19 +17,18 @@ def sort_and_tie_brake(input_dict: Dict[str, float], tie_braking_lottery: Dict[str, float], course_capacity: int) -> List[tuple[str, float]]: """ - Sorts a dictionary by its values in descending order and adds a number - to the values of keys with the same value. - stops if the count passed the course's capacity and its not on a tie. + Sorts a dictionary by its values in descending order and adds a number + to the values of keys with the same value to break ties. + Stops if the count surpasses the course's capacity and is not in a tie. Parameters: - input_dict (Dict[str, float]): A dictionary with string keys and float values. - tie_braking_lottery (Dict[str, float]): A dictionary with string keys and float values. - course_capacity (int): The number of students allowed at the course + input_dict (Dict[str, float]): A dictionary with string keys and float values representing student bids. + tie_braking_lottery (Dict[str, float]): A dictionary with string keys and float values for tie-breaking. + course_capacity (int): The number of students allowed in the course. Returns: - dict: A new dictionary with sorted and modified values. + List[tuple[str, float]]: A list of tuples containing student names and their modified bids, sorted in descending order. """ - # Sort the dictionary by values in descending order sorted_dict = dict(sorted(input_dict.items(), key=lambda item: item[1], reverse=True)) @@ -56,6 +54,7 @@ def sort_and_tie_brake(input_dict: Dict[str, float], tie_braking_lottery: Dict[s previous_value = sorted_dict[key] prev_key = key + # Sort again after tie-breaking sorted_dict = (sorted(sorted_dict.items(), key=lambda item: item[1], reverse=True)) return sorted_dict @@ -63,11 +62,17 @@ def sort_and_tie_brake(input_dict: Dict[str, float], tie_braking_lottery: Dict[s def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, List[str]], tie_braking_lottery: Dict[str, float]): """ - Allocate the given items to the given agents using the gale_shapley protocol. - :param alloc: an allocation builder which tracks agent_capacities, item_capacities, valuations. - :param course_order_per_student: a dictionary that matches each agent to hes course rankings in order to indicate his preferences - :param tie_braking_lottery: a dictionary that matches each agent to hes tie breaking additive points (a sample from unified distribution [0,1]) + Allocate the given items to the given agents using the Gale-Shapley protocol. + + Parameters: + alloc (AllocationBuilder): An allocation builder which tracks agent capacities, item capacities, and valuations. + course_order_per_student (Dict[str, List[str]]): A dictionary that matches each agent to their course rankings indicating preferences. + tie_braking_lottery (Dict[str, float]): A dictionary that matches each agent to their tie-breaking additive points (sampled from a uniform distribution [0,1]). + + Returns: + Dict[str, List[str]]: A dictionary representing the final allocation of courses to students. + Example: >>> from fairpyx.adaptors import divide >>> s1 = {"c1": 40, "c2": 60} >>> s2 = {"c1": 70, "c2": 30} @@ -84,9 +89,7 @@ def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, L {'Alice': ['c2'], 'Bob': ['c1'], 'Chana': ['c1'], 'Dana': ['c2'], 'Dor': ['c1']} """ - if(not tie_braking_lottery): - tie_braking_lottery = {agent: float(np.random.uniform(0.001, 1, 1)) for agent in alloc.remaining_agents()} - + # Check if inputs are dictionaries input_to_check_types = [alloc.remaining_agent_capacities, alloc.remaining_item_capacities, course_order_per_student, tie_braking_lottery] for input_to_check in input_to_check_types: if(type(input_to_check) != dict): @@ -99,7 +102,7 @@ def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, L student_to_rejection_count: Dict[str, int] = {student: alloc.remaining_agent_capacities[student] for student in alloc.remaining_agents()} while(was_an_offer_declined): - logger.info("Each student proposes to his top qI courses based on his stated preferences.") + logger.info("Each student who is rejected from k > 0 courses in the previous step proposes to his best remaining k courses based on his stated preferences") for student in alloc.remaining_agents(): student_capability: int = student_to_rejection_count[student] for index in range(student_capability): @@ -113,16 +116,17 @@ def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, L except Exception as e: return {} - logger.info("Each course c rejects all but the highest bidding qc students among those who have proposed") + logger.info("Each course c considers the new proposals together with the proposals on hold and rejects all but the highest bidding Qc (the maximum capacity of students in course c) students") student_to_rejection_count = {student: 0 for student in alloc.remaining_agents()} for course_name in course_to_on_hold_students: course_capacity = alloc.remaining_item_capacities[course_name] course_to_offerings = course_to_on_hold_students[course_name] - if(len(course_to_offerings) == 0): + if len(course_to_offerings) == 0: continue - elif(len(course_to_offerings) <= course_capacity): + elif len(course_to_offerings) <= course_capacity: was_an_offer_declined = False continue + logger.info("In case there is a tie, the tie-breaking lottery is used to determine who is rejected and who will be kept on hold.") on_hold_students_sorted_and_tie_braked = sort_and_tie_brake(course_to_offerings, tie_braking_lottery, course_capacity) course_to_on_hold_students[course_name].clear() for key, value in on_hold_students_sorted_and_tie_braked[:course_capacity]: @@ -138,8 +142,7 @@ def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, L for course_name, matching in final_course_matchings: for student, bid in matching.items(): alloc.give(student, course_name, logger) - - + if __name__ == "__main__": import doctest - print(doctest.testmod()) \ No newline at end of file + print(doctest.testmod()) From b6fa162f738c83a0c36fbb8728db681d4b6c988d Mon Sep 17 00:00:00 2001 From: zachibs Date: Wed, 12 Jun 2024 20:23:06 +0300 Subject: [PATCH 012/111] refactor: fixed imports in doctests --- .../Gale_Shapley_pareto_dominant_market_mechanism.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py index b1d7e5c..ef57033 100644 --- a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py +++ b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py @@ -73,6 +73,7 @@ def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, L Dict[str, List[str]]: A dictionary representing the final allocation of courses to students. Example: + >>> from fairpyx import Instance, AllocationBuilder >>> from fairpyx.adaptors import divide >>> s1 = {"c1": 40, "c2": 60} >>> s2 = {"c1": 70, "c2": 30} From ea325677448662d180ddac11bc251945fb067bc1 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Thu, 20 Jun 2024 23:21:33 +0300 Subject: [PATCH 013/111] fixing FaStGen and LookAheadRoutine fixing FaStGen and LookAheadRoutine according to the author's insights. --- .../Optimization_Matching/FaStGen.py | 153 ++++++++++++------ tests/test_FaStGen.py | 70 ++++---- tests/test_look_ahead.py | 62 +++---- 3 files changed, 170 insertions(+), 115 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index b6225e4..bf8e470 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -8,6 +8,7 @@ from fairpyx import Instance, AllocationBuilder, ExplanationLogger from FaSt import Demote import logging +import sys logger = logging.getLogger(__name__) def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:dict)->dict: @@ -20,23 +21,26 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d :param items_valuations: a dictionary represents how items valuates the agents >>> from fairpyx.adaptors import divide - >>> S = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} - >>> C = {"c1", "c2", "c3"} - >>> V = { - "c1" : ["s1","s2","s3","s4","s5","s6","s7"], - "c2" : ["s2","s4","s1","s3","s5","s6","s7"], - "c3" : [s3,s5,s6,s1,s2,s4,s7]} #the colleges valuations - >>> U = { - "s1" : ["c1","c3","c2"], - "s2" : ["c2","c1","c3"], - "s3" : ["c1","c3","c2"], - "s4" : ["c3","c2","c1"], - "s5" : ["c2","c3","c1"], - "s6" :["c3","c1","c2"], - "s7" : ["c1","c2","c3"]} #the students valuations + >>> S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + >>> C = ["c1", "c2", "c3", "c4"] + >>> V = { #the colleges valuations + "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + } + >>> U = { #the students valuations + "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, + "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, + "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, + "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, + "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, + "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, + "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} + } >>> instance = Instance(agents=S, items=C) >>> divide(FaStGen, instance=instance, agents_valuations=U, items_valuations=V) - {'c1' : ['s1','s2'], 'c2' : ['s3','s4','s5']. 'c3' : ['s6','s7']} + {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} """ S = alloc.instance.agents C = alloc.instance.items @@ -46,21 +50,25 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d SoftFix = [] UnFixed = [item for item in C if item not in UpperFix] - matching_valuations_sum = { + matching_valuations_sum = { #in the artical it looks like this: vj(mu) colleague: sum(items_valuations[colleague][student] for student in students) for colleague, students in match.items() } - while len(LowerFix) + len([item for item in UpperFix if item not in LowerFix]) < len(C) - 1: + while len(LowerFix) + len([item for item in UpperFix if item not in LowerFix]) < len(C): up = min([j for j in C if j not in LowerFix]) - down = min(valuations_sum for valuations_sum in matching_valuations_sum.values()) + down = min(valuations_sum for key, valuations_sum in matching_valuations_sum.items() if key in UnFixed) SoftFix = [pair for pair in SoftFix if not (pair[1] <= up < pair[0])] - if (len(match[up]) == 1) or (): + if (len(match[up]) == 1) or (matching_valuations_sum[up] <= matching_valuations_sum[down]): LowerFix.append(up) else: - _match = Demote(_match, up, down) - if calcluate_leximin(_match) >= calcluate_leximin(match): + #check the lowest-rank student who currently belongs to mu(c_{down-1}) + agant_to_demote = get_lowest_ranked_student(down-1, match, items_valuations) + _match = Demote(_match, agant_to_demote, up, down) + _match_leximin_tuple = create_leximin_tuple(match=_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + match_leximin_tuple = create_leximin_tuple(match=match, agents_valuations=agents_valuations, items_valuations=items_valuations) + if compare_leximin(match_leximin_tuple, _match_leximin_tuple): match = _match elif sourceDec(_match, match) == up: LowerFix.append(up) @@ -72,7 +80,7 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d A = [j for j in UnFixed if (j > t + 1)] SoftFix.extend((j, t+1) for j in A) else: - match, LowerFix, UpperFix, SoftFix = LookAheadRoutine(match, down, LowerFix, UpperFix, SoftFix) + match, LowerFix, UpperFix, SoftFix = LookAheadRoutine((S, C, agents_valuations, items_valuations), match, down, LowerFix, UpperFix, SoftFix) UnFixed = [ j for j in alloc.instance.items if (j not in UpperFix) or @@ -81,7 +89,6 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d return match - def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list, SoftFix:list)->tuple: """ Algorithem 4-LookAheadRoutine: Designed to handle cases where a decrease in the leximin value @@ -100,19 +107,19 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list >>> from fairpyx.adaptors import divide - >>> S = {"s1", "s2", "s3", "s4", "s5"} - >>> C = {"c1", "c2", "c3", "c4"} - >>> V = { - "c1" : ["s1","s2","s3","s4","s5"], - "c2" : ["s2","s1","s3","s4","s5"], - "c3" : ["s3","s2","s1","s4","s5"], - "c4" : ["s4","s3","s2","s1","s5"]} #the colleges valuations - >>> U = { - "s1" : ["c1","c3","c2","c4"], - "s2" : ["c2","c3","c4","c1"], - "s3" : ["c3","c4","c1","c2"], - "s4" : ["c4","c1","c2","c3"], - "s5" : ["c1","c3","c2","c4"]} #the students valuations + >>> S = ["s1", "s2", "s3", "s4", "s5"] + >>> C = ["c1", "c2", "c3", "c4"] + >>> V = { #the colleges valuations + "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10}, + "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26}, + "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28}, + "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15}} + >>> U = { #the students valuations + "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, + "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, + "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, + "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, + "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}} >>> I = (S, C, U ,V) >>> match = { "c1" : ["s1","s2"], @@ -124,20 +131,30 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list >>> UpperFix = [] >>> SoftFix = [] >>> LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) - ({'c1': ['s1', 's2'], 'c2': ['s5'], 'c3' : ['s3'], 'c4' : ['s4']}, ['c2'], [], []) + ({"c1": ["s1", "s2"], "c2": ["s5"], "c3" : ["s3"], "c4" : ["s4"]}, ["c1"], [], []) """ agents, items, agents_valuations, items_valuations = I LF = LowerFix.copy() UF = UpperFix.copy() _match = match.copy() + matching_valuations_sum = { #in the artical it looks like this: vj(mu) + colleague: sum(items_valuations[colleague][student] for student in students) + for colleague, students in match.items() + } + + while len(LF) + len([item for item in UF if item not in LF]) < len(items) - 1: up = min([j for j in items if j not in LowerFix]) - if (len(match[up]) == 1) or (): + if (len(match[up]) == 1) or (matching_valuations_sum[up] <= matching_valuations_sum[down]): LF.append(up) else: - _match = Demote(_match, len(agents) - 1, up, down) - if calcluate_leximin(_match) >= calcluate_leximin(match): + #check the lowest-rank student who currently belongs to mu(c_{down-1}) + agant_to_demote = get_lowest_ranked_student(down-1, match) + _match = Demote(_match, agant_to_demote, up, down) + _match_leximin_tuple = create_leximin_tuple(match=_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + match_leximin_tuple = create_leximin_tuple(match=match, agents_valuations=agents_valuations, items_valuations=items_valuations) + if compare_leximin(match_leximin_tuple, _match_leximin_tuple): match = _match LowerFix = LF UpperFix = UF @@ -146,7 +163,7 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list LF.append(up) UF.append(up + 1) elif sourceDec(_match, match) in agents: - t = match[sourceDec(_match, match)] + t = _match[sourceDec(_match, match)] if t == down: UpperFix.append(down) else: @@ -154,24 +171,60 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list break return (match, LowerFix, UpperFix, SoftFix) -def calcluate_leximin(match:dict)->int: - return 0 +def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:dict): + leximin_tuple = [] + for item in match.keys(): + for agent in match[item]: + leximin_tuple.append((agent,items_valuations[item][agent])) + leximin_tuple.append((item, agents_valuations[agent][item])) + leximin_tuple.sort(key = lambda x: x[1]) + return leximin_tuple -def sourceDec(new_match:dict, old_match:dict): +def compare_leximin(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->bool: + """ + Determain whethere the leximin tuple of the new match is grater or equal to the leximin tuple of the old match + + Parameters: + - new_match_leximin_tuple: the leximin tuple of the new matching + - old_match_leximin_tuple: the leximin tuple of the old matching + + Return: + - true or false if new_match_leximin_tuple >= old_match_leximin_tuple + """ + for k in range(0, len(new_match_leximin_tuple)): + if new_match_leximin_tuple[k][1] == old_match_leximin_tuple[k][1]: + continue + elif new_match_leximin_tuple[k][1] > old_match_leximin_tuple[k][1]: + return True + else: + return False + +def sourceDec(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->str: """ Determines the agent causing the leximin decrease between two matchings mu1 and mu2. Parameters: - - new_match: First matching (dict of colleges to students) - - old_match: Second matching (dict of colleges to students) + - new_match_leximin_tuple: the leximin tuple of the new matching + - old_match_leximin_tuple: the leximin tuple of the old matching Returns: - The agent (student) causing the leximin decrease. """ - for agent in new_match: - if new_match[agent] != old_match[agent]: - return agent - return None + for k in range(0, len(new_match_leximin_tuple)): + if new_match_leximin_tuple[k][1] < old_match_leximin_tuple[k][1]: + return old_match_leximin_tuple[k][0] + return "" + +def get_lowest_ranked_student(item:int, match:dict, items_valuations:dict)->str: + # min = sys.maxsize + # lowest_ranked_student = 0 + # for agant in match[item]: + # minTemp = items_valuations[item][agant] + # if minTemp < min: + # min = minTemp + # lowest_ranked_student = agant + # return lowest_ranked_student + return min(match[item], key=lambda agant: items_valuations[item][agant]) if __name__ == "__main__": import doctest, sys diff --git a/tests/test_FaStGen.py b/tests/test_FaStGen.py index 5f98030..36e370b 100644 --- a/tests/test_FaStGen.py +++ b/tests/test_FaStGen.py @@ -8,7 +8,7 @@ import pytest from fairpyx import Instance -import FaStGen +from fairpyx.algorithms.Optimization_Matching.FaStGen import FaStGen def test_FaStGen_basic_case(): """ @@ -16,30 +16,32 @@ def test_FaStGen_basic_case(): """ # Define the instance S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] - C = ["c1", "c2", "c3"] - V = { - "c1": ["s1", "s2", "s3", "s4", "s5", "s6", "s7"], - "c2": ["s2", "s4", "s1", "s3", "s5", "s6", "s7"], - "c3": ["s3", "s5", "s6", "s1", "s2", "s4", "s7"] - } - U = { - "s1": ["c1", "c3", "c2"], - "s2": ["c2", "c1", "c3"], - "s3": ["c1", "c3", "c2"], - "s4": ["c3", "c2", "c1"], - "s5": ["c2", "c3", "c1"], - "s6": ["c3", "c1", "c2"], - "s7": ["c1", "c2", "c3"] - } + C = ["c1", "c2", "c3", "c4"] + V = { #the colleges valuations + "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + } + U = { #the students valuations + "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, + "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, + "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, + "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, + "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, + "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, + "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} + } + # Assuming `Instance` can handle student and course preferences directly - instance = Instance(S, C, U, V) + instance = Instance(agents=S, items=C) # Run the FaStGen algorithm - allocation = FaStGen(instance) + allocation = FaStGen(instance, agents_valuations=U, items_valuations=V) # Define the expected allocation (this is hypothetical; you should set it based on the actual expected output) - expected_allocation = {'s1': 'c1', 's2': 'c2', 's3': 'c3', 's4': 'c1', 's5': 'c2', 's6': 'c3', 's7': 'c1'} + expected_allocation = {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} # Assert the result assert allocation == expected_allocation, "FaStGen algorithm basic case failed" @@ -49,18 +51,18 @@ def test_FaStGen_edge_cases(): Test edge cases for the FaStGen algorithm. """ # Edge case 1: Empty input - instance_empty = Instance([], [], {}, {}) - allocation_empty = FaStGen(instance_empty) + instance_empty = Instance([], []) + allocation_empty = FaStGen(instance_empty, {}, {}) assert allocation_empty == {}, "FaStGen algorithm failed on empty input" # Edge case 2: Single student and single course S_single = ["s1"] C_single = ["c1"] - U_single = {"s1": ["c1"]} - V_single = {"c1": ["s1"]} - instance_single = Instance(S_single, C_single, U_single, V_single) - allocation_single = FaStGen(instance_single) - assert allocation_single == {"s1": "c1"}, "FaStGen algorithm failed on single student and single course" + U_single = {"s1": {"c1":100}} + V_single = {"c1": {"s1":50}} + instance_single = Instance(S_single, C_single) + allocation_single = FaStGen(instance_single, U_single, V_single) + assert allocation_single == {"c1": ["s1"]}, "FaStGen algorithm failed on single student and single course" def test_FaStGen_large_input(): """ @@ -68,25 +70,25 @@ def test_FaStGen_large_input(): """ # Define the instance with a large number of students and courses num_students = 100 - num_courses = 50 + num_colleges = 50 students = [f"s{i}" for i in range(1, num_students + 1)] - courses = [f"c{i}" for i in range(1, num_courses + 1)] - valuations = {course: students for course in courses} - preferences = {student: courses for student in students} # Assuming all students prefer all courses equally + colleges = [f"c{i}" for i in range(1, num_colleges + 1)] + colleges_valuations = {college: students for college in colleges} + students_valuations = {student: college for student in students} # Assuming all students prefer all courses equally - instance_large = Instance(students, courses, preferences, valuations) + instance_large = Instance(students, colleges) # Run the FaStGen algorithm - allocation_large = FaStGen(instance_large) + allocation_large = FaStGen(instance_large, agents_valuations=students_valuations, items_valuations=colleges_valuations) # Add assertions # Ensure that all students are assigned to a course assert len(allocation_large) == len(students), "Not all students are assigned to a course" # Ensure that each course has students assigned to it - for student, course in allocation_large.items(): + for student, college in allocation_large.items(): assert student in students, f"Unexpected student {student} in allocation" - assert course in courses, f"Unexpected course {course} in allocation" + assert college in colleges, f"Unexpected course {college} in allocation" if __name__ == "__main__": pytest.main(["-v", __file__]) diff --git a/tests/test_look_ahead.py b/tests/test_look_ahead.py index 5549cd7..81da191 100644 --- a/tests/test_look_ahead.py +++ b/tests/test_look_ahead.py @@ -8,47 +8,47 @@ import pytest from fairpyx import Instance -import LookAheadRoutine +from fairpyx.algorithms.Optimization_Matching.FaStGen import LookAheadRoutine def test_look_ahead_routine_basic_case(): """ Basic test case for the Look Ahead Routine algorithm. """ # Define the instance - S = {"s1", "s2", "s3", "s4", "s5"} - C = {"c1", "c2", "c3", "c4"} - U = { - "s1": ["c1", "c3", "c2", "c4"], - "s2": ["c2", "c3", "c4", "c1"], - "s3": ["c3", "c4", "c1", "c2"], - "s4": ["c4", "c1", "c2", "c3"], - "s5": ["c1", "c3", "c2", "c4"] - } - V = { - "c1": ["s1", "s2", "s3", "s4", "s5"], - "c2": ["s2", "s1", "s3", "s4", "s5"], - "c3": ["s3", "s2", "s1", "s4", "s5"], - "c4": ["s4", "s3", "s2", "s1", "s5"] - } - - I = (S, C, U, V) + S = ["s1", "s2", "s3", "s4", "s5"] + C = ["c1", "c2", "c3", "c4"] + V = { #the colleges valuations + "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10}, + "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26}, + "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28}, + "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15} + } + U = { #the students valuations + "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, + "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, + "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, + "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, + "s5" : {"c1":36,"c2":19,"c3":9,"c4":6} + } + I = (S, C, U ,V) match = { - "c1": ["s1", "s2"], - "c2": ["s3", "s5"], - "c3": ["s4"], - "c4": [] + "c1" : ["s1","s2"], + "c2" : ["s3","s5"], + "c3" : ["s4"], + "c4" : [] } down = "c4" LowerFix = [] UpperFix = [] SoftFix = [] + # Run the Look Ahead Routine algorithm new_match, new_LowerFix, new_UpperFix, new_SoftFix = LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) # Define the expected output - expected_new_match = {'c1': ['s1', 's2'], 'c2': ['s5'], 'c3': ['s3'], 'c4': ['s4']} - expected_new_LowerFix = ['c2'] + expected_new_match = {"c1": ["s1", "s2"], "c2": ["s5"], "c3" : ["s3"], "c4" : ["s4"]} + expected_new_LowerFix = ['c1'] expected_new_UpperFix = [] expected_new_SoftFix = [] @@ -77,7 +77,7 @@ def test_look_ahead_routine_edge_cases(): assert new_SoftFix_empty == [], "Look Ahead Routine algorithm failed on empty input (SoftFix)" # Edge case 2: Single student and single course - I_single = ({"s1"}, {"c1"}, {"s1": ["c1"]}, {"c1": ["s1"]}) + I_single = ({"s1"}, {"c1"}, {"s1": {"c1":100}}, {"c1": {"s1":80}}) match_single = {"c1": ["s1"]} down_single = "c1" LowerFix_single = [] @@ -96,14 +96,14 @@ def test_look_ahead_routine_large_input(): """ # Define the instance with a large number of students and courses num_students = 100 - num_courses = 50 + num_college = 50 students = {f"s{i}" for i in range(1, num_students + 1)} - courses = {f"c{i}" for i in range(1, num_courses + 1)} - U = {student: list(courses) for student in students} - V = {course: list(students) for course in courses} + colleges = {f"c{i}" for i in range(1, num_college + 1)} + U = {student: list(colleges) for student in students} + V = {course: list(students) for course in colleges} - I_large = (students, courses, U, V) - match_large = {course: [] for course in courses} + I_large = (students, colleges, U, V) + match_large = {course: [] for course in colleges} down_large = "c1" LowerFix_large = [] UpperFix_large = [] From faca0602c5c7be2abe43a3cbd6dde5238dd7a28a Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Fri, 21 Jun 2024 16:41:06 +0300 Subject: [PATCH 014/111] Update FaStGen.py --- .../Optimization_Matching/FaStGen.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index bf8e470..7c6e7df 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -50,10 +50,7 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d SoftFix = [] UnFixed = [item for item in C if item not in UpperFix] - matching_valuations_sum = { #in the artical it looks like this: vj(mu) - colleague: sum(items_valuations[colleague][student] for student in students) - for colleague, students in match.items() - } + matching_valuations_sum = update_matching_valuations_sum(match=match,items_valuations=items_valuations, agents=S, items=C) while len(LowerFix) + len([item for item in UpperFix if item not in LowerFix]) < len(C): up = min([j for j in C if j not in LowerFix]) @@ -69,7 +66,8 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d _match_leximin_tuple = create_leximin_tuple(match=_match, agents_valuations=agents_valuations, items_valuations=items_valuations) match_leximin_tuple = create_leximin_tuple(match=match, agents_valuations=agents_valuations, items_valuations=items_valuations) if compare_leximin(match_leximin_tuple, _match_leximin_tuple): - match = _match + match = _match + matching_valuations_sum = update_matching_valuations_sum(match=match,items_valuations=items_valuations, agents=S, items=C) elif sourceDec(_match, match) == up: LowerFix.append(up) UpperFix.append(up + 1) @@ -138,11 +136,7 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list UF = UpperFix.copy() _match = match.copy() - matching_valuations_sum = { #in the artical it looks like this: vj(mu) - colleague: sum(items_valuations[colleague][student] for student in students) - for colleague, students in match.items() - } - + matching_valuations_sum = update_matching_valuations_sum(match=_match,items_valuations=items_valuations, agents=agents, items=items) while len(LF) + len([item for item in UF if item not in LF]) < len(items) - 1: up = min([j for j in items if j not in LowerFix]) @@ -152,6 +146,7 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list #check the lowest-rank student who currently belongs to mu(c_{down-1}) agant_to_demote = get_lowest_ranked_student(down-1, match) _match = Demote(_match, agant_to_demote, up, down) + matching_valuations_sum = update_matching_valuations_sum(match=_match,items_valuations=items_valuations, agents=agents, items=items) _match_leximin_tuple = create_leximin_tuple(match=_match, agents_valuations=agents_valuations, items_valuations=items_valuations) match_leximin_tuple = create_leximin_tuple(match=match, agents_valuations=agents_valuations, items_valuations=items_valuations) if compare_leximin(match_leximin_tuple, _match_leximin_tuple): @@ -212,7 +207,7 @@ def sourceDec(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->str: """ for k in range(0, len(new_match_leximin_tuple)): if new_match_leximin_tuple[k][1] < old_match_leximin_tuple[k][1]: - return old_match_leximin_tuple[k][0] + return new_match_leximin_tuple[k][0] return "" def get_lowest_ranked_student(item:int, match:dict, items_valuations:dict)->str: @@ -226,6 +221,13 @@ def get_lowest_ranked_student(item:int, match:dict, items_valuations:dict)->str: # return lowest_ranked_student return min(match[item], key=lambda agant: items_valuations[item][agant]) +def update_matching_valuations_sum(match:dict, items_valuations:dict, agents:list, items:list)->dict: + matching_valuations_sum = { #in the artical it looks like this: vj(mu) + colleague: sum(items_valuations[colleague][student] for student in students) + for colleague, students in match.items() + } + return matching_valuations_sum + if __name__ == "__main__": import doctest, sys print(doctest.testmod()) From a1d72b156cb836084b18dd5b988a2af793f6c6f4 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Mon, 24 Jun 2024 13:16:46 +0300 Subject: [PATCH 015/111] Update FaStGen.py --- .../Optimization_Matching/FaStGen.py | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index 7c6e7df..e910668 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -7,8 +7,9 @@ from fairpyx import Instance, AllocationBuilder, ExplanationLogger from FaSt import Demote +#import sys + import logging -import sys logger = logging.getLogger(__name__) def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:dict)->dict: @@ -229,5 +230,34 @@ def update_matching_valuations_sum(match:dict, items_valuations:dict, agents:lis return matching_valuations_sum if __name__ == "__main__": - import doctest, sys - print(doctest.testmod()) + # import doctest, sys + # print(doctest.testmod()) + # Define the instance + S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + C = ["c1", "c2", "c3", "c4"] + V = { #the colleges valuations + "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + } + U = { #the students valuations + "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, + "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, + "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, + "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, + "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, + "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, + "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} + } + + + # Assuming `Instance` can handle student and course preferences directly + instance = Instance(agents=S, items=C) + + # Run the FaStGen algorithm + allocation = FaStGen(instance, agents_valuations=U, items_valuations=V) + print(allocation) + # Define the expected allocation (this is hypothetical; you should set it based on the actual expected output) + expected_allocation = {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} + From d75a0ff482d169f2f18e6af10618a0886afb4329 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Wed, 26 Jun 2024 10:54:40 +0300 Subject: [PATCH 016/111] updating doctest for all functions in FaStGen --- .../Optimization_Matching/FaStGen.py | 213 ++++++++++++++---- 1 file changed, 170 insertions(+), 43 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index e910668..c935472 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -45,12 +45,13 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d """ S = alloc.instance.agents C = alloc.instance.items - match = [] + match = create_stable_matching(len(S), len(C)) UpperFix = [C[1]] LowerFix = [C[len(C)]] SoftFix = [] UnFixed = [item for item in C if item not in UpperFix] + #creating a dictionary of vj(µ) = Pi∈µ(cj) for each j in C matching_valuations_sum = update_matching_valuations_sum(match=match,items_valuations=items_valuations, agents=S, items=C) while len(LowerFix) + len([item for item in UpperFix if item not in LowerFix]) < len(C): @@ -168,6 +169,37 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list return (match, LowerFix, UpperFix, SoftFix) def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:dict): + """ + Create a leximin tuple from the given match, agents' valuations, and items' valuations. + + Args: + - match (dict): A dictionary where keys are items and values are lists of agents. + - agents_valuations (dict): A dictionary where keys are agents and values are dictionaries of item valuations. + - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. + + Returns: + - list: A sorted list of tuples representing the leximin tuple. + + Example: + >>> match = {"c1":["s1","s2","s3"], "c2":["s4"], "c3":["s5"], "c4":["s7","s6"]} + >>> items_valuations = { #the colleges valuations + "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + } + >>> agents_valuations = { #the students valuations + "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, + "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, + "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, + "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, + "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, + "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, + "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} + } + >>> create_leximin_tuple(match, agents_valuations, items_valuations) + [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] + """ leximin_tuple = [] for item in match.keys(): for agent in match[item]: @@ -178,14 +210,25 @@ def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:di def compare_leximin(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->bool: """ - Determain whethere the leximin tuple of the new match is grater or equal to the leximin tuple of the old match - - Parameters: - - new_match_leximin_tuple: the leximin tuple of the new matching - - old_match_leximin_tuple: the leximin tuple of the old matching + Determine whether the leximin tuple of the new match is greater or equal to the leximin tuple of the old match. - Return: - - true or false if new_match_leximin_tuple >= old_match_leximin_tuple + Args: + - new_match_leximin_tuple (list): The leximin tuple of the new matching. + - old_match_leximin_tuple (list): The leximin tuple of the old matching. + + Returns: + - bool: True if new_match_leximin_tuple >= old_match_leximin_tuple, otherwise False. + + Example: + >>> new_match = [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] + >>> old_match = [("s7",1),("c4",1),("s4",13),("c1",16),("c3",18),("c2",19),("s6",20),("s3",21),("s2",23),("s5",26),("c1",29),("c1",36),("c1",41),("s1",50)] + >>> compare_leximin(new_match, old_match) + False + + >>> new_match = [("c4",0),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] + >>> old_match = [("c4",3),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] + >>> compare_leximin(new_match, old_match) + True """ for k in range(0, len(new_match_leximin_tuple)): if new_match_leximin_tuple[k][1] == old_match_leximin_tuple[k][1]: @@ -197,21 +240,54 @@ def compare_leximin(new_match_leximin_tuple:list, old_match_leximin_tuple:list)- def sourceDec(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->str: """ - Determines the agent causing the leximin decrease between two matchings mu1 and mu2. - - Parameters: - - new_match_leximin_tuple: the leximin tuple of the new matching - - old_match_leximin_tuple: the leximin tuple of the old matching - + Determine the agent causing the leximin decrease between two matchings. + + Args: + - new_match_leximin_tuple (list): The leximin tuple of the new matching. + - old_match_leximin_tuple (list): The leximin tuple of the old matching. + Returns: - - The agent (student) causing the leximin decrease. + - str: The agent (student) causing the leximin decrease. + + Example: + >>> new_match = [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] + >>> old_match = [("s7",1),("c4",1),("s4",13),("c1",16),("c3",18),("c2",19),("s6",20),("s3",21),("s2",23),("s5",26),("c1",29),("c1",36),("c1",41),("s1",50)] + >>> sourceDec(new_match, old_match) + 's6' + + >>> new_match = [("c4",3),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] + >>> old_match = [("c4",3),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] + >>> sourceDec(new_match, old_match) + 'c3' """ for k in range(0, len(new_match_leximin_tuple)): if new_match_leximin_tuple[k][1] < old_match_leximin_tuple[k][1]: return new_match_leximin_tuple[k][0] return "" -def get_lowest_ranked_student(item:int, match:dict, items_valuations:dict)->str: +def get_lowest_ranked_student(item, match:dict, items_valuations:dict)->str: + """ + Get the lowest ranked student for a given item. + + Args: + - item: The item for which the lowest ranked student is to be found. + - match (dict): A dictionary where keys are items and values are lists of agents. + - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. + + Returns: + - str: The lowest ranked student for the given item. + + Example: + >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} + >>> items_valuations = { #the colleges valuations + "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + } + >>> get_lowest_ranked_student("c3", match, items_valuations) + 's6' + """ # min = sys.maxsize # lowest_ranked_student = 0 # for agant in match[item]: @@ -223,41 +299,92 @@ def get_lowest_ranked_student(item:int, match:dict, items_valuations:dict)->str: return min(match[item], key=lambda agant: items_valuations[item][agant]) def update_matching_valuations_sum(match:dict, items_valuations:dict, agents:list, items:list)->dict: + """ + Update the sum of valuations for each item in the matching. + + Args: + - match (dict): A dictionary where keys are items and values are lists of agents. + - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. + - agents (list): List of agents. + - items (list): List of items. + + Returns: + - dict: A dictionary with the sum of valuations for each item. + + Example: + >>> match = {c1:[s1,s2,s3,s4], c2:[s5], c3:[s6], c4:[s7]} + >>> items_valuations = { #the colleges valuations + "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + } + >>> agents = ["s1","s2","s3","s4","s5","s6","s7"] + >>> items = ["c1","c2","c3","c4"] + >>> update_matching_valuations_sum(match, items_valuations, agents, items) + {"c1": 107, "c2": 26, "c3": 20, "c4": 1} + """ matching_valuations_sum = { #in the artical it looks like this: vj(mu) colleague: sum(items_valuations[colleague][student] for student in students) for colleague, students in match.items() } return matching_valuations_sum +def create_stable_matching(agents_size, items_size): + """ + Create a stable matching of agents to items. + + Args: + - agents_size (int): The number of agents. + - items_size (int): The number of items. + + Returns: + - dict: A dictionary representing the stable matching. + + Example: + >>> create_stable_matching(7, 4) + {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} + """ + # Initialize the matching dictionary + matching = {} + + # Assign the first m-1 students to c1 + matching['c1'] = {f's{i}' for i in range(1, agents_size - items_size + 2)} + + # Assign the remaining students to cj for j >= 2 + for j in range(2, items_size + 1): + matching[f'c{j}'] = {f's{agents_size - (items_size - j)}'} + + if __name__ == "__main__": - # import doctest, sys - # print(doctest.testmod()) + import doctest, sys + print(doctest.testmod()) # Define the instance - S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] - C = ["c1", "c2", "c3", "c4"] - V = { #the colleges valuations - "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} - } - U = { #the students valuations - "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, - "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, - "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, - "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, - "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, - "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, - "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} - } + # S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + # C = ["c1", "c2", "c3", "c4"] + # V = { #the colleges valuations + # "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + # "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + # "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + # "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + # } + # U = { #the students valuations + # "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, + # "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, + # "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, + # "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, + # "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, + # "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, + # "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} + # } - # Assuming `Instance` can handle student and course preferences directly - instance = Instance(agents=S, items=C) - - # Run the FaStGen algorithm - allocation = FaStGen(instance, agents_valuations=U, items_valuations=V) - print(allocation) - # Define the expected allocation (this is hypothetical; you should set it based on the actual expected output) - expected_allocation = {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} + # # Assuming `Instance` can handle student and course preferences directly + # instance = Instance(agents=S, items=C) + + # # Run the FaStGen algorithm + # allocation = FaStGen(instance, agents_valuations=U, items_valuations=V) + # print(allocation) + # # Define the expected allocation (this is hypothetical; you should set it based on the actual expected output) + # expected_allocation = {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} From 8aac66a5ffd0a0d94ecfb5a757db96a74746202a Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Thu, 27 Jun 2024 12:02:14 +0300 Subject: [PATCH 017/111] updating FaStGen --- .../algorithms/Optimization_Matching/FaSt.py | 48 +++++------ .../Optimization_Matching/FaStGen.py | 84 ++++++++++++------- 2 files changed, 78 insertions(+), 54 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 865ed21..8347f02 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -12,38 +12,35 @@ -def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dict: +def Demote(matching, student_index, down, up): """ - Demote algorithm: Adjust the matching by moving a student to a lower-ranked college - while maintaining the invariant of a complete stable matching. - The Demote algorithm is a helper function used within the FaSt algorithm to adjust the matching while maintaining stability. - - :param matching: the matchinf of the students with colleges. - :param student_index: Index of the student to move. - :param down_index: Index of the college to move the student to. - :param up_index: Index of the upper bound college. - - # The test is the same as the running example we gave in Ex2. - - >>> from fairpyx import AllocationBuilder - >>> alloc = AllocationBuilder(agent_capacities={"s1": 1, "s2": 1, "s3": 1, "s4": 1, "s5": 1}, item_capacities={"c1": 1, "c2": 2, "c3": 2}) - >>> alloc.add_allocation(0, 0) # s1 -> c1 - >>> alloc.add_allocation(1, 1) # s2 -> c2 - >>> alloc.add_allocation(2, 1) # s3 -> c2 - >>> alloc.add_allocation(3, 2) # s4 -> c3 - >>> alloc.add_allocation(4, 2) # s5 -> c3 - >>> Demote(alloc, 2, 2, 1) - >>> alloc.get_allocation() - {'s1': ['c1'], 's2': ['c2'], 's3': ['c3'], 's4': ['c3'], 's5': ['c2']} + Perform the demote operation to maintain stability. + + :param matching: The current matching dictionary + :param student_index: The student being demoted + :param down: The current college of the student + :param up: The new college for the student """ # Move student to college 'down' while reducing the number of students in 'up' # Set t to student_index t = student_index # Set p to 'down' - p = down_index + p = down + # Check if the student 't' is in college 'Cp-1' + print( "Now demote student", t) + print ("t ",t) + print( "p " , p) + print ("matching[p - 1]: ", matching[p - 1]) + if t not in matching[p - 1]: + raise ValueError(f"Student {t} should be in matching to college {p - 1}") + # Check that all colleges have at least one student + for college, students in matching.items(): + if len(students) < 1: + raise ValueError(f"All colleges must contain at least 1 student. College number {college} has only {len(students)} students.") # While p > up - while p > up_index: + while p > up: + print ("while loop") # Remove student 't' from college 'cp-1' matching[p - 1].remove(t) # Add student 't' to college 'cp' @@ -51,8 +48,9 @@ def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dic # Decrement t and p t -= 1 p -= 1 + print (matching) - return matching #Return the matching after the change + return matching def FaSt(alloc: AllocationBuilder)-> dict: """ diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index c935472..dc60920 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -43,9 +43,14 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d >>> divide(FaStGen, instance=instance, agents_valuations=U, items_valuations=V) {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} """ + logger.info("Starting FaStGen algorithm") + S = alloc.instance.agents C = alloc.instance.items match = create_stable_matching(len(S), len(C)) + + logger.debug(f"Initial match: {match}") + UpperFix = [C[1]] LowerFix = [C[len(C)]] SoftFix = [] @@ -59,8 +64,11 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d down = min(valuations_sum for key, valuations_sum in matching_valuations_sum.items() if key in UnFixed) SoftFix = [pair for pair in SoftFix if not (pair[1] <= up < pair[0])] + logger.debug(f"UpperFix: {UpperFix}, LowerFix: {LowerFix}, SoftFix: {SoftFix}, UnFixed: {UnFixed}") + if (len(match[up]) == 1) or (matching_valuations_sum[up] <= matching_valuations_sum[down]): LowerFix.append(up) + logger.info(f"Added {up} to LowerFix") else: #check the lowest-rank student who currently belongs to mu(c_{down-1}) agant_to_demote = get_lowest_ranked_student(down-1, match, items_valuations) @@ -70,23 +78,28 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d if compare_leximin(match_leximin_tuple, _match_leximin_tuple): match = _match matching_valuations_sum = update_matching_valuations_sum(match=match,items_valuations=items_valuations, agents=S, items=C) + logger.debug(f"Match updated: {match}") elif sourceDec(_match, match) == up: LowerFix.append(up) UpperFix.append(up + 1) + logger.info(f"Updated LowerFix and UpperFix with {up}") elif sourceDec(_match, match) in alloc.instance.agents: t = match[sourceDec(_match, match)] LowerFix.append(t) UpperFix.append(t+1) A = [j for j in UnFixed if (j > t + 1)] SoftFix.extend((j, t+1) for j in A) + logger.info(f"Updated LowerFix and UpperFix with {t}") else: match, LowerFix, UpperFix, SoftFix = LookAheadRoutine((S, C, agents_valuations, items_valuations), match, down, LowerFix, UpperFix, SoftFix) + logger.debug(f"LookAheadRoutine result: match={match}, LowerFix={LowerFix}, UpperFix={UpperFix}, SoftFix={SoftFix}") UnFixed = [ j for j in alloc.instance.items if (j not in UpperFix) or any((j, _j) not in SoftFix for _j in alloc.instance.items if _j > j) ] + logger.info("Finished FaStGen algorithm") return match def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list, SoftFix:list)->tuple: @@ -138,15 +151,23 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list UF = UpperFix.copy() _match = match.copy() + logger.info("Starting LookAheadRoutine") + logger.debug(f"Initial parameters - match: {match}, down: {down}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") + matching_valuations_sum = update_matching_valuations_sum(match=_match,items_valuations=items_valuations, agents=agents, items=items) while len(LF) + len([item for item in UF if item not in LF]) < len(items) - 1: up = min([j for j in items if j not in LowerFix]) + logger.debug(f"Selected 'up': {up}") + if (len(match[up]) == 1) or (matching_valuations_sum[up] <= matching_valuations_sum[down]): LF.append(up) + logger.info(f"Appended {up} to LowerFix") else: #check the lowest-rank student who currently belongs to mu(c_{down-1}) - agant_to_demote = get_lowest_ranked_student(down-1, match) + agant_to_demote = get_lowest_ranked_student(down-1, match, items_valuations) + logger.debug(f"Agent to demote: {agant_to_demote}") + _match = Demote(_match, agant_to_demote, up, down) matching_valuations_sum = update_matching_valuations_sum(match=_match,items_valuations=items_valuations, agents=agents, items=items) _match_leximin_tuple = create_leximin_tuple(match=_match, agents_valuations=agents_valuations, items_valuations=items_valuations) @@ -155,17 +176,23 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list match = _match LowerFix = LF UpperFix = UF + logger.info("Updated match and fixed LowerFix and UpperFix") break elif sourceDec(_match, match) == up: LF.append(up) UF.append(up + 1) + logger.info(f"Appended {up} to LowerFix and {up+1} to UpperFix") elif sourceDec(_match, match) in agents: t = _match[sourceDec(_match, match)] if t == down: UpperFix.append(down) else: SoftFix.append((down, t)) + logger.info(f"Appended {down} to UpperFix or SoftFix") break + + logger.info("Completed LookAheadRoutine") + logger.debug(f"Final result - match: {match}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") return (match, LowerFix, UpperFix, SoftFix) def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:dict): @@ -357,34 +384,33 @@ def create_stable_matching(agents_size, items_size): if __name__ == "__main__": - import doctest, sys - print(doctest.testmod()) + # import doctest, sys + # print(doctest.testmod()) # Define the instance - # S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] - # C = ["c1", "c2", "c3", "c4"] - # V = { #the colleges valuations - # "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - # "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - # "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - # "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} - # } - # U = { #the students valuations - # "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, - # "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, - # "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, - # "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, - # "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, - # "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, - # "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} - # } + S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + C = ["c1", "c2", "c3", "c4"] + V = { #the colleges valuations + "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + } + U = { #the students valuations + "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, + "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, + "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, + "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, + "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, + "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, + "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} + } - # # Assuming `Instance` can handle student and course preferences directly - # instance = Instance(agents=S, items=C) - - # # Run the FaStGen algorithm - # allocation = FaStGen(instance, agents_valuations=U, items_valuations=V) - # print(allocation) - # # Define the expected allocation (this is hypothetical; you should set it based on the actual expected output) - # expected_allocation = {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} - + # Assuming `Instance` can handle student and course preferences directly + instance = Instance(agents=S, items=C) + + # Run the FaStGen algorithm + allocation = FaStGen(instance, agents_valuations=U, items_valuations=V) + print(allocation) + # Define the expected allocation (this is hypothetical; you should set it based on the actual expected output) + expected_allocation = {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} \ No newline at end of file From a2674a7661e57e65a586472c43cfbbeb924bfd4d Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Fri, 28 Jun 2024 03:16:19 +0300 Subject: [PATCH 018/111] Update FaSt.py Fast.py finished --- .../algorithms/Optimization_Matching/FaSt.py | 382 ++++++++++++------ 1 file changed, 250 insertions(+), 132 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 865ed21..585d50c 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -24,24 +24,27 @@ def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dic :param up_index: Index of the upper bound college. # The test is the same as the running example we gave in Ex2. - - >>> from fairpyx import AllocationBuilder - >>> alloc = AllocationBuilder(agent_capacities={"s1": 1, "s2": 1, "s3": 1, "s4": 1, "s5": 1}, item_capacities={"c1": 1, "c2": 2, "c3": 2}) - >>> alloc.add_allocation(0, 0) # s1 -> c1 - >>> alloc.add_allocation(1, 1) # s2 -> c2 - >>> alloc.add_allocation(2, 1) # s3 -> c2 - >>> alloc.add_allocation(3, 2) # s4 -> c3 - >>> alloc.add_allocation(4, 2) # s5 -> c3 - >>> Demote(alloc, 2, 2, 1) - >>> alloc.get_allocation() - {'s1': ['c1'], 's2': ['c2'], 's3': ['c3'], 's4': ['c3'], 's5': ['c2']} - """ +# ... valuations = {"Alice": {"c1": 11, "c2": 22}, "Bob": {"c1": 33, "c2": 44}}, + + >>> matching = {1: [1, 6], 2: [2, 3],3: [4, 5]} + >>> UP = 1 + >>> DOWN = 3 + >>> I = 2 + >>> Demote(matching, I, DOWN, UP) + {1: [6], 2: [3, 1], 3: [4, 5, 2]}""" # Move student to college 'down' while reducing the number of students in 'up' # Set t to student_index t = student_index # Set p to 'down' p = down_index + if t not in matching[p - 1]: + raise ValueError(f"Student {t} should be in matching to college {p - 1}") + # Check that all colleges have at least one students + for college, students in matching.items(): + if len(students) < 1: + raise ValueError(f"All colleges must contain at least 1 student. College number {college} has only {len(students)} students.") + # While p > up while p > up_index: # Remove student 't' from college 'cp-1' @@ -54,150 +57,265 @@ def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dic return matching #Return the matching after the change -def FaSt(alloc: AllocationBuilder)-> dict: + +def get_leximin_tuple(matching, V): """ - FaSt algorithm: Find a leximin optimal stable matching under ranked isometric valuations. - # def FaSt(instance: Instance, explanation_logger: ExplanationLogger = ExplanationLogger()): - :param alloc: an allocation builder, which tracks the allocation and the remaining capacity for items and agents. - # The test is not the same as the running example we gave in Ex2. - # We asked to change it to be with 3 courses and 7 students, like in algorithm 3 (FaSt-Gen algo). + Generate the leximin tuple based on the given matching and evaluations, + including the sum of valuations for each college. - >>> from fairpyx.adaptors import divide - >>> S = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} #Student set - >>> C = {"c1", "c2", "c3"} #College set - >>> V = { - ... "s1": {"c1": 1, "c3": 2, "c2": 3}, - ... "s2": {"c2": 1, "c1": 2, "c3": 3}, - ... "s3": {"c1": 1, "c3": 2, "c2": 3}, - ... "s4": {"c3": 1, "c2": 2, "c1": 3}, - ... "s5": {"c2": 1, "c3": 2, "c1": 3}, - ... "s6": {"c3": 1, "c1": 2, "c2": 3}, - ... "s7": {"c1": 1, "c2": 2, "c3": 3} - ... } # V[i][j] is the valuation of Si for matching with Cj - >>> instance = Instance(agents=S, items=C, _valuations=V) - >>> divide(FaSt, instance=instance) - {'s1': ['c1'], 's2': ['c2'], 's3': ['c1'], 's4': ['c3'], 's5': ['c3'], 's6': ['c3'], 's7': ['c2']} + :param matching: The current matching dictionary + :param V: The evaluations matrix + :return: Leximin tuple """ - S = alloc.instance.agents - C = alloc.instance.items - V = alloc.instance._valuations - # Now V look like this: - # "Alice": {"c1":2, "c2": 3, "c3": 4}, - # "Bob": {"c1": 4, "c2": 5, "c3": 6} + leximin_tuple = [] + college_sums = [] + + # Iterate over each college in the matching + for college, students in matching.items(): + college_sum = 0 + # For each student in the college, append their valuation for the college to the leximin tuple + for student in students: + valuation = V[student - 1][college - 1] + leximin_tuple.append(valuation) + college_sum += valuation + college_sums.append(college_sum) + # Append the college sums to the leximin tuple + leximin_tuple.extend(college_sums) + + # Sort the leximin tuple in descending order + leximin_tuple.sort(reverse=False) + + return leximin_tuple + - # Initialize a stable matching - matching = initialize_stable_matching(S, C, V) - # Compute the initial leximin value and position array - leximin_value, pos = compute_leximin_value(matching, V) +def get_unsorted_leximin_tuple(matching, V): + """ + Generate the leximin tuple based on the given matching and evaluations, + including the sum of valuations for each college. - # Iterate to find leximin optimal stable matching - for i in range(len(S) - 1, -1, -1): - for j in range(len(C) - 1, 0, -1): - # Check if moving student i to college j improves the leximin value - if can_improve_leximin(S[i], C[j], V, leximin_value): - # If it does improve - perform the demote operation to maintain stability - Demote(matching, S[i], C[j-1], C[j]) - # Recompute the leximin value and position array after the demotion - leximin_value, pos = compute_leximin_value(matching, V) + :param matching: The current matching dictionary + :param V: The evaluations matrix + :return: UNSORTED Leximin tuple + """ + leximin_tuple = [] + college_sums = [] - # Return the final stable matching - return matching + # Iterate over each college in the matching + for college, students in matching.items(): + college_sum = 0 + # For each student in the college, append their valuation for the college to the leximin tuple + for student in students: + valuation = V[student - 1][college - 1] + leximin_tuple.append(valuation) + college_sum += valuation + college_sums.append(college_sum) + # Append the college sums to the leximin tuple + leximin_tuple.extend(college_sums) + return leximin_tuple -def can_improve_leximin(student, college, V, leximin_value)-> bool: +def build_pos_array(matching, V): """ - Check if moving the student to the college improves the leximin value. + Build the pos array based on the leximin tuple and the matching. - :param student: The student being considered for reassignment - :param college: The college being considered for the student's reassignment - :param V: Valuation matrix where V[i][j] is the valuation of student i for college j - :param leximin_value: The current leximin value - :return: True if the new leximin value is an improvement, otherwise False + :param leximin_tuple: The leximin tuple + :param matching: The current matching dictionary + :param V: The evaluations matrix + :return: Pos array """ - # Get the current value of the student for the new college - current_value = V[student - 1][college - 1] # assuming students and colleges are 1-indexed - # Create a copy of the current leximin values - new_values = leximin_value[:] - # Remove the current value of the student in their current college from the leximin values - new_values.remove(current_value) - # Add the current value of the student for the new college to the leximin values - new_values.append(current_value) - # Sort the new leximin values to form the new leximin tuple - new_values.sort() - # Return True if the new leximin tuple is lexicographically greater than the current leximin tuple - return new_values > leximin_value - - -def update_leximin_value(matching, V)-> list: - # Update the leximin value after demotion - values = [] + pos = [] # Initialize pos array + student_index = 0 + college_index = 0 + leximin_unsorted_tuple = get_unsorted_leximin_tuple(matching, V) + leximin_sorted_tuple = sorted(leximin_unsorted_tuple) + while student_index < len(V): + pos_value = leximin_sorted_tuple.index(leximin_unsorted_tuple[student_index]) + pos.append(pos_value) + student_index += 1 + while college_index < len(matching): + pos_value = leximin_sorted_tuple.index(leximin_unsorted_tuple[student_index + college_index]) + pos.append(pos_value) + college_index += 1 + return pos + + +def create_L(matching): + """ + Create the L list based on the matching. + :param matching: The current matching + :return: L list + """ + L = [] + + # Create a list of tuples (college, student) for college, students in matching.items(): for student in students: - student_index = student - 1 # assuming students are 1-indexed - college_index = college - 1 # assuming colleges are 1-indexed - values.append(V[student_index][college_index]) - values.sort() - return values + L.append((college, student)) + + return L -def compute_leximin_value(matching, V)-> tuple: +def build_college_values(matching, V): """ - Compute the leximin value of the current matching. + Build the college_values dictionary that sums the students' valuations for each college. - This function calculates the leximin value of the current matching by evaluating the - valuations of students for their assigned colleges. The leximin value is the sorted - list of these valuations. It also returns the position array that tracks the positions - of the valuations in the sorted list. + :param matching: The current matching dictionary + :param V: The evaluations matrix + :return: College values dictionary + """ + college_values = {} - :param matching: A dictionary representing the current matching where each college is a key and the value is a list of assigned students - :param V: Valuation matrix where V[i][j] is the valuation of student i for college j - :return: A tuple (values, pos) where values is the sorted list of valuations (leximin value) and pos is the position array - """ + # Iterate over each college in the matching + for college, students in matching.items(): + college_sum = sum(V[student - 1][college - 1] for student in students) + college_values[college] = college_sum + + return college_values - values = []# Initialize an empty list to store the valuations - for college, students in matching.items():# Iterate over each college and its assigned students in the matching - for student in students:# Iterate over each student assigned to the current college - student_index = student - 1 # assuming students are 1-indexed - college_index = college - 1 # assuming colleges are 1-indexed - # Append the student's valuation for the current college to the values list - values.append(V[student_index][college_index]) - # Sort the valuations in non-decreasing order to form the leximin tuple - values.sort() - pos = [0] * len(values)# Initialize the position array to track the positions of the valuations - # Populate the position array with the index of each valuation - for idx, value in enumerate(values): - pos[idx] = idx - # Return the sorted leximin values and the position array - return values, pos - - -def initialize_stable_matching(S, C, V)-> dict: + +def initialize_matching(n, m): """ - Initialize a student optimal stable matching. - This function creates an initial stable matching by assigning students to colleges based on - their preferences. The first n - m + 1 students are assigned to the highest-ranked college, - and the remaining students are assigned to the other colleges in sequence. - - :param S: List of students - :param C: List of colleges - :param V: Valuation matrix where V[i][j] is the valuation of student i for college j - :return: A dictionary representing the initial stable matching where each college is a key and the value is a list of assigned students + Initialize the first stable matching. + + :param n: Number of students + :param m: Number of colleges + :return: Initial stable matching """ - # Get the number of students and colleges - n = len(S) - m = len(C) - # Create an empty matching dictionary where each college has an empty list of assigned students - matching = {c: [] for c in C} + initial_matching = {k: [] for k in range(1, m + 1)} # Create a dictionary for the matching + # Assign the first (n - m + 1) students to the first college (c1) + for student in range(1, n - m + 2): + initial_matching[1].append(student) + # Assign each remaining student to the subsequent colleges (c2, c3, ...) + for j in range(2, m + 1): + initial_matching[j].append(n - m + j) + return initial_matching + +def convert_valuations_to_matrix(valuations): + """ + Convert the dictionary of valuations to a matrix format. + + :param valuations: Dictionary of valuations + :return: Matrix of valuations + """ + students = sorted(valuations.keys()) # Sort student keys to maintain order + colleges = sorted(valuations[students[0]].keys()) # Sort college keys to maintain order + + V = [] + for student in students: + V.append([valuations[student][college] for college in colleges]) + + return V + +def FaSt(alloc: AllocationBuilder)-> dict: + """ + FaSt algorithm: Find a leximin optimal stable matching under ranked isometric valuations. + # def FaSt(instance: Instance, explanation_logger: ExplanationLogger = ExplanationLogger()): + :param alloc: an allocation builder, which tracks the allocation and the remaining capacity for items and agents. + # The test is not the same as the running example we gave in Ex2. + # We asked to change it to be with 3 courses and 7 students, like in algorithm 3 (FaSt-Gen algo). + + >>> from fairpyx.adaptors import divide + >>> agents = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} #Student set=S + >>> items = {"c1", "c2", "c3"} #College set=C + >>> valuation= {"S1": {"c1": 9, "c2": 8, "c3": 7}, + ... "S2": {"c1": 8, "c2": 7, "c3":6}, + ... "S3": {"c1": 7, "c2": 6, "c3":5}, + ... "S4": {"c1": 6, "c2": 5, "c3":4}, + ... "S5": {"c1": 5, "c2": 4, "c3":3}, + ... "S6": {"c1": 4, "c2": 3, "c3":2}, + ... "S7": {"c1": 3, "c2": 2, "c3":1}}# V[i][j] is the valuation of Si for matching with Cj + >>> ins = Instance(agents=agents, items=items, valuations=valuation) + >>> alloc = AllocationBuilder(instance=ins) + >>> FaSt(alloc=alloc) + {1: [1,2,3], 2: [5, 4], 3: [7, 6]}""" + + S = alloc.instance.agents + C = alloc.instance.items + V = alloc.instance._valuations + # Now V look like this: + # "Alice": {"c1":2, "c2": 3, "c3": 4}, + # "Bob": {"c1": 4, "c2": 5, "c3": 6} + n=len(S)# number of students + m = len(C) # number of colleges + i = n - 1 # start from the last student + j = m - 1 # start from the last college + # Initialize the first stable matching + initial_matching = initialize_matching(n, m) + # Convert Valuations to only numerical matrix + V= convert_valuations_to_matrix(V) + + lex_tupl=get_leximin_tuple(initial_matching,V) + +# Initialize the leximin tuple L and position array pos + pos= build_pos_array(initial_matching, V) + + L=create_L(initial_matching) + + college_values=build_college_values(initial_matching,V) + print("i: ", i) + print("j: ", j) + index = 1 + while i > j - 1 and j > 0: + + print("******** Iteration number ", index, "********") + print("i: ", i) + print("j: ", j) + print("college_values[j+1]: ", college_values[j + 1]) + print("V[i-1][j]: ", V[i - 1][j]) + print("college_values: ", college_values) + if college_values[j + 1] >= V[i - 1][j]: ###need to update after each iteration + j -= 1 + else: + if college_values[j + 1] < V[i - 1][j]: + print("V[i-1][j]:", V[i - 1][j]) + # if V[i][j - 1] > L[j - 1]: + initial_matching = Demote(initial_matching, i, j + 1, 1) + print("initial_matching after demote:", initial_matching) + else: + if V[i][j - 1] < college_values[j]: + j -= 1 + else: + # Lookahead + k = i + t = pos[i] + µ_prime = initial_matching.copy() + while k > j - 1: + if V[k][j - 1] > L[t - 1]: + i = k + initial_matching = Demote(µ_prime, k, j, 1) + break + elif V[k][j - 1] < college_values[j]: + j -= 1 + break + else: + µ_prime = Demote(µ_prime, k, j, 1) + k -= 1 + t += 1 + if k == j - 1 and initial_matching != µ_prime: + j -= 1 + # Updates + college_values = build_college_values(initial_matching, V) # Update the college values + lex_tupl = get_leximin_tuple(initial_matching, V) + print("lex_tupl: ", lex_tupl) + L = create_L(initial_matching) + print("L:", L) ################todo : update POS + pos = build_pos_array(initial_matching, V) + print("pos:", pos) + + i -= 1 + index += 1 + print("END while :") + print("i: ", i) + print("j: ", j) + + return initial_matching + + + + - # Assign the first n - m + 1 students to the highest ranked college (C1) - for i in range(n - m + 1): - matching[C[0]].append(S[i]) - # Assign the remaining students to the other colleges in sequence - for j in range(1, m): - matching[C[j]].append(S[n - m + j]) - return matching# Return the initialized stable matching if __name__ == "__main__": From 2551e9499d061b2725ec63849f0d5101f58600dc Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Fri, 28 Jun 2024 03:18:40 +0300 Subject: [PATCH 019/111] Update FaSt.py Tweaks in FaSt.py --- .../algorithms/Optimization_Matching/FaSt.py | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 4559822..585d50c 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -12,9 +12,11 @@ -def Demote(matching, student_index, down, up): +def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dict: """ - Perform the demote operation to maintain stability. + Demote algorithm: Adjust the matching by moving a student to a lower-ranked college + while maintaining the invariant of a complete stable matching. + The Demote algorithm is a helper function used within the FaSt algorithm to adjust the matching while maintaining stability. :param matching: the matchinf of the students with colleges. :param student_index: Index of the student to move. @@ -22,34 +24,19 @@ def Demote(matching, student_index, down, up): :param up_index: Index of the upper bound college. # The test is the same as the running example we gave in Ex2. - - >>> from fairpyx import AllocationBuilder - >>> alloc = AllocationBuilder(agent_capacities={"s1": 1, "s2": 1, "s3": 1, "s4": 1, "s5": 1}, item_capacities={"c1": 1, "c2": 2, "c3": 2}) - >>> alloc.add_allocation(0, 0) # s1 -> c1 - >>> alloc.add_allocation(1, 1) # s2 -> c2 - >>> alloc.add_allocation(2, 1) # s3 -> c2 - >>> alloc.add_allocation(3, 2) # s4 -> c3 - >>> alloc.add_allocation(4, 2) # s5 -> c3 - >>> Demote(alloc, 2, 2, 1) - >>> alloc.get_allocation() - {'s1': ['c1'], 's2': ['c2'], 's3': ['c3'], 's4': ['c3'], 's5': ['c2']} - """ +# ... valuations = {"Alice": {"c1": 11, "c2": 22}, "Bob": {"c1": 33, "c2": 44}}, + + >>> matching = {1: [1, 6], 2: [2, 3],3: [4, 5]} + >>> UP = 1 + >>> DOWN = 3 + >>> I = 2 + >>> Demote(matching, I, DOWN, UP) + {1: [6], 2: [3, 1], 3: [4, 5, 2]}""" # Move student to college 'down' while reducing the number of students in 'up' # Set t to student_index t = student_index # Set p to 'down' - p = down - # Check if the student 't' is in college 'Cp-1' - print( "Now demote student", t) - print ("t ",t) - print( "p " , p) - print ("matching[p - 1]: ", matching[p - 1]) - if t not in matching[p - 1]: - raise ValueError(f"Student {t} should be in matching to college {p - 1}") - # Check that all colleges have at least one student - for college, students in matching.items(): - if len(students) < 1: - raise ValueError(f"All colleges must contain at least 1 student. College number {college} has only {len(students)} students.") + p = down_index if t not in matching[p - 1]: raise ValueError(f"Student {t} should be in matching to college {p - 1}") @@ -59,8 +46,7 @@ def Demote(matching, student_index, down, up): raise ValueError(f"All colleges must contain at least 1 student. College number {college} has only {len(students)} students.") # While p > up - while p > up: - print ("while loop") + while p > up_index: # Remove student 't' from college 'cp-1' matching[p - 1].remove(t) # Add student 't' to college 'cp' @@ -68,9 +54,8 @@ def Demote(matching, student_index, down, up): # Decrement t and p t -= 1 p -= 1 - print (matching) - return matching + return matching #Return the matching after the change def get_leximin_tuple(matching, V): From 40f7c6bed92a37c5ff01cd1372f49f80cee11142 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Fri, 28 Jun 2024 17:00:28 +0300 Subject: [PATCH 020/111] Update FaStGen.py --- .../Optimization_Matching/FaStGen.py | 70 +++++++++++-------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index dc60920..f8fefc1 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -12,7 +12,7 @@ import logging logger = logging.getLogger(__name__) -def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:dict)->dict: +def FaStGen(alloc: AllocationBuilder, items_valuations:dict)->dict: """ Algorithem 3-FaSt-Gen: finding a match for the general ranked valuations setting. @@ -47,21 +47,26 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d S = alloc.instance.agents C = alloc.instance.items - match = create_stable_matching(len(S), len(C)) + agents_valuations = alloc.instance._valuations + match = create_stable_matching(S, C) logger.debug(f"Initial match: {match}") - UpperFix = [C[1]] - LowerFix = [C[len(C)]] + UpperFix = [C[0]] + LowerFix = [C[len(C)-1]] SoftFix = [] UnFixed = [item for item in C if item not in UpperFix] + print(set(match)) #creating a dictionary of vj(µ) = Pi∈µ(cj) for each j in C matching_valuations_sum = update_matching_valuations_sum(match=match,items_valuations=items_valuations, agents=S, items=C) while len(LowerFix) + len([item for item in UpperFix if item not in LowerFix]) < len(C): up = min([j for j in C if j not in LowerFix]) - down = min(valuations_sum for key, valuations_sum in matching_valuations_sum.items() if key in UnFixed) + down = min(UnFixed, key=lambda j: matching_valuations_sum[j]) + print("up: " + up) + print("down: " + down) + #removing from SoftFix thr pairs (j, j') that meet the requirment j' <= up < j SoftFix = [pair for pair in SoftFix if not (pair[1] <= up < pair[0])] logger.debug(f"UpperFix: {UpperFix}, LowerFix: {LowerFix}, SoftFix: {SoftFix}, UnFixed: {UnFixed}") @@ -71,11 +76,12 @@ def FaStGen(alloc: AllocationBuilder, agents_valuations:dict, items_valuations:d logger.info(f"Added {up} to LowerFix") else: #check the lowest-rank student who currently belongs to mu(c_{down-1}) - agant_to_demote = get_lowest_ranked_student(down-1, match, items_valuations) - _match = Demote(_match, agant_to_demote, up, down) + agant_to_demote = int(get_lowest_ranked_student(str(int(down)-1), match, items_valuations)) + _match = Demote(match, agant_to_demote, up, down) + #creating for each match it's leximin tuple _match_leximin_tuple = create_leximin_tuple(match=_match, agents_valuations=agents_valuations, items_valuations=items_valuations) match_leximin_tuple = create_leximin_tuple(match=match, agents_valuations=agents_valuations, items_valuations=items_valuations) - if compare_leximin(match_leximin_tuple, _match_leximin_tuple): + if is_bigger_leximin(match_leximin_tuple, _match_leximin_tuple): match = _match matching_valuations_sum = update_matching_valuations_sum(match=match,items_valuations=items_valuations, agents=S, items=C) logger.debug(f"Match updated: {match}") @@ -170,9 +176,10 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list _match = Demote(_match, agant_to_demote, up, down) matching_valuations_sum = update_matching_valuations_sum(match=_match,items_valuations=items_valuations, agents=agents, items=items) + #creating for each match it's leximin tuple _match_leximin_tuple = create_leximin_tuple(match=_match, agents_valuations=agents_valuations, items_valuations=items_valuations) match_leximin_tuple = create_leximin_tuple(match=match, agents_valuations=agents_valuations, items_valuations=items_valuations) - if compare_leximin(match_leximin_tuple, _match_leximin_tuple): + if is_bigger_leximin(match_leximin_tuple, _match_leximin_tuple): match = _match LowerFix = LF UpperFix = UF @@ -235,7 +242,7 @@ def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:di leximin_tuple.sort(key = lambda x: x[1]) return leximin_tuple -def compare_leximin(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->bool: +def is_bigger_leximin(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->bool: """ Determine whether the leximin tuple of the new match is greater or equal to the leximin tuple of the old match. @@ -357,7 +364,7 @@ def update_matching_valuations_sum(match:dict, items_valuations:dict, agents:lis } return matching_valuations_sum -def create_stable_matching(agents_size, items_size): +def create_stable_matching(agents, items): """ Create a stable matching of agents to items. @@ -376,41 +383,44 @@ def create_stable_matching(agents_size, items_size): matching = {} # Assign the first m-1 students to c1 - matching['c1'] = {f's{i}' for i in range(1, agents_size - items_size + 2)} + matching[items[0]] = {agents[i] for i in range(0, len(agents) - len(items) + 1)} # Assign the remaining students to cj for j >= 2 - for j in range(2, items_size + 1): - matching[f'c{j}'] = {f's{agents_size - (items_size - j)}'} + for j in range(1, len(items)): + matching[items[j]] = {agents[len(agents) - (len(items) - j)]} + + return matching if __name__ == "__main__": # import doctest, sys # print(doctest.testmod()) # Define the instance - S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] - C = ["c1", "c2", "c3", "c4"] + S = ["1", "2", "3", "4", "5", "6", "7"] + C = ["1", "2", "3", "4"] V = { #the colleges valuations - "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + "1" : {"1":50,"2":23,"3":21,"4":13,"5":10,"6":6,"7":5}, + "2" : {"1":45,"2":40,"3":32,"4":29,"5":26,"6":11,"7":4}, + "3" : {"1":90,"2":79,"3":60,"4":35,"5":28,"6":20,"7":15}, + "4" : {"1":80,"2":48,"3":36,"4":29,"5":15,"6":6,"7":1} } U = { #the students valuations - "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, - "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, - "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, - "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, - "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, - "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, - "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} + "1" : {"1":16,"2":10,"3":6,"4":5}, + "2" : {"1":36,"2":20,"3":10,"4":1}, + "3" : {"1":29,"2":24,"3":12,"4":10}, + "4" : {"1":41,"2":24,"3":5,"4":3}, + "5" : {"1":36,"2":19,"3":9,"4":6}, + "6" :{"1":39,"2":30,"3":18,"4":7}, + "7" : {"1":40,"2":29,"3":6,"4":1} } # Assuming `Instance` can handle student and course preferences directly - instance = Instance(agents=S, items=C) + instance = Instance(agents=S, items=C, valuations=U) + allocation = AllocationBuilder(instance) # Run the FaStGen algorithm - allocation = FaStGen(instance, agents_valuations=U, items_valuations=V) - print(allocation) + match = FaStGen(allocation, items_valuations=V) + print(match) # Define the expected allocation (this is hypothetical; you should set it based on the actual expected output) expected_allocation = {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} \ No newline at end of file From b474d98c403234a3f29c09131290614c64e1f85e Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Sun, 30 Jun 2024 21:44:04 +0300 Subject: [PATCH 021/111] Tweaks --- .../algorithms/Optimization_Matching/FaSt.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 585d50c..a1d8d17 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -23,7 +23,7 @@ def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dic :param down_index: Index of the college to move the student to. :param up_index: Index of the upper bound college. - # The test is the same as the running example we gave in Ex2. + #*** The test is the same as the running example we gave in Ex2.*** # ... valuations = {"Alice": {"c1": 11, "c2": 22}, "Bob": {"c1": 33, "c2": 44}}, >>> matching = {1: [1, 6], 2: [2, 3],3: [4, 5]} @@ -38,9 +38,10 @@ def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dic # Set p to 'down' p = down_index + # Check if student 't' is in college 'Cp-1' if t not in matching[p - 1]: raise ValueError(f"Student {t} should be in matching to college {p - 1}") - # Check that all colleges have at least one students + # Check that all colleges have at least one students for college, students in matching.items(): if len(students) < 1: raise ValueError(f"All colleges must contain at least 1 student. College number {college} has only {len(students)} students.") @@ -77,6 +78,7 @@ def get_leximin_tuple(matching, V): for student in students: valuation = V[student - 1][college - 1] leximin_tuple.append(valuation) + # Sum to add the valuations of colleges in the end of the tuple (according to Ex2) college_sum += valuation college_sums.append(college_sum) # Append the college sums to the leximin tuple @@ -117,6 +119,10 @@ def get_unsorted_leximin_tuple(matching, V): def build_pos_array(matching, V): """ Build the pos array based on the leximin tuple and the matching. + For example: + Leximin Tuple: [**9**, 8, 7, 6, 5, 3, 1, 35, 3, 1] -> V of S1 is 9 + Sorted Leximin Tuple: [1, 1, 3, 3, 5, 6, 7, 8, 9,35]-> 9 is in index 8 + pos=[8,7, 6,5,4,3,1,9,2,0] :param leximin_tuple: The leximin tuple :param matching: The current matching dictionary @@ -126,12 +132,16 @@ def build_pos_array(matching, V): pos = [] # Initialize pos array student_index = 0 college_index = 0 + # Get the unsorted leximin tuple leximin_unsorted_tuple = get_unsorted_leximin_tuple(matching, V) + # Get the sorted leximin tuple leximin_sorted_tuple = sorted(leximin_unsorted_tuple) + # Build pos array for students while student_index < len(V): pos_value = leximin_sorted_tuple.index(leximin_unsorted_tuple[student_index]) pos.append(pos_value) student_index += 1 + # Build pos array for colleges while college_index < len(matching): pos_value = leximin_sorted_tuple.index(leximin_unsorted_tuple[student_index + college_index]) pos.append(pos_value) @@ -158,7 +168,10 @@ def create_L(matching): def build_college_values(matching, V): """ Build the college_values dictionary that sums the students' valuations for each college. - + For example: + c1: [9, 8, 7, 6, 5] =35 + c2: [3] =3 + c3: [1] =1 :param matching: The current matching dictionary :param V: The evaluations matrix :return: College values dictionary @@ -186,7 +199,7 @@ def initialize_matching(n, m): for student in range(1, n - m + 2): initial_matching[1].append(student) # Assign each remaining student to the subsequent colleges (c2, c3, ...) - for j in range(2, m + 1): + for j in range(2, m + 1):# 2 because we started from C2 initial_matching[j].append(n - m + j) return initial_matching @@ -298,7 +311,7 @@ def FaSt(alloc: AllocationBuilder)-> dict: lex_tupl = get_leximin_tuple(initial_matching, V) print("lex_tupl: ", lex_tupl) L = create_L(initial_matching) - print("L:", L) ################todo : update POS + print("L:", L) pos = build_pos_array(initial_matching, V) print("pos:", pos) From 7f046d3f627e5daf7589823d6a935e5ccb35e653 Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Sun, 30 Jun 2024 21:45:33 +0300 Subject: [PATCH 022/111] Tweaks --- fairpyx/algorithms/Optimization_Matching/FaSt.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index a1d8d17..5536ff7 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -327,10 +327,6 @@ def FaSt(alloc: AllocationBuilder)-> dict: - - - - if __name__ == "__main__": import doctest doctest.testmod() From afb6e12abe02d8dcd29a6f9969ade6a2844277d2 Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Sun, 30 Jun 2024 21:46:06 +0300 Subject: [PATCH 023/111] Tweaks --- fairpyx/algorithms/Optimization_Matching/FaSt.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 5536ff7..9fd5e71 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -324,9 +324,6 @@ def FaSt(alloc: AllocationBuilder)-> dict: return initial_matching - - - if __name__ == "__main__": import doctest doctest.testmod() From 3a95cff816b883d73c9b94dd60eb22b5fcc61197 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Tue, 2 Jul 2024 01:45:59 +0300 Subject: [PATCH 024/111] updating the FaStGen algorithms --- .../algorithms/Optimization_Matching/FaSt.py | 14 +- .../Optimization_Matching/FaStGen.py | 505 ++++++++---------- 2 files changed, 234 insertions(+), 285 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 585d50c..7111593 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -37,13 +37,12 @@ def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dic t = student_index # Set p to 'down' p = down_index - if t not in matching[p - 1]: raise ValueError(f"Student {t} should be in matching to college {p - 1}") # Check that all colleges have at least one students - for college, students in matching.items(): - if len(students) < 1: - raise ValueError(f"All colleges must contain at least 1 student. College number {college} has only {len(students)} students.") + # for college, students in matching.items(): + # if len(students) < 1: + # raise ValueError(f"All colleges must contain at least 1 student. College number {college} has only {len(students)} students.") # While p > up while p > up_index: @@ -311,13 +310,6 @@ def FaSt(alloc: AllocationBuilder)-> dict: return initial_matching - - - - - - - if __name__ == "__main__": import doctest doctest.testmod() diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index f8fefc1..0f25c3c 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -15,100 +15,97 @@ def FaStGen(alloc: AllocationBuilder, items_valuations:dict)->dict: """ Algorithem 3-FaSt-Gen: finding a match for the general ranked valuations setting. - :param alloc: an allocation builder, which tracks the allocation and the remaining capacity for items and agents. - :param agents_valuations: a dictionary represents how agents valuates the items :param items_valuations: a dictionary represents how items valuates the agents >>> from fairpyx.adaptors import divide >>> S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] >>> C = ["c1", "c2", "c3", "c4"] - >>> V = { #the colleges valuations - "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} - } - >>> U = { #the students valuations - "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, - "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, - "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, - "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, - "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, - "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, - "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} - } - >>> instance = Instance(agents=S, items=C) - >>> divide(FaStGen, instance=instance, agents_valuations=U, items_valuations=V) - {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} + >>> V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1}} + >>> U = { "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, "s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, "s7" : {"c1":40,"c2":29,"c3":6,"c4":1}} + >>> ins = Instance(agents=S, items=C, valuations=U) + >>> alloc = AllocationBuilder(instance=ins) + >>> FaStGen(alloc=alloc, items_valuations=V) + {'c1': ['s1'], 'c2': ['s2'], 'c3': ['s5', 's4', 's3'], 'c4': ['s7', 's6']} """ logger.info("Starting FaStGen algorithm") S = alloc.instance.agents C = alloc.instance.items agents_valuations = alloc.instance._valuations - match = create_stable_matching(S, C) + #Convert the list of the agents and item to dictionary so that each agent\item will have its coresponding integer + S_dict = generate_dictionary(S) + C_dict = generate_dictionary(C) - logger.debug(f"Initial match: {match}") + #Creating a match of the integers and the string coresponding one another to deal with the demote function and the leximin tuple as well + integer_match = create_stable_matching(agents=S, items=C, agents_dict=S_dict, items_dict=C_dict) + str_match = integer_to_str_matching(integer_match=integer_match, agent_dict=S_dict, items_dict=C_dict) - UpperFix = [C[0]] - LowerFix = [C[len(C)-1]] + logger.debug(f"Initial match: {str_match}") + + UpperFix = [C_dict[C[0]]] + LowerFix = [C_dict[C[len(C)-1]]] SoftFix = [] - UnFixed = [item for item in C if item not in UpperFix] + UnFixed = [item for item in C_dict.values() if item not in UpperFix] + - print(set(match)) #creating a dictionary of vj(µ) = Pi∈µ(cj) for each j in C - matching_valuations_sum = update_matching_valuations_sum(match=match,items_valuations=items_valuations, agents=S, items=C) + matching_valuations_sum = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) while len(LowerFix) + len([item for item in UpperFix if item not in LowerFix]) < len(C): - up = min([j for j in C if j not in LowerFix]) - down = min(UnFixed, key=lambda j: matching_valuations_sum[j]) - print("up: " + up) - print("down: " + down) - #removing from SoftFix thr pairs (j, j') that meet the requirment j' <= up < j - SoftFix = [pair for pair in SoftFix if not (pair[1] <= up < pair[0])] + up = min([j for j in C_dict.values() if j not in LowerFix]) + down = min(UnFixed, key=lambda j: matching_valuations_sum[get_key_by_value(value=j, items_dict=C_dict)]) + SoftFix = [pair for pair in SoftFix if not (pair[1] <= up < pair[0])] logger.debug(f"UpperFix: {UpperFix}, LowerFix: {LowerFix}, SoftFix: {SoftFix}, UnFixed: {UnFixed}") - if (len(match[up]) == 1) or (matching_valuations_sum[up] <= matching_valuations_sum[down]): + if (len(integer_match[up]) == 1) or (matching_valuations_sum[get_key_by_value(value=up, items_dict=C_dict)] <= matching_valuations_sum[get_key_by_value(value=down, items_dict=C_dict)]): LowerFix.append(up) logger.info(f"Added {up} to LowerFix") else: #check the lowest-rank student who currently belongs to mu(c_{down-1}) - agant_to_demote = int(get_lowest_ranked_student(str(int(down)-1), match, items_valuations)) - _match = Demote(match, agant_to_demote, up, down) - #creating for each match it's leximin tuple - _match_leximin_tuple = create_leximin_tuple(match=_match, agents_valuations=agents_valuations, items_valuations=items_valuations) - match_leximin_tuple = create_leximin_tuple(match=match, agents_valuations=agents_valuations, items_valuations=items_valuations) - if is_bigger_leximin(match_leximin_tuple, _match_leximin_tuple): - match = _match - matching_valuations_sum = update_matching_valuations_sum(match=match,items_valuations=items_valuations, agents=S, items=C) - logger.debug(f"Match updated: {match}") - elif sourceDec(_match, match) == up: + agant_to_demote = get_lowest_ranked_student(down-1, integer_match, items_valuations, C_dict, S_dict) + _match = Demote(integer_match, agant_to_demote, up_index=up, down_index=down) + _match_str = integer_to_str_matching(integer_match=_match, agent_dict=S_dict, items_dict=C_dict) + + #Creating a leximin tuple for the new match from the demote and for the old match to compare + _match_leximin_tuple = create_leximin_tuple(match=_match_str, agents_valuations=agents_valuations, items_valuations=items_valuations) + match_leximin_tuple = create_leximin_tuple(match=str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + + if compare_leximin(old_match_leximin_tuple=match_leximin_tuple, new_match_leximin_tuple=_match_leximin_tuple): + integer_match = _match + str_match = integer_to_str_matching(integer_match=integer_match, agent_dict=S_dict, items_dict=C_dict) + matching_valuations_sum = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) + logger.debug(f"Match updated: {str_match}") + + elif sourceDec(_match_leximin_tuple, match_leximin_tuple) == up: LowerFix.append(up) UpperFix.append(up + 1) logger.info(f"Updated LowerFix and UpperFix with {up}") - elif sourceDec(_match, match) in alloc.instance.agents: - t = match[sourceDec(_match, match)] + + elif sourceDec(_match_leximin_tuple, match_leximin_tuple) in alloc.instance.agents: + t = C_dict[get_match(match=str_match, value=sourceDec(_match_leximin_tuple, match_leximin_tuple))] LowerFix.append(t) UpperFix.append(t+1) A = [j for j in UnFixed if (j > t + 1)] SoftFix.extend((j, t+1) for j in A) logger.info(f"Updated LowerFix and UpperFix with {t}") + else: - match, LowerFix, UpperFix, SoftFix = LookAheadRoutine((S, C, agents_valuations, items_valuations), match, down, LowerFix, UpperFix, SoftFix) - logger.debug(f"LookAheadRoutine result: match={match}, LowerFix={LowerFix}, UpperFix={UpperFix}, SoftFix={SoftFix}") - UnFixed = [ - j for j in alloc.instance.items - if (j not in UpperFix) or - any((j, _j) not in SoftFix for _j in alloc.instance.items if _j > j) - ] + str_match, LowerFix, UpperFix, SoftFix = LookAheadRoutine((S, C, agents_valuations, items_valuations), integer_match, down, LowerFix, UpperFix, SoftFix) + logger.debug(f"LookAheadRoutine result: match={str_match}, LowerFix={LowerFix}, UpperFix={UpperFix}, SoftFix={SoftFix}") + + UnFixed = [ + j for j in C_dict.values() + if (j not in UpperFix) or + any((j, _j) not in SoftFix for _j in C_dict.values() if _j > j) + ] logger.info("Finished FaStGen algorithm") - return match + return str_match #We want to return the final march in his string form -def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list, SoftFix:list)->tuple: +def LookAheadRoutine(I:tuple, match:dict, down:int, LowerFix:list, UpperFix:list, SoftFix:list)->tuple: """ Algorithem 4-LookAheadRoutine: Designed to handle cases where a decrease in the leximin value may be balanced by future changes in the pairing, @@ -128,142 +125,134 @@ def LookAheadRoutine(I:tuple, match:dict, down:str, LowerFix:list, UpperFix:list >>> from fairpyx.adaptors import divide >>> S = ["s1", "s2", "s3", "s4", "s5"] >>> C = ["c1", "c2", "c3", "c4"] - >>> V = { #the colleges valuations - "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10}, - "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26}, - "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28}, - "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15}} - >>> U = { #the students valuations - "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, - "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, - "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, - "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, - "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}} - >>> I = (S, C, U ,V) - >>> match = { - "c1" : ["s1","s2"], - "c2" : ["s3","s5"], - "c3" : ["s4"], - "c4" : []} - >>> down = "c4" - >>> LowerFix = [] + >>> V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10},"c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26},"c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15}} + >>> U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5},"s2" : {"c1":36,"c2":20,"c3":10,"c4":1},"s3" : {"c1":29,"c2":24,"c3":12,"c4":10},"s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}} + >>> match = {1 : [1,2],2 : [3,5],3 : [4],4 : []} + >>> I = (S,C,U,V) + >>> down = 4 + >>> LowerFix = [1] >>> UpperFix = [] >>> SoftFix = [] >>> LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) - ({"c1": ["s1", "s2"], "c2": ["s5"], "c3" : ["s3"], "c4" : ["s4"]}, ["c1"], [], []) - """ + ({'c1': ['s1', 's2'], 'c2': ['s5'], 'c3': ['s3'], 'c4': ['s4']}, [1], [], []) + """ agents, items, agents_valuations, items_valuations = I + agents_dict = generate_dictionary(agents) + items_dict = generate_dictionary(items) LF = LowerFix.copy() UF = UpperFix.copy() _match = match.copy() + str_match = integer_to_str_matching(integer_match=_match, items_dict=items_dict, agent_dict=agents_dict) + giving_str_match = integer_to_str_matching(integer_match=match, items_dict=items_dict, agent_dict=agents_dict) logger.info("Starting LookAheadRoutine") - logger.debug(f"Initial parameters - match: {match}, down: {down}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") - - matching_valuations_sum = update_matching_valuations_sum(match=_match,items_valuations=items_valuations, agents=agents, items=items) - - while len(LF) + len([item for item in UF if item not in LF]) < len(items) - 1: - up = min([j for j in items if j not in LowerFix]) + logger.debug(f"Initial parameters - match: {str_match}, down: {down}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") + matching_valuations_sum = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) + while len(LF) + len([item for item in UF if item not in LF]) < len(items): + up = min([j for j in items_dict.values() if j not in LowerFix]) logger.debug(f"Selected 'up': {up}") - - if (len(match[up]) == 1) or (matching_valuations_sum[up] <= matching_valuations_sum[down]): + if (len(_match[up]) == 1) or (matching_valuations_sum[get_key_by_value(value=up, items_dict=items_dict)] <= matching_valuations_sum[get_key_by_value(value=down, items_dict=items_dict)]): LF.append(up) logger.info(f"Appended {up} to LowerFix") else: #check the lowest-rank student who currently belongs to mu(c_{down-1}) - agant_to_demote = get_lowest_ranked_student(down-1, match, items_valuations) + agant_to_demote = get_lowest_ranked_student(item=down-1, match=_match, items_valuations=items_valuations, items_dict=items_dict, agent_dict=agents_dict) logger.debug(f"Agent to demote: {agant_to_demote}") - _match = Demote(_match, agant_to_demote, up, down) - matching_valuations_sum = update_matching_valuations_sum(match=_match,items_valuations=items_valuations, agents=agents, items=items) - #creating for each match it's leximin tuple - _match_leximin_tuple = create_leximin_tuple(match=_match, agents_valuations=agents_valuations, items_valuations=items_valuations) - match_leximin_tuple = create_leximin_tuple(match=match, agents_valuations=agents_valuations, items_valuations=items_valuations) - if is_bigger_leximin(match_leximin_tuple, _match_leximin_tuple): + _match = Demote(_match, agant_to_demote, up_index=up, down_index=down) + str_match = integer_to_str_matching(integer_match=_match, items_dict=items_dict, agent_dict=agents_dict) + matching_valuations_sum = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) + + new_match_leximin_tuple = create_leximin_tuple(match=str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + old_match_leximin_tuple = create_leximin_tuple(match=giving_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + if compare_leximin(old_match_leximin_tuple=old_match_leximin_tuple, new_match_leximin_tuple=new_match_leximin_tuple): match = _match LowerFix = LF UpperFix = UF logger.info("Updated match and fixed LowerFix and UpperFix") break - elif sourceDec(_match, match) == up: + elif sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple) == up: LF.append(up) UF.append(up + 1) logger.info(f"Appended {up} to LowerFix and {up+1} to UpperFix") - elif sourceDec(_match, match) in agents: - t = _match[sourceDec(_match, match)] + elif sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple) in agents: + t = items_dict[get_match(match=str_match, value=sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple))] if t == down: UpperFix.append(down) else: SoftFix.append((down, t)) logger.info(f"Appended {down} to UpperFix or SoftFix") break - + + final_match = integer_to_str_matching(integer_match=match, items_dict=items_dict, agent_dict=agents_dict) logger.info("Completed LookAheadRoutine") - logger.debug(f"Final result - match: {match}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") - return (match, LowerFix, UpperFix, SoftFix) + logger.debug(f"Final result - match: {final_match}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") + return (final_match, LowerFix, UpperFix, SoftFix) def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:dict): - """ - Create a leximin tuple from the given match, agents' valuations, and items' valuations. - - Args: - - match (dict): A dictionary where keys are items and values are lists of agents. - - agents_valuations (dict): A dictionary where keys are agents and values are dictionaries of item valuations. - - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. - - Returns: - - list: A sorted list of tuples representing the leximin tuple. - - Example: - >>> match = {"c1":["s1","s2","s3"], "c2":["s4"], "c3":["s5"], "c4":["s7","s6"]} - >>> items_valuations = { #the colleges valuations - "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} - } - >>> agents_valuations = { #the students valuations - "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, - "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, - "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, - "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, - "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, - "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, - "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} - } - >>> create_leximin_tuple(match, agents_valuations, items_valuations) - [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] - """ + # """ + # Create a leximin tuple from the given match, agents' valuations, and items' valuations. + + # Args: + # - match (dict): A dictionary where keys are items and values are lists of agents. + # - agents_valuations (dict): A dictionary where keys are agents and values are dictionaries of item valuations. + # - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. + + # Returns: + # - list: A sorted list of tuples representing the leximin tuple. + + # Example: + # >>> match = {"c1":["s1","s2","s3"], "c2":["s4"], "c3":["s5"], "c4":["s7","s6"]} + # >>> items_valuations = { #the colleges valuations + # "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + # "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + # "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + # "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + # } + # >>> agents_valuations = { #the students valuations + # "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, + # "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, + # "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, + # "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, + # "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, + # "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, + # "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} + # } + # >>> create_leximin_tuple(match, agents_valuations, items_valuations) + # [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] + # """ leximin_tuple = [] for item in match.keys(): + if len(match[item]) == 0: + leximin_tuple.append((item, 0)) for agent in match[item]: leximin_tuple.append((agent,items_valuations[item][agent])) leximin_tuple.append((item, agents_valuations[agent][item])) leximin_tuple.sort(key = lambda x: x[1]) return leximin_tuple -def is_bigger_leximin(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->bool: - """ - Determine whether the leximin tuple of the new match is greater or equal to the leximin tuple of the old match. +def compare_leximin(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->bool: + # """ + # Determine whether the leximin tuple of the new match is greater or equal to the leximin tuple of the old match. - Args: - - new_match_leximin_tuple (list): The leximin tuple of the new matching. - - old_match_leximin_tuple (list): The leximin tuple of the old matching. + # Args: + # - new_match_leximin_tuple (list): The leximin tuple of the new matching. + # - old_match_leximin_tuple (list): The leximin tuple of the old matching. - Returns: - - bool: True if new_match_leximin_tuple >= old_match_leximin_tuple, otherwise False. + # Returns: + # - bool: True if new_match_leximin_tuple >= old_match_leximin_tuple, otherwise False. - Example: - >>> new_match = [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] - >>> old_match = [("s7",1),("c4",1),("s4",13),("c1",16),("c3",18),("c2",19),("s6",20),("s3",21),("s2",23),("s5",26),("c1",29),("c1",36),("c1",41),("s1",50)] - >>> compare_leximin(new_match, old_match) - False + # Example: + # >>> new_match = [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] + # >>> old_match = [("s7",1),("c4",1),("s4",13),("c1",16),("c3",18),("c2",19),("s6",20),("s3",21),("s2",23),("s5",26),("c1",29),("c1",36),("c1",41),("s1",50)] + # >>> compare_leximin(new_match, old_match) + # False - >>> new_match = [("c4",0),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] - >>> old_match = [("c4",3),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] - >>> compare_leximin(new_match, old_match) - True - """ + # >>> new_match = [("c4",0),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] + # >>> old_match = [("c4",3),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] + # >>> compare_leximin(new_match, old_match) + # True + # """ for k in range(0, len(new_match_leximin_tuple)): if new_match_leximin_tuple[k][1] == old_match_leximin_tuple[k][1]: continue @@ -273,154 +262,122 @@ def is_bigger_leximin(new_match_leximin_tuple:list, old_match_leximin_tuple:list return False def sourceDec(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->str: - """ - Determine the agent causing the leximin decrease between two matchings. - - Args: - - new_match_leximin_tuple (list): The leximin tuple of the new matching. - - old_match_leximin_tuple (list): The leximin tuple of the old matching. - - Returns: - - str: The agent (student) causing the leximin decrease. - - Example: - >>> new_match = [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] - >>> old_match = [("s7",1),("c4",1),("s4",13),("c1",16),("c3",18),("c2",19),("s6",20),("s3",21),("s2",23),("s5",26),("c1",29),("c1",36),("c1",41),("s1",50)] - >>> sourceDec(new_match, old_match) - 's6' - - >>> new_match = [("c4",3),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] - >>> old_match = [("c4",3),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] - >>> sourceDec(new_match, old_match) - 'c3' - """ + # """ + # Determine the agent causing the leximin decrease between two matchings. + + # Args: + # - new_match_leximin_tuple (list): The leximin tuple of the new matching. + # - old_match_leximin_tuple (list): The leximin tuple of the old matching. + + # Returns: + # - str: The agent (student) causing the leximin decrease. + + # Example: + # >>> new_match = [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] + # >>> old_match = [("s7",1),("c4",1),("s4",13),("c1",16),("c3",18),("c2",19),("s6",20),("s3",21),("s2",23),("s5",26),("c1",29),("c1",36),("c1",41),("s1",50)] + # >>> sourceDec(new_match, old_match) + # 's6' + + # >>> new_match = [("c4",3),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] + # >>> old_match = [("c4",3),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] + # >>> sourceDec(new_match, old_match) + # 'c3' + # """ for k in range(0, len(new_match_leximin_tuple)): if new_match_leximin_tuple[k][1] < old_match_leximin_tuple[k][1]: return new_match_leximin_tuple[k][0] return "" -def get_lowest_ranked_student(item, match:dict, items_valuations:dict)->str: - """ - Get the lowest ranked student for a given item. - - Args: - - item: The item for which the lowest ranked student is to be found. - - match (dict): A dictionary where keys are items and values are lists of agents. - - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. - - Returns: - - str: The lowest ranked student for the given item. - - Example: - >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} - >>> items_valuations = { #the colleges valuations - "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} - } - >>> get_lowest_ranked_student("c3", match, items_valuations) - 's6' - """ - # min = sys.maxsize - # lowest_ranked_student = 0 - # for agant in match[item]: - # minTemp = items_valuations[item][agant] - # if minTemp < min: - # min = minTemp - # lowest_ranked_student = agant - # return lowest_ranked_student - return min(match[item], key=lambda agant: items_valuations[item][agant]) - -def update_matching_valuations_sum(match:dict, items_valuations:dict, agents:list, items:list)->dict: - """ - Update the sum of valuations for each item in the matching. - - Args: - - match (dict): A dictionary where keys are items and values are lists of agents. - - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. - - agents (list): List of agents. - - items (list): List of items. - - Returns: - - dict: A dictionary with the sum of valuations for each item. - - Example: - >>> match = {c1:[s1,s2,s3,s4], c2:[s5], c3:[s6], c4:[s7]} - >>> items_valuations = { #the colleges valuations - "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} - } - >>> agents = ["s1","s2","s3","s4","s5","s6","s7"] - >>> items = ["c1","c2","c3","c4"] - >>> update_matching_valuations_sum(match, items_valuations, agents, items) - {"c1": 107, "c2": 26, "c3": 20, "c4": 1} - """ +def get_lowest_ranked_student(item, match:dict, items_valuations:dict, items_dict:dict, agent_dict:dict): + # """ + # Get the lowest ranked student for a given item. + + # Args: + # - item: The item for which the lowest ranked student is to be found. + # - match (dict): A dictionary where keys are items and values are lists of agents. + # - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. + + # Returns: + # - str: The lowest ranked student for the given item. + + # Example: + # >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} + # >>> items_valuations = { #the colleges valuations + # "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + # "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + # "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + # "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + # } + # >>> get_lowest_ranked_student("c3", match, items_valuations) + # 's6' + # """ + return min(match[item], key=lambda agant: items_valuations[get_key_by_value(value=item, items_dict=items_dict)][get_key_by_value(value=agant, items_dict=agent_dict)]) + +def update_matching_valuations_sum(match:dict, items_valuations:dict)->dict: + # """ + # Update the sum of valuations for each item in the matching. + + # Args: + # - match (dict): A dictionary where keys are items and values are lists of agents. + # - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. + # - agents (list): List of agents. + # - items (list): List of items. + + # Returns: + # - dict: A dictionary with the sum of valuations for each item. + + # Example: + # >>> match = {c1:[s1,s2,s3,s4], c2:[s5], c3:[s6], c4:[s7]} + # >>> items_valuations = { #the colleges valuations + # "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + # "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + # "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + # "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + # } + # >>> agents = ["s1","s2","s3","s4","s5","s6","s7"] + # >>> items = ["c1","c2","c3","c4"] + # >>> update_matching_valuations_sum(match, items_valuations, agents, items) + # {"c1": 107, "c2": 26, "c3": 20, "c4": 1} + # """ matching_valuations_sum = { #in the artical it looks like this: vj(mu) colleague: sum(items_valuations[colleague][student] for student in students) for colleague, students in match.items() } return matching_valuations_sum -def create_stable_matching(agents, items): - """ - Create a stable matching of agents to items. - - Args: - - agents_size (int): The number of agents. - - items_size (int): The number of items. - - Returns: - - dict: A dictionary representing the stable matching. - - Example: - >>> create_stable_matching(7, 4) - {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} - """ +def create_stable_matching(agents, agents_dict, items, items_dict): # Initialize the matching dictionary matching = {} # Assign the first m-1 students to c1 - matching[items[0]] = {agents[i] for i in range(0, len(agents) - len(items) + 1)} + matching[items_dict[items[0]]] = [agents_dict[agents[i]] for i in range(0, len(agents) - len(items) + 1)] # Assign the remaining students to cj for j >= 2 for j in range(1, len(items)): - matching[items[j]] = {agents[len(agents) - (len(items) - j)]} + matching[items_dict[items[j]]] = [agents_dict[agents[len(agents) - (len(items) - j)]]] return matching +def generate_dictionary(input_list:list)->dict: + return {item: index + 1 for index, item in enumerate(input_list)} -if __name__ == "__main__": - # import doctest, sys - # print(doctest.testmod()) - # Define the instance - S = ["1", "2", "3", "4", "5", "6", "7"] - C = ["1", "2", "3", "4"] - V = { #the colleges valuations - "1" : {"1":50,"2":23,"3":21,"4":13,"5":10,"6":6,"7":5}, - "2" : {"1":45,"2":40,"3":32,"4":29,"5":26,"6":11,"7":4}, - "3" : {"1":90,"2":79,"3":60,"4":35,"5":28,"6":20,"7":15}, - "4" : {"1":80,"2":48,"3":36,"4":29,"5":15,"6":6,"7":1} - } - U = { #the students valuations - "1" : {"1":16,"2":10,"3":6,"4":5}, - "2" : {"1":36,"2":20,"3":10,"4":1}, - "3" : {"1":29,"2":24,"3":12,"4":10}, - "4" : {"1":41,"2":24,"3":5,"4":3}, - "5" : {"1":36,"2":19,"3":9,"4":6}, - "6" :{"1":39,"2":30,"3":18,"4":7}, - "7" : {"1":40,"2":29,"3":6,"4":1} - } - +def get_key_by_value(value, items_dict): + return next(key for key, val in items_dict.items() if val == value) + +def integer_to_str_matching(integer_match:dict, agent_dict:dict, items_dict:dict)->dict: + # Reverse the s_dict and c_dict to map integer values back to their string keys + s_reverse_dict = {v: k for k, v in agent_dict.items()} + c_reverse_dict = {v: k for k, v in items_dict.items()} - # Assuming `Instance` can handle student and course preferences directly - instance = Instance(agents=S, items=C, valuations=U) - - allocation = AllocationBuilder(instance) - # Run the FaStGen algorithm - match = FaStGen(allocation, items_valuations=V) - print(match) - # Define the expected allocation (this is hypothetical; you should set it based on the actual expected output) - expected_allocation = {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} \ No newline at end of file + # Create the new dictionary with string keys and lists of string values + return {c_reverse_dict[c_key]: [s_reverse_dict[s_val] for s_val in s_values] for c_key, s_values in integer_match.items()} + +def get_match(match:dict, value:str)->any: + if value in match.keys(): + return match[value] + else: + return next((key for key, values_list in match.items() if value in values_list), None) + +if __name__ == "__main__": + import doctest, sys + print(doctest.testmod()) \ No newline at end of file From b4be7c0a5ee492cc91aa50b1b610b3cf786b8439 Mon Sep 17 00:00:00 2001 From: Erel Segal-Halevi Date: Wed, 10 Jul 2024 15:33:22 +0300 Subject: [PATCH 025/111] comments --- .../Optimization_Matching/FaStGen.py | 261 +++++++++++------- 1 file changed, 161 insertions(+), 100 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index 0f25c3c..719c448 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -7,6 +7,7 @@ from fairpyx import Instance, AllocationBuilder, ExplanationLogger from FaSt import Demote +from copy import deepcopy #import sys import logging @@ -35,82 +36,97 @@ def FaStGen(alloc: AllocationBuilder, items_valuations:dict)->dict: C = alloc.instance.items agents_valuations = alloc.instance._valuations #Convert the list of the agents and item to dictionary so that each agent\item will have its coresponding integer - S_dict = generate_dictionary(S) - C_dict = generate_dictionary(C) + student_name_to_int = generate_dict_from_str_to_int(S) + college_name_to_int = generate_dict_from_str_to_int(C) + int_to_student_name = generate_dict_from_int_to_str(S) + int_to_college_name = generate_dict_from_int_to_str(C) #Creating a match of the integers and the string coresponding one another to deal with the demote function and the leximin tuple as well - integer_match = create_stable_matching(agents=S, items=C, agents_dict=S_dict, items_dict=C_dict) - str_match = integer_to_str_matching(integer_match=integer_match, agent_dict=S_dict, items_dict=C_dict) + integer_match = create_stable_matching(agents=S, items=C, agents_dict=student_name_to_int, items_dict=college_name_to_int) + str_match = integer_to_str_matching(integer_match=integer_match, agent_dict=student_name_to_int, items_dict=college_name_to_int) logger.debug(f"Initial match: {str_match}") - UpperFix = [C_dict[C[0]]] - LowerFix = [C_dict[C[len(C)-1]]] + UpperFix = [college_name_to_int[C[0]]] # Inidces of colleges to which we cannot add any more students. + LowerFix = [college_name_to_int[C[len(C)-1]]] # Inidces of colleges from which we cannot remove any more students. SoftFix = [] - UnFixed = [item for item in C_dict.values() if item not in UpperFix] + UnFixed = [item for item in college_name_to_int.values() if item not in UpperFix] #creating a dictionary of vj(µ) = Pi∈µ(cj) for each j in C - matching_valuations_sum = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) + matching_college_valuations = update_matching_valuations_sum(match=str_match, items_valuations=items_valuations) + logger.debug(f"matching_college_valuations: {matching_college_valuations}") while len(LowerFix) + len([item for item in UpperFix if item not in LowerFix]) < len(C): - up = min([j for j in C_dict.values() if j not in LowerFix]) - down = min(UnFixed, key=lambda j: matching_valuations_sum[get_key_by_value(value=j, items_dict=C_dict)]) + logger.debug(f"\nstr_match: {str_match}, integer_match: {integer_match}, UpperFix: {UpperFix}, LowerFix: {LowerFix}, SoftFix: {SoftFix}, UnFixed: {UnFixed}") + up = min([j for j in college_name_to_int.values() if j not in LowerFix]) + down = min(UnFixed, key=lambda j: matching_college_valuations[int_to_college_name[j]]) + logger.debug(f"up: {up}, down: {down}") SoftFix = [pair for pair in SoftFix if not (pair[1] <= up < pair[0])] - logger.debug(f"UpperFix: {UpperFix}, LowerFix: {LowerFix}, SoftFix: {SoftFix}, UnFixed: {UnFixed}") - - if (len(integer_match[up]) == 1) or (matching_valuations_sum[get_key_by_value(value=up, items_dict=C_dict)] <= matching_valuations_sum[get_key_by_value(value=down, items_dict=C_dict)]): + logger.debug(f"Updating SoftFix to {SoftFix}") + + logger.debug(f"vup(mu)={matching_college_valuations[int_to_college_name[up]]}, vdown(mu)={matching_college_valuations[int_to_college_name[down]]}") + if (len(integer_match[up]) == 1) or (matching_college_valuations[int_to_college_name[up]] <= matching_college_valuations[int_to_college_name[down]]): LowerFix.append(up) - logger.info(f"Added {up} to LowerFix") + logger.info(f"Cannot remove any more students from c_{up}: Added c_{up} to LowerFix") else: #check the lowest-rank student who currently belongs to mu(c_{down-1}) - agant_to_demote = get_lowest_ranked_student(down-1, integer_match, items_valuations, C_dict, S_dict) - _match = Demote(integer_match, agant_to_demote, up_index=up, down_index=down) - _match_str = integer_to_str_matching(integer_match=_match, agent_dict=S_dict, items_dict=C_dict) + agant_to_demote = get_lowest_ranked_student(down-1, integer_match, items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) + new_integer_match = Demote(deepcopy(integer_match), agant_to_demote, up_index=up, down_index=down) + logger.info(f"Demoting from {up} to {down}, starting at student {agant_to_demote}. New match: {new_integer_match}") + new_match_str = integer_to_str_matching(integer_match=new_integer_match, agent_dict=student_name_to_int, items_dict=college_name_to_int) #Creating a leximin tuple for the new match from the demote and for the old match to compare - _match_leximin_tuple = create_leximin_tuple(match=_match_str, agents_valuations=agents_valuations, items_valuations=items_valuations) - match_leximin_tuple = create_leximin_tuple(match=str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + match_leximin_tuple = create_leximin_tuple(match=str_match , agents_valuations=agents_valuations, items_valuations=items_valuations) + logger.info(f"Old match leximin tuple: {match_leximin_tuple}") + new_match_leximin_tuple = create_leximin_tuple(match=new_match_str, agents_valuations=agents_valuations, items_valuations=items_valuations) + logger.info(f"New match leximin tuple: {new_match_leximin_tuple}") - if compare_leximin(old_match_leximin_tuple=match_leximin_tuple, new_match_leximin_tuple=_match_leximin_tuple): - integer_match = _match - str_match = integer_to_str_matching(integer_match=integer_match, agent_dict=S_dict, items_dict=C_dict) - matching_valuations_sum = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) - logger.debug(f"Match updated: {str_match}") + if is_leximin_at_least(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=match_leximin_tuple): + logger.debug(f"New match is leximin-better than old match:") + integer_match = new_integer_match + str_match = new_match_str + matching_college_valuations = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) + logger.debug(f" Match updated to {str_match}") - elif sourceDec(_match_leximin_tuple, match_leximin_tuple) == up: + elif sourceDec(new_match_leximin_tuple, match_leximin_tuple) == up: + logger.debug(f"New match is leximin-worse because of c_up = c_{up}:") LowerFix.append(up) UpperFix.append(up + 1) - logger.info(f"Updated LowerFix and UpperFix with {up}") + logger.info(f" Updated LowerFix and UpperFix with {up}") - elif sourceDec(_match_leximin_tuple, match_leximin_tuple) in alloc.instance.agents: - t = C_dict[get_match(match=str_match, value=sourceDec(_match_leximin_tuple, match_leximin_tuple))] + elif sourceDec(new_match_leximin_tuple, match_leximin_tuple) in alloc.instance.agents: + sd = sourceDec(new_match_leximin_tuple, match_leximin_tuple) + logger.debug(f"New match is leximin-worse because of student {sd}: ") + t = college_name_to_int[get_match(match=str_match, value=sd)] LowerFix.append(t) UpperFix.append(t+1) + logger.debug(f" sourceDec student {sd} is matched to c_t = c_{t}: adding c_{t} to LowerFix and c_{t+1} to UpperFix.") A = [j for j in UnFixed if (j > t + 1)] SoftFix.extend((j, t+1) for j in A) - logger.info(f"Updated LowerFix and UpperFix with {t}") + logger.debug(f" Updating SoftFix to {SoftFix}") else: + logger.debug(f"New match is leximin-worse because of college {sourceDec(new_match_leximin_tuple, match_leximin_tuple)}: ") str_match, LowerFix, UpperFix, SoftFix = LookAheadRoutine((S, C, agents_valuations, items_valuations), integer_match, down, LowerFix, UpperFix, SoftFix) - logger.debug(f"LookAheadRoutine result: match={str_match}, LowerFix={LowerFix}, UpperFix={UpperFix}, SoftFix={SoftFix}") + logger.debug(f" LookAheadRoutine result: match={str_match}, LowerFix={LowerFix}, UpperFix={UpperFix}, SoftFix={SoftFix}") UnFixed = [ - j for j in C_dict.values() + j for j in college_name_to_int.values() if (j not in UpperFix) or - any((j, _j) not in SoftFix for _j in C_dict.values() if _j > j) + any((j, _j) not in SoftFix for _j in college_name_to_int.values() if _j > j) ] + logger.debug(f" Updating UnFixed to {UnFixed}") logger.info("Finished FaStGen algorithm") return str_match #We want to return the final march in his string form -def LookAheadRoutine(I:tuple, match:dict, down:int, LowerFix:list, UpperFix:list, SoftFix:list)->tuple: +def LookAheadRoutine(I:tuple, integer_match:dict, down:int, LowerFix:list, UpperFix:list, SoftFix:list)->tuple: """ Algorithem 4-LookAheadRoutine: Designed to handle cases where a decrease in the leximin value may be balanced by future changes in the pairing, the goal is to ensure that the sumi pairing will maintain a good leximin value or even improve it. - :param I: A presentation of the problem, aka a tuple that contain the list of students(S), the list of colleges(C) when the capacity of each college is n-1 where n is the number of students, student valuation function(U), college valuation function(V). @@ -121,7 +137,6 @@ def LookAheadRoutine(I:tuple, match:dict, down:int, LowerFix:list, UpperFix:list :param SoftFix: A set of temporary upper limits. *We will asume that in the colleges list in index 0 there is college 1 in index 1 there is coll - >>> from fairpyx.adaptors import divide >>> S = ["s1", "s2", "s3", "s4", "s5"] >>> C = ["c1", "c2", "c3", "c4"] @@ -137,57 +152,71 @@ def LookAheadRoutine(I:tuple, match:dict, down:int, LowerFix:list, UpperFix:list ({'c1': ['s1', 's2'], 'c2': ['s5'], 'c3': ['s3'], 'c4': ['s4']}, [1], [], []) """ agents, items, agents_valuations, items_valuations = I - agents_dict = generate_dictionary(agents) - items_dict = generate_dictionary(items) + + student_name_to_int = generate_dict_from_str_to_int(agents) + college_name_to_int = generate_dict_from_str_to_int(items) + int_to_student_name = generate_dict_from_int_to_str(agents) + int_to_college_name = generate_dict_from_int_to_str(items) + LF = LowerFix.copy() UF = UpperFix.copy() - _match = match.copy() - str_match = integer_to_str_matching(integer_match=_match, items_dict=items_dict, agent_dict=agents_dict) - giving_str_match = integer_to_str_matching(integer_match=match, items_dict=items_dict, agent_dict=agents_dict) + given_str_match = integer_to_str_matching(integer_match=integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) + new_integer_match = deepcopy(integer_match) + new_str_match = integer_to_str_matching(integer_match=new_integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) - logger.info("Starting LookAheadRoutine") - logger.debug(f"Initial parameters - match: {str_match}, down: {down}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") - matching_valuations_sum = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) + logger.info(f"Starting LookAheadRoutine. Initial parameters - match: {new_str_match}, down: {down}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") + matching_college_valuations = update_matching_valuations_sum(match=given_str_match,items_valuations=items_valuations) while len(LF) + len([item for item in UF if item not in LF]) < len(items): - up = min([j for j in items_dict.values() if j not in LowerFix]) - logger.debug(f"Selected 'up': {up}") - if (len(_match[up]) == 1) or (matching_valuations_sum[get_key_by_value(value=up, items_dict=items_dict)] <= matching_valuations_sum[get_key_by_value(value=down, items_dict=items_dict)]): + up = min([j for j in college_name_to_int.values() if j not in LowerFix]) + logger.debug(f" Selected 'up': {up}") + if (len(integer_match[up]) == 1) or (matching_college_valuations[int_to_college_name[up]] <= matching_college_valuations[int_to_college_name[down]]): LF.append(up) - logger.info(f"Appended {up} to LowerFix") + logger.info(f" Cannot remove any more students from c_{up}: appended c_{up} to LF") else: - #check the lowest-rank student who currently belongs to mu(c_{down-1}) - agant_to_demote = get_lowest_ranked_student(item=down-1, match=_match, items_valuations=items_valuations, items_dict=items_dict, agent_dict=agents_dict) - logger.debug(f"Agent to demote: {agant_to_demote}") - - _match = Demote(_match, agant_to_demote, up_index=up, down_index=down) - str_match = integer_to_str_matching(integer_match=_match, items_dict=items_dict, agent_dict=agents_dict) - matching_valuations_sum = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) + #check the lowest-rank student who currently belongs to mu(c_{down-1})d + agant_to_demote = get_lowest_ranked_student(item_int=down-1, match_int=new_integer_match, items_valuations=items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) + new_integer_match = Demote(new_integer_match, agant_to_demote, up_index=up, down_index=down) + logger.info(f" Demoting from {up} to {down}, starting at student {agant_to_demote}. New match: {new_integer_match}") + + new_str_match = integer_to_str_matching(integer_match=new_integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) + matching_college_valuations = update_matching_valuations_sum(match=new_str_match,items_valuations=items_valuations) - new_match_leximin_tuple = create_leximin_tuple(match=str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) - old_match_leximin_tuple = create_leximin_tuple(match=giving_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) - if compare_leximin(old_match_leximin_tuple=old_match_leximin_tuple, new_match_leximin_tuple=new_match_leximin_tuple): - match = _match + old_match_leximin_tuple = create_leximin_tuple(match=given_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + new_match_leximin_tuple = create_leximin_tuple(match=new_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + logger.info(f" Old match leximin tuple: {old_match_leximin_tuple}") + new_match_leximin_tuple = create_leximin_tuple(match=new_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + logger.info(f" New match leximin tuple: {new_match_leximin_tuple}") + + if is_leximin_at_least(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple): + logger.debug(f" New match is leximin-better than old match:") + integer_match = new_integer_match LowerFix = LF UpperFix = UF - logger.info("Updated match and fixed LowerFix and UpperFix") + logger.info(" Updated match and fixed LowerFix and UpperFix") break + elif sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple) == up: + logger.debug(f" New match is leximin-worse because of c_up = c_{up}:") LF.append(up) UF.append(up + 1) - logger.info(f"Appended {up} to LowerFix and {up+1} to UpperFix") + logger.info(f" Appended {up} to LF and {up+1} to UF") + elif sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple) in agents: - t = items_dict[get_match(match=str_match, value=sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple))] - if t == down: - UpperFix.append(down) - else: - SoftFix.append((down, t)) - logger.info(f"Appended {down} to UpperFix or SoftFix") - break - - final_match = integer_to_str_matching(integer_match=match, items_dict=items_dict, agent_dict=agents_dict) - logger.info("Completed LookAheadRoutine") - logger.debug(f"Final result - match: {final_match}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") - return (final_match, LowerFix, UpperFix, SoftFix) + sd = sourceDec(new_match_leximin_tuple, old_match_leximin_tuple) + logger.debug(f" New match is leximin-worse because of student {sd}: ") + t = college_name_to_int[get_match(match=new_str_match, value=sd)] + logger.debug(f" sourceDec student {sd} is matched to c_t = c_{t}.") + if t == down: + logger.debug(f" t=down={down}: adding c_{down} to UpperFix") # UF? + UpperFix.append(down) + else: + logger.info(f" t!=down: adding ({down},{t}) to SoftFix") + SoftFix.append((down, t)) + break + + final_match_str = integer_to_str_matching(integer_match=integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) + logger.info(f"Completed LookAheadRoutine. Final result - match: {final_match_str}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") + return (final_match_str, LowerFix, UpperFix, SoftFix) def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:dict): # """ @@ -231,8 +260,8 @@ def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:di leximin_tuple.sort(key = lambda x: x[1]) return leximin_tuple -def compare_leximin(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->bool: - # """ +def is_leximin_at_least(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->bool: + """ # Determine whether the leximin tuple of the new match is greater or equal to the leximin tuple of the old match. # Args: @@ -243,16 +272,24 @@ def compare_leximin(new_match_leximin_tuple:list, old_match_leximin_tuple:list)- # - bool: True if new_match_leximin_tuple >= old_match_leximin_tuple, otherwise False. # Example: - # >>> new_match = [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] - # >>> old_match = [("s7",1),("c4",1),("s4",13),("c1",16),("c3",18),("c2",19),("s6",20),("s3",21),("s2",23),("s5",26),("c1",29),("c1",36),("c1",41),("s1",50)] - # >>> compare_leximin(new_match, old_match) - # False - - # >>> new_match = [("c4",0),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] - # >>> old_match = [("c4",3),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] - # >>> compare_leximin(new_match, old_match) - # True - # """ + >>> new_match = [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] + >>> old_match = [("s7",1),("c4",1),("s4",13),("c1",16),("c3",18),("c2",19),("s6",20),("s3",21),("s2",23),("s5",26),("c1",29),("c1",36),("c1",41),("s1",50)] + >>> is_leximin_at_least(new_match, old_match) + False + + >>> new_match = [("c4",0),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] + >>> old_match = [("c4",3),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] + >>> is_leximin_at_least(new_match, old_match) + False + + >>> new_match = [("c4",3),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] + >>> old_match = [("c4",0),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] + >>> is_leximin_at_least(new_match, old_match) + True + + >>> is_leximin_at_least(new_match, new_match) + True + """ for k in range(0, len(new_match_leximin_tuple)): if new_match_leximin_tuple[k][1] == old_match_leximin_tuple[k][1]: continue @@ -260,6 +297,7 @@ def compare_leximin(new_match_leximin_tuple:list, old_match_leximin_tuple:list)- return True else: return False + return True def sourceDec(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->str: # """ @@ -288,9 +326,9 @@ def sourceDec(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->str: return new_match_leximin_tuple[k][0] return "" -def get_lowest_ranked_student(item, match:dict, items_valuations:dict, items_dict:dict, agent_dict:dict): - # """ - # Get the lowest ranked student for a given item. +def get_lowest_ranked_student(item_int:int, match_int:dict, items_valuations:dict, int_to_college_name:dict, int_to_student_name:dict): + """ + Get the lowest ranked student that is matched to the item with the given index. # Args: # - item: The item for which the lowest ranked student is to be found. @@ -301,17 +339,23 @@ def get_lowest_ranked_student(item, match:dict, items_valuations:dict, items_dic # - str: The lowest ranked student for the given item. # Example: - # >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} - # >>> items_valuations = { #the colleges valuations - # "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - # "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - # "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - # "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} - # } - # >>> get_lowest_ranked_student("c3", match, items_valuations) - # 's6' - # """ - return min(match[item], key=lambda agant: items_valuations[get_key_by_value(value=item, items_dict=items_dict)][get_key_by_value(value=agant, items_dict=agent_dict)]) + >>> match = {1:[1,2,3,4], 2:[5], 3:[6], 4:[7]} + >>> items_valuations = { #the colleges valuations + ... "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + ... "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + ... "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + ... "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + ... } + >>> int_to_college_name = {i: f"c{i}" for i in [1,2,3,4]} + >>> int_to_student_name = {i: f"s{i}" for i in [1,2,3,4,5,6,7]} + >>> get_lowest_ranked_student(1, match, items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) + 4 + >>> get_lowest_ranked_student(2, match, items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) + 5 + >>> get_lowest_ranked_student(3, match, items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) + 6 + """ + return min(match_int[item_int], key=lambda agent: items_valuations[int_to_college_name[item_int]][int_to_student_name[agent]]) def update_matching_valuations_sum(match:dict, items_valuations:dict)->dict: # """ @@ -358,9 +402,12 @@ def create_stable_matching(agents, agents_dict, items, items_dict): return matching -def generate_dictionary(input_list:list)->dict: +def generate_dict_from_str_to_int(input_list:list)->dict: return {item: index + 1 for index, item in enumerate(input_list)} +def generate_dict_from_int_to_str(input_list:list)->dict: + return {index + 1: item for index, item in enumerate(input_list)} + def get_key_by_value(value, items_dict): return next(key for key, val in items_dict.items() if val == value) @@ -380,4 +427,18 @@ def get_match(match:dict, value:str)->any: if __name__ == "__main__": import doctest, sys - print(doctest.testmod()) \ No newline at end of file + # print(doctest.testmod()) + # doctest.run_docstring_examples(is_leximin_at_least, globals()) + # doctest.run_docstring_examples(get_lowest_ranked_student, globals()) + # sys.exit(0) + + logger.setLevel(logging.DEBUG) + logger.addHandler(logging.StreamHandler()) + from fairpyx.adaptors import divide + S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + C = ["c1", "c2", "c3", "c4"] + V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1}} + U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, "s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, "s7" : {"c1":40,"c2":29,"c3":6,"c4":1}} + ins = Instance(agents=S, items=C, valuations=U) + alloc = AllocationBuilder(instance=ins) + FaStGen(alloc=alloc, items_valuations=V) From 1fd08017d7207de9a4180fd8fa8f4a150862404e Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:23:44 +0300 Subject: [PATCH 026/111] Update FaSt.py --- .../algorithms/Optimization_Matching/FaSt.py | 154 ++++++++++-------- 1 file changed, 85 insertions(+), 69 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 9fd5e71..ac846d0 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -8,7 +8,9 @@ from fairpyx import Instance, AllocationBuilder, ExplanationLogger import logging -logger = logging.getLogger(__name__) +# Object to insert the relevant data +logger = logging.getLogger("data") + @@ -57,7 +59,7 @@ def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dic p -= 1 return matching #Return the matching after the change - +##ADD THAT CHANGE THE MATCHING (THE ORIGINAL)#####RETURN NULL AND CHANGE THE MATCHING##MAKE SURE WITH HADAR.#################### def get_leximin_tuple(matching, V): """ @@ -67,37 +69,30 @@ def get_leximin_tuple(matching, V): :param matching: The current matching dictionary :param V: The evaluations matrix :return: Leximin tuple + >>> matching = {1: [1, 2, 3, 4], 2: [5], 3: [7, 6]} + >>> V = [[9, 8, 7], [8, 7, 6], [7, 6, 5], [6, 5, 4], [5, 4, 3], [4, 3, 2], [3, 2, 1]] + >>> get_leximin_tuple(matching, V) + [1, 2, 3, 4, 4, 6, 7, 8, 9, 30] """ - leximin_tuple = [] - college_sums = [] - - # Iterate over each college in the matching - for college, students in matching.items(): - college_sum = 0 - # For each student in the college, append their valuation for the college to the leximin tuple - for student in students: - valuation = V[student - 1][college - 1] - leximin_tuple.append(valuation) - # Sum to add the valuations of colleges in the end of the tuple (according to Ex2) - college_sum += valuation - college_sums.append(college_sum) - # Append the college sums to the leximin tuple - leximin_tuple.extend(college_sums) - + leximin_tuple=get_unsorted_leximin_tuple(matching, V) # Sort the leximin tuple in descending order leximin_tuple.sort(reverse=False) return leximin_tuple - def get_unsorted_leximin_tuple(matching, V): """ Generate the leximin tuple based on the given matching and evaluations, including the sum of valuations for each college. + Using in calculate pos array. :param matching: The current matching dictionary :param V: The evaluations matrix :return: UNSORTED Leximin tuple + >>> matching = {1: [1, 2, 3, 4], 2: [5], 3: [7, 6]} + >>> V = [[9, 8, 7], [8, 7, 6], [7, 6, 5], [6, 5, 4], [5, 4, 3], [4, 3, 2], [3, 2, 1]] + >>> get_unsorted_leximin_tuple(matching, V) + [9, 8, 7, 6, 4, 1, 2, 30, 4, 3] """ leximin_tuple = [] college_sums = [] @@ -115,7 +110,7 @@ def get_unsorted_leximin_tuple(matching, V): leximin_tuple.extend(college_sums) return leximin_tuple - +## ADD START IND of SI IS 0## def build_pos_array(matching, V): """ Build the pos array based on the leximin tuple and the matching. @@ -128,6 +123,10 @@ def build_pos_array(matching, V): :param matching: The current matching dictionary :param V: The evaluations matrix :return: Pos array + >>> matching = {1: [1, 2, 3, 4], 2: [5], 3: [7, 6]} + >>> V = [[9, 8, 7], [8, 7, 6], [7, 6, 5], [6, 5, 4], [5, 4, 3], [4, 3, 2], [3, 2, 1]] + >>> build_pos_array(matching, V) + [8, 7, 6, 5, 3, 0, 1, 9, 3, 2] """ pos = [] # Initialize pos array student_index = 0 @@ -149,22 +148,6 @@ def build_pos_array(matching, V): return pos -def create_L(matching): - """ - Create the L list based on the matching. - :param matching: The current matching - :return: L list - """ - L = [] - - # Create a list of tuples (college, student) - for college, students in matching.items(): - for student in students: - L.append((college, student)) - - return L - - def build_college_values(matching, V): """ Build the college_values dictionary that sums the students' valuations for each college. @@ -175,6 +158,10 @@ def build_college_values(matching, V): :param matching: The current matching dictionary :param V: The evaluations matrix :return: College values dictionary + >>> matching = {1: [1, 2, 3, 4], 2: [5], 3: [7, 6]} + >>> V = [[9, 8, 7], [8, 7, 6], [7, 6, 5], [6, 5, 4], [5, 4, 3], [4, 3, 2], [3, 2, 1]] + >>> build_college_values(matching, V) + {1: 30, 2: 4, 3: 3} """ college_values = {} @@ -188,12 +175,16 @@ def build_college_values(matching, V): def initialize_matching(n, m): """ - Initialize the first stable matching. + Initialize the first stable matching. - :param n: Number of students - :param m: Number of colleges - :return: Initial stable matching - """ + :param n: Number of students + :param m: Number of colleges + :return: Initial stable matching + + >>> n = 7 + >>> m = 3 + >>> initialize_matching(n, m) + {1: [1, 2, 3, 4, 5], 2: [6], 3: [7]}""" initial_matching = {k: [] for k in range(1, m + 1)} # Create a dictionary for the matching # Assign the first (n - m + 1) students to the first college (c1) for student in range(1, n - m + 2): @@ -206,9 +197,13 @@ def initialize_matching(n, m): def convert_valuations_to_matrix(valuations): """ Convert the dictionary of valuations to a matrix format. + To be the same as in the algo . :param valuations: Dictionary of valuations :return: Matrix of valuations + >>> valuations={'S1': {'c1': 9, 'c2': 8, 'c3': 7}, 'S2': {'c1': 8, 'c2': 7, 'c3': 6}, 'S3': {'c1': 7, 'c2': 6, 'c3': 5}, 'S4': {'c1': 6, 'c2': 5, 'c3': 4}, 'S5': {'c1': 5, 'c2': 4, 'c3': 3}, 'S6': {'c1': 4, 'c2': 3, 'c3': 2}, 'S7': {'c1': 3, 'c2': 2, 'c3': 1}} + >>> convert_valuations_to_matrix(valuations) + [[9, 8, 7], [8, 7, 6], [7, 6, 5], [6, 5, 4], [5, 4, 3], [4, 3, 2], [3, 2, 1]] """ students = sorted(valuations.keys()) # Sort student keys to maintain order colleges = sorted(valuations[students[0]].keys()) # Sort college keys to maintain order @@ -248,56 +243,70 @@ def FaSt(alloc: AllocationBuilder)-> dict: # Now V look like this: # "Alice": {"c1":2, "c2": 3, "c3": 4}, # "Bob": {"c1": 4, "c2": 5, "c3": 6} + logger.info('FaSt(%g)',alloc) n=len(S)# number of students m = len(C) # number of colleges i = n - 1 # start from the last student - j = m - 1 # start from the last college + j = m # start from the last college # Initialize the first stable matching initial_matching = initialize_matching(n, m) # Convert Valuations to only numerical matrix V= convert_valuations_to_matrix(V) - + # Initialize the leximin tuple lex_tupl=get_leximin_tuple(initial_matching,V) -# Initialize the leximin tuple L and position array pos +# Initialize the position array pos pos= build_pos_array(initial_matching, V) - - L=create_L(initial_matching) - college_values=build_college_values(initial_matching,V) + logger.debug('Initial i:%g', i) + logger.debug('Initial j:%g', j) + print("i: ", i) print("j: ", j) index = 1 - while i > j - 1 and j > 0: - + while i > j - 1 and j > 1: + logger.debug('Iteration number %g', index) + logger.debug('Current i:%g', i) + logger.debug('Current j:%g ', j) print("******** Iteration number ", index, "********") print("i: ", i) print("j: ", j) - print("college_values[j+1]: ", college_values[j + 1]) - print("V[i-1][j]: ", V[i - 1][j]) + # logger.debug('college_values[j+1]: %g', college_values[j + 1])############################ + # print("college_values[j+1]: ", college_values[j + 1])##########################3 print("college_values: ", college_values) - if college_values[j + 1] >= V[i - 1][j]: ###need to update after each iteration + print("V:", V) + logger.debug('V[i-1][j-1]: %g', V[i - 1][j - 1]) + logger.debug('college_values: %g', college_values) + + print("V[i-1][j-1]: ", V[i - 1][j - 1]) + print("college_values[j]: ", college_values[j]) + # IMPORTANT! in the variable college_values we use in j and not j-1 because it build like this: {1: 35, 2: 3, 3: 1} + # So this: college_values[j] indeed gave us the right index ! [i.e. different structure!] + if college_values[j] >= V[i - 1][j-1]: # In the algo the college_values is actually v j -= 1 else: - if college_values[j + 1] < V[i - 1][j]: - print("V[i-1][j]:", V[i - 1][j]) + if college_values[j] < V[i - 1][j - 1]: #Raw 11- different indixes because of different structures. + logger.debug('V[i-1][j-1]: %g', V[i-1][j-1]) + print("V[i-1][j-1]:", V[i - 1][j-1]) # if V[i][j - 1] > L[j - 1]: - initial_matching = Demote(initial_matching, i, j + 1, 1) + initial_matching = Demote(initial_matching, i, j, 1) print("initial_matching after demote:", initial_matching) + logger.debug('initial_matching after demote: %g', initial_matching) + else: - if V[i][j - 1] < college_values[j]: + if V[i - 1][j - 1] < college_values[j]:#Raw 14 j -= 1 else: # Lookahead - k = i - t = pos[i] - µ_prime = initial_matching.copy() + k = i - 1 + t = pos[i - 1] + µ_prime = copy.deepcopy(initial_matching) # Deep copy while k > j - 1: - if V[k][j - 1] > L[t - 1]: + if V[k][j -1] > lex_tupl[t]: i = k - initial_matching = Demote(µ_prime, k, j, 1) + initial_matching = Demote(µ_prime, k, j - 1, 1) break - elif V[k][j - 1] < college_values[j]: + elif V[k][j - 1] < college_values[j]:# raw 24 in the article j -= 1 break else: @@ -306,24 +315,31 @@ def FaSt(alloc: AllocationBuilder)-> dict: t += 1 if k == j - 1 and initial_matching != µ_prime: j -= 1 + ### ADD UPDATE F######################################################################## # Updates college_values = build_college_values(initial_matching, V) # Update the college values - lex_tupl = get_leximin_tuple(initial_matching, V) + lex_tupl = get_leximin_tuple(initial_matching, V) ############################################ + logger.debug('lex_tupl: %g', lex_tupl) print("lex_tupl: ", lex_tupl) - L = create_L(initial_matching) - print("L:", L) - pos = build_pos_array(initial_matching, V) + pos = build_pos_array(initial_matching, V)############################################### + logger.debug('pos: %g', pos) print("pos:", pos) i -= 1 index += 1 - print("END while :") - print("i: ", i) - print("j: ", j) + logger.debug('END while, i: %g, j: %g',i, j) + + #print("END while :") + #print("i: ", i) + #print("j: ", j) return initial_matching if __name__ == "__main__": import doctest + console=logging.StreamHandler() #writes to stderr (= cerr) + logger.handlers=[console] # we want the logs to be written to console + # Change logger level + logger.setLevel(logging.INFO) doctest.testmod() From 0db69bbb6b68baedb07102b0ef6e94b167afd810 Mon Sep 17 00:00:00 2001 From: ErgaDN Date: Sun, 14 Jul 2024 18:57:17 +0300 Subject: [PATCH 027/111] for "Clearing error is None" - break and perform an empty division --- ...allocation_algorithms_ACEEI_Tabu_Search.py | 65 ++++--- .../results/course_allocation_szws.csv | 64 +++---- .../results/course_allocation_uniform.csv | 160 +++++++++++++++--- fairpyx/algorithms/ACEEI/ACEEI.py | 21 ++- 4 files changed, 221 insertions(+), 89 deletions(-) diff --git a/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py b/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py index b10e67e..cd14323 100644 --- a/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py +++ b/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py @@ -21,15 +21,18 @@ from fairpyx.algorithms.ACEEI.tabu_search import run_tabu_search algorithms_to_check = [ - ACEEI_without_EFTB, + # ACEEI_without_EFTB, ACEEI_with_EFTB, ACEEI_with_contested_EFTB, run_tabu_search, - # crs.iterated_maximum_matching_adjusted, - # crs.bidirectional_round_robin, + crs.iterated_maximum_matching_adjusted, + crs.bidirectional_round_robin, ] def evaluate_algorithm_on_instance(algorithm, instance): + # print(f" -!-!- instance._valuations = {instance._valuations} -!-!-") + # capacity = {course: instance.item_capacity(course) for course in instance.items} + # print(f" -!-!- capacity = {capacity} -!-!-") allocation = divide(algorithm, instance) matrix = AgentBundleValueMatrix(instance, allocation) matrix.use_normalized_values() @@ -107,10 +110,10 @@ def course_allocation_with_random_instance_szws( def run_szws_experiment(): # Run on SZWS simulated data: - experiment = experiments_csv.Experiment("results/", "course_allocation_szws.csv", backup_folder="results/backup/") + experiment = experiments_csv.Experiment("results/", "course_allocation_szws_ACEEI.csv", backup_folder="results/backup/") input_ranges = { - "num_of_agents": [5,10], - "num_of_items": [6], # in SZWS: 25 + "num_of_agents": [5, 10, 15], + "num_of_items": [4, 8], # in SZWS: 25 "agent_capacity": [5], # as in SZWS "supply_ratio": [1.1, 1.25, 1.5], # as in SZWS "num_of_popular_items": [6, 9], # as in SZWS @@ -159,7 +162,26 @@ def run_ariel_experiment(): } experiment.run_with_time_limit(course_allocation_with_random_instance_sample, input_ranges, time_limit=TIME_LIMIT) +import pandas as pd +import matplotlib.pyplot as plt +# Function to load experiment results from CSV +def load_experiment_results(filename): + df = pd.read_csv(filename) + return df + +# Function to plot average runtime vs. number of students +def plot_average_runtime_vs_students(df, algorithm_name): + average_runtime = df.groupby('num_of_agents')['runtime'].mean() + num_of_agents = average_runtime.index + runtime = average_runtime.values + + plt.plot(num_of_agents, runtime, marker='o', label=algorithm_name) + plt.xlabel('Number of Students') + plt.ylabel('Average Runtime (seconds)') + plt.title(f'Average Runtime vs. Number of Students ({algorithm_name})') + plt.legend() + plt.grid(True) ######### MAIN PROGRAM ########## @@ -169,30 +191,6 @@ def run_ariel_experiment(): run_uniform_experiment() run_szws_experiment() # run_ariel_experiment() - # - - import pandas as pd - import matplotlib.pyplot as plt - - - # Function to load experiment results from CSV - def load_experiment_results(filename): - df = pd.read_csv(filename) - return df - - - # Function to plot runtime vs. number of students - def plot_runtime_vs_students(df, algorithm_name): - num_of_agents = df['num_of_agents'] - runtime = df['runtime'] - - plt.plot(num_of_agents, runtime, marker='o', label=algorithm_name) - plt.xlabel('Number of Students') - plt.ylabel('Runtime (seconds)') - plt.title(f'Runtime vs. Number of Students ({algorithm_name})') - plt.legend() - plt.grid(True) - # Load and plot data for run_uniform_experiment() uniform_results = load_experiment_results('results/course_allocation_uniform.csv') @@ -201,21 +199,20 @@ def plot_runtime_vs_students(df, algorithm_name): for algorithm in algorithms_to_check: algorithm_name = algorithm.__name__ algorithm_data = uniform_results[uniform_results['algorithm'] == algorithm_name] - plot_runtime_vs_students(algorithm_data, algorithm_name) + plot_average_runtime_vs_students(algorithm_data, algorithm_name) plt.tight_layout() plt.show() # Load and plot data for run_szws_experiment() - szws_results = load_experiment_results('results/course_allocation_szws.csv') + szws_results = load_experiment_results('results/course_allocation_szws_ACEEI.csv') plt.figure(figsize=(10, 6)) # Adjust figure size if needed for algorithm in algorithms_to_check: algorithm_name = algorithm.__name__ algorithm_data = szws_results[szws_results['algorithm'] == algorithm_name] - plot_runtime_vs_students(algorithm_data, algorithm_name) + plot_average_runtime_vs_students(algorithm_data, algorithm_name) plt.tight_layout() plt.show() - diff --git a/experiments/results/course_allocation_szws.csv b/experiments/results/course_allocation_szws.csv index ae91e2f..66e8a01 100644 --- a/experiments/results/course_allocation_szws.csv +++ b/experiments/results/course_allocation_szws.csv @@ -1716,7 +1716,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 100,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,1,75.55428421305076,43.32688588007737,49.09819639278557,20.8775309593512,0,0.0,33,38,39,0.029544299992267 100,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,2,74.65595878222157,44.67213114754098,46.55963302752294,22.47071407859002,0,0.0,33,34,34,0.0276106999954208 100,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,3,73.99974543016157,46.98544698544699,53.01455301455301,22.98979223627492,0,0.0,31,32,36,0.0281615999992936 -100,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,4,75.6538787590335,42.85714285714285,49.14893617021278,21.070494627658316,0,0.0,32,35,36,0.0283601999981328 +100,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,4,75.6538787590335,42.85714285714285,49.14893617021278,21.07049462765832,0,0.0,32,35,36,0.0283601999981328 100,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",round_robin,0,81.61506675610414,60.526315789473685,12.841530054644808,1.214114099734815,0,0.0,100,100,100,0.026696000015363 100,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",round_robin,1,81.20039674763473,62.903225806451616,12.024048096192388,1.6097921469375132,0,0.0,100,100,100,0.027284099953249 100,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",round_robin,2,80.62387391708727,67.32456140350878,9.633027522935777,1.2504444670853316,0,0.0,100,100,100,0.026694200001657 @@ -1777,20 +1777,20 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 100,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,2,95.3533087053234,83.65122615803816,7.629427792915521,0.1699124011249716,0,0.0,98,100,100,3.760512200009544 100,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,3,94.86775229815848,85.61946902654867,5.735660847880297,0.237312101113917,0,0.0,97,100,100,3.6752663000370376 100,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,4,94.49649212223233,84.27518427518427,7.526881720430111,0.1543475860607361,0,0.0,97,100,100,3.715827399981208 -100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,0,88.37596254548055,72.58064516129032,16.17647058823529,1.8326020476733291,0,0.0,96,98,98,0.1147959000081755 +100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,0,88.37596254548055,72.58064516129032,16.17647058823529,1.832602047673329,0,0.0,96,98,98,0.1147959000081755 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,1,88.68336105076305,69.12568306010928,15.123456790123456,1.4426847821961448,0,0.0,95,96,96,0.1100748000317253 -100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,2,89.03995729658382,64.43298969072166,20.103092783505147,1.8304046030144476,0,0.0,94,95,95,0.12204919999931 +100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,2,89.03995729658382,64.43298969072166,20.103092783505147,1.830404603014448,0,0.0,94,95,95,0.12204919999931 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,3,88.26355412684512,69.73684210526315,18.684210526315795,2.1870704161696755,0,0.0,93,95,95,0.122071899997536 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,4,88.57399016322364,72.7536231884058,18.23708206686929,1.5667564173500523,0,0.0,93,95,96,0.1203136999974958 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,0,87.64277030305146,75.50607287449392,7.356321839080465,0.0735632183908046,0,0.0,100,100,100,0.207210800028406 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,1,87.90293903857197,74.29193899782135,0.0,0.0,0,0.0,100,100,100,0.1990224000182934 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,2,88.3625579190454,76.90677966101694,2.083333333333343,0.0208333333333334,0,0.0,100,100,100,0.2077723999973386 -100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,3,87.41903379484373,77.40492170022371,1.9607843137254972,0.0196078431372549,0,0.0,100,100,100,0.2019275000202469 +100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,3,87.41903379484373,77.40492170022371,1.9607843137254968,0.0196078431372549,0,0.0,100,100,100,0.2019275000202469 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,4,87.87196875304755,75.93360995850622,1.1976047904191631,0.0119760479041916,0,0.0,100,100,100,0.2021414999617263 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,0,87.64277030305146,75.50607287449392,7.356321839080465,0.0735632183908046,0,0.0,100,100,100,0.2046333000180311 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,1,87.90293903857197,74.29193899782135,0.0,0.0,0,0.0,100,100,100,0.2030532999779097 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,2,88.3625579190454,76.90677966101694,2.083333333333343,0.0208333333333334,0,0.0,100,100,100,0.2083516999846324 -100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,3,87.41903379484373,77.40492170022371,1.9607843137254972,0.0196078431372549,0,0.0,100,100,100,0.2737088000285439 +100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,3,87.41903379484373,77.40492170022371,1.9607843137254968,0.0196078431372549,0,0.0,100,100,100,0.2737088000285439 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,4,87.87196875304755,75.93360995850622,1.1976047904191631,0.0119760479041916,0,0.0,100,100,100,0.2071685999981127 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",serial_dictatorship,0,79.56971977396223,42.29249011857708,56.07287449392713,15.18445925173305,0,0.0,48,51,53,0.028897300013341 100,25,5,1.1,9,3.85,"(50, 100)","(0, 50)",serial_dictatorship,1,78.79061031636826,42.462311557788944,53.01507537688443,16.20103564779142,0,0.0,48,51,52,0.0300406999886035 @@ -1941,7 +1941,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,1,91.21942620878484,72.48520710059172,12.721893491124264,1.0923811642760408,0,0.0,96,98,98,0.1051684999838471 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,2,91.50419079514504,74.71910112359551,17.69662921348315,1.3034337116040944,0,0.0,98,99,99,0.1071667000069283 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,3,90.7269941006346,69.73684210526315,18.684210526315795,2.0948057305473724,0,0.0,94,95,95,0.1098421000060625 -100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,4,90.9697749796041,75.98784194528876,20.638820638820636,1.7792587968252862,0,0.0,96,97,97,0.1062646000063978 +100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,4,90.9697749796041,75.98784194528876,20.63882063882064,1.7792587968252862,0,0.0,96,97,97,0.1062646000063978 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,0,90.42709582590028,77.34204793028321,5.977011494252892,0.2406262864329051,0,0.0,100,100,100,0.2017462000367231 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,1,90.71497117087132,80.07590132827325,6.93069306930694,0.1164601851765853,0,0.0,100,100,100,0.2079617999843321 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,2,91.1634670468718,80.73394495412845,5.581395348837219,0.0849424638668826,0,0.0,100,100,100,0.2077202999498695 @@ -1971,7 +1971,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,1,91.02009508468818,81.83807439824945,9.358288770053464,0.6100736388756135,0,0.0,100,100,100,3.7684393999516033 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,2,91.09983615486568,79.49308755760369,9.452736318407958,0.6482075266696243,0,0.0,100,100,100,3.7868599999928847 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,3,90.40238097275127,80.0,12.149532710280369,0.6209955405724046,0,0.0,98,99,99,3.747194299998228 -100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,4,90.78483580915982,76.54867256637168,9.25449871465294,0.6683484379336214,0,0.0,97,100,100,3.8687032000161703 +100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,4,90.78483580915982,76.54867256637168,9.25449871465294,0.6683484379336214,0,0.0,97,100,100,3.8687032000161694 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,0,90.67349474258468,80.96446700507614,10.427807486631025,0.7533620238499813,0,0.0,99,100,100,3.7799345000530593 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,1,91.12202502761971,82.27146814404432,9.921671018276754,0.5527601379596027,0,0.0,99,100,100,3.711948099953588 100,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,2,91.08044264875572,79.77011494252874,14.285714285714278,0.6805101782266709,0,0.0,99,100,100,3.727884699997958 @@ -2034,7 +2034,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 100,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,4,87.66739407907521,73.84615384615385,5.599999999999994,0.2110422707023697,0,0.0,100,100,100,0.2998904000269249 100,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,0,80.12503625781198,44.534412955465584,55.06072874493927,17.9556047410143,0,0.0,45,48,48,0.0280791000113822 100,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,1,80.64277408816763,48.49699398797595,49.09819639278557,17.265326779383365,0,0.0,47,49,50,0.0272844999562948 -100,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,2,79.01526838881641,50.50301810865191,46.55963302752294,18.997858064290917,0,0.0,44,44,45,0.0262802999932318 +100,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,2,79.01526838881641,50.50301810865191,46.55963302752294,18.99785806429092,0,0.0,44,44,45,0.0262802999932318 100,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,3,78.90221255399143,46.98544698544699,53.01455301455301,19.115403694421776,0,0.0,44,46,48,0.0248305000131949 100,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,4,80.3001637677358,42.85714285714285,49.31506849315068,17.615596576215054,0,0.0,43,45,47,0.0270913999993354 100,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",round_robin,0,86.91928384637573,63.73390557939914,11.007025761124112,0.8226823816665797,0,0.0,100,100,100,0.026147500029765 @@ -2103,12 +2103,12 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,3,94.23404300812784,80.0,8.16901408450704,0.7659305682020799,0,0.0,98,99,99,0.0986308999708853 100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,4,94.40838997103434,75.98784194528876,24.01215805471124,1.1275087333743858,0,0.0,97,98,98,0.1009276999975554 100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,0,94.0121625807498,86.61971830985915,4.427083333333314,0.1323590288162297,0,0.0,100,100,100,0.2841342000174336 -100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,1,94.32369911457435,86.71023965141612,1.8372703412073437,0.0366921824187976,0,0.0,100,100,100,0.2025335000362247 +100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,1,94.32369911457435,86.71023965141612,1.837270341207344,0.0366921824187976,0,0.0,100,100,100,0.2025335000362247 100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,2,94.51803935944886,87.67123287671232,4.511278195488728,0.0880140994039673,0,0.0,100,100,100,0.2036411999724805 100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,3,93.5541659795278,82.58928571428571,2.743142144638398,0.0656228822412282,0,0.0,100,100,100,0.1968189000035636 100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,4,94.1873047513372,85.48387096774194,9.701492537313428,0.278339103067892,0,0.0,100,100,100,0.2040672000148333 100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,0,94.0121625807498,86.61971830985915,4.427083333333314,0.1323590288162297,0,0.0,100,100,100,0.3056519000092521 -100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,1,94.32369911457435,86.71023965141612,1.8372703412073437,0.0366921824187976,0,0.0,100,100,100,0.2118226999882608 +100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,1,94.32369911457435,86.71023965141612,1.837270341207344,0.0366921824187976,0,0.0,100,100,100,0.2118226999882608 100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,2,94.51803935944886,87.67123287671232,4.511278195488728,0.0880140994039673,0,0.0,100,100,100,0.2098478999687358 100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,3,93.5541659795278,82.58928571428571,2.743142144638398,0.0656228822412282,0,0.0,100,100,100,0.207793700043112 100,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,4,94.1873047513372,85.48387096774194,9.701492537313428,0.278339103067892,0,0.0,100,100,100,0.2099673000047914 @@ -2194,7 +2194,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,4,82.77419524938936,68.7960687960688,10.561797752808983,1.6300103303131308,0,0.0,200,200,200,0.6274078999995254 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,0,75.56335155385105,46.875,49.14163090128756,22.31256354463468,0,0.0,65,71,71,0.0806565000093542 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,1,75.99974773450161,48.61751152073733,49.567099567099575,22.10292253842733,0,0.0,69,70,71,0.0776422999915666 -200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,2,74.15669102736379,36.234458259325045,63.76554174067496,24.002224313650636,0,0.0,65,66,66,0.0767040000064298 +200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,2,74.15669102736379,36.234458259325045,63.76554174067496,24.00222431365064,0,0.0,65,66,66,0.0767040000064298 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,3,74.70486225782845,43.43629343629344,51.93050193050192,23.523013963447507,0,0.0,64,67,69,0.0768796000047586 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,4,75.49080112948269,42.85714285714285,55.77299412915851,22.738917677269704,0,0.0,66,67,68,0.080289100005757 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",round_robin,0,81.62682300348021,63.73390557939914,14.024390243902443,2.474136882189849,0,0.0,200,200,200,0.0795854000025428 @@ -2206,7 +2206,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",bidirectional_round_robin,1,81.33466530398334,62.903225806451616,11.617312072892943,2.2658928720280525,0,0.0,200,200,200,0.0778894000104628 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",bidirectional_round_robin,2,80.6005762309216,62.95503211991434,12.83185840707965,2.376392099269366,0,0.0,200,200,200,0.0788017999730072 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",bidirectional_round_robin,3,81.13358028353274,63.1578947368421,13.84615384615384,2.1625406068433493,0,0.0,200,200,200,0.0800681999535299 -200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",bidirectional_round_robin,4,81.49091381153882,61.839530332681015,14.468085106382986,2.5996218418135357,0,0.0,200,200,200,0.0812450000084936 +200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",bidirectional_round_robin,4,81.49091381153882,61.839530332681015,14.468085106382986,2.599621841813536,0,0.0,200,200,200,0.0812450000084936 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,0,82.78327287037297,67.09844559585493,19.444444444444457,2.647328635870427,0,0.0,175,181,181,8.161837699997704 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,1,82.40035396740838,68.90380313199105,20.698924731182785,2.6718751133961054,0,0.0,181,186,186,7.934783999982756 200,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,2,81.4739035684889,66.58595641646488,23.486682808716708,2.898037416690331,0,0.0,178,186,186,7.977408999984618 @@ -2223,12 +2223,12 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",utilitarian_matching,3,95.19253756329546,75.59681697612733,17.664670658682624,0.7021058210034048,0,0.0,188,193,197,0.2641950000543147 200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",utilitarian_matching,4,95.6285194632036,82.63473053892216,9.281437125748496,0.2549762720170945,0,0.0,198,198,199,0.2681760999839753 200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,0,94.72262829612411,83.11965811965813,5.035971223021576,0.0734992275525728,0,0.0,200,200,200,0.4712708999868482 -200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,1,94.88484272963932,82.16704288939052,3.9267015706806343,0.0329695304983183,0,0.0,200,200,200,0.4639628999866545 +200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,1,94.88484272963932,82.16704288939052,3.9267015706806334,0.0329695304983183,0,0.0,200,200,200,0.4639628999866545 200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,2,94.9364128318822,79.90074441687345,0.0,0.0,0,0.0,200,200,200,0.4728078999905847 200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,3,94.68378798704094,80.16877637130801,3.982300884955748,0.0604938865879083,0,0.0,200,200,200,0.4706644000252709 200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,4,95.1132457818247,82.4742268041237,8.851674641148335,0.1505958128338,0,0.0,200,200,200,0.4753381999908015 200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,0,94.72262829612411,83.11965811965813,5.035971223021576,0.0734992275525728,0,0.0,200,200,200,0.4911124999634921 -200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,1,94.88484272963932,82.16704288939052,3.9267015706806343,0.0329695304983183,0,0.0,200,200,200,0.486203700012993 +200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,1,94.88484272963932,82.16704288939052,3.9267015706806334,0.0329695304983183,0,0.0,200,200,200,0.486203700012993 200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,2,94.9364128318822,79.90074441687345,0.0,0.0,0,0.0,200,200,200,0.6627411000081338 200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,3,94.68378798704094,80.16877637130801,3.982300884955748,0.0604938865879083,0,0.0,200,200,200,0.479436399997212 200,25,5,1.1,9,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,4,95.1132457818247,82.4742268041237,8.851674641148335,0.1505958128338,0,0.0,200,200,200,0.4815866000135429 @@ -2339,7 +2339,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 200,25,5,1.25,6,2.6,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,4,92.19066036015025,79.64824120603015,11.420612813370468,0.954143969423088,0,0.0,191,193,200,8.380367400008254 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",utilitarian_matching,0,85.33890311365296,61.458333333333336,26.30208333333332,5.573683754752485,0,0.0,172,176,176,0.2627882999950088 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",utilitarian_matching,1,84.8336170235696,60.31331592689296,28.35820895522388,7.131983841795546,0,0.0,167,172,172,0.2597390000009909 -200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",utilitarian_matching,2,83.93086150892121,64.84375,30.024813895781637,8.129491093317801,0,0.0,170,173,174,0.253229400026612 +200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",utilitarian_matching,2,83.93086150892121,64.84375,30.02481389578164,8.129491093317801,0,0.0,170,173,174,0.253229400026612 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",utilitarian_matching,3,84.5010887728544,62.77173913043478,33.5195530726257,6.056199678517271,0,0.0,179,179,179,0.2551558000268414 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",utilitarian_matching,4,85.06841893935434,62.30366492146597,28.97727272727272,6.0191189849506,0,0.0,172,172,172,0.2614659999962896 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,0,85.13344794209947,70.31630170316302,10.165484633569733,1.3437634639384286,0,0.0,200,200,200,0.4861956000095233 @@ -2352,7 +2352,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,2,83.89745333619926,68.75,10.79136690647482,1.5251651285000196,0,0.0,200,200,200,0.6916359000024386 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,3,84.45880922459408,70.09345794392523,10.098522167487673,1.3397104096482726,0,0.0,200,200,200,0.4855215999996289 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,4,84.91092201287267,71.46067415730337,11.111111111111114,1.2377180176227316,0,0.0,200,200,200,0.4989804999786429 -200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,0,77.46472578748066,48.31223628691983,47.85992217898832,20.747034274245134,0,0.0,77,79,80,0.0782337999553419 +200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,0,77.46472578748066,48.31223628691983,47.85992217898832,20.74703427424513,0,0.0,77,79,80,0.0782337999553419 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,1,77.63644741988841,48.61751152073733,50.5952380952381,20.898868098863776,0,0.0,78,79,80,0.077538400015328 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,2,76.32387978984406,36.234458259325045,63.76554174067496,22.03235784589797,0,0.0,75,76,77,0.0786682000034488 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,3,76.56456678483575,43.43629343629344,55.01930501930502,21.823553425949243,0,0.0,76,78,80,0.0787051999941468 @@ -2367,7 +2367,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",bidirectional_round_robin,2,82.64467490863952,67.32456140350878,13.348946135831383,2.255061417186358,0,0.0,200,200,200,0.0787484999746084 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",bidirectional_round_robin,3,83.43217616223791,63.1578947368421,11.392405063291136,1.9519022619220283,0,0.0,200,200,200,0.0791248999885283 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",bidirectional_round_robin,4,83.51261981157923,61.839530332681015,14.468085106382986,2.303638712885994,0,0.0,200,200,200,0.0793260000064037 -200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,0,84.89778603906156,71.84684684684684,18.090452261306535,2.1840106033808917,0,0.0,185,189,189,8.130346899968572 +200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,0,84.89778603906156,71.84684684684684,18.09045226130653,2.184010603380892,0,0.0,185,189,189,8.130346899968572 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,1,84.47041194746024,70.13333333333334,20.53333333333332,2.621260732981193,0,0.0,186,189,189,8.294714499963447 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,2,83.76935587348797,69.55503512880561,23.382045929018787,2.962306316442057,0,0.0,187,191,191,8.130298300005961 200,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,3,84.1408919021938,70.74829931972789,22.777777777777786,2.4056978962989746,0,0.0,190,193,193,7.873634499963373 @@ -2535,7 +2535,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 200,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,0,88.240302330689,76.55502392344498,19.61722488038278,2.492805019225708,0,0.0,193,195,195,8.105190600035712 200,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,1,87.72482279890839,75.1111111111111,13.054830287206272,1.905244012861544,0,0.0,195,196,196,8.195641400001477 200,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,2,87.05593418378048,75.78125,13.380281690140848,2.337702126061905,0,0.0,195,198,198,8.222593199985567 -200,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,3,87.45962379984006,75.36585365853658,19.55307262569832,1.9827060225478916,0,0.0,196,197,197,8.141647000040393 +200,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,3,87.45962379984006,75.36585365853658,19.55307262569832,1.982706022547892,0,0.0,196,197,197,8.141647000040393 200,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,4,88.04116767531528,75.98152424942263,13.51981351981351,2.2901947819680912,0,0.0,200,200,200,8.299967400031164 200,25,5,1.5,9,2.6,"(50, 100)","(0, 50)",utilitarian_matching,0,99.05897259153896,92.5207756232687,2.506963788300837,0.0373363463761182,0,0.0,200,200,200,0.2477076000068336 200,25,5,1.5,9,2.6,"(50, 100)","(0, 50)",utilitarian_matching,1,98.96468286043702,91.64882226980728,5.217391304347828,0.1408042608464455,0,0.0,199,200,200,0.2481063000159338 @@ -2641,7 +2641,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 300,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",round_robin,1,89.27400511402298,69.07020872865274,12.975391498881436,1.63823434091175,0,0.0,300,300,300,0.1594217999954708 300,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",round_robin,2,88.93977896052405,62.75992438563327,14.957264957264954,2.019979804508316,0,0.0,300,300,300,0.152544200012926 300,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",round_robin,3,88.8812550505404,68.45360824742268,16.744186046511643,1.8126730427987927,0,0.0,300,300,300,0.158491599955596 -300,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",round_robin,4,89.02663027924241,68.31460674157303,14.769975786924944,1.8691662122520156,0,0.0,300,300,300,0.1627441000309772 +300,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",round_robin,4,89.02663027924241,68.31460674157303,14.769975786924944,1.869166212252016,0,0.0,300,300,300,0.1627441000309772 300,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",bidirectional_round_robin,0,89.15453049257067,70.28824833702882,16.861826697892283,1.820141568133923,0,0.0,300,300,300,0.165602300025057 300,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",bidirectional_round_robin,1,88.96673405493335,69.07020872865274,17.382413087934566,1.8359636664354293,0,0.0,300,300,300,0.1605513000395149 300,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",bidirectional_round_robin,2,88.84473153276109,62.75992438563327,12.5,1.878126440527337,0,0.0,300,300,300,0.1667011000099592 @@ -2689,7 +2689,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 300,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",bidirectional_round_robin,4,81.57135048018809,61.839530332681015,14.421252371916507,2.778074283118719,0,0.0,300,300,300,0.1584126999950967 300,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,0,82.32607530713096,68.48958333333334,27.34375,3.2118381227613524,0,0.0,272,279,279,12.676167800033 300,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,1,82.46353291930463,66.95095948827291,22.9381443298969,3.8060913086026487,0,0.0,270,277,278,12.7228441000334 -300,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,2,82.0594588329246,67.13947990543736,23.209876543209877,3.995023834935862,0,0.0,272,275,277,13.168412400002126 +300,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,2,82.0594588329246,67.13947990543736,23.20987654320988,3.995023834935862,0,0.0,272,275,277,13.168412400002126 300,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,3,82.27436324479855,66.40471512770138,21.1031175059952,3.165325579883347,0,0.0,277,282,282,13.245585500029849 300,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,4,82.52302450165756,67.5603217158177,20.7977207977208,3.447997088799698,0,0.0,272,279,279,12.9568975000293 300,25,5,1.1,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,0,82.38144759294828,69.14660831509846,20.7977207977208,3.240208676263699,0,0.0,273,276,276,13.553919299971312 @@ -2833,7 +2833,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,3,84.69134000467913,72.47706422018348,10.098522167487673,1.66963413265915,0,0.0,300,300,300,0.8658964000060223 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_adjusted,4,84.85490545389116,71.99074074074075,9.375,1.5349599573907335,0,0.0,300,300,300,1.1455448999768123 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,0,76.9966240062246,41.70040485829959,53.1496062992126,21.853793680197985,0,0.0,113,116,116,0.1540140999713912 -300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,1,77.5965849937523,44.57831325301205,55.42168674698795,21.256737872663518,0,0.0,115,117,120,0.1567158999969251 +300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,1,77.5965849937523,44.57831325301205,55.42168674698795,21.25673787266352,0,0.0,115,117,120,0.1567158999969251 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,2,76.91779939302224,36.234458259325045,63.76554174067496,21.89110787276072,0,0.0,114,117,121,0.156534899957478 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,3,76.94181095899269,43.43629343629344,55.01930501930502,21.742665428457407,0,0.0,112,117,120,0.1585993999615311 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",serial_dictatorship,4,77.22358550498136,45.601436265709154,52.96229802513466,21.75580956242816,0,0.0,112,116,118,0.1540533999796025 @@ -2850,7 +2850,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,0,84.52230807691095,70.31630170316302,20.044543429844097,3.161737361691262,0,0.0,280,284,284,13.138180200010538 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,1,84.54109254934386,70.13333333333334,23.09582309582308,2.9702978139206606,0,0.0,286,289,290,12.737744000041856 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,2,84.28821050126908,71.0762331838565,22.22222222222221,3.0776694252573584,0,0.0,286,288,288,12.245447600027546 -300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,3,84.39390691449232,70.33707865168539,20.620842572062077,2.953456299724706,0,0.0,291,292,292,12.821678600041196 +300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,3,84.39390691449232,70.33707865168539,20.62084257206208,2.953456299724706,0,0.0,291,292,292,12.821678600041196 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,4,84.63360500893116,70.37861915367483,19.51219512195121,2.9819599236055083,0,0.0,288,291,291,12.43295859999489 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,0,84.54810082695397,70.31630170316302,20.044543429844097,3.003001379154697,0,0.0,278,284,284,12.811049100011587 300,25,5,1.25,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,1,84.54346883108616,70.90517241379311,21.333333333333343,2.8001834303311783,0,0.0,285,289,290,13.04236340004718 @@ -2901,7 +2901,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 300,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,1,91.25915737629052,69.62750716332378,23.34293948126802,2.346187786039772,0,0.0,291,292,292,0.4628923999844119 300,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,2,90.9946556531222,73.71007371007371,18.37837837837837,2.6286545069921288,0,0.0,291,295,295,0.457198399992194 300,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,3,91.13969819136702,72.8125,19.43661971830987,2.759831960110701,0,0.0,290,293,293,0.4494042999576777 -300,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,4,91.66044243092756,73.71428571428571,18.28571428571429,1.8973170981999092,0,0.0,297,297,297,0.4227488999604247 +300,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",utilitarian_matching,4,91.66044243092756,73.71428571428571,18.28571428571429,1.8973170981999088,0,0.0,297,297,297,0.4227488999604247 300,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,0,90.30258587738804,79.91071428571429,12.222222222222214,0.6095551147542653,0,0.0,300,300,300,0.8411170999752358 300,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,1,90.8921537591387,80.95238095238095,6.93069306930694,0.5582997534011516,0,0.0,300,300,300,0.8349432999966666 300,25,5,1.25,9,3.85,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,2,90.6618079863019,80.73394495412845,8.92857142857143,0.5108100492958745,0,0.0,300,300,300,0.8296321000088938 @@ -3008,7 +3008,7 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 300,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",bidirectional_round_robin,3,86.84439138816523,63.1578947368421,12.783505154639172,1.2185141706329805,0,0.0,300,300,300,0.1523537000175565 300,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",bidirectional_round_robin,4,86.7356977283893,62.97872340425532,14.988290398126452,1.304005245232812,0,0.0,300,300,300,0.1574333000462502 300,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,0,87.79002384374746,74.8283752860412,21.689497716894977,2.9876988261126343,0,0.0,299,299,299,12.98451700003352 -300,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,1,87.79535055747195,76.53061224489795,17.164179104477626,2.9958001846539397,0,0.0,293,295,296,12.375850199954584 +300,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,1,87.79535055747195,76.53061224489795,17.164179104477626,2.99580018465394,0,0.0,293,295,296,12.375850199954584 300,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,2,87.58289248696057,75.57603686635944,15.43778801843318,2.886913127145628,0,0.0,290,294,294,12.665112700022291 300,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,3,87.67796499095391,75.87064676616916,17.1875,2.6062324573664744,0,0.0,298,300,300,12.932693699956872 300,25,5,1.5,6,3.85,"(50, 100)","(0, 50)",almost_egalitarian_without_donation,4,87.83427744946994,75.35885167464114,18.010752688172047,2.9564458292871034,0,0.0,297,298,298,12.388241999957245 @@ -3098,11 +3098,11 @@ num_of_agents,num_of_items,agent_capacity,supply_ratio,num_of_popular_items,mean 300,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,3,94.4752559892894,83.48214285714286,9.050772626931575,0.9005964772161119,0,0.0,298,300,300,12.719845000014177 300,25,5,1.5,9,3.85,"(50, 100)","(0, 50)",almost_egalitarian_with_donation,4,94.76965154539184,86.4693446088795,11.200000000000005,0.7632502270457473,0,0.0,300,300,300,12.896742700017056 100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",utilitarian_matching,0,91.01465052092666,75.58441558441558,19.61325966850829,1.6565634869489474,0,0.0,85,87,95,0.4999400000087917 -100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",utilitarian_matching,1,90.46266823624113,72.77777777777777,13.63636363636364,1.5098292614546187,0,0.0,83,84,91,0.5025423000333831 -100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",utilitarian_matching,2,90.66139355620881,74.71264367816092,15.34772182254197,1.1566266260879317,0,0.0,85,86,95,0.41050120000727475 -100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",utilitarian_matching,3,90.54056073413525,73.48066298342542,23.180592991913755,1.534518839391132,0,0.0,88,88,93,0.42110229993704706 -100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",utilitarian_matching,4,90.90706955039724,77.92553191489363,17.72486772486772,1.2599697463498045,0,0.0,86,86,93,0.41987139999400824 -100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,0,90.69800944892519,73.91304347826086,4.926108374384242,0.2530781252975517,0,0.0,100,100,100,0.7979893999872729 -100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,1,90.05898169298018,75.75057736720554,7.6530612244897895,0.28109584936391385,0,0.0,98,100,100,0.9945702999830246 -100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,2,90.31369025023268,76.63755458515283,4.092071611253203,0.24592381006821384,0,0.0,100,100,100,0.7193849000614136 -100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,3,90.17477726567944,76.06837606837607,6.43564356435644,0.27102276919757345,0,0.0,100,100,100,0.7424426999641582 +100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",utilitarian_matching,1,90.46266823624111,72.77777777777777,13.63636363636364,1.5098292614546187,0,0.0,83,84,91,0.5025423000333831 +100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",utilitarian_matching,2,90.6613935562088,74.71264367816092,15.34772182254197,1.1566266260879317,0,0.0,85,86,95,0.4105012000072747 +100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",utilitarian_matching,3,90.54056073413524,73.48066298342542,23.180592991913755,1.534518839391132,0,0.0,88,88,93,0.421102299937047 +100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",utilitarian_matching,4,90.90706955039724,77.92553191489363,17.72486772486772,1.2599697463498043,0,0.0,86,86,93,0.4198713999940082 +100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,0,90.6980094489252,73.91304347826086,4.926108374384242,0.2530781252975517,0,0.0,100,100,100,0.7979893999872729 +100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,1,90.05898169298018,75.75057736720554,7.653061224489789,0.2810958493639138,0,0.0,98,100,100,0.9945702999830246 +100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,2,90.31369025023268,76.63755458515283,4.092071611253203,0.2459238100682138,0,0.0,100,100,100,0.7193849000614136 +100,25,5,1.1,6,2.6,"(50, 100)","(0, 50)",iterated_maximum_matching_unadjusted,3,90.17477726567944,76.06837606837607,6.43564356435644,0.2710227691975734,0,0.0,100,100,100,0.7424426999641582 diff --git a/experiments/results/course_allocation_uniform.csv b/experiments/results/course_allocation_uniform.csv index 8793783..9aa38bc 100644 --- a/experiments/results/course_allocation_uniform.csv +++ b/experiments/results/course_allocation_uniform.csv @@ -639,23 +639,143 @@ num_of_agents,num_of_items,value_noise_ratio,algorithm,random_seed,utilitarian_v 8,4,0.2,iterated_maximum_matching_adjusted,2,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0109016000060364 8,4,0.2,iterated_maximum_matching_adjusted,3,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0065165000269189 8,4,0.2,iterated_maximum_matching_adjusted,4,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0059906999813392 -5,4,0.2,run_tabu_search,0,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0018102999310940504 -5,4,0.2,run_tabu_search,1,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0016278000548481941 -5,4,0.2,run_tabu_search,2,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0018075000261887908 -5,4,0.2,run_tabu_search,3,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.001779200043529272 -5,4,0.2,run_tabu_search,4,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0024667999241501093 -5,4,0.2,bidirectional_round_robin,0,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.00043070001993328333 -5,4,0.2,bidirectional_round_robin,1,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0009566000662744045 -5,4,0.2,bidirectional_round_robin,2,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0004254000959917903 -5,4,0.2,bidirectional_round_robin,3,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0004088999703526497 -5,4,0.2,bidirectional_round_robin,4,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.00040289992466568947 -8,4,0.2,run_tabu_search,0,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0022117000771686435 -8,4,0.2,run_tabu_search,1,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.002252400037832558 -8,4,0.2,run_tabu_search,2,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.002163599943742156 -8,4,0.2,run_tabu_search,3,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0023390999995172024 -8,4,0.2,run_tabu_search,4,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.003005199949257076 -8,4,0.2,bidirectional_round_robin,0,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0005970000056549907 -8,4,0.2,bidirectional_round_robin,1,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0010493999579921365 -8,4,0.2,bidirectional_round_robin,2,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0006288000149652362 -8,4,0.2,bidirectional_round_robin,3,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0006845999741926789 -8,4,0.2,bidirectional_round_robin,4,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0009022000012919307 +5,4,0.2,run_tabu_search,0,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.001810299931094 +5,4,0.2,run_tabu_search,1,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0016278000548481 +5,4,0.2,run_tabu_search,2,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0018075000261887 +5,4,0.2,run_tabu_search,3,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0017792000435292 +5,4,0.2,run_tabu_search,4,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0024667999241501 +5,4,0.2,bidirectional_round_robin,0,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0004307000199332 +5,4,0.2,bidirectional_round_robin,1,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0009566000662744 +5,4,0.2,bidirectional_round_robin,2,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0004254000959917 +5,4,0.2,bidirectional_round_robin,3,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0004088999703526 +5,4,0.2,bidirectional_round_robin,4,100.0,100.0,0.0,0.0,2,2.0,5,5,5,0.0004028999246656 +8,4,0.2,run_tabu_search,0,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0022117000771686 +8,4,0.2,run_tabu_search,1,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0022524000378325 +8,4,0.2,run_tabu_search,2,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0021635999437421 +8,4,0.2,run_tabu_search,3,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0023390999995172 +8,4,0.2,run_tabu_search,4,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.003005199949257 +8,4,0.2,bidirectional_round_robin,0,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0005970000056549 +8,4,0.2,bidirectional_round_robin,1,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0010493999579921 +8,4,0.2,bidirectional_round_robin,2,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0006288000149652 +8,4,0.2,bidirectional_round_robin,3,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0006845999741926 +8,4,0.2,bidirectional_round_robin,4,100.0,100.0,0.0,0.0,2,2.0,8,8,8,0.0009022000012919 +5,6,0.2,ACEEI_without_EFTB,0,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.1032566999783739 +5,6,0.2,ACEEI_without_EFTB,1,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0074490999104455 +5,6,0.2,ACEEI_without_EFTB,2,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0081060000229626 +5,6,0.2,ACEEI_without_EFTB,3,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0070445999735966 +5,6,0.2,ACEEI_without_EFTB,4,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.009740800014697 +5,6,0.2,ACEEI_with_EFTB,0,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0078571999911218 +5,6,0.2,ACEEI_with_EFTB,1,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0074447999941185 +5,6,0.2,ACEEI_with_EFTB,2,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0082609000382944 +5,6,0.2,ACEEI_with_EFTB,3,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0074551999568939 +5,6,0.2,ACEEI_with_EFTB,4,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0071453999262303 +5,6,0.2,ACEEI_with_contested_EFTB,0,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0078374999575316 +5,6,0.2,ACEEI_with_contested_EFTB,1,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0072668000357225 +5,6,0.2,ACEEI_with_contested_EFTB,2,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0073217999888584 +5,6,0.2,ACEEI_with_contested_EFTB,3,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0106656999560073 +5,6,0.2,ACEEI_with_contested_EFTB,4,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0109293999848887 +5,6,0.2,run_tabu_search,0,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0077432999387383 +5,6,0.2,run_tabu_search,1,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0066023999825119 +5,6,0.2,run_tabu_search,2,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.004988299915567 +5,6,0.2,run_tabu_search,3,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.004820300033316 +5,6,0.2,run_tabu_search,4,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0048400000669062 +8,6,0.2,ACEEI_without_EFTB,0,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.009573100018315 +8,6,0.2,ACEEI_without_EFTB,1,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0124118999810889 +8,6,0.2,ACEEI_without_EFTB,2,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0087465000106021 +8,6,0.2,ACEEI_without_EFTB,3,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0117914000293239 +8,6,0.2,ACEEI_without_EFTB,4,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0083237000508233 +8,6,0.2,ACEEI_with_EFTB,0,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0083484000060707 +8,6,0.2,ACEEI_with_EFTB,1,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0085145999910309 +8,6,0.2,ACEEI_with_EFTB,2,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0088424999266862 +8,6,0.2,ACEEI_with_EFTB,3,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0079494999954476 +8,6,0.2,ACEEI_with_EFTB,4,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0104706999845802 +8,6,0.2,ACEEI_with_contested_EFTB,0,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0112147000618278 +8,6,0.2,ACEEI_with_contested_EFTB,1,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0094426999567076 +8,6,0.2,ACEEI_with_contested_EFTB,2,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0117905000224709 +8,6,0.2,ACEEI_with_contested_EFTB,3,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0095511999679729 +8,6,0.2,ACEEI_with_contested_EFTB,4,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0092981000198051 +8,6,0.2,run_tabu_search,0,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0093028000555932 +8,6,0.2,run_tabu_search,1,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0080950000556185 +8,6,0.2,run_tabu_search,2,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0079149000812321 +8,6,0.2,run_tabu_search,3,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0100786000257357 +8,6,0.2,run_tabu_search,4,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0078744000056758 +10,4,0.2,ACEEI_without_EFTB,0,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0102428999962285 +10,4,0.2,ACEEI_without_EFTB,1,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0087871000869199 +10,4,0.2,ACEEI_without_EFTB,2,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.009943600045517 +10,4,0.2,ACEEI_without_EFTB,3,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0067794000497087 +10,4,0.2,ACEEI_without_EFTB,4,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0086940999608486 +10,4,0.2,ACEEI_with_EFTB,0,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0099980999948456 +10,4,0.2,ACEEI_with_EFTB,1,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0098744999850168 +10,4,0.2,ACEEI_with_EFTB,2,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0088371000019833 +10,4,0.2,ACEEI_with_EFTB,3,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0119272000156342 +10,4,0.2,ACEEI_with_EFTB,4,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0098998999455943 +10,4,0.2,ACEEI_with_contested_EFTB,0,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0084606000455096 +10,4,0.2,ACEEI_with_contested_EFTB,1,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0069877000059932 +10,4,0.2,ACEEI_with_contested_EFTB,2,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0092869000509381 +10,4,0.2,ACEEI_with_contested_EFTB,3,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0080760999117046 +10,4,0.2,ACEEI_with_contested_EFTB,4,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0092641999945044 +10,4,0.2,run_tabu_search,0,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0044351000105962 +10,4,0.2,run_tabu_search,1,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.003530399990268 +10,4,0.2,run_tabu_search,2,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0036937999539077 +10,4,0.2,run_tabu_search,3,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0029859000351279 +10,4,0.2,run_tabu_search,4,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0030036000534892 +10,6,0.2,ACEEI_without_EFTB,0,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0120642000110819 +10,6,0.2,ACEEI_without_EFTB,1,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0079886999446898 +10,6,0.2,ACEEI_without_EFTB,2,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0101704999106004 +10,6,0.2,ACEEI_without_EFTB,3,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0083410999504849 +10,6,0.2,ACEEI_without_EFTB,4,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0101151000708341 +10,6,0.2,ACEEI_with_EFTB,0,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.009154999977909 +10,6,0.2,ACEEI_with_EFTB,1,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0104319000383839 +10,6,0.2,ACEEI_with_EFTB,2,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0089503000490367 +10,6,0.2,ACEEI_with_EFTB,3,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0130765000358223 +10,6,0.2,ACEEI_with_EFTB,4,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0079753999598324 +10,6,0.2,ACEEI_with_contested_EFTB,0,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0096413999563083 +10,6,0.2,ACEEI_with_contested_EFTB,1,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0086482999613508 +10,6,0.2,ACEEI_with_contested_EFTB,2,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0140445999568328 +10,6,0.2,ACEEI_with_contested_EFTB,3,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0096911001019179 +10,6,0.2,ACEEI_with_contested_EFTB,4,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0113552999682724 +10,6,0.2,run_tabu_search,0,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0159305999986827 +10,6,0.2,run_tabu_search,1,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0092136000748723 +10,6,0.2,run_tabu_search,2,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0099282000446692 +10,6,0.2,run_tabu_search,3,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0093474999302998 +10,6,0.2,run_tabu_search,4,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0100785000249743 +5,6,0.2,iterated_maximum_matching_adjusted,0,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.011897699907422066 +5,6,0.2,iterated_maximum_matching_adjusted,1,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.006825599819421768 +5,6,0.2,iterated_maximum_matching_adjusted,2,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.006694599986076355 +5,6,0.2,iterated_maximum_matching_adjusted,3,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.008814600063487887 +5,6,0.2,iterated_maximum_matching_adjusted,4,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.00711299991235137 +5,6,0.2,bidirectional_round_robin,0,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.00047389999963343143 +5,6,0.2,bidirectional_round_robin,1,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0004702999722212553 +5,6,0.2,bidirectional_round_robin,2,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0005901001859456301 +5,6,0.2,bidirectional_round_robin,3,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0005557001568377018 +5,6,0.2,bidirectional_round_robin,4,100.0,100.0,0.0,0.0,0,0.0,5,5,5,0.0004577001091092825 +8,6,0.2,iterated_maximum_matching_adjusted,0,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.010064400034025311 +8,6,0.2,iterated_maximum_matching_adjusted,1,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.00949440011754632 +8,6,0.2,iterated_maximum_matching_adjusted,2,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.01333770016208291 +8,6,0.2,iterated_maximum_matching_adjusted,3,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.010566700017079711 +8,6,0.2,iterated_maximum_matching_adjusted,4,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.010068400064483285 +8,6,0.2,bidirectional_round_robin,0,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0012214998714625835 +8,6,0.2,bidirectional_round_robin,1,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0007842998020350933 +8,6,0.2,bidirectional_round_robin,2,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0008011001627892256 +8,6,0.2,bidirectional_round_robin,3,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0011072999332100153 +8,6,0.2,bidirectional_round_robin,4,100.0,100.0,0.0,0.0,0,0.0,8,8,8,0.0007382000330835581 +10,4,0.2,iterated_maximum_matching_adjusted,0,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.006647099973633885 +10,4,0.2,iterated_maximum_matching_adjusted,1,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.007132499944418669 +10,4,0.2,iterated_maximum_matching_adjusted,2,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.014505499973893166 +10,4,0.2,iterated_maximum_matching_adjusted,3,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.006487799808382988 +10,4,0.2,iterated_maximum_matching_adjusted,4,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.009113499894738197 +10,4,0.2,bidirectional_round_robin,0,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0006787998136132956 +10,4,0.2,bidirectional_round_robin,1,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0010331999510526657 +10,4,0.2,bidirectional_round_robin,2,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0006971999537199736 +10,4,0.2,bidirectional_round_robin,3,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0007164999842643738 +10,4,0.2,bidirectional_round_robin,4,100.0,100.0,0.0,0.0,2,2.0,10,10,10,0.0006943000480532646 +10,6,0.2,iterated_maximum_matching_adjusted,0,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.01150650018826127 +10,6,0.2,iterated_maximum_matching_adjusted,1,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.013017499819397926 +10,6,0.2,iterated_maximum_matching_adjusted,2,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.01752589992247522 +10,6,0.2,iterated_maximum_matching_adjusted,3,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.013737200060859323 +10,6,0.2,iterated_maximum_matching_adjusted,4,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.02244319999590516 +10,6,0.2,bidirectional_round_robin,0,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0009720998350530863 +10,6,0.2,bidirectional_round_robin,1,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0010019000619649887 +10,6,0.2,bidirectional_round_robin,2,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0010850001126527786 +10,6,0.2,bidirectional_round_robin,3,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0012934000696986914 +10,6,0.2,bidirectional_round_robin,4,100.0,100.0,0.0,0.0,0,0.0,10,10,10,0.0011931001208722591 diff --git a/fairpyx/algorithms/ACEEI/ACEEI.py b/fairpyx/algorithms/ACEEI/ACEEI.py index 1a2e6a4..ee11b5b 100644 --- a/fairpyx/algorithms/ACEEI/ACEEI.py +++ b/fairpyx/algorithms/ACEEI/ACEEI.py @@ -117,6 +117,18 @@ def find_ACEEI_with_EFTB(alloc: AllocationBuilder, **kwargs): >>> stringify(divide(find_ACEEI_with_EFTB, instance=instance, initial_budgets=initial_budgets, ... delta=delta, epsilon=epsilon, t=t)) "{avi:['x', 'z'], beni:['y', 'z']}" + + # INFISIBLE + >>> instance = Instance(valuations={'s1': {'c1': 184, 'c2': 172, 'c3': 62, 'c4': 50, 'c5': 84, 'c6': 75, 'c7': 37, 'c8': 39, 'c9': 80, 'c10': 54, 'c11': 69, 'c12': 93}, 's2': {'c1': 81, 'c2': 2, 'c3': 223, 'c4': 61, 'c5': 89, 'c6': 229, 'c7': 81, 'c8': 94, 'c9': 18, 'c10': 103, 'c11': 2, 'c12': 17}, 's3': {'c1': 178, 'c2': 44, 'c3': 210, 'c4': 78, 'c5': 49, 'c6': 174, 'c7': 59, 'c8': 23, 'c9': 101, 'c10': 43, 'c11': 33, 'c12': 7}, 's4': {'c1': 165, 'c2': 134, 'c3': 8, 'c4': 36, 'c5': 146, 'c6': 210, 'c7': 15, 'c8': 52, 'c9': 88, 'c10': 56, 'c11': 55, 'c12': 35}, 's5': {'c1': 42, 'c2': 21, 'c3': 155, 'c4': 82, 'c5': 122, 'c6': 146, 'c7': 75, 'c8': 51, 'c9': 91, 'c10': 81, 'c11': 61, 'c12': 72}, 's6': {'c1': 82, 'c2': 141, 'c3': 42, 'c4': 159, 'c5': 172, 'c6': 13, 'c7': 45, 'c8': 32, 'c9': 104, 'c10': 84, 'c11': 56, 'c12': 69}, 's7': {'c1': 188, 'c2': 192, 'c3': 96, 'c4': 7, 'c5': 36, 'c6': 36, 'c7': 44, 'c8': 129, 'c9': 26, 'c10': 33, 'c11': 85, 'c12': 127}, 's8': {'c1': 38, 'c2': 89, 'c3': 131, 'c4': 48, 'c5': 186, 'c6': 89, 'c7': 72, 'c8': 86, 'c9': 110, 'c10': 95, 'c11': 7, 'c12': 48}, 's9': {'c1': 34, 'c2': 223, 'c3': 115, 'c4': 144, 'c5': 64, 'c6': 75, 'c7': 61, 'c8': 0, 'c9': 82, 'c10': 36, 'c11': 89, 'c12': 76}, 's10': {'c1': 52, 'c2': 52, 'c3': 127, 'c4': 185, 'c5': 37, 'c6': 165, 'c7': 23, 'c8': 23, 'c9': 87, 'c10': 89, 'c11': 72, 'c12': 87}}, + ... agent_capacities=5, + ... item_capacities={'c1': 5.0, 'c2': 5.0, 'c3': 5.0, 'c4': 5.0, 'c5': 5.0, 'c6': 5.0, 'c7': 5.0, 'c8': 5.0, 'c9': 5.0, 'c10': 5.0, 'c11': 5.0, 'c12': 5.0}) + >>> initial_budgets = {'s1': 0.1650725918656969, 's2': 0.16262501524662654, 's3': 0.3201931268150584, 's4': 0.2492903523388018, 's5': 0.8017230433275404, 's6': 0.4141205417185544, 's7': 0.6544436816508201, 's8': 0.37386229094484114, 's9': 0.18748235872379515, 's10': 0.6342641285976163} + >>> delta = 0.5 + >>> epsilon = 3 + >>> t = EFTBStatus.EF_TB + >>> stringify(divide(find_ACEEI_with_EFTB, instance=instance, initial_budgets=initial_budgets, + ... delta=delta, epsilon=epsilon, t=t)) + '{s1:[], s10:[], s2:[], s3:[], s4:[], s5:[], s6:[], s7:[], s8:[], s9:[]}' """ # allocation = [[0 for _ in range(instance.num_of_agents)] for _ in range(instance.num_of_items)] # 1) init prices vector to be 0 @@ -140,7 +152,9 @@ def find_ACEEI_with_EFTB(alloc: AllocationBuilder, **kwargs): initial_budgets, epsilon, prices, alloc.instance, t, combinations_courses_sorted) if clearing_error is None: - raise ValueError("Clearing error is None") + print("Clearing error is None - No Solution") + # raise ValueError("Clearing error is None") + break # 3) If ∥𝒛˜(𝒖,𝒄, 𝒑, 𝒃) ∥2 = 0, terminate with 𝒑* = 𝒑, 𝒃* = 𝒃 logger.info("Clearing error is %s", clearing_error) if np.allclose(clearing_error, 0): @@ -280,8 +294,8 @@ def find_budget_perturbation(initial_budgets: dict, epsilon: float, prices: dict logger.debug( " Budget perturbation with lowest clearing error: new_budgets = %s, clearing_error = %s, excess_demand_per_course = %s", new_budgets, clearing_error, excess_demand_per_course) - if clearing_error is None: - raise ValueError("Clearing error is None") + # if clearing_error is None: + # raise ValueError("Clearing error is None") return new_budgets, clearing_error, map_student_to_best_bundle_per_budget, excess_demand_per_course @@ -293,6 +307,7 @@ def ACEEI_without_EFTB(alloc: AllocationBuilder, **kwargs): def ACEEI_with_EFTB(alloc: AllocationBuilder, **kwargs): initial_budgets = random_initial_budgets(alloc.instance.num_of_agents) + # print(f"--- initial_budgets = {initial_budgets} ---") return find_ACEEI_with_EFTB(alloc, initial_budgets=initial_budgets, delta=0.5, epsilon=3.0, t=EFTBStatus.EF_TB, **kwargs) From cf05f6dbb972d2994089f2cba71b8559b88a19b7 Mon Sep 17 00:00:00 2001 From: zachibs Date: Mon, 15 Jul 2024 20:08:13 +0300 Subject: [PATCH 028/111] added tests and fixed logic in the algorithm --- ...hapley_pareto_dominant_market_mechanism.py | 129 ++++++++++++------ .../test_gale_shapley.py | 53 ++++++- 2 files changed, 136 insertions(+), 46 deletions(-) diff --git a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py index ef57033..ca3e8cd 100644 --- a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py +++ b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py @@ -10,57 +10,69 @@ from fairpyx import AllocationBuilder import numpy as np -from typing import Dict, List +from typing import Dict, List, Union import logging +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) -def sort_and_tie_brake(input_dict: Dict[str, float], tie_braking_lottery: Dict[str, float], course_capacity: int) -> List[tuple[str, float]]: + +def sort_and_tie_brake(input_dict: Dict[str, float], tie_braking_lottery: Dict[str, float]) -> List[tuple[str, float]]: """ Sorts a dictionary by its values in descending order and adds a number to the values of keys with the same value to break ties. - Stops if the count surpasses the course's capacity and is not in a tie. Parameters: input_dict (Dict[str, float]): A dictionary with string keys and float values representing student bids. tie_braking_lottery (Dict[str, float]): A dictionary with string keys and float values for tie-breaking. - course_capacity (int): The number of students allowed in the course. Returns: List[tuple[str, float]]: A list of tuples containing student names and their modified bids, sorted in descending order. - """ - # Sort the dictionary by values in descending order - sorted_dict = dict(sorted(input_dict.items(), key=lambda item: item[1], reverse=True)) - - # Initialize previous value to track duplicate values - previous_value = None - prev_key = "" - - # Initialize a variable to track count - count: int = 0 - # Iterate over the sorted dictionary and modify values - for key in sorted_dict: - current_value = sorted_dict[key] - - if current_value == previous_value: - # If current value is the same as previous, add the number to both current and previous values - sorted_dict[key] += tie_braking_lottery[key] - sorted_dict[prev_key] += tie_braking_lottery[prev_key] - elif count >= course_capacity: - break - - # Update previous_value and prev_key to current_value and key for next iteration - previous_value = sorted_dict[key] - prev_key = key + Examples: + >>> input_dict = {"Alice": 45, "Bob": 55, "Chana": 45, "Dana": 60} + >>> tie_braking_lottery = {"Alice": 0.3, "Bob": 0.2, "Chana": 0.4, "Dana": 0.1} + >>> sort_and_tie_brake(input_dict, tie_braking_lottery) + [('Dana', 60), ('Bob', 55), ('Chana', 45), ('Alice', 45)] + """ + - # Sort again after tie-breaking - sorted_dict = (sorted(sorted_dict.items(), key=lambda item: item[1], reverse=True)) + # Sort the dictionary by adjusted values in descending order + sorted_dict = (sorted(input_dict.items(), key=lambda item: item[1] + tie_braking_lottery[item[0]], reverse=True)) return sorted_dict +def sort_lists_by_numeric_suffix(input_dict): + """ + Sorts lists of strings in a dictionary based on the numeric values after the character 'c'. + + Parameters: + input_dict (dict[str, list[str]]): A dictionary where each key is a string and the value is a list of strings + starting with 'c' followed by a numeric value. -def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, List[str]], tie_braking_lottery: Dict[str, float]): + Returns: + dict[str, list[str]]: A dictionary with the same keys as input_dict but with sorted lists. + + Example: + >>> input_dict = {'Alice': ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15'], + ... 'Bob': ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15', 'c16', 'c17', 'c18', 'c19', 'c20'], + ... 'Chana': ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15', 'c16', 'c17', 'c18'], + ... 'Dana': ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15', 'c16', 'c17'], + ... 'Dor': ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15', 'c16']} + >>> sorted_dict = sort_lists_by_numeric_suffix(input_dict) + >>> sorted_dict['Alice'] + ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15'] + >>> sorted_dict['Bob'] + ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15', 'c16', 'c17', 'c18', 'c19', 'c20'] + """ + + def numeric_suffix(item): + return int(item[1:]) # Convert the numeric part of the string to an integer + + sorted_dict = {key: sorted(value, key=numeric_suffix) for key, value in input_dict.items()} + return sorted_dict + +def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, List[str]], tie_braking_lottery: Union[None, Dict[str, float]] = None): """ Allocate the given items to the given agents using the Gale-Shapley protocol. @@ -72,7 +84,7 @@ def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, L Returns: Dict[str, List[str]]: A dictionary representing the final allocation of courses to students. - Example: + Naive Example: >>> from fairpyx import Instance, AllocationBuilder >>> from fairpyx.adaptors import divide >>> s1 = {"c1": 40, "c2": 60} @@ -88,53 +100,89 @@ def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, L >>> instance = Instance(agent_capacities=agent_capacities, item_capacities=course_capacities, valuations=valuations) >>> divide(gale_shapley, instance=instance, course_order_per_student=course_order_per_student, tie_braking_lottery=tie_braking_lottery) {'Alice': ['c2'], 'Bob': ['c1'], 'Chana': ['c1'], 'Dana': ['c2'], 'Dor': ['c1']} + + + Example where the students course order does not align with the bids: + >>> s1 = {"c1": 20, "c2": 15, "c3": 35, "c4": 10, "c5": 20} + >>> s2 = {"c1": 30, "c2": 15, "c3": 20, "c4": 20, "c5": 15} + >>> s3 = {"c1": 40, "c2": 10, "c3": 25, "c4": 10, "c5": 15} + >>> s4 = {"c1": 10, "c2": 10, "c3": 15, "c4": 30, "c5": 35} + >>> s5 = {"c1": 25, "c2": 20, "c3": 30, "c4": 10, "c5": 15} + >>> agent_capacities = {"Alice": 3, "Bob": 3, "Chana": 3, "Dana": 3, "Dor": 3} + >>> course_capacities = {"c1": 4, "c2": 4, "c3": 2, "c4": 3, "c5": 2} + >>> valuations = {"Alice": s1, "Bob": s2, "Chana": s3, "Dana": s4, "Dor": s5} + >>> course_order_per_student = {"Alice": ["c5", "c3", "c1", "c2", "c4"], "Bob": ["c1", "c4", "c5", "c2", "c3"], "Chana": ["c5", "c1", "c4", "c3", "c2"], "Dana": ["c3", "c4", "c1", "c5", "c2"], "Dor": ["c5", "c1", "c4", "c3", "c2"]} + >>> tie_braking_lottery = {"Alice": 0.6, "Bob": 0.4, "Chana": 0.3, "Dana": 0.8, "Dor": 0.2} + >>> instance = Instance(agent_capacities=agent_capacities, item_capacities=course_capacities, valuations=valuations) + >>> divide(gale_shapley, instance=instance, course_order_per_student=course_order_per_student, tie_braking_lottery=tie_braking_lottery) + {'Alice': ['c1', 'c3', 'c5'], 'Bob': ['c1', 'c2', 'c4'], 'Chana': ['c1', 'c2', 'c4'], 'Dana': ['c2', 'c4', 'c5'], 'Dor': ['c1', 'c2', 'c3']} """ # Check if inputs are dictionaries - input_to_check_types = [alloc.remaining_agent_capacities, alloc.remaining_item_capacities, course_order_per_student, tie_braking_lottery] + input_to_check_types = [alloc.remaining_agent_capacities, alloc.remaining_item_capacities, course_order_per_student] for input_to_check in input_to_check_types: if(type(input_to_check) != dict): - raise TypeError; + raise TypeError(f"In the input {input_to_check}, Expected a dict, but got {type(input_to_check).__name__}") + if(type(tie_braking_lottery) not in [None, dict]): + raise TypeError(f"in tie_braking_lottery Expected None or a dict, but got {type(tie_braking_lottery).__name__}") + + if not tie_braking_lottery: + tie_braking_lottery = {student : np.random.uniform(low=0, high=1) for student in alloc.remaining_agents()} - if(alloc.remaining_agent_capacities == {} or alloc.remaining_item_capacities == {}): - return {} was_an_offer_declined: bool = True course_to_on_hold_students: Dict[str, Dict[str, float]] = {course: {} for course in alloc.remaining_items()} student_to_rejection_count: Dict[str, int] = {student: alloc.remaining_agent_capacities[student] for student in alloc.remaining_agents()} + logger.info(f"We have {len(alloc.remaining_agents())} agents") + logger.info(f"The students allocation capacities are: {alloc.remaining_agent_capacities}") + logger.info(f"The courses capacities are: {alloc.remaining_item_capacities}") + logger.info(f"The tie-braking lottery results are: {tie_braking_lottery}") + for agent in alloc.remaining_agents(): + agent_bids = {course: alloc.effective_value(agent, course) for course in alloc.remaining_items()} + logger.info(f"Student '{agent}' bids are: {agent_bids}") + + step = 0 while(was_an_offer_declined): + step += 1 + logger.info(f"Starting step #{step}") + was_an_offer_declined = False logger.info("Each student who is rejected from k > 0 courses in the previous step proposes to his best remaining k courses based on his stated preferences") for student in alloc.remaining_agents(): student_capability: int = student_to_rejection_count[student] for index in range(student_capability): - wanted_course = course_order_per_student[student].pop(index) + if(not course_order_per_student[student]): + logger.info(f"Student {student} already proposed to all his desired courses") + continue + wanted_course = course_order_per_student[student].pop(0) if(wanted_course in course_to_on_hold_students): if(student in course_to_on_hold_students[wanted_course]): continue try: student_to_course_proposal = alloc.effective_value(student, wanted_course) course_to_on_hold_students[wanted_course][student] = student_to_course_proposal + logger.info(f"Student '{student} proposes to course {wanted_course} with a bid of {student_to_course_proposal}") except Exception as e: return {} - + logger.info("Each course c considers the new proposals together with the proposals on hold and rejects all but the highest bidding Qc (the maximum capacity of students in course c) students") student_to_rejection_count = {student: 0 for student in alloc.remaining_agents()} for course_name in course_to_on_hold_students: course_capacity = alloc.remaining_item_capacities[course_name] course_to_offerings = course_to_on_hold_students[course_name] + logger.info(f"Course {course_name} considers the next offerings: {course_to_offerings}") if len(course_to_offerings) == 0: continue elif len(course_to_offerings) <= course_capacity: - was_an_offer_declined = False continue logger.info("In case there is a tie, the tie-breaking lottery is used to determine who is rejected and who will be kept on hold.") - on_hold_students_sorted_and_tie_braked = sort_and_tie_brake(course_to_offerings, tie_braking_lottery, course_capacity) + on_hold_students_sorted_and_tie_braked = sort_and_tie_brake(course_to_offerings, tie_braking_lottery) course_to_on_hold_students[course_name].clear() for key, value in on_hold_students_sorted_and_tie_braked[:course_capacity]: course_to_on_hold_students[course_name][key] = value rejected_students = on_hold_students_sorted_and_tie_braked[course_capacity:] for rejected_student, bid in rejected_students: + logger.info(f"Agent '{rejected_student}' was rejected from course {course_name}") student_to_rejection_count[rejected_student] += 1 was_an_offer_declined = True @@ -143,6 +191,7 @@ def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, L for course_name, matching in final_course_matchings: for student, bid in matching.items(): alloc.give(student, course_name, logger) + logger.info(f"The final course matchings are: {alloc.bundles}") if __name__ == "__main__": import doctest diff --git a/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py b/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py index a255fb2..a06eb3c 100644 --- a/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py +++ b/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py @@ -27,7 +27,27 @@ def test_regular_case(): instance=instance, course_order_per_student=course_order_per_student, tie_braking_lottery=tie_braking_lottery) - assert allocation == {'Alice': ['c2'], 'Bob': ['c1'], 'Chana': ['c1'], 'Dana': ['c2'], 'Dor': ['c1']}, "failed" + assert allocation == {'Alice': ['c2'], 'Bob': ['c1'], 'Chana': ['c1'], 'Dana': ['c2'], 'Dor': ['c1']}, "allocation's did not match" + +def test_order_does_not_align_with_bids(): + s1 = {"c1": 20, "c2": 15, "c3": 35, "c4": 10, "c5": 20} + s2 = {"c1": 30, "c2": 15, "c3": 20, "c4": 20, "c5": 15} + s3 = {"c1": 40, "c2": 10, "c3": 25, "c4": 10, "c5": 15} + s4 = {"c1": 10, "c2": 10, "c3": 15, "c4": 30, "c5": 35} + s5 = {"c1": 25, "c2": 20, "c3": 30, "c4": 10, "c5": 15} + agent_capacities = {"Alice": 3, "Bob": 3, "Chana": 3, "Dana": 3, "Dor": 3} + course_capacities = {"c1": 4, "c2": 4, "c3": 2, "c4": 3, "c5": 2} + valuations = {"Alice": s1, "Bob": s2, "Chana": s3, "Dana": s4, "Dor": s5} + course_order_per_student = {"Alice": ["c5", "c3", "c1", "c2", "c4"], "Bob": ["c1", "c4", "c5", "c2", "c3"], "Chana": ["c5", "c1", "c4", "c3", "c2"], "Dana": ["c3", "c4", "c1", "c5", "c2"], "Dor": ["c5", "c1", "c4", "c3", "c2"]} + tie_braking_lottery = {"Alice": 0.6, "Bob": 0.4, "Chana": 0.3, "Dana": 0.8, "Dor": 0.2} + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + allocation = fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + assert allocation == {'Alice': ['c1', 'c3', 'c5'], 'Bob': ['c1', 'c2', 'c4'], 'Chana': ['c1', 'c2', 'c4'], 'Dana': ['c2', 'c4', 'c5'], 'Dor': ['c1', 'c2', 'c3']}, "allocation's did not match" def test_one_agent(): s1 = {"c1": 40, "c2": 60} @@ -43,7 +63,7 @@ def test_one_agent(): instance=instance, course_order_per_student=course_order_per_student, tie_braking_lottery=tie_braking_lottery) - assert allocation == {'Alice': ['c2']}, "failed" + assert allocation == {'Alice': ['c2']}, "allocation's did not match" def test_empty_input(): agent_capacities = {} @@ -152,8 +172,29 @@ def test_large_input(): course_order_per_student=course_order_per_student, tie_braking_lottery=tie_braking_lottery) # Validate that the allocation is valid - assert len(allocation) == num_students, "failed" - assert all(len(courses) == 1 for courses in allocation.values()), "failed" + assert len(allocation) == num_students, "length of allocation did not match the number of students" + assert all(len(courses) == 1 for courses in allocation.values()), "not every course got exactly 1 student" + fairpyx.validate_allocation(instance, allocation, title=f"gale_shapley") + +def test_large_number_of_students_and_courses(): + s1 = {"c1": 20, "c2": 15, "c3": 35, "c4": 10, "c5": 20, "c6": 30, "c7": 25, "c8": 30, "c9": 15, "c10": 20, "c11": 25, "c12": 10, "c13": 30, "c14": 20, "c15": 15, "c16": 35, "c17": 20, "c18": 10, "c19": 25, "c20": 30} # sum = 440 + s2 = {"c1": 30, "c2": 15, "c3": 20, "c4": 20, "c5": 15, "c6": 25, "c7": 10, "c8": 20, "c9": 30, "c10": 25, "c11": 20, "c12": 15, "c13": 10, "c14": 20, "c15": 30, "c16": 15, "c17": 25, "c18": 20, "c19": 10, "c20": 35} # sum = 440 + s3 = {"c1": 40, "c2": 10, "c3": 25, "c4": 10, "c5": 15, "c6": 20, "c7": 25, "c8": 30, "c9": 35, "c10": 20, "c11": 15, "c12": 10, "c13": 20, "c14": 25, "c15": 30, "c16": 15, "c17": 10, "c18": 20, "c19": 25, "c20": 30} # sum = 440 + s4 = {"c1": 10, "c2": 10, "c3": 15, "c4": 30, "c5": 35, "c6": 20, "c7": 15, "c8": 10, "c9": 25, "c10": 20, "c11": 30, "c12": 15, "c13": 10, "c14": 25, "c15": 20, "c16": 30, "c17": 35, "c18": 20, "c19": 15, "c20": 30} # sum = 440 + s5 = {"c1": 25, "c2": 20, "c3": 30, "c4": 10, "c5": 15, "c6": 35, "c7": 25, "c8": 20, "c9": 10, "c10": 30, "c11": 15, "c12": 20, "c13": 25, "c14": 10, "c15": 35, "c16": 20, "c17": 15, "c18": 10, "c19": 30, "c20": 25} # sum = 440 + agent_capacities = {"Alice": 15, "Bob": 20, "Chana": 18, "Dana": 17, "Dor": 16} + course_capacities = {"c1": 10, "c2": 10, "c3": 8, "c4": 7, "c5": 6, "c6": 5, "c7": 4, "c8": 3, "c9": 2, "c10": 1, "c11": 10, "c12": 9, "c13": 8, "c14": 7, "c15": 6, "c16": 5, "c17": 4, "c18": 3, "c19": 2, "c20": 1} + valuations = {"Alice": s1, "Bob": s2, "Chana": s3, "Dana": s4, "Dor": s5} + course_order_per_student = {"Alice": ["c5", "c3", "c1", "c2", "c4", "c6", "c7", "c8", "c9", "c10", "c11", "c12", "c13", "c14", "c15", "c16", "c17", "c18", "c19", "c20"], "Bob": ["c1", "c4", "c5", "c2", "c3", "c6", "c7", "c8", "c9", "c10", "c11", "c12", "c13", "c14", "c15", "c16", "c17", "c18", "c19", "c20"], "Chana": ["c5", "c1", "c4", "c3", "c2", "c6", "c7", "c8", "c9", "c10", "c11", "c12", "c13", "c14", "c15", "c16", "c17", "c18", "c19", "c20"], "Dana": ["c3", "c4", "c1", "c5", "c2", "c6", "c7", "c8", "c9", "c10", "c11", "c12", "c13", "c14", "c15", "c16", "c17", "c18", "c19", "c20"], "Dor": ["c5", "c1", "c4", "c3", "c2", "c6", "c7", "c8", "c9", "c10", "c11", "c12", "c13", "c14", "c15", "c16", "c17", "c18", "c19", "c20"]} + tie_braking_lottery = {"Alice": 0.6, "Bob": 0.4, "Chana": 0.3, "Dana": 0.8, "Dor": 0.2} + instance = fairpyx.Instance(agent_capacities=agent_capacities, + item_capacities=course_capacities, + valuations=valuations) + allocation = fairpyx.divide(fairpyx.algorithms.gale_shapley, + instance=instance, + course_order_per_student=course_order_per_student, + tie_braking_lottery=tie_braking_lottery) + # Validate that the allocation is valid fairpyx.validate_allocation(instance, allocation, title=f"gale_shapley") @@ -172,8 +213,8 @@ def test_edge_case_tie(): instance=instance, course_order_per_student=course_order_per_student, tie_braking_lottery=tie_braking_lottery) - assert set(allocation.keys()) == {"Alice", "Bob"}, "failed" - assert set(allocation["Alice"] + allocation["Bob"]) == {"c1", "c2"}, "failed" + assert set(allocation.keys()) == {"Alice", "Bob"}, "the keys in the allocation did not match 'Alice', 'Bob'" + assert set(allocation["Alice"] + allocation["Bob"]) == {"c1", "c2"}, "the total allocation of courses for Alice and Bob did not match 'c1', 'c2'" if __name__ == "__main__": pytest.main(["-v",__file__]) \ No newline at end of file From 61120f50eec292ecac1f65196c3f855bd7d83b07 Mon Sep 17 00:00:00 2001 From: zachibs Date: Mon, 15 Jul 2024 20:08:47 +0300 Subject: [PATCH 029/111] removed unused function --- ...hapley_pareto_dominant_market_mechanism.py | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py index ca3e8cd..c32eabd 100644 --- a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py +++ b/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py @@ -42,36 +42,6 @@ def sort_and_tie_brake(input_dict: Dict[str, float], tie_braking_lottery: Dict[s return sorted_dict -def sort_lists_by_numeric_suffix(input_dict): - """ - Sorts lists of strings in a dictionary based on the numeric values after the character 'c'. - - Parameters: - input_dict (dict[str, list[str]]): A dictionary where each key is a string and the value is a list of strings - starting with 'c' followed by a numeric value. - - Returns: - dict[str, list[str]]: A dictionary with the same keys as input_dict but with sorted lists. - - Example: - >>> input_dict = {'Alice': ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15'], - ... 'Bob': ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15', 'c16', 'c17', 'c18', 'c19', 'c20'], - ... 'Chana': ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15', 'c16', 'c17', 'c18'], - ... 'Dana': ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15', 'c16', 'c17'], - ... 'Dor': ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15', 'c16']} - >>> sorted_dict = sort_lists_by_numeric_suffix(input_dict) - >>> sorted_dict['Alice'] - ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15'] - >>> sorted_dict['Bob'] - ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'c12', 'c13', 'c14', 'c15', 'c16', 'c17', 'c18', 'c19', 'c20'] - """ - - def numeric_suffix(item): - return int(item[1:]) # Convert the numeric part of the string to an integer - - sorted_dict = {key: sorted(value, key=numeric_suffix) for key, value in input_dict.items()} - return sorted_dict - def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, List[str]], tie_braking_lottery: Union[None, Dict[str, float]] = None): """ Allocate the given items to the given agents using the Gale-Shapley protocol. From 5b76f5ab292c76f38907b483a6caebd9416e40d5 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Tue, 16 Jul 2024 17:30:44 +0300 Subject: [PATCH 030/111] Update FaStGen.py fixing the doctests of the functions and fixing the function of creating the leximiin tuple. --- .../Optimization_Matching/FaStGen.py | 306 ++++++++++++------ 1 file changed, 206 insertions(+), 100 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index 719c448..ed7b52e 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -24,11 +24,11 @@ def FaStGen(alloc: AllocationBuilder, items_valuations:dict)->dict: >>> S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] >>> C = ["c1", "c2", "c3", "c4"] >>> V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1}} - >>> U = { "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, "s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, "s7" : {"c1":40,"c2":29,"c3":6,"c4":1}} + >>> U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, "s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, "s7" : {"c1":40,"c2":29,"c3":6,"c4":1}} >>> ins = Instance(agents=S, items=C, valuations=U) >>> alloc = AllocationBuilder(instance=ins) >>> FaStGen(alloc=alloc, items_valuations=V) - {'c1': ['s1'], 'c2': ['s2'], 'c3': ['s5', 's4', 's3'], 'c4': ['s7', 's6']} + {'c1': ['s1', 's2', 's3'], 'c2': ['s4'], 'c3': ['s5'], 'c4': ['s7', 's6']} """ logger.info("Starting FaStGen algorithm") @@ -219,45 +219,53 @@ def LookAheadRoutine(I:tuple, integer_match:dict, down:int, LowerFix:list, Upper return (final_match_str, LowerFix, UpperFix, SoftFix) def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:dict): - # """ - # Create a leximin tuple from the given match, agents' valuations, and items' valuations. + """ + Create a leximin tuple from the given match, agents' valuations, and items' valuations. - # Args: - # - match (dict): A dictionary where keys are items and values are lists of agents. - # - agents_valuations (dict): A dictionary where keys are agents and values are dictionaries of item valuations. - # - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. + Args: + - match (dict): A dictionary where keys are items and values are lists of agents. + - agents_valuations (dict): A dictionary where keys are agents and values are dictionaries of item valuations. + - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. - # Returns: - # - list: A sorted list of tuples representing the leximin tuple. + Returns: + - list: A sorted list of tuples representing the leximin tuple. - # Example: - # >>> match = {"c1":["s1","s2","s3"], "c2":["s4"], "c3":["s5"], "c4":["s7","s6"]} - # >>> items_valuations = { #the colleges valuations - # "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - # "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - # "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - # "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} - # } - # >>> agents_valuations = { #the students valuations - # "s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, - # "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, - # "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, - # "s4" : {"c1":41,"c2":24,"c3":5,"c4":3}, - # "s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, - # "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, - # "s7" : {"c1":40,"c2":29,"c3":6,"c4":1} - # } - # >>> create_leximin_tuple(match, agents_valuations, items_valuations) - # [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] - # """ + Example: + >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} + >>> items_valuations = { #the colleges valuations + ... "c1":{"s1":50, "s2":23, "s3":21, "s4":13, "s5":10, "s6":6, "s7":5}, + ... "c2":{"s1":45, "s2":40, "s3":32, "s4":29, "s5":26, "s6":11, "s7":4}, + ... "c3":{"s1":90, "s2":79, "s3":60, "s4":35, "s5":28, "s6":20, "s7":15}, + ... "c4":{"s1":80, "s2":48, "s3":36, "s4":29, "s5":15, "s6":7, "s7":1}, + ... } + >>> agents_valuations = { #the students valuations + ... "s1":{"c1":16, "c2":10, "c3":6, "c4":5}, + ... "s2":{"c1":36, "c2":20, "c3":10, "c4":1}, + ... "s3":{"c1":29, "c2":24, "c3":12, "c4":10}, + ... "s4":{"c1":41, "c2":24, "c3":5, "c4":3}, + ... "s5":{"c1":36, "c2":19, "c3":9, "c4":6}, + ... "s6":{"c1":39, "c2":30, "c3":18, "c4":7}, + ... "s7":{"c1":40, "c2":29, "c3":6, "c4":1} + ... } + >>> create_leximin_tuple(match, agents_valuations, items_valuations) + [('c4', 1), ('s7', 1), ('s1', 16), ('s6', 18), ('s5', 19), ('c3', 20), ('c2', 26), ('s3', 29), ('s2', 36), ('s4', 41), ('c1', 107)] + + >>> match = {"c1":["s1","s2","s3"], "c2":["s4"], "c3":["s5"], "c4":["s7","s6"]} + >>> create_leximin_tuple(match, agents_valuations, items_valuations) + [('s7', 1), ('s6', 7), ('c4', 8), ('s5', 9), ('s1', 16), ('s4', 24), ('c3', 28), ('c2', 29), ('s3', 29), ('s2', 36), ('c1', 94)] + """ leximin_tuple = [] + matching_college_valuations = update_matching_valuations_sum(match=match, items_valuations=items_valuations) + logger.debug(f"matching_college_valuations: {matching_college_valuations}") for item in match.keys(): if len(match[item]) == 0: leximin_tuple.append((item, 0)) + else: + leximin_tuple.append((item, matching_college_valuations[item])) for agent in match[item]: - leximin_tuple.append((agent,items_valuations[item][agent])) - leximin_tuple.append((item, agents_valuations[agent][item])) - leximin_tuple.sort(key = lambda x: x[1]) + leximin_tuple.append((agent,agents_valuations[agent][item])) + + leximin_tuple = sorted(leximin_tuple, key=lambda x: (x[1], x[0])) return leximin_tuple def is_leximin_at_least(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->bool: @@ -272,23 +280,15 @@ def is_leximin_at_least(new_match_leximin_tuple:list, old_match_leximin_tuple:li # - bool: True if new_match_leximin_tuple >= old_match_leximin_tuple, otherwise False. # Example: - >>> new_match = [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] - >>> old_match = [("s7",1),("c4",1),("s4",13),("c1",16),("c3",18),("c2",19),("s6",20),("s3",21),("s2",23),("s5",26),("c1",29),("c1",36),("c1",41),("s1",50)] + >>> new_match = [("s7",1),("s6",7),("c4",8),("s5",9),("s1",16),("s4",24),("c3",28),("c2",29),("s3",29),("s2",36),("c1",94)] + >>> old_match = [("c4",1),("s7",1),("s1",16),("s6",18),("s5",19),("c3",20),("c2",26),("s3",29),("s2",36),("s4",41),("c1",107)] >>> is_leximin_at_least(new_match, old_match) - False + True - >>> new_match = [("c4",0),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] - >>> old_match = [("c4",3),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] + >>> new_match = [("s7",1),("s4",5),("s5",6),("s6",7),("c4",14),("s1",16),("s3",24),("c2",32),("c3",35),("s2",36),("c1",52)] + >>> old_match = [("s7",1),("s6",7),("c4",8),("s5",9),("s1",16),("s4",24),("c3",28),("c2",29),("s3",29),("s2",36),("c1",94)] >>> is_leximin_at_least(new_match, old_match) False - - >>> new_match = [("c4",3),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] - >>> old_match = [("c4",0),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] - >>> is_leximin_at_least(new_match, old_match) - True - - >>> is_leximin_at_least(new_match, new_match) - True """ for k in range(0, len(new_match_leximin_tuple)): if new_match_leximin_tuple[k][1] == old_match_leximin_tuple[k][1]: @@ -300,27 +300,27 @@ def is_leximin_at_least(new_match_leximin_tuple:list, old_match_leximin_tuple:li return True def sourceDec(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->str: - # """ - # Determine the agent causing the leximin decrease between two matchings. + """ + Determine the agent causing the leximin decrease between two matchings. - # Args: - # - new_match_leximin_tuple (list): The leximin tuple of the new matching. - # - old_match_leximin_tuple (list): The leximin tuple of the old matching. + Args: + - new_match_leximin_tuple (list): The leximin tuple of the new matching. + - old_match_leximin_tuple (list): The leximin tuple of the old matching. - # Returns: - # - str: The agent (student) causing the leximin decrease. + Returns: + - str: The agent (student) causing the leximin decrease. - # Example: - # >>> new_match = [("s7",1),("c4",1),("s6",6),("c4",7),("c3",9),("c1",16),("s3",21),("s2",23),("c2",24),("s4",29),("c1",29),("c1",36),("s1",50)] - # >>> old_match = [("s7",1),("c4",1),("s4",13),("c1",16),("c3",18),("c2",19),("s6",20),("s3",21),("s2",23),("s5",26),("c1",29),("c1",36),("c1",41),("s1",50)] - # >>> sourceDec(new_match, old_match) - # 's6' - - # >>> new_match = [("c4",3),("c3",5),("c1",16),("c2",19),("s2",23),("c2",24),("s5",26),("s3",32),("s4",35),("c1",36),("s1",50)] - # >>> old_match = [("c4",3),("c3",12),("c1",16),("c2",19),("s2",23),("s5",26),("s4",29),("c1",36),("s1",50),("s3",60)] - # >>> sourceDec(new_match, old_match) - # 'c3' - # """ + Example: + >>> new_match = [("s7",1),("s4",5),("s5",6),("s6",7),("c4",14),("s1",16),("s3",24),("c2",32),("c3",35),("s2",36),("c1",52)] + >>> old_match = [("s7",1),("s6",7),("c4",8),("s5",9),("s1",16),("s4",24),("c3",28),("c2",29),("s3",29),("s2",36),("c1",94)] + >>> sourceDec(new_match, old_match) + 's4' + + >>> new_match = [("s7",1),("s4",7),("s5",8),("s6",9),("c4",14),("s1",16),("s3",24),("c2",32),("c3",35),("s2",36),("c1",52)] + >>> old_match = [("s7",1),("s6",7),("c4",8),("s5",9),("s1",16),("s4",24),("c3",28),("c2",29),("s3",29),("s2",36),("c1",94)] + >>> sourceDec(new_match, old_match) + 'c4' + """ for k in range(0, len(new_match_leximin_tuple)): if new_match_leximin_tuple[k][1] < old_match_leximin_tuple[k][1]: return new_match_leximin_tuple[k][0] @@ -358,31 +358,33 @@ def get_lowest_ranked_student(item_int:int, match_int:dict, items_valuations:dic return min(match_int[item_int], key=lambda agent: items_valuations[int_to_college_name[item_int]][int_to_student_name[agent]]) def update_matching_valuations_sum(match:dict, items_valuations:dict)->dict: - # """ - # Update the sum of valuations for each item in the matching. + """ + Update the sum of valuations for each item in the matching. - # Args: - # - match (dict): A dictionary where keys are items and values are lists of agents. - # - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. - # - agents (list): List of agents. - # - items (list): List of items. + Args: + - match (dict): A dictionary where keys are items and values are lists of agents. + - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. + - agents (list): List of agents. + - items (list): List of items. - # Returns: - # - dict: A dictionary with the sum of valuations for each item. + Returns: + - dict: A dictionary with the sum of valuations for each item. - # Example: - # >>> match = {c1:[s1,s2,s3,s4], c2:[s5], c3:[s6], c4:[s7]} - # >>> items_valuations = { #the colleges valuations - # "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - # "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - # "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - # "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} - # } - # >>> agents = ["s1","s2","s3","s4","s5","s6","s7"] - # >>> items = ["c1","c2","c3","c4"] - # >>> update_matching_valuations_sum(match, items_valuations, agents, items) - # {"c1": 107, "c2": 26, "c3": 20, "c4": 1} - # """ + Example: + >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} + >>> items_valuations = { #the colleges valuations + ... "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + ... "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + ... "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + ... "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":7,"s7":1} + ... } + >>> update_matching_valuations_sum(match, items_valuations) + {'c1': 107, 'c2': 26, 'c3': 20, 'c4': 1} + + >>> match = {"c1":["s1","s2","s3"], "c2":["s4"], "c3":["s5"], "c4":["s7","s6"]} + >>> update_matching_valuations_sum(match, items_valuations) + {'c1': 94, 'c2': 29, 'c3': 28, 'c4': 8} + """ matching_valuations_sum = { #in the artical it looks like this: vj(mu) colleague: sum(items_valuations[colleague][student] for student in students) for colleague, students in match.items() @@ -390,6 +392,28 @@ def update_matching_valuations_sum(match:dict, items_valuations:dict)->dict: return matching_valuations_sum def create_stable_matching(agents, agents_dict, items, items_dict): + """ + Creating a stable matching according to this: + the first collage get the first n-m+1 students + each collage in deacrising order get the n-(m-j)th student + + Args: + - items_dict: A dictionary of all the items and there indexes like this: ("c1":1). + - agents_dict: A dictionary of all the agents and there indexes like this: ("s1":1). + - agents (list): List of agents. + - items (list): List of items. + + Returns: + - dict: A stable matching of integers + + Example: + >>> agents = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + >>> items = ["c1", "c2", "c3", "c4"] + >>> agents_dict = {"s1":1, "s2":2, "s3":3, "s4":4, "s5":5, "s6":6, "s7":7} + >>> items_dict = {"c1":1, "c2":2, "c3":3, "c4":4} + >>> create_stable_matching(agents, agents_dict, items, items_dict) + {1: [1, 2, 3, 4], 2: [5], 3: [6], 4: [7]} + """ # Initialize the matching dictionary matching = {} @@ -403,15 +427,71 @@ def create_stable_matching(agents, agents_dict, items, items_dict): return matching def generate_dict_from_str_to_int(input_list:list)->dict: + """ + Creating a dictionary that includes for each string item in the list an index representing it, key=string. + + Args: + - input_list: A list of strings + + Returns: + - dict: a dictionary of strings ang indexes + + Example: + >>> agents = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + >>> items = ["c1", "c2", "c3", "c4"] + + >>> generate_dict_from_str_to_int(agents) + {'s1': 1, 's2': 2, 's3': 3, 's4': 4, 's5': 5, 's6': 6, 's7': 7} + + >>> generate_dict_from_str_to_int(items) + {'c1': 1, 'c2': 2, 'c3': 3, 'c4': 4} + """ return {item: index + 1 for index, item in enumerate(input_list)} def generate_dict_from_int_to_str(input_list:list)->dict: + """ + Creating a dictionary that includes for each string item in the list an index representing it, key=integer. + + Args: + - input_list: A list of strings + + Returns: + - dict: a dictionary of strings ang indexes + + Example: + >>> agents = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + >>> items = ["c1", "c2", "c3", "c4"] + + >>> generate_dict_from_int_to_str(agents) + {1: 's1', 2: 's2', 3: 's3', 4: 's4', 5: 's5', 6: 's6', 7: 's7'} + + >>> generate_dict_from_int_to_str(items) + {1: 'c1', 2: 'c2', 3: 'c3', 4: 'c4'} + """ return {index + 1: item for index, item in enumerate(input_list)} -def get_key_by_value(value, items_dict): - return next(key for key, val in items_dict.items() if val == value) +# def get_key_by_value(value, items_dict): +# return next(key for key, val in items_dict.items() if val == value) def integer_to_str_matching(integer_match:dict, agent_dict:dict, items_dict:dict)->dict: + """ + Converting an integer match to a string match. + + Args: + - integer_match: A matching of agents to items out of numbers. + - items_dict: A dictionary of all the items and there indexes like this: ("c1":1). + - agents_dict: A dictionary of all the agents and there indexes like this: ("s1":1). + + Returns: + - dict: A string matching. + + Example: + >>> agents_dict = {"s1":1, "s2":2, "s3":3, "s4":4, "s5":5, "s6":6, "s7":7} + >>> items_dict = {"c1":1, "c2":2, "c3":3, "c4":4} + >>> integer_match = {1: [1, 2, 3, 4], 2: [5], 3: [6], 4: [7]} + >>> integer_to_str_matching(integer_match, agents_dict, items_dict) + {'c1': ['s1', 's2', 's3', 's4'], 'c2': ['s5'], 'c3': ['s6'], 'c4': ['s7']} + """ # Reverse the s_dict and c_dict to map integer values back to their string keys s_reverse_dict = {v: k for k, v in agent_dict.items()} c_reverse_dict = {v: k for k, v in items_dict.items()} @@ -420,6 +500,31 @@ def integer_to_str_matching(integer_match:dict, agent_dict:dict, items_dict:dict return {c_reverse_dict[c_key]: [s_reverse_dict[s_val] for s_val in s_values] for c_key, s_values in integer_match.items()} def get_match(match:dict, value:str)->any: + """ + Giving a match and an agent or an item the function will produce its match + + Args: + - match: A matching of agents to items. + - value: An agent or an item. + + Returns: + - any: An agent or an item. + + Example: + >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} + + >>> value = "c1" + >>> get_match(match, value) + ['s1', 's2', 's3', 's4'] + + >>> value = "s4" + >>> get_match(match, value) + 'c1' + + >>> value = "c4" + >>> get_match(match, value) + ['s7'] + """ if value in match.keys(): return match[value] else: @@ -430,15 +535,16 @@ def get_match(match:dict, value:str)->any: # print(doctest.testmod()) # doctest.run_docstring_examples(is_leximin_at_least, globals()) # doctest.run_docstring_examples(get_lowest_ranked_student, globals()) - # sys.exit(0) - - logger.setLevel(logging.DEBUG) - logger.addHandler(logging.StreamHandler()) - from fairpyx.adaptors import divide - S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] - C = ["c1", "c2", "c3", "c4"] - V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1}} - U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, "s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, "s7" : {"c1":40,"c2":29,"c3":6,"c4":1}} - ins = Instance(agents=S, items=C, valuations=U) - alloc = AllocationBuilder(instance=ins) - FaStGen(alloc=alloc, items_valuations=V) + doctest.run_docstring_examples(LookAheadRoutine, globals()) + sys.exit(0) + + # logger.setLevel(logging.DEBUG) + # logger.addHandler(logging.StreamHandler()) + # from fairpyx.adaptors import divide + # S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + # C = ["c1", "c2", "c3", "c4"] + # V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1}} + # U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, "s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, "s7" : {"c1":40,"c2":29,"c3":6,"c4":1}} + # ins = Instance(agents=S, items=C, valuations=U) + # alloc = AllocationBuilder(instance=ins) + # FaStGen(alloc=alloc, items_valuations=V) From 023231fafc30799c8b2741e141c790dbd7d69e64 Mon Sep 17 00:00:00 2001 From: Renana Turgeman Date: Wed, 17 Jul 2024 12:43:15 +0300 Subject: [PATCH 031/111] change folder name --- ...allocation_algorithms_ACEEI_Tabu_Search.py | 4 +-- .../{ACEEI => ACEEI_algorithms}/ACEEI.py | 6 ++-- .../{ACEEI => ACEEI_algorithms}/__init__.py | 0 .../calculate_combinations.py | 0 .../find_profitable_manipulation.py | 20 ++++++------- .../linear_program.py | 28 +++++++++---------- .../tabu_search.py | 2 +- fairpyx/algorithms/__init__.py | 4 +-- tests/test_ACEEI.py | 6 ++-- tests/test_tabu_search.py | 2 +- 10 files changed, 36 insertions(+), 36 deletions(-) rename fairpyx/algorithms/{ACEEI => ACEEI_algorithms}/ACEEI.py (98%) rename fairpyx/algorithms/{ACEEI => ACEEI_algorithms}/__init__.py (100%) rename fairpyx/algorithms/{ACEEI => ACEEI_algorithms}/calculate_combinations.py (100%) rename fairpyx/algorithms/{ACEEI => ACEEI_algorithms}/find_profitable_manipulation.py (96%) rename fairpyx/algorithms/{ACEEI => ACEEI_algorithms}/linear_program.py (94%) rename fairpyx/algorithms/{ACEEI => ACEEI_algorithms}/tabu_search.py (99%) diff --git a/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py b/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py index cd14323..20d55d5 100644 --- a/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py +++ b/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py @@ -17,8 +17,8 @@ normalized_sum_of_values = 1000 TIME_LIMIT = 100 -from fairpyx.algorithms.ACEEI.ACEEI import ACEEI_without_EFTB, ACEEI_with_EFTB, ACEEI_with_contested_EFTB -from fairpyx.algorithms.ACEEI.tabu_search import run_tabu_search +from fairpyx.algorithms.ACEEI_algorithms.ACEEI import ACEEI_without_EFTB, ACEEI_with_EFTB, ACEEI_with_contested_EFTB +from fairpyx.algorithms.ACEEI_algorithms.tabu_search import run_tabu_search algorithms_to_check = [ # ACEEI_without_EFTB, diff --git a/fairpyx/algorithms/ACEEI/ACEEI.py b/fairpyx/algorithms/ACEEI_algorithms/ACEEI.py similarity index 98% rename from fairpyx/algorithms/ACEEI/ACEEI.py rename to fairpyx/algorithms/ACEEI_algorithms/ACEEI.py index ee11b5b..3e2f35d 100644 --- a/fairpyx/algorithms/ACEEI/ACEEI.py +++ b/fairpyx/algorithms/ACEEI_algorithms/ACEEI.py @@ -13,8 +13,8 @@ import numpy as np from fairpyx import Instance, AllocationBuilder -from fairpyx.algorithms.ACEEI import linear_program as lp -from fairpyx.algorithms.ACEEI.calculate_combinations import get_combinations_courses_sorted +from fairpyx.algorithms.ACEEI_algorithms import linear_program as lp +from fairpyx.algorithms.ACEEI_algorithms.calculate_combinations import get_combinations_courses_sorted class EFTBStatus(Enum): @@ -138,7 +138,7 @@ def find_ACEEI_with_EFTB(alloc: AllocationBuilder, **kwargs): epsilon = kwargs.get('epsilon') t = kwargs.get('t') - logger.info("ACEEI algorithm with initial budgets = %s, delta = %s, epsilon = %s, t = %s", initial_budgets, delta, + logger.info("ACEEI_algorithms algorithm with initial budgets = %s, delta = %s, epsilon = %s, t = %s", initial_budgets, delta, epsilon, t) prices = {key: 0 for key in alloc.remaining_items()} diff --git a/fairpyx/algorithms/ACEEI/__init__.py b/fairpyx/algorithms/ACEEI_algorithms/__init__.py similarity index 100% rename from fairpyx/algorithms/ACEEI/__init__.py rename to fairpyx/algorithms/ACEEI_algorithms/__init__.py diff --git a/fairpyx/algorithms/ACEEI/calculate_combinations.py b/fairpyx/algorithms/ACEEI_algorithms/calculate_combinations.py similarity index 100% rename from fairpyx/algorithms/ACEEI/calculate_combinations.py rename to fairpyx/algorithms/ACEEI_algorithms/calculate_combinations.py diff --git a/fairpyx/algorithms/ACEEI/find_profitable_manipulation.py b/fairpyx/algorithms/ACEEI_algorithms/find_profitable_manipulation.py similarity index 96% rename from fairpyx/algorithms/ACEEI/find_profitable_manipulation.py rename to fairpyx/algorithms/ACEEI_algorithms/find_profitable_manipulation.py index 4b76407..7187b2f 100644 --- a/fairpyx/algorithms/ACEEI/find_profitable_manipulation.py +++ b/fairpyx/algorithms/ACEEI_algorithms/find_profitable_manipulation.py @@ -14,7 +14,7 @@ from fairpyx import Instance, AllocationBuilder from fairpyx.adaptors import divide -from fairpyx.algorithms.ACEEI.ACEEI import find_ACEEI_with_EFTB +from fairpyx.algorithms.ACEEI_algorithms.ACEEI import find_ACEEI_with_EFTB class criteria_for_profitable_manipulation(Enum): @@ -48,7 +48,7 @@ def find_profitable_manipulation(mechanism: callable, student: str, true_student return: The profitable manipulation >>> from fairpyx.algorithms.ACEEI.ACEEI import find_ACEEI_with_EFTB - >>> from fairpyx.algorithms import ACEEI, tabu_search + >>> from fairpyx.algorithms import ACEEI_algorithms, tabu_search Example run 1 @@ -65,7 +65,7 @@ def find_profitable_manipulation(mechanism: callable, student: str, true_student >>> initial_budgets = random_initial_budgets(instance, beta) >>> delta = 0.5 >>> epsilon = 0.5 - >>> t = ACEEI.ACEEI.EFTBStatus.NO_EF_TB + >>> t = ACEEI_algorithms.ACEEI.EFTBStatus.NO_EF_TB >>> find_profitable_manipulation(mechanism, student, true_student_utility, criteria, eta, instance, initial_budgets, beta, delta=delta, epsilon=epsilon, t=t) {'x': 1, 'y': 2, 'z': 4} @@ -83,7 +83,7 @@ def find_profitable_manipulation(mechanism: callable, student: str, true_student >>> initial_budgets = random_initial_budgets(instance, beta) >>> delta = 0.5 >>> epsilon = 0.5 - >>> t = ACEEI.ACEEI.EFTBStatus.EF_TB + >>> t = ACEEI_algorithms.ACEEI.EFTBStatus.EF_TB >>> find_profitable_manipulation(mechanism, student, true_student_utility, criteria, eta, instance, initial_budgets, beta, delta=delta, epsilon=epsilon, t=t) {'x': 1, 'y': 2, 'z': 4} @@ -102,7 +102,7 @@ def find_profitable_manipulation(mechanism: callable, student: str, true_student >>> initial_budgets = random_initial_budgets(instance, beta) >>> delta = 0.5 >>> epsilon = 0.5 - >>> t = ACEEI.ACEEI.EFTBStatus.NO_EF_TB + >>> t = ACEEI_algorithms.ACEEI.EFTBStatus.NO_EF_TB >>> find_profitable_manipulation(mechanism, student, true_student_utility, criteria, eta, instance, initial_budgets, beta, delta=delta, epsilon=epsilon, t=t) {'x': 6, 'y': 2} @@ -119,7 +119,7 @@ def find_profitable_manipulation(mechanism: callable, student: str, true_student >>> initial_budgets = random_initial_budgets(instance, beta) >>> delta = 0.5 >>> epsilon = 0.5 - >>> t = ACEEI.ACEEI.EFTBStatus.NO_EF_TB + >>> t = ACEEI_algorithms.ACEEI.EFTBStatus.NO_EF_TB >>> find_profitable_manipulation(mechanism, student, true_student_utility, criteria, eta, instance, initial_budgets, beta, delta=delta, epsilon=epsilon, t=t) {'x': 1, 'y': 2, 'z': 5} @@ -267,7 +267,7 @@ def expected_value_of_specific_report_for_randomness(random_utilities: dict, ran 2 for contested EF-TB :param report: our student's utility - >>> from fairpyx.algorithms.ACEEI.ACEEI import find_ACEEI_with_EFTB, EFTBStatus + >>> from fairpyx.algorithms.ACEEI_algorithms.ACEEI_algorithms import find_ACEEI_with_EFTB, EFTBStatus >>> random_utilities = {"avi":{"x":5, "y":5, "z":5},"beni":{"x":4, "y":6, "z":3}} >>> random_budgets = [{"avi": 5, "beni":2},{"avi": 5, "beni":2},{"avi": 5, "beni":2},{"avi": 5, "beni":2},{"avi": 5, "beni":2},{"avi": 5, "beni":2},{"avi": 5, "beni":2},{"avi": 5, "beni":2},{"avi": 5, "beni":2},{"avi": 5, "beni":2}] >>> mechanism = find_ACEEI_with_EFTB @@ -369,7 +369,7 @@ def criteria_randomness(mechanism: callable, student: str, utility: dict, instan print(doctest.testmod()) logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.INFO) - # from fairpyx.algorithms import ACEEI + # from fairpyx.algorithms import ACEEI_algorithms # mechanism = find_ACEEI_with_EFTB # student = "moti" # utility = {"x": 1, "y": 2, "z": 4} @@ -382,7 +382,7 @@ def criteria_randomness(mechanism: callable, student: str, utility: dict, instan # initial_budgets = random_initial_budgets(instance, beta) # delta = 0.5 # epsilon = 0.5 - # t = ACEEI.ACEEI.EFTBStatus.NO_EF_TB + # t = ACEEI_algorithms.ACEEI_algorithms.EFTBStatus.NO_EF_TB # find_profitable_manipulation(mechanism, student, utility, criteria, neu, instance, initial_budgets, beta, delta=delta, epsilon=epsilon, t=t) # @@ -398,5 +398,5 @@ def criteria_randomness(mechanism: callable, student: str, utility: dict, instan # initial_budgets = random_initial_budgets(instance, beta) # delta = 0.5 # epsilon = 0.5 - # t = ACEEI.ACEEI.EFTBStatus.NO_EF_TB + # t = ACEEI_algorithms.ACEEI_algorithms.EFTBStatus.NO_EF_TB # find_profitable_manipulation(mechanism, student, utility, criteria, neu, instance, initial_budgets, beta, delta=delta, epsilon=epsilon, t=t) diff --git a/fairpyx/algorithms/ACEEI/linear_program.py b/fairpyx/algorithms/ACEEI_algorithms/linear_program.py similarity index 94% rename from fairpyx/algorithms/ACEEI/linear_program.py rename to fairpyx/algorithms/ACEEI_algorithms/linear_program.py index 2a31863..d960694 100644 --- a/fairpyx/algorithms/ACEEI/linear_program.py +++ b/fairpyx/algorithms/ACEEI_algorithms/linear_program.py @@ -14,7 +14,7 @@ import os from fairpyx import Instance -from fairpyx.algorithms import ACEEI +from fairpyx.algorithms import ACEEI_algorithms logger = logging.getLogger(__name__) @@ -38,7 +38,7 @@ def optimize_model(map_student_to_best_bundle_per_budget: dict, instance: Instan :return final courses prices, final budgets, final allocation >>> from fairpyx import Instance - >>> from fairpyx.algorithms import ACEEI + >>> from fairpyx.algorithms import ACEEI_algorithms Example run 6 iteration 5 >>> instance = Instance( @@ -48,7 +48,7 @@ def optimize_model(map_student_to_best_bundle_per_budget: dict, instance: Instan >>> map_student_to_best_bundle_per_budget = {'Alice': {3.5: ('x', 'y'), 3: ('x', 'z')}, 'Bob': {3.5: ('x', 'y'), 2: ('y', 'z')}} >>> initial_budgets = {"Alice": 5, "Bob": 4} >>> prices = {"x": 1.5, "y": 2, "z": 0} - >>> t = ACEEI.ACEEI.EFTBStatus.EF_TB + >>> t = ACEEI_algorithms.ACEEI.EFTBStatus.EF_TB >>> optimize_model(map_student_to_best_bundle_per_budget,instance,prices,t,initial_budgets) ({'Alice': (3, ('x', 'z')), 'Bob': (2, ('y', 'z'))}, 0.0, {'x': 0.0, 'y': 0.0, 'z': 0.0}) @@ -60,7 +60,7 @@ def optimize_model(map_student_to_best_bundle_per_budget: dict, instance: Instan >>> map_student_to_best_bundle_per_budget = {'avi': {1.3: ('x',)}, 'beni': {0: ()}} >>> initial_budgets = {"avi": 1.1, "beni": 1} >>> prices = {"x": 1.3} - >>> t = ACEEI.ACEEI.EFTBStatus.EF_TB + >>> t = ACEEI_algorithms.ACEEI.EFTBStatus.EF_TB >>> optimize_model(map_student_to_best_bundle_per_budget,instance,prices,t,initial_budgets) ({'avi': (1.3, ('x',)), 'beni': (0, ())}, 0.0, {'x': 0.0}) @@ -118,10 +118,10 @@ def optimize_model(map_student_to_best_bundle_per_budget: dict, instance: Instan model += xsum(x[student, bundle] for bundle in map_student_to_best_bundle_per_budget[student].values()) == 1 # Add EF-TB constraints based on parameter t - if t == ACEEI.ACEEI.EFTBStatus.NO_EF_TB: + if t == ACEEI_algorithms.ACEEI.EFTBStatus.NO_EF_TB: pass # No EF-TB constraints, no need to anything - elif t == ACEEI.ACEEI.EFTBStatus.EF_TB or t == ACEEI.ACEEI.EFTBStatus.CONTESTED_EF_TB: + elif t == ACEEI_algorithms.ACEEI.EFTBStatus.EF_TB or t == ACEEI_algorithms.ACEEI.EFTBStatus.CONTESTED_EF_TB: # Add EF-TB constraints here envy_constraints = get_envy_constraints(instance, initial_budgets, map_student_to_best_bundle_per_budget, t, prices) for constraint in envy_constraints: @@ -177,7 +177,7 @@ def check_envy(instance: Instance, student: str, other_student: str, a: dict, t: Example run 6 iteration 5 >>> from fairpyx import Instance - >>> from fairpyx.algorithms import ACEEI + >>> from fairpyx.algorithms import ACEEI_algorithms >>> instance = Instance( ... valuations={"Alice":{"x":5, "y":4, "z":1}, "Bob":{"x":4, "y":6, "z":3}}, @@ -186,7 +186,7 @@ def check_envy(instance: Instance, student: str, other_student: str, a: dict, t: >>> student = "Alice" >>> other_student = "Bob" >>> a = {'Alice': {3.5: ('x', 'y'), 3: ('x', 'z')}, 'Bob': {3.5: ('x', 'y'), 2: ('y', 'z')}} - >>> t = ACEEI.ACEEI.EFTBStatus.EF_TB + >>> t = ACEEI_algorithms.ACEEI.EFTBStatus.EF_TB >>> prices = {"x": 1.5, "y": 2, "z": 0} >>> check_envy(instance, student, other_student, a, t, prices) [(('x', 'z'), ('x', 'y'))] @@ -198,7 +198,7 @@ def check_envy(instance: Instance, student: str, other_student: str, a: dict, t: >>> student = "Alice" >>> other_student = "Bob" >>> a = {'Alice': {0: (), 1.1: ('y')}, 'Bob': {1.1: ('y'), 1: ('x')}} - >>> t = ACEEI.ACEEI.EFTBStatus.EF_TB + >>> t = ACEEI_algorithms.ACEEI.EFTBStatus.EF_TB >>> prices = {"x": 1, "y": 1.1} >>> check_envy(instance, student, other_student, a, t, prices) [((), 'y'), ((), 'x')] @@ -211,7 +211,7 @@ def check_envy(instance: Instance, student: str, other_student: str, a: dict, t: >>> student = "Alice" >>> other_student = "Bob" >>> a = {'Alice': {3.5: ('x', 'y')}, 'Bob': {3.5: ('x'), 2: ('y', 'z')}} - >>> t = ACEEI.ACEEI.EFTBStatus.CONTESTED_EF_TB + >>> t = ACEEI_algorithms.ACEEI.EFTBStatus.CONTESTED_EF_TB >>> prices = {"x": 1, "y": 0.1, "z": 0, "w": 0} >>> check_envy(instance, student, other_student, a, t, prices) [(('x', 'y'), 'x'), (('x', 'y'), ('y', 'z'))] @@ -222,7 +222,7 @@ def check_envy(instance: Instance, student: str, other_student: str, a: dict, t: for bundle_i in a[student].values(): for bundle_j in a[other_student].values(): original_bundle_j = bundle_j - if t == ACEEI.ACEEI.EFTBStatus.CONTESTED_EF_TB: + if t == ACEEI_algorithms.ACEEI.EFTBStatus.CONTESTED_EF_TB: bundle_j = list(bundle_j) # Convert bundle_j to a list # Iterate through keys in prices @@ -269,14 +269,14 @@ def get_envy_constraints(instance: Instance, initial_budgets: dict, a: dict, t: Example run 6 iteration 5 >>> from fairpyx import Instance - >>> from fairpyx.algorithms import ACEEI + >>> from fairpyx.algorithms import ACEEI_algorithms >>> instance = Instance( ... valuations={"Alice":{"x":5, "y":4, "z":1}, "Bob":{"x":4, "y":6, "z":3}}, ... agent_capacities=2, ... item_capacities={"x":1, "y":1, "z":2}) >>> initial_budgets = {"Alice": 5, "Bob": 4} >>> a = {'Alice': {3.5: ('x', 'y'), 3: ('x', 'z')}, 'Bob': {3.5: ('x', 'y'), 2: ('y', 'z')}} - >>> t = ACEEI.ACEEI.EFTBStatus.EF_TB + >>> t = ACEEI_algorithms.ACEEI.EFTBStatus.EF_TB >>> prices = {"x": 1, "y": 1.1} >>> get_envy_constraints(instance, initial_budgets, a, t, prices) [(('Alice', ('x', 'z')), ('Bob', ('x', 'y')))] @@ -287,7 +287,7 @@ def get_envy_constraints(instance: Instance, initial_budgets: dict, a: dict, t: ... item_capacities={"x":1, "y":1}) >>> initial_budgets = {"Alice": 1.1, "Bob": 1} >>> a = {'Alice': {0: (), 1.1: ('y')}, 'Bob': {1.1: ('y'), 1: ('x')}} - >>> t = ACEEI.ACEEI.EFTBStatus.EF_TB + >>> t = ACEEI_algorithms.ACEEI.EFTBStatus.EF_TB >>> prices = {"x": 1, "y": 1.1} >>> get_envy_constraints(instance, initial_budgets, a, t, prices) [(('Alice', ()), ('Bob', 'y')), (('Alice', ()), ('Bob', 'x'))] diff --git a/fairpyx/algorithms/ACEEI/tabu_search.py b/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py similarity index 99% rename from fairpyx/algorithms/ACEEI/tabu_search.py rename to fairpyx/algorithms/ACEEI_algorithms/tabu_search.py index bcd18cb..778a4a1 100644 --- a/fairpyx/algorithms/ACEEI/tabu_search.py +++ b/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py @@ -14,7 +14,7 @@ import numpy as np from fairpyx import Instance, AllocationBuilder -from fairpyx.algorithms.ACEEI.calculate_combinations import get_combinations_courses_sorted +from fairpyx.algorithms.ACEEI_algorithms.calculate_combinations import get_combinations_courses_sorted from functools import lru_cache # Setup logger and colored logs diff --git a/fairpyx/algorithms/__init__.py b/fairpyx/algorithms/__init__.py index 261466d..4cbd0fc 100644 --- a/fairpyx/algorithms/__init__.py +++ b/fairpyx/algorithms/__init__.py @@ -2,8 +2,8 @@ from fairpyx.algorithms.iterated_maximum_matching import iterated_maximum_matching, iterated_maximum_matching_adjusted, iterated_maximum_matching_unadjusted from fairpyx.algorithms.picking_sequence import round_robin, bidirectional_round_robin, serial_dictatorship from fairpyx.algorithms.utilitarian_matching import utilitarian_matching -from fairpyx.algorithms.ACEEI.tabu_search import tabu_search -from fairpyx.algorithms.ACEEI.ACEEI import find_ACEEI_with_EFTB +from fairpyx.algorithms.ACEEI_algorithms.tabu_search import tabu_search +from fairpyx.algorithms.ACEEI_algorithms.ACEEI import find_ACEEI_with_EFTB from fairpyx.algorithms.Optimization_based_Mechanisms.OC import OC_function from fairpyx.algorithms.Optimization_based_Mechanisms.SP_O import SP_O_function from fairpyx.algorithms.Optimization_based_Mechanisms.SP import SP_function diff --git a/tests/test_ACEEI.py b/tests/test_ACEEI.py index a81932f..e958f51 100644 --- a/tests/test_ACEEI.py +++ b/tests/test_ACEEI.py @@ -1,7 +1,7 @@ """ "Practical algorithms and experimentally validated incentives for equilibrium-based fair division (A-CEEI)" -tests for algorithm 1 - ACEEI +tests for algorithm 1 - ACEEI_algorithms Programmers: Erga Bar-Ilan, Ofir Shitrit and Renana Turgeman. Since: 2024-01 @@ -12,9 +12,9 @@ import logging import fairpyx from fairpyx import Instance, divide -# from fairpyx.algorithms import ACEEI +# from fairpyx.algorithms import ACEEI_algorithms # from fairpyx.algorithms.linear_program import optimize_model -from fairpyx.algorithms.ACEEI.ACEEI import EFTBStatus, logger, find_ACEEI_with_EFTB +from fairpyx.algorithms.ACEEI_algorithms.ACEEI import EFTBStatus, logger, find_ACEEI_with_EFTB import numpy as np diff --git a/tests/test_tabu_search.py b/tests/test_tabu_search.py index 006306c..6cdb810 100644 --- a/tests/test_tabu_search.py +++ b/tests/test_tabu_search.py @@ -13,7 +13,7 @@ import fairpyx from fairpyx import Instance, divide -from fairpyx.algorithms.ACEEI.tabu_search import tabu_search +from fairpyx.algorithms.ACEEI_algorithms.tabu_search import tabu_search random_delta = {random.uniform(0.1, 1)} random_beta = random.uniform(1, 100) From 157387d8c45432638cb8e74f36ba1ccc97c68ee9 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Wed, 17 Jul 2024 13:41:40 +0300 Subject: [PATCH 032/111] Update FaStGen.py combining the is_leximin_at_least function with the sourceDec function --- .../Optimization_Matching/FaStGen.py | 71 +++++++------------ 1 file changed, 25 insertions(+), 46 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index ed7b52e..fda654e 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -83,21 +83,25 @@ def FaStGen(alloc: AllocationBuilder, items_valuations:dict)->dict: new_match_leximin_tuple = create_leximin_tuple(match=new_match_str, agents_valuations=agents_valuations, items_valuations=items_valuations) logger.info(f"New match leximin tuple: {new_match_leximin_tuple}") - if is_leximin_at_least(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=match_leximin_tuple): + #Extarcting from the SourceDec function the problematic item or agent, if there isn't one then it will be "" + problematic_component = sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=match_leximin_tuple) + logger.info(f" problematic component: {problematic_component}") + + if problematic_component == "": logger.debug(f"New match is leximin-better than old match:") integer_match = new_integer_match str_match = new_match_str matching_college_valuations = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) logger.debug(f" Match updated to {str_match}") - elif sourceDec(new_match_leximin_tuple, match_leximin_tuple) == up: + elif problematic_component == int_to_college_name[up]: logger.debug(f"New match is leximin-worse because of c_up = c_{up}:") LowerFix.append(up) UpperFix.append(up + 1) logger.info(f" Updated LowerFix and UpperFix with {up}") - elif sourceDec(new_match_leximin_tuple, match_leximin_tuple) in alloc.instance.agents: - sd = sourceDec(new_match_leximin_tuple, match_leximin_tuple) + elif problematic_component in alloc.instance.agents: + sd = problematic_component logger.debug(f"New match is leximin-worse because of student {sd}: ") t = college_name_to_int[get_match(match=str_match, value=sd)] LowerFix.append(t) @@ -119,8 +123,8 @@ def FaStGen(alloc: AllocationBuilder, items_valuations:dict)->dict: ] logger.debug(f" Updating UnFixed to {UnFixed}") - logger.info("Finished FaStGen algorithm") - return str_match #We want to return the final march in his string form + logger.info(f"Finished FaStGen algorithm, final result: {str_match}") + return str_match #We want to return the final match in his string form def LookAheadRoutine(I:tuple, integer_match:dict, down:int, LowerFix:list, UpperFix:list, SoftFix:list)->tuple: """ @@ -186,8 +190,12 @@ def LookAheadRoutine(I:tuple, integer_match:dict, down:int, LowerFix:list, Upper logger.info(f" Old match leximin tuple: {old_match_leximin_tuple}") new_match_leximin_tuple = create_leximin_tuple(match=new_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) logger.info(f" New match leximin tuple: {new_match_leximin_tuple}") + + #Extarcting from the SourceDec function the problematic item or agent, if there isn't one then it will be "" + problematic_component = sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple) + logger.info(f" problematic component: {problematic_component}") - if is_leximin_at_least(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple): + if problematic_component == "": logger.debug(f" New match is leximin-better than old match:") integer_match = new_integer_match LowerFix = LF @@ -195,14 +203,14 @@ def LookAheadRoutine(I:tuple, integer_match:dict, down:int, LowerFix:list, Upper logger.info(" Updated match and fixed LowerFix and UpperFix") break - elif sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple) == up: + elif problematic_component == int_to_college_name[up]: logger.debug(f" New match is leximin-worse because of c_up = c_{up}:") LF.append(up) UF.append(up + 1) logger.info(f" Appended {up} to LF and {up+1} to UF") - elif sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple) in agents: - sd = sourceDec(new_match_leximin_tuple, old_match_leximin_tuple) + elif problematic_component in agents: + sd = problematic_component logger.debug(f" New match is leximin-worse because of student {sd}: ") t = college_name_to_int[get_match(match=new_str_match, value=sd)] logger.debug(f" sourceDec student {sd} is matched to c_t = c_{t}.") @@ -268,37 +276,6 @@ def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:di leximin_tuple = sorted(leximin_tuple, key=lambda x: (x[1], x[0])) return leximin_tuple -def is_leximin_at_least(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->bool: - """ - # Determine whether the leximin tuple of the new match is greater or equal to the leximin tuple of the old match. - - # Args: - # - new_match_leximin_tuple (list): The leximin tuple of the new matching. - # - old_match_leximin_tuple (list): The leximin tuple of the old matching. - - # Returns: - # - bool: True if new_match_leximin_tuple >= old_match_leximin_tuple, otherwise False. - - # Example: - >>> new_match = [("s7",1),("s6",7),("c4",8),("s5",9),("s1",16),("s4",24),("c3",28),("c2",29),("s3",29),("s2",36),("c1",94)] - >>> old_match = [("c4",1),("s7",1),("s1",16),("s6",18),("s5",19),("c3",20),("c2",26),("s3",29),("s2",36),("s4",41),("c1",107)] - >>> is_leximin_at_least(new_match, old_match) - True - - >>> new_match = [("s7",1),("s4",5),("s5",6),("s6",7),("c4",14),("s1",16),("s3",24),("c2",32),("c3",35),("s2",36),("c1",52)] - >>> old_match = [("s7",1),("s6",7),("c4",8),("s5",9),("s1",16),("s4",24),("c3",28),("c2",29),("s3",29),("s2",36),("c1",94)] - >>> is_leximin_at_least(new_match, old_match) - False - """ - for k in range(0, len(new_match_leximin_tuple)): - if new_match_leximin_tuple[k][1] == old_match_leximin_tuple[k][1]: - continue - elif new_match_leximin_tuple[k][1] > old_match_leximin_tuple[k][1]: - return True - else: - return False - return True - def sourceDec(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->str: """ Determine the agent causing the leximin decrease between two matchings. @@ -322,7 +299,11 @@ def sourceDec(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->str: 'c4' """ for k in range(0, len(new_match_leximin_tuple)): - if new_match_leximin_tuple[k][1] < old_match_leximin_tuple[k][1]: + if new_match_leximin_tuple[k][1] == old_match_leximin_tuple[k][1]: + continue + elif new_match_leximin_tuple[k][1] > old_match_leximin_tuple[k][1]: + return "" + else: #new_match_leximin_tuple[k][1] < old_match_leximin_tuple[k][1] return new_match_leximin_tuple[k][0] return "" @@ -532,10 +513,8 @@ def get_match(match:dict, value:str)->any: if __name__ == "__main__": import doctest, sys - # print(doctest.testmod()) - # doctest.run_docstring_examples(is_leximin_at_least, globals()) - # doctest.run_docstring_examples(get_lowest_ranked_student, globals()) - doctest.run_docstring_examples(LookAheadRoutine, globals()) + print(doctest.testmod()) + # doctest.run_docstring_examples(LookAheadRoutine, globals()) sys.exit(0) # logger.setLevel(logging.DEBUG) From 8f0a0c531d2b2842df8afe1e4aa149d77659789e Mon Sep 17 00:00:00 2001 From: ErgaDN Date: Wed, 17 Jul 2024 13:43:31 +0300 Subject: [PATCH 033/111] add find_profitable_manipulation ti init --- fairpyx/algorithms/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fairpyx/algorithms/__init__.py b/fairpyx/algorithms/__init__.py index 4cbd0fc..2817bd4 100644 --- a/fairpyx/algorithms/__init__.py +++ b/fairpyx/algorithms/__init__.py @@ -4,6 +4,7 @@ from fairpyx.algorithms.utilitarian_matching import utilitarian_matching from fairpyx.algorithms.ACEEI_algorithms.tabu_search import tabu_search from fairpyx.algorithms.ACEEI_algorithms.ACEEI import find_ACEEI_with_EFTB +from fairpyx.algorithms.ACEEI_algorithms.find_profitable_manipulation import find_profitable_manipulation from fairpyx.algorithms.Optimization_based_Mechanisms.OC import OC_function from fairpyx.algorithms.Optimization_based_Mechanisms.SP_O import SP_O_function from fairpyx.algorithms.Optimization_based_Mechanisms.SP import SP_function From a7a561f5fe5d18231f0817a2f305a4edad1283eb Mon Sep 17 00:00:00 2001 From: ErgaDN Date: Wed, 17 Jul 2024 17:13:12 +0300 Subject: [PATCH 034/111] add create_constraints_from_neighbors --- .../ACEEI_algorithms/tabu_search.py | 94 ++++++++++++++----- 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py b/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py index 778a4a1..a9b1291 100644 --- a/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py +++ b/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py @@ -22,6 +22,7 @@ # ---------------------The main function--------------------- + def tabu_search(alloc: AllocationBuilder, **kwargs): """ ALGORITHM 3: Tabu search @@ -93,6 +94,8 @@ def tabu_search(alloc: AllocationBuilder, **kwargs): delta = kwargs.get('delta') logger.info("Tabu search: initial budgets = %s, beta = %s, delta = %s", initial_budgets, beta, delta) + print(f"--- initial_budgets = {initial_budgets} ---") + prices = {course: random.uniform(1, 1 + beta) for course in alloc.instance.items} logger.info("1) Let 𝒑 ← uniform(1, 1 + 𝛽)^𝑚, H ← ∅: p = %s", prices) history = [] @@ -100,6 +103,7 @@ def tabu_search(alloc: AllocationBuilder, **kwargs): logger.info("2) If ∥𝒛(𝒖,𝒄, 𝒑, 𝒃0)∥2 = 0, terminate with 𝒑∗ = 𝒑.") + best_allocation, best_prices, best_norma = None, None, np.inf while True: max_utilities_allocations = student_best_bundles(prices.copy(), alloc.instance, initial_budgets, combinations_courses_sorted) @@ -107,7 +111,7 @@ def tabu_search(alloc: AllocationBuilder, **kwargs): max_utilities_allocations) logger.info("\nprices=%s, excess demand=%s, best bundle=%s, norma=%s", prices, excess_demand_vector, allocation, norma) - best_allocation, best_prices, best_norma = allocation, prices, norma + # best_allocation, best_prices, best_norma = allocation, prices, norma if np.allclose(norma, 0): logger.info("2) ∥𝒛(𝒖,𝒄, 𝒑, 𝒃0)∥2 = 0: terminate with 𝒑∗ = 𝒑.") break @@ -115,9 +119,13 @@ def tabu_search(alloc: AllocationBuilder, **kwargs): logger.info("3) Include all equivalent prices of 𝒑 into the history: H ← H + {𝒑′ : 𝒑′ ∼𝑝 𝒑}") equivalent_prices = find_all_equivalent_prices(alloc.instance, initial_budgets, allocation) history.append(equivalent_prices) + # logger.warning(type(equivalent_prices)) neighbors = find_all_neighbors(alloc.instance, history, prices, delta, excess_demand_vector, initial_budgets, allocation, combinations_courses_sorted) + constraints = create_constraints_from_neighbors(neighbors) + # history.append(neighbors) + # logger.warning(type(neighbors)) logger.info("Found %d neighbors", len(neighbors)) if len(neighbors) == 0: logger.info("--- No new neighbors to price-vector - no optimal solution") @@ -127,7 +135,7 @@ def tabu_search(alloc: AllocationBuilder, **kwargs): allocation, excess_demand_vector, norma, prices = find_min_error_prices(alloc.instance, neighbors, initial_budgets, combinations_courses_sorted) - + logger.warning(f"--- allocation = {allocation} ---") if norma < best_norma: logger.info(" Found a better norma") best_allocation, best_prices, best_norma = allocation, prices, norma @@ -143,6 +151,39 @@ def tabu_search(alloc: AllocationBuilder, **kwargs): # ---------------------helper functions:--------------------- +def create_constraints_from_neighbors(neighbors): + """ + Create lambda constraints from neighbors. + + :param neighbors: (list of dicts): List of dictionaries where each dictionary represents a neighbor with courses + and their values. + + :return: lambda_groups (list of list of lambda functions): List of groups of lambda functions, each group + representing constraints based on a neighbor. + + + + >>> neighbors = [{'x': 1, 'y': 4, 'z': 0}] + >>> ans = create_constraints_from_neighbors(neighbors) + >>> p = {'x': 1, 'y': 4, 'z': 0} + >>> all([f(p) for f in ans[0]]) + True + + """ + lambda_groups = [] + + for neighbor in neighbors: + lambda_group = [] + + for course, value in neighbor.items(): + lambda_func = lambda p, key=course, val=value: p[key] == val + lambda_group.append(lambda_func) + + lambda_groups.append(lambda_group) + + return lambda_groups + + def min_excess_demand_for_allocation(instance: Instance, prices: dict, max_utilities_allocations: list[dict]): """ Goes through all allocations with the highest utilities of the students, and returns the allocation with the @@ -442,9 +483,6 @@ def find_all_equivalent_prices(instance: Instance, initial_budgets: dict, alloca continue if current_utility >= original_utility: - # Create a copy of sorted_combination for the lambda function - combination_copy = sorted_combination.copy() # todo - we dont use it - func = lambda p, agent=student, keys=allocation[student]: ( sum(p[key] for key in keys) > initial_budgets[agent]) equivalent_prices.append(func) @@ -723,6 +761,7 @@ def run_tabu_search(alloc: AllocationBuilder, **kwargs): initial_budgets = random_initial_budgets(alloc.instance.num_of_agents, beta) return tabu_search(alloc, initial_budgets=initial_budgets, beta=beta, delta={0.34}, **kwargs) + def random_initial_budgets(num_of_agents: int, beta: float = 100) -> dict: # Create initial budgets for each agent, uniformly distributed in the range [1, 1 + beta] initial_budgets = np.random.uniform(1, 1 + beta, num_of_agents) @@ -739,6 +778,7 @@ def random_initial_budgets(num_of_agents: int, beta: float = 100) -> dict: logger.setLevel(logging.INFO) import coloredlogs + level_styles = { 'debug': {'color': 'green'}, 'info': {'color': 'cyan'}, @@ -756,18 +796,19 @@ def random_initial_budgets(num_of_agents: int, beta: float = 100) -> dict: def random_initial_budgets(num): return {f"s{key}": random.uniform(1, 1 + random_beta) for key in range(1, num + 1)} - - num_of_agents = 3 - utilities = {f"s{i}": {f"c{num_of_agents + 1 - j}": j for j in range(num_of_agents, 0, -1)} for i in - range(1, num_of_agents + 1)} - instance = Instance(valuations=utilities, agent_capacities=1, item_capacities=1) - initial_budgets = {f"s{key}": (num_of_agents + 1 - key) for key in range(1, num_of_agents + 1)} - logger.error(f"initial_budgets = {initial_budgets}") - logger.error(f"random_beta = {random_beta}") - # initial_budgets = {f"s{key}": (random_beta + key) for key in range(1, num_of_agents + 1)} - allocation = divide(tabu_search, instance=instance, - initial_budgets=initial_budgets, - beta=random_beta, delta=random_delta) + + + # num_of_agents = 3 + # utilities = {f"s{i}": {f"c{num_of_agents + 1 - j}": j for j in range(num_of_agents, 0, -1)} for i in + # range(1, num_of_agents + 1)} + # instance = Instance(valuations=utilities, agent_capacities=1, item_capacities=1) + # initial_budgets = {f"s{key}": (num_of_agents + 1 - key) for key in range(1, num_of_agents + 1)} + # logger.error(f"initial_budgets = {initial_budgets}") + # logger.error(f"random_beta = {random_beta}") + # # initial_budgets = {f"s{key}": (random_beta + key) for key in range(1, num_of_agents + 1)} + # allocation = divide(tabu_search, instance=instance, + # initial_budgets=initial_budgets, + # beta=random_beta, delta=random_delta) # for i in range(1, num_of_agents + 1): # assert (f"c{i}" in allocation[f"s{i}"]) @@ -780,11 +821,20 @@ def random_initial_budgets(num): # with open('seed.txt', 'a') as file: # file.write(f"seed is {seed}\n") # - # # instance = Instance(valuations = {"ami": {"x": 4, "y": 3, "z": 2}, "tami": {"x": 5, "y": 1, "z": 2}}, agent_capacities = 2, - # # item_capacities = {"x": 1, "y": 2, "z": 3}) - # # initial_budgets = {"ami": 6, "tami": 4} - # # beta = 6 - # # divide(tabu_search, instance=instance, initial_budgets=initial_budgets, beta=beta, delta={0.72}) + + random.seed(0) + instance = Instance( + valuations={'s1': {'c1': 275, 'c2': 79, 'c3': 59, 'c4': 63, 'c5': 54, 'c6': 226, 'c7': 133, 'c8': 110}, + 's2': {'c1': 105, 'c2': 17, 'c3': 222, 'c4': 202, 'c5': 227, 'c6': 89, 'c7': 30, 'c8': 107}, + 's3': {'c1': 265, 'c2': 120, 'c3': 37, 'c4': 230, 'c5': 160, 'c6': 44, 'c7': 30, 'c8': 113}, + 's4': {'c1': 194, 'c2': 132, 'c3': 224, 'c4': 77, 'c5': 29, 'c6': 230, 'c7': 62, 'c8': 52}, + 's5': {'c1': 174, 'c2': 89, 'c3': 229, 'c4': 249, 'c5': 24, 'c6': 83, 'c7': 99, 'c8': 52}}, + agent_capacities=5, + item_capacities={'c1': 3.0, 'c2': 3.0, 'c3': 3.0, 'c4': 3.0, 'c5': 3.0, 'c6': 3.0, 'c7': 3.0, 'c8': 3.0}) + initial_budgets = {'s1': 1.0005695511898616, 's2': 1.0009070710569965, 's3': 1.000699704772071, + 's4': 1.000078616581918, 's5': 1.0008131880118405} + beta = 0.001 + divide(tabu_search, instance=instance, initial_budgets=initial_budgets, beta=beta, delta={0.34}) # # # "{ami:['x', 'y'], tami:['y', 'z']}" # # # instance = Instance(valuations = {"ami": {"x": 4, "y": 3, "z": 2}, "tami": {"x": 5, "y": 1, "z": 2}}, From 0933c5352e1a62164f433cb8362289a0364bf0fe Mon Sep 17 00:00:00 2001 From: ErgaDN Date: Wed, 17 Jul 2024 17:24:40 +0300 Subject: [PATCH 035/111] add neighbors to the history --- fairpyx/algorithms/ACEEI_algorithms/tabu_search.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py b/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py index a9b1291..f90aed0 100644 --- a/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py +++ b/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py @@ -94,7 +94,7 @@ def tabu_search(alloc: AllocationBuilder, **kwargs): delta = kwargs.get('delta') logger.info("Tabu search: initial budgets = %s, beta = %s, delta = %s", initial_budgets, beta, delta) - print(f"--- initial_budgets = {initial_budgets} ---") + # print(f"--- initial_budgets = {initial_budgets} ---") prices = {course: random.uniform(1, 1 + beta) for course in alloc.instance.items} logger.info("1) Let 𝒑 ← uniform(1, 1 + 𝛽)^𝑚, H ← ∅: p = %s", prices) @@ -124,7 +124,7 @@ def tabu_search(alloc: AllocationBuilder, **kwargs): initial_budgets, allocation, combinations_courses_sorted) constraints = create_constraints_from_neighbors(neighbors) - # history.append(neighbors) + history.extend(constraints) # logger.warning(type(neighbors)) logger.info("Found %d neighbors", len(neighbors)) if len(neighbors) == 0: @@ -169,6 +169,16 @@ def create_constraints_from_neighbors(neighbors): >>> all([f(p) for f in ans[0]]) True + >>> neighbors = [{'x': 1, 'y': 4, 'z': 0}, {'x': 2, 'y': 3, 'z': 5}] + >>> ans = create_constraints_from_neighbors(neighbors) + >>> p = {'x': 1, 'y': 3, 'z': 0} + >>> any(all([f(p) for f in s]) for s in ans) + False + + >>> p = {'x': 2, 'y': 3, 'z': 5} + >>> any(all([f(p) for f in s]) for s in ans) + True + """ lambda_groups = [] From 3a91e7f2e5f050516e46eb0c630ee8e11c4466b4 Mon Sep 17 00:00:00 2001 From: ErgaDN Date: Wed, 17 Jul 2024 17:34:53 +0300 Subject: [PATCH 036/111] clear loggers --- .../ACEEI_algorithms/tabu_search.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py b/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py index f90aed0..1d7d623 100644 --- a/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py +++ b/fairpyx/algorithms/ACEEI_algorithms/tabu_search.py @@ -88,14 +88,27 @@ def tabu_search(alloc: AllocationBuilder, **kwargs): >>> beta = 5 >>> stringify(divide(tabu_search, instance=instance, initial_budgets=initial_budgets,beta=beta, delta={0.34})) "{ami:['x', 'y'], tami:['y', 'z'], tzumi:['z']}" + + >>> random.seed(0) + >>> instance = Instance( + ... valuations={'s1': {'c1': 275, 'c2': 79, 'c3': 59, 'c4': 63, 'c5': 54, 'c6': 226, 'c7': 133, 'c8': 110}, + ... 's2': {'c1': 105, 'c2': 17, 'c3': 222, 'c4': 202, 'c5': 227, 'c6': 89, 'c7': 30, 'c8': 107}, + ... 's3': {'c1': 265, 'c2': 120, 'c3': 37, 'c4': 230, 'c5': 160, 'c6': 44, 'c7': 30, 'c8': 113}, + ... 's4': {'c1': 194, 'c2': 132, 'c3': 224, 'c4': 77, 'c5': 29, 'c6': 230, 'c7': 62, 'c8': 52}, + ... 's5': {'c1': 174, 'c2': 89, 'c3': 229, 'c4': 249, 'c5': 24, 'c6': 83, 'c7': 99, 'c8': 52}}, + ... agent_capacities=5, + ... item_capacities={'c1': 3.0, 'c2': 3.0, 'c3': 3.0, 'c4': 3.0, 'c5': 3.0, 'c6': 3.0, 'c7': 3.0, 'c8': 3.0}) + >>> initial_budgets = {'s1': 1.0005695511898616, 's2': 1.0009070710569965, 's3': 1.000699704772071, + ... 's4': 1.000078616581918, 's5': 1.0008131880118405} + >>> beta = 0.001 + >>> stringify(divide(tabu_search, instance=instance, initial_budgets=initial_budgets,beta=beta, delta={0.34})) + "{s1:['c1', 'c2', 'c6', 'c7', 'c8'], s2:['c1', 'c3', 'c4', 'c5', 'c8'], s3:['c1', 'c2', 'c4', 'c5', 'c8'], s4:['c2', 'c3', 'c6', 'c7'], s5:['c3', 'c4', 'c7']}" """ initial_budgets = kwargs.get('initial_budgets') beta = kwargs.get('beta') delta = kwargs.get('delta') logger.info("Tabu search: initial budgets = %s, beta = %s, delta = %s", initial_budgets, beta, delta) - # print(f"--- initial_budgets = {initial_budgets} ---") - prices = {course: random.uniform(1, 1 + beta) for course in alloc.instance.items} logger.info("1) Let 𝒑 ← uniform(1, 1 + 𝛽)^𝑚, H ← ∅: p = %s", prices) history = [] @@ -119,13 +132,11 @@ def tabu_search(alloc: AllocationBuilder, **kwargs): logger.info("3) Include all equivalent prices of 𝒑 into the history: H ← H + {𝒑′ : 𝒑′ ∼𝑝 𝒑}") equivalent_prices = find_all_equivalent_prices(alloc.instance, initial_budgets, allocation) history.append(equivalent_prices) - # logger.warning(type(equivalent_prices)) neighbors = find_all_neighbors(alloc.instance, history, prices, delta, excess_demand_vector, initial_budgets, allocation, combinations_courses_sorted) constraints = create_constraints_from_neighbors(neighbors) history.extend(constraints) - # logger.warning(type(neighbors)) logger.info("Found %d neighbors", len(neighbors)) if len(neighbors) == 0: logger.info("--- No new neighbors to price-vector - no optimal solution") @@ -135,7 +146,6 @@ def tabu_search(alloc: AllocationBuilder, **kwargs): allocation, excess_demand_vector, norma, prices = find_min_error_prices(alloc.instance, neighbors, initial_budgets, combinations_courses_sorted) - logger.warning(f"--- allocation = {allocation} ---") if norma < best_norma: logger.info(" Found a better norma") best_allocation, best_prices, best_norma = allocation, prices, norma From 5adf1778c6807d84d7a91e855b8ffc14ba8c5369 Mon Sep 17 00:00:00 2001 From: ErgaDN Date: Wed, 17 Jul 2024 17:36:22 +0300 Subject: [PATCH 037/111] clear loggers --- fairpyx/algorithms/ACEEI_algorithms/ACEEI.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fairpyx/algorithms/ACEEI_algorithms/ACEEI.py b/fairpyx/algorithms/ACEEI_algorithms/ACEEI.py index 3e2f35d..6320315 100644 --- a/fairpyx/algorithms/ACEEI_algorithms/ACEEI.py +++ b/fairpyx/algorithms/ACEEI_algorithms/ACEEI.py @@ -152,7 +152,7 @@ def find_ACEEI_with_EFTB(alloc: AllocationBuilder, **kwargs): initial_budgets, epsilon, prices, alloc.instance, t, combinations_courses_sorted) if clearing_error is None: - print("Clearing error is None - No Solution") + logger.info("Clearing error is None - No Solution") # raise ValueError("Clearing error is None") break # 3) If ∥𝒛˜(𝒖,𝒄, 𝒑, 𝒃) ∥2 = 0, terminate with 𝒑* = 𝒑, 𝒃* = 𝒃 @@ -307,7 +307,6 @@ def ACEEI_without_EFTB(alloc: AllocationBuilder, **kwargs): def ACEEI_with_EFTB(alloc: AllocationBuilder, **kwargs): initial_budgets = random_initial_budgets(alloc.instance.num_of_agents) - # print(f"--- initial_budgets = {initial_budgets} ---") return find_ACEEI_with_EFTB(alloc, initial_budgets=initial_budgets, delta=0.5, epsilon=3.0, t=EFTBStatus.EF_TB, **kwargs) From 059bc41016075ef54917f33c47ca507b9fd89497 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Wed, 17 Jul 2024 17:56:00 +0300 Subject: [PATCH 038/111] fixing FaStGen --- fairpyx/algorithms/Optimization_Matching/FaStGen.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index fda654e..b964502 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -7,8 +7,8 @@ from fairpyx import Instance, AllocationBuilder, ExplanationLogger from FaSt import Demote +from AgentItem import AgentItem, create_agent_items from copy import deepcopy -#import sys import logging logger = logging.getLogger(__name__) @@ -28,7 +28,7 @@ def FaStGen(alloc: AllocationBuilder, items_valuations:dict)->dict: >>> ins = Instance(agents=S, items=C, valuations=U) >>> alloc = AllocationBuilder(instance=ins) >>> FaStGen(alloc=alloc, items_valuations=V) - {'c1': ['s1', 's2', 's3'], 'c2': ['s4'], 'c3': ['s5'], 'c4': ['s7', 's6']} + {'c1': ['s1', 's2', 's3'], 'c2': ['s4'], 'c3': ['s5'], 'c4': ['s7', 's6']} """ logger.info("Starting FaStGen algorithm") @@ -169,9 +169,9 @@ def LookAheadRoutine(I:tuple, integer_match:dict, down:int, LowerFix:list, Upper new_str_match = integer_to_str_matching(integer_match=new_integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) logger.info(f"Starting LookAheadRoutine. Initial parameters - match: {new_str_match}, down: {down}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") - matching_college_valuations = update_matching_valuations_sum(match=given_str_match,items_valuations=items_valuations) + matching_college_valuations = update_matching_valuations_sum(match=new_str_match,items_valuations=items_valuations) while len(LF) + len([item for item in UF if item not in LF]) < len(items): - up = min([j for j in college_name_to_int.values() if j not in LowerFix]) + up = min([j for j in college_name_to_int.values() if j not in LF]) logger.debug(f" Selected 'up': {up}") if (len(integer_match[up]) == 1) or (matching_college_valuations[int_to_college_name[up]] <= matching_college_valuations[int_to_college_name[down]]): LF.append(up) @@ -450,9 +450,6 @@ def generate_dict_from_int_to_str(input_list:list)->dict: {1: 'c1', 2: 'c2', 3: 'c3', 4: 'c4'} """ return {index + 1: item for index, item in enumerate(input_list)} - -# def get_key_by_value(value, items_dict): -# return next(key for key, val in items_dict.items() if val == value) def integer_to_str_matching(integer_match:dict, agent_dict:dict, items_dict:dict)->dict: """ From 6d559a3a8b7e6391eea6b26e6a1220bc12e569c0 Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Thu, 18 Jul 2024 09:09:37 +0300 Subject: [PATCH 039/111] Update FaSt.py --- .../algorithms/Optimization_Matching/FaSt.py | 97 +++++++++---------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index ac846d0..08d7a45 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -8,9 +8,15 @@ from fairpyx import Instance, AllocationBuilder, ExplanationLogger import logging -# Object to insert the relevant data +#Object to insert the relevant data logger = logging.getLogger("data") - +console=logging.StreamHandler() #writes to stderr (= cerr) +logger.handlers=[console] # we want the logs to be written to console +# Change logger level +logger.setLevel(logging.DEBUG) +#logger.addHandler(console) +logger.debug("blah blah") + @@ -59,7 +65,6 @@ def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dic p -= 1 return matching #Return the matching after the change -##ADD THAT CHANGE THE MATCHING (THE ORIGINAL)#####RETURN NULL AND CHANGE THE MATCHING##MAKE SURE WITH HADAR.#################### def get_leximin_tuple(matching, V): """ @@ -235,7 +240,7 @@ def FaSt(alloc: AllocationBuilder)-> dict: >>> ins = Instance(agents=agents, items=items, valuations=valuation) >>> alloc = AllocationBuilder(instance=ins) >>> FaSt(alloc=alloc) - {1: [1,2,3], 2: [5, 4], 3: [7, 6]}""" + {1: [1, 2, 3], 2: [5, 4], 3: [7, 6]}""" S = alloc.instance.agents C = alloc.instance.items @@ -243,7 +248,7 @@ def FaSt(alloc: AllocationBuilder)-> dict: # Now V look like this: # "Alice": {"c1":2, "c2": 3, "c3": 4}, # "Bob": {"c1": 4, "c2": 5, "c3": 6} - logger.info('FaSt(%g)',alloc) + logger.info('FaSt(%s,%s,%s)',S,C,V) n=len(S)# number of students m = len(C) # number of colleges i = n - 1 # start from the last student @@ -255,43 +260,37 @@ def FaSt(alloc: AllocationBuilder)-> dict: # Initialize the leximin tuple lex_tupl=get_leximin_tuple(initial_matching,V) -# Initialize the position array pos +# Initialize the position array pos, F, college_values pos= build_pos_array(initial_matching, V) college_values=build_college_values(initial_matching,V) - logger.debug('Initial i:%g', i) - logger.debug('Initial j:%g', j) + logger.debug('Initial i:%d', i) + logger.debug('Initial j:%d', j) + # Initialize F as a list of two lists: one for students, one for colleges + F = [[], []] + F[0].append(n) # Add sn to the student list in F + logger.debug('Initialized F: %s', F) + - print("i: ", i) - print("j: ", j) index = 1 while i > j - 1 and j > 1: - logger.debug('Iteration number %g', index) - logger.debug('Current i:%g', i) - logger.debug('Current j:%g ', j) - print("******** Iteration number ", index, "********") - print("i: ", i) - print("j: ", j) - # logger.debug('college_values[j+1]: %g', college_values[j + 1])############################ - # print("college_values[j+1]: ", college_values[j + 1])##########################3 - print("college_values: ", college_values) - print("V:", V) - logger.debug('V[i-1][j-1]: %g', V[i - 1][j - 1]) - logger.debug('college_values: %g', college_values) - - print("V[i-1][j-1]: ", V[i - 1][j - 1]) - print("college_values[j]: ", college_values[j]) + logger.debug('**Iteration number %d**', index) + logger.debug('Current i:%d', i) + logger.debug('Current j:%d ', j) + + logger.debug('V: %s', V) + logger.debug('V[i-1][j-1]: %d', V[i - 1][j - 1]) + logger.debug('college_values:%s ', college_values) + logger.debug('college_values[j]: %d', college_values[j]) + initial_j=j #for updating F in the end # IMPORTANT! in the variable college_values we use in j and not j-1 because it build like this: {1: 35, 2: 3, 3: 1} # So this: college_values[j] indeed gave us the right index ! [i.e. different structure!] if college_values[j] >= V[i - 1][j-1]: # In the algo the college_values is actually v j -= 1 else: - if college_values[j] < V[i - 1][j - 1]: #Raw 11- different indixes because of different structures. - logger.debug('V[i-1][j-1]: %g', V[i-1][j-1]) - print("V[i-1][j-1]:", V[i - 1][j-1]) - # if V[i][j - 1] > L[j - 1]: + if college_values[j] < V[i - 1][j - 1]: #Raw 11 in the article- different indixes because of different structures. + # print("V[i-1][j-1]:", V[i - 1][j-1]) initial_matching = Demote(initial_matching, i, j, 1) - print("initial_matching after demote:", initial_matching) - logger.debug('initial_matching after demote: %g', initial_matching) + logger.debug('initial_matching after demote: %s', initial_matching) else: if V[i - 1][j - 1] < college_values[j]:#Raw 14 @@ -315,31 +314,31 @@ def FaSt(alloc: AllocationBuilder)-> dict: t += 1 if k == j - 1 and initial_matching != µ_prime: j -= 1 - ### ADD UPDATE F######################################################################## # Updates - college_values = build_college_values(initial_matching, V) # Update the college values - lex_tupl = get_leximin_tuple(initial_matching, V) ############################################ - logger.debug('lex_tupl: %g', lex_tupl) - print("lex_tupl: ", lex_tupl) - pos = build_pos_array(initial_matching, V)############################################### - logger.debug('pos: %g', pos) - print("pos:", pos) + # Update the college values + college_values = build_college_values(initial_matching, V) + # Update leximin tuple + lex_tupl = get_leximin_tuple(initial_matching, V) + logger.debug('lex_tupl: %s', lex_tupl) + + # Update position array + pos = build_pos_array(initial_matching, V) + logger.debug('pos: %s', pos) + + # Update F + # Insert student i + F[0].append(i) + # Insert college j + if j not in F[1]: + F[1].append(j) + logger.debug('Updated F: %s', F) i -= 1 index += 1 - logger.debug('END while, i: %g, j: %g',i, j) - - #print("END while :") - #print("i: ", i) - #print("j: ", j) + logger.debug('END while, i: %d, j: %d',i, j) return initial_matching - if __name__ == "__main__": import doctest - console=logging.StreamHandler() #writes to stderr (= cerr) - logger.handlers=[console] # we want the logs to be written to console - # Change logger level - logger.setLevel(logging.INFO) doctest.testmod() From 96736e83811d1fb3e459ae970218b074e6d24426 Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:34:16 +0300 Subject: [PATCH 040/111] Update FaSt.py --- fairpyx/algorithms/Optimization_Matching/FaSt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 08d7a45..3c55972 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -7,6 +7,7 @@ from fairpyx import Instance, AllocationBuilder, ExplanationLogger import logging +import copy # For deep copy use #Object to insert the relevant data logger = logging.getLogger("data") @@ -14,12 +15,9 @@ logger.handlers=[console] # we want the logs to be written to console # Change logger level logger.setLevel(logging.DEBUG) -#logger.addHandler(console) -logger.debug("blah blah") - def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dict: """ Demote algorithm: Adjust the matching by moving a student to a lower-ranked college From 930d33bbc24028328d4869c2ea2e2aae8390fae7 Mon Sep 17 00:00:00 2001 From: Erel Segal-Halevi Date: Fri, 19 Jul 2024 11:57:46 +0300 Subject: [PATCH 041/111] change F --- .../algorithms/Optimization_Matching/FaSt.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 3c55972..bb72d9e 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -11,11 +11,6 @@ #Object to insert the relevant data logger = logging.getLogger("data") -console=logging.StreamHandler() #writes to stderr (= cerr) -logger.handlers=[console] # we want the logs to be written to console -# Change logger level -logger.setLevel(logging.DEBUG) - def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dict: @@ -118,8 +113,8 @@ def build_pos_array(matching, V): """ Build the pos array based on the leximin tuple and the matching. For example: - Leximin Tuple: [**9**, 8, 7, 6, 5, 3, 1, 35, 3, 1] -> V of S1 is 9 - Sorted Leximin Tuple: [1, 1, 3, 3, 5, 6, 7, 8, 9,35]-> 9 is in index 8 + Unsorted Leximin Tuple: [**9**, 8, 7, 6, 5, 3, 1, 35, 3, 1] -> V of S1 is 9 + Sorted Leximin Tuple: [1, 1, 3, 3, 5, 6, 7, 8, 9,35]-> 9 is in index 8 pos=[8,7, 6,5,4,3,1,9,2,0] :param leximin_tuple: The leximin tuple @@ -258,15 +253,16 @@ def FaSt(alloc: AllocationBuilder)-> dict: # Initialize the leximin tuple lex_tupl=get_leximin_tuple(initial_matching,V) -# Initialize the position array pos, F, college_values + # Initialize the position array pos, F, college_values pos= build_pos_array(initial_matching, V) college_values=build_college_values(initial_matching,V) logger.debug('Initial i:%d', i) logger.debug('Initial j:%d', j) # Initialize F as a list of two lists: one for students, one for colleges - F = [[], []] - F[0].append(n) # Add sn to the student list in F - logger.debug('Initialized F: %s', F) + F_stduents = [] + F_colleges = [] + F_stduents.append(n) # Add sn to the student list in F + logger.debug('Initialized F_students: %s, F_colleges: %s', F_stduents, F_colleges) index = 1 @@ -325,11 +321,11 @@ def FaSt(alloc: AllocationBuilder)-> dict: # Update F # Insert student i - F[0].append(i) + F_stduents.append(i) # Insert college j - if j not in F[1]: - F[1].append(j) - logger.debug('Updated F: %s', F) + if j not in F_colleges: + F_colleges.append(j) + logger.debug('Updated F_students: %s, F_colleges: %s', F_stduents, F_colleges) i -= 1 index += 1 @@ -338,5 +334,10 @@ def FaSt(alloc: AllocationBuilder)-> dict: return initial_matching if __name__ == "__main__": + console=logging.StreamHandler() #writes to stderr (= cerr) + logger.handlers=[console] # we want the logs to be written to console + # Change logger level + logger.setLevel(logging.DEBUG) + import doctest doctest.testmod() From e570275fe654b8d3af684d3dabef0a863ac0c945 Mon Sep 17 00:00:00 2001 From: Erel Segal-Halevi Date: Fri, 19 Jul 2024 16:56:24 +0300 Subject: [PATCH 042/111] shorten function --- fairpyx/algorithms/Optimization_Matching/FaSt.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index bb72d9e..01ef352 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -205,12 +205,7 @@ def convert_valuations_to_matrix(valuations): """ students = sorted(valuations.keys()) # Sort student keys to maintain order colleges = sorted(valuations[students[0]].keys()) # Sort college keys to maintain order - - V = [] - for student in students: - V.append([valuations[student][college] for college in colleges]) - - return V + return [[valuations[student][college] for college in colleges] for student in students] def FaSt(alloc: AllocationBuilder)-> dict: """ @@ -265,9 +260,9 @@ def FaSt(alloc: AllocationBuilder)-> dict: logger.debug('Initialized F_students: %s, F_colleges: %s', F_stduents, F_colleges) - index = 1 + iteration = 1 # For logging while i > j - 1 and j > 1: - logger.debug('**Iteration number %d**', index) + logger.debug('\n**Iteration number %d**', iteration) logger.debug('Current i:%d', i) logger.debug('Current j:%d ', j) @@ -328,7 +323,7 @@ def FaSt(alloc: AllocationBuilder)-> dict: logger.debug('Updated F_students: %s, F_colleges: %s', F_stduents, F_colleges) i -= 1 - index += 1 + iteration += 1 logger.debug('END while, i: %d, j: %d',i, j) return initial_matching From 9abe5e0f8d75e34631b73884c7b00d85750d57d2 Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Sat, 20 Jul 2024 21:55:24 +0300 Subject: [PATCH 043/111] FaSt is fixed as article. --- .../algorithms/Optimization_Matching/FaSt.py | 88 +++++++++++-------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 01ef352..f6c3964 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -38,7 +38,7 @@ def Demote(matching:dict, student_index:int, down_index:int, up_index:int)-> dic t = student_index # Set p to 'down' p = down_index - + logger.info("matching: %s",matching) # Check if student 't' is in college 'Cp-1' if t not in matching[p - 1]: raise ValueError(f"Student {t} should be in matching to college {p - 1}") @@ -68,10 +68,11 @@ def get_leximin_tuple(matching, V): :param V: The evaluations matrix :return: Leximin tuple >>> matching = {1: [1, 2, 3, 4], 2: [5], 3: [7, 6]} - >>> V = [[9, 8, 7], [8, 7, 6], [7, 6, 5], [6, 5, 4], [5, 4, 3], [4, 3, 2], [3, 2, 1]] + >>> V = [[], [0, 9, 8, 7], [0, 8, 7, 6], [0, 7, 6, 5], [0, 6, 5, 4], [0, 5, 4, 3], [0, 4, 3, 2], [0, 3, 2, 1]] >>> get_leximin_tuple(matching, V) [1, 2, 3, 4, 4, 6, 7, 8, 9, 30] """ + leximin_tuple=get_unsorted_leximin_tuple(matching, V) # Sort the leximin tuple in descending order leximin_tuple.sort(reverse=False) @@ -88,7 +89,7 @@ def get_unsorted_leximin_tuple(matching, V): :param V: The evaluations matrix :return: UNSORTED Leximin tuple >>> matching = {1: [1, 2, 3, 4], 2: [5], 3: [7, 6]} - >>> V = [[9, 8, 7], [8, 7, 6], [7, 6, 5], [6, 5, 4], [5, 4, 3], [4, 3, 2], [3, 2, 1]] + >>> V = [[], [0, 9, 8, 7], [0, 8, 7, 6], [0, 7, 6, 5], [0, 6, 5, 4], [0, 5, 4, 3], [0, 4, 3, 2], [0, 3, 2, 1]] >>> get_unsorted_leximin_tuple(matching, V) [9, 8, 7, 6, 4, 1, 2, 30, 4, 3] """ @@ -100,7 +101,7 @@ def get_unsorted_leximin_tuple(matching, V): college_sum = 0 # For each student in the college, append their valuation for the college to the leximin tuple for student in students: - valuation = V[student - 1][college - 1] + valuation = V[student][college] leximin_tuple.append(valuation) college_sum += valuation college_sums.append(college_sum) @@ -108,7 +109,6 @@ def get_unsorted_leximin_tuple(matching, V): leximin_tuple.extend(college_sums) return leximin_tuple -## ADD START IND of SI IS 0## def build_pos_array(matching, V): """ Build the pos array based on the leximin tuple and the matching. @@ -122,7 +122,7 @@ def build_pos_array(matching, V): :param V: The evaluations matrix :return: Pos array >>> matching = {1: [1, 2, 3, 4], 2: [5], 3: [7, 6]} - >>> V = [[9, 8, 7], [8, 7, 6], [7, 6, 5], [6, 5, 4], [5, 4, 3], [4, 3, 2], [3, 2, 1]] + >>> V = [[], [0, 9, 8, 7], [0, 8, 7, 6], [0, 7, 6, 5], [0, 6, 5, 4], [0, 5, 4, 3], [0, 4, 3, 2], [0, 3, 2, 1]] >>> build_pos_array(matching, V) [8, 7, 6, 5, 3, 0, 1, 9, 3, 2] """ @@ -134,7 +134,7 @@ def build_pos_array(matching, V): # Get the sorted leximin tuple leximin_sorted_tuple = sorted(leximin_unsorted_tuple) # Build pos array for students - while student_index < len(V): + while student_index < len(V)-1:# -1 because i added an element to V pos_value = leximin_sorted_tuple.index(leximin_unsorted_tuple[student_index]) pos.append(pos_value) student_index += 1 @@ -157,15 +157,15 @@ def build_college_values(matching, V): :param V: The evaluations matrix :return: College values dictionary >>> matching = {1: [1, 2, 3, 4], 2: [5], 3: [7, 6]} - >>> V = [[9, 8, 7], [8, 7, 6], [7, 6, 5], [6, 5, 4], [5, 4, 3], [4, 3, 2], [3, 2, 1]] + >>> V = [[], [0, 9, 8, 7], [0, 8, 7, 6], [0, 7, 6, 5], [0, 6, 5, 4], [0, 5, 4, 3], [0, 4, 3, 2], [0, 3, 2, 1]] >>> build_college_values(matching, V) - {1: 30, 2: 4, 3: 3} + {0: 0, 1: 30, 2: 4, 3: 3} """ - college_values = {} + college_values = {0 : 0} # Iterate over each college in the matching for college, students in matching.items(): - college_sum = sum(V[student - 1][college - 1] for student in students) + college_sum = sum(V[student][college] for student in students) college_values[college] = college_sum return college_values @@ -201,11 +201,15 @@ def convert_valuations_to_matrix(valuations): :return: Matrix of valuations >>> valuations={'S1': {'c1': 9, 'c2': 8, 'c3': 7}, 'S2': {'c1': 8, 'c2': 7, 'c3': 6}, 'S3': {'c1': 7, 'c2': 6, 'c3': 5}, 'S4': {'c1': 6, 'c2': 5, 'c3': 4}, 'S5': {'c1': 5, 'c2': 4, 'c3': 3}, 'S6': {'c1': 4, 'c2': 3, 'c3': 2}, 'S7': {'c1': 3, 'c2': 2, 'c3': 1}} >>> convert_valuations_to_matrix(valuations) - [[9, 8, 7], [8, 7, 6], [7, 6, 5], [6, 5, 4], [5, 4, 3], [4, 3, 2], [3, 2, 1]] + [[], [0, 9, 8, 7], [0, 8, 7, 6], [0, 7, 6, 5], [0, 6, 5, 4], [0, 5, 4, 3], [0, 4, 3, 2], [0, 3, 2, 1]] """ students = sorted(valuations.keys()) # Sort student keys to maintain order colleges = sorted(valuations[students[0]].keys()) # Sort college keys to maintain order - return [[valuations[student][college] for college in colleges] for student in students] + V = [] + V.append([]) + for student in students: + V.append([0] + [valuations[student][college] for college in colleges]) + return V def FaSt(alloc: AllocationBuilder)-> dict: """ @@ -228,8 +232,8 @@ def FaSt(alloc: AllocationBuilder)-> dict: >>> ins = Instance(agents=agents, items=items, valuations=valuation) >>> alloc = AllocationBuilder(instance=ins) >>> FaSt(alloc=alloc) - {1: [1, 2, 3], 2: [5, 4], 3: [7, 6]}""" - + {1: [1, 2], 2: [4, 3], 3: [7, 6, 5]}""" + # this is the prev matching that i understand to be wrong because of indexes problem {1: [1, 2, 3], 2: [5, 4], 3: [7, 6]}""" S = alloc.instance.agents C = alloc.instance.items V = alloc.instance._valuations @@ -245,6 +249,7 @@ def FaSt(alloc: AllocationBuilder)-> dict: initial_matching = initialize_matching(n, m) # Convert Valuations to only numerical matrix V= convert_valuations_to_matrix(V) + # Initialize the leximin tuple lex_tupl=get_leximin_tuple(initial_matching,V) @@ -257,8 +262,9 @@ def FaSt(alloc: AllocationBuilder)-> dict: F_stduents = [] F_colleges = [] F_stduents.append(n) # Add sn to the student list in F - logger.debug('Initialized F_students: %s, F_colleges: %s', F_stduents, F_colleges) + #logger.debug('Initialized F_students: %s, F_colleges: %s', F_stduents, F_colleges) + logger.debug('\n**initial_matching %s**', initial_matching) iteration = 1 # For logging while i > j - 1 and j > 1: @@ -267,40 +273,53 @@ def FaSt(alloc: AllocationBuilder)-> dict: logger.debug('Current j:%d ', j) logger.debug('V: %s', V) - logger.debug('V[i-1][j-1]: %d', V[i - 1][j - 1]) + logger.debug('V[i][j-1]: %d', V[i][j-1]) logger.debug('college_values:%s ', college_values) logger.debug('college_values[j]: %d', college_values[j]) - initial_j=j #for updating F in the end # IMPORTANT! in the variable college_values we use in j and not j-1 because it build like this: {1: 35, 2: 3, 3: 1} # So this: college_values[j] indeed gave us the right index ! [i.e. different structure!] - if college_values[j] >= V[i - 1][j-1]: # In the algo the college_values is actually v + if college_values[j] >= V[i][j-1]: # In the algo the college_values is actually v j -= 1 else: - if college_values[j] < V[i - 1][j - 1]: #Raw 11 in the article- different indixes because of different structures. - # print("V[i-1][j-1]:", V[i - 1][j-1]) + logger.info("index i:%s", i ) + logger.info("index j: %s", j) + logger.debug('V[i][j]: %d', V[i][j]) + if V[i][j] > college_values[j]: #Raw 11 in the article- different indixes because of different structures. initial_matching = Demote(initial_matching, i, j, 1) logger.debug('initial_matching after demote: %s', initial_matching) else: - if V[i - 1][j - 1] < college_values[j]:#Raw 14 + if V[i][j] < college_values[j]:#Raw 14 j -= 1 else: # Lookahead - k = i - 1 - t = pos[i - 1] + k = i + t = pos[i] µ_prime = copy.deepcopy(initial_matching) # Deep copy + logger.debug('k: %s', k) + logger.debug('t: %s', t) + logger.debug('V[k][j]: %s', V[k][j]) + logger.debug('lex_tupl[t]: %s', lex_tupl[t]) + logger.debug('i: %s', i) + while k > j - 1: - if V[k][j -1] > lex_tupl[t]: + if V[k][j] > lex_tupl[t]: i = k - initial_matching = Demote(µ_prime, k, j - 1, 1) - break - elif V[k][j - 1] < college_values[j]:# raw 24 in the article - j -= 1 + initial_matching = Demote(µ_prime, k, j, 1) + logger.debug('matching after demote: %s', initial_matching) break else: - µ_prime = Demote(µ_prime, k, j, 1) - k -= 1 - t += 1 + if V[i][j] < college_values[j]:# raw 24 in the article + j -= 1 + break + else: + µ_prime = Demote(µ_prime, k, j, 1) + k -= 1 + t += 1 + logger.debug('k:%s ,j: %s', k,j) + logger.debug('matching new: %s', µ_prime) + logger.debug('initial_matching: %s', initial_matching) + if k == j - 1 and initial_matching != µ_prime: j -= 1 # Updates @@ -332,7 +351,6 @@ def FaSt(alloc: AllocationBuilder)-> dict: console=logging.StreamHandler() #writes to stderr (= cerr) logger.handlers=[console] # we want the logs to be written to console # Change logger level - logger.setLevel(logging.DEBUG) - + logger.setLevel(logging.DEBUG) # Set logger level to DEBUG import doctest - doctest.testmod() + doctest.testmod() \ No newline at end of file From fb9da527a94171ca3461fdffdcc43bf4f2cd3af4 Mon Sep 17 00:00:00 2001 From: yuvalTrip <77538019+yuvalTrip@users.noreply.github.com> Date: Sat, 20 Jul 2024 22:32:58 +0300 Subject: [PATCH 044/111] F Fixed --- .../algorithms/Optimization_Matching/FaSt.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index f6c3964..6e675ad 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -259,9 +259,9 @@ def FaSt(alloc: AllocationBuilder)-> dict: logger.debug('Initial i:%d', i) logger.debug('Initial j:%d', j) # Initialize F as a list of two lists: one for students, one for colleges - F_stduents = [] + F_students = [] F_colleges = [] - F_stduents.append(n) # Add sn to the student list in F + F_students.append(n) # Add sn to the student list in F #logger.debug('Initialized F_students: %s, F_colleges: %s', F_stduents, F_colleges) logger.debug('\n**initial_matching %s**', initial_matching) @@ -334,12 +334,16 @@ def FaSt(alloc: AllocationBuilder)-> dict: logger.debug('pos: %s', pos) # Update F - # Insert student i - F_stduents.append(i) - # Insert college j - if j not in F_colleges: - F_colleges.append(j) - logger.debug('Updated F_students: %s, F_colleges: %s', F_stduents, F_colleges) + # Insert all students from i to n + for student in range(i, n + 1): + if student not in F_students: + F_students.append(student) + # Insert all colleges from j+1 to m + for college in range(j + 1, m + 1): + if college not in F_colleges: + F_colleges.append(college) + + logger.debug('Updated F_students: %s, F_colleges: %s', F_students, F_colleges) i -= 1 iteration += 1 From f08105f57209b47e55380765515a477a8e945323 Mon Sep 17 00:00:00 2001 From: Erel Segal-Halevi Date: Sun, 21 Jul 2024 11:58:06 +0300 Subject: [PATCH 045/111] add logs before and after demote --- fairpyx/algorithms/Optimization_Matching/FaSt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 6e675ad..b8b0937 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -305,8 +305,9 @@ def FaSt(alloc: AllocationBuilder)-> dict: while k > j - 1: if V[k][j] > lex_tupl[t]: i = k + logger.debug('Before demote: µ=%s, µ_prime=%s', initial_matching, µ_prime) initial_matching = Demote(µ_prime, k, j, 1) - logger.debug('matching after demote: %s', initial_matching) + logger.debug('After demote: µ=%s, µ_prime=%s', initial_matching, µ_prime) break else: if V[i][j] < college_values[j]:# raw 24 in the article From b40a31a3cff7aabc26766e4dcfd3ee287a398a79 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Sun, 21 Jul 2024 15:28:45 +0300 Subject: [PATCH 046/111] update FaStGen --- .../Optimization_Matching/AgentItem.py | 38 ++ .../Optimization_Matching/FaStGen.py | 12 +- .../Optimization_Matching/tryFaStGen.py | 526 ++++++++++++++++++ 3 files changed, 570 insertions(+), 6 deletions(-) create mode 100644 fairpyx/algorithms/Optimization_Matching/AgentItem.py create mode 100644 fairpyx/algorithms/Optimization_Matching/tryFaStGen.py diff --git a/fairpyx/algorithms/Optimization_Matching/AgentItem.py b/fairpyx/algorithms/Optimization_Matching/AgentItem.py new file mode 100644 index 0000000..0fb365d --- /dev/null +++ b/fairpyx/algorithms/Optimization_Matching/AgentItem.py @@ -0,0 +1,38 @@ +# """ +# "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) + +# Programmer: Hadar Bitan, Yuval Ben-Simhon +# Date: 19.5.2024 +# """ + +# class AgentItem: + +# def __init__(self, str_format, int_format, matching) -> None: +# self.str_format = str_format +# self.int_format = int_format +# self.matching = matching + +# def GetStrFormat(self): +# return self.str_format + +# def GetStrFormatFromInt(self, int_format): + + +# def GetIntFormat(self): +# return self.int_format + +# def GetMatching(self): +# return self.matching + +# def SetMatching(self, matching): +# self.matching = matching + +# # Helper function to create AgentItem instances and store them in a list +# def create_agent_items(prefix, start, end): +# items = [] +# for i in range(start, end + 1): +# str_format = f"{prefix}{i}" +# int_format = i +# matching = None # or any initial value for matching +# items.append(AgentItem(str_format, int_format, matching)) +# return items \ No newline at end of file diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index b964502..b1f783b 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -7,7 +7,7 @@ from fairpyx import Instance, AllocationBuilder, ExplanationLogger from FaSt import Demote -from AgentItem import AgentItem, create_agent_items +# from AgentItem import AgentItem, create_agent_items from copy import deepcopy import logging @@ -511,16 +511,16 @@ def get_match(match:dict, value:str)->any: if __name__ == "__main__": import doctest, sys print(doctest.testmod()) - # doctest.run_docstring_examples(LookAheadRoutine, globals()) + # doctest.run_docstring_examples(FaStGen, globals()) sys.exit(0) # logger.setLevel(logging.DEBUG) # logger.addHandler(logging.StreamHandler()) - # from fairpyx.adaptors import divide - # S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] - # C = ["c1", "c2", "c3", "c4"] + + # S = create_agent_items('s', 1, 7) + # C = create_agent_items('c', 1, 4) # V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1}} # U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, "s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, "s7" : {"c1":40,"c2":29,"c3":6,"c4":1}} # ins = Instance(agents=S, items=C, valuations=U) # alloc = AllocationBuilder(instance=ins) - # FaStGen(alloc=alloc, items_valuations=V) + # # FaStGen(alloc=alloc, items_valuations=V) diff --git a/fairpyx/algorithms/Optimization_Matching/tryFaStGen.py b/fairpyx/algorithms/Optimization_Matching/tryFaStGen.py new file mode 100644 index 0000000..0c3eeba --- /dev/null +++ b/fairpyx/algorithms/Optimization_Matching/tryFaStGen.py @@ -0,0 +1,526 @@ +""" + "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) + + Programmer: Hadar Bitan, Yuval Ben-Simhon + Date: 19.5.2024 +""" + +from fairpyx import Instance, AllocationBuilder, ExplanationLogger +from FaSt import Demote +from AgentItem import AgentItem, create_agent_items +from copy import deepcopy + +import logging +logger = logging.getLogger(__name__) + +def FaStGen(alloc: AllocationBuilder, items_valuations:dict)->dict: + """ + Algorithem 3-FaSt-Gen: finding a match for the general ranked valuations setting. + + :param alloc: an allocation builder, which tracks the allocation and the remaining capacity for items and agents. + :param items_valuations: a dictionary represents how items valuates the agents + + >>> from fairpyx.adaptors import divide + >>> S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + >>> C = ["c1", "c2", "c3", "c4"] + >>> V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1}} + >>> U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, "s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, "s7" : {"c1":40,"c2":29,"c3":6,"c4":1}} + >>> ins = Instance(agents=S, items=C, valuations=U) + >>> alloc = AllocationBuilder(instance=ins) + >>> FaStGen(alloc=alloc, items_valuations=V) + {'c1': ['s1', 's2', 's3'], 'c2': ['s4'], 'c3': ['s5'], 'c4': ['s7', 's6']} + """ + logger.info("Starting FaStGen algorithm") + + agents = alloc.instance.agents + items = alloc.instance.items + agents_valuations = alloc.instance._valuations + # #Convert the list of the agents and item to dictionary so that each agent\item will have its coresponding integer + # student_name_to_int = generate_dict_from_str_to_int(S) + # college_name_to_int = generate_dict_from_str_to_int(C) + # int_to_student_name = generate_dict_from_int_to_str(S) + # int_to_college_name = generate_dict_from_int_to_str(C) + + #Creating a match of the integers and the string coresponding one another to deal with the demote function and the leximin tuple as well + match = create_stable_matching(agents=agents, items=items) + # str_match = integer_to_str_matching(integer_match=integer_match, agent_dict=student_name_to_int, items_dict=college_name_to_int) + str_match = {item.GetStrFormat(): [agent.GetStrFormat() for agent in agent_list] for item, agent_list in match.items()} + logger.debug(f"Initial match: {str_match}") + + UpperFix = [items[0].GetIntFormat()] # Inidces of colleges to which we cannot add any more students. + LowerFix = [items[len(items)-1].GetIntFormat()] # Inidces of colleges from which we cannot remove any more students. + SoftFix = [] + UnFixed = [item.GetIntFormat() for item in agents if item.GetIntFormat() not in UpperFix] + + + #creating a dictionary of vj(µ) = Pi∈µ(cj) for each j in C + matching_college_valuations = update_matching_valuations_sum(match=str_match, items_valuations=items_valuations) + logger.debug(f"matching_college_valuations: {matching_college_valuations}") + + while len(LowerFix) + len([item for item in UpperFix if item not in LowerFix]) < len(items): + logger.debug(f"\nstr_match: {str_match}, integer_match: {integer_match}, UpperFix: {UpperFix}, LowerFix: {LowerFix}, SoftFix: {SoftFix}, UnFixed: {UnFixed}") + up = min([j.GetIntFormat() for j in items if j.GetIntFormat() not in LowerFix]) + down = min(UnFixed, key=lambda j: matching_college_valuations[f"c{j}"]) + logger.debug(f"up: {up}, down: {down}") + + SoftFix = [pair for pair in SoftFix if not (pair[1] <= up < pair[0])] + logger.debug(f"Updating SoftFix to {SoftFix}") + + logger.debug(f"vup(mu)={matching_college_valuations[f"c{up}"]}, vdown(mu)={matching_college_valuations[f"c{down}"]}") + if (len(integer_match[up]) == 1) or (matching_college_valuations[f"c{up}"] <= matching_college_valuations[f"c{down}"]): + LowerFix.append(up) + logger.info(f"Cannot remove any more students from c_{up}: Added c_{up} to LowerFix") + else: + #check the lowest-rank student who currently belongs to mu(c_{down-1}) + agant_to_demote = get_lowest_ranked_student(down-1, integer_match, items_valuations, int_to_college_name=items, int_to_student_name=agents) + new_integer_match = Demote(deepcopy(integer_match), agant_to_demote, up_index=up, down_index=down) + logger.info(f"Demoting from {up} to {down}, starting at student {agant_to_demote}. New match: {new_integer_match}") + new_match_str = integer_to_str_matching(integer_match=new_integer_match, agent_dict=student_name_to_int, items_dict=college_name_to_int) + + #Creating a leximin tuple for the new match from the demote and for the old match to compare + match_leximin_tuple = create_leximin_tuple(match=str_match , agents_valuations=agents_valuations, items_valuations=items_valuations) + logger.info(f"Old match leximin tuple: {match_leximin_tuple}") + new_match_leximin_tuple = create_leximin_tuple(match=new_match_str, agents_valuations=agents_valuations, items_valuations=items_valuations) + logger.info(f"New match leximin tuple: {new_match_leximin_tuple}") + + #Extarcting from the SourceDec function the problematic item or agent, if there isn't one then it will be "" + problematic_component = sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=match_leximin_tuple) + logger.info(f" problematic component: {problematic_component}") + + if problematic_component == "": + logger.debug(f"New match is leximin-better than old match:") + integer_match = new_integer_match + str_match = new_match_str + matching_college_valuations = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) + logger.debug(f" Match updated to {str_match}") + + elif problematic_component == int_to_college_name[up]: + logger.debug(f"New match is leximin-worse because of c_up = c_{up}:") + LowerFix.append(up) + UpperFix.append(up + 1) + logger.info(f" Updated LowerFix and UpperFix with {up}") + + elif problematic_component in alloc.instance.agents: + sd = problematic_component + logger.debug(f"New match is leximin-worse because of student {sd}: ") + t = college_name_to_int[get_match(match=str_match, value=sd)] + LowerFix.append(t) + UpperFix.append(t+1) + logger.debug(f" sourceDec student {sd} is matched to c_t = c_{t}: adding c_{t} to LowerFix and c_{t+1} to UpperFix.") + A = [j for j in UnFixed if (j > t + 1)] + SoftFix.extend((j, t+1) for j in A) + logger.debug(f" Updating SoftFix to {SoftFix}") + + else: + logger.debug(f"New match is leximin-worse because of college {sourceDec(new_match_leximin_tuple, match_leximin_tuple)}: ") + str_match, LowerFix, UpperFix, SoftFix = LookAheadRoutine((S, C, agents_valuations, items_valuations), integer_match, down, LowerFix, UpperFix, SoftFix) + logger.debug(f" LookAheadRoutine result: match={str_match}, LowerFix={LowerFix}, UpperFix={UpperFix}, SoftFix={SoftFix}") + + UnFixed = [ + j for j in college_name_to_int.values() + if (j not in UpperFix) or + any((j, _j) not in SoftFix for _j in college_name_to_int.values() if _j > j) + ] + logger.debug(f" Updating UnFixed to {UnFixed}") + + logger.info(f"Finished FaStGen algorithm, final result: {str_match}") + return str_match #We want to return the final match in his string form + +def LookAheadRoutine(I:tuple, integer_match:dict, down:int, LowerFix:list, UpperFix:list, SoftFix:list)->tuple: + """ + Algorithem 4-LookAheadRoutine: Designed to handle cases where a decrease in the leximin value + may be balanced by future changes in the pairing, + the goal is to ensure that the sumi pairing will maintain a good leximin value or even improve it. + + :param I: A presentation of the problem, aka a tuple that contain the list of students(S), the list of colleges(C) when the capacity + of each college is n-1 where n is the number of students, student valuation function(U), college valuation function(V). + :param match: The current match of the students and colleges. + :param down: The lowest ranked unaffixed college + :param LowerFix: The group of colleges whose lower limit is fixed + :param UpperFix: The group of colleges whose upper limit is fixed. + :param SoftFix: A set of temporary upper limits. + *We will asume that in the colleges list in index 0 there is college 1 in index 1 there is coll + + >>> from fairpyx.adaptors import divide + >>> S = ["s1", "s2", "s3", "s4", "s5"] + >>> C = ["c1", "c2", "c3", "c4"] + >>> V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10},"c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26},"c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15}} + >>> U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5},"s2" : {"c1":36,"c2":20,"c3":10,"c4":1},"s3" : {"c1":29,"c2":24,"c3":12,"c4":10},"s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}} + >>> match = {1 : [1,2],2 : [3,5],3 : [4],4 : []} + >>> I = (S,C,U,V) + >>> down = 4 + >>> LowerFix = [1] + >>> UpperFix = [] + >>> SoftFix = [] + >>> LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) + ({'c1': ['s1', 's2'], 'c2': ['s5'], 'c3': ['s3'], 'c4': ['s4']}, [1], [], []) + """ + agents, items, agents_valuations, items_valuations = I + + student_name_to_int = generate_dict_from_str_to_int(agents) + college_name_to_int = generate_dict_from_str_to_int(items) + int_to_student_name = generate_dict_from_int_to_str(agents) + int_to_college_name = generate_dict_from_int_to_str(items) + + LF = LowerFix.copy() + UF = UpperFix.copy() + given_str_match = integer_to_str_matching(integer_match=integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) + new_integer_match = deepcopy(integer_match) + new_str_match = integer_to_str_matching(integer_match=new_integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) + + logger.info(f"Starting LookAheadRoutine. Initial parameters - match: {new_str_match}, down: {down}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") + matching_college_valuations = update_matching_valuations_sum(match=new_str_match,items_valuations=items_valuations) + while len(LF) + len([item for item in UF if item not in LF]) < len(items): + up = min([j for j in college_name_to_int.values() if j not in LF]) + logger.debug(f" Selected 'up': {up}") + if (len(integer_match[up]) == 1) or (matching_college_valuations[int_to_college_name[up]] <= matching_college_valuations[int_to_college_name[down]]): + LF.append(up) + logger.info(f" Cannot remove any more students from c_{up}: appended c_{up} to LF") + else: + #check the lowest-rank student who currently belongs to mu(c_{down-1})d + agant_to_demote = get_lowest_ranked_student(item_int=down-1, match_int=new_integer_match, items_valuations=items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) + new_integer_match = Demote(new_integer_match, agant_to_demote, up_index=up, down_index=down) + logger.info(f" Demoting from {up} to {down}, starting at student {agant_to_demote}. New match: {new_integer_match}") + + new_str_match = integer_to_str_matching(integer_match=new_integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) + matching_college_valuations = update_matching_valuations_sum(match=new_str_match,items_valuations=items_valuations) + + old_match_leximin_tuple = create_leximin_tuple(match=given_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + new_match_leximin_tuple = create_leximin_tuple(match=new_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + logger.info(f" Old match leximin tuple: {old_match_leximin_tuple}") + new_match_leximin_tuple = create_leximin_tuple(match=new_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) + logger.info(f" New match leximin tuple: {new_match_leximin_tuple}") + + #Extarcting from the SourceDec function the problematic item or agent, if there isn't one then it will be "" + problematic_component = sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple) + logger.info(f" problematic component: {problematic_component}") + + if problematic_component == "": + logger.debug(f" New match is leximin-better than old match:") + integer_match = new_integer_match + LowerFix = LF + UpperFix = UF + logger.info(" Updated match and fixed LowerFix and UpperFix") + break + + elif problematic_component == int_to_college_name[up]: + logger.debug(f" New match is leximin-worse because of c_up = c_{up}:") + LF.append(up) + UF.append(up + 1) + logger.info(f" Appended {up} to LF and {up+1} to UF") + + elif problematic_component in agents: + sd = problematic_component + logger.debug(f" New match is leximin-worse because of student {sd}: ") + t = college_name_to_int[get_match(match=new_str_match, value=sd)] + logger.debug(f" sourceDec student {sd} is matched to c_t = c_{t}.") + if t == down: + logger.debug(f" t=down={down}: adding c_{down} to UpperFix") # UF? + UpperFix.append(down) + else: + logger.info(f" t!=down: adding ({down},{t}) to SoftFix") + SoftFix.append((down, t)) + break + + final_match_str = integer_to_str_matching(integer_match=integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) + logger.info(f"Completed LookAheadRoutine. Final result - match: {final_match_str}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") + return (final_match_str, LowerFix, UpperFix, SoftFix) + +def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:dict): + """ + Create a leximin tuple from the given match, agents' valuations, and items' valuations. + + Args: + - match (dict): A dictionary where keys are items and values are lists of agents. + - agents_valuations (dict): A dictionary where keys are agents and values are dictionaries of item valuations. + - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. + + Returns: + - list: A sorted list of tuples representing the leximin tuple. + + Example: + >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} + >>> items_valuations = { #the colleges valuations + ... "c1":{"s1":50, "s2":23, "s3":21, "s4":13, "s5":10, "s6":6, "s7":5}, + ... "c2":{"s1":45, "s2":40, "s3":32, "s4":29, "s5":26, "s6":11, "s7":4}, + ... "c3":{"s1":90, "s2":79, "s3":60, "s4":35, "s5":28, "s6":20, "s7":15}, + ... "c4":{"s1":80, "s2":48, "s3":36, "s4":29, "s5":15, "s6":7, "s7":1}, + ... } + >>> agents_valuations = { #the students valuations + ... "s1":{"c1":16, "c2":10, "c3":6, "c4":5}, + ... "s2":{"c1":36, "c2":20, "c3":10, "c4":1}, + ... "s3":{"c1":29, "c2":24, "c3":12, "c4":10}, + ... "s4":{"c1":41, "c2":24, "c3":5, "c4":3}, + ... "s5":{"c1":36, "c2":19, "c3":9, "c4":6}, + ... "s6":{"c1":39, "c2":30, "c3":18, "c4":7}, + ... "s7":{"c1":40, "c2":29, "c3":6, "c4":1} + ... } + >>> create_leximin_tuple(match, agents_valuations, items_valuations) + [('c4', 1), ('s7', 1), ('s1', 16), ('s6', 18), ('s5', 19), ('c3', 20), ('c2', 26), ('s3', 29), ('s2', 36), ('s4', 41), ('c1', 107)] + + >>> match = {"c1":["s1","s2","s3"], "c2":["s4"], "c3":["s5"], "c4":["s7","s6"]} + >>> create_leximin_tuple(match, agents_valuations, items_valuations) + [('s7', 1), ('s6', 7), ('c4', 8), ('s5', 9), ('s1', 16), ('s4', 24), ('c3', 28), ('c2', 29), ('s3', 29), ('s2', 36), ('c1', 94)] + """ + leximin_tuple = [] + matching_college_valuations = update_matching_valuations_sum(match=match, items_valuations=items_valuations) + logger.debug(f"matching_college_valuations: {matching_college_valuations}") + for item in match.keys(): + if len(match[item]) == 0: + leximin_tuple.append((item, 0)) + else: + leximin_tuple.append((item, matching_college_valuations[item])) + for agent in match[item]: + leximin_tuple.append((agent,agents_valuations[agent][item])) + + leximin_tuple = sorted(leximin_tuple, key=lambda x: (x[1], x[0])) + return leximin_tuple + +def sourceDec(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->str: + """ + Determine the agent causing the leximin decrease between two matchings. + + Args: + - new_match_leximin_tuple (list): The leximin tuple of the new matching. + - old_match_leximin_tuple (list): The leximin tuple of the old matching. + + Returns: + - str: The agent (student) causing the leximin decrease. + + Example: + >>> new_match = [("s7",1),("s4",5),("s5",6),("s6",7),("c4",14),("s1",16),("s3",24),("c2",32),("c3",35),("s2",36),("c1",52)] + >>> old_match = [("s7",1),("s6",7),("c4",8),("s5",9),("s1",16),("s4",24),("c3",28),("c2",29),("s3",29),("s2",36),("c1",94)] + >>> sourceDec(new_match, old_match) + 's4' + + >>> new_match = [("s7",1),("s4",7),("s5",8),("s6",9),("c4",14),("s1",16),("s3",24),("c2",32),("c3",35),("s2",36),("c1",52)] + >>> old_match = [("s7",1),("s6",7),("c4",8),("s5",9),("s1",16),("s4",24),("c3",28),("c2",29),("s3",29),("s2",36),("c1",94)] + >>> sourceDec(new_match, old_match) + 'c4' + """ + for k in range(0, len(new_match_leximin_tuple)): + if new_match_leximin_tuple[k][1] == old_match_leximin_tuple[k][1]: + continue + elif new_match_leximin_tuple[k][1] > old_match_leximin_tuple[k][1]: + return "" + else: #new_match_leximin_tuple[k][1] < old_match_leximin_tuple[k][1] + return new_match_leximin_tuple[k][0] + return "" + +def get_lowest_ranked_student(item_int:int, match_int:dict, items_valuations:dict, int_to_college_name:dict, int_to_student_name:dict): + """ + Get the lowest ranked student that is matched to the item with the given index. + + # Args: + # - item: The item for which the lowest ranked student is to be found. + # - match (dict): A dictionary where keys are items and values are lists of agents. + # - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. + + # Returns: + # - str: The lowest ranked student for the given item. + + # Example: + >>> match = {1:[1,2,3,4], 2:[5], 3:[6], 4:[7]} + >>> items_valuations = { #the colleges valuations + ... "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + ... "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + ... "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + ... "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} + ... } + >>> int_to_college_name = {i: f"c{i}" for i in [1,2,3,4]} + >>> int_to_student_name = {i: f"s{i}" for i in [1,2,3,4,5,6,7]} + >>> get_lowest_ranked_student(1, match, items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) + 4 + >>> get_lowest_ranked_student(2, match, items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) + 5 + >>> get_lowest_ranked_student(3, match, items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) + 6 + """ + return min(match_int[item_int], key=lambda agent: items_valuations[int_to_college_name[item_int]][int_to_student_name[agent]]) + +def update_matching_valuations_sum(match:dict, items_valuations:dict)->dict: + """ + Update the sum of valuations for each item in the matching. + + Args: + - match (dict): A dictionary where keys are items and values are lists of agents. + - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. + - agents (list): List of agents. + - items (list): List of items. + + Returns: + - dict: A dictionary with the sum of valuations for each item. + + Example: + >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} + >>> items_valuations = { #the colleges valuations + ... "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, + ... "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, + ... "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, + ... "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":7,"s7":1} + ... } + >>> update_matching_valuations_sum(match, items_valuations) + {'c1': 107, 'c2': 26, 'c3': 20, 'c4': 1} + + >>> match = {"c1":["s1","s2","s3"], "c2":["s4"], "c3":["s5"], "c4":["s7","s6"]} + >>> update_matching_valuations_sum(match, items_valuations) + {'c1': 94, 'c2': 29, 'c3': 28, 'c4': 8} + """ + matching_valuations_sum = { #in the artical it looks like this: vj(mu) + colleague: sum(items_valuations[colleague][student] for student in students) + for colleague, students in match.items() + } + return matching_valuations_sum + +def create_stable_matching(agents, items): + """ + Creating a stable matching according to this: + the first collage get the first n-m+1 students + each collage in deacrising order get the n-(m-j)th student + + Args: + - items_dict: A dictionary of all the items and there indexes like this: ("c1":1). + - agents_dict: A dictionary of all the agents and there indexes like this: ("s1":1). + - agents (list): List of agents. + - items (list): List of items. + + Returns: + - dict: A stable matching of integers + + Example: + >>> agents = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + >>> items = ["c1", "c2", "c3", "c4"] + >>> agents_dict = {"s1":1, "s2":2, "s3":3, "s4":4, "s5":5, "s6":6, "s7":7} + >>> items_dict = {"c1":1, "c2":2, "c3":3, "c4":4} + >>> create_stable_matching(agents, agents_dict, items, items_dict) + {1: [1, 2, 3, 4], 2: [5], 3: [6], 4: [7]} + """ + # Initialize the matching dictionary + matching = {} + + # Assign the first m-1 students to c1 + matching[items[0]] = [agents[i] for i in range(0, len(agents) - len(items) + 1)] + + # Assign the remaining students to cj for j >= 2 + for j in range(1, len(items)): + matching[items[j]] = [agents[len(agents) - (len(items) - j)]] + + return matching + +def generate_dict_from_str_to_int(input_list:list)->dict: + """ + Creating a dictionary that includes for each string item in the list an index representing it, key=string. + + Args: + - input_list: A list of strings + + Returns: + - dict: a dictionary of strings ang indexes + + Example: + >>> agents = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + >>> items = ["c1", "c2", "c3", "c4"] + + >>> generate_dict_from_str_to_int(agents) + {'s1': 1, 's2': 2, 's3': 3, 's4': 4, 's5': 5, 's6': 6, 's7': 7} + + >>> generate_dict_from_str_to_int(items) + {'c1': 1, 'c2': 2, 'c3': 3, 'c4': 4} + """ + return {item: index + 1 for index, item in enumerate(input_list)} + +def generate_dict_from_int_to_str(input_list:list)->dict: + """ + Creating a dictionary that includes for each string item in the list an index representing it, key=integer. + + Args: + - input_list: A list of strings + + Returns: + - dict: a dictionary of strings ang indexes + + Example: + >>> agents = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] + >>> items = ["c1", "c2", "c3", "c4"] + + >>> generate_dict_from_int_to_str(agents) + {1: 's1', 2: 's2', 3: 's3', 4: 's4', 5: 's5', 6: 's6', 7: 's7'} + + >>> generate_dict_from_int_to_str(items) + {1: 'c1', 2: 'c2', 3: 'c3', 4: 'c4'} + """ + return {index + 1: item for index, item in enumerate(input_list)} + +def integer_to_str_matching(integer_match:dict, agent_dict:dict, items_dict:dict)->dict: + """ + Converting an integer match to a string match. + + Args: + - integer_match: A matching of agents to items out of numbers. + - items_dict: A dictionary of all the items and there indexes like this: ("c1":1). + - agents_dict: A dictionary of all the agents and there indexes like this: ("s1":1). + + Returns: + - dict: A string matching. + + Example: + >>> agents_dict = {"s1":1, "s2":2, "s3":3, "s4":4, "s5":5, "s6":6, "s7":7} + >>> items_dict = {"c1":1, "c2":2, "c3":3, "c4":4} + >>> integer_match = {1: [1, 2, 3, 4], 2: [5], 3: [6], 4: [7]} + >>> integer_to_str_matching(integer_match, agents_dict, items_dict) + {'c1': ['s1', 's2', 's3', 's4'], 'c2': ['s5'], 'c3': ['s6'], 'c4': ['s7']} + """ + # Reverse the s_dict and c_dict to map integer values back to their string keys + s_reverse_dict = {v: k for k, v in agent_dict.items()} + c_reverse_dict = {v: k for k, v in items_dict.items()} + + # Create the new dictionary with string keys and lists of string values + return {c_reverse_dict[c_key]: [s_reverse_dict[s_val] for s_val in s_values] for c_key, s_values in integer_match.items()} + +def get_match(match:dict, value:str)->any: + """ + Giving a match and an agent or an item the function will produce its match + + Args: + - match: A matching of agents to items. + - value: An agent or an item. + + Returns: + - any: An agent or an item. + + Example: + >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} + + >>> value = "c1" + >>> get_match(match, value) + ['s1', 's2', 's3', 's4'] + + >>> value = "s4" + >>> get_match(match, value) + 'c1' + + >>> value = "c4" + >>> get_match(match, value) + ['s7'] + """ + if value in match.keys(): + return match[value] + else: + return next((key for key, values_list in match.items() if value in values_list), None) + +if __name__ == "__main__": + import doctest, sys + # print(doctest.testmod()) + # # doctest.run_docstring_examples(LookAheadRoutine, globals()) + # sys.exit(0) + + logger.setLevel(logging.DEBUG) + logger.addHandler(logging.StreamHandler()) + + S = create_agent_items('s', 1, 7) + C = create_agent_items('c', 1, 4) + V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1}} + U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, "s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, "s7" : {"c1":40,"c2":29,"c3":6,"c4":1}} + ins = Instance(agents=S, items=C, valuations=U) + alloc = AllocationBuilder(instance=ins) + # FaStGen(alloc=alloc, items_valuations=V) From fa896811ef56a76857f3f9e4adbb0f66e36d7d01 Mon Sep 17 00:00:00 2001 From: ErgaDN Date: Sun, 21 Jul 2024 15:32:16 +0300 Subject: [PATCH 047/111] compare_course_allocation_algorithms_ACEEI_Tabu_Search.py finish --- ...allocation_algorithms_ACEEI_Tabu_Search.py | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py b/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py index 20d55d5..89e774c 100644 --- a/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py +++ b/experiments/compare_course_allocation_algorithms_ACEEI_Tabu_Search.py @@ -21,7 +21,7 @@ from fairpyx.algorithms.ACEEI_algorithms.tabu_search import run_tabu_search algorithms_to_check = [ - # ACEEI_without_EFTB, + ACEEI_without_EFTB, ACEEI_with_EFTB, ACEEI_with_contested_EFTB, run_tabu_search, @@ -30,9 +30,6 @@ ] def evaluate_algorithm_on_instance(algorithm, instance): - # print(f" -!-!- instance._valuations = {instance._valuations} -!-!-") - # capacity = {course: instance.item_capacity(course) for course in instance.items} - # print(f" -!-!- capacity = {capacity} -!-!-") allocation = divide(algorithm, instance) matrix = AgentBundleValueMatrix(instance, allocation) matrix.use_normalized_values() @@ -112,9 +109,9 @@ def run_szws_experiment(): # Run on SZWS simulated data: experiment = experiments_csv.Experiment("results/", "course_allocation_szws_ACEEI.csv", backup_folder="results/backup/") input_ranges = { - "num_of_agents": [5, 10, 15], - "num_of_items": [4, 8], # in SZWS: 25 - "agent_capacity": [5], # as in SZWS + "num_of_agents": [10, 20, 30, 40], + "num_of_items": [5, 10, 14], # in SZWS: 25 + "agent_capacity": [5, 7, 9], # as in SZWS "supply_ratio": [1.1, 1.25, 1.5], # as in SZWS "num_of_popular_items": [6, 9], # as in SZWS "mean_num_of_favorite_items": [2.6, 3.85], # as in SZWS code https://github.com/marketdesignresearch/Course-Match-Preference-Simulator/blob/main/preference_generator_demo.ipynb @@ -179,7 +176,7 @@ def plot_average_runtime_vs_students(df, algorithm_name): plt.plot(num_of_agents, runtime, marker='o', label=algorithm_name) plt.xlabel('Number of Students') plt.ylabel('Average Runtime (seconds)') - plt.title(f'Average Runtime vs. Number of Students ({algorithm_name})') + plt.title(f'Average Runtime vs. Number of Students') plt.legend() plt.grid(True) @@ -188,21 +185,21 @@ def plot_average_runtime_vs_students(df, algorithm_name): if __name__ == "__main__": import logging, experiments_csv experiments_csv.logger.setLevel(logging.INFO) - run_uniform_experiment() + # run_uniform_experiment() run_szws_experiment() # run_ariel_experiment() # Load and plot data for run_uniform_experiment() - uniform_results = load_experiment_results('results/course_allocation_uniform.csv') - plt.figure(figsize=(10, 6)) # Adjust figure size if needed - - for algorithm in algorithms_to_check: - algorithm_name = algorithm.__name__ - algorithm_data = uniform_results[uniform_results['algorithm'] == algorithm_name] - plot_average_runtime_vs_students(algorithm_data, algorithm_name) - - plt.tight_layout() - plt.show() + # uniform_results = load_experiment_results('results/course_allocation_uniform.csv') + # plt.figure(figsize=(10, 6)) # Adjust figure size if needed + # + # for algorithm in algorithms_to_check: + # algorithm_name = algorithm.__name__ + # algorithm_data = uniform_results[uniform_results['algorithm'] == algorithm_name] + # plot_average_runtime_vs_students(algorithm_data, algorithm_name) + # + # plt.tight_layout() + # plt.show() # Load and plot data for run_szws_experiment() szws_results = load_experiment_results('results/course_allocation_szws_ACEEI.csv') From ef51f5949299068634a3e2d4005ecc26437a610b Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Sun, 21 Jul 2024 17:05:46 +0300 Subject: [PATCH 048/111] deleting unwanted files and updating FaSt --- .../Optimization_Matching/AgentItem.py | 38 -- .../algorithms/Optimization_Matching/FaSt.py | 4 +- .../Optimization_Matching/tryFaStGen.py | 526 ------------------ 3 files changed, 2 insertions(+), 566 deletions(-) delete mode 100644 fairpyx/algorithms/Optimization_Matching/AgentItem.py delete mode 100644 fairpyx/algorithms/Optimization_Matching/tryFaStGen.py diff --git a/fairpyx/algorithms/Optimization_Matching/AgentItem.py b/fairpyx/algorithms/Optimization_Matching/AgentItem.py deleted file mode 100644 index 0fb365d..0000000 --- a/fairpyx/algorithms/Optimization_Matching/AgentItem.py +++ /dev/null @@ -1,38 +0,0 @@ -# """ -# "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) - -# Programmer: Hadar Bitan, Yuval Ben-Simhon -# Date: 19.5.2024 -# """ - -# class AgentItem: - -# def __init__(self, str_format, int_format, matching) -> None: -# self.str_format = str_format -# self.int_format = int_format -# self.matching = matching - -# def GetStrFormat(self): -# return self.str_format - -# def GetStrFormatFromInt(self, int_format): - - -# def GetIntFormat(self): -# return self.int_format - -# def GetMatching(self): -# return self.matching - -# def SetMatching(self, matching): -# self.matching = matching - -# # Helper function to create AgentItem instances and store them in a list -# def create_agent_items(prefix, start, end): -# items = [] -# for i in range(start, end + 1): -# str_format = f"{prefix}{i}" -# int_format = i -# matching = None # or any initial value for matching -# items.append(AgentItem(str_format, int_format, matching)) -# return items \ No newline at end of file diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index b8b0937..8c78685 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -306,7 +306,7 @@ def FaSt(alloc: AllocationBuilder)-> dict: if V[k][j] > lex_tupl[t]: i = k logger.debug('Before demote: µ=%s, µ_prime=%s', initial_matching, µ_prime) - initial_matching = Demote(µ_prime, k, j, 1) + initial_matching = Demote(copy.deepcopy(µ_prime), k, j, 1) logger.debug('After demote: µ=%s, µ_prime=%s', initial_matching, µ_prime) break else: @@ -314,7 +314,7 @@ def FaSt(alloc: AllocationBuilder)-> dict: j -= 1 break else: - µ_prime = Demote(µ_prime, k, j, 1) + µ_prime = Demote(copy.deepcopy(µ_prime), k, j, 1) k -= 1 t += 1 logger.debug('k:%s ,j: %s', k,j) diff --git a/fairpyx/algorithms/Optimization_Matching/tryFaStGen.py b/fairpyx/algorithms/Optimization_Matching/tryFaStGen.py deleted file mode 100644 index 0c3eeba..0000000 --- a/fairpyx/algorithms/Optimization_Matching/tryFaStGen.py +++ /dev/null @@ -1,526 +0,0 @@ -""" - "OnAchieving Fairness and Stability in Many-to-One Matchings", by Shivika Narang, Arpita Biswas, and Y Narahari (2022) - - Programmer: Hadar Bitan, Yuval Ben-Simhon - Date: 19.5.2024 -""" - -from fairpyx import Instance, AllocationBuilder, ExplanationLogger -from FaSt import Demote -from AgentItem import AgentItem, create_agent_items -from copy import deepcopy - -import logging -logger = logging.getLogger(__name__) - -def FaStGen(alloc: AllocationBuilder, items_valuations:dict)->dict: - """ - Algorithem 3-FaSt-Gen: finding a match for the general ranked valuations setting. - - :param alloc: an allocation builder, which tracks the allocation and the remaining capacity for items and agents. - :param items_valuations: a dictionary represents how items valuates the agents - - >>> from fairpyx.adaptors import divide - >>> S = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] - >>> C = ["c1", "c2", "c3", "c4"] - >>> V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1}} - >>> U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, "s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, "s7" : {"c1":40,"c2":29,"c3":6,"c4":1}} - >>> ins = Instance(agents=S, items=C, valuations=U) - >>> alloc = AllocationBuilder(instance=ins) - >>> FaStGen(alloc=alloc, items_valuations=V) - {'c1': ['s1', 's2', 's3'], 'c2': ['s4'], 'c3': ['s5'], 'c4': ['s7', 's6']} - """ - logger.info("Starting FaStGen algorithm") - - agents = alloc.instance.agents - items = alloc.instance.items - agents_valuations = alloc.instance._valuations - # #Convert the list of the agents and item to dictionary so that each agent\item will have its coresponding integer - # student_name_to_int = generate_dict_from_str_to_int(S) - # college_name_to_int = generate_dict_from_str_to_int(C) - # int_to_student_name = generate_dict_from_int_to_str(S) - # int_to_college_name = generate_dict_from_int_to_str(C) - - #Creating a match of the integers and the string coresponding one another to deal with the demote function and the leximin tuple as well - match = create_stable_matching(agents=agents, items=items) - # str_match = integer_to_str_matching(integer_match=integer_match, agent_dict=student_name_to_int, items_dict=college_name_to_int) - str_match = {item.GetStrFormat(): [agent.GetStrFormat() for agent in agent_list] for item, agent_list in match.items()} - logger.debug(f"Initial match: {str_match}") - - UpperFix = [items[0].GetIntFormat()] # Inidces of colleges to which we cannot add any more students. - LowerFix = [items[len(items)-1].GetIntFormat()] # Inidces of colleges from which we cannot remove any more students. - SoftFix = [] - UnFixed = [item.GetIntFormat() for item in agents if item.GetIntFormat() not in UpperFix] - - - #creating a dictionary of vj(µ) = Pi∈µ(cj) for each j in C - matching_college_valuations = update_matching_valuations_sum(match=str_match, items_valuations=items_valuations) - logger.debug(f"matching_college_valuations: {matching_college_valuations}") - - while len(LowerFix) + len([item for item in UpperFix if item not in LowerFix]) < len(items): - logger.debug(f"\nstr_match: {str_match}, integer_match: {integer_match}, UpperFix: {UpperFix}, LowerFix: {LowerFix}, SoftFix: {SoftFix}, UnFixed: {UnFixed}") - up = min([j.GetIntFormat() for j in items if j.GetIntFormat() not in LowerFix]) - down = min(UnFixed, key=lambda j: matching_college_valuations[f"c{j}"]) - logger.debug(f"up: {up}, down: {down}") - - SoftFix = [pair for pair in SoftFix if not (pair[1] <= up < pair[0])] - logger.debug(f"Updating SoftFix to {SoftFix}") - - logger.debug(f"vup(mu)={matching_college_valuations[f"c{up}"]}, vdown(mu)={matching_college_valuations[f"c{down}"]}") - if (len(integer_match[up]) == 1) or (matching_college_valuations[f"c{up}"] <= matching_college_valuations[f"c{down}"]): - LowerFix.append(up) - logger.info(f"Cannot remove any more students from c_{up}: Added c_{up} to LowerFix") - else: - #check the lowest-rank student who currently belongs to mu(c_{down-1}) - agant_to_demote = get_lowest_ranked_student(down-1, integer_match, items_valuations, int_to_college_name=items, int_to_student_name=agents) - new_integer_match = Demote(deepcopy(integer_match), agant_to_demote, up_index=up, down_index=down) - logger.info(f"Demoting from {up} to {down}, starting at student {agant_to_demote}. New match: {new_integer_match}") - new_match_str = integer_to_str_matching(integer_match=new_integer_match, agent_dict=student_name_to_int, items_dict=college_name_to_int) - - #Creating a leximin tuple for the new match from the demote and for the old match to compare - match_leximin_tuple = create_leximin_tuple(match=str_match , agents_valuations=agents_valuations, items_valuations=items_valuations) - logger.info(f"Old match leximin tuple: {match_leximin_tuple}") - new_match_leximin_tuple = create_leximin_tuple(match=new_match_str, agents_valuations=agents_valuations, items_valuations=items_valuations) - logger.info(f"New match leximin tuple: {new_match_leximin_tuple}") - - #Extarcting from the SourceDec function the problematic item or agent, if there isn't one then it will be "" - problematic_component = sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=match_leximin_tuple) - logger.info(f" problematic component: {problematic_component}") - - if problematic_component == "": - logger.debug(f"New match is leximin-better than old match:") - integer_match = new_integer_match - str_match = new_match_str - matching_college_valuations = update_matching_valuations_sum(match=str_match,items_valuations=items_valuations) - logger.debug(f" Match updated to {str_match}") - - elif problematic_component == int_to_college_name[up]: - logger.debug(f"New match is leximin-worse because of c_up = c_{up}:") - LowerFix.append(up) - UpperFix.append(up + 1) - logger.info(f" Updated LowerFix and UpperFix with {up}") - - elif problematic_component in alloc.instance.agents: - sd = problematic_component - logger.debug(f"New match is leximin-worse because of student {sd}: ") - t = college_name_to_int[get_match(match=str_match, value=sd)] - LowerFix.append(t) - UpperFix.append(t+1) - logger.debug(f" sourceDec student {sd} is matched to c_t = c_{t}: adding c_{t} to LowerFix and c_{t+1} to UpperFix.") - A = [j for j in UnFixed if (j > t + 1)] - SoftFix.extend((j, t+1) for j in A) - logger.debug(f" Updating SoftFix to {SoftFix}") - - else: - logger.debug(f"New match is leximin-worse because of college {sourceDec(new_match_leximin_tuple, match_leximin_tuple)}: ") - str_match, LowerFix, UpperFix, SoftFix = LookAheadRoutine((S, C, agents_valuations, items_valuations), integer_match, down, LowerFix, UpperFix, SoftFix) - logger.debug(f" LookAheadRoutine result: match={str_match}, LowerFix={LowerFix}, UpperFix={UpperFix}, SoftFix={SoftFix}") - - UnFixed = [ - j for j in college_name_to_int.values() - if (j not in UpperFix) or - any((j, _j) not in SoftFix for _j in college_name_to_int.values() if _j > j) - ] - logger.debug(f" Updating UnFixed to {UnFixed}") - - logger.info(f"Finished FaStGen algorithm, final result: {str_match}") - return str_match #We want to return the final match in his string form - -def LookAheadRoutine(I:tuple, integer_match:dict, down:int, LowerFix:list, UpperFix:list, SoftFix:list)->tuple: - """ - Algorithem 4-LookAheadRoutine: Designed to handle cases where a decrease in the leximin value - may be balanced by future changes in the pairing, - the goal is to ensure that the sumi pairing will maintain a good leximin value or even improve it. - - :param I: A presentation of the problem, aka a tuple that contain the list of students(S), the list of colleges(C) when the capacity - of each college is n-1 where n is the number of students, student valuation function(U), college valuation function(V). - :param match: The current match of the students and colleges. - :param down: The lowest ranked unaffixed college - :param LowerFix: The group of colleges whose lower limit is fixed - :param UpperFix: The group of colleges whose upper limit is fixed. - :param SoftFix: A set of temporary upper limits. - *We will asume that in the colleges list in index 0 there is college 1 in index 1 there is coll - - >>> from fairpyx.adaptors import divide - >>> S = ["s1", "s2", "s3", "s4", "s5"] - >>> C = ["c1", "c2", "c3", "c4"] - >>> V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10},"c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26},"c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15}} - >>> U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5},"s2" : {"c1":36,"c2":20,"c3":10,"c4":1},"s3" : {"c1":29,"c2":24,"c3":12,"c4":10},"s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}} - >>> match = {1 : [1,2],2 : [3,5],3 : [4],4 : []} - >>> I = (S,C,U,V) - >>> down = 4 - >>> LowerFix = [1] - >>> UpperFix = [] - >>> SoftFix = [] - >>> LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) - ({'c1': ['s1', 's2'], 'c2': ['s5'], 'c3': ['s3'], 'c4': ['s4']}, [1], [], []) - """ - agents, items, agents_valuations, items_valuations = I - - student_name_to_int = generate_dict_from_str_to_int(agents) - college_name_to_int = generate_dict_from_str_to_int(items) - int_to_student_name = generate_dict_from_int_to_str(agents) - int_to_college_name = generate_dict_from_int_to_str(items) - - LF = LowerFix.copy() - UF = UpperFix.copy() - given_str_match = integer_to_str_matching(integer_match=integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) - new_integer_match = deepcopy(integer_match) - new_str_match = integer_to_str_matching(integer_match=new_integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) - - logger.info(f"Starting LookAheadRoutine. Initial parameters - match: {new_str_match}, down: {down}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") - matching_college_valuations = update_matching_valuations_sum(match=new_str_match,items_valuations=items_valuations) - while len(LF) + len([item for item in UF if item not in LF]) < len(items): - up = min([j for j in college_name_to_int.values() if j not in LF]) - logger.debug(f" Selected 'up': {up}") - if (len(integer_match[up]) == 1) or (matching_college_valuations[int_to_college_name[up]] <= matching_college_valuations[int_to_college_name[down]]): - LF.append(up) - logger.info(f" Cannot remove any more students from c_{up}: appended c_{up} to LF") - else: - #check the lowest-rank student who currently belongs to mu(c_{down-1})d - agant_to_demote = get_lowest_ranked_student(item_int=down-1, match_int=new_integer_match, items_valuations=items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) - new_integer_match = Demote(new_integer_match, agant_to_demote, up_index=up, down_index=down) - logger.info(f" Demoting from {up} to {down}, starting at student {agant_to_demote}. New match: {new_integer_match}") - - new_str_match = integer_to_str_matching(integer_match=new_integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) - matching_college_valuations = update_matching_valuations_sum(match=new_str_match,items_valuations=items_valuations) - - old_match_leximin_tuple = create_leximin_tuple(match=given_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) - new_match_leximin_tuple = create_leximin_tuple(match=new_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) - logger.info(f" Old match leximin tuple: {old_match_leximin_tuple}") - new_match_leximin_tuple = create_leximin_tuple(match=new_str_match, agents_valuations=agents_valuations, items_valuations=items_valuations) - logger.info(f" New match leximin tuple: {new_match_leximin_tuple}") - - #Extarcting from the SourceDec function the problematic item or agent, if there isn't one then it will be "" - problematic_component = sourceDec(new_match_leximin_tuple=new_match_leximin_tuple, old_match_leximin_tuple=old_match_leximin_tuple) - logger.info(f" problematic component: {problematic_component}") - - if problematic_component == "": - logger.debug(f" New match is leximin-better than old match:") - integer_match = new_integer_match - LowerFix = LF - UpperFix = UF - logger.info(" Updated match and fixed LowerFix and UpperFix") - break - - elif problematic_component == int_to_college_name[up]: - logger.debug(f" New match is leximin-worse because of c_up = c_{up}:") - LF.append(up) - UF.append(up + 1) - logger.info(f" Appended {up} to LF and {up+1} to UF") - - elif problematic_component in agents: - sd = problematic_component - logger.debug(f" New match is leximin-worse because of student {sd}: ") - t = college_name_to_int[get_match(match=new_str_match, value=sd)] - logger.debug(f" sourceDec student {sd} is matched to c_t = c_{t}.") - if t == down: - logger.debug(f" t=down={down}: adding c_{down} to UpperFix") # UF? - UpperFix.append(down) - else: - logger.info(f" t!=down: adding ({down},{t}) to SoftFix") - SoftFix.append((down, t)) - break - - final_match_str = integer_to_str_matching(integer_match=integer_match, items_dict=college_name_to_int, agent_dict=student_name_to_int) - logger.info(f"Completed LookAheadRoutine. Final result - match: {final_match_str}, LowerFix: {LowerFix}, UpperFix: {UpperFix}, SoftFix: {SoftFix}") - return (final_match_str, LowerFix, UpperFix, SoftFix) - -def create_leximin_tuple(match:dict, agents_valuations:dict, items_valuations:dict): - """ - Create a leximin tuple from the given match, agents' valuations, and items' valuations. - - Args: - - match (dict): A dictionary where keys are items and values are lists of agents. - - agents_valuations (dict): A dictionary where keys are agents and values are dictionaries of item valuations. - - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. - - Returns: - - list: A sorted list of tuples representing the leximin tuple. - - Example: - >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} - >>> items_valuations = { #the colleges valuations - ... "c1":{"s1":50, "s2":23, "s3":21, "s4":13, "s5":10, "s6":6, "s7":5}, - ... "c2":{"s1":45, "s2":40, "s3":32, "s4":29, "s5":26, "s6":11, "s7":4}, - ... "c3":{"s1":90, "s2":79, "s3":60, "s4":35, "s5":28, "s6":20, "s7":15}, - ... "c4":{"s1":80, "s2":48, "s3":36, "s4":29, "s5":15, "s6":7, "s7":1}, - ... } - >>> agents_valuations = { #the students valuations - ... "s1":{"c1":16, "c2":10, "c3":6, "c4":5}, - ... "s2":{"c1":36, "c2":20, "c3":10, "c4":1}, - ... "s3":{"c1":29, "c2":24, "c3":12, "c4":10}, - ... "s4":{"c1":41, "c2":24, "c3":5, "c4":3}, - ... "s5":{"c1":36, "c2":19, "c3":9, "c4":6}, - ... "s6":{"c1":39, "c2":30, "c3":18, "c4":7}, - ... "s7":{"c1":40, "c2":29, "c3":6, "c4":1} - ... } - >>> create_leximin_tuple(match, agents_valuations, items_valuations) - [('c4', 1), ('s7', 1), ('s1', 16), ('s6', 18), ('s5', 19), ('c3', 20), ('c2', 26), ('s3', 29), ('s2', 36), ('s4', 41), ('c1', 107)] - - >>> match = {"c1":["s1","s2","s3"], "c2":["s4"], "c3":["s5"], "c4":["s7","s6"]} - >>> create_leximin_tuple(match, agents_valuations, items_valuations) - [('s7', 1), ('s6', 7), ('c4', 8), ('s5', 9), ('s1', 16), ('s4', 24), ('c3', 28), ('c2', 29), ('s3', 29), ('s2', 36), ('c1', 94)] - """ - leximin_tuple = [] - matching_college_valuations = update_matching_valuations_sum(match=match, items_valuations=items_valuations) - logger.debug(f"matching_college_valuations: {matching_college_valuations}") - for item in match.keys(): - if len(match[item]) == 0: - leximin_tuple.append((item, 0)) - else: - leximin_tuple.append((item, matching_college_valuations[item])) - for agent in match[item]: - leximin_tuple.append((agent,agents_valuations[agent][item])) - - leximin_tuple = sorted(leximin_tuple, key=lambda x: (x[1], x[0])) - return leximin_tuple - -def sourceDec(new_match_leximin_tuple:list, old_match_leximin_tuple:list)->str: - """ - Determine the agent causing the leximin decrease between two matchings. - - Args: - - new_match_leximin_tuple (list): The leximin tuple of the new matching. - - old_match_leximin_tuple (list): The leximin tuple of the old matching. - - Returns: - - str: The agent (student) causing the leximin decrease. - - Example: - >>> new_match = [("s7",1),("s4",5),("s5",6),("s6",7),("c4",14),("s1",16),("s3",24),("c2",32),("c3",35),("s2",36),("c1",52)] - >>> old_match = [("s7",1),("s6",7),("c4",8),("s5",9),("s1",16),("s4",24),("c3",28),("c2",29),("s3",29),("s2",36),("c1",94)] - >>> sourceDec(new_match, old_match) - 's4' - - >>> new_match = [("s7",1),("s4",7),("s5",8),("s6",9),("c4",14),("s1",16),("s3",24),("c2",32),("c3",35),("s2",36),("c1",52)] - >>> old_match = [("s7",1),("s6",7),("c4",8),("s5",9),("s1",16),("s4",24),("c3",28),("c2",29),("s3",29),("s2",36),("c1",94)] - >>> sourceDec(new_match, old_match) - 'c4' - """ - for k in range(0, len(new_match_leximin_tuple)): - if new_match_leximin_tuple[k][1] == old_match_leximin_tuple[k][1]: - continue - elif new_match_leximin_tuple[k][1] > old_match_leximin_tuple[k][1]: - return "" - else: #new_match_leximin_tuple[k][1] < old_match_leximin_tuple[k][1] - return new_match_leximin_tuple[k][0] - return "" - -def get_lowest_ranked_student(item_int:int, match_int:dict, items_valuations:dict, int_to_college_name:dict, int_to_student_name:dict): - """ - Get the lowest ranked student that is matched to the item with the given index. - - # Args: - # - item: The item for which the lowest ranked student is to be found. - # - match (dict): A dictionary where keys are items and values are lists of agents. - # - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. - - # Returns: - # - str: The lowest ranked student for the given item. - - # Example: - >>> match = {1:[1,2,3,4], 2:[5], 3:[6], 4:[7]} - >>> items_valuations = { #the colleges valuations - ... "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - ... "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - ... "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - ... "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1} - ... } - >>> int_to_college_name = {i: f"c{i}" for i in [1,2,3,4]} - >>> int_to_student_name = {i: f"s{i}" for i in [1,2,3,4,5,6,7]} - >>> get_lowest_ranked_student(1, match, items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) - 4 - >>> get_lowest_ranked_student(2, match, items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) - 5 - >>> get_lowest_ranked_student(3, match, items_valuations, int_to_college_name=int_to_college_name, int_to_student_name=int_to_student_name) - 6 - """ - return min(match_int[item_int], key=lambda agent: items_valuations[int_to_college_name[item_int]][int_to_student_name[agent]]) - -def update_matching_valuations_sum(match:dict, items_valuations:dict)->dict: - """ - Update the sum of valuations for each item in the matching. - - Args: - - match (dict): A dictionary where keys are items and values are lists of agents. - - items_valuations (dict): A dictionary where keys are items and values are dictionaries of agent valuations. - - agents (list): List of agents. - - items (list): List of items. - - Returns: - - dict: A dictionary with the sum of valuations for each item. - - Example: - >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} - >>> items_valuations = { #the colleges valuations - ... "c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, - ... "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, - ... "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15}, - ... "c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":7,"s7":1} - ... } - >>> update_matching_valuations_sum(match, items_valuations) - {'c1': 107, 'c2': 26, 'c3': 20, 'c4': 1} - - >>> match = {"c1":["s1","s2","s3"], "c2":["s4"], "c3":["s5"], "c4":["s7","s6"]} - >>> update_matching_valuations_sum(match, items_valuations) - {'c1': 94, 'c2': 29, 'c3': 28, 'c4': 8} - """ - matching_valuations_sum = { #in the artical it looks like this: vj(mu) - colleague: sum(items_valuations[colleague][student] for student in students) - for colleague, students in match.items() - } - return matching_valuations_sum - -def create_stable_matching(agents, items): - """ - Creating a stable matching according to this: - the first collage get the first n-m+1 students - each collage in deacrising order get the n-(m-j)th student - - Args: - - items_dict: A dictionary of all the items and there indexes like this: ("c1":1). - - agents_dict: A dictionary of all the agents and there indexes like this: ("s1":1). - - agents (list): List of agents. - - items (list): List of items. - - Returns: - - dict: A stable matching of integers - - Example: - >>> agents = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] - >>> items = ["c1", "c2", "c3", "c4"] - >>> agents_dict = {"s1":1, "s2":2, "s3":3, "s4":4, "s5":5, "s6":6, "s7":7} - >>> items_dict = {"c1":1, "c2":2, "c3":3, "c4":4} - >>> create_stable_matching(agents, agents_dict, items, items_dict) - {1: [1, 2, 3, 4], 2: [5], 3: [6], 4: [7]} - """ - # Initialize the matching dictionary - matching = {} - - # Assign the first m-1 students to c1 - matching[items[0]] = [agents[i] for i in range(0, len(agents) - len(items) + 1)] - - # Assign the remaining students to cj for j >= 2 - for j in range(1, len(items)): - matching[items[j]] = [agents[len(agents) - (len(items) - j)]] - - return matching - -def generate_dict_from_str_to_int(input_list:list)->dict: - """ - Creating a dictionary that includes for each string item in the list an index representing it, key=string. - - Args: - - input_list: A list of strings - - Returns: - - dict: a dictionary of strings ang indexes - - Example: - >>> agents = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] - >>> items = ["c1", "c2", "c3", "c4"] - - >>> generate_dict_from_str_to_int(agents) - {'s1': 1, 's2': 2, 's3': 3, 's4': 4, 's5': 5, 's6': 6, 's7': 7} - - >>> generate_dict_from_str_to_int(items) - {'c1': 1, 'c2': 2, 'c3': 3, 'c4': 4} - """ - return {item: index + 1 for index, item in enumerate(input_list)} - -def generate_dict_from_int_to_str(input_list:list)->dict: - """ - Creating a dictionary that includes for each string item in the list an index representing it, key=integer. - - Args: - - input_list: A list of strings - - Returns: - - dict: a dictionary of strings ang indexes - - Example: - >>> agents = ["s1", "s2", "s3", "s4", "s5", "s6", "s7"] - >>> items = ["c1", "c2", "c3", "c4"] - - >>> generate_dict_from_int_to_str(agents) - {1: 's1', 2: 's2', 3: 's3', 4: 's4', 5: 's5', 6: 's6', 7: 's7'} - - >>> generate_dict_from_int_to_str(items) - {1: 'c1', 2: 'c2', 3: 'c3', 4: 'c4'} - """ - return {index + 1: item for index, item in enumerate(input_list)} - -def integer_to_str_matching(integer_match:dict, agent_dict:dict, items_dict:dict)->dict: - """ - Converting an integer match to a string match. - - Args: - - integer_match: A matching of agents to items out of numbers. - - items_dict: A dictionary of all the items and there indexes like this: ("c1":1). - - agents_dict: A dictionary of all the agents and there indexes like this: ("s1":1). - - Returns: - - dict: A string matching. - - Example: - >>> agents_dict = {"s1":1, "s2":2, "s3":3, "s4":4, "s5":5, "s6":6, "s7":7} - >>> items_dict = {"c1":1, "c2":2, "c3":3, "c4":4} - >>> integer_match = {1: [1, 2, 3, 4], 2: [5], 3: [6], 4: [7]} - >>> integer_to_str_matching(integer_match, agents_dict, items_dict) - {'c1': ['s1', 's2', 's3', 's4'], 'c2': ['s5'], 'c3': ['s6'], 'c4': ['s7']} - """ - # Reverse the s_dict and c_dict to map integer values back to their string keys - s_reverse_dict = {v: k for k, v in agent_dict.items()} - c_reverse_dict = {v: k for k, v in items_dict.items()} - - # Create the new dictionary with string keys and lists of string values - return {c_reverse_dict[c_key]: [s_reverse_dict[s_val] for s_val in s_values] for c_key, s_values in integer_match.items()} - -def get_match(match:dict, value:str)->any: - """ - Giving a match and an agent or an item the function will produce its match - - Args: - - match: A matching of agents to items. - - value: An agent or an item. - - Returns: - - any: An agent or an item. - - Example: - >>> match = {"c1":["s1","s2","s3","s4"], "c2":["s5"], "c3":["s6"], "c4":["s7"]} - - >>> value = "c1" - >>> get_match(match, value) - ['s1', 's2', 's3', 's4'] - - >>> value = "s4" - >>> get_match(match, value) - 'c1' - - >>> value = "c4" - >>> get_match(match, value) - ['s7'] - """ - if value in match.keys(): - return match[value] - else: - return next((key for key, values_list in match.items() if value in values_list), None) - -if __name__ == "__main__": - import doctest, sys - # print(doctest.testmod()) - # # doctest.run_docstring_examples(LookAheadRoutine, globals()) - # sys.exit(0) - - logger.setLevel(logging.DEBUG) - logger.addHandler(logging.StreamHandler()) - - S = create_agent_items('s', 1, 7) - C = create_agent_items('c', 1, 4) - V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10,"s6":6,"s7":5}, "c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26,"s6":11,"s7":4}, "c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28,"s6":20,"s7":15},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15,"s6":6,"s7":1}} - U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5}, "s2" : {"c1":36,"c2":20,"c3":10,"c4":1}, "s3" : {"c1":29,"c2":24,"c3":12,"c4":10}, "s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}, "s6" :{"c1":39,"c2":30,"c3":18,"c4":7}, "s7" : {"c1":40,"c2":29,"c3":6,"c4":1}} - ins = Instance(agents=S, items=C, valuations=U) - alloc = AllocationBuilder(instance=ins) - # FaStGen(alloc=alloc, items_valuations=V) From 7647175505fc3b91ea57fb2abe67a6ec6704223c Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Sun, 21 Jul 2024 21:31:15 +0300 Subject: [PATCH 049/111] final update-working code --- fairpyx/algorithms/Optimization_Matching/FaSt.py | 2 -- fairpyx/algorithms/Optimization_Matching/FaStGen.py | 10 +++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index 8c78685..e2fa335 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -145,7 +145,6 @@ def build_pos_array(matching, V): college_index += 1 return pos - def build_college_values(matching, V): """ Build the college_values dictionary that sums the students' valuations for each college. @@ -170,7 +169,6 @@ def build_college_values(matching, V): return college_values - def initialize_matching(n, m): """ Initialize the first stable matching. diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index b1f783b..10906b8 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -7,7 +7,7 @@ from fairpyx import Instance, AllocationBuilder, ExplanationLogger from FaSt import Demote -# from AgentItem import AgentItem, create_agent_items +# from bidict import bidict from copy import deepcopy import logging @@ -146,14 +146,14 @@ def LookAheadRoutine(I:tuple, integer_match:dict, down:int, LowerFix:list, Upper >>> C = ["c1", "c2", "c3", "c4"] >>> V = {"c1" : {"s1":50,"s2":23,"s3":21,"s4":13,"s5":10},"c2" : {"s1":45,"s2":40,"s3":32,"s4":29,"s5":26},"c3" : {"s1":90,"s2":79,"s3":60,"s4":35,"s5":28},"c4" : {"s1":80,"s2":48,"s3":36,"s4":29,"s5":15}} >>> U = {"s1" : {"c1":16,"c2":10,"c3":6,"c4":5},"s2" : {"c1":36,"c2":20,"c3":10,"c4":1},"s3" : {"c1":29,"c2":24,"c3":12,"c4":10},"s4" : {"c1":41,"c2":24,"c3":5,"c4":3},"s5" : {"c1":36,"c2":19,"c3":9,"c4":6}} - >>> match = {1 : [1,2],2 : [3,5],3 : [4],4 : []} + >>> match = {1 : [1,2],2 : [3],3 : [4],4 : [5]} >>> I = (S,C,U,V) >>> down = 4 - >>> LowerFix = [1] - >>> UpperFix = [] + >>> LowerFix = [4] + >>> UpperFix = [1] >>> SoftFix = [] >>> LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) - ({'c1': ['s1', 's2'], 'c2': ['s5'], 'c3': ['s3'], 'c4': ['s4']}, [1], [], []) + ({'c1': ['s1', 's2'], 'c2': ['s3'], 'c3': ['s4'], 'c4': ['s5']}, [4], [1, 4], []) """ agents, items, agents_valuations, items_valuations = I From 762df3cac8f3061514db16c9b5495601306e3172 Mon Sep 17 00:00:00 2001 From: ErgaDN Date: Sun, 21 Jul 2024 23:43:11 +0300 Subject: [PATCH 050/111] fix import for doctest --- .../algorithms/ACEEI_algorithms/find_profitable_manipulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fairpyx/algorithms/ACEEI_algorithms/find_profitable_manipulation.py b/fairpyx/algorithms/ACEEI_algorithms/find_profitable_manipulation.py index 7187b2f..e7ae485 100644 --- a/fairpyx/algorithms/ACEEI_algorithms/find_profitable_manipulation.py +++ b/fairpyx/algorithms/ACEEI_algorithms/find_profitable_manipulation.py @@ -47,7 +47,7 @@ def find_profitable_manipulation(mechanism: callable, student: str, true_student return: The profitable manipulation - >>> from fairpyx.algorithms.ACEEI.ACEEI import find_ACEEI_with_EFTB + >>> from fairpyx.algorithms.ACEEI_algorithms.ACEEI import find_ACEEI_with_EFTB >>> from fairpyx.algorithms import ACEEI_algorithms, tabu_search From 67fd5aacee5261fd765810ebbe91de24b804b98d Mon Sep 17 00:00:00 2001 From: Erel Segal-Halevi Date: Mon, 22 Jul 2024 08:22:41 +0300 Subject: [PATCH 051/111] main --- fairpyx/algorithms/Optimization_Matching/FaSt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fairpyx/algorithms/Optimization_Matching/FaSt.py b/fairpyx/algorithms/Optimization_Matching/FaSt.py index e2fa335..99c5e0e 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaSt.py +++ b/fairpyx/algorithms/Optimization_Matching/FaSt.py @@ -356,4 +356,4 @@ def FaSt(alloc: AllocationBuilder)-> dict: # Change logger level logger.setLevel(logging.DEBUG) # Set logger level to DEBUG import doctest - doctest.testmod() \ No newline at end of file + print(doctest.testmod()) From 0e0affc55a806a6d0a5073f9137d700371e7ea54 Mon Sep 17 00:00:00 2001 From: zachibs Date: Wed, 24 Jul 2024 15:22:46 +0300 Subject: [PATCH 052/111] refactor: taking the files out the dir's --- .../__init__.py | 0 ...hapley_pareto_dominant_market_mechanism.py | 51 ++++++++++--------- fairpyx/algorithms/__init__.py | 2 +- .../test_gale_shapley.py | 0 4 files changed, 27 insertions(+), 26 deletions(-) delete mode 100644 fairpyx/algorithms/Course_bidding_at_business_schools/__init__.py rename fairpyx/algorithms/{Course_bidding_at_business_schools => }/Gale_Shapley_pareto_dominant_market_mechanism.py (99%) rename tests/{Test_Course_bidding_at_business_schools => }/test_gale_shapley.py (100%) diff --git a/fairpyx/algorithms/Course_bidding_at_business_schools/__init__.py b/fairpyx/algorithms/Course_bidding_at_business_schools/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py b/fairpyx/algorithms/Gale_Shapley_pareto_dominant_market_mechanism.py similarity index 99% rename from fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py rename to fairpyx/algorithms/Gale_Shapley_pareto_dominant_market_mechanism.py index c32eabd..34f7bea 100644 --- a/fairpyx/algorithms/Course_bidding_at_business_schools/Gale_Shapley_pareto_dominant_market_mechanism.py +++ b/fairpyx/algorithms/Gale_Shapley_pareto_dominant_market_mechanism.py @@ -17,31 +17,6 @@ logger = logging.getLogger(__name__) -def sort_and_tie_brake(input_dict: Dict[str, float], tie_braking_lottery: Dict[str, float]) -> List[tuple[str, float]]: - """ - Sorts a dictionary by its values in descending order and adds a number - to the values of keys with the same value to break ties. - - Parameters: - input_dict (Dict[str, float]): A dictionary with string keys and float values representing student bids. - tie_braking_lottery (Dict[str, float]): A dictionary with string keys and float values for tie-breaking. - - Returns: - List[tuple[str, float]]: A list of tuples containing student names and their modified bids, sorted in descending order. - - Examples: - >>> input_dict = {"Alice": 45, "Bob": 55, "Chana": 45, "Dana": 60} - >>> tie_braking_lottery = {"Alice": 0.3, "Bob": 0.2, "Chana": 0.4, "Dana": 0.1} - >>> sort_and_tie_brake(input_dict, tie_braking_lottery) - [('Dana', 60), ('Bob', 55), ('Chana', 45), ('Alice', 45)] - """ - - - # Sort the dictionary by adjusted values in descending order - sorted_dict = (sorted(input_dict.items(), key=lambda item: item[1] + tie_braking_lottery[item[0]], reverse=True)) - - return sorted_dict - def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, List[str]], tie_braking_lottery: Union[None, Dict[str, float]] = None): """ Allocate the given items to the given agents using the Gale-Shapley protocol. @@ -163,6 +138,32 @@ def gale_shapley(alloc: AllocationBuilder, course_order_per_student: Dict[str, L alloc.give(student, course_name, logger) logger.info(f"The final course matchings are: {alloc.bundles}") + +def sort_and_tie_brake(input_dict: Dict[str, float], tie_braking_lottery: Dict[str, float]) -> List[tuple[str, float]]: + """ + Sorts a dictionary by its values in descending order and adds a number + to the values of keys with the same value to break ties. + + Parameters: + input_dict (Dict[str, float]): A dictionary with string keys and float values representing student bids. + tie_braking_lottery (Dict[str, float]): A dictionary with string keys and float values for tie-breaking. + + Returns: + List[tuple[str, float]]: A list of tuples containing student names and their modified bids, sorted in descending order. + + Examples: + >>> input_dict = {"Alice": 45, "Bob": 55, "Chana": 45, "Dana": 60} + >>> tie_braking_lottery = {"Alice": 0.3, "Bob": 0.2, "Chana": 0.4, "Dana": 0.1} + >>> sort_and_tie_brake(input_dict, tie_braking_lottery) + [('Dana', 60), ('Bob', 55), ('Chana', 45), ('Alice', 45)] + """ + + + # Sort the dictionary by adjusted values in descending order + sorted_dict = (sorted(input_dict.items(), key=lambda item: item[1] + tie_braking_lottery[item[0]], reverse=True)) + + return sorted_dict + if __name__ == "__main__": import doctest print(doctest.testmod()) diff --git a/fairpyx/algorithms/__init__.py b/fairpyx/algorithms/__init__.py index 6ea020f..aeea878 100644 --- a/fairpyx/algorithms/__init__.py +++ b/fairpyx/algorithms/__init__.py @@ -2,7 +2,7 @@ from fairpyx.algorithms.iterated_maximum_matching import iterated_maximum_matching, iterated_maximum_matching_adjusted, iterated_maximum_matching_unadjusted from fairpyx.algorithms.picking_sequence import round_robin, bidirectional_round_robin, serial_dictatorship from fairpyx.algorithms.utilitarian_matching import utilitarian_matching -from fairpyx.algorithms.Course_bidding_at_business_schools.Gale_Shapley_pareto_dominant_market_mechanism import gale_shapley +from fairpyx.algorithms.Gale_Shapley_pareto_dominant_market_mechanism import gale_shapley from fairpyx.algorithms.ACEEI.tabu_search import tabu_search from fairpyx.algorithms.ACEEI.ACEEI import find_ACEEI_with_EFTB from fairpyx.algorithms.Optimization_based_Mechanisms.OC import OC_function diff --git a/tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py b/tests/test_gale_shapley.py similarity index 100% rename from tests/Test_Course_bidding_at_business_schools/test_gale_shapley.py rename to tests/test_gale_shapley.py From 7e0a775e5f7c4da67f8230b797c0c807f385363a Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Wed, 24 Jul 2024 18:11:47 +0300 Subject: [PATCH 053/111] update --- experiments/algorithms_compare.py | 166 ++++++++++++++++++++++++++++++ tests/test_FaStGen.py | 2 +- tests/test_look_ahead.py | 16 +-- 3 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 experiments/algorithms_compare.py diff --git a/experiments/algorithms_compare.py b/experiments/algorithms_compare.py new file mode 100644 index 0000000..108120d --- /dev/null +++ b/experiments/algorithms_compare.py @@ -0,0 +1,166 @@ +######### COMMON VARIABLES AND ROUTINES ########## + +from fairpyx import divide, AgentBundleValueMatrix, Instance +import fairpyx.algorithms as crs +from typing import * +import numpy as np +from fairpyx.algorithms.Optimization_Matching import FaStGen, FaSt # Ensure your FaStGen and FaSt are properly imported +from fairpyx import AllocationBuilder # Assuming this is where AllocationBuilder comes from + +max_value = 1000 +normalized_sum_of_values = 1000 +TIME_LIMIT = 100 + +algorithms_to_check = [ + crs.utilitarian_matching, + crs.iterated_maximum_matching_unadjusted, + crs.iterated_maximum_matching_adjusted, + crs.serial_dictatorship, # Very bad performance + crs.round_robin, + crs.bidirectional_round_robin, + crs.almost_egalitarian_without_donation, + crs.almost_egalitarian_with_donation, + FaStGen, + FaSt +] + +def evaluate_algorithm_on_instance(algorithm, instance): + if algorithm in [FaStGen, FaSt]: + alloc = AllocationBuilder(instance=instance) + if algorithm == FaStGen: + items_valuations = instance._valuations # Assuming you need to pass this + allocation = algorithm(alloc, items_valuations) + else: + allocation = algorithm(alloc) + else: + allocation = divide(algorithm, instance) + matrix = AgentBundleValueMatrix(instance, allocation) + matrix.use_normalized_values() + return { + "utilitarian_value": matrix.utilitarian_value(), + "egalitarian_value": matrix.egalitarian_value(), + "max_envy": matrix.max_envy(), + "mean_envy": matrix.mean_envy(), + "max_deficit": matrix.max_deficit(), + "mean_deficit": matrix.mean_deficit(), + "num_with_top_1": matrix.count_agents_with_top_rank(1), + "num_with_top_2": matrix.count_agents_with_top_rank(2), + "num_with_top_3": matrix.count_agents_with_top_rank(3), + } + +######### EXPERIMENT WITH UNIFORMLY-RANDOM DATA ########## + +def course_allocation_with_random_instance_uniform( + num_of_agents:int, num_of_items:int, + value_noise_ratio:float, + algorithm:Callable, + random_seed: int,): + agent_capacity_bounds = [6,6] + item_capacity_bounds = [40,40] + np.random.seed(random_seed) + instance = Instance.random_uniform( + num_of_agents=num_of_agents, num_of_items=num_of_items, + normalized_sum_of_values=normalized_sum_of_values, + agent_capacity_bounds=agent_capacity_bounds, + item_capacity_bounds=item_capacity_bounds, + item_base_value_bounds=[1,max_value], + item_subjective_ratio_bounds=[1-value_noise_ratio, 1+value_noise_ratio] + ) + return evaluate_algorithm_on_instance(algorithm, instance) + +def run_uniform_experiment(): + # Run on uniformly-random data: + experiment = experiments_csv.Experiment("results/", "course_allocation_uniform.csv", backup_folder="results/backup/") + input_ranges = { + "num_of_agents": [100,200,300], + "num_of_items": [25], + "value_noise_ratio": [0, 0.2, 0.5, 0.8, 1], + "algorithm": algorithms_to_check, + "random_seed": range(5), + } + experiment.run_with_time_limit(course_allocation_with_random_instance_uniform, input_ranges, time_limit=TIME_LIMIT) + +######### EXPERIMENT WITH DATA GENERATED ACCORDING TO THE SZWS MODEL ########## + +def course_allocation_with_random_instance_szws( + num_of_agents:int, num_of_items:int, + agent_capacity:int, + supply_ratio:float, + num_of_popular_items:int, + mean_num_of_favorite_items:float, + favorite_item_value_bounds:tuple[int,int], + nonfavorite_item_value_bounds:tuple[int,int], + algorithm:Callable, + random_seed: int,): + np.random.seed(random_seed) + instance = Instance.random_szws( + num_of_agents=num_of_agents, num_of_items=num_of_items, normalized_sum_of_values=normalized_sum_of_values, + agent_capacity=agent_capacity, + supply_ratio=supply_ratio, + num_of_popular_items=num_of_popular_items, + mean_num_of_favorite_items=mean_num_of_favorite_items, + favorite_item_value_bounds=favorite_item_value_bounds, + nonfavorite_item_value_bounds=nonfavorite_item_value_bounds, + ) + return evaluate_algorithm_on_instance(algorithm, instance) + +def run_szws_experiment(): + # Run on SZWS simulated data: + experiment = experiments_csv.Experiment("results/", "course_allocation_szws.csv", backup_folder="results/backup/") + input_ranges = { + "num_of_agents": [100,200,300], + "num_of_items": [25], # in SZWS: 25 + "agent_capacity": [5], # as in SZWS + "supply_ratio": [1.1, 1.25, 1.5], # as in SZWS + "num_of_popular_items": [6, 9], # as in SZWS + "mean_num_of_favorite_items": [2.6, 3.85], # as in SZWS code https://github.com/marketdesignresearch/Course-Match-Preference-Simulator/blob/main/preference_generator_demo.ipynb + "favorite_item_value_bounds": [(50,100)], # as in SZWS code https://github.com/marketdesignresearch/Course-Match-Preference-Simulator/blob/main/preference_generator.py + "nonfavorite_item_value_bounds": [(0,50)], # as in SZWS code https://github.com/marketdesignresearch/Course-Match-Preference-Simulator/blob/main/preference_generator.py + "algorithm": algorithms_to_check, + "random_seed": range(5), + } + experiment.run_with_time_limit(course_allocation_with_random_instance_szws, input_ranges, time_limit=TIME_LIMIT) + +######### EXPERIMENT WITH DATA SAMPLED FROM ARIEL 5783 DATA ########## + +import json +filename = "data/ariel_5783_input.json" +with open(filename, "r", encoding="utf-8") as file: + ariel_5783_input = json.load(file) + +def course_allocation_with_random_instance_sample( + max_total_agent_capacity:int, + algorithm:Callable, + random_seed: int,): + np.random.seed(random_seed) + + (valuations, agent_capacities, item_capacities, agent_conflicts, item_conflicts) = \ + (ariel_5783_input["valuations"], ariel_5783_input["agent_capacities"], ariel_5783_input["item_capacities"], ariel_5783_input["agent_conflicts"], ariel_5783_input["item_conflicts"]) + instance = Instance.random_sample( + max_num_of_agents = max_total_agent_capacity, + max_total_agent_capacity = max_total_agent_capacity, + prototype_agent_conflicts=agent_conflicts, + prototype_agent_capacities=agent_capacities, + prototype_valuations=valuations, + item_capacities=item_capacities, + item_conflicts=item_conflicts) + return evaluate_algorithm_on_instance(algorithm, instance) + +def run_ariel_experiment(): + # Run on Ariel sample data: + experiment = experiments_csv.Experiment("results/", "course_allocation_ariel.csv", backup_folder="results/backup/") + input_ranges = { + "max_total_agent_capacity": [1000, 1115, 1500, 2000], # in reality: 1115 + "algorithm": algorithms_to_check, + "random_seed": range(10), + } + experiment.run_with_time_limit(course_allocation_with_random_instance_sample, input_ranges, time_limit=TIME_LIMIT) + +######### MAIN PROGRAM ########## + +if __name__ == "__main__": + import logging, experiments_csv + experiments_csv.logger.setLevel(logging.INFO) + run_uniform_experiment() + run_szws_experiment() + run_ariel_experiment() diff --git a/tests/test_FaStGen.py b/tests/test_FaStGen.py index 36e370b..b1bf800 100644 --- a/tests/test_FaStGen.py +++ b/tests/test_FaStGen.py @@ -41,7 +41,7 @@ def test_FaStGen_basic_case(): allocation = FaStGen(instance, agents_valuations=U, items_valuations=V) # Define the expected allocation (this is hypothetical; you should set it based on the actual expected output) - expected_allocation = {"c1" : ["s1","s2","s3","s4"], "c2" : ["s5"], "c3" : ["s6"], "c4" : ["s7"]} + expected_allocation = {"c1" : ["s1","s2","s3"], "c2" : ["s4"], "c3" : ["s5"], "c4" : ["s7", "s6"]} # Assert the result assert allocation == expected_allocation, "FaStGen algorithm basic case failed" diff --git a/tests/test_look_ahead.py b/tests/test_look_ahead.py index 81da191..a61828d 100644 --- a/tests/test_look_ahead.py +++ b/tests/test_look_ahead.py @@ -33,13 +33,13 @@ def test_look_ahead_routine_basic_case(): I = (S, C, U ,V) match = { "c1" : ["s1","s2"], - "c2" : ["s3","s5"], + "c2" : ["s3"], "c3" : ["s4"], - "c4" : [] + "c4" : ["s5"] } - down = "c4" - LowerFix = [] - UpperFix = [] + down = 4 + LowerFix = [4] + UpperFix = [1] SoftFix = [] @@ -47,9 +47,9 @@ def test_look_ahead_routine_basic_case(): new_match, new_LowerFix, new_UpperFix, new_SoftFix = LookAheadRoutine(I, match, down, LowerFix, UpperFix, SoftFix) # Define the expected output - expected_new_match = {"c1": ["s1", "s2"], "c2": ["s5"], "c3" : ["s3"], "c4" : ["s4"]} - expected_new_LowerFix = ['c1'] - expected_new_UpperFix = [] + expected_new_match = {"c1": ["s1", "s2"], "c2": ["s3"], "c3" : ["s4"], "c4" : ["s5"]} + expected_new_LowerFix = [4] + expected_new_UpperFix = [1, 4] expected_new_SoftFix = [] # Assert the result From 4f3ff1025c291d57a92a22cc81a0117ef3a743da Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Wed, 24 Jul 2024 20:28:20 +0300 Subject: [PATCH 054/111] updating the comparance --- experiments/algorithms_compare.py | 268 +++++++----------- .../Optimization_Matching/FaStGen.py | 2 + 2 files changed, 104 insertions(+), 166 deletions(-) diff --git a/experiments/algorithms_compare.py b/experiments/algorithms_compare.py index 108120d..fb84a95 100644 --- a/experiments/algorithms_compare.py +++ b/experiments/algorithms_compare.py @@ -1,166 +1,102 @@ -######### COMMON VARIABLES AND ROUTINES ########## - -from fairpyx import divide, AgentBundleValueMatrix, Instance -import fairpyx.algorithms as crs -from typing import * -import numpy as np -from fairpyx.algorithms.Optimization_Matching import FaStGen, FaSt # Ensure your FaStGen and FaSt are properly imported -from fairpyx import AllocationBuilder # Assuming this is where AllocationBuilder comes from - -max_value = 1000 -normalized_sum_of_values = 1000 -TIME_LIMIT = 100 - -algorithms_to_check = [ - crs.utilitarian_matching, - crs.iterated_maximum_matching_unadjusted, - crs.iterated_maximum_matching_adjusted, - crs.serial_dictatorship, # Very bad performance - crs.round_robin, - crs.bidirectional_round_robin, - crs.almost_egalitarian_without_donation, - crs.almost_egalitarian_with_donation, - FaStGen, - FaSt -] - -def evaluate_algorithm_on_instance(algorithm, instance): - if algorithm in [FaStGen, FaSt]: - alloc = AllocationBuilder(instance=instance) - if algorithm == FaStGen: - items_valuations = instance._valuations # Assuming you need to pass this - allocation = algorithm(alloc, items_valuations) - else: - allocation = algorithm(alloc) - else: - allocation = divide(algorithm, instance) - matrix = AgentBundleValueMatrix(instance, allocation) - matrix.use_normalized_values() - return { - "utilitarian_value": matrix.utilitarian_value(), - "egalitarian_value": matrix.egalitarian_value(), - "max_envy": matrix.max_envy(), - "mean_envy": matrix.mean_envy(), - "max_deficit": matrix.max_deficit(), - "mean_deficit": matrix.mean_deficit(), - "num_with_top_1": matrix.count_agents_with_top_rank(1), - "num_with_top_2": matrix.count_agents_with_top_rank(2), - "num_with_top_3": matrix.count_agents_with_top_rank(3), - } - -######### EXPERIMENT WITH UNIFORMLY-RANDOM DATA ########## - -def course_allocation_with_random_instance_uniform( - num_of_agents:int, num_of_items:int, - value_noise_ratio:float, - algorithm:Callable, - random_seed: int,): - agent_capacity_bounds = [6,6] - item_capacity_bounds = [40,40] - np.random.seed(random_seed) - instance = Instance.random_uniform( - num_of_agents=num_of_agents, num_of_items=num_of_items, - normalized_sum_of_values=normalized_sum_of_values, - agent_capacity_bounds=agent_capacity_bounds, - item_capacity_bounds=item_capacity_bounds, - item_base_value_bounds=[1,max_value], - item_subjective_ratio_bounds=[1-value_noise_ratio, 1+value_noise_ratio] - ) - return evaluate_algorithm_on_instance(algorithm, instance) - -def run_uniform_experiment(): - # Run on uniformly-random data: - experiment = experiments_csv.Experiment("results/", "course_allocation_uniform.csv", backup_folder="results/backup/") - input_ranges = { - "num_of_agents": [100,200,300], - "num_of_items": [25], - "value_noise_ratio": [0, 0.2, 0.5, 0.8, 1], - "algorithm": algorithms_to_check, - "random_seed": range(5), - } - experiment.run_with_time_limit(course_allocation_with_random_instance_uniform, input_ranges, time_limit=TIME_LIMIT) - -######### EXPERIMENT WITH DATA GENERATED ACCORDING TO THE SZWS MODEL ########## - -def course_allocation_with_random_instance_szws( - num_of_agents:int, num_of_items:int, - agent_capacity:int, - supply_ratio:float, - num_of_popular_items:int, - mean_num_of_favorite_items:float, - favorite_item_value_bounds:tuple[int,int], - nonfavorite_item_value_bounds:tuple[int,int], - algorithm:Callable, - random_seed: int,): - np.random.seed(random_seed) - instance = Instance.random_szws( - num_of_agents=num_of_agents, num_of_items=num_of_items, normalized_sum_of_values=normalized_sum_of_values, - agent_capacity=agent_capacity, - supply_ratio=supply_ratio, - num_of_popular_items=num_of_popular_items, - mean_num_of_favorite_items=mean_num_of_favorite_items, - favorite_item_value_bounds=favorite_item_value_bounds, - nonfavorite_item_value_bounds=nonfavorite_item_value_bounds, - ) - return evaluate_algorithm_on_instance(algorithm, instance) - -def run_szws_experiment(): - # Run on SZWS simulated data: - experiment = experiments_csv.Experiment("results/", "course_allocation_szws.csv", backup_folder="results/backup/") - input_ranges = { - "num_of_agents": [100,200,300], - "num_of_items": [25], # in SZWS: 25 - "agent_capacity": [5], # as in SZWS - "supply_ratio": [1.1, 1.25, 1.5], # as in SZWS - "num_of_popular_items": [6, 9], # as in SZWS - "mean_num_of_favorite_items": [2.6, 3.85], # as in SZWS code https://github.com/marketdesignresearch/Course-Match-Preference-Simulator/blob/main/preference_generator_demo.ipynb - "favorite_item_value_bounds": [(50,100)], # as in SZWS code https://github.com/marketdesignresearch/Course-Match-Preference-Simulator/blob/main/preference_generator.py - "nonfavorite_item_value_bounds": [(0,50)], # as in SZWS code https://github.com/marketdesignresearch/Course-Match-Preference-Simulator/blob/main/preference_generator.py - "algorithm": algorithms_to_check, - "random_seed": range(5), - } - experiment.run_with_time_limit(course_allocation_with_random_instance_szws, input_ranges, time_limit=TIME_LIMIT) - -######### EXPERIMENT WITH DATA SAMPLED FROM ARIEL 5783 DATA ########## - -import json -filename = "data/ariel_5783_input.json" -with open(filename, "r", encoding="utf-8") as file: - ariel_5783_input = json.load(file) - -def course_allocation_with_random_instance_sample( - max_total_agent_capacity:int, - algorithm:Callable, - random_seed: int,): - np.random.seed(random_seed) - - (valuations, agent_capacities, item_capacities, agent_conflicts, item_conflicts) = \ - (ariel_5783_input["valuations"], ariel_5783_input["agent_capacities"], ariel_5783_input["item_capacities"], ariel_5783_input["agent_conflicts"], ariel_5783_input["item_conflicts"]) - instance = Instance.random_sample( - max_num_of_agents = max_total_agent_capacity, - max_total_agent_capacity = max_total_agent_capacity, - prototype_agent_conflicts=agent_conflicts, - prototype_agent_capacities=agent_capacities, - prototype_valuations=valuations, - item_capacities=item_capacities, - item_conflicts=item_conflicts) - return evaluate_algorithm_on_instance(algorithm, instance) - -def run_ariel_experiment(): - # Run on Ariel sample data: - experiment = experiments_csv.Experiment("results/", "course_allocation_ariel.csv", backup_folder="results/backup/") - input_ranges = { - "max_total_agent_capacity": [1000, 1115, 1500, 2000], # in reality: 1115 - "algorithm": algorithms_to_check, - "random_seed": range(10), - } - experiment.run_with_time_limit(course_allocation_with_random_instance_sample, input_ranges, time_limit=TIME_LIMIT) - -######### MAIN PROGRAM ########## - -if __name__ == "__main__": - import logging, experiments_csv - experiments_csv.logger.setLevel(logging.INFO) - run_uniform_experiment() - run_szws_experiment() - run_ariel_experiment() + +from fairpyx.algorithms.Optimization_Matching import FaSt, FaStGen +from fairpyx import Instance, AllocationBuilder, ExplanationLogger +import experiments_csv +import time +import random + +def generate_isometric_data(): + """valuation= {"S1": {"c1": 9, "c2": 8, "c3": 7}, + ... "S2": {"c1": 8, "c2": 7, "c3":6}, + ... "S3": {"c1": 7, "c2": 6, "c3":5}, + ... "S4": {"c1": 6, "c2": 5, "c3":4}, + ... "S5": {"c1": 5, "c2": 4, "c3":3}, + ... "S6": {"c1": 4, "c2": 3, "c3":2}, + ... "S7": {"c1": 3, "c2": 2, "c3":1}}# V[i][j] is the valuation of Si for matching with Cj""" + # Generate the number of agents + num_of_agents = random.randint(100, 400) + + # Generate the number of items, ensuring it is not greater than the number of agents + num_of_items = random.randint(1, num_of_agents) + + random_integers = random.sample(range(1, 1001), num_of_agents) + random_integers.sort() + #agents dict creation > agents = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} #Student set=S + agents = [f"s{i}" for i in range (1,num_of_agents + 1)] + #items dict creation > items = {"c1", "c2", "c3"} #College set=C + items = [f"c{i}" for i in range (1,num_of_items + 1)] + + valuations = {} + for student in agents: + valuations_for_items = sorted([random.randint(1, 1000) for _ in items], reverse=True) + valuations[student] = {college: valuations_for_items[i] for i, college in enumerate(items)} + + return agents, items, valuations + +def generate_regular_data(): + # Generate the number of agents + num_of_agents = random.randint(100, 400) + + # Generate the number of items, ensuring it is not greater than the number of agents + num_of_items = random.randint(1, num_of_agents) + + random_integers = random.sample(range(1, 1001), num_of_agents) + random_integers.sort() + #agents dict creation > agents = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} #Student set=S + agents = [f"s{i}" for i in range (1,num_of_agents + 1)] + #items dict creation > items = {"c1", "c2", "c3"} #College set=C + items = [f"c{i}" for i in range (1,num_of_items + 1)] + + agents_valuations = {} + for agent in agents: + valuations_for_items = sorted([random.randint(1, 1000) for _ in items], reverse=True) + agents_valuations[agent] = {item: valuations_for_items[i] for i, item in enumerate(items)} + + items_valuations = {} + for item in items: + valuations_for_agents = sorted([random.randint(1, 1000) for _ in agents], reverse=True) + items_valuations[item] = {agent: valuations_for_agents[i] for i, agent in enumerate(agents)} + + return agents, items, agents_valuations, items_valuations + + +if __name__ == "main": + ex = experiments_csv.Experiment("results/", "FaStVsFaStGen.csv") + + agents, items, valuation = generate_isometric_data() + ins = Instance(agents=agents, items=items, valuations=valuation) + alloc = AllocationBuilder(instance=ins) + + start_time = time.time() # Measure time for improvements + FaSt(alloc=alloc) + end_time = time.time()# Measure time for improvements + print("Time for FaSt: ", end_time - start_time) + + # ex.clear_previous_results() + # ex.run(add_three_numbers, input_ranges) + + start_time = time.time() # Measure time for improvements + ins = Instance(agents=agents, items=items, valuations=valuation) + alloc = AllocationBuilder(instance=ins) + FaStGen(alloc=alloc, items_valuations=valuation) + end_time = time.time()# Measure time for improvements + print("Time for FaStGen: ", end_time - start_time) + + + # agents, items, agents_valuations, items_valuation = generate_isometric_data() + + # start_time = time.time() # Measure time for improvements + # ins = Instance(agents=agents, items=items, valuations=agents_valuations) + # alloc = AllocationBuilder(instance=ins) + # FaStGen(alloc=alloc, items_valuations=items_valuation) + # end_time = time.time()# Measure time for improvements + # print("Time for FaStGen over 1st data: ", end_time - start_time) + + # agents, items, items_valuation, agents_valuations = generate_isometric_data() + + # start_time = time.time() # Measure time for improvements + # ins = Instance(agents=agents, items=items, valuations=agents_valuations) + # alloc = AllocationBuilder(instance=ins) + # FaStGen(alloc=alloc, items_valuations=items_valuation) + # end_time = time.time()# Measure time for improvements + # print("Time for FaStGen over 2nd data: ", end_time - start_time) \ No newline at end of file diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index 10906b8..550ef5d 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -9,6 +9,8 @@ from FaSt import Demote # from bidict import bidict from copy import deepcopy +import random + import logging logger = logging.getLogger(__name__) From 44840d873ac69a98dc210b71fceea52967230d5b Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Tue, 30 Jul 2024 18:20:03 +0300 Subject: [PATCH 055/111] updating the compare algorithms and uploading the first app try --- experiments/FaSt_FaStGen_compare.py | 81 ++++++++++++ experiments/algorithms_compare.py | 102 --------------- .../Optimization_Matching/FaStGen.py | 3 +- matchingApp/app.py | 88 +++++++++++++ matchingApp/templates/index.html | 116 ++++++++++++++++++ 5 files changed, 286 insertions(+), 104 deletions(-) create mode 100644 experiments/FaSt_FaStGen_compare.py delete mode 100644 experiments/algorithms_compare.py create mode 100644 matchingApp/app.py create mode 100644 matchingApp/templates/index.html diff --git a/experiments/FaSt_FaStGen_compare.py b/experiments/FaSt_FaStGen_compare.py new file mode 100644 index 0000000..0c3f2e6 --- /dev/null +++ b/experiments/FaSt_FaStGen_compare.py @@ -0,0 +1,81 @@ + +import inspect +import logging +from fairpyx.algorithms.Optimization_Matching import FaSt, FaStGen +from fairpyx import Instance, AllocationBuilder, ExplanationLogger +import experiments_csv +from matplotlib import pyplot as plt +import random + +TIME_LIMIT = 100 + +algorithms_to_check = [ + FaSt, + FaStGen + ] + + + +def generate_isometric_data(num_of_agents, num_of_items): + #agents dict creation > agents = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} #Student set=S + agents = [f"s{i}" for i in range (1,num_of_agents + 1)] + #items dict creation > items = {"c1", "c2", "c3"} #College set=C + items = [f"c{i}" for i in range (1,num_of_items + 1)] + + valuations = {} + for student in agents: + valuations_for_items = sorted([random.randint(1, 1000) for _ in items], reverse=True) + valuations[student] = {college: valuations_for_items[i] for i, college in enumerate(items)} + + return agents, items, valuations + +def generate_regular_data(num_of_agents, num_of_items): + #agents dict creation > agents = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} #Student set=S + agents = [f"s{i}" for i in range (1,num_of_agents + 1)] + #items dict creation > items = {"c1", "c2", "c3"} #College set=C + items = [f"c{i}" for i in range (1,num_of_items + 1)] + + agents_valuations = {} + for agent in agents: + valuations_for_items = sorted([random.randint(1, 1000) for _ in items], reverse=True) + agents_valuations[agent] = {item: valuations_for_items[i] for i, item in enumerate(items)} + + items_valuations = {} + for item in items: + valuations_for_agents = sorted([random.randint(1, 1000) for _ in agents], reverse=True) + items_valuations[item] = {agent: valuations_for_agents[i] for i, agent in enumerate(agents)} + + return agents, items, agents_valuations, items_valuations + +def evaluate_algorithm_output(matching, valuation): + + return {} + + +def run_algorithm_with_random_instance_uniform(num_of_agents, num_of_items, algorithm): + matchingFaSt, matchingFaStGen = {} + agents, items, valuations = generate_isometric_data(num_of_agents=num_of_agents, num_of_items=num_of_items) + allocation = AllocationBuilder(agents, items, valuations) + if inspect.getsource(algorithm) == inspect.getsource(FaSt): + matchingFaSt = FaSt(allocation) + if inspect.getsource(algorithm) == inspect.getsource(FaStGen): + matchingFaStGen = FaSt(allocation, valuations) + return { + "FaSt" : evaluate_algorithm_output(matching=matchingFaSt, valuation=valuations), + "FaStGen" : evaluate_algorithm_output(matching=matchingFaStGen, valuation=valuations) + } + +def run_uniform_experiment(): + # Run on uniformly-random data: + experiment = experiments_csv.Experiment("results/", "FaStVsFaStGen.csv", backup_folder="results/backup/") + input_ranges = { + "num_of_agents": [100,200,300], + "num_of_items": [25], + "algorithm": algorithms_to_check, + "random_seed": range(5), + } + experiment.run_with_time_limit(run_algorithm_with_random_instance_uniform, input_ranges, time_limit=TIME_LIMIT) + +if __name__ == "__main__": + experiments_csv.logger.setLevel(logging.INFO) + run_uniform_experiment() \ No newline at end of file diff --git a/experiments/algorithms_compare.py b/experiments/algorithms_compare.py deleted file mode 100644 index fb84a95..0000000 --- a/experiments/algorithms_compare.py +++ /dev/null @@ -1,102 +0,0 @@ - -from fairpyx.algorithms.Optimization_Matching import FaSt, FaStGen -from fairpyx import Instance, AllocationBuilder, ExplanationLogger -import experiments_csv -import time -import random - -def generate_isometric_data(): - """valuation= {"S1": {"c1": 9, "c2": 8, "c3": 7}, - ... "S2": {"c1": 8, "c2": 7, "c3":6}, - ... "S3": {"c1": 7, "c2": 6, "c3":5}, - ... "S4": {"c1": 6, "c2": 5, "c3":4}, - ... "S5": {"c1": 5, "c2": 4, "c3":3}, - ... "S6": {"c1": 4, "c2": 3, "c3":2}, - ... "S7": {"c1": 3, "c2": 2, "c3":1}}# V[i][j] is the valuation of Si for matching with Cj""" - # Generate the number of agents - num_of_agents = random.randint(100, 400) - - # Generate the number of items, ensuring it is not greater than the number of agents - num_of_items = random.randint(1, num_of_agents) - - random_integers = random.sample(range(1, 1001), num_of_agents) - random_integers.sort() - #agents dict creation > agents = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} #Student set=S - agents = [f"s{i}" for i in range (1,num_of_agents + 1)] - #items dict creation > items = {"c1", "c2", "c3"} #College set=C - items = [f"c{i}" for i in range (1,num_of_items + 1)] - - valuations = {} - for student in agents: - valuations_for_items = sorted([random.randint(1, 1000) for _ in items], reverse=True) - valuations[student] = {college: valuations_for_items[i] for i, college in enumerate(items)} - - return agents, items, valuations - -def generate_regular_data(): - # Generate the number of agents - num_of_agents = random.randint(100, 400) - - # Generate the number of items, ensuring it is not greater than the number of agents - num_of_items = random.randint(1, num_of_agents) - - random_integers = random.sample(range(1, 1001), num_of_agents) - random_integers.sort() - #agents dict creation > agents = {"s1", "s2", "s3", "s4", "s5", "s6", "s7"} #Student set=S - agents = [f"s{i}" for i in range (1,num_of_agents + 1)] - #items dict creation > items = {"c1", "c2", "c3"} #College set=C - items = [f"c{i}" for i in range (1,num_of_items + 1)] - - agents_valuations = {} - for agent in agents: - valuations_for_items = sorted([random.randint(1, 1000) for _ in items], reverse=True) - agents_valuations[agent] = {item: valuations_for_items[i] for i, item in enumerate(items)} - - items_valuations = {} - for item in items: - valuations_for_agents = sorted([random.randint(1, 1000) for _ in agents], reverse=True) - items_valuations[item] = {agent: valuations_for_agents[i] for i, agent in enumerate(agents)} - - return agents, items, agents_valuations, items_valuations - - -if __name__ == "main": - ex = experiments_csv.Experiment("results/", "FaStVsFaStGen.csv") - - agents, items, valuation = generate_isometric_data() - ins = Instance(agents=agents, items=items, valuations=valuation) - alloc = AllocationBuilder(instance=ins) - - start_time = time.time() # Measure time for improvements - FaSt(alloc=alloc) - end_time = time.time()# Measure time for improvements - print("Time for FaSt: ", end_time - start_time) - - # ex.clear_previous_results() - # ex.run(add_three_numbers, input_ranges) - - start_time = time.time() # Measure time for improvements - ins = Instance(agents=agents, items=items, valuations=valuation) - alloc = AllocationBuilder(instance=ins) - FaStGen(alloc=alloc, items_valuations=valuation) - end_time = time.time()# Measure time for improvements - print("Time for FaStGen: ", end_time - start_time) - - - # agents, items, agents_valuations, items_valuation = generate_isometric_data() - - # start_time = time.time() # Measure time for improvements - # ins = Instance(agents=agents, items=items, valuations=agents_valuations) - # alloc = AllocationBuilder(instance=ins) - # FaStGen(alloc=alloc, items_valuations=items_valuation) - # end_time = time.time()# Measure time for improvements - # print("Time for FaStGen over 1st data: ", end_time - start_time) - - # agents, items, items_valuation, agents_valuations = generate_isometric_data() - - # start_time = time.time() # Measure time for improvements - # ins = Instance(agents=agents, items=items, valuations=agents_valuations) - # alloc = AllocationBuilder(instance=ins) - # FaStGen(alloc=alloc, items_valuations=items_valuation) - # end_time = time.time()# Measure time for improvements - # print("Time for FaStGen over 2nd data: ", end_time - start_time) \ No newline at end of file diff --git a/fairpyx/algorithms/Optimization_Matching/FaStGen.py b/fairpyx/algorithms/Optimization_Matching/FaStGen.py index 550ef5d..a50a37c 100644 --- a/fairpyx/algorithms/Optimization_Matching/FaStGen.py +++ b/fairpyx/algorithms/Optimization_Matching/FaStGen.py @@ -6,10 +6,9 @@ """ from fairpyx import Instance, AllocationBuilder, ExplanationLogger -from FaSt import Demote +from fairpyx.algorithms.Optimization_Matching.FaSt import Demote # from bidict import bidict from copy import deepcopy -import random import logging diff --git a/matchingApp/app.py b/matchingApp/app.py new file mode 100644 index 0000000..42677fd --- /dev/null +++ b/matchingApp/app.py @@ -0,0 +1,88 @@ +import json +from flask import Flask, render_template, request +from fairpyx import Instance, AllocationBuilder +from fairpyx.algorithms.Optimization_Matching.FaSt import FaSt +from fairpyx.algorithms.Optimization_Matching.FaStGen import FaStGen +import logging + +# Configure logging +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +app = Flask(__name__) + +def make_s_list(n): + """Converts a number into a set of student identifiers.""" + return {f"s{i+1}" for i in range(int(n))} + +def make_c_list(m): + """Converts a number into a set of college identifiers.""" + return {f"c{i+1}" for i in range(int(m))} + +@app.route('/', methods=['GET', 'POST']) +def index(): + logger.debug("Entered index function") + error = None + result = None + + if request.method == 'POST': + try: + # Parse input data from the form + agents = int(request.form['k']) + items = int(request.form['m']) + list_students = make_s_list(agents) + list_colleges = make_c_list(items) + + logger.info("Received number of students (n): %s", agents) + logger.info("Received number of colleges (m): %s", items) + logger.info("List of students: %s", list_students) + logger.info("List of colleges: %s", list_colleges) + + algorithm = request.form['algorithm'] + logger.info("Selected algorithm: %s", algorithm) + + # Select and run the appropriate algorithm + if algorithm == 'FaSt': + valuation = request.form['valuation'] + logger.debug("Received valuation (JSON): %s", valuation) + valuation_dict = json.loads(valuation) + logger.debug("Converted valuation to dict: %s", valuation_dict) + + ins = Instance(agents=list_students, items=list_colleges, valuations=valuation_dict) + alloc = AllocationBuilder(instance=ins) + logger.debug("Instance and AllocationBuilder created") + + result = FaSt(alloc) + logger.debug("FaSt algorithm result: %s", result) + + elif algorithm == 'FaStGen': + agents_valuation = request.form['agentsValues'] + logger.debug("Received agentsValues (JSON): %s", agents_valuation) + agents_valuation_dict = json.loads(agents_valuation) + logger.debug("Converted agentsValues to dict: %s", agents_valuation_dict) + ins = Instance(agents=list_students, items=list_colleges, valuations=agents_valuation_dict) + alloc = AllocationBuilder(instance=ins) + logger.debug("Instance and AllocationBuilder created for FaStGen") + + items_valuation=request.form['itemsValues'] + logger.debug("Received itemsValues (JSON): %s", items_valuation) + items_valuation_dict=json.loads(items_valuation) + logger.debug("Converted itemsValues to dict: %s", items_valuation_dict) + result = FaStGen(alloc, items_valuations=items_valuation_dict) + logger.debug("FaStGen algorithm result: %s", result) + + else: + error = "Invalid algorithm selected." + logger.warning("Invalid algorithm selected by user") + + except json.JSONDecodeError as jde: + error = "Invalid JSON format. Please check your input." + logger.error("JSON decode error: %s", jde) + except Exception as e: + error = str(e) + logger.error("An error occurred: %s", error) + + return render_template('index.html', error=error, result=result) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/matchingApp/templates/index.html b/matchingApp/templates/index.html new file mode 100644 index 0000000..6ca0248 --- /dev/null +++ b/matchingApp/templates/index.html @@ -0,0 +1,116 @@ + + + + + Fairness and Stability in Many-to-One Matchings + + + + + +
+

Fairness and Stability in Many-to-One Matchings

+
+
+ {% if error %} +

{{ error }}

+ {% endif %} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + + + +
+ {% if result %} +
+

Result:

+
{{ result }}
+
+ {% endif %} +
+
+
+ + \ No newline at end of file From 13a155634943ad497780a880d1cd8bcc11021833 Mon Sep 17 00:00:00 2001 From: Hadar Bitan Date: Thu, 1 Aug 2024 17:56:55 +0300 Subject: [PATCH 056/111] updating the app --- matchingApp/app.py | 36 ++++++++++++++++++++------------ matchingApp/templates/index.html | 18 +++++++--------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/matchingApp/app.py b/matchingApp/app.py index 42677fd..7bdf7f2 100644 --- a/matchingApp/app.py +++ b/matchingApp/app.py @@ -7,9 +7,9 @@ # Configure logging logging.basicConfig(level=logging.DEBUG) -logger = logging.getLogger(__name__) +logger = logging.getLogger(_name_) -app = Flask(__name__) +app = Flask(_name_) def make_s_list(n): """Converts a number into a set of student identifiers.""" @@ -19,6 +19,13 @@ def make_c_list(m): """Converts a number into a set of college identifiers.""" return {f"c{i+1}" for i in range(int(m))} +def fastgen_make_s_list(n): + """Converts a number into a list of student identifiers.""" + return [f"s{i+1}" for i in range(int(n))] + +def fastgen_make_c_list(m): + """Converts a number into a list of college identifiers.""" + return [f"c{i+1}" for i in range(int(m))] @app.route('/', methods=['GET', 'POST']) def index(): logger.debug("Entered index function") @@ -56,19 +63,22 @@ def index(): logger.debug("FaSt algorithm result: %s", result) elif algorithm == 'FaStGen': - agents_valuation = request.form['agentsValues'] - logger.debug("Received agentsValues (JSON): %s", agents_valuation) - agents_valuation_dict = json.loads(agents_valuation) - logger.debug("Converted agentsValues to dict: %s", agents_valuation_dict) - ins = Instance(agents=list_students, items=list_colleges, valuations=agents_valuation_dict) + list_students=fastgen_make_s_list(agents) + list_colleges=fastgen_make_c_list(items) + u_valuation = request.form['uValues'] + logger.debug("Received uValues (JSON): %s", u_valuation) + u_valuation_dict = json.loads(u_valuation) + logger.debug("Converted uValues to dict: %s", u_valuation_dict) + ins = Instance(agents=list_students, items=list_colleges, valuations=u_valuation_dict) alloc = AllocationBuilder(instance=ins) + + v_valuation=request.form['valuation'] + logger.debug("Received vValues (JSON): %s", v_valuation) + v_valuation_dict=json.loads(v_valuation) + logger.debug("Converted vValues to dict: %s", v_valuation_dict) + result = FaStGen(alloc, items_valuations=v_valuation_dict) logger.debug("Instance and AllocationBuilder created for FaStGen") - items_valuation=request.form['itemsValues'] - logger.debug("Received itemsValues (JSON): %s", items_valuation) - items_valuation_dict=json.loads(items_valuation) - logger.debug("Converted itemsValues to dict: %s", items_valuation_dict) - result = FaStGen(alloc, items_valuations=items_valuation_dict) logger.debug("FaStGen algorithm result: %s", result) else: @@ -84,5 +94,5 @@ def index(): return render_template('index.html', error=error, result=result) -if __name__ == '__main__': +if __name__ == "__main__": app.run(debug=True) \ No newline at end of file diff --git a/matchingApp/templates/index.html b/matchingApp/templates/index.html index 6ca0248..e608c83 100644 --- a/matchingApp/templates/index.html +++ b/matchingApp/templates/index.html @@ -49,14 +49,14 @@