Lab 2d: DASNet Inference¶
In this lab, you will learn how to:
- Load one Monterey Bay DAS tile from Hugging Face (AI4EPS/quakeflow_das), same flow as Lab 2a;
- Run DASNet for detection, classification, masks, and arrival-oriented picks;
- Inspect saved JSON and quicklook figures;
- Optionally run the same pipeline through predict.py from the shell.
References:
- Zhang, C., et al. (2026). "A deep learning framework for marine acoustic and seismic monitoring with distributed acoustic sensing." arXiv:2603.14844.
- Romanowicz, B., et al. (2023). "SeaFOAM: A year‐long DAS deployment in Monterey Bay, California." SRL, 94(5), 2348-2359.
Setup¶
!pip install dasnet huggingface_hub -q
import json
import urllib.request
from pathlib import Path
import h5py
import matplotlib.pyplot as plt
import numpy as np
import torch
from IPython.display import Image, display
from huggingface_hub import hf_hub_download
from scipy.signal import sosfiltfilt
from dasnet.data.das import _safe_design_sos_bandpass, _safe_design_sos_highpass
from dasnet import (
build_dasnet_model,
default_device,
extract_peaks_for_instances,
filter_by_score,
forward_raw,
load_checkpoint,
make_infer_dataloader,
plot_das_predictions,
postprocess_batch,
save_predictions_json,
label_map
)
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {DEVICE}")
DS = 2 # downsample factor for plotting
def plot_das(ax, arr, nx, nt, dt_s, **kwargs):
"""Plot a downsampled DAS 2D array."""
arr = np.asarray(arr)
ax.imshow(
arr[::DS, ::DS],
aspect="auto",
interpolation="bilinear",
extent=[0, nt * dt_s, nx, 0],
**kwargs,
)
from scipy.signal import spectrogram
def plot_channel_spectrogram(
input_data,
channel_idx,
save_path,
view_idx=0,
fs=100.0,
nperseg=256,
noverlap=192,
mode="psd",
log_scale=True,
fmin=None,
fmax=None,
):
x = input_data
if isinstance(x, (list, tuple)) and len(x) == 1:
x = x[0]
arr = np.asarray(x, dtype=np.float32)
if arr.ndim == 3:
if not (0 <= view_idx < arr.shape[0]):
raise ValueError(f"view_idx={view_idx} out of range for input shape {arr.shape}")
image_data = arr[view_idx] # (time, channel)
elif arr.ndim == 2:
image_data = arr # (time, channel)
else:
raise ValueError(f"Expected 2D or 3D array, got shape {arr.shape}")
n_time, n_channel = image_data.shape
if not (0 <= channel_idx < n_channel):
raise ValueError(f"channel_idx={channel_idx} out of range [0, {n_channel-1}]")
trace = image_data[:, channel_idx]
f, t, Sxx = spectrogram(
trace,
fs=fs,
nperseg=nperseg,
noverlap=noverlap,
mode=mode,
scaling="density",
)
if log_scale:
Sxx = 10 * np.log10(Sxx + 1e-12)
if fmin is not None or fmax is not None:
mask = np.ones_like(f, dtype=bool)
if fmin is not None:
mask &= (f >= fmin)
if fmax is not None:
mask &= (f <= fmax)
f = f[mask]
Sxx = Sxx[mask, :]
plt.figure(figsize=(10, 4))
plt.pcolormesh(t, f, Sxx, shading="auto", cmap="viridis")
plt.colorbar(label="Power (dB)" if log_scale else mode)
plt.xlabel("Time (s)")
plt.ylabel("Frequency (Hz)")
plt.title(f"Spectrogram - view {view_idx}, channel {channel_idx}")
plt.tight_layout()
plt.savefig(save_path, bbox_inches="tight", dpi=300)
plt.close()
1. Load DAS Data¶
We use DAS data from SeaFOAM in Monterey Bay, California (Romanowicz et al., 2023). DASNet was trained on SeaFOAM. The deployment uses a ~52 km fiber with 5.2 m channel spacing at 200 Hz sampling.
Shallow parts of the cable are dominated by cultural and sea-surface noise, so we focus on the deep-water section (channel index > 7400).
Each workshop tile is an HDF5 file containing a 2D array of shape (nx, nt) — channels by time — plus metadata for the sample interval.
Set EVENT_ID in the next cell. The following figure shows raw strain rate, 2–10 Hz bandpass, and >10 Hz highpass.
HF_REPO = "AI4EPS/quakeflow_das"
EVENT_ID = "20231109T132510Z"
h5_path = hf_hub_download(
HF_REPO,
f"monterey_bay/data/{EVENT_ID}.h5",
repo_type="dataset",
local_dir="data/quakeflow_das",
)
SELECTED_H5 = str(Path(h5_path).resolve())
with h5py.File(SELECTED_H5, "r") as fp:
data = fp["data"][:]
attrs = dict(fp["data"].attrs)
dt_s = float(attrs.get("dt_s"))
begin_time = str(attrs.get("begin_time", ""))
nx, nt = data.shape
print(f"Event: {EVENT_ID}")
print(f"Shape: (nx={nx}, nt={nt})")
print(f"Sampling interval: {dt_s} s ({1/dt_s:.0f} Hz)")
print(f"Duration: {nt * dt_s:.1f} s")
if begin_time:
print(f"Begin time: {begin_time}")
Event: 20231109T132510Z Shape: (nx=2845, nt=12000) Sampling interval: 0.005000114440917969 s (200 Hz) Duration: 60.0 s Begin time: 2023-11-09T13:25:10.000000+00:00
# DASNet preprocesses strain rate into 3 channels:
# channel 0 = raw strain rate (z-score normalized)
# channel 1 = 2–10 Hz bandpass (z-score normalized)
# channel 2 = >10 Hz highpass (z-score normalized)
sos_bp = _safe_design_sos_bandpass(dt_s, 2.0, 10.0, order=4)
sos_hp = _safe_design_sos_highpass(dt_s, 10.0, order=4)
das_bp = sosfiltfilt(sos_bp, data, axis=1).astype(np.float32)
das_hp = sosfiltfilt(sos_hp, data, axis=1).astype(np.float32)
fig, axes = plt.subplots(3, 1, figsize=(12, 8))
titles = ["Raw strain rate", "2–10 Hz bandpass", ">10 Hz highpass"]
arrays = [data, das_bp, das_hp]
for ax, arr, title in zip(axes, arrays, titles):
vmax = 2 * float(np.percentile(np.abs(arr), 95))
plot_das(ax, arr, nx, nt, dt_s, cmap="seismic", vmin=-vmax, vmax=vmax)
ax.set_title(title)
ax.set_xlabel("Time (s)")
ax.set_ylabel("Channel index")
fig.suptitle(f"DAS waveform — {EVENT_ID}", y=1.02)
plt.tight_layout()
plt.show()
2. Run DASNet¶
DASNet outputs a class, confidence, box, and soft mask per detection; peaks along the mask summarize arrivals where applicable. The next cell downloads the workshop checkpoint, runs the selected tile (EVENT_ID above) through the model, and saves JSON plus a figure under pred_notebook/.
# Download checkpoint
CKPT_URL = "https://github.com/AI4EPS/models/releases/download/DASNet-v1/dasnet_v1.pth"
CKPT_PATH = Path("dasnet_v1.pth")
if not CKPT_PATH.exists():
print("Downloading checkpoint...")
urllib.request.urlretrieve(CKPT_URL, CKPT_PATH)
print(f"Checkpoint: {CKPT_PATH} ({CKPT_PATH.stat().st_size / 1e6:.0f} MB)")
# Build model
RESIZE_SCALE = 0.5
MIN_PROB = 0.8
device = default_device()
print(f"Building model...")
model = build_dasnet_model()
load_checkpoint(model, str(CKPT_PATH), device)
print(f"Model loaded on {device}")
# Prepare data
print(f"Loading and preprocessing {SELECTED_H5}...")
loader, _ = make_infer_dataloader(
[SELECTED_H5],
batch_size=1,
num_workers=0,
resize_scale=RESIZE_SCALE,
storage_backend="local",
)
images, names = next(iter(loader))
print(f"Input shape: {images[0].shape}")
# Inference
print("Running inference...")
raw_outputs = forward_raw(model, list(images), device)
_, processed = postprocess_batch(names, raw_outputs)
selected = filter_by_score(processed[0], MIN_PROB)
peak_points_list, peak_scores_list = extract_peaks_for_instances(selected)
print(f"Detected {len(selected['scores'])} instances (min_prob={MIN_PROB})")
# Save results
RESULT_DIR = Path("pred_notebook")
FIG_DIR = RESULT_DIR / "figures_dasnet"
RESULT_DIR.mkdir(parents=True, exist_ok=True)
FIG_DIR.mkdir(parents=True, exist_ok=True)
fn0 = names[0]
np_selected = {k: (v.detach().cpu().numpy() if torch.is_tensor(v) else v) for k, v in selected.items()}
save_predictions_json(fn0, np_selected, peak_points_list, peak_scores_list, str(RESULT_DIR), resize_scale=RESIZE_SCALE)
print("Generating figure (this may take a moment)...")
fig_path = FIG_DIR / (Path(fn0).stem + ".jpg")
plot_das_predictions(images[0].cpu().numpy(), np_selected, str(fig_path), score_threshold=MIN_PROB)
spe_path = FIG_DIR / (Path(fn0).stem + "_spe.jpg")
plot_channel_spectrogram(images[0].cpu().numpy(),
channel_idx=1000,
save_path=spe_path,
view_idx=0,
fs=100.0,
nperseg=500,
noverlap=480,
fmin=0,
fmax=50,
)
print(f"Saved JSON + figure → {RESULT_DIR}")
Checkpoint: dasnet_v1.pth (415 MB) Building model... Model loaded on cuda Loading and preprocessing /global/scratch/users/zhuwq0/DAS_Seismology_Workshop/notebooks/lab2_phasenet_das/Notebooks/data/quakeflow_das/monterey_bay/data/20231109T132510Z.h5... Input shape: torch.Size([3, 6000, 1422]) Running inference... Detected 6 instances (min_prob=0.8) Generating figure (this may take a moment)... Saved JSON + figure → pred_notebook
3. Check output¶
Open the quicklook figure and JSON for the same tile as EVENT_ID / Run DASNet. Each instance stores the box and (when present) pick coordinates in full-resolution channel/time units after undoing the resize, together with class id, score, and per-pick mask scores.
if fig_path.exists():
display(Image(filename=str(fig_path)))
if spe_path.exists():
display(Image(filename=str(spe_path)))
# Summary table of detections
json_path = RESULT_DIR / (Path(fn0).stem + ".json")
payload = json.loads(json_path.read_text())
print(f"File: {payload['file_name']} | Resize scale: {payload['resize_scale']}")
print(f"{'#':<4} {'Class':<16} {'Score':>6} {'Box (ch_min, t_min, ch_max, t_max)':<45} {'Picks':>6}")
print("-" * 82)
for i, inst in enumerate(payload.get("instances", [])):
name = inst.get("label_name", label_map.get(inst["label"], str(inst["label"])))
box = ", ".join(f"{v:.0f}" for v in inst["box"])
n_picks = len(inst.get("picks", []))
print(f"{i:<4} {name:<16} {inst['score']:>6.3f} {box:<45} {n_picks:>6}")
File: 20231109T132510Z.h5 | Resize scale: 0.5 # Class Score Box (ch_min, t_min, ch_max, t_max) Picks ---------------------------------------------------------------------------------- 0 Fin whale 1.000 164, 909, 2844, 3043 1339 1 Fin whale 0.999 167, 9504, 2841, 11616 1335 2 Fin whale 0.999 301, 5275, 2835, 7269 1263 3 Fin whale 0.998 501, 8803, 2844, 10269 1170 4 Fin whale 0.995 561, 209, 2844, 1575 1139 5 Fin whale 0.992 720, 4532, 2844, 5964 1060
Optional: Batch inference¶
Run the same pipeline on multiple events. Each tile is downloaded from Hugging Face, processed through DASNet, and saved as JSON + quicklook figure.
from tqdm.auto import tqdm
BATCH_EVENT_IDS = [
"20231116T130210Z",
"20231110T105410Z",
"20231124T104510Z",
"20231130T194610Z",
"20240204T004410Z",
"20240428T100822Z",
"20220828T062358Z",
"20220828T140758Z",
"20230407T074209Z",
]
# Download all tiles
h5_paths = []
for eid in tqdm(BATCH_EVENT_IDS, desc="Downloading", unit="file"):
p = hf_hub_download(
"AI4EPS/quakeflow_das",
f"monterey_bay/data/{eid}.h5",
repo_type="dataset",
local_dir="data/quakeflow_das",
)
h5_paths.append(str(Path(p).resolve()))
# Batch inference
BATCH_DIR = Path("pred_batch")
BATCH_FIG_DIR = BATCH_DIR / "figures_dasnet"
BATCH_DIR.mkdir(parents=True, exist_ok=True)
BATCH_FIG_DIR.mkdir(parents=True, exist_ok=True)
loader, _ = make_infer_dataloader(h5_paths, batch_size=1, num_workers=0, resize_scale=RESIZE_SCALE, storage_backend="local")
for images, names in tqdm(loader, desc="Predicting", unit="file"):
raw_out = forward_raw(model, list(images), device)
fnames, results = postprocess_batch(names, raw_out)
for i, fn in enumerate(fnames):
sel = filter_by_score(results[i], MIN_PROB)
if len(sel["scores"]) == 0:
continue
pts, scores = extract_peaks_for_instances(sel)
np_sel = {k: (v.detach().cpu().numpy() if torch.is_tensor(v) else v) for k, v in sel.items()}
save_predictions_json(fn, np_sel, pts, scores, str(BATCH_DIR), resize_scale=RESIZE_SCALE)
plot_das_predictions(images[i].cpu().numpy(), np_sel, str(BATCH_FIG_DIR / (Path(fn).stem + ".jpg")), score_threshold=MIN_PROB)
n_json = len(list(BATCH_DIR.glob("*.json")))
print(f"Done. {n_json} JSON files saved to {BATCH_DIR}")
Downloading: 100%|██████████| 9/9 [00:01<00:00, 8.17file/s] Predicting: 100%|██████████| 9/9 [00:39<00:00, 4.42s/file]
Done. 9 JSON files saved to pred_batch
FIG_PREVIEW_N = None # None = show all; set int to limit
figs = sorted(BATCH_FIG_DIR.glob("*.jpg"))
if FIG_PREVIEW_N is not None:
figs = figs[:FIG_PREVIEW_N]
print(f"Showing {len(figs)} of {len(list(BATCH_FIG_DIR.glob('*.jpg')))} figures")
for fig in figs:
print(fig.stem)
display(Image(filename=str(fig)))
print()
Showing 9 of 9 figures 20220828T062358Z
20220828T140758Z
20230407T074209Z
20231110T105410Z
20231116T130210Z
20231124T104510Z
20231130T194610Z
20240204T004410Z
20240428T100822Z