requake 0.7.3__tar.gz → 0.8__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.7.3 → requake-0.8}/CHANGELOG.md +12 -0
- {requake-0.7.3 → requake-0.8}/PKG-INFO +11 -2
- {requake-0.7.3 → requake-0.8}/README.md +9 -0
- {requake-0.7.3 → requake-0.8}/requake/_version.py +3 -3
- {requake-0.7.3 → requake-0.8}/requake/catalog/catalog.py +48 -87
- {requake-0.7.3 → requake-0.8}/requake/catalog/print_catalog.py +1 -3
- {requake-0.7.3 → requake-0.8}/requake/catalog/read_catalog.py +27 -11
- {requake-0.7.3 → requake-0.8}/requake/catalog/read_catalog_from_csv.py +5 -7
- {requake-0.7.3 → requake-0.8}/requake/config/__init__.py +5 -1
- requake-0.8/requake/config/config.py +125 -0
- requake-0.8/requake/config/configobj/_version.py +6 -0
- {requake-0.7.3 → requake-0.8}/requake/config/generic_printer.py +1 -1
- {requake-0.7.3 → requake-0.8}/requake/config/parse_arguments.py +14 -15
- {requake-0.7.3 → requake-0.8}/requake/config/rq_setup.py +9 -23
- {requake-0.7.3 → requake-0.8}/requake/config/utils.py +22 -7
- requake-0.8/requake/database/__init__.py +12 -0
- requake-0.8/requake/database/catalog.py +136 -0
- requake-0.8/requake/database/db.py +158 -0
- requake-0.8/requake/database/families.py +174 -0
- requake-0.8/requake/database/pairs.py +180 -0
- requake-0.8/requake/database/templates.py +175 -0
- {requake-0.7.3 → requake-0.8}/requake/families/__init__.py +3 -2
- {requake-0.7.3 → requake-0.8}/requake/families/build_families.py +19 -26
- {requake-0.7.3 → requake-0.8}/requake/families/build_templates.py +1 -3
- {requake-0.7.3 → requake-0.8}/requake/families/families.py +21 -71
- requake-0.8/requake/families/flag_family.py +31 -0
- {requake-0.7.3 → requake-0.8}/requake/families/pairs.py +14 -44
- {requake-0.7.3 → requake-0.8}/requake/families/print_families.py +3 -5
- {requake-0.7.3 → requake-0.8}/requake/families/print_pairs.py +1 -3
- {requake-0.7.3 → requake-0.8}/requake/formulas/conversion.py +2 -4
- {requake-0.7.3 → requake-0.8}/requake/formulas/moment.py +2 -2
- {requake-0.7.3 → requake-0.8}/requake/formulas/slip.py +6 -6
- {requake-0.7.3 → requake-0.8}/requake/main.py +1 -1
- {requake-0.7.3 → requake-0.8}/requake/plot/cached_tiler.py +1 -1
- {requake-0.7.3 → requake-0.8}/requake/plot/colormaps.py +0 -1
- {requake-0.7.3 → requake-0.8}/requake/plot/map_families.py +1 -3
- {requake-0.7.3 → requake-0.8}/requake/plot/map_tiles.py +25 -25
- {requake-0.7.3 → requake-0.8}/requake/plot/plot_cumulative.py +5 -11
- {requake-0.7.3 → requake-0.8}/requake/plot/plot_families.py +23 -20
- {requake-0.7.3 → requake-0.8}/requake/plot/plot_pair.py +1 -3
- {requake-0.7.3 → requake-0.8}/requake/plot/plot_timespans.py +2 -6
- {requake-0.7.3 → requake-0.8}/requake/plot/plot_utils.py +11 -12
- {requake-0.7.3 → requake-0.8}/requake/scan/scan_catalog.py +29 -31
- {requake-0.7.3 → requake-0.8}/requake/scan/scan_templates.py +36 -47
- {requake-0.7.3 → requake-0.8}/requake/waveforms/waveform_pair.py +1 -0
- {requake-0.7.3 → requake-0.8}/requake/waveforms/waveforms.py +9 -12
- {requake-0.7.3 → requake-0.8}/requake.egg-info/PKG-INFO +11 -2
- {requake-0.7.3 → requake-0.8}/requake.egg-info/SOURCES.txt +6 -0
- {requake-0.7.3 → requake-0.8}/setup.py +1 -1
- requake-0.7.3/requake/config/config.py +0 -31
- requake-0.7.3/requake/config/configobj/_version.py +0 -2
- requake-0.7.3/requake/families/flag_family.py +0 -46
- {requake-0.7.3 → requake-0.8}/CONTRIBUTORS.txt +0 -0
- {requake-0.7.3 → requake-0.8}/LICENSE.txt +0 -0
- {requake-0.7.3 → requake-0.8}/MANIFEST.in +0 -0
- {requake-0.7.3 → requake-0.8}/pyproject.toml +0 -0
- {requake-0.7.3 → requake-0.8}/requake/__init__.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake/catalog/__init__.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake/catalog/read_catalog_from_fdsnws.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake/catalog/read_catalog_from_quakeml.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake/config/configobj/LICENSE +0 -0
- {requake-0.7.3 → requake-0.8}/requake/config/configobj/__init__.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake/config/configobj/validate.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake/config/configspec.conf +0 -0
- {requake-0.7.3 → requake-0.8}/requake/formulas/__init__.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake/plot/__init__.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake/scan/__init__.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake/waveforms/__init__.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake/waveforms/arrivals.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake/waveforms/station_metadata.py +0 -0
- {requake-0.7.3 → requake-0.8}/requake.egg-info/dependency_links.txt +0 -0
- {requake-0.7.3 → requake-0.8}/requake.egg-info/entry_points.txt +0 -0
- {requake-0.7.3 → requake-0.8}/requake.egg-info/requires.txt +0 -0
- {requake-0.7.3 → requake-0.8}/requake.egg-info/top_level.txt +0 -0
- {requake-0.7.3 → requake-0.8}/setup.cfg +0 -0
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2021-2026 Claudio Satriano <satriano@ipgp.fr>
|
|
4
4
|
|
|
5
|
+
## [0.8] - 2026-05-22
|
|
6
|
+
|
|
7
|
+
This release introduces a major storage change.
|
|
8
|
+
|
|
9
|
+
Requake now stores scan-related data (catalog, event pairs, and families) in a
|
|
10
|
+
SQLite database instead of CSV files. This improves storage efficiency and
|
|
11
|
+
data access performance, especially for large catalogs.
|
|
12
|
+
|
|
13
|
+
This change is not backward compatible at the data format level. The
|
|
14
|
+
high-level behavior of the command-line interface remains unchanged.
|
|
15
|
+
|
|
5
16
|
## [0.7.3] - 2026-04-14
|
|
6
17
|
|
|
7
18
|
This release is intended to validate the new release workflow.
|
|
@@ -140,6 +151,7 @@ Mostly a bugfix release with a slightly improved trace plotting.
|
|
|
140
151
|
|
|
141
152
|
- Initial release, not yet feature complete
|
|
142
153
|
|
|
154
|
+
[0.8]: https://github.com/SeismicSource/requake/compare/v0.7.3...v0.8
|
|
143
155
|
[0.7.3]: https://github.com/SeismicSource/requake/compare/v0.7.2...v0.7.3
|
|
144
156
|
[0.7.2]: https://github.com/SeismicSource/requake/compare/v0.7.1...v0.7.2
|
|
145
157
|
[0.7.1]: https://github.com/SeismicSource/requake/compare/v0.7...v0.7.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: requake
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8
|
|
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@e3ac4cce14f782d62a5f22f432a5391a2ccacfe7/imgs/Requake_logo.svg" width="400">
|
|
41
41
|
|
|
42
42
|
# Requake
|
|
43
43
|
|
|
@@ -140,6 +140,15 @@ You can keep your local Requake repository updated by running `git pull`
|
|
|
140
140
|
from times to times. Thanks to `pip`'s "editable mode", you don't need to
|
|
141
141
|
reinstall Requake after each update.
|
|
142
142
|
|
|
143
|
+
If you want the repository style rules to run locally before each commit,
|
|
144
|
+
enable the included git hook once per clone:
|
|
145
|
+
|
|
146
|
+
scripts/setup_local_hooks.sh
|
|
147
|
+
|
|
148
|
+
You can also run the local style checks manually with:
|
|
149
|
+
|
|
150
|
+
scripts/check_style.sh
|
|
151
|
+
|
|
143
152
|
## Running
|
|
144
153
|
|
|
145
154
|
### Command line arguments
|
|
@@ -101,6 +101,15 @@ You can keep your local Requake repository updated by running `git pull`
|
|
|
101
101
|
from times to times. Thanks to `pip`'s "editable mode", you don't need to
|
|
102
102
|
reinstall Requake after each update.
|
|
103
103
|
|
|
104
|
+
If you want the repository style rules to run locally before each commit,
|
|
105
|
+
enable the included git hook once per clone:
|
|
106
|
+
|
|
107
|
+
scripts/setup_local_hooks.sh
|
|
108
|
+
|
|
109
|
+
You can also run the local style checks manually with:
|
|
110
|
+
|
|
111
|
+
scripts/check_style.sh
|
|
112
|
+
|
|
104
113
|
## Running
|
|
105
114
|
|
|
106
115
|
### Command line arguments
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2026-
|
|
11
|
+
"date": "2026-05-22T08:53:09+0200",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.
|
|
14
|
+
"full-revisionid": "e3ac4cce14f782d62a5f22f432a5391a2ccacfe7",
|
|
15
|
+
"version": "0.8"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -28,6 +28,7 @@ class RequakeEvent():
|
|
|
28
28
|
depth=None, mag_type=None, mag=None, author=None,
|
|
29
29
|
catalog=None, contributor=None, contributor_id=None,
|
|
30
30
|
mag_author=None, location_name=None, trace_id=None):
|
|
31
|
+
"""Initialize an event container."""
|
|
31
32
|
self.evid = evid
|
|
32
33
|
self.orig_time = orig_time
|
|
33
34
|
self.lon = lon
|
|
@@ -45,24 +46,31 @@ class RequakeEvent():
|
|
|
45
46
|
self.correlations = {}
|
|
46
47
|
|
|
47
48
|
def __eq__(self, other):
|
|
49
|
+
"""Return True when two events have the same identity."""
|
|
48
50
|
return self.evid == other.evid and self.trace_id == other.trace_id
|
|
49
51
|
|
|
50
52
|
def __gt__(self, other):
|
|
53
|
+
"""Compare events by origin time."""
|
|
51
54
|
return self.orig_time > other.orig_time
|
|
52
55
|
|
|
53
56
|
def __ge__(self, other):
|
|
57
|
+
"""Compare events by origin time."""
|
|
54
58
|
return self.orig_time >= other.orig_time
|
|
55
59
|
|
|
56
60
|
def __lt__(self, other):
|
|
61
|
+
"""Compare events by origin time."""
|
|
57
62
|
return self.orig_time < other.orig_time
|
|
58
63
|
|
|
59
64
|
def __le__(self, other):
|
|
65
|
+
"""Compare events by origin time."""
|
|
60
66
|
return self.orig_time <= other.orig_time
|
|
61
67
|
|
|
62
68
|
def __hash__(self):
|
|
69
|
+
"""Return the event hash."""
|
|
63
70
|
return self.evid.__hash__()
|
|
64
71
|
|
|
65
72
|
def __str__(self):
|
|
73
|
+
"""Return a compact string representation of the event."""
|
|
66
74
|
return (
|
|
67
75
|
f'{self.evid} {self.orig_time} '
|
|
68
76
|
f'{self.lon} {self.lat} {self.depth} {self.mag_type} {self.mag}'
|
|
@@ -148,46 +156,12 @@ class RequakeCatalog(list):
|
|
|
148
156
|
"""
|
|
149
157
|
self[:] = sorted(self, key=lambda ev: ev.orig_time)
|
|
150
158
|
|
|
151
|
-
def read(self, filename):
|
|
152
|
-
"""
|
|
153
|
-
Read catalog from FDSN text file format.
|
|
154
|
-
|
|
155
|
-
Skips events already in the catalog.
|
|
156
|
-
|
|
157
|
-
:param filename: input filename
|
|
158
|
-
:type filename: str
|
|
159
|
-
|
|
160
|
-
:raises FileNotFoundError: if filename does not exist
|
|
161
|
-
:raises ValueError: if line is not in FDSN text file format
|
|
162
|
-
"""
|
|
163
|
-
with open(filename, 'r', encoding='utf8') as fp:
|
|
164
|
-
for line in fp:
|
|
165
|
-
if not line:
|
|
166
|
-
continue
|
|
167
|
-
if line[0] == '#':
|
|
168
|
-
continue
|
|
169
|
-
ev = RequakeEvent()
|
|
170
|
-
ev.from_fdsn_text(line)
|
|
171
|
-
self.append(ev)
|
|
172
|
-
self.deduplicate()
|
|
173
|
-
|
|
174
|
-
def write(self, filename):
|
|
175
|
-
"""
|
|
176
|
-
Write catalog in FDSN text file format.
|
|
177
|
-
|
|
178
|
-
:param filename: output filename
|
|
179
|
-
:type filename: str
|
|
180
|
-
"""
|
|
181
|
-
with open(filename, 'w', encoding='utf8') as fp:
|
|
182
|
-
for ev in self:
|
|
183
|
-
fp.write(ev.fdsn_text() + '\n')
|
|
184
|
-
|
|
185
159
|
def filter(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
160
|
+
self, starttime=None, endtime=None,
|
|
161
|
+
minlatitude=None, maxlatitude=None,
|
|
162
|
+
minlongitude=None, maxlongitude=None,
|
|
163
|
+
mindepth=None, maxdepth=None,
|
|
164
|
+
minmagnitude=None, maxmagnitude=None
|
|
191
165
|
):
|
|
192
166
|
"""
|
|
193
167
|
Filter the catalog, based on the specified criteria.
|
|
@@ -220,60 +194,47 @@ class RequakeCatalog(list):
|
|
|
220
194
|
"""
|
|
221
195
|
outcat = RequakeCatalog()
|
|
222
196
|
for ev in self:
|
|
223
|
-
if ev.orig_time
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if ev.lon is not None:
|
|
234
|
-
if minlongitude is not None and ev.lon < minlongitude:
|
|
235
|
-
continue
|
|
236
|
-
if maxlongitude is not None and ev.lon > maxlongitude:
|
|
237
|
-
continue
|
|
238
|
-
if ev.depth is not None:
|
|
239
|
-
if mindepth is not None and ev.depth < mindepth:
|
|
240
|
-
continue
|
|
241
|
-
if maxdepth is not None and ev.depth > maxdepth:
|
|
242
|
-
continue
|
|
243
|
-
if ev.mag is not None:
|
|
244
|
-
if minmagnitude is not None and ev.mag < minmagnitude:
|
|
245
|
-
continue
|
|
246
|
-
if maxmagnitude is not None and ev.mag > maxmagnitude:
|
|
247
|
-
continue
|
|
197
|
+
if _out_of_range(ev.orig_time, starttime, endtime):
|
|
198
|
+
continue
|
|
199
|
+
if _out_of_range(ev.lat, minlatitude, maxlatitude):
|
|
200
|
+
continue
|
|
201
|
+
if _out_of_range(ev.lon, minlongitude, maxlongitude):
|
|
202
|
+
continue
|
|
203
|
+
if _out_of_range(ev.depth, mindepth, maxdepth):
|
|
204
|
+
continue
|
|
205
|
+
if _out_of_range(ev.mag, minmagnitude, maxmagnitude):
|
|
206
|
+
continue
|
|
248
207
|
outcat.append(ev)
|
|
249
208
|
return outcat
|
|
250
209
|
|
|
251
210
|
|
|
211
|
+
def _out_of_range(value, minimum, maximum):
|
|
212
|
+
"""Return True when a value falls outside the requested bounds."""
|
|
213
|
+
if value is None:
|
|
214
|
+
return False
|
|
215
|
+
return (
|
|
216
|
+
(minimum is not None and value < minimum)
|
|
217
|
+
or (maximum is not None and value > maximum)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
252
221
|
def read_stored_catalog():
|
|
253
222
|
"""
|
|
254
|
-
Read the catalog stored in the output
|
|
223
|
+
Read the catalog stored in the output database.
|
|
255
224
|
|
|
256
225
|
:return: Catalog object.
|
|
257
226
|
:rtype: RequakeCatalog
|
|
258
227
|
|
|
259
|
-
:raises ValueError: if
|
|
260
|
-
:raises FileNotFoundError: if
|
|
228
|
+
:raises ValueError: if the stored catalog is empty
|
|
229
|
+
:raises FileNotFoundError: if the output database is not found
|
|
261
230
|
"""
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
except ValueError as err:
|
|
270
|
-
raise ValueError(
|
|
271
|
-
f'Error reading catalog file {config.scan_catalog_file}: {err}'
|
|
272
|
-
) from err
|
|
273
|
-
except (FileNotFoundError, NotADirectoryError) as err:
|
|
274
|
-
raise FileNotFoundError(
|
|
275
|
-
f'Catalog file {config.scan_catalog_file} not found'
|
|
276
|
-
) from err
|
|
231
|
+
from ..database.catalog import read_catalog as read_catalog_from_db
|
|
232
|
+
|
|
233
|
+
cat = read_catalog_from_db(config)
|
|
234
|
+
cat.sort()
|
|
235
|
+
if not cat:
|
|
236
|
+
raise ValueError('Empty catalog')
|
|
237
|
+
return cat
|
|
277
238
|
|
|
278
239
|
|
|
279
240
|
def fix_non_locatable_events(catalog):
|
|
@@ -308,9 +269,9 @@ def _base26(val):
|
|
|
308
269
|
:rtype: str
|
|
309
270
|
"""
|
|
310
271
|
chars = [
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
272
|
+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
|
273
|
+
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
|
|
274
|
+
'u', 'v', 'w', 'x', 'y', 'z'
|
|
314
275
|
]
|
|
315
276
|
base = len(chars)
|
|
316
277
|
ret = ''
|
|
@@ -337,7 +298,7 @@ def generate_evid(orig_time):
|
|
|
337
298
|
orig_year = UTCDateTime(year=year, month=1, day=1)
|
|
338
299
|
val = int(orig_time - orig_year)
|
|
339
300
|
# normalize val between 0 (aaaaaa) and 26**6-1 (zzzzzz)
|
|
340
|
-
maxval = 366*24*3600 # max number of seconds in leap year
|
|
341
|
-
normval = int(val/maxval * (26**6-1))
|
|
301
|
+
maxval = 366 * 24 * 3600 # max number of seconds in leap year
|
|
302
|
+
normval = int(val / maxval * (26 ** 6 - 1))
|
|
342
303
|
ret = _base26(normval)
|
|
343
304
|
return f'{prefix}{year}{ret}'
|
|
@@ -16,9 +16,7 @@ logger = logging.getLogger(__name__.rsplit('.', maxsplit=1)[-1])
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def print_catalog():
|
|
19
|
-
"""
|
|
20
|
-
Print the event catalog to screen.
|
|
21
|
-
"""
|
|
19
|
+
"""Print the event catalog to screen."""
|
|
22
20
|
try:
|
|
23
21
|
catalog = read_stored_catalog()
|
|
24
22
|
except (ValueError, FileNotFoundError) as msg:
|
|
@@ -13,7 +13,9 @@ import os
|
|
|
13
13
|
import logging
|
|
14
14
|
import contextlib
|
|
15
15
|
from ..config import config, rq_exit
|
|
16
|
-
from .
|
|
16
|
+
from ..database.db import get_db_path
|
|
17
|
+
from ..database.catalog import write_catalog as write_stored_catalog
|
|
18
|
+
from .catalog import RequakeEvent, RequakeCatalog
|
|
17
19
|
from .read_catalog_from_fdsnws import read_catalog_from_fdsnws
|
|
18
20
|
from .read_catalog_from_quakeml import read_catalog_from_quakeml
|
|
19
21
|
from .read_catalog_from_csv import read_catalog_from_csv
|
|
@@ -45,7 +47,16 @@ def _read_catalog_from_file():
|
|
|
45
47
|
# try to read the catalog as a FDSN text file
|
|
46
48
|
with contextlib.suppress(ValueError):
|
|
47
49
|
cat = RequakeCatalog()
|
|
48
|
-
|
|
50
|
+
with open(catalog_file, 'r', encoding='utf8') as fp:
|
|
51
|
+
for line in fp:
|
|
52
|
+
if not line:
|
|
53
|
+
continue
|
|
54
|
+
if line[0] == '#':
|
|
55
|
+
continue
|
|
56
|
+
ev = RequakeEvent()
|
|
57
|
+
ev.from_fdsn_text(line)
|
|
58
|
+
cat.append(ev)
|
|
59
|
+
cat.deduplicate()
|
|
49
60
|
return cat
|
|
50
61
|
# try to read the catalog as a CSV file
|
|
51
62
|
# raises ValueError in case of failure
|
|
@@ -54,8 +65,7 @@ def _read_catalog_from_file():
|
|
|
54
65
|
|
|
55
66
|
def _filter_catalog(catalog):
|
|
56
67
|
"""
|
|
57
|
-
Filter an event catalog
|
|
58
|
-
in the configuration.
|
|
68
|
+
Filter an event catalog using the configured selection criteria.
|
|
59
69
|
|
|
60
70
|
:param catalog: Event catalog.
|
|
61
71
|
:type catalog: requake.catalog.RequakeCatalog
|
|
@@ -99,14 +109,17 @@ def read_catalog():
|
|
|
99
109
|
:rtype: requake.catalog.RequakeCatalog
|
|
100
110
|
"""
|
|
101
111
|
catalog = RequakeCatalog()
|
|
102
|
-
output_cat_file = config.scan_catalog_file
|
|
103
112
|
nevs_read = 0
|
|
104
113
|
if config.args.append:
|
|
105
114
|
with contextlib.suppress(FileNotFoundError):
|
|
106
|
-
catalog
|
|
107
|
-
|
|
115
|
+
from .catalog import read_stored_catalog
|
|
116
|
+
|
|
117
|
+
catalog = read_stored_catalog()
|
|
108
118
|
nevs_read = len(catalog)
|
|
109
|
-
logger.info(
|
|
119
|
+
logger.info(
|
|
120
|
+
f'{nevs_read} events read from db file '
|
|
121
|
+
f'{get_db_path(config)}'
|
|
122
|
+
)
|
|
110
123
|
logger.info('Reading catalog...')
|
|
111
124
|
input_cat_file = config.args.catalog_file
|
|
112
125
|
if input_cat_file is not None:
|
|
@@ -136,7 +149,10 @@ def read_catalog():
|
|
|
136
149
|
logger.info(f'{nevs_dedup} duplicate events removed')
|
|
137
150
|
# Sort catalog in increasing time order
|
|
138
151
|
catalog.sort()
|
|
139
|
-
# Write catalog to output
|
|
140
|
-
catalog
|
|
152
|
+
# Write catalog to the output database
|
|
153
|
+
write_stored_catalog(catalog, config)
|
|
141
154
|
nevs_written = len(catalog) - nevs_read
|
|
142
|
-
logger.info(
|
|
155
|
+
logger.info(
|
|
156
|
+
f'{nevs_written} events written to db file '
|
|
157
|
+
f'{get_db_path(config)}'
|
|
158
|
+
)
|
|
@@ -19,8 +19,7 @@ logger = logging.getLogger(__name__.rsplit('.', maxsplit=1)[-1])
|
|
|
19
19
|
|
|
20
20
|
def _guess_field_names(input_fields):
|
|
21
21
|
"""
|
|
22
|
-
Guess
|
|
23
|
-
depth, magnitude and magnitude type.
|
|
22
|
+
Guess field names for origin time, location, depth, and magnitude.
|
|
24
23
|
|
|
25
24
|
:param input_fields: list of field names
|
|
26
25
|
:type input_fields: list of str
|
|
@@ -80,9 +79,8 @@ def _guess_field_names(input_fields):
|
|
|
80
79
|
# make a list of duplicated fields, which have been matched more than once
|
|
81
80
|
duplicated_fields = [
|
|
82
81
|
(key, value, output_field_scores[key])
|
|
83
|
-
for key, value in output_fields.items()
|
|
84
|
-
value is not None and
|
|
85
|
-
list(output_fields.values()).count(value) > 1
|
|
82
|
+
for key, value in output_fields.items()
|
|
83
|
+
if value is not None and list(output_fields.values()).count(value) > 1
|
|
86
84
|
]
|
|
87
85
|
# if there are duplicated fields, keep the one with the highest score
|
|
88
86
|
for _key, value, score in duplicated_fields:
|
|
@@ -167,7 +165,7 @@ def read_catalog_from_csv(filename):
|
|
|
167
165
|
nrows -= 1 # first row is the header
|
|
168
166
|
cat = RequakeCatalog()
|
|
169
167
|
for n, row in enumerate(reader):
|
|
170
|
-
print(f'reading row {n+1}/{nrows}\r', end='')
|
|
168
|
+
print(f'reading row {n + 1}/{nrows}\r', end='')
|
|
171
169
|
if fields['orig_time'] is None:
|
|
172
170
|
# try build a date-time field from year, month, day, hour,
|
|
173
171
|
# minute and seconds fields
|
|
@@ -195,7 +193,7 @@ def read_catalog_from_csv(filename):
|
|
|
195
193
|
)
|
|
196
194
|
except ValueError:
|
|
197
195
|
logger.error(
|
|
198
|
-
f'Unable to parse origin time at row {n+2}: '
|
|
196
|
+
f'Unable to parse origin time at row {n + 2}: '
|
|
199
197
|
f'"{orig_time_str}"')
|
|
200
198
|
continue
|
|
201
199
|
row[None] = None
|
|
@@ -14,4 +14,8 @@ 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()
|
|
16
16
|
# function in rq_setup.py
|
|
17
|
-
from .config import
|
|
17
|
+
from .config import ( # noqa
|
|
18
|
+
config,
|
|
19
|
+
to_picklable_config_dict,
|
|
20
|
+
from_picklable_config_dict,
|
|
21
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# -*- coding: utf8 -*-
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
"""
|
|
4
|
+
Config class for Requake.
|
|
5
|
+
|
|
6
|
+
:copyright:
|
|
7
|
+
2021-2026 Claudio Satriano <satriano@ipgp.fr>
|
|
8
|
+
:license:
|
|
9
|
+
GNU General Public License v3.0 or later
|
|
10
|
+
(https://www.gnu.org/licenses/gpl-3.0-standalone.html)
|
|
11
|
+
"""
|
|
12
|
+
from argparse import Namespace
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Runtime objects that are useful in-process but should not be sent to
|
|
16
|
+
# worker processes via ProcessPoolExecutor.
|
|
17
|
+
RUNTIME_ONLY_KEYS = frozenset({
|
|
18
|
+
'station_client',
|
|
19
|
+
'dataselect_client',
|
|
20
|
+
'catalog_fdsn_event_clients',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Config(dict):
|
|
25
|
+
"""A class to access config values with dot notation."""
|
|
26
|
+
|
|
27
|
+
def __setitem__(self, key, value):
|
|
28
|
+
"""Make Config keys accessible as attributes."""
|
|
29
|
+
super().__setattr__(key, value)
|
|
30
|
+
super().__setitem__(key, value)
|
|
31
|
+
|
|
32
|
+
def __getattr__(self, key):
|
|
33
|
+
"""Make Config keys accessible as attributes."""
|
|
34
|
+
try:
|
|
35
|
+
return self.__getitem__(key)
|
|
36
|
+
except KeyError as err:
|
|
37
|
+
raise AttributeError(err) from err
|
|
38
|
+
|
|
39
|
+
__setattr__ = __setitem__
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def to_picklable_config_dict(
|
|
43
|
+
cfg,
|
|
44
|
+
drop_inventory=False,
|
|
45
|
+
extra_excluded_keys=None,
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Build a pickle-safe config snapshot for multiprocessing.
|
|
49
|
+
|
|
50
|
+
This helper strips known runtime-only objects (for example ObsPy clients
|
|
51
|
+
containing SSL contexts) and converts ``args`` from ``argparse.Namespace``
|
|
52
|
+
to a plain ``dict``.
|
|
53
|
+
|
|
54
|
+
Typical usage with ``concurrent.futures.ProcessPoolExecutor``::
|
|
55
|
+
|
|
56
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
57
|
+
from requake.config import (
|
|
58
|
+
config,
|
|
59
|
+
to_picklable_config_dict,
|
|
60
|
+
from_picklable_config_dict,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
cfg_dict = to_picklable_config_dict(config)
|
|
64
|
+
|
|
65
|
+
def worker(cfg_dict, event_id):
|
|
66
|
+
cfg = from_picklable_config_dict(cfg_dict)
|
|
67
|
+
# Rebuild only what is needed in each process.
|
|
68
|
+
# Re-create network clients here, not in the parent process.
|
|
69
|
+
return cfg['cc_min'], event_id
|
|
70
|
+
|
|
71
|
+
def _worker_call(args):
|
|
72
|
+
cfg, evid = args
|
|
73
|
+
return worker(cfg, evid)
|
|
74
|
+
|
|
75
|
+
with ProcessPoolExecutor() as pool:
|
|
76
|
+
worker_args = ((cfg_dict, evid) for evid in event_ids)
|
|
77
|
+
list(pool.map(_worker_call, worker_args))
|
|
78
|
+
|
|
79
|
+
:param cfg: Requake config-like mapping.
|
|
80
|
+
:type cfg: Mapping
|
|
81
|
+
:param drop_inventory: Exclude ``inventory`` from the snapshot.
|
|
82
|
+
:type drop_inventory: bool
|
|
83
|
+
:param extra_excluded_keys: Additional keys to remove from the snapshot.
|
|
84
|
+
:type extra_excluded_keys: iterable or None
|
|
85
|
+
:return: A pickle-safe shallow copy of the configuration.
|
|
86
|
+
:rtype: dict
|
|
87
|
+
"""
|
|
88
|
+
cfg_dict = dict(cfg)
|
|
89
|
+
excluded_keys = set(RUNTIME_ONLY_KEYS)
|
|
90
|
+
if drop_inventory:
|
|
91
|
+
excluded_keys.add('inventory')
|
|
92
|
+
if extra_excluded_keys is not None:
|
|
93
|
+
excluded_keys.update(extra_excluded_keys)
|
|
94
|
+
for key in excluded_keys:
|
|
95
|
+
cfg_dict.pop(key, None)
|
|
96
|
+
|
|
97
|
+
args = cfg_dict.get('args')
|
|
98
|
+
if isinstance(args, Namespace):
|
|
99
|
+
cfg_dict['args'] = vars(args).copy()
|
|
100
|
+
|
|
101
|
+
return cfg_dict
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def from_picklable_config_dict(cfg_dict):
|
|
105
|
+
"""
|
|
106
|
+
Rebuild a ``Config`` instance from a pickle-safe snapshot.
|
|
107
|
+
|
|
108
|
+
This helper is the counterpart of ``to_picklable_config_dict()`` and is
|
|
109
|
+
intended for worker processes receiving a serialized config payload.
|
|
110
|
+
|
|
111
|
+
If ``args`` is a dict, it is converted back to ``argparse.Namespace``.
|
|
112
|
+
|
|
113
|
+
:param cfg_dict: Pickle-safe config snapshot.
|
|
114
|
+
:type cfg_dict: Mapping
|
|
115
|
+
:return: Reconstructed config object.
|
|
116
|
+
:rtype: Config
|
|
117
|
+
"""
|
|
118
|
+
cfg = Config()
|
|
119
|
+
cfg.update(dict(cfg_dict))
|
|
120
|
+
args = cfg.get('args')
|
|
121
|
+
if isinstance(args, dict):
|
|
122
|
+
cfg['args'] = Namespace(**args)
|
|
123
|
+
return cfg
|
|
124
|
+
|
|
125
|
+
config = Config() # noqa
|
|
@@ -21,7 +21,7 @@ logger = logging.getLogger(__name__.rsplit('.', maxsplit=1)[-1])
|
|
|
21
21
|
|
|
22
22
|
def generic_printer(rows, headers_fmt, print_headers=True):
|
|
23
23
|
"""
|
|
24
|
-
|
|
24
|
+
Print rows using the configured output format.
|
|
25
25
|
|
|
26
26
|
:param rows: Rows to print.
|
|
27
27
|
:type rows: list of rows
|
|
@@ -18,9 +18,8 @@ from .._version import get_versions
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class NewlineHelpFormatter(argparse.HelpFormatter):
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
"""
|
|
21
|
+
"""Custom help formatter that preserves newlines in help messages."""
|
|
22
|
+
|
|
24
23
|
def _split_lines(self, text, width):
|
|
25
24
|
lines = []
|
|
26
25
|
for line in text.splitlines(): # Split the text by newlines first
|
|
@@ -36,11 +35,11 @@ class NewlineHelpFormatter(argparse.HelpFormatter):
|
|
|
36
35
|
|
|
37
36
|
class SubcommandHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|
38
37
|
"""
|
|
39
|
-
Custom help formatter that removes the list
|
|
40
|
-
message.
|
|
38
|
+
Custom help formatter that removes the subcommand list from help.
|
|
41
39
|
|
|
42
40
|
See: https://stackoverflow.com/a/13429281/2021880
|
|
43
41
|
"""
|
|
42
|
+
|
|
44
43
|
def _format_action(self, action):
|
|
45
44
|
parts = super(
|
|
46
45
|
argparse.RawDescriptionHelpFormatter, self
|
|
@@ -130,7 +129,7 @@ def parse_arguments(progname='requake'):
|
|
|
130
129
|
'- CSV\n'
|
|
131
130
|
'- Space-separated text files.\n\n'
|
|
132
131
|
'For CSV or space-separated formats, the file must include '
|
|
133
|
-
|
|
132
|
+
"at least one column for the event's origin time "
|
|
134
133
|
'and may contain additional columns for event ID, longitude, '
|
|
135
134
|
'latitude, depth, and magnitude. Column names '
|
|
136
135
|
'must be specified in the first row.\n\n'
|
|
@@ -472,8 +471,8 @@ def parse_arguments(progname='requake'):
|
|
|
472
471
|
pass
|
|
473
472
|
except ValueError:
|
|
474
473
|
sys.stderr.write(
|
|
475
|
-
f
|
|
476
|
-
|
|
474
|
+
f'{progname} {args.action}: '
|
|
475
|
+
'error: argument -l/--longerthan: '
|
|
477
476
|
f"invalid value: '{args.longerthan}'\n"
|
|
478
477
|
)
|
|
479
478
|
sys.exit(2)
|
|
@@ -483,8 +482,8 @@ def parse_arguments(progname='requake'):
|
|
|
483
482
|
pass
|
|
484
483
|
except ValueError:
|
|
485
484
|
sys.stderr.write(
|
|
486
|
-
f
|
|
487
|
-
|
|
485
|
+
f'{progname} {args.action}: '
|
|
486
|
+
'error: argument -S/--shorterthan: '
|
|
488
487
|
f"invalid value: '{args.shorterthan}'\n"
|
|
489
488
|
)
|
|
490
489
|
sys.exit(2)
|
|
@@ -513,13 +512,13 @@ def _timespec_to_sec(timespec):
|
|
|
513
512
|
elif suffix == 'm':
|
|
514
513
|
time_in_seconds *= 60
|
|
515
514
|
elif suffix == 'h':
|
|
516
|
-
time_in_seconds *= 24*60
|
|
515
|
+
time_in_seconds *= 24 * 60
|
|
517
516
|
elif suffix == 'd':
|
|
518
|
-
time_in_seconds *= 24*60*60
|
|
517
|
+
time_in_seconds *= 24 * 60 * 60
|
|
519
518
|
elif suffix == 'M':
|
|
520
|
-
time_in_seconds *= 30*24*60*60
|
|
519
|
+
time_in_seconds *= 30 * 24 * 60 * 60
|
|
521
520
|
elif suffix == 'y':
|
|
522
|
-
time_in_seconds *= 365*24*60*60
|
|
521
|
+
time_in_seconds *= 365 * 24 * 60 * 60
|
|
523
522
|
else:
|
|
524
|
-
raise ValueError(f
|
|
523
|
+
raise ValueError(f'Invalid time specification: {timespec}')
|
|
525
524
|
return time_in_seconds
|