1212# Hvor stort buffer man ønsker å ha mellom intervjuene
1313APPLICANT_BUFFER_LENGTH = timedelta (minutes = 15 )
1414
15- # Et mål på hvor viktig det er at intervjuer er i nærheten av hverandre
16- CLUSTERING_WEIGHT = 0.001
17-
1815# Når på dagen man helst vil ha intervjuene rundt
1916CLUSTERING_TIME_BASELINE = time (12 , 00 )
20- MAX_SCALE_CLUSTERING_TIME = timedelta (seconds = 43200 ) # TODO: Rename variable
17+ MAX_SCALE_CLUSTERING_TIME = timedelta (seconds = 43200 )
18+
19+ # En liste med alle sekundærmål-vekter.
20+ # Hver vekt bestemmer hvor mye det tilhørende sekundærmålet påvirker optimeringen.
21+ # Høyere vekt = sterkere preferanse for det målet.
22+
23+ # n^2*x + n*x + h, der n er antall intervjuer, x er en konstant.
24+ def calculate_secondary_objective_weights (num_interviews : int ) -> dict [str , float ]:
25+ # Vekten for clustering øker kvadratisk med antall intervjuer, for å prioritere det mer når det er mange intervjuer.
26+ clustering_weight = 0.001 * (num_interviews ** 2 )
27+
28+ # Vekten for spredning over perioden øker lineært med antall intervjuer, for å sikre at det fortsatt har en betydelig effekt.
29+ firstDay_weight = 0.001 * num_interviews
30+
31+ return {
32+ "clustering" : clustering_weight ,
33+ "first_day" : firstDay_weight
34+ }
2135
2236
2337def match_meetings (applicants : set [Applicant ], committees : set [Committee ]) -> MeetingMatch :
@@ -26,6 +40,7 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me
2640
2741 m : dict [Matching , mip .Var ] = {}
2842
43+
2944 # Lager alle maksimeringsvariabler
3045 for applicant in applicants :
3146 for committee in applicant .get_committees ():
@@ -52,35 +67,61 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me
5267 for room in committee .get_rooms (interval )
5368 # type: ignore
5469 ) <= 1
55-
5670 # Legger inn begrensninger for at en søker ikke kan ha overlappende intervjutider
5771 # og minst har et buffer mellom hvert intervju som angitt
5872 for applicant in applicants :
59- potential_interviews = set (slot for slot in m .keys () if slot [0 ] == applicant )
60-
61- for interview_a , interview_b in combinations (potential_interviews , r = 2 ):
62- if interview_a [2 ].intersects (interview_b [2 ]) or interview_a [2 ].is_within_distance (interview_b [2 ], APPLICANT_BUFFER_LENGTH ):
63- model += m [interview_a ] + m [interview_b ] <= 1 # type: ignore
73+ # Grupper variabler per unikt (komité, intervall) — rom er irrelevant for overlap
74+ unique_slots : dict [tuple , list [mip .Var ]] = {}
75+ for slot in m .keys ():
76+ if slot [0 ] == applicant :
77+ key = (slot [1 ], slot [2 ]) # (committee, interval)
78+ if key not in unique_slots :
79+ unique_slots [key ] = []
80+ unique_slots [key ].append (m [slot ])
81+
82+ slot_keys = list (unique_slots .keys ())
83+ for i , key_a in enumerate (slot_keys ):
84+ for key_b in slot_keys [i + 1 :]:
85+ interval_a = key_a [1 ]
86+ interval_b = key_b [1 ]
87+ if interval_a .intersects (interval_b ) or interval_a .is_within_distance (interval_b , APPLICANT_BUFFER_LENGTH ):
88+ # Sum av alle rom-variabler for begge slots <= 1
89+ model += mip .xsum (unique_slots [key_a ]) + mip .xsum (unique_slots [key_b ]) <= 1 # type: ignore
90+
91+ SECONDARY_OBJECTIVE_WEIGHTS = calculate_secondary_objective_weights (model .num_cols );
6492
6593 # Legger til sekundærmål om at man ønsker å sentrere intervjuer rundt CLUSTERING_TIME_BASELINE
66- clustering_penalties = []
94+ # og at man foretrekker intervjuer senere i søknadsperioden
95+ secondary_penalties = []
96+
97+ # Finn den tidligste og seneste datoen blant alle intervjuer for å normalisere
98+ all_dates = [interval .start for (_ , _ , interval , _ ) in m .keys ()]
99+ min_date = min (all_dates )
67100
68101 for name , variable in m .items ():
69102 applicant , committee , interval , room = name
103+
104+ # Sekundærmål 1: Clustering rundt CLUSTERING_TIME_BASELINE
70105 if interval .start .time () < CLUSTERING_TIME_BASELINE :
71106 relative_distance_from_baseline = subtract_time (CLUSTERING_TIME_BASELINE ,
72107 interval .end .time ()) / MAX_SCALE_CLUSTERING_TIME
73108 else :
74109 relative_distance_from_baseline = subtract_time (interval .start .time (),
75110 CLUSTERING_TIME_BASELINE ) / MAX_SCALE_CLUSTERING_TIME
76111
77- clustering_penalties .append (
78- CLUSTERING_WEIGHT * relative_distance_from_baseline * variable ) # type: ignore
112+ secondary_penalties .append (
113+ SECONDARY_OBJECTIVE_WEIGHTS [ "clustering" ] * relative_distance_from_baseline * variable ) # type: ignore
79114
115+ # Sekundærmål 2: Foretrekk intervjuer senere i perioden
116+ # Gir lavere straff jo senere i perioden intervjuet er
117+ if interval .start .date () == min_date .date ():
118+ secondary_penalties .append (
119+ SECONDARY_OBJECTIVE_WEIGHTS ["first_day" ] * variable ) # type: ignore
120+
80121 # Setter mål til å være maksimering av antall møter
81- # med sekundærmål om å samle intervjuene rundt CLUSTERING_TIME_BASELINE
122+ # med sekundærmål om å samle intervjuene og foretrekke senere datoer
82123 model .objective = mip .maximize (
83- mip .xsum (m .values ()) - mip .xsum (clustering_penalties ))
124+ mip .xsum (m .values ()) - mip .xsum (secondary_penalties ))
84125
85126 # Kjør optimeringen
86127 solver_status = model .optimize ()
@@ -95,6 +136,9 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me
95136
96137 total_wanted_meetings = sum (
97138 len (applicant .get_committees ()) for applicant in applicants )
139+
140+ print (f"Matched { total_matched_meetings } out of { total_wanted_meetings } wanted meetings." )
141+ print (model .num_cols )
98142
99143 match_object : MeetingMatch = {
100144 "solver_status" : solver_status ,
0 commit comments