diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index 44d6cab4..27f6d6bb 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 +CLUSTERING_WEIGHT = 0.001 + +# Når på dagen man helst vil ha intervjuene rundt +CLUSTERING_TIME_BASELINE = time(12, 00) +MAX_SCALE_CLUSTERING_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 CLUSTERING_TIME_BASELINE + clustering_objectives = [] + + for name, variable in m.items(): + applicant, committee, interval = name + 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(), + CLUSTERING_TIME_BASELINE) / MAX_SCALE_CLUSTERING_TIME + + 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 CLUSTERING_TIME_BASELINE + model.objective = mip.maximize( + 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 new file mode 100644 index 00000000..b248ae3e --- /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_clustering(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