UCS2014 commited on
Commit
fc3fc49
·
verified ·
1 Parent(s): cdd0bd3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +246 -527
app.py CHANGED
@@ -6,7 +6,7 @@ import pandas as pd
6
  import numpy as np
7
  import joblib
8
 
9
- # matplotlib only for PREVIEW modal (static thumbnails)
10
  import matplotlib
11
  matplotlib.use("Agg")
12
  import matplotlib.pyplot as plt
@@ -15,7 +15,7 @@ import plotly.graph_objects as go
15
  from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
16
 
17
  # =========================
18
- # Defaults / Constants
19
  # =========================
20
  FEATURES = ["Q, gpm", "SPP(psi)", "T (kft.lbf)", "WOB (klbf)", "ROP (ft/h)"]
21
  TARGET = "UCS"
@@ -25,34 +25,29 @@ MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
25
 
26
  COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
27
 
28
- # ——— Exact pixel sizes (match preview strip & square cross-plot) ———
29
- CROSS_W, CROSS_H = 420, 420 # square cross-plot (px)
30
- TRACK_W, TRACK_H = 220, 700 # match preview strip look (px)
31
- FONT_SIZE = 13
32
 
33
  # =========================
34
- # Page / Theme
35
  # =========================
36
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
37
-
38
- # ---- CSS ----
39
  st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
40
  st.markdown(
41
  """
42
  <style>
43
- .stApp { background: #FFFFFF; }
44
- section[data-testid="stSidebar"] { background: #F6F9FC; }
45
- .block-container { padding-top: .5rem; padding-bottom: .5rem; }
46
- /* primary buttons */
47
- .stButton>button{ background:#007bff; color:#fff; font-weight:bold; border-radius:8px; border:none; padding:10px 24px; }
48
- .stButton>button:hover{ background:#0056b3; }
49
- /* hero */
50
- .st-hero { display:flex; align-items:center; gap:16px; padding-top: 4px; }
51
- .st-hero .brand { width:110px; height:110px; object-fit:contain; }
52
- .st-hero h1 { margin:0; line-height:1.05; }
53
- .st-hero .tagline { margin:2px 0 0 2px; color:#6b7280; font-size:1.05rem; font-style:italic; }
54
- /* tighter top margin for first block */
55
- [data-testid="stBlock"]{ margin-top:0 !important; }
56
  </style>
57
  """,
58
  unsafe_allow_html=True
@@ -69,8 +64,7 @@ def inline_logo(path="logo.png") -> str:
69
  except Exception:
70
  return ""
71
 
72
- def add_password_gate() -> bool:
73
- """Branded access screen until correct APP_PASSWORD in Secrets/Env is entered."""
74
  try:
75
  required = st.secrets.get("APP_PASSWORD", "")
76
  except Exception:
@@ -88,8 +82,7 @@ def add_password_gate() -> bool:
88
  </div>
89
  <div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected Area</div>
90
  <div style="color:#6b7280;margin-bottom:14px;">
91
- Admin action required: set <code>APP_PASSWORD</code> in <b>Settings → Secrets</b> (or as an
92
- environment variable) and restart the Space.
93
  </div>
94
  """,
95
  unsafe_allow_html=True,
@@ -97,7 +90,7 @@ def add_password_gate() -> bool:
97
  st.stop()
98
 
99
  if st.session_state.get("auth_ok", False):
100
- return True
101
 
102
  st.markdown(
103
  f"""
@@ -109,30 +102,24 @@ def add_password_gate() -> bool:
109
  </div>
110
  </div>
111
  <div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected</div>
112
- <div style="color:#6b7280;margin-bottom:14px;">
113
- Please enter your access key to continue.
114
- </div>
115
  """,
116
  unsafe_allow_html=True
117
  )
118
 
119
  pwd = st.text_input("Access key", type="password", placeholder="••••••••")
120
- col1, _ = st.columns([1, 3])
121
- with col1:
122
- if st.button("Unlock", type="primary", use_container_width=True):
123
- if pwd == required:
124
- st.session_state.auth_ok = True
125
- st.rerun()
126
- else:
127
- st.error("Incorrect key. Please try again.")
128
-
129
  st.stop()
130
 
131
- # call it now
132
  add_password_gate()
133
 
134
  # =========================
135
- # Helpers
136
  # =========================
137
  try:
138
  dialog = st.dialog
@@ -145,18 +132,8 @@ except AttributeError:
145
  return wrapper
146
  return deco
147
 
148
- def _get_model_url():
149
- return (os.environ.get("MODEL_URL", "") or "").strip()
150
-
151
  def rmse(y_true, y_pred): return float(np.sqrt(mean_squared_error(y_true, y_pred)))
152
 
153
- def ensure_cols(df, cols):
154
- miss = [c for c in cols if c not in df.columns]
155
- if miss:
156
- st.error(f"Missing columns: {miss}\nFound: {list(df.columns)}")
157
- return False
158
- return True
159
-
160
  @st.cache_resource(show_spinner=False)
161
  def load_model(model_path: str):
162
  return joblib.load(model_path)
@@ -167,20 +144,16 @@ def parse_excel(data_bytes: bytes):
167
  xl = pd.ExcelFile(bio)
168
  return {sh: xl.parse(sh) for sh in xl.sheet_names}
169
 
170
- def read_book_bytes(data_bytes: bytes):
171
- if not data_bytes: return {}
172
- try: return parse_excel(data_bytes)
173
- except Exception as e:
174
- st.error(f"Failed to read Excel: {e}"); return {}
175
-
176
  def find_sheet(book, names):
177
  low2orig = {k.lower(): k for k in book.keys()}
178
  for nm in names:
179
  if nm.lower() in low2orig: return low2orig[nm.lower()]
180
  return None
181
 
182
- # ---------- Interactive plotting (fixed sizes, full outline, crisp fonts) ----------
183
- def cross_plot_interactive(actual, pred):
184
  a = pd.Series(actual).astype(float)
185
  p = pd.Series(pred).astype(float)
186
  lo = float(np.nanmin([a.min(), p.min()]))
@@ -192,7 +165,7 @@ def cross_plot_interactive(actual, pred):
192
  fig.add_trace(go.Scatter(
193
  x=a, y=p, mode="markers",
194
  marker=dict(size=6, color=COLORS["pred"]),
195
- hovertemplate="Actual: %{x:.2f}<br>Pred: %{y:.2f}<extra></extra>",
196
  showlegend=False
197
  ))
198
  fig.add_trace(go.Scatter(
@@ -200,174 +173,126 @@ def cross_plot_interactive(actual, pred):
200
  line=dict(color=COLORS["ref"], width=1.2, dash="dash"),
201
  hoverinfo="skip", showlegend=False
202
  ))
203
-
204
  fig.update_layout(
205
- paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
206
- margin=dict(l=64, r=16, t=10, b=48),
207
- hovermode="closest", font=dict(size=FONT_SIZE),
208
- width=CROSS_W, height=CROSS_H
209
- )
210
- fig.update_xaxes(
211
- title_text="<b>Actual UCS</b>",
212
- range=[x0, x1], ticks="outside",
213
- showline=True, linewidth=1.2, linecolor="#444", mirror="allticks",
214
- showgrid=True, gridcolor="rgba(0,0,0,0.12)", tickformat=",.0f",
215
- automargin=True
216
- )
217
- fig.update_yaxes(
218
- title_text="<b>Predicted UCS</b>",
219
- range=[x0, x1], ticks="outside",
220
- showline=True, linewidth=1.2, linecolor="#444", mirror="allticks",
221
- showgrid=True, gridcolor="rgba(0,0,0,0.12)", tickformat=",.0f",
222
- scaleanchor="x", scaleratio=1, automargin=True
223
  )
 
 
 
 
 
 
 
 
224
  return fig
225
 
226
- def depth_or_index_track_interactive(df, include_actual=True):
227
  depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
228
  if depth_col is not None:
229
- y = df[depth_col]; y_label = depth_col
230
- autorange = "reversed"
231
  else:
232
- y = np.arange(1, len(df) + 1); y_label = "Point Index"
233
- autorange = "reversed"
234
 
235
  fig = go.Figure()
236
  fig.add_trace(go.Scatter(
237
  x=df["UCS_Pred"], y=y, mode="lines",
238
  line=dict(color=COLORS["pred"], width=1.8),
239
  name="UCS_Pred",
240
- hovertemplate="UCS_Pred: %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
241
  ))
242
  if include_actual and TARGET in df.columns:
243
  fig.add_trace(go.Scatter(
244
  x=df[TARGET], y=y, mode="lines",
245
  line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
246
  name="UCS (actual)",
247
- hovertemplate="UCS (actual): %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
248
  ))
249
 
250
  fig.update_layout(
251
- paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
252
- margin=dict(l=72, r=18, t=36, b=48),
253
- hovermode="closest", font=dict(size=FONT_SIZE),
254
  legend=dict(
255
  x=0.98, y=0.05, xanchor="right", yanchor="bottom",
256
- bgcolor="rgba(255,255,255,0.75)", bordercolor="#cccccc", borderwidth=1
257
  ),
258
- legend_title_text="",
259
- width=TRACK_W, height=TRACK_H
260
- )
261
- fig.update_xaxes(
262
- title_text="<b>UCS</b>", side="top",
263
- ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror="allticks",
264
- showgrid=True, gridcolor="rgba(0,0,0,0.12)", tickformat=",.0f",
265
- automargin=True
266
- )
267
- fig.update_yaxes(
268
- title_text=f"<b>{y_label}</b>", autorange=autorange,
269
- ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror="allticks",
270
- showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
271
  )
 
 
 
 
 
 
 
 
272
  return fig
273
 
274
- # ---------- Preview modal helpers (matplotlib static) ----------
275
- def make_index_tracks(df: pd.DataFrame, cols: list[str]):
276
  cols = [c for c in cols if c in df.columns]
277
  n = len(cols)
278
  if n == 0:
279
- fig, ax = plt.subplots(figsize=(4, 2))
280
- ax.text(0.5, 0.5, "No selected columns in sheet", ha="center", va="center")
281
- ax.axis("off"); return fig
282
- width_per = 2.2
283
- fig_h = 7.0
284
- fig, axes = plt.subplots(1, n, figsize=(width_per * n, fig_h), sharey=True, dpi=100)
285
  if n == 1: axes = [axes]
286
  idx = np.arange(1, len(df) + 1)
287
  for ax, col in zip(axes, cols):
288
  ax.plot(df[col], idx, '-', lw=1.4, color="#333")
289
- ax.set_xlabel(col)
290
- ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
291
  ax.grid(True, linestyle=":", alpha=0.3)
292
- # draw full box
293
- for spine in ax.spines.values(): spine.set_visible(True)
294
  axes[0].set_ylabel("Point Index")
295
  return fig
296
 
297
- def stats_table(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
298
- cols = [c for c in cols if c in df.columns]
299
- if not cols:
300
- return pd.DataFrame({"Feature": [], "Min": [], "Max": [], "Mean": [], "Std": []})
301
- out = df[cols].agg(['min', 'max', 'mean', 'std']).T
302
- out = out.rename(columns={"min": "Min", "max": "Max", "mean": "Mean", "std": "Std"})
303
- return out.reset_index().rename(columns={"index": "Feature"})
304
-
305
  @dialog("Preview data")
306
- def preview_modal_dev(book: dict[str, pd.DataFrame], feature_cols: list[str]):
307
  if not book:
308
  st.info("No data loaded yet."); return
309
- sh_train = find_sheet(book, ["Train","Training","training2","train","training"])
310
- sh_test = find_sheet(book, ["Test","Testing","testing2","test","testing"])
311
- tabs, data = [], []
312
- if sh_train: tabs.append("Train"); data.append(book[sh_train])
313
- if sh_test: tabs.append("Test"); data.append(book[sh_test])
314
- if not tabs:
315
- first_name = list(book.keys())[0]
316
- tabs = [first_name]; data = [book[first_name]]
317
- st.write("Use the tabs to switch between Train/Test views (if available).")
318
- t_objs = st.tabs(tabs)
319
- for t, df in zip(t_objs, data):
320
  with t:
 
321
  t1, t2 = st.tabs(["Tracks", "Summary"])
322
- with t1: st.pyplot(make_index_tracks(df, FEATURES), use_container_width=True)
323
- with t2: st.dataframe(stats_table(df, FEATURES), use_container_width=True)
324
-
325
- @dialog("Preview data")
326
- def preview_modal_val(book: dict[str, pd.DataFrame], feature_cols: list[str]):
327
- if not book:
328
- st.info("No data loaded yet."); return
329
- vname = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
330
- df = book[vname]
331
- t1, t2 = st.tabs(["Tracks", "Summary"])
332
- with t1: st.pyplot(make_index_tracks(df, feature_cols), use_container_width=True)
333
- with t2: st.dataframe(stats_table(df, feature_cols), use_container_width=True)
334
 
335
  # =========================
336
- # Model presence
337
  # =========================
338
- MODEL_URL = _get_model_url()
339
- def ensure_model_present() -> Path:
340
  for p in [DEFAULT_MODEL, *MODEL_FALLBACKS]:
341
- if p.exists() and p.stat().st_size > 0:
342
- return p
343
- if not MODEL_URL:
344
- return None
345
  try:
346
  import requests
347
  DEFAULT_MODEL.parent.mkdir(parents=True, exist_ok=True)
348
- with st.status("Downloading model…", expanded=False):
349
- with requests.get(MODEL_URL, stream=True, timeout=30) as r:
350
- r.raise_for_status()
351
- with open(DEFAULT_MODEL, "wb") as f:
352
- for chunk in r.iter_content(chunk_size=1<<20):
353
- if chunk: f.write(chunk)
354
  return DEFAULT_MODEL
355
- except Exception as e:
356
- st.error(f"Failed to download model from MODEL_URL: {e}")
357
  return None
358
 
359
- model_path = ensure_model_present()
360
- if not model_path:
361
- st.error("Model not found. Upload models/ucs_rf.joblib (or set MODEL_URL in Settings → Variables).")
362
  st.stop()
363
-
364
  try:
365
- model = load_model(str(model_path))
366
  except Exception as e:
367
- st.error(f"Failed to load model: {model_path}\n{e}")
368
  st.stop()
369
 
370
- # Meta override/infer
371
  meta_path = MODELS_DIR / "meta.json"
372
  if meta_path.exists():
373
  try:
@@ -375,42 +300,20 @@ if meta_path.exists():
375
  FEATURES = meta.get("features", FEATURES); TARGET = meta.get("target", TARGET)
376
  except Exception:
377
  pass
378
- else:
379
- def infer_features_from_model(m):
380
- try:
381
- if hasattr(m, "feature_names_in_") and len(getattr(m, "feature_names_in_")):
382
- return [str(x) for x in m.feature_names_in_]
383
- except Exception: pass
384
- try:
385
- if hasattr(m, "steps") and len(m.steps):
386
- last = m.steps[-1][1]
387
- if hasattr(last, "feature_names_in_") and len(last.feature_names_in_):
388
- return [str(x) for x in last.feature_names_in_]
389
- except Exception: pass
390
- return None
391
- infer = infer_features_from_model(model)
392
- if infer: FEATURES = infer
393
 
394
  # =========================
395
  # Session state
396
  # =========================
397
- if "app_step" not in st.session_state: st.session_state.app_step = "intro"
398
- if "results" not in st.session_state: st.session_state.results = {}
399
- if "train_ranges" not in st.session_state: st.session_state.train_ranges = None
400
 
401
- # Dev page file state
402
  for k, v in {
403
- "dev_ready": False,
404
- "dev_file_loaded": False,
405
- "dev_previewed": False,
406
- "dev_file_signature": None,
407
- "dev_preview_request": False,
408
- "dev_file_bytes": b"",
409
- "dev_file_name": "",
410
- "dev_file_rows": 0,
411
- "dev_file_cols": 0,
412
  }.items():
413
- if k not in st.session_state: st.session_state[k] = v
414
 
415
  # =========================
416
  # Hero
@@ -434,384 +337,200 @@ st.markdown(
434
  if st.session_state.app_step == "intro":
435
  st.header("Welcome!")
436
  st.markdown("This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data.")
437
- st.subheader("Expected Input Features (in Order)")
438
- st.markdown(
439
- "- Q, gpm — Flow rate (gallons per minute) \n"
440
- "- SPP(psi) — Stand pipe pressure \n"
441
- "- T (kft.lbf) — Torque (thousand foot-pounds) \n"
442
- "- WOB (klbf) — Weight on bit \n"
443
- "- ROP (ft/h) — Rate of penetration"
444
- )
445
  st.subheader("How It Works")
446
  st.markdown(
447
- "1. **Upload your data to build the case and preview the performance of our model.** \n"
448
- "2. Click **Run Model** to compute metrics and plots. \n"
449
- "3. Click **Proceed to Validation** to evaluate on a new dataset. \n"
450
- "4. Click **Proceed to Prediction** for prediction-only (no actual UCS). \n"
451
- "5. Export results to Excel at any time."
452
  )
453
- if st.button("Start Showcase", type="primary", key="start_showcase"):
454
  st.session_state.app_step = "dev"; st.rerun()
455
 
456
  # =========================
457
- # CASE BUILDING (Development)
458
  # =========================
459
  if st.session_state.app_step == "dev":
460
  st.sidebar.header("Case Building (Development)")
461
- dev_label = "Upload Data (Excel)" if not st.session_state.dev_file_name else "Replace data (Excel)"
462
- train_test_file = st.sidebar.file_uploader(dev_label, type=["xlsx","xls"], key="dev_upload")
463
-
464
- if train_test_file is not None:
465
- try:
466
- file_bytes = train_test_file.getvalue(); size = len(file_bytes)
467
- except Exception:
468
- file_bytes = b""; size = 0
469
- sig = (train_test_file.name, size)
470
- if sig != st.session_state.dev_file_signature and size > 0:
471
- st.session_state.dev_file_signature = sig
472
- st.session_state.dev_file_name = train_test_file.name
473
- st.session_state.dev_file_bytes = file_bytes
474
- _book_tmp = read_book_bytes(file_bytes)
475
- if _book_tmp:
476
- first_df = next(iter(_book_tmp.values()))
477
- st.session_state.dev_file_rows = int(first_df.shape[0])
478
- st.session_state.dev_file_cols = int(first_df.shape[1])
479
- st.session_state.dev_file_loaded = True
480
- st.session_state.dev_previewed = False
481
- st.session_state.dev_ready = False
482
-
483
  if st.session_state.dev_file_loaded:
484
- st.sidebar.caption(
485
- f"**Data loaded:** {st.session_state.dev_file_name} • "
486
- f"{st.session_state.dev_file_rows} rows × {st.session_state.dev_file_cols} cols"
487
- )
488
 
489
- preview_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=not st.session_state.dev_file_loaded)
490
- if preview_btn and st.session_state.dev_file_loaded:
491
- st.session_state.dev_preview_request = True
492
 
493
- run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
494
- # Always show navigation buttons (enabled feel)
495
- if st.sidebar.button("Proceed to Validation ▶", use_container_width=True):
496
- st.session_state.app_step = "validate"; st.rerun()
497
- if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True):
498
- st.session_state.app_step = "predict"; st.rerun()
499
 
500
- # helper bar at top
501
  st.subheader("Case Building (Development)")
502
- if st.session_state.dev_ready:
503
- st.success("Case has been built and results are displayed below.")
504
- elif st.session_state.dev_file_loaded and st.session_state.dev_previewed:
505
- st.info("Previewed ✓ — now click **Run Model** to build the case.")
506
  elif st.session_state.dev_file_loaded:
507
  st.info("📄 **Preview uploaded data** using the sidebar button, then click **Run Model**.")
508
  else:
509
  st.write("**Upload your data to build a case, then run the model to review development performance.**")
510
 
511
- if st.session_state.dev_preview_request and st.session_state.dev_file_bytes:
512
- _book = read_book_bytes(st.session_state.dev_file_bytes)
513
- st.session_state.dev_previewed = True
514
- st.session_state.dev_preview_request = False
515
- preview_modal_dev(_book, FEATURES)
516
-
517
- if run_btn and st.session_state.dev_file_bytes:
518
- with st.status("Processing…", expanded=False) as status:
519
- book = read_book_bytes(st.session_state.dev_file_bytes)
520
- if not book: status.update(label="Failed to read workbook.", state="error"); st.stop()
521
- status.update(label="Workbook read ✓")
522
- sh_train = find_sheet(book, ["Train","Training","training2","train","training"])
523
- sh_test = find_sheet(book, ["Test","Testing","testing2","test","testing"])
524
- if sh_train is None or sh_test is None:
525
- status.update(label="Workbook must include Train/Training/training2 and Test/Testing/testing2.", state="error"); st.stop()
526
- df_tr = book[sh_train].copy(); df_te = book[sh_test].copy()
527
- if not (ensure_cols(df_tr, FEATURES + [TARGET]) and ensure_cols(df_te, FEATURES + [TARGET])):
528
- status.update(label="Missing required columns.", state="error"); st.stop()
529
- status.update(label="Columns validated ✓"); status.update(label="Predicting…")
530
-
531
- df_tr["UCS_Pred"] = model.predict(df_tr[FEATURES])
532
- df_te["UCS_Pred"] = model.predict(df_te[FEATURES])
533
- st.session_state.results["Train"] = df_tr; st.session_state.results["Test"] = df_te
534
-
535
- st.session_state.results["metrics_train"] = {
536
- "R2": r2_score(df_tr[TARGET], df_tr["UCS_Pred"]),
537
- "RMSE": rmse(df_tr[TARGET], df_tr["UCS_Pred"]),
538
- "MAE": mean_absolute_error(df_tr[TARGET], df_tr["UCS_Pred"]),
539
- }
540
- st.session_state.results["metrics_test"] = {
541
- "R2": r2_score(df_te[TARGET], df_te["UCS_Pred"]),
542
- "RMSE": rmse(df_te[TARGET], df_te["UCS_Pred"]),
543
- "MAE": mean_absolute_error(df_te[TARGET], df_te["UCS_Pred"]),
544
- }
545
-
546
- tr_min = df_tr[FEATURES].min().to_dict(); tr_max = df_tr[FEATURES].max().to_dict()
547
- st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
548
-
549
- st.session_state.dev_ready = True
550
- status.update(label="Done ✓", state="complete"); st.rerun()
551
-
552
- # Results
553
- if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
554
  tab1, tab2 = st.tabs(["Training", "Testing"])
555
 
556
- def dev_block(df, metrics):
557
  c1,c2,c3 = st.columns(3)
558
- c1.metric("R²", f"{metrics['R2']:.4f}")
559
- c2.metric("RMSE", f"{metrics['RMSE']:.4f}")
560
- c3.metric("MAE", f"{metrics['MAE']:.4f}")
561
-
562
- # layout: spacer | crossplot | spacer | track
563
- sp_left, col_cross, sp_mid, col_track, sp_right = st.columns([0.2, 0.6, 0.05, 0.35, 0.2])
564
- with col_cross:
565
- st.plotly_chart(
566
- cross_plot_interactive(df[TARGET], df["UCS_Pred"]),
567
- use_container_width=False,
568
- config={"displayModeBar": False, "scrollZoom": True}
569
- )
570
- with col_track:
571
- st.plotly_chart(
572
- depth_or_index_track_interactive(df, include_actual=True),
573
- use_container_width=False,
574
- config={"displayModeBar": False, "scrollZoom": True}
575
- )
576
-
577
- if "Train" in st.session_state.results:
578
- with tab1:
579
- dev_block(st.session_state.results["Train"], st.session_state.results["metrics_train"])
580
- if "Test" in st.session_state.results:
581
- with tab2:
582
- dev_block(st.session_state.results["Test"], st.session_state.results["metrics_test"])
583
 
584
- st.markdown("---")
585
- sheets = {}; rows = []
586
  if "Train" in st.session_state.results:
587
- sheets["Train_with_pred"] = st.session_state.results["Train"]
588
- rows.append({"Split":"Train", **{k:round(v,6) for k,v in st.session_state.results["metrics_train"].items()}})
589
  if "Test" in st.session_state.results:
590
- sheets["Test_with_pred"] = st.session_state.results["Test"]
591
- rows.append({"Split":"Test", **{k:round(v,6) for k,v in st.session_state.results["metrics_test"].items()}})
592
-
593
- summary_df = pd.DataFrame(rows) if rows else None
594
- try:
595
- buf = io.BytesIO()
596
- with pd.ExcelWriter(buf, engine="openpyxl") as xw:
597
- for name, frame in sheets.items():
598
- frame.to_excel(xw, sheet_name=name[:31], index=False)
599
- if summary_df is not None:
600
- summary_df.to_excel(xw, sheet_name="Summary", index=False)
601
- st.download_button(
602
- "Export Development Results to Excel",
603
- data=buf.getvalue(),
604
- file_name="UCS_Dev_Results.xlsx",
605
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
606
- )
607
- except Exception as e:
608
- st.warning(str(e))
609
 
610
  # =========================
611
  # VALIDATION (with actual UCS)
612
  # =========================
613
  if st.session_state.app_step == "validate":
614
  st.sidebar.header("Validate the Model")
615
- validation_file = st.sidebar.file_uploader("Upload Validation Excel", type=["xlsx","xls"], key="val_upload")
616
- if validation_file is not None:
617
- _book_tmp = read_book_bytes(validation_file.getvalue())
618
- if _book_tmp:
619
- first_df = next(iter(_book_tmp.values()))
620
- st.sidebar.caption(f"**Data loaded:** {validation_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
621
-
622
- preview_val_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=(validation_file is None))
623
- if preview_val_btn and validation_file is not None:
624
- _book = read_book_bytes(validation_file.getvalue())
625
- preview_modal_val(_book, FEATURES)
626
-
627
- predict_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
628
- if st.sidebar.button("⬅ Back to Case Building", use_container_width=True):
629
- st.session_state.app_step = "dev"; st.rerun()
630
- if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True):
631
- st.session_state.app_step = "predict"; st.rerun()
632
 
633
  st.subheader("Validate the Model")
634
- st.write("Upload a dataset containing the same feature columns and **UCS** to evaluate model performance.")
635
-
636
- if predict_btn and validation_file is not None:
637
- with st.status("Predicting…", expanded=False) as status:
638
- vbook = read_book_bytes(validation_file.getvalue())
639
- if not vbook: status.update(label="Could not read the Validation Excel.", state="error"); st.stop()
640
- status.update(label="Workbook read ")
641
- vname = find_sheet(vbook, ["Validation","Validate","validation2","Val","val"]) or list(vbook.keys())[0]
642
- df_val = vbook[vname].copy()
643
- if not ensure_cols(df_val, FEATURES + [TARGET]): status.update(label="Missing required columns.", state="error"); st.stop()
644
- status.update(label="Columns validated ✓")
645
- df_val["UCS_Pred"] = model.predict(df_val[FEATURES])
646
- st.session_state.results["Validate"] = df_val
647
-
648
- ranges = st.session_state.train_ranges; oor_table = None; oor_pct = 0.0
649
- if ranges:
650
- viol = {f: (df_val[f] < ranges[f][0]) | (df_val[f] > ranges[f][1]) for f in FEATURES}
651
- any_viol = pd.DataFrame(viol).any(axis=1); oor_pct = float(any_viol.mean()*100.0)
652
- if any_viol.any():
653
- offenders = df_val.loc[any_viol, FEATURES].copy()
654
- offenders["Violations"] = pd.DataFrame(viol).loc[any_viol].apply(lambda r: ", ".join([c for c,v in r.items() if v]), axis=1)
655
- offenders.index = offenders.index + 1; oor_table = offenders
656
-
657
- metrics_val = {
658
- "R2": r2_score(df_val[TARGET], df_val["UCS_Pred"]),
659
- "RMSE": rmse(df_val[TARGET], df_val["UCS_Pred"]),
660
- "MAE": mean_absolute_error(df_val[TARGET], df_val["UCS_Pred"])
661
- }
662
- st.session_state.results["metrics_val"] = metrics_val
663
- st.session_state.results["summary_val"] = {
664
- "n_points": len(df_val),
665
- "pred_min": float(df_val["UCS_Pred"].min()),
666
- "pred_max": float(df_val["UCS_Pred"].max()),
667
- "oor_pct": oor_pct
668
- }
669
- st.session_state.results["oor_table"] = oor_table
670
- status.update(label="Predictions ready ✓", state="complete")
671
 
672
  if "Validate" in st.session_state.results:
673
- metrics_val = st.session_state.results.get("metrics_val", {})
674
- c1, c2, c3 = st.columns(3)
675
- c1.metric("R²", f"{metrics_val.get('R2',0):.4f}")
676
- c2.metric("RMSE", f"{metrics_val.get('RMSE',0):.4f}")
677
- c3.metric("MAE", f"{metrics_val.get('MAE',0):.4f}")
678
-
679
- left_sp, col_cross, sp_mid, col_track, sp_right = st.columns([0.2, 0.6, 0.05, 0.35, 0.2])
680
- with col_cross:
681
- st.plotly_chart(
682
- cross_plot_interactive(
683
- st.session_state.results["Validate"][TARGET],
684
- st.session_state.results["Validate"]["UCS_Pred"]),
685
- use_container_width=False,
686
- config={"displayModeBar": False, "scrollZoom": True}
687
- )
688
- with col_track:
689
- st.plotly_chart(
690
- depth_or_index_track_interactive(
691
- st.session_state.results["Validate"], include_actual=True),
692
- use_container_width=False,
693
- config={"displayModeBar": False, "scrollZoom": True}
694
- )
695
-
696
- sv = st.session_state.results["summary_val"]; oor_table = st.session_state.results.get("oor_table")
697
- if sv["oor_pct"] > 0:
698
- st.warning("Some validation inputs fall outside the **training min–max** ranges. Interpret predictions with caution.")
699
- if oor_table is not None:
700
  st.write("*Out-of-range rows (vs. Training min–max):*")
701
- st.dataframe(oor_table, use_container_width=True)
702
-
703
- st.markdown("---")
704
- sheets = {"Validate_with_pred": st.session_state.results["Validate"]}
705
- rows = []
706
- for name, key in [("Train","metrics_train"), ("Test","metrics_test"), ("Validate","metrics_val")]:
707
- m = st.session_state.results.get(key)
708
- if m: rows.append({"Split": name, **{k: round(v,6) for k,v in m.items()}})
709
- summary_df = pd.DataFrame(rows) if rows else None
710
- try:
711
- buf = io.BytesIO()
712
- with pd.ExcelWriter(buf, engine="openpyxl") as xw:
713
- for name, frame in sheets.items():
714
- frame.to_excel(xw, sheet_name=name[:31], index=False)
715
- if summary_df is not None:
716
- summary_df.to_excel(xw, sheet_name="Summary", index=False)
717
- st.download_button(
718
- "Export Validation Results to Excel",
719
- data=buf.getvalue(),
720
- file_name="UCS_Validation_Results.xlsx",
721
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
722
- )
723
- except Exception as e:
724
- st.warning(str(e))
725
 
726
  # =========================
727
- # PREDICTION-ONLY (no actual UCS)
728
  # =========================
729
  if st.session_state.app_step == "predict":
730
  st.sidebar.header("Prediction (No Actual UCS)")
731
- pred_file = st.sidebar.file_uploader("Upload Prediction Excel", type=["xlsx","xls"], key="pred_upload")
732
- if pred_file is not None:
733
- _book_tmp = read_book_bytes(pred_file.getvalue())
734
- if _book_tmp:
735
- first_df = next(iter(_book_tmp.values()))
736
- st.sidebar.caption(f"**Data loaded:** {pred_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
737
-
738
- preview_pred_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=(pred_file is None))
739
- if preview_pred_btn and pred_file is not None:
740
- _book = read_book_bytes(pred_file.getvalue())
741
- preview_modal_val(_book, FEATURES)
742
-
743
- do_pred = st.sidebar.button("Predict", type="primary", use_container_width=True)
744
- if st.sidebar.button("⬅ Back to Case Building", use_container_width=True):
745
- st.session_state.app_step = "dev"; st.rerun()
746
 
747
  st.subheader("Prediction")
748
- st.write("Upload a dataset with the feature columns (no **UCS**). You’ll get predicted UCS, a UCS track, and a compact summary.")
749
-
750
- if do_pred and pred_file is not None:
751
- with st.status("Predicting…", expanded=False) as status:
752
- pbook = read_book_bytes(pred_file.getvalue())
753
- if not pbook: status.update(label="Could not read the Excel.", state="error"); st.stop()
754
- status.update(label="Workbook read ✓")
755
- pname = list(pbook.keys())[0]
756
- dfp = pbook[pname].copy()
757
- if not ensure_cols(dfp, FEATURES): status.update(label="Missing required columns.", state="error"); st.stop()
758
- status.update(label="Columns validated ✓")
759
- dfp["UCS_Pred"] = model.predict(dfp[FEATURES])
760
- st.session_state.results["PredictOnly"] = dfp
761
-
762
- ranges = st.session_state.train_ranges; oor_pct = 0.0
763
- if ranges:
764
- any_viol = pd.DataFrame({f: (dfp[f] < ranges[f][0]) | (dfp[f] > ranges[f][1]) for f in FEATURES}).any(axis=1)
765
- oor_pct = float(any_viol.mean()*100.0)
766
-
767
- st.session_state.results["summary_predonly"] = {
768
- "n_points": len(dfp),
769
- "pred_min": float(dfp["UCS_Pred"].min()),
770
- "pred_max": float(dfp["UCS_Pred"].max()),
771
- "pred_mean": float(dfp["UCS_Pred"].mean()),
772
- "pred_std": float(dfp["UCS_Pred"].std(ddof=0)),
773
- "oor_pct": oor_pct,
774
- }
775
- status.update(label="Predictions ready ✓", state="complete")
776
 
777
  if "PredictOnly" in st.session_state.results:
778
- dfp = st.session_state.results["PredictOnly"]
779
- sv = st.session_state.results["summary_predonly"]
780
-
781
- # Left summary table (in place of cross-plot), right UCS track
782
- left_sp, col_summary, sp_mid, col_track, sp_right = st.columns([0.18, 0.42, 0.05, 0.35, 0.2])
783
 
784
- with col_summary:
785
- tbl = pd.DataFrame({
 
786
  "Metric": ["# points","Pred min","Pred max","Pred mean","Pred std","OOR %"],
787
- "Value": [sv["n_points"], sv["pred_min"], sv["pred_max"], sv["pred_mean"], sv["pred_std"], f'{sv["oor_pct"]:.1f}%']
788
  })
789
  st.success("Predictions ready ✓")
790
- st.dataframe(tbl, use_container_width=True, hide_index=True)
791
-
792
- st.caption("**★ OOR** = percentage of rows whose input features fall outside the training min–max range.")
793
-
794
- with col_track:
795
- st.plotly_chart(
796
- depth_or_index_track_interactive(dfp, include_actual=False),
797
- use_container_width=False,
798
- config={"displayModeBar": False, "scrollZoom": True}
799
- )
800
-
801
- st.markdown("---")
802
- try:
803
- buf = io.BytesIO()
804
- with pd.ExcelWriter(buf, engine="openpyxl") as xw:
805
- dfp.to_excel(xw, sheet_name="Predictions", index=False)
806
- pd.DataFrame([sv]).to_excel(xw, sheet_name="Summary", index=False)
807
- st.download_button(
808
- "Export Prediction Results to Excel",
809
- data=buf.getvalue(),
810
- file_name="UCS_Prediction_Results.xlsx",
811
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
812
- )
813
- except Exception as e:
814
- st.warning(str(e))
815
 
816
  # =========================
817
  # Footer
 
6
  import numpy as np
7
  import joblib
8
 
9
+ # matplotlib only for PREVIEW modal
10
  import matplotlib
11
  matplotlib.use("Agg")
12
  import matplotlib.pyplot as plt
 
15
  from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
16
 
17
  # =========================
18
+ # Constants (simple & robust)
19
  # =========================
20
  FEATURES = ["Q, gpm", "SPP(psi)", "T (kft.lbf)", "WOB (klbf)", "ROP (ft/h)"]
21
  TARGET = "UCS"
 
25
 
26
  COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
27
 
28
+ # exact pixel sizes (no container stretching)
29
+ CROSS_W = 420; CROSS_H = 420 # square cross-plot
30
+ TRACK_W = 220; TRACK_H = 700 # match preview strip
31
+ FONT_SZ = 13
32
 
33
  # =========================
34
+ # Page / CSS
35
  # =========================
36
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
 
 
37
  st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
38
  st.markdown(
39
  """
40
  <style>
41
+ .stApp { background:#fff; }
42
+ section[data-testid="stSidebar"] { background:#F6F9FC; }
43
+ .block-container { padding-top:.5rem; padding-bottom:.5rem; }
44
+ .stButton>button { background:#007bff; color:#fff; font-weight:600; border-radius:8px; border:none; }
45
+ .stButton>button:hover { background:#0056b3; }
46
+ .st-hero { display:flex; align-items:center; gap:16px; padding-top: 4px; }
47
+ .st-hero .brand { width:110px; height:110px; object-fit:contain; }
48
+ .st-hero h1 { margin:0; line-height:1.05; }
49
+ .st-hero .tagline { margin:2px 0 0 2px; color:#6b7280; font-size:1.05rem; font-style:italic; }
50
+ [data-testid="stBlock"]{ margin-top:0 !important; }
 
 
 
51
  </style>
52
  """,
53
  unsafe_allow_html=True
 
64
  except Exception:
65
  return ""
66
 
67
+ def add_password_gate() -> None:
 
68
  try:
69
  required = st.secrets.get("APP_PASSWORD", "")
70
  except Exception:
 
82
  </div>
83
  <div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected Area</div>
84
  <div style="color:#6b7280;margin-bottom:14px;">
85
+ Set <code>APP_PASSWORD</code> in <b>Settings → Secrets</b> (or environment) and restart.
 
86
  </div>
87
  """,
88
  unsafe_allow_html=True,
 
90
  st.stop()
91
 
92
  if st.session_state.get("auth_ok", False):
93
+ return
94
 
95
  st.markdown(
96
  f"""
 
102
  </div>
103
  </div>
104
  <div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected</div>
105
+ <div style="color:#6b7280;margin-bottom:14px;">Please enter your access key to continue.</div>
 
 
106
  """,
107
  unsafe_allow_html=True
108
  )
109
 
110
  pwd = st.text_input("Access key", type="password", placeholder="••••••••")
111
+ if st.button("Unlock", type="primary"):
112
+ if pwd == required:
113
+ st.session_state.auth_ok = True
114
+ st.rerun()
115
+ else:
116
+ st.error("Incorrect key.")
 
 
 
117
  st.stop()
118
 
 
119
  add_password_gate()
120
 
121
  # =========================
122
+ # Utilities
123
  # =========================
124
  try:
125
  dialog = st.dialog
 
132
  return wrapper
133
  return deco
134
 
 
 
 
135
  def rmse(y_true, y_pred): return float(np.sqrt(mean_squared_error(y_true, y_pred)))
136
 
 
 
 
 
 
 
 
137
  @st.cache_resource(show_spinner=False)
138
  def load_model(model_path: str):
139
  return joblib.load(model_path)
 
144
  xl = pd.ExcelFile(bio)
145
  return {sh: xl.parse(sh) for sh in xl.sheet_names}
146
 
147
+ def read_book_bytes(b: bytes): return parse_excel(b) if b else {}
148
+ def ensure_cols(df, cols): return not [c for c in cols if c not in df.columns] or False
 
 
 
 
149
  def find_sheet(book, names):
150
  low2orig = {k.lower(): k for k in book.keys()}
151
  for nm in names:
152
  if nm.lower() in low2orig: return low2orig[nm.lower()]
153
  return None
154
 
155
+ # ---------- Plot builders (fixed pixel sizes; full outline; crisp) ----------
156
+ def cross_plot(actual, pred):
157
  a = pd.Series(actual).astype(float)
158
  p = pd.Series(pred).astype(float)
159
  lo = float(np.nanmin([a.min(), p.min()]))
 
165
  fig.add_trace(go.Scatter(
166
  x=a, y=p, mode="markers",
167
  marker=dict(size=6, color=COLORS["pred"]),
168
+ hovertemplate="Actual: %{x:.0f}<br>Pred: %{y:.0f}<extra></extra>",
169
  showlegend=False
170
  ))
171
  fig.add_trace(go.Scatter(
 
173
  line=dict(color=COLORS["ref"], width=1.2, dash="dash"),
174
  hoverinfo="skip", showlegend=False
175
  ))
 
176
  fig.update_layout(
177
+ width=CROSS_W, height=CROSS_H, paper_bgcolor="#fff", plot_bgcolor="#fff",
178
+ margin=dict(l=64, r=18, t=10, b=48), hovermode="closest",
179
+ font=dict(size=FONT_SZ)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  )
181
+ fig.update_xaxes(title_text="<b>Actual UCS</b>", range=[x0, x1],
182
+ ticks="outside", tickformat=",.0f",
183
+ showline=True, linewidth=1.2, linecolor="#444", mirror=True, showgrid=True, gridcolor="rgba(0,0,0,0.12)",
184
+ automargin=True)
185
+ fig.update_yaxes(title_text="<b>Predicted UCS</b>", range=[x0, x1],
186
+ ticks="outside", tickformat=",.0f",
187
+ showline=True, linewidth=1.2, linecolor="#444", mirror=True, showgrid=True, gridcolor="rgba(0,0,0,0.12)",
188
+ scaleanchor="x", scaleratio=1, automargin=True)
189
  return fig
190
 
191
+ def track_plot(df, include_actual=True):
192
  depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
193
  if depth_col is not None:
194
+ y = df[depth_col]; ylab = depth_col; autorange = "reversed"
 
195
  else:
196
+ y = np.arange(1, len(df) + 1); ylab = "Point Index"; autorange = "reversed"
 
197
 
198
  fig = go.Figure()
199
  fig.add_trace(go.Scatter(
200
  x=df["UCS_Pred"], y=y, mode="lines",
201
  line=dict(color=COLORS["pred"], width=1.8),
202
  name="UCS_Pred",
203
+ hovertemplate="UCS_Pred: %{x:.0f}<br>"+ylab+": %{y}<extra></extra>"
204
  ))
205
  if include_actual and TARGET in df.columns:
206
  fig.add_trace(go.Scatter(
207
  x=df[TARGET], y=y, mode="lines",
208
  line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
209
  name="UCS (actual)",
210
+ hovertemplate="UCS (actual): %{x:.0f}<br>"+ylab+": %{y}<extra></extra>"
211
  ))
212
 
213
  fig.update_layout(
214
+ width=TRACK_W, height=TRACK_H, paper_bgcolor="#fff", plot_bgcolor="#fff",
215
+ margin=dict(l=72, r=18, t=36, b=48), hovermode="closest",
216
+ font=dict(size=FONT_SZ),
217
  legend=dict(
218
  x=0.98, y=0.05, xanchor="right", yanchor="bottom",
219
+ bgcolor="rgba(255,255,255,0.75)", bordercolor="#ccc", borderwidth=1
220
  ),
221
+ legend_title_text=""
 
 
 
 
 
 
 
 
 
 
 
 
222
  )
223
+ fig.update_xaxes(title_text="<b>UCS</b>", side="top",
224
+ ticks="outside", tickformat=",.0f",
225
+ showline=True, linewidth=1.2, linecolor="#444", mirror=True, showgrid=True, gridcolor="rgba(0,0,0,0.12)",
226
+ automargin=True)
227
+ fig.update_yaxes(title_text=f"<b>{ylab}</b>", autorange=autorange,
228
+ ticks="outside",
229
+ showline=True, linewidth=1.2, linecolor="#444", mirror=True, showgrid=True, gridcolor="rgba(0,0,0,0.12)",
230
+ automargin=True)
231
  return fig
232
 
233
+ # ---------- Preview modal (matplotlib) ----------
234
+ def preview_tracks(df: pd.DataFrame, cols: list[str]):
235
  cols = [c for c in cols if c in df.columns]
236
  n = len(cols)
237
  if n == 0:
238
+ fig, ax = plt.subplots(figsize=(4, 2)); ax.text(0.5,0.5,"No selected columns",ha="center",va="center"); ax.axis("off"); return fig
239
+ fig, axes = plt.subplots(1, n, figsize=(2.2*n, 7.0), sharey=True, dpi=100)
 
 
 
 
240
  if n == 1: axes = [axes]
241
  idx = np.arange(1, len(df) + 1)
242
  for ax, col in zip(axes, cols):
243
  ax.plot(df[col], idx, '-', lw=1.4, color="#333")
244
+ ax.set_xlabel(col); ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
 
245
  ax.grid(True, linestyle=":", alpha=0.3)
246
+ for s in ax.spines.values(): s.set_visible(True)
 
247
  axes[0].set_ylabel("Point Index")
248
  return fig
249
 
 
 
 
 
 
 
 
 
250
  @dialog("Preview data")
251
+ def preview_modal(book: dict[str, pd.DataFrame]):
252
  if not book:
253
  st.info("No data loaded yet."); return
254
+ names = list(book.keys())
255
+ tabs = st.tabs(names)
256
+ for t, name in zip(tabs, names):
 
 
 
 
 
 
 
 
257
  with t:
258
+ df = book[name]
259
  t1, t2 = st.tabs(["Tracks", "Summary"])
260
+ with t1: st.pyplot(preview_tracks(df, FEATURES), use_container_width=True)
261
+ with t2:
262
+ tbl = df[FEATURES].agg(['min','max','mean','std']).T.rename(columns={"min":"Min","max":"Max","mean":"Mean","std":"Std"})
263
+ st.dataframe(tbl.reset_index(names="Feature"), use_container_width=True)
 
 
 
 
 
 
 
 
264
 
265
  # =========================
266
+ # Load model (simple)
267
  # =========================
268
+ def ensure_model() -> Path|None:
 
269
  for p in [DEFAULT_MODEL, *MODEL_FALLBACKS]:
270
+ if p.exists() and p.stat().st_size > 0: return p
271
+ url = os.environ.get("MODEL_URL", "")
272
+ if not url: return None
 
273
  try:
274
  import requests
275
  DEFAULT_MODEL.parent.mkdir(parents=True, exist_ok=True)
276
+ with requests.get(url, stream=True, timeout=30) as r:
277
+ r.raise_for_status()
278
+ with open(DEFAULT_MODEL, "wb") as f:
279
+ for chunk in r.iter_content(1<<20):
280
+ if chunk: f.write(chunk)
 
281
  return DEFAULT_MODEL
282
+ except Exception:
 
283
  return None
284
 
285
+ mpath = ensure_model()
286
+ if not mpath:
287
+ st.error("Model not found. Upload models/ucs_rf.joblib (or set MODEL_URL).")
288
  st.stop()
 
289
  try:
290
+ model = load_model(str(mpath))
291
  except Exception as e:
292
+ st.error(f"Failed to load model: {e}")
293
  st.stop()
294
 
295
+ # Try to pull features from model if provided
296
  meta_path = MODELS_DIR / "meta.json"
297
  if meta_path.exists():
298
  try:
 
300
  FEATURES = meta.get("features", FEATURES); TARGET = meta.get("target", TARGET)
301
  except Exception:
302
  pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
  # =========================
305
  # Session state
306
  # =========================
307
+ st.session_state.setdefault("app_step", "intro")
308
+ st.session_state.setdefault("results", {})
309
+ st.session_state.setdefault("train_ranges", None)
310
 
311
+ # dev file state
312
  for k, v in {
313
+ "dev_file_name":"", "dev_file_bytes":b"", "dev_file_loaded":False,
314
+ "dev_preview":False
 
 
 
 
 
 
 
315
  }.items():
316
+ st.session_state.setdefault(k, v)
317
 
318
  # =========================
319
  # Hero
 
337
  if st.session_state.app_step == "intro":
338
  st.header("Welcome!")
339
  st.markdown("This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data.")
 
 
 
 
 
 
 
 
340
  st.subheader("How It Works")
341
  st.markdown(
342
+ "1) **Upload your data to build the case and preview the performance of our model.** \n"
343
+ "2) Click **Run Model** to compute metrics and plots. \n"
344
+ "3) **Proceed to Validation** (with actual UCS) or **Proceed to Prediction** (no UCS)."
 
 
345
  )
346
+ if st.button("Start Showcase", type="primary"):
347
  st.session_state.app_step = "dev"; st.rerun()
348
 
349
  # =========================
350
+ # CASE BUILDING
351
  # =========================
352
  if st.session_state.app_step == "dev":
353
  st.sidebar.header("Case Building (Development)")
354
+ up = st.sidebar.file_uploader("Upload Train/Test Excel", type=["xlsx","xls"])
355
+ if up is not None:
356
+ st.session_state.dev_file_bytes = up.getvalue()
357
+ st.session_state.dev_file_name = up.name
358
+ st.session_state.dev_file_loaded = True
359
+ st.session_state.dev_preview = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  if st.session_state.dev_file_loaded:
361
+ tmp = read_book_bytes(st.session_state.dev_file_bytes)
362
+ if tmp:
363
+ df0 = next(iter(tmp.values()))
364
+ st.sidebar.caption(f"**Data loaded:** {st.session_state.dev_file_name} • {df0.shape[0]} rows × {df0.shape[1]} cols")
365
 
366
+ if st.sidebar.button("Preview data", use_container_width=True, disabled=not st.session_state.dev_file_loaded):
367
+ preview_modal(read_book_bytes(st.session_state.dev_file_bytes))
368
+ st.session_state.dev_preview = True
369
 
370
+ run = st.sidebar.button("Run Model", type="primary", use_container_width=True)
371
+ # always available nav
372
+ if st.sidebar.button("Proceed to Validation ▶", use_container_width=True): st.session_state.app_step="validate"; st.rerun()
373
+ if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
 
 
374
 
 
375
  st.subheader("Case Building (Development)")
376
+ if st.session_state.dev_file_loaded and st.session_state.dev_preview:
377
+ st.info("Previewed now click **Run Model**.")
 
 
378
  elif st.session_state.dev_file_loaded:
379
  st.info("📄 **Preview uploaded data** using the sidebar button, then click **Run Model**.")
380
  else:
381
  st.write("**Upload your data to build a case, then run the model to review development performance.**")
382
 
383
+ if run and st.session_state.dev_file_bytes:
384
+ book = read_book_bytes(st.session_state.dev_file_bytes)
385
+ sh_train = find_sheet(book, ["Train","Training","training2","train","training"])
386
+ sh_test = find_sheet(book, ["Test","Testing","testing2","test","testing"])
387
+ if sh_train is None or sh_test is None:
388
+ st.error("Workbook must include Train/Training/training2 and Test/Testing/testing2 sheets."); st.stop()
389
+ tr = book[sh_train].copy(); te = book[sh_test].copy()
390
+ if not (ensure_cols(tr, FEATURES+[TARGET]) and ensure_cols(te, FEATURES+[TARGET])):
391
+ st.error("Missing required columns."); st.stop()
392
+ tr["UCS_Pred"] = model.predict(tr[FEATURES])
393
+ te["UCS_Pred"] = model.predict(te[FEATURES])
394
+
395
+ st.session_state.results["Train"]=tr; st.session_state.results["Test"]=te
396
+ st.session_state.results["m_train"]={"R2":r2_score(tr[TARGET],tr["UCS_Pred"]), "RMSE":rmse(tr[TARGET],tr["UCS_Pred"]), "MAE":mean_absolute_error(tr[TARGET],tr["UCS_Pred"])}
397
+ st.session_state.results["m_test"] ={"R2":r2_score(te[TARGET],te["UCS_Pred"]), "RMSE":rmse(te[TARGET],te["UCS_Pred"]), "MAE":mean_absolute_error(te[TARGET],te["UCS_Pred"])}
398
+
399
+ tr_min = tr[FEATURES].min().to_dict(); tr_max = tr[FEATURES].max().to_dict()
400
+ st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
401
+ st.success("Case has been built and results are displayed below.")
402
+
403
+ if "Train" in st.session_state.results or "Test" in st.session_state.results:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  tab1, tab2 = st.tabs(["Training", "Testing"])
405
 
406
+ def dev_block(df, m):
407
  c1,c2,c3 = st.columns(3)
408
+ c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
409
+ col_left, col_right = st.columns(2)
410
+ with col_left:
411
+ st.plotly_chart(cross_plot(df[TARGET], df["UCS_Pred"]),
412
+ use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
413
+ with col_right:
414
+ st.plotly_chart(track_plot(df, include_actual=True),
415
+ use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
 
 
 
417
  if "Train" in st.session_state.results:
418
+ with tab1: dev_block(st.session_state.results["Train"], st.session_state.results["m_train"])
 
419
  if "Test" in st.session_state.results:
420
+ with tab2: dev_block(st.session_state.results["Test"], st.session_state.results["m_test"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
422
  # =========================
423
  # VALIDATION (with actual UCS)
424
  # =========================
425
  if st.session_state.app_step == "validate":
426
  st.sidebar.header("Validate the Model")
427
+ up = st.sidebar.file_uploader("Upload Validation Excel", type=["xlsx","xls"])
428
+ if up is not None:
429
+ book = read_book_bytes(up.getvalue())
430
+ if book:
431
+ df0 = next(iter(book.values()))
432
+ st.sidebar.caption(f"**Data loaded:** {up.name} • {df0.shape[0]} rows × {df0.shape[1]} cols")
433
+ if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
434
+ preview_modal(read_book_bytes(up.getvalue()))
435
+ go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
436
+ if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
437
+ if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
 
 
 
 
 
 
438
 
439
  st.subheader("Validate the Model")
440
+ st.write("Upload a dataset with the same **features** and **UCS** to evaluate performance.")
441
+
442
+ if go_btn and up is not None:
443
+ book = read_book_bytes(up.getvalue())
444
+ name = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
445
+ df = book[name].copy()
446
+ if not ensure_cols(df, FEATURES+[TARGET]): st.error("Missing required columns."); st.stop()
447
+ df["UCS_Pred"] = model.predict(df[FEATURES])
448
+ st.session_state.results["Validate"]=df
449
+
450
+ ranges = st.session_state.train_ranges; oor_pct = 0.0; tbl=None
451
+ if ranges:
452
+ any_viol = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).any(axis=1)
453
+ oor_pct = float(any_viol.mean()*100.0)
454
+ if any_viol.any():
455
+ tbl = df.loc[any_viol, FEATURES].copy()
456
+ tbl["Violations"] = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).loc[any_viol].apply(lambda r:", ".join([c for c,v in r.items() if v]), axis=1)
457
+ st.session_state.results["m_val"]={"R2":r2_score(df[TARGET],df["UCS_Pred"]), "RMSE":rmse(df[TARGET],df["UCS_Pred"]), "MAE":mean_absolute_error(df[TARGET],df["UCS_Pred"])}
458
+ st.session_state.results["sv_val"]={"n":len(df),"pred_min":float(df["UCS_Pred"].min()),"pred_max":float(df["UCS_Pred"].max()),"oor":oor_pct}
459
+ st.session_state.results["oor_tbl"]=tbl
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
 
461
  if "Validate" in st.session_state.results:
462
+ m = st.session_state.results["m_val"]; sv = st.session_state.results["sv_val"]
463
+ c1,c2,c3 = st.columns(3)
464
+ c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
465
+
466
+ col_left, col_right = st.columns(2)
467
+ with col_left:
468
+ st.plotly_chart(cross_plot(st.session_state.results["Validate"][TARGET],
469
+ st.session_state.results["Validate"]["UCS_Pred"]),
470
+ use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
471
+ with col_right:
472
+ st.plotly_chart(track_plot(st.session_state.results["Validate"], include_actual=True),
473
+ use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
474
+
475
+ if sv["oor"] > 0: st.warning("Some inputs fall outside **training min–max** ranges.")
476
+ if st.session_state.results["oor_tbl"] is not None:
 
 
 
 
 
 
 
 
 
 
 
 
477
  st.write("*Out-of-range rows (vs. Training min–max):*")
478
+ st.dataframe(st.session_state.results["oor_tbl"], use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
 
480
  # =========================
481
+ # PREDICTION (no actual UCS)
482
  # =========================
483
  if st.session_state.app_step == "predict":
484
  st.sidebar.header("Prediction (No Actual UCS)")
485
+ up = st.sidebar.file_uploader("Upload Prediction Excel", type=["xlsx","xls"])
486
+ if up is not None:
487
+ book = read_book_bytes(up.getvalue())
488
+ if book:
489
+ df0 = next(iter(book.values()))
490
+ st.sidebar.caption(f"**Data loaded:** {up.name} • {df0.shape[0]} rows × {df0.shape[1]} cols")
491
+ if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
492
+ preview_modal(read_book_bytes(up.getvalue()))
493
+ go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
494
+ if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
 
 
 
 
 
495
 
496
  st.subheader("Prediction")
497
+ st.write("Upload a dataset with the feature columns (no **UCS**).")
498
+
499
+ if go_btn and up is not None:
500
+ book = read_book_bytes(up.getvalue()); name = list(book.keys())[0]
501
+ df = book[name].copy()
502
+ if not ensure_cols(df, FEATURES): st.error("Missing required columns."); st.stop()
503
+ df["UCS_Pred"] = model.predict(df[FEATURES])
504
+ st.session_state.results["PredictOnly"]=df
505
+
506
+ ranges = st.session_state.train_ranges; oor_pct = 0.0
507
+ if ranges:
508
+ any_viol = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).any(axis=1)
509
+ oor_pct = float(any_viol.mean()*100.0)
510
+ st.session_state.results["sv_pred"]={
511
+ "n":len(df),
512
+ "pred_min":float(df["UCS_Pred"].min()),
513
+ "pred_max":float(df["UCS_Pred"].max()),
514
+ "pred_mean":float(df["UCS_Pred"].mean()),
515
+ "pred_std":float(df["UCS_Pred"].std(ddof=0)),
516
+ "oor":oor_pct
517
+ }
 
 
 
 
 
 
 
518
 
519
  if "PredictOnly" in st.session_state.results:
520
+ df = st.session_state.results["PredictOnly"]; sv = st.session_state.results["sv_pred"]
 
 
 
 
521
 
522
+ col_left, col_right = st.columns(2)
523
+ with col_left:
524
+ table = pd.DataFrame({
525
  "Metric": ["# points","Pred min","Pred max","Pred mean","Pred std","OOR %"],
526
+ "Value": [sv["n"], sv["pred_min"], sv["pred_max"], sv["pred_mean"], sv["pred_std"], f'{sv["oor"]:.1f}%']
527
  })
528
  st.success("Predictions ready ✓")
529
+ st.dataframe(table, use_container_width=True, hide_index=True)
530
+ st.caption("**★ OOR** = % of rows whose input features fall outside the training min–max range.")
531
+ with col_right:
532
+ st.plotly_chart(track_plot(df, include_actual=False),
533
+ use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
  # =========================
536
  # Footer