Spaces:
Sleeping
Sleeping
Commit
·
4ee8fb3
1
Parent(s):
10ceedb
add interactive intensity map feature using Folium and update related functions
Browse files- app.py +187 -65
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -2,7 +2,6 @@ import gradio as gr
|
|
| 2 |
import numpy as np
|
| 3 |
import matplotlib.pyplot as plt
|
| 4 |
from obspy import read
|
| 5 |
-
from datasets import load_dataset
|
| 6 |
import xarray as xr
|
| 7 |
import torch
|
| 8 |
import torch.nn as nn
|
|
@@ -382,12 +381,6 @@ def get_vs30(lat, lon, user_vs30=600):
|
|
| 382 |
return float(vs30)
|
| 383 |
|
| 384 |
|
| 385 |
-
def get_station_position(station):
|
| 386 |
-
latitude, longitude, elevation = site_info.loc[
|
| 387 |
-
(site_info["Station"] == station), ["Latitude", "Longitude", "Elevation"]
|
| 388 |
-
].values[0]
|
| 389 |
-
return latitude, longitude, elevation
|
| 390 |
-
|
| 391 |
|
| 392 |
def calculate_intensity(pga, label=False):
|
| 393 |
intensity_label = ["0", "1", "2", "3", "4", "5-", "5+", "6-", "6+", "7"]
|
|
@@ -501,17 +494,18 @@ def extract_waveforms_from_stream(st, selected_stations, start_time, end_time, v
|
|
| 501 |
n_data = signal_processing(n_data)
|
| 502 |
e_data = signal_processing(e_data)
|
| 503 |
|
| 504 |
-
#
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
waveform_3c =
|
| 513 |
-
|
| 514 |
-
|
|
|
|
| 515 |
waveforms.append(waveform_3c)
|
| 516 |
|
| 517 |
# 準備測站資訊
|
|
@@ -533,14 +527,19 @@ def extract_waveforms_from_stream(st, selected_stations, start_time, end_time, v
|
|
| 533 |
|
| 534 |
|
| 535 |
def plot_waveform(st, selected_stations, start_time, end_time):
|
| 536 |
-
"""
|
| 537 |
-
fig,
|
|
|
|
|
|
|
|
|
|
| 538 |
|
| 539 |
-
|
| 540 |
-
|
|
|
|
| 541 |
|
| 542 |
-
for i, station_data in enumerate(selected_stations
|
| 543 |
station_code = station_data["station"]
|
|
|
|
| 544 |
|
| 545 |
try:
|
| 546 |
st_station = st.select(station=station_code)
|
|
@@ -549,56 +548,178 @@ def plot_waveform(st, selected_stations, start_time, end_time):
|
|
| 549 |
times = tr.times()
|
| 550 |
data = tr.data
|
| 551 |
|
| 552 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
axes[i].axvspan(start_time, end_time, alpha=0.2, color='blue')
|
| 558 |
|
| 559 |
-
axes[i].set_ylabel(f'{station_code}\n({station_data["distance"]:.2f}°)', fontsize=8)
|
| 560 |
-
axes[i].grid(True, alpha=0.3)
|
| 561 |
-
axes[i].tick_params(labelsize=8)
|
| 562 |
except Exception as e:
|
| 563 |
logger.warning(f"無法繪製測站 {station_code}: {e}")
|
| 564 |
|
| 565 |
-
|
| 566 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
plt.tight_layout()
|
| 568 |
|
| 569 |
return fig
|
| 570 |
|
| 571 |
|
| 572 |
-
def
|
| 573 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
|
| 575 |
-
#
|
| 576 |
-
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
|
| 579 |
-
#
|
| 580 |
-
lats, lons, intensities = [], [], []
|
| 581 |
for i, target_name in enumerate(target_names):
|
| 582 |
target = next((t for t in target_dict if t["station"] == target_name), None)
|
| 583 |
if target:
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
|
| 603 |
|
| 604 |
def load_and_display_waveform(event_name, start_time, end_time, epicenter_lon, epicenter_lat):
|
|
@@ -693,8 +814,9 @@ def predict_intensity(event_name, start_time, end_time, epicenter_lon, epicenter
|
|
| 693 |
weight, sigma, mu = model(tensor_data)
|
| 694 |
pga_list = torch.sum(weight * mu, dim=2).cpu().detach().numpy().flatten().tolist()
|
| 695 |
|
| 696 |
-
# 8.
|
| 697 |
-
|
|
|
|
| 698 |
|
| 699 |
# 9. 統計資訊
|
| 700 |
max_intensity = max([calculate_intensity(pga, label=True) for pga in pga_list])
|
|
@@ -705,7 +827,7 @@ def predict_intensity(event_name, start_time, end_time, epicenter_lon, epicenter
|
|
| 705 |
stats += f"預測最大震度: {max_intensity}"
|
| 706 |
|
| 707 |
logger.info("預測完成!")
|
| 708 |
-
return
|
| 709 |
|
| 710 |
except Exception as e:
|
| 711 |
logger.error(f"預測過程發生錯誤: {e}")
|
|
@@ -735,8 +857,8 @@ with gr.Blocks(title="TTSAM 震度預測系統") as demo:
|
|
| 735 |
|
| 736 |
gr.Markdown("### 震央位置")
|
| 737 |
with gr.Row():
|
| 738 |
-
epicenter_lon_input = gr.Number(value=121.
|
| 739 |
-
epicenter_lat_input = gr.Number(value=
|
| 740 |
|
| 741 |
gr.Markdown("### 場址參數")
|
| 742 |
with gr.Row():
|
|
@@ -764,7 +886,7 @@ with gr.Blocks(title="TTSAM 震度預測系統") as demo:
|
|
| 764 |
# 右側:震度分布圖
|
| 765 |
with gr.Column(scale=1):
|
| 766 |
gr.Markdown("## 預測震度分布")
|
| 767 |
-
|
| 768 |
stats_output = gr.Textbox(label="預測統計", lines=4)
|
| 769 |
|
| 770 |
# 下方:波形圖
|
|
@@ -785,7 +907,7 @@ with gr.Blocks(title="TTSAM 震度預測系統") as demo:
|
|
| 785 |
predict_btn.click(
|
| 786 |
fn=predict_intensity,
|
| 787 |
inputs=[event_dropdown, start_slider, end_slider, epicenter_lon_input, epicenter_lat_input, vs30_input],
|
| 788 |
-
outputs=[
|
| 789 |
)
|
| 790 |
|
| 791 |
demo.launch()
|
|
|
|
| 2 |
import numpy as np
|
| 3 |
import matplotlib.pyplot as plt
|
| 4 |
from obspy import read
|
|
|
|
| 5 |
import xarray as xr
|
| 6 |
import torch
|
| 7 |
import torch.nn as nn
|
|
|
|
| 381 |
return float(vs30)
|
| 382 |
|
| 383 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
|
| 385 |
def calculate_intensity(pga, label=False):
|
| 386 |
intensity_label = ["0", "1", "2", "3", "4", "5-", "5+", "6-", "6+", "7"]
|
|
|
|
| 494 |
n_data = signal_processing(n_data)
|
| 495 |
e_data = signal_processing(e_data)
|
| 496 |
|
| 497 |
+
# 先創建全零陣列 (3000, 3)
|
| 498 |
+
waveform_3c = np.zeros((target_length, 3))
|
| 499 |
+
|
| 500 |
+
# 填入實際資料(自動處理長度不足或過長的情況)
|
| 501 |
+
z_len = min(len(z_data), target_length)
|
| 502 |
+
n_len = min(len(n_data), target_length)
|
| 503 |
+
e_len = min(len(e_data), target_length)
|
| 504 |
+
|
| 505 |
+
waveform_3c[:z_len, 0] = z_data[:z_len]
|
| 506 |
+
waveform_3c[:n_len, 1] = n_data[:n_len]
|
| 507 |
+
waveform_3c[:e_len, 2] = e_data[:e_len]
|
| 508 |
+
|
| 509 |
waveforms.append(waveform_3c)
|
| 510 |
|
| 511 |
# 準備測站資訊
|
|
|
|
| 527 |
|
| 528 |
|
| 529 |
def plot_waveform(st, selected_stations, start_time, end_time):
|
| 530 |
+
"""繪製選定測站的波形圖(距離-時間圖,可顯示全部 25 個測站)"""
|
| 531 |
+
fig, ax = plt.subplots(figsize=(14, 10))
|
| 532 |
+
|
| 533 |
+
# 設定振幅縮放比例(避免波形重疊)
|
| 534 |
+
amplitude_scale = 0.03 # 可調整此值來控制波形大小
|
| 535 |
|
| 536 |
+
plotted_count = 0
|
| 537 |
+
distances = []
|
| 538 |
+
station_names = []
|
| 539 |
|
| 540 |
+
for i, station_data in enumerate(selected_stations):
|
| 541 |
station_code = station_data["station"]
|
| 542 |
+
distance = station_data["distance"]
|
| 543 |
|
| 544 |
try:
|
| 545 |
st_station = st.select(station=station_code)
|
|
|
|
| 548 |
times = tr.times()
|
| 549 |
data = tr.data
|
| 550 |
|
| 551 |
+
# 正規化波形振幅
|
| 552 |
+
data_normalized = data / (np.max(np.abs(data)) + 1e-10)
|
| 553 |
+
|
| 554 |
+
# 繪製波形,Y軸位置為距離
|
| 555 |
+
ax.plot(times, distance + data_normalized * amplitude_scale,
|
| 556 |
+
'black', linewidth=0.3, alpha=0.8)
|
| 557 |
|
| 558 |
+
distances.append(distance)
|
| 559 |
+
station_names.append(station_code)
|
| 560 |
+
plotted_count += 1
|
|
|
|
| 561 |
|
|
|
|
|
|
|
|
|
|
| 562 |
except Exception as e:
|
| 563 |
logger.warning(f"無法繪製測站 {station_code}: {e}")
|
| 564 |
|
| 565 |
+
# 標記選取時間範圍
|
| 566 |
+
ax.axvline(start_time, color='red', linestyle='--', linewidth=2,
|
| 567 |
+
alpha=0.7, label='選取範圍')
|
| 568 |
+
ax.axvline(end_time, color='red', linestyle='--', linewidth=2, alpha=0.7)
|
| 569 |
+
ax.axvspan(start_time, end_time, alpha=0.15, color='blue')
|
| 570 |
+
|
| 571 |
+
# 設定軸標籤和標題
|
| 572 |
+
ax.set_xlabel('Time (s)', fontsize=12)
|
| 573 |
+
ax.set_ylabel('Distance from Epicenter (°)', fontsize=12)
|
| 574 |
+
ax.set_title(f'Record Section - {plotted_count} Stations Sorted by Distance',
|
| 575 |
+
fontsize=14, fontweight='bold')
|
| 576 |
+
|
| 577 |
+
# 在右側標註測站名稱
|
| 578 |
+
if distances:
|
| 579 |
+
ax2 = ax.twinx()
|
| 580 |
+
ax2.set_ylim(ax.get_ylim())
|
| 581 |
+
ax2.set_ylabel('Station Code', fontsize=12)
|
| 582 |
+
|
| 583 |
+
# 每隔幾個測站標註一次(避免過於擁擠)
|
| 584 |
+
step = max(1, len(distances) // 10)
|
| 585 |
+
tick_positions = distances[::step]
|
| 586 |
+
tick_labels = station_names[::step]
|
| 587 |
+
ax2.set_yticks(tick_positions)
|
| 588 |
+
ax2.set_yticklabels(tick_labels, fontsize=8)
|
| 589 |
+
|
| 590 |
+
ax.grid(True, alpha=0.3, axis='x')
|
| 591 |
+
ax.legend(loc='upper right')
|
| 592 |
plt.tight_layout()
|
| 593 |
|
| 594 |
return fig
|
| 595 |
|
| 596 |
|
| 597 |
+
def get_intensity_color(intensity):
|
| 598 |
+
"""根據震度等級返回對應顏色(參考 intensityMap.html)"""
|
| 599 |
+
color_map = {
|
| 600 |
+
0: "#ffffff", # 白色
|
| 601 |
+
1: "#33FFDD", # 青色
|
| 602 |
+
2: "#34ff32", # 綠色
|
| 603 |
+
3: "#fefd32", # 黃色
|
| 604 |
+
4: "#fe8532", # 橘色
|
| 605 |
+
5: "#fd5233", # 紅橘色 (5-)
|
| 606 |
+
6: "#c43f3b", # 深紅色 (5+)
|
| 607 |
+
7: "#9d4646", # 暗紅色 (6-)
|
| 608 |
+
8: "#9a4c86", # 紫紅色 (6+)
|
| 609 |
+
9: "#b51fea", # 紫色 (7)
|
| 610 |
+
}
|
| 611 |
+
return color_map.get(intensity, "#ffffff")
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
def create_intensity_map(pga_list, target_names, epicenter_lat=None, epicenter_lon=None):
|
| 615 |
+
"""使用 Folium 創建互動式震度分布地圖"""
|
| 616 |
+
import folium
|
| 617 |
+
from folium import plugins
|
| 618 |
+
|
| 619 |
+
# 創建地圖,中心點設在台灣中心
|
| 620 |
+
m = folium.Map(
|
| 621 |
+
location=[23.5, 121],
|
| 622 |
+
zoom_start=7,
|
| 623 |
+
tiles='OpenStreetMap'
|
| 624 |
+
)
|
| 625 |
|
| 626 |
+
# 如果有震央位置,標記震央
|
| 627 |
+
if epicenter_lat and epicenter_lon:
|
| 628 |
+
folium.Marker(
|
| 629 |
+
[epicenter_lat, epicenter_lon],
|
| 630 |
+
popup=f'震央<br>({epicenter_lat:.3f}, {epicenter_lon:.3f})',
|
| 631 |
+
icon=folium.Icon(color='red', icon='star', prefix='fa'),
|
| 632 |
+
tooltip='震央位置'
|
| 633 |
+
).add_to(m)
|
| 634 |
|
| 635 |
+
# 添加震度測站標記
|
|
|
|
| 636 |
for i, target_name in enumerate(target_names):
|
| 637 |
target = next((t for t in target_dict if t["station"] == target_name), None)
|
| 638 |
if target:
|
| 639 |
+
lat = target["latitude"]
|
| 640 |
+
lon = target["longitude"]
|
| 641 |
+
intensity = calculate_intensity(pga_list[i])
|
| 642 |
+
intensity_label = calculate_intensity(pga_list[i], label=True)
|
| 643 |
+
color = get_intensity_color(intensity)
|
| 644 |
+
pga = pga_list[i]
|
| 645 |
+
|
| 646 |
+
# 創建 HTML popup 內容
|
| 647 |
+
popup_html = f"""
|
| 648 |
+
<div style="font-family: Arial; min-width: 150px;">
|
| 649 |
+
<h4 style="margin: 0 0 10px 0;">{target_name}</h4>
|
| 650 |
+
<table style="width:100%;">
|
| 651 |
+
<tr><td><b>震度:</b></td><td style="color: {color}; font-weight: bold; font-size: 16px;">{intensity_label}</td></tr>
|
| 652 |
+
<tr><td><b>PGA:</b></td><td>{pga:.4f} m/s²</td></tr>
|
| 653 |
+
<tr><td><b>位置:</b></td><td>({lat:.3f}, {lon:.3f})</td></tr>
|
| 654 |
+
</table>
|
| 655 |
+
</div>
|
| 656 |
+
"""
|
| 657 |
+
|
| 658 |
+
# 創建圓形標記
|
| 659 |
+
folium.CircleMarker(
|
| 660 |
+
location=[lat, lon],
|
| 661 |
+
radius=12,
|
| 662 |
+
popup=folium.Popup(popup_html, max_width=250),
|
| 663 |
+
tooltip=f'{target_name}: 震度 {intensity_label}',
|
| 664 |
+
color='black',
|
| 665 |
+
fillColor=color,
|
| 666 |
+
fillOpacity=0.8,
|
| 667 |
+
weight=2
|
| 668 |
+
).add_to(m)
|
| 669 |
+
|
| 670 |
+
# 在圓圈中心添加震度文字
|
| 671 |
+
folium.Marker(
|
| 672 |
+
[lat, lon],
|
| 673 |
+
icon=folium.DivIcon(html=f'''
|
| 674 |
+
<div style="
|
| 675 |
+
font-size: 10px;
|
| 676 |
+
font-weight: bold;
|
| 677 |
+
color: black;
|
| 678 |
+
text-align: center;
|
| 679 |
+
text-shadow: 1px 1px 2px white, -1px -1px 2px white;
|
| 680 |
+
">{intensity_label}</div>
|
| 681 |
+
''')
|
| 682 |
+
).add_to(m)
|
| 683 |
+
|
| 684 |
+
# 添加圖例
|
| 685 |
+
legend_html = '''
|
| 686 |
+
<div style="
|
| 687 |
+
position: fixed;
|
| 688 |
+
top: 10px; left: 10px;
|
| 689 |
+
width: 180px;
|
| 690 |
+
background-color: white;
|
| 691 |
+
border: 2px solid grey;
|
| 692 |
+
z-index: 9999;
|
| 693 |
+
font-size: 14px;
|
| 694 |
+
padding: 10px;
|
| 695 |
+
border-radius: 5px;
|
| 696 |
+
box-shadow: 2px 2px 6px rgba(0,0,0,0.3);
|
| 697 |
+
">
|
| 698 |
+
<h4 style="margin: 0 0 10px 0;">震度等級 Intensity</h4>
|
| 699 |
+
<table style="width: 100%;">
|
| 700 |
+
'''
|
| 701 |
+
|
| 702 |
+
intensity_levels = ["0", "1", "2", "3", "4", "5-", "5+", "6-", "6+", "7"]
|
| 703 |
+
for idx, level in enumerate(intensity_levels):
|
| 704 |
+
color = get_intensity_color(idx)
|
| 705 |
+
legend_html += f'''
|
| 706 |
+
<tr>
|
| 707 |
+
<td style="width: 30px; height: 20px; background-color: {color}; border: 1px solid black;"></td>
|
| 708 |
+
<td style="padding-left: 5px;">{level}</td>
|
| 709 |
+
</tr>
|
| 710 |
+
'''
|
| 711 |
+
|
| 712 |
+
legend_html += '''
|
| 713 |
+
</table>
|
| 714 |
+
</div>
|
| 715 |
+
'''
|
| 716 |
+
|
| 717 |
+
m.get_root().html.add_child(folium.Element(legend_html))
|
| 718 |
+
|
| 719 |
+
# 添加全屏按鈕
|
| 720 |
+
plugins.Fullscreen().add_to(m)
|
| 721 |
+
|
| 722 |
+
return m
|
| 723 |
|
| 724 |
|
| 725 |
def load_and_display_waveform(event_name, start_time, end_time, epicenter_lon, epicenter_lat):
|
|
|
|
| 814 |
weight, sigma, mu = model(tensor_data)
|
| 815 |
pga_list = torch.sum(weight * mu, dim=2).cpu().detach().numpy().flatten().tolist()
|
| 816 |
|
| 817 |
+
# 8. 繪製互動式地圖
|
| 818 |
+
intensity_map = create_intensity_map(pga_list, target_names, epicenter_lat, epicenter_lon)
|
| 819 |
+
map_html = intensity_map._repr_html_()
|
| 820 |
|
| 821 |
# 9. 統計資訊
|
| 822 |
max_intensity = max([calculate_intensity(pga, label=True) for pga in pga_list])
|
|
|
|
| 827 |
stats += f"預測最大震度: {max_intensity}"
|
| 828 |
|
| 829 |
logger.info("預測完成!")
|
| 830 |
+
return map_html, stats
|
| 831 |
|
| 832 |
except Exception as e:
|
| 833 |
logger.error(f"預測過程發生錯誤: {e}")
|
|
|
|
| 857 |
|
| 858 |
gr.Markdown("### 震央位置")
|
| 859 |
with gr.Row():
|
| 860 |
+
epicenter_lon_input = gr.Number(value=121.57, label="震央經度")
|
| 861 |
+
epicenter_lat_input = gr.Number(value=23.88, label="震央緯度")
|
| 862 |
|
| 863 |
gr.Markdown("### 場址參數")
|
| 864 |
with gr.Row():
|
|
|
|
| 886 |
# 右側:震度分布圖
|
| 887 |
with gr.Column(scale=1):
|
| 888 |
gr.Markdown("## 預測震度分布")
|
| 889 |
+
intensity_map = gr.HTML(label="互動式震度地圖")
|
| 890 |
stats_output = gr.Textbox(label="預測統計", lines=4)
|
| 891 |
|
| 892 |
# 下方:波形圖
|
|
|
|
| 907 |
predict_btn.click(
|
| 908 |
fn=predict_intensity,
|
| 909 |
inputs=[event_dropdown, start_slider, end_slider, epicenter_lon_input, epicenter_lat_input, vs30_input],
|
| 910 |
+
outputs=[intensity_map, stats_output]
|
| 911 |
)
|
| 912 |
|
| 913 |
demo.launch()
|
requirements.txt
CHANGED
|
@@ -11,3 +11,4 @@ scipy
|
|
| 11 |
pandas
|
| 12 |
loguru
|
| 13 |
huggingface_hub
|
|
|
|
|
|
| 11 |
pandas
|
| 12 |
loguru
|
| 13 |
huggingface_hub
|
| 14 |
+
folium
|