OmarOmar91 commited on
Commit
facbc6a
·
verified ·
1 Parent(s): 76adb8c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +246 -254
app.py CHANGED
@@ -1,314 +1,306 @@
1
- ```python
2
- # Gradio UI aligned to the training script column names (October1.xlsx)
3
- # - Uses the trained pipeline saved as: stress_gf_xgb.joblib
4
- # - Makes many inputs optional; missing values are handled by the pipeline imputers
5
-
6
  import os
7
  import joblib
8
  import numpy as np
9
  import pandas as pd
10
  import gradio as gr
11
 
12
- # ========================= Column Names (match training script) =========================
13
 
14
  CF_COL = "Conductive Filler Conc. (wt%)"
15
  TARGET_COL = "Stress GF (MPa-1)"
16
 
17
  MAIN_VARIABLES = [
18
- "Filler 1 Type",
19
- "Filler 1 Diameter (µm)",
20
- "Filler 1 Length (mm)",
21
- CF_COL,
22
- "Filler 1 Dimensionality",
23
- "Filler 2 Type",
24
- "Filler 2 Diameter (µm)",
25
- "Filler 2 Length (mm)",
26
- "Filler 2 Dimensionality",
27
- "Specimen Volume (mm3)",
28
- "Probe Count",
29
- "Probe Material",
30
- "W/B",
31
- "S/B",
32
- "Gauge Length (mm)",
33
- "Curing Condition",
34
- "Number of Fillers",
35
- "Drying Temperature (°C)",
36
- "Drying Duration (hr)",
37
- "Loading Rate (MPa/s)",
38
- "Modulus of Elasticity (GPa)",
39
- "Current Type",
40
- "Applied Voltage (V)"
41
  ]
42
 
43
  NUMERIC_COLS = {
44
- "Filler 1 Diameter (µm)",
45
- "Filler 1 Length (mm)",
46
- CF_COL,
47
- "Filler 2 Diameter (µm)",
48
- "Filler 2 Length (mm)",
49
- "Specimen Volume (mm3)",
50
- "Probe Count",
51
- "W/B",
52
- "S/B",
53
- "Gauge Length (mm)",
54
- "Number of Fillers",
55
- "Drying Temperature (°C)",
56
- "Drying Duration (hr)",
57
- "Loading Rate (MPa/s)",
58
- "Modulus of Elasticity (GPa)",
59
- "Applied Voltage (V)"
60
  }
61
 
62
  CATEGORICAL_COLS = {
63
- "Filler 1 Type",
64
- "Filler 1 Dimensionality",
65
- "Filler 2 Type",
66
- "Filler 2 Dimensionality",
67
- "Probe Material",
68
- "Curing Condition",
69
- "Current Type"
70
  }
71
 
72
- # Reasonable UI choices (free text is still allowed)
 
73
  DIM_CHOICES = ["0D", "1D", "2D", "3D", "NA"]
74
  CURRENT_CHOICES = ["DC", "AC", "NA"]
75
 
76
- # ========================= Model Loader ================================================
77
 
78
  MODEL_CANDIDATES = [
79
- "stress_gf_xgb.joblib",
80
- "models/stress_gf_xgb.joblib",
81
- "/home/user/app/stress_gf_xgb.joblib",
82
  ]
83
 
84
  def _load_model_or_error():
85
- for p in MODEL_CANDIDATES:
86
- if os.path.exists(p):
87
- try:
88
- return joblib.load(p)
89
- except Exception as e:
90
- return f"Could not load model from {p}: {e}"
91
- return (
92
- "Model file not found. Upload your trained pipeline as "
93
- "stress_gf_xgb.joblib (or put it in models/)."
94
- )
95
-
96
- # ========================= Input Coercion =============================================
97
 
98
  def _coerce_to_row(form_dict: dict) -> pd.DataFrame:
99
- """
100
- Convert raw UI dict -> single-row DataFrame with columns MAIN_VARIABLES.
101
- Numeric fields: float or NaN; categorical: stripped string (or empty).
102
- Missing columns are filled with NaN/'' so the pipeline imputers can handle them.
103
- """
104
- row = {}
105
- for col in MAIN_VARIABLES:
106
- v = form_dict.get(col, None)
107
- if col in NUMERIC_COLS:
108
- if v in ("", None):
109
- row[col] = np.nan
110
- else:
111
- try:
112
- row[col] = float(v)
113
- except:
114
- row[col] = np.nan
115
- else:
116
- row[col] = "" if v in (None, "NA") else str(v).strip()
117
- # Ensure exact column order
118
- return pd.DataFrame([row], columns=MAIN_VARIABLES)
119
-
120
- # ========================= Predict Function ===========================================
121
 
122
  def predict_fn(**kwargs):
123
- mdl = _load_model_or_error()
124
- if isinstance(mdl, str):
125
- # return a friendly error string
126
- return mdl
127
 
128
- X_new = _coerce_to_row(kwargs)
129
 
130
- try:
131
- y_log = mdl.predict(X_new) # model predicts log1p(target)
132
- y = float(np.expm1(y_log)[0]) # back to original scale MPa^-1
133
- # protect from tiny negative numeric noise
134
- if -1e-10 < y < 0:
135
- y = 0.0
136
- return y
137
- except Exception as e:
138
- return f"Prediction error: {e}"
139
 
140
- # ========================= Example Prefill ============================================
141
 
142
  EXAMPLE = {
143
- "Filler 1 Type": "CNT",
144
- "Filler 1 Dimensionality": "1D",
145
- "Filler 1 Diameter (µm)": 0.02,
146
- "Filler 1 Length (mm)": 1.2,
147
- CF_COL: 0.5,
148
- "Filler 2 Type": "",
149
- "Filler 2 Dimensionality": "NA",
150
- "Filler 2 Diameter (µm)": None,
151
- "Filler 2 Length (mm)": None,
152
- "Specimen Volume (mm3)": 1000,
153
- "Probe Count": 2,
154
- "Probe Material": "Copper",
155
- "W/B": 0.4,
156
- "S/B": 2.5,
157
- "Gauge Length (mm)": 20,
158
- "Curing Condition": "28d water, 20°C",
159
- "Number of Fillers": 1,
160
- "Drying Temperature (°C)": 60,
161
- "Drying Duration (hr)": 24,
162
- "Loading Rate (MPa/s)": 0.1,
163
- "Modulus of Elasticity (GPa)": 25,
164
- "Current Type": "DC",
165
- "Applied Voltage (V)": 5.0,
166
  }
167
 
168
  def _fill_example():
169
- return [EXAMPLE.get(k, None) for k in MAIN_VARIABLES]
170
 
171
  def _clear_all():
172
- # Return blanks in the same order as MAIN_VARIABLES
173
- cleared = []
174
- for col in MAIN_VARIABLES:
175
- if col in NUMERIC_COLS:
176
- cleared.append(None)
177
- elif col in {"Filler 1 Dimensionality", "Filler 2 Dimensionality"}:
178
- cleared.append("NA")
179
- elif col == "Current Type":
180
- cleared.append("NA")
181
- else:
182
- cleared.append("")
183
- return cleared
184
-
185
- # ========================= UI =========================================================
186
 
187
  CSS = """
188
  /* blue→green background (replaces grayscale) */
189
  .gradio-container {
190
- background:
191
- radial-gradient(1200px 600px at 10% -10%, rgba(14,165,233,0.30), transparent 60%),
192
- radial-gradient(1000px 500px at 110% 110%, rgba(16,185,129,0.28), transparent 55%),
193
- linear-gradient(135deg,#0b1220 0%, #06231b 100%);
194
  }
195
 
196
  /* keep your font stack */
197
- * { font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; }
 
198
 
199
  /* cards */
200
  .card { background: rgba(255,255,255,0.05) !important; border: 1px solid rgba(255,255,255,0.10); }
201
 
202
  /* labels */
203
- label.svelte-1ipelgc { color: #dbeafe !important; } /* light blue */
204
  """
205
 
206
  theme = gr.themes.Soft(
207
- primary_hue="sky", # blue
208
- secondary_hue="emerald", # green
209
- neutral_hue="slate"
210
  ).set(
211
- body_background_fill="#0b1220",
212
- body_text_color="#e6f1f5",
213
- input_background_fill="#0f172a",
214
- input_border_color="#1f2937",
215
- button_primary_background_fill="#0284c7", # blue
216
- button_primary_text_color="#ffffff",
217
- button_secondary_background_fill="#0b2a22" # deep teal
218
  )
219
 
220
  with gr.Blocks(css=CSS, theme=theme, fill_height=True) as demo:
221
- gr.Markdown(
222
- "<h1 style='margin:0'>🔮 Stress Gauge Factor (MPa⁻¹) — ML Predictor</h1>"
223
- "<p style='opacity:.9'>Fields and units match your training data. "
224
- "Leave anything blank if unknown — the model handles missing values.</p>"
225
- )
226
-
227
- with gr.Row():
228
- # ---------------- Inputs (Left) ----------------
229
- with gr.Column(scale=7):
230
- # Primary filler
231
- with gr.Accordion("Primary conductive filler", open=True, elem_classes=["card"]):
232
- f1_type = gr.Textbox(label="Filler 1 Type", placeholder="e.g., CNT, Graphite, Steel fiber")
233
- f1_dim = gr.Dropdown(DIM_CHOICES, value="NA", label="Filler 1 Dimensionality")
234
- f1_diam = gr.Number(label="Filler 1 Diameter (µm)")
235
- f1_len = gr.Number(label="Filler 1 Length (mm)")
236
- cf_conc = gr.Number(label=f"{CF_COL}", info="Weight percent of total binder")
237
-
238
- # Secondary filler (optional)
239
- with gr.Accordion("➕ Secondary filler (optional)", open=False, elem_classes=["card"]):
240
- f2_type = gr.Textbox(label="Filler 2 Type", placeholder="Optional")
241
- f2_dim = gr.Dropdown(DIM_CHOICES, value="NA", label="Filler 2 Dimensionality")
242
- f2_diam = gr.Number(label="Filler 2 Diameter (µm)")
243
- f2_len = gr.Number(label="Filler 2 Length (mm)")
244
-
245
- # Mix & specimen
246
- with gr.Accordion("Mix design & specimen", open=False, elem_classes=["card"]):
247
- spec_vol = gr.Number(label="Specimen Volume (mm3)")
248
- probe_cnt = gr.Number(label="Probe Count")
249
- probe_mat = gr.Textbox(label="Probe Material", placeholder="e.g., Copper, Silver paste")
250
- wb = gr.Number(label="W/B")
251
- sb = gr.Number(label="S/B")
252
- gauge_len = gr.Number(label="Gauge Length (mm)")
253
- curing = gr.Textbox(label="Curing Condition", placeholder="e.g., 28d water, 20°C")
254
- n_fillers = gr.Number(label="Number of Fillers")
255
-
256
- # Processing
257
- with gr.Accordion("Processing", open=False, elem_classes=["card"]):
258
- dry_temp = gr.Number(label="Drying Temperature (°C)")
259
- dry_hrs = gr.Number(label="Drying Duration (hr)")
260
-
261
- # Mechanical & Electrical loading
262
- with gr.Accordion("Mechanical & electrical loading", open=False, elem_classes=["card"]):
263
- load_rate = gr.Number(label="Loading Rate (MPa/s)")
264
- E_mod = gr.Number(label="Modulus of Elasticity (GPa)")
265
- current = gr.Dropdown(CURRENT_CHOICES, value="NA", label="Current Type")
266
- voltage = gr.Number(label="Applied Voltage (V)")
267
-
268
- # ---------------- Output (Right) ----------------
269
- with gr.Column(scale=5):
270
- with gr.Group(elem_classes=["card"]):
271
- out_pred = gr.Number(label="Predicted Stress GF (MPa-1)", precision=6)
272
- with gr.Row():
273
- btn_pred = gr.Button("Predict", variant="primary")
274
- btn_clear = gr.Button("Clear")
275
- btn_demo = gr.Button("Fill Example")
276
-
277
- with gr.Accordion("About this model", open=False, elem_classes=["card"]):
278
- gr.Markdown(
279
- "- Pipeline: **ColumnTransformer → (RobustScaler + OneHot) → XGBoost**\n"
280
- "- Target: **Stress GF (MPa⁻¹)** on original scale (model trains on log1p).\n"
281
- "- Missing values are safely imputed per-feature.\n"
282
- "- Trained columns:\n"
283
- f" `{', '.join(MAIN_VARIABLES)}`"
284
- )
285
-
286
- # Wire buttons
287
- inputs_in_order = [
288
- # MAIN_VARIABLES exact order:
289
- # "Filler 1 Type","Filler 1 Diameter (µm)","Filler 1 Length (mm)", CF_COL,
290
- # "Filler 1 Dimensionality","Filler 2 Type","Filler 2 Diameter (µm)","Filler 2 Length (mm)",
291
- # "Filler 2 Dimensionality","Specimen Volume (mm3)","Probe Count","Probe Material",
292
- # "W/B","S/B","Gauge Length (mm)","Curing Condition","Number of Fillers",
293
- # "Drying Temperature (°C)","Drying Duration (hr)","Loading Rate (MPa/s)",
294
- # "Modulus of Elasticity (GPa)","Current Type","Applied Voltage (V)"
295
- f1_type, f1_diam, f1_len, cf_conc,
296
- f1_dim, f2_type, f2_diam, f2_len,
297
- f2_dim, spec_vol, probe_cnt, probe_mat,
298
- wb, sb, gauge_len, curing, n_fillers,
299
- dry_temp, dry_hrs, load_rate,
300
- E_mod, current, voltage
301
- ]
302
-
303
- def _predict_wrapper(*vals):
304
- data = {k: v for k, v in zip(MAIN_VARIABLES, vals)}
305
- return predict_fn(**data)
306
-
307
- btn_pred.click(_predict_wrapper, inputs=inputs_in_order, outputs=out_pred)
308
- btn_clear.click(lambda: _clear_all(), inputs=None, outputs=inputs_in_order)
309
- btn_demo.click(lambda: _fill_example(), inputs=None, outputs=inputs_in_order)
310
-
311
- # ------------- Launch -------------
312
- if __name__ == "__main__":
313
- demo.queue().launch()
314
- ```
 
 
 
 
 
 
1
  import os
2
  import joblib
3
  import numpy as np
4
  import pandas as pd
5
  import gradio as gr
6
 
7
+ ========================= Column Names (match training script) =========================
8
 
9
  CF_COL = "Conductive Filler Conc. (wt%)"
10
  TARGET_COL = "Stress GF (MPa-1)"
11
 
12
  MAIN_VARIABLES = [
13
+ "Filler 1 Type",
14
+ "Filler 1 Diameter (µm)",
15
+ "Filler 1 Length (mm)",
16
+ CF_COL,
17
+ "Filler 1 Dimensionality",
18
+ "Filler 2 Type",
19
+ "Filler 2 Diameter (µm)",
20
+ "Filler 2 Length (mm)",
21
+ "Filler 2 Dimensionality",
22
+ "Specimen Volume (mm3)",
23
+ "Probe Count",
24
+ "Probe Material",
25
+ "W/B",
26
+ "S/B",
27
+ "Gauge Length (mm)",
28
+ "Curing Condition",
29
+ "Number of Fillers",
30
+ "Drying Temperature (°C)",
31
+ "Drying Duration (hr)",
32
+ "Loading Rate (MPa/s)",
33
+ "Modulus of Elasticity (GPa)",
34
+ "Current Type",
35
+ "Applied Voltage (V)"
36
  ]
37
 
38
  NUMERIC_COLS = {
39
+ "Filler 1 Diameter (µm)",
40
+ "Filler 1 Length (mm)",
41
+ CF_COL,
42
+ "Filler 2 Diameter (µm)",
43
+ "Filler 2 Length (mm)",
44
+ "Specimen Volume (mm3)",
45
+ "Probe Count",
46
+ "W/B",
47
+ "S/B",
48
+ "Gauge Length (mm)",
49
+ "Number of Fillers",
50
+ "Drying Temperature (°C)",
51
+ "Drying Duration (hr)",
52
+ "Loading Rate (MPa/s)",
53
+ "Modulus of Elasticity (GPa)",
54
+ "Applied Voltage (V)"
55
  }
56
 
57
  CATEGORICAL_COLS = {
58
+ "Filler 1 Type",
59
+ "Filler 1 Dimensionality",
60
+ "Filler 2 Type",
61
+ "Filler 2 Dimensionality",
62
+ "Probe Material",
63
+ "Curing Condition",
64
+ "Current Type"
65
  }
66
 
67
+ Reasonable UI choices (free text is still allowed)
68
+
69
  DIM_CHOICES = ["0D", "1D", "2D", "3D", "NA"]
70
  CURRENT_CHOICES = ["DC", "AC", "NA"]
71
 
72
+ ========================= Model Loader ================================================
73
 
74
  MODEL_CANDIDATES = [
75
+ "stress_gf_xgb.joblib",
76
+ "models/stress_gf_xgb.joblib",
77
+ "/home/user/app/stress_gf_xgb.joblib",
78
  ]
79
 
80
  def _load_model_or_error():
81
+ for p in MODEL_CANDIDATES:
82
+ if os.path.exists(p):
83
+ try:
84
+ return joblib.load(p)
85
+ except Exception as e:
86
+ return f"Could not load model from {p}: {e}"
87
+ return (
88
+ "Model file not found. Upload your trained pipeline as "
89
+ "stress_gf_xgb.joblib (or put it in models/)."
90
+ )
91
+
92
+ ========================= Input Coercion =============================================
93
 
94
  def _coerce_to_row(form_dict: dict) -> pd.DataFrame:
95
+ """
96
+ Convert raw UI dict -> single-row DataFrame with columns MAIN_VARIABLES.
97
+ Numeric fields: float or NaN; categorical: stripped string (or empty).
98
+ Missing columns are filled with NaN/'' so the pipeline imputers can handle them.
99
+ """
100
+ row = {}
101
+ for col in MAIN_VARIABLES:
102
+ v = form_dict.get(col, None)
103
+ if col in NUMERIC_COLS:
104
+ if v in ("", None):
105
+ row[col] = np.nan
106
+ else:
107
+ try:
108
+ row[col] = float(v)
109
+ except:
110
+ row[col] = np.nan
111
+ else:
112
+ row[col] = "" if v in (None, "NA") else str(v).strip()
113
+ # Ensure exact column order
114
+ return pd.DataFrame([row], columns=MAIN_VARIABLES)
115
+
116
+ ========================= Predict Function ===========================================
117
 
118
  def predict_fn(**kwargs):
119
+ mdl = _load_model_or_error()
120
+ if isinstance(mdl, str):
121
+ # return a friendly error string
122
+ return mdl
123
 
124
+ X_new = _coerce_to_row(kwargs)
125
 
126
+ try:
127
+ y_log = mdl.predict(X_new) # model predicts log1p(target)
128
+ y = float(np.expm1(y_log)[0]) # back to original scale MPa^-1
129
+ # protect from tiny negative numeric noise
130
+ if -1e-10 < y < 0:
131
+ y = 0.0
132
+ return y
133
+ except Exception as e:
134
+ return f"Prediction error: {e}"
135
 
136
+ ========================= Example Prefill ============================================
137
 
138
  EXAMPLE = {
139
+ "Filler 1 Type": "CNT",
140
+ "Filler 1 Dimensionality": "1D",
141
+ "Filler 1 Diameter (µm)": 0.02,
142
+ "Filler 1 Length (mm)": 1.2,
143
+ CF_COL: 0.5,
144
+ "Filler 2 Type": "",
145
+ "Filler 2 Dimensionality": "NA",
146
+ "Filler 2 Diameter (µm)": None,
147
+ "Filler 2 Length (mm)": None,
148
+ "Specimen Volume (mm3)": 1000,
149
+ "Probe Count": 2,
150
+ "Probe Material": "Copper",
151
+ "W/B": 0.4,
152
+ "S/B": 2.5,
153
+ "Gauge Length (mm)": 20,
154
+ "Curing Condition": "28d water, 20°C",
155
+ "Number of Fillers": 1,
156
+ "Drying Temperature (°C)": 60,
157
+ "Drying Duration (hr)": 24,
158
+ "Loading Rate (MPa/s)": 0.1,
159
+ "Modulus of Elasticity (GPa)": 25,
160
+ "Current Type": "DC",
161
+ "Applied Voltage (V)": 5.0,
162
  }
163
 
164
  def _fill_example():
165
+ return [EXAMPLE.get(k, None) for k in MAIN_VARIABLES]
166
 
167
  def _clear_all():
168
+ # Return blanks in the same order as MAIN_VARIABLES
169
+ cleared = []
170
+ for col in MAIN_VARIABLES:
171
+ if col in NUMERIC_COLS:
172
+ cleared.append(None)
173
+ elif col in {"Filler 1 Dimensionality", "Filler 2 Dimensionality"}:
174
+ cleared.append("NA")
175
+ elif col == "Current Type":
176
+ cleared.append("NA")
177
+ else:
178
+ cleared.append("")
179
+ return cleared
180
+
181
+ ========================= UI =========================================================
182
 
183
  CSS = """
184
  /* blue→green background (replaces grayscale) */
185
  .gradio-container {
186
+ background:
187
+ radial-gradient(1200px 600px at 10% -10%, rgba(14,165,233,0.30), transparent 60%),
188
+ radial-gradient(1000px 500px at 110% 110%, rgba(16,185,129,0.28), transparent 55%),
189
+ linear-gradient(135deg,#0b1220 0%, #06231b 100%);
190
  }
191
 
192
  /* keep your font stack */
193
+
194
+ { font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; }
195
 
196
  /* cards */
197
  .card { background: rgba(255,255,255,0.05) !important; border: 1px solid rgba(255,255,255,0.10); }
198
 
199
  /* labels */
200
+ label.svelte-1ipelgc { color: #dbeafe !important; } # light blue
201
  """
202
 
203
  theme = gr.themes.Soft(
204
+ primary_hue="sky", # blue
205
+ secondary_hue="emerald", # green
206
+ neutral_hue="slate"
207
  ).set(
208
+ body_background_fill="#0b1220",
209
+ body_text_color="#e6f1f5",
210
+ input_background_fill="#0f172a",
211
+ input_border_color="#1f2937",
212
+ button_primary_background_fill="#0284c7", # blue
213
+ button_primary_text_color="#ffffff",
214
+ button_secondary_background_fill="#0b2a22" # deep teal
215
  )
216
 
217
  with gr.Blocks(css=CSS, theme=theme, fill_height=True) as demo:
218
+ gr.Markdown(
219
+ "<h1 style='margin:0'>🔮 Stress Gauge Factor (MPa⁻¹) — ML Predictor</h1>"
220
+ "<p style='opacity:.9'>Fields and units match your training data. "
221
+ "Leave anything blank if unknown — the model handles missing values.</p>"
222
+ )
223
+
224
+ with gr.Row():
225
+ # ---------------- Inputs (Left) ----------------
226
+ with gr.Column(scale=7):
227
+ # Primary filler
228
+ with gr.Accordion("Primary conductive filler", open=True, elem_classes=["card"]):
229
+ f1_type = gr.Textbox(label="Filler 1 Type", placeholder="e.g., CNT, Graphite, Steel fiber")
230
+ f1_dim = gr.Dropdown(DIM_CHOICES, value="NA", label="Filler 1 Dimensionality")
231
+ f1_diam = gr.Number(label="Filler 1 Diameter (µm)")
232
+ f1_len = gr.Number(label="Filler 1 Length (mm)")
233
+ cf_conc = gr.Number(label=f"{CF_COL}", info="Weight percent of total binder")
234
+
235
+ # Secondary filler (optional)
236
+ with gr.Accordion("➕ Secondary filler (optional)", open=False, elem_classes=["card"]):
237
+ f2_type = gr.Textbox(label="Filler 2 Type", placeholder="Optional")
238
+ f2_dim = gr.Dropdown(DIM_CHOICES, value="NA", label="Filler 2 Dimensionality")
239
+ f2_diam = gr.Number(label="Filler 2 Diameter (µm)")
240
+ f2_len = gr.Number(label="Filler 2 Length (mm)")
241
+
242
+ # Mix & specimen
243
+ with gr.Accordion("Mix design & specimen", open=False, elem_classes=["card"]):
244
+ spec_vol = gr.Number(label="Specimen Volume (mm3)")
245
+ probe_cnt = gr.Number(label="Probe Count")
246
+ probe_mat = gr.Textbox(label="Probe Material", placeholder="e.g., Copper, Silver paste")
247
+ wb = gr.Number(label="W/B")
248
+ sb = gr.Number(label="S/B")
249
+ gauge_len = gr.Number(label="Gauge Length (mm)")
250
+ curing = gr.Textbox(label="Curing Condition", placeholder="e.g., 28d water, 20°C")
251
+ n_fillers = gr.Number(label="Number of Fillers")
252
+
253
+ # Processing
254
+ with gr.Accordion("Processing", open=False, elem_classes=["card"]):
255
+ dry_temp = gr.Number(label="Drying Temperature (°C)")
256
+ dry_hrs = gr.Number(label="Drying Duration (hr)")
257
+
258
+ # Mechanical & Electrical loading
259
+ with gr.Accordion("Mechanical & electrical loading", open=False, elem_classes=["card"]):
260
+ load_rate = gr.Number(label="Loading Rate (MPa/s)")
261
+ E_mod = gr.Number(label="Modulus of Elasticity (GPa)")
262
+ current = gr.Dropdown(CURRENT_CHOICES, value="NA", label="Current Type")
263
+ voltage = gr.Number(label="Applied Voltage (V)")
264
+
265
+ # ---------------- Output (Right) ----------------
266
+ with gr.Column(scale=5):
267
+ with gr.Group(elem_classes=["card"]):
268
+ out_pred = gr.Number(label="Predicted Stress GF (MPa-1)", precision=6)
269
+ with gr.Row():
270
+ btn_pred = gr.Button("Predict", variant="primary")
271
+ btn_clear = gr.Button("Clear")
272
+ btn_demo = gr.Button("Fill Example")
273
+
274
+ with gr.Accordion("About this model", open=False, elem_classes=["card"]):
275
+ gr.Markdown(
276
+ "- Pipeline: **ColumnTransformer → (RobustScaler + OneHot) → XGBoost**\n"
277
+ "- Target: **Stress GF (MPa⁻¹)** on original scale (model trains on log1p).\n"
278
+ "- Missing values are safely imputed per-feature.\n"
279
+ "- Trained columns:\n"
280
+ f" `{', '.join(MAIN_VARIABLES)}`"
281
+ )
282
+
283
+ # Wire buttons
284
+ inputs_in_order = [
285
+ # MAIN_VARIABLES exact order:
286
+ # "Filler 1 Type","Filler 1 Diameter (µm)","Filler 1 Length (mm)", CF_COL,
287
+ # "Filler 1 Dimensionality","Filler 2 Type","Filler 2 Diameter (µm)","Filler 2 Length (mm)",
288
+ # "Filler 2 Dimensionality","Specimen Volume (mm3)","Probe Count","Probe Material",
289
+ # "W/B","S/B","Gauge Length (mm)","Curing Condition","Number of Fillers",
290
+ # "Drying Temperature (°C)","Drying Duration (hr)","Loading Rate (MPa/s)",
291
+ # "Modulus of Elasticity (GPa)","Current Type","Applied Voltage (V)"
292
+ f1_type, f1_diam, f1_len, cf_conc,
293
+ f1_dim, f2_type, f2_diam, f2_len,
294
+ f2_dim, spec_vol, probe_cnt, probe_mat,
295
+ wb, sb, gauge_len, curing, n_fillers,
296
+ dry_temp, dry_hrs, load_rate,
297
+ E_mod, current, voltage
298
+ ]
299
+
300
+ def _predict_wrapper(*vals):
301
+ data = {k: v for k, v in zip(MAIN_VARIABLES, vals)}
302
+ return predict_fn(**data)
303
+
304
+ btn_pred.click(_predict_wrapper, inputs=inputs_in_order, outputs=out_pred)
305
+ btn_clear.click(lambda: _clear_all(), inputs=None, outputs=inputs_in_order)
306
+ btn_demo.click(lambda: _fill_example(), inputs=None, outputs=inputs_in_order)