Last active 1 month ago

Revision 72d84025b9177d2b1ae16ca73568a573fe38a0b5

RunoutTime.py Raw
1import tkinter as tk
2from tkinter import ttk
3
4UPDATE_MS = 1000 # 1 second
5
6
7class Station:
8 def __init__(self, name, max_qty, rate_per_min, refill_amount):
9 self.name = name
10 self.max_qty = max_qty
11 self.current_qty = max_qty
12 self.rate_per_min = rate_per_min
13 self.refill_amount = refill_amount
14
15 def reset(self):
16 self.current_qty = self.max_qty
17
18 def add(self):
19 self.current_qty = min(
20 self.max_qty,
21 self.current_qty + self.refill_amount
22 )
23
24 @property
25 def minutes_remaining(self):
26 if self.rate_per_min <= 0:
27 return 9999
28 return self.current_qty / self.rate_per_min
29
30
31class RunoutSimulator:
32
33 def __init__(self, root):
34 self.root = root
35 self.root.title("Runout Simulator")
36
37 self.running = False
38 self.speed_factor = 1.0
39 self.base_line_speed = 60 # devices/min
40
41 self.stations = [
42 Station("Tray Infeed", 135, 11, 27),
43 Station("Tray Outfeed", 135, 11, 27),
44 Station("IFU Loader", 2000, 60, 100),
45 Station("Carton Magazine", 1000, 60, 20),
46 Station("Case Magazine", 100, 3, 10),
47 ]
48
49 self.pallet_capacity = 60
50 self.pallet_cases = 0
51 self.case_rate = 3
52
53 self.station_widgets = []
54
55 self.build_ui()
56 self.update_display()
57
58 def build_ui(self):
59
60 controls = tk.Frame(self.root)
61 controls.pack(fill="x", padx=10, pady=5)
62
63 tk.Button(
64 controls,
65 text="Start",
66 command=self.start
67 ).pack(side="left")
68
69 tk.Button(
70 controls,
71 text="Pause",
72 command=self.pause
73 ).pack(side="left", padx=5)
74
75 tk.Button(
76 controls,
77 text="Reset",
78 command=self.reset
79 ).pack(side="left")
80
81 tk.Button(
82 controls,
83 text="Speed +10%",
84 command=self.speed_up
85 ).pack(side="left", padx=20)
86
87 tk.Button(
88 controls,
89 text="Speed -10%",
90 command=self.speed_down
91 ).pack(side="left")
92
93 self.speed_label = tk.Label(
94 controls,
95 text="Line Speed: 100%"
96 )
97 self.speed_label.pack(side="left", padx=20)
98
99 for station in self.stations:
100
101 frame = tk.LabelFrame(
102 self.root,
103 text=station.name,
104 padx=5,
105 pady=5
106 )
107 frame.pack(
108 fill="x",
109 padx=10,
110 pady=5
111 )
112
113 bar = ttk.Progressbar(
114 frame,
115 maximum=station.max_qty,
116 )
117 bar.pack(fill="x", expand=True)
118
119 qty_label = tk.Label(frame)
120 qty_label.pack(anchor="w")
121
122 time_label = tk.Label(frame)
123 time_label.pack(anchor="w")
124
125 refill_btn = tk.Button(
126 frame,
127 text=f"+{station.refill_amount}",
128 command=station.add
129 )
130 refill_btn.pack(anchor="e")
131
132 self.station_widgets.append(
133 (bar, qty_label, time_label)
134 )
135
136 pallet_frame = tk.LabelFrame(
137 self.root,
138 text="Finished Pallet",
139 padx=5,
140 pady=5
141 )
142 pallet_frame.pack(fill="x", padx=10, pady=5)
143
144 self.pallet_bar = ttk.Progressbar(
145 pallet_frame,
146 maximum=self.pallet_capacity,
147 )
148 self.pallet_bar.pack(fill="x", expand=True)
149
150 self.pallet_label = tk.Label(pallet_frame)
151 self.pallet_label.pack(anchor="w")
152
153 self.pallet_button = tk.Button(
154 pallet_frame,
155 text="Replace Full Pallet",
156 command=self.replace_pallet
157 )
158 self.pallet_button.pack(anchor="e")
159
160 def start(self):
161 if not self.running:
162 self.running = True
163 self.tick()
164
165 def pause(self):
166 self.running = False
167
168 def reset(self):
169 self.running = False
170
171 for station in self.stations:
172 station.reset()
173
174 self.pallet_cases = 0
175
176 self.speed_factor = 1.0
177 self.update_display()
178
179 def speed_up(self):
180 self.speed_factor *= 1.10
181 self.update_display()
182
183 def speed_down(self):
184 self.speed_factor *= 0.90
185 self.update_display()
186
187 def replace_pallet(self):
188 if self.pallet_cases >= self.pallet_capacity:
189 self.pallet_cases = 0
190
191 def tick(self):
192
193 if not self.running:
194 return
195
196 for station in self.stations:
197
198 usage_per_second = (
199 station.rate_per_min
200 * self.speed_factor
201 / 60
202 )
203
204 station.current_qty = max(
205 0,
206 station.current_qty - usage_per_second
207 )
208
209 self.pallet_cases += (self.case_rate * self.speed_factor / 60)
210
211 if self.pallet_cases > self.pallet_capacity:
212 self.pallet_cases = self.pallet_capacity
213
214 self.update_display()
215
216 self.root.after(
217 UPDATE_MS,
218 self.tick
219 )
220
221
222 def update_display(self):
223
224 for station, widgets in zip(
225 self.stations,
226 self.station_widgets):
227
228 bar, qty_label, time_label = widgets
229
230 bar["value"] = station.current_qty
231
232 qty_label.config(
233 text=f"Qty: {station.current_qty:.1f}/{station.max_qty}"
234 )
235
236 time_label.config(
237 text=f"Remaining: {station.minutes_remaining:.1f} min"
238 )
239
240 current_speed = self.base_line_speed * self.speed_factor
241
242 self.speed_label.config(
243 text=f"Line Speed: {current_speed:.0f} devices/min "
244 f"({self.speed_factor * 100:.0f}%)"
245 )
246
247 self.pallet_bar["value"] = self.pallet_cases
248
249 self.pallet_label.config(
250 text=f"{self.pallet_cases:.1f} / "
251 f"{self.pallet_capacity} cases"
252 )
253
254 if self.pallet_cases >= self.pallet_capacity:
255 self.pallet_button.config(state="normal")
256 else:
257 self.pallet_button.config(state="disabled")
258
259
260root = tk.Tk()
261app = RunoutSimulator(root)
262root.mainloop()
RunoutTime2.py Raw
1import tkinter as tk
2from tkinter import ttk
3
4UPDATE_MS = 1000 # 1 second
5
6TRAY_SIZE = 27
7MAX_INFEED_TRAYS = 50
8MAX_OUTFEED_TRAYS = 50
9
10START_CARTONS = 1000
11START_IFUS = 2000
12START_CASES = 100
13
14PALLET_CAPACITY = 60
15
16
17class RunoutSimulator:
18
19 def __init__(self, root):
20 self.root = root
21 self.root.title("Runout Simulator")
22
23 self.running = False
24
25 self.base_line_speed = 60 # devices/min
26 self.speed_factor = 1.0
27 self.device_credit = 0.0
28
29 self.mode = tk.StringVar(value="4-Pack")
30
31 self.reset_values()
32
33 self.build_ui()
34 self.update_display()
35
36 def reset_values(self):
37 self.infeed_trays = [TRAY_SIZE] * MAX_INFEED_TRAYS
38 self.outfeed_trays = 0
39
40 self.cartons = START_CARTONS
41 self.ifus = START_IFUS
42 self.cases = START_CASES
43
44 self.current_carton_devices = 0
45 self.current_case_cartons = 0
46 self.pallet_cases = 0
47
48 self.device_credit = 0.0
49 self.status = "Ready"
50
51 def devices_per_carton(self):
52 if self.mode.get() == "4-Pack":
53 return 4
54 return 1
55
56 def cartons_per_case(self):
57 if self.mode.get() == "4-Pack":
58 return 18
59 return 40
60
61 def build_ui(self):
62
63 controls = tk.Frame(self.root)
64 controls.pack(fill="x", padx=10, pady=5)
65
66 tk.Button(
67 controls,
68 text="Start",
69 command=self.start
70 ).pack(side="left")
71
72 tk.Button(
73 controls,
74 text="Pause",
75 command=self.pause
76 ).pack(side="left", padx=5)
77
78 tk.Button(
79 controls,
80 text="Reset",
81 command=self.reset
82 ).pack(side="left")
83
84 tk.Button(
85 controls,
86 text="Speed +10%",
87 command=self.speed_up
88 ).pack(side="left", padx=20)
89
90 tk.Button(
91 controls,
92 text="Speed -10%",
93 command=self.speed_down
94 ).pack(side="left")
95
96 self.speed_label = tk.Label(controls)
97 self.speed_label.pack(side="left", padx=20)
98
99 mode_frame = tk.Frame(self.root)
100 mode_frame.pack(fill="x", padx=10, pady=5)
101
102 tk.Label(
103 mode_frame,
104 text="Mode:"
105 ).pack(side="left")
106
107 tk.Radiobutton(
108 mode_frame,
109 text="4-Pack",
110 variable=self.mode,
111 value="4-Pack",
112 command=self.change_mode
113 ).pack(side="left")
114
115 tk.Radiobutton(
116 mode_frame,
117 text="1-Pack",
118 variable=self.mode,
119 value="1-Pack",
120 command=self.change_mode
121 ).pack(side="left")
122
123 # Infeed
124 self.infeed_frame = tk.LabelFrame(
125 self.root,
126 text="Device Infeed",
127 padx=5,
128 pady=5
129 )
130 self.infeed_frame.pack(fill="x", padx=10, pady=5)
131
132 self.infeed_bar = ttk.Progressbar(
133 self.infeed_frame,
134 maximum=MAX_INFEED_TRAYS * TRAY_SIZE
135 )
136 self.infeed_bar.pack(fill="x", expand=True)
137
138 self.infeed_label = tk.Label(self.infeed_frame)
139 self.infeed_label.pack(anchor="w")
140
141 tk.Button(
142 self.infeed_frame,
143 text="+5 Trays",
144 command=self.add_infeed_trays
145 ).pack(anchor="e")
146
147 # Outfeed
148 self.outfeed_frame = tk.LabelFrame(
149 self.root,
150 text="Tray Outfeed",
151 padx=5,
152 pady=5
153 )
154 self.outfeed_frame.pack(fill="x", padx=10, pady=5)
155
156 self.outfeed_bar = ttk.Progressbar(
157 self.outfeed_frame,
158 maximum=MAX_OUTFEED_TRAYS
159 )
160 self.outfeed_bar.pack(fill="x", expand=True)
161
162 self.outfeed_label = tk.Label(self.outfeed_frame)
163 self.outfeed_label.pack(anchor="w")
164
165 tk.Button(
166 self.outfeed_frame,
167 text="Clear Outfeed",
168 command=self.clear_outfeed
169 ).pack(anchor="e")
170
171 # Cartons
172 self.carton_frame = tk.LabelFrame(
173 self.root,
174 text="Carton Magazine",
175 padx=5,
176 pady=5
177 )
178 self.carton_frame.pack(fill="x", padx=10, pady=5)
179
180 self.carton_bar = ttk.Progressbar(
181 self.carton_frame,
182 maximum=START_CARTONS
183 )
184 self.carton_bar.pack(fill="x", expand=True)
185
186 self.carton_label = tk.Label(self.carton_frame)
187 self.carton_label.pack(anchor="w")
188
189 tk.Button(
190 self.carton_frame,
191 text="+20 Cartons",
192 command=self.add_cartons
193 ).pack(anchor="e")
194
195 # IFUs
196 self.ifu_frame = tk.LabelFrame(
197 self.root,
198 text="IFU Loader",
199 padx=5,
200 pady=5
201 )
202 self.ifu_frame.pack(fill="x", padx=10, pady=5)
203
204 self.ifu_bar = ttk.Progressbar(
205 self.ifu_frame,
206 maximum=START_IFUS
207 )
208 self.ifu_bar.pack(fill="x", expand=True)
209
210 self.ifu_label = tk.Label(self.ifu_frame)
211 self.ifu_label.pack(anchor="w")
212
213 tk.Button(
214 self.ifu_frame,
215 text="+100 IFUs",
216 command=self.add_ifus
217 ).pack(anchor="e")
218
219 # Case Supply
220 self.case_supply_frame = tk.LabelFrame(
221 self.root,
222 text="Case Magazine",
223 padx=5,
224 pady=5
225 )
226 self.case_supply_frame.pack(fill="x", padx=10, pady=5)
227
228 self.case_supply_bar = ttk.Progressbar(
229 self.case_supply_frame,
230 maximum=START_CASES
231 )
232 self.case_supply_bar.pack(fill="x", expand = True)
233
234 self.case_supply_label = tk.Label(self.case_supply_frame)
235 self.case_supply_label.pack(anchor="w")
236
237 tk.Button(
238 self.case_supply_frame,
239 text=f"10 Cases",
240 command=self.add_cases
241 ).pack(anchor="e")
242
243 # Current carton
244 self.current_carton_frame = tk.LabelFrame(
245 self.root,
246 text="Current Carton",
247 padx=5,
248 pady=5
249 )
250 self.current_carton_frame.pack(fill="x", padx=10, pady=5)
251
252 self.current_carton_bar = ttk.Progressbar(
253 self.current_carton_frame
254 )
255 self.current_carton_bar.pack(fill="x", expand=True)
256
257 self.current_carton_label = tk.Label(self.current_carton_frame)
258 self.current_carton_label.pack(anchor="w")
259
260 # Current case
261 self.current_case_frame = tk.LabelFrame(
262 self.root,
263 text="Current Case",
264 padx=5,
265 pady=5
266 )
267 self.current_case_frame.pack(fill="x", padx=10, pady=5)
268
269 self.current_case_bar = ttk.Progressbar(
270 self.current_case_frame
271 )
272 self.current_case_bar.pack(fill="x", expand=True)
273
274 self.current_case_label = tk.Label(self.current_case_frame)
275 self.current_case_label.pack(anchor="w")
276
277 # Pallet
278 self.pallet_frame = tk.LabelFrame(
279 self.root,
280 text="Finished Pallet",
281 padx=5,
282 pady=5
283 )
284 self.pallet_frame.pack(fill="x", padx=10, pady=5)
285
286 self.pallet_bar = ttk.Progressbar(
287 self.pallet_frame,
288 maximum=PALLET_CAPACITY
289 )
290 self.pallet_bar.pack(fill="x", expand=True)
291
292 self.pallet_label = tk.Label(self.pallet_frame)
293 self.pallet_label.pack(anchor="w")
294
295 self.pallet_button = tk.Button(
296 self.pallet_frame,
297 text="Replace Full Pallet",
298 command=self.replace_pallet
299 )
300 self.pallet_button.pack(anchor="e")
301
302 # Status
303 self.status_label = tk.Label(
304 self.root,
305 font=("Arial", 11, "bold")
306 )
307 self.status_label.pack(anchor="w", padx=10, pady=10)
308
309 def start(self):
310 if not self.running:
311 self.running = True
312 self.status = "Running"
313 self.tick()
314
315 def pause(self):
316 self.running = False
317 self.status = "Paused"
318 self.update_display()
319
320 def reset(self):
321 self.running = False
322 self.speed_factor = 1.0
323 self.reset_values()
324 self.update_display()
325
326 def change_mode(self):
327 self.running = False
328 self.speed_factor = 1.0
329 self.reset_values()
330 self.update_display()
331
332 def speed_up(self):
333 self.speed_factor *= 1.10
334 self.update_display()
335
336 def speed_down(self):
337 self.speed_factor *= 0.90
338 self.update_display()
339
340 def add_infeed_trays(self):
341 space_available = MAX_INFEED_TRAYS - len(self.infeed_trays)
342 trays_to_add = min(5, space_available)
343
344 for _ in range(trays_to_add):
345 self.infeed_trays.append(TRAY_SIZE)
346
347 self.update_display()
348
349 def clear_outfeed(self):
350 self.outfeed_trays = 0
351 self.update_display()
352
353 def add_cartons(self):
354 self.cartons = min(START_CARTONS, self.cartons + 20)
355 self.update_display()
356
357 def add_ifus(self):
358 self.ifus = min(START_IFUS, self.ifus + 100)
359 self.update_display()
360
361 def add_cases(self):
362 self.cases = min(START_CASES, self.cases + 10)
363 self.update_display()
364
365 def replace_pallet(self):
366 if self.pallet_cases >= PALLET_CAPACITY:
367 self.pallet_cases = 0
368 self.status = "Ready"
369 self.update_display()
370
371 def tick(self):
372
373 if not self.running:
374 return
375
376 current_speed = self.base_line_speed * self.speed_factor
377 self.device_credit += current_speed / 60
378
379 while self.device_credit >= 1 and self.running:
380 success = self.process_one_device()
381
382 if success:
383 self.device_credit -= 1
384 else:
385 self.running = False
386 self.device_credit = 0
387
388 self.update_display()
389
390 if self.running:
391 self.root.after(UPDATE_MS, self.tick)
392
393 def process_one_device(self):
394
395 if len(self.infeed_trays) == 0:
396 self.status = "Stopped - No infeed trays"
397 return False
398
399 if self.pallet_cases >= PALLET_CAPACITY:
400 self.status = "Stopped - Pallet full"
401 return False
402
403 active_tray_devices = self.infeed_trays[0]
404
405 if active_tray_devices == 1 and self.outfeed_trays >= MAX_OUTFEED_TRAYS:
406 self.status = "Stopped - Tray outfeed full"
407 return False
408
409 will_complete_carton = (
410 self.current_carton_devices + 1 >= self.devices_per_carton()
411 )
412
413 if will_complete_carton:
414 if self.cartons <= 0:
415 self.status = "Stopped - No cartons"
416 return False
417
418 if self.ifus <= 0:
419 self.status = "Stopped - No IFUs"
420 return False
421
422 # Remove one device from the current tray
423 self.infeed_trays[0] -= 1
424
425 # Add device to current carton
426 self.current_carton_devices += 1
427
428 # If tray is empty, move it to outfeed
429 if self.infeed_trays[0] == 0:
430 self.infeed_trays.pop(0)
431 self.outfeed_trays += 1
432
433 # If carton is complete, consume 1 carton and 1 IFU
434 if self.current_carton_devices >= self.devices_per_carton():
435 self.current_carton_devices = 0
436 self.cartons -= 1
437 self.ifus -= 1
438
439 self.current_case_cartons += 1
440
441 # If case is complete, move 1 case to pallet
442 if self.current_case_cartons >= self.cartons_per_case():
443
444 if self.cases <= 0:
445 self.status = "Stopped - No cases"
446 return False
447
448 self.current_case_cartons = 0
449 self.cases -= 1
450 self.pallet_cases += 1
451
452 if self.pallet_cases >= PALLET_CAPACITY:
453 self.pallet_cases = PALLET_CAPACITY
454 self.status = "Stopped - Pallet full"
455 return False
456
457 self.status = "Running"
458 return True
459
460 def update_display(self):
461
462 current_speed = self.base_line_speed * self.speed_factor
463
464 self.speed_label.config(
465 text=f"Line Speed: {current_speed:.0f} devices/min "
466 f"({self.speed_factor * 100:.0f}%)"
467 )
468
469 total_infeed_devices = sum(self.infeed_trays)
470 self.infeed_bar["value"] = total_infeed_devices
471
472 if len(self.infeed_trays) > 0:
473 first_tray_text = f"Current tray: {self.infeed_trays[0]} / {TRAY_SIZE} devices"
474 else:
475 first_tray_text = "Current tray: none"
476
477 self.infeed_label.config(
478 text=f"{len(self.infeed_trays)} trays, "
479 f"{total_infeed_devices} devices remaining\n"
480 f"{first_tray_text}"
481 )
482
483 self.outfeed_bar["value"] = self.outfeed_trays
484 self.outfeed_label.config(
485 text=f"{self.outfeed_trays} / {MAX_OUTFEED_TRAYS} empty trays"
486 )
487
488 self.carton_bar["value"] = self.cartons
489 self.carton_label.config(
490 text=f"{self.cartons} / {START_CARTONS} cartons"
491 )
492
493 self.ifu_bar["value"] = self.ifus
494 self.ifu_label.config(
495 text=f"{self.ifus} / {START_IFUS} IFUs"
496 )
497
498 self.case_supply_bar["value"] = self.cases
499 self.case_supply_label.config(
500 text=f"{self.cases} / {START_CASES} cases available"
501 )
502
503 self.current_carton_bar["maximum"] = self.devices_per_carton()
504 self.current_carton_bar["value"] = self.current_carton_devices
505 self.current_carton_label.config(
506 text=f"{self.current_carton_devices} / "
507 f"{self.devices_per_carton()} devices"
508 )
509
510 self.current_case_bar["maximum"] = self.cartons_per_case()
511 self.current_case_bar["value"] = self.current_case_cartons
512 self.current_case_label.config(
513 text=f"{self.current_case_cartons} / "
514 f"{self.cartons_per_case()} cartons"
515 )
516
517 self.pallet_bar["value"] = self.pallet_cases
518 self.pallet_label.config(
519 text=f"{self.pallet_cases} / {PALLET_CAPACITY} cases"
520 )
521
522 if self.pallet_cases >= PALLET_CAPACITY:
523 self.pallet_button.config(state="normal")
524 else:
525 self.pallet_button.config(state="disabled")
526
527 self.status_label.config(
528 text=f"Status: {self.status}"
529 )
530
531
532root = tk.Tk()
533app = RunoutSimulator(root)
534root.mainloop()
RunoutTime2.zip Raw
This file can't be rendered. View the full file.