Skip to content

Commit

Permalink
feat: add cluttering
Browse files Browse the repository at this point in the history
  • Loading branch information
jorgengaldal committed Aug 18, 2024
1 parent 98ca2bf commit 3a83af9
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 5 deletions.
36 changes: 31 additions & 5 deletions algorithm/src/mip_matching/match_meetings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand All @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
41 changes: 41 additions & 0 deletions algorithm/src/mip_matching/utils.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 3a83af9

Please sign in to comment.