requake 0.8__tar.gz → 0.8.2__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.
- {requake-0.8 → requake-0.8.2}/CHANGELOG.md +38 -0
- {requake-0.8 → requake-0.8.2}/PKG-INFO +10 -7
- {requake-0.8 → requake-0.8.2}/README.md +8 -5
- {requake-0.8 → requake-0.8.2}/requake/__init__.py +9 -0
- {requake-0.8 → requake-0.8.2}/requake/_version.py +3 -3
- {requake-0.8 → requake-0.8.2}/requake/catalog/catalog.py +2 -3
- {requake-0.8 → requake-0.8.2}/requake/catalog/read_catalog.py +5 -5
- {requake-0.8 → requake-0.8.2}/requake/config/__init__.py +1 -1
- {requake-0.8 → requake-0.8.2}/requake/config/configspec.conf +14 -0
- {requake-0.8 → requake-0.8.2}/requake/config/parse_arguments.py +42 -3
- {requake-0.8 → requake-0.8.2}/requake/config/rq_setup.py +47 -4
- {requake-0.8 → requake-0.8.2}/requake/database/catalog.py +5 -5
- {requake-0.8 → requake-0.8.2}/requake/database/db.py +12 -7
- {requake-0.8 → requake-0.8.2}/requake/database/families.py +8 -8
- requake-0.8.2/requake/database/pairs.py +481 -0
- {requake-0.8 → requake-0.8.2}/requake/database/templates.py +9 -9
- requake-0.8.2/requake/database/trace_metadata.py +343 -0
- {requake-0.8 → requake-0.8.2}/requake/families/build_families.py +26 -15
- {requake-0.8 → requake-0.8.2}/requake/families/families.py +2 -2
- {requake-0.8 → requake-0.8.2}/requake/families/flag_family.py +1 -1
- {requake-0.8 → requake-0.8.2}/requake/families/pairs.py +8 -17
- requake-0.8.2/requake/families/print_families.py +247 -0
- {requake-0.8 → requake-0.8.2}/requake/families/print_pairs.py +17 -7
- requake-0.8.2/requake/scan/scan_catalog.py +220 -0
- requake-0.8.2/requake/scan/scan_catalog_helpers.py +317 -0
- requake-0.8.2/requake/scan/scan_catalog_pairs.py +182 -0
- requake-0.8.2/requake/scan/scan_catalog_workers.py +561 -0
- {requake-0.8 → requake-0.8.2}/requake/scan/scan_templates.py +3 -3
- {requake-0.8 → requake-0.8.2}/requake/waveforms/waveform_pair.py +127 -37
- {requake-0.8 → requake-0.8.2}/requake/waveforms/waveforms.py +141 -12
- {requake-0.8 → requake-0.8.2}/requake.egg-info/PKG-INFO +10 -7
- {requake-0.8 → requake-0.8.2}/requake.egg-info/SOURCES.txt +4 -0
- requake-0.8/requake/database/pairs.py +0 -180
- requake-0.8/requake/families/print_families.py +0 -118
- requake-0.8/requake/scan/scan_catalog.py +0 -132
- {requake-0.8 → requake-0.8.2}/CONTRIBUTORS.txt +0 -0
- {requake-0.8 → requake-0.8.2}/LICENSE.txt +0 -0
- {requake-0.8 → requake-0.8.2}/MANIFEST.in +0 -0
- {requake-0.8 → requake-0.8.2}/pyproject.toml +0 -0
- {requake-0.8 → requake-0.8.2}/requake/catalog/__init__.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/catalog/print_catalog.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/catalog/read_catalog_from_csv.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/catalog/read_catalog_from_fdsnws.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/catalog/read_catalog_from_quakeml.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/config/config.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/config/configobj/LICENSE +0 -0
- {requake-0.8 → requake-0.8.2}/requake/config/configobj/__init__.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/config/configobj/_version.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/config/configobj/validate.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/config/generic_printer.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/config/utils.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/database/__init__.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/families/__init__.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/families/build_templates.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/formulas/__init__.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/formulas/conversion.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/formulas/moment.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/formulas/slip.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/main.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/plot/__init__.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/plot/cached_tiler.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/plot/colormaps.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/plot/map_families.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/plot/map_tiles.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/plot/plot_cumulative.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/plot/plot_families.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/plot/plot_pair.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/plot/plot_timespans.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/plot/plot_utils.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/scan/__init__.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/waveforms/__init__.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/waveforms/arrivals.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake/waveforms/station_metadata.py +0 -0
- {requake-0.8 → requake-0.8.2}/requake.egg-info/dependency_links.txt +0 -0
- {requake-0.8 → requake-0.8.2}/requake.egg-info/entry_points.txt +0 -0
- {requake-0.8 → requake-0.8.2}/requake.egg-info/requires.txt +0 -0
- {requake-0.8 → requake-0.8.2}/requake.egg-info/top_level.txt +0 -0
- {requake-0.8 → requake-0.8.2}/setup.cfg +0 -0
- {requake-0.8 → requake-0.8.2}/setup.py +0 -0
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2021-2026 Claudio Satriano <satriano@ipgp.fr>
|
|
4
4
|
|
|
5
|
+
## [0.8.2] - 2026-06-03
|
|
6
|
+
|
|
7
|
+
Note: you might want to run `requake update_config` to update your config file
|
|
8
|
+
to the latest version.
|
|
9
|
+
|
|
10
|
+
- Parallel processing of earthquake pairs in `scan_catalog`. On by default
|
|
11
|
+
with automatic detection of the number of worker processes. This can be
|
|
12
|
+
configured with the `catalog_scan_nprocs` config option.
|
|
13
|
+
|
|
14
|
+
## [0.8.1] - 2026-05-31
|
|
15
|
+
|
|
16
|
+
- Slimmed-down DB size by optimizing `event_pairs` storage. This allows storing
|
|
17
|
+
also invalid pairs, so that resuming catalog scan is faster.
|
|
18
|
+
- Dramatic speedup of event pairs building by using a KD-tree
|
|
19
|
+
spatial index and approximate geodetic distance calculations.
|
|
20
|
+
- Dramatic speedup of waveform fetching by improving the caching mechanism.
|
|
21
|
+
A new config option `catalog_waveform_cache_size` allows to set the maximum
|
|
22
|
+
number of waveforms to cache.
|
|
23
|
+
- Dramatically faster reading of event pairs with cross-correlation
|
|
24
|
+
filters in large catalogs, thanks to the new SQLite-backed storage.
|
|
25
|
+
- Add an optional disk cache, through the `catalog_waveform_disk_cache_enabled`
|
|
26
|
+
config option.
|
|
27
|
+
- `scan_catalog`: when running as a batch job, log a progress
|
|
28
|
+
report every minute, instead of showing the progress bar.
|
|
29
|
+
- `scan_catalog`: if event pairs already exist in the database,
|
|
30
|
+
ask whether to overwrite or continue from an interrupted run.
|
|
31
|
+
Add `--force` to restart from scratch and `--force-continue`
|
|
32
|
+
(short `-c`) to resume without prompting.
|
|
33
|
+
- `print_families`: add horizontal and vertical distance min/max values
|
|
34
|
+
to both summary and detailed (`-d`) outputs.
|
|
35
|
+
- `print_families`: add `-f stats` output format for summary family
|
|
36
|
+
statistics.
|
|
37
|
+
|
|
5
38
|
## [0.8] - 2026-05-22
|
|
6
39
|
|
|
7
40
|
This release introduces a major storage change.
|
|
@@ -13,6 +46,9 @@ data access performance, especially for large catalogs.
|
|
|
13
46
|
This change is not backward compatible at the data format level. The
|
|
14
47
|
high-level behavior of the command-line interface remains unchanged.
|
|
15
48
|
|
|
49
|
+
Note: further improvements based on this new storage system will be introduced
|
|
50
|
+
throughout the 0.8.x release series.
|
|
51
|
+
|
|
16
52
|
## [0.7.3] - 2026-04-14
|
|
17
53
|
|
|
18
54
|
This release is intended to validate the new release workflow.
|
|
@@ -151,6 +187,8 @@ Mostly a bugfix release with a slightly improved trace plotting.
|
|
|
151
187
|
|
|
152
188
|
- Initial release, not yet feature complete
|
|
153
189
|
|
|
190
|
+
[0.8.2]: https://github.com/SeismicSource/requake/compare/v0.8.1...v0.8.2
|
|
191
|
+
[0.8.1]: https://github.com/SeismicSource/requake/compare/v0.8...v0.8.1
|
|
154
192
|
[0.8]: https://github.com/SeismicSource/requake/compare/v0.7.3...v0.8
|
|
155
193
|
[0.7.3]: https://github.com/SeismicSource/requake/compare/v0.7.2...v0.7.3
|
|
156
194
|
[0.7.2]: https://github.com/SeismicSource/requake/compare/v0.7.1...v0.7.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: requake
|
|
3
|
-
Version: 0.8
|
|
3
|
+
Version: 0.8.2
|
|
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@
|
|
40
|
+
<img src="https://cdn.jsdelivr.net/gh/SeismicSource/requake@9071fd7343802f3ef642a16f6bb02ff5ce1d88f5/imgs/Requake_logo.svg" width="400">
|
|
41
41
|
|
|
42
42
|
# Requake
|
|
43
43
|
|
|
@@ -223,11 +223,14 @@ repeating earthquake families:
|
|
|
223
223
|
|
|
224
224
|
## Performances
|
|
225
225
|
|
|
226
|
-
- `requake scan_catalog`
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
- `requake scan_catalog` is designed to fully utilize available CPU cores by
|
|
227
|
+
processing earthquake pairs in parallel. On an M2 MacBook Air, scanning
|
|
228
|
+
95,034 earthquake pairs downloaded via FDSNWS took approximately 5 minutes
|
|
229
|
+
using 7 worker processes, yielding ~19,000 pairs per minute. When repeating
|
|
230
|
+
the same scan with a fully cached set of waveforms, runtime dropped to 84
|
|
231
|
+
seconds (~68,000 pairs per minute). These results indicate that overall
|
|
232
|
+
performance is typically dominated by waveform download latency rather than
|
|
233
|
+
computation.
|
|
231
234
|
|
|
232
235
|
- `requake build_families` is fast™.
|
|
233
236
|
|
|
@@ -184,11 +184,14 @@ repeating earthquake families:
|
|
|
184
184
|
|
|
185
185
|
## Performances
|
|
186
186
|
|
|
187
|
-
- `requake scan_catalog`
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
- `requake scan_catalog` is designed to fully utilize available CPU cores by
|
|
188
|
+
processing earthquake pairs in parallel. On an M2 MacBook Air, scanning
|
|
189
|
+
95,034 earthquake pairs downloaded via FDSNWS took approximately 5 minutes
|
|
190
|
+
using 7 worker processes, yielding ~19,000 pairs per minute. When repeating
|
|
191
|
+
the same scan with a fully cached set of waveforms, runtime dropped to 84
|
|
192
|
+
seconds (~68,000 pairs per minute). These results indicate that overall
|
|
193
|
+
performance is typically dominated by waveform download latency rather than
|
|
194
|
+
computation.
|
|
192
195
|
|
|
193
196
|
- `requake build_families` is fast™.
|
|
194
197
|
|
|
@@ -9,5 +9,14 @@ Initialize requake package.
|
|
|
9
9
|
GNU General Public License v3.0 or later
|
|
10
10
|
(https://www.gnu.org/licenses/gpl-3.0-standalone.html)
|
|
11
11
|
"""
|
|
12
|
+
import multiprocessing
|
|
13
|
+
import signal
|
|
14
|
+
|
|
15
|
+
# Child processes spawned for parallel scans may import requake before worker
|
|
16
|
+
# initializers run. Ignore SIGINT early in children so Ctrl+C is managed by
|
|
17
|
+
# the parent process only.
|
|
18
|
+
if multiprocessing.current_process().name != 'MainProcess':
|
|
19
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
20
|
+
|
|
12
21
|
from . import _version
|
|
13
22
|
__version__ = _version.get_versions()['version']
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2026-
|
|
11
|
+
"date": "2026-06-03T14:39:27+0200",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.8"
|
|
14
|
+
"full-revisionid": "9071fd7343802f3ef642a16f6bb02ff5ce1d88f5",
|
|
15
|
+
"version": "0.8.2"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -11,7 +11,6 @@ Classes and functions for downloading, reading and writing catalogs.
|
|
|
11
11
|
"""
|
|
12
12
|
import numpy as np
|
|
13
13
|
from obspy import UTCDateTime
|
|
14
|
-
from ..config import config
|
|
15
14
|
from ..formulas import float_or_none
|
|
16
15
|
from ..waveforms import get_traceid_coords
|
|
17
16
|
|
|
@@ -230,7 +229,7 @@ def read_stored_catalog():
|
|
|
230
229
|
"""
|
|
231
230
|
from ..database.catalog import read_catalog as read_catalog_from_db
|
|
232
231
|
|
|
233
|
-
cat = read_catalog_from_db(
|
|
232
|
+
cat = read_catalog_from_db()
|
|
234
233
|
cat.sort()
|
|
235
234
|
if not cat:
|
|
236
235
|
raise ValueError('Empty catalog')
|
|
@@ -246,7 +245,7 @@ def fix_non_locatable_events(catalog):
|
|
|
246
245
|
"""
|
|
247
246
|
if not any(ev.lat is None or ev.lon is None for ev in catalog):
|
|
248
247
|
return
|
|
249
|
-
traceid_coords = get_traceid_coords(
|
|
248
|
+
traceid_coords = get_traceid_coords()
|
|
250
249
|
mean_lat = np.mean([
|
|
251
250
|
coords['latitude'] for coords in traceid_coords.values()])
|
|
252
251
|
mean_lon = np.mean([
|
|
@@ -117,8 +117,8 @@ def read_catalog():
|
|
|
117
117
|
catalog = read_stored_catalog()
|
|
118
118
|
nevs_read = len(catalog)
|
|
119
119
|
logger.info(
|
|
120
|
-
f'{nevs_read} events read from db file '
|
|
121
|
-
f'{get_db_path(
|
|
120
|
+
f'{nevs_read:n} events read from db file '
|
|
121
|
+
f'{get_db_path()}'
|
|
122
122
|
)
|
|
123
123
|
logger.info('Reading catalog...')
|
|
124
124
|
input_cat_file = config.args.catalog_file
|
|
@@ -150,9 +150,9 @@ def read_catalog():
|
|
|
150
150
|
# Sort catalog in increasing time order
|
|
151
151
|
catalog.sort()
|
|
152
152
|
# Write catalog to the output database
|
|
153
|
-
write_stored_catalog(catalog
|
|
153
|
+
write_stored_catalog(catalog)
|
|
154
154
|
nevs_written = len(catalog) - nevs_read
|
|
155
155
|
logger.info(
|
|
156
|
-
f'{nevs_written} events written to db file '
|
|
157
|
-
f'{get_db_path(
|
|
156
|
+
f'{nevs_written:n} events written to db file '
|
|
157
|
+
f'{get_db_path()}'
|
|
158
158
|
)
|
|
@@ -9,7 +9,7 @@ Configuration and initialization of the Requake package.
|
|
|
9
9
|
GNU General Public License v3.0 or later
|
|
10
10
|
(https://www.gnu.org/licenses/gpl-3.0-standalone.html)
|
|
11
11
|
"""
|
|
12
|
-
from .rq_setup import rq_exit # noqa
|
|
12
|
+
from .rq_setup import rq_exit, wait_for_sigint_pause # noqa
|
|
13
13
|
from .generic_printer import generic_printer # noqa
|
|
14
14
|
# The config object is created in config.py and needs to be populated
|
|
15
15
|
# when using requake from command line, this is done by the configure()
|
|
@@ -68,6 +68,20 @@ catalog_search_range = float(default=30)
|
|
|
68
68
|
## If more than one trace_id is specified (separated by commas), the closest
|
|
69
69
|
## station will be used for a given event pair.
|
|
70
70
|
catalog_trace_id = force_list(default=NET.STA.LOC.CHAN)
|
|
71
|
+
## Maximum number of event waveforms kept in memory cache during
|
|
72
|
+
## catalog-based scan. Higher values increase memory usage and can reduce
|
|
73
|
+
## repeated waveform downloads.
|
|
74
|
+
catalog_waveform_cache_size = integer(min=2, default=5000)
|
|
75
|
+
## Per-worker waveform cache size for parallel catalog scan.
|
|
76
|
+
## Set to 0 to derive it automatically from catalog_waveform_cache_size.
|
|
77
|
+
catalog_waveform_cache_size_parallel = integer(min=0, default=0)
|
|
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
|
|
80
|
+
## limited by catalog_waveform_cache_size.
|
|
81
|
+
catalog_waveform_disk_cache_enabled = boolean(default=true)
|
|
82
|
+
## Number of worker processes for catalog scan.
|
|
83
|
+
## 0 means automatic selection; 1 disables parallelism.
|
|
84
|
+
catalog_scan_nprocs = integer(min=0, default=0)
|
|
71
85
|
|
|
72
86
|
#### Template-based scan
|
|
73
87
|
### The following parameters are for template-based scan:
|
|
@@ -49,6 +49,21 @@ class SubcommandHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|
|
49
49
|
return parts
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def _nonnegative_int(value):
|
|
53
|
+
"""Argparse type for integers >= 0."""
|
|
54
|
+
try:
|
|
55
|
+
int_value = int(value)
|
|
56
|
+
except ValueError as err:
|
|
57
|
+
raise argparse.ArgumentTypeError(
|
|
58
|
+
f'invalid non-negative integer value: {value}'
|
|
59
|
+
) from err
|
|
60
|
+
if int_value < 0:
|
|
61
|
+
raise argparse.ArgumentTypeError(
|
|
62
|
+
f'invalid non-negative integer value: {value}'
|
|
63
|
+
)
|
|
64
|
+
return int_value
|
|
65
|
+
|
|
66
|
+
|
|
52
67
|
def parse_arguments(progname='requake'):
|
|
53
68
|
"""
|
|
54
69
|
Parse command line arguments.
|
|
@@ -160,11 +175,30 @@ def parse_arguments(progname='requake'):
|
|
|
160
175
|
)
|
|
161
176
|
# ---
|
|
162
177
|
# --- scan_catalog
|
|
163
|
-
subparser.add_parser(
|
|
178
|
+
scan_catalog = subparser.add_parser(
|
|
164
179
|
'scan_catalog',
|
|
165
|
-
parents=[force],
|
|
166
180
|
help='scan an existing catalog for earthquake pairs'
|
|
167
181
|
)
|
|
182
|
+
scan_catalog_mode = scan_catalog.add_mutually_exclusive_group()
|
|
183
|
+
scan_catalog_mode.add_argument(
|
|
184
|
+
'-f',
|
|
185
|
+
'--force',
|
|
186
|
+
action='store_true',
|
|
187
|
+
help='delete existing event pairs and restart the scan'
|
|
188
|
+
)
|
|
189
|
+
scan_catalog_mode.add_argument(
|
|
190
|
+
'-c',
|
|
191
|
+
'--force-continue',
|
|
192
|
+
action='store_true',
|
|
193
|
+
help='continue an interrupted scan and keep existing event pairs'
|
|
194
|
+
)
|
|
195
|
+
scan_catalog.add_argument(
|
|
196
|
+
'--nprocs',
|
|
197
|
+
type=_nonnegative_int,
|
|
198
|
+
default=None,
|
|
199
|
+
help='number of worker processes for scan_catalog '
|
|
200
|
+
'(0: auto, 1: disable parallelism)'
|
|
201
|
+
)
|
|
168
202
|
# ---
|
|
169
203
|
# --- print_pairs
|
|
170
204
|
print_pairs = subparser.add_parser(
|
|
@@ -324,10 +358,15 @@ def parse_arguments(progname='requake'):
|
|
|
324
358
|
print_families = subparser.add_parser(
|
|
325
359
|
'print_families',
|
|
326
360
|
parents=[
|
|
327
|
-
longerthan, shorterthan, minevents, family_numbers
|
|
361
|
+
longerthan, shorterthan, minevents, family_numbers
|
|
328
362
|
],
|
|
329
363
|
help='print families to screen'
|
|
330
364
|
)
|
|
365
|
+
print_families.add_argument(
|
|
366
|
+
'-f', '--format', type=str, default='simple',
|
|
367
|
+
choices=['simple', 'markdown', 'csv', 'stats'],
|
|
368
|
+
help='format for the output (default: %(default)s)'
|
|
369
|
+
)
|
|
331
370
|
print_families.add_argument(
|
|
332
371
|
'-d', '--detailed', action='store_true',
|
|
333
372
|
help='print detailed information for each family, including a list '
|
|
@@ -10,11 +10,13 @@ Setup functions for Requake.
|
|
|
10
10
|
(https://www.gnu.org/licenses/gpl-3.0-standalone.html)
|
|
11
11
|
"""
|
|
12
12
|
import contextlib
|
|
13
|
+
import multiprocessing
|
|
13
14
|
import sys
|
|
14
15
|
import os
|
|
15
16
|
import shutil
|
|
16
17
|
import logging
|
|
17
18
|
import signal
|
|
19
|
+
import time
|
|
18
20
|
import tqdm
|
|
19
21
|
from .._version import get_versions
|
|
20
22
|
from ..database.db import get_db_path
|
|
@@ -31,6 +33,10 @@ PYTHON_VERSION_STR = None
|
|
|
31
33
|
NUMPY_VERSION_STR = None
|
|
32
34
|
SCIPY_VERSION_STR = None
|
|
33
35
|
OBSPY_VERSION_STR = None
|
|
36
|
+
SIGINT_CONFIRM_WINDOW_S = 2.0
|
|
37
|
+
SIGINT_PAUSE_POLL_S = 0.05
|
|
38
|
+
_LAST_SIGINT_TS = None
|
|
39
|
+
_SIGINT_PAUSE_UNTIL_TS = None
|
|
34
40
|
|
|
35
41
|
|
|
36
42
|
def _check_library_versions():
|
|
@@ -273,7 +279,7 @@ def configure(args):
|
|
|
273
279
|
should_write_catalog = (
|
|
274
280
|
args.action == 'read_catalog'
|
|
275
281
|
and not args.append
|
|
276
|
-
and not write_ok(get_db_path(
|
|
282
|
+
and not write_ok(get_db_path(), args.force)
|
|
277
283
|
)
|
|
278
284
|
if should_write_catalog:
|
|
279
285
|
print('Exiting now.')
|
|
@@ -330,8 +336,45 @@ def rq_exit(retval=0, abort=False, progname='requake'):
|
|
|
330
336
|
|
|
331
337
|
|
|
332
338
|
def sigint_handler(_sig, _frame):
|
|
333
|
-
"""
|
|
334
|
-
|
|
339
|
+
"""Ask for confirmation on first Ctrl+C and abort on second."""
|
|
340
|
+
global _LAST_SIGINT_TS # pylint: disable=global-statement
|
|
341
|
+
global _SIGINT_PAUSE_UNTIL_TS # pylint: disable=global-statement
|
|
335
342
|
|
|
343
|
+
if multiprocessing.current_process().name != 'MainProcess':
|
|
344
|
+
signal.default_int_handler(_sig, _frame)
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
now = time.monotonic()
|
|
348
|
+
if (
|
|
349
|
+
_LAST_SIGINT_TS is not None
|
|
350
|
+
and now - _LAST_SIGINT_TS <= SIGINT_CONFIRM_WINDOW_S
|
|
351
|
+
):
|
|
352
|
+
rq_exit(1, abort=True)
|
|
353
|
+
_LAST_SIGINT_TS = now
|
|
354
|
+
_SIGINT_PAUSE_UNTIL_TS = now + SIGINT_CONFIRM_WINDOW_S
|
|
355
|
+
print(
|
|
356
|
+
'\nCtrl+C pressed. '
|
|
357
|
+
f'Press Ctrl+C again within {SIGINT_CONFIRM_WINDOW_S:g}s '
|
|
358
|
+
'to abort.'
|
|
359
|
+
)
|
|
336
360
|
|
|
337
|
-
|
|
361
|
+
|
|
362
|
+
def wait_for_sigint_pause():
|
|
363
|
+
"""Block while the first Ctrl+C confirmation window is active."""
|
|
364
|
+
global _SIGINT_PAUSE_UNTIL_TS # pylint: disable=global-statement
|
|
365
|
+
|
|
366
|
+
if multiprocessing.current_process().name != 'MainProcess':
|
|
367
|
+
return
|
|
368
|
+
while _SIGINT_PAUSE_UNTIL_TS is not None:
|
|
369
|
+
if time.monotonic() >= _SIGINT_PAUSE_UNTIL_TS:
|
|
370
|
+
_SIGINT_PAUSE_UNTIL_TS = None
|
|
371
|
+
break
|
|
372
|
+
time.sleep(SIGINT_PAUSE_POLL_S)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if multiprocessing.current_process().name == 'MainProcess':
|
|
376
|
+
signal.signal(signal.SIGINT, sigint_handler)
|
|
377
|
+
else:
|
|
378
|
+
# Workers must ignore SIGINT from startup to avoid pool breakage during
|
|
379
|
+
# spawn, before the worker initializer runs.
|
|
380
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
@@ -86,9 +86,9 @@ def _event_from_row(row):
|
|
|
86
86
|
)
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
def write_catalog(catalog,
|
|
89
|
+
def write_catalog(catalog, db_path=None):
|
|
90
90
|
"""Write the stored catalog into SQLite."""
|
|
91
|
-
conn = get_db_connection(
|
|
91
|
+
conn = get_db_connection(initdb=True, db_path=db_path)
|
|
92
92
|
try:
|
|
93
93
|
cursor = conn.cursor()
|
|
94
94
|
_ensure_catalog_table(cursor)
|
|
@@ -112,11 +112,11 @@ def write_catalog(catalog, config, db_path=None):
|
|
|
112
112
|
conn.close()
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
def read_catalog(
|
|
115
|
+
def read_catalog(db_path=None):
|
|
116
116
|
"""Read the stored catalog from SQLite."""
|
|
117
117
|
from ..catalog.catalog import RequakeCatalog
|
|
118
118
|
|
|
119
|
-
conn = get_db_connection(
|
|
119
|
+
conn = get_db_connection(initdb=False, db_path=db_path)
|
|
120
120
|
try:
|
|
121
121
|
rows = conn.execute(
|
|
122
122
|
f'SELECT * FROM {CATALOG_TABLE} ORDER BY orig_time'
|
|
@@ -125,7 +125,7 @@ def read_catalog(config, db_path=None):
|
|
|
125
125
|
if MISSING_CATALOG_TABLE in str(err):
|
|
126
126
|
raise FileNotFoundError(
|
|
127
127
|
'Catalog not found in db file '
|
|
128
|
-
f'{get_db_path(
|
|
128
|
+
f'{get_db_path(db_path=db_path)}'
|
|
129
129
|
) from err
|
|
130
130
|
raise
|
|
131
131
|
finally:
|
|
@@ -14,6 +14,7 @@ import contextlib
|
|
|
14
14
|
import random
|
|
15
15
|
import sqlite3
|
|
16
16
|
import time
|
|
17
|
+
from ..config.config import config
|
|
17
18
|
|
|
18
19
|
DB_VERSION = 1
|
|
19
20
|
DB_FILENAME = 'requake.sqlite'
|
|
@@ -29,7 +30,11 @@ class DatabaseBusyError(RuntimeError):
|
|
|
29
30
|
"""Raised when retries for transient SQLite lock errors are exhausted."""
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
class DatabaseCorruptError(RuntimeError):
|
|
34
|
+
"""Raised when the SQLite database file is corrupt or malformed."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_db_path(db_path=None):
|
|
33
38
|
"""Return the SQLite path."""
|
|
34
39
|
if db_path is not None:
|
|
35
40
|
return db_path
|
|
@@ -61,9 +66,9 @@ def get_db_path(config, db_path=None):
|
|
|
61
66
|
return os.path.join(outdir, DB_FILENAME)
|
|
62
67
|
|
|
63
68
|
|
|
64
|
-
def check_db_exists(
|
|
69
|
+
def check_db_exists(initdb=False, db_path=None):
|
|
65
70
|
"""Validate the configured database path for read or init mode."""
|
|
66
|
-
db_path = get_db_path(
|
|
71
|
+
db_path = get_db_path(db_path=db_path)
|
|
67
72
|
if initdb:
|
|
68
73
|
_ensure_parent_dir(db_path)
|
|
69
74
|
return db_path
|
|
@@ -138,15 +143,15 @@ def execute_with_retry(operation, operation_name):
|
|
|
138
143
|
attempt += 1
|
|
139
144
|
|
|
140
145
|
|
|
141
|
-
def initialize_database_if_needed(
|
|
146
|
+
def initialize_database_if_needed(db_path=None):
|
|
142
147
|
"""Initialize database schema version and pragmas if missing."""
|
|
143
|
-
conn = get_db_connection(
|
|
148
|
+
conn = get_db_connection(initdb=True, db_path=db_path)
|
|
144
149
|
conn.close()
|
|
145
150
|
|
|
146
151
|
|
|
147
|
-
def get_db_connection(
|
|
152
|
+
def get_db_connection(initdb=False, db_path=None):
|
|
148
153
|
"""Open a SQLite connection for the configured database file."""
|
|
149
|
-
db_path = check_db_exists(
|
|
154
|
+
db_path = check_db_exists(initdb=initdb, db_path=db_path)
|
|
150
155
|
conn = sqlite3.connect(db_path)
|
|
151
156
|
_configure_connection(conn)
|
|
152
157
|
conn.row_factory = sqlite3.Row
|
|
@@ -64,9 +64,9 @@ def _family_row(family, event):
|
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
def write_families(families
|
|
67
|
+
def write_families(families):
|
|
68
68
|
"""Write catalog-scan families into SQLite."""
|
|
69
|
-
conn = get_db_connection(
|
|
69
|
+
conn = get_db_connection(initdb=True)
|
|
70
70
|
try:
|
|
71
71
|
cursor = conn.cursor()
|
|
72
72
|
_ensure_families_table(cursor)
|
|
@@ -95,12 +95,12 @@ def write_families(families, config):
|
|
|
95
95
|
conn.close()
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def read_families(
|
|
98
|
+
def read_families():
|
|
99
99
|
"""Read catalog-scan families from SQLite."""
|
|
100
100
|
from ..catalog import RequakeEvent
|
|
101
101
|
from ..families.families import Family
|
|
102
102
|
|
|
103
|
-
conn = get_db_connection(
|
|
103
|
+
conn = get_db_connection(initdb=False)
|
|
104
104
|
try:
|
|
105
105
|
cursor = conn.cursor()
|
|
106
106
|
try:
|
|
@@ -113,7 +113,7 @@ def read_families(config):
|
|
|
113
113
|
except sqlite3.OperationalError as err:
|
|
114
114
|
if MISSING_FAMILIES_TABLE in str(err):
|
|
115
115
|
raise FileNotFoundError(
|
|
116
|
-
f'Families not found in db file {get_db_path(
|
|
116
|
+
f'Families not found in db file {get_db_path()}'
|
|
117
117
|
) from err
|
|
118
118
|
raise
|
|
119
119
|
finally:
|
|
@@ -147,9 +147,9 @@ def read_families(config):
|
|
|
147
147
|
return families
|
|
148
148
|
|
|
149
149
|
|
|
150
|
-
def update_family_valid(
|
|
150
|
+
def update_family_valid(family_number, is_valid):
|
|
151
151
|
"""Update the validity flag for all events in one family."""
|
|
152
|
-
conn = get_db_connection(
|
|
152
|
+
conn = get_db_connection(initdb=False)
|
|
153
153
|
try:
|
|
154
154
|
cursor = conn.cursor()
|
|
155
155
|
try:
|
|
@@ -166,7 +166,7 @@ def update_family_valid(config, family_number, is_valid):
|
|
|
166
166
|
except sqlite3.OperationalError as err:
|
|
167
167
|
if MISSING_FAMILIES_TABLE in str(err):
|
|
168
168
|
raise FileNotFoundError(
|
|
169
|
-
f'Families not found in db file {get_db_path(
|
|
169
|
+
f'Families not found in db file {get_db_path()}'
|
|
170
170
|
) from err
|
|
171
171
|
raise
|
|
172
172
|
conn.commit()
|