UCS2014 commited on
Commit
fa1bd3b
·
verified ·
1 Parent(s): 19c4599

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +153 -419
app.py CHANGED
@@ -1,6 +1,6 @@
1
  # app.py — ST_Min_Horizontal_Stress (σhmin)
2
- # End-to-end Streamlit app that loads MODEL/META from Hugging Face Hub using hf_hub_download
3
- # (handles large LFS files; uses AUTH_TOKEN if repo is private). Falls back to local/mnt/data.
4
 
5
  import io, json, os, base64, math
6
  from pathlib import Path
@@ -16,12 +16,10 @@ import matplotlib
16
  matplotlib.use("Agg")
17
  import matplotlib.pyplot as plt
18
  from matplotlib.ticker import FuncFormatter
19
- from huggingface_hub import hf_hub_download
20
 
21
  import plotly.graph_objects as go
22
  from sklearn.metrics import mean_squared_error
23
 
24
-
25
  # =========================
26
  # App constants / defaults
27
  # =========================
@@ -44,12 +42,6 @@ BOLD_FONT = "Arial Black, Arial, sans-serif"
44
 
45
  STRICT_VERSION_CHECK = True
46
 
47
- # Local fallbacks (optional) — used only if HF Hub fails, or if you upload to /mnt/data
48
- MODELS_DIR = Path("models")
49
- DEFAULT_MODEL = MODELS_DIR / "minstress_model.joblib"
50
- MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
51
- META_CANDIDATES = [MODELS_DIR / "minstress_meta.json", MODELS_DIR / "meta.json"]
52
-
53
  # =========================
54
  # Page / CSS
55
  # =========================
@@ -79,7 +71,7 @@ TABLE_CENTER_CSS = [
79
  ]
80
 
81
  # =========================
82
- # Password gate
83
  # =========================
84
  def inline_logo(path="logo.png") -> str:
85
  try:
@@ -90,15 +82,14 @@ def inline_logo(path="logo.png") -> str:
90
  return ""
91
 
92
  def add_password_gate() -> None:
93
- # Read password from Secrets or ENV
94
  try:
95
  required = st.secrets.get("APP_PASSWORD", "")
96
  except Exception:
97
  required = os.environ.get("APP_PASSWORD", "")
98
 
99
  if not required:
100
- st.warning("Set APP_PASSWORD in Secrets (or environment) and restart.")
101
- st.stop()
102
  if st.session_state.get("auth_ok", False):
103
  return
104
 
@@ -214,347 +205,6 @@ def _make_X(df: pd.DataFrame, features: list[str]) -> pd.DataFrame:
214
  X[c] = pd.to_numeric(X[c], errors="coerce")
215
  return X
216
 
217
- # =========================
218
- # Export helpers
219
- # =========================
220
- def _summary_table(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
221
- cols = [c for c in cols if c in df.columns]
222
- if not cols: return pd.DataFrame()
223
- tbl = (df[cols]
224
- .agg(['min','max','mean','std'])
225
- .T.rename(columns={"min":"Min","max":"Max","mean":"Mean","std":"Std"})
226
- .reset_index(names="Field"))
227
- return _round_numeric(tbl, 3)
228
-
229
- def _train_ranges_df(ranges: dict[str, tuple[float, float]]) -> pd.DataFrame:
230
- if not ranges: return pd.DataFrame()
231
- df = pd.DataFrame(ranges).T.reset_index()
232
- df.columns = ["Feature", "Min", "Max"]
233
- return _round_numeric(df, 3)
234
-
235
- def _excel_autofit(writer, sheet_name: str, df: pd.DataFrame, min_w: int = 8, max_w: int = 40):
236
- try:
237
- import xlsxwriter # noqa: F401
238
- except Exception:
239
- return
240
- ws = writer.sheets[sheet_name]
241
- for i, col in enumerate(df.columns):
242
- series = df[col].astype(str)
243
- max_len = max([len(str(col))] + series.map(len).tolist())
244
- ws.set_column(i, i, max(min_w, min(max_len + 2, max_w)))
245
- ws.freeze_panes(1, 0)
246
-
247
- def _available_sections() -> list[str]:
248
- res = st.session_state.get("results", {})
249
- sections = []
250
- if "Train" in res: sections += ["Training","Training_Metrics","Training_Summary"]
251
- if "Test" in res: sections += ["Testing","Testing_Metrics","Testing_Summary"]
252
- if "Validate" in res: sections += ["Validation","Validation_Metrics","Validation_Summary","Validation_OOR"]
253
- if "PredictOnly" in res: sections += ["Prediction","Prediction_Summary"]
254
- if st.session_state.get("train_ranges"): sections += ["Training_Ranges"]
255
- sections += ["Info"]
256
- return sections
257
-
258
- def build_export_workbook(selected: list[str], ndigits: int = 3, do_autofit: bool = True) -> tuple[bytes|None, str|None, list[str]]:
259
- res = st.session_state.get("results", {})
260
- if not res: return None, None, []
261
- sheets: dict[str, pd.DataFrame] = {}
262
- order: list[str] = []
263
-
264
- def _add(name: str, df: pd.DataFrame):
265
- if df is None or (isinstance(df, pd.DataFrame) and df.empty): return
266
- sheets[name] = _round_numeric(df, ndigits); order.append(name)
267
-
268
- if "Training" in selected and "Train" in res: _add("Training", res["Train"])
269
- if "Training_Metrics" in selected and res.get("m_train"): _add("Training_Metrics", pd.DataFrame([res["m_train"]]))
270
- if "Training_Summary" in selected and "Train" in res:
271
- tr_cols = FEATURES + [c for c in [TARGET, PRED_COL] if c in res["Train"].columns]
272
- _add("Training_Summary", _summary_table(res["Train"], tr_cols))
273
-
274
- if "Testing" in selected and "Test" in res: _add("Testing", res["Test"])
275
- if "Testing_Metrics" in selected and res.get("m_test"): _add("Testing_Metrics", pd.DataFrame([res["m_test"]]))
276
- if "Testing_Summary" in selected and "Test" in res:
277
- te_cols = FEATURES + [c for c in [TARGET, PRED_COL] if c in res["Test"].columns]
278
- _add("Testing_Summary", _summary_table(res["Test"], te_cols))
279
-
280
- if "Validation" in selected and "Validate" in res: _add("Validation", res["Validate"])
281
- if "Validation_Metrics" in selected and res.get("m_val"): _add("Validation_Metrics", pd.DataFrame([res["m_val"]]))
282
- if "Validation_Summary" in selected and res.get("sv_val"): _add("Validation_Summary", pd.DataFrame([res["sv_val"]]))
283
- if "Validation_OOR" in selected and isinstance(res.get("oor_tbl"), pd.DataFrame) and not res["oor_tbl"].empty:
284
- _add("Validation_OOR", res["oor_tbl"].reset_index(drop=True))
285
-
286
- if "Prediction" in selected and "PredictOnly" in res: _add("Prediction", res["PredictOnly"])
287
- if "Prediction_Summary" in selected and res.get("sv_pred"): _add("Prediction_Summary", pd.DataFrame([res["sv_pred"]]))
288
-
289
- if "Training_Ranges" in selected and st.session_state.get("train_ranges"):
290
- _add("Training_Ranges", _train_ranges_df(st.session_state["train_ranges"]))
291
-
292
- if "Info" in selected:
293
- info = pd.DataFrame([
294
- {"Key": "AppName", "Value": APP_NAME},
295
- {"Key": "Tagline", "Value": TAGLINE},
296
- {"Key": "Target", "Value": TARGET},
297
- {"Key": "PredColumn", "Value": PRED_COL},
298
- {"Key": "Features", "Value": ", ".join(FEATURES)},
299
- {"Key": "ExportedAt", "Value": datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
300
- ])
301
- _add("Info", info)
302
-
303
- if not order: return None, None, []
304
-
305
- bio = io.BytesIO()
306
- engine = _excel_engine()
307
- with pd.ExcelWriter(bio, engine=engine) as writer:
308
- for name in order:
309
- df = sheets[name]; sheet = _excel_safe_name(name)
310
- df.to_excel(writer, sheet_name=sheet, index=False)
311
- if do_autofit: _excel_autofit(writer, sheet, df)
312
- bio.seek(0)
313
- fname = f"MinStress_Export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
314
- return bio.getvalue(), fname, order
315
-
316
- def render_export_button(phase_key: str) -> None:
317
- res = st.session_state.get("results", {})
318
- if not res: return
319
- st.divider()
320
- st.markdown("### Export to Excel")
321
- options = _available_sections()
322
- selected_sheets = st.multiselect(
323
- "Sheets to include",
324
- options=options,
325
- default=[],
326
- placeholder="Choose option(s)",
327
- help="Pick the sheets you want in the Excel export.",
328
- key=f"sheets_{phase_key}",
329
- )
330
- if not selected_sheets:
331
- st.caption("Select one or more sheets above to enable export.")
332
- st.download_button("⬇️ Export Excel", data=b"", file_name="MinStress_Export.xlsx",
333
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
334
- disabled=True, key=f"download_{phase_key}")
335
- return
336
- data, fname, names = build_export_workbook(selected=selected_sheets, ndigits=3, do_autofit=True)
337
- if names: st.caption("Will include: " + ", ".join(names))
338
- st.download_button("⬇️ Export Excel", data=(data or b""), file_name=(fname or "MinStress_Export.xlsx"),
339
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
340
- disabled=(data is None), key=f"download_{phase_key}")
341
-
342
- # =========================
343
- # Plots
344
- # =========================
345
- def cross_plot_static(actual, pred):
346
- a = pd.Series(actual, dtype=float)
347
- p = pd.Series(pred, dtype=float)
348
- lo = float(min(a.min(), p.min())); hi = float(max(a.max(), p.max()))
349
- pad = 0.03 * (hi - lo if hi > lo else 1.0)
350
- lo2, hi2 = lo - pad, hi + pad
351
- ticks = np.linspace(lo2, hi2, 5)
352
-
353
- dpi = 110
354
- fig, ax = plt.subplots(figsize=(CROSS_W / dpi, CROSS_H / dpi), dpi=dpi, constrained_layout=False)
355
- ax.scatter(a, p, s=14, c=COLORS["pred"], alpha=0.9, linewidths=0)
356
- ax.plot([lo2, hi2], [lo2, hi2], linestyle="--", linewidth=1.2, color=COLORS["ref"])
357
-
358
- ax.set_xlim(lo2, hi2); ax.set_ylim(lo2, hi2)
359
- ax.set_xticks(ticks); ax.set_yticks(ticks)
360
- ax.set_aspect("equal", adjustable="box")
361
-
362
- fmt = FuncFormatter(lambda x, _: f"{x:.2f}")
363
- ax.xaxis.set_major_formatter(fmt); ax.yaxis.set_major_formatter(fmt)
364
-
365
- ax.set_xlabel(f"Actual Min Stress ({UNITS})", fontweight="bold", fontsize=10, color="black")
366
- ax.set_ylabel(f"Predicted Min Stress ({UNITS})", fontweight="bold", fontsize=10, color="black")
367
- ax.tick_params(labelsize=6, colors="black")
368
- ax.grid(True, linestyle=":", alpha=0.3)
369
- for spine in ax.spines.values():
370
- spine.set_linewidth(1.1); spine.set_color("#444")
371
-
372
- fig.subplots_adjust(left=0.16, bottom=0.16, right=0.98, top=0.98)
373
- return fig
374
-
375
- def track_plot(df, include_actual=True):
376
- depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
377
- if depth_col is not None:
378
- y = pd.to_numeric(df[depth_col], errors="coerce"); ylab = depth_col
379
- y_range = [float(np.nanmax(y)), float(np.nanmin(y))] # reversed
380
- else:
381
- y = pd.Series(np.arange(1, len(df) + 1)); ylab = "Point Index"
382
- y_range = [float(y.max()), float(y.min())]
383
-
384
- x_series = pd.Series(df.get(PRED_COL, pd.Series(dtype=float))).astype(float)
385
- act_col = ACTUAL_COL if (ACTUAL_COL and ACTUAL_COL in df.columns) else TARGET
386
- if include_actual and act_col in df.columns:
387
- x_series = pd.concat([x_series, pd.Series(df[act_col]).astype(float)], ignore_index=True)
388
- x_lo, x_hi = float(x_series.min()), float(x_series.max())
389
- x_pad = 0.03 * (x_hi - x_lo if x_hi > x_lo else 1.0)
390
- xmin, xmax = x_lo - x_pad, x_hi + x_pad
391
- tick0 = _nice_tick0(xmin, step=max((xmax - xmin) / 10.0, 0.1))
392
-
393
- fig = go.Figure()
394
- if PRED_COL in df.columns:
395
- fig.add_trace(go.Scatter(
396
- x=df[PRED_COL], y=y, mode="lines",
397
- line=dict(color=COLORS["pred"], width=1.8),
398
- name=PRED_COL,
399
- hovertemplate=f"{PRED_COL}: "+"%{x:.2f}<br>"+ylab+": %{y}<extra></extra>"
400
- ))
401
- if include_actual and act_col in df.columns:
402
- fig.add_trace(go.Scatter(
403
- x=df[act_col], y=y, mode="lines",
404
- line=dict(color=COLORS["actual"], width=2.0, dash="dot"),
405
- name=f"{act_col} (actual)",
406
- hovertemplate=f"{act_col}: "+"%{x:.2f}<br>"+ylab+": %{y}<extra></extra>"
407
- ))
408
-
409
- fig.update_layout(
410
- height=TRACK_H, width=TRACK_W, autosize=False,
411
- paper_bgcolor="#fff", plot_bgcolor="#fff",
412
- margin=dict(l=64, r=16, t=36, b=48), hovermode="closest",
413
- font=dict(size=FONT_SZ, color="#000"),
414
- legend=dict(x=0.98, y=0.05, xanchor="right", yanchor="bottom",
415
- bgcolor="rgba(255,255,255,0.75)", bordercolor="#ccc", borderwidth=1),
416
- legend_title_text=""
417
- )
418
- fig.update_xaxes(
419
- title_text=f"Min Stress ({UNITS})",
420
- title_font=dict(size=20, family=BOLD_FONT, color="#000"),
421
- tickfont=dict(size=15, family=BOLD_FONT, color="#000"),
422
- side="top", range=[xmin, xmax],
423
- ticks="outside", tickformat=",.2f", tickmode="auto", tick0=tick0,
424
- showline=True, linewidth=1.2, linecolor="#444", mirror=True,
425
- showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
426
- )
427
- fig.update_yaxes(
428
- title_text=ylab,
429
- title_font=dict(size=20, family=BOLD_FONT, color="#000"),
430
- tickfont=dict(size=15, family=BOLD_FONT, color="#000"),
431
- range=y_range, ticks="outside",
432
- showline=True, linewidth=1.2, linecolor="#444", mirror=True,
433
- showgrid=True, gridcolor="rgba(0,0,0,0.12)", automargin=True
434
- )
435
- return fig
436
-
437
- def preview_tracks(df: pd.DataFrame, cols: list[str]):
438
- cols = [c for c in cols if c in df.columns]
439
- n = len(cols)
440
- if n == 0:
441
- fig, ax = plt.subplots(figsize=(4, 2))
442
- ax.text(0.5, 0.5, "No selected columns", ha="center", va="center")
443
- ax.axis("off")
444
- return fig
445
-
446
- depth_col = next((c for c in df.columns if 'depth' in str(c).lower()), None)
447
- if depth_col is not None:
448
- idx = pd.to_numeric(df[depth_col], errors="coerce")
449
- y_label = depth_col
450
- y_min, y_max = float(np.nanmin(idx)), float(np.nanmax(idx))
451
- else:
452
- idx = pd.Series(np.arange(1, len(df) + 1))
453
- y_label = "Point Index"
454
- y_min, y_max = float(idx.min()), float(idx.max())
455
-
456
- cmap = plt.get_cmap("tab20")
457
- col_colors = {col: cmap(i % cmap.N) for i, col in enumerate(cols)}
458
-
459
- fig, axes = plt.subplots(1, n, figsize=(2.4 * n, 7.0), sharey=True, dpi=100)
460
- if n == 1:
461
- axes = [axes]
462
-
463
- for i, (ax, col) in enumerate(zip(axes, cols)):
464
- x = pd.to_numeric(df[col], errors="coerce")
465
- ax.plot(x, idx, '-', lw=1.6, color=col_colors[col])
466
- ax.set_xlabel(col); ax.xaxis.set_label_position('top'); ax.xaxis.tick_top()
467
- ax.set_ylim(y_max, y_min) # reversed depth down
468
- ax.grid(True, linestyle=":", alpha=0.3)
469
- if i == 0:
470
- ax.set_ylabel(y_label)
471
- else:
472
- ax.tick_params(labelleft=False); ax.set_ylabel("")
473
-
474
- fig.tight_layout()
475
- return fig
476
-
477
- # =========================
478
- # HF Hub loaders (primary) + local fallbacks
479
- # =========================
480
- def _hf_repo_id() -> str:
481
- # Defaults to your repo; can be overridden via secrets/env
482
- return (
483
- os.environ.get("HF_REPO_ID", "")
484
- or (st.secrets.get("HF_REPO_ID", "") if hasattr(st, "secrets") else "")
485
- or "Smart-Thinking/minstress_model"
486
- )
487
-
488
- def _hf_token() -> str:
489
- return os.environ.get("AUTH_TOKEN", "") or (st.secrets.get("AUTH_TOKEN", "") if hasattr(st, "secrets") else "")
490
-
491
- def _model_file() -> str:
492
- return (os.environ.get("MODEL_FILE", "")
493
- or (st.secrets.get("MODEL_FILE", "") if hasattr(st, "secrets") else "")
494
- or "minstress_model/minstress_model.joblib")
495
-
496
- def _meta_file() -> str:
497
- return (os.environ.get("META_FILE", "")
498
- or (st.secrets.get("META_FILE", "") if hasattr(st, "secrets") else "")
499
- or "minstress_model/minstress_meta.json")
500
-
501
- @st.cache_resource(show_spinner=False)
502
- def load_model_from_hub_or_local() -> object:
503
- repo_id = _hf_repo_id()
504
- filename = _model_file()
505
- token = _hf_token()
506
-
507
- # Try HF Hub (works with LFS/large files)
508
- try:
509
- model_path = hf_hub_download(repo_id=repo_id, filename=filename, token=token)
510
- return joblib.load(model_path)
511
- except Exception as e:
512
- st.warning(f"HF Hub model download failed ({repo_id}/{filename}): {e}. Trying local fallbacks…")
513
-
514
- # Local fallbacks in repo
515
- for p in [DEFAULT_MODEL, *MODEL_FALLBACKS]:
516
- if p.exists() and p.stat().st_size > 0:
517
- return joblib.load(str(p))
518
-
519
- # /mnt/data fallback (manual upload for quick testing)
520
- p2 = Path("/mnt/data/minstress_model.joblib")
521
- if p2.exists() and p2.stat().st_size > 0:
522
- return joblib.load(str(p2))
523
-
524
- raise FileNotFoundError("No model available via HF Hub or local fallbacks.")
525
-
526
- @st.cache_data(show_spinner=False)
527
- def load_meta_from_hub_or_local() -> dict:
528
- repo_id = _hf_repo_id()
529
- filename = _meta_file()
530
- token = _hf_token()
531
-
532
- # Try HF Hub
533
- try:
534
- meta_path = hf_hub_download(repo_id=repo_id, filename=filename, token=token)
535
- with open(meta_path, "r", encoding="utf-8") as f:
536
- return json.load(f)
537
- except Exception as e:
538
- st.warning(f"HF Hub meta download failed ({repo_id}/{filename}): {e}. Trying local fallbacks…")
539
-
540
- # Local fallbacks (repo)
541
- for p in META_CANDIDATES:
542
- if p.exists():
543
- try:
544
- return json.loads(p.read_text(encoding="utf-8"))
545
- except Exception:
546
- pass
547
-
548
- # /mnt/data fallback
549
- p2 = Path("/mnt/data/minstress_meta.json")
550
- if p2.exists():
551
- try:
552
- return json.loads(p2.read_text(encoding="utf-8"))
553
- except Exception:
554
- pass
555
-
556
- return {}
557
-
558
  # =========================
559
  # Session state
560
  # =========================
@@ -566,9 +216,12 @@ st.session_state.setdefault("dev_file_bytes",b"")
566
  st.session_state.setdefault("dev_file_loaded",False)
567
  st.session_state.setdefault("dev_preview",False)
568
  st.session_state.setdefault("show_preview_modal", False)
 
 
 
569
 
570
  # =========================
571
- # Sidebar branding
572
  # =========================
573
  st.sidebar.markdown(f"""
574
  <div class="centered-container">
@@ -578,45 +231,36 @@ st.sidebar.markdown(f"""
578
  </div>
579
  """, unsafe_allow_html=True)
580
 
581
- def sticky_header(title, message):
582
- st.markdown(
583
- f"""
584
- <style>
585
- .sticky-container {{
586
- position: sticky; top: 0; background-color: white; z-index: 100;
587
- padding-top: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;
588
- }}
589
- </style>
590
- <div class="sticky-container">
591
- <h3>{title}</h3>
592
- <p>{message}</p>
593
- </div>
594
- """,
595
- unsafe_allow_html=True
596
- )
597
 
598
- # =========================
599
- # Load model + meta
600
- # =========================
601
- try:
602
- model = load_model_from_hub_or_local()
603
- except Exception as e:
604
- st.error(
605
- "Failed to load model.\n"
606
- " Ensure HF_REPO_ID, MODEL_FILE are correct and the repo/file exists.\n"
607
- "• If the repo is private, set AUTH_TOKEN (Read).\n"
608
- "• Or upload /mnt/data/minstress_model.joblib for testing.\n\n"
609
- f"Details: {e}"
610
- )
611
- st.stop()
 
 
 
 
 
 
612
 
613
- meta = {}
614
- ALIASES = None
615
- try:
616
- meta = load_meta_from_hub_or_local()
617
- except Exception as e:
618
- st.warning(f"Could not load meta.json: {e}")
619
 
 
 
620
  if meta:
621
  FEATURES = meta.get("features", FEATURES)
622
  TARGET = meta.get("target", TARGET)
@@ -634,18 +278,46 @@ if meta:
634
  msg.append(f"scikit-learn {mv['scikit_learn']} expected, running {_skl.__version__}")
635
  if msg:
636
  st.warning("Environment mismatch: " + " | ".join(msg))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
 
638
  # =========================
639
  # INTRO
640
  # =========================
641
  if st.session_state.app_step == "intro":
642
- st.header("Welcome!")
643
- st.markdown(f"This software is developed by *Smart Thinking AI-Solutions Team* to estimate **Minimum Horizontal Stress** ({UNITS}) from drilling/offset data.")
644
- st.subheader("How It Works")
645
  st.markdown(
646
- "1) **Upload your data to build the case and preview the model performance.** \n"
647
- "2) Click **Run Model** to compute metrics and plots. \n"
648
- "3) **Proceed to Validation** (with actual) or **Proceed to Prediction** (no actual)."
649
  )
650
  if st.button("Start Showcase", type="primary"):
651
  st.session_state.app_step = "dev"; st.rerun()
@@ -660,8 +332,8 @@ def _find_sheet(book, names):
660
  return None
661
 
662
  if st.session_state.app_step == "dev":
663
- st.sidebar.header("Case Building")
664
- up = st.sidebar.file_uploader("Upload Your Data File (Train/Test sheets)", type=["xlsx","xls"])
665
  if up is not None:
666
  st.session_state.dev_file_bytes = up.getvalue()
667
  st.session_state.dev_file_name = up.name
@@ -751,13 +423,61 @@ if st.session_state.app_step == "dev":
751
  with tab1: _dev_block(st.session_state.results["Train"], st.session_state.results["m_train"])
752
  if "Test" in st.session_state.results:
753
  with tab2: _dev_block(st.session_state.results["Test"], st.session_state.results["m_test"])
754
- render_export_button(phase_key="dev")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
755
 
756
  # =========================
757
  # VALIDATION (with actual)
758
  # =========================
759
  if st.session_state.app_step == "validate":
760
- st.sidebar.header("Validate the Model")
761
  up = st.sidebar.file_uploader("Upload Validation Excel", type=["xlsx","xls"])
762
  if up is not None:
763
  book = read_book_bytes(up.getvalue())
@@ -777,11 +497,9 @@ if st.session_state.app_step == "validate":
777
  names = list(book.keys())
778
  name = next((s for s in names if s.lower() in ("validation","validate","validation2","val","val2")), names[0])
779
  df0 = _normalize_columns(book[name].copy(), FEATURES, TARGET, ALIASES)
780
-
781
  act_col = ACTUAL_COL if (ACTUAL_COL and ACTUAL_COL in df0.columns) else TARGET
782
  if not ensure_cols(df0, FEATURES+[act_col]):
783
  st.markdown('<div class="st-message-box st-error">Missing required columns.</div>', unsafe_allow_html=True); st.stop()
784
-
785
  df = df0.copy()
786
  df[PRED_COL] = _inv_transform(model.predict(_make_X(df0, FEATURES)), TRANSFORM)
787
  st.session_state.results["Validate"] = df
@@ -828,19 +546,23 @@ if st.session_state.app_step == "validate":
828
  st.session_state.results["Validate"][PRED_COL]),
829
  use_container_width=False)
830
 
831
- render_export_button(phase_key="validate")
832
-
833
- sv = st.session_state.results["sv_val"]
834
- 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)
835
- if st.session_state.results["oor_tbl"] is not None:
836
- st.write("*Out-of-range rows (vs. Training min–max):*")
837
- df_centered_rounded(st.session_state.results["oor_tbl"])
 
 
 
 
838
 
839
  # =========================
840
  # PREDICTION (no actual)
841
  # =========================
842
  if st.session_state.app_step == "predict":
843
- st.sidebar.header("Prediction (No Actual)")
844
  up = st.sidebar.file_uploader("Upload Prediction Excel", type=["xlsx","xls"])
845
  if up is not None:
846
  book = read_book_bytes(up.getvalue())
@@ -887,11 +609,10 @@ if st.session_state.app_step == "predict":
887
  })
888
  st.markdown('<div class="st-message-box st-success">Predictions ready ✓</div>', unsafe_allow_html=True)
889
  df_centered_rounded(table, hide_index=True)
890
- st.caption("**★ OOR** = %% of rows with input features outside the training min–max range.")
891
  with col_right:
892
  st.plotly_chart(track_plot(df, include_actual=False),
893
  use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
894
- render_export_button(phase_key="predict")
895
 
896
  # =========================
897
  # Preview modal
@@ -914,20 +635,33 @@ if st.session_state.show_preview_modal:
914
  df = _normalize_columns(book_to_preview[name], FEATURES, TARGET, ALIASES)
915
  t1, t2 = st.tabs(["Tracks", "Summary"])
916
  with t1:
917
- st.pyplot(preview_tracks(df, FEATURES), use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
  with t2:
919
- feat_present = [c for c in FEATURES if c in df.columns]
920
- if not feat_present:
921
  st.info("No feature columns found to summarize.")
922
  else:
923
  tbl = (
924
- df[feat_present]
925
  .agg(['min','max','mean','std'])
926
  .T.rename(columns={"min":"Min","max":"Max","mean":"Mean","std":"Std"})
927
  .reset_index(names="Feature")
928
  )
929
  df_centered_rounded(tbl)
930
-
931
  st.session_state.show_preview_modal = False
932
 
933
  # =========================
 
1
  # app.py — ST_Min_Horizontal_Stress (σhmin)
2
+ # Streamlit app that LOADS THE MODEL/META FROM USER UPLOADS (memory only; no auth, no saving).
3
+ # After the model is in memory, the rest of the workflow (Train/Test/Validate/Predict) is unchanged.
4
 
5
  import io, json, os, base64, math
6
  from pathlib import Path
 
16
  matplotlib.use("Agg")
17
  import matplotlib.pyplot as plt
18
  from matplotlib.ticker import FuncFormatter
 
19
 
20
  import plotly.graph_objects as go
21
  from sklearn.metrics import mean_squared_error
22
 
 
23
  # =========================
24
  # App constants / defaults
25
  # =========================
 
42
 
43
  STRICT_VERSION_CHECK = True
44
 
 
 
 
 
 
 
45
  # =========================
46
  # Page / CSS
47
  # =========================
 
71
  ]
72
 
73
  # =========================
74
+ # Password gate (optional)
75
  # =========================
76
  def inline_logo(path="logo.png") -> str:
77
  try:
 
82
  return ""
83
 
84
  def add_password_gate() -> None:
 
85
  try:
86
  required = st.secrets.get("APP_PASSWORD", "")
87
  except Exception:
88
  required = os.environ.get("APP_PASSWORD", "")
89
 
90
  if not required:
91
+ return # disable gate if no password set
92
+
93
  if st.session_state.get("auth_ok", False):
94
  return
95
 
 
205
  X[c] = pd.to_numeric(X[c], errors="coerce")
206
  return X
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  # =========================
209
  # Session state
210
  # =========================
 
216
  st.session_state.setdefault("dev_file_loaded",False)
217
  st.session_state.setdefault("dev_preview",False)
218
  st.session_state.setdefault("show_preview_modal", False)
219
+ st.session_state.setdefault("model_loaded", False)
220
+ st.session_state.setdefault("model_obj", None)
221
+ st.session_state.setdefault("meta_dict", {})
222
 
223
  # =========================
224
+ # Sidebar: branding + model upload
225
  # =========================
226
  st.sidebar.markdown(f"""
227
  <div class="centered-container">
 
231
  </div>
232
  """, unsafe_allow_html=True)
233
 
234
+ with st.sidebar.expander("① Load model (upload)", expanded=True):
235
+ up_model = st.file_uploader("Model file (.joblib)", type=["joblib","pkl"], key="mdl_up")
236
+ up_meta = st.file_uploader("Meta file (.json)", type=["json"], key="meta_up")
237
+ load_btn = st.button("Load model", type="primary")
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
+ if load_btn:
240
+ if not up_model:
241
+ st.error("Please upload the model .joblib file.")
242
+ st.stop()
243
+ try:
244
+ st.session_state.model_obj = joblib.load(io.BytesIO(up_model.getvalue()))
245
+ st.session_state.model_loaded = True
246
+ except Exception as e:
247
+ st.error(f"Failed to load model: {e}")
248
+ st.stop()
249
+
250
+ if up_meta:
251
+ try:
252
+ st.session_state.meta_dict = json.loads(up_meta.getvalue().decode("utf-8"))
253
+ except Exception as e:
254
+ st.warning(f"Could not parse meta.json: {e}")
255
+ st.session_state.meta_dict = {}
256
+ else:
257
+ st.warning("No meta.json uploaded — using app defaults.")
258
+ st.session_state.meta_dict = {}
259
 
260
+ st.success("Model loaded in memory ✓")
 
 
 
 
 
261
 
262
+ # Apply meta (if provided)
263
+ meta = st.session_state.meta_dict
264
  if meta:
265
  FEATURES = meta.get("features", FEATURES)
266
  TARGET = meta.get("target", TARGET)
 
278
  msg.append(f"scikit-learn {mv['scikit_learn']} expected, running {_skl.__version__}")
279
  if msg:
280
  st.warning("Environment mismatch: " + " | ".join(msg))
281
+ else:
282
+ ALIASES = None
283
+
284
+ # Guard: require model first
285
+ if not st.session_state.model_loaded:
286
+ st.header("Welcome!")
287
+ st.info("Upload your **model** (.joblib) and optional **meta.json** in the left sidebar, then click **Load model**.")
288
+ st.stop()
289
+
290
+ # Keep a short alias
291
+ model = st.session_state.model_obj
292
+
293
+ # =========================
294
+ # Sticky header helper
295
+ # =========================
296
+ def sticky_header(title, message):
297
+ st.markdown(
298
+ f"""
299
+ <style>
300
+ .sticky-container {{
301
+ position: sticky; top: 0; background-color: white; z-index: 100;
302
+ padding-top: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;
303
+ }}
304
+ </style>
305
+ <div class="sticky-container">
306
+ <h3>{title}</h3>
307
+ <p>{message}</p>
308
+ </div>
309
+ """,
310
+ unsafe_allow_html=True
311
+ )
312
 
313
  # =========================
314
  # INTRO
315
  # =========================
316
  if st.session_state.app_step == "intro":
317
+ st.header("Model ready ✓")
 
 
318
  st.markdown(
319
+ f"This software estimates **Minimum Horizontal Stress** ({UNITS}). "
320
+ "Now build a case, validate, or predict."
 
321
  )
322
  if st.button("Start Showcase", type="primary"):
323
  st.session_state.app_step = "dev"; st.rerun()
 
332
  return None
333
 
334
  if st.session_state.app_step == "dev":
335
+ st.sidebar.header("Case Building")
336
+ up = st.sidebar.file_uploader("Upload Train/Test Excel", type=["xlsx","xls"])
337
  if up is not None:
338
  st.session_state.dev_file_bytes = up.getvalue()
339
  st.session_state.dev_file_name = up.name
 
423
  with tab1: _dev_block(st.session_state.results["Train"], st.session_state.results["m_train"])
424
  if "Test" in st.session_state.results:
425
  with tab2: _dev_block(st.session_state.results["Test"], st.session_state.results["m_test"])
426
+ # Export
427
+ st.divider()
428
+ st.markdown("### Export to Excel")
429
+ options = ["Training","Training_Metrics","Training_Summary","Testing","Testing_Metrics","Testing_Summary","Info"]
430
+ selected = st.multiselect("Sheets to include", options=options, default=[])
431
+ def _summary_table(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
432
+ cols = [c for c in cols if c in df.columns]
433
+ if not cols: return pd.DataFrame()
434
+ tbl = (df[cols].agg(['min','max','mean','std'])
435
+ .T.rename(columns={"min":"Min","max":"Max","mean":"Mean","std":"Std"})
436
+ .reset_index(names="Field"))
437
+ return _round_numeric(tbl, 3)
438
+ def build_export(selected: list[str]) -> tuple[bytes|None, str|None]:
439
+ res = st.session_state.get("results", {})
440
+ if not res: return None, None
441
+ sheets, order = {}, []
442
+ def _add(n, d):
443
+ if isinstance(d, pd.DataFrame) and not d.empty: sheets[n]=_round_numeric(d,3); order.append(n)
444
+ if "Training" in selected and "Train" in res: _add("Training", res["Train"])
445
+ if "Training_Metrics" in selected and res.get("m_train"): _add("Training_Metrics", pd.DataFrame([res["m_train"]]))
446
+ if "Training_Summary" in selected and "Train" in res:
447
+ tr_cols = FEATURES + [c for c in [TARGET, PRED_COL] if c in res["Train"].columns]
448
+ _add("Training_Summary", _summary_table(res["Train"], tr_cols))
449
+ if "Testing" in selected and "Test" in res: _add("Testing", res["Test"])
450
+ if "Testing_Metrics" in selected and res.get("m_test"): _add("Testing_Metrics", pd.DataFrame([res["m_test"]]))
451
+ if "Testing_Summary" in selected and "Test" in res:
452
+ te_cols = FEATURES + [c for c in [TARGET, PRED_COL] if c in res["Test"].columns]
453
+ _add("Testing_Summary", _summary_table(res["Test"], te_cols))
454
+ if "Info" in selected:
455
+ info = pd.DataFrame([
456
+ {"Key":"AppName","Value":APP_NAME},
457
+ {"Key":"Tagline","Value":TAGLINE},
458
+ {"Key":"Target","Value":TARGET},
459
+ {"Key":"PredColumn","Value":PRED_COL},
460
+ {"Key":"Features","Value":", ".join(FEATURES)},
461
+ {"Key":"ExportedAt","Value":datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
462
+ ])
463
+ _add("Info", info)
464
+ if not order: return None, None
465
+ bio = io.BytesIO()
466
+ with pd.ExcelWriter(bio, engine=_excel_engine()) as w:
467
+ for name in order:
468
+ df = sheets[name]; df.to_excel(w, sheet_name=_excel_safe_name(name), index=False)
469
+ bio.seek(0)
470
+ return bio.getvalue(), f"MinStress_Export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
471
+ data, fname = build_export(selected)
472
+ st.download_button("⬇️ Export Excel", data=(data or b""), file_name=(fname or "MinStress_Export.xlsx"),
473
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
474
+ disabled=(data is None))
475
 
476
  # =========================
477
  # VALIDATION (with actual)
478
  # =========================
479
  if st.session_state.app_step == "validate":
480
+ st.sidebar.header("Validate the Model")
481
  up = st.sidebar.file_uploader("Upload Validation Excel", type=["xlsx","xls"])
482
  if up is not None:
483
  book = read_book_bytes(up.getvalue())
 
497
  names = list(book.keys())
498
  name = next((s for s in names if s.lower() in ("validation","validate","validation2","val","val2")), names[0])
499
  df0 = _normalize_columns(book[name].copy(), FEATURES, TARGET, ALIASES)
 
500
  act_col = ACTUAL_COL if (ACTUAL_COL and ACTUAL_COL in df0.columns) else TARGET
501
  if not ensure_cols(df0, FEATURES+[act_col]):
502
  st.markdown('<div class="st-message-box st-error">Missing required columns.</div>', unsafe_allow_html=True); st.stop()
 
503
  df = df0.copy()
504
  df[PRED_COL] = _inv_transform(model.predict(_make_X(df0, FEATURES)), TRANSFORM)
505
  st.session_state.results["Validate"] = df
 
546
  st.session_state.results["Validate"][PRED_COL]),
547
  use_container_width=False)
548
 
549
+ # Export button
550
+ st.divider()
551
+ val_tbl = st.session_state.results["Validate"]
552
+ bio = io.BytesIO()
553
+ with pd.ExcelWriter(bio, engine=_excel_engine()) as w:
554
+ val_tbl.to_excel(w, sheet_name="Validation", index=False)
555
+ pd.DataFrame([m]).to_excel(w, sheet_name="Validation_Metrics", index=False)
556
+ bio.seek(0)
557
+ st.download_button("⬇️ Export Excel", data=bio.getvalue(),
558
+ file_name=f"Validation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
559
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
560
 
561
  # =========================
562
  # PREDICTION (no actual)
563
  # =========================
564
  if st.session_state.app_step == "predict":
565
+ st.sidebar.header("Prediction (No Actual)")
566
  up = st.sidebar.file_uploader("Upload Prediction Excel", type=["xlsx","xls"])
567
  if up is not None:
568
  book = read_book_bytes(up.getvalue())
 
609
  })
610
  st.markdown('<div class="st-message-box st-success">Predictions ready ✓</div>', unsafe_allow_html=True)
611
  df_centered_rounded(table, hide_index=True)
612
+ st.caption("**★ OOR** = % of rows with input features outside the training min–max range.")
613
  with col_right:
614
  st.plotly_chart(track_plot(df, include_actual=False),
615
  use_container_width=False, config={"displayModeBar": False, "scrollZoom": True})
 
616
 
617
  # =========================
618
  # Preview modal
 
635
  df = _normalize_columns(book_to_preview[name], FEATURES, TARGET, ALIASES)
636
  t1, t2 = st.tabs(["Tracks", "Summary"])
637
  with t1:
638
+ # small quick-look plot of the features
639
+ cols = [c for c in FEATURES if c in df.columns]
640
+ if not cols:
641
+ st.info("No feature columns to preview.")
642
+ else:
643
+ idx = np.arange(1, len(df)+1)
644
+ fig, axes = plt.subplots(1, len(cols), figsize=(2.4*len(cols), 7.0), sharey=True, dpi=100)
645
+ if len(cols)==1: axes=[axes]
646
+ for ax, col in zip(axes, cols):
647
+ x = pd.to_numeric(df[col], errors="coerce")
648
+ ax.plot(x, idx, '-', lw=1.6)
649
+ ax.set_xlabel(col); ax.xaxis.set_label_position('top'); ax.xaxis.tick_top()
650
+ ax.set_ylim(idx.max(), idx.min()); ax.grid(True, linestyle=":", alpha=0.3)
651
+ fig.tight_layout()
652
+ st.pyplot(fig, use_container_width=True)
653
  with t2:
654
+ cols = [c for c in FEATURES if c in df.columns]
655
+ if not cols:
656
  st.info("No feature columns found to summarize.")
657
  else:
658
  tbl = (
659
+ df[cols]
660
  .agg(['min','max','mean','std'])
661
  .T.rename(columns={"min":"Min","max":"Max","mean":"Mean","std":"Std"})
662
  .reset_index(names="Feature")
663
  )
664
  df_centered_rounded(tbl)
 
665
  st.session_state.show_preview_modal = False
666
 
667
  # =========================