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.
@@ -0,0 +1,6 @@
1
+ """ChilmAI が利用する OR-Tools ベースの保育所利用調整ソルバー。
2
+
3
+ この package は低レイヤーの CP-SAT 実装を含む。
4
+ 通常の呼び出し元は、これらの module を直接 import せず、
5
+ `chilmai.generic.service.MatchingService` を利用する。
6
+ """
@@ -0,0 +1,348 @@
1
+ import logging
2
+
3
+ from chilmai.constants import UNMATCHED_DAYCARE_ID
4
+ from .CP_agents import CP_Daycare, CP_Child, CP_Family
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def get_agent(agent_id, agents):
10
+ """
11
+ 指定 ID に対応する CP_agent インスタンスを返す(児童・保育所・世帯を含む)。
12
+ """
13
+ agent = next((x for x in agents if x.id == agent_id), None)
14
+ return agent
15
+
16
+
17
+ def create_children(children_dic):
18
+ """
19
+ children_dic に保存されたデータから CP_Child インスタンスのリストを作成する。
20
+ """
21
+ children = []
22
+ for c_id in children_dic.keys():
23
+ c_id = children_dic[c_id]["id"]
24
+ age = children_dic[c_id]["age"]
25
+ if children_dic[c_id]["family_id"] is None:
26
+ family_id = children_dic[c_id]["id"]
27
+ else:
28
+ family_id = children_dic[c_id]["family_id"]
29
+ initial_daycare_id = (
30
+ UNMATCHED_DAYCARE_ID
31
+ if children_dic[c_id]["initial_daycare_id"] is None
32
+ else children_dic[c_id]["initial_daycare_id"]
33
+ )
34
+ preference_list = children_dic[c_id]["preference_list"]
35
+ # create a CP_Child instance
36
+ c = CP_Child(c_id, age, family_id, initial_daycare_id, preference_list)
37
+ children.append(c)
38
+ return children
39
+
40
+
41
+ def create_daycares(daycares_dic):
42
+ """
43
+ daycares_dic に保存されたデータから CP_Daycare インスタンスのリストを作成する。
44
+ """
45
+ daycares = []
46
+ for d_id in daycares_dic.keys():
47
+ d_id = daycares_dic[d_id]["id"]
48
+ recruiting_numbers_list = daycares_dic[d_id]["recruiting_numbers_list"]
49
+ share_ages_list = daycares_dic[d_id]["share_ages_list"]
50
+ priority_child_id_list = daycares_dic[d_id]["priority_child_id_list"]
51
+ priority_score_list = daycares_dic[d_id]["priority_score_list"]
52
+
53
+ # update April 13, 2024
54
+ if "is_use_transfer" in daycares_dic[d_id].keys():
55
+ is_use_transfer = daycares_dic[d_id]["is_use_transfer"]
56
+ else:
57
+ is_use_transfer = [True, True, True, True, True, True]
58
+
59
+ # create a CP_Daycare instance
60
+ d = CP_Daycare(
61
+ d_id,
62
+ recruiting_numbers_list,
63
+ share_ages_list,
64
+ priority_child_id_list,
65
+ priority_score_list,
66
+ is_use_transfer,
67
+ )
68
+ daycares.append(d)
69
+
70
+ # create a dummy daycare for being unmatched
71
+ dummy_id = UNMATCHED_DAYCARE_ID
72
+ recruiting_numbers_list = [9999 for i in range(6)] # large capacity (not a daycare ID)
73
+ share_ages_list = []
74
+ priority_child_id_list = []
75
+ priority_score_list = []
76
+ is_use_transfer = [True, True, True, True, True, True]
77
+ dummy = CP_Daycare(
78
+ dummy_id,
79
+ recruiting_numbers_list,
80
+ share_ages_list,
81
+ priority_child_id_list,
82
+ priority_score_list,
83
+ is_use_transfer,
84
+ )
85
+ daycares.append(dummy)
86
+ return daycares
87
+
88
+
89
+ def create_families(families_dic):
90
+ """
91
+ families_dic に保存されたデータから CP_Family インスタンスのリストを作成する。
92
+ """
93
+ families = []
94
+ for f_id in families_dic.keys():
95
+ f_id = families_dic[f_id]["id"]
96
+ children_id_list = families_dic[f_id]["children"]
97
+ pref_list = families_dic[f_id]["pref"]
98
+ assignment = None
99
+ # create a CP_Family instance
100
+ f = CP_Family(f_id, children_id_list, pref_list, assignment)
101
+ families.append(f)
102
+ return families
103
+
104
+
105
+ def update_families_attributes(families):
106
+ """
107
+ 単独児世帯について、以下を適用する。
108
+ 1) f.pref: list[int] を f.pref: list[tuple(int)] に変換する
109
+ 2) None を UNMATCHED_DAYCARE_ID に変換する
110
+ """
111
+ for f in families:
112
+ if f.has_siblings is False:
113
+ f.pref = [tuple([d_id]) for d_id in f.pref]
114
+ for pos in range(len(f.pref)):
115
+ new = []
116
+ tup_p = f.pref[pos]
117
+ for item in tup_p:
118
+ if item is None:
119
+ new.append(UNMATCHED_DAYCARE_ID)
120
+ else:
121
+ new.append(item)
122
+ f.pref[pos] = tuple(new)
123
+
124
+
125
+ def update_children_attributes(children, families):
126
+ """
127
+ 1) f.pref から c.projected_pref を作る
128
+ 2) c.projected_pref から c.all_daycare_ids を計算する
129
+ """
130
+ # update c.projected_pref
131
+ for f in families:
132
+ for pos in range(len(f.pref)):
133
+ tup_p = f.pref[pos]
134
+ for index, d_id in enumerate(tup_p):
135
+ f_c = get_agent(f.children[index], children)
136
+ f_c.projected_pref.append(tup_p[index])
137
+ # update c.all_daycare_ids
138
+ for c in children:
139
+ c_d_ids = []
140
+ for d_id in c.projected_pref:
141
+ if d_id not in c_d_ids:
142
+ c_d_ids.append(d_id)
143
+ c.all_daycare_ids = c_d_ids
144
+
145
+
146
+ def update_daycares_attributes(children, daycares):
147
+ """
148
+ 1) ダミー保育所(UNMATCHED_DAYCARE_ID)の優先順位・スコアリストを更新する
149
+ 2) d.priority_age_dic と d.priority_age_share_dic を更新する
150
+ 3) d.total_numbers と d.total_numbers_share を更新する
151
+ """
152
+ # update dummy.priority
153
+ dummy = get_agent(UNMATCHED_DAYCARE_ID, daycares)
154
+ for c in children:
155
+ if UNMATCHED_DAYCARE_ID in c.projected_pref and c.id not in dummy.priority:
156
+ dummy.priority.append(c.id)
157
+
158
+ # update dummy.score_list
159
+ dummy.score_list = [100 for x in range(len(dummy.priority))]
160
+
161
+ # update d.priority_age_dic & d.priority_age_share_dic
162
+ for d in daycares:
163
+ d.update_priority_age_dic(children)
164
+ d.update_priority_age_share_dic(children)
165
+
166
+ # update d.total_numbers
167
+ # 在園児が転園アウトすると枠が空くため、在園先の定員へ加算する。
168
+ # use_transfer[age] が False の年齢は空いた枠を再配分しないので加算しない。
169
+ for c in children:
170
+ initial = get_agent(c.initial_daycare, daycares)
171
+ if initial.use_transfer[c.age] is True:
172
+ initial.total_numbers[c.age] += 1
173
+
174
+ # update d.total_numbers_share
175
+ for d in daycares:
176
+ d.total_numbers_share = [x for x in d.total_numbers]
177
+ if len(d.all_shared_ages) > 0:
178
+ for ages in d.share_ages_list:
179
+ quota_ages = 0
180
+ for age in ages:
181
+ quota_ages += d.total_numbers[age]
182
+ for age in ages:
183
+ d.total_numbers_share[age] = quota_ages
184
+
185
+
186
+ def create_agents(children_dic, daycares_dic, families_dic):
187
+ """
188
+ 以下の関数呼び出し順序を変更しないこと。
189
+ """
190
+ children = create_children(children_dic)
191
+ daycares = create_daycares(daycares_dic)
192
+ families = create_families(families_dic)
193
+ update_families_attributes(families)
194
+ update_children_attributes(children, families)
195
+ update_daycares_attributes(children, daycares)
196
+ return children, daycares, families
197
+
198
+
199
+ def check_IR(children):
200
+ non_IR_children = []
201
+ for c in children:
202
+ if c.initial_daycare != UNMATCHED_DAYCARE_ID and c.assigned_daycare == UNMATCHED_DAYCARE_ID:
203
+ non_IR_children.append(c.id)
204
+
205
+ if len(non_IR_children) == 0:
206
+ logger.debug("the outcome satisfies IR")
207
+ else:
208
+ logger.debug("the outcome does not satisfy IR")
209
+
210
+ return non_IR_children
211
+
212
+
213
+ # NOT modified yet 2024.04.25
214
+ def check_feasibility(children, daycares, share_bool):
215
+ infeasible_daycare_ids = []
216
+
217
+ # number of matched children by age
218
+ outcome_daycares = {}
219
+ for d in daycares:
220
+ outcome_daycares[d.id] = [0 for g in range(6)]
221
+ for c in children:
222
+ if c.assigned_daycare is not None:
223
+ outcome_daycares[c.assigned_daycare][c.age] += 1
224
+
225
+ # check feasibility
226
+ if share_bool is False:
227
+ for d in daycares:
228
+ for g in range(6):
229
+ if outcome_daycares[d.id][g] > d.total_numbers[g] and d.id not in infeasible_daycare_ids:
230
+ infeasible_daycare_ids.append(d.id)
231
+ else:
232
+ for d in daycares:
233
+ for g in range(6):
234
+ if g in d.all_shared_ages:
235
+ related_ages = d.return_related_ages(g)
236
+ sum_matched = 0
237
+ for x in related_ages:
238
+ sum_matched += outcome_daycares[d.id][x]
239
+
240
+ if sum_matched > d.total_numbers_share[g] and d.id not in infeasible_daycare_ids:
241
+ infeasible_daycare_ids.append(d.id)
242
+ else:
243
+ if (
244
+ outcome_daycares[d.id][g] > d.total_numbers[g]
245
+ and d.id not in infeasible_daycare_ids
246
+ ):
247
+ infeasible_daycare_ids.append(d.id)
248
+
249
+ if len(infeasible_daycare_ids) == 0:
250
+ logger.debug("the outcome is feasible")
251
+ else:
252
+ logger.debug("the outcome is NOT feasible")
253
+
254
+ return infeasible_daycare_ids
255
+
256
+
257
+ # NOT modified yet 2024.04.25
258
+ def check_bp(children_dic, daycares_dic, families_dic, outcome_f):
259
+ # create CP_agents
260
+ children, daycares, families = create_agents(children_dic, daycares_dic, families_dic)
261
+
262
+ # update f.assignment & c.assigned_dacayre
263
+ for f in families:
264
+ for p in range(len(f.pref)):
265
+ # 注意: outcome_f は重要だ!!!
266
+ if outcome_f[f.id, p] == 1:
267
+ f.assignment = p
268
+
269
+ for cid in f.children:
270
+ c = get_agent(cid, children)
271
+ c.assigned_daycare = c.projected_pref[p]
272
+
273
+ if f.assignment is None:
274
+ f.assignment = len(f.pref)
275
+
276
+ # blocking coalition
277
+ bp_dic = {}
278
+ for f in families:
279
+ bp_dic[f.id] = []
280
+
281
+ for f in families:
282
+ # ignore all families who are matched to their top choice
283
+ if f.assignment != 0:
284
+ # check every position prior to f.assignment
285
+ for p in range(f.assignment):
286
+ # D(f, p)
287
+ D_fp = f.return_daycare_id_for_certain_position(p)
288
+ bool_all = 0
289
+
290
+ for d in D_fp:
291
+ # default all 0 (could accept C_fpd)
292
+ bool_fpd = [0 for i in range(6)]
293
+ # C(f,p,d)
294
+ f.return_children_for_certain_position_and_daycare(p, d)
295
+ for g in range(6):
296
+ # C(f,p,d,g)
297
+ C_fpdg = f.return_siblings_for_certain_position_daycare_age(
298
+ p, d, g, False, children, daycares
299
+ )
300
+ if len(C_fpdg) != 0:
301
+ w_fpd = f.return_lowest_sibling_for_certain_position_daycare_age(
302
+ p, d, g, False, children, daycares
303
+ )
304
+ daycare = get_agent(d, daycares)
305
+ better_cid = daycare.return_weak_better_children_than_child_excluding_siblings(
306
+ w_fpd, children, False, True, 0
307
+ )
308
+ better_number = 0
309
+ for x in better_cid:
310
+ xc = get_agent(x, children)
311
+ if xc.assigned_daycare == d:
312
+ better_number += 1
313
+
314
+ # CP モデルの定員制約と check_feasibility に合わせ、転園アウト
315
+ # 込みの total_numbers を閾値にする。
316
+ if better_number + len(C_fpdg) > daycare.total_numbers[g]:
317
+ bool_fpd[g] = 1
318
+
319
+ if sum(bool_fpd) == 0:
320
+ bool_all += 1
321
+
322
+ if bool_all == len(D_fp):
323
+ if p not in bp_dic[f.id]:
324
+ bp_dic[f.id].append(p)
325
+
326
+ num_bp = 0
327
+ for key in bp_dic.keys():
328
+ num_bp += len(bp_dic[key])
329
+
330
+ if num_bp == 0:
331
+ logger.debug("the outcome is stable")
332
+ else:
333
+ logger.debug("the outcome is NOT stable")
334
+
335
+ return bp_dic
336
+
337
+
338
+ def check_outcome(children_dic, daycares_dic, families_dic, outcome_children_dic, outcome_f, share_bool):
339
+ children, daycares, families = create_agents(children_dic, daycares_dic, families_dic)
340
+
341
+ for c in children:
342
+ c.assigned_daycare = outcome_children_dic[c.id]["CP"]
343
+
344
+ non_IR_children = check_IR(children)
345
+ infeasible_daycare_ids = check_feasibility(children, daycares, share_bool)
346
+ bp_dic = check_bp(children_dic, daycares_dic, families_dic, outcome_f)
347
+
348
+ return non_IR_children, infeasible_daycare_ids, bp_dic
chilmai/constants.py ADDED
@@ -0,0 +1,3 @@
1
+ # Sentinel value for "unmatched" dummy daycare.
2
+ # Negative so it can never collide with a real daycare ID (positive integers only).
3
+ UNMATCHED_DAYCARE_ID = -1
@@ -0,0 +1,7 @@
1
+ """Generic municipality matching package."""
2
+
3
+ from chilmai.generic.config import ConfigStore
4
+ from chilmai.generic.preprocessor import BasePreprocessor
5
+ from chilmai.generic.service import MatchingService
6
+
7
+ __all__ = ["ConfigStore", "MatchingService", "BasePreprocessor"]
@@ -0,0 +1,214 @@
1
+ {
2
+ "children": {
3
+ "child_id": {
4
+ "aliases": [
5
+ "申請者番号",
6
+ "申請者ID",
7
+ "申請ID",
8
+ "申請者No",
9
+ "申請No",
10
+ "申込者番号",
11
+ "申込者ID",
12
+ "申込番号",
13
+ "申請書番号",
14
+ "受付番号",
15
+ "受付No",
16
+ "管理番号",
17
+ "整理番号",
18
+ "ケースNo",
19
+ "利用者番号",
20
+ "児童ID",
21
+ "児童番号",
22
+ "子ども番号"
23
+ ]
24
+ },
25
+ "household_id": {
26
+ "aliases": [
27
+ "世帯番号",
28
+ "世帯ID",
29
+ "世帯No",
30
+ "世帯コード",
31
+ "家族番号",
32
+ "家族ID",
33
+ "保護者番号",
34
+ "保護者ID",
35
+ "世帯主番号"
36
+ ]
37
+ },
38
+ "age": {
39
+ "aliases": [
40
+ "年齢",
41
+ "年齢区分",
42
+ "クラス年齢",
43
+ "入所時年齢",
44
+ "入所希望年齢",
45
+ "入所予定年齢",
46
+ "申請年齢",
47
+ "保育年齢"
48
+ ]
49
+ },
50
+ "score": {
51
+ "aliases": [
52
+ "点数",
53
+ "スコア",
54
+ "指数",
55
+ "score",
56
+ "優先点",
57
+ "選考指数",
58
+ "調整指数",
59
+ "基準指数",
60
+ "入所指数",
61
+ "希望指数",
62
+ "ポイント"
63
+ ]
64
+ },
65
+ "score_prefix": {
66
+ "templates": [
67
+ "指数N",
68
+ "スコアN",
69
+ "score_N",
70
+ "第N希望点数",
71
+ "第N希望スコア",
72
+ "第N希望指数",
73
+ "N希望指数",
74
+ "N希望点数",
75
+ "選考指数N",
76
+ "調整指数N",
77
+ "基準指数N",
78
+ "入所指数N",
79
+ "点数N",
80
+ "優先点N",
81
+ "ポイントN"
82
+ ]
83
+ },
84
+ "preference_prefix": {
85
+ "templates": [
86
+ "希望保育園ID_N",
87
+ "希望保育所ID_N",
88
+ "希望施設ID_N",
89
+ "第N希望保育園",
90
+ "第N希望保育所",
91
+ "第N希望施設",
92
+ "第N希望施設ID",
93
+ "第N希望園",
94
+ "N番目希望",
95
+ "希望N",
96
+ "pref_N"
97
+ ]
98
+ },
99
+ "enrolled_daycare_id": {
100
+ "aliases": [
101
+ "在籍保育所ID",
102
+ "在籍保育園ID",
103
+ "在籍施設ID",
104
+ "在籍園ID",
105
+ "転園元保育所ID",
106
+ "転園元保育園ID",
107
+ "転園元施設ID",
108
+ "転園元園ID",
109
+ "現保育所ID",
110
+ "現在の保育所ID",
111
+ "現在の施設ID",
112
+ "現在の園ID",
113
+ "在園保育所ID",
114
+ "通園中保育所ID"
115
+ ]
116
+ },
117
+ "sibling_pattern": {
118
+ "aliases": [
119
+ "きょうだいパターン",
120
+ "兄弟姉妹パターン",
121
+ "兄弟パターン",
122
+ "きょうだい希望パターン",
123
+ "兄弟入所区分",
124
+ "きょうだい申請区分",
125
+ "多子申請区分",
126
+ "同時申請区分"
127
+ ]
128
+ }
129
+ },
130
+ "daycares": {
131
+ "daycare_id": {
132
+ "aliases": [
133
+ "保育所ID",
134
+ "保育園ID",
135
+ "施設ID",
136
+ "園ID",
137
+ "保育所コード",
138
+ "保育園コード",
139
+ "施設コード",
140
+ "園コード",
141
+ "保育所番号",
142
+ "施設番号",
143
+ "園番号",
144
+ "保育所管理番号",
145
+ "施設管理番号"
146
+ ]
147
+ },
148
+ "daycare_name": {
149
+ "aliases": [
150
+ "保育所名",
151
+ "保育園名",
152
+ "施設名",
153
+ "園名",
154
+ "保育所名称",
155
+ "保育園名称",
156
+ "施設名称",
157
+ "園名称",
158
+ "施設正式名称",
159
+ "認可施設名"
160
+ ]
161
+ },
162
+ "capacity_prefix": {
163
+ "templates": [
164
+ "N歳募集人数",
165
+ "N歳定員",
166
+ "N歳児定員",
167
+ "N歳児募集人数",
168
+ "N歳クラス定員",
169
+ "N歳クラス募集人数",
170
+ "N歳受入可能数",
171
+ "定員N歳",
172
+ "定員N歳枠",
173
+ "募集人数N",
174
+ "capacity_age_N",
175
+ "capacity_ageN"
176
+ ]
177
+ }
178
+ },
179
+ "combination": {
180
+ "household_id": {
181
+ "aliases": [
182
+ "ファミリーコード",
183
+ "家族コード",
184
+ "世帯コード",
185
+ "世帯番号",
186
+ "世帯ID"
187
+ ]
188
+ },
189
+ "rank": {
190
+ "aliases": [
191
+ "総当たり順位",
192
+ "組み合わせ順位",
193
+ "希望順位",
194
+ "組合せ順位"
195
+ ]
196
+ },
197
+ "child_code_prefix": {
198
+ "templates": [
199
+ "宛名コードN",
200
+ "児童コードN",
201
+ "申請者コードN",
202
+ "子どもコードN"
203
+ ]
204
+ },
205
+ "facility_prefix": {
206
+ "templates": [
207
+ "希望施設N",
208
+ "希望保育所N",
209
+ "希望保育園N",
210
+ "施設コードN"
211
+ ]
212
+ }
213
+ }
214
+ }