# Copyright 2017-2023 Tom Eulenfeld, MIT license
"""
Command line interface and main entry point
"""
import argparse
from argparse import SUPPRESS
from copy import deepcopy
import glob
import json
import logging
import logging.config
import sys
import time
import obspy
import yam
from yam.util import (_load_func, create_config,
LOGLEVELS, LOGGING_DEFAULT_CONFIG, ParseError,
ConfigError)
log = logging.getLogger('yam')
log.addHandler(logging.NullHandler())
[docs]class ConfigJSONDecoder(json.JSONDecoder):
[docs] def decode(self, s):
"""Decode JSON config with comments stripped"""
s = '\n'.join(l.split('#', 1)[0] for l in s.split('\n'))
return super(ConfigJSONDecoder, self).decode(s)
def configure_logging(loggingc, verbose=0, loglevel=3, logfile=None):
if loggingc is None:
loggingc = deepcopy(LOGGING_DEFAULT_CONFIG)
if verbose > 3:
verbose = 3
loggingc['handlers']['console']['level'] = LOGLEVELS[verbose]
loggingc['handlers']['console_tqdm']['level'] = LOGLEVELS[verbose]
if logfile is None or loglevel == 0:
del loggingc['handlers']['file']
loggingc['loggers']['yam']['handlers'] = ['console_tqdm']
loggingc['loggers']['py.warnings']['handlers'] = ['console_tqdm']
else:
loggingc['handlers']['file']['level'] = LOGLEVELS[loglevel]
loggingc['handlers']['file']['filename'] = logfile
logging.config.dictConfig(loggingc)
logging.captureWarnings(loggingc.get('capture_warnings', False))
def __get_station(seedid):
"""Station name from seed id"""
st = seedid.rsplit('.', 2)[0]
if st.startswith('.'):
st = st[1:]
return st
def load_inventory(inventory):
try:
if not isinstance(inventory, obspy.Inventory):
if isinstance(inventory, str):
format_ = None
else:
inventory, format_ = inventory
expr = inventory
inventory = None
for fname in glob.glob(expr):
inv2 = obspy.read_inventory(fname, format_)
if inventory is None:
inventory = inv2
else:
inventory += inv2
channels = inventory.get_contents()['channels']
stations = list(set(__get_station(ch) for ch in channels))
log.info('read inventory with %d stations', len(stations))
except Exception:
log.exception('cannot read stations')
return
return inventory
def _get_kwargs(kwargs, id_):
kw = kwargs[id_]
if 'based_on' in kw:
kw2 = kwargs[kw.pop('based_on')]
kw2.update(kw)
kw = kw2
return kw
[docs]def run(command, conf=None, tutorial=False, less_data=False, pdb=False,
**args):
"""Main entry point for a direct call from Python
Example usage:
>>> from yam import run
>>> run(conf='conf.json')
:param command: if ``'create'`` the example configuration is created,
optionally the tutorial data files are downloaded
For all other commands this function loads the configuration
and construct the arguments which are passed to `run2()`
All args correspond to the respective command line and
configuration options.
See the example configuration file for help and possible arguments.
Options in args can overwrite the configuration from the file.
E.g. ``run(conf='conf.json', bla='bla')`` will set bla configuration
value to ``'bla'``.
"""
if pdb:
import traceback, pdb
def info(type, value, tb):
traceback.print_exception(type, value, tb)
print()
pdb.pm()
sys.excepthook = info
if conf in ('None', 'none', 'null', ''):
conf = None
# Copy example files if create_config or tutorial
if command == 'create':
if conf is None:
conf = 'conf.json'
create_config(conf, tutorial=tutorial, less_data=less_data)
return
# Parse config file
if conf:
try:
with open(conf) as f:
conf = json.load(f, cls=ConfigJSONDecoder)
except ValueError as ex:
msg = 'Error while parsing the configuration: %s' % ex
raise ConfigError(msg)
except IOError as ex:
raise ConfigError(ex)
# Populate args with conf, but prefer args
conf.update(args)
args = conf
run2(command, **args)
[docs]def run2(command, io,
logging=None, verbose=0, loglevel=3, logfile=None,
key=None, keys=None, corrid=None, stackid=None, stretchid=None,
correlate=None, stack=None, stretch=None,
**args):
"""
Second main function for unpacking arguments
Initialize logging, load inventory if necessary, load options from
configuration dictionary into args
(for correlate, stack and stretch commands) and run the corresponding
command in `~yam.commands` module. If ``"based_on"`` key is set the
configuration dictionary will be preloaded with the specified
configuration.
:param command: specified subcommand, will call one of
`~yam.commands.start_correlate()`,
`~yam.commands.start_stack()`,
`~yam.commands.start_stretch()`,
`~yam.commands.info()`,
`~yam.commands.load()`,
`~yam.commands.plot()`,
`~yam.commands.remove()`
:param logging,verbose,loglevel,logfile: logging configuration
:param key: the key to work with
:param keys: keys to remove (only remove command)
:param correlate,stack,stretch: corresponding configuration dictionaries
:param \*id: the configuration id to load from the config dictionaries
:param \*\*args: all other arguments are passed to next called function
"""
time_start = time.time()
# Configure logging
if command in ('correlate', 'stack', 'stretch'):
configure_logging(loggingc=logging, verbose=verbose,
loglevel=loglevel, logfile=logfile)
log.info('Yam version %s', yam.__version__)
log.info('do not' * (not yam.correlate._USE_FFTWS) + 'use pyfftw library')
if key is not None and '/' in key:
key, subkey = key.split('/', 1)
subkey = '/' + subkey
else:
subkey = ''
# load data plugin, discard unnecessary io kwargs
data_plugin = io.get('data_plugin')
if command == 'correlate' or (
command in ('print', 'load', 'plot') and
key in ('data', 'prepdata')):
if data_plugin is not None:
modulename, funcname = data_plugin.split(':')
io['data'] = _load_func(modulename.strip(), funcname.strip())
# pop plotting options
if command != 'plot':
for k in list(args.keys()):
if k.startswith('plot_'):
args.pop(k)
if command in ('correlate', 'stack', 'stretch'):
args.setdefault('dataset_kwargs', io.get('dataset_kwargs'))
# Start main routine
if command == 'correlate':
kw = _get_kwargs(correlate, corrid)
kw['outkey'] = 'c' + corrid
args.update(kw)
io['inventory'] = load_inventory(kw.get('inventory') or
io.get('inventory'))
yam.commands.start_correlate(io, **args)
elif command == 'stack':
if stack is not None and stackid in stack:
kw = _get_kwargs(stack, stackid)
else:
kw = {}
if 'm' in stackid:
kw['length'], kw['move'] = stackid.split('m')
elif stackid in ('', 'all', 'None', 'none', 'null'):
stackid = ''
kw['length'] = None
else:
kw['length'] = stackid
kw['outkey'] = key + '_s' + stackid
args.update(kw)
yam.commands.start_stack(io, key, subkey=subkey, **args)
elif command == 'stretch':
kw = _get_kwargs(stretch, stretchid)
kw['outkey'] = key + '_t' + stretchid
args.update(kw)
yam.commands.start_stretch(io, key, subkey=subkey, **args)
elif command == 'remove':
yam.commands.remove(io, keys)
elif command in ('info', 'print', 'load', 'plot', 'export'):
if key == 'stations' or command == 'info' and key is None:
io['inventory'] = load_inventory(io['inventory'])
if key == 'prepdata':
if corrid is None:
msg = 'seed id, day and corrid need to be set for prepdata'
raise ParseError(msg)
kw = _get_kwargs(correlate, corrid)
io['inventory'] = load_inventory(kw.get('inventory') or
io['inventory'])
args['prep_kw'] = kw
if command == 'info':
yam.commands.info(io, key=key, subkey=subkey,
config=(correlate, stack, stretch), **args)
elif command == 'print':
yam.commands.load(io, key=key + subkey, do='print', **args)
elif command == 'load':
yam.commands.load(io, key=key + subkey, do='load', **args)
elif command == 'export':
yam.commands.load(io, key=key + subkey, do='export', **args)
else:
yam.commands.plot(io, key + subkey, corrid=corrid, **args)
elif command == 'scan':
data_glob = yam.commands._get_data_glob(io['data'])
print('Please use the obspy-scan script provided with ObsPy to '
'scan the archive for data availability.')
print('Suggested call to obspy-scan:')
parts = []
if io.get('data_format') is not None:
parts.append('-f ' + io['data_format'])
parts.append(data_glob)
print('obspy-scan ' + ' '.join(['-w scan.npz -o scan.png'] + parts))
print()
print('Please use the obspy-ppsd script provided at '
'https://github.com/trichter/obspy-ppsd to calculate and plot '
'probabilistic power spectral densities.')
print('Suggested call to obspy-ppsd:')
inv = io['inventory']
if '?' in inv or '*' in inv:
inv = '"' + inv + '"'
print(' '.join(['obspy-ppsd add -i', inv] + parts))
print()
print('The calls will not work if you use the data_plugin '
'configuration. '
'For more options check obspy-scan -h and obspy-ppsd -h.')
else:
raise ValueError('Unknown command')
time_end = time.time()
log.debug('used time: %.1fs', time_end - time_start)
[docs]def run_cmdline(args=None):
"""Main entry point from the command line"""
# Define command line arguments
from yam import __version__
msg = ('Yam: Yet another monitoring tool using cross-correlations of '
'ambient noise')
epilog = 'To get help on a subcommand run: yam command -h'
p = argparse.ArgumentParser(description=msg, epilog=epilog)
version = '%(prog)s ' + __version__
p.add_argument('--version', action='version', version=version)
msg = 'configuration file to load (default: conf.json)'
p.add_argument('-c', '--conf', default='conf.json', help=msg)
msg = 'if an exception occurs start the debugger'
p.add_argument('--pdb', action='store_true', help=msg)
sub = p.add_subparsers(title='commands', dest='command')
sub.required = True
msg = 'create config file in current directory'
p_create = sub.add_parser('create', help=msg)
msg = 'preprocess and correlate data'
p_correlate = sub.add_parser('correlate', help=msg)
msg = 'stack correlations or stacked correlations'
p_stack = sub.add_parser('stack', help=msg)
msg = 'stretch correlations or stacked correlations'
p_stretch = sub.add_parser('stretch', help=msg)
msg = 'remove keys from HDF5 files'
p_remove = sub.add_parser('remove', help=msg)
msg = 'print information about project or objects'
p_info = sub.add_parser('info', help=msg)
msg = 'print objects'
p_print = sub.add_parser('print', help=msg)
msg = ('print suggested call for obspy-scan script (data availability) '
'and obspy-ppsd script (probabilistic power spectral densities)')
p_scan = sub.add_parser('scan', help=msg)
msg = 'plot objects'
p_plot = sub.add_parser('plot', help=msg)
msg = 'load objects into IPython session'
p_load = sub.add_parser('load', help=msg)
msg = ('export corrrelations or stacks to another file format '
'for processing with other programs, '
'preprocessed data can also be saved')
p_export = sub.add_parser('export', help=msg)
msg = 'additionaly create example files for tutorial'
p_create.add_argument('--tutorial', help=msg, action='store_true')
p_create.add_argument('--less-data', help=SUPPRESS, action='store_true')
msg = 'use the data defined by this key (processing chain)'
p_stack.add_argument('key', help=msg)
p_stretch.add_argument('key', help=msg)
msg = 'keys of data to be removed'
p_remove.add_argument('keys', help=msg, nargs='+')
msg = 'configuration key for correlation'
p_correlate.add_argument('corrid', help=msg)
msg = 'configuration key or explicit configuration for stacking'
p_stack.add_argument('stackid', help=msg)
msg = 'configuration key for stretching'
p_stretch.add_argument('stretchid', help=msg)
msg = 'key of reference trace (stack)'
p_stretch.add_argument('--reftrid', help=msg)
msg1 = 'more detailed information about '
msg2 = ('stations, data or specific processing key '
'(e.g. stations, data, '
'c1_s1d, c1_s1d_t1/CX.PB06-CX.PB06/.BHZ-.BHZ)')
p_info.add_argument('key', help=msg1 + msg2, nargs='?')
msg1 = 'print contents of '
msg2 = ('stations, data, preprocessed data or specific processing key '
'(e.g. stations, data, prepdata, '
'c1_s1d, c1_s1d_t1/CX.PB06-CX.PB06/.BHZ-.BHZ)')
p_print.add_argument('key', help=msg1 + msg2)
msg1 = 'load IPython session with contents of '
p_load.add_argument('key', help=msg1 + msg2)
msg1 = 'plot '
p_plot.add_argument('key', help=msg1 + msg2)
msg1 = 'export '
msg2 = msg2.replace('stations, ', '').replace('_t1', '')
p_export.add_argument('key', help=msg1 + msg2)
p_export.add_argument('fname', help='filename')
for p2 in (p_print, p_load, p_plot, p_export):
msg = 'seed id (only for data or prepdata, e.g. CX.PATCX..BHZ)'
p2.add_argument('seedid', help=msg, nargs='?', default=SUPPRESS)
msg = 'day (only for data or prepdata, e.g. 2010-02-03, 2010-035)'
p2.add_argument('day', help=msg, nargs='?', default=SUPPRESS)
msg = 'configuration key for correlation (only for prepdata)'
p2.add_argument('corrid', help=msg, nargs='?', default=SUPPRESS)
for p2 in (p_correlate, p_stack, p_stretch):
msg = 'Set chattiness on command line. Up to 3 -v flags are possible'
p2.add_argument('-v', '--verbose', help=msg, action='count',
default=SUPPRESS)
for p2 in (p_correlate, p_stack, p_stretch):
msg = ('Number of cores to use (default: all), '
'only applies to some commands')
p2.add_argument('-n', '--njobs', default=None, type=int, help=msg)
msg = ('Run inner loops parallel instead of outer loop '
'(preproccessing of different stations and correlation of different '
'pairs versus processing of different days). '
'Useful for a datset with many stations.')
p_correlate.add_argument('--parallel-inner-loop', action='store_true',
help=msg)
msg = ('type of plot (for correlations, default is versus time, '
'for stretching results, default is similarity matrix, '
'other options: vs_dist - wiggles vs distance, wiggle - wiggles vs time, '
'velocity - joint velocity estimate vs time')
choices = ('vs_dist', 'wiggle', 'velocity')
p_plot.add_argument('--plottype', help=msg, choices=choices)
msg = 'specify some plot options (dictionary in JSON format)'
p_plot.add_argument('--plot-options', default=SUPPRESS, type=json.loads,
help=msg)
p_plot.add_argument('--show', action='store_true', default=SUPPRESS,
help='show figures')
p_plot.add_argument('--no-show', dest='show',
action='store_false', default=SUPPRESS,
help='do not show figures')
msg = 'format supported by ObsPy (default: auto-detected by extension)'
p_export.add_argument('-f', '--format', help=msg)
# Get command line arguments and start run function
args = vars(p.parse_args(args))
try:
run(**args)
except ParseError as ex:
p.error(ex)
except ConfigError as ex:
print(ex, file=sys.stderr)
sys.exit(1)