Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fikser tildeling av samme rom ved parallelle intervjuer #318

Merged
merged 6 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions algorithm/bridge/fetch_applicants_and_committees.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ def main():
application_end = datetime.fromisoformat(period["applicationPeriod"]["end"].replace("Z", "+00:00"))

now = datetime.now(timezone.utc)


#or period["name"] == "Juli Opptak"
if (application_end < now and period["hasSentInterviewTimes"] == False):
Expand Down Expand Up @@ -115,11 +114,13 @@ def format_match_results(match_results: MeetingMatch, applicants: List[dict], pe
time_interval = result[2]
start = time_interval.start.isoformat()
end = time_interval.end.isoformat()
room = result[3]

transformed_results[applicant_id]["interviews"].append({
"start": start,
"end": end,
"committeeName": committee.name
"committeeName": committee.name,
"room": room
})

return list(transformed_results.values())
Expand Down Expand Up @@ -156,8 +157,8 @@ def create_committee_objects(committee_data: List[dict]) -> set[Committee]:
start=datetime.fromisoformat(interval_data['start'].replace("Z", "+00:00")),
end=datetime.fromisoformat(interval_data['end'].replace("Z", "+00:00"))
)
capacity = interval_data.get('capacity', 1)
committee.add_interval(interval, capacity)
room = interval_data["room"]
committee.add_interview_slot(interval, room)
committees.add(committee)
return committees

Expand Down
41 changes: 20 additions & 21 deletions algorithm/src/mip_matching/Committee.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from typing import Iterator

from mip_matching.types import Room


class Committee:
"""
Expand All @@ -21,39 +23,40 @@ class Committee:
"""

def __init__(self, name: str, interview_length: timedelta = timedelta(minutes=15)):
self.capacities: dict[TimeInterval, int] = dict()
self.interview_slots: dict[TimeInterval, set[Room]] = dict()
self.interview_length: timedelta = interview_length
self.applicants: set[Applicant] = set()
self.name = name

def add_interval(self, interval: TimeInterval, capacity: int = 1) -> None:
"""Legger til et nytt intervall med gitt kapasitet hvis intervallet
ikke allerede har en kapasitet for denne komitéen.
def add_interview_slot(self, interval: TimeInterval, room: Room) -> None:
"""Legger til et nytt intervall med gitt rom.
Når intervaller legges til deles det automatisk opp i
intervaller med lik lengde som intervjulengder."""
minimal_intervals = TimeInterval.divide_interval(
interval=interval, length=self.interview_length)
for interval in minimal_intervals:
if interval not in self.capacities:
self.capacities[interval] = capacity
else:
self.capacities[interval] += capacity

def add_intervals_with_capacities(self, intervals_with_capacities: dict[TimeInterval, int]):
"""Legger til flere tidsintervaller samtidig."""
for interval, capacity in intervals_with_capacities.items():
self.add_interval(interval, capacity)
if interval not in self.interview_slots:
self.interview_slots[interval] = set()
self.interview_slots[interval].add(room)

def get_intervals_and_capacities(self) -> Iterator[tuple[TimeInterval, int]]:
"""Generator som returnerer interval-kapasitet-par."""
for interval, capacity in self.capacities.items():
yield interval, capacity
for interval, rooms in self.interview_slots.items():
yield interval, len(rooms)

def get_capacity(self, interval: TimeInterval) -> int:
"""Returnerer komitéens kapasitet ved et gitt interval (ferdiginndelt etter lengde)"""
return len(self.interview_slots[interval])

def get_intervals(self) -> Iterator[TimeInterval]:
"""Generator som returnerer kun intervallene"""
for interval in self.capacities.keys():
for interval in self.interview_slots.keys():
yield interval

def get_rooms(self, interval: TimeInterval) -> Iterator[Room]:
for room in self.interview_slots[interval]:
yield room

def _add_applicant(self, applicant: Applicant):
"""Metode brukt for å holde toveis-assosiasjonen."""
self.applicants.add(applicant)
Expand All @@ -62,10 +65,6 @@ def get_applicants(self) -> Iterator[Applicant]:
for applicant in self.applicants:
yield applicant

def get_capacity(self, interval: TimeInterval) -> int:
"""Returnerer komitéens kapasitet ved et gitt interval (ferdiginndelt etter lengde)"""
return self.capacities[interval]

def get_applicant_count(self) -> int:
return len(self.applicants)

Expand All @@ -77,4 +76,4 @@ def __repr__(self):


if __name__ == "__main__":
print("running")
print("running")
2 changes: 1 addition & 1 deletion algorithm/src/mip_matching/TimeInterval.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def divide(self, length: timedelta) -> list[TimeInterval]:
return TimeInterval.divide_interval(self, length)

def is_within_distance(self, other: TimeInterval, distance: timedelta) -> bool:
return (self.end <= other.start and self.end + distance > other.start) or (other.end <= self.start and other.end + distance > self.start)
return (self.end <= other.start < self.end + distance) or (other.end <= self.start < other.end + distance)

@staticmethod
def divide_interval(interval: TimeInterval, length: timedelta) -> list[TimeInterval]:
Expand Down
45 changes: 19 additions & 26 deletions algorithm/src/mip_matching/match_meetings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from datetime import timedelta, time
from itertools import combinations

from mip_matching.types import Matching, MeetingMatch
from mip_matching.utils import subtract_time


Expand All @@ -22,60 +23,53 @@
MAX_SCALE_CLUSTERING_TIME = timedelta(seconds=43200) # TODO: Rename variable


class MeetingMatch(TypedDict):
"""Type definition of a meeting match object"""
solver_status: mip.OptimizationStatus
matched_meetings: int
total_wanted_meetings: int
matchings: list[tuple[Applicant, Committee, TimeInterval]]


def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> MeetingMatch:
"""Matches meetings and returns a MeetingMatch-object"""
model = mip.Model(sense=mip.MAXIMIZE)

m: dict[tuple[Applicant, Committee, TimeInterval], mip.Var] = {}
m: dict[Matching, mip.Var] = {}

# Lager alle maksimeringsvariabler
for applicant in applicants:
for committee in applicant.get_committees():
for interval in applicant.get_fitting_committee_slots(committee):
m[(applicant, committee, interval)] = model.add_var(
var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval})")
for room in committee.get_rooms(interval):
m[(applicant, committee, interval, room)] = model.add_var(
var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval}, {room})")

# Legger inn begrensninger for at en komité kun kan ha antall møter i et slot lik kapasiteten.
for committee in committees:
for interval, capacity in committee.get_intervals_and_capacities():
model += mip.xsum(m[(applicant, committee, interval)]
model += mip.xsum(m[(applicant, committee, interval, room)]
for applicant in committee.get_applicants()
for room in committee.get_rooms(interval)
if (applicant, committee, interval, room) in m
# type: ignore
if (applicant, committee, interval) in m) <= capacity
) <= capacity

# Legger inn begrensninger for at en person kun har ett intervju med hver komité
for applicant in applicants:
for committee in applicant.get_committees():
model += mip.xsum(m[(applicant, committee, interval)]
model += mip.xsum(m[(applicant, committee, interval, room)]
for interval in applicant.get_fitting_committee_slots(committee)
for room in committee.get_rooms(interval)
# type: ignore
for interval in applicant.get_fitting_committee_slots(committee)) <= 1
) <= 1

# Legger inn begrensninger for at en søker ikke kan ha overlappende intervjutider
# og minst har et buffer mellom hvert intervju som angitt
for applicant in applicants:
potential_interviews: set[tuple[Committee, TimeInterval]] = set()
for applicant_candidate, committee, interval in m:
if applicant == applicant_candidate:
potential_interviews.add((committee, interval))
potential_interviews = set(slot for slot in m.keys() if slot[0] == applicant)

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 # type: ignore
if interview_a[2].intersects(interview_b[2]) or interview_a[2].is_within_distance(interview_b[2], APPLICANT_BUFFER_LENGTH):
model += m[interview_a] + m[interview_b] <= 1 # type: ignore

# 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
applicant, committee, interval, room = name
if interval.start.time() < CLUSTERING_TIME_BASELINE:
relative_distance_from_baseline = subtract_time(CLUSTERING_TIME_BASELINE,
interval.end.time()) / MAX_SCALE_CLUSTERING_TIME
Expand All @@ -86,8 +80,8 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me
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
# 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))

Expand All @@ -101,7 +95,6 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me
if variable.x:
antall_matchede_møter += 1
matchings.append(name)
print(f"{name}")

antall_ønskede_møter = sum(
len(applicant.get_committees()) for applicant in applicants)
Expand Down
22 changes: 22 additions & 0 deletions algorithm/src/mip_matching/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
Typealiaser
"""

from typing import TypedDict, TYPE_CHECKING
import mip
if TYPE_CHECKING:
# Unngår cyclic import
from mip_matching.Applicant import Applicant
from mip_matching.Committee import Committee
from mip_matching.TimeInterval import TimeInterval


type Room = str
type Matching = tuple[Applicant, Committee, TimeInterval, Room]

class MeetingMatch(TypedDict):
"""Type definition of a meeting match object"""
solver_status: mip.OptimizationStatus
matched_meetings: int
total_wanted_meetings: int
matchings: list[Matching]
29 changes: 11 additions & 18 deletions algorithm/tests/CommitteeTest.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
from __future__ import annotations
from datetime import datetime, timedelta
import unittest
from mip_matching.TimeInterval import TimeInterval
from mip_matching.Committee import Committee
# from __future__ import annotations
# from datetime import datetime, timedelta
# import unittest
# from mip_matching.TimeInterval import TimeInterval
# from mip_matching.Committee import Committee


class ApplicantTest(unittest.TestCase):
def setUp(self) -> None:
self.committee = Committee(
"TestKom", interview_length=timedelta(minutes=30))
self.committee.add_intervals_with_capacities({
TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 30)): 1,
TimeInterval(datetime(2024, 8, 24, 8, 30), datetime(2024, 8, 24, 9, 30)): 1
})
# class ApplicantTest(unittest.TestCase):
# def setUp(self) -> None:
# self.committee = Committee(
# "TestKom", interview_length=timedelta(minutes=30))


def test_capacity_stacking(self) -> None:
self.assertEqual(1, self.committee.get_capacity(
TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 30))))
self.assertEqual(2, self.committee.get_capacity(
TimeInterval(datetime(2024, 8, 24, 8, 30), datetime(2024, 8, 24, 9, 0))))

Loading