UCS2014 commited on
Commit
047eee8
·
verified ·
1 Parent(s): 617b9e0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +74 -102
app.py CHANGED
@@ -21,10 +21,8 @@ MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
21
  # =========================
22
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
23
 
24
- # Hide Streamlit default header/footer
25
  st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
26
-
27
- # Compact page, bigger logo, tidy hero
28
  st.markdown(
29
  """
30
  <style>
@@ -44,14 +42,13 @@ st.markdown(
44
  )
45
 
46
  # =========================
47
- # Small helpers
48
  # =========================
49
  def _get_model_url():
50
- """HuggingFace exposes Space variables via environment; avoid st.secrets to prevent red banner."""
51
  return (os.environ.get("MODEL_URL", "") or "").strip()
52
 
53
- def rmse(y_true, y_pred):
54
- return float(np.sqrt(mean_squared_error(y_true, y_pred)))
55
 
56
  def ensure_cols(df, cols):
57
  miss = [c for c in cols if c not in df.columns]
@@ -71,24 +68,20 @@ def parse_excel(data_bytes: bytes):
71
  return {sh: xl.parse(sh) for sh in xl.sheet_names}
72
 
73
  def read_book(upload):
74
- if upload is None:
75
- return {}
76
- try:
77
- return parse_excel(upload.getvalue())
78
  except Exception as e:
79
- st.error(f"Failed to read Excel: {e}")
80
- return {}
81
 
82
  def find_sheet(book, names):
83
  low2orig = {k.lower(): k for k in book.keys()}
84
  for nm in names:
85
- if nm.lower() in low2orig:
86
- return low2orig[nm.lower()]
87
  return None
88
 
89
- def cross_plot(actual, pred, title, size=(5.0, 5.0)):
90
  fig, ax = plt.subplots(figsize=size, dpi=100)
91
- ax.scatter(actual, pred, s=14, alpha=0.75)
92
  lo = float(np.nanmin([actual.min(), pred.min()]))
93
  hi = float(np.nanmax([actual.max(), pred.max()]))
94
  pad = 0.03 * (hi - lo if hi > lo else 1.0)
@@ -96,94 +89,80 @@ def cross_plot(actual, pred, title, size=(5.0, 5.0)):
96
  ax.set_xlim(lo - pad, hi + pad)
97
  ax.set_ylim(lo - pad, hi + pad)
98
  ax.set_aspect('equal', 'box') # perfect 1:1
99
- ax.set_xlabel("Actual UCS")
100
- ax.set_ylabel("Predicted UCS")
101
- ax.set_title(title)
102
  ax.grid(True, ls=":", alpha=0.4)
103
  return fig
104
 
105
- def depth_or_index_track(df, title, include_actual=True):
106
- # depth-like column?
107
  depth_col = None
108
  for c in df.columns:
109
  if 'depth' in str(c).lower():
110
- depth_col = c
111
- break
112
- # taller for a log-profile look
113
- fig_h = 8.8 if depth_col is not None else 8.0
114
- fig, ax = plt.subplots(figsize=(6.2, fig_h), dpi=100)
115
 
116
  if depth_col is not None:
117
  ax.plot(df["UCS_Pred"], df[depth_col], '--', lw=1.6, label="UCS_Pred")
118
  if include_actual and TARGET in df.columns:
119
- ax.plot(df[TARGET], df[depth_col], '-', lw=2.0, alpha=0.8, label="UCS (actual)")
120
- ax.set_ylabel(depth_col)
121
- ax.set_xlabel("UCS")
122
  ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
123
  else:
124
  idx = np.arange(1, len(df) + 1)
125
  ax.plot(df["UCS_Pred"], idx, '--', lw=1.6, label="UCS_Pred")
126
  if include_actual and TARGET in df.columns:
127
- ax.plot(df[TARGET], idx, '-', lw=2.0, alpha=0.8, label="UCS (actual)")
128
- ax.set_ylabel("Point Index")
129
- ax.set_xlabel("UCS")
130
  ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
131
 
132
  ax.grid(True, linestyle=":", alpha=0.4)
133
- ax.set_title(title, pad=10)
134
  ax.legend(loc="best")
135
  return fig
136
 
137
  def export_workbook(sheets_dict, summary_df=None):
138
- try:
139
- import openpyxl # noqa
140
- except Exception:
141
- raise RuntimeError("Export requires openpyxl. Please add it to requirements or install it.")
142
  buf = io.BytesIO()
143
  with pd.ExcelWriter(buf, engine="openpyxl") as xw:
144
  for name, frame in sheets_dict.items():
145
  frame.to_excel(xw, sheet_name=name[:31], index=False)
146
- if summary_df is not None:
147
- summary_df.to_excel(xw, sheet_name="Summary", index=False)
148
  return buf.getvalue()
149
 
150
  def toast(msg):
151
- try:
152
- st.toast(msg)
153
- except Exception:
154
- st.info(msg)
155
 
156
  def infer_features_from_model(m):
157
  try:
158
  if hasattr(m, "feature_names_in_") and len(getattr(m, "feature_names_in_")):
159
  return [str(x) for x in m.feature_names_in_]
160
- except Exception:
161
- pass
162
  try:
163
  if hasattr(m, "steps") and len(m.steps):
164
  last = m.steps[-1][1]
165
  if hasattr(last, "feature_names_in_") and len(last.feature_names_in_):
166
  return [str(x) for x in last.feature_names_in_]
167
- except Exception:
168
- pass
169
  return None
170
 
171
  def inline_logo(path="logo.png") -> str:
172
  try:
173
  p = Path(path)
174
- if not p.exists():
175
- return ""
176
  return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
177
  except Exception:
178
  return ""
179
 
180
  # =========================
181
- # Model availability (download on cloud if needed)
182
  # =========================
183
  MODEL_URL = _get_model_url()
184
 
185
  def ensure_model_present() -> Path:
186
- """Return a local model path, trying local files first, then (optionally) downloading with timeout."""
187
  for p in [DEFAULT_MODEL, *MODEL_FALLBACKS]:
188
  if p.exists() and p.stat().st_size > 0:
189
  return p
@@ -196,9 +175,8 @@ def ensure_model_present() -> Path:
196
  with requests.get(MODEL_URL, stream=True, timeout=30) as r:
197
  r.raise_for_status()
198
  with open(DEFAULT_MODEL, "wb") as f:
199
- for chunk in r.iter_content(chunk_size=1 << 20):
200
- if chunk:
201
- f.write(chunk)
202
  return DEFAULT_MODEL
203
  except Exception as e:
204
  st.error(f"Failed to download model from MODEL_URL: {e}")
@@ -220,24 +198,25 @@ meta_path = MODELS_DIR / "meta.json"
220
  if meta_path.exists():
221
  try:
222
  meta = json.loads(meta_path.read_text(encoding="utf-8"))
223
- FEATURES = meta.get("features", FEATURES)
224
- TARGET = meta.get("target", TARGET)
225
- except Exception:
226
- pass
227
  else:
228
  infer = infer_features_from_model(model)
229
- if infer:
230
- FEATURES = infer
231
 
232
  # =========================
233
  # Session state
234
  # =========================
235
- if "app_step" not in st.session_state:
236
- st.session_state.app_step = "intro"
237
- if "results" not in st.session_state:
238
- st.session_state.results = {}
239
- if "train_ranges" not in st.session_state:
240
- st.session_state.train_ranges = None
 
 
 
 
241
 
242
  # =========================
243
  # Hero header (logo + title)
@@ -261,8 +240,7 @@ st.markdown(
261
  if st.session_state.app_step == "intro":
262
  st.header("Welcome!")
263
  st.markdown(
264
- "This software is developed by *Smart Thinking AI-Solutions Team* "
265
- "to predict the UCS of the underlying formations while drilling using the drilling data."
266
  )
267
  st.subheader("Required Input Columns")
268
  st.markdown(
@@ -274,55 +252,50 @@ if st.session_state.app_step == "intro":
274
  )
275
  st.subheader("How It Works")
276
  st.markdown(
277
- "1. *Upload the Model Development Data.* This should contain your training and testing sets.\n"
278
- "2. Click *Run Model* to view metrics, cross-plots, and a track plot.\n"
279
- "3. Click *Proceed to Prediction* and upload a new dataset to get predictions.\n"
280
- "4. *Export* everything to Excel for further analysis."
281
  )
282
  if st.button("Start Showcase", type="primary", key="start_showcase"):
283
- st.session_state.app_step = "dev"
284
- st.rerun()
285
 
286
  # =========================
287
  # MODEL DEVELOPMENT (Train/Test)
288
  # =========================
289
  if st.session_state.app_step == "dev":
290
  st.sidebar.header("Model Development Data")
291
- train_test_file = st.sidebar.file_uploader("Upload Data (Excel)", type=["xlsx", "xls"], key="dev_upload")
292
 
293
- # Always show the nav button, disabled until results exist once.
294
- ready_for_pred = ("Train" in st.session_state.results) or ("Test" in st.session_state.results)
 
295
  st.sidebar.button(
296
  "Proceed to Prediction ▶",
297
  use_container_width=True,
298
- disabled=not ready_for_pred,
299
- on_click=(lambda: st.session_state.update(app_step="predict")) if ready_for_pred else None,
300
  )
301
 
302
- run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
303
 
304
  st.subheader("Model Development")
305
  if run_btn and train_test_file is not None:
306
  with st.status("Processing…", expanded=False) as status:
307
  book = read_book(train_test_file)
308
- if not book:
309
- status.update(label="Failed to read workbook.", state="error")
310
- st.stop()
311
  status.update(label="Workbook read ✓")
312
 
313
- # still expect Train/Test sheets internally
314
- sh_train = find_sheet(book, ["Train", "Training", "training2", "train", "training"])
315
- sh_test = find_sheet(book, ["Test", "Testing", "testing2", "test", "testing"])
316
  if sh_train is None or sh_test is None:
317
- status.update(label="Workbook must include Train/Training/training2 and Test/Testing/testing2.", state="error")
318
- st.stop()
319
 
320
  df_tr = book[sh_train].copy(); df_te = book[sh_test].copy()
321
  if not (ensure_cols(df_tr, FEATURES + [TARGET]) and ensure_cols(df_te, FEATURES + [TARGET])):
322
  status.update(label="Missing required columns.", state="error"); st.stop()
323
 
324
- status.update(label="Columns validated ✓")
325
- status.update(label="Predicting…")
326
 
327
  df_tr["UCS_Pred"] = model.predict(df_tr[FEATURES])
328
  df_te["UCS_Pred"] = model.predict(df_te[FEATURES])
@@ -342,7 +315,9 @@ if st.session_state.app_step == "dev":
342
  tr_min = df_tr[FEATURES].min().to_dict(); tr_max = df_tr[FEATURES].max().to_dict()
343
  st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
344
 
 
345
  status.update(label="Done ✓", state="complete"); toast("Model run complete 🚀")
 
346
 
347
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
348
  tab1, tab2 = st.tabs(["Training", "Testing"])
@@ -350,27 +325,24 @@ if st.session_state.app_step == "dev":
350
  with tab1:
351
  df = st.session_state.results["Train"]; m = st.session_state.results["metrics_train"]
352
  c1,c2,c3 = st.columns(3)
353
- c1.metric("R²", f"{m['R2']:.4f}")
354
- c2.metric("RMSE", f"{m['RMSE']:.4f}")
355
- c3.metric("MAE", f"{m['MAE']:.4f}")
356
  left,right = st.columns([1,1])
357
  with left:
358
  st.pyplot(cross_plot(df[TARGET], df["UCS_Pred"], "Training: Actual vs Predicted"), use_container_width=True)
359
  with right:
360
- st.pyplot(depth_or_index_track(df, "Training: Depth/Index Track", include_actual=True), use_container_width=True)
 
361
 
362
  if "Test" in st.session_state.results:
363
  with tab2:
364
  df = st.session_state.results["Test"]; m = st.session_state.results["metrics_test"]
365
  c1,c2,c3 = st.columns(3)
366
- c1.metric("R²", f"{m['R2']:.4f}")
367
- c2.metric("RMSE", f"{m['RMSE']:.4f}")
368
- c3.metric("MAE", f"{m['MAE']:.4f}")
369
  left,right = st.columns([1,1])
370
  with left:
371
  st.pyplot(cross_plot(df[TARGET], df["UCS_Pred"], "Testing: Actual vs Predicted"), use_container_width=True)
372
  with right:
373
- st.pyplot(depth_or_index_track(df, "Testing: Depth/Index Track", include_actual=True), use_container_width=True)
374
 
375
  st.markdown("---")
376
  sheets = {}; rows = []
@@ -383,7 +355,7 @@ if st.session_state.app_step == "dev":
383
  summary_df = pd.DataFrame(rows) if rows else None
384
  try:
385
  data_bytes = export_workbook(sheets, summary_df)
386
- st.download_button("Export Train/Test Results to Excel",
387
  data=data_bytes, file_name="UCS_Dev_Results.xlsx",
388
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
389
  except RuntimeError as e:
@@ -399,7 +371,7 @@ if st.session_state.app_step == "predict":
399
  st.sidebar.button("⬅ Back", on_click=lambda: st.session_state.update(app_step="dev"), use_container_width=True)
400
 
401
  st.subheader("Prediction")
402
- st.write("Upload a new dataset to get UCS predictions and see how the model performs on new data.")
403
 
404
  if predict_btn and validation_file is not None:
405
  with st.status("Predicting…", expanded=False) as status:
@@ -452,7 +424,7 @@ if st.session_state.app_step == "predict":
452
  else:
453
  st.info("Actual UCS values are not available in the validation data. Cross-plot cannot be generated.")
454
  with right:
455
- st.pyplot(depth_or_index_track(st.session_state.results["Validate"], "Validation: Depth/Index Track", include_actual=(TARGET in st.session_state.results["Validate"].columns)), use_container_width=True)
456
  if oor_table is not None:
457
  st.write("*Out-of-range rows (vs. Training min–max):*")
458
  st.dataframe(oor_table, use_container_width=True)
 
21
  # =========================
22
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
23
 
24
+ # Hide Streamlit default header/footer and tighten layout
25
  st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
 
 
26
  st.markdown(
27
  """
28
  <style>
 
42
  )
43
 
44
  # =========================
45
+ # Helpers
46
  # =========================
47
  def _get_model_url():
48
+ """Read optional MODEL_URL from environment only (avoid st.secrets banner)."""
49
  return (os.environ.get("MODEL_URL", "") or "").strip()
50
 
51
+ def rmse(y_true, y_pred): return float(np.sqrt(mean_squared_error(y_true, y_pred)))
 
52
 
53
  def ensure_cols(df, cols):
54
  miss = [c for c in cols if c not in df.columns]
 
68
  return {sh: xl.parse(sh) for sh in xl.sheet_names}
69
 
70
  def read_book(upload):
71
+ if upload is None: return {}
72
+ try: return parse_excel(upload.getvalue())
 
 
73
  except Exception as e:
74
+ st.error(f"Failed to read Excel: {e}"); return {}
 
75
 
76
  def find_sheet(book, names):
77
  low2orig = {k.lower(): k for k in book.keys()}
78
  for nm in names:
79
+ if nm.lower() in low2orig: return low2orig[nm.lower()]
 
80
  return None
81
 
82
+ def cross_plot(actual, pred, title, size=(4.6, 4.6)):
83
  fig, ax = plt.subplots(figsize=size, dpi=100)
84
+ ax.scatter(actual, pred, s=14, alpha=0.8)
85
  lo = float(np.nanmin([actual.min(), pred.min()]))
86
  hi = float(np.nanmax([actual.max(), pred.max()]))
87
  pad = 0.03 * (hi - lo if hi > lo else 1.0)
 
89
  ax.set_xlim(lo - pad, hi + pad)
90
  ax.set_ylim(lo - pad, hi + pad)
91
  ax.set_aspect('equal', 'box') # perfect 1:1
92
+ ax.set_xlabel("Actual UCS"); ax.set_ylabel("Predicted UCS"); ax.set_title(title)
 
 
93
  ax.grid(True, ls=":", alpha=0.4)
94
  return fig
95
 
96
+ def depth_or_index_track(df, title=None, include_actual=True):
97
+ # Find depth-like column if available
98
  depth_col = None
99
  for c in df.columns:
100
  if 'depth' in str(c).lower():
101
+ depth_col = c; break
102
+
103
+ fig_h = 7.4 if depth_col is not None else 7.0 # taller track; still fits most screens
104
+ fig, ax = plt.subplots(figsize=(6.0, fig_h), dpi=100)
 
105
 
106
  if depth_col is not None:
107
  ax.plot(df["UCS_Pred"], df[depth_col], '--', lw=1.6, label="UCS_Pred")
108
  if include_actual and TARGET in df.columns:
109
+ ax.plot(df[TARGET], df[depth_col], '-', lw=2.0, alpha=0.85, label="UCS (actual)")
110
+ ax.set_ylabel(depth_col); ax.set_xlabel("UCS")
 
111
  ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
112
  else:
113
  idx = np.arange(1, len(df) + 1)
114
  ax.plot(df["UCS_Pred"], idx, '--', lw=1.6, label="UCS_Pred")
115
  if include_actual and TARGET in df.columns:
116
+ ax.plot(df[TARGET], idx, '-', lw=2.0, alpha=0.85, label="UCS (actual)")
117
+ ax.set_ylabel("Point Index"); ax.set_xlabel("UCS")
 
118
  ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
119
 
120
  ax.grid(True, linestyle=":", alpha=0.4)
121
+ if title: ax.set_title(title, pad=8) # no title if None/empty
122
  ax.legend(loc="best")
123
  return fig
124
 
125
  def export_workbook(sheets_dict, summary_df=None):
126
+ try: import openpyxl # noqa
127
+ except Exception: raise RuntimeError("Export requires openpyxl. Please add it to requirements or install it.")
 
 
128
  buf = io.BytesIO()
129
  with pd.ExcelWriter(buf, engine="openpyxl") as xw:
130
  for name, frame in sheets_dict.items():
131
  frame.to_excel(xw, sheet_name=name[:31], index=False)
132
+ if summary_df is not None: summary_df.to_excel(xw, sheet_name="Summary", index=False)
 
133
  return buf.getvalue()
134
 
135
  def toast(msg):
136
+ try: st.toast(msg)
137
+ except Exception: st.info(msg)
 
 
138
 
139
  def infer_features_from_model(m):
140
  try:
141
  if hasattr(m, "feature_names_in_") and len(getattr(m, "feature_names_in_")):
142
  return [str(x) for x in m.feature_names_in_]
143
+ except Exception: pass
 
144
  try:
145
  if hasattr(m, "steps") and len(m.steps):
146
  last = m.steps[-1][1]
147
  if hasattr(last, "feature_names_in_") and len(last.feature_names_in_):
148
  return [str(x) for x in last.feature_names_in_]
149
+ except Exception: pass
 
150
  return None
151
 
152
  def inline_logo(path="logo.png") -> str:
153
  try:
154
  p = Path(path)
155
+ if not p.exists(): return ""
 
156
  return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
157
  except Exception:
158
  return ""
159
 
160
  # =========================
161
+ # Model presence (local or optional download)
162
  # =========================
163
  MODEL_URL = _get_model_url()
164
 
165
  def ensure_model_present() -> Path:
 
166
  for p in [DEFAULT_MODEL, *MODEL_FALLBACKS]:
167
  if p.exists() and p.stat().st_size > 0:
168
  return p
 
175
  with requests.get(MODEL_URL, stream=True, timeout=30) as r:
176
  r.raise_for_status()
177
  with open(DEFAULT_MODEL, "wb") as f:
178
+ for chunk in r.iter_content(chunk_size=1<<20):
179
+ if chunk: f.write(chunk)
 
180
  return DEFAULT_MODEL
181
  except Exception as e:
182
  st.error(f"Failed to download model from MODEL_URL: {e}")
 
198
  if meta_path.exists():
199
  try:
200
  meta = json.loads(meta_path.read_text(encoding="utf-8"))
201
+ FEATURES = meta.get("features", FEATURES); TARGET = meta.get("target", TARGET)
202
+ except Exception: pass
 
 
203
  else:
204
  infer = infer_features_from_model(model)
205
+ if infer: FEATURES = infer
 
206
 
207
  # =========================
208
  # Session state
209
  # =========================
210
+ if "app_step" not in st.session_state: st.session_state.app_step = "intro"
211
+ if "results" not in st.session_state: st.session_state.results = {}
212
+ if "train_ranges" not in st.session_state: st.session_state.train_ranges = None
213
+ # Track when dev run has completed at least once (for enabling the Proceed button immediately)
214
+ if "dev_ready" not in st.session_state:
215
+ st.session_state.dev_ready = False
216
+
217
+ # If results already exist (page refresh) mark ready
218
+ if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
219
+ st.session_state.dev_ready = True
220
 
221
  # =========================
222
  # Hero header (logo + title)
 
240
  if st.session_state.app_step == "intro":
241
  st.header("Welcome!")
242
  st.markdown(
243
+ "This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data."
 
244
  )
245
  st.subheader("Required Input Columns")
246
  st.markdown(
 
252
  )
253
  st.subheader("How It Works")
254
  st.markdown(
255
+ "1. **Upload your development data (Excel)** and click **Run Model** to compute metrics and review plots. \n"
256
+ "2. Click **Proceed to Prediction** to upload a new dataset for validation and view results. \n"
257
+ "3. Export results to Excel at any time."
 
258
  )
259
  if st.button("Start Showcase", type="primary", key="start_showcase"):
260
+ st.session_state.app_step = "dev"; st.rerun()
 
261
 
262
  # =========================
263
  # MODEL DEVELOPMENT (Train/Test)
264
  # =========================
265
  if st.session_state.app_step == "dev":
266
  st.sidebar.header("Model Development Data")
267
+ train_test_file = st.sidebar.file_uploader("Upload Data (Excel)", type=["xlsx","xls"], key="dev_upload")
268
 
269
+ run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
270
+
271
+ # Proceed button BELOW run, always visible; enables immediately after first successful run
272
  st.sidebar.button(
273
  "Proceed to Prediction ▶",
274
  use_container_width=True,
275
+ disabled=not st.session_state.dev_ready,
276
+ on_click=(lambda: st.session_state.update(app_step="predict")) if st.session_state.dev_ready else None,
277
  )
278
 
279
+ st.caption("Upload your data to train the model and review the development performance.")
280
 
281
  st.subheader("Model Development")
282
  if run_btn and train_test_file is not None:
283
  with st.status("Processing…", expanded=False) as status:
284
  book = read_book(train_test_file)
285
+ if not book: status.update(label="Failed to read workbook.", state="error"); st.stop()
 
 
286
  status.update(label="Workbook read ✓")
287
 
288
+ # Internally still expect Train/Test sheets
289
+ sh_train = find_sheet(book, ["Train","Training","training2","train","training"])
290
+ sh_test = find_sheet(book, ["Test","Testing","testing2","test","testing"])
291
  if sh_train is None or sh_test is None:
292
+ status.update(label="Workbook must include Train/Training/training2 and Test/Testing/testing2.", state="error"); st.stop()
 
293
 
294
  df_tr = book[sh_train].copy(); df_te = book[sh_test].copy()
295
  if not (ensure_cols(df_tr, FEATURES + [TARGET]) and ensure_cols(df_te, FEATURES + [TARGET])):
296
  status.update(label="Missing required columns.", state="error"); st.stop()
297
 
298
+ status.update(label="Columns validated ✓"); status.update(label="Predicting…")
 
299
 
300
  df_tr["UCS_Pred"] = model.predict(df_tr[FEATURES])
301
  df_te["UCS_Pred"] = model.predict(df_te[FEATURES])
 
315
  tr_min = df_tr[FEATURES].min().to_dict(); tr_max = df_tr[FEATURES].max().to_dict()
316
  st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
317
 
318
+ st.session_state.dev_ready = True # enable Proceed button immediately
319
  status.update(label="Done ✓", state="complete"); toast("Model run complete 🚀")
320
+ st.rerun() # refresh to enable the sidebar button without a second click
321
 
322
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
323
  tab1, tab2 = st.tabs(["Training", "Testing"])
 
325
  with tab1:
326
  df = st.session_state.results["Train"]; m = st.session_state.results["metrics_train"]
327
  c1,c2,c3 = st.columns(3)
328
+ c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
 
 
329
  left,right = st.columns([1,1])
330
  with left:
331
  st.pyplot(cross_plot(df[TARGET], df["UCS_Pred"], "Training: Actual vs Predicted"), use_container_width=True)
332
  with right:
333
+ # no title on the track (cleaner)
334
+ st.pyplot(depth_or_index_track(df, title=None, include_actual=True), use_container_width=True)
335
 
336
  if "Test" in st.session_state.results:
337
  with tab2:
338
  df = st.session_state.results["Test"]; m = st.session_state.results["metrics_test"]
339
  c1,c2,c3 = st.columns(3)
340
+ c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
 
 
341
  left,right = st.columns([1,1])
342
  with left:
343
  st.pyplot(cross_plot(df[TARGET], df["UCS_Pred"], "Testing: Actual vs Predicted"), use_container_width=True)
344
  with right:
345
+ st.pyplot(depth_or_index_track(df, title=None, include_actual=True), use_container_width=True)
346
 
347
  st.markdown("---")
348
  sheets = {}; rows = []
 
355
  summary_df = pd.DataFrame(rows) if rows else None
356
  try:
357
  data_bytes = export_workbook(sheets, summary_df)
358
+ st.download_button("Export Development Results to Excel",
359
  data=data_bytes, file_name="UCS_Dev_Results.xlsx",
360
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
361
  except RuntimeError as e:
 
371
  st.sidebar.button("⬅ Back", on_click=lambda: st.session_state.update(app_step="dev"), use_container_width=True)
372
 
373
  st.subheader("Prediction")
374
+ st.write("Upload a new dataset to generate UCS predictions and evaluate performance on unseen data.")
375
 
376
  if predict_btn and validation_file is not None:
377
  with st.status("Predicting…", expanded=False) as status:
 
424
  else:
425
  st.info("Actual UCS values are not available in the validation data. Cross-plot cannot be generated.")
426
  with right:
427
+ st.pyplot(depth_or_index_track(st.session_state.results["Validate"], title=None, include_actual=(TARGET in st.session_state.results["Validate"].columns)), use_container_width=True)
428
  if oor_table is not None:
429
  st.write("*Out-of-range rows (vs. Training min–max):*")
430
  st.dataframe(oor_table, use_container_width=True)