Upload 8 files
Browse files- __pycache__/model.cpython-311.pyc +0 -0
- app.py +305 -0
- assets/depth_grid.npy +3 -0
- assets/examples.npz +3 -0
- assets/period_grid.npy +3 -0
- checkpoints/best_model.pth +3 -0
- model.py +165 -0
- requirements.txt +5 -0
__pycache__/model.cpython-311.pyc
ADDED
|
Binary file (12.6 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Streamlit app (Hugging Face Spaces): shear-wave velocity prediction from a
|
| 2 |
+
phase-velocity dispersion curve with the phase-only DispFormer trained on
|
| 3 |
+
OpenSWI-shallow.
|
| 4 |
+
|
| 5 |
+
Self-contained: model code, checkpoint, and assets live in this folder.
|
| 6 |
+
Run locally with:
|
| 7 |
+
streamlit run app.py
|
| 8 |
+
"""
|
| 9 |
+
import io
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import streamlit as st
|
| 15 |
+
import matplotlib.pyplot as plt
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from model import DispersionTransformerAblate
|
| 19 |
+
|
| 20 |
+
APP_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 21 |
+
|
| 22 |
+
# proposed model of paper3 (v4): physically-coded encoder-decoder, 2.44 M params
|
| 23 |
+
CKPT = os.path.join(APP_DIR, "checkpoints", "best_model.pth")
|
| 24 |
+
PERIOD = np.load(os.path.join(APP_DIR, "assets/period_grid.npy"))
|
| 25 |
+
DEPTH = np.load(os.path.join(APP_DIR, "assets/depth_grid.npy"))
|
| 26 |
+
C1, C3 = 1 / 3, 1 / 2 # wavelength heuristic coefficients (Xia et al., 1999)
|
| 27 |
+
|
| 28 |
+
st.set_page_config(page_title="Vs from dispersion curve", page_icon="🌍",
|
| 29 |
+
layout="wide")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@st.cache_resource
|
| 33 |
+
def load_model():
|
| 34 |
+
device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
| 35 |
+
model = DispersionTransformerAblate(
|
| 36 |
+
model_dim=128, num_heads=8, num_layers=3, output_dim=72,
|
| 37 |
+
scale_factor=4.5, local=False, decoder="depthq",
|
| 38 |
+
depth_values=DEPTH, decoder_layers=1).to(device)
|
| 39 |
+
model.load_state_dict(torch.load(CKPT, map_location=device))
|
| 40 |
+
model.train() # evaluation protocol of the training pipeline
|
| 41 |
+
return model, device
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@st.cache_data
|
| 45 |
+
def load_examples():
|
| 46 |
+
d = np.load(os.path.join(APP_DIR, "assets/examples.npz"))
|
| 47 |
+
return d["curves"], d["profiles"], list(d["names"])
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def snap_to_grid(periods, velocities):
|
| 51 |
+
"""Place picks on the fixed 100-period grid (nearest period; picks that
|
| 52 |
+
share a grid node are averaged). Returns (curve on grid, n_used, n_out)."""
|
| 53 |
+
grid = np.full(len(PERIOD), -1.0, dtype=np.float32)
|
| 54 |
+
counts = np.zeros(len(PERIOD))
|
| 55 |
+
sums = np.zeros(len(PERIOD))
|
| 56 |
+
n_out = 0
|
| 57 |
+
for T, c in zip(periods, velocities):
|
| 58 |
+
if not (PERIOD.min() <= T <= PERIOD.max()) or c <= 0:
|
| 59 |
+
n_out += 1
|
| 60 |
+
continue
|
| 61 |
+
j = int(np.abs(PERIOD - T).argmin())
|
| 62 |
+
sums[j] += c
|
| 63 |
+
counts[j] += 1
|
| 64 |
+
used = counts > 0
|
| 65 |
+
grid[used] = (sums[used] / counts[used]).astype(np.float32)
|
| 66 |
+
return grid, int(used.sum()), n_out
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def usable_depth_range(curve):
|
| 70 |
+
"""Constrained depth interval from the wavelength heuristic."""
|
| 71 |
+
valid = curve > 0
|
| 72 |
+
if not valid.any():
|
| 73 |
+
return 0, len(DEPTH)
|
| 74 |
+
p, v = PERIOD[valid], curve[valid]
|
| 75 |
+
dmin = C1 * p.min() * v[p.argmin()]
|
| 76 |
+
dmax = C3 * p.max() * v[p.argmax()]
|
| 77 |
+
lo = max(0, int(np.abs(DEPTH - dmin).argmin()) - 1)
|
| 78 |
+
hi = min(len(DEPTH), int(np.abs(DEPTH - dmax).argmin()) + 1)
|
| 79 |
+
return lo, hi
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def predict(curve):
|
| 83 |
+
model, device = load_model()
|
| 84 |
+
x = np.full((1, 3, len(PERIOD)), -1.0, dtype=np.float32)
|
| 85 |
+
x[0, 0] = PERIOD
|
| 86 |
+
x[0, 1] = curve
|
| 87 |
+
x = torch.from_numpy(x).to(device)
|
| 88 |
+
mask = (x[:, 1] == -1) & (x[:, 2] == -1)
|
| 89 |
+
with torch.no_grad():
|
| 90 |
+
out = model(x, mask)
|
| 91 |
+
return out[0, :len(DEPTH)].cpu().numpy()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def parse_table(df, period_col, vel_col, freq_input, vel_unit):
|
| 95 |
+
p = pd.to_numeric(df[period_col], errors="coerce").to_numpy(dtype=float)
|
| 96 |
+
v = pd.to_numeric(df[vel_col], errors="coerce").to_numpy(dtype=float)
|
| 97 |
+
ok = np.isfinite(p) & np.isfinite(v)
|
| 98 |
+
p, v = p[ok], v[ok]
|
| 99 |
+
if freq_input:
|
| 100 |
+
p = np.where(p > 0, 1.0 / p, np.nan)
|
| 101 |
+
v = v[np.isfinite(p)]
|
| 102 |
+
p = p[np.isfinite(p)]
|
| 103 |
+
if vel_unit == "m/s":
|
| 104 |
+
v = v / 1000.0
|
| 105 |
+
return p, v
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# ----------------------------------------------------------------- interface
|
| 109 |
+
st.title("Shear-wave velocity from a phase-velocity dispersion curve")
|
| 110 |
+
st.markdown(
|
| 111 |
+
"Inverts a fundamental-mode Rayleigh **phase-velocity** curve (the SASW/MASW "
|
| 112 |
+
"observable) for a 70-layer 1-D Vs profile in one forward pass — no initial "
|
| 113 |
+
"model. Proposed physically-coded transformer encoder–decoder (2.44 M params: "
|
| 114 |
+
"period tokens in, depth-query tokens out) trained on **OpenSWI-shallow** "
|
| 115 |
+
"(22 M curve/profile pairs).")
|
| 116 |
+
|
| 117 |
+
with st.sidebar:
|
| 118 |
+
st.header("Input")
|
| 119 |
+
source = st.radio("Curve source",
|
| 120 |
+
["Example from test data", "Upload CSV", "Paste values"])
|
| 121 |
+
st.divider()
|
| 122 |
+
st.header("Site scale")
|
| 123 |
+
st.caption("The physics is scale-invariant: measured periods are multiplied "
|
| 124 |
+
"by k to enter the model's 0.2–10 s band, and output depths are "
|
| 125 |
+
"divided by k. Velocities are never rescaled.")
|
| 126 |
+
preset = st.selectbox(
|
| 127 |
+
"Scale factor k",
|
| 128 |
+
["Native (k = 1): 0.2–10 s, 0–2.76 km",
|
| 129 |
+
"Engineering ≈ 30 m (k = 100): 2 ms–0.1 s, 0–27.6 m",
|
| 130 |
+
"Custom k"])
|
| 131 |
+
if preset.startswith("Custom"):
|
| 132 |
+
k = float(st.number_input("k (period multiplier)", min_value=1.0,
|
| 133 |
+
max_value=10000.0, value=100.0, step=1.0))
|
| 134 |
+
elif preset.startswith("Engineering"):
|
| 135 |
+
k = 100.0
|
| 136 |
+
else:
|
| 137 |
+
k = 1.0
|
| 138 |
+
if source == "Example from test data" and k != 1.0:
|
| 139 |
+
st.info("Examples are native-scale; k is applied to uploaded/pasted "
|
| 140 |
+
"curves only.")
|
| 141 |
+
k = 1.0
|
| 142 |
+
st.caption(
|
| 143 |
+
f"Accepted measured band at k = {k:g}: "
|
| 144 |
+
f"{PERIOD.min()/k:.4g}–{PERIOD.max()/k:.4g} s "
|
| 145 |
+
f"({k/PERIOD.max():.3g}–{k/PERIOD.min():.3g} Hz). "
|
| 146 |
+
f"Output depth range: {DEPTH.max()/k*1000:.3g} m."
|
| 147 |
+
if k > 1 else
|
| 148 |
+
f"Model grid: {len(PERIOD)} periods, {PERIOD.min():.1f}–{PERIOD.max():.0f} s. "
|
| 149 |
+
f"Output: {len(DEPTH)} layers, 0–{DEPTH.max():.2f} km (40 m spacing).")
|
| 150 |
+
st.divider()
|
| 151 |
+
st.caption("Picks are snapped to the nearest grid period; the model accepts "
|
| 152 |
+
"gaps and band-limited curves natively. Velocity support "
|
| 153 |
+
"≈ 0.3–4.5 km/s at any scale (velocities are not rescaled).")
|
| 154 |
+
|
| 155 |
+
# display-unit helpers: show everything in the user's field units
|
| 156 |
+
DEPTH_DISP = DEPTH / k
|
| 157 |
+
PERIOD_DISP = PERIOD / k
|
| 158 |
+
DEPTH_IN_M = DEPTH_DISP.max() < 0.2 # show meters for shallow scales
|
| 159 |
+
DUNIT = "m" if DEPTH_IN_M else "km"
|
| 160 |
+
DSC = 1000.0 if DEPTH_IN_M else 1.0
|
| 161 |
+
|
| 162 |
+
curve = None
|
| 163 |
+
true_vs = None
|
| 164 |
+
|
| 165 |
+
if source == "Example from test data":
|
| 166 |
+
curves, profiles, names = load_examples()
|
| 167 |
+
labels = [f"{n} #{i}" for i, n in enumerate(names)]
|
| 168 |
+
pick = st.sidebar.selectbox("Example", labels)
|
| 169 |
+
idx = labels.index(pick)
|
| 170 |
+
curve = curves[idx].copy()
|
| 171 |
+
true_vs = profiles[idx]
|
| 172 |
+
lo_p, hi_p = st.sidebar.slider(
|
| 173 |
+
"Restrict period band (s) — simulates a band-limited survey",
|
| 174 |
+
float(PERIOD.min()), float(PERIOD.max()),
|
| 175 |
+
(float(PERIOD.min()), float(PERIOD.max())))
|
| 176 |
+
curve[(PERIOD < lo_p) | (PERIOD > hi_p)] = -1.0
|
| 177 |
+
|
| 178 |
+
elif source == "Upload CSV":
|
| 179 |
+
st.sidebar.markdown("CSV with one column of period (s) **or** frequency (Hz), "
|
| 180 |
+
"and one of phase velocity.")
|
| 181 |
+
up = st.sidebar.file_uploader("CSV file", type=["csv", "txt"])
|
| 182 |
+
freq_input = st.sidebar.checkbox("First column is frequency (Hz)", False)
|
| 183 |
+
vel_unit = st.sidebar.radio("Velocity unit", ["km/s", "m/s"], horizontal=True)
|
| 184 |
+
if up is not None:
|
| 185 |
+
df = pd.read_csv(up)
|
| 186 |
+
cols = list(df.columns)
|
| 187 |
+
c1_, c2_ = st.sidebar.selectbox("Period/frequency column", cols, index=0), \
|
| 188 |
+
st.sidebar.selectbox("Velocity column", cols,
|
| 189 |
+
index=min(1, len(cols) - 1))
|
| 190 |
+
p, v = parse_table(df, c1_, c2_, freq_input, vel_unit)
|
| 191 |
+
curve, n_used, n_out = snap_to_grid(p * k, v)
|
| 192 |
+
st.sidebar.success(
|
| 193 |
+
f"{n_used} grid periods filled"
|
| 194 |
+
+ (f" · {n_out} picks outside the accepted band dropped" if n_out else ""))
|
| 195 |
+
|
| 196 |
+
else: # paste
|
| 197 |
+
st.sidebar.markdown("One `period_s, velocity_km_s` pair per line:")
|
| 198 |
+
default = "\n".join(f"{t:.3f}, {c:.3f}" for t, c in
|
| 199 |
+
zip(PERIOD[::12], load_examples()[0][0][::12])
|
| 200 |
+
if c > 0)
|
| 201 |
+
txt = st.sidebar.text_area("Values", default, height=200)
|
| 202 |
+
try:
|
| 203 |
+
rows = [list(map(float, ln.replace(",", " ").split()))
|
| 204 |
+
for ln in txt.strip().splitlines() if ln.strip()]
|
| 205 |
+
arr = np.array([r[:2] for r in rows if len(r) >= 2])
|
| 206 |
+
curve, n_used, n_out = snap_to_grid(arr[:, 0] * k, arr[:, 1])
|
| 207 |
+
st.sidebar.success(f"{n_used} grid periods filled")
|
| 208 |
+
except Exception as e:
|
| 209 |
+
st.sidebar.error(f"Could not parse input: {e}")
|
| 210 |
+
|
| 211 |
+
# ----------------------------------------------------------------- results
|
| 212 |
+
if curve is None or (curve > 0).sum() == 0:
|
| 213 |
+
st.info("Provide a dispersion curve in the sidebar to run the inversion.")
|
| 214 |
+
st.stop()
|
| 215 |
+
|
| 216 |
+
if (curve > 0).sum() < 5:
|
| 217 |
+
st.warning("Very few valid picks — the prediction will be poorly constrained.")
|
| 218 |
+
|
| 219 |
+
vmin_meas = float(curve[curve > 0].min())
|
| 220 |
+
if vmin_meas < 0.3:
|
| 221 |
+
st.warning(
|
| 222 |
+
f"Lowest measured phase velocity is {vmin_meas*1000:.0f} m/s — below the "
|
| 223 |
+
"training support (≈ 0.3–4.5 km/s, unchanged by the scale factor). "
|
| 224 |
+
"Typical of soft-soil sites; predictions there are extrapolation and the "
|
| 225 |
+
"recommended path is fine-tuning on engineering-scale synthetics "
|
| 226 |
+
"(paper3 §6.2).")
|
| 227 |
+
|
| 228 |
+
vs = predict(curve)
|
| 229 |
+
lo, hi = usable_depth_range(curve)
|
| 230 |
+
|
| 231 |
+
col1, col2 = st.columns(2)
|
| 232 |
+
valid = curve > 0
|
| 233 |
+
|
| 234 |
+
with col1:
|
| 235 |
+
fig, ax = plt.subplots(figsize=(5.5, 4))
|
| 236 |
+
ax.plot(PERIOD_DISP[valid], curve[valid], "o-", ms=3.5, lw=1.2, color="#1f77b4")
|
| 237 |
+
ax.set_xscale("log")
|
| 238 |
+
ax.set_xlabel("period (s)" + (f" [measured; k = {k:g}]" if k != 1 else ""))
|
| 239 |
+
ax.set_ylabel("phase velocity (km/s)")
|
| 240 |
+
ax.set_title(f"Input curve ({int(valid.sum())} of {len(PERIOD)} grid periods)")
|
| 241 |
+
ax.grid(alpha=0.3)
|
| 242 |
+
st.pyplot(fig, use_container_width=True)
|
| 243 |
+
plt.close(fig)
|
| 244 |
+
|
| 245 |
+
with col2:
|
| 246 |
+
fig, ax = plt.subplots(figsize=(5.5, 4))
|
| 247 |
+
D = DEPTH_DISP * DSC
|
| 248 |
+
if true_vs is not None:
|
| 249 |
+
ax.step(true_vs, D, where="mid", color="k", lw=1.6, label="true Vs")
|
| 250 |
+
ax.step(vs, D, where="mid", color="#d62728", lw=1.6, label="predicted Vs")
|
| 251 |
+
if lo > 0:
|
| 252 |
+
ax.axhspan(0, D[lo], color="gray", alpha=0.15)
|
| 253 |
+
if hi < len(DEPTH):
|
| 254 |
+
ax.axhspan(D[hi - 1], D[-1], color="gray", alpha=0.15)
|
| 255 |
+
ax.invert_yaxis()
|
| 256 |
+
ax.set_xlabel("Vs (km/s)")
|
| 257 |
+
ax.set_ylabel(f"depth ({DUNIT})")
|
| 258 |
+
ax.set_title("Predicted 1-D Vs profile")
|
| 259 |
+
ax.legend(fontsize=8)
|
| 260 |
+
ax.grid(alpha=0.3)
|
| 261 |
+
st.pyplot(fig, use_container_width=True)
|
| 262 |
+
plt.close(fig)
|
| 263 |
+
|
| 264 |
+
st.caption(
|
| 265 |
+
f"Gray bands mark depths outside the range the input band physically "
|
| 266 |
+
f"constrains (wavelength heuristic: ≈ ⅓·λ_min to ½·λ_max → "
|
| 267 |
+
f"{DEPTH_DISP[lo]*DSC:.2f}–{DEPTH_DISP[min(hi, len(DEPTH)) - 1]*DSC:.2f} "
|
| 268 |
+
f"{DUNIT} here); treat the profile there as extrapolation."
|
| 269 |
+
+ (f" Scale factor k = {k:g}: periods ×{k:g} into the model, depths ÷{k:g} "
|
| 270 |
+
"on output; velocities unchanged." if k != 1 else ""))
|
| 271 |
+
|
| 272 |
+
if true_vs is not None:
|
| 273 |
+
err = vs - true_vs
|
| 274 |
+
m1, m2, m3 = st.columns(3)
|
| 275 |
+
m1.metric("RMSE vs truth", f"{np.sqrt((err ** 2).mean()):.3f} km/s")
|
| 276 |
+
m2.metric("MAE vs truth", f"{np.abs(err).mean():.3f} km/s")
|
| 277 |
+
m3.metric("MAPE vs truth", f"{(np.abs(err) / true_vs).mean() * 100:.1f} %")
|
| 278 |
+
|
| 279 |
+
out_df = pd.DataFrame({f"depth_{DUNIT}": DEPTH_DISP * DSC, "vs_km_s": vs,
|
| 280 |
+
"constrained": [(lo <= i < hi) for i in range(len(DEPTH))]})
|
| 281 |
+
buf = io.StringIO()
|
| 282 |
+
out_df.to_csv(buf, index=False)
|
| 283 |
+
st.download_button("Download predicted profile (CSV)", buf.getvalue(),
|
| 284 |
+
file_name="predicted_vs_profile.csv", mime="text/csv")
|
| 285 |
+
|
| 286 |
+
with st.expander("Model & protocol details"):
|
| 287 |
+
st.markdown(
|
| 288 |
+
"- **Model:** physically-coded encoder–decoder (paper3): each dispersion "
|
| 289 |
+
"pick is a token carrying its physical period; each output depth is a query "
|
| 290 |
+
"token carrying its physical depth, reading the period tokens by masked "
|
| 291 |
+
"cross-attention — band-limited and gappy curves are handled natively "
|
| 292 |
+
"(no interpolation), and its learned attention reproduces the classical "
|
| 293 |
+
"λ/3 sensitivity rule (paper3, Fig. 7).\n"
|
| 294 |
+
"- **Checkpoint:** v4 finalist (valid masked MSE 0.0216 (km/s)²); test "
|
| 295 |
+
"accuracy 0.151 km/s full-profile RMSE / 6.0 % MAPE / R² 0.949 on 50 k "
|
| 296 |
+
"held-out samples; Long Beach field data 38 m/s MAE vs the tomographic "
|
| 297 |
+
"reference (5,297 real curves).\n"
|
| 298 |
+
"- **Scope:** trained on 0.2–10 s periods, 0–2.76 km depth, Vs ≈ 0.3–4.5 km/s "
|
| 299 |
+
"(OpenSWI-shallow). Curves outside this envelope — e.g. soft-soil sites with "
|
| 300 |
+
"Vs < 0.3 km/s — are out of distribution.\n"
|
| 301 |
+
"- **Site scale factor k:** the elastodynamic problem is scale-invariant, so "
|
| 302 |
+
"a high-frequency engineering curve is inverted by stretching its periods "
|
| 303 |
+
"×k into the training band and shrinking the output depths ÷k (e.g. k = 100 "
|
| 304 |
+
"→ 70 layers over 0.4–27.6 m at 0.4 m spacing). Velocities are never "
|
| 305 |
+
"rescaled — the ≈ 0.3–4.5 km/s support applies at every scale; see paper3 §6.2.")
|
assets/depth_grid.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d4a4b11686b46c4f31a80d3089ab56047a947d45c955a87981d8b5c5fdaece9c
|
| 3 |
+
size 408
|
assets/examples.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:db4ab30c21707a1a081d0c7e7e954802f069c87396c63992e654885df471dd78
|
| 3 |
+
size 4684
|
assets/period_grid.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:37213bd5c860938086c308070aee6523f9f19519b68cca882d5df6f46c75b7f7
|
| 3 |
+
size 528
|
checkpoints/best_model.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3b89903aae4400a4df07fc988f100d038a3fcfe71d400b43fcdcd909de10b48d
|
| 3 |
+
size 9780661
|
model.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Standalone copy of the paper3 proposed model (v4): physically-coded
|
| 2 |
+
transformer encoder-decoder with a depth-query decoder, 2.44 M params.
|
| 3 |
+
|
| 4 |
+
Self-contained for Hugging Face Spaces deployment — merges the pieces of
|
| 5 |
+
`ablation_models.py` and `SWInversion/model/dispformer_local_global_v1.py`
|
| 6 |
+
that the served configuration (pos='period', local=False, transformer=True,
|
| 7 |
+
decoder='depthq') actually uses, so the checkpoint loads verbatim.
|
| 8 |
+
"""
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn as nn
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class MaskedConv1d(nn.Conv1d):
|
| 15 |
+
"""Convolution that zeroes missing entries and renormalizes each window
|
| 16 |
+
by its valid count, so sentinel values never leak into features."""
|
| 17 |
+
|
| 18 |
+
def __init__(self, *args, **kwargs):
|
| 19 |
+
kwargs['bias'] = False
|
| 20 |
+
super().__init__(*args, **kwargs)
|
| 21 |
+
|
| 22 |
+
def forward(self, x, mask):
|
| 23 |
+
# mask: (B, 1, L), x: (B, C_in, L)
|
| 24 |
+
conv_out = super().forward(x * mask)
|
| 25 |
+
with torch.no_grad():
|
| 26 |
+
ones_kernel = torch.ones((1, 1, self.kernel_size[0]),
|
| 27 |
+
device=x.device)
|
| 28 |
+
valid_count = F.conv1d(mask.float(), ones_kernel, bias=None,
|
| 29 |
+
stride=self.stride[0],
|
| 30 |
+
padding=self.padding[0],
|
| 31 |
+
dilation=self.dilation[0]).clamp(min=1e-6)
|
| 32 |
+
return conv_out / valid_count
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class LocalFeatureExtraction(nn.Module):
|
| 36 |
+
def __init__(self, model_dim):
|
| 37 |
+
super().__init__()
|
| 38 |
+
self.conv1 = MaskedConv1d(model_dim, model_dim, kernel_size=7, padding=3)
|
| 39 |
+
self.conv2 = MaskedConv1d(model_dim, model_dim, kernel_size=5, padding=2)
|
| 40 |
+
self.conv3 = MaskedConv1d(model_dim, model_dim, kernel_size=3, padding=1)
|
| 41 |
+
self.relu = nn.ReLU()
|
| 42 |
+
|
| 43 |
+
def forward(self, x, mask):
|
| 44 |
+
x = self.relu(self.conv1(x, mask.clone()))
|
| 45 |
+
x = self.relu(self.conv2(x, mask.clone()))
|
| 46 |
+
return self.relu(self.conv3(x, mask.clone()))
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class DepthQueryDecoder(nn.Module):
|
| 50 |
+
"""Per-depth cross-attention decoder: each output depth is a query token
|
| 51 |
+
embedding its PHYSICAL depth value (mirroring the period stream on the
|
| 52 |
+
input side), decoded by a standard transformer decoder (self-attention
|
| 53 |
+
over depths + cross-attention to the period tokens, key-padding mask
|
| 54 |
+
applied) and a shared bounded linear head."""
|
| 55 |
+
|
| 56 |
+
def __init__(self, depth_values, model_dim, num_heads, num_layers=2,
|
| 57 |
+
scale_factor=4.5):
|
| 58 |
+
super().__init__()
|
| 59 |
+
self.register_buffer("depth_values",
|
| 60 |
+
torch.as_tensor(depth_values, dtype=torch.float32))
|
| 61 |
+
self.depth_embedding = nn.Sequential(nn.Linear(1, model_dim), nn.ReLU())
|
| 62 |
+
self.decoder = nn.TransformerDecoder(
|
| 63 |
+
nn.TransformerDecoderLayer(d_model=model_dim, nhead=num_heads,
|
| 64 |
+
dropout=0, batch_first=True),
|
| 65 |
+
num_layers=num_layers)
|
| 66 |
+
self.out = nn.Linear(model_dim, 1)
|
| 67 |
+
self.scale_factor = scale_factor
|
| 68 |
+
|
| 69 |
+
def forward(self, memory, memory_key_padding_mask=None):
|
| 70 |
+
B = memory.shape[0]
|
| 71 |
+
q = self.depth_embedding(self.depth_values[:, None]) # (L, d)
|
| 72 |
+
q = q.unsqueeze(0).expand(B, -1, -1) # (B, L, d)
|
| 73 |
+
z = self.decoder(q, memory,
|
| 74 |
+
memory_key_padding_mask=memory_key_padding_mask)
|
| 75 |
+
return torch.sigmoid(self.out(z).squeeze(-1)) * self.scale_factor
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class DispersionTransformerAblate(nn.Module):
|
| 79 |
+
def __init__(self, model_dim, num_heads, num_layers, output_dim,
|
| 80 |
+
scale_factor=6.5, seq_len=100, pos="period",
|
| 81 |
+
masked_conv=True, key_padding=True, local=True,
|
| 82 |
+
transformer=True, pool="avgmax", head="bounded",
|
| 83 |
+
decoder="pooled", depth_values=None, decoder_layers=2):
|
| 84 |
+
super().__init__()
|
| 85 |
+
self.flags = dict(pos=pos, masked_conv=masked_conv,
|
| 86 |
+
key_padding=key_padding, local=local,
|
| 87 |
+
transformer=transformer, pool=pool, head=head,
|
| 88 |
+
decoder=decoder, decoder_layers=decoder_layers)
|
| 89 |
+
|
| 90 |
+
self.period_embedding = nn.Sequential(
|
| 91 |
+
nn.Conv1d(1, model_dim, kernel_size=1, stride=1), nn.ReLU())
|
| 92 |
+
self.phase_velocity_encoding = nn.Sequential(
|
| 93 |
+
nn.Conv1d(1, model_dim, kernel_size=1, stride=1), nn.ReLU())
|
| 94 |
+
self.group_velocity_encoding = nn.Sequential(
|
| 95 |
+
nn.Conv1d(1, model_dim, kernel_size=1, stride=1), nn.ReLU())
|
| 96 |
+
if pos == "learned":
|
| 97 |
+
self.learned_pe = nn.Parameter(torch.randn(model_dim, seq_len) * 0.02)
|
| 98 |
+
|
| 99 |
+
if local:
|
| 100 |
+
self.local_feature_extraction_phaseVelocity = \
|
| 101 |
+
LocalFeatureExtraction(model_dim=model_dim)
|
| 102 |
+
self.local_feature_extraction_groupVelocity = \
|
| 103 |
+
LocalFeatureExtraction(model_dim=model_dim)
|
| 104 |
+
|
| 105 |
+
if transformer:
|
| 106 |
+
self.transformer_encoder = nn.TransformerEncoder(
|
| 107 |
+
nn.TransformerEncoderLayer(d_model=model_dim, nhead=num_heads,
|
| 108 |
+
dropout=0, batch_first=True),
|
| 109 |
+
num_layers=num_layers)
|
| 110 |
+
|
| 111 |
+
if decoder == "depthq":
|
| 112 |
+
assert depth_values is not None, "depthq decoder needs depth grid"
|
| 113 |
+
self.depth_decoder = DepthQueryDecoder(
|
| 114 |
+
depth_values, model_dim, num_heads,
|
| 115 |
+
num_layers=decoder_layers, scale_factor=scale_factor)
|
| 116 |
+
else:
|
| 117 |
+
self.global_pooling = nn.AdaptiveAvgPool1d(1)
|
| 118 |
+
self.max_pooling = nn.AdaptiveMaxPool1d(1)
|
| 119 |
+
fc_in = 2 * model_dim if pool == "avgmax" else model_dim
|
| 120 |
+
fc = [nn.Linear(fc_in, 1024), nn.ReLU(),
|
| 121 |
+
nn.Linear(1024, 1024), nn.ReLU(),
|
| 122 |
+
nn.Linear(1024, output_dim)]
|
| 123 |
+
if head == "bounded":
|
| 124 |
+
fc.append(nn.Sigmoid())
|
| 125 |
+
self.fc_fuse = nn.Sequential(*fc)
|
| 126 |
+
self.scale_factor = scale_factor
|
| 127 |
+
|
| 128 |
+
def forward(self, input_data, mask=None):
|
| 129 |
+
period_data = input_data[:, 0, :]
|
| 130 |
+
phase_velocity = input_data[:, 1, :]
|
| 131 |
+
group_velocity = input_data[:, 2, :]
|
| 132 |
+
phase_mask = (phase_velocity > 0).unsqueeze(1)
|
| 133 |
+
group_mask = (group_velocity > 0).unsqueeze(1)
|
| 134 |
+
|
| 135 |
+
phase_emb = self.phase_velocity_encoding(phase_velocity.unsqueeze(1))
|
| 136 |
+
group_emb = self.group_velocity_encoding(group_velocity.unsqueeze(1))
|
| 137 |
+
if self.flags["local"]:
|
| 138 |
+
phase_emb = self.local_feature_extraction_phaseVelocity(phase_emb, phase_mask)
|
| 139 |
+
group_emb = self.local_feature_extraction_groupVelocity(group_emb, group_mask)
|
| 140 |
+
|
| 141 |
+
combined = phase_emb + group_emb
|
| 142 |
+
if self.flags["pos"] == "period":
|
| 143 |
+
combined = combined + self.period_embedding(period_data.unsqueeze(1))
|
| 144 |
+
elif self.flags["pos"] == "learned":
|
| 145 |
+
combined = combined + self.learned_pe.unsqueeze(0)
|
| 146 |
+
|
| 147 |
+
fused = combined.permute(0, 2, 1)
|
| 148 |
+
if self.flags["transformer"]:
|
| 149 |
+
kp = mask if self.flags["key_padding"] else None
|
| 150 |
+
fused = self.transformer_encoder(fused, src_key_padding_mask=kp)
|
| 151 |
+
|
| 152 |
+
if self.flags["decoder"] == "depthq":
|
| 153 |
+
kp = mask if self.flags["key_padding"] else None
|
| 154 |
+
return self.depth_decoder(fused, memory_key_padding_mask=kp)
|
| 155 |
+
|
| 156 |
+
seq = fused.permute(0, 2, 1)
|
| 157 |
+
if self.flags["pool"] == "avgmax":
|
| 158 |
+
pooled = torch.cat([self.global_pooling(seq), self.max_pooling(seq)], dim=1)
|
| 159 |
+
else:
|
| 160 |
+
pooled = self.global_pooling(seq)
|
| 161 |
+
|
| 162 |
+
out = self.fc_fuse(pooled.squeeze(-1))
|
| 163 |
+
if self.flags["head"] == "bounded":
|
| 164 |
+
out = out * self.scale_factor
|
| 165 |
+
return out
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit>=1.38
|
| 2 |
+
torch>=2.3
|
| 3 |
+
numpy
|
| 4 |
+
pandas
|
| 5 |
+
matplotlib
|