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

27 lage algoritme for å matche personer og komite med intervjutider #56

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bf7d38a
La til forslag til algoritme
jorgengaldal Oct 15, 2023
4f6cf4a
snapshot: add snapshot
jorgengaldal Feb 18, 2024
5da52f6
docs: add venv-guide
jorgengaldal Mar 3, 2024
ff87557
docs: add modelling of MILP-algorithm
jorgengaldal Mar 3, 2024
bde1fc0
test: add unittests for Applicant, Committee and TimeInterval
jorgengaldal Mar 3, 2024
948a0a9
fix: made debugging easier
jorgengaldal Mar 17, 2024
ffe7e8a
refactor(algorithm): :recycle: move fixed test to test framework
jorgengaldal Mar 17, 2024
18a8575
config: add standard type checking for python
jorgengaldal Mar 17, 2024
d7fe7d5
fix: add constraint "only one interview per slot per person"
jorgengaldal Mar 17, 2024
22ebe24
test: add constraint test
jorgengaldal Mar 17, 2024
3910100
test: add large randomized test
jorgengaldal Mar 17, 2024
9aa14c2
config: la til requirements for venv
jorgengaldal Apr 14, 2024
8a70f13
fix: skrevet om til at timeInterval bruker datetime
jorgengaldal Apr 14, 2024
66ac457
fix: fikset constraint for at en person kun kan ha ett intervju på hv…
jorgengaldal Apr 14, 2024
cfe3617
feat: add support for union of timeinterval
jorgengaldal Apr 18, 2024
c361957
feat: La til sanitizing i Applicant
jorgengaldal Apr 25, 2024
2f43059
test: la til test med sammenhengende intervaller og ulike kapasiteter
jorgengaldal Apr 28, 2024
b03f0f4
test(algorithm): add realistic test
jorgengaldal May 5, 2024
4751993
docs: la til litt forklaringer til realistisk test
jorgengaldal May 5, 2024
88d7f53
refactor: fjernet ubrukt kode
jorgengaldal May 5, 2024
324c6e0
refactor: fjernet ubrukt kode
jorgengaldal May 5, 2024
04064b5
cleanup: fjernet csp-modul
jorgengaldal May 9, 2024
deaaba9
Checkpoint for omorganisering av filer for å kunne kjøre algoritmen.
jorgengaldal Jul 21, 2024
f7661d6
Remove unused file
jorgengaldal Jul 21, 2024
005c8ec
relative imports
fredrir Jul 21, 2024
9f82d39
Merge branch 'main' into 27-eksperimental-ordne-installering
jorgengaldal Jul 21, 2024
834bf73
Update algorithm.yml
jorgengaldal Jul 21, 2024
efb2dc0
Update algorithm.yml
jorgengaldal Jul 21, 2024
d980180
Merge branch 'main' into 27-lage-algoritme-for-å-matche-personer-og-k…
jorgengaldal Jul 21, 2024
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
8 changes: 7 additions & 1 deletion .github/workflows/algorithm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ jobs:
matrix:
python-version: ["3.12"]

steps:
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"

17 changes: 15 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
"editor.formatOnSave": true,
"python.testing.unittestArgs": [
"-v",
"-s",
"./algorithm",
"-p",
"*test.py"
],
"python.analysis.typeCheckingMode": "standard",
"python.testing.unittestEnabled": true,
"python.testing.pytestEnabled": false,
"conventionalCommits.scopes": [
"algorithm"
],
}
24 changes: 24 additions & 0 deletions algorithm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Algoritme

Algoritmen baserer seg på MIP-programmering (Mixed Integer Linear Programming).

## Setup Python Venv

```bash
cd algorithm
python -m venv ".venv"
```

Lag så en fil i `.\.venv\Lib\site-packages` som slutter på `.pth` og inneholder den absolutte filstien til `mip_matching`-mappen.

```
.\.venv\Scripts\activate
python -m pip install -r requirements.txt
```

## 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é.)
20 changes: 20 additions & 0 deletions algorithm/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[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"]
Binary file added algorithm/requirements.txt
Binary file not shown.
52 changes: 52 additions & 0 deletions algorithm/src/Modellering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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/

## 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) <= 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`
Empty file added algorithm/src/__init__.py
Empty file.
86 changes: 86 additions & 0 deletions algorithm/src/mip_matching/Applicant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from __future__ import annotations

from typing import TYPE_CHECKING
if TYPE_CHECKING:
# Unngår cyclic import
from Committee import Committee
from 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)
87 changes: 87 additions & 0 deletions algorithm/src/mip_matching/Committee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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 typing import Iterator
# from typing import TYPE_CHECKING
# if TYPE_CHECKING:
# # Unngår cyclic import
from mip_matching.TimeInterval import TimeInterval


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.capacities: dict[TimeInterval, int] = 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.
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)

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

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

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

def __str__(self):
return f"{self.name}"

def __repr__(self):
return str(self)


if __name__ == "__main__":
print("running")
Loading