diff --git a/.github/workflows/algorithm.yml b/.github/workflows/algorithm.yml deleted file mode 100644 index ce22a1e0..00000000 --- a/.github/workflows/algorithm.yml +++ /dev/null @@ -1,33 +0,0 @@ -on: - push: - paths: - - "algorithm/**" - pull_request: - paths: - - "algorithm/**" - workflow_dispatch: - -jobs: - test: - runs-on: "ubuntu-latest" - - strategy: - matrix: - python-version: ["3.12"] - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Install mip_matching - run: | - cd algorithm - python -m pip install -e . - - - name: Run tests - run: | - cd algorithm - python -m unittest discover -p "*test.py" - diff --git a/.github/workflows/match_interviews.yml b/.github/workflows/match_interviews.yml deleted file mode 100644 index 357d02f9..00000000 --- a/.github/workflows/match_interviews.yml +++ /dev/null @@ -1,26 +0,0 @@ -on: - workflow_dispatch: - -jobs: - run: - runs-on: "ubuntu-latest" - - strategy: - matrix: - python-version: ["3.12"] - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Install mip_matching - run: | - cd algorithm - python -m pip install -e . - - - name: Run tests - run: | - cd algorithm - python bridge/fetch_applicants_and_committees.py diff --git a/README.md b/README.md index e56ed489..e9b75521 100644 --- a/README.md +++ b/README.md @@ -51,3 +51,20 @@ yarn dev ``` Open with your browser to see the result. + +### Running the matching algorithm + +Setup Python virtual environment: + +```bash +cd algorithm +python -m venv ".venv" +.\.venv\Scripts\activate +pip install -r requirements.txt +``` + +Then, run the file [fetch_applicants_and_committees.py](algorithm/fetch_applicants_and_committees.py): + +```bash +python fetch_applicants_and_committees.py +``` diff --git a/algorithm/bridge/.env.template b/algorithm/.env.template similarity index 100% rename from algorithm/bridge/.env.template rename to algorithm/.env.template diff --git a/algorithm/README.md b/algorithm/README.md deleted file mode 100644 index 0bb231d9..00000000 --- a/algorithm/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Algoritme - -**mip_matching** er en pakke for å tildele intervjutider til søkere basert på ledige tider for søkere og komitéer. - -Algoritmen baserer seg på MIP-programmering (Mixed Integer Linear Programming). Se [Modellering.md](./src/Modellering.md) for detaljer. - -## Setup Python Venv - -```bash -cd algorithm -python -m venv ".venv" -.\.venv\Scripts\activate -pip install -e . -pip install -r requirements.txt -pip install pymongo[srv] -``` diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/fetch_applicants_and_committees.py similarity index 70% rename from algorithm/bridge/fetch_applicants_and_committees.py rename to algorithm/fetch_applicants_and_committees.py index 98b2ce76..02798007 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/fetch_applicants_and_committees.py @@ -10,112 +10,125 @@ from mip_matching.Applicant import Applicant from mip_matching.match_meetings import match_meetings, MeetingMatch + def main(): + print("Starting matching") periods = fetch_periods() - + for period in periods: periodId = str(period["_id"]) - application_end = datetime.fromisoformat(period["applicationPeriod"]["end"].replace("Z", "+00:00")) - + 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): + if (application_end < now and period["hasSentInterviewTimes"] == False): applicants = fetch_applicants(periodId) committee_times = fetch_committee_times(periodId) - + committee_objects = create_committee_objects(committee_times) - - all_committees = {committee.name: committee for committee in committee_objects} - - applicant_objects = create_applicant_objects(applicants, all_committees) - - print(applicant_objects) - print(committee_objects) - + + all_committees = { + committee.name: committee for committee in committee_objects} + + applicant_objects = create_applicant_objects( + applicants, all_committees) + match_result = match_meetings(applicant_objects, committee_objects) - + print( + f"Matching finished with status {match_result["solver_status"]}") + print( + f"Matched {match_result["matched_meetings"]}/{match_result["total_wanted_meetings"]} ({match_result["matched_meetings"]/match_result["total_wanted_meetings"]:.2f}) meetings") + send_to_db(match_result, applicants, periodId) + print("Meetings sent to database") return match_result - + + def send_to_db(match_result: MeetingMatch, applicants: List[dict], periodId): load_dotenv() - formatted_results = format_match_results(match_result, applicants, periodId) + formatted_results = format_match_results( + match_result, applicants, periodId) print("Sending to db") print(formatted_results) - + mongo_uri = os.getenv("MONGODB_URI") db_name = os.getenv("DB_NAME") client = MongoClient(mongo_uri, tlsCAFile=certifi.where()) - - db = client[db_name] # type: ignore - + + db = client[db_name] # type: ignore + collection = db["interviews"] - + collection.insert_many(formatted_results) - + client.close() + def connect_to_db(collection_name): load_dotenv() - + mongo_uri = os.getenv("MONGODB_URI") db_name = os.getenv("DB_NAME") client = MongoClient(mongo_uri, tlsCAFile=certifi.where()) - - db = client[db_name] # type: ignore - + + db = client[db_name] # type: ignore + collection = db[collection_name] - + return collection, client + def fetch_periods(): collection, client = connect_to_db("periods") - + periods = list(collection.find()) - + client.close() - + return periods + def fetch_applicants(periodId): collection, client = connect_to_db("applications") - + applicants = list(collection.find({"periodId": periodId})) - + client.close() - + return applicants + def fetch_committee_times(periodId): collection, client = connect_to_db("committees") - + committee_times = list(collection.find({"periodId": periodId})) - + client.close() - + return committee_times + def format_match_results(match_results: MeetingMatch, applicants: List[dict], periodId) -> List[Dict]: transformed_results = {} - + for result in match_results['matchings']: applicant_id = str(result[0]) - + if applicant_id not in transformed_results: transformed_results[applicant_id] = { "periodId": periodId, "applicantId": applicant_id, "interviews": [] } - + committee = result[1] 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, @@ -125,37 +138,46 @@ def format_match_results(match_results: MeetingMatch, applicants: List[dict], pe return list(transformed_results.values()) + def create_applicant_objects(applicants_data: List[dict], all_committees: dict[str, Committee]) -> set[Applicant]: applicants = set() for data in applicants_data: applicant = Applicant(name=str(data['_id'])) - + optional_committee_names = data.get('optionalCommittees', []) - optional_committees = {all_committees[name] for name in optional_committee_names if name in all_committees} + optional_committees = { + all_committees[name] for name in optional_committee_names if name in all_committees} applicant.add_committees(optional_committees) - + preferences = data.get('preferences', {}) - preference_committees = {all_committees[committee_name] for committee_name in preferences.values() if committee_name in all_committees} + preference_committees = {all_committees[committee_name] for committee_name in preferences.values( + ) if committee_name in all_committees} applicant.add_committees(preference_committees) for interval_data in data['selectedTimes']: interval = TimeInterval( - start=datetime.fromisoformat(interval_data['start'].replace("Z", "+00:00")), - end=datetime.fromisoformat(interval_data['end'].replace("Z", "+00:00")) + start=datetime.fromisoformat( + interval_data['start'].replace("Z", "+00:00")), + end=datetime.fromisoformat( + interval_data['end'].replace("Z", "+00:00")) ) applicant.add_interval(interval) - + applicants.add(applicant) return applicants + def create_committee_objects(committee_data: List[dict]) -> set[Committee]: committees = set() for data in committee_data: - committee = Committee(name=data['committee'], interview_length=timedelta(minutes=int(data["timeslot"]))) + committee = Committee(name=data['committee'], interview_length=timedelta( + minutes=int(data["timeslot"]))) for interval_data in data['availabletimes']: interval = TimeInterval( - start=datetime.fromisoformat(interval_data['start'].replace("Z", "+00:00")), - end=datetime.fromisoformat(interval_data['end'].replace("Z", "+00:00")) + start=datetime.fromisoformat( + interval_data['start'].replace("Z", "+00:00")), + end=datetime.fromisoformat( + interval_data['end'].replace("Z", "+00:00")) ) room = interval_data["room"] committee.add_interview_slot(interval, room) diff --git a/algorithm/pyproject.toml b/algorithm/pyproject.toml deleted file mode 100644 index e22dea2b..00000000 --- a/algorithm/pyproject.toml +++ /dev/null @@ -1,20 +0,0 @@ -[project] -name = "mip_matching" -version = "0.0.1" -description = "Project for matching meetings using Mixed Integer Linear Programming" -dependencies = [ - "cffi==1.15.0", - "Faker==24.11.0", - "mip==1.14.2", - "pycparser==2.21", - "python-dateutil==2.9.0.post0", - "six==1.16.0", -] - - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/mip_matching"] \ No newline at end of file diff --git a/algorithm/requirements.txt b/algorithm/requirements.txt index 6b8206b4..48727460 100644 Binary files a/algorithm/requirements.txt and b/algorithm/requirements.txt differ diff --git a/algorithm/src/Modellering.md b/algorithm/src/Modellering.md deleted file mode 100644 index 9f1ab19b..00000000 --- a/algorithm/src/Modellering.md +++ /dev/null @@ -1,62 +0,0 @@ -# Modellering av møtetildelingsproblem gjennom Mixed Integer Linear Programming - -## Variabler - -`p` - -- Person - -`k` - -- Komité - -`t` - -- Timeslot - -`m(p, k, t)` - -- Binær variabel -- Person `p` har møte med komité `k` i timeslot `t` - -## Hjelpevariabler - -`c(p, t)` - -- Binære variabler -- Tidspunkt `t` passer for person `p` - -`c(k, t)` - -- Heltallsvariabel -- Kapasitet for komité `k` på tidspunkt `t` (hvor mange intervju de kan ha på det gitte tidspunktet) - -## Begrensninger - -For alle `p`: - -- `m(p, k, t_1) + m(p, k, t_2) < 2` for alle gyldige `k, t_1` og `k, t_2`, hvor t_1 og t_2 overlapper eller er innenfor et gitt buffer-intervall. -- `m(p, k, t) <= 1` dersom - - `p` har søkt på komité `k` - - `c(p, t) => 1` - - `c(k, t) => 1` -- `m(p, k, t) <= 0` ellers - -For alle `k`: - -- `sum(m(p, k, t)) <= c(k, t)` for alle personer `p` og tidspunkt `t` - -## Mål - -Maksimere `sum(m(p, k, t))` for alle `p`, `k` og `t`. Altså: Maksimere antall intervjuer som tildeles. - -### Sekundærmål - -- La intervjuene klumpe seg rundt klokken 12 og dermed også minske hvor mange hull komitéene får i sin intervjuplan. - -## Nyttige ressurser - -- https://python-mip.readthedocs.io/en/latest/quickstart.html -- https://towardsdatascience.com/mixed-integer-linear-programming-1-bc0ef201ee87 -- https://towardsdatascience.com/mixed-integer-linear-programming-formal-definition-and-solution-space-6b3286d54892 -- https://www.gurobi.com/resources/mixed-integer-programming-mip-a-primer-on-the-basics/ diff --git a/algorithm/src/__init__.py b/algorithm/src/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/algorithm/src/mip_matching/Applicant.py b/algorithm/src/mip_matching/Applicant.py deleted file mode 100644 index 6172e5fb..00000000 --- a/algorithm/src/mip_matching/Applicant.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING -if TYPE_CHECKING: - # Unngår cyclic import - from mip_matching.Committee import Committee - from mip_matching.TimeInterval import TimeInterval - -import itertools - - -class Applicant: - """ - Klasse som holder styr over en søker, med data om hvilke - komitéer hen har søkt på, og når søkeren kan ha intervjuer. - """ - - def __init__(self, name: str): - self.committees: list[Committee] = [] - self.slots: set[TimeInterval] = set() - self.name = name - - def add_committee(self, committee: Committee) -> None: - self.committees.append(committee) - committee._add_applicant(self) - - def add_committees(self, committees: set[Committee]) -> None: - for committee in committees: - self.add_committee(committee) - - def add_interval(self, interval: TimeInterval) -> None: - """ - Slår også sammen overlappende intervaller. - - Maksimalt to typer slots som må merges: - - Alle som inngår i dette intervallet - - De to som grenser møtes i grensene. - Merger først med førstnevnte, fordi etter det vil det kun være (opptil) to som kan merges (i sistnevnte kategori) - """ - for other in interval.get_contained_slots(list(self.slots)): - self.slots.remove(other) - interval = interval.union(other) - - slots_to_merge = set() - for _ in range(2): - for other in self.slots: - if interval.is_mergable(other): - # Må legge til en liste midlertidig for å unngå concurrency errors. - slots_to_merge.add(other) - - for slot in slots_to_merge: - self.slots.remove(slot) - interval = interval.union(slot) - - self.slots.add(interval) - - def add_intervals(self, intervals: set[TimeInterval]) -> None: - for interval in intervals: - self.add_interval(interval) - - def get_intervals(self) -> set[TimeInterval]: - return self.slots.copy() - - def get_fitting_committee_slots(self, committee: Committee) -> set[TimeInterval]: - """ - Returnerer alle tidsintervallene i *komiteen* - som er inneholdt i et av *self* sine intervaller. - """ - - result: set[TimeInterval] = set() - - for applicant_interval, committee_interval in itertools.product(self.slots, committee.get_intervals()): - if applicant_interval.contains(committee_interval): - result.add(committee_interval) - - return result - - def get_committees(self) -> set[Committee]: - """Returnerer en grunn kopi av komitéene.""" - return set(self.committees) - - def __str__(self) -> str: - return self.name - - def __repr__(self) -> str: - return str(self) \ No newline at end of file diff --git a/algorithm/src/mip_matching/Committee.py b/algorithm/src/mip_matching/Committee.py deleted file mode 100644 index 29095116..00000000 --- a/algorithm/src/mip_matching/Committee.py +++ /dev/null @@ -1,79 +0,0 @@ -from __future__ import annotations -from datetime import timedelta - -from mip_matching.Applicant import Applicant -from mip_matching.TimeInterval import TimeInterval - -from typing import Iterator - -from mip_matching.types import Room - - -class Committee: - """ - En klasse som representerer en komité - og holder oversikt over når komitéene kan ha - møte og hvor lange intervjuene er. - - NOTE: - - Kan foreløpig kun aksessere ved hjelp av det faktiske - intervallet slik det er inndelt basert på intervju-lengde, - men er usikker på om vi kanskje burde fått med annen måte å - aksessere på. - """ - - def __init__(self, name: str, interview_length: timedelta = timedelta(minutes=15)): - self.interview_slots: dict[TimeInterval, set[Room]] = dict() - self.interview_length: timedelta = interview_length - self.applicants: set[Applicant] = set() - self.name = name - - 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.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, 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.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) - - def get_applicants(self) -> Iterator[Applicant]: - for applicant in self.applicants: - yield applicant - - def get_applicant_count(self) -> int: - return len(self.applicants) - - def __str__(self): - return f"{self.name}" - - def __repr__(self): - return str(self) - - -if __name__ == "__main__": - print("running") diff --git a/algorithm/src/mip_matching/TimeInterval.py b/algorithm/src/mip_matching/TimeInterval.py deleted file mode 100644 index 83064dad..00000000 --- a/algorithm/src/mip_matching/TimeInterval.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import annotations -from dataclasses import dataclass -from datetime import datetime -from datetime import timedelta -from typing import Any - - -@dataclass(frozen=True) -class TimeInterval: - """ - Definerer et tidsintervall fra og med start til og uten end. - """ - start: datetime - end: datetime - - def __post_init__(self) -> None: - """Metode som sikrer at start og end er av type datetime og at de er i kronologisk rekkefølge.""" - if not (isinstance(self.start, datetime) and isinstance(self.end, datetime)): - raise TypeError("Start and end must be of type datetime.") - - if not (self.start <= self.end): - raise ValueError("Start must be before end") - - def intersects(self, other: TimeInterval) -> bool: - """Returnerer true om to tidsintervaller er helt eller delvis overlappende.""" - return other.start <= self.start < other.end or self.start <= other.start < self.end - - def is_tangent_to(self, other: TimeInterval) -> bool: - """Returnerer true om self tangerer other (er helt inntil, men ikke overlappende).""" - return not self.intersects(other) and (other.start == self.end or self.start == other.end) - - def union(self, other: TimeInterval) -> TimeInterval: - """Returnerer union av tidsintervall dersom de to intervallene har overlapp eller er inntil hverandre""" - - if not self.is_mergable(other): - raise ValueError("Cannot have union with gaps between") - - start = min(self.start, other.start) - end = max(self.end, other.end) - return TimeInterval(start, end) - - def contains(self, other: TimeInterval) -> bool: - """Returnerer true om other inngår helt i self.""" - return self.start <= other.start and other.end <= self.end - - def is_mergable(self, other: TimeInterval) -> bool: - return self.intersects(other) or self.is_tangent_to(other) - - def intersection(self, other: TimeInterval) -> TimeInterval | None: - """Returnerer et snitt av to tidsintervaller.""" - if not self.intersects(other): - # Snittet er tomt grunnet ingen overlapp - return None - - start = max(self.start, other.start) - end = min(self.end, other.end) - return TimeInterval(start, end) - - def get_contained_slots(self, slots: list[TimeInterval]): - """Returnerer en delmengde av de intervaller i listen - "slots", som inngår helt i dette tidsintervallet.""" - return set(slot for slot in slots if self.contains(slot)) - - 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 < self.end + distance) or (other.end <= self.start < other.end + distance) - - @staticmethod - def divide_interval(interval: TimeInterval, length: timedelta) -> list[TimeInterval]: - """ - - Deler opp et intervall i mindre intervaller av lengde *length*. - - Note: - - Det antas at intervallet kan deles opp i hele deler av lengde *length*. - Overskytende tid vil bli ignorert. - """ - result = [] - global_start = interval.start - local_start = global_start - local_end = local_start + length - - while local_end <= interval.end: - result.append(TimeInterval(local_start, local_end)) - local_start = local_end - local_end += length - - return result diff --git a/algorithm/src/mip_matching/__init__.py b/algorithm/src/mip_matching/__init__.py deleted file mode 100644 index c36a779a..00000000 --- a/algorithm/src/mip_matching/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# import Applicant, Committee, match_meetings, TimeInterval - -# __all__ = ("Applicant", "Committee", "match_meetings", "TimeInterval") \ No newline at end of file diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py deleted file mode 100644 index 4f51cf21..00000000 --- a/algorithm/src/mip_matching/match_meetings.py +++ /dev/null @@ -1,109 +0,0 @@ -from typing import TypedDict - -from mip_matching.TimeInterval import TimeInterval -from mip_matching.Committee import Committee -from mip_matching.Applicant import Applicant -import mip - -from datetime import timedelta, time -from itertools import combinations - -from mip_matching.types import Matching, MeetingMatch -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 - - -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[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): - 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, room)] - for applicant in committee.get_applicants() - for room in committee.get_rooms(interval) - if (applicant, committee, interval, room) in m - # type: ignore - ) <= 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, room)] - for interval in applicant.get_fitting_committee_slots(committee) - for room in committee.get_rooms(interval) - # type: ignore - ) <= 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(slot for slot in m.keys() if slot[0] == applicant) - - for interview_a, interview_b in combinations(potential_interviews, r=2): - 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, 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 - 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() - - # Få de faktiske møtetidene - antall_matchede_møter: int = 0 - matchings: list = [] - for name, variable in m.items(): - if variable.x: - antall_matchede_møter += 1 - matchings.append(name) - - antall_ønskede_møter = sum( - len(applicant.get_committees()) for applicant in applicants) - - match_object: MeetingMatch = { - "solver_status": solver_status, - "matched_meetings": antall_matchede_møter, - "total_wanted_meetings": antall_ønskede_møter, - "matchings": matchings, - } - - return match_object diff --git a/algorithm/src/mip_matching/types.py b/algorithm/src/mip_matching/types.py deleted file mode 100644 index a7318a28..00000000 --- a/algorithm/src/mip_matching/types.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -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] \ No newline at end of file diff --git a/algorithm/src/mip_matching/utils.py b/algorithm/src/mip_matching/utils.py deleted file mode 100644 index b248ae3e..00000000 --- a/algorithm/src/mip_matching/utils.py +++ /dev/null @@ -1,41 +0,0 @@ -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 diff --git a/algorithm/tests/ApplicantTest.py b/algorithm/tests/ApplicantTest.py deleted file mode 100644 index 8c51138a..00000000 --- a/algorithm/tests/ApplicantTest.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations -from datetime import datetime, timedelta -import unittest -from mip_matching.TimeInterval import TimeInterval -from mip_matching.Applicant import Applicant -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, 9, 0), datetime(2024, 8, 24, 9, 30)): 1 - }) - - self.test_applicant = Applicant("Test Testesen") - - self.test_applicant.add_interval(TimeInterval(datetime(2024, 8, 24, 7, 30), - datetime(2024, 8, 24, 10, 0))) - - def test_get_fitting_committee_slots(self) -> None: - - self.test_applicant.get_fitting_committee_slots(self.committee) - - self.assertEqual(set([TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 8, 30)), - TimeInterval(datetime(2024, 8, 24, 8, 30), - datetime(2024, 8, 24, 9, 0)), - TimeInterval(datetime(2024, 8, 24, 9, 0), - datetime(2024, 8, 24, 9, 30))]), - self.test_applicant.get_fitting_committee_slots(self.committee)) - - def test_add_interval_sanitizes(self) -> None: - - self.test_applicant.add_intervals({ - TimeInterval(datetime(2024, 8, 24, 9, 30), - datetime(2024, 8, 24, 10, 30)), - TimeInterval(datetime(2024, 8, 24, 4, 30), - datetime(2024, 8, 24, 6, 0)), - TimeInterval(datetime(2024, 8, 24, 10, 30), - datetime(2024, 8, 24, 11, 30)) - }) - - self.assertEqual(self.test_applicant.get_intervals(), { - TimeInterval(datetime(2024, 8, 24, 4, 30), - datetime(2024, 8, 24, 6, 0)), - TimeInterval(datetime(2024, 8, 24, 7, 30), - datetime(2024, 8, 24, 11, 30)), - }) - -if __name__ == "__main__": - unittest.main(exit=False) - \ No newline at end of file diff --git a/algorithm/tests/CommitteeTest.py b/algorithm/tests/CommitteeTest.py deleted file mode 100644 index 8ffebd52..00000000 --- a/algorithm/tests/CommitteeTest.py +++ /dev/null @@ -1,14 +0,0 @@ -# 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)) - - - \ No newline at end of file diff --git a/algorithm/tests/TimeIntervalTest.py b/algorithm/tests/TimeIntervalTest.py deleted file mode 100644 index 41f6edef..00000000 --- a/algorithm/tests/TimeIntervalTest.py +++ /dev/null @@ -1,123 +0,0 @@ -from __future__ import annotations -import unittest -from mip_matching.TimeInterval import TimeInterval -from datetime import datetime -from datetime import timedelta -# from mip_matching.Applicant import Applicant -# from mip_matching.Committee import Committee - - -class TimeIntervalTest(unittest.TestCase): - def setUp(self): - self.interval = TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 9, 30)) - self.t1: TimeInterval = TimeInterval( - datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 45)) - self.t2: TimeInterval = TimeInterval( - datetime(2024, 8, 24, 8, 45), datetime(2024, 8, 24, 9, 0)) - self.t3: TimeInterval = TimeInterval( - datetime(2024, 8, 24, 8, 30), datetime(2024, 8, 24, 9, 0)) - self.t4: TimeInterval = TimeInterval( - datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 30)) - - def test_overlapping(self): - - interval1: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 8, 30)) - interval2: TimeInterval = TimeInterval( - datetime(2024, 8, 24, 8, 15), - datetime(2024, 8, 24, 8, 45)) - - self.assertTrue(interval1.intersects(interval2)) - - def test_overlapping_edge(self): - interval1: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 8, 15)) - interval2: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 15), - datetime(2024, 8, 24, 8, 30)) - - self.assertFalse(interval1.intersects(interval2)) - - interval3: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 8, 30)) - - self.assertTrue(interval1.intersects(interval3)) - - def test_division(self): - actual_division = self.interval.divide(timedelta(minutes=30)) - expected_division = [TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 8, 30)), - TimeInterval(datetime(2024, 8, 24, 8, 30), - datetime(2024, 8, 24, 9, 0)), - TimeInterval(datetime(2024, 8, 24, 9, 0), - datetime(2024, 8, 24, 9, 30))] - - self.assertEqual(expected_division, actual_division) - - def test_contains(self): - self.assertTrue(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 9, 0)))) - self.assertTrue(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 9, 30)))) - self.assertTrue(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 9, 0), - datetime(2024, 8, 24, 9, 30)))) - self.assertTrue(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 8, 30), - datetime(2024, 8, 24, 9, 0)))) - - self.assertFalse(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 7, 45), - datetime(2024, 8, 24, 8, 30)))) - self.assertFalse(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 7, 45), - datetime(2024, 8, 24, 9, 31)))) - self.assertFalse(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 8, 30), - datetime(2024, 8, 24, 9, 31)))) - - def test_intersection(self): - self.assertEqual(TimeInterval( - datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)), - self.interval.intersection(TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 45)))) - self.assertEqual(TimeInterval( - datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)), - self.interval.intersection(TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)))) - self.assertEqual(TimeInterval( - datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 15)), - self.interval.intersection(TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 15)))) - self.assertEqual(TimeInterval( - datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 15)), - self.interval.intersection(TimeInterval(datetime(2024, 8, 24, 6, 45), datetime(2024, 8, 24, 9, 15)))) - - def test_tangent(self): - self.assertTrue(self.t1.is_tangent_to(self.t2)) - self.assertFalse(self.t1.is_tangent_to(self.t3)) - self.assertFalse(self.t4.is_tangent_to(self.t2)) - - def test_union(self): - with self.assertRaises(ValueError): - self.t4.union(self.t2) - - self.assertEqual(self.t1.union(self.t2), TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 9, 0))) - - self.assertEqual(self.t3.union(self.t4), TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 9, 0))) - - def test_get_contained_slots(self): - test_case_slots = [TimeInterval(datetime(2024, 8, 24, 7, 45), - datetime(2024, 8, 24, 8, 30)), - TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 8, 30)), - TimeInterval(datetime(2024, 8, 24, 8, 15), - datetime(2024, 8, 24, 9, 0)), - TimeInterval(datetime(2024, 8, 24, 9, 0), - datetime(2024, 8, 24, 10, 0)), - TimeInterval(datetime(2024, 8, 24, 8, 45), - datetime(2024, 8, 24, 9, 30))] - actual_contained = self.interval.get_contained_slots(test_case_slots) - expected_contained = [TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 8, 30)), - TimeInterval(datetime(2024, 8, 24, 8, 15), - datetime(2024, 8, 24, 9, 0)), - TimeInterval(datetime(2024, 8, 24, 8, 45), - datetime(2024, 8, 24, 9, 30))] - - self.assertTrue(len(expected_contained), len(actual_contained)) - self.assertEqual(set(expected_contained), set(actual_contained)) diff --git a/algorithm/tests/__init__.py b/algorithm/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/algorithm/tests/mip_test.py b/algorithm/tests/mip_test.py deleted file mode 100644 index 5c66870f..00000000 --- a/algorithm/tests/mip_test.py +++ /dev/null @@ -1,392 +0,0 @@ -from __future__ import annotations -from datetime import datetime, timedelta, date, time - -from mip_matching.TimeInterval import TimeInterval -from mip_matching.Committee import Committee -from mip_matching.Applicant import Applicant - -from mip_matching.match_meetings import match_meetings - -from faker import Faker - -import unittest -import random -from itertools import combinations - -from mip_matching.types import Matching - - -def print_matchings(committees: list[Committee], - intervals: list[TimeInterval], - matchings: list[Matching]): - - print("Tid".ljust(15), end="|") - print("|".join(str(com).ljust(8) for com in committees)) - - for interval in intervals: - print(interval.start.strftime("%d.%m %H:%M").ljust(15), end="|") - for committee in committees: - name = "" - cands = [a.name for a, c, - i, r in matchings if interval == i and c == committee] - name = cands[0] if len(cands) > 0 else "" - - print(name.rjust(8), end="|") - - print() - - -class MipTest(unittest.TestCase): - - def check_constraints(self, matchings: list[Matching]): - """Checks if the constraints are satisfied in the provided matchings. - TODO: Add more constraint tests.""" - - self.assertEqual(len(matchings), len(set((applicant, interval) - for applicant, _, interval, _ in matchings)), - "Constraint \"Applicant can only have one meeting during each TimeInterval\" failed.") - - load_per_committee_per_slot: dict[Committee, dict[TimeInterval, int]] = { - } - for _, committee, interval, _ in matchings: - if committee not in load_per_committee_per_slot: - load_per_committee_per_slot[committee] = {} - - # Øker antall med 1, eller setter inn 1 - load_per_committee_per_slot[committee][interval] = load_per_committee_per_slot[committee].get( - interval, 0) + 1 - - for committee, load_per_interval in load_per_committee_per_slot.items(): - for interval, load in load_per_interval.items(): - self.assertGreaterEqual(committee.get_capacity(interval), load, - f"Constraint \"Number of interviews per slot per committee cannot exceed capacity\" failed for Committee {committee} and interval {interval}") - - # Overlapping interviews per applicant - interviews_per_applicant: dict[Applicant, - set[Matching]] = {} - for interview in matchings: - applicant = interview[0] - if applicant not in interviews_per_applicant: - interviews_per_applicant[applicant] = set() - - interviews_per_applicant[applicant].add(interview) - - for applicant, interviews in interviews_per_applicant.items(): - for interview_a, interview_b in combinations(interviews, r=2): - self.assertFalse(interview_a[2].intersects(interview_b[2]), f"Constraint \"Applicant cannot have time-overlapping interviews\" failed for { - applicant}'s interviews with {interview_a[1]} ({interview_a[1]}) and {interview_b[1]} ({interview_b[2]})") - - def test_fixed_small(self): - """Small, fixed test with all capacities set to one""" - - appkom = Committee(name="Appkom") - - appkom.add_interview_slot( - TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 15)), "AppkomRom") - - oil = Committee(name="OIL") - oil.add_interview_slot( - TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)), "OilRom") - - prokom = Committee(name="Prokom") - prokom.add_interview_slot(TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 45)), "ProkomRom") - prokom.add_interview_slot(TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)), "ProkomRom") - - committees: set[Committee] = {appkom, oil, prokom} - - jørgen: Applicant = Applicant(name="Jørgen") - jørgen.add_committees({appkom, prokom}) - jørgen.add_intervals( - {TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 0))}) - - sindre: Applicant = Applicant(name="Sindre") - sindre.add_committees({appkom, oil}) - sindre.add_intervals({TimeInterval(datetime(2024, 8, 24, 8, 30), datetime( - 2024, 8, 24, 8, 45)), - TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30))}) - - julian: Applicant = Applicant(name="Julian") - julian.add_committees({appkom, prokom, oil}) - julian.add_intervals( - {TimeInterval(datetime(2024, 8, 24, 8, 45), datetime(2024, 8, 24, 9, 0)), - TimeInterval(datetime(2024, 8, 24, 8, 0), - datetime(2024, 8, 24, 8, 30)), - TimeInterval(datetime(2024, 8, 24, 9, 15), datetime(2024, 8, 24, 9, 30))}) - - fritz: Applicant = Applicant(name="Fritz") - fritz.add_committees({oil}) - fritz.add_intervals( - {TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 30)), - TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 15))}) - - applicants: set[Applicant] = {jørgen, sindre, julian, fritz} - - match = match_meetings(applicants=applicants, committees=committees) - - # Expectations - expected_number_of_matched_meetings = 7 - - print( - f"Klarte å matche {match['matched_meetings']} av {match['total_wanted_meetings']} ({match['matched_meetings']/match['total_wanted_meetings']:2f})") - - self.assertEqual(expected_number_of_matched_meetings, - match["matched_meetings"]) - - self.check_constraints(matchings=match["matchings"]) - - def test_randomized_large(self): - self.randomized_test(200, (10, 20), (5*5*2, 8*5*2), (1, 3)) - self.randomized_test(200, (10, 20), (5*5*2, 8*5*2), (3, 3)) - self.randomized_test(350, (15, 25), (5*5*2, 8*5*2), (3, 3)) - - def test_realistic(self): - """ - En realistisk test (grovt) basert på historiske søkertall og info fra komitéer. - - 200 søkere - - Intervjuperiode (én uke og 8-18 hver dag) - - Hver person søker på 2 eller 3 komitéer - - Hver søker har mellom 2 og 16 timer ledig i løpet av uken. - - Intervjulengder etter komitéers ønsker. - - """ - - fake = Faker() - - ANTALL_PERSONER = 200 - - INTERVALLENGDE_PER_PERSON_MIN = timedelta(minutes=30) - INTERVALLENGDE_PER_PERSON_MAKS = timedelta(hours=4) - ANTALL_INTERVALL_FORSØK = 4 - - START_DATE = date(2024, 8, 26) - END_DATE = date(2024, 8, 30) - START_TIME_PER_DAY = time(hour=8, minute=0) - END_TIME_PER_DAY = time(hour=18, minute=0) - DAY_LENGTH = datetime.combine(date.today( - # type: ignore - ), END_TIME_PER_DAY) - datetime.combine(date.today(), START_TIME_PER_DAY) - - def get_random_interval(interval_date: date, interval_length_min: timedelta, interval_length_max: timedelta) -> TimeInterval: - interval_start = datetime.combine(interval_date, START_TIME_PER_DAY) + \ - fake.time_delta(DAY_LENGTH) - - interval_end = interval_start + interval_length_min + \ - fake.time_delta(interval_length_max - interval_length_min) - - if interval_end > datetime.combine(interval_date, END_TIME_PER_DAY): - interval_end = datetime.combine( - interval_date, END_TIME_PER_DAY) - - return TimeInterval(interval_start, interval_end) - - # Gir tider til hver søker - applicants: set[Applicant] = set() - for person in range(ANTALL_PERSONER): - applicant = Applicant(name=str(person)) - - for _ in range(ANTALL_INTERVALL_FORSØK): - interval_date = fake.date_between_dates(START_DATE, END_DATE) - - applicant.add_interval( - get_random_interval(interval_date=interval_date, interval_length_min=INTERVALLENGDE_PER_PERSON_MIN, interval_length_max=INTERVALLENGDE_PER_PERSON_MAKS)) - - applicants.add(applicant) - - KAPASITET_PER_INTERVALL_MIN = 1 - KAPASITET_PER_INTERVALL_MAKS = 1 - INTERVALLENGDE_PER_KOMTIE_MIN = timedelta(hours=2) - INTERVALLENGDE_PER_KOMTIE_MAKS = timedelta(hours=8) - ANTALL_INTERVALL_FORSØK_KOMITE = 8 - - ANTALL_KOMITEER_PER_PERSON_MIN = 2 - ANTALL_KOMITEER_PER_PERSON_MAKS = 3 - - # Gir intervaller til hver komité. - committees: set[Committee] = { - Committee(name="Appkom", interview_length=timedelta(minutes=20)), - Committee(name="Prokom", interview_length=timedelta(minutes=20)), - Committee(name="Arrkom", interview_length=timedelta(minutes=20)), - Committee(name="Dotkom", interview_length=timedelta(minutes=30)), - Committee(name="OIL", interview_length=timedelta(minutes=20)), - Committee(name="Fagkom", interview_length=timedelta(minutes=20)), - Committee(name="Bedkom", interview_length=timedelta(minutes=30)), - Committee(name="FemInIT", interview_length=timedelta(minutes=30)), - Committee(name="Backlog", interview_length=timedelta(minutes=20)), - Committee(name="Trikom", interview_length=timedelta(minutes=35)), - } - - for committee in committees: - - for _ in range(ANTALL_INTERVALL_FORSØK_KOMITE): - interval_date = fake.date_between_dates(START_DATE, END_DATE) - - for _ in range(random.randint(KAPASITET_PER_INTERVALL_MIN, KAPASITET_PER_INTERVALL_MAKS)): - committee.add_interview_slot(get_random_interval(interval_date, INTERVALLENGDE_PER_KOMTIE_MIN, INTERVALLENGDE_PER_KOMTIE_MAKS), - room=str(random.getrandbits(128))) - - # Lar hver søker søke på tilfeldige komiteer - committees_list = list(committees) - # Må ha liste for at random.sample skal kunne velge ut riktig - for applicant in applicants: - applicant.add_committees(set(random.sample(committees_list, random.randint( - ANTALL_KOMITEER_PER_PERSON_MIN, ANTALL_KOMITEER_PER_PERSON_MAKS)))) - - match = match_meetings(applicants=applicants, committees=committees) - self.check_constraints(matchings=match["matchings"]) - - print( - f"Klarte å matche {match['matched_meetings']} av {match['total_wanted_meetings']} ({match['matched_meetings']/match['total_wanted_meetings']:2f})") - print(f"Solver status: {match['solver_status']}") - - def test_randomized_with_continuous_intervals(self): - """ - Gjør en randomisert test hvor hver person kan i sammenhengende - tidsperioder i stedet for tilfeldige slots. - - Hver komité har fremdeles like lange intervjutider. - """ - fake = Faker() - - ANTALL_PERSONER = 400 - - DEFAULT_INTERVIEW_TIME = timedelta(minutes=20) - - INTERVALLENGDE_PER_PERSON_MAKS = timedelta(hours=10) - ANTALL_INTERVALL_FORSØK = 4 - - START_DATE = date(2024, 8, 26) - END_DATE = date(2024, 8, 30) - START_TIME_PER_DAY = time(hour=8, minute=0) - END_TIME_PER_DAY = time(hour=18, minute=0) - - def get_random_interval(interval_date: date) -> TimeInterval: - interval_start = datetime.combine(interval_date, START_TIME_PER_DAY) + \ - fake.time_delta(INTERVALLENGDE_PER_PERSON_MAKS) - - interval_end = interval_start + \ - fake.time_delta(INTERVALLENGDE_PER_PERSON_MAKS) - - if interval_end > datetime.combine(interval_date, END_TIME_PER_DAY): - interval_end = datetime.combine( - interval_date, END_TIME_PER_DAY) - - return TimeInterval(interval_start, interval_end) - - # Gir tider til hver søker - applicants: set[Applicant] = set() - for person in range(ANTALL_PERSONER): - applicant = Applicant(name=str(person)) - - for _ in range(ANTALL_INTERVALL_FORSØK): - interval_date = fake.date_between_dates(START_DATE, END_DATE) - - applicant.add_interval( - get_random_interval(interval_date=interval_date)) - - applicants.add(applicant) - - KAPASITET_PER_INTERVALL_MIN = 1 - KAPASITET_PER_INTERVALL_MAKS = 3 - ANTALL_INTERVALL_FORSØK_KOMITE = 10 - - ANTALL_KOMITEER_PER_PERSON_MIN = 2 - ANTALL_KOMITEER_PER_PERSON_MAKS = 3 - - # Gir intervaller til hver komité. - committees: set[Committee] = { - Committee(name=navn, interview_length=DEFAULT_INTERVIEW_TIME) for navn in {"Appkom", "Prokom", "Arrkom", "Dotkom", "Bankkom", - "OIL", "Fagkom", "Bedkom", "FemInIT", "Backlog", "Trikom"}} - for committee in committees: - - for _ in range(ANTALL_INTERVALL_FORSØK_KOMITE): - interval_date = fake.date_between_dates(START_DATE, END_DATE) - - for _ in range(random.randint(KAPASITET_PER_INTERVALL_MIN, KAPASITET_PER_INTERVALL_MAKS)): - committee.add_interview_slot(get_random_interval(interval_date), - room=str(random.getrandbits(128))) - - # Lar hver søker søke på tilfeldige komiteer - committees_list = list(committees) - # Må ha liste for at random.sample skal kunne velge ut riktig - for applicant in applicants: - applicant.add_committees(set(random.sample(committees_list, random.randint( - ANTALL_KOMITEER_PER_PERSON_MIN, ANTALL_KOMITEER_PER_PERSON_MAKS)))) - - match = match_meetings(applicants=applicants, committees=committees) - self.check_constraints(matchings=match["matchings"]) - - print( - f"Klarte å matche {match['matched_meetings']} av {match['total_wanted_meetings']} ({match['matched_meetings']/match['total_wanted_meetings']:2f})") - print(f"Solver status: {match['solver_status']}") - - def randomized_test(self, - antall_personer: int, - antall_slots_per_person_interval: tuple[int, int], - antall_slots_per_komite_interval: tuple[int, int], - antall_komiteer_per_person_interval: tuple[int, int]): - """ - Tester tilfeldige utvalg av søkere, komitéer og tidsintervaller. - Alle komitéer har en kapasitet lik 1. - - Tester først og fremst performance. - TODO: Legg til flere asserts. - """ - - DEFAULT_INTERVIEW_TIME = timedelta(minutes=20) - - SLOTS: list[TimeInterval] = [] - for dag in range(0, 5): - """Lager slots for fra 0800 til 1800 hver dag""" - dagsintervall = TimeInterval( - datetime(2024, 8, 19+dag, 8, 0), datetime(2024, 8, 19+dag, 18, 0)) - [SLOTS.append(interval) for interval in dagsintervall.divide( - DEFAULT_INTERVIEW_TIME)] - - ANTALL_PERSONER = antall_personer - - ANTALL_SLOTS_PER_PERSON_MIN = antall_slots_per_person_interval[0] - ANTALL_SLOTS_PER_PERSON_MAKS = antall_slots_per_person_interval[1] - - ANTALL_SLOTS_PER_KOMITE_MIN = antall_slots_per_komite_interval[0] - ANTALL_SLOTS_PER_KOMITE_MAKS = antall_slots_per_komite_interval[1] - - ANTALL_KOMITEER_PER_PERSON_MIN = antall_komiteer_per_person_interval[0] - ANTALL_KOMITEER_PER_PERSON_MAKS = antall_komiteer_per_person_interval[1] - - komite_navn = {"Appkom", "Prokom", "Arrkom", "Dotkom", "Bankkom", - "OIL", "Fagkom", "Bedkom", "FemInIT", "Backlog", "Trikom"} - committees: set[Committee] = { - Committee(name=navn, interview_length=DEFAULT_INTERVIEW_TIME) for navn in komite_navn} - - # Gir tider til hver søker - applicants: set[Applicant] = set() - for person in range(ANTALL_PERSONER): - applicant = Applicant(name=str(person)) - # Velger ut et tilfeldig antall slots (alle av lengde 1) innenfor boundsene. - applicant.add_intervals(set(random.sample(SLOTS, random.randint( - ANTALL_SLOTS_PER_PERSON_MIN, ANTALL_SLOTS_PER_PERSON_MAKS)))) - - applicants.add(applicant) - - # Gir intervaller til hver komité. - for committee in committees: - - for slot in random.sample( - SLOTS, random.randint(ANTALL_SLOTS_PER_KOMITE_MIN, ANTALL_SLOTS_PER_KOMITE_MAKS)): - committee.add_interview_slot(slot, str(random.getrandbits(128))) - - # Lar hver søker søke på tilfeldige komiteer - committees_list = list(committees) - # Må ha liste for at random.sample skal kunne velge ut riktig - for applicant in applicants: - applicant.add_committees(set(random.sample(committees_list, random.randint( - ANTALL_KOMITEER_PER_PERSON_MIN, ANTALL_KOMITEER_PER_PERSON_MAKS)))) - - match = match_meetings(applicants=applicants, committees=committees) - self.check_constraints(matchings=match["matchings"]) - - print( - f"Klarte å matche {match['matched_meetings']} av {match['total_wanted_meetings']} ({match['matched_meetings']/match['total_wanted_meetings']:2f})") - print(f"Solver status: {match['solver_status']}") - - print_matchings(committees_list, SLOTS, match["matchings"])