haileyhalimj@gmail.com
commited on
Commit
Β·
fa2c20f
1
Parent(s):
8c62ccd
Refactor optimization configuration and constants integration
Browse files- Updated `optimization_config.py` to utilize constants from `constants.py` for better maintainability.
- Added new constants for line counts, maximum parallel workers, and default rates.
- Enhanced session state initialization in `config_page.py` to use values from `DefaultConfig`.
- Improved architecture diagram to reflect changes in optimization flow.
- ARCHITECTURE_DIAGRAM.md +1 -1
- src/config/constants.py +50 -1
- src/config/optimization_config.py +43 -38
- src/models/optimizer_real.py +722 -668
- src/preprocess/hierarchy_parser.py +93 -0
- ui/pages/config_page.py +155 -118
ARCHITECTURE_DIAGRAM.md
CHANGED
|
@@ -486,7 +486,7 @@ sequenceDiagram
|
|
| 486 |
Config->>OptConfig: Get all parameters
|
| 487 |
OptConfig-->>Config: Return config
|
| 488 |
|
| 489 |
-
Config->>Optimizer:
|
| 490 |
|
| 491 |
Optimizer->>OptConfig: Get products
|
| 492 |
Optimizer->>OptConfig: Get demand
|
|
|
|
| 486 |
Config->>OptConfig: Get all parameters
|
| 487 |
OptConfig-->>Config: Return config
|
| 488 |
|
| 489 |
+
Config->>Optimizer: Optimizer().run_optimization()
|
| 490 |
|
| 491 |
Optimizer->>OptConfig: Get products
|
| 492 |
Optimizer->>OptConfig: Get demand
|
src/config/constants.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
Constants module for Supply Roster Optimization Tool
|
| 3 |
Replaces hard-coded magic numbers with meaningful named constants
|
| 4 |
"""
|
|
|
|
| 5 |
|
| 6 |
class ShiftType:
|
| 7 |
"""
|
|
@@ -138,6 +139,14 @@ class DefaultConfig:
|
|
| 138 |
# Default minimum UNICEF fixed-term employees per day
|
| 139 |
FIXED_MIN_UNICEF_PER_DAY = 2
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
# Default cost rates (example values)
|
| 142 |
DEFAULT_COST_RATES = {
|
| 143 |
"UNICEF Fixed term": {
|
|
@@ -150,4 +159,44 @@ class DefaultConfig:
|
|
| 150 |
ShiftType.EVENING: 27.94,
|
| 151 |
ShiftType.OVERTIME: 41.91
|
| 152 |
}
|
| 153 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
Constants module for Supply Roster Optimization Tool
|
| 3 |
Replaces hard-coded magic numbers with meaningful named constants
|
| 4 |
"""
|
| 5 |
+
from src.preprocess import extract
|
| 6 |
|
| 7 |
class ShiftType:
|
| 8 |
"""
|
|
|
|
| 139 |
# Default minimum UNICEF fixed-term employees per day
|
| 140 |
FIXED_MIN_UNICEF_PER_DAY = 2
|
| 141 |
|
| 142 |
+
# Default line counts
|
| 143 |
+
LINE_COUNT_LONG_LINE = 3
|
| 144 |
+
LINE_COUNT_MINI_LOAD = 2
|
| 145 |
+
|
| 146 |
+
# Default max parallel workers per line (for UI)
|
| 147 |
+
MAX_PARALLEL_WORKERS_LONG_LINE = 7
|
| 148 |
+
MAX_PARALLEL_WORKERS_MINI_LOAD = 5
|
| 149 |
+
|
| 150 |
# Default cost rates (example values)
|
| 151 |
DEFAULT_COST_RATES = {
|
| 152 |
"UNICEF Fixed term": {
|
|
|
|
| 159 |
ShiftType.EVENING: 27.94,
|
| 160 |
ShiftType.OVERTIME: 41.91
|
| 161 |
}
|
| 162 |
+
}
|
| 163 |
+
#get employee type list from data files
|
| 164 |
+
EMPLOYEE_TYPE_LIST =extract.read_employee_data()["employment_type"].unique().tolist()
|
| 165 |
+
SHIFT_LIST = extract.get_shift_info()["id"].unique().tolist()
|
| 166 |
+
EVENING_SHIFT_MODE = "normal"
|
| 167 |
+
EVENING_SHIFT_DEMAND_THRESHOLD = 0.9
|
| 168 |
+
|
| 169 |
+
# Default schedule type
|
| 170 |
+
SCHEDULE_TYPE = "weekly"
|
| 171 |
+
|
| 172 |
+
# Default fixed staff mode
|
| 173 |
+
FIXED_STAFF_MODE = "priority"
|
| 174 |
+
|
| 175 |
+
# Default hourly rates for UI (simplified)
|
| 176 |
+
UNICEF_RATE_SHIFT_1 = 12.5
|
| 177 |
+
UNICEF_RATE_SHIFT_2 = 15.0
|
| 178 |
+
UNICEF_RATE_SHIFT_3 = 18.75
|
| 179 |
+
HUMANIZER_RATE_SHIFT_1 = 10.0
|
| 180 |
+
HUMANIZER_RATE_SHIFT_2 = 12.0
|
| 181 |
+
HUMANIZER_RATE_SHIFT_3 = 15.0
|
| 182 |
+
LINE_LIST = extract.read_packaging_line_data()["id"].unique().tolist()
|
| 183 |
+
LINE_CNT_PER_TYPE = extract.read_packaging_line_data().set_index("id")["line_count"].to_dict()
|
| 184 |
+
|
| 185 |
+
# Dynamic method to get max employee per type on day
|
| 186 |
+
@staticmethod
|
| 187 |
+
def get_max_employee_per_type_on_day(date_span):
|
| 188 |
+
"""Get max employee per type configuration for given date span"""
|
| 189 |
+
return {
|
| 190 |
+
"UNICEF Fixed term": {
|
| 191 |
+
t: 8 for t in date_span
|
| 192 |
+
},
|
| 193 |
+
"Humanizer": {
|
| 194 |
+
t: 10 for t in date_span
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
MAX_UNICEF_PER_DAY = 8
|
| 198 |
+
MAX_HUMANIZER_PER_DAY = 10
|
| 199 |
+
MAX_HOUR_PER_PERSON_PER_DAY = 14
|
| 200 |
+
KIT_LEVELS, KIT_DEPENDENCIES, PRODUCTION_PRIORITY_ORDER = extract.get_production_order_data()
|
| 201 |
+
|
| 202 |
+
|
src/config/optimization_config.py
CHANGED
|
@@ -91,6 +91,7 @@ EVENING_SHIFT_MODE = "normal" # Default: only regular + overtime
|
|
| 91 |
# If demand cannot be met with regular + overtime, suggest evening shift activation
|
| 92 |
EVENING_SHIFT_DEMAND_THRESHOLD = 0.9 # Activate if regular+overtime capacity < 90% of demand
|
| 93 |
|
|
|
|
| 94 |
def get_active_shift_list():
|
| 95 |
"""
|
| 96 |
Get the list of active shifts based on EVENING_SHIFT_MODE setting.
|
|
@@ -122,7 +123,7 @@ def get_active_shift_list():
|
|
| 122 |
# DO NOT load at import time - always call get_active_shift_list() dynamically
|
| 123 |
# SHIFT_LIST = get_active_shift_list() # REMOVED - was causing stale data!
|
| 124 |
|
| 125 |
-
|
| 126 |
def get_line_list():
|
| 127 |
"""Get line list - try from streamlit session state first, then from data files"""
|
| 128 |
try:
|
|
@@ -143,7 +144,7 @@ def get_line_list():
|
|
| 143 |
# DO NOT load at import time - always call get_line_list() dynamically
|
| 144 |
# LINE_LIST = get_line_list() # REMOVED - was causing stale data!
|
| 145 |
|
| 146 |
-
|
| 147 |
def get_kit_line_match():
|
| 148 |
kit_line_match = extract.read_kit_line_match_data()
|
| 149 |
kit_line_match_dict = kit_line_match.set_index("kit_name")["line_type"].to_dict()
|
|
@@ -200,6 +201,7 @@ def get_line_cnt_per_type():
|
|
| 200 |
# DO NOT load at import time - always call get_line_cnt_per_type() dynamically
|
| 201 |
# LINE_CNT_PER_TYPE = get_line_cnt_per_type() # REMOVED - was causing stale data!
|
| 202 |
|
|
|
|
| 203 |
def get_demand_dictionary(force_reload=False):
|
| 204 |
"""
|
| 205 |
Get filtered demand dictionary.
|
|
@@ -224,6 +226,7 @@ def get_demand_dictionary(force_reload=False):
|
|
| 224 |
# DO NOT load at import time - always call get_demand_dictionary() dynamically
|
| 225 |
# DEMAND_DICTIONARY = get_demand_dictionary() # REMOVED - was causing stale data!
|
| 226 |
|
|
|
|
| 227 |
def get_cost_list_per_emp_shift():
|
| 228 |
try:
|
| 229 |
# Try to get from streamlit session state (from config page)
|
|
@@ -260,7 +263,7 @@ def line_code_to_name():
|
|
| 260 |
|
| 261 |
|
| 262 |
|
| 263 |
-
|
| 264 |
def get_team_requirements(product_list=None):
|
| 265 |
"""
|
| 266 |
Extract team requirements from Kits Calculation CSV.
|
|
@@ -310,58 +313,57 @@ def get_max_employee_per_type_on_day():
|
|
| 310 |
print(f"Could not get max employee counts from streamlit session: {e}")
|
| 311 |
|
| 312 |
print(f"Loading default max employee values")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
max_employee_per_type_on_day = {
|
| 314 |
"UNICEF Fixed term": {
|
| 315 |
-
t: 8 for t in
|
| 316 |
},
|
| 317 |
"Humanizer": {
|
| 318 |
-
t: 10 for t in
|
| 319 |
}
|
| 320 |
}
|
| 321 |
return max_employee_per_type_on_day
|
| 322 |
|
| 323 |
|
| 324 |
-
|
| 325 |
MAX_HOUR_PER_PERSON_PER_DAY = 14 # legal standard
|
| 326 |
def get_max_hour_per_shift_per_person():
|
| 327 |
-
"""Get max hours per shift per person
|
| 328 |
try:
|
| 329 |
import streamlit as st
|
| 330 |
-
if hasattr(st, 'session_state')
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
except Exception as e:
|
| 333 |
print(f"Could not get max hours per shift from session: {e}")
|
| 334 |
|
| 335 |
-
# Fallback to default
|
| 336 |
return DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON
|
| 337 |
|
| 338 |
|
| 339 |
|
| 340 |
# Keep these complex getters that access DefaultConfig or have complex logic:
|
| 341 |
def get_evening_shift_demand_threshold():
|
| 342 |
-
"""Get evening shift demand threshold
|
| 343 |
try:
|
| 344 |
import streamlit as st
|
| 345 |
-
if hasattr(st, 'session_state')
|
| 346 |
-
return st.session_state.
|
| 347 |
except Exception as e:
|
| 348 |
print(f"Could not get evening shift threshold from session: {e}")
|
| 349 |
|
| 350 |
-
# Fallback to default
|
| 351 |
-
return
|
| 352 |
-
|
| 353 |
-
def get_fixed_min_unicef_per_day():
|
| 354 |
-
"""Get fixed minimum UNICEF staff per day - checks Streamlit session state first"""
|
| 355 |
-
try:
|
| 356 |
-
import streamlit as st
|
| 357 |
-
if hasattr(st, 'session_state') and 'fixed_min_unicef_per_day' in st.session_state:
|
| 358 |
-
return st.session_state.fixed_min_unicef_per_day
|
| 359 |
-
except Exception as e:
|
| 360 |
-
print(f"Could not get fixed min UNICEF from session: {e}")
|
| 361 |
-
|
| 362 |
-
# Fallback to default only if not configured by user
|
| 363 |
-
return getattr(DefaultConfig, 'FIXED_MIN_UNICEF_PER_DAY', {1: 1, 2: 1, 3: 1, 4: 1, 5: 1})
|
| 364 |
-
|
| 365 |
|
| 366 |
|
| 367 |
# ---- Kit Hierarchy for Production Ordering ----
|
|
@@ -383,22 +385,25 @@ def get_kit_dependencies():
|
|
| 383 |
_, dependencies, _ = get_kit_hierarchy_data()
|
| 384 |
return dependencies
|
| 385 |
|
| 386 |
-
def get_production_priority_order():
|
| 387 |
-
"""Get production priority order lazily - returns [kit_ids] sorted by production priority"""
|
| 388 |
-
_, _, priority_order = get_kit_hierarchy_data()
|
| 389 |
-
return priority_order
|
| 390 |
-
|
| 391 |
def get_max_parallel_workers():
|
| 392 |
-
"""Get max parallel workers
|
| 393 |
try:
|
| 394 |
import streamlit as st
|
| 395 |
-
if hasattr(st, 'session_state')
|
| 396 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
except Exception as e:
|
| 398 |
print(f"Could not get max parallel workers from session: {e}")
|
| 399 |
|
| 400 |
-
# Fallback to default
|
| 401 |
-
return
|
|
|
|
|
|
|
|
|
|
| 402 |
|
| 403 |
|
| 404 |
|
|
|
|
| 91 |
# If demand cannot be met with regular + overtime, suggest evening shift activation
|
| 92 |
EVENING_SHIFT_DEMAND_THRESHOLD = 0.9 # Activate if regular+overtime capacity < 90% of demand
|
| 93 |
|
| 94 |
+
#Where?
|
| 95 |
def get_active_shift_list():
|
| 96 |
"""
|
| 97 |
Get the list of active shifts based on EVENING_SHIFT_MODE setting.
|
|
|
|
| 123 |
# DO NOT load at import time - always call get_active_shift_list() dynamically
|
| 124 |
# SHIFT_LIST = get_active_shift_list() # REMOVED - was causing stale data!
|
| 125 |
|
| 126 |
+
#where?
|
| 127 |
def get_line_list():
|
| 128 |
"""Get line list - try from streamlit session state first, then from data files"""
|
| 129 |
try:
|
|
|
|
| 144 |
# DO NOT load at import time - always call get_line_list() dynamically
|
| 145 |
# LINE_LIST = get_line_list() # REMOVED - was causing stale data!
|
| 146 |
|
| 147 |
+
#where?
|
| 148 |
def get_kit_line_match():
|
| 149 |
kit_line_match = extract.read_kit_line_match_data()
|
| 150 |
kit_line_match_dict = kit_line_match.set_index("kit_name")["line_type"].to_dict()
|
|
|
|
| 201 |
# DO NOT load at import time - always call get_line_cnt_per_type() dynamically
|
| 202 |
# LINE_CNT_PER_TYPE = get_line_cnt_per_type() # REMOVED - was causing stale data!
|
| 203 |
|
| 204 |
+
#where?
|
| 205 |
def get_demand_dictionary(force_reload=False):
|
| 206 |
"""
|
| 207 |
Get filtered demand dictionary.
|
|
|
|
| 226 |
# DO NOT load at import time - always call get_demand_dictionary() dynamically
|
| 227 |
# DEMAND_DICTIONARY = get_demand_dictionary() # REMOVED - was causing stale data!
|
| 228 |
|
| 229 |
+
#delete as already using default cost rates
|
| 230 |
def get_cost_list_per_emp_shift():
|
| 231 |
try:
|
| 232 |
# Try to get from streamlit session state (from config page)
|
|
|
|
| 263 |
|
| 264 |
|
| 265 |
|
| 266 |
+
#where to put?
|
| 267 |
def get_team_requirements(product_list=None):
|
| 268 |
"""
|
| 269 |
Extract team requirements from Kits Calculation CSV.
|
|
|
|
| 313 |
print(f"Could not get max employee counts from streamlit session: {e}")
|
| 314 |
|
| 315 |
print(f"Loading default max employee values")
|
| 316 |
+
# Get date span dynamically if not available
|
| 317 |
+
if DATE_SPAN is None:
|
| 318 |
+
date_span, _, _ = get_date_span()
|
| 319 |
+
else:
|
| 320 |
+
date_span = DATE_SPAN
|
| 321 |
+
|
| 322 |
max_employee_per_type_on_day = {
|
| 323 |
"UNICEF Fixed term": {
|
| 324 |
+
t: 8 for t in date_span
|
| 325 |
},
|
| 326 |
"Humanizer": {
|
| 327 |
+
t: 10 for t in date_span
|
| 328 |
}
|
| 329 |
}
|
| 330 |
return max_employee_per_type_on_day
|
| 331 |
|
| 332 |
|
| 333 |
+
# Keep the constant for backward compatibility, but use function instead
|
| 334 |
MAX_HOUR_PER_PERSON_PER_DAY = 14 # legal standard
|
| 335 |
def get_max_hour_per_shift_per_person():
|
| 336 |
+
"""Get max hours per shift per person from session state or default"""
|
| 337 |
try:
|
| 338 |
import streamlit as st
|
| 339 |
+
if hasattr(st, 'session_state'):
|
| 340 |
+
# Build from individual session state values
|
| 341 |
+
max_hours = {
|
| 342 |
+
ShiftType.REGULAR: st.session_state.get('max_hours_shift_1', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.REGULAR]),
|
| 343 |
+
ShiftType.EVENING: st.session_state.get('max_hours_shift_2', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.EVENING]),
|
| 344 |
+
ShiftType.OVERTIME: st.session_state.get('max_hours_shift_3', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.OVERTIME])
|
| 345 |
+
}
|
| 346 |
+
return max_hours
|
| 347 |
except Exception as e:
|
| 348 |
print(f"Could not get max hours per shift from session: {e}")
|
| 349 |
|
| 350 |
+
# Fallback to default
|
| 351 |
return DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON
|
| 352 |
|
| 353 |
|
| 354 |
|
| 355 |
# Keep these complex getters that access DefaultConfig or have complex logic:
|
| 356 |
def get_evening_shift_demand_threshold():
|
| 357 |
+
"""Get evening shift demand threshold from session state or default"""
|
| 358 |
try:
|
| 359 |
import streamlit as st
|
| 360 |
+
if hasattr(st, 'session_state'):
|
| 361 |
+
return st.session_state.get('evening_shift_threshold', DefaultConfig.EVENING_SHIFT_DEMAND_THRESHOLD)
|
| 362 |
except Exception as e:
|
| 363 |
print(f"Could not get evening shift threshold from session: {e}")
|
| 364 |
|
| 365 |
+
# Fallback to default
|
| 366 |
+
return DefaultConfig.EVENING_SHIFT_DEMAND_THRESHOLD
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
|
| 369 |
# ---- Kit Hierarchy for Production Ordering ----
|
|
|
|
| 385 |
_, dependencies, _ = get_kit_hierarchy_data()
|
| 386 |
return dependencies
|
| 387 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
def get_max_parallel_workers():
|
| 389 |
+
"""Get max parallel workers from session state or default"""
|
| 390 |
try:
|
| 391 |
import streamlit as st
|
| 392 |
+
if hasattr(st, 'session_state'):
|
| 393 |
+
# Build from individual session state values
|
| 394 |
+
max_parallel_workers = {
|
| 395 |
+
LineType.LONG_LINE: st.session_state.get('max_parallel_workers_long_line', DefaultConfig.MAX_PARALLEL_WORKERS_LONG_LINE),
|
| 396 |
+
LineType.MINI_LOAD: st.session_state.get('max_parallel_workers_mini_load', DefaultConfig.MAX_PARALLEL_WORKERS_MINI_LOAD)
|
| 397 |
+
}
|
| 398 |
+
return max_parallel_workers
|
| 399 |
except Exception as e:
|
| 400 |
print(f"Could not get max parallel workers from session: {e}")
|
| 401 |
|
| 402 |
+
# Fallback to default
|
| 403 |
+
return {
|
| 404 |
+
LineType.LONG_LINE: DefaultConfig.MAX_PARALLEL_WORKERS_LONG_LINE,
|
| 405 |
+
LineType.MINI_LOAD: DefaultConfig.MAX_PARALLEL_WORKERS_MINI_LOAD
|
| 406 |
+
}
|
| 407 |
|
| 408 |
|
| 409 |
|
src/models/optimizer_real.py
CHANGED
|
@@ -9,718 +9,772 @@
|
|
| 9 |
|
| 10 |
from ortools.linear_solver import pywraplp
|
| 11 |
from math import ceil
|
|
|
|
| 12 |
from src.config.constants import ShiftType, LineType, KitLevel
|
| 13 |
|
| 14 |
# ---- config import ----
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
EVENING_SHIFT_DEMAND_THRESHOLD,
|
| 33 |
-
# Hierarchy variables for production ordering
|
| 34 |
-
get_kit_levels, # DYNAMIC: {kit_id: level} where 0=prepack, 1=subkit, 2=master
|
| 35 |
-
get_kit_dependencies, # DYNAMIC: {kit_id: [dependency_list]}
|
| 36 |
-
get_production_priority_order, # DYNAMIC: [kit_ids] sorted by production priority
|
| 37 |
-
# Fixed staffing requirements
|
| 38 |
-
get_fixed_min_unicef_per_day, # DYNAMIC: Minimum UNICEF employees required per day
|
| 39 |
-
)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
# 2) kit_line_match - no printing to avoid spam
|
| 44 |
-
# KIT_LINE_MATCH_DICT loaded lazily in functions that need it
|
| 45 |
-
|
| 46 |
-
# 3) If specific product is not produced on specific date, set it to 0
|
| 47 |
-
# ACTIVE will be built dynamically in solve function based on fresh PRODUCT_LIST
|
| 48 |
-
# Example: ACTIVE[2]['C'] = 0 # Disable product C on day 2
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def build_lines():
|
| 52 |
-
"""List of line instances.
|
| 53 |
-
line_tuples elements are (line_type_id, idx) tuples. e.g., (6,1), (6,2), (7,1), ...
|
| 54 |
-
"""
|
| 55 |
-
line_tuples = []
|
| 56 |
-
LINE_LIST = get_line_list() # Dynamic call
|
| 57 |
-
LINE_CNT_PER_TYPE = get_line_cnt_per_type() # Dynamic call
|
| 58 |
-
|
| 59 |
-
for lt in LINE_LIST: # lt: 6 or 7
|
| 60 |
-
cnt = int(LINE_CNT_PER_TYPE.get(lt, 0))
|
| 61 |
-
for i in range(1, cnt + 1):
|
| 62 |
-
line_tuples.append((lt, i))
|
| 63 |
-
return line_tuples
|
| 64 |
-
|
| 65 |
-
# DISABLED: Module-level initialization was causing infinite loops
|
| 66 |
-
# These variables are now created dynamically when needed
|
| 67 |
-
# line_tuples=build_lines()
|
| 68 |
-
# print("line_tuples",line_tuples)
|
| 69 |
-
# PER_PRODUCT_SPEED = extract.read_package_speed_data() # Dynamic call
|
| 70 |
-
# print("PER_PRODUCT_SPEED",PER_PRODUCT_SPEED)
|
| 71 |
-
|
| 72 |
-
def sort_products_by_hierarchy(product_list):
|
| 73 |
-
"""
|
| 74 |
-
Sort products by hierarchy levels and dependencies using topological sorting.
|
| 75 |
-
Returns products in optimal production order: prepacks β subkits β masters
|
| 76 |
-
Dependencies within the same level are properly ordered.
|
| 77 |
-
"""
|
| 78 |
-
from collections import defaultdict, deque
|
| 79 |
-
|
| 80 |
-
# Filter products that are in our production list and have hierarchy data
|
| 81 |
-
products_with_hierarchy = [p for p in product_list if p in KIT_LEVELS]
|
| 82 |
-
products_without_hierarchy = [p for p in product_list if p not in KIT_LEVELS]
|
| 83 |
-
|
| 84 |
-
if products_without_hierarchy:
|
| 85 |
-
print(f"[HIERARCHY] Products without hierarchy data: {products_without_hierarchy}")
|
| 86 |
-
|
| 87 |
-
# Build dependency graph for products in our list
|
| 88 |
-
graph = defaultdict(list) # product -> [dependents]
|
| 89 |
-
in_degree = defaultdict(int) # product -> number of dependencies
|
| 90 |
-
|
| 91 |
-
# Initialize all products
|
| 92 |
-
for product in products_with_hierarchy:
|
| 93 |
-
in_degree[product] = 0
|
| 94 |
-
|
| 95 |
-
# Build edges based on actual dependencies
|
| 96 |
-
# KIT_DEPENDENCIES = {product: [dependencies]} - "What does THIS product need?"
|
| 97 |
-
# graph = {dependency: [products]} - "What depends on THIS dependency?"
|
| 98 |
-
#
|
| 99 |
-
# Example transformation:
|
| 100 |
-
# KIT_DEPENDENCIES = {'subkit_A': ['prepack_1'], 'master_B': ['subkit_A']}
|
| 101 |
-
# After building: graph = {'prepack_1': ['subkit_A'], 'subkit_A': ['master_B']}
|
| 102 |
-
# This means: prepack_1 is needed by subkit_A, subkit_A is needed by master_B
|
| 103 |
-
#
|
| 104 |
-
# Example:
|
| 105 |
-
# 1. product='subkit_A', deps=['prepack_1']
|
| 106 |
-
# β graph['prepack_1'].append('subkit_A')
|
| 107 |
-
# β graph = {'prepack_1': ['subkit_A']}
|
| 108 |
-
# 2. product='master_B', deps=['subkit_A']
|
| 109 |
-
# β graph['subkit_A'].append('master_B')
|
| 110 |
-
# β graph = {'prepack_1': ['subkit_A'], 'subkit_A': ['master_B']}
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
for product in products_with_hierarchy:
|
| 114 |
-
deps = KIT_DEPENDENCIES.get(product, []) #dependencies = products that has to be packed first
|
| 115 |
-
for dep in deps:
|
| 116 |
-
if dep in products_with_hierarchy: # Only if dependency is in our production list
|
| 117 |
-
# REVERSE THE RELATIONSHIP:
|
| 118 |
-
# KIT_DEPENDENCIES says: "product needs dep"
|
| 119 |
-
# graph says: "dep is needed by product"
|
| 120 |
-
graph[dep].append(product) # dep -> product (reverse the relationship!)
|
| 121 |
-
in_degree[product] += 1
|
| 122 |
-
|
| 123 |
-
# Topological sort with hierarchy level priority
|
| 124 |
-
sorted_products = []
|
| 125 |
-
#queue = able to remove from both sides
|
| 126 |
-
queue = deque()
|
| 127 |
-
|
| 128 |
-
# Start with products that have no dependencies
|
| 129 |
-
for product in products_with_hierarchy:
|
| 130 |
-
if in_degree[product] == 0:
|
| 131 |
-
queue.append(product)
|
| 132 |
-
|
| 133 |
-
while queue:
|
| 134 |
-
current = queue.popleft()
|
| 135 |
-
sorted_products.append(current)
|
| 136 |
-
|
| 137 |
-
# Process dependents - sort by hierarchy level first
|
| 138 |
-
for dependent in sorted(graph[current], key=lambda p: (KIT_LEVELS.get(p, 999), p)):
|
| 139 |
-
in_degree[dependent] -= 1 #decrement the in_degree of the dependent
|
| 140 |
-
if in_degree[dependent] == 0: #if the in_degree of the dependent is 0, add it to the queue so that it can be processed
|
| 141 |
-
queue.append(dependent)
|
| 142 |
-
|
| 143 |
-
# Check for cycles (shouldn't happen with proper hierarchy)
|
| 144 |
-
if len(sorted_products) != len(products_with_hierarchy):
|
| 145 |
-
remaining = [p for p in products_with_hierarchy if p not in sorted_products]
|
| 146 |
-
print(f"[HIERARCHY] WARNING: Potential circular dependencies detected in: {remaining}")
|
| 147 |
-
# Add remaining products sorted by level as fallback
|
| 148 |
-
remaining_sorted = sorted(remaining, key=lambda p: (KIT_LEVELS.get(p, 999), p))
|
| 149 |
-
sorted_products.extend(remaining_sorted)
|
| 150 |
-
|
| 151 |
-
# Add products without hierarchy information at the end
|
| 152 |
-
sorted_products.extend(sorted(products_without_hierarchy))
|
| 153 |
-
|
| 154 |
-
print(f"[HIERARCHY] Dependency-aware production order: {len(sorted_products)} products")
|
| 155 |
-
for i, p in enumerate(sorted_products[:10]): # Show first 10
|
| 156 |
-
level = KIT_LEVELS.get(p, "unknown")
|
| 157 |
-
level_name = KitLevel.get_name(level)
|
| 158 |
-
deps = KIT_DEPENDENCIES.get(p, [])
|
| 159 |
-
deps_in_list = [d for d in deps if d in products_with_hierarchy]
|
| 160 |
-
print(f" {i+1}. {p} (level {level}={level_name}, deps: {len(deps_in_list)})")
|
| 161 |
-
if deps_in_list:
|
| 162 |
-
print(f" Dependencies: {deps_in_list}")
|
| 163 |
-
|
| 164 |
-
if len(sorted_products) > 10:
|
| 165 |
-
print(f" ... and {len(sorted_products) - 10} more products")
|
| 166 |
-
|
| 167 |
-
return sorted_products
|
| 168 |
-
|
| 169 |
-
# Removed get_dependency_timing_weight function - no longer needed
|
| 170 |
-
# Dependency ordering is now handled by topological sorting in sort_products_by_hierarchy()
|
| 171 |
-
|
| 172 |
-
def run_optimization_for_week():
|
| 173 |
-
# Load hierarchy data lazily at the start of optimization
|
| 174 |
-
global KIT_LEVELS, KIT_DEPENDENCIES, PRODUCTION_PRIORITY_ORDER, KIT_LINE_MATCH_DICT
|
| 175 |
-
KIT_LEVELS = get_kit_levels()
|
| 176 |
-
KIT_DEPENDENCIES = get_kit_dependencies()
|
| 177 |
-
PRODUCTION_PRIORITY_ORDER = get_production_priority_order()
|
| 178 |
-
KIT_LINE_MATCH_DICT = get_kit_line_match()
|
| 179 |
-
|
| 180 |
-
# *** CRITICAL: Load fresh data to reflect current Streamlit configs ***
|
| 181 |
-
print("\n" + "="*60)
|
| 182 |
-
print("π LOADING FRESH DATA FOR OPTIMIZATION")
|
| 183 |
-
print("="*60)
|
| 184 |
-
|
| 185 |
-
# Get fresh product list and demand data
|
| 186 |
-
PRODUCT_LIST = get_product_list()
|
| 187 |
-
DEMAND_DICTIONARY = get_demand_dictionary()
|
| 188 |
-
TEAM_REQ_PER_PRODUCT = get_team_requirements(PRODUCT_LIST)
|
| 189 |
-
|
| 190 |
-
# Get date span dynamically
|
| 191 |
-
DATE_SPAN, start_date, end_date = get_date_span()
|
| 192 |
-
|
| 193 |
-
print(f"π¦ LOADED PRODUCTS: {len(PRODUCT_LIST)} products")
|
| 194 |
-
print(f"π LOADED DEMAND: {sum(DEMAND_DICTIONARY.values())} total units")
|
| 195 |
-
print(f"π₯ LOADED TEAM REQUIREMENTS: {len(TEAM_REQ_PER_PRODUCT)} employee types")
|
| 196 |
-
|
| 197 |
-
# Build ACTIVE schedule for fresh product list
|
| 198 |
-
ACTIVE = {t: {p: 1 for p in PRODUCT_LIST} for t in DATE_SPAN}
|
| 199 |
-
|
| 200 |
-
# --- Sets ---
|
| 201 |
-
date_span_list = list(DATE_SPAN)
|
| 202 |
-
# print("date_span_list",date_span_list)
|
| 203 |
-
active_shift_list = sorted(list(get_active_shift_list())) # Dynamic call
|
| 204 |
-
employee_type_list = list(get_employee_type_list()) # Dynamic call - e.g., ['UNICEF Fixed term','Humanizer']
|
| 205 |
-
print("employee_type_list",employee_type_list)
|
| 206 |
-
# *** HIERARCHY SORTING: Sort products by production priority ***
|
| 207 |
-
print("\n" + "="*60)
|
| 208 |
-
print("π APPLYING HIERARCHY-BASED PRODUCTION ORDERING")
|
| 209 |
-
print("="*60)
|
| 210 |
-
sorted_product_list = sort_products_by_hierarchy(list(PRODUCT_LIST))
|
| 211 |
-
|
| 212 |
-
line_tuples = build_lines()
|
| 213 |
-
print("Lines",line_tuples)
|
| 214 |
-
|
| 215 |
-
# Load product speed data dynamically
|
| 216 |
-
# Import extract module for direct access to data functions
|
| 217 |
-
from src.preprocess import extract
|
| 218 |
-
PER_PRODUCT_SPEED = extract.read_package_speed_data()
|
| 219 |
-
print("PER_PRODUCT_SPEED",PER_PRODUCT_SPEED)
|
| 220 |
-
|
| 221 |
-
# --- Short aliases for parameters ---
|
| 222 |
-
Hmax_s = dict(get_max_hour_per_shift_per_person()) # Dynamic call - per-shift hours
|
| 223 |
-
Hmax_daily = MAX_HOUR_PER_PERSON_PER_DAY # {6:cap, 7:cap}
|
| 224 |
-
max_workers_line = dict(get_max_parallel_workers()) # Dynamic call - per line type
|
| 225 |
-
max_employee_type_day = get_max_employee_per_type_on_day() # Dynamic call - {emp_type:{t:headcount}}
|
| 226 |
-
cost = get_cost_list_per_emp_shift() # Dynamic call - {emp_type:{shift:cost}}
|
| 227 |
-
# --- Feasibility quick checks ---
|
| 228 |
-
|
| 229 |
-
# 1) If team size is greater than max_workers_line, block the product-line type combination
|
| 230 |
-
for p in sorted_product_list:
|
| 231 |
-
req_total = sum(TEAM_REQ_PER_PRODUCT[e][p] for e in employee_type_list)
|
| 232 |
-
lt = KIT_LINE_MATCH_DICT.get(p, 6) # Default to long line (6) if not found
|
| 233 |
-
if p not in KIT_LINE_MATCH_DICT:
|
| 234 |
-
print(f"[WARN] Product {p}: No line type mapping found, defaulting to long line (6)")
|
| 235 |
-
if req_total > max_workers_line.get(lt, 1e9):
|
| 236 |
-
print(f"[WARN] Product {p}: team size {req_total} > MAX_PARALLEL_WORKERS[{lt}] "
|
| 237 |
-
f"= {max_workers_line.get(lt)}. Blocked.")
|
| 238 |
-
|
| 239 |
-
# 2) Check if demand can be met without evening shift (only if in normal mode)
|
| 240 |
-
if EVENING_SHIFT_MODE == "normal":
|
| 241 |
-
total_demand = sum(DEMAND_DICTIONARY.get(p, 0) for p in sorted_product_list)
|
| 242 |
-
|
| 243 |
-
# Calculate maximum capacity with regular + overtime shifts only
|
| 244 |
-
regular_overtime_shifts = [s for s in active_shift_list if s in ShiftType.REGULAR_AND_OVERTIME]
|
| 245 |
-
max_capacity = 0
|
| 246 |
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
|
| 261 |
-
print(
|
| 262 |
-
|
| 263 |
-
|
|
|
|
|
|
|
| 264 |
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
|
| 273 |
-
|
| 274 |
-
solver = pywraplp.Solver.CreateSolver('CBC')
|
| 275 |
-
if not solver:
|
| 276 |
-
raise RuntimeError("CBC solver not found.")
|
| 277 |
-
INF = solver.infinity()
|
| 278 |
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
| 284 |
for s in active_shift_list:
|
| 285 |
for t in date_span_list:
|
| 286 |
-
|
| 287 |
-
Assignment[p, ell, s, t] = solver.BoolVar(f"Z_{p}_{ell[0]}_{ell[1]}_s{s}_d{t}")
|
| 288 |
-
#How many hours does product p run on line ell, during shift s, on day t?
|
| 289 |
-
Hours[p, ell, s, t] = solver.NumVar(0, Hmax_s[s], f"T_{p}_{ell[0]}_{ell[1]}_s{s}_d{t}")
|
| 290 |
-
#How many units does product p run on line ell, during shift s, on day t?
|
| 291 |
-
Units[p, ell, s, t] = solver.NumVar(0, INF, f"U_{p}_{ell[0]}_{ell[1]}_s{s}_d{t}")
|
| 292 |
-
|
| 293 |
-
# Note: IDLE variables removed - we only track employees actually working on production
|
| 294 |
-
|
| 295 |
-
# Load fixed minimum UNICEF requirement (needed for EMPLOYEE_COUNT variable creation)
|
| 296 |
-
FIXED_MIN_UNICEF_PER_DAY = get_fixed_min_unicef_per_day() # Dynamic call
|
| 297 |
-
|
| 298 |
-
# Variable to track actual number of employees of each type working each shift each day
|
| 299 |
-
# This represents how many distinct employees of type e are working in shift s on day t
|
| 300 |
-
EMPLOYEE_COUNT = {}
|
| 301 |
-
for e in employee_type_list:
|
| 302 |
-
for s in active_shift_list:
|
| 303 |
-
for t in date_span_list:
|
| 304 |
-
# Note: Minimum staffing is per day, not per shift
|
| 305 |
-
# We'll handle the daily minimum constraint separately
|
| 306 |
-
max_count = max_employee_type_day.get(e, {}).get(t, 100)
|
| 307 |
-
EMPLOYEE_COUNT[e, s, t] = solver.IntVar(
|
| 308 |
-
0, # No minimum per shift (daily minimum handled separately)
|
| 309 |
-
max_count,
|
| 310 |
-
f"EmpCount_{e}_s{s}_day{t}"
|
| 311 |
-
)
|
| 312 |
-
|
| 313 |
-
# Track total person-hours worked by each employee type per shift per day
|
| 314 |
-
# This is needed for employee-centric wage calculation
|
| 315 |
-
EMPLOYEE_HOURS = {}
|
| 316 |
-
for e in employee_type_list:
|
| 317 |
-
for s in active_shift_list:
|
| 318 |
-
for t in date_span_list:
|
| 319 |
-
# Sum of all work hours for employee type e in shift s on day t
|
| 320 |
-
# This represents total person-hours (e.g., 5 employees Γ 8 hours = 40 person-hours)
|
| 321 |
-
EMPLOYEE_HOURS[e, s, t] = solver.Sum(
|
| 322 |
-
TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, s, t]
|
| 323 |
-
for p in sorted_product_list
|
| 324 |
-
for ell in line_tuples
|
| 325 |
-
)
|
| 326 |
-
|
| 327 |
-
# Note: Binary variables for bulk payment are now created inline in the cost calculation
|
| 328 |
-
|
| 329 |
-
# --- Objective: Minimize total labor cost (wages) ---
|
| 330 |
-
# Employee-centric approach: calculate wages based on actual employees and their hours
|
| 331 |
-
PAYMENT_MODE_CONFIG = get_payment_mode_config() # Dynamic call
|
| 332 |
-
print(f"Payment mode configuration: {PAYMENT_MODE_CONFIG}")
|
| 333 |
-
|
| 334 |
-
# Build cost terms based on payment mode
|
| 335 |
-
cost_terms = []
|
| 336 |
-
|
| 337 |
-
for e in employee_type_list:
|
| 338 |
-
for s in active_shift_list:
|
| 339 |
-
for t in date_span_list:
|
| 340 |
-
payment_mode = PAYMENT_MODE_CONFIG.get(s, "partial") # Default to partial if not specified
|
| 341 |
-
|
| 342 |
-
if payment_mode == "partial":
|
| 343 |
-
# Partial payment: pay for actual person-hours worked
|
| 344 |
-
# Cost = hourly_rate Γ total_person_hours
|
| 345 |
-
# Example: $20/hr Γ 40 person-hours = $800
|
| 346 |
-
cost_terms.append(cost[e][s] * EMPLOYEE_HOURS[e, s, t])
|
| 347 |
-
|
| 348 |
-
elif payment_mode == "bulk":
|
| 349 |
-
# Bulk payment: if ANY work happens in shift, pay ALL working employees for FULL shift
|
| 350 |
-
# We need to know: did employee type e work at all in shift s on day t?
|
| 351 |
-
|
| 352 |
-
# Create binary: 1 if employee type e worked in this shift
|
| 353 |
-
work_in_shift = solver.BoolVar(f"work_{e}_s{s}_d{t}")
|
| 354 |
-
|
| 355 |
-
# Link binary to work hours
|
| 356 |
-
# If EMPLOYEE_HOURS > 0, then work_in_shift = 1
|
| 357 |
-
# If EMPLOYEE_HOURS = 0, then work_in_shift = 0
|
| 358 |
-
max_possible_hours = Hmax_s[s] * max_employee_type_day[e][t]
|
| 359 |
-
solver.Add(EMPLOYEE_HOURS[e, s, t] <= max_possible_hours * work_in_shift)
|
| 360 |
-
solver.Add(work_in_shift * 0.001 <= EMPLOYEE_HOURS[e, s, t])
|
| 361 |
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
|
| 400 |
-
|
| 401 |
-
solver.Add(total_production >= demand)
|
| 402 |
|
| 403 |
-
#
|
| 404 |
-
|
|
|
|
|
|
|
| 405 |
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
|
|
|
|
|
|
| 418 |
for ell in line_tuples:
|
| 419 |
-
allowed = (ell[0] == req_lt) and (req_total <= max_workers_line.get(ell[0], 1e9))
|
| 420 |
for s in active_shift_list:
|
| 421 |
for t in date_span_list:
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
solver.Add(Hours[p, ell, s, t]
|
| 425 |
-
solver.Add(Units[p, ell, s, t] == 0)
|
| 426 |
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
for s in active_shift_list:
|
| 431 |
for t in date_span_list:
|
| 432 |
-
#
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
solver.Add(
|
| 438 |
-
Units[p, ell, s, t] <= speed * Hours[p, ell, s, t]
|
| 439 |
-
)
|
| 440 |
-
# Lower bound: if working, must produce (prevent phantom work)
|
| 441 |
-
solver.Add(
|
| 442 |
-
Units[p, ell, s, t] >= speed * Hours[p, ell, s, t]
|
| 443 |
-
)
|
| 444 |
-
else:
|
| 445 |
-
# Default speed if not found
|
| 446 |
-
default_speed = 800 / 7.5 # units per hour
|
| 447 |
-
print(f"Warning: No speed data for product {p}, using default {default_speed:.1f} per hour")
|
| 448 |
-
# Upper bound: units cannot exceed capacity
|
| 449 |
-
solver.Add(
|
| 450 |
-
Units[p, ell, s, t] <= default_speed * Hours[p, ell, s, t]
|
| 451 |
-
)
|
| 452 |
-
# Lower bound: if working, must produce (prevent phantom work)
|
| 453 |
-
solver.Add(
|
| 454 |
-
Units[p, ell, s, t] >= default_speed * Hours[p, ell, s, t]
|
| 455 |
-
)
|
| 456 |
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
for s in active_shift_list:
|
| 460 |
-
for t in date_span_list:
|
| 461 |
-
# No idle employee constraints - employees are only counted when working
|
| 462 |
-
solver.Add(
|
| 463 |
-
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, s, t] for p in sorted_product_list for ell in line_tuples)
|
| 464 |
-
<= Hmax_s[s] * max_employee_type_day[e][t]
|
| 465 |
-
)
|
| 466 |
-
|
| 467 |
-
# 6) Per-shift staffing capacity by type: link employee count to actual work hours
|
| 468 |
-
# This constraint ensures EMPLOYEE_COUNT[e,s,t] represents the actual number of employees needed in each shift
|
| 469 |
-
for e in employee_type_list:
|
| 470 |
-
for s in active_shift_list:
|
| 471 |
-
for t in date_span_list:
|
| 472 |
-
# Total person-hours worked by employee type e in shift s on day t
|
| 473 |
-
total_person_hours_in_shift = solver.Sum(
|
| 474 |
-
TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, s, t]
|
| 475 |
-
for p in sorted_product_list
|
| 476 |
-
for ell in line_tuples
|
| 477 |
-
)
|
| 478 |
-
|
| 479 |
-
# Employee count must be sufficient to cover the work in this shift
|
| 480 |
-
# If employees work H person-hours total and each can work max M hours/shift,
|
| 481 |
-
# then we need at least ceil(H/M) employees
|
| 482 |
-
# Constraint: employee_count Γ max_hours_per_shift >= total_person_hours_in_shift
|
| 483 |
-
solver.Add(EMPLOYEE_COUNT[e, s, t] * Hmax_s[s] >= total_person_hours_in_shift)
|
| 484 |
-
|
| 485 |
-
# 7) Shift ordering constraints (only apply if shifts are available)
|
| 486 |
-
# Evening shift after regular shift
|
| 487 |
-
if ShiftType.EVENING in active_shift_list and ShiftType.REGULAR in active_shift_list: # Only if both shifts are available
|
| 488 |
for e in employee_type_list:
|
| 489 |
-
for
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
regular_capacity = max_employee_type_day[e][t]
|
| 504 |
-
|
| 505 |
-
# Total regular shift usage for this employee type and day
|
| 506 |
-
regular_usage = solver.Sum(
|
| 507 |
-
TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, ShiftType.REGULAR, t]
|
| 508 |
-
for p in sorted_product_list for ell in line_tuples
|
| 509 |
-
)
|
| 510 |
-
|
| 511 |
-
# Total overtime usage for this employee type and day
|
| 512 |
-
overtime_usage = solver.Sum(
|
| 513 |
-
TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, ShiftType.OVERTIME, t]
|
| 514 |
-
for p in sorted_product_list for ell in line_tuples
|
| 515 |
-
)
|
| 516 |
-
|
| 517 |
-
# Create binary variable: 1 if using overtime, 0 otherwise
|
| 518 |
-
using_overtime = solver.IntVar(0, 1, f'using_overtime_{e}_{t}')
|
| 519 |
-
|
| 520 |
-
# If using overtime, regular capacity must be utilized significantly
|
| 521 |
-
# Regular usage must be at least 90% of capacity to allow overtime
|
| 522 |
-
min_regular_for_overtime = int(0.9 * regular_capacity)
|
| 523 |
-
|
| 524 |
-
# Constraint 1: Can only use overtime if regular usage is high
|
| 525 |
-
solver.Add(regular_usage >= min_regular_for_overtime * using_overtime)
|
| 526 |
-
|
| 527 |
-
# Constraint 2: If any overtime is used, set the binary variable
|
| 528 |
-
solver.Add(overtime_usage <= regular_capacity * using_overtime)
|
| 529 |
-
|
| 530 |
-
overtime_constraints_added = len(employee_type_list) * len(date_span_list) * 2 # 2 constraints per employee type per day
|
| 531 |
-
print(f"[OVERTIME] Added {overtime_constraints_added} constraints ensuring overtime only when regular shifts are at 90%+ capacity")
|
| 532 |
-
|
| 533 |
-
# 7.5) Bulk payment linking constraints are now handled inline in the cost calculation
|
| 534 |
-
|
| 535 |
-
# 7.6) *** FIXED MINIMUM UNICEF EMPLOYEES CONSTRAINT ***
|
| 536 |
-
# Ensure minimum UNICEF fixed-term staff work in the REGULAR shift every day
|
| 537 |
-
# The minimum applies to the regular shift specifically (not overtime or evening)
|
| 538 |
-
if 'UNICEF Fixed term' in employee_type_list and FIXED_MIN_UNICEF_PER_DAY > 0:
|
| 539 |
-
if ShiftType.REGULAR in active_shift_list:
|
| 540 |
-
print(f"\n[FIXED STAFFING] Adding constraint for minimum {FIXED_MIN_UNICEF_PER_DAY} UNICEF employees in REGULAR shift per day...")
|
| 541 |
-
for t in date_span_list:
|
| 542 |
-
# At least FIXED_MIN_UNICEF_PER_DAY employees must work in the regular shift each day
|
| 543 |
-
solver.Add(
|
| 544 |
-
EMPLOYEE_COUNT['UNICEF Fixed term', ShiftType.REGULAR, t] >= FIXED_MIN_UNICEF_PER_DAY
|
| 545 |
-
)
|
| 546 |
-
print(f"[FIXED STAFFING] Added {len(date_span_list)} constraints ensuring >= {FIXED_MIN_UNICEF_PER_DAY} UNICEF employees in regular shift per day")
|
| 547 |
-
else:
|
| 548 |
-
print(f"\n[FIXED STAFFING] Warning: Regular shift not available, cannot enforce minimum UNICEF staffing")
|
| 549 |
-
|
| 550 |
-
# 8) *** HIERARCHY DEPENDENCY CONSTRAINTS ***
|
| 551 |
-
# For subkits with prepack dependencies: dependencies should be produced before or same time
|
| 552 |
-
print("\n[HIERARCHY] Adding dependency constraints...")
|
| 553 |
-
dependency_constraints_added = 0
|
| 554 |
-
|
| 555 |
-
for p in sorted_product_list:
|
| 556 |
-
dependencies = KIT_DEPENDENCIES.get(p, [])
|
| 557 |
-
if dependencies:
|
| 558 |
-
# Get the level of the current product
|
| 559 |
-
p_level = KIT_LEVELS.get(p, 2)
|
| 560 |
|
| 561 |
-
for
|
| 562 |
-
|
| 563 |
-
#
|
| 564 |
-
|
| 565 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
)
|
| 567 |
-
|
| 568 |
-
|
|
|
|
|
|
|
|
|
|
| 569 |
)
|
| 570 |
|
| 571 |
-
#
|
| 572 |
-
solver.
|
| 573 |
-
dependency_constraints_added += 1
|
| 574 |
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
for s in active_shift_list:
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
'units': Units[p, ell, s, t].solution_value(),
|
| 613 |
-
})
|
| 614 |
-
result['run_schedule'] = schedule
|
| 615 |
-
|
| 616 |
-
# Implied headcount by type/shift/day (ceil)
|
| 617 |
-
headcount = []
|
| 618 |
-
for e in employee_type_list:
|
| 619 |
-
for s in active_shift_list:
|
| 620 |
for t in date_span_list:
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
for t in date_span_list:
|
| 642 |
-
|
|
|
|
| 643 |
used_hours = sum(TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, s, t].solution_value()
|
| 644 |
-
|
| 645 |
-
avg_hours_per_employee = used_hours /
|
| 646 |
-
if
|
| 647 |
-
|
| 648 |
'emp_type': e,
|
| 649 |
-
'shift': s,
|
| 650 |
'day': t,
|
| 651 |
-
'employee_count':
|
| 652 |
'total_person_hours': used_hours,
|
| 653 |
'avg_hours_per_employee': avg_hours_per_employee,
|
| 654 |
'available': max_employee_type_day[e][t]
|
| 655 |
})
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
print(
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
print("\n--- Total person-hours by type/day ---")
|
| 700 |
-
for row in ph_by_day:
|
| 701 |
-
print(f"{row['emp_type']}, date_span_list{row['day']}: used={row['used_person_hours']:.1f} "
|
| 702 |
-
f"(cap {row['cap_person_hours']})")
|
| 703 |
-
|
| 704 |
-
print("\n--- Actual employee count by type/shift/day ---")
|
| 705 |
-
for row in employee_count_by_shift:
|
| 706 |
-
shift_name = ShiftType.get_name(row['shift'])
|
| 707 |
-
print(f"{row['emp_type']}, {shift_name}, date_span_list{row['day']}: "
|
| 708 |
-
f"count={row['employee_count']} employees, "
|
| 709 |
-
f"total_hours={row['total_person_hours']:.1f}h, "
|
| 710 |
-
f"avg={row['avg_hours_per_employee']:.1f}h/employee")
|
| 711 |
-
|
| 712 |
-
print("\n--- Daily employee totals by type/day (sum across shifts) ---")
|
| 713 |
-
for row in employee_count_by_day:
|
| 714 |
-
print(f"{row['emp_type']}, date_span_list{row['day']}: "
|
| 715 |
-
f"count={row['employee_count']} employees total, "
|
| 716 |
-
f"total_hours={row['total_person_hours']:.1f}h, "
|
| 717 |
-
f"avg={row['avg_hours_per_employee']:.1f}h/employee "
|
| 718 |
-
f"(available: {row['available']})")
|
| 719 |
|
| 720 |
-
|
| 721 |
|
| 722 |
-
|
| 723 |
|
| 724 |
|
| 725 |
if __name__ == "__main__":
|
| 726 |
-
|
|
|
|
|
|
| 9 |
|
| 10 |
from ortools.linear_solver import pywraplp
|
| 11 |
from math import ceil
|
| 12 |
+
import datetime
|
| 13 |
from src.config.constants import ShiftType, LineType, KitLevel
|
| 14 |
|
| 15 |
# ---- config import ----
|
| 16 |
+
# Import constants and other modules directly
|
| 17 |
+
from src.config.constants import ShiftType, LineType, DefaultConfig
|
| 18 |
+
import src.preprocess.extract as extract
|
| 19 |
+
from src.preprocess.hierarchy_parser import sort_products_by_hierarchy
|
| 20 |
+
|
| 21 |
+
class Optimizer:
|
| 22 |
+
"""Workforce optimization class that handles all configuration and optimization logic"""
|
| 23 |
+
|
| 24 |
+
def __init__(self):
|
| 25 |
+
"""Initialize optimizer with session state configuration"""
|
| 26 |
+
self.load_session_state_config()
|
| 27 |
+
self.load_data()
|
| 28 |
+
|
| 29 |
+
def load_session_state_config(self):
|
| 30 |
+
"""Load all configuration from session state"""
|
| 31 |
+
import streamlit as st
|
| 32 |
+
import datetime as dt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
# Date configuration
|
| 35 |
+
self.start_date = st.session_state.start_date
|
| 36 |
+
self.planning_days = st.session_state.planning_days
|
| 37 |
+
self.start_datetime = dt.datetime.combine(self.start_date, dt.datetime.min.time())
|
| 38 |
+
self.end_date = self.start_datetime + dt.timedelta(days=self.planning_days - 1)
|
| 39 |
+
self.date_span = list(range(1, self.planning_days + 1))
|
| 40 |
+
|
| 41 |
+
# Employee and shift configuration
|
| 42 |
+
self.employee_type_list = list(st.session_state.selected_employee_types)
|
| 43 |
+
self.active_shift_list = sorted(list(st.session_state.selected_shifts))
|
| 44 |
+
|
| 45 |
+
print("\n[DEBUG] From session_state.selected_employee_types:")
|
| 46 |
+
for emp in self.employee_type_list:
|
| 47 |
+
print(f" - '{emp}' (len={len(emp)}, repr={repr(emp)})")
|
| 48 |
+
|
| 49 |
+
# Working hours configuration
|
| 50 |
+
self.max_hour_per_person_per_day = st.session_state.max_hour_per_person_per_day
|
| 51 |
+
self.max_hours_shift = {
|
| 52 |
+
ShiftType.REGULAR: st.session_state.max_hours_shift_1,
|
| 53 |
+
ShiftType.EVENING: st.session_state.max_hours_shift_2,
|
| 54 |
+
ShiftType.OVERTIME: st.session_state.max_hours_shift_3
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
# Workforce limits
|
| 58 |
+
self.max_employee_per_type_on_day = st.session_state.max_employee_per_type_on_day
|
| 59 |
+
|
| 60 |
+
# Operations configuration
|
| 61 |
+
self.line_counts = st.session_state.line_counts
|
| 62 |
+
self.max_parallel_workers = {
|
| 63 |
+
LineType.LONG_LINE: st.session_state.max_parallel_workers_long_line,
|
| 64 |
+
LineType.MINI_LOAD: st.session_state.max_parallel_workers_mini_load
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
# Cost configuration
|
| 68 |
+
self.cost_list_per_emp_shift = st.session_state.cost_list_per_emp_shift
|
| 69 |
+
|
| 70 |
+
# Payment mode configuration
|
| 71 |
+
self.payment_mode_config = st.session_state.payment_mode_config
|
| 72 |
+
|
| 73 |
+
# Fixed staffing requirements
|
| 74 |
+
self.fixed_min_unicef_per_day = st.session_state.fixed_min_unicef_per_day
|
| 75 |
+
|
| 76 |
+
print("β
Session state configuration loaded successfully")
|
| 77 |
+
|
| 78 |
+
def load_data(self):
|
| 79 |
+
"""Load all required data from files"""
|
| 80 |
+
# Load hierarchy data
|
| 81 |
+
try:
|
| 82 |
+
kit_levels, dependencies, priority_order = extract.get_production_order_data()
|
| 83 |
+
self.kit_levels = kit_levels
|
| 84 |
+
self.kit_dependencies = dependencies
|
| 85 |
+
self.production_priority_order = priority_order
|
| 86 |
+
except:
|
| 87 |
+
self.kit_levels = {}
|
| 88 |
+
self.kit_dependencies = {}
|
| 89 |
+
self.production_priority_order = []
|
| 90 |
+
|
| 91 |
+
# Load kit line match data
|
| 92 |
+
try:
|
| 93 |
+
kit_line_match = extract.read_kit_line_match_data()
|
| 94 |
+
kit_line_match_dict = kit_line_match.set_index("kit_name")["line_type"].to_dict()
|
| 95 |
+
|
| 96 |
+
# Create line name to ID mapping
|
| 97 |
+
line_name_to_id = {
|
| 98 |
+
"long line": LineType.LONG_LINE,
|
| 99 |
+
"mini load": LineType.MINI_LOAD
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
# Convert line names to IDs
|
| 103 |
+
self.kit_line_match_dict = {}
|
| 104 |
+
for kit_name, line_name in kit_line_match_dict.items():
|
| 105 |
+
self.kit_line_match_dict[kit_name] = line_name_to_id.get(line_name.lower(), line_name)
|
| 106 |
+
except:
|
| 107 |
+
self.kit_line_match_dict = {}
|
| 108 |
+
|
| 109 |
+
# Load product and demand data
|
| 110 |
+
try:
|
| 111 |
+
from src.demand_filtering import DemandFilter
|
| 112 |
+
filter_instance = DemandFilter()
|
| 113 |
+
filter_instance.load_data(force_reload=True)
|
| 114 |
+
self.product_list = filter_instance.get_filtered_product_list()
|
| 115 |
+
self.demand_dictionary = filter_instance.get_filtered_demand_dictionary()
|
| 116 |
+
except:
|
| 117 |
+
self.product_list = []
|
| 118 |
+
self.demand_dictionary = {}
|
| 119 |
+
|
| 120 |
+
# Load team requirements
|
| 121 |
+
try:
|
| 122 |
+
print("\n[DEBUG] Loading team requirements from Kits Calculation...")
|
| 123 |
+
kits_df = extract.read_personnel_requirement_data()
|
| 124 |
+
print(f"[DEBUG] Loaded kits_df with {len(kits_df)} rows")
|
| 125 |
+
print(f"[DEBUG] Columns: {list(kits_df.columns)}")
|
| 126 |
+
|
| 127 |
+
# Initialize team requirements dictionary
|
| 128 |
+
self.team_req_per_product = {
|
| 129 |
+
"UNICEF Fixed term": {},
|
| 130 |
+
"Humanizer": {}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
# Process each product in the product list
|
| 134 |
+
for product in self.product_list:
|
| 135 |
+
product_data = kits_df[kits_df['Kit'] == product]
|
| 136 |
+
if not product_data.empty:
|
| 137 |
+
# Extract Humanizer and UNICEF staff requirements
|
| 138 |
+
humanizer_req = product_data["Humanizer"].iloc[0]
|
| 139 |
+
unicef_req = product_data["UNICEF staff"].iloc[0]
|
| 140 |
+
|
| 141 |
+
# Convert to int (data is already cleaned in extract function)
|
| 142 |
+
self.team_req_per_product["Humanizer"][product] = int(humanizer_req)
|
| 143 |
+
self.team_req_per_product["UNICEF Fixed term"][product] = int(unicef_req)
|
| 144 |
+
else:
|
| 145 |
+
print(f"[WARN] Product {product} not found in Kits Calculation, setting requirements to 0")
|
| 146 |
+
self.team_req_per_product["Humanizer"][product] = 0
|
| 147 |
+
self.team_req_per_product["UNICEF Fixed term"][product] = 0
|
| 148 |
+
|
| 149 |
+
print(f"\n[DEBUG] team_req_per_product keys after loading:")
|
| 150 |
+
for key in self.team_req_per_product.keys():
|
| 151 |
+
product_count = len(self.team_req_per_product[key])
|
| 152 |
+
print(f" - '{key}' (len={len(key)}, {product_count} products)")
|
| 153 |
|
| 154 |
+
except Exception as e:
|
| 155 |
+
print(f"[ERROR] Failed to load team requirements: {e}")
|
| 156 |
+
import traceback
|
| 157 |
+
traceback.print_exc()
|
| 158 |
+
self.team_req_per_product = {}
|
| 159 |
|
| 160 |
+
# Load product speed data
|
| 161 |
+
try:
|
| 162 |
+
self.per_product_speed = extract.read_package_speed_data()
|
| 163 |
+
except:
|
| 164 |
+
self.per_product_speed = {}
|
| 165 |
|
| 166 |
+
print("β
All data loaded successfully")
|
| 167 |
+
|
| 168 |
+
def build_lines(self):
|
| 169 |
+
"""Build line instances from session state configuration"""
|
| 170 |
+
line_tuples = []
|
| 171 |
|
| 172 |
+
try:
|
| 173 |
+
import streamlit as st
|
| 174 |
+
# Get selected line types from Data Selection tab
|
| 175 |
+
selected_lines = st.session_state.selected_lines
|
| 176 |
+
# Get line counts from Operations tab
|
| 177 |
+
line_counts = st.session_state.line_counts
|
| 178 |
+
|
| 179 |
+
print(f"Using lines from session state - selected: {selected_lines}, counts: {line_counts}")
|
| 180 |
+
for line_type in selected_lines:
|
| 181 |
+
count = line_counts.get(line_type, 0)
|
| 182 |
+
for i in range(1, count + 1):
|
| 183 |
+
line_tuples.append((line_type, i))
|
| 184 |
+
|
| 185 |
+
return line_tuples
|
| 186 |
+
|
| 187 |
+
except Exception as e:
|
| 188 |
+
print(f"Could not get line config from session state: {e}")
|
| 189 |
+
# Fallback: Use default values
|
| 190 |
+
print("Falling back to default line configuration")
|
| 191 |
+
default_selected_lines = [LineType.LONG_LINE, LineType.MINI_LOAD]
|
| 192 |
+
default_line_counts = {
|
| 193 |
+
LineType.LONG_LINE: DefaultConfig.LINE_COUNT_LONG_LINE,
|
| 194 |
+
LineType.MINI_LOAD: DefaultConfig.LINE_COUNT_MINI_LOAD
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
for line_type in default_selected_lines:
|
| 198 |
+
count = default_line_counts.get(line_type, 0)
|
| 199 |
+
for i in range(1, count + 1):
|
| 200 |
+
line_tuples.append((line_type, i))
|
| 201 |
+
|
| 202 |
+
return line_tuples
|
| 203 |
+
|
| 204 |
+
def run_optimization(self):
|
| 205 |
+
"""Run the main optimization algorithm"""
|
| 206 |
+
# *** CRITICAL: Load fresh data to reflect current Streamlit configs ***
|
| 207 |
+
print("\n" + "="*60)
|
| 208 |
+
print("π LOADING FRESH DATA FOR OPTIMIZATION")
|
| 209 |
+
print("="*60)
|
| 210 |
+
|
| 211 |
+
print(f"π¦ LOADED PRODUCTS: {len(self.product_list)} products")
|
| 212 |
+
print(f"π LOADED DEMAND: {sum(self.demand_dictionary.values())} total units")
|
| 213 |
+
print(f"π₯ LOADED TEAM REQUIREMENTS: {len(self.team_req_per_product)} employee types")
|
| 214 |
+
|
| 215 |
+
# Debug: Print team requirements keys
|
| 216 |
+
print("\n[DEBUG] team_req_per_product employee types:")
|
| 217 |
+
for emp_type in self.team_req_per_product.keys():
|
| 218 |
+
print(f" - '{emp_type}'")
|
| 219 |
+
|
| 220 |
+
print("\n[DEBUG] self.employee_type_list:")
|
| 221 |
+
for emp_type in self.employee_type_list:
|
| 222 |
+
print(f" - '{emp_type}'")
|
| 223 |
+
|
| 224 |
+
# Build ACTIVE schedule for fresh product list
|
| 225 |
+
ACTIVE = {t: {p: 1 for p in self.product_list} for t in self.date_span}
|
| 226 |
+
|
| 227 |
+
# --- Sets ---
|
| 228 |
+
date_span_list = list(self.date_span)
|
| 229 |
+
employee_type_list = self.employee_type_list
|
| 230 |
+
active_shift_list = self.active_shift_list
|
| 231 |
+
print(f"\n[DEBUG] employee_type_list: {employee_type_list}")
|
| 232 |
+
print(f"[DEBUG] active_shift_list: {active_shift_list}")
|
| 233 |
+
|
| 234 |
+
# *** HIERARCHY SORTING: Sort products by production priority ***
|
| 235 |
+
print("\n" + "="*60)
|
| 236 |
+
print("π APPLYING HIERARCHY-BASED PRODUCTION ORDERING")
|
| 237 |
+
print("="*60)
|
| 238 |
+
sorted_product_list = sort_products_by_hierarchy(list(self.product_list), self.kit_levels, self.kit_dependencies)
|
| 239 |
+
|
| 240 |
+
line_tuples = self.build_lines()
|
| 241 |
+
print("Lines", line_tuples)
|
| 242 |
+
|
| 243 |
+
print("PER_PRODUCT_SPEED", self.per_product_speed)
|
| 244 |
+
|
| 245 |
+
# --- Short aliases for parameters ---
|
| 246 |
+
print("\n[DEBUG] Creating variable aliases...")
|
| 247 |
+
Hmax_s = dict(self.max_hours_shift) # per-shift hours
|
| 248 |
+
Hmax_daily = self.max_hour_per_person_per_day
|
| 249 |
+
max_workers_line = dict(self.max_parallel_workers) # per line type
|
| 250 |
+
max_employee_type_day = self.max_employee_per_type_on_day # {emp_type:{t:headcount}}
|
| 251 |
+
cost = self.cost_list_per_emp_shift # {emp_type:{shift:cost}}
|
| 252 |
+
|
| 253 |
+
# Create aliases for data dictionaries
|
| 254 |
+
TEAM_REQ_PER_PRODUCT = self.team_req_per_product
|
| 255 |
+
DEMAND_DICTIONARY = self.demand_dictionary
|
| 256 |
+
KIT_LINE_MATCH_DICT = self.kit_line_match_dict
|
| 257 |
+
KIT_LEVELS = self.kit_levels
|
| 258 |
+
KIT_DEPENDENCIES = self.kit_dependencies
|
| 259 |
+
PER_PRODUCT_SPEED = self.per_product_speed
|
| 260 |
+
FIXED_MIN_UNICEF_PER_DAY = self.fixed_min_unicef_per_day
|
| 261 |
+
PAYMENT_MODE_CONFIG = self.payment_mode_config
|
| 262 |
+
|
| 263 |
+
# Mock missing config variables (if they exist in config, they'll be overridden)
|
| 264 |
+
EVENING_SHIFT_MODE = "normal"
|
| 265 |
+
EVENING_SHIFT_DEMAND_THRESHOLD = 0.9
|
| 266 |
+
|
| 267 |
+
print(f"[DEBUG] TEAM_REQ_PER_PRODUCT has {len(TEAM_REQ_PER_PRODUCT)} employee types")
|
| 268 |
+
print(f"[DEBUG] employee_type_list has {len(employee_type_list)} types")
|
| 269 |
+
|
| 270 |
+
# --- Feasibility quick checks ---
|
| 271 |
+
print("\n[DEBUG] Starting feasibility checks...")
|
| 272 |
+
|
| 273 |
+
# 1) If team size is greater than max_workers_line, block the product-line type combination
|
| 274 |
+
for i, p in enumerate(sorted_product_list):
|
| 275 |
+
print(f"[DEBUG] Checking product {i+1}/{len(sorted_product_list)}: {p}")
|
| 276 |
+
|
| 277 |
+
# Check if all employee types exist in TEAM_REQ_PER_PRODUCT
|
| 278 |
+
for e in employee_type_list:
|
| 279 |
+
if e not in TEAM_REQ_PER_PRODUCT:
|
| 280 |
+
print(f"[ERROR] Employee type '{e}' not found in TEAM_REQ_PER_PRODUCT!")
|
| 281 |
+
print(f"[ERROR] Available keys: {list(TEAM_REQ_PER_PRODUCT.keys())}")
|
| 282 |
+
raise KeyError(f"Employee type '{e}' not in team requirements data")
|
| 283 |
+
if p not in TEAM_REQ_PER_PRODUCT[e]:
|
| 284 |
+
print(f"[ERROR] Product '{p}' not found in TEAM_REQ_PER_PRODUCT['{e}']!")
|
| 285 |
+
raise KeyError(f"Product '{p}' not in team requirements for employee type '{e}'")
|
| 286 |
+
|
| 287 |
+
req_total = sum(TEAM_REQ_PER_PRODUCT[e][p] for e in employee_type_list)
|
| 288 |
+
print(f"[DEBUG] req_total: {req_total}")
|
| 289 |
+
lt = KIT_LINE_MATCH_DICT.get(p, 6) # Default to long line (6) if not found
|
| 290 |
+
if p not in KIT_LINE_MATCH_DICT:
|
| 291 |
+
print(f"[WARN] Product {p}: No line type mapping found, defaulting to long line (6)")
|
| 292 |
+
if req_total > max_workers_line.get(lt, 1e9):
|
| 293 |
+
print(f"[WARN] Product {p}: team size {req_total} > MAX_PARALLEL_WORKERS[{lt}] "
|
| 294 |
+
f"= {max_workers_line.get(lt)}. Blocked.")
|
| 295 |
+
|
| 296 |
+
# 2) Check if demand can be met without evening shift (only if in normal mode)
|
| 297 |
+
if EVENING_SHIFT_MODE == "normal":
|
| 298 |
+
total_demand = sum(DEMAND_DICTIONARY.get(p, 0) for p in sorted_product_list)
|
| 299 |
+
|
| 300 |
+
# Calculate maximum capacity with regular + overtime shifts only
|
| 301 |
+
regular_overtime_shifts = [s for s in active_shift_list if s in ShiftType.REGULAR_AND_OVERTIME]
|
| 302 |
+
max_capacity = 0
|
| 303 |
+
|
| 304 |
+
for p in sorted_product_list:
|
| 305 |
+
if p in PER_PRODUCT_SPEED:
|
| 306 |
+
product_speed = PER_PRODUCT_SPEED[p] # units per hour
|
| 307 |
+
# Calculate max hours available for this product across all lines and shifts
|
| 308 |
+
max_hours_per_product = 0
|
| 309 |
+
for ell in line_tuples:
|
| 310 |
+
for s in regular_overtime_shifts:
|
| 311 |
+
for t in date_span_list:
|
| 312 |
+
max_hours_per_product += Hmax_s[s]
|
| 313 |
+
|
| 314 |
+
max_capacity += product_speed * max_hours_per_product
|
| 315 |
+
|
| 316 |
+
capacity_ratio = max_capacity / total_demand if total_demand > 0 else float('inf')
|
| 317 |
+
|
| 318 |
+
print(f"[CAPACITY CHECK] Total demand: {total_demand}")
|
| 319 |
+
print(f"[CAPACITY CHECK] Max capacity (Regular + Overtime): {max_capacity:.1f}")
|
| 320 |
+
print(f"[CAPACITY CHECK] Capacity ratio: {capacity_ratio:.2f}")
|
| 321 |
+
|
| 322 |
+
if capacity_ratio < EVENING_SHIFT_DEMAND_THRESHOLD:
|
| 323 |
+
print(f"\nπ¨ [ALERT] DEMAND TOO HIGH!")
|
| 324 |
+
print(f" Current capacity can only meet {capacity_ratio*100:.1f}% of demand")
|
| 325 |
+
print(f" Threshold: {EVENING_SHIFT_DEMAND_THRESHOLD*100:.1f}%")
|
| 326 |
+
print(f" RECOMMENDATION: Change EVENING_SHIFT_MODE to 'activate_evening' to enable evening shift")
|
| 327 |
+
print(f" This will add shift 3 to increase capacity\n")
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
# --- Solver ---
|
| 331 |
+
solver = pywraplp.Solver.CreateSolver('CBC')
|
| 332 |
+
if not solver:
|
| 333 |
+
raise RuntimeError("CBC solver not found.")
|
| 334 |
+
INF = solver.infinity()
|
| 335 |
+
|
| 336 |
+
# --- Variables ---
|
| 337 |
+
# Assignment[p,ell,s,t] β {0,1}: 1 if product p runs on (line,shift,day)
|
| 338 |
+
Assignment, Hours, Units = {}, {}, {} # Hours: run hours, Units: production units
|
| 339 |
+
for p in sorted_product_list:
|
| 340 |
+
for ell in line_tuples: # ell = (line_type_id, idx)
|
| 341 |
+
for s in active_shift_list:
|
| 342 |
+
for t in date_span_list:
|
| 343 |
+
#Is product p assigned to run on line ell, during shift s, on day t?
|
| 344 |
+
Assignment[p, ell, s, t] = solver.BoolVar(f"Z_{p}_{ell[0]}_{ell[1]}_s{s}_d{t}")
|
| 345 |
+
#How many hours does product p run on line ell, during shift s, on day t?
|
| 346 |
+
Hours[p, ell, s, t] = solver.NumVar(0, Hmax_s[s], f"T_{p}_{ell[0]}_{ell[1]}_s{s}_d{t}")
|
| 347 |
+
#How many units does product p run on line ell, during shift s, on day t?
|
| 348 |
+
Units[p, ell, s, t] = solver.NumVar(0, INF, f"U_{p}_{ell[0]}_{ell[1]}_s{s}_d{t}")
|
| 349 |
+
|
| 350 |
+
# Note: IDLE variables removed - we only track employees actually working on production
|
| 351 |
+
|
| 352 |
+
# Variable to track actual number of employees of each type working each shift each day
|
| 353 |
+
# This represents how many distinct employees of type e are working in shift s on day t
|
| 354 |
+
EMPLOYEE_COUNT = {}
|
| 355 |
+
for e in employee_type_list:
|
| 356 |
+
for s in active_shift_list:
|
| 357 |
+
for t in date_span_list:
|
| 358 |
+
# Note: Minimum staffing is per day, not per shift
|
| 359 |
+
# We'll handle the daily minimum constraint separately
|
| 360 |
+
max_count = max_employee_type_day.get(e, {}).get(t, 100)
|
| 361 |
+
EMPLOYEE_COUNT[e, s, t] = solver.IntVar(
|
| 362 |
+
0, # No minimum per shift (daily minimum handled separately)
|
| 363 |
+
max_count,
|
| 364 |
+
f"EmpCount_{e}_s{s}_day{t}"
|
| 365 |
+
)
|
| 366 |
|
| 367 |
+
# Track total person-hours worked by each employee type per shift per day
|
| 368 |
+
# This is needed for employee-centric wage calculation
|
| 369 |
+
EMPLOYEE_HOURS = {}
|
| 370 |
+
for e in employee_type_list:
|
| 371 |
+
for s in active_shift_list:
|
| 372 |
+
for t in date_span_list:
|
| 373 |
+
# Sum of all work hours for employee type e in shift s on day t
|
| 374 |
+
# This represents total person-hours (e.g., 5 employees Γ 8 hours = 40 person-hours)
|
| 375 |
+
EMPLOYEE_HOURS[e, s, t] = solver.Sum(
|
| 376 |
+
TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, s, t]
|
| 377 |
+
for p in sorted_product_list
|
| 378 |
+
for ell in line_tuples
|
| 379 |
+
)
|
| 380 |
|
| 381 |
+
# Note: Binary variables for bulk payment are now created inline in the cost calculation
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
|
| 383 |
+
# --- Objective: Minimize total labor cost (wages) ---
|
| 384 |
+
# Employee-centric approach: calculate wages based on actual employees and their hours
|
| 385 |
+
print(f"\n[DEBUG] Payment mode configuration: {PAYMENT_MODE_CONFIG}")
|
| 386 |
+
|
| 387 |
+
# Build cost terms based on payment mode
|
| 388 |
+
cost_terms = []
|
| 389 |
+
|
| 390 |
+
for e in employee_type_list:
|
| 391 |
for s in active_shift_list:
|
| 392 |
for t in date_span_list:
|
| 393 |
+
payment_mode = PAYMENT_MODE_CONFIG.get(s, "partial") # Default to partial if not specified
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
|
| 395 |
+
if payment_mode == "partial":
|
| 396 |
+
# Partial payment: pay for actual person-hours worked
|
| 397 |
+
# Cost = hourly_rate Γ total_person_hours
|
| 398 |
+
# Example: $20/hr Γ 40 person-hours = $800
|
| 399 |
+
cost_terms.append(cost[e][s] * EMPLOYEE_HOURS[e, s, t])
|
| 400 |
|
| 401 |
+
elif payment_mode == "bulk":
|
| 402 |
+
# Bulk payment: if ANY work happens in shift, pay ALL working employees for FULL shift
|
| 403 |
+
# We need to know: did employee type e work at all in shift s on day t?
|
| 404 |
+
|
| 405 |
+
# Create binary: 1 if employee type e worked in this shift
|
| 406 |
+
work_in_shift = solver.BoolVar(f"work_{e}_s{s}_d{t}")
|
| 407 |
+
|
| 408 |
+
# Link binary to work hours
|
| 409 |
+
# If EMPLOYEE_HOURS > 0, then work_in_shift = 1
|
| 410 |
+
# If EMPLOYEE_HOURS = 0, then work_in_shift = 0
|
| 411 |
+
max_possible_hours = Hmax_s[s] * max_employee_type_day[e][t]
|
| 412 |
+
solver.Add(EMPLOYEE_HOURS[e, s, t] <= max_possible_hours * work_in_shift)
|
| 413 |
+
solver.Add(work_in_shift * 0.001 <= EMPLOYEE_HOURS[e, s, t])
|
| 414 |
+
|
| 415 |
+
# Calculate number of employees working in this shift
|
| 416 |
+
# This is approximately: ceil(EMPLOYEE_HOURS / Hmax_s[s])
|
| 417 |
+
# But we can use: employees_working_in_shift
|
| 418 |
+
# For simplicity, use EMPLOYEE_HOURS / Hmax_s[s] as continuous approximation
|
| 419 |
+
# Or better: create a variable for employees per shift
|
| 420 |
+
|
| 421 |
+
# Simpler approach: For bulk payment, assume if work happens,
|
| 422 |
+
# we need approximately EMPLOYEE_HOURS/Hmax_s[s] employees,
|
| 423 |
+
# and each gets paid for full shift
|
| 424 |
+
# Cost β (EMPLOYEE_HOURS / Hmax_s[s]) Γ Hmax_s[s] Γ hourly_rate = EMPLOYEE_HOURS Γ hourly_rate
|
| 425 |
+
# But that's the same as partial! The difference is we round up employees.
|
| 426 |
+
|
| 427 |
+
# Better approach: Create variable for employees working in this specific shift
|
| 428 |
+
employees_in_shift = solver.IntVar(0, max_employee_type_day[e][t], f"emp_{e}_s{s}_d{t}")
|
| 429 |
+
|
| 430 |
+
# Link employees_in_shift to work requirements
|
| 431 |
+
# If EMPLOYEE_HOURS requires N employees, then employees_in_shift >= ceil(N)
|
| 432 |
+
solver.Add(employees_in_shift * Hmax_s[s] >= EMPLOYEE_HOURS[e, s, t])
|
| 433 |
+
|
| 434 |
+
# Cost: pay each employee for full shift
|
| 435 |
+
cost_terms.append(cost[e][s] * Hmax_s[s] * employees_in_shift)
|
| 436 |
+
|
| 437 |
+
# Note: No idle employee costs - only pay for employees actually working
|
| 438 |
|
| 439 |
+
total_cost = solver.Sum(cost_terms)
|
|
|
|
| 440 |
|
| 441 |
+
# Objective: minimize total labor cost (wages)
|
| 442 |
+
# This finds the optimal production schedule (product order, line assignment, timing)
|
| 443 |
+
# that minimizes total wages while meeting all demand and capacity constraints
|
| 444 |
+
solver.Minimize(total_cost)
|
| 445 |
|
| 446 |
+
# --- Constraints ---
|
| 447 |
+
|
| 448 |
+
# 1) Weekly demand - must meet exactly (no over/under production)
|
| 449 |
+
for p in sorted_product_list:
|
| 450 |
+
total_production = solver.Sum(Units[p, ell, s, t] for ell in line_tuples for s in active_shift_list for t in date_span_list)
|
| 451 |
+
demand = DEMAND_DICTIONARY.get(p, 0)
|
| 452 |
+
|
| 453 |
+
# Must produce at least the demand
|
| 454 |
+
solver.Add(total_production >= demand)
|
| 455 |
+
|
| 456 |
+
# Must not produce more than the demand (prevent overproduction)
|
| 457 |
+
solver.Add(total_production <= demand)
|
| 458 |
+
|
| 459 |
+
# 2) One product per (line,shift,day) + time gating
|
| 460 |
for ell in line_tuples:
|
|
|
|
| 461 |
for s in active_shift_list:
|
| 462 |
for t in date_span_list:
|
| 463 |
+
solver.Add(solver.Sum(Assignment[p, ell, s, t] for p in sorted_product_list) <= 1)
|
| 464 |
+
for p in sorted_product_list:
|
| 465 |
+
solver.Add(Hours[p, ell, s, t] <= Hmax_s[s] * Assignment[p, ell, s, t])
|
|
|
|
| 466 |
|
| 467 |
+
# 3) Product-line type compatibility + (optional) activity by day
|
| 468 |
+
for p in sorted_product_list:
|
| 469 |
+
req_lt = KIT_LINE_MATCH_DICT.get(p, LineType.LONG_LINE) # Default to long line if not found
|
| 470 |
+
req_total = sum(TEAM_REQ_PER_PRODUCT[e][p] for e in employee_type_list)
|
| 471 |
+
for ell in line_tuples:
|
| 472 |
+
allowed = (ell[0] == req_lt) and (req_total <= max_workers_line.get(ell[0], 1e9))
|
| 473 |
+
for s in active_shift_list:
|
| 474 |
+
for t in date_span_list:
|
| 475 |
+
if ACTIVE[t][p] == 0 or not allowed:
|
| 476 |
+
solver.Add(Assignment[p, ell, s, t] == 0)
|
| 477 |
+
solver.Add(Hours[p, ell, s, t] == 0)
|
| 478 |
+
solver.Add(Units[p, ell, s, t] == 0)
|
| 479 |
+
|
| 480 |
+
# 4) Line throughput: Units β€ product_speed * Hours
|
| 481 |
+
for p in sorted_product_list:
|
| 482 |
+
for ell in line_tuples:
|
| 483 |
+
for s in active_shift_list:
|
| 484 |
+
for t in date_span_list:
|
| 485 |
+
# Get product speed (same speed regardless of line type)
|
| 486 |
+
if p in PER_PRODUCT_SPEED:
|
| 487 |
+
# Convert kit per day to kit per hour (assuming 7.5 hour workday)
|
| 488 |
+
speed = PER_PRODUCT_SPEED[p]
|
| 489 |
+
# Upper bound: units cannot exceed capacity
|
| 490 |
+
solver.Add(
|
| 491 |
+
Units[p, ell, s, t] <= speed * Hours[p, ell, s, t]
|
| 492 |
+
)
|
| 493 |
+
# Lower bound: if working, must produce (prevent phantom work)
|
| 494 |
+
solver.Add(
|
| 495 |
+
Units[p, ell, s, t] >= speed * Hours[p, ell, s, t]
|
| 496 |
+
)
|
| 497 |
+
else:
|
| 498 |
+
# Default speed if not found
|
| 499 |
+
default_speed = 800 / 7.5 # units per hour
|
| 500 |
+
print(f"Warning: No speed data for product {p}, using default {default_speed:.1f} per hour")
|
| 501 |
+
# Upper bound: units cannot exceed capacity
|
| 502 |
+
solver.Add(
|
| 503 |
+
Units[p, ell, s, t] <= default_speed * Hours[p, ell, s, t]
|
| 504 |
+
)
|
| 505 |
+
# Lower bound: if working, must produce (prevent phantom work)
|
| 506 |
+
solver.Add(
|
| 507 |
+
Units[p, ell, s, t] >= default_speed * Hours[p, ell, s, t]
|
| 508 |
+
)
|
| 509 |
+
|
| 510 |
+
# Working hours constraint: active employees cannot exceed shift hour capacity
|
| 511 |
+
for e in employee_type_list:
|
| 512 |
for s in active_shift_list:
|
| 513 |
for t in date_span_list:
|
| 514 |
+
# No idle employee constraints - employees are only counted when working
|
| 515 |
+
solver.Add(
|
| 516 |
+
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, s, t] for p in sorted_product_list for ell in line_tuples)
|
| 517 |
+
<= Hmax_s[s] * max_employee_type_day[e][t]
|
| 518 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
|
| 520 |
+
# 6) Per-shift staffing capacity by type: link employee count to actual work hours
|
| 521 |
+
# This constraint ensures EMPLOYEE_COUNT[e,s,t] represents the actual number of employees needed in each shift
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
for e in employee_type_list:
|
| 523 |
+
for s in active_shift_list:
|
| 524 |
+
for t in date_span_list:
|
| 525 |
+
# Total person-hours worked by employee type e in shift s on day t
|
| 526 |
+
total_person_hours_in_shift = solver.Sum(
|
| 527 |
+
TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, s, t]
|
| 528 |
+
for p in sorted_product_list
|
| 529 |
+
for ell in line_tuples
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
# Employee count must be sufficient to cover the work in this shift
|
| 533 |
+
# If employees work H person-hours total and each can work max M hours/shift,
|
| 534 |
+
# then we need at least ceil(H/M) employees
|
| 535 |
+
# Constraint: employee_count Γ max_hours_per_shift >= total_person_hours_in_shift
|
| 536 |
+
solver.Add(EMPLOYEE_COUNT[e, s, t] * Hmax_s[s] >= total_person_hours_in_shift)
|
| 537 |
+
|
| 538 |
+
# 7) Shift ordering constraints (only apply if shifts are available)
|
| 539 |
+
# Evening shift after regular shift
|
| 540 |
+
if ShiftType.EVENING in active_shift_list and ShiftType.REGULAR in active_shift_list: # Only if both shifts are available
|
| 541 |
+
for e in employee_type_list:
|
| 542 |
+
for t in date_span_list:
|
| 543 |
+
solver.Add(
|
| 544 |
+
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, ShiftType.EVENING, t] for p in sorted_product_list for ell in line_tuples)
|
| 545 |
+
<=
|
| 546 |
+
solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, ShiftType.REGULAR, t] for p in sorted_product_list for ell in line_tuples)
|
| 547 |
+
)
|
| 548 |
|
| 549 |
+
# Overtime should only be used when regular shift is at capacity
|
| 550 |
+
if ShiftType.OVERTIME in active_shift_list and ShiftType.REGULAR in active_shift_list: # Only if both shifts are available
|
| 551 |
+
print("\n[OVERTIME] Adding constraints to ensure overtime only when regular shift is insufficient...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
|
| 553 |
+
for e in employee_type_list:
|
| 554 |
+
for t in date_span_list:
|
| 555 |
+
# Get available regular capacity for this employee type and day
|
| 556 |
+
regular_capacity = max_employee_type_day[e][t]
|
| 557 |
+
|
| 558 |
+
# Total regular shift usage for this employee type and day
|
| 559 |
+
regular_usage = solver.Sum(
|
| 560 |
+
TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, ShiftType.REGULAR, t]
|
| 561 |
+
for p in sorted_product_list for ell in line_tuples
|
| 562 |
)
|
| 563 |
+
|
| 564 |
+
# Total overtime usage for this employee type and day
|
| 565 |
+
overtime_usage = solver.Sum(
|
| 566 |
+
TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, ShiftType.OVERTIME, t]
|
| 567 |
+
for p in sorted_product_list for ell in line_tuples
|
| 568 |
)
|
| 569 |
|
| 570 |
+
# Create binary variable: 1 if using overtime, 0 otherwise
|
| 571 |
+
using_overtime = solver.IntVar(0, 1, f'using_overtime_{e}_{t}')
|
|
|
|
| 572 |
|
| 573 |
+
# If using overtime, regular capacity must be utilized significantly
|
| 574 |
+
# Regular usage must be at least 90% of capacity to allow overtime
|
| 575 |
+
min_regular_for_overtime = int(0.9 * regular_capacity)
|
| 576 |
+
|
| 577 |
+
# Constraint 1: Can only use overtime if regular usage is high
|
| 578 |
+
solver.Add(regular_usage >= min_regular_for_overtime * using_overtime)
|
| 579 |
+
|
| 580 |
+
# Constraint 2: If any overtime is used, set the binary variable
|
| 581 |
+
solver.Add(overtime_usage <= regular_capacity * using_overtime)
|
| 582 |
+
|
| 583 |
+
overtime_constraints_added = len(employee_type_list) * len(date_span_list) * 2 # 2 constraints per employee type per day
|
| 584 |
+
print(f"[OVERTIME] Added {overtime_constraints_added} constraints ensuring overtime only when regular shifts are at 90%+ capacity")
|
| 585 |
+
|
| 586 |
+
# 7.5) Bulk payment linking constraints are now handled inline in the cost calculation
|
| 587 |
+
|
| 588 |
+
# 7.6) *** FIXED MINIMUM UNICEF EMPLOYEES CONSTRAINT ***
|
| 589 |
+
# Ensure minimum UNICEF fixed-term staff work in the REGULAR shift every day
|
| 590 |
+
# The minimum applies to the regular shift specifically (not overtime or evening)
|
| 591 |
+
if 'UNICEF Fixed term' in employee_type_list and FIXED_MIN_UNICEF_PER_DAY > 0:
|
| 592 |
+
if ShiftType.REGULAR in active_shift_list:
|
| 593 |
+
print(f"\n[FIXED STAFFING] Adding constraint for minimum {FIXED_MIN_UNICEF_PER_DAY} UNICEF employees in REGULAR shift per day...")
|
| 594 |
+
for t in date_span_list:
|
| 595 |
+
# At least FIXED_MIN_UNICEF_PER_DAY employees must work in the regular shift each day
|
| 596 |
+
solver.Add(
|
| 597 |
+
EMPLOYEE_COUNT['UNICEF Fixed term', ShiftType.REGULAR, t] >= FIXED_MIN_UNICEF_PER_DAY
|
| 598 |
+
)
|
| 599 |
+
print(f"[FIXED STAFFING] Added {len(date_span_list)} constraints ensuring >= {FIXED_MIN_UNICEF_PER_DAY} UNICEF employees in regular shift per day")
|
| 600 |
+
else:
|
| 601 |
+
print(f"\n[FIXED STAFFING] Warning: Regular shift not available, cannot enforce minimum UNICEF staffing")
|
| 602 |
+
|
| 603 |
+
# 8) *** HIERARCHY DEPENDENCY CONSTRAINTS ***
|
| 604 |
+
# For subkits with prepack dependencies: dependencies should be produced before or same time
|
| 605 |
+
print("\n[HIERARCHY] Adding dependency constraints...")
|
| 606 |
+
dependency_constraints_added = 0
|
| 607 |
+
|
| 608 |
+
for p in sorted_product_list:
|
| 609 |
+
dependencies = KIT_DEPENDENCIES.get(p, [])
|
| 610 |
+
if dependencies:
|
| 611 |
+
# Get the level of the current product
|
| 612 |
+
p_level = KIT_LEVELS.get(p, 2)
|
| 613 |
+
|
| 614 |
+
for dep in dependencies:
|
| 615 |
+
if dep in sorted_product_list: # Only if dependency is also in production list
|
| 616 |
+
# Calculate "completion time" for each product (sum of all production times)
|
| 617 |
+
p_completion = solver.Sum(
|
| 618 |
+
t * Hours[p, ell, s, t] for ell in line_tuples for s in active_shift_list for t in date_span_list
|
| 619 |
+
)
|
| 620 |
+
dep_completion = solver.Sum(
|
| 621 |
+
t * Hours[dep, ell, s, t] for ell in line_tuples for s in active_shift_list for t in date_span_list
|
| 622 |
+
)
|
| 623 |
+
|
| 624 |
+
# Dependency should complete before or at the same time
|
| 625 |
+
solver.Add(dep_completion <= p_completion)
|
| 626 |
+
dependency_constraints_added += 1
|
| 627 |
+
|
| 628 |
+
print(f" Added constraint: {dep} (dependency) <= {p} (level {p_level})")
|
| 629 |
+
|
| 630 |
+
print(f"[HIERARCHY] Added {dependency_constraints_added} dependency constraints")
|
| 631 |
+
|
| 632 |
+
# --- Solve ---
|
| 633 |
+
status = solver.Solve()
|
| 634 |
+
if status != pywraplp.Solver.OPTIMAL:
|
| 635 |
+
status_names = {pywraplp.Solver.INFEASIBLE: "INFEASIBLE", pywraplp.Solver.UNBOUNDED: "UNBOUNDED"}
|
| 636 |
+
print(f"No optimal solution. Status: {status} ({status_names.get(status, 'UNKNOWN')})")
|
| 637 |
+
# Debug hint:
|
| 638 |
+
# solver.EnableOutput()
|
| 639 |
+
# solver.ExportModelAsLpFile("model.lp")
|
| 640 |
+
return None
|
| 641 |
+
|
| 642 |
+
# --- Report ---
|
| 643 |
+
result = {}
|
| 644 |
+
result['objective'] = solver.Objective().Value()
|
| 645 |
+
|
| 646 |
+
# Weekly production
|
| 647 |
+
prod_week = {p: sum(Units[p, ell, s, t].solution_value() for ell in line_tuples for s in active_shift_list for t in date_span_list) for p in sorted_product_list}
|
| 648 |
+
result['weekly_production'] = prod_week
|
| 649 |
+
|
| 650 |
+
# Which product ran on which line/shift/day
|
| 651 |
+
schedule = []
|
| 652 |
+
for t in date_span_list:
|
| 653 |
+
for ell in line_tuples:
|
| 654 |
+
for s in active_shift_list:
|
| 655 |
+
chosen = [p for p in sorted_product_list if Assignment[p, ell, s, t].solution_value() > 0.5]
|
| 656 |
+
if chosen:
|
| 657 |
+
p = chosen[0]
|
| 658 |
+
schedule.append({
|
| 659 |
+
'day': t,
|
| 660 |
+
'line_type_id': ell[0],
|
| 661 |
+
'line_idx': ell[1],
|
| 662 |
+
'shift': s,
|
| 663 |
+
'product': p,
|
| 664 |
+
'run_hours': Hours[p, ell, s, t].solution_value(),
|
| 665 |
+
'units': Units[p, ell, s, t].solution_value(),
|
| 666 |
+
})
|
| 667 |
+
result['run_schedule'] = schedule
|
| 668 |
+
|
| 669 |
+
# Implied headcount by type/shift/day (ceil)
|
| 670 |
+
headcount = []
|
| 671 |
+
for e in employee_type_list:
|
| 672 |
for s in active_shift_list:
|
| 673 |
+
for t in date_span_list:
|
| 674 |
+
used_ph = sum(TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, s, t].solution_value() for p in sorted_product_list for ell in line_tuples)
|
| 675 |
+
need = ceil(used_ph / (Hmax_s[s] + 1e-9))
|
| 676 |
+
headcount.append({'emp_type': e, 'shift': s, 'day': t,
|
| 677 |
+
'needed': need, 'available': max_employee_type_day[e][t]})
|
| 678 |
+
result['headcount_per_shift'] = headcount
|
| 679 |
+
|
| 680 |
+
# Total person-hours by type/day (β€ 14h * headcount)
|
| 681 |
+
ph_by_day = []
|
| 682 |
+
for e in employee_type_list:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
for t in date_span_list:
|
| 684 |
+
used = sum(TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, s, t].solution_value() for s in active_shift_list for p in sorted_product_list for ell in line_tuples)
|
| 685 |
+
ph_by_day.append({'emp_type': e, 'day': t,
|
| 686 |
+
'used_person_hours': used,
|
| 687 |
+
'cap_person_hours': Hmax_daily * max_employee_type_day[e][t]})
|
| 688 |
+
result['person_hours_by_day'] = ph_by_day
|
| 689 |
+
|
| 690 |
+
# Actual employee count per type/shift/day (from EMPLOYEE_COUNT variable)
|
| 691 |
+
employee_count_by_shift = []
|
| 692 |
+
for e in employee_type_list:
|
| 693 |
+
for s in active_shift_list:
|
| 694 |
+
for t in date_span_list:
|
| 695 |
+
count = int(EMPLOYEE_COUNT[e, s, t].solution_value())
|
| 696 |
+
used_hours = sum(TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, s, t].solution_value()
|
| 697 |
+
for p in sorted_product_list for ell in line_tuples)
|
| 698 |
+
avg_hours_per_employee = used_hours / count if count > 0 else 0
|
| 699 |
+
if count > 0: # Only add entries where employees are working
|
| 700 |
+
employee_count_by_shift.append({
|
| 701 |
+
'emp_type': e,
|
| 702 |
+
'shift': s,
|
| 703 |
+
'day': t,
|
| 704 |
+
'employee_count': count,
|
| 705 |
+
'total_person_hours': used_hours,
|
| 706 |
+
'avg_hours_per_employee': avg_hours_per_employee,
|
| 707 |
+
'available': max_employee_type_day[e][t]
|
| 708 |
+
})
|
| 709 |
+
result['employee_count_by_shift'] = employee_count_by_shift
|
| 710 |
+
|
| 711 |
+
# Also calculate daily totals (summing across shifts)
|
| 712 |
+
employee_count_by_day = []
|
| 713 |
+
for e in employee_type_list:
|
| 714 |
for t in date_span_list:
|
| 715 |
+
# Sum employees across all shifts for this day
|
| 716 |
+
total_count = sum(int(EMPLOYEE_COUNT[e, s, t].solution_value()) for s in active_shift_list)
|
| 717 |
used_hours = sum(TEAM_REQ_PER_PRODUCT[e][p] * Hours[p, ell, s, t].solution_value()
|
| 718 |
+
for s in active_shift_list for p in sorted_product_list for ell in line_tuples)
|
| 719 |
+
avg_hours_per_employee = used_hours / total_count if total_count > 0 else 0
|
| 720 |
+
if total_count > 0: # Only add days where employees are working
|
| 721 |
+
employee_count_by_day.append({
|
| 722 |
'emp_type': e,
|
|
|
|
| 723 |
'day': t,
|
| 724 |
+
'employee_count': total_count,
|
| 725 |
'total_person_hours': used_hours,
|
| 726 |
'avg_hours_per_employee': avg_hours_per_employee,
|
| 727 |
'available': max_employee_type_day[e][t]
|
| 728 |
})
|
| 729 |
+
result['employee_count_by_day'] = employee_count_by_day
|
| 730 |
+
|
| 731 |
+
# Note: Idle employee tracking removed - only counting employees actually working
|
| 732 |
+
|
| 733 |
+
# Pretty print
|
| 734 |
+
print("Objective (min cost):", result['objective'])
|
| 735 |
+
print("\n--- Weekly production by product ---")
|
| 736 |
+
for p, u in prod_week.items():
|
| 737 |
+
print(f"{p}: {u:.1f} / demand {DEMAND_DICTIONARY.get(p,0)}")
|
| 738 |
+
|
| 739 |
+
print("\n--- Schedule (line, shift, day) ---")
|
| 740 |
+
for row in schedule:
|
| 741 |
+
shift_name = ShiftType.get_name(row['shift'])
|
| 742 |
+
line_name = LineType.get_name(row['line_type_id'])
|
| 743 |
+
print(f"date_span_list{row['day']} {line_name}-{row['line_idx']} {shift_name}: "
|
| 744 |
+
f"{row['product']} Hours={row['run_hours']:.2f}h Units={row['units']:.1f}")
|
| 745 |
+
|
| 746 |
+
print("\n--- Implied headcount need (per type/shift/day) ---")
|
| 747 |
+
for row in headcount:
|
| 748 |
+
shift_name = ShiftType.get_name(row['shift'])
|
| 749 |
+
print(f"{row['emp_type']}, {shift_name}, date_span_list{row['day']}: "
|
| 750 |
+
f"need={row['needed']} (avail {row['available']})")
|
| 751 |
+
|
| 752 |
+
print("\n--- Total person-hours by type/day ---")
|
| 753 |
+
for row in ph_by_day:
|
| 754 |
+
print(f"{row['emp_type']}, date_span_list{row['day']}: used={row['used_person_hours']:.1f} "
|
| 755 |
+
f"(cap {row['cap_person_hours']})")
|
| 756 |
+
|
| 757 |
+
print("\n--- Actual employee count by type/shift/day ---")
|
| 758 |
+
for row in employee_count_by_shift:
|
| 759 |
+
shift_name = ShiftType.get_name(row['shift'])
|
| 760 |
+
print(f"{row['emp_type']}, {shift_name}, date_span_list{row['day']}: "
|
| 761 |
+
f"count={row['employee_count']} employees, "
|
| 762 |
+
f"total_hours={row['total_person_hours']:.1f}h, "
|
| 763 |
+
f"avg={row['avg_hours_per_employee']:.1f}h/employee")
|
| 764 |
+
|
| 765 |
+
print("\n--- Daily employee totals by type/day (sum across shifts) ---")
|
| 766 |
+
for row in employee_count_by_day:
|
| 767 |
+
print(f"{row['emp_type']}, date_span_list{row['day']}: "
|
| 768 |
+
f"count={row['employee_count']} employees total, "
|
| 769 |
+
f"total_hours={row['total_person_hours']:.1f}h, "
|
| 770 |
+
f"avg={row['avg_hours_per_employee']:.1f}h/employee "
|
| 771 |
+
f"(available: {row['available']})")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 772 |
|
| 773 |
+
# Note: Idle employee reporting removed - only tracking employees actually working
|
| 774 |
|
| 775 |
+
return result
|
| 776 |
|
| 777 |
|
| 778 |
if __name__ == "__main__":
|
| 779 |
+
optimizer = Optimizer()
|
| 780 |
+
optimizer.run_optimization()
|
src/preprocess/hierarchy_parser.py
CHANGED
|
@@ -107,6 +107,99 @@ class KitHierarchyParser:
|
|
| 107 |
|
| 108 |
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
def main():
|
| 111 |
"""Demo the hierarchy parser"""
|
| 112 |
parser = KitHierarchyParser()
|
|
|
|
| 107 |
|
| 108 |
|
| 109 |
|
| 110 |
+
def sort_products_by_hierarchy(product_list: List[str],
|
| 111 |
+
kit_levels: Dict[str, int],
|
| 112 |
+
kit_dependencies: Dict[str, List[str]]) -> List[str]:
|
| 113 |
+
"""
|
| 114 |
+
Sort products by hierarchy levels and dependencies using topological sorting.
|
| 115 |
+
Returns products in optimal production order: prepacks β subkits β masters
|
| 116 |
+
Dependencies within the same level are properly ordered.
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
product_list: List of product names to sort
|
| 120 |
+
kit_levels: Dictionary mapping product names to hierarchy levels (0=prepack, 1=subkit, 2=master)
|
| 121 |
+
kit_dependencies: Dictionary mapping product names to their dependencies (products that must be made first)
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
List of products sorted in production order (dependencies first)
|
| 125 |
+
"""
|
| 126 |
+
# Filter products that are in our production list and have hierarchy data
|
| 127 |
+
products_with_hierarchy = [p for p in product_list if p in kit_levels]
|
| 128 |
+
products_without_hierarchy = [p for p in product_list if p not in kit_levels]
|
| 129 |
+
|
| 130 |
+
if products_without_hierarchy:
|
| 131 |
+
print(f"[HIERARCHY] Products without hierarchy data: {products_without_hierarchy}")
|
| 132 |
+
|
| 133 |
+
# Build dependency graph for products in our list
|
| 134 |
+
graph = defaultdict(list) # product -> [dependents]
|
| 135 |
+
in_degree = defaultdict(int) # product -> number of dependencies
|
| 136 |
+
|
| 137 |
+
# Initialize all products
|
| 138 |
+
for product in products_with_hierarchy:
|
| 139 |
+
in_degree[product] = 0
|
| 140 |
+
|
| 141 |
+
for product in products_with_hierarchy:
|
| 142 |
+
deps = kit_dependencies.get(product, []) # dependencies = products that has to be packed first
|
| 143 |
+
for dep in deps:
|
| 144 |
+
if dep in products_with_hierarchy: # Only if dependency is in our production list
|
| 145 |
+
# REVERSE THE RELATIONSHIP:
|
| 146 |
+
# kit_dependencies says: "product needs dep"
|
| 147 |
+
# graph says: "dep is needed by product"
|
| 148 |
+
graph[dep].append(product) # dep -> product (reverse the relationship!)
|
| 149 |
+
in_degree[product] += 1
|
| 150 |
+
|
| 151 |
+
# Topological sort with hierarchy level priority
|
| 152 |
+
sorted_products = []
|
| 153 |
+
# queue = able to remove from both sides
|
| 154 |
+
queue = deque()
|
| 155 |
+
|
| 156 |
+
# Start with products that have no dependencies
|
| 157 |
+
for product in products_with_hierarchy:
|
| 158 |
+
if in_degree[product] == 0:
|
| 159 |
+
queue.append(product)
|
| 160 |
+
|
| 161 |
+
while queue:
|
| 162 |
+
current = queue.popleft()
|
| 163 |
+
sorted_products.append(current)
|
| 164 |
+
|
| 165 |
+
# Process dependents - sort by hierarchy level first
|
| 166 |
+
for dependent in sorted(graph[current], key=lambda p: (kit_levels.get(p, 999), p)):
|
| 167 |
+
in_degree[dependent] -= 1 # decrement the in_degree of the dependent
|
| 168 |
+
if in_degree[dependent] == 0: # if the in_degree of the dependent is 0, add it to the queue so that it can be processed
|
| 169 |
+
queue.append(dependent)
|
| 170 |
+
|
| 171 |
+
# Check for cycles (shouldn't happen with proper hierarchy)
|
| 172 |
+
if len(sorted_products) != len(products_with_hierarchy):
|
| 173 |
+
remaining = [p for p in products_with_hierarchy if p not in sorted_products]
|
| 174 |
+
print(f"[HIERARCHY] WARNING: Potential circular dependencies detected in: {remaining}")
|
| 175 |
+
# Add remaining products sorted by level as fallback
|
| 176 |
+
remaining_sorted = sorted(remaining, key=lambda p: (kit_levels.get(p, 999), p))
|
| 177 |
+
sorted_products.extend(remaining_sorted)
|
| 178 |
+
|
| 179 |
+
# Add products without hierarchy information at the end
|
| 180 |
+
sorted_products.extend(sorted(products_without_hierarchy))
|
| 181 |
+
|
| 182 |
+
print(f"[HIERARCHY] Dependency-aware production order: {len(sorted_products)} products")
|
| 183 |
+
for i, p in enumerate(sorted_products[:10]): # Show first 10
|
| 184 |
+
level = kit_levels.get(p, "unknown")
|
| 185 |
+
# Import here to avoid circular dependency
|
| 186 |
+
try:
|
| 187 |
+
from src.config.constants import KitLevel
|
| 188 |
+
level_name = KitLevel.get_name(level)
|
| 189 |
+
except:
|
| 190 |
+
level_name = f"level_{level}"
|
| 191 |
+
deps = kit_dependencies.get(p, [])
|
| 192 |
+
deps_in_list = [d for d in deps if d in products_with_hierarchy]
|
| 193 |
+
print(f" {i+1}. {p} (level {level}={level_name}, deps: {len(deps_in_list)})")
|
| 194 |
+
if deps_in_list:
|
| 195 |
+
print(f" Dependencies: {deps_in_list}")
|
| 196 |
+
|
| 197 |
+
if len(sorted_products) > 10:
|
| 198 |
+
print(f" ... and {len(sorted_products) - 10} more products")
|
| 199 |
+
|
| 200 |
+
return sorted_products
|
| 201 |
+
|
| 202 |
+
|
| 203 |
def main():
|
| 204 |
"""Demo the hierarchy parser"""
|
| 205 |
parser = KitHierarchyParser()
|
ui/pages/config_page.py
CHANGED
|
@@ -8,7 +8,24 @@ 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 |
def render_config_page():
|
| 14 |
"""Render the configuration page with all user input controls"""
|
|
@@ -48,20 +65,8 @@ def render_config_page():
|
|
| 48 |
config = save_configuration()
|
| 49 |
st.success("β
Settings saved successfully!")
|
| 50 |
|
| 51 |
-
# Clear
|
| 52 |
-
|
| 53 |
-
del st.session_state.optimization_results
|
| 54 |
-
|
| 55 |
-
# Clear any previous validation state
|
| 56 |
-
if 'show_validation_after_save' in st.session_state:
|
| 57 |
-
del st.session_state.show_validation_after_save
|
| 58 |
-
if 'settings_just_saved' in st.session_state:
|
| 59 |
-
del st.session_state.settings_just_saved
|
| 60 |
-
|
| 61 |
-
# Clear any cached validation results since settings changed
|
| 62 |
-
validation_cache_key = f"validation_results_{st.session_state.get('start_date', 'default')}"
|
| 63 |
-
if validation_cache_key in st.session_state:
|
| 64 |
-
del st.session_state[validation_cache_key]
|
| 65 |
|
| 66 |
# Trigger fresh demand validation after saving settings
|
| 67 |
st.session_state.show_validation_after_save = True
|
|
@@ -245,56 +250,56 @@ def render_config_page():
|
|
| 245 |
display_optimization_results(st.session_state.optimization_results)
|
| 246 |
|
| 247 |
def initialize_session_state():
|
| 248 |
-
"""Initialize session state with
|
| 249 |
|
| 250 |
-
#
|
| 251 |
# Use setdefault to avoid overwriting existing values
|
| 252 |
|
| 253 |
# Schedule defaults
|
| 254 |
st.session_state.setdefault('start_date', datetime.date(2025, 7, 7))
|
| 255 |
-
st.session_state.setdefault('schedule_type',
|
| 256 |
|
| 257 |
# Shift defaults
|
| 258 |
-
st.session_state.setdefault('evening_shift_mode',
|
| 259 |
-
st.session_state.setdefault('evening_shift_threshold',
|
| 260 |
|
| 261 |
# Staff defaults
|
| 262 |
-
st.session_state.setdefault('fixed_staff_mode',
|
| 263 |
-
st.session_state.setdefault('fixed_min_unicef_per_day',
|
| 264 |
|
| 265 |
# Payment modes
|
| 266 |
-
st.session_state.setdefault('payment_mode_shift_1',
|
| 267 |
-
st.session_state.setdefault('payment_mode_shift_2',
|
| 268 |
-
st.session_state.setdefault('payment_mode_shift_3',
|
| 269 |
|
| 270 |
# Working hours
|
| 271 |
-
st.session_state.setdefault('max_hour_per_person_per_day',
|
| 272 |
-
st.session_state.setdefault('max_hours_shift_1',
|
| 273 |
-
st.session_state.setdefault('max_hours_shift_2',
|
| 274 |
-
st.session_state.setdefault('max_hours_shift_3',
|
| 275 |
|
| 276 |
# Workforce limits
|
| 277 |
-
st.session_state.setdefault('max_unicef_per_day',
|
| 278 |
-
st.session_state.setdefault('max_humanizer_per_day',
|
| 279 |
|
| 280 |
# Operations
|
| 281 |
-
st.session_state.setdefault('line_count_long_line',
|
| 282 |
-
st.session_state.setdefault('line_count_mini_load',
|
| 283 |
-
st.session_state.setdefault('max_parallel_workers_long_line',
|
| 284 |
-
st.session_state.setdefault('max_parallel_workers_mini_load',
|
| 285 |
|
| 286 |
# Cost rates
|
| 287 |
-
st.session_state.setdefault('unicef_rate_shift_1',
|
| 288 |
-
st.session_state.setdefault('unicef_rate_shift_2',
|
| 289 |
-
st.session_state.setdefault('unicef_rate_shift_3',
|
| 290 |
-
st.session_state.setdefault('humanizer_rate_shift_1',
|
| 291 |
-
st.session_state.setdefault('humanizer_rate_shift_2',
|
| 292 |
-
st.session_state.setdefault('humanizer_rate_shift_3',
|
| 293 |
|
| 294 |
# Data selection
|
| 295 |
st.session_state.setdefault('selected_employee_types', ["UNICEF Fixed term", "Humanizer"])
|
| 296 |
-
st.session_state.setdefault('selected_shifts', [
|
| 297 |
-
st.session_state.setdefault('selected_lines', [
|
| 298 |
|
| 299 |
def render_schedule_config():
|
| 300 |
"""Render schedule configuration section"""
|
|
@@ -303,9 +308,10 @@ def render_schedule_config():
|
|
| 303 |
col1, col2 = st.columns(2)
|
| 304 |
|
| 305 |
with col1:
|
| 306 |
-
st.
|
| 307 |
"Start Date",
|
| 308 |
-
value=
|
|
|
|
| 309 |
help="Exact start date to filter demand data - will only use orders that start on this specific date"
|
| 310 |
)
|
| 311 |
|
|
@@ -316,12 +322,13 @@ def render_schedule_config():
|
|
| 316 |
# Evening shift configuration
|
| 317 |
st.subheader("π Evening Shift Configuration")
|
| 318 |
|
| 319 |
-
|
|
|
|
|
|
|
| 320 |
"Evening Shift Mode",
|
| 321 |
-
options=
|
| 322 |
-
index=
|
| 323 |
-
|
| 324 |
-
),
|
| 325 |
help="""
|
| 326 |
- **Normal**: Only regular shift (1) and overtime shift (3)
|
| 327 |
- **Activate Evening**: Allow evening shift (2) when demand is high
|
|
@@ -329,12 +336,13 @@ def render_schedule_config():
|
|
| 329 |
"""
|
| 330 |
)
|
| 331 |
|
| 332 |
-
if st.session_state.
|
| 333 |
-
st.
|
| 334 |
"Evening Shift Activation Threshold",
|
| 335 |
min_value=0.1,
|
| 336 |
max_value=1.0,
|
| 337 |
-
value=
|
|
|
|
| 338 |
step=0.1,
|
| 339 |
help="Activate evening shift if regular+overtime capacity < threshold of demand"
|
| 340 |
)
|
|
@@ -344,12 +352,13 @@ def render_workforce_config():
|
|
| 344 |
st.header("π₯ Workforce Configuration")
|
| 345 |
|
| 346 |
# Fixed staff constraint mode
|
| 347 |
-
|
|
|
|
|
|
|
| 348 |
"Fixed Staff Constraint Mode",
|
| 349 |
-
options=
|
| 350 |
-
index=
|
| 351 |
-
|
| 352 |
-
),
|
| 353 |
help="""
|
| 354 |
- **Mandatory**: Forces all fixed staff to work full hours every day
|
| 355 |
- **Available**: Staff available up to limits but not forced
|
|
@@ -364,73 +373,80 @@ def render_workforce_config():
|
|
| 364 |
col1, col2 = st.columns(2)
|
| 365 |
|
| 366 |
with col1:
|
| 367 |
-
st.
|
| 368 |
"Max UNICEF Fixed Term per Day",
|
| 369 |
min_value=1,
|
| 370 |
max_value=50,
|
| 371 |
-
value=
|
|
|
|
| 372 |
help="Maximum number of UNICEF fixed term employees per day"
|
| 373 |
)
|
| 374 |
|
| 375 |
with col2:
|
| 376 |
-
st.
|
| 377 |
"Max Humanizer per Day",
|
| 378 |
min_value=1,
|
| 379 |
max_value=50,
|
| 380 |
-
value=
|
|
|
|
| 381 |
help="Maximum number of Humanizer employees per day"
|
| 382 |
)
|
| 383 |
|
| 384 |
# Fixed minimum UNICEF requirement
|
| 385 |
st.subheader("π Fixed Minimum Requirements")
|
| 386 |
|
| 387 |
-
st.
|
| 388 |
"Fixed Minimum UNICEF per Day",
|
| 389 |
min_value=0,
|
| 390 |
max_value=20,
|
| 391 |
-
value=
|
|
|
|
| 392 |
help="Minimum number of UNICEF Fixed term employees required every working day (constraint)"
|
| 393 |
)
|
| 394 |
|
| 395 |
# Working hours configuration
|
| 396 |
st.subheader("β° Working Hours Configuration")
|
| 397 |
|
| 398 |
-
st.
|
| 399 |
"Max Hours per Person per Day",
|
| 400 |
min_value=1,
|
| 401 |
max_value=24,
|
| 402 |
-
value=
|
|
|
|
| 403 |
help="Legal maximum working hours per person per day"
|
| 404 |
)
|
| 405 |
|
| 406 |
col1, col2, col3 = st.columns(3)
|
| 407 |
|
| 408 |
with col1:
|
| 409 |
-
st.
|
| 410 |
"Max Hours - Shift 1 (Regular)",
|
| 411 |
min_value=1.0,
|
| 412 |
max_value=12.0,
|
| 413 |
-
value=float(
|
|
|
|
| 414 |
step=0.5,
|
| 415 |
help="Maximum hours per person for regular shift"
|
| 416 |
)
|
| 417 |
|
| 418 |
with col2:
|
| 419 |
-
st.
|
| 420 |
"Max Hours - Shift 2 (Evening)",
|
| 421 |
min_value=1.0,
|
| 422 |
max_value=12.0,
|
| 423 |
-
value=float(
|
|
|
|
| 424 |
step=0.5,
|
| 425 |
help="Maximum hours per person for evening shift"
|
| 426 |
)
|
| 427 |
|
| 428 |
with col3:
|
| 429 |
-
st.
|
| 430 |
"Max Hours - Shift 3 (Overtime)",
|
| 431 |
min_value=1.0,
|
| 432 |
max_value=12.0,
|
| 433 |
-
value=float(
|
|
|
|
| 434 |
step=0.5,
|
| 435 |
help="Maximum hours per person for overtime shift"
|
| 436 |
)
|
|
@@ -445,36 +461,40 @@ def render_operations_config():
|
|
| 445 |
col1, col2 = st.columns(2)
|
| 446 |
|
| 447 |
with col1:
|
| 448 |
-
st.
|
| 449 |
"Number of Long Lines",
|
| 450 |
min_value=1,
|
| 451 |
max_value=20,
|
| 452 |
-
value=
|
|
|
|
| 453 |
help="Number of long line production lines available"
|
| 454 |
)
|
| 455 |
|
| 456 |
-
st.
|
| 457 |
"Max Workers per Long Line",
|
| 458 |
min_value=1,
|
| 459 |
max_value=50,
|
| 460 |
-
value=
|
|
|
|
| 461 |
help="Maximum number of workers that can work simultaneously on a long line"
|
| 462 |
)
|
| 463 |
|
| 464 |
with col2:
|
| 465 |
-
st.
|
| 466 |
"Number of Mini Load Lines",
|
| 467 |
min_value=1,
|
| 468 |
max_value=20,
|
| 469 |
-
value=
|
|
|
|
| 470 |
help="Number of mini load production lines available"
|
| 471 |
)
|
| 472 |
|
| 473 |
-
st.
|
| 474 |
"Max Workers per Mini Load Line",
|
| 475 |
min_value=1,
|
| 476 |
max_value=50,
|
| 477 |
-
value=
|
|
|
|
| 478 |
help="Maximum number of workers that can work simultaneously on a mini load line"
|
| 479 |
)
|
| 480 |
|
|
@@ -493,27 +513,32 @@ def render_cost_config():
|
|
| 493 |
|
| 494 |
col1, col2, col3 = st.columns(3)
|
| 495 |
|
|
|
|
|
|
|
| 496 |
with col1:
|
| 497 |
-
st.
|
| 498 |
"Shift 1 (Regular) Payment",
|
| 499 |
-
options=
|
| 500 |
-
index=
|
|
|
|
| 501 |
help="Payment mode for regular shift"
|
| 502 |
)
|
| 503 |
|
| 504 |
with col2:
|
| 505 |
-
st.
|
| 506 |
"Shift 2 (Evening) Payment",
|
| 507 |
-
options=
|
| 508 |
-
index=
|
|
|
|
| 509 |
help="Payment mode for evening shift"
|
| 510 |
)
|
| 511 |
|
| 512 |
with col3:
|
| 513 |
-
st.
|
| 514 |
"Shift 3 (Overtime) Payment",
|
| 515 |
-
options=
|
| 516 |
-
index=
|
|
|
|
| 517 |
help="Payment mode for overtime shift"
|
| 518 |
)
|
| 519 |
|
|
@@ -524,33 +549,36 @@ def render_cost_config():
|
|
| 524 |
col1, col2, col3 = st.columns(3)
|
| 525 |
|
| 526 |
with col1:
|
| 527 |
-
st.
|
| 528 |
"Shift 1 (Regular) - UNICEF",
|
| 529 |
min_value=0.0,
|
| 530 |
max_value=200.0,
|
| 531 |
-
value=float(
|
|
|
|
| 532 |
step=0.01,
|
| 533 |
format="%.2f",
|
| 534 |
help="Hourly rate for UNICEF Fixed Term staff during regular shift"
|
| 535 |
)
|
| 536 |
|
| 537 |
with col2:
|
| 538 |
-
st.
|
| 539 |
"Shift 2 (Evening) - UNICEF",
|
| 540 |
min_value=0.0,
|
| 541 |
max_value=200.0,
|
| 542 |
-
value=float(
|
|
|
|
| 543 |
step=0.01,
|
| 544 |
format="%.2f",
|
| 545 |
help="Hourly rate for UNICEF Fixed Term staff during evening shift"
|
| 546 |
)
|
| 547 |
|
| 548 |
with col3:
|
| 549 |
-
st.
|
| 550 |
"Shift 3 (Overtime) - UNICEF",
|
| 551 |
min_value=0.0,
|
| 552 |
max_value=200.0,
|
| 553 |
-
value=float(
|
|
|
|
| 554 |
step=0.01,
|
| 555 |
format="%.2f",
|
| 556 |
help="Hourly rate for UNICEF Fixed Term staff during overtime shift"
|
|
@@ -560,33 +588,36 @@ def render_cost_config():
|
|
| 560 |
col1, col2, col3 = st.columns(3)
|
| 561 |
|
| 562 |
with col1:
|
| 563 |
-
st.
|
| 564 |
"Shift 1 (Regular) - Humanizer",
|
| 565 |
min_value=0.0,
|
| 566 |
max_value=200.0,
|
| 567 |
-
value=float(
|
|
|
|
| 568 |
step=0.01,
|
| 569 |
format="%.2f",
|
| 570 |
help="Hourly rate for Humanizer staff during regular shift"
|
| 571 |
)
|
| 572 |
|
| 573 |
with col2:
|
| 574 |
-
st.
|
| 575 |
"Shift 2 (Evening) - Humanizer",
|
| 576 |
min_value=0.0,
|
| 577 |
max_value=200.0,
|
| 578 |
-
value=float(
|
|
|
|
| 579 |
step=0.01,
|
| 580 |
format="%.2f",
|
| 581 |
help="Hourly rate for Humanizer staff during evening shift"
|
| 582 |
)
|
| 583 |
|
| 584 |
with col3:
|
| 585 |
-
st.
|
| 586 |
"Shift 3 (Overtime) - Humanizer",
|
| 587 |
min_value=0.0,
|
| 588 |
max_value=200.0,
|
| 589 |
-
value=float(
|
|
|
|
| 590 |
step=0.01,
|
| 591 |
format="%.2f",
|
| 592 |
help="Hourly rate for Humanizer staff during overtime shift"
|
|
@@ -602,13 +633,10 @@ def render_data_selection_config():
|
|
| 602 |
st.subheader("π₯ Employee Types")
|
| 603 |
available_employee_types = ["UNICEF Fixed term", "Humanizer"]
|
| 604 |
|
| 605 |
-
if 'selected_employee_types' not in st.session_state:
|
| 606 |
-
st.session_state.selected_employee_types = available_employee_types
|
| 607 |
-
|
| 608 |
selected_employee_types = st.multiselect(
|
| 609 |
"Select Employee Types to Include",
|
| 610 |
available_employee_types,
|
| 611 |
-
default=st.session_state.
|
| 612 |
help="Choose which employee types to include in the optimization"
|
| 613 |
)
|
| 614 |
st.session_state.selected_employee_types = selected_employee_types
|
|
@@ -618,13 +646,10 @@ def render_data_selection_config():
|
|
| 618 |
available_shifts = list(optimization_config.shift_code_to_name().keys())
|
| 619 |
shift_names = optimization_config.shift_code_to_name()
|
| 620 |
|
| 621 |
-
if 'selected_shifts' not in st.session_state:
|
| 622 |
-
st.session_state.selected_shifts = available_shifts
|
| 623 |
-
|
| 624 |
selected_shifts = st.multiselect(
|
| 625 |
"Select Shifts to Include",
|
| 626 |
available_shifts,
|
| 627 |
-
default=st.session_state.
|
| 628 |
format_func=lambda x: f"Shift {x} ({shift_names[x]})",
|
| 629 |
help="Choose which shifts to include in the optimization"
|
| 630 |
)
|
|
@@ -635,13 +660,10 @@ def render_data_selection_config():
|
|
| 635 |
available_lines = list(optimization_config.line_code_to_name().keys())
|
| 636 |
line_names = optimization_config.line_code_to_name()
|
| 637 |
|
| 638 |
-
if 'selected_lines' not in st.session_state:
|
| 639 |
-
st.session_state.selected_lines = available_lines
|
| 640 |
-
|
| 641 |
selected_lines = st.multiselect(
|
| 642 |
"Select Production Lines to Include",
|
| 643 |
available_lines,
|
| 644 |
-
default=st.session_state.
|
| 645 |
format_func=lambda x: f"Line {x} ({line_names[x]})",
|
| 646 |
help="Choose which production lines to include in the optimization"
|
| 647 |
)
|
|
@@ -982,9 +1004,11 @@ def clear_all_cache_and_results():
|
|
| 982 |
|
| 983 |
st.info("π§Ή Clearing all cached data and previous results...")
|
| 984 |
|
| 985 |
-
#
|
|
|
|
|
|
|
|
|
|
| 986 |
keys_to_clear = [
|
| 987 |
-
'optimization_results',
|
| 988 |
'demand_dictionary',
|
| 989 |
'kit_hierarchy_data',
|
| 990 |
'team_requirements'
|
|
@@ -1064,11 +1088,12 @@ def run_optimization():
|
|
| 1064 |
st.info(f"π₯ Max UNICEF/day: {st.session_state.max_unicef_per_day}, Max Humanizer/day: {st.session_state.max_humanizer_per_day}")
|
| 1065 |
|
| 1066 |
# Import and run the optimization (after clearing)
|
| 1067 |
-
from src.models.optimizer_real import
|
| 1068 |
|
| 1069 |
-
# Run the optimization
|
| 1070 |
with st.spinner('Optimizing workforce schedule...'):
|
| 1071 |
-
|
|
|
|
| 1072 |
|
| 1073 |
if results is None:
|
| 1074 |
st.error("β Optimization failed! The problem may be infeasible with current settings.")
|
|
@@ -1083,8 +1108,20 @@ def run_optimization():
|
|
| 1083 |
st.rerun() # Refresh to show results
|
| 1084 |
|
| 1085 |
except Exception as e:
|
|
|
|
| 1086 |
st.error(f"β Error during optimization: {str(e)}")
|
| 1087 |
st.error("Please check your settings and data files.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1088 |
|
| 1089 |
def check_critical_data_issues():
|
| 1090 |
"""
|
|
|
|
| 8 |
import sys
|
| 9 |
import os
|
| 10 |
from src.config import optimization_config
|
| 11 |
+
from src.config.constants import ShiftType, LineType, DefaultConfig
|
| 12 |
+
|
| 13 |
+
def clear_optimization_cache():
|
| 14 |
+
"""Clear all cached optimization results and validation data"""
|
| 15 |
+
# Clear any previous optimization results since settings changed
|
| 16 |
+
if 'optimization_results' in st.session_state:
|
| 17 |
+
del st.session_state.optimization_results
|
| 18 |
+
|
| 19 |
+
# Clear any previous validation state
|
| 20 |
+
if 'show_validation_after_save' in st.session_state:
|
| 21 |
+
del st.session_state.show_validation_after_save
|
| 22 |
+
if 'settings_just_saved' in st.session_state:
|
| 23 |
+
del st.session_state.settings_just_saved
|
| 24 |
+
|
| 25 |
+
# Clear any cached validation results since settings changed
|
| 26 |
+
validation_cache_key = f"validation_results_{st.session_state.get('start_date', 'default')}"
|
| 27 |
+
if validation_cache_key in st.session_state:
|
| 28 |
+
del st.session_state[validation_cache_key]
|
| 29 |
|
| 30 |
def render_config_page():
|
| 31 |
"""Render the configuration page with all user input controls"""
|
|
|
|
| 65 |
config = save_configuration()
|
| 66 |
st.success("β
Settings saved successfully!")
|
| 67 |
|
| 68 |
+
# Clear all cached optimization and validation data
|
| 69 |
+
clear_optimization_cache()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
# Trigger fresh demand validation after saving settings
|
| 72 |
st.session_state.show_validation_after_save = True
|
|
|
|
| 250 |
display_optimization_results(st.session_state.optimization_results)
|
| 251 |
|
| 252 |
def initialize_session_state():
|
| 253 |
+
"""Initialize session state with default values from constants.py"""
|
| 254 |
|
| 255 |
+
# Use constants from DefaultConfig - no more hardcoded values
|
| 256 |
# Use setdefault to avoid overwriting existing values
|
| 257 |
|
| 258 |
# Schedule defaults
|
| 259 |
st.session_state.setdefault('start_date', datetime.date(2025, 7, 7))
|
| 260 |
+
st.session_state.setdefault('schedule_type', DefaultConfig.SCHEDULE_TYPE)
|
| 261 |
|
| 262 |
# Shift defaults
|
| 263 |
+
st.session_state.setdefault('evening_shift_mode', DefaultConfig.EVENING_SHIFT_MODE)
|
| 264 |
+
st.session_state.setdefault('evening_shift_threshold', DefaultConfig.EVENING_SHIFT_DEMAND_THRESHOLD)
|
| 265 |
|
| 266 |
# Staff defaults
|
| 267 |
+
st.session_state.setdefault('fixed_staff_mode', DefaultConfig.FIXED_STAFF_MODE)
|
| 268 |
+
st.session_state.setdefault('fixed_min_unicef_per_day', DefaultConfig.FIXED_MIN_UNICEF_PER_DAY)
|
| 269 |
|
| 270 |
# Payment modes
|
| 271 |
+
st.session_state.setdefault('payment_mode_shift_1', DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.REGULAR])
|
| 272 |
+
st.session_state.setdefault('payment_mode_shift_2', DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.EVENING])
|
| 273 |
+
st.session_state.setdefault('payment_mode_shift_3', DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.OVERTIME])
|
| 274 |
|
| 275 |
# Working hours
|
| 276 |
+
st.session_state.setdefault('max_hour_per_person_per_day', DefaultConfig.MAX_HOUR_PER_PERSON_PER_DAY)
|
| 277 |
+
st.session_state.setdefault('max_hours_shift_1', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.REGULAR])
|
| 278 |
+
st.session_state.setdefault('max_hours_shift_2', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.EVENING])
|
| 279 |
+
st.session_state.setdefault('max_hours_shift_3', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.OVERTIME])
|
| 280 |
|
| 281 |
# Workforce limits
|
| 282 |
+
st.session_state.setdefault('max_unicef_per_day', DefaultConfig.MAX_UNICEF_PER_DAY)
|
| 283 |
+
st.session_state.setdefault('max_humanizer_per_day', DefaultConfig.MAX_HUMANIZER_PER_DAY)
|
| 284 |
|
| 285 |
# Operations
|
| 286 |
+
st.session_state.setdefault('line_count_long_line', DefaultConfig.LINE_COUNT_LONG_LINE)
|
| 287 |
+
st.session_state.setdefault('line_count_mini_load', DefaultConfig.LINE_COUNT_MINI_LOAD)
|
| 288 |
+
st.session_state.setdefault('max_parallel_workers_long_line', DefaultConfig.MAX_PARALLEL_WORKERS_LONG_LINE)
|
| 289 |
+
st.session_state.setdefault('max_parallel_workers_mini_load', DefaultConfig.MAX_PARALLEL_WORKERS_MINI_LOAD)
|
| 290 |
|
| 291 |
# Cost rates
|
| 292 |
+
st.session_state.setdefault('unicef_rate_shift_1', DefaultConfig.UNICEF_RATE_SHIFT_1)
|
| 293 |
+
st.session_state.setdefault('unicef_rate_shift_2', DefaultConfig.UNICEF_RATE_SHIFT_2)
|
| 294 |
+
st.session_state.setdefault('unicef_rate_shift_3', DefaultConfig.UNICEF_RATE_SHIFT_3)
|
| 295 |
+
st.session_state.setdefault('humanizer_rate_shift_1', DefaultConfig.HUMANIZER_RATE_SHIFT_1)
|
| 296 |
+
st.session_state.setdefault('humanizer_rate_shift_2', DefaultConfig.HUMANIZER_RATE_SHIFT_2)
|
| 297 |
+
st.session_state.setdefault('humanizer_rate_shift_3', DefaultConfig.HUMANIZER_RATE_SHIFT_3)
|
| 298 |
|
| 299 |
# Data selection
|
| 300 |
st.session_state.setdefault('selected_employee_types', ["UNICEF Fixed term", "Humanizer"])
|
| 301 |
+
st.session_state.setdefault('selected_shifts', [ShiftType.REGULAR, ShiftType.OVERTIME])
|
| 302 |
+
st.session_state.setdefault('selected_lines', [LineType.LONG_LINE, LineType.MINI_LOAD])
|
| 303 |
|
| 304 |
def render_schedule_config():
|
| 305 |
"""Render schedule configuration section"""
|
|
|
|
| 308 |
col1, col2 = st.columns(2)
|
| 309 |
|
| 310 |
with col1:
|
| 311 |
+
st.date_input(
|
| 312 |
"Start Date",
|
| 313 |
+
value=datetime.date(2025, 7, 7),
|
| 314 |
+
key='start_date',
|
| 315 |
help="Exact start date to filter demand data - will only use orders that start on this specific date"
|
| 316 |
)
|
| 317 |
|
|
|
|
| 322 |
# Evening shift configuration
|
| 323 |
st.subheader("π Evening Shift Configuration")
|
| 324 |
|
| 325 |
+
shift_mode_options = ['normal', 'activate_evening', 'always_available']
|
| 326 |
+
|
| 327 |
+
st.selectbox(
|
| 328 |
"Evening Shift Mode",
|
| 329 |
+
options=shift_mode_options,
|
| 330 |
+
index=shift_mode_options.index(DefaultConfig.EVENING_SHIFT_MODE),
|
| 331 |
+
key='evening_shift_mode',
|
|
|
|
| 332 |
help="""
|
| 333 |
- **Normal**: Only regular shift (1) and overtime shift (3)
|
| 334 |
- **Activate Evening**: Allow evening shift (2) when demand is high
|
|
|
|
| 336 |
"""
|
| 337 |
)
|
| 338 |
|
| 339 |
+
if st.session_state.evening_shift_mode == 'activate_evening':
|
| 340 |
+
st.slider(
|
| 341 |
"Evening Shift Activation Threshold",
|
| 342 |
min_value=0.1,
|
| 343 |
max_value=1.0,
|
| 344 |
+
value=DefaultConfig.EVENING_SHIFT_DEMAND_THRESHOLD,
|
| 345 |
+
key='evening_shift_threshold',
|
| 346 |
step=0.1,
|
| 347 |
help="Activate evening shift if regular+overtime capacity < threshold of demand"
|
| 348 |
)
|
|
|
|
| 352 |
st.header("π₯ Workforce Configuration")
|
| 353 |
|
| 354 |
# Fixed staff constraint mode
|
| 355 |
+
staff_mode_options = ['mandatory', 'available', 'priority', 'none']
|
| 356 |
+
|
| 357 |
+
st.selectbox(
|
| 358 |
"Fixed Staff Constraint Mode",
|
| 359 |
+
options=staff_mode_options,
|
| 360 |
+
index=staff_mode_options.index(DefaultConfig.FIXED_STAFF_MODE),
|
| 361 |
+
key='fixed_staff_mode',
|
|
|
|
| 362 |
help="""
|
| 363 |
- **Mandatory**: Forces all fixed staff to work full hours every day
|
| 364 |
- **Available**: Staff available up to limits but not forced
|
|
|
|
| 373 |
col1, col2 = st.columns(2)
|
| 374 |
|
| 375 |
with col1:
|
| 376 |
+
st.number_input(
|
| 377 |
"Max UNICEF Fixed Term per Day",
|
| 378 |
min_value=1,
|
| 379 |
max_value=50,
|
| 380 |
+
value=DefaultConfig.MAX_UNICEF_PER_DAY,
|
| 381 |
+
key='max_unicef_per_day',
|
| 382 |
help="Maximum number of UNICEF fixed term employees per day"
|
| 383 |
)
|
| 384 |
|
| 385 |
with col2:
|
| 386 |
+
st.number_input(
|
| 387 |
"Max Humanizer per Day",
|
| 388 |
min_value=1,
|
| 389 |
max_value=50,
|
| 390 |
+
value=DefaultConfig.MAX_HUMANIZER_PER_DAY,
|
| 391 |
+
key='max_humanizer_per_day',
|
| 392 |
help="Maximum number of Humanizer employees per day"
|
| 393 |
)
|
| 394 |
|
| 395 |
# Fixed minimum UNICEF requirement
|
| 396 |
st.subheader("π Fixed Minimum Requirements")
|
| 397 |
|
| 398 |
+
st.number_input(
|
| 399 |
"Fixed Minimum UNICEF per Day",
|
| 400 |
min_value=0,
|
| 401 |
max_value=20,
|
| 402 |
+
value=DefaultConfig.FIXED_MIN_UNICEF_PER_DAY,
|
| 403 |
+
key='fixed_min_unicef_per_day',
|
| 404 |
help="Minimum number of UNICEF Fixed term employees required every working day (constraint)"
|
| 405 |
)
|
| 406 |
|
| 407 |
# Working hours configuration
|
| 408 |
st.subheader("β° Working Hours Configuration")
|
| 409 |
|
| 410 |
+
st.number_input(
|
| 411 |
"Max Hours per Person per Day",
|
| 412 |
min_value=1,
|
| 413 |
max_value=24,
|
| 414 |
+
value=DefaultConfig.MAX_HOUR_PER_PERSON_PER_DAY,
|
| 415 |
+
key='max_hour_per_person_per_day',
|
| 416 |
help="Legal maximum working hours per person per day"
|
| 417 |
)
|
| 418 |
|
| 419 |
col1, col2, col3 = st.columns(3)
|
| 420 |
|
| 421 |
with col1:
|
| 422 |
+
st.number_input(
|
| 423 |
"Max Hours - Shift 1 (Regular)",
|
| 424 |
min_value=1.0,
|
| 425 |
max_value=12.0,
|
| 426 |
+
value=float(DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.REGULAR]),
|
| 427 |
+
key='max_hours_shift_1',
|
| 428 |
step=0.5,
|
| 429 |
help="Maximum hours per person for regular shift"
|
| 430 |
)
|
| 431 |
|
| 432 |
with col2:
|
| 433 |
+
st.number_input(
|
| 434 |
"Max Hours - Shift 2 (Evening)",
|
| 435 |
min_value=1.0,
|
| 436 |
max_value=12.0,
|
| 437 |
+
value=float(DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.EVENING]),
|
| 438 |
+
key='max_hours_shift_2',
|
| 439 |
step=0.5,
|
| 440 |
help="Maximum hours per person for evening shift"
|
| 441 |
)
|
| 442 |
|
| 443 |
with col3:
|
| 444 |
+
st.number_input(
|
| 445 |
"Max Hours - Shift 3 (Overtime)",
|
| 446 |
min_value=1.0,
|
| 447 |
max_value=12.0,
|
| 448 |
+
value=float(DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.OVERTIME]),
|
| 449 |
+
key='max_hours_shift_3',
|
| 450 |
step=0.5,
|
| 451 |
help="Maximum hours per person for overtime shift"
|
| 452 |
)
|
|
|
|
| 461 |
col1, col2 = st.columns(2)
|
| 462 |
|
| 463 |
with col1:
|
| 464 |
+
st.number_input(
|
| 465 |
"Number of Long Lines",
|
| 466 |
min_value=1,
|
| 467 |
max_value=20,
|
| 468 |
+
value=DefaultConfig.LINE_COUNT_LONG_LINE,
|
| 469 |
+
key='line_count_long_line',
|
| 470 |
help="Number of long line production lines available"
|
| 471 |
)
|
| 472 |
|
| 473 |
+
st.number_input(
|
| 474 |
"Max Workers per Long Line",
|
| 475 |
min_value=1,
|
| 476 |
max_value=50,
|
| 477 |
+
value=DefaultConfig.MAX_PARALLEL_WORKERS_LONG_LINE,
|
| 478 |
+
key='max_parallel_workers_long_line',
|
| 479 |
help="Maximum number of workers that can work simultaneously on a long line"
|
| 480 |
)
|
| 481 |
|
| 482 |
with col2:
|
| 483 |
+
st.number_input(
|
| 484 |
"Number of Mini Load Lines",
|
| 485 |
min_value=1,
|
| 486 |
max_value=20,
|
| 487 |
+
value=DefaultConfig.LINE_COUNT_MINI_LOAD,
|
| 488 |
+
key='line_count_mini_load',
|
| 489 |
help="Number of mini load production lines available"
|
| 490 |
)
|
| 491 |
|
| 492 |
+
st.number_input(
|
| 493 |
"Max Workers per Mini Load Line",
|
| 494 |
min_value=1,
|
| 495 |
max_value=50,
|
| 496 |
+
value=DefaultConfig.MAX_PARALLEL_WORKERS_MINI_LOAD,
|
| 497 |
+
key='max_parallel_workers_mini_load',
|
| 498 |
help="Maximum number of workers that can work simultaneously on a mini load line"
|
| 499 |
)
|
| 500 |
|
|
|
|
| 513 |
|
| 514 |
col1, col2, col3 = st.columns(3)
|
| 515 |
|
| 516 |
+
payment_options = ['bulk', 'partial']
|
| 517 |
+
|
| 518 |
with col1:
|
| 519 |
+
st.selectbox(
|
| 520 |
"Shift 1 (Regular) Payment",
|
| 521 |
+
options=payment_options,
|
| 522 |
+
index=payment_options.index(DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.REGULAR]),
|
| 523 |
+
key='payment_mode_shift_1',
|
| 524 |
help="Payment mode for regular shift"
|
| 525 |
)
|
| 526 |
|
| 527 |
with col2:
|
| 528 |
+
st.selectbox(
|
| 529 |
"Shift 2 (Evening) Payment",
|
| 530 |
+
options=payment_options,
|
| 531 |
+
index=payment_options.index(DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.EVENING]),
|
| 532 |
+
key='payment_mode_shift_2',
|
| 533 |
help="Payment mode for evening shift"
|
| 534 |
)
|
| 535 |
|
| 536 |
with col3:
|
| 537 |
+
st.selectbox(
|
| 538 |
"Shift 3 (Overtime) Payment",
|
| 539 |
+
options=payment_options,
|
| 540 |
+
index=payment_options.index(DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.OVERTIME]),
|
| 541 |
+
key='payment_mode_shift_3',
|
| 542 |
help="Payment mode for overtime shift"
|
| 543 |
)
|
| 544 |
|
|
|
|
| 549 |
col1, col2, col3 = st.columns(3)
|
| 550 |
|
| 551 |
with col1:
|
| 552 |
+
st.number_input(
|
| 553 |
"Shift 1 (Regular) - UNICEF",
|
| 554 |
min_value=0.0,
|
| 555 |
max_value=200.0,
|
| 556 |
+
value=float(DefaultConfig.UNICEF_RATE_SHIFT_1),
|
| 557 |
+
key='unicef_rate_shift_1',
|
| 558 |
step=0.01,
|
| 559 |
format="%.2f",
|
| 560 |
help="Hourly rate for UNICEF Fixed Term staff during regular shift"
|
| 561 |
)
|
| 562 |
|
| 563 |
with col2:
|
| 564 |
+
st.number_input(
|
| 565 |
"Shift 2 (Evening) - UNICEF",
|
| 566 |
min_value=0.0,
|
| 567 |
max_value=200.0,
|
| 568 |
+
value=float(DefaultConfig.UNICEF_RATE_SHIFT_2),
|
| 569 |
+
key='unicef_rate_shift_2',
|
| 570 |
step=0.01,
|
| 571 |
format="%.2f",
|
| 572 |
help="Hourly rate for UNICEF Fixed Term staff during evening shift"
|
| 573 |
)
|
| 574 |
|
| 575 |
with col3:
|
| 576 |
+
st.number_input(
|
| 577 |
"Shift 3 (Overtime) - UNICEF",
|
| 578 |
min_value=0.0,
|
| 579 |
max_value=200.0,
|
| 580 |
+
value=float(DefaultConfig.UNICEF_RATE_SHIFT_3),
|
| 581 |
+
key='unicef_rate_shift_3',
|
| 582 |
step=0.01,
|
| 583 |
format="%.2f",
|
| 584 |
help="Hourly rate for UNICEF Fixed Term staff during overtime shift"
|
|
|
|
| 588 |
col1, col2, col3 = st.columns(3)
|
| 589 |
|
| 590 |
with col1:
|
| 591 |
+
st.number_input(
|
| 592 |
"Shift 1 (Regular) - Humanizer",
|
| 593 |
min_value=0.0,
|
| 594 |
max_value=200.0,
|
| 595 |
+
value=float(DefaultConfig.HUMANIZER_RATE_SHIFT_1),
|
| 596 |
+
key='humanizer_rate_shift_1',
|
| 597 |
step=0.01,
|
| 598 |
format="%.2f",
|
| 599 |
help="Hourly rate for Humanizer staff during regular shift"
|
| 600 |
)
|
| 601 |
|
| 602 |
with col2:
|
| 603 |
+
st.number_input(
|
| 604 |
"Shift 2 (Evening) - Humanizer",
|
| 605 |
min_value=0.0,
|
| 606 |
max_value=200.0,
|
| 607 |
+
value=float(DefaultConfig.HUMANIZER_RATE_SHIFT_2),
|
| 608 |
+
key='humanizer_rate_shift_2',
|
| 609 |
step=0.01,
|
| 610 |
format="%.2f",
|
| 611 |
help="Hourly rate for Humanizer staff during evening shift"
|
| 612 |
)
|
| 613 |
|
| 614 |
with col3:
|
| 615 |
+
st.number_input(
|
| 616 |
"Shift 3 (Overtime) - Humanizer",
|
| 617 |
min_value=0.0,
|
| 618 |
max_value=200.0,
|
| 619 |
+
value=float(DefaultConfig.HUMANIZER_RATE_SHIFT_3),
|
| 620 |
+
key='humanizer_rate_shift_3',
|
| 621 |
step=0.01,
|
| 622 |
format="%.2f",
|
| 623 |
help="Hourly rate for Humanizer staff during overtime shift"
|
|
|
|
| 633 |
st.subheader("π₯ Employee Types")
|
| 634 |
available_employee_types = ["UNICEF Fixed term", "Humanizer"]
|
| 635 |
|
|
|
|
|
|
|
|
|
|
| 636 |
selected_employee_types = st.multiselect(
|
| 637 |
"Select Employee Types to Include",
|
| 638 |
available_employee_types,
|
| 639 |
+
default=st.session_state.selected_employee_types,
|
| 640 |
help="Choose which employee types to include in the optimization"
|
| 641 |
)
|
| 642 |
st.session_state.selected_employee_types = selected_employee_types
|
|
|
|
| 646 |
available_shifts = list(optimization_config.shift_code_to_name().keys())
|
| 647 |
shift_names = optimization_config.shift_code_to_name()
|
| 648 |
|
|
|
|
|
|
|
|
|
|
| 649 |
selected_shifts = st.multiselect(
|
| 650 |
"Select Shifts to Include",
|
| 651 |
available_shifts,
|
| 652 |
+
default=st.session_state.selected_shifts,
|
| 653 |
format_func=lambda x: f"Shift {x} ({shift_names[x]})",
|
| 654 |
help="Choose which shifts to include in the optimization"
|
| 655 |
)
|
|
|
|
| 660 |
available_lines = list(optimization_config.line_code_to_name().keys())
|
| 661 |
line_names = optimization_config.line_code_to_name()
|
| 662 |
|
|
|
|
|
|
|
|
|
|
| 663 |
selected_lines = st.multiselect(
|
| 664 |
"Select Production Lines to Include",
|
| 665 |
available_lines,
|
| 666 |
+
default=st.session_state.selected_lines,
|
| 667 |
format_func=lambda x: f"Line {x} ({line_names[x]})",
|
| 668 |
help="Choose which production lines to include in the optimization"
|
| 669 |
)
|
|
|
|
| 1004 |
|
| 1005 |
st.info("π§Ή Clearing all cached data and previous results...")
|
| 1006 |
|
| 1007 |
+
# Clear optimization cache using the dedicated function
|
| 1008 |
+
clear_optimization_cache()
|
| 1009 |
+
|
| 1010 |
+
# Clear additional optimization-related session state
|
| 1011 |
keys_to_clear = [
|
|
|
|
| 1012 |
'demand_dictionary',
|
| 1013 |
'kit_hierarchy_data',
|
| 1014 |
'team_requirements'
|
|
|
|
| 1088 |
st.info(f"π₯ Max UNICEF/day: {st.session_state.max_unicef_per_day}, Max Humanizer/day: {st.session_state.max_humanizer_per_day}")
|
| 1089 |
|
| 1090 |
# Import and run the optimization (after clearing)
|
| 1091 |
+
from src.models.optimizer_real import Optimizer
|
| 1092 |
|
| 1093 |
+
# Run the optimization using Optimizer class
|
| 1094 |
with st.spinner('Optimizing workforce schedule...'):
|
| 1095 |
+
optimizer = Optimizer()
|
| 1096 |
+
results = optimizer.run_optimization()
|
| 1097 |
|
| 1098 |
if results is None:
|
| 1099 |
st.error("β Optimization failed! The problem may be infeasible with current settings.")
|
|
|
|
| 1108 |
st.rerun() # Refresh to show results
|
| 1109 |
|
| 1110 |
except Exception as e:
|
| 1111 |
+
import traceback
|
| 1112 |
st.error(f"β Error during optimization: {str(e)}")
|
| 1113 |
st.error("Please check your settings and data files.")
|
| 1114 |
+
|
| 1115 |
+
# Show detailed traceback in expander
|
| 1116 |
+
with st.expander("π Show detailed error traceback"):
|
| 1117 |
+
st.code(traceback.format_exc())
|
| 1118 |
+
|
| 1119 |
+
# Also print to console for debugging
|
| 1120 |
+
print("\n" + "="*60)
|
| 1121 |
+
print("β OPTIMIZATION ERROR - FULL TRACEBACK:")
|
| 1122 |
+
print("="*60)
|
| 1123 |
+
traceback.print_exc()
|
| 1124 |
+
print("="*60)
|
| 1125 |
|
| 1126 |
def check_critical_data_issues():
|
| 1127 |
"""
|