HaLim
commited on
Commit
·
de19c07
1
Parent(s):
131af7c
add configuration class
Browse files- config_page.py +56 -53
- src/config/optimization_config.py +16 -33
- src/models/optimizer_real.py +22 -19
config_page.py
CHANGED
|
@@ -7,6 +7,8 @@ import streamlit as st
|
|
| 7 |
import datetime
|
| 8 |
import sys
|
| 9 |
import os
|
|
|
|
|
|
|
| 10 |
|
| 11 |
# Add src directory to path for imports
|
| 12 |
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
@@ -78,7 +80,8 @@ def initialize_session_state():
|
|
| 78 |
MAX_HOUR_PER_PERSON_PER_DAY, MAX_HOUR_PER_SHIFT_PER_PERSON,
|
| 79 |
MAX_PARALLEL_WORKERS, COST_LIST_PER_EMP_SHIFT,
|
| 80 |
PAYMENT_MODE_CONFIG, LINE_CNT_PER_TYPE,
|
| 81 |
-
MAX_EMPLOYEE_PER_TYPE_ON_DAY, start_date, end_date
|
|
|
|
| 82 |
)
|
| 83 |
|
| 84 |
# Get the actual computed default values from optimization_config.py
|
|
@@ -96,35 +99,35 @@ def initialize_session_state():
|
|
| 96 |
'fixed_staff_mode': FIXED_STAFF_CONSTRAINT_MODE,
|
| 97 |
|
| 98 |
# Payment configuration - from optimization_config.py
|
| 99 |
-
'payment_mode_shift_1': PAYMENT_MODE_CONFIG.get(
|
| 100 |
-
'payment_mode_shift_2': PAYMENT_MODE_CONFIG.get(
|
| 101 |
-
'payment_mode_shift_3': PAYMENT_MODE_CONFIG.get(
|
| 102 |
|
| 103 |
# Working hours - from optimization_config.py
|
| 104 |
'max_hour_per_person_per_day': MAX_HOUR_PER_PERSON_PER_DAY,
|
| 105 |
-
'max_hours_shift_1': MAX_HOUR_PER_SHIFT_PER_PERSON.get(
|
| 106 |
-
'max_hours_shift_2': MAX_HOUR_PER_SHIFT_PER_PERSON.get(
|
| 107 |
-
'max_hours_shift_3': MAX_HOUR_PER_SHIFT_PER_PERSON.get(
|
| 108 |
|
| 109 |
# Operations - from optimization_config.py
|
| 110 |
-
'max_parallel_workers_long_line': MAX_PARALLEL_WORKERS.get(
|
| 111 |
-
'max_parallel_workers_mini_load': MAX_PARALLEL_WORKERS.get(
|
| 112 |
|
| 113 |
# Workforce limits - from optimization_config.py (computed values)
|
| 114 |
'max_unicef_per_day': list(MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("UNICEF Fixed term", {}).values())[0] if MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("UNICEF Fixed term") else 8,
|
| 115 |
'max_humanizer_per_day': list(MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("Humanizer", {}).values())[0] if MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("Humanizer") else 10,
|
| 116 |
|
| 117 |
# Line counts - from optimization_config.py (data-driven)
|
| 118 |
-
'line_count_long_line': LINE_CNT_PER_TYPE.get(
|
| 119 |
-
'line_count_mini_load': LINE_CNT_PER_TYPE.get(
|
| 120 |
|
| 121 |
# Cost rates - from optimization_config.py (computed or default values)
|
| 122 |
-
'unicef_rate_shift_1': COST_LIST_PER_EMP_SHIFT.get("UNICEF Fixed term", {}).get(
|
| 123 |
-
'unicef_rate_shift_2': COST_LIST_PER_EMP_SHIFT.get("UNICEF Fixed term", {}).get(
|
| 124 |
-
'unicef_rate_shift_3': COST_LIST_PER_EMP_SHIFT.get("UNICEF Fixed term", {}).get(
|
| 125 |
-
'humanizer_rate_shift_1': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(
|
| 126 |
-
'humanizer_rate_shift_2': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(
|
| 127 |
-
'humanizer_rate_shift_3': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(
|
| 128 |
}
|
| 129 |
|
| 130 |
except Exception as e:
|
|
@@ -456,8 +459,8 @@ def render_data_selection_config():
|
|
| 456 |
|
| 457 |
# Shifts selection
|
| 458 |
st.subheader("🕐 Shifts")
|
| 459 |
-
available_shifts =
|
| 460 |
-
shift_names =
|
| 461 |
|
| 462 |
if 'selected_shifts' not in st.session_state:
|
| 463 |
st.session_state.selected_shifts = available_shifts
|
|
@@ -473,8 +476,8 @@ def render_data_selection_config():
|
|
| 473 |
|
| 474 |
# Production lines selection
|
| 475 |
st.subheader("🏭 Production Lines")
|
| 476 |
-
available_lines =
|
| 477 |
-
line_names =
|
| 478 |
|
| 479 |
if 'selected_lines' not in st.session_state:
|
| 480 |
st.session_state.selected_lines = available_lines
|
|
@@ -510,9 +513,9 @@ def save_configuration():
|
|
| 510 |
'evening_shift_threshold': st.session_state.evening_shift_threshold,
|
| 511 |
'fixed_staff_mode': st.session_state.fixed_staff_mode,
|
| 512 |
'payment_mode_config': {
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
},
|
| 517 |
'workforce_limits': {
|
| 518 |
'max_unicef_per_day': st.session_state.max_unicef_per_day,
|
|
@@ -521,31 +524,31 @@ def save_configuration():
|
|
| 521 |
'working_hours': {
|
| 522 |
'max_hour_per_person_per_day': st.session_state.max_hour_per_person_per_day,
|
| 523 |
'max_hours_per_shift': {
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
}
|
| 528 |
},
|
| 529 |
'operations': {
|
| 530 |
'line_counts': {
|
| 531 |
-
|
| 532 |
-
|
| 533 |
},
|
| 534 |
'max_parallel_workers': {
|
| 535 |
-
|
| 536 |
-
|
| 537 |
}
|
| 538 |
},
|
| 539 |
'cost_rates': {
|
| 540 |
'UNICEF Fixed term': {
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
},
|
| 545 |
'Humanizer': {
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
}
|
| 550 |
},
|
| 551 |
'data_selection': {
|
|
@@ -603,11 +606,11 @@ def display_user_friendly_summary(config):
|
|
| 603 |
st.subheader("🏭 Operations Settings")
|
| 604 |
col1, col2 = st.columns(2)
|
| 605 |
with col1:
|
| 606 |
-
st.write(f"**Long Lines Available:** {config['operations']['line_counts'][
|
| 607 |
-
st.write(f"**Mini Load Lines Available:** {config['operations']['line_counts'][
|
| 608 |
with col2:
|
| 609 |
-
st.write(f"**Max Workers per Long Line:** {config['operations']['max_parallel_workers'][
|
| 610 |
-
st.write(f"**Max Workers per Mini Load Line:** {config['operations']['max_parallel_workers'][
|
| 611 |
|
| 612 |
# Cost Settings
|
| 613 |
st.subheader("💰 Cost Settings")
|
|
@@ -616,15 +619,15 @@ def display_user_friendly_summary(config):
|
|
| 616 |
col1, col2 = st.columns(2)
|
| 617 |
with col1:
|
| 618 |
st.write("*UNICEF Fixed Term Staff:*")
|
| 619 |
-
st.write(f"• Regular Shift: €{config['cost_rates']['UNICEF Fixed term'][
|
| 620 |
-
st.write(f"• Evening Shift: €{config['cost_rates']['UNICEF Fixed term'][
|
| 621 |
-
st.write(f"• Overtime Shift: €{config['cost_rates']['UNICEF Fixed term'][
|
| 622 |
|
| 623 |
with col2:
|
| 624 |
st.write("*Humanizer Staff:*")
|
| 625 |
-
st.write(f"• Regular Shift: €{config['cost_rates']['Humanizer'][
|
| 626 |
-
st.write(f"• Evening Shift: €{config['cost_rates']['Humanizer'][
|
| 627 |
-
st.write(f"• Overtime Shift: €{config['cost_rates']['Humanizer'][
|
| 628 |
|
| 629 |
# Payment Settings
|
| 630 |
st.write("**Payment Modes:**")
|
|
@@ -635,15 +638,15 @@ def display_user_friendly_summary(config):
|
|
| 635 |
|
| 636 |
col1, col2, col3 = st.columns(3)
|
| 637 |
with col1:
|
| 638 |
-
mode = config['payment_mode_config'][
|
| 639 |
st.write(f"• **Regular Shift:** {mode.title()}")
|
| 640 |
st.caption(payment_descriptions[mode])
|
| 641 |
with col2:
|
| 642 |
-
mode = config['payment_mode_config'][
|
| 643 |
st.write(f"• **Evening Shift:** {mode.title()}")
|
| 644 |
st.caption(payment_descriptions[mode])
|
| 645 |
with col3:
|
| 646 |
-
mode = config['payment_mode_config'][
|
| 647 |
st.write(f"• **Overtime Shift:** {mode.title()}")
|
| 648 |
st.caption(payment_descriptions[mode])
|
| 649 |
|
|
@@ -659,14 +662,14 @@ def display_user_friendly_summary(config):
|
|
| 659 |
|
| 660 |
with col2:
|
| 661 |
shifts = config['data_selection']['selected_shifts']
|
| 662 |
-
shift_names =
|
| 663 |
st.write(f"**Shifts:** {len(shifts)} selected")
|
| 664 |
for shift in shifts:
|
| 665 |
st.write(f"• Shift {shift} ({shift_names.get(shift, 'Unknown')})")
|
| 666 |
|
| 667 |
with col3:
|
| 668 |
lines = config['data_selection']['selected_lines']
|
| 669 |
-
line_names =
|
| 670 |
st.write(f"**Production Lines:** {len(lines)} selected")
|
| 671 |
for line in lines:
|
| 672 |
st.write(f"• Line {line} ({line_names.get(line, 'Unknown')})")
|
|
@@ -684,7 +687,7 @@ def display_user_friendly_summary(config):
|
|
| 684 |
st.metric("Max Daily Staff", f"{total_staff} people")
|
| 685 |
|
| 686 |
with col3:
|
| 687 |
-
total_lines = config['operations']['line_counts'][
|
| 688 |
st.metric("Production Lines", f"{total_lines} lines")
|
| 689 |
|
| 690 |
with col4:
|
|
|
|
| 7 |
import datetime
|
| 8 |
import sys
|
| 9 |
import os
|
| 10 |
+
from config import optimization_config
|
| 11 |
+
from src.config.constants import ShiftType, LineType
|
| 12 |
|
| 13 |
# Add src directory to path for imports
|
| 14 |
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
|
|
| 80 |
MAX_HOUR_PER_PERSON_PER_DAY, MAX_HOUR_PER_SHIFT_PER_PERSON,
|
| 81 |
MAX_PARALLEL_WORKERS, COST_LIST_PER_EMP_SHIFT,
|
| 82 |
PAYMENT_MODE_CONFIG, LINE_CNT_PER_TYPE,
|
| 83 |
+
MAX_EMPLOYEE_PER_TYPE_ON_DAY, start_date, end_date,
|
| 84 |
+
shift_code_to_name
|
| 85 |
)
|
| 86 |
|
| 87 |
# Get the actual computed default values from optimization_config.py
|
|
|
|
| 99 |
'fixed_staff_mode': FIXED_STAFF_CONSTRAINT_MODE,
|
| 100 |
|
| 101 |
# Payment configuration - from optimization_config.py
|
| 102 |
+
'payment_mode_shift_1': PAYMENT_MODE_CONFIG.get(ShiftType.REGULAR),
|
| 103 |
+
'payment_mode_shift_2': PAYMENT_MODE_CONFIG.get(ShiftType.EVENING),
|
| 104 |
+
'payment_mode_shift_3': PAYMENT_MODE_CONFIG.get(ShiftType.OVERTIME),
|
| 105 |
|
| 106 |
# Working hours - from optimization_config.py
|
| 107 |
'max_hour_per_person_per_day': MAX_HOUR_PER_PERSON_PER_DAY,
|
| 108 |
+
'max_hours_shift_1': MAX_HOUR_PER_SHIFT_PER_PERSON.get(ShiftType.REGULAR),
|
| 109 |
+
'max_hours_shift_2': MAX_HOUR_PER_SHIFT_PER_PERSON.get(ShiftType.EVENING),
|
| 110 |
+
'max_hours_shift_3': MAX_HOUR_PER_SHIFT_PER_PERSON.get(ShiftType.OVERTIME),
|
| 111 |
|
| 112 |
# Operations - from optimization_config.py
|
| 113 |
+
'max_parallel_workers_long_line': MAX_PARALLEL_WORKERS.get(LineType.LONG_LINE),
|
| 114 |
+
'max_parallel_workers_mini_load': MAX_PARALLEL_WORKERS.get(LineType.MINI_LOAD),
|
| 115 |
|
| 116 |
# Workforce limits - from optimization_config.py (computed values)
|
| 117 |
'max_unicef_per_day': list(MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("UNICEF Fixed term", {}).values())[0] if MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("UNICEF Fixed term") else 8,
|
| 118 |
'max_humanizer_per_day': list(MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("Humanizer", {}).values())[0] if MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("Humanizer") else 10,
|
| 119 |
|
| 120 |
# Line counts - from optimization_config.py (data-driven)
|
| 121 |
+
'line_count_long_line': LINE_CNT_PER_TYPE.get(LineType.LONG_LINE),
|
| 122 |
+
'line_count_mini_load': LINE_CNT_PER_TYPE.get(LineType.MINI_LOAD),
|
| 123 |
|
| 124 |
# Cost rates - from optimization_config.py (computed or default values)
|
| 125 |
+
'unicef_rate_shift_1': COST_LIST_PER_EMP_SHIFT.get("UNICEF Fixed term", {}).get(ShiftType.REGULAR),
|
| 126 |
+
'unicef_rate_shift_2': COST_LIST_PER_EMP_SHIFT.get("UNICEF Fixed term", {}).get(ShiftType.EVENING),
|
| 127 |
+
'unicef_rate_shift_3': COST_LIST_PER_EMP_SHIFT.get("UNICEF Fixed term", {}).get(ShiftType.OVERTIME),
|
| 128 |
+
'humanizer_rate_shift_1': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(ShiftType.REGULAR),
|
| 129 |
+
'humanizer_rate_shift_2': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(ShiftType.EVENING),
|
| 130 |
+
'humanizer_rate_shift_3': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(ShiftType.OVERTIME),
|
| 131 |
}
|
| 132 |
|
| 133 |
except Exception as e:
|
|
|
|
| 459 |
|
| 460 |
# Shifts selection
|
| 461 |
st.subheader("🕐 Shifts")
|
| 462 |
+
available_shifts = list(optimization_config.shift_code_to_name().keys())
|
| 463 |
+
shift_names = optimization_config.shift_code_to_name()
|
| 464 |
|
| 465 |
if 'selected_shifts' not in st.session_state:
|
| 466 |
st.session_state.selected_shifts = available_shifts
|
|
|
|
| 476 |
|
| 477 |
# Production lines selection
|
| 478 |
st.subheader("🏭 Production Lines")
|
| 479 |
+
available_lines = list(optimization_config.line_code_to_name().keys())
|
| 480 |
+
line_names = optimization_config.line_code_to_name()
|
| 481 |
|
| 482 |
if 'selected_lines' not in st.session_state:
|
| 483 |
st.session_state.selected_lines = available_lines
|
|
|
|
| 513 |
'evening_shift_threshold': st.session_state.evening_shift_threshold,
|
| 514 |
'fixed_staff_mode': st.session_state.fixed_staff_mode,
|
| 515 |
'payment_mode_config': {
|
| 516 |
+
ShiftType.REGULAR: st.session_state.payment_mode_shift_1,
|
| 517 |
+
ShiftType.EVENING: st.session_state.payment_mode_shift_2,
|
| 518 |
+
ShiftType.OVERTIME: st.session_state.payment_mode_shift_3,
|
| 519 |
},
|
| 520 |
'workforce_limits': {
|
| 521 |
'max_unicef_per_day': st.session_state.max_unicef_per_day,
|
|
|
|
| 524 |
'working_hours': {
|
| 525 |
'max_hour_per_person_per_day': st.session_state.max_hour_per_person_per_day,
|
| 526 |
'max_hours_per_shift': {
|
| 527 |
+
ShiftType.REGULAR: st.session_state.max_hours_shift_1,
|
| 528 |
+
ShiftType.EVENING: st.session_state.max_hours_shift_2,
|
| 529 |
+
ShiftType.OVERTIME: st.session_state.max_hours_shift_3,
|
| 530 |
}
|
| 531 |
},
|
| 532 |
'operations': {
|
| 533 |
'line_counts': {
|
| 534 |
+
LineType.LONG_LINE: st.session_state.line_count_long_line,
|
| 535 |
+
LineType.MINI_LOAD: st.session_state.line_count_mini_load,
|
| 536 |
},
|
| 537 |
'max_parallel_workers': {
|
| 538 |
+
LineType.LONG_LINE: st.session_state.max_parallel_workers_long_line,
|
| 539 |
+
LineType.MINI_LOAD: st.session_state.max_parallel_workers_mini_load,
|
| 540 |
}
|
| 541 |
},
|
| 542 |
'cost_rates': {
|
| 543 |
'UNICEF Fixed term': {
|
| 544 |
+
ShiftType.REGULAR: st.session_state.unicef_rate_shift_1,
|
| 545 |
+
ShiftType.EVENING: st.session_state.unicef_rate_shift_2,
|
| 546 |
+
ShiftType.OVERTIME: st.session_state.unicef_rate_shift_3,
|
| 547 |
},
|
| 548 |
'Humanizer': {
|
| 549 |
+
ShiftType.REGULAR: st.session_state.humanizer_rate_shift_1,
|
| 550 |
+
ShiftType.EVENING: st.session_state.humanizer_rate_shift_2,
|
| 551 |
+
ShiftType.OVERTIME: st.session_state.humanizer_rate_shift_3,
|
| 552 |
}
|
| 553 |
},
|
| 554 |
'data_selection': {
|
|
|
|
| 606 |
st.subheader("🏭 Operations Settings")
|
| 607 |
col1, col2 = st.columns(2)
|
| 608 |
with col1:
|
| 609 |
+
st.write(f"**Long Lines Available:** {config['operations']['line_counts'][LineType.LONG_LINE]} lines")
|
| 610 |
+
st.write(f"**Mini Load Lines Available:** {config['operations']['line_counts'][LineType.MINI_LOAD]} lines")
|
| 611 |
with col2:
|
| 612 |
+
st.write(f"**Max Workers per Long Line:** {config['operations']['max_parallel_workers'][LineType.LONG_LINE]} people")
|
| 613 |
+
st.write(f"**Max Workers per Mini Load Line:** {config['operations']['max_parallel_workers'][LineType.MINI_LOAD]} people")
|
| 614 |
|
| 615 |
# Cost Settings
|
| 616 |
st.subheader("💰 Cost Settings")
|
|
|
|
| 619 |
col1, col2 = st.columns(2)
|
| 620 |
with col1:
|
| 621 |
st.write("*UNICEF Fixed Term Staff:*")
|
| 622 |
+
st.write(f"• Regular Shift: €{config['cost_rates']['UNICEF Fixed term'][ShiftType.REGULAR]:.2f}/hour")
|
| 623 |
+
st.write(f"• Evening Shift: €{config['cost_rates']['UNICEF Fixed term'][ShiftType.EVENING]:.2f}/hour")
|
| 624 |
+
st.write(f"• Overtime Shift: €{config['cost_rates']['UNICEF Fixed term'][ShiftType.OVERTIME]:.2f}/hour")
|
| 625 |
|
| 626 |
with col2:
|
| 627 |
st.write("*Humanizer Staff:*")
|
| 628 |
+
st.write(f"• Regular Shift: €{config['cost_rates']['Humanizer'][ShiftType.REGULAR]:.2f}/hour")
|
| 629 |
+
st.write(f"• Evening Shift: €{config['cost_rates']['Humanizer'][ShiftType.EVENING]:.2f}/hour")
|
| 630 |
+
st.write(f"• Overtime Shift: €{config['cost_rates']['Humanizer'][ShiftType.OVERTIME]:.2f}/hour")
|
| 631 |
|
| 632 |
# Payment Settings
|
| 633 |
st.write("**Payment Modes:**")
|
|
|
|
| 638 |
|
| 639 |
col1, col2, col3 = st.columns(3)
|
| 640 |
with col1:
|
| 641 |
+
mode = config['payment_mode_config'][ShiftType.REGULAR]
|
| 642 |
st.write(f"• **Regular Shift:** {mode.title()}")
|
| 643 |
st.caption(payment_descriptions[mode])
|
| 644 |
with col2:
|
| 645 |
+
mode = config['payment_mode_config'][ShiftType.EVENING]
|
| 646 |
st.write(f"• **Evening Shift:** {mode.title()}")
|
| 647 |
st.caption(payment_descriptions[mode])
|
| 648 |
with col3:
|
| 649 |
+
mode = config['payment_mode_config'][ShiftType.OVERTIME]
|
| 650 |
st.write(f"• **Overtime Shift:** {mode.title()}")
|
| 651 |
st.caption(payment_descriptions[mode])
|
| 652 |
|
|
|
|
| 662 |
|
| 663 |
with col2:
|
| 664 |
shifts = config['data_selection']['selected_shifts']
|
| 665 |
+
shift_names = ShiftType.get_all_names()
|
| 666 |
st.write(f"**Shifts:** {len(shifts)} selected")
|
| 667 |
for shift in shifts:
|
| 668 |
st.write(f"• Shift {shift} ({shift_names.get(shift, 'Unknown')})")
|
| 669 |
|
| 670 |
with col3:
|
| 671 |
lines = config['data_selection']['selected_lines']
|
| 672 |
+
line_names = LineType.get_all_names()
|
| 673 |
st.write(f"**Production Lines:** {len(lines)} selected")
|
| 674 |
for line in lines:
|
| 675 |
st.write(f"• Line {line} ({line_names.get(line, 'Unknown')})")
|
|
|
|
| 687 |
st.metric("Max Daily Staff", f"{total_staff} people")
|
| 688 |
|
| 689 |
with col3:
|
| 690 |
+
total_lines = config['operations']['line_counts'][LineType.LONG_LINE] + config['operations']['line_counts'][LineType.MINI_LOAD]
|
| 691 |
st.metric("Production Lines", f"{total_lines} lines")
|
| 692 |
|
| 693 |
with col4:
|
src/config/optimization_config.py
CHANGED
|
@@ -3,6 +3,7 @@ import src.etl.transform as transformed_data
|
|
| 3 |
import datetime
|
| 4 |
from datetime import timedelta
|
| 5 |
import src.etl.extract as extract
|
|
|
|
| 6 |
|
| 7 |
# Re-import all the packages
|
| 8 |
import importlib
|
|
@@ -98,8 +99,8 @@ def get_active_shift_list():
|
|
| 98 |
all_shifts = get_shift_list()
|
| 99 |
|
| 100 |
if EVENING_SHIFT_MODE == "normal":
|
| 101 |
-
# Only regular
|
| 102 |
-
active_shifts = [s for s in all_shifts if s in
|
| 103 |
print(f"[SHIFT MODE] Normal mode: Using shifts {active_shifts} (Regular + Overtime only, NO evening)")
|
| 104 |
|
| 105 |
elif EVENING_SHIFT_MODE == "activate_evening":
|
|
@@ -114,7 +115,7 @@ def get_active_shift_list():
|
|
| 114 |
|
| 115 |
else:
|
| 116 |
# Default to normal mode
|
| 117 |
-
active_shifts = [s for s in all_shifts if s in
|
| 118 |
print(f"[SHIFT MODE] Unknown mode '{EVENING_SHIFT_MODE}', defaulting to normal: {active_shifts}")
|
| 119 |
|
| 120 |
return active_shifts
|
|
@@ -150,10 +151,10 @@ def get_kit_line_match():
|
|
| 150 |
|
| 151 |
# Create line name to ID mapping
|
| 152 |
line_name_to_id = {
|
| 153 |
-
"long line":
|
| 154 |
-
"mini load":
|
| 155 |
-
"Long_line":
|
| 156 |
-
"Mini_load":
|
| 157 |
}
|
| 158 |
|
| 159 |
# Convert string line names to numeric IDs
|
|
@@ -167,13 +168,13 @@ def get_kit_line_match():
|
|
| 167 |
else:
|
| 168 |
print(f"Warning: Unknown line type '{line_name}' for kit {kit}")
|
| 169 |
# Default to long line if unknown
|
| 170 |
-
converted_dict[kit] =
|
| 171 |
elif isinstance(line_name, (int, float)) and not pd.isna(line_name):
|
| 172 |
# Already numeric
|
| 173 |
converted_dict[kit] = int(line_name)
|
| 174 |
else:
|
| 175 |
# Missing or empty line type - default to long line
|
| 176 |
-
converted_dict[kit] =
|
| 177 |
|
| 178 |
return converted_dict
|
| 179 |
|
|
@@ -231,24 +232,14 @@ def get_cost_list_per_emp_shift():
|
|
| 231 |
|
| 232 |
print(f"Loading default cost values")
|
| 233 |
# Default hourly rates - Important: multiple employment types with different costs
|
| 234 |
-
|
| 235 |
-
return {"UNICEF Fixed term":{1:43.27,2:43.27,3:64.91},"Humanizer":{1:27.94,2:27.94,3:41.91}}
|
| 236 |
|
| 237 |
def shift_code_to_name():
|
| 238 |
-
|
| 239 |
-
1: "Regular",
|
| 240 |
-
2: "Evening",
|
| 241 |
-
3: "Overtime"
|
| 242 |
-
}
|
| 243 |
-
return shift_code_to_name_dict
|
| 244 |
|
| 245 |
def line_code_to_name():
|
| 246 |
"""Convert line type IDs to readable names"""
|
| 247 |
-
|
| 248 |
-
6: "Long Line",
|
| 249 |
-
7: "Mini Load"
|
| 250 |
-
}
|
| 251 |
-
return line_code_to_name_dict
|
| 252 |
|
| 253 |
COST_LIST_PER_EMP_SHIFT = get_cost_list_per_emp_shift()
|
| 254 |
# print("cost list per emp shift",COST_LIST_PER_EMP_SHIFT)
|
|
@@ -339,7 +330,7 @@ print("max employee per type on day",MAX_EMPLOYEE_PER_TYPE_ON_DAY)
|
|
| 339 |
# available employee but for fixed in shift 1, it is mandatory employment
|
| 340 |
|
| 341 |
MAX_HOUR_PER_PERSON_PER_DAY = 14 # legal standard
|
| 342 |
-
MAX_HOUR_PER_SHIFT_PER_PERSON =
|
| 343 |
def get_per_product_speed():
|
| 344 |
try:
|
| 345 |
# Try to get from streamlit session state (from config page)
|
|
@@ -378,10 +369,7 @@ def get_kit_hierarchy_data():
|
|
| 378 |
KIT_LEVELS, KIT_DEPENDENCIES, PRODUCTION_PRIORITY_ORDER = get_kit_hierarchy_data()
|
| 379 |
print(f"Kit Hierarchy loaded: {len(KIT_LEVELS)} kits, Priority order: {len(PRODUCTION_PRIORITY_ORDER)} items")
|
| 380 |
|
| 381 |
-
MAX_PARALLEL_WORKERS =
|
| 382 |
-
6: 15, # long line can have max 15 workers simultaneously
|
| 383 |
-
7: 15, # mini load can have max 15 workers simultaneously
|
| 384 |
-
}
|
| 385 |
# maximum number of workers that can work on a line at the same time
|
| 386 |
|
| 387 |
DAILY_WEEKLY_SCHEDULE = "daily" # daily or weekly ,this needs to be implementedin in if F_x1_day is not None... F_x1_week is not None... also need to change x1 to Fixedstaff_first_shift
|
|
@@ -412,13 +400,8 @@ def get_payment_mode_config():
|
|
| 412 |
print(f"Could not get payment mode config from streamlit session: {e}")
|
| 413 |
|
| 414 |
# Default payment mode configuration
|
| 415 |
-
# Shift 1: bulk, Shift 2: bulk (evening), Shift 3: partial (overtime)
|
| 416 |
print(f"Loading default payment mode configuration")
|
| 417 |
-
payment_mode_config =
|
| 418 |
-
1: "bulk", # Regular shift - bulk payment
|
| 419 |
-
2: "bulk", # Evening shift - bulk payment
|
| 420 |
-
3: "partial" # Overtime shift - partial payment
|
| 421 |
-
}
|
| 422 |
|
| 423 |
return payment_mode_config
|
| 424 |
|
|
|
|
| 3 |
import datetime
|
| 4 |
from datetime import timedelta
|
| 5 |
import src.etl.extract as extract
|
| 6 |
+
from src.config.constants import ShiftType, LineType, KitLevel, DefaultConfig
|
| 7 |
|
| 8 |
# Re-import all the packages
|
| 9 |
import importlib
|
|
|
|
| 99 |
all_shifts = get_shift_list()
|
| 100 |
|
| 101 |
if EVENING_SHIFT_MODE == "normal":
|
| 102 |
+
# Only regular and overtime shifts - NO evening shift
|
| 103 |
+
active_shifts = [s for s in all_shifts if s in ShiftType.REGULAR_AND_OVERTIME]
|
| 104 |
print(f"[SHIFT MODE] Normal mode: Using shifts {active_shifts} (Regular + Overtime only, NO evening)")
|
| 105 |
|
| 106 |
elif EVENING_SHIFT_MODE == "activate_evening":
|
|
|
|
| 115 |
|
| 116 |
else:
|
| 117 |
# Default to normal mode
|
| 118 |
+
active_shifts = [s for s in all_shifts if s in ShiftType.REGULAR_AND_OVERTIME]
|
| 119 |
print(f"[SHIFT MODE] Unknown mode '{EVENING_SHIFT_MODE}', defaulting to normal: {active_shifts}")
|
| 120 |
|
| 121 |
return active_shifts
|
|
|
|
| 151 |
|
| 152 |
# Create line name to ID mapping
|
| 153 |
line_name_to_id = {
|
| 154 |
+
"long line": LineType.LONG_LINE,
|
| 155 |
+
"mini load": LineType.MINI_LOAD,
|
| 156 |
+
"Long_line": LineType.LONG_LINE, # Alternative naming
|
| 157 |
+
"Mini_load": LineType.MINI_LOAD, # Alternative naming
|
| 158 |
}
|
| 159 |
|
| 160 |
# Convert string line names to numeric IDs
|
|
|
|
| 168 |
else:
|
| 169 |
print(f"Warning: Unknown line type '{line_name}' for kit {kit}")
|
| 170 |
# Default to long line if unknown
|
| 171 |
+
converted_dict[kit] = LineType.LONG_LINE
|
| 172 |
elif isinstance(line_name, (int, float)) and not pd.isna(line_name):
|
| 173 |
# Already numeric
|
| 174 |
converted_dict[kit] = int(line_name)
|
| 175 |
else:
|
| 176 |
# Missing or empty line type - default to long line
|
| 177 |
+
converted_dict[kit] = LineType.LONG_LINE
|
| 178 |
|
| 179 |
return converted_dict
|
| 180 |
|
|
|
|
| 232 |
|
| 233 |
print(f"Loading default cost values")
|
| 234 |
# Default hourly rates - Important: multiple employment types with different costs
|
| 235 |
+
return DefaultConfig.DEFAULT_COST_RATES
|
|
|
|
| 236 |
|
| 237 |
def shift_code_to_name():
|
| 238 |
+
return ShiftType.get_all_names()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
def line_code_to_name():
|
| 241 |
"""Convert line type IDs to readable names"""
|
| 242 |
+
return LineType.get_all_names()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
COST_LIST_PER_EMP_SHIFT = get_cost_list_per_emp_shift()
|
| 245 |
# print("cost list per emp shift",COST_LIST_PER_EMP_SHIFT)
|
|
|
|
| 330 |
# available employee but for fixed in shift 1, it is mandatory employment
|
| 331 |
|
| 332 |
MAX_HOUR_PER_PERSON_PER_DAY = 14 # legal standard
|
| 333 |
+
MAX_HOUR_PER_SHIFT_PER_PERSON = DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON
|
| 334 |
def get_per_product_speed():
|
| 335 |
try:
|
| 336 |
# Try to get from streamlit session state (from config page)
|
|
|
|
| 369 |
KIT_LEVELS, KIT_DEPENDENCIES, PRODUCTION_PRIORITY_ORDER = get_kit_hierarchy_data()
|
| 370 |
print(f"Kit Hierarchy loaded: {len(KIT_LEVELS)} kits, Priority order: {len(PRODUCTION_PRIORITY_ORDER)} items")
|
| 371 |
|
| 372 |
+
MAX_PARALLEL_WORKERS = DefaultConfig.MAX_PARALLEL_WORKERS
|
|
|
|
|
|
|
|
|
|
| 373 |
# maximum number of workers that can work on a line at the same time
|
| 374 |
|
| 375 |
DAILY_WEEKLY_SCHEDULE = "daily" # daily or weekly ,this needs to be implementedin in if F_x1_day is not None... F_x1_week is not None... also need to change x1 to Fixedstaff_first_shift
|
|
|
|
| 400 |
print(f"Could not get payment mode config from streamlit session: {e}")
|
| 401 |
|
| 402 |
# Default payment mode configuration
|
|
|
|
| 403 |
print(f"Loading default payment mode configuration")
|
| 404 |
+
payment_mode_config = DefaultConfig.PAYMENT_MODE_CONFIG
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
|
| 406 |
return payment_mode_config
|
| 407 |
|
src/models/optimizer_real.py
CHANGED
|
@@ -9,6 +9,7 @@
|
|
| 9 |
|
| 10 |
from ortools.linear_solver import pywraplp
|
| 11 |
from math import ceil
|
|
|
|
| 12 |
|
| 13 |
# ---- config import (프로젝트 경로에 맞춰 조정) ----
|
| 14 |
from src.config.optimization_config import (
|
|
@@ -49,7 +50,7 @@ print("KIT_LINE_MATCH_DICT",KIT_LINE_MATCH_DICT)
|
|
| 49 |
|
| 50 |
# 3) If specific product is not produced on specific date, set it to 0
|
| 51 |
ACTIVE = {t: {p: 1 for p in PRODUCT_LIST} for t in DATE_SPAN}
|
| 52 |
-
#
|
| 53 |
|
| 54 |
|
| 55 |
def build_lines():
|
|
@@ -89,7 +90,7 @@ def sort_products_by_hierarchy(product_list):
|
|
| 89 |
print(f"[HIERARCHY] Production order: {len(sorted_products)} products")
|
| 90 |
for i, p in enumerate(sorted_products[:10]): # Show first 10
|
| 91 |
level = KIT_LEVELS.get(p, "unknown")
|
| 92 |
-
level_name =
|
| 93 |
deps = KIT_DEPENDENCIES.get(p, [])
|
| 94 |
print(f" {i+1}. {p} (level {level}={level_name}, deps: {len(deps)})")
|
| 95 |
|
|
@@ -103,10 +104,8 @@ def get_dependency_timing_weight(product):
|
|
| 103 |
Calculate timing weight based on hierarchy level.
|
| 104 |
Lower levels (prepacks) should be produced earlier.
|
| 105 |
"""
|
| 106 |
-
level = KIT_LEVELS.get(product,
|
| 107 |
-
|
| 108 |
-
weights = {0: 0.1, 1: 0.5, 2: 1.0}
|
| 109 |
-
return weights.get(level, 1.0)
|
| 110 |
|
| 111 |
def solve_fixed_team_weekly():
|
| 112 |
# --- Sets ---
|
|
@@ -148,7 +147,7 @@ def solve_fixed_team_weekly():
|
|
| 148 |
total_demand = sum(DEMAND_DICTIONARY.get(p, 0) for p in P)
|
| 149 |
|
| 150 |
# Calculate maximum capacity with regular + overtime shifts only
|
| 151 |
-
regular_overtime_shifts = [s for s in S if s in
|
| 152 |
max_capacity = 0
|
| 153 |
|
| 154 |
for p in P:
|
|
@@ -268,7 +267,7 @@ def solve_fixed_team_weekly():
|
|
| 268 |
|
| 269 |
# 3) Product-line type compatibility + (optional) activity by day
|
| 270 |
for p in P:
|
| 271 |
-
req_lt = KIT_LINE_MATCH_DICT.get(p,
|
| 272 |
req_total = sum(TEAM_REQ_PER_PRODUCT[e][p] for e in E)
|
| 273 |
for ell in L:
|
| 274 |
allowed = (ell[0] == req_lt) and (req_total <= max_workers_line.get(ell[0], 1e9))
|
|
@@ -317,24 +316,24 @@ def solve_fixed_team_weekly():
|
|
| 317 |
)
|
| 318 |
|
| 319 |
# 7) Shift ordering constraints (only apply if shifts are available)
|
| 320 |
-
# Evening shift
|
| 321 |
-
if
|
| 322 |
for e in E:
|
| 323 |
for t in D:
|
| 324 |
solver.Add(
|
| 325 |
-
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell,
|
| 326 |
<=
|
| 327 |
-
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell,
|
| 328 |
)
|
| 329 |
|
| 330 |
-
# Overtime shift
|
| 331 |
-
if
|
| 332 |
for e in E:
|
| 333 |
for t in D:
|
| 334 |
solver.Add(
|
| 335 |
-
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell,
|
| 336 |
<=
|
| 337 |
-
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell,
|
| 338 |
)
|
| 339 |
|
| 340 |
# 7.5) Bulk payment linking constraints are now handled inline in the cost calculation
|
|
@@ -371,7 +370,8 @@ def solve_fixed_team_weekly():
|
|
| 371 |
# --- Solve ---
|
| 372 |
status = solver.Solve()
|
| 373 |
if status != pywraplp.Solver.OPTIMAL:
|
| 374 |
-
|
|
|
|
| 375 |
# Debug hint:
|
| 376 |
# solver.EnableOutput()
|
| 377 |
# solver.ExportModelAsLpFile("model.lp")
|
|
@@ -433,12 +433,15 @@ def solve_fixed_team_weekly():
|
|
| 433 |
|
| 434 |
print("\n--- Schedule (line, shift, day) ---")
|
| 435 |
for row in schedule:
|
| 436 |
-
|
|
|
|
|
|
|
| 437 |
f"{row['product']} T={row['run_hours']:.2f}h U={row['units']:.1f}")
|
| 438 |
|
| 439 |
print("\n--- Implied headcount need (per type/shift/day) ---")
|
| 440 |
for row in headcount:
|
| 441 |
-
|
|
|
|
| 442 |
f"need={row['needed']} (avail {row['available']})")
|
| 443 |
|
| 444 |
print("\n--- Total person-hours by type/day ---")
|
|
|
|
| 9 |
|
| 10 |
from ortools.linear_solver import pywraplp
|
| 11 |
from math import ceil
|
| 12 |
+
from src.config.constants import ShiftType, LineType, KitLevel
|
| 13 |
|
| 14 |
# ---- config import (프로젝트 경로에 맞춰 조정) ----
|
| 15 |
from src.config.optimization_config import (
|
|
|
|
| 50 |
|
| 51 |
# 3) If specific product is not produced on specific date, set it to 0
|
| 52 |
ACTIVE = {t: {p: 1 for p in PRODUCT_LIST} for t in DATE_SPAN}
|
| 53 |
+
# Example: ACTIVE[2]['C'] = 0 # Disable product C on day 2
|
| 54 |
|
| 55 |
|
| 56 |
def build_lines():
|
|
|
|
| 90 |
print(f"[HIERARCHY] Production order: {len(sorted_products)} products")
|
| 91 |
for i, p in enumerate(sorted_products[:10]): # Show first 10
|
| 92 |
level = KIT_LEVELS.get(p, "unknown")
|
| 93 |
+
level_name = KitLevel.get_name(level)
|
| 94 |
deps = KIT_DEPENDENCIES.get(p, [])
|
| 95 |
print(f" {i+1}. {p} (level {level}={level_name}, deps: {len(deps)})")
|
| 96 |
|
|
|
|
| 104 |
Calculate timing weight based on hierarchy level.
|
| 105 |
Lower levels (prepacks) should be produced earlier.
|
| 106 |
"""
|
| 107 |
+
level = KIT_LEVELS.get(product, KitLevel.MASTER) # Default to master level
|
| 108 |
+
return KitLevel.get_timing_weight(level)
|
|
|
|
|
|
|
| 109 |
|
| 110 |
def solve_fixed_team_weekly():
|
| 111 |
# --- Sets ---
|
|
|
|
| 147 |
total_demand = sum(DEMAND_DICTIONARY.get(p, 0) for p in P)
|
| 148 |
|
| 149 |
# Calculate maximum capacity with regular + overtime shifts only
|
| 150 |
+
regular_overtime_shifts = [s for s in S if s in ShiftType.REGULAR_AND_OVERTIME]
|
| 151 |
max_capacity = 0
|
| 152 |
|
| 153 |
for p in P:
|
|
|
|
| 267 |
|
| 268 |
# 3) Product-line type compatibility + (optional) activity by day
|
| 269 |
for p in P:
|
| 270 |
+
req_lt = KIT_LINE_MATCH_DICT.get(p, LineType.LONG_LINE) # Default to long line if not found
|
| 271 |
req_total = sum(TEAM_REQ_PER_PRODUCT[e][p] for e in E)
|
| 272 |
for ell in L:
|
| 273 |
allowed = (ell[0] == req_lt) and (req_total <= max_workers_line.get(ell[0], 1e9))
|
|
|
|
| 316 |
)
|
| 317 |
|
| 318 |
# 7) Shift ordering constraints (only apply if shifts are available)
|
| 319 |
+
# Evening shift after regular shift
|
| 320 |
+
if ShiftType.EVENING in S and ShiftType.REGULAR in S: # Only if both shifts are available
|
| 321 |
for e in E:
|
| 322 |
for t in D:
|
| 323 |
solver.Add(
|
| 324 |
+
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell, ShiftType.EVENING, t] for p in P for ell in L)
|
| 325 |
<=
|
| 326 |
+
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell, ShiftType.REGULAR, t] for p in P for ell in L)
|
| 327 |
)
|
| 328 |
|
| 329 |
+
# Overtime shift after regular shift
|
| 330 |
+
if ShiftType.OVERTIME in S and ShiftType.REGULAR in S: # Only if both shifts are available
|
| 331 |
for e in E:
|
| 332 |
for t in D:
|
| 333 |
solver.Add(
|
| 334 |
+
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell, ShiftType.OVERTIME, t] for p in P for ell in L)
|
| 335 |
<=
|
| 336 |
+
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell, ShiftType.REGULAR, t] for p in P for ell in L)
|
| 337 |
)
|
| 338 |
|
| 339 |
# 7.5) Bulk payment linking constraints are now handled inline in the cost calculation
|
|
|
|
| 370 |
# --- Solve ---
|
| 371 |
status = solver.Solve()
|
| 372 |
if status != pywraplp.Solver.OPTIMAL:
|
| 373 |
+
status_names = {pywraplp.Solver.INFEASIBLE: "INFEASIBLE", pywraplp.Solver.UNBOUNDED: "UNBOUNDED"}
|
| 374 |
+
print(f"No optimal solution. Status: {status} ({status_names.get(status, 'UNKNOWN')})")
|
| 375 |
# Debug hint:
|
| 376 |
# solver.EnableOutput()
|
| 377 |
# solver.ExportModelAsLpFile("model.lp")
|
|
|
|
| 433 |
|
| 434 |
print("\n--- Schedule (line, shift, day) ---")
|
| 435 |
for row in schedule:
|
| 436 |
+
shift_name = ShiftType.get_name(row['shift'])
|
| 437 |
+
line_name = LineType.get_name(row['line_type_id'])
|
| 438 |
+
print(f"D{row['day']} {line_name}-{row['line_idx']} {shift_name}: "
|
| 439 |
f"{row['product']} T={row['run_hours']:.2f}h U={row['units']:.1f}")
|
| 440 |
|
| 441 |
print("\n--- Implied headcount need (per type/shift/day) ---")
|
| 442 |
for row in headcount:
|
| 443 |
+
shift_name = ShiftType.get_name(row['shift'])
|
| 444 |
+
print(f"{row['emp_type']}, {shift_name}, D{row['day']}: "
|
| 445 |
f"need={row['needed']} (avail {row['available']})")
|
| 446 |
|
| 447 |
print("\n--- Total person-hours by type/day ---")
|