File size: 14,730 Bytes
cd87ae5
f8a0929
cd87ae5
 
 
 
 
 
 
 
 
 
179d6f0
 
 
cd87ae5
 
 
 
 
 
 
 
 
 
 
f8a0929
cd87ae5
 
 
f8a0929
cd87ae5
179d6f0
f8a0929
cd87ae5
 
f8a0929
cd87ae5
 
f8a0929
 
 
 
cd87ae5
 
 
 
 
f8a0929
 
cd87ae5
 
 
 
f8a0929
cd87ae5
 
 
f8a0929
cd87ae5
f8a0929
 
 
cd87ae5
 
 
 
 
 
 
 
 
 
f8a0929
cd87ae5
 
 
179d6f0
cd87ae5
 
179d6f0
f8a0929
179d6f0
 
cd87ae5
f8a0929
cd87ae5
179d6f0
cd87ae5
179d6f0
cd87ae5
 
f8a0929
 
 
cd87ae5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8a0929
 
 
 
 
cd87ae5
f8a0929
cd87ae5
 
 
 
 
 
f8a0929
 
 
 
cd87ae5
 
 
 
 
 
f8a0929
 
 
cd87ae5
f8a0929
cd87ae5
 
 
 
f8a0929
 
 
cd87ae5
 
 
 
 
 
 
f8a0929
 
 
 
 
 
cd87ae5
 
 
 
 
 
 
 
f8a0929
cd87ae5
f8a0929
 
cd87ae5
 
 
 
f8a0929
 
 
cd87ae5
f8a0929
 
cd87ae5
f8a0929
cd87ae5
 
 
f8a0929
 
 
 
 
cd87ae5
 
 
f8a0929
cd87ae5
 
 
f8a0929
 
 
179d6f0
cd87ae5
f8a0929
179d6f0
cd87ae5
 
 
f8a0929
 
 
cd87ae5
 
f8a0929
cd87ae5
 
 
f8a0929
 
 
cd87ae5
f8a0929
cd87ae5
 
179d6f0
cd87ae5
 
f8a0929
cd87ae5
f8a0929
cd87ae5
 
 
 
 
179d6f0
f8a0929
179d6f0
cd87ae5
 
 
 
 
 
 
 
f8a0929
 
cd87ae5
 
f8a0929
cd87ae5
 
 
 
 
f8a0929
 
 
 
 
cd87ae5
f8a0929
cd87ae5
 
 
 
f8a0929
 
cd87ae5
 
f8a0929
 
cd87ae5
 
f8a0929
 
cd87ae5
 
 
f8a0929
 
 
cd87ae5
f8a0929
 
cd87ae5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8a0929
cd87ae5
f8a0929
cd87ae5
f8a0929
cd87ae5
 
f8a0929
 
 
cd87ae5
 
 
f8a0929
cd87ae5
 
 
f8a0929
 
cd87ae5
f8a0929
 
cd87ae5
 
 
 
f8a0929
cd87ae5
 
 
f8a0929
179d6f0
f8a0929
cd87ae5
f8a0929
cd87ae5
f8a0929
cd87ae5
f8a0929
 
179d6f0
 
cd87ae5
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# 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()