# 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()