UCS2014 commited on
Commit
19c6336
·
verified ·
1 Parent(s): f6aa027

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +309 -241
app.py CHANGED
@@ -6,7 +6,7 @@ import pandas as pd
6
  import numpy as np
7
  import joblib
8
 
9
- # keep matplotlib ONLY for the preview modal (static thumbnails)
10
  import matplotlib
11
  matplotlib.use("Agg")
12
  import matplotlib.pyplot as plt
@@ -14,40 +14,74 @@ import matplotlib.pyplot as plt
14
  import plotly.graph_objects as go
15
  from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
16
 
17
- # =========================
18
- # Fixed plot sizes (px)
19
- # =========================
20
- CROSS_W, CROSS_H = 390, 390 # cross-plot (square & compact)
21
- TRACK_W, TRACK_H = 220, 700 # EXACT match to preview strips (≈ 2.2in × 7.0in @100dpi)
22
 
23
  # =========================
24
- # Defaults
25
  # =========================
26
  FEATURES = ["Q, gpm", "SPP(psi)", "T (kft.lbf)", "WOB (klbf)", "ROP (ft/h)"]
27
  TARGET = "UCS"
 
28
  MODELS_DIR = Path("models")
29
  DEFAULT_MODEL = MODELS_DIR / "ucs_rf.joblib"
30
  MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
31
 
32
  COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
33
 
34
- # ------------------------------------------------------------
35
- # Small utilities that must exist before we use them
36
- # ------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  def inline_logo(path="logo.png") -> str:
38
  try:
39
  p = Path(path)
40
- if not p.exists(): return ""
 
41
  return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
42
  except Exception:
43
  return ""
44
 
 
 
 
 
45
  def add_password_gate() -> bool:
46
  """
47
- Branded password screen until APP_PASSWORD is provided.
48
- Set APP_PASSWORD in HF Space: Settings Secrets.
49
  """
50
- # Read password from secrets or env
51
  required = ""
52
  try:
53
  required = st.secrets.get("APP_PASSWORD", "")
@@ -66,7 +100,8 @@ def add_password_gate() -> bool:
66
  </div>
67
  <div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected Area</div>
68
  <div style="color:#6b7280;margin-bottom:14px;">
69
- Admin action required: set <code>APP_PASSWORD</code> in <b>Settings → Secrets</b> then restart the Space.
 
70
  </div>
71
  """,
72
  unsafe_allow_html=True,
@@ -86,51 +121,26 @@ def add_password_gate() -> bool:
86
  </div>
87
  </div>
88
  <div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected</div>
89
- <div style="color:#6b7280;margin-bottom:14px;">
90
- Please enter your access key to continue.
91
- </div>
92
  """,
93
  unsafe_allow_html=True
94
  )
95
-
96
  pwd = st.text_input("Access key", type="password", placeholder="••••••••")
97
- col1, _ = st.columns([1, 3])
98
- with col1:
99
- if st.button("Unlock", type="primary", use_container_width=True):
100
- if pwd == required:
101
- st.session_state.auth_ok = True
102
- st.rerun()
103
- else:
104
- st.error("Incorrect key. Please try again.")
105
  st.stop()
106
 
107
- # =========================
108
- # Page / Theme
109
- # =========================
110
- st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
111
- add_password_gate() # 🔒 gate
112
 
113
- st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
114
- st.markdown(
115
- """
116
- <style>
117
- .stApp { background: #FFFFFF; }
118
- section[data-testid="stSidebar"] { background: #F6F9FC; }
119
- .block-container { padding-top: .5rem; padding-bottom: .5rem; }
120
- .stButton>button{ background:#007bff; color:#fff; font-weight:bold; border-radius:8px; border:none; padding:10px 24px; }
121
- .stButton>button:hover{ background:#0056b3; }
122
- .st-hero { display:flex; align-items:center; gap:16px; padding-top: 4px; }
123
- .st-hero .brand { width:110px; height:110px; object-fit:contain; }
124
- .st-hero h1 { margin:0; line-height:1.05; }
125
- .st-hero .tagline { margin:2px 0 0 2px; color:#6b7280; font-size:1.05rem; font-style:italic; }
126
- [data-testid="stBlock"]{ margin-top:0 !important; }
127
- </style>
128
- """,
129
- unsafe_allow_html=True
130
- )
131
 
132
  # =========================
133
- # Helpers
134
  # =========================
135
  try:
136
  dialog = st.dialog
@@ -167,19 +177,28 @@ def parse_excel(data_bytes: bytes):
167
 
168
  def read_book_bytes(data_bytes: bytes):
169
  if not data_bytes: return {}
170
- try: return parse_excel(data_bytes)
 
171
  except Exception as e:
172
- st.error(f"Failed to read Excel: {e}"); return {}
 
173
 
174
  def find_sheet(book, names):
175
  low2orig = {k.lower(): k for k in book.keys()}
176
  for nm in names:
177
- if nm.lower() in low2orig: return low2orig[nm.lower()]
 
178
  return None
179
 
180
- # ---------- Interactive plotting (fixed sizes, full outline) ----------
 
 
 
 
 
 
 
181
  def cross_plot_interactive(actual, pred):
182
- """Interactive cross-plot: blue points, dashed 1:1, equal axes, NO title, full outline."""
183
  a = pd.Series(actual).astype(float)
184
  p = pd.Series(pred).astype(float)
185
  lo = float(np.nanmin([a.min(), p.min()]))
@@ -207,24 +226,20 @@ def cross_plot_interactive(actual, pred):
207
  width=CROSS_W, height=CROSS_H
208
  )
209
  fig.update_xaxes(
210
- title_text="<b>Actual UCS</b>",
211
- range=[x0, x1], ticks="outside",
212
- showline=True, linewidth=1.2, linecolor="#444", mirror=True,
213
- showgrid=True, gridcolor="rgba(0,0,0,0.12)",
214
- tickformat=",.0f", automargin=True
215
  )
216
  fig.update_yaxes(
217
- title_text="<b>Predicted UCS</b>",
218
- range=[x0, x1], ticks="outside",
219
- showline=True, linewidth=1.2, linecolor="#444", mirror=True,
220
- showgrid=True, gridcolor="rgba(0,0,0,0.12)",
221
- tickformat=",.0f", scaleanchor="x", scaleratio=1,
222
  automargin=True
223
  )
 
224
  return fig
225
 
226
  def depth_or_index_track_interactive(df, include_actual=True):
227
- """Interactive UCS track: fixed width/height EXACTLY like preview strips."""
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
@@ -232,15 +247,12 @@ def depth_or_index_track_interactive(df, include_actual=True):
232
  y = np.arange(1, len(df) + 1); y_label = "Point Index"
233
 
234
  fig = go.Figure()
235
-
236
- # Predicted (solid blue)
237
  fig.add_trace(go.Scatter(
238
  x=df["UCS_Pred"], y=y, mode="lines",
239
  line=dict(color=COLORS["pred"], width=1.8),
240
  name="UCS_Pred",
241
  hovertemplate="UCS_Pred: %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
242
  ))
243
- # Actual (dotted yellow)
244
  if include_actual and TARGET in df.columns:
245
  fig.add_trace(go.Scatter(
246
  x=df[TARGET], y=y, mode="lines",
@@ -251,29 +263,27 @@ def depth_or_index_track_interactive(df, include_actual=True):
251
 
252
  fig.update_layout(
253
  paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
254
- margin=dict(l=44, r=6, t=6, b=36), # slim margins like preview
255
  hovermode="closest", font=dict(size=13),
256
- legend=dict(
257
- x=0.98, y=0.05, xanchor="right", yanchor="bottom",
258
- bgcolor="rgba(255,255,255,0.75)", bordercolor="#cccccc", borderwidth=1
259
- ),
260
  legend_title_text="",
261
  width=TRACK_W, height=TRACK_H
262
  )
263
  fig.update_xaxes(
264
- title_text="<b>UCS</b>", side="top",
265
  ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror=True,
266
- showgrid=True, gridcolor="rgba(0,0,0,0.12)",
267
- tickformat=",.0f", automargin=True
268
  )
269
  fig.update_yaxes(
270
  title_text=f"<b>{y_label}</b>", autorange="reversed",
271
  ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror=True,
272
- showgrid=True, gridcolor="rgba(0,0,0,0.12)",
273
- automargin=True
274
  )
 
275
  return fig
276
 
 
277
  # ---------- Preview modal helpers (matplotlib static) ----------
278
  def make_index_tracks(df: pd.DataFrame, cols: list[str]):
279
  cols = [c for c in cols if c in df.columns]
@@ -315,24 +325,24 @@ def preview_modal_dev(book: dict[str, pd.DataFrame], feature_cols: list[str]):
315
  if not tabs:
316
  first_name = list(book.keys())[0]
317
  tabs = [first_name]; data = [book[first_name]]
318
- st.write("Use the tabs to switch between Train/Test views (if available).")
319
  t_objs = st.tabs(tabs)
320
  for t, df in zip(t_objs, data):
321
  with t:
322
  t1, t2 = st.tabs(["Tracks", "Summary"])
323
- with t1: st.pyplot(make_index_tracks(df, feature_cols), use_container_width=True)
324
- with t2: st.dataframe(stats_table(df, feature_cols), use_container_width=True)
325
 
326
  @dialog("Preview data")
327
- def preview_modal_val(book: dict[str, pd.DataFrame], feature_cols: list[str]):
328
  if not book:
329
  st.info("No data loaded yet."); return
330
- vname = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
331
  df = book[vname]
332
  t1, t2 = st.tabs(["Tracks", "Summary"])
333
  with t1: st.pyplot(make_index_tracks(df, feature_cols), use_container_width=True)
334
  with t2: st.dataframe(stats_table(df, feature_cols), use_container_width=True)
335
 
 
336
  # =========================
337
  # Model presence
338
  # =========================
@@ -369,37 +379,25 @@ except Exception as e:
369
  st.error(f"Failed to load model: {model_path}\n{e}")
370
  st.stop()
371
 
372
- # Meta overrides / inference
373
  meta_path = MODELS_DIR / "meta.json"
374
  if meta_path.exists():
375
  try:
376
  meta = json.loads(meta_path.read_text(encoding="utf-8"))
377
- FEATURES = meta.get("features", FEATURES); TARGET = meta.get("target", TARGET)
378
- except Exception: pass
379
- else:
380
- def infer_features_from_model(m):
381
- try:
382
- if hasattr(m, "feature_names_in_") and len(getattr(m, "feature_names_in_")):
383
- return [str(x) for x in m.feature_names_in_]
384
- except Exception: 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: pass
391
- return None
392
- infer = infer_features_from_model(model)
393
- if infer: FEATURES = infer
394
 
395
  # =========================
396
  # Session state
397
  # =========================
398
- if "app_step" not in st.session_state: st.session_state.app_step = "dev" # start at Case Building after gate
399
  if "results" not in st.session_state: st.session_state.results = {}
400
  if "train_ranges" not in st.session_state: st.session_state.train_ranges = None
401
 
402
- # Dev page state (persist file)
403
  for k, v in {
404
  "dev_ready": False,
405
  "dev_file_loaded": False,
@@ -413,6 +411,7 @@ for k, v in {
413
  }.items():
414
  if k not in st.session_state: st.session_state[k] = v
415
 
 
416
  # =========================
417
  # Hero header
418
  # =========================
@@ -429,6 +428,33 @@ st.markdown(
429
  unsafe_allow_html=True,
430
  )
431
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  # =========================
433
  # CASE BUILDING (Development)
434
  # =========================
@@ -463,46 +489,45 @@ if st.session_state.app_step == "dev":
463
  f"{st.session_state.dev_file_rows} rows × {st.session_state.dev_file_cols} cols"
464
  )
465
 
466
- preview_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=not st.session_state.dev_file_loaded)
467
- if preview_btn and st.session_state.dev_file_loaded:
468
  st.session_state.dev_preview_request = True
469
-
470
  run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
 
472
- # Navigation
473
- st.sidebar.button("Proceed to Validation ▶", use_container_width=True,
474
- on_click=lambda: st.session_state.update(app_step="val"))
475
- st.sidebar.button("Proceed to Prediction ▶", use_container_width=True,
476
- on_click=lambda: st.session_state.update(app_step="pred"))
477
-
478
- # Helper
479
- helper_top = st.container()
480
- with helper_top:
481
- st.subheader("Case Building (Development)")
482
- if st.session_state.dev_ready:
483
- st.success("Case has been built and results are displayed below.")
484
- elif st.session_state.dev_file_loaded and st.session_state.dev_previewed:
485
- st.info("Previewed ✓ — now click **Run Model** to build the case.")
486
- elif st.session_state.dev_file_loaded:
487
- st.info("📄 **Preview uploaded data** using the sidebar button, then click **Run Model**.")
488
- else:
489
- st.write("**Upload your data to build a case, then run the model to review development performance.**")
490
-
491
  if st.session_state.dev_preview_request and st.session_state.dev_file_bytes:
492
  _book = read_book_bytes(st.session_state.dev_file_bytes)
493
  st.session_state.dev_previewed = True
494
  st.session_state.dev_preview_request = False
495
  preview_modal_dev(_book, FEATURES)
496
 
 
497
  if run_btn and st.session_state.dev_file_bytes:
498
  with st.status("Processing…", expanded=False) as status:
499
  book = read_book_bytes(st.session_state.dev_file_bytes)
500
- if not book: status.update(label="Failed to read workbook.", state="error"); st.stop()
 
501
  status.update(label="Workbook read ✓")
502
  sh_train = find_sheet(book, ["Train","Training","training2","train","training"])
503
  sh_test = find_sheet(book, ["Test","Testing","testing2","test","testing"])
504
  if sh_train is None or sh_test is None:
505
  status.update(label="Workbook must include Train/Training/training2 and Test/Testing/testing2.", state="error"); st.stop()
 
506
  df_tr = book[sh_train].copy(); df_te = book[sh_test].copy()
507
  if not (ensure_cols(df_tr, FEATURES + [TARGET]) and ensure_cols(df_te, FEATURES + [TARGET])):
508
  status.update(label="Missing required columns.", state="error"); st.stop()
@@ -527,39 +552,47 @@ if st.session_state.app_step == "dev":
527
  st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
528
 
529
  st.session_state.dev_ready = True
530
- status.update(label="Done ✓", state="complete"); st.rerun()
 
531
 
532
- # Results (if available)
533
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
534
  tab1, tab2 = st.tabs(["Training", "Testing"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
  if "Train" in st.session_state.results:
536
  with tab1:
537
- df = st.session_state.results["Train"]; m = st.session_state.results["metrics_train"]
538
- c1,c2,c3 = st.columns(3)
539
- c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
540
- # fixed-size plots side-by-side
541
- left, right, _ = st.columns([1, 1, 3])
542
- with left:
543
- st.plotly_chart(cross_plot_interactive(df[TARGET], df["UCS_Pred"]),
544
- use_container_width=False, config={"displayModeBar": False})
545
- with right:
546
- st.plotly_chart(depth_or_index_track_interactive(df, include_actual=True),
547
- use_container_width=False, config={"displayModeBar": False})
548
  if "Test" in st.session_state.results:
549
  with tab2:
550
- df = st.session_state.results["Test"]; m = st.session_state.results["metrics_test"]
551
- c1,c2,c3 = st.columns(3)
552
- c1.metric("R²", f"{m['R2']:.4f}"); c2.metric("RMSE", f"{m['RMSE']:.4f}"); c3.metric("MAE", f"{m['MAE']:.4f}")
553
- left, right, _ = st.columns([1, 1, 3])
554
- with left:
555
- st.plotly_chart(cross_plot_interactive(df[TARGET], df["UCS_Pred"]),
556
- use_container_width=False, config={"displayModeBar": False})
557
- with right:
558
- st.plotly_chart(depth_or_index_track_interactive(df, include_actual=True),
559
- use_container_width=False, config={"displayModeBar": False})
560
 
561
  st.markdown("---")
562
- sheets = {}; rows = []
 
 
563
  if "Train" in st.session_state.results:
564
  sheets["Train_with_pred"] = st.session_state.results["Train"]
565
  rows.append({"Split":"Train", **{k:round(v,6) for k,v in st.session_state.results["metrics_train"].items()}})
@@ -583,8 +616,9 @@ if st.session_state.app_step == "dev":
583
  except Exception as e:
584
  st.warning(str(e))
585
 
 
586
  # =========================
587
- # VALIDATION (with actuals)
588
  # =========================
589
  if st.session_state.app_step == "val":
590
  st.sidebar.header("Validate the model")
@@ -595,18 +629,18 @@ if st.session_state.app_step == "val":
595
  first_df = next(iter(_book_tmp.values()))
596
  st.sidebar.caption(f"**Data loaded:** {validation_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
597
 
598
- preview_val_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=(validation_file is None))
599
- if preview_val_btn and validation_file is not None:
600
  _book = read_book_bytes(validation_file.getvalue())
601
- preview_modal_val(_book, FEATURES)
602
 
603
  predict_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
604
- st.sidebar.button(" Back to Case Building", on_click=lambda: st.session_state.update(app_step="dev"), use_container_width=True)
605
- st.sidebar.button("Proceed to Prediction ▶", use_container_width=True,
606
- on_click=lambda: st.session_state.update(app_step="pred"))
 
607
 
608
- st.subheader("Validate the model")
609
- st.write("Upload a dataset that includes actual UCS to evaluate model performance.")
610
 
611
  if predict_btn and validation_file is not None:
612
  with st.status("Predicting…", expanded=False) as status:
@@ -615,7 +649,8 @@ if st.session_state.app_step == "val":
615
  status.update(label="Workbook read ✓")
616
  vname = find_sheet(vbook, ["Validation","Validate","validation2","Val","val"]) or list(vbook.keys())[0]
617
  df_val = vbook[vname].copy()
618
- if not ensure_cols(df_val, FEATURES + [TARGET]): status.update(label="Missing required columns.", state="error"); st.stop()
 
619
  status.update(label="Columns validated ✓")
620
  df_val["UCS_Pred"] = model.predict(df_val[FEATURES])
621
  st.session_state.results["Validate"] = df_val
@@ -629,11 +664,13 @@ if st.session_state.app_step == "val":
629
  offenders["Violations"] = pd.DataFrame(viol).loc[any_viol].apply(lambda r: ", ".join([c for c,v in r.items() if v]), axis=1)
630
  offenders.index = offenders.index + 1; oor_table = offenders
631
 
632
- metrics_val = {
633
- "R2": r2_score(df_val[TARGET], df_val["UCS_Pred"]),
634
- "RMSE": rmse(df_val[TARGET], df_val["UCS_Pred"]),
635
- "MAE": mean_absolute_error(df_val[TARGET], df_val["UCS_Pred"])
636
- }
 
 
637
  st.session_state.results["metrics_val"] = metrics_val
638
  st.session_state.results["summary_val"] = {
639
  "n_points": len(df_val),
@@ -645,49 +682,58 @@ if st.session_state.app_step == "val":
645
  status.update(label="Predictions ready ✓", state="complete")
646
 
647
  if "Validate" in st.session_state.results:
648
- st.subheader("Validation Results")
649
- sv = st.session_state.results["summary_val"]; oor_table = st.session_state.results.get("oor_table")
 
650
  metrics_val = st.session_state.results.get("metrics_val")
651
- c1, c2, c3 = st.columns(3)
652
- c1.metric("R²", f"{metrics_val['R2']:.4f}")
653
- c2.metric("RMSE", f"{metrics_val['RMSE']:.4f}")
654
- c3.metric("MAE", f"{metrics_val['MAE']:.4f}")
655
-
656
- left, right, _ = st.columns([1, 1, 3])
657
- with left:
658
- st.plotly_chart(
659
- cross_plot_interactive(st.session_state.results["Validate"][TARGET],
660
- st.session_state.results["Validate"]["UCS_Pred"]),
661
- use_container_width=False, config={"displayModeBar": False}
662
- )
663
- with right:
664
- st.plotly_chart(
665
- depth_or_index_track_interactive(st.session_state.results["Validate"],
666
- include_actual=True),
667
- use_container_width=False, config={"displayModeBar": False}
668
- )
669
 
670
  if sv["oor_pct"] > 0:
671
- st.write("**★ Note:** Some validation inputs fall outside the training min–max ranges. Interpret predictions with caution.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
 
673
  if oor_table is not None:
674
  st.write("*Out-of-range rows (vs. Training min–max):*")
675
  st.dataframe(oor_table, use_container_width=True)
676
 
677
  st.markdown("---")
678
- sheets = {"Validate_with_pred": st.session_state.results["Validate"]}
679
- rows = []
680
- for name, key in [("Train","metrics_train"), ("Test","metrics_test"), ("Validate","metrics_val")]:
681
- m = st.session_state.results.get(key)
682
- if m: rows.append({"Split": name, **{k: round(v,6) for k,v in m.items()}})
683
- summary_df = pd.DataFrame(rows) if rows else None
684
  try:
685
  buf = io.BytesIO()
686
  with pd.ExcelWriter(buf, engine="openpyxl") as xw:
687
- for name, frame in sheets.items():
688
- frame.to_excel(xw, sheet_name=name[:31], index=False)
689
- if summary_df is not None:
690
- summary_df.to_excel(xw, sheet_name="Summary", index=False)
 
691
  st.download_button(
692
  "Export Validation Results to Excel",
693
  data=buf.getvalue(),
@@ -697,89 +743,111 @@ if st.session_state.app_step == "val":
697
  except Exception as e:
698
  st.warning(str(e))
699
 
 
700
  # =========================
701
  # PREDICTION (no actuals)
702
  # =========================
703
  if st.session_state.app_step == "pred":
704
- st.sidebar.header("Prediction (New data)")
705
- pred_file = st.sidebar.file_uploader("Upload Prediction Excel (no UCS column)", type=["xlsx","xls"], key="pred_upload")
706
  if pred_file is not None:
707
  _book_tmp = read_book_bytes(pred_file.getvalue())
708
  if _book_tmp:
709
  first_df = next(iter(_book_tmp.values()))
710
  st.sidebar.caption(f"**Data loaded:** {pred_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
711
 
712
- preview_pred_btn = st.sidebar.button("Preview data", use_container_width=True, disabled=(pred_file is None))
713
- if preview_pred_btn and pred_file is not None:
714
  _book = read_book_bytes(pred_file.getvalue())
715
- preview_modal_val(_book, FEATURES) # same preview (tracks & summary)
716
 
717
- do_pred_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
718
- st.sidebar.button("⬅ Back to Case Building", on_click=lambda: st.session_state.update(app_step="dev"), use_container_width=True)
719
- st.sidebar.button("⬅ Back to Validation", on_click=lambda: st.session_state.update(app_step="val"), use_container_width=True)
720
 
721
  st.subheader("Prediction")
722
- st.write("Upload a new dataset (without UCS) and generate UCS predictions.")
723
 
724
- if do_pred_btn and pred_file is not None:
725
  with st.status("Predicting…", expanded=False) as status:
726
  pbook = read_book_bytes(pred_file.getvalue())
727
  if not pbook: status.update(label="Could not read the Prediction Excel.", state="error"); st.stop()
728
  status.update(label="Workbook read ✓")
729
- vname = list(pbook.keys())[0]
730
- df = pbook[vname].copy()
731
- if not ensure_cols(df, FEATURES): status.update(label="Missing required columns.", state="error"); st.stop()
 
732
  status.update(label="Columns validated ✓")
733
- df["UCS_Pred"] = model.predict(df[FEATURES])
734
- st.session_state.results["PredictOnly"] = df
735
 
736
- ranges = st.session_state.train_ranges; oor_pct = 0.0
 
737
  if ranges:
738
- any_viol = pd.DataFrame({f: (df[f] < ranges[f][0]) | (df[f] > ranges[f][1]) for f in FEATURES}).any(axis=1)
 
739
  oor_pct = float(any_viol.mean()*100.0)
740
 
741
- st.session_state.results["summary_predonly"] = {
742
- "n_points": len(df),
743
- "pred_min": float(df["UCS_Pred"].min()),
744
- "pred_max": float(df["UCS_Pred"].max()),
745
- "pred_mean": float(df["UCS_Pred"].mean()),
746
- "pred_std": float(df["UCS_Pred"].std(ddof=0)),
747
- "oor_pct": oor_pct
748
  }
749
  status.update(label="Predictions ready ✓", state="complete")
750
 
751
  if "PredictOnly" in st.session_state.results:
752
- st.success("Predictions ready ✓")
753
- s = st.session_state.results["summary_predonly"]
754
- # Summary table left, track right
755
- left, right, _ = st.columns([1, 1, 3])
756
- with left:
757
- st.table(pd.DataFrame({
758
- "Metric": ["# points", "Pred min", "Pred max", "Pred mean", "Pred std", "OOR %"],
759
- "Value": [s["n_points"], s["pred_min"], s["pred_max"], s["pred_mean"], s["pred_std"], f"{s['oor_pct']:.1f}%"]
760
- }))
761
- st.caption(" OOR%: fraction of rows where any input lies outside the training min–max range.")
762
- with right:
763
- st.plotly_chart(
764
- depth_or_index_track_interactive(st.session_state.results["PredictOnly"], include_actual=False),
765
- use_container_width=False, config={"displayModeBar": False}
766
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
 
768
  st.markdown("---")
 
769
  try:
770
  buf = io.BytesIO()
771
  with pd.ExcelWriter(buf, engine="openpyxl") as xw:
772
- st.session_state.results["PredictOnly"].to_excel(xw, sheet_name="Predictions", index=False)
773
- pd.DataFrame([s]).to_excel(xw, sheet_name="Summary", index=False)
774
  st.download_button(
775
- "Export Predictions to Excel",
776
  data=buf.getvalue(),
777
- file_name="UCS_Predictions.xlsx",
778
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
779
  )
780
  except Exception as e:
781
  st.warning(str(e))
782
 
 
783
  # =========================
784
  # Footer
785
  # =========================
 
6
  import numpy as np
7
  import joblib
8
 
9
+ # matplotlib only for preview modal thumbnails
10
  import matplotlib
11
  matplotlib.use("Agg")
12
  import matplotlib.pyplot as plt
 
14
  import plotly.graph_objects as go
15
  from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
16
 
 
 
 
 
 
17
 
18
  # =========================
19
+ # Defaults / Constants
20
  # =========================
21
  FEATURES = ["Q, gpm", "SPP(psi)", "T (kft.lbf)", "WOB (klbf)", "ROP (ft/h)"]
22
  TARGET = "UCS"
23
+
24
  MODELS_DIR = Path("models")
25
  DEFAULT_MODEL = MODELS_DIR / "ucs_rf.joblib"
26
  MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
27
 
28
  COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
29
 
30
+ # Fixed pixel sizes to keep appearance stable across pages
31
+ CROSS_W, CROSS_H = 390, 390 # cross-plot = square
32
+ TRACK_W, TRACK_H = 220, 700 # slim/tall log-style track
33
+
34
+
35
+ # =========================
36
+ # Page / Theme
37
+ # =========================
38
+ st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
39
+ st.markdown(
40
+ """
41
+ <style>
42
+ header, footer { visibility: hidden !important; }
43
+ .stApp { background: #FFFFFF; }
44
+ section[data-testid="stSidebar"] { background: #F6F9FC; }
45
+ .block-container { padding-top: .5rem; padding-bottom: .5rem; }
46
+ .stButton>button{
47
+ background:#007bff; color:#fff; font-weight:bold;
48
+ border-radius:8px; border:none; padding:10px 24px;
49
+ }
50
+ .stButton>button:hover{ background:#0056b3; }
51
+ .st-hero { display:flex; align-items:center; gap:16px; padding-top: 4px; }
52
+ .st-hero .brand { width:110px; height:110px; object-fit:contain; }
53
+ .st-hero h1 { margin:0; line-height:1.05; }
54
+ .st-hero .tagline { margin:2px 0 0 2px; color:#6b7280; font-size:1.05rem; font-style:italic; }
55
+ [data-testid="stBlock"]{ margin-top:0 !important; }
56
+ .help-foot { color:#6b7280; font-size:0.95rem; }
57
+ </style>
58
+ """,
59
+ unsafe_allow_html=True
60
+ )
61
+
62
+
63
+ # =========================
64
+ # Small helpers (used by password gate too)
65
+ # =========================
66
  def inline_logo(path="logo.png") -> str:
67
  try:
68
  p = Path(path)
69
+ if not p.exists():
70
+ return ""
71
  return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
72
  except Exception:
73
  return ""
74
 
75
+
76
+ # =========================
77
+ # Password gate (branded)
78
+ # =========================
79
  def add_password_gate() -> bool:
80
  """
81
+ Ask for a password (APP_PASSWORD in Secrets or Env) before rendering the app.
82
+ If not configured, block with a clear admin message.
83
  """
84
+ # pull required password
85
  required = ""
86
  try:
87
  required = st.secrets.get("APP_PASSWORD", "")
 
100
  </div>
101
  <div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected Area</div>
102
  <div style="color:#6b7280;margin-bottom:14px;">
103
+ Admin action required: set <code>APP_PASSWORD</code> in <b>Settings → Secrets</b> (or as an
104
+ environment variable) and restart the Space.
105
  </div>
106
  """,
107
  unsafe_allow_html=True,
 
121
  </div>
122
  </div>
123
  <div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected</div>
124
+ <div style="color:#6b7280;margin-bottom:14px;">Please enter your access key to continue.</div>
 
 
125
  """,
126
  unsafe_allow_html=True
127
  )
 
128
  pwd = st.text_input("Access key", type="password", placeholder="••••••••")
129
+ if st.button("Unlock", type="primary"):
130
+ if pwd == required:
131
+ st.session_state.auth_ok = True
132
+ st.rerun()
133
+ else:
134
+ st.error("Incorrect key. Please try again.")
 
 
135
  st.stop()
136
 
 
 
 
 
 
137
 
138
+ # Call the password gate before anything else is drawn
139
+ add_password_gate()
140
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
  # =========================
143
+ # General helpers
144
  # =========================
145
  try:
146
  dialog = st.dialog
 
177
 
178
  def read_book_bytes(data_bytes: bytes):
179
  if not data_bytes: return {}
180
+ try:
181
+ return parse_excel(data_bytes)
182
  except Exception as e:
183
+ st.error(f"Failed to read Excel: {e}")
184
+ return {}
185
 
186
  def find_sheet(book, names):
187
  low2orig = {k.lower(): k for k in book.keys()}
188
  for nm in names:
189
+ if nm.lower() in low2orig:
190
+ return low2orig[nm.lower()]
191
  return None
192
 
193
+
194
+ # ---------- Plot helpers (interactive, fixed size, full outline) ----------
195
+ def _add_full_frame(fig):
196
+ fig.update_layout(shapes=[dict(
197
+ type="rect", xref="paper", yref="paper", x0=0, y0=0, x1=1, y1=1,
198
+ line=dict(color="#444", width=1), fillcolor="rgba(0,0,0,0)"
199
+ )])
200
+
201
  def cross_plot_interactive(actual, pred):
 
202
  a = pd.Series(actual).astype(float)
203
  p = pd.Series(pred).astype(float)
204
  lo = float(np.nanmin([a.min(), p.min()]))
 
226
  width=CROSS_W, height=CROSS_H
227
  )
228
  fig.update_xaxes(
229
+ title_text="<b>Actual UCS</b>", range=[x0, x1], tickformat=",.0f",
230
+ ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror=True,
231
+ showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
 
 
232
  )
233
  fig.update_yaxes(
234
+ title_text="<b>Predicted UCS</b>", range=[x0, x1], tickformat=",.0f",
235
+ ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror=True,
236
+ showgrid=True, gridcolor="rgba(0,0,0,0.12)", scaleanchor="x", scaleratio=1,
 
 
237
  automargin=True
238
  )
239
+ _add_full_frame(fig)
240
  return fig
241
 
242
  def depth_or_index_track_interactive(df, include_actual=True):
 
243
  depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
244
  if depth_col is not None:
245
  y = df[depth_col]; y_label = depth_col
 
247
  y = np.arange(1, len(df) + 1); y_label = "Point Index"
248
 
249
  fig = go.Figure()
 
 
250
  fig.add_trace(go.Scatter(
251
  x=df["UCS_Pred"], y=y, mode="lines",
252
  line=dict(color=COLORS["pred"], width=1.8),
253
  name="UCS_Pred",
254
  hovertemplate="UCS_Pred: %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
255
  ))
 
256
  if include_actual and TARGET in df.columns:
257
  fig.add_trace(go.Scatter(
258
  x=df[TARGET], y=y, mode="lines",
 
263
 
264
  fig.update_layout(
265
  paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
266
+ margin=dict(l=44, r=6, t=6, b=36),
267
  hovermode="closest", font=dict(size=13),
268
+ legend=dict(x=0.98, y=0.05, xanchor="right", yanchor="bottom",
269
+ bgcolor="rgba(255,255,255,0.75)", bordercolor="#cccccc", borderwidth=1),
 
 
270
  legend_title_text="",
271
  width=TRACK_W, height=TRACK_H
272
  )
273
  fig.update_xaxes(
274
+ title_text="<b>UCS</b>", side="top", tickformat=",.0f",
275
  ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror=True,
276
+ showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
 
277
  )
278
  fig.update_yaxes(
279
  title_text=f"<b>{y_label}</b>", autorange="reversed",
280
  ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror=True,
281
+ showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
 
282
  )
283
+ _add_full_frame(fig)
284
  return fig
285
 
286
+
287
  # ---------- Preview modal helpers (matplotlib static) ----------
288
  def make_index_tracks(df: pd.DataFrame, cols: list[str]):
289
  cols = [c for c in cols if c in df.columns]
 
325
  if not tabs:
326
  first_name = list(book.keys())[0]
327
  tabs = [first_name]; data = [book[first_name]]
 
328
  t_objs = st.tabs(tabs)
329
  for t, df in zip(t_objs, data):
330
  with t:
331
  t1, t2 = st.tabs(["Tracks", "Summary"])
332
+ with t1: st.pyplot(make_index_tracks(df, FEATURES), use_container_width=True)
333
+ with t2: st.dataframe(stats_table(df, FEATURES), use_container_width=True)
334
 
335
  @dialog("Preview data")
336
+ def preview_modal_single(book: dict[str, pd.DataFrame], feature_cols: list[str], names=("Validation","Validate","validation2","Val","val","Prediction","Pred")):
337
  if not book:
338
  st.info("No data loaded yet."); return
339
+ vname = find_sheet(book, list(names)) or list(book.keys())[0]
340
  df = book[vname]
341
  t1, t2 = st.tabs(["Tracks", "Summary"])
342
  with t1: st.pyplot(make_index_tracks(df, feature_cols), use_container_width=True)
343
  with t2: st.dataframe(stats_table(df, feature_cols), use_container_width=True)
344
 
345
+
346
  # =========================
347
  # Model presence
348
  # =========================
 
379
  st.error(f"Failed to load model: {model_path}\n{e}")
380
  st.stop()
381
 
382
+ # meta overrides / inference
383
  meta_path = MODELS_DIR / "meta.json"
384
  if meta_path.exists():
385
  try:
386
  meta = json.loads(meta_path.read_text(encoding="utf-8"))
387
+ FEATURES = meta.get("features", FEATURES)
388
+ TARGET = meta.get("target", TARGET)
389
+ except Exception:
390
+ pass
391
+
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
  # =========================
394
  # Session state
395
  # =========================
396
+ if "app_step" not in st.session_state: st.session_state.app_step = "intro"
397
  if "results" not in st.session_state: st.session_state.results = {}
398
  if "train_ranges" not in st.session_state: st.session_state.train_ranges = None
399
 
400
+ # persist dev file
401
  for k, v in {
402
  "dev_ready": False,
403
  "dev_file_loaded": False,
 
411
  }.items():
412
  if k not in st.session_state: st.session_state[k] = v
413
 
414
+
415
  # =========================
416
  # Hero header
417
  # =========================
 
428
  unsafe_allow_html=True,
429
  )
430
 
431
+
432
+ # =========================
433
+ # INTRO
434
+ # =========================
435
+ if st.session_state.app_step == "intro":
436
+ st.header("Welcome!")
437
+ st.markdown("This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data.")
438
+ st.subheader("Expected Input Features (in Order)")
439
+ st.markdown(
440
+ "- Q, gpm — Flow rate (gallons per minute)\n"
441
+ "- SPP(psi) — Stand pipe pressure\n"
442
+ "- T (kft.lbf) — Torque (thousand foot-pounds)\n"
443
+ "- WOB (klbf) — Weight on bit\n"
444
+ "- ROP (ft/h) — Rate of penetration"
445
+ )
446
+ st.subheader("How It Works")
447
+ st.markdown(
448
+ "1. **Upload your data to build the case and preview the performance of our model.** \n"
449
+ "2. Click **Run Model** to compute metrics and plots. \n"
450
+ "3. Click **Proceed to Validation** to validate on a new dataset (with actual UCS, if available). \n"
451
+ "4. Click **Proceed to Prediction** to generate predictions only (no actuals). \n"
452
+ "5. Export results to Excel at any time."
453
+ )
454
+ if st.button("Start Showcase", type="primary"):
455
+ st.session_state.app_step = "dev"; st.rerun()
456
+
457
+
458
  # =========================
459
  # CASE BUILDING (Development)
460
  # =========================
 
489
  f"{st.session_state.dev_file_rows} rows × {st.session_state.dev_file_cols} cols"
490
  )
491
 
492
+ # Sidebar actions (proceed buttons enabled always)
493
+ if st.sidebar.button("Preview data", use_container_width=True, disabled=not st.session_state.dev_file_loaded):
494
  st.session_state.dev_preview_request = True
 
495
  run_btn = st.sidebar.button("Run Model", type="primary", use_container_width=True)
496
+ if st.sidebar.button("Proceed to Validation ▶", use_container_width=True):
497
+ st.session_state.app_step = "val"; st.rerun()
498
+ if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True):
499
+ st.session_state.app_step = "pred"; st.rerun()
500
+
501
+ # Title + helper
502
+ st.subheader("Case Building (Development)")
503
+ if st.session_state.dev_ready:
504
+ st.success("Case has been built and results are displayed below.")
505
+ elif st.session_state.dev_file_loaded and st.session_state.dev_previewed:
506
+ st.info("Previewed ✓ — now click **Run Model** to build the case.")
507
+ elif st.session_state.dev_file_loaded:
508
+ st.info("📄 **Preview uploaded data** using the sidebar button, then click **Run Model**.")
509
+ else:
510
+ st.write("**Upload your data to build a case, then run the model to review development performance.**")
511
 
512
+ # open preview dialog if requested
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  if st.session_state.dev_preview_request and st.session_state.dev_file_bytes:
514
  _book = read_book_bytes(st.session_state.dev_file_bytes)
515
  st.session_state.dev_previewed = True
516
  st.session_state.dev_preview_request = False
517
  preview_modal_dev(_book, FEATURES)
518
 
519
+ # Run
520
  if run_btn and st.session_state.dev_file_bytes:
521
  with st.status("Processing…", expanded=False) as status:
522
  book = read_book_bytes(st.session_state.dev_file_bytes)
523
+ if not book:
524
+ status.update(label="Failed to read workbook.", state="error"); st.stop()
525
  status.update(label="Workbook read ✓")
526
  sh_train = find_sheet(book, ["Train","Training","training2","train","training"])
527
  sh_test = find_sheet(book, ["Test","Testing","testing2","test","testing"])
528
  if sh_train is None or sh_test is None:
529
  status.update(label="Workbook must include Train/Training/training2 and Test/Testing/testing2.", state="error"); st.stop()
530
+
531
  df_tr = book[sh_train].copy(); df_te = book[sh_test].copy()
532
  if not (ensure_cols(df_tr, FEATURES + [TARGET]) and ensure_cols(df_te, FEATURES + [TARGET])):
533
  status.update(label="Missing required columns.", state="error"); st.stop()
 
552
  st.session_state.train_ranges = {f:(float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
553
 
554
  st.session_state.dev_ready = True
555
+ status.update(label="Done ✓", state="complete")
556
+ st.rerun()
557
 
558
+ # Results
559
  if ("Train" in st.session_state.results) or ("Test" in st.session_state.results):
560
  tab1, tab2 = st.tabs(["Training", "Testing"])
561
+
562
+ def _result_block(df, metrics):
563
+ c1,c2,c3 = st.columns(3)
564
+ c1.metric("R²", f"{metrics['R2']:.4f}")
565
+ c2.metric("RMSE", f"{metrics['RMSE']:.4f}")
566
+ c3.metric("MAE", f"{metrics['MAE']:.4f}")
567
+
568
+ # center band with two equal columns
569
+ sp_l, main, sp_r = st.columns([1, 8, 1])
570
+ with main:
571
+ col1, col2 = st.columns(2)
572
+ with col1:
573
+ st.plotly_chart(
574
+ cross_plot_interactive(df[TARGET], df["UCS_Pred"]),
575
+ use_container_width=False,
576
+ config={"displayModeBar": False, "scrollZoom": True}
577
+ )
578
+ with col2:
579
+ st.plotly_chart(
580
+ depth_or_index_track_interactive(df, include_actual=True),
581
+ use_container_width=False,
582
+ config={"displayModeBar": False, "scrollZoom": True}
583
+ )
584
+
585
  if "Train" in st.session_state.results:
586
  with tab1:
587
+ _result_block(st.session_state.results["Train"], st.session_state.results["metrics_train"])
 
 
 
 
 
 
 
 
 
 
588
  if "Test" in st.session_state.results:
589
  with tab2:
590
+ _result_block(st.session_state.results["Test"], st.session_state.results["metrics_test"])
 
 
 
 
 
 
 
 
 
591
 
592
  st.markdown("---")
593
+ # export
594
+ sheets = {}
595
+ rows = []
596
  if "Train" in st.session_state.results:
597
  sheets["Train_with_pred"] = st.session_state.results["Train"]
598
  rows.append({"Split":"Train", **{k:round(v,6) for k,v in st.session_state.results["metrics_train"].items()}})
 
616
  except Exception as e:
617
  st.warning(str(e))
618
 
619
+
620
  # =========================
621
+ # VALIDATION (with actuals if present)
622
  # =========================
623
  if st.session_state.app_step == "val":
624
  st.sidebar.header("Validate the model")
 
629
  first_df = next(iter(_book_tmp.values()))
630
  st.sidebar.caption(f"**Data loaded:** {validation_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
631
 
632
+ if st.sidebar.button("Preview data", use_container_width=True, disabled=(validation_file is None)):
 
633
  _book = read_book_bytes(validation_file.getvalue())
634
+ preview_modal_single(_book, FEATURES)
635
 
636
  predict_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
637
+ if st.sidebar.button("Proceed to Prediction ", use_container_width=True):
638
+ st.session_state.app_step = "pred"; st.rerun()
639
+ if st.sidebar.button("⬅ Back to Case Building", use_container_width=True):
640
+ st.session_state.app_step = "dev"; st.rerun()
641
 
642
+ st.subheader("Validation")
643
+ st.write("Upload a dataset to generate UCS predictions and evaluate performance on unseen data.")
644
 
645
  if predict_btn and validation_file is not None:
646
  with st.status("Predicting…", expanded=False) as status:
 
649
  status.update(label="Workbook read ✓")
650
  vname = find_sheet(vbook, ["Validation","Validate","validation2","Val","val"]) or list(vbook.keys())[0]
651
  df_val = vbook[vname].copy()
652
+ if not ensure_cols(df_val, FEATURES):
653
+ status.update(label="Missing required columns.", state="error"); st.stop()
654
  status.update(label="Columns validated ✓")
655
  df_val["UCS_Pred"] = model.predict(df_val[FEATURES])
656
  st.session_state.results["Validate"] = df_val
 
664
  offenders["Violations"] = pd.DataFrame(viol).loc[any_viol].apply(lambda r: ", ".join([c for c,v in r.items() if v]), axis=1)
665
  offenders.index = offenders.index + 1; oor_table = offenders
666
 
667
+ metrics_val = None
668
+ if TARGET in df_val.columns:
669
+ metrics_val = {
670
+ "R2": r2_score(df_val[TARGET], df_val["UCS_Pred"]),
671
+ "RMSE": rmse(df_val[TARGET], df_val["UCS_Pred"]),
672
+ "MAE": mean_absolute_error(df_val[TARGET], df_val["UCS_Pred"])
673
+ }
674
  st.session_state.results["metrics_val"] = metrics_val
675
  st.session_state.results["summary_val"] = {
676
  "n_points": len(df_val),
 
682
  status.update(label="Predictions ready ✓", state="complete")
683
 
684
  if "Validate" in st.session_state.results:
685
+ dfv = st.session_state.results["Validate"]
686
+ sv = st.session_state.results["summary_val"]
687
+ oor_table = st.session_state.results.get("oor_table")
688
  metrics_val = st.session_state.results.get("metrics_val")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ if metrics_val is not None:
694
+ c1, c2, c3 = st.columns(3)
695
+ c1.metric("R²", f"{metrics_val['R2']:.4f}")
696
+ c2.metric("RMSE", f"{metrics_val['RMSE']:.4f}")
697
+ c3.metric("MAE", f"{metrics_val['MAE']:.4f}")
698
+ else:
699
+ c1, c2, c3 = st.columns(3)
700
+ c1.metric("# points", f"{sv['n_points']}")
701
+ c2.metric("Pred min", f"{sv['pred_min']:.2f}")
702
+ c3.metric("Pred max", f"{sv['pred_max']:.2f}")
703
+
704
+ sp_l, main, sp_r = st.columns([1, 8, 1])
705
+ with main:
706
+ col1, col2 = st.columns(2)
707
+ with col1:
708
+ if TARGET in dfv.columns:
709
+ st.plotly_chart(
710
+ cross_plot_interactive(dfv[TARGET], dfv["UCS_Pred"]),
711
+ use_container_width=False,
712
+ config={"displayModeBar": False, "scrollZoom": True}
713
+ )
714
+ else:
715
+ st.info("Actual UCS values are not available in the validation data. Cross-plot cannot be generated.")
716
+ with col2:
717
+ st.plotly_chart(
718
+ depth_or_index_track_interactive(dfv, include_actual=(TARGET in dfv.columns)),
719
+ use_container_width=False,
720
+ config={"displayModeBar": False, "scrollZoom": True}
721
+ )
722
 
723
  if oor_table is not None:
724
  st.write("*Out-of-range rows (vs. Training min–max):*")
725
  st.dataframe(oor_table, use_container_width=True)
726
 
727
  st.markdown("---")
728
+ # export
 
 
 
 
 
729
  try:
730
  buf = io.BytesIO()
731
  with pd.ExcelWriter(buf, engine="openpyxl") as xw:
732
+ dfv.to_excel(xw, sheet_name="Validate_with_pred", index=False)
733
+ if metrics_val is not None:
734
+ pd.DataFrame([{"Split":"Validate", **{k: round(v,6) for k,v in metrics_val.items()}}]).to_excel(
735
+ xw, sheet_name="Summary", index=False
736
+ )
737
  st.download_button(
738
  "Export Validation Results to Excel",
739
  data=buf.getvalue(),
 
743
  except Exception as e:
744
  st.warning(str(e))
745
 
746
+
747
  # =========================
748
  # PREDICTION (no actuals)
749
  # =========================
750
  if st.session_state.app_step == "pred":
751
+ st.sidebar.header("Prediction")
752
+ pred_file = st.sidebar.file_uploader("Upload Prediction Excel", type=["xlsx","xls"], key="pred_upload")
753
  if pred_file is not None:
754
  _book_tmp = read_book_bytes(pred_file.getvalue())
755
  if _book_tmp:
756
  first_df = next(iter(_book_tmp.values()))
757
  st.sidebar.caption(f"**Data loaded:** {pred_file.name} • {first_df.shape[0]} rows × {first_df.shape[1]} cols")
758
 
759
+ if st.sidebar.button("Preview data", use_container_width=True, disabled=(pred_file is None)):
 
760
  _book = read_book_bytes(pred_file.getvalue())
761
+ preview_modal_single(_book, FEATURES, names=("Prediction","Pred","Sheet1","Data"))
762
 
763
+ pred_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
764
+ if st.sidebar.button("⬅ Back to Validation", use_container_width=True):
765
+ st.session_state.app_step = "val"; st.rerun()
766
 
767
  st.subheader("Prediction")
768
+ st.write("Upload a dataset to generate UCS predictions (no actuals required).")
769
 
770
+ if pred_btn and pred_file is not None:
771
  with st.status("Predicting…", expanded=False) as status:
772
  pbook = read_book_bytes(pred_file.getvalue())
773
  if not pbook: status.update(label="Could not read the Prediction Excel.", state="error"); st.stop()
774
  status.update(label="Workbook read ✓")
775
+ pname = find_sheet(pbook, ["Prediction","Pred"]) or list(pbook.keys())[0]
776
+ dfp = pbook[pname].copy()
777
+ if not ensure_cols(dfp, FEATURES):
778
+ status.update(label="Missing required columns.", state="error"); st.stop()
779
  status.update(label="Columns validated ✓")
780
+ dfp["UCS_Pred"] = model.predict(dfp[FEATURES])
781
+ st.session_state.results["PredictOnly"] = dfp
782
 
783
+ ranges = st.session_state.train_ranges
784
+ oor_pct = None
785
  if ranges:
786
+ viol = {f: (dfp[f] < ranges[f][0]) | (dfp[f] > ranges[f][1]) for f in FEATURES}
787
+ any_viol = pd.DataFrame(viol).any(axis=1)
788
  oor_pct = float(any_viol.mean()*100.0)
789
 
790
+ st.session_state.results["summary_pred"] = {
791
+ "n_points": len(dfp),
792
+ "pred_min": float(dfp["UCS_Pred"].min()),
793
+ "pred_max": float(dfp["UCS_Pred"].max()),
794
+ "pred_mean": float(dfp["UCS_Pred"].mean()),
795
+ "pred_std": float(dfp["UCS_Pred"].std(ddof=0)),
796
+ "oor_pct": oor_pct if oor_pct is not None else None
797
  }
798
  status.update(label="Predictions ready ✓", state="complete")
799
 
800
  if "PredictOnly" in st.session_state.results:
801
+ dfp = st.session_state.results["PredictOnly"]
802
+ sp = st.session_state.results["summary_pred"]
803
+
804
+ # summary table (left) + track (right)
805
+ sp_l, main, sp_r = st.columns([1, 8, 1])
806
+ with main:
807
+ col1, col2 = st.columns([1, 1])
808
+ with col1:
809
+ table = pd.DataFrame({
810
+ "Metric": ["# points", "Pred min", "Pred max", "Pred mean", "Pred std", "OOR %"],
811
+ "Value": [
812
+ sp["n_points"],
813
+ f"{sp['pred_min']:.2f}",
814
+ f"{sp['pred_max']:.2f}",
815
+ f"{sp['pred_mean']:.2f}",
816
+ f"{sp['pred_std']:.2f}",
817
+ (f"{sp['oor_pct']:.1f}%" if sp["oor_pct"] is not None else "N/A")
818
+ ]
819
+ })
820
+ st.write("✅ Predictions ready ✓")
821
+ st.dataframe(table, use_container_width=True)
822
+ st.markdown(
823
+ "<div class='help-foot'>* OOR% = percentage of rows with any input feature outside the "
824
+ "training min–max range (computed when Case Building has been run).</div>",
825
+ unsafe_allow_html=True
826
+ )
827
+ with col2:
828
+ st.plotly_chart(
829
+ depth_or_index_track_interactive(dfp, include_actual=False),
830
+ use_container_width=False,
831
+ config={"displayModeBar": False, "scrollZoom": True}
832
+ )
833
 
834
  st.markdown("---")
835
+ # export
836
  try:
837
  buf = io.BytesIO()
838
  with pd.ExcelWriter(buf, engine="openpyxl") as xw:
839
+ dfp.to_excel(xw, sheet_name="Prediction_with_pred", index=False)
840
+ pd.DataFrame([sp]).to_excel(xw, sheet_name="Summary", index=False)
841
  st.download_button(
842
+ "Export Prediction Results to Excel",
843
  data=buf.getvalue(),
844
+ file_name="UCS_Prediction_Results.xlsx",
845
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
846
  )
847
  except Exception as e:
848
  st.warning(str(e))
849
 
850
+
851
  # =========================
852
  # Footer
853
  # =========================