UCS2014 commited on
Commit
42fab84
·
verified ·
1 Parent(s): 44c73c9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +168 -175
app.py CHANGED
@@ -1,4 +1,5 @@
1
- # app.py — ST_GeoMech_YM (mirrors your UCS GUI, adapted for Young's Modulus)
 
2
  import io, json, os, base64, math
3
  from pathlib import Path
4
  import streamlit as st
@@ -17,41 +18,52 @@ import plotly.graph_objects as go
17
  from sklearn.metrics import mean_squared_error, mean_absolute_error
18
 
19
  # =========================
20
- # Constants (Ym variant)
 
 
 
 
21
  # =========================
22
- FEATURES = ["WOB(klbf)", "TORQUE(kft.lbf)", "SPP(psi)", "RPM(1/min)", "ROP(ft/h)", "Flow Rate, gpm"]
23
- TARGET = "Actual Ym"
24
- PRED_COL = "Ym_Pred"
 
 
 
 
 
25
 
26
  MODELS_DIR = Path("models")
27
  DEFAULT_MODEL = MODELS_DIR / "ym_rf.joblib"
28
  MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
 
29
  COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
30
 
31
  # ---- Plot sizing controls ----
32
  CROSS_W = 350
33
  CROSS_H = 350
34
  TRACK_H = 1000
35
- TRACK_W = 500
36
- FONT_SZ = 13
37
  BOLD_FONT = "Arial Black, Arial, sans-serif"
38
 
39
  # =========================
40
  # Page / CSS
41
  # =========================
42
- st.set_page_config(page_title="ST_GeoMech_YM", page_icon="logo.png", layout="wide")
43
 
 
44
  st.markdown("""
45
  <style>
46
  .brand-logo { width: 200px; height: auto; object-fit: contain; }
47
  .sidebar-header { display:flex; align-items:center; gap:12px; }
48
  .sidebar-header .text h1 { font-size: 1.05rem; margin:0; line-height:1.1; }
49
  .sidebar-header .text .tag { font-size: .85rem; color:#6b7280; margin:2px 0 0; }
50
- .centered-container { display: flex; flex-direction: column; align-items: center; text-align: center; }
51
  </style>
52
  """, unsafe_allow_html=True)
53
 
54
- # Sticky helpers
55
  st.markdown("""
56
  <style>
57
  .main .block-container { overflow: unset !important; }
@@ -69,7 +81,7 @@ section[data-testid="stFileUploader"] p, section[data-testid="stFileUploader"] s
69
  </style>
70
  """, unsafe_allow_html=True)
71
 
72
- # Make the Preview expander title & tabs sticky (pinned to the top)
73
  st.markdown("""
74
  <style>
75
  div[data-testid="stExpander"] > details > summary {
@@ -81,19 +93,19 @@ div[data-testid="stExpander"] div[data-baseweb="tab-list"] {
81
  </style>
82
  """, unsafe_allow_html=True)
83
 
84
- # Center text in all pandas Styler tables
85
  TABLE_CENTER_CSS = [
86
  dict(selector="th", props=[("text-align", "center")]),
87
  dict(selector="td", props=[("text-align", "center")]),
88
  ]
89
 
90
- # Message box CSS
91
  st.markdown("""
92
  <style>
93
- .st-message-box { background-color: #f0f2f6; color: #333; padding: 10px; border-radius: 10px; border: 1px solid #e6e9ef; }
94
- .st-message-box.st-success { background-color: #d4edda; color: #155724; border-color: #c3e6cb; }
95
- .st-message-box.st-warning { background-color: #fff3cd; color: #856404; border-color: #ffeeba; }
96
- .st-message-box.st-error { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
97
  </style>
98
  """, unsafe_allow_html=True)
99
 
@@ -103,7 +115,8 @@ st.markdown("""
103
  def inline_logo(path="logo.png") -> str:
104
  try:
105
  p = Path(path)
106
- if not p.exists(): return ""
 
107
  return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
108
  except Exception:
109
  return ""
@@ -124,7 +137,7 @@ def add_password_gate() -> None:
124
  st.sidebar.markdown(f"""
125
  <div class="centered-container">
126
  <img src="{inline_logo('logo.png')}" style="width: 200px; height: auto; object-fit: contain;">
127
- <div style='font-weight:800;font-size:1.2rem; margin-top: 10px;'>ST_GeoMech_YM</div>
128
  <div style='color:#667085;'>Smart Thinking • Secure Access</div>
129
  </div>
130
  """, unsafe_allow_html=True
@@ -149,7 +162,8 @@ def rmse(y_true, y_pred) -> float:
149
  def pearson_r(y_true, y_pred) -> float:
150
  a = np.asarray(y_true, dtype=float)
151
  p = np.asarray(y_pred, dtype=float)
152
- if a.size < 2: return float("nan")
 
153
  return float(np.corrcoef(a, p)[0, 1])
154
 
155
  @st.cache_resource(show_spinner=False)
@@ -162,31 +176,34 @@ def parse_excel(data_bytes: bytes):
162
  xl = pd.ExcelFile(bio)
163
  return {sh: xl.parse(sh) for sh in xl.sheet_names}
164
 
165
- def read_book_bytes(b: bytes): return parse_excel(b) if b else {}
 
 
 
 
 
 
 
 
 
 
 
166
 
167
  def ensure_cols(df, cols):
168
- """
169
- Check required columns exist; auto-fix common typos first.
170
- """
171
- # Auto-fix known variants before checking
172
- fixed = _normalize_columns(df)
173
- miss = [c for c in cols if c not in fixed.columns]
174
  if miss:
175
- st.error(f"Missing columns: {miss}\nFound: {list(fixed.columns)}")
176
  return False
177
- # If everything exists in the fixed version, reflect back to caller
178
- # (callers typically use the same df instance; we return True only)
179
  return True
180
 
181
-
182
  def find_sheet(book, names):
183
  low2orig = {k.lower(): k for k in book.keys()}
184
  for nm in names:
185
- if nm.lower() in low2orig: return low2orig[nm.lower()]
 
186
  return None
187
 
188
  def _nice_tick0(xmin: float, step: float = 0.1) -> float:
189
- # Rounded start tick for continuous Ym scales (unit-agnostic)
190
  return step * math.floor(xmin / step) if np.isfinite(xmin) else xmin
191
 
192
  def df_centered_rounded(df: pd.DataFrame, hide_index=True):
@@ -194,13 +211,13 @@ def df_centered_rounded(df: pd.DataFrame, hide_index=True):
194
  numcols = out.select_dtypes(include=[np.number]).columns
195
  styler = (
196
  out.style
197
- .format({c: "{:.2f}" for c in numcols})
198
- .set_properties(**{"text-align": "center"})
199
- .set_table_styles(TABLE_CENTER_CSS)
200
  )
201
  st.dataframe(styler, use_container_width=True, hide_index=hide_index)
202
 
203
- # === Excel export helpers =================================================
204
  def _excel_engine() -> str:
205
  try:
206
  import xlsxwriter # noqa: F401
@@ -224,11 +241,12 @@ def _summary_table(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
224
  cols = [c for c in cols if c in df.columns]
225
  if not cols:
226
  return pd.DataFrame()
227
- tbl = (df[cols]
228
- .agg(['min','max','mean','std'])
229
- .T.rename(columns={"min":"Min","max":"Max","mean":"Mean","std":"Std"})
230
- .reset_index(names="Field"))
231
- return tbl
 
232
 
233
  def _train_ranges_df(ranges: dict[str, tuple[float, float]]) -> pd.DataFrame:
234
  if not ranges:
@@ -238,40 +256,34 @@ def _train_ranges_df(ranges: dict[str, tuple[float, float]]) -> pd.DataFrame:
238
  return df
239
 
240
  def _excel_autofit(writer, sheet_name: str, df: pd.DataFrame, min_w: int = 8, max_w: int = 40):
241
- """Auto-fit columns when using xlsxwriter."""
242
  try:
243
  import xlsxwriter # noqa: F401
244
  except Exception:
245
  return
246
  ws = writer.sheets[sheet_name]
247
- # header
248
  for i, col in enumerate(df.columns):
249
  series = df[col].astype(str)
250
  max_len = max([len(str(col))] + series.map(len).tolist())
251
  ws.set_column(i, i, max(min_w, min(max_len + 2, max_w)))
252
- # freeze header row
253
  ws.freeze_panes(1, 0)
254
 
255
  def _add_sheet(sheets: dict, order: list, name: str, df: pd.DataFrame, ndigits: int):
256
- if df is None or df.empty:
257
- return
258
- sheets[name] = _round_numeric(df, ndigits)
259
- order.append(name)
260
 
261
  def _available_sections():
262
- """Compute which sections exist, to build a sensible default list."""
263
  res = st.session_state.get("results", {})
264
  sections = []
265
  if "Train" in res: sections += ["Training","Training_Metrics","Training_Summary"]
266
  if "Test" in res: sections += ["Testing","Testing_Metrics","Testing_Summary"]
267
  if "Validate" in res: sections += ["Validation","Validation_Metrics","Validation_Summary","Validation_OOR"]
268
- if "PredictOnly" in res: sections += ["Prediction","Prediction_Summary","Prediction_OOR"]
269
  if st.session_state.get("train_ranges"): sections += ["Training_Ranges"]
270
  sections += ["Info"]
271
  return sections
272
 
273
  def build_export_workbook(selected: list[str], ndigits: int = 2) -> tuple[bytes|None, str|None, list[str]]:
274
- """Builds an in-memory Excel workbook based on selected sheet names."""
275
  res = st.session_state.get("results", {})
276
  if not res:
277
  return None, None, []
@@ -285,7 +297,7 @@ def build_export_workbook(selected: list[str], ndigits: int = 2) -> tuple[bytes|
285
  if "Training_Metrics" in selected and res.get("m_train"):
286
  _add_sheet(sheets, order, "Training_Metrics", pd.DataFrame([res["m_train"]]), ndigits)
287
  if "Training_Summary" in selected and "Train" in res:
288
- tr_cols = FEATURES + [c for c in ["GR_Actual","GR_Pred"] if c in res["Train"].columns]
289
  _add_sheet(sheets, order, "Training_Summary", _summary_table(res["Train"], tr_cols), ndigits)
290
 
291
  # Testing
@@ -294,7 +306,7 @@ def build_export_workbook(selected: list[str], ndigits: int = 2) -> tuple[bytes|
294
  if "Testing_Metrics" in selected and res.get("m_test"):
295
  _add_sheet(sheets, order, "Testing_Metrics", pd.DataFrame([res["m_test"]]), ndigits)
296
  if "Testing_Summary" in selected and "Test" in res:
297
- te_cols = FEATURES + [c for c in ["GR_Actual","GR_Pred"] if c in res["Test"].columns]
298
  _add_sheet(sheets, order, "Testing_Summary", _summary_table(res["Test"], te_cols), ndigits)
299
 
300
  # Validation
@@ -312,8 +324,6 @@ def build_export_workbook(selected: list[str], ndigits: int = 2) -> tuple[bytes|
312
  _add_sheet(sheets, order, "Prediction", res["PredictOnly"], ndigits)
313
  if "Prediction_Summary" in selected and res.get("sv_pred"):
314
  _add_sheet(sheets, order, "Prediction_Summary", pd.DataFrame([res["sv_pred"]]), ndigits)
315
- if "Prediction_OOR" in selected and isinstance(res.get("oor_tbl_pred"), pd.DataFrame) and not res["oor_tbl_pred"].empty:
316
- _add_sheet(sheets, order, "Prediction_OOR", res["oor_tbl_pred"].reset_index(drop=True), ndigits)
317
 
318
  # Training ranges
319
  if "Training_Ranges" in selected and st.session_state.get("train_ranges"):
@@ -323,79 +333,69 @@ def build_export_workbook(selected: list[str], ndigits: int = 2) -> tuple[bytes|
323
  # Info
324
  if "Info" in selected:
325
  info = pd.DataFrame([
326
- {"Key": "AppName", "Value": APP_NAME},
327
- {"Key": "Tagline", "Value": TAGLINE},
328
- {"Key": "Target", "Value": TARGET},
329
- {"Key": "TargetTransform", "Value": TARGET_TRANSFORM},
330
- {"Key": "ActualColumn", "Value": ACTUAL_COL},
331
- {"Key": "Features", "Value": ", ".join(FEATURES)},
332
- {"Key": "ExportedAt", "Value": datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
333
  ])
334
  _add_sheet(sheets, order, "Info", info, ndigits)
335
 
336
  if not order:
337
  return None, None, []
338
 
339
- # Write workbook to memory
340
  bio = io.BytesIO()
341
  engine = _excel_engine()
342
  with pd.ExcelWriter(bio, engine=engine) as writer:
343
  for name in order:
344
  df = sheets[name]
345
- df.to_excel(writer, sheet_name=_excel_safe_name(name), index=False)
346
- _excel_autofit(writer, _excel_safe_name(name), df)
 
347
  bio.seek(0)
348
 
349
- fname = f"GR_Export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
350
  return bio.getvalue(), fname, order
351
 
352
  def render_export_button(key: str = "export_main") -> None:
353
  st.divider()
354
  st.markdown("### Export to Excel")
355
 
356
- # Defaults: include everything that currently exists
357
  default_sections = _available_sections()
358
  all_sections = [
359
  "Training","Training_Metrics","Training_Summary",
360
  "Testing","Testing_Metrics","Testing_Summary",
361
  "Validation","Validation_Metrics","Validation_Summary","Validation_OOR",
362
- "Prediction","Prediction_Summary","Prediction_OOR",
363
  "Training_Ranges","Info"
364
  ]
365
- selected = st.multiselect(
366
- "Sheets to include",
367
- options=all_sections,
368
- default=default_sections,
369
- help="Choose which sheets to include in the Excel export."
370
- )
371
-
372
- c1, c2, c3 = st.columns([1,1,2])
373
  with c1:
374
- ndigits = st.number_input("Rounding (decimals)", min_value=0, max_value=6, value=2, step=1)
375
  with c2:
376
- base_name = st.text_input("Base filename", value="GR_Export")
377
- with c3:
378
- st.caption("• Columns auto-fit & header row frozen (if xlsxwriter is available).")
379
-
380
- data, default_fname, names = build_export_workbook(selected=selected, ndigits=int(ndigits))
381
 
 
382
  if names:
383
  st.caption("Will include: " + ", ".join(names))
 
384
  st.download_button(
385
- label="⬇️ Export Excel",
386
  data=(data or b""),
387
- file_name=((base_name or "GR_Export") + "_" + datetime.now().strftime("%Y%m%d_%H%M%S") + ".xlsx") if data else "GR_Export.xlsx",
388
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
389
  disabled=(data is None),
390
- help="Exports selected sheets with optional rounding, auto-fit columns, and frozen headers.",
391
  key=key,
392
  )
 
393
  # =========================
394
  # Cross plot (Matplotlib) — auto-scaled for Ym
395
  # =========================
396
  def cross_plot_static(actual, pred, xlabel="Actual Ym", ylabel="Predicted Ym"):
397
  a = pd.Series(actual, dtype=float)
398
- p = pd.Series(pred, dtype=float)
399
 
400
  lo = float(min(a.min(), p.min()))
401
  hi = float(max(a.max(), p.max()))
@@ -416,7 +416,6 @@ def cross_plot_static(actual, pred, xlabel="Actual Ym", ylabel="Predicted Ym"):
416
  ax.set_yticks(ticks)
417
  ax.set_aspect("equal", adjustable="box")
418
 
419
- # Generic numeric formatting (2 decimals)
420
  fmt = FuncFormatter(lambda x, _: f"{x:.2f}")
421
  ax.xaxis.set_major_formatter(fmt)
422
  ax.yaxis.set_major_formatter(fmt)
@@ -434,7 +433,7 @@ def cross_plot_static(actual, pred, xlabel="Actual Ym", ylabel="Predicted Ym"):
434
  return fig
435
 
436
  # =========================
437
- # Track plot (Plotly)
438
  # =========================
439
  def track_plot(df, include_actual=True):
440
  # Depth (or index) on Y
@@ -463,20 +462,18 @@ def track_plot(df, include_actual=True):
463
  x=df[PRED_COL], y=y, mode="lines",
464
  line=dict(color=COLORS["pred"], width=1.8),
465
  name=PRED_COL,
466
- hovertemplate=f"{PRED_COL}: "+"%{x:.2f}<br>"+ylab+": %{y}<extra></extra>"
467
  ))
468
  if include_actual and TARGET in df.columns:
469
  fig.add_trace(go.Scatter(
470
  x=df[TARGET], y=y, mode="lines",
471
  line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
472
  name=f"{TARGET} (actual)",
473
- hovertemplate=f"{TARGET}: "+"%{x:.2f}<br>"+ylab+": %{y}<extra></extra>"
474
  ))
475
 
476
  fig.update_layout(
477
- height=TRACK_H,
478
- width=TRACK_W,
479
- autosize=False,
480
  paper_bgcolor="#fff", plot_bgcolor="#fff",
481
  margin=dict(l=64, r=16, t=36, b=48), hovermode="closest",
482
  font=dict(size=FONT_SZ, color="#000"),
@@ -484,34 +481,24 @@ def track_plot(df, include_actual=True):
484
  bgcolor="rgba(255,255,255,0.75)", bordercolor="#ccc", borderwidth=1),
485
  legend_title_text=""
486
  )
487
-
488
  fig.update_xaxes(
489
  title_text="Ym",
490
  title_font=dict(size=20, family=BOLD_FONT, color="#000"),
491
  tickfont=dict(size=15, family=BOLD_FONT, color="#000"),
492
- side="top",
493
- range=[xmin, xmax],
494
- ticks="outside",
495
- tickformat=",.2f",
496
- tickmode="auto",
497
- tick0=tick0,
498
  showline=True, linewidth=1.2, linecolor="#444", mirror=True,
499
  showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
500
  )
501
-
502
- fig.update_xaxes(
503
- title_text="Ym",
504
  title_font=dict(size=20, family=BOLD_FONT, color="#000"),
505
  tickfont=dict(size=15, family=BOLD_FONT, color="#000"),
506
- side="top",
507
- range=[xmin, xmax],
508
- ticks="outside",
509
- tickformat=",.0f", # ← integer, thousands separated, no decimals
510
- tickmode="auto",
511
- tick0=tick0,
512
  showline=True, linewidth=1.2, linecolor="#444", mirror=True,
513
  showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
514
  )
 
515
  return fig
516
 
517
  # ---------- Preview modal (matplotlib) ----------
@@ -523,17 +510,18 @@ def preview_tracks(df: pd.DataFrame, cols: list[str]):
523
  ax.text(0.5,0.5,"No selected columns",ha="center",va="center"); ax.axis("off")
524
  return fig
525
  fig, axes = plt.subplots(1, n, figsize=(2.2*n, 7.0), sharey=True, dpi=100)
526
- if n == 1: axes = [axes]
 
527
  idx = np.arange(1, len(df) + 1)
528
  for ax, col in zip(axes, cols):
529
- ax.plot(df[col], idx, '-', lw=1.4, color="#333")
530
  ax.set_xlabel(col); ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
531
  ax.grid(True, linestyle=":", alpha=0.3)
532
  for s in ax.spines.values(): s.set_visible(True)
533
  axes[0].set_ylabel("Point Index")
534
  return fig
535
 
536
- # Modal wrapper
537
  try:
538
  dialog = st.dialog
539
  except AttributeError:
@@ -557,19 +545,28 @@ def preview_modal(book: dict[str, pd.DataFrame]):
557
  with t1:
558
  st.pyplot(preview_tracks(df, FEATURES), use_container_width=True)
559
  with t2:
560
- tbl = (df[FEATURES]
561
- .agg(['min','max','mean','std'])
562
- .T.rename(columns={"min":"Min","max":"Max","mean":"Mean","std":"Std"}))
563
- df_centered_rounded(tbl.reset_index(names="Feature"))
 
 
 
 
 
 
 
564
 
565
  # =========================
566
- # Load model
567
  # =========================
568
  def ensure_model() -> Path|None:
569
  for p in [DEFAULT_MODEL, *MODEL_FALLBACKS]:
570
- if p.exists() and p.stat().st_size > 0: return p
 
571
  url = os.environ.get("MODEL_URL", "")
572
- if not url: return None
 
573
  try:
574
  import requests
575
  DEFAULT_MODEL.parent.mkdir(parents=True, exist_ok=True)
@@ -577,7 +574,8 @@ def ensure_model() -> Path|None:
577
  r.raise_for_status()
578
  with open(DEFAULT_MODEL, "wb") as f:
579
  for chunk in r.iter_content(1<<20):
580
- if chunk: f.write(chunk)
 
581
  return DEFAULT_MODEL
582
  except Exception:
583
  return None
@@ -592,25 +590,24 @@ except Exception as e:
592
  st.error(f"Failed to load model: {e}")
593
  st.stop()
594
 
595
- # ---------- Load meta (optional) ----------
596
- meta = {} # define first, so it's always safe to reference later
597
-
598
- # Support either models/meta.json or models/ym_meta.json
599
- meta_candidates = [MODELS_DIR / "meta.json", MODELS_DIR / "ym_meta.json"]
600
- meta_path = next((p for p in meta_candidates if p.exists()), None)
601
-
602
- if meta_path:
603
- try:
604
- meta = json.loads(meta_path.read_text(encoding="utf-8"))
605
- FEATURES = meta.get("features", FEATURES)
606
- TARGET = meta.get("target", TARGET)
607
- except Exception as e:
608
- st.warning(f"Could not parse meta file ({meta_path.name}): {e}")
609
-
610
- # ---------- Optional: warn if runtime != training versions ----------
611
- import numpy as _np, sklearn as _skl
612
- mv = meta.get("versions", {})
613
- if mv:
614
  msg = []
615
  if mv.get("numpy") and mv["numpy"] != _np.__version__:
616
  msg.append(f"NumPy {mv['numpy']} expected, running {_np.__version__}")
@@ -618,7 +615,8 @@ if mv:
618
  msg.append(f"scikit-learn {mv['scikit_learn']} expected, running {_skl.__version__}")
619
  if msg:
620
  st.warning("Environment mismatch: " + " | ".join(msg))
621
-
 
622
 
623
  # =========================
624
  # Session state
@@ -638,14 +636,14 @@ st.session_state.setdefault("show_preview_modal", False)
638
  st.sidebar.markdown(f"""
639
  <div class="centered-container">
640
  <img src="{inline_logo('logo.png')}" style="width: 200px; height: auto; object-fit: contain;">
641
- <div style='font-weight:800;font-size:1.2rem;'>ST_GeoMech_YM</div>
642
- <div style='color:#667085;'>Real-Time Young's Modulus Tracking</div>
643
  </div>
644
  """, unsafe_allow_html=True
645
  )
646
 
647
  # =========================
648
- # Reusable Sticky Header Function
649
  # =========================
650
  def sticky_header(title, message):
651
  st.markdown(
@@ -672,8 +670,8 @@ if st.session_state.app_step == "intro":
672
  st.markdown("This software is developed by *Smart Thinking AI-Solutions Team* to estimate Young's Modulus (Ym) from drilling data.")
673
  st.subheader("How It Works")
674
  st.markdown(
675
- "1) **Upload your data to build the case and preview the model performance.** \n"
676
- "2) Click **Run Model** to compute metrics and plots. \n"
677
  "3) **Proceed to Validation** (with actual Ym) or **Proceed to Prediction** (no Ym)."
678
  )
679
  if st.button("Start Showcase", type="primary"):
@@ -682,13 +680,6 @@ if st.session_state.app_step == "intro":
682
  # =========================
683
  # CASE BUILDING
684
  # =========================
685
- def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
686
- out = df.copy()
687
- out.columns = [c.strip() for c in out.columns]
688
- # Fix flow-rate typo variants
689
- out = out.rename(columns={"Fow Rate, gpm": "Flow Rate, gpm", "Fow Rate, gpm ": "Flow Rate, gpm"})
690
- return out
691
-
692
  if st.session_state.app_step == "dev":
693
  st.sidebar.header("Case Building")
694
  up = st.sidebar.file_uploader("Upload Your Data File", type=["xlsx","xls"])
@@ -708,8 +699,10 @@ if st.session_state.app_step == "dev":
708
  st.session_state.dev_preview = True
709
 
710
  run = st.sidebar.button("Run Model", type="primary", use_container_width=True)
711
- if st.sidebar.button("Proceed to Validation ▶", use_container_width=True): st.session_state.app_step="validate"; st.rerun()
712
- if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
 
 
713
 
714
  if st.session_state.dev_file_loaded and st.session_state.dev_preview:
715
  sticky_header("Case Building", "Previewed ✓ — now click **Run Model**.")
@@ -728,7 +721,6 @@ if st.session_state.app_step == "dev":
728
  tr = _normalize_columns(book[sh_train].copy())
729
  te = _normalize_columns(book[sh_test].copy())
730
 
731
- # Depth is allowed but not required
732
  if not (ensure_cols(tr, FEATURES+[TARGET]) and ensure_cols(te, FEATURES+[TARGET])):
733
  st.markdown('<div class="st-message-box st-error">Missing required columns.</div>', unsafe_allow_html=True)
734
  st.stop()
@@ -784,7 +776,7 @@ if st.session_state.app_step == "dev":
784
  if "Train" in st.session_state.results:
785
  with tab1: _dev_block(st.session_state.results["Train"], st.session_state.results["m_train"])
786
  if "Test" in st.session_state.results:
787
- with tab2: _dev_block(st.session_state.results["Test"], st.session_state.results["m_test"])
788
 
789
  # =========================
790
  # VALIDATION (with actual Ym)
@@ -800,8 +792,10 @@ if st.session_state.app_step == "validate":
800
  if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
801
  st.session_state.show_preview_modal = True
802
  go_btn = st.sidebar.button("Predict & Validate", type="primary", use_container_width=True)
803
- if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
804
- if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True): st.session_state.app_step="predict"; st.rerun()
 
 
805
 
806
  sticky_header("Validate the Model", "Upload a dataset with the same **features** and **Actual Ym** to evaluate performance.")
807
 
@@ -809,8 +803,9 @@ if st.session_state.app_step == "validate":
809
  book = read_book_bytes(up.getvalue())
810
  name = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
811
  df = _normalize_columns(book[name].copy())
812
- if not ensure_cols(df, FEATURES+[TARGET]):
813
  st.markdown('<div class="st-message-box st-error">Missing required columns.</div>', unsafe_allow_html=True); st.stop()
 
814
  df[PRED_COL] = model.predict(df[FEATURES])
815
  st.session_state.results["Validate"]=df
816
 
@@ -861,7 +856,8 @@ if st.session_state.app_step == "validate":
861
  )
862
 
863
  sv = st.session_state.results["sv_val"]
864
- if sv["oor"] > 0: st.markdown('<div class="st-message-box st-warning">Some inputs fall outside **training min–max** ranges.</div>', unsafe_allow_html=True)
 
865
  if st.session_state.results["oor_tbl"] is not None:
866
  st.write("*Out-of-range rows (vs. Training min–max):*")
867
  df_centered_rounded(st.session_state.results["oor_tbl"])
@@ -880,14 +876,15 @@ if st.session_state.app_step == "predict":
880
  if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
881
  st.session_state.show_preview_modal = True
882
  go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
883
- if st.sidebar.button("⬅ Back to Case Building", use_container_width=True): st.session_state.app_step="dev"; st.rerun()
 
884
 
885
  sticky_header("Prediction", "Upload a dataset with the feature columns (no **Actual Ym**).")
886
 
887
  if go_btn and up is not None:
888
  book = read_book_bytes(up.getvalue()); name = list(book.keys())[0]
889
  df = _normalize_columns(book[name].copy())
890
- if not ensure_cols(df, FEATURES):
891
  st.markdown('<div class="st-message-box st-error">Missing required columns.</div>', unsafe_allow_html=True); st.stop()
892
  df[PRED_COL] = model.predict(df[FEATURES])
893
  st.session_state.results["PredictOnly"]=df
@@ -930,10 +927,9 @@ if st.session_state.app_step == "predict":
930
  )
931
 
932
  # =========================
933
- # Run preview modal after all other elements
934
  # =========================
935
  if st.session_state.show_preview_modal:
936
- # Select the correct workbook bytes for this step
937
  book_to_preview = {}
938
  if st.session_state.app_step == "dev":
939
  book_to_preview = read_book_bytes(st.session_state.dev_file_bytes)
@@ -948,16 +944,10 @@ if st.session_state.show_preview_modal:
948
  tabs = st.tabs(names)
949
  for t, name in zip(tabs, names):
950
  with t:
951
- # 🔧 Normalize columns BEFORE plotting/summarizing
952
- df_raw = book_to_preview[name]
953
- df = _normalize_columns(df_raw)
954
-
955
- # Tracks
956
  t1, t2 = st.tabs(["Tracks", "Summary"])
957
  with t1:
958
  st.pyplot(preview_tracks(df, FEATURES), use_container_width=True)
959
-
960
- # Summary (guard against any missing cols after normalization)
961
  with t2:
962
  feat_present = [c for c in FEATURES if c in df.columns]
963
  if not feat_present:
@@ -970,10 +960,13 @@ if st.session_state.show_preview_modal:
970
  .reset_index(names="Feature")
971
  )
972
  df_centered_rounded(tbl)
973
-
974
- # Reset the flag so the modal doesn't stick around
975
  st.session_state.show_preview_modal = False
976
 
 
 
 
 
 
977
 
978
  # =========================
979
  # Footer
@@ -985,4 +978,4 @@ st.markdown("""
985
  © 2025 Smart Thinking AI-Solutions Team. All rights reserved.<br>
986
  Website: <a href="https://smartthinking.com.sa" target="_blank" rel="noopener noreferrer">smartthinking.com.sa</a>
987
  </div>
988
- """, unsafe_allow_html=True)
 
1
+ # app.py — ST_GeoMech_YM (Young's Modulus), mirrors UCS GUI
2
+
3
  import io, json, os, base64, math
4
  from pathlib import Path
5
  import streamlit as st
 
18
  from sklearn.metrics import mean_squared_error, mean_absolute_error
19
 
20
  # =========================
21
+ # App identity (YM)
22
+ # =========================
23
+ APP_NAME = "ST_GeoMech_YM"
24
+ TAGLINE = "Real-Time Young's Modulus Tracking"
25
+
26
  # =========================
27
+ # Constants (YM)
28
+ # =========================
29
+ FEATURES = [
30
+ "WOB(klbf)", "TORQUE(kft.lbf)", "SPP(psi)", "RPM(1/min)",
31
+ "ROP(ft/h)", "Flow Rate, gpm"
32
+ ]
33
+ TARGET = "Actual Ym" # column with actual Young's Modulus
34
+ PRED_COL = "Ym_Pred" # column we will create with predictions
35
 
36
  MODELS_DIR = Path("models")
37
  DEFAULT_MODEL = MODELS_DIR / "ym_rf.joblib"
38
  MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
39
+
40
  COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
41
 
42
  # ---- Plot sizing controls ----
43
  CROSS_W = 350
44
  CROSS_H = 350
45
  TRACK_H = 1000
46
+ TRACK_W = 400
47
+ FONT_SZ = 13
48
  BOLD_FONT = "Arial Black, Arial, sans-serif"
49
 
50
  # =========================
51
  # Page / CSS
52
  # =========================
53
+ st.set_page_config(page_title=APP_NAME, page_icon="logo.png", layout="wide")
54
 
55
+ # General CSS
56
  st.markdown("""
57
  <style>
58
  .brand-logo { width: 200px; height: auto; object-fit: contain; }
59
  .sidebar-header { display:flex; align-items:center; gap:12px; }
60
  .sidebar-header .text h1 { font-size: 1.05rem; margin:0; line-height:1.1; }
61
  .sidebar-header .text .tag { font-size: .85rem; color:#6b7280; margin:2px 0 0; }
62
+ .centered-container { display:flex; flex-direction:column; align-items:center; text-align:center; }
63
  </style>
64
  """, unsafe_allow_html=True)
65
 
66
+ # Allow sticky bits (preview expander header & tabs)
67
  st.markdown("""
68
  <style>
69
  .main .block-container { overflow: unset !important; }
 
81
  </style>
82
  """, unsafe_allow_html=True)
83
 
84
+ # Sticky Preview expander & tabs
85
  st.markdown("""
86
  <style>
87
  div[data-testid="stExpander"] > details > summary {
 
93
  </style>
94
  """, unsafe_allow_html=True)
95
 
96
+ # Center text in all pandas Styler tables (headers + cells)
97
  TABLE_CENTER_CSS = [
98
  dict(selector="th", props=[("text-align", "center")]),
99
  dict(selector="td", props=[("text-align", "center")]),
100
  ]
101
 
102
+ # Message box styles
103
  st.markdown("""
104
  <style>
105
+ .st-message-box { background:#f0f2f6; color:#333; padding:10px; border-radius:10px; border:1px solid #e6e9ef; }
106
+ .st-message-box.st-success { background:#d4edda; color:#155724; border-color:#c3e6cb; }
107
+ .st-message-box.st-warning { background:#fff3cd; color:#856404; border-color:#ffeeba; }
108
+ .st-message-box.st-error { background:#f8d7da; color:#721c24; border-color:#f5c6cb; }
109
  </style>
110
  """, unsafe_allow_html=True)
111
 
 
115
  def inline_logo(path="logo.png") -> str:
116
  try:
117
  p = Path(path)
118
+ if not p.exists():
119
+ return ""
120
  return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
121
  except Exception:
122
  return ""
 
137
  st.sidebar.markdown(f"""
138
  <div class="centered-container">
139
  <img src="{inline_logo('logo.png')}" style="width: 200px; height: auto; object-fit: contain;">
140
+ <div style='font-weight:800;font-size:1.2rem;'>{APP_NAME}</div>
141
  <div style='color:#667085;'>Smart Thinking • Secure Access</div>
142
  </div>
143
  """, unsafe_allow_html=True
 
162
  def pearson_r(y_true, y_pred) -> float:
163
  a = np.asarray(y_true, dtype=float)
164
  p = np.asarray(y_pred, dtype=float)
165
+ if a.size < 2:
166
+ return float("nan")
167
  return float(np.corrcoef(a, p)[0, 1])
168
 
169
  @st.cache_resource(show_spinner=False)
 
176
  xl = pd.ExcelFile(bio)
177
  return {sh: xl.parse(sh) for sh in xl.sheet_names}
178
 
179
+ def read_book_bytes(b: bytes):
180
+ return parse_excel(b) if b else {}
181
+
182
+ def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
183
+ out = df.copy()
184
+ out.columns = [str(c).strip() for c in out.columns]
185
+ # Fix common typos
186
+ out = out.rename(columns={
187
+ "Fow Rate, gpm": "Flow Rate, gpm",
188
+ "Fow Rate, gpm ": "Flow Rate, gpm"
189
+ })
190
+ return out
191
 
192
  def ensure_cols(df, cols):
193
+ miss = [c for c in cols if c not in df.columns]
 
 
 
 
 
194
  if miss:
195
+ st.error(f"Missing columns: {miss}\nFound: {list(df.columns)}")
196
  return False
 
 
197
  return True
198
 
 
199
  def find_sheet(book, names):
200
  low2orig = {k.lower(): k for k in book.keys()}
201
  for nm in names:
202
+ if nm.lower() in low2orig:
203
+ return low2orig[nm.lower()]
204
  return None
205
 
206
  def _nice_tick0(xmin: float, step: float = 0.1) -> float:
 
207
  return step * math.floor(xmin / step) if np.isfinite(xmin) else xmin
208
 
209
  def df_centered_rounded(df: pd.DataFrame, hide_index=True):
 
211
  numcols = out.select_dtypes(include=[np.number]).columns
212
  styler = (
213
  out.style
214
+ .format({c: "{:.2f}" for c in numcols})
215
+ .set_properties(**{"text-align": "center"})
216
+ .set_table_styles(TABLE_CENTER_CSS)
217
  )
218
  st.dataframe(styler, use_container_width=True, hide_index=hide_index)
219
 
220
+ # === Excel export helpers (YM) ================================================
221
  def _excel_engine() -> str:
222
  try:
223
  import xlsxwriter # noqa: F401
 
241
  cols = [c for c in cols if c in df.columns]
242
  if not cols:
243
  return pd.DataFrame()
244
+ return (
245
+ df[cols]
246
+ .agg(['min','max','mean','std'])
247
+ .T.rename(columns={"min":"Min","max":"Max","mean":"Mean","std":"Std"})
248
+ .reset_index(names="Field")
249
+ )
250
 
251
  def _train_ranges_df(ranges: dict[str, tuple[float, float]]) -> pd.DataFrame:
252
  if not ranges:
 
256
  return df
257
 
258
  def _excel_autofit(writer, sheet_name: str, df: pd.DataFrame, min_w: int = 8, max_w: int = 40):
 
259
  try:
260
  import xlsxwriter # noqa: F401
261
  except Exception:
262
  return
263
  ws = writer.sheets[sheet_name]
 
264
  for i, col in enumerate(df.columns):
265
  series = df[col].astype(str)
266
  max_len = max([len(str(col))] + series.map(len).tolist())
267
  ws.set_column(i, i, max(min_w, min(max_len + 2, max_w)))
 
268
  ws.freeze_panes(1, 0)
269
 
270
  def _add_sheet(sheets: dict, order: list, name: str, df: pd.DataFrame, ndigits: int):
271
+ if isinstance(df, pd.DataFrame) and not df.empty:
272
+ sheets[name] = _round_numeric(df, ndigits)
273
+ order.append(name)
 
274
 
275
  def _available_sections():
 
276
  res = st.session_state.get("results", {})
277
  sections = []
278
  if "Train" in res: sections += ["Training","Training_Metrics","Training_Summary"]
279
  if "Test" in res: sections += ["Testing","Testing_Metrics","Testing_Summary"]
280
  if "Validate" in res: sections += ["Validation","Validation_Metrics","Validation_Summary","Validation_OOR"]
281
+ if "PredictOnly" in res: sections += ["Prediction","Prediction_Summary"]
282
  if st.session_state.get("train_ranges"): sections += ["Training_Ranges"]
283
  sections += ["Info"]
284
  return sections
285
 
286
  def build_export_workbook(selected: list[str], ndigits: int = 2) -> tuple[bytes|None, str|None, list[str]]:
 
287
  res = st.session_state.get("results", {})
288
  if not res:
289
  return None, None, []
 
297
  if "Training_Metrics" in selected and res.get("m_train"):
298
  _add_sheet(sheets, order, "Training_Metrics", pd.DataFrame([res["m_train"]]), ndigits)
299
  if "Training_Summary" in selected and "Train" in res:
300
+ tr_cols = FEATURES + [c for c in [TARGET, PRED_COL] if c in res["Train"].columns]
301
  _add_sheet(sheets, order, "Training_Summary", _summary_table(res["Train"], tr_cols), ndigits)
302
 
303
  # Testing
 
306
  if "Testing_Metrics" in selected and res.get("m_test"):
307
  _add_sheet(sheets, order, "Testing_Metrics", pd.DataFrame([res["m_test"]]), ndigits)
308
  if "Testing_Summary" in selected and "Test" in res:
309
+ te_cols = FEATURES + [c for c in [TARGET, PRED_COL] if c in res["Test"].columns]
310
  _add_sheet(sheets, order, "Testing_Summary", _summary_table(res["Test"], te_cols), ndigits)
311
 
312
  # Validation
 
324
  _add_sheet(sheets, order, "Prediction", res["PredictOnly"], ndigits)
325
  if "Prediction_Summary" in selected and res.get("sv_pred"):
326
  _add_sheet(sheets, order, "Prediction_Summary", pd.DataFrame([res["sv_pred"]]), ndigits)
 
 
327
 
328
  # Training ranges
329
  if "Training_Ranges" in selected and st.session_state.get("train_ranges"):
 
333
  # Info
334
  if "Info" in selected:
335
  info = pd.DataFrame([
336
+ {"Key": "AppName", "Value": APP_NAME},
337
+ {"Key": "Tagline", "Value": TAGLINE},
338
+ {"Key": "Target", "Value": TARGET},
339
+ {"Key": "PredColumn", "Value": PRED_COL},
340
+ {"Key": "Features", "Value": ", ".join(FEATURES)},
341
+ {"Key": "ExportedAt", "Value": datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
 
342
  ])
343
  _add_sheet(sheets, order, "Info", info, ndigits)
344
 
345
  if not order:
346
  return None, None, []
347
 
 
348
  bio = io.BytesIO()
349
  engine = _excel_engine()
350
  with pd.ExcelWriter(bio, engine=engine) as writer:
351
  for name in order:
352
  df = sheets[name]
353
+ sheet = _excel_safe_name(name)
354
+ df.to_excel(writer, sheet_name=sheet, index=False)
355
+ _excel_autofit(writer, sheet, df)
356
  bio.seek(0)
357
 
358
+ fname = f"YM_Export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
359
  return bio.getvalue(), fname, order
360
 
361
  def render_export_button(key: str = "export_main") -> None:
362
  st.divider()
363
  st.markdown("### Export to Excel")
364
 
 
365
  default_sections = _available_sections()
366
  all_sections = [
367
  "Training","Training_Metrics","Training_Summary",
368
  "Testing","Testing_Metrics","Testing_Summary",
369
  "Validation","Validation_Metrics","Validation_Summary","Validation_OOR",
370
+ "Prediction","Prediction_Summary",
371
  "Training_Ranges","Info"
372
  ]
373
+ selected = st.multiselect("Sheets to include", options=all_sections, default=default_sections)
374
+ c1, c2 = st.columns([1,2])
 
 
 
 
 
 
375
  with c1:
376
+ ndigits = st.number_input("Rounding (decimals)", 0, 6, 2, 1)
377
  with c2:
378
+ base_name = st.text_input("Base filename", value="YM_Export")
 
 
 
 
379
 
380
+ data, _, names = build_export_workbook(selected=selected, ndigits=int(ndigits))
381
  if names:
382
  st.caption("Will include: " + ", ".join(names))
383
+
384
  st.download_button(
385
+ "⬇️ Export Excel",
386
  data=(data or b""),
387
+ file_name=((base_name or "YM_Export") + "_" + datetime.now().strftime("%Y%m%d_%H%M%S") + ".xlsx") if data else "YM_Export.xlsx",
388
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
389
  disabled=(data is None),
 
390
  key=key,
391
  )
392
+
393
  # =========================
394
  # Cross plot (Matplotlib) — auto-scaled for Ym
395
  # =========================
396
  def cross_plot_static(actual, pred, xlabel="Actual Ym", ylabel="Predicted Ym"):
397
  a = pd.Series(actual, dtype=float)
398
+ p = pd.Series(pred, dtype=float)
399
 
400
  lo = float(min(a.min(), p.min()))
401
  hi = float(max(a.max(), p.max()))
 
416
  ax.set_yticks(ticks)
417
  ax.set_aspect("equal", adjustable="box")
418
 
 
419
  fmt = FuncFormatter(lambda x, _: f"{x:.2f}")
420
  ax.xaxis.set_major_formatter(fmt)
421
  ax.yaxis.set_major_formatter(fmt)
 
433
  return fig
434
 
435
  # =========================
436
+ # Track plot (Plotly) — x axis with NO decimals
437
  # =========================
438
  def track_plot(df, include_actual=True):
439
  # Depth (or index) on Y
 
462
  x=df[PRED_COL], y=y, mode="lines",
463
  line=dict(color=COLORS["pred"], width=1.8),
464
  name=PRED_COL,
465
+ hovertemplate=f"{PRED_COL}: "+"%{x:.0f}<br>"+ylab+": %{y}<extra></extra>"
466
  ))
467
  if include_actual and TARGET in df.columns:
468
  fig.add_trace(go.Scatter(
469
  x=df[TARGET], y=y, mode="lines",
470
  line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
471
  name=f"{TARGET} (actual)",
472
+ hovertemplate=f"{TARGET}: "+"%{x:.0f}<br>"+ylab+": %{y}<extra></extra>"
473
  ))
474
 
475
  fig.update_layout(
476
+ height=TRACK_H, width=TRACK_W, autosize=False,
 
 
477
  paper_bgcolor="#fff", plot_bgcolor="#fff",
478
  margin=dict(l=64, r=16, t=36, b=48), hovermode="closest",
479
  font=dict(size=FONT_SZ, color="#000"),
 
481
  bgcolor="rgba(255,255,255,0.75)", bordercolor="#ccc", borderwidth=1),
482
  legend_title_text=""
483
  )
 
484
  fig.update_xaxes(
485
  title_text="Ym",
486
  title_font=dict(size=20, family=BOLD_FONT, color="#000"),
487
  tickfont=dict(size=15, family=BOLD_FONT, color="#000"),
488
+ side="top", range=[xmin, xmax],
489
+ ticks="outside", tickformat=",.0f", tickmode="auto", tick0=tick0,
 
 
 
 
490
  showline=True, linewidth=1.2, linecolor="#444", mirror=True,
491
  showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
492
  )
493
+ fig.update_yaxes(
494
+ title_text=ylab,
 
495
  title_font=dict(size=20, family=BOLD_FONT, color="#000"),
496
  tickfont=dict(size=15, family=BOLD_FONT, color="#000"),
497
+ range=y_range, ticks="outside",
 
 
 
 
 
498
  showline=True, linewidth=1.2, linecolor="#444", mirror=True,
499
  showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
500
  )
501
+
502
  return fig
503
 
504
  # ---------- Preview modal (matplotlib) ----------
 
510
  ax.text(0.5,0.5,"No selected columns",ha="center",va="center"); ax.axis("off")
511
  return fig
512
  fig, axes = plt.subplots(1, n, figsize=(2.2*n, 7.0), sharey=True, dpi=100)
513
+ if n == 1:
514
+ axes = [axes]
515
  idx = np.arange(1, len(df) + 1)
516
  for ax, col in zip(axes, cols):
517
+ ax.plot(pd.to_numeric(df[col], errors="coerce"), idx, '-', lw=1.4, color="#333")
518
  ax.set_xlabel(col); ax.xaxis.set_label_position('top'); ax.xaxis.tick_top(); ax.invert_yaxis()
519
  ax.grid(True, linestyle=":", alpha=0.3)
520
  for s in ax.spines.values(): s.set_visible(True)
521
  axes[0].set_ylabel("Point Index")
522
  return fig
523
 
524
+ # Modal wrapper (Streamlit compatibility)
525
  try:
526
  dialog = st.dialog
527
  except AttributeError:
 
545
  with t1:
546
  st.pyplot(preview_tracks(df, FEATURES), use_container_width=True)
547
  with t2:
548
+ feat_present = [c for c in FEATURES if c in df.columns]
549
+ if not feat_present:
550
+ st.info("No feature columns found to summarize.")
551
+ else:
552
+ tbl = (
553
+ df[feat_present]
554
+ .agg(['min','max','mean','std'])
555
+ .T.rename(columns={"min":"Min","max":"Max","mean":"Mean","std":"Std"})
556
+ .reset_index(names="Feature")
557
+ )
558
+ df_centered_rounded(tbl)
559
 
560
  # =========================
561
+ # Load model + meta
562
  # =========================
563
  def ensure_model() -> Path|None:
564
  for p in [DEFAULT_MODEL, *MODEL_FALLBACKS]:
565
+ if p.exists() and p.stat().st_size > 0:
566
+ return p
567
  url = os.environ.get("MODEL_URL", "")
568
+ if not url:
569
+ return None
570
  try:
571
  import requests
572
  DEFAULT_MODEL.parent.mkdir(parents=True, exist_ok=True)
 
574
  r.raise_for_status()
575
  with open(DEFAULT_MODEL, "wb") as f:
576
  for chunk in r.iter_content(1<<20):
577
+ if chunk:
578
+ f.write(chunk)
579
  return DEFAULT_MODEL
580
  except Exception:
581
  return None
 
590
  st.error(f"Failed to load model: {e}")
591
  st.stop()
592
 
593
+ # Load meta (optional): support models/meta.json or models/ym_meta.json
594
+ meta = {}
595
+ for cand in [MODELS_DIR / "meta.json", MODELS_DIR / "ym_meta.json"]:
596
+ if cand.exists():
597
+ try:
598
+ meta = json.loads(cand.read_text(encoding="utf-8"))
599
+ break
600
+ except Exception:
601
+ pass
602
+
603
+ if meta:
604
+ FEATURES = meta.get("features", FEATURES)
605
+ TARGET = meta.get("target", TARGET)
606
+
607
+ # Warn if runtime != training versions
608
+ try:
609
+ import numpy as _np, sklearn as _skl
610
+ mv = meta.get("versions", {}) if isinstance(meta, dict) else {}
 
611
  msg = []
612
  if mv.get("numpy") and mv["numpy"] != _np.__version__:
613
  msg.append(f"NumPy {mv['numpy']} expected, running {_np.__version__}")
 
615
  msg.append(f"scikit-learn {mv['scikit_learn']} expected, running {_skl.__version__}")
616
  if msg:
617
  st.warning("Environment mismatch: " + " | ".join(msg))
618
+ except Exception:
619
+ pass
620
 
621
  # =========================
622
  # Session state
 
636
  st.sidebar.markdown(f"""
637
  <div class="centered-container">
638
  <img src="{inline_logo('logo.png')}" style="width: 200px; height: auto; object-fit: contain;">
639
+ <div style='font-weight:800;font-size:1.2rem;'>{APP_NAME}</div>
640
+ <div style='color:#667085;'>{TAGLINE}</div>
641
  </div>
642
  """, unsafe_allow_html=True
643
  )
644
 
645
  # =========================
646
+ # Reusable Sticky Header
647
  # =========================
648
  def sticky_header(title, message):
649
  st.markdown(
 
670
  st.markdown("This software is developed by *Smart Thinking AI-Solutions Team* to estimate Young's Modulus (Ym) from drilling data.")
671
  st.subheader("How It Works")
672
  st.markdown(
673
+ "1) **Upload your data to build the case and preview the model performance.** \n"
674
+ "2) Click **Run Model** to compute metrics and plots. \n"
675
  "3) **Proceed to Validation** (with actual Ym) or **Proceed to Prediction** (no Ym)."
676
  )
677
  if st.button("Start Showcase", type="primary"):
 
680
  # =========================
681
  # CASE BUILDING
682
  # =========================
 
 
 
 
 
 
 
683
  if st.session_state.app_step == "dev":
684
  st.sidebar.header("Case Building")
685
  up = st.sidebar.file_uploader("Upload Your Data File", type=["xlsx","xls"])
 
699
  st.session_state.dev_preview = True
700
 
701
  run = st.sidebar.button("Run Model", type="primary", use_container_width=True)
702
+ if st.sidebar.button("Proceed to Validation ▶", use_container_width=True):
703
+ st.session_state.app_step="validate"; st.rerun()
704
+ if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True):
705
+ st.session_state.app_step="predict"; st.rerun()
706
 
707
  if st.session_state.dev_file_loaded and st.session_state.dev_preview:
708
  sticky_header("Case Building", "Previewed ✓ — now click **Run Model**.")
 
721
  tr = _normalize_columns(book[sh_train].copy())
722
  te = _normalize_columns(book[sh_test].copy())
723
 
 
724
  if not (ensure_cols(tr, FEATURES+[TARGET]) and ensure_cols(te, FEATURES+[TARGET])):
725
  st.markdown('<div class="st-message-box st-error">Missing required columns.</div>', unsafe_allow_html=True)
726
  st.stop()
 
776
  if "Train" in st.session_state.results:
777
  with tab1: _dev_block(st.session_state.results["Train"], st.session_state.results["m_train"])
778
  if "Test" in st.session_state.results:
779
+ with tab2: _dev_block(st.session_state.results["Test"], st.session_state.results["m_test"])
780
 
781
  # =========================
782
  # VALIDATION (with actual Ym)
 
792
  if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
793
  st.session_state.show_preview_modal = True
794
  go_btn = st.sidebar.button("Predict & Validate", type="primary", use_container_width=True)
795
+ if st.sidebar.button("⬅ Back to Case Building", use_container_width=True):
796
+ st.session_state.app_step="dev"; st.rerun()
797
+ if st.sidebar.button("Proceed to Prediction ▶", use_container_width=True):
798
+ st.session_state.app_step="predict"; st.rerun()
799
 
800
  sticky_header("Validate the Model", "Upload a dataset with the same **features** and **Actual Ym** to evaluate performance.")
801
 
 
803
  book = read_book_bytes(up.getvalue())
804
  name = find_sheet(book, ["Validation","Validate","validation2","Val","val"]) or list(book.keys())[0]
805
  df = _normalize_columns(book[name].copy())
806
+ if not ensure_cols(df, FEATURES+[TARGET]):
807
  st.markdown('<div class="st-message-box st-error">Missing required columns.</div>', unsafe_allow_html=True); st.stop()
808
+
809
  df[PRED_COL] = model.predict(df[FEATURES])
810
  st.session_state.results["Validate"]=df
811
 
 
856
  )
857
 
858
  sv = st.session_state.results["sv_val"]
859
+ if sv["oor"] > 0:
860
+ st.markdown('<div class="st-message-box st-warning">Some inputs fall outside **training min–max** ranges.</div>', unsafe_allow_html=True)
861
  if st.session_state.results["oor_tbl"] is not None:
862
  st.write("*Out-of-range rows (vs. Training min–max):*")
863
  df_centered_rounded(st.session_state.results["oor_tbl"])
 
876
  if st.sidebar.button("Preview data", use_container_width=True, disabled=(up is None)):
877
  st.session_state.show_preview_modal = True
878
  go_btn = st.sidebar.button("Predict", type="primary", use_container_width=True)
879
+ if st.sidebar.button("⬅ Back to Case Building", use_container_width=True):
880
+ st.session_state.app_step="dev"; st.rerun()
881
 
882
  sticky_header("Prediction", "Upload a dataset with the feature columns (no **Actual Ym**).")
883
 
884
  if go_btn and up is not None:
885
  book = read_book_bytes(up.getvalue()); name = list(book.keys())[0]
886
  df = _normalize_columns(book[name].copy())
887
+ if not ensure_cols(df, FEATURES):
888
  st.markdown('<div class="st-message-box st-error">Missing required columns.</div>', unsafe_allow_html=True); st.stop()
889
  df[PRED_COL] = model.predict(df[FEATURES])
890
  st.session_state.results["PredictOnly"]=df
 
927
  )
928
 
929
  # =========================
930
+ # Preview modal (re-usable)
931
  # =========================
932
  if st.session_state.show_preview_modal:
 
933
  book_to_preview = {}
934
  if st.session_state.app_step == "dev":
935
  book_to_preview = read_book_bytes(st.session_state.dev_file_bytes)
 
944
  tabs = st.tabs(names)
945
  for t, name in zip(tabs, names):
946
  with t:
947
+ df = _normalize_columns(book_to_preview[name])
 
 
 
 
948
  t1, t2 = st.tabs(["Tracks", "Summary"])
949
  with t1:
950
  st.pyplot(preview_tracks(df, FEATURES), use_container_width=True)
 
 
951
  with t2:
952
  feat_present = [c for c in FEATURES if c in df.columns]
953
  if not feat_present:
 
960
  .reset_index(names="Feature")
961
  )
962
  df_centered_rounded(tbl)
 
 
963
  st.session_state.show_preview_modal = False
964
 
965
+ # === Bottom-of-page Export (per step) =========================================
966
+ if st.session_state.app_step in ("dev", "validate", "predict"):
967
+ has_results = any(k in st.session_state.results for k in ("Train", "Test", "Validate", "PredictOnly"))
968
+ if has_results:
969
+ render_export_button(key=f"export_{st.session_state.app_step}")
970
 
971
  # =========================
972
  # Footer
 
978
  © 2025 Smart Thinking AI-Solutions Team. All rights reserved.<br>
979
  Website: <a href="https://smartthinking.com.sa" target="_blank" rel="noopener noreferrer">smartthinking.com.sa</a>
980
  </div>
981
+ """, unsafe_allow_html=True)