From bf7d38a8d967c97d3af8b6adc8e7f38cdc8904fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 15 Oct 2023 17:11:59 +0200 Subject: [PATCH 01/48] La til forslag til algoritme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lagde undersøkende kode for matchingalgoritme #27 --- algorithm/CSP_test.py | 86 ++++++++++++++++++ .../__pycache__/CSP_test.cpython-311.pyc | Bin 0 -> 6170 bytes algorithm/__pycache__/CSP_test.cpython-39.pyc | Bin 0 -> 3489 bytes algorithm/testing.py | 74 +++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 algorithm/CSP_test.py create mode 100644 algorithm/__pycache__/CSP_test.cpython-311.pyc create mode 100644 algorithm/__pycache__/CSP_test.cpython-39.pyc create mode 100644 algorithm/testing.py diff --git a/algorithm/CSP_test.py b/algorithm/CSP_test.py new file mode 100644 index 00000000..a378a849 --- /dev/null +++ b/algorithm/CSP_test.py @@ -0,0 +1,86 @@ +class CSP: + def __init__(self, variables, Domains,constraints): + self.variables = variables + self.domains = Domains + self.constraints = constraints + self.solution = None + + def solve(self): + print("Test") + assignment = {} + self.solution = self.backtrack(assignment) + return self.solution + + def backtrack(self, assignment): + print(f"{len(assignment)=}\r", end="") + if len(assignment) == len(self.variables): + return assignment + + var = self.select_unassigned_variable(assignment) + for value in self.order_domain_values(var, assignment): + if self.is_consistent(var, value, assignment): + assignment[var] = value + result = self.backtrack(assignment) + if result is not None: + return result + del assignment[var] + return None + + def select_unassigned_variable(self, assignment): + unassigned_vars = [var for var in self.variables if var not in assignment] + return min(unassigned_vars, key=lambda var: len(self.domains[var])) + + def order_domain_values(self, var, assignment): + return self.domains[var] + + def is_consistent(self, var, value, assignment): + for constraint_var in self.constraints[var]: + if constraint_var in assignment and assignment[constraint_var] == value: + return False + return True + + + + +personer_og_tidsslots = {"Jørgen": {1, 2, 3, 4}, "Sindre": {2, 4, 5, 6}, "Julian": {3, 1, 6}, "Fritz": {1, 4}} +komiteer_og_tidsslots = {"Appkom": {1, 2, 3, 4, 5}, "OIL": {4, 5, 6}, "Prokom": {1, 2, 3, 5}} + +timeslots = range(1, 9) +personer = personer_og_tidsslots.keys() +komiteer = komiteer_og_tidsslots.keys() + +komiteer_per_person = {"Jørgen": {"Appkom", "Prokom"}, "Sindre": {"Appkom", "OIL"}, "Julian": {"Appkom", "Prokom", "OIL"}, "Fritz": {"OIL"}} + +# Variables +variables = set() +for person in komiteer_per_person: + for komite in komiteer_per_person[person]: + variables.add((person, komite)) +# Domains +domains = {var: personer_og_tidsslots[var[0]].intersection(komiteer_og_tidsslots[var[1]]) for var in variables} + +import itertools +constraints = {var: set() for var in variables} +for var, annen in itertools.permutations(variables, 2): + if annen[0] == var[0]: + constraints[var].add(annen) + if annen[1] == var[1]: + constraints[var].add(annen) + +# # Solution +csp = CSP(variables, domains, constraints) +sol = csp.solve() +print(sol) + + +solution = {komite: [None for _ in timeslots] for komite in komiteer} +for person, komite in sol: + solution[komite][sol[person, komite]] = person + +print("---------") +print(solution) +# print_sudoku(solution) + +for komite in solution: + print(komite.ljust(15), end="|") + print("|".join([str(slot).rjust(15) for slot in solution[komite]])) diff --git a/algorithm/__pycache__/CSP_test.cpython-311.pyc b/algorithm/__pycache__/CSP_test.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18edb462bc3f2af23e8ce03ec94a63d69522f99b GIT binary patch literal 6170 zcmbstTWk|o_Rbx9{Ei(vF-~|TBq10`ps+<0Xei{-E@V?^pbsn5xSjz_96OzHP-~gh z1VzHEvRcz^l}Ou4mfB@gwX5Z0KalpP(*EpMHIAmj8mSV}?oa-*Vk;rO_MAI@L#wX# z`p%jAJolV)&ujcAkH<-%{OyN*(tT+C6Fb#nuQVQi%M-FfD0!8LWQmB}Jxa(|@R%h+ zt`U)b&H>~Zw0zEqR#CV{Mcb#A2#(v?xPw6_%(_11Ba{+}hjs22Y!-?7`fCRaY=T`)m1wW#=0VI z$yxN&h8h}@OLAYK_c)*s9+Mxs zl2hz#mJ`quoq;=u_#*)+Ao}Z+O8&;)EW0OptK^d!*8B(;fmRLxv$Y9xL+}5?ynGor z1j$HklJ6eq*H`eECDI{v6g#WE#&?ShgMeX+6l|qg41z>$&qzEZ1wm#ZCNme@q)Q4( z?grk1<{j2YPQcO2U3iALrS%MUa;SAw3c(5IB5pS8F4j>sFd??=61%?cZoXL#EDg@= zDt0$)t2M=~z+DsNt(-Pf)!YM}JTC^ONqAelh7lT7oqc==IxBE55l$o#DsoFC!iyB3 zMdSfmMGHVdv;wq=0zkWH18jDKADuoIw>02HwZkiYJi<`sxEzVrrgIc})k8^+hhEK{ zhy;2yWe~|U`I4IoTMWmIn4E~cmy{I4CZ^|NiIifv;^~xy^J5!Lv9w%ijHTlXa}wa3 zNv3CG$?4g2Qks|3SLN8;oFpewS7&1L^Rwx>nb+s%GqG7k`B2G7b2A`p!}A{+ z_GmPbN@SwZtQ&@ht3BjW=>wpWa$uX1Vvie{E?syr0_DG}j=bX1Q0KBF%BrpvT1qDyq~F(n`-wu!drz?bMcn zowNj7a4;usg1tOH^uc-_cEG81cPjVcSdOAQgMBEqXUa1aS#p*|c;b`;`@`KYljWDu z7Q@!5tS>30_SA1X{PI)IV}O+!yp$5dHW{Oq!NVfc0K35!rFbT~kTUm3Vzg??Mj$PV zk{mVd8HSS!VBVgD5=FyKfK{b3vLDcetxU<}07DJi4J%u02(qLsBr^(T3~1M;(y={R z52{>ESy6wKZvim1)9gEcdpMuHb8`74G|gIkL!*02bdOH=K;wI>J>R_;U5b9jua12> zRSNCXL;KdP`!)&3cb1(U8sG8A?tW+=T(=LdIZO7#y8W=)SM~-TdI#6NgKPX+q~txS zdyj4sfe$gDN@cO|S*fitavvfHm-!e80$FC5N$pMYuPJD#-*6a}-33(&YtZgVRn&t2ibncMG> z>o8`P*06DtHfcD`6WBwS>|gWERsuR;nQC%NsJeJDkGBE%1dh83igw8k?uWZWZ*YGR za@<|I>;OkjyB}aj+@tjN>D8* zBc{UJps@nlR9D*^R~LL$)SM~h3Tv%;RbdBFWw#(Z&+$3EFsXO}02dH&wiV*Kqg$ih zCTXY@)q-R((k7wt8I8`R#f2oc z-O=d#3$dg*Bjcx%+<^dnxQq%V<2A@gDRTu(zRd1r6e+`zF79AgtQrpfXz za12d-1FdU>(fi0qf$ruw?-qK_$;Lgy}>|lqEwrvye>0QigRxPGqtph|Ju> z4q$k2xBMai^MG2H9bi(!PO0y}#o%IvQ!Ga{_}70qX$a@!G$gDWn6Sym5S&189Ds=z zTlI-1_m24*GWYn`09Hr>o%=C#?iuJ;OAbC5>Ka~eJwN}*{XJbi zj@jr2?qV_m7CYx5c~7Sxfk0}zHEd|w}I zKo_q_8F>`ijeIcL8Nd@2WD^)@J7Ct;cwYHr^zQh_wh)mY&|t-}}uaXB)HIf*F>#KnHtU>2PFqmK~IJ3{}ep#+9dBPDbjQ#8DIO8!%HzoBD5ZjX6haX8wG85=pbQ zfH((G_%c!-rl2Aj$e88p3-BFRVcNn(z><+lfJEWS2;PK8*$V(f={s%KwT@2$g$sAP zKJHrC`O(gzztrBZxA)hkP2p}eo%k!TRXz;>j*O+QO7ape|I{bm=E%Sc#mVC21NPG{T&mQ>buBrkT}x@9G*;4C?mJXGSv>jRCH!gUW-2vw zNrOr{AGzD}VBIxPat-RPLDl}uz@xr>Yj112j(l_U9~ZUL z(^@oZ)=ffQrIQq16ZBX3+A>vv8VPI)#N*44-WgvWhcnuGA%=306aav)5bv(y5#75N z!zhdaP~R+5*X^NOLxtdKQ1kVaXs=FtHC91Puu34Vc@7<*lmb|CrgdjpwLKy}ty;^> zBNg8PH&kjnptl`RU1k5CLRk0jS?jugs^mYS`;S1V=5{c!ct#z~ca|-VeDBh01-@|V zV`1GAL~3@FsV5(YKTsah;5rQ!7D{xxPT`M(?f={ACqT5#smAl+!dt7eC1I~F>{a=) z;JBT*l~|lznuQLrs#Z0==v;EDPFU`q;K24Dyau4;8rNOpa2>%eAc})8I{=GQ>O_8` z96q3i^1F4ez0g;`evSCe1FkK4m%OSM4)8+iL%DHG zX#m1iaDv*Y-ngB+l`EVs*|zAmEvwTS*IVLxb*>j0<^tvR2e&>jE%biq!EXJ9Q_v~- aPU`?|qq=Rh#Et3Pn8xa+wSEFK=Klgh*(%fk literal 0 HcmV?d00001 diff --git a/algorithm/__pycache__/CSP_test.cpython-39.pyc b/algorithm/__pycache__/CSP_test.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca0ff869327b0aeb570a51c80e23ea4b9af8bd8f GIT binary patch literal 3489 zcma)8OK%*-7Ov{<8TULr89&Df!Q9JhAXovhii`z{1tLNL$*^e|_3cbmoQ`|C$5lO+ zVvX5MB(gyQv1Kt9tNa7@{0A1jWi<;{hz(-FcdFg?*a_&)s7_VaId$IWR0WMjmErgH zZ*R&&tbV1B+20&K-p9~CamGGkf(iZ@V{Ccs0Tb4b9MCq<`H_o~C_k{o+z*aN`>xrq z7+3|i_Ji$N(gsx>=N-u>r4#0rrDf(h(GX3XZix8@7EZTtV*UX)++0~1Gp{P$ZT985 zSCg)+br{xduP&>ThOEi@ed~mCcJS@NV{t{aA2V6qW@GL(#e!Ij=Dm5bG_%{1^OGyG zDwa7sNt`$u_}#OZ_EVvfW|ezCfITEg38zu)kcy9{eI9Q8s03Yl@0Byl`*i(MD>tvO~K!w4*Na}1(B>f`QRmjgtK!+SyMcIZ(--3i^Gi!gy~t7!3B4{_dxPK^p99{Ya;>zY7!nRO)nN@F;VA zKa9iF_sK?05*aU>|4|)u8wI+Dk~#YkEop3gvW!K@j^ULFd~-tH!Fb4bm?FBal~wKq zy?xl;+gD|?qo@+IYM^zv7n1?T)Era@Rf2;KC26DS1mg~`^P>tWWH3GAT>QveL}j#z zut=fDm;*wKF-L+gkDv|ovq-;VL~ddue%utytz*P+;^5oHj;(IcJhC>4zGUc~kt5iU ztN)E0i*ZDm@b9sM8x--}#&TvyGB#1n)(}wHOZ{P-+b4xz&(XStYnC@vXXijUG|qb(*UieU@+e1j#cF?-sCQ?V$lDn zLEbxy#03vhEXj*B>(>FkX7{-%AomG>h~_kg*HT#hHOD>`lq*W#SSXnC-DeVm3TA;A ztpe=9jbFBq9Q+A83#6L8P~^C=Opxk}{brHA*(INZAu)%7}GfXAx0L5k`?QQ0r7XO4}Lj z+|4NH-xI;s3$#z>9^L%^b<2--d1mj+M_nsV9=jjLUHd%v-u%Th(ndU}DKm01Di}2n za1}$F*2Ne3(e;1Vq(>eoezc~Fx{aaf4n^f4%R6WJjmTlQP>j4W6Dbo)qeTmd7csOc zPkyvQIy@)kFErR9@r>{doGygZ&2o(58`+0URZ*5s2W=PST*WPfKpj*CrJ#NH!_N^S zVpEijQINuGwDg+iy_ZTckKno1S3vk5jMMJdg!fP`S4j5@m3g;Yp>`Pued+sI)%W{} z7)G?N`~JZ&h;ob~2P&#YQ{ghTy z^6QkMHKjd+Q0rJJ(tk=Wm{KR)A`(TBP%ja9`RtSEy3UXi`SdRF$~$2!l=RAXhEW*A zUg<*>rbk=EEN8J!E5nCQtN#Mz0?s~-fLt3R^!x-7ckQhFfw_k=`_qp<$;zLp1eem( zq|7VC@-+gl0_1$n9(@|bh1#{SOMbhJpEiA&PM&se3PY|TE?2BG?qBkVTEX@mA~St) z>D)gvZ5j4e(y%X$^U>4{t8(u3%lych=lm*f6~8I9zCZbhd?jP?j3H-ESOc9gqC=uLM4Y3ysQuz_&G)%0Q%})HTA0Apxqk)U=zf9 zQr)DrgEo(+Uy*p3TVz+ZP^^3)X&^Hdb;A5kD4*`UO Date: Sun, 18 Feb 2024 14:09:30 +0100 Subject: [PATCH 02/48] snapshot: add snapshot Snapshot for tracking previously local files. #27 --- .gitignore | 5 +- .../CSP_test.py | 0 .../testing.py | 0 .../Applicant.py | 41 ++++ .../Committee.py | 28 +++ .../Modellering.md | 52 +++++ .../fixed_test.py | 177 ++++++++++++++++++ .../mip_test.py | 70 +++++++ algorithm/README.md | 3 + .../__pycache__/CSP_test.cpython-311.pyc | Bin 6170 -> 0 bytes algorithm/__pycache__/CSP_test.cpython-39.pyc | Bin 3489 -> 0 bytes 11 files changed, 375 insertions(+), 1 deletion(-) rename algorithm/{ => Constraint Satisfaction Problem}/CSP_test.py (100%) rename algorithm/{ => Constraint Satisfaction Problem}/testing.py (100%) create mode 100644 algorithm/Mixed Integer Linear Programming/Applicant.py create mode 100644 algorithm/Mixed Integer Linear Programming/Committee.py create mode 100644 algorithm/Mixed Integer Linear Programming/Modellering.md create mode 100644 algorithm/Mixed Integer Linear Programming/fixed_test.py create mode 100644 algorithm/Mixed Integer Linear Programming/mip_test.py create mode 100644 algorithm/README.md delete mode 100644 algorithm/__pycache__/CSP_test.cpython-311.pyc delete mode 100644 algorithm/__pycache__/CSP_test.cpython-39.pyc diff --git a/.gitignore b/.gitignore index 2b7ceea6..bbaa3429 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ yarn-error.log* next-env.d.ts # SWC -.babelrc \ No newline at end of file +.babelrc + +# Python +__pycache__/ \ No newline at end of file diff --git a/algorithm/CSP_test.py b/algorithm/Constraint Satisfaction Problem/CSP_test.py similarity index 100% rename from algorithm/CSP_test.py rename to algorithm/Constraint Satisfaction Problem/CSP_test.py diff --git a/algorithm/testing.py b/algorithm/Constraint Satisfaction Problem/testing.py similarity index 100% rename from algorithm/testing.py rename to algorithm/Constraint Satisfaction Problem/testing.py diff --git a/algorithm/Mixed Integer Linear Programming/Applicant.py b/algorithm/Mixed Integer Linear Programming/Applicant.py new file mode 100644 index 00000000..eda06bdc --- /dev/null +++ b/algorithm/Mixed Integer Linear Programming/Applicant.py @@ -0,0 +1,41 @@ +from Committee import Committee +from fixed_test 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): + self.committee: list[Committee] = [] + self.slots: set[TimeInterval] = [] + + def add_committee(self, committee: Committee) -> None: + self.committee.append(committee) + + def add_committees(self, committees: set[Committee]) -> None: + for committee in committees: + self.add_committee(committee) + + def add_interval(self, interval: TimeInterval) -> None: + # TODO: Vurder å gjøre "sanitizing" ved å slå sammen overlappende intervaller. + self.slots.add(interval) + + def add_intervals(self, intervals: set[TimeInterval]) -> None: + for interval in intervals: + self.add_interval(interval) + + 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.slots): + if applicant_interval.contains(committee_interval): + result.add(committee_interval) + + return result diff --git a/algorithm/Mixed Integer Linear Programming/Committee.py b/algorithm/Mixed Integer Linear Programming/Committee.py new file mode 100644 index 00000000..ef338954 --- /dev/null +++ b/algorithm/Mixed Integer Linear Programming/Committee.py @@ -0,0 +1,28 @@ +from fixed_test 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. + """ + + def __init__(self, interview_length: int = 1): + self.capacities: dict[TimeInterval, int] = dict() + self.interview_length: int = interview_length + + 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 + + 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) diff --git a/algorithm/Mixed Integer Linear Programming/Modellering.md b/algorithm/Mixed Integer Linear Programming/Modellering.md new file mode 100644 index 00000000..3fea01e0 --- /dev/null +++ b/algorithm/Mixed Integer Linear Programming/Modellering.md @@ -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, 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` diff --git a/algorithm/Mixed Integer Linear Programming/fixed_test.py b/algorithm/Mixed Integer Linear Programming/fixed_test.py new file mode 100644 index 00000000..5d7ca480 --- /dev/null +++ b/algorithm/Mixed Integer Linear Programming/fixed_test.py @@ -0,0 +1,177 @@ +""" +TODO: +- [Gjort] Lage funksjon som deler opp fra en komités slot +- 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é.) +""" + +from __future__ import annotations +from dataclasses import dataclass + +import itertools + +from Committee import Committee +from Applicant import Applicant + + +@dataclass(frozen=True) +class TimeInterval: + start: int + end: int + + def overlaps(self, other: TimeInterval) -> bool: + """Returnerer true om to timeslots er helt eller delvis overlappende.""" + return other.start < self.start < other.end or self.start < other.start < self.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 intersection(self, other: TimeInterval) -> TimeInterval: + """Returnerer et snitt av to timeslots.""" + if not self.overlaps(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 slots i listen av timeslots + "slots", som inngår helt i dette timeslottet.""" + return set(slot for slot in slots if self.contains(slot)) + + @staticmethod + def divide_interval(interval: TimeInterval, length: int) -> 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)) + + 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__() + + +committees: set[Committee] + +appkom = Committee() +appkom.add_intervals_with_capacities({TimeInterval(1, 2): 1, + TimeInterval(2, 3): 1, + TimeInterval(3, 4): 1, + TimeInterval(4, 5): 1}) + +oil = Committee() +oil.add_intervals_with_capacities({TimeInterval(4, 5): 1, + TimeInterval(5, 6): 1}) + +prokom = Committee() +prokom.add_intervals_with_capacities({TimeInterval(1, 3): 1, + TimeInterval(4, 6): 1}) + +committees = {appkom, oil, prokom} + +jørgen: Applicant = Applicant() +jørgen.add_committees({appkom, prokom}) +jørgen.add_intervals({TimeInterval(1, 4)}) + +sindre: Applicant = Applicant() +sindre.add_committees({appkom, oil}) +sindre.add_intervals({TimeInterval(2, 3), TimeInterval(4, 6)}) + +julian: Applicant = Applicant() +julian.add_committees({appkom, prokom, oil}) +julian.add_intervals({TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) + +fritz: Applicant = Applicant() +fritz.add_committees({oil}) +fritz.add_intervals({TimeInterval(1, 2), TimeInterval(4, 5)}) + +applicants: set[Applicant] = {jørgen, sindre, julian, fritz} + + +# personer_og_tidsslots = {"Jørgen": {TimeInterval(1, 4)}, +# "Sindre": {TimeInterval(2, 3), TimeInterval(4, 6)}, +# "Julian": {TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}, +# "Fritz": {TimeInterval(1, 2), TimeInterval(4, 5)}} +# komiteer_og_tidsslots = {"Appkom": {TimeInterval(1, 2), TimeInterval(2, 3), TimeInterval(3, 4), TimeInterval(4, 5)}, +# "OIL": {TimeInterval(4, 5), TimeInterval(5, 6)}, +# "Prokom": {TimeInterval(1, 3), TimeInterval(4, 6)}} + +# komite_kapasiteter = {"Appkom": {TimeInterval(1, 2): 1, +# TimeInterval(2, 3): 1, +# TimeInterval(3, 4): 1, +# TimeInterval(4, 5): 1}, +# "OIL": {TimeInterval(4, 5): 1, +# TimeInterval(5, 6): 1}, +# "Prokom": {TimeInterval(1, 3): 1, +# TimeInterval(4, 6): 1}} + + +# personer = personer_og_tidsslots.keys() +# komiteer = komiteer_og_tidsslots.keys() + +# intervjulengder = {"Appkom": 1, +# "Prokom": 2, +# "OIL": 1} + +# komiteer_per_person = {"Jørgen": {"Appkom", "Prokom"}, +# "Sindre": {"Appkom", "OIL"}, +# "Julian": {"Appkom", "Prokom", "OIL"}, +# "Fritz": {"OIL"}} + + +""" +Tror det følgende er gammel kode som ble brukt for CSP. +""" + +# # Variables +# variables = set() +# for person in komiteer_per_person: +# for komite in komiteer_per_person[person]: +# variables.add((person, komite)) +# # Domains +# domains = {var: personer_og_tidsslots[var[0]].intersection( +# komiteer_og_tidsslots[var[1]]) for var in variables} + +# constraints = {var: set() for var in variables} +# for var, annen in itertools.permutations(variables, 2): +# if annen[0] == var[0]: +# constraints[var].add(annen) +# if annen[1] == var[1]: +# constraints[var].add(annen) diff --git a/algorithm/Mixed Integer Linear Programming/mip_test.py b/algorithm/Mixed Integer Linear Programming/mip_test.py new file mode 100644 index 00000000..08ea7db9 --- /dev/null +++ b/algorithm/Mixed Integer Linear Programming/mip_test.py @@ -0,0 +1,70 @@ +import mip +import itertools + +from fixed_test import TimeIntervals + +from fixed_test import komiteer_og_tidsslots, komiteer, komite_kapasiteter +from fixed_test import komiteer_per_person, personer, personer_og_tidsslots + +model = mip.Model(sense=mip.MAXIMIZE) + +m = {} + +# Lager alle maksimeringsvariabler +for person in personer: + + for komite in komiteer_per_person[person]: + for person_intervall, komite_intervall in itertools.product(personer_og_tidsslots[person], + komiteer_og_tidsslots[komite]): + # Går gjennom alle kombinasjoner av intervaller for person og for komité + + if person_intervall.contains(komite_intervall): + # Legger til alle intervaller for komitéen som passer for personen + + m[(person, komite, komite_intervall) + ] = model.add_var(var_type=mip.BINARY, name=f"({person}, {komite}, {komite_intervall})") + +print(m) + +# Legger inn begrensninger for at en komité kun kan ha antall møter i et slot lik kapasiteten. +for komite in komiteer: + for slot in komiteer_og_tidsslots[komite]: + model += mip.xsum(m[(person, komite, slot)] + for person in personer if (person, komite, slot) in m) <= komite_kapasiteter[komite][slot] + + +# Legger inn begrensninger for at en person kun har ett intervju med hver komité +for person in personer: + for komite in komiteer: + # if person == "Jørgen": + # print(person, komite) + # print(personer_og_tidsslots[person]) + # print(komiteer_og_tidsslots[komite]) + # print(personer_og_tidsslots[person].intersection( + # komiteer_og_tidsslots[komite])) + + person_tider: TimeIntervals = TimeIntervals(personer_og_tidsslots[person]) + komite_tider: TimeIntervals = TimeIntervals(komiteer_og_tidsslots[komite]) + + model += mip.xsum(m[(person, komite, slot)] for slot in person_tider.recursive_intersection(komite_tider) if komite in komiteer_per_person[person]) <= 1 + +model.objective = mip.maximize(mip.xsum(m.values())) + +solver_status = model.optimize() + + +# Møtetider: +antall_matchede_møter: int = 0 +for name, variable in m.items(): + if variable.x: + antall_matchede_møter += 1 + print(f"{name}: {variable.x}") + +antall_ønskede_møter = sum( + len(komiteer_per_person[person]) for person in personer) + +print( + f"Klarte å matche {antall_matchede_møter} av {antall_ønskede_møter} ({antall_matchede_møter/antall_ønskede_møter:2f})") + + +print(solver_status) diff --git a/algorithm/README.md b/algorithm/README.md new file mode 100644 index 00000000..12684a6b --- /dev/null +++ b/algorithm/README.md @@ -0,0 +1,3 @@ +# Algoritme + +Prøvde først med CSP-algoritme, men har nå gått over til MIP-programmering. diff --git a/algorithm/__pycache__/CSP_test.cpython-311.pyc b/algorithm/__pycache__/CSP_test.cpython-311.pyc deleted file mode 100644 index 18edb462bc3f2af23e8ce03ec94a63d69522f99b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6170 zcmbstTWk|o_Rbx9{Ei(vF-~|TBq10`ps+<0Xei{-E@V?^pbsn5xSjz_96OzHP-~gh z1VzHEvRcz^l}Ou4mfB@gwX5Z0KalpP(*EpMHIAmj8mSV}?oa-*Vk;rO_MAI@L#wX# z`p%jAJolV)&ujcAkH<-%{OyN*(tT+C6Fb#nuQVQi%M-FfD0!8LWQmB}Jxa(|@R%h+ zt`U)b&H>~Zw0zEqR#CV{Mcb#A2#(v?xPw6_%(_11Ba{+}hjs22Y!-?7`fCRaY=T`)m1wW#=0VI z$yxN&h8h}@OLAYK_c)*s9+Mxs zl2hz#mJ`quoq;=u_#*)+Ao}Z+O8&;)EW0OptK^d!*8B(;fmRLxv$Y9xL+}5?ynGor z1j$HklJ6eq*H`eECDI{v6g#WE#&?ShgMeX+6l|qg41z>$&qzEZ1wm#ZCNme@q)Q4( z?grk1<{j2YPQcO2U3iALrS%MUa;SAw3c(5IB5pS8F4j>sFd??=61%?cZoXL#EDg@= zDt0$)t2M=~z+DsNt(-Pf)!YM}JTC^ONqAelh7lT7oqc==IxBE55l$o#DsoFC!iyB3 zMdSfmMGHVdv;wq=0zkWH18jDKADuoIw>02HwZkiYJi<`sxEzVrrgIc})k8^+hhEK{ zhy;2yWe~|U`I4IoTMWmIn4E~cmy{I4CZ^|NiIifv;^~xy^J5!Lv9w%ijHTlXa}wa3 zNv3CG$?4g2Qks|3SLN8;oFpewS7&1L^Rwx>nb+s%GqG7k`B2G7b2A`p!}A{+ z_GmPbN@SwZtQ&@ht3BjW=>wpWa$uX1Vvie{E?syr0_DG}j=bX1Q0KBF%BrpvT1qDyq~F(n`-wu!drz?bMcn zowNj7a4;usg1tOH^uc-_cEG81cPjVcSdOAQgMBEqXUa1aS#p*|c;b`;`@`KYljWDu z7Q@!5tS>30_SA1X{PI)IV}O+!yp$5dHW{Oq!NVfc0K35!rFbT~kTUm3Vzg??Mj$PV zk{mVd8HSS!VBVgD5=FyKfK{b3vLDcetxU<}07DJi4J%u02(qLsBr^(T3~1M;(y={R z52{>ESy6wKZvim1)9gEcdpMuHb8`74G|gIkL!*02bdOH=K;wI>J>R_;U5b9jua12> zRSNCXL;KdP`!)&3cb1(U8sG8A?tW+=T(=LdIZO7#y8W=)SM~-TdI#6NgKPX+q~txS zdyj4sfe$gDN@cO|S*fitavvfHm-!e80$FC5N$pMYuPJD#-*6a}-33(&YtZgVRn&t2ibncMG> z>o8`P*06DtHfcD`6WBwS>|gWERsuR;nQC%NsJeJDkGBE%1dh83igw8k?uWZWZ*YGR za@<|I>;OkjyB}aj+@tjN>D8* zBc{UJps@nlR9D*^R~LL$)SM~h3Tv%;RbdBFWw#(Z&+$3EFsXO}02dH&wiV*Kqg$ih zCTXY@)q-R((k7wt8I8`R#f2oc z-O=d#3$dg*Bjcx%+<^dnxQq%V<2A@gDRTu(zRd1r6e+`zF79AgtQrpfXz za12d-1FdU>(fi0qf$ruw?-qK_$;Lgy}>|lqEwrvye>0QigRxPGqtph|Ju> z4q$k2xBMai^MG2H9bi(!PO0y}#o%IvQ!Ga{_}70qX$a@!G$gDWn6Sym5S&189Ds=z zTlI-1_m24*GWYn`09Hr>o%=C#?iuJ;OAbC5>Ka~eJwN}*{XJbi zj@jr2?qV_m7CYx5c~7Sxfk0}zHEd|w}I zKo_q_8F>`ijeIcL8Nd@2WD^)@J7Ct;cwYHr^zQh_wh)mY&|t-}}uaXB)HIf*F>#KnHtU>2PFqmK~IJ3{}ep#+9dBPDbjQ#8DIO8!%HzoBD5ZjX6haX8wG85=pbQ zfH((G_%c!-rl2Aj$e88p3-BFRVcNn(z><+lfJEWS2;PK8*$V(f={s%KwT@2$g$sAP zKJHrC`O(gzztrBZxA)hkP2p}eo%k!TRXz;>j*O+QO7ape|I{bm=E%Sc#mVC21NPG{T&mQ>buBrkT}x@9G*;4C?mJXGSv>jRCH!gUW-2vw zNrOr{AGzD}VBIxPat-RPLDl}uz@xr>Yj112j(l_U9~ZUL z(^@oZ)=ffQrIQq16ZBX3+A>vv8VPI)#N*44-WgvWhcnuGA%=306aav)5bv(y5#75N z!zhdaP~R+5*X^NOLxtdKQ1kVaXs=FtHC91Puu34Vc@7<*lmb|CrgdjpwLKy}ty;^> zBNg8PH&kjnptl`RU1k5CLRk0jS?jugs^mYS`;S1V=5{c!ct#z~ca|-VeDBh01-@|V zV`1GAL~3@FsV5(YKTsah;5rQ!7D{xxPT`M(?f={ACqT5#smAl+!dt7eC1I~F>{a=) z;JBT*l~|lznuQLrs#Z0==v;EDPFU`q;K24Dyau4;8rNOpa2>%eAc})8I{=GQ>O_8` z96q3i^1F4ez0g;`evSCe1FkK4m%OSM4)8+iL%DHG zX#m1iaDv*Y-ngB+l`EVs*|zAmEvwTS*IVLxb*>j0<^tvR2e&>jE%biq!EXJ9Q_v~- aPU`?|qq=Rh#Et3Pn8xa+wSEFK=Klgh*(%fk diff --git a/algorithm/__pycache__/CSP_test.cpython-39.pyc b/algorithm/__pycache__/CSP_test.cpython-39.pyc deleted file mode 100644 index ca0ff869327b0aeb570a51c80e23ea4b9af8bd8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3489 zcma)8OK%*-7Ov{<8TULr89&Df!Q9JhAXovhii`z{1tLNL$*^e|_3cbmoQ`|C$5lO+ zVvX5MB(gyQv1Kt9tNa7@{0A1jWi<;{hz(-FcdFg?*a_&)s7_VaId$IWR0WMjmErgH zZ*R&&tbV1B+20&K-p9~CamGGkf(iZ@V{Ccs0Tb4b9MCq<`H_o~C_k{o+z*aN`>xrq z7+3|i_Ji$N(gsx>=N-u>r4#0rrDf(h(GX3XZix8@7EZTtV*UX)++0~1Gp{P$ZT985 zSCg)+br{xduP&>ThOEi@ed~mCcJS@NV{t{aA2V6qW@GL(#e!Ij=Dm5bG_%{1^OGyG zDwa7sNt`$u_}#OZ_EVvfW|ezCfITEg38zu)kcy9{eI9Q8s03Yl@0Byl`*i(MD>tvO~K!w4*Na}1(B>f`QRmjgtK!+SyMcIZ(--3i^Gi!gy~t7!3B4{_dxPK^p99{Ya;>zY7!nRO)nN@F;VA zKa9iF_sK?05*aU>|4|)u8wI+Dk~#YkEop3gvW!K@j^ULFd~-tH!Fb4bm?FBal~wKq zy?xl;+gD|?qo@+IYM^zv7n1?T)Era@Rf2;KC26DS1mg~`^P>tWWH3GAT>QveL}j#z zut=fDm;*wKF-L+gkDv|ovq-;VL~ddue%utytz*P+;^5oHj;(IcJhC>4zGUc~kt5iU ztN)E0i*ZDm@b9sM8x--}#&TvyGB#1n)(}wHOZ{P-+b4xz&(XStYnC@vXXijUG|qb(*UieU@+e1j#cF?-sCQ?V$lDn zLEbxy#03vhEXj*B>(>FkX7{-%AomG>h~_kg*HT#hHOD>`lq*W#SSXnC-DeVm3TA;A ztpe=9jbFBq9Q+A83#6L8P~^C=Opxk}{brHA*(INZAu)%7}GfXAx0L5k`?QQ0r7XO4}Lj z+|4NH-xI;s3$#z>9^L%^b<2--d1mj+M_nsV9=jjLUHd%v-u%Th(ndU}DKm01Di}2n za1}$F*2Ne3(e;1Vq(>eoezc~Fx{aaf4n^f4%R6WJjmTlQP>j4W6Dbo)qeTmd7csOc zPkyvQIy@)kFErR9@r>{doGygZ&2o(58`+0URZ*5s2W=PST*WPfKpj*CrJ#NH!_N^S zVpEijQINuGwDg+iy_ZTckKno1S3vk5jMMJdg!fP`S4j5@m3g;Yp>`Pued+sI)%W{} z7)G?N`~JZ&h;ob~2P&#YQ{ghTy z^6QkMHKjd+Q0rJJ(tk=Wm{KR)A`(TBP%ja9`RtSEy3UXi`SdRF$~$2!l=RAXhEW*A zUg<*>rbk=EEN8J!E5nCQtN#Mz0?s~-fLt3R^!x-7ckQhFfw_k=`_qp<$;zLp1eem( zq|7VC@-+gl0_1$n9(@|bh1#{SOMbhJpEiA&PM&se3PY|TE?2BG?qBkVTEX@mA~St) z>D)gvZ5j4e(y%X$^U>4{t8(u3%lych=lm*f6~8I9zCZbhd?jP?j3H-ESOc9gqC=uLM4Y3ysQuz_&G)%0Q%})HTA0Apxqk)U=zf9 zQr)DrgEo(+Uy*p3TVz+ZP^^3)X&^Hdb;A5kD4*`UO Date: Sun, 3 Mar 2024 16:56:12 +0100 Subject: [PATCH 03/48] docs: add venv-guide --- .gitignore | 3 ++- algorithm/README.md | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index bbaa3429..da2a43d7 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ next-env.d.ts .babelrc # Python -__pycache__/ \ No newline at end of file +__pycache__/ +.venv/ \ No newline at end of file diff --git a/algorithm/README.md b/algorithm/README.md index 12684a6b..af9f9141 100644 --- a/algorithm/README.md +++ b/algorithm/README.md @@ -1,3 +1,17 @@ # Algoritme -Prøvde først med CSP-algoritme, men har nå gått over til MIP-programmering. +Prøvde først med CSP-algoritme, men har nå gått over til MIP-programmering (Mixd 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 +``` From ff875572c298b977dd0db8a21205677def8ed72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 3 Mar 2024 16:57:43 +0100 Subject: [PATCH 04/48] docs: add modelling of MILP-algorithm --- algorithm/mip_matching/Modellering.md | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 algorithm/mip_matching/Modellering.md diff --git a/algorithm/mip_matching/Modellering.md b/algorithm/mip_matching/Modellering.md new file mode 100644 index 00000000..3fea01e0 --- /dev/null +++ b/algorithm/mip_matching/Modellering.md @@ -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, 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` From bde1fc0df2aaeb8dc3a07908f64fde2ff312a5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 3 Mar 2024 16:59:20 +0100 Subject: [PATCH 05/48] test: add unittests for Applicant, Committee and TimeInterval --- .vscode/settings.json | 13 ++- .../Committee.py | 28 ------ .../Modellering.md | 52 ---------- .../mip_test.py | 70 ------------- .../Applicant.py | 33 +++++-- algorithm/mip_matching/Committee.py | 78 +++++++++++++++ algorithm/mip_matching/TimeInterval.py | 85 ++++++++++++++++ algorithm/mip_matching/__init__.py | 0 algorithm/mip_matching/tests/ApplicantTest.py | 24 +++++ algorithm/mip_matching/tests/CommitteeTest.py | 17 ++++ .../mip_matching/tests/TimeIntervalTest.py | 63 ++++++++++++ algorithm/mip_matching/tests/__init__.py | 0 .../tests}/fixed_test.py | 97 +++---------------- algorithm/mip_matching/tests/mip_test.py | 62 ++++++++++++ 14 files changed, 379 insertions(+), 243 deletions(-) delete mode 100644 algorithm/Mixed Integer Linear Programming/Committee.py delete mode 100644 algorithm/Mixed Integer Linear Programming/Modellering.md delete mode 100644 algorithm/Mixed Integer Linear Programming/mip_test.py rename algorithm/{Mixed Integer Linear Programming => mip_matching}/Applicant.py (62%) create mode 100644 algorithm/mip_matching/Committee.py create mode 100644 algorithm/mip_matching/TimeInterval.py create mode 100644 algorithm/mip_matching/__init__.py create mode 100644 algorithm/mip_matching/tests/ApplicantTest.py create mode 100644 algorithm/mip_matching/tests/CommitteeTest.py create mode 100644 algorithm/mip_matching/tests/TimeIntervalTest.py create mode 100644 algorithm/mip_matching/tests/__init__.py rename algorithm/{Mixed Integer Linear Programming => mip_matching/tests}/fixed_test.py (53%) create mode 100644 algorithm/mip_matching/tests/mip_test.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 9bf4d12b..c648504e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,13 @@ { "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true -} + "editor.formatOnSave": true, + "python.testing.unittestArgs": [ + "-v", + "-s", + "./algorithm", + "-p", + "*test.py" + ], + "python.testing.unittestEnabled": true, + "python.testing.pytestEnabled": false, +} \ No newline at end of file diff --git a/algorithm/Mixed Integer Linear Programming/Committee.py b/algorithm/Mixed Integer Linear Programming/Committee.py deleted file mode 100644 index ef338954..00000000 --- a/algorithm/Mixed Integer Linear Programming/Committee.py +++ /dev/null @@ -1,28 +0,0 @@ -from fixed_test 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. - """ - - def __init__(self, interview_length: int = 1): - self.capacities: dict[TimeInterval, int] = dict() - self.interview_length: int = interview_length - - 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 - - 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) diff --git a/algorithm/Mixed Integer Linear Programming/Modellering.md b/algorithm/Mixed Integer Linear Programming/Modellering.md deleted file mode 100644 index 3fea01e0..00000000 --- a/algorithm/Mixed Integer Linear Programming/Modellering.md +++ /dev/null @@ -1,52 +0,0 @@ -# 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, 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` diff --git a/algorithm/Mixed Integer Linear Programming/mip_test.py b/algorithm/Mixed Integer Linear Programming/mip_test.py deleted file mode 100644 index 08ea7db9..00000000 --- a/algorithm/Mixed Integer Linear Programming/mip_test.py +++ /dev/null @@ -1,70 +0,0 @@ -import mip -import itertools - -from fixed_test import TimeIntervals - -from fixed_test import komiteer_og_tidsslots, komiteer, komite_kapasiteter -from fixed_test import komiteer_per_person, personer, personer_og_tidsslots - -model = mip.Model(sense=mip.MAXIMIZE) - -m = {} - -# Lager alle maksimeringsvariabler -for person in personer: - - for komite in komiteer_per_person[person]: - for person_intervall, komite_intervall in itertools.product(personer_og_tidsslots[person], - komiteer_og_tidsslots[komite]): - # Går gjennom alle kombinasjoner av intervaller for person og for komité - - if person_intervall.contains(komite_intervall): - # Legger til alle intervaller for komitéen som passer for personen - - m[(person, komite, komite_intervall) - ] = model.add_var(var_type=mip.BINARY, name=f"({person}, {komite}, {komite_intervall})") - -print(m) - -# Legger inn begrensninger for at en komité kun kan ha antall møter i et slot lik kapasiteten. -for komite in komiteer: - for slot in komiteer_og_tidsslots[komite]: - model += mip.xsum(m[(person, komite, slot)] - for person in personer if (person, komite, slot) in m) <= komite_kapasiteter[komite][slot] - - -# Legger inn begrensninger for at en person kun har ett intervju med hver komité -for person in personer: - for komite in komiteer: - # if person == "Jørgen": - # print(person, komite) - # print(personer_og_tidsslots[person]) - # print(komiteer_og_tidsslots[komite]) - # print(personer_og_tidsslots[person].intersection( - # komiteer_og_tidsslots[komite])) - - person_tider: TimeIntervals = TimeIntervals(personer_og_tidsslots[person]) - komite_tider: TimeIntervals = TimeIntervals(komiteer_og_tidsslots[komite]) - - model += mip.xsum(m[(person, komite, slot)] for slot in person_tider.recursive_intersection(komite_tider) if komite in komiteer_per_person[person]) <= 1 - -model.objective = mip.maximize(mip.xsum(m.values())) - -solver_status = model.optimize() - - -# Møtetider: -antall_matchede_møter: int = 0 -for name, variable in m.items(): - if variable.x: - antall_matchede_møter += 1 - print(f"{name}: {variable.x}") - -antall_ønskede_møter = sum( - len(komiteer_per_person[person]) for person in personer) - -print( - f"Klarte å matche {antall_matchede_møter} av {antall_ønskede_møter} ({antall_matchede_møter/antall_ønskede_møter:2f})") - - -print(solver_status) diff --git a/algorithm/Mixed Integer Linear Programming/Applicant.py b/algorithm/mip_matching/Applicant.py similarity index 62% rename from algorithm/Mixed Integer Linear Programming/Applicant.py rename to algorithm/mip_matching/Applicant.py index eda06bdc..ee0181b0 100644 --- a/algorithm/Mixed Integer Linear Programming/Applicant.py +++ b/algorithm/mip_matching/Applicant.py @@ -1,18 +1,28 @@ -from Committee import Committee -from fixed_test import TimeInterval +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): - self.committee: list[Committee] = [] - self.slots: set[TimeInterval] = [] + 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.committee.append(committee) + self.committees.append(committee) + committee._add_applicant(self) def add_committees(self, committees: set[Committee]) -> None: for committee in committees: @@ -34,8 +44,15 @@ def get_fitting_committee_slots(self, committee: Committee) -> set[TimeInterval] result: set[TimeInterval] = set() - for applicant_interval, committee_interval in itertools.product(self.slots, committee.slots): + 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 diff --git a/algorithm/mip_matching/Committee.py b/algorithm/mip_matching/Committee.py new file mode 100644 index 00000000..22ba4578 --- /dev/null +++ b/algorithm/mip_matching/Committee.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +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: int = 1): + self.capacities: dict[TimeInterval, int] = dict() + self.interview_length: int = 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} - {self.get_applicant_count()} applicants" + + def __repr__(self): + return str(self) diff --git a/algorithm/mip_matching/TimeInterval.py b/algorithm/mip_matching/TimeInterval.py new file mode 100644 index 00000000..6fcd7b78 --- /dev/null +++ b/algorithm/mip_matching/TimeInterval.py @@ -0,0 +1,85 @@ +from __future__ import annotations +from dataclasses import dataclass + + +@dataclass(frozen=True) +class TimeInterval: + """ + Definerer et tidsintervall fra og med start til og uten end. + """ + start: int + end: int + + def overlaps(self, other: TimeInterval) -> bool: + """Returnerer true om to timeslots er helt eller delvis overlappende.""" + return other.start <= self.start < other.end or self.start <= other.start < self.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 intersection(self, other: TimeInterval) -> TimeInterval: + """Returnerer et snitt av to timeslots.""" + if not self.overlaps(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 slots i listen av timeslots + "slots", som inngår helt i dette timeslottet.""" + return set(slot for slot in slots if self.contains(slot)) + + def divide(self, length: int) -> list[TimeInterval]: + return TimeInterval.divide_interval(self, length) + + @staticmethod + def divide_interval(interval: TimeInterval, length: int) -> 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 + + +""" +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__() diff --git a/algorithm/mip_matching/__init__.py b/algorithm/mip_matching/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/algorithm/mip_matching/tests/ApplicantTest.py b/algorithm/mip_matching/tests/ApplicantTest.py new file mode 100644 index 00000000..5d99cd06 --- /dev/null +++ b/algorithm/mip_matching/tests/ApplicantTest.py @@ -0,0 +1,24 @@ +from __future__ import annotations +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=2) + self.committee.add_intervals_with_capacities({ + TimeInterval(0, 6): 1, + TimeInterval(4, 6): 1 + }) + + def test_get_fitting_committee_slots(self) -> None: + test_applicant = Applicant("Test Testesen") + + test_applicant.add_interval(TimeInterval(-2, 8)) + + test_applicant.get_fitting_committee_slots(self.committee) + + self.assertEqual(set([TimeInterval(0, 2), TimeInterval(2, 4), TimeInterval( + 4, 6)]), test_applicant.get_fitting_committee_slots(self.committee)) diff --git a/algorithm/mip_matching/tests/CommitteeTest.py b/algorithm/mip_matching/tests/CommitteeTest.py new file mode 100644 index 00000000..64260456 --- /dev/null +++ b/algorithm/mip_matching/tests/CommitteeTest.py @@ -0,0 +1,17 @@ +from __future__ import annotations +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=2) + self.committee.add_intervals_with_capacities({ + TimeInterval(0, 6): 1, + TimeInterval(2, 6): 1 + }) + + def test_capacity_stacking(self) -> None: + self.assertEqual(1, self.committee.get_capacity(TimeInterval(0, 2))) + self.assertEqual(2, self.committee.get_capacity(TimeInterval(2, 4))) diff --git a/algorithm/mip_matching/tests/TimeIntervalTest.py b/algorithm/mip_matching/tests/TimeIntervalTest.py new file mode 100644 index 00000000..21646d08 --- /dev/null +++ b/algorithm/mip_matching/tests/TimeIntervalTest.py @@ -0,0 +1,63 @@ +from __future__ import annotations +import unittest +from mip_matching.TimeInterval import TimeInterval +# from mip_matching.Applicant import Applicant +# from mip_matching.Committee import Committee + + +class TimeIntervalTest(unittest.TestCase): + def setUp(self): + self.interval = TimeInterval(0, 6) + + def test_overlapping(self): + interval1: TimeInterval = TimeInterval(0, 2) + interval2: TimeInterval = TimeInterval(1, 3) + + self.assertTrue(interval1.overlaps(interval2)) + + def test_overlapping_edge(self): + interval1: TimeInterval = TimeInterval(0, 1) + interval2: TimeInterval = TimeInterval(1, 2) + + self.assertFalse(interval1.overlaps(interval2)) + + interval3: TimeInterval = TimeInterval(0, 2) + + self.assertTrue(interval1.overlaps(interval3)) + + def test_division(self): + actual_division = self.interval.divide(2) + expected_division = [TimeInterval( + 0, 2), TimeInterval(2, 4), TimeInterval(4, 6)] + + self.assertEqual(expected_division, actual_division) + + def test_contains(self): + self.assertTrue(self.interval.contains(TimeInterval(0, 4))) + self.assertTrue(self.interval.contains(TimeInterval(0, 6))) + self.assertTrue(self.interval.contains(TimeInterval(4, 6))) + self.assertTrue(self.interval.contains(TimeInterval(2, 4))) + + self.assertFalse(self.interval.contains(TimeInterval(-1, 2))) + self.assertFalse(self.interval.contains(TimeInterval(-1, 7))) + self.assertFalse(self.interval.contains(TimeInterval(2, 7))) + + def test_intersection(self): + self.assertEqual(TimeInterval( + 4, 6), self.interval.intersection(TimeInterval(4, 7))) + self.assertEqual(TimeInterval( + 4, 6), self.interval.intersection(TimeInterval(4, 6))) + self.assertEqual(TimeInterval( + 4, 5), self.interval.intersection(TimeInterval(4, 5))) + self.assertEqual(TimeInterval( + 0, 5), self.interval.intersection(TimeInterval(-5, 5))) + + def test_get_contained_slots(self): + test_case_slots = [TimeInterval(-1, 2), TimeInterval(0, 2), + TimeInterval(1, 4), TimeInterval(4, 8), TimeInterval(3, 6)] + actual_contained = self.interval.get_contained_slots(test_case_slots) + expected_contained = [TimeInterval( + 0, 2), TimeInterval(1, 4), TimeInterval(3, 6)] + + self.assertTrue(len(expected_contained), len(actual_contained)) + self.assertEqual(set(expected_contained), set(actual_contained)) diff --git a/algorithm/mip_matching/tests/__init__.py b/algorithm/mip_matching/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/algorithm/Mixed Integer Linear Programming/fixed_test.py b/algorithm/mip_matching/tests/fixed_test.py similarity index 53% rename from algorithm/Mixed Integer Linear Programming/fixed_test.py rename to algorithm/mip_matching/tests/fixed_test.py index 5d7ca480..4c32a7e9 100644 --- a/algorithm/Mixed Integer Linear Programming/fixed_test.py +++ b/algorithm/mip_matching/tests/fixed_test.py @@ -7,118 +7,49 @@ """ from __future__ import annotations -from dataclasses import dataclass -import itertools +import sys +print(sys.path) -from Committee import Committee -from Applicant import Applicant +from mip_matching.Committee import Committee +from mip_matching.Applicant import Applicant +from mip_matching.TimeInterval import TimeInterval -@dataclass(frozen=True) -class TimeInterval: - start: int - end: int - - def overlaps(self, other: TimeInterval) -> bool: - """Returnerer true om to timeslots er helt eller delvis overlappende.""" - return other.start < self.start < other.end or self.start < other.start < self.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 intersection(self, other: TimeInterval) -> TimeInterval: - """Returnerer et snitt av to timeslots.""" - if not self.overlaps(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 slots i listen av timeslots - "slots", som inngår helt i dette timeslottet.""" - return set(slot for slot in slots if self.contains(slot)) - - @staticmethod - def divide_interval(interval: TimeInterval, length: int) -> 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)) - - 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__() committees: set[Committee] -appkom = Committee() +appkom = Committee(name="Appkom") appkom.add_intervals_with_capacities({TimeInterval(1, 2): 1, TimeInterval(2, 3): 1, TimeInterval(3, 4): 1, TimeInterval(4, 5): 1}) -oil = Committee() +oil = Committee(name="OIL") oil.add_intervals_with_capacities({TimeInterval(4, 5): 1, TimeInterval(5, 6): 1}) -prokom = Committee() +prokom = Committee(name="Prokom") prokom.add_intervals_with_capacities({TimeInterval(1, 3): 1, TimeInterval(4, 6): 1}) committees = {appkom, oil, prokom} -jørgen: Applicant = Applicant() +jørgen: Applicant = Applicant(name="Jørgen") jørgen.add_committees({appkom, prokom}) jørgen.add_intervals({TimeInterval(1, 4)}) -sindre: Applicant = Applicant() +sindre: Applicant = Applicant(name="Sindre") sindre.add_committees({appkom, oil}) sindre.add_intervals({TimeInterval(2, 3), TimeInterval(4, 6)}) -julian: Applicant = Applicant() +julian: Applicant = Applicant(name="Julian") julian.add_committees({appkom, prokom, oil}) -julian.add_intervals({TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) +julian.add_intervals( + {TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) -fritz: Applicant = Applicant() +fritz: Applicant = Applicant(name="Fritz") fritz.add_committees({oil}) fritz.add_intervals({TimeInterval(1, 2), TimeInterval(4, 5)}) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py new file mode 100644 index 00000000..c39f4183 --- /dev/null +++ b/algorithm/mip_matching/tests/mip_test.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from mip_matching.tests.fixed_test import applicants, committees +import mip + +import unittest + + +model = mip.Model(sense=mip.MAXIMIZE) + +m = {} + +# Lager alle maksimeringsvariabler +for applicant in applicants: + print(applicant) + for committee in applicant.get_committees(): + for interval in applicant.get_fitting_committee_slots(committee): + m[(applicant, committee, interval)] = model.add_var( + var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval})") + +print(m) + +# 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)] + for applicant in committee.get_applicants() + if (applicant, committee, interval) in m) <= 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)] + for interval in applicant.get_fitting_committee_slots(committee)) <= 1 + +model.objective = mip.maximize(mip.xsum(m.values())) + +# Kjør optimeringen +solver_status = model.optimize() + + +# Få de faktiske møtetidene +antall_matchede_møter: int = 0 +for name, variable in m.items(): + if variable.x: + antall_matchede_møter += 1 + print(f"{name}: {variable.x}") + +antall_ønskede_møter = sum( + len(applicant.get_committees()) for applicant in applicants) + +print( + f"Klarte å matche {antall_matchede_møter} av {antall_ønskede_møter} ({antall_matchede_møter/antall_ønskede_møter:2f})") + + +print(solver_status) + + +class TestTest(unittest.TestCase): + def test_test(self): + self.assertTrue(True) \ No newline at end of file From 948a0a973b5b06943b811e4076df54358dfb9227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 13:27:59 +0100 Subject: [PATCH 06/48] fix: made debugging easier --- algorithm/mip_matching/Applicant.py | 3 +++ algorithm/mip_matching/Committee.py | 2 +- algorithm/mip_matching/tests/mip_test.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/algorithm/mip_matching/Applicant.py b/algorithm/mip_matching/Applicant.py index ee0181b0..85ed4bb8 100644 --- a/algorithm/mip_matching/Applicant.py +++ b/algorithm/mip_matching/Applicant.py @@ -56,3 +56,6 @@ def get_committees(self) -> set[Committee]: def __str__(self) -> str: return self.name + + def __repr__(self) -> str: + return str(self) diff --git a/algorithm/mip_matching/Committee.py b/algorithm/mip_matching/Committee.py index 22ba4578..e81b1645 100644 --- a/algorithm/mip_matching/Committee.py +++ b/algorithm/mip_matching/Committee.py @@ -72,7 +72,7 @@ def get_applicant_count(self) -> int: return len(self.applicants) def __str__(self): - return f"{self.name} - {self.get_applicant_count()} applicants" + return f"{self.name}" def __repr__(self): return str(self) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index c39f4183..fa8601f7 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -45,7 +45,7 @@ for name, variable in m.items(): if variable.x: antall_matchede_møter += 1 - print(f"{name}: {variable.x}") + print(f"{name}") antall_ønskede_møter = sum( len(applicant.get_committees()) for applicant in applicants) @@ -59,4 +59,4 @@ class TestTest(unittest.TestCase): def test_test(self): - self.assertTrue(True) \ No newline at end of file + self.assertTrue(True) From ffe7e8a3b65afed270e27ac0bf2fcd58af85b18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 14:40:46 +0100 Subject: [PATCH 07/48] refactor(algorithm): :recycle: move fixed test to test framework --- .vscode/settings.json | 3 + algorithm/mip_matching/tests/fixed_test.py | 47 +------ algorithm/mip_matching/tests/mip_test.py | 137 +++++++++++++++------ 3 files changed, 105 insertions(+), 82 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c648504e..d058f362 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,4 +10,7 @@ ], "python.testing.unittestEnabled": true, "python.testing.pytestEnabled": false, + "conventionalCommits.scopes": [ + "algorithm" + ], } \ No newline at end of file diff --git a/algorithm/mip_matching/tests/fixed_test.py b/algorithm/mip_matching/tests/fixed_test.py index 4c32a7e9..043c2b6e 100644 --- a/algorithm/mip_matching/tests/fixed_test.py +++ b/algorithm/mip_matching/tests/fixed_test.py @@ -7,54 +7,13 @@ """ from __future__ import annotations +from mip_matching.TimeInterval import TimeInterval +from mip_matching.Applicant import Applicant +from mip_matching.Committee import Committee import sys print(sys.path) -from mip_matching.Committee import Committee -from mip_matching.Applicant import Applicant -from mip_matching.TimeInterval import TimeInterval - - - - -committees: set[Committee] - -appkom = Committee(name="Appkom") -appkom.add_intervals_with_capacities({TimeInterval(1, 2): 1, - TimeInterval(2, 3): 1, - TimeInterval(3, 4): 1, - TimeInterval(4, 5): 1}) - -oil = Committee(name="OIL") -oil.add_intervals_with_capacities({TimeInterval(4, 5): 1, - TimeInterval(5, 6): 1}) - -prokom = Committee(name="Prokom") -prokom.add_intervals_with_capacities({TimeInterval(1, 3): 1, - TimeInterval(4, 6): 1}) - -committees = {appkom, oil, prokom} - -jørgen: Applicant = Applicant(name="Jørgen") -jørgen.add_committees({appkom, prokom}) -jørgen.add_intervals({TimeInterval(1, 4)}) - -sindre: Applicant = Applicant(name="Sindre") -sindre.add_committees({appkom, oil}) -sindre.add_intervals({TimeInterval(2, 3), TimeInterval(4, 6)}) - -julian: Applicant = Applicant(name="Julian") -julian.add_committees({appkom, prokom, oil}) -julian.add_intervals( - {TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) - -fritz: Applicant = Applicant(name="Fritz") -fritz.add_committees({oil}) -fritz.add_intervals({TimeInterval(1, 2), TimeInterval(4, 5)}) - -applicants: set[Applicant] = {jørgen, sindre, julian, fritz} - # personer_og_tidsslots = {"Jørgen": {TimeInterval(1, 4)}, # "Sindre": {TimeInterval(2, 3), TimeInterval(4, 6)}, diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index fa8601f7..03626246 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -1,62 +1,123 @@ from __future__ import annotations +from mip_matching.TimeInterval import TimeInterval +from mip_matching.Committee import Committee from mip_matching.tests.fixed_test import applicants, committees +from mip_matching.Applicant import Applicant import mip +from typing import TypedDict + import unittest -model = mip.Model(sense=mip.MAXIMIZE) +class MeetingMatch(TypedDict): + """Type definition of a meeting match object""" + solver_status: mip.OptimizationStatus + matched_meetings: int + total_wanted_meetings: int + matchings: list[tuple[Applicant, Committee, TimeInterval]] + + +def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> MeetingMatch: + """Matches meetings and returns a MeetingMatch-object""" + model = mip.Model(sense=mip.MAXIMIZE) + + m = {} + + # Lager alle maksimeringsvariabler + for applicant in applicants: + for committee in applicant.get_committees(): + for interval in applicant.get_fitting_committee_slots(committee): + m[(applicant, committee, interval)] = model.add_var( + var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval})") + + # 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)] + for applicant in committee.get_applicants() + if (applicant, committee, interval) in m) <= 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)] + for interval in applicant.get_fitting_committee_slots(committee)) <= 1 + + # Setter mål til å være maksimering av antall møter + model.objective = mip.maximize(mip.xsum(m.values())) + + # 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) + print(f"{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 -m = {} -# Lager alle maksimeringsvariabler -for applicant in applicants: - print(applicant) - for committee in applicant.get_committees(): - for interval in applicant.get_fitting_committee_slots(committee): - m[(applicant, committee, interval)] = model.add_var( - var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval})") +class MipTest(unittest.TestCase): + def test_fixed_small(self): + """Small, fixed test with all capacities set to one""" -print(m) + appkom = Committee(name="Appkom") + appkom.add_intervals_with_capacities({TimeInterval(1, 5): 1}) -# 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)] - for applicant in committee.get_applicants() - if (applicant, committee, interval) in m) <= capacity + oil = Committee(name="OIL") + oil.add_intervals_with_capacities({TimeInterval(4, 6): 1}) + prokom = Committee(name="Prokom") + prokom.add_intervals_with_capacities({TimeInterval(1, 3): 1, + TimeInterval(4, 6): 1}) -# 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)] - for interval in applicant.get_fitting_committee_slots(committee)) <= 1 + committees: set[Committee] = {appkom, oil, prokom} -model.objective = mip.maximize(mip.xsum(m.values())) + jørgen: Applicant = Applicant(name="Jørgen") + jørgen.add_committees({appkom, prokom}) + jørgen.add_intervals({TimeInterval(1, 4)}) -# Kjør optimeringen -solver_status = model.optimize() + sindre: Applicant = Applicant(name="Sindre") + sindre.add_committees({appkom, oil}) + sindre.add_intervals({TimeInterval(2, 3), TimeInterval(4, 6)}) + julian: Applicant = Applicant(name="Julian") + julian.add_committees({appkom, prokom, oil}) + julian.add_intervals( + {TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) -# Få de faktiske møtetidene -antall_matchede_møter: int = 0 -for name, variable in m.items(): - if variable.x: - antall_matchede_møter += 1 - print(f"{name}") + fritz: Applicant = Applicant(name="Fritz") + fritz.add_committees({oil}) + fritz.add_intervals({TimeInterval(1, 2), TimeInterval(4, 5)}) -antall_ønskede_møter = sum( - len(applicant.get_committees()) for applicant in applicants) + applicants: set[Applicant] = {jørgen, sindre, julian, fritz} -print( - f"Klarte å matche {antall_matchede_møter} av {antall_ønskede_møter} ({antall_matchede_møter/antall_ønskede_møter:2f})") + match = match_meetings(applicants=applicants, committees=committees) + # Expectations + expected_number_of_matched_meetings = 7 -print(solver_status) + 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"]) -class TestTest(unittest.TestCase): - def test_test(self): - self.assertTrue(True) + def test_randomized_large(self): + \ No newline at end of file From 18a85759435db3de061dd162fff70a7afae2aa42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 17:18:27 +0100 Subject: [PATCH 08/48] config: add standard type checking for python --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index d058f362..f780532c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "-p", "*test.py" ], + "python.analysis.typeCheckingMode": "standard", "python.testing.unittestEnabled": true, "python.testing.pytestEnabled": false, "conventionalCommits.scopes": [ From d7fe7d5f983c14726fb242e7c04ef2f94af4079a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 17:30:23 +0100 Subject: [PATCH 09/48] fix: add constraint "only one interview per slot per person" --- algorithm/mip_matching/tests/mip_test.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index 03626246..f7adbc23 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -37,13 +37,20 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me for interval, capacity in committee.get_intervals_and_capacities(): model += mip.xsum(m[(applicant, committee, interval)] for applicant in committee.get_applicants() - if (applicant, committee, interval) in m) <= capacity + if (applicant, committee, interval) in m) <= capacity # type: ignore # 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)] - for interval in applicant.get_fitting_committee_slots(committee)) <= 1 + for interval in applicant.get_fitting_committee_slots(committee)) <= 1 # type: ignore + + # Legger inn begrensninger for at en person kun kan ha ett intervju på hvert tidspunkt + for applicant in applicants: + for interval in applicant.get_intervals(): + model += mip.xsum(m[(applicant, committee, interval)] + for committee in applicant.get_committees() + if (applicant, committee, interval) in m) <= 1 # type: ignore # Setter mål til å være maksimering av antall møter model.objective = mip.maximize(mip.xsum(m.values())) From 22ebe24df216f21f9eef0a36a08f3025387c50c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 17:32:01 +0100 Subject: [PATCH 10/48] test: add constraint test --- algorithm/mip_matching/tests/mip_test.py | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index f7adbc23..391d9b90 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -81,6 +81,29 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me class MipTest(unittest.TestCase): + def check_constraints(self, matchings: list[tuple[Applicant, Committee, TimeInterval]]): + """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}") + def test_fixed_small(self): """Small, fixed test with all capacities set to one""" @@ -126,5 +149,7 @@ def test_fixed_small(self): self.assertEqual(expected_number_of_matched_meetings, match["matched_meetings"]) + self.check_constraints(matchings=match["matchings"]) + def test_randomized_large(self): \ No newline at end of file From 39101002a9e753f8e6adf7cc4e6ea4b813f23888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 17:33:44 +0100 Subject: [PATCH 11/48] test: add large randomized test --- algorithm/README.md | 7 +++ algorithm/mip_matching/Applicant.py | 3 + algorithm/mip_matching/tests/fixed_test.py | 67 ---------------------- algorithm/mip_matching/tests/mip_test.py | 58 ++++++++++++++++++- 4 files changed, 66 insertions(+), 69 deletions(-) delete mode 100644 algorithm/mip_matching/tests/fixed_test.py diff --git a/algorithm/README.md b/algorithm/README.md index af9f9141..4cb42e83 100644 --- a/algorithm/README.md +++ b/algorithm/README.md @@ -15,3 +15,10 @@ Lag så en fil i `.\.venv\Lib\site-packages` som slutter på `.pth` og inneholde .\.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é.) diff --git a/algorithm/mip_matching/Applicant.py b/algorithm/mip_matching/Applicant.py index 85ed4bb8..a09abf39 100644 --- a/algorithm/mip_matching/Applicant.py +++ b/algorithm/mip_matching/Applicant.py @@ -36,6 +36,9 @@ 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* diff --git a/algorithm/mip_matching/tests/fixed_test.py b/algorithm/mip_matching/tests/fixed_test.py deleted file mode 100644 index 043c2b6e..00000000 --- a/algorithm/mip_matching/tests/fixed_test.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -TODO: -- [Gjort] Lage funksjon som deler opp fra en komités slot -- 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é.) -""" - -from __future__ import annotations -from mip_matching.TimeInterval import TimeInterval -from mip_matching.Applicant import Applicant -from mip_matching.Committee import Committee - -import sys -print(sys.path) - - -# personer_og_tidsslots = {"Jørgen": {TimeInterval(1, 4)}, -# "Sindre": {TimeInterval(2, 3), TimeInterval(4, 6)}, -# "Julian": {TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}, -# "Fritz": {TimeInterval(1, 2), TimeInterval(4, 5)}} -# komiteer_og_tidsslots = {"Appkom": {TimeInterval(1, 2), TimeInterval(2, 3), TimeInterval(3, 4), TimeInterval(4, 5)}, -# "OIL": {TimeInterval(4, 5), TimeInterval(5, 6)}, -# "Prokom": {TimeInterval(1, 3), TimeInterval(4, 6)}} - -# komite_kapasiteter = {"Appkom": {TimeInterval(1, 2): 1, -# TimeInterval(2, 3): 1, -# TimeInterval(3, 4): 1, -# TimeInterval(4, 5): 1}, -# "OIL": {TimeInterval(4, 5): 1, -# TimeInterval(5, 6): 1}, -# "Prokom": {TimeInterval(1, 3): 1, -# TimeInterval(4, 6): 1}} - - -# personer = personer_og_tidsslots.keys() -# komiteer = komiteer_og_tidsslots.keys() - -# intervjulengder = {"Appkom": 1, -# "Prokom": 2, -# "OIL": 1} - -# komiteer_per_person = {"Jørgen": {"Appkom", "Prokom"}, -# "Sindre": {"Appkom", "OIL"}, -# "Julian": {"Appkom", "Prokom", "OIL"}, -# "Fritz": {"OIL"}} - - -""" -Tror det følgende er gammel kode som ble brukt for CSP. -""" - -# # Variables -# variables = set() -# for person in komiteer_per_person: -# for komite in komiteer_per_person[person]: -# variables.add((person, komite)) -# # Domains -# domains = {var: personer_og_tidsslots[var[0]].intersection( -# komiteer_og_tidsslots[var[1]]) for var in variables} - -# constraints = {var: set() for var in variables} -# for var, annen in itertools.permutations(variables, 2): -# if annen[0] == var[0]: -# constraints[var].add(annen) -# if annen[1] == var[1]: -# constraints[var].add(annen) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index 391d9b90..f38557e1 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -2,7 +2,6 @@ from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee -from mip_matching.tests.fixed_test import applicants, committees from mip_matching.Applicant import Applicant import mip @@ -152,4 +151,59 @@ def test_fixed_small(self): self.check_constraints(matchings=match["matchings"]) def test_randomized_large(self): - \ No newline at end of file + """ + Tests a randomized selection of applicants, committees and slots. + All committees have a capacity of one. + + This test is without asserts, and mostly to test performance. + """ + + import random + + START_TID = 0 + # Hadde -1 her, men husker ikke hvorfor, så har fjernet det inntil videre. + SLUTT_TID = 10*5*2 + ANTALL_PERSONER = 400 + + ANTALL_SLOTS_PER_PERSON_MIN = 10 + ANTALL_SLOTS_PER_PERSON_MAKS = 20 + + ANTALL_SLOTS_PER_KOMITE_MIN = 5*5*2 + ANTALL_SLOTS_PER_KOMITE_MAKS = 8*5*2 + + ANTALL_KOMITEER_PER_PERSON_MIN = 1 + ANTALL_KOMITEER_PER_PERSON_MAKS = 3 + + komite_navn = {"Appkom", "Prokom", "Arrkom", "Dotkom", "Bankkom", + "OIL", "Fagkom", "Bedkom", "FemInIT", "Backlog", "Trikom"} + committees: set[Committee] = { + Committee(name=navn) 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(TimeInterval(start_tid, start_tid+1) for start_tid in set((random.sample(range( + START_TID, SLUTT_TID+1), random.randint(ANTALL_SLOTS_PER_PERSON_MIN, ANTALL_SLOTS_PER_PERSON_MAKS)))))) + + applicants.add(applicant) + + # Gir intervaller til hver komité. + for committee in committees: + committee.add_intervals_with_capacities({TimeInterval(start_tid, start_tid + 1): 1 for start_tid in (random.sample(range( + START_TID, SLUTT_TID+1), random.randint(ANTALL_SLOTS_PER_KOMITE_MIN, ANTALL_SLOTS_PER_KOMITE_MAKS)))}) + + # 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)))) # type: ignore + + 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']}") From 9aa14c290468bd0730ec2a19042c3dfcdefdc296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 14 Apr 2024 15:26:25 +0200 Subject: [PATCH 12/48] config: la til requirements for venv --- algorithm/requirements.txt | Bin 0 -> 90 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 algorithm/requirements.txt diff --git a/algorithm/requirements.txt b/algorithm/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..a910af925a406ea85a4f357cf4d7689b597af9b5 GIT binary patch literal 90 zcmezWFPR|?2s0UMfzXga4~R{{BrgLOLoQIf08Pe#0VGqvP|1)CBoi5m7>XHEfh0)3 M5m2WQ&;*bQ0KzB@asU7T literal 0 HcmV?d00001 From 8a70f1330d4e3aab57f54ffda96acc5092a3ad41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 14 Apr 2024 17:41:47 +0200 Subject: [PATCH 13/48] fix: skrevet om til at timeInterval bruker datetime --- algorithm/mip_matching/Committee.py | 5 +- algorithm/mip_matching/TimeInterval.py | 26 ++++-- algorithm/mip_matching/tests/ApplicantTest.py | 20 +++-- algorithm/mip_matching/tests/CommitteeTest.py | 14 +-- .../mip_matching/tests/TimeIntervalTest.py | 85 +++++++++++++------ 5 files changed, 104 insertions(+), 46 deletions(-) diff --git a/algorithm/mip_matching/Committee.py b/algorithm/mip_matching/Committee.py index e81b1645..9a43d7c7 100644 --- a/algorithm/mip_matching/Committee.py +++ b/algorithm/mip_matching/Committee.py @@ -1,4 +1,5 @@ from __future__ import annotations +from datetime import timedelta from mip_matching.Applicant import Applicant @@ -22,9 +23,9 @@ class Committee: aksessere på. """ - def __init__(self, name: str, interview_length: int = 1): + def __init__(self, name: str, interview_length: timedelta = timedelta(minutes=15)): self.capacities: dict[TimeInterval, int] = dict() - self.interview_length: int = interview_length + self.interview_length: timedelta = interview_length self.applicants: set[Applicant] = set() self.name = name diff --git a/algorithm/mip_matching/TimeInterval.py b/algorithm/mip_matching/TimeInterval.py index 6fcd7b78..0c8296ca 100644 --- a/algorithm/mip_matching/TimeInterval.py +++ b/algorithm/mip_matching/TimeInterval.py @@ -1,5 +1,8 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime +from datetime import timedelta +from typing import Any @dataclass(frozen=True) @@ -7,19 +10,24 @@ class TimeInterval: """ Definerer et tidsintervall fra og med start til og uten end. """ - start: int - end: int + start: datetime + end: datetime + + def __post_init__(self) -> None: + """Metode som sikrer at start og end er av type datetime.""" + if not (isinstance(self.start, datetime) and isinstance(self.end, datetime)): + raise ValueError("Start and end must be of type datetime.") def overlaps(self, other: TimeInterval) -> bool: - """Returnerer true om to timeslots er helt eller delvis overlappende.""" + """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 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 intersection(self, other: TimeInterval) -> TimeInterval: - """Returnerer et snitt av to timeslots.""" + def intersection(self, other: TimeInterval) -> TimeInterval | None: + """Returnerer et snitt av to tidsintervaller.""" if not self.overlaps(other): # Snittet er tomt grunnet ingen overlapp return None @@ -29,15 +37,15 @@ def intersection(self, other: TimeInterval) -> TimeInterval: return TimeInterval(start, end) def get_contained_slots(self, slots: list[TimeInterval]): - """Returnerer en delmengde av de slots i listen av timeslots - "slots", som inngår helt i dette timeslottet.""" + """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: int) -> list[TimeInterval]: + def divide(self, length: timedelta) -> list[TimeInterval]: return TimeInterval.divide_interval(self, length) @staticmethod - def divide_interval(interval: TimeInterval, length: int) -> list[TimeInterval]: + def divide_interval(interval: TimeInterval, length: timedelta) -> list[TimeInterval]: """ Deler opp et intervall i mindre intervaller av lengde *length*. diff --git a/algorithm/mip_matching/tests/ApplicantTest.py b/algorithm/mip_matching/tests/ApplicantTest.py index 5d99cd06..cf66737c 100644 --- a/algorithm/mip_matching/tests/ApplicantTest.py +++ b/algorithm/mip_matching/tests/ApplicantTest.py @@ -1,4 +1,5 @@ from __future__ import annotations +from datetime import datetime, timedelta import unittest from mip_matching.TimeInterval import TimeInterval from mip_matching.Applicant import Applicant @@ -7,18 +8,25 @@ class ApplicantTest(unittest.TestCase): def setUp(self) -> None: - self.committee = Committee("TestKom", interview_length=2) + self.committee = Committee( + "TestKom", interview_length=timedelta(minutes=30)) self.committee.add_intervals_with_capacities({ - TimeInterval(0, 6): 1, - TimeInterval(4, 6): 1 + 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 }) def test_get_fitting_committee_slots(self) -> None: test_applicant = Applicant("Test Testesen") - test_applicant.add_interval(TimeInterval(-2, 8)) + test_applicant.add_interval(TimeInterval(datetime(2024, 8, 24, 7, 30), + datetime(2024, 8, 24, 10, 0))) test_applicant.get_fitting_committee_slots(self.committee) - self.assertEqual(set([TimeInterval(0, 2), TimeInterval(2, 4), TimeInterval( - 4, 6)]), 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))]), + test_applicant.get_fitting_committee_slots(self.committee)) diff --git a/algorithm/mip_matching/tests/CommitteeTest.py b/algorithm/mip_matching/tests/CommitteeTest.py index 64260456..4d63ac24 100644 --- a/algorithm/mip_matching/tests/CommitteeTest.py +++ b/algorithm/mip_matching/tests/CommitteeTest.py @@ -1,4 +1,5 @@ from __future__ import annotations +from datetime import datetime, timedelta import unittest from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee @@ -6,12 +7,15 @@ class ApplicantTest(unittest.TestCase): def setUp(self) -> None: - self.committee = Committee("TestKom", interview_length=2) + self.committee = Committee( + "TestKom", interview_length=timedelta(minutes=30)) self.committee.add_intervals_with_capacities({ - TimeInterval(0, 6): 1, - TimeInterval(2, 6): 1 + TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 30)): 1, + TimeInterval(datetime(2024, 8, 24, 8, 30), datetime(2024, 8, 24, 9, 30)): 1 }) def test_capacity_stacking(self) -> None: - self.assertEqual(1, self.committee.get_capacity(TimeInterval(0, 2))) - self.assertEqual(2, self.committee.get_capacity(TimeInterval(2, 4))) + self.assertEqual(1, self.committee.get_capacity( + TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 30)))) + self.assertEqual(2, self.committee.get_capacity( + TimeInterval(datetime(2024, 8, 24, 8, 30), datetime(2024, 8, 24, 9, 0)))) diff --git a/algorithm/mip_matching/tests/TimeIntervalTest.py b/algorithm/mip_matching/tests/TimeIntervalTest.py index 21646d08..fa68d29d 100644 --- a/algorithm/mip_matching/tests/TimeIntervalTest.py +++ b/algorithm/mip_matching/tests/TimeIntervalTest.py @@ -1,63 +1,100 @@ 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(0, 6) + self.interval = TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 9, 30)) def test_overlapping(self): - interval1: TimeInterval = TimeInterval(0, 2) - interval2: TimeInterval = TimeInterval(1, 3) + + 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.overlaps(interval2)) def test_overlapping_edge(self): - interval1: TimeInterval = TimeInterval(0, 1) - interval2: TimeInterval = TimeInterval(1, 2) + 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.overlaps(interval2)) - interval3: TimeInterval = TimeInterval(0, 2) + interval3: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 8, 30)) self.assertTrue(interval1.overlaps(interval3)) def test_division(self): - actual_division = self.interval.divide(2) - expected_division = [TimeInterval( - 0, 2), TimeInterval(2, 4), TimeInterval(4, 6)] + 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(0, 4))) - self.assertTrue(self.interval.contains(TimeInterval(0, 6))) - self.assertTrue(self.interval.contains(TimeInterval(4, 6))) - self.assertTrue(self.interval.contains(TimeInterval(2, 4))) + 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(-1, 2))) - self.assertFalse(self.interval.contains(TimeInterval(-1, 7))) - self.assertFalse(self.interval.contains(TimeInterval(2, 7))) + 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( - 4, 6), self.interval.intersection(TimeInterval(4, 7))) + 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( - 4, 6), self.interval.intersection(TimeInterval(4, 6))) + 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( - 4, 5), self.interval.intersection(TimeInterval(4, 5))) + 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( - 0, 5), self.interval.intersection(TimeInterval(-5, 5))) + 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_get_contained_slots(self): - test_case_slots = [TimeInterval(-1, 2), TimeInterval(0, 2), - TimeInterval(1, 4), TimeInterval(4, 8), TimeInterval(3, 6)] + 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( - 0, 2), TimeInterval(1, 4), TimeInterval(3, 6)] + 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)) From 66ac457d0683ab81a7d15bf510ae54b4ba8fbbb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 14 Apr 2024 17:42:59 +0200 Subject: [PATCH 14/48] =?UTF-8?q?fix:=20fikset=20constraint=20for=20at=20e?= =?UTF-8?q?n=20person=20kun=20kan=20ha=20ett=20intervju=20p=C3=A5=20hvert?= =?UTF-8?q?=20tidspunkt=20og=20refactoret=20litt=20kode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- algorithm/mip_matching/match_meetings.py | 88 ++++++++++++ algorithm/mip_matching/tests/mip_test.py | 166 +++++++++++------------ 2 files changed, 165 insertions(+), 89 deletions(-) create mode 100644 algorithm/mip_matching/match_meetings.py diff --git a/algorithm/mip_matching/match_meetings.py b/algorithm/mip_matching/match_meetings.py new file mode 100644 index 00000000..b8505eaa --- /dev/null +++ b/algorithm/mip_matching/match_meetings.py @@ -0,0 +1,88 @@ +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 typing import TypedDict + + +class MeetingMatch(TypedDict): + """Type definition of a meeting match object""" + solver_status: mip.OptimizationStatus + matched_meetings: int + total_wanted_meetings: int + matchings: list[tuple[Applicant, Committee, TimeInterval]] + + +def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> MeetingMatch: + """Matches meetings and returns a MeetingMatch-object""" + model = mip.Model(sense=mip.MAXIMIZE) + + m = {} + + # Lager alle maksimeringsvariabler + for applicant in applicants: + for committee in applicant.get_committees(): + for interval in applicant.get_fitting_committee_slots(committee): + m[(applicant, committee, interval)] = model.add_var( + var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval})") + + # 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)] + for applicant in committee.get_applicants() + if (applicant, committee, interval) in m) <= capacity # type: ignore + + # 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)] + for interval in applicant.get_fitting_committee_slots(committee)) <= 1 # type: ignore + + # Legger inn begrensninger for at en person kun kan ha ett intervju på hvert tidspunkt + for applicant in applicants: + potential_intervals = set() + for applicant_candidate, committee, interval in m: + if applicant == applicant_candidate: + potential_intervals.add(interval) + + for interval in potential_intervals: + + model += mip.xsum(m[(applicant, committee, interval)] + for committee in applicant.get_committees() + if (applicant, committee, interval) in m) <= 1 # type: ignore + + # print(f"{applicant}, {interval}:") + # for committee in applicant.get_committees(): + # if (applicant, committee, interval) in m: + # print(f"\t {(applicant, committee, interval)}") + + # Setter mål til å være maksimering av antall møter + model.objective = mip.maximize(mip.xsum(m.values())) + + # 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) + print(f"{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/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index f38557e1..1eb8cd3a 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -1,89 +1,48 @@ from __future__ import annotations +from datetime import datetime, timedelta from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee from mip_matching.Applicant import Applicant import mip +from mip_matching.match_meetings import match_meetings + from typing import TypedDict import unittest -class MeetingMatch(TypedDict): - """Type definition of a meeting match object""" - solver_status: mip.OptimizationStatus - matched_meetings: int - total_wanted_meetings: int - matchings: list[tuple[Applicant, Committee, TimeInterval]] - - -def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> MeetingMatch: - """Matches meetings and returns a MeetingMatch-object""" - model = mip.Model(sense=mip.MAXIMIZE) - - m = {} - - # Lager alle maksimeringsvariabler - for applicant in applicants: - for committee in applicant.get_committees(): - for interval in applicant.get_fitting_committee_slots(committee): - m[(applicant, committee, interval)] = model.add_var( - var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval})") - - # 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)] - for applicant in committee.get_applicants() - if (applicant, committee, interval) in m) <= capacity # type: ignore - - # 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)] - for interval in applicant.get_fitting_committee_slots(committee)) <= 1 # type: ignore - - # Legger inn begrensninger for at en person kun kan ha ett intervju på hvert tidspunkt - for applicant in applicants: - for interval in applicant.get_intervals(): - model += mip.xsum(m[(applicant, committee, interval)] - for committee in applicant.get_committees() - if (applicant, committee, interval) in m) <= 1 # type: ignore +def print_matchings(committees: list[Committee], intervals: list[TimeInterval], matchings: list[tuple[Applicant, Committee, TimeInterval]]): - # Setter mål til å være maksimering av antall møter - model.objective = mip.maximize(mip.xsum(m.values())) + print("Tid".ljust(15), end="|") + print("|".join(str(com).ljust(8) for com in committees)) - # Kjør optimeringen - solver_status = model.optimize() + for interval in intervals: + print(interval.start.strftime("%d.%m %H:%M").ljust(15), end="|") + print() - # 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) - print(f"{name}") + # for komite in solution: + # print(komite.ljust(8), end="|") + # print("|".join([str(slot).rjust(2) for slot in solution[komite]])) - 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 + # print("|".join(["Slot"] + [komite.rjust(8) for komite in komiteer])) + # for slot in solution2: + # print(str(slot).ljust(4), end="|") + # print("|".join([str(solution2[slot][komite]).rjust(8) + # for komite in solution2[slot]])) class MipTest(unittest.TestCase): + def check_constraints(self, matchings: list[tuple[Applicant, Committee, TimeInterval]]): """Checks if the constraints are satisfied in the provided matchings. TODO: Add more constraint tests.""" + print("Matchings:") + for matching in matchings: + print(matching) + self.assertEqual(len(matchings), len(set((applicant, interval) for applicant, _, interval in matchings)), "Constraint \"Applicant can only have one meeting during each TimeInterval\" failed.") @@ -107,33 +66,43 @@ def test_fixed_small(self): """Small, fixed test with all capacities set to one""" appkom = Committee(name="Appkom") - appkom.add_intervals_with_capacities({TimeInterval(1, 5): 1}) + appkom.add_intervals_with_capacities( + {TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 15)): 1}) oil = Committee(name="OIL") - oil.add_intervals_with_capacities({TimeInterval(4, 6): 1}) + oil.add_intervals_with_capacities( + {TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)): 1}) prokom = Committee(name="Prokom") - prokom.add_intervals_with_capacities({TimeInterval(1, 3): 1, - TimeInterval(4, 6): 1}) + prokom.add_intervals_with_capacities({TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 45)): 1, + TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)): 1}) committees: set[Committee] = {appkom, oil, prokom} jørgen: Applicant = Applicant(name="Jørgen") jørgen.add_committees({appkom, prokom}) - jørgen.add_intervals({TimeInterval(1, 4)}) + 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(2, 3), TimeInterval(4, 6)}) + 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(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) + {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(1, 2), TimeInterval(4, 5)}) + 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} @@ -151,28 +120,45 @@ def test_fixed_small(self): 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 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]): """ - Tests a randomized selection of applicants, committees and slots. - All committees have a capacity of one. + Tester tilfeldige utvalg av søkere, komitéer og tidsintervaller. + Alle komitéer har en kapasitet lik 1. - This test is without asserts, and mostly to test performance. + Tester først og fremst performance. + TODO: Legg til flere asserts. """ import random - START_TID = 0 - # Hadde -1 her, men husker ikke hvorfor, så har fjernet det inntil videre. - SLUTT_TID = 10*5*2 - ANTALL_PERSONER = 400 + DEFAULT_INTERVIEW_TIME = timedelta(minutes=15) - ANTALL_SLOTS_PER_PERSON_MIN = 10 - ANTALL_SLOTS_PER_PERSON_MAKS = 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_SLOTS_PER_KOMITE_MIN = 5*5*2 - ANTALL_SLOTS_PER_KOMITE_MAKS = 8*5*2 + ANTALL_PERSONER = antall_personer - ANTALL_KOMITEER_PER_PERSON_MIN = 1 - ANTALL_KOMITEER_PER_PERSON_MAKS = 3 + 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"} @@ -184,22 +170,22 @@ def test_randomized_large(self): 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(TimeInterval(start_tid, start_tid+1) for start_tid in set((random.sample(range( - START_TID, SLUTT_TID+1), random.randint(ANTALL_SLOTS_PER_PERSON_MIN, ANTALL_SLOTS_PER_PERSON_MAKS)))))) + 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: - committee.add_intervals_with_capacities({TimeInterval(start_tid, start_tid + 1): 1 for start_tid in (random.sample(range( - START_TID, SLUTT_TID+1), random.randint(ANTALL_SLOTS_PER_KOMITE_MIN, ANTALL_SLOTS_PER_KOMITE_MAKS)))}) + committee.add_intervals_with_capacities({slot: 1 for slot in random.sample( + SLOTS, random.randint(ANTALL_SLOTS_PER_KOMITE_MIN, ANTALL_SLOTS_PER_KOMITE_MAKS))}) # 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)))) # type: ignore + ANTALL_KOMITEER_PER_PERSON_MIN, ANTALL_KOMITEER_PER_PERSON_MAKS)))) match = match_meetings(applicants=applicants, committees=committees) self.check_constraints(matchings=match["matchings"]) @@ -207,3 +193,5 @@ def test_randomized_large(self): 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"]) From cfe3617d358d36a53b3f4cc01e3eb49641b201d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Thu, 18 Apr 2024 15:42:33 +0200 Subject: [PATCH 15/48] feat: add support for union of timeinterval --- algorithm/mip_matching/TimeInterval.py | 25 +++++++++++++--- .../mip_matching/tests/TimeIntervalTest.py | 29 +++++++++++++++++-- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/algorithm/mip_matching/TimeInterval.py b/algorithm/mip_matching/TimeInterval.py index 0c8296ca..c6196a28 100644 --- a/algorithm/mip_matching/TimeInterval.py +++ b/algorithm/mip_matching/TimeInterval.py @@ -14,21 +14,38 @@ class TimeInterval: end: datetime def __post_init__(self) -> None: - """Metode som sikrer at start og end er av type datetime.""" + """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 ValueError("Start and end must be of type datetime.") + raise TypeError("Start and end must be of type datetime.") - def overlaps(self, other: TimeInterval) -> bool: + 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_tangent_to(other) or self.intersects(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 intersection(self, other: TimeInterval) -> TimeInterval | None: """Returnerer et snitt av to tidsintervaller.""" - if not self.overlaps(other): + if not self.intersects(other): # Snittet er tomt grunnet ingen overlapp return None diff --git a/algorithm/mip_matching/tests/TimeIntervalTest.py b/algorithm/mip_matching/tests/TimeIntervalTest.py index fa68d29d..41f6edef 100644 --- a/algorithm/mip_matching/tests/TimeIntervalTest.py +++ b/algorithm/mip_matching/tests/TimeIntervalTest.py @@ -11,6 +11,14 @@ 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): @@ -20,7 +28,7 @@ def test_overlapping(self): datetime(2024, 8, 24, 8, 15), datetime(2024, 8, 24, 8, 45)) - self.assertTrue(interval1.overlaps(interval2)) + self.assertTrue(interval1.intersects(interval2)) def test_overlapping_edge(self): interval1: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 0), @@ -28,12 +36,12 @@ def test_overlapping_edge(self): interval2: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 15), datetime(2024, 8, 24, 8, 30)) - self.assertFalse(interval1.overlaps(interval2)) + self.assertFalse(interval1.intersects(interval2)) interval3: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 30)) - self.assertTrue(interval1.overlaps(interval3)) + self.assertTrue(interval1.intersects(interval3)) def test_division(self): actual_division = self.interval.divide(timedelta(minutes=30)) @@ -77,6 +85,21 @@ def test_intersection(self): 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)), From c361957a0a89beba60eade68afb483a89d435c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Thu, 25 Apr 2024 15:05:52 +0200 Subject: [PATCH 16/48] feat: La til sanitizing i Applicant --- algorithm/mip_matching/Applicant.py | 23 ++++++++++++++ algorithm/mip_matching/TimeInterval.py | 5 ++- algorithm/mip_matching/tests/ApplicantTest.py | 31 +++++++++++++++---- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/algorithm/mip_matching/Applicant.py b/algorithm/mip_matching/Applicant.py index a09abf39..9c72ac2d 100644 --- a/algorithm/mip_matching/Applicant.py +++ b/algorithm/mip_matching/Applicant.py @@ -30,6 +30,29 @@ def add_committees(self, committees: set[Committee]) -> None: def add_interval(self, interval: TimeInterval) -> None: # TODO: Vurder å gjøre "sanitizing" ved å slå sammen overlappende intervaller. + """ + 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: diff --git a/algorithm/mip_matching/TimeInterval.py b/algorithm/mip_matching/TimeInterval.py index c6196a28..f2156d5e 100644 --- a/algorithm/mip_matching/TimeInterval.py +++ b/algorithm/mip_matching/TimeInterval.py @@ -32,7 +32,7 @@ def is_tangent_to(self, other: TimeInterval) -> bool: def union(self, other: TimeInterval) -> TimeInterval: """Returnerer union av tidsintervall dersom de to intervallene har overlapp eller er inntil hverandre""" - if not (self.is_tangent_to(other) or self.intersects(other)): + if not self.is_mergable(other): raise ValueError("Cannot have union with gaps between") start = min(self.start, other.start) @@ -43,6 +43,9 @@ 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): diff --git a/algorithm/mip_matching/tests/ApplicantTest.py b/algorithm/mip_matching/tests/ApplicantTest.py index cf66737c..a52d517b 100644 --- a/algorithm/mip_matching/tests/ApplicantTest.py +++ b/algorithm/mip_matching/tests/ApplicantTest.py @@ -15,13 +15,14 @@ def setUp(self) -> None: TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)): 1 }) - def test_get_fitting_committee_slots(self) -> None: - test_applicant = Applicant("Test Testesen") + self.test_applicant = Applicant("Test Testesen") + + self.test_applicant.add_interval(TimeInterval(datetime(2024, 8, 24, 7, 30), + datetime(2024, 8, 24, 10, 0))) - 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: - test_applicant.get_fitting_committee_slots(self.committee) + 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)), @@ -29,4 +30,22 @@ def test_get_fitting_committee_slots(self) -> None: datetime(2024, 8, 24, 9, 0)), TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30))]), - test_applicant.get_fitting_committee_slots(self.committee)) + 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)), + }) From 2f4305965fa7519f378db67d655a3a43649899e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 28 Apr 2024 14:30:10 +0200 Subject: [PATCH 17/48] test: la til test med sammenhengende intervaller og ulike kapasiteter --- algorithm/mip_matching/tests/mip_test.py | 134 ++++++++++++++++++++--- algorithm/requirements.txt | Bin 90 -> 208 bytes 2 files changed, 116 insertions(+), 18 deletions(-) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index 1eb8cd3a..56de36fc 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -1,5 +1,5 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date, time from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee @@ -10,27 +10,30 @@ from typing import TypedDict +from faker import Faker + import unittest +import random -def print_matchings(committees: list[Committee], intervals: list[TimeInterval], matchings: list[tuple[Applicant, Committee, TimeInterval]]): +def print_matchings(committees: list[Committee], + intervals: list[TimeInterval], + matchings: list[tuple[Applicant, Committee, TimeInterval]]): 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="|") - print() + for committee in committees: + name = "" + cands = [a.name for a, c, + i in matchings if interval == i and c == committee] + name = cands[0] if len(cands) > 0 else "" - # for komite in solution: - # print(komite.ljust(8), end="|") - # print("|".join([str(slot).rjust(2) for slot in solution[komite]])) + print(name.rjust(8), end="|") - # print("|".join(["Slot"] + [komite.rjust(8) for komite in komiteer])) - # for slot in solution2: - # print(str(slot).ljust(4), end="|") - # print("|".join([str(solution2[slot][komite]).rjust(8) - # for komite in solution2[slot]])) + print() class MipTest(unittest.TestCase): @@ -39,9 +42,9 @@ def check_constraints(self, matchings: list[tuple[Applicant, Committee, TimeInte """Checks if the constraints are satisfied in the provided matchings. TODO: Add more constraint tests.""" - print("Matchings:") - for matching in matchings: - print(matching) + # print("Matchings:") + # for matching in matchings: + # print(matching) self.assertEqual(len(matchings), len(set((applicant, interval) for applicant, _, interval in matchings)), @@ -62,6 +65,10 @@ def check_constraints(self, matchings: list[tuple[Applicant, Committee, TimeInte 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}") + def get_default_slots(self): + """ + Returnerer slots""" + def test_fixed_small(self): """Small, fixed test with all capacities set to one""" @@ -124,6 +131,99 @@ def test_randomized_large(self): 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_randomized_with_different_interview_sizes(self): + """ + Plan: + Lager flere komitéer med ulike intervjulengder + """ + pass + + 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) + + # ANTALL_INTERVALL_PER_PERSON_MIN = 5 + # ANTALL_INTERVALL_PER_PERSON_MAKS = 8 + # INTERVALLENGDE_PER_PERSON_MIN = timedelta(minutes=30) + 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 + # INTERVALLENGDE_PER_KOMTIE_MAKS = timedelta(hours=10) + ANTALL_INTERVALL_FORSØK_KOMITE = 10 + + ANTALL_KOMITEER_PER_PERSON_MIN = 2 + ANTALL_KOMITEER_PER_PERSON_MAKS = 3 + + # print([applicant.get_intervals() for applicant in applicants]) + + # 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) + + committee.add_intervals_with_capacities({get_random_interval(interval_date): random.randint( + KAPASITET_PER_INTERVALL_MIN, KAPASITET_PER_INTERVALL_MAKS)}) + + # 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], @@ -137,9 +237,7 @@ def randomized_test(self, TODO: Legg til flere asserts. """ - import random - - DEFAULT_INTERVIEW_TIME = timedelta(minutes=15) + DEFAULT_INTERVIEW_TIME = timedelta(minutes=20) SLOTS: list[TimeInterval] = [] for dag in range(0, 5): @@ -163,7 +261,7 @@ def randomized_test(self, komite_navn = {"Appkom", "Prokom", "Arrkom", "Dotkom", "Bankkom", "OIL", "Fagkom", "Bedkom", "FemInIT", "Backlog", "Trikom"} committees: set[Committee] = { - Committee(name=navn) for navn in komite_navn} + Committee(name=navn, interview_length=DEFAULT_INTERVIEW_TIME) for navn in komite_navn} # Gir tider til hver søker applicants: set[Applicant] = set() diff --git a/algorithm/requirements.txt b/algorithm/requirements.txt index a910af925a406ea85a4f357cf4d7689b597af9b5..349630b1bd346bcbe0b649e972a8500e82667c86 100644 GIT binary patch delta 123 zcma#Lz&Jrp+KnNRA)6tUp@_j22#pv_81xtnfl!aZV4|dBSOG&NLkU9$Lq07&Sp|qMZ!@4qgJ~ From b03f0f4d6e949521ee3e7d08df64df4033abdb2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 5 May 2024 13:37:14 +0200 Subject: [PATCH 18/48] test(algorithm): add realistic test --- algorithm/mip_matching/tests/mip_test.py | 91 ++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index 56de36fc..c517f374 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -138,6 +138,97 @@ def test_randomized_with_different_interview_sizes(self): """ pass + def test_realistic(self): + """ + En realistisk test (grovt) basert på historisk data. + """ + + 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( + ), 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) + + committee.add_intervals_with_capacities({get_random_interval(interval_date, INTERVALLENGDE_PER_KOMTIE_MIN, INTERVALLENGDE_PER_KOMTIE_MAKS): random.randint( + KAPASITET_PER_INTERVALL_MIN, KAPASITET_PER_INTERVALL_MAKS)}) + + # 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 From 4751993594495e30d9d09cd84684bb7ba739c698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 5 May 2024 13:44:42 +0200 Subject: [PATCH 19/48] docs: la til litt forklaringer til realistisk test --- algorithm/mip_matching/tests/mip_test.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index c517f374..ea07960c 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -140,7 +140,13 @@ def test_randomized_with_different_interview_sizes(self): def test_realistic(self): """ - En realistisk test (grovt) basert på historisk data. + 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() From 88d7f5396550955ddfad6f9925d5e0e1984f92ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 5 May 2024 13:46:14 +0200 Subject: [PATCH 20/48] refactor: fjernet ubrukt kode --- algorithm/mip_matching/tests/mip_test.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index ea07960c..7dc768d1 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -4,12 +4,9 @@ from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee from mip_matching.Applicant import Applicant -import mip from mip_matching.match_meetings import match_meetings -from typing import TypedDict - from faker import Faker import unittest @@ -42,10 +39,6 @@ def check_constraints(self, matchings: list[tuple[Applicant, Committee, TimeInte """Checks if the constraints are satisfied in the provided matchings. TODO: Add more constraint tests.""" - # print("Matchings:") - # for matching in matchings: - # print(matching) - self.assertEqual(len(matchings), len(set((applicant, interval) for applicant, _, interval in matchings)), "Constraint \"Applicant can only have one meeting during each TimeInterval\" failed.") @@ -65,10 +58,6 @@ def check_constraints(self, matchings: list[tuple[Applicant, Committee, TimeInte 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}") - def get_default_slots(self): - """ - Returnerer slots""" - def test_fixed_small(self): """Small, fixed test with all capacities set to one""" @@ -131,13 +120,6 @@ def test_randomized_large(self): 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_randomized_with_different_interview_sizes(self): - """ - Plan: - Lager flere komitéer med ulike intervjulengder - """ - pass - def test_realistic(self): """ En realistisk test (grovt) basert på historiske søkertall og info fra komitéer. @@ -248,9 +230,6 @@ def test_randomized_with_continuous_intervals(self): DEFAULT_INTERVIEW_TIME = timedelta(minutes=20) - # ANTALL_INTERVALL_PER_PERSON_MIN = 5 - # ANTALL_INTERVALL_PER_PERSON_MAKS = 8 - # INTERVALLENGDE_PER_PERSON_MIN = timedelta(minutes=30) INTERVALLENGDE_PER_PERSON_MAKS = timedelta(hours=10) ANTALL_INTERVALL_FORSØK = 4 @@ -287,14 +266,11 @@ def get_random_interval(interval_date: date) -> TimeInterval: KAPASITET_PER_INTERVALL_MIN = 1 KAPASITET_PER_INTERVALL_MAKS = 3 - # INTERVALLENGDE_PER_KOMTIE_MAKS = timedelta(hours=10) ANTALL_INTERVALL_FORSØK_KOMITE = 10 ANTALL_KOMITEER_PER_PERSON_MIN = 2 ANTALL_KOMITEER_PER_PERSON_MAKS = 3 - # print([applicant.get_intervals() for applicant in applicants]) - # Gir intervaller til hver komité. committees: set[Committee] = { Committee(name=navn, interview_length=DEFAULT_INTERVIEW_TIME) for navn in {"Appkom", "Prokom", "Arrkom", "Dotkom", "Bankkom", From 324c6e0d2a0013fe4f41179d1fcd48ec799f99cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 5 May 2024 13:55:17 +0200 Subject: [PATCH 21/48] refactor: fjernet ubrukt kode --- algorithm/mip_matching/match_meetings.py | 5 ----- algorithm/mip_matching/tests/mip_test.py | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/algorithm/mip_matching/match_meetings.py b/algorithm/mip_matching/match_meetings.py index b8505eaa..3f8792a8 100644 --- a/algorithm/mip_matching/match_meetings.py +++ b/algorithm/mip_matching/match_meetings.py @@ -55,11 +55,6 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me for committee in applicant.get_committees() if (applicant, committee, interval) in m) <= 1 # type: ignore - # print(f"{applicant}, {interval}:") - # for committee in applicant.get_committees(): - # if (applicant, committee, interval) in m: - # print(f"\t {(applicant, committee, interval)}") - # Setter mål til å være maksimering av antall møter model.objective = mip.maximize(mip.xsum(m.values())) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index 7dc768d1..749d4585 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -144,7 +144,7 @@ def test_realistic(self): START_TIME_PER_DAY = time(hour=8, minute=0) END_TIME_PER_DAY = time(hour=18, minute=0) DAY_LENGTH = datetime.combine(date.today( - ), END_TIME_PER_DAY) - datetime.combine(date.today(), START_TIME_PER_DAY) + ), END_TIME_PER_DAY) - datetime.combine(date.today(), START_TIME_PER_DAY) # type: ignore 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) + \ From 04064b56f78f42fa1001ba475d19df36bedb339a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Thu, 9 May 2024 17:59:43 +0200 Subject: [PATCH 22/48] cleanup: fjernet csp-modul --- .../CSP_test.py | 86 ------------------- .../testing.py | 74 ---------------- algorithm/README.md | 2 +- 3 files changed, 1 insertion(+), 161 deletions(-) delete mode 100644 algorithm/Constraint Satisfaction Problem/CSP_test.py delete mode 100644 algorithm/Constraint Satisfaction Problem/testing.py diff --git a/algorithm/Constraint Satisfaction Problem/CSP_test.py b/algorithm/Constraint Satisfaction Problem/CSP_test.py deleted file mode 100644 index a378a849..00000000 --- a/algorithm/Constraint Satisfaction Problem/CSP_test.py +++ /dev/null @@ -1,86 +0,0 @@ -class CSP: - def __init__(self, variables, Domains,constraints): - self.variables = variables - self.domains = Domains - self.constraints = constraints - self.solution = None - - def solve(self): - print("Test") - assignment = {} - self.solution = self.backtrack(assignment) - return self.solution - - def backtrack(self, assignment): - print(f"{len(assignment)=}\r", end="") - if len(assignment) == len(self.variables): - return assignment - - var = self.select_unassigned_variable(assignment) - for value in self.order_domain_values(var, assignment): - if self.is_consistent(var, value, assignment): - assignment[var] = value - result = self.backtrack(assignment) - if result is not None: - return result - del assignment[var] - return None - - def select_unassigned_variable(self, assignment): - unassigned_vars = [var for var in self.variables if var not in assignment] - return min(unassigned_vars, key=lambda var: len(self.domains[var])) - - def order_domain_values(self, var, assignment): - return self.domains[var] - - def is_consistent(self, var, value, assignment): - for constraint_var in self.constraints[var]: - if constraint_var in assignment and assignment[constraint_var] == value: - return False - return True - - - - -personer_og_tidsslots = {"Jørgen": {1, 2, 3, 4}, "Sindre": {2, 4, 5, 6}, "Julian": {3, 1, 6}, "Fritz": {1, 4}} -komiteer_og_tidsslots = {"Appkom": {1, 2, 3, 4, 5}, "OIL": {4, 5, 6}, "Prokom": {1, 2, 3, 5}} - -timeslots = range(1, 9) -personer = personer_og_tidsslots.keys() -komiteer = komiteer_og_tidsslots.keys() - -komiteer_per_person = {"Jørgen": {"Appkom", "Prokom"}, "Sindre": {"Appkom", "OIL"}, "Julian": {"Appkom", "Prokom", "OIL"}, "Fritz": {"OIL"}} - -# Variables -variables = set() -for person in komiteer_per_person: - for komite in komiteer_per_person[person]: - variables.add((person, komite)) -# Domains -domains = {var: personer_og_tidsslots[var[0]].intersection(komiteer_og_tidsslots[var[1]]) for var in variables} - -import itertools -constraints = {var: set() for var in variables} -for var, annen in itertools.permutations(variables, 2): - if annen[0] == var[0]: - constraints[var].add(annen) - if annen[1] == var[1]: - constraints[var].add(annen) - -# # Solution -csp = CSP(variables, domains, constraints) -sol = csp.solve() -print(sol) - - -solution = {komite: [None for _ in timeslots] for komite in komiteer} -for person, komite in sol: - solution[komite][sol[person, komite]] = person - -print("---------") -print(solution) -# print_sudoku(solution) - -for komite in solution: - print(komite.ljust(15), end="|") - print("|".join([str(slot).rjust(15) for slot in solution[komite]])) diff --git a/algorithm/Constraint Satisfaction Problem/testing.py b/algorithm/Constraint Satisfaction Problem/testing.py deleted file mode 100644 index 8d1392fd..00000000 --- a/algorithm/Constraint Satisfaction Problem/testing.py +++ /dev/null @@ -1,74 +0,0 @@ -from CSP_test import CSP -import random - -START_TID = 0 -SLUTT_TID = 10*5*2 - 1 -ANTALL_PERSONER = 210 - -ANTALL_SLOTS_PER_PERSON_MIN = 10 -ANTALL_SLOTS_PER_PERSON_MAKS = 20 - -ANTALL_SLOTS_PER_KOMITE_MIN = 5*5*2 -ANTALL_SLOTS_PER_KOMITE_MAKS = 8*5*2 - -ANTALL_KOMITEER_PER_PERSON_MIN = 1 -ANTALL_KOMITEER_PER_PERSON_MAKS = 3 - -komiteer = {"Appkom", "Prokom", "Arrkom", "Dotkom", "Bankkom", "OIL", "Fagkom", "Bedkom", "FemInIT", "Backlog", "Trikom"} - -personer_og_tidsslots = {} - -for person in range(ANTALL_PERSONER): - personer_og_tidsslots[person] = set((random.sample(range(START_TID, SLUTT_TID+1), random.randint(ANTALL_SLOTS_PER_PERSON_MIN, ANTALL_SLOTS_PER_PERSON_MAKS)))) - -komiteer_og_tidsslots = {} - -for komite in komiteer: - komiteer_og_tidsslots[komite] = set((random.sample(range(START_TID, SLUTT_TID+1), random.randint(ANTALL_SLOTS_PER_KOMITE_MIN, ANTALL_SLOTS_PER_KOMITE_MAKS)))) - - -personer = personer_og_tidsslots.keys() -komiteer = list(komiteer_og_tidsslots.keys()) - -komiteer_per_person = {person: random.sample(sorted(komiteer), random.randint(ANTALL_KOMITEER_PER_PERSON_MIN, ANTALL_KOMITEER_PER_PERSON_MAKS)) for person in personer} - -print(personer_og_tidsslots) -print(komiteer_og_tidsslots) - -variables = set() -for person in komiteer_per_person: - for komite in komiteer_per_person[person]: - variables.add((person, komite)) -# Domains -domains = {var: personer_og_tidsslots[var[0]].intersection(komiteer_og_tidsslots[var[1]]) for var in variables} - -import itertools -constraints = {var: set() for var in variables} -for var, annen in itertools.permutations(variables, 2): - if annen[0] == var[0]: - constraints[var].add(annen) - if annen[1] == var[1]: - constraints[var].add(annen) - -csp = CSP(variables, domains, constraints) -sol = csp.solve() -if sol is None: - print("Ingen løsning") - exit() -print(sol) - - -solution = {komite: [-1 for _ in range(START_TID, SLUTT_TID+1)] for komite in komiteer} -solution2 = {slot: {komite: "Tom" if slot in komiteer_og_tidsslots[komite] else "-" for komite in komiteer} for slot in range(START_TID, SLUTT_TID+1)} -for person, komite in sol: - solution[komite][sol[person, komite]] = person - solution2[sol[person, komite]][komite] = person - -for komite in solution: - print(komite.ljust(8), end="|") - print("|".join([str(slot).rjust(2) for slot in solution[komite]])) - -print("|".join(["Slot"] + [komite.rjust(8) for komite in komiteer])) -for slot in solution2: - print(str(slot).ljust(4), end="|") - print("|".join([str(solution2[slot][komite]).rjust(8) for komite in solution2[slot]])) \ No newline at end of file diff --git a/algorithm/README.md b/algorithm/README.md index 4cb42e83..a7d5f291 100644 --- a/algorithm/README.md +++ b/algorithm/README.md @@ -1,6 +1,6 @@ # Algoritme -Prøvde først med CSP-algoritme, men har nå gått over til MIP-programmering (Mixd Integer Linear Programming). +Algoritmen baserer seg på MIP-programmering (Mixed Integer Linear Programming). ## Setup Python Venv From 9bd9ded1a9f3a00fb34f465a1d3c62051b867946 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sat, 20 Jul 2024 19:06:23 +0200 Subject: [PATCH 23/48] added committe card --- components/committee/CommitteApplicants.tsx | 25 ++- components/committee/CommitteeCard.tsx | 22 +++ .../[period-id]/[committee]/index.tsx | 140 +++++++++++++++++ pages/committee/[period-id]/index.tsx | 143 +++++------------- 4 files changed, 220 insertions(+), 110 deletions(-) create mode 100644 components/committee/CommitteeCard.tsx create mode 100644 pages/committee/[period-id]/[committee]/index.tsx diff --git a/components/committee/CommitteApplicants.tsx b/components/committee/CommitteApplicants.tsx index 2a4cd078..1cf377f3 100644 --- a/components/committee/CommitteApplicants.tsx +++ b/components/committee/CommitteApplicants.tsx @@ -28,6 +28,29 @@ const CommitteeApplicants: NextPage = ({ routeString }) => { setPeriods( filteredPeriods.map((period: periodType) => { + const userCommittees = session?.user?.committees?.map((committee) => + committee.toLowerCase() + ); + const periodCommittees = period.committees.map((committee) => + committee.toLowerCase() + ); + + period.optionalCommittees.forEach((committee) => { + periodCommittees.push(committee.toLowerCase()); + }); + + const commonCommittees = userCommittees!.filter((committee) => + periodCommittees.includes(committee) + ); + + let uriLink = ""; + + if (commonCommittees.length > 1) { + uriLink = `committee/${period._id}`; + } else { + uriLink = `committee/${period._id}/${commonCommittees[0]}`; + } + return { name: period.name, preparation: @@ -43,7 +66,7 @@ const CommitteeApplicants: NextPage = ({ routeString }) => { " til " + formatDate(period.interviewPeriod.end), committees: period.committees, - link: `committee/${period._id}`, + link: uriLink, }; }) ); diff --git a/components/committee/CommitteeCard.tsx b/components/committee/CommitteeCard.tsx new file mode 100644 index 00000000..774ff464 --- /dev/null +++ b/components/committee/CommitteeCard.tsx @@ -0,0 +1,22 @@ +import router from "next/router"; +import React from "react"; + +interface Props { + committee: string; + link: string; +} + +const CommitteeCard = ({ committee, link }: Props) => { + return ( + + ); +}; + +export default CommitteeCard; diff --git a/pages/committee/[period-id]/[committee]/index.tsx b/pages/committee/[period-id]/[committee]/index.tsx new file mode 100644 index 00000000..d4b4f133 --- /dev/null +++ b/pages/committee/[period-id]/[committee]/index.tsx @@ -0,0 +1,140 @@ +import { NextPage } from "next"; +import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; +import { applicantType, periodType } from "../../../../lib/types/types"; +import { useRouter } from "next/router"; +import ApplicantsOverview from "../../../../components/applicantoverview/ApplicantsOverview"; +import { + CalendarIcon, + InboxIcon, + UserGroupIcon, +} from "@heroicons/react/24/solid"; +import { Tabs } from "../../../../components/Tabs"; +import SendCommitteeMessage from "../../../../components/committee/SendCommitteeMessage"; +import CommitteeInterviewTimes from "../../../../components/committee/CommitteeInterviewTimes"; +import LoadingPage from "../../../../components/LoadingPage"; + +const CommitteeApplicantOverView: NextPage = () => { + const { data: session } = useSession(); + const [loading, setLoading] = useState(true); + + const router = useRouter(); + const periodId = router.query["period-id"] as string; + const [committees, setCommittees] = useState(null); + const [period, setPeriod] = useState(null); + const [activeTab, setActiveTab] = useState(0); + const [tabClicked, setTabClicked] = useState(0); + + useEffect(() => { + if (!session || !periodId) return; + + const fetchPeriod = async () => { + try { + const res = await fetch(`/api/periods/${periodId}`); + const data = await res.json(); + setPeriod(data.period); + } catch (error) { + console.error("Failed to fetch interview periods:", error); + } finally { + setLoading(false); + } + }; + + fetchPeriod(); + }, [session, periodId]); + + useEffect(() => { + if (period && session) { + const userCommittees = session.user!.committees; + const periodCommittees = period.committees; + + if (period.optionalCommittees != null) { + periodCommittees.push(...period.optionalCommittees); + } + + const filteredCommittees = periodCommittees.filter( + (committee) => userCommittees?.includes(committee.toLowerCase()) + ); + setCommittees(filteredCommittees); + } + }, [period, session]); + + if (!session || !session.user?.isCommitee) { + return

Ingen Tilgang!

; + } + + if (loading) { + return ; + } + + const interviewPeriodEnd = period?.interviewPeriod.end + ? new Date(period.interviewPeriod.end) + : null; + + //Satt frist til 14 dager etter intervju perioden, så får man ikke tilgang + if ( + interviewPeriodEnd && + interviewPeriodEnd.getTime() + 14 * 24 * 60 * 60 * 1000 < + new Date().getTime() + ) { + return ( +
+

Opptaket er ferdig!

+
+

+ Du kan ikke lenger se søkere eller planlegge intervjuer. +

+

+ {" "} + Har det skjedd noe feil eller trenger du tilgang til informasjonen? Ta + kontakt med{" "} + + Appkom + {" "} +

+
+ ); + } + + return ( +
+ { + setActiveTab(index); + setTabClicked(index); + }} + content={[ + { + title: "Intervjutider", + icon: , + content: , + }, + { + title: "Melding", + icon: , + content: ( + + ), + }, + { + title: "Søkere", + icon: , + content: ( + + ), + }, + ]} + /> +
+ ); +}; + +export default CommitteeApplicantOverView; diff --git a/pages/committee/[period-id]/index.tsx b/pages/committee/[period-id]/index.tsx index 38bdd53c..3519c0ce 100644 --- a/pages/committee/[period-id]/index.tsx +++ b/pages/committee/[period-id]/index.tsx @@ -1,140 +1,65 @@ -import { NextPage } from "next"; +import { useRouter } from "next/router"; import { useEffect, useState } from "react"; +import { periodType } from "../../../lib/types/types"; import { useSession } from "next-auth/react"; -import { applicantType, periodType } from "../../../lib/types/types"; -import { useRouter } from "next/router"; -import ApplicantsOverview from "../../../components/applicantoverview/ApplicantsOverview"; -import { - CalendarIcon, - InboxIcon, - UserGroupIcon, -} from "@heroicons/react/24/solid"; -import { Tabs } from "../../../components/Tabs"; -import SendCommitteeMessage from "../../../components/committee/SendCommitteeMessage"; -import CommitteeInterviewTimes from "../../../components/committee/CommitteeInterviewTimes"; import LoadingPage from "../../../components/LoadingPage"; +import CommitteeCard from "../../../components/committee/CommitteeCard"; -const CommitteeApplicantOverView: NextPage = () => { +const ChooseCommittee = () => { const { data: session } = useSession(); - const [loading, setLoading] = useState(true); - const router = useRouter(); - const periodId = router.query["period-id"] as string; - const [committees, setCommittees] = useState(null); + const periodId = router.query["period-id"]; const [period, setPeriod] = useState(null); - const [activeTab, setActiveTab] = useState(0); - const [tabClicked, setTabClicked] = useState(0); + const [committees, setCommittees] = useState(null); + const [loading, setLoading] = useState(true); useEffect(() => { - if (!session || !periodId) return; - const fetchPeriod = async () => { + if (!session || !periodId) return; + try { const res = await fetch(`/api/periods/${periodId}`); const data = await res.json(); setPeriod(data.period); + + if (data.period) { + const userCommittees = session!.user!.committees; + const periodCommittees = data.period.committees; + + if (data.period.optionalCommittees != null) { + periodCommittees.push(...data.period.optionalCommittees); + } + + const filteredCommittees = periodCommittees.filter( + (committee: string) => + userCommittees?.includes(committee.toLowerCase()) + ); + setCommittees(filteredCommittees); + } } catch (error) { console.error("Failed to fetch interview periods:", error); } finally { setLoading(false); } }; - fetchPeriod(); - }, [session, periodId]); - - useEffect(() => { - if (period && session) { - const userCommittees = session.user!.committees; - const periodCommittees = period.committees; - - if (period.optionalCommittees != null) { - periodCommittees.push(...period.optionalCommittees); - } - - const filteredCommittees = periodCommittees.filter( - (committee) => userCommittees?.includes(committee.toLowerCase()) - ); - setCommittees(filteredCommittees); - } - }, [period, session]); - - if (!session || !session.user?.isCommitee) { - return

Ingen Tilgang!

; - } + }, [periodId, session]); if (loading) { return ; } - const interviewPeriodEnd = period?.interviewPeriod.end - ? new Date(period.interviewPeriod.end) - : null; - - //Satt frist til 14 dager etter intervju perioden, så får man ikke tilgang - if ( - interviewPeriodEnd && - interviewPeriodEnd.getTime() + 14 * 24 * 60 * 60 * 1000 < - new Date().getTime() - ) { - return ( -
-

Opptaket er ferdig!

-
-

- Du kan ikke lenger se søkere eller planlegge intervjuer. -

-

- {" "} - Har det skjedd noe feil eller trenger du tilgang til informasjonen? Ta - kontakt med{" "} - - Appkom - {" "} -

-
- ); - } - return ( -
- { - setActiveTab(index); - setTabClicked(index); - }} - content={[ - { - title: "Intervjutider", - icon: , - content: , - }, - { - title: "Melding", - icon: , - content: ( - - ), - }, - { - title: "Søkere", - icon: , - content: ( - - ), - }, - ]} - /> +
+

Velg komite

+ {committees?.map((committee) => + CommitteeCard({ + committee, + link: `${periodId}/${committee.toLowerCase()}`, + }) + )}
); }; -export default CommitteeApplicantOverView; +export default ChooseCommittee; From 6ae878f96e01aec4b2e16c268644460a54b692f6 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sat, 20 Jul 2024 19:59:11 +0200 Subject: [PATCH 24/48] remove committee selection in committeeInterviewTimes --- .../applicantoverview/ApplicantsOverview.tsx | 19 +----- .../committee/CommitteeInterviewTimes.tsx | 61 +++--------------- .../[period-id]/[committee]/index.tsx | 63 ++++++++++++------- 3 files changed, 49 insertions(+), 94 deletions(-) diff --git a/components/applicantoverview/ApplicantsOverview.tsx b/components/applicantoverview/ApplicantsOverview.tsx index bf05c862..264d2c72 100644 --- a/components/applicantoverview/ApplicantsOverview.tsx +++ b/components/applicantoverview/ApplicantsOverview.tsx @@ -11,7 +11,7 @@ import ApplicantOverviewSkeleton from "./ApplicantOverviewSkeleton"; interface Props { period: periodType | null; - committees: string[] | null; + committees: string; includePreferences: boolean; } @@ -206,23 +206,6 @@ const ApplicantsOverview = ({ ref={filterMenuRef} className="absolute right-0 top-10 w-48 bg-white dark:bg-online-darkBlue border border-gray-300 dark:border-gray-600 p-4 rounded shadow-lg z-10" > - {committees && ( -
- - -
- )}
- {filteredCommittees.map((committee) => ( - - ))} - -
-

Velg ledige tider ved å trykke på eller dra over flere celler.

Intervjuene vil bli satt opp etter hverandre fra første ledige diff --git a/pages/committee/[period-id]/[committee]/index.tsx b/pages/committee/[period-id]/[committee]/index.tsx index d4b4f133..4e9e5ad6 100644 --- a/pages/committee/[period-id]/[committee]/index.tsx +++ b/pages/committee/[period-id]/[committee]/index.tsx @@ -13,6 +13,8 @@ import { Tabs } from "../../../../components/Tabs"; import SendCommitteeMessage from "../../../../components/committee/SendCommitteeMessage"; import CommitteeInterviewTimes from "../../../../components/committee/CommitteeInterviewTimes"; import LoadingPage from "../../../../components/LoadingPage"; +import { changeDisplayName } from "../../../../lib/utils/toString"; +import Custom404 from "../../../404"; const CommitteeApplicantOverView: NextPage = () => { const { data: session } = useSession(); @@ -20,11 +22,13 @@ const CommitteeApplicantOverView: NextPage = () => { const router = useRouter(); const periodId = router.query["period-id"] as string; - const [committees, setCommittees] = useState(null); + const committee = router.query["committee"] as string; const [period, setPeriod] = useState(null); const [activeTab, setActiveTab] = useState(0); const [tabClicked, setTabClicked] = useState(0); + const [hasAccess, setHasAccess] = useState(false); + useEffect(() => { if (!session || !periodId) return; @@ -35,38 +39,48 @@ const CommitteeApplicantOverView: NextPage = () => { setPeriod(data.period); } catch (error) { console.error("Failed to fetch interview periods:", error); - } finally { - setLoading(false); } }; - fetchPeriod(); - }, [session, periodId]); + const checkAccess = () => { + if (!period) { + return; + } - useEffect(() => { - if (period && session) { - const userCommittees = session.user!.committees; - const periodCommittees = period.committees; + const userCommittees = session?.user?.committees?.map((committee) => + committee.toLowerCase() + ); + const periodCommittees = period.committees.map((committee) => + committee.toLowerCase() + ); - if (period.optionalCommittees != null) { - periodCommittees.push(...period.optionalCommittees); - } + period.optionalCommittees.forEach((committee) => { + periodCommittees.push(committee.toLowerCase()); + }); - const filteredCommittees = periodCommittees.filter( - (committee) => userCommittees?.includes(committee.toLowerCase()) + const commonCommittees = userCommittees!.filter((committee) => + periodCommittees.includes(committee) ); - setCommittees(filteredCommittees); - } - }, [period, session]); + if (commonCommittees.includes(committee)) { + setHasAccess(true); + setLoading(false); + } else { + setLoading(false); + } + }; - if (!session || !session.user?.isCommitee) { - return

Ingen Tilgang!

; - } + fetchPeriod(); + checkAccess(); + }, [session, periodId, period]); if (loading) { return ; } + if (!session || !hasAccess) { + return ; + } + const interviewPeriodEnd = period?.interviewPeriod.end ? new Date(period.interviewPeriod.end) : null; @@ -101,6 +115,9 @@ const CommitteeApplicantOverView: NextPage = () => { return (
+ +

{`${period?.name} --> ${changeDisplayName(committee)}`}

+
{ @@ -111,7 +128,9 @@ const CommitteeApplicantOverView: NextPage = () => { { title: "Intervjutider", icon: , - content: , + content: ( + + ), }, { title: "Melding", @@ -126,7 +145,7 @@ const CommitteeApplicantOverView: NextPage = () => { content: ( ), From f7eeec7e3aa5c719fa8cbd1d3e37ed025e8e6801 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sat, 20 Jul 2024 20:26:28 +0200 Subject: [PATCH 25/48] refactored the API --- .../committee/CommitteeInterviewTimes.tsx | 33 ++++++++--------- lib/mongo/committees.ts | 21 +++++------ .../[period-id]/{index.ts => [committee].ts} | 25 +++++-------- .../[period-id]/[committee]/index.tsx | 36 +++++++++---------- 4 files changed, 53 insertions(+), 62 deletions(-) rename pages/api/committees/times/[period-id]/{index.ts => [committee].ts} (82%) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 3bc33bbd..c8142b44 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -32,9 +32,8 @@ const CommitteeInterviewTimes = ({ period, committee }: Props) => { const [selectedTimeslot, setSelectedTimeslot] = useState("15"); const [isLoading, setIsLoading] = useState(true); - const [committeeInterviewTimes, setCommitteeInterviewTimes] = useState< - committeeInterviewType[] - >([]); + const [committeeInterviewTimes, setCommitteeInterviewTimes] = + useState(); const [calendarEvents, setCalendarEvents] = useState([]); const [hasAlreadySubmitted, setHasAlreadySubmitted] = useState(false); @@ -51,28 +50,33 @@ const CommitteeInterviewTimes = ({ period, committee }: Props) => { useEffect(() => { const fetchCommitteeInterviewTimes = async () => { + if (!period) return; + try { - const res = await fetch(`/api/committees/times/${period?._id}`); + const res = await fetch( + `/api/committees/times/${period?._id}/${committee}` + ); const data = await res.json(); - if (data && Array.isArray(data.committees)) { + if (data) { setCommitteeInterviewTimes(data.committees); - setIsLoading(false); } else { console.error( - "Fetched data does not contain an 'committees' array:", + "Fetched data does not contain a 'committees' array:", data ); - setCommitteeInterviewTimes([]); } } catch (error) { console.error("Error fetching committee interview times:", error); - setCommitteeInterviewTimes([]); + } finally { + setIsLoading(false); } }; - fetchCommitteeInterviewTimes(); - }, []); + if (period && committee) { + fetchCommitteeInterviewTimes(); + } + }, [period, committee]); useEffect(() => { if (committee && Array.isArray(committeeInterviewTimes)) { @@ -91,7 +95,7 @@ const CommitteeInterviewTimes = ({ period, committee }: Props) => { if (relevantTimes.length > 0) { setHasAlreadySubmitted(true); const events = relevantTimes.flatMap((time) => - time.availabletimes.map((at) => ({ + time.availabletimes.map((at: any) => ({ start: new Date(at.start).toISOString(), end: new Date(at.end).toISOString(), })) @@ -226,13 +230,10 @@ const CommitteeInterviewTimes = ({ period, committee }: Props) => { const deleteSubmission = async (e: BaseSyntheticEvent) => { e.preventDefault(); - const queryParams = new URLSearchParams({ - committee: committee, //TODO fjernes - }).toString(); try { const response = await fetch( - `/api/committees/times/${period?._id}?${queryParams}`, + `/api/committees/times/${period?._id}/${committee}`, { method: "DELETE", } diff --git a/lib/mongo/committees.ts b/lib/mongo/committees.ts index 5f9ccdd2..0e57d1c1 100644 --- a/lib/mongo/committees.ts +++ b/lib/mongo/committees.ts @@ -70,12 +70,17 @@ export const updateCommitteeMessage = async ( export const getCommittees = async ( periodId: string, + selectedCommittee: string, userCommittees: string[] ) => { try { if (!committees) await init(); + if (!userHasAccessCommittee(userCommittees, selectedCommittee)) { + return { error: "User is unauthenticated" }; + } + const result = await committees - .find({ committee: { $in: userCommittees }, periodId: periodId }) + .find({ committee: selectedCommittee, periodId: periodId }) .toArray(); return { committees: result }; } catch (error) { @@ -141,26 +146,18 @@ export const deleteCommittee = async ( return { error: "User does not have access to this committee" }; } - let validPeriodId = periodId; - if (typeof periodId === "string") { - if (!ObjectId.isValid(periodId)) { - console.error("Invalid ObjectId:", periodId); - return { error: "Invalid ObjectId format" }; - } - validPeriodId = periodId; - } - const count = await committees.countDocuments({ committee: committee, - periodId: validPeriodId, + periodId: periodId, }); + if (count === 0) { return { error: "Committee not found or already deleted" }; } const result = await committees.deleteOne({ committee: committee, - periodId: validPeriodId, + periodId: periodId, }); if (result.deletedCount === 1) { diff --git a/pages/api/committees/times/[period-id]/index.ts b/pages/api/committees/times/[period-id]/[committee].ts similarity index 82% rename from pages/api/committees/times/[period-id]/index.ts rename to pages/api/committees/times/[period-id]/[committee].ts index a5f317d7..62716157 100644 --- a/pages/api/committees/times/[period-id]/index.ts +++ b/pages/api/committees/times/[period-id]/[committee].ts @@ -15,6 +15,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (!isInCommitee(res, session)) return; const periodId = req.query["period-id"]; + const selectedCommittee = req.query.committee; + + if (typeof selectedCommittee !== "string") { + return res.status(400).json({ error: "Invalid committee parameter" }); + } if (!periodId || typeof periodId !== "string") { return res @@ -26,6 +31,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { try { const { committees, error } = await getCommittees( periodId, + selectedCommittee, session!.user?.committees ?? [] ); @@ -38,18 +44,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } if (req.method === "PUT") { - const { committee, message } = req.body; - - if (!committee) { - console.error("Missing or invalid parameters", { - committee, - }); - return res.status(400).json({ error: "Missing or invalid parameters" }); - } + const { message } = req.body; try { const { updatedMessage, error } = await updateCommitteeMessage( - committee, + selectedCommittee, periodId, message, session!.user?.committees ?? [] @@ -63,15 +62,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } if (req.method === "DELETE") { - const committee = req.query.committee as string; - - if (!committee || !periodId) { - return res.status(400).json({ error: "Missing or invalid parameters" }); - } - try { const { error } = await deleteCommittee( - committee, + selectedCommittee, periodId, session!.user?.committees ?? [] ); diff --git a/pages/committee/[period-id]/[committee]/index.tsx b/pages/committee/[period-id]/[committee]/index.tsx index 4e9e5ad6..36eed713 100644 --- a/pages/committee/[period-id]/[committee]/index.tsx +++ b/pages/committee/[period-id]/[committee]/index.tsx @@ -132,24 +132,24 @@ const CommitteeApplicantOverView: NextPage = () => { ), }, - { - title: "Melding", - icon: , - content: ( - - ), - }, - { - title: "Søkere", - icon: , - content: ( - - ), - }, + // { + // title: "Melding", + // icon: , + // content: ( + // + // ), + // }, + // { + // title: "Søkere", + // icon: , + // content: ( + // + // ), + // }, ]} />
From 9bb0a2ca22b0a64adf835cdebba7760878b826f9 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sat, 20 Jul 2024 21:21:31 +0200 Subject: [PATCH 26/48] refactoring --- .../applicantoverview/ApplicantsOverview.tsx | 19 ++- .../committee/CommitteeInterviewTimes.tsx | 46 +----- components/committee/SendCommitteeMessage.tsx | 132 ++++++------------ lib/mongo/committees.ts | 17 ++- .../times/{ => [period-id]}/index.ts | 18 ++- .../[period-id]/[committee]/index.tsx | 56 +++++++- 6 files changed, 144 insertions(+), 144 deletions(-) rename pages/api/committees/times/{ => [period-id]}/index.ts (66%) diff --git a/components/applicantoverview/ApplicantsOverview.tsx b/components/applicantoverview/ApplicantsOverview.tsx index 264d2c72..bf05c862 100644 --- a/components/applicantoverview/ApplicantsOverview.tsx +++ b/components/applicantoverview/ApplicantsOverview.tsx @@ -11,7 +11,7 @@ import ApplicantOverviewSkeleton from "./ApplicantOverviewSkeleton"; interface Props { period: periodType | null; - committees: string; + committees: string[] | null; includePreferences: boolean; } @@ -206,6 +206,23 @@ const ApplicantsOverview = ({ ref={filterMenuRef} className="absolute right-0 top-10 w-48 bg-white dark:bg-online-darkBlue border border-gray-300 dark:border-gray-600 p-4 rounded shadow-lg z-10" > + {committees && ( +
+ + +
+ )}
- {userCommittees.map((committee) => ( - - ))} - -
-
- {!committeeHasSubmitedTimes && (

For å sende en egendefinert melding må du først fylle ut intervju @@ -195,7 +145,7 @@ const SendCommitteeMessage = ({ period, tabClicked }: Props) => {

diff --git a/lib/mongo/committees.ts b/lib/mongo/committees.ts index 0e57d1c1..3aece6ec 100644 --- a/lib/mongo/committees.ts +++ b/lib/mongo/committees.ts @@ -1,6 +1,7 @@ import { Collection, Db, MongoClient, ObjectId, UpdateResult } from "mongodb"; import clientPromise from "./mongodb"; import { commiteeType } from "../types/types"; +import { co } from "@fullcalendar/core/internal-common"; let client: MongoClient; let db: Db; @@ -104,7 +105,8 @@ export const getCommittee = async (id: string) => { export const createCommittee = async ( committeeData: commiteeType, - userCommittes: string[] + userCommittes: string[], + periodId: string ) => { try { if (!committees) await init(); @@ -112,11 +114,24 @@ export const createCommittee = async ( return { error: "User does not have access to this committee" }; } + if (!ObjectId.isValid(periodId)) { + return { error: "Invalid periodId" }; + } + const parsedCommitteeData = typeof committeeData === "string" ? JSON.parse(committeeData) : committeeData; + const count = await committees.countDocuments({ + periodId: periodId, + committee: committeeData.committee, + }); + + if (count > 0) { + return { error: "Committee Times already exists" }; + } + const result = await committees.insertOne(parsedCommitteeData); if (result.insertedId) { const insertedCommittee = await committees.findOne({ diff --git a/pages/api/committees/times/index.ts b/pages/api/committees/times/[period-id]/index.ts similarity index 66% rename from pages/api/committees/times/index.ts rename to pages/api/committees/times/[period-id]/index.ts index 39d8ed47..ba8f91da 100644 --- a/pages/api/committees/times/index.ts +++ b/pages/api/committees/times/[period-id]/index.ts @@ -4,14 +4,21 @@ import { createCommittee, deleteCommittee, updateCommitteeMessage, -} from "../../../../lib/mongo/committees"; +} from "../../../../../lib/mongo/committees"; import { getServerSession } from "next-auth"; -import { authOptions } from "../../auth/[...nextauth]"; -import { hasSession, isInCommitee } from "../../../../lib/utils/apiChecks"; -import { isCommitteeType } from "../../../../lib/utils/validators"; +import { authOptions } from "../../../auth/[...nextauth]"; +import { hasSession, isInCommitee } from "../../../../../lib/utils/apiChecks"; +import { isCommitteeType } from "../../../../../lib/utils/validators"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); + const periodId = req.query["period-id"]; + + if (!periodId || typeof periodId !== "string") { + return res + .status(400) + .json({ error: "Invalid or missing periodId parameter" }); + } if (!hasSession(res, session)) return; if (!isInCommitee(res, session)) return; @@ -26,7 +33,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { try { const { committee, error } = await createCommittee( committeeData, - session!.user?.committees ?? [] + session!.user?.committees ?? [], + periodId ); if (error) throw new Error(error); diff --git a/pages/committee/[period-id]/[committee]/index.tsx b/pages/committee/[period-id]/[committee]/index.tsx index 36eed713..33e3c4b8 100644 --- a/pages/committee/[period-id]/[committee]/index.tsx +++ b/pages/committee/[period-id]/[committee]/index.tsx @@ -1,7 +1,11 @@ import { NextPage } from "next"; import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; -import { applicantType, periodType } from "../../../../lib/types/types"; +import { + applicantType, + committeeInterviewType, + periodType, +} from "../../../../lib/types/types"; import { useRouter } from "next/router"; import ApplicantsOverview from "../../../../components/applicantoverview/ApplicantsOverview"; import { @@ -28,6 +32,8 @@ const CommitteeApplicantOverView: NextPage = () => { const [tabClicked, setTabClicked] = useState(0); const [hasAccess, setHasAccess] = useState(false); + const [committeeInterviewTimes, setCommitteeInterviewTimes] = + useState(null); useEffect(() => { if (!session || !periodId) return; @@ -42,11 +48,40 @@ const CommitteeApplicantOverView: NextPage = () => { } }; + fetchPeriod(); + }, [periodId]); + + useEffect(() => { + if (!session || !periodId || !committee) return; + + const fetchCommitteeInterviewTimes = async () => { + if (!session || committeeInterviewTimes) { + return; + } + if (period?._id === undefined) return; + + try { + const response = await fetch( + `/api/committees/times/${period?._id}/${committee}` + ); + const data = await response.json(); + console.log(data); + if (response.ok) { + setCommitteeInterviewTimes(data.period); + } else { + throw new Error(data.error || "Unknown error"); + } + } catch (error) { + console.error("Error checking period:", error); + } finally { + setLoading(false); + } + }; + const checkAccess = () => { if (!period) { return; } - const userCommittees = session?.user?.committees?.map((committee) => committee.toLowerCase() ); @@ -63,15 +98,14 @@ const CommitteeApplicantOverView: NextPage = () => { ); if (commonCommittees.includes(committee)) { setHasAccess(true); - setLoading(false); + fetchCommitteeInterviewTimes(); } else { setLoading(false); } }; - fetchPeriod(); checkAccess(); - }, [session, periodId, period]); + }, [period]); if (loading) { return ; @@ -129,14 +163,22 @@ const CommitteeApplicantOverView: NextPage = () => { title: "Intervjutider", icon: , content: ( - + ), }, // { // title: "Melding", // icon: , // content: ( - // + // // ), // }, // { From f6b04e3c5628ddfad21e79f1e9e0142a890e0583 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sat, 20 Jul 2024 21:36:57 +0200 Subject: [PATCH 27/48] refactored sendCommitteeMessage --- .../committee/CommitteeInterviewTimes.tsx | 21 +++---- components/committee/SendCommitteeMessage.tsx | 56 +++++-------------- .../[period-id]/[committee]/index.tsx | 28 +++++----- 3 files changed, 37 insertions(+), 68 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 1a7df891..5bfbc37f 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -51,37 +51,34 @@ const CommitteeInterviewTimes = ({ }, [period]); useEffect(() => { - if (committee && Array.isArray(committeeInterviewTimes)) { + if (committee && committeeInterviewTimes) { const cleanString = (input: string) => input .replace(/[\x00-\x1F\x7F-\x9F]/g, "") .trim() .toLowerCase(); - const relevantTimes = committeeInterviewTimes.filter((time) => { - const cleanCommittee = cleanString(time.committee); - const cleanSelectedCommittee = cleanString(committee); - return cleanCommittee === cleanSelectedCommittee; - }); + const cleanCommittee = cleanString(committeeInterviewTimes.committee); + const cleanSelectedCommittee = cleanString(committee); - if (relevantTimes.length > 0) { + if (cleanCommittee === cleanSelectedCommittee) { setHasAlreadySubmitted(true); - const events = relevantTimes.flatMap((time) => - time.availabletimes.map((at: any) => ({ + const events = committeeInterviewTimes.availabletimes.map( + (at: any) => ({ start: new Date(at.start).toISOString(), end: new Date(at.end).toISOString(), - })) + }) ); setCalendarEvents(events); - setSelectedTimeslot(relevantTimes[0].timeslot); + setSelectedTimeslot(committeeInterviewTimes.timeslot); } else { setHasAlreadySubmitted(false); setCalendarEvents([]); setSelectedTimeslot("15"); } } - }, [period, committee, committeeInterviewTimes]); + }, [committeeInterviewTimes]); const createInterval = (selectionInfo: any) => { const event = { diff --git a/components/committee/SendCommitteeMessage.tsx b/components/committee/SendCommitteeMessage.tsx index 2cf5073d..9db56c1e 100644 --- a/components/committee/SendCommitteeMessage.tsx +++ b/components/committee/SendCommitteeMessage.tsx @@ -10,16 +10,17 @@ import toast from "react-hot-toast"; interface Props { period: periodType | null; committee: string; + committeeInterviewTimes: committeeInterviewType | null; tabClicked: number; } -const SendCommitteeMessage = ({ period, committee, tabClicked }: Props) => { - const { data: session } = useSession(); - const [isLoading, setIsLoading] = useState(true); +const SendCommitteeMessage = ({ + committee, + committeeInterviewTimes, +}: Props) => { const router = useRouter(); const periodId = router.query["period-id"] as string; - const [committeeInterviewTimes, setCommitteeInterviewTimes] = - useState(); + const [committeeHasSubmitedTimes, setCommitteeHasSubmitedTimes] = useState(false); @@ -27,52 +28,25 @@ const SendCommitteeMessage = ({ period, committee, tabClicked }: Props) => { useState(false); const [message, setMessage] = useState(""); - useEffect(() => { - const fetchCommitteeInterviewTimes = async () => { - if (!session) { - return; - } - if (period?._id === undefined) return; - - try { - const response = await fetch( - `/api/committees/times/${period?._id}/${committee}` - ); - const data = await response.json(); - console.log(data); - if (response.ok) { - setCommitteeInterviewTimes(data); - } else { - throw new Error(data.error || "Unknown error"); - } - } catch (error) { - console.error("Error checking period:", error); - } finally { - setIsLoading(false); - } - }; - - fetchCommitteeInterviewTimes(); - }, [tabClicked]); + const [updatedCommitteeInterviewTimes, setUpdatedCommitteeInterviewTimes] = + useState(committeeInterviewTimes); useEffect(() => { - if (committeeInterviewTimes) { + if (updatedCommitteeInterviewTimes) { setCommitteeHasSubmitedTimes(true); - if (committeeInterviewTimes.message === "") { + if (updatedCommitteeInterviewTimes.message === "") { setCommitteeHasSubmitedMessage(false); } else { setCommitteeHasSubmitedMessage(true); - setMessage(committeeInterviewTimes.message); + setMessage(updatedCommitteeInterviewTimes.message); } - setMessage(committeeInterviewTimes.message || ""); + setMessage(updatedCommitteeInterviewTimes.message || ""); } else { setCommitteeHasSubmitedTimes(false); setCommitteeHasSubmitedMessage(false); setMessage(""); } - - console.log(message); - }, [committeeInterviewTimes]); + }, [updatedCommitteeInterviewTimes]); const handleMessageChange = (value: string) => { setMessage(value); @@ -99,7 +73,7 @@ const SendCommitteeMessage = ({ period, committee, tabClicked }: Props) => { } const updatedData = await res.json(); - setCommitteeInterviewTimes(updatedData); + setUpdatedCommitteeInterviewTimes(updatedData); toast.success("Innsending er vellykket!"); } catch (error) { toast.error("Det skjede en feil under innsendingen!"); @@ -107,8 +81,6 @@ const SendCommitteeMessage = ({ period, committee, tabClicked }: Props) => { } }; - if (isLoading) return ; - return (

diff --git a/pages/committee/[period-id]/[committee]/index.tsx b/pages/committee/[period-id]/[committee]/index.tsx index 33e3c4b8..ba70f69f 100644 --- a/pages/committee/[period-id]/[committee]/index.tsx +++ b/pages/committee/[period-id]/[committee]/index.tsx @@ -65,9 +65,8 @@ const CommitteeApplicantOverView: NextPage = () => { `/api/committees/times/${period?._id}/${committee}` ); const data = await response.json(); - console.log(data); if (response.ok) { - setCommitteeInterviewTimes(data.period); + setCommitteeInterviewTimes(data.committees[0]); } else { throw new Error(data.error || "Unknown error"); } @@ -105,7 +104,7 @@ const CommitteeApplicantOverView: NextPage = () => { }; checkAccess(); - }, [period]); + }, [period, tabClicked]); if (loading) { return ; @@ -170,17 +169,18 @@ const CommitteeApplicantOverView: NextPage = () => { /> ), }, - // { - // title: "Melding", - // icon: , - // content: ( - // - // ), - // }, + { + title: "Melding", + icon: , + content: ( + + ), + }, // { // title: "Søkere", // icon: , From 0cf6f74824f177cac90dfadba6a2e211662c6f7c Mon Sep 17 00:00:00 2001 From: fredrir Date: Sat, 20 Jul 2024 21:50:33 +0200 Subject: [PATCH 28/48] refactored sendCommitteeMessage --- components/committee/SendCommitteeMessage.tsx | 16 +++++++--------- .../committee/[period-id]/[committee]/index.tsx | 11 ++++++++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/components/committee/SendCommitteeMessage.tsx b/components/committee/SendCommitteeMessage.tsx index 9db56c1e..8e1e85b4 100644 --- a/components/committee/SendCommitteeMessage.tsx +++ b/components/committee/SendCommitteeMessage.tsx @@ -28,25 +28,22 @@ const SendCommitteeMessage = ({ useState(false); const [message, setMessage] = useState(""); - const [updatedCommitteeInterviewTimes, setUpdatedCommitteeInterviewTimes] = - useState(committeeInterviewTimes); - useEffect(() => { - if (updatedCommitteeInterviewTimes) { + if (committeeInterviewTimes) { setCommitteeHasSubmitedTimes(true); - if (updatedCommitteeInterviewTimes.message === "") { + if (committeeInterviewTimes.message === "") { setCommitteeHasSubmitedMessage(false); } else { setCommitteeHasSubmitedMessage(true); - setMessage(updatedCommitteeInterviewTimes.message); + setMessage(committeeInterviewTimes.message); } - setMessage(updatedCommitteeInterviewTimes.message || ""); + setMessage(committeeInterviewTimes.message || ""); } else { setCommitteeHasSubmitedTimes(false); setCommitteeHasSubmitedMessage(false); setMessage(""); } - }, [updatedCommitteeInterviewTimes]); + }, [committeeInterviewTimes]); const handleMessageChange = (value: string) => { setMessage(value); @@ -73,7 +70,8 @@ const SendCommitteeMessage = ({ } const updatedData = await res.json(); - setUpdatedCommitteeInterviewTimes(updatedData); + setMessage(updatedData.message); + setCommitteeHasSubmitedMessage(true); toast.success("Innsending er vellykket!"); } catch (error) { toast.error("Det skjede en feil under innsendingen!"); diff --git a/pages/committee/[period-id]/[committee]/index.tsx b/pages/committee/[period-id]/[committee]/index.tsx index ba70f69f..b710f909 100644 --- a/pages/committee/[period-id]/[committee]/index.tsx +++ b/pages/committee/[period-id]/[committee]/index.tsx @@ -55,7 +55,7 @@ const CommitteeApplicantOverView: NextPage = () => { if (!session || !periodId || !committee) return; const fetchCommitteeInterviewTimes = async () => { - if (!session || committeeInterviewTimes) { + if (!session) { return; } if (period?._id === undefined) return; @@ -77,6 +77,12 @@ const CommitteeApplicantOverView: NextPage = () => { } }; + fetchCommitteeInterviewTimes(); + }, [tabClicked, period]); + + useEffect(() => { + if (!session || !periodId || !committee) return; + const checkAccess = () => { if (!period) { return; @@ -97,14 +103,13 @@ const CommitteeApplicantOverView: NextPage = () => { ); if (commonCommittees.includes(committee)) { setHasAccess(true); - fetchCommitteeInterviewTimes(); } else { setLoading(false); } }; checkAccess(); - }, [period, tabClicked]); + }, [period]); if (loading) { return ; From 62191c8c535d8d38588882d795116cb73a02e9f1 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sat, 20 Jul 2024 22:06:22 +0200 Subject: [PATCH 29/48] refactored ApplicantOverview --- .../applicantoverview/ApplicantsOverview.tsx | 8 ++++--- lib/mongo/applicants.ts | 6 ++++- .../[period-id]/[committee].ts} | 18 ++++++++++----- .../[period-id]/[committee]/index.tsx | 22 +++++++++---------- 4 files changed, 34 insertions(+), 20 deletions(-) rename pages/api/committees/{[period-id]/index.ts => applicants/[period-id]/[committee].ts} (67%) diff --git a/components/applicantoverview/ApplicantsOverview.tsx b/components/applicantoverview/ApplicantsOverview.tsx index bf05c862..a95674d9 100644 --- a/components/applicantoverview/ApplicantsOverview.tsx +++ b/components/applicantoverview/ApplicantsOverview.tsx @@ -11,7 +11,8 @@ import ApplicantOverviewSkeleton from "./ApplicantOverviewSkeleton"; interface Props { period: periodType | null; - committees: string[] | null; + committees?: string[] | null; + committee?: string; includePreferences: boolean; } @@ -24,6 +25,7 @@ const isPreferencesType = ( const ApplicantsOverview = ({ period, committees, + committee, includePreferences, }: Props) => { const [filteredApplicants, setFilteredApplicants] = useState( @@ -47,7 +49,7 @@ const ApplicantsOverview = ({ const apiUrl = includePreferences ? `/api/applicants/${period?._id}` - : `/api/committees/${period?._id}`; + : `/api/committees/applicants/${period?._id}/${committee}`; useEffect(() => { const fetchApplicants = async () => { @@ -206,7 +208,7 @@ const ApplicantsOverview = ({ ref={filterMenuRef} className="absolute right-0 top-10 w-48 bg-white dark:bg-online-darkBlue border border-gray-300 dark:border-gray-600 p-4 rounded shadow-lg z-10" > - {committees && ( + {Array.isArray(committees) && (
Date: Sun, 21 Jul 2024 12:11:46 +0200 Subject: [PATCH 33/48] =?UTF-8?q?Checkpoint=20for=20omorganisering=20av=20?= =?UTF-8?q?filer=20for=20=C3=A5=20kunne=20kj=C3=B8re=20algoritmen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- algorithm/pyproject.toml | 20 ++++++++++++++++++ algorithm/requirements.txt | Bin 208 -> 208 bytes .../{mip_matching => src}/Modellering.md | 0 algorithm/{mip_matching => src}/__init__.py | 0 algorithm/{ => src}/mip_matching/Applicant.py | 5 ++--- algorithm/{ => src}/mip_matching/Committee.py | 12 +++++++++-- .../{ => src}/mip_matching/TimeInterval.py | 0 algorithm/src/mip_matching/__init__.py | 3 +++ .../{ => src}/mip_matching/match_meetings.py | 17 +++++++++------ .../tests/__init__.py => test.txt} | 0 .../{mip_matching => }/tests/ApplicantTest.py | 4 ++++ .../{mip_matching => }/tests/CommitteeTest.py | 0 .../tests/TimeIntervalTest.py | 0 algorithm/tests/__init__.py | 0 .../{mip_matching => }/tests/mip_test.py | 4 +++- 15 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 algorithm/pyproject.toml rename algorithm/{mip_matching => src}/Modellering.md (100%) rename algorithm/{mip_matching => src}/__init__.py (100%) rename algorithm/{ => src}/mip_matching/Applicant.py (93%) rename algorithm/{ => src}/mip_matching/Committee.py (90%) rename algorithm/{ => src}/mip_matching/TimeInterval.py (100%) create mode 100644 algorithm/src/mip_matching/__init__.py rename algorithm/{ => src}/mip_matching/match_meetings.py (89%) rename algorithm/{mip_matching/tests/__init__.py => test.txt} (100%) rename algorithm/{mip_matching => }/tests/ApplicantTest.py (97%) rename algorithm/{mip_matching => }/tests/CommitteeTest.py (100%) rename algorithm/{mip_matching => }/tests/TimeIntervalTest.py (100%) create mode 100644 algorithm/tests/__init__.py rename algorithm/{mip_matching => }/tests/mip_test.py (99%) diff --git a/algorithm/pyproject.toml b/algorithm/pyproject.toml new file mode 100644 index 00000000..e22dea2b --- /dev/null +++ b/algorithm/pyproject.toml @@ -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"] \ No newline at end of file diff --git a/algorithm/requirements.txt b/algorithm/requirements.txt index 349630b1bd346bcbe0b649e972a8500e82667c86..6a0b6cf32162297694a54be67d1b9294b0ba825f 100644 GIT binary patch delta 20 bcmcb>c!6<(1f#)3NmEu620aF&iNUo1JZ=S5 delta 20 bcmcb>c!6<(1f$_ZNmEu+20aFYiNUo1JbMLI diff --git a/algorithm/mip_matching/Modellering.md b/algorithm/src/Modellering.md similarity index 100% rename from algorithm/mip_matching/Modellering.md rename to algorithm/src/Modellering.md diff --git a/algorithm/mip_matching/__init__.py b/algorithm/src/__init__.py similarity index 100% rename from algorithm/mip_matching/__init__.py rename to algorithm/src/__init__.py diff --git a/algorithm/mip_matching/Applicant.py b/algorithm/src/mip_matching/Applicant.py similarity index 93% rename from algorithm/mip_matching/Applicant.py rename to algorithm/src/mip_matching/Applicant.py index 9c72ac2d..5d78429d 100644 --- a/algorithm/mip_matching/Applicant.py +++ b/algorithm/src/mip_matching/Applicant.py @@ -3,8 +3,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: # Unngår cyclic import - from mip_matching.Committee import Committee - from mip_matching.TimeInterval import TimeInterval + from Committee import Committee + from TimeInterval import TimeInterval import itertools @@ -29,7 +29,6 @@ def add_committees(self, committees: set[Committee]) -> None: self.add_committee(committee) def add_interval(self, interval: TimeInterval) -> None: - # TODO: Vurder å gjøre "sanitizing" ved å slå sammen overlappende intervaller. """ Slår også sammen overlappende intervaller. diff --git a/algorithm/mip_matching/Committee.py b/algorithm/src/mip_matching/Committee.py similarity index 90% rename from algorithm/mip_matching/Committee.py rename to algorithm/src/mip_matching/Committee.py index 9a43d7c7..eac33a7c 100644 --- a/algorithm/mip_matching/Committee.py +++ b/algorithm/src/mip_matching/Committee.py @@ -1,13 +1,17 @@ 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 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 +from TimeInterval import TimeInterval class Committee: @@ -77,3 +81,7 @@ def __str__(self): def __repr__(self): return str(self) + + +if __name__ == "__main__": + print("running") diff --git a/algorithm/mip_matching/TimeInterval.py b/algorithm/src/mip_matching/TimeInterval.py similarity index 100% rename from algorithm/mip_matching/TimeInterval.py rename to algorithm/src/mip_matching/TimeInterval.py diff --git a/algorithm/src/mip_matching/__init__.py b/algorithm/src/mip_matching/__init__.py new file mode 100644 index 00000000..c36a779a --- /dev/null +++ b/algorithm/src/mip_matching/__init__.py @@ -0,0 +1,3 @@ +# import Applicant, Committee, match_meetings, TimeInterval + +# __all__ = ("Applicant", "Committee", "match_meetings", "TimeInterval") \ No newline at end of file diff --git a/algorithm/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py similarity index 89% rename from algorithm/mip_matching/match_meetings.py rename to algorithm/src/mip_matching/match_meetings.py index 3f8792a8..849027c2 100644 --- a/algorithm/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -1,11 +1,11 @@ from typing import TypedDict -from mip_matching.TimeInterval import TimeInterval -from mip_matching.Committee import Committee -from mip_matching.Applicant import Applicant +from TimeInterval import TimeInterval +from Committee import Committee +from Applicant import Applicant import mip -from typing import TypedDict +# from typing import TypedDict class MeetingMatch(TypedDict): @@ -34,13 +34,15 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me for interval, capacity in committee.get_intervals_and_capacities(): model += mip.xsum(m[(applicant, committee, interval)] for applicant in committee.get_applicants() - if (applicant, committee, interval) in m) <= capacity # type: ignore + # type: ignore + if (applicant, committee, interval) in m) <= 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)] - for interval in applicant.get_fitting_committee_slots(committee)) <= 1 # type: ignore + # 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 for applicant in applicants: @@ -53,7 +55,8 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me model += mip.xsum(m[(applicant, committee, interval)] for committee in applicant.get_committees() - if (applicant, committee, interval) in m) <= 1 # type: ignore + # type: ignore + if (applicant, committee, interval) in m) <= 1 # Setter mål til å være maksimering av antall møter model.objective = mip.maximize(mip.xsum(m.values())) diff --git a/algorithm/mip_matching/tests/__init__.py b/algorithm/test.txt similarity index 100% rename from algorithm/mip_matching/tests/__init__.py rename to algorithm/test.txt diff --git a/algorithm/mip_matching/tests/ApplicantTest.py b/algorithm/tests/ApplicantTest.py similarity index 97% rename from algorithm/mip_matching/tests/ApplicantTest.py rename to algorithm/tests/ApplicantTest.py index a52d517b..8c51138a 100644 --- a/algorithm/mip_matching/tests/ApplicantTest.py +++ b/algorithm/tests/ApplicantTest.py @@ -49,3 +49,7 @@ def test_add_interval_sanitizes(self) -> None: 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/mip_matching/tests/CommitteeTest.py b/algorithm/tests/CommitteeTest.py similarity index 100% rename from algorithm/mip_matching/tests/CommitteeTest.py rename to algorithm/tests/CommitteeTest.py diff --git a/algorithm/mip_matching/tests/TimeIntervalTest.py b/algorithm/tests/TimeIntervalTest.py similarity index 100% rename from algorithm/mip_matching/tests/TimeIntervalTest.py rename to algorithm/tests/TimeIntervalTest.py diff --git a/algorithm/tests/__init__.py b/algorithm/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/tests/mip_test.py similarity index 99% rename from algorithm/mip_matching/tests/mip_test.py rename to algorithm/tests/mip_test.py index 749d4585..a8b3a9f8 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/tests/mip_test.py @@ -1,5 +1,6 @@ from __future__ import annotations from datetime import datetime, timedelta, date, time +# import ..algorithm.mip_matching.core.Applicant.py as applicant from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee @@ -144,7 +145,8 @@ def test_realistic(self): START_TIME_PER_DAY = time(hour=8, minute=0) END_TIME_PER_DAY = time(hour=18, minute=0) DAY_LENGTH = datetime.combine(date.today( - ), END_TIME_PER_DAY) - datetime.combine(date.today(), START_TIME_PER_DAY) # type: ignore + # 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) + \ From f7661d66dd405dd30cb95043997afb2a1b06fd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 21 Jul 2024 12:13:06 +0200 Subject: [PATCH 34/48] Remove unused file --- algorithm/test.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 algorithm/test.txt diff --git a/algorithm/test.txt b/algorithm/test.txt deleted file mode 100644 index e69de29b..00000000 From 005c8ec2ba35a5c25e1f8ce47fb0712a7a800af0 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 21 Jul 2024 12:55:50 +0200 Subject: [PATCH 35/48] relative imports --- algorithm/src/mip_matching/Committee.py | 4 ++-- algorithm/src/mip_matching/match_meetings.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/algorithm/src/mip_matching/Committee.py b/algorithm/src/mip_matching/Committee.py index eac33a7c..115debc4 100644 --- a/algorithm/src/mip_matching/Committee.py +++ b/algorithm/src/mip_matching/Committee.py @@ -5,13 +5,13 @@ print(__name__) # sys.path.append("C:\\Users\\Jørgen Galdal\\Documents\\lokalSkoleprogrammering\\appkom\\OnlineOpptak\\algorithm\\mip_matching") -from Applicant import Applicant +from mip_matching.Applicant import Applicant from typing import Iterator # from typing import TYPE_CHECKING # if TYPE_CHECKING: # # Unngår cyclic import -from TimeInterval import TimeInterval +from mip_matching.TimeInterval import TimeInterval class Committee: diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index 849027c2..9bbd512e 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -1,8 +1,8 @@ from typing import TypedDict -from TimeInterval import TimeInterval -from Committee import Committee -from Applicant import Applicant +from mip_matching.TimeInterval import TimeInterval +from mip_matching.Committee import Committee +from mip_matching.Applicant import Applicant import mip # from typing import TypedDict From b196c7fa334ac72a9e12836bbff9acf6ae725ba3 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 21 Jul 2024 13:54:00 +0200 Subject: [PATCH 36/48] . --- components/committee/CommitteeInterviewTimes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 5bfbc37f..4ec28e3c 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -104,7 +104,7 @@ const CommitteeInterviewTimes = ({ const dataToSend = { periodId: period!._id, period_name: period!.name, - committee: committee, //TODO FJERN + committee: committee, availabletimes: formattedEvents, timeslot: `${selectedTimeslot}`, message: "", From b646ad5f17cbd61e47d0f894e482616e420d1cf9 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 21 Jul 2024 13:58:36 +0200 Subject: [PATCH 37/48] refactoring --- components/committee/CommitteApplicants.tsx | 105 -------------------- pages/committee/index.tsx | 93 ++++++++++++++++- 2 files changed, 88 insertions(+), 110 deletions(-) delete mode 100644 components/committee/CommitteApplicants.tsx diff --git a/components/committee/CommitteApplicants.tsx b/components/committee/CommitteApplicants.tsx deleted file mode 100644 index 1cf377f3..00000000 --- a/components/committee/CommitteApplicants.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { NextPage } from "next"; -import { useEffect, useState } from "react"; -import { useSession } from "next-auth/react"; -import Table from "../Table"; -import { formatDate } from "../../lib/utils/dateUtils"; -import { periodType } from "../../lib/types/types"; - -interface Props { - routeString: string; -} - -const CommitteeApplicants: NextPage = ({ routeString }) => { - const { data: session } = useSession(); - const [periods, setPeriods] = useState([]); - - const fetchPeriods = async () => { - try { - const response = await fetch("/api/periods"); - const data = await response.json(); - const userCommittees = session?.user?.committees || []; - - // Viser bare aktuelle perioder - const filteredPeriods = data.periods.filter((period: periodType) => - period.committees.some((committee: string) => - userCommittees.includes(committee.toLowerCase()) - ) - ); - - setPeriods( - filteredPeriods.map((period: periodType) => { - const userCommittees = session?.user?.committees?.map((committee) => - committee.toLowerCase() - ); - const periodCommittees = period.committees.map((committee) => - committee.toLowerCase() - ); - - period.optionalCommittees.forEach((committee) => { - periodCommittees.push(committee.toLowerCase()); - }); - - const commonCommittees = userCommittees!.filter((committee) => - periodCommittees.includes(committee) - ); - - let uriLink = ""; - - if (commonCommittees.length > 1) { - uriLink = `committee/${period._id}`; - } else { - uriLink = `committee/${period._id}/${commonCommittees[0]}`; - } - - return { - name: period.name, - preparation: - formatDate(period.preparationPeriod.start) + - " til " + - formatDate(period.preparationPeriod.end), - application: - formatDate(period.applicationPeriod.start) + - " til " + - formatDate(period.applicationPeriod.end), - interview: - formatDate(period.interviewPeriod.start) + - " til " + - formatDate(period.interviewPeriod.end), - committees: period.committees, - link: uriLink, - }; - }) - ); - } catch (error) { - console.error("Failed to fetch application periods:", error); - } - }; - - useEffect(() => { - fetchPeriods(); - }, []); - - const periodsColumns = [ - { label: "Navn", field: "name" }, - { label: "Forberedelse", field: "preparation" }, - { label: "Søknad", field: "application" }, - { label: "Intervju", field: "interview" }, - ]; - - if (!session || !session.user?.isCommitee) { - return

Ingen tilgang!

; - } - - return ( -
-

Velg opptak

-
- {periods.length > 0 && ( - - )} - - - ); -}; - -export default CommitteeApplicants; diff --git a/pages/committee/index.tsx b/pages/committee/index.tsx index 8702b245..25fbd279 100644 --- a/pages/committee/index.tsx +++ b/pages/committee/index.tsx @@ -1,12 +1,90 @@ import type { NextPage } from "next"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; -import CommitteeInterViewTimes from "../../components/committee/CommitteeInterviewTimes"; -import CommitteeApplicants from "../../components/committee/CommitteApplicants"; +import Table from "../../components/Table"; +import { formatDate } from "../../lib/utils/dateUtils"; +import { periodType } from "../../lib/types/types"; const Committee: NextPage = () => { const { data: session } = useSession(); - const [activeTab, setActiveTab] = useState(0); + const [periods, setPeriods] = useState([]); + + const fetchPeriods = async () => { + try { + const response = await fetch("/api/periods"); + const data = await response.json(); + const userCommittees = session?.user?.committees || []; + + // Viser bare aktuelle perioder + const filteredPeriods = data.periods.filter((period: periodType) => + period.committees.some((committee: string) => + userCommittees.includes(committee.toLowerCase()) + ) + ); + + setPeriods( + filteredPeriods.map((period: periodType) => { + const userCommittees = session?.user?.committees?.map((committee) => + committee.toLowerCase() + ); + const periodCommittees = period.committees.map((committee) => + committee.toLowerCase() + ); + + period.optionalCommittees.forEach((committee) => { + periodCommittees.push(committee.toLowerCase()); + }); + + const commonCommittees = userCommittees!.filter((committee) => + periodCommittees.includes(committee) + ); + + let uriLink = ""; + + if (commonCommittees.length > 1) { + uriLink = `committee/${period._id}`; + } else { + uriLink = `committee/${period._id}/${commonCommittees[0]}`; + } + + return { + name: period.name, + preparation: + formatDate(period.preparationPeriod.start) + + " til " + + formatDate(period.preparationPeriod.end), + application: + formatDate(period.applicationPeriod.start) + + " til " + + formatDate(period.applicationPeriod.end), + interview: + formatDate(period.interviewPeriod.start) + + " til " + + formatDate(period.interviewPeriod.end), + committees: period.committees, + link: uriLink, + }; + }) + ); + } catch (error) { + console.error("Failed to fetch application periods:", error); + } + }; + + useEffect(() => { + fetchPeriods(); + }, []); + + const periodsColumns = [ + { label: "Navn", field: "name" }, + { label: "Forberedelse", field: "preparation" }, + { label: "Søknad", field: "application" }, + { label: "Intervju", field: "interview" }, + ]; + + if (!session || !session.user?.isCommitee) { + return

Ingen tilgang!

; + } if (!session || !session.user?.isCommitee) { return

Ingen tilgang!

; @@ -14,7 +92,12 @@ const Committee: NextPage = () => { return (
- +

Velg opptak

+
+ {periods.length > 0 && ( +
+ )} + ); }; From b0ebea79f812326a4735f691235a4cac592092d8 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 21 Jul 2024 14:01:47 +0200 Subject: [PATCH 38/48] loading page --- pages/committee/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pages/committee/index.tsx b/pages/committee/index.tsx index 25fbd279..651aaf93 100644 --- a/pages/committee/index.tsx +++ b/pages/committee/index.tsx @@ -4,10 +4,12 @@ import { useSession } from "next-auth/react"; import Table from "../../components/Table"; import { formatDate } from "../../lib/utils/dateUtils"; import { periodType } from "../../lib/types/types"; +import LoadingPage from "../../components/LoadingPage"; const Committee: NextPage = () => { const { data: session } = useSession(); const [periods, setPeriods] = useState([]); + const [isLoading, setIsLoading] = useState(true); const fetchPeriods = async () => { try { @@ -68,6 +70,8 @@ const Committee: NextPage = () => { ); } catch (error) { console.error("Failed to fetch application periods:", error); + } finally { + setIsLoading(false); } }; @@ -86,8 +90,8 @@ const Committee: NextPage = () => { return

Ingen tilgang!

; } - if (!session || !session.user?.isCommitee) { - return

Ingen tilgang!

; + if (isLoading) { + return ; } return ( From 834bf730782010367ad2b99dd1b74257d0618e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 21 Jul 2024 14:15:35 +0200 Subject: [PATCH 39/48] Update algorithm.yml add setup-py and checkout --- .github/workflows/algorithm.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/algorithm.yml b/.github/workflows/algorithm.yml index cf88e24e..981ed444 100644 --- a/.github/workflows/algorithm.yml +++ b/.github/workflows/algorithm.yml @@ -15,7 +15,12 @@ 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 From efb2dc0d9ca2de12d97569cda06d511d598585d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 21 Jul 2024 14:20:15 +0200 Subject: [PATCH 40/48] Update algorithm.yml --- .github/workflows/algorithm.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/algorithm.yml b/.github/workflows/algorithm.yml index 981ed444..96ba20c3 100644 --- a/.github/workflows/algorithm.yml +++ b/.github/workflows/algorithm.yml @@ -28,5 +28,6 @@ jobs: - name: Run tests run: | + cd algorithm python -m unittest discover -p "*test.py" From 7f99a7c4299762ecd0c8fc6de88c4d6aeb19bd9f Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 21 Jul 2024 14:25:29 +0200 Subject: [PATCH 41/48] added check garbage email --- pages/application/[period-id].tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pages/application/[period-id].tsx b/pages/application/[period-id].tsx index a50fd8ea..495a25dd 100644 --- a/pages/application/[period-id].tsx +++ b/pages/application/[period-id].tsx @@ -193,8 +193,11 @@ const Application: NextPage = () => {

+ {` Vi har mottatt din søknad og sendt deg en bekreftelse på e-post! Du - vil få enda en e-post med intervjutider når søknadsperioden er over. + vil få enda en e-post med intervjutider når søknadsperioden er over. `} +

+ {`(Sjekk spam-mappen din hvis du ikke finner e-posten)`}

+ )} {fetchedApplicationData && (
Date: Sun, 21 Jul 2024 15:02:32 +0200 Subject: [PATCH 43/48] =?UTF-8?q?=F0=9F=AB=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/application/[period-id].tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pages/application/[period-id].tsx b/pages/application/[period-id].tsx index 495a25dd..4368e1b0 100644 --- a/pages/application/[period-id].tsx +++ b/pages/application/[period-id].tsx @@ -14,6 +14,7 @@ import Schedule from "../../components/committee/Schedule"; import { validateApplication } from "../../lib/utils/validateApplication"; import ApplicantCard from "../../components/applicantoverview/ApplicantCard"; import LoadingPage from "../../components/LoadingPage"; +import { formatDateNorwegian } from "../../lib/utils/dateUtils"; interface FetchedApplicationData { exists: boolean; @@ -193,11 +194,15 @@ const Application: NextPage = () => {

- {` - Vi har mottatt din søknad og sendt deg en bekreftelse på e-post! Du - vil få enda en e-post med intervjutider når søknadsperioden er over. `} -

- {`(Sjekk spam-mappen din hvis du ikke finner e-posten)`} + Vi har mottatt din søknad og sendt deg en bekreftelse på e-post! +

+

+ Du vil få enda en e-post med intervjutider når søknadsperioden er over + (rundt {formatDateNorwegian(period?.applicationPeriod?.end)}). +

+

+ (Hvis du ikke finner e-posten din, sjekk "søppelpost"- eller + "spam"-mappen.)