UCS2014 commited on
Commit
7f0fa68
·
verified ·
1 Parent(s): d3b3c33

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +201 -145
app.py CHANGED
@@ -22,9 +22,7 @@ TARGET = "UCS"
22
  MODELS_DIR = Path("models")
23
  DEFAULT_MODEL = MODELS_DIR / "ucs_rf.joblib"
24
  MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
25
-
26
- # Updated colors for a dark blue theme
27
- COLORS = {"pred": "#4A90E2", "actual": "#FFC107", "ref": "#B0B0B0"}
28
 
29
  # ---- Plot sizing controls ----
30
  CROSS_W = 450 # px (matplotlib figure size; Streamlit will still scale)
@@ -38,44 +36,39 @@ BOLD_FONT = "Arial Black, Arial, sans-serif" # used for bold axis titles & tick
38
  # =========================
39
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
40
 
41
- # This is the new CSS block for the sticky header and message box
42
  st.markdown("""
43
  <style>
44
- /* Changed to a lighter blue for visibility on a dark background */
45
- .app-title {
46
- color: #89b4f2;
47
- font-weight:800;
48
- font-size:1.2rem;
 
 
 
49
  }
50
 
51
- /* Changed background to a dark blue to match the theme */
52
  .fixed-header-container {
53
  position: sticky;
54
  top: 0;
55
  z-index: 1000;
56
- background-color: #0a192f;
57
  padding: 10px 0;
58
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
59
- }
60
-
61
- /* Changed to a light gray/blue to be visible on the dark background */
62
- .info-box {
63
- background-color: #172a45;
64
- color: #b1c7e0;
65
- padding: 10px;
66
- border-radius: 5px;
67
- margin-bottom: 1rem;
68
  }
69
 
 
70
  .main .block-container {
71
- padding-top: 250px;
72
  }
73
 
74
  /* General CSS */
75
  .brand-logo { width: 16px; height: auto; object-fit: contain; }
76
  .sidebar-header { display:flex; align-items:center; gap:12px; }
77
  .sidebar-header .text h1 { font-size: 1.05rem; margin:0; line-height:1.1; }
78
- .sidebar-header .text .tag { font-size: .85rem; color:#b1c7e0; margin:2px 0 0; } /* Changed color */
79
  .centered-container {
80
  display: flex;
81
  flex-direction: column;
@@ -85,24 +78,37 @@ st.markdown("""
85
  </style>
86
  """, unsafe_allow_html=True)
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  # Make the Preview expander title & tabs sticky (pinned to the top)
89
  st.markdown("""
90
  <style>
91
- /* Updated to a dark background for consistency */
92
  div[data-testid="stExpander"] > details > summary {
93
- position: sticky;
94
- top: 0;
95
- z-index: 10;
96
- background: #0a192f;
97
- border-bottom: 1px solid #172a45;
98
  }
99
- /* Updated to a dark background for consistency */
100
  div[data-testid="stExpander"] div[data-baseweb="tab-list"] {
101
- position: sticky;
102
- top: 42px;
103
- z-index: 9;
104
- background: #0a192f;
105
- padding-top: 6px;
106
  }
107
  </style>
108
  """, unsafe_allow_html=True)
@@ -140,8 +146,8 @@ def add_password_gate() -> None:
140
  st.sidebar.markdown(f"""
141
  <div class="centered-container">
142
  <img src="{inline_logo('logo.png')}" style="width: 200px; height: auto; object-fit: contain;">
143
- <div class='app-title' style='margin-top: 10px;'>ST_GeoMech_UCS</div>
144
- <div style='color:#b1c7e0;'>Smart Thinking • Secure Access</div>
145
  </div>
146
  """, unsafe_allow_html=True
147
  )
@@ -213,7 +219,7 @@ def df_centered_rounded(df: pd.DataFrame, hide_index=True):
213
  # =========================
214
  def cross_plot_static(actual, pred):
215
  a = pd.Series(actual, dtype=float)
216
- p = pd.Series(pred, dtype=float)
217
 
218
  fixed_min, fixed_max = 6000, 10000
219
  ticks = np.arange(fixed_min, fixed_max + 1, 1000)
@@ -224,9 +230,6 @@ def cross_plot_static(actual, pred):
224
  dpi=dpi,
225
  constrained_layout=False
226
  )
227
- # Set the background color of the plot
228
- fig.patch.set_facecolor('#0a192f')
229
- ax.set_facecolor('#0a192f')
230
 
231
  ax.scatter(a, p, s=16, c=COLORS["pred"], alpha=0.9, linewidths=0)
232
  ax.plot([fixed_min, fixed_max], [fixed_min, fixed_max],
@@ -234,21 +237,22 @@ def cross_plot_static(actual, pred):
234
 
235
  ax.set_xlim(fixed_min, fixed_max)
236
  ax.set_ylim(fixed_min, fixed_max)
 
 
237
  ax.set_aspect("equal", adjustable="box") # true 45°
238
 
239
  fmt = FuncFormatter(lambda x, _: f"{int(x):,}")
240
  ax.xaxis.set_major_formatter(fmt)
241
  ax.yaxis.set_major_formatter(fmt)
242
 
243
- # Changed text and tick colors to a light blue for contrast
244
- ax.set_xlabel("Actual UCS (psi)", fontweight="bold", fontsize=12, color="#E0E7FF")
245
- ax.set_ylabel("Predicted UCS (psi)", fontweight="bold", fontsize=12, color="#E0E7FF")
246
- ax.tick_params(labelsize=10, colors="#E0E7FF")
247
 
248
  ax.grid(True, linestyle=":", alpha=0.3)
249
  for spine in ax.spines.values():
250
  spine.set_linewidth(1.1)
251
- spine.set_color("#A0A0A0") # Changed spine color to a lighter gray
252
 
253
  fig.subplots_adjust(left=0.16, bottom=0.16, right=0.98, top=0.98)
254
  return fig
@@ -293,40 +297,38 @@ def track_plot(df, include_actual=True):
293
 
294
  fig.update_layout(
295
  height=TRACK_H, width=None, # width auto-fits the column
296
- # Updated background colors for dark theme
297
- paper_bgcolor="#0a192f", plot_bgcolor="#0a192f",
298
  margin=dict(l=64, r=16, t=36, b=48), hovermode="closest",
299
- # Updated font color
300
- font=dict(size=FONT_SZ, color="#E0E7FF"),
301
  legend=dict(
302
  x=0.98, y=0.05, xanchor="right", yanchor="bottom",
303
- bgcolor="rgba(10,25,47,0.75)", bordercolor="#ccc", borderwidth=1
304
  ),
305
  legend_title_text=""
306
  )
307
 
308
- # Bold, light blue axis titles & ticks
309
  fig.update_xaxes(
310
  title_text="UCS (psi)",
311
- title_font=dict(size=16, family=BOLD_FONT, color="#E0E7FF"),
312
- tickfont=dict(size=11, family=BOLD_FONT, color="#E0E7FF"),
313
  side="top",
314
  range=[xmin, xmax],
315
  ticks="outside",
316
  tickformat=",.0f",
317
  tickmode="auto",
318
  tick0=tick0,
319
- showline=True, linewidth=1.2, linecolor="#A0A0A0", mirror=True,
320
- showgrid=True, gridcolor="rgba(255,255,255,0.12)", automargin=True
321
  )
322
  fig.update_yaxes(
323
  title_text=ylab,
324
- title_font=dict(size=16, family=BOLD_FONT, color="#E0E7FF"),
325
- tickfont=dict(size=11, family=BOLD_FONT, color="#E0E7FF"),
326
  range=y_range,
327
  ticks="outside",
328
- showline=True, linewidth=1.2, linecolor="#A0A0A0", mirror=True,
329
- showgrid=True, gridcolor="rgba(255,255,255,0.12)", automargin=True
330
  )
331
 
332
  return fig
@@ -337,22 +339,17 @@ def preview_tracks(df: pd.DataFrame, cols: list[str]):
337
  n = len(cols)
338
  if n == 0:
339
  fig, ax = plt.subplots(figsize=(4, 2))
340
- ax.text(0.5,0.5,"No selected columns",ha="center",va="center", color="#E0E7FF"); ax.axis("off")
341
  return fig
342
  fig, axes = plt.subplots(1, n, figsize=(2.2*n, 7.0), sharey=True, dpi=100)
343
- fig.patch.set_facecolor('#0a192f')
344
  if n == 1: axes = [axes]
345
  idx = np.arange(1, len(df) + 1)
346
  for ax, col in zip(axes, cols):
347
- ax.set_facecolor('#0a192f')
348
- ax.plot(df[col], idx, '-', lw=1.4, color="#b1c7e0") # Light blue plot line
349
- ax.set_xlabel(col, color="#b1c7e0"); ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis() # Light blue labels
350
- ax.tick_params(axis='both', colors='#b1c7e0') # Light blue ticks
351
  ax.grid(True, linestyle=":", alpha=0.3)
352
- for s in ax.spines.values():
353
- s.set_visible(True)
354
- s.set_edgecolor("#A0A0A0") # Lighter spine color
355
- axes[0].set_ylabel("Point Index", color="#b1c7e0") # Light blue label
356
  return fig
357
 
358
  # Modal wrapper (Streamlit compatibility)
@@ -440,17 +437,12 @@ st.session_state.setdefault("dev_preview",False)
440
  st.sidebar.markdown(f"""
441
  <div class="centered-container">
442
  <img src="{inline_logo('logo.png')}" style="width: 200px; height: auto; object-fit: contain;">
443
- <div class='app-title'>ST_GeoMech_UCS</div>
444
- <div style='color:#b1c7e0;'>Real-Time UCS Tracking While Drilling</div>
445
  </div>
446
  """, unsafe_allow_html=True
447
  )
448
 
449
- # =========================
450
- # Reusable Sticky Header Function
451
- # This function is now removed, replaced by the fixed-header-container
452
- # =========================
453
-
454
  # =========================
455
  # The main sticky container is defined here, before the app logic
456
  # This ensures it's rendered at the top of the page regardless of state.
@@ -484,7 +476,9 @@ with st.container():
484
  # =========================
485
  if st.session_state.app_step == "intro":
486
  st.header("Welcome!")
487
- st.markdown("This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data.")
 
 
488
  st.subheader("How It Works")
489
  st.markdown(
490
  "1) **Upload your data to build the case and preview the performance of our model.** \n"
@@ -492,7 +486,8 @@ if st.session_state.app_step == "intro":
492
  "3) **Proceed to Validation** (with actual UCS) or **Proceed to Prediction** (no UCS)."
493
  )
494
  if st.button("Start Showcase", type="primary"):
495
- st.session_state.app_step = "dev"; st.rerun()
 
496
 
497
  # =========================
498
  # CASE BUILDING
@@ -509,47 +504,70 @@ if st.session_state.app_step == "dev":
509
  tmp = read_book_bytes(st.session_state.dev_file_bytes)
510
  if tmp:
511
  df0 = next(iter(tmp.values()))
512
- st.sidebar.caption(f"**Data loaded:** {st.session_state.dev_file_name} • {df0.shape[0]} rows × {df0.shape[1]} cols")
 
 
513
 
514
- if st.sidebar.button("Preview data", use_container_width=True, disabled=not st.session_state.dev_file_loaded):
 
 
 
 
515
  preview_modal(read_book_bytes(st.session_state.dev_file_bytes))
516
  st.session_state.dev_preview = True
517
 
518
  run = st.sidebar.button("Run Model", type="primary", use_container_width=True)
519
- if st.sidebar.button("Proceed to Validation ▶", use_container_width=True): st.session_state.app_step="validate"; st.rerun()
520
- if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
 
 
 
 
521
 
522
  if run and st.session_state.dev_file_bytes:
523
  book = read_book_bytes(st.session_state.dev_file_bytes)
524
- sh_train = find_sheet(book, ["Train","Training","training2","train","training"])
525
- sh_test = find_sheet(book, ["Test","Testing","testing2","test","testing"])
 
 
 
 
526
  if sh_train is None or sh_test is None:
527
- st.error("Workbook must include Train/Training/training2 and Test/Testing/testing2 sheets."); st.stop()
528
- tr = book[sh_train].copy(); te = book[sh_test].copy()
529
- if not (ensure_cols(tr, FEATURES+[TARGET]) and ensure_cols(te, FEATURES+[TARGET])):
530
- st.error("Missing required columns."); st.stop()
 
 
 
 
 
531
  tr["UCS_Pred"] = model.predict(tr[FEATURES])
532
  te["UCS_Pred"] = model.predict(te[FEATURES])
533
 
534
- st.session_state.results["Train"]=tr; st.session_state.results["Test"]=te
535
- st.session_state.results["m_train"]={
 
536
  "R": pearson_r(tr[TARGET], tr["UCS_Pred"]),
537
  "RMSE": rmse(tr[TARGET], tr["UCS_Pred"]),
538
  "MAE": mean_absolute_error(tr[TARGET], tr["UCS_Pred"])
539
  }
540
- st.session_state.results["m_test"]={
541
  "R": pearson_r(te[TARGET], te["UCS_Pred"]),
542
  "RMSE": rmse(te[TARGET], te["UCS_Pred"]),
543
  "MAE": mean_absolute_error(te[TARGET], te["UCS_Pred"])
544
  }
545
 
546
- tr_min = tr[FEATURES].min().to_dict(); tr_max = 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
  st.success("Case has been built and results are displayed below.")
549
 
550
  def _dev_block(df, m):
551
- c1,c2,c3 = st.columns(3)
552
- c1.metric("R", f"{m['R']:.2f}"); c2.metric("RMSE", f"{m['RMSE']:.2f}"); c3.metric("MAE", f"{m['MAE']:.2f}")
 
 
553
 
554
  # 2-column layout, big gap (prevents overlap)
555
  col_cross, col_track = st.columns([3, 2], gap="large")
@@ -565,16 +583,18 @@ if st.session_state.app_step == "dev":
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:
568
- with tab1: _dev_block(st.session_state.results["Train"], st.session_state.results["m_train"])
 
569
  if "Test" in st.session_state.results:
570
- with tab2: _dev_block(st.session_state.results["Test"], st.session_state.results["m_test"])
 
571
 
572
  # =========================
573
  # VALIDATION (with actual UCS)
574
  # =========================
575
  if st.session_state.app_step == "validate":
576
  st.sidebar.header("Validate the Model")
577
- up = st.sidebar.file_uploader("Upload Validation Excel", type=["xlsx","xls"])
578
  if up is not None:
579
  book = read_book_bytes(up.getvalue())
580
  if book:
@@ -583,54 +603,78 @@ if st.session_state.app_step == "validate":
583
  if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
584
  preview_modal(read_book_bytes(up.getvalue()))
585
  go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
586
- if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
587
- if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
 
 
 
 
588
 
589
  if go_btn and up is not None:
590
  book = read_book_bytes(up.getvalue())
591
- name = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
592
  df = book[name].copy()
593
- if not ensure_cols(df, FEATURES+[TARGET]): st.error("Missing required columns."); st.stop()
 
 
594
  df["UCS_Pred"] = model.predict(df[FEATURES])
595
- st.session_state.results["Validate"]=df
596
 
597
- ranges = st.session_state.train_ranges; oor_pct = 0.0; tbl=None
 
 
598
  if ranges:
599
- any_viol = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).any(axis=1)
600
- oor_pct = float(any_viol.mean()*100.0)
 
 
601
  if any_viol.any():
602
  tbl = df.loc[any_viol, FEATURES].copy()
603
  for c in FEATURES:
604
- if pd.api.types.is_numeric_dtype(tbl[c]): tbl[c] = tbl[c].round(2)
605
- 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)
606
- st.session_state.results["m_val"]={
 
 
 
607
  "R": pearson_r(df[TARGET], df["UCS_Pred"]),
608
  "RMSE": rmse(df[TARGET], df["UCS_Pred"]),
609
  "MAE": mean_absolute_error(df[TARGET], df["UCS_Pred"])
610
  }
611
- 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}
612
- st.session_state.results["oor_tbl"]=tbl
 
 
 
 
 
613
 
614
  if "Validate" in st.session_state.results:
615
  m = st.session_state.results["m_val"]
616
- c1,c2,c3 = st.columns(3)
617
- c1.metric("R", f"{m['R']:.2f}"); c2.metric("RMSE", f"{m['RMSE']:.2f}"); c3.metric("MAE", f"{m['MAE']:.2f}")
 
 
618
 
619
  col_cross, col_track = st.columns([3, 2], gap="large")
620
  with col_cross:
621
  st.pyplot(
622
- cross_plot_static(st.session_state.results["Validate"][TARGET],
623
- st.session_state.results["Validate"]["UCS_Pred"]),
 
 
624
  use_container_width=True
625
  )
626
  with col_track:
627
  st.plotly_chart(
628
  track_plot(st.session_state.results["Validate"], include_actual=True),
629
- use_container_width=True, config={"displayModeBar": False, "scrollZoom": True}
 
630
  )
631
 
632
  sv = st.session_state.results["sv_val"]
633
- if sv["oor"] > 0: st.warning("Some inputs fall outside **training min–max** ranges.")
 
634
  if st.session_state.results["oor_tbl"] is not None:
635
  st.write("*Out-of-range rows (vs. Training min–max):*")
636
  df_centered_rounded(st.session_state.results["oor_tbl"])
@@ -640,7 +684,7 @@ if st.session_state.app_step == "validate":
640
  # =========================
641
  if st.session_state.app_step == "predict":
642
  st.sidebar.header("Prediction (No Actual UCS)")
643
- up = st.sidebar.file_uploader("Upload Prediction Excel", type=["xlsx","xls"])
644
  if up is not None:
645
  book = read_book_bytes(up.getvalue())
646
  if book:
@@ -649,41 +693,52 @@ if st.session_state.app_step == "predict":
649
  if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
650
  preview_modal(read_book_bytes(up.getvalue()))
651
  go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
652
- if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
 
 
653
 
654
  if go_btn and up is not None:
655
- book = read_book_bytes(up.getvalue()); name = list(book.keys())[0]
 
656
  df = book[name].copy()
657
- if not ensure_cols(df, FEATURES): st.error("Missing required columns."); st.stop()
 
 
658
  df["UCS_Pred"] = model.predict(df[FEATURES])
659
- st.session_state.results["PredictOnly"]=df
660
 
661
- ranges = st.session_state.train_ranges; oor_pct = 0.0
 
662
  if ranges:
663
- any_viol = pd.DataFrame({f:(df[f]<ranges[f][0])|(df[f]>ranges[f][1]) for f in FEATURES}).any(axis=1)
664
- oor_pct = float(any_viol.mean()*100.0)
665
- st.session_state.results["sv_pred"]={
666
- "n":len(df),
667
- "pred_min":float(df["UCS_Pred"].min()),
668
- "pred_max":float(df["UCS_Pred"].max()),
669
- "pred_mean":float(df["UCS_Pred"].mean()),
670
- "pred_std":float(df["UCS_Pred"].std(ddof=0)),
671
- "oor":oor_pct
 
 
672
  }
673
 
674
  if "PredictOnly" in st.session_state.results:
675
- df = st.session_state.results["PredictOnly"]; sv = st.session_state.results["sv_pred"]
 
676
 
677
- col_left, col_right = st.columns([2,3], gap="large")
678
  with col_left:
679
  table = pd.DataFrame({
680
- "Metric": ["# points","Pred min","Pred max","Pred mean","Pred std","OOR %"],
681
- "Value": [sv["n"],
682
- round(sv["pred_min"],2),
683
- round(sv["pred_max"],2),
684
- round(sv["pred_mean"],2),
685
- round(sv["pred_std"],2),
686
- f'{sv["oor"]:.1f}%']
 
 
687
  })
688
  st.success("Predictions ready ✓")
689
  df_centered_rounded(table, hide_index=True)
@@ -691,7 +746,8 @@ if st.session_state.app_step == "predict":
691
  with col_right:
692
  st.plotly_chart(
693
  track_plot(df, include_actual=False),
694
- use_container_width=True, config={"displayModeBar": False, "scrollZoom": True}
 
695
  )
696
 
697
  # =========================
@@ -700,10 +756,10 @@ if st.session_state.app_step == "predict":
700
  st.markdown("---")
701
  st.markdown(
702
  """
703
- <div style='text-align:center; color:#b1c7e0; line-height:1.6'>
704
  ST_GeoMech_UCS • © Smart Thinking<br/>
705
  <strong>Visit our website:</strong> <a href='https://www.smartthinking.com.sa' target='_blank'>smartthinking.com.sa</a>
706
  </div>
707
  """,
708
  unsafe_allow_html=True
709
- )
 
22
  MODELS_DIR = Path("models")
23
  DEFAULT_MODEL = MODELS_DIR / "ucs_rf.joblib"
24
  MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
25
+ COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
 
 
26
 
27
  # ---- Plot sizing controls ----
28
  CROSS_W = 450 # px (matplotlib figure size; Streamlit will still scale)
 
36
  # =========================
37
  st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
38
 
39
+ # This is the new CSS block with the added h1 color rule
40
  st.markdown("""
41
  <style>
42
+ /* Add a light gray background to the main app container */
43
+ .stApp {
44
+ background-color: #f0f2f6;
45
+ }
46
+
47
+ /* Change the color of the main page title (h1) to light blue */
48
+ h1 {
49
+ color: #add8e6;
50
  }
51
 
52
+ /* CSS to make the header sticky */
53
  .fixed-header-container {
54
  position: sticky;
55
  top: 0;
56
  z-index: 1000;
57
+ background-color: #f0f2f6; /* Ensure sticky header background matches app background */
58
  padding: 10px 0;
59
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
 
 
 
 
 
 
 
 
 
60
  }
61
 
62
+ /* CSS to ensure main content doesn't get hidden under the sticky header */
63
  .main .block-container {
64
+ padding-top: 250px; /* Adjust this value to match the height of your sticky header */
65
  }
66
 
67
  /* General CSS */
68
  .brand-logo { width: 16px; height: auto; object-fit: contain; }
69
  .sidebar-header { display:flex; align-items:center; gap:12px; }
70
  .sidebar-header .text h1 { font-size: 1.05rem; margin:0; line-height:1.1; }
71
+ .sidebar-header .text .tag { font-size: .85rem; color:#6b7280; margin:2px 0 0; }
72
  .centered-container {
73
  display: flex;
74
  flex-direction: column;
 
78
  </style>
79
  """, unsafe_allow_html=True)
80
 
81
+
82
+ # Hide uploader helper text ("Drag and drop file here", limits, etc.)
83
+ st.markdown("""
84
+ <style>
85
+ /* Older builds (helper wrapped in a Markdown container) */
86
+ section[data-testid="stFileUploader"] div[data-testid="stMarkdownContainer"]{display:none !important;}
87
+ /* 1.31–1.34: helper is the first child in the dropzone */
88
+ section[data-testid="stFileUploader"] [data-testid="stFileUploaderDropzone"] > div:first-child{display:none !important;}
89
+ /* 1.35+: explicit helper container */
90
+ section[data-testid="stFileUploader"] [data-testid="stFileUploaderInstructions"]{display:none !important;}
91
+ /* Fallback: any paragraph/small text inside the uploader */
92
+ section[data-testid="stFileUploader"] p, section[data-testid="stFileUploader"] small{display:none !important;}
93
+ </style>
94
+ """, unsafe_allow_html=True)
95
+
96
  # Make the Preview expander title & tabs sticky (pinned to the top)
97
  st.markdown("""
98
  <style>
 
99
  div[data-testid="stExpander"] > details > summary {
100
+ position: sticky;
101
+ top: 0;
102
+ z-index: 10;
103
+ background: #fff;
104
+ border-bottom: 1px solid #eee;
105
  }
 
106
  div[data-testid="stExpander"] div[data-baseweb="tab-list"] {
107
+ position: sticky;
108
+ top: 42px; /* adjust if your expander header height differs */
109
+ z-index: 9;
110
+ background: #fff;
111
+ padding-top: 6px;
112
  }
113
  </style>
114
  """, unsafe_allow_html=True)
 
146
  st.sidebar.markdown(f"""
147
  <div class="centered-container">
148
  <img src="{inline_logo('logo.png')}" style="width: 200px; height: auto; object-fit: contain;">
149
+ <div style='font-weight:800;font-size:1.2rem; margin-top: 10px;'>ST_GeoMech_UCS</div>
150
+ <div style='color:#667085;'>Smart Thinking • Secure Access</div>
151
  </div>
152
  """, unsafe_allow_html=True
153
  )
 
219
  # =========================
220
  def cross_plot_static(actual, pred):
221
  a = pd.Series(actual, dtype=float)
222
+ p = pd.Series(pred, dtype=float)
223
 
224
  fixed_min, fixed_max = 6000, 10000
225
  ticks = np.arange(fixed_min, fixed_max + 1, 1000)
 
230
  dpi=dpi,
231
  constrained_layout=False
232
  )
 
 
 
233
 
234
  ax.scatter(a, p, s=16, c=COLORS["pred"], alpha=0.9, linewidths=0)
235
  ax.plot([fixed_min, fixed_max], [fixed_min, fixed_max],
 
237
 
238
  ax.set_xlim(fixed_min, fixed_max)
239
  ax.set_ylim(fixed_min, fixed_max)
240
+ ax.set_xticks(ticks)
241
+ ax.set_yticks(ticks)
242
  ax.set_aspect("equal", adjustable="box") # true 45°
243
 
244
  fmt = FuncFormatter(lambda x, _: f"{int(x):,}")
245
  ax.xaxis.set_major_formatter(fmt)
246
  ax.yaxis.set_major_formatter(fmt)
247
 
248
+ ax.set_xlabel("Actual UCS (psi)", fontweight="bold", fontsize=12, color="black")
249
+ ax.set_ylabel("Predicted UCS (psi)", fontweight="bold", fontsize=12, color="black")
250
+ ax.tick_params(labelsize=10, colors="black")
 
251
 
252
  ax.grid(True, linestyle=":", alpha=0.3)
253
  for spine in ax.spines.values():
254
  spine.set_linewidth(1.1)
255
+ spine.set_color("#444")
256
 
257
  fig.subplots_adjust(left=0.16, bottom=0.16, right=0.98, top=0.98)
258
  return fig
 
297
 
298
  fig.update_layout(
299
  height=TRACK_H, width=None, # width auto-fits the column
300
+ paper_bgcolor="#fff", plot_bgcolor="#fff",
 
301
  margin=dict(l=64, r=16, t=36, b=48), hovermode="closest",
302
+ font=dict(size=FONT_SZ, color="#000"),
 
303
  legend=dict(
304
  x=0.98, y=0.05, xanchor="right", yanchor="bottom",
305
+ bgcolor="rgba(255,255,255,0.75)", bordercolor="#ccc", borderwidth=1
306
  ),
307
  legend_title_text=""
308
  )
309
 
310
+ # Bold, black axis titles & ticks
311
  fig.update_xaxes(
312
  title_text="UCS (psi)",
313
+ title_font=dict(size=16, family=BOLD_FONT, color="#000"),
314
+ tickfont=dict(size=11, family=BOLD_FONT, color="#000"),
315
  side="top",
316
  range=[xmin, xmax],
317
  ticks="outside",
318
  tickformat=",.0f",
319
  tickmode="auto",
320
  tick0=tick0,
321
+ showline=True, linewidth=1.2, linecolor="#444", mirror=True,
322
+ showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
323
  )
324
  fig.update_yaxes(
325
  title_text=ylab,
326
+ title_font=dict(size=16, family=BOLD_FONT, color="#000"),
327
+ tickfont=dict(size=11, family=BOLD_FONT, color="#000"),
328
  range=y_range,
329
  ticks="outside",
330
+ showline=True, linewidth=1.2, linecolor="#444", mirror=True,
331
+ showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
332
  )
333
 
334
  return fig
 
339
  n = len(cols)
340
  if n == 0:
341
  fig, ax = plt.subplots(figsize=(4, 2))
342
+ ax.text(0.5,0.5,"No selected columns",ha="center",va="center"); ax.axis("off")
343
  return fig
344
  fig, axes = plt.subplots(1, n, figsize=(2.2*n, 7.0), sharey=True, dpi=100)
 
345
  if n == 1: axes = [axes]
346
  idx = np.arange(1, len(df) + 1)
347
  for ax, col in zip(axes, cols):
348
+ ax.plot(df[col], idx, '-', lw=1.4, color="#333")
349
+ ax.set_xlabel(col); ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
 
 
350
  ax.grid(True, linestyle=":", alpha=0.3)
351
+ for s in ax.spines.values(): s.set_visible(True)
352
+ axes[0].set_ylabel("Point Index")
 
 
353
  return fig
354
 
355
  # Modal wrapper (Streamlit compatibility)
 
437
  st.sidebar.markdown(f"""
438
  <div class="centered-container">
439
  <img src="{inline_logo('logo.png')}" style="width: 200px; height: auto; object-fit: contain;">
440
+ <div style='font-weight:800;font-size:1.2rem;'>ST_GeoMech_UCS</div>
441
+ <div style='color:#667085;'>Real-Time UCS Tracking While Drilling</div>
442
  </div>
443
  """, unsafe_allow_html=True
444
  )
445
 
 
 
 
 
 
446
  # =========================
447
  # The main sticky container is defined here, before the app logic
448
  # This ensures it's rendered at the top of the page regardless of state.
 
476
  # =========================
477
  if st.session_state.app_step == "intro":
478
  st.header("Welcome!")
479
+ st.markdown(
480
+ "This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data."
481
+ )
482
  st.subheader("How It Works")
483
  st.markdown(
484
  "1) **Upload your data to build the case and preview the performance of our model.** \n"
 
486
  "3) **Proceed to Validation** (with actual UCS) or **Proceed to Prediction** (no UCS)."
487
  )
488
  if st.button("Start Showcase", type="primary"):
489
+ st.session_state.app_step = "dev"
490
+ st.rerun()
491
 
492
  # =========================
493
  # CASE BUILDING
 
504
  tmp = read_book_bytes(st.session_state.dev_file_bytes)
505
  if tmp:
506
  df0 = next(iter(tmp.values()))
507
+ st.sidebar.caption(
508
+ f"**Data loaded:** {st.session_state.dev_file_name} • {df0.shape[0]} rows × {df0.shape[1]} cols"
509
+ )
510
 
511
+ if st.sidebar.button(
512
+ "Preview data",
513
+ use_container_width=True,
514
+ disabled=not st.session_state.dev_file_loaded
515
+ ):
516
  preview_modal(read_book_bytes(st.session_state.dev_file_bytes))
517
  st.session_state.dev_preview = True
518
 
519
  run = st.sidebar.button("Run Model", type="primary", use_container_width=True)
520
+ if st.sidebar.button("Proceed to Validation ▶", use_container_width=True):
521
+ st.session_state.app_step = "validate"
522
+ st.rerun()
523
+ if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True):
524
+ st.session_state.app_step = "predict"
525
+ st.rerun()
526
 
527
  if run and st.session_state.dev_file_bytes:
528
  book = read_book_bytes(st.session_state.dev_file_bytes)
529
+ sh_train = find_sheet(
530
+ book, ["Train", "Training", "training2", "train", "training"]
531
+ )
532
+ sh_test = find_sheet(
533
+ book, ["Test", "Testing", "testing2", "test", "testing"]
534
+ )
535
  if sh_train is None or sh_test is None:
536
+ st.error(
537
+ "Workbook must include Train/Training/training2 and Test/Testing/testing2 sheets."
538
+ )
539
+ st.stop()
540
+ tr = book[sh_train].copy()
541
+ te = book[sh_test].copy()
542
+ if not (ensure_cols(tr, FEATURES + [TARGET]) and ensure_cols(te, FEATURES + [TARGET])):
543
+ st.error("Missing required columns.")
544
+ st.stop()
545
  tr["UCS_Pred"] = model.predict(tr[FEATURES])
546
  te["UCS_Pred"] = model.predict(te[FEATURES])
547
 
548
+ st.session_state.results["Train"] = tr
549
+ st.session_state.results["Test"] = te
550
+ st.session_state.results["m_train"] = {
551
  "R": pearson_r(tr[TARGET], tr["UCS_Pred"]),
552
  "RMSE": rmse(tr[TARGET], tr["UCS_Pred"]),
553
  "MAE": mean_absolute_error(tr[TARGET], tr["UCS_Pred"])
554
  }
555
+ st.session_state.results["m_test"] = {
556
  "R": pearson_r(te[TARGET], te["UCS_Pred"]),
557
  "RMSE": rmse(te[TARGET], te["UCS_Pred"]),
558
  "MAE": mean_absolute_error(te[TARGET], te["UCS_Pred"])
559
  }
560
 
561
+ tr_min = tr[FEATURES].min().to_dict()
562
+ tr_max = tr[FEATURES].max().to_dict()
563
+ st.session_state.train_ranges = {f: (float(tr_min[f]), float(tr_max[f])) for f in FEATURES}
564
  st.success("Case has been built and results are displayed below.")
565
 
566
  def _dev_block(df, m):
567
+ c1, c2, c3 = st.columns(3)
568
+ c1.metric("R", f"{m['R']:.2f}")
569
+ c2.metric("RMSE", f"{m['RMSE']:.2f}")
570
+ c3.metric("MAE", f"{m['MAE']:.2f}")
571
 
572
  # 2-column layout, big gap (prevents overlap)
573
  col_cross, col_track = st.columns([3, 2], gap="large")
 
583
  if "Train" in st.session_state.results or "Test" in st.session_state.results:
584
  tab1, tab2 = st.tabs(["Training", "Testing"])
585
  if "Train" in st.session_state.results:
586
+ with tab1:
587
+ _dev_block(st.session_state.results["Train"], st.session_state.results["m_train"])
588
  if "Test" in st.session_state.results:
589
+ with tab2:
590
+ _dev_block(st.session_state.results["Test"], st.session_state.results["m_test"])
591
 
592
  # =========================
593
  # VALIDATION (with actual UCS)
594
  # =========================
595
  if st.session_state.app_step == "validate":
596
  st.sidebar.header("Validate the Model")
597
+ up = st.sidebar.file_uploader("Upload Validation Excel", type=["xlsx", "xls"])
598
  if up is not None:
599
  book = read_book_bytes(up.getvalue())
600
  if book:
 
603
  if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
604
  preview_modal(read_book_bytes(up.getvalue()))
605
  go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
606
+ if st.sidebar.button("⬅ Back to Case Building", use_container_width=True):
607
+ st.session_state.app_step = "dev"
608
+ st.rerun()
609
+ if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True):
610
+ st.session_state.app_step = "predict"
611
+ st.rerun()
612
 
613
  if go_btn and up is not None:
614
  book = read_book_bytes(up.getvalue())
615
+ name = find_sheet(book, ["Validation", "Validate", "validation2", "Val", "val"]) or list(book.keys())[0]
616
  df = book[name].copy()
617
+ if not ensure_cols(df, FEATURES + [TARGET]):
618
+ st.error("Missing required columns.")
619
+ st.stop()
620
  df["UCS_Pred"] = model.predict(df[FEATURES])
621
+ st.session_state.results["Validate"] = df
622
 
623
+ ranges = st.session_state.train_ranges
624
+ oor_pct = 0.0
625
+ tbl = None
626
  if ranges:
627
+ any_viol = pd.DataFrame(
628
+ {f: (df[f] < ranges[f][0]) | (df[f] > ranges[f][1]) for f in FEATURES}
629
+ ).any(axis=1)
630
+ oor_pct = float(any_viol.mean() * 100.0)
631
  if any_viol.any():
632
  tbl = df.loc[any_viol, FEATURES].copy()
633
  for c in FEATURES:
634
+ if pd.api.types.is_numeric_dtype(tbl[c]):
635
+ tbl[c] = tbl[c].round(2)
636
+ tbl["Violations"] = pd.DataFrame(
637
+ {f: (df[f] < ranges[f][0]) | (df[f] > ranges[f][1]) for f in FEATURES}
638
+ ).loc[any_viol].apply(lambda r: ", ".join([c for c, v in r.items() if v]), axis=1)
639
+ st.session_state.results["m_val"] = {
640
  "R": pearson_r(df[TARGET], df["UCS_Pred"]),
641
  "RMSE": rmse(df[TARGET], df["UCS_Pred"]),
642
  "MAE": mean_absolute_error(df[TARGET], df["UCS_Pred"])
643
  }
644
+ st.session_state.results["sv_val"] = {
645
+ "n": len(df),
646
+ "pred_min": float(df["UCS_Pred"].min()),
647
+ "pred_max": float(df["UCS_Pred"].max()),
648
+ "oor": oor_pct
649
+ }
650
+ st.session_state.results["oor_tbl"] = tbl
651
 
652
  if "Validate" in st.session_state.results:
653
  m = st.session_state.results["m_val"]
654
+ c1, c2, c3 = st.columns(3)
655
+ c1.metric("R", f"{m['R']:.2f}")
656
+ c2.metric("RMSE", f"{m['RMSE']:.2f}")
657
+ c3.metric("MAE", f"{m['MAE']:.2f}")
658
 
659
  col_cross, col_track = st.columns([3, 2], gap="large")
660
  with col_cross:
661
  st.pyplot(
662
+ cross_plot_static(
663
+ st.session_state.results["Validate"][TARGET],
664
+ st.session_state.results["Validate"]["UCS_Pred"]
665
+ ),
666
  use_container_width=True
667
  )
668
  with col_track:
669
  st.plotly_chart(
670
  track_plot(st.session_state.results["Validate"], include_actual=True),
671
+ use_container_width=True,
672
+ config={"displayModeBar": False, "scrollZoom": True}
673
  )
674
 
675
  sv = st.session_state.results["sv_val"]
676
+ if sv["oor"] > 0:
677
+ st.warning("Some inputs fall outside **training min–max** ranges.")
678
  if st.session_state.results["oor_tbl"] is not None:
679
  st.write("*Out-of-range rows (vs. Training min–max):*")
680
  df_centered_rounded(st.session_state.results["oor_tbl"])
 
684
  # =========================
685
  if st.session_state.app_step == "predict":
686
  st.sidebar.header("Prediction (No Actual UCS)")
687
+ up = st.sidebar.file_uploader("Upload Prediction Excel", type=["xlsx", "xls"])
688
  if up is not None:
689
  book = read_book_bytes(up.getvalue())
690
  if book:
 
693
  if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
694
  preview_modal(read_book_bytes(up.getvalue()))
695
  go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
696
+ if st.sidebar.button("⬅ Back to Case Building", use_container_width=True):
697
+ st.session_state.app_step = "dev"
698
+ st.rerun()
699
 
700
  if go_btn and up is not None:
701
+ book = read_book_bytes(up.getvalue())
702
+ name = list(book.keys())[0]
703
  df = book[name].copy()
704
+ if not ensure_cols(df, FEATURES):
705
+ st.error("Missing required columns.")
706
+ st.stop()
707
  df["UCS_Pred"] = model.predict(df[FEATURES])
708
+ st.session_state.results["PredictOnly"] = df
709
 
710
+ ranges = st.session_state.train_ranges
711
+ oor_pct = 0.0
712
  if ranges:
713
+ any_viol = pd.DataFrame(
714
+ {f: (df[f] < ranges[f][0]) | (df[f] > ranges[f][1]) for f in FEATURES}
715
+ ).any(axis=1)
716
+ oor_pct = float(any_viol.mean() * 100.0)
717
+ st.session_state.results["sv_pred"] = {
718
+ "n": len(df),
719
+ "pred_min": float(df["UCS_Pred"].min()),
720
+ "pred_max": float(df["UCS_Pred"].max()),
721
+ "pred_mean": float(df["UCS_Pred"].mean()),
722
+ "pred_std": float(df["UCS_Pred"].std(ddof=0)),
723
+ "oor": oor_pct
724
  }
725
 
726
  if "PredictOnly" in st.session_state.results:
727
+ df = st.session_state.results["PredictOnly"]
728
+ sv = st.session_state.results["sv_pred"]
729
 
730
+ col_left, col_right = st.columns([2, 3], gap="large")
731
  with col_left:
732
  table = pd.DataFrame({
733
+ "Metric": ["# points", "Pred min", "Pred max", "Pred mean", "Pred std", "OOR %"],
734
+ "Value": [
735
+ sv["n"],
736
+ round(sv["pred_min"], 2),
737
+ round(sv["pred_max"], 2),
738
+ round(sv["pred_mean"], 2),
739
+ round(sv["pred_std"], 2),
740
+ f'{sv["oor"]:.1f}%'
741
+ ]
742
  })
743
  st.success("Predictions ready ✓")
744
  df_centered_rounded(table, hide_index=True)
 
746
  with col_right:
747
  st.plotly_chart(
748
  track_plot(df, include_actual=False),
749
+ use_container_width=True,
750
+ config={"displayModeBar": False, "scrollZoom": True}
751
  )
752
 
753
  # =========================
 
756
  st.markdown("---")
757
  st.markdown(
758
  """
759
+ <div style='text-align:center; color:#6b7280; line-height:1.6'>
760
  ST_GeoMech_UCS • © Smart Thinking<br/>
761
  <strong>Visit our website:</strong> <a href='https://www.smartthinking.com.sa' target='_blank'>smartthinking.com.sa</a>
762
  </div>
763
  """,
764
  unsafe_allow_html=True
765
+ )