HaLim commited on
Commit Β·
1b82889
1
Parent(s): 3d08e0e
update viz
Browse files- config_page.py +317 -24
- optimization_results.py +287 -14
config_page.py
CHANGED
|
@@ -7,7 +7,7 @@ import streamlit as st
|
|
| 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
|
|
@@ -24,7 +24,7 @@ def render_config_page():
|
|
| 24 |
initialize_session_state()
|
| 25 |
|
| 26 |
# Create tabs for better organization
|
| 27 |
-
tab1, tab2, tab3, tab4 = st.tabs(["π
Schedule", "π₯ Workforce", "π Operations", "π° Cost"])
|
| 28 |
|
| 29 |
with tab1:
|
| 30 |
render_schedule_config()
|
|
@@ -38,6 +38,9 @@ def render_config_page():
|
|
| 38 |
with tab4:
|
| 39 |
render_cost_config()
|
| 40 |
|
|
|
|
|
|
|
|
|
|
| 41 |
# Save configuration button
|
| 42 |
st.markdown("---")
|
| 43 |
col1, col2, col3 = st.columns([1, 1, 1])
|
|
@@ -45,6 +48,8 @@ def render_config_page():
|
|
| 45 |
if st.button("πΎ Save Settings", type="primary", use_container_width=True):
|
| 46 |
config = save_configuration()
|
| 47 |
st.success("β
Settings saved successfully!")
|
|
|
|
|
|
|
| 48 |
|
| 49 |
# Display settings summary at full width (outside columns)
|
| 50 |
st.markdown("---")
|
|
@@ -52,15 +57,60 @@ def render_config_page():
|
|
| 52 |
with st.expander("π Settings Summary", expanded=False):
|
| 53 |
display_user_friendly_summary(st.session_state.optimization_config)
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
# Optimization section
|
| 56 |
st.markdown("---")
|
| 57 |
st.header("π Run Optimization")
|
| 58 |
st.markdown("Once you've configured your settings, run the optimization to generate the optimal workforce schedule.")
|
|
|
|
| 59 |
|
| 60 |
col1, col2, col3 = st.columns([1, 1, 1])
|
|
|
|
|
|
|
|
|
|
| 61 |
with col2:
|
| 62 |
if st.button("π Optimize Schedule", type="primary", use_container_width=True):
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
# Display optimization results if available
|
| 66 |
if 'optimization_results' in st.session_state and st.session_state.optimization_results is not None:
|
|
@@ -81,14 +131,13 @@ def initialize_session_state():
|
|
| 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, FIXED_MIN_UNICEF_PER_DAY
|
| 85 |
)
|
| 86 |
|
| 87 |
# Get the actual computed default values from optimization_config.py
|
| 88 |
defaults = {
|
| 89 |
# Schedule configuration - from optimization_config.py
|
| 90 |
'start_date': start_date.date() if hasattr(start_date, 'date') else start_date,
|
| 91 |
-
'end_date': end_date.date() if hasattr(end_date, 'date') else end_date,
|
| 92 |
'schedule_type': DAILY_WEEKLY_SCHEDULE,
|
| 93 |
|
| 94 |
# Shift configuration - from optimization_config.py
|
|
@@ -128,7 +177,12 @@ def initialize_session_state():
|
|
| 128 |
'unicef_rate_shift_3': COST_LIST_PER_EMP_SHIFT.get("UNICEF Fixed term", {}).get(ShiftType.OVERTIME),
|
| 129 |
'humanizer_rate_shift_1': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(ShiftType.REGULAR),
|
| 130 |
'humanizer_rate_shift_2': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(ShiftType.EVENING),
|
| 131 |
-
'humanizer_rate_shift_3': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(ShiftType.OVERTIME),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
}
|
| 133 |
|
| 134 |
except Exception as e:
|
|
@@ -151,21 +205,14 @@ def render_schedule_config():
|
|
| 151 |
st.session_state.start_date = st.date_input(
|
| 152 |
"Start Date",
|
| 153 |
value=st.session_state.start_date,
|
| 154 |
-
help="
|
| 155 |
)
|
| 156 |
|
| 157 |
with col2:
|
| 158 |
-
st.
|
| 159 |
-
"End Date",
|
| 160 |
-
value=st.session_state.end_date,
|
| 161 |
-
help="End date for the optimization period"
|
| 162 |
-
)
|
| 163 |
-
|
| 164 |
-
# Validate date range
|
| 165 |
-
if st.session_state.start_date > st.session_state.end_date:
|
| 166 |
-
st.error("β οΈ Start date must be before or equal to end date!")
|
| 167 |
|
| 168 |
# Schedule type
|
|
|
|
| 169 |
st.session_state.schedule_type = st.selectbox(
|
| 170 |
"Schedule Type",
|
| 171 |
options=['daily', 'weekly'],
|
|
@@ -514,11 +561,49 @@ def render_data_selection_config():
|
|
| 514 |
def save_configuration():
|
| 515 |
"""Save current configuration to session state and potentially to file"""
|
| 516 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
# Create comprehensive configuration dictionary
|
| 518 |
config = {
|
| 519 |
'date_range': {
|
| 520 |
'start_date': st.session_state.start_date,
|
| 521 |
-
'end_date':
|
|
|
|
| 522 |
},
|
| 523 |
'schedule_type': st.session_state.schedule_type,
|
| 524 |
'evening_shift_mode': st.session_state.evening_shift_mode,
|
|
@@ -571,13 +656,17 @@ def save_configuration():
|
|
| 571 |
}
|
| 572 |
}
|
| 573 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
# Store individual items in session state for optimization_config.py to access
|
| 575 |
st.session_state.line_counts = config['operations']['line_counts']
|
| 576 |
st.session_state.cost_list_per_emp_shift = config['cost_rates']
|
| 577 |
st.session_state.payment_mode_config = config['payment_mode_config']
|
| 578 |
st.session_state.max_employee_per_type_on_day = {
|
| 579 |
-
"UNICEF Fixed term": {t: st.session_state.max_unicef_per_day for t in
|
| 580 |
-
"Humanizer": {t: st.session_state.max_humanizer_per_day for t in
|
| 581 |
}
|
| 582 |
|
| 583 |
# Store complete configuration
|
|
@@ -595,15 +684,21 @@ def display_user_friendly_summary(config):
|
|
| 595 |
with col1:
|
| 596 |
st.write(f"**Start Date:** {config['date_range']['start_date']}")
|
| 597 |
with col2:
|
| 598 |
-
st.write(f"**
|
| 599 |
with col3:
|
| 600 |
-
st.write(f"**
|
| 601 |
with col4:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
st.write(f"**Evening Shift Mode:** {config['evening_shift_mode'].replace('_', ' ').title()}")
|
| 603 |
|
| 604 |
# Show additional schedule details if evening shift threshold is relevant
|
| 605 |
if config['evening_shift_mode'] == 'activate_evening':
|
| 606 |
-
|
|
|
|
| 607 |
|
| 608 |
# Workforce Settings
|
| 609 |
st.subheader("π₯ Workforce Settings")
|
|
@@ -693,7 +788,7 @@ def display_user_friendly_summary(config):
|
|
| 693 |
col1, col2, col3, col4 = st.columns(4)
|
| 694 |
|
| 695 |
with col1:
|
| 696 |
-
duration =
|
| 697 |
st.metric("Planning Period", f"{duration} days")
|
| 698 |
|
| 699 |
with col2:
|
|
@@ -707,13 +802,161 @@ def display_user_friendly_summary(config):
|
|
| 707 |
with col4:
|
| 708 |
avg_unicef_rate = sum(config['cost_rates']['UNICEF Fixed term'].values()) / 3
|
| 709 |
st.metric("Avg UNICEF Rate", f"β¬{avg_unicef_rate:.2f}/hr")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 710 |
|
| 711 |
def run_optimization():
|
| 712 |
"""Run the optimization model and store results"""
|
| 713 |
try:
|
| 714 |
st.info("π Running optimization... This may take a few moments.")
|
| 715 |
|
| 716 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
sys.path.append('src')
|
| 718 |
from models.optimizer_real import solve_fixed_team_weekly
|
| 719 |
|
|
@@ -723,6 +966,8 @@ def run_optimization():
|
|
| 723 |
|
| 724 |
if results is None:
|
| 725 |
st.error("β Optimization failed! The problem may be infeasible with current settings.")
|
|
|
|
|
|
|
| 726 |
st.error("Try adjusting your workforce limits, line counts, or evening shift settings.")
|
| 727 |
return
|
| 728 |
|
|
@@ -735,6 +980,54 @@ def run_optimization():
|
|
| 735 |
st.error(f"β Error during optimization: {str(e)}")
|
| 736 |
st.error("Please check your settings and data files.")
|
| 737 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
def display_optimization_results(results):
|
| 739 |
"""Import and display optimization results"""
|
| 740 |
from optimization_results import display_optimization_results as display_results
|
|
|
|
| 7 |
import datetime
|
| 8 |
import sys
|
| 9 |
import os
|
| 10 |
+
from src.config import optimization_config
|
| 11 |
from src.config.constants import ShiftType, LineType
|
| 12 |
|
| 13 |
# Add src directory to path for imports
|
|
|
|
| 24 |
initialize_session_state()
|
| 25 |
|
| 26 |
# Create tabs for better organization
|
| 27 |
+
tab1, tab2, tab3, tab4, tab5 = st.tabs(["π
Schedule", "π₯ Workforce", "π Operations", "π° Cost", "π Data Selection"])
|
| 28 |
|
| 29 |
with tab1:
|
| 30 |
render_schedule_config()
|
|
|
|
| 38 |
with tab4:
|
| 39 |
render_cost_config()
|
| 40 |
|
| 41 |
+
with tab5:
|
| 42 |
+
render_data_selection_config()
|
| 43 |
+
|
| 44 |
# Save configuration button
|
| 45 |
st.markdown("---")
|
| 46 |
col1, col2, col3 = st.columns([1, 1, 1])
|
|
|
|
| 48 |
if st.button("πΎ Save Settings", type="primary", use_container_width=True):
|
| 49 |
config = save_configuration()
|
| 50 |
st.success("β
Settings saved successfully!")
|
| 51 |
+
# Trigger demand validation after saving settings
|
| 52 |
+
st.session_state.show_validation_after_save = True
|
| 53 |
|
| 54 |
# Display settings summary at full width (outside columns)
|
| 55 |
st.markdown("---")
|
|
|
|
| 57 |
with st.expander("π Settings Summary", expanded=False):
|
| 58 |
display_user_friendly_summary(st.session_state.optimization_config)
|
| 59 |
|
| 60 |
+
# Show demand validation after saving settings
|
| 61 |
+
if st.session_state.get('show_validation_after_save', False):
|
| 62 |
+
st.markdown("---")
|
| 63 |
+
st.header("π Data Validation Results")
|
| 64 |
+
st.markdown("Analyzing your demand data to identify potential optimization issues...")
|
| 65 |
+
|
| 66 |
+
try:
|
| 67 |
+
from src.demand_validation import display_demand_validation
|
| 68 |
+
display_demand_validation()
|
| 69 |
+
|
| 70 |
+
# Show validation reminder before optimization
|
| 71 |
+
st.info("π‘ **Review validation results above before running optimization.** " +
|
| 72 |
+
"Fix any critical issues (especially missing line assignments) to improve optimization success.")
|
| 73 |
+
|
| 74 |
+
except Exception as e:
|
| 75 |
+
st.error(f"β Error in demand validation: {str(e)}")
|
| 76 |
+
st.info("π‘ You can still proceed with optimization, but data issues may cause problems.")
|
| 77 |
+
|
| 78 |
+
# Reset the flag so validation doesn't show every time
|
| 79 |
+
if st.button("β
Validation Reviewed - Continue to Optimization"):
|
| 80 |
+
st.session_state.show_validation_after_save = False
|
| 81 |
+
st.rerun()
|
| 82 |
+
|
| 83 |
# Optimization section
|
| 84 |
st.markdown("---")
|
| 85 |
st.header("π Run Optimization")
|
| 86 |
st.markdown("Once you've configured your settings, run the optimization to generate the optimal workforce schedule.")
|
| 87 |
+
st.markdown("**π‘ Tip:** Optimization automatically clears all previous cache and results to ensure fresh calculations with your current settings.")
|
| 88 |
|
| 89 |
col1, col2, col3 = st.columns([1, 1, 1])
|
| 90 |
+
with col1:
|
| 91 |
+
if st.button("π§Ή Clear Cache", use_container_width=True):
|
| 92 |
+
clear_all_cache_and_results()
|
| 93 |
with col2:
|
| 94 |
if st.button("π Optimize Schedule", type="primary", use_container_width=True):
|
| 95 |
+
# Quick validation check before optimization
|
| 96 |
+
validation_warnings = check_critical_data_issues()
|
| 97 |
+
if validation_warnings:
|
| 98 |
+
st.warning("β οΈ **Data validation warnings detected:**")
|
| 99 |
+
for warning in validation_warnings:
|
| 100 |
+
st.warning(f"β’ {warning}")
|
| 101 |
+
st.warning("**These issues may cause optimization to fail. Consider fixing them first.**")
|
| 102 |
+
|
| 103 |
+
# Give user option to proceed anyway
|
| 104 |
+
if st.button("β οΈ Proceed with Optimization Anyway", type="secondary"):
|
| 105 |
+
run_optimization()
|
| 106 |
+
else:
|
| 107 |
+
run_optimization()
|
| 108 |
+
with col3:
|
| 109 |
+
# Show status of current results
|
| 110 |
+
if 'optimization_results' in st.session_state and st.session_state.optimization_results is not None:
|
| 111 |
+
st.success("β
Results Available")
|
| 112 |
+
else:
|
| 113 |
+
st.info("β³ No Results Yet")
|
| 114 |
|
| 115 |
# Display optimization results if available
|
| 116 |
if 'optimization_results' in st.session_state and st.session_state.optimization_results is not None:
|
|
|
|
| 131 |
MAX_PARALLEL_WORKERS, COST_LIST_PER_EMP_SHIFT,
|
| 132 |
PAYMENT_MODE_CONFIG, LINE_CNT_PER_TYPE,
|
| 133 |
MAX_EMPLOYEE_PER_TYPE_ON_DAY, start_date, end_date,
|
| 134 |
+
shift_code_to_name, line_code_to_name, FIXED_MIN_UNICEF_PER_DAY
|
| 135 |
)
|
| 136 |
|
| 137 |
# Get the actual computed default values from optimization_config.py
|
| 138 |
defaults = {
|
| 139 |
# Schedule configuration - from optimization_config.py
|
| 140 |
'start_date': start_date.date() if hasattr(start_date, 'date') else start_date,
|
|
|
|
| 141 |
'schedule_type': DAILY_WEEKLY_SCHEDULE,
|
| 142 |
|
| 143 |
# Shift configuration - from optimization_config.py
|
|
|
|
| 177 |
'unicef_rate_shift_3': COST_LIST_PER_EMP_SHIFT.get("UNICEF Fixed term", {}).get(ShiftType.OVERTIME),
|
| 178 |
'humanizer_rate_shift_1': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(ShiftType.REGULAR),
|
| 179 |
'humanizer_rate_shift_2': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(ShiftType.EVENING),
|
| 180 |
+
'humanizer_rate_shift_3': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(ShiftType.OVERTIME),
|
| 181 |
+
|
| 182 |
+
# Data Selection defaults - reasonable defaults, not ALL available
|
| 183 |
+
'selected_employee_types': ["UNICEF Fixed term", "Humanizer"],
|
| 184 |
+
'selected_shifts': [1, 3], # Regular and Overtime by default, not evening
|
| 185 |
+
'selected_lines': [6, 7], # Both Long Line and Mini Load
|
| 186 |
}
|
| 187 |
|
| 188 |
except Exception as e:
|
|
|
|
| 205 |
st.session_state.start_date = st.date_input(
|
| 206 |
"Start Date",
|
| 207 |
value=st.session_state.start_date,
|
| 208 |
+
help="Exact start date to filter demand data - will only use orders that start on this specific date"
|
| 209 |
)
|
| 210 |
|
| 211 |
with col2:
|
| 212 |
+
st.info("π‘ **Date Filtering**: System will use only demand data that starts on the exact date you select.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
# Schedule type
|
| 215 |
+
st.subheader("π
Scheduling Options")
|
| 216 |
st.session_state.schedule_type = st.selectbox(
|
| 217 |
"Schedule Type",
|
| 218 |
options=['daily', 'weekly'],
|
|
|
|
| 561 |
def save_configuration():
|
| 562 |
"""Save current configuration to session state and potentially to file"""
|
| 563 |
|
| 564 |
+
# Get available demand dates from data to determine the actual date range
|
| 565 |
+
try:
|
| 566 |
+
sys.path.append('src')
|
| 567 |
+
import src.etl.extract as extract
|
| 568 |
+
import pandas as pd
|
| 569 |
+
from datetime import datetime
|
| 570 |
+
|
| 571 |
+
# Get data for the exact start date only
|
| 572 |
+
start_datetime = datetime.combine(st.session_state.start_date, datetime.min.time())
|
| 573 |
+
demand_data = extract.read_orders_data(start_date=start_datetime)
|
| 574 |
+
|
| 575 |
+
if not demand_data.empty:
|
| 576 |
+
# Get the unique finish dates for this start date
|
| 577 |
+
finish_dates = pd.to_datetime(demand_data["Basic finish date"]).dt.date.unique()
|
| 578 |
+
finish_dates = sorted(finish_dates)
|
| 579 |
+
|
| 580 |
+
if finish_dates:
|
| 581 |
+
calculated_end_date = max(finish_dates)
|
| 582 |
+
calculated_days = (calculated_end_date - st.session_state.start_date).days + 1
|
| 583 |
+
st.info(f"π Found demand data starting on {st.session_state.start_date} ending on {calculated_end_date} ({calculated_days} days, {len(demand_data)} orders)")
|
| 584 |
+
else:
|
| 585 |
+
calculated_end_date = st.session_state.start_date
|
| 586 |
+
calculated_days = 1
|
| 587 |
+
else:
|
| 588 |
+
calculated_end_date = st.session_state.start_date
|
| 589 |
+
calculated_days = 1
|
| 590 |
+
st.warning(f"β οΈ No demand data found for start date {st.session_state.start_date}")
|
| 591 |
+
except Exception as e:
|
| 592 |
+
st.warning(f"Could not determine date range from data: {e}. Using default 5-day period.")
|
| 593 |
+
from datetime import timedelta
|
| 594 |
+
calculated_end_date = st.session_state.start_date + timedelta(days=4)
|
| 595 |
+
calculated_days = 5
|
| 596 |
+
|
| 597 |
+
# Store calculated values in session state for compatibility
|
| 598 |
+
st.session_state.end_date = calculated_end_date
|
| 599 |
+
st.session_state.planning_days = calculated_days
|
| 600 |
+
|
| 601 |
# Create comprehensive configuration dictionary
|
| 602 |
config = {
|
| 603 |
'date_range': {
|
| 604 |
'start_date': st.session_state.start_date,
|
| 605 |
+
'end_date': calculated_end_date,
|
| 606 |
+
'planning_days': calculated_days,
|
| 607 |
},
|
| 608 |
'schedule_type': st.session_state.schedule_type,
|
| 609 |
'evening_shift_mode': st.session_state.evening_shift_mode,
|
|
|
|
| 656 |
}
|
| 657 |
}
|
| 658 |
|
| 659 |
+
# Calculate date span for proper employee limits (use calculated planning_days)
|
| 660 |
+
date_span_length = calculated_days
|
| 661 |
+
date_span = list(range(1, date_span_length + 1))
|
| 662 |
+
|
| 663 |
# Store individual items in session state for optimization_config.py to access
|
| 664 |
st.session_state.line_counts = config['operations']['line_counts']
|
| 665 |
st.session_state.cost_list_per_emp_shift = config['cost_rates']
|
| 666 |
st.session_state.payment_mode_config = config['payment_mode_config']
|
| 667 |
st.session_state.max_employee_per_type_on_day = {
|
| 668 |
+
"UNICEF Fixed term": {t: st.session_state.max_unicef_per_day for t in date_span},
|
| 669 |
+
"Humanizer": {t: st.session_state.max_humanizer_per_day for t in date_span}
|
| 670 |
}
|
| 671 |
|
| 672 |
# Store complete configuration
|
|
|
|
| 684 |
with col1:
|
| 685 |
st.write(f"**Start Date:** {config['date_range']['start_date']}")
|
| 686 |
with col2:
|
| 687 |
+
st.write(f"**Planning Period:** {config['date_range']['planning_days']} days")
|
| 688 |
with col3:
|
| 689 |
+
st.write(f"**End Date:** {config['date_range']['end_date']}")
|
| 690 |
with col4:
|
| 691 |
+
st.write(f"**Schedule Type:** {config['schedule_type'].title()}")
|
| 692 |
+
|
| 693 |
+
# Add evening shift mode in new row
|
| 694 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 695 |
+
with col1:
|
| 696 |
st.write(f"**Evening Shift Mode:** {config['evening_shift_mode'].replace('_', ' ').title()}")
|
| 697 |
|
| 698 |
# Show additional schedule details if evening shift threshold is relevant
|
| 699 |
if config['evening_shift_mode'] == 'activate_evening':
|
| 700 |
+
with col2:
|
| 701 |
+
st.write(f"**Evening Shift Threshold:** {config['evening_shift_threshold']:.0%} demand capacity")
|
| 702 |
|
| 703 |
# Workforce Settings
|
| 704 |
st.subheader("π₯ Workforce Settings")
|
|
|
|
| 788 |
col1, col2, col3, col4 = st.columns(4)
|
| 789 |
|
| 790 |
with col1:
|
| 791 |
+
duration = config['date_range']['planning_days']
|
| 792 |
st.metric("Planning Period", f"{duration} days")
|
| 793 |
|
| 794 |
with col2:
|
|
|
|
| 802 |
with col4:
|
| 803 |
avg_unicef_rate = sum(config['cost_rates']['UNICEF Fixed term'].values()) / 3
|
| 804 |
st.metric("Avg UNICEF Rate", f"β¬{avg_unicef_rate:.2f}/hr")
|
| 805 |
+
|
| 806 |
+
# Demand Data Preview
|
| 807 |
+
st.subheader("π Demand Data Preview")
|
| 808 |
+
try:
|
| 809 |
+
sys.path.append('src')
|
| 810 |
+
import src.etl.extract as extract
|
| 811 |
+
from datetime import datetime
|
| 812 |
+
|
| 813 |
+
# Get demand data for the configured start date
|
| 814 |
+
start_date = config['date_range']['start_date']
|
| 815 |
+
start_datetime = datetime.combine(start_date, datetime.min.time())
|
| 816 |
+
demand_data = extract.read_orders_data(start_date=start_datetime)
|
| 817 |
+
|
| 818 |
+
if not demand_data.empty:
|
| 819 |
+
# Show summary statistics
|
| 820 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 821 |
+
|
| 822 |
+
with col1:
|
| 823 |
+
st.metric("π¦ Total Orders", len(demand_data))
|
| 824 |
+
|
| 825 |
+
with col2:
|
| 826 |
+
total_quantity = demand_data['Order quantity (GMEIN)'].sum()
|
| 827 |
+
st.metric("π Total Quantity", total_quantity)
|
| 828 |
+
|
| 829 |
+
with col3:
|
| 830 |
+
unique_products = demand_data['Material Number'].nunique()
|
| 831 |
+
st.metric("π·οΈ Unique Products", unique_products)
|
| 832 |
+
|
| 833 |
+
with col4:
|
| 834 |
+
# Calculate date range
|
| 835 |
+
if 'Basic finish date' in demand_data.columns:
|
| 836 |
+
import pandas as pd
|
| 837 |
+
finish_dates = pd.to_datetime(demand_data['Basic finish date']).dt.date
|
| 838 |
+
min_finish = finish_dates.min()
|
| 839 |
+
max_finish = finish_dates.max()
|
| 840 |
+
if min_finish == max_finish:
|
| 841 |
+
date_range = f"{min_finish}"
|
| 842 |
+
else:
|
| 843 |
+
date_range = f"{min_finish} to {max_finish}"
|
| 844 |
+
else:
|
| 845 |
+
date_range = "N/A"
|
| 846 |
+
st.metric("π
Finish Dates for the start date orders", date_range)
|
| 847 |
+
|
| 848 |
+
# Show top products
|
| 849 |
+
if 'Material' in demand_data.columns and 'Quantity' in demand_data.columns:
|
| 850 |
+
with st.expander("π Top 10 Products by Quantity", expanded=False):
|
| 851 |
+
top_products = demand_data.groupby('Material')['Quantity'].sum().sort_values(ascending=False).head(10)
|
| 852 |
+
|
| 853 |
+
if not top_products.empty:
|
| 854 |
+
# Create a nice table
|
| 855 |
+
import pandas as pd
|
| 856 |
+
top_products_df = pd.DataFrame({
|
| 857 |
+
'Product': top_products.index,
|
| 858 |
+
'Total Quantity': top_products.values
|
| 859 |
+
}).reset_index(drop=True)
|
| 860 |
+
top_products_df.index = top_products_df.index + 1 # Start index from 1
|
| 861 |
+
st.dataframe(top_products_df, use_container_width=True)
|
| 862 |
+
else:
|
| 863 |
+
st.info("No product quantity data available.")
|
| 864 |
+
|
| 865 |
+
else:
|
| 866 |
+
st.warning(f"β οΈ No demand data found for {start_date}. Please select a different date or check your data files.")
|
| 867 |
+
|
| 868 |
+
except Exception as e:
|
| 869 |
+
st.error(f"β Error loading demand data: {e}")
|
| 870 |
+
|
| 871 |
+
def clear_all_cache_and_results():
|
| 872 |
+
"""Clear all cached data, modules, and results from previous runs"""
|
| 873 |
+
import importlib
|
| 874 |
+
import sys
|
| 875 |
+
|
| 876 |
+
st.info("π§Ή Clearing all cached data and previous results...")
|
| 877 |
+
|
| 878 |
+
# 1. Clear all optimization-related session state
|
| 879 |
+
keys_to_clear = [
|
| 880 |
+
'optimization_results',
|
| 881 |
+
'demand_dictionary',
|
| 882 |
+
'per_product_speed',
|
| 883 |
+
'kit_hierarchy_data',
|
| 884 |
+
'team_requirements'
|
| 885 |
+
]
|
| 886 |
+
|
| 887 |
+
cleared_keys = []
|
| 888 |
+
for key in keys_to_clear:
|
| 889 |
+
if key in st.session_state:
|
| 890 |
+
del st.session_state[key]
|
| 891 |
+
cleared_keys.append(key)
|
| 892 |
+
|
| 893 |
+
if cleared_keys:
|
| 894 |
+
st.write(f"ποΈ Cleared session state: {', '.join(cleared_keys)}")
|
| 895 |
+
|
| 896 |
+
# 2. Force reload all related modules to clear any module-level caches
|
| 897 |
+
modules_to_reload = [
|
| 898 |
+
'src.etl.extract',
|
| 899 |
+
'src.etl.transform',
|
| 900 |
+
'src.config.optimization_config',
|
| 901 |
+
'src.models.optimizer_real'
|
| 902 |
+
]
|
| 903 |
+
|
| 904 |
+
reloaded_modules = []
|
| 905 |
+
for module_name in modules_to_reload:
|
| 906 |
+
if module_name in sys.modules:
|
| 907 |
+
importlib.reload(sys.modules[module_name])
|
| 908 |
+
reloaded_modules.append(module_name)
|
| 909 |
+
|
| 910 |
+
if reloaded_modules:
|
| 911 |
+
st.write(f"π Reloaded modules: {', '.join(reloaded_modules)}")
|
| 912 |
+
|
| 913 |
+
# 3. Get end date from session state (calculated during save) and update global dates
|
| 914 |
+
if 'end_date' in st.session_state and 'planning_days' in st.session_state:
|
| 915 |
+
calculated_end_date = st.session_state.end_date
|
| 916 |
+
planning_days = st.session_state.planning_days
|
| 917 |
+
else:
|
| 918 |
+
# Fallback - use start date only for now, will be recalculated
|
| 919 |
+
from datetime import timedelta
|
| 920 |
+
calculated_end_date = st.session_state.start_date + timedelta(days=4)
|
| 921 |
+
planning_days = 5
|
| 922 |
+
|
| 923 |
+
sys.path.append('src')
|
| 924 |
+
import src.etl.extract as extract
|
| 925 |
+
extract.set_global_dates(st.session_state.start_date, calculated_end_date)
|
| 926 |
+
st.write(f"π
Updated global dates: {st.session_state.start_date} to {calculated_end_date} ({planning_days} days)")
|
| 927 |
+
|
| 928 |
+
st.success("β
All caches and previous results cleared!")
|
| 929 |
|
| 930 |
def run_optimization():
|
| 931 |
"""Run the optimization model and store results"""
|
| 932 |
try:
|
| 933 |
st.info("π Running optimization... This may take a few moments.")
|
| 934 |
|
| 935 |
+
# Always clear everything first for clean slate
|
| 936 |
+
clear_all_cache_and_results()
|
| 937 |
+
|
| 938 |
+
# Show brief validation summary
|
| 939 |
+
st.info("π Performing pre-optimization data validation...")
|
| 940 |
+
validation_warnings = check_critical_data_issues()
|
| 941 |
+
if validation_warnings:
|
| 942 |
+
with st.expander("β οΈ Data Validation Warnings", expanded=True):
|
| 943 |
+
for warning in validation_warnings:
|
| 944 |
+
st.warning(f"β’ {warning}")
|
| 945 |
+
st.warning("**Optimization may fail due to these issues. Consider fixing them first.**")
|
| 946 |
+
else:
|
| 947 |
+
st.success("β
Data validation passed - no critical issues detected")
|
| 948 |
+
|
| 949 |
+
# Show current configuration being used
|
| 950 |
+
if 'end_date' in st.session_state and 'planning_days' in st.session_state:
|
| 951 |
+
calculated_end_date = st.session_state.end_date
|
| 952 |
+
planning_days = st.session_state.planning_days
|
| 953 |
+
st.info(f"ποΈ Planning period: {st.session_state.start_date} to {calculated_end_date} ({planning_days} days)")
|
| 954 |
+
else:
|
| 955 |
+
st.info(f"ποΈ Start date: {st.session_state.start_date} (will determine end date from demand data)")
|
| 956 |
+
|
| 957 |
+
st.info(f"π₯ Max UNICEF/day: {st.session_state.max_unicef_per_day}, Max Humanizer/day: {st.session_state.max_humanizer_per_day}")
|
| 958 |
+
|
| 959 |
+
# Import and run the optimization (after clearing)
|
| 960 |
sys.path.append('src')
|
| 961 |
from models.optimizer_real import solve_fixed_team_weekly
|
| 962 |
|
|
|
|
| 966 |
|
| 967 |
if results is None:
|
| 968 |
st.error("β Optimization failed! The problem may be infeasible with current settings.")
|
| 969 |
+
if validation_warnings:
|
| 970 |
+
st.error("π‘ **Likely cause:** Data validation warnings detected above. Fix missing line assignments and other data issues first.")
|
| 971 |
st.error("Try adjusting your workforce limits, line counts, or evening shift settings.")
|
| 972 |
return
|
| 973 |
|
|
|
|
| 980 |
st.error(f"β Error during optimization: {str(e)}")
|
| 981 |
st.error("Please check your settings and data files.")
|
| 982 |
|
| 983 |
+
def check_critical_data_issues():
|
| 984 |
+
"""
|
| 985 |
+
Quick check for critical data issues that could cause optimization failure
|
| 986 |
+
Returns list of warning messages
|
| 987 |
+
"""
|
| 988 |
+
warnings = []
|
| 989 |
+
|
| 990 |
+
try:
|
| 991 |
+
# Add src to path if needed
|
| 992 |
+
import sys
|
| 993 |
+
import os
|
| 994 |
+
src_path = os.path.join(os.path.dirname(__file__), 'src')
|
| 995 |
+
if src_path not in sys.path:
|
| 996 |
+
sys.path.append(src_path)
|
| 997 |
+
|
| 998 |
+
from src.demand_validation import DemandValidator
|
| 999 |
+
|
| 1000 |
+
# Initialize validator and load data
|
| 1001 |
+
validator = DemandValidator()
|
| 1002 |
+
if not validator.load_data():
|
| 1003 |
+
warnings.append("Failed to load validation data")
|
| 1004 |
+
return warnings
|
| 1005 |
+
|
| 1006 |
+
# Quick validation check
|
| 1007 |
+
validation_df = validator.validate_all_products()
|
| 1008 |
+
summary_stats = validator.get_summary_statistics(validation_df)
|
| 1009 |
+
|
| 1010 |
+
# Check for critical issues
|
| 1011 |
+
if summary_stats['no_line_assignment'] > 0:
|
| 1012 |
+
warnings.append(f"{summary_stats['no_line_assignment']} products missing line assignments")
|
| 1013 |
+
|
| 1014 |
+
if summary_stats['no_staffing'] > 0:
|
| 1015 |
+
warnings.append(f"{summary_stats['no_staffing']} products missing staffing requirements")
|
| 1016 |
+
|
| 1017 |
+
if summary_stats['no_speed'] > 0:
|
| 1018 |
+
warnings.append(f"{summary_stats['no_speed']} products missing production speed data")
|
| 1019 |
+
|
| 1020 |
+
# Calculate failure risk
|
| 1021 |
+
invalid_ratio = summary_stats['invalid_products'] / summary_stats['total_products']
|
| 1022 |
+
if invalid_ratio > 0.5:
|
| 1023 |
+
warnings.append(f"High failure risk: {invalid_ratio:.0%} of products have data issues")
|
| 1024 |
+
|
| 1025 |
+
return warnings
|
| 1026 |
+
|
| 1027 |
+
except Exception as e:
|
| 1028 |
+
warnings.append(f"Validation check failed: {str(e)}")
|
| 1029 |
+
return warnings
|
| 1030 |
+
|
| 1031 |
def display_optimization_results(results):
|
| 1032 |
"""Import and display optimization results"""
|
| 1033 |
from optimization_results import display_optimization_results as display_results
|
optimization_results.py
CHANGED
|
@@ -45,12 +45,14 @@ def display_optimization_results(results):
|
|
| 45 |
st.header("π Optimization Results")
|
| 46 |
|
| 47 |
# Create tabs for different views
|
| 48 |
-
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
| 49 |
"π Weekly Summary",
|
| 50 |
"π
Daily Deep Dive",
|
| 51 |
"π Line Schedules",
|
| 52 |
"π¦ Kit Production",
|
| 53 |
-
"π° Cost Analysis"
|
|
|
|
|
|
|
| 54 |
])
|
| 55 |
|
| 56 |
with tab1:
|
|
@@ -67,6 +69,12 @@ def display_optimization_results(results):
|
|
| 67 |
|
| 68 |
with tab5:
|
| 69 |
display_cost_analysis(results)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
def display_weekly_summary(results):
|
| 72 |
"""Display weekly summary with key metrics and charts"""
|
|
@@ -213,11 +221,10 @@ def display_daily_deep_dive(results):
|
|
| 213 |
try:
|
| 214 |
from src.config.optimization_config import MAX_EMPLOYEE_PER_TYPE_ON_DAY
|
| 215 |
|
| 216 |
-
# Add capacity columns
|
| 217 |
for emp_type in ['UNICEF Fixed term', 'Humanizer']:
|
| 218 |
if emp_type in summary_pivot.columns:
|
| 219 |
capacity_col = f'{emp_type} Capacity'
|
| 220 |
-
utilization_col = f'{emp_type} Utilization %'
|
| 221 |
|
| 222 |
# Extract day number from 'Day X' format
|
| 223 |
summary_pivot['Day_Num'] = summary_pivot['Day'].str.extract(r'(\d+)').astype(int)
|
|
@@ -226,15 +233,6 @@ def display_daily_deep_dive(results):
|
|
| 226 |
summary_pivot[capacity_col] = summary_pivot['Day_Num'].apply(
|
| 227 |
lambda day: MAX_EMPLOYEE_PER_TYPE_ON_DAY.get(emp_type, {}).get(day, 0)
|
| 228 |
)
|
| 229 |
-
|
| 230 |
-
# Calculate utilization percentage
|
| 231 |
-
summary_pivot[utilization_col] = (
|
| 232 |
-
summary_pivot[emp_type] / summary_pivot[capacity_col] * 100
|
| 233 |
-
).round(1)
|
| 234 |
-
|
| 235 |
-
# Replace inf and NaN with 0
|
| 236 |
-
summary_pivot[utilization_col] = summary_pivot[utilization_col].fillna(0)
|
| 237 |
-
summary_pivot.loc[summary_pivot[capacity_col] == 0, utilization_col] = 0
|
| 238 |
|
| 239 |
# Drop temporary column
|
| 240 |
summary_pivot = summary_pivot.drop('Day_Num', axis=1)
|
|
@@ -681,4 +679,279 @@ def display_cost_analysis(results):
|
|
| 681 |
df_costs_with_total = pd.concat([df_costs, total_row], ignore_index=True)
|
| 682 |
|
| 683 |
st.subheader("π Detailed Cost Breakdown")
|
| 684 |
-
st.dataframe(df_costs_with_total, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
st.header("π Optimization Results")
|
| 46 |
|
| 47 |
# Create tabs for different views
|
| 48 |
+
tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs([
|
| 49 |
"π Weekly Summary",
|
| 50 |
"π
Daily Deep Dive",
|
| 51 |
"π Line Schedules",
|
| 52 |
"π¦ Kit Production",
|
| 53 |
+
"π° Cost Analysis",
|
| 54 |
+
"π Input Data",
|
| 55 |
+
"π Demand Validation"
|
| 56 |
])
|
| 57 |
|
| 58 |
with tab1:
|
|
|
|
| 69 |
|
| 70 |
with tab5:
|
| 71 |
display_cost_analysis(results)
|
| 72 |
+
|
| 73 |
+
with tab6:
|
| 74 |
+
display_input_data_inspection()
|
| 75 |
+
|
| 76 |
+
with tab7:
|
| 77 |
+
display_demand_validation_tab()
|
| 78 |
|
| 79 |
def display_weekly_summary(results):
|
| 80 |
"""Display weekly summary with key metrics and charts"""
|
|
|
|
| 221 |
try:
|
| 222 |
from src.config.optimization_config import MAX_EMPLOYEE_PER_TYPE_ON_DAY
|
| 223 |
|
| 224 |
+
# Add capacity columns (removed utilization percentage)
|
| 225 |
for emp_type in ['UNICEF Fixed term', 'Humanizer']:
|
| 226 |
if emp_type in summary_pivot.columns:
|
| 227 |
capacity_col = f'{emp_type} Capacity'
|
|
|
|
| 228 |
|
| 229 |
# Extract day number from 'Day X' format
|
| 230 |
summary_pivot['Day_Num'] = summary_pivot['Day'].str.extract(r'(\d+)').astype(int)
|
|
|
|
| 233 |
summary_pivot[capacity_col] = summary_pivot['Day_Num'].apply(
|
| 234 |
lambda day: MAX_EMPLOYEE_PER_TYPE_ON_DAY.get(emp_type, {}).get(day, 0)
|
| 235 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
# Drop temporary column
|
| 238 |
summary_pivot = summary_pivot.drop('Day_Num', axis=1)
|
|
|
|
| 679 |
df_costs_with_total = pd.concat([df_costs, total_row], ignore_index=True)
|
| 680 |
|
| 681 |
st.subheader("π Detailed Cost Breakdown")
|
| 682 |
+
st.dataframe(df_costs_with_total, use_container_width=True)
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
def display_input_data_inspection():
|
| 686 |
+
"""
|
| 687 |
+
Display comprehensive input data inspection showing what was fed into the optimizer
|
| 688 |
+
"""
|
| 689 |
+
st.subheader("π Input Data Inspection")
|
| 690 |
+
st.markdown("This section shows all the input data and parameters that were fed into the optimization model.")
|
| 691 |
+
|
| 692 |
+
# Import the optimization config to get current values
|
| 693 |
+
try:
|
| 694 |
+
from src.config import optimization_config
|
| 695 |
+
from src.config.constants import ShiftType, LineType, KitLevel
|
| 696 |
+
|
| 697 |
+
# Create expandable sections for different data categories
|
| 698 |
+
with st.expander("π
**Schedule & Time Parameters**", expanded=True):
|
| 699 |
+
col1, col2 = st.columns(2)
|
| 700 |
+
|
| 701 |
+
with col1:
|
| 702 |
+
st.write("**Date Range:**")
|
| 703 |
+
date_span = optimization_config.get_date_span()
|
| 704 |
+
st.write(f"β’ Planning Period: {len(date_span)} days")
|
| 705 |
+
st.write(f"β’ Date Span: {list(date_span)}")
|
| 706 |
+
|
| 707 |
+
st.write("**Shift Configuration:**")
|
| 708 |
+
shift_list = optimization_config.get_shift_list()
|
| 709 |
+
for shift in shift_list:
|
| 710 |
+
shift_name = ShiftType.get_name(shift)
|
| 711 |
+
st.write(f"β’ {shift_name} (ID: {shift})")
|
| 712 |
+
|
| 713 |
+
with col2:
|
| 714 |
+
st.write("**Work Hours Configuration:**")
|
| 715 |
+
max_hours_shift = optimization_config.get_max_hour_per_shift_per_person()
|
| 716 |
+
for shift_id, hours in max_hours_shift.items():
|
| 717 |
+
shift_name = ShiftType.get_name(shift_id)
|
| 718 |
+
st.write(f"β’ {shift_name}: {hours} hours/shift")
|
| 719 |
+
|
| 720 |
+
max_daily_hours = optimization_config.get_max_hour_per_person_per_day()
|
| 721 |
+
st.write(f"β’ Maximum daily hours per person: {max_daily_hours}")
|
| 722 |
+
|
| 723 |
+
with st.expander("π₯ **Workforce Parameters**", expanded=False):
|
| 724 |
+
col1, col2 = st.columns(2)
|
| 725 |
+
|
| 726 |
+
with col1:
|
| 727 |
+
st.write("**Employee Types:**")
|
| 728 |
+
emp_types = optimization_config.get_employee_type_list()
|
| 729 |
+
for emp_type in emp_types:
|
| 730 |
+
st.write(f"β’ {emp_type}")
|
| 731 |
+
|
| 732 |
+
st.write("**Daily Workforce Capacity:**")
|
| 733 |
+
max_emp_per_day = optimization_config.get_max_employee_per_type_on_day()
|
| 734 |
+
for emp_type, daily_caps in max_emp_per_day.items():
|
| 735 |
+
st.write(f"**{emp_type}:**")
|
| 736 |
+
for day, count in daily_caps.items():
|
| 737 |
+
st.write(f" - Day {day}: {count} employees")
|
| 738 |
+
|
| 739 |
+
with col2:
|
| 740 |
+
st.write("**Team Requirements per Product:**")
|
| 741 |
+
team_req = optimization_config.get_team_req_per_product()
|
| 742 |
+
st.write("*Sample products:*")
|
| 743 |
+
# Show first few products as examples
|
| 744 |
+
sample_products = list(team_req.get('UNICEF Fixed term', {}).keys())[:5]
|
| 745 |
+
for product in sample_products:
|
| 746 |
+
st.write(f"**{product}:**")
|
| 747 |
+
for emp_type in emp_types:
|
| 748 |
+
req = team_req.get(emp_type, {}).get(product, 0)
|
| 749 |
+
if req > 0:
|
| 750 |
+
st.write(f" - {emp_type}: {req}")
|
| 751 |
+
|
| 752 |
+
if len(team_req.get('UNICEF Fixed term', {})) > 5:
|
| 753 |
+
remaining = len(team_req.get('UNICEF Fixed term', {})) - 5
|
| 754 |
+
st.write(f"... and {remaining} more products")
|
| 755 |
+
|
| 756 |
+
with st.expander("π **Production & Line Parameters**", expanded=False):
|
| 757 |
+
col1, col2 = st.columns(2)
|
| 758 |
+
|
| 759 |
+
with col1:
|
| 760 |
+
st.write("**Line Configuration:**")
|
| 761 |
+
line_list = optimization_config.get_line_list()
|
| 762 |
+
line_cnt = optimization_config.get_line_cnt_per_type()
|
| 763 |
+
|
| 764 |
+
for line_type in line_list:
|
| 765 |
+
line_name = LineType.get_name(line_type)
|
| 766 |
+
count = line_cnt.get(line_type, 0)
|
| 767 |
+
st.write(f"β’ {line_name} (ID: {line_type}): {count} lines")
|
| 768 |
+
|
| 769 |
+
st.write("**Maximum Workers per Line:**")
|
| 770 |
+
max_workers = optimization_config.get_max_parallel_workers()
|
| 771 |
+
for line_type, max_count in max_workers.items():
|
| 772 |
+
line_name = LineType.get_name(line_type)
|
| 773 |
+
st.write(f"β’ {line_name}: {max_count} workers max")
|
| 774 |
+
|
| 775 |
+
with col2:
|
| 776 |
+
st.write("**Product-Line Matching:**")
|
| 777 |
+
kit_line_match = optimization_config.get_kit_line_match_dict()
|
| 778 |
+
st.write("*Sample mappings:*")
|
| 779 |
+
sample_items = list(kit_line_match.items())[:10]
|
| 780 |
+
for product, line_type in sample_items:
|
| 781 |
+
line_name = LineType.get_name(line_type)
|
| 782 |
+
st.write(f"β’ {product}: {line_name}")
|
| 783 |
+
|
| 784 |
+
if len(kit_line_match) > 10:
|
| 785 |
+
remaining = len(kit_line_match) - 10
|
| 786 |
+
st.write(f"... and {remaining} more product mappings")
|
| 787 |
+
|
| 788 |
+
with st.expander("π¦ **Product & Demand Data**", expanded=False):
|
| 789 |
+
col1, col2 = st.columns(2)
|
| 790 |
+
|
| 791 |
+
with col1:
|
| 792 |
+
st.write("**Product List:**")
|
| 793 |
+
product_list = optimization_config.get_product_list()
|
| 794 |
+
st.write(f"β’ Total products: {len(product_list)}")
|
| 795 |
+
st.write("*Sample products:*")
|
| 796 |
+
for product in product_list[:10]:
|
| 797 |
+
st.write(f" - {product}")
|
| 798 |
+
if len(product_list) > 10:
|
| 799 |
+
st.write(f" ... and {len(product_list) - 10} more")
|
| 800 |
+
|
| 801 |
+
st.write("**Production Speed (units/hour):**")
|
| 802 |
+
speed_data = optimization_config.get_per_product_speed()
|
| 803 |
+
st.write("*Sample speeds:*")
|
| 804 |
+
sample_speeds = list(speed_data.items())[:5]
|
| 805 |
+
for product, speed in sample_speeds:
|
| 806 |
+
st.write(f"β’ {product}: {speed:.1f} units/hour")
|
| 807 |
+
if len(speed_data) > 5:
|
| 808 |
+
remaining = len(speed_data) - 5
|
| 809 |
+
st.write(f"... and {remaining} more products")
|
| 810 |
+
|
| 811 |
+
with col2:
|
| 812 |
+
st.write("**Weekly Demand:**")
|
| 813 |
+
demand_dict = optimization_config.get_demand_dictionary()
|
| 814 |
+
st.write(f"β’ Total products with demand: {len(demand_dict)}")
|
| 815 |
+
|
| 816 |
+
# Calculate total demand
|
| 817 |
+
total_demand = sum(demand_dict.values())
|
| 818 |
+
st.write(f"β’ Total weekly demand: {total_demand:,.0f} units")
|
| 819 |
+
|
| 820 |
+
st.write("*Sample demands:*")
|
| 821 |
+
# Sort by demand to show highest first
|
| 822 |
+
sorted_demands = sorted(demand_dict.items(), key=lambda x: x[1], reverse=True)[:10]
|
| 823 |
+
for product, demand in sorted_demands:
|
| 824 |
+
st.write(f"β’ {product}: {demand:,.0f} units")
|
| 825 |
+
|
| 826 |
+
if len(demand_dict) > 10:
|
| 827 |
+
remaining = len(demand_dict) - 10
|
| 828 |
+
st.write(f"... and {remaining} more products")
|
| 829 |
+
|
| 830 |
+
with st.expander("ποΈ **Kit Hierarchy & Dependencies**", expanded=False):
|
| 831 |
+
col1, col2 = st.columns(2)
|
| 832 |
+
|
| 833 |
+
with col1:
|
| 834 |
+
st.write("**Kit Levels:**")
|
| 835 |
+
kit_levels = optimization_config.get_kit_levels()
|
| 836 |
+
|
| 837 |
+
# Count by level
|
| 838 |
+
level_counts = {}
|
| 839 |
+
for kit, level in kit_levels.items():
|
| 840 |
+
level_name = KitLevel.get_name(level)
|
| 841 |
+
if level_name not in level_counts:
|
| 842 |
+
level_counts[level_name] = 0
|
| 843 |
+
level_counts[level_name] += 1
|
| 844 |
+
|
| 845 |
+
for level_name, count in level_counts.items():
|
| 846 |
+
st.write(f"β’ {level_name}: {count} kits")
|
| 847 |
+
|
| 848 |
+
st.write("*Sample kit levels:*")
|
| 849 |
+
sample_levels = list(kit_levels.items())[:10]
|
| 850 |
+
for kit, level in sample_levels:
|
| 851 |
+
level_name = KitLevel.get_name(level)
|
| 852 |
+
st.write(f" - {kit}: {level_name}")
|
| 853 |
+
|
| 854 |
+
if len(kit_levels) > 10:
|
| 855 |
+
remaining = len(kit_levels) - 10
|
| 856 |
+
st.write(f" ... and {remaining} more kits")
|
| 857 |
+
|
| 858 |
+
with col2:
|
| 859 |
+
st.write("**Dependencies:**")
|
| 860 |
+
kit_deps = optimization_config.get_kit_dependencies()
|
| 861 |
+
|
| 862 |
+
# Count dependencies
|
| 863 |
+
total_deps = sum(len(deps) for deps in kit_deps.values())
|
| 864 |
+
kits_with_deps = len([k for k, deps in kit_deps.items() if deps])
|
| 865 |
+
|
| 866 |
+
st.write(f"β’ Total dependency relationships: {total_deps}")
|
| 867 |
+
st.write(f"β’ Kits with dependencies: {kits_with_deps}")
|
| 868 |
+
|
| 869 |
+
st.write("*Sample dependencies:*")
|
| 870 |
+
sample_deps = [(k, deps) for k, deps in kit_deps.items() if deps][:5]
|
| 871 |
+
for kit, deps in sample_deps:
|
| 872 |
+
st.write(f"β’ {kit}:")
|
| 873 |
+
for dep in deps[:3]: # Show max 3 deps per kit
|
| 874 |
+
st.write(f" - depends on: {dep}")
|
| 875 |
+
if len(deps) > 3:
|
| 876 |
+
st.write(f" - ... and {len(deps) - 3} more")
|
| 877 |
+
|
| 878 |
+
if len(sample_deps) > 5:
|
| 879 |
+
remaining = len([k for k, deps in kit_deps.items() if deps]) - 5
|
| 880 |
+
st.write(f"... and {remaining} more kits with dependencies")
|
| 881 |
+
|
| 882 |
+
with st.expander("π° **Cost & Payment Configuration**", expanded=False):
|
| 883 |
+
col1, col2 = st.columns(2)
|
| 884 |
+
|
| 885 |
+
with col1:
|
| 886 |
+
st.write("**Hourly Cost Rates:**")
|
| 887 |
+
cost_rates = optimization_config.get_cost_list_per_emp_shift()
|
| 888 |
+
|
| 889 |
+
for emp_type, shift_costs in cost_rates.items():
|
| 890 |
+
st.write(f"**{emp_type}:**")
|
| 891 |
+
for shift_id, cost in shift_costs.items():
|
| 892 |
+
shift_name = ShiftType.get_name(shift_id)
|
| 893 |
+
st.write(f" - {shift_name}: β¬{cost:.2f}/hour")
|
| 894 |
+
|
| 895 |
+
with col2:
|
| 896 |
+
st.write("**Payment Mode Configuration:**")
|
| 897 |
+
payment_config = optimization_config.get_payment_mode_config()
|
| 898 |
+
|
| 899 |
+
payment_descriptions = {
|
| 900 |
+
'bulk': 'Full shift payment (even for partial hours)',
|
| 901 |
+
'partial': 'Pay only for actual hours worked'
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
for shift_id, mode in payment_config.items():
|
| 905 |
+
shift_name = ShiftType.get_name(shift_id)
|
| 906 |
+
description = payment_descriptions.get(mode, mode)
|
| 907 |
+
st.write(f"β’ **{shift_name}:** {mode.title()}")
|
| 908 |
+
st.caption(f" {description}")
|
| 909 |
+
|
| 910 |
+
with st.expander("βοΈ **Additional Configuration**", expanded=False):
|
| 911 |
+
col1, col2 = st.columns(2)
|
| 912 |
+
|
| 913 |
+
with col1:
|
| 914 |
+
st.write("**Schedule Mode:**")
|
| 915 |
+
schedule_mode = optimization_config.get_daily_weekly_schedule()
|
| 916 |
+
st.write(f"β’ Planning mode: {schedule_mode}")
|
| 917 |
+
|
| 918 |
+
st.write("**Evening Shift Mode:**")
|
| 919 |
+
evening_mode = optimization_config.get_evening_shift_mode()
|
| 920 |
+
evening_threshold = optimization_config.get_evening_shift_demand_threshold()
|
| 921 |
+
st.write(f"β’ Mode: {evening_mode}")
|
| 922 |
+
st.write(f"β’ Activation threshold: {evening_threshold:.1%}")
|
| 923 |
+
|
| 924 |
+
with col2:
|
| 925 |
+
st.write("**Fixed Staffing:**")
|
| 926 |
+
fixed_min_unicef = optimization_config.get_fixed_min_unicef_per_day()
|
| 927 |
+
st.write(f"β’ Minimum UNICEF staff per day: {fixed_min_unicef}")
|
| 928 |
+
|
| 929 |
+
st.write("**Data Sources:**")
|
| 930 |
+
st.write("β’ Kit hierarchy: kit_hierarchy.json")
|
| 931 |
+
st.write("β’ Production orders: CSV files")
|
| 932 |
+
st.write("β’ Personnel data: WH_Workforce CSV")
|
| 933 |
+
st.write("β’ Speed data: Kits_Calculation CSV")
|
| 934 |
+
|
| 935 |
+
except Exception as e:
|
| 936 |
+
st.error(f"β Error loading input data inspection: {str(e)}")
|
| 937 |
+
st.info("π‘ This may happen if the optimization configuration is not properly loaded. Please check the Settings page first.")
|
| 938 |
+
|
| 939 |
+
# Add refresh button
|
| 940 |
+
st.markdown("---")
|
| 941 |
+
if st.button("π Refresh Input Data", help="Reload the current configuration data"):
|
| 942 |
+
st.rerun()
|
| 943 |
+
|
| 944 |
+
|
| 945 |
+
def display_demand_validation_tab():
|
| 946 |
+
"""
|
| 947 |
+
Display demand validation in the optimization results tab
|
| 948 |
+
"""
|
| 949 |
+
try:
|
| 950 |
+
from src.demand_validation import display_demand_validation
|
| 951 |
+
display_demand_validation()
|
| 952 |
+
except ImportError as e:
|
| 953 |
+
st.error(f"β Error loading demand validation module: {str(e)}")
|
| 954 |
+
st.info("π‘ Please ensure the demand validation module is properly installed.")
|
| 955 |
+
except Exception as e:
|
| 956 |
+
st.error(f"β Error in demand validation: {str(e)}")
|
| 957 |
+
st.info("π‘ Please check the data files and configuration.")
|