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.
Files changed (87) hide show
  1. phasenet-0.2.6.dev2/.github/workflows/publish.yml +28 -0
  2. phasenet-0.2.6.dev2/.gitignore +15 -0
  3. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/PKG-INFO +17 -29
  4. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/README.md +16 -28
  5. phasenet-0.2.6.dev2/docs/notebooks/lab2a_phasenet_das_inference.ipynb +285 -0
  6. phasenet-0.2.6.dev2/docs/notebooks/lab2b_pick_association.ipynb +195 -0
  7. phasenet-0.2.6.dev2/docs/notebooks/lab2c_semisupervised_training.ipynb +487 -0
  8. phasenet-0.2.6.dev2/examples/ceed_training_plots.py +180 -0
  9. phasenet-0.2.6.dev2/examples/italy/prepare_inventory.py +233 -0
  10. phasenet-0.2.6.dev2/examples/italy/prepare_mseed_list.py +118 -0
  11. phasenet-0.2.6.dev2/examples/italy/run_all_years.sh +64 -0
  12. phasenet-0.2.6.dev2/examples/italy/upload_loop.sh +12 -0
  13. phasenet-0.2.6.dev2/examples/italy/upload_phasenet_origin.sh +47 -0
  14. phasenet-0.2.6.dev2/phasenet/data/README.md +313 -0
  15. phasenet-0.2.6.dev2/phasenet/data/__init__.py +6 -0
  16. phasenet-0.2.6.dev2/phasenet/data/ceed.py +621 -0
  17. phasenet-0.2.6.dev2/phasenet/data/das.py +408 -0
  18. phasenet-0.2.6.dev2/phasenet/data/plot_ceed.py +589 -0
  19. phasenet-0.2.6.dev2/phasenet/data/plot_das.py +580 -0
  20. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/data/preprocess.py +1 -1
  21. phasenet-0.2.6.dev2/phasenet/data/sample.py +427 -0
  22. phasenet-0.2.6.dev2/phasenet/data/tables.py +161 -0
  23. phasenet-0.2.6.dev2/phasenet/data/transforms.py +888 -0
  24. phasenet-0.2.6.dev2/phasenet/models/README.md +280 -0
  25. phasenet-0.2.6.dev2/phasenet/models/__init__.py +1 -0
  26. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/phasenet.py +91 -214
  27. phasenet-0.2.6.dev2/phasenet/models/unet.py +819 -0
  28. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/unet2018.py +8 -1
  29. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/normalize.py +8 -3
  30. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/utils/__init__.py +0 -1
  31. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/utils/inference.py +25 -19
  32. phasenet-0.2.6.dev2/phasenet/utils/postprocess.py +315 -0
  33. phasenet-0.2.6.dev2/phasenet/utils/visualization.py +574 -0
  34. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet.egg-info/PKG-INFO +17 -29
  35. phasenet-0.2.6.dev2/phasenet.egg-info/SOURCES.txt +69 -0
  36. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet.egg-info/entry_points.txt +0 -1
  37. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet.egg-info/top_level.txt +0 -1
  38. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/pyproject.toml +2 -3
  39. phasenet-0.2.6.dev2/requirements.txt +16 -0
  40. phasenet-0.2.6.dev2/scripts/aug_verify.py +258 -0
  41. phasenet-0.2.6.dev2/scripts/augmentation_gallery.py +305 -0
  42. phasenet-0.2.6.dev2/scripts/das_catalog_events.py +55 -0
  43. phasenet-0.2.6.dev2/scripts/plot_normalization.py +157 -0
  44. phasenet-0.2.6.dev2/scripts/predict_ceed.py +493 -0
  45. phasenet-0.2.6.dev2/scripts/predict_ceed.sh +41 -0
  46. phasenet-0.2.6.dev2/scripts/predict_das.py +583 -0
  47. phasenet-0.2.6.dev2/scripts/predict_das.sh +51 -0
  48. phasenet-0.2.6.dev2/scripts/predict_mseed.py +582 -0
  49. phasenet-0.2.6.dev2/scripts/semisupervised_das.sh +216 -0
  50. phasenet-0.2.6.dev2/scripts/train_ceed.sh +20 -0
  51. phasenet-0.2.6.dev2/scripts/train_das.sh +54 -0
  52. phasenet-0.2.6.dev2/scripts/upgrade_checkpoint.py +77 -0
  53. phasenet-0.2.6.dev2/tests/benchmark_dataloader.py +373 -0
  54. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/tests/test_augmentations.py +17 -15
  55. phasenet-0.2.6.dev2/tests/test_labels.py +805 -0
  56. phasenet-0.2.6.dev2/tests/test_models.py +168 -0
  57. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/tests/test_normalize.py +14 -20
  58. phasenet-0.2.6.dev2/tests/test_recipes.py +21 -0
  59. phasenet-0.2.6.dev2/tests/test_transforms.py +199 -0
  60. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/train.py +213 -271
  61. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/utils.py +1 -144
  62. phasenet-0.2.6.dev1/phasenet/data/__init__.py +0 -7
  63. phasenet-0.2.6.dev1/phasenet/data/ceed.py +0 -2337
  64. phasenet-0.2.6.dev1/phasenet/data/das.py +0 -2328
  65. phasenet-0.2.6.dev1/phasenet/data/transforms.py +0 -831
  66. phasenet-0.2.6.dev1/phasenet/models/__init__.py +0 -8
  67. phasenet-0.2.6.dev1/phasenet/models/phasenet_das.py +0 -75
  68. phasenet-0.2.6.dev1/phasenet/models/phasenet_das_plus.py +0 -46
  69. phasenet-0.2.6.dev1/phasenet/models/phasenet_plus.py +0 -27
  70. phasenet-0.2.6.dev1/phasenet/models/phasenet_tf.py +0 -21
  71. phasenet-0.2.6.dev1/phasenet/models/phasenet_tf_plus.py +0 -27
  72. phasenet-0.2.6.dev1/phasenet/models/unet.py +0 -1331
  73. phasenet-0.2.6.dev1/phasenet/utils/postprocess.py +0 -711
  74. phasenet-0.2.6.dev1/phasenet/utils/visualization.py +0 -1556
  75. phasenet-0.2.6.dev1/phasenet.egg-info/SOURCES.txt +0 -40
  76. phasenet-0.2.6.dev1/predict.py +0 -634
  77. phasenet-0.2.6.dev1/tests/test_labels.py +0 -367
  78. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/__init__.py +0 -0
  79. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/prompt/__init__.py +0 -0
  80. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/prompt/common.py +0 -0
  81. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/prompt/mask_decoder.py +0 -0
  82. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/prompt/prompt_encoder.py +0 -0
  83. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/models/prompt/transformer.py +0 -0
  84. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet/utils/detect_peaks_cpu.py +0 -0
  85. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet.egg-info/dependency_links.txt +0 -0
  86. {phasenet-0.2.6.dev1 → phasenet-0.2.6.dev2}/phasenet.egg-info/requires.txt +0 -0
  87. {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
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phasenet
3
- Version: 0.2.6.dev1
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 --result-dir results/picks \
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 --result-dir results/picks \
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-dir results/ceed
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-dir data/quakeflow_das/arcata/data \
97
- --model-type phasenet_das_plus \
98
- --checkpoint output/train_das_arcata/checkpoint.pth \
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
- --file-list file_list.txt \
105
- --model-type phasenet_das_plus \
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
- --model phasenet_plus \
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
- --lr 3e-4 \
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
- --model phasenet_das_plus \
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
- --label-list results/das/phasenet/arcata/labels.txt \
151
- --nx 2048 --nt 4096 \
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-dir output/train_das_arcata_v26
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 --result-dir results/picks \
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 --result-dir results/picks \
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-dir results/ceed
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-dir data/quakeflow_das/arcata/data \
67
- --model-type phasenet_das_plus \
68
- --checkpoint output/train_das_arcata/checkpoint.pth \
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
- --file-list file_list.txt \
75
- --model-type phasenet_das_plus \
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
- --model phasenet_plus \
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
- --lr 3e-4 \
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
- --model phasenet_das_plus \
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
- --label-list results/das/phasenet/arcata/labels.txt \
121
- --nx 2048 --nt 4096 \
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-dir output/train_das_arcata_v26
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": "[![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
+ }