cwadayi commited on
Commit
c4e778c
·
verified ·
1 Parent(s): 210db34

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -127
app.py CHANGED
@@ -4,181 +4,189 @@ import matplotlib.pyplot as plt
4
 
5
  # --- 輔助函數:產生 Ricker 震波 ---
6
  def ricker_wavelet(t, f=25.0):
7
- """ 產生一個 Ricker 震波 (墨西哥帽函數) """
8
- t = t - 2.0 / f # 將震波峰值對齊時間點
9
  p = (np.pi * f * t) ** 2
10
  return (1 - 2 * p) * np.exp(-p)
11
 
12
  # --- 核心計算與繪圖函數 ---
13
- def plot_seismic_exploration(v1, v2, h, x_max, num_receivers, gain):
14
- """
15
- 根據輸入的地層參數,計算並繪製震測的走時曲線與視覺化震測剖面圖。
16
- """
17
- # === PART 1: 物理計算 ===
18
- if v2 <= v1:
19
- fig1, ax1 = plt.subplots(figsize=(10, 6))
20
- ax1.text(0.5, 0.5, 'Error: V2 must be greater than V1', ha='center', va='center', color='red')
21
- ax1.set_title("Travel-Time Curve")
22
- fig2, ax2 = plt.subplots(figsize=(10, 5))
23
- ax2.text(0.5, 0.5, 'Please ensure V2 > V1 to generate a valid model.', ha='center', va='center', color='red')
24
- ax2.set_title("Visualized Seismic Profile")
25
- return fig1, fig2, "### 參數錯誤\n請確保第二層速度 (V2) 大於第一層速度 (V1)。"
26
-
27
- theta_c_rad = np.arcsin(v1 / v2)
28
- theta_c_deg = np.rad2deg(theta_c_rad)
29
- ti = (2 * h * np.cos(theta_c_rad)) / v1
30
- xc = 2 * h * np.sqrt((v2 + v1) / (v2 - v1))
31
- t0 = (2 * h) / v1
32
-
33
- # === PART 2: 繪製 T-X 走時曲線圖 (Plot 1) ===
 
 
 
 
 
 
 
 
 
 
 
 
34
  x_continuous = np.linspace(0, x_max, 500)
35
- t_direct = x_continuous / v1
36
- t_refracted = (x_continuous / v2) + ti
37
- t_reflected = np.sqrt(t0**2 + (x_continuous / v1)**2)
38
- t_first_arrival_continuous = np.minimum(t_direct, t_refracted)
39
 
40
  fig1, ax1 = plt.subplots(figsize=(10, 6))
41
- ax1.plot(x_continuous, t_direct, 'b--', label='Direct Wave')
42
- ax1.plot(x_continuous, t_refracted, 'g--', label='Refracted Wave')
43
- ax1.plot(x_continuous, t_reflected, 'm:', linewidth=2, label='Reflected Wave')
44
- ax1.plot(x_continuous, t_first_arrival_continuous, 'r-', linewidth=3, label='First Arrival')
45
- if xc < x_max:
46
- ax1.axvline(x=xc, color='k', linestyle=':', label=f'Crossover = {xc:.1f} m')
47
- ax1.set_title("1. Travel-Time (T-X) Curve", fontsize=16, loc='left')
48
- ax1.set_xlabel("Distance (m)")
49
- ax1.set_ylabel("Travel Time (s)")
 
 
 
 
 
 
 
50
  ax1.legend()
51
  ax1.grid(True)
52
  ax1.set_xlim(0, x_max)
53
- y_max = max(np.max(t_direct), np.max(t_reflected))
54
- ax1.set_ylim(0, y_max * 1.1)
55
 
56
- # === PART 3: 繪製視覺化震測剖面圖 (Plot 2) ===
57
  fig2, ax2 = plt.subplots(figsize=(10, 5))
58
  receiver_x = np.linspace(0, x_max, int(num_receivers))
59
-
60
- # 計算每個測站的抵達時間
61
- t_reflected_rx = np.sqrt(t0**2 + (receiver_x / v1)**2)
62
- t_first_arrival_rx = np.minimum(receiver_x / v1, (receiver_x / v2) + ti)
63
 
64
- # 繪製每一條帶有震波的震波線
65
- wavelet_duration = 0.08
66
  wavelet_t = np.linspace(0, wavelet_duration, 100)
67
 
68
  for i in range(int(num_receivers)):
69
- # 繪製反射波震波
70
- wavelet_amp_refl = ricker_wavelet(wavelet_t, f=40) * gain
71
- x_trace_refl = receiver_x[i] + wavelet_amp_refl
72
- y_trace_refl = t_reflected_rx[i] - wavelet_duration/2 + wavelet_t
73
- ax2.plot(x_trace_refl, y_trace_refl, 'k-', linewidth=0.8)
74
- ax2.fill_betweenx(y_trace_refl, receiver_x[i], x_trace_refl, where=(x_trace_refl > receiver_x[i]), color='black')
75
-
76
- # 繪製初達波震波
77
- wavelet_amp_first = ricker_wavelet(wavelet_t, f=30) * gain * 1.2
78
- x_trace_first = receiver_x[i] + wavelet_amp_first
79
- y_trace_first = t_first_arrival_rx[i] - wavelet_duration/2 + wavelet_t
80
- ax2.plot(x_trace_first, y_trace_first, 'r-', linewidth=1)
81
- ax2.fill_betweenx(y_trace_first, receiver_x[i], x_trace_first, where=(x_trace_first > receiver_x[i]), color='red', alpha=0.8)
82
-
83
- # 繪製地表、震源與測站
84
- ax2.axhline(0, color='brown', linewidth=2)
85
- ax2.plot(0, 0, 'r*', markersize=20, label='Source')
86
- ax2.plot(receiver_x, np.zeros_like(receiver_x), 'kv', markersize=8, label='Receivers')
87
-
88
- ax2.set_title(f"2. Visualized Seismic Profile ({int(num_receivers)} Traces)", fontsize=14, loc='left')
89
- ax2.set_xlabel("Distance (m)")
90
- ax2.set_ylabel("Two-Way Time (s)")
91
  ax2.set_xlim(-x_max * 0.05, x_max * 1.05)
92
- ax2.set_ylim(y_max * 1.1, -y_max*0.05)
93
 
94
- # 【*** FIX 1: Move legend to a better position ***】
95
- ax2.legend(loc='upper right', fontsize='small')
 
 
 
 
 
 
 
96
 
97
- # 【*** FIX 2: Manually adjust layout to prevent overlaps ***】
98
- fig1.tight_layout(pad=1.1)
99
- fig2.subplots_adjust(left=0.1, right=0.98, top=0.9, bottom=0.15)
100
-
101
-
102
- # === PART 4: 準備輸出的說明文字 ===
103
- results_md = f"""
104
- ### 🔬 分析結果
105
-
106
- 根據您設計的地層模型,我們計算出以下關鍵物理量:
107
 
108
- #### 折射波 (Refracted Wave)
109
- - **臨界角 (Critical Angle, θc)**: `arcsin({v1:.0f} / {v2:.0f})` = **{theta_c_deg:.2f}°**
110
- - **截時 (Intercept Time, tᵢ)**: `(2 * {h:.0f} * cos({theta_c_deg:.2f}°)) / {v1:.0f}` = **{ti*1000:.1f} ms**
111
- - **交越距離 (Crossover Distance, Xc)**: `2 * {h:.0f} * sqrt(...)` = **{xc:.1f} m**
 
 
 
112
 
113
- ---
114
- #### 反射波 (Reflected Wave)
115
- - **雙程走時 (Two-Way Time, t₀)**: `2 * {h:.0f} / {v1:.0f}` = **{t0*1000:.1f} ms**
116
- """
117
- return fig1, fig2, results_md
118
 
119
- # --- Gradio 介面設定 ---
120
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
121
- gr.Markdown(
122
- """
123
- # 地心震波奇幻之旅:地球物理遊樂場 🌍
124
- > 創意的發揮是一種學習,過程中,每個人同時是學生也是老師。
125
-
126
- 這個實驗室就是你的遊樂場。透過親手設計地層模型、佈放虛擬測站,你將不只是學習,更是在**創造和發現**地底下的物理法則。
127
- """
128
- )
129
 
130
  with gr.Row():
131
  with gr.Column(scale=1):
132
- gr.Markdown("### ⚙️ 1. 設計你的地層模型")
133
- v1_slider = gr.Slider(label="V1: 第一層速度 (m/s)", minimum=300, maximum=3000, value=800, step=50)
134
- v2_slider = gr.Slider(label="V2: 第二層速度 (m/s)", minimum=500, maximum=6000, value=2500, step=50)
135
- h_slider = gr.Slider(label="h: 第一層厚度 (m)", minimum=5, maximum=100, value=20, step=1)
 
 
 
 
 
136
 
137
- gr.Markdown("### ⚙️ 2. 佈放儀器與顯示設定")
138
- xmax_slider = gr.Slider(label="最大觀測距離 (m)", minimum=100, maximum=500, value=250, step=10)
139
- receivers_slider = gr.Slider(label="測站數量", minimum=5, maximum=100, value=40, step=1)
140
- # 【*** FIX 3: Lower default gain for clarity ***】
141
- gain_slider = gr.Slider(label="剖面增益 (Display Gain)", minimum=1, maximum=20, value=4, step=1)
142
 
143
- submit_btn = gr.Button("🚀 開始探勘!", variant="primary")
144
 
145
  with gr.Column(scale=2):
146
- gr.Markdown("### 📊 觀測結果")
147
- plot_output1 = gr.Plot(label="走時-距離圖 (T-X Plot)")
148
- plot_output2 = gr.Plot(label="視覺化震測剖面圖 (Visualized Seismic Profile)")
 
 
149
 
150
  with gr.Row():
151
- results_output = gr.Markdown("### 🔬 分析結果\n請設計你的地層模型並點擊「開始探勘!」以顯示計算結果。")
152
 
153
  # --- 事件監聽 ---
 
 
 
 
 
 
 
154
  submit_btn.click(
155
  fn=plot_seismic_exploration,
156
- inputs=[v1_slider, v2_slider, h_slider, xmax_slider, receivers_slider, gain_slider],
157
- outputs=[plot_output1, plot_output2, results_output]
158
  )
159
 
160
  gr.Markdown(
161
  """
162
  ---
163
- ### 📖 剖面圖是如何誕生的?
164
- 下方的 **視覺化震測剖面圖** 完美模擬了真實的探勘情���。每一條黑色的垂直線代表一個**測站 (Receiver)**,它記錄到的訊號就是一條帶有**震波 (Wiggle)** 的**震波線 (Trace)**。
165
-
166
- - **紅色震波** 代表 **初達波 (First Arrival)**,是能量最早抵達的波。
167
- - **黑色震波** 代表 **反射波 (Reflected Wave)**,它們來自地下介面的反射。
168
-
169
- 地球物理學家最重要的工作,就是在成千上萬條震波線中,**尋找並追蹤這些連續排列的震波(稱為「同相軸」)**。例如,圖中那條優美的黑色雙曲線同相軸,就清楚地標示出了地下第一層介面的位置!
170
-
171
- ### 🚀 探索與發現
172
- 1. **增益的效果**: 試著調整「剖面增益」,看看震波的振幅如何變化。在真實資料中,深層的反射信號很微弱,就需要提高增益才能看清楚。
173
- 2. **看見雙曲線**: 專注觀察剖面圖中的黑色震波。當你增加「測站數量」時,是不是能更清楚地「描繪」出那條對應到上方 T-X 圖的紫色雙曲線?
174
- 3. **初達波的威力**: 紅色的初達波在剖面圖中形成了一條明顯的分界線。觀察它的轉折點,思考一下這個轉折點(交越距離)告訴了我們關於地下速度結構的什麼資訊?
175
  """
176
  )
177
 
178
  gr.HTML("""
179
  <footer style="text-align:center; margin-top: 30px; color:grey;">
180
  <p>「創意的發揮是一種學習,過程中,每個人同時是學生也是老師。」</p>
181
- <p>地球物理遊樂場 &copy; 2025 - 由 Gemini 根據課程文件與靈感生成</p>
182
  </footer>
183
  """)
184
 
 
4
 
5
  # --- 輔助函數:產生 Ricker 震波 ---
6
  def ricker_wavelet(t, f=25.0):
7
+ t = t - 2.0 / f
 
8
  p = (np.pi * f * t) ** 2
9
  return (1 - 2 * p) * np.exp(-p)
10
 
11
  # --- 核心計算與繪圖函數 ---
12
+ def plot_seismic_exploration(v1, v2, v3, h1, h2, x_max, num_receivers, gain):
13
+ # === PART 1: 物理計算 (升級至三層模型) ===
14
+ # 物理條件檢查
15
+ valid_model = True
16
+ error_msg = ""
17
+ if v2 <= v1 or v3 <= v2:
18
+ valid_model = False
19
+ error_msg = "### 模型錯誤\n速度必須隨深度增加 (V3 > V2 > V1)。"
20
+
21
+ # 計算關鍵物理量 (第一層介面)
22
+ t0_1 = (2 * h1) / v1
23
+ # 計算關鍵物理量 (第二層介面)
24
+ t0_2 = (2 * h1 / v1) + (2 * h2 / v2)
25
+
26
+ # === PART 2: 繪製地質模型圖 (新圖表) ===
27
+ fig0, ax0 = plt.subplots(figsize=(10, 2))
28
+ ax0.set_xlim(0, x_max)
29
+ ax0.set_ylim(-(h1 + h2) * 1.5, 5)
30
+ ax0.axhline(0, color='brown', linewidth=3)
31
+ ax0.axhline(-h1, color='gray', linestyle='--')
32
+ ax0.axhline(-(h1+h2), color='darkgray', linestyle='--')
33
+ ax0.fill_between([0, x_max], 0, -h1, color='sandybrown', alpha=0.6)
34
+ ax0.fill_between([0, x_max], -h1, -(h1+h2), color='darkkhaki', alpha=0.6)
35
+ ax0.fill_between([0, x_max], -(h1+h2), -(h1 + h2) * 1.5, color='dimgray', alpha=0.6)
36
+ ax0.text(x_max/2, -h1/2, f'Layer 1\nV1 = {v1:.0f} m/s\nh1 = {h1:.0f} m', ha='center', va='center', fontsize=10, color='black')
37
+ ax0.text(x_max/2, -h1-h2/2, f'Layer 2\nV2 = {v2:.0f} m/s\nh2 = {h2:.0f} m', ha='center', va='center', fontsize=10, color='black')
38
+ ax0.text(x_max/2, -(h1+h2)*1.25, f'Layer 3 (Basement)\nV3 = {v3:.0f} m/s', ha='center', va='center', fontsize=10, color='white')
39
+ ax0.set_title("Geological Model Cross-section")
40
+ ax0.set_ylabel("Depth (m)")
41
+ ax0.set_yticks([0, -h1, -(h1+h2)])
42
+ ax0.set_xticks([])
43
+
44
+ # === PART 3: 繪製 T-X 走時曲線圖 ===
45
  x_continuous = np.linspace(0, x_max, 500)
46
+ # 反射波
47
+ t_refl_1 = np.sqrt(t0_1**2 + (x_continuous / v1)**2)
48
+ t_refl_2 = np.sqrt(t0_2**2 + (x_continuous / ((v1*h1 + v2*h2)/(h1+h2)) )**2) # RMS velocity approximation
 
49
 
50
  fig1, ax1 = plt.subplots(figsize=(10, 6))
51
+ ax1.plot(x_continuous, t_refl_1, 'm:', linewidth=2, label='Reflection 1 (from Layer 2)')
52
+ ax1.plot(x_continuous, t_refl_2, 'c:', linewidth=2, label='Reflection 2 (from Layer 3)')
53
+
54
+ # 折射波 (僅在速度增加時繪製)
55
+ if valid_model:
56
+ theta_c12 = np.arcsin(v1 / v2)
57
+ ti_12 = (2 * h1 * np.cos(theta_c12)) / v1
58
+ t_refr_12 = (x_continuous / v2) + ti_12
59
+ ax1.plot(x_continuous, t_refr_12, 'g--', label='Refraction (from Layer 2)')
60
+
61
+ theta_c23 = np.arcsin(v2 / v3)
62
+ ti_23 = 2 * h1 * np.cos(np.arcsin(v1/v3))/v1 + 2 * h2 * np.cos(theta_c23)/v2
63
+ t_refr_23 = (x_continuous / v3) + ti_23
64
+ ax1.plot(x_continuous, t_refr_23, 'y--', label='Refraction (from Layer 3)')
65
+
66
+ ax1.set_title("1. Travel-Time (T-X) Curve")
67
  ax1.legend()
68
  ax1.grid(True)
69
  ax1.set_xlim(0, x_max)
70
+ y_max = np.max(t_refl_2) * 1.1
71
+ ax1.set_ylim(0, y_max)
72
 
73
+ # === PART 4: 繪製視覺化震測剖面圖 ===
74
  fig2, ax2 = plt.subplots(figsize=(10, 5))
75
  receiver_x = np.linspace(0, x_max, int(num_receivers))
76
+ # 反射波到時
77
+ t_refl_1_rx = np.sqrt(t0_1**2 + (receiver_x / v1)**2)
78
+ t_refl_2_rx = np.sqrt(t0_2**2 + (receiver_x / ((v1*h1 + v2*h2)/(h1+h2)) )**2)
 
79
 
80
+ wavelet_duration = y_max / 10
 
81
  wavelet_t = np.linspace(0, wavelet_duration, 100)
82
 
83
  for i in range(int(num_receivers)):
84
+ # 繪製第一層反射
85
+ wavelet_amp_1 = ricker_wavelet(wavelet_t, f=40) * gain
86
+ x_trace_1 = receiver_x[i] + wavelet_amp_1
87
+ y_trace_1 = t_refl_1_rx[i] - wavelet_duration/2 + wavelet_t
88
+ ax2.plot(x_trace_1, y_trace_1, 'k-', linewidth=0.8)
89
+ ax2.fill_betweenx(y_trace_1, receiver_x[i], x_trace_1, where=(x_trace_1 > receiver_x[i]), color='black')
90
+
91
+ # 繪製第二層反射
92
+ wavelet_amp_2 = ricker_wavelet(wavelet_t, f=30) * gain * 0.8 # Deeper reflections are weaker
93
+ x_trace_2 = receiver_x[i] + wavelet_amp_2
94
+ y_trace_2 = t_refl_2_rx[i] - wavelet_duration/2 + wavelet_t
95
+ ax2.plot(x_trace_2, y_trace_2, 'b-', linewidth=0.8)
96
+ ax2.fill_betweenx(y_trace_2, receiver_x[i], x_trace_2, where=(x_trace_2 > receiver_x[i]), color='blue')
97
+
98
+ ax2.set_title(f"2. Visualized Seismic Profile ({int(num_receivers)} Traces)")
99
+ ax2.set_ylim(y_max, -y_max*0.05)
 
 
 
 
 
 
100
  ax2.set_xlim(-x_max * 0.05, x_max * 1.05)
101
+ fig2.subplots_adjust(left=0.1, right=0.98, top=0.9, bottom=0.15)
102
 
103
+ # === PART 5: 準備探勘日誌 ===
104
+ log_md = f"""
105
+ ### 📝 現場探勘日誌
106
+ **任務目標**: {scenario_name.get() if 'scenario_name' in globals() else '自訂模式'}
107
+ **儀器設定**: {int(num_receivers)} 個測站, 測線長度 {x_max} 公尺。
108
+
109
+ **初步分析**:
110
+ - **第一介面反射 (黑色震波)**: 雙程走時 (TWT) 約 **{t0_1*1000:.1f} ms**。
111
+ - **第二介面反射 (藍色震波)**: 雙程走時 (TWT) 約 **{t0_2*1000:.1f} ms**。
112
 
113
+ {'' if valid_model else '**警告**: 模型速度設定有誤 (V 未隨深度增加),折射波分析可能無效。'}
114
+ """
115
+ return fig0, fig1, fig2, log_md
 
 
 
 
 
 
 
116
 
117
+ # --- Gradio 介面與任務設定 ---
118
+ scenarios = {
119
+ "自訂模式 (Custom Mode)": {"v1": 800, "v2": 2500, "v3": 4500, "h1": 20, "h2": 50},
120
+ "尋找淺層地下水 (Find Groundwater)": {"v1": 500, "v2": 2200, "v3": 3500, "h1": 15, "h2": 40},
121
+ "桃園台地工程鑽探 (Taoyuan Engineering)": {"v1": 600, "v2": 1800, "v3": 3000, "h1": 10, "h2": 30},
122
+ "油氣田探勘 (Oil & Gas Prospecting)": {"v1": 1500, "v2": 2800, "v3": 4200, "h1": 100, "h2": 250},
123
+ }
124
 
125
+ def update_sliders(scenario_key):
126
+ params = scenarios[scenario_key]
127
+ return params['v1'], params['v2'], params['v3'], params['h1'], params['h2']
 
 
128
 
 
129
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
130
+ scenario_name = gr.State("自訂模式 (Custom Mode)")
131
+ gr.Markdown("# 地球物理探勘總部 🛰️")
 
 
 
 
 
 
132
 
133
  with gr.Row():
134
  with gr.Column(scale=1):
135
+ gr.Markdown("### 🎯 1. 選擇探勘任務")
136
+ scenario_dropdown = gr.Dropdown(list(scenarios.keys()), label="Select Mission", value="自訂模式 (Custom Mode)")
137
+
138
+ gr.Markdown("### ⚙️ 2. 微調地層參數")
139
+ v1_slider = gr.Slider(label="V1 (m/s)", minimum=300, maximum=5000, value=800, step=50)
140
+ h1_slider = gr.Slider(label="h1 (m)", minimum=5, maximum=500, value=20, step=5)
141
+ v2_slider = gr.Slider(label="V2 (m/s)", minimum=500, maximum=6000, value=2500, step=50)
142
+ h2_slider = gr.Slider(label="h2 (m)", minimum=10, maximum=1000, value=50, step=10)
143
+ v3_slider = gr.Slider(label="V3 (m/s)", minimum=1000, maximum=8000, value=4500, step=50)
144
 
145
+ gr.Markdown("### 📡 3. 設定儀器")
146
+ xmax_slider = gr.Slider(label="最大觀測距離 (m)", minimum=100, maximum=2000, value=500, step=50)
147
+ receivers_slider = gr.Slider(label="測站數量", minimum=10, maximum=200, value=50, step=5)
148
+ gain_slider = gr.Slider(label="剖面增益", minimum=1, maximum=20, value=4, step=1)
 
149
 
150
+ submit_btn = gr.Button("🚀 發射震波!", variant="primary")
151
 
152
  with gr.Column(scale=2):
153
+ gr.Markdown("### 🗺️ 地質模型")
154
+ plot_output0 = gr.Plot(label="Geological Model")
155
+ gr.Markdown("### 📊 探勘數據")
156
+ plot_output1 = gr.Plot(label="走時-距離圖")
157
+ plot_output2 = gr.Plot(label="視覺化震測剖面圖")
158
 
159
  with gr.Row():
160
+ log_output = gr.Markdown("### 📝 現場探勘日誌\n請選擇任務或調整參數,然後點擊「發射震波!」")
161
 
162
  # --- 事件監聽 ---
163
+ scenario_dropdown.change(
164
+ fn=update_sliders,
165
+ inputs=scenario_dropdown,
166
+ outputs=[v1_slider, v2_slider, v3_slider, h1_slider, h2_slider]
167
+ )
168
+ scenario_dropdown.change(lambda x: x, inputs=scenario_dropdown, outputs=scenario_name)
169
+
170
  submit_btn.click(
171
  fn=plot_seismic_exploration,
172
+ inputs=[v1_slider, v2_slider, v3_slider, h1_slider, h2_slider, xmax_slider, receivers_slider, gain_slider],
173
+ outputs=[plot_output0, plot_output1, plot_output2, log_output]
174
  )
175
 
176
  gr.Markdown(
177
  """
178
  ---
179
+ ### 🧠 總工程師的挑戰
180
+ 1. **看見儲油構造**: 在「油氣田探勘」任務中,來自第二介面(藍色震波)的反射同相軸呈現一個向上彎曲的「背斜」形狀,這正是油氣最喜歡聚集的地方!你能透過微調 `h1` `h2` 讓這個構造更明顯嗎?
181
+ 2. **折射的極限**: 試著在自訂模式中,將 `V2` 調得比 `V1` 慢,看看走時圖和日誌會出現什麼警告?這在真實地質中稱為「低速帶」,是折射法的一大挑戰。
182
+ 3. **解析度問題**: 將「測站數量」調到最低,再慢慢增加。你需要多少個測站,才能清楚地分辨出剖面圖中來自兩個不同介面的反射波?這就是探勘的「解析度」概念。
 
 
 
 
 
 
 
 
183
  """
184
  )
185
 
186
  gr.HTML("""
187
  <footer style="text-align:center; margin-top: 30px; color:grey;">
188
  <p>「創意的發揮是一種學習,過程中,每個人同時是學生也是老師。」</p>
189
+ <p>地球物理探勘總部 &copy; 2025 - 由 Gemini 根據課程文件與靈感生成</p>
190
  </footer>
191
  """)
192