GeneticWFM / src /config.py
GaetanoParente's picture
first commit
9e62f55
import json
import os
from src.utils.hf_storage import load_json
class ConfigManager:
"""
Singleton implementation for global configuration state management.
Gestisce il caricamento a cascata (Cascade Loading) dei parametri di sistema e di dominio.
"""
_instance = None
def __new__(cls, activity_name=None):
if cls._instance is None:
cls._instance = super(ConfigManager, cls).__new__(cls)
cls._instance.initialized = False
return cls._instance
def __init__(self, activity_name=None):
# Lazy Initialization: previene la sovrascrittura dello stato su chiamate multiple
if getattr(self, 'initialized', False) and not activity_name:
return
# --- L0: HARDCODED FALLBACKS (Safety Net) ---
self.activity_name = None
self.system_slot_minutes = 15
self.planning_slot_minutes = 30
self.expansion_factor = 2
self.daily_slots = 96
# Safe allocations per le strutture di dominio
self.client_settings = {"day_start_hour": 8, "day_end_hour": 20}
self.system_settings = {"vdt_interval_minutes": 120, "vdt_break_minutes": 15}
self.weights = {"understaffing": 1000, "overstaffing": 10, "homogeneity": 20, "soft_preference": 50}
# Hyper-parametri di default per l'Engine Genetico
self.genetic_params = {
"population_size": 200, "generations": 100, "mutation_rate": 0.3,
"crossover_rate": 0.8, "elitism_rate": 0.02, "tournament_size": 5,
"heuristic_rate": 0.8, "guided_mutation_split": 0.4, "heuristic_noise": 0.2
}
# Inizializzazione sicura del routing orario
self.operating_hours = {"default": "09:00-18:00", "exceptions": {}}
self.hours = self.operating_hours
if activity_name:
self.load_configurations(activity_name)
else:
print("[WARN] ConfigManager istanziato senza context. Approvvigionamento defaults (L0) completato.")
self.initialized = True
def load_configurations(self, activity_name):
"""Orchestratore del configuration loading a 3 livelli (L0 -> L1 -> L2)."""
self.activity_name = activity_name
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# --- L1: ENGINE CONFIG (Global System Overrides) ---
engine_path = os.path.join(base_path, "src", "config", "engine_config.json")
try:
if os.path.exists(engine_path):
with open(engine_path, 'r') as f:
engine_data = json.load(f)
if 'system_settings' in engine_data:
self.system_settings.update(engine_data['system_settings'])
self.system_slot_minutes = self.system_settings.get('system_slot_minutes', 15)
if 'genetic_params' in engine_data:
self.genetic_params.update(engine_data['genetic_params'])
except Exception as e:
print(f"[WARN] Impossibile risolvere L1 engine_config.json ({e}). Proceeding with L0.")
# --- L2: ACTIVITY CONFIG (Tenant/Domain Specifics via Object Storage) ---
activity_data = load_json(activity_name, "activity_config.json")
if not activity_data:
print(f"[FATAL] Configurazione L2 mancante sul Dataset HF per l'attività '{activity_name}'.")
return
# Merging dei layer applicativi
self.client_settings = activity_data.get('client_settings', self.client_settings)
self.planning_slot_minutes = self.client_settings.get('planning_slot_minutes', 30)
if 'weights' in activity_data:
self.weights = activity_data['weights']
if 'operating_hours' in activity_data:
self.operating_hours = activity_data['operating_hours']
self.hours = self.operating_hours
if 'genetic_params' in activity_data:
self.genetic_params.update(activity_data['genetic_params'])
# --- DERIVED METRICS COMPUTATION ---
self.slot_minutes = self.system_slot_minutes
self.expansion_factor = int(self.planning_slot_minutes / self.system_slot_minutes)
if self.expansion_factor < 1:
self.expansion_factor = 1
# Calcolo dimensione tensore giornaliero
start = self.client_settings.get('day_start_hour', 8)
end = self.client_settings.get('day_end_hour', 20)
self.daily_slots = int((end - start) * 60 / self.system_slot_minutes)
if self.daily_slots <= 0:
self.daily_slots = 96
self.initialized = True
print(f"[OK] State Sync completato: {activity_name} (Shift Bounds: {start}:00-{end}:00, Grid: {self.daily_slots} slots)")
def get_closing_slot(self, day_idx):
"""Mappa l'orario di chiusura algebrico sull'indice dello slot di sistema."""
day_str = str(day_idx)
exceptions = self.hours.get('exceptions', {})
# 1. Rule Extraction (Exception override vs Baseline)
if day_str in exceptions:
time_range = exceptions[day_str]
else:
time_range = self.hours.get('default', "09:00-18:00")
# 2. Explicit closure flag
if str(time_range).strip().upper() == "CLOSED":
return 0
try:
# 3. Time-to-Slot Quantization
_, close_time = time_range.split('-')
h, m = map(int, close_time.split(':'))
start_h = self.client_settings.get('day_start_hour', 8)
minutes_from_start = (h - start_h) * 60 + m
divisor = self.system_slot_minutes if self.system_slot_minutes > 0 else 15
slot_idx = int(minutes_from_start / divisor)
return max(0, slot_idx)
except Exception as e:
# Failsafe: Parsing error mappato come hard closure per prevenire out-of-bounds nell'engine C/Numba
print(f"[ERROR] Constraint parsing fallito sul Day {day_idx} ('{time_range}'): {e}. Forzatura status CLOSED.")
return 0
def is_day_closed(self, day_idx):
return self.get_closing_slot(day_idx) == 0
# Istanza globale esportata per i moduli downstream
cfg = ConfigManager()