jimmy60504 commited on
Commit
d03ae4c
·
1 Parent(s): 23eb3ea

docs: refactor waveform loading and processing steps for improved caching and error handling

Browse files
Files changed (1) hide show
  1. app.py +117 -144
app.py CHANGED
@@ -806,112 +806,116 @@ def load_observed_intensity_image(event_name):
806
  return None
807
 
808
 
809
- def load_and_display_waveform(event_name, duration):
810
- """載入並顯示波形,讓使用者確認範圍
811
-
812
- 從全域 earthquake_metadata 讀取震央座標
813
 
814
- spec #2:測站選擇上限 (25 站)、波形取樣率 (100 Hz)、時間窗長度 (30 秒)
815
  """
816
  try:
817
- # 從全域 earthquake_metadata 讀取震央座標
818
  epicenter_lat = earthquake_metadata[event_name]["epicenter_lat"]
819
  epicenter_lon = earthquake_metadata[event_name]["epicenter_lon"]
820
  mseed_file = earthquake_metadata[event_name]["mseed_file"]
821
- first_pick = earthquake_metadata[event_name]["first_pick"]
822
-
823
- # 計算結束時間(用於顯示資訊)
824
 
825
-
826
- # 1. 載入完整的 mseed 檔案
827
- logger.info(f"載入地震事件: {event_name}")
828
  st = read(mseed_file)
829
  logger.info(f"載入了 {len(st)} 個 trace")
830
 
831
- # 2. 根據震央距離選擇最近的 25 個測站
832
  logger.info(f"選擇距離震央 ({epicenter_lat}, {epicenter_lon}) 最近的測站...")
833
  selected_stations = select_nearest_stations(
834
  st, epicenter_lat, epicenter_lon, n_stations=25
835
  )
836
 
837
  if len(selected_stations) == 0:
838
- return (
839
- None,
840
- None,
841
- "錯誤:找不到有效的測站資料",
842
- gr.update(interactive=False),
843
- )
844
-
845
- waveform_plot = plot_waveform(st, selected_stations, first_pick, duration)
846
 
847
- # 4. 創建輸入測站地圖
848
  station_map = create_input_station_map(
849
  selected_stations, epicenter_lat, epicenter_lon
850
  )
851
 
852
- logger.info("波形載入完成")
853
- return station_map, waveform_plot, gr.update(interactive=True)
854
 
855
  except Exception as e:
856
- logger.error(f"波形載入發生錯誤: {e}")
857
  import traceback
858
-
859
  traceback.print_exc()
860
- return None, None, gr.update(interactive=False)
861
 
862
 
863
- def predict_intensity(event_name, duration):
 
864
  """
865
- 執行震度預測
866
-
867
- 介面改以 start+duration,使用固定高度 800 的地圖輸出
868
- 從全域 earthquake_metadata 讀取震央座標
869
 
870
- spec #2:測站選擇上限 (25 站)、波形取樣率 (100 Hz)、時間窗長度 (30 秒)
871
- spec #3:推論流程、PGA → 震度轉換
872
  """
873
  try:
874
- # 從全域 earthquake_metadata 讀取震央座標
875
- epicenter_lat = earthquake_metadata[event_name]["epicenter_lat"]
876
- epicenter_lon = earthquake_metadata[event_name]["epicenter_lon"]
877
- mseed_file = earthquake_metadata[event_name]["mseed_file"]
878
-
879
-
880
- # 1. 載入完整的 mseed 檔案
881
- logger.info(f"載入地震事件: {event_name}")
882
- st = read(mseed_file)
883
- logger.info(f"載入了 {len(st)} 個 trace")
884
 
885
- # 2. 根據震央距離選擇最近的 25 個測站
886
- logger.info(f"選擇距離震央 ({epicenter_lat}, {epicenter_lon}) 最近的測站...")
887
- selected_stations = select_nearest_stations(
888
- st, epicenter_lat, epicenter_lon, n_stations=25
889
- )
890
 
891
- if len(selected_stations) == 0:
892
- return None, None, "錯誤:找不到有效的測站資料"
893
 
894
- # 3. 從選定的測站提取波形(vs30_input 使用預設值 600,會被資料庫值覆蓋)
895
- logger.info(
896
- f"提取波形資料(時間範圍: 初動��� {duration} 秒)..."
897
- )
898
  waveforms, station_info_list, valid_stations, missing_components_count = (
899
  extract_waveforms_from_stream(
900
- event_name, st, selected_stations, duration, vs30_input=600
901
  )
902
  )
903
 
904
  if len(waveforms) == 0:
905
- return None, "錯誤:無法提取波形資料"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
906
 
907
- # 4. Padding 25 個測站(模型要求)
 
 
 
 
 
 
 
 
 
 
 
 
 
908
  max_stations = 25
909
  waveform_padded = np.zeros((max_stations, 3000, 3))
910
  station_info_padded = np.zeros((max_stations, 4))
911
 
912
- for i in range(min(len(waveforms), max_stations)):
913
- waveform_padded[i] = waveforms[i]
914
- station_info_padded[i] = station_info_list[i]
915
 
916
  # 準備所有目標測站資訊(分批處理)
917
  all_pga_list = []
@@ -956,14 +960,14 @@ def predict_intensity(event_name, duration):
956
  for i in range(len(target_list)):
957
  target_padded[i] = target_list[i]
958
 
959
- # 6. 組合成 torch tensor
960
  tensor_data = {
961
  "waveform": torch.tensor(waveform_padded).unsqueeze(0).double(),
962
  "station": torch.tensor(station_info_padded).unsqueeze(0).double(),
963
  "target": torch.tensor(target_padded).unsqueeze(0).double(),
964
  }
965
 
966
- # 7. 執行預測
967
  with torch.no_grad():
968
  weight, sigma, mu = model(tensor_data)
969
  batch_pga = (
@@ -988,65 +992,20 @@ def predict_intensity(event_name, duration):
988
  pga_list, target_names, epicenter_lat, epicenter_lon
989
  )
990
 
991
- # 載入實際觀測震度圖(filepath;左側以 800 高顯示)
992
  observed_intensity_path = load_observed_intensity_image(event_name)
993
 
994
- logger.info("預測完成!")
995
  return observed_intensity_path, intensity_map
996
 
997
  except Exception as e:
998
- logger.error(f"預測過程發生錯誤: {e}")
999
  import traceback
1000
 
1001
  traceback.print_exc()
1002
  return None, None
1003
 
1004
 
1005
- def on_full_workflow(event_name, duration):
1006
- """
1007
- 執行完整的工作流:波形載入 → 測站選擇 → 推論 → 結果展示
1008
-
1009
- 此函數用於首次應用加載與事件切換時自動執行完整流程
1010
-
1011
- 返回所有必要的 UI 組件輸出:
1012
- (station_map, waveform_plot, predicted_map, observed_img)
1013
-
1014
- spec #2:測站選擇上限 (25 站)、波形取樣率 (100 Hz)、時間窗長度 (30 秒)
1015
- spec #3:推論流程、PGA → 震度轉換
1016
- """
1017
- try:
1018
- logger.info(f"[on_full_workflow] 開始執行完整工作流 - 事件: {event_name}")
1019
-
1020
- # 步驟 1: 載入波形
1021
- logger.info(f"[on_full_workflow] 步驟 1/3: 波形載入...")
1022
- station_map, waveform_plot, _ = load_and_display_waveform(
1023
- event_name, duration
1024
- )
1025
-
1026
- if station_map is None:
1027
- logger.error("[on_full_workflow] 波形載入失敗")
1028
- return None, None, None, None
1029
-
1030
- # ��驟 2: 執行推論
1031
- logger.info(f"[on_full_workflow] 步驟 2/3: 模型推論...")
1032
- observed_img, predicted_map = predict_intensity(
1033
- event_name, duration
1034
- )
1035
-
1036
- if predicted_map is None:
1037
- logger.error("[on_full_workflow] 推論失敗")
1038
- return station_map, waveform_plot, None, observed_img
1039
-
1040
- logger.info(f"[on_full_workflow] 步驟 3/3: 完成")
1041
-
1042
- return station_map, waveform_plot, predicted_map, observed_img
1043
-
1044
- except Exception as e:
1045
- logger.error(f"[on_full_workflow] 完整工作流發生錯誤: {e}")
1046
- import traceback
1047
-
1048
- traceback.print_exc()
1049
- return None, None, f"錯誤: {str(e)}", None, f"錯誤: {str(e)}", None
1050
 
1051
  # ============ Gradio 介面 ============
1052
  with gr.Blocks(title="TTSAM 震度預測系統", fill_height=True) as demo:
@@ -1059,12 +1018,11 @@ with gr.Blocks(title="TTSAM 震度預測系統", fill_height=True) as demo:
1059
  gr.Markdown("## 使用步驟")
1060
  gr.Markdown(
1061
  """
1062
- 1. 選擇地震事件和時間範圍
1063
- 2. 輸入震央位置和場址參數
1064
- 3. 點擊載入波形確認波形範圍
1065
- 4. 確認無誤後點擊執行預測
1066
 
1067
- 系統會自動選擇距離震央最近的 25 個測站
1068
  """
1069
  )
1070
  with gr.Column(scale=1):
@@ -1112,44 +1070,59 @@ with gr.Blocks(title="TTSAM 震度預測系統", fill_height=True) as demo:
1112
  value=load_observed_intensity_image(list(earthquake_metadata.keys())[0]),
1113
  )
1114
 
1115
- # 綁定事件
 
 
 
 
 
 
 
 
1116
  event_dropdown.change(
1117
- fn=lambda event_name, duration: (
1118
- *on_full_workflow(event_name, duration),
1119
- ),
1120
- inputs=[event_dropdown, duration_slider],
1121
- outputs=[
1122
- input_station_map,
1123
- waveform_plot,
1124
- predicted_intensity_map,
1125
- observed_intensity_image,
1126
- ],
 
 
 
 
1127
  )
1128
 
 
1129
  load_waveform_btn.click(
1130
- fn=load_and_display_waveform,
1131
- inputs=[event_dropdown, duration_slider],
1132
- outputs=[input_station_map, waveform_plot, predict_btn],
 
 
 
 
1133
  )
1134
 
 
1135
  predict_btn.click(
1136
- fn=predict_intensity,
1137
- inputs=[event_dropdown, duration_slider],
1138
- outputs=[observed_intensity_image, predicted_intensity_map],
1139
  )
1140
 
1141
- # 應用啟動時自動執行完整工作流
1142
  demo.load(
1143
- fn=lambda event_name, duration: (
1144
- *on_full_workflow(event_name, duration),
1145
- ),
1146
- inputs=[event_dropdown, duration_slider],
1147
- outputs=[
1148
- input_station_map,
1149
- waveform_plot,
1150
- predicted_intensity_map,
1151
- observed_intensity_image,
1152
- ],
1153
  )
1154
 
1155
  demo.launch()
 
806
  return None
807
 
808
 
809
+ # ============ 步驟 1:載入 mseed + 選擇測站(快取到 gr.State)============
810
+ def step1_load_mseed_and_select_stations(event_name):
811
+ """
812
+ 步驟 1:載入 mseed 檔案並選擇最近的 25 個測站
813
 
814
+ 這一步只執行一次(切換事件時),結果會快取在 gr.State
815
  """
816
  try:
 
817
  epicenter_lat = earthquake_metadata[event_name]["epicenter_lat"]
818
  epicenter_lon = earthquake_metadata[event_name]["epicenter_lon"]
819
  mseed_file = earthquake_metadata[event_name]["mseed_file"]
 
 
 
820
 
821
+ logger.info(f"[步驟 1] 載入地震事件: {event_name}")
 
 
822
  st = read(mseed_file)
823
  logger.info(f"載入了 {len(st)} 個 trace")
824
 
825
+ # 選擇距離震央最近的 25 個測站
826
  logger.info(f"選擇距離震央 ({epicenter_lat}, {epicenter_lon}) 最近的測站...")
827
  selected_stations = select_nearest_stations(
828
  st, epicenter_lat, epicenter_lon, n_stations=25
829
  )
830
 
831
  if len(selected_stations) == 0:
832
+ logger.error("找不到有效的測站資料")
833
+ return None, None, None, None, gr.update(interactive=False)
 
 
 
 
 
 
834
 
835
+ # 創建輸入測站地圖
836
  station_map = create_input_station_map(
837
  selected_stations, epicenter_lat, epicenter_lon
838
  )
839
 
840
+ logger.info("[步驟 1] 完成 - mseed 已載入,測站已選擇")
841
+ return st, selected_stations, station_map, None, gr.update(interactive=False)
842
 
843
  except Exception as e:
844
+ logger.error(f"[步驟 1] 發生錯誤: {e}")
845
  import traceback
 
846
  traceback.print_exc()
847
+ return None, None, None, None, gr.update(interactive=False)
848
 
849
 
850
+ # ============ 步驟 2:提取波形(使用快取的 stream + stations)============
851
+ def step2_extract_and_plot_waveforms(cached_stream, cached_stations, event_name, duration):
852
  """
853
+ 步驟 2:根據時間範圍提取波形並繪圖
 
 
 
854
 
855
+ 使用快取的 stream selected_stations,避免重複讀檔
856
+ 用戶調整時間範圍時會重複執行此步驟
857
  """
858
  try:
859
+ if cached_stream is None or cached_stations is None:
860
+ logger.warning("[步驟 2] 快取資料不存在,請先載入波形")
861
+ return None, None, None, gr.update(interactive=False)
 
 
 
 
 
 
 
862
 
863
+ first_pick = earthquake_metadata[event_name]["first_pick"]
 
 
 
 
864
 
865
+ logger.info(f"[步驟 2] 提取波形資料(P 波後 {duration} 秒)...")
 
866
 
867
+ # 提取波形資料
 
 
 
868
  waveforms, station_info_list, valid_stations, missing_components_count = (
869
  extract_waveforms_from_stream(
870
+ event_name, cached_stream, cached_stations, duration, vs30_input=600
871
  )
872
  )
873
 
874
  if len(waveforms) == 0:
875
+ logger.error("[步驟 2] 無法提取波形資料")
876
+ return None, None, None, gr.update(interactive=False)
877
+
878
+ # 繪製波形圖
879
+ waveform_plot = plot_waveform(cached_stream, cached_stations, first_pick, duration)
880
+
881
+ logger.info(f"[步驟 2] 完成 - 已提取 {len(waveforms)} 個測站的波形")
882
+ return waveforms, station_info_list, waveform_plot, gr.update(interactive=True)
883
+
884
+ except Exception as e:
885
+ logger.error(f"[步驟 2] 發生錯誤: {e}")
886
+ import traceback
887
+ traceback.print_exc()
888
+ return None, None, None, gr.update(interactive=False)
889
+
890
+
891
+ # ============ 步驟 3:執行模型推論(使用快取的波形)============
892
+ def step3_predict_intensity(cached_waveforms, cached_station_info, event_name):
893
+ """
894
+ 步驟 3:執行震度預測
895
+
896
+ 直接使用快取的波形資料和測站資訊,無需重新讀檔或提取波形
897
 
898
+ spec #2:測站選擇上限 (25 站)、波形取樣率 (100 Hz)、時間窗長度 (30 秒)
899
+ spec #3:推論流程、PGA → 震度轉換
900
+ """
901
+ try:
902
+ if cached_waveforms is None or cached_station_info is None:
903
+ logger.warning("[步驟 3] 快取資料不存在,請先載入並提取波形")
904
+ return None, None
905
+
906
+ epicenter_lat = earthquake_metadata[event_name]["epicenter_lat"]
907
+ epicenter_lon = earthquake_metadata[event_name]["epicenter_lon"]
908
+
909
+ logger.info("[步驟 3] 開始模型推論...")
910
+
911
+ # Padding 到 25 個測站(模型要求)
912
  max_stations = 25
913
  waveform_padded = np.zeros((max_stations, 3000, 3))
914
  station_info_padded = np.zeros((max_stations, 4))
915
 
916
+ for i in range(min(len(cached_waveforms), max_stations)):
917
+ waveform_padded[i] = cached_waveforms[i]
918
+ station_info_padded[i] = cached_station_info[i]
919
 
920
  # 準備所有目標測站資訊(分批處理)
921
  all_pga_list = []
 
960
  for i in range(len(target_list)):
961
  target_padded[i] = target_list[i]
962
 
963
+ # 組合成 torch tensor
964
  tensor_data = {
965
  "waveform": torch.tensor(waveform_padded).unsqueeze(0).double(),
966
  "station": torch.tensor(station_info_padded).unsqueeze(0).double(),
967
  "target": torch.tensor(target_padded).unsqueeze(0).double(),
968
  }
969
 
970
+ # 執行預測
971
  with torch.no_grad():
972
  weight, sigma, mu = model(tensor_data)
973
  batch_pga = (
 
992
  pga_list, target_names, epicenter_lat, epicenter_lon
993
  )
994
 
995
+ # 載入實際觀測震度圖
996
  observed_intensity_path = load_observed_intensity_image(event_name)
997
 
998
+ logger.info("[步驟 3] 預測完成!")
999
  return observed_intensity_path, intensity_map
1000
 
1001
  except Exception as e:
1002
+ logger.error(f"[步驟 3] 發生錯誤: {e}")
1003
  import traceback
1004
 
1005
  traceback.print_exc()
1006
  return None, None
1007
 
1008
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1009
 
1010
  # ============ Gradio 介面 ============
1011
  with gr.Blocks(title="TTSAM 震度預測系統", fill_height=True) as demo:
 
1018
  gr.Markdown("## 使用步驟")
1019
  gr.Markdown(
1020
  """
1021
+ 1. 選擇地震事件(自動載入測站)
1022
+ 2. 調整 P 波後時間範圍(即時更新波形)
1023
+ 3. 點擊「執行預測」查看震度分布
 
1024
 
1025
+ 系統會自動選擇距離震央最近的 25 個測站,並快取資料避免重複讀檔。
1026
  """
1027
  )
1028
  with gr.Column(scale=1):
 
1070
  value=load_observed_intensity_image(list(earthquake_metadata.keys())[0]),
1071
  )
1072
 
1073
+ # ========== 隱藏的 State 變數(用於快取中間結果)==========
1074
+ cached_stream = gr.State(None) # ObsPy Stream object
1075
+ cached_stations = gr.State(None) # 選中的 25 個測站列表
1076
+ cached_waveforms = gr.State(None) # 提取的波形資料
1077
+ cached_station_info = gr.State(None) # 測站資訊列表
1078
+
1079
+ # ========== 事件綁定(使用鏈式觸發 + gr.State 快取)==========
1080
+
1081
+ # 【觸發點 1】事件切換:重新執行步驟 1 → 步驟 2
1082
  event_dropdown.change(
1083
+ fn=step1_load_mseed_and_select_stations,
1084
+ inputs=[event_dropdown],
1085
+ outputs=[cached_stream, cached_stations, input_station_map, waveform_plot, predict_btn]
1086
+ ).then( # 鏈式觸發步驟 2
1087
+ fn=step2_extract_and_plot_waveforms,
1088
+ inputs=[cached_stream, cached_stations, event_dropdown, duration_slider],
1089
+ outputs=[cached_waveforms, cached_station_info, waveform_plot, predict_btn]
1090
+ )
1091
+
1092
+ # 【觸發點 2】時間範圍調整:只重新執行步驟 2(快速,不重新讀檔)
1093
+ duration_slider.change(
1094
+ fn=step2_extract_and_plot_waveforms,
1095
+ inputs=[cached_stream, cached_stations, event_dropdown, duration_slider],
1096
+ outputs=[cached_waveforms, cached_station_info, waveform_plot, predict_btn]
1097
  )
1098
 
1099
+ # 【觸發點 3】載入波形按鈕:手動觸發步驟 1 → 步驟 2
1100
  load_waveform_btn.click(
1101
+ fn=step1_load_mseed_and_select_stations,
1102
+ inputs=[event_dropdown],
1103
+ outputs=[cached_stream, cached_stations, input_station_map, waveform_plot, predict_btn]
1104
+ ).then(
1105
+ fn=step2_extract_and_plot_waveforms,
1106
+ inputs=[cached_stream, cached_stations, event_dropdown, duration_slider],
1107
+ outputs=[cached_waveforms, cached_station_info, waveform_plot, predict_btn]
1108
  )
1109
 
1110
+ # 【觸發點 4】執行預測:使用快取的波形執行步驟 3
1111
  predict_btn.click(
1112
+ fn=step3_predict_intensity,
1113
+ inputs=[cached_waveforms, cached_station_info, event_dropdown],
1114
+ outputs=[observed_intensity_image, predicted_intensity_map]
1115
  )
1116
 
1117
+ # 【冷啟動】應用載入時自動執行步驟 1 → 步驟 2
1118
  demo.load(
1119
+ fn=step1_load_mseed_and_select_stations,
1120
+ inputs=[event_dropdown],
1121
+ outputs=[cached_stream, cached_stations, input_station_map, waveform_plot, predict_btn]
1122
+ ).then(
1123
+ fn=step2_extract_and_plot_waveforms,
1124
+ inputs=[cached_stream, cached_stations, event_dropdown, duration_slider],
1125
+ outputs=[cached_waveforms, cached_station_info, waveform_plot, predict_btn]
 
 
 
1126
  )
1127
 
1128
  demo.launch()