requake 0.8.2__tar.gz → 0.8.3__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 (82) hide show
  1. {requake-0.8.2 → requake-0.8.3}/CHANGELOG.md +20 -0
  2. {requake-0.8.2 → requake-0.8.3}/PKG-INFO +19 -3
  3. {requake-0.8.2 → requake-0.8.3}/README.md +17 -1
  4. {requake-0.8.2 → requake-0.8.3}/requake/_version.py +3 -3
  5. {requake-0.8.2 → requake-0.8.3}/requake/config/configspec.conf +11 -1
  6. {requake-0.8.2 → requake-0.8.3}/requake/config/parse_arguments.py +173 -0
  7. {requake-0.8.2 → requake-0.8.3}/requake/config/rq_setup.py +13 -15
  8. {requake-0.8.2 → requake-0.8.3}/requake/config/utils.py +31 -0
  9. {requake-0.8.2 → requake-0.8.3}/requake/database/families.py +26 -8
  10. {requake-0.8.2 → requake-0.8.3}/requake/database/pairs.py +37 -15
  11. {requake-0.8.2 → requake-0.8.3}/requake/families/families.py +9 -5
  12. {requake-0.8.2 → requake-0.8.3}/requake/main.py +15 -0
  13. {requake-0.8.2 → requake-0.8.3}/requake/plot/colormaps.py +13 -13
  14. {requake-0.8.2 → requake-0.8.3}/requake/plot/plot_families.py +4 -1
  15. requake-0.8.3/requake/scan/parallel_diagnostics.py +273 -0
  16. {requake-0.8.2 → requake-0.8.3}/requake/scan/scan_catalog.py +51 -38
  17. requake-0.8.3/requake/scan/scan_catalog_helpers.py +359 -0
  18. {requake-0.8.2 → requake-0.8.3}/requake/scan/scan_catalog_pairs.py +34 -42
  19. {requake-0.8.2 → requake-0.8.3}/requake/scan/scan_catalog_workers.py +332 -77
  20. requake-0.8.3/requake/scan/slurm_diagnostics.py +130 -0
  21. {requake-0.8.2 → requake-0.8.3}/requake/waveforms/station_metadata.py +85 -1
  22. {requake-0.8.2 → requake-0.8.3}/requake/waveforms/waveform_pair.py +72 -2
  23. {requake-0.8.2 → requake-0.8.3}/requake/waveforms/waveforms.py +126 -67
  24. requake-0.8.3/requake/wfcache/__init__.py +35 -0
  25. requake-0.8.3/requake/wfcache/commands.py +919 -0
  26. requake-0.8.3/requake/wfcache/storage.py +765 -0
  27. {requake-0.8.2 → requake-0.8.3}/requake.egg-info/PKG-INFO +19 -3
  28. {requake-0.8.2 → requake-0.8.3}/requake.egg-info/SOURCES.txt +6 -1
  29. requake-0.8.2/requake/scan/scan_catalog_helpers.py +0 -317
  30. {requake-0.8.2 → requake-0.8.3}/CONTRIBUTORS.txt +0 -0
  31. {requake-0.8.2 → requake-0.8.3}/LICENSE.txt +0 -0
  32. {requake-0.8.2 → requake-0.8.3}/MANIFEST.in +0 -0
  33. {requake-0.8.2 → requake-0.8.3}/pyproject.toml +0 -0
  34. {requake-0.8.2 → requake-0.8.3}/requake/__init__.py +0 -0
  35. {requake-0.8.2 → requake-0.8.3}/requake/catalog/__init__.py +0 -0
  36. {requake-0.8.2 → requake-0.8.3}/requake/catalog/catalog.py +0 -0
  37. {requake-0.8.2 → requake-0.8.3}/requake/catalog/print_catalog.py +0 -0
  38. {requake-0.8.2 → requake-0.8.3}/requake/catalog/read_catalog.py +0 -0
  39. {requake-0.8.2 → requake-0.8.3}/requake/catalog/read_catalog_from_csv.py +0 -0
  40. {requake-0.8.2 → requake-0.8.3}/requake/catalog/read_catalog_from_fdsnws.py +0 -0
  41. {requake-0.8.2 → requake-0.8.3}/requake/catalog/read_catalog_from_quakeml.py +0 -0
  42. {requake-0.8.2 → requake-0.8.3}/requake/config/__init__.py +0 -0
  43. {requake-0.8.2 → requake-0.8.3}/requake/config/config.py +0 -0
  44. {requake-0.8.2 → requake-0.8.3}/requake/config/configobj/LICENSE +0 -0
  45. {requake-0.8.2 → requake-0.8.3}/requake/config/configobj/__init__.py +0 -0
  46. {requake-0.8.2 → requake-0.8.3}/requake/config/configobj/_version.py +0 -0
  47. {requake-0.8.2 → requake-0.8.3}/requake/config/configobj/validate.py +0 -0
  48. {requake-0.8.2 → requake-0.8.3}/requake/config/generic_printer.py +0 -0
  49. {requake-0.8.2 → requake-0.8.3}/requake/database/__init__.py +0 -0
  50. {requake-0.8.2 → requake-0.8.3}/requake/database/catalog.py +0 -0
  51. {requake-0.8.2 → requake-0.8.3}/requake/database/db.py +0 -0
  52. {requake-0.8.2 → requake-0.8.3}/requake/database/templates.py +0 -0
  53. {requake-0.8.2 → requake-0.8.3}/requake/database/trace_metadata.py +0 -0
  54. {requake-0.8.2 → requake-0.8.3}/requake/families/__init__.py +0 -0
  55. {requake-0.8.2 → requake-0.8.3}/requake/families/build_families.py +0 -0
  56. {requake-0.8.2 → requake-0.8.3}/requake/families/build_templates.py +0 -0
  57. {requake-0.8.2 → requake-0.8.3}/requake/families/flag_family.py +0 -0
  58. {requake-0.8.2 → requake-0.8.3}/requake/families/pairs.py +0 -0
  59. {requake-0.8.2 → requake-0.8.3}/requake/families/print_families.py +0 -0
  60. {requake-0.8.2 → requake-0.8.3}/requake/families/print_pairs.py +0 -0
  61. {requake-0.8.2 → requake-0.8.3}/requake/formulas/__init__.py +0 -0
  62. {requake-0.8.2 → requake-0.8.3}/requake/formulas/conversion.py +0 -0
  63. {requake-0.8.2 → requake-0.8.3}/requake/formulas/moment.py +0 -0
  64. {requake-0.8.2 → requake-0.8.3}/requake/formulas/slip.py +0 -0
  65. {requake-0.8.2 → requake-0.8.3}/requake/plot/__init__.py +0 -0
  66. {requake-0.8.2 → requake-0.8.3}/requake/plot/cached_tiler.py +0 -0
  67. {requake-0.8.2 → requake-0.8.3}/requake/plot/map_families.py +0 -0
  68. {requake-0.8.2 → requake-0.8.3}/requake/plot/map_tiles.py +0 -0
  69. {requake-0.8.2 → requake-0.8.3}/requake/plot/plot_cumulative.py +0 -0
  70. {requake-0.8.2 → requake-0.8.3}/requake/plot/plot_pair.py +0 -0
  71. {requake-0.8.2 → requake-0.8.3}/requake/plot/plot_timespans.py +0 -0
  72. {requake-0.8.2 → requake-0.8.3}/requake/plot/plot_utils.py +0 -0
  73. {requake-0.8.2 → requake-0.8.3}/requake/scan/__init__.py +0 -0
  74. {requake-0.8.2 → requake-0.8.3}/requake/scan/scan_templates.py +0 -0
  75. {requake-0.8.2 → requake-0.8.3}/requake/waveforms/__init__.py +0 -0
  76. {requake-0.8.2 → requake-0.8.3}/requake/waveforms/arrivals.py +0 -0
  77. {requake-0.8.2 → requake-0.8.3}/requake.egg-info/dependency_links.txt +0 -0
  78. {requake-0.8.2 → requake-0.8.3}/requake.egg-info/entry_points.txt +0 -0
  79. {requake-0.8.2 → requake-0.8.3}/requake.egg-info/requires.txt +0 -0
  80. {requake-0.8.2 → requake-0.8.3}/requake.egg-info/top_level.txt +0 -0
  81. {requake-0.8.2 → requake-0.8.3}/setup.cfg +0 -0
  82. {requake-0.8.2 → requake-0.8.3}/setup.py +0 -0
@@ -2,6 +2,25 @@
2
2
 
3
3
  Copyright (c) 2021-2026 Claudio Satriano <satriano@ipgp.fr>
4
4
 
5
+ ## [0.8.3] - 2026-06-19
6
+
7
+ Note: you should run `requake update_config` to update your config file
8
+ to the latest version.
9
+
10
+ The waveform disk cache is now a single SQLite database
11
+ (`OUTDIR/waveform_cache.sqlite`) with a persistent negative cache that
12
+ remembers failed downloads across runs, eliminating redundant HTTP requests.
13
+
14
+ - New `wfcache` CLI: `prefetch`, `print`/`inspect`, `extract`,
15
+ `reset-failures`.
16
+ - New config: `catalog_waveform_cache_failure_max_retries`,
17
+ `catalog_waveform_cache_failure_backoff_s`,
18
+ `catalog_waveform_require_prefetch`.
19
+ - Several optimization for parallel runs in `scan_catalog` (especially on
20
+ Slurm clusters) and for startup time of `scan_catalog -c` (continue an
21
+ interrupted scan).
22
+ - Extended documentation for `read_catalog`, `scan_catalog` and `wfcache`.
23
+
5
24
  ## [0.8.2] - 2026-06-03
6
25
 
7
26
  Note: you might want to run `requake update_config` to update your config file
@@ -187,6 +206,7 @@ Mostly a bugfix release with a slightly improved trace plotting.
187
206
 
188
207
  - Initial release, not yet feature complete
189
208
 
209
+ [0.8.3]: https://github.com/SeismicSource/requake/compare/v0.8.2...v0.8.3
190
210
  [0.8.2]: https://github.com/SeismicSource/requake/compare/v0.8.1...v0.8.2
191
211
  [0.8.1]: https://github.com/SeismicSource/requake/compare/v0.8...v0.8.1
192
212
  [0.8]: https://github.com/SeismicSource/requake/compare/v0.7.3...v0.8
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: requake
3
- Version: 0.8.2
3
+ Version: 0.8.3
4
4
  Summary: Repeating earthquakes search and analysis
5
5
  Author-email: Claudio Satriano <satriano@ipgp.fr>
6
6
  License-Expression: GPL-3.0-or-later
@@ -37,7 +37,7 @@ Dynamic: description
37
37
  Dynamic: description-content-type
38
38
  Dynamic: license-file
39
39
 
40
- <img src="https://cdn.jsdelivr.net/gh/SeismicSource/requake@9071fd7343802f3ef642a16f6bb02ff5ce1d88f5/imgs/Requake_logo.svg" width="400">
40
+ <img src="https://cdn.jsdelivr.net/gh/SeismicSource/requake@407e6c96ec3574f6e3743741763ca5b7c4daf914/imgs/Requake_logo.svg" width="400">
41
41
 
42
42
  # Requake
43
43
 
@@ -181,6 +181,8 @@ Different commands are available:
181
181
  families
182
182
  scan_templates scan a continuous waveform stream using one or more
183
183
  templates
184
+ wfcache manage persistent waveform cache (prefetch, print,
185
+ inspect, extract, reset_failures)
184
186
 
185
187
  Certain commands (e.g., `plot_pair`) require further arguments (use, e.g.,
186
188
  `requake plot_pair -h` to get help).
@@ -216,6 +218,14 @@ Now, build the catalog of event pairs with:
216
218
 
217
219
  requake scan_catalog
218
220
 
221
+ When relying on FDSN web services for waveform data, it is strongly
222
+ recommended to prefetch all waveform windows before running the scan.
223
+ This downloads every required waveform once and stores it in a local
224
+ SQLite cache, avoiding repeated downloads and dramatically reducing
225
+ overall runtime for large catalogs:
226
+
227
+ requake wfcache prefetch
228
+
219
229
  Once done ([it will take time!](#performances)), you are ready to build
220
230
  repeating earthquake families:
221
231
 
@@ -232,6 +242,12 @@ repeating earthquake families:
232
242
  performance is typically dominated by waveform download latency rather than
233
243
  computation.
234
244
 
245
+ - For large FDSN-based runs, use `requake wfcache prefetch` before
246
+ `requake scan_catalog` to download all waveforms upfront into a local
247
+ SQLite cache. This eliminates repeated downloads, lets the scan read
248
+ exclusively from disk, and is the single most effective way to speed up
249
+ catalog scanning.
250
+
235
251
  - `requake build_families` is fast™.
236
252
 
237
253
  ## How to Cite
@@ -244,7 +260,7 @@ If you used Requake for a scientific paper, please cite it as:
244
260
  Please replace `X.Y` with the Requake version number you used.
245
261
 
246
262
  You can also cite the following abstract presented at the
247
- 2016 AGU Fall Meeting:
263
+ 2021 AGU Fall Meeting:
248
264
 
249
265
  > Satriano, C., Doucet, A. & Bouin, M.-P. (2021).
250
266
  > Probing the creep rate along the Lesser Antilles Subduction Zone through repeating earthquakes.
@@ -142,6 +142,8 @@ Different commands are available:
142
142
  families
143
143
  scan_templates scan a continuous waveform stream using one or more
144
144
  templates
145
+ wfcache manage persistent waveform cache (prefetch, print,
146
+ inspect, extract, reset_failures)
145
147
 
146
148
  Certain commands (e.g., `plot_pair`) require further arguments (use, e.g.,
147
149
  `requake plot_pair -h` to get help).
@@ -177,6 +179,14 @@ Now, build the catalog of event pairs with:
177
179
 
178
180
  requake scan_catalog
179
181
 
182
+ When relying on FDSN web services for waveform data, it is strongly
183
+ recommended to prefetch all waveform windows before running the scan.
184
+ This downloads every required waveform once and stores it in a local
185
+ SQLite cache, avoiding repeated downloads and dramatically reducing
186
+ overall runtime for large catalogs:
187
+
188
+ requake wfcache prefetch
189
+
180
190
  Once done ([it will take time!](#performances)), you are ready to build
181
191
  repeating earthquake families:
182
192
 
@@ -193,6 +203,12 @@ repeating earthquake families:
193
203
  performance is typically dominated by waveform download latency rather than
194
204
  computation.
195
205
 
206
+ - For large FDSN-based runs, use `requake wfcache prefetch` before
207
+ `requake scan_catalog` to download all waveforms upfront into a local
208
+ SQLite cache. This eliminates repeated downloads, lets the scan read
209
+ exclusively from disk, and is the single most effective way to speed up
210
+ catalog scanning.
211
+
196
212
  - `requake build_families` is fast™.
197
213
 
198
214
  ## How to Cite
@@ -205,7 +221,7 @@ If you used Requake for a scientific paper, please cite it as:
205
221
  Please replace `X.Y` with the Requake version number you used.
206
222
 
207
223
  You can also cite the following abstract presented at the
208
- 2016 AGU Fall Meeting:
224
+ 2021 AGU Fall Meeting:
209
225
 
210
226
  > Satriano, C., Doucet, A. & Bouin, M.-P. (2021).
211
227
  > Probing the creep rate along the Lesser Antilles Subduction Zone through repeating earthquakes.
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2026-06-03T14:39:27+0200",
11
+ "date": "2026-06-19T10:48:17+0200",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "9071fd7343802f3ef642a16f6bb02ff5ce1d88f5",
15
- "version": "0.8.2"
14
+ "full-revisionid": "407e6c96ec3574f6e3743741763ca5b7c4daf914",
15
+ "version": "0.8.3"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -76,9 +76,19 @@ catalog_waveform_cache_size = integer(min=2, default=5000)
76
76
  ## Set to 0 to derive it automatically from catalog_waveform_cache_size.
77
77
  catalog_waveform_cache_size_parallel = integer(min=0, default=0)
78
78
  ## Keep a persistent copy of fetched event waveforms on disk to speed up
79
- ## reruns. This on-disk cache lives in OUTDIR/waveform_cache and is not
79
+ ## reruns. This on-disk cache lives in OUTDIR/waveform_cache.sqlite and is
80
80
  ## limited by catalog_waveform_cache_size.
81
81
  catalog_waveform_disk_cache_enabled = boolean(default=true)
82
+ ## Maximum number of retries for a failed waveform download before
83
+ ## suppressing new attempts for the same cache key.
84
+ catalog_waveform_cache_failure_max_retries = integer(min=0, default=3)
85
+ ## Base backoff in seconds for persistent waveform download failures.
86
+ ## The retry delay grows exponentially with each failure.
87
+ catalog_waveform_cache_failure_backoff_s = float(min=0, default=600)
88
+ ## When True, skip live downloads for events absent from both positive
89
+ ## and negative cache, trusting a prior prefetch run to have covered
90
+ ## all fetchable events. Requires disk cache to be enabled.
91
+ catalog_waveform_require_prefetch = boolean(default=false)
82
92
  ## Number of worker processes for catalog scan.
83
93
  ## 0 means automatic selection; 1 disables parallelism.
84
94
  catalog_scan_nprocs = integer(min=0, default=0)
@@ -498,6 +498,179 @@ def parse_arguments(progname='requake'):
498
498
  '"A" header field'
499
499
  )
500
500
  # ---
501
+ # --- wfcache
502
+ wfcache = subparser.add_parser(
503
+ 'wfcache',
504
+ help='manage persistent waveform cache'
505
+ )
506
+ wfcache_sub = wfcache.add_subparsers(dest='wfcache_action')
507
+ wfcache_sub.required = True
508
+
509
+ wfcache_row_filters = argparse.ArgumentParser(add_help=False)
510
+ wfcache_row_filters.add_argument(
511
+ '--event-id',
512
+ action='append',
513
+ default=[],
514
+ help='event ID filter (repeatable)'
515
+ )
516
+ wfcache_row_filters.add_argument(
517
+ '--event-id-file',
518
+ type=str,
519
+ default=None,
520
+ help='path to text file with event IDs'
521
+ )
522
+ wfcache_row_filters.add_argument(
523
+ '--trace-id',
524
+ action='append',
525
+ default=[],
526
+ help='trace-ID filter (repeatable)'
527
+ )
528
+ wfcache_row_filters.add_argument(
529
+ '--start-time',
530
+ type=str,
531
+ default=None,
532
+ help='minimum start time (UTCDateTime-compatible string)'
533
+ )
534
+ wfcache_row_filters.add_argument(
535
+ '--end-time',
536
+ type=str,
537
+ default=None,
538
+ help='maximum end time (UTCDateTime-compatible string)'
539
+ )
540
+ wfcache_row_filters.add_argument(
541
+ '--limit',
542
+ type=_nonnegative_int,
543
+ default=None,
544
+ help='limit number of returned rows'
545
+ )
546
+
547
+ wfcache_prefetch = wfcache_sub.add_parser(
548
+ 'prefetch',
549
+ help='prefetch waveform windows into the persistent cache'
550
+ )
551
+ wfcache_prefetch.add_argument(
552
+ '--event-id',
553
+ action='append',
554
+ default=[],
555
+ help='event ID filter (repeatable)'
556
+ )
557
+ wfcache_prefetch.add_argument(
558
+ '--event-id-file',
559
+ type=str,
560
+ default=None,
561
+ help='path to text file with event IDs'
562
+ )
563
+ wfcache_prefetch.add_argument(
564
+ '--trace-id',
565
+ action='append',
566
+ default=[],
567
+ help='trace-ID filter (repeatable)'
568
+ )
569
+ wfcache_prefetch.add_argument(
570
+ '--max-events',
571
+ type=_nonnegative_int,
572
+ default=None,
573
+ help='maximum number of catalog events to prefetch'
574
+ )
575
+ wfcache_prefetch.add_argument(
576
+ '--batch-size',
577
+ type=_nonnegative_int,
578
+ default=500,
579
+ help='progress logging batch size (default: %(default)s)'
580
+ )
581
+ wfcache_prefetch.add_argument(
582
+ '--group-window',
583
+ type=str,
584
+ default='1h',
585
+ metavar='DURATION',
586
+ help='max grouped download span (e.g., 30m, 1h; default: '
587
+ '%(default)s)'
588
+ )
589
+ wfcache_prefetch.set_defaults(action='wfcache_prefetch')
590
+
591
+ wfcache_print = wfcache_sub.add_parser(
592
+ 'print',
593
+ parents=[wfcache_row_filters],
594
+ help='print cached waveform rows'
595
+ )
596
+ wfcache_print.add_argument(
597
+ '--json',
598
+ action='store_true',
599
+ help='print rows as JSON'
600
+ )
601
+ wfcache_print.set_defaults(action='wfcache_print')
602
+
603
+ wfcache_inspect = wfcache_sub.add_parser(
604
+ 'inspect',
605
+ help='print persistent waveform-cache summary'
606
+ )
607
+ wfcache_inspect.add_argument(
608
+ '--integrity',
609
+ action='store_true',
610
+ help='run PRAGMA integrity_check'
611
+ )
612
+ wfcache_inspect.add_argument(
613
+ '--json',
614
+ action='store_true',
615
+ help='print summary as JSON'
616
+ )
617
+ wfcache_inspect.set_defaults(action='wfcache_inspect')
618
+
619
+ wfcache_extract = wfcache_sub.add_parser(
620
+ 'extract',
621
+ parents=[wfcache_row_filters],
622
+ help='extract cached waveforms to external files'
623
+ )
624
+ wfcache_extract.add_argument(
625
+ '--format',
626
+ type=str,
627
+ choices=['mseed', 'sac'],
628
+ default='mseed',
629
+ help='output format (default: %(default)s)'
630
+ )
631
+ wfcache_extract.add_argument(
632
+ '--output-dir',
633
+ type=str,
634
+ default='waveform_cache',
635
+ help='output directory (default: %(default)s)'
636
+ )
637
+ wfcache_extract.set_defaults(action='wfcache_extract')
638
+
639
+ wfcache_reset = wfcache_sub.add_parser(
640
+ 'reset-failures',
641
+ help='reset persistent waveform failure-cache rows'
642
+ )
643
+ wfcache_reset.add_argument(
644
+ '--event-id',
645
+ action='append',
646
+ default=[],
647
+ help='event ID to reset (repeatable)'
648
+ )
649
+ wfcache_reset.add_argument(
650
+ '--event-id-file',
651
+ type=str,
652
+ default=None,
653
+ help='path to text file with event IDs to reset'
654
+ )
655
+ wfcache_reset.add_argument(
656
+ '--all',
657
+ action='store_true',
658
+ help='reset all failure rows'
659
+ )
660
+ wfcache_reset.add_argument(
661
+ '--older-than',
662
+ type=str,
663
+ default=None,
664
+ metavar='DURATION',
665
+ help='reset rows older than DURATION (e.g., 6h, 7d)'
666
+ )
667
+ wfcache_reset.add_argument(
668
+ '--dry-run',
669
+ action='store_true',
670
+ help='show affected rows without deleting'
671
+ )
672
+ wfcache_reset.set_defaults(action='wfcache_reset_failures')
673
+ # ---
501
674
  argcomplete.autocomplete(parser)
502
675
  args = parser.parse_args()
503
676
  if args.action is None:
@@ -28,6 +28,12 @@ from .utils import (
28
28
  # Note: modules are lazily imported to speed up the startup time.
29
29
  # pylint: disable=relative-beyond-top-level,import-outside-toplevel
30
30
 
31
+
32
+ def _memory_log_filter(record):
33
+ """Suppress [MEM] messages from TTY console handlers."""
34
+ return '[MEM]' not in record.getMessage()
35
+
36
+
31
37
  logger = None # pylint: disable=invalid-name
32
38
  PYTHON_VERSION_STR = None
33
39
  NUMPY_VERSION_STR = None
@@ -112,6 +118,7 @@ def _setup_tqdm_logging(logger_root):
112
118
  self.handleError(record)
113
119
  console = TqdmLoggingHandler()
114
120
  console.setLevel(logging.INFO)
121
+ console.addFilter(_memory_log_filter)
115
122
  # Add logger color coding on all platforms but win32
116
123
  if sys.platform != 'win32' and sys.stdout.isatty():
117
124
  console.emit = _color_handler_emit(console.emit)
@@ -172,23 +179,13 @@ def _connect_station_dataselect():
172
179
  Connect to station and dataselect services.
173
180
 
174
181
  Those can be either FDSN web services or local files.
182
+ Both FDSN clients are created lazily on first use to avoid
183
+ unnecessary network I/O when data comes from local sources.
175
184
  """
176
- from obspy.clients.fdsn import Client as FDSNClient
177
- if config.station_metadata_path is None:
178
- config.station_client = FDSNClient(config.fdsn_station_url)
179
- logger.info(
180
- f'Connected to FDSN station server: {config.fdsn_station_url}'
181
- )
185
+ config.station_client = None
182
186
  config.dataselect_client = None
183
187
  if config.sds_data_path is not None:
184
188
  _connect_sds()
185
- if config.event_data_path is not None:
186
- return
187
- config.dataselect_client = FDSNClient(config.fdsn_dataselect_url)
188
- logger.info(
189
- 'Connected to FDSN dataselect server: '
190
- f'{config.fdsn_dataselect_url}'
191
- )
192
189
 
193
190
 
194
191
  def _connect_sds():
@@ -267,6 +264,7 @@ def configure(args):
267
264
  # update config with the contents of config_obj
268
265
  config.update(config_obj)
269
266
  config.args = args
267
+ config.args.outdir = os.path.abspath(config.args.outdir)
270
268
  config.scan_catalog_pairs_file = os.path.join(
271
269
  config.args.outdir, 'requake.event_pairs.csv'
272
270
  )
@@ -293,14 +291,14 @@ def configure(args):
293
291
  # save config to output dir (only for actions that write to outdir)
294
292
  actions_writing_to_outdir = (
295
293
  'read_catalog', 'scan_catalog', 'build_families',
296
- 'build_templates', 'scan_templates'
294
+ 'build_templates', 'scan_templates', 'wfcache_prefetch'
297
295
  )
298
296
  if args.action in actions_writing_to_outdir:
299
297
  shutil.copy(args.configfile, args.outdir)
300
298
  _parse_catalog_options()
301
299
  actions_needing_fdsn_station_dataselect = (
302
300
  'scan_catalog', 'plot_pair', 'plot_families', 'build_templates',
303
- 'scan_templates'
301
+ 'scan_templates', 'wfcache_prefetch'
304
302
  )
305
303
  try:
306
304
  if args.action in actions_needing_fdsn_station_dataselect:
@@ -13,12 +13,43 @@ import os
13
13
  import sys
14
14
  import locale
15
15
  import shutil
16
+ import hashlib
16
17
  from datetime import datetime
17
18
  from .configobj import ConfigObj, ParseError
18
19
  from .configobj.validate import Validator
19
20
  locale.setlocale(locale.LC_ALL, '')
20
21
 
21
22
 
23
+ _seen_msgs = set()
24
+
25
+
26
+ def log_once(logger, level, msg, *args, **kwargs):
27
+ """
28
+ Log a message only once.
29
+
30
+ :param logger: Logger object.
31
+ :type logger: logging.Logger
32
+ :param level: Log level.
33
+ :type level: str
34
+ :param msg: Message to log.
35
+ :type msg: str
36
+ :param args: Arguments for the message.
37
+ :param kwargs: Keyword arguments for the message.
38
+ """
39
+ text = str(msg)
40
+ digest = hashlib.blake2b(
41
+ text.encode('utf-8'),
42
+ digest_size=8,
43
+ ).digest()
44
+ if digest in _seen_msgs:
45
+ return
46
+ _seen_msgs.add(digest)
47
+ log_fn = getattr(logger, level, None)
48
+ if log_fn is None:
49
+ raise ValueError(f'Invalid log level: {level}')
50
+ log_fn(text, *args, **kwargs)
51
+
52
+
22
53
  def err_exit(msg):
23
54
  """
24
55
  Exit with error message.
@@ -95,8 +95,14 @@ def write_families(families):
95
95
  conn.close()
96
96
 
97
97
 
98
- def read_families():
99
- """Read catalog-scan families from SQLite."""
98
+ def read_families(family_numbers=None):
99
+ """Read catalog-scan families from SQLite.
100
+
101
+ :param family_numbers: If given, only read families with these numbers.
102
+ :type family_numbers: iterable of int or None
103
+ :return: List of families.
104
+ :rtype: list of Family
105
+ """
100
106
  from ..catalog import RequakeEvent
101
107
  from ..families.families import Family
102
108
 
@@ -104,12 +110,24 @@ def read_families():
104
110
  try:
105
111
  cursor = conn.cursor()
106
112
  try:
107
- rows = cursor.execute(
108
- f'''
109
- SELECT * FROM {FAMILIES_TABLE}
110
- ORDER BY family_number, orig_time, evid, trace_id
111
- '''
112
- ).fetchall()
113
+ if family_numbers is not None:
114
+ family_numbers = sorted(set(family_numbers))
115
+ placeholders = ','.join('?' * len(family_numbers))
116
+ rows = cursor.execute(
117
+ f'''
118
+ SELECT * FROM {FAMILIES_TABLE}
119
+ WHERE family_number IN ({placeholders})
120
+ ORDER BY family_number, orig_time, evid, trace_id
121
+ ''',
122
+ family_numbers,
123
+ ).fetchall()
124
+ else:
125
+ rows = cursor.execute(
126
+ f'''
127
+ SELECT * FROM {FAMILIES_TABLE}
128
+ ORDER BY family_number, orig_time, evid, trace_id
129
+ '''
130
+ ).fetchall()
113
131
  except sqlite3.OperationalError as err:
114
132
  if MISSING_FAMILIES_TABLE in str(err):
115
133
  raise FileNotFoundError(
@@ -315,13 +315,19 @@ def write_pair_records(pairs, append=True):
315
315
 
316
316
 
317
317
  def count_pairs():
318
- """Return the number of stored event pairs."""
318
+ """Return the number of stored event pairs.
319
+
320
+ Uses ``SELECT 1 ... LIMIT 1`` instead of ``COUNT(*)`` because
321
+ the latter performs a full table scan, which is prohibitively
322
+ slow (20+ minutes for 300M rows) on network filesystems such
323
+ as GPFS.
324
+ """
319
325
  conn = get_db_connection(initdb=False)
320
326
  try:
321
327
  cursor = conn.cursor()
322
328
  try:
323
329
  row = cursor.execute(
324
- f'SELECT COUNT(*) AS npairs FROM {EVENT_PAIRS_TABLE}'
330
+ f'SELECT 1 FROM {EVENT_PAIRS_TABLE} LIMIT 1'
325
331
  ).fetchone()
326
332
  except sqlite3.OperationalError as err:
327
333
  if _is_missing_pairs_table_error(err):
@@ -329,7 +335,7 @@ def count_pairs():
329
335
  raise
330
336
  finally:
331
337
  conn.close()
332
- return int(row['npairs'])
338
+ return 1 if row is not None else 0
333
339
 
334
340
 
335
341
  def read_event_key_rows():
@@ -343,7 +349,7 @@ def read_event_key_rows():
343
349
  SELECT event_id, evid
344
350
  FROM {EVENT_KEYS_TABLE}
345
351
  '''
346
- ).fetchall()
352
+ )
347
353
  except sqlite3.OperationalError as err:
348
354
  if _is_missing_pairs_table_error(err):
349
355
  return []
@@ -358,18 +364,25 @@ def read_event_key_rows():
358
364
  return [(int(row['event_id']), row['evid']) for row in rows]
359
365
 
360
366
 
361
- def read_pair_key_ids():
362
- """Read stored event-pair foreign keys as integer ID tuples."""
367
+ def read_packed_pair_ids(event_id_to_idx, nevents):
368
+ """Stream pair keys from DB and return packed ``uint64`` IDs.
369
+
370
+ Reads ``(event1_id, event2_id)`` rows from the database and
371
+ packs them into a set of canonical ``uint64`` values in a
372
+ single pass, avoiding the intermediate tuple set.
373
+
374
+ :param event_id_to_idx: mapping from ``event_id`` to catalog index
375
+ :param nevents: number of events in the catalog
376
+ :return: set of packed IDs, each computed as
377
+ ``min(idx1, idx2) * nevents + max(idx1, idx2)``
378
+ """
363
379
  conn = get_db_connection(initdb=False)
364
380
  try:
365
381
  cursor = conn.cursor()
366
382
  try:
367
383
  rows = cursor.execute(
368
- f'''
369
- SELECT DISTINCT event1_id, event2_id
370
- FROM {EVENT_PAIRS_TABLE}
371
- '''
372
- ).fetchall()
384
+ f'SELECT event1_id, event2_id FROM {EVENT_PAIRS_TABLE}'
385
+ )
373
386
  except sqlite3.OperationalError as err:
374
387
  if _is_missing_pairs_table_error(err):
375
388
  return set()
@@ -379,12 +392,21 @@ def read_pair_key_ids():
379
392
  f'Requake version in db file {get_db_path()}'
380
393
  ) from err
381
394
  raise
395
+ get_idx = event_id_to_idx.get
396
+ result = set()
397
+ add_id = result.add
398
+ for event1_id, event2_id in rows:
399
+ idx1 = get_idx(int(event1_id))
400
+ idx2 = get_idx(int(event2_id))
401
+ if idx1 is None or idx2 is None:
402
+ continue
403
+ if idx1 < idx2:
404
+ add_id(idx1 * nevents + idx2)
405
+ else:
406
+ add_id(idx2 * nevents + idx1)
407
+ return result
382
408
  finally:
383
409
  conn.close()
384
- return {
385
- (int(row['event1_id']), int(row['event2_id']))
386
- for row in rows
387
- }
388
410
 
389
411
 
390
412
  def read_pairs(cc_min=None, cc_max=None):
@@ -151,15 +151,17 @@ class Family(list):
151
151
  return distance / 1e3
152
152
 
153
153
 
154
- def _read_families_from_catalog_scan():
154
+ def _read_families_from_catalog_scan(family_numbers=None):
155
155
  """
156
156
  Read a list of families from the catalog scan output.
157
157
 
158
+ :param family_numbers: If given, only read families with these numbers.
159
+ :type family_numbers: iterable of int or None
158
160
  :return: List of families.
159
161
  :rtype: list of Family
160
162
  """
161
163
  from ..database.families import read_families as read_families_from_db
162
- return read_families_from_db()
164
+ return read_families_from_db(family_numbers=family_numbers)
163
165
 
164
166
 
165
167
  def _read_families_from_template_scan():
@@ -175,16 +177,18 @@ def _read_families_from_template_scan():
175
177
  return read_template_families_from_db()
176
178
 
177
179
 
178
- def read_families():
180
+ def read_families(family_numbers=None):
179
181
  """
180
182
  Read families from the catalog scan output or template scan output.
181
183
 
184
+ :param family_numbers: If given, only read families with these numbers.
185
+ :type family_numbers: iterable of int or None
182
186
  :return: List of families.
183
187
  :rtype: list of Family
184
188
  """
185
189
  if getattr(config.args, 'template', False):
186
190
  return _read_families_from_template_scan()
187
- return _read_families_from_catalog_scan()
191
+ return _read_families_from_catalog_scan(family_numbers=family_numbers)
188
192
 
189
193
 
190
194
  def read_selected_families():
@@ -197,7 +201,7 @@ def read_selected_families():
197
201
  :raises FamilyNotFoundError: if no family is found
198
202
  """
199
203
  family_numbers = _build_family_number_list()
200
- families = read_families()
204
+ families = read_families(family_numbers=family_numbers)
201
205
  families_selected = []
202
206
  for family in families:
203
207
  if family.number not in family_numbers: