HaLim commited on
Commit
f8a0929
·
1 Parent(s): 29608b7

Update the variable name into more intuitive names.

Browse files
Files changed (1) hide show
  1. src/models/optimizer_real.py +113 -113
src/models/optimizer_real.py CHANGED
@@ -1,5 +1,5 @@
1
  # Option A (with lines) + 7-day horizon (weekly demand only)
2
- # Generalized: arbitrary products (P_all) and day-varying headcount N_day[e][t]
3
  # -----------------------------------------------------------------------------
4
  # pip install ortools
5
  from ortools.linear_solver import pywraplp
@@ -24,43 +24,43 @@ class OptimizerReal:
24
  # 1) SETS
25
  # -----------------------------
26
  # Days
27
- D = self.config.DATE_SPAN
28
 
29
  # Products (master set; you can have many)
30
  # Fill with all SKUs that may appear over the week
31
- P_all = self.config.PRODUCT_LIST # EDIT: add/remove products freely
32
 
33
  # Employee types (fixed to two types Fixed,Humanizer; headcount varies by day)
34
- E = self.config.EMPLOYEE_TYPE_LIST
35
 
36
  # Shifts: 1=usual, 2=overtime, 3=evening
37
- S = self.config.SHIFT_LIST
38
 
39
  # Line types and explicit line list
40
- T_line = self.config.LINE_LIST
41
- K = self.config.LINE_LIST_PER_TYPE # number of physical lines per type (EDIT)
42
- L = [
43
- (t, i) for t in T_line for i in range(1, K[t] + 1)
44
  ] # pair of line type and line number (e.g., ('long', 1))
45
 
46
  # -----------------------------
47
  # 2) PARAMETERS (EDIT THESE)
48
  # -----------------------------
49
- # Weekly demand (units) for each product in P_all
50
- d_week = self.config.DEMAND_LIST
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.
54
  active = {
55
- t: {p: 1 for p in P_all} for t in D
56
  } # EDIT per day if some SKUs are not available
57
 
58
  # Per-hour labor cost by employee type & shift
59
- c = self.config.COST_LIST_PER_EMP_SHIFT
60
 
61
- # Productivity q[e][s][p] = units per hour (assumed line-independent here)
62
- # Provide entries for ALL products in P_all
63
- q = self.config.PRODUCTIVITY_LIST_PER_EMP_PRODUCT
64
  # If productivity depends on line, switch to q_line[(e,s,p,ell)] and use it in constraints.
65
 
66
  # Day-varying available headcount per type
@@ -71,7 +71,7 @@ class OptimizerReal:
71
  Hmax_daily_per_person = (
72
  self.config.MAX_HOUR_PER_PERSON_PER_DAY
73
  ) # per person per day
74
- Hmax_s = self.config.MAX_HOUR_PER_SHIFT_PER_PERSON # per-shift hour caps
75
  # Per-line unit/hour capacity (physical)
76
  Cap = self.config.CAP_PER_LINE_PER_HOUR
77
 
@@ -79,20 +79,20 @@ class OptimizerReal:
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_s[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 D
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 = {}
93
- for e in E:
94
- for p in P_all:
95
- for ell in L:
96
  allow[(e, p, ell)] = 1 # EDIT as needed
97
 
98
  # -----------------------------
@@ -108,41 +108,41 @@ class OptimizerReal:
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 E:
112
- for s in S:
113
- for p in P_all:
114
- for ell in L:
115
- for t in D:
116
  # Upper bound per (e,s,t): shift cap * available headcount that day
117
- ub = Hmax_s[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}"
120
  )
121
 
122
  # u[p,ell,s,t] = units of product p produced on line ell during shift s on day t
123
  u = {}
124
- for p in P_all:
125
- for ell in L:
126
- for s in S:
127
- for t in D:
128
  u[p, ell, s, t] = solver.NumVar(
129
  0, INF, f"u_{p}_{ell[0]}{ell[1]}_{s}_d{t}"
130
  )
131
 
132
  # tline[ell,s,t] = operating hours of line ell during shift s on day t
133
  tline = {}
134
- for ell in L:
135
- for s in S:
136
- for t in D:
137
  tline[ell, s, t] = solver.NumVar(
138
- 0, Hmax_s[s], f"t_{ell[0]}{ell[1]}_{s}_d{t}"
139
  )
140
 
141
  # ybin[e,s,t] = shift usage binaries per type/day (to gate OT after usual)
142
  ybin = {}
143
- for e in E:
144
- for s in S:
145
- for t in D:
146
  ybin[e, s, t] = solver.BoolVar(f"y_{e}_{s}_d{t}")
147
 
148
  # -----------------------------
@@ -150,12 +150,12 @@ class OptimizerReal:
150
  # -----------------------------
151
  solver.Minimize(
152
  solver.Sum(
153
- c[e][s] * h[e, s, p, ell, t]
154
- for e in E
155
- for s in S
156
- for p in P_all
157
- for ell in L
158
- for t in D
159
  )
160
  )
161
 
@@ -164,76 +164,76 @@ class OptimizerReal:
164
  # -----------------------------
165
 
166
  # 6.1 Weekly demand (no daily demand)
167
- for p in P_all:
168
  solver.Add(
169
- solver.Sum(u[p, ell, s, t] for ell in L for s in S for t in D)
170
- >= d_week.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.
175
- BIG_H = max(Hmax_s.values()) * sum(N_day[e][t] for e in E for t in D)
176
- for p in P_all:
177
- for t in D:
178
  if active[t][p] == 0:
179
- for ell in L:
180
- for s in S:
181
  solver.Add(u[p, ell, s, t] == 0)
182
- for e in E:
183
  solver.Add(h[e, s, p, ell, t] == 0)
184
 
185
  # 6.3 Labor -> units (per line/shift/day)
186
- # If productivity depends on line, swap q[e][s][p] with q_line[(e,s,p,ell)] here.
187
- for p in P_all:
188
- for ell in L:
189
- for s in S:
190
- for t in D:
191
  # Gate by activity (if inactive, both sides are already 0 from 6.2)
192
  solver.Add(
193
  u[p, ell, s, t]
194
- <= solver.Sum(q[e][s][p] * h[e, s, p, ell, t] for e in E)
195
  )
196
 
197
  # 6.4 Per-line throughput cap (units/hour × line-hours)
198
- for ell in L:
199
- for s in S:
200
- for t in D:
201
  line_type = ell[0] # 'long' or 'short'
202
  solver.Add(
203
- solver.Sum(u[p, ell, s, t] for p in P_all)
204
  <= cap_per_line_per_hour[line_type] * tline[ell, s, t]
205
  )
206
 
207
  # 6.5 Couple line hours & worker-hours (single-operator lines → tight equality)
208
- for ell in L:
209
- for s in S:
210
- for t in D:
211
  solver.Add(
212
  tline[ell, s, t]
213
- == solver.Sum(h[e, s, p, ell, t] for e in E for p in P_all)
214
  )
215
  # If multi-operator lines (up to Wmax[ell] concurrent workers), replace above with:
216
  # Wmax = {ell: 2, ...}
217
- # for ell in L:
218
- # for s in S:
219
- # for t in D:
220
  # solver.Add(
221
- # solver.Sum(h[e, s, p, ell, t] for e in E for p in P_all) <= Wmax[ell] * tline[ell, s, t]
222
  # )
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 D:
228
  solver.Add(
229
- solver.Sum(h["Fixed", 1, p, ell, t] for p in P_all for ell in L)
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 P_all for ell in L for t in D
237
  )
238
  == F_x1_week
239
  )
@@ -243,46 +243,46 @@ class OptimizerReal:
243
  )
244
 
245
  # 6.7 Daily hours cap per employee type (14h per person per day)
246
- for e in E:
247
- for t in D:
248
  solver.Add(
249
  solver.Sum(
250
- h[e, s, p, ell, t] for s in S for p in P_all for ell in L
251
  )
252
  <= Hmax_daily_per_person * N_day[e][t]
253
  )
254
 
255
  # 6.8 Link hours to shift-usage binaries (per type/day)
256
- # Use a type/day-specific Big-M: M_e_s_t = Hmax_s[s] * N_day[e][t]
257
- for e in E:
258
- for s in S:
259
- for t in D:
260
- M_e_s_t = Hmax_s[s] * N_day[e][t]
261
  solver.Add(
262
- solver.Sum(h[e, s, p, ell, t] for p in P_all for ell in L)
263
  <= M_e_s_t * ybin[e, s, t]
264
  )
265
 
266
  # 6.9 Overtime only after usual (per day). Also bound OT hours <= usual hours
267
- for e in E:
268
- for t in D:
269
  solver.Add(ybin[e, 2, t] <= ybin[e, 1, t])
270
  solver.Add(
271
- solver.Sum(h[e, 2, p, ell, t] for p in P_all for ell in L)
272
- <= solver.Sum(h[e, 1, p, ell, t] for p in P_all for ell in L)
273
  )
274
  # (Optional) evening only after usual:
275
- # for e in E:
276
- # for t in D:
277
  # solver.Add(ybin[e, 3, t] <= ybin[e, 1, t])
278
 
279
  # 6.10 Skill/compatibility mask
280
- for e in E:
281
- for p in P_all:
282
- for ell in L:
283
  if allow[(e, p, ell)] == 0:
284
- for s in S:
285
- for t in D:
286
  solver.Add(h[e, s, p, ell, t] == 0)
287
 
288
  # -----------------------------
@@ -299,46 +299,46 @@ class OptimizerReal:
299
  print("Objective (min cost):", solver.Objective().Value())
300
 
301
  print("\n--- Weekly production by product ---")
302
- for p in P_all:
303
  produced = sum(
304
- u[p, ell, s, t].solution_value() for ell in L for s in S for t in D
305
  )
306
- print(f"{p}: {produced:.1f} (weekly demand {d_week.get(p,0)})")
307
 
308
  print("\n--- Line operating hours by shift/day ---")
309
- for ell in L:
310
- for s in S:
311
- hours = [tline[ell, s, t].solution_value() for t in D]
312
  if sum(hours) > 1e-6:
313
  print(
314
  f"Line {ell} Shift {s}: "
315
- + ", ".join([f"D{t}={hours[t-1]:.2f}h" for t in D])
316
  )
317
 
318
  print("\n--- Hours by employee type / shift / day ---")
319
- for e in E:
320
- for s in S:
321
  day_hours = [
322
- sum(h[e, s, p, ell, t].solution_value() for p in P_all for ell in L)
323
- for t in D
324
  ]
325
  if sum(day_hours) > 1e-6:
326
  print(
327
  f"e={e}, s={s}: "
328
- + ", ".join([f"D{t}={day_hours[t-1]:.2f}h" for t in D])
329
  )
330
 
331
  print("\n--- Implied headcount by type / shift / day ---")
332
- for e in E:
333
  print(e)
334
- for s in S:
335
  row = []
336
- for t in D:
337
  hours = sum(
338
- h[e, s, p, ell, t].solution_value() for p in P_all for ell in L
339
  )
340
- need = int((hours + Hmax_s[s] - 1) // Hmax_s[s]) # ceil
341
- row.append(f"D{t}={need}")
342
 
343
  if any("=0" not in Fixed for Fixed in row):
344
  print(f"e={e}, s={s}: " + ", ".join(row))
 
1
  # Option A (with lines) + 7-day horizon (weekly demand only)
2
+ # Generalized: arbitrary products (product_list) and day-varying headcount N_day[e][t]
3
  # -----------------------------------------------------------------------------
4
  # pip install ortools
5
  from ortools.linear_solver import pywraplp
 
24
  # 1) SETS
25
  # -----------------------------
26
  # Days
27
+ days = self.config.DATE_SPAN
28
 
29
  # Products (master set; you can have many)
30
  # Fill with all SKUs that may appear over the week
31
+ product_list = self.config.PRODUCT_LIST # EDIT: add/remove products freely
32
 
33
  # Employee types (fixed to two types Fixed,Humanizer; headcount varies by day)
34
+ employee_types = self.config.EMPLOYEE_TYPE_LIST
35
 
36
  # Shifts: 1=usual, 2=overtime, 3=evening
37
+ shift_list = self.config.SHIFT_LIST
38
 
39
  # Line types and explicit line list
40
+ line_list = self.config.LINE_LIST
41
+ line_cnt_per_type = self.config.LINE_LIST_PER_TYPE # number of physical lines per type (EDIT)
42
+ line_type_cnt_tuple = [
43
+ (t, i) for t in line_list for i in range(1, line_cnt_per_type[t] + 1)
44
  ] # pair of line type and line number (e.g., ('long', 1))
45
 
46
  # -----------------------------
47
  # 2) PARAMETERS (EDIT THESE)
48
  # -----------------------------
49
+ # Weekly demand (units) for each product in product_list
50
+ weekly_demand = self.config.DEMAND_LIST
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.
54
  active = {
55
+ t: {p: 1 for p in product_list} for t in days
56
  } # EDIT per day if some SKUs are not available
57
 
58
  # Per-hour labor cost by employee type & shift
59
+ wage_types = self.config.COST_LIST_PER_EMP_SHIFT
60
 
61
+ # Productivity productivities[e][s][p] = units per hour (assumed line-independent here)
62
+ # Provide entries for ALL products in product_list
63
+ productivities = self.config.PRODUCTIVITY_LIST_PER_EMP_PRODUCT
64
  # If productivity depends on line, switch to q_line[(e,s,p,ell)] and use it in constraints.
65
 
66
  # Day-varying available headcount per type
 
71
  Hmax_daily_per_person = (
72
  self.config.MAX_HOUR_PER_PERSON_PER_DAY
73
  ) # per person per day
74
+ Hmax_shift = self.config.MAX_HOUR_PER_SHIFT_PER_PERSON # per-shift hour caps
75
  # Per-line unit/hour capacity (physical)
76
  Cap = self.config.CAP_PER_LINE_PER_HOUR
77
 
 
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 = {}
93
+ for e in employee_types:
94
+ for p in product_list:
95
+ for ell in line_type_cnt_tuple:
96
  allow[(e, p, ell)] = 1 # EDIT as needed
97
 
98
  # -----------------------------
 
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:
113
+ for p in product_list:
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}"
120
  )
121
 
122
  # u[p,ell,s,t] = units of product p produced on line ell during shift s on day t
123
  u = {}
124
+ for p in product_list:
125
+ for ell in line_type_cnt_tuple:
126
+ for s in shift_list:
127
+ for t in days:
128
  u[p, ell, s, t] = solver.NumVar(
129
  0, INF, f"u_{p}_{ell[0]}{ell[1]}_{s}_d{t}"
130
  )
131
 
132
  # tline[ell,s,t] = operating hours of line ell during shift s on day t
133
  tline = {}
134
+ for ell in line_type_cnt_tuple:
135
+ for s in shift_list:
136
+ for t in days:
137
  tline[ell, s, t] = solver.NumVar(
138
+ 0, Hmax_shift[s], f"t_{ell[0]}{ell[1]}_{s}_d{t}"
139
  )
140
 
141
  # ybin[e,s,t] = shift usage binaries per type/day (to gate OT after usual)
142
  ybin = {}
143
+ for e in employee_types:
144
+ for s in shift_list:
145
+ for t in days:
146
  ybin[e, s, t] = solver.BoolVar(f"y_{e}_{s}_d{t}")
147
 
148
  # -----------------------------
 
150
  # -----------------------------
151
  solver.Minimize(
152
  solver.Sum(
153
+ wage_types[e][s] * h[e, s, p, ell, t]
154
+ for e in employee_types
155
+ for s in shift_list
156
+ for p in product_list
157
+ for ell in line_type_cnt_tuple
158
+ for t in days
159
  )
160
  )
161
 
 
164
  # -----------------------------
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.
175
+ BIG_H = max(Hmax_shift.values()) * sum(N_day[e][t] for e in employee_types for t in days)
176
+ for p in product_list:
177
+ for t in days:
178
  if active[t][p] == 0:
179
+ for ell in line_type_cnt_tuple:
180
+ for s in shift_list:
181
  solver.Add(u[p, ell, s, t] == 0)
182
+ for e in employee_types:
183
  solver.Add(h[e, s, p, ell, t] == 0)
184
 
185
  # 6.3 Labor -> units (per line/shift/day)
186
+ # If productivity depends on line, swap productivities[e][s][p] with q_line[(e,s,p,ell)] here.
187
+ for p in product_list:
188
+ for ell in line_type_cnt_tuple:
189
+ for s in shift_list:
190
+ for t in days:
191
  # Gate by activity (if inactive, both sides are already 0 from 6.2)
192
  solver.Add(
193
  u[p, ell, s, t]
194
+ <= solver.Sum(productivities[e][s][p] * h[e, s, p, ell, t] for e in employee_types)
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:
201
  line_type = ell[0] # 'long' or 'short'
202
  solver.Add(
203
+ solver.Sum(u[p, ell, s, t] for p in product_list)
204
  <= cap_per_line_per_hour[line_type] * tline[ell, s, t]
205
  )
206
 
207
  # 6.5 Couple line hours & worker-hours (single-operator lines → tight equality)
208
+ for ell in line_type_cnt_tuple:
209
+ for s in shift_list:
210
+ for t in days:
211
  solver.Add(
212
  tline[ell, s, t]
213
+ == solver.Sum(h[e, s, p, ell, t] for e in employee_types for p in product_list)
214
  )
215
  # If multi-operator lines (up to Wmax[ell] concurrent workers), replace above with:
216
  # Wmax = {ell: 2, ...}
217
+ # for ell in line_type_cnt_tuple:
218
+ # for s in shift_list:
219
+ # for t in days:
220
  # solver.Add(
221
+ # solver.Sum(h[e, s, p, ell, t] for e in employee_types for p in product_list) <= Wmax[ell] * tline[ell, s, t]
222
  # )
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
  )
 
243
  )
244
 
245
  # 6.7 Daily hours cap per employee type (14h per person per day)
246
+ for e in employee_types:
247
+ for t in days:
248
  solver.Add(
249
  solver.Sum(
250
+ h[e, s, p, ell, t] for s in shift_list for p in product_list for ell in line_type_cnt_tuple
251
  )
252
  <= Hmax_daily_per_person * N_day[e][t]
253
  )
254
 
255
  # 6.8 Link hours to shift-usage binaries (per type/day)
256
+ # Use a type/day-specific Big-M: M_e_s_t = Hmax_shift[s] * N_day[e][t]
257
+ for e in employee_types:
258
+ for s in shift_list:
259
+ for t in days:
260
+ M_e_s_t = Hmax_shift[s] * N_day[e][t]
261
  solver.Add(
262
+ solver.Sum(h[e, s, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
263
  <= M_e_s_t * ybin[e, s, t]
264
  )
265
 
266
  # 6.9 Overtime only after usual (per day). Also bound OT hours <= usual hours
267
+ for e in employee_types:
268
+ for t in days:
269
  solver.Add(ybin[e, 2, t] <= ybin[e, 1, t])
270
  solver.Add(
271
+ solver.Sum(h[e, 2, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
272
+ <= solver.Sum(h[e, 1, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
273
  )
274
  # (Optional) evening only after usual:
275
+ # for e in employee_types:
276
+ # for t in days:
277
  # solver.Add(ybin[e, 3, t] <= ybin[e, 1, t])
278
 
279
  # 6.10 Skill/compatibility mask
280
+ for e in employee_types:
281
+ for p in product_list:
282
+ for ell in line_type_cnt_tuple:
283
  if allow[(e, p, ell)] == 0:
284
+ for s in shift_list:
285
+ for t in days:
286
  solver.Add(h[e, s, p, ell, t] == 0)
287
 
288
  # -----------------------------
 
299
  print("Objective (min cost):", solver.Objective().Value())
300
 
301
  print("\n--- Weekly production by product ---")
302
+ for p in product_list:
303
  produced = sum(
304
+ u[p, ell, s, t].solution_value() for ell in line_type_cnt_tuple for s in shift_list for t in days
305
  )
306
+ print(f"{p}: {produced:.1f} (weekly demand {weekly_demand.get(p,0)})")
307
 
308
  print("\n--- Line operating hours by shift/day ---")
309
+ for ell in line_type_cnt_tuple:
310
+ for s in shift_list:
311
+ hours = [tline[ell, s, t].solution_value() for t in days]
312
  if sum(hours) > 1e-6:
313
  print(
314
  f"Line {ell} Shift {s}: "
315
+ + ", ".join([f"days{t}={hours[t-1]:.2f}h" for t in days])
316
  )
317
 
318
  print("\n--- Hours by employee type / shift / day ---")
319
+ for e in employee_types:
320
+ for s in shift_list:
321
  day_hours = [
322
+ sum(h[e, s, p, ell, t].solution_value() for p in product_list for ell in line_type_cnt_tuple)
323
+ for t in days
324
  ]
325
  if sum(day_hours) > 1e-6:
326
  print(
327
  f"e={e}, s={s}: "
328
+ + ", ".join([f"days{t}={day_hours[t-1]:.2f}h" for t in days])
329
  )
330
 
331
  print("\n--- Implied headcount by type / shift / day ---")
332
+ for e in employee_types:
333
  print(e)
334
+ for s in shift_list:
335
  row = []
336
+ for t in days:
337
  hours = sum(
338
+ h[e, s, p, ell, t].solution_value() for p in product_list for ell in line_type_cnt_tuple
339
  )
340
+ need = int((hours + Hmax_shift[s] - 1) // Hmax_shift[s]) # ceil
341
+ row.append(f"days{t}={need}")
342
 
343
  if any("=0" not in Fixed for Fixed in row):
344
  print(f"e={e}, s={s}: " + ", ".join(row))