ChilmAI 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
chilmai/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,391 @@
1
+ from chilmai.constants import UNMATCHED_DAYCARE_ID
2
+
3
+
4
+ class CP_Child:
5
+ """児童エージェント。
6
+
7
+ Attributes:
8
+ id (int): 児童ID。
9
+ age (int): 年齢(0-5)。
10
+ family (int): Family.id。
11
+ initial_daycare (int): 初期割当保育所の Daycare.id。
12
+ pref (list[int]): 希望保育所 ID リスト。
13
+ projected_pref (list[int]): 後で family.pref から作られる投影済み希望リスト。
14
+ all_daycare_ids (list[int]): projected_pref から導出される全保育所 ID。
15
+ assigned_daycare (int | None): CP アルゴリズムによる割当先の Daycare.id。
16
+ """
17
+
18
+ def __init__(
19
+ self, c_id: int, age: int, family_id: int, initial_daycare_id: int, preference_list: list[int]
20
+ ):
21
+ # attributes from dictionary
22
+ self.id = c_id
23
+ self.age = age
24
+ self.family = family_id
25
+ self.initial_daycare = initial_daycare_id
26
+ self.pref = [
27
+ d_id if d_id is not None else UNMATCHED_DAYCARE_ID for d_id in preference_list
28
+ ] # replace None with UNMATCHED_DAYCARE_ID
29
+ # additional attributes
30
+ self.projected_pref = []
31
+ self.all_daycare_ids = [] # notation D(\succ_c)
32
+ self.assigned_daycare = None
33
+
34
+ def __str__(self):
35
+ return f"child {self.id}"
36
+
37
+ def __repr__(self):
38
+ return f"child {self.id}"
39
+
40
+ # notation P(c, d)
41
+ def return_all_positions_of_certain_dacyare_in_projected_pref(self, daycare_id):
42
+ """
43
+ self.projected_pref 内で daycare_id に対応する位置のリストを返す。
44
+ """
45
+ positions = []
46
+ for index, d_id in enumerate(self.projected_pref):
47
+ if d_id == daycare_id and index not in positions:
48
+ positions.append(index)
49
+ return positions
50
+
51
+
52
+ class CP_Daycare:
53
+ """保育所エージェント。
54
+
55
+ Attributes:
56
+ id (int): 保育所ID。id=UNMATCHED_DAYCARE_ID のダミー保育所は、児童が未割当になる選択肢を表す。
57
+ recruiting_numbers (list[int]): 年齢ごとの募集人数。各位置が1つの年齢に対応する。
58
+ share_ages_list (list[list[int]]): recruiting_numbers を共有できる年齢グループのリスト。
59
+ 例: [[0,1],[4,5]] は0歳と1歳、4歳と5歳が枠を共有できることを表す。
60
+ priority (list[int]): 各保育所の優先順位リスト(child.id のリスト)。
61
+ score_list (list[int]): priority に対応する優先スコアリスト。
62
+ use_transfer (list[bool]): 年齢ごとの転園枠再配分フラグ。
63
+ all_shared_ages (list[int]): share_ages_list を平坦化した年齢リスト。
64
+ total_numbers (list[int]): 年齢ごとに転園希望児童数を追加で含めた数。在園枠の再利用可否に依存する。
65
+ total_numbers_share (list[int]): 年齢枠共有を考慮した total_numbers。
66
+ priority_age_dic (dict): 年齢ごとの優先順位辞書。
67
+ priority_age_share_dic (dict): 年齢枠共有を考慮した年齢ごとの優先順位辞書。
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ d_id: int,
73
+ recruiting_numbers_list: list[int],
74
+ share_ages_list: list[list[int]],
75
+ priority_child_id_list: list[int],
76
+ priority_score_list: list[int],
77
+ is_use_transfer: list[bool],
78
+ ):
79
+ # attributes from dictionary
80
+ self.id = d_id
81
+ self.recruiting_numbers = recruiting_numbers_list
82
+ self.share_ages_list = share_ages_list
83
+ self.priority = priority_child_id_list
84
+ self.score_list = priority_score_list
85
+ # use_transfer は is True / is False で判定するため、1 や numpy.bool_ が
86
+ # 渡ってもどちらかの分岐が必ず成立するよう bool へ正規化する。
87
+ self.use_transfer = [bool(x) for x in is_use_transfer]
88
+
89
+ # additional attributes
90
+ self.all_shared_ages = [age for ages in self.share_ages_list for age in ages]
91
+ self.total_numbers = [x for x in self.recruiting_numbers]
92
+ self.total_numbers_share = [x for x in self.recruiting_numbers]
93
+ self.priority_age_dic = {}
94
+ self.priority_age_share_dic = {}
95
+
96
+ def __str__(self):
97
+ return f"daycare {self.id}"
98
+
99
+ def __repr__(self):
100
+ return f"daycare {self.id}"
101
+
102
+ def update_priority_age_dic(self, children):
103
+ """
104
+ self.priority_list から年齢別の優先順位辞書を作る。
105
+
106
+ 2024.04.26 の重要な変更:
107
+ c が d に在園しており、かつ c の在園枠を再配分できない場合
108
+ (d.use_transfer[c.age] == False)、c は priority_age_dic に追加しない。
109
+ """
110
+
111
+ self.priority_age_dic = {}
112
+ for age in range(6):
113
+ self.priority_age_dic[age] = []
114
+
115
+ for c_id in self.priority:
116
+ child = next((c for c in children if c.id == c_id), None)
117
+
118
+ ################################################################################
119
+ # exclude the case if child is initially enrolled and occupied seats correspond to his age cannot be reused
120
+ if child.initial_daycare == self.id and self.use_transfer[child.age] is False:
121
+ continue
122
+ ################################################################################
123
+
124
+ if c_id not in self.priority_age_dic[child.age]:
125
+ self.priority_age_dic[child.age].append(c_id)
126
+
127
+ def update_priority_age_share_dic(self, children):
128
+ """
129
+ self.priority_list から、柔軟な年齢枠共有を考慮した年齢別優先順位辞書を作る。
130
+ """
131
+ if self.share_ages_list is None:
132
+ self.priority_age_share_dic = self.priority_age_dic
133
+ else:
134
+ self.priority_age_share_dic = {}
135
+ for age in range(6):
136
+ self.priority_age_share_dic[age] = []
137
+ # traversing the children in self.priority_child_id_list once
138
+ for c_id in self.priority:
139
+ c = next((c for c in children if c.id == c_id), None)
140
+
141
+ ################################################################################
142
+ # exclude the case if child is initially enrolled and occupied seats correspond to his age cannot be reused
143
+ if c.initial_daycare == self.id and self.use_transfer[c.age] is False:
144
+ continue
145
+ ################################################################################
146
+
147
+ if c.age in self.all_shared_ages:
148
+ for ages in self.share_ages_list:
149
+ if c.age in ages: # if c belongs to some group`ages` in self.share_ages_list
150
+ for age in (
151
+ ages
152
+ ): # add child to each priority_age_share_dic[age] with age in group`ages`
153
+ if (
154
+ c.id not in self.priority_age_share_dic[age]
155
+ ): # caution : use `age` instead of `c.age`
156
+ self.priority_age_share_dic[age].append(c.id)
157
+ else:
158
+ if c.id not in self.priority_age_share_dic[c.age]:
159
+ self.priority_age_share_dic[c.age].append(c.id)
160
+
161
+ # notation \hat{G}(d, g)
162
+ def return_related_ages(self, age):
163
+ """
164
+ 指定された年齢と同じグループに属する年齢のリストを返す。
165
+ """
166
+ if age in self.all_shared_ages:
167
+ for ages in self.share_ages_list:
168
+ if age in ages:
169
+ return ages
170
+ else:
171
+ ralted_ages = []
172
+ ralted_ages.append(age)
173
+ return ralted_ages
174
+
175
+ # notation C_{better}(d, c, bool)
176
+ def return_better_children_than_child_excluding_siblings(
177
+ self,
178
+ child_id: int,
179
+ children: list[CP_Child],
180
+ allow_share_bool: bool = True,
181
+ exclude_bool: bool = True,
182
+ ) -> list[int]:
183
+ """指定した child_id より優先順位が高い児童の一覧を返す。
184
+
185
+ Args:
186
+ child_id: 基準とする child_id。
187
+ children: 全児童リスト。
188
+ allow_share_bool: True の場合、年齢枠共有を考慮した優先順位辞書を使う。
189
+ exclude_bool: True の場合、指定された児童と同じファミリーのきょうだいを除外する。
190
+ 定員チェックでのきょうだいの二重カウントを防ぐために True を使う。
191
+
192
+ Returns:
193
+ 以下を満たす child_id のリスト:
194
+ i) 指定された child_id より優先順位が高い
195
+ ii) allow_share_bool=False の場合は指定された児童と同じ年齢、
196
+ allow_share_bool=True の場合は定員枠を共有する年齢グループ内
197
+ iii) exclude_bool=True の場合、指定された児童のきょうだいではない
198
+ """
199
+ better_children_id = []
200
+ rank_dic = self.priority_age_share_dic if allow_share_bool is True else self.priority_age_dic
201
+ child = next((c for c in children if c.id == child_id), None)
202
+ pos = rank_dic[child.age].index(child_id) # find the position of child_id in rank_dic
203
+ for index in range(pos): # update better_children_id
204
+ c = next((c for c in children if c.id == rank_dic[child.age][index]), None)
205
+ if exclude_bool is True:
206
+ if c.family != child.family and c.id not in better_children_id: # exclude child's siblings
207
+ better_children_id.append(c.id)
208
+ else:
209
+ if c.id not in better_children_id: # include child's siblings
210
+ better_children_id.append(c.id)
211
+ return better_children_id
212
+
213
+ # notation C^{weak}_{better}(d, c, bool)
214
+ def return_weak_better_children_than_child_excluding_siblings(
215
+ self,
216
+ child_id: int,
217
+ children: list[CP_Child],
218
+ allow_share_bool: bool = True,
219
+ exclude_bool: bool = True,
220
+ search_depth: int = 5,
221
+ ) -> list[int]:
222
+ """指定した child_id 以上の優先スコアを持つ児童の一覧を返す(ほぼ同点を含む)。
223
+
224
+ Args:
225
+ child_id: 基準とする child_id。
226
+ children: 全児童リスト。
227
+ allow_share_bool: True の場合、年齢枠共有を考慮した優先順位辞書を使う。
228
+ exclude_bool: True の場合、指定された児童と同じファミリーのきょうだいを除外する。
229
+ 定員チェックでのきょうだいの二重カウントを防ぐために True を使う。
230
+ search_depth: 同スコアの児童を同順位グループとして探索する追加人数。
231
+ 0 のとき同スコアの児童はリスト上の並び順で優先される(位置がタイブレーカーになる)。
232
+ 基本は0でよい。
233
+ 0 より大きくすると同スコアの児童を同順位グループとして扱い、
234
+ 最適化ソルバーがグループ内の入所人数を最大化できるようになるが、
235
+ 値が大きいほど計算時間が増加する。
236
+
237
+ Returns:
238
+ 以下を満たす child_id のリスト:
239
+ i) 指定された child_id より優先順位が高い、
240
+ またはランクリスト上で search_depth 以内の位置にあり完全に同じスコアを持つ
241
+ ii) allow_share_bool=False の場合は指定された児童と同じ年齢、
242
+ allow_share_bool=True の場合は定員枠を共有する年齢グループ内
243
+ iii) exclude_bool=True の場合、指定された児童のきょうだいではない
244
+ """
245
+ better_children_id = []
246
+ rank_dic = self.priority_age_share_dic if allow_share_bool is True else self.priority_age_dic
247
+ child = next((c for c in children if c.id == child_id), None)
248
+ pos = rank_dic[child.age].index(child_id) # find the position of child_id in rank_dic
249
+ # determine how many children with the same priority score but lower priority need to be searched
250
+ end = min(pos + search_depth, len(rank_dic[child.age]))
251
+ for index in range(end):
252
+ c = next((c for c in children if c.id == rank_dic[child.age][index]), None)
253
+ if index < pos:
254
+ if exclude_bool is True:
255
+ if (
256
+ c.family != child.family and c.id not in better_children_id
257
+ ): # exclude child's siblings
258
+ better_children_id.append(c.id)
259
+ else:
260
+ if c.id not in better_children_id: # include child's siblings
261
+ better_children_id.append(c.id)
262
+ else:
263
+ child_score = self.score_list[self.priority.index(child.id)]
264
+ c_score = self.score_list[self.priority.index(c.id)]
265
+ if exclude_bool is True:
266
+ if (
267
+ c_score == child_score
268
+ and c.id != child.id
269
+ and c.family != child.family
270
+ and c.id not in better_children_id
271
+ ):
272
+ better_children_id.append(c.id)
273
+ else:
274
+ if c_score == child_score and c.id != child.id and c.id not in better_children_id:
275
+ better_children_id.append(c.id)
276
+
277
+ return better_children_id
278
+
279
+
280
+ class CP_Family:
281
+ """家族エージェント。
282
+
283
+ Attributes:
284
+ id (int): 家族ID。
285
+ children (list[int]): Child.id のリスト。
286
+ pref (list[list[int]]): 希望順位ごとの Daycare.id リスト(きょうだい人数分の要素を持つ)。
287
+ assignment (list[int] | None): 割当結果の Daycare.id リスト。未割当の場合は None。
288
+ has_siblings (bool): きょうだいがいるかどうか。
289
+ """
290
+
291
+ def __init__(
292
+ self,
293
+ f_id: int,
294
+ children_id_list: list = [int],
295
+ pref_list: list = [int],
296
+ assignment_daycare_id_list=None,
297
+ ):
298
+ self.id = f_id
299
+ self.children = children_id_list
300
+ self.pref = pref_list
301
+ self.assignment = assignment_daycare_id_list
302
+ self.has_siblings = len(self.children) > 1
303
+
304
+ def __str__(self):
305
+ return f"family {self.id}"
306
+
307
+ def __repr__(self):
308
+ return f"family {self.id}"
309
+
310
+ # notation D(f, p)
311
+ def return_daycare_id_for_certain_position(self, position):
312
+ """
313
+ family.pref の指定位置にある重複なしの daycare_id 集合を返す。
314
+ """
315
+ disjoint_d_ids = []
316
+ for d_id in self.pref[position]:
317
+ if d_id not in disjoint_d_ids:
318
+ disjoint_d_ids.append(d_id)
319
+ return disjoint_d_ids
320
+
321
+ # notation C(f, p, d)
322
+ def return_children_for_certain_position_and_daycare(self, position, daycare_id):
323
+ """
324
+ 指定位置で daycare_id に申し込むきょうだい集合を返す。
325
+ """
326
+ children_index = []
327
+ for index, d_id in enumerate(self.pref[position]):
328
+ if d_id == daycare_id and index not in children_index:
329
+ children_index.append(index)
330
+ # convert children_index into children_id
331
+ children_id = []
332
+ for index in children_index:
333
+ children_id.append(self.children[index])
334
+ return children_id
335
+
336
+ # notation C(f,p,d,g,bool)
337
+ def return_siblings_for_certain_position_daycare_age(
338
+ self, position, daycare_id, age, share_bool, children, daycares
339
+ ):
340
+ """
341
+ 以下を満たすきょうだい集合を返す。
342
+ i) 指定位置で daycare_id に申し込む
343
+ ii) 同じ年齢、または同じ年齢グループに属する
344
+ iii) 在園中だが在園枠を他児童へ再配分できない児童は除外する
345
+ """
346
+ children_id_age = []
347
+ children_id = self.return_children_for_certain_position_and_daycare(position, daycare_id)
348
+ used_ages = []
349
+ daycare = next((d for d in daycares if d.id == daycare_id), None)
350
+ if share_bool is True:
351
+ used_ages = daycare.return_related_ages(age)
352
+ else:
353
+ used_ages = [age]
354
+
355
+ # update children_id_age
356
+ if len(children_id) != 0:
357
+ for c_id in children_id:
358
+ child = next((c for c in children if c.id == c_id), None)
359
+
360
+ ################################################################################
361
+ # exclude the case if child is initially enrolled and occupied seats correspond to his age cannot be reused
362
+ if child.initial_daycare == daycare_id and daycare.use_transfer[child.age] is False:
363
+ continue
364
+ ################################################################################
365
+
366
+ if child.age in used_ages and child.id not in children_id_age:
367
+ children_id_age.append(child.id)
368
+ return children_id_age
369
+
370
+ # function C_{worst}(f, p, d, g, bool)
371
+ def return_lowest_sibling_for_certain_position_daycare_age(
372
+ self, position, daycare_id, age, share_bool, children_list, daycare_list
373
+ ):
374
+ """
375
+ 指定位置で daycare_id に申し込み、同じ年齢または同じ年齢グループに属する
376
+ きょうだいのうち、最も優先順位が低い児童を返す。
377
+ """
378
+ children_id_age = self.return_siblings_for_certain_position_daycare_age(
379
+ position, daycare_id, age, share_bool, children_list, daycare_list
380
+ )
381
+ daycare = next((d for d in daycare_list if d.id == daycare_id), None)
382
+ # determine which priority_dic will be used controlled by share_bool
383
+ rank_dic = daycare.priority_age_share_dic if share_bool is True else daycare.priority_age_dic
384
+ # find the worst child_id
385
+ worst_index = -1
386
+ worst_child_id = -1
387
+ for c_id in children_id_age:
388
+ if rank_dic[age].index(c_id) > worst_index:
389
+ worst_index = rank_dic[age].index(c_id)
390
+ worst_child_id = c_id
391
+ return worst_child_id
@@ -0,0 +1,294 @@
1
+ from logging import getLogger
2
+
3
+ from ortools.sat.python import cp_model
4
+
5
+ from chilmai.constants import UNMATCHED_DAYCARE_ID
6
+ from .helper_functions import *
7
+
8
+ log = getLogger("chilmai.matching")
9
+
10
+
11
+ class OptimizationFailureError(Exception):
12
+ pass
13
+
14
+
15
+ def CP(
16
+ children_dic,
17
+ daycares_dic,
18
+ families_dic,
19
+ share_bool,
20
+ bp_num=0,
21
+ solver_time=360,
22
+ exclude_bool=True,
23
+ search_depth=5,
24
+ ):
25
+ model = cp_model.CpModel()
26
+ # generate agents
27
+ children, daycares, families = create_agents(children_dic, daycares_dic, families_dic)
28
+
29
+ # generate CP variables
30
+ xfp, xcd, alpha, gamma_fp, gamma_fpd, gamma_fpdg, age_fpd, beta = create_variables(
31
+ children, daycares, families, share_bool, model, exclude_bool, search_depth
32
+ )
33
+
34
+ # impose feasibility constraints
35
+ feasibility_constraints(children, daycares, families, share_bool, xfp, xcd, model)
36
+
37
+ # impose blocking coalition constraints
38
+ bp = [beta[f.id, p] for f in families for p in range(len(f.pref))]
39
+ model.Add(sum(bp) <= bp_num)
40
+
41
+ # objective: maximize the number of matched children
42
+ all_xcd = [
43
+ xcd[c.id, d_id] for c in children for d_id in c.all_daycare_ids if d_id != UNMATCHED_DAYCARE_ID
44
+ ]
45
+ model.Maximize(sum(all_xcd))
46
+
47
+ # running time
48
+ import time
49
+
50
+ time_sta = time.time()
51
+
52
+ # invoke the CP-SAT solver
53
+ solver = cp_model.CpSolver()
54
+ solver.parameters.num_search_workers = 8
55
+ solver.parameters.max_time_in_seconds = solver_time
56
+ status = solver.Solve(model)
57
+ if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
58
+ raise OptimizationFailureError(status)
59
+ log.debug(solver.StatusName(status))
60
+ log.debug(f"Totally matched: {solver.ObjectiveValue()}")
61
+
62
+ # ending time
63
+ time_end = time.time()
64
+ tim = time_end - time_sta
65
+ log.debug(f"solver time elapsed {tim}")
66
+
67
+ # determine assignment
68
+ outcome_fp = {}
69
+ for f in families:
70
+ for p in range(len(f.pref)):
71
+ outcome_fp[f.id, p] = solver.Value(xfp[f.id, p])
72
+ if outcome_fp[f.id, p] == 1:
73
+ for c_id in f.children:
74
+ c = get_agent(c_id, children)
75
+ c.assigned_daycare = c.projected_pref[p]
76
+
77
+ # store outcome
78
+ outcome_children_dic = {}
79
+ for c in children:
80
+ outcome_children_dic[c.id] = {}
81
+ if c.assigned_daycare is None:
82
+ c.assigned_daycare = UNMATCHED_DAYCARE_ID
83
+ outcome_children_dic[c.id]["CP"] = c.assigned_daycare
84
+
85
+ return outcome_children_dic, outcome_fp, children, daycares, families, status
86
+
87
+
88
+ # create xfp
89
+ def creat_variables_xfp(families, model):
90
+ xfp = {}
91
+ for f in families:
92
+ for p in range(len(f.pref)):
93
+ xfp[f.id, p] = model.NewBoolVar(f"xfp_[{f.id}, {p}]")
94
+ return xfp
95
+
96
+
97
+ # create xcd
98
+ def creat_variables_xcd(children, daycares, families, xfp, model):
99
+ xcd = {}
100
+ for c in children:
101
+ if len(c.projected_pref) != 0: # ignore children who do not have preferences
102
+ # only consider daycares which are listed in c.projected_pref, denoted by c.all_daycare_ids
103
+ for d_id in c.all_daycare_ids:
104
+ xcd[c.id, d_id] = model.NewBoolVar(f"xcd_[{c.id}, {d_id}]")
105
+ f_c = get_agent(c.family, families)
106
+ all_positions = c.return_all_positions_of_certain_dacyare_in_projected_pref(d_id)
107
+ model.Add((xcd[c.id, d_id] == (sum(xfp[f_c.id, p] for p in all_positions))))
108
+ return xcd
109
+
110
+
111
+ # create alpha
112
+ def creat_variables_alpha(families, xfp, model):
113
+ alpha = {}
114
+ for f in families:
115
+ for p in range(len(f.pref)):
116
+ alpha[f.id, p] = model.NewBoolVar(f"alpha_[{f.id}, {p}]")
117
+ if p == 0:
118
+ # Base case: alpha[f,0] mirrors xfp[f,0]
119
+ model.Add(alpha[f.id, p] == xfp[f.id, p])
120
+ else:
121
+ prev_alpha = alpha[f.id, p - 1]
122
+ current_xfp = xfp[f.id, p]
123
+ # The following three constraints together enforce:
124
+ # alpha[f,p] == (prev_alpha OR current_xfp)
125
+ # which is equivalent to:
126
+ # alpha[f,p] == sum(xfp[f,k] for k in range(p+1))
127
+ model.AddImplication(prev_alpha, alpha[f.id, p])
128
+ model.AddImplication(current_xfp, alpha[f.id, p])
129
+ model.AddBoolOr([prev_alpha, current_xfp, alpha[f.id, p].Not()])
130
+ return alpha
131
+
132
+
133
+ # create gamma
134
+ # updated on 2024.04.26
135
+ def creat_variables_gamma_siblings(
136
+ children,
137
+ daycares,
138
+ share_bool,
139
+ f,
140
+ xcd,
141
+ alpha,
142
+ gamma_fp,
143
+ gamma_fpd,
144
+ gamma_fpdg,
145
+ age_fpd,
146
+ model,
147
+ exclude_bool,
148
+ search_depth,
149
+ ):
150
+ # for each position p, add one constraint for gamma[f, p]
151
+ for p in range(len(f.pref)):
152
+ for d_id in f.return_daycare_id_for_certain_position(p): # for each d \in D(f, p)
153
+ d = get_agent(d_id, daycares)
154
+ age_fpd[f.id, p, d_id] = [] # a list of ages that will be used later
155
+ capacity = d.total_numbers_share if share_bool is True else d.total_numbers
156
+
157
+ for g in range(6):
158
+ # number_fpdg : the number of children who i) are from the same family f ii) have grade related to g iii) apply to d w.r.t. \succ_{f, p} and iv) can distribute initial seat if appliable
159
+ number_fpdg = len(
160
+ f.return_siblings_for_certain_position_daycare_age(
161
+ p, d_id, g, share_bool, children, daycares
162
+ )
163
+ )
164
+ # only consider the case where number_fpdg > 0
165
+ if number_fpdg != 0:
166
+ # variable gamma_fpdg
167
+ gamma_fpdg[f.id, p, d_id, g] = model.NewBoolVar(
168
+ f"gamma_fpdg_[{f.id}, {p}, {d_id}, {g}]"
169
+ )
170
+ age_fpd[f.id, p, d_id].append(g) # update age_fpd
171
+ # find the child c* with the lowest priority who i) is from family f ii) has grade related to g iii) applies to d at \succ_{c, p}
172
+ worst_id = f.return_lowest_sibling_for_certain_position_daycare_age(
173
+ p, d_id, g, share_bool, children, daycares
174
+ )
175
+ # find all children who i) are not from family f ii) have higher priority than c*
176
+ children_better = d.return_weak_better_children_than_child_excluding_siblings(
177
+ worst_id, children, share_bool, exclude_bool, search_depth
178
+ )
179
+ # consider the case where no child better than c* exists
180
+ if len(children_better) == 0:
181
+ if capacity[g] >= number_fpdg:
182
+ model.Add(gamma_fpdg[f.id, p, d_id, g] == 1)
183
+ else:
184
+ model.Add(gamma_fpdg[f.id, p, d_id, g] == 0)
185
+ # Channeling constraints: refer to the AAAI 24 paper
186
+ else:
187
+ model.Add(
188
+ (sum(xcd[c_id, d.id] for c_id in children_better) + number_fpdg) <= capacity[g]
189
+ ).OnlyEnforceIf(gamma_fpdg[f.id, p, d_id, g])
190
+ model.Add(
191
+ (sum(xcd[c_id, d.id] for c_id in children_better) + number_fpdg) > capacity[g]
192
+ ).OnlyEnforceIf(gamma_fpdg[f.id, p, d_id, g].Not())
193
+
194
+ # variable \gamma[f,p,d]
195
+ gamma_fpd[f.id, p, d_id] = model.NewBoolVar(f"gamma_fpd_[{f.id}, {p}, {d_id}]")
196
+ # Channeling constraints: refer to the AAAI 24 paper
197
+ model.Add(
198
+ sum(gamma_fpdg[f.id, p, d_id, g] for g in age_fpd[f.id, p, d_id])
199
+ == len(age_fpd[f.id, p, d_id])
200
+ ).OnlyEnforceIf(gamma_fpd[f.id, p, d_id])
201
+ model.Add(
202
+ sum(gamma_fpdg[f.id, p, d_id, g] for g in age_fpd[f.id, p, d_id])
203
+ < len(age_fpd[f.id, p, d_id])
204
+ ).OnlyEnforceIf(gamma_fpd[f.id, p, d_id].Not())
205
+
206
+ # variable \gamma[f,p]
207
+ gamma_fp[f.id, p] = model.NewBoolVar(f"gamma_fp_[{f.id}, {p}]")
208
+ id_fp = f.return_daycare_id_for_certain_position(p)
209
+ # Channeling constraints: refer to the AAAI 24 paper
210
+ model.Add(sum(gamma_fpd[f.id, p, d_id] for d_id in id_fp) == len(id_fp)).OnlyEnforceIf(
211
+ gamma_fp[f.id, p]
212
+ )
213
+ model.Add(sum(gamma_fpd[f.id, p, d_id] for d_id in id_fp) < len(id_fp)).OnlyEnforceIf(
214
+ gamma_fp[f.id, p].Not()
215
+ )
216
+
217
+
218
+ # creat gamma
219
+ def creat_variables_gamma(
220
+ children, daycares, families, share_bool, xcd, alpha, model, exclude_bool, search_depth
221
+ ):
222
+ gamma_fpdg = {}
223
+ gamma_fpd = {}
224
+ gamma_fp = {}
225
+ age_fpd = {}
226
+
227
+ for f in families:
228
+ creat_variables_gamma_siblings(
229
+ children,
230
+ daycares,
231
+ share_bool,
232
+ f,
233
+ xcd,
234
+ alpha,
235
+ gamma_fp,
236
+ gamma_fpd,
237
+ gamma_fpdg,
238
+ age_fpd,
239
+ model,
240
+ exclude_bool,
241
+ search_depth,
242
+ )
243
+
244
+ return gamma_fp, gamma_fpd, gamma_fpdg, age_fpd
245
+
246
+
247
+ # create beta
248
+ def creat_variables_beta(families, alpha, gamma_fp, model):
249
+ beta = {}
250
+ for f in families:
251
+ for p in range(len(f.pref)):
252
+ beta[f.id, p] = model.NewBoolVar(f"beta_[{f.id}, {p}]")
253
+ model.Add((beta[f.id, p] == 0)).OnlyEnforceIf(alpha[f.id, p])
254
+ model.Add((beta[f.id, p] == gamma_fp[f.id, p])).OnlyEnforceIf(alpha[f.id, p].Not())
255
+ return beta
256
+
257
+
258
+ # create all variables
259
+ def create_variables(children, daycares, families, share_bool, model, exclude_bool, search_depth):
260
+ xfp = creat_variables_xfp(families, model)
261
+ xcd = creat_variables_xcd(children, daycares, families, xfp, model)
262
+ alpha = creat_variables_alpha(families, xfp, model)
263
+ gamma_fp, gamma_fpd, gamma_fpdg, age_fpd = creat_variables_gamma(
264
+ children, daycares, families, share_bool, xcd, alpha, model, exclude_bool, search_depth
265
+ )
266
+ beta = creat_variables_beta(families, alpha, gamma_fp, model)
267
+ return xfp, xcd, alpha, gamma_fp, gamma_fpd, gamma_fpdg, age_fpd, beta
268
+
269
+
270
+ # feasibility constraints for families and daycares
271
+ # update Apr 26, 2024
272
+ def feasibility_constraints(children, daycares, families, share_bool, xfp, xcd, model):
273
+ # find a set of families with children who prefer to transfer
274
+ f_transfer = []
275
+ for c in children:
276
+ if c.initial_daycare != UNMATCHED_DAYCARE_ID and c.family not in f_transfer:
277
+ f_transfer.append(c.family)
278
+
279
+ # feasibility constraints for families
280
+ for f in families:
281
+ if f.id in f_transfer:
282
+ model.Add(sum(xfp[f.id, p] for p in range(len(f.pref))) == 1)
283
+ else:
284
+ model.Add(sum(xfp[f.id, p] for p in range(len(f.pref))) <= 1)
285
+
286
+ # feasibility constraints for daycares
287
+ for d in daycares:
288
+ rank_dic = d.priority_age_share_dic if share_bool is True else d.priority_age_dic
289
+ for g in range(6):
290
+ if len(rank_dic[g]) != 0:
291
+ if share_bool is False:
292
+ model.Add(sum(xcd[c_id, d.id] for c_id in rank_dic[g]) <= d.total_numbers[g])
293
+ else:
294
+ model.Add(sum(xcd[c_id, d.id] for c_id in rank_dic[g]) <= d.total_numbers_share[g])