1#!/usr/bin/env python
3from __future__ import print_function, absolute_import
5import sys
6import os.path as op
7import logging
8from optparse import OptionParser, OptionValueError
9import grond
10from io import StringIO
12try:
13 from pyrocko import util, marker
14except ImportError:
15 print('Pyrocko is required for Grond!'
16 'Go to https://pyrocko.org/ for installation instructions.')
19logger = logging.getLogger('grond.main')
20km = 1e3
23class Color:
24 PURPLE = '\033[95m'
25 CYAN = '\033[96m'
26 DARKCYAN = '\033[36m'
27 BLUE = '\033[94m'
28 GREEN = '\033[92m'
29 YELLOW = '\033[93m'
30 RED = '\033[91m'
31 BOLD = '\033[1m'
32 UNDERLINE = '\033[4m'
33 END = '\033[0m'
36def d2u(d):
37 if isinstance(d, dict):
38 return dict((k.replace('-', '_'), v) for (k, v) in d.items())
39 else:
40 return d.replace('-', '_')
43subcommand_descriptions = {
44 'init': 'initialise new project structure or print configuration',
45 'scenario': 'create a forward-modelled scenario project',
46 'events': 'print available event names for given configuration',
47 'check': 'check data and configuration',
48 'go': 'run Grond optimisation',
49 'forward': 'run forward modelling',
50 'harvest': 'manually run harvesting',
51 'cluster': 'run cluster analysis on result ensemble',
52 'plot': 'plot optimisation result',
53 'movie': 'visualize optimiser evolution',
54 'export': 'export results',
55 'tag': 'add user-defined label to run directories',
56 'report': 'create result report',
57 'diff': 'compare two configs or other normalized Grond YAML files',
58 'qc-polarization': 'check sensor orientations with polarization analysis',
59 'upgrade-config': 'upgrade config file to the latest version of Grond',
60 'version': 'print version number of Grond and its main dependencies',
61}
63subcommand_usages = {
64 'init': (
65 'init list [options]',
66 'init <example> [options]',
67 'init <example> <projectdir> [options]'),
68 'scenario': 'scenario [options] <projectdir>',
69 'events': 'events <configfile>',
70 'check': 'check <configfile> <eventnames> ... [options]',
71 'go': 'go <configfile> <eventnames> ... [options]',
72 'forward': (
73 'forward <rundir> [options]',
74 'forward <configfile> <eventnames> ... [options]'),
75 'harvest': 'harvest <rundir> [options]',
76 'cluster': (
77 'cluster <method> <rundir> [options]',
78 'cluster <clusteringconfigfile> <rundir> [options]'),
79 'plot': (
80 'plot <plotnames> ( <rundir> | <configfile> <eventname> ) [options]',
81 'plot all ( <rundir> | <configfile> <eventname> ) [options]',
82 'plot <plotconfigfile> ( <rundir> | <configfile> <eventname> ) [options]', # noqa
83 'plot list ( <rundir> | <configfile> <eventname> ) [options]',
84 'plot config ( <rundir> | <configfile> <eventname> ) [options]'),
85 'movie': 'movie <rundir> <xpar> <ypar> <filetemplate> [options]',
86 'export': 'export (best|mean|ensemble|stats) <rundirs> ... [options]',
87 'tag': (
88 'tag add <tag> <rundir>',
89 'tag remove <tag> <rundir>',
90 'tag list <rundir>'),
91 'report': (
92 'report <rundir> ... [options]',
93 'report <configfile> <eventnames> ...'),
94 'diff': 'diff <left_path> <right_path>',
95 'qc-polarization': 'qc-polarization <configfile> <eventname> '
96 '<target_group_path> [options]',
97 'upgrade-config': 'upgrade-config <configfile>',
98 'version': 'version',
99}
101subcommands = subcommand_descriptions.keys()
103program_name = 'grond'
105usage_tdata = d2u(subcommand_descriptions)
106usage_tdata['program_name'] = program_name
107usage_tdata['version_number'] = grond.__version__
110usage = '''%(program_name)s <subcommand> [options] [--] <arguments> ...
112Grond is a probabilistic earthquake source inversion framework.
114This is Grond version %(version_number)s.
116Subcommands:
118 scenario %(scenario)s
119 init %(init)s
120 events %(events)s
121 check %(check)s
122 go %(go)s
123 forward %(forward)s
124 harvest %(harvest)s
125 cluster %(cluster)s
126 plot %(plot)s
127 movie %(movie)s
128 export %(export)s
129 tag %(tag)s
130 report %(report)s
131 diff %(diff)s
132 qc-polarization %(qc_polarization)s
133 upgrade-config %(upgrade_config)s
134 version %(version)s
136To get further help and a list of available options for any subcommand run:
138 %(program_name)s <subcommand> --help
140What do you want to bust today?!
141''' % usage_tdata
144class CLIHints(object):
145 init = '''
146We created a folder structure in {project_dir}.
147Check out the YAML configuration in {config} and start the optimisation by:
149 grond go {config}
150'''
151 scenario = '''
152To start the scenario's optimisation, change to folder
154 cd {project_dir}
156Check out the YAML configuration in {config} and start the optimisation by:
158 grond go {config}
159'''
160 report = '''
161To open the report in your web browser, run
163 grond report -s --open {config}
164'''
165 check = '''
166To start the optimisation, run
168 grond go {config}
169'''
170 go = '''
171To look at the results, run
173 grond report -so {rundir}
174'''
176 def __new__(cls, command, **kwargs):
177 return '{c.BOLD}Hint{c.END}\n'.format(c=Color) +\
178 getattr(cls, command).format(**kwargs)
181def main(args=None):
182 if not args:
183 args = sys.argv
185 args = list(args)
186 if len(args) < 2:
187 sys.exit('Usage: %s' % usage)
189 args.pop(0)
190 command = args.pop(0)
192 if command in subcommands:
193 globals()['command_' + d2u(command)](args)
195 elif command in ('--help', '-h', 'help'):
196 if command == 'help' and args:
197 acommand = args[0]
198 if acommand in subcommands:
199 globals()['command_' + acommand](['--help'])
201 sys.exit('Usage: %s' % usage)
203 else:
204 die('No such subcommand: %s' % command)
207def add_common_options(parser):
208 parser.add_option(
209 '--loglevel',
210 action='store',
211 dest='loglevel',
212 type='choice',
213 choices=('critical', 'error', 'warning', 'info', 'debug'),
214 default='info',
215 help='set logger level to '
216 '"critical", "error", "warning", "info", or "debug". '
217 'Default is "%default".')
219 parser.add_option(
220 '--docs',
221 dest='rst_docs',
222 action='store_true')
225def print_docs(command, parser):
227 from optparse import IndentedHelpFormatter
229 class DocsFormatter(IndentedHelpFormatter):
231 def format_heading(self, heading):
232 return '%s\n%s\n\n' % (heading, '.'*len(heading))
234 def format_usage(self, usage):
235 lines = usage.splitlines()
236 return self.format_heading('Usage') + \
237 '.. code-block:: none\n\n%s' % '\n'.join(
238 ' '+line.strip() for line in lines)
240 def format_option(self, option):
241 if not option.help:
242 return ''
244 result = []
245 opts = self.option_strings[option]
246 result.append('\n.. describe:: %s\n\n' % opts)
248 help_text = self.expand_default(option)
249 result.append(' %s\n\n' % help_text)
251 return ''.join(result)
253 parser.formatter = DocsFormatter()
254 parser.formatter.set_parser(parser)
256 def format_help(parser):
257 formatter = parser.formatter
258 result = []
260 result.append(parser.format_description(formatter) + "\n")
262 if parser.usage:
263 result.append(parser.get_usage() + "\n")
265 result.append('\n')
267 result.append(parser.format_option_help(formatter))
269 result.append('\n')
271 result.append(parser.format_epilog(formatter))
272 return "".join(result)
274 print(command)
275 print('-' * len(command))
276 print()
277 print('.. program:: %s' % program_name)
278 print()
279 print('.. option:: %s' % command)
280 print()
281 print(format_help(parser))
284def process_common_options(command, parser, options):
285 util.setup_logging(program_name, options.loglevel)
286 if options.rst_docs:
287 print_docs(command, parser)
288 exit(0)
291def cl_parse(command, args, setup=None, details=None):
292 usage = subcommand_usages[command]
293 descr = subcommand_descriptions[command]
295 if isinstance(usage, str):
296 usage = [usage]
298 susage = '%s %s' % (program_name, usage[0])
299 for s in usage[1:]:
300 susage += '\n%s%s %s' % (' '*7, program_name, s)
302 description = descr[0].upper() + descr[1:] + '.'
304 if details:
305 description = description + '\n\n%s' % details
307 parser = OptionParser(usage=susage, description=description)
309 if setup:
310 setup(parser)
312 add_common_options(parser)
313 (options, args) = parser.parse_args(args)
314 process_common_options(command, parser, options)
315 return parser, options, args
318def die(message, err='', prelude=''):
319 if prelude:
320 prelude = prelude + '\n'
322 if err:
323 err = '\n' + err
325 sys.exit('%s%s failed: %s%s' % (prelude, program_name, message, err))
328def help_and_die(parser, message):
329 sio = StringIO()
330 parser.print_help(sio)
331 die(message, prelude=sio.getvalue())
334def multiple_choice(option, opt_str, value, parser, choices):
335 options = value.split(',')
336 for opt in options:
337 if opt not in choices:
338 raise OptionValueError('Invalid option %s - valid options are: %s'
339 % (opt, ', '.join(choices)))
340 setattr(parser.values, option.dest, options)
343def magnitude_range(option, opt_str, value, parser):
344 mag_range = value.split('-')
345 if len(mag_range) != 2:
346 raise OptionValueError(
347 'Invalid magnitude %s - valid range is e.g. 6-7.' % value)
348 try:
349 mag_range = tuple(map(float, mag_range))
350 except ValueError:
351 raise OptionValueError('Magnitudes must be numbers.')
353 if mag_range[0] > mag_range[1]:
354 raise OptionValueError('Minimum magnitude must be larger than'
355 ' maximum magnitude.')
356 setattr(parser.values, option.dest, mag_range)
359def command_scenario(args):
361 STORE_STATIC = 'crust2_ib_static'
362 STORE_WAVEFORMS = 'crust2_ib'
364 def setup(parser):
365 parser.add_option(
366 '--targets', action='callback', dest='targets', type=str,
367 callback=multiple_choice, callback_kwargs={
368 'choices': ('waveforms', 'gnss', 'insar')
369 },
370 default='waveforms',
371 help='forward modelling targets for the scenario. Select from:'
372 ' waveforms, gnss and insar. '
373 '(default: --targets=%default,'
374 ' multiple selection by --targets=waveforms,gnss,insar)')
375 parser.add_option(
376 '--problem', dest='problem', default='cmt',
377 type='choice', choices=['cmt', 'rectangular'],
378 help='problem to generate: \'dc\' (double couple)'
379 ' or \'rectangular\' (rectangular finite fault)'
380 ' (default: \'%default\')')
381 parser.add_option(
382 '--magnitude-range', dest='magnitude_range', type=str,
383 action='callback', callback=magnitude_range, default=[6.0, 7.0],
384 help='Magnitude range min_mag-max_mag (default: %default)')
385 parser.add_option(
386 '--nstations', dest='nstations', type=int, default=20,
387 help='number of seismic stations to create (default: %default)')
388 parser.add_option(
389 '--gnss_nstations', dest='gnss_nstations', type=int, default=20,
390 help='number of GNSS campaign stations to create'
391 ' (default: %default)')
392 parser.add_option(
393 '--nevents', dest='nevents', type=int, default=1,
394 help='number of events to create (default: %default)')
395 parser.add_option(
396 '--lat', dest='lat', type=float, default=41.0,
397 help='center latitude of the scenario (default: %default)')
398 parser.add_option(
399 '--lon', dest='lon', type=float, default=33.3,
400 help='center latitude of the scenario (default: %default)')
401 parser.add_option(
402 '--radius', dest='radius', type=float, default=100.,
403 help='radius of the scenario in [km] (default: %default)')
404 parser.add_option(
405 '--source-radius', dest='source_radius', type=float, default=10.,
406 help='radius of the source area in [km] (default: %default)')
407 parser.add_option(
408 '--stations-paths', dest='stations_paths', type=str, default=None,
409 help='paths to a Pyrocko station file, seperated by \',\''
410 '(default: %default)')
411 parser.add_option(
412 '--stationxml-paths', dest='stationxml_paths', type=str,
413 default=None,
414 help='paths to StationXML files, seperated by \',\''
415 '(default: %default)')
416 parser.add_option(
417 '--gf-waveforms', dest='store_waveforms', type=str,
418 default=STORE_WAVEFORMS,
419 help='Green\'s function store for waveform modelling, '
420 '(default: %default)')
421 parser.add_option(
422 '--gf-static', dest='store_statics', type=str,
423 default=STORE_STATIC,
424 help='Green\'s function store for static modelling, '
425 '(default: %default)')
426 parser.add_option(
427 '--force', dest='force', action='store_true',
428 help='overwrite existing project folder.')
429 parser.add_option(
430 '--gf-store-superdirs',
431 dest='gf_store_superdirs',
432 help='Comma-separated list of directories containing GF stores')
433 parser.add_option(
434 '--no-map',
435 dest='make_map',
436 default=True,
437 action='store_false',
438 help='suppress generation of map')
439 parser.add_option(
440 '--rebuild',
441 dest='rebuild', action='store_true', default=False,
442 help='Rebuild a manually configured grond scenario')
444 parser, options, args = cl_parse('scenario', args, setup)
446 gf_store_superdirs = None
447 if options.gf_store_superdirs:
448 gf_store_superdirs = options.gf_store_superdirs.split(',')
450 if len(args) == 1:
451 project_dir = args[0]
452 else:
453 parser.print_help()
454 sys.exit(1)
456 from grond import scenario as grond_scenario
458 try:
459 scenario = grond_scenario.GrondScenario(
460 project_dir,
461 center_lat=options.lat, center_lon=options.lon,
462 radius=options.radius*km)
464 scenario.rebuild = options.rebuild
465 if options.rebuild:
466 options.force = True
468 if 'waveforms' in options.targets:
469 if options.stationxml_paths:
470 options.stationxml_paths = [
471 op.abspath(path) for path in
472 options.stationxml_paths.split(',')]
474 if options.stations_paths:
475 options.stations_paths = [
476 op.abspath(path) for path in
477 options.stations_paths.split(',')]
479 obs = grond_scenario.WaveformObservation(
480 nstations=options.nstations,
481 store_id=options.store_waveforms,
482 stations_paths=options.stations_paths,
483 stationxml_paths=options.stationxml_paths)
484 scenario.add_observation(obs)
486 if 'insar' in options.targets:
487 obs = grond_scenario.InSARObservation(
488 store_id=options.store_statics)
489 scenario.add_observation(obs)
491 if 'gnss' in options.targets:
492 obs = grond_scenario.GNSSCampaignObservation(
493 nstations=options.gnss_nstations,
494 store_id=options.store_statics)
495 scenario.add_observation(obs)
497 if options.problem == 'cmt':
498 problem = grond_scenario.DCSourceProblem(
499 nevents=options.nevents,
500 radius=options.source_radius*km,
501 magnitude_min=options.magnitude_range[0],
502 magnitude_max=options.magnitude_range[1])
503 elif options.problem == 'rectangular':
504 problem = grond_scenario.RectangularSourceProblem(
505 nevents=options.nevents)
506 scenario.set_problem(problem)
508 scenario.build(
509 force=options.force,
510 interactive=True,
511 gf_store_superdirs=gf_store_superdirs,
512 make_map=options.make_map)
514 logger.info(CLIHints('scenario',
515 config=scenario.get_grond_config_path(),
516 project_dir=project_dir))
518 except grond.GrondError as e:
519 die(str(e))
522def command_init(args):
524 from .cmd_init import GrondInit
526 grond_init = GrondInit()
528 def print_section(entries):
529 if len(entries) == 0:
530 return '\tNone available.'
532 padding = max([len(n) for n in entries.keys()])
533 rstr = []
534 lcat = None
535 for name, desc in entries.items():
537 cat = name.split('_')[0]
538 if lcat is not None and lcat != cat:
539 rstr.append('')
540 lcat = cat
542 rstr.append(' {c.BOLD}{name:<{padding}}{c.END} : {desc}'.format(
543 name=name, desc=desc, padding=padding, c=Color))
544 return '\n'.join(rstr)
546 help_text = '''Available configuration examples for Grond.
548{c.BOLD}Example Projects{c.END}
550 Deploy a full project structure into a directory.
552 usage: grond init <example> <projectdir>
554 where <example> is any of the following:
556{examples_list}
558{c.BOLD}Config Sections{c.END}
560 Print out configuration snippets for various components.
562 usage: grond init <section>
564 where <section> is any of the following:
566{sections_list}
567'''.format(c=Color,
568 examples_list=print_section(grond_init.get_examples()),
569 sections_list=print_section(grond_init.get_sections()))
571 def setup(parser):
572 parser.add_option(
573 '--force', dest='force', action='store_true')
575 parser, options, args = cl_parse(
576 'init', args, setup,
577 'Use grond init list to show available examples.')
579 if len(args) not in (1, 2):
580 help_and_die(parser, '1 or 2 arguments required')
582 if args[0] == 'list':
583 print(help_text)
584 return
586 if args[0].startswith('example_'):
587 if len(args) == 1:
588 config = grond_init.get_content_example(args[0])
589 if not config:
590 help_and_die(parser, 'Unknown example: %s' % args[0])
592 sys.stdout.write(config+'\n\n')
594 logger.info('Hint: To create a project, use: grond init <example> '
595 '<projectdir>')
597 elif op.exists(op.abspath(args[1])) and not options.force:
598 help_and_die(
599 parser,
600 'Directory "%s" already exists! Use --force to overwrite.'
601 % args[1])
602 else:
603 try:
604 grond_init.init_example(args[0], args[1], force=options.force)
605 except OSError as e:
606 print(str(e))
608 else:
609 sec = grond_init.get_content_snippet(args[0])
610 if not sec:
611 help_and_die(parser, 'Unknown snippet: %s' % args[0])
613 sys.stdout.write(sec)
616def command_init_old(args):
618 from . import cmd_init as init
620 def setup(parser):
621 parser.add_option(
622 '--targets', action='callback', dest='targets', type=str,
623 callback=multiple_choice, callback_kwargs={
624 'choices': ('waveforms', 'gnss', 'insar', 'all')
625 },
626 default='waveforms',
627 help='select from:'
628 ' waveforms, gnss and insar. '
629 '(default: --targets=%default,'
630 ' multiple selection by --targets=waveforms,gnss,insar)')
631 parser.add_option(
632 '--problem', dest='problem',
633 type='choice', choices=['cmt', 'rectangular'],
634 help='problem to generate: \'dc\' (double couple)'
635 ' or\'rectangular\' (rectangular finite fault)'
636 ' (default: \'%default\')')
637 parser.add_option(
638 '--force', dest='force', action='store_true',
639 help='overwrite existing project folder')
641 parser, options, args = cl_parse('init', args, setup)
643 try:
644 project = init.GrondProject()
646 if 'all' in options.targets:
647 targets = ['waveforms', 'gnss', 'insar']
648 else:
649 targets = options.targets
651 if not options.problem:
652 if 'insar' in targets or 'gnss' in targets:
653 problem = 'rectangular'
654 else:
655 problem = 'cmt'
656 else:
657 problem = options.problem
659 if problem == 'rectangular':
660 project.set_rectangular_source()
661 elif problem == 'cmt':
662 project.set_cmt_source()
664 if 'waveforms' in targets:
665 project.add_waveforms()
667 if 'insar' in targets:
668 project.add_insar()
670 if 'gnss' in targets:
671 project.add_gnss()
673 if len(args) == 1:
674 project_dir = args[0]
675 project.build(project_dir, options.force)
676 logger.info(CLIHints(
677 'init', project_dir=project_dir,
678 config=op.join(project_dir, 'config', 'config.gronf')))
679 else:
680 sys.stdout.write(project.dump())
682 except grond.GrondError as e:
683 die(str(e))
686def command_events(args):
687 def setup(parser):
688 pass
690 parser, options, args = cl_parse('events', args, setup)
691 if len(args) != 1:
692 help_and_die(parser, 'missing arguments')
694 config_path = args[0]
695 try:
696 config = grond.read_config(config_path)
698 for event_name in grond.get_event_names(config):
699 print(event_name)
701 except grond.GrondError as e:
702 die(str(e))
705def command_check(args):
707 from grond.environment import Environment
709 def setup(parser):
710 parser.add_option(
711 '--target-ids', dest='target_string_ids', metavar='TARGET_IDS',
712 help='process only selected targets. TARGET_IDS is a '
713 'comma-separated list of target IDs. Target IDs have the '
714 'form SUPERGROUP.GROUP.NETWORK.STATION.LOCATION.CHANNEL.')
716 parser.add_option(
717 '--waveforms', dest='show_waveforms', action='store_true',
718 help='show raw, restituted, projected, and processed waveforms')
720 parser.add_option(
721 '--nrandom', dest='n_random_synthetics', metavar='N', type=int,
722 default=10,
723 help='set number of random synthetics to forward model (default: '
724 '10). If set to zero, create synthetics for the reference '
725 'solution.')
727 parser.add_option(
728 '--save-stations-used', dest='stations_used_path',
729 metavar='FILENAME',
730 help='aggregate all stations used by the setup into a file')
732 parser, options, args = cl_parse('check', args, setup)
733 if len(args) < 1:
734 help_and_die(parser, 'missing arguments')
736 try:
737 env = Environment(args)
738 config = env.get_config()
740 target_string_ids = None
741 if options.target_string_ids:
742 target_string_ids = options.target_string_ids.split(',')
744 grond.check(
745 config,
746 event_names=env.get_selected_event_names(),
747 target_string_ids=target_string_ids,
748 show_waveforms=options.show_waveforms,
749 n_random_synthetics=options.n_random_synthetics,
750 stations_used_path=options.stations_used_path)
752 logger.info(CLIHints('check', config=env.get_config_path()))
754 except grond.GrondError as e:
755 die(str(e))
758def command_go(args):
760 from grond.environment import Environment
762 def setup(parser):
763 parser.add_option(
764 '--force', dest='force', action='store_true',
765 help='overwrite existing run directory')
766 parser.add_option(
767 '--preserve', dest='preserve', action='store_true',
768 help='preserve old rundir')
769 parser.add_option(
770 '--status', dest='status', default='state',
771 type='choice', choices=['state', 'quiet'],
772 help='status output selection (choices: state, quiet, default: '
773 'state)')
774 parser.add_option(
775 '--parallel', dest='nparallel', type=int, default=1,
776 help='set number of events to process in parallel, '
777 'if set to more than one, --status=quiet is implied.')
778 parser.add_option(
779 '--threads', dest='nthreads', type=int, default=1,
780 help='set number of threads per process (default: 1). '
781 'Set to 0 to use all available cores.')
783 parser, options, args = cl_parse('go', args, setup)
785 try:
786 env = Environment(args)
788 status = options.status
789 if options.nparallel != 1:
790 status = 'quiet'
792 grond.go(
793 env,
794 force=options.force,
795 preserve=options.preserve,
796 status=status,
797 nparallel=options.nparallel,
798 nthreads=options.nthreads)
799 if len(env.get_selected_event_names()) == 1:
800 logger.info(CLIHints(
801 'go', rundir=env.get_rundir_path()))
803 except grond.GrondError as e:
804 die(str(e))
807def command_forward(args):
809 from grond.environment import Environment
811 def setup(parser):
812 parser.add_option(
813 '--show', dest='show', metavar='WHAT',
814 default='filtered',
815 choices=('filtered', 'processed'),
816 help='select whether to show only "filtered" or fully "processed" '
817 '(i.e. tapered) waveforms (default "%default").')
819 parser, options, args = cl_parse('forward', args, setup)
820 if len(args) < 1:
821 help_and_die(parser, 'missing arguments')
823 try:
824 env = Environment(args)
825 grond.forward(env, show=options.show)
826 except grond.GrondError as e:
827 die(str(e))
830def command_harvest(args):
831 def setup(parser):
832 parser.add_option(
833 '--force', dest='force', action='store_true',
834 help='overwrite existing harvest directory')
835 parser.add_option(
836 '--neach', dest='neach', type=int, default=10,
837 help='take NEACH best samples from each chain (default: %default)')
838 parser.add_option(
839 '--weed', dest='weed', type=int, default=0,
840 help='weed out bootstrap samples with bad global performance. '
841 '0: no weeding (default), '
842 '1: only bootstrap chains where all NEACH best samples '
843 'global misfit is less than the global average misfit of all '
844 'NEACH best in all chains plus one standard deviation are '
845 'included in the harvest ensemble, '
846 '2: same as 1 but additionally individual samples are '
847 'removed if their global misfit is greater than the global '
848 'average misfit of all NEACH best in all chains, '
849 '3: harvesting is done on the global chain only, bootstrap '
850 'chains are excluded')
851 parser.add_option(
852 '--export-fits', dest='export_fits', default='',
853 help='additionally export details about the fit of individual '
854 'targets. "best" - export fits of best model, "mean" - '
855 'export fits of ensemble mean model, "ensemble" - export '
856 'fits of all models in harvest ensemble.')
858 parser, options, args = cl_parse('harvest', args, setup)
859 if len(args) < 1:
860 help_and_die(parser, 'no rundir')
862 export_fits = []
863 if options.export_fits.strip():
864 export_fits = [x.strip() for x in options.export_fits.split(',')]
866 for run_path in args:
867 try:
868 grond.harvest(
869 run_path,
870 force=options.force,
871 nbest=options.neach,
872 weed=options.weed,
873 export_fits=export_fits)
875 except grond.DirectoryAlreadyExists as e:
876 die(str(e) + '\n Use --force to overwrite.')
878 except grond.GrondError as e:
879 die(str(e))
882def command_cluster(args):
883 from grond import Clustering
884 from grond.clustering import metrics, methods, read_config, write_config
886 def setup(parser):
887 parser.add_option(
888 '--metric', dest='metric', metavar='METRIC',
889 default='kagan_angle',
890 choices=metrics.metrics,
891 help='metric to measure model distances. Choices: [%s]. Default: '
892 'kagan_angle' % ', '.join(metrics.metrics))
894 parser.add_option(
895 '--write-config',
896 dest='write_config',
897 metavar='FILE',
898 help='write configuration (or default configuration) to FILE')
900 method = args[0] if args else ''
901 try:
902 parser, options, args = cl_parse(
903 'cluster', args[1:], setup=Clustering.cli_setup(method, setup),
904 details='Available clustering methods: [%s]. Use '
905 '"grond cluster <method> --help" to get list of method '
906 'dependent options.' % ', '.join(methods))
908 if method not in Clustering.name_to_class and not op.exists(method):
909 help_and_die(
910 parser,
911 'no such clustering method: %s' % method if method else
912 'no clustering method specified')
914 if op.exists(method):
915 clustering = read_config(method)
916 else:
917 clustering = Clustering.cli_instantiate(method, options)
919 if options.write_config:
920 write_config(clustering, options.write_config)
921 else:
922 if len(args) != 1:
923 help_and_die(parser, 'no rundir')
924 run_path, = args
926 grond.cluster(run_path, clustering, metric=options.metric)
928 except grond.GrondError as e:
929 die(str(e))
932def command_plot(args):
934 def setup(parser):
935 parser.add_option(
936 '--show', dest='show', action='store_true',
937 help='show plot for interactive inspection')
939 details = ''
941 parser, options, args = cl_parse('plot', args, setup, details)
943 if not options.show:
944 import matplotlib
945 matplotlib.use('Agg')
947 from grond.environment import Environment
949 if len(args) not in (1, 2, 3):
950 help_and_die(parser, '1, 2 or 3 arguments required')
952 if len(args) > 1:
953 env = Environment(args[1:])
954 else:
955 env = None
957 from grond import plot
958 if args[0] == 'list':
960 def get_doc_title(doc):
961 for ln in doc.split('\n'):
962 ln = ln.strip()
963 if ln != '':
964 ln = ln.strip('.')
965 return ln
966 return 'Undocumented.'
968 if env:
969 plot_classes = env.get_plot_classes()
970 else:
971 plot_classes = plot.get_all_plot_classes()
973 plot_names, plot_doc = zip(*[(pc.name, pc.__doc__)
974 for pc in plot_classes])
976 plot_descs = [get_doc_title(doc) for doc in plot_doc]
977 left_spaces = max([len(pn) for pn in plot_names])
979 for name, desc in zip(plot_names, plot_descs):
980 print('{name:<{ls}} - {desc}'.format(
981 ls=left_spaces, name=name, desc=desc))
983 elif args[0] == 'config':
984 plot_config_collection = plot.get_plot_config_collection(env)
985 print(plot_config_collection)
987 elif args[0] == 'all':
988 if env is None:
989 help_and_die(parser, 'two or three arguments required')
990 plot_names = plot.get_plot_names(env)
991 plot.make_plots(env, plot_names=plot_names, show=options.show)
993 elif op.exists(args[0]):
994 if env is None:
995 help_and_die(parser, 'two or three arguments required')
996 plots = plot.PlotConfigCollection.load(args[0])
997 plot.make_plots(env, plots, show=options.show)
999 else:
1000 if env is None:
1001 help_and_die(parser, 'two or three arguments required')
1002 plot_names = [name.strip() for name in args[0].split(',')]
1003 plot.make_plots(env, plot_names=plot_names, show=options.show)
1006def command_movie(args):
1008 import matplotlib
1009 matplotlib.use('Agg')
1011 def setup(parser):
1012 pass
1014 parser, options, args = cl_parse('movie', args, setup)
1016 if len(args) != 4:
1017 help_and_die(parser, 'four arguments required')
1019 run_path, xpar_name, ypar_name, movie_filename_template = args
1021 from grond import plot
1023 movie_filename = movie_filename_template % {
1024 'xpar': xpar_name,
1025 'ypar': ypar_name}
1027 try:
1028 plot.make_movie(run_path, xpar_name, ypar_name, movie_filename)
1030 except grond.GrondError as e:
1031 die(str(e))
1034def command_export(args):
1036 def setup(parser):
1037 parser.add_option(
1038 '--type', dest='type', metavar='TYPE',
1039 choices=('event', 'event-yaml', 'source', 'vector'),
1040 help='select type of objects to be exported. Choices: '
1041 '"event" (default), "event-yaml", "source", "vector".')
1043 parser.add_option(
1044 '--parameters', dest='parameters', metavar='PLIST',
1045 help='select parameters to be exported. PLIST is a '
1046 'comma-separated list where each entry has the form '
1047 '"<parameter>[.<measure>]". Available measures: "best", '
1048 '"mean", "std", "minimum", "percentile16", "median", '
1049 '"percentile84", "maximum".')
1051 parser.add_option(
1052 '--selection', dest='selection', metavar='EXPRESSION',
1053 help='only export data for runs which match EXPRESSION. '
1054 'Example expression: "tags_contains:excellent,good"')
1056 parser.add_option(
1057 '--output', dest='filename', metavar='FILE',
1058 help='write output to FILE')
1060 parser.add_option(
1061 '--effective-lat-lon', dest='effective_lat_lon',
1062 action='store_true',
1063 help='convert north_shift/east_shift offsets to true lat/lon '
1064 'coordinates (when outputting event objects).')
1066 parser, options, args = cl_parse('export', args, setup)
1067 if len(args) < 2:
1068 help_and_die(parser, 'arguments required')
1070 what = args[0]
1072 dirnames = args[1:]
1074 what_choices = ('best', 'mean', 'ensemble', 'stats')
1076 if what not in what_choices:
1077 help_and_die(
1078 parser,
1079 'invalid choice: %s (choose from %s)' % (
1080 repr(what), ', '.join(repr(x) for x in what_choices)))
1082 if options.parameters:
1083 pnames = options.parameters.split(',')
1084 else:
1085 pnames = None
1087 try:
1088 grond.export(
1089 what,
1090 dirnames,
1091 filename=options.filename,
1092 type=options.type,
1093 pnames=pnames,
1094 selection=options.selection,
1095 effective_lat_lon=options.effective_lat_lon)
1097 except grond.GrondError as e:
1098 die(str(e))
1101def command_tag(args):
1103 def setup(parser):
1104 parser.add_option(
1105 '-d', '--dir-names',
1106 dest='show_dirnames',
1107 action='store_true',
1108 help='show directory names instead of run names')
1110 parser, options, args = cl_parse('tag', args, setup)
1111 if len(args) < 2:
1112 help_and_die(parser, 'two or more arguments required')
1114 action = args.pop(0)
1116 if action not in ('add', 'remove', 'list'):
1117 help_and_die(parser, 'invalid action: %s' % action)
1119 if action in ('add', 'remove'):
1120 if len(args) < 2:
1121 help_and_die(parser, 'three or more arguments required')
1123 tag = args.pop(0)
1125 rundirs = args
1127 if action == 'list':
1128 rundirs = args
1130 from grond.environment import Environment
1132 errors = False
1133 for rundir in rundirs:
1134 try:
1135 env = Environment([rundir])
1136 if options.show_dirnames:
1137 name = rundir
1138 else:
1139 name = env.get_problem().name
1141 info = env.get_run_info()
1142 if action == 'add':
1143 info.add_tag(tag)
1144 env.set_run_info(info)
1145 elif action == 'remove':
1146 info.remove_tag(tag)
1147 env.set_run_info(info)
1148 elif action == 'list':
1149 print('%-60s : %s' % (
1150 name,
1151 ', '.join(info.tags)))
1153 except grond.GrondError as e:
1154 errors = True
1155 logger.error(e)
1157 if errors:
1158 die('Errors occurred, see log messages above.')
1161def make_report(env_args, event_name, conf, update_without_plotting, nthreads):
1162 from grond.environment import Environment
1163 from grond.report import report
1164 try:
1165 env = Environment(env_args)
1166 if event_name:
1167 env.set_current_event_name(event_name)
1169 report(
1170 env, conf,
1171 update_without_plotting=update_without_plotting,
1172 make_index=False,
1173 make_archive=False,
1174 nthreads=nthreads)
1176 return True
1178 except grond.GrondError as e:
1179 logger.error(str(e))
1180 return False
1183def command_report(args):
1185 import matplotlib
1186 matplotlib.use('Agg')
1188 from pyrocko import parimap
1190 from grond.environment import Environment
1191 from grond.report import \
1192 report_index, report_archive, serve_ip, serve_report, read_config, \
1193 write_config, ReportConfig
1195 def setup(parser):
1196 parser.add_option(
1197 '--index-only',
1198 dest='index_only',
1199 action='store_true',
1200 help='create index only')
1201 parser.add_option(
1202 '--serve', '-s',
1203 dest='serve',
1204 action='store_true',
1205 help='start http service')
1206 parser.add_option(
1207 '--serve-external', '-S',
1208 dest='serve_external',
1209 action='store_true',
1210 help='shortcut for --serve --host=default --fixed-port')
1211 parser.add_option(
1212 '--host',
1213 dest='host',
1214 default='localhost',
1215 help='<ip> to start the http server on. Special values for '
1216 '<ip>: "*" binds to all available interfaces, "default" '
1217 'to default external interface, "localhost" to "127.0.0.1".')
1218 parser.add_option(
1219 '--port',
1220 dest='port',
1221 type=int,
1222 default=8383,
1223 help='set default http server port. Will count up if port is '
1224 'already in use unless --fixed-port is given.')
1225 parser.add_option(
1226 '--fixed-port',
1227 dest='fixed_port',
1228 action='store_true',
1229 help='fail if port is already in use')
1230 parser.add_option(
1231 '--open', '-o',
1232 dest='open',
1233 action='store_true',
1234 help='open report in browser')
1235 parser.add_option(
1236 '--config',
1237 dest='config',
1238 metavar='FILE',
1239 help='report configuration file to use')
1240 parser.add_option(
1241 '--write-config',
1242 dest='write_config',
1243 metavar='FILE',
1244 help='write configuration (or default configuration) to FILE')
1245 parser.add_option(
1246 '--update-without-plotting',
1247 dest='update_without_plotting',
1248 action='store_true',
1249 help='quick-and-dirty update parameter files without plotting')
1250 parser.add_option(
1251 '--parallel', dest='nparallel', type=int, default=1,
1252 help='set number of runs to process in parallel, '
1253 'If set to more than one, --status=quiet is implied.')
1254 parser.add_option(
1255 '--threads', dest='nthreads', type=int, default=1,
1256 help='set number of threads per process (default: 1).'
1257 'Set to 0 to use all available cores.')
1258 parser.add_option(
1259 '--no-archive',
1260 dest='no_archive',
1261 action='store_true',
1262 help='don\'t create archive file.')
1264 parser, options, args = cl_parse('report', args, setup)
1266 s_conf = ''
1267 if options.config:
1268 try:
1269 conf = read_config(options.config)
1270 except grond.GrondError as e:
1271 die(str(e))
1273 s_conf = ' --config="%s"' % options.config
1274 else:
1275 from grond import plot
1276 conf = ReportConfig(
1277 plot_config_collection=plot.get_plot_config_collection())
1278 conf.set_basepath('.')
1280 if options.write_config:
1281 try:
1282 write_config(conf, options.write_config)
1283 sys.exit(0)
1285 except grond.GrondError as e:
1286 die(str(e))
1288 # commandline options that can override config values
1289 if options.no_archive:
1290 conf.make_archive = False
1292 if len(args) == 1 and op.exists(op.join(args[0], 'index.html')):
1293 conf.report_base_path = conf.rel_path(args[0])
1294 s_conf = ' %s' % args[0]
1295 args = []
1297 report_base_path = conf.expand_path(conf.report_base_path)
1299 if options.index_only:
1300 report_index(conf)
1301 report_archive(conf)
1302 args = []
1304 entries_generated = False
1306 payload = []
1307 if args and all(op.isdir(rundir) for rundir in args):
1308 rundirs = args
1309 all_failed = True
1310 for rundir in rundirs:
1311 payload.append((
1312 [rundir], None, conf, options.update_without_plotting,
1313 options.nthreads))
1315 elif args:
1316 try:
1317 env = Environment(args)
1318 for event_name in env.get_selected_event_names():
1319 payload.append((
1320 args, event_name, conf, options.update_without_plotting,
1321 options.nthreads))
1323 except grond.GrondError as e:
1324 die(str(e))
1326 if payload:
1327 entries_generated = []
1328 for result in parimap.parimap(
1329 make_report, *zip(*payload), nprocs=options.nparallel):
1331 entries_generated.append(result)
1333 all_failed = not any(entries_generated)
1334 entries_generated = any(entries_generated)
1336 if all_failed:
1337 die('no report entries generated')
1339 report_index(conf)
1340 report_archive(conf)
1342 if options.serve or options.serve_external:
1343 if options.serve_external:
1344 host = 'default'
1345 else:
1346 host = options.host
1348 addr = serve_ip(host), options.port
1350 serve_report(
1351 addr,
1352 report_config=conf,
1353 fixed_port=options.fixed_port or options.serve_external,
1354 open=options.open)
1356 elif options.open:
1357 import webbrowser
1358 url = 'file://%s/index.html' % op.abspath(report_base_path)
1359 webbrowser.open(url)
1361 else:
1362 if not entries_generated and not options.index_only:
1363 logger.info('Nothing to do, see: grond report --help')
1365 if entries_generated and not (options.serve or options.serve_external):
1366 logger.info(CLIHints('report', config=s_conf))
1369def command_qc_polarization(args):
1371 def setup(parser):
1372 parser.add_option(
1373 '--time-factor-pre', dest='time_factor_pre', type=float,
1374 metavar='NUMBER',
1375 default=0.5,
1376 help='set duration to extract before synthetic P phase arrival, '
1377 'relative to 1/fmin. fmin is taken from the selected target '
1378 'group in the config file (default=%default)')
1379 parser.add_option(
1380 '--time-factor-post', dest='time_factor_post', type=float,
1381 metavar='NUMBER',
1382 default=0.5,
1383 help='set duration to extract after synthetic P phase arrival, '
1384 'relative to 1/fmin. fmin is taken from the selected target '
1385 'group in the config file (default=%default)')
1386 parser.add_option(
1387 '--distance-min', dest='distance_min', type=float,
1388 metavar='NUMBER',
1389 help='minimum event-station distance [m]')
1390 parser.add_option(
1391 '--distance-max', dest='distance_max', type=float,
1392 metavar='NUMBER',
1393 help='maximum event-station distance [m]')
1394 parser.add_option(
1395 '--depth-min', dest='depth_min', type=float,
1396 metavar='NUMBER',
1397 help='minimum station depth [m]')
1398 parser.add_option(
1399 '--depth-max', dest='depth_max', type=float,
1400 metavar='NUMBER',
1401 help='maximum station depth [m]')
1402 parser.add_option(
1403 '--picks', dest='picks_filename',
1404 metavar='FILENAME',
1405 help='add file with P picks in Snuffler marker format')
1406 parser.add_option(
1407 '--save', dest='output_filename',
1408 metavar='FILENAME.FORMAT',
1409 help='save output to file FILENAME.FORMAT')
1410 parser.add_option(
1411 '--dpi', dest='output_dpi', type=float, default=120.,
1412 metavar='NUMBER',
1413 help='DPI setting for raster formats (default=120)')
1415 parser, options, args = cl_parse('qc-polarization', args, setup)
1416 if len(args) != 3:
1417 help_and_die(parser, 'missing arguments')
1419 if options.output_filename:
1420 import matplotlib
1421 matplotlib.use('Agg')
1423 import grond.qc
1425 config_path, event_name, target_group_path = args
1427 try:
1428 config = grond.read_config(config_path)
1429 except grond.GrondError as e:
1430 die(str(e))
1432 ds = config.get_dataset(event_name)
1434 engine = config.engine_config.get_engine()
1436 nsl_to_time = None
1437 if options.picks_filename:
1438 markers = marker.load_markers(options.picks_filename)
1439 marker.associate_phases_to_events(markers)
1441 nsl_to_time = {}
1442 for m in markers:
1443 if isinstance(m, marker.PhaseMarker):
1444 ev = m.get_event()
1445 if ev is not None and ev.name == event_name:
1446 nsl_to_time[m.one_nslc()[:3]] = m.tmin
1448 if not nsl_to_time:
1449 help_and_die(
1450 parser,
1451 'no markers associated with event "%s" found in file "%s"' % (
1452 event_name, options.picks_filename))
1454 target_group_paths_avail = []
1455 for target_group in config.target_groups:
1456 name = target_group.path
1457 if name == target_group_path:
1458 imc = target_group.misfit_config
1459 fmin = imc.fmin
1460 fmax = imc.fmax
1461 ffactor = imc.ffactor
1463 store = engine.get_store(target_group.store_id)
1464 timing = '{cake:P|cake:p|cake:P\\|cake:p\\}'
1466 grond.qc.polarization(
1467 ds, store, timing, fmin=fmin, fmax=fmax, ffactor=ffactor,
1468 time_factor_pre=options.time_factor_pre,
1469 time_factor_post=options.time_factor_post,
1470 distance_min=options.distance_min,
1471 distance_max=options.distance_max,
1472 depth_min=options.depth_min,
1473 depth_max=options.depth_max,
1474 nsl_to_time=nsl_to_time,
1475 output_filename=options.output_filename,
1476 output_dpi=options.output_dpi)
1478 return
1480 target_group_paths_avail.append(name)
1482 die('no target group with path "%s" found. Available: %s' % (
1483 target_group_path, ', '.join(target_group_paths_avail)))
1486def command_upgrade_config(args):
1487 def setup(parser):
1488 parser.add_option(
1489 '--diff', dest='diff', action='store_true',
1490 help='create diff between normalized old and new versions')
1492 parser, options, args = cl_parse('upgrade-config', args, setup)
1493 if len(args) != 1:
1494 help_and_die(parser, 'missing argument <configfile>')
1496 from grond import upgrade
1497 upgrade.upgrade_config_file(args[0], diff=options.diff)
1500def command_diff(args):
1501 def setup(parser):
1502 pass
1504 parser, options, args = cl_parse('diff', args, setup)
1505 if len(args) != 2:
1506 help_and_die(parser, 'requires exactly two arguments')
1508 from grond.config import diff_configs
1509 diff_configs(*args)
1512def command_version(args):
1513 def setup(parser):
1514 parser.add_option(
1515 '--short', dest='short', action='store_true',
1516 help='only print Grond\'s version number')
1517 parser.add_option(
1518 '--failsafe', dest='failsafe', action='store_true',
1519 help='do not get irritated when some dependencies are missing')
1521 parser, options, args = cl_parse('version', args, setup)
1523 if options.short:
1524 print(grond.__version__)
1525 return
1527 elif not options.failsafe:
1528 from grond import info
1529 print(info.version_info())
1530 return
1532 print("grond: %s" % grond.__version__)
1534 try:
1535 import pyrocko
1536 print('pyrocko: %s' % pyrocko.long_version)
1537 except ImportError:
1538 print('pyrocko: N/A')
1540 try:
1541 import numpy
1542 print('numpy: %s' % numpy.__version__)
1543 except ImportError:
1544 print('numpy: N/A')
1546 try:
1547 import scipy
1548 print('scipy: %s' % scipy.__version__)
1549 except ImportError:
1550 print('scipy: N/A')
1552 try:
1553 import matplotlib
1554 print('matplotlib: %s' % matplotlib.__version__)
1555 except ImportError:
1556 print('matplotlib: N/A')
1558 try:
1559 from pyrocko.gui.qt_compat import Qt
1560 print('PyQt: %s' % Qt.PYQT_VERSION_STR)
1561 print('Qt: %s' % Qt.QT_VERSION_STR)
1562 except ImportError:
1563 print('PyQt: N/A')
1564 print('Qt: N/A')
1566 import sys
1567 print('python: %s.%s.%s' % sys.version_info[:3])
1569 if not options.failsafe:
1570 die('fell back to failsafe version printing')
1573if __name__ == '__main__':
1574 main()