Source code for pyrocko.apps.fomosto

# http://pyrocko.org - GPLv3
#
# The Pyrocko Developers, 21st Century
# ---|P------/S----------~Lg----------

'''
Fomosto - Green's function database management.
'''

import sys
import re
import os.path as op
import logging
import copy
import shutil
from optparse import OptionParser

from pyrocko import util, trace, gf, cake, io, fomosto
from pyrocko.gui.snuffler import marker
from pyrocko.util import mpl_show

logger = logging.getLogger('pyrocko.apps.fomosto')
km = 1e3


def d2u(d):
    return dict((k.replace('-', '_'), v) for (k, v) in d.items())


subcommand_descriptions = {
    'init':          'create a new empty GF store',
    'build':         'compute GFs and fill into store',
    'stats':         'print information about a GF store',
    'check':         'check for problems in GF store',
    'decimate':      'build decimated variant of a GF store',
    'redeploy':      'copy traces from one GF store into another',
    'view':          'view selected traces',
    'extract':       'extract selected traces',
    'import':        'convert Kiwi GFDB to GF store format',
    'export':        'convert GF store to Kiwi GFDB format',
    'ttt':           'create travel time tables',
    'tttview':       'plot travel time table',
    'tttextract':    'extract selected travel times',
    'tttlsd':        'fix holes in travel time tables',
    'sat':           'create stored ray attribute table',
    'satview':       'plot stored ray attribute table',
    'server':        'run seismosizer server',
    'download':      'download GF store from a server',
    'modelview':     'plot earthmodels',
    'upgrade':       'upgrade store format to latest version',
    'addref':        'import citation references to GF store config',
    'qc':            'quality check',
    'report':        "report for Green's Function databases",
    'trans2rot':     'create rotational from translational store',
    'fd2trans':      'extract elastic10 from elastic10_fd store',
}

subcommand_usages = {
    'init':          ['init <type> <store-dir> [options]',
                      'init redeploy <source> <destination> [options]'],
    'build':         'build [store-dir] [options]',
    'stats':         'stats [store-dir] [options]',
    'check':         'check [store-dir] [options]',
    'decimate':      'decimate [store-dir] <factor> [options]',
    'redeploy':      'redeploy <source> <destination> [options]',
    'view':          'view [store-dir] ... [options]',
    'extract':       'extract [store-dir] <selection>',
    'import':        'import <source> <destination> [options]',
    'export':        'export [store-dir] <destination> [options]',
    'ttt':           'ttt [store-dir] [options]',
    'tttview':       'tttview [store-dir] <phase-ids> [options]',
    'tttextract':    'tttextract [store-dir] <phase> <selection>',
    'tttlsd':        'tttlsd [store-dir] <phase>',
    'sat':           'sat [store-dir] [options]',
    'satview':       'satview [store-dir] <phase-ids> [options]',
    'server':        'server [options] <store-super-dir> ...',
    'download':      'download [options] <site> <store-id>',
    'modelview':     'modelview <selection>',
    'upgrade':       'upgrade [store-dir] ...',
    'addref':        'addref [store-dir] ... <filename> ...',
    'qc':            'qc [store-dir]',
    'report':        'report <subcommand> <arguments>... [options]',
    'trans2rot':     'trans2rot <source> <destination>',
    'fd2trans':      'fd2trans <source> <destination>',
}

subcommands = subcommand_descriptions.keys()

program_name = 'fomosto'

usage = program_name + ''' <subcommand> <arguments> ... [options]

Subcommands:

    init          %(init)s
    build         %(build)s
    stats         %(stats)s
    check         %(check)s
    decimate      %(decimate)s
    redeploy      %(redeploy)s
    view          %(view)s
    extract       %(extract)s
    import        %(import)s
    export        %(export)s
    ttt           %(ttt)s
    tttview       %(tttview)s
    tttextract    %(tttextract)s
    tttlsd        %(tttlsd)s
    sat           %(sat)s
    satview       %(satview)s
    server        %(server)s
    download      %(download)s
    modelview     %(modelview)s
    upgrade       %(upgrade)s
    addref        %(addref)s
    qc            %(qc)s
    report        %(report)s
    trans2rot     %(trans2rot)s
    fd2trans      %(fd2trans)s

To get further help and a list of available options for any subcommand run:

    fomosto <subcommand> --help

''' % d2u(subcommand_descriptions)


def add_common_options(parser):
    parser.add_option(
        '--loglevel',
        action='store',
        dest='loglevel',
        type='choice',
        choices=('critical', 'error', 'warning', 'info', 'debug'),
        default='info',
        help='set logger level to '
             '"critical", "error", "warning", "info", or "debug". '
             'Default is "%default".')


def process_common_options(options):
    util.setup_logging(program_name, options.loglevel)


def cl_parse(command, args, setup=None, details=None):
    usage = subcommand_usages[command]
    descr = subcommand_descriptions[command]

    if isinstance(usage, str):
        usage = [usage]

    susage = '%s %s' % (program_name, usage[0])
    for s in usage[1:]:
        susage += '\n%s%s %s' % (' '*7, program_name, s)

    description = descr[0].upper() + descr[1:] + '.'

    if details:
        description = description + ' %s' % details

    parser = OptionParser(usage=susage, description=description)
    parser.format_description = lambda formatter: description

    if setup:
        setup(parser)

    add_common_options(parser)
    (options, args) = parser.parse_args(args)
    process_common_options(options)
    return parser, options, args


def die(message, err=''):
    sys.exit('%s: error: %s \n %s' % (program_name, message, err))


def fomo_wrapper_module(name):
    try:
        if not re.match(gf.meta.StringID.pattern, name):
            raise ValueError('invalid name')

        words = name.split('.', 1)
        if len(words) == 2:
            name, variant = words
        else:
            name = words[0]
            variant = None

        name_clean = re.sub(r'[.-]', '_', name)
        modname = '.'.join(['pyrocko', 'fomosto', name_clean])
        mod = __import__(modname, level=0)
        return getattr(mod.fomosto, name_clean), variant

    except ValueError:
        die('invalid modelling code wrapper name')

    except ImportError:
        die('''modelling code wrapper "%s" not available or not installed
                (module probed: "%s")''' % (name, modname))


def command_init(args):

    details = '''

Available modelling backends:
%s

  More information at
    https://pyrocko.org/docs/current/apps/fomosto/backends.html
''' % '\n'.join(['  * %s' % b for b in fomosto.AVAILABLE_BACKENDS])

    parser, options, args = cl_parse(
        'init', args,
        details=details)

    if len(args) == 0:
        sys.exit(parser.format_help())

    if args[0] == 'redeploy':
        if len(args) != 3:
            parser.error('incorrect number of arguments')

        source_dir, dest_dir = args[1:]

        try:
            source = gf.Store(source_dir)
        except gf.StoreError as e:
            die(e)

        config = copy.deepcopy(source.config)
        config.derived_from_id = source.config.id
        try:
            config_filenames = gf.store.Store.create_editables(
                dest_dir, config=config)

        except gf.StoreError as e:
            die(e)

        try:
            dest = gf.Store(dest_dir)
        except gf.StoreError as e:
            die(e)

        for k in source.extra_keys():
            source_fn = source.get_extra_path(k)
            dest_fn = dest.get_extra_path(k)
            shutil.copyfile(source_fn, dest_fn)

        logger.info(
            '(1) configure settings in files:\n  %s'
            % '\n  '.join(config_filenames))

        logger.info(
            '(2) run "fomosto redeploy <source> <dest>", as needed')

    else:
        if len(args) != 2:
            parser.error('incorrect number of arguments')

        (modelling_code_id, store_dir) = args

        module, variant = fomo_wrapper_module(modelling_code_id)
        try:
            config_filenames = module.init(store_dir, variant)
        except gf.StoreError as e:
            die(e)

        logger.info('(1) configure settings in files:\n  %s'
                    % '\n  '.join(config_filenames))
        logger.info('(2) run "fomosto ttt" in directory "%s"' % store_dir)
        logger.info('(3) run "fomosto build" in directory "%s"' % store_dir)


def get_store_dir(args):
    if len(args) == 1:
        store_dir = op.abspath(args.pop(0))
    else:
        store_dir = op.abspath(op.curdir)

    if not op.isdir(store_dir):
        die('not a directory: %s' % store_dir)

    return store_dir


def get_store_dirs(args):
    if len(args) == 0:
        store_dirs = [op.abspath(op.curdir)]
    else:
        store_dirs = [op.abspath(x) for x in args]

    for store_dir in store_dirs:
        if not op.isdir(store_dir):
            die('not a directory: %s' % store_dir)

    return store_dirs


def command_build(args):

    def setup(parser):
        parser.add_option(
            '--force', dest='force', action='store_true',
            help='overwrite existing files')

        parser.add_option(
            '--nworkers', dest='nworkers', type='int', metavar='N',
            help='run N worker processes in parallel')

        parser.add_option(
            '--continue', dest='continue_', action='store_true',
            help='continue suspended build')

        parser.add_option(
            '--step', dest='step', type='int', metavar='I',
            help='process block number IBLOCK')

        parser.add_option(
            '--block', dest='iblock', type='int', metavar='I',
            help='process block number IBLOCK')

    parser, options, args = cl_parse('build', args, setup=setup)

    store_dir = get_store_dir(args)
    try:
        if options.step is not None:
            step = options.step - 1
        else:
            step = None

        if options.iblock is not None:
            iblock = options.iblock - 1
        else:
            iblock = None

        store = gf.Store(store_dir)
        module, _ = fomo_wrapper_module(store.config.modelling_code_id)
        module.build(
            store_dir,
            force=options.force,
            nworkers=options.nworkers, continue_=options.continue_,
            step=step,
            iblock=iblock)

    except gf.StoreError as e:
        die(e)


def command_stats(args):

    parser, options, args = cl_parse('stats', args)
    store_dir = get_store_dir(args)

    try:
        store = gf.Store(store_dir)
        s = store.stats()

    except gf.StoreError as e:
        die(e)

    for k in store.stats_keys:
        print('%s: %s' % (k, s[k]))


def command_check(args):

    parser, options, args = cl_parse('check', args)
    store_dir = get_store_dir(args)

    try:
        store = gf.Store(store_dir)
        problems = store.check(show_progress=True)
        if problems:
            die('problems detected with gf store: %s' % store_dir)

    except gf.StoreError as e:
        die(e)


def load_config(fn):
    try:
        config = gf.meta.load(filename=fn)
        assert isinstance(config, gf.Config)

    except Exception:
        die('cannot load gf config from file: %s' % fn)

    return config


def command_decimate(args):

    def setup(parser):
        parser.add_option(
            '--config', dest='config_fn', metavar='FILE',
            help='use modified spacial sampling given in FILE')

        parser.add_option(
            '--force', dest='force', action='store_true',
            help='overwrite existing files')

    parser, options, args = cl_parse('decimate', args, setup=setup)
    try:
        decimate = int(args.pop())
    except Exception:
        parser.error('cannot get <factor> argument')

    store_dir = get_store_dir(args)

    config = None
    if options.config_fn:
        config = load_config(options.config_fn)

    try:
        store = gf.Store(store_dir)
        store.make_decimated(decimate, config=config, force=options.force,
                             show_progress=True)

    except gf.StoreError as e:
        die(e)


def sindex(args):
    return '(%s)' % ', '.join('%g' % x for x in args)


def command_redeploy(args):

    parser, options, args = cl_parse('redeploy', args)

    if not len(args) == 2:
        sys.exit(parser.format_help())

    source_store_dir, dest_store_dir = args

    try:
        source = gf.Store(source_store_dir)
    except gf.StoreError as e:
        die(e)

    try:
        gf.store.Store.create_dependants(dest_store_dir)
    except gf.StoreError:
        pass

    try:
        dest = gf.Store(dest_store_dir, 'w')

    except gf.StoreError as e:
        die(e)

    show_progress = True

    try:
        if show_progress:
            pbar = util.progressbar('redeploying', dest.config.nrecords)

        for i, args in enumerate(dest.config.iter_nodes()):
            try:
                tr = source.get(args, interpolation='off')
                dest.put(args, tr)

            except (gf.meta.OutOfBounds, gf.store.NotAllowedToInterpolate) as e:  # noqa
                logger.debug('skipping %s, (%s)' % (sindex(args), e))

            except gf.store.StoreError as e:
                logger.warning('cannot insert %s, (%s)' % (sindex(args), e))

            if show_progress:
                pbar.update(i+1)

    finally:
        if show_progress:
            pbar.finish()


def command_trans2rot(args):
    from pyrocko.fomosto.elastic10_to_rotational8 \
        import elastic10_to_rotational8

    details = '''

Create a rotational GF store from a translational one using finite differences
approximations (elastic10 or elastic10_fd to rotational8).

If the source store is of type A and receivers at the surface, the free
surface condition is used to replace the vertical derivatives with horizontal
ones. If the input is of type B, the complete set of derivatives are used. It
is important that the spatial sampling of the source store is high enough so
that the finite difference approximations are sufficiently accurate.

If the destination store directory does not exist, it is created and the extent
and spacing of the source store is used for the rotational store. If it does
exist and contains a valid `config`, it is used in place in order to allow for
different input and output extents and spacings. The store must be empty in the
latter case (delete the `index` and `traces` files if needed).
'''

    parser, options, args = cl_parse(
        'trans2rot', args, details=details)

    if not len(args) == 2:
        sys.exit(parser.format_help())

    source_store_dir, dest_store_dir = args

    try:
        elastic10_to_rotational8(source_store_dir, dest_store_dir)
    except gf.store.StoreError as e:
        die(e)


def command_fd2trans(args):
    from pyrocko.fomosto.elastic10_fd_to_elastic10 \
        import elastic10_fd_to_elastic10

    details = '''

Extract regular GF store (elastic10) from a finite difference support GF store
(elastic10_fd).
'''

    parser, options, args = cl_parse(
        'fd2trans', args, details=details)

    if not len(args) == 2:
        sys.exit(parser.format_help())

    source_store_dir, dest_store_dir = args

    try:
        elastic10_fd_to_elastic10(source_store_dir, dest_store_dir)
    except gf.store.StoreError as e:
        die(e)


def command_view(args):
    def setup(parser):
        parser.add_option(
            '--extract',
            dest='extract',
            metavar='start:stop[:step|@num],...',
            help='specify which traces to show')

        parser.add_option(
            '--show-phases',
            dest='showphases',
            default=None,
            metavar='[phase_id1,phase_id2,...|all]',
            help='add phase markers from ttt')

        parser.add_option(
            '--opengl',
            dest='opengl',
            action='store_true',
            default=None,
            help='use OpenGL for drawing')

        parser.add_option(
            '--no-opengl',
            dest='opengl',
            action='store_false',
            default=None,
            help='do not use OpenGL for drawing')

    parser, options, args = cl_parse('view', args, setup=setup)

    gdef = None
    if options.extract:
        try:
            gdef = gf.meta.parse_grid_spec(options.extract)
        except gf.meta.GridSpecError as e:
            die(e)

    store_dirs = get_store_dirs(args)

    alpha = 'abcdefghijklmnopqrstxyz'.upper()

    markers = []
    traces = []

    try:
        for istore, store_dir in enumerate(store_dirs):
            store = gf.Store(store_dir)
            if options.showphases == 'all':
                phasenames = [pn.id for pn in store.config.tabulated_phases]
            elif options.showphases is not None:
                phasenames = options.showphases.split(',')

            ii = 0
            for args in store.config.iter_extraction(gdef):
                gtr = store.get(args)

                loc_code = ''
                if len(store_dirs) > 1:
                    loc_code = alpha[istore % len(alpha)]

                if gtr:

                    sta_code = '%04i (%s)' % (
                        ii, ','.join('%gk' % (x/1000.) for x in args[:-1]))
                    tmin = gtr.deltat * gtr.itmin

                    tr = trace.Trace(
                        '',
                        sta_code,
                        loc_code,
                        '%02i' % args[-1],
                        ydata=gtr.data,
                        deltat=gtr.deltat,
                        tmin=tmin)

                    if options.showphases:
                        for phasename in phasenames:
                            phase_tmin = store.t(phasename, args[:-1])
                            if phase_tmin:
                                m = marker.PhaseMarker(
                                    [('',
                                      sta_code,
                                      loc_code,
                                      '%02i' % args[-1])],
                                    phase_tmin,
                                    phase_tmin,
                                    0,
                                    phasename=phasename)
                                markers.append(m)

                    traces.append(tr)

                ii += 1

    except (gf.meta.GridSpecError, gf.StoreError, gf.meta.OutOfBounds) as e:
        die(e)

    def setup(win):
        viewer = win.get_view()
        viewer.menuitem_showboxes.setChecked(False)
        viewer.menuitem_colortraces.setChecked(True)
        viewer.menuitem_demean.setChecked(False)
        viewer.menuitems_sorting[5][0].setChecked(True)
        viewer.menuitems_scaling[2][0].setChecked(True)
        viewer.sortingmode_change()
        viewer.scalingmode_change()

    trace.snuffle(
        traces, markers=markers, opengl=options.opengl, launch_hook=setup)


def command_extract(args):
    def setup(parser):
        parser.add_option(
            '--format', dest='format', default='mseed',
            choices=['mseed', 'sac', 'text', 'yaff'],
            help='export to format "mseed", "sac", "text", or "yaff". '
                 'Default is "mseed".')

        fndfl = 'extracted/%(irecord)s_%(args)s.%(extension)s'
        parser.add_option(
            '--output', dest='output_fn', default=fndfl, metavar='TEMPLATE',
            help='output path template [default: "%s"]' % fndfl)

    parser, options, args = cl_parse('extract', args, setup=setup)
    try:
        sdef = args.pop()
    except Exception:
        parser.error('cannot get <selection> argument')

    try:
        gdef = gf.meta.parse_grid_spec(sdef)
    except gf.meta.GridSpecError as e:
        die(e)

    store_dir = get_store_dir(args)

    extensions = {
        'mseed': 'mseed',
        'sac': 'sac',
        'text': 'txt',
        'yaff': 'yaff'}

    try:
        store = gf.Store(store_dir)
        for args in store.config.iter_extraction(gdef):
            gtr = store.get(args)
            if gtr:
                tr = trace.Trace(
                    '', '', '', util.zfmt(store.config.ncomponents) % args[-1],
                    ydata=gtr.data,
                    deltat=gtr.deltat,
                    tmin=gtr.deltat * gtr.itmin)

                additional = dict(
                    args='_'.join('%g' % x for x in args),
                    irecord=store.str_irecord(args),
                    extension=extensions[options.format])

                io.save(
                    tr,
                    options.output_fn,
                    format=options.format,
                    additional=additional)

    except (gf.meta.GridSpecError, gf.StoreError, gf.meta.OutOfBounds) as e:
        die(e)


def command_import(args):
    try:
        from tunguska import gfdb
    except ImportError:
        die('the kiwi tools must be installed to use this feature')

    parser, options, args = cl_parse('import', args)

    show_progress = True

    if not len(args) == 2:
        sys.exit(parser.format_help())

    source_path, dest_store_dir = args

    if op.isdir(source_path):
        source_path = op.join(source_path, 'db')

    source_path = re.sub(r'(\.\d+\.chunk|\.index)$', '', source_path)

    db = gfdb.Gfdb(source_path)

    config = gf.meta.ConfigTypeA(
        id='imported_gfs',
        distance_min=db.firstx,
        distance_max=db.firstx + (db.nx-1) * db.dx,
        distance_delta=db.dx,
        source_depth_min=db.firstz,
        source_depth_max=db.firstz + (db.nz-1) * db.dz,
        source_depth_delta=db.dz,
        sample_rate=1.0/db.dt,
        ncomponents=db.ng
    )

    try:
        gf.store.Store.create(dest_store_dir, config=config)
        dest = gf.Store(dest_store_dir, 'w')
        try:
            if show_progress:
                pbar = util.progressbar(
                    'importing', dest.config.nrecords/dest.config.ncomponents)

            for i, args in enumerate(dest.config.iter_nodes(level=-1)):
                source_depth, distance = [float(x) for x in args]
                traces = db.get_traces_pyrocko(distance, source_depth)
                ig_to_trace = dict((tr.meta['ig']-1, tr) for tr in traces)
                for ig in range(db.ng):
                    if ig in ig_to_trace:
                        tr = ig_to_trace[ig]
                        gf_tr = gf.store.GFTrace(
                            tr.get_ydata(),
                            int(round(tr.tmin / tr.deltat)),
                            tr.deltat)

                    else:
                        gf_tr = gf.store.Zero

                    dest.put((source_depth, distance, ig), gf_tr)

                if show_progress:
                    pbar.update(i+1)

        finally:
            if show_progress:
                pbar.finish()

        dest.close()

    except gf.StoreError as e:
        die(e)


def command_export(args):
    from subprocess import Popen, PIPE

    try:
        from tunguska import gfdb
    except ImportError as err:
        die('the kiwi tools must be installed to use this feature', err)

    def setup(parser):
        parser.add_option(
            '--nchunks', dest='nchunks', type='int', default=1, metavar='N',
            help='split output gfdb into N chunks')

    parser, options, args = cl_parse('export', args, setup=setup)

    show_progress = True

    if len(args) not in (1, 2):
        sys.exit(parser.format_help())

    target_path = args.pop()
    if op.isdir(target_path):
        target_path = op.join(target_path, 'kiwi_gfdb')
        logger.warning('exported gfdb will be named as "%s.*"' % target_path)

    source_store_dir = get_store_dir(args)

    source = gf.Store(source_store_dir, 'r')
    config = source.config

    if not isinstance(config, gf.meta.ConfigTypeA):
        die('only stores of type A can be exported to Kiwi format')

    if op.isfile(target_path + '.index'):
        die('destation already exists')

    cmd = [str(x) for x in [
        'gfdb_build',
        target_path,
        options.nchunks,
        config.ndistances,
        config.nsource_depths,
        config.ncomponents,
        config.deltat,
        config.distance_delta,
        config.source_depth_delta,
        config.distance_min,
        config.source_depth_min]]

    p = Popen(cmd, stdin=PIPE)
    p.communicate()

    out_db = gfdb.Gfdb(target_path)

    try:
        if show_progress:
            pbar = util.progressbar(
                'exporting', config.nrecords/config.ncomponents)

        for i, (z, x) in enumerate(config.iter_nodes(level=-1)):

            data_out = []
            for ig in range(config.ncomponents):
                try:
                    tr = source.get((z, x, ig), interpolation='off')
                    data_out.append((tr.t, tr.data * config.factor))

                except gf.store.StoreError as e:
                    logger.warning(
                        'cannot get %s, (%s)' % (sindex((z, x, ig)), e))
                    data_out.append(None)

            # put a zero valued sample to no-data zero-traces at a compatible
            # time
            tmins = [
                entry[0][0]
                for entry in data_out
                if entry is not None and entry[0].size != 0]

            if tmins:
                tmin = min(tmins)
                for entry in data_out:
                    if entry is not None and entry[0].size == 0:
                        entry[0].resize(1)
                        entry[1].resize(1)
                        entry[0][0] = tmin
                        entry[1][0] = 0.0

            out_db.put_traces_slow(x, z, data_out)

            if show_progress:
                pbar.update(i+1)

        source.close()

    finally:
        if show_progress:
            pbar.finish()


def phasedef_or_horvel(x):
    try:
        return float(x)
    except ValueError:
        return cake.PhaseDef(x)


def mkp(s):
    return [phasedef_or_horvel(ps) for ps in s.split(',')]


def stored_attribute_table_plots(phase_ids, options, args, attribute):
    import numpy as num
    from pyrocko.plot.cake_plot import labelspace, xscaled, yscaled, mpl_init

    plt = mpl_init()

    np = 1
    store_dir = get_store_dir(args)
    for phase_id in phase_ids:
        try:
            store = gf.Store(store_dir)

            if options.receiver_depth is not None:
                receiver_depth = options.receiver_depth * 1000.0
            else:
                if isinstance(store.config, (gf.ConfigTypeA, gf.ConfigTypeC)):
                    receiver_depth = store.config.receiver_depth

                elif isinstance(store.config, gf.ConfigTypeB):
                    receiver_depth = store.config.receiver_depth_min

                else:
                    receiver_depth = 0.0

            phase = store.get_stored_phase(phase_id, attribute)
            axes = plt.subplot(2, len(phase_ids), np)
            labelspace(axes)
            xscaled(1. / km, axes)
            yscaled(1. / km, axes)
            x = None
            if isinstance(store.config, gf.ConfigTypeB):
                x = (receiver_depth, None, None)

            phase.plot_2d(axes, x=x)
            axes.set_title(phase_id)
            np += 1
        except gf.StoreError as e:
            die(e)

    axes = plt.subplot(2, 1, 2)
    num_d = 100
    distances = num.linspace(store.config.distance_min,
                             store.config.distance_max,
                             num_d)

    if options.source_depth is not None:
        source_depth = options.source_depth * km
    else:
        source_depth = store.config.source_depth_min + (
            store.config.source_depth_max - store.config.source_depth_min) / 2.

    if isinstance(store.config, gf.ConfigTypeA):
        attribute_vals = num.empty(num_d)
        for phase_id in phase_ids:
            attribute_vals[:] = num.NAN
            for i, d in enumerate(distances):
                if attribute == 'phase':
                    attribute_vals[i] = store.t(phase_id, (source_depth, d))
                    ylabel = 'TT [s]'
                else:
                    attribute_vals[i] = store.get_stored_attribute(
                        phase_id, options.attribute, (source_depth, d))
                    ylabel = '%s [deg]' % options.attribute

            axes.plot(distances / km, attribute_vals, label=phase_id)

        axes.set_title('source source_depth %s km' % (source_depth / km))
        axes.set_xlabel('distance [km]')
        axes.set_ylabel(ylabel)
        axes.legend()

    plt.tight_layout()
    mpl_show(plt)


def command_ttt(args):
    def setup(parser):
        parser.add_option(
            '--force', dest='force', action='store_true',
            help='overwrite existing files')

    parser, options, args = cl_parse('ttt', args, setup=setup)

    store_dir = get_store_dir(args)
    try:
        store = gf.Store(store_dir)
        store.make_travel_time_tables(force=options.force)

    except gf.StoreError as e:
        die(e)


def command_tttview(args):

    def setup(parser):
        parser.add_option(
            '--source-depth', dest='source_depth', type=float,
            help='Source depth in km')

        parser.add_option(
            '--receiver-depth', dest='receiver_depth', type=float,
            help='Receiver depth in km')

    parser, options, args = cl_parse(
        'tttview', args, setup=setup,
        details="Comma seperated <phase-ids>, eg. 'fomosto tttview Pdiff,S'.")

    try:
        phase_ids = args.pop().split(',')
    except Exception:
        parser.error('cannot get <phase-ids> argument')

    stored_attribute_table_plots(phase_ids, options, args, attribute='phase')


def command_sat(args):
    def setup(parser):
        parser.add_option(
            '--force', dest='force', action='store_true',
            help='overwrite existing files')

        parser.add_option(
            '--attribute',
            action='store',
            dest='attribute',
            type='choice',
            choices=gf.store.available_stored_tables[1::],
            default='takeoff_angle',
            help='calculate interpolation table for selected ray attributes.')

    parser, options, args = cl_parse('sat', args, setup=setup)

    store_dir = get_store_dir(args)
    try:
        store = gf.Store(store_dir)
        store.make_stored_table(options.attribute, force=options.force)

    except gf.StoreError as e:
        die(e)


def command_satview(args):

    def setup(parser):
        parser.add_option(
            '--source-depth', dest='source_depth', type=float,
            help='Source depth in km')

        parser.add_option(
            '--receiver-depth', dest='receiver_depth', type=float,
            help='Receiver depth in km')

        parser.add_option(
            '--attribute',
            action='store',
            dest='attribute',
            type='choice',
            choices=gf.store.available_stored_tables[1::],
            default='takeoff_angle',
            help='view selected ray attribute.')

    parser, options, args = cl_parse(
        'satview', args, setup=setup,
        details="Comma seperated <phase-ids>, eg. 'fomosto satview Pdiff,S'.")

    try:
        phase_ids = args.pop().split(',')
    except Exception:
        parser.error('cannot get <phase-ids> argument')

    logger.info('Plotting stored attribute %s' % options.attribute)

    stored_attribute_table_plots(
        phase_ids, options, args, attribute=options.attribute)


def command_tttextract(args):
    def setup(parser):
        parser.add_option(
            '--output', dest='output_fn', metavar='TEMPLATE',
            help='output to text files instead of stdout '
                 '(example TEMPLATE: "extracted/%(args)s.txt")')

    parser, options, args = cl_parse('tttextract', args, setup=setup)
    try:
        sdef = args.pop()
    except Exception:
        parser.error('cannot get <selection> argument')

    try:
        sphase = args.pop()
    except Exception:
        parser.error('cannot get <phase> argument')

    try:
        phases = [gf.meta.Timing(x.strip()) for x in sphase.split(',')]
    except gf.meta.InvalidTimingSpecification:
        parser.error('invalid phase specification: "%s"' % sphase)

    try:
        gdef = gf.meta.parse_grid_spec(sdef)
    except gf.meta.GridSpecError as e:
        die(e)

    store_dir = get_store_dir(args)

    try:
        store = gf.Store(store_dir)
        for args in store.config.iter_extraction(gdef, level=-1):
            s = ['%e' % x for x in args]
            for phase in phases:
                t = store.t(phase, args)
                if t is not None:
                    s.append('%e' % t)
                else:
                    s.append('nan')

            if options.output_fn:
                d = dict(
                    args='_'.join('%e' % x for x in args),
                    extension='txt')

                fn = options.output_fn % d
                util.ensuredirs(fn)
                with open(fn, 'a') as f:
                    f.write(' '.join(s))
                    f.write('\n')
            else:
                print(' '.join(s))

    except (gf.meta.GridSpecError, gf.StoreError, gf.meta.OutOfBounds) as e:
        die(e)


def command_tttlsd(args):

    def setup(parser):
        pass

    parser, options, args = cl_parse(
        'tttlsd', args,
        setup=setup,
        details='''

This subcommand fills holes in travel-time-tables by using an eikonal solver to
predict travel-times for the missing values. The approach works best for simple
P or S phases without any reflections or conversions. It creates new
travel-time-tables which are named by adding the suffix `.lsd` to the original
name. E.g. running `fomosto tttlsd begin` would produce a new travel-time-table
`begin.lsd`. The new name can be referenced anywhere where a stored
travel-time-table can be used, e.g. in `extra/qseis`.
''')

    try:
        sphase_ids = args.pop()
    except Exception:
        parser.error('cannot get <phase> argument')

    try:
        phase_ids = [x.strip() for x in sphase_ids.split(',')]
    except gf.meta.InvalidTimingSpecification:
        parser.error('invalid phase specification: "%s"' % sphase_ids)

    store_dir = get_store_dir(args)

    try:
        store = gf.Store(store_dir)
        for phase_id in phase_ids:
            store.fix_ttt_holes(phase_id)

    except gf.StoreError as e:
        die(e)


def command_server(args):
    from pyrocko.gf import server

    def setup(parser):
        parser.add_option(
            '--port', dest='port', metavar='PORT', type='int', default=8080,
            help='serve on port PORT')

        parser.add_option(
            '--ip', dest='ip', metavar='IP', default='',
            help='serve on ip address IP')

    parser, options, args = cl_parse('server', args, setup=setup)

    engine = gf.LocalEngine(store_superdirs=args)
    server.run(options.ip, options.port, engine)


def command_download(args):
    from pyrocko.gf import ws

    details = '''

Browse pre-calculated Green's function stores online:

  https://greens-mill.pyrocko.org
'''

    def setup(parser):
        parser.add_option(
            '--force', dest='force', action='store_true',
            help='overwrite existing files')

    parser, options, args = cl_parse(
        'download', args, setup=setup, details=details)
    if len(args) not in (1, 2):
        sys.exit(parser.format_help())

    if len(args) == 2:
        site, store_id = args
        if not re.match(gf.meta.StringID.pattern, store_id):
            die('invalid store ID')
    else:
        site, store_id = args[0], None

    if site not in gf.ws.g_site_abbr:
        if -1 == site.find('://'):
            site = 'http://' + site

    try:
        ws.download_gf_store(site=site, store_id=store_id, force=options.force)
    except ws.DownloadError as e:
        die(str(e))


def command_modelview(args):

    import matplotlib.pyplot as plt
    import numpy as num
    from pyrocko.plot.cake_plot import mpl_init, labelspace, xscaled, yscaled
    mpl_init()

    neat_labels = {
        'vp': '$v_p$',
        'vs': '$v_s$',
        'qp': '$Q_p$',
        'qs': '$Q_s$',
        'rho': '$\\rho$'}

    def setup(parser):
        parser.add_option(
            '--parameters', dest='parameters',
            default='vp,vs', metavar='vp,vs,....',
            help='select one or several of vp, vs, rho, qp, qs, vp/vs, qp/qs')

    units = {'vp': '[km/s]', 'vs': '[km/s]', 'rho': '[g/cm^3]'}

    parser, options, args = cl_parse('modelview', args, setup=setup)

    store_dirs = get_store_dirs(args)

    parameters = options.parameters.split(',')

    fig, axes = plt.subplots(1,
                             len(parameters),
                             sharey=True,
                             figsize=(len(parameters)*3, 5))

    if not isinstance(axes, num.ndarray):
        axes = [axes]

    axes = dict(zip(parameters, axes))

    for store_id in store_dirs:
        try:
            store = gf.Store(store_id)
            mod = store.config.earthmodel_1d
            z = mod.profile('z')

            for p in parameters:
                ax = axes[p]
                labelspace(ax)
                if '/' in p:
                    p1, p2 = p.split('/')
                    profile = mod.profile(p1)/mod.profile(p2)
                else:
                    profile = mod.profile(p)

                ax.plot(profile, -z, label=store_id.split('/')[-1])
                if p in ['vs', 'vp', 'rho']:
                    xscaled(1./1000., ax)

                yscaled(1./km, ax)

        except gf.StoreError as e:
            die(e)

    for p, ax in axes.items():
        ax.grid()
        if p in neat_labels:
            lab = neat_labels[p]
        elif all(x in neat_labels for x in p.split('/')):
            lab = '/'.join(neat_labels[x] for x in p.split('/'))
        else:
            lab = p

        ax.set_title(lab, y=1.02)

        if p in units:
            lab += ' ' + units[p]

        ax.autoscale()
        ax.set_xlabel(lab)

    axes[parameters[0]].set_ylabel('Depth [km]')

    handles, labels = ax.get_legend_handles_labels()

    if len(store_dirs) > 1:
        ax.legend(
            handles,
            labels,
            bbox_to_anchor=(0.5, 0.12),
            bbox_transform=fig.transFigure,
            ncol=3,
            loc='upper center',
            fancybox=True)

    plt.subplots_adjust(bottom=0.22,
                        wspace=0.05)

    mpl_show(plt)


def command_upgrade(args):
    parser, options, args = cl_parse('upgrade', args)
    store_dirs = get_store_dirs(args)
    try:
        for store_dir in store_dirs:
            store = gf.Store(store_dir)
            nup = store.upgrade()
            if nup == 0:
                print('%s: already up-to-date.' % store_dir)
            else:
                print('%s: %i file%s upgraded.' % (
                    store_dir, nup, ['s', ''][nup == 1]))

    except gf.StoreError as e:
        die(e)


def command_addref(args):
    parser, options, args = cl_parse('addref', args)

    store_dirs = []
    filenames = []
    for arg in args:
        if op.isdir(arg):
            store_dirs.append(arg)
        elif op.isfile(arg):
            filenames.append(arg)
        else:
            die('invalid argument: %s' % arg)

    if not store_dirs:
        store_dirs.append('.')

    references = []
    try:
        for filename in args:
            references.extend(gf.meta.Reference.from_bibtex(filename=filename))
    except ImportError:
        die('pybtex module must be installed to use this function')

    if not references:
        die('no references found')

    for store_dir in store_dirs:
        try:
            store = gf.Store(store_dir)
            ids = [ref.id for ref in store.config.references]
            for ref in references:
                if ref.id in ids:
                    die('duplicate reference id: %s' % ref.id)

                ids.append(ref.id)
                store.config.references.append(ref)

            store.save_config(make_backup=True)

        except gf.StoreError as e:
            die(e)


def command_qc(args):
    parser, options, args = cl_parse('qc', args)

    store_dir = get_store_dir(args)

    try:
        store = gf.Store(store_dir)
        s = store.stats()
        if s['empty'] != 0:
            print('has empty records')

        for aname in ['author', 'author_email', 'description', 'references']:

            if not getattr(store.config, aname):
                print('%s empty' % aname)

    except gf.StoreError as e:
        die(e)


def command_report(args):
    from pyrocko.fomosto.report import report_call
    report_call.run_program(args)


[docs]def main(args=None): ''' CLI entry point for Pyrocko's ``fomosto`` app. ''' if args is None: args = sys.argv[1:] if len(args) < 1: sys.exit('Usage: %s' % usage) command = args.pop(0) if command in subcommands: globals()['command_' + command](args) elif command in ('--help', '-h', 'help'): if command == 'help' and args: acommand = args[0] if acommand in subcommands: globals()['command_' + acommand](['--help']) sys.exit('Usage: %s' % usage) else: sys.exit('fomosto: error: no such subcommand: %s' % command)
if __name__ == '__main__': main()