Skip to content

Commit

Permalink
Merge branch 'main' into 151
Browse files Browse the repository at this point in the history
  • Loading branch information
julian-ao committed Jul 21, 2024
2 parents 0765c77 + 9757213 commit 47f896b
Show file tree
Hide file tree
Showing 36 changed files with 1,752 additions and 616 deletions.
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

0 comments on commit 47f896b

Please sign in to comment.