From 3a83af95d66ea30dfef61432159496d0e6ba346b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 18 Aug 2024 13:45:55 +0000 Subject: [PATCH 1/2] feat: add cluttering --- algorithm/src/mip_matching/match_meetings.py | 36 ++++++++++++++--- algorithm/src/mip_matching/utils.py | 41 ++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 algorithm/src/mip_matching/utils.py diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index 44d6cab4..8af9371d 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -5,13 +5,22 @@ from mip_matching.Applicant import Applicant import mip -from datetime import timedelta +from datetime import timedelta, time from itertools import combinations +from mip_matching.utils import subtract_time + # Hvor stort buffer man ønsker å ha mellom intervjuene APPLICANT_BUFFER_LENGTH = timedelta(minutes=15) +# Et mål på hvor viktig det er at intervjuer er i nærheten av hverandre +CLUTTERING_WEIGHT = 0.001 + +# Når på dagen man helst vil ha intervjuene rundt +CLUTTERING_TIME_BASELINE = time(12, 00) +MAX_SCALE_CLUTTERING_TIME = timedelta(seconds=43200) # TODO: Rename variable + class MeetingMatch(TypedDict): """Type definition of a meeting match object""" @@ -25,7 +34,7 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me """Matches meetings and returns a MeetingMatch-object""" model = mip.Model(sense=mip.MAXIMIZE) - m = {} + m: dict[tuple[Applicant, Committee, TimeInterval], mip.Var] = {} # Lager alle maksimeringsvariabler for applicant in applicants: @@ -60,10 +69,27 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me for interview_a, interview_b in combinations(potential_interviews, r=2): if interview_a[1].intersects(interview_b[1]) or interview_a[1].is_within_distance(interview_b[1], APPLICANT_BUFFER_LENGTH): model += m[(applicant, *interview_a)] + \ - m[(applicant, *interview_b)] <= 1 + m[(applicant, *interview_b)] <= 1 # type: ignore - # Setter mål til å være maksimering av antall møter - model.objective = mip.maximize(mip.xsum(m.values())) + # Legger til sekundærmål om at man ønsker å sentrere intervjuer rundt CLUTTERING_TIME_BASELINE + cluttering_objectives = [] + + for name, variable in m.items(): + applicant, committee, interval = name + if interval.start.time() < CLUTTERING_TIME_BASELINE: + relative_distance_from_baseline = subtract_time(CLUTTERING_TIME_BASELINE, + interval.end.time()) / MAX_SCALE_CLUTTERING_TIME + else: + relative_distance_from_baseline = subtract_time(interval.start.time(), + CLUTTERING_TIME_BASELINE) / MAX_SCALE_CLUTTERING_TIME + + cluttering_objectives.append( + CLUTTERING_WEIGHT * relative_distance_from_baseline * variable) # type: ignore + + # Setter mål til å være maksimering av antall møter + # med sekundærmål om å samle intervjuene rundt CLUTTERING_TIME_BASELINE + model.objective = mip.maximize( + mip.xsum(m.values()) + mip.xsum(cluttering_objectives)) # Kjør optimeringen solver_status = model.optimize() diff --git a/algorithm/src/mip_matching/utils.py b/algorithm/src/mip_matching/utils.py new file mode 100644 index 00000000..8f9dd758 --- /dev/null +++ b/algorithm/src/mip_matching/utils.py @@ -0,0 +1,41 @@ +from mip_matching.Applicant import Applicant +from mip_matching.Committee import Committee +from mip_matching.TimeInterval import TimeInterval + +from datetime import time, date, datetime, timedelta + + +def group_by_committee(meetings: list[tuple[Applicant, Committee, TimeInterval]]) -> dict[Committee, list[tuple[Applicant, Committee, TimeInterval]]]: + result = {} + + for applicant, committee, interval in meetings: + if committee not in result: + result[committee] = [] + + result[committee].append((applicant, committee, interval)) + + return result + + +def measure_cluttering(meetings: list[tuple[Applicant, Committee, TimeInterval]]) -> int: + grouped_meetings = group_by_committee(meetings) + + holes = 0 + + for _, committee_meetings in grouped_meetings.items(): + committee_meetings.sort(key=lambda meeting: meeting[2].end) + + previous_interval: TimeInterval = committee_meetings[0][2] + for _, _, interval in committee_meetings[1:]: + if not previous_interval.is_within_distance(interval, timedelta(minutes=1)): + holes += 1 + previous_interval = interval + + return holes + + +def subtract_time(minuend: time, subtrahend: time) -> timedelta: + minuend_date = datetime.combine(date.min, minuend) + subtrahend_date = datetime.combine(date.min, subtrahend) + + return minuend_date - subtrahend_date From 613006b2a930e9fbca7e91d1cec46925ba9914d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 18 Aug 2024 13:51:26 +0000 Subject: [PATCH 2/2] cluttering -> clustering --- algorithm/src/mip_matching/match_meetings.py | 26 ++++++++++---------- algorithm/src/mip_matching/utils.py | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index 8af9371d..27f6d6bb 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -15,11 +15,11 @@ APPLICANT_BUFFER_LENGTH = timedelta(minutes=15) # Et mål på hvor viktig det er at intervjuer er i nærheten av hverandre -CLUTTERING_WEIGHT = 0.001 +CLUSTERING_WEIGHT = 0.001 # Når på dagen man helst vil ha intervjuene rundt -CLUTTERING_TIME_BASELINE = time(12, 00) -MAX_SCALE_CLUTTERING_TIME = timedelta(seconds=43200) # TODO: Rename variable +CLUSTERING_TIME_BASELINE = time(12, 00) +MAX_SCALE_CLUSTERING_TIME = timedelta(seconds=43200) # TODO: Rename variable class MeetingMatch(TypedDict): @@ -71,25 +71,25 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me model += m[(applicant, *interview_a)] + \ m[(applicant, *interview_b)] <= 1 # type: ignore - # Legger til sekundærmål om at man ønsker å sentrere intervjuer rundt CLUTTERING_TIME_BASELINE - cluttering_objectives = [] + # Legger til sekundærmål om at man ønsker å sentrere intervjuer rundt CLUSTERING_TIME_BASELINE + clustering_objectives = [] for name, variable in m.items(): applicant, committee, interval = name - if interval.start.time() < CLUTTERING_TIME_BASELINE: - relative_distance_from_baseline = subtract_time(CLUTTERING_TIME_BASELINE, - interval.end.time()) / MAX_SCALE_CLUTTERING_TIME + if interval.start.time() < CLUSTERING_TIME_BASELINE: + relative_distance_from_baseline = subtract_time(CLUSTERING_TIME_BASELINE, + interval.end.time()) / MAX_SCALE_CLUSTERING_TIME else: relative_distance_from_baseline = subtract_time(interval.start.time(), - CLUTTERING_TIME_BASELINE) / MAX_SCALE_CLUTTERING_TIME + CLUSTERING_TIME_BASELINE) / MAX_SCALE_CLUSTERING_TIME - cluttering_objectives.append( - CLUTTERING_WEIGHT * relative_distance_from_baseline * variable) # type: ignore + clustering_objectives.append( + CLUSTERING_WEIGHT * relative_distance_from_baseline * variable) # type: ignore # Setter mål til å være maksimering av antall møter - # med sekundærmål om å samle intervjuene rundt CLUTTERING_TIME_BASELINE + # med sekundærmål om å samle intervjuene rundt CLUSTERING_TIME_BASELINE model.objective = mip.maximize( - mip.xsum(m.values()) + mip.xsum(cluttering_objectives)) + mip.xsum(m.values()) + mip.xsum(clustering_objectives)) # Kjør optimeringen solver_status = model.optimize() diff --git a/algorithm/src/mip_matching/utils.py b/algorithm/src/mip_matching/utils.py index 8f9dd758..b248ae3e 100644 --- a/algorithm/src/mip_matching/utils.py +++ b/algorithm/src/mip_matching/utils.py @@ -17,7 +17,7 @@ def group_by_committee(meetings: list[tuple[Applicant, Committee, TimeInterval]] return result -def measure_cluttering(meetings: list[tuple[Applicant, Committee, TimeInterval]]) -> int: +def measure_clustering(meetings: list[tuple[Applicant, Committee, TimeInterval]]) -> int: grouped_meetings = group_by_committee(meetings) holes = 0