Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# app.py (Version 2.
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
"""
|
| 4 |
Refraction & Reflection Seismology Demonstrator (Gradio Web App)
|
|
@@ -6,10 +6,11 @@ Refraction & Reflection Seismology Demonstrator (Gradio Web App)
|
|
| 6 |
This application is generated based on the Specification-Driven Development (SDD)
|
| 7 |
process, fulfilling all user stories and acceptance criteria defined in `spec.md` v2.2+.
|
| 8 |
|
| 9 |
-
Version 2.
|
| 10 |
-
1. (
|
| 11 |
-
|
| 12 |
-
|
|
|
|
| 13 |
"""
|
| 14 |
|
| 15 |
# =============================
|
|
@@ -28,45 +29,45 @@ import matplotlib.pyplot as plt
|
|
| 28 |
from PIL import Image
|
| 29 |
|
| 30 |
# =============================
|
| 31 |
-
# 2. Core Physics & Data Models
|
| 32 |
# =============================
|
| 33 |
|
| 34 |
@dataclass
|
| 35 |
class Model2LayerRefraction:
|
| 36 |
-
V1: float
|
| 37 |
-
V2: float
|
| 38 |
-
h1: float
|
| 39 |
|
| 40 |
def validate(self):
|
| 41 |
if not (self.V1 > 0 and self.V2 > 0 and self.h1 > 0):
|
| 42 |
-
raise ValueError("
|
| 43 |
if self.V2 <= self.V1:
|
| 44 |
-
raise ValueError("V2
|
| 45 |
|
| 46 |
@dataclass
|
| 47 |
class ModelReflectionSimple:
|
| 48 |
-
V1: float
|
| 49 |
-
h1: float
|
| 50 |
|
| 51 |
def validate(self):
|
| 52 |
if not (self.V1 > 0 and self.h1 > 0):
|
| 53 |
-
raise ValueError("
|
| 54 |
|
| 55 |
@dataclass
|
| 56 |
class SurveyLine:
|
| 57 |
-
length: float
|
| 58 |
n_geophones: int
|
| 59 |
|
| 60 |
def validate(self):
|
| 61 |
if self.length <= 0:
|
| 62 |
-
raise ValueError("
|
| 63 |
if self.n_geophones < 2:
|
| 64 |
-
raise ValueError("
|
| 65 |
|
| 66 |
def positions(self) -> np.ndarray:
|
| 67 |
return np.linspace(0.0, self.length, self.n_geophones)
|
| 68 |
|
| 69 |
-
|
| 70 |
def critical_angle(V1: float, V2: float) -> float:
|
| 71 |
s = np.clip(V1 / V2, -1 + 1e-12, 1 - 1e-12)
|
| 72 |
return float(np.arcsin(s))
|
|
@@ -87,7 +88,7 @@ def refraction_travel_times(model: Model2LayerRefraction, x: np.ndarray) -> Tupl
|
|
| 87 |
def robust_line_fit(x: np.ndarray, t: np.ndarray) -> Tuple[float, float]:
|
| 88 |
mask = np.isfinite(x) & np.isfinite(t)
|
| 89 |
if np.sum(mask) < 2:
|
| 90 |
-
raise ValueError("
|
| 91 |
A = np.vstack([x[mask], np.ones(np.sum(mask))]).T
|
| 92 |
a, b = np.linalg.lstsq(A, t[mask], rcond=None)[0]
|
| 93 |
return float(a), float(b)
|
|
@@ -96,16 +97,16 @@ def invert_from_first_arrivals(x: np.ndarray, t_first: np.ndarray, which_first:
|
|
| 96 |
x_dir, t_dir = x[which_first == 0], t_first[which_first == 0]
|
| 97 |
x_ref, t_ref = x[which_first == 1], t_first[which_first == 1]
|
| 98 |
|
| 99 |
-
if x_dir.size < 2: raise ValueError("
|
| 100 |
-
if x_ref.size < 2: raise ValueError("
|
| 101 |
|
| 102 |
a1, _ = robust_line_fit(x_dir, t_dir)
|
| 103 |
a2, b2 = robust_line_fit(x_ref, t_ref)
|
| 104 |
|
| 105 |
-
if a1 <= 0 or a2 <= 0: raise ValueError("
|
| 106 |
V1_est, V2_est, t0_est = 1.0 / a1, 1.0 / a2, b2
|
| 107 |
|
| 108 |
-
if V2_est <= V1_est: raise ValueError("
|
| 109 |
|
| 110 |
ic_est = math.asin(min(0.999999999999, V1_est / V2_est))
|
| 111 |
h1_est = (t0_est * V1_est) / (2.0 * math.cos(ic_est))
|
|
@@ -165,7 +166,7 @@ def plot_reflection_png(x, t_direct, t_reflect, t0, title="Reflection T-X (Singl
|
|
| 165 |
buf.seek(0)
|
| 166 |
return Image.open(buf).convert("RGBA")
|
| 167 |
|
| 168 |
-
def plot_inversion_exercise_png(true_data: Dict, guess_data: Dict
|
| 169 |
fig, ax = plt.subplots(figsize=(7, 5), dpi=160)
|
| 170 |
|
| 171 |
ax.plot(true_data['x'], true_data['t'], 'k--', linewidth=2, label="Original T-X Curve (Problem)")
|
|
@@ -176,7 +177,7 @@ def plot_inversion_exercise_png(true_data: Dict, guess_data: Dict, title: str =
|
|
| 176 |
|
| 177 |
ax.set_xlim(0, max(true_data['x']))
|
| 178 |
ax.set_ylim(0, max(true_data['t']) * 1.1)
|
| 179 |
-
ax.set(xlabel="Epicentral Distance (km)", ylabel="P-wave Travel Time (s)", title=
|
| 180 |
ax.grid(True, linestyle="--", alpha=0.4)
|
| 181 |
ax.legend()
|
| 182 |
fig.tight_layout()
|
|
@@ -187,7 +188,7 @@ def plot_inversion_exercise_png(true_data: Dict, guess_data: Dict, title: str =
|
|
| 187 |
return Image.open(buf).convert("RGBA")
|
| 188 |
|
| 189 |
# =============================
|
| 190 |
-
# 4. Gradio UI Callback Functions
|
| 191 |
# =============================
|
| 192 |
def simulate_refraction(V1, V2, h1, length, n_geophones, plot_reflection_toggle):
|
| 193 |
try:
|
|
@@ -210,15 +211,15 @@ def simulate_refraction(V1, V2, h1, length, n_geophones, plot_reflection_toggle)
|
|
| 210 |
|
| 211 |
try:
|
| 212 |
V1_est, V2_est, h1_est = invert_from_first_arrivals(x, t_first, which_first)
|
| 213 |
-
inv_md = (f"###
|
| 214 |
except Exception as e:
|
| 215 |
-
inv_md = f"###
|
| 216 |
|
| 217 |
ic_deg = math.degrees(critical_angle(model.V1, model.V2))
|
| 218 |
-
info_md = (f"###
|
| 219 |
|
| 220 |
if plot_reflection_toggle and t0_reflection is not None:
|
| 221 |
-
info_md += f"-
|
| 222 |
|
| 223 |
pil_img = plot_combined_png(x, t_direct, t_refrac, t_first, which_first, t_reflect=t_reflect_data)
|
| 224 |
|
|
@@ -233,7 +234,7 @@ def simulate_refraction(V1, V2, h1, length, n_geophones, plot_reflection_toggle)
|
|
| 233 |
return pil_img, info_md + "\n" + inv_md, df, tmp_csv.name
|
| 234 |
|
| 235 |
except Exception as e:
|
| 236 |
-
return None, f"❌
|
| 237 |
|
| 238 |
|
| 239 |
def simulate_reflection(V1, h1, length, n_geophones):
|
|
@@ -247,7 +248,7 @@ def simulate_reflection(V1, h1, length, n_geophones):
|
|
| 247 |
t_direct = x / model.V1
|
| 248 |
t_reflect, t0 = reflection_tx_hyperbola(model.V1, model.h1, x)
|
| 249 |
|
| 250 |
-
info_md = (f"###
|
| 251 |
pil_img = plot_reflection_png(x, t_direct, t_reflect, t0)
|
| 252 |
df = pd.DataFrame({"x_km": x, "t_direct_s": t_direct, "t_reflection_s": t_reflect})
|
| 253 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".csv", mode='w', newline='') as tmp_csv:
|
|
@@ -255,14 +256,13 @@ def simulate_reflection(V1, h1, length, n_geophones):
|
|
| 255 |
return pil_img, info_md, df, tmp_csv.name
|
| 256 |
|
| 257 |
except Exception as e:
|
| 258 |
-
return None, f"❌
|
| 259 |
|
| 260 |
-
# UNITS: All presets updated to km and km/s
|
| 261 |
REFRACTION_PRESETS: Dict[str, Dict] = {
|
| 262 |
-
"
|
| 263 |
-
"
|
| 264 |
-
"
|
| 265 |
-
"
|
| 266 |
}
|
| 267 |
def fill_refraction_preset(key: str):
|
| 268 |
p = REFRACTION_PRESETS[key]
|
|
@@ -284,115 +284,176 @@ def interactive_exercise_callback(v1_guess, v2_guess, h1_guess):
|
|
| 284 |
guess_data = {'x': x_km, 't_direct': t_direct, 't_refrac': t_refrac, 't_first': t_first}
|
| 285 |
|
| 286 |
feedback_md = (
|
| 287 |
-
f"###
|
| 288 |
-
f"-
|
| 289 |
-
f"-
|
| 290 |
f"--- \n"
|
| 291 |
-
f"
|
| 292 |
)
|
| 293 |
pil_img = plot_inversion_exercise_png(TRUE_TX_DATA, guess_data)
|
| 294 |
return pil_img, feedback_md
|
| 295 |
except Exception as e:
|
| 296 |
-
return None, f"❌
|
| 297 |
|
| 298 |
|
| 299 |
# =============================
|
| 300 |
-
# 5. Gradio UI Layout
|
| 301 |
# =============================
|
| 302 |
-
OVERVIEW_PRINCIPLES_MD = "
|
| 303 |
-
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
theme = gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky).set(
|
| 307 |
body_background_fill="#f8f9fa", block_background_fill="white", block_radius="16px", block_shadow="0 4px 12px rgba(0,0,0,.08)")
|
| 308 |
|
| 309 |
with gr.Blocks(theme=theme, css=".markdown h2, .markdown h3 { margin-top: .6rem; } .footer-note { font-size: 12px; color: #6b7280; }") as demo:
|
| 310 |
-
gr.Markdown("#
|
|
|
|
| 311 |
with gr.Tabs():
|
| 312 |
-
with gr.Tab("Overview"):
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
with gr.Row():
|
| 317 |
with gr.Column(scale=1):
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
run_r = gr.Button("🚀 Run Refraction Simulation", variant="primary")
|
| 326 |
with gr.Column(scale=2):
|
| 327 |
-
out_img_r = gr.Image(label="T-X
|
| 328 |
out_info_r = gr.Markdown()
|
| 329 |
-
out_df_r = gr.Dataframe(label="
|
| 330 |
-
out_csv_r = gr.File(label="
|
| 331 |
run_r.click(simulate_refraction, inputs=[V1_r, V2_r, h1_r, length_r, n_r, plot_reflection_toggle_r], outputs=[out_img_r, out_info_r, out_df_r, out_csv_r])
|
| 332 |
|
| 333 |
-
with gr.Tab("Reflection
|
| 334 |
with gr.Row():
|
| 335 |
with gr.Column(scale=1):
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
run_s = gr.Button("🚀 Run Reflection Simulation", variant="primary")
|
| 342 |
with gr.Column(scale=2):
|
| 343 |
-
out_img_s = gr.Image(label="T-X
|
| 344 |
out_info_s = gr.Markdown()
|
| 345 |
-
out_df_s = gr.Dataframe(label="
|
| 346 |
-
out_csv_s = gr.File(label="
|
| 347 |
run_s.click(simulate_reflection, inputs=[V1_s, h1_s, length_s, n_s], outputs=[out_img_s, out_info_s, out_df_s, out_csv_s])
|
| 348 |
|
| 349 |
-
with gr.Tab("
|
| 350 |
with gr.Row():
|
| 351 |
with gr.Column(scale=1):
|
| 352 |
-
preset_choice = gr.Radio(list(REFRACTION_PRESETS.keys()), label="
|
| 353 |
-
load_btn = gr.Button("
|
| 354 |
-
run_preset_btn = gr.Button("
|
| 355 |
with gr.Column(scale=2):
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
plot_reflection_toggle_p = gr.Checkbox(label="Overlay Reflection Plot", value=False)
|
| 363 |
|
| 364 |
load_btn.click(fill_refraction_preset, inputs=[preset_choice], outputs=[V1_p, V2_p, h1_p, length_p, n_p])
|
| 365 |
|
| 366 |
with gr.Row():
|
| 367 |
with gr.Column(scale=1):
|
| 368 |
-
out_img_p = gr.Image(label="T-X
|
| 369 |
with gr.Column(scale=1):
|
| 370 |
out_info_p = gr.Markdown()
|
| 371 |
-
out_df_p = gr.Dataframe(label="
|
| 372 |
-
out_csv_p = gr.File(label="
|
| 373 |
|
| 374 |
run_preset_btn.click(simulate_refraction, inputs=[V1_p, V2_p, h1_p, length_p, n_p, plot_reflection_toggle_p], outputs=[out_img_p, out_info_p, out_df_p, out_csv_p])
|
| 375 |
|
| 376 |
-
with gr.Tab("
|
| 377 |
with gr.Row():
|
| 378 |
with gr.Column(scale=1):
|
| 379 |
-
gr.Markdown("###
|
| 380 |
-
gr.Image("problem.jpg", label="
|
| 381 |
-
gr.Markdown("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
with gr.Column(scale=2):
|
| 383 |
-
gr.Markdown("###
|
| 384 |
-
gr.Markdown("
|
| 385 |
with gr.Row():
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
h1_guess = gr.Slider(minimum=5.0, maximum=40.0, value=20.0, step=0.5, label="Guessed Crustal Thickness h1 (km)")
|
| 390 |
|
| 391 |
-
plot_exercise = gr.Image(label="
|
| 392 |
feedback_exercise = gr.Markdown()
|
| 393 |
|
| 394 |
-
with gr.Accordion("
|
| 395 |
-
gr.Markdown(
|
| 396 |
|
| 397 |
for slider in [v1_guess, v2_guess, h1_guess]:
|
| 398 |
slider.change(
|
|
@@ -402,7 +463,7 @@ with gr.Blocks(theme=theme, css=".markdown h2, .markdown h3 { margin-top: .6rem;
|
|
| 402 |
show_progress="hidden"
|
| 403 |
)
|
| 404 |
|
| 405 |
-
gr.Markdown("---
|
| 406 |
|
| 407 |
if __name__ == "__main__":
|
| 408 |
demo.launch()
|
|
|
|
| 1 |
+
# app.py (Version 2.5 - Chinese UI & English Plots)
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
"""
|
| 4 |
Refraction & Reflection Seismology Demonstrator (Gradio Web App)
|
|
|
|
| 6 |
This application is generated based on the Specification-Driven Development (SDD)
|
| 7 |
process, fulfilling all user stories and acceptance criteria defined in `spec.md` v2.2+.
|
| 8 |
|
| 9 |
+
Version 2.5 changes:
|
| 10 |
+
1. (I18N) Reverted all UI components (labels, buttons, titles) and dynamic
|
| 11 |
+
Markdown text to Traditional Chinese.
|
| 12 |
+
2. (I18N) Kept plot-internal text (axes, legends, titles) in English as requested.
|
| 13 |
+
3. (Units) The unit system remains in kilometers (km) and km/s for usability.
|
| 14 |
"""
|
| 15 |
|
| 16 |
# =============================
|
|
|
|
| 29 |
from PIL import Image
|
| 30 |
|
| 31 |
# =============================
|
| 32 |
+
# 2. Core Physics & Data Models (Units: km, km/s)
|
| 33 |
# =============================
|
| 34 |
|
| 35 |
@dataclass
|
| 36 |
class Model2LayerRefraction:
|
| 37 |
+
V1: float
|
| 38 |
+
V2: float
|
| 39 |
+
h1: float
|
| 40 |
|
| 41 |
def validate(self):
|
| 42 |
if not (self.V1 > 0 and self.V2 > 0 and self.h1 > 0):
|
| 43 |
+
raise ValueError("模型參數 (V1, V2, h1) 必須是正數。")
|
| 44 |
if self.V2 <= self.V1:
|
| 45 |
+
raise ValueError("V2 必須大於 V1 才能發生臨界折射。")
|
| 46 |
|
| 47 |
@dataclass
|
| 48 |
class ModelReflectionSimple:
|
| 49 |
+
V1: float
|
| 50 |
+
h1: float
|
| 51 |
|
| 52 |
def validate(self):
|
| 53 |
if not (self.V1 > 0 and self.h1 > 0):
|
| 54 |
+
raise ValueError("模型參數 (V1, h1) 必須是正數。")
|
| 55 |
|
| 56 |
@dataclass
|
| 57 |
class SurveyLine:
|
| 58 |
+
length: float
|
| 59 |
n_geophones: int
|
| 60 |
|
| 61 |
def validate(self):
|
| 62 |
if self.length <= 0:
|
| 63 |
+
raise ValueError("測線長度需為正數。")
|
| 64 |
if self.n_geophones < 2:
|
| 65 |
+
raise ValueError("檢波器數量需 ≥ 2。")
|
| 66 |
|
| 67 |
def positions(self) -> np.ndarray:
|
| 68 |
return np.linspace(0.0, self.length, self.n_geophones)
|
| 69 |
|
| 70 |
+
|
| 71 |
def critical_angle(V1: float, V2: float) -> float:
|
| 72 |
s = np.clip(V1 / V2, -1 + 1e-12, 1 - 1e-12)
|
| 73 |
return float(np.arcsin(s))
|
|
|
|
| 88 |
def robust_line_fit(x: np.ndarray, t: np.ndarray) -> Tuple[float, float]:
|
| 89 |
mask = np.isfinite(x) & np.isfinite(t)
|
| 90 |
if np.sum(mask) < 2:
|
| 91 |
+
raise ValueError("線性回歸的點數不足。")
|
| 92 |
A = np.vstack([x[mask], np.ones(np.sum(mask))]).T
|
| 93 |
a, b = np.linalg.lstsq(A, t[mask], rcond=None)[0]
|
| 94 |
return float(a), float(b)
|
|
|
|
| 97 |
x_dir, t_dir = x[which_first == 0], t_first[which_first == 0]
|
| 98 |
x_ref, t_ref = x[which_first == 1], t_first[which_first == 1]
|
| 99 |
|
| 100 |
+
if x_dir.size < 2: raise ValueError("直接波初達點不足,無法反演。")
|
| 101 |
+
if x_ref.size < 2: raise ValueError("折射波初達點不足,請增加測線長度或層厚。")
|
| 102 |
|
| 103 |
a1, _ = robust_line_fit(x_dir, t_dir)
|
| 104 |
a2, b2 = robust_line_fit(x_ref, t_ref)
|
| 105 |
|
| 106 |
+
if a1 <= 0 or a2 <= 0: raise ValueError("從數據導出的斜率非物理值 (≤0)。")
|
| 107 |
V1_est, V2_est, t0_est = 1.0 / a1, 1.0 / a2, b2
|
| 108 |
|
| 109 |
+
if V2_est <= V1_est: raise ValueError("反演得到 V2 ≤ V1,幾何可能不足或假設不適用。")
|
| 110 |
|
| 111 |
ic_est = math.asin(min(0.999999999999, V1_est / V2_est))
|
| 112 |
h1_est = (t0_est * V1_est) / (2.0 * math.cos(ic_est))
|
|
|
|
| 166 |
buf.seek(0)
|
| 167 |
return Image.open(buf).convert("RGBA")
|
| 168 |
|
| 169 |
+
def plot_inversion_exercise_png(true_data: Dict, guess_data: Dict):
|
| 170 |
fig, ax = plt.subplots(figsize=(7, 5), dpi=160)
|
| 171 |
|
| 172 |
ax.plot(true_data['x'], true_data['t'], 'k--', linewidth=2, label="Original T-X Curve (Problem)")
|
|
|
|
| 177 |
|
| 178 |
ax.set_xlim(0, max(true_data['x']))
|
| 179 |
ax.set_ylim(0, max(true_data['t']) * 1.1)
|
| 180 |
+
ax.set(xlabel="Epicentral Distance (km)", ylabel="P-wave Travel Time (s)", title="T-X Plot: Your Guess vs. Problem")
|
| 181 |
ax.grid(True, linestyle="--", alpha=0.4)
|
| 182 |
ax.legend()
|
| 183 |
fig.tight_layout()
|
|
|
|
| 188 |
return Image.open(buf).convert("RGBA")
|
| 189 |
|
| 190 |
# =============================
|
| 191 |
+
# 4. Gradio UI Callback Functions (I18N: Chinese Text Output)
|
| 192 |
# =============================
|
| 193 |
def simulate_refraction(V1, V2, h1, length, n_geophones, plot_reflection_toggle):
|
| 194 |
try:
|
|
|
|
| 211 |
|
| 212 |
try:
|
| 213 |
V1_est, V2_est, h1_est = invert_from_first_arrivals(x, t_first, which_first)
|
| 214 |
+
inv_md = (f"### 反演結果 (由初達)\n- V1_est ≈ **{V1_est:.2f} 公里/秒**\n- V2_est ≈ **{V2_est:.2f} 公里/秒**\n- h1_est ≈ **{h1_est:.2f} 公里**\n")
|
| 215 |
except Exception as e:
|
| 216 |
+
inv_md = f"### 反演結果\n- 無法反演:{e}"
|
| 217 |
|
| 218 |
ic_deg = math.degrees(critical_angle(model.V1, model.V2))
|
| 219 |
+
info_md = (f"### 模型摘要\n- 輸入:V1={model.V1:.2f} 公里/秒,V2={model.V2:.2f} 公里/秒,h1={model.h1:.2f} 公里\n- 臨界角 ic ≈ **{ic_deg:.2f}°**\n- 截距時間 t0 ≈ **{t0:.4f} s**\n- 交會距離 x_c ≈ **{x_c:.2f} 公里**\n")
|
| 220 |
|
| 221 |
if plot_reflection_toggle and t0_reflection is not None:
|
| 222 |
+
info_md += f"- **同時顯示反射波**\n - 假設反射界面深度={h1:.2f} 公里,上層速度={V1:.2f} 公里/秒\n - 零位移反射時間 t0(ref) ≈ **{t0_reflection:.4f} s**\n"
|
| 223 |
|
| 224 |
pil_img = plot_combined_png(x, t_direct, t_refrac, t_first, which_first, t_reflect=t_reflect_data)
|
| 225 |
|
|
|
|
| 234 |
return pil_img, info_md + "\n" + inv_md, df, tmp_csv.name
|
| 235 |
|
| 236 |
except Exception as e:
|
| 237 |
+
return None, f"❌ 錯誤:{e}", None, None
|
| 238 |
|
| 239 |
|
| 240 |
def simulate_reflection(V1, h1, length, n_geophones):
|
|
|
|
| 248 |
t_direct = x / model.V1
|
| 249 |
t_reflect, t0 = reflection_tx_hyperbola(model.V1, model.h1, x)
|
| 250 |
|
| 251 |
+
info_md = (f"### 模型摘要\n- 輸入:V1={model.V1:.2f} 公里/秒,反射界面深度 h1={model.h1:.2f} 公里\n- 零位移二次程時間 t0 = **{t0:.4f} s**\n### 說明\n- 單一水平反射界面、單層速度的簡化 NMO 超曲線: t(x) = √(t0² + (x/V1)²)")
|
| 252 |
pil_img = plot_reflection_png(x, t_direct, t_reflect, t0)
|
| 253 |
df = pd.DataFrame({"x_km": x, "t_direct_s": t_direct, "t_reflection_s": t_reflect})
|
| 254 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".csv", mode='w', newline='') as tmp_csv:
|
|
|
|
| 256 |
return pil_img, info_md, df, tmp_csv.name
|
| 257 |
|
| 258 |
except Exception as e:
|
| 259 |
+
return None, f"❌ 錯誤:{e}", None, None
|
| 260 |
|
|
|
|
| 261 |
REFRACTION_PRESETS: Dict[str, Dict] = {
|
| 262 |
+
"工程地質 / 岩盤面": dict(V1=0.5, V2=2.5, h1=0.006, length=0.3, n_geophones=61),
|
| 263 |
+
"水文地質 / 地下水面": dict(V1=0.4, V2=1.6, h1=0.004, length=0.2, n_geophones=41),
|
| 264 |
+
"寒區工程 / 永凍土": dict(V1=0.7, V2=3.0, h1=0.0015, length=0.15, n_geophones=51),
|
| 265 |
+
"地殼尺度 / 莫霍面模型": dict(V1=6.0, V2=8.0, h1=15.0, length=200.0, n_geophones=81),
|
| 266 |
}
|
| 267 |
def fill_refraction_preset(key: str):
|
| 268 |
p = REFRACTION_PRESETS[key]
|
|
|
|
| 284 |
guess_data = {'x': x_km, 't_direct': t_direct, 't_refrac': t_refrac, 't_first': t_first}
|
| 285 |
|
| 286 |
feedback_md = (
|
| 287 |
+
f"### 你的猜測結果\n"
|
| 288 |
+
f"- 交會距離 x_c ≈ **{x_c:.2f} 公里**\n"
|
| 289 |
+
f"- 截距時間 t0 ≈ **{t0:.2f} s**\n"
|
| 290 |
f"--- \n"
|
| 291 |
+
f"**提示:** 調整 $V_1$ 會改變第一段的斜率;調整 $V_2$ 會改變第二段的斜率;調整 $h_1$ 主要會改變截距時間 $t_0$ 和交會距離。"
|
| 292 |
)
|
| 293 |
pil_img = plot_inversion_exercise_png(TRUE_TX_DATA, guess_data)
|
| 294 |
return pil_img, feedback_md
|
| 295 |
except Exception as e:
|
| 296 |
+
return None, f"❌ 錯誤:{e}"
|
| 297 |
|
| 298 |
|
| 299 |
# =============================
|
| 300 |
+
# 5. Gradio UI Layout (I18N: Chinese UI)
|
| 301 |
# =============================
|
| 302 |
+
OVERVIEW_PRINCIPLES_MD = """
|
| 303 |
+
### 地震震測法概述 (Seismic Methods Overview)
|
| 304 |
+
地震震測法利用人造震源產生的震波在地下傳播、反射與折射,再被地表的檢波器接收。透過分析震波的走時 (Travel Time) 與接收位置 (Offset),可以反演出地下地層的構造與速度參數。本演示器著重於兩種最基本的震測法:**折射震測** 和 **反射震測**。
|
| 305 |
+
#### **1. 折射震測原理 (Seismic Refraction Principles)**
|
| 306 |
+
折射震測主要用於探測近地表地層的速度分佈和界面深度,特別是在存在明顯速度差異(通常是下層速度大於上層速度)的界面。
|
| 307 |
+
* **波路徑 (Wave Paths):**
|
| 308 |
+
* **直接波 (Direct Wave):** 震波沿著地表淺層(第一層介質)直接傳播到檢波器。其走時 $t_{\\text{direct}} = x / V_1$,呈直線關係。
|
| 309 |
+
* **折射波 (Head Wave / Critical Refraction):** 震波從震源發出,在傳播到第一層與第二層的界面時,若入射角達到「臨界角」(Critical Angle) $i_c$,震波會在界面處產生折射,並沿著界面以第二層速度 $V_2$ 傳播,同時向上層發射出「頭波」(Head Wave)被檢波器接收。
|
| 310 |
+
* **臨界角 $i_c$:** 遵循 Snell's Law,當 $V_2 > V_1$ 時,$i_c = \\arcsin(V_1 / V_2)$。
|
| 311 |
+
* **折射波走時 $t_{\\text{refracted}}$:** 通常表示為 $t_{\\text{refracted}} = x / V_2 + t_0$,其中 $t_0$ 為「截距時間」(Intercept Time):$t_0 = \\frac{2h_1 \\cos(i_c)}{V_1}$。
|
| 312 |
+
* **T-X 圖 (Travel Time - Offset Plot):**
|
| 313 |
+
* 在 T-X 圖上,直接波表現為過原點的直線,斜率為 $1/V_1$。
|
| 314 |
+
* 折射波表現為一條斜率較小(因為 $V_2 > V_1$)的直線,其截距為 $t_0$。
|
| 315 |
+
* 在近距離,直接波先到達;超過某個「交會距離」(Crossover Distance) 後,折射波會先到達。透過分析這些初達波 (First Arrivals),可以反演出 $V_1, V_2, h_1$。
|
| 316 |
+
#### **2. 反射震測原理 (Seismic Reflection Principles)**
|
| 317 |
+
反射震測主要用於獲得地下精細的地層構造圖,類似於超音波成像。震波在遇到不同地質介質(阻抗差異)的界面時會產生反射。
|
| 318 |
+
* **波路徑:** 震波從震源發出,在界面處反射,然後被檢波器接收。
|
| 319 |
+
* **走時關係:** 對於單一水平反射界面,震源和檢波器之間的距離為 $x$,界面深度為 $h_1$,上覆地層速度為 $V_1$:
|
| 320 |
+
* **零位移反射時間 $t_0$:** 當 $x=0$ 時(震源和檢波器在同一點),$t_0 = 2h_1 / V_1$。
|
| 321 |
+
* **反射波走時 $t(x)$ (NMO 超曲線):** $t(x) = \\sqrt{t_0^2 + (x/V_1)^2}$。這是一個雙曲線 (Hyperbola) 形狀,稱為「正常時差」(Normal Moveout, NMO) 超曲線。
|
| 322 |
+
* **T-X 圖:** 反射波在 T-X 圖上呈現為一個以 $t_0$ 為頂點的超曲線。分析其形狀可以反演出地層速度和深度。
|
| 323 |
+
---
|
| 324 |
+
"""
|
| 325 |
+
REFERENCE_LINKS_MD = """
|
| 326 |
+
### 參考資料 / 延伸閱讀
|
| 327 |
+
- US EPA — Seismic Refraction:<https://www.epa.gov/environmental-geophysics/seismic-refraction>
|
| 328 |
+
- USGS TWRI 2-D2 — *Application of Seismic-Refraction Techniques to Hydrologic Studies*:<https://pubs.usgs.gov/twri/twri2d2/pdf/twri_2-d2.pdf>
|
| 329 |
+
- CLU-IN — *Seismic Reflection and Refraction – Geophysical Methods*:<https://www.cluin.org/characterization/technologies/default2.focus/sec/Geophysical_Methods/cat/Seismic_Reflection_and_Refraction/>
|
| 330 |
+
- SEG Wiki — *Seismic refraction*(入門與教學圖):<https://wiki.seg.org/wiki/Seismic_refraction>
|
| 331 |
+
- UBC Open Textbook — *Geophysics for Practicing Geoscientists*(Seismic Refraction 章節):<https://opentextbc.ca/geophysics/chapter/seismic-refraction/>
|
| 332 |
+
- SEG Wiki — *Normal moveout*:<https://wiki.seg.org/wiki/Normal_moveout>
|
| 333 |
+
- SEG Wiki — *Seismic reflection*:<https://wiki.seg.org/wiki/Seismic_reflection>
|
| 334 |
+
---
|
| 335 |
+
"""
|
| 336 |
+
SOLUTION_MD = """
|
| 337 |
+
### 參考解答與說明
|
| 338 |
+
1. **地殼速度 ($V_1$)**:
|
| 339 |
+
* 從圖中第一段直線(直接波)讀取,它通過 (0 km, 0 s) 和交會點 (30 km, 5 s)。
|
| 340 |
+
* $V_1 = \\Delta x / \\Delta t = (30 - 0) \\, \\text{km} / (5 - 0) \\, \\text{s} = \\mathbf{6.0 \\, \\text{km/s}}$。
|
| 341 |
+
2. **地函速度 ($V_2$)**:
|
| 342 |
+
* 從圖中第二段直線(折射波)讀取斜率,它通過 (30 km, 5 s) 和 (100 km, 13 s)。
|
| 343 |
+
* 斜率 $m_2 = \\Delta t / \\Delta x = (13 - 5) \\, \\text{s} / (100 - 30) \\, \\text{km} = 8 \\, \\text{s} / 70 \\, \\text{km}$。
|
| 344 |
+
* $V_2 = 1 / m_2 = 70 / 8 = \\mathbf{8.75 \\, \\text{km/s}}$。
|
| 345 |
+
3. **地殼厚度 ($h_1$)**:
|
| 346 |
+
* 首先計算截距時間 $t_0$。將第二段直線反向延伸至 $x=0$。
|
| 347 |
+
* 使用點斜式:$t - t_1 = m_2 (x - x_1) \\Rightarrow t - 5 = (8/70)(x - 30)$。
|
| 348 |
+
* 當 $x=0$ 時,$t_0 = 5 - (8/70) \\times 30 = 5 - 24/7 \\approx 1.57 \\, \\text{s}$。
|
| 349 |
+
* 使用公式 $h_1 = \\frac{t_0 V_1 V_2}{2\\sqrt{V_2^2 - V_1^2}}$。
|
| 350 |
+
* $h_1 = \\frac{1.57 \\times 6.0 \\times 8.75}{2\\sqrt{8.75^2 - 6.0^2}} \\approx \\frac{82.425}{2\\sqrt{76.56 - 36}} \\approx \\frac{82.425}{2 \\times 6.37} \\approx \\mathbf{16.5 \\, \\text{km}}$。
|
| 351 |
+
4. **推論**:
|
| 352 |
+
* 地殼 P 波速度約 6.0 km/s,地函 P 波速度約 8.75 km/s,這與大陸或海洋地殼的典型速度相符。
|
| 353 |
+
* 然而,地殼厚度約 16.5 km,這比典型的海洋地殼(約 5-10 km)厚,但比典型的大陸地殼(約 30-50 km)薄很多。
|
| 354 |
+
* 因此,這可能代表**過渡型地殼**,例如大陸棚、大陸裂谷,或是一個較厚的海洋地殼區域(如冰島)。
|
| 355 |
+
"""
|
| 356 |
|
| 357 |
theme = gr.themes.Soft(primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.sky).set(
|
| 358 |
body_background_fill="#f8f9fa", block_background_fill="white", block_radius="16px", block_shadow="0 4px 12px rgba(0,0,0,.08)")
|
| 359 |
|
| 360 |
with gr.Blocks(theme=theme, css=".markdown h2, .markdown h3 { margin-top: .6rem; } .footer-note { font-size: 12px; color: #6b7280; }") as demo:
|
| 361 |
+
gr.Markdown("# 折射 & 反射 震測原理演示器")
|
| 362 |
+
|
| 363 |
with gr.Tabs():
|
| 364 |
+
with gr.Tab("概述 Overview"):
|
| 365 |
+
gr.Markdown(
|
| 366 |
+
"""
|
| 367 |
+
**目標**:以簡化二層水平模型(折射)與單層水平反射界面(反射),說明近地表地震方法的走時幾何與參數反演。
|
| 368 |
+
**注意**:此為教學簡化模型。真實場域常見側向變化、多層、低速夾層、速度反轉與噪音;需採用更進階處理(多層射線追蹤、折射層析、RMS/NMO/疊加),或與 **GPR / MASW / 電阻率** 等多方法聯合解譯。
|
| 369 |
+
""",
|
| 370 |
+
elem_classes=["footer-note"]
|
| 371 |
+
)
|
| 372 |
+
gr.Markdown(OVERVIEW_PRINCIPLES_MD)
|
| 373 |
+
gr.Markdown(REFERENCE_LINKS_MD)
|
| 374 |
+
|
| 375 |
+
with gr.Tab("折射 Refraction 模擬器"):
|
| 376 |
with gr.Row():
|
| 377 |
with gr.Column(scale=1):
|
| 378 |
+
V1_r = gr.Number(label="V1 (公里/秒)", value=1.5, precision=2)
|
| 379 |
+
V2_r = gr.Number(label="V2 (公里/秒)", value=3.0, precision=2)
|
| 380 |
+
h1_r = gr.Number(label="h1 (公里)", value=0.02, precision=3)
|
| 381 |
+
length_r = gr.Number(label="測線長度 (公里)", value=1.0, precision=2)
|
| 382 |
+
n_r = gr.Slider(label="檢波器數量", value=51, minimum=2, maximum=401, step=1)
|
| 383 |
+
plot_reflection_toggle_r = gr.Checkbox(label="同時顯示反射波", value=False)
|
| 384 |
+
run_r = gr.Button("🚀 執行折射模擬", variant="primary")
|
|
|
|
| 385 |
with gr.Column(scale=2):
|
| 386 |
+
out_img_r = gr.Image(label="T-X 圖 (英文)", type="pil")
|
| 387 |
out_info_r = gr.Markdown()
|
| 388 |
+
out_df_r = gr.Dataframe(label="合成資料 (可下載 CSV)")
|
| 389 |
+
out_csv_r = gr.File(label="下載 CSV")
|
| 390 |
run_r.click(simulate_refraction, inputs=[V1_r, V2_r, h1_r, length_r, n_r, plot_reflection_toggle_r], outputs=[out_img_r, out_info_r, out_df_r, out_csv_r])
|
| 391 |
|
| 392 |
+
with gr.Tab("反射 Reflection 模擬器"):
|
| 393 |
with gr.Row():
|
| 394 |
with gr.Column(scale=1):
|
| 395 |
+
V1_s = gr.Number(label="V1 (公里/秒)", value=1.6, precision=2)
|
| 396 |
+
h1_s = gr.Number(label="反射界面深度 h1 (公里)", value=0.02, precision=3)
|
| 397 |
+
length_s = gr.Number(label="測線長度 (公里)", value=0.6, precision=2)
|
| 398 |
+
n_s = gr.Slider(label="檢波器數量", value=61, minimum=2, maximum=401, step=1)
|
| 399 |
+
run_s = gr.Button("🚀 執行反射模擬", variant="primary")
|
|
|
|
| 400 |
with gr.Column(scale=2):
|
| 401 |
+
out_img_s = gr.Image(label="T-X 圖 (英文)", type="pil")
|
| 402 |
out_info_s = gr.Markdown()
|
| 403 |
+
out_df_s = gr.Dataframe(label="合成資料 (可下載 CSV)")
|
| 404 |
+
out_csv_s = gr.File(label="下載 CSV")
|
| 405 |
run_s.click(simulate_reflection, inputs=[V1_s, h1_s, length_s, n_s], outputs=[out_img_s, out_info_s, out_df_s, out_csv_s])
|
| 406 |
|
| 407 |
+
with gr.Tab("應用示範 (折射)"):
|
| 408 |
with gr.Row():
|
| 409 |
with gr.Column(scale=1):
|
| 410 |
+
preset_choice = gr.Radio(list(REFRACTION_PRESETS.keys()), label="情境", value="工程地質 / 岩盤面")
|
| 411 |
+
load_btn = gr.Button("載入參數")
|
| 412 |
+
run_preset_btn = gr.Button("一鍵執行")
|
| 413 |
with gr.Column(scale=2):
|
| 414 |
+
V1_p = gr.Number(label="V1 (公里/秒)", interactive=True)
|
| 415 |
+
V2_p = gr.Number(label="V2 (公里/秒)", interactive=True)
|
| 416 |
+
h1_p = gr.Number(label="h1 (公里)", interactive=True)
|
| 417 |
+
length_p = gr.Number(label="測線長度 (公里)", interactive=True)
|
| 418 |
+
n_p = gr.Slider(label="檢波器數量", minimum=2, maximum=401, step=1, value=61, interactive=True)
|
| 419 |
+
plot_reflection_toggle_p = gr.Checkbox(label="同時顯示反射波", value=False)
|
|
|
|
| 420 |
|
| 421 |
load_btn.click(fill_refraction_preset, inputs=[preset_choice], outputs=[V1_p, V2_p, h1_p, length_p, n_p])
|
| 422 |
|
| 423 |
with gr.Row():
|
| 424 |
with gr.Column(scale=1):
|
| 425 |
+
out_img_p = gr.Image(label="T-X 圖 (英文)", type="pil")
|
| 426 |
with gr.Column(scale=1):
|
| 427 |
out_info_p = gr.Markdown()
|
| 428 |
+
out_df_p = gr.Dataframe(label="合成資料")
|
| 429 |
+
out_csv_p = gr.File(label="下載 CSV")
|
| 430 |
|
| 431 |
run_preset_btn.click(simulate_refraction, inputs=[V1_p, V2_p, h1_p, length_p, n_p, plot_reflection_toggle_p], outputs=[out_img_p, out_info_p, out_df_p, out_csv_p])
|
| 432 |
|
| 433 |
+
with gr.Tab("實作練習:走時曲線反演"):
|
| 434 |
with gr.Row():
|
| 435 |
with gr.Column(scale=1):
|
| 436 |
+
gr.Markdown("### 題目")
|
| 437 |
+
gr.Image("problem.jpg", label="題目走時曲線圖", show_download_button=False)
|
| 438 |
+
gr.Markdown("""
|
| 439 |
+
上圖所示為 P 波之走時曲線圖,假設地震發生於地表,橫軸是震央距離(公里),縱軸為 P 波走時(秒),如果主要的震波速度變化在地殼與地函交界,請問:
|
| 440 |
+
1. 地殼及地函的震波速度各為多少?
|
| 441 |
+
2. 地殼的厚度又是多少?
|
| 442 |
+
3. 就你所得的結果推測,此為海洋板塊或是大陸板塊?
|
| 443 |
+
""")
|
| 444 |
with gr.Column(scale=2):
|
| 445 |
+
gr.Markdown("### 互動正演沙盒")
|
| 446 |
+
gr.Markdown("在此調整你猜測的模型參數,看看產生的走時曲線是否能與題目中的曲線吻合!")
|
| 447 |
with gr.Row():
|
| 448 |
+
v1_guess = gr.Slider(minimum=3.0, maximum=8.0, value=5.0, step=0.1, label="猜測的地殼速度 V1 (公里/秒)")
|
| 449 |
+
v2_guess = gr.Slider(minimum=6.0, maximum=10.0, value=7.5, step=0.1, label="猜測的地函速度 V2 (公里/秒)")
|
| 450 |
+
h1_guess = gr.Slider(minimum=5.0, maximum=40.0, value=20.0, step=0.5, label="猜測的地殼厚度 h1 (公里)")
|
|
|
|
| 451 |
|
| 452 |
+
plot_exercise = gr.Image(label="疊加比較圖 (英文)", type="pil")
|
| 453 |
feedback_exercise = gr.Markdown()
|
| 454 |
|
| 455 |
+
with gr.Accordion("顯示/隱藏參考答案", open=False):
|
| 456 |
+
gr.Markdown(SOLUTION_MD)
|
| 457 |
|
| 458 |
for slider in [v1_guess, v2_guess, h1_guess]:
|
| 459 |
slider.change(
|
|
|
|
| 463 |
show_progress="hidden"
|
| 464 |
)
|
| 465 |
|
| 466 |
+
gr.Markdown("--- 由 **Seismology Demonstrator** 提供 ---", elem_classes=["footer-note"])
|
| 467 |
|
| 468 |
if __name__ == "__main__":
|
| 469 |
demo.launch()
|