HaLim commited on
Commit
1b82889
Β·
1 Parent(s): 3d08e0e

update viz

Browse files
Files changed (2) hide show
  1. config_page.py +317 -24
  2. 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
- run_optimization()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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="Start date for the optimization period"
155
  )
156
 
157
  with col2:
158
- st.session_state.end_date = st.date_input(
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': st.session_state.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 range(1, 6)},
580
- "Humanizer": {t: st.session_state.max_humanizer_per_day for t in range(1, 6)}
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"**End Date:** {config['date_range']['end_date']}")
599
  with col3:
600
- st.write(f"**Schedule Type:** {config['schedule_type'].title()}")
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
- st.write(f"**Evening Shift Threshold:** {config['evening_shift_threshold']:.0%} demand capacity")
 
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 = (config['date_range']['end_date'] - config['date_range']['start_date']).days + 1
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
- # Import and run the optimization
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.")