UCS2014 commited on
Commit
d99ed4d
·
verified ·
1 Parent(s): 6dc8e42

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +214 -81
app.py CHANGED
@@ -26,33 +26,35 @@ MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
26
  COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
27
 
28
  # =========================
29
- # Page / Theme (must be first)
30
  # =========================
31
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
32
 
33
- # ---------- utilities used by the gate ----------
34
  def inline_logo(path="logo.png") -> str:
35
  try:
36
  p = Path(path)
37
- if p.exists():
38
- return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
39
  except Exception:
40
- pass
41
- return ""
42
 
43
- # ---------- Password gate (define BEFORE calling) ----------
 
 
44
  def add_password_gate() -> bool:
45
  """
46
  Shows a branded access screen until the correct password is entered.
47
- Set the password through Settings → Secrets as APP_PASSWORD (or an env var).
48
  """
49
- # 1) Read required password
 
50
  try:
51
  required = st.secrets.get("APP_PASSWORD", "")
52
  except Exception:
53
  required = os.environ.get("APP_PASSWORD", "")
54
 
55
- # 2) If not configured, block with an admin hint
56
  if not required:
57
  st.markdown(
58
  f"""
@@ -77,7 +79,7 @@ def add_password_gate() -> bool:
77
  if st.session_state.get("auth_ok", False):
78
  return True
79
 
80
- # 4) Prompt
81
  st.markdown(
82
  f"""
83
  <div style="display:flex;align-items:center;gap:14px;margin:8px 0 6px 0;">
@@ -105,10 +107,10 @@ def add_password_gate() -> bool:
105
  st.error("Incorrect key. Please try again.")
106
  st.stop()
107
 
108
- # 🔒 Enforce gate BEFORE any UI is rendered
109
  add_password_gate()
110
 
111
- # After the gate: global CSS/UI may render
112
  st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
113
  st.markdown(
114
  """
@@ -129,7 +131,7 @@ st.markdown(
129
  )
130
 
131
  # =========================
132
- # Helpers / caching
133
  # =========================
134
  try:
135
  dialog = st.dialog
@@ -177,9 +179,8 @@ def find_sheet(book, names):
177
  if nm.lower() in low2orig: return low2orig[nm.lower()]
178
  return None
179
 
180
- # ---------- Interactive plotting ----------
181
  def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
182
- """Interactive cross-plot: blue points, dashed 1:1, equal axes, no title, numeric ticks, full box outline."""
183
  a = pd.Series(actual).astype(float)
184
  p = pd.Series(pred).astype(float)
185
  lo = float(np.nanmin([a.min(), p.min()]))
@@ -219,11 +220,11 @@ def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
219
  tickformat=",.0f", scaleanchor="x", scaleratio=1,
220
  automargin=True
221
  )
222
- fig.update_layout(width=int(size[0]*100), height=int(size[1]*100))
 
223
  return fig
224
 
225
  def depth_or_index_track_interactive(df, title=None, include_actual=True):
226
- """Interactive UCS track: blue solid pred, yellow dotted actual, legend inside; x on top; full box outline."""
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
@@ -244,6 +245,7 @@ def depth_or_index_track_interactive(df, title=None, include_actual=True):
244
  name="UCS (actual)",
245
  hovertemplate="UCS (actual): %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
246
  ))
 
247
  fig.update_layout(
248
  paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
249
  margin=dict(l=60, r=10, t=10, b=36),
@@ -332,29 +334,27 @@ def preview_modal_val(book: dict[str, pd.DataFrame], feature_cols: list[str]):
332
  # =========================
333
  # Model presence
334
  # =========================
335
- def _download_model(url: str, dest: Path) -> bool:
 
 
 
 
 
 
 
336
  try:
337
  import requests
338
- dest.parent.mkdir(parents=True, exist_ok=True)
339
  with st.status("Downloading model…", expanded=False):
340
- with requests.get(url, stream=True, timeout=30) as r:
341
  r.raise_for_status()
342
- with open(dest, "wb") as f:
343
- for chunk in r.iter_content(chunk_size=1 << 20):
344
  if chunk: f.write(chunk)
345
- return True
346
  except Exception as e:
347
  st.error(f"Failed to download model from MODEL_URL: {e}")
348
- return False
349
-
350
- MODEL_URL = _get_model_url()
351
- def ensure_model_present() -> Path | None:
352
- for p in [DEFAULT_MODEL, *MODEL_FALLBACKS]:
353
- if p.exists() and p.stat().st_size > 0:
354
- return p
355
- if MODEL_URL and _download_model(MODEL_URL, DEFAULT_MODEL):
356
- return DEFAULT_MODEL
357
- return None
358
 
359
  model_path = ensure_model_present()
360
  if not model_path:
@@ -380,15 +380,13 @@ else:
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:
384
- pass
385
  try:
386
  if hasattr(m, "steps") and len(m.steps):
387
  last = m.steps[-1][1]
388
  if hasattr(last, "feature_names_in_") and len(last.feature_names_in_):
389
  return [str(x) for x in last.feature_names_in_]
390
- except Exception:
391
- pass
392
  return None
393
  infer = infer_features_from_model(model)
394
  if infer: FEATURES = infer
@@ -411,11 +409,19 @@ for k, v in {
411
  "dev_file_name": "",
412
  "dev_file_rows": 0,
413
  "dev_file_cols": 0,
 
 
 
 
 
 
 
 
414
  }.items():
415
  if k not in st.session_state: st.session_state[k] = v
416
 
417
  # =========================
418
- # Hero header (after gate)
419
  # =========================
420
  st.markdown(
421
  f"""
@@ -435,7 +441,9 @@ st.markdown(
435
  # =========================
436
  if st.session_state.app_step == "intro":
437
  st.header("Welcome!")
438
- st.markdown("This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data.")
 
 
439
  st.subheader("Expected Input Features (in Order)")
440
  st.markdown(
441
  "- Q, gpm — Flow rate (gallons per minute) \n"
@@ -448,17 +456,18 @@ if st.session_state.app_step == "intro":
448
  st.markdown(
449
  "1. **Upload your data to build the case and preview the performance of our model.** \n"
450
  "2. Click **Run Model** to compute metrics and plots. \n"
451
- "3. Click **Proceed to Prediction** to validate on a new dataset. \n"
452
- "4. Export results to Excel at any time."
 
453
  )
454
  if st.button("Start Showcase", type="primary", key="start_showcase"):
455
  st.session_state.app_step = "dev"; st.rerun()
456
 
457
  # =========================
458
- # Case Building
459
  # =========================
460
  if st.session_state.app_step == "dev":
461
- st.sidebar.header("Data for Case Building")
462
  dev_label = "Upload Data (Excel)" if not st.session_state.dev_file_name else "Replace data (Excel)"
463
  train_test_file = st.sidebar.file_uploader(dev_label, type=["xlsx","xls"], key="dev_upload")
464
 
@@ -473,7 +482,6 @@ if st.session_state.app_step == "dev":
473
  st.session_state.dev_file_signature = sig
474
  st.session_state.dev_file_name = train_test_file.name
475
  st.session_state.dev_file_bytes = file_bytes
476
- # Inspect first sheet for rows/cols
477
  _book_tmp = read_book_bytes(file_bytes)
478
  if _book_tmp:
479
  first_df = next(iter(_book_tmp.values()))
@@ -483,31 +491,28 @@ if st.session_state.app_step == "dev":
483
  st.session_state.dev_previewed = False
484
  st.session_state.dev_ready = False
485
 
486
- # Sidebar caption
487
  if st.session_state.dev_file_loaded:
488
  st.sidebar.caption(
489
  f"**Data loaded:** {st.session_state.dev_file_name} • "
490
  f"{st.session_state.dev_file_rows} rows × {st.session_state.dev_file_cols} cols"
491
  )
492
 
493
- # Sidebar actions
494
  preview_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=not st.session_state.dev_file_loaded)
495
  if preview_btn and st.session_state.dev_file_loaded:
496
  st.session_state.dev_preview_request = True
497
 
498
  run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
499
 
500
- proceed_clicked = st.sidebar.button(
501
- "Proceed to Prediction ▶",
502
- use_container_width=True,
503
- disabled=not st.session_state.dev_ready
504
- )
505
- if proceed_clicked and st.session_state.dev_ready:
506
  st.session_state.app_step = "predict"; st.rerun()
507
 
508
- # ----- ALWAYS-ON TOP: Title + helper -----
509
- helper_top = st.container()
510
- with helper_top:
511
  st.subheader("Case Building")
512
  if st.session_state.dev_ready:
513
  st.success("Case has been built and results are displayed below.")
@@ -518,14 +523,12 @@ if st.session_state.app_step == "dev":
518
  else:
519
  st.write("**Upload your data to build a case, then run the model to review development performance.**")
520
 
521
- # If user clicked preview, open modal *after* helper so helper stays on top
522
  if st.session_state.dev_preview_request and st.session_state.dev_file_bytes:
523
  _book = read_book_bytes(st.session_state.dev_file_bytes)
524
  st.session_state.dev_previewed = True
525
  st.session_state.dev_preview_request = False
526
  preview_modal_dev(_book, FEATURES)
527
 
528
- # Run model (from persisted bytes)
529
  if run_btn and st.session_state.dev_file_bytes:
530
  with st.status("Processing…", expanded=False) as status:
531
  book = read_book_bytes(st.session_state.dev_file_bytes)
@@ -561,7 +564,6 @@ if st.session_state.app_step == "dev":
561
  st.session_state.dev_ready = True
562
  status.update(label="Done ✓", state="complete"); st.rerun()
563
 
564
- # Results (if available)
565
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
566
  tab1, tab2 = st.tabs(["Training", "Testing"])
567
  if "Train" in st.session_state.results:
@@ -623,31 +625,46 @@ if st.session_state.app_step == "dev":
623
  st.warning(str(e))
624
 
625
  # =========================
626
- # PREDICTION (Validation)
627
  # =========================
628
- if st.session_state.app_step == "predict":
629
- st.sidebar.header("Prediction (Validation)")
630
  validation_file = st.sidebar.file_uploader("Upload Validation Excel", type=["xlsx","xls"], key="val_upload")
 
631
  if validation_file is not None:
632
- _book_tmp = read_book_bytes(validation_file.getvalue())
 
633
  if _book_tmp:
634
  first_df = next(iter(_book_tmp.values()))
 
635
  st.sidebar.caption(f"**Data loaded:** {validation_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
636
 
637
- preview_val_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=(validation_file is None))
638
- if preview_val_btn and validation_file is not None:
639
- _book = read_book_bytes(validation_file.getvalue())
640
- preview_modal_val(_book, FEATURES)
641
 
642
- predict_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
643
- st.sidebar.button("⬅ Back", on_click=lambda: st.session_state.update(app_step="dev"), use_container_width=True)
644
 
645
- st.subheader("Prediction")
646
- st.write("Upload a new dataset to generate UCS predictions and evaluate performance on unseen data.")
 
 
 
647
 
648
- if predict_btn and validation_file is not None:
649
- with st.status("Predicting…", expanded=False) as status:
650
- vbook = read_book_bytes(validation_file.getvalue())
 
 
 
 
 
 
 
 
 
 
 
651
  if not vbook: status.update(label="Could not read the Validation Excel.", state="error"); st.stop()
652
  status.update(label="Workbook read ✓")
653
  vname = find_sheet(vbook, ["Validation","Validate","validation2","Val","val"]) or list(vbook.keys())[0]
@@ -681,16 +698,15 @@ if st.session_state.app_step == "predict":
681
  "oor_pct": oor_pct
682
  }
683
  st.session_state.results["oor_table"] = oor_table
684
- status.update(label="Predictions ready ✓", state="complete")
685
 
 
686
  if "Validate" in st.session_state.results:
687
- st.subheader("Validation Results")
688
  sv = st.session_state.results["summary_val"]; oor_table = st.session_state.results.get("oor_table")
689
 
690
  if sv["oor_pct"] > 0:
691
  st.warning("Some validation inputs fall outside the **training min–max** ranges. Interpret predictions with caution.")
692
 
693
- # Prefer dev-like metrics when actual UCS exists
694
  metrics_val = st.session_state.results.get("metrics_val")
695
  if metrics_val is not None:
696
  c1, c2, c3 = st.columns(3)
@@ -698,11 +714,10 @@ if st.session_state.app_step == "predict":
698
  c2.metric("RMSE", f"{metrics_val['RMSE']:.4f}")
699
  c3.metric("MAE", f"{metrics_val['MAE']:.4f}")
700
  else:
701
- c1, c2, c3, c4 = st.columns(4)
702
- c1.metric("points", f"{sv['n_points']}")
703
  c2.metric("Pred min", f"{sv['pred_min']:.2f}")
704
  c3.metric("Pred max", f"{sv['pred_max']:.2f}")
705
- c4.metric("OOR %", f"{sv['oor_pct']:.1f}%")
706
 
707
  left, right = st.columns([0.9, 0.55])
708
  with left:
@@ -754,6 +769,124 @@ if st.session_state.app_step == "predict":
754
  except Exception as e:
755
  st.warning(str(e))
756
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
  # =========================
758
  # Footer
759
  # =========================
@@ -766,4 +899,4 @@ st.markdown(
766
  </div>
767
  """,
768
  unsafe_allow_html=True
769
- )
 
26
  COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
27
 
28
  # =========================
29
+ # Page / Theme
30
  # =========================
31
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
32
 
33
+ # ---------- inline logo (used by password gate + header) ----------
34
  def inline_logo(path="logo.png") -> str:
35
  try:
36
  p = Path(path)
37
+ if not p.exists(): return ""
38
+ return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
39
  except Exception:
40
+ return ""
 
41
 
42
+ # =========================
43
+ # Password (brand-gated)
44
+ # =========================
45
  def add_password_gate() -> bool:
46
  """
47
  Shows a branded access screen until the correct password is entered.
48
+ Requires APP_PASSWORD in Secrets (or environment).
49
  """
50
+ # 1) Read password
51
+ required = ""
52
  try:
53
  required = st.secrets.get("APP_PASSWORD", "")
54
  except Exception:
55
  required = os.environ.get("APP_PASSWORD", "")
56
 
57
+ # 2) If not configured, BLOCK (admin instruction)
58
  if not required:
59
  st.markdown(
60
  f"""
 
79
  if st.session_state.get("auth_ok", False):
80
  return True
81
 
82
+ # 4) Branded prompt
83
  st.markdown(
84
  f"""
85
  <div style="display:flex;align-items:center;gap:14px;margin:8px 0 6px 0;">
 
107
  st.error("Incorrect key. Please try again.")
108
  st.stop()
109
 
110
+ # 🔒 Gate the app
111
  add_password_gate()
112
 
113
+ # CSS
114
  st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
115
  st.markdown(
116
  """
 
131
  )
132
 
133
  # =========================
134
+ # Helpers
135
  # =========================
136
  try:
137
  dialog = st.dialog
 
179
  if nm.lower() in low2orig: return low2orig[nm.lower()]
180
  return None
181
 
182
+ # ---------- Interactive plotting (full outline, bold axis titles) ----------
183
  def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
 
184
  a = pd.Series(actual).astype(float)
185
  p = pd.Series(pred).astype(float)
186
  lo = float(np.nanmin([a.min(), p.min()]))
 
220
  tickformat=",.0f", scaleanchor="x", scaleratio=1,
221
  automargin=True
222
  )
223
+ w = int(size[0] * 100); h = int(size[1] * 100)
224
+ fig.update_layout(width=w, height=h)
225
  return fig
226
 
227
  def depth_or_index_track_interactive(df, title=None, include_actual=True):
 
228
  depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
229
  if depth_col is not None:
230
  y = df[depth_col]; y_label = depth_col
 
245
  name="UCS (actual)",
246
  hovertemplate="UCS (actual): %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
247
  ))
248
+
249
  fig.update_layout(
250
  paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
251
  margin=dict(l=60, r=10, t=10, b=36),
 
334
  # =========================
335
  # Model presence
336
  # =========================
337
+ MODEL_URL = _get_model_url()
338
+
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:
 
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
 
409
  "dev_file_name": "",
410
  "dev_file_rows": 0,
411
  "dev_file_cols": 0,
412
+ # validation (was predict)
413
+ "val_file_bytes": b"",
414
+ "val_file_loaded": False,
415
+ "val_preview_request": False,
416
+ # prediction (new)
417
+ "pred_file_bytes": b"",
418
+ "pred_file_loaded": False,
419
+ "pred_preview_request": False,
420
  }.items():
421
  if k not in st.session_state: st.session_state[k] = v
422
 
423
  # =========================
424
+ # Hero header
425
  # =========================
426
  st.markdown(
427
  f"""
 
441
  # =========================
442
  if st.session_state.app_step == "intro":
443
  st.header("Welcome!")
444
+ st.markdown(
445
+ "This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data."
446
+ )
447
  st.subheader("Expected Input Features (in Order)")
448
  st.markdown(
449
  "- Q, gpm — Flow rate (gallons per minute) \n"
 
456
  st.markdown(
457
  "1. **Upload your data to build the case and preview the performance of our model.** \n"
458
  "2. Click **Run Model** to compute metrics and plots. \n"
459
+ "3. Click **Proceed to Validation** to evaluate on a new dataset with actual UCS (if available). \n"
460
+ "4. Click **Proceed to Prediction** to generate production predictions (no actuals). \n"
461
+ "5. Export results to Excel at any time."
462
  )
463
  if st.button("Start Showcase", type="primary", key="start_showcase"):
464
  st.session_state.app_step = "dev"; st.rerun()
465
 
466
  # =========================
467
+ # 1) CASE BUILDING (Development)
468
  # =========================
469
  if st.session_state.app_step == "dev":
470
+ st.sidebar.header("Case Building (Development)")
471
  dev_label = "Upload Data (Excel)" if not st.session_state.dev_file_name else "Replace data (Excel)"
472
  train_test_file = st.sidebar.file_uploader(dev_label, type=["xlsx","xls"], key="dev_upload")
473
 
 
482
  st.session_state.dev_file_signature = sig
483
  st.session_state.dev_file_name = train_test_file.name
484
  st.session_state.dev_file_bytes = file_bytes
 
485
  _book_tmp = read_book_bytes(file_bytes)
486
  if _book_tmp:
487
  first_df = next(iter(_book_tmp.values()))
 
491
  st.session_state.dev_previewed = False
492
  st.session_state.dev_ready = False
493
 
 
494
  if st.session_state.dev_file_loaded:
495
  st.sidebar.caption(
496
  f"**Data loaded:** {st.session_state.dev_file_name} • "
497
  f"{st.session_state.dev_file_rows} rows × {st.session_state.dev_file_cols} cols"
498
  )
499
 
 
500
  preview_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=not st.session_state.dev_file_loaded)
501
  if preview_btn and st.session_state.dev_file_loaded:
502
  st.session_state.dev_preview_request = True
503
 
504
  run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
505
 
506
+ # Always enabled so users can jump ahead
507
+ proceed_val = st.sidebar.button("Proceed to Validation ▶", use_container_width=True)
508
+ proceed_pred = st.sidebar.button("Proceed to Prediction ▶", use_container_width=True)
509
+ if proceed_val:
510
+ st.session_state.app_step = "validate"; st.rerun()
511
+ if proceed_pred:
512
  st.session_state.app_step = "predict"; st.rerun()
513
 
514
+ # Helper (always at top)
515
+ with st.container():
 
516
  st.subheader("Case Building")
517
  if st.session_state.dev_ready:
518
  st.success("Case has been built and results are displayed below.")
 
523
  else:
524
  st.write("**Upload your data to build a case, then run the model to review development performance.**")
525
 
 
526
  if st.session_state.dev_preview_request and st.session_state.dev_file_bytes:
527
  _book = read_book_bytes(st.session_state.dev_file_bytes)
528
  st.session_state.dev_previewed = True
529
  st.session_state.dev_preview_request = False
530
  preview_modal_dev(_book, FEATURES)
531
 
 
532
  if run_btn and st.session_state.dev_file_bytes:
533
  with st.status("Processing…", expanded=False) as status:
534
  book = read_book_bytes(st.session_state.dev_file_bytes)
 
564
  st.session_state.dev_ready = True
565
  status.update(label="Done ✓", state="complete"); st.rerun()
566
 
 
567
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
568
  tab1, tab2 = st.tabs(["Training", "Testing"])
569
  if "Train" in st.session_state.results:
 
625
  st.warning(str(e))
626
 
627
  # =========================
628
+ # 2) VALIDATE THE MODEL (was predict)
629
  # =========================
630
+ if st.session_state.app_step == "validate":
631
+ st.sidebar.header("Validate the model")
632
  validation_file = st.sidebar.file_uploader("Upload Validation Excel", type=["xlsx","xls"], key="val_upload")
633
+
634
  if validation_file is not None:
635
+ st.session_state.val_file_bytes = validation_file.getvalue()
636
+ _book_tmp = read_book_bytes(st.session_state.val_file_bytes)
637
  if _book_tmp:
638
  first_df = next(iter(_book_tmp.values()))
639
+ st.session_state.val_file_loaded = True
640
  st.sidebar.caption(f"**Data loaded:** {validation_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
641
 
642
+ preview_val_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=not st.session_state.val_file_loaded)
643
+ if preview_val_btn and st.session_state.val_file_loaded:
644
+ st.session_state.val_preview_request = True
 
645
 
646
+ predict_btn = st.sidebar.button("Run Validation", type="primary", use_container_width=True)
 
647
 
648
+ # Always enabled
649
+ proceed_pred = st.sidebar.button("Proceed to Prediction ▶", use_container_width=True)
650
+ st.sidebar.button("⬅ Back to Case Building", on_click=lambda: st.session_state.update(app_step="dev"), use_container_width=True)
651
+ if proceed_pred:
652
+ st.session_state.app_step = "predict"; st.rerun()
653
 
654
+ # Helper
655
+ with st.container():
656
+ st.subheader("Validate the model")
657
+ st.write("Upload a validation dataset (with actual UCS if available), preview it, then run to view metrics and plots.")
658
+
659
+ if st.session_state.val_preview_request and st.session_state.val_file_bytes:
660
+ _book = read_book_bytes(st.session_state.val_file_bytes)
661
+ st.session_state.val_preview_request = False
662
+ preview_modal_val(_book, FEATURES)
663
+
664
+ # Run validation
665
+ if predict_btn and st.session_state.val_file_bytes:
666
+ with st.status("Validating…", expanded=False) as status:
667
+ vbook = read_book_bytes(st.session_state.val_file_bytes)
668
  if not vbook: status.update(label="Could not read the Validation Excel.", state="error"); st.stop()
669
  status.update(label="Workbook read ✓")
670
  vname = find_sheet(vbook, ["Validation","Validate","validation2","Val","val"]) or list(vbook.keys())[0]
 
698
  "oor_pct": oor_pct
699
  }
700
  st.session_state.results["oor_table"] = oor_table
701
+ status.update(label="Validation ready ✓", state="complete")
702
 
703
+ # Display
704
  if "Validate" in st.session_state.results:
 
705
  sv = st.session_state.results["summary_val"]; oor_table = st.session_state.results.get("oor_table")
706
 
707
  if sv["oor_pct"] > 0:
708
  st.warning("Some validation inputs fall outside the **training min–max** ranges. Interpret predictions with caution.")
709
 
 
710
  metrics_val = st.session_state.results.get("metrics_val")
711
  if metrics_val is not None:
712
  c1, c2, c3 = st.columns(3)
 
714
  c2.metric("RMSE", f"{metrics_val['RMSE']:.4f}")
715
  c3.metric("MAE", f"{metrics_val['MAE']:.4f}")
716
  else:
717
+ c1, c2, c3 = st.columns(3)
718
+ c1.metric("# points", f"{sv['n_points']}")
719
  c2.metric("Pred min", f"{sv['pred_min']:.2f}")
720
  c3.metric("Pred max", f"{sv['pred_max']:.2f}")
 
721
 
722
  left, right = st.columns([0.9, 0.55])
723
  with left:
 
769
  except Exception as e:
770
  st.warning(str(e))
771
 
772
+ # =========================
773
+ # 3) PREDICTION (production scoring, no actual UCS)
774
+ # =========================
775
+ if st.session_state.app_step == "predict":
776
+ st.sidebar.header("Prediction")
777
+ pred_file = st.sidebar.file_uploader("Upload Prediction Excel", type=["xlsx","xls"], key="pred_upload")
778
+
779
+ if pred_file is not None:
780
+ st.session_state.pred_file_bytes = pred_file.getvalue()
781
+ _book_tmp = read_book_bytes(st.session_state.pred_file_bytes)
782
+ if _book_tmp:
783
+ first_df = next(iter(_book_tmp.values()))
784
+ st.session_state.pred_file_loaded = True
785
+ st.sidebar.caption(f"**Data loaded:** {pred_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
786
+
787
+ preview_pred_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=not st.session_state.pred_file_loaded)
788
+ if preview_pred_btn and st.session_state.pred_file_loaded:
789
+ st.session_state.pred_preview_request = True
790
+
791
+ predict_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
792
+ st.sidebar.button("⬅ Back to Validation", on_click=lambda: st.session_state.update(app_step="validate"), use_container_width=True)
793
+
794
+ with st.container():
795
+ st.subheader("Prediction")
796
+ st.write("Upload a dataset (no actual UCS needed), preview it, then click **Predict** to generate UCS estimates.")
797
+
798
+ if st.session_state.pred_preview_request and st.session_state.pred_file_bytes:
799
+ _book = read_book_bytes(st.session_state.pred_file_bytes)
800
+ st.session_state.pred_preview_request = False
801
+ # Reuse the same previewer (no special sheet naming required)
802
+ preview_modal_val(_book, FEATURES)
803
+
804
+ # Run prediction
805
+ if predict_btn and st.session_state.pred_file_bytes:
806
+ with st.status("Predicting…", expanded=False) as status:
807
+ pbook = read_book_bytes(st.session_state.pred_file_bytes)
808
+ if not pbook: status.update(label="Could not read the Excel file.", state="error"); st.stop()
809
+ status.update(label="Workbook read ✓")
810
+ pname = list(pbook.keys())[0]
811
+ df_pred = pbook[pname].copy()
812
+ if not ensure_cols(df_pred, FEATURES): status.update(label="Missing required columns.", state="error"); st.stop()
813
+ status.update(label="Columns validated ✓")
814
+ df_pred["UCS_Pred"] = model.predict(df_pred[FEATURES])
815
+ st.session_state.results["Prediction"] = df_pred
816
+
817
+ # OOR vs training ranges if available
818
+ ranges = st.session_state.train_ranges; oor_table = None; oor_pct = 0.0
819
+ if ranges:
820
+ viol = {f: (df_pred[f] < ranges[f][0]) | (df_pred[f] > ranges[f][1]) for f in FEATURES}
821
+ any_viol = pd.DataFrame(viol).any(axis=1); oor_pct = float(any_viol.mean()*100.0)
822
+ if any_viol.any():
823
+ offenders = df_pred.loc[any_viol, FEATURES].copy()
824
+ offenders["Violations"] = pd.DataFrame(viol).loc[any_viol].apply(lambda r: ", ".join([c for c,v in r.items() if v]), axis=1)
825
+ offenders.index = offenders.index + 1; oor_table = offenders
826
+
827
+ st.session_state.results["summary_pred"] = {
828
+ "n_points": len(df_pred),
829
+ "pred_min": float(df_pred["UCS_Pred"].min()),
830
+ "pred_max": float(df_pred["UCS_Pred"].max()),
831
+ "pred_mean": float(df_pred["UCS_Pred"].mean()),
832
+ "pred_std": float(df_pred["UCS_Pred"].std(ddof=0)),
833
+ "oor_pct": oor_pct
834
+ }
835
+ st.session_state.results["oor_table_pred"] = oor_table
836
+ status.update(label="Predictions ready ✓", state="complete")
837
+
838
+ # Display prediction results (no cross-plot)
839
+ if "Prediction" in st.session_state.results:
840
+ sv = st.session_state.results["summary_pred"]
841
+ if sv.get("oor_pct", 0) > 0:
842
+ st.warning("Some inputs fall outside the **training min–max** ranges. Interpret predictions with caution.")
843
+
844
+ # Two columns: table (left), track (right)
845
+ left, right = st.columns([0.6, 0.9])
846
+ with left:
847
+ table = pd.DataFrame(
848
+ {
849
+ "Metric": ["# points", "Pred min", "Pred max", "Pred mean", "Pred std", "OOR %"],
850
+ "Value": [
851
+ f"{sv['n_points']}",
852
+ f"{sv['pred_min']:.2f}",
853
+ f"{sv['pred_max']:.2f}",
854
+ f"{sv['pred_mean']:.2f}",
855
+ f"{sv['pred_std']:.2f}",
856
+ f"{sv['oor_pct']:.1f}%",
857
+ ],
858
+ }
859
+ )
860
+ st.dataframe(table, use_container_width=True, hide_index=True)
861
+ with right:
862
+ st.plotly_chart(
863
+ depth_or_index_track_interactive(
864
+ st.session_state.results["Prediction"], title=None, include_actual=False
865
+ ),
866
+ use_container_width=True, config={"displayModeBar": False}
867
+ )
868
+
869
+ # OOR table if any
870
+ if st.session_state.results.get("oor_table_pred") is not None:
871
+ st.write("*Out-of-range rows (vs. Training min–max):*")
872
+ st.dataframe(st.session_state.results["oor_table_pred"], use_container_width=True)
873
+
874
+ st.markdown("---")
875
+ # Export predictions + summary
876
+ try:
877
+ buf = io.BytesIO()
878
+ with pd.ExcelWriter(buf, engine="openpyxl") as xw:
879
+ st.session_state.results["Prediction"].to_excel(xw, sheet_name="Prediction_with_pred", index=False)
880
+ pd.DataFrame([sv]).to_excel(xw, sheet_name="Summary", index=False)
881
+ st.download_button(
882
+ "Export Prediction Results to Excel",
883
+ data=buf.getvalue(),
884
+ file_name="UCS_Prediction_Results.xlsx",
885
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
886
+ )
887
+ except Exception as e:
888
+ st.warning(str(e))
889
+
890
  # =========================
891
  # Footer
892
  # =========================
 
899
  </div>
900
  """,
901
  unsafe_allow_html=True
902
+ )