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 +
|
| 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,
|
| 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 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 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.
|
| 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 |
-
|
|
|
|
| 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 {
|
| 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 |
-
|
| 188 |
-
|
| 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/
|
| 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 |
-
|
| 99 |
-
print(
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 80 |
-
#
|
| 81 |
-
|
| 82 |
first_shift_hour = Hmax_shift[1]
|
| 83 |
daily_weekly_type = self.config.DAILY_WEEKLY_SCHEDULE
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 169 |
-
|
| 170 |
-
|
| 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 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
| 240 |
else:
|
| 241 |
-
|
| 242 |
-
|
| 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:
|