HaLim commited on
Commit
89d7197
·
1 Parent(s): 08284a1

Finalize ETL and optimization

Browse files
data/real_data_excel/converted_csv/WH_Workforce_Hourly_Pay_Scale_processed.csv CHANGED
@@ -5,7 +5,7 @@ id,employment_type_id,employment_type,shift,shift_type_id,level,Value (USD)
5
  3,1,UNICEF Fixed term,Day shift,1, GS4 ,43.27451923076923
6
  3,1,UNICEF Fixed term,evening_shift,2, GS4 ,43.27451923076923
7
  4,1,UNICEF Fixed term,overtime,3, GS4 ,64.91177884615385
8
- 5,1,UNICEF Fixed term,Day shift,1, GS5 ,44.35817307692308,
9
  5,1,UNICEF Fixed term,evening_shift,2, GS5 ,44.35817307692308
10
  6,1,UNICEF Fixed term,overtime,3, GS5 ,66.53725961538461
11
  7,1,UNICEF Fixed term,Day shift,1, GS7 ,57.666826923076925
 
5
  3,1,UNICEF Fixed term,Day shift,1, GS4 ,43.27451923076923
6
  3,1,UNICEF Fixed term,evening_shift,2, GS4 ,43.27451923076923
7
  4,1,UNICEF Fixed term,overtime,3, GS4 ,64.91177884615385
8
+ 5,1,UNICEF Fixed term,Day shift,1, GS5 ,44.35817307692308
9
  5,1,UNICEF Fixed term,evening_shift,2, GS5 ,44.35817307692308
10
  6,1,UNICEF Fixed term,overtime,3, GS5 ,66.53725961538461
11
  7,1,UNICEF Fixed term,Day shift,1, GS7 ,57.666826923076925
data/real_data_excel/converted_csv/Work_Centre_Capacity_processed.csv CHANGED
@@ -5,4 +5,4 @@ id,Work_Area,per_hour_capacity,per_hour_unit,line_for_packaging,line_count
5
  4,Pre-pack_lines,300.0,cartons,False,1
6
  5,Pre-pick_station,54.0,pallets,False,1
7
  6,Long_line,300.0,cartons,True,2
8
- 7,Short_ine,54.0,pallets,True,2
 
5
  4,Pre-pack_lines,300.0,cartons,False,1
6
  5,Pre-pick_station,54.0,pallets,False,1
7
  6,Long_line,300.0,cartons,True,2
8
+ 7,Short_ine,54.0,pallets,True,2
src/config/optimization_config.py CHANGED
@@ -10,12 +10,15 @@ def get_date_span():
10
  try:
11
  start_date = dashboard.start_date
12
  end_date = dashboard.end_date
13
- date_span = list(range(1, (end_date - start_date).days + 1))
14
  print(f"date from user input")
 
 
 
15
  return date_span, start_date, end_date
16
  except Exception as e:
17
  print(f"using default value for date span")
18
- return list(range(1, 5)), datetime(2025, 3, 24), datetime(2025, 3, 28) # Default 7 days
19
 
20
 
21
  #fetch date from streamlit or default value. The streamlit and default references the demand data (COOIS_Planned_and_Released.csv)
@@ -28,17 +31,17 @@ print("product list",PRODUCT_LIST)
28
 
29
  def get_employee_type_list():
30
  try:
31
- pass
32
- # streamlit_employee_type_list = dashboard.employee_type_list
33
- # return streamlit_employee_type_list
34
- except Exception as e:
35
- print(f"using default value for employee type list")
36
  employee_type_list = extract.read_employee_data()
 
37
  emp_type_list = employee_type_list["employment_type"].unique()
38
  return emp_type_list
39
-
40
  EMPLOYEE_TYPE_LIST = get_employee_type_list()
41
- print("employee type list",EMPLOYEE_TYPE_LIST)
42
 
43
  def get_shift_list():
44
  try:
@@ -46,11 +49,11 @@ def get_shift_list():
46
  return streamlit_shift_list
47
  except Exception as e:
48
  print(f"using default value for shift list")
49
- shift_list = extract.read_shift_cost_data()
50
  shift_list = shift_list["id"].unique()
51
  return shift_list
52
  SHIFT_LIST = get_shift_list()
53
- print(SHIFT_LIST)
54
 
55
 
56
  def get_line_list():
@@ -64,7 +67,7 @@ def get_line_list():
64
  return line_list
65
 
66
  LINE_LIST = get_line_list()
67
- print(LINE_LIST)
68
 
69
  def get_line_cnt_per_type():
70
  try:
@@ -74,11 +77,12 @@ def get_line_cnt_per_type():
74
  print(f"using default value for line cnt per type")
75
  line_df = extract.read_packaging_line_data()
76
  line_cnt_per_type = line_df.set_index("id")["line_count"].to_dict()
 
77
  print(line_cnt_per_type)
78
  return line_cnt_per_type
79
 
80
  LINE_CNT_PER_TYPE = get_line_cnt_per_type()
81
- print(LINE_CNT_PER_TYPE)
82
 
83
  def get_demand_dictionary():
84
  try:
@@ -86,12 +90,13 @@ def get_demand_dictionary():
86
  return streamlit_demand_dictionary
87
  except Exception as e:
88
  print(f"using default value for demand dictionary")
89
- demand_df = extract.read_demand_data()
 
90
  demand_dictionary = demand_df.groupby('Material Number')["Order quantity (GMEIN)"].sum().to_dict()
91
  return demand_dictionary
92
 
93
  DEMAND_DICTIONARY = get_demand_dictionary()
94
- print(DEMAND_DICTIONARY)
95
 
96
  def get_cost_list_per_emp_shift():
97
  try:
@@ -102,10 +107,10 @@ def get_cost_list_per_emp_shift():
102
  shift_cost_df = extract.read_shift_cost_data()
103
  #question - Important : there is multiple type of employment type in terms of the cost
104
  #1 -. unicef 2 - humanizer
105
- return {1:{1:43,2:43,6:64},2:{1:27,2:27,6:41}}
106
 
107
  COST_LIST_PER_EMP_SHIFT = get_cost_list_per_emp_shift()
108
- print(COST_LIST_PER_EMP_SHIFT)
109
 
110
 
111
 
@@ -156,7 +161,7 @@ def get_productivity(PRODUCT_LIST):
156
  return productivity_list_per_emp_product
157
 
158
  PRODUCTIVITY_LIST_PER_EMP_PRODUCT = get_productivity(PRODUCT_LIST)
159
- print(PRODUCTIVITY_LIST_PER_EMP_PRODUCT)
160
 
161
 
162
  # PRODUCTIVITY_LIST_PER_EMP_PRODUCT = { # Kits_Calculation
@@ -184,10 +189,19 @@ MAX_EMPLOYEE_PER_TYPE_ON_DAY = { # Not available information
184
  MAX_HOUR_PER_PERSON_PER_DAY = 14 # legal standard
185
  MAX_HOUR_PER_SHIFT_PER_PERSON = {1: 7, 2: 7, 3: 3} # work_shifts_timing.csv #Need to be fixed
186
  CAP_PER_LINE_PER_HOUR = {
187
- "long": 2200,
188
- "short": 1600,
189
  }
190
  # number of products that can be produced per hour per line
191
  #This information is critical and it should not rely on the productivity information
192
 
193
  DAILY_WEEKLY_SCHEDULE = "daily" # daily or weekly ,this needs to be implementedin in if F_x1_day is not None... F_x1_week is not None... also need to change x1 to Fixedstaff_first_shift
 
 
 
 
 
 
 
 
 
 
10
  try:
11
  start_date = dashboard.start_date
12
  end_date = dashboard.end_date
13
+ date_span = list(range(1, (end_date - start_date).days + 2))
14
  print(f"date from user input")
15
+ print("date span",date_span)
16
+ print("start date",start_date)
17
+ print("end date",end_date)
18
  return date_span, start_date, end_date
19
  except Exception as e:
20
  print(f"using default value for date span")
21
+ return list(range(1, 6)), datetime(2025, 3, 24), datetime(2025, 3, 29) # Default 7 days
22
 
23
 
24
  #fetch date from streamlit or default value. The streamlit and default references the demand data (COOIS_Planned_and_Released.csv)
 
31
 
32
  def get_employee_type_list():
33
  try:
34
+ streamlit_employee_type_list = dashboard.employee_type_list
35
+ return streamlit_employee_type_list
36
+ except (AttributeError, Exception) as e:
37
+ print(f"using default value for employee type list: {e}")
 
38
  employee_type_list = extract.read_employee_data()
39
+ # print("employee type df",employee_type_list)
40
  emp_type_list = employee_type_list["employment_type"].unique()
41
  return emp_type_list
42
+
43
  EMPLOYEE_TYPE_LIST = get_employee_type_list()
44
+ # print("employee type list",EMPLOYEE_TYPE_LIST)
45
 
46
  def get_shift_list():
47
  try:
 
49
  return streamlit_shift_list
50
  except Exception as e:
51
  print(f"using default value for shift list")
52
+ shift_list = extract.get_shift_info()
53
  shift_list = shift_list["id"].unique()
54
  return shift_list
55
  SHIFT_LIST = get_shift_list()
56
+ # print("shift list",SHIFT_LIST)
57
 
58
 
59
  def get_line_list():
 
67
  return line_list
68
 
69
  LINE_LIST = get_line_list()
70
+ # print("line list",LINE_LIST)
71
 
72
  def get_line_cnt_per_type():
73
  try:
 
77
  print(f"using default value for line cnt per type")
78
  line_df = extract.read_packaging_line_data()
79
  line_cnt_per_type = line_df.set_index("id")["line_count"].to_dict()
80
+ print("line cnt per type")
81
  print(line_cnt_per_type)
82
  return line_cnt_per_type
83
 
84
  LINE_CNT_PER_TYPE = get_line_cnt_per_type()
85
+ print("line cnt per type",LINE_CNT_PER_TYPE)
86
 
87
  def get_demand_dictionary():
88
  try:
 
90
  return streamlit_demand_dictionary
91
  except Exception as e:
92
  print(f"using default value for demand dictionary")
93
+ # Use released orders instead of planned orders for demand
94
+ demand_df = extract.read_released_orders_data(start_date=start_date, end_date=end_date)
95
  demand_dictionary = demand_df.groupby('Material Number')["Order quantity (GMEIN)"].sum().to_dict()
96
  return demand_dictionary
97
 
98
  DEMAND_DICTIONARY = get_demand_dictionary()
99
+ # print("demand dictionary",DEMAND_DICTIONARY)
100
 
101
  def get_cost_list_per_emp_shift():
102
  try:
 
107
  shift_cost_df = extract.read_shift_cost_data()
108
  #question - Important : there is multiple type of employment type in terms of the cost
109
  #1 -. unicef 2 - humanizer
110
+ return {"UNICEF Fixed term":{1:43,2:43,3:64},"Humanizer":{1:27,2:27,3:41}}
111
 
112
  COST_LIST_PER_EMP_SHIFT = get_cost_list_per_emp_shift()
113
+ # print("cost list per emp shift",COST_LIST_PER_EMP_SHIFT)
114
 
115
 
116
 
 
161
  return productivity_list_per_emp_product
162
 
163
  PRODUCTIVITY_LIST_PER_EMP_PRODUCT = get_productivity(PRODUCT_LIST)
164
+ # print("productivity list per emp product",PRODUCTIVITY_LIST_PER_EMP_PRODUCT)
165
 
166
 
167
  # PRODUCTIVITY_LIST_PER_EMP_PRODUCT = { # Kits_Calculation
 
189
  MAX_HOUR_PER_PERSON_PER_DAY = 14 # legal standard
190
  MAX_HOUR_PER_SHIFT_PER_PERSON = {1: 7, 2: 7, 3: 3} # work_shifts_timing.csv #Need to be fixed
191
  CAP_PER_LINE_PER_HOUR = {
192
+ 6: 2200, #long
193
+ 7: 1600, #short
194
  }
195
  # number of products that can be produced per hour per line
196
  #This information is critical and it should not rely on the productivity information
197
 
198
  DAILY_WEEKLY_SCHEDULE = "daily" # daily or weekly ,this needs to be implementedin in if F_x1_day is not None... F_x1_week is not None... also need to change x1 to Fixedstaff_first_shift
199
+
200
+ # Fixed staff constraint mode
201
+ # Options:
202
+ # "mandatory" - Forces all fixed staff to work full hours every day (expensive, 99.7% waste)
203
+ # "available" - Staff available up to limits but not forced (balanced approach)
204
+ # "priority" - Fixed staff used first, then temporary staff (realistic business model)
205
+ # "none" - Purely demand-driven scheduling (cost-efficient)
206
+ FIXED_STAFF_CONSTRAINT_MODE = "priority" # Recommended: "priority" for realistic business model
207
+
src/etl/extract.py CHANGED
@@ -28,9 +28,15 @@ def read_employee_data(
28
  ) -> pd.DataFrame:
29
  return pd.read_csv(path)
30
 
 
 
 
 
 
 
31
 
32
  def read_shift_cost_data(
33
- path="data/real_data_excel/converted_csv/work_shift.csv",
34
  ) -> pd.DataFrame:
35
  return pd.read_csv(path)
36
 
@@ -95,5 +101,7 @@ def read_released_orders_data(
95
 
96
 
97
  if __name__ == "__main__":
98
- demand_data = read_demand_data()
99
- print(demand_data.head())
 
 
 
28
  ) -> pd.DataFrame:
29
  return pd.read_csv(path)
30
 
31
+ def get_shift_info(
32
+ path = "data/real_data_excel/converted_csv/work_shift.csv"
33
+ ) -> pd.DataFrame:
34
+ df = pd.read_csv(path)
35
+ return df
36
+
37
 
38
  def read_shift_cost_data(
39
+ path="data/real_data_excel/converted_csv/WH_Workforce_Hourly_Pay_Scale_processed.csv",
40
  ) -> pd.DataFrame:
41
  return pd.read_csv(path)
42
 
 
101
 
102
 
103
  if __name__ == "__main__":
104
+ employee_data = read_employee_data()
105
+ print("employee data")
106
+ print(employee_data)
107
+
src/etl/models.py DELETED
@@ -1 +0,0 @@
1
-
 
 
src/models/optimizer_real.py CHANGED
@@ -48,6 +48,13 @@ class OptimizerReal:
48
  # -----------------------------
49
  # Weekly demand (units) for each product in product_list
50
  weekly_demand = self.config.DEMAND_DICTIONARY
 
 
 
 
 
 
 
51
 
52
  # Daily activity toggle for each product (1=can be produced on day t; 0=cannot)
53
  # If a product is not active on a day, we force its production and hours to 0 that day.
@@ -76,17 +83,33 @@ class OptimizerReal:
76
  Cap = self.config.CAP_PER_LINE_PER_HOUR
77
 
78
  # Fixed regular hours for type Fixed on shift 1
79
- # Choose either PER-DAY values or a single PER-WEEK total.
80
- # Common in practice: per-day fixed hours (regulars show up daily).
81
- # F_x1_day = Fixed working hour for fixed staff on shift 1
82
  first_shift_hour = Hmax_shift[1]
83
  daily_weekly_type = self.config.DAILY_WEEKLY_SCHEDULE
84
- print(first_shift_hour)
85
- F_x1_day = {
86
- t: first_shift_hour * N_day["Fixed"][t] + 1 for t in days
87
- } # EDIT if different from "all regulars do full usual shift"
88
- print(F_x1_day)
89
- F_x1_week = None # e.g., sum(F_x1_day.values()) if you want weekly instead (then set F_x1_day=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  cap_per_line_per_hour = self.config.CAP_PER_LINE_PER_HOUR
91
  # Optional skill/compatibility: allow[(e,p,ell)] = 1/0 (1=allowed; 0=forbid)
92
  allow = {}
@@ -107,6 +130,7 @@ class OptimizerReal:
107
  # 4) DECISION VARIABLES
108
  # -----------------------------
109
  # h[e,s,p,ell,t] = worker-hours of type e on shift s for product p on line ell on day t (integer)
 
110
  h = {}
111
  for e in employee_types:
112
  for s in shift_list:
@@ -114,6 +138,7 @@ class OptimizerReal:
114
  for ell in line_type_cnt_tuple:
115
  for t in days:
116
  # Upper bound per (e,s,t): shift cap * available headcount that day
 
117
  ub = Hmax_shift[s] * N_day[e][t]
118
  h[e, s, p, ell, t] = solver.IntVar(
119
  0, ub, f"h_{e}_{s}_{p}_{ell[0]}{ell[1]}_d{t}"
@@ -148,6 +173,8 @@ class OptimizerReal:
148
  # -----------------------------
149
  # 5) OBJECTIVE: Minimize total labor cost over the week
150
  # -----------------------------
 
 
151
  solver.Minimize(
152
  solver.Sum(
153
  wage_types[e][s] * h[e, s, p, ell, t]
@@ -165,10 +192,12 @@ class OptimizerReal:
165
 
166
  # 6.1 Weekly demand (no daily demand)
167
  for p in product_list:
168
- solver.Add(
169
- solver.Sum(u[p, ell, s, t] for ell in line_type_cnt_tuple for s in shift_list for t in days)
170
- >= weekly_demand.get(p, 0)
171
- )
 
 
172
 
173
  # 6.2 If a product is inactive on a day, force zero production and hours for that day
174
  # This makes "varying products per day" explicit.
@@ -195,6 +224,9 @@ class OptimizerReal:
195
  )
196
 
197
  # 6.4 Per-line throughput cap (units/hour × line-hours)
 
 
 
198
  for ell in line_type_cnt_tuple:
199
  for s in shift_list:
200
  for t in days:
@@ -223,24 +255,55 @@ class OptimizerReal:
223
 
224
  # 6.6 Fixed regular hours for type Fixed on shift 1
225
  if F_x1_day is not None:
226
- # Per-day fixed hours
227
  for t in days:
228
  solver.Add(
229
- solver.Sum(h["Fixed", 1, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
230
  == F_x1_day[t]
231
  )
232
- elif F_x1_week is not None:
233
- # Per-week fixed hours
234
- solver.Add(
235
- solver.Sum(
236
- h["Fixed", 1, p, ell, t] for p in product_list for ell in line_type_cnt_tuple for t in days
237
- )
238
- == F_x1_week
239
- )
 
 
240
  else:
241
- raise ValueError(
242
- "Specify either F_x1_day (dict by day) or F_x1_week (scalar)."
243
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
  # 6.7 Daily hours cap per employee type (14h per person per day)
246
  for e in employee_types:
 
48
  # -----------------------------
49
  # Weekly demand (units) for each product in product_list
50
  weekly_demand = self.config.DEMAND_DICTIONARY
51
+
52
+ # Validate demand - check if any products have positive demand
53
+ total_demand = sum(weekly_demand.get(p, 0) for p in product_list)
54
+ if total_demand == 0:
55
+ print("Warning: Total demand is zero for all products. Optimization may not be meaningful.")
56
+ print("Products:", product_list)
57
+ print("Demands:", {p: weekly_demand.get(p, 0) for p in product_list})
58
 
59
  # Daily activity toggle for each product (1=can be produced on day t; 0=cannot)
60
  # If a product is not active on a day, we force its production and hours to 0 that day.
 
83
  Cap = self.config.CAP_PER_LINE_PER_HOUR
84
 
85
  # Fixed regular hours for type Fixed on shift 1
86
+ # BUSINESS LOGIC: Fixed staff availability vs mandatory hours
87
+ # Controlled by FIXED_STAFF_CONSTRAINT_MODE in optimization_config
88
+
89
  first_shift_hour = Hmax_shift[1]
90
  daily_weekly_type = self.config.DAILY_WEEKLY_SCHEDULE
91
+ constraint_mode = self.config.FIXED_STAFF_CONSTRAINT_MODE
92
+
93
+ if constraint_mode == "mandatory":
94
+ # Option 1: Mandatory hours (forces staff to work even when idle)
95
+ F_x1_day = {t: first_shift_hour * N_day["UNICEF Fixed term"][t] for t in days}
96
+ print(f"Using MANDATORY fixed hours constraint: {sum(F_x1_day.values())} hours/week")
97
+ elif constraint_mode == "available":
98
+ # Option 2: Available hours constraint (staff can work up to limit but not forced)
99
+ F_x1_day = None
100
+ print("Using AVAILABLE hours constraint (not yet implemented)")
101
+ elif constraint_mode == "priority":
102
+ # Option 3: Priority-based (realistic business model)
103
+ F_x1_day = None
104
+ print("Using PRIORITY constraint - fixed staff first, then temporary staff")
105
+ elif constraint_mode == "none":
106
+ # Option 4: No constraint (fully demand-driven)
107
+ F_x1_day = None
108
+ print("Using NO fixed hours constraint - demand-driven scheduling")
109
+ else:
110
+ raise ValueError(f"Invalid FIXED_STAFF_CONSTRAINT_MODE: {constraint_mode}. Use 'mandatory', 'available', 'priority', or 'none'")
111
+
112
+ # e.g., F_x1_day = sum(F_x1_day.values()) if you want weekly instead (then set F_x1_day=None)
113
  cap_per_line_per_hour = self.config.CAP_PER_LINE_PER_HOUR
114
  # Optional skill/compatibility: allow[(e,p,ell)] = 1/0 (1=allowed; 0=forbid)
115
  allow = {}
 
130
  # 4) DECISION VARIABLES
131
  # -----------------------------
132
  # h[e,s,p,ell,t] = worker-hours of type e on shift s for product p on line ell on day t (integer)
133
+
134
  h = {}
135
  for e in employee_types:
136
  for s in shift_list:
 
138
  for ell in line_type_cnt_tuple:
139
  for t in days:
140
  # Upper bound per (e,s,t): shift cap * available headcount that day
141
+
142
  ub = Hmax_shift[s] * N_day[e][t]
143
  h[e, s, p, ell, t] = solver.IntVar(
144
  0, ub, f"h_{e}_{s}_{p}_{ell[0]}{ell[1]}_d{t}"
 
173
  # -----------------------------
174
  # 5) OBJECTIVE: Minimize total labor cost over the week
175
  # -----------------------------
176
+ print("wage_types",wage_types)
177
+ print("h",h)
178
  solver.Minimize(
179
  solver.Sum(
180
  wage_types[e][s] * h[e, s, p, ell, t]
 
192
 
193
  # 6.1 Weekly demand (no daily demand)
194
  for p in product_list:
195
+ demand_value = weekly_demand.get(p, 0)
196
+ if demand_value > 0: # Only add constraint if there's actual demand
197
+ solver.Add(
198
+ solver.Sum(u[p, ell, s, t] for ell in line_type_cnt_tuple for s in shift_list for t in days)
199
+ >= demand_value
200
+ )
201
 
202
  # 6.2 If a product is inactive on a day, force zero production and hours for that day
203
  # This makes "varying products per day" explicit.
 
224
  )
225
 
226
  # 6.4 Per-line throughput cap (units/hour × line-hours)
227
+ print("================================================")
228
+ print("cap_per_line_per_hour",cap_per_line_per_hour)
229
+ print("tline",tline)
230
  for ell in line_type_cnt_tuple:
231
  for s in shift_list:
232
  for t in days:
 
255
 
256
  # 6.6 Fixed regular hours for type Fixed on shift 1
257
  if F_x1_day is not None:
258
+ # Per-day fixed hours (mandatory - expensive)
259
  for t in days:
260
  solver.Add(
261
+ solver.Sum(h["UNICEF Fixed term", 1, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
262
  == F_x1_day[t]
263
  )
264
+ print("Applied mandatory fixed hours constraint")
265
+ # elif F_x1_week is not None:
266
+ # # Per-week fixed hours (mandatory - expensive)
267
+ # solver.Add(
268
+ # solver.Sum(
269
+ # h["UNICEF Fixed term", 1, p, ell, t] for p in product_list for ell in line_type_cnt_tuple for t in days
270
+ # )
271
+ # == F_x1_week
272
+ # )
273
+ # print("Applied mandatory weekly fixed hours constraint")
274
  else:
275
+ # No fixed constraint - purely demand-driven (cost-efficient)
276
+ print("No mandatory fixed hours constraint - using demand-driven scheduling")
277
+ # The availability constraint (6.7) already limits maximum hours
278
+
279
+ # Special handling for priority mode
280
+ if constraint_mode == "priority":
281
+ print("Implementing priority constraints: UNICEF Fixed term used before Humanizer")
282
+ # Add constraints to prioritize fixed staff usage before temporary staff
283
+
284
+ # Priority constraint: For each day, product, and line,
285
+ # Humanizer hours can only be used if UNICEF Fixed term is at capacity
286
+ for t in days:
287
+ for p in product_list:
288
+ for ell in line_type_cnt_tuple:
289
+ # Create binary variable to indicate if UNICEF Fixed term is at capacity
290
+ unicef_at_capacity = solver.IntVar(0, 1, f"unicef_at_capacity_{p}_{ell[0]}{ell[1]}_d{t}")
291
+
292
+ # UNICEF Fixed term capacity for this specific (p, ell, t)
293
+ max_unicef_hours = Hmax_shift[1] * N_day["UNICEF Fixed term"][t]
294
+
295
+ # If UNICEF is not at capacity (unicef_at_capacity = 0), then Humanizer hours must be 0
296
+ # If UNICEF is at capacity (unicef_at_capacity = 1), then Humanizer can work
297
+ solver.Add(
298
+ solver.Sum(h["Humanizer", s, p, ell, t] for s in shift_list)
299
+ <= unicef_at_capacity * max_unicef_hours # Large M value
300
+ )
301
+
302
+ # Force unicef_at_capacity = 1 only when UNICEF hours are significant
303
+ solver.Add(
304
+ solver.Sum(h["UNICEF Fixed term", s, p, ell, t] for s in shift_list)
305
+ >= unicef_at_capacity * 0.1 # Small threshold to trigger capacity flag
306
+ )
307
 
308
  # 6.7 Daily hours cap per employee type (14h per person per day)
309
  for e in employee_types: