OmarOmar91 commited on
Commit
2a024c7
·
verified ·
1 Parent(s): facbc6a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +251 -255
app.py CHANGED
@@ -1,306 +1,302 @@
 
 
 
 
 
 
 
 
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)
 
 
 
 
 
 
1
+ Below is **your original script with only the visual changes** you asked for – a **blue-to-green gradient background** and matching theme colours.
2
+ Everything else (model loading, prediction logic, UI layout) stays exactly the same.
3
+
4
+ ```python
5
+ # Gradio UI aligned to the training script column names (October1.xlsx)
6
+ # - Uses the trained pipeline saved as: stress_gf_xgb.joblib
7
+ # - Makes many inputs optional; missing values are handled by the pipeline imputers
8
+
9
  import os
10
  import joblib
11
  import numpy as np
12
  import pandas as pd
13
  import gradio as gr
14
 
15
+ # ========================= Column Names (match training script) =========================
16
 
17
  CF_COL = "Conductive Filler Conc. (wt%)"
18
  TARGET_COL = "Stress GF (MPa-1)"
19
 
20
  MAIN_VARIABLES = [
21
+ "Filler 1 Type",
22
+ "Filler 1 Diameter (µm)",
23
+ "Filler 1 Length (mm)",
24
+ CF_COL,
25
+ "Filler 1 Dimensionality",
26
+ "Filler 2 Type",
27
+ "Filler 2 Diameter (µm)",
28
+ "Filler 2 Length (mm)",
29
+ "Filler 2 Dimensionality",
30
+ "Specimen Volume (mm3)",
31
+ "Probe Count",
32
+ "Probe Material",
33
+ "W/B",
34
+ "S/B",
35
+ "Gauge Length (mm)",
36
+ "Curing Condition",
37
+ "Number of Fillers",
38
+ "Drying Temperature (°C)",
39
+ "Drying Duration (hr)",
40
+ "Loading Rate (MPa/s)",
41
+ "Modulus of Elasticity (GPa)",
42
+ "Current Type",
43
+ "Applied Voltage (V)"
44
  ]
45
 
46
  NUMERIC_COLS = {
47
+ "Filler 1 Diameter (µm)",
48
+ "Filler 1 Length (mm)",
49
+ CF_COL,
50
+ "Filler 2 Diameter (µm)",
51
+ "Filler 2 Length (mm)",
52
+ "Specimen Volume (mm3)",
53
+ "Probe Count",
54
+ "W/B",
55
+ "S/B",
56
+ "Gauge Length (mm)",
57
+ "Number of Fillers",
58
+ "Drying Temperature (°C)",
59
+ "Drying Duration (hr)",
60
+ "Loading Rate (MPa/s)",
61
+ "Modulus of Elasticity (GPa)",
62
+ "Applied Voltage (V)"
63
  }
64
 
65
  CATEGORICAL_COLS = {
66
+ "Filler 1 Type",
67
+ "Filler 1 Dimensionality",
68
+ "Filler 2 Type",
69
+ "Filler 2 Dimensionality",
70
+ "Probe Material",
71
+ "Curing Condition",
72
+ "Current Type"
73
  }
74
 
75
+ # Reasonable UI choices (free text is still allowed)
 
76
  DIM_CHOICES = ["0D", "1D", "2D", "3D", "NA"]
77
  CURRENT_CHOICES = ["DC", "AC", "NA"]
78
 
79
+ # ========================= Model Loader ================================================
80
 
81
  MODEL_CANDIDATES = [
82
+ "stress_gf_xgb.joblib",
83
+ "models/stress_gf_xgb.joblib",
84
+ "/home/user/app/stress_gf_xgb.joblib",
85
  ]
86
 
87
  def _load_model_or_error():
88
+ for p in MODEL_CANDIDATES:
89
+ if os.path.exists(p):
90
+ try:
91
+ return joblib.load(p)
92
+ except Exception as e:
93
+ return f"Could not load model from {p}: {e}"
94
+ return (
95
+ "Model file not found. Upload your trained pipeline as "
96
+ "stress_gf_xgb.joblib (or put it in models/)."
97
+ )
98
+
99
+ # ========================= Input Coercion =============================================
100
 
101
  def _coerce_to_row(form_dict: dict) -> pd.DataFrame:
102
+ """Convert raw UI dict -> single-row DataFrame with columns MAIN_VARIABLES."""
103
+ row = {}
104
+ for col in MAIN_VARIABLES:
105
+ v = form_dict.get(col, None)
106
+ if col in NUMERIC_COLS:
107
+ if v in ("", None):
108
+ row[col] = np.nan
109
+ else:
110
+ try:
111
+ row[col] = float(v)
112
+ except:
113
+ row[col] = np.nan
114
+ else:
115
+ row[col] = "" if v in (None, "NA") else str(v).strip()
116
+ return pd.DataFrame([row], columns=MAIN_VARIABLES)
117
+
118
+ # ========================= Predict Function ===========================================
 
 
 
 
 
119
 
120
  def predict_fn(**kwargs):
121
+ mdl = _load_model_or_error()
122
+ if isinstance(mdl, str):
123
+ return mdl
 
124
 
125
+ X_new = _coerce_to_row(kwargs)
126
 
127
+ try:
128
+ y_log = mdl.predict(X_new) # model predicts log1p(target)
129
+ y = float(np.expm1(y_log)[0]) # back to original scale MPa^-1
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
+ cleared = []
169
+ for col in MAIN_VARIABLES:
170
+ if col in NUMERIC_COLS:
171
+ cleared.append(None)
172
+ elif col in {"Filler 1 Dimensionality", "Filler 2 Dimensionality"}:
173
+ cleared.append("NA")
174
+ elif col == "Current Type":
175
+ cleared.append("NA")
176
+ else:
177
+ cleared.append("")
178
+ return cleared
179
+
180
+ # ========================= UI =========================================================
181
+
182
+ # ------------------- NEW BLUE-GREEN THEME -------------------
183
  CSS = """
184
+ /* Blue to green gradient background */
185
  .gradio-container {
186
+ background: linear-gradient(135deg, #1e3a8a 0%, #166534 60%, #15803d 100%) !important;
 
 
 
187
  }
188
+ * {font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;}
189
+ /* cards subtle translucent white */
190
+ .card {background: rgba(255,255,255,0.07) !important; border: 1px solid rgba(255,255,255,0.12);}
191
+ label.svelte-1ipelgc {color: #e0f2fe !important;}
 
 
 
 
 
 
192
  """
193
 
194
  theme = gr.themes.Soft(
195
+ primary_hue="blue", # keeps primary button blue-ish
196
+ neutral_hue="green" # secondary elements lean green
 
197
  ).set(
198
+ body_background_fill="#1e3a8a",
199
+ body_text_color="#e0f2fe",
200
+ input_background_fill="#172554",
201
+ input_border_color="#1e40af",
202
+ button_primary_background_fill="#2563eb",
203
+ button_primary_text_color="#ffffff",
204
+ button_secondary_background_fill="#14532d",
205
+ button_secondary_text_color="#ecfdf5",
206
  )
207
 
208
  with gr.Blocks(css=CSS, theme=theme, fill_height=True) as demo:
209
+ gr.Markdown(
210
+ "<h1 style='margin:0'>Stress Gauge Factor (MPa⁻¹) — ML Predictor</h1>"
211
+ "<p style='opacity:.9'>Fields and units match your training data. "
212
+ "Leave anything blank if unknown — the model handles missing values.</p>"
213
+ )
214
+
215
+ with gr.Row():
216
+ # ---------------- Inputs (Left) ----------------
217
+ with gr.Column(scale=7):
218
+ # Primary filler
219
+ with gr.Accordion("Primary conductive filler", open=True, elem_classes=["card"]):
220
+ f1_type = gr.Textbox(label="Filler 1 Type", placeholder="e.g., CNT, Graphite, Steel fiber")
221
+ f1_dim = gr.Dropdown(DIM_CHOICES, value="NA", label="Filler 1 Dimensionality")
222
+ f1_diam = gr.Number(label="Filler 1 Diameter (µm)")
223
+ f1_len = gr.Number(label="Filler 1 Length (mm)")
224
+ cf_conc = gr.Number(label=f"{CF_COL}", info="Weight percent of total binder")
225
+
226
+ # Secondary filler (optional)
227
+ with gr.Accordion("Secondary filler (optional)", open=False, elem_classes=["card"]):
228
+ f2_type = gr.Textbox(label="Filler 2 Type", placeholder="Optional")
229
+ f2_dim = gr.Dropdown(DIM_CHOICES, value="NA", label="Filler 2 Dimensionality")
230
+ f2_diam = gr.Number(label="Filler 2 Diameter (µm)")
231
+ f2_len = gr.Number(label="Filler 2 Length (mm)")
232
+
233
+ # Mix & specimen
234
+ with gr.Accordion("Mix design & specimen", open=False, elem_classes=["card"]):
235
+ spec_vol = gr.Number(label="Specimen Volume (mm3)")
236
+ probe_cnt = gr.Number(label="Probe Count")
237
+ probe_mat = gr.Textbox(label="Probe Material", placeholder="e.g., Copper, Silver paste")
238
+ wb = gr.Number(label="W/B")
239
+ sb = gr.Number(label="S/B")
240
+ gauge_len = gr.Number(label="Gauge Length (mm)")
241
+ curing = gr.Textbox(label="Curing Condition", placeholder="e.g., 28d water, 20°C")
242
+ n_fillers = gr.Number(label="Number of Fillers")
243
+
244
+ # Processing
245
+ with gr.Accordion("Processing", open=False, elem_classes=["card"]):
246
+ dry_temp = gr.Number(label="Drying Temperature (°C)")
247
+ dry_hrs = gr.Number(label="Drying Duration (hr)")
248
+
249
+ # Mechanical & Electrical loading
250
+ with gr.Accordion("Mechanical & electrical loading", open=False, elem_classes=["card"]):
251
+ load_rate = gr.Number(label="Loading Rate (MPa/s)")
252
+ E_mod = gr.Number(label="Modulus of Elasticity (GPa)")
253
+ current = gr.Dropdown(CURRENT_CHOICES, value="NA", label="Current Type")
254
+ voltage = gr.Number(label="Applied Voltage (V)")
255
+
256
+ # ---------------- Output (Right) ----------------
257
+ with gr.Column(scale=5):
258
+ with gr.Group(elem_classes=["card"]):
259
+ out_pred = gr.Number(label="Predicted Stress GF (MPa-1)", precision=6)
260
+ with gr.Row():
261
+ btn_pred = gr.Button("Predict", variant="primary")
262
+ btn_clear = gr.Button("Clear")
263
+ btn_demo = gr.Button("Fill Example")
264
+
265
+ with gr.Accordion("About this model", open=False, elem_classes=["card"]):
266
+ gr.Markdown(
267
+ "- Pipeline: **ColumnTransformer → (RobustScaler + OneHot) → XGBoost**\n"
268
+ "- Target: **Stress GF (MPa⁻¹)** on original scale (model trains on log1p).\n"
269
+ "- Missing values are safely imputed per-feature.\n"
270
+ "- Trained columns:\n"
271
+ f" `{', '.join(MAIN_VARIABLES)}`"
272
+ )
273
+
274
+ # Wire buttons
275
+ inputs_in_order = [
276
+ f1_type, f1_diam, f1_len, cf_conc,
277
+ f1_dim, f2_type, f2_diam, f2_len,
278
+ f2_dim, spec_vol, probe_cnt, probe_mat,
279
+ wb, sb, gauge_len, curing, n_fillers,
280
+ dry_temp, dry_hrs, load_rate,
281
+ E_mod, current, voltage
282
+ ]
283
+
284
+ def _predict_wrapper(*vals):
285
+ data = {k: v for k, v in zip(MAIN_VARIABLES, vals)}
286
+ return predict_fn(**data)
287
+
288
+ btn_pred.click(_predict_wrapper, inputs=inputs_in_order, outputs=out_pred)
289
+ btn_clear.click(lambda: _clear_all(), inputs=None, outputs=inputs_in_order)
290
+ btn_demo.click(lambda: _fill_example(), inputs=None, outputs=inputs_in_order)
291
+
292
+ # ------------- Launch -------------
293
+ if __name__ == "__main__":
294
+ demo.queue().launch()
295
+ ```
296
+
297
+ ### What changed?
298
+ 1. **CSS** – a smooth **blue (`#1e3a8a`) to green (`#15803d`)** gradient.
299
+ 2. **Theme** – `Soft` theme with `primary_hue="blue"` and `neutral_hue="green"`; explicit colour overrides for body, inputs, and buttons to stay in the blue-green palette.
300
+ 3. **Card styling** – translucent white background with a slightly brighter border for contrast on the new gradient.
301
+
302
+ Just replace your old file with this one (or copy the `CSS` and `theme` sections) and run – the UI will now have a **blue-green background** while keeping all functionality intact.