File size: 8,650 Bytes
fca8237 694b174 fca8237 694b174 fca8237 fa6ccff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | """DBFT Pavement Strain Predictor — Gradio app for Hugging Face Spaces.
Predicts the two critical pavement strains directly from an FWD deflection
basin + layer thicknesses, using the best model from the paper (combined
loss lambda_f = 1.0, extended surrogate), with a local SHAP explanation
for every prediction.
"""
from pathlib import Path
try:
import spaces # ZeroGPU: must be imported before torch
except ImportError: # local run / CPU Space without the spaces package
spaces = None
import gradio as gr
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import shap
import torch
from fwd_fusion_transformer import DBFT, basin_indices
HERE = Path(__file__).resolve().parent
ASSETS = HERE / "assets"
TAG = "combined_lam1.0_ext"
FEATURES = ["D0", "D200", "D300", "D450", "D600", "D900", "D1200", "D1500",
"D1800", "h_AC", "h_Base", "h_Subbase"]
NSAMPLES = 200
# ---------------- model ----------------
sc = np.load(ASSETS / f"dbft_{TAG}_scalers.npz")
model = DBFT()
model.load_state_dict(torch.load(ASSETS / f"dbft_{TAG}.pt", map_location="cpu"))
model.eval()
def f(X):
"""X: (n, 12) raw [D0..D1800 um, h_AC..h_Subbase mm] -> (n, 2) strains ue."""
X = np.asarray(X, np.float32)
D, H = X[:, :9], X[:, 9:12]
I = basin_indices(D).astype(np.float32)
t = lambda a, k: torch.tensor((a - sc[f"{k}_mean"]) / sc[f"{k}_std"],
dtype=torch.float32)
with torch.no_grad():
p = model(t(D, "D"), t(I, "I"), t(H, "H")).numpy()
return p * sc["Y_std"] + sc["Y_mean"]
bg = np.load(ASSETS / "bg_cache.npz")["bg"]
explainer = shap.KernelExplainer(f, bg)
BASE = np.asarray(explainer.expected_value, float)
RED, BLUE, GREEN, AMBER = "#d64545", "#3b7dd8", "#10a37f", "#d69e2e"
def severity(v, lo, hi):
return (("low", GREEN) if v < lo else
("moderate", AMBER) if v < hi else ("high", RED))
def shap_figure(sv_ac, sv_sg, pred):
"""Two-panel horizontal bar chart of local SHAP values (ue)."""
fig, axes = plt.subplots(1, 2, figsize=(11, 4.2), dpi=140)
titles = [(r"$\varepsilon_t$ — AC tensile", sv_ac, BASE[0], pred[0]),
(r"$\varepsilon_c$ — subgrade compressive", sv_sg, BASE[1],
pred[1])]
for ax, (title, sv, base, p) in zip(axes, titles):
order = np.argsort(np.abs(sv))
names = [FEATURES[i] for i in order]
vals = sv[order]
colors = [RED if v >= 0 else BLUE for v in vals]
ax.barh(range(len(vals)), vals, color=colors, alpha=0.85)
ax.axvline(0, color="#999", lw=1)
ax.set_yticks(range(len(vals)))
ax.set_yticklabels(names, fontsize=9)
ax.set_xlabel("SHAP value (με)", fontsize=9)
ax.set_title(f"{title}\nbaseline {base:.0f} με → prediction "
f"{p:.0f} με", fontsize=10)
ax.grid(alpha=0.3, axis="x")
for s in ["top", "right"]:
ax.spines[s].set_visible(False)
fig.suptitle("Local SHAP — red pushes strain up, blue pushes it down",
fontsize=10.5, y=1.02)
fig.tight_layout()
return fig
def _gpu(fn):
"""ZeroGPU hardware refuses to start without a @spaces.GPU function.
Inference itself runs on CPU in <1 s (SHAP ~3 s), so the short duration
just satisfies the check while keeping queue priority high."""
return spaces.GPU(duration=30)(fn) if spaces is not None else fn
@_gpu
def predict(d0, d200, d300, d450, d600, d900, d1200, d1500, d1800,
h_ac, h_base, h_subbase, explain):
d = [d0, d200, d300, d450, d600, d900, d1200, d1500, d1800]
h = [h_ac, h_base, h_subbase]
if any(v is None for v in d + h):
raise gr.Error("Please fill in all 12 inputs (use 0 only for "
"h_Subbase when there is no subbase).")
d, h = np.array(d, np.float32), np.array(h, np.float32)
if np.any(d <= 0):
raise gr.Error("Deflections must be positive (μm at 707 kPa).")
if h[0] <= 0 or np.any(h < 0):
raise gr.Error("Thicknesses must be ≥ 0 mm with h_AC > 0.")
if d[0] < d[-1]:
raise gr.Error("D0 should exceed D1800 — check the basin order.")
x = np.concatenate([d, h])[None, :]
eps_ac, eps_sg = map(float, f(x)[0])
s_ac, c_ac = severity(eps_ac, 150, 400)
s_sg, c_sg = severity(eps_sg, 300, 600)
html = f"""
<div style="display:flex;gap:14px;flex-wrap:wrap;font-family:system-ui">
<div style="flex:1;min-width:230px;border:1px solid #e3e3e6;
border-radius:12px;padding:14px;background:#fafbfc">
<div style="font-size:13px;color:#6e6e80">ε<sub>t</sub> — AC tensile
strain (fatigue criterion)</div>
<div style="font-size:30px;font-weight:700">{eps_ac:.1f}
<span style="font-size:15px;color:#6e6e80">με</span>
<span style="font-size:14px;color:{c_ac}">· {s_ac}</span></div>
<div style="font-size:12px;color:#6e6e80">bottom of the asphalt layer
</div>
</div>
<div style="flex:1;min-width:230px;border:1px solid #e3e3e6;
border-radius:12px;padding:14px;background:#fafbfc">
<div style="font-size:13px;color:#6e6e80">ε<sub>c</sub> — subgrade
compressive strain (rutting criterion)</div>
<div style="font-size:30px;font-weight:700">{eps_sg:.1f}
<span style="font-size:15px;color:#6e6e80">με</span>
<span style="font-size:14px;color:{c_sg}">· {s_sg}</span></div>
<div style="font-size:12px;color:#6e6e80">top of the subgrade</div>
</div>
</div>"""
fig = None
if explain:
sv = explainer.shap_values(x, nsamples=NSAMPLES, silent=True)
sv = np.array(sv)
if sv.ndim == 3 and sv.shape[-1] == 2:
sv = np.moveaxis(sv, -1, 0)
fig = shap_figure(sv[0, 0], sv[1, 0], (eps_ac, eps_sg))
return html, fig
EXAMPLES = [
# Thai DOH route 23 (thin structure, unbound base)
[171.0, 154.5, 145.1, 89.6, 62.8, 47.4, 32.9, 22.6, 17.9,
100, 150, 300, True],
# factorial-like softer structure
[1126.8, 940.1, 815.5, 663.5, 548.0, 390.7, 293.9, 231.7, 189.9,
100, 200, 300, True],
# stiff bound-base section
[120.0, 100.0, 90.0, 75.0, 62.0, 45.0, 33.0, 25.0, 20.0,
50, 200, 0, True],
]
with gr.Blocks(theme=gr.themes.Soft(primary_hue="emerald"),
title="DBFT — Pavement Strain Predictor") as demo:
gr.Markdown(
"# 🛣️ DBFT — Pavement Strain Predictor\n"
"Predicts the two critical pavement strains **directly** from a "
"Falling Weight Deflectometer test — no backcalculation step. "
"Model: Deflection-Basin Fusion Transformer (~150k parameters), "
"combined loss λ = 1.0 on an extended layered-elastic surrogate + "
"7,651 Thai DOH field points. Held-out-route field accuracy: "
"**R² = 0.96 (AC) / 0.88 (subgrade)**."
)
gr.Image(str(ASSETS / "fwd_diagram.png"), show_label=False,
container=False, interactive=False, show_download_button=False,
show_fullscreen_button=False)
gr.Markdown("### Deflection basin — μm, normalized to 707 kPa")
with gr.Row():
d_in = [gr.Number(label=lab, precision=1) for lab in
["D0", "D200", "D300", "D450", "D600", "D900", "D1200",
"D1500", "D1800"]]
gr.Markdown("### Layer thicknesses — mm (h_Subbase = 0 if no subbase)")
with gr.Row():
h_in = [gr.Number(label="h_AC (mm)", precision=0),
gr.Number(label="h_Base (mm)", precision=0),
gr.Number(label="h_Subbase (mm)", precision=0)]
explain_in = gr.Checkbox(value=True,
label="Explain with local SHAP (~3 s)")
btn = gr.Button("Predict", variant="primary")
out_html = gr.HTML()
out_plot = gr.Plot(label="Why? — local SHAP attribution")
btn.click(predict, inputs=d_in + h_in + [explain_in],
outputs=[out_html, out_plot])
gr.Examples(examples=EXAMPLES, inputs=d_in + h_in + [explain_in],
label="Examples (click a row, then Predict)")
gr.Markdown(
"<small>Research prototype — trained on Thai DOH FWD data and "
"layered-elastic theory at 707 kPa / 150 mm plate. SHAP: "
"KernelExplainer over the 12 measurable inputs; red bars push the "
"prediction up, blue bars down.</small>"
)
if __name__ == "__main__":
# ssr_mode=False: the experimental SSR (Node.js) path is flaky on
# HF Spaces — "Stopping Node.js server..." restart loops.
demo.launch(ssr_mode=False)
|