Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -26,49 +26,33 @@ MODEL_FALLBACKS = [MODELS_DIR / "model.joblib", MODELS_DIR / "model.pkl"]
|
|
| 26 |
COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
|
| 27 |
|
| 28 |
# =========================
|
| 29 |
-
# Page / Theme
|
| 30 |
# =========================
|
| 31 |
st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
|
| 32 |
-
|
| 33 |
-
#
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
.stButton>button:hover{ background:#0056b3; }
|
| 45 |
-
.st-hero { display:flex; align-items:center; gap:16px; padding-top: 4px; }
|
| 46 |
-
.st-hero .brand { width:110px; height:110px; object-fit:contain; }
|
| 47 |
-
.st-hero h1 { margin:0; line-height:1.05; }
|
| 48 |
-
.st-hero .tagline { margin:2px 0 0 2px; color:#6b7280; font-size:1.05rem; font-style:italic; }
|
| 49 |
-
[data-testid="stBlock"]{ margin-top:0 !important; }
|
| 50 |
-
</style>
|
| 51 |
-
""",
|
| 52 |
-
unsafe_allow_html=True
|
| 53 |
-
)
|
| 54 |
-
# =========================
|
| 55 |
-
# Password
|
| 56 |
-
# =========================
|
| 57 |
-
# --- Password gate (branded, strict) ---
|
| 58 |
def add_password_gate() -> bool:
|
| 59 |
"""
|
| 60 |
Shows a branded access screen until the correct password is entered.
|
| 61 |
-
|
| 62 |
-
Blocks (shows admin instructions) if APP_PASSWORD is not configured.
|
| 63 |
"""
|
| 64 |
-
# 1) Read password
|
| 65 |
-
required = ""
|
| 66 |
try:
|
| 67 |
required = st.secrets.get("APP_PASSWORD", "")
|
| 68 |
except Exception:
|
| 69 |
required = os.environ.get("APP_PASSWORD", "")
|
| 70 |
|
| 71 |
-
# 2) If not configured,
|
| 72 |
if not required:
|
| 73 |
st.markdown(
|
| 74 |
f"""
|
|
@@ -81,8 +65,8 @@ def add_password_gate() -> bool:
|
|
| 81 |
</div>
|
| 82 |
<div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected Area</div>
|
| 83 |
<div style="color:#6b7280;margin-bottom:14px;">
|
| 84 |
-
Admin action required: set <code>APP_PASSWORD</code> in <b>Settings → Secrets</b>
|
| 85 |
-
environment variable) and restart the Space.
|
| 86 |
</div>
|
| 87 |
""",
|
| 88 |
unsafe_allow_html=True,
|
|
@@ -93,7 +77,7 @@ def add_password_gate() -> bool:
|
|
| 93 |
if st.session_state.get("auth_ok", False):
|
| 94 |
return True
|
| 95 |
|
| 96 |
-
# 4)
|
| 97 |
st.markdown(
|
| 98 |
f"""
|
| 99 |
<div style="display:flex;align-items:center;gap:14px;margin:8px 0 6px 0;">
|
|
@@ -110,7 +94,6 @@ def add_password_gate() -> bool:
|
|
| 110 |
""",
|
| 111 |
unsafe_allow_html=True
|
| 112 |
)
|
| 113 |
-
|
| 114 |
pwd = st.text_input("Access key", type="password", placeholder="••••••••")
|
| 115 |
col1, _ = st.columns([1, 3])
|
| 116 |
with col1:
|
|
@@ -120,12 +103,33 @@ def add_password_gate() -> bool:
|
|
| 120 |
st.rerun()
|
| 121 |
else:
|
| 122 |
st.error("Incorrect key. Please try again.")
|
| 123 |
-
|
| 124 |
-
# Don’t proceed until correct
|
| 125 |
st.stop()
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
# =========================
|
| 128 |
-
# Helpers
|
| 129 |
# =========================
|
| 130 |
try:
|
| 131 |
dialog = st.dialog
|
|
@@ -173,7 +177,7 @@ def find_sheet(book, names):
|
|
| 173 |
if nm.lower() in low2orig: return low2orig[nm.lower()]
|
| 174 |
return None
|
| 175 |
|
| 176 |
-
# ---------- Interactive plotting
|
| 177 |
def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
|
| 178 |
"""Interactive cross-plot: blue points, dashed 1:1, equal axes, no title, numeric ticks, full box outline."""
|
| 179 |
a = pd.Series(actual).astype(float)
|
|
@@ -184,28 +188,22 @@ def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
|
|
| 184 |
x0, x1 = lo - pad, hi + pad
|
| 185 |
|
| 186 |
fig = go.Figure()
|
| 187 |
-
|
| 188 |
-
# Points (blue)
|
| 189 |
fig.add_trace(go.Scatter(
|
| 190 |
x=a, y=p, mode="markers",
|
| 191 |
marker=dict(size=6, color=COLORS["pred"]),
|
| 192 |
hovertemplate="Actual: %{x:.2f}<br>Pred: %{y:.2f}<extra></extra>",
|
| 193 |
showlegend=False
|
| 194 |
))
|
| 195 |
-
|
| 196 |
-
# 1:1 line (dashed grey)
|
| 197 |
fig.add_trace(go.Scatter(
|
| 198 |
x=[x0, x1], y=[x0, x1], mode="lines",
|
| 199 |
line=dict(color=COLORS["ref"], width=1.2, dash="dash"),
|
| 200 |
hoverinfo="skip", showlegend=False
|
| 201 |
))
|
| 202 |
-
|
| 203 |
fig.update_layout(
|
| 204 |
paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
|
| 205 |
margin=dict(l=50, r=10, t=10, b=36),
|
| 206 |
hovermode="closest", font=dict(size=13)
|
| 207 |
)
|
| 208 |
-
# FULL OUTLINE via mirror=True (top/right) + showline
|
| 209 |
fig.update_xaxes(
|
| 210 |
title_text="<b>Actual UCS</b>",
|
| 211 |
range=[x0, x1], ticks="outside",
|
|
@@ -221,8 +219,7 @@ def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
|
|
| 221 |
tickformat=",.0f", scaleanchor="x", scaleratio=1,
|
| 222 |
automargin=True
|
| 223 |
)
|
| 224 |
-
|
| 225 |
-
fig.update_layout(width=w, height=h)
|
| 226 |
return fig
|
| 227 |
|
| 228 |
def depth_or_index_track_interactive(df, title=None, include_actual=True):
|
|
@@ -234,15 +231,12 @@ def depth_or_index_track_interactive(df, title=None, include_actual=True):
|
|
| 234 |
y = np.arange(1, len(df) + 1); y_label = "Point Index"
|
| 235 |
|
| 236 |
fig = go.Figure()
|
| 237 |
-
|
| 238 |
-
# Predicted (solid blue)
|
| 239 |
fig.add_trace(go.Scatter(
|
| 240 |
x=df["UCS_Pred"], y=y, mode="lines",
|
| 241 |
line=dict(color=COLORS["pred"], width=1.8),
|
| 242 |
name="UCS_Pred",
|
| 243 |
hovertemplate="UCS_Pred: %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
|
| 244 |
))
|
| 245 |
-
# Actual (dotted yellow)
|
| 246 |
if include_actual and TARGET in df.columns:
|
| 247 |
fig.add_trace(go.Scatter(
|
| 248 |
x=df[TARGET], y=y, mode="lines",
|
|
@@ -250,7 +244,6 @@ def depth_or_index_track_interactive(df, title=None, include_actual=True):
|
|
| 250 |
name="UCS (actual)",
|
| 251 |
hovertemplate="UCS (actual): %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
|
| 252 |
))
|
| 253 |
-
|
| 254 |
fig.update_layout(
|
| 255 |
paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
|
| 256 |
margin=dict(l=60, r=10, t=10, b=36),
|
|
@@ -263,7 +256,6 @@ def depth_or_index_track_interactive(df, title=None, include_actual=True):
|
|
| 263 |
width=int(3.1 * 100),
|
| 264 |
height=int((7.6 if depth_col is not None else 7.2) * 100),
|
| 265 |
)
|
| 266 |
-
# FULL OUTLINE via mirror=True (adds bottom/right sides)
|
| 267 |
fig.update_xaxes(
|
| 268 |
title_text="<b>UCS</b>", side="top",
|
| 269 |
ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror=True,
|
|
@@ -340,27 +332,29 @@ def preview_modal_val(book: dict[str, pd.DataFrame], feature_cols: list[str]):
|
|
| 340 |
# =========================
|
| 341 |
# Model presence
|
| 342 |
# =========================
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
def ensure_model_present() -> Path:
|
| 346 |
-
for p in [DEFAULT_MODEL, *MODEL_FALLBACKS]:
|
| 347 |
-
if p.exists() and p.stat().st_size > 0:
|
| 348 |
-
return p
|
| 349 |
-
if not MODEL_URL:
|
| 350 |
-
return None
|
| 351 |
try:
|
| 352 |
import requests
|
| 353 |
-
|
| 354 |
with st.status("Downloading model…", expanded=False):
|
| 355 |
-
with requests.get(
|
| 356 |
r.raise_for_status()
|
| 357 |
-
with open(
|
| 358 |
-
for chunk in r.iter_content(chunk_size=1<<20):
|
| 359 |
if chunk: f.write(chunk)
|
| 360 |
-
return
|
| 361 |
except Exception as e:
|
| 362 |
st.error(f"Failed to download model from MODEL_URL: {e}")
|
| 363 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
model_path = ensure_model_present()
|
| 366 |
if not model_path:
|
|
@@ -379,19 +373,22 @@ if meta_path.exists():
|
|
| 379 |
try:
|
| 380 |
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
| 381 |
FEATURES = meta.get("features", FEATURES); TARGET = meta.get("target", TARGET)
|
| 382 |
-
except Exception:
|
|
|
|
| 383 |
else:
|
| 384 |
def infer_features_from_model(m):
|
| 385 |
try:
|
| 386 |
if hasattr(m, "feature_names_in_") and len(getattr(m, "feature_names_in_")):
|
| 387 |
return [str(x) for x in m.feature_names_in_]
|
| 388 |
-
except Exception:
|
|
|
|
| 389 |
try:
|
| 390 |
if hasattr(m, "steps") and len(m.steps):
|
| 391 |
last = m.steps[-1][1]
|
| 392 |
if hasattr(last, "feature_names_in_") and len(last.feature_names_in_):
|
| 393 |
return [str(x) for x in last.feature_names_in_]
|
| 394 |
-
except Exception:
|
|
|
|
| 395 |
return None
|
| 396 |
infer = infer_features_from_model(model)
|
| 397 |
if infer: FEATURES = infer
|
|
@@ -418,16 +415,8 @@ for k, v in {
|
|
| 418 |
if k not in st.session_state: st.session_state[k] = v
|
| 419 |
|
| 420 |
# =========================
|
| 421 |
-
# Hero header
|
| 422 |
# =========================
|
| 423 |
-
def inline_logo(path="logo.png") -> str:
|
| 424 |
-
try:
|
| 425 |
-
p = Path(path)
|
| 426 |
-
if not p.exists(): return ""
|
| 427 |
-
return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
|
| 428 |
-
except Exception:
|
| 429 |
-
return ""
|
| 430 |
-
|
| 431 |
st.markdown(
|
| 432 |
f"""
|
| 433 |
<div class="st-hero">
|
|
@@ -446,9 +435,7 @@ st.markdown(
|
|
| 446 |
# =========================
|
| 447 |
if st.session_state.app_step == "intro":
|
| 448 |
st.header("Welcome!")
|
| 449 |
-
st.markdown(
|
| 450 |
-
"This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data."
|
| 451 |
-
)
|
| 452 |
st.subheader("Expected Input Features (in Order)")
|
| 453 |
st.markdown(
|
| 454 |
"- Q, gpm — Flow rate (gallons per minute) \n"
|
|
@@ -468,18 +455,17 @@ if st.session_state.app_step == "intro":
|
|
| 468 |
st.session_state.app_step = "dev"; st.rerun()
|
| 469 |
|
| 470 |
# =========================
|
| 471 |
-
#
|
| 472 |
# =========================
|
| 473 |
if st.session_state.app_step == "dev":
|
| 474 |
-
st.sidebar.header("
|
| 475 |
dev_label = "Upload Data (Excel)" if not st.session_state.dev_file_name else "Replace data (Excel)"
|
| 476 |
train_test_file = st.sidebar.file_uploader(dev_label, type=["xlsx","xls"], key="dev_upload")
|
| 477 |
|
| 478 |
# Detect new/changed file and PERSIST BYTES
|
| 479 |
if train_test_file is not None:
|
| 480 |
try:
|
| 481 |
-
file_bytes = train_test_file.getvalue()
|
| 482 |
-
size = len(file_bytes)
|
| 483 |
except Exception:
|
| 484 |
file_bytes = b""; size = 0
|
| 485 |
sig = (train_test_file.name, size)
|
|
@@ -497,7 +483,7 @@ if st.session_state.app_step == "dev":
|
|
| 497 |
st.session_state.dev_previewed = False
|
| 498 |
st.session_state.dev_ready = False
|
| 499 |
|
| 500 |
-
# Sidebar caption
|
| 501 |
if st.session_state.dev_file_loaded:
|
| 502 |
st.sidebar.caption(
|
| 503 |
f"**Data loaded:** {st.session_state.dev_file_name} • "
|
|
@@ -522,7 +508,7 @@ if st.session_state.app_step == "dev":
|
|
| 522 |
# ----- ALWAYS-ON TOP: Title + helper -----
|
| 523 |
helper_top = st.container()
|
| 524 |
with helper_top:
|
| 525 |
-
st.subheader("
|
| 526 |
if st.session_state.dev_ready:
|
| 527 |
st.success("Case has been built and results are displayed below.")
|
| 528 |
elif st.session_state.dev_file_loaded and st.session_state.dev_previewed:
|
|
@@ -704,7 +690,7 @@ if st.session_state.app_step == "predict":
|
|
| 704 |
if sv["oor_pct"] > 0:
|
| 705 |
st.warning("Some validation inputs fall outside the **training min–max** ranges. Interpret predictions with caution.")
|
| 706 |
|
| 707 |
-
#
|
| 708 |
metrics_val = st.session_state.results.get("metrics_val")
|
| 709 |
if metrics_val is not None:
|
| 710 |
c1, c2, c3 = st.columns(3)
|
|
@@ -712,11 +698,11 @@ if st.session_state.app_step == "predict":
|
|
| 712 |
c2.metric("RMSE", f"{metrics_val['RMSE']:.4f}")
|
| 713 |
c3.metric("MAE", f"{metrics_val['MAE']:.4f}")
|
| 714 |
else:
|
| 715 |
-
|
| 716 |
-
c1,
|
| 717 |
-
c1.metric("# points", f"{sv['n_points']}")
|
| 718 |
c2.metric("Pred min", f"{sv['pred_min']:.2f}")
|
| 719 |
c3.metric("Pred max", f"{sv['pred_max']:.2f}")
|
|
|
|
| 720 |
|
| 721 |
left, right = st.columns([0.9, 0.55])
|
| 722 |
with left:
|
|
@@ -780,4 +766,4 @@ st.markdown(
|
|
| 780 |
</div>
|
| 781 |
""",
|
| 782 |
unsafe_allow_html=True
|
| 783 |
-
)
|
|
|
|
| 26 |
COLORS = {"pred": "#1f77b4", "actual": "#f2b702", "ref": "#5a5a5a"}
|
| 27 |
|
| 28 |
# =========================
|
| 29 |
+
# Page / Theme (must be first)
|
| 30 |
# =========================
|
| 31 |
st.set_page_config(page_title="ST_GeoMech_UCS", page_icon="logo.png", layout="wide")
|
| 32 |
+
|
| 33 |
+
# ---------- utilities used by the gate ----------
|
| 34 |
+
def inline_logo(path="logo.png") -> str:
|
| 35 |
+
try:
|
| 36 |
+
p = Path(path)
|
| 37 |
+
if p.exists():
|
| 38 |
+
return f"data:image/png;base64,{base64.b64encode(p.read_bytes()).decode('ascii')}"
|
| 39 |
+
except Exception:
|
| 40 |
+
pass
|
| 41 |
+
return ""
|
| 42 |
+
|
| 43 |
+
# ---------- Password gate (define BEFORE calling) ----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
def add_password_gate() -> bool:
|
| 45 |
"""
|
| 46 |
Shows a branded access screen until the correct password is entered.
|
| 47 |
+
Set the password through Settings → Secrets as APP_PASSWORD (or an env var).
|
|
|
|
| 48 |
"""
|
| 49 |
+
# 1) Read required password
|
|
|
|
| 50 |
try:
|
| 51 |
required = st.secrets.get("APP_PASSWORD", "")
|
| 52 |
except Exception:
|
| 53 |
required = os.environ.get("APP_PASSWORD", "")
|
| 54 |
|
| 55 |
+
# 2) If not configured, block with an admin hint
|
| 56 |
if not required:
|
| 57 |
st.markdown(
|
| 58 |
f"""
|
|
|
|
| 65 |
</div>
|
| 66 |
<div style="font-size:1.25rem;font-weight:700;margin:8px 0 4px 0;">Protected Area</div>
|
| 67 |
<div style="color:#6b7280;margin-bottom:14px;">
|
| 68 |
+
Admin action required: set <code>APP_PASSWORD</code> in <b>Settings → Secrets</b>
|
| 69 |
+
(or as an environment variable) and restart the Space.
|
| 70 |
</div>
|
| 71 |
""",
|
| 72 |
unsafe_allow_html=True,
|
|
|
|
| 77 |
if st.session_state.get("auth_ok", False):
|
| 78 |
return True
|
| 79 |
|
| 80 |
+
# 4) Prompt
|
| 81 |
st.markdown(
|
| 82 |
f"""
|
| 83 |
<div style="display:flex;align-items:center;gap:14px;margin:8px 0 6px 0;">
|
|
|
|
| 94 |
""",
|
| 95 |
unsafe_allow_html=True
|
| 96 |
)
|
|
|
|
| 97 |
pwd = st.text_input("Access key", type="password", placeholder="••••••••")
|
| 98 |
col1, _ = st.columns([1, 3])
|
| 99 |
with col1:
|
|
|
|
| 103 |
st.rerun()
|
| 104 |
else:
|
| 105 |
st.error("Incorrect key. Please try again.")
|
|
|
|
|
|
|
| 106 |
st.stop()
|
| 107 |
|
| 108 |
+
# 🔒 Enforce gate BEFORE any UI is rendered
|
| 109 |
+
add_password_gate()
|
| 110 |
+
|
| 111 |
+
# After the gate: global CSS/UI may render
|
| 112 |
+
st.markdown("<style>header, footer{visibility:hidden !important;}</style>", unsafe_allow_html=True)
|
| 113 |
+
st.markdown(
|
| 114 |
+
"""
|
| 115 |
+
<style>
|
| 116 |
+
.stApp { background: #FFFFFF; }
|
| 117 |
+
section[data-testid="stSidebar"] { background: #F6F9FC; }
|
| 118 |
+
.block-container { padding-top: .5rem; padding-bottom: .5rem; }
|
| 119 |
+
.stButton>button{ background:#007bff; color:#fff; font-weight:bold; border-radius:8px; border:none; padding:10px 24px; }
|
| 120 |
+
.stButton>button:hover{ background:#0056b3; }
|
| 121 |
+
.st-hero { display:flex; align-items:center; gap:16px; padding-top: 4px; }
|
| 122 |
+
.st-hero .brand { width:110px; height:110px; object-fit:contain; }
|
| 123 |
+
.st-hero h1 { margin:0; line-height:1.05; }
|
| 124 |
+
.st-hero .tagline { margin:2px 0 0 2px; color:#6b7280; font-size:1.05rem; font-style:italic; }
|
| 125 |
+
[data-testid="stBlock"]{ margin-top:0 !important; }
|
| 126 |
+
</style>
|
| 127 |
+
""",
|
| 128 |
+
unsafe_allow_html=True
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
# =========================
|
| 132 |
+
# Helpers / caching
|
| 133 |
# =========================
|
| 134 |
try:
|
| 135 |
dialog = st.dialog
|
|
|
|
| 177 |
if nm.lower() in low2orig: return low2orig[nm.lower()]
|
| 178 |
return None
|
| 179 |
|
| 180 |
+
# ---------- Interactive plotting ----------
|
| 181 |
def cross_plot_interactive(actual, pred, size=(3.9, 3.9)):
|
| 182 |
"""Interactive cross-plot: blue points, dashed 1:1, equal axes, no title, numeric ticks, full box outline."""
|
| 183 |
a = pd.Series(actual).astype(float)
|
|
|
|
| 188 |
x0, x1 = lo - pad, hi + pad
|
| 189 |
|
| 190 |
fig = go.Figure()
|
|
|
|
|
|
|
| 191 |
fig.add_trace(go.Scatter(
|
| 192 |
x=a, y=p, mode="markers",
|
| 193 |
marker=dict(size=6, color=COLORS["pred"]),
|
| 194 |
hovertemplate="Actual: %{x:.2f}<br>Pred: %{y:.2f}<extra></extra>",
|
| 195 |
showlegend=False
|
| 196 |
))
|
|
|
|
|
|
|
| 197 |
fig.add_trace(go.Scatter(
|
| 198 |
x=[x0, x1], y=[x0, x1], mode="lines",
|
| 199 |
line=dict(color=COLORS["ref"], width=1.2, dash="dash"),
|
| 200 |
hoverinfo="skip", showlegend=False
|
| 201 |
))
|
|
|
|
| 202 |
fig.update_layout(
|
| 203 |
paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
|
| 204 |
margin=dict(l=50, r=10, t=10, b=36),
|
| 205 |
hovermode="closest", font=dict(size=13)
|
| 206 |
)
|
|
|
|
| 207 |
fig.update_xaxes(
|
| 208 |
title_text="<b>Actual UCS</b>",
|
| 209 |
range=[x0, x1], ticks="outside",
|
|
|
|
| 219 |
tickformat=",.0f", scaleanchor="x", scaleratio=1,
|
| 220 |
automargin=True
|
| 221 |
)
|
| 222 |
+
fig.update_layout(width=int(size[0]*100), height=int(size[1]*100))
|
|
|
|
| 223 |
return fig
|
| 224 |
|
| 225 |
def depth_or_index_track_interactive(df, title=None, include_actual=True):
|
|
|
|
| 231 |
y = np.arange(1, len(df) + 1); y_label = "Point Index"
|
| 232 |
|
| 233 |
fig = go.Figure()
|
|
|
|
|
|
|
| 234 |
fig.add_trace(go.Scatter(
|
| 235 |
x=df["UCS_Pred"], y=y, mode="lines",
|
| 236 |
line=dict(color=COLORS["pred"], width=1.8),
|
| 237 |
name="UCS_Pred",
|
| 238 |
hovertemplate="UCS_Pred: %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
|
| 239 |
))
|
|
|
|
| 240 |
if include_actual and TARGET in df.columns:
|
| 241 |
fig.add_trace(go.Scatter(
|
| 242 |
x=df[TARGET], y=y, mode="lines",
|
|
|
|
| 244 |
name="UCS (actual)",
|
| 245 |
hovertemplate="UCS (actual): %{x:.2f}<br>"+y_label+": %{y}<extra></extra>"
|
| 246 |
))
|
|
|
|
| 247 |
fig.update_layout(
|
| 248 |
paper_bgcolor="#ffffff", plot_bgcolor="#ffffff",
|
| 249 |
margin=dict(l=60, r=10, t=10, b=36),
|
|
|
|
| 256 |
width=int(3.1 * 100),
|
| 257 |
height=int((7.6 if depth_col is not None else 7.2) * 100),
|
| 258 |
)
|
|
|
|
| 259 |
fig.update_xaxes(
|
| 260 |
title_text="<b>UCS</b>", side="top",
|
| 261 |
ticks="outside", showline=True, linewidth=1.2, linecolor="#444", mirror=True,
|
|
|
|
| 332 |
# =========================
|
| 333 |
# Model presence
|
| 334 |
# =========================
|
| 335 |
+
def _download_model(url: str, dest: Path) -> bool:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
try:
|
| 337 |
import requests
|
| 338 |
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
| 339 |
with st.status("Downloading model…", expanded=False):
|
| 340 |
+
with requests.get(url, stream=True, timeout=30) as r:
|
| 341 |
r.raise_for_status()
|
| 342 |
+
with open(dest, "wb") as f:
|
| 343 |
+
for chunk in r.iter_content(chunk_size=1 << 20):
|
| 344 |
if chunk: f.write(chunk)
|
| 345 |
+
return True
|
| 346 |
except Exception as e:
|
| 347 |
st.error(f"Failed to download model from MODEL_URL: {e}")
|
| 348 |
+
return False
|
| 349 |
+
|
| 350 |
+
MODEL_URL = _get_model_url()
|
| 351 |
+
def ensure_model_present() -> Path | None:
|
| 352 |
+
for p in [DEFAULT_MODEL, *MODEL_FALLBACKS]:
|
| 353 |
+
if p.exists() and p.stat().st_size > 0:
|
| 354 |
+
return p
|
| 355 |
+
if MODEL_URL and _download_model(MODEL_URL, DEFAULT_MODEL):
|
| 356 |
+
return DEFAULT_MODEL
|
| 357 |
+
return None
|
| 358 |
|
| 359 |
model_path = ensure_model_present()
|
| 360 |
if not model_path:
|
|
|
|
| 373 |
try:
|
| 374 |
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
| 375 |
FEATURES = meta.get("features", FEATURES); TARGET = meta.get("target", TARGET)
|
| 376 |
+
except Exception:
|
| 377 |
+
pass
|
| 378 |
else:
|
| 379 |
def infer_features_from_model(m):
|
| 380 |
try:
|
| 381 |
if hasattr(m, "feature_names_in_") and len(getattr(m, "feature_names_in_")):
|
| 382 |
return [str(x) for x in m.feature_names_in_]
|
| 383 |
+
except Exception:
|
| 384 |
+
pass
|
| 385 |
try:
|
| 386 |
if hasattr(m, "steps") and len(m.steps):
|
| 387 |
last = m.steps[-1][1]
|
| 388 |
if hasattr(last, "feature_names_in_") and len(last.feature_names_in_):
|
| 389 |
return [str(x) for x in last.feature_names_in_]
|
| 390 |
+
except Exception:
|
| 391 |
+
pass
|
| 392 |
return None
|
| 393 |
infer = infer_features_from_model(model)
|
| 394 |
if infer: FEATURES = infer
|
|
|
|
| 415 |
if k not in st.session_state: st.session_state[k] = v
|
| 416 |
|
| 417 |
# =========================
|
| 418 |
+
# Hero header (after gate)
|
| 419 |
# =========================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
st.markdown(
|
| 421 |
f"""
|
| 422 |
<div class="st-hero">
|
|
|
|
| 435 |
# =========================
|
| 436 |
if st.session_state.app_step == "intro":
|
| 437 |
st.header("Welcome!")
|
| 438 |
+
st.markdown("This software is developed by *Smart Thinking AI-Solutions Team* to estimate UCS from drilling data.")
|
|
|
|
|
|
|
| 439 |
st.subheader("Expected Input Features (in Order)")
|
| 440 |
st.markdown(
|
| 441 |
"- Q, gpm — Flow rate (gallons per minute) \n"
|
|
|
|
| 455 |
st.session_state.app_step = "dev"; st.rerun()
|
| 456 |
|
| 457 |
# =========================
|
| 458 |
+
# Case Building
|
| 459 |
# =========================
|
| 460 |
if st.session_state.app_step == "dev":
|
| 461 |
+
st.sidebar.header("Data for Case Building")
|
| 462 |
dev_label = "Upload Data (Excel)" if not st.session_state.dev_file_name else "Replace data (Excel)"
|
| 463 |
train_test_file = st.sidebar.file_uploader(dev_label, type=["xlsx","xls"], key="dev_upload")
|
| 464 |
|
| 465 |
# Detect new/changed file and PERSIST BYTES
|
| 466 |
if train_test_file is not None:
|
| 467 |
try:
|
| 468 |
+
file_bytes = train_test_file.getvalue(); size = len(file_bytes)
|
|
|
|
| 469 |
except Exception:
|
| 470 |
file_bytes = b""; size = 0
|
| 471 |
sig = (train_test_file.name, size)
|
|
|
|
| 483 |
st.session_state.dev_previewed = False
|
| 484 |
st.session_state.dev_ready = False
|
| 485 |
|
| 486 |
+
# Sidebar caption
|
| 487 |
if st.session_state.dev_file_loaded:
|
| 488 |
st.sidebar.caption(
|
| 489 |
f"**Data loaded:** {st.session_state.dev_file_name} • "
|
|
|
|
| 508 |
# ----- ALWAYS-ON TOP: Title + helper -----
|
| 509 |
helper_top = st.container()
|
| 510 |
with helper_top:
|
| 511 |
+
st.subheader("Case Building")
|
| 512 |
if st.session_state.dev_ready:
|
| 513 |
st.success("Case has been built and results are displayed below.")
|
| 514 |
elif st.session_state.dev_file_loaded and st.session_state.dev_previewed:
|
|
|
|
| 690 |
if sv["oor_pct"] > 0:
|
| 691 |
st.warning("Some validation inputs fall outside the **training min–max** ranges. Interpret predictions with caution.")
|
| 692 |
|
| 693 |
+
# Prefer dev-like metrics when actual UCS exists
|
| 694 |
metrics_val = st.session_state.results.get("metrics_val")
|
| 695 |
if metrics_val is not None:
|
| 696 |
c1, c2, c3 = st.columns(3)
|
|
|
|
| 698 |
c2.metric("RMSE", f"{metrics_val['RMSE']:.4f}")
|
| 699 |
c3.metric("MAE", f"{metrics_val['MAE']:.4f}")
|
| 700 |
else:
|
| 701 |
+
c1, c2, c3, c4 = st.columns(4)
|
| 702 |
+
c1.metric("points", f"{sv['n_points']}")
|
|
|
|
| 703 |
c2.metric("Pred min", f"{sv['pred_min']:.2f}")
|
| 704 |
c3.metric("Pred max", f"{sv['pred_max']:.2f}")
|
| 705 |
+
c4.metric("OOR %", f"{sv['oor_pct']:.1f}%")
|
| 706 |
|
| 707 |
left, right = st.columns([0.9, 0.55])
|
| 708 |
with left:
|
|
|
|
| 766 |
</div>
|
| 767 |
""",
|
| 768 |
unsafe_allow_html=True
|
| 769 |
+
)
|