cwadayi commited on
Commit
8138ef9
·
verified ·
1 Parent(s): 2d896c5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +162 -101
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # app.py (Version 2.4 - English Plots & Kilometer Units)
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.4 changes:
10
- 1. (Units) Converted all inputs, calculations, and displays from meters to kilometers.
11
- 2. (I18N) Translated all plot titles, labels, and legends to English.
12
- 3. Removed the CJK font setup as it is no longer needed for the plots.
 
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 # UNITS: Now in km/s
37
- V2: float # UNITS: Now in km/s
38
- h1: float # UNITS: Now in km
39
 
40
  def validate(self):
41
  if not (self.V1 > 0 and self.V2 > 0 and self.h1 > 0):
42
- raise ValueError("Model parameters (V1, V2, h1) must be positive.")
43
  if self.V2 <= self.V1:
44
- raise ValueError("V2 must be greater than V1 for critical refraction.")
45
 
46
  @dataclass
47
  class ModelReflectionSimple:
48
- V1: float # UNITS: Now in km/s
49
- h1: float # UNITS: Now in km
50
 
51
  def validate(self):
52
  if not (self.V1 > 0 and self.h1 > 0):
53
- raise ValueError("Model parameters (V1, h1) must be positive.")
54
 
55
  @dataclass
56
  class SurveyLine:
57
- length: float # UNITS: Now in km
58
  n_geophones: int
59
 
60
  def validate(self):
61
  if self.length <= 0:
62
- raise ValueError("Survey length must be positive.")
63
  if self.n_geophones < 2:
64
- raise ValueError("Number of geophones must be at least 2.")
65
 
66
  def positions(self) -> np.ndarray:
67
  return np.linspace(0.0, self.length, self.n_geophones)
68
 
69
- # --- Physics functions now operate in km and km/s ---
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("Not enough points for linear regression.")
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("Not enough direct wave arrivals for inversion.")
100
- if x_ref.size < 2: raise ValueError("Not enough refracted wave arrivals for inversion. Increase survey length or layer thickness.")
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("Non-physical slope (<=0) derived from data.")
106
  V1_est, V2_est, t0_est = 1.0 / a1, 1.0 / a2, b2
107
 
108
- if V2_est <= V1_est: raise ValueError("Inversion resulted in V2 <= V1, geometry may be insufficient.")
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, title: str = "T-X Plot: Your Guess vs. Problem"):
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=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"### Inversion Results (from First Arrivals)\n- V1_est ≈ **{V1_est:.2f} km/s**\n- V2_est ≈ **{V2_est:.2f} km/s**\n- h1_est ≈ **{h1_est:.2f} km**\n")
214
  except Exception as e:
215
- inv_md = f"### Inversion Results\n- Inversion failed: {e}"
216
 
217
  ic_deg = math.degrees(critical_angle(model.V1, model.V2))
218
- info_md = (f"### Model Summary\n- Input: V1={model.V1:.2f} km/s, V2={model.V2:.2f} km/s, h1={model.h1:.2f} km\n- Critical Angle ic ≈ **{ic_deg:.2f}°**\n- Intercept Time t0 ≈ **{t0:.4f} s**\n- Crossover Distance x_c ≈ **{x_c:.2f} km**\n")
219
 
220
  if plot_reflection_toggle and t0_reflection is not None:
221
- info_md += f"- **Also showing reflection**\n - Assumed reflector depth={h1:.2f} km, velocity={V1:.2f} km/s\n - Reflection t0 ≈ **{t0_reflection:.4f} s**\n"
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"❌ Error: {e}", None, None
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"### Model Summary\n- Input: V1={model.V1:.2f} km/s, Reflector Depth h1={model.h1:.2f} km\n- Zero-offset time t0 = **{t0:.4f} s**\n### Notes\n- Simplified NMO Hyperbola: t(x) = √(t0² + (x/V1)²)")
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"❌ Error: {e}", None, None
259
 
260
- # UNITS: All presets updated to km and km/s
261
  REFRACTION_PRESETS: Dict[str, Dict] = {
262
- "Engineering Geology / Bedrock Rippability": dict(V1=0.5, V2=2.5, h1=0.006, length=0.3, n_geophones=61),
263
- "Hydrogeology / Water Table": dict(V1=0.4, V2=1.6, h1=0.004, length=0.2, n_geophones=41),
264
- "Permafrost Engineering": dict(V1=0.7, V2=3.0, h1=0.0015, length=0.15, n_geophones=51),
265
- "Crustal Scale / Moho Model": 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,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"### Results for Your Guess\n"
288
- f"- Crossover Distance x_c ≈ **{x_c:.2f} km**\n"
289
- f"- Intercept Time t0 ≈ **{t0:.2f} s**\n"
290
  f"--- \n"
291
- f"**Hint:** Adjusting $V_1$ changes the first slope; $V_2$ changes the second slope; $h_1$ changes the intercept time and crossover distance."
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"❌ Error: {e}"
297
 
298
 
299
  # =============================
300
- # 5. Gradio UI Layout
301
  # =============================
302
- OVERVIEW_PRINCIPLES_MD = "..." # Content omitted for brevity
303
- REFERENCE_LINKS_MD = "..." # Content omitted for brevity
304
- SOLUTION_MD = "..." # Content omitted for brevity
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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("# Refraction & Reflection Seismology Demonstrator")
 
311
  with gr.Tabs():
312
- with gr.Tab("Overview"):
313
- # UI content...
314
- pass
315
- with gr.Tab("Refraction Simulator"):
 
 
 
 
 
 
 
 
316
  with gr.Row():
317
  with gr.Column(scale=1):
318
- # UNITS: All labels and values changed to km, km/s
319
- V1_r = gr.Number(label="V1 (km/s)", value=1.5, precision=2)
320
- V2_r = gr.Number(label="V2 (km/s)", value=3.0, precision=2)
321
- h1_r = gr.Number(label="h1 (km)", value=0.02, precision=3)
322
- length_r = gr.Number(label="Survey Length (km)", value=1.0, precision=2)
323
- n_r = gr.Slider(label="Number of Geophones", value=51, minimum=2, maximum=401, step=1)
324
- plot_reflection_toggle_r = gr.Checkbox(label="Overlay Reflection Plot", value=False)
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 Plot (Refraction / Combined)", type="pil")
328
  out_info_r = gr.Markdown()
329
- out_df_r = gr.Dataframe(label="Synthetic Data (Downloadable CSV)")
330
- out_csv_r = gr.File(label="Download CSV")
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 Simulator"):
334
  with gr.Row():
335
  with gr.Column(scale=1):
336
- # UNITS: All labels and values changed to km, km/s
337
- V1_s = gr.Number(label="V1 (km/s)", value=1.6, precision=2)
338
- h1_s = gr.Number(label="Reflector Depth h1 (km)", value=0.02, precision=3)
339
- length_s = gr.Number(label="Survey Length (km)", value=0.6, precision=2)
340
- n_s = gr.Slider(label="Number of Geophones", value=61, minimum=2, maximum=401, step=1)
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 Plot (Reflection)", type="pil")
344
  out_info_s = gr.Markdown()
345
- out_df_s = gr.Dataframe(label="Synthetic Data (Downloadable CSV)")
346
- out_csv_s = gr.File(label="Download CSV")
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("Application Presets (Refraction)"):
350
  with gr.Row():
351
  with gr.Column(scale=1):
352
- preset_choice = gr.Radio(list(REFRACTION_PRESETS.keys()), label="Scenario", value="Engineering Geology / Bedrock Rippability")
353
- load_btn = gr.Button("Load Parameters")
354
- run_preset_btn = gr.Button("Run with Preset")
355
  with gr.Column(scale=2):
356
- # UNITS: All labels and values changed to km, km/s
357
- V1_p = gr.Number(label="V1 (km/s)", interactive=True)
358
- V2_p = gr.Number(label="V2 (km/s)", interactive=True)
359
- h1_p = gr.Number(label="h1 (km)", interactive=True)
360
- length_p = gr.Number(label="Survey Length (km)", interactive=True)
361
- n_p = gr.Slider(label="Number of Geophones", minimum=2, maximum=401, step=1, value=61, interactive=True)
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 Plot (Refraction / Combined)", type="pil")
369
  with gr.Column(scale=1):
370
  out_info_p = gr.Markdown()
371
- out_df_p = gr.Dataframe(label="Synthetic Data")
372
- out_csv_p = gr.File(label="Download CSV")
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("Interactive Exercise: T-X Inversion"):
377
  with gr.Row():
378
  with gr.Column(scale=1):
379
- gr.Markdown("### Problem")
380
- gr.Image("problem.jpg", label="Problem T-X Curve", show_download_button=False)
381
- gr.Markdown("The P-wave travel-time curve is shown above. Assuming the main velocity change occurs at the crust-mantle boundary, please determine: ...") # Simplified problem text
 
 
 
 
 
382
  with gr.Column(scale=2):
383
- gr.Markdown("### Interactive Forward-Modeling Sandbox")
384
- gr.Markdown("Adjust the parameters to see if your modeled T-X curve matches the problem's curve!")
385
  with gr.Row():
386
- # UNITS: All labels and values changed to km, km/s
387
- v1_guess = gr.Slider(minimum=3.0, maximum=8.0, value=5.0, step=0.1, label="Guessed Crustal Velocity V1 (km/s)")
388
- v2_guess = gr.Slider(minimum=6.0, maximum=10.0, value=7.5, step=0.1, label="Guessed Mantle Velocity V2 (km/s)")
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="Your Guess (Color) vs. Problem (Dashed Black)", type="pil")
392
  feedback_exercise = gr.Markdown()
393
 
394
- with gr.Accordion("Show/Hide Reference Solution", open=False):
395
- gr.Markdown("...") # Solution Markdown content
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("--- Provided by the **Seismology Demonstrator** ---", elem_classes=["footer-note"])
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()