phasenet 0.2.6.dev1__tar.gz → 0.2.6.dev2__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.
- phasenet-0.2.6.dev2/.github/workflows/publish.yml +28 -0
- phasenet-0.2.6.dev2/.gitignore +15 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/PKG-INFO +17 -29
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/README.md +16 -28
- phasenet-0.2.6.dev2/docs/notebooks/lab2a_phasenet_das_inference.ipynb +285 -0
- phasenet-0.2.6.dev2/docs/notebooks/lab2b_pick_association.ipynb +195 -0
- phasenet-0.2.6.dev2/docs/notebooks/lab2c_semisupervised_training.ipynb +487 -0
- phasenet-0.2.6.dev2/examples/ceed_training_plots.py +180 -0
- phasenet-0.2.6.dev2/examples/italy/prepare_inventory.py +233 -0
- phasenet-0.2.6.dev2/examples/italy/prepare_mseed_list.py +118 -0
- phasenet-0.2.6.dev2/examples/italy/run_all_years.sh +64 -0
- phasenet-0.2.6.dev2/examples/italy/upload_loop.sh +12 -0
- phasenet-0.2.6.dev2/examples/italy/upload_phasenet_origin.sh +47 -0
- phasenet-0.2.6.dev2/phasenet/data/README.md +313 -0
- phasenet-0.2.6.dev2/phasenet/data/__init__.py +6 -0
- phasenet-0.2.6.dev2/phasenet/data/ceed.py +621 -0
- phasenet-0.2.6.dev2/phasenet/data/das.py +408 -0
- phasenet-0.2.6.dev2/phasenet/data/plot_ceed.py +589 -0
- phasenet-0.2.6.dev2/phasenet/data/plot_das.py +580 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/data/preprocess.py +1 -1
- phasenet-0.2.6.dev2/phasenet/data/sample.py +427 -0
- phasenet-0.2.6.dev2/phasenet/data/tables.py +161 -0
- phasenet-0.2.6.dev2/phasenet/data/transforms.py +888 -0
- phasenet-0.2.6.dev2/phasenet/models/README.md +280 -0
- phasenet-0.2.6.dev2/phasenet/models/__init__.py +1 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/phasenet.py +91 -214
- phasenet-0.2.6.dev2/phasenet/models/unet.py +819 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/unet2018.py +8 -1
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/normalize.py +8 -3
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/utils/__init__.py +0 -1
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/utils/inference.py +25 -19
- phasenet-0.2.6.dev2/phasenet/utils/postprocess.py +315 -0
- phasenet-0.2.6.dev2/phasenet/utils/visualization.py +574 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet.egg-info/PKG-INFO +17 -29
- phasenet-0.2.6.dev2/phasenet.egg-info/SOURCES.txt +69 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet.egg-info/entry_points.txt +0 -1
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet.egg-info/top_level.txt +0 -1
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/pyproject.toml +2 -3
- phasenet-0.2.6.dev2/requirements.txt +16 -0
- phasenet-0.2.6.dev2/scripts/aug_verify.py +258 -0
- phasenet-0.2.6.dev2/scripts/augmentation_gallery.py +305 -0
- phasenet-0.2.6.dev2/scripts/das_catalog_events.py +55 -0
- phasenet-0.2.6.dev2/scripts/plot_normalization.py +157 -0
- phasenet-0.2.6.dev2/scripts/predict_ceed.py +493 -0
- phasenet-0.2.6.dev2/scripts/predict_ceed.sh +41 -0
- phasenet-0.2.6.dev2/scripts/predict_das.py +583 -0
- phasenet-0.2.6.dev2/scripts/predict_das.sh +51 -0
- phasenet-0.2.6.dev2/scripts/predict_mseed.py +582 -0
- phasenet-0.2.6.dev2/scripts/semisupervised_das.sh +216 -0
- phasenet-0.2.6.dev2/scripts/train_ceed.sh +20 -0
- phasenet-0.2.6.dev2/scripts/train_das.sh +54 -0
- phasenet-0.2.6.dev2/scripts/upgrade_checkpoint.py +77 -0
- phasenet-0.2.6.dev2/tests/benchmark_dataloader.py +373 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/tests/test_augmentations.py +17 -15
- phasenet-0.2.6.dev2/tests/test_labels.py +805 -0
- phasenet-0.2.6.dev2/tests/test_models.py +168 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/tests/test_normalize.py +14 -20
- phasenet-0.2.6.dev2/tests/test_recipes.py +21 -0
- phasenet-0.2.6.dev2/tests/test_transforms.py +199 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/train.py +213 -271
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/utils.py +1 -144
- phasenet-0.2.6.dev1/phasenet/data/__init__.py +0 -7
- phasenet-0.2.6.dev1/phasenet/data/ceed.py +0 -2337
- phasenet-0.2.6.dev1/phasenet/data/das.py +0 -2328
- phasenet-0.2.6.dev1/phasenet/data/transforms.py +0 -831
- phasenet-0.2.6.dev1/phasenet/models/__init__.py +0 -8
- phasenet-0.2.6.dev1/phasenet/models/phasenet_das.py +0 -75
- phasenet-0.2.6.dev1/phasenet/models/phasenet_das_plus.py +0 -46
- phasenet-0.2.6.dev1/phasenet/models/phasenet_plus.py +0 -27
- phasenet-0.2.6.dev1/phasenet/models/phasenet_tf.py +0 -21
- phasenet-0.2.6.dev1/phasenet/models/phasenet_tf_plus.py +0 -27
- phasenet-0.2.6.dev1/phasenet/models/unet.py +0 -1331
- phasenet-0.2.6.dev1/phasenet/utils/postprocess.py +0 -711
- phasenet-0.2.6.dev1/phasenet/utils/visualization.py +0 -1556
- phasenet-0.2.6.dev1/phasenet.egg-info/SOURCES.txt +0 -40
- phasenet-0.2.6.dev1/predict.py +0 -634
- phasenet-0.2.6.dev1/tests/test_labels.py +0 -367
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/__init__.py +0 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/prompt/__init__.py +0 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/prompt/common.py +0 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/prompt/mask_decoder.py +0 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/prompt/prompt_encoder.py +0 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/prompt/transformer.py +0 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/utils/detect_peaks_cpu.py +0 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet.egg-info/dependency_links.txt +0 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet.egg-info/requires.txt +0 -0
- {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: phasenet
|
|
3
|
-
Version: 0.2.6.
|
|
3
|
+
Version: 0.2.6.dev2
|
|
4
4
|
Summary: A PyTorch implementation of PhaseNet for seismic and DAS phase picking
|
|
5
5
|
Author: Weiqiang Zhu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -54,11 +54,11 @@ The original PhaseNet-2018 weights, converted from TensorFlow, load through `loa
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
# PhaseNet-2018 weights (the default), run as TF PhaseNet ran them: a 1 Hz highpass, accelerometers picked on velocity
|
|
57
|
-
python scripts/predict_mseed.py --data-list mseed_list.txt --inventory stations.xml --
|
|
57
|
+
python scripts/predict_mseed.py --data-list mseed_list.txt --inventory stations.xml --output-path results/picks \
|
|
58
58
|
--highpass 1.0 --integrate-acceleration
|
|
59
59
|
|
|
60
60
|
# a retrained model: its checkpoint brings its contract (normalization, windows); filtering is your choice
|
|
61
|
-
python scripts/predict_mseed.py --data-list mseed_list.txt --inventory stations.xml --
|
|
61
|
+
python scripts/predict_mseed.py --data-list mseed_list.txt --inventory stations.xml --output-path results/picks \
|
|
62
62
|
--checkpoint output/train_ceed/checkpoint.pth
|
|
63
63
|
```
|
|
64
64
|
|
|
@@ -79,7 +79,7 @@ This is the picker the QuakeFlow pipeline runs (`scripts/run_phasenet_pytorch.py
|
|
|
79
79
|
python scripts/predict_ceed.py --n-events 5
|
|
80
80
|
|
|
81
81
|
# Process all days for a year (saves parquets to results/ceed/)
|
|
82
|
-
python scripts/predict_ceed.py --all --year 2025 --output-
|
|
82
|
+
python scripts/predict_ceed.py --all --year 2025 --output-path results/ceed
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
Output: one parquet per day file in `results/ceed/{region}/`, with columns:
|
|
@@ -93,17 +93,15 @@ python scripts/predict_das.py --subset arcata --plot
|
|
|
93
93
|
|
|
94
94
|
# Predict with a trained DAS model on local data
|
|
95
95
|
python scripts/predict_das.py \
|
|
96
|
-
--data-
|
|
97
|
-
--model
|
|
98
|
-
--
|
|
99
|
-
--output-dir results/das/train_das_arcata/arcata \
|
|
96
|
+
--data-path data/quakeflow_das/arcata/data \
|
|
97
|
+
--model output/train_das_arcata/checkpoint.pth \
|
|
98
|
+
--output-path results/das/train_das_arcata/arcata \
|
|
100
99
|
--no-ema --plot
|
|
101
100
|
|
|
102
101
|
# Predict from a file list
|
|
103
102
|
python scripts/predict_das.py \
|
|
104
|
-
--
|
|
105
|
-
--model
|
|
106
|
-
--checkpoint output/model.pth \
|
|
103
|
+
--data-list file_list.txt \
|
|
104
|
+
--model output/model.pth \
|
|
107
105
|
--plot
|
|
108
106
|
|
|
109
107
|
# Multi-GPU prediction
|
|
@@ -121,18 +119,14 @@ By default, picks are associated using P-S pairing. Use `--use-event-head` to as
|
|
|
121
119
|
|
|
122
120
|
```bash
|
|
123
121
|
python train.py \
|
|
124
|
-
--
|
|
125
|
-
--dataset-type ceed \
|
|
122
|
+
--recipe seismic \
|
|
126
123
|
--label-path results/ceed \
|
|
127
|
-
--nx 16 \
|
|
128
|
-
--max-iters 100000 \
|
|
129
|
-
--batch-size 8 \
|
|
130
124
|
--workers 4 \
|
|
131
|
-
--
|
|
132
|
-
--eval-interval 5000 \
|
|
133
|
-
--output-dir output/train_ceed
|
|
125
|
+
--output-path output/train_ceed
|
|
134
126
|
```
|
|
135
127
|
|
|
128
|
+
`--recipe seismic` (or `das`) sets the model and every training value to the released model's (`RECIPES` in `train.py`); a flag given beside it overrides that one value. Without `--label-path` the labels are the catalog's.
|
|
129
|
+
|
|
136
130
|
The input contract is set here and saved in the checkpoint: `--norm-kind trailing --norm-window 1024 --norm-scope station --norm-stat std` are the defaults (see Design). Filtering is an augmentation, not a setting.
|
|
137
131
|
|
|
138
132
|
### DAS Training
|
|
@@ -143,19 +137,13 @@ bash scripts/train_das.sh 0 arcata v26
|
|
|
143
137
|
|
|
144
138
|
# Or directly
|
|
145
139
|
python train.py \
|
|
146
|
-
--
|
|
147
|
-
--dataset-type das \
|
|
140
|
+
--recipe das \
|
|
148
141
|
--data-path data/quakeflow_das/arcata/data \
|
|
149
142
|
--label-path results/das/phasenet/arcata/picks \
|
|
150
|
-
--
|
|
151
|
-
--
|
|
152
|
-
--num-patch 16 \
|
|
153
|
-
--max-iters 50000 \
|
|
154
|
-
--batch-size 2 --workers 8 \
|
|
155
|
-
--lr 1e-4 --weight-decay 0.01 \
|
|
156
|
-
--model-ema --model-ema-decay 0.999 \
|
|
143
|
+
--data-list results/das/phasenet/arcata/labels.txt \
|
|
144
|
+
--workers 8 \
|
|
157
145
|
--eval-interval 1000 --save-interval 1000 \
|
|
158
|
-
--output-
|
|
146
|
+
--output-path output/train_das_arcata_v26
|
|
159
147
|
```
|
|
160
148
|
|
|
161
149
|
### Key Training Options
|
|
@@ -24,11 +24,11 @@ The original PhaseNet-2018 weights, converted from TensorFlow, load through `loa
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
# PhaseNet-2018 weights (the default), run as TF PhaseNet ran them: a 1 Hz highpass, accelerometers picked on velocity
|
|
27
|
-
python scripts/predict_mseed.py --data-list mseed_list.txt --inventory stations.xml --
|
|
27
|
+
python scripts/predict_mseed.py --data-list mseed_list.txt --inventory stations.xml --output-path results/picks \
|
|
28
28
|
--highpass 1.0 --integrate-acceleration
|
|
29
29
|
|
|
30
30
|
# a retrained model: its checkpoint brings its contract (normalization, windows); filtering is your choice
|
|
31
|
-
python scripts/predict_mseed.py --data-list mseed_list.txt --inventory stations.xml --
|
|
31
|
+
python scripts/predict_mseed.py --data-list mseed_list.txt --inventory stations.xml --output-path results/picks \
|
|
32
32
|
--checkpoint output/train_ceed/checkpoint.pth
|
|
33
33
|
```
|
|
34
34
|
|
|
@@ -49,7 +49,7 @@ This is the picker the QuakeFlow pipeline runs (`scripts/run_phasenet_pytorch.py
|
|
|
49
49
|
python scripts/predict_ceed.py --n-events 5
|
|
50
50
|
|
|
51
51
|
# Process all days for a year (saves parquets to results/ceed/)
|
|
52
|
-
python scripts/predict_ceed.py --all --year 2025 --output-
|
|
52
|
+
python scripts/predict_ceed.py --all --year 2025 --output-path results/ceed
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Output: one parquet per day file in `results/ceed/{region}/`, with columns:
|
|
@@ -63,17 +63,15 @@ python scripts/predict_das.py --subset arcata --plot
|
|
|
63
63
|
|
|
64
64
|
# Predict with a trained DAS model on local data
|
|
65
65
|
python scripts/predict_das.py \
|
|
66
|
-
--data-
|
|
67
|
-
--model
|
|
68
|
-
--
|
|
69
|
-
--output-dir results/das/train_das_arcata/arcata \
|
|
66
|
+
--data-path data/quakeflow_das/arcata/data \
|
|
67
|
+
--model output/train_das_arcata/checkpoint.pth \
|
|
68
|
+
--output-path results/das/train_das_arcata/arcata \
|
|
70
69
|
--no-ema --plot
|
|
71
70
|
|
|
72
71
|
# Predict from a file list
|
|
73
72
|
python scripts/predict_das.py \
|
|
74
|
-
--
|
|
75
|
-
--model
|
|
76
|
-
--checkpoint output/model.pth \
|
|
73
|
+
--data-list file_list.txt \
|
|
74
|
+
--model output/model.pth \
|
|
77
75
|
--plot
|
|
78
76
|
|
|
79
77
|
# Multi-GPU prediction
|
|
@@ -91,18 +89,14 @@ By default, picks are associated using P-S pairing. Use `--use-event-head` to as
|
|
|
91
89
|
|
|
92
90
|
```bash
|
|
93
91
|
python train.py \
|
|
94
|
-
--
|
|
95
|
-
--dataset-type ceed \
|
|
92
|
+
--recipe seismic \
|
|
96
93
|
--label-path results/ceed \
|
|
97
|
-
--nx 16 \
|
|
98
|
-
--max-iters 100000 \
|
|
99
|
-
--batch-size 8 \
|
|
100
94
|
--workers 4 \
|
|
101
|
-
--
|
|
102
|
-
--eval-interval 5000 \
|
|
103
|
-
--output-dir output/train_ceed
|
|
95
|
+
--output-path output/train_ceed
|
|
104
96
|
```
|
|
105
97
|
|
|
98
|
+
`--recipe seismic` (or `das`) sets the model and every training value to the released model's (`RECIPES` in `train.py`); a flag given beside it overrides that one value. Without `--label-path` the labels are the catalog's.
|
|
99
|
+
|
|
106
100
|
The input contract is set here and saved in the checkpoint: `--norm-kind trailing --norm-window 1024 --norm-scope station --norm-stat std` are the defaults (see Design). Filtering is an augmentation, not a setting.
|
|
107
101
|
|
|
108
102
|
### DAS Training
|
|
@@ -113,19 +107,13 @@ bash scripts/train_das.sh 0 arcata v26
|
|
|
113
107
|
|
|
114
108
|
# Or directly
|
|
115
109
|
python train.py \
|
|
116
|
-
--
|
|
117
|
-
--dataset-type das \
|
|
110
|
+
--recipe das \
|
|
118
111
|
--data-path data/quakeflow_das/arcata/data \
|
|
119
112
|
--label-path results/das/phasenet/arcata/picks \
|
|
120
|
-
--
|
|
121
|
-
--
|
|
122
|
-
--num-patch 16 \
|
|
123
|
-
--max-iters 50000 \
|
|
124
|
-
--batch-size 2 --workers 8 \
|
|
125
|
-
--lr 1e-4 --weight-decay 0.01 \
|
|
126
|
-
--model-ema --model-ema-decay 0.999 \
|
|
113
|
+
--data-list results/das/phasenet/arcata/labels.txt \
|
|
114
|
+
--workers 8 \
|
|
127
115
|
--eval-interval 1000 --save-interval 1000 \
|
|
128
|
-
--output-
|
|
116
|
+
--output-path output/train_das_arcata_v26
|
|
129
117
|
```
|
|
130
118
|
|
|
131
119
|
### Key Training Options
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"metadata": {},
|
|
6
|
+
"source": "[](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
|
+
}
|