Real-time seismic P-wave detector using a streaming neural network ensemble, deployed to seismic.fib896.com.
A 3-seed ensemble of StreamingNet models (1D CNN + orbit-permuted Hebbian buffer) listens to live SeedLink feeds from GEOFON and supplementary networks. When N_CONSENSUS stations independently fire within a 240-second window, a detection is logged (widened from 60s on 2026-08-19 — backfilled M5.5-6.9 events showed 98-208s between confirming stations' peak-confidence times, well past the old window). Epicenters are estimated via a flat-earth P-wave arrival time inversion (Nelder-Mead). Detections are cross-checked against USGS and EMSC earthquake catalogs.
SeedLink stream → normalize → StreamingNet × 3 seeds → ensemble vote
→ consensus across stations → epicenter localization → catalog lookup → dashboard
StreamingNet — 3-channel 1D CNN with an orbit-permuted Hebbian buffer:
Input: (3, 100) — 3-component seismogram, 1 second at 100 Hz
→ ConvBlock(3→32) → ConvBlock(32→64) → ConvBlock(64→128) → AdaptiveAvgPool
→ orbit-permuted Hebbian buffer (CYCLES=1, DECAY=0.876, STRENGTH=1.429)
→ Linear(128, 2) — seismic / noise classifier
→ Linear(128, 1) — magnitude estimator
The buffer permutation is seeded per ensemble member, diversifying the feature basis across seeds. Pre-trained on the STEAD dataset (chunk2) with magnitude-stratified sampling, then fine-tuned on station-collected regional data.
Performance (STEAD holdout, threshold=0.835): 88.0% precision / 95.7% recall
Performance (after regional fine-tune, eval split): 94.4% / 93.2% / 93.2% precision across seeds, mean F1 = 0.919
| Network | Code | Location |
|---|---|---|
| GE | APE | Aegean, Greece |
| GE | MORC | Morava, Czech Republic |
| GE | KBS | Svalbard, Norway |
| GE | WLF | Walferdange, Luxembourg |
| GE | MATE | Matera, Italy |
| GE | KARP | Karpathos, Greece |
| CX | PSGCX | Paso Grande, Chile |
| CX | HMBCX | Humberstone, Chile |
| DK | GDH | Godhavn, Greenland |
| DK | SCO | Scoresbysund, Greenland |
| GE | KIBK | Kibungo, Rwanda |
| GE | LBTB | Lobatse, Botswana |
| GE | PMG | Port Moresby, Papua New Guinea |
| GE | HNR | Honiara, Solomon Islands |
Primary streams via geofon.gfz-potsdam.de:18000 (GEOFON). Station list above reflects fly.toml's STATIONS — check that file for the current live set, since it's changed more often than this table.
IRIS note: rtserve.iris.washington.edu:18000 was upgraded to RingServer/4.5.6 (SeedLink v4.0 protocol) in 2026. obspy EasySeedLinkClient (v3.1) is incompatible — station SELECT and DATA commands are rejected. IRIS stations (IRIS_STATIONS in fly.toml) currently do not stream successfully; set to empty if you want a clean log.
# Deploy to Fly.io (requires fly CLI + authenticated account)
# Smart: static/template-only changes push via sftp with no restart;
# anything else triggers a full `fly deploy`
make deploy
# Force-push static/template files to the running container, skipping the smart-deploy diff check
make deploy-static
# Force full rebuild (e.g. after Dockerfile changes)
make deploy-clean
# Tail live logs
make fly-logsNote: env vars set in fly.toml override the Python defaults in config.py — if you change a default in code (e.g. the P_LEAD_S 0.4→0.5 fix on 2026-08-20), also update fly.toml/fly.alpha.toml or the code change won't actually take effect in prod. This has bitten us before.
Requires STEAD chunk2:
wget -c 'https://zenodo.org/records/3911667/files/chunk2.hdf5'
wget -c 'https://zenodo.org/records/3911667/files/chunk2.csv'
pip install torch numpy h5py scipy scikit-learn tqdm pandas
python train.py --data /data/training --checkpoints checkpoints/ --epochs 30GPU strongly recommended. CPU training for 3 seeds × 30 epochs takes several hours.
collect_regional.py fetches P-wave windows for M5.5+ events at Pacific/Australian stations (IU.NWAO, II.WRAB, IU.MAJO, IU.SNZO) via IRIS FDSN, using IASP91 travel times to window around the P arrival:
python collect_regional.py # saves .npz files to ./training/Then fine-tune the existing ensemble on the collected windows:
python train.py --data training --checkpoints checkpoints --epochs 30 --lr 1e-4
# checkpoints/seed_{n}_pretrain.pt backups are written before overwritingFine-tuning uses a two-phase schedule: classifier head only for the first half of epochs (higher LR), then all layers unfrozen at 10× lower LR. A class-balanced weighted sampler handles imbalanced positive/negative ratios.
cp .env.example .env
# edit .env — set SEEDLINK_SERVER, STATIONS, etc.
make dev # starts via docker compose
make logs # tail logs
make shell # bash into containerAll parameters are set via environment variables (see fly.toml and .env.example):
| Variable | Default | Description |
|---|---|---|
SEEDLINK_SERVER |
geofon.gfz-potsdam.de:18000 |
Primary SeedLink server |
IRIS_SERVER |
rtserve.iris.washington.edu:18000 |
Secondary SeedLink server (see IRIS note above) |
STATIONS |
GE.APE,... |
Comma-separated station list for primary server |
IRIS_STATIONS |
IU.COR,... |
Comma-separated station list for secondary server |
CHANNELS |
HHZ,HHN,HHE |
Seismic channels |
THRESHOLD |
0.835 |
Detection confidence threshold |
N_CONSENSUS |
2 |
Stations required to confirm a detection (2 is appropriate for this sparse network) |
CONSENSUS_WINDOW |
240 |
Seconds within which consensus must occur (widened from 60 on 2026-08-19, see above) |
ALERT_COOLDOWN |
60 (300 in prod) |
Minimum gap between fired alerts overall |
PER_STATION_COOLDOWN |
120 |
Minimum gap between alerts from the same station |
N_SEEDS |
3 |
Ensemble size |
STALTA_ON |
1 |
Enable STA/LTA pre-filter (blocks low-energy noise) |
STALTA_SHORT_S |
0.5 |
STA window length (seconds) |
STALTA_LONG_S |
10.0 |
LTA window length (seconds) |
STALTA_THRESH |
2.5 |
Minimum STA/LTA ratio to pass to consensus |
STALTA_LARGE_THRESH |
12.0 |
STA/LTA ratio indicating a large event |
THRESHOLD_LARGE |
0.45 |
Lowered confidence threshold used for large-event rescue |
MIN_LOGIT_GAP |
0.0 |
Minimum classifier logit gap required (0 = disabled, no empirical baseline yet) |
P_LEAD_S |
0.5 |
Model's pre-P horizon; must match between config.py's default and fly.toml's env override — they drifted once, see Deploy note |
P_VEL_KM_S |
8.0 |
Assumed P-wave velocity for epicenter localization |
LOC_MIN_STA |
3 |
Minimum stations required to attempt localization |
USGS_MIN_MAG |
4.0 |
Minimum magnitude for USGS catalog cross-check |
EMSC_MIN_MAG |
2.0 |
Minimum magnitude for EMSC fallback cross-check |
USGS_SIG_MIN_MAG |
5.5 |
Minimum magnitude to flag as a "significant event" poll |
TELE_MATCH_WINDOW |
300 |
± seconds around expected P-arrival to count as a catalog match |
STATION_LOG_DIR / STATION_LOG_RETENTION_DAYS |
/data/station_log / 3 |
Persisted per-station confidence log (Fly's own log buffer only keeps a couple minutes — added after a 2026-08-19 missed-M5.5 near Ruteng, Indonesia was unrecoverable at the station-score level) |
API_KEY |
unset | If set, required as X-API-Key header (or ?api_key=) on /api/config, /api/stations, and other non-public endpoints |
Slack + Bitcoin integrations (undocumented until now): SLACK_WEBHOOK_URL/SLACK_SIGNING_SECRET wire up /slack/command for a Slack slash-command interface; BTCVM_LEDGER_PATH/BTCVM_BROADCAST/BTC_WIF back /api/btcvm, an optional Bitcoin OP_RETURN detection ledger.
# Python (detection/consensus logic)
pytest tests/ -v
# Cypress E2E (web dashboard — map, replay slider, tooltips)
npm install
npm run e2e # headless: boots the Flask web layer + runs Cypress
npm run e2e:open # interactive Cypress runnerThe E2E suite boots only the web layer (scripts/e2e_server.py — no torch/seedlink needed) and exists specifically to catch template/static desyncs: a 2026-08-18 incident shipped stale index.html alongside fresh app.js because Flask caches compiled templates and doesn't reload them without a process restart. CI runs both suites (test, e2e jobs) before every deploy.
AGPL-3.0-or-later — see LICENSE