PhaseNet 0.1.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. phasenet-0.2.1/.github/workflows/publish.yml +28 -0
  2. phasenet-0.2.1/.gitignore +15 -0
  3. phasenet-0.2.1/PKG-INFO +163 -0
  4. phasenet-0.2.1/README.md +133 -0
  5. phasenet-0.2.1/docs/notebooks/lab2a_phasenet_das_inference.ipynb +285 -0
  6. phasenet-0.2.1/docs/notebooks/lab2b_pick_association.ipynb +195 -0
  7. phasenet-0.2.1/docs/notebooks/lab2c_semisupervised_training.ipynb +487 -0
  8. phasenet-0.2.1/examples/ceed_training_plots.py +181 -0
  9. phasenet-0.2.1/examples/italy/prepare_inventory.py +233 -0
  10. phasenet-0.2.1/examples/italy/prepare_mseed_list.py +118 -0
  11. phasenet-0.2.1/examples/italy/run_all_years.sh +64 -0
  12. phasenet-0.2.1/examples/italy/upload_loop.sh +12 -0
  13. phasenet-0.2.1/examples/italy/upload_phasenet_origin.sh +47 -0
  14. phasenet-0.2.1/phasenet/__init__.py +3 -0
  15. phasenet-0.2.1/phasenet/data/__init__.py +7 -0
  16. phasenet-0.2.1/phasenet/data/ceed.py +2294 -0
  17. phasenet-0.2.1/phasenet/data/das.py +2555 -0
  18. phasenet-0.2.1/phasenet/data/transforms.py +776 -0
  19. phasenet-0.2.1/phasenet/models/__init__.py +8 -0
  20. phasenet-0.2.1/phasenet/models/phasenet.py +480 -0
  21. phasenet-0.2.1/phasenet/models/phasenet_das.py +75 -0
  22. phasenet-0.2.1/phasenet/models/phasenet_das_plus.py +46 -0
  23. phasenet-0.2.1/phasenet/models/phasenet_plus.py +27 -0
  24. phasenet-0.2.1/phasenet/models/phasenet_tf.py +21 -0
  25. phasenet-0.2.1/phasenet/models/phasenet_tf_plus.py +27 -0
  26. phasenet-0.2.1/phasenet/models/prompt/__init__.py +11 -0
  27. phasenet-0.2.1/phasenet/models/prompt/common.py +43 -0
  28. phasenet-0.2.1/phasenet/models/prompt/mask_decoder.py +185 -0
  29. phasenet-0.2.1/phasenet/models/prompt/prompt_encoder.py +242 -0
  30. phasenet-0.2.1/phasenet/models/prompt/transformer.py +232 -0
  31. phasenet-0.2.1/phasenet/models/unet.py +1316 -0
  32. phasenet-0.2.1/phasenet/models/unet2018.py +253 -0
  33. phasenet-0.2.1/phasenet/utils/__init__.py +3 -0
  34. PhaseNet-0.1.0/phasenet/detect_peaks.py → phasenet-0.2.1/phasenet/utils/detect_peaks_cpu.py +46 -14
  35. phasenet-0.2.1/phasenet/utils/inference.py +405 -0
  36. phasenet-0.2.1/phasenet/utils/postprocess.py +711 -0
  37. phasenet-0.2.1/phasenet/utils/visualization.py +1465 -0
  38. phasenet-0.2.1/phasenet.egg-info/PKG-INFO +163 -0
  39. phasenet-0.2.1/phasenet.egg-info/SOURCES.txt +58 -0
  40. phasenet-0.2.1/phasenet.egg-info/entry_points.txt +3 -0
  41. phasenet-0.2.1/phasenet.egg-info/requires.txt +16 -0
  42. phasenet-0.2.1/predict.py +630 -0
  43. phasenet-0.2.1/pyproject.toml +48 -0
  44. phasenet-0.2.1/requirements.txt +16 -0
  45. phasenet-0.2.1/scripts/predict_ceed.py +489 -0
  46. phasenet-0.2.1/scripts/predict_ceed.sh +41 -0
  47. phasenet-0.2.1/scripts/predict_das.py +607 -0
  48. phasenet-0.2.1/scripts/predict_das.sh +55 -0
  49. phasenet-0.2.1/scripts/predict_mseed.py +487 -0
  50. phasenet-0.2.1/scripts/semisupervised_das.sh +221 -0
  51. phasenet-0.2.1/scripts/train_ceed.sh +21 -0
  52. phasenet-0.2.1/scripts/train_das.sh +57 -0
  53. phasenet-0.2.1/tests/benchmark_dataloader.py +373 -0
  54. phasenet-0.2.1/tests/test_augmentations.py +234 -0
  55. phasenet-0.2.1/tests/test_labels.py +288 -0
  56. phasenet-0.2.1/train.py +807 -0
  57. phasenet-0.2.1/utils.py +488 -0
  58. PhaseNet-0.1.0/PKG-INFO +0 -20
  59. PhaseNet-0.1.0/PhaseNet.egg-info/PKG-INFO +0 -20
  60. PhaseNet-0.1.0/PhaseNet.egg-info/SOURCES.txt +0 -14
  61. PhaseNet-0.1.0/PhaseNet.egg-info/requires.txt +0 -9
  62. PhaseNet-0.1.0/README.md +0 -124
  63. PhaseNet-0.1.0/phasenet/__init__.py +0 -1
  64. PhaseNet-0.1.0/phasenet/data_reader.py +0 -520
  65. PhaseNet-0.1.0/phasenet/model.py +0 -467
  66. PhaseNet-0.1.0/phasenet/predict.py +0 -143
  67. PhaseNet-0.1.0/phasenet/train.py +0 -297
  68. PhaseNet-0.1.0/phasenet/util.py +0 -203
  69. PhaseNet-0.1.0/setup.py +0 -116
  70. {PhaseNet-0.1.0/PhaseNet.egg-info → phasenet-0.2.1/phasenet.egg-info}/dependency_links.txt +0 -0
  71. {PhaseNet-0.1.0/PhaseNet.egg-info → phasenet-0.2.1/phasenet.egg-info}/top_level.txt +0 -0
  72. {PhaseNet-0.1.0 → phasenet-0.2.1}/setup.cfg +0 -0
@@ -0,0 +1,28 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ id-token: write
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.10"
20
+
21
+ - name: Install build tools
22
+ run: pip install build
23
+
24
+ - name: Build package
25
+ run: python -m build
26
+
27
+ - name: Publish to PyPI
28
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,15 @@
1
+ docs/notebooks/data/
2
+ figures_backup/
3
+ figures/
4
+ results/
5
+ output/
6
+ __pycache__/
7
+ wandb/
8
+ ToDelete/
9
+ phasenet.egg-info/
10
+ CLAUDE.md
11
+ *.pth
12
+ *.png
13
+ *.pdf
14
+ *.whl
15
+ *.gzdocs/notebooks/*_executed.ipynb
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: phasenet
3
+ Version: 0.2.1
4
+ Summary: A PyTorch implementation of PhaseNet for seismic and DAS phase picking
5
+ Author: Weiqiang Zhu
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/AI4EPS/phasenet-pytorch
8
+ Project-URL: Repository, https://github.com/AI4EPS/phasenet-pytorch
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Scientific/Engineering
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: torch
15
+ Requires-Dist: torchvision
16
+ Requires-Dist: einops
17
+ Requires-Dist: numpy
18
+ Requires-Dist: scipy
19
+ Requires-Dist: h5py
20
+ Requires-Dist: matplotlib
21
+ Requires-Dist: pandas
22
+ Requires-Dist: tqdm
23
+ Requires-Dist: fsspec
24
+ Requires-Dist: obspy
25
+ Requires-Dist: gcsfs
26
+ Requires-Dist: datasets
27
+ Requires-Dist: pyarrow
28
+ Requires-Dist: wandb
29
+ Requires-Dist: scikit-learn
30
+
31
+ # PhaseNet-PyTorch
32
+
33
+ PyTorch implementation of PhaseNet for seismic and DAS phase picking, event detection, and polarity classification.
34
+
35
+ ## Models
36
+
37
+ | Model | Features | Data Type |
38
+ |-------|----------|-----------|
39
+ | `phasenet` | Phase (P/S) picking | Seismic 3-component |
40
+ | `phasenet_tf` | Phase picking + STFT spectrogram | Seismic 3-component |
41
+ | `phasenet_plus` | Phase + polarity + event detection | Seismic 3-component |
42
+ | `phasenet_tf_plus` | Phase + polarity + event detection + STFT | Seismic 3-component |
43
+ | `phasenet_das` | Phase picking | DAS single-channel |
44
+ | `phasenet_das_plus` | Phase + event detection | DAS single-channel |
45
+
46
+ The `_tf` variants add a Short-Time Fourier Transform (STFT) branch that extracts frequency features alongside the temporal waveform, improving performance on noisy data.
47
+
48
+ ## Prediction
49
+
50
+ ### CEED (Seismic) Prediction
51
+
52
+ ```bash
53
+ # Demo: process a few events with plots
54
+ python scripts/predict_ceed.py --n-events 5
55
+
56
+ # Process all days for a year (saves parquets to results/ceed/)
57
+ python scripts/predict_ceed.py --all --year 2025 --output-dir results/ceed
58
+ ```
59
+
60
+ Output: one parquet per day file in `results/ceed/{region}/`, with columns:
61
+ `event_id, station_id, waveform_index, origin_id, origin_index, origin_time, phase_index, phase_time, phase_score, phase_type, phase_polarity`
62
+
63
+ ### DAS Prediction
64
+
65
+ ```bash
66
+ # Predict with base PhaseNet on a HuggingFace subset
67
+ python scripts/predict_das.py --subset arcata --plot
68
+
69
+ # Predict with a trained DAS model on local data
70
+ python scripts/predict_das.py \
71
+ --data-dir data/quakeflow_das/arcata/data \
72
+ --model-type phasenet_das_plus \
73
+ --checkpoint output/train_das_arcata/checkpoint.pth \
74
+ --output-dir results/das/train_das_arcata/arcata \
75
+ --no-ema --plot
76
+
77
+ # Predict from a file list
78
+ python scripts/predict_das.py \
79
+ --file-list file_list.txt \
80
+ --model-type phasenet_das_plus \
81
+ --checkpoint output/model.pth \
82
+ --plot
83
+
84
+ # Multi-GPU prediction
85
+ bash scripts/predict_das.sh phasenet_das_plus arcata 8 output/model.pth
86
+ ```
87
+
88
+ Output: one parquet per event in `results/das/{model_name}/{subset}/`, with columns:
89
+ `event_id, channel_index, origin_id, origin_index, origin_time, phase_index, phase_time, phase_score, phase_type, dt_s, ps_center, ps_interval`
90
+
91
+ By default, picks are associated using P-S pairing. Use `--use-event-head` to associate via the model's event detection head instead.
92
+
93
+ ## Training
94
+
95
+ ### CEED (Seismic) Training
96
+
97
+ ```bash
98
+ python train.py \
99
+ --model phasenet_plus \
100
+ --dataset-type ceed \
101
+ --label-path results/ceed \
102
+ --nx 16 \
103
+ --max-iters 100000 \
104
+ --batch-size 8 \
105
+ --workers 4 \
106
+ --lr 3e-4 \
107
+ --eval-interval 5000 \
108
+ --output-dir output/train_ceed
109
+ ```
110
+
111
+ ### DAS Training
112
+
113
+ ```bash
114
+ # Using the training script
115
+ bash scripts/train_das.sh 0 arcata v26
116
+
117
+ # Or directly
118
+ python train.py \
119
+ --model phasenet_das_plus \
120
+ --dataset-type das \
121
+ --data-path data/quakeflow_das/arcata/data \
122
+ --label-path results/das/phasenet/arcata/picks \
123
+ --label-list results/das/phasenet/arcata/labels.txt \
124
+ --nx 2048 --nt 4096 \
125
+ --num-patch 16 \
126
+ --max-iters 50000 \
127
+ --batch-size 2 --workers 8 \
128
+ --lr 1e-4 --weight-decay 0.01 \
129
+ --model-ema --model-ema-decay 0.999 \
130
+ --eval-interval 1000 --save-interval 1000 \
131
+ --output-dir output/train_das_arcata_v26
132
+ ```
133
+
134
+ ### Key Training Options
135
+
136
+ | Option | Description | Default |
137
+ |--------|-------------|---------|
138
+ | `--num-patch N` | Random crops per DAS sample (amortizes IO) | 2 |
139
+ | `--model-ema` | Enable exponential moving average | off |
140
+ | `--gradient-accumulation-steps N` | Accumulate gradients for larger effective batch | 1 |
141
+ | `--clip-grad-norm V` | Gradient clipping | 1.0 |
142
+ | `--compile` | Enable torch.compile | off |
143
+ | `--resume --checkpoint PATH` | Resume from checkpoint | - |
144
+ | `--reset-lr` | Reset LR schedule when resuming | off |
145
+
146
+ ## Semi-supervised Training
147
+
148
+ Iterative self-training pipeline for DAS: predict → train on predictions → predict with new model → repeat.
149
+
150
+ ```bash
151
+ # Start from PhaseNet (train DAS model from scratch)
152
+ bash scripts/semisupervised_das.sh arcata 5 0
153
+
154
+ # Start from a pretrained DAS model
155
+ bash scripts/semisupervised_das.sh arcata 5 0 phasenet_das output/train_das_v26/checkpoint.pth
156
+ ```
157
+
158
+ Arguments: `[subset] [num_iterations] [gpu] [start_from] [checkpoint]`
159
+
160
+ - **From phasenet**: iteration 0 predicts with PhaseNet, iteration 1 trains from scratch (10k steps, warmup), iterations 2+ continue (1k steps, no warmup)
161
+ - **From phasenet_das**: iteration 0 predicts with pretrained DAS model, iterations 1+ continue (1k steps, no warmup)
162
+
163
+ Results are saved to `results/semisupervised_das/` and checkpoints to `output/semisupervised_das_v{N}/`.
@@ -0,0 +1,133 @@
1
+ # PhaseNet-PyTorch
2
+
3
+ PyTorch implementation of PhaseNet for seismic and DAS phase picking, event detection, and polarity classification.
4
+
5
+ ## Models
6
+
7
+ | Model | Features | Data Type |
8
+ |-------|----------|-----------|
9
+ | `phasenet` | Phase (P/S) picking | Seismic 3-component |
10
+ | `phasenet_tf` | Phase picking + STFT spectrogram | Seismic 3-component |
11
+ | `phasenet_plus` | Phase + polarity + event detection | Seismic 3-component |
12
+ | `phasenet_tf_plus` | Phase + polarity + event detection + STFT | Seismic 3-component |
13
+ | `phasenet_das` | Phase picking | DAS single-channel |
14
+ | `phasenet_das_plus` | Phase + event detection | DAS single-channel |
15
+
16
+ The `_tf` variants add a Short-Time Fourier Transform (STFT) branch that extracts frequency features alongside the temporal waveform, improving performance on noisy data.
17
+
18
+ ## Prediction
19
+
20
+ ### CEED (Seismic) Prediction
21
+
22
+ ```bash
23
+ # Demo: process a few events with plots
24
+ python scripts/predict_ceed.py --n-events 5
25
+
26
+ # Process all days for a year (saves parquets to results/ceed/)
27
+ python scripts/predict_ceed.py --all --year 2025 --output-dir results/ceed
28
+ ```
29
+
30
+ Output: one parquet per day file in `results/ceed/{region}/`, with columns:
31
+ `event_id, station_id, waveform_index, origin_id, origin_index, origin_time, phase_index, phase_time, phase_score, phase_type, phase_polarity`
32
+
33
+ ### DAS Prediction
34
+
35
+ ```bash
36
+ # Predict with base PhaseNet on a HuggingFace subset
37
+ python scripts/predict_das.py --subset arcata --plot
38
+
39
+ # Predict with a trained DAS model on local data
40
+ python scripts/predict_das.py \
41
+ --data-dir data/quakeflow_das/arcata/data \
42
+ --model-type phasenet_das_plus \
43
+ --checkpoint output/train_das_arcata/checkpoint.pth \
44
+ --output-dir results/das/train_das_arcata/arcata \
45
+ --no-ema --plot
46
+
47
+ # Predict from a file list
48
+ python scripts/predict_das.py \
49
+ --file-list file_list.txt \
50
+ --model-type phasenet_das_plus \
51
+ --checkpoint output/model.pth \
52
+ --plot
53
+
54
+ # Multi-GPU prediction
55
+ bash scripts/predict_das.sh phasenet_das_plus arcata 8 output/model.pth
56
+ ```
57
+
58
+ Output: one parquet per event in `results/das/{model_name}/{subset}/`, with columns:
59
+ `event_id, channel_index, origin_id, origin_index, origin_time, phase_index, phase_time, phase_score, phase_type, dt_s, ps_center, ps_interval`
60
+
61
+ By default, picks are associated using P-S pairing. Use `--use-event-head` to associate via the model's event detection head instead.
62
+
63
+ ## Training
64
+
65
+ ### CEED (Seismic) Training
66
+
67
+ ```bash
68
+ python train.py \
69
+ --model phasenet_plus \
70
+ --dataset-type ceed \
71
+ --label-path results/ceed \
72
+ --nx 16 \
73
+ --max-iters 100000 \
74
+ --batch-size 8 \
75
+ --workers 4 \
76
+ --lr 3e-4 \
77
+ --eval-interval 5000 \
78
+ --output-dir output/train_ceed
79
+ ```
80
+
81
+ ### DAS Training
82
+
83
+ ```bash
84
+ # Using the training script
85
+ bash scripts/train_das.sh 0 arcata v26
86
+
87
+ # Or directly
88
+ python train.py \
89
+ --model phasenet_das_plus \
90
+ --dataset-type das \
91
+ --data-path data/quakeflow_das/arcata/data \
92
+ --label-path results/das/phasenet/arcata/picks \
93
+ --label-list results/das/phasenet/arcata/labels.txt \
94
+ --nx 2048 --nt 4096 \
95
+ --num-patch 16 \
96
+ --max-iters 50000 \
97
+ --batch-size 2 --workers 8 \
98
+ --lr 1e-4 --weight-decay 0.01 \
99
+ --model-ema --model-ema-decay 0.999 \
100
+ --eval-interval 1000 --save-interval 1000 \
101
+ --output-dir output/train_das_arcata_v26
102
+ ```
103
+
104
+ ### Key Training Options
105
+
106
+ | Option | Description | Default |
107
+ |--------|-------------|---------|
108
+ | `--num-patch N` | Random crops per DAS sample (amortizes IO) | 2 |
109
+ | `--model-ema` | Enable exponential moving average | off |
110
+ | `--gradient-accumulation-steps N` | Accumulate gradients for larger effective batch | 1 |
111
+ | `--clip-grad-norm V` | Gradient clipping | 1.0 |
112
+ | `--compile` | Enable torch.compile | off |
113
+ | `--resume --checkpoint PATH` | Resume from checkpoint | - |
114
+ | `--reset-lr` | Reset LR schedule when resuming | off |
115
+
116
+ ## Semi-supervised Training
117
+
118
+ Iterative self-training pipeline for DAS: predict → train on predictions → predict with new model → repeat.
119
+
120
+ ```bash
121
+ # Start from PhaseNet (train DAS model from scratch)
122
+ bash scripts/semisupervised_das.sh arcata 5 0
123
+
124
+ # Start from a pretrained DAS model
125
+ bash scripts/semisupervised_das.sh arcata 5 0 phasenet_das output/train_das_v26/checkpoint.pth
126
+ ```
127
+
128
+ Arguments: `[subset] [num_iterations] [gpu] [start_from] [checkpoint]`
129
+
130
+ - **From phasenet**: iteration 0 predicts with PhaseNet, iteration 1 trains from scratch (10k steps, warmup), iterations 2+ continue (1k steps, no warmup)
131
+ - **From phasenet_das**: iteration 0 predicts with pretrained DAS model, iterations 1+ continue (1k steps, no warmup)
132
+
133
+ Results are saved to `results/semisupervised_das/` and checkpoints to `output/semisupervised_das_v{N}/`.
@@ -0,0 +1,285 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/AI4EPS/phasenet-pytorch/blob/main/docs/notebooks/lab2a_phasenet_das_inference.ipynb)\n\n# Lab 2a: PhaseNet-DAS Inference\n\nIn this lab, you will learn how to:\n- Load DAS (Distributed Acoustic Sensing) data from the USGS Arcata array\n- Apply PhaseNet for P/S phase picking on DAS data\n- Apply PhaseNet-DAS+, a DAS-specific model with event detection\n- Compare picking quality between a general seismic model and a DAS-specific model\n\n**References:**\n- Zhu, W. & Beroza, G. (2019). PhaseNet: a deep-neural-network-based seismic arrival-time picking method. *GJI*.\n- Zhu, W. et al. (2023). Seismic arrival-time picking on distributed acoustic sensing data using semi-supervised learning. *Nature Communications*, 14, 8192.\n- Zhu, W. et al. (2025). Towards end-to-end earthquake monitoring using a multitask deep learning model. [arXiv:2506.06939](https://arxiv.org/abs/2506.06939)\n- McGuire, J.J. et al. (2025). The GorDAS DAS Experiment Above the Cascadia Locked Zone and Subducted Gorda Slab. *SRL*, 96(4), 2489-2503.\n\n---"
7
+ },
8
+ {
9
+ "cell_type": "markdown",
10
+ "metadata": {},
11
+ "source": [
12
+ "## Setup"
13
+ ]
14
+ },
15
+ {
16
+ "cell_type": "code",
17
+ "execution_count": null,
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": "# Install phasenet (only needed once)\n# !pip install phasenet"
21
+ },
22
+ {
23
+ "cell_type": "code",
24
+ "execution_count": null,
25
+ "metadata": {},
26
+ "outputs": [],
27
+ "source": "import os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport h5py\nimport torch\nfrom huggingface_hub import hf_hub_download\n\nimport phasenet.models\nfrom phasenet.utils.inference import predict_2d, detect_picks\nfrom phasenet.utils.postprocess import (\n estimate_origins_from_ps_pairs,\n associate_picks,\n)\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"Using device: {DEVICE}\")\n\nDS = 10 # downsample factor for plotting\n\ndef plot_das(ax, arr, nx, nt, dt_s, **kwargs):\n \"\"\"Plot a downsampled DAS 2D array.\"\"\"\n arr = np.asarray(arr)\n ax.imshow(arr[::DS, ::DS], aspect=\"auto\", interpolation=\"bilinear\",\n extent=[0, nt * dt_s, nx, 0], **kwargs)\n\ndef plot_picks(ax, df, dt_s):\n \"\"\"Plot P and S picks.\"\"\"\n for phase, color in [(\"P\", \"red\"), (\"S\", \"blue\")]:\n sub = df[df[\"phase_type\"] == phase]\n ax.scatter(sub[\"phase_index\"] * dt_s, sub[\"channel_index\"].astype(int),\n s=0.5, c=color, alpha=0.3,\n label=f\"{phase} ({len(sub)})\")"
28
+ },
29
+ {
30
+ "cell_type": "markdown",
31
+ "metadata": {},
32
+ "source": "## 1. Load DAS Data\n\nWe use DAS data from the USGS Arcata array in Northern California (McGuire et al. 2025), deployed to monitor the 2022 M6.4 Ferndale aftershock sequence. The array records strain rate along a fiber-optic cable with ~2m channel spacing at 250 Hz sampling rate.\n\nWe chose this dataset because:\n1. It is **independent** from any data used in previous PhaseNet/PhaseNet-DAS development — allowing unbiased evaluation\n2. It is a **small, self-contained** dataset (~2,470 events) — ideal for demonstrating how semi-supervised learning can rapidly improve a model on a new DAS array\n\nEach event file is an H5 file containing a 2D array of shape `(nx, nt)` — number of spatial channels by number of time samples."
33
+ },
34
+ {
35
+ "cell_type": "code",
36
+ "execution_count": null,
37
+ "metadata": {},
38
+ "outputs": [],
39
+ "source": [
40
+ "HF_REPO = \"AI4EPS/quakeflow_das\"\n",
41
+ "\n",
42
+ "# Download a strong event with clear P and S arrivals\n",
43
+ "EVENT_ID = \"20230106T122215Z\"\n",
44
+ "h5_path = hf_hub_download(\n",
45
+ " HF_REPO, f\"arcata/data/{EVENT_ID}.h5\", repo_type=\"dataset\",\n",
46
+ " local_dir=\"data/quakeflow_das\",\n",
47
+ ")\n",
48
+ "\n",
49
+ "with h5py.File(h5_path, \"r\") as fp:\n",
50
+ " data = fp[\"data\"][:]\n",
51
+ " attrs = dict(fp[\"data\"].attrs)\n",
52
+ "\n",
53
+ "dt_s = float(attrs.get(\"dt_s\", 0.008))\n",
54
+ "begin_time = str(attrs.get(\"begin_time\", \"\"))\n",
55
+ "nx, nt = data.shape\n",
56
+ "\n",
57
+ "print(f\"Event: {EVENT_ID}\")\n",
58
+ "print(f\"Shape: (nx={nx}, nt={nt})\")\n",
59
+ "print(f\"Sampling interval: {dt_s} s ({1/dt_s:.0f} Hz)\")\n",
60
+ "print(f\"Duration: {nt * dt_s:.1f} s\")\n",
61
+ "print(f\"Begin time: {begin_time}\")"
62
+ ]
63
+ },
64
+ {
65
+ "cell_type": "code",
66
+ "execution_count": null,
67
+ "metadata": {},
68
+ "outputs": [],
69
+ "source": "# Plot raw DAS waveform\nfig, ax = plt.subplots(figsize=(12, 8))\nvmax = 2 * np.percentile(np.abs(data), 95)\nplot_das(ax, data, nx, nt, dt_s, cmap=\"seismic\", vmin=-vmax, vmax=vmax)\nax.set_xlabel(\"Time (s)\")\nax.set_ylabel(\"Channel index\")\nax.set_title(f\"Raw DAS waveform — {EVENT_ID}\")\nplt.tight_layout()\nplt.show()"
70
+ },
71
+ {
72
+ "cell_type": "markdown",
73
+ "metadata": {},
74
+ "source": [
75
+ "## 2. Run PhaseNet (Seismic Model)\n",
76
+ "\n",
77
+ "PhaseNet (Zhu & Beroza, 2019) is a deep learning model originally designed for 3-component seismic data. It can be applied to single-component DAS data by treating the DAS channel as the vertical (Z) component and zero-padding the other two components.\n",
78
+ "\n",
79
+ "PhaseNet processes each channel independently — it sees `(3, 1, nt)` per channel, treating each DAS channel as a single seismic station."
80
+ ]
81
+ },
82
+ {
83
+ "cell_type": "code",
84
+ "execution_count": null,
85
+ "metadata": {},
86
+ "outputs": [],
87
+ "source": [
88
+ "# Prepare input: DAS → 3-component format (E=0, N=0, Z=data)\n",
89
+ "waveform_3ch = np.zeros((3, nx, nt), dtype=np.float32)\n",
90
+ "waveform_3ch[2] = data.astype(np.float32) # Z-component\n",
91
+ "\n",
92
+ "print(f\"Input shape: {waveform_3ch.shape} (3 channels, {nx} stations, {nt} samples)\")"
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "code",
97
+ "execution_count": null,
98
+ "metadata": {},
99
+ "outputs": [],
100
+ "source": [
101
+ "# Load pretrained PhaseNet model\n",
102
+ "import urllib.request\n",
103
+ "\n",
104
+ "MODELS_REPO = \"AI4EPS/models\"\n",
105
+ "\n",
106
+ "def download_model(release, filename):\n",
107
+ " \"\"\"Download a model from GitHub releases (cached locally).\"\"\"\n",
108
+ " cache_dir = os.path.expanduser(f\"~/.cache/phasenet/{release}\")\n",
109
+ " local_path = os.path.join(cache_dir, filename)\n",
110
+ " if not os.path.exists(local_path):\n",
111
+ " os.makedirs(cache_dir, exist_ok=True)\n",
112
+ " url = f\"https://github.com/{MODELS_REPO}/releases/download/{release}/{filename}\"\n",
113
+ " print(f\"Downloading {url}\")\n",
114
+ " urllib.request.urlretrieve(url, local_path)\n",
115
+ " return local_path\n",
116
+ "\n",
117
+ "# Load PhaseNet-2018\n",
118
+ "ckpt_path = download_model(\"PhaseNet-2018\", \"phasenet2018.pth\")\n",
119
+ "ckpt = torch.load(ckpt_path, map_location=\"cpu\", weights_only=False)\n",
120
+ "model_pn = phasenet.models.phasenet.build_model(\n",
121
+ " backbone=\"unet2018\", **ckpt.get(\"model_config\", {})\n",
122
+ ")\n",
123
+ "model_pn.load_state_dict(ckpt[\"model\"])\n",
124
+ "model_pn.to(DEVICE).eval()\n",
125
+ "print(\"PhaseNet-2018 loaded\")"
126
+ ]
127
+ },
128
+ {
129
+ "cell_type": "code",
130
+ "execution_count": null,
131
+ "metadata": {},
132
+ "outputs": [],
133
+ "source": [
134
+ "# Run inference\n",
135
+ "# PhaseNet uses a sliding window of (256 channels, 3000 samples)\n",
136
+ "NX_PN, NT_PN = 256, 3000\n",
137
+ "MIN_PROB = 0.3\n",
138
+ "\n",
139
+ "with torch.no_grad():\n",
140
+ " scores_pn = predict_2d(\n",
141
+ " model_pn, waveform_3ch, NX_PN, NT_PN,\n",
142
+ " min_t_overlap=0.1, device=DEVICE,\n",
143
+ " taper_nx=False, taper_nt=True, # Independent channels → no nx taper\n",
144
+ " )\n",
145
+ "\n",
146
+ "print(f\"Output scores shape: {scores_pn.shape}\") # (3, nx, nt) — P, S, Noise"
147
+ ]
148
+ },
149
+ {
150
+ "cell_type": "code",
151
+ "execution_count": null,
152
+ "metadata": {},
153
+ "outputs": [],
154
+ "source": [
155
+ "# Detect picks from probability scores using peak detection\n",
156
+ "channel_ids = [str(i) for i in range(nx)]\n",
157
+ "\n",
158
+ "picks_pn = detect_picks(\n",
159
+ " scores_pn, channel_ids, EVENT_ID, begin_time,\n",
160
+ " dt_s=dt_s, vmin=MIN_PROB, device=DEVICE,\n",
161
+ ")\n",
162
+ "\n",
163
+ "df_pn = pd.DataFrame(picks_pn)\n",
164
+ "n_p = (df_pn[\"phase_type\"] == \"P\").sum()\n",
165
+ "n_s = (df_pn[\"phase_type\"] == \"S\").sum()\n",
166
+ "print(f\"PhaseNet picks: {len(df_pn)} total ({n_p} P, {n_s} S)\")"
167
+ ]
168
+ },
169
+ {
170
+ "cell_type": "code",
171
+ "execution_count": null,
172
+ "metadata": {},
173
+ "outputs": [],
174
+ "source": "# Plot picks on waveform\nfig, ax = plt.subplots(figsize=(12, 8))\nplot_das(ax, data, nx, nt, dt_s, cmap=\"seismic\", vmin=-vmax, vmax=vmax)\nplot_picks(ax, df_pn, dt_s)\nax.set_xlabel(\"Time (s)\")\nax.set_ylabel(\"Channel index\")\nax.set_title(f\"PhaseNet picks — {EVENT_ID}\")\nax.legend(loc=\"upper right\", markerscale=10)\nplt.tight_layout()\nplt.show()"
175
+ },
176
+ {
177
+ "cell_type": "markdown",
178
+ "metadata": {},
179
+ "source": "## 3. Run PhaseNet-DAS+\n\nPhaseNet-DAS+ (Zhu et al. 2023, 2025) is specifically designed for DAS data. Unlike PhaseNet which treats each channel independently, PhaseNet-DAS+ uses 2D convolutions across both spatial (channel) and temporal dimensions, learning the spatiotemporal patterns of seismic waves propagating along the fiber.\n\nThe model was trained using a semi-supervised approach: initial labels from PhaseNet picks were iteratively refined by retraining on associated picks.\n\nKey differences from PhaseNet:\n- **Input**: 1 channel (native DAS strain rate) instead of 3\n- **Architecture**: 2D convolutions across (nx, nt) instead of independent 1D per channel\n- **Output**: P/S scores + event detection head (PhaseNet+ multi-task framework)"
180
+ },
181
+ {
182
+ "cell_type": "code",
183
+ "execution_count": null,
184
+ "metadata": {},
185
+ "outputs": [],
186
+ "source": [
187
+ "# Prepare input: native DAS format (1 channel)\n",
188
+ "waveform_1ch = data.astype(np.float32)[np.newaxis, :, :] # (1, nx, nt)\n",
189
+ "print(f\"Input shape: {waveform_1ch.shape} (1 channel, {nx} stations, {nt} samples)\")"
190
+ ]
191
+ },
192
+ {
193
+ "cell_type": "code",
194
+ "execution_count": null,
195
+ "metadata": {},
196
+ "outputs": [],
197
+ "source": [
198
+ "# Load pretrained PhaseNet-DAS-Plus (Arcata)\n",
199
+ "ckpt_path = download_model(\"PhaseNet-DAS-Plus-Arcata-v1\", \"phasenet_das_plus_arcata_v1.pth\")\n",
200
+ "ckpt = torch.load(ckpt_path, map_location=\"cpu\", weights_only=False)\n",
201
+ "model_das = phasenet.models.phasenet_das_plus.build_model(**ckpt.get(\"model_config\", {}))\n",
202
+ "\n",
203
+ "# Use EMA (Exponential Moving Average) weights for better generalization\n",
204
+ "if \"model_ema_weights\" in ckpt:\n",
205
+ " model_das.load_state_dict(ckpt[\"model_ema_weights\"])\n",
206
+ "else:\n",
207
+ " model_das.load_state_dict(ckpt[\"model\"])\n",
208
+ "\n",
209
+ "model_das.to(DEVICE).eval()\n",
210
+ "print(\"PhaseNet-DAS-Plus loaded\")"
211
+ ]
212
+ },
213
+ {
214
+ "cell_type": "code",
215
+ "source": "# Run inference\n# PhaseNet-DAS uses a larger window: (2048 channels, 4096 samples)\nNX_DAS, NT_DAS = 2048, 4096\n\nwith torch.no_grad():\n result = predict_2d(\n model_das, waveform_1ch, NX_DAS, NT_DAS,\n min_t_overlap=0.1, device=DEVICE,\n taper_nx=True, taper_nt=True, # DAS model: taper both axes\n )\n\n# PhaseNet-DAS+ returns (scores, event_detections)\nif isinstance(result, tuple):\n scores_das, event_detections = result\nelse:\n scores_das = result\n event_detections = None\n\nprint(f\"Phase scores shape: {scores_das.shape}\")",
216
+ "metadata": {},
217
+ "execution_count": null,
218
+ "outputs": []
219
+ },
220
+ {
221
+ "cell_type": "code",
222
+ "execution_count": null,
223
+ "metadata": {},
224
+ "outputs": [],
225
+ "source": [
226
+ "# Detect picks\n",
227
+ "picks_das = detect_picks(\n",
228
+ " scores_das, channel_ids, EVENT_ID, begin_time,\n",
229
+ " dt_s=dt_s, vmin=MIN_PROB, device=DEVICE,\n",
230
+ ")\n",
231
+ "\n",
232
+ "df_das = pd.DataFrame(picks_das)\n",
233
+ "n_p = (df_das[\"phase_type\"] == \"P\").sum()\n",
234
+ "n_s = (df_das[\"phase_type\"] == \"S\").sum()\n",
235
+ "print(f\"PhaseNet-DAS picks: {len(df_das)} total ({n_p} P, {n_s} S)\")"
236
+ ]
237
+ },
238
+ {
239
+ "cell_type": "code",
240
+ "execution_count": null,
241
+ "metadata": {},
242
+ "outputs": [],
243
+ "source": "# Side-by-side comparison\nfig, axes = plt.subplots(1, 2, figsize=(20, 8))\n\nfor ax, df, title in zip(axes, [df_pn, df_das], [\"PhaseNet\", \"PhaseNet-DAS+\"]):\n plot_das(ax, data, nx, nt, dt_s, cmap=\"seismic\", vmin=-vmax, vmax=vmax)\n plot_picks(ax, df, dt_s)\n ax.set_xlabel(\"Time (s)\")\n ax.set_ylabel(\"Channel index\")\n ax.set_title(f\"{title} — {len(df)} picks\")\n ax.legend(loc=\"upper right\", markerscale=10)\n\nplt.tight_layout()\nplt.show()"
244
+ },
245
+ {
246
+ "cell_type": "markdown",
247
+ "source": "## 4. Compare at Scale\n\nWe have pre-computed picks for all 2,470 Arcata events using both models. Let's compare on specific examples.",
248
+ "metadata": {}
249
+ },
250
+ {
251
+ "cell_type": "code",
252
+ "metadata": {},
253
+ "source": "# Visualize a weak event: PhaseNet fails, PhaseNet-DAS+ succeeds\nWEAK_EVENT = \"20230119T202515Z\"\n\nh5_path = hf_hub_download(\n HF_REPO, f\"arcata/data/{WEAK_EVENT}.h5\", repo_type=\"dataset\",\n local_dir=\"data/quakeflow_das\",\n)\nwith h5py.File(h5_path, \"r\") as fp:\n weak_data = fp[\"data\"][:]\n\npn_path = hf_hub_download(\n HF_REPO, f\"arcata/phasenet/picks/{WEAK_EVENT}.parquet\",\n repo_type=\"dataset\", local_dir=\"data/quakeflow_das\",\n)\ndas_path = hf_hub_download(\n HF_REPO, f\"arcata/phasenet_das/picks/{WEAK_EVENT}.parquet\",\n repo_type=\"dataset\", local_dir=\"data/quakeflow_das\",\n)\n\npn_weak = pd.read_parquet(pn_path)\ndas_weak = pd.read_parquet(das_path)\n\nfig, axes = plt.subplots(1, 2, figsize=(20, 8))\nvmax_w = 2 * np.percentile(np.abs(weak_data), 95)\nnx_w, nt_w = weak_data.shape\n\nfor ax, df, title in zip(axes, [pn_weak, das_weak], [\"PhaseNet\", \"PhaseNet-DAS+\"]):\n plot_das(ax, weak_data, nx_w, nt_w, dt_s, cmap=\"seismic\", vmin=-vmax_w, vmax=vmax_w)\n\n assoc = df[df[\"origin_id\"] >= 0]\n unassoc = df[df[\"origin_id\"] < 0]\n\n if len(unassoc) > 0:\n ax.scatter(unassoc[\"phase_index\"] * dt_s, unassoc[\"channel_index\"].astype(int),\n s=0.3, c=\"gray\", alpha=0.1, label=f\"Unassociated ({len(unassoc)})\")\n if len(assoc) > 0:\n for phase, color in [(\"P\", \"red\"), (\"S\", \"blue\")]:\n a = assoc[assoc[\"phase_type\"] == phase]\n ax.scatter(a[\"phase_index\"] * dt_s, a[\"channel_index\"].astype(int),\n s=1, c=color, alpha=0.5, label=f\"{phase} assoc ({len(a)})\")\n\n ax.set_xlabel(\"Time (s)\")\n ax.set_ylabel(\"Channel index\")\n ax.set_title(f\"{title} — {WEAK_EVENT}\")\n ax.legend(loc=\"upper right\", markerscale=10)\n\nplt.tight_layout()\nplt.show()",
254
+ "outputs": [],
255
+ "execution_count": null
256
+ },
257
+ {
258
+ "cell_type": "code",
259
+ "execution_count": null,
260
+ "metadata": {},
261
+ "outputs": [],
262
+ "source": "# Noise-only event: PhaseNet hallucinates picks, DAS+ correctly rejects\nNOISE_EVENT = \"20230103T220320Z\"\n\nh5_path = hf_hub_download(\n HF_REPO, f\"arcata/data/{NOISE_EVENT}.h5\", repo_type=\"dataset\",\n local_dir=\"data/quakeflow_das\",\n)\nwith h5py.File(h5_path, \"r\") as fp:\n noise_data = fp[\"data\"][:]\n\npn_noise = pd.read_parquet(hf_hub_download(\n HF_REPO, f\"arcata/phasenet/picks/{NOISE_EVENT}.parquet\",\n repo_type=\"dataset\", local_dir=\"data/quakeflow_das\",\n))\n\nfig, axes = plt.subplots(1, 2, figsize=(20, 8))\nvmax_n = 2 * np.percentile(np.abs(noise_data), 95)\nnx_n, nt_n = noise_data.shape\n\nax = axes[0]\nplot_das(ax, noise_data, nx_n, nt_n, dt_s, cmap=\"seismic\", vmin=-vmax_n, vmax=vmax_n)\nax.scatter(pn_noise[\"phase_index\"] * dt_s, pn_noise[\"channel_index\"].astype(int),\n s=0.3, c=\"orange\", alpha=0.1)\nax.set_title(f\"PhaseNet — {len(pn_noise)} false picks\")\nax.set_xlabel(\"Time (s)\")\nax.set_ylabel(\"Channel index\")\n\nax = axes[1]\nplot_das(ax, noise_data, nx_n, nt_n, dt_s, cmap=\"seismic\", vmin=-vmax_n, vmax=vmax_n)\nax.set_title(\"PhaseNet-DAS+ — 0 picks (correct)\")\nax.set_xlabel(\"Time (s)\")\nax.set_ylabel(\"Channel index\")\n\nplt.tight_layout()\nplt.show()"
263
+ },
264
+ {
265
+ "cell_type": "markdown",
266
+ "execution_count": null,
267
+ "metadata": {},
268
+ "outputs": [],
269
+ "source": "## Summary\n\n| | PhaseNet (seismic) | PhaseNet-DAS+ |\n|---|---|---|\n| **Input** | 3-component (zero-padded DAS) | Native 1-component DAS |\n| **Architecture** | Independent per-channel 1D | Spatiotemporal 2D convolutions |\n| **Strong events** | Detects P/S arrivals | Detects P/S + event heads |\n| **Weak events** | Misses most arrivals | Detects using spatial coherence |\n| **Noise** | Many false picks | Correctly rejects noise |\n\nBrowse all 2,467 prediction figures: [Google Drive](https://drive.google.com/drive/folders/1natqPKvg0q07AxLWFOSEpTEcE1OTzt2h?usp=sharing)\n\n**Next:** [Lab 2b](lab2b_pick_association.ipynb) explores how raw picks are grouped into seismic events using P-S pairing and origin time estimation."
270
+ }
271
+ ],
272
+ "metadata": {
273
+ "kernelspec": {
274
+ "display_name": "Python 3",
275
+ "language": "python",
276
+ "name": "python3"
277
+ },
278
+ "language_info": {
279
+ "name": "python",
280
+ "version": "3.10.0"
281
+ }
282
+ },
283
+ "nbformat": 4,
284
+ "nbformat_minor": 4
285
+ }