valegro commited on
Commit
474ac94
·
verified ·
1 Parent(s): 3b603bd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +776 -322
app.py CHANGED
@@ -1,85 +1,98 @@
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
4
- from PIL import Image
5
- import os
 
 
6
 
7
- # Scikit-learn
8
- from sklearn.model_selection import train_test_split
9
- from sklearn.preprocessing import LabelEncoder, StandardScaler
10
- from sklearn.metrics import accuracy_score, confusion_matrix
11
  from sklearn.pipeline import Pipeline
 
 
12
  from sklearn.linear_model import LogisticRegression
13
- from sklearn.ensemble import RandomForestClassifier
14
- import matplotlib.pyplot as plt
15
- import seaborn as sns
16
 
17
- # PyTorch
18
  import torch
19
  import torch.nn as nn
20
  import torch.nn.functional as F
 
21
  import random
22
 
23
- ###########################################
24
- # 1) Libreria Destinazioni (multi-class)
25
- ###########################################
26
- TARGET_LABELS = ["Armadio", "Dock", "Vetrina", "Tool"]
27
-
28
- def pick_random_target(row):
29
- """
30
- Logica fittizia: in base ad area e shape_code assegna una destinazione
31
- """
32
- # Semplifichiamo: se shape_code = 0 => preferiamo "Armadio"
33
- # shape_code = 1 => "Dock"
34
- # shape_code = 2 => "Vetrina"
35
- # shape_code = 3 => "Tool"
36
- # ma aggiungiamo un po' di random
37
- code = int(row["shape_code"])
38
- return TARGET_LABELS[code]
39
-
40
- ###########################################
41
- # 2) Genera dataset sintetico
42
- ###########################################
43
- def generate_synthetic_data_mc(n=300, seed=42):
44
- """
45
- Genera un dataset multi-classe:
46
- - volume, area, lunghezza, spessore, shape_code (0..3), usura
47
- - label = "Armadio"/"Dock"/"Vetrina"/"Tool" (fittizi)
48
- """
49
- np.random.seed(seed)
50
- random.seed(seed)
51
-
52
- volume = np.clip(np.random.normal(50,15,n), 10, 200)
53
- area = np.clip(np.random.normal(20,8,n), 5, 200)
54
- lung = np.clip(np.random.normal(100,30,n), 20, 300)
55
- spess = np.clip(np.random.normal(5,1,n), 0.5, 10)
56
- usura = np.random.rand(n) # 0..1
57
- shape_code = np.random.randint(0,4,n) # 4 possibili "forme"
58
-
59
- df = pd.DataFrame({
60
- "volume": volume,
61
- "area": area,
62
- "lunghezza": lung,
63
- "spessore": spess,
64
- "usura": usura,
65
- "shape_code": shape_code
66
- })
67
-
68
- # label => in base a shape_code
69
- df["target"] = df.apply(pick_random_target, axis=1)
70
- return df
71
-
72
- ###########################################
73
- # 3) Modello PyTorch VAE per generative reuse
74
- ###########################################
 
 
 
 
 
 
 
75
  class MiniVAE(nn.Module):
76
- def __init__(self, input_dim=4, latent_dim=2):
 
77
  super().__init__()
78
- self.fc1 = nn.Linear(input_dim, 16)
79
- self.fc21 = nn.Linear(16, latent_dim)
80
- self.fc22 = nn.Linear(16, latent_dim)
81
- self.fc3 = nn.Linear(latent_dim, 16)
82
- self.fc4 = nn.Linear(16, input_dim)
 
 
83
 
84
  def encode(self, x):
85
  h = F.relu(self.fc1(x))
@@ -88,7 +101,7 @@ class MiniVAE(nn.Module):
88
  def reparameterize(self, mu, logvar):
89
  std = torch.exp(0.5 * logvar)
90
  eps = torch.randn_like(std)
91
- return mu + eps*std
92
 
93
  def decode(self, z):
94
  h = F.relu(self.fc3(z))
@@ -100,269 +113,710 @@ class MiniVAE(nn.Module):
100
  recon = self.decode(z)
101
  return recon, mu, logvar
102
 
 
103
  def vae_loss(recon_x, x, mu, logvar):
104
- mse = F.mse_loss(recon_x, x, reduction='sum')
105
  kld = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
106
- return mse + kld
107
-
108
-
109
- ###########################################
110
- # STREAMLIT SETUP
111
- ###########################################
112
- st.set_page_config(page_title="WEEKO Full AI App", layout="wide")
113
- st.title("WEEKO Full AI Demo: multi-class + generative + overlay")
114
-
115
- if "df" not in st.session_state:
116
- st.session_state["df"] = None
117
- if "model" not in st.session_state:
118
- st.session_state["model"] = None
119
- if "vae" not in st.session_state:
120
- st.session_state["vae"] = None
121
- if "vae_trained" not in st.session_state:
122
- st.session_state["vae_trained"] = False
123
- if "label_enc" not in st.session_state:
124
- st.session_state["label_enc"] = None
125
-
126
-
127
- ###########################################
128
- # SIDEBAR
129
- ###########################################
130
- menu = st.sidebar.radio("Fasi", ["Dataset","Training","Inferenza","Generative Reuse (VAE)","Overlay Estetico","Dashboard"])
131
-
132
- ###########################################
133
- # FUNZIONE: Caricamento/generazione dataset
134
- ###########################################
135
- def dataset_phase():
136
- st.subheader("Fase 1: Dataset")
137
-
138
- data_option = st.radio("Scegli la fonte del dataset", ["Genera Sintetico","Carica CSV"], horizontal=True)
139
- if data_option == "Genera Sintetico":
140
- n = st.slider("Numero di campioni", 50,1000,300,step=50)
141
- if st.button("Genera"):
142
- df = generate_synthetic_data_mc(n=n)
143
- st.session_state["df"] = df
144
- st.success("Dataset sintetico generato!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  else:
146
- file = st.file_uploader("Carica CSV con colonne: volume, area, lunghezza, spessore, usura, shape_code, target", type=["csv"])
147
- if file:
148
- df = pd.read_csv(file)
149
- st.session_state["df"] = df
150
- st.success("Dataset caricato!")
151
-
152
- if st.session_state["df"] is not None:
153
- st.write("Anteprima dataset:")
154
- st.dataframe(st.session_state["df"].head(10))
155
- st.write("Distribuzione target:", st.session_state["df"]["target"].value_counts())
156
- csv = st.session_state["df"].to_csv(index=False)
157
- st.download_button("Scarica Dataset", csv, "dataset.csv","text/csv")
158
-
159
- ###########################################
160
- # FUNZIONE: Training multi-class (es. RandomForest)
161
- ###########################################
162
- def training_phase():
163
- st.subheader("Fase 2: Training Modello Multi-Classe")
164
- df = st.session_state["df"]
165
- if df is None:
166
- st.error("Prima genera o carica il dataset!")
167
- return
168
-
169
- st.write("Esempio: usiamo un RandomForest + pipeline StandardScaler.")
170
- from sklearn.pipeline import Pipeline
171
- from sklearn.ensemble import RandomForestClassifier
172
-
173
- # Prepara X,y
174
- # NB: shape_code e -> numerico, target -> labelEnc
175
- if "target" not in df.columns:
176
- st.error("Il dataset deve avere colonna 'target'!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  return
178
-
179
- # Estrai X e y
180
- # Minimal check: [volume, area, lunghezza, spessore, usura, shape_code]
181
- needed_cols = ["volume","area","lunghezza","spessore","usura","shape_code","target"]
182
- for c in needed_cols:
183
- if c not in df.columns:
184
- st.error(f"Colonna '{c}' assente nel dataset!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  return
186
 
187
- X = df[["volume","area","lunghezza","spessore","usura","shape_code"]]
188
- y = df["target"].astype(str)
189
-
190
- # label encode
191
- from sklearn.preprocessing import LabelEncoder
192
- le = LabelEncoder()
193
- y_enc = le.fit_transform(y)
194
-
195
- st.session_state["label_enc"] = le
196
-
197
- # train test split
198
- from sklearn.model_selection import train_test_split
199
- X_train, X_test, y_train, y_test = train_test_split(X, y_enc, test_size=0.2, random_state=42)
200
-
201
- # pipeline
202
- pipe = Pipeline([
203
- ("scaler", StandardScaler()),
204
- ("clf", RandomForestClassifier(n_estimators=100, random_state=42))
205
- ])
206
-
207
- pipe.fit(X_train, y_train)
208
- pred = pipe.predict(X_test)
209
-
210
- acc = accuracy_score(y_test, pred)
211
- st.write(f"Accuracy su test: {acc:.3f}")
212
-
213
- cm = confusion_matrix(y_test, pred)
214
- fig, ax = plt.subplots()
215
- sns.heatmap(cm, annot=True, fmt='d', ax=ax)
216
- st.pyplot(fig)
217
-
218
- st.session_state["model"] = pipe
219
- st.success("Modello addestrato e salvato in session_state['model'].")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
- ###########################################
222
- # FUNZIONE: Inference multi-class
223
- ###########################################
224
- def inference_phase():
225
- st.subheader("Fase 3: Inferenza su un nuovo EoL")
226
- if st.session_state["model"] is None:
227
- st.error("Devi prima addestrare il modello!")
228
  return
229
- model = st.session_state["model"]
230
- le = st.session_state["label_enc"]
231
-
232
- # Inserimento EoL
233
- col1, col2 = st.columns(2)
234
- with col1:
235
- vol = st.number_input("Volume (cm³)",0.0,1000.0,50.0,step=1.0)
236
- area= st.number_input("Area (cm²)",0.0,1000.0,30.0,step=1.0)
237
- lung= st.number_input("Lunghezza (mm)",0.0,2000.0,120.0,step=1.0)
238
- with col2:
239
- spess= st.number_input("Spessore (mm)",0.0,20.0,5.0,step=0.5)
240
- usura= st.slider("Usura (0=nuovo,1=usuratissimo)",0.0,1.0,0.3)
241
- shape_code = st.selectbox("Shape Code", [0,1,2,3], index=0)
242
-
243
- if st.button("Calcola Compatibilità"):
244
- x_np = np.array([[vol, area, lung, spess, usura, shape_code]], dtype=float)
245
- proba = model.predict_proba(x_np)[0]
246
- classes = le.inverse_transform(range(len(proba)))
247
- # Ordina disc
248
- sorted_idx = np.argsort(-proba)
249
- st.write("**Compatibilità con i possibili target** (ordine decrescente):")
250
- for i in sorted_idx:
251
- st.write(f"- {classes[i]}: {proba[i]*100:.1f}%")
252
- # Top1
253
- top1 = classes[sorted_idx[0]]
254
- st.success(f"**Consiglio**: {top1}")
255
-
256
- ###########################################
257
- # FUNZIONE: Mini VAE generative reuse
258
- ###########################################
259
- def generative_phase():
260
- st.subheader("Fase 4: Generative Reuse (VAE)")
261
-
262
- # Creiamo / inizializziamo
263
- if st.session_state["vae"] is None:
264
- st.session_state["vae"] = MiniVAE()
265
  vae = st.session_state["vae"]
266
 
267
- # Se non allenato, lo alleniamo su dati fittizi
268
- if not st.session_state["vae_trained"]:
269
- if st.button("Allena VAE su dataset fittizio di 4 feature"):
270
- # Generiamo un dataset: [dim1, dim2, spess, dec]
271
- rng = np.random.default_rng(123)
272
- n = 500
273
- dim1 = rng.normal(50,10,n)
274
- dim2 = rng.normal(20,5,n)
275
- spess = rng.normal(5,1,n)
276
- dec = rng.uniform(0,1,n)
277
- arr = np.column_stack([dim1, dim2, spess, dec])
278
- X_t = torch.tensor(arr,dtype=torch.float32)
279
-
280
- optimizer = torch.optim.Adam(vae.parameters(), lr=1e-3)
281
- epochs=20
282
- batch_size=32
283
- dataset = torch.utils.data.TensorDataset(X_t)
284
- loader = torch.utils.data.DataLoader(dataset,batch_size=batch_size,shuffle=True)
285
-
286
- for ep in range(epochs):
287
- total_loss=0
288
- for (batch,) in loader:
289
- optimizer.zero_grad()
290
- recon, mu, logvar = vae(batch)
291
- loss=vae_loss(recon, batch, mu, logvar)
292
- loss.backward()
293
- optimizer.step()
294
- total_loss += loss.item()
295
- st.session_state["vae_trained"] = True
296
- st.success("VAE addestrato!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  else:
298
- st.info("VAE già addestrato.")
299
-
300
- # Genera sample
301
- if st.session_state["vae_trained"]:
302
- if st.button("Genera 5 soluzioni"):
303
- with torch.no_grad():
304
- z = torch.randn(5,2)
305
- recon = vae.decode(z)
306
- arr = recon.numpy()
307
- df = pd.DataFrame(arr, columns=["Dim1","Dim2","Spess","Decor"])
308
- st.dataframe(df.round(2))
309
- st.write("Interpretazione: Dim1, Dim2 = dimensioni, Spess=spessore, Decor=0..1 stima di ‘finitura artistica’?")
310
-
311
- ###########################################
312
- # FUNZIONE: Overlay Estetico
313
- ###########################################
314
- def overlay_phase():
315
- st.subheader("Fase 5: Overlay Estetico")
316
-
317
- col1, col2 = st.columns(2)
318
- with col1:
319
- file_eol = st.file_uploader("Foto EoL", type=["jpg","png"], key="uplEolOverlay")
320
- with col2:
321
- file_obj = st.file_uploader("Foto Oggetto Finale", type=["jpg","png"], key="uplObjOverlay")
322
-
323
- alpha = st.slider("Trasparenza EoL", 0.0,1.0,0.5)
324
-
325
- if file_eol and file_obj:
326
- eol_img = Image.open(file_eol).convert("RGBA")
327
- obj_img = Image.open(file_obj).convert("RGBA")
328
- eol_img = eol_img.resize(obj_img.size)
329
- blended = Image.blend(obj_img, eol_img, alpha)
330
- st.image(blended, caption="Overlay EoL + Oggetto Finale", use_column_width=True)
331
- else:
332
- st.info("Carica entrambe le immagini per vedere l'overlay")
333
-
334
- ###########################################
335
- # FUNZIONE: Dashboard
336
- ###########################################
337
- def dashboard_phase():
338
- st.subheader("Dashboard")
339
- if st.session_state["df"] is None:
340
- st.error("Nessun dataset caricato/generato.")
341
  return
342
- df = st.session_state["df"]
343
- st.write("**Info dataset**")
344
- st.write(df.describe())
345
-
346
- if "model" in st.session_state and st.session_state["model"] is not None:
347
- st.write("**Modello**: RandomForest + pipeline (scaler). Se addestrato con logistic regression, stesso pipeline logic.")
348
- # Non stai salvando metriche, potresti salvare accuracy e confusion matrix in session state
349
- st.info("Per metrics dettagliate, vedi la fase Training.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  else:
351
- st.warning("Modello non addestrato. Niente metriche da mostrare.")
352
-
353
-
354
- ###########################################
355
- # MAIN flow
356
- ###########################################
357
- if menu=="Dataset":
358
- dataset_phase()
359
- elif menu=="Training":
360
- training_phase()
361
- elif menu=="Inferenza":
362
- inference_phase()
363
- elif menu=="Generative Reuse (VAE)":
364
- generative_phase()
365
- elif menu=="Overlay Estetico":
366
- overlay_phase()
367
- elif menu=="Dashboard":
368
- dashboard_phase()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import pandas as pd
3
  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
21
  import torch.nn as nn
22
  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="♻️",
30
+ layout="wide"
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),
76
+ "Logistic Regression": LogisticRegression(random_state=42, max_iter=500, class_weight='balanced'),
77
+ "Support Vector Machine": SVC(probability=True, random_state=42, class_weight='balanced'),
78
+ "TabTransformer (Dummy)": DummyTabTransformerClassifier(),
79
+ "SAINT (Dummy)": DummySAINTClassifier()
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
 
97
  def encode(self, x):
98
  h = F.relu(self.fc1(x))
 
101
  def reparameterize(self, mu, logvar):
102
  std = torch.exp(0.5 * logvar)
103
  eps = torch.randn_like(std)
104
+ return mu + eps * std
105
 
106
  def decode(self, z):
107
  h = F.relu(self.fc3(z))
 
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()