valegro commited on
Commit
571cf64
·
verified ·
1 Parent(s): 474ac94

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +485 -631
app.py CHANGED
@@ -4,17 +4,17 @@ import numpy as np
4
  import seaborn as sns
5
  import matplotlib.pyplot as plt
6
  from statistics import mode, StatisticsError
7
- import io # Per gestione file upload
8
 
9
  # --- Scikit-learn ---
10
  from sklearn.model_selection import train_test_split, GridSearchCV
11
- from sklearn.preprocessing import StandardScaler, LabelEncoder # LabelEncoder servirà se usiamo VAE con shape_code
12
  from sklearn.pipeline import Pipeline
13
  from sklearn.metrics import confusion_matrix, accuracy_score, f1_score
14
  from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
15
  from sklearn.linear_model import LogisticRegression
16
  from sklearn.svm import SVC
17
- from sklearn.neural_network import MLPClassifier # Usato nei dummy models
18
 
19
  # --- PyTorch (per VAE) ---
20
  import torch
@@ -23,7 +23,7 @@ import torch.nn.functional as F
23
  import torch.utils.data
24
  import random
25
 
26
- # --- Impostazioni Pagina Streamlit ---
27
  st.set_page_config(
28
  page_title="WEEKO - AI Reuse Analyzer",
29
  page_icon="♻️",
@@ -31,45 +31,49 @@ st.set_page_config(
31
  )
32
 
33
  ##########################################
34
- # 1. PLACEHOLDER / DUMMY MODELS (dal codice Zero Scarto)
35
  ##########################################
 
36
  class DummyTabTransformerClassifier:
37
- # Semplificato: usa MLP come base per il placeholder
38
- def __init__(self, input_dim=8): # Input dim deve corrispondere alle feature usate
39
- # Architettura minima
40
- self.clf = MLPClassifier(hidden_layer_sizes=(max(16,input_dim*2), max(8,input_dim)), max_iter=100, random_state=42, alpha=0.01, learning_rate_init=0.01)
 
 
 
41
  def fit(self, X, y):
42
  self.clf.fit(X, y)
43
  return self
44
  def predict(self, X):
45
  return self.clf.predict(X)
46
  def predict_proba(self, X):
47
- # Assicurati che predict_proba sia disponibile
48
  if hasattr(self.clf, 'predict_proba'):
49
  return self.clf.predict_proba(X)
50
- else: # Fallback se il modello non ha predict_proba (improbabile per MLP)
51
  preds = self.clf.predict(X)
52
  return np.array([[1.0, 0.0] if p == 0 else [0.0, 1.0] for p in preds])
53
 
54
-
55
  class DummySAINTClassifier:
56
- # Semplificato: usa MLP come base per il placeholder
57
- def __init__(self, input_dim=8): # Input dim deve corrispondere alle feature usate
58
- # Architettura minima
59
- self.clf = MLPClassifier(hidden_layer_sizes=(max(20,input_dim*2), max(10,input_dim)), max_iter=120, random_state=42, alpha=0.005, learning_rate_init=0.005)
 
 
 
60
  def fit(self, X, y):
61
  self.clf.fit(X, y)
62
  return self
63
  def predict(self, X):
64
  return self.clf.predict(X)
65
  def predict_proba(self, X):
66
- if hasattr(self.clf, 'predict_proba'):
67
  return self.clf.predict_proba(X)
68
- else:
69
  preds = self.clf.predict(X)
70
- return np.array([[1.0, 0.0] if p == 0 else [0.0, 1.0] for p in preds])
71
 
72
- # Dizionario Modelli ML (Step 1)
73
  MODELS = {
74
  "Random Forest": RandomForestClassifier(random_state=42, n_estimators=100, class_weight='balanced'),
75
  "Gradient Boosting": GradientBoostingClassifier(random_state=42, n_estimators=100),
@@ -80,17 +84,15 @@ MODELS = {
80
  }
81
 
82
  ##########################################
83
- # 2. DEFINIZIONE MODELLO VAE (Step 2 - Generative)
84
  ##########################################
 
85
  class MiniVAE(nn.Module):
86
- # input_dim: numero di feature geometriche/fisiche usate dal VAE
87
  def __init__(self, input_dim=5, latent_dim=2):
88
  super().__init__()
89
- # Encoder
90
  self.fc1 = nn.Linear(input_dim, 32)
91
- self.fc21 = nn.Linear(32, latent_dim) # Mu
92
- self.fc22 = nn.Linear(32, latent_dim) # LogVar
93
- # Decoder
94
  self.fc3 = nn.Linear(latent_dim, 32)
95
  self.fc4 = nn.Linear(32, input_dim)
96
 
@@ -113,710 +115,562 @@ class MiniVAE(nn.Module):
113
  recon = self.decode(z)
114
  return recon, mu, logvar
115
 
116
- # Loss function per VAE
117
  def vae_loss(recon_x, x, mu, logvar):
118
  recon_loss = F.mse_loss(recon_x, x, reduction='sum')
119
  kld = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
120
  return recon_loss + kld
121
 
122
- # Funzione Helper per ottenere Embeddings VAE (se servirà in futuro)
123
- def get_vae_embeddings(data_df, vae_model, scaler):
124
- if vae_model is None or scaler is None: return None
125
- try:
126
- if not hasattr(scaler, 'feature_names_in_'): raise ValueError("Scaler non fittato o senza feature names.")
127
- ordered_cols = scaler.feature_names_in_
128
- if not all(col in data_df.columns for col in ordered_cols): raise ValueError("Colonne mancanti per VAE.")
129
- data_ordered = data_df[ordered_cols]
130
- data_scaled = scaler.transform(data_ordered)
131
- data_t = torch.tensor(data_scaled, dtype=torch.float32)
132
- vae_model.eval()
133
- with torch.no_grad():
134
- mu, _ = vae_model.encode(data_t)
135
- return mu.numpy()
136
- except Exception as e:
137
- st.error(f"Errore embedding VAE: {e}")
138
- return None
139
-
140
  ##########################################
141
- # 3. FUNZIONI LOGICA OR6 (Step 1 - Zero Scarto Analyzer)
142
  ##########################################
143
- # Features: length, width, RUL, margin, shape, weight, thickness
144
- DEFAULT_FEATURES_STEP1 = ['length', 'width', 'RUL', 'margin', 'shape', 'weight', 'thickness']
145
- # Features numeriche usate per ML (shape diventa shape_code)
146
- ML_FEATURES_STEP1 = ['length', 'width', 'shape_code', 'weight', 'thickness', 'RUL', 'margin', 'compat_dim']
147
- # Features geometriche/fisiche per VAE (Step 2) - Sottoinsieme delle precedenti
148
- VAE_FEATURES_STEP2 = ['length', 'width', 'weight', 'thickness', 'shape_code'] # Escludiamo RUL, margin, compat_dim
149
 
 
 
 
 
 
 
 
 
 
150
 
 
 
 
151
  def generate_synthetic_data(n_samples=300, seed=42):
152
  np.random.seed(seed)
153
- length = np.clip(np.random.normal(loc=100, scale=20, size=n_samples), 50, 250) # Aumentato range
154
- width = np.clip(np.random.normal(loc=50, scale=15, size=n_samples), 20, 150) # Aumentato range
155
- RUL = np.clip(np.random.normal(loc=500, scale=250, size=n_samples), 0, 1000).astype(int) # Più varianza RUL
156
- margin = np.clip(np.random.normal(loc=150, scale=150, size=n_samples), -200, 600).astype(int) # Più varianza margin
157
- shapes = np.random.choice(['axisymmetric', 'sheet_metal', 'alloy_plate', 'complex_plastic'], size=n_samples, p=[0.4, 0.3, 0.15, 0.15]) # Aggiunta forma
158
- weight = np.clip(np.random.normal(loc=80, scale=30, size=n_samples), 10, 250) # Range peso più ampio
159
- thickness = np.clip(np.random.normal(loc=8, scale=4, size=n_samples), 0.5, 30) # Range spessore più ampio
 
160
 
161
  return pd.DataFrame({
162
- 'length': length, 'width': width, 'RUL': RUL, 'margin': margin,
163
- 'shape': shapes, 'weight': weight, 'thickness': thickness
164
  })
165
 
166
- # Funzione per match dimensionale (resta uguale)
167
- def dimension_match(row, target_length, target_width, target_shape, target_weight, target_thickness,
 
 
 
168
  tol_len, tol_wid, tol_weight, tol_thickness):
169
  cond_length = abs(row['length'] - target_length) <= tol_len
170
- cond_width = abs(row['width'] - target_width) <= tol_wid
171
- cond_shape = row['shape'] == target_shape
172
- cond_weight = abs(row['weight'] - target_weight) <= tol_weight
173
- cond_thickness = abs(row['thickness'] - target_thickness) <= tol_thickness
174
- # Ora richiede TUTTE le condizioni (più stringente)
175
  return 1 if (cond_length and cond_width and cond_shape and cond_weight and cond_thickness) else 0
176
 
177
- # Funzione per assegnare classe (resta uguale)
178
  def assign_class(row, threshold_score=0.5, alpha=0.5, beta=0.5):
179
- rul_norm = row['RUL'] / 1000.0 # Normalizza RUL
180
- margin_norm = (row['margin'] + 200.0) / 800.0 # Normalizza margin [-200, 600] -> [0, 1]
181
- score = alpha * rul_norm + beta * margin_norm
182
- if row['compat_dim'] == 1 and score >= threshold_score:
183
  return "Riutilizzo Funzionale"
184
  else:
185
- # Se non c'è compatibilità dimensionale O lo score è basso -> Upcycling
186
  return "Upcycling Creativo"
187
 
188
- # Mapping forma a codice numerico
189
- SHAPE_MAPPING = {'axisymmetric': 0, 'sheet_metal': 1, 'alloy_plate': 2, 'complex_plastic': 3}
190
-
191
  ##########################################
192
- # 4. FUNZIONI STREAMLIT PER LE FASI
193
  ##########################################
194
-
195
- # --- Fase 1: Preparazione Dataset ---
196
  def prepare_dataset():
197
  st.header("♻️ 1. Preparazione Dataset EoL")
198
- # Tabs per organizzare
199
- tab1, tab2 = st.tabs(["Carica/Genera Dati", "Definisci Compatibilità & Target"])
200
 
201
- data_loaded = False
 
202
  with tab1:
203
- st.subheader("Fonte Dati")
204
- data_option = st.radio("Scegli", ["Genera dati sintetici", "Carica un CSV"], horizontal=True, key="data_opt")
205
- data = None # Inizializza data a None
206
-
207
- if data_option == "Genera dati sintetici":
208
- n_samples = st.slider("Numero di campioni", 100, 2000, 500, help="Seleziona il numero di campioni da generare", key="gen_n")
209
  if st.button("Genera Dati"):
210
- data = generate_synthetic_data(n_samples=n_samples)
211
- st.session_state.data_source = "generated" # Salva la fonte
212
  else:
213
- uploaded_file = st.file_uploader("Carica un file CSV", type=["csv"], key="csv_up")
214
- if uploaded_file:
215
  try:
216
- data = pd.read_csv(uploaded_file)
217
- # Controllo colonne minime
218
  if not all(col in data.columns for col in DEFAULT_FEATURES_STEP1):
219
- st.error(f"Il CSV deve contenere almeno le colonne: {', '.join(DEFAULT_FEATURES_STEP1)}")
220
- data = None # Invalida i dati caricati
221
  else:
222
- st.session_state.data_source = "uploaded" # Salva la fonte
223
  except Exception as e:
224
- st.error(f"Errore lettura CSV: {str(e)}")
225
- data = None
226
- #else:
227
- #st.info("Carica un file CSV o scegli 'Genera dati sintetici'.")
228
 
229
- # Se i dati sono stati generati o caricati correttamente, procedi
230
  if data is not None:
231
- with tab2:
232
- st.subheader("Parametri per Compatibilità e Classe Target")
233
- st.markdown("Definisci i requisiti per il 'Riutilizzo Funzionale' e come calcolare lo score.")
234
- # Parametri Target (per dimension_match)
235
- col_t1, col_t2 = st.columns(2)
236
- with col_t1:
237
- target_length = st.number_input("Lunghezza target (mm)", 50.0, 250.0, 100.0, step=1.0, key="t_len")
238
- target_width = st.number_input("Larghezza target (mm)", 20.0, 150.0, 50.0, step=1.0, key="t_wid")
239
- target_shape = st.selectbox("Forma target", list(SHAPE_MAPPING.keys()), index=0, key="t_shape")
240
- with col_t2:
241
- target_weight = st.number_input("Peso target (kg)", 10.0, 250.0, 80.0, step=1.0, key="t_wei")
242
- target_thickness = st.number_input("Spessore target (mm)", 0.5, 30.0, 8.0, step=0.5, key="t_thi")
243
-
244
- # Tolleranze
245
- st.markdown("**Tolleranze Dimensionali:**")
246
- col_tol1, col_tol2 = st.columns(2)
247
  with col_tol1:
248
- tol_len = st.slider("Tolleranza lunghezza (±mm)", 0.0, 20.0, 5.0, step=0.5, key="tol_l")
249
- tol_wid = st.slider("Tolleranza larghezza (±mm)", 0.0, 15.0, 3.0, step=0.5, key="tol_w")
250
  with col_tol2:
251
- tol_weight = st.slider("Tolleranza peso (±kg)", 0.0, 30.0, 10.0, step=1.0, key="tol_we")
252
- tol_thickness = st.slider("Tolleranza spessore (±mm)", 0.0, 5.0, 1.0, step=0.1, key="tol_t")
253
-
254
- # Parametri per Score (assegnazione classe)
255
- st.markdown("**Parametri per Score (RUL & Margin):**")
256
- threshold_score = st.slider("Soglia minima score per Riutilizzo", 0.0, 1.0, 0.5, step=0.05, key="score_thr")
257
- alpha = st.slider("Peso RUL nello score (α)", 0.0, 1.0, 0.5, step=0.05, key="alpha_w")
258
- beta = st.slider("Peso Margin nello score (β)", 0.0, 1.0, 0.5, step=0.05, key="beta_w")
259
-
260
- # --- Calcoli sul dataset (DOPO aver definito i parametri) ---
261
- # Salva i parametri in session_state per usarli anche in inferenza
262
- st.session_state.target_params = {
263
- "target_length": target_length, "target_width": target_width, "target_shape": target_shape,
264
- "target_weight": target_weight, "target_thickness": target_thickness,
265
- "tol_len": tol_len, "tol_wid": tol_wid, "tol_weight": tol_weight, "tol_thickness": tol_thickness
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  }
267
- st.session_state.score_params = {"threshold_score": threshold_score, "alpha": alpha, "beta": beta}
268
-
269
- # Codifica numerica della forma (necessaria per ML e VAE)
270
- data['shape_code'] = data['shape'].map(SHAPE_MAPPING)
271
- # Gestisce eventuali shape non mappate (NaN) riempiendole con un codice default (es. -1)
272
- data['shape_code'] = data['shape_code'].fillna(-1).astype(int)
273
-
274
-
275
- # Calcola compat_dim
276
- data['compat_dim'] = data.apply(lambda row: dimension_match(row, **st.session_state.target_params), axis=1)
277
-
278
- # Assegna Target ("Riutilizzo Funzionale" o "Upcycling Creativo")
279
- data['Target'] = data.apply(lambda row: assign_class(row, **st.session_state.score_params), axis=1)
280
 
281
- # --- Visualizzazione e Download ---
282
- st.subheader("Dataset Elaborato")
283
  st.dataframe(data.head(10))
284
- st.write("Distribuzione classi target generate:")
285
- st.bar_chart(data['Target'].value_counts())
286
 
287
- # Heatmap Correlazione (solo su colonne numeriche)
288
  numeric_cols = data.select_dtypes(include=np.number)
289
  if not numeric_cols.empty:
290
- with st.expander("Visualizza Heatmap Correlazioni"):
291
- fig, ax = plt.subplots(figsize=(8, 6))
292
- sns.heatmap(numeric_cols.corr(), annot=True, cmap='viridis', fmt=".2f", ax=ax)
293
- st.pyplot(fig)
294
-
295
- # Download
296
- csv_processed = data.to_csv(index=False).encode('utf-8')
297
- st.download_button("Scarica Dataset Elaborato (CSV)", csv_processed, "dataset_processed.csv", "text/csv")
298
-
299
- # Salva il dataframe elaborato in session state e resetta i modelli
300
- st.session_state.data = data
301
- st.session_state.models = None # Resetta modelli ML
302
- st.session_state.vae_trained_on_eol = False # Resetta VAE
303
- data_loaded = True # Flag per indicare che i dati sono pronti
304
-
305
- # Mostra messaggio se i dati non sono ancora stati caricati/generati/elaborati
306
- if not data_loaded and st.session_state.get("data_source") is not None:
307
- st.info("Dati caricati/generati. Configura i parametri nella Tab 'Definisci Compatibilità & Target' per elaborare il dataset.")
308
- elif st.session_state.get("data_source") is None:
309
- st.info("Inizia generando o caricando un dataset nella Tab 'Carica/Genera Dati'.")
310
-
311
- # --- Fase 2: Addestramento Modelli ML (Step 1) ---
312
  def train_models(data):
313
- st.header("🤖 2. Addestramento Modelli Classificazione (Step 1)")
314
  if data is None:
315
- st.error("Dataset non disponibile. Preparalo nella Fase 1.")
316
- return None
317
  if 'Target' not in data.columns:
318
- st.error("Colonna 'Target' non trovata nel dataset elaborato.")
319
- return None
320
-
321
- st.markdown("Addestra diversi modelli per predire 'Riutilizzo Funzionale' vs 'Upcycling Creativo'.")
322
 
323
- # Preparazione X, y
324
- # Usiamo le feature definite in ML_FEATURES_STEP1, assicurandoci che esistano nel df
325
- features_to_use = [f for f in ML_FEATURES_STEP1 if f in data.columns]
326
- if len(features_to_use) < len(ML_FEATURES_STEP1):
327
- st.warning(f"Alcune feature attese ({ML_FEATURES_STEP1}) non trovate. Usando: {features_to_use}")
328
  if not features_to_use:
329
- st.error("Nessuna feature valida trovata per l'addestramento.")
330
- return None
331
-
332
- X = data[features_to_use]
333
- # Mappiamo le classi target a 0 e 1
334
- y = data['Target'].map({"Riutilizzo Funzionale": 0, "Upcycling Creativo": 1})
335
-
336
- # Controllo bilanciamento classi
337
- if len(y.unique()) < 2:
338
- st.error("Il dataset elaborato contiene una sola classe target. "
339
- "Verifica i parametri di compatibilità/score o il dataset originale. Impossibile addestrare.")
340
- return None
341
-
342
- # Split Train/Test
343
- X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
344
- st.write(f"Dataset diviso in: {len(X_train)} training samples, {len(X_test)} test samples.")
345
-
346
- # Opzione Tuning (solo per RF come esempio)
347
- tune_rf = st.checkbox("Ottimizza iperparametri per Random Forest (lento)", value=False, key="tune_rf")
348
-
349
- trained_pipelines = {}
350
- results = []
351
- all_models_container = st.container() # Container per i risultati dei modelli
352
-
353
- with st.spinner("Addestramento modelli in corso..."):
354
- # Aggiorna input_dim per i dummy models basato sulle feature reali
355
- MODELS["TabTransformer (Dummy)"] = DummyTabTransformerClassifier(input_dim=X_train.shape[1])
356
- MODELS["SAINT (Dummy)"] = DummySAINTClassifier(input_dim=X_train.shape[1])
357
-
358
- for name, model in MODELS.items():
359
- # Usiamo colonne per layout più compatto
360
- col1, col2 = all_models_container.columns([3, 1]) # Nome modello + Bottone/Risultati
361
- with col1:
362
- st.subheader(f"Modello: {name}")
363
- try:
364
- pipeline = Pipeline([
365
- ('scaler', StandardScaler()), # Scaling è sempre il primo step
366
- ('classifier', model)
367
- ])
368
-
369
- # Tuning opzionale per Random Forest
370
- if tune_rf and name == "Random Forest":
371
- with col1:
372
- st.write("Esecuzione GridSearchCV per Random Forest...")
373
- param_grid = {
374
- 'classifier__n_estimators': [50, 100], # Ridotto per velocità
375
- 'classifier__max_depth': [None, 10, 15],
376
- 'classifier__min_samples_split': [2, 5]
377
- }
378
- # Usiamo CV=2 per velocità nel demo
379
- grid = GridSearchCV(pipeline, param_grid, cv=2, scoring='accuracy', n_jobs=-1)
380
- grid.fit(X_train, y_train)
381
- best_pipeline = grid.best_estimator_
382
- with col1:
383
- st.write(f"Migliori parametri trovati: `{grid.best_params_}`")
384
- pipeline_to_evaluate = best_pipeline # Valuta il modello ottimizzato
385
- else:
386
- # Addestramento standard
387
- pipeline.fit(X_train, y_train)
388
- pipeline_to_evaluate = pipeline # Valuta il modello standard
389
-
390
- # Valutazione
391
- y_pred = pipeline_to_evaluate.predict(X_test)
392
- acc = accuracy_score(y_test, y_pred)
393
- f1 = f1_score(y_test, y_pred, average='weighted') # Usiamo weighted F1
394
-
395
- # Salva risultati e pipeline addestrata
396
- results.append({'Modello': name, 'Accuracy': acc, 'F1 Score': f1})
397
- trained_pipelines[name] = pipeline_to_evaluate
398
-
399
- # Mostra risultati per il modello corrente
400
- with col2:
401
- st.metric("Accuracy", f"{acc:.3f}")
402
- st.metric("F1 Score", f"{f1:.3f}")
403
-
404
- # Matrice di Confusione
405
- with col1:
406
- with st.expander("Mostra Matrice di Confusione"):
407
- fig, ax = plt.subplots(figsize=(4, 3))
408
- cm = confusion_matrix(y_test, y_pred)
409
- sns.heatmap(cm, annot=True, fmt='d', ax=ax, cmap="Greens",
410
- xticklabels=["Riutilizzo", "Upcycling"], yticklabels=["Riutilizzo", "Upcycling"])
411
- plt.xlabel("Predicted")
412
- plt.ylabel("True")
413
- st.pyplot(fig)
414
- all_models_container.markdown("---") # Separatore
415
-
416
- except Exception as e:
417
- with col1:
418
- st.error(f"Errore durante l'addestramento di {name}: {e}")
419
-
420
- # Mostra tabella riassuntiva finale
421
- st.subheader("Risultati Complessivi Addestramento")
422
  if results:
423
- results_df = pd.DataFrame(results).sort_values(by="Accuracy", ascending=False).reset_index(drop=True)
424
- st.dataframe(results_df.style.format({'Accuracy': "{:.3f}", 'F1 Score': "{:.3f}"})
425
- .highlight_max(subset=['Accuracy', 'F1 Score'], color='lightgreen', axis=0))
426
- st.session_state.train_results = results_df # Salva per Dashboard
427
- st.session_state.models = trained_pipelines # Salva i modelli addestrati
428
- return trained_pipelines
429
  else:
430
- st.error("Nessun modello è stato addestrato con successo.")
431
- st.session_state.models = None
432
- return None
 
 
 
 
 
433
 
434
- # --- Fase 3: Inferenza (Step 1) + Trigger VAE (Step 2) ---
435
- def model_inference(trained_pipelines, data_stats): # Passiamo stats per i default
436
- st.header("🔮 3. Inferenza: Previsione Riutilizzo vs Upcycling")
437
  if not trained_pipelines:
438
- st.error("Nessun modello ML addestrato disponibile (Fase 2).")
 
 
 
 
 
 
439
  return
440
- if 'target_params' not in st.session_state:
441
- st.error("Parametri target non definiti. Completa la Fase 1.")
442
- return
443
-
444
- with st.form(key="inference_form_step1"):
445
- st.markdown("#### Inserisci Dati Componente EoL")
446
- # Usiamo data_stats (median) per valori di default sensati
447
- col_inf1, col_inf2, col_inf3 = st.columns(3)
448
- with col_inf1:
449
- length = st.number_input("Lunghezza (mm)", min_value=0.0, value=float(data_stats['length'].median()), step=1.0)
450
- width = st.number_input("Larghezza (mm)", min_value=0.0, value=float(data_stats['width'].median()), step=1.0)
451
- selected_shape = st.selectbox("Forma", list(SHAPE_MAPPING.keys()), index=0)
452
- with col_inf2:
453
- weight = st.number_input("Peso (kg)", min_value=0.0, value=float(data_stats['weight'].median()), step=0.1)
454
- thickness = st.number_input("Spessore (mm)", min_value=0.0, value=float(data_stats['thickness'].median()), step=0.1)
455
- RUL = st.number_input("RUL (0-1000)", min_value=0, max_value=1000, value=int(data_stats['RUL'].median()), step=10)
456
- with col_inf3:
457
- # Margin è calcolato, chiediamo Costo e Valore
458
- valore_mercato = st.number_input("Valore Mercato Stimato (€)", min_value=0.0, value=float(data_stats['margin'].median()+50), step=10.0) # Default basato su margin mediano + costo fittizio
459
- costo_riparazione = st.number_input("Costo Riparazione Stimato (€)", min_value=0.0, value=50.0, step=10.0)
460
-
461
- submit_button = st.form_submit_button("Esegui Predizione (Step 1)")
462
-
463
- if submit_button:
464
- # --- Preparazione Input per Modelli ML ---
465
- margin = valore_mercato - costo_riparazione
466
- shape_code = SHAPE_MAPPING.get(selected_shape, -1) # Usa mapping, default -1 se non trovato
467
-
468
- # Crea dizionario input per compat_dim e ML
469
- input_dict_ml = {
470
- "length": length, "width": width, "shape": selected_shape, # shape stringa per dimension_match
471
- "weight": weight, "thickness": thickness, "RUL": RUL, "margin": margin
 
472
  }
473
- input_df_temp = pd.DataFrame([input_dict_ml])
474
 
475
- # Calcola compat_dim usando parametri salvati
476
- input_df_temp['compat_dim'] = input_df_temp.apply(lambda row: dimension_match(row, **st.session_state.target_params), axis=1)
 
477
 
478
- # Aggiungi shape_code e rimuovi shape stringa per predizione ML
479
- input_df_ml = input_df_temp.copy()
480
- input_df_ml['shape_code'] = shape_code
481
- input_df_ml = input_df_ml.drop(columns=['shape'])
482
 
483
- # Assicura che le colonne siano nell'ordine atteso dai modelli (basato su ML_FEATURES_STEP1)
484
  try:
485
- input_df_ml_ordered = input_df_ml[ML_FEATURES_STEP1]
486
  except KeyError as e:
487
- st.error(f"Errore: Colonna mancante nell'input per ML: {e}. Feature attese: {ML_FEATURES_STEP1}")
488
- st.dataframe(input_df_ml) # Mostra cosa è stato preparato
489
- return # Interrompi se l'input non è corretto
490
-
491
- # --- Predizione con tutti i modelli addestrati ---
492
- model_predictions = []
493
- model_details_list = []
494
- with st.spinner("Esecuzione predizioni modelli ML..."):
495
- for name, pipe in trained_pipelines.items():
496
- try:
497
- pred_num = pipe.predict(input_df_ml_ordered)[0] # 0 o 1
498
- proba = pipe.predict_proba(input_df_ml_ordered)[0] # Probabilità [prob_0, prob_1]
499
- model_predictions.append(pred_num)
500
- model_details_list.append({
501
- "Modello": name,
502
- "Predizione (0=Riutilizzo, 1=Upcycling)": pred_num,
503
- "Prob. Riutilizzo": proba[0],
504
- "Prob. Upcycling": proba[1]
505
- })
506
- except Exception as e:
507
- st.warning(f"Errore durante la predizione con {name}: {e}")
508
 
509
- # --- Aggregazione Risultati ---
510
  if not model_predictions:
511
- st.error("Nessun modello ha prodotto una predizione valida.")
512
  return
513
 
 
514
  try:
515
- # Usa la moda (predizione più frequente)
516
- aggregated_pred_num = mode(model_predictions)
517
  except StatisticsError:
518
- # Se c'è pareggio, usa la media delle probabilità di 'Riutilizzo'
519
- avg_prob_reuse = np.mean([d["Prob. Riutilizzo"] for d in model_details_list])
520
- aggregated_pred_num = 0 if avg_prob_reuse >= 0.5 else 1
521
-
522
- aggregated_label = "Riutilizzo Funzionale" if aggregated_pred_num == 0 else "Upcycling Creativo"
523
-
524
- # --- Mostra Risultati Step 1 ---
525
- st.subheader("Risultato Predizione (Step 1)")
526
- st.metric("Previsione Aggregata:", aggregated_label)
527
-
528
- with st.expander("Dettagli Predizioni Singoli Modelli"):
529
- details_df = pd.DataFrame(model_details_list)
530
- details_df["Prob. Riutilizzo"] = details_df["Prob. Riutilizzo"].apply(lambda x: f"{x:.1%}")
531
- details_df["Prob. Upcycling"] = details_df["Prob. Upcycling"].apply(lambda x: f"{x:.1%}")
532
- st.dataframe(details_df)
533
-
534
- # --- LOGICA CONDIZIONALE PER STEP 2 (VAE/GenAI) ---
535
- if aggregated_label == "Upcycling Creativo":
536
  st.markdown("---")
537
- st.subheader("🧬 Step 2: Esplorazione Generativa (Upcycling)")
538
- st.warning("La predizione suggerisce 'Upcycling Creativo'. Puoi usare il VAE per generare idee di riuso.")
539
 
540
- # Controlla se il VAE è stato addestrato
541
- if not st.session_state.get("vae_trained_on_eol", False):
542
- st.error("Il modello VAE non è stato ancora addestrato. Vai alla fase '🧬 Training VAE' e addestralo prima di generare idee.")
543
  else:
544
- vae_model = st.session_state.get("vae")
545
- vae_scaler = st.session_state.get("vae_scaler")
546
  if vae_model is None or vae_scaler is None:
547
- st.error("Errore: Modello VAE o scaler non trovati in session_state anche se marcato come addestrato.")
548
  else:
549
- n_generate_vae = st.number_input("Quante idee generare?", 1, 10, 3, key="n_gen_vae_inf")
550
- if st.button("Genera Idee di Riuso con VAE"):
551
- with st.spinner("Generazione VAE in corso..."):
552
- vae_model.eval()
553
- with torch.no_grad():
554
- # Recupera latent_dim dal modello caricato
555
- latent_dim = vae_model.fc21.out_features
556
- z = torch.randn(n_generate_vae, latent_dim)
557
- recon_scaled = vae_model.decode(z)
558
-
559
- try:
560
- # Decodifica e mostra
561
- recon_original = vae_scaler.inverse_transform(recon_scaled.numpy())
562
- # Le colonne sono quelle usate per addestrare il VAE
563
- vae_feature_names = vae_scaler.feature_names_in_
564
- df_gen = pd.DataFrame(recon_original, columns=vae_feature_names)
565
-
566
- st.write(f"**{n_generate_vae} Configurazioni Geometriche Generate:**")
567
- # Arrotonda e formatta shape_code come intero
568
- if 'shape_code' in df_gen.columns:
569
- df_gen['shape_code'] = df_gen['shape_code'].round().astype(int)
570
- # Opzionale: riconverti shape_code in nome forma
571
- inv_shape_map = {v: k for k, v in SHAPE_MAPPING.items()}
572
- df_gen['shape'] = df_gen['shape_code'].map(inv_shape_map).fillna('sconosciuto')
573
-
574
- st.dataframe(df_gen.round(2))
575
- st.caption("Nota: Queste sono configurazioni generate casualmente dal VAE, basate sulla distribuzione appresa. Rappresentano 'idee' o punti di partenza.")
576
-
577
- except Exception as e:
578
- st.error(f"Errore durante decodifica VAE: {e}")
579
-
580
- elif aggregated_label == "Riutilizzo Funzionale":
581
- st.success("La predizione suggerisce 'Riutilizzo Funzionale'. Non è richiesta la generazione VAE per questo caso.")
582
-
583
- # --- Fase 4: Training VAE (NUOVA FASE) ---
584
  def vae_training_phase():
585
- st.header("🧬 4. Training VAE (Generative AI - Step 2)")
586
- st.markdown("Addestra il Variational Autoencoder (VAE) sulle feature geometriche/fisiche del dataset per la generazione di idee di upcycling.")
587
 
588
  if 'data' not in st.session_state or st.session_state['data'] is None:
589
- st.error("Dataset non disponibile. Prepara il dataset nella Fase 1.")
590
  return
591
 
592
- data = st.session_state['data']
593
- # Seleziona le feature definite in VAE_FEATURES_STEP2, controllando che esistano
594
- features_for_vae = [f for f in VAE_FEATURES_STEP2 if f in data.columns]
595
- if not features_for_vae:
596
- st.error(f"Nessuna delle feature richieste per il VAE ({VAE_FEATURES_STEP2}) trovata nel dataset.")
597
  return
 
 
 
 
 
 
 
598
 
599
- st.write(f"Il VAE sarà addestrato su: `{', '.join(features_for_vae)}`")
600
- INPUT_DIM_VAE = len(features_for_vae)
601
-
602
- # --- Configurazione VAE ---
603
- with st.expander("Parametri VAE", expanded=False):
604
- latent_dim = st.slider("Dimensione Latente VAE", 2, 16, 3, step=1, key="vae_lat_dim_train")
605
- epochs = st.number_input("Epochs VAE", 10, 500, 100, step=10, key="vae_epo_train")
606
- lr = st.number_input("Learning Rate VAE", 1e-5, 1e-2, 1e-3, format="%e", key="vae_lr_train")
607
- batch_size = st.selectbox("Batch Size VAE", [16, 32, 64, 128], index=1, key="vae_bs_train")
608
-
609
- # --- Inizializzazione/Reinizializzazione VAE ---
610
- vae_needs_reinit = False
611
- if "vae" not in st.session_state or st.session_state["vae"] is None: vae_needs_reinit = True
612
- elif st.session_state["vae"].fc1.in_features != INPUT_DIM_VAE or st.session_state["vae"].fc21.out_features != latent_dim: vae_needs_reinit = True
613
-
614
- if vae_needs_reinit:
615
- st.session_state["vae"] = MiniVAE(input_dim=INPUT_DIM_VAE, latent_dim=latent_dim)
616
- st.session_state["vae_trained_on_eol"] = False
617
- st.session_state["vae_scaler"] = None
618
- st.info(f"VAE Inizializzato (Input={INPUT_DIM_VAE}, Latent={latent_dim}). Pronto per l'addestramento.")
619
- vae = st.session_state["vae"]
620
-
621
- # --- Bottone e Logica di Training ---
622
- if not st.session_state.get("vae_trained_on_eol", False):
623
- st.warning("VAE non ancora addestrato.")
624
  if st.button("Avvia Training VAE"):
625
- X_vae = data[features_for_vae].copy()
626
- # Gestione valori NaN (importante!) - Sostituzione con mediana come esempio
627
- for col in X_vae.columns:
628
- if X_vae[col].isnull().any():
629
- median_val = X_vae[col].median()
630
- X_vae[col] = X_vae[col].fillna(median_val)
631
- st.warning(f"Valori NaN in '{col}' sostituiti con mediana ({median_val:.2f})")
632
-
633
- with st.spinner("Training VAE in corso..."):
634
- # Scaling
635
- scaler = StandardScaler()
636
- X_scaled = scaler.fit_transform(X_vae)
637
- st.session_state["vae_scaler"] = scaler # Salva lo scaler FITTATO
638
- X_t = torch.tensor(X_scaled, dtype=torch.float32)
639
-
640
- # DataLoader & Optimizer
641
- dataset = torch.utils.data.TensorDataset(X_t)
642
- loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)
643
- optimizer = torch.optim.Adam(vae.parameters(), lr=lr)
644
-
645
- # Training Loop
646
- losses = []
647
- progress_bar = st.progress(0)
648
- status_text = st.empty()
649
- vae.train()
650
- for ep in range(epochs):
651
- epoch_loss = 0
652
- for batch_idx, (batch_data,) in enumerate(loader):
653
- optimizer.zero_grad()
654
- recon, mu, logvar = vae(batch_data)
655
- loss = vae_loss(recon, batch_data, mu, logvar)
656
- loss.backward()
657
- optimizer.step()
658
- epoch_loss += loss.item()
659
- avg_loss = epoch_loss / len(loader.dataset)
660
- losses.append(avg_loss)
661
- status_text.text(f"Epoch {ep+1}/{epochs} | Avg Loss: {avg_loss:.4f}")
662
- progress_bar.progress((ep + 1) / epochs)
663
-
664
- st.session_state["vae_trained_on_eol"] = True
665
- st.success("Training VAE completato!")
666
- st.line_chart(pd.DataFrame(losses, columns=['VAE Training Loss']))
667
  else:
668
- st.info("Il VAE risulta già addestrato con i parametri correnti.")
669
  if st.button("Riallena VAE"):
670
- st.session_state["vae_trained_on_eol"] = False
671
- st.rerun()
672
 
673
- # --- Fase 5: Dashboard (resta simile) ---
 
 
674
  def show_dashboard():
675
- st.header("📊 Dashboard Riepilogativa")
676
  if 'data' not in st.session_state or st.session_state['data'] is None:
677
- st.error("Nessun dataset disponibile. Prepara il dataset nella Fase 1.")
678
  return
679
-
680
- data = st.session_state['data']
681
- st.subheader("Panoramica Dataset Elaborato")
682
- total_samples = len(data)
683
- class_counts = data['Target'].value_counts()
684
- reuse_pct = (class_counts.get("Riutilizzo Funzionale", 0) / total_samples) * 100
685
- upcycling_pct = (class_counts.get("Upcycling Creativo", 0) / total_samples) * 100
686
-
687
- col1, col2, col3 = st.columns(3)
688
- col1.metric("Campioni Totali", total_samples)
689
- col2.metric("Previsti Riutilizzo Funzionale", f"{reuse_pct:.1f}%")
690
- col3.metric("Previsti Upcycling Creativo", f"{upcycling_pct:.1f}%")
691
-
692
- # Grafico a Torta Distribuzione Classi
693
- if not class_counts.empty:
694
- fig_pie, ax_pie = plt.subplots(figsize=(5, 3))
695
- ax_pie.pie(class_counts, labels=class_counts.index, autopct='%1.1f%%', startangle=90, colors=['#66c2a5','#fc8d62']) # Colori esempio
696
- ax_pie.axis('equal')
697
- st.pyplot(fig_pie)
698
-
699
- st.subheader("Performance Modelli ML (Step 1)")
700
- if 'train_results' in st.session_state:
701
- results_df = st.session_state['train_results']
702
- avg_accuracy = results_df['Accuracy'].mean()
703
- best_model_idx = results_df['Accuracy'].idxmax()
704
- best_model_name = results_df.loc[best_model_idx]['Modello']
705
- best_model_acc = results_df.loc[best_model_idx]['Accuracy']
706
-
707
- col4, col5 = st.columns(2)
708
- col4.metric("Accuratezza Media Modelli", f"{avg_accuracy:.3f}")
709
- col5.metric(f"Miglior Modello: {best_model_name}", f"{best_model_acc:.3f}")
710
- st.dataframe(results_df.style.format({'Accuracy': "{:.3f}", 'F1 Score': "{:.3f}"}))
711
- else:
712
- st.info("Addestra i modelli ML (Fase 2) per visualizzare le metriche di performance.")
713
-
714
- st.subheader("Stato Modello VAE (Step 2)")
715
- if st.session_state.get("vae_trained_on_eol", False) and st.session_state.get("vae") is not None:
716
- vae = st.session_state["vae"]
717
- st.success("Modello VAE addestrato.")
718
- col_v1, col_v2 = st.columns(2)
719
- col_v1.metric("Feature Input VAE", vae.fc1.in_features)
720
- col_v2.metric("Dimensione Latente VAE", vae.fc21.out_features)
721
- elif "vae" in st.session_state and st.session_state["vae"] is not None:
722
- st.warning("Modello VAE inizializzato ma non addestrato.")
723
  else:
724
- st.info("Modello VAE non ancora inizializzato (visitare Fase 4).")
725
 
 
 
 
 
 
726
 
727
- # --- Fase 6: Guida (resta simile) ---
 
 
728
  def show_help():
729
- st.header("ℹ️ Guida all'Uso")
730
  st.markdown("""
731
- **Workflow Applicazione:**
732
-
733
- 1. **♻️ Dataset:**
734
- * Genera dati sintetici o carica un tuo file CSV con le caratteristiche dei componenti EoL.
735
- * Nella tab "Definisci Compatibilità & Target", imposta i parametri dimensionali target, le tolleranze e i pesi per lo score RUL/Margin.
736
- * Il sistema elabora i dati, calcola la compatibilità dimensionale (`compat_dim`) e assegna la classe **"Riutilizzo Funzionale"** o **"Upcycling Creativo"** a ciascun campione.
737
- * Visualizza l'anteprima, la distribuzione delle classi e scarica il dataset elaborato.
738
-
739
- 2. **🤖 Addestramento Modelli ML (Step 1):**
740
- * Addestra una serie di modelli di Machine Learning (Random Forest, Gradient Boosting, ecc.) per predire la classe ("Riutilizzo Funzionale" / "Upcycling Creativo") basandosi sulle feature elaborate.
741
- * Visualizza le performance (Accuracy, F1 Score) e le matrici di confusione per ciascun modello.
742
-
743
- 3. **🔮 Inferenza (Step 1 & 2):**
744
- * Inserisci le caratteristiche dimensionali ed economiche di un **nuovo** componente EoL.
745
- * Clicca "Esegui Predizione". Il sistema usa i modelli ML addestrati per predire la classe più probabile (aggregando i risultati).
746
- * **Flusso Condizionale:**
747
- * Se la predizione è **"Riutilizzo Funzionale"**, il processo termina qui per questo componente.
748
- * Se la predizione è **"Upcycling Creativo"**, appare una nuova sezione che ti permette di usare il modello VAE (Generative AI) per **generare idee** di configurazioni geometriche alternative, basate sulla distribuzione appresa dai dati. (Assicurati di aver addestrato il VAE nella Fase 4!).
749
-
750
- 4. **🧬 Training VAE (Step 2):**
751
- * Questa fase serve ad addestrare il modello VAE (Generative AI) usando le **feature geometriche/fisiche** del dataset preparato nella Fase 1.
752
- * Questo modello impara la "forma" tipica dei dati e può essere usato nella Fase di Inferenza per generare nuove idee quando viene predetto "Upcycling Creativo". **Devi addestrare il VAE qui prima di poter generare idee nella fase di Inferenza.**
753
-
754
- 5. **📊 Dashboard:**
755
- * Visualizza una sintesi dello stato del dataset, delle performance dei modelli ML e dello stato del modello VAE.
756
-
757
- **Reset:** Usa il pulsante "Reset" nella sidebar per cancellare tutti i dati e i modelli in memoria e ricominciare.
758
  """)
759
 
760
- # --- Funzione Reset ---
 
 
761
  def reset_app():
762
- # Lista delle chiavi da cancellare o resettare
763
- keys_to_clear = ['data', 'models', 'train_results', 'vae', 'vae_trained_on_eol', 'vae_scaler', 'target_params', 'score_params', 'data_source']
764
- for key in keys_to_clear:
765
- if key in st.session_state:
766
- del st.session_state[key]
767
- st.success("Stato dell'applicazione resettato.")
768
- # Potrebbe essere utile fare st.rerun() qui per aggiornare subito la UI
769
- st.rerun()
770
-
771
- ##########################################
772
- # 5. MAIN FUNCTION (Flusso Principale App)
773
- ##########################################
 
774
  def main():
775
- st.sidebar.image("https://www.weeko.it/wp-content/uploads/2023/07/logo-weeko-esteso-1.png", width=200) # Logo esempio
776
  st.sidebar.title("Menu Principale")
777
-
778
- # Inizializza session_state se non esiste (prima esecuzione)
779
- if 'data' not in st.session_state: st.session_state.data = None
780
- if 'models' not in st.session_state: st.session_state.models = None
781
- if 'vae' not in st.session_state: st.session_state.vae = None
782
- if 'vae_trained_on_eol' not in st.session_state: st.session_state.vae_trained_on_eol = False
783
- if 'vae_scaler' not in st.session_state: st.session_state.vae_scaler = None
784
- if 'target_params' not in st.session_state: st.session_state.target_params = {}
785
- if 'score_params' not in st.session_state: st.session_state.score_params = {}
786
- if 'train_results' not in st.session_state: st.session_state.train_results = None
787
-
788
-
789
- # Menu Sidebar con le fasi corrette
790
- phase = st.sidebar.radio(
791
- "Seleziona fase:",
792
- ["♻️ Dataset", "🤖 Addestramento ML (Step 1)", "🔮 Inferenza (Step 1 & 2)", "🧬 Training VAE (Step 2)", "📊 Dashboard", "ℹ️ Guida"],
793
- key="main_menu"
794
- )
795
-
796
- # Pulsante Reset
797
- st.sidebar.markdown("---")
798
- st.sidebar.button("⚠️ Reset Applicazione", on_click=reset_app, type="primary")
799
-
800
- # Esecuzione fase selezionata
801
- if phase == "♻️ Dataset":
802
- prepare_dataset() # Questa funzione ora salva i dati in st.session_state.data
803
- elif phase == "🤖 Addestramento ML (Step 1)":
804
- # Passiamo i dati dalla sessione
805
- train_models(st.session_state.get('data'))
806
- elif phase == "🔮 Inferenza (Step 1 & 2)":
807
- # Controlla se i modelli ML e i dati esistono
808
- if st.session_state.get('models') is None or st.session_state.get('data') is None:
809
- st.error("Errore: Devi prima preparare il Dataset (Fase 1) e addestrare i Modelli ML (Fase 2).")
810
  else:
811
- # Passa i modelli e le statistiche del dataset (per i default nell'input form)
812
- model_inference(st.session_state['models'], st.session_state['data'])
813
- elif phase == "🧬 Training VAE (Step 2)":
814
- # Questa fase usa st.session_state.data internamente
815
  vae_training_phase()
816
- elif phase == "📊 Dashboard":
817
  show_dashboard()
818
- elif phase == "ℹ️ Guida":
819
  show_help()
820
 
821
- if __name__ == "__main__":
822
- main()
 
4
  import seaborn as sns
5
  import matplotlib.pyplot as plt
6
  from statistics import mode, StatisticsError
7
+ import io
8
 
9
  # --- Scikit-learn ---
10
  from sklearn.model_selection import train_test_split, GridSearchCV
11
+ from sklearn.preprocessing import StandardScaler, LabelEncoder
12
  from sklearn.pipeline import Pipeline
13
  from sklearn.metrics import confusion_matrix, accuracy_score, f1_score
14
  from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
15
  from sklearn.linear_model import LogisticRegression
16
  from sklearn.svm import SVC
17
+ from sklearn.neural_network import MLPClassifier
18
 
19
  # --- PyTorch (per VAE) ---
20
  import torch
 
23
  import torch.utils.data
24
  import random
25
 
26
+ # Impostazioni generali Streamlit
27
  st.set_page_config(
28
  page_title="WEEKO - AI Reuse Analyzer",
29
  page_icon="♻️",
 
31
  )
32
 
33
  ##########################################
34
+ # 1) MODELLI ML (Zero Scarto Analyzer)
35
  ##########################################
36
+
37
  class DummyTabTransformerClassifier:
38
+ """
39
+ Placeholder modello: in realtà è un MLP,
40
+ ma finge di essere un TabTransformer
41
+ """
42
+ def __init__(self, input_dim=8):
43
+ self.clf = MLPClassifier(hidden_layer_sizes=(max(16,input_dim*2), max(8,input_dim)),
44
+ max_iter=100, random_state=42, alpha=0.01, learning_rate_init=0.01)
45
  def fit(self, X, y):
46
  self.clf.fit(X, y)
47
  return self
48
  def predict(self, X):
49
  return self.clf.predict(X)
50
  def predict_proba(self, X):
 
51
  if hasattr(self.clf, 'predict_proba'):
52
  return self.clf.predict_proba(X)
53
+ else:
54
  preds = self.clf.predict(X)
55
  return np.array([[1.0, 0.0] if p == 0 else [0.0, 1.0] for p in preds])
56
 
 
57
  class DummySAINTClassifier:
58
+ """
59
+ Placeholder modello: in realtà è un MLP,
60
+ ma finge di essere un SAINT
61
+ """
62
+ def __init__(self, input_dim=8):
63
+ self.clf = MLPClassifier(hidden_layer_sizes=(max(20,input_dim*2), max(10,input_dim)),
64
+ max_iter=120, random_state=42, alpha=0.005, learning_rate_init=0.005)
65
  def fit(self, X, y):
66
  self.clf.fit(X, y)
67
  return self
68
  def predict(self, X):
69
  return self.clf.predict(X)
70
  def predict_proba(self, X):
71
+ if hasattr(self.clf, 'predict_proba'):
72
  return self.clf.predict_proba(X)
73
+ else:
74
  preds = self.clf.predict(X)
75
+ return np.array([[1.0,0.0] if p == 0 else [0.0,1.0] for p in preds])
76
 
 
77
  MODELS = {
78
  "Random Forest": RandomForestClassifier(random_state=42, n_estimators=100, class_weight='balanced'),
79
  "Gradient Boosting": GradientBoostingClassifier(random_state=42, n_estimators=100),
 
84
  }
85
 
86
  ##########################################
87
+ # 2) VAE per generative reuse (Fase 2)
88
  ##########################################
89
+
90
  class MiniVAE(nn.Module):
 
91
  def __init__(self, input_dim=5, latent_dim=2):
92
  super().__init__()
 
93
  self.fc1 = nn.Linear(input_dim, 32)
94
+ self.fc21 = nn.Linear(32, latent_dim)
95
+ self.fc22 = nn.Linear(32, latent_dim)
 
96
  self.fc3 = nn.Linear(latent_dim, 32)
97
  self.fc4 = nn.Linear(32, input_dim)
98
 
 
115
  recon = self.decode(z)
116
  return recon, mu, logvar
117
 
 
118
  def vae_loss(recon_x, x, mu, logvar):
119
  recon_loss = F.mse_loss(recon_x, x, reduction='sum')
120
  kld = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
121
  return recon_loss + kld
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  ##########################################
124
+ # Feature sets
125
  ##########################################
126
+ DEFAULT_FEATURES_STEP1 = ['length','width','RUL','margin','shape','weight','thickness']
127
+ ML_FEATURES_STEP1 = ['length','width','shape_code','weight','thickness','RUL','margin','compat_dim']
128
+ VAE_FEATURES_STEP2 = ['length','width','weight','thickness','shape_code']
 
 
 
129
 
130
+ ##########################################
131
+ # Mappatura forma -> shape_code
132
+ ##########################################
133
+ SHAPE_MAPPING = {
134
+ 'axisymmetric': 0,
135
+ 'sheet_metal': 1,
136
+ 'alloy_plate': 2,
137
+ 'complex_plastic': 3
138
+ }
139
 
140
+ ##########################################
141
+ # Generazione dataset sintetico
142
+ ##########################################
143
  def generate_synthetic_data(n_samples=300, seed=42):
144
  np.random.seed(seed)
145
+ length = np.clip(np.random.normal(100, 20, n_samples), 50, 250)
146
+ width = np.clip(np.random.normal(50, 15, n_samples), 20, 150)
147
+ RUL = np.clip(np.random.normal(500, 250, n_samples), 0, 1000).astype(int)
148
+ margin = np.clip(np.random.normal(150,150,n_samples), -200,600).astype(int)
149
+ shapes = np.random.choice(['axisymmetric','sheet_metal','alloy_plate','complex_plastic'],
150
+ size=n_samples, p=[0.4,0.3,0.15,0.15])
151
+ weight = np.clip(np.random.normal(80,30,n_samples), 10, 250)
152
+ thickness = np.clip(np.random.normal(8,4,n_samples), 0.5, 30)
153
 
154
  return pd.DataFrame({
155
+ 'length': length, 'width': width, 'RUL':RUL, 'margin':margin,
156
+ 'shape':shapes, 'weight':weight, 'thickness':thickness
157
  })
158
 
159
+ ##########################################
160
+ # dimension_match + assegnazione classe
161
+ ##########################################
162
+ def dimension_match(row, target_length, target_width, target_shape,
163
+ target_weight, target_thickness,
164
  tol_len, tol_wid, tol_weight, tol_thickness):
165
  cond_length = abs(row['length'] - target_length) <= tol_len
166
+ cond_width = abs(row['width'] - target_width) <= tol_wid
167
+ cond_shape = row['shape'] == target_shape
168
+ cond_weight = abs(row['weight'] - target_weight)<= tol_weight
169
+ cond_thickness = abs(row['thickness'] - target_thickness)<=tol_thickness
 
170
  return 1 if (cond_length and cond_width and cond_shape and cond_weight and cond_thickness) else 0
171
 
 
172
  def assign_class(row, threshold_score=0.5, alpha=0.5, beta=0.5):
173
+ rul_norm = row['RUL']/1000.0
174
+ margin_norm = (row['margin']+200)/800.0
175
+ score = alpha*rul_norm + beta*margin_norm
176
+ if row['compat_dim']==1 and score>=threshold_score:
177
  return "Riutilizzo Funzionale"
178
  else:
 
179
  return "Upcycling Creativo"
180
 
 
 
 
181
  ##########################################
182
+ # --- Fase 1: Preparazione Dataset
183
  ##########################################
 
 
184
  def prepare_dataset():
185
  st.header("♻️ 1. Preparazione Dataset EoL")
 
 
186
 
187
+ tab1, tab2 = st.tabs(["Carica/Genera Dati", "Definisci Compatibilità & Target"])
188
+ data = None
189
  with tab1:
190
+ data_option = st.radio("Fonte Dati", ["Genera dati sintetici","Carica CSV"], horizontal=True)
191
+ if data_option=="Genera dati sintetici":
192
+ n_samples=st.slider("Numero di campioni",100,2000,500,step=50)
 
 
 
193
  if st.button("Genera Dati"):
194
+ data=generate_synthetic_data(n_samples=n_samples)
195
+ st.session_state.data_source="generated"
196
  else:
197
+ file=st.file_uploader("Carica CSV con le feature minime", type=["csv"])
198
+ if file:
199
  try:
200
+ data=pd.read_csv(file)
 
201
  if not all(col in data.columns for col in DEFAULT_FEATURES_STEP1):
202
+ st.error(f"Il CSV deve contenere almeno: {DEFAULT_FEATURES_STEP1}")
203
+ data=None
204
  else:
205
+ st.session_state.data_source="uploaded"
206
  except Exception as e:
207
+ st.error(f"Errore lettura CSV: {e}")
208
+ data=None
 
 
209
 
 
210
  if data is not None:
211
+ with tab2:
212
+ st.subheader("Parametri di Compatibilità")
213
+ c1,c2=st.columns(2)
214
+ with c1:
215
+ t_len=st.number_input("Lunghezza target (mm)",50.0,250.0,100.0,step=1.0)
216
+ t_wid=st.number_input("Larghezza target (mm)",20.0,150.0,50.0,step=1.0)
217
+ t_shape=st.selectbox("Forma target", list(SHAPE_MAPPING.keys()), index=0)
218
+ with c2:
219
+ t_weight=st.number_input("Peso target (kg)",10.0,250.0,80.0,step=1.0)
220
+ t_thick=st.number_input("Spessore target (mm)",0.5,30.0,8.0,step=0.5)
221
+
222
+ st.markdown("**Tolleranze**")
223
+ col_tol1,col_tol2=st.columns(2)
 
 
 
224
  with col_tol1:
225
+ tol_len=st.slider("Tolleranza lunghezza ±",0.0,20.0,5.0,step=0.5)
226
+ tol_wid=st.slider("Tolleranza larghezza ±",0.0,15.0,3.0,step=0.5)
227
  with col_tol2:
228
+ tol_we=st.slider("Tolleranza peso ±",0.0,30.0,10.0,step=1.0)
229
+ tol_th=st.slider("Tolleranza spessore ±",0.0,5.0,1.0,step=0.1)
230
+
231
+ st.markdown("**Score RUL & Margin**")
232
+ thr_score=st.slider("Soglia minima score",0.0,1.0,0.5,step=0.05)
233
+ alpha=st.slider("Peso RUL (α)",0.0,1.0,0.5,step=0.05)
234
+ beta=st.slider("Peso Margin (β)",0.0,1.0,0.5,step=0.05)
235
+
236
+ # Codifica shape
237
+ data['shape_code']=data['shape'].map(SHAPE_MAPPING).fillna(-1).astype(int)
238
+
239
+ # compat_dim
240
+ data['compat_dim'] = data.apply(
241
+ lambda row: dimension_match(row,
242
+ target_length=t_len, target_width=t_wid, target_shape=t_shape,
243
+ target_weight=t_weight, target_thickness=t_thick,
244
+ tol_len=tol_len, tol_wid=tol_wid,
245
+ tol_weight=tol_we, tol_thickness=tol_th),
246
+ axis=1
247
+ )
248
+
249
+ # Assegna Target
250
+ data['Target']=data.apply(lambda row: assign_class(row,
251
+ threshold_score=thr_score,
252
+ alpha=alpha, beta=beta),
253
+ axis=1)
254
+
255
+ st.session_state.target_params={
256
+ "target_length": t_len,
257
+ "target_width": t_wid,
258
+ "target_shape": t_shape,
259
+ "target_weight": t_weight,
260
+ "target_thickness": t_thick,
261
+ "tol_len": tol_len,
262
+ "tol_wid": tol_wid,
263
+ "tol_weight": tol_we,
264
+ "tol_thickness": tol_th
265
+ }
266
+ st.session_state.score_params={
267
+ "threshold_score": thr_score,
268
+ "alpha": alpha,
269
+ "beta": beta
270
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
 
 
272
  st.dataframe(data.head(10))
273
+ st.write("Distribuzione Classi:", data['Target'].value_counts())
 
274
 
 
275
  numeric_cols = data.select_dtypes(include=np.number)
276
  if not numeric_cols.empty:
277
+ fig,ax=plt.subplots(figsize=(8,6))
278
+ sns.heatmap(numeric_cols.corr(), annot=True, cmap='viridis', fmt=".2f", ax=ax)
279
+ st.pyplot(fig)
280
+
281
+ st.session_state.data=data
282
+ csv=data.to_csv(index=False).encode('utf-8')
283
+ st.download_button("Scarica Dataset Elaborato CSV", csv, "dataset_processed.csv")
284
+
285
+ #######################################
286
+ # Fase 2: Training Modelli ML
287
+ #######################################
 
 
 
 
 
 
 
 
 
 
 
288
  def train_models(data):
289
+ st.header("🤖 2. Addestramento ML (Riutilizzo vs Upcycling)")
290
  if data is None:
291
+ st.error("Nessun dataset elaborato. Torna alla Fase 1.")
292
+ return
293
  if 'Target' not in data.columns:
294
+ st.error("Colonna 'Target' assente. Verifica la Fase 1.")
295
+ return
 
 
296
 
297
+ # X,y
298
+ features_to_use=[f for f in ML_FEATURES_STEP1 if f in data.columns]
 
 
 
299
  if not features_to_use:
300
+ st.error("Nessuna feature valida per l'addestramento ML.")
301
+ return
302
+
303
+ X=data[features_to_use].copy()
304
+ y=data['Target'].map({"Riutilizzo Funzionale":0, "Upcycling Creativo":1})
305
+
306
+ # Se c'è una sola classe
307
+ if len(y.unique())<2:
308
+ st.error("Il dataset contiene una sola classe. Aggiusta i parametri in Fase 1.")
309
+ return
310
+
311
+ X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.25,random_state=42,stratify=y)
312
+ st.write(f"Dati: {len(X_train)} train, {len(X_test)} test.")
313
+ tune_rf=st.checkbox("Ottimizza RandomForest (GridSearchCV)", value=False)
314
+
315
+ trained_pipelines={}
316
+ results=[]
317
+ for name,model in MODELS.items():
318
+ st.subheader(f"Modello: {name}")
319
+ from sklearn.pipeline import Pipeline
320
+ pipe=Pipeline([
321
+ ("scaler",StandardScaler()),
322
+ ("clf",model)
323
+ ])
324
+ try:
325
+ if tune_rf and name=="Random Forest":
326
+ st.write("GridSearch per RandomForest...")
327
+ param_grid={
328
+ 'clf__n_estimators':[50,100],
329
+ 'clf__max_depth':[None,10,15]
330
+ }
331
+ grid=GridSearchCV(pipe, param_grid, cv=2, scoring='accuracy', n_jobs=-1)
332
+ grid.fit(X_train,y_train)
333
+ best_model=grid.best_estimator_
334
+ st.write(f"Migliori parametri: {grid.best_params_}")
335
+ y_pred=best_model.predict(X_test)
336
+ pipe_to_use=best_model
337
+ else:
338
+ pipe.fit(X_train,y_train)
339
+ y_pred=pipe.predict(X_test)
340
+ pipe_to_use=pipe
341
+
342
+ acc=accuracy_score(y_test,y_pred)
343
+ f1=f1_score(y_test,y_pred,average='weighted')
344
+ results.append({"Modello":name,"Accuracy":acc,"F1 Score":f1})
345
+ trained_pipelines[name]=pipe_to_use
346
+
347
+ cm=confusion_matrix(y_test,y_pred)
348
+ fig,ax=plt.subplots()
349
+ sns.heatmap(cm,annot=True,fmt='d',ax=ax,cmap="Greens")
350
+ plt.xlabel("Pred")
351
+ plt.ylabel("True")
352
+ st.pyplot(fig)
353
+
354
+ st.metric("Accuracy",f"{acc:.3f}")
355
+ st.metric("F1 Score",f"{f1:.3f}")
356
+
357
+ except Exception as e:
358
+ st.error(f"Errore addestramento {name}: {e}")
359
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  if results:
361
+ results_df=pd.DataFrame(results).sort_values(by="Accuracy",ascending=False)
362
+ st.dataframe(results_df)
363
+ st.session_state.train_results=results_df
364
+ st.session_state.models=trained_pipelines
 
 
365
  else:
366
+ st.error("Nessun modello addestrato con successo.")
367
+ st.session_state.models=None
368
+
369
+ #######################################
370
+ # Fase 3: Inferenza + Trigger Upcycling -> VAE
371
+ #######################################
372
+ def model_inference(trained_pipelines, data):
373
+ st.header("🔮 3. Inferenza: Step 1 (Riutilizzo vs Upcycling) & Step 2 (VAE Upcycling)")
374
 
 
 
 
375
  if not trained_pipelines:
376
+ st.error("Prima addestra i modelli (Fase 2).")
377
+ return
378
+ if data is None:
379
+ st.error("Nessun dataset disponibile in session. Torna a Fase 1.")
380
+ return
381
+ if 'target_params' not in st.session_state or 'score_params' not in st.session_state:
382
+ st.error("Parametri target non definiti. Completa la Fase 1.")
383
  return
384
+
385
+ data_stats=data # useremo mediane per default
386
+
387
+ with st.form(key="inference_form"):
388
+ st.subheader("Inserisci Caratteristiche EoL")
389
+ c1,c2,c3=st.columns(3)
390
+ with c1:
391
+ length=st.number_input("Lunghezza (mm)",0.0,300.0,float(data_stats['length'].median()),step=1.0)
392
+ width=st.number_input("Larghezza (mm)",0.0,200.0,float(data_stats['width'].median()),step=1.0)
393
+ shape_name=st.selectbox("Forma", list(SHAPE_MAPPING.keys()))
394
+ with c2:
395
+ weight=st.number_input("Peso (kg)",0.0,300.0,float(data_stats['weight'].median()),step=1.0)
396
+ thickness=st.number_input("Spessore (mm)",0.0,50.0,float(data_stats['thickness'].median()),step=0.5)
397
+ RUL=st.number_input("RUL (0-1000)",0,1000,int(data_stats['RUL'].median()))
398
+ with c3:
399
+ val_merc=st.number_input("Valore Mercato ()",0.0,1e5, float(data_stats['margin'].median()+200),step=10.0)
400
+ costo_rip=st.number_input("Costo Riparazione (€)",0.0,1e5,50.0,step=10.0)
401
+
402
+ submitted=st.form_submit_button("Predizione Step 1")
403
+
404
+ if submitted:
405
+ margin= val_merc - costo_rip
406
+ shape_code= SHAPE_MAPPING.get(shape_name, -1)
407
+
408
+ # Prepariamo input per dimension_match
409
+ input_dict={
410
+ "length": length,
411
+ "width": width,
412
+ "shape": shape_name,
413
+ "weight": weight,
414
+ "thickness": thickness,
415
+ "RUL": RUL,
416
+ "margin": margin
417
  }
418
+ temp_df=pd.DataFrame([input_dict])
419
 
420
+ # Calcola compat_dim
421
+ tparams=st.session_state.target_params
422
+ temp_df['compat_dim']=temp_df.apply(lambda r: dimension_match(r,**tparams), axis=1)
423
 
424
+ # Rimuovi shape testo e aggiungi shape_code
425
+ temp_df['shape_code']=shape_code
426
+ temp_df_ml= temp_df.drop(columns=['shape'])
 
427
 
428
+ # Assicuriamoci di avere ML_FEATURES_STEP1
429
  try:
430
+ X_inference=temp_df_ml[ML_FEATURES_STEP1]
431
  except KeyError as e:
432
+ st.error(f"Mancano colonne per ML: {e}")
433
+ return
434
+
435
+ # Predici con tutti i modelli
436
+ model_predictions=[]
437
+ details=[]
438
+ for name,pipe in trained_pipelines.items():
439
+ try:
440
+ pred_num=pipe.predict(X_inference)[0]
441
+ proba=pipe.predict_proba(X_inference)[0]
442
+ details.append({
443
+ "Modello": name,
444
+ "Pred (0=Riuso,1=Upcycling)":pred_num,
445
+ "Prob. Riuso": proba[0],
446
+ "Prob. Upcycling": proba[1]
447
+ })
448
+ model_predictions.append(pred_num)
449
+ except Exception as e:
450
+ st.error(f"Errore predizione {name}: {e}")
 
 
451
 
 
452
  if not model_predictions:
453
+ st.error("Nessun modello ha prodotto predizioni.")
454
  return
455
 
456
+ # Aggrega con mode
457
  try:
458
+ final_pred=mode(model_predictions)
 
459
  except StatisticsError:
460
+ # Se c'è pareggio, media prob upcycling
461
+ avg_prob_upc=np.mean([d["Prob. Upcycling"] for d in details])
462
+ final_pred=1 if avg_prob_upc>=0.5 else 0
463
+
464
+ final_label="Riutilizzo Funzionale" if final_pred==0 else "Upcycling Creativo"
465
+ st.subheader("Risultato Aggregato (Step 1)")
466
+ st.metric("Classe Predetta:", final_label)
467
+
468
+ with st.expander("Dettagli Singoli Modelli"):
469
+ df_details=pd.DataFrame(details)
470
+ df_details["Prob. Riuso"]=df_details["Prob. Riuso"].apply(lambda x:f"{x:.1%}")
471
+ df_details["Prob. Upcycling"]=df_details["Prob. Upcycling"].apply(lambda x:f"{x:.1%}")
472
+ st.dataframe(df_details)
473
+
474
+ # Se risulta "Upcycling Creativo", allora Step 2 (VAE)
475
+ if final_label=="Upcycling Creativo":
 
 
476
  st.markdown("---")
477
+ st.subheader("Upcycling Creativo Esplorazione Generativa (VAE)")
 
478
 
479
+ if not st.session_state.get("vae_trained_on_eol",False):
480
+ st.error("VAE non addestrato. Vai alla fase '🧬 Training VAE (Step 2)' prima.")
 
481
  else:
482
+ vae_model=st.session_state.get("vae", None)
483
+ vae_scaler=st.session_state.get("vae_scaler", None)
484
  if vae_model is None or vae_scaler is None:
485
+ st.error("Errore: VAE o scaler non disponibile in session.")
486
  else:
487
+ n_ideas=st.number_input("Quante idee generare con VAE?",1,10,3)
488
+ if st.button("Genera Idee Upcycling"):
489
+ vae_model.eval()
490
+ with torch.no_grad():
491
+ z_dim=vae_model.fc21.out_features
492
+ z=torch.randn(n_ideas,z_dim)
493
+ recon=vae_model.decode(z)
494
+ arr=recon.numpy()
495
+ # Proviamo a invertire lo scaler
496
+ try:
497
+ arr_inv=vae_scaler.inverse_transform(arr)
498
+ feat_names=vae_scaler.feature_names_in_
499
+ df_gen=pd.DataFrame(arr_inv, columns=feat_names)
500
+
501
+ # Se c'è shape_code, arrotondiamolo
502
+ if 'shape_code' in df_gen.columns:
503
+ df_gen['shape_code']=df_gen['shape_code'].round().astype(int)
504
+ inv_shape_map={v:k for k,v in SHAPE_MAPPING.items()}
505
+ df_gen['shape']=df_gen['shape_code'].map(inv_shape_map).fillna('unknown')
506
+ st.write("**Idee Generative** (dimensioni, spessore, shape..)")
507
+ st.dataframe(df_gen.round(2))
508
+ except Exception as e:
509
+ st.error(f"Errore decoding VAE: {e}")
510
+ else:
511
+ st.success("Compatibilità => Riutilizzo Funzionale. Nessun passaggio generativo necessario.")
512
+
513
+ #######################################
514
+ # Fase 4: Training VAE
515
+ #######################################
 
 
 
 
 
 
516
  def vae_training_phase():
517
+ st.header("🧬 Training VAE - Step 2")
 
518
 
519
  if 'data' not in st.session_state or st.session_state['data'] is None:
520
+ st.error("Non c'è un dataset elaborato. Torna in Fase 1.")
521
  return
522
 
523
+ data=st.session_state['data']
524
+ # Verifica se abbiamo le feature per il VAE
525
+ feats= [f for f in VAE_FEATURES_STEP2 if f in data.columns]
526
+ if not feats:
527
+ st.error(f"Il dataset non contiene feature minime per VAE: {VAE_FEATURES_STEP2}")
528
  return
529
+ st.write(f"**Il VAE userà le feature**: {feats}")
530
+
531
+ # Parametri
532
+ lat_dim=st.slider("Dimensione latente VAE",2,10,3)
533
+ ep=st.number_input("Epochs",10,300,50)
534
+ lr=st.number_input("Learning Rate",1e-5,1e-2,1e-3,format="%e")
535
+ bs=st.selectbox("Batch Size",[16,32,64,128],index=1)
536
 
537
+ if not st.session_state.get("vae_trained_on_eol",False):
538
+ st.warning("VAE non addestrato su EoL. Clicca il bottone per avviare.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  if st.button("Avvia Training VAE"):
540
+ st.session_state["vae"] = MiniVAE(input_dim=len(feats), latent_dim=lat_dim)
541
+ vae=st.session_state["vae"]
542
+ # Sostituisci eventuali NaN
543
+ X_vae=data[feats].copy()
544
+ for c in X_vae.columns:
545
+ if X_vae[c].isnull().any():
546
+ X_vae[c].fillna(X_vae[c].median(), inplace=True)
547
+
548
+ # Scalatura
549
+ from sklearn.preprocessing import StandardScaler
550
+ scaler=StandardScaler()
551
+ X_scaled=scaler.fit_transform(X_vae)
552
+ st.session_state["vae_scaler"]=scaler
553
+
554
+ dataset=torch.utils.data.TensorDataset(torch.tensor(X_scaled,dtype=torch.float32))
555
+ loader=torch.utils.data.DataLoader(dataset,batch_size=bs,shuffle=True)
556
+ optimizer=torch.optim.Adam(vae.parameters(),lr=lr)
557
+
558
+ losses=[]
559
+ vae.train()
560
+ for epoch in range(int(ep)):
561
+ epoch_loss=0.0
562
+ for (batch,) in loader:
563
+ optimizer.zero_grad()
564
+ recon,mu,logvar=vae(batch)
565
+ loss=vae_loss(recon,batch,mu,logvar)
566
+ loss.backward()
567
+ optimizer.step()
568
+ epoch_loss+=loss.item()
569
+ avg_l=epoch_loss/len(dataset)
570
+ losses.append(avg_l)
571
+ st.progress((epoch+1)/ep)
572
+ st.session_state["vae_trained_on_eol"]=True
573
+ st.success(f"Training VAE completato. Loss finale ~ {avg_l:.2f}")
574
+ st.line_chart(losses)
 
 
 
 
 
 
 
575
  else:
576
+ st.success("VAE risulta già addestrato su EoL. Se vuoi rifarlo, premi 'Riallena' qui sotto.")
577
  if st.button("Riallena VAE"):
578
+ st.session_state["vae_trained_on_eol"]=False
579
+ st.rerun()
580
 
581
+ #######################################
582
+ # Fase 5: Dashboard
583
+ #######################################
584
  def show_dashboard():
585
+ st.header("📊 Dashboard")
586
  if 'data' not in st.session_state or st.session_state['data'] is None:
587
+ st.error("No dataset. Torna alla Fase 1.")
588
  return
589
+ data=st.session_state['data']
590
+ st.subheader("Distribuzione Classi EoL")
591
+ st.write(data['Target'].value_counts())
592
+
593
+ if 'train_results' in st.session_state and st.session_state['train_results'] is not None:
594
+ st.subheader("Risultati Modelli ML")
595
+ df_res=st.session_state['train_results']
596
+ st.dataframe(df_res)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  else:
598
+ st.info("Modelli ML non addestrati o nessun risultato salvato.")
599
 
600
+ st.subheader("Stato VAE")
601
+ if st.session_state.get("vae_trained_on_eol",False):
602
+ st.success("VAE Addestrato")
603
+ else:
604
+ st.warning("VAE non addestrato o parametri cambiati.")
605
 
606
+ #######################################
607
+ # Fase 6: Guida
608
+ #######################################
609
  def show_help():
610
+ st.header("ℹ️ Guida")
611
  st.markdown("""
612
+ **Flusso a due fasi**:
613
+
614
+ 1. **Fase 1: Dataset**
615
+ - Genera o carica dati su componenti EoL (dimensioni, RUL, margin, shape...)
616
+ - Definisci parametri di compatibilità dimensionale e calcolo score RUL+margin. Il sistema assegna 'Riutilizzo Funzionale' o 'Upcycling Creativo'.
617
+ 2. **Fase 2: Addestramento ML**
618
+ - Addestra modelli (RF, GB, SVM...) per predire la stessa classe su dati nuovi.
619
+ 3. **Fase 3: Inferenza**
620
+ - Inserisci un nuovo pezzo EoL. Il sistema predice se 'Riutilizzo Funzionale' o 'Upcycling Creativo'.
621
+ - Se 'Upcycling Creativo', appare la possibilità di generare idee con VAE (Fase successiva).
622
+ 4. **Fase 4: Training VAE**
623
+ - Allena un VAE sulle feature geometriche per generare configurazioni fittizie (design space).
624
+ 5. **Fase 3 (continuazione)**
625
+ - Se la previsione era 'Upcycling Creativo' e il VAE è addestrato, si possono generare soluzioni creative.
626
+ 6. **Fase 5: Dashboard**
627
+ - Visualizza statistiche, performance modelli, ecc.
 
 
 
 
 
 
 
 
 
 
 
628
  """)
629
 
630
+ #######################################
631
+ # Funzione reset
632
+ #######################################
633
  def reset_app():
634
+ keys=[
635
+ 'data','models','train_results','vae','vae_trained_on_eol',
636
+ 'vae_scaler','target_params','score_params','data_source'
637
+ ]
638
+ for k in keys:
639
+ if k in st.session_state:
640
+ del st.session_state[k]
641
+ st.success("Reset completato.")
642
+ st.experimental_rerun()
643
+
644
+ #######################################
645
+ # MAIN
646
+ #######################################
647
  def main():
 
648
  st.sidebar.title("Menu Principale")
649
+ step=st.sidebar.radio("Fasi", [
650
+ "♻️ Dataset",
651
+ "🤖 Addestramento ML (Step 1)",
652
+ "🔮 Inferenza (Step 1 & 2)",
653
+ "🧬 Training VAE (Step 2)",
654
+ "📊 Dashboard",
655
+ "ℹ️ Guida"
656
+ ])
657
+ st.sidebar.button("Reset App", on_click=reset_app)
658
+
659
+ if step=="♻️ Dataset":
660
+ prepare_dataset()
661
+ elif step=="🤖 Addestramento ML (Step 1)":
662
+ train_models(st.session_state.get('data',None))
663
+ elif step=="🔮 Inferenza (Step 1 & 2)":
664
+ if 'models' not in st.session_state or st.session_state['models'] is None:
665
+ st.error("Non hai ancora addestrato i modelli ML (Fase 2).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  else:
667
+ model_inference(st.session_state['models'], st.session_state.get('data',None))
668
+ elif step=="🧬 Training VAE (Step 2)":
 
 
669
  vae_training_phase()
670
+ elif step=="📊 Dashboard":
671
  show_dashboard()
672
+ elif step=="ℹ️ Guida":
673
  show_help()
674
 
675
+ if __name__=="__main__":
676
+ main()