HaLim
Update the variable name into more intuitive names.
f8a0929
raw
history blame
14.7 kB
# Option A (with lines) + 7-day horizon (weekly demand only)
# Generalized: arbitrary products (product_list) and day-varying headcount N_day[e][t]
# -----------------------------------------------------------------------------
# pip install ortools
from ortools.linear_solver import pywraplp
import pandas as pd
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
from src.config import optimization_config
import importlib
importlib.reload(optimization_config)
class OptimizerReal:
def __init__(self):
self.config = optimization_config
def solve_option_A_multi_day_generalized(self):
# -----------------------------
# 1) SETS
# -----------------------------
# Days
days = self.config.DATE_SPAN
# Products (master set; you can have many)
# Fill with all SKUs that may appear over the week
product_list = self.config.PRODUCT_LIST # EDIT: add/remove products freely
# Employee types (fixed to two types Fixed,Humanizer; headcount varies by day)
employee_types = self.config.EMPLOYEE_TYPE_LIST
# Shifts: 1=usual, 2=overtime, 3=evening
shift_list = self.config.SHIFT_LIST
# Line types and explicit line list
line_list = self.config.LINE_LIST
line_cnt_per_type = self.config.LINE_LIST_PER_TYPE # number of physical lines per type (EDIT)
line_type_cnt_tuple = [
(t, i) for t in line_list for i in range(1, line_cnt_per_type[t] + 1)
] # pair of line type and line number (e.g., ('long', 1))
# -----------------------------
# 2) PARAMETERS (EDIT THESE)
# -----------------------------
# Weekly demand (units) for each product in product_list
weekly_demand = self.config.DEMAND_LIST
# Daily activity toggle for each product (1=can be produced on day t; 0=cannot)
# If a product is not active on a day, we force its production and hours to 0 that day.
active = {
t: {p: 1 for p in product_list} for t in days
} # EDIT per day if some SKUs are not available
# Per-hour labor cost by employee type & shift
wage_types = self.config.COST_LIST_PER_EMP_SHIFT
# Productivity productivities[e][s][p] = units per hour (assumed line-independent here)
# Provide entries for ALL products in product_list
productivities = self.config.PRODUCTIVITY_LIST_PER_EMP_PRODUCT
# If productivity depends on line, switch to q_line[(e,s,p,ell)] and use it in constraints.
# Day-varying available headcount per type
# N_day[e][t] = number of employees of type e available on day t
N_day = self.config.MAX_EMPLOYEE_PER_TYPE_ON_DAY
# Limits
Hmax_daily_per_person = (
self.config.MAX_HOUR_PER_PERSON_PER_DAY
) # per person per day
Hmax_shift = self.config.MAX_HOUR_PER_SHIFT_PER_PERSON # per-shift hour caps
# Per-line unit/hour capacity (physical)
Cap = self.config.CAP_PER_LINE_PER_HOUR
# Fixed regular hours for type Fixed on shift 1
# Choose either PER-DAY values or a single PER-WEEK total.
# Common in practice: per-day fixed hours (regulars show up daily).
# F_x1_day = Fixed working hour for fixed staff on shift 1
first_shift_hour = Hmax_shift[1]
daily_weekly_type = self.config.DAILY_WEEKLY_SCHEDULE
print(first_shift_hour)
F_x1_day = {
t: first_shift_hour * N_day["Fixed"][t] + 1 for t in days
} # EDIT if different from "all regulars do full usual shift"
print(F_x1_day)
F_x1_week = None # e.g., sum(F_x1_day.values()) if you want weekly instead (then set F_x1_day=None)
cap_per_line_per_hour = self.config.CAP_PER_LINE_PER_HOUR
# Optional skill/compatibility: allow[(e,p,ell)] = 1/0 (1=allowed; 0=forbid)
allow = {}
for e in employee_types:
for p in product_list:
for ell in line_type_cnt_tuple:
allow[(e, p, ell)] = 1 # EDIT as needed
# -----------------------------
# 3) SOLVER
# -----------------------------
solver = pywraplp.Solver.CreateSolver("CBC") # or 'SCIP' if available
if not solver:
raise RuntimeError("Failed to create solver. Check OR-Tools installation.")
INF = solver.infinity()
# -----------------------------
# 4) DECISION VARIABLES
# -----------------------------
# h[e,s,p,ell,t] = worker-hours of type e on shift s for product p on line ell on day t (integer)
h = {}
for e in employee_types:
for s in shift_list:
for p in product_list:
for ell in line_type_cnt_tuple:
for t in days:
# Upper bound per (e,s,t): shift cap * available headcount that day
ub = Hmax_shift[s] * N_day[e][t]
h[e, s, p, ell, t] = solver.IntVar(
0, ub, f"h_{e}_{s}_{p}_{ell[0]}{ell[1]}_d{t}"
)
# u[p,ell,s,t] = units of product p produced on line ell during shift s on day t
u = {}
for p in product_list:
for ell in line_type_cnt_tuple:
for s in shift_list:
for t in days:
u[p, ell, s, t] = solver.NumVar(
0, INF, f"u_{p}_{ell[0]}{ell[1]}_{s}_d{t}"
)
# tline[ell,s,t] = operating hours of line ell during shift s on day t
tline = {}
for ell in line_type_cnt_tuple:
for s in shift_list:
for t in days:
tline[ell, s, t] = solver.NumVar(
0, Hmax_shift[s], f"t_{ell[0]}{ell[1]}_{s}_d{t}"
)
# ybin[e,s,t] = shift usage binaries per type/day (to gate OT after usual)
ybin = {}
for e in employee_types:
for s in shift_list:
for t in days:
ybin[e, s, t] = solver.BoolVar(f"y_{e}_{s}_d{t}")
# -----------------------------
# 5) OBJECTIVE: Minimize total labor cost over the week
# -----------------------------
solver.Minimize(
solver.Sum(
wage_types[e][s] * h[e, s, p, ell, t]
for e in employee_types
for s in shift_list
for p in product_list
for ell in line_type_cnt_tuple
for t in days
)
)
# -----------------------------
# 6) CONSTRAINTS
# -----------------------------
# 6.1 Weekly demand (no daily demand)
for p in product_list:
solver.Add(
solver.Sum(u[p, ell, s, t] for ell in line_type_cnt_tuple for s in shift_list for t in days)
>= weekly_demand.get(p, 0)
)
# 6.2 If a product is inactive on a day, force zero production and hours for that day
# This makes "varying products per day" explicit.
BIG_H = max(Hmax_shift.values()) * sum(N_day[e][t] for e in employee_types for t in days)
for p in product_list:
for t in days:
if active[t][p] == 0:
for ell in line_type_cnt_tuple:
for s in shift_list:
solver.Add(u[p, ell, s, t] == 0)
for e in employee_types:
solver.Add(h[e, s, p, ell, t] == 0)
# 6.3 Labor -> units (per line/shift/day)
# If productivity depends on line, swap productivities[e][s][p] with q_line[(e,s,p,ell)] here.
for p in product_list:
for ell in line_type_cnt_tuple:
for s in shift_list:
for t in days:
# Gate by activity (if inactive, both sides are already 0 from 6.2)
solver.Add(
u[p, ell, s, t]
<= solver.Sum(productivities[e][s][p] * h[e, s, p, ell, t] for e in employee_types)
)
# 6.4 Per-line throughput cap (units/hour × line-hours)
for ell in line_type_cnt_tuple:
for s in shift_list:
for t in days:
line_type = ell[0] # 'long' or 'short'
solver.Add(
solver.Sum(u[p, ell, s, t] for p in product_list)
<= cap_per_line_per_hour[line_type] * tline[ell, s, t]
)
# 6.5 Couple line hours & worker-hours (single-operator lines → tight equality)
for ell in line_type_cnt_tuple:
for s in shift_list:
for t in days:
solver.Add(
tline[ell, s, t]
== solver.Sum(h[e, s, p, ell, t] for e in employee_types for p in product_list)
)
# If multi-operator lines (up to Wmax[ell] concurrent workers), replace above with:
# Wmax = {ell: 2, ...}
# for ell in line_type_cnt_tuple:
# for s in shift_list:
# for t in days:
# solver.Add(
# solver.Sum(h[e, s, p, ell, t] for e in employee_types for p in product_list) <= Wmax[ell] * tline[ell, s, t]
# )
# 6.6 Fixed regular hours for type Fixed on shift 1
if F_x1_day is not None:
# Per-day fixed hours
for t in days:
solver.Add(
solver.Sum(h["Fixed", 1, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
== F_x1_day[t]
)
elif F_x1_week is not None:
# Per-week fixed hours
solver.Add(
solver.Sum(
h["Fixed", 1, p, ell, t] for p in product_list for ell in line_type_cnt_tuple for t in days
)
== F_x1_week
)
else:
raise ValueError(
"Specify either F_x1_day (dict by day) or F_x1_week (scalar)."
)
# 6.7 Daily hours cap per employee type (14h per person per day)
for e in employee_types:
for t in days:
solver.Add(
solver.Sum(
h[e, s, p, ell, t] for s in shift_list for p in product_list for ell in line_type_cnt_tuple
)
<= Hmax_daily_per_person * N_day[e][t]
)
# 6.8 Link hours to shift-usage binaries (per type/day)
# Use a type/day-specific Big-M: M_e_s_t = Hmax_shift[s] * N_day[e][t]
for e in employee_types:
for s in shift_list:
for t in days:
M_e_s_t = Hmax_shift[s] * N_day[e][t]
solver.Add(
solver.Sum(h[e, s, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
<= M_e_s_t * ybin[e, s, t]
)
# 6.9 Overtime only after usual (per day). Also bound OT hours <= usual hours
for e in employee_types:
for t in days:
solver.Add(ybin[e, 2, t] <= ybin[e, 1, t])
solver.Add(
solver.Sum(h[e, 2, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
<= solver.Sum(h[e, 1, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
)
# (Optional) evening only after usual:
# for e in employee_types:
# for t in days:
# solver.Add(ybin[e, 3, t] <= ybin[e, 1, t])
# 6.10 Skill/compatibility mask
for e in employee_types:
for p in product_list:
for ell in line_type_cnt_tuple:
if allow[(e, p, ell)] == 0:
for s in shift_list:
for t in days:
solver.Add(h[e, s, p, ell, t] == 0)
# -----------------------------
# 7) SOLVE
# -----------------------------
status = solver.Solve()
if status != pywraplp.Solver.OPTIMAL:
print("No optimal solution. Status:", status)
return
# -----------------------------
# 8) REPORT
# -----------------------------
print("Objective (min cost):", solver.Objective().Value())
print("\n--- Weekly production by product ---")
for p in product_list:
produced = sum(
u[p, ell, s, t].solution_value() for ell in line_type_cnt_tuple for s in shift_list for t in days
)
print(f"{p}: {produced:.1f} (weekly demand {weekly_demand.get(p,0)})")
print("\n--- Line operating hours by shift/day ---")
for ell in line_type_cnt_tuple:
for s in shift_list:
hours = [tline[ell, s, t].solution_value() for t in days]
if sum(hours) > 1e-6:
print(
f"Line {ell} Shift {s}: "
+ ", ".join([f"days{t}={hours[t-1]:.2f}h" for t in days])
)
print("\n--- Hours by employee type / shift / day ---")
for e in employee_types:
for s in shift_list:
day_hours = [
sum(h[e, s, p, ell, t].solution_value() for p in product_list for ell in line_type_cnt_tuple)
for t in days
]
if sum(day_hours) > 1e-6:
print(
f"e={e}, s={s}: "
+ ", ".join([f"days{t}={day_hours[t-1]:.2f}h" for t in days])
)
print("\n--- Implied headcount by type / shift / day ---")
for e in employee_types:
print(e)
for s in shift_list:
row = []
for t in days:
hours = sum(
h[e, s, p, ell, t].solution_value() for p in product_list for ell in line_type_cnt_tuple
)
need = int((hours + Hmax_shift[s] - 1) // Hmax_shift[s]) # ceil
row.append(f"days{t}={need}")
if any("=0" not in Fixed for Fixed in row):
print(f"e={e}, s={s}: " + ", ".join(row))
if __name__ == "__main__":
optimizer = OptimizerReal()
optimizer.solve_option_A_multi_day_generalized()