diff --git a/.idea/csv-editor.xml b/.idea/csv-editor.xml index 8596b68..21efdf6 100644 --- a/.idea/csv-editor.xml +++ b/.idea/csv-editor.xml @@ -17,6 +17,20 @@ + + + + + + + + + + + + diff --git a/src/auth/__init__.py b/src/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/auth/login.py b/src/auth/login.py new file mode 100644 index 0000000..c07b72d --- /dev/null +++ b/src/auth/login.py @@ -0,0 +1,91 @@ +import dash_bootstrap_components as dbc +from dash import html, dcc + +from components.ids import Ids + +def create_login_layout(): + return dbc.Container([ + dbc.Row([ + dbc.Col([ + html.H2("Login Dashboard Olio d'Oliva", className="text-center mb-4"), + dbc.Card([ + dbc.CardBody([ + dbc.Input( + id=Ids.LOGIN_USERNAME, + type="text", + placeholder="Username", + className="mb-3" + ), + dbc.Input( + id=Ids.LOGIN_PASSWORD, + type="password", + placeholder="Password", + className="mb-3" + ), + dbc.Button( + "Login", + id=Ids.LOGIN_BUTTON, + color="primary", + className="w-100 mb-3" + ), + html.Div(id=Ids.LOGIN_ERROR), + html.Hr(), + html.P("Non hai un account?", className="text-center"), + dbc.Button( + "Registrati", + id=Ids.SHOW_REGISTER_BUTTON, + color="secondary", + className="w-100" + ) + ]) + ]) + ], md=6, className="mx-auto") + ], className="vh-100 align-items-center") + ]) + +def create_register_layout(): + return dbc.Container([ + dbc.Row([ + dbc.Col([ + html.H2("Registrazione", className="text-center mb-4"), + dbc.Card([ + dbc.CardBody([ + dbc.Input( + id=Ids.REGISTER_USERNAME, + type="text", + placeholder="Username", + className="mb-3" + ), + dbc.Input( + id=Ids.REGISTER_PASSWORD, + type="password", + placeholder="Password", + className="mb-3" + ), + dbc.Input( + id=Ids.REGISTER_CONFIRM, + type="password", + placeholder="Conferma Password", + className="mb-3" + ), + dbc.Button( + "Registrati", + id=Ids.REGISTER_BUTTON, + color="primary", + className="w-100 mb-3" + ), + html.Div(id=Ids.REGISTER_ERROR), + html.Div(id=Ids.REGISTER_SUCCESS), + html.Hr(), + html.P("Hai già un account?", className="text-center"), + dbc.Button( + "Torna al Login", + id=Ids.SHOW_LOGIN_BUTTON, + color="secondary", + className="w-100" + ) + ]) + ]) + ], md=6, className="mx-auto") + ], className="vh-100 align-items-center") + ]) \ No newline at end of file diff --git a/src/auth/utils.py b/src/auth/utils.py new file mode 100644 index 0000000..fa726df --- /dev/null +++ b/src/auth/utils.py @@ -0,0 +1,236 @@ +import json +import os +import hashlib +import jwt as pyjwt +from datetime import datetime, timedelta +import secrets + +# Costanti +SECRET_KEY = 'M!3EmyJ@P$yqt$dYRQ#73QtxFy$aTn8M98P8i5T9x9Fd5LHMcHgdfEEt#?H9EPg&9Qhokh$#pTyYLHxL' # In produzione, usare una variabile d'ambiente +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +USERS_FILE = os.path.join(BASE_DIR, "users", "users.json") +CONFIGS_DIR = os.path.join(BASE_DIR, "users", "configs") + +def init_directory_structure(): + """ + Inizializza la struttura delle directory necessarie per l'applicazione + """ + # Crea directory per utenti e configurazioni + os.makedirs(os.path.dirname(USERS_FILE), exist_ok=True) + os.makedirs(CONFIGS_DIR, exist_ok=True) + + # Se il file users.json non esiste, crealo vuoto + if not os.path.exists(USERS_FILE): + with open(USERS_FILE, 'w') as f: + json.dump({}, f) + + print(f"Initialized directory structure:") + print(f"Users file: {USERS_FILE}") + print(f"Configs directory: {CONFIGS_DIR}") + +def hash_password(password): + """Hash la password usando SHA-256""" + return hashlib.sha256(password.encode()).hexdigest() + +# In auth/utils.py +def create_user(username, password): + """ + Crea un nuovo utente + Args: + username: nome utente + password: password in chiaro + Returns: + tuple: (success: bool, message: str) + """ + try: + print(f"Tentativo di creazione utente: {username}") + + # Inizializza la struttura delle directory + init_directory_structure() + + # Inizializza o carica il dizionario degli utenti + users = {} + if os.path.exists(USERS_FILE): + try: + with open(USERS_FILE, 'r') as f: + content = f.read() + if content.strip(): # Verifica che il file non sia vuoto + users = json.loads(content) + print(f"Utenti esistenti: {len(users)}") + except json.JSONDecodeError as e: + print(f"Errore nel parsing del file utenti: {e}") + pass + + # Validazioni + if not username or len(username) < 3: + return False, "Username deve essere almeno 3 caratteri" + if not password or len(password) < 6: + return False, "Password deve essere almeno 6 caratteri" + if username in users: + return False, "Username già esistente" + + # Genera il salt e hash della password + salt = secrets.token_hex(8) + password_hash = hashlib.sha256((password + salt).encode()).hexdigest() + + # Ottieni il percorso della configurazione utente + user_config = get_user_config_path(username) + print(f"Percorso configurazione utente: {user_config}") + + # Crea la configurazione dell'utente + os.makedirs(os.path.dirname(user_config), exist_ok=True) + + # Salva la configurazione di default + default_config = get_default_config() + with open(user_config, 'w') as f: + json.dump(default_config, f, indent=4) + print("Configurazione default salvata") + + # Aggiungi nuovo utente con struttura semplificata + users[username] = { + "salt": salt, + "password_hash": password_hash + } + + # Salva il file utenti + with open(USERS_FILE, 'w') as f: + json.dump(users, f, indent=4) + print("File utenti aggiornato con successo") + + return True, "Utente creato con successo" + + except Exception as e: + print(f"Errore nella creazione dell'utente: {str(e)}") + import traceback + traceback.print_exc() + return False, f"Errore nella creazione dell'utente: {str(e)}" + +def verify_user(username, password): + """ + Verifica le credenziali utente usando salt e hash + Args: + username: nome utente + password: password in chiaro + Returns: + bool: True se le credenziali sono valide, False altrimenti + """ + try: + if not os.path.exists(USERS_FILE): + print("File utenti non trovato") + return False + + with open(USERS_FILE, 'r') as f: + users = json.load(f) + + if username not in users: + print("Username non trovato") + return False + + # Ottieni il salt e l'hash salvati + user_data = users[username] + stored_salt = user_data['salt'] + stored_hash = user_data['password_hash'] + + # Calcola l'hash della password fornita con il salt salvato + password_hash = hashlib.sha256((password + stored_salt).encode()).hexdigest() + + # Confronta gli hash + return stored_hash == password_hash + + except Exception as e: + print(f"Errore nella verifica dell'utente: {str(e)}") + return False + +def create_token(username): + """Crea JWT token""" + expiration = datetime.utcnow() + timedelta(hours=24) + return pyjwt.encode( + {"user": username, "exp": expiration}, + SECRET_KEY, + algorithm="HS256" + ) + +def verify_token(token): + """Verifica JWT token""" + try: + payload = pyjwt.decode(token, SECRET_KEY, algorithms=["HS256"]) + return True, payload["user"] + except pyjwt.ExpiredSignatureError: + return False, "Token scaduto" + except pyjwt.InvalidTokenError: + return False, "Token non valido" + +def get_user_config_path(username): + """ + Restituisce il percorso del file di configurazione per l'utente specificato + Args: + username: nome utente + Returns: + str: percorso assoluto del file di configurazione + """ + # Sostituisci caratteri non validi nel nome utente + safe_username = "".join(c for c in username if c.isalnum() or c in ('-', '_')) + + # Costruisci il percorso assoluto + base_dir = os.path.dirname(os.path.abspath(__file__)) + config_dir = os.path.join(base_dir, 'config', 'users') + config_path = os.path.join(config_dir, f"{safe_username}_config.json") + + # Assicurati che la directory esista + os.makedirs(os.path.dirname(config_path), exist_ok=True) + + return config_path + +def get_default_config(): + return { + "oliveto": { + "hectares": 1, + "varieties": [ + { + "variety": "Nocellara dell'Etna", + "technique": "tradizionale", + "percentage": 50 + }, + { + "variety": "Frantoio", + "technique": "tradizionale", + "percentage": 10 + }, + { + "variety": "Coratina", + "technique": "tradizionale", + "percentage": 40 + } + ] + }, + "costs": { + "fixed": { + "ammortamento": 2000, + "assicurazione": 500, + "manutenzione": 800, + "certificazioni": 3000 + }, + "variable": { + "raccolta": 0.35, + "potatura": 600, + "fertilizzanti": 400, + "irrigazione": 300 + }, + "transformation": { + "molitura": 0.15, + "stoccaggio": 0.2, + "bottiglia": 1.2, + "etichettatura": 0.3 + }, + "marketing": { + "budget_annuale": 15000, + "costi_commerciali": 0.5, + "prezzo_vendita": 12, + "perc_vendita_diretta": 30 + } + }, + "inference": { + "debug_mode": True, + 'model_path': './sources/olive_oil_transformer/olive_oil_transformer_model.keras' + } + } \ No newline at end of file diff --git a/src/components/ids.py b/src/components/ids.py new file mode 100644 index 0000000..8f6d43a --- /dev/null +++ b/src/components/ids.py @@ -0,0 +1,104 @@ +# components/ids.py +from dataclasses import dataclass + +@dataclass +class Ids: + # Auth Container + AUTH_CONTAINER = 'auth-container' + DASHBOARD_CONTAINER = 'dashboard-container' + + # Login Form + LOGIN_FORM = 'login-form' + LOGIN_USERNAME = 'login-username' + LOGIN_PASSWORD = 'login-password' + LOGIN_BUTTON = 'login-button' + LOGIN_ERROR = 'login-error' + + # Register Form + REGISTER_FORM = 'register-form' + REGISTER_USERNAME = 'register-username' + REGISTER_PASSWORD = 'register-password' + REGISTER_CONFIRM = 'register-confirm' + REGISTER_BUTTON = 'register-button' + REGISTER_ERROR = 'register-error' + REGISTER_SUCCESS = 'register-success' + + # Navigation + SHOW_REGISTER_BUTTON = 'show-register-button' + SHOW_LOGIN_BUTTON = 'show-login-button' + + # Inference + INFERENCE_CONTAINER = 'inference-container' + INFERENCE_STATUS = 'inference-status' + INFERENCE_MODE = 'inference-mode' + INFERENCE_LATENCY = 'inference-latency' + INFERENCE_REQUESTS = 'inference-requests' + INFERENCE_COUNTER = 'inference-counter' + DEBUG_SWITCH = 'debug-switch' + + # Simulation + SIMULATE_BUTTON = 'simulate-btn' + GROWTH_CHART = 'growth-simulation-chart' + PRODUCTION_CHART = 'production-simulation-chart' + SIMULATION_SUMMARY = 'simulation-summary' + KPI_CONTAINER = 'kpi-container' + PRODUCTION_DEBUG_SWITCH = 'production-debug-switch' + PRODUCTION_INFERENCE_REQUESTS = 'production-inference-requests' + PRODUCTION_INFERENCE_MODE = 'production-inference-mode' + + # Environment Controls + TEMP_SLIDER = 'temp-slider' + HUMIDITY_SLIDER = 'humidity-slider' + RAINFALL_INPUT = 'rainfall-input' + RADIATION_INPUT = 'radiation-input' + + # Production Views + OLIVE_PRODUCTION_HA = 'olive-production_ha' + OIL_PRODUCTION_HA = 'oil-production_ha' + WATER_NEED_HA = 'water-need_ha' + OLIVE_PRODUCTION = 'olive-production' + OIL_PRODUCTION = 'oil-production' + WATER_NEED = 'water-need' + PRODUCTION_DETAILS = 'production-details' + WEATHER_IMPACT = 'weather-impact' + WATER_NEEDS = 'water-needs' + EXTRA_INFO = 'extra-info' + + # Configuration + HECTARES_INPUT = 'hectares-input' + VARIETY_1_DROPDOWN = 'variety-1-dropdown' + TECHNIQUE_1_DROPDOWN = 'technique-1-dropdown' + PERCENTAGE_1_INPUT = 'percentage-1-input' + VARIETY_2_DROPDOWN = 'variety-2-dropdown' + TECHNIQUE_2_DROPDOWN = 'technique-2-dropdown' + PERCENTAGE_2_INPUT = 'percentage-2-input' + VARIETY_3_DROPDOWN = 'variety-3-dropdown' + TECHNIQUE_3_DROPDOWN = 'technique-3-dropdown' + PERCENTAGE_3_INPUT = 'percentage-3-input' + PERCENTAGE_WARNING = 'percentage-warning' + + # Cost Inputs + COST_AMMORTAMENTO = 'cost-ammortamento' + COST_ASSICURAZIONE = 'cost-assicurazione' + COST_MANUTENZIONE = 'cost-manutenzione' + COST_CERTIFICAZIONI = 'cost-certificazioni' + COST_RACCOLTA = 'cost-raccolta' + COST_POTATURA = 'cost-potatura' + COST_FERTILIZZANTI = 'cost-fertilizzanti' + COST_IRRIGAZIONE = 'cost-irrigazione' + COST_MOLITURA = 'cost-molitura' + COST_STOCCAGGIO = 'cost-stoccaggio' + COST_BOTTIGLIA = 'cost-bottiglia' + COST_ETICHETTATURA = 'cost-etichettatura' + COST_MARKETING = 'cost-marketing' + COST_COMMERCIALI = 'cost-commerciali' + PRICE_OLIO = 'price-olio' + PERC_VENDITA_DIRETTA = 'perc-vendita-diretta' + + # Other + LOADING_ALERT = 'loading-alert' + TABS = 'tabs' + SAVE_CONFIG_BUTTON = 'save-config-button' + SAVE_CONFIG_MESSAGE = 'save-config-message' + LOGOUT_BUTTON = 'logout-button' + DEV_MODE = 'dev-mode' \ No newline at end of file diff --git a/src/olive-oil-dashboard.py b/src/olive-oil-dashboard.py index 3d5375e..30f7f71 100755 --- a/src/olive-oil-dashboard.py +++ b/src/olive-oil-dashboard.py @@ -1,3 +1,4 @@ +import flask import plotly.express as px from dash import Dash, dcc, html, Input, Output, State, callback_context import tensorflow as tf @@ -7,12 +8,18 @@ import dash_bootstrap_components as dbc import os import argparse import json +from dash.exceptions import PreventUpdate + +from auth import utils from utils.helpers import clean_column_name from dashboard.environmental_simulator import * -from dotenv import load_dotenv from dash import no_update - -CONFIG_FILE = 'olive_config.json' +from auth.utils import ( + init_directory_structure, verify_user, create_token, + verify_token, create_user, get_user_config_path, get_default_config +) +from auth.login import create_login_layout, create_register_layout +from components.ids import Ids # Reduce TensorFlow logging verbosity os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' @@ -29,79 +36,90 @@ MODEL_LOADING = False def load_config(): - default_config = { - 'oliveto': { - 'hectares': 1, - 'varieties': [ - { - 'variety': olive_varieties['Varietà di Olive'].iloc[0], - 'technique': 'Tradizionale', - 'percentage': 100 - } - ] - }, - 'costs': { - 'fixed': { - 'ammortamento': 2000, - 'assicurazione': 500, - 'manutenzione': 800 - }, - 'variable': { - 'raccolta': 0.35, - 'potatura': 600, - 'fertilizzanti': 400 - }, - 'transformation': { - 'molitura': 0.15, - 'stoccaggio': 0.20, - 'bottiglia': 1.20, - 'etichettatura': 0.30 - }, - 'selling_price': 12.00 - }, - 'inference': { - 'debug_mode': True, - 'model_path': './sources/olive_oil_transformer/olive_oil_transformer_model.keras' - } - } - try: - if os.path.exists(CONFIG_FILE): - with open(CONFIG_FILE, 'r') as f: - return json.load(f) - return default_config + config = None + # Prova a leggere la sessione corrente + session_data = check_session() + if session_data: + username = session_data.get('username') + if username: + config_path = get_user_config_path(username) + if os.path.exists(config_path): + with open(config_path, 'r') as f: + config = json.load(f) + print(f'Loaded configuration for user: {username}') + + # Se non c'è config utente, usa quella di default + if config is None: + config = get_default_config() + print('Using default configuration') + + return config except Exception as e: print(f"Errore nel caricamento della configurazione: {e}") - return default_config + return get_default_config() -# Funzione per salvare la configurazione def save_config(config): + """ + Salva la configurazione nel file di configurazione + Returns: (success: bool, message: str) + """ try: - with open(CONFIG_FILE, 'w') as f: + config_path = None + + # Determina il percorso del file di configurazione + if flask.has_request_context(): + try: + session_data = check_session() + if session_data and 'username' in session_data: + username = session_data['username'] + config_path = get_user_config_path(username) + print(f'Using configuration path for user {username}: {config_path}') + except Exception as e: + print(f"Error accessing session: {e}") + import traceback + traceback.print_exc() + + # Se non abbiamo un percorso utente specifico + if config_path is None: + print("WARNING: No user found in session!") + return False, "Nessun utente trovato nella sessione. Effettua nuovamente il login." + + # Assicurati che la directory esista + os.makedirs(os.path.dirname(config_path), exist_ok=True) + + # Verifica che la configurazione sia valida + if not isinstance(config, dict): + return False, "Configurazione non valida" + + # Salva la configurazione + with open(config_path, 'w') as f: json.dump(config, f, indent=4) - return True + + return True, f"Configurazione salvata con successo in {config_path}" + except Exception as e: - print(f"Errore nel salvataggio della configurazione: {e}") - return False + print(f"Errore nel salvataggio della configurazione: {str(e)}") + import traceback + traceback.print_exc() + return False, f"Errore nel salvataggio: {str(e)}" try: + print(f"Caricamento dataset e scaler...") + simulated_data = pd.read_parquet("./sources/olive_training_dataset.parquet") weather_data = pd.read_parquet("./sources/weather_data_solarenergy.parquet") olive_varieties = pd.read_parquet("./sources/olive_varieties.parquet") scaler_temporal = joblib.load('./sources/olive_oil_transformer/olive_oil_transformer_scaler_temporal.joblib') scaler_static = joblib.load('./sources/olive_oil_transformer/olive_oil_transformer_scaler_static.joblib') scaler_y = joblib.load('./sources/olive_oil_transformer/olive_oil_transformer_scaler_y.joblib') - - config = load_config() - DEV_MODE = config.get('inference', {}).get('debug_mode', True) except Exception as e: print(f"Errore nel caricamento: {str(e)}") raise e -# Funzioni di supporto per la dashboard def prepare_static_features_multiple(varieties_info, percentages, hectares, all_varieties): """ Prepara le feature statiche per multiple varietà seguendo la struttura esatta della simulazione. @@ -150,7 +168,7 @@ def prepare_static_features_multiple(varieties_info, percentages, hectares, all_ variety_info['Fabbisogno Acqua Estate (m³/ettaro)'] + variety_info['Fabbisogno Acqua Autunno (m³/ettaro)'] + variety_info['Fabbisogno Acqua Inverno (m³/ettaro)'] - ) / 4 * percentage / 100 * hectares + ) / 12 * percentage / 100 * hectares variety_data[variety_name].update({ 'pct': percentage / 100, @@ -265,7 +283,7 @@ def mock_make_prediction(weather_data, varieties_info, percentages, hectares, si total_oil_production = 0 for variety_info, percentage in zip(varieties_info, percentages): - print(f"Elaborazione varietà: {variety_info['Varietà di Olive']}") + # print(f"Elaborazione varietà: {variety_info['Varietà di Olive']}") # Calcola la produzione di olive per ettaro base_prod_per_ha = float(variety_info['Produzione (tonnellate/ettaro)']) * 1000 * (percentage / 100) @@ -280,9 +298,9 @@ def mock_make_prediction(weather_data, varieties_info, percentages, hectares, si # Calcolo fabbisogno idrico water_need = float(variety_info[season_water_need[current_season]]) * (percentage / 100) - print(f" Produzione olive/ha: {prod_per_ha:.2f}") - print(f" Produzione olio/ha: {oil_per_ha:.2f}") - print(f" Fabbisogno idrico: {water_need:.2f}") + # print(f" Produzione olive/ha: {prod_per_ha:.2f}") + # print(f" Produzione olio/ha: {oil_per_ha:.2f}") + # print(f" Fabbisogno idrico: {water_need:.2f}") variety_details.append({ 'variety': variety_info['Varietà di Olive'], @@ -330,6 +348,7 @@ def mock_make_prediction(weather_data, varieties_info, percentages, hectares, si def make_prediction(weather_data, varieties_info, percentages, hectares, simulation_data=None): + print(f"DEV_MODE: {DEV_MODE}") if DEV_MODE: return mock_make_prediction(weather_data, varieties_info, percentages, hectares, simulation_data) try: @@ -464,11 +483,11 @@ def make_prediction(weather_data, varieties_info, percentages, hectares, simulat print("\nRaw prediction:", prediction) target_features = [ - 'olive_prod', # Produzione olive kg/ha - 'min_oil_prod', # Produzione minima olio L/ha - 'max_oil_prod', # Produzione massima olio L/ha - 'avg_oil_prod', # Produzione media olio L/ha - 'total_water_need' # Fabbisogno idrico totale m³/ha + 'olive_prod', # Produzione olive kg/ha + 'min_oil_prod', # Produzione minima olio L/ha + 'max_oil_prod', # Produzione massima olio L/ha + 'avg_oil_prod', # Produzione media olio L/ha + 'total_water_need' # Fabbisogno idrico totale m³/ha ] prediction = scaler_y.inverse_transform(prediction)[0] @@ -482,6 +501,8 @@ def make_prediction(weather_data, varieties_info, percentages, hectares, simulat print(f"Applied stress factor: {stress_factor}") print(f"Prediction after stress:", prediction) + prediction[4] = prediction[4] / 4 # correggo il bias creato dai dati di simulazione errati @todo nel prossimo modello addestrato con i dati corretti sarà dovrà essere rimosso + # Calcola i valori per ettaro dividendo per il numero di ettari olive_prod_ha = prediction[0] / hectares min_oil_prod_ha = prediction[1] / hectares @@ -529,19 +550,19 @@ def make_prediction(weather_data, varieties_info, percentages, hectares, simulat 'oil_total': oil_total, 'water_need': water_need_ha * (percentage / 100), # Distribuisci il fabbisogno idrico in base alla percentuale 'base_production': base_prod_per_ha, # Produzione senza stress - 'base_oil': base_oil_per_ha, # Produzione olio senza stress + 'base_oil': base_oil_per_ha, # Produzione olio senza stress 'stress_factor': stress_factor if simulation_data is not None else 1.0 }) return { - 'olive_production': olive_prod_ha, # kg/ha - 'olive_production_total': prediction[0], # kg totali - 'min_oil_production': min_oil_prod_ha, # L/ha - 'max_oil_production': max_oil_prod_ha, # L/ha - 'avg_oil_production': avg_oil_prod_ha, # L/ha + 'olive_production': olive_prod_ha, # kg/ha + 'olive_production_total': prediction[0], # kg totali + 'min_oil_production': min_oil_prod_ha, # L/ha + 'max_oil_production': max_oil_prod_ha, # L/ha + 'avg_oil_production': avg_oil_prod_ha, # L/ha 'avg_oil_production_total': prediction[3], # L totali - 'water_need': water_need_ha, # m³/ha - 'water_need_total': prediction[4], # m³ totali + 'water_need': water_need_ha, # m³/ha + 'water_need_total': prediction[4], # m³ totali 'variety_details': variety_details, 'hectares': hectares, 'stress_factor': stress_factor if simulation_data is not None else 1.0 @@ -699,583 +720,68 @@ def create_kpi_indicators(kpis: dict) -> html.Div: return indicators +server = flask.Flask(__name__) +server.secret_key = utils.SECRET_KEY + app = Dash( __name__, external_stylesheets=[dbc.themes.FLATLY], + server=server, meta_tags=[ {"name": "viewport", "content": "width=device-width, initial-scale=1"} ], - prevent_initial_callbacks='initial_duplicate' + prevent_initial_callbacks='initial_duplicate', + suppress_callback_exceptions=True ) # Stili comuni CARD_STYLE = { "height": "100%", - "margin-bottom": "15px" + "marginBottom": "15px" } CARD_BODY_STYLE = { "padding": "15px" } -# Modifiche al layout - aggiungi tooltips per chiarire la funzionalità -variety2_tooltip = dbc.Tooltip( - "Seleziona una seconda varietà per creare un mix", - target="variety-2-dropdown", - placement="top" -) - -variety3_tooltip = dbc.Tooltip( - "Seleziona una terza varietà per completare il mix", - target="variety-3-dropdown", - placement="top" -) - - -def create_configuration_tab(): - return dbc.Tab([ - dbc.Row([ - # Configurazione Oliveto - dbc.Col([ - create_costs_config_section() - ], md=6), - dbc.Col([ - dbc.Card([ - dbc.CardHeader([ - html.H4("Configurazione Oliveto", className="text-primary mb-0"), - ], className="bg-light"), - dbc.CardBody([ - # Hectares input - dbc.Row([ - dbc.Col([ - dbc.Label("Ettari totali:", className="fw-bold"), - dbc.Input( - id='hectares-input', - type='number', - value=1, - min=1, - className="mb-3" - ) - ]) - ]), - - # Variety sections - html.Div([ - # Variety 1 - html.Div([ - html.H6("Varietà 1", className="text-primary mb-3"), - dbc.Row([ - dbc.Col([ - dbc.Label("Varietà:", className="fw-bold"), - dcc.Dropdown( - id='variety-1-dropdown', - options=[{'label': v, 'value': v} - for v in olive_varieties['Varietà di Olive'].unique()], - value=olive_varieties['Varietà di Olive'].iloc[0], - className="mb-2" - ), - ], md=4), - dbc.Col([ - dbc.Label("Tecnica:", className="fw-bold"), - dcc.Dropdown( - id='technique-1-dropdown', - options=[ - {'label': 'Tradizionale', 'value': 'tradizionale'}, - {'label': 'Intensiva', 'value': 'intensiva'}, - {'label': 'Superintensiva', 'value': 'superintensiva'} - ], - value='Tradizionale', - className="mb-2" - ), - ], md=4), - dbc.Col([ - dbc.Label("Percentuale:", className="fw-bold"), - dbc.Input( - id='percentage-1-input', - type='number', - min=1, - max=100, - value=100, - className="mb-2" - ) - ], md=4) - ]) - ], className="mb-4"), - - # Variety 2 - html.Div([ - html.H6("Varietà 2 (opzionale)", className="text-primary mb-3"), - dbc.Row([ - dbc.Col([ - dbc.Label("Varietà:", className="fw-bold"), - dcc.Dropdown( - id='variety-2-dropdown', - options=[{'label': v, 'value': v} - for v in olive_varieties['Varietà di Olive'].unique()], - value=None, - className="mb-2" - ), - ], md=4), - dbc.Col([ - dbc.Label("Tecnica:", className="fw-bold"), - dcc.Dropdown( - id='technique-2-dropdown', - options=[ - {'label': 'Tradizionale', 'value': 'tradizionale'}, - {'label': 'Intensiva', 'value': 'intensiva'}, - {'label': 'Superintensiva', 'value': 'superintensiva'} - ], - value=None, - disabled=True, - className="mb-2" - ), - ], md=4), - dbc.Col([ - dbc.Label("Percentuale:", className="fw-bold"), - dbc.Input( - id='percentage-2-input', - type='number', - min=0, - max=99, - value=0, - disabled=True, - className="mb-2" - ) - ], md=4) - ]) - ], className="mb-4"), - - # Variety 3 - html.Div([ - html.H6("Varietà 3 (opzionale)", className="text-primary mb-3"), - dbc.Row([ - dbc.Col([ - dbc.Label("Varietà:", className="fw-bold"), - dcc.Dropdown( - id='variety-3-dropdown', - options=[{'label': v, 'value': v} - for v in olive_varieties['Varietà di Olive'].unique()], - value=None, - className="mb-2" - ), - ], md=4), - dbc.Col([ - dbc.Label("Tecnica:", className="fw-bold"), - dcc.Dropdown( - id='technique-3-dropdown', - options=[ - {'label': 'Tradizionale', 'value': 'tradizionale'}, - {'label': 'Intensiva', 'value': 'intensiva'}, - {'label': 'Superintensiva', 'value': 'superintensiva'} - ], - value=None, - disabled=True, - className="mb-2" - ), - ], md=4), - dbc.Col([ - dbc.Label("Percentuale:", className="fw-bold"), - dbc.Input( - id='percentage-3-input', - type='number', - min=0, - max=99, - value=0, - disabled=True, - className="mb-2" - ) - ], md=4) - ]) - ], className="mb-4"), - ]), - - # Warning message - html.Div( - id='percentage-warning', - className="text-danger mt-3" - ) - ]) - ], className="mb-4") - ], md=6), - dbc.Row([ - dbc.Col([ - create_inference_config_section() - ], md=12) - ]), - # Configurazione Costi - html.Div([ - dbc.Button( - "Salva Configurazione", - id="save-config-button", - color="primary", - className="mt-3" - ), - html.Div( - id="save-config-message", - className="mt-2" - ) - ], className="text-center") - ]) - ], label="Configurazione", tab_id="tab-config") - - -@app.callback( - Output('loading-alert', 'children'), - [Input('simulate-btn', 'n_clicks'), - Input('debug-switch', 'value')] -) -def update_loading_status(n_clicks, debug_mode): - if MODEL_LOADING: - return dbc.Alert( - [ - html.I(className="fas fa-spinner fa-spin me-2"), - "Caricamento del modello in corso..." - ], - color="warning", - is_open=True - ) - return None - - -@app.callback( - [Output('inference-status', 'children'), - Output('inference-mode', 'children'), - Output('inference-latency', 'children'), - Output('inference-requests', 'children')], - [Input('debug-switch', 'value')] -) -def toggle_inference_mode(debug_mode): - global DEV_MODE, model, MODEL_LOADING, scaler_temporal, scaler_static, scaler_y - try: - config = load_config() - - # Aggiorna la modalità debug nella configurazione - config['inference'] = config.get('inference', {}) # Crea la sezione se non esiste - config['inference']['debug_mode'] = debug_mode - - # Salva la configurazione aggiornata - try: - with open(CONFIG_FILE, 'w') as f: - json.dump(config, f, indent=4) - except Exception as e: - print(f"Errore nel salvataggio della configurazione: {e}") - - DEV_MODE = debug_mode - - if debug_mode: - MODEL_LOADING = False - model = None - return ( - dbc.Alert("Modalità Debug attiva - Using mock predictions", color="info"), - "Debug (Mock)", - "< 1ms", - "N/A" - ) - else: - try: - MODEL_LOADING = True - print(f"Keras version: {keras.__version__}") - print(f"TensorFlow version: {tf.__version__}") - print(f"CUDA available: {tf.test.is_built_with_cuda()}") - print(f"GPU devices: {tf.config.list_physical_devices('GPU')}") - - # GPU memory configuration - gpus = tf.config.experimental.list_physical_devices('GPU') - if gpus: - try: - for gpu in gpus: - tf.config.experimental.set_memory_growth(gpu, True) - - logical_gpus = tf.config.experimental.list_logical_devices('GPU') - print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs") - except RuntimeError as e: - print(e) - - @keras.saving.register_keras_serializable() - class DataAugmentation(tf.keras.layers.Layer): - """Custom layer per l'augmentation dei dati""" - - def __init__(self, noise_stddev=0.03, **kwargs): - super().__init__(**kwargs) - self.noise_stddev = noise_stddev - - def call(self, inputs, training=None): - if training: - return inputs + tf.random.normal( - shape=tf.shape(inputs), - mean=0.0, - stddev=self.noise_stddev - ) - return inputs - - def get_config(self): - config = super().get_config() - config.update({"noise_stddev": self.noise_stddev}) - return config - - @keras.saving.register_keras_serializable() - class PositionalEncoding(tf.keras.layers.Layer): - """Custom layer per l'encoding posizionale""" - - def __init__(self, d_model, **kwargs): - super().__init__(**kwargs) - self.d_model = d_model - - def build(self, input_shape): - _, seq_length, _ = input_shape - - # Crea la matrice di encoding posizionale - position = tf.range(seq_length, dtype=tf.float32)[:, tf.newaxis] - div_term = tf.exp( - tf.range(0, self.d_model, 2, dtype=tf.float32) * - (-tf.math.log(10000.0) / self.d_model) - ) - - # Calcola sin e cos - pos_encoding = tf.zeros((1, seq_length, self.d_model)) - pos_encoding_even = tf.sin(position * div_term) - pos_encoding_odd = tf.cos(position * div_term) - - # Assegna i valori alle posizioni pari e dispari - pos_encoding = tf.concat( - [tf.expand_dims(pos_encoding_even, -1), - tf.expand_dims(pos_encoding_odd, -1)], - axis=-1 - ) - pos_encoding = tf.reshape(pos_encoding, (1, seq_length, -1)) - pos_encoding = pos_encoding[:, :, :self.d_model] - - # Salva l'encoding come peso non trainabile - self.pos_encoding = self.add_weight( - shape=(1, seq_length, self.d_model), - initializer=tf.keras.initializers.Constant(pos_encoding), - trainable=False, - name='positional_encoding' - ) - - super().build(input_shape) - - def call(self, inputs): - # Broadcast l'encoding posizionale sul batch - batch_size = tf.shape(inputs)[0] - pos_encoding_tiled = tf.tile(self.pos_encoding, [batch_size, 1, 1]) - return inputs + pos_encoding_tiled - - def get_config(self): - config = super().get_config() - config.update({"d_model": self.d_model}) - return config - - @keras.saving.register_keras_serializable() - class WarmUpLearningRateSchedule(tf.keras.optimizers.schedules.LearningRateSchedule): - """Custom learning rate schedule with linear warmup and exponential decay.""" - - def __init__(self, initial_learning_rate=1e-3, warmup_steps=500, decay_steps=5000): - super().__init__() - self.initial_learning_rate = initial_learning_rate - self.warmup_steps = warmup_steps - self.decay_steps = decay_steps - - def __call__(self, step): - warmup_pct = tf.cast(step, tf.float32) / self.warmup_steps - warmup_lr = self.initial_learning_rate * warmup_pct - decay_factor = tf.pow(0.1, tf.cast(step, tf.float32) / self.decay_steps) - decayed_lr = self.initial_learning_rate * decay_factor - return tf.where(step < self.warmup_steps, warmup_lr, decayed_lr) - - def get_config(self): - return { - 'initial_learning_rate': self.initial_learning_rate, - 'warmup_steps': self.warmup_steps, - 'decay_steps': self.decay_steps - } - - @keras.saving.register_keras_serializable() - def weighted_huber_loss(y_true, y_pred): - # Pesi per diversi output - weights = tf.constant([1.0, 0.8, 0.8, 1.0, 0.6], dtype=tf.float32) - huber = tf.keras.losses.Huber(delta=1.0) - loss = huber(y_true, y_pred) - weighted_loss = tf.reduce_mean(loss * weights) - return weighted_loss - - print("Caricamento modello e scaler...") - - # Verifica che il modello sia disponibile - model_path = './sources/olive_oil_transformer/olive_oil_transformer_model.keras' - if not os.path.exists(model_path): - raise FileNotFoundError(f"Modello non trovato in: {model_path}") - - # Prova a caricare il modello - model = tf.keras.models.load_model(model_path, custom_objects={ - 'DataAugmentation': DataAugmentation, - 'PositionalEncoding': PositionalEncoding, - 'WarmUpLearningRateSchedule': WarmUpLearningRateSchedule, - 'weighted_huber_loss': weighted_huber_loss - }) - MODEL_LOADING = False - return ( - dbc.Alert("Modello caricato correttamente", color="success"), - "Produzione (Local Model)", - "~ 100ms", - "0" - ) - except Exception as e: - print(f"Errore nel caricamento del modello: {str(e)}") - # Se c'è un errore nel caricamento del modello, torna in modalità debug - DEV_MODE = True - MODEL_LOADING = False - return ( - dbc.Alert(f"Errore nel caricamento del modello: {str(e)}", color="danger"), - "Debug (Mock) - Fallback", - "N/A", - "N/A" - ) - except Exception as e: - print(f"Errore nella configurazione inferenza: {str(e)}") - MODEL_LOADING = False - return ( - dbc.Alert(f"Errore: {str(e)}", color="danger"), - "Errore", - "Errore", - "Errore" - ) - - -def create_economic_analysis_tab(): - return dbc.Tab([ - # Sezione Costi di Trasformazione - dbc.Row([ - dbc.Col([ - dbc.Card([ - dbc.CardHeader("Costi di Trasformazione"), - dbc.CardBody([ - dbc.Row([ - dbc.Col([ - html.H5("Frantoio", className="mb-3"), - dbc.ListGroup([ - dbc.ListGroupItem([ - html.Strong("Molitura: "), - "€0.15/kg olive" - ]), - dbc.ListGroupItem([ - html.Strong("Stoccaggio: "), - "€0.20/L olio" - ]) - ], flush=True) - ], md=6), - dbc.Col([ - html.H5("Imbottigliamento", className="mb-3"), - dbc.ListGroup([ - dbc.ListGroupItem([ - html.Strong("Bottiglia (1L): "), - "€1.20/unità" - ]), - dbc.ListGroupItem([ - html.Strong("Etichettatura: "), - "€0.30/bottiglia" - ]) - ], flush=True) - ], md=6) - ]) - ]) - ], style=CARD_STYLE) - ], md=12) - ]), - - # Sezione Ricavi e Guadagni - dbc.Row([ - dbc.Col([ - dbc.Card([ - dbc.CardHeader("Analisi Economica"), - dbc.CardBody([ - dbc.Row([ - dbc.Col([ - html.H5("Ricavi", className="mb-3"), - dbc.ListGroup([ - dbc.ListGroupItem([ - html.Strong("Prezzo vendita olio: "), - "€12.00/L" - ]), - dbc.ListGroupItem([ - html.Strong("Ricavo totale: "), - "€48,000.00" - ]) - ], flush=True) - ], md=4), - dbc.Col([ - html.H5("Costi Totali", className="mb-3"), - dbc.ListGroup([ - dbc.ListGroupItem([ - html.Strong("Costi produzione: "), - "€25,000.00" - ]), - dbc.ListGroupItem([ - html.Strong("Costi trasformazione: "), - "€8,000.00" - ]) - ], flush=True) - ], md=4), - dbc.Col([ - html.H5("Margini", className="mb-3"), - dbc.ListGroup([ - dbc.ListGroupItem([ - html.Strong("Margine lordo: "), - "€15,000.00" - ]), - dbc.ListGroupItem([ - html.Strong("Margine per litro: "), - "€3.75/L" - ]) - ], flush=True) - ], md=4) - ]) - ]) - ], style=CARD_STYLE) - ], md=12) - ]), - - # Grafici Finanziari - dbc.Row([ - dbc.Col([ - dbc.Card([ - dbc.CardHeader("Distribuzione Costi"), - dbc.CardBody([ - dcc.Graph( - figure=px.pie( - values=[2000, 500, 800, 1500, 600, 400], - names=['Ammortamento', 'Assicurazione', 'Manutenzione', - 'Raccolta', 'Potatura', 'Fertilizzanti'], - title='Distribuzione Costi per Ettaro' - ), - config={'displayModeBar': False} - ) - ]) - ], style=CARD_STYLE) - ], md=6), - dbc.Col([ - dbc.Card([ - dbc.CardHeader("Analisi Break-Even"), - dbc.CardBody([ - dcc.Graph( - figure=px.line( - x=[0, 1000, 2000, 3000, 4000], - y=[[0, 12000, 24000, 36000, 48000], - [5000, 15000, 25000, 35000, 45000]], - title='Analisi Break-Even', - labels={'x': 'Litri di olio', 'y': 'Euro'} - ), - config={'displayModeBar': False} - ) - ]) - ], style=CARD_STYLE) - ], md=6) - ]) - ], label="Analisi Economica", tab_id="tab-financial") - def create_production_tab(): return dbc.Tab([ + dbc.Row([ + # Aggiungiamo un card per lo stato del modello + dbc.Col([ + dbc.Card([ + dbc.CardHeader([ + html.H4("Modalità Inferenza", className="text-primary mb-0"), + ], className="bg-light"), + dbc.CardBody([ + dbc.Row([ + dbc.Col([ + html.Div([ + html.P("Stato corrente:", className="mb-2"), + html.H5(id=Ids.PRODUCTION_INFERENCE_MODE, className="text-info") + ], className="text-center") + ], width=8), + dbc.Col([ + dbc.Switch( + id=Ids.PRODUCTION_DEBUG_SWITCH, + label="Modalità Debug", + value=True, + className="mt-2" + ), + ], width=4), + ]), + html.Hr(), + dbc.Row([ + dbc.Col([ + html.P("Richieste totali:", className="mb-2"), + html.H5(id=Ids.PRODUCTION_INFERENCE_REQUESTS, className="text-muted") + ], className="text-center") + ]) + ]) + ], className="mb-4"), + ], md=12), + ]), dbc.Row([ dbc.Col([ dbc.Card([ @@ -1526,178 +1032,323 @@ def create_environmental_simulation_tab(): ], label="Simulazione Ambientale") -def create_growth_simulation_figure(sim_data: pd.DataFrame) -> go.Figure: - """Crea il grafico della simulazione di crescita""" - fig = make_subplots(specs=[[{"secondary_y": True}]]) +def create_economic_analysis_tab(): + return dbc.Tab([ + # Sezione Costi di Trasformazione + dbc.Row([ + dbc.Col([ + dbc.Card([ + dbc.CardHeader("Costi di Trasformazione"), + dbc.CardBody([ + dbc.Row([ + dbc.Col([ + html.H5("Frantoio", className="mb-3"), + dbc.ListGroup([ + dbc.ListGroupItem([ + html.Strong("Molitura: "), + "€0.15/kg olive" + ]), + dbc.ListGroupItem([ + html.Strong("Stoccaggio: "), + "€0.20/L olio" + ]) + ], flush=True) + ], md=6), + dbc.Col([ + html.H5("Imbottigliamento", className="mb-3"), + dbc.ListGroup([ + dbc.ListGroupItem([ + html.Strong("Bottiglia (1L): "), + "€1.20/unità" + ]), + dbc.ListGroupItem([ + html.Strong("Etichettatura: "), + "€0.30/bottiglia" + ]) + ], flush=True) + ], md=6) + ]) + ]) + ], style=CARD_STYLE) + ], md=12) + ]), - # Aggiunge la linea di crescita - fig.add_trace( - go.Scatter( - x=sim_data['date'], - y=sim_data['growth_rate'], - name="Tasso di Crescita", - line=dict(color='#2E86C1', width=2) - ), - secondary_y=False - ) + # Sezione Ricavi e Guadagni + dbc.Row([ + dbc.Col([ + dbc.Card([ + dbc.CardHeader("Analisi Economica"), + dbc.CardBody([ + dbc.Row([ + dbc.Col([ + html.H5("Ricavi", className="mb-3"), + dbc.ListGroup([ + dbc.ListGroupItem([ + html.Strong("Prezzo vendita olio: "), + "€12.00/L" + ]), + dbc.ListGroupItem([ + html.Strong("Ricavo totale: "), + "€48,000.00" + ]) + ], flush=True) + ], md=4), + dbc.Col([ + html.H5("Costi Totali", className="mb-3"), + dbc.ListGroup([ + dbc.ListGroupItem([ + html.Strong("Costi produzione: "), + "€25,000.00" + ]), + dbc.ListGroupItem([ + html.Strong("Costi trasformazione: "), + "€8,000.00" + ]) + ], flush=True) + ], md=4), + dbc.Col([ + html.H5("Margini", className="mb-3"), + dbc.ListGroup([ + dbc.ListGroupItem([ + html.Strong("Margine lordo: "), + "€15,000.00" + ]), + dbc.ListGroupItem([ + html.Strong("Margine per litro: "), + "€3.75/L" + ]) + ], flush=True) + ], md=4) + ]) + ]) + ], style=CARD_STYLE) + ], md=12) + ]), - # Aggiunge l'indice di stress - fig.add_trace( - go.Scatter( - x=sim_data['date'], - y=sim_data['stress_index'], - name="Indice di Stress", - line=dict(color='#E74C3C', width=2) - ), - secondary_y=True - ) - - # Aggiungi indicatori delle fasi - for phase in sim_data['phase'].unique(): - phase_data = sim_data[sim_data['phase'] == phase] - fig.add_trace( - go.Scatter( - x=[phase_data['date'].iloc[0]], - y=[0], - name=phase, - mode='markers+text', - text=[phase], - textposition='top center', - marker=dict(size=10) - ), - secondary_y=False - ) - - # Configurazione layout - fig.update_layout( - title='Simulazione Crescita e Stress Ambientale', - xaxis_title='Data', - yaxis_title='Tasso di Crescita (%)', - yaxis2_title='Indice di Stress', - hovermode='x unified', - showlegend=True, - height=500 - ) - - return fig + # Grafici Finanziari + dbc.Row([ + dbc.Col([ + dbc.Card([ + dbc.CardHeader("Distribuzione Costi"), + dbc.CardBody([ + dcc.Graph( + figure=px.pie( + values=[2000, 500, 800, 1500, 600, 400], + names=['Ammortamento', 'Assicurazione', 'Manutenzione', + 'Raccolta', 'Potatura', 'Fertilizzanti'], + title='Distribuzione Costi per Ettaro' + ), + config={'displayModeBar': False} + ) + ]) + ], style=CARD_STYLE) + ], md=6), + dbc.Col([ + dbc.Card([ + dbc.CardHeader("Analisi Break-Even"), + dbc.CardBody([ + dcc.Graph( + figure=px.line( + x=[0, 1000, 2000, 3000, 4000], + y=[[0, 12000, 24000, 36000, 48000], + [5000, 15000, 25000, 35000, 45000]], + title='Analisi Break-Even', + labels={'x': 'Litri di olio', 'y': 'Euro'} + ), + config={'displayModeBar': False} + ) + ]) + ], style=CARD_STYLE) + ], md=6) + ]) + ], label="Analisi Economica", tab_id="tab-financial") -def create_production_impact_figure(sim_data: pd.DataFrame) -> go.Figure: - """ - Crea il grafico dell'impatto sulla produzione, con gestione corretta del resampling mensile. +def create_configuration_tab(): + return dbc.Tab([ + dbc.Row([ + # Configurazione Oliveto + dbc.Col([ + create_costs_config_section() + ], md=6), + dbc.Col([ + dbc.Card([ + dbc.CardHeader([ + html.H4("Configurazione Oliveto", className="text-primary mb-0"), + ], className="bg-light"), + dbc.CardBody([ + # Hectares input + dbc.Row([ + dbc.Col([ + dbc.Label("Ettari totali:", className="fw-bold"), + dbc.Input( + id='hectares-input', + type='number', + value=1, + min=1, + className="mb-3" + ) + ]) + ]), - Parameters - ---------- - sim_data : pd.DataFrame - DataFrame contenente i dati della simulazione con colonne: - - date: datetime - - stress_index: float - - phase: str - - temperature: float - - growth_rate: float + # Variety sections + html.Div([ + # Variety 1 + html.Div([ + html.H6("Varietà 1", className="text-primary mb-3"), + dbc.Row([ + dbc.Col([ + dbc.Label("Varietà:", className="fw-bold"), + dcc.Dropdown( + id='variety-1-dropdown', + options=[{'label': v, 'value': v} + for v in olive_varieties['Varietà di Olive'].unique()], + value=olive_varieties['Varietà di Olive'].iloc[0], + className="mb-2" + ), + ], md=4), + dbc.Col([ + dbc.Label("Tecnica:", className="fw-bold"), + dcc.Dropdown( + id='technique-1-dropdown', + options=[ + {'label': 'Tradizionale', 'value': 'tradizionale'}, + {'label': 'Intensiva', 'value': 'intensiva'}, + {'label': 'Superintensiva', 'value': 'superintensiva'} + ], + value='Tradizionale', + className="mb-2" + ), + ], md=4), + dbc.Col([ + dbc.Label("Percentuale:", className="fw-bold"), + dbc.Input( + id='percentage-1-input', + type='number', + min=1, + max=100, + value=100, + className="mb-2" + ) + ], md=4) + ]) + ], className="mb-4"), - Returns - ------- - go.Figure - Figura Plotly con il grafico dell'impatto sulla produzione - """ - # Verifica che la colonna date sia datetime - if not pd.api.types.is_datetime64_any_dtype(sim_data['date']): - sim_data['date'] = pd.to_datetime(sim_data['date']) + # Variety 2 + html.Div([ + html.H6("Varietà 2 (opzionale)", className="text-primary mb-3"), + dbc.Row([ + dbc.Col([ + dbc.Label("Varietà:", className="fw-bold"), + dcc.Dropdown( + id='variety-2-dropdown', + options=[{'label': v, 'value': v} + for v in olive_varieties['Varietà di Olive'].unique()], + value=None, + className="mb-2" + ), + ], md=4), + dbc.Col([ + dbc.Label("Tecnica:", className="fw-bold"), + dcc.Dropdown( + id='technique-2-dropdown', + options=[ + {'label': 'Tradizionale', 'value': 'tradizionale'}, + {'label': 'Intensiva', 'value': 'intensiva'}, + {'label': 'Superintensiva', 'value': 'superintensiva'} + ], + value=None, + disabled=True, + className="mb-2" + ), + ], md=4), + dbc.Col([ + dbc.Label("Percentuale:", className="fw-bold"), + dbc.Input( + id='percentage-2-input', + type='number', + min=0, + max=99, + value=0, + disabled=True, + className="mb-2" + ) + ], md=4) + ]) + ], className="mb-4"), - # Setta l'indice come datetime - sim_data_indexed = sim_data.set_index('date') + # Variety 3 + html.Div([ + html.H6("Varietà 3 (opzionale)", className="text-primary mb-3"), + dbc.Row([ + dbc.Col([ + dbc.Label("Varietà:", className="fw-bold"), + dcc.Dropdown( + id='variety-3-dropdown', + options=[{'label': v, 'value': v} + for v in olive_varieties['Varietà di Olive'].unique()], + value=None, + className="mb-2" + ), + ], md=4), + dbc.Col([ + dbc.Label("Tecnica:", className="fw-bold"), + dcc.Dropdown( + id='technique-3-dropdown', + options=[ + {'label': 'Tradizionale', 'value': 'tradizionale'}, + {'label': 'Intensiva', 'value': 'intensiva'}, + {'label': 'Superintensiva', 'value': 'superintensiva'} + ], + value=None, + disabled=True, + className="mb-2" + ), + ], md=4), + dbc.Col([ + dbc.Label("Percentuale:", className="fw-bold"), + dbc.Input( + id='percentage-3-input', + type='number', + min=0, + max=99, + value=0, + disabled=True, + className="mb-2" + ) + ], md=4) + ]) + ], className="mb-4"), + ]), - # Calcola medie mensili solo per le colonne numeriche - numeric_columns = ['stress_index', 'temperature', 'growth_rate'] - monthly_means = {} - - for col in numeric_columns: - if col in sim_data_indexed.columns: - monthly_means[col] = sim_data_indexed[col].resample('ME').mean() - - # Crea DataFrame con le medie mensili - monthly_data = pd.DataFrame(monthly_means) - - # Crea la figura - fig = go.Figure() - - # Calcola la produzione stimata (100% - stress%) - production_estimate = 100 * (1 - monthly_data['stress_index']) - - # Aggiungi traccia principale - fig.add_trace( - go.Bar( - x=monthly_data.index, - y=production_estimate, - name='Produzione Stimata (%)', - marker_color='#27AE60' - ) - ) - - # Aggiungi linea di trend - fig.add_trace( - go.Scatter( - x=monthly_data.index, - y=production_estimate.rolling(window=3, min_periods=1).mean(), - name='Trend (Media Mobile 3 mesi)', - line=dict(color='#2C3E50', width=2, dash='dot') - ) - ) - - # Configura il layout - fig.update_layout( - title={ - 'text': 'Impatto Stimato sulla Produzione', - 'x': 0.5, - 'xanchor': 'center', - 'font': dict(size=20) - }, - xaxis_title='Mese', - yaxis_title='Produzione Stimata (%)', - hovermode='x unified', - showlegend=True, - height=500, - template='plotly_white', - yaxis=dict( - range=[0, 100], - tickformat='.0f', - ticksuffix='%' - ), - xaxis=dict( - tickformat='%B %Y', - tickangle=45 - ), - legend=dict( - yanchor="top", - y=0.99, - xanchor="left", - x=0.01 - ), - margin=dict(t=80, b=80) - ) - - # Aggiungi annotazioni per valori estremi - min_prod = production_estimate.min() - max_prod = production_estimate.max() - - fig.add_annotation( - x=production_estimate.idxmin(), - y=min_prod, - text=f"Min: {min_prod:.1f}%", - showarrow=True, - arrowhead=1 - ) - - fig.add_annotation( - x=production_estimate.idxmax(), - y=max_prod, - text=f"Max: {max_prod:.1f}%", - showarrow=True, - arrowhead=1 - ) - - return fig + # Warning message + html.Div( + id='percentage-warning', + className="text-danger mt-3" + ) + ]) + ], className="mb-4") + ], md=6), + dbc.Row([ + dbc.Col([ + create_inference_config_section() + ], md=12) + ]), + # Configurazione Costi + html.Div([ + dbc.Button( + "Salva Configurazione", + id="save-config-button", + color="primary", + className="mt-3" + ), + html.Div( + id="save-config-message", + className="mt-2" + ) + ], className="text-center") + ]) + ], label="Configurazione", tab_id="tab-config") def create_costs_config_section(): @@ -1906,105 +1557,294 @@ def create_costs_config_section(): def create_inference_config_section(): - config = load_config() - debug_mode = config.get('inference', {}).get('debug_mode', True) - return dbc.Card([ - dbc.CardHeader([ - html.H4("Configurazione Inferenza", className="text-primary mb-0"), - dbc.Switch( - id='debug-switch', - label="Modalità Debug", - value=debug_mode, - className="mt-2" - ), - ], className="bg-light"), - dbc.CardBody([ - # Stato del servizio - dbc.Row([ - dbc.Col([ - html.Div([ - html.H5("Stato Servizio", className="mb-3"), - html.Div(id='inference-status', className="mb-3"), - ]) - ]) - ], className="mb-4"), - - # Configurazioni - dbc.Row([ - dbc.Col([ - html.H5("Configurazioni", className="mb-3"), - dbc.Form([ - dbc.Row([ - dbc.Col([ - dbc.Label("Modello:", className="fw-bold"), - # Usa html.Div invece di dbc.Input per il percorso in sola lettura - html.Div( - "./sources/olive_oil_transformer/olive_oil_transformer_model.keras", - id='model-path', - className="mb-2 p-2 bg-light border rounded", - style={ - "font-family": "monospace", - "font-size": "0.9rem" - } - ) - ], md=12), - ]), - ]) - ]) - ]), - - # Metriche e monitoraggio - dbc.Row([ - dbc.Col([ - html.H5("Metriche", className="mb-3"), - dbc.ListGroup([ - dbc.ListGroupItem([ - html.Strong("Modalità: "), - html.Span(id='inference-mode') - ]), - dbc.ListGroupItem([ - html.Strong("Latenza media: "), - html.Span(id='inference-latency') - ]), - dbc.ListGroupItem([ - html.Strong("Richieste totali: "), - html.Span(id='inference-requests') + html.Div(id=Ids.INFERENCE_CONTAINER, children=[ + dbc.CardHeader([ + html.H4("Configurazione Inferenza", className="text-primary mb-0"), + dbc.Switch( + id=Ids.DEBUG_SWITCH, + label="Modalità Debug", + value=True, + className="mt-2" + ), + ], className="bg-light"), + dbc.CardBody([ + # Stato del servizio + dbc.Row([ + dbc.Col([ + html.Div([ + html.H5("Stato Servizio", className="mb-3"), + html.Div(id=Ids.INFERENCE_STATUS, className="mb-3"), ]) - ], flush=True) - ]) - ], className="mt-4") + ]) + ], className="mb-4"), + + # Metriche e monitoraggio + dbc.Row([ + dbc.Col([ + html.H5("Metriche", className="mb-3"), + dbc.ListGroup([ + dbc.ListGroupItem([ + html.Strong("Modalità: "), + html.Span(id=Ids.INFERENCE_MODE) + ]), + dbc.ListGroupItem([ + html.Strong("Latenza media: "), + html.Span(id=Ids.INFERENCE_LATENCY) + ]), + dbc.ListGroupItem([ + html.Strong("Richieste totali: "), + html.Span(id=Ids.INFERENCE_REQUESTS) + ]) + ], flush=True) + ]) + ], className="mt-4") + ]) ]) ]) -app.layout = dbc.Container([ - dcc.Location(id='_pages_location'), - html.Div(id='loading-alert'), - variety2_tooltip, - variety3_tooltip, - # Header - dbc.Row([ - dbc.Col([ - html.Div([ - html.H1("Dashboard Produzione Olio d'Oliva", - className="text-primary text-center mb-3") - ], className="mt-4 mb-4") - ]) - ]), +def create_growth_simulation_figure(sim_data: pd.DataFrame) -> go.Figure: + """Crea il grafico della simulazione di crescita""" + fig = make_subplots(specs=[[{"secondary_y": True}]]) - # Main content - dbc.Row([ - dbc.Col([ - dbc.Tabs([ - create_production_tab(), - create_environmental_simulation_tab(), - create_economic_analysis_tab(), - create_configuration_tab(), - ], id="tabs", active_tab="tab-production") - ], md=12, lg=12) - ]) -], fluid=True, className="px-4 py-3") + # Aggiunge la linea di crescita + fig.add_trace( + go.Scatter( + x=sim_data['date'], + y=sim_data['growth_rate'], + name="Tasso di Crescita", + line=dict(color='#2E86C1', width=2) + ), + secondary_y=False + ) + + # Aggiunge l'indice di stress + fig.add_trace( + go.Scatter( + x=sim_data['date'], + y=sim_data['stress_index'], + name="Indice di Stress", + line=dict(color='#E74C3C', width=2) + ), + secondary_y=True + ) + + # Aggiungi indicatori delle fasi + for phase in sim_data['phase'].unique(): + phase_data = sim_data[sim_data['phase'] == phase] + fig.add_trace( + go.Scatter( + x=[phase_data['date'].iloc[0]], + y=[0], + name=phase, + mode='markers+text', + text=[phase], + textposition='top center', + marker=dict(size=10) + ), + secondary_y=False + ) + + # Configurazione layout + fig.update_layout( + title='Simulazione Crescita e Stress Ambientale', + xaxis_title='Data', + yaxis_title='Tasso di Crescita (%)', + yaxis2_title='Indice di Stress', + hovermode='x unified', + showlegend=True, + height=500 + ) + + return fig + + +def create_production_impact_figure(sim_data: pd.DataFrame) -> go.Figure: + """ + Crea il grafico dell'impatto sulla produzione, con gestione corretta del resampling mensile. + + Parameters + ---------- + sim_data : pd.DataFrame + DataFrame contenente i dati della simulazione con colonne: + - date: datetime + - stress_index: float + - phase: str + - temperature: float + - growth_rate: float + + Returns + ------- + go.Figure + Figura Plotly con il grafico dell'impatto sulla produzione + """ + # Verifica che la colonna date sia datetime + if not pd.api.types.is_datetime64_any_dtype(sim_data['date']): + sim_data['date'] = pd.to_datetime(sim_data['date']) + + # Setta l'indice come datetime + sim_data_indexed = sim_data.set_index('date') + + # Calcola medie mensili solo per le colonne numeriche + numeric_columns = ['stress_index', 'temperature', 'growth_rate'] + monthly_means = {} + + for col in numeric_columns: + if col in sim_data_indexed.columns: + monthly_means[col] = sim_data_indexed[col].resample('ME').mean() + + # Crea DataFrame con le medie mensili + monthly_data = pd.DataFrame(monthly_means) + + # Crea la figura + fig = go.Figure() + + # Calcola la produzione stimata (100% - stress%) + production_estimate = 100 * (1 - monthly_data['stress_index']) + + # Aggiungi traccia principale + fig.add_trace( + go.Bar( + x=monthly_data.index, + y=production_estimate, + name='Produzione Stimata (%)', + marker_color='#27AE60' + ) + ) + + # Aggiungi linea di trend + fig.add_trace( + go.Scatter( + x=monthly_data.index, + y=production_estimate.rolling(window=3, min_periods=1).mean(), + name='Trend (Media Mobile 3 mesi)', + line=dict(color='#2C3E50', width=2, dash='dot') + ) + ) + + # Configura il layout + fig.update_layout( + title={ + 'text': 'Impatto Stimato sulla Produzione', + 'x': 0.5, + 'xanchor': 'center', + 'font': dict(size=20) + }, + xaxis_title='Mese', + yaxis_title='Produzione Stimata (%)', + hovermode='x unified', + showlegend=True, + height=500, + template='plotly_white', + yaxis=dict( + range=[0, 100], + tickformat='.0f', + ticksuffix='%' + ), + xaxis=dict( + tickformat='%B %Y', + tickangle=45 + ), + legend=dict( + yanchor="top", + y=0.99, + xanchor="left", + x=0.01 + ), + margin=dict(t=80, b=80) + ) + + # Aggiungi annotazioni per valori estremi + min_prod = production_estimate.min() + max_prod = production_estimate.max() + + fig.add_annotation( + x=production_estimate.idxmin(), + y=min_prod, + text=f"Min: {min_prod:.1f}%", + showarrow=True, + arrowhead=1 + ) + + fig.add_annotation( + x=production_estimate.idxmax(), + y=max_prod, + text=f"Max: {max_prod:.1f}%", + showarrow=True, + arrowhead=1 + ) + + return fig + + +app.layout = html.Div([ + dcc.Location(id='url', refresh=False), + dcc.Store(id='session', storage_type='local'), + dcc.Store(id='user-data', storage_type='local'), + dcc.Store(id=Ids.INFERENCE_COUNTER, storage_type='session', data={'count': 0}), + dcc.Store(id=Ids.DEV_MODE, storage_type='session', data={'count': 0}), + html.Div(id=Ids.AUTH_CONTAINER), + html.Div(id=Ids.DASHBOARD_CONTAINER), +]) + + +def create_main_layout(): + return dbc.Container([ + dcc.Location(id='_pages_location'), + # Navbar con logout button + dbc.Navbar( + dbc.Container([ + dbc.NavbarBrand([ + html.I(className="fas fa-leaf me-2"), # Icona olivo + "Dashboard Produzione Olio d'Oliva" + ], className="text-white"), + dbc.Nav([ + dbc.Button( + [ + html.I(className="fas fa-sign-out-alt me-2"), # Icona logout + "Logout" + ], + id="logout-button", + color="light", + outline=True, + size="sm", + className="ms-2" + ) + ], className="ms-auto d-flex align-items-center") # Modificato qui per un migliore allineamento + ], + fluid=True, # Aggiunto per sfruttare tutta la larghezza + ), + color="primary", + dark=True, + className="mb-3" + ), + html.Div(id='loading-alert'), + dbc.Tooltip( + "Seleziona una seconda varietà per creare un mix", + target="variety-2-dropdown", + placement="top" + ), + dbc.Tooltip( + "Seleziona una terza varietà per completare il mix", + target="variety-3-dropdown", + placement="top" + ), + # Header + dbc.Row([ + dbc.Col([ + dbc.Tabs([ + create_production_tab(), + create_environmental_simulation_tab(), + create_economic_analysis_tab(), + create_configuration_tab(), + ], id="tabs", active_tab="tab-production") + ], md=12, lg=12) + ]), + + # Store per dati simulazione + dcc.Store(id='simulation-data') + ], fluid=True, className="px-4 py-3") def create_extra_info_component(prediction, varieties_info): @@ -2159,6 +1999,635 @@ def create_extra_info_component(prediction, varieties_info): ]) +def create_figure_layout(fig, title): + fig.update_layout( + title=title, + title_x=0.5, + margin=dict(l=20, r=20, t=40, b=20), + paper_bgcolor='rgba(0,0,0,0)', + plot_bgcolor='rgba(0,0,0,0)', + height=350, + font=dict(family="Helvetica, Arial, sans-serif"), + showlegend=True, + legend=dict( + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="right", + x=1 + ) + ) + return fig + + +def create_production_details_figure(prediction): + """Crea il grafico dei dettagli produzione con il nuovo stile""" + details_data = prepare_details_data(prediction) + fig = px.bar( + details_data, + x='Varietà', + y='Produzione', + color='Tipo', + barmode='group', + color_discrete_map={'Olive': '#2185d0', 'Olio': '#21ba45'} + ) + return create_figure_layout(fig, 'Dettagli Produzione per Varietà') + + +def create_weather_impact_figure(weather_data): + """Crea il grafico dell'impatto meteorologico con il nuovo stile""" + recent_weather = weather_data.tail(41).copy() + fig = px.scatter( + recent_weather, + x='temp', + y='solarradiation', + size='precip', + color_discrete_sequence=['#2185d0'] + ) + return create_figure_layout(fig, 'Condizioni Meteorologiche') + + +def create_water_needs_figure(prediction): + """Crea il grafico del fabbisogno idrico con il nuovo stile""" + # Definisci i mesi in italiano + months = ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', + 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'] + + water_data = [] + for detail in prediction['variety_details']: + for month in months: + season = get_season_from_month(month) + variety_info = olive_varieties[ + olive_varieties['Varietà di Olive'] == detail['variety'] + ].iloc[0] + + water_need = variety_info[f'Fabbisogno Acqua {season} (m³/ettaro)'] + water_data.append({ + 'Month': month, + 'Variety': detail['variety'], + 'Water_Need': water_need * (detail['percentage'] / 100) + }) + + water_df = pd.DataFrame(water_data) + fig = px.bar( + water_df, + x='Month', + y='Water_Need', + color='Variety', + barmode='stack', + color_discrete_sequence=['#2185d0', '#21ba45', '#6435c9'] + ) + + return create_figure_layout(fig, 'Fabbisogno Idrico Mensile') + + +def prepare_details_data(prediction): + """Prepara i dati per il grafico dei dettagli di produzione""" + details_data = [] + + # Dati per ogni varietà + for detail in prediction['variety_details']: + details_data.extend([ + { + 'Varietà': f"{detail['variety']} ({detail['percentage']}%)", + 'Tipo': 'Olive', + 'Produzione': detail['production_per_ha'] + }, + { + 'Varietà': f"{detail['variety']} ({detail['percentage']}%)", + 'Tipo': 'Olio', + 'Produzione': detail['oil_per_ha'] + } + ]) + + # Aggiungi totali + details_data.extend([ + { + 'Varietà': 'Totale', + 'Tipo': 'Olive', + 'Produzione': prediction['olive_production'] + }, + { + 'Varietà': 'Totale', + 'Tipo': 'Olio', + 'Produzione': prediction['avg_oil_production'] + } + ]) + + return pd.DataFrame(details_data) + + +def get_season_from_month(month): + """Helper function per determinare la stagione dal mese.""" + seasons = { + 'Gen': 'Inverno', 'Feb': 'Inverno', 'Mar': 'Primavera', + 'Apr': 'Primavera', 'Mag': 'Primavera', 'Giu': 'Estate', + 'Lug': 'Estate', 'Ago': 'Estate', 'Set': 'Autunno', + 'Ott': 'Autunno', 'Nov': 'Autunno', 'Dic': 'Inverno' + } + return seasons[month] + + +@app.callback( + Output('loading-alert', 'children'), + [Input('simulate-btn', 'n_clicks'), + Input('debug-switch', 'value')], + running=[ + (Output(Ids.DASHBOARD_CONTAINER, 'children'), + [Input('url', 'pathname')], + lambda x: x == '/') + ] +) +def update_loading_status(n_clicks, debug_mode): + global DEV_MODE + + config = load_config() + + print(config) + DEV_MODE = config['inference']['debug_mode'] + if MODEL_LOADING: + return dbc.Alert( + [ + html.I(className="fas fa-spinner fa-spin me-2"), + "Caricamento del modello in corso..." + ], + color="warning", + is_open=True + ) + return None + + +@app.callback( + [ + Output(Ids.PRODUCTION_INFERENCE_MODE, 'children'), + Output(Ids.PRODUCTION_INFERENCE_REQUESTS, 'children'), + Output(Ids.INFERENCE_COUNTER, 'data', allow_duplicate=True) + ], + [Input(Ids.PRODUCTION_DEBUG_SWITCH, 'value')], + [State(Ids.INFERENCE_COUNTER, 'data')], + prevent_initial_call=True +) +def update_inference_status(debug_mode, counter_data): + try: + toggle_inference_mode(debug_mode) + mode_text = "Debug (Mock)" if debug_mode else "Produzione (Model)" + # Resetta il contatore + new_counter_data = {'count': 0} + return mode_text, "0", new_counter_data + except Exception as e: + print(f"Errore nell'aggiornamento dello stato di inferenza: {e}") + return "Errore", "N/A", {'count': 0} + + +@app.callback( + [ + Output(Ids.INFERENCE_STATUS, 'children'), + Output(Ids.INFERENCE_MODE, 'children', allow_duplicate=True), + Output(Ids.INFERENCE_LATENCY, 'children'), + Output(Ids.INFERENCE_REQUESTS, 'children', allow_duplicate=True), + Output(Ids.INFERENCE_COUNTER, 'data', allow_duplicate=True) + ], + [Input(Ids.INFERENCE_CONTAINER, 'value')], + running=[ + (Output(Ids.DASHBOARD_CONTAINER, 'children'), + [Input('url', 'pathname')], + lambda x: x == '/') + ], + prevent_initial_call=True +) +def toggle_inference_mode(debug_mode): + global DEV_MODE, model, MODEL_LOADING, scaler_temporal, scaler_static, scaler_y + new_counter_data = {'count': 0} + try: + config = load_config() + print(f"debug mode: {debug_mode}") + # Aggiorna la modalità debug nella configurazione + config['inference'] = config.get('inference', {}) # Crea la sezione se non esiste + config['inference']['debug_mode'] = debug_mode + + DEV_MODE = debug_mode + print(f"DEV_MODE: {DEV_MODE}") + dcc.Store(id=Ids.INFERENCE_COUNTER, data=new_counter_data) + if debug_mode: + + MODEL_LOADING = False + return ( + dbc.Alert("Modalità Debug attiva - Using mock predictions", color="info"), + "Debug (Mock)", + "< 1ms", + "N/A", + new_counter_data + ) + else: + if model is None: + try: + MODEL_LOADING = True + print(f"Keras version: {keras.__version__}") + print(f"TensorFlow version: {tf.__version__}") + print(f"CUDA available: {tf.test.is_built_with_cuda()}") + print(f"GPU devices: {tf.config.list_physical_devices('GPU')}") + + # GPU memory configuration + gpus = tf.config.experimental.list_physical_devices('GPU') + if gpus: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + + logical_gpus = tf.config.experimental.list_logical_devices('GPU') + print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs") + except RuntimeError as e: + print(e) + + @keras.saving.register_keras_serializable() + class DataAugmentation(tf.keras.layers.Layer): + """Custom layer per l'augmentation dei dati""" + + def __init__(self, noise_stddev=0.03, **kwargs): + super().__init__(**kwargs) + self.noise_stddev = noise_stddev + + def call(self, inputs, training=None): + if training: + return inputs + tf.random.normal( + shape=tf.shape(inputs), + mean=0.0, + stddev=self.noise_stddev + ) + return inputs + + def get_config(self): + config = super().get_config() + config.update({"noise_stddev": self.noise_stddev}) + return config + + @keras.saving.register_keras_serializable() + class PositionalEncoding(tf.keras.layers.Layer): + """Custom layer per l'encoding posizionale""" + + def __init__(self, d_model, **kwargs): + super().__init__(**kwargs) + self.d_model = d_model + + def build(self, input_shape): + _, seq_length, _ = input_shape + + # Crea la matrice di encoding posizionale + position = tf.range(seq_length, dtype=tf.float32)[:, tf.newaxis] + div_term = tf.exp( + tf.range(0, self.d_model, 2, dtype=tf.float32) * + (-tf.math.log(10000.0) / self.d_model) + ) + + # Calcola sin e cos + pos_encoding = tf.zeros((1, seq_length, self.d_model)) + pos_encoding_even = tf.sin(position * div_term) + pos_encoding_odd = tf.cos(position * div_term) + + # Assegna i valori alle posizioni pari e dispari + pos_encoding = tf.concat( + [tf.expand_dims(pos_encoding_even, -1), + tf.expand_dims(pos_encoding_odd, -1)], + axis=-1 + ) + pos_encoding = tf.reshape(pos_encoding, (1, seq_length, -1)) + pos_encoding = pos_encoding[:, :, :self.d_model] + + # Salva l'encoding come peso non trainabile + self.pos_encoding = self.add_weight( + shape=(1, seq_length, self.d_model), + initializer=tf.keras.initializers.Constant(pos_encoding), + trainable=False, + name='positional_encoding' + ) + + super().build(input_shape) + + def call(self, inputs): + # Broadcast l'encoding posizionale sul batch + batch_size = tf.shape(inputs)[0] + pos_encoding_tiled = tf.tile(self.pos_encoding, [batch_size, 1, 1]) + return inputs + pos_encoding_tiled + + def get_config(self): + config = super().get_config() + config.update({"d_model": self.d_model}) + return config + + @keras.saving.register_keras_serializable() + class WarmUpLearningRateSchedule(tf.keras.optimizers.schedules.LearningRateSchedule): + """Custom learning rate schedule with linear warmup and exponential decay.""" + + def __init__(self, initial_learning_rate=1e-3, warmup_steps=500, decay_steps=5000): + super().__init__() + self.initial_learning_rate = initial_learning_rate + self.warmup_steps = warmup_steps + self.decay_steps = decay_steps + + def __call__(self, step): + warmup_pct = tf.cast(step, tf.float32) / self.warmup_steps + warmup_lr = self.initial_learning_rate * warmup_pct + decay_factor = tf.pow(0.1, tf.cast(step, tf.float32) / self.decay_steps) + decayed_lr = self.initial_learning_rate * decay_factor + return tf.where(step < self.warmup_steps, warmup_lr, decayed_lr) + + def get_config(self): + return { + 'initial_learning_rate': self.initial_learning_rate, + 'warmup_steps': self.warmup_steps, + 'decay_steps': self.decay_steps + } + + @keras.saving.register_keras_serializable() + def weighted_huber_loss(y_true, y_pred): + # Pesi per diversi output + weights = tf.constant([1.0, 0.8, 0.8, 1.0, 0.6], dtype=tf.float32) + huber = tf.keras.losses.Huber(delta=1.0) + loss = huber(y_true, y_pred) + weighted_loss = tf.reduce_mean(loss * weights) + return weighted_loss + + print("Caricamento modello...") + + # Verifica che il modello sia disponibile + model_path = './sources/olive_oil_transformer/olive_oil_transformer_model.keras' + if not os.path.exists(model_path): + raise FileNotFoundError(f"Modello non trovato in: {model_path}") + + # Prova a caricare il modello + model = tf.keras.models.load_model(model_path, custom_objects={ + 'DataAugmentation': DataAugmentation, + 'PositionalEncoding': PositionalEncoding, + 'WarmUpLearningRateSchedule': WarmUpLearningRateSchedule, + 'weighted_huber_loss': weighted_huber_loss + }) + MODEL_LOADING = False + return ( + dbc.Alert("Modello caricato correttamente", color="success"), + "Produzione (Local Model)", + "~ 100ms", + "0", + new_counter_data + ) + except Exception as e: + print(f"Errore nel caricamento del modello: {str(e)}") + # Se c'è un errore nel caricamento del modello, torna in modalità debug + DEV_MODE = True + MODEL_LOADING = False + return ( + dbc.Alert(f"Errore nel caricamento del modello: {str(e)}", color="danger"), + "Debug (Mock) - Fallback", + "N/A", + "N/A", + new_counter_data + ) + + else: + MODEL_LOADING = False + except Exception as e: + print(f"Errore nella configurazione inferenza: {str(e)}") + MODEL_LOADING = False + return ( + dbc.Alert(f"Errore: {str(e)}", color="danger"), + "Errore", + "Errore", + "Errore", + new_counter_data + ) + + +@app.callback( + [ + Output(Ids.DEBUG_SWITCH, 'value'), + Output(Ids.PRODUCTION_DEBUG_SWITCH, 'value') + ], + [Input('url', 'pathname')], + running=[ + (Output(Ids.DASHBOARD_CONTAINER, 'children'), + [Input('url', 'pathname')], + lambda x: x == '/') + ] +) +def init_debug_switch(pathname): + if pathname != '/': + raise PreventUpdate + try: + config = load_config() + return config.get('inference', {}).get('debug_mode', True), config.get('inference', {}).get('debug_mode', True) # Default a True se non configurato + except Exception as e: + print(f"Errore nel caricamento della configurazione debug: {str(e)}") + return True, True # Default a True in caso di errore + + +@app.callback( + [Output(Ids.AUTH_CONTAINER, 'children'), + Output(Ids.DASHBOARD_CONTAINER, 'children')], + [Input('url', 'pathname'), + Input('session', 'data')], +) +def display_page(pathname, session_data): + # print(f"Session data: {session_data}") # Debug print + + if pathname == '/register': + return create_register_layout(), html.Div() + + if not session_data: + print("No session data found") # Debug print + return create_login_layout(), html.Div() + + if 'token' not in session_data: + print("No token in session data") # Debug print + return create_login_layout(), html.Div() + + is_valid, username = verify_token(session_data['token']) + if not is_valid: + print("Invalid token") # Debug print + return create_login_layout(), html.Div() + + # print(f"Valid session for user: {username}") # Debug print + return html.Div(), create_main_layout() + + +def check_session(): + """Verifica lo stato della sessione e restituisce i dati se disponibili""" + if not flask.has_request_context(): + print("No request context available") + return None + + try: + print("\nChecking session...") + # Prima prova a leggere dalla sessione corrente + if 'user_session' in flask.session: + print(f"Found session in flask.session: {flask.session['user_session']}") + return flask.session['user_session'] + + # Poi prova a leggere dai cookies + session_data = flask.request.cookies.get('session') + print(f"Session cookie data: {session_data}") + + if session_data: + try: + session_data = json.loads(session_data) + print(f"Parsed session data: {session_data}") + if 'username' in session_data: + print(f"Found username in session: {session_data['username']}") + return session_data + except json.JSONDecodeError as e: + print(f"Error decoding session data: {e}") + # Prova a decodificare in base64 + try: + import base64 + decoded = base64.b64decode(session_data) + session_data = json.loads(decoded) + if 'username' in session_data: + print(f"Found username in decoded session: {session_data['username']}") + return session_data + except Exception as be: + print(f"Error decoding base64 session: {be}") + return None + except Exception as e: + print(f"Error checking session: {str(e)}") + import traceback + traceback.print_exc() + print("No session data found") + return None + + +def save_session_data(username, token): + """Salva i dati di sessione sia in Flask che nello store Dash""" + session_data = { + 'token': token, + 'username': username, + 'authenticated': True + } + + if flask.has_request_context(): + try: + flask.session['user_session'] = session_data + print(f'Saved session data in Flask session: {session_data}') + except Exception as e: + print(f"Error saving to Flask session: {e}") + + return session_data + + +@app.callback( + [Output('session', 'data'), + Output(Ids.LOGIN_ERROR, 'children'), + Output('url', 'pathname', allow_duplicate=True)], + [Input(Ids.LOGIN_BUTTON, 'n_clicks')], + [State(Ids.LOGIN_USERNAME, 'value'), + State(Ids.LOGIN_PASSWORD, 'value')], + prevent_initial_call=True +) +def login(n_clicks, username, password): + if n_clicks is None: + raise PreventUpdate + + print(f"\nAttempting login for user: {username}") + + if verify_user(username, password): + try: + token = create_token(username) + print(f"Token created: {token}") + + # Salva i dati di sessione + session_data = save_session_data(username, token) + print(f"Session data saved: {session_data}") + + # Verifica che i dati siano stati salvati + current_session = check_session() + print(f"Current session after save: {current_session}") + + return session_data, '', '/' + + except Exception as e: + print(f"Error in login process: {e}") + import traceback + traceback.print_exc() + return None, dbc.Alert(f"Errore durante il login: {str(e)}", color="danger"), no_update + + return None, dbc.Alert("Credenziali non valide", color="danger"), '/login' + + +@app.callback( + [Output(Ids.REGISTER_ERROR, 'children'), + Output(Ids.REGISTER_SUCCESS, 'children'), + Output('url', 'pathname', allow_duplicate=True)], + [Input(Ids.REGISTER_BUTTON, 'n_clicks')], + [State(Ids.REGISTER_USERNAME, 'value'), + State(Ids.REGISTER_PASSWORD, 'value'), + State(Ids.REGISTER_CONFIRM, 'value')], + prevent_initial_call=True +) +def register(n_clicks, username, password, password_confirm): + if n_clicks is None: + raise PreventUpdate + + if password != password_confirm: + return dbc.Alert("Le password non coincidono", color="danger"), None, no_update + + success, message = create_user(username, password) + if success: + return None, dbc.Alert(message, color="success"), '/login' + return dbc.Alert(message, color="danger"), None, no_update + + +@app.callback( + Output('url', 'pathname', allow_duplicate=True), + [Input(Ids.SHOW_REGISTER_BUTTON, 'n_clicks')], + prevent_initial_call=True +) +def navigate_to_register(n_clicks): + if n_clicks is None: + raise PreventUpdate + return '/register' + + +@app.callback( + Output('url', 'pathname', allow_duplicate=True), + [Input(Ids.SHOW_LOGIN_BUTTON, 'n_clicks')], + prevent_initial_call=True +) +def navigate_to_login(n_clicks): + if n_clicks is None: + raise PreventUpdate + return '/login' + + +@app.callback( + [Output('session', 'clear_data'), + Output('url', 'pathname', allow_duplicate=True)], + [Input('logout-button', 'n_clicks')], + prevent_initial_call=True +) +def logout(n_clicks): + if n_clicks is None: + raise PreventUpdate + + print("Performing logout") # Debug print + try: + # Pulisci eventuali dati di sessione Flask + if flask.has_request_context(): + flask.session.clear() + except Exception as e: + print(f"Error clearing Flask session: {e}") + + return True, '/login' + + +@app.server.before_request +def before_request_func(): + # print("\n=== Request Info ===") + # print(f"Path: {flask.request.path}") + # print(f"Cookies: {flask.request.cookies}") + if flask.has_request_context(): + session_data = flask.request.cookies.get('session') + # if session_data: + # print(f"Session Data: {session_data}") + # print("==================\n") + + @app.callback( Output("save-config-message", "children"), [Input("save-config-button", "n_clicks")], @@ -2191,11 +2660,18 @@ def create_extra_info_component(prediction, varieties_info): State("cost-marketing", "value"), State("cost-commerciali", "value"), State("price-olio", "value"), - State("perc-vendita-diretta", "value")] + State("perc-vendita-diretta", "value"), + # Debug mode + State(Ids.DEBUG_SWITCH, "value")], + running=[ + (Output(Ids.DASHBOARD_CONTAINER, 'children'), + [Input('url', 'pathname')], + lambda x: x == '/') + ] ) def save_configuration(n_clicks, hectares, var1, tech1, perc1, var2, tech2, perc2, var3, tech3, perc3, amm, ass, man, cert, rac, pot, fer, irr, - mol, sto, bot, eti, mark, comm, price, perc_dir): + mol, sto, bot, eti, mark, comm, price, perc_dir, debug_mode): if n_clicks is None: return no_update @@ -2236,10 +2712,15 @@ def save_configuration(n_clicks, hectares, var1, tech1, perc1, var2, tech2, perc 'prezzo_vendita': price, 'perc_vendita_diretta': perc_dir } + }, + 'inference': { + 'debug_mode': debug_mode, + 'model_path': './sources/olive_oil_transformer/olive_oil_transformer_model.keras' } } - if save_config(config): + success, message = save_config(config) + if success: return dbc.Alert( "Configurazione salvata con successo!", color="success", @@ -2248,43 +2729,24 @@ def save_configuration(n_clicks, hectares, var1, tech1, perc1, var2, tech2, perc ) else: return dbc.Alert( - "Errore nel salvataggio della configurazione", + f"Errore nel salvataggio della configurazione: {message}", color="danger", duration=4000, is_open=True ) -# Aggiorna la configurazione dei grafici per essere più responsive -def create_figure_layout(fig, title): - fig.update_layout( - title=title, - title_x=0.5, - margin=dict(l=20, r=20, t=40, b=20), - paper_bgcolor='rgba(0,0,0,0)', - plot_bgcolor='rgba(0,0,0,0)', - height=350, - font=dict(family="Helvetica, Arial, sans-serif"), - showlegend=True, - legend=dict( - orientation="h", - yanchor="bottom", - y=1.02, - xanchor="right", - x=1 - ) - ) - return fig - - - - @app.callback( Output("percentage-warning", "children"), [ Input("percentage-1-input", "value"), Input("percentage-2-input", "value"), Input("percentage-3-input", "value") + ], + running=[ + (Output(Ids.DASHBOARD_CONTAINER, 'children'), + [Input('url', 'pathname')], + lambda x: x == '/') ] ) def check_percentages(perc1, perc2, perc3): @@ -2298,12 +2760,19 @@ def check_percentages(perc1, perc2, perc3): color="danger", className="mt-2" ) + if total < 100: + return dbc.Alert( + f"La somma delle percentuali è {total}% (non può essere inferiore a 100%)", + color="danger", + className="mt-2" + ) return "" except Exception as e: print(f"Errore nel controllo delle percentuali: {str(e)}") return "" + @app.callback( [ # Outputs per i costi e configurazioni base (15 outputs, escluso warning) @@ -2348,6 +2817,11 @@ def check_percentages(perc1, perc2, perc3): Input("variety-2-dropdown", "value"), Input("variety-3-dropdown", "value"), Input('_pages_location', 'pathname') + ], + running=[ + (Output(Ids.DASHBOARD_CONTAINER, 'children'), + [Input('url', 'pathname')], + lambda x: x == '/') ] ) def load_configuration(active_tab, variety2, variety3, pathname): @@ -2415,162 +2889,38 @@ def load_configuration(active_tab, variety2, variety3, pathname): return [no_update] * 31 -def create_production_details_figure(prediction): - """Crea il grafico dei dettagli produzione con il nuovo stile""" - details_data = prepare_details_data(prediction) - fig = px.bar( - details_data, - x='Varietà', - y='Produzione', - color='Tipo', - barmode='group', - color_discrete_map={'Olive': '#2185d0', 'Olio': '#21ba45'} - ) - return create_figure_layout(fig, 'Dettagli Produzione per Varietà') - - -def create_weather_impact_figure(weather_data): - """Crea il grafico dell'impatto meteorologico con il nuovo stile""" - recent_weather = weather_data.tail(41).copy() - fig = px.scatter( - recent_weather, - x='temp', - y='solarradiation', - size='precip', - color_discrete_sequence=['#2185d0'] - ) - return create_figure_layout(fig, 'Condizioni Meteorologiche') - - -def create_water_needs_figure(prediction): - """Crea il grafico del fabbisogno idrico con il nuovo stile""" - # Definisci i mesi in italiano - months = ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', - 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'] - - water_data = [] - for detail in prediction['variety_details']: - for month in months: - season = get_season_from_month(month) - variety_info = olive_varieties[ - olive_varieties['Varietà di Olive'] == detail['variety'] - ].iloc[0] - - water_need = variety_info[f'Fabbisogno Acqua {season} (m³/ettaro)'] - water_data.append({ - 'Month': month, - 'Variety': detail['variety'], - 'Water_Need': water_need * (detail['percentage'] / 100) - }) - - water_df = pd.DataFrame(water_data) - fig = px.bar( - water_df, - x='Month', - y='Water_Need', - color='Variety', - barmode='stack', - color_discrete_sequence=['#2185d0', '#21ba45', '#6435c9'] - ) - - return create_figure_layout(fig, 'Fabbisogno Idrico Mensile') - - -def get_season_from_month(month): - """Helper function per determinare la stagione dal mese.""" - seasons = { - 'Gen': 'Inverno', - 'Feb': 'Inverno', - 'Mar': 'Primavera', - 'Apr': 'Primavera', - 'Mag': 'Primavera', - 'Giu': 'Estate', - 'Lug': 'Estate', - 'Ago': 'Estate', - 'Set': 'Autunno', - 'Ott': 'Autunno', - 'Nov': 'Autunno', - 'Dic': 'Inverno' - } - return seasons[month] - - -def prepare_details_data(prediction): - """Prepara i dati per il grafico dei dettagli di produzione""" - details_data = [] - - # Dati per ogni varietà - for detail in prediction['variety_details']: - details_data.extend([ - { - 'Varietà': f"{detail['variety']} ({detail['percentage']}%)", - 'Tipo': 'Olive', - 'Produzione': detail['production_per_ha'] - }, - { - 'Varietà': f"{detail['variety']} ({detail['percentage']}%)", - 'Tipo': 'Olio', - 'Produzione': detail['oil_per_ha'] - } - ]) - - # Aggiungi totali - details_data.extend([ - { - 'Varietà': 'Totale', - 'Tipo': 'Olive', - 'Produzione': prediction['olive_production'] - }, - { - 'Varietà': 'Totale', - 'Tipo': 'Olio', - 'Produzione': prediction['avg_oil_production'] - } - ]) - - return pd.DataFrame(details_data) - - -def get_season_from_month(month): - """Helper function per determinare la stagione dal mese.""" - seasons = { - 'Gen': 'Inverno', 'Feb': 'Inverno', 'Mar': 'Primavera', - 'Apr': 'Primavera', 'Mag': 'Primavera', 'Giu': 'Estate', - 'Lug': 'Estate', 'Ago': 'Estate', 'Set': 'Autunno', - 'Ott': 'Autunno', 'Nov': 'Autunno', 'Dic': 'Inverno' - } - return seasons[month] - - @app.callback( [ - Output('growth-simulation-chart', 'figure'), - Output('production-simulation-chart', 'figure'), - Output('simulation-summary', 'children'), - Output('kpi-container', 'children', allow_duplicate=True), - Output('olive-production_ha', 'children'), - Output('oil-production_ha', 'children'), - Output('water-need_ha', 'children'), - Output('olive-production', 'children'), - Output('oil-production', 'children'), - Output('water-need', 'children'), - Output('production-details', 'figure'), - Output('weather-impact', 'figure'), - Output('water-needs', 'figure'), - Output('extra-info', 'children') + Output(Ids.GROWTH_CHART, 'figure'), + Output(Ids.PRODUCTION_CHART, 'figure'), + Output(Ids.SIMULATION_SUMMARY, 'children'), + Output(Ids.KPI_CONTAINER, 'children', allow_duplicate=True), + Output(Ids.OLIVE_PRODUCTION_HA, 'children'), + Output(Ids.OIL_PRODUCTION_HA, 'children'), + Output(Ids.WATER_NEED_HA, 'children'), + Output(Ids.OLIVE_PRODUCTION, 'children'), + Output(Ids.OIL_PRODUCTION, 'children'), + Output(Ids.WATER_NEED, 'children'), + Output(Ids.PRODUCTION_DETAILS, 'figure'), + Output(Ids.WEATHER_IMPACT, 'figure'), + Output(Ids.WATER_NEEDS, 'figure'), + Output(Ids.EXTRA_INFO, 'children'), + Output(Ids.INFERENCE_REQUESTS, 'children') ], - [ - Input('simulate-btn', 'n_clicks') - ], - [ - State('temp-slider', 'value'), - State('humidity-slider', 'value'), - State('rainfall-input', 'value'), - State('radiation-input', 'value') - ], - prevent_initial_call='initial_duplicate' + [Input(Ids.SIMULATE_BUTTON, 'n_clicks')], + [State(Ids.TEMP_SLIDER, 'value'), + State(Ids.HUMIDITY_SLIDER, 'value'), + State(Ids.RAINFALL_INPUT, 'value'), + State(Ids.RADIATION_INPUT, 'value'), + State(Ids.INFERENCE_COUNTER, 'data')], + prevent_initial_call='initial_duplicate', + running=[ + (Output(Ids.DASHBOARD_CONTAINER, 'children'), + [Input('url', 'pathname')], + lambda x: x == '/') + ] ) -def update_simulation(n_clicks, temp_range, humidity, rainfall, radiation): +def update_simulation(n_clicks, temp_range, humidity, rainfall, radiation, counter_data): """ Callback principale per aggiornare tutti i componenti della simulazione """ @@ -2580,7 +2930,7 @@ def update_simulation(n_clicks, temp_range, humidity, rainfall, radiation): empty_production_fig = go.Figure() empty_summary = html.Div() empty_kpis = html.Div() - return empty_growth_fig, empty_production_fig, empty_summary, empty_kpis, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", {}, {}, {}, "" + return empty_growth_fig, empty_production_fig, empty_summary, empty_kpis, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", {}, {}, {}, "", 0 try: # Inizializza il simulatore @@ -2638,8 +2988,12 @@ def update_simulation(n_clicks, temp_range, humidity, rainfall, radiation): varieties_info.append(variety_data.iloc[0]) percentages.append(variety_config['percentage']) + current_count = counter_data.get('count', 0) + 1 + prediction = make_prediction(weather_data, varieties_info, percentages, hectares, sim_data) + dcc.Store(id=Ids.INFERENCE_COUNTER, data={'count': current_count}) + # Formattazione output con valori per ettaro e totali olive_prod_text_ha = f"{prediction['olive_production']:.0f} kg/ha\n" olive_prod_text = f"Totale: {prediction['olive_production_total']:.0f} kg" @@ -2662,11 +3016,11 @@ def update_simulation(n_clicks, temp_range, humidity, rainfall, radiation): growth_fig, production_fig, summary, kpi_indicators, olive_prod_text_ha, oil_prod_text_ha, water_need_text_ha, olive_prod_text, oil_prod_text, water_need_text, - details_fig, weather_fig, water_fig, extra_info) + details_fig, weather_fig, water_fig, extra_info, f"{current_count}") except Exception as e: print(f"Errore nell'aggiornamento dashboard: {str(e)}") - return growth_fig, production_fig, summary, kpi_indicators, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", {}, {}, {}, "" + return growth_fig, production_fig, summary, kpi_indicators, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", {}, {}, {}, "", 0 except Exception as e: print(f"Errore nella simulazione: {str(e)}") @@ -2678,16 +3032,20 @@ def update_simulation(n_clicks, temp_range, humidity, rainfall, radiation): color="danger" ) empty_kpis = html.Div() - return empty_growth_fig, empty_production_fig, error_summary, empty_kpis, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", {}, {}, {}, "" + return empty_growth_fig, empty_production_fig, error_summary, empty_kpis, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", {}, {}, {}, "", 0 -# Aggiungiamo un callback per gestire l'abilitazione del pulsante di simulazione @app.callback( - Output('simulate-btn', 'disabled'), - [Input('temp-slider', 'value'), - Input('humidity-slider', 'value'), - Input('rainfall-input', 'value'), - Input('radiation-input', 'value')] + Output(Ids.SIMULATE_BUTTON, 'disabled'), + [Input(Ids.TEMP_SLIDER, 'value'), + Input(Ids.HUMIDITY_SLIDER, 'value'), + Input(Ids.RAINFALL_INPUT, 'value'), + Input(Ids.RADIATION_INPUT, 'value')], + running=[ + (Output(Ids.DASHBOARD_CONTAINER, 'children'), + [Input('url', 'pathname')], + lambda x: x == '/') + ] ) def update_button_state(temp_range, humidity, rainfall, radiation): """ @@ -2699,77 +3057,108 @@ def update_button_state(temp_range, humidity, rainfall, radiation): # Verifica range validi if not (0 <= humidity <= 100): return True - if not (0 <= rainfall <= 500): + if not (0 <= rainfall <= 1000): return True - if not (0 <= radiation <= 1000): + if not (0 <= radiation <= 1200): return True return False -# Callback per aggiornare il layout dei grafici in base alle dimensioni della finestra -'''@app.clientside_callback( - """ - function(value) { - return { - 'height': window.innerHeight * 0.6 - } - } - """, - Output('growth-simulation-chart', 'style'), - Input('growth-simulation-chart', 'id') -) -''' - - @app.callback( - Output('kpi-container', 'children', allow_duplicate=True), - [Input('growth-simulation-chart', 'figure'), - Input('production-simulation-chart', 'figure')], - [State('temp-slider', 'value'), - State('humidity-slider', 'value'), - State('rainfall-input', 'value'), - State('radiation-input', 'value')], - prevent_initial_callbacks='initial_duplicate' + Output(Ids.KPI_CONTAINER, 'children', allow_duplicate=True), + [Input(Ids.GROWTH_CHART, 'figure'), + Input(Ids.PRODUCTION_CHART, 'figure')], + [State(Ids.TEMP_SLIDER, 'value'), + State(Ids.HUMIDITY_SLIDER, 'value'), + State(Ids.RAINFALL_INPUT, 'value'), + State(Ids.RADIATION_INPUT, 'value')], + prevent_initial_callbacks='initial_duplicate', + running=[ + (Output(Ids.DASHBOARD_CONTAINER, 'children'), + [Input('url', 'pathname')], + lambda x: x == '/') + ] ) def update_kpis(growth_fig, prod_fig, temp_range, humidity, rainfall, radiation): """Aggiorna i KPI quando cambia la simulazione""" - # Ricalcola la simulazione simulator = EnvironmentalSimulator() sim_data = simulator.simulate_growth(temp_range, humidity, rainfall, radiation) - # Calcola i KPI kpis = calculate_kpis(sim_data) - # Crea gli indicatori return create_kpi_indicators(kpis) @app.callback( Output('growth-simulation-chart', 'style'), - Input('growth-simulation-chart', 'id') + Input('growth-simulation-chart', 'id'), + running=[ + (Output(Ids.DASHBOARD_CONTAINER, 'children'), + [Input('url', 'pathname')], + lambda x: x == '/') + ] ) def update_graph_style(graph_id): return { 'height': '60vh', # Altezza responsive - 'min-height': '400px', - 'max-height': '800px' + 'minHeight': '400px', + 'maxHeight': '800px' } -if __name__ == '__main__': - port = int(os.environ.get('DASH_PORT', 8888)) - debug = int(os.environ.get('DASH_DEBUG', False)) +@app.callback( + Output(Ids.INFERENCE_COUNTER, 'data'), + [Input(Ids.SIMULATE_BUTTON, 'n_clicks')], + [State(Ids.INFERENCE_COUNTER, 'data')], + prevent_initial_call=True +) +def update_inference_counter(n_clicks, current_data): + if n_clicks is None: + raise PreventUpdate - # Oppure usando argparse per gli argomenti da riga di comando + try: + current_count = current_data.get('count', 0) + 1 + return {'count': current_count} + except Exception as e: + print(f"Errore nell'aggiornamento del contatore: {str(e)}") + return current_data + + +@app.callback( + [ + Output(Ids.INFERENCE_REQUESTS, 'children', allow_duplicate=True), + Output(Ids.PRODUCTION_INFERENCE_REQUESTS, 'children', allow_duplicate=True) + ], + [Input(Ids.INFERENCE_COUNTER, 'data')], + prevent_initial_call=True +) +def display_inference_count(counter_data): + try: + count = counter_data.get('count', 0) + return f"{count}", f"{count}" + except Exception as e: + print(f"Errore nella visualizzazione del contatore: {str(e)}") + return "0", "0" + + +if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('--port', type=int, default=8888, help='Port to run the server on') - parser.add_argument('--debug', type=bool, default=False, help='Debug') + parser.add_argument('--port', type=int, help='Port to run the server on') + parser.add_argument('--debug', action='store_true', help='Debug mode') args = parser.parse_args() + env_port = int(os.environ.get('DASH_PORT', 8888)) + env_debug = os.environ.get('DASH_DEBUG', '').lower() == 'true' + + port = args.port if args.port is not None else env_port + debug = args.debug if args.debug else env_debug + + print(f"Starting server on port {port} with debug={'on' if debug else 'off'}") + app.run_server( host='0.0.0.0', - port=args.port, + port=port, debug=debug ) diff --git a/src/olive_config.json b/src/olive_config.json index b1f0ad7..50d2b76 100755 --- a/src/olive_config.json +++ b/src/olive_config.json @@ -1,6 +1,6 @@ { "oliveto": { - "hectares": 4.35, + "hectares": 1, "varieties": [ { "variety": "Nocellara dell'Etna", @@ -10,12 +10,12 @@ { "variety": "Frantoio", "technique": "tradizionale", - "percentage": 10 + "percentage": 20 }, { "variety": "Coratina", "technique": "tradizionale", - "percentage": 40 + "percentage": 30 } ] }, @@ -46,6 +46,7 @@ } }, "inference": { - "debug_mode": false + "debug_mode": false, + "model_path": "./sources/olive_oil_transformer/olive_oil_transformer_model.keras" } } \ No newline at end of file diff --git a/src/olive_oil_train_dataset/README.MD b/src/olive_oil_train_dataset/README.MD new file mode 100644 index 0000000..9700d11 --- /dev/null +++ b/src/olive_oil_train_dataset/README.MD @@ -0,0 +1,65 @@ +# Olive Oil Production Training Dataset Generator + +## Overview +This Python script generates a synthetic training dataset for olive oil production simulation. It simulates various factors affecting olive production including weather conditions, olive varieties, cultivation techniques, and geographical zones. + +## Features +- Parallel processing for efficient dataset generation +- Batch processing to manage memory usage +- Configurable simulation parameters +- Weather effect calculations +- Multiple olive varieties and cultivation techniques support +- Zone-based production simulation + +## Prerequisites +Required Python packages: +- pandas +- numpy +- psutil +- tqdm +- concurrent.futures (part of Python standard library) +- multiprocessing (part of Python standard library) + +Required input data files: +- `./sources/weather_data_solarenergy.parquet`: Weather data including solar energy measurements +- `./sources/olive_varieties.parquet`: Olive varieties and their characteristics + +## Usage + +### Command Line Arguments +```bash +python olive_oil_train_dataset.create_train_dataset [options] +``` + +Options: +- `--random-seed`: Seed for reproducible results (optional) +- `--num-simulations`: Total number of simulations to run (default: 100000) +- `--num-zones`: Number of zones per simulation (default: same as num-simulations) +- `--batch-size`: Size of each simulation batch (default: 10000) +- `--output-path`: Output file path (default: './sources/olive_training_dataset.parquet') +- `--max-workers`: Number of parallel workers (default: automatically optimized) + +### Example +```bash +python olive_oil_train_dataset.create_train_dataset --num-simulations 50 --num-zones 10 --batch-size 50 --output-path "./output/olive_dataset.parquet" +``` + +## Output +The script generates a Parquet file containing simulated olive production data with the following key features: +- Simulation and zone identifiers +- Weather conditions (temperature, precipitation, solar energy) +- Production metrics per olive variety +- Oil yield calculations +- Water requirements +- Cultivation techniques + +## Technical Details + +### Simulation Parameters +The simulation takes into account: +- Temperature effects on production +- Water availability and drought resistance +- Solar radiation impact +- Variety-specific characteristics +- Cultivation technique influence +- Zone-specific variations \ No newline at end of file diff --git a/src/olive_oil_train_dataset/create_train_dataset.py b/src/olive_oil_train_dataset/create_train_dataset.py index b3cd45a..1d21054 100755 --- a/src/olive_oil_train_dataset/create_train_dataset.py +++ b/src/olive_oil_train_dataset/create_train_dataset.py @@ -133,22 +133,24 @@ def simulate_zone(base_weather, olive_varieties, year, zone, all_varieties, vari variety_info['Fabbisogno Acqua Estate (m³/ettaro)'] + variety_info['Fabbisogno Acqua Autunno (m³/ettaro)'] + variety_info['Fabbisogno Acqua Inverno (m³/ettaro)'] - ) / 4 + ) / 12 monthly_water_need = zone_weather.apply( lambda row: calculate_water_need(row, base_water_need, variety_info['Temperatura Ottimale']), axis=1 ) + #print(f'Monthly - {variety} - hectares {hectares} - {monthly_water_need}') monthly_water_need *= np.random.uniform(0.95, 1.05, len(monthly_water_need)) + #print(f'Monthly 2 - {variety} - hectares {hectares} - {monthly_water_need}') annual_variety_water_need = monthly_water_need.sum() * percentage * hectares - + #print(f'Annual Variety - {variety} - hectares {hectares} - {annual_variety_water_need}') # Aggiorna totali annuali annual_production += annual_variety_production annual_min_oil += min_oil_production annual_max_oil += max_oil_production annual_avg_oil += avg_oil_production annual_water_need += annual_variety_water_need - + #print(f'Total Annual {annual_water_need}') # Aggiorna dati varietà clean_variety = clean_column_name(variety) variety_data[clean_variety].update({ @@ -240,9 +242,9 @@ def simulate_olive_production_parallel(weather_data, olive_varieties, num_simula print(f"Utilizzando {max_workers} workers ottimali basati sulle risorse del sistema") # Calcolo numero di batch - num_batches = (num_simulations + batch_size - 1) // batch_size + num_batches = (num_simulations * num_zones - 1) // batch_size print(f"Elaborazione di {num_simulations} simulazioni con {num_zones} zone in {num_batches} batch") - print(f"Totale record attesi: {num_simulations * num_zones:,}") + print(f"Totale record attesi: {num_simulations * num_zones}") # Lista per contenere tutti i DataFrame dei batch all_batches = [] @@ -368,11 +370,11 @@ def calculate_production(variety_info, weather, percentage, hectares, seed): variety_info['Fabbisogno Acqua Inverno (m³/ettaro)'] ) / 4 * percentage * hectares - water_need = ( - base_water_need * - (1 + max(0, (weather['temp_mean'] - 20) / 50)) * - max(0.6, 1 - (weather['precip_sum'] / 1000)) - ) + temp_factor = 1 + max(0, (weather["temp_mean"] - 20) / 50) + rain_factor = max(0.6, 1 - (weather["precip_sum"] / 1000)) + water_need = base_water_need * temp_factor * rain_factor + + print(f'temp factor: {temp_factor} rainfall factor: {rain_factor} water_need: {water_need}') return { 'variety': variety_info['Varietà di Olive'], diff --git a/src/requirements.txt b/src/requirements.txt index 38978d9..53cf16a 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -18,4 +18,7 @@ python-dotenv>=0.21.0 psutil>=5.9.0 # File handling -dvc>=2.0.0 # Per la gestione dei file sources.dvc \ No newline at end of file +dvc>=2.0.0 # Per la gestione dei file sources.dvc + +# Auth +PyJWT==2.7.0 \ No newline at end of file diff --git a/src/utils/helpers.py b/src/utils/helpers.py index 3f35a74..1db6e93 100755 --- a/src/utils/helpers.py +++ b/src/utils/helpers.py @@ -424,10 +424,20 @@ def calculate_weather_effect(row, optimal_temp): return combined_effect def calculate_water_need(weather_data, base_need, optimal_temp): - # Calcola il fabbisogno idrico basato su temperatura e precipitazioni - temp_factor = 1 + 0.05 * (weather_data['temp_mean'] - optimal_temp) # Aumenta del 5% per ogni grado sopra l'ottimale - rain_factor = 1 - 0.001 * weather_data['precip_sum'] # Diminuisce leggermente con l'aumentare delle precipitazioni - return base_need * temp_factor * rain_factor + # Calcola il fattore temperatura (minimo 80% del fabbisogno) + temp_factor = max(0.8, 1 + 0.05 * (weather_data['temp_mean'] - optimal_temp)) + + # Calcola il fattore precipitazioni (minimo 50% del fabbisogno) + rain_factor = max(0.5, 1 - 0.001 * weather_data['precip_sum']) + + # Calcola il fabbisogno idrico totale + water_need = base_need * temp_factor * rain_factor + + # Debug: controlla che il fabbisogno idrico sia positivo + assert water_need >= 0, "Il fabbisogno idrico calcolato è negativo!" + + return water_need + def create_technique_mapping(olive_varieties, mapping_path='./sources/technique_mapping.joblib'): # Estrai tutte le tecniche uniche dal dataset e convertile in lowercase