Skip to content

Commit

Permalink
Merge branch 'main' into 258-upgrade-to-next-1425
Browse files Browse the repository at this point in the history
  • Loading branch information
julian-ao committed Nov 17, 2024
2 parents 80930cb + d1c2a71 commit e11935d
Show file tree
Hide file tree
Showing 59 changed files with 1,887 additions and 778 deletions.
3 changes: 3 additions & 0 deletions .env.local.template
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ AUTH0_ISSUER=#issuer base url, example: example.us.auth0.com

AWS_SECRET_ACCESS_KEY=#aws secret access key
AWS_ACCESS_KEY_ID=#aws access key id

TWILIO_ACCOUNT_SID=#twilio account sid
TWILIO_AUTH_TOKEN=#twilio auth token
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
15 changes: 4 additions & 11 deletions algorithm/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
# Algoritme

Algoritmen baserer seg på MIP-programmering (Mixed Integer Linear Programming).
**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]
```

## TODOs

- [x] Lage funksjon som deler opp fra en komités slot
- [x] Sette opp begrensningene fra modelleringen
- [ ] Flikke litt på modelleringen.
- [ ] Finn ut hvordan man kan preprosessere dataen for å få ned kjøretiden (f. eks ved å lage lister av personer for hver komité.)
133 changes: 112 additions & 21 deletions algorithm/bridge/fetch_applicants_and_committees.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,60 @@
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 +71,97 @@ def connect_to_db(collection_name):
return collection, client

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

periods = collection.find()

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")

committee_times = collection.find({"periodId": periodId})
collection, client = connect_to_db("committees")

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()
room = result[3]

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

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"))
)
room = interval_data["room"]
committee.add_interview_slot(interval, room)
committees.add(committee)
return committees


if __name__ == "__main__":
main()
main()
42 changes: 26 additions & 16 deletions algorithm/src/Modellering.md
Original file line number Diff line number Diff line change
@@ -1,52 +1,62 @@
# Modellering av problem gjennom Mixed Integer Linear Programming

## 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/
# Modellering av møtetildelingsproblem gjennom Mixed Integer Linear Programming

## Variabler

`p`

- Person

`k`

- Komité

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

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

## Mål

Maksimere `sum(m(p, k, t))` for alle `p`, `k` og `t`. Altså: Maksimere antall intervjuer som tildeles.

## Mål
### Sekundærmål

- La intervjuene klumpe seg rundt klokken 12 og dermed også minske hvor mange hull komitéene får i sin intervjuplan.

Maksimere `sum(m(p, k, t))` for alle `p`, `k` og `t`
## 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/
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)
Loading

0 comments on commit e11935d

Please sign in to comment.