Coverage for /usr/local/lib/python3.13/dist-packages/pyrocko/squirrel/tool/commands/jackseis.py: 24%
374 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-12-04 10:41 +0000
1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6'''
7Implementation of :app:`squirrel jackseis`.
8'''
10import re
11import sys
12import logging
13import os.path as op
15import numpy as num
17from pyrocko import io, trace, util
18from pyrocko import progress
19from pyrocko.has_paths import Path, HasPaths
20from pyrocko.guts import Dict, String, Choice, Float, Bool, List, Timestamp, \
21 StringChoice, IntChoice, Defer, load_all, clone
23from pyrocko.squirrel.base import Squirrel
24from pyrocko.squirrel.dataset import Dataset
25from pyrocko.squirrel.client.local import LocalData
26from pyrocko.squirrel.error import ToolError, SquirrelError
27from pyrocko.squirrel.model import CodesNSLCE, QuantityType
28from pyrocko.squirrel.operators.base import NetworkGrouping, StationGrouping, \
29 ChannelGrouping, SensorGrouping
30from pyrocko.squirrel.storage import StorageSchemeChoice, StorageScheme, \
31 StorageSchemeLayout, get_storage_scheme
32from pyrocko.squirrel.tool.common import ldq
34tts = util.time_to_str
36guts_prefix = 'jackseis'
37logger = logging.getLogger('psq.cli.jackseis')
40g_filenames_all = set()
43def check_append_hook(fn):
44 return fn in g_filenames_all
47def dset(kwargs, keys, values):
48 for k, v in zip(keys, values):
49 kwargs[k] = v
52def make_task(*args):
53 return progress.task(*args, logger=logger)
56def parse_rename_rule_from_string(s):
57 s = s.strip()
58 if re.match(r'^([^:,]*:[^:,]*,?)+', s):
59 return dict(
60 x.split(':') for x in s.strip(',').split(','))
61 else:
62 return s
65class JackseisError(ToolError):
66 pass
69class Chain(object):
70 def __init__(self, node, parent=None):
71 self.node = node
72 self.parent = parent
74 def mcall(self, name, *args, **kwargs):
75 ret = []
76 if self.parent is not None:
77 ret.append(self.parent.mcall(name, *args, **kwargs))
79 ret.append(getattr(self.node, name)(*args, **kwargs))
80 return ret
82 def fcall(self, name, *args, **kwargs):
83 v = getattr(self.node, name)(*args, **kwargs)
84 if v is None and self.parent is not None:
85 return self.parent.fcall(name, *args, **kwargs)
86 else:
87 return v
89 def get(self, name):
90 v = getattr(self.node, name)
91 if v is None and self.parent is not None:
92 return self.parent.get(name)
93 else:
94 return v
96 def dget(self, name, k):
97 v = getattr(self.node, name).get(k, None)
98 if v is None and self.parent is not None:
99 return self.parent.dget(name, k)
100 else:
101 return v
104class OutputFormatChoice(StringChoice):
105 choices = io.allowed_formats('save')
108class OutputDataTypeChoice(StringChoice):
109 choices = ['int32', 'int64', 'float32', 'float64']
110 name_to_dtype = {
111 'int32': num.int32,
112 'int64': num.int64,
113 'float32': num.float32,
114 'float64': num.float64}
117class TraversalChoice(StringChoice):
118 choices = ['network', 'station', 'channel', 'sensor']
119 name_to_grouping = {
120 'network': NetworkGrouping(),
121 'station': StationGrouping(),
122 'sensor': SensorGrouping(),
123 'channel': ChannelGrouping()}
126class InstrumentCorrectionMode(StringChoice):
127 choices = ['complete', 'sensor']
130class Converter(HasPaths):
132 in_dataset = Dataset.T(optional=True)
133 in_path = String.T(optional=True)
134 in_paths = List.T(String.T(optional=True))
136 codes = List.T(CodesNSLCE.T(), optional=True)
138 rename = Dict.T(
139 String.T(),
140 Choice.T([
141 String.T(),
142 Dict.T(String.T(), String.T())]))
143 tmin = Timestamp.T(optional=True)
144 tmax = Timestamp.T(optional=True)
145 tinc = Float.T(optional=True)
147 downsample = Float.T(optional=True)
149 quantity = QuantityType.T(optional=True)
150 fmin = Float.T(optional=True)
151 fmax = Float.T(optional=True)
152 fcut_factor = Float.T(optional=True)
153 fmin_cut = Float.T(optional=True)
154 fmax_cut = Float.T(optional=True)
155 instrument_correction_mode = InstrumentCorrectionMode.T(
156 default='complete')
158 rotate_to_enz = Bool.T(default=False)
160 out_path = Path.T(optional=True)
161 out_sds_path = Path.T(optional=True)
162 out_storage_path = Path.T(optional=True)
163 out_storage_scheme = StorageSchemeChoice.T(default='default')
164 out_format = OutputFormatChoice.T(optional=True)
165 out_data_type = OutputDataTypeChoice.T(optional=True)
166 out_mseed_record_length = IntChoice.T(
167 optional=True,
168 choices=list(io.mseed.VALID_RECORD_LENGTHS))
169 out_mseed_steim = IntChoice.T(
170 optional=True,
171 choices=[1, 2])
172 out_meta_path = Path.T(optional=True)
174 traversal = TraversalChoice.T(optional=True)
176 parts = List.T(Defer('Converter.T'))
178 def get_effective_frequency_taper(self, chain):
179 fmin = chain.get('fmin')
180 fmax = chain.get('fmax')
182 if None in (fmin, fmax):
183 if fmin is not None:
184 raise JackseisError('Converter: fmax not specified.')
185 if fmax is not None:
186 raise JackseisError('Converter: fmin not specified.')
188 return None
190 fcut_factor = chain.get('fcut_factor') or 2.0
191 fmin_cut = chain.get('fmin_cut')
192 fmax_cut = chain.get('fmax_cut')
193 fmin_cut = fmin_cut if fmin_cut is not None else fmin / fcut_factor
194 fmax_cut = fmax_cut if fmax_cut is not None else fmax * fcut_factor
196 return fmin_cut, fmin, fmax, fmax_cut
198 @classmethod
199 def add_arguments(cls, p):
200 p.add_squirrel_query_arguments(without=['time'])
202 p.add_argument(
203 '--tinc',
204 dest='tinc',
205 type=float,
206 metavar='SECONDS',
207 help='Set time length of output files [s].')
209 p.add_argument(
210 '--downsample',
211 dest='downsample',
212 type=float,
213 metavar='RATE',
214 help='Downsample to RATE [Hz].')
216 p.add_argument(
217 '--quantity',
218 dest='quantity',
219 metavar='QUANTITY',
220 choices=QuantityType.choices,
221 help='''
222Restitute waveforms to selected ``QUANTITY``. Restitution is performed by
223multiplying the waveform spectra with a tapered inverse of the instrument
224response transfer function. The frequency band of the taper can be adjusted
225using the ``--band`` option. Choices: %s.
226'''.strip() % ldq(QuantityType.choices))
228 p.add_argument(
229 '--band',
230 metavar='FMIN,FMAX or FMIN,FMAX,CUTFACTOR or '
231 'FMINCUT,FMIN,FMAX,FMAXCUT',
232 help='''
233Frequency band used in restitution (see ``--quantity``) or for (acausal)
234filtering. Waveform spectra are multiplied with a taper with cosine-shaped
235flanks and which is flat between ``FMIN`` and ``FMAX``. The flanks of the taper
236drop to zero at ``FMINCUT`` and ``FMAXCUT``. If ``CUTFACTOR`` is given,
237``FMINCUT`` and ``FMAXCUT`` are set to ``FMIN/CUTFACTOR`` and
238``FMAX*CUTFACTOR`` respectively. ``CUTFACTOR`` defaults to 2.
239'''.strip())
241 p.add_argument(
242 '--instrument-correction-mode',
243 dest='instrument_correction_mode',
244 choices=['complete', 'sensor'],
245 default='complete',
246 help='''
247Select mode of the instrument correction when performing a restition with
248``--quantity``. This option selects which stages of the instrument response
249should be considered completely, i.e. including their frequency dependence, and
250which stages should be considered by only considering their overall gain
251factor. Choices: ``complete`` -- all stages are considered completely
252(default). ``sensor`` -- only the first stage of the insrument response is
253treated completely. The first stage of the instrument response conventionally
254represents the characteristics of the sensor and is usually given in poles and
255zeros representation. The frequency response of the FIR filters of the
256digitizer's downsampling stages are not considered in ``sensor`` mode. Instead,
257replacement gain factors are computed by evaluating the frequency response of
258the respective stages at the lower frequency bound of the restitution ``FMIN``
259(see ``--band``). The assumption here is, that the decimation FIR filters are
260flat at this frequency and representative for the whole pass band.
261'''.strip())
263 p.add_argument(
264 '--rotate-to-enz',
265 action='store_true',
266 dest='rotate_to_enz',
267 help='''
268Rotate waveforms to east-north-vertical (ENZ) coordinate system. The samples
269in the input data must be properly aligned and the channel orientations must
270by set in the station metadata (StationXML). Output channels are renamed with
271last letter replaced by ``E``, ``N``, and ``Z`` respectively.
272'''.strip())
274 p.add_argument(
275 '--out-path',
276 dest='out_path',
277 metavar='TEMPLATE',
278 help='''
279Set output path to ``TEMPLATE``. Available placeholders are ``%%n``: network,
280``%%s``: station, ``%%l``: location, ``%%c``: channel, ``%%b``: begin time,
281``%%e``: end time, ``%%j``: julian day of year. The following additional
282placeholders use the current processing window's begin and end times rather
283than trace begin and end times (to suppress producing many small files for
284gappy traces), ``%%(wmin_year)s``, ``%%(wmin_month)s``, ``%%(wmin_day)s``,
285``%%(wmin)s``, ``%%(wmin_jday)s``, ``%%(wmax_year)s``, ``%%(wmax_month)s``,
286``%%(wmax_day)s``, ``%%(wmax)s``, ``%%(wmax_jday)s``. Example: ``--out-path
287'data/%%s/trace-%%s-%%c.mseed'``
288'''.strip())
290 p.add_argument(
291 '--out-sds-path',
292 dest='out_sds_path',
293 metavar='PATH',
294 help='''
295Set output path to create an SDS archive
296(https://www.seiscomp.de/seiscomp3/doc/applications/slarchive/SDS.html), rooted
297at PATH. Implies ``--tinc 3600`` if not specified otherwise. Equivalent to
298``--out-storage-path PATH --out-storage-scheme sds``. Example:
299``--out-sds-path data/sds``
300'''.strip())
302 p.add_argument(
303 '--out-storage-path',
304 dest='out_storage_path',
305 metavar='PATH',
306 help='''
307Create storage directory under PATH. The storage scheme can be set with
308``--out-storage-scheme``.
309'''.strip())
311 p.add_argument(
312 '--out-storage-scheme',
313 dest='out_storage_scheme',
314 default='default',
315 choices=StorageSchemeChoice.choices,
316 metavar='SCHEME',
317 help='''
318Set storage scheme to produce when using ``--out-storage-path``. Choices: %s
319'''.strip() % ldq(StorageSchemeChoice.choices))
321 p.add_argument(
322 '--out-format',
323 dest='out_format',
324 choices=io.allowed_formats('save'),
325 metavar='FORMAT',
326 help='Set output file format. Choices: %s' % io.allowed_formats(
327 'save', 'cli_help', 'mseed'))
329 p.add_argument(
330 '--out-data-type',
331 dest='out_data_type',
332 choices=OutputDataTypeChoice.choices,
333 metavar='DTYPE',
334 help='Set numerical data type. Choices: %s. The output file '
335 'format must support the given type. By default, the data '
336 'type is kept unchanged.' % ldq(
337 OutputDataTypeChoice.choices))
339 p.add_argument(
340 '--out-mseed-record-length',
341 dest='out_mseed_record_length',
342 type=int,
343 choices=io.mseed.VALID_RECORD_LENGTHS,
344 metavar='INT',
345 help='Set the Mini-SEED record length in bytes. Choices: %s. '
346 'Default is 4096 bytes, which is commonly used for archiving.'
347 % ldq(str(b) for b in io.mseed.VALID_RECORD_LENGTHS))
349 p.add_argument(
350 '--out-mseed-steim',
351 dest='out_mseed_steim',
352 type=int,
353 choices=(1, 2),
354 metavar='INT',
355 help='Set the Mini-SEED STEIM compression. Choices: ``1`` or '
356 '``2``. Default is STEIM-2. Note: STEIM-2 is limited to 30 '
357 'bit dynamic range.')
359 p.add_argument(
360 '--out-meta-path',
361 dest='out_meta_path',
362 metavar='PATH',
363 help='Set output path for station metadata (StationXML) export.')
365 p.add_argument(
366 '--traversal',
367 dest='traversal',
368 metavar='GROUPING',
369 choices=TraversalChoice.choices,
370 help='By default the outermost processing loop is over time. '
371 'Add outer loop with given GROUPING. Choices: %s'
372 % ldq(TraversalChoice.choices))
374 p.add_argument(
375 '--rename-network',
376 dest='rename_network',
377 metavar='REPLACEMENT',
378 help="""
379Replace network code. REPLACEMENT can be a string for direct replacement, a
380mapping for selective replacement, or a regular expression for tricky
381replacements. Examples: Direct replacement: ```XX``` - set all network codes to
382```XX```. Mapping: ```AA:XX,BB:YY``` - replace ```AA``` with ```XX``` and
383```BB``` with ```YY```. Regular expression: ```/A(\\d)/X\\1/``` - replace
384```A1``` with ```X1``` and ```A2``` with ```X2``` etc.
385""".strip())
387 p.add_argument(
388 '--rename-station',
389 dest='rename_station',
390 metavar='REPLACEMENT',
391 help='Replace station code. See ``--rename-network``.')
393 p.add_argument(
394 '--rename-location',
395 dest='rename_location',
396 metavar='REPLACEMENT',
397 help='Replace location code. See ``--rename-network``.')
399 p.add_argument(
400 '--rename-channel',
401 dest='rename_channel',
402 metavar='REPLACEMENT',
403 help='Replace channel code. See ``--rename-network``.')
405 p.add_argument(
406 '--rename-extra',
407 dest='rename_extra',
408 metavar='REPLACEMENT',
409 help='Replace extra code. See ``--rename-network``. Note: the '
410 '```extra``` code is not available in Mini-SEED.')
412 @classmethod
413 def from_arguments(cls, args):
414 kwargs = args.squirrel_query
416 rename = {}
417 for (k, v) in [
418 ('network', args.rename_network),
419 ('station', args.rename_station),
420 ('location', args.rename_location),
421 ('channel', args.rename_channel),
422 ('extra', args.rename_extra)]:
424 if v is not None:
425 rename[k] = parse_rename_rule_from_string(v)
427 if args.band:
428 try:
429 values = list(map(float, args.band.split(',')))
430 if len(values) not in (2, 3, 4):
431 raise ValueError()
433 if len(values) == 2:
434 dset(kwargs, 'fmin fmax'.split(), values)
435 elif len(values) == 3:
436 dset(kwargs, 'fmin fmax fcut_factor'.split(), values)
437 elif len(values) == 4:
438 dset(kwargs, 'fmin_cut fmin fmax fmax_cut'.split(), values)
440 except ValueError:
441 raise JackseisError(
442 'Invalid argument to --band: %s' % args.band) from None
444 obj = cls(
445 downsample=args.downsample,
446 quantity=args.quantity,
447 instrument_correction_mode=args.instrument_correction_mode,
448 rotate_to_enz=args.rotate_to_enz,
449 out_format=args.out_format,
450 out_path=args.out_path,
451 tinc=args.tinc,
452 out_sds_path=args.out_sds_path,
453 out_storage_path=args.out_storage_path,
454 out_storage_scheme=args.out_storage_scheme,
455 out_data_type=args.out_data_type,
456 out_mseed_record_length=args.out_mseed_record_length,
457 out_mseed_steim=args.out_mseed_steim,
458 out_meta_path=args.out_meta_path,
459 traversal=args.traversal,
460 rename=rename,
461 **kwargs)
463 obj.validate()
464 return obj
466 def add_dataset(self, sq):
467 if self.in_dataset is not None:
468 sq.add_dataset(self.in_dataset)
470 if self.in_path is not None:
471 ds = Dataset(sources=[LocalData(paths=[self.in_path])])
472 ds.set_basepath_from(self)
473 sq.add_dataset(ds)
475 if self.in_paths:
476 ds = Dataset(sources=[LocalData(paths=self.in_paths)])
477 ds.set_basepath_from(self)
478 sq.add_dataset(ds)
480 def get_effective_rename_rules(self, chain):
481 d = {}
482 for k in ['network', 'station', 'location', 'channel']:
483 v = chain.dget('rename', k)
484 if isinstance(v, str):
485 m = re.match(r'/([^/]+)/([^/]*)/', v)
486 if m:
487 try:
488 v = (re.compile(m.group(1)), m.group(2))
489 except Exception:
490 raise JackseisError(
491 'Invalid replacement pattern: /%s/' % m.group(1))
493 d[k] = v
495 return d
497 def get_effective_storage_scheme(self):
498 nset = sum(x is not None for x in (
499 self.out_path,
500 self.out_sds_path,
501 self.out_storage_path))
503 if nset > 1:
504 raise JackseisError(
505 'More than one out of [out_path, out_sds_path, '
506 'out_storage_path] set.')
508 if self.out_path:
509 scheme = StorageScheme(
510 name='custom',
511 layouts=[StorageSchemeLayout(
512 name='custom',
513 path_template=self.expand_path(self.out_path))])
515 elif self.out_sds_path:
516 scheme = clone(get_storage_scheme('sds'))
517 scheme.set_base_path(self.expand_path(self.out_sds_path))
519 elif self.out_storage_path:
520 scheme = clone(get_storage_scheme(self.out_storage_scheme))
521 scheme.set_base_path(self.expand_path(self.out_storage_path))
523 else:
524 scheme = None
526 return scheme
528 def get_effective_out_meta_path(self):
529 if self.out_meta_path is not None:
530 return self.expand_path(self.out_meta_path)
531 else:
532 return None
534 def do_rename(self, rules, tr):
535 rename = {}
536 for k in ['network', 'station', 'location', 'channel']:
537 v = rules.get(k, None)
538 if isinstance(v, str):
539 rename[k] = v
540 elif isinstance(v, dict):
541 try:
542 oldval = getattr(tr, k)
543 rename[k] = v[oldval]
544 except KeyError:
545 raise ToolError(
546 'No mapping defined for %s code "%s".' % (k, oldval))
548 elif isinstance(v, tuple):
549 pat, repl = v
550 oldval = getattr(tr, k)
551 newval, n = pat.subn(repl, oldval)
552 if n:
553 rename[k] = newval
555 tr.set_codes(**rename)
557 def convert(
558 self,
559 squirrel_factory=None,
560 force=False,
561 append=False,
562 overrides=None,
563 chain=None):
565 if chain is None:
566 defaults = clone(g_defaults)
567 defaults.set_basepath_from(self)
568 chain = Chain(defaults)
570 chain = Chain(self, chain)
572 if self.parts:
573 task = make_task('Jackseis parts')
574 for part in task(self.parts):
575 part.convert(
576 squirrel_factory,
577 force=force,
578 append=append,
579 overrides=overrides,
580 chain=chain)
582 del task
584 else:
585 if squirrel_factory is None:
586 sq = Squirrel()
587 else:
588 sq = squirrel_factory()
590 if overrides:
591 chain = Chain(overrides, chain)
593 chain.mcall('add_dataset', sq)
595 tmin = chain.get('tmin')
596 tmax = chain.get('tmax')
597 tinc = chain.get('tinc')
598 codes = chain.get('codes')
599 downsample = chain.get('downsample')
600 rotate_to_enz = chain.get('rotate_to_enz')
601 storage_scheme = chain.fcall('get_effective_storage_scheme')
603 out_format = chain.get('out_format')
605 if storage_scheme and storage_scheme.name in ('sds', 'default'):
606 if tinc is None:
607 logger.warning(
608 'Setting processing time window to 1 hour to fill '
609 '"%s" storage.',
610 storage_scheme.name)
611 tinc = 3600.0
613 else:
614 eps = 1e-6
615 if (86400.0+eps) % tinc > 2.*eps:
616 raise JackseisError(
617 'Day length is not a multiple of the time '
618 'window (--tinc).')
620 if storage_scheme and storage_scheme.format is not None \
621 and out_format != storage_scheme.format:
623 logger.warning(
624 'Setting file format to "%s" to generate storage with '
625 'scheme "%s"' % (
626 storage_scheme.format, storage_scheme.name))
628 out_data_type = chain.get('out_data_type')
630 out_meta_path = chain.fcall('get_effective_out_meta_path')
632 if out_meta_path is not None:
633 sx = sq.get_stationxml(codes=codes, tmin=tmin, tmax=tmax)
634 util.ensuredirs(out_meta_path)
635 sx.dump_xml(filename=out_meta_path)
636 if storage_scheme is None:
637 return
639 target_deltat = None
640 if downsample is not None:
641 target_deltat = 1.0 / float(downsample)
643 save_kwargs = {}
644 if out_format == 'mseed':
645 save_kwargs['record_length'] = chain.get(
646 'out_mseed_record_length')
647 save_kwargs['steim'] = chain.get(
648 'out_mseed_steim')
650 traversal = chain.get('traversal')
651 if traversal is not None:
652 grouping = TraversalChoice.name_to_grouping[traversal]
653 else:
654 grouping = None
656 frequency_taper = self.get_effective_frequency_taper(chain)
658 if frequency_taper is not None:
659 if frequency_taper[0] != 0.0:
660 frequency_taper_tpad = 1.0 / frequency_taper[0]
661 else:
662 if frequency_taper[1] == 0.0:
663 raise JackseisError(
664 'Converter: fmin must be greater than zero.')
666 frequency_taper_tpad = 2.0 / frequency_taper[1]
667 else:
668 frequency_taper_tpad = 0.0
670 quantity = chain.get('quantity')
671 ic_mode = chain.get('instrument_correction_mode')
673 do_transfer = \
674 quantity is not None or frequency_taper is not None
676 tpad = 0.0
677 if target_deltat is not None:
678 tpad += target_deltat * 50.
680 if do_transfer:
681 tpad += frequency_taper_tpad
683 task = None
684 rename_rules = self.get_effective_rename_rules(chain)
685 for batch in sq.chopper_waveforms(
686 tmin=tmin, tmax=tmax, tpad=tpad, tinc=tinc,
687 codes=codes,
688 snap_window=True,
689 grouping=grouping):
691 if task is None:
692 task = make_task(
693 'Jackseis blocks', batch.n * batch.ngroups)
695 tlabel = '%s%s - %s' % (
696 'groups %i / %i: ' % (batch.igroup, batch.ngroups)
697 if batch.ngroups > 1 else '',
698 util.time_to_str(batch.tmin),
699 util.time_to_str(batch.tmax))
701 task.update(batch.i + batch.igroup * batch.n, tlabel)
703 twmin = batch.tmin
704 twmax = batch.tmax
706 traces = batch.traces
708 if target_deltat is not None:
709 downsampled_traces = []
710 for tr in traces:
711 try:
712 tr.downsample_to(
713 target_deltat, snap=True, demean=False,
714 allow_upsample_max=4)
716 downsampled_traces.append(tr)
718 except (trace.TraceTooShort, trace.NoData) as e:
719 logger.warning(str(e))
721 traces = downsampled_traces
723 if do_transfer:
724 restituted_traces = []
725 for tr in traces:
726 try:
727 if quantity is not None:
728 resp = sq.get_response(tr).get_effective(
729 input_quantity=quantity,
730 mode=ic_mode,
731 gain_frequency=frequency_taper[1])
732 else:
733 resp = None
735 restituted_traces.append(tr.transfer(
736 frequency_taper_tpad,
737 frequency_taper,
738 transfer_function=resp,
739 invert=True))
741 except (trace.NoData, trace.TraceTooShort,
742 SquirrelError) as e:
743 logger.warning(str(e))
745 traces = restituted_traces
747 if rotate_to_enz:
748 sensors = sq.get_sensors(
749 tmin=twmin,
750 tmax=twmax,
751 codes=list(set(tr.codes for tr in traces)))
753 rotated_traces = []
754 for sensor in sensors:
755 sensor_traces = [
756 tr for tr in traces
757 if tr.codes.matches(sensor.codes)]
759 rotated_traces.extend(
760 sensor.project_to_enz(sensor_traces))
762 traces = rotated_traces
764 for tr in traces:
765 self.do_rename(rename_rules, tr)
767 if out_data_type:
768 for tr in traces:
769 tr.ydata = tr.ydata.astype(
770 OutputDataTypeChoice.name_to_dtype[out_data_type])
772 chopped_traces = []
773 for tr in traces:
774 try:
775 otr = tr.chop(twmin, twmax, inplace=False)
776 chopped_traces.append(otr)
777 except trace.NoData:
778 pass
780 traces = chopped_traces
781 if storage_scheme is not None:
782 try:
783 g_filenames_all.update(storage_scheme.save(
784 traces,
785 format=out_format,
786 overwrite=force,
787 append=True,
788 check_append=True,
789 check_append_hook=check_append_hook
790 if not append else None,
791 additional=dict(
792 wmin_year=tts(twmin, format='%Y'),
793 wmin_month=tts(twmin, format='%m'),
794 wmin_day=tts(twmin, format='%d'),
795 wmin_jday=tts(twmin, format='%j'),
796 wmin=tts(twmin, format='%Y-%m-%d_%H-%M-%S'),
797 wmax_year=tts(twmax, format='%Y'),
798 wmax_month=tts(twmax, format='%m'),
799 wmax_day=tts(twmax, format='%d'),
800 wmax_jday=tts(twmax, format='%j'),
801 wmax=tts(twmax, format='%Y-%m-%d_%H-%M-%S')),
802 **save_kwargs))
804 except io.FileSaveError as e:
805 raise JackseisError(str(e))
807 else:
808 for tr in traces:
809 print(tr.summary_stats)
811 if task:
812 task.done()
815g_defaults = Converter(
816 out_mseed_record_length=4096,
817 out_format='mseed',
818 out_mseed_steim=2)
821headline = 'Convert waveform archive data.'
824def make_subparser(subparsers):
825 return subparsers.add_parser(
826 'jackseis',
827 help=headline,
828 description=headline)
831def setup(parser):
832 parser.add_squirrel_selection_arguments()
834 parser.add_argument(
835 '--config',
836 dest='config_path',
837 metavar='NAME',
838 help='File containing `jackseis.Converter` settings.')
840 parser.add_argument(
841 '--dump-config',
842 dest='dump_config',
843 action='store_true',
844 default=False,
845 help='''
846Print configuration file snippet representing given command line arguments to
847standard output and exit. Only command line options affecting the conversion
848are included in the dump. Additional ``--config`` settings, data collection and
849data query options are ignored.
850'''.strip())
852 parser.add_argument(
853 '--force',
854 dest='force',
855 action='store_true',
856 default=False,
857 help='Force overwriting of existing files.')
859 parser.add_argument(
860 '--append',
861 dest='append',
862 action='store_true',
863 default=False,
864 help='Append to existing files. This only works for mseed files. '
865 'Checks are preformed to ensure that appended traces have no '
866 'overlap with already existing traces.')
868 Converter.add_arguments(parser)
871def run(parser, args):
872 if args.dump_config:
873 converter = Converter.from_arguments(args)
874 print(converter)
875 sys.exit(0)
877 if args.config_path:
878 try:
879 converters = load_all(filename=args.config_path)
880 except Exception as e:
881 raise ToolError(str(e))
883 for converter in converters:
884 if not isinstance(converter, Converter):
885 raise ToolError(
886 'Config file should only contain '
887 '`jackseis.Converter` objects.')
889 converter.set_basepath(op.dirname(args.config_path))
891 else:
892 converter = Converter()
893 converter.set_basepath('.')
894 converters = [converter]
896 cli_overrides = Converter.from_arguments(args)
897 cli_overrides.set_basepath('.')
899 def squirrel_factory():
900 return args.make_squirrel(check_have_data=not args.config_path)
902 with progress.view():
903 task = make_task('Jackseis jobs')
904 for converter in task(converters):
906 converter.convert(
907 squirrel_factory=squirrel_factory,
908 force=args.force,
909 append=args.append,
910 overrides=cli_overrides)