supply-roster-optimization / ui /pages /config_page.py
haileyhalimj@gmail.com
Refactor optimization configuration and constants integration
fa2c20f
"""
Configuration Page for Supply Roster Optimization Tool
Contains all user input controls with default values from config
"""
import streamlit as st
import datetime
import sys
import os
from src.config import optimization_config
from src.config.constants import ShiftType, LineType, DefaultConfig
def clear_optimization_cache():
"""Clear all cached optimization results and validation data"""
# Clear any previous optimization results since settings changed
if 'optimization_results' in st.session_state:
del st.session_state.optimization_results
# Clear any previous validation state
if 'show_validation_after_save' in st.session_state:
del st.session_state.show_validation_after_save
if 'settings_just_saved' in st.session_state:
del st.session_state.settings_just_saved
# Clear any cached validation results since settings changed
validation_cache_key = f"validation_results_{st.session_state.get('start_date', 'default')}"
if validation_cache_key in st.session_state:
del st.session_state[validation_cache_key]
def render_config_page():
"""Render the configuration page with all user input controls"""
st.title("βš™οΈ Settings")
st.markdown("---")
st.markdown("Adjust the settings for your workforce optimization. These settings control how the system schedules employees and calculates costs.")
# Initialize session state for all configuration values
initialize_session_state()
# Create tabs for better organization
tab1, tab2, tab3, tab4, tab5 = st.tabs(["πŸ“… Schedule", "πŸ‘₯ Workforce", "🏭 Operations", "πŸ’° Cost", "πŸ“Š Data Selection"])
with tab1:
render_schedule_config()
with tab2:
render_workforce_config()
with tab3:
render_operations_config()
with tab4:
render_cost_config()
with tab5:
render_data_selection_config()
# Save configuration button
st.markdown("---")
col1, col2, col3 = st.columns([1, 1, 1])
with col2:
if st.button("πŸ’Ύ Save Settings", type="primary", use_container_width=True):
config = save_configuration()
st.success("βœ… Settings saved successfully!")
# Clear all cached optimization and validation data
clear_optimization_cache()
# Trigger fresh demand validation after saving settings
st.session_state.show_validation_after_save = True
st.session_state.settings_just_saved = True
# Force a page refresh to show the updated validation
st.rerun()
# Display settings summary at full width (outside columns)
st.markdown("---")
if 'optimization_config' in st.session_state:
with st.expander("πŸ“‹ Settings Summary", expanded=False):
display_user_friendly_summary(st.session_state.optimization_config)
# Show demand validation after saving settings
if st.session_state.get('show_validation_after_save', False) or st.session_state.get('settings_just_saved', False):
st.markdown("---")
st.header("πŸ“‹ Data Validation Results")
st.markdown("Analyzing your demand data to identify potential optimization issues...")
# Check if we have cached validation results
validation_cache_key = f"validation_results_{st.session_state.get('start_date', 'default')}"
if validation_cache_key not in st.session_state:
# Run validation and cache results
with st.spinner("πŸ”„ Running data validation..."):
try:
from src.demand_validation_viz import DemandValidationViz
# Initialize validator and run validation
validator = DemandValidationViz()
print("validator",validator)
if validator.load_data():
validation_df = validator.validate_all_products()
print("validation_df", validation_df)
summary_stats = validator.get_summary_statistics(validation_df)
print("summary_stats", summary_stats)
# Cache the results
st.session_state[validation_cache_key] = {
'validation_df': validation_df,
'summary_stats': summary_stats,
'validator': validator
}
else:
st.error("❌ Failed to load validation data")
st.session_state[validation_cache_key] = None
except Exception as e:
st.error(f"❌ Error in demand validation: {str(e)}")
st.session_state[validation_cache_key] = None
# Display cached validation results
if st.session_state.get(validation_cache_key):
# print("cached_results",st.session_state[validation_cache_key])
cached_results = st.session_state[validation_cache_key]
validation_df = cached_results['validation_df']
# print("validation_df",validation_df)
summary_stats = cached_results['summary_stats']
# print("summary_stats",summary_stats)
# Display summary statistics
st.subheader("πŸ“Š Summary Statistics")
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total Products", summary_stats['total_products'])
st.metric("Included in Optimization", summary_stats['included_products'], delta="Ready for optimization")
with col2:
st.metric("Total Demand", f"{summary_stats['total_demand']:,}")
st.metric("Excluded from Optimization", summary_stats['excluded_products'], delta="Omitted")
with col3:
st.metric("Included Demand", f"{summary_stats['included_demand']:,}", delta="Will be optimized")
st.metric("UNICEF Staff Needed", summary_stats['total_unicef_needed'])
with col4:
st.metric("Excluded Demand", f"{summary_stats['excluded_demand']:,}", delta="Omitted")
st.metric("Humanizer Staff Needed", summary_stats['total_humanizer_needed'])
# Separate the results into included and excluded
included_df = validation_df[validation_df['Excluded from Optimization'] == False].copy()
excluded_df = validation_df[validation_df['Excluded from Optimization'] == True].copy()
# Products Included in Optimization
st.subheader("βœ… Products Included in Optimization")
st.write(f"**{len(included_df)} products** will be included in the optimization with total demand of **{included_df['Demand'].sum():,} units**")
if len(included_df) > 0:
# Configure column display for included
included_columns = ['Product ID', 'Demand', 'Product Type', 'Line Type', 'UNICEF Staff', 'Humanizer Staff', 'Production Speed (units/hour)', 'Validation Status']
st.dataframe(
included_df[included_columns],
use_container_width=True,
height=300
)
else:
st.warning("No products are included in optimization!")
# Products Excluded from Optimization
st.subheader("🚫 Products Excluded from Optimization")
st.write(f"**{len(excluded_df)} products** are excluded from optimization with total demand of **{excluded_df['Demand'].sum():,} units**")
st.info("These products are omitted from optimization due to missing line assignments or zero staffing requirements.")
if len(excluded_df) > 0:
# Show exclusion breakdown
exclusion_reasons = excluded_df['Exclusion Reasons'].value_counts()
st.write("**Exclusion reasons:**")
for reason, count in exclusion_reasons.items():
st.write(f"β€’ {reason}: {count} products")
# Configure column display for excluded
excluded_columns = ['Product ID', 'Demand', 'Product Type', 'Exclusion Reasons', 'UNICEF Staff', 'Humanizer Staff', 'Line Type']
st.dataframe(
excluded_df[excluded_columns],
use_container_width=True,
height=200
)
else:
st.info("No products are excluded from optimization.")
# Show validation reminder before optimization
st.info("πŸ’‘ **Review validation results above before running optimization.** " +
"Fix any critical issues (especially missing line assignments) to improve optimization success.")
else:
st.error("❌ Validation failed to run properly")
st.info("πŸ’‘ You can still proceed with optimization, but data issues may cause problems.")
# Reset the flags so validation doesn't show every time
col1, col2, col3 = st.columns([1, 1, 1])
with col2:
if st.button("βœ… Validation Reviewed - Continue to Optimization", use_container_width=True):
st.session_state.show_validation_after_save = False
st.session_state.settings_just_saved = False
# Clear validation cache to force fresh validation next time
if validation_cache_key in st.session_state:
del st.session_state[validation_cache_key]
st.rerun() # Refresh to hide validation section
# Optimization section
st.markdown("---")
st.header("πŸš€ Run Optimization")
st.markdown("Once you've configured your settings, run the optimization to generate the optimal workforce schedule.")
st.markdown("**πŸ’‘ Tip:** Optimization automatically clears all previous cache and results to ensure fresh calculations with your current settings.")
col1, col2, col3 = st.columns([1, 1, 1])
with col1:
if st.button("🧹 Clear Cache", use_container_width=True):
clear_all_cache_and_results()
with col2:
if st.button("πŸš€ Optimize Schedule", type="primary", use_container_width=True):
# Quick validation check before optimization
validation_warnings = check_critical_data_issues()
if validation_warnings:
st.warning("⚠️ **Data validation warnings detected:**")
for warning in validation_warnings:
st.warning(f"β€’ {warning}")
st.warning("**These issues may cause optimization to fail. Consider fixing them first.**")
# Give user option to proceed anyway
if st.button("⚠️ Proceed with Optimization Anyway", type="secondary"):
run_optimization()
else:
run_optimization()
with col3:
# Show status of current results
if 'optimization_results' in st.session_state and st.session_state.optimization_results is not None:
st.success("βœ… Results Available")
elif 'optimization_config' in st.session_state:
st.info("πŸ”„ Settings Saved - Ready to Optimize")
else:
st.info("⏳ No Results Yet")
# Display optimization results if available
if 'optimization_results' in st.session_state and st.session_state.optimization_results is not None:
st.markdown("---")
display_optimization_results(st.session_state.optimization_results)
def initialize_session_state():
"""Initialize session state with default values from constants.py"""
# Use constants from DefaultConfig - no more hardcoded values
# Use setdefault to avoid overwriting existing values
# Schedule defaults
st.session_state.setdefault('start_date', datetime.date(2025, 7, 7))
st.session_state.setdefault('schedule_type', DefaultConfig.SCHEDULE_TYPE)
# Shift defaults
st.session_state.setdefault('evening_shift_mode', DefaultConfig.EVENING_SHIFT_MODE)
st.session_state.setdefault('evening_shift_threshold', DefaultConfig.EVENING_SHIFT_DEMAND_THRESHOLD)
# Staff defaults
st.session_state.setdefault('fixed_staff_mode', DefaultConfig.FIXED_STAFF_MODE)
st.session_state.setdefault('fixed_min_unicef_per_day', DefaultConfig.FIXED_MIN_UNICEF_PER_DAY)
# Payment modes
st.session_state.setdefault('payment_mode_shift_1', DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.REGULAR])
st.session_state.setdefault('payment_mode_shift_2', DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.EVENING])
st.session_state.setdefault('payment_mode_shift_3', DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.OVERTIME])
# Working hours
st.session_state.setdefault('max_hour_per_person_per_day', DefaultConfig.MAX_HOUR_PER_PERSON_PER_DAY)
st.session_state.setdefault('max_hours_shift_1', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.REGULAR])
st.session_state.setdefault('max_hours_shift_2', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.EVENING])
st.session_state.setdefault('max_hours_shift_3', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.OVERTIME])
# Workforce limits
st.session_state.setdefault('max_unicef_per_day', DefaultConfig.MAX_UNICEF_PER_DAY)
st.session_state.setdefault('max_humanizer_per_day', DefaultConfig.MAX_HUMANIZER_PER_DAY)
# Operations
st.session_state.setdefault('line_count_long_line', DefaultConfig.LINE_COUNT_LONG_LINE)
st.session_state.setdefault('line_count_mini_load', DefaultConfig.LINE_COUNT_MINI_LOAD)
st.session_state.setdefault('max_parallel_workers_long_line', DefaultConfig.MAX_PARALLEL_WORKERS_LONG_LINE)
st.session_state.setdefault('max_parallel_workers_mini_load', DefaultConfig.MAX_PARALLEL_WORKERS_MINI_LOAD)
# Cost rates
st.session_state.setdefault('unicef_rate_shift_1', DefaultConfig.UNICEF_RATE_SHIFT_1)
st.session_state.setdefault('unicef_rate_shift_2', DefaultConfig.UNICEF_RATE_SHIFT_2)
st.session_state.setdefault('unicef_rate_shift_3', DefaultConfig.UNICEF_RATE_SHIFT_3)
st.session_state.setdefault('humanizer_rate_shift_1', DefaultConfig.HUMANIZER_RATE_SHIFT_1)
st.session_state.setdefault('humanizer_rate_shift_2', DefaultConfig.HUMANIZER_RATE_SHIFT_2)
st.session_state.setdefault('humanizer_rate_shift_3', DefaultConfig.HUMANIZER_RATE_SHIFT_3)
# Data selection
st.session_state.setdefault('selected_employee_types', ["UNICEF Fixed term", "Humanizer"])
st.session_state.setdefault('selected_shifts', [ShiftType.REGULAR, ShiftType.OVERTIME])
st.session_state.setdefault('selected_lines', [LineType.LONG_LINE, LineType.MINI_LOAD])
def render_schedule_config():
"""Render schedule configuration section"""
st.header("πŸ“… Schedule Configuration")
col1, col2 = st.columns(2)
with col1:
st.date_input(
"Start Date",
value=datetime.date(2025, 7, 7),
key='start_date',
help="Exact start date to filter demand data - will only use orders that start on this specific date"
)
with col2:
st.info("πŸ’‘ **Date Filtering**: System will use only demand data that starts on the exact date you select.")
# Evening shift configuration
st.subheader("πŸŒ™ Evening Shift Configuration")
shift_mode_options = ['normal', 'activate_evening', 'always_available']
st.selectbox(
"Evening Shift Mode",
options=shift_mode_options,
index=shift_mode_options.index(DefaultConfig.EVENING_SHIFT_MODE),
key='evening_shift_mode',
help="""
- **Normal**: Only regular shift (1) and overtime shift (3)
- **Activate Evening**: Allow evening shift (2) when demand is high
- **Always Available**: Evening shift always available as option
"""
)
if st.session_state.evening_shift_mode == 'activate_evening':
st.slider(
"Evening Shift Activation Threshold",
min_value=0.1,
max_value=1.0,
value=DefaultConfig.EVENING_SHIFT_DEMAND_THRESHOLD,
key='evening_shift_threshold',
step=0.1,
help="Activate evening shift if regular+overtime capacity < threshold of demand"
)
def render_workforce_config():
"""Render workforce configuration section"""
st.header("πŸ‘₯ Workforce Configuration")
# Fixed staff constraint mode
staff_mode_options = ['mandatory', 'available', 'priority', 'none']
st.selectbox(
"Fixed Staff Constraint Mode",
options=staff_mode_options,
index=staff_mode_options.index(DefaultConfig.FIXED_STAFF_MODE),
key='fixed_staff_mode',
help="""
- **Mandatory**: Forces all fixed staff to work full hours every day
- **Available**: Staff available up to limits but not forced
- **Priority**: Fixed staff used first, then temporary staff (recommended)
- **None**: Purely demand-driven scheduling
"""
)
# Workforce limits
st.subheader("πŸ‘¨β€πŸ’Ό Daily Workforce Limits")
col1, col2 = st.columns(2)
with col1:
st.number_input(
"Max UNICEF Fixed Term per Day",
min_value=1,
max_value=50,
value=DefaultConfig.MAX_UNICEF_PER_DAY,
key='max_unicef_per_day',
help="Maximum number of UNICEF fixed term employees per day"
)
with col2:
st.number_input(
"Max Humanizer per Day",
min_value=1,
max_value=50,
value=DefaultConfig.MAX_HUMANIZER_PER_DAY,
key='max_humanizer_per_day',
help="Maximum number of Humanizer employees per day"
)
# Fixed minimum UNICEF requirement
st.subheader("πŸ”’ Fixed Minimum Requirements")
st.number_input(
"Fixed Minimum UNICEF per Day",
min_value=0,
max_value=20,
value=DefaultConfig.FIXED_MIN_UNICEF_PER_DAY,
key='fixed_min_unicef_per_day',
help="Minimum number of UNICEF Fixed term employees required every working day (constraint)"
)
# Working hours configuration
st.subheader("⏰ Working Hours Configuration")
st.number_input(
"Max Hours per Person per Day",
min_value=1,
max_value=24,
value=DefaultConfig.MAX_HOUR_PER_PERSON_PER_DAY,
key='max_hour_per_person_per_day',
help="Legal maximum working hours per person per day"
)
col1, col2, col3 = st.columns(3)
with col1:
st.number_input(
"Max Hours - Shift 1 (Regular)",
min_value=1.0,
max_value=12.0,
value=float(DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.REGULAR]),
key='max_hours_shift_1',
step=0.5,
help="Maximum hours per person for regular shift"
)
with col2:
st.number_input(
"Max Hours - Shift 2 (Evening)",
min_value=1.0,
max_value=12.0,
value=float(DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.EVENING]),
key='max_hours_shift_2',
step=0.5,
help="Maximum hours per person for evening shift"
)
with col3:
st.number_input(
"Max Hours - Shift 3 (Overtime)",
min_value=1.0,
max_value=12.0,
value=float(DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.OVERTIME]),
key='max_hours_shift_3',
step=0.5,
help="Maximum hours per person for overtime shift"
)
def render_operations_config():
"""Render operations configuration section"""
st.header("🏭 Operations Configuration")
# Line configuration
st.subheader("πŸ“¦ Production Line Configuration")
col1, col2 = st.columns(2)
with col1:
st.number_input(
"Number of Long Lines",
min_value=1,
max_value=20,
value=DefaultConfig.LINE_COUNT_LONG_LINE,
key='line_count_long_line',
help="Number of long line production lines available"
)
st.number_input(
"Max Workers per Long Line",
min_value=1,
max_value=50,
value=DefaultConfig.MAX_PARALLEL_WORKERS_LONG_LINE,
key='max_parallel_workers_long_line',
help="Maximum number of workers that can work simultaneously on a long line"
)
with col2:
st.number_input(
"Number of Mini Load Lines",
min_value=1,
max_value=20,
value=DefaultConfig.LINE_COUNT_MINI_LOAD,
key='line_count_mini_load',
help="Number of mini load production lines available"
)
st.number_input(
"Max Workers per Mini Load Line",
min_value=1,
max_value=50,
value=DefaultConfig.MAX_PARALLEL_WORKERS_MINI_LOAD,
key='max_parallel_workers_mini_load',
help="Maximum number of workers that can work simultaneously on a mini load line"
)
def render_cost_config():
"""Render cost configuration section"""
st.header("πŸ’° Cost Configuration")
# Payment mode configuration
st.subheader("πŸ’³ Payment Mode Configuration")
st.markdown("""
**Payment Modes:**
- **Bulk**: If employee works any hours in shift, pay for full shift hours
- **Partial**: Pay only for actual hours worked
""")
col1, col2, col3 = st.columns(3)
payment_options = ['bulk', 'partial']
with col1:
st.selectbox(
"Shift 1 (Regular) Payment",
options=payment_options,
index=payment_options.index(DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.REGULAR]),
key='payment_mode_shift_1',
help="Payment mode for regular shift"
)
with col2:
st.selectbox(
"Shift 2 (Evening) Payment",
options=payment_options,
index=payment_options.index(DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.EVENING]),
key='payment_mode_shift_2',
help="Payment mode for evening shift"
)
with col3:
st.selectbox(
"Shift 3 (Overtime) Payment",
options=payment_options,
index=payment_options.index(DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.OVERTIME]),
key='payment_mode_shift_3',
help="Payment mode for overtime shift"
)
# Hourly rates configuration - editable with defaults from config
st.subheader("πŸ’΅ Hourly Rates Configuration")
st.markdown("**UNICEF Fixed Term Hourly Rates:**")
col1, col2, col3 = st.columns(3)
with col1:
st.number_input(
"Shift 1 (Regular) - UNICEF",
min_value=0.0,
max_value=200.0,
value=float(DefaultConfig.UNICEF_RATE_SHIFT_1),
key='unicef_rate_shift_1',
step=0.01,
format="%.2f",
help="Hourly rate for UNICEF Fixed Term staff during regular shift"
)
with col2:
st.number_input(
"Shift 2 (Evening) - UNICEF",
min_value=0.0,
max_value=200.0,
value=float(DefaultConfig.UNICEF_RATE_SHIFT_2),
key='unicef_rate_shift_2',
step=0.01,
format="%.2f",
help="Hourly rate for UNICEF Fixed Term staff during evening shift"
)
with col3:
st.number_input(
"Shift 3 (Overtime) - UNICEF",
min_value=0.0,
max_value=200.0,
value=float(DefaultConfig.UNICEF_RATE_SHIFT_3),
key='unicef_rate_shift_3',
step=0.01,
format="%.2f",
help="Hourly rate for UNICEF Fixed Term staff during overtime shift"
)
st.markdown("**Humanizer Hourly Rates:**")
col1, col2, col3 = st.columns(3)
with col1:
st.number_input(
"Shift 1 (Regular) - Humanizer",
min_value=0.0,
max_value=200.0,
value=float(DefaultConfig.HUMANIZER_RATE_SHIFT_1),
key='humanizer_rate_shift_1',
step=0.01,
format="%.2f",
help="Hourly rate for Humanizer staff during regular shift"
)
with col2:
st.number_input(
"Shift 2 (Evening) - Humanizer",
min_value=0.0,
max_value=200.0,
value=float(DefaultConfig.HUMANIZER_RATE_SHIFT_2),
key='humanizer_rate_shift_2',
step=0.01,
format="%.2f",
help="Hourly rate for Humanizer staff during evening shift"
)
with col3:
st.number_input(
"Shift 3 (Overtime) - Humanizer",
min_value=0.0,
max_value=200.0,
value=float(DefaultConfig.HUMANIZER_RATE_SHIFT_3),
key='humanizer_rate_shift_3',
step=0.01,
format="%.2f",
help="Hourly rate for Humanizer staff during overtime shift"
)
def render_data_selection_config():
"""Render data selection configuration section"""
st.header("πŸ“Š Data Selection Configuration")
st.markdown("Configure which data elements to include in the optimization.")
# Employee types selection
st.subheader("πŸ‘₯ Employee Types")
available_employee_types = ["UNICEF Fixed term", "Humanizer"]
selected_employee_types = st.multiselect(
"Select Employee Types to Include",
available_employee_types,
default=st.session_state.selected_employee_types,
help="Choose which employee types to include in the optimization"
)
st.session_state.selected_employee_types = selected_employee_types
# Shifts selection
st.subheader("πŸ• Shifts")
available_shifts = list(optimization_config.shift_code_to_name().keys())
shift_names = optimization_config.shift_code_to_name()
selected_shifts = st.multiselect(
"Select Shifts to Include",
available_shifts,
default=st.session_state.selected_shifts,
format_func=lambda x: f"Shift {x} ({shift_names[x]})",
help="Choose which shifts to include in the optimization"
)
st.session_state.selected_shifts = selected_shifts
# Production lines selection
st.subheader("🏭 Production Lines")
available_lines = list(optimization_config.line_code_to_name().keys())
line_names = optimization_config.line_code_to_name()
selected_lines = st.multiselect(
"Select Production Lines to Include",
available_lines,
default=st.session_state.selected_lines,
format_func=lambda x: f"Line {x} ({line_names[x]})",
help="Choose which production lines to include in the optimization"
)
st.session_state.selected_lines = selected_lines
# Validation warnings
if not selected_employee_types:
st.error("⚠️ At least one employee type must be selected!")
if not selected_shifts:
st.error("⚠️ At least one shift must be selected!")
if not selected_lines:
st.error("⚠️ At least one production line must be selected!")
def save_configuration():
"""Save current configuration to session state and potentially to file"""
# Get available demand dates from data to determine the actual date range
try:
import src.preprocess.extract as extract
import pandas as pd
from datetime import datetime
# Get data for the exact start date only
start_datetime = datetime.combine(st.session_state.start_date, datetime.min.time())
demand_data = extract.read_orders_data(start_date=start_datetime)
if not demand_data.empty:
# Get the unique finish dates for this start date
finish_dates = pd.to_datetime(demand_data["Basic finish date"]).dt.date.unique()
finish_dates = sorted(finish_dates)
if finish_dates:
calculated_end_date = max(finish_dates)
calculated_days = (calculated_end_date - st.session_state.start_date).days + 1
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)")
else:
raise Exception("No finish date found for start date")
# calculated_end_date = st.session_state.start_date
# calculated_days = 1
else:
# calculated_end_date = st.session_state.start_date
# calculated_days = 1
st.warning(f"⚠️ No demand data found for start date {st.session_state.start_date}")
raise Exception("No demand data found for start date")
except Exception as e:
st.warning(f"Could not determine date range from data: {e}. Using default 5-day period.")
raise Exception("Could not determine date range from data")
# from datetime import timedelta
# calculated_end_date = st.session_state.start_date + timedelta(days=4)
# calculated_days = 5
# Store calculated values in session state for compatibility
st.session_state.end_date = calculated_end_date
st.session_state.planning_days = calculated_days
# Create comprehensive configuration dictionary
config = {
'date_range': {
'start_date': st.session_state.start_date,
'end_date': st.session_state.end_date,
'planning_days': st.session_state.planning_days,
},
'schedule_type': st.session_state.schedule_type,
'evening_shift_mode': st.session_state.evening_shift_mode,
'evening_shift_threshold': st.session_state.evening_shift_threshold,
'fixed_staff_mode': st.session_state.fixed_staff_mode,
'payment_mode_config': {
ShiftType.REGULAR: st.session_state.payment_mode_shift_1,
ShiftType.EVENING: st.session_state.payment_mode_shift_2,
ShiftType.OVERTIME: st.session_state.payment_mode_shift_3,
},
'workforce_limits': {
'max_unicef_per_day': st.session_state.max_unicef_per_day,
'max_humanizer_per_day': st.session_state.max_humanizer_per_day,
'fixed_min_unicef_per_day': st.session_state.fixed_min_unicef_per_day,
},
'working_hours': {
'max_hour_per_person_per_day': st.session_state.max_hour_per_person_per_day,
'max_hours_per_shift': {
ShiftType.REGULAR: st.session_state.max_hours_shift_1,
ShiftType.EVENING: st.session_state.max_hours_shift_2,
ShiftType.OVERTIME: st.session_state.max_hours_shift_3,
}
},
'operations': {
'line_counts': {
LineType.LONG_LINE: st.session_state.line_count_long_line,
LineType.MINI_LOAD: st.session_state.line_count_mini_load,
},
'max_parallel_workers': {
LineType.LONG_LINE: st.session_state.max_parallel_workers_long_line,
LineType.MINI_LOAD: st.session_state.max_parallel_workers_mini_load,
}
},
'cost_rates': {
'UNICEF Fixed term': {
ShiftType.REGULAR: st.session_state.unicef_rate_shift_1,
ShiftType.EVENING: st.session_state.unicef_rate_shift_2,
ShiftType.OVERTIME: st.session_state.unicef_rate_shift_3,
},
'Humanizer': {
ShiftType.REGULAR: st.session_state.humanizer_rate_shift_1,
ShiftType.EVENING: st.session_state.humanizer_rate_shift_2,
ShiftType.OVERTIME: st.session_state.humanizer_rate_shift_3,
}
},
'data_selection': {
'selected_employee_types': st.session_state.get('selected_employee_types', []),
'selected_shifts': st.session_state.get('selected_shifts', []),
'selected_lines': st.session_state.get('selected_lines', []),
}
}
# Calculate date span for proper employee limits (use calculated planning_days)
date_span_length = st.session_state.planning_days
date_span = list(range(1, date_span_length + 1))
# Store individual items in session state for optimization_config.py to access
st.session_state.line_counts = config['operations']['line_counts']
st.session_state.cost_list_per_emp_shift = config['cost_rates']
st.session_state.payment_mode_config = config['payment_mode_config']
st.session_state.max_employee_per_type_on_day = {
"UNICEF Fixed term": {t: st.session_state.max_unicef_per_day for t in date_span},
"Humanizer": {t: st.session_state.max_humanizer_per_day for t in date_span}
}
# Store complete configuration
st.session_state.optimization_config = config
# Refresh module-level variables to pick up new configuration
# try:
# from src.config.optimization_config import _ensure_fresh_config
# _ensure_fresh_config()
# print("βœ… Refreshed module-level configuration variables")
# except Exception as e:
# print(f"⚠️ Could not refresh module-level variables: {e}")
# Return config for use in main function
return config
def display_user_friendly_summary(config):
"""Display a user-friendly summary of the configuration settings"""
# Schedule Settings
st.subheader("πŸ“… Schedule Settings")
col1, col2, col3, col4 = st.columns(4)
with col1:
st.write(f"**Start Date:** {config['date_range']['start_date']}")
with col2:
st.write(f"**Planning Period:** {config['date_range']['planning_days']} days")
with col3:
st.write(f"**End Date:** {config['date_range']['end_date']}")
with col4:
st.write(f"**Schedule Type:** {config['schedule_type'].title()}")
# Add evening shift mode in new row
col1, col2, col3, col4 = st.columns(4)
with col1:
st.write(f"**Evening Shift Mode:** {config['evening_shift_mode'].replace('_', ' ').title()}")
# Show additional schedule details if evening shift threshold is relevant
if config['evening_shift_mode'] == 'activate_evening':
with col2:
st.write(f"**Evening Shift Threshold:** {config['evening_shift_threshold']:.0%} demand capacity")
# Workforce Settings
st.subheader("πŸ‘₯ Workforce Settings")
col1, col2 = st.columns(2)
with col1:
st.write(f"**Max UNICEF Staff per Day:** {config['workforce_limits']['max_unicef_per_day']} people")
st.write(f"**Max Humanizer Staff per Day:** {config['workforce_limits']['max_humanizer_per_day']} people")
st.write(f"**Fixed Minimum UNICEF per Day:** {config['workforce_limits']['fixed_min_unicef_per_day']} people")
with col2:
st.write(f"**Staff Management Mode:** {config['fixed_staff_mode'].replace('_', ' ').title()}")
st.write(f"**Max Hours per Person per Day:** {config['working_hours']['max_hour_per_person_per_day']} hours")
# Operations Settings
st.subheader("🏭 Operations Settings")
col1, col2 = st.columns(2)
with col1:
st.write(f"**Long Lines Available:** {config['operations']['line_counts'][LineType.LONG_LINE]} lines")
st.write(f"**Mini Load Lines Available:** {config['operations']['line_counts'][LineType.MINI_LOAD]} lines")
with col2:
st.write(f"**Max Workers per Long Line:** {config['operations']['max_parallel_workers'][LineType.LONG_LINE]} people")
st.write(f"**Max Workers per Mini Load Line:** {config['operations']['max_parallel_workers'][LineType.MINI_LOAD]} people")
# Cost Settings
st.subheader("πŸ’° Cost Settings")
st.write("**Hourly Rates:**")
col1, col2 = st.columns(2)
with col1:
st.write("*UNICEF Fixed Term Staff:*")
st.write(f"β€’ Regular Shift: €{config['cost_rates']['UNICEF Fixed term'][ShiftType.REGULAR]:.2f}/hour")
st.write(f"β€’ Evening Shift: €{config['cost_rates']['UNICEF Fixed term'][ShiftType.EVENING]:.2f}/hour")
st.write(f"β€’ Overtime Shift: €{config['cost_rates']['UNICEF Fixed term'][ShiftType.OVERTIME]:.2f}/hour")
with col2:
st.write("*Humanizer Staff:*")
st.write(f"β€’ Regular Shift: €{config['cost_rates']['Humanizer'][ShiftType.REGULAR]:.2f}/hour")
st.write(f"β€’ Evening Shift: €{config['cost_rates']['Humanizer'][ShiftType.EVENING]:.2f}/hour")
st.write(f"β€’ Overtime Shift: €{config['cost_rates']['Humanizer'][ShiftType.OVERTIME]:.2f}/hour")
# Payment Settings
st.write("**Payment Modes:**")
payment_descriptions = {
'bulk': 'Full shift payment (even for partial hours)',
'partial': 'Pay only for actual hours worked'
}
col1, col2, col3 = st.columns(3)
with col1:
mode = config['payment_mode_config'][ShiftType.REGULAR]
st.write(f"β€’ **Regular Shift:** {mode.title()}")
st.caption(payment_descriptions[mode])
with col2:
mode = config['payment_mode_config'][ShiftType.EVENING]
st.write(f"β€’ **Evening Shift:** {mode.title()}")
st.caption(payment_descriptions[mode])
with col3:
mode = config['payment_mode_config'][ShiftType.OVERTIME]
st.write(f"β€’ **Overtime Shift:** {mode.title()}")
st.caption(payment_descriptions[mode])
# Data Selection Settings (if available)
if 'data_selection' in config:
st.subheader("πŸ“Š Data Selection")
col1, col2, col3 = st.columns(3)
with col1:
employee_types = config['data_selection']['selected_employee_types']
st.write(f"**Employee Types:** {len(employee_types)} selected")
for emp_type in employee_types:
st.write(f"β€’ {emp_type}")
with col2:
shifts = config['data_selection']['selected_shifts']
shift_names = ShiftType.get_all_names()
st.write(f"**Shifts:** {len(shifts)} selected")
for shift in shifts:
st.write(f"β€’ Shift {shift} ({shift_names.get(shift, 'Unknown')})")
with col3:
lines = config['data_selection']['selected_lines']
line_names = LineType.get_all_names()
st.write(f"**Production Lines:** {len(lines)} selected")
for line in lines:
st.write(f"β€’ Line {line} ({line_names.get(line, 'Unknown')})")
# Summary totals
st.subheader("πŸ“Š Quick Summary")
col1, col2, col3, col4 = st.columns(4)
with col1:
duration = config['date_range']['planning_days']
st.metric("Planning Period", f"{duration} days")
with col2:
total_staff = config['workforce_limits']['max_unicef_per_day'] + config['workforce_limits']['max_humanizer_per_day']
st.metric("Max Daily Staff", f"{total_staff} people")
with col3:
total_lines = config['operations']['line_counts'][LineType.LONG_LINE] + config['operations']['line_counts'][LineType.MINI_LOAD]
st.metric("Production Lines", f"{total_lines} lines")
with col4:
avg_unicef_rate = sum(config['cost_rates']['UNICEF Fixed term'].values()) / 3
st.metric("Avg UNICEF Rate", f"€{avg_unicef_rate:.2f}/hr")
# Demand Data Preview
st.subheader("πŸ“Š Demand Data Preview")
try:
import src.preprocess.extract as extract
from datetime import datetime
# Get demand data for the configured start date
start_date = config['date_range']['start_date']
start_datetime = datetime.combine(start_date, datetime.min.time())
demand_data = extract.read_orders_data(start_date=start_datetime)
if not demand_data.empty:
# Show summary statistics
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("πŸ“¦ Total Orders", len(demand_data))
with col2:
total_quantity = demand_data['Order quantity (GMEIN)'].sum()
st.metric("πŸ“Š Total Quantity", total_quantity)
with col3:
unique_products = demand_data['Material Number'].nunique()
st.metric("🏷️ Unique Products", unique_products)
with col4:
# Calculate date range
if 'Basic finish date' in demand_data.columns:
import pandas as pd
finish_dates = pd.to_datetime(demand_data['Basic finish date']).dt.date
min_finish = finish_dates.min()
max_finish = finish_dates.max()
if min_finish == max_finish:
date_range = f"{min_finish}"
else:
date_range = f"{min_finish} to {max_finish}"
else:
date_range = "N/A"
st.metric("πŸ“… Finish Dates for the start date orders", date_range)
# Show top products
if 'Material' in demand_data.columns and 'Quantity' in demand_data.columns:
with st.expander("πŸ” Top 10 Products by Quantity", expanded=False):
top_products = demand_data.groupby('Material')['Quantity'].sum().sort_values(ascending=False).head(10)
if not top_products.empty:
# Create a nice table
import pandas as pd
top_products_df = pd.DataFrame({
'Product': top_products.index,
'Total Quantity': top_products.values
}).reset_index(drop=True)
top_products_df.index = top_products_df.index + 1 # Start index from 1
st.dataframe(top_products_df, use_container_width=True)
else:
st.info("No product quantity data available.")
else:
st.warning(f"⚠️ No demand data found for {start_date}. Please select a different date or check your data files.")
except Exception as e:
st.error(f"❌ Error loading demand data: {e}")
def clear_all_cache_and_results():
"""Clear all cached data, modules, and results from previous runs"""
import importlib
import sys
st.info("🧹 Clearing all cached data and previous results...")
# Clear optimization cache using the dedicated function
clear_optimization_cache()
# Clear additional optimization-related session state
keys_to_clear = [
'demand_dictionary',
'kit_hierarchy_data',
'team_requirements'
]
cleared_keys = []
for key in keys_to_clear:
if key in st.session_state:
del st.session_state[key]
cleared_keys.append(key)
if cleared_keys:
st.write(f"πŸ—‘οΈ Cleared session state: {', '.join(cleared_keys)}")
# 2. Force reload all related modules to clear any module-level caches
modules_to_reload = [
'src.preprocess.extract',
'src.preprocess.transform',
'src.config.optimization_config',
'src.models.optimizer_real'
]
reloaded_modules = []
for module_name in modules_to_reload:
if module_name in sys.modules:
importlib.reload(sys.modules[module_name])
reloaded_modules.append(module_name)
if reloaded_modules:
st.write(f"πŸ”„ Reloaded modules: {', '.join(reloaded_modules)}")
# 3. Get end date from session state (calculated during save) and update global dates
if 'end_date' in st.session_state and 'planning_days' in st.session_state:
calculated_end_date = st.session_state.end_date
planning_days = st.session_state.planning_days
else:
# Fallback - use start date only for now, will be recalculated
from datetime import timedelta
calculated_end_date = st.session_state.start_date + timedelta(days=4)
planning_days = 5
# Note: Dates are handled directly in optimization_config.py via get_date_span()
# No need to set global dates - extract functions take start_date as parameter
st.write(f"πŸ“… Date configuration: {st.session_state.start_date} to {calculated_end_date} ({planning_days} days)")
st.info("πŸ’‘ Dates will be applied when optimization runs (via optimization_config.get_date_span())")
st.success("βœ… All caches and previous results cleared!")
def run_optimization():
"""Run the optimization model and store results"""
try:
st.info("πŸ”„ Running optimization... This may take a few moments.")
# Always clear everything first for clean slate
clear_all_cache_and_results()
# Show brief validation summary
st.info("πŸ“‹ Performing pre-optimization data validation...")
validation_warnings = check_critical_data_issues()
if validation_warnings:
with st.expander("⚠️ Data Validation Warnings", expanded=True):
for warning in validation_warnings:
st.warning(f"β€’ {warning}")
st.warning("**Optimization may fail due to these issues. Consider fixing them first.**")
else:
st.success("βœ… Data validation passed - no critical issues detected")
# Show current configuration being used
if 'end_date' in st.session_state and 'planning_days' in st.session_state:
calculated_end_date = st.session_state.end_date
planning_days = st.session_state.planning_days
st.info(f"πŸ—“οΈ Planning period: {st.session_state.start_date} to {calculated_end_date} ({planning_days} days)")
else:
st.info(f"πŸ—“οΈ Start date: {st.session_state.start_date} (will determine end date from demand data)")
st.info(f"πŸ‘₯ Max UNICEF/day: {st.session_state.max_unicef_per_day}, Max Humanizer/day: {st.session_state.max_humanizer_per_day}")
# Import and run the optimization (after clearing)
from src.models.optimizer_real import Optimizer
# Run the optimization using Optimizer class
with st.spinner('Optimizing workforce schedule...'):
optimizer = Optimizer()
results = optimizer.run_optimization()
if results is None:
st.error("❌ Optimization failed! The problem may be infeasible with current settings.")
if validation_warnings:
st.error("πŸ’‘ **Likely cause:** Data validation warnings detected above. Fix missing line assignments and other data issues first.")
st.error("Try adjusting your workforce limits, line counts, or evening shift settings.")
return
# Store results in session state
st.session_state.optimization_results = results
st.success("βœ… Optimization completed successfully!")
st.rerun() # Refresh to show results
except Exception as e:
import traceback
st.error(f"❌ Error during optimization: {str(e)}")
st.error("Please check your settings and data files.")
# Show detailed traceback in expander
with st.expander("πŸ” Show detailed error traceback"):
st.code(traceback.format_exc())
# Also print to console for debugging
print("\n" + "="*60)
print("❌ OPTIMIZATION ERROR - FULL TRACEBACK:")
print("="*60)
traceback.print_exc()
print("="*60)
def check_critical_data_issues():
"""
Quick check for critical data issues that could cause optimization failure
Returns list of warning messages
"""
warnings = []
try:
# Add src to path if needed
from src.demand_validation_viz import DemandValidationViz
# Initialize validator and load data
validator = DemandValidationViz()
if not validator.load_data():
warnings.append("Failed to load validation data")
return warnings
# Quick validation check
validation_df = validator.validate_all_products()
summary_stats = validator.get_summary_statistics(validation_df)
# Check for critical issues
if summary_stats['no_line_assignment'] > 0:
warnings.append(f"{summary_stats['no_line_assignment']} products missing line assignments")
if summary_stats['no_staffing'] > 0:
warnings.append(f"{summary_stats['no_staffing']} products missing staffing requirements")
if summary_stats['no_speed'] > 0:
warnings.append(f"{summary_stats['no_speed']} products missing production speed data")
# Calculate failure risk
# invalid_ratio = summary_stats['invalid_products'] / summary_stats['total_products']
# if invalid_ratio > 0.5:
# warnings.append(f"High failure risk: {invalid_ratio:.0%} of products have data issues")
return warnings
except Exception as e:
warnings.append(f"Validation check failed: {str(e)}")
return warnings
def display_optimization_results(results):
"""Import and display optimization results"""
from ui.pages.optimization_results import display_optimization_results as display_results
display_results(results)