PhaseNet 0.2.5__tar.gz → 0.2.6.dev1__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.dev1/PKG-INFO +375 -0
- phasenet-0.2.6.dev1/README.md +345 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/data/__init__.py +2 -2
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/data/ceed.py +240 -197
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/data/das.py +100 -327
- phasenet-0.2.6.dev1/phasenet/data/preprocess.py +207 -0
- phasenet-0.2.6.dev1/phasenet/data/transforms.py +831 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/phasenet.py +34 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/unet.py +17 -2
- phasenet-0.2.6.dev1/phasenet/normalize.py +195 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/utils/detect_peaks_cpu.py +0 -1
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/utils/inference.py +69 -43
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/utils/visualization.py +144 -33
- phasenet-0.2.6.dev1/phasenet.egg-info/PKG-INFO +375 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet.egg-info/SOURCES.txt +4 -22
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/pyproject.toml +2 -2
- phasenet-0.2.6.dev1/tests/test_augmentations.py +105 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/tests/test_labels.py +79 -0
- phasenet-0.2.6.dev1/tests/test_normalize.py +245 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/train.py +117 -20
- phasenet-0.2.5/.github/workflows/publish.yml +0 -28
- phasenet-0.2.5/.gitignore +0 -15
- phasenet-0.2.5/PKG-INFO +0 -163
- phasenet-0.2.5/README.md +0 -133
- phasenet-0.2.5/docs/notebooks/lab2a_phasenet_das_inference.ipynb +0 -285
- phasenet-0.2.5/docs/notebooks/lab2b_pick_association.ipynb +0 -195
- phasenet-0.2.5/docs/notebooks/lab2c_semisupervised_training.ipynb +0 -487
- phasenet-0.2.5/examples/ceed_training_plots.py +0 -181
- phasenet-0.2.5/examples/italy/prepare_inventory.py +0 -233
- phasenet-0.2.5/examples/italy/prepare_mseed_list.py +0 -118
- phasenet-0.2.5/examples/italy/run_all_years.sh +0 -64
- phasenet-0.2.5/examples/italy/upload_loop.sh +0 -12
- phasenet-0.2.5/examples/italy/upload_phasenet_origin.sh +0 -47
- phasenet-0.2.5/phasenet/data/transforms.py +0 -776
- phasenet-0.2.5/phasenet.egg-info/PKG-INFO +0 -163
- phasenet-0.2.5/requirements.txt +0 -16
- phasenet-0.2.5/scripts/predict_ceed.py +0 -489
- phasenet-0.2.5/scripts/predict_ceed.sh +0 -41
- phasenet-0.2.5/scripts/predict_das.py +0 -607
- phasenet-0.2.5/scripts/predict_das.sh +0 -55
- phasenet-0.2.5/scripts/predict_mseed.py +0 -487
- phasenet-0.2.5/scripts/semisupervised_das.sh +0 -221
- phasenet-0.2.5/scripts/train_ceed.sh +0 -21
- phasenet-0.2.5/scripts/train_das.sh +0 -57
- phasenet-0.2.5/tests/benchmark_dataloader.py +0 -373
- phasenet-0.2.5/tests/test_augmentations.py +0 -234
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/__init__.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/__init__.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/phasenet_das.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/phasenet_das_plus.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/phasenet_plus.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/phasenet_tf.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/phasenet_tf_plus.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/prompt/__init__.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/prompt/common.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/prompt/mask_decoder.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/prompt/prompt_encoder.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/prompt/transformer.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/models/unet2018.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/utils/__init__.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet/utils/postprocess.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet.egg-info/dependency_links.txt +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet.egg-info/entry_points.txt +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet.egg-info/requires.txt +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/phasenet.egg-info/top_level.txt +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/predict.py +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/setup.cfg +0 -0
- {phasenet-0.2.5 → phasenet-0.2.6.dev1}/utils.py +0 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phasenet
|
|
3
|
+
Version: 0.2.6.dev1
|
|
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.10
|
|
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
|
+
Python 3.10 or later.
|
|
35
|
+
|
|
36
|
+
## Models
|
|
37
|
+
|
|
38
|
+
| Model | Features | Data Type |
|
|
39
|
+
|-------|----------|-----------|
|
|
40
|
+
| `phasenet` | Phase (P/S) picking | Seismic 3-component |
|
|
41
|
+
| `phasenet_tf` | Phase picking + STFT spectrogram | Seismic 3-component |
|
|
42
|
+
| `phasenet_plus` | Phase + polarity + event detection | Seismic 3-component |
|
|
43
|
+
| `phasenet_tf_plus` | Phase + polarity + event detection + STFT | Seismic 3-component |
|
|
44
|
+
| `phasenet_das` | Phase picking | DAS single-channel |
|
|
45
|
+
| `phasenet_das_plus` | Phase + event detection | DAS single-channel |
|
|
46
|
+
|
|
47
|
+
The `_tf` variants add a Short-Time Fourier Transform (STFT) branch that extracts frequency features alongside the temporal waveform, improving performance on noisy data.
|
|
48
|
+
|
|
49
|
+
The original PhaseNet-2018 weights, converted from TensorFlow, load through `load_phasenet2018_model` (and as `--model phasenet` in `predict_das.py`); every other model is trained here with `train.py`.
|
|
50
|
+
|
|
51
|
+
## Prediction
|
|
52
|
+
|
|
53
|
+
### Continuous mseed (seismic network)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
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 \
|
|
58
|
+
--highpass 1.0 --integrate-acceleration
|
|
59
|
+
|
|
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 \
|
|
62
|
+
--checkpoint output/train_ceed/checkpoint.pth
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`--data-list` has one station-instrument per line, its component files separated by `|`. `--inventory` is one
|
|
66
|
+
StationXML file for every station or a directory, whose `*.xml` files (flat or nested) are matched to each
|
|
67
|
+
station by `NET.STA`, then `STA`, then `NET_network`; it serves the `phase_amplitude` column and the channel
|
|
68
|
+
orientation, picking runs on counts, and a station without a match is still picked from its channel names. `--sampling-rate`
|
|
69
|
+
(100) is the working rate: the input is resampled to it and pick indices become times with it. `--workers`
|
|
70
|
+
(8) sets the reader processes; the reader, not the model, is the bottleneck.
|
|
71
|
+
|
|
72
|
+
Output: one CSV per station-instrument with `station_id, begin_time, phase_index, phase_time, phase_score, phase_type[, phase_amplitude]`.
|
|
73
|
+
This is the picker the QuakeFlow pipeline runs (`scripts/run_phasenet_pytorch.py` there).
|
|
74
|
+
|
|
75
|
+
### CEED (Seismic) Prediction
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Demo: process a few events with plots
|
|
79
|
+
python scripts/predict_ceed.py --n-events 5
|
|
80
|
+
|
|
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
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Output: one parquet per day file in `results/ceed/{region}/`, with columns:
|
|
86
|
+
`event_id, station_id, waveform_index, origin_id, origin_index, origin_time, phase_index, phase_time, phase_score, phase_type, phase_polarity`
|
|
87
|
+
|
|
88
|
+
### DAS Prediction
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Predict with base PhaseNet on a HuggingFace subset
|
|
92
|
+
python scripts/predict_das.py --subset arcata --plot
|
|
93
|
+
|
|
94
|
+
# Predict with a trained DAS model on local data
|
|
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 \
|
|
100
|
+
--no-ema --plot
|
|
101
|
+
|
|
102
|
+
# Predict from a file list
|
|
103
|
+
python scripts/predict_das.py \
|
|
104
|
+
--file-list file_list.txt \
|
|
105
|
+
--model-type phasenet_das_plus \
|
|
106
|
+
--checkpoint output/model.pth \
|
|
107
|
+
--plot
|
|
108
|
+
|
|
109
|
+
# Multi-GPU prediction
|
|
110
|
+
bash scripts/predict_das.sh phasenet_das_plus arcata 8 output/model.pth
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Output: one parquet per event in `results/das/{model_name}/{subset}/`, with columns:
|
|
114
|
+
`event_id, channel_index, origin_id, origin_index, origin_time, phase_index, phase_time, phase_score, phase_type, dt_s, ps_center, ps_interval`
|
|
115
|
+
|
|
116
|
+
By default, picks are associated using P-S pairing. Use `--use-event-head` to associate via the model's event detection head instead.
|
|
117
|
+
|
|
118
|
+
## Training
|
|
119
|
+
|
|
120
|
+
### CEED (Seismic) Training
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
python train.py \
|
|
124
|
+
--model phasenet_plus \
|
|
125
|
+
--dataset-type ceed \
|
|
126
|
+
--label-path results/ceed \
|
|
127
|
+
--nx 16 \
|
|
128
|
+
--max-iters 100000 \
|
|
129
|
+
--batch-size 8 \
|
|
130
|
+
--workers 4 \
|
|
131
|
+
--lr 3e-4 \
|
|
132
|
+
--eval-interval 5000 \
|
|
133
|
+
--output-dir output/train_ceed
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
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
|
+
|
|
138
|
+
### DAS Training
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Using the training script
|
|
142
|
+
bash scripts/train_das.sh 0 arcata v26
|
|
143
|
+
|
|
144
|
+
# Or directly
|
|
145
|
+
python train.py \
|
|
146
|
+
--model phasenet_das_plus \
|
|
147
|
+
--dataset-type das \
|
|
148
|
+
--data-path data/quakeflow_das/arcata/data \
|
|
149
|
+
--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 \
|
|
157
|
+
--eval-interval 1000 --save-interval 1000 \
|
|
158
|
+
--output-dir output/train_das_arcata_v26
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Key Training Options
|
|
162
|
+
|
|
163
|
+
| Option | Description | Default |
|
|
164
|
+
|--------|-------------|---------|
|
|
165
|
+
| `--norm-kind` | where the statistics come from: `window` (the whole input), `trailing` (before each sample), `centred` (around it) | trailing |
|
|
166
|
+
| `--norm-window N` | normalization window, samples (`trailing`, `centred`) | 1024 |
|
|
167
|
+
| `--norm-scope` | what shares one scale: `station`, `component`, `global` | station |
|
|
168
|
+
| `--norm-stat` | `std` or `l1` | std |
|
|
169
|
+
| `--num-patch N` | Random crops per DAS sample (amortizes IO) | 2 |
|
|
170
|
+
| `--model-ema` | Enable exponential moving average | off |
|
|
171
|
+
| `--gradient-accumulation-steps N` | Accumulate gradients for larger effective batch | 1 |
|
|
172
|
+
| `--clip-grad-norm V` | Gradient clipping | 1.0 |
|
|
173
|
+
| `--compile` | Enable torch.compile | off |
|
|
174
|
+
| `--resume --checkpoint PATH` | Resume from checkpoint | - |
|
|
175
|
+
| `--reset-lr` | Reset LR schedule when resuming | off |
|
|
176
|
+
|
|
177
|
+
## Semi-supervised Training
|
|
178
|
+
|
|
179
|
+
Iterative self-training pipeline for DAS: predict → train on predictions → predict with new model → repeat.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Start from PhaseNet (train DAS model from scratch)
|
|
183
|
+
bash scripts/semisupervised_das.sh arcata 5 0
|
|
184
|
+
|
|
185
|
+
# Start from a pretrained DAS model
|
|
186
|
+
bash scripts/semisupervised_das.sh arcata 5 0 phasenet_das output/train_das_v26/checkpoint.pth
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Arguments: `[subset] [num_iterations] [gpu] [start_from] [checkpoint]`
|
|
190
|
+
|
|
191
|
+
- **From phasenet**: iteration 0 predicts with PhaseNet, iteration 1 trains from scratch (10k steps, warmup), iterations 2+ continue (1k steps, no warmup)
|
|
192
|
+
- **From phasenet_das**: iteration 0 predicts with pretrained DAS model, iterations 1+ continue (1k steps, no warmup)
|
|
193
|
+
|
|
194
|
+
Results are saved to `results/semisupervised_das/` and checkpoints to `output/semisupervised_das_v{N}/`.
|
|
195
|
+
|
|
196
|
+
## Design: one input contract per model
|
|
197
|
+
|
|
198
|
+
Three models share one code path; what differs between them is written down in each model's input
|
|
199
|
+
contract, not in the scripts that run them.
|
|
200
|
+
|
|
201
|
+
| | PhaseNet-2018 (TF port) | PhaseNet (retrain) | PhaseNet-DAS (retrain) |
|
|
202
|
+
|---|---|---|---|
|
|
203
|
+
| weights | converted TF checkpoint, no config of its own | `train.py` | `train.py` |
|
|
204
|
+
| backbone | `unet2018`, convolutions only | `unet2018` or `unet` | `unet` (attention over time and space) |
|
|
205
|
+
| contract | pinned in code, `PHASENET_2018_INPUT` | `model_config["input"]`, written by `train.py` | same |
|
|
206
|
+
| filtering | none of its own; the QuakeFlow pipeline applies a 1 Hz highpass and picks accelerometers on velocity, as TF did | trained with a random highpass (augmentation); at inference the caller's `--highpass` | trained with random highpass and common-mode removal (augmentations); at inference common-mode removal is on (`--no-common-mode`), the highpass off (`--highpass`) |
|
|
207
|
+
| normalization | TF `normalize_long` (`tf_interp`, 3000 samples) | trailing 1024, per station, std | trailing 1024, per channel (a channel is a one-component station) |
|
|
208
|
+
| inference cut | whole trace | whole trace for `unet2018`, else windows of `--nt`, half overlap, overlap-discard | windows of `--nt` x `--nx`, half overlap, overlap-discard |
|
|
209
|
+
| trained on | TF, 30 s windows | CEED event windows (`--dataset-type ceed`) | DAS HDF5 patches (`--dataset-type das`) |
|
|
210
|
+
| run on | continuous mseed (`scripts/predict_mseed.py`) | continuous mseed or CEED days (`scripts/predict_ceed.py`) | DAS HDF5 events (`scripts/predict_das.py`) |
|
|
211
|
+
|
|
212
|
+
### The contract
|
|
213
|
+
|
|
214
|
+
`InputSpec` (`phasenet/data/preprocess.py`) is saved in a checkpoint's `model_config["input"]` and
|
|
215
|
+
rebuilt by every loader; a checkpoint without one is refused. Its fields:
|
|
216
|
+
|
|
217
|
+
- `normalize`: a `NormalizeSpec` (`phasenet/normalize.py`): `kind` (`window`: the classic per-window
|
|
218
|
+
statistics; `trailing`: the samples before each sample, causal, the default for the retrains;
|
|
219
|
+
`centred`: the samples around each sample; `tf_interp`: the 2018 port's TF `normalize_long`),
|
|
220
|
+
`scope` (`station`: one scale shared by a station's components, which keeps their relative
|
|
221
|
+
amplitudes; `component`; `global`), `stat` (`std` or `l1`), `window` in samples, `compress`
|
|
222
|
+
(`None` or `log1p`).
|
|
223
|
+
- `window_nt`: the longest input, in samples, the backbone takes at once. `None` means any length,
|
|
224
|
+
which is true of a convolution-only backbone, so inference runs on the whole trace.
|
|
225
|
+
- `window_nx`: stations per patch, `None` for all at once. A model that couples channels (DAS) sets
|
|
226
|
+
it and gets a taper along that axis.
|
|
227
|
+
|
|
228
|
+
The model knows samples, not a rate. The caller owns the working rate (`predict_mseed.py
|
|
229
|
+
--sampling-rate`, 100 Hz by default): it resamples the input to it, applies any Hz corner at it, and
|
|
230
|
+
turns pick indices into times with it. The same weights therefore apply to a network at 100 Hz or a
|
|
231
|
+
borehole at 500 Hz. Filtering is not in the contract: training applies a highpass (and, for DAS,
|
|
232
|
+
common-mode removal) at random as an augmentation, so the model works with or without it and the
|
|
233
|
+
choice belongs to whoever runs it.
|
|
234
|
+
|
|
235
|
+
### Preprocessing, shared with the dataset
|
|
236
|
+
|
|
237
|
+
Three functions live in `phasenet/data/preprocess.py` with identical copies in
|
|
238
|
+
`QuakeFlow/datasets/common.py`, asserted identical by `datasets/tests/test_preprocess.py`, so the
|
|
239
|
+
training data and the picker's input are prepared by the same code, in the same order: merge,
|
|
240
|
+
resample, orient. `merge_stream` joins a station's
|
|
241
|
+
fragments: overlaps resolved, every contiguous piece demeaned on its own, gaps filled with zeros that
|
|
242
|
+
continue each piece at its own level. `orient_from_inventory` recovers Z-up, N and E from
|
|
243
|
+
each channel's inventory azimuth and dip, whatever the channel is named, passing the usual E/N/Z
|
|
244
|
+
stations through untouched and recording `z_sign` where a vertical had to be negated; the picker
|
|
245
|
+
applies it when it has an inventory. `resample_trace` brings a trace to the working rate: polyphase
|
|
246
|
+
interpolation upward (an FIR lowpass at the old Nyquist, flat over the recorded band and free of edge
|
|
247
|
+
artifacts; obspy's FFT resampling puts a spike on the first sample, and its default Hann window is a
|
|
248
|
+
lowpass that removed 10-20 Hz from 40 Hz stations), a causal 4-pole Butterworth at 0.4 x the target
|
|
249
|
+
rate then decimation downward (causal, so nothing precedes an onset). On the demo the polyphase and
|
|
250
|
+
the FFT kernel give the same analyst-pick recall on every instrument class and differ in 0.06 % of the
|
|
251
|
+
picks. `QuakeFlow/datasets/common.py` holds an identical copy and
|
|
252
|
+
`datasets/tests/test_preprocess.py` asserts the two agree sample by sample.
|
|
253
|
+
|
|
254
|
+
`highpass` is the detrend, when one is wanted: TF PhaseNet's spline detrend gave the same picks at six
|
|
255
|
+
times the cost and follows the long-period motion of large earthquakes; the highpass is causal and
|
|
256
|
+
streams. `remove_common_mode` subtracts the median over DAS channels. Both are augmentations in
|
|
257
|
+
training (`RandomHighpass`, 0.5-2 Hz half the time; `RandomCommonMode`, half the time) and options
|
|
258
|
+
at inference: for DAS the common-mode removal is on by default (cheap), the highpass off (costly).
|
|
259
|
+
|
|
260
|
+
### What miniSEED data does, and what happens to it
|
|
261
|
+
|
|
262
|
+
A station-day is rarely three clean channels of one continuous record. Every case below is handled by
|
|
263
|
+
the same code on both sides (`merge_stream`, `orient_from_inventory`, `resample_trace`, the
|
|
264
|
+
normalization), so a training record and a picker input that came from the same kind of file look the
|
|
265
|
+
same to the model. The picker reports a station-day it cannot read as an error and moves on; the
|
|
266
|
+
dataset skips the station.
|
|
267
|
+
|
|
268
|
+
| the data | what is done | where |
|
|
269
|
+
|---|---|---|
|
|
270
|
+
| Overlapping fragments (re-sent or duplicated records) | obspy merge method 1: the later fragment wins, sample for sample, no blending. Duplicates agree exactly and leave no mark. | `merge_stream` |
|
|
271
|
+
| Timing tears (gaps up to 0.1 s between records, or at the join of two day files whose sample grids differ by a fraction of a sample) | bridged by linear interpolation: a tear is not missing data, and a zero there would be a spike | `merge_stream` |
|
|
272
|
+
| Gaps longer than 0.1 s (outages) | every contiguous piece is demeaned on its own, then the gap is filled with zeros, which continue each piece at its own level. The zeros are data from then on: the normalization does not infer gaps from values, a window straddling a gap is scaled by a std the zeros lower, and a filter applied afterwards rings a little into the gap. Nothing is tapered: real samples are never altered. | `merge_stream`; the normalization |
|
|
273
|
+
| A fragment with a wrong timestamp (months away) | fragments farther than two days from the station-day's median start are dropped before the merge | picker |
|
|
274
|
+
| Fragments of one channel at different sampling rates | obspy cannot merge them: the station-day fails with an error (picker) or the station is skipped (dataset) | `merge_stream` |
|
|
275
|
+
| A channel recorded below or above the working rate (BH at 40 Hz, borehole at 500 Hz) | resampled to the working rate, the same kernel on both sides | `resample_trace` |
|
|
276
|
+
| A missing channel (one or two of three) | the present ones are placed by name, the missing component is zeros. A component without variation has scale 0, read as 1, so it stays zero; with `station` scope the live components come out sqrt(3/live) larger, the 2018 model's `tf_interp` applies TF's 3/live gain | picker, dataset cut; the normalization |
|
|
277
|
+
| A dead channel (present, flat) | the same as missing: the dataset leaves a flat component zero and unlisted, the normalization gives it scale 0 | dataset cut; the normalization |
|
|
278
|
+
| Channels named 1/2/3 or X/Y/Z, or mislabeled E/N/Z (Z with dip +90, horizontals swapped or rotated) | with an inventory, Z-up, N and E are recovered from each channel's azimuth and dip whatever its name; `z_sign` records a negated vertical so polarity labels follow. Without an inventory the picker places channels by a name table | `orient_from_inventory`; picker |
|
|
279
|
+
| Nearly parallel channel vectors (a metadata error: two channels with the same azimuth) | no rotation can separate them, so the group is kept as named instead of averaging or amplifying the channels | `orient_from_inventory` |
|
|
280
|
+
| A single channel | at most one component is recovered (Z, or a horizontal on its axis); an oblique single horizontal gives nothing | `orient_from_inventory` |
|
|
281
|
+
| Several location codes or bands for one station | one station-day per location and instrument in the picker's file list; the dataset keeps the catalog's location and one band per record | picker, dataset cut |
|
|
282
|
+
| A record shorter than the model's window, or not a multiple of 16 samples | zero padding at the end, the zeros being data as above | `RandomCrop`, `predict_2d` |
|
|
283
|
+
|
|
284
|
+
Consistency is asserted, not assumed: `datasets/tests/test_preprocess.py` requires the three
|
|
285
|
+
functions to have identical source in both copies and `resample_trace` to give identical samples.
|
|
286
|
+
|
|
287
|
+
### Normalization
|
|
288
|
+
|
|
289
|
+
Training normalizes exactly as inference does, so the two cannot differ. For the local kinds
|
|
290
|
+
(`trailing`, `centred`) inference normalizes the whole record and cuts it afterwards; training
|
|
291
|
+
normalizes the whole loaded waveform and the random crop is a view of the result. For the
|
|
292
|
+
`window` kind the statistics are defined on the window, so each inference patch is normalized on
|
|
293
|
+
its own and the training crop comes before the normalization. `tests/test_normalize.py` asserts
|
|
294
|
+
that a training crop equals what `predict_2d` computes, for every kind.
|
|
295
|
+
|
|
296
|
+
`trailing`, the default for the retrains, scales every sample by the statistics of the `window`
|
|
297
|
+
samples ending at it (the first window's for the first window). It is causal by design: a centred
|
|
298
|
+
window lets a large event inflate the scale for half a window *before* it happens, so a small
|
|
299
|
+
event shortly before a larger one loses its amplitude; with a trailing window it keeps it
|
|
300
|
+
(measured on the Ridgecrest demo: 90 % of its isolated amplitude against 28 % under the centred TF
|
|
301
|
+
window). The statistics come from cumulative sums over every sample; a zero is a sample like any
|
|
302
|
+
other, and missing data is recognised by its variation, not its value: a window, station or record
|
|
303
|
+
with none has scale 0, read as 1, so a dead component comes out as zeros. The same `Normalizer` runs on a training waveform, a
|
|
304
|
+
whole day, or a stream with carried state. The model never normalizes in `forward`: the data
|
|
305
|
+
transforms and `predict_2d` call `model.normalizer`.
|
|
306
|
+
|
|
307
|
+
In the training pipelines, augmentations that shape amplitude or spectrum (highpass, bandpass,
|
|
308
|
+
common-mode removal, event stacking) run before the normalization; calibrated noise, flips,
|
|
309
|
+
masking and the median filter run after it, on normalized data, which is what the model meets at
|
|
310
|
+
inference.
|
|
311
|
+
|
|
312
|
+
The 2018 port keeps TF's `normalize_long` as the `tf_interp` branch: a centred window measured
|
|
313
|
+
every `window // 2` samples and interpolated, with TF's 3/live gain for stations missing
|
|
314
|
+
components. Those weights were validated against TF with exactly this; an exact centred window is
|
|
315
|
+
not equivalent (6.8 % of the demo picks change), so it is not a shortcut.
|
|
316
|
+
|
|
317
|
+
### Inference
|
|
318
|
+
|
|
319
|
+
`predict_2d(model, waveform, device)` is the one entry point. It normalizes the whole `(nch, nx,
|
|
320
|
+
nt)` input, then cuts by the contract: the whole trace as one window when `window_nt` is `None`
|
|
321
|
+
(patches only add seams, and the trace is padded to a multiple of 16 rather than the U-Net's
|
|
322
|
+
full 1024, since a wall of zeros inside the receptive field erases picks at the day boundary),
|
|
323
|
+
otherwise windows of `window_nt` overlapping by half, each sample taken from the window whose centre
|
|
324
|
+
is nearest (overlap-discard, so nothing is predicted within a quarter window of an edge), and along
|
|
325
|
+
stations `window_nx` patches assigned the same way when set. The three predict scripts differ only in how
|
|
326
|
+
they read data.
|
|
327
|
+
|
|
328
|
+
### Training
|
|
329
|
+
|
|
330
|
+
`train.py` builds the contract from `--norm-kind`, `--norm-window`, `--norm-scope`, `--norm-stat`,
|
|
331
|
+
`--norm-compress`, `--nt` and `--nx`, gives it to the data transforms and writes it into the
|
|
332
|
+
checkpoint. The default transforms run, in order: `RandomHighpass` (and `RandomCommonMode` for DAS),
|
|
333
|
+
a whole-sample `Normalize` that only equalizes samples for the augmentations,
|
|
334
|
+
event stacking, `InputNormalize` (before `RandomCrop` for the local kinds, after it for `window`),
|
|
335
|
+
then noise, flips and masking.
|
|
336
|
+
|
|
337
|
+
### Verification
|
|
338
|
+
|
|
339
|
+
Ten percent of CEED training samples are event-free: the 40.96 s of real noise before each record's
|
|
340
|
+
first P, through the same pipeline. `python tests/test_normalize.py` pins the contract: the `tf_interp` branch against a numpy transcription
|
|
341
|
+
of TF's `normalize_long`, the window, trailing and centred kinds against a sample-by-sample brute
|
|
342
|
+
force for every scope and statistic, the training pipelines against `predict_2d`, streamed chunks
|
|
343
|
+
against the whole trace, gaps and dead components, and the spec's round trip through `model_config`. On the
|
|
344
|
+
QuakeFlow Ridgecrest demo (45 station-days) the port reproduces TF PhaseNet: identical picks on the
|
|
345
|
+
100 Hz stations, analyst-pick recall 0.861 for both, the 40 Hz stations differing only by the
|
|
346
|
+
resampling kernel. `scripts/plot_normalization.py` shows what each candidate normalization does to
|
|
347
|
+
a station-day.
|
|
348
|
+
|
|
349
|
+
### Open issues
|
|
350
|
+
|
|
351
|
+
- **Station grouping.** With `--nx` above 1 the CEED loader builds one sample per event from its `nx`
|
|
352
|
+
nearest stations, and the `unet` backbone attends across them. `scripts/predict_mseed.py` picks one
|
|
353
|
+
station-day at a time, so it hands such a model a single station, a situation it was never trained on
|
|
354
|
+
and in which its cross-station attention has nothing to attend to. `scripts/predict_ceed.py` builds the
|
|
355
|
+
same event arrays as training and is not affected. Until resolved: train the model that picks
|
|
356
|
+
continuous data with `--nx 1`, and `predict_mseed.py` should refuse a checkpoint whose `window_nx`
|
|
357
|
+
is greater than 1 instead of feeding it one station. The full fix is a multi-station reader that
|
|
358
|
+
assembles concurrent station-days into a `(3, nx, nt)` array with a spatial station order used in
|
|
359
|
+
training as well (the training sort by distance to the event does not exist at inference), plus
|
|
360
|
+
chunked inference along time to bound memory.
|
|
361
|
+
- **Crop edges.** Training crops are 4,096 samples while the `unet2018` receptive field is 3,331, so
|
|
362
|
+
most training samples sit within half a receptive field of a zero-padded crop edge, which a whole
|
|
363
|
+
day never shows the model. Longer crops (the record has 12,288 samples) or a loss mask near edges.
|
|
364
|
+
- **Trace-start rule in training.** A crop that overlaps the first 1,024 samples of its record is
|
|
365
|
+
scaled by the first window's statistics; inference meets that once a day. Start crops at or after
|
|
366
|
+
the normalization window.
|
|
367
|
+
- **DAS archives.** The training loader differentiates OptaSense continuous files to strain rate;
|
|
368
|
+
the event files used for training and prediction are stored as strain rate already. A model run on
|
|
369
|
+
continuous archives needs that step in the shared preprocessing.
|
|
370
|
+
- **Gap edges.** A gap is zeros after each piece's own demean (see the miniSEED table), and nothing
|
|
371
|
+
is done about picks at its edges. What the model does there is a training question: a gap
|
|
372
|
+
augmentation for CEED (DAS has `Masking`) and a pick filter near gap edges.
|
|
373
|
+
- **Shared preprocessing is a copy.** `QuakeFlow/datasets/common.py` holds identical copies of
|
|
374
|
+
`merge_stream`, `orient_from_inventory` and `resample_trace`, kept equal by a test. One source,
|
|
375
|
+
importable without torch (move `InputSpec` out of `preprocess.py`), would make the guarantee structural.
|