Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/appKom/online-opptak into m…
Browse files Browse the repository at this point in the history
…anual-send-interview-times
  • Loading branch information
fredrir committed Aug 15, 2024
2 parents 0a74f08 + 2a15799 commit a7b3d82
Show file tree
Hide file tree
Showing 12 changed files with 234 additions and 141 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/algorithm.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
on:
push:
paths:
- "algorithm/*"
- "algorithm/**"
pull_request:
paths:
- "algorithm/*"
- "algorithm/**"
workflow_dispatch:

jobs:
Expand Down
132 changes: 111 additions & 21 deletions algorithm/bridge/fetch_applicants_and_committees.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,61 @@
from pymongo import MongoClient
from dotenv import load_dotenv
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
import os
import certifi
from typing import List, Dict

from mip_matching.Committee import Committee
from mip_matching.TimeInterval import TimeInterval
from mip_matching.Applicant import Applicant
from mip_matching.match_meetings import match_meetings, MeetingMatch

def main():
periods = fetch_periods()

#Sjekker om perioden er etter søknadstiden og før intervjuslutt og hasSentInterviewtimes er false, og returnerer søkere og komitétider dersom det er tilfelle
for period in periods:
periodId = str(period["_id"])
interview_end = datetime.fromisoformat(period["interviewPeriod"]["end"].replace("Z", "+00:00"))
application_end = datetime.fromisoformat(period["applicationPeriod"]["end"].replace("Z", "+00:00"))


now = datetime.now(timezone.utc)


if application_end > now and period["hasSentInterviewTimes"] == False and interview_end < now:
#or period["name"] == "Juli Opptak"
if (application_end < now and period["hasSentInterviewTimes"] == False):
applicants = fetch_applicants(periodId)
committee_times = fetch_committee_times(periodId)
print(applicants)
print(committee_times)

return applicants, committee_times
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)

match_result = match_meetings(applicant_objects, committee_objects)

send_to_db(match_result, applicants, periodId)
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)
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

collection = db["interviews"]

collection.insert_many(formatted_results)

client.close()

def connect_to_db(collection_name):
load_dotenv()
Expand All @@ -40,37 +72,95 @@ def connect_to_db(collection_name):
return collection, client

def fetch_periods():
collection, client = connect_to_db("period")

periods = collection.find()
collection, client = connect_to_db("periods")

periods = list(periods)
periods = list(collection.find())

client.close()

return periods

def fetch_applicants(periodId):
collection, client = connect_to_db("applicant")

applicants = collection.find({"periodId": periodId})
collection, client = connect_to_db("applications")

applicants = list(applicants)
applicants = list(collection.find({"periodId": periodId}))

client.close()

return applicants

def fetch_committee_times(periodId):
collection, client = connect_to_db("committee")
collection, client = connect_to_db("committees")

committee_times = collection.find({"periodId": periodId})

committee_times = list(committee_times)
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()

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

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}
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}
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"))
)
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"])))
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"))
)
capacity = interval_data.get('capacity', 1)
committee.add_interval(interval, capacity)
committees.add(committee)
return committees


if __name__ == "__main__":
main()
main()
24 changes: 17 additions & 7 deletions algorithm/src/Modellering.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,43 +10,53 @@
## Variabler

`p`

- Person

`k`

- Komité

`t`

- Timeslot (Må gjøres til intervaller etter hvert)

`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_1, t_1) + m(p, k_2, t_2) < 2` for alle par `k`, hvor t_1 og t_2 overlapper - Dette blir først aktuelt etter at timeslots har ulike tidsintervaller -->
- `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_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`


- `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`

### Sekundærmål

- [Ikke enda implementert] La det være færrest mulig og minst mulig mellomrom mellom intervjuene for komitéene.
2 changes: 1 addition & 1 deletion algorithm/src/mip_matching/Applicant.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,4 @@ def __str__(self) -> str:
return self.name

def __repr__(self) -> str:
return str(self)
return str(self)
11 changes: 2 additions & 9 deletions algorithm/src/mip_matching/Committee.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
from __future__ import annotations
from datetime import timedelta
import sys
print(sys.path)
print(__name__)
# sys.path.append("C:\\Users\\Jørgen Galdal\\Documents\\lokalSkoleprogrammering\\appkom\\OnlineOpptak\\algorithm\\mip_matching")

from mip_matching.Applicant import Applicant
from mip_matching.TimeInterval import TimeInterval

from typing import Iterator
# from typing import TYPE_CHECKING
# if TYPE_CHECKING:
# # Unngår cyclic import
from mip_matching.TimeInterval import TimeInterval


class Committee:
Expand Down Expand Up @@ -84,4 +77,4 @@ def __repr__(self):


if __name__ == "__main__":
print("running")
print("running")
29 changes: 3 additions & 26 deletions algorithm/src/mip_matching/TimeInterval.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ def get_contained_slots(self, slots: list[TimeInterval]):
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)

@staticmethod
def divide_interval(interval: TimeInterval, length: timedelta) -> list[TimeInterval]:
"""
Expand All @@ -85,29 +88,3 @@ def divide_interval(interval: TimeInterval, length: timedelta) -> list[TimeInter
local_end += length

return result


"""
Dette er gammel kode som nå er flyttet til de passende komité-/søker-klassene.
Foreløpig beholdt for referanse.
"""
# class TimeIntervals:
# def __init__(self, initial_list: list[TimeInterval] = None):
# self.list: list[TimeInterval] = initial_list if initial_list else []

# def add(self, interval: TimeInterval):
# self.list.append(interval)

# def recursive_intersection(self, other: TimeIntervals):
# """
# Returnerer alle tidsintervallene i *other* som er inneholdt i et av *self* sine intervaller"""
# result = TimeIntervals()

# for self_interval, other_interval in itertools.product(self.list, other.list):
# if self_interval.contains(other_interval):
# result.add(other_interval)

# return result

# def __iter__(self):
# return self.list.__iter__()
24 changes: 14 additions & 10 deletions algorithm/src/mip_matching/match_meetings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
from mip_matching.Applicant import Applicant
import mip

# from typing import TypedDict
from datetime import timedelta
from itertools import combinations


# Hvor stort buffer man ønsker å ha mellom intervjuene
APPLICANT_BUFFER_LENGTH = timedelta(minutes=15)


class MeetingMatch(TypedDict):
Expand Down Expand Up @@ -44,19 +49,18 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me
# type: ignore
for interval in applicant.get_fitting_committee_slots(committee)) <= 1

# Legger inn begrensninger for at en person kun kan ha ett intervju på hvert tidspunkt
# 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_intervals = set()
potential_interviews: set[tuple[Committee, TimeInterval]] = set()
for applicant_candidate, committee, interval in m:
if applicant == applicant_candidate:
potential_intervals.add(interval)

for interval in potential_intervals:
potential_interviews.add((committee, interval))

model += mip.xsum(m[(applicant, committee, interval)]
for committee in applicant.get_committees()
# type: ignore
if (applicant, committee, interval) in m) <= 1
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

# Setter mål til å være maksimering av antall møter
model.objective = mip.maximize(mip.xsum(m.values()))
Expand Down
Loading

0 comments on commit a7b3d82

Please sign in to comment.